Texture based morph targets with meshopt compressed mesh

Hello, I’m converting morph targets to a texture to overcome webgl limit (4 active morph targets at the same time). My model has 49 morph targets and the file is way too big (40 mb) so I want to compress the model with meshopt (140kb). The model is skinned and I’m playing skeletal animations.

The issue is, the code for converting morph targets to texture based morph targets totally deforms the model (or how it appears). I see only blinking patches and stripes of color. This piece of code is way beyond my understanding so I don’t know where to even start looking for an issue or if this approach is even technologically possible. Everything works perfectly fine without meshopt compression.
Is it possible that meshopt reduces the amount of shape keys so the real amount is different? I even tried to manually change the number of shape keys in this code from 1-49 - btw at 1 it looked ok just the shape keys were not applied to the model.

changeToTextureBasedMorphTargets(skinnedMesh) {
// Create morphTarget positions texture
let size = 4096 * 4096;
let data = new Float32Array(3 * size);
let vertIndexs = new Float32Array(
  skinnedMesh.geometry.attributes.position.count
);

//Loop over each original vertex
let stride = 0;
let vertexId = 0;
let min = Number.MAX_VALUE;
let max = Number.MIN_VALUE;

for (
  let v = 0;
  v < skinnedMesh.geometry.attributes.position.array.length;
  v += 3
) {
  //Loop over all blendshapes
  for (
    let i = 0;
    i < skinnedMesh.geometry.morphAttributes.position.length;
    i++
  ) {
    let morphAttr = skinnedMesh.geometry.morphAttributes.position[i];

    checkMinMax(morphAttr.array, v);

    //Copy x, y, and z for the given vertex
    data[stride] = morphAttr.array[v];
    data[stride + 1] = morphAttr.array[v + 1];
    data[stride + 2] = morphAttr.array[v + 2];

    stride += 3;
  }

  vertexId++;
  //Also set vertIndex at v to v which is the vert index
  vertIndexs[vertexId] = vertexId;
}

//console.log("min: ", min, "max: ", max);

function checkMinMaxComponent(value) {
  if (value < min) {
    min = value;
  }
  if (value > max) {
    max = value;
  }
}

function checkMinMax(array, startIndex) {
  checkMinMaxComponent(array[startIndex]);
  checkMinMaxComponent(array[startIndex + 1]);
  checkMinMaxComponent(array[startIndex + 2]);
}

// Remove existing morph target code related attributes
skinnedMesh.geometry.deleteAttribute("morphTarget0");
skinnedMesh.geometry.deleteAttribute("morphTarget1");
skinnedMesh.geometry.deleteAttribute("morphTarget2");

// Add new vert index attribute
skinnedMesh.geometry.setAttribute(
  "vertIndex",
  new THREE.BufferAttribute(vertIndexs, 1)
);

//CREATE DATA TEXTURE AND PLACE ON SHADER MAT
let dataTexture = new THREE.DataTexture(
  data,
  4096,
  4096,
  THREE.RGBFormat,
  THREE.FloatType
);
dataTexture.needsUpdate = true;

//Disable all morphTarget logic by setting appropriate flags
skinnedMesh.material.morphTargets = false;
skinnedMesh.material.morphNormals = false;

const morphTargetDeclarations = `
  //Data texture
  uniform sampler2D texture0;
  //Blendshape influences
  uniform float morphTargetInfluences[49];
  //Current vertex index
  attribute float vertIndex;
  `;
const morphTargetVertexShaderCode = `

  //Offset used for fixing the x y coordinates on the data texture
  float offset = vertIndex * 49.;
  //Loop over every blendshape
  for(int i=0; i<49; i++) {
    float iFloat = float(i);
    //If influence is 0, lets not waste GPU processing, move on
    if(morphTargetInfluences[i] == 0.) {
      continue;
    }
    //Find the x and y position of the vertex data based on vertex index and blendshape index
    float x = mod(offset + iFloat, 4096.);
    float y = floor((offset + iFloat) / 4096.);

    //Grab the data at x and y
    vec2 texCoord = vec2(x / 4096.,y / 4096.);
    vec4 data = texture2D(texture0, texCoord);

    //Modify the current vertex position with the data found in the texture and the current blendshape influence
    transformed.x += data.x * morphTargetInfluences[i];
    transformed.y += data.y * morphTargetInfluences[i];
    transformed.z += data.z * morphTargetInfluences[i];
  }
`;

// Replace all existing morph target vertex shader code with our own
skinnedMesh.material.onBeforeCompile = function (shader) {
  // Add new necessary uniforms
  shader.uniforms.texture0 = {
    type: "t",
    value: dataTexture,
  };
  // Now when we change the morphTargetInfluences it's linked to show up in the
  // vertex shader
  shader.uniforms.morphTargetInfluences = {
    value: skinnedMesh.morphTargetInfluences,
  };

  // Replace morphTarget code declarations with our own
  shader.vertexShader = shader.vertexShader.replace(
    "#include <morphtarget_pars_vertex>",
    morphTargetDeclarations
  );

  shader.vertexShader = shader.vertexShader.replace(
    "#include <morphtarget_vertex>",
    morphTargetVertexShaderCode
  );
};

}

This is what happens to my meshopt compressed model. I see only blinking patches of color.

Thank you for any help.

three.js automatically converts morph targets to textures and supports more than the earlier limit of 4-8 morph targets since r133 (WebGLRenderer: Support more than 8 morph targets. by Mugen87 · Pull Request #22293 · mrdoob/three.js · GitHub), you might not need to manually convert with this code anymore?

You might also want to try omitting morph normals (a blender export option) to reduce the size.

1 Like

Now I feel pretty dumb, I really didn’t notice this new feature. Thank you!