Procedural Skinned Mesh

Hey, I’ve got a basic skinned mesh creator - I believe I’ve got the correct VertexBuffer format with BlendWeight and BlendIndicies. I’ve created a Skin and added it to the mesh, created a SkinInstance and added it to the model. Resolved out the bones to the “Entities” (not GraphNodes) beneath the root of the model that represent the bone positions and put them in the SkinInstance. I’ve setup a bunch of identity matrix bindPoses. But looks like it’s just rendering the meshes without the skin. Guessing I’ve missed some other step I need to take?

(This is for combining multiple similar objects into a single skinned mesh to reduce draw calls - so I’m making 1 bone per object and setting that index and the weight of that single bone to 1. I’m making up new bones as Entities and then linking them to the original objects - whose model is turned off - hopefully allowing the position of the original object to basically move the correct part of the skinned mesh).

For reference the issue was not setting the skinInstance on the meshInstance!

So the sequence is:

  • Create a mesh with vertex buffers containing BLENDINDICES and BLENDWEIGHT

  • Set up a Skin containing an array of bone names and bind poses (for my case these are the world transform matrices of the original object, so that its movements are reflected in the skinned mesh)

  • Create the Mesh

  • Set the skin property of the mesh to the Skin

  • Create a MeshInstance

  • Create a SkinInstance and resolve out the bones to entities

  • Setup a rootBone on the MeshInstance

  • Set the skinInstance on the MeshInstance

  • Create an array of MeshInstances and SkinInstances on the the model

      pc.script.attribute('enabled', 'boolean', true);
      pc.script.create('skinnedcombiner', function (app) {
    
    
      var createMesh = function (device, positions, opts) {
          // Check the supplied options and provide defaults for unspecified ones
          var normals = opts && opts.normals !== undefined ? opts.normals : null;
          var tangents = opts && opts.tangents !== undefined ? opts.tangents : null;
          var uvs = opts && opts.uvs !== undefined ? opts.uvs : null;
          var uvs1 = opts && opts.uvs1 !== undefined ? opts.uvs1 : null;
          var indices = opts && opts.indices !== undefined ? opts.indices : null;
          var blendWeights = opts && opts.blendWeights !== undefined ? opts.blendWeights : null;
          var blendIndices = opts && opts.blendIndices !== undefined ? opts.blendIndices : null;
    
          var vertexDesc = [
              { semantic: pc.SEMANTIC_POSITION, components: 3, type: pc.ELEMENTTYPE_FLOAT32 }
          ];
          if (normals !== null) {
              vertexDesc.push({ semantic: pc.SEMANTIC_NORMAL, components: 3, type: pc.ELEMENTTYPE_FLOAT32 });
          }
          if (tangents !== null) {
              vertexDesc.push({ semantic: pc.SEMANTIC_TANGENT, components: 4, type: pc.ELEMENTTYPE_FLOAT32 });
          }
          if (uvs !== null) {
              vertexDesc.push({ semantic: pc.SEMANTIC_TEXCOORD0, components: 2, type: pc.ELEMENTTYPE_FLOAT32 });
          }
          if (uvs1 !== null) {
              vertexDesc.push({ semantic: pc.SEMANTIC_TEXCOORD1, components: 2, type: pc.ELEMENTTYPE_FLOAT32 });
          }
          if (blendWeights !== null) {
              vertexDesc.push({ semantic: pc.SEMANTIC_BLENDWEIGHT, components: 4, type: pc.ELEMENTTYPE_FLOAT32 });
          }
          if (blendIndices !== null) {
              vertexDesc.push({ semantic: pc.SEMANTIC_BLENDINDICES, components: 4, type: pc.ELEMENTTYPE_UINT8 });
          }
          var vertexFormat = new pc.VertexFormat(device, vertexDesc);
    
          // Create the vertex buffer
          var numVertices  = positions.length / 3;
          var vertexBuffer = new pc.VertexBuffer(device, vertexFormat, numVertices);
    
          // Write the vertex data into the vertex buffer
          var iterator = new pc.VertexIterator(vertexBuffer);
          for (var i = 0; i < numVertices; i++) {
              iterator.element[pc.SEMANTIC_POSITION].set(positions[i*3], positions[i*3+1], positions[i*3+2]);
              if (normals !== null) {
                  iterator.element[pc.SEMANTIC_NORMAL].set(normals[i*3], normals[i*3+1], normals[i*3+2]);
              }
              if (tangents !== null) {
                  iterator.element[pc.SEMANTIC_TANGENT].set(tangents[i*4], tangents[i*4+1], tangents[i*4+2], tangents[i*4+3]);
              }
              if (uvs !== null) {
                  iterator.element[pc.SEMANTIC_TEXCOORD0].set(uvs[i*2], uvs[i*2+1]);
              }
              if (uvs1 !== null) {
                  iterator.element[pc.SEMANTIC_TEXCOORD1].set(uvs1[i*2], uvs1[i*2+1]);
              }
              if(blendWeights !== null) {
                  iterator.element[pc.SEMANTIC_BLENDWEIGHT].set(blendWeights[i*4], blendWeights[i*4+1],blendWeights[i*4+2], blendWeights[i*4+3]);
              }
              if(blendIndices !== null) {
                  iterator.element[pc.SEMANTIC_BLENDINDICES].set(blendIndices[i*4], blendIndices[i*4+1],blendIndices[i*4+2], blendIndices[i*4+3]);
              }
              iterator.next();
          }
          iterator.end();
    
          // Create the index buffer
          var indexBuffer = null;
          var indexed = (indices !== null);
          if (indexed) {
              indexBuffer = new pc.IndexBuffer(device, pc.INDEXFORMAT_UINT16, indices.length);
    
              // Read the indicies into the index buffer
              var dst = new Uint16Array(indexBuffer.lock());
              dst.set(indices);
              indexBuffer.unlock();
          }
    
          var aabb = new pc.BoundingBox();
          aabb.compute(positions);
    
          var mesh = new pc.Mesh();
          mesh.vertexBuffer = vertexBuffer;
          mesh.indexBuffer[0] = indexBuffer;
          mesh.primitive[0].type = pc.PRIMITIVE_TRIANGLES;
          mesh.primitive[0].base = 0;
          mesh.primitive[0].count = indexed ? indices.length : numVertices;
          mesh.primitive[0].indexed = indexed;
          mesh.aabb = aabb;
          return mesh;
      };
      
      var SkinnedCombiner = function SkinnedCombiner(entity) {
          this.entity = entity;
          //this.enabled = this.enabled;
      };
    
      window.allCombined = window.allCombined || [];
    
      function float32(locked, element) {
          return new Float32Array(locked, element.offset);
      }
      
      var validTypes = {
          "POSITION": float32,
          "NORMAL": float32,
          "TANGENT": float32,
          "TEXCOORD0": float32
      };
    
      window.nextId = window.nextId || 1;
      var vec = new pc.Vec3();
    
      SkinnedCombiner.prototype = {
          postInitialize: function () {
              if (!this.enabled) {
                  return;
              }
              this.enabled = false;
              var meshes = [];
              pc.utils.ofType(this.entity, 'model')
                  .forEach(function (model) {
                      if (model.model && model.enabled && model.entity._enabled) {
                          model.model.meshInstances.forEach(function (mesh) {
                              meshes.push({
                                  mesh: mesh,
                                  material: mesh.material,
                                  model: model
                              });
                          });
                      }
                  });
              var byMaterial = _.filter(_.groupBy(meshes, function (mesh) {
                  return mesh.material.name;
              }), function (g) {
                  return g.length > 0;
              });
              var sizes = _.map(byMaterial, function (m) {
                  return _.reduce(m, function (result, c) {
                      return result + c.mesh.mesh.vertexBuffer.numVertices;
                  }, 0);
              });
              var replace = new pc.Entity(app);
              app.root.addChild(replace);
              replace.name = "ReplaceSkinned";
              replace.enabled = true;
    
              _.forEachRight(byMaterial, function (list, materialIndex) {
                  console.log(materialIndex);
                  var material = list[0].material;
                  var combined = new pc.Entity();
                  replace.addChild(combined);
                  combined.addComponent('model');
                  combined.addComponent('script', {
                      scripts: [{url:'meshfixer.js'}]
                  });
                  combined.enabled = true;
                  window.allCombined.push(combined);
    
                  var pos = [];
                  var uv = [];
                  var normal = [];
                  var indices = [];
                  var tangents = [];
                  var blendIndices = [];
                  var blendWeights = [];
                  var bindPoses = [];
                  var p = 0;
                  var ind = 0;
                  var bones = [];
                  var boneEntities = [];
                  var root = new pc.scene.GraphNode();
    
                  //Now loop through and transform everything
                  list.forEach(function (m, index) {
                      //Add a bone for this object
                      var bone = new pc.Entity();
                      boneEntities.push(bone);
                      bone.name = 'bone_' + nextId + '_' + index;
                      bones.push(bone.name);
                      root.addChild(bone);
                      //Add a bind pose which is the matrix of the original object
                      var mat = m.model.entity.getWorldTransform();
                      bindPoses.push(mat);
                      var vb = m.mesh.mesh.vertexBuffer;
                      var ib = m.mesh.mesh.indexBuffer[pc.RENDERSTYLE_SOLID];
                      var iblocked = ib.lock();
                      var indexes = new Uint16Array(iblocked);
                      var locked = vb.lock();
                      var format = vb.getFormat();
                      var base = m.mesh.mesh.primitive[0].base;
                      var stride = format.size / 4;
                      var data = {};
                      for (j = 0; j < format.elements.length; j++) {
                          var element = format.elements[j];
                          var resolver = validTypes[element.name];
                          if (resolver) {
                              data[element.name] = resolver(locked, element);
                          }
                      }
                      var positions = data["POSITION"];
                      var t = p;
    
                      //Make room for the new ones
                      for (var i = 0; i < Math.floor(positions.length / stride); i++) {
                          pos.push(0);
                          pos.push(0);
                          pos.push(0);
                          uv.push(0);
                          uv.push(0);
                          tangents.push(0);
                          tangents.push(0);
                          tangents.push(0);
                          normal.push(0);
                          normal.push(0);
                          normal.push(0);
                          //Give this vertex weighting to the sequenced bone
                          blendIndices.push(index);
                          blendIndices.push(0);
                          blendIndices.push(0);
                          blendIndices.push(0);
                          blendWeights.push(1.0);
                          blendWeights.push(0);
                          blendWeights.push(0);
                          blendWeights.push(0);
                      }
                      var tv;
                      for (i = 0; i < positions.length; i += stride) {
                          pos[t] = positions[i];
                          pos[t+1] = positions[i+1];
                          pos[t+2] = positions[i+2];
                          t += 3;
                      }
                      var normals = data["NORMAL"];
                      t = p;
                      if (normals) {
                          for (i = 0; i < normals.length; i += stride) {
                              normal[t] = normals[i];
                              normal[t+1] = normals[i+1];
                              normal[t+2] = normals[i+2];
                              t += 3;
                          }
    
                      }
                      var uvs = data["TEXCOORD0"];
                      t = p / 3 * 2;
                      if (uvs) {
                          for (i = 0; i < uvs.length; i += stride, t += 2) {
                              uv[t] = uvs[i];
                              uv[t + 1] = uvs[i + 1];
                          }
                      }
    
                      var numIndices = m.mesh.mesh.primitive[0].count;
    
    
                      for (i = 0; i < numIndices; i++) {
                          indices.push(indexes[i + base] + p / 3);
                      }
                      p += (positions.length / stride) * 3;
                      //Turn off the existing object
                      m.model.entity.model.enabled = false;
    
                      vb.unlock();
                      ib.unlock();
    
                  });
                  var data;
                  var mesh = createMesh(app.graphicsDevice, pos, data = {
                      normals: normal,
                      uvs: uv,
                      indices: indices,
                      blendWeights: blendWeights,
                      blendIndices: blendIndices
                  });
                  //Create a skin
                  var skin = new pc.Skin(app.graphicsDevice, bindPoses, bones);
                  mesh.skin = skin;
                  var instance = new pc.scene.MeshInstance(root, mesh, material);
                  var model = new pc.scene.Model();
                  model.graph = root;
                  
                  var skinInstance = new pc.SkinInstance(skin);
                  skinInstance.rootNode = combined;
                  skinInstance.bones = boneEntities;
                  model.meshInstances = [instance];
                  model.skinInstances = [skinInstance];
                  instance.skinInstance = skinInstance;
                  combined.model.model = model;
                  var id = nextId++;
                  combined.script.meshfixer.model = id;
                  addModel(id, model);
                  combined.enabled = true;
    
    
              });
              replace.enabled = true;
    
          }
    
      };
    
      return SkinnedCombiner;
    
      });

You can attach the script above to a parent entity and it will create a skinned mesh for each material within the children, grouping together all of the objects with that material to reduce draw calls.

You also need this script called meshfixer.js which associates the generated model with a component.

var models = window.models || {};

var addModel = addModel || function addModel(id, model) {
    models[id] = model;
};

pc.script.attribute('model', 'number', 0);
pc.script.create('meshfixer', function(app) {

    var MeshFixer = function(entity) {
        this.entity = entity;
    };


    MeshFixer.prototype = {

        initialize: function() {
            setTimeout(function() {
                if (this.entity.model) {
//                    console.log('fix', this.model);
                    this.entity.model.model = models[this.model];
                }
            }.bind(this), 4);
        }

    };

    return MeshFixer;

});

Kudos for figuring this stuff out! :smile:

Perhaps we should update our version of createMesh to make your life easier. There’s already an issue open on GitHub to add vertex color support.

Is it possible to make a tutorial of this? I’m new with playcanvas and I’m trying to make a skinned mesh animation as I do in Unity3D but the only animation that I can see is rotation or translation. What I mean by this are animations like inflating a balloon or make a muscle bigger.

1 Like