Index: ps/trunk/source/collada/PSAConvert.cpp =================================================================== --- ps/trunk/source/collada/PSAConvert.cpp (revision 26066) +++ ps/trunk/source/collada/PSAConvert.cpp (revision 26067) @@ -1,323 +1,325 @@ -/* Copyright (C) 2018 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "PSAConvert.h" #include "CommonConvert.h" #include "FCollada.h" #include "FCDocument/FCDocument.h" #include "FCDocument/FCDocumentTools.h" #include "FCDocument/FCDAnimated.h" #include "FCDocument/FCDAnimationCurve.h" #include "FCDocument/FCDAnimationKey.h" #include "FCDocument/FCDController.h" #include "FCDocument/FCDControllerInstance.h" #include "FCDocument/FCDExtra.h" #include "FCDocument/FCDGeometry.h" #include "FCDocument/FCDGeometryMesh.h" #include "FCDocument/FCDGeometryPolygons.h" #include "FCDocument/FCDGeometrySource.h" #include "FCDocument/FCDSceneNode.h" #include "StdSkeletons.h" #include "Decompose.h" #include "Maths.h" #include "GeomReindex.h" #include #include #include #include #include class PSAConvert { public: /** * Converts a COLLADA XML document into the PSA animation format. * * @param input XML document to parse * @param output callback for writing the PSA data; called lots of times * with small strings * @param xmlErrors output - errors reported by the XML parser * @throws ColladaException on failure */ static void ColladaToPSA(const char* input, OutputCB& output, std::string& xmlErrors) { CommonConvert converter(input, xmlErrors); if (converter.GetInstance().GetType() == FCDEntityInstance::CONTROLLER) { FCDControllerInstance& controllerInstance = static_cast(converter.GetInstance()); FixSkeletonRoots(controllerInstance); assert(converter.GetInstance().GetEntity()->GetType() == FCDEntity::CONTROLLER); // assume this is always true? FCDController* controller = static_cast(converter.GetInstance().GetEntity()); FCDSkinController* skin = controller->GetSkinController(); REQUIRE(skin != NULL, "is skin controller"); const Skeleton& skeleton = FindSkeleton(controllerInstance); float frameLength = 1.f / 30.f; // currently we always want to create PMDs at fixed 30fps // Find the extents of the animation: float timeStart = 0, timeEnd = 0; GetAnimationRange(converter.GetDocument(), skeleton, controllerInstance, timeStart, timeEnd); // To catch broken animations / skeletons.xml: REQUIRE(timeEnd > timeStart, "animation end frame must come after start frame"); // Count frames; don't include the last keyframe size_t frameCount = (size_t)((timeEnd - timeStart) / frameLength - 0.5f); REQUIRE(frameCount > 0, "animation must have frames"); // (TODO: sort out the timing/looping problems) - size_t boneCount = skeleton.GetBoneCount(); + const size_t boneCount = skeleton.GetBoneCount(); + if (boneCount > 64) + Log(LOG_ERROR, "Skeleton has too many bones %zu/64", boneCount); std::vector boneTransforms; for (size_t frame = 0; frame < frameCount; ++frame) { float time = timeStart + frameLength * frame; BoneTransform boneDefault = { { 0, 0, 0 }, { 0, 0, 0, 1 } }; - std::vector frameBoneTransforms (boneCount, boneDefault); + std::vector frameBoneTransforms(boneCount, boneDefault); // Move the model into the new animated pose // (We can't tell exactly which nodes should be animated, so // just update the entire world recursively) EvaluateAnimations(converter.GetRoot(), time); // Convert the pose into the form require by the game for (size_t i = 0; i < controllerInstance.GetJointCount(); ++i) { FCDSceneNode* joint = controllerInstance.GetJoint(i); int boneId = skeleton.GetRealBoneID(joint->GetName().c_str()); if (boneId < 0) continue; // not a recognised bone - ignore it, same as before FMMatrix44 worldTransform = joint->CalculateWorldTransform(); HMatrix matrix; memcpy(matrix, worldTransform.Transposed().m, sizeof(matrix)); AffineParts parts; decomp_affine(matrix, &parts); BoneTransform b = { { parts.t.x, parts.t.y, parts.t.z }, { parts.q.x, parts.q.y, parts.q.z, parts.q.w } }; frameBoneTransforms[boneId] = b; } // Push frameBoneTransforms onto the back of boneTransforms copy(frameBoneTransforms.begin(), frameBoneTransforms.end(), std::inserter(boneTransforms, boneTransforms.end())); } // Convert into game's coordinate space TransformVertices(boneTransforms, skin->GetBindShapeTransform(), converter.IsYUp(), converter.IsXSI()); // Write out the file WritePSA(output, frameCount, boneCount, boneTransforms); } else { throw ColladaException("Unrecognised object type"); } } /** * Writes the animation data in the PSA format. */ static void WritePSA(OutputCB& output, size_t frameCount, size_t boneCount, const std::vector& boneTransforms) { output("PSSA", 4); // magic number write(output, (uint32)1); // version number write(output, (uint32)( 4 + 0 + // name 4 + // frameLength 4 + 4 + // numBones, numFrames 7*4*boneCount*frameCount // boneStates )); // data size // Name write(output, (uint32)0); // Frame length write(output, 1000.f/30.f); write(output, (uint32)boneCount); write(output, (uint32)frameCount); for (size_t i = 0; i < boneCount*frameCount; ++i) { output((char*)&boneTransforms[i], 7*4); } } static void TransformVertices(std::vector& bones, const FMMatrix44& transform, bool yUp, bool isXSI) { // HACK: we want to handle scaling in XSI because that makes it easy // for artists to adjust the models to the right size. But this way // doesn't work in Max, and I can't see how to make it do so, so this // is only applied to models from XSI. if (isXSI) { TransformBones(bones, DecomposeToScaleMatrix(transform), yUp); } else { TransformBones(bones, FMMatrix44_Identity, yUp); } } static void GetAnimationRange(const FColladaDocument& doc, const Skeleton& skeleton, const FCDControllerInstance& controllerInstance, float& timeStart, float& timeEnd) { // FCollada tools export info in the scene to specify the start // and end times. // If that isn't available, we have to search for the earliest and latest // keyframes on any of the bones. if (doc.GetDocument()->HasStartTime() && doc.GetDocument()->HasEndTime()) { timeStart = doc.GetDocument()->GetStartTime(); timeEnd = doc.GetDocument()->GetEndTime(); return; } // XSI exports relevant information in // // (and 'end' and 'frameRate') so use those if (GetAnimationRange_XSI(doc, timeStart, timeEnd)) return; timeStart = std::numeric_limits::max(); timeEnd = -std::numeric_limits::max(); for (size_t i = 0; i < controllerInstance.GetJointCount(); ++i) { const FCDSceneNode* joint = controllerInstance.GetJoint(i); REQUIRE(joint != NULL, "joint exists"); int boneId = skeleton.GetBoneID(joint->GetName().c_str()); if (boneId < 0) { // unrecognised joint - it's probably just a prop point // or something, so ignore it continue; } // Skip unanimated joints if (joint->GetTransformCount() == 0) continue; for (size_t j = 0; j < joint->GetTransformCount(); ++j) { const FCDTransform* transform = joint->GetTransform(j); if (! transform->IsAnimated()) continue; // Iterate over all curves to find the earliest and latest keys const FCDAnimated* anim = transform->GetAnimated(); const FCDAnimationCurveListList& curvesList = anim->GetCurves(); for (size_t k = 0; k < curvesList.size(); ++k) { const FCDAnimationCurveTrackList& curves = curvesList[k]; for (size_t l = 0; l < curves.size(); ++l) { const FCDAnimationCurve* curve = curves[l]; timeStart = std::min(timeStart, curve->GetKeys()[0]->input); timeEnd = std::max(timeEnd, curve->GetKeys()[curve->GetKeyCount()-1]->input); } } } } } static bool GetAnimationRange_XSI(const FColladaDocument& doc, float& timeStart, float& timeEnd) { FCDExtra* extra = doc.GetExtra(); if (! extra) return false; FCDEType* type = extra->GetDefaultType(); if (! type) return false; FCDETechnique* technique = type->FindTechnique("XSI"); if (! technique) return false; FCDENode* scene = technique->FindChildNode("SI_Scene"); if (! scene) return false; float start = FLT_MAX, end = -FLT_MAX, framerate = 0.f; FCDENodeList paramNodes; scene->FindChildrenNodes("xsi_param", paramNodes); for (FCDENodeList::iterator it = paramNodes.begin(); it != paramNodes.end(); ++it) { if ((*it)->ReadAttribute("sid") == "start") start = FUStringConversion::ToFloat((*it)->GetContent()); else if ((*it)->ReadAttribute("sid") == "end") end = FUStringConversion::ToFloat((*it)->GetContent()); else if ((*it)->ReadAttribute("sid") == "frameRate") framerate = FUStringConversion::ToFloat((*it)->GetContent()); } if (framerate != 0.f && start != FLT_MAX && end != -FLT_MAX) { timeStart = start / framerate; timeEnd = end / framerate; return true; } return false; } static void EvaluateAnimations(FCDSceneNode& node, float time) { for (size_t i = 0; i < node.GetTransformCount(); ++i) { FCDTransform* transform = node.GetTransform(i); FCDAnimated* anim = transform->GetAnimated(); if (anim) anim->Evaluate(time); } for (size_t i = 0; i < node.GetChildrenCount(); ++i) EvaluateAnimations(*node.GetChild(i), time); } }; // The above stuff is just in a class since I don't like having to bother // with forward declarations of functions - but provide the plain function // interface here: void ColladaToPSA(const char* input, OutputCB& output, std::string& xmlErrors) { PSAConvert::ColladaToPSA(input, output, xmlErrors); } Index: ps/trunk/source/renderer/InstancingModelRenderer.cpp =================================================================== --- ps/trunk/source/renderer/InstancingModelRenderer.cpp (revision 26066) +++ ps/trunk/source/renderer/InstancingModelRenderer.cpp (revision 26067) @@ -1,385 +1,392 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "renderer/InstancingModelRenderer.h" #include "graphics/Color.h" #include "graphics/LightEnv.h" #include "graphics/Model.h" #include "graphics/ModelDef.h" #include "lib/ogl.h" #include "maths/Vector3D.h" #include "maths/Vector4D.h" #include "ps/CLogger.h" #include "ps/CStrInternStatic.h" #include "renderer/Renderer.h" #include "renderer/RenderModifiers.h" #include "renderer/VertexArray.h" #include "third_party/mikktspace/weldmesh.h" struct IModelDef : public CModelDefRPrivate { /// Static per-CModel vertex array VertexArray m_Array; /// Position and normals are static VertexArray::Attribute m_Position; VertexArray::Attribute m_Normal; VertexArray::Attribute m_Tangent; VertexArray::Attribute m_BlendJoints; // valid iff gpuSkinning == true VertexArray::Attribute m_BlendWeights; // valid iff gpuSkinning == true /// The number of UVs is determined by the model std::vector m_UVs; /// Indices are the same for all models, so share them VertexIndexArray m_IndexArray; IModelDef(const CModelDefPtr& mdef, bool gpuSkinning, bool calculateTangents); }; IModelDef::IModelDef(const CModelDefPtr& mdef, bool gpuSkinning, bool calculateTangents) : m_IndexArray(GL_STATIC_DRAW), m_Array(GL_STATIC_DRAW) { size_t numVertices = mdef->GetNumVertices(); m_Position.type = GL_FLOAT; m_Position.elems = 3; m_Array.AddAttribute(&m_Position); m_Normal.type = GL_FLOAT; m_Normal.elems = 3; m_Array.AddAttribute(&m_Normal); m_UVs.resize(mdef->GetNumUVsPerVertex()); for (size_t i = 0; i < mdef->GetNumUVsPerVertex(); i++) { m_UVs[i].type = GL_FLOAT; m_UVs[i].elems = 2; m_Array.AddAttribute(&m_UVs[i]); } if (gpuSkinning) { + // We can't use a lot of bones because it costs uniform memory. Recommended + // number of bones per model is 32. + // Add 1 to NumBones because of the special 'root' bone. + if (mdef->GetNumBones() + 1 > 64) + LOGERROR("Model '%s' has too many bones %zu/64", mdef->GetName().string8().c_str(), mdef->GetNumBones() + 1); + ENSURE(mdef->GetNumBones() + 1 <= 64); + m_BlendJoints.type = GL_UNSIGNED_BYTE; m_BlendJoints.elems = 4; m_Array.AddAttribute(&m_BlendJoints); m_BlendWeights.type = GL_UNSIGNED_BYTE; m_BlendWeights.elems = 4; m_Array.AddAttribute(&m_BlendWeights); } if (calculateTangents) { // Generate tangents for the geometry:- m_Tangent.type = GL_FLOAT; m_Tangent.elems = 4; m_Array.AddAttribute(&m_Tangent); // floats per vertex; position + normal + tangent + UV*sets [+ GPUskinning] int numVertexAttrs = 3 + 3 + 4 + 2 * mdef->GetNumUVsPerVertex(); if (gpuSkinning) { numVertexAttrs += 8; } // the tangent generation can increase the number of vertices temporarily // so reserve a bit more memory to avoid reallocations in GenTangents (in most cases) std::vector newVertices; newVertices.reserve(numVertexAttrs * numVertices * 2); // Generate the tangents ModelRenderer::GenTangents(mdef, newVertices, gpuSkinning); // how many vertices do we have after generating tangents? int newNumVert = newVertices.size() / numVertexAttrs; std::vector remapTable(newNumVert); std::vector vertexDataOut(newNumVert * numVertexAttrs); // re-weld the mesh to remove duplicated vertices int numVertices2 = WeldMesh(&remapTable[0], &vertexDataOut[0], &newVertices[0], newNumVert, numVertexAttrs); // Copy the model data to graphics memory:- m_Array.SetNumVertices(numVertices2); m_Array.Layout(); VertexArrayIterator Position = m_Position.GetIterator(); VertexArrayIterator Normal = m_Normal.GetIterator(); VertexArrayIterator Tangent = m_Tangent.GetIterator(); VertexArrayIterator BlendJoints; VertexArrayIterator BlendWeights; if (gpuSkinning) { BlendJoints = m_BlendJoints.GetIterator(); BlendWeights = m_BlendWeights.GetIterator(); } // copy everything into the vertex array for (int i = 0; i < numVertices2; i++) { int q = numVertexAttrs * i; Position[i] = CVector3D(vertexDataOut[q + 0], vertexDataOut[q + 1], vertexDataOut[q + 2]); q += 3; Normal[i] = CVector3D(vertexDataOut[q + 0], vertexDataOut[q + 1], vertexDataOut[q + 2]); q += 3; Tangent[i] = CVector4D(vertexDataOut[q + 0], vertexDataOut[q + 1], vertexDataOut[q + 2], vertexDataOut[q + 3]); q += 4; if (gpuSkinning) { for (size_t j = 0; j < 4; ++j) { BlendJoints[i][j] = (u8)vertexDataOut[q + 0 + 2 * j]; BlendWeights[i][j] = (u8)vertexDataOut[q + 1 + 2 * j]; } q += 8; } for (size_t j = 0; j < mdef->GetNumUVsPerVertex(); j++) { VertexArrayIterator UVit = m_UVs[j].GetIterator(); UVit[i][0] = vertexDataOut[q + 0 + 2 * j]; UVit[i][1] = vertexDataOut[q + 1 + 2 * j]; } } // upload vertex data m_Array.Upload(); m_Array.FreeBackingStore(); m_IndexArray.SetNumVertices(mdef->GetNumFaces() * 3); m_IndexArray.Layout(); VertexArrayIterator Indices = m_IndexArray.GetIterator(); size_t idxidx = 0; // reindex geometry and upload index for (size_t j = 0; j < mdef->GetNumFaces(); ++j) { Indices[idxidx++] = remapTable[j * 3 + 0]; Indices[idxidx++] = remapTable[j * 3 + 1]; Indices[idxidx++] = remapTable[j * 3 + 2]; } m_IndexArray.Upload(); m_IndexArray.FreeBackingStore(); } else { // Upload model without calculating tangents:- m_Array.SetNumVertices(numVertices); m_Array.Layout(); VertexArrayIterator Position = m_Position.GetIterator(); VertexArrayIterator Normal = m_Normal.GetIterator(); ModelRenderer::CopyPositionAndNormals(mdef, Position, Normal); for (size_t i = 0; i < mdef->GetNumUVsPerVertex(); i++) { VertexArrayIterator UVit = m_UVs[i].GetIterator(); ModelRenderer::BuildUV(mdef, UVit, i); } if (gpuSkinning) { VertexArrayIterator BlendJoints = m_BlendJoints.GetIterator(); VertexArrayIterator BlendWeights = m_BlendWeights.GetIterator(); for (size_t i = 0; i < numVertices; ++i) { const SModelVertex& vtx = mdef->GetVertices()[i]; for (size_t j = 0; j < 4; ++j) { BlendJoints[i][j] = vtx.m_Blend.m_Bone[j]; BlendWeights[i][j] = (u8)(255.f * vtx.m_Blend.m_Weight[j]); } } } m_Array.Upload(); m_Array.FreeBackingStore(); m_IndexArray.SetNumVertices(mdef->GetNumFaces()*3); m_IndexArray.Layout(); ModelRenderer::BuildIndices(mdef, m_IndexArray.GetIterator()); m_IndexArray.Upload(); m_IndexArray.FreeBackingStore(); } } struct InstancingModelRendererInternals { bool gpuSkinning; bool calculateTangents; /// Previously prepared modeldef IModelDef* imodeldef; /// Index base for imodeldef u8* imodeldefIndexBase; }; // Construction and Destruction InstancingModelRenderer::InstancingModelRenderer(bool gpuSkinning, bool calculateTangents) { m = new InstancingModelRendererInternals; m->gpuSkinning = gpuSkinning; m->calculateTangents = calculateTangents; m->imodeldef = 0; } InstancingModelRenderer::~InstancingModelRenderer() { delete m; } // Build modeldef data if necessary - we have no per-CModel data CModelRData* InstancingModelRenderer::CreateModelData(const void* key, CModel* model) { CModelDefPtr mdef = model->GetModelDef(); IModelDef* imodeldef = (IModelDef*)mdef->GetRenderData(m); if (m->gpuSkinning) ENSURE(model->IsSkinned()); else ENSURE(!model->IsSkinned()); if (!imodeldef) { imodeldef = new IModelDef(mdef, m->gpuSkinning, m->calculateTangents); mdef->SetRenderData(m, imodeldef); } return new CModelRData(key); } void InstancingModelRenderer::UpdateModelData(CModel* UNUSED(model), CModelRData* UNUSED(data), int UNUSED(updateflags)) { // We have no per-CModel data } // Setup one rendering pass. void InstancingModelRenderer::BeginPass(int streamflags) { ENSURE(streamflags == (streamflags & (STREAM_POS|STREAM_NORMAL|STREAM_UV0|STREAM_UV1))); } // Cleanup rendering pass. void InstancingModelRenderer::EndPass(int UNUSED(streamflags)) { CVertexBuffer::Unbind(); } // Prepare UV coordinates for this modeldef void InstancingModelRenderer::PrepareModelDef(const CShaderProgramPtr& shader, int streamflags, const CModelDef& def) { m->imodeldef = (IModelDef*)def.GetRenderData(m); ENSURE(m->imodeldef); u8* base = m->imodeldef->m_Array.Bind(); GLsizei stride = (GLsizei)m->imodeldef->m_Array.GetStride(); m->imodeldefIndexBase = m->imodeldef->m_IndexArray.Bind(); if (streamflags & STREAM_POS) shader->VertexPointer(3, GL_FLOAT, stride, base + m->imodeldef->m_Position.offset); if (streamflags & STREAM_NORMAL) shader->NormalPointer(GL_FLOAT, stride, base + m->imodeldef->m_Normal.offset); if (m->calculateTangents) shader->VertexAttribPointer(str_a_tangent, 4, GL_FLOAT, GL_FALSE, stride, base + m->imodeldef->m_Tangent.offset); // The last UV set is STREAM_UV3 for (size_t uv = 0; uv < 4; ++uv) if (streamflags & (STREAM_UV0 << uv)) { if (def.GetNumUVsPerVertex() >= uv + 1) shader->TexCoordPointer(GL_TEXTURE0 + uv, 2, GL_FLOAT, stride, base + m->imodeldef->m_UVs[uv].offset); else ONCE(LOGERROR("Model '%s' has no UV%d set.", def.GetName().string8().c_str(), uv)); } // GPU skinning requires extra attributes to compute positions/normals if (m->gpuSkinning) { shader->VertexAttribPointer(str_a_skinJoints, 4, GL_UNSIGNED_BYTE, GL_FALSE, stride, base + m->imodeldef->m_BlendJoints.offset); shader->VertexAttribPointer(str_a_skinWeights, 4, GL_UNSIGNED_BYTE, GL_TRUE, stride, base + m->imodeldef->m_BlendWeights.offset); } shader->AssertPointersBound(); } // Render one model void InstancingModelRenderer::RenderModel(const CShaderProgramPtr& shader, int UNUSED(streamflags), CModel* model, CModelRData* UNUSED(data)) { const CModelDefPtr& mdldef = model->GetModelDef(); if (m->gpuSkinning) { // Bind matrices for current animation state. // Add 1 to NumBones because of the special 'root' bone. // HACK: NVIDIA drivers return uniform name with "[0]", Intel Windows drivers without; // try uploading both names since one of them should work, and this is easier than // canonicalising the uniform names in CShaderProgramGLSL shader->Uniform(str_skinBlendMatrices_0, mdldef->GetNumBones() + 1, model->GetAnimatedBoneMatrices()); shader->Uniform(str_skinBlendMatrices, mdldef->GetNumBones() + 1, model->GetAnimatedBoneMatrices()); } // render the lot size_t numFaces = mdldef->GetNumFaces(); if (!g_Renderer.m_SkipSubmit) { // Draw with DrawRangeElements where available, since it might be more efficient #if CONFIG2_GLES glDrawElements(GL_TRIANGLES, (GLsizei)numFaces*3, GL_UNSIGNED_SHORT, m->imodeldefIndexBase); #else pglDrawRangeElementsEXT(GL_TRIANGLES, 0, (GLuint)m->imodeldef->m_Array.GetNumVertices()-1, (GLsizei)numFaces*3, GL_UNSIGNED_SHORT, m->imodeldefIndexBase); #endif } // bump stats g_Renderer.m_Stats.m_DrawCalls++; g_Renderer.m_Stats.m_ModelTris += numFaces; }