Hybrid GPU Shader Animations for Bones
Contents
Overview
While working on an Android project, I found that jPCT+Bones for skeletal animations were becoming too CPU intensive on the UI thread. When adding twenty animated models on screen, I was noticing a considerable drop in the frame-rate. So I developed a hybrid solution of offloading calculations to the GPU for my project, which I will share with you now.
I have added notes and personal recommendations. This architecture may not be perfect for your project, so please alter and use to fit your needs.
Comments and suggestions are welcome as I will be adding a FAQ to the bottom of the page.
Requirements
JPCT (requires GL ES 2+ as it uses GLSL Shaders) Bones
How It Works
This implementation works by offloading all of the vertex calculations to the GPU. The bone matrix transforms for the poses are still performed on the CPU side.
Gains: Speed. You will harness the GPU which is setup to handle floating point calculation quickly and is dedicated to such tasks. As an example with my project: having 20 animated models dropped the frame-rate to 8-9 fps on my device. Using this method, it went back up to the 60-62 fps cap.
Losses: Software knowledge of the transformed pose. Since the meshes of your models will not be animated on the software-side, it will have no knowledge of its transformed pose. The polygons will be in the initial state, which will not give you an accurate bounding-box, ray-trace collisions, etc... of the current animated mesh pose as it will not actually be transformed on the software-side.
The Code
This code is free, open-source code and you are welcome to use it as you see fit.
GPUAnimated3D Class:
package yournamespacegoeshere; import yournamespacegoeshere.GPUAnimated3DShader; import com.threed.jpct.GLSLShader; import com.threed.jpct.IRenderHook; import com.threed.jpct.Object3D; import raft.jpct.bones.Animated3D; import raft.jpct.bones.BonesNamespaceUtils; import raft.jpct.bones.SkinClip; /** * Created by Dougie on 6/21/16. */ public class GPUAnimated3D extends Animated3D implements IRenderHook { public GPUAnimated3D(Animated3D object) { super(object); BonesNamespaceUtils.setSkinAttributes(object); setRenderHook(this); } public void animateSkin(float index, int sequence) { if(getSkinClipSequence() != null) { if(sequence == 0) { BonesNamespaceUtils.animate(this, index*getSkinClipSequence().getTime(), getSkeletonPose()); } else { SkinClip clip = getSkinClipSequence().getClip(sequence - 1); clip.applyTo(index * clip.getTime(), getSkeletonPose()); } getSkeletonPose().updateTransforms(); } } public void animatePose(float index, int sequence, float weight) { BonesNamespaceUtils.animatePoseDontApply(this, index, sequence, weight); } @Override public void beforeRendering(int i) { ; } @Override public void afterRendering(int i) { ; } @Override public void setCurrentObject3D(Object3D object3D) { ; } @Override public void setCurrentShader(GLSLShader glslShader) { if(glslShader!=null && glslShader instanceof GPUAnimated3DShader) { ((GPUAnimated3DShader)glslShader).updateBeforeRenderingObject(this); } } @Override public void setTransparency(float v) { ; } @Override public void onDispose() { ; } @Override public boolean repeatRendering() { return false; } }
GPUAnimated3DShader Class:
package yournamespacegoeshere; import android.opengl.GLES20; import com.threed.jpct.GLSLShader; import com.threed.jpct.Matrix; import raft.jpct.bones.Animated3D; import raft.jpct.bones.BonesNamespaceUtils; /** * Created by Dougie on 6/21/16. */ public class GPUAnimated3DShader extends GLSLShader { int skinWeightsHandle = -1; int jointIndicesHandle = -1; public GPUAnimated3DShader(String vertexShaderSource, String fragmentShaderSource) { super(vertexShaderSource, fragmentShaderSource); skinWeightsHandle = GLES20.glGetAttribLocation(getProgram(), BonesNamespaceUtils.ATTR_SKIN_WEIGHTS); jointIndicesHandle = GLES20.glGetAttribLocation(getProgram(), BonesNamespaceUtils.ATTR_JOINT_INDICES); } public void updateBeforeRenderingObject(Animated3D pObject) { if(pObject!=null) { Matrix[] skelPose = BonesNamespaceUtils.getSkeletonPosePallete(pObject.getSkeletonPose()); if(skelPose!=null && skelPose.length>0) { setUniform(BonesNamespaceUtils.UNIFORM_SKEL_POSE_SIZE, skelPose.length); setUniform(BonesNamespaceUtils.UNIFORM_SKEL_POSE, skelPose); } else { setUniform(BonesNamespaceUtils.UNIFORM_SKEL_POSE_SIZE, 0); } } } }