tge/tools/ms2dtsExporter/DTSMilkshapeShape.cpp
2017-04-17 06:17:10 -06:00

754 lines
28 KiB
C++
Executable File

#pragma warning ( disable: 4786 )
#include <windows.h>
#include <stack>
#include <string>
#include <vector>
#include <map>
#include <set>
#include <cmath>
#include <cassert>
#include "DTSShape.h"
#include "DTSBrushMesh.h"
#include "DTSMilkshapeMesh.h"
#include "DTSMilkshapeShape.h"
#include "msLib.h"
namespace DTS
{
// -----------------------------------------------------------------------
// Interpolation methods used to compute tween frames - phdana
void lerpTranslation(Point *out, Point &a, Point&b, float alpha)
{
out->x(a.x() + (b.x()-a.x()) * alpha);
out->y(a.y() + (b.y()-a.y()) * alpha);
out->z(a.z() + (b.z()-a.z()) * alpha);
}
void lerpTranslation(Point *out, Point &a, Point &b, int outFrame, int aFrame, int bFrame)
{
float alpha = (float)(outFrame - aFrame) / (float)(bFrame - aFrame);
lerpTranslation(out,a,b,alpha);
}
void lerpRotation(Quaternion *out, Quaternion &q1, Quaternion &q2, float alpha)
{
//-----------------------------------
// Calculate the cosine of the angle:
double cosOmega = q1.x() * q2.x() + q1.y() * q2.y() + q1.z() * q2.z() + q1.w() * q2.w();
//-----------------------------------
// adjust signs if necessary:
float sign2;
if ( cosOmega < 0.0 )
{
cosOmega = -cosOmega;
sign2 = -1.0f;
}
else
sign2 = 1.0f;
//-----------------------------------
// calculate interpolating coeffs:
double scale1, scale2;
if ( (1.0 - cosOmega) > 0.00001 )
{
// standard case
double omega = acos(cosOmega);
double sinOmega = sin(omega);
scale1 = sin((1.0 - alpha) * omega) / sinOmega;
scale2 = sign2 * sin(alpha * omega) / sinOmega;
}
else
{
// if quats are very close, just do linear interpolation
scale1 = 1.0 - alpha;
scale2 = sign2 * alpha;
}
//-----------------------------------
// actually do the interpolation:
out->x( float(scale1 * q1.x() + scale2 * q2.x()));
out->y( float(scale1 * q1.y() + scale2 * q2.y()));
out->z( float(scale1 * q1.z() + scale2 * q2.z()));
out->w( float(scale1 * q1.w() + scale2 * q2.w()));
}
void lerpRotation(Quaternion *out, Quaternion &a, Quaternion &b,
int outFrame, int aFrame, int bFrame)
{
float alpha = (float)(outFrame - aFrame) / (float)(bFrame - aFrame);
lerpRotation(out,a,b,alpha);
}
// -----------------------------------------------------------------------
int extractMeshFlags(char * name)
{
// Extract comma seperated flags embeded in the name. Anything after
// the ":" in the name is assumed to be flag arguments and stripped
// off the name.
int flags = 0;
char *ptr = strstr(name,":");
if (ptr)
{
*ptr = 0;
for (char *tok = strtok(ptr+1,","); tok != 0; tok = strtok(0,","))
{
// Strip lead/trailing white space
for (; isspace(*tok); tok++) ;
for (char* itr = tok + strlen(tok)-1; itr > tok && isspace(*itr); itr--)
*itr = 0;
//
if (!stricmp(tok,"billboard"))
flags |= Mesh::Billboard;
else
if (!stricmp(tok,"billboardz"))
flags |= Mesh::Billboard | Mesh::BillboardZ;
else
if (!stricmp(tok,"enormals"))
flags |= Mesh::EncodedNormals;
}
}
return flags;
}
void extractMaterialFlags(char * name, int& flags)
{
// Extract comma seperated flags embeded in the name. Anything after
// the ":" in the name is assumed to be flag arguments and stripped
// off the name.
char *ptr = strstr(name,":");
if (ptr)
{
*ptr = 0;
for (char *tok = strtok(ptr+1,","); tok != 0; tok = strtok(0,","))
{
// Strip lead/trailing white space
for (; isspace(*tok); tok++) ;
for (char* itr = tok + strlen(tok)-1; itr > tok && isspace(*itr); itr--)
*itr = 0;
if (!stricmp(tok,"add"))
flags |= Material::Additive;
else
if (!stricmp(tok,"sub"))
flags |= Material::Subtractive;
else
if (!stricmp(tok,"illum"))
flags |= Material::SelfIlluminating;
else
if (!stricmp(tok,"nomip"))
flags |= Material::NoMipMap;
else
if (!stricmp(tok,"mipzero"))
flags |= Material::MipMapZeroBorder;
}
}
}
// -----------------------------------------------------------------------
bool isBoneAnimated(msBone *bone, int start, int end)
{
// Returns true if the bone contains any key frames
// within the given time range.
int numPKeys = msBone_GetPositionKeyCount(bone);
for (int j = 0 ; j < numPKeys ; j++) {
msPositionKey * msKey = msBone_GetPositionKeyAt (bone, j);
// MS is one based, start/end 0 based..
if (msKey->fTime > start && msKey->fTime <= end)
return true;
}
int numRKeys = msBone_GetRotationKeyCount(bone);
for (int i = 0 ; i < numRKeys ; i++)
{
msRotationKey * msKey = msBone_GetRotationKeyAt (bone, i);
// MS is one based, start/end 0 based..
if (msKey->fTime > start && msKey->fTime <= end)
return true;
}
return false;
}
// -----------------------------------------------------------------------
// Imports a Milkshape Model
// -----------------------------------------------------------------------
MilkshapeShape::ImportConfig::ImportConfig ()
{
withMaterials = true ;
collisionType = C_None ;
collisionComplexity = 0.2f ;
collisionVisible = false ;
animation = true ;
reset();
}
void MilkshapeShape::ImportConfig::reset()
{
// Reset values that must be specified using opt: or seq:
scaleFactor = 0.1f ;
minimumSize = 0 ;
animationFPS = 15 ;
animationCyclic = false;
sequence.resize(0);
}
MilkshapeShape::MilkshapeShape (msModel * model, MilkshapeShape::ImportConfig config)
{
int i, j ;
char buffer[512] ;
int numBones = msModel_GetBoneCount(model) ;
int numMeshes = msModel_GetMeshCount(model) ;
int numMaterials = msModel_GetMaterialCount(model) ;
// --------------------------------------------------------
// Materials (optional)
// --------------------------------------------------------
if (config.withMaterials)
{
for (i = 0 ; i < numMaterials ; i++)
{
msMaterial * msMat = msModel_GetMaterialAt(model, i) ;
char textureName[MS_MAX_PATH+1] ;
// Check for "reserved" names first. Reserved names
// are special materials used to encode exporter options...
// a total hack, but what can you do?
msMaterial_GetName (msMat, textureName, MS_MAX_PATH);
if (!strncmp(textureName,"seq:",4) || !strncmp(textureName,"opt:",4))
continue;
// If no texture, use the material name
msMaterial_GetDiffuseTexture (msMat, textureName, MS_MAX_PATH) ;
if (!textureName[0])
{
msMaterial_GetName (msMat, textureName, MS_MAX_PATH) ;
// If no name, create one
if (!textureName[0])
strcpy (textureName, "Unnamed") ;
}
// Strip texture file extension
char * ptr = textureName + strlen(textureName) ;
while (ptr > textureName && *ptr != '.' && *ptr != '\\') ptr-- ;
if (*ptr == '.') *ptr = '\0' ;
// Strip texture file path
ptr = textureName + strlen(textureName) ;
while (ptr > textureName && *ptr != '\\') ptr-- ;
if (*ptr == '\\') memmove (textureName, ptr+1, strlen(ptr)+1) ;
// Create the material
Material mat ;
mat.name = textureName ;
mat.flags = Material::SWrap | Material::TWrap ;
mat.reflectance = materials.size() ;
mat.bump = -1 ;
mat.detail = -1 ;
mat.detailScale = 1.0f ;
// take the strength of the enviro mapping from the shininess
float shininess = msMaterial_GetShininess(msMat) / 128.0f;
if (shininess > 0.0f)
{
mat.reflection = shininess;
}
else
{
mat.flags |= Material::NeverEnvMap;
mat.reflection = 0.0f;
}
// look for tags in material name.. the tags are short because the max
// length of a material name is only 32 chars
char materialName[MS_MAX_PATH+1];
msMaterial_GetName (msMat, materialName, MS_MAX_PATH);
extractMaterialFlags(materialName, mat.flags);
// now check for transparency < 1.0
if (msMaterial_GetTransparency (msMat) < 1.0f)
mat.flags |= Material::Translucent;
materials.push_back(mat) ;
}
}
// --------------------------------------------------------
// Nodes & Bones
// --------------------------------------------------------
Node n ;
// Loop through first and import all the bones. I assume here
// that the parent joints always exist before their children.
// The Torque engine certainly expects nodes to be organized
// that way. Other parts of the code assume that Milkshape
// Joint index = Torque node index
assert(!nodes.size());
for (int bone_num = 0 ; bone_num < numBones ; bone_num++)
{
char bone_name[256];
char parent_name[256];
msBone * bone = msModel_GetBoneAt (model, bone_num) ;
msBone_GetName (bone, bone_name, 256) ;
msBone_GetParentName (bone, parent_name, 256);
// Create a node for this bone
n.parent = msModel_FindBoneByName (model, parent_name);
n.name = addName (bone_name) ;
nodes.push_back (n) ;
// Create the default position and rotation for the node
msVec3 pos ;
msVec3 rot ;
msBone_GetPosition (bone, pos) ;
msBone_GetRotation (bone, rot) ;
nodeDefTranslations.push_back(MilkshapePoint(pos) * config.scaleFactor) ;
nodeDefRotations.push_back(MilkshapeQuaternion(rot)) ;
}
// Add a default "root" node for shapes. All meshes need to
// be assigned to a node and the Root will be used to catch
// any mesh not assigned to a bone.
n.name = addName ("Root") ;
n.parent = -1;
nodes.push_back(n) ;
// Node indexes map one to one with the default rotation and
// tranlation arrays: push identity transform for the Root.
nodeDefRotations.push_back(Quaternion(0,0,0,1)) ;
nodeDefTranslations.push_back(Point(0,0,0)) ;
// --------------------------------------------------------
// Mesh objects.
// --------------------------------------------------------
Object o ;
for (i = 0 ; i < numMeshes ; i++)
{
msMesh * mesh = msModel_GetMeshAt(model, i) ;
msMesh_GetName (mesh, buffer, 510) ;
int flags = extractMeshFlags(buffer);
// Changed 01/16/2004 by Sven Knie
// Ignore meshes reserved for collision
if (!strnicmp(buffer,"Collision",9))
continue;
// added 01/16/2004 by Sven Knie
// Ignore meshes reserved for LoS collision
if (!strnicmp(buffer,"LoSCollision", 11))
continue;
// Get object struct ready. Objects are entities that
// represent renderable items. Objects can have more
// than one mesh to represent different detail levels.
o.name = addName(buffer) ;
o.numMeshes = 1 ;
o.firstMesh = meshes.size() ;
o.node = nodes.size() - 1;
// Process the raw data.
meshes.push_back(MilkshapeMesh(mesh, o.node, config.scaleFactor, config.withMaterials)) ;
Mesh& m = meshes.back();
m.setFlag(flags);
// Rigid meshes can be attached to a single node, in which
// case we need to transform the vertices into the node's
// local space.
if (m.getType() == Mesh::T_Standard)
{
o.node = m.getNodeIndex(0);
if (o.node != -1) {
// Transform the mesh into node space. The mesh vertices
// must all be relative to the bone their attached to.
Quaternion world_rot;
Point world_trans;
getNodeWorldPosRot(o.node,world_trans, world_rot);
m.translate(-world_trans);
m.rotate(world_rot.inverse());
}
}
// Skin meshes need transform information to be able to
// transform vertices into bone space (should fix the Torque
// engine to calculate these at load time).
if (m.getType() == Mesh::T_Skin)
{
for (int n = 0; n < m.getNodeIndexCount(); n++)
{
Quaternion world_rot;
Point world_trans;
getNodeWorldPosRot(m.getNodeIndex(n),world_trans, world_rot);
m.setNodeTransform(n,world_trans,world_rot);
}
}
objects.push_back(o) ;
}
// --------------------------------------------------------
// Detail levels
// Not fully supported, we'll just export all our current
// stuff as single detail level.
// --------------------------------------------------------
DetailLevel dl ;
dl.name = addName("Detail-1") ;
dl.size = 0 ; // Projected size of detail level
dl.objectDetail = 0 ;
dl.polyCount = 0 ;
dl.subshape = 0 ;
dl.maxError = -1 ;
dl.avgError = -1 ;
std::vector<Mesh>::iterator mesh_ptr ;
for (mesh_ptr = meshes.begin() ; mesh_ptr != meshes.end() ; mesh_ptr++)
dl.polyCount += mesh_ptr->getPolyCount() ;
detailLevels.push_back(dl) ;
// --------------------------------------------------------
// Collision detail levels and meshes (optional)
// --------------------------------------------------------
// Update our bounds values as we might use them to build
// a collision mesh.
calculateBounds() ;
calculateCenter() ;
calculateRadius() ;
calculateTubeRadius() ;
// Export every mesh called "Collision" as a collision mesh.
if (config.collisionType != Shape::C_None)
{
// Material for visible meshes
int meshMaterial = materials.size();
if (config.collisionVisible)
{
Material mat ;
mat.name = "Collision_Mesh" ;
mat.flags = Material::NeverEnvMap | Material::SelfIlluminating;
mat.reflectance = materials.size() ;
mat.bump = -1 ;
mat.detail = -1 ;
mat.reflection = 1.0f ;
mat.detailScale = 1.0f ;
materials.push_back(mat) ;
}
// Loop through all the meshes and extract those that will be used for collision
if (config.collisionType == Shape::C_Mesh)
{
int numCollisions = 1;
for (i = 0 ; i < numMeshes ; i++)
{
msMesh * mesh = msModel_GetMeshAt(model, i);
msMesh_GetName (mesh, buffer, 510) ;
// Changed 01/16/2004 by Sven Knie
bool colMesh = !strnicmp(buffer,"Collision", 9);
bool losMesh = !strnicmp(buffer,"LoSCollision", 11);
if (!colMesh && !losMesh)
continue;
Mesh* collmesh = new MilkshapeMesh(mesh, nodes.size() - 1, config.scaleFactor, true) ;
// Create the collision detail level
char detailName[256];
sprintf(detailName,colMesh? "Collision-%d": "LOS-%d", numCollisions++);
dl.name = addName(detailName) ;
// Changed 01/16/2004 by Sven Knie
dl.size = (float)numCollisions * (-1) ; // negativ sized detail levels are never rendered
dl.objectDetail = detailLevels.size() ;
dl.subshape = 0 ;
dl.polyCount = collmesh->getPolyCount() ;
detailLevels.push_back(dl) ;
// Create an object for the collision mesh and attach it to
// the "Root" node.
o.name = colMesh? addName("Col"): addName("LoSCol") ;
o.node = nodes.size() - 1 ;
o.firstMesh = meshes.size() ;
o.numMeshes = dl.objectDetail + 1 ;
objects.push_back(o) ;
// Create a renderable copy of the collision meshes
// for visible detail levels, or a null placeholder
for (int d = 0 ; d < dl.objectDetail ; d++)
if (config.collisionVisible)
{
meshes.push_back(*collmesh) ;
meshes[meshes.size()-1].setMaterial(meshMaterial) ;
}
else
meshes.push_back(Mesh(Mesh::T_Null)) ;
// Add the mesh to the list
meshes.push_back(*collmesh);
}
}
else
{
// Let's create the specified collision mesh type
Mesh* collmesh;
switch (config.collisionType)
{
case C_BBox:
collmesh = new BoxMesh(getBounds()) ;
break ;
case C_Cylinder:
collmesh = new CylinderMesh(getBounds(), getTubeRadius(), config.collisionComplexity) ;
break ;
default:
assert (0 && "Invalid collision mesh type") ;
}
// Create the collision detail level
dl.name = addName("Collision-1") ;
dl.size = -1 ; // -1 sized detail levels are never rendered
dl.objectDetail = detailLevels.size() ;
dl.subshape = 0 ;
dl.polyCount = collmesh->getPolyCount() ;
detailLevels.push_back(dl) ;
// Create an object for the collision mesh and attach it to
// the "Root" node.
o.name = addName("Col") ;
o.node = nodes.size() - 1 ;
o.firstMesh = meshes.size() ;
o.numMeshes = dl.objectDetail + 1 ;
objects.push_back(o) ;
// Create a renderable copy of the collision meshes
// for visible detail levels, or a null placeholder
for (int d = 0 ; d < dl.objectDetail ; d++)
if (config.collisionVisible)
{
meshes.push_back(*collmesh) ;
meshes[meshes.size()-1].setMaterial(meshMaterial) ;
}
else
meshes.push_back(Mesh(Mesh::T_Null)) ;
// Add the mesh to the list
meshes.push_back(*collmesh);
}
}
// --------------------------------------------------------
// Subshape and final stuff
// --------------------------------------------------------
// Create a subshape with everything
Subshape s ;
s.firstObject = 0 ;
s.firstNode = 0 ;
s.firstDecal = 0 ;
s.numNodes = nodes.size() ;
s.numObjects = objects.size() ;
s.numDecals = decals.size() ;
subshapes.push_back(s) ;
// Create an object state for each object (not sure about this)
ObjectState os ;
os.vis = 1.0f ;
os.frame = 0 ;
os.matFrame = 0 ;
for (i = 0 ; i < objects.size() ; i++)
objectStates.push_back(os) ;
// Recalculate bounds (they may have changed)
calculateBounds() ;
calculateCenter() ;
calculateRadius() ;
calculateTubeRadius() ;
setSmallestSize(config.minimumSize) ;
// --------------------------------------------------------
// Animation (optional)
// For each sequence, Torque wants an array of frame * nodes
// information for all nodes affected by that sequence. Node
// animation information can be translation, rotation, scale,
// visibility, material, etc.
//
// The sequence contains a "matters" array for each type of
// animation info. Each type has it's own array of frame * nodes
// which contains only the nodes affected by that type of
// information for the sequence. Since each array is NxN,
// if a node is animated on a single frame of the sequence, it
// will get an entry for every frame.
// --------------------------------------------------------
int frameCount = msModel_GetTotalFrames (model);
if (frameCount && numBones && config.animation)
{
// Process all the sequences.
for (int sc = 0; sc < config.sequence.size(); sc++)
{
MilkshapeShape::ImportConfig::Sequence& si = config.sequence[sc];
// Build the exported sequence structure
Sequence s;
s.flags = Sequence::UniformScale;
if (si.cyclic)
s.flags |= Sequence::Cyclic;
s.nameIndex = addName(si.name) ;
s.numKeyFrames = si.end - si.start;
s.duration = float(s.numKeyFrames) / si.fps;
s.baseTranslation = nodeTranslations.size() ;
s.baseRotation = nodeRotations.size();
// Count how many nodes are affected by the sequence and
// set the sequence.matter arrays to indicate which ones.
s.matters.translation.assign (nodes.size(), false);
s.matters.rotation.assign (nodes.size(), false);
int nodeCount = 0;
for (i = 0 ; i < numBones ; i++)
{
msBone * bone = msModel_GetBoneAt(model, i);
if (isBoneAnimated(bone,si.start,si.end))
{
// Milkshape seems to always produce rotation & position
// keys in pairs, so we'll just deal with them together.
s.matters.translation[i] = true;
s.matters.rotation[i] = true;
nodeCount++;
}
}
// Size arrays to hold keyframe * nodeCount
nodeTranslations.resize (nodeTranslations.size() + nodeCount * s.numKeyFrames);
nodeRotations.resize (nodeTranslations.size());
// Set the keyframe data for each affected bone. Unaffected
// bones are skipped so the final NxN array of transforms
// and rotations is "compressed", sort of. We could compress
// both the rotation and the position arrays individually,
// but MS seems to always generate the values in pairs, so
// we'll do them together. Though the msKey loops are seperate,
// just in case.
int index = 0;
for (i = 0 ; i < numBones ; i++)
{
msBone * bone = msModel_GetBoneAt(model, i) ;
if (isBoneAnimated(bone,si.start,si.end))
{
int numPKeys = msBone_GetPositionKeyCount(bone);
int numRKeys = msBone_GetRotationKeyCount(bone);
// Insert translation keys into the table.
Point *translations = &nodeTranslations[s.baseTranslation + index * s.numKeyFrames];
int lastFrame = 0;
for (j = 0 ; j < numPKeys ; j++)
{
msPositionKey * msKey = msBone_GetPositionKeyAt (bone, j);
int frame = int(msKey->fTime) - 1;
// Only want keys in the sequence range. If it's before
// our range, we'll put it into the 0 frame in case we don't
// get a key for that frame later.
if (frame >= si.end)
break;
if (frame < si.start)
frame = 0;
else
frame -= si.start;
// Store the total translation for the node and fill in
// the initial frame if this is the first key frame.
translations[frame] = nodeDefTranslations[i] +
nodeDefRotations[i].apply(MilkshapePoint(msKey->Position) * config.scaleFactor);
if (!j && frame > 0)
translations[0] = translations[frame];
// Interpolate the missing frames.
for (int f = lastFrame + 1; f < frame; f++)
lerpTranslation(&translations[f],translations[lastFrame],
translations[frame], f, lastFrame, frame);
lastFrame = frame;
}
// Duplicate the last frame to the end.
for (int t = lastFrame + 1; t < s.numKeyFrames; t++)
translations[t] = translations[lastFrame];
// Insert rotation keys into the table.
Quaternion *rotations = &nodeRotations[s.baseTranslation + index * s.numKeyFrames];
lastFrame = 0;
for (j = 0 ; j < numRKeys ; j++)
{
msRotationKey * msKey = msBone_GetRotationKeyAt (bone, j) ;
int frame = int(msKey->fTime) - 1;
// Only want keys in the sequence range. If it's before
// our range, we'll put it into the 0 frame in case we don't
// get a key for that frame later.
if (frame >= si.end)
break;
if (frame < si.start)
frame = 0;
else
frame -= si.start;
// Store the total rotation for the node and fill in
// the initial frame if this is the first key frame.
rotations[frame] = MilkshapeQuaternion(msKey->Rotation) * nodeDefRotations[i];
if (!j && frame > 0)
rotations[0] = rotations[frame];
// Interpolate the missing frames.
for (int f = lastFrame + 1; f < frame; f++)
lerpRotation(&rotations[f],rotations[lastFrame],
rotations[frame], f, lastFrame, frame);
lastFrame = frame;
}
// Duplicate the last frame to the end.
for (int r = lastFrame + 1; r < s.numKeyFrames; r++)
rotations[r] = rotations[lastFrame];
// Increment the position & rotation array index
index++;
}
}
sequences.push_back(s);
}
}
}
} // DTS namespace