package electrosphere.renderer.model; import electrosphere.renderer.RenderPipelineState; import electrosphere.renderer.actor.ActorBoneRotator; import electrosphere.renderer.actor.ActorMeshMask; import electrosphere.renderer.actor.ActorShaderMask; import electrosphere.renderer.actor.ActorStaticMorph; import electrosphere.renderer.actor.ActorTextureMask; import electrosphere.renderer.anim.AnimChannel; import electrosphere.renderer.anim.Animation; import electrosphere.renderer.loading.ModelPretransforms; import electrosphere.renderer.meshgen.MeshLoader; import electrosphere.renderer.shader.ShaderProgram; import electrosphere.renderer.anim.AnimNode; import electrosphere.engine.Globals; import electrosphere.logger.LoggerInterface; import java.util.ArrayList; import java.util.List; import org.joml.Matrix4d; import org.lwjgl.PointerBuffer; import org.lwjgl.assimp.AIMaterial; import org.lwjgl.assimp.AIMesh; import org.lwjgl.assimp.AIScene; import org.lwjgl.assimp.Assimp; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import org.joml.Sphered; import org.joml.Vector3d; import org.lwjgl.assimp.AIAnimation; import org.lwjgl.assimp.AIBone; import org.lwjgl.assimp.AINode; /** * A model * Contains a list of meshes to draw * Contains animations to apply to those meshes */ public class Model { //the model matrix for this model private Matrix4d modelMatrix = new Matrix4d(); //an optional global transform applied to the parent bone. Typically found in models loaded from files private Matrix4d globalInverseTransform = new Matrix4d(); //the meshes in the model private List meshes = new ArrayList(); //the materials in the model private List materials = new ArrayList(); //bone data for the model private List bones = new ArrayList(); private Map boneMap = new HashMap(); //these structures are used to parse the tree of bones of the model private AnimNode rootAnimNode; private Map nodeMap = new HashMap(); //animation data for the model private List animations = new ArrayList(); private Map animMap = new HashMap(); //these are masks that can be applied to the model to overwrite meshes, shaders, and textures of it private ActorMeshMask meshMask; private Map shaderMask = new HashMap(); private Map textureMap = new HashMap(); //The bounding sphere for this particular model private Sphered boundingSphere = new Sphered(); /** * Constructor */ public Model(){ } /** * Loads a model from an ai scene object * @param path The path of the model * @param s the ai scene * @return The model object */ public static Model createModelFromAiscene(String path, AIScene s){ Model rVal = new Model(); ModelPretransforms.ModelMetadata modelMetadata = Globals.modelPretransforms.getModel(path); ModelPretransforms.GlobalTransform globalTransform = null; if(modelMetadata != null){ globalTransform = modelMetadata.getGlobalTransform(); } // //load meshes // int meshCount = s.mNumMeshes(); PointerBuffer meshesBuffer = s.mMeshes(); rVal.meshes = new ArrayList(); while(meshesBuffer.hasRemaining()){ AIMesh aiMesh = AIMesh.create(meshesBuffer.get()); ModelPretransforms.MeshMetadata meshMetadata = null; if(modelMetadata != null){ meshMetadata = modelMetadata.getMesh(aiMesh.mName().dataString()); } Mesh currentMesh = MeshLoader.createMeshFromAIScene(aiMesh, meshMetadata); rVal.meshes.add(currentMesh); currentMesh.setParent(rVal); //update model bounding sphere if(currentMesh.getBoundingSphere().r > rVal.boundingSphere.r){ rVal.boundingSphere.r = currentMesh.getBoundingSphere().r; } } // //register bones // rVal.bones = new ArrayList(); rVal.boneMap = new HashMap(); meshesBuffer.rewind(); 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(!rVal.boneMap.containsKey(currentBoneName)){ Bone boneObject = new Bone(currentBone); rVal.boneMap.put(currentBoneName, boneObject); rVal.bones.add(boneObject); } } } Iterator meshIterator = rVal.meshes.iterator(); rVal.bones = new ArrayList(); rVal.boneMap = new HashMap(); rVal.nodeMap = new HashMap(); while(meshIterator.hasNext()){ Mesh currentMesh = meshIterator.next(); Iterator boneIterator = currentMesh.getBones().iterator(); ArrayList to_remove_queue = new ArrayList(); ArrayList to_add_queue = new ArrayList(); while(boneIterator.hasNext()){ Bone currentBone = boneIterator.next(); if(!rVal.boneMap.containsKey(currentBone.boneID)){ rVal.bones.add(currentBone); rVal.boneMap.put(currentBone.boneID, currentBone); } } boneIterator = to_remove_queue.iterator(); while(boneIterator.hasNext()){ currentMesh.getBones().remove(boneIterator.next()); } boneIterator = to_add_queue.iterator(); while(boneIterator.hasNext()){ currentMesh.getBones().add(boneIterator.next()); } } // //parse animation nodes and form hierarchy // AINode rootNode = s.mRootNode(); rVal.globalInverseTransform = electrosphere.util.Utilities.convertAIMatrixd(rootNode.mTransformation()); if(globalTransform != null){ rVal.globalInverseTransform.scale(globalTransform.getScale()); } LoggerInterface.loggerRenderer.DEBUG("Global Inverse Transform"); LoggerInterface.loggerRenderer.DEBUG(rVal.globalInverseTransform + ""); rVal.rootAnimNode = rVal.buildAnimNodeMap(s.mRootNode(),null); // //load animations // int animCount = s.mNumAnimations(); PointerBuffer animBuffer = s.mAnimations(); rVal.animations = new ArrayList(); rVal.animMap = new HashMap(); for(int i = 0; i < animCount; i++){ Animation newAnim = new Animation(AIAnimation.create(animBuffer.get(i)), i); rVal.animations.add(newAnim); rVal.animMap.put(newAnim.name,newAnim); } // //Load materials // if(s.mNumMaterials() > 0){ rVal.materials = new ArrayList(); PointerBuffer material_buffer = s.mMaterials(); while(material_buffer.hasRemaining()){ rVal.materials.add(Material.load_material_from_aimaterial(AIMaterial.create(material_buffer.get()))); } } //garbage collect System.gc(); return rVal; } /** * Draws the model * @param renderPipelineState the render pipeline state */ public void draw(RenderPipelineState renderPipelineState){ Iterator mesh_Iterator = meshes.iterator(); while(mesh_Iterator.hasNext()){ Mesh currentMesh = mesh_Iterator.next(); if(meshMask == null || (meshMask != null && !meshMask.isBlockedMesh(currentMesh.getMeshName()))){ //set shader ShaderProgram original = currentMesh.getShader(); ShaderProgram shader = getCorrectShader(shaderMask, currentMesh, currentMesh.getShader()); currentMesh.setShader(shader); //set texture mask if(this.textureMap != null && textureMap.containsKey(currentMesh.getMeshName())){ currentMesh.setTextureMask(textureMap.get(currentMesh.getMeshName())); } //draw currentMesh.complexDraw(renderPipelineState); //reset texture mask currentMesh.setTextureMask(null); //reset shader currentMesh.setShader(original); } } if(meshMask != null){ for(Mesh toDraw : meshMask.getToDrawMeshes()){ toDraw.setBones(bones); toDraw.setParent(this); ShaderProgram original = toDraw.getShader(); ShaderProgram shader = getCorrectShader(shaderMask, toDraw, toDraw.getShader()); toDraw.setShader(shader); toDraw.complexDraw(renderPipelineState); toDraw.setShader(original); } } } /** * Determines the correct shader to use for a given mesh * @param shaderMask The shader mask * @param mesh The mesh * @param oldShader The original shader on the mesh * @return The correct shader program to use for this mesh */ ShaderProgram getCorrectShader(Map shaderMask, Mesh mesh, ShaderProgram oldShader){ ShaderProgram rVal = oldShader; if(shaderMask.containsKey(mesh.getMeshName())){ ActorShaderMask specificMask = shaderMask.get(mesh.getMeshName()); ShaderProgram overwriteShader = null; if((overwriteShader = Globals.assetManager.fetchShader(specificMask.getVertexShaderPath(), specificMask.getGeometryShaderPath(), specificMask.getFragmentShaderPath())) != null){ rVal = overwriteShader; } } return rVal; } /** * Applies an animation mask to the model * @param animationName The name of the animation * @param time The temporal location in the animation to mask * @param mask The mask */ 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 currentBone.deform = new Matrix4d(); currentBone.deform.translate(currentChannel.getCurrentPosition()); currentBone.deform.rotate(currentChannel.getCurrentRotation()); currentBone.deform.scale(new Vector3d(currentChannel.getCurrentScale())); } } } } /** * 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(); } } } /** * Recursively builds the bone tree * @param node The current assimp bone to operate on * @param parent The parent bone * @return The current bone */ 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 the position of all bones to their correct locations given bone rotators, static morph, and current animation * @param boneRotators The bone rotators to apply * @param staticMorph The static morph to apply */ public void updateNodeTransform(Map boneRotators, ActorStaticMorph staticMorph){ if(this.rootAnimNode != null){ updateNodeTransform(this.rootAnimNode,boneRotators,staticMorph); } } /** * Recursively updates the position of all bones to their correct locations given bone rotators, static morph, and current animation * @param n the current node to operate on * @param boneRotators The bone rotators to apply * @param staticMorph The static morph to apply */ void updateNodeTransform(AnimNode n, Map boneRotators, ActorStaticMorph staticMorph){ if(n.parent != null){ n.transform = new Matrix4d(n.parent.transform); } else { n.transform = new Matrix4d(); } if(n.is_bone){ // //bone rotators (turrets, hair, etc) Bone target_bone = boneMap.get(n.id); n.transform = n.transform.mul(target_bone.deform); if(boneRotators.containsKey(target_bone.boneID)){ n.transform.rotate(boneRotators.get(target_bone.boneID).getRotation()); } // //static morph (changing nose size, eye distance, etc) Matrix4d bone_matrix = new Matrix4d(n.transform); if(staticMorph != null && staticMorph.getBoneTransforms(n.id) != null){ bone_matrix.mul(staticMorph.getBoneTransforms(n.id).getTransform()); n.transform.mul(staticMorph.getBoneTransforms(n.id).getTransform()); } // //Calculate final offset from initial bone bone_matrix.mul(target_bone.inverseBindPoseMatrix); bone_matrix = new Matrix4d(globalInverseTransform).mul(bone_matrix); target_bone.final_transform = bone_matrix; } else { n.transform = n.transform.mul(electrosphere.util.Utilities.convertAIMatrix(n.raw_data.mTransformation())); } Iterator node_iterator = n.children.iterator(); while(node_iterator.hasNext()){ AnimNode current_node = node_iterator.next(); updateNodeTransform(current_node,boneRotators,staticMorph); } } /** * Draws a ui model */ public void drawUI(){ for(Mesh m : meshes){ m.complexDraw(Globals.renderingEngine.getRenderPipelineState()); } } /** * Pushes a uniform to a given mesh * @param meshName The name of the mesh * @param uniformKey The uniform key * @param uniform The value of the uniform */ public void pushUniformToMesh(String meshName, String uniformKey, Object uniform){ for(Mesh m : meshes){ if(m.getMeshName().equals(meshName)){ m.setUniform(uniformKey, uniform); } } } /** * Logs all bone IDs (names) */ public void listAllBoneIDs(){ for(String id : boneMap.keySet()){ LoggerInterface.loggerRenderer.DEBUG(id); } } /** * Gets a given mesh by its name * @param meshName the name of the mesh * @return The mesh if it exists, or null */ public Mesh getMesh(String meshName){ for(Mesh mesh : meshes){ if(mesh.getMeshName().matches(meshName)){ return mesh; } } return null; } /** * Gets an animation by its name * @param animName The name of the animation * @return the animation if it exists, or null */ public Animation getAnimation(String animName){ return animMap.get(animName); } /** * Describes the model at a high level */ public void describeHighLevel(){ LoggerInterface.loggerRenderer.DEBUG("Meshes: "); for(Mesh mesh : meshes){ LoggerInterface.loggerRenderer.DEBUG(mesh.getMeshName()); } LoggerInterface.loggerRenderer.DEBUG("Animations: "); for(Animation anim : animations){ LoggerInterface.loggerRenderer.DEBUG(anim.name); } LoggerInterface.loggerRenderer.DEBUG("Bones:"); for(Bone bone : bones){ LoggerInterface.loggerRenderer.DEBUG(bone.boneID); } } // // Pure getters and setters // /** * Gets the meshes in this model * @return the list of meshes */ public List getMeshes(){ return meshes; } /** * Sets the model matrix for this model * @param modelMatrix the model matrix */ public void setModelMatrix(Matrix4d modelMatrix){ this.modelMatrix = modelMatrix; } /** * Gets the model matrix for this model * @return the model matrix */ public Matrix4d getModelMatrix(){ return modelMatrix; } /** * Gets the list of bones * @return the list of bones */ public List getBones(){ return bones; } /** * Gets the map of bone name -> bone * @return the map */ public Map getBoneMap(){ return boneMap; } /** * Gets the list of materials in this model * @return The list of materials */ public List getMaterials(){ return materials; } /** * Sets the mesh mask * @param meshMask the mesh mask to set */ public void setMeshMask(ActorMeshMask meshMask){ this.meshMask = meshMask; } /** * Sets the texture mask * @param textureMask the texture mask */ public void setTextureMask(Map textureMask){ this.textureMap = textureMask; } /** * Gets the shader mask map * @return The shader mask map */ public Map getShaderMask(){ return shaderMask; } /** * Utility method to attempt overwriting the model's meshes with a new material * @param material The material */ public void tryOverwriteMaterial(Material material){ for(Mesh mesh : meshes){ mesh.setMaterial(material); } } /** * Gets the bounding sphere for this model * @return The bounding sphere */ public Sphered getBoundingSphere(){ return boundingSphere; } /** * Sets the bounding sphere * @param boundingSphere The bounding sphere to be stored */ public void setBoundingSphere(Sphered boundingSphere){ this.boundingSphere = boundingSphere; } }