Skip to main content
MorphTargetBuffer manages vertex morphing data (blend shapes) for animating mesh deformations. It supports both automatic morphing for positions/tangents and manual morphing for custom attributes.

Overview

Morph targets (also called blend shapes) allow smooth interpolation between different vertex positions, enabling facial animations, corrective shapes, and other mesh deformations. A MorphTargetBuffer stores multiple target shapes, each containing vertex offsets from the base mesh.

Morphing Modes

MorphTargetBuffer operates in a hybrid model:

1. Automatic (Built-in Attributes)

For positions and tangents, the buffer holds data internally:
  • Enable via withPositions(true) or withTangents(true)
  • Upload data with setPositionsAt() or setTangentsAt()
  • Filament automatically applies morphing in the vertex shader

2. Manual (Custom Attributes)

For UVs, colors, or other custom data:
  • Enable via enableCustomMorphing(true)
  • Create and manage a separate Texture to hold morph data
  • Bind the texture to your material as a sampler2DArray
  • Manually call morphData2, morphData3, or morphData4 in the vertex shader

Creating a MorphTargetBuffer

#include <filament/Engine.h>
#include <filament/MorphTargetBuffer.h>

using namespace filament;

Engine* engine = Engine::create();

MorphTargetBuffer* mtb = MorphTargetBuffer::Builder()
    .vertexCount(100)
    .count(3)              // 3 morph targets
    .withPositions(true)   // Enable position morphing
    .withTangents(true)    // Enable tangent morphing
    .build(*engine);

Builder Methods

vertexCount

Sets the number of vertices per morph target.
Builder& vertexCount(size_t vertexCount) noexcept;
This should match the vertex count of the base mesh geometry.

count

Sets the number of morph targets.
Builder& count(size_t count) noexcept;
Each target represents a different deformation state of the mesh.

withPositions

Enables automatic position morphing.
Builder& withPositions(bool enable = true) noexcept;
When enabled, allocates internal storage for position offsets and automatically applies morphing.

withTangents

Enables automatic tangent/normal morphing.
Builder& withTangents(bool enable = true) noexcept;
When enabled, allocates internal storage for tangent offsets and automatically applies morphing.

enableCustomMorphing

Enables manual morphing for custom attributes.
Builder& enableCustomMorphing(bool enable) noexcept;
This enables shader functions (morphData2, morphData3, morphData4) but does NOT allocate storage. You must provide a texture with the morph data.

name

Associates a debug name with the buffer.
Builder& name(utils::StaticString const& name) noexcept;

build

Creates the MorphTargetBuffer.
MorphTargetBuffer* build(Engine& engine);

Uploading Morph Data

setPositionsAt (float3)

Uploads position offsets for a target.
void setPositionsAt(Engine& engine,
                   size_t targetIndex,
                   math::float3 const* positions,
                   size_t count,
                   size_t offset = 0);
Parameters:
  • targetIndex - Which morph target (0 to count-1)
  • positions - Array of position offsets (not absolute positions)
  • count - Number of vertices
  • offset - Starting vertex index in the target
This is equivalent to the float4 version with w=1.0.

setPositionsAt (float4)

Uploads position offsets with explicit 4th component.
void setPositionsAt(Engine& engine,
                   size_t targetIndex,
                   math::float4 const* positions,
                   size_t count,
                   size_t offset = 0);
The 4th component can be used for additional per-vertex data.

setTangentsAt

Uploads tangent offsets for a target.
void setTangentsAt(Engine& engine,
                  size_t targetIndex,
                  math::short4 const* tangents,
                  size_t count,
                  size_t offset = 0);
Tangent format: Quaternions represented as signed shorts, where the range [-1, +1] is multiplied by 32767. Example conversion:
// Normalized quaternion
math::quatf q = {0.7071f, 0.0f, 0.7071f, 0.0f};

// Convert to short4
math::short4 tangent = {
    static_cast<short>(q.x * 32767.0f),
    static_cast<short>(q.y * 32767.0f),
    static_cast<short>(q.z * 32767.0f),
    static_cast<short>(q.w * 32767.0f)
};

