//----------------------------------------------------------------------------- // Copyright (c) 2012 GarageGames, LLC // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. //----------------------------------------------------------------------------- #include "platform/platform.h" // Make GCC happy. Needs to have seen this before processing the // hash table template. struct VertTuple; namespace DictHash { inline U32 hash( const VertTuple& data ); } #include "ts/collada/colladaExtensions.h" #include "ts/collada/colladaAppMesh.h" #include "ts/collada/colladaAppNode.h" #include "ts/collada/colladaAppMaterial.h" #include "core/util/tDictionary.h" #include "core/stringTable.h" using namespace ColladaUtils; bool ColladaAppMesh::fixedSizeEnabled = false; S32 ColladaAppMesh::fixedSize = 2; //----------------------------------------------------------------------------- // Define a VertTuple dictionary to allow fast tuple lookups namespace DictHash { inline U32 hash(const VertTuple& data) { return (U32)data.vertex; } } typedef Map VertTupleMap; //----------------------------------------------------------------------------- // Find a source with matching ID. Cannot use the DOM .getElement method since // some lame Collada exporters generate s with non-unique IDs. daeElement* findInputSource(const daeElement* input) { // Try using the DOM .getElement method => the resolved element's parent // should be the input's grandparent daeElement* parent = ((daeElement*)input)->getParentElement(); daeElement* grandparent = parent ? parent->getParentElement() : 0; if (!grandparent) return NULL; const domURIFragmentType* uri = 0; if (input->getElementType() == COLLADA_TYPE::INPUTLOCAL) uri = &daeSafeCast((daeElement*)input)->getSource(); else if (input->getElementType() == COLLADA_TYPE::INPUTLOCALOFFSET) uri = &daeSafeCast((daeElement*)input)->getSource(); if (!uri) return NULL; daeElement* element = uri->getElement(); if (element && element->getParentElement() == grandparent) return element; else { // Probably a non-unique ID => search for the matching element manually // Skip the leading '#' on source IDs const char* id = uri->originalStr().c_str(); if (id && (id[0] == '#')) id++; for (S32 iChild = 0; iChild < grandparent->getChildren().getCount(); iChild++) { element = grandparent->getChildren()[iChild]; if ((element->getElementType() != COLLADA_TYPE::SOURCE) && (element->getElementType() != COLLADA_TYPE::VERTICES)) continue; if (dStrEqual(id, element->getAttribute("id").c_str())) return element; } } return NULL; } //----------------------------------------------------------------------------- // Collada scatters the data required for geometry all over the place; this class // helps to group it all together. class MeshStreams { public: // The sources we want to read from the mesh stream. Can be any order, but // sources of the same type (eg. UVs and UV2s) must be sequential (to allow // ordering by set index) enum eSourceType { Points, Normals, Colors, UVs, UV2s, Joints, Weights, InvBindMatrices, NumStreams }; const char* SourceTypeToSemantic(eSourceType type) { switch ( type ) { case Points: return "POSITION"; case Normals: return "NORMAL"; case Colors: return "COLOR"; case UVs: case UV2s: return "TEXCOORD"; case Joints: return "JOINT"; case Weights: return "WEIGHT"; case InvBindMatrices: return "INV_BIND_MATRIX"; default: return ""; } } private: /// Classify a single input template static void selectInput(T input, T sortedInputs[], S32 start, S32 end=-1) { if (end == -1) end = start; // Get the set for this input const domInputLocalOffset* localOffset = daeSafeCast(input); domUint newSet = localOffset ? localOffset->getSet() : 0; // Add the input to the right place in the list (somewhere between start and end) for (S32 i = start; i <= end; i++) { localOffset = daeSafeCast(sortedInputs[i]); domUint set = localOffset ? localOffset->getSet() : 0xFFFFFFFF; if (newSet < set) { for (S32 j = i + 1; j <= end; j++) sortedInputs[j] = sortedInputs[j-1]; sortedInputs[i] = input; return; } } } /// Attempt to initialise a _SourceReader template bool initSourceReader(T input, eSourceType type, _SourceReader& reader, const char* params[]) { if (!input) return false; // Try to get the source element const domSource* source = 0; daeElement *element = findInputSource(input); if (element->getElementType() == COLLADA_TYPE::SOURCE) source = daeSafeCast(element); else if (element->getElementType() == COLLADA_TYPE::VERTICES) { const domVertices* vertices = daeSafeCast(element); // Search for the input with the desired semantic const char* semantic = SourceTypeToSemantic( type ); for (S32 iInput = 0; iInput < vertices->getInput_array().getCount(); iInput++) { domInputLocal* vInput = vertices->getInput_array().get(iInput); if (dStrEqual(vInput->getSemantic(), semantic)) { source = daeSafeCast(findInputSource(vInput)); break; } } } if (!source) return false; return reader.initFromSource(source, params); } public: _SourceReader points; _SourceReader normals; _SourceReader colors; _SourceReader uvs; _SourceReader uv2s; _SourceReader joints; _SourceReader weights; _SourceReader invBindMatrices; /// Clear the mesh streams void reset() { points.reset(); normals.reset(); colors.reset(); uvs.reset(); uv2s.reset(); joints.reset(); weights.reset(); invBindMatrices.reset(); } /// Classify a set of inputs by type and set number (needs to be a template /// because Collada has two forms of input arrays that may be accessed in /// an identical fashion, but the classes are unrelated. Sigh. template static void classifyInputs(const daeTArray& inputs, T sortedInputs[], U32 *maxOffset=0) { if (maxOffset) *maxOffset = 0; // Clear output array for (S32 i = 0; i < NumStreams; i++) sortedInputs[i] = 0; // Separate inputs by type, and sort by set (ie. lowest TEXCOORD set becomes UV, // next TEXCOORD set becomes UV2 etc) for (S32 iInput = 0; iInput < inputs.getCount(); iInput++) { const T& input = inputs[iInput]; const daeString semantic = input->getSemantic(); if (dStrEqual(semantic, "VERTEX")) { domVertices* vertices = daeSafeCast(findInputSource(input)); // The element may contain multiple inputs (eg. POSITION, NORMAL etc) domInputLocalRef verticesInputs[NumStreams]; classifyInputs(vertices->getInput_array(), verticesInputs); for (S32 iStream = 0; iStream < NumStreams; iStream++) { if (verticesInputs[iStream] != 0) sortedInputs[iStream] = input; } } else if (dStrEqual(semantic, "POSITION")) selectInput(input, sortedInputs, Points); else if (dStrEqual(semantic, "NORMAL")) selectInput(input, sortedInputs, Normals); else if (dStrEqual(semantic, "COLOR")) selectInput(input, sortedInputs, Colors); else if (dStrEqual(semantic, "TEXCOORD")) selectInput(input, sortedInputs, UVs, UV2s); else if (dStrEqual(semantic, "JOINT")) selectInput(input, sortedInputs, Joints); else if (dStrEqual(semantic, "WEIGHT")) selectInput(input, sortedInputs, Weights); else if (dStrEqual(semantic, "INV_BIND_MATRIX")) selectInput(input, sortedInputs, InvBindMatrices); if (maxOffset) { const domInputLocalOffset* localOffset = daeSafeCast(input); domUint offset = localOffset ? localOffset->getOffset() : 0; if (offset > (*maxOffset)) *maxOffset = offset; } } } /// Read a set of inputs into the named sources. There may be multiple 'sets' /// of COLOR or TEXCOORD (uvs) streams, but we are only interested in the /// first COLOR set (ie. smallest set value), and the first 2 TEXCOORDS sets. template bool readInputs(const daeTArray& inputs) { // Sort inputs by type and set to find the ones we are interested in T sortedInputs[NumStreams]; classifyInputs(inputs, sortedInputs); // Attempt to initialise the SourceReaders const char* vertex_params[] = { "X", "Y", "Z", "" }; initSourceReader(sortedInputs[Points], Points, points, vertex_params); const char* normal_params[] = { "X", "Y", "Z", "" }; initSourceReader(sortedInputs[Normals], Normals, normals, normal_params); const char* color_params[] = { "R", "G", "B", "A", "" }; initSourceReader(sortedInputs[Colors], Colors, colors, color_params); const char* uv_params[] = { "S", "T", "" }; const char* uv_params2[] = { "U", "V", "" }; // some files use the nonstandard U,V or X,Y param names const char* uv_params3[] = { "X", "Y", "" }; if (!initSourceReader(sortedInputs[UVs], UVs, uvs, uv_params)) if (!initSourceReader(sortedInputs[UVs], UVs, uvs, uv_params2)) initSourceReader(sortedInputs[UVs], UVs, uvs, uv_params3); if (!initSourceReader(sortedInputs[UV2s], UV2s, uv2s, uv_params)) if (!initSourceReader(sortedInputs[UV2s], UV2s, uv2s, uv_params2)) initSourceReader(sortedInputs[UV2s], UV2s, uv2s, uv_params3); const char* joint_params[] = { "JOINT", "" }; initSourceReader(sortedInputs[Joints], Joints, joints, joint_params); const char* weight_params[] = { "WEIGHT", "" }; initSourceReader(sortedInputs[Weights], Weights, weights, weight_params); const char* matrix_params[] = { "TRANSFORM", "" }; initSourceReader(sortedInputs[InvBindMatrices], InvBindMatrices, invBindMatrices, matrix_params); return true; } }; //------------------------------------------------------------------------------ ColladaAppMesh::ColladaAppMesh(const domInstance_geometry* instance, ColladaAppNode* node) : appNode(node),instanceGeom(instance), instanceCtrl(0), geomExt(0) { flags = 0; numFrames = 0; numMatFrames = 0; } ColladaAppMesh::ColladaAppMesh(const domInstance_controller* instance, ColladaAppNode* node) : appNode(node),instanceGeom(0), instanceCtrl(instance), geomExt(0) { flags = 0; numFrames = 0; numMatFrames = 0; } const char* ColladaAppMesh::getName(bool allowFixed) { // Some exporters add a 'PIVOT' or unnamed node between the mesh and the // actual object node. Detect this and return the object node name instead // of the pivot node. const char* nodeName = appNode->getName(); if ( dStrEqual(nodeName, "null") || dStrEndsWith(nodeName, "PIVOT") ) nodeName = appNode->getParentName(); // If all geometry is being fixed to the same size, append the size // to the name return allowFixed && fixedSizeEnabled ? avar("%s %d", nodeName, fixedSize) : nodeName; } MatrixF ColladaAppMesh::getMeshTransform(F32 time) { return appNode->getNodeTransform(time); } bool ColladaAppMesh::animatesVis(const AppSequence* appSeq) { #define IS_VIS_ANIMATED(node) \ (dynamic_cast(node)->nodeExt->visibility.isAnimated(appSeq->getStart(), appSeq->getEnd())) // Check if the node visibility is animated within the sequence interval return IS_VIS_ANIMATED(appNode) || (appNode->appParent ? IS_VIS_ANIMATED(appNode->appParent) : false); } bool ColladaAppMesh::animatesMatFrame(const AppSequence* appSeq) { // Texture coordinates may be animated in two ways: // - by animating the MAYA profile texture transform (diffuse texture) // - by animating the morph weights for morph targets with different UVs // Check if the MAYA profile texture transform is animated for (S32 iMat = 0; iMat < appMaterials.size(); iMat++) { ColladaAppMaterial* appMat = static_cast(appMaterials[iMat]); if (appMat->effectExt && appMat->effectExt->animatesTextureTransform(appSeq->getStart(), appSeq->getEnd())) return true; } // Check that the morph weights are animated within the sequence interval, // and that the morph targets have different UVs to the base geometry. bool animated = false; bool differentUVs = false; if (const domMorph* morph = getMorph()) { for (S32 iInput = 0; iInput < morph->getTargets()->getInput_array().getCount(); iInput++) { const domInputLocal* input = morph->getTargets()->getInput_array()[iInput]; if (dStrEqual(input->getSemantic(), "MORPH_TARGET")) { // @todo: Check if morph targets have different UVs to base geometry differentUVs = false; } if (dStrEqual(input->getSemantic(), "MORPH_WEIGHT")) { const domSource* source = daeSafeCast(findInputSource(input)); AnimatedFloatList weights(source ? source->getFloat_array() : 0); animated = weights.isAnimated(appSeq->getStart(), appSeq->getEnd()); } } } return (animated && differentUVs); } bool ColladaAppMesh::animatesFrame(const AppSequence* appSeq) { // Collada s ALWAYS contain vert positions, so just need to check if // the morph weights are animated within the sequence interval bool animated = false; if (const domMorph* morph = getMorph()) { for (S32 iInput = 0; iInput < morph->getTargets()->getInput_array().getCount(); iInput++) { const domInputLocal* input = morph->getTargets()->getInput_array()[iInput]; if (dStrEqual(input->getSemantic(), "MORPH_WEIGHT")) { const domSource* source = daeSafeCast(findInputSource(input)); AnimatedFloatList weights(source ? source->getFloat_array() : 0); animated = weights.isAnimated(appSeq->getStart(), appSeq->getEnd()); break; } } } return animated; } F32 ColladaAppMesh::getVisValue(F32 t) { #define GET_VIS(node) \ (dynamic_cast(node)->nodeExt->visibility.getValue(t)) // Get the visibility of the mesh's node at time, 't' return GET_VIS(appNode) * (appNode->appParent ? GET_VIS(appNode->appParent) : 1.0f); } S32 ColladaAppMesh::addMaterial(const char* symbol) { if (!symbol) return TSDrawPrimitive::NoMaterial; // Lookup the symbol in the materials already bound to this geometry/controller // instance Map::Iterator itr = boundMaterials.find(symbol); if (itr != boundMaterials.end()) return itr->value; // Find the Collada material that this symbol maps to U32 matIndex = TSDrawPrimitive::NoMaterial; const domBind_material* binds = instanceGeom ? instanceGeom->getBind_material() : instanceCtrl->getBind_material(); if (binds) { const domInstance_material_Array& matArray = binds->getTechnique_common()->getInstance_material_array(); for (S32 iBind = 0; iBind < matArray.getCount(); iBind++) { if (dStrEqual(matArray[iBind]->getSymbol(), symbol)) { // Find the index of the bound material in the shape global list const domMaterial* mat = daeSafeCast(matArray[iBind]->getTarget().getElement()); for (matIndex = 0; matIndex < appMaterials.size(); matIndex++) { if (static_cast(appMaterials[matIndex])->mat == mat) break; } // Check if this material needs to be added to the shape global list if (matIndex == appMaterials.size()) { if (mat) appMaterials.push_back(new ColladaAppMaterial(mat)); else appMaterials.push_back(new ColladaAppMaterial(symbol)); } break; } } } else { // No Collada material is present for this symbol, so just create an empty one appMaterials.push_back(new ColladaAppMaterial(symbol)); } // Add this symbol to the bound list for the mesh boundMaterials.insert(StringTable->insert(symbol), matIndex); return matIndex; } void ColladaAppMesh::getPrimitives(const domGeometry* geometry) { // Only do this once if (primitives.size()) return; // Read the extension if (!geomExt) geomExt = new ColladaExtension_geometry(geometry); // Get the supported primitive elements for this geometry, and warn // about unsupported elements Vector meshPrims; const daeElementRefArray& contents = geometry->getMesh()->getContents(); for (S32 iElem = 0; iElem < contents.getCount(); iElem++) { if (BasePrimitive::isPrimitive(contents[iElem])) { if (BasePrimitive::isSupportedPrimitive(contents[iElem])) meshPrims.push_back(BasePrimitive::get(contents[iElem])); else { daeErrorHandler::get()->handleWarning(avar("Collada <%s> element " "in %s is not supported.", contents[iElem]->getElementName(), _GetNameOrId(geometry))); } } } MeshStreams streams; VertTupleMap tupleMap; // Create Torque primitives for (S32 iPrim = 0; iPrim < meshPrims.size(); iPrim++) { // Primitive element must have at least 1 triangle const domListOfUInts* pTriData = meshPrims[iPrim]->getTriangleData(); if (!pTriData) continue; U32 numTriangles = pTriData->getCount() / meshPrims[iPrim]->getStride() / 3; if (!numTriangles) continue; // Create TSMesh primitive primitives.increment(); TSDrawPrimitive& primitive = primitives.last(); primitive.start = indices.size(); primitive.matIndex = (TSDrawPrimitive::Triangles | TSDrawPrimitive::Indexed) | addMaterial(meshPrims[iPrim]->getMaterial()); // Get the AppMaterial associated with this primitive ColladaAppMaterial* appMat = 0; if (!(primitive.matIndex & TSDrawPrimitive::NoMaterial)) appMat = static_cast(appMaterials[primitive.matIndex & TSDrawPrimitive::MaterialMask]); // Force the material to be double-sided if this geometry is double-sided. if (geomExt->double_sided && appMat && appMat->effectExt) appMat->effectExt->double_sided = true; // Pre-allocate triangle indices primitive.numElements = numTriangles * 3; indices.setSize(indices.size() + primitive.numElements); U32* dstIndex = indices.end() - primitive.numElements; // Determine the offset for each element type in the stream, and also the // maximum input offset, which will be the number of indices per vertex we // need to skip. domInputLocalOffsetRef sortedInputs[MeshStreams::NumStreams]; MeshStreams::classifyInputs(meshPrims[iPrim]->getInputs(), sortedInputs); S32 offsets[MeshStreams::NumStreams]; for (S32 i = 0; i < MeshStreams::NumStreams; i++) offsets[i] = sortedInputs[i] ? sortedInputs[i]->getOffset() : -1; // Loop through indices const domUint* pSrcData = &(pTriData->get(0)); for (U32 iTri = 0; iTri < numTriangles; iTri++) { // If the next triangle could cause us to index across a 16-bit // boundary, split this primitive and clear the tuple map to // ensure primitives only index verts within a 16-bit range. if (vertTuples.size() && (((vertTuples.size()-1) ^ (vertTuples.size()+2)) & 0x10000)) { // Pad vertTuples up to the next 16-bit boundary while (vertTuples.size() & 0xFFFF) vertTuples.push_back(VertTuple(vertTuples.last())); // Split the primitive at the current triangle S32 indicesRemaining = (numTriangles - iTri) * 3; if (iTri > 0) { daeErrorHandler::get()->handleWarning(avar("Splitting primitive " "in %s: too many verts for 16-bit indices.", _GetNameOrId(geometry))); primitives.last().numElements -= indicesRemaining; primitives.push_back(TSDrawPrimitive(primitives.last())); } primitives.last().numElements = indicesRemaining; primitives.last().start = indices.size() - indicesRemaining; tupleMap.clear(); } streams.reset(); streams.readInputs(meshPrims[iPrim]->getInputs()); for (U32 v = 0; v < 3; v++) { // Collect vert tuples into a single array so we can easily grab // vertex data later. VertTuple tuple; tuple.prim = iPrim; tuple.vertex = offsets[MeshStreams::Points] >= 0 ? pSrcData[offsets[MeshStreams::Points]] : -1; tuple.normal = offsets[MeshStreams::Normals] >= 0 ? pSrcData[offsets[MeshStreams::Normals]] : -1; tuple.color = offsets[MeshStreams::Colors] >= 0 ? pSrcData[offsets[MeshStreams::Colors]] : -1; tuple.uv = offsets[MeshStreams::UVs] >= 0 ? pSrcData[offsets[MeshStreams::UVs]] : -1; tuple.uv2 = offsets[MeshStreams::UV2s] >= 0 ? pSrcData[offsets[MeshStreams::UV2s]] : -1; tuple.dataVertex = tuple.vertex > -1 ? streams.points.getPoint3FValue(tuple.vertex) : Point3F::Max; tuple.dataNormal = tuple.normal > -1 ? streams.normals.getPoint3FValue(tuple.normal) : Point3F::Max; tuple.dataColor = tuple.color > -1 ? streams.colors.getColorIValue(tuple.color) : ColorI(0,0,0); tuple.dataUV = tuple.uv > -1 ? streams.uvs.getPoint2FValue(tuple.uv) : Point2F::Max; tuple.dataUV2 = tuple.uv2 > -1 ? streams.uv2s.getPoint2FValue(tuple.uv2) : Point2F::Max; VertTupleMap::Iterator itr = tupleMap.find(tuple); if (itr == tupleMap.end()) { itr = tupleMap.insert(tuple, vertTuples.size()); vertTuples.push_back(tuple); } // Collada uses CCW for front face and Torque uses the opposite, so // for normal (non-inverted) meshes, the indices are flipped. if (appNode->invertMeshes) dstIndex[v] = itr->value; else dstIndex[2 - v] = itr->value; pSrcData += meshPrims[iPrim]->getStride(); } dstIndex += 3; } } for (S32 iPrim = 0; iPrim < meshPrims.size(); iPrim++) delete meshPrims[iPrim]; } void ColladaAppMesh::getVertexData(const domGeometry* geometry, F32 time, const MatrixF& objOffset, Vector& v_points, Vector& v_norms, Vector& v_colors, Vector& v_uvs, Vector& v_uv2s, bool appendValues) { if (!primitives.size()) return; MeshStreams streams; S32 lastPrimitive = -1; ColladaAppMaterial* appMat = 0; // Get the supported primitive elements for this geometry Vector meshPrims; const daeElementRefArray& contents = geometry->getMesh()->getContents(); for (S32 iElem = 0; iElem < contents.getCount(); iElem++) { if (BasePrimitive::isSupportedPrimitive(contents[iElem])) meshPrims.push_back(BasePrimitive::get(contents[iElem])); } // If appending values, pre-allocate the arrays if (appendValues) { v_points.setSize(v_points.size() + vertTuples.size()); v_uvs.setSize(v_uvs.size() + vertTuples.size()); } // Get pointers to arrays Point3F* points_array = &v_points[v_points.size() - vertTuples.size()]; Point2F* uvs_array = &v_uvs[v_uvs.size() - vertTuples.size()]; Point3F* norms_array = NULL; ColorI* colors_array = NULL; Point2F* uv2s_array = NULL; for (S32 iVert = 0; iVert < vertTuples.size(); iVert++) { const VertTuple& tuple = vertTuples[iVert]; // Change primitives? if (tuple.prim != lastPrimitive) { if (meshPrims.size() <= tuple.prim) { daeErrorHandler::get()->handleError(avar("Failed to get vertex data " "for %s. Primitives do not match base geometry.", geometry->getID())); break; } // Update vertex/normal/UV streams and get the new material index streams.reset(); streams.readInputs(meshPrims[tuple.prim]->getInputs()); S32 matIndex = addMaterial(meshPrims[tuple.prim]->getMaterial()); if (matIndex != TSDrawPrimitive::NoMaterial) appMat = static_cast(appMaterials[matIndex]); else appMat = 0; lastPrimitive = tuple.prim; } // If we are NOT appending values, only set the value if it actually exists // in the mesh data stream. if (appendValues || ((tuple.vertex >= 0) && (tuple.vertex < streams.points.size()))) { points_array[iVert] = streams.points.getPoint3FValue(tuple.vertex); // Flip verts for inverted meshes if (appNode->invertMeshes) points_array[iVert].z = -points_array[iVert].z; objOffset.mulP(points_array[iVert]); } if (appendValues || ((tuple.uv >= 0) && (tuple.uv < streams.uvs.size()))) { uvs_array[iVert] = streams.uvs.getPoint2FValue(tuple.uv); if (appMat && appMat->effectExt) appMat->effectExt->applyTextureTransform(uvs_array[iVert], time); uvs_array[iVert].y = 1.0f - uvs_array[iVert].y; // Collada texcoords are upside down compared to TGE } // The rest is non-required data... if it doesn't exist then don't append it. if ( (tuple.normal >= 0) && (tuple.normal < streams.normals.size()) ) { if ( !norms_array && iVert == 0 ) { v_norms.setSize(v_norms.size() + vertTuples.size()); norms_array = &v_norms[v_norms.size() - vertTuples.size()]; } if ( norms_array ) { norms_array[iVert] = streams.normals.getPoint3FValue(tuple.normal); // Flip normals for inverted meshes if (appNode->invertMeshes) norms_array[iVert].z = -norms_array[iVert].z; } } if ( (tuple.color >= 0) && (tuple.color < streams.colors.size())) { if ( !colors_array && iVert == 0 ) { v_colors.setSize(v_colors.size() + vertTuples.size()); colors_array = &v_colors[v_colors.size() - vertTuples.size()]; } if ( colors_array ) colors_array[iVert] = streams.colors.getColorIValue(tuple.color); } if ( (tuple.uv2 >= 0) && (tuple.uv2 < streams.uv2s.size()) ) { if ( !uv2s_array && iVert == 0 ) { v_uv2s.setSize(v_uv2s.size() + vertTuples.size()); uv2s_array = &v_uv2s[v_uv2s.size() - vertTuples.size()]; } if ( uv2s_array ) { uv2s_array[iVert] = streams.uv2s.getPoint2FValue(tuple.uv2); if (appMat && appMat->effectExt) appMat->effectExt->applyTextureTransform(uv2s_array[iVert], time); uv2s_array[iVert].y = 1.0f - uv2s_array[iVert].y; // Collada texcoords are upside down compared to TGE } } } for (S32 iPrim = 0; iPrim < meshPrims.size(); iPrim++) delete meshPrims[iPrim]; } void ColladaAppMesh::getMorphVertexData(const domMorph* morph, F32 time, const MatrixF& objOffset, Vector& v_points, Vector& v_norms, Vector& v_colors, Vector& v_uvs, Vector& v_uv2s) { // @todo: Could the base geometry (or any target geometry) also be a morph? // Get the target geometries and weights (could be animated) Vector targetGeoms; domListOfFloats targetWeights; for (S32 iInput = 0; iInput < morph->getTargets()->getInput_array().getCount(); iInput++) { const domInputLocal* input = morph->getTargets()->getInput_array()[iInput]; const domSource* source = daeSafeCast(findInputSource(input)); if (dStrEqual(input->getSemantic(), "MORPH_TARGET")) { // Get the morph targets _SourceReader srcTargets; srcTargets.initFromSource(source); for (S32 iTarget = 0; iTarget < srcTargets.size(); iTarget++) { // Lookup the element and add to the targets list daeIDRef idref(srcTargets.getStringValue(iTarget)); idref.setContainer(morph->getDocument()->getDomRoot()); targetGeoms.push_back(daeSafeCast(idref.getElement())); } } else if (dStrEqual(input->getSemantic(), "MORPH_WEIGHT")) { // Get the (possibly animated) morph weight targetWeights = AnimatedFloatList(source->getFloat_array()).getValue(time); } } // Check that we have a weight for each target if (targetGeoms.size() != targetWeights.getCount()) { domController* ctrl = daeSafeCast(const_cast(morph)->getParent()); Con::warnf("Mismatched morph targets and weights in %s.", _GetNameOrId(ctrl)); // Set unused targets to zero weighting (unused weights are ignored) while (targetGeoms.size() > targetWeights.getCount()) targetWeights.append(0.0f); } // Get the base geometry and vertex data const domGeometry* baseGeometry = daeSafeCast(morph->getSource().getElement()); if (!baseGeometry) return; getPrimitives(baseGeometry); getVertexData(baseGeometry, time, objOffset, v_points, v_norms, v_colors, v_uvs, v_uv2s, true); // Get pointers to the arrays of base geometry data Point3F* points_array = &v_points[v_points.size() - vertTuples.size()]; Point3F* norms_array = &v_norms[v_norms.size() - vertTuples.size()]; Point2F* uvs_array = &v_uvs[v_uvs.size() - vertTuples.size()]; ColorI* colors_array = v_colors.size() ? &v_colors[v_colors.size() - vertTuples.size()] : 0; Point2F* uv2s_array = v_uv2s.size() ? &v_uv2s[v_uv2s.size() - vertTuples.size()] : 0; // Normalize base vertex data? if (morph->getMethod() == MORPHMETHODTYPE_NORMALIZED) { F32 weightSum = 0.0f; for (S32 iWeight = 0; iWeight < targetWeights.getCount(); iWeight++) { weightSum += targetWeights[iWeight]; } // Result = Base*(1.0-w1-w2 ... -wN) + w1*Target1 + w2*Target2 ... + wN*TargetN weightSum = mClampF(1.0f - weightSum, 0.0f, 1.0f); for (S32 iVert = 0; iVert < vertTuples.size(); iVert++) { points_array[iVert] *= weightSum; norms_array[iVert] *= weightSum; uvs_array[iVert] *= weightSum; } if (uv2s_array) { for (S32 iVert = 0; iVert < vertTuples.size(); iVert++) uv2s_array[iVert] *= weightSum; } } // Interpolate using the target geometry and weights for (S32 iTarget = 0; iTarget < targetGeoms.size(); iTarget++) { // Ignore empty weights if (targetWeights[iTarget] == 0.0f) continue; // Get target geometry data into temporary arrays Vector targetPoints; Vector targetNorms; Vector targetUvs; Vector targetColors; Vector targetUv2s; // Copy base geometry into target geometry (will be used if target does // not define normals or uvs) targetPoints.set(points_array, vertTuples.size()); targetNorms.set(norms_array, vertTuples.size()); targetUvs.set(uvs_array, vertTuples.size()); if (colors_array) targetColors.set(colors_array, vertTuples.size()); if (uv2s_array) targetUv2s.set(uv2s_array, vertTuples.size()); getVertexData(targetGeoms[iTarget], time, objOffset, targetPoints, targetNorms, targetColors, targetUvs, targetUv2s, false); // Combine with base geometry for (S32 iVert = 0; iVert < vertTuples.size(); iVert++) { points_array[iVert] += targetPoints[iVert] * targetWeights[iTarget]; norms_array[iVert] += targetNorms[iVert] * targetWeights[iTarget]; uvs_array[iVert] += targetUvs[iVert] * targetWeights[iTarget]; } if (uv2s_array) { for (S32 iVert = 0; iVert < vertTuples.size(); iVert++) uv2s_array[iVert] += targetUv2s[iVert] * targetWeights[iTarget]; } if (colors_array) { for (S32 iVert = 0; iVert < vertTuples.size(); iVert++) { LinearColorF tCol = colors_array[iVert]; tCol += LinearColorF(targetColors[iVert]) * (F32)targetWeights[iTarget]; colors_array[iVert] = tCol.toColorI(); } } } } void ColladaAppMesh::lockMesh(F32 t, const MatrixF& objOffset) { // Find the geometry element for this mesh. Could be one of 3 things: // 1) a simple static mesh (Collada element) // 2) a simple morph (some combination of static meshes) // 3) a skin (skin geometry could also be a morph!) daeElement* geometry = 0; if (instanceGeom) { // Simple, static mesh geometry = instanceGeom->getUrl().getElement(); } else if (instanceCtrl) { const domController* ctrl = daeSafeCast(instanceCtrl->getUrl().getElement()); if (!ctrl) { daeErrorHandler::get()->handleWarning(avar("Failed to find " "element for %s", getName())); return; } else if (ctrl->getMorph()) { // Morph controller geometry = ctrl->getMorph(); } else { // Skinned mesh: source geometry could be static geometry or a morph controller. geometry = ctrl->getSkin()->getSource().getElement(); if (geometry && geometry->getElementType() == COLLADA_TYPE::CONTROLLER) geometry = daeSafeCast(geometry)->getMorph(); } } if (!geometry) { daeErrorHandler::get()->handleWarning(avar("Failed to find source geometry " "for %s", getName())); return; } // Now get the vertex data at the specified time if (geometry->getElementType() == COLLADA_TYPE::GEOMETRY) { getPrimitives(daeSafeCast(geometry)); getVertexData(daeSafeCast(geometry), t, objOffset, points, normals, colors, uvs, uv2s, true); } else if (geometry->getElementType() == COLLADA_TYPE::MORPH) { getMorphVertexData(daeSafeCast(geometry), t, objOffset, points, normals, colors, uvs, uv2s); } else { daeErrorHandler::get()->handleWarning(avar("Unsupported geometry type " "'<%s>' for %s", geometry->getElementName(), getName())); } } void ColladaAppMesh::lookupSkinData() { // Only lookup skin data once if (!isSkin() || weight.size()) return; // Get the skin and vertex weight data const domSkin* skin = daeSafeCast(instanceCtrl->getUrl().getElement())->getSkin(); const domSkin::domVertex_weights& weightIndices = *(skin->getVertex_weights()); const domListOfInts& weights_v = weightIndices.getV()->getValue(); const domListOfUInts& weights_vcount = weightIndices.getVcount()->getValue(); MeshStreams streams; streams.readInputs(skin->getJoints()->getInput_array()); streams.readInputs(weightIndices.getInput_array()); MatrixF invObjOffset(objectOffset); invObjOffset.inverse(); // Get the bind shape matrix MatrixF bindShapeMatrix(true); if (skin->getBind_shape_matrix()) bindShapeMatrix = vecToMatrixF(skin->getBind_shape_matrix()->getValue()); bindShapeMatrix.mul(invObjOffset); // Determine the offset into the vindices array for each vertex (since each // vertex may have multiple [bone, weight] pairs in the array) Vector vindicesOffset; const domInt* vindices = (domInt*)weights_v.getRaw(0); for (S32 iWeight = 0; iWeight < weights_vcount.getCount(); iWeight++) { // Store the offset into the vindices array for this vertex vindicesOffset.push_back(vindices - (domInt*)weights_v.getRaw(0)); vindices += (weights_vcount[iWeight]*2); // 2 indices [bone, weight] per vert } // Set vertex weights bool tooManyWeightsWarning = false; for (S32 iVert = 0; iVert < vertsPerFrame; iVert++) { const domUint* vcount = (domUint*)weights_vcount.getRaw(0); vindices = (domInt*)weights_v.getRaw(0); vindices += vindicesOffset[vertTuples[iVert].vertex]; S32 nonZeroWeightCount = 0; for (S32 iWeight = 0; iWeight < vcount[vertTuples[iVert].vertex]; iWeight++) { S32 bIndex = vindices[iWeight*2]; F32 bWeight = streams.weights.getFloatValue( vindices[iWeight*2 + 1] ); // Ignore empty weights if ( bIndex < 0 || bWeight == 0 ) continue; // Limit the number of weights per bone (keep the N largest influences) if ( nonZeroWeightCount >= TSSkinMesh::BatchData::maxBonePerVert ) { if (vcount[vertTuples[iVert].vertex] > TSSkinMesh::BatchData::maxBonePerVert) { if (!tooManyWeightsWarning) { tooManyWeightsWarning = true; daeErrorHandler::get()->handleWarning(avar("At least one vertex has " "too many bone weights. Limiting to the largest %d influences.", TSSkinMesh::BatchData::maxBonePerVert)); } } // Too many weights => find and replace the smallest one S32 minIndex = weight.size() - TSSkinMesh::BatchData::maxBonePerVert; F32 minWeight = weight[minIndex]; for (S32 i = minIndex + 1; i < weight.size(); i++) { if (weight[i] < minWeight) { minWeight = weight[i]; minIndex = i; } } boneIndex[minIndex] = bIndex; weight[minIndex] = bWeight; } else { vertexIndex.push_back( iVert ); boneIndex.push_back( bIndex ); weight.push_back( bWeight ); nonZeroWeightCount++; } } } // Normalize vertex weights (force weights for each vert to sum to 1) S32 iWeight = 0; while (iWeight < weight.size()) { // Find the last weight with the same vertex number, and sum all weights for // that vertex F32 invTotalWeight = 0; S32 iLast; for (iLast = iWeight; iLast < weight.size(); iLast++) { if (vertexIndex[iLast] != vertexIndex[iWeight]) break; invTotalWeight += weight[iLast]; } // Then normalize the vertex weights invTotalWeight = 1.0f / invTotalWeight; for (; iWeight < iLast; iWeight++) weight[iWeight] *= invTotalWeight; } // Add dummy AppNodes to allow Collada joints to be mapped to 3space nodes bones.setSize(streams.joints.size()); initialTransforms.setSize(streams.joints.size()); for (S32 iJoint = 0; iJoint < streams.joints.size(); iJoint++) { const char* jointName = streams.joints.getStringValue(iJoint); // Lookup the joint element const domNode* joint = 0; if (instanceCtrl->getSkeleton_array().getCount()) { // Search for the node using the as the base element for (S32 iSkel = 0; iSkel < instanceCtrl->getSkeleton_array().getCount(); iSkel++) { xsAnyURI skeleton = instanceCtrl->getSkeleton_array()[iSkel]->getValue(); daeSIDResolver resolver(skeleton.getElement(), jointName); joint = daeSafeCast(resolver.getElement()); if (joint) break; } } else { // Search for the node from the root level daeSIDResolver resolver(skin->getDocument()->getDomRoot(), jointName); joint = daeSafeCast(resolver.getElement()); } if (!joint) { daeErrorHandler::get()->handleWarning(avar("Failed to find bone '%s', " "defaulting to instance_controller parent node '%s'", jointName, appNode->getName())); joint = appNode->getDomNode(); } bones[iJoint] = new ColladaAppNode(joint); initialTransforms[iJoint] = objectOffset; // Bone scaling is generally ignored during import, since 3space only // stores default node transform and rotation. Compensate for this by // removing the scaling from the inverse bind transform as well MatrixF invBind = streams.invBindMatrices.getMatrixFValue(iJoint); if (!ColladaUtils::getOptions().ignoreNodeScale) { Point3F invScale = invBind.getScale(); invScale.x = invScale.x ? (1.0f / invScale.x) : 0; invScale.y = invScale.y ? (1.0f / invScale.y) : 0; invScale.z = invScale.z ? (1.0f / invScale.z) : 0; initialTransforms[iJoint].scale(invScale); } // Inverted node coordinate spaces (negative scale factor) are corrected // in ColladaAppNode::getNodeTransform, so need to apply the same operation // here to match if (m_matF_determinant(invBind) < 0.0f) initialTransforms[iJoint].scale(Point3F(1, 1, -1)); initialTransforms[iJoint].mul(invBind); initialTransforms[iJoint].mul(bindShapeMatrix); } }