SkinnedMeshRender

SkinnedMeshRender is a component for rendering skinned meshes with animation. It enables the creation of characters and other animated objects with deformable geometry.

Overview

SkinnedMeshRender extends MeshRenderer functionality to work with skeletal animation. It supports:

  • Mesh deformation based on skeleton pose

  • GPU skinning for performance

  • Blend shapes (morph targets)

  • Multiple animation layers

  • Animation blending

Component Properties

mesh (Mesh)

Reference to the skinned mesh. The mesh must contain bone weight data (bone weights) and bone indices for proper deformation.

  • Type: Mesh resource

  • Editor: Asset picker

  • Can be changed at runtime via setMesh()

materials (Material[])

Array of materials for each sub-mesh. If there are fewer materials than sub-meshes, the remaining sub-meshes are not rendered.

  • Type: array of Material resources

  • Editor: Asset picker

  • Can be changed at runtime via setMaterials()

armature (Armature)

Skeleton (armature) used for mesh deformation. Armature contains the bone hierarchy and their transformations. Usually loaded together with the mesh from the file, but can be assigned separately.

  • Type: Armature component (must be attached to the same actor or one of its parents)

  • Editor: Component picker

  • Default: nullptr (skeleton is extracted from the mesh)

boundsCenter (Vector3)

Center of the bounding box for the skinned mesh in local space.

  • Used for frustum culling and occlusion culling

  • Can be manually adjusted for culling optimization

  • Default: automatically calculated based on mesh geometry

boundsExtent (Vector3)

Half-size of the bounding box (extent) along each axis. Together with boundsCenter forms an AABB (Axis-Aligned Bounding Box).

  • Range: positive values

  • Default: automatically calculated based on mesh geometry

castShadows (bool) — currently not implemented

Determines whether the object casts shadows.

receiveShadows (bool) — currently not implemented

Determines whether the object receives shadows.

enabled (bool)

Enables/disables component rendering. Default: true.

layer (int)

Rendering layer. Used for camera culling mask. Default: 0.

Note

For SkinnedMeshRender to work correctly, the Armature component must be on the same actor or one of its parents. If armature is not explicitly specified, it will be automatically found in the hierarchy.

Usage Examples

Basic Character Creation

// Create an actor for the character
Actor* character = Engine::composeActor<SkinnedMeshRender>("Hero");
character->setPosition(Vector3(0.0f, 0.0f, 0.0f));

// Add SkinnedMeshRender
SkinnedMeshRender* skin = character->getComponent<SkinnedMeshRender>();
skin->setMesh(Engine::loadResource<Mesh>("characters/hero.gltf/Mesh01"));
skin->setMaterial(Engine::loadResource<Material>("characters/hero.mat"));

// Skeleton (armature) is loaded automatically from the mesh or can be assigned separately
// Armature is usually located on the same actor or a child object

Configuring Bounds for Optimization

SkinnedMeshRender* skin = character->getComponent<SkinnedMeshRender>();

// Automatic bounds (default)
Vector3 autoCenter = skin->getBoundsCenter();
Vector3 autoExtent = skin->getBoundsExtent();

// Manual adjustment for better culling
// For example, if the character is very tall, expand bounds along Y
skin->setBoundsCenter(Vector3(0.0f, 1.5f, 0.0f));
skin->setBoundsExtent(Vector3(1.0f, 2.0f, 1.0f));

Changing Skins (Materials)

// Load different material variants
Material* defaultSkin = Engine::loadResource<Material>("characters/default.mat");
Material* redSkin = Engine::loadResource<Material>("characters/red.mat");
Material* blueSkin = Engine::loadResource<Material>("characters/blue.mat");

SkinnedMeshRender* skin = getComponent<SkinnedMeshRender>();

// Change appearance
void setCharacterColor(int color) {
    switch (color) {
        case 0: skin->setMaterial(defaultSkin); break;
        case 1: skin->setMaterial(redSkin); break;
        case 2: skin->setMaterial(blueSkin); break;
    }
}

Using a Separate Armature Component

// Sometimes the skeleton can be a separate object
Actor* skeletonActor = Engine::composeActor<Armature>("Skeleton");
Armature* armature = skeletonActor->getComponent<Armature>();
armature->setBindPose(Engine::loadResource<BindPose>("characters/hero_mesh.gltf/pose"));

// Create character attached to the skeleton
Actor* character = Engine::composeActor<SkinnedMeshRender>("Skeleton", skeletonActor);
SkinnedMeshRender* skin = character->getComponent<SkinnedMeshRender>();
skin->setMesh(Engine::loadResource<Mesh>("characters/hero_mesh.gltf/Mesh01"));
skin->setArmature(armature);  // Explicitly specify the skeleton
skin->setMaterial(Engine::loadResource<Material>("characters/hero.mat"));

Blend Shapes (Morph Targets)currently not implemented

// For meshes with blend shapes (e.g., facial expressions)
SkinnedMeshRender* face = getComponent<SkinnedMeshRender>();

// Set blend shape weight
face->setBlendShapeWeight("smile", 0.5f);   // 50% smile
face->setBlendShapeWeight("blink", 0.0f);   // eyes open

// Smooth expression change
void updateFacialExpression(float deltaTime) {
    float target = Input::isKeyPressed(Input::KEY_SPACE) ? 1.0f : 0.0f;
    m_currentSmile += (target - m_currentSmile) * deltaTime * 5.0f;
    face->setBlendShapeWeight("smile", m_currentSmile);
}

Dynamic Mesh Switching

class CharacterCustomization : public Actor {
public:
    void start() override {
        m_skin = addComponent<SkinnedMeshRender>();

        // Load different outfit variants
        m_armorMesh = Engine::loadResource<Mesh>("characters/armor.gltf");
        m_robeMesh = Engine::loadResource<Mesh>("characters/robe.gltf");
        m_leatherMesh = Engine::loadResource<Mesh>("characters/leather.gltf");

        m_skin->setMesh(m_armorMesh);
        m_skin->setMaterial(armorMaterial);
    }

    void equipArmor() {
        m_skin->setMesh(m_armorMesh);
    }

    void equipRobe() {
        m_skin->setMesh(m_robeMesh);
    }

    SkinnedMeshRender* m_skin = nullptr;
    Mesh* m_armorMesh = nullptr;
    Mesh* m_robeMesh = nullptr;
    Mesh* m_leatherMesh = nullptr;
};

Recommendations

Performance Optimization
  • Limit the number of bones per character (recommended up to 64 on mobile, up to 256 on desktop)

  • For large numbers of characters, use LOD with transition to static meshes at long distances

  • Configure boundsCenter and boundsExtent for accurate culling — too large bounds reduce culling effectiveness, too small bounds may lead to premature culling

Animations
  • Use compression for animation clips (import settings)

  • For background characters, use fewer bones and simpler animations

  • Preload animations that will be used

Blend Shapescurrently not implemented
  • Blend shapes consume more memory and GPU time

  • Use only for critical expressions (face)

  • Limit the number of active blend shapes to 8-16

Armature Placement
  • Armature must be on the same actor or on a parent object

  • If animation does not play, verify that armature is correctly assigned

  • For multiple characters with the same skeleton, one armature can be reused (if they use the same animations)

Bounds
  • After changing the mesh, always check the bounds — they may change

  • Use resetBounds() for automatic recalculation

  • For animated characters, bounds may change dynamically during animation (arm swings, jumps). Consider manual configuration with margin.

Common Issues and Solutions

Character Does Not Render
  • Verify that the mesh is loaded and contains bone weights

  • Ensure a material is assigned

  • Check that armature is correctly assigned (automatically or manually)

  • Ensure the component is enabled (enabled = true)

Animation Does Not Play
  • Verify that Import Animation was enabled when importing the model

  • Ensure AnimationClip is loaded and assigned

  • Check that the skeleton in the mesh matches the armature

  • Ensure the armature is being updated (typically via the Animation component)

Mesh Deformation Is Incorrect
  • Verify that the mesh contains correct bone weights and bone indices

  • Ensure the number of bones does not exceed the limit (256 on desktop, 64 on mobile)

  • Check that bone names in the mesh match names in the armature

Object Is Culled by Camera
  • Check the bounds (boundsCenter and boundsExtent) — they may be too small

  • Call resetBounds() for automatic recalculation

  • Temporarily disable culling for debugging: camera->setCullingMask(-1)