Basic Example

From the hellomorphing sample:
using namespace filament;
using namespace filament::math;

// Define morph target positions (offsets from base mesh)
float3 target0[3] = {
    {-2, 0, 0},  // Vertex 0 moves left
    { 0, 2, 0},  // Vertex 1 moves up
    { 1, 0, 0}   // Vertex 2 moves right
};

float3 target1[3] = {
    { 1, 1, 0},
    {-1, 0, 0},
    {-1, 0, 0}
};

float3 target2[3] = {
    {0, 0, 0},  // No change
    {0, 0, 0},
    {0, 0, 0}
};

// Define tangent offsets (all zero for this example)
short4 tangents[3] = {
    {0, 0, 0, 0},
    {0, 0, 0, 0},
    {0, 0, 0, 0}
};

// Create buffer
MorphTargetBuffer* mtb = MorphTargetBuffer::Builder()
    .vertexCount(3)
    .count(3)  // 3 morph targets
    .withPositions(true)
    .withTangents(true)
    .build(*engine);

// Upload data for each target
mtb->setPositionsAt(*engine, 0, target0, 3, 0);
mtb->setPositionsAt(*engine, 1, target1, 3, 0);
mtb->setPositionsAt(*engine, 2, target2, 3, 0);

mtb->setTangentsAt(*engine, 0, tangents, 3, 0);
mtb->setTangentsAt(*engine, 1, tangents, 3, 0);
mtb->setTangentsAt(*engine, 2, tangents, 3, 0);

Attaching to Renderables

using namespace filament;

utils::Entity renderable = utils::EntityManager::get().create();

RenderableManager::Builder(1)
    .boundingBox({{-1, -1, -1}, {1, 1, 1}})
    .material(0, materialInstance)
    .geometry(0, RenderableManager::PrimitiveType::TRIANGLES,
              vertexBuffer, indexBuffer, 0, 3)
    .morphing(mtb)           // Attach the MorphTargetBuffer
    .morphing(0, 0, 0)       // primitive 0, target count, offset
    .build(*engine, renderable);

morphing() Methods:

  1. morphing(MorphTargetBuffer)* - Attaches the buffer to all primitives
  2. morphing(primitiveIndex, targetCount, offset) - Configures which targets each primitive uses
    • primitiveIndex - Which primitive (if renderable has multiple)
    • targetCount - How many targets this primitive uses
    • offset - Starting target index in the buffer

Setting Morph Weights

Control the blend between targets using weights:
// In animation loop
FilamentApp::get().animate([&](Engine* engine, View* view, double now) {
    auto& rm = engine->getRenderableManager();
    auto instance = rm.getInstance(renderable);
    
    // Calculate weights (should sum to reasonable values)
    float t = static_cast<float>(sin(now) / 2.0f + 0.5f);
    float weights[] = {
        1.0f - t,  // Target 0 weight
        t / 2.0f,  // Target 1 weight
        t / 2.0f   // Target 2 weight
    };
    
    // Apply weights
    rm.setMorphWeights(instance, weights, 3, 0);
});

setMorphWeights

void RenderableManager::setMorphWeights(
    Instance instance,
    const float* weights,
    size_t count,
    size_t offset = 0);
Parameters:
  • instance - The renderable instance
  • weights - Array of weight values (0.0 to 1.0 typical, but can exceed)
  • count - Number of weights
  • offset - Starting target index
Weight interpretation:
  • 0.0 - Target has no effect
  • 1.0 - Target fully applied
  • Values can exceed 1.0 for exaggerated effects
  • Final vertex position = base + sum(weight[i] * offset[i])

Multiple Primitives Example

When a renderable has multiple primitives sharing morph targets:
// Create buffer with space for 2 primitives (6 vertices each, 3 targets)
MorphTargetBuffer* mtb = MorphTargetBuffer::Builder()
    .vertexCount(18)  // 2 primitives * 9 vertices (3 verts * 3 targets)
    .count(3)
    .withPositions(true)
    .withTangents(true)
    .build(*engine);

