Pārlūkot izejas kodu

Hardware Skinning first commit, still non functionnal as no material implements it. also it's disabled by default in the skeleton control

git-svn-id: https://jmonkeyengine.googlecode.com/svn/trunk@10537 75d07b2b-3a1a-0410-a2c5-0572b91ccdca
rem..om 12 gadi atpakaļ
vecāks
revīzija
eb5525e581

+ 50 - 31
engine/src/core-data/Common/ShaderLib/Skinning.glsllib

@@ -1,36 +1,55 @@
-#ifdef USE_HWSKINNING
-
-#ifndef NUM_BONES
-#error A required pre-processor define "NUM_BONES" is not set!
-#endif
-
+ #ifdef NUM_BONES
+ 
 attribute vec4 inBoneWeight;
-attribute vec4 inBoneIndices;
+attribute vec4 inBoneIndex;
 uniform mat4 m_BoneMatrices[NUM_BONES];
-
-void Skinning_Compute(inout vec4 position, inout vec4 normal){
-    vec4 index  = inBoneIndices;
-    vec4 weight = inBoneWeight;
-
-    vec4 newPos    = vec4(0.0);
-    vec4 newNormal = vec4(0.0);
-
-    for (float i = 0.0; i < 4.0; i += 1.0){
-        mat4 skinMat = m_BoneMatrices[int(index.x)];
-        newPos    += weight.x * (skinMat * position);
-        newNormal += weight.x * (skinMat * normal);
-        index = index.yzwx;
-        weight = weight.yzwx;
-    }
-
-    position = newPos;
-    normal = newNormal;
+ 
+void Skinning_Compute(inout vec4 position){
+    #if NUM_WEIGHTS_PER_VERT == 1
+    position = m_BoneMatrices[int(inBoneIndex.x)] * position;
+    #else
+    mat4 mat = mat4(0.0);
+    mat += m_BoneMatrices[int(inBoneIndex.x)] * inBoneWeight.x;
+    mat += m_BoneMatrices[int(inBoneIndex.y)] * inBoneWeight.y;
+    mat += m_BoneMatrices[int(inBoneIndex.z)] * inBoneWeight.z;
+    mat += m_BoneMatrices[int(inBoneIndex.w)] * inBoneWeight.w;
+    position = mat * position;
+    #endif
 }
-
-#else
-
-void Skinning_Compute(inout vec4 position, inout vec4 normal){
-   // skinning disabled, leave position and normal unaltered
+ 
+void Skinning_Compute(inout vec4 position, inout vec3 normal){
+    #if NUM_WEIGHTS_PER_VERT == 1
+    position = m_BoneMatrices[int(inBoneIndex.x)] * position;
+    normal = (mat3(m_BoneMatrices[int(inBoneIndex.x)][0].xyz,
+                   m_BoneMatrices[int(inBoneIndex.x)][1].xyz,
+                   m_BoneMatrices[int(inBoneIndex.x)][2].xyz) * normal);
+    #else
+    mat4 mat = mat4(0.0);
+    mat += m_BoneMatrices[int(inBoneIndex.x)] * inBoneWeight.x;
+    mat += m_BoneMatrices[int(inBoneIndex.y)] * inBoneWeight.y;
+    mat += m_BoneMatrices[int(inBoneIndex.z)] * inBoneWeight.z;
+    mat += m_BoneMatrices[int(inBoneIndex.w)] * inBoneWeight.w;
+    position = mat * position;
+    normal = (mat3(mat[0].xyz,mat[1].xyz,mat[2].xyz) * normal);
+    #endif
+}
+ 
+void Skinning_Compute(inout vec4 position, inout vec4 tangent, inout vec3 normal){
+    #if NUM_WEIGHTS_PER_VERT == 1
+    position = m_BoneMatrices[int(inBoneIndex.x)] * position;
+    tangent = m_BoneMatrices[int(inBoneIndex.x)] * tangent;
+    normal = (mat3(m_BoneMatrices[int(inBoneIndex.x)][0].xyz,
+                   m_BoneMatrices[int(inBoneIndex.x)][1].xyz,
+                   m_BoneMatrices[int(inBoneIndex.x)][2].xyz) * normal);
+    #else
+    mat4 mat = mat4(0.0);
+    mat += m_BoneMatrices[int(inBoneIndex.x)] * inBoneWeight.x;
+    mat += m_BoneMatrices[int(inBoneIndex.y)] * inBoneWeight.y;
+    mat += m_BoneMatrices[int(inBoneIndex.z)] * inBoneWeight.z;
+    mat += m_BoneMatrices[int(inBoneIndex.w)] * inBoneWeight.w;
+    position = mat * position;
+    tangent = mat * tangent;
+    normal = (mat3(mat[0].xyz,mat[1].xyz,mat[2].xyz) * normal);
+    #endif
 }
-
 #endif

+ 189 - 73
engine/src/core/com/jme3/animation/SkeletonControl.java

@@ -32,25 +32,32 @@
 package com.jme3.animation;
 
 import com.jme3.export.*;
+import com.jme3.material.Material;
 import com.jme3.math.FastMath;
 import com.jme3.math.Matrix4f;
 import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.RendererException;
 import com.jme3.renderer.ViewPort;
 import com.jme3.scene.*;
 import com.jme3.scene.VertexBuffer.Type;
 import com.jme3.scene.control.AbstractControl;
 import com.jme3.scene.control.Control;
+import com.jme3.shader.VarType;
 import com.jme3.util.TempVars;
 import java.io.IOException;
+import java.nio.Buffer;
 import java.nio.ByteBuffer;
 import java.nio.FloatBuffer;
 import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 /**
- * The Skeleton control deforms a model according to a skeleton, 
- * It handles the computation of the deformation matrices and performs 
- * the transformations on the mesh
- * 
+ * The Skeleton control deforms a model according to a skeleton, It handles the
+ * computation of the deformation matrices and performs the transformations on
+ * the mesh
+ *
  * @author Rémy Bouquet Based on AnimControl by Kirill Vainer
  */
 public class SkeletonControl extends AbstractControl implements Cloneable {
@@ -64,10 +71,28 @@ public class SkeletonControl extends AbstractControl implements Cloneable {
      */
     private Mesh[] targets;
     /**
-     * Used to track when a mesh was updated. Meshes are only updated
-     * if they are visible in at least one camera.
+     * Used to track when a mesh was updated. Meshes are only updated if they
+     * are visible in at least one camera.
      */
     private boolean wasMeshUpdated = false;
+    /**
+     * Flag to enable hardware/gpu skinning if available, disable for
+     * software/cpu skinning, enabled by default
+     */
+    private boolean useHwSkinning = false;
+    /**
+     * Flag to check if we have to check the shader if it would work and on fail
+     * switch to sw skinning
+     */
+    private boolean triedHwSkinning = false;
+    /**
+     * Bone offset matrices, recreated each frame
+     */
+    private transient Matrix4f[] offsetMatrices;
+    /**
+     * Material references used for hardware skinning
+     */
+    private Material[] materials;
 
     /**
      * Serialization only. Do not use.
@@ -76,10 +101,50 @@ public class SkeletonControl extends AbstractControl implements Cloneable {
     }
 
     /**
-     * Creates a skeleton control.
-     * The list of targets will be acquired automatically when
-     * the control is attached to a node.
-     * 
+     * Hint to use hardware/software skinning mode. If gpu skinning fails or is
+     * disabledIf in hardware mode all or some models display the same animation
+     * cycle make sure your materials are not identical but clones
+     *
+     * @param useHwSkinning the useHwSkinning to set
+     *
+     */
+    public void setUseHwSkinning(boolean useHwSkinning) {
+        this.useHwSkinning = useHwSkinning;
+        this.triedHwSkinning = false;
+        //next full 10 bones (e.g. 30 on 24 bones )
+        int bones = ((skeleton.getBoneCount() / 10) + 1) * 10;
+        for (Material m : materials) {
+            if (useHwSkinning) {
+                try {
+                    m.setInt("NumberOfBones", bones);
+                } catch (java.lang.IllegalArgumentException e) {
+                    Logger.getLogger(SkeletonControl.class.getName()).log(Level.INFO, "{0} material doesn't support Hardware Skinning reverting to software", new String[]{m.getName()});
+                    setUseHwSkinning(false);
+                }
+            } else {
+                if (m.getParam("NumberOfBones") != null) {
+                    m.clearParam("NumberOfBones");
+                }
+            }
+        }
+
+        for (Mesh mesh : targets) {
+            if (isMeshAnimated(mesh)) {
+                mesh.prepareForAnim(!useHwSkinning); // prepare for software animation
+            }
+        }
+        if (useHwSkinning) {
+        }
+    }
+
+    public boolean isUseHwSkinning() {
+        return useHwSkinning;
+    }
+
+    /**
+     * Creates a skeleton control. The list of targets will be acquired
+     * automatically when the control is attached to a node.
+     *
      * @param skeleton the skeleton
      */
     public SkeletonControl(Skeleton skeleton) {
@@ -88,7 +153,7 @@ public class SkeletonControl extends AbstractControl implements Cloneable {
 
     /**
      * Creates a skeleton control.
-     * 
+     *
      * @param targets the meshes controlled by the skeleton
      * @param skeleton the skeleton
      */
@@ -102,44 +167,45 @@ public class SkeletonControl extends AbstractControl implements Cloneable {
         return mesh.getBuffer(Type.BindPosePosition) != null;
     }
 
-    private Mesh[] findTargets(Node node) {
+    private void findTargets(Node node, ArrayList<Mesh> targets, HashSet<Material> materials) {
         Mesh sharedMesh = null;
-        ArrayList<Mesh> animatedMeshes = new ArrayList<Mesh>();
 
-        for (Spatial child : node.getChildren()) {
-            if (!(child instanceof Geometry)) {
-                continue; // could be an attachment node, ignore.
-            }
-
-            Geometry geom = (Geometry) child;
 
-            // is this geometry using a shared mesh?
-            Mesh childSharedMesh = geom.getUserData(UserData.JME_SHAREDMESH);
-
-            if (childSharedMesh != null) {
-                // Don't bother with non-animated shared meshes
-                if (isMeshAnimated(childSharedMesh)) {
-                    // child is using shared mesh,
-                    // so animate the shared mesh but ignore child
-                    if (sharedMesh == null) {
-                        sharedMesh = childSharedMesh;
-                    } else if (sharedMesh != childSharedMesh) {
-                        throw new IllegalStateException("Two conflicting shared meshes for " + node);
+        for (Spatial child : node.getChildren()) {
+            if (child instanceof Geometry) {
+                Geometry geom = (Geometry) child;
+
+                // is this geometry using a shared mesh?
+                Mesh childSharedMesh = geom.getUserData(UserData.JME_SHAREDMESH);
+
+                if (childSharedMesh != null) {
+                    // Don’t bother with non-animated shared meshes
+                    if (isMeshAnimated(childSharedMesh)) {
+                        // child is using shared mesh,
+                        // so animate the shared mesh but ignore child
+                        if (sharedMesh == null) {
+                            sharedMesh = childSharedMesh;
+                        } else if (sharedMesh != childSharedMesh) {
+                            throw new IllegalStateException("Two conflicting shared meshes for " + node);
+                        }
+                        materials.add(geom.getMaterial());
+                    }
+                } else {
+                    Mesh mesh = geom.getMesh();
+                    if (isMeshAnimated(mesh)) {
+                        targets.add(mesh);
+                        materials.add(geom.getMaterial());
                     }
                 }
-            } else {
-                Mesh mesh = geom.getMesh();
-                if (isMeshAnimated(mesh)) {
-                    animatedMeshes.add(mesh);
-                }
+            } else if (child instanceof Node) {
+                findTargets((Node) child, targets, materials);
             }
         }
 
         if (sharedMesh != null) {
-            animatedMeshes.add(sharedMesh);
+            targets.add(sharedMesh);
         }
 
-        return animatedMeshes.toArray(new Mesh[animatedMeshes.size()]);
     }
 
     @Override
@@ -147,28 +213,51 @@ public class SkeletonControl extends AbstractControl implements Cloneable {
         super.setSpatial(spatial);
         if (spatial != null) {
             Node node = (Node) spatial;
-            targets = findTargets(node);
+            HashSet<Material> mats = new HashSet<Material>();
+            ArrayList<Mesh> meshes = new ArrayList<Mesh>();
+            findTargets(node, meshes, mats);
+            targets = meshes.toArray(new Mesh[meshes.size()]);
+            materials = mats.toArray(new Material[mats.size()]);
+            //try hw skinning, will be reset to sw skinning if render call fails
+            setUseHwSkinning(true);
         } else {
             targets = null;
+            materials = null;
         }
     }
 
     @Override
     protected void controlRender(RenderManager rm, ViewPort vp) {
         if (!wasMeshUpdated) {
-            resetToBind(); // reset morph meshes to bind pose
-
-            Matrix4f[] offsetMatrices = skeleton.computeSkinningMatrices();
-
-            // if hardware skinning is supported, the matrices and weight buffer
-            // will be sent by the SkinningShaderLogic object assigned to the shader
-            for (int i = 0; i < targets.length; i++) {
-                // NOTE: This assumes that code higher up
-                // Already ensured those targets are animated
-                // otherwise a crash will happen in skin update
-                //if (isMeshAnimated(targets[i])) {
-                softwareSkinUpdate(targets[i], offsetMatrices);
-                //}
+            if (useHwSkinning) {
+                //preload scene to check if shader won’t blow with too many bones
+                if (!triedHwSkinning) {
+                    triedHwSkinning = true;
+                    try {
+                        rm.preloadScene(this.spatial);
+                    } catch (RendererException e) {
+                        //revert back to sw skinning for this model
+                        setUseHwSkinning(false);
+                    }
+                }
+                offsetMatrices = skeleton.computeSkinningMatrices();
+
+                hardwareSkinUpdate();
+            } else {
+                resetToBind(); // reset morph meshes to bind pose
+
+                offsetMatrices = skeleton.computeSkinningMatrices();
+
+                // if hardware skinning is supported, the matrices and weight buffer
+                // will be sent by the SkinningShaderLogic object assigned to the shader
+                for (int i = 0; i < targets.length; i++) {
+                    // NOTE: This assumes that code higher up
+                    // Already ensured those targets are animated
+                    // otherwise a crash will happen in skin update
+                    //if (isMeshAnimated(targets)) {
+                    softwareSkinUpdate(targets[i], offsetMatrices);
+                    //}
+                }
             }
 
             wasMeshUpdated = true;
@@ -180,13 +269,14 @@ public class SkeletonControl extends AbstractControl implements Cloneable {
         wasMeshUpdated = false;
     }
 
+    //only do this for software updates
     void resetToBind() {
         for (Mesh mesh : targets) {
-            if (isMeshAnimated(mesh)) {                
-                FloatBuffer bwBuff = (FloatBuffer) mesh.getBuffer(Type.BoneWeight).getData();
-                ByteBuffer biBuff = (ByteBuffer)mesh.getBuffer(Type.BoneIndex).getData();                
+            if (isMeshAnimated(mesh)) {
+                Buffer bwBuff = mesh.getBuffer(Type.BoneWeight).getData();
+                Buffer biBuff = mesh.getBuffer(Type.BoneIndex).getData();
                 if (!biBuff.hasArray() || !bwBuff.hasArray()) {
-                    mesh.prepareForAnim(true); // prepare for software animation
+                    mesh.prepareForAnim(!useHwSkinning); // prepare for software animation
                 }
                 VertexBuffer bindPos = mesh.getBuffer(Type.BindPosePosition);
                 VertexBuffer bindNorm = mesh.getBuffer(Type.BindPoseNormal);
@@ -223,11 +313,10 @@ public class SkeletonControl extends AbstractControl implements Cloneable {
         Node clonedNode = (Node) spatial;
         AnimControl ctrl = spatial.getControl(AnimControl.class);
         SkeletonControl clone = new SkeletonControl();
-        clone.setSpatial(clonedNode);
 
         clone.skeleton = ctrl.getSkeleton();
-        // Fix animated targets for the cloned node
-        clone.targets = findTargets(clonedNode);
+
+        clone.setSpatial(clonedNode);
 
         // Fix attachments for the cloned node
         for (int i = 0; i < clonedNode.getQuantity(); i++) {
@@ -250,9 +339,9 @@ public class SkeletonControl extends AbstractControl implements Cloneable {
     }
 
     /**
-     * 
+     *
      * @param boneName the name of the bone
-     * @return the node attached to this bone    
+     * @return the node attached to this bone
      */
     public Node getAttachmentsNode(String boneName) {
         Bone b = skeleton.getBone(boneName);
@@ -269,7 +358,8 @@ public class SkeletonControl extends AbstractControl implements Cloneable {
 
     /**
      * returns the skeleton of this control
-     * @return 
+     *
+     * @return
      */
     public Skeleton getSkeleton() {
         return skeleton;
@@ -277,30 +367,34 @@ public class SkeletonControl extends AbstractControl implements Cloneable {
 
     /**
      * sets the skeleton for this control
-     * @param skeleton 
+     *
+     * @param skeleton
      */
 //    public void setSkeleton(Skeleton skeleton) {
 //        this.skeleton = skeleton;
 //    }
     /**
      * returns the targets meshes of this control
-     * @return 
+     *
+     * @return
      */
     public Mesh[] getTargets() {
         return targets;
     }
 
     /**
-     * sets the target  meshes of this control
-     * @param targets 
+     * sets the target meshes of this control
+     *
+     * @param targets
      */
 //    public void setTargets(Mesh[] targets) {
 //        this.targets = targets;
 //    }
     /**
      * Update the mesh according to the given transformation matrices
+     *
      * @param mesh then mesh
-     * @param offsetMatrices the transformation matrices to apply 
+     * @param offsetMatrices the transformation matrices to apply
      */
     private void softwareSkinUpdate(Mesh mesh, Matrix4f[] offsetMatrices) {
 
@@ -317,7 +411,20 @@ public class SkeletonControl extends AbstractControl implements Cloneable {
     }
 
     /**
-     * Method to apply skinning transforms to a mesh's buffers    
+     * Update the mesh according to the given transformation matrices
+     *
+     * @param mesh then mesh
+     * @param offsetMatrices the transformation matrices to apply
+     */
+    private void hardwareSkinUpdate() {
+        for (Material m : materials) {
+            m.setParam("BoneMatrices", VarType.Matrix4Array, offsetMatrices);
+        }
+    }
+
+    /**
+     * Method to apply skinning transforms to a mesh's buffers
+     *
      * @param mesh the mesh
      * @param offsetMatrices the offset matices to apply
      */
@@ -373,7 +480,7 @@ public class SkeletonControl extends AbstractControl implements Cloneable {
                     idxWeights += 4;
                     continue;
                 }
-                
+
                 float nmx = normBuf[idxPositions];
                 float vtx = posBuf[idxPositions++];
                 float nmy = normBuf[idxPositions];
@@ -421,9 +528,12 @@ public class SkeletonControl extends AbstractControl implements Cloneable {
     }
 
     /**
-     * Specific method for skinning with tangents to avoid cluttering the classic skinning calculation with
-     * null checks that would slow down the process even if tangents don't have to be computed.
-     * Also the iteration has additional indexes since tangent has 4 components instead of 3 for pos and norm
+     * Specific method for skinning with tangents to avoid cluttering the
+     * classic skinning calculation with null checks that would slow down the
+     * process even if tangents don't have to be computed. Also the iteration
+     * has additional indexes since tangent has 4 components instead of 3 for
+     * pos and norm
+     *
      * @param maxWeightsPerVert maximum number of weights per vertex
      * @param mesh the mesh
      * @param offsetMatrices the offsetMaytrices to apply
@@ -496,7 +606,7 @@ public class SkeletonControl extends AbstractControl implements Cloneable {
                     idxWeights += 4;
                     continue;
                 }
-                
+
                 float nmx = normBuf[idxPositions];
                 float vtx = posBuf[idxPositions++];
                 float nmy = normBuf[idxPositions];
@@ -574,6 +684,7 @@ public class SkeletonControl extends AbstractControl implements Cloneable {
         OutputCapsule oc = ex.getCapsule(this);
         oc.write(targets, "targets", null);
         oc.write(skeleton, "skeleton", null);
+        oc.write(materials, "materials", null);
     }
 
     @Override
@@ -586,5 +697,10 @@ public class SkeletonControl extends AbstractControl implements Cloneable {
             System.arraycopy(sav, 0, targets, 0, sav.length);
         }
         skeleton = (Skeleton) in.readSavable("skeleton", null);
+        sav = in.readSavableArray("materials", null);
+        if (sav != null) {
+            materials = new Material[sav.length];
+            System.arraycopy(sav, 0, materials, 0, sav.length);
+        }
     }
 }

+ 57 - 9
engine/src/core/com/jme3/scene/Mesh.java

@@ -341,7 +341,7 @@ public class Mesh implements Savable, Cloneable {
                         BufferUtils.clone(tangents.getData()));
                 setBuffer(bindTangents);
                 tangents.setUsage(Usage.Stream);
-            }
+            }// else hardware setup does nothing, mesh already in bind pose
         }
     }
 
@@ -352,22 +352,70 @@ public class Mesh implements Savable, Cloneable {
      * @param forSoftwareAnim Should be true to enable the conversion.
      */
     public void prepareForAnim(boolean forSoftwareAnim){
-        if (forSoftwareAnim){
-            // convert indices
+        if (forSoftwareAnim) {
+            // convert indices to ubytes on the heap or floats
             VertexBuffer indices = getBuffer(Type.BoneIndex);
-            ByteBuffer originalIndex = (ByteBuffer) indices.getData();
-            ByteBuffer arrayIndex = ByteBuffer.allocate(originalIndex.capacity());
-            originalIndex.clear();
-            arrayIndex.put(originalIndex);
-            indices.updateData(arrayIndex);
+            Buffer buffer = indices.getData();
+            if (buffer instanceof ByteBuffer) {
+                ByteBuffer originalIndex = (ByteBuffer) buffer;
+                ByteBuffer arrayIndex = ByteBuffer.allocate(originalIndex.capacity());
+                originalIndex.clear();
+                arrayIndex.put(originalIndex);
+                indices.updateData(arrayIndex);
+            } else if (buffer instanceof FloatBuffer) {
+                //Floats back to bytes
+                FloatBuffer originalIndex = (FloatBuffer) buffer;
+                ByteBuffer arrayIndex = ByteBuffer.allocate(originalIndex.capacity());
+                originalIndex.clear();
+                for (int i = 0; i < originalIndex.capacity(); i++) {
+                    arrayIndex.put((byte) originalIndex.get(i));
+                }
+                indices.updateData(arrayIndex);
+            }
 
-            // convert weights
+            // convert weights on the heap
             VertexBuffer weights = getBuffer(Type.BoneWeight);
             FloatBuffer originalWeight = (FloatBuffer) weights.getData();
             FloatBuffer arrayWeight = FloatBuffer.allocate(originalWeight.capacity());
             originalWeight.clear();
             arrayWeight.put(originalWeight);
             weights.updateData(arrayWeight);
+        } else {
+            //BoneIndex must be 32 bit for attribute type constraints in shaders
+            VertexBuffer indices = getBuffer(Type.BoneIndex);
+            Buffer buffer = indices.getData();
+            if (buffer instanceof ByteBuffer) {
+                ByteBuffer bIndex = (ByteBuffer) buffer;
+                final float[] rval = new float[bIndex.capacity()];
+                for (int i = 0; i < rval.length; i++) {
+                    rval[i] = bIndex.get(i);
+                }
+                clearBuffer(Type.BoneIndex);
+
+                VertexBuffer ib = new VertexBuffer(Type.BoneIndex);
+                ib.setupData(Usage.Stream,
+                        4,
+                        Format.Float,
+                        BufferUtils.createFloatBuffer(rval));
+                setBuffer(ib);
+            } else if (buffer instanceof FloatBuffer) {
+                //BoneWeights on DirectBuffer
+                FloatBuffer originalIndices = (FloatBuffer) buffer;
+                FloatBuffer arrayIndices = BufferUtils.createFloatBuffer(originalIndices.capacity());
+                originalIndices.clear();
+                arrayIndices.put(originalIndices);
+                indices.setUsage(Usage.Stream);
+                indices.updateData(arrayIndices);
+            }
+
+            //BoneWeights on DirectBuffer
+            VertexBuffer weights = getBuffer(Type.BoneWeight);
+            FloatBuffer originalWeight = (FloatBuffer) weights.getData();
+            FloatBuffer arrayWeight = BufferUtils.createFloatBuffer(originalWeight.capacity());
+            originalWeight.clear();
+            arrayWeight.put(originalWeight);
+            weights.setUsage(Usage.Static);
+            weights.updateData(arrayWeight);
         }
     }
 

+ 2 - 1
engine/src/core/com/jme3/scene/VertexBuffer.java

@@ -145,7 +145,8 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable {
          * Bone indices, used with animation (4 ubytes).
          * If used with software skinning, the usage should be 
          * {@link Usage#CpuOnly}, and the buffer should be allocated
-         * on the heap.
+         * on the heap as a ubytes buffer. For Hardware skinning this should be
+         * either an int or float buffer due to shader attribute types restrictions.
          */
         BoneIndex,