m3g/m3gcore11/src/m3g_skinnedmesh.c
changeset 0 5d03bc08d59c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/m3g/m3gcore11/src/m3g_skinnedmesh.c	Tue Feb 02 01:47:50 2010 +0200
@@ -0,0 +1,2074 @@
+/*
+* Copyright (c) 2003 Nokia Corporation and/or its subsidiary(-ies).
+* All rights reserved.
+* This component and the accompanying materials are made available
+* under the terms of the License "Eclipse Public License v1.0"
+* which accompanies this distribution, and is available
+* at the URL "http://www.eclipse.org/legal/epl-v10.html".
+*
+* Initial Contributors:
+* Nokia Corporation - initial contribution.
+*
+* Contributors:
+*
+* Description: SkinnedMesh implementation
+*
+*/
+
+
+/*!
+ * \internal
+ * \file
+ * \brief SkinnedMesh implementation
+ */
+
+#ifndef M3G_CORE_INCLUDE
+#   error included by m3g_core.c; do not compile separately.
+#endif
+
+#include "m3g_skinnedmesh.h"
+#include "m3g_memory.h"
+#include "m3g_animationtrack.h"
+
+/*----------------------------------------------------------------------
+ * Internal structures
+ *--------------------------------------------------------------------*/
+
+struct BoneRecord
+{
+    Node *node;
+
+    /*! \internal \brief "At-rest" transformation from skinned mesh to bone */
+    Matrix toBone;
+
+    /*! \internal \brief Cached animated transformation for positions */
+    M3Gshort baseMatrix[9];
+    M3Gshort posVec[3];
+    M3Gshort baseExp, posExp, maxExp;
+
+    /*! \internal \brief Cached animated transformation for normals */
+    M3Gshort normalMatrix[9];
+};
+
+/*----------------------------------------------------------------------
+ * Internal functions
+ *--------------------------------------------------------------------*/
+
+/*!
+ * \internal
+ * \brief Destroys this SkinnedMesh object.
+ *
+ * \param obj SkinnedMesh object
+ */
+static void m3gDestroySkinnedMesh(Object *obj)
+{
+    SkinnedMesh *mesh = (SkinnedMesh *) obj;
+    M3G_VALIDATE_OBJECT(mesh);
+    {
+        int i;
+        Interface *m3g = M3G_INTERFACE(mesh);
+        
+        m3gDeleteVertexBuffer(mesh->morphedVB);
+
+        for (i = 0; i < mesh->bonesPerVertex; ++i) {
+            m3gFree(m3g, mesh->boneIndices[i]);
+            m3gFree(m3g, mesh->boneWeights[i]);
+            m3gFree(m3g, mesh->normalizedWeights[i]);
+        }
+        m3gFree(m3g, mesh->weightShifts);
+        
+        for (i = 0; i < m3gArraySize(&mesh->bones); ++i) {
+            m3gFree(m3g, m3gGetArrayElement(&mesh->bones, i));
+        }
+        m3gDestroyArray(&mesh->bones, m3g);
+        
+        if (mesh->skeleton != NULL) {
+            m3gSetParent((Node*) mesh->skeleton, NULL);
+            M3G_ASSIGN_REF(mesh->skeleton, NULL);
+        }
+    }
+    m3gDestroyMesh(obj);
+}
+
+
+/*!
+ * \internal
+ * \brief Get a bone index for a given node
+ *
+ * This finds an existing record if the bone has been added
+ * previously, or creates a new one if no record exists yet.
+ *
+ * \note Inline because only called from AddTransform.
+ */
+static M3G_INLINE M3Gint m3gBoneIndex(SkinnedMesh *mesh, Node *node)
+{
+    PointerArray *boneArray = &mesh->bones;
+    const int numBones = m3gArraySize(boneArray);
+    
+    /* First look for an existing record in the array */
+    {
+        int i;
+        
+        for (i = 0; i < numBones; ++i) {
+            Bone *b = m3gGetArrayElement(boneArray, i);
+            if (b->node == node) {
+                return i;
+            }
+        }
+    }
+
+    /* Not found; create a new one, append to the array, and set up
+     * the "at-rest" transformation for the bone. Note, however, that
+     * we can only store a maximum of 256 bones with byte indices! */
+    {
+        Interface *m3g = M3G_INTERFACE(mesh);
+        
+        if (numBones >= 256) {
+            /* Out of available bone indices */
+            m3gRaiseError(m3g, M3G_OUT_OF_MEMORY);
+            return -1;
+        }
+        else {
+            M3Gint idx;
+            Bone *bone = (Bone*) m3gAllocZ(m3g, sizeof(Bone));
+            if (!bone || !m3gGetTransformTo((Node*) mesh, node,
+                                            &bone->toBone)) {
+                m3gFree(m3g, bone);
+                return -1; /* out of memory or singular transform */
+            }
+            bone->node = node;
+
+            idx = m3gArrayAppend(boneArray, bone, m3g);
+            if (idx < 0) {
+                m3gFree(m3g, bone);
+                return -1; /* out of memory */
+            }
+            return idx;
+        }
+    }
+}
+
+/*!
+ * \internal
+ * \brief Reallocate the per-vertex data if necessary.
+ *
+ * \note Inline because only called from AddTransform.
+ */
+static M3G_INLINE M3Gbool m3gEnsureVertexCount(SkinnedMesh *mesh, M3Gint count)
+{
+    /* Reallocate only if vertex count increased */
+    
+    if (count > mesh->weightedVertexCount) {
+        
+        Interface *m3g = M3G_INTERFACE(mesh);
+
+        int i;
+
+        /* Reallocate the weight shift array */
+        {
+            M3Gubyte *pNew = (M3Gubyte*) m3gAllocZ(m3g, count);
+            if (!pNew) {
+                return M3G_FALSE;
+            }
+            m3gCopy(pNew, mesh->weightShifts, mesh->weightedVertexCount);
+            m3gFree(m3g, mesh->weightShifts);
+            mesh->weightShifts = pNew;
+        }
+
+        /* Reallocate each of the bone index and weight arrays */
+        
+        for (i = 0; i < mesh->bonesPerVertex; ++i) {
+            
+            M3Gubyte *pNew;
+            
+            /* Weights */
+            pNew = (M3Gubyte*) m3gAllocZ(m3g, count);
+            if (!pNew) {
+                return M3G_FALSE; /* out of memory */
+            }
+            m3gCopy(pNew, mesh->boneWeights[i], mesh->weightedVertexCount);
+            m3gFree(m3g, mesh->boneWeights[i]);
+            mesh->boneWeights[i] = pNew;
+            
+            pNew = (M3Gubyte*) m3gAllocZ(m3g, count);
+            if (!pNew) {
+                return M3G_FALSE; /* out of memory */
+            }
+            m3gCopy(pNew, mesh->normalizedWeights[i],
+                    mesh->weightedVertexCount);
+            m3gFree(m3g, mesh->normalizedWeights[i]);
+            mesh->normalizedWeights[i] = pNew;
+
+            /* Indices */
+            pNew = (M3Gubyte*) m3gAllocZ(m3g, count);
+            if (!pNew) {
+                return M3G_FALSE; /* out of memory */
+            }
+            m3gCopy(pNew, mesh->boneIndices[i], mesh->weightedVertexCount);
+            m3gFree(m3g, mesh->boneIndices[i]);
+            mesh->boneIndices[i] = pNew;
+        }
+
+        mesh->weightedVertexCount = count;
+    }
+    return M3G_TRUE;
+}
+    
+/*!
+ * \internal
+ * \brief Reallocate the per-vertex data if necessary.
+ *
+ * \note Inline because only called from AddTransform.
+ */
+static M3G_INLINE M3Gbool m3gEnsureBonesPerVertex(SkinnedMesh *mesh,
+                                                  M3Gint count)
+{
+    M3G_ASSERT(count <= M3G_MAX_VERTEX_TRANSFORMS);
+
+    /* Allocate only if per-vertex bone count increased */
+    
+    if (count > mesh->bonesPerVertex) {
+        
+        Interface *m3g = M3G_INTERFACE(mesh);
+        
+        const M3Gint vertexCount = mesh->weightedVertexCount;
+        M3Gubyte *pNew;
+        
+        int i;
+
+        /* Allocate new arrays for bone indices and weights until
+         * we're satisfied */
+        
+        for (i = mesh->bonesPerVertex; i < count; ++i) {
+            pNew = (M3Gubyte*) m3gAllocZ(m3g, vertexCount);
+            if (!pNew) {
+                goto AllocFailed; /* out of memory */
+            }
+            mesh->boneIndices[i] = pNew;
+            
+            pNew = (M3Gubyte*) m3gAllocZ(m3g, vertexCount);
+            if (!pNew) {
+                goto AllocFailed; /* out of memory */
+            }
+            mesh->boneWeights[i] = pNew;
+            
+            pNew = (M3Gubyte*) m3gAllocZ(m3g, vertexCount);
+            if (!pNew) {
+                goto AllocFailed; /* out of memory */
+            }
+            mesh->normalizedWeights[i] = pNew;
+        }
+
+        mesh->bonesPerVertex = count;
+        return M3G_TRUE;
+
+        /* In case of failure, clean up to keep the bonesPerVertex
+         * counter in sync with the actual number of arrays
+         * allocated */
+    
+    AllocFailed:
+        for (i = mesh->bonesPerVertex; i < count; ++i) {
+            m3gFree(m3g, mesh->boneIndices[i]);
+            m3gFree(m3g, mesh->boneWeights[i]);
+            m3gFree(m3g, mesh->normalizedWeights[i]);
+
+            mesh->boneIndices[i] = NULL;
+            mesh->boneWeights[i] = NULL;
+            mesh->normalizedWeights[i] = NULL;
+        }
+        return M3G_FALSE;
+    }
+    return M3G_TRUE;
+}
+    
+/*!
+ * \internal
+ * \brief Add a new bone influence to a vertex
+ *
+ * If the target vertex is already affected by
+ * M3G_MAX_VERTEX_TRANSFORMS bones, the one with the lowest weight is
+ * discarded.
+ */
+static M3G_INLINE void m3gAddInfluence(SkinnedMesh *mesh,
+                                       M3Gint vertexIndex,
+                                       M3Gint boneIndex,
+                                       M3Gint weight) 
+{
+    M3Gint bonesPerVertex = mesh->bonesPerVertex;
+    M3Guint minWeight = weight;
+    M3Gint minWeightIndex = -1;
+    int i;
+
+    /* Shift the weight into the same scale with the other weights for
+     * this vertex. */
+
+    weight >>= mesh->weightShifts[vertexIndex];
+
+    /* Look for an existing weight for our bone, or find the index
+     * with the lowest weight if not found, and store it in
+     * minWeightIndex.  Note that we're not separately tagging indices
+     * as used/unused; unused ones will merely have a weight of
+     * zero. */
+        
+    for (i = 0; i < bonesPerVertex; ++i) {
+
+        /* If we find an existing weight for our bone, just add to
+         * that and break out. Otherwise, keep track of the minimum
+         * weight encountered so far. */
+        
+        if (mesh->boneIndices[i][vertexIndex] == boneIndex) {
+            weight += mesh->boneWeights[i][vertexIndex];
+            minWeightIndex = i;
+            break;
+        }
+        else {
+            M3Guint tempWeight = mesh->boneWeights[i][vertexIndex];
+            if (tempWeight < minWeight) {
+                minWeight = tempWeight;
+                minWeightIndex = i;
+            }
+        }
+    }
+
+    /* Check whether our total weight exceeds the allocated range,
+     * shifting all existing weights down if necessary */
+
+    while (weight >= (1 << 8)) { /* byte range */
+        weight >>= 1;
+        mesh->weightShifts[vertexIndex] += 1;
+        for (i = 0; i < bonesPerVertex; ++i) {
+            mesh->boneWeights[i][vertexIndex] >>= 1;
+        }
+        M3G_ASSERT(mesh->weightShifts[vertexIndex] <= 31);
+    }
+
+    /* Add the index and weight contribution of the new
+     * transformation, provided that the minimum weight found was
+     * indeed smaller than the one we're adding */
+        
+    if (minWeightIndex >= 0) {
+        mesh->boneIndices[minWeightIndex][vertexIndex] = (M3Gubyte) boneIndex;
+        mesh->boneWeights[minWeightIndex][vertexIndex] = (M3Gubyte) weight;
+
+        /* Need an update of the normalizing scales, too, as well as
+         * the actual transformed vertices */
+        
+        mesh->weightsDirty = M3G_TRUE;
+        m3gInvalidateNode((Node*) mesh, NODE_TRANSFORMS_BIT|NODE_BBOX_BIT);
+    }
+}
+
+/*!
+ * \internal
+ * \brief Computes the normalization scales for vertex weights
+ */
+static void m3gNormalizeWeights(SkinnedMesh *mesh)
+{
+    const M3Gint bonesPerVertex = mesh->bonesPerVertex;
+    const M3Gint vertexCount = mesh->weightedVertexCount;
+    M3Gint vi;
+
+    for (vi = 0; vi < vertexCount; ++vi) {
+        M3Gint k;
+        
+        /* Sum up the 8-bit (possibly downshifted) weights */
+        
+        M3Guint sum = 0;
+        for (k = 0; k < bonesPerVertex; ++k) {
+            sum += mesh->boneWeights[k][vi];
+        }
+
+        /* Compute an 8.24 reciprocal of the weights, then scale with
+         * that to normalize, and shift to 1.7 fixed point */
+        {
+            M3Guint s = (sum > 0 ? (1U << 24) / sum : 0);
+
+            sum = 0;
+            for (k = 0; k < bonesPerVertex; ++k) {
+                M3Guint normalized = (s * mesh->boneWeights[k][vi]) >> 17;
+                M3G_ASSERT(m3gInRange((M3Gint)normalized, 0, 128));
+                sum += normalized;
+                mesh->normalizedWeights[k][vi] = (M3Gubyte) normalized;
+            }
+            
+            /* NOTE there is a maximum of ½ rounding error per
+             * component, plus the rounding error from the reciprocal
+             * calculation, so the sum of weights will often not sum
+             * to 128 exactly! We therefore only assert against
+             * clearly out-of-range values here */
+            
+            M3G_ASSERT(sum == 0 || m3gInRange((M3Gint) sum, 96, 128));
+        }
+    }
+
+    mesh->weightsDirty = M3G_FALSE;
+}
+
+/*!
+ * \internal
+ * \brief Computes an optimal exponent value for a fixed point
+ * transformation
+ *
+ * This scales the translation exponent up to optimally utilize the
+ * 32-bit intermediate precision if the matrix exponent is smaller.
+ */
+static M3Gint m3gOptimalExponent(M3Gint matrixExp, M3Gint transExp)
+{
+    M3Gint maxExp = matrixExp;
+    M3Gint shift = transExp - matrixExp;
+    if (shift > 0) {
+
+        /* The matrix part will always occupy less than half of the
+         * available range if shifted down by at least one bit, so we
+         * can shift the translation up by a maximum of 15 bits.  If
+         * the matrix is shifted by more than 31 bits, it will always
+         * flush to zero, freeing the full 32-bit range for the
+         * translation alone. */
+
+        if (shift >= 32) {      /* matrix will flush to zero */
+            shift = 16;
+        }
+        else if (shift >= 16) { /* matrix always < half of the range */
+            shift = 15;
+        }
+        else {
+            shift -= 1;     /* shift matrix by at least one bit */
+        }
+        
+        maxExp = transExp - shift;
+    }
+    
+    M3G_ASSERT(maxExp >= matrixExp && maxExp >= transExp - 16);
+    return maxExp;
+}
+
+/*
+ * \brief Fixed point vertex transformation
+ *
+ * \param mtx        pointer to a 3x3 16-bit matrix
+ * \param mtxExp     exponent for the matrix elements (upshift from int)
+ * \param trans      pointer to 3-element 16-bit translation vector
+ * \param transExp   exponent for the translation vector
+ * \param maxExp     precalculated "optimal" exponent
+ * \param vx         vertex X coordinate (16-bit range)
+ * \param vy         vertex Y coordinate (16-bit range)
+ * \param vz         vertex Z coordinate (16-bit range)
+ * \param out        output vertex, 25 bits of precision
+ * \return exponent value for \c out
+ */
+static M3Gint m3gFixedPointTransform(const M3Gshort *mtx, M3Gint mtxExp,
+                                     const M3Gshort *trans, M3Gint transExp,
+                                     M3Gint maxExp,
+                                     M3Gint vx, M3Gint vy, M3Gint vz,
+                                     M3Gint *out)
+{
+    M3Gint shift;
+    M3Gint ox = 0, oy = 0, oz = 0;
+    
+    /* First put in the translation part, upscaled to the optimal
+     * range for this bone */
+
+    if (trans) {
+        shift = maxExp - (transExp - 16);
+        M3G_ASSERT(shift >= 0);
+        if (shift < 32) {
+            ox += ((M3Gint) trans[0] << 16) >> shift;
+            oy += ((M3Gint) trans[1] << 16) >> shift;
+            oz += ((M3Gint) trans[2] << 16) >> shift;
+        }
+    }
+        
+    /* Add the input multiplied with the base 3x3 matrix and shifted
+     * to the "maxExp" scale, provided that it has any effect on the
+     * outcome */
+    
+    shift = maxExp - mtxExp;
+    M3G_ASSERT(shift >= 0);
+    if (shift < 32) {
+        
+#       if defined(M3G_DEBUG)
+        M3Gint iMin = (-1 << 31) + (65535 * 32768 >> shift);
+        M3Gint iMax = (M3Gint)((1u << 31)-1) - (65535 * 32768 >> shift);
+        M3G_ASSERT(m3gInRange(ox, iMin, iMax));
+        M3G_ASSERT(m3gInRange(oy, iMin, iMax));
+        M3G_ASSERT(m3gInRange(oz, iMin, iMax));
+#       endif /* M3G_DEBUG */
+        
+        ox += (mtx[0] * vx + mtx[3] * vy + mtx[6] * vz) >> shift;
+        oy += (mtx[1] * vx + mtx[4] * vy + mtx[7] * vz) >> shift;
+        oz += (mtx[2] * vx + mtx[5] * vy + mtx[8] * vz) >> shift;
+    }
+
+    /* Shift the output down to fit into 25 bits; we're dropping 7
+     * bits of precision here, so adjust the exponent accordingly */
+
+    out[0] = ox >> 7;
+    out[1] = oy >> 7;
+    out[2] = oz >> 7;
+    return maxExp + 7;
+}
+
+/*!
+ * \internal
+ * \brief Applies scale and bias to a vertex
+ *
+ * This is required for vertices that have no bones attached.
+ * 
+ * \param mesh    the SkinnedMesh object
+ * \param vx      vertex X coordinate (16-bit range)
+ * \param vy      vertex Y coordinate (16-bit range)
+ * \param vz      vertex Z coordinate (16-bit range)
+ * \param upshift scaling value for the input coordinates and the
+ *                translation component of the transformation
+ * \param vertex  output vertex position
+ * \return exponent value for \c vertex
+ */
+static M3Gint m3gScaleAndBiasVertex(const SkinnedMesh *mesh,
+                                    M3Gint vx, M3Gint vy, M3Gint vz,
+                                    M3Gint upshift,
+                                    M3Gshort *vertex)
+{
+    M3Gint temp[3];
+    M3Gint expo;
+
+    M3G_ASSERT(m3gInRange(vx, -1 << 15, (1 << 15) - 1));
+    M3G_ASSERT(m3gInRange(vy, -1 << 15, (1 << 15) - 1));
+    M3G_ASSERT(m3gInRange(vz, -1 << 15, (1 << 15) - 1));
+    
+    expo = m3gFixedPointTransform(mesh->scaleMatrix, mesh->scaleExp,
+                                  mesh->biasVector, mesh->biasExp + upshift,
+                                  mesh->scaleBiasExp,
+                                  vx << upshift, vy << upshift, vz << upshift,
+                                  temp) - upshift;
+
+    /* Scale down from 25 to 16 bits, adjusting the exponent
+     * accordingly */
+    
+    vertex[0] = (M3Gshort)(temp[0] >> 9);
+    vertex[1] = (M3Gshort)(temp[1] >> 9);
+    vertex[2] = (M3Gshort)(temp[2] >> 9);
+    expo += 9;
+    
+    M3G_ASSERT(m3gInRange(expo, -127, 127));
+    return expo;
+}
+
+/*!
+ * \internal
+ * \brief Computes the blended position for a single vertex
+ *
+ * \param mesh    the SkinnedMesh object
+ * \param vidx    vertex index (for accessing bone data)
+ * \param vx      vertex X coordinate (16-bit range)
+ * \param vy      vertex Y coordinate (16-bit range)
+ * \param vz      vertex Z coordinate (16-bit range)
+ * \param upshift scaling value for the input coordinates and the
+ *                translation component of the transformation
+ * \param vertex  output vertex position
+ * \return exponent value for \c vertex
+ */
+static M3Gint m3gBlendVertex(const SkinnedMesh *mesh,
+                             M3Gint vidx,
+                             M3Gint vx, M3Gint vy, M3Gint vz,
+                             M3Gint upshift,
+                             M3Gshort *vertex)
+{
+    const M3Gint boneCount = mesh->bonesPerVertex;
+    const PointerArray *boneArray = &mesh->bones;
+    M3Gint i;
+    
+    M3Gint outExp = -128;
+    M3Gint sumWeights = 0;
+    
+    M3Gint ox = 0, oy = 0, oz = 0;
+    
+    vx <<= upshift;
+    vy <<= upshift;
+    vz <<= upshift;
+
+    M3G_ASSERT(m3gInRange(vx, -1 << 15, (1 << 15) - 1));
+    M3G_ASSERT(m3gInRange(vy, -1 << 15, (1 << 15) - 1));
+    M3G_ASSERT(m3gInRange(vz, -1 << 15, (1 << 15) - 1));
+
+    /* Loop over the bones and sum the contribution from each */
+    
+    for (i = 0; i < boneCount; ++i) {
+        
+        M3Gint weight = (M3Gint) mesh->normalizedWeights[i][vidx];
+        sumWeights += weight;
+
+        /* Skip bones with zero weights */
+        
+        if (weight > 0) {
+            
+            const Bone *bone = (const Bone *)
+                m3gGetArrayElement(boneArray, mesh->boneIndices[i][vidx]);
+            M3Gint temp[3];
+            M3Gint shift;
+
+            shift = m3gFixedPointTransform(bone->baseMatrix, bone->baseExp,
+                                           bone->posVec, bone->posExp + upshift,
+                                           bone->maxExp,
+                                           vx, vy, vz,
+                                           temp);
+
+            shift = outExp - shift;
+            if (shift < 0) {
+                shift = -shift;
+                if (shift < 31) {
+                    ox >>= shift;
+                    oy >>= shift;
+                    oz >>= shift;
+                }
+                else {
+                    ox = oy = oz = 0;
+                }
+                outExp += shift;
+                shift = 0;
+            }
+
+            /* Apply the vertex weights: 1.7 * 25.0 -> 26.7, but since
+             * the weights are positive and sum to 1, we should stay
+             * within the 32-bit range */
+            
+            if (shift < 31) {
+                
+                M3G_ASSERT(m3gInRange(temp[0], -1 << 24, (1 << 24) - 1));
+                M3G_ASSERT(m3gInRange(temp[1], -1 << 24, (1 << 24) - 1));
+                M3G_ASSERT(m3gInRange(temp[2], -1 << 24, (1 << 24) - 1));
+                
+                ox += (weight * temp[0]) >> shift;
+                oy += (weight * temp[1]) >> shift;
+                oz += (weight * temp[2]) >> shift;
+            }
+        }
+    }
+
+    /* Before returning, we still need to check for the special case
+     * of all-zero weights, and shift the values from the post-scaling
+     * 32-bit precision back into the 16-bit range; we're essentially
+     * dropping the (25 - 16) bits of the blended result, so the
+     * exponent must change accordingly */
+
+    if (sumWeights > 0) {
+        vertex[0] = (M3Gshort)(ox >> 16);
+        vertex[1] = (M3Gshort)(oy >> 16);
+        vertex[2] = (M3Gshort)(oz >> 16);
+        outExp = outExp - upshift + 9;
+
+        M3G_ASSERT(m3gInRange(outExp, -127, 127));
+        return outExp;
+    }
+    else {
+        vx >>= upshift;
+        vy >>= upshift;
+        vz >>= upshift;
+        return m3gScaleAndBiasVertex(mesh, vx, vy, vz, upshift, vertex);
+    }
+}
+
+/*!
+ * \internal
+ * \brief Computes the blended normal vector for a single vertex
+ *
+ * \param mesh    the SkinnedMesh object
+ * \param vidx    vertex index (for accessing bone data)
+ * \param nx      normal X coordinate (16-bit range)
+ * \param ny      normal Y coordinate (16-bit range)
+ * \param nz      normal Z coordinate (16-bit range)
+ * \param upshift scaling for input coordinates to increase precision
+ * \param normal  output normal vector (8-bit range!)
+ * \return a shift value for the output vertex (scale from integer)
+ */
+static void m3gBlendNormal(const SkinnedMesh *mesh,
+                           M3Gint vidx,
+                           M3Gint nx, M3Gint ny, M3Gint nz,
+                           M3Gint upshift,
+                           M3Gbyte *normal)
+{
+    const M3Gint boneCount = mesh->bonesPerVertex;
+    const PointerArray *boneArray = &mesh->bones;
+    M3Gint i;
+    
+    M3Gint outExp = -128;
+    M3Gint sumWeights = 0;
+    
+    M3Gint ox = 0, oy = 0, oz = 0;
+
+    nx <<= upshift;
+    ny <<= upshift;
+    nz <<= upshift;
+    
+    M3G_ASSERT(m3gInRange(nx, -1 << 15, (1 << 15) - 1));
+    M3G_ASSERT(m3gInRange(ny, -1 << 15, (1 << 15) - 1));
+    M3G_ASSERT(m3gInRange(nz, -1 << 15, (1 << 15) - 1));
+
+    /* Loop over the bones and sum the contribution from each */
+    
+    for (i = 0; i < boneCount; ++i) {
+        
+        M3Gint weight = (M3Gint) mesh->normalizedWeights[i][vidx];
+        sumWeights += weight;
+
+        /* Skip bones with zero weights */
+        
+        if (weight > 0) {
+            
+            const Bone *bone = (const Bone *)
+                m3gGetArrayElement(boneArray, mesh->boneIndices[i][vidx]);
+            M3Gint temp[3];
+            M3Gint shift;
+
+            shift = m3gFixedPointTransform(bone->normalMatrix, 0,
+                                           NULL, 0,
+                                           0,
+                                           nx, ny, nz,
+                                           temp);
+
+            shift = outExp - shift;
+            if (shift < 0) {
+                shift = -shift;
+                if (shift < 31) {
+                    ox >>= shift;
+                    oy >>= shift;
+                    oz >>= shift;
+                }
+                else {
+                    ox = oy = oz = 0;
+                }
+                outExp += shift;
+                shift = 0;
+            }
+
+            /* Apply the vertex weights: 1.7 * 25.0 -> 26.7, but since
+             * the weights are positive and sum to 1, we should stay
+             * within the 32-bit range */
+            
+            if (shift < 31) {
+                
+                M3G_ASSERT(m3gInRange(temp[0], -1 << 24, (1 << 24) - 1));
+                M3G_ASSERT(m3gInRange(temp[1], -1 << 24, (1 << 24) - 1));
+                M3G_ASSERT(m3gInRange(temp[2], -1 << 24, (1 << 24) - 1));
+                
+                ox += (weight * temp[0]) >> shift;
+                oy += (weight * temp[1]) >> shift;
+                oz += (weight * temp[2]) >> shift;
+            }
+        }
+    }
+
+    /* Before returning, we still need to check for the special case
+     * of all-zero weights, and shift the values from the post-scaling
+     * 32-bit precision down into the 8-bit range */
+
+    if (sumWeights > 0) {
+        normal[0] = (M3Gbyte)(ox >> 24);
+        normal[1] = (M3Gbyte)(oy >> 24);
+        normal[2] = (M3Gbyte)(oz >> 24);
+    }
+    else {
+        normal[0] = (M3Gbyte)(ox >> 8);
+        normal[1] = (M3Gbyte)(oy >> 8);
+        normal[2] = (M3Gbyte)(oz >> 8);
+    }
+}
+
+/*!
+ * \internal
+ * \brief Updates internal vertex buffer
+ *
+ * \param mesh SkinnedMesh object
+ *
+ * \retval M3G_TRUE VertexBuffer is up to date
+ * \retval M3G_FALSE Failed to update VertexBuffer, out of memory exception raised
+ */
+static M3Gbool m3gSkinnedMeshUpdateVB(SkinnedMesh *mesh)
+{
+    M3Gint vbTimestamp;
+    M3G_ASSERT(mesh->mesh.vertexBuffer != NULL);
+    M3G_ASSERT(mesh->morphedVB != NULL);
+    
+    /* Source vertex buffer array configuration changed since last
+     * update? */
+
+    vbTimestamp = m3gGetTimestamp(mesh->mesh.vertexBuffer);
+    
+    if (mesh->vbTimestamp != vbTimestamp) {
+        Interface *m3g = M3G_INTERFACE(mesh);
+        VertexArray *array;
+        M3Gint vcount = m3gGetVertexCount(mesh->mesh.vertexBuffer);
+
+        /* Must ensure that our internal morphing buffer matches the
+         * configuration of the source buffer, with dedicated arrays
+         * for the morphed positions and normals */
+        
+        if (!m3gMakeModifiedVertexBuffer(mesh->morphedVB,
+                                         mesh->mesh.vertexBuffer,
+                                         M3G_POSITION_BIT|M3G_NORMAL_BIT,
+                                         M3G_FALSE)) {
+            return M3G_FALSE; /* out of memory */
+        }
+
+        /* We always have the vertex positions as shorts, but the
+         * array may not be actually initialized yet, so we must check
+         * whether to create a copy or not */
+
+        if (mesh->mesh.vertexBuffer->vertices) {
+            array = m3gCreateVertexArray(m3g, vcount, 3, M3G_SHORT);
+            if (!array) {
+                return M3G_FALSE;
+            }
+            m3gSetVertexArray(mesh->morphedVB, array, 1.f, NULL, 0);
+        }
+
+        /* Normals (always bytes) only exist if in the original VB */
+        
+        if (mesh->mesh.vertexBuffer->normals) {
+            array = m3gCreateVertexArray(m3g, vcount, 3, M3G_BYTE);
+            if (!array) {
+                return M3G_FALSE;
+            }
+            m3gSetNormalArray(mesh->morphedVB, array);
+        }
+    
+        mesh->vbTimestamp = vbTimestamp;
+    }
+    
+    /* The default color must always be updated, because it can be
+     * animated (doesn't affect timestamp) */
+    
+    mesh->morphedVB->defaultColor = mesh->mesh.vertexBuffer->defaultColor;
+    return M3G_TRUE;
+}
+
+
+/*!
+ * \internal
+ * \brief Gets the transformation(s) for a single bone record
+ *
+ * Also stores the normal transformation matrix if needed.
+ *
+ * \param mesh       pointer to the mesh object
+ * \param bone       pointer to the bone record
+ * \param hasNormals flag indicating whether the normals transformation
+ *                   should be computed and cached in the bone record
+ * \param mtx        matrix to store the vertex transformation in
+ */
+static M3G_INLINE M3Gbool m3gGetBoneTransformInternal(SkinnedMesh *mesh,
+                                              Bone *bone,
+                                              M3Gbool hasNormals,
+                                              Matrix *mtx)
+{
+    const VertexBuffer *vb = mesh->mesh.vertexBuffer;
+
+    /* Get the vertex transformation and concatenate it with the
+     * at-rest matrix and the vertex scale and bias transformations.
+     * The resulting 3x4 transformation matrix is then split into a
+     * fixed point 3x3 matrix and translation vector */
+    
+    if (!m3gGetTransformTo(bone->node, (Node*) mesh, mtx)) {
+        return M3G_FALSE; /* no path or singular transform */
+    }
+    m3gMulMatrix(mtx, &bone->toBone);
+
+    /* If normals are enabled, compute and store the inverse transpose
+     * matrix for transforming normals at this stage */
+    
+    if (hasNormals) {
+        Matrix t;
+        if (!m3gInverseTranspose(&t, mtx)) {
+            m3gRaiseError(M3G_INTERFACE(mesh), M3G_ARITHMETIC_ERROR);
+            return M3G_FALSE; /* singular transform */
+        }
+        m3gGetFixedPoint3x3Basis(&t, bone->normalMatrix);
+    }
+
+    /* Apply the vertex bias and scale to the transformation */
+    
+    m3gTranslateMatrix(
+        mtx, vb->vertexBias[0], vb->vertexBias[1], vb->vertexBias[2]);
+    m3gScaleMatrix(mtx, vb->vertexScale, vb->vertexScale, vb->vertexScale);
+    
+    return M3G_TRUE;
+}
+
+/*!
+ * \internal
+ * \brief Compute and cache the bone transformations for morphing
+ *
+ * \param mesh     the SkinnedMesh object
+ * \param posShift vertex position value "gain"
+ */
+static M3Gbool m3gPreComputeTransformations(SkinnedMesh *mesh,
+                                            M3Gint posShift,
+                                            M3Gbool hasNormals)
+{
+    M3Gint boneCount = m3gArraySize(&mesh->bones);
+    M3Gint i;
+    Matrix *tBone = NULL;
+
+    /* First, just compute the floating point transformation matrices
+     * for the bones, caching them in a temp array */
+
+    if (boneCount > 0) {
+        tBone = m3gAllocTemp(M3G_INTERFACE(mesh), boneCount * sizeof(Matrix));
+        if (!tBone) {
+            return M3G_FALSE; /* out of memory */
+        }    
+        for (i = 0; i < boneCount; ++i) {
+            Bone *bone = m3gGetArrayElement(&mesh->bones, i);
+            if (!m3gGetBoneTransformInternal(mesh, bone, hasNormals, &tBone[i])) {
+                return M3G_FALSE;
+            }
+        }
+    }
+
+    /* Find the value range of the bone translations, and offset the
+     * bones to center output vertex values (roughly) around the
+     * origin */
+    {
+        const VertexBuffer *vb = mesh->mesh.vertexBuffer;
+        M3Gfloat min[3], max[3], bias[3];
+        M3Gint maxExp;
+        Vec4 t;
+
+        /* Find the minimum and maximum values; start with the plain
+         * vertex bias for non-weighted bones */
+        
+        min[0] = max[0] = vb->vertexBias[0];
+        min[1] = max[1] = vb->vertexBias[1];
+        min[2] = max[2] = vb->vertexBias[2];
+        
+        for (i = 0; i < boneCount; ++i) {
+            m3gGetMatrixColumn(&tBone[i], 3, &t); 
+            min[0] = M3G_MIN(min[0], t.x);
+            max[0] = M3G_MAX(max[0], t.x);
+            min[1] = M3G_MIN(min[1], t.y);
+            max[1] = M3G_MAX(max[1], t.y);
+            min[2] = M3G_MIN(min[2], t.z);
+            max[2] = M3G_MAX(max[2], t.z);
+        }
+        
+        /* Divide to get the mean translation, store in the
+         * destination VB, and invert for de-biasing the bones */
+        
+        for (i = 0; i < 3; ++i) {
+            bias[i] = m3gMul(0.5f, m3gAdd(min[i], max[i]));
+            mesh->morphedVB->vertexBias[i] = bias[i];
+            bias[i] = m3gNegate(bias[i]);
+        }
+        
+        /* Offset bones by the (now inverted) bias vector, and store
+         * the fixed point matrix & vector parts in the bone record;
+         * also set the maximum bone exponent into the mesh */
+
+        maxExp = -128;
+        for (i = 0; i < boneCount; ++i) {
+            Bone *bone = m3gGetArrayElement(&mesh->bones, i);
+            m3gPreTranslateMatrix(&tBone[i], bias[0], bias[1], bias[2]); /*lint !e613 tBone not null if boneCount > 0 */
+            
+            bone->baseExp = (M3Gshort)
+                m3gGetFixedPoint3x3Basis(&tBone[i], bone->baseMatrix); /*lint !e613 tBone not null if boneCount > 0 */
+            bone->posExp = (M3Gshort)
+                m3gGetFixedPointTranslation(&tBone[i], bone->posVec); /*lint !e613 tBone not null if boneCount > 0 */
+            bone->maxExp = (M3Gshort)
+                m3gOptimalExponent(bone->baseExp, bone->posExp + posShift);
+
+            maxExp = M3G_MAX(maxExp, bone->maxExp);
+        }
+
+        /* Make a fixed-point matrix for applying the scale and bias as
+         * well, for vertices not attached to any bone (this is not the
+         * optimal way to store the information, but we can just reuse
+         * existing code this way) */
+        {
+            Matrix sb;
+            m3gTranslationMatrix(&sb,
+                                 m3gAdd(bias[0], vb->vertexBias[0]),
+                                 m3gAdd(bias[1], vb->vertexBias[1]),
+                                 m3gAdd(bias[2], vb->vertexBias[2]));
+            m3gScaleMatrix(&sb,
+                           vb->vertexScale, vb->vertexScale, vb->vertexScale);
+            
+            mesh->scaleExp = (M3Gshort)
+                m3gGetFixedPoint3x3Basis(&sb, mesh->scaleMatrix);
+            mesh->biasExp = (M3Gshort)
+                m3gGetFixedPointTranslation(&sb, mesh->biasVector);
+            mesh->scaleBiasExp = (M3Gshort)
+                m3gOptimalExponent(mesh->scaleExp, mesh->biasExp + posShift);
+        
+            maxExp = M3G_MAX(mesh->scaleBiasExp, maxExp);
+        }
+
+        /* Compute the maximum post-blending exponent and store it as the
+         * morphed vertex buffer scale -- this is dependent on the
+         * implementations of m3gBlendVertex, m3gScaleAndBiasVertex, and
+         * m3gFixedPointTransform! */
+
+        maxExp = maxExp + 16 - posShift;
+        M3G_ASSERT(m3gInRange(maxExp, -127, 127));
+        *(M3Gint*)&mesh->morphedVB->vertexScale = (maxExp + 127) << 23;
+        mesh->maxExp = (M3Gshort) maxExp;
+    }
+    
+    if (boneCount > 0) {
+        m3gFreeTemp(M3G_INTERFACE(mesh));
+    }
+    
+    return M3G_TRUE;
+}
+
+/*!
+ * \internal
+ * \brief Computes derived data required for bounding volumes and skinning
+ */
+static M3Gbool m3gSkinnedMeshPreMorph(SkinnedMesh *mesh)
+{
+    const VertexBuffer *srcVB = mesh->mesh.vertexBuffer;
+    M3Gint posShift = 0, normalShift = 0;
+    
+    /* Compute upscaling shift values for positions and normals so
+     * that we can maximize precision even for absurdly small
+     * vertex values */
+    {
+        M3Gint minVal, maxVal;
+        
+        if (srcVB->normals) {
+            m3gGetArrayValueRange(srcVB->normals, &minVal, &maxVal);
+            maxVal = M3G_MAX(-minVal, maxVal);
+            M3G_ASSERT(maxVal >= 0);
+            if (maxVal) {
+                while ((maxVal << normalShift) < (1 << 14)) {
+                    ++normalShift;
+                }
+            }
+        }
+            
+        m3gGetArrayValueRange(srcVB->vertices, &minVal, &maxVal);
+        maxVal = M3G_MAX(-minVal, maxVal);
+        M3G_ASSERT(maxVal >= 0);
+        if (maxVal) {
+            while ((maxVal << posShift) < (1 << 14)) {
+                ++posShift;
+            }
+        }
+        
+        mesh->posShift    = (M3Gshort) posShift;
+        mesh->normalShift = (M3Gshort) normalShift;
+    }
+
+    /* Now that we can compute the optimized exponents for the
+     * transformations based on the position upshift value, let's
+     * resolve the bone transformations; this will also cache the
+     * maximum bone exponent in mesh->maxExp */
+
+    if (!m3gPreComputeTransformations(mesh,
+                                      posShift,
+                                      srcVB->normals != NULL)) { 
+        return M3G_FALSE; /* invalid transform */
+    }
+    
+    return M3G_TRUE;
+}
+
+/*!
+ * \internal
+ * \brief Does the actual vertex morphing into the internal vertex buffer
+ *
+ * \param mesh   SkinnedMesh object
+ * \retval M3G_TRUE     skinning ok
+ * \retval M3G_FALSE    skinning failed, exception raised
+ */
+static void m3gSkinnedMeshMorph(SkinnedMesh *mesh)
+{
+    const VertexBuffer *srcVB = mesh->mesh.vertexBuffer;
+    const void *srcPositions;
+    const void *srcNormals = NULL;
+    VertexBuffer *dstVB = mesh->morphedVB;
+    M3Gshort *dstPositions;
+    M3Gbyte *dstNormals = NULL;
+    M3Gint vertexCount = mesh->weightedVertexCount;
+    M3Gint maxExp = mesh->maxExp;
+    M3Gint posShift = mesh->posShift, normShift = mesh->normalShift;
+    M3Gint i;
+
+    M3G_ASSERT(!((Node*) mesh)->dirtyBits);
+    
+    /* Let's update the vertex weights if we need to */
+        
+    if (mesh->weightsDirty) {
+        m3gNormalizeWeights(mesh);
+    }
+
+    /* Get pointers to source and destination position and normal
+     * data; the latter will always be shorts and bytes,
+     * respectively, while the former can be either */
+        
+    srcPositions = m3gMapVertexArrayReadOnly(srcVB->vertices);
+    dstPositions = (M3Gshort*) m3gMapVertexArray(dstVB->vertices);
+    if (srcVB->normals) {
+        srcNormals = m3gMapVertexArrayReadOnly(srcVB->normals);
+        dstNormals = (M3Gbyte*) m3gMapVertexArray(dstVB->normals);
+    }
+        
+    /* Transform the vertices that are affected by bones */
+    {
+        M3Gshort *dst = dstPositions;
+            
+        if (srcVB->vertices->elementType == GL_BYTE) {
+            const M3Gbyte *src = (const M3Gbyte*) srcPositions;
+            for (i = 0; i < vertexCount; ++i) {
+                M3Gint shift =
+                    maxExp - m3gBlendVertex(mesh, i,
+                                            src[0], src[1], src[2],
+                                            posShift,
+                                            dst);
+                if (shift > 31) {
+                    *dst++ = 0;
+                    *dst++ = 0;
+                    *dst++ = 0;
+                }
+                else {
+                    *dst++ >>= shift;
+                    *dst++ >>= shift;
+                    *dst++ >>= shift;
+                }
+            
+                src += 4; /* byte data always padded to 32 bits */
+            }
+        }
+        else {
+            const M3Gshort *src = (const M3Gshort*) srcPositions;
+            for (i = 0; i < vertexCount; ++i) {
+                M3Gint shift =
+                    maxExp - m3gBlendVertex(mesh, i,
+                                            src[0], src[1], src[2],
+                                            posShift,
+                                            dst);
+                if (shift > 31) {
+                    *dst++ = 0;
+                    *dst++ = 0;
+                    *dst++ = 0;
+                }
+                else {
+                    *dst++ >>= shift;
+                    *dst++ >>= shift;
+                    *dst++ >>= shift;
+                }
+                
+                src += 3;
+            }
+        }
+    }
+
+    /* Transform the normals (if enabled).  Normals will be
+     * normalized when rendering, so no need to keep track of
+     * scales here */
+        
+    if (srcNormals) {
+        M3Gbyte *dst = dstNormals;
+            
+        if (srcVB->normals->elementType == GL_BYTE) {
+            const M3Gbyte *src = (const M3Gbyte*) srcNormals;
+            for (i = 0; i < vertexCount; ++i) {
+                m3gBlendNormal(mesh, i,
+                               src[0], src[1], src[2],
+                               normShift,
+                               dst);
+                src += 4; /* byte data padded to 32 bits */
+                dst += 4; 
+            }
+        }
+        else {
+            const M3Gshort *src = (const M3Gshort*) srcNormals;
+            for (i = 0; i < vertexCount; ++i) {
+                m3gBlendNormal(mesh, i,
+                               src[0], src[1], src[2],
+                               normShift,
+                               dst);
+                src += 3;
+                dst += 4; 
+            }
+        }
+    }
+
+    /* Finally, handle the remaining vertices, which have no bones
+     * attached; these just need to have the scale and bias
+     * applied */
+
+    vertexCount = m3gGetNumVertices(srcVB);
+    if (i < vertexCount) {
+            
+        M3Gint startIndex = i;
+        M3Gshort *dstPos = dstPositions + startIndex * 3;
+        M3Gshort temp[3];
+            
+        if (srcVB->vertices->elementType == GL_BYTE) {
+            const M3Gbyte *src = ((const M3Gbyte*) srcPositions) + startIndex * 4;
+            for (i = startIndex ; i < vertexCount; ++i) {
+                M3Gint shift =
+                    maxExp - m3gScaleAndBiasVertex(mesh,
+                                                   src[0], src[1], src[2],
+                                                   posShift,
+                                                   temp);
+                *dstPos++ = (M3Gshort)(temp[0] >> shift);
+                *dstPos++ = (M3Gshort)(temp[1] >> shift);
+                *dstPos++ = (M3Gshort)(temp[2] >> shift);                    
+                src += 4; /* byte data, padded to 32 bits */
+            }
+        }
+        else {
+            const M3Gshort *src = ((const M3Gshort*) srcPositions) + startIndex * 3;
+            for (i = startIndex ; i < vertexCount; ++i) {
+                M3Gint shift =
+                    maxExp - m3gScaleAndBiasVertex(mesh,
+                                                   src[0], src[1], src[2],
+                                                   posShift,
+                                                   temp);
+                *dstPos++ = (M3Gshort)(temp[0] >> shift);
+                *dstPos++ = (M3Gshort)(temp[1] >> shift);
+                *dstPos++ = (M3Gshort)(temp[2] >> shift);                    
+                src += 3;
+            }
+        }
+            
+        /* Byte normals can just use a memcopy, as we don't have
+         * to scale them at all; shorts will require a conversion,
+         * after prescaling with the normal upshift to avoid
+         * underflowing to zero */
+                
+        if (srcNormals) {
+            M3Gbyte *dstNorm = dstNormals + startIndex * 4; 
+            if (srcVB->normals->elementType == GL_BYTE) {
+                const M3Gbyte *src =
+                    ((const M3Gbyte*) srcNormals) + startIndex * 4;
+                m3gCopy(dstNorm, src, (vertexCount - startIndex) * 4);
+            }
+            else {
+                const M3Gshort *src =
+                    ((const M3Gshort*) srcNormals) + startIndex * 3;
+                for (i = startIndex ; i < vertexCount; ++i) {
+                    *dstNorm++ = (M3Gbyte)((*src++ << normShift) >> 8);
+                    *dstNorm++ = (M3Gbyte)((*src++ << normShift) >> 8);
+                    *dstNorm++ = (M3Gbyte)((*src++ << normShift) >> 8);
+                    ++dstNorm; /* again, padding for byte values */
+                }
+            }
+        }
+    }
+        
+    /* All done! Clean up and exit */
+
+    m3gUnmapVertexArray(srcVB->vertices);
+    m3gUnmapVertexArray(dstVB->vertices);
+    if (srcNormals) {
+        m3gUnmapVertexArray(srcVB->normals);
+        m3gUnmapVertexArray(dstVB->normals);
+    }
+}
+
+/*!
+ * \internal
+ * \brief Overloaded Node method.
+ *
+ * Setup skinned mesh render. Call mesh render setup,
+ * do skinning calculations and traverse into the skeleton or the parent
+ *
+ * \param self SkinnedMesh object
+ * \param toCamera transform to camera
+ * \param alphaFactor total alpha factor
+ * \param caller caller node
+ * \param renderQueue RenderQueue
+ *
+ * \retval M3G_TRUE continue render setup
+ * \retval M3G_FALSE abort render setup
+ */
+static M3Gbool m3gSkinnedMeshSetupRender(Node *self,
+                                         const Node *caller,
+                                         SetupRenderState *s,
+                                         RenderQueue *renderQueue)
+{
+    SkinnedMesh *mesh = (SkinnedMesh *)self;
+    Node *skeleton = (Node*) mesh->skeleton;
+    M3Gbool enabled, success = M3G_TRUE;
+    m3gIncStat(M3G_INTERFACE(self), M3G_STAT_RENDER_NODES, 1);
+    
+    /* Optimize the rendering-enable checking for top-down traversal */
+
+    enabled = (self->enableBits & NODE_RENDER_BIT) != 0;
+    if (caller != self->parent) {
+        enabled = m3gHasEnabledPath(self, renderQueue->root);
+        s->cullMask = CULLMASK_ALL;
+    }
+
+    /* Handle self and the skeleton if enabled */
+
+    if (enabled) {
+        
+        /* Traverse into the skeleton unless coming from there */
+    
+        if (skeleton != caller) {
+            SetupRenderState cs;
+            cs.cullMask = s->cullMask;
+        
+            M3G_BEGIN_PROFILE(M3G_INTERFACE(self), M3G_PROFILE_SETUP_TRANSFORMS);
+            m3gGetCompositeNodeTransform(skeleton, &cs.toCamera);
+            m3gPreMultiplyMatrix(&cs.toCamera, &s->toCamera);
+            M3G_END_PROFILE(M3G_INTERFACE(self), M3G_PROFILE_SETUP_TRANSFORMS);
+        
+            success = M3G_VFUNC(Node, skeleton, setupRender)(skeleton,
+                                                             self,
+                                                             &cs,
+                                                             renderQueue);
+        }
+
+        /* Handle self if in scope */
+        
+        if ((self->scope & renderQueue->scope) != 0) {
+
+            /* Try view frustum culling */
+
+#           if defined(M3G_ENABLE_VF_CULLING)
+            m3gUpdateCullingMask(s, renderQueue->camera, &mesh->bbox);
+#           endif
+
+            if (s->cullMask == 0) {
+                m3gIncStat(M3G_INTERFACE(self),
+                           M3G_STAT_RENDER_NODES_CULLED, 1);
+            }
+            else {
+                success &= m3gQueueMesh((Mesh*) self, &s->toCamera, renderQueue);
+                
+                if (success) {
+                    M3G_BEGIN_PROFILE(M3G_INTERFACE(self), M3G_PROFILE_SKIN);
+                    m3gSkinnedMeshMorph(mesh);
+                    M3G_END_PROFILE(M3G_INTERFACE(self), M3G_PROFILE_SKIN);
+                }
+            }
+        }
+    }
+
+    /* Traverse into the parent node unless coming from there.  Again,
+     * discard the old traversal state at this point, as we're not
+     * coming back. */
+    
+    if (success && self != renderQueue->root) {
+        Node *parent = self->parent;
+        if (parent != NULL && parent != caller) {
+            Matrix t;
+            
+            M3G_BEGIN_PROFILE(M3G_INTERFACE(self), M3G_PROFILE_SETUP_TRANSFORMS);
+            if (!m3gGetInverseNodeTransform(self, &t)) {
+                return M3G_FALSE;
+            }
+            m3gMulMatrix(&s->toCamera, &t);
+            M3G_END_PROFILE(M3G_INTERFACE(self), M3G_PROFILE_SETUP_TRANSFORMS);
+
+            success = M3G_VFUNC(Node, parent, setupRender)(parent,
+                                                           self,
+                                                           s,
+                                                           renderQueue);
+        }
+    }
+
+    return success;
+}
+
+/*!
+ * \internal
+ * \brief Overloaded Node method.
+ *
+ * Renders one skinned submesh.
+ *
+ * \param self SkinnedMesh object
+ * \param ctx current render context
+ * \param patchIndex submesh index
+ */
+static void m3gSkinnedMeshDoRender(Node *self,
+                                   RenderContext *ctx,
+                                   const Matrix *toCamera,
+                                   int patchIndex)
+{
+    SkinnedMesh *mesh = (SkinnedMesh *)self;
+    IndexBuffer *indexBuffer = mesh->mesh.indexBuffers[patchIndex];
+    Appearance *appearance = mesh->mesh.appearances[patchIndex];
+
+    if (indexBuffer == NULL || appearance == NULL)
+        return;
+
+    m3gDrawMesh(ctx,
+                mesh->morphedVB,
+                indexBuffer,
+                appearance,
+                toCamera,
+                mesh->mesh.totalAlphaFactor + 1,
+                mesh->mesh.node.scope);
+}
+
+/*!
+ * \internal
+ * \brief Overloaded Node method.
+ *
+ * Do skinning calculations and forward to Mesh internal ray intersect.
+ *
+ * \param self      SkinnedMesh object
+ * \param mask      pick scope mask
+ * \param ray       pick ray
+ * \param ri        RayIntersection object
+ * \param toGroup   transform to originating group
+ * \retval          M3G_TRUE    continue pick
+ * \retval          M3G_FALSE   abort pick
+ */
+static M3Gbool m3gSkinnedMeshRayIntersect(  Node *self,
+                                            M3Gint mask,
+                                            M3Gfloat *ray,
+                                            RayIntersection *ri,
+                                            Matrix *toGroup)
+{
+    SkinnedMesh *mesh = (SkinnedMesh *)self;
+    M3G_BEGIN_PROFILE(M3G_INTERFACE(self), M3G_PROFILE_SKIN);
+
+    if ((((Node *)mesh)->scope & mask) == 0) {
+        return M3G_TRUE;
+    }
+
+    if (!m3gSkinnedMeshPreMorph(mesh)) {
+        return M3G_FALSE;
+    }
+    m3gSkinnedMeshMorph(mesh);
+    M3G_END_PROFILE(M3G_INTERFACE(self), M3G_PROFILE_SKIN);
+    return m3gMeshRayIntersectInternal( &mesh->mesh,
+                                        mesh->morphedVB,
+                                        mask,
+                                        ray,
+                                        ri,
+                                        toGroup);
+}
+
+/*!
+ * \internal
+ * \brief Overloaded Object3D method.
+ *
+ * \param self SkinnedMesh object
+ * \param time current world time
+ * \return minimum validity
+ */
+static M3Gint m3gSkinnedMeshApplyAnimation(Object *self, M3Gint time)
+{
+    SkinnedMesh *mesh = (SkinnedMesh *)self;
+    
+    M3Gint validity = m3gMeshApplyAnimation((Object*) &mesh->mesh, time);
+    
+    if (validity > 0) {
+        M3Gint validity2 =
+            M3G_VFUNC(Object, mesh->skeleton, applyAnimation)(
+                (Object *)mesh->skeleton, time);
+        return (validity < validity2 ? validity : validity2);
+    }
+    return 0;
+}
+
+/*!
+ * \internal
+ * \brief Overloaded Object3D method.
+ *
+ * \param self SkinnedMesh object
+ * \param references array of reference objects
+ * \return number of references
+ */
+static M3Gint m3gSkinnedMeshDoGetReferences(Object *self, Object **references)
+{
+    SkinnedMesh *smesh = (SkinnedMesh *)self;
+    M3Gint num = m3gMeshDoGetReferences(self, references);
+    if (smesh->skeleton != NULL)
+    {
+        if (references != NULL)
+            references[num] = (Object *)smesh->skeleton;
+        num++;
+    }
+    return num;
+}
+
+/*!
+ * \internal
+ * \brief Overloaded Object3D method.
+ */
+static Object *m3gSkinnedMeshFindID(Object *self, M3Gint userID)
+{
+    SkinnedMesh *smesh = (SkinnedMesh *)self;
+    Object *found = m3gMeshFindID(self, userID);
+    
+    if (!found && smesh->skeleton != NULL) {
+        found = m3gFindID((Object*) smesh->skeleton, userID);
+    }
+    return found;
+}
+
+/*!
+ * \internal
+ * \brief Overloaded Object3D method.
+ *
+ * \param originalObj original SkinnedMesh object
+ * \param cloneObj pointer to cloned SkinnedMesh object
+ * \param pairs array for all object-duplicate pairs
+ * \param numPairs number of pairs
+ */
+static M3Gbool m3gSkinnedMeshDuplicate(const Object *originalObj,
+                                       Object **cloneObj,
+                                       Object **pairs,
+                                       M3Gint *numPairs)
+{
+    M3Gint i;
+    SkinnedMesh *original = (SkinnedMesh *)originalObj;
+    Group *skeleton = NULL;
+    SkinnedMesh *clone;
+    M3G_ASSERT(*cloneObj == NULL); /* no derived classes */
+    
+    /* Duplicate the skeleton group first, as this is a prerequisite
+     * for creating the clone SkinnedMesh.  If this fails, we must
+     * manually delete the skeleton, as no record of it will be stored
+     * anywhere else; we also need to hold a reference until ownership
+     * of the skeleton transfers to the clone SkinnedMesh. */
+    
+    if (!M3G_VFUNC(Object, original->skeleton, duplicate)(
+            (Object*) original->skeleton,
+            (Object**) &skeleton, pairs, numPairs)) {
+        m3gDeleteObject((Object*) skeleton);
+        return M3G_FALSE;
+    }
+    m3gAddRef((Object*) skeleton); /* don't leave this floating */
+
+    /* Create the actual clone object */
+    
+    clone = (SkinnedMesh*)
+        m3gCreateSkinnedMesh(originalObj->interface,
+                             original->mesh.vertexBuffer,
+                             original->mesh.indexBuffers,
+                             original->mesh.appearances,
+                             original->mesh.trianglePatchCount,
+                             skeleton);    
+    m3gDeleteRef((Object*) skeleton); /* ownership transferred to clone */
+    if (!clone) {
+        return M3G_FALSE;
+    }
+    *cloneObj = (Object *)clone;
+
+    /* Duplicate base class data; we're OK for normal deletion at this
+     * point, so can just leave it up to the caller on failure */
+    
+    if (!m3gMeshDuplicate(originalObj, cloneObj, pairs, numPairs)) {
+        return M3G_FALSE;
+    }
+
+    /* Duplicate the rest of our own data */
+
+    if (!m3gEnsureVertexCount(clone, original->weightedVertexCount) ||
+        !m3gEnsureBonesPerVertex(clone, original->bonesPerVertex)) {
+        return M3G_FALSE; /* out of memory */
+    }
+    
+    for (i = 0; i < clone->bonesPerVertex; i++) {
+        m3gCopy(clone->boneIndices[i], original->boneIndices[i],
+                clone->weightedVertexCount);
+        m3gCopy(clone->boneWeights[i], original->boneWeights[i],
+                clone->weightedVertexCount);
+        m3gCopy(clone->normalizedWeights[i], original->normalizedWeights[i],
+                clone->weightedVertexCount);
+    }
+    clone->weightsDirty = original->weightsDirty;
+    m3gCopy(clone->weightShifts, original->weightShifts,
+            clone->weightedVertexCount);
+
+    for (i = 0; i < m3gArraySize(&original->bones); i++) {
+        Bone *cloneBone = (Bone*) m3gAllocZ(originalObj->interface,
+                                            sizeof(Bone));
+        if (!cloneBone) {
+            return M3G_FALSE; /* out of memory */
+        }
+        /* this line looks odd, but really just copies the *contents*
+         * of the bone structure... */
+        *cloneBone = *(Bone*)m3gGetArrayElement(&original->bones, i);
+
+        if (m3gArrayAppend(&clone->bones, cloneBone, originalObj->interface) < 0) {
+            m3gFree(originalObj->interface, cloneBone);
+            return M3G_FALSE; /* out of memory */
+        }
+    }
+    
+    return M3G_TRUE;
+}
+
+/*!
+ * \internal
+ * \brief Overloaded Node method
+ */
+static M3Gint m3gSkinnedMeshGetBBox(Node *self, AABB *bbox)
+{
+    SkinnedMesh *mesh = (SkinnedMesh*) self;
+    Node *skeleton = (Node*) mesh->skeleton;
+
+    /* First update our local bounding box if necessary */
+    
+    if (self->dirtyBits & NODE_BBOX_BIT) {
+        
+        /* Compute an estimated bounding box from the morphed vertex
+         * buffer scale and bias (from PreComputeTransformations).
+         * The morphed vertex array is always scaled to utilize most
+         * of the 16-bit short range, so we just use that as the
+         * extents. */
+        {
+            const GLfloat scale = mesh->morphedVB->vertexScale;
+            const GLfloat *bias = mesh->morphedVB->vertexBias;
+            int i;
+            
+            for (i = 0; i < 3; ++i) {
+                mesh->bbox.min[i] = m3gMadd(scale, -1 << 15, bias[i]);
+                mesh->bbox.max[i] = m3gMadd(scale, (1 << 15) - 1, bias[i]);
+            }
+        }
+    }
+    *bbox = mesh->bbox;
+    
+    /* Mix in the skeleton bounding box if we need to -- but only into
+     * the output bbox, as we're handling the local mesh bbox
+     * specially in SetupRender! */
+        
+    if (skeleton->hasRenderables && skeleton->enableBits) {
+        AABB skeletonBBox;
+        if (m3gGetNodeBBox(skeleton, &skeletonBBox)) {
+            Matrix t;
+            m3gGetCompositeNodeTransform(self, &t);
+            m3gTransformAABB(&skeletonBBox, &t);
+            m3gFitAABB(bbox, &skeletonBBox, bbox);
+        }
+    }    
+    return m3gArraySize(&mesh->bones) * VFC_NODE_OVERHEAD;
+}
+
+/*!
+ * \internal
+ * \brief Overloaded Node method
+ */
+static M3Gbool m3gSkinnedMeshValidate(Node *self, M3Gbitmask stateBits, M3Gint scope)
+{
+    SkinnedMesh *mesh = (SkinnedMesh*) self;
+    Interface *m3g = M3G_INTERFACE(mesh);
+    Node *skeleton = (Node*) mesh->skeleton;
+    const VertexBuffer *srcVB = mesh->mesh.vertexBuffer;
+    M3Gint vertexCount = mesh->weightedVertexCount;
+
+    if ((scope & self->scope) != 0) {
+        if (stateBits & self->enableBits) {
+            
+            /* Check for invalid SkinnedMesh state */
+        
+            if (srcVB->vertices == NULL || vertexCount > srcVB->vertexCount) {
+                m3gRaiseError(m3g, M3G_INVALID_OPERATION);
+                return M3G_FALSE;
+            }
+            if (!m3gSkinnedMeshUpdateVB(mesh)) { /* Memory allocation failed */
+                return M3G_FALSE;
+            }
+        
+            /* Validate the skeleton */
+        
+            if (!m3gValidateNode(skeleton, stateBits, scope)) {
+                return M3G_FALSE;
+            }
+    
+            /* Validate our local state */
+    
+            if ((self->dirtyBits & NODE_TRANSFORMS_BIT) != 0 || 
+                m3gGetTimestamp(srcVB) != mesh->mesh.vbTimestamp) {
+                if (!m3gSkinnedMeshPreMorph((SkinnedMesh*) self)) {
+                    return M3G_FALSE;
+                }
+            }
+            if (self->dirtyBits & NODE_BBOX_BIT) {
+                M3G_BEGIN_PROFILE(M3G_INTERFACE(self), M3G_PROFILE_VFC_UPDATE);
+                m3gGetNodeBBox(self, &mesh->bbox);
+                M3G_END_PROFILE(M3G_INTERFACE(self), M3G_PROFILE_VFC_UPDATE);
+            }
+    
+            return m3gMeshValidate(self, stateBits, scope);
+        }
+    }
+    return M3G_TRUE;
+}
+
+/*!
+ * \internal
+ * \brief Overloaded Object3D method.
+ *
+ * \param self SkinnedMesh object
+ * \param pairs array for all object-duplicate pairs
+ * \param numPairs number of pairs
+ */
+static void m3gSkinnedMeshUpdateDuplicateReferences(Node *self, Object **pairs, M3Gint numPairs)
+{
+    SkinnedMesh *skinned = (SkinnedMesh *)self;
+    SkinnedMesh *duplicate = (SkinnedMesh *)m3gGetDuplicatedInstance(self, pairs, numPairs);
+    M3Gint i, n;
+    
+    m3gNodeUpdateDuplicateReferences(self, pairs, numPairs);
+    
+    n = m3gArraySize(&duplicate->bones);
+    for (i = 0; i < n; i++) {
+        Bone *bone = (Bone*) m3gGetArrayElement(&duplicate->bones, i);
+        Node *boneDuplicate = m3gGetDuplicatedInstance(bone->node, pairs, numPairs);
+        if (boneDuplicate != NULL) {
+            bone->node = boneDuplicate;
+        }
+    }
+    
+    M3G_VFUNC(Node, skinned->skeleton, updateDuplicateReferences)(
+        (Node *)skinned->skeleton, pairs, numPairs);
+}
+
+/*!
+ * \internal
+ * \brief Initializes a SkinnedMesh object. See specification
+ * for default values.
+ *
+ * \param m3g                   M3G interface
+ * \param mesh           SkinnedMesh object
+ * \param hVertices             VertexBuffer object
+ * \param hTriangles            array of IndexBuffer objects
+ * \param hAppearances          array of Appearance objects
+ * \param trianglePatchCount    number of submeshes
+ * \param hSkeleton             Group containing the skeleton
+ * \retval                      M3G_TRUE success
+ * \retval                      M3G_FALSE failure
+ */
+static M3Gbool m3gInitSkinnedMesh(Interface *m3g,
+                                  SkinnedMesh *mesh,
+                                  M3GVertexBuffer hVertices,
+                                  M3GIndexBuffer *hTriangles,
+                                  M3GAppearance *hAppearances,
+                                  M3Gint trianglePatchCount,
+                                  M3GGroup hSkeleton)
+{
+    /* SkinnedMesh is derived from Mesh */
+    if (!m3gInitMesh(m3g, &mesh->mesh,
+                     hVertices, hTriangles, hAppearances,
+                     trianglePatchCount,
+                     M3G_CLASS_SKINNED_MESH))
+    {
+        return M3G_FALSE;
+    }
+
+    /* Make sure our mesh gets blended even if no bones are added */    
+    ((Node*)mesh)->dirtyBits |= NODE_TRANSFORMS_BIT;
+        
+    /* Set default values, see RI SkinnedMesh.java for reference */
+    m3gSetParent(&((Group *)hSkeleton)->node, &mesh->mesh.node);
+    M3G_ASSIGN_REF(mesh->skeleton, (Group *)hSkeleton);
+
+    m3gInitArray(&mesh->bones);
+    
+    mesh->morphedVB = (VertexBuffer *)m3gCreateVertexBuffer(m3g);
+    if (mesh->morphedVB == NULL
+        || m3gSkinnedMeshUpdateVB(mesh) == M3G_FALSE) {
+        
+        /* We're sufficiently initialized at this point that the
+         * destructor can be called for cleaning up */
+        
+        m3gDestroySkinnedMesh((Object *)mesh);
+        return M3G_FALSE;
+    }
+    return M3G_TRUE;
+}
+
+/*----------------------------------------------------------------------
+ * Virtual function table
+ *--------------------------------------------------------------------*/
+
+static const NodeVFTable m3gvf_SkinnedMesh = {
+    {
+        {
+            m3gSkinnedMeshApplyAnimation,
+            m3gNodeIsCompatible,
+            m3gNodeUpdateProperty,
+            m3gSkinnedMeshDoGetReferences,
+            m3gSkinnedMeshFindID,
+            m3gSkinnedMeshDuplicate,
+            m3gDestroySkinnedMesh
+        }
+    },
+    m3gNodeAlign,
+    m3gSkinnedMeshDoRender,
+    m3gSkinnedMeshGetBBox,
+    m3gSkinnedMeshRayIntersect,
+    m3gSkinnedMeshSetupRender,
+    m3gSkinnedMeshUpdateDuplicateReferences,
+    m3gSkinnedMeshValidate
+};
+
+
+/*----------------------------------------------------------------------
+ * Public API functions
+ *--------------------------------------------------------------------*/
+
+/*!
+ * \brief Creates a SkinnedMesh object.
+ *
+ * \param interface             M3G interface
+ * \param hVertices             VertexBuffer object
+ * \param hTriangles            array of IndexBuffer objects
+ * \param hAppearances          array of Appearance objects
+ * \param trianglePatchCount    number of submeshes
+ * \param hSkeleton             Group containing the skeleton
+ * \retval                      SkinnedMesh new SkinnedMesh object
+ * \retval                      NULL SkinnedMesh creating failed
+ */
+M3G_API M3GSkinnedMesh m3gCreateSkinnedMesh(M3GInterface interface,
+                                            M3GVertexBuffer hVertices,
+                                            M3GIndexBuffer *hTriangles,
+                                            M3GAppearance *hAppearances,
+                                            M3Gint trianglePatchCount,
+                                            M3GGroup hSkeleton)
+{
+    Interface *m3g = (Interface *) interface;
+    M3G_VALIDATE_INTERFACE(m3g);
+    {
+        SkinnedMesh *mesh = NULL;
+        Group *skeleton = (Group *) hSkeleton;
+        if (skeleton == NULL) {
+            m3gRaiseError(m3g, M3G_NULL_POINTER);
+            return NULL;
+        }
+        if (skeleton->node.parent != NULL ||
+            M3G_CLASS(skeleton) == M3G_CLASS_WORLD) {
+            m3gRaiseError(m3g, M3G_INVALID_VALUE);
+            return NULL;
+        }
+        
+        mesh = m3gAllocZ(m3g, sizeof(SkinnedMesh));
+        if (mesh) {
+            if (!m3gInitSkinnedMesh(m3g, mesh,
+                                    hVertices, hTriangles, hAppearances,
+                                    trianglePatchCount,
+                                    hSkeleton)) {
+                m3gFree(m3g, mesh);
+                return NULL;
+            }
+        }
+        return (M3GSkinnedMesh)mesh;
+    }
+}
+
+/*!
+ * \brief Add new weighted transformation (bone) to range of vertices
+ *
+ * 
+ * \param handle        SkinnedMesh object
+ * \param hNode         bone to transform the vertices with
+ * \param weight        weight of the bone
+ * \param firstVertex   index to the first affected vertex
+ * \param numVertices   number of affected vertices
+ */
+M3G_API void m3gAddTransform(M3GSkinnedMesh handle,
+                             M3GNode hNode,
+                             M3Gint weight,
+                             M3Gint firstVertex, M3Gint numVertices)
+{
+    SkinnedMesh *mesh = (SkinnedMesh *)handle;
+    Node *boneNode = (Node *)hNode;
+    Interface *m3g = M3G_INTERFACE(mesh);
+    
+    M3Gint lastVertex = firstVertex + numVertices;
+    M3G_VALIDATE_OBJECT(mesh);
+
+    /* Check for errors */
+    
+    if (!boneNode) {
+        m3gRaiseError(m3g, M3G_NULL_POINTER);
+        return;
+    }
+    M3G_VALIDATE_OBJECT(boneNode);
+    if (!m3gIsChildOf((const Node*) mesh, boneNode)
+        || numVertices <= 0 || weight <= 0) {
+        m3gRaiseError(m3g, M3G_INVALID_VALUE);
+        return;
+    }
+    if (firstVertex < 0 || lastVertex > 65535) {
+        m3gRaiseError(m3g, M3G_INVALID_INDEX);
+        return;
+    }
+
+    /* Make sure we have enough per-vertex data */
+    
+    if (!m3gEnsureVertexCount(mesh, lastVertex)) {
+        return; /* out of memory */
+    }
+    
+    /* Check whether we may need to increase the number of bone
+     * entries per vertex, or whether we're already maxed out */
+    
+    if (mesh->bonesPerVertex < M3G_MAX_VERTEX_TRANSFORMS) {
+
+        /* Scan the input vertex range to find the maximum number of
+         * transforms per vertex (with non-zero weights) already in
+         * use, then make sure we can fit one more */
+        
+        int numBones = mesh->bonesPerVertex;
+        int maxBones = 0;
+        
+        int vertex;
+        for (vertex = firstVertex; vertex < lastVertex; ++vertex) {
+            int k;
+            for (k = numBones; k > 0; --k) {
+                if (mesh->boneWeights[k-1][vertex] > 0) {
+                    maxBones = M3G_MAX(maxBones,  k);
+                    break;
+                }
+            }
+        }
+        if (!m3gEnsureBonesPerVertex(mesh, maxBones + 1)) {
+            return; /* out of memory */
+        }
+    }
+    
+    /* Get a bone record for the bone node, and add the bone influence
+     * to all affected vertices */
+    {
+        int i;
+        
+        M3Gint boneIndex = m3gBoneIndex(mesh, boneNode);
+        if (boneIndex < 0) {
+            return; /* out of memory */
+        }
+        
+        for (i = firstVertex; i < lastVertex; i++) {
+            m3gAddInfluence(mesh, i, boneIndex, weight);
+        }
+    }
+    
+    /* Update the bone flag for the bone node and its parents up to
+     * the SkinnedMesh node */
+    
+    while (boneNode != (Node*) mesh) { /* boneNode must be a child of ours */
+        M3G_ASSERT(boneNode);
+        boneNode->hasBones = M3G_TRUE;
+        boneNode = boneNode->parent;
+    }
+}
+
+/*!
+ * \brief Getter for skeleton.
+ *
+ * \param handle                SkinnedMesh object
+ * \return                      Group object
+ */
+M3G_API M3GGroup m3gGetSkeleton(M3GSkinnedMesh handle)
+{
+    SkinnedMesh *mesh = (SkinnedMesh *)handle;
+    M3G_VALIDATE_OBJECT(mesh);
+
+    return mesh->skeleton;
+}
+
+/*!
+ * \brief Getter for bone transform.
+ *
+ * \param handle                SkinnedMesh object
+ * \param hBone                 Bone
+ * \param transform             Transform
+ */
+M3G_API void m3gGetBoneTransform(M3GSkinnedMesh handle,
+                                 M3GNode hBone,
+                                 M3GMatrix *transform)
+{
+    SkinnedMesh *mesh = (SkinnedMesh *)handle;
+    Node *node = (Node *)hBone;
+    M3Gint i;
+    M3Gint boneCount;
+
+    M3G_VALIDATE_OBJECT(mesh);
+    M3G_VALIDATE_OBJECT(node);
+
+    if (!m3gIsChildOf((Node*) mesh->skeleton, node)) {
+        m3gRaiseError(M3G_INTERFACE(mesh), M3G_INVALID_VALUE);
+        return;
+    }   
+    
+    boneCount = m3gArraySize(&mesh->bones);
+
+    for (i = 0; i < boneCount; ++i) {
+        Bone *bone = m3gGetArrayElement(&mesh->bones, i);
+
+        if (bone->node == node) {
+            m3gCopyMatrix(transform, &bone->toBone);
+            break;
+        }
+    }
+}
+
+/*!
+ * \brief Getter for bone vertices.
+ *
+ * \param handle                SkinnedMesh object
+ * \param hBone                 Bone
+ * \param indices               Influenced indices
+ * \param weights               Weights
+ * \return                      Number of influenced vertices
+ */
+M3G_API M3Gint m3gGetBoneVertices(M3GSkinnedMesh handle,
+                                  M3GNode hBone,
+                                  M3Gint *indices, M3Gfloat *weights)
+{
+    SkinnedMesh *mesh = (SkinnedMesh *)handle;
+    Node *node = (Node *)hBone;
+    M3Gint boneIndex, boneCount, count = 0;
+
+    M3G_VALIDATE_OBJECT(mesh);
+    M3G_VALIDATE_OBJECT(node);
+
+    /* Check for errors */
+
+    if (!m3gIsChildOf((Node*) mesh->skeleton, node)) {
+        m3gRaiseError(M3G_INTERFACE(mesh), M3G_INVALID_VALUE);
+        return 0;
+    }   
+        
+    /* Find the bone index corresponding to our bone node */
+    
+    boneCount = m3gArraySize(&mesh->bones);
+
+    for (boneIndex = 0; boneIndex < boneCount; ++boneIndex) {
+        Bone *bone = m3gGetArrayElement(&mesh->bones, boneIndex);
+        if (bone->node == node) {
+            break;
+        }
+    }
+
+    /* Loop over the vertices, outputting index-weight pairs for each
+     * vertex influenced by the bone */
+
+    if (boneIndex < boneCount) {
+        M3Gint i, j;
+
+        for (i = 0; i < mesh->weightedVertexCount; ++i) {
+            for (j = 0; j < mesh->bonesPerVertex; ++j) {
+                if (mesh->boneIndices[j][i] == boneIndex && mesh->boneWeights[j][i] > 0) {
+                    if (indices != NULL && weights != NULL) {
+                        M3Gint k, sum = 0;
+                        for (k = 0; k < mesh->bonesPerVertex; ++k) {
+                            sum += mesh->boneWeights[k][i];
+                        }
+                        indices[count] = i;
+                        if (sum != 0) {
+                            weights[count] = ((M3Gfloat) mesh->boneWeights[j][i]) / sum;
+                        }
+                        else {
+                            weights[count] = 0;
+                        }
+                    }
+                    ++count;
+                }
+            }                    
+        }
+    }
+    return count;
+}
+