// Upload data for first primitive (vertices 0-8)
mtb->setPositionsAt(*engine, 0, target0_positions, 3, 0);
mtb->setPositionsAt(*engine, 1, target1_positions, 3, 0);
mtb->setPositionsAt(*engine, 2, target2_positions, 3, 0);

// Upload data for second primitive (vertices 9-17)
mtb->setPositionsAt(*engine, 0, target0_positions2, 3, 9);
mtb->setPositionsAt(*engine, 1, target1_positions2, 3, 9);
mtb->setPositionsAt(*engine, 2, target2_positions2, 3, 9);

// Create renderable with 2 primitives
RenderableManager::Builder(2)
    .boundingBox({{-1, -1, -1}, {1, 1, 1}})
    .material(0, materialInstance)
    .material(1, materialInstance)
    .geometry(0, RenderableManager::PrimitiveType::TRIANGLES, vb, ib, 0, 3)
    .geometry(1, RenderableManager::PrimitiveType::TRIANGLES, vb, ib, 0, 3)
    .morphing(mtb)
    .morphing(0, 0, 0)   // Primitive 0: 3 targets starting at offset 0
    .morphing(1, 0, 9)   // Primitive 1: 3 targets starting at offset 9
    .build(*engine, renderable);

Query Methods

getVertexCount

Returns the vertex count of the buffer.
size_t getVertexCount() const noexcept;

getCount

Returns the number of morph targets.
size_t getCount() const noexcept;

hasPositions

Returns true if the buffer has position morphing enabled.
bool hasPositions() const noexcept;

hasTangents

Returns true if the buffer has tangent morphing enabled.
bool hasTangents() const noexcept;

isCustomMorphingEnabled

Returns true if custom morphing is enabled.
bool isCustomMorphingEnabled() const noexcept;

Custom Morphing (Advanced)

For morphing custom attributes like UVs or vertex colors:

1. Enable Custom Morphing

MorphTargetBuffer* mtb = MorphTargetBuffer::Builder()
    .vertexCount(100)
    .count(3)
    .withPositions(true)         // Still use automatic for positions
    .enableCustomMorphing(true)  // Enable custom morphing functions
    .build(*engine);

2. Create Texture with Morph Data

// Create a 2D array texture to hold custom morph offsets
// Dimensions: width = vertex count, layers = target count
Texture* morphTexture = Texture::Builder()
    .width(vertexCount)
    .height(1)
    .depth(targetCount)
    .levels(1)
    .format(Texture::InternalFormat::RGBA32F)
    .sampler(Texture::Sampler::SAMPLER_2D_ARRAY)
    .build(*engine);

// Upload morph offset data
// (You need to prepare the data in the correct format)
morphTexture->setImage(*engine, 0, /* pixel data */);

3. Bind to Material

materialInstance->setParameter("morphUVs", morphTexture,
    TextureSampler(TextureSampler::MinFilter::NEAREST,
                   TextureSampler::MagFilter::NEAREST));

4. Use in Vertex Shader

materialVertex {
    // Get morph weights uniform (automatically provided by Filament)
    // when MorphTargetBuffer is attached
    
    // Morph custom UV coordinates
    vec2 uvDelta = morphData2(morphUVs, vertex_id).xy;
    material.uv = baseUV + uvDelta;
}
Available functions:
  • morphData2(sampler2DArray, vertexId) - Returns vec2
  • morphData3(sampler2DArray, vertexId) - Returns vec3
  • morphData4(sampler2DArray, vertexId) - Returns vec4
These functions automatically blend morph targets based on the current weights.

Performance Tips

  1. Minimize active targets - Use only necessary morph targets; fewer targets = better performance
  2. Share buffers - Reuse MorphTargetBuffer across multiple renderables when possible
  3. Optimize target count - Typical facial animation uses 50-100 targets; consider which are essential
  4. Use sparse targets - Many vertices can have zero offsets; consider sparse encoding for custom morphing

Cleanup

engine->destroy(morphTargetBuffer);
Ensure all renderables using the buffer are destroyed first, or updated to not reference it.

Build docs developers (and LLMs) love