package electrosphere.server.poseactor; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.joml.Matrix4d; import org.joml.Vector3d; import org.lwjgl.PointerBuffer; import org.lwjgl.assimp.AIAnimation; import org.lwjgl.assimp.AIBone; import org.lwjgl.assimp.AIMesh; import org.lwjgl.assimp.AINode; import org.lwjgl.assimp.AIScene; import electrosphere.engine.Globals; import electrosphere.logger.LoggerInterface; import electrosphere.renderer.actor.ActorBoneRotator; import electrosphere.renderer.actor.ActorStaticMorph; import electrosphere.renderer.anim.AnimChannel; import electrosphere.renderer.anim.AnimNode; import electrosphere.renderer.anim.Animation; import electrosphere.renderer.loading.ModelPretransforms; import electrosphere.renderer.model.Bone; /** * Used server side to load model data for positioning hitboxes based on animations */ public class PoseModel { List bones; Map boneMap; Matrix4d globalInverseTransform; Matrix4d rootTransform; Map nodeMap; AnimNode rootAnimNode; List animations; Map animMap; /** * Private constructor for static creation methods */ private PoseModel(){ } /** * Constructor * @param path Path on disk to this posemodel * @param scene The AI Scene parsed from the file on disk */ public PoseModel(String path, AIScene scene){ ModelPretransforms.ModelMetadata modelMetadata = Globals.modelPretransforms.getModel(path); ModelPretransforms.GlobalTransform globalTransform = null; if(modelMetadata != null){ globalTransform = modelMetadata.getGlobalTransform(); } bones = new ArrayList(); boneMap = new HashMap(); nodeMap = new HashMap(); animations = new ArrayList(); animMap = new HashMap(); // //parse bones // PointerBuffer meshesBuffer = scene.mMeshes(); while(meshesBuffer.hasRemaining()){ AIMesh currentMeshData = AIMesh.createSafe(meshesBuffer.get()); LoggerInterface.loggerRenderer.DEBUG("mesh name:" + currentMeshData.mName().dataString()); PointerBuffer boneBuffer = currentMeshData.mBones(); for(int j = 0; j < currentMeshData.mNumBones(); j++){ AIBone currentBone = AIBone.createSafe(boneBuffer.get()); String currentBoneName = currentBone.mName().dataString(); if(!boneMap.containsKey(currentBoneName)){ Bone boneObject = new Bone(currentBone); boneMap.put(currentBoneName, boneObject); bones.add(boneObject); } } } // //parse animation nodes and form hierarchy // AINode rootNode = scene.mRootNode(); rootTransform = electrosphere.util.Utilities.convertAIMatrixd(rootNode.mTransformation()); if(globalTransform != null){ globalInverseTransform = new Matrix4d(rootTransform).invert().scale(globalTransform.getScale()); globalInverseTransform.scale(globalTransform.getScale()); } else { globalInverseTransform = new Matrix4d(); } rootAnimNode = buildAnimNodeMap(scene.mRootNode(),null); // //load animations // int animCount = scene.mNumAnimations(); PointerBuffer animBuffer = scene.mAnimations(); animations = new ArrayList(); animMap = new HashMap(); for(int i = 0; i < animCount; i++){ Animation newAnim = new Animation(AIAnimation.create(animBuffer.get(i))); animations.add(newAnim); animMap.put(newAnim.name,newAnim); } } /** * Constructor * @param path Path on disk to this posemodel * @param scene The AI Scene parsed from the file on disk */ public static PoseModel createEmpty(){ PoseModel rVal = new PoseModel(); rVal.bones = new ArrayList(); rVal.boneMap = new HashMap(); rVal.nodeMap = new HashMap(); rVal.animations = new ArrayList(); rVal.animMap = new HashMap(); return rVal; } /** * Applies an animation to a certain set of bones * @param animationName The name of the animation * @param time The time in the animation to apply * @param mask The set of bones to apply the animation to */ public void applyAnimationMask(String animationName, double time, List mask){ Animation animationCurrent = animMap.get(animationName); if(animationCurrent != null){ for(String boneName : mask){ AnimChannel currentChannel = animationCurrent.getChannel(boneName); Bone currentBone = boneMap.get(currentChannel.getNodeID()); currentChannel.setTime(time); // System.out.println(currentChannel + " " + currentBone); if(currentBone != null){ // System.out.println("Applying to bone"); //T * S * R Matrix4d deform = new Matrix4d(); deform.translate(currentChannel.getCurrentPosition()); deform.rotate(currentChannel.getCurrentRotation()); deform.scale(new Vector3d(currentChannel.getCurrentScale())); currentBone.setDeform(deform); } } } } /** * Builds an AnimNode map based on an AINode from assimp * @param node The AINode from assimp * @param parent The parent AnimNode if it exists * @return The AnimNode map relative to this node */ public final AnimNode buildAnimNodeMap(AINode node, AnimNode parent){ AnimNode node_object = new AnimNode(node.mName().dataString(), parent, node); nodeMap.put(node_object.id, node_object); if(boneMap.containsKey(node_object.id)){ node_object.is_bone = true; } int num_children = node.mNumChildren(); for(int i = 0; i < num_children; i++){ AnimNode temp_child = buildAnimNodeMap(AINode.create(node.mChildren().get(i)),node_object); node_object.children.add(temp_child); } return node_object; } /** * Updates node transforms based on bone rotators and static morph * @param boneRotators The bone rotators * @param staticMorph The static morph */ public void updateNodeTransform(Map boneRotators, ActorStaticMorph staticMorph){ if(this.rootAnimNode != null){ updateNodeTransform(this.rootAnimNode,boneRotators,staticMorph); } } /** * Gets the map of bone name -> bone * @return the map */ public Map getBoneMap(){ return boneMap; } /** * Internal recursive method behind the public updateNodeTransform * @param boneRotators The bone rotators * @param staticMorph The static morph */ void updateNodeTransform(AnimNode n, Map boneRotators, ActorStaticMorph staticMorph){ //grab parent transform if exists Matrix4d parentTransform = new Matrix4d(); if(n.parent != null){ parentTransform = new Matrix4d(n.parent.getTransform()); } //if this is a bone, calculate the transform for the bone if(n.is_bone){ Bone target_bone = boneMap.get(n.id); Matrix4d deformTransform = parentTransform.mul(target_bone.getDeform()); if(boneRotators.containsKey(target_bone.boneID)){ deformTransform.rotate(boneRotators.get(target_bone.boneID).getRotation()); } Matrix4d bone_matrix = new Matrix4d(deformTransform); if(staticMorph != null && staticMorph.getBoneTransforms(n.id) != null){ bone_matrix.mul(staticMorph.getBoneTransforms(n.id).getTransform()); } n.setTransform(deformTransform); // //Calculate final offset from initial bone //https://stackoverflow.com/a/59869381 bone_matrix.mul(target_bone.getMOffset()); bone_matrix = new Matrix4d(globalInverseTransform).mul(bone_matrix); target_bone.setFinalTransform(bone_matrix); } else { //not a bone, so use transform directly from data n.setTransform(parentTransform.mul(electrosphere.util.Utilities.convertAIMatrix(n.raw_data.mTransformation()))); } //update all children accordingly Iterator node_iterator = n.children.iterator(); while(node_iterator.hasNext()){ AnimNode current_node = node_iterator.next(); updateNodeTransform(current_node,boneRotators,staticMorph); } } /** * Gets an animation with a specific name * @param animName The name of the animation * @return The animation if it exists, null otherwise */ public Animation getAnimation(String animName){ return animMap.get(animName); } /** * Logs all animations for a given model */ public void describeAllAnimations(){ if(animations.size() > 0){ LoggerInterface.loggerRenderer.DEBUG("====================="); LoggerInterface.loggerRenderer.DEBUG(animations.size() + " animations available in model!"); Iterator animIterator = animations.iterator(); while(animIterator.hasNext()){ Animation currentAnim = animIterator.next(); currentAnim.describeAnimation(); } } } /** * Gets the list of bones in this pose model * @return The list of bones */ public List getBones(){ return this.bones; } }