TUTORIALS - ModelMD3
Abstract
This tutorial explains how to set up a basic environment with an animated MD3 model. Instructions to prepare a standard MD3 model for loading are covered as well some tips to understand better the syntax of the LUA language.
Index
- Comments to the ModelMD3 source
- Preparing MD3 models for loading
1. Comments to the ModelMD3 source
This tutorial refers to the ModelMD3.lua sources included in the file DemoPack0-lua-0.8.0.zip available for download at the Downloads page. It's better to have a copy of the source for easy reference and the HTML manual with the list of APOCALYX API because it's too long to describe here the meaning of all the parameters of the functions.
Now let's read the source line by line and comment it.
----MD3 MODEL LOADING Tutorial
----A loader of "pure" MD3 models
----Questions? Contact: Leonardo Boselli <boselli@uno.it>
--[[
In LUA comments are prepended by a couple of hyphens "-". Multiple comment lines must be included between "--[[" and
"--]]". So the lines listed above are ignored by the interpreter and these multi-line comments are ignored too.
--]]
----INITIALIZATION----
--[[
LUA scripts written for APOCALYX usually contain four main functions that control the behaviour of a single scene. They are named as follows:
- init() is called when the engine initilizes the scene
- update() is called once for every frame
- final() is called when the engine finalizes (drops) the scene
- keyDown() is called when a keyboard key is pressed
The code that follows is included in init(), so it specifies all the actions to be performed during initialization.
--]]
function init()
----CAMERA----
setAmbient(.5,.5,.5)
setPerspective(60,.25,1000)
enableFog(200, .522,.373,.298)
local camera = getCamera()
camera:reset()
camera:setPosition(0,1.8,-3)
empty()
--[[
The calls listed above have the following meaning. In order:
- a color for the ambient light is set (half red, half green, half blue = half gray)
- the perspective of the camera has an aperture size of 60 degrees, a near clipping plane of 0.25 (meters, if you want) and a far clipping plane of 1000 (always meters, if this is your length unit of choice).
- then the main camera object is put in the camera local variable (in LUA, variables are specified local if they must be dropped when out of context - in this case out of the init() function - otherwise they are global in scope and accessible from every other function in this module)
- the camera object is reset using the reset() function (note the ":" that specifies that the reset() function is applied to the camera object. In C++ or Java the "." has the same use, but in LUA the meaning of the full stop is different and explained elsewhere) and positioned 1.8 meters up and a 3 meters backward from the origin (in APOCALYX the X axis points to the left, the Y axis points up and the Z axis points forward).
- finally the function empty() is called to remove all the objects from the world - objects possibly added to the world by a previous initialization.
--]]
if not fileExists("DemoPack0.dat") then
showConsole()
error("\nERROR: File 'DemoPack0.dat' not found.")
end
local zip = Zip("DemoPack0.dat")
--[[
Now it's time to load the resources of the demo. First of all, it's better to verify if the package of the resources is available and display an error on the console if it lacks. Then the package is opened and a reference is stored in the local variable zip.
--]]
----SKYBOX----
local skyTxt = {
zip:getTexture("skyboxTop2.jpg"),
zip:getTexture("skyboxLeft2.jpg"),
zip:getTexture("skyboxFront2.jpg"),
zip:getTexture("skyboxRight2.jpg"),
zip:getTexture("skyboxBack2.jpg")
}
local skyBackground = MirroredSky(skyTxt)
skyBackground:rotStanding(3.1415)
setBackground(skyBackground)
--[[
The first resource we are going to load is the skybox. The skybox is a simple cube that surrounds the camera with several textures applied that simulate a sky with clouds. The images are created in a particular way so they warp on the cube and the observer does not realize to watch a simple cube instead of a far realistic horizon.
To create the images for the skybox one can use Terragen, a beautiful program that generates realistic landscapes (registration is required for commercial purposes). In practice, one must take five shots of the landscape generated by Terragen with an aperture of 45 degrees. Then the five images are loaded by the engine to create the skybox.
In this case the skybox is mirrored to simulate reflections on a planar ground. The rotStanding of 180 degrees (angle specified in radiants in the source) is necessary for the particular images chosen in the demo to make the sun appear on the back of the camera. Finally the skybox is added to the world with setBackground().
Note that skyTxt is a list of textures passed to MirroredSky(). Lists are called tables in LUA and are more flexible than C arrays.
--]]
----SUN----
sun = Sun(
zip:getTexture("light.jpg"),0.25,
0.0, 0.2588, -0.9659,
zip:getTexture("lensflares.png"),
5, 0.1
)
sun:setColor(0.855,0.475,0.298);
setSun(sun)
--[[
Now let's add a sun with its lens flare. Simply create a Sun() object and add it to the world with setSun(). The parameters specified are in order: the texture of the corona, the size of the corona, the view direction from which the camera sees the sun (3 coordinates), the texture of the lens flares (four images per texture), the number of lens flares and, finally, their size. A color for the sun is also specified (a bit reddish).
--]]
----TERRAIN----
local terrainMaterial = Material()
terrainMaterial:setAmbient(1,1,1)
terrainMaterial:setDiffuse(0,0,0)
terrainMaterial:setDiffuseTexture(zip:getTexture("marble.jpg",1))
local ground = FlatTerrain(terrainMaterial,500,125)
ground:setReflective()
ground:setShadowed()
ground:setShadowOffset(0.01)
setTerrain(ground)
--[[
After the sky and the sun, it's the turn of the ground to put the feet of the model on. This time we use a simple flat ground.
First we create the material to be attached to the ground. Materials are built up from several types of colors and textures. In this case full ambient and no diffuse color. The texture is taken from the marble.jpg image and it is going to be tiled (that's the meaning of the 1 as second argument).
Then the material is used to create the FlatTerrain. The size is 500 meters and the texture is tiled on it 125 times. The terrain will be reflective and shadowed. The shadow offset is the distance of the shadow from the ground to avoid the bad artifacts of Z fighting.
Finally the ground is added to the world with setTerrain().
--]]
----MODELS----
weapon = zip:getModel("gun.md3","gun.jpg")
weapon:rescale(0.04)
--[[
Now it's the time of the models. First we load the gun of the warrior. The MD3 is the model while the JPG is the texture applied to it. The rescale() is necessary because the length unit of the model is to large for our environment.
--]]
torso = 11 ---> TORSO_STAND
legs = 15 ---> LEGS_IDLE
avatar = zip:getBot("warrior.mdl")
avatar:rescale(0.04)
avatar:pitch(-1.5708)
avatar:rotStanding(1.5708)
avatar:move(0,1,0)
avatar:getUpper():link("tag_weapon",weapon)
avatar:setUpperAnimation(torso)
avatar:setLowerAnimation(legs)
addObject(avatar)
--[[
The lines listed above load the real warrior. The calls to pitch() and rotStanding() are necessary because the axis of APOCALYX differ from those of MD3 models. The move() raises the model so it touch the ground with its feet.
Then we must link the weapon to the upper section of the bot (the place to attach the weapon is marked by the string tag_weapon) and finally we specify the indexes of the starting animations for the upper and lower sections of the bot (both are idle attitudes at the beginning). The addObject() function adds the object to the world.
--]]
local shadow = Shadow(avatar)
addShadow(shadow)
--[[
Why not to add a shadow? The two lines above apply a planar shadow to the object.
The initialization is almost done. The last steps are needed to define the text to be displayed as help for the user.
--]]
----HELP----
local help = {
"The model of this tutorial was made by:",
" ALPHAwolf ",
"The weapon of this tutorial was made by:",
" Janus ",
" ",
"[ MOUSE ] Look around",
"[ UP/DOWN ] Move Forward/Back",
"[PREV/NEXT] Raise/Lower View",
"[ Z key ] Change Torso Animation",
"[ X key ] Change Legs Animation",
"[ C key ] Death Animation",
"[ Q,W,E,R ] Rotate/Bend Head",
"[ A,S,D,F ] Rotate/Bend Torso",
"[ SPACE ] Rotate Scene",
" ",
"[ENTER] Main Menu",
"[F1] Show/Hide Help",
}
setHelp(help)
showHelpReduced()
hideConsole()
--[[
Note that the text is passed to setHelp() as a table of strings.
--]]
----DELETE ZIP----
zip:delete()
end
--[[
The initialization is finally done. The zip must be deleted so it releases some memory resources. The final end closes the init() function.
Now let's consider the update() function. It is called once per frame, so it manages the evolution of the world initialized by init().
The first lines move and rotate the bot according to the index of the legs animation, so it can run or walk on the ground realistically.
--]]
----LOOP----
function update()
local camera = getCamera()
local timeStep = getTimeStep()
local fwdSpeed = 0
local rotSpeed = 0
if legs == 6 then ---> LEGS_WALKCR
fwdSpeed = 2.5
rotSpeed = 0.31415
elseif legs == 7 then ---> LEGS_WALK
fwdSpeed = 2.5
rotSpeed = 0.6283
elseif legs == 8 then ---> LEGS_RUN
fwdSpeed = 5
rotSpeed = 0.6283
elseif legs == 9 then ---> LEGS_BACK
fwdSpeed = -3.5
rotSpeed = 0.31415
elseif legs == 10 then ---> LEGS_SWIM
fwdSpeed = 2.5
rotSpeed = 0.31415
elseif legs == 17 then ---> LEGS_TURN
rotSpeed = 1.5708
end
avatar:walk(fwdSpeed*timeStep);
avatar:rotStanding(rotSpeed*timeStep);
--[[
The function walk() moves the bot forward to the specified distance, while rotStanding() rotates it around its vertical axis. Note that the local variable timeStep contains the elapsed time from the last rendering. The use of speeds (for forward movement and rotations) to specify motion is the best choice to achieve frame rate independence.
The following lines instead are needed to complete actions broken in more animations.
--]]
local stopped = avatar:getLower():getStoppedAnimation()
if stopped == 11 then ---> LEGS_JUMP
avatar:setLowerAnimation(12) ---> LEGS_LAND
legs = 12
elseif stopped == 13 then ---> LEGS_JUMPB
avatar:setLowerAnimation(14) ---> LEGS_LANDB
legs = 14
end
stopped = avatar:getUpper():getStoppedAnimation()
if stopped >= 6 then ---> TORSO_GESTURE
avatar:setUpperAnimation(11) ---> TORSO_STAND
end
--[[
When an animation ends, the getStoppedAnimation() function returns its index, so the programmer can start the animation that should follow. For example, after a jump, the bot must land.
The automatic bot movements are now completely defined. It's the turn of the camera.
--]]
----ROTATE VIEW----
if rotateView then
local rotAngle = .13*timeStep
camera:rotAround(rotAngle)
local posX,posY,posZ = avatar:getPosition()
camera:pointTo(posX,posY+1,posZ)
end
--[[
The lines above control the movement of the camera when automatic rotation about the origin is requested. The function pointTo points the camera to the specified point, while rotAround rotates it around the origin.
The user can control the camera using some keyboard keys.
--]]
----MOVE CAMERA (KEYBOARD)----
local moveSpeed = 15
local climbSpeed = 15
if isKeyPressed(38) then --> VK_UP
camera:moveStanding(moveSpeed*timeStep)
elseif isKeyPressed(40) then --> VK_DOWN
camera:moveStanding(-moveSpeed*timeStep)
elseif isKeyPressed(37) then --> VK_LEFT
camera:rotStanding(0.4*timeStep)
elseif isKeyPressed(39) then --> VK_RIGHT
camera:rotStanding(-0.4*timeStep)
elseif isKeyPressed(33) then --> VK_PRIOR
camera:move(0,climbSpeed*timeStep,0)
elseif isKeyPressed(34) then --> VK_NEXT
camera:move(0,-climbSpeed*timeStep,0)
local posX,posY,posZ = camera:getPosition()
if posY < 0.5 then
camera:setPosition(posX,0.5,posZ)
end
end
--[[
The lines above apply a movement to the camera according to the pressed key. Again speeds combined with the duration of the timeStep guarantee frame rate independence.
A similar structure permits the control of the bot's sections bends and rotations as follows.
--]]
----MODEL MOVEMENT----
if isKeyPressed(string.byte("D")) then
local angle = 3.1415*timeStep
avatar:getUpper():addPitchAngle(angle,0.7854,-0.5236)
elseif isKeyPressed(string.byte("F")) then
local angle = -3.1415*timeStep
avatar:getUpper():addPitchAngle(angle,0.7854,-0.5236)
elseif isKeyPressed(string.byte("A")) then
local angle = 3.1415*timeStep
avatar:getUpper():addYawAngle(angle,1.5708)
elseif isKeyPressed(string.byte("S")) then
local angle = -3.1415*timeStep
avatar:getUpper():addYawAngle(angle,1.5708)
elseif isKeyPressed(string.byte("E")) then
local angle = 3.1415*timeStep
avatar:getHead():addPitchAngle(angle,0.7854)
elseif isKeyPressed(string.byte("R")) then
local angle = -3.1415*timeStep
avatar:getHead():addPitchAngle(angle,0.7854)
elseif isKeyPressed(string.byte("Q")) then
local angle = 3.1415*timeStep
avatar:getHead():addYawAngle(angle,1.5708)
elseif isKeyPressed(string.byte("W")) then
local angle = -3.1415*timeStep
avatar:getHead():addYawAngle(angle,1.5708)
end
--[[
The yaws usually represent a rotation around the vertical axis, while pitch a rotation around a horizontal axis.
Finally we want to control the camera with the mouse too.
--]]
----MOVE CAMERA (MOUSE)----
if not rotateView then
local dx, dy = getMouseMove()
local changeStep = 0.15*timeStep;
if dx ~= 0 then
camera:rotStanding(-dx*changeStep)
end
if dy ~= 0 then
camera:pitch(-dy*changeStep)
end
end
end
--[[
The function getMouseMove() returns the movement of the mouse and the orientation of the camera follows it.
Another important function, update(), is finally closed. Let's consider the last two.
The final() function deletes all the objects and clears the memory. It's called when a scene is changed and all the initialized objects must be finalized.
--]]
----FINALIZATION----
function final()
----DELETE GLOBALS----
rotateView = nil
torso = nil
legs = nil
----EMPTY WORLD----
sun = nil
avatar = nil
if weapon then
weapon:delete()
weapon = nil
end
disableFog()
empty()
end
--[[
As you can see, a lot of global variables defined in init() (they were global by default because they were not defined local) are cleared. In LUA a variable is removed from memoty when its content becomes nil. Other objects needs to be specifically deleted with delete(): In this case only the weapon object because it was linked to the bot but not added to the world. The objects that were added to the world are deleted automatically with a call to empty().
The last function is keyDown(). It manages the pressed keys and performs several actions.
--]]
----KEYBOARD----
function keyDown(key)
if key == 32 then --> SPACE
releaseKey(32)
if rotateView then
rotateView = nil
else
rotateView = 1
end
end
--[[
When the space (ascii code 32) is pressed, the automatic rotation of the camera begins or ends according to the current state.
--]]
----VARIOUS SCENE MODIFIERS----
if key == string.byte("Z") then
releaseKey(string.byte("Z"))
if(torso >= 6) then ---> TORSO_GESTURE
torso = torso+1
else
legs = 15 ---> LEGS_IDLE
avatar:setLowerAnimation(legs)
torso = 11 ---> TORSO_STAND
end
if torso >= 13 then ---> MAX_TORSO_ANIMATIONS
torso = 6 ---> TORSO_GESTURE
end
avatar:setUpperAnimation(torso)
--[[
When Z is pressed, a torso animation is chosen.
--]]
elseif key == string.byte("X") then
releaseKey(string.byte("X"))
if(legs >= 6) then ---> LEGS_WALKCR
legs = legs+1
else
torso = 11 ---> TORSO_STAND
avatar:setUpperAnimation(torso)
legs = 15 ---> LEGS_IDLE
end
if legs >= 18 then ---> MAX_LEGS_ANIMATIONS
legs = 6 ---> LEGS_WALKCR
end
avatar:setLowerAnimation(legs)
--[[
When X is pressed, a legs animation is chosen.
--]]
elseif key == string.byte("C") then
releaseKey(string.byte("C"))
if legs < 4 then ---> LEGS_DEAD3
torso = torso+2
legs = legs+2
else
torso = 0 ---> BOTH_DEATH1
legs = 0 ---> BOTH_DEATH1
end
avatar:setLowerAnimation(legs)
avatar:setUpperAnimation(torso)
end
--[[
When C is pressed, a death animation is chosen.
--]]
----LOAD MAIN MENU----
if key == 13 then
releaseKey(13)
if fileExists("main.lua") then
final()
dofile("main.lua")
end
end
end
--[[
Finally, when "enter" is pressed (ascii code 13), the main.lua script is executed.
This ends the description of the main functions necessary to the engine. The lines above define the four functions, but the engine does not know yet their meaning, so the following line is very important.
--]]
----SCENE SETUP----
setScene(Scene(init,update,final,keyDown))
When it is executed, a Scene() object is created and specified as the current scene with setScene(). The arguments are those four functions described above. This means that, if you pass the functions in the correct order, you can choose the names that you prefer.
After the scene is defined, the engine begins to execute the init() function to initialize the world and then the update() once per frame. When a key is pressed, keyDown() is executed and, when the scene is substituted by another one, the final() function is called before.
I hope that these explanations are enough for you to understand better how things work in APOCALYX, but feel free to ask for more information if anything is not clear.
2. Preparing MD3 models for loading
2.1 Introduction
This section explains how to prepare an MD3 model for loading in APOCALYX. MD3 is a very common Quake3 format and there is plenty of (almost) free models already available for download (visit for example http://www.polycount.com).
To introduce the argument, let's make some step backward. After downloading and unzipping the file DemoPack0-lua-0.8.0.zip, you get several files the most important of which for this tutorial are:
- ModelMD3.lua, that contains the code of the demo. It is a simple text file the contents of which were covered in the previous section.
- DemoPack0.dat, that includes the resources (images, models etc.) needed by the demos. It is a ZIP file the extension of which was renamed to prevent unexperienced users to unzip it.
If you open DemoPack0.dat with an unzip utility, you'll find a lot of files in it. The most important to follow this section are:
- warrior.mdl, that contains information about the model structure and the animation parameters;
- lower.md3, upper.md3, head.md3, that are the 3 pieces in which a Quake3 bot is broken;
- body.jpg, head.jpg, that are the images to be applied as textures to the model.
Now let's see how to prepare all these files starting from a bot modelled for Quake3 or one of its MODs (remember to check the license of the resources before using them in your products, in particular when commercial use is planned - for example, Quake3 resources can not be used without a license from ID Software).
2.2 Preparing the MD3 files
The files needed from a standard "*.pk3" file to reconstruct a bot are:
- lower.md3, upper.md3, head.md3 (or equivalent names) taken from the subdirectory of the model. They represent 3 different sections of the model: The legs, the torso and the head, of course. Remember that even the "*.pk3" files are simple ZIP the extension of which was renamed, so you can read their contents and browse their subdirectories with an unzip utility.
- body.jpg, head.jpg (or equivalent names), that are the textures applied to the sections. APOCALYX supports only one texture per section.
- animation.cfg, that is a file that accompanies the model files and specifies all the animation data.
Once you get all these files you can simply zip the first two types in the resource file that the script will use. In the case of this demo that file is simply DemoPack0.dat. The third type instead (animation.cfg) must be renamed to "warrior.mdl" or anything else and edited as described here:
First of all, you must add a single line that follows this simple simple format:
lower.md3 body.jpg upper.md3 * head.md3 head.jpg
where "lower.md3" is the model for the legs, "body.jpg" is the texture to be applied to the legs, "upper.md3" is the model for the torso, "*" means that the torso uses the same texture of the legs (but you can specify whatever image you want), "head.md3" is the model for the head, "head.jpg" is the texture for the head (use another "*" if all the 3 pieces share the same texture).
Then you need to remove from warrior.mdl some lines (not useful for the engine) and keep only the ones that specify the animation frames. The text will look something like:
lower.md3 body.jpg upper.md3 * head.md3 head.jpg
0 30 0 25 // BOTH_DEATH1
29 1 0 25 // BOTH_DEAD1
30 30 0 25 // BOTH_DEATH2
... and so on.
Remember to leave not any blank line in the text otherwise the parser will complain.
Finally, this warrior.mdl must be zipped in the resource file with the already zipped models and textures.
2.3 MDX Format
If you don't need all the animations included in a Quake3 MD3 model, you may consider to use my MD3toMDX utility (download it from MD3toMDX_0.6.0_WIN-040114.zip).
This program converts the MD3 files that made up a complete model (legs, torso and head) and their associated "animation.cfg" file in a collection of 3 MDX files. MDX is a format used by APOCALYX to reduce the size of existent MD3 files stripping unnecessary animations for a particular application.
So MDX are simple MD3 from which some of the animations are removed. I want to keep the data of my demos as small as possible (to reduce the size of downloaded files), thus I remove from the MD3 files all the unnnecessary animations for the demos. This means that if you put a full MD3 version in place of a MDX, my demos don't work as inteded because the ordinal number of the animations changes. MDX includes even the animation data, so there is no need for an animation.cfg file.
To get an MDX you must:
- Get the file MD3toMDX.exe
- Put the 3 MD3 files (head, legs and torso) in the same folder of MD3toMDX.exe
- Put a file with extension ".mdl" in the same folder of MD3toMDX.exe
The last file must use the following format: the first line is the same as the one described in the previous section for warrior.mdl and the other lines are taken from "animation.cfg". The ".mdl" file must be edited to remove unwanted animations: When a line starts with an asterisk, the animation is removed from the model (in the following example, BOTH_DEATH1 and BOTH_DEAD1 are removed, while BOTH_DEATH2 is kept and so on)
lower.md3 lowerTxt.jpg upper.md3 * head.md3 headTxt.jpg
*0 30 0 25 // BOTH_DEATH1
*29 1 0 25 // BOTH_DEAD1
30 30 0 25 // BOTH_DEATH2
- Write at the command line:
MD3toMDX fileName.mdl
Three MDX files will appear in the folder (with names matching the previous MD3 files but smaller in size)
- To load MDX models using a script, the ".mdl" does not need any more the 25 lines that describe the animation data and you can remove them
I hope that these few lines are enough for you to understand the use of MD3toMdX.exe, but feel free to ask for more information if anything is not clear.