/* =========================================================================== Doom 3 BFG Edition GPL Source Code Copyright (C) 2022-2023 Harrie van Ginneken Copyright (C) 2022-2023 Robert Beckebans This file is part of the Doom 3 BFG Edition GPL Source Code ("Doom 3 BFG Edition Source Code"). Doom 3 BFG Edition Source Code 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 3 of the License, or (at your option) any later version. Doom 3 BFG Edition Source Code 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 Doom 3 BFG Edition Source Code. If not, see . In addition, the Doom 3 BFG Edition Source Code is also subject to certain additional terms. You should have received a copy of these additional terms immediately following the terms and conditions of the GNU General Public License which accompanied the Doom 3 BFG Edition Source Code. If not, please request a copy in writing from id Software at the address below. If you have questions concerning this license or the applicable additional terms, you may contact in writing id Software LLC, c/o ZeniMax Media Inc., Suite 120, Rockville, Maryland 20850 USA. =========================================================================== */ #include "precompiled.h" #pragma hdrstop #include "Model_gltf.h" #include "Model_local.h" #include "RenderCommon.h" // HVG_TODO: this has to be moved out before release #include "d3xp/anim/Anim.h" #include "d3xp/Game_local.h" idCVar gltf_ForceBspMeshTexture( "gltf_ForceBspMeshTexture", "0", CVAR_SYSTEM | CVAR_BOOL, "all world geometry has the same forced texture" ); idCVar gltf_ModelSceneName( "gltf_ModelSceneName", "Scene", CVAR_SYSTEM , "Scene to use when loading specific models" ); idCVar gltf_AnimSampleRate( "gltf_AnimSampleRate", "24", CVAR_SYSTEM | CVAR_INTEGER , "The frame rate of the converted md5anim" ); static const byte GLMB_VERSION = 102; static const unsigned int GLMB_MAGIC = ( 'M' << 24 ) | ( 'L' << 16 ) | ( 'G' << 8 ) | GLMB_VERSION; static const char* GLTF_SnapshotName = "_GLTF_Snapshot_"; static const idMat4 blenderToDoomTransform( idAngles( 0.0f, 0.0f, 90 ).ToMat3(), vec3_origin ); //static const idMat4 blenderToDoomTransform = mat4_identity; static idRenderModelGLTF* lastMeshFromFile = nullptr; bool idRenderModelStatic::ConvertGltfMeshToModelsurfaces( const gltfMesh* mesh ) { return false; } void idRenderModelGLTF::ProcessNode_r( gltfNode* modelNode, const idMat4& parentTransform, const idMat4& globalTransform, gltfData* data ) { auto& meshList = data->MeshList(); auto& nodeList = data->NodeList(); gltfData::ResolveNodeMatrix( modelNode ); idMat4 nodeToWorldTransform = parentTransform * modelNode->matrix; if( modelNode->mesh >= 0 ) { gltfMesh* targetMesh = meshList[modelNode->mesh]; for( auto prim : targetMesh->primitives ) { // FIXME ConvertFromMeshGltf should only be used for the map // here ConvertGltfMeshToModelsurfaces should be used. MapPolygonMesh* mesh = MapPolygonMesh::ConvertFromMeshGltf( prim, data, globalTransform * nodeToWorldTransform ); modelSurface_t surf; gltfMaterial* mat = NULL; if( prim->material != -1 ) { mat = data->MaterialList()[prim->material]; } if( mat != NULL && !gltf_ForceBspMeshTexture.GetBool() ) { surf.shader = declManager->FindMaterial( mat->name ); } else { surf.shader = declManager->FindMaterial( "textures/base_wall/snpanel2rust" ); } surf.id = this->NumSurfaces(); srfTriangles_t* tri = R_AllocStaticTriSurf(); tri->numIndexes = mesh->GetNumPolygons() * 3; tri->numVerts = mesh->GetNumVertices(); R_AllocStaticTriSurfIndexes( tri, tri->numIndexes ); R_AllocStaticTriSurfVerts( tri, tri->numVerts ); int indx = 0; for( int i = 0; i < mesh->GetNumPolygons(); i++ ) { auto& face = mesh->GetFace( i ); auto& faceIdxs = face.GetIndexes(); tri->indexes[indx] = faceIdxs[0]; tri->indexes[indx + 1] = faceIdxs[1]; tri->indexes[indx + 2] = faceIdxs[2]; indx += 3; } tri->bounds.Clear(); for( int i = 0; i < tri->numVerts; ++i ) { tri->verts[i] = mesh->GetDrawVerts()[i]; tri->bounds.AddPoint( tri->verts[i].xyz ); } bounds.AddBounds( tri->bounds ); surf.geometry = tri; AddSurface( surf ); delete mesh; } } for( auto& child : modelNode->children ) { ProcessNode_r( nodeList[child], nodeToWorldTransform, globalTransform, data ); } } static void KeepNodes( gltfData* data, const idStrList& keepList, idList& boneList ) { idStrList finalList; idStr nodeName; idList nodesToKeep; for( int i = 0 ; i < keepList.Num(); i++ ) { bool parentWildcard = false; bool childWildcard = false; if( keepList[i] == "*" ) { parentWildcard = true; i++; } const idStr item = keepList[i]; gltfNode* node = data->GetNode( item ); if( node == nullptr ) { continue; } int idx = data->GetNodeIndex( node ); if( boneList.Find( idx ) ) { nodesToKeep.Insert( idx ); } if( ( i < ( keepList.Num() - 1 ) ) && keepList[i + 1] == "*" ) { childWildcard = true; i++; } if( parentWildcard && node->parent ) { gltfNode* parent = node->parent; while( parent ) { idx = data->GetNodeIndex( parent ); if( boneList.Find( idx ) ) { nodesToKeep.Insert( idx ); } } } if( childWildcard && node->children.Num() ) { idList tmpIdx; tmpIdx.Append( node->children ); for( int j = 0; j < tmpIdx.Num(); j++ ) { idx = tmpIdx[j]; if( boneList.Find( idx ) ) { nodesToKeep.Insert( idx ); gltfNode* tmpNode = data->NodeList()[idx]; if( tmpNode ) { for( int child : tmpNode->children ) { tmpIdx.AddUnique( child ); } } } } } } #if 0 common->SetRefreshOnPrint( true ); common->DPrintf( "=====\n" ); for( int nodeID : boneList ) { if( nodesToKeep.Find( nodeID ) ) { common->DPrintf( "^7[^2Kept^7]\t\t bone \'%s\'\n", data->NodeList()[nodeID]->name.c_str() ); } else { common->DPrintf( "^7[^1Discard^7]\t bone \'%s\'\n ", data->NodeList()[nodeID]->name.c_str() ); } } common->DPrintf( "=====\n" ); common->SetRefreshOnPrint( false ); #endif boneList = nodesToKeep; } static gltfNode* GetBoneNode( gltfData* data, const idList& boneList, const idStr& name ) { auto& nodelist = data->NodeList(); for( int boneId : boneList ) { gltfNode* boneNode = nodelist[boneId]; if( boneNode->name == name ) { return boneNode; } } return nullptr; } static void RemapNodes( gltfData* data, const idList& remapList, const idList& boneList ) { // we need to be _very_ careful with modifying the GLTF data since it is not saved or cached!!! auto& nodeList = data->NodeList(); for( const auto& remap : remapList ) { gltfNode* from = GetBoneNode( data, boneList, remap.from ); gltfNode* to = GetBoneNode( data, boneList, remap.to ); if( !from || !to ) { common->Error( "Invalid remap name pair \'%s\'[\"%s\"] : \'%s\'[\"%s\"]", remap.from.c_str(), from ? from->name.c_str() : "Not Found", remap.to.c_str(), to ? to->name.c_str() : "Not Found" ); } common->Warning( "Remapping.. setting \'%s\' as parent of \'%s\' ", remap.to.c_str(), remap.from.c_str() ); from->parent = to; to->children.Alloc() = data->GetNodeIndex( from ); } } static int AddOriginBone( gltfData* data, idList& bones, gltfNode* root ) { //we need to be _very_ careful with modifying the GLTF data since it is not saved or cached!!! auto& nodeList = data->NodeList(); gltfNode* newNode = data->Node(); int newIdx = nodeList.Num() - 1; bones.Insert( newIdx ); newNode->name = "origin"; //patch children for( int childId : root->children ) { newNode->children.Alloc() = childId; gltfNode* childNode = nodeList[childId]; childNode->parent = newNode; } root->children.Clear(); root->children.Alloc() = nodeList.Num() - 1; newNode->parent = root; common->Warning( "Added origin bone!" ); return newIdx; } static void RenameNodes( gltfData* data, const idList& renameList, const idList& boneList ) { // we need to be _very_ careful with modifying the GLTF data since it is not saved or cached!!! auto& nodeList = data->NodeList(); for( const auto& rename : renameList ) { gltfNode* from = GetBoneNode( data, boneList, rename.from ); if( !from ) { common->Warning( "Invalid rename name pair from \'%s\' -> [\"%s\"] ", rename.from.c_str(), from ? from->name.c_str() : "Not Found" ); return; } common->Warning( "Renaming.. \'%s\' to \'%s\' ", rename.from.c_str(), rename.to.c_str() ); from->name = rename.to; } } // return Armature/Rig node static gltfNode* FindModelRoot( gltfData* data, const idImportOptions* options, idStr& rootName, int* rootID, gltfSkin** rootSkin ) { // According to CG Dive is the most common workflow to get skeletal models from Blender into UE4/5 // by looking for an Armature that is called "root". Usually everything else is supposed to fail. // So the expectations in common .glb files is that the first scene node in the hierarchy is the Armature // and skinned meshes are supposed to be children of the Rig. // 1. If there was an explicit name given in the identifer try it gltfNode* root = nullptr; auto nodes = data->NodeList(); if( options != nullptr && !options->armature.IsEmpty() ) { idLib::Printf( "Looking for armature %s\n", options->armature.c_str() ); auto skin = data->GetSkin( options->armature ); if( skin && ( skin->skeleton > -1 && skin->skeleton < nodes.Num() ) ) { *rootID = skin->skeleton; root = nodes[skin->skeleton]; rootName = options->armature; // because options aren't available in LoadModel() if( rootSkin ) { *rootSkin = skin; } idLib::Printf( "Found glTF2 Armature: name = '%s' ID=%i\n", rootName.c_str(), *rootID ); return root; } } // 2. If an explict root node name is given use that even if it might be not an Armature if( root == nullptr && !rootName.IsEmpty() ) { // try explicit node which can be an Armature name root = data->GetNode( rootName, rootID ); if( root ) { idList meshNodeIDs; data->GetAllSkinnedMeshes( root, meshNodeIDs ); int skinID = -1; for( int meshID : meshNodeIDs ) { gltfNode* meshNode = nodes[meshID]; if( meshNode->skin >= 0 ) { skinID = meshNode->skin; break; } } if( skinID != -1 ) { auto skin = data->SkinList()[skinID]; *rootID = skin->skeleton; root = nodes[skin->skeleton]; rootName = nodes[skin->skeleton]->name; if( rootSkin ) { *rootSkin = skin; } idLib::Printf( "Found glTF2 Rig: name = '%s' ID=%i\n", rootName.c_str(), *rootID ); return root; } } // Special case where you want multiple hiearchical models in the same .glb that are static models if( !root ) { root = data->GetMeshNode( rootName, rootID ); idLib::Printf( "Found glTF2 Mesh: name = '%s' ID=%i\n", rootName.c_str(), *rootID ); } } // 3. Find Armature using skin->skeleton using the child meshes of the armature if( root == nullptr ) { // get all skinned mesh IDs without a given root node idList meshNodeIDs; data->GetAllSkinnedMeshes( meshNodeIDs ); int skinID = -1; for( int meshID : meshNodeIDs ) { gltfNode* meshNode = nodes[meshID]; if( meshNode->skin >= 0 ) { skinID = meshNode->skin; break; } } if( skinID != -1 ) { auto skin = data->SkinList()[skinID]; *rootID = skin->skeleton; root = nodes[skin->skeleton]; rootName = nodes[skin->skeleton]->name; if( rootSkin ) { *rootSkin = skin; } idLib::Printf( "Found glTF2 Rig: name = '%s' ID=%i\n", rootName.c_str(), *rootID ); return root; } } // 4. There was no Armature so far so just try to use the first Mesh from the default scene /* if( root == nullptr ) { // try the first mesh from the default scene if( data->MeshList().Num() > 0 ) { int sceneId = data->DefaultScene(); assert( sceneId >= 0 ); auto scene = data->SceneList()[sceneId]; assert( scene ); gltfMesh* firstMesh = data->MeshList()[0]; //fileExclusive = true; root = data->GetNode( scene, firstMesh, rootID ); if( rootID != nullptr ) { rootName = root->name; } } } */ return root; } // constructs a renderModel from a gltfScene node found in the "models" scene of the given gltfFile. // override with gltf_ModelSceneName // warning : nodeName cannot have dots! // [fileName].[nodeName/nodeId].[gltf/glb] // If no nodeName/nodeId is given, all primitives active in default scene will be added as surfaces. void idRenderModelGLTF::InitFromFile( const char* fileName, const idImportOptions* options ) { hasAnimations = false; fileExclusive = false; root = nullptr; rootID = -1; int meshID = -1; name = fileName; currentSkin = nullptr; idImportOptions* localOptions = nullptr; PurgeModel(); //FIXME FIXME FIXME maxJointVertDist = 10; gltfFileName = idStr( fileName ); model_state = DM_STATIC; if( options ) { commandLine = options->commandLine; localOptions = const_cast( options ); } else { if( !commandLine.IsEmpty() ) { localOptions = new idImportOptions(); localOptions->Init( commandLine, fileName ); } } gltfManager::ExtractIdentifier( gltfFileName, meshID, rootName ); GLTF_Parser gltf; if( !gltf.Load( gltfFileName ) ) { MakeDefaultModel(); if( localOptions && !options ) { delete localOptions; } return; } timeStamp = fileSystem->GetTimestamp( gltfFileName ); data = gltf.currentAsset; bounds.Clear(); int sceneId = data->DefaultScene(); assert( sceneId >= 0 ); auto scene = data->SceneList()[sceneId]; assert( scene ); auto nodes = data->NodeList(); assert( nodes.Num() ); // determine root node if( ( localOptions != nullptr && localOptions->armature.IsEmpty() ) || rootName.IsEmpty() ) { fileExclusive = true; } root = FindModelRoot( data, localOptions, rootName, &rootID, ¤tSkin ); MeshNodeIds.Clear(); bones.Clear(); if( rootID != -1 ) { if( currentSkin ) { // also collect meshes above armature but they need to be bound to the skeleton data->GetAllSkinnedMeshes( currentSkin, MeshNodeIds ); } else { // get all meshes in hierachy, starting at root data->GetAllMeshes( root, MeshNodeIds ); } } else { // collect all meshes without root data->GetAllMeshes( MeshNodeIds ); if( MeshNodeIds.Num() == 0 ) { common->Warning( "Can't find meshes in static glTF2 model: '%s'", name.c_str() ); MakeDefaultModel(); if( localOptions && !options ) { delete localOptions; } return; } } // find all animations and bones for the current skin int totalAnims = 0; int lastSkin = -1; for( int meshID : MeshNodeIds ) { gltfNode* meshNode = nodes[meshID]; int animCount = 0; if( meshNode->skin != -1 && meshNode->skin != lastSkin && lastSkin == -1 ) { animCount = data->GetAnimationIds( meshNode, animIds ); // check if this model has a skeleton/bones // if not but it has an anim, create a bone from the target mesh-node as origin. if( meshNode->skin >= 0 ) { lastSkin = meshNode->skin; //currentSkin = data->SkinList()[meshNode->skin]; //assert( currentSkin ); if( currentSkin->joints.Num() ) { //assert( currentSkin->skeleton == rootID ); // armature node is origin bone //bones.Append( currentSkin->skeleton ); // skeleton bones bones.Append( currentSkin->joints ); animCount = data->GetAnimationIds( nodes[bones[0]], animIds ); } if( localOptions ) { if( localOptions->keepjoints.Num() ) { KeepNodes( data, localOptions->keepjoints, bones ); } if( localOptions->addOrigin ) { gltfNode* armatureNode = nodes[bones[0]]->parent; AddOriginBone( data, bones, armatureNode ); } if( localOptions->remapjoints.Num() ) { RemapNodes( data, localOptions->remapjoints, bones ); } if( localOptions->renamejoints.Num() ) { RenameNodes( data, localOptions->renamejoints, bones ); } } } else { // Boneless TRS animation. animCount = data->GetAnimationIds( meshNode, animIds ); bones.Append( meshID ); } } totalAnims += animCount; } hasAnimations = totalAnims > 0; model_state = hasAnimations ? DM_CACHED : DM_STATIC; bool useMikktspace = true; // combine all scaling, rotating options into globalTransform globalTransform = blenderToDoomTransform; if( localOptions ) { const auto blenderToDoomRotation = idAngles( 0.0f, 0.0f, 90 ).ToMat3(); idMat3 rotationMat = blenderToDoomRotation; if( localOptions->reOrient != ang_zero ) { rotationMat = localOptions->reOrient.ToMat3(); } else if( localOptions->rotate != 0 ) { rotationMat = idAngles( 0.0f, localOptions->rotate, 90 ).ToMat3(); } float scale = localOptions->scale; idMat3 scaleMat( scale, 0, 0, 0, scale, 0, 0, 0, scale ); globalTransform = idMat4( rotationMat * scaleMat, vec3_origin ); if( localOptions->noMikktspace ) { useMikktspace = false; } } #if 0 if( rootID != -1 ) { ProcessNode_r( root, mat4_identity, globalTransform, data ); } else #endif { // rootless mode for( int meshID : MeshNodeIds ) { gltfNode* meshNode = nodes[meshID]; ProcessNode_r( meshNode, mat4_identity, globalTransform, data ); } } if( surfaces.Num() <= 0 ) { common->Warning( "Couldn't load model: '%s'", name.c_str() ); MakeDefaultModel(); data = nullptr; if( localOptions && !options ) { delete localOptions; } return; } #if 0 // patch bone indices; // offset with 1 because armature node is added as root for( auto& surf : surfaces ) { for( int i = 0; i < surf.geometry->numVerts; i++ ) { idDrawVert& base = surf.geometry->verts[i]; base.color[0] += 1; base.color[1] += 1; base.color[2] += 1; base.color[3] += 1; } } #endif if( options ) { if( options->addOrigin ) { // patch bone indices; // offset with one because an origin bone is inserted after root. for( auto& surf : surfaces ) { for( int i = 0; i < surf.geometry->numVerts; i++ ) { idDrawVert& base = surf.geometry->verts[i]; base.color[0] += 1; base.color[1] += 1; base.color[2] += 1; base.color[3] += 1; } } } } // derive mikktspace tangents from normals FinishSurfaces( useMikktspace ); LoadModel(); // it is now available for use lastMeshFromFile = this; if( localOptions && !options ) { delete localOptions; } } bool idRenderModelGLTF::LoadBinaryModel( idFile* file, const ID_TIME_T sourceTimeStamp ) { hasAnimations = false; fileExclusive = false; // not written. root = nullptr; if( !idRenderModelStatic::LoadBinaryModel( file, sourceTimeStamp ) ) { data = nullptr; return false; } unsigned int magic = 0; file->ReadBig( magic ); if( magic != GLMB_MAGIC ) { data = nullptr; return false; } file->ReadString( commandLine ); file->ReadBig( model_state ); file->ReadBig( rootID ); idStr dataFilename; file->ReadString( dataFilename ); int animCnt; file->ReadBig( animCnt ); if( animCnt > 0 ) { animIds.Resize( animCnt, 1 ); file->ReadBigArray( animIds.Ptr(), animCnt ); animIds.SetNum( animCnt ); } hasAnimations = animCnt > 0; int tempNum; file->ReadBig( tempNum ); md5joints.SetNum( tempNum ); for( int i = 0; i < md5joints.Num(); i++ ) { file->ReadString( md5joints[i].name ); int offset; file->ReadBig( offset ); if( offset >= 0 ) { md5joints[i].parent = md5joints.Ptr() + offset; } else { md5joints[i].parent = NULL; } } int boneCnt; file->ReadBig( boneCnt ); if( boneCnt > 0 ) { bones.Resize( boneCnt, 1 ); file->ReadBigArray( bones.Ptr(), boneCnt ); bones.SetNum( boneCnt ); } else { if( hasAnimations && !bones.Num() ) { bones.Clear(); bones.Append( rootID ); } } file->ReadBig( tempNum ); defaultPose.SetNum( tempNum ); for( int i = 0; i < defaultPose.Num(); i++ ) { file->ReadBig( defaultPose[i].q.x ); file->ReadBig( defaultPose[i].q.y ); file->ReadBig( defaultPose[i].q.z ); file->ReadBig( defaultPose[i].q.w ); file->ReadVec3( defaultPose[i].t ); } file->ReadBig( tempNum ); invertedDefaultPose.SetNum( tempNum ); for( int i = 0; i < invertedDefaultPose.Num(); i++ ) { file->ReadBigArray( invertedDefaultPose[i].ToFloatPtr(), JOINTMAT_TYPESIZE ); } SIMD_INIT_LAST_JOINT( invertedDefaultPose.Ptr(), md5joints.Num() ); model_state = hasAnimations ? DM_CACHED : DM_STATIC; lastMeshFromFile = this; data = nullptr; return true; } const idMD5Joint* idRenderModelGLTF::FindMD5Joint( const idStr& name ) const { for( auto& joint : md5joints ) { if( joint.name == name ) { return &joint; } } assert( 0 ); static idMD5Joint staticJoint; return &staticJoint; } // unused void idRenderModelGLTF::UpdateMd5Joints() { // FIXME, for added origin with no skin if( bones.Num() == 1 ) { //patch bone indices //for( auto& surf : surfaces ) //{ // for( int i = 0; i < surf.geometry->numVerts; i++ ) // { // idDrawVert& base = surf.geometry->verts[i]; // base.SetColor( PackColor( ( vec4_zero ) ) ); // base.SetColor2( PackColor( ( vec4_one / 4 ) ) ); // } //} } } void idRenderModelGLTF::DrawJoints( const struct renderEntity_s* ent, const viewDef_t* view ) { int i; int num; idVec3 pos; const idJointMat* joint; const idMD5Joint* md5Joint; int parentNum; num = ent->numJoints; joint = ent->joints; md5Joint = md5joints.Ptr(); for( i = 0; i < num; i++, joint++, md5Joint++ ) { pos = ent->origin + joint->ToVec3() * ent->axis; if( md5Joint->parent ) { parentNum = md5Joint->parent - md5joints.Ptr(); common->RW()->DebugLine( colorWhite, ent->origin + ent->joints[parentNum].ToVec3() * ent->axis, pos ); } common->RW()->DebugLine( colorRed, pos, pos + joint->ToMat3()[0] * 2.0f * ent->axis ); common->RW()->DebugLine( colorGreen, pos, pos + joint->ToMat3()[1] * 2.0f * ent->axis ); common->RW()->DebugLine( colorBlue, pos, pos + joint->ToMat3()[2] * 2.0f * ent->axis ); } idBounds bounds; bounds.FromTransformedBounds( ent->bounds, vec3_zero, ent->axis ); common->RW()->DebugBounds( colorMagenta, bounds, ent->origin ); if( ( r_jointNameScale.GetFloat() != 0.0f ) && ( bounds.Expand( 128.0f ).ContainsPoint( view->renderView.vieworg - ent->origin ) ) ) { idVec3 offset( 0, 0, r_jointNameOffset.GetFloat() ); float scale; scale = r_jointNameScale.GetFloat(); joint = ent->joints; num = ent->numJoints; for( i = 0; i < num; i++, joint++ ) { pos = ent->origin + joint->ToVec3() * ent->axis; common->RW()->DrawText( md5joints[i].name, pos + offset, scale, colorWhite, view->renderView.viewaxis, 1 ); } } } static bool GatherBoneInfo( gltfData* data, gltfAnimation* gltfAnim, idList& bones, idList& jointInfo , gltfSkin* skin, const idImportOptions* options ) { bool boneLess = false; int targetNode = lastMeshFromFile->GetRootID(); auto targets = data->GetAnimTargets( gltfAnim ); auto& nodeList = data->NodeList(); if( skin == nullptr ) { boneLess = true; } // we cant be sure channels are sorted by bone? if( !boneLess ) { if( skin == nullptr ) { skin = data->GetSkin( targetNode ); } assert( skin ); // armature node is origin/root bone //bones.Append( skin->skeleton ); // skeleton bones bones.Append( skin->joints ); } else { bones.Append( targetNode ); } if( options ) { if( options->keepjoints.Num() ) { KeepNodes( data, options->keepjoints, bones ); } if( options->addOrigin ) { gltfNode* armatureNode = data->NodeList()[bones[0]]->parent; AddOriginBone( data, bones, armatureNode ); } if( options->remapjoints.Num() ) { RemapNodes( data, options->remapjoints, bones ); } if( options->renamejoints.Num() ) { RenameNodes( data, options->renamejoints, bones ); } } // create jointInfo jointInfo.SetGranularity( 1 ); jointInfo.SetNum( bones.Num() ); int idx = 0; for( auto& joint : jointInfo ) { joint.animBits = ~63; joint.firstComponent = -1; const char* name = nodeList[bones[idx++]]->name.c_str(); joint.nameIndex = animationLib.JointIndex( name ); } return boneLess; } static idList GetPose( idList& bones, idJointMat* poseMat, const idMat4& globalTransform ) { // resolve each glTF2 bone to world space and convert to idJointQuat idList ret; ret.AssureSize( bones.Num() ); for( int i = 0; i < bones.Num(); i++ ) { auto* node = &bones[i]; idMat4 trans = mat4_identity; gltfData::ResolveNodeMatrix( node, &trans ); if( node->parent == nullptr ) { // RB: FIXME double check this. probably needs to be reversed node->matrix *= globalTransform; trans = node->matrix; } idJointQuat& pose = ret[i]; pose.q = ( trans.ToMat3().ToQuat() ); pose.t = idVec3( trans[0][3], trans[1][3], trans[2][3] ); pose.w = pose.q.CalcW(); } // calculate the relative transform from each to bone to its parent and store to idJointMat for( int i = 0; i < bones.Num(); i++ ) { const gltfNode* joint = &bones[i]; idJointQuat* pose = &ret[i]; poseMat[i].SetRotation( pose->q.ToMat3() ); poseMat[i].SetTranslation( pose->t ); if( joint->parent ) { int parentNum = bones.FindIndex( *joint->parent ); pose->q = ( poseMat[i].ToMat3() * poseMat[parentNum].ToMat3().Transpose() ).ToQuat(); pose->t = ( poseMat[i].ToVec3() - poseMat[parentNum].ToVec3() ) * poseMat[parentNum].ToMat3().Transpose(); } } return ret; } static int CopyBones( gltfData* data, const idList& bones, idList& out ) { out.Clear(); auto nodes = data->NodeList(); for( auto jointId : bones ) { auto* newNode = &out.Alloc(); *newNode = *nodes[jointId]; } // patch parents for( auto& bone : out ) { bool found = false; for( int i = 0; i < out.Num(); i++ ) { if( bone.parent && bone.parent->name == out[i].name ) { bone.parent = &out[i]; found = true; break; } } if( !found ) { bone.parent = nullptr; } } return out.Num(); } idFile_Memory* idRenderModelGLTF::GetAnimBin( const idStr& animName, const ID_TIME_T sourceTimeStamp, const idImportOptions* options ) { assert( lastMeshFromFile ); // keep in sync with game! static const byte B_ANIM_MD5_VERSION = 101; static const unsigned int B_ANIM_MD5_MAGIC = ( 'B' << 24 ) | ( 'M' << 16 ) | ( 'D' << 8 ) | B_ANIM_MD5_VERSION; // convert animName to original glTF2 filename and load it GLTF_Parser gltf; int rootMotionCopyTargetId = -1; int id; idStr gltfFileName = idStr( animName ); idStr name; gltfManager::ExtractIdentifier( gltfFileName, id, name ); gltf.Load( gltfFileName ); gltfData* data = gltf.currentAsset; auto& accessors = data->AccessorList(); auto& nodes = data->NodeList(); idStr lastGltfFileName = idStr( lastMeshFromFile->name ); idStr lastName; gltfManager::ExtractIdentifier( lastGltfFileName, id, lastName ); gltfSkin* skin = nullptr; int rootID = -1; idStr rootName; FindModelRoot( data, options, rootName, &rootID, &skin ); if( rootID == -1 || !skin || ( skin && !skin->joints.Num() ) ) { common->Error( "Could not determine the Armature's rootID" ); return nullptr; } auto gltfAnim = data->GetAnimation( name, skin->joints[0] ); if( !gltfAnim ) { common->Warning( "Could not find action %s in %s !", name.c_str(), gltfFileName.c_str() ); return nullptr; } idList bones; idList jointInfo; bool boneLess = GatherBoneInfo( data, gltfAnim, bones, jointInfo, skin, options ); idList> animBones; idList componentFrames; idList baseFrame; idList bounds; int numFrames = 0; int frameRate = 0; int numJoints = bones.Num(); int numAnimatedComponents = 0; gameLocal.Printf( "Generating MD5Anim for GLTF anim %s from scene %s\n", name.c_str(), gltf_ModelSceneName.GetString() ); idMat4 globalTransform = blenderToDoomTransform; if( options ) { if( !options->transferRootMotion.IsEmpty() ) { gltfNode* target = data->GetNode( options->transferRootMotion ); if( !target ) { common->Warning( "Target bone to copy root motion from is not found" ); } rootMotionCopyTargetId = data->GetNodeIndex( target ); } const auto blenderToDoomRotation = idAngles( 0.0f, 0.0f, 90 ).ToMat3(); idMat3 rotationMat = blenderToDoomRotation; if( options->reOrient != ang_zero ) { rotationMat = options->reOrient.ToMat3(); } else if( options->rotate != 0 ) { rotationMat = idAngles( 0.0f, options->rotate, 90 ).ToMat3(); } float scale = options->scale; idMat3 scaleMat( scale, 0, 0, 0, scale, 0, 0, 0, scale ); globalTransform = idMat4( rotationMat * scaleMat, vec3_origin ); } // setup jointinfo's animbits for every joint that is animated int channelCount = 0; bool hasArmatureTransform = false; for( auto channel : gltfAnim->channels ) { int boneIndex = bones.FindIndex( channel->target.node ); if( boneIndex < 0 ) { continue; } auto* sampler = gltfAnim->samplers[channel->sampler]; auto* input = accessors[sampler->input]; auto* output = accessors[sampler->output]; auto* target = nodes[channel->target.node]; jointAnimInfo_t* newJoint = &( jointInfo[boneIndex] ); idList& timeStamps = data->GetAccessorView( input ); int frames = timeStamps.Num(); if( numFrames != 0 && numFrames != frames ) { common->Warning( "Not all channel animations have the same amount of frames" ); } if( frames > numFrames ) { numFrames = frames; } int parentIndex = data->GetNodeIndex( target->parent ); newJoint->nameIndex = animationLib.JointIndex( boneLess ? "origin" : target->name ); newJoint->parentNum = bones.FindIndex( parentIndex ); //assert( newJoint->parentNum >= 0 ); if( newJoint->firstComponent == -1 ) { newJoint->firstComponent = numAnimatedComponents; } switch( channel->target.TRS ) { default: break; case gltfAnimation_Channel_Target::none: break; case gltfAnimation_Channel_Target::rotation: newJoint->animBits |= ANIM_QX | ANIM_QY | ANIM_QZ; numAnimatedComponents += 3; if( !boneIndex ) { hasArmatureTransform = true; } break; case gltfAnimation_Channel_Target::translation: newJoint->animBits |= ANIM_TX | ANIM_TY | ANIM_TZ; numAnimatedComponents += 3; if( !boneIndex ) { hasArmatureTransform = true; } break; case gltfAnimation_Channel_Target::scale: // this is not supported by engine, but it should be for gltf break; } channelCount++; } if( options ) { if( options->addOrigin ) { // patch jointinfo when origin was inserted jointInfo[1].parentNum = 0; } // patch animbits for added joint when root motion is being transferred if( !options->transferRootMotion.IsEmpty() ) { jointAnimInfo_t* newJoint = &( jointInfo[0] ); newJoint->animBits |= ANIM_TX | ANIM_TY | ANIM_TZ; numAnimatedComponents += 3; newJoint->firstComponent = -3; for( auto& joint : jointInfo ) { joint.firstComponent += 3; } } } // create skeletons for each frame animBones.AssureSize( numFrames ); animBones.SetNum( numFrames ); for( int i = 0; i < numFrames; i++ ) { int totalCopied = CopyBones( data, bones, animBones[i] ); assert( totalCopied ); } gameLocal.Printf( "Total bones %i \n", bones.Num() ); // we can calculate frame rate by: // max_timestamp_value / totalFrames // but keeping it fixed for now. frameRate = gltf_AnimSampleRate.GetInteger(); int animLength = ( ( numFrames - 1 ) * 1000 + frameRate - 1 ) / frameRate; #if 0 for( int i = 0; i < jointInfo.Num(); i++ ) { jointAnimInfo_t& j = jointInfo[i]; idStr jointName = animationLib.JointName( j.nameIndex ); if( i == 0 && ( jointName != "origin" ) ) { gameLocal.Warning( "Renaming bone 0 from %s to %s \n", jointName.c_str(), "origin" ); jointName = "origin"; } } #endif baseFrame.SetGranularity( 1 ); baseFrame.SetNum( bones.Num() ); idJointMat* poseMat = ( idJointMat* ) _alloca16( bones.Num() * sizeof( poseMat[0] ) ); baseFrame = GetPose( animBones[0], poseMat, globalTransform ); componentFrames.SetGranularity( 1 ); componentFrames.SetNum( ( ( numAnimatedComponents * numFrames ) ) + 1 ); int componentFrameIndex = 0; for( int i = 0; i < numFrames; i++ ) { for( auto channel : gltfAnim->channels ) { int boneIndex = bones.FindIndex( channel->target.node ); if( boneIndex < 0 ) { continue; } auto sampler = gltfAnim->samplers[channel->sampler]; auto* input = accessors[sampler->input]; auto* output = accessors[sampler->output]; idList& timeStamps = data->GetAccessorView( input ); switch( channel->target.TRS ) { default: break; case gltfAnimation_Channel_Target::none: break; case gltfAnimation_Channel_Target::rotation: { idList& values = data->GetAccessorView( output ); if( values.Num() > i ) { animBones[i][boneIndex].rotation = *values[i]; } break; } case gltfAnimation_Channel_Target::translation: { idList& values = data->GetAccessorView( output ); if( values.Num() > i ) { if( channel->target.node == rootMotionCopyTargetId ) { animBones[i][boneIndex].translation.y = values[i]->y; animBones[i][0].translation = *values[i]; animBones[i][0].translation.y = 0; } else { animBones[i][boneIndex].translation = *values[i]; } } break; } case gltfAnimation_Channel_Target::scale: { idList& values = data->GetAccessorView( output ); if( values.Num() > i ) { animBones[i][boneIndex].scale = *values[i] ; } break; } } } for( int b = 0; b < bones.Num(); b++ ) { auto* node = &animBones[i][b]; jointAnimInfo_t* joint = &( jointInfo[b] ); gltfNode tmpNode = *node; if( joint->animBits & ( ANIM_QX | ANIM_QY | ANIM_QZ | ANIM_TX | ANIM_TY | ANIM_TZ ) ) { if( node->parent == nullptr ) { idMat4 trans = mat4_identity; gltfData::ResolveNodeMatrix( &tmpNode, &trans ); tmpNode.matrix *= globalTransform * tmpNode.matrix.Transpose(); tmpNode.rotation = ( tmpNode.matrix.ToMat3().ToQuat() ); tmpNode.translation = idVec3( trans[0][3], trans[1][3], trans[2][3] ); tmpNode.rotation.w = tmpNode.rotation.CalcW(); } else { tmpNode.rotation *= -tmpNode.rotation; } } idQuat q = tmpNode.rotation; idVec3 t = tmpNode.translation; if( joint->animBits & ( ANIM_TX | ANIM_TY | ANIM_TZ ) ) { if( !hasArmatureTransform && node->parent == nullptr ) { t = globalTransform * t; } componentFrames[componentFrameIndex++] = t.x; componentFrames[componentFrameIndex++] = t.y; componentFrames[componentFrameIndex++] = t.z; } if( joint->animBits & ( ANIM_QX | ANIM_QY | ANIM_QZ ) ) { if( !hasArmatureTransform ) { if( node->parent == nullptr ) { q = globalTransform.ToMat3().ToQuat() * animBones[i][b].rotation; } else { q = -animBones[i][b].rotation; } } componentFrames[componentFrameIndex++] = q.x; componentFrames[componentFrameIndex++] = q.y; componentFrames[componentFrameIndex++] = q.z; } } } assert( componentFrames.Num() == ( componentFrameIndex + 1 ) ); bounds.SetGranularity( 1 ); bounds.AssureSize( numFrames ); bounds.SetNum( numFrames ); // do software skinning to determine bounds. idJointMat* currJoints = ( idJointMat* ) _alloca16( bones.Num() * sizeof( poseMat[0] ) ); for( int i = 0; i < numFrames; i++ ) { bounds[i].Clear(); for( int b = 0; b < animBones[i].Num(); b++ ) { if( animBones[i][b].parent == nullptr ) { animBones[i][b].translation = globalTransform * animBones[i][b].translation; } } idList joints; GetPose( animBones[i], currJoints, globalTransform ); for( int b = 0; b < animBones[i].Num(); b++ ) { idJointMat mat = poseMat[b]; mat.Invert(); idJointMat::Multiply( joints.Alloc(), currJoints[b], mat ); } // an mesh entry _should_ always be before an anim entry! // use those verts as base. if( lastMeshFromFile != nullptr ) { for( modelSurface_t& surf : lastMeshFromFile->surfaces ) { idDrawVert* verts = surf.geometry->verts; int numVerts = surf.geometry->numVerts; for( int v = 0; v < numVerts; v++ ) { const idDrawVert& base = verts[v]; const idJointMat& j0 = joints[base.color[0]]; const idJointMat& j1 = joints[base.color[1]]; const idJointMat& j2 = joints[base.color[2]]; const idJointMat& j3 = joints[base.color[3]]; const float w0 = base.color2[0] * ( 1.0f / 255.0f ); const float w1 = base.color2[1] * ( 1.0f / 255.0f ); const float w2 = base.color2[2] * ( 1.0f / 255.0f ); const float w3 = base.color2[3] * ( 1.0f / 255.0f ); idJointMat accum; idJointMat::Mul( accum, j0, w0 ); idJointMat::Mad( accum, j1, w1 ); idJointMat::Mad( accum, j2, w2 ); idJointMat::Mad( accum, j3, w3 ); idVec3 pos = accum * idVec4( base.xyz.x, base.xyz.y, base.xyz.z, 1.0f ); bounds[i].AddPoint( pos ); } } } } ////////////////////////////////////////////////////////////////////////// /// Start writing //////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////// idFile_Memory* file = new idFile_Memory(); file->WriteBig( B_ANIM_MD5_MAGIC ); file->WriteBig( sourceTimeStamp ); file->WriteBig( numFrames ); file->WriteBig( frameRate ); file->WriteBig( animLength ); file->WriteBig( numJoints ); file->WriteBig( numAnimatedComponents ); file->WriteBig( bounds.Num() ); for( int i = 0; i < bounds.Num(); i++ ) { idBounds& b = bounds[i]; file->WriteBig( b[0] ); file->WriteBig( b[1] ); } // namestr list file->WriteBig( jointInfo.Num() ); for( int i = 0; i < jointInfo.Num(); i++ ) { jointAnimInfo_t& j = jointInfo[i]; idStr jointName = animationLib.JointName( j.nameIndex ); file->WriteString( jointName ); file->WriteBig( j.parentNum ); file->WriteBig( j.animBits ); file->WriteBig( j.firstComponent ); } // base frame file->WriteBig( baseFrame.Num() ); for( int i = 0; i < baseFrame.Num(); i++ ) { idJointQuat& j = baseFrame[i]; file->WriteBig( j.q.x ); file->WriteBig( j.q.y ); file->WriteBig( j.q.z ); file->WriteBig( j.q.w ); file->WriteVec3( j.t ); } // per joint timestamp values, T R file->WriteBig( componentFrames.Num() - 1 ); for( int i = 0; i < componentFrames.Num(); i++ ) { file->WriteFloat( componentFrames[i] ); } float* componentPtr = componentFrames.Ptr(); idVec3 totaldelta; // get total move delta if( !numAnimatedComponents ) { totaldelta.Zero(); } else { if( jointInfo[0].animBits & ( ANIM_QX | ANIM_QY | ANIM_QZ | ANIM_TX | ANIM_TY | ANIM_TZ ) ) { componentPtr = &componentFrames[jointInfo[0].firstComponent]; } // if there is root movement on a different bone , for example when adding a root bone, this wil fail. if( jointInfo[0].animBits & ANIM_TX ) { for( int i = 0; i < numFrames; i++ ) { componentPtr[numAnimatedComponents * i] -= baseFrame[0].t.x; } totaldelta.x = componentPtr[numAnimatedComponents * ( numFrames - 1 )]; componentPtr++; } else { totaldelta.x = 0.0f; } if( jointInfo[0].animBits & ANIM_TY ) { for( int i = 0; i < numFrames; i++ ) { componentPtr[numAnimatedComponents * i] -= baseFrame[0].t.y; } totaldelta.y = componentPtr[numAnimatedComponents * ( numFrames - 1 )]; componentPtr++; } else { totaldelta.y = 0.0f; } if( jointInfo[0].animBits & ANIM_TZ ) { for( int i = 0; i < numFrames; i++ ) { componentPtr[numAnimatedComponents * i] -= baseFrame[0].t.z; } totaldelta.z = componentPtr[numAnimatedComponents * ( numFrames - 1 )]; } else { totaldelta.z = 0.0f; } } file->WriteVec3( totaldelta ); file->Seek( 0, FS_SEEK_SET ); file->TakeDataOwnership(); common->UpdateLevelLoadPacifier(); return file; } void idRenderModelGLTF::WriteBinaryModel( idFile* file, ID_TIME_T* _timeStamp /*= NULL */ ) const { idRenderModelStatic::WriteBinaryModel( file , _timeStamp ); if( file == NULL ) { return; } file->WriteBig( GLMB_MAGIC ); file->WriteString( commandLine ); file->WriteBig( model_state ); file->WriteBig( rootID ); file->WriteString( file->GetName() ); file->WriteBig( animIds.Num() ); if( animIds.Num() ) { file->WriteBigArray( animIds.Ptr(), animIds.Num() ); } file->WriteBig( md5joints.Num() ); for( int i = 0; i < md5joints.Num(); i++ ) { file->WriteString( md5joints[i].name ); int offset = -1; if( md5joints[i].parent != NULL ) { offset = md5joints[i].parent - md5joints.Ptr(); } file->WriteBig( offset ); } file->WriteBig( bones.Num() ); if( bones.Num() ) { file->WriteBigArray( bones.Ptr(), bones.Num() ); } file->WriteBig( defaultPose.Num() ); for( int i = 0; i < defaultPose.Num(); i++ ) { file->WriteBig( defaultPose[i].q.x ); file->WriteBig( defaultPose[i].q.y ); file->WriteBig( defaultPose[i].q.z ); file->WriteBig( defaultPose[i].q.w ); file->WriteVec3( defaultPose[i].t ); } file->WriteBig( invertedDefaultPose.Num() ); for( int i = 0; i < invertedDefaultPose.Num(); i++ ) { file->WriteBigArray( invertedDefaultPose[i].ToFloatPtr(), JOINTMAT_TYPESIZE ); } common->UpdateLevelLoadPacifier(); } void idRenderModelGLTF::PurgeModel() { idRenderModelStatic::PurgeModel(); purged = true; md5joints.Clear(); defaultPose.Clear(); invertedDefaultPose.Clear(); animIds.Clear(); bones.Clear(); MeshNodeIds.Clear(); gltfFileName.Clear(); // RB: keep rootName for reloadModels because we don't have options there //rootName.Clear(); // if no root id was set, it is a generated one. if( rootID == -1 && root ) { delete root; } data = nullptr; } void idRenderModelGLTF::LoadModel() { int num; auto& accessors = data->AccessorList(); auto& nodes = data->NodeList(); gltfNode* modelRoot = root; if( !fileExclusive ) { modelRoot = root = FindModelRoot( data, nullptr, rootName, &rootID, nullptr ); } num = bones.Num(); md5joints.SetGranularity( 1 ); md5joints.SetNum( num ); defaultPose.SetGranularity( 1 ); defaultPose.SetNum( num ); for( int i = 0; i < bones.Num(); i++ ) { gltfNode* node = nodes[bones[i]]; // check for TRS anim and its artficial root bone if( bones.Num() == 0 && node->mesh != -1 ) { assert( 0 ); //md5joints[i].name = "origin"; } else { md5joints[i].name = node->name; } } for( int i = 0; i < bones.Num(); i++ ) { auto* node = nodes[bones[i]]; if( node->parent && node->parent != root ) { md5joints[i].parent = FindMD5Joint( node->parent->name ); } } idJointMat* poseMat = ( idJointMat* ) _alloca16( bones.Num() * sizeof( poseMat[0] ) ); idList animBones; int totalCopied = CopyBones( data, bones, animBones ); defaultPose = GetPose( animBones, poseMat, globalTransform ); //----------------------------------------- // create the inverse of the base pose joints to support tech6 style deformation // of base pose vertexes, normals, and tangents. // // vertex * joints * inverseJoints == vertex when joints is the base pose // When the joints are in another pose, it gives the animated vertex position //----------------------------------------- invertedDefaultPose.SetNum( SIMD_ROUND_JOINTS( md5joints.Num() ) ); for( int i = 0; i < md5joints.Num(); i++ ) { invertedDefaultPose[i] = poseMat[i]; invertedDefaultPose[i].Invert(); } SIMD_INIT_LAST_JOINT( invertedDefaultPose.Ptr(), md5joints.Num() ); //auto deformInfo = R_BuildDeformInfo( texCoords.Num(), basePose, tris.Num(), tris.Ptr(), // shader->UseUnsmoothedTangents() ); model_state = hasAnimations ? DM_CACHED : DM_STATIC; // set the timestamp for reloadmodels fileSystem->ReadFile( name, NULL, &timeStamp ); purged = false; common->UpdateLevelLoadPacifier(); } void idRenderModelGLTF::TouchData() { for( int i = 0; i < surfaces.Num(); i++ ) { declManager->FindMaterial( surfaces[i].shader->GetName() ); } } void idRenderModelGLTF::Print() const { idRenderModelStatic::Print(); // TODO } void idRenderModelGLTF::List() const { idRenderModelStatic::List(); // TODO } int idRenderModelGLTF::Memory() const { return idRenderModelStatic::Memory(); // TODO } dynamicModel_t idRenderModelGLTF::IsDynamicModel() const { return model_state; } idList TransformVertsAndTangents_GLTF( idDrawVert* targetVerts, const int numVerts, const idDrawVert* baseVerts, const idJointMat* joints ) { idList jointIds; for( int i = 0; i < numVerts; i++ ) { const idDrawVert& base = baseVerts[i]; const idJointMat& j0 = joints[base.color[0]]; const idJointMat& j1 = joints[base.color[1]]; const idJointMat& j2 = joints[base.color[2]]; const idJointMat& j3 = joints[base.color[3]]; for( int j = 0; j < 4; j++ ) { jointIds.AddUnique( base.color[j] ); } const float w0 = base.color2[0] * ( 1.0f / 255.0f ); const float w1 = base.color2[1] * ( 1.0f / 255.0f ); const float w2 = base.color2[2] * ( 1.0f / 255.0f ); const float w3 = base.color2[3] * ( 1.0f / 255.0f ); idJointMat accum; idJointMat::Mul( accum, j0, w0 ); idJointMat::Mad( accum, j1, w1 ); idJointMat::Mad( accum, j2, w2 ); idJointMat::Mad( accum, j3, w3 ); targetVerts[i].xyz = accum * idVec4( base.xyz.x, base.xyz.y, base.xyz.z, 1.0f ); targetVerts[i].SetNormal( accum * base.GetNormal() ); targetVerts[i].SetTangent( accum * base.GetTangent() ); targetVerts[i].tangent[3] = base.tangent[3]; } return jointIds; } void idRenderModelGLTF::UpdateSurface( const struct renderEntity_s* ent, const idJointMat* entJoints, const idJointMat* entJointsInverted, modelSurface_t* surf, const modelSurface_t& sourceSurf ) { #if defined(USE_INTRINSICS_SSE) static const __m128 vector_float_posInfinity = { idMath::INFINITUM, idMath::INFINITUM, idMath::INFINITUM, idMath::INFINITUM }; static const __m128 vector_float_negInfinity = { -idMath::INFINITUM, -idMath::INFINITUM, -idMath::INFINITUM, -idMath::INFINITUM }; #endif // add skinning if( surf->geometry != NULL ) { R_FreeStaticTriSurfVertexCaches( surf->geometry ); } else { surf->geometry = R_AllocStaticTriSurf(); } srfTriangles_t* tri = surf->geometry; int numVerts = sourceSurf.geometry->numVerts; idDrawVert* verts = sourceSurf.geometry->verts; tri->referencedIndexes = true; tri->numIndexes = sourceSurf.geometry->numIndexes; tri->indexes = sourceSurf.geometry->indexes; tri->silIndexes = sourceSurf.geometry->silIndexes; tri->numMirroredVerts = sourceSurf.geometry->numMirroredVerts; tri->mirroredVerts = sourceSurf.geometry->mirroredVerts; tri->numDupVerts = sourceSurf.geometry->numDupVerts; tri->dupVerts = sourceSurf.geometry->dupVerts; tri->indexCache = sourceSurf.geometry->indexCache; tri->numVerts = numVerts; idList jointIds; if( r_useGPUSkinning.GetBool() ) { if( tri->verts != NULL && tri->verts != verts ) { R_FreeStaticTriSurfVerts( tri ); } tri->verts = verts; tri->ambientCache = sourceSurf.geometry->ambientCache; tri->referencedVerts = true; } else { if( tri->verts == NULL || tri->verts == verts ) { tri->verts = NULL; R_AllocStaticTriSurfVerts( tri, numVerts ); assert( tri->verts != NULL ); // quiet analyze warning memcpy( tri->verts, verts, numVerts * sizeof( verts[0] ) ); // copy over the texture coordinates tri->referencedVerts = false; } jointIds = TransformVertsAndTangents_GLTF( tri->verts, numVerts, verts, entJointsInverted ); } tri->tangentsCalculated = true; // calculate bounds #if defined(USE_INTRINSICS_SSE) __m128 minX = vector_float_posInfinity; __m128 minY = vector_float_posInfinity; __m128 minZ = vector_float_posInfinity; __m128 maxX = vector_float_negInfinity; __m128 maxY = vector_float_negInfinity; __m128 maxZ = vector_float_negInfinity; for( int i = 0; i < md5joints.Num(); i++ ) { const idJointMat& joint = entJoints[i]; __m128 x = _mm_load_ps( joint.ToFloatPtr() + 0 * 4 ); __m128 y = _mm_load_ps( joint.ToFloatPtr() + 1 * 4 ); __m128 z = _mm_load_ps( joint.ToFloatPtr() + 2 * 4 ); minX = _mm_min_ps( minX, x ); minY = _mm_min_ps( minY, y ); minZ = _mm_min_ps( minZ, z ); maxX = _mm_max_ps( maxX, x ); maxY = _mm_max_ps( maxY, y ); maxZ = _mm_max_ps( maxZ, z ); } __m128 expand = _mm_splat_ps( _mm_load_ss( &maxJointVertDist ), 0 ); minX = _mm_sub_ps( minX, expand ); minY = _mm_sub_ps( minY, expand ); minZ = _mm_sub_ps( minZ, expand ); maxX = _mm_add_ps( maxX, expand ); maxY = _mm_add_ps( maxY, expand ); maxZ = _mm_add_ps( maxZ, expand ); _mm_store_ss( tri->bounds.ToFloatPtr() + 0, _mm_splat_ps( minX, 3 ) ); _mm_store_ss( tri->bounds.ToFloatPtr() + 1, _mm_splat_ps( minY, 3 ) ); _mm_store_ss( tri->bounds.ToFloatPtr() + 2, _mm_splat_ps( minZ, 3 ) ); _mm_store_ss( tri->bounds.ToFloatPtr() + 3, _mm_splat_ps( maxX, 3 ) ); _mm_store_ss( tri->bounds.ToFloatPtr() + 4, _mm_splat_ps( maxY, 3 ) ); _mm_store_ss( tri->bounds.ToFloatPtr() + 5, _mm_splat_ps( maxZ, 3 ) ); #else bounds.Clear(); for( int i = 0; i < jointIds.Num(); i++ ) { const idJointMat& joint = entJoints[i]; bounds.AddPoint( joint.GetTranslation() ); } bounds.ExpandSelf( maxJointVertDist ); #endif } /* ==================== TransformJoints ==================== */ static void TransformJointsFast( idJointMat* __restrict outJoints, const int numJoints, const idJointMat* __restrict inJoints1, const idJointMat* __restrict inJoints2 ) { float* outFloats = outJoints->ToFloatPtr(); const float* inFloats1 = inJoints1->ToFloatPtr(); const float* inFloats2 = inJoints2->ToFloatPtr(); assert_16_byte_aligned( outFloats ); assert_16_byte_aligned( inFloats1 ); assert_16_byte_aligned( inFloats2 ); #if defined(USE_INTRINSICS_SSE) const __m128 mask_keep_last = __m128c( _mm_set_epi32( 0xFFFFFFFF, 0x00000000, 0x00000000, 0x00000000 ) ); for( int i = 0; i < numJoints; i += 2, inFloats1 += 2 * 12, inFloats2 += 2 * 12, outFloats += 2 * 12 ) { __m128 m1a0 = _mm_load_ps( inFloats1 + 0 * 12 + 0 ); __m128 m1b0 = _mm_load_ps( inFloats1 + 0 * 12 + 4 ); __m128 m1c0 = _mm_load_ps( inFloats1 + 0 * 12 + 8 ); __m128 m1a1 = _mm_load_ps( inFloats1 + 1 * 12 + 0 ); __m128 m1b1 = _mm_load_ps( inFloats1 + 1 * 12 + 4 ); __m128 m1c1 = _mm_load_ps( inFloats1 + 1 * 12 + 8 ); __m128 m2a0 = _mm_load_ps( inFloats2 + 0 * 12 + 0 ); __m128 m2b0 = _mm_load_ps( inFloats2 + 0 * 12 + 4 ); __m128 m2c0 = _mm_load_ps( inFloats2 + 0 * 12 + 8 ); __m128 m2a1 = _mm_load_ps( inFloats2 + 1 * 12 + 0 ); __m128 m2b1 = _mm_load_ps( inFloats2 + 1 * 12 + 4 ); __m128 m2c1 = _mm_load_ps( inFloats2 + 1 * 12 + 8 ); __m128 tj0 = _mm_and_ps( m1a0, mask_keep_last ); __m128 tk0 = _mm_and_ps( m1b0, mask_keep_last ); __m128 tl0 = _mm_and_ps( m1c0, mask_keep_last ); __m128 tj1 = _mm_and_ps( m1a1, mask_keep_last ); __m128 tk1 = _mm_and_ps( m1b1, mask_keep_last ); __m128 tl1 = _mm_and_ps( m1c1, mask_keep_last ); __m128 ta0 = _mm_splat_ps( m1a0, 0 ); __m128 td0 = _mm_splat_ps( m1b0, 0 ); __m128 tg0 = _mm_splat_ps( m1c0, 0 ); __m128 ta1 = _mm_splat_ps( m1a1, 0 ); __m128 td1 = _mm_splat_ps( m1b1, 0 ); __m128 tg1 = _mm_splat_ps( m1c1, 0 ); __m128 ra0 = _mm_add_ps( tj0, _mm_mul_ps( ta0, m2a0 ) ); __m128 rd0 = _mm_add_ps( tk0, _mm_mul_ps( td0, m2a0 ) ); __m128 rg0 = _mm_add_ps( tl0, _mm_mul_ps( tg0, m2a0 ) ); __m128 ra1 = _mm_add_ps( tj1, _mm_mul_ps( ta1, m2a1 ) ); __m128 rd1 = _mm_add_ps( tk1, _mm_mul_ps( td1, m2a1 ) ); __m128 rg1 = _mm_add_ps( tl1, _mm_mul_ps( tg1, m2a1 ) ); __m128 tb0 = _mm_splat_ps( m1a0, 1 ); __m128 te0 = _mm_splat_ps( m1b0, 1 ); __m128 th0 = _mm_splat_ps( m1c0, 1 ); __m128 tb1 = _mm_splat_ps( m1a1, 1 ); __m128 te1 = _mm_splat_ps( m1b1, 1 ); __m128 th1 = _mm_splat_ps( m1c1, 1 ); __m128 rb0 = _mm_add_ps( ra0, _mm_mul_ps( tb0, m2b0 ) ); __m128 re0 = _mm_add_ps( rd0, _mm_mul_ps( te0, m2b0 ) ); __m128 rh0 = _mm_add_ps( rg0, _mm_mul_ps( th0, m2b0 ) ); __m128 rb1 = _mm_add_ps( ra1, _mm_mul_ps( tb1, m2b1 ) ); __m128 re1 = _mm_add_ps( rd1, _mm_mul_ps( te1, m2b1 ) ); __m128 rh1 = _mm_add_ps( rg1, _mm_mul_ps( th1, m2b1 ) ); __m128 tc0 = _mm_splat_ps( m1a0, 2 ); __m128 tf0 = _mm_splat_ps( m1b0, 2 ); __m128 ti0 = _mm_splat_ps( m1c0, 2 ); __m128 tf1 = _mm_splat_ps( m1b1, 2 ); __m128 ti1 = _mm_splat_ps( m1c1, 2 ); __m128 tc1 = _mm_splat_ps( m1a1, 2 ); __m128 rc0 = _mm_add_ps( rb0, _mm_mul_ps( tc0, m2c0 ) ); __m128 rf0 = _mm_add_ps( re0, _mm_mul_ps( tf0, m2c0 ) ); __m128 ri0 = _mm_add_ps( rh0, _mm_mul_ps( ti0, m2c0 ) ); __m128 rc1 = _mm_add_ps( rb1, _mm_mul_ps( tc1, m2c1 ) ); __m128 rf1 = _mm_add_ps( re1, _mm_mul_ps( tf1, m2c1 ) ); __m128 ri1 = _mm_add_ps( rh1, _mm_mul_ps( ti1, m2c1 ) ); _mm_store_ps( outFloats + 0 * 12 + 0, rc0 ); _mm_store_ps( outFloats + 0 * 12 + 4, rf0 ); _mm_store_ps( outFloats + 0 * 12 + 8, ri0 ); _mm_store_ps( outFloats + 1 * 12 + 0, rc1 ); _mm_store_ps( outFloats + 1 * 12 + 4, rf1 ); _mm_store_ps( outFloats + 1 * 12 + 8, ri1 ); } #else for( int i = 0; i < numJoints; i++ ) { idJointMat::Multiply( outJoints[i], inJoints1[i], inJoints2[i] ); } #endif } idRenderModel* idRenderModelGLTF::InstantiateDynamicModel( const struct renderEntity_s* ent, const viewDef_t* view, idRenderModel* cachedModel ) { if( cachedModel != NULL && !r_useCachedDynamicModels.GetBool() ) { delete cachedModel; cachedModel = NULL; } if( purged ) { common->DWarning( "model %s instantiated while purged", Name() ); LoadModel(); } if( !ent->joints ) { common->Printf( "idRenderModelGLTF::InstantiateDynamicModel: NULL joints on renderEntity for '%s'\n", Name() ); delete cachedModel; return NULL; } else if( ent->numJoints != md5joints.Num() ) { common->Printf( "idRenderModelGLTF::InstantiateDynamicModel: renderEntity has different number of joints than model for '%s'\n", Name() ); delete cachedModel; return NULL; } idRenderModelStatic* staticModel; if( cachedModel != NULL ) { assert( dynamic_cast< idRenderModelStatic* >( cachedModel ) != NULL ); assert( idStr::Icmp( cachedModel->Name(), GLTF_SnapshotName ) == 0 ); staticModel = static_cast< idRenderModelStatic* >( cachedModel ); } else { staticModel = new( TAG_MODEL ) idRenderModelStatic; staticModel->InitEmpty( GLTF_SnapshotName ); staticModel->jointsInverted = NULL;; } staticModel->bounds.Clear(); if( r_showSkel.GetInteger() ) { if( ( view != NULL ) && ( !r_skipSuppress.GetBool() || !ent->suppressSurfaceInViewID || ( ent->suppressSurfaceInViewID != view->renderView.viewID ) ) ) { // only draw the skeleton DrawJoints( ent, view ); } if( r_showSkel.GetInteger() > 1 ) { // turn off the model when showing the skeleton staticModel->InitEmpty( GLTF_SnapshotName ); return staticModel; } } // update the GPU joints array const int numInvertedJoints = SIMD_ROUND_JOINTS( md5joints.Num() ); if( staticModel->jointsInverted == NULL ) { staticModel->numInvertedJoints = numInvertedJoints; staticModel->jointsInverted = ( idJointMat* ) Mem_ClearedAlloc( numInvertedJoints * sizeof( idJointMat ), TAG_JOINTMAT ); staticModel->jointsInvertedBuffer = 0; } else { assert( staticModel->numInvertedJoints == numInvertedJoints ); } TransformJointsFast( staticModel->jointsInverted, md5joints.Num(), ent->joints, invertedDefaultPose.Ptr() ); if( !staticModel->surfaces.Num() ) { for( int i = 0 ; i < surfaces.Num(); i++ ) { modelSurface_t* newSurf = &staticModel->surfaces.Alloc(); newSurf->geometry = NULL; newSurf->shader = surfaces[i].shader; } } int surfIdx = 0; for( modelSurface_t& surf : staticModel->surfaces ) { const idMaterial* shader = surf.shader; shader = R_RemapShaderBySkin( shader, ent->customSkin, ent->customShader ); if( !shader || ( !shader->IsDrawn() && !shader->SurfaceCastsShadow() ) ) { staticModel->DeleteSurfaceWithId( surfIdx++ ); continue; } UpdateSurface( ent, ent->joints, staticModel->jointsInverted, &surf, surfaces[surfIdx++] ); assert( surf.geometry != NULL ); surf.geometry->staticModelWithJoints = staticModel; staticModel->bounds.AddBounds( surf.geometry->bounds ); } return staticModel; } int idRenderModelGLTF::NumJoints() const { return bones.Num(); } const idMD5Joint* idRenderModelGLTF::GetJoints() const { idMD5Joint* result = nullptr; if( md5joints.Num() ) { return &md5joints[0]; } else { common->Warning( "GltfModel has no Joints" ); return nullptr; } } jointHandle_t idRenderModelGLTF::GetJointHandle( const char* name ) const { const idMD5Joint* joint = md5joints.Ptr(); for( int i = 0; i < md5joints.Num(); i++, joint++ ) { if( idStr::Icmp( joint->name.c_str(), name ) == 0 ) { return ( jointHandle_t ) i; } } return INVALID_JOINT; } const char* idRenderModelGLTF::GetJointName( jointHandle_t handle ) const { if( ( handle < 0 ) || ( handle >= md5joints.Num() ) ) { return ""; } return md5joints[handle].name; } const idJointQuat* idRenderModelGLTF::GetDefaultPose() const { return defaultPose.Ptr(); } int idRenderModelGLTF::NearestJoint( int surfaceNum, int a, int b, int c ) const { for( const modelSurface_t& surf : surfaces ) { idDrawVert* verts = surf.geometry->verts; int numVerts = surf.geometry->numVerts; for( int v = 0; v < numVerts; v++ ) { // duplicated vertices might not have weights int vertNum; if( a >= 0 && a < numVerts ) { vertNum = a; } else if( b >= 0 && b < numVerts ) { vertNum = b; } else if( c >= 0 && c < numVerts ) { vertNum = c; } else { // all vertices are duplicates which shouldn't happen return 0; } const idDrawVert& vert = verts[vertNum]; int bestWeight = 0; int bestJoint = 0; for( int i = 0; i < 4; i++ ) { if( vert.color2[i] > bestWeight ) { bestWeight = vert.color2[i]; bestJoint = vert.color[i]; } } return bestJoint; } } common->Warning( "Couldn't find NearestJoint for : '%s'", name.c_str() ); return 0; } idBounds idRenderModelGLTF::Bounds( const struct renderEntity_s* ent ) const { if( ent == NULL ) { // this is the bounds for the reference pose return bounds; } return ent->bounds; }