Skip to main content

forge-skinning-animations

Add skeletal skinning animations with glTF joint hierarchies, inverse bind matrices, and per-vertex blend weights to an SDL GPU project.

설치로 이동

소스 정보

저장소
Nebulavenus/forge-gpu
최근 소스 활동
2026년 3월 10일 02:20
감지된 SKILL.md 언어
영어
스타
38
포크
7

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
forge-skinning-animations
description
Add skeletal skinning animations with glTF joint hierarchies, inverse bind matrices, and per-vertex blend weights to an SDL GPU project.
Add vertex skinning to an SDL3 GPU scene using skeletal animation data from glTF files. The technique evaluates keyframe channels for translation, rotation (slerp), and scale, rebuilds the joint hierarchy, computes joint matrices (`inverse(meshWorld) × jointWorld × inverseBindMatrix`), and blends up to 4 joint transforms per vertex in the shader. Use this skill when you need animated characters, creatures, or any mesh that deforms with a skeleton. See [GPU Lesson 32 — Skinning Animations](../../../lessons/gpu/32-skinning-animations/) for the full walkthrough. ## Key API calls | Function | Purpose | |----------|---------| | `forge_gltf_load()` | Load glTF with skins, joints, and animation data | | `forge_gltf_anim_apply()` | Evaluate all animation channels at time t | | `forge_gltf_compute_world_transforms()` | Propagate local transforms through the node hierarchy | | `mat4_multiply(a, b)` | Compose joint matrix: `inverse(meshWorld) * jointWorld * inverseBindMatrix` | | `mat4_inverse(m)` | Compute inverse mesh world for joint matrix formula | ## Skinned vertex layout ```c typedef struct SkinVertex { vec3 position; /* 12 bytes — FLOAT3 */ vec3 normal; /* 12 bytes — FLOAT3 */ vec2 uv; /* 8 bytes — FLOAT2 */ Uint16 joints[4]; /* 8 bytes — USHORT4 */ float weights[4]; /* 16 bytes — FLOAT4 */ } SkinVertex; /* 56 bytes total */ ``` Pipeline vertex attributes: - Location 0: `FLOAT3` at `offsetof(SkinVertex, position)` - Location 1: `FLOAT3` at `offsetof(SkinVertex, normal)` - Location 2: `FLOAT2` at `offsetof(SkinVertex, uv)` - Location 3: `USHORT4` at `offsetof(SkinVertex, joints)` - Location 4: `FLOAT4` at `offsetof(SkinVertex, weights)` ## Joint matrix computation (per frame) ```c /* Full glTF formula: inverse(meshWorld) * jointWorld * inverseBindMatrix. * The inverse mesh world accounts for parent nodes above the mesh * (e.g. Z_UP, Armature rotations in CesiumMan). */ mat4 inv_mesh_world = mat4_inverse(mesh_node->world_transform); for (int i = 0; i < skin->joint_count; i++) { int node = skin->joints[i]; joint_matrices[i] = mat4_multiply( inv_mesh_world, mat4_multiply(scene.nodes[node].world_transform, skin->inverse_bind_matrices[i])); } ``` ## Shader pattern (vertex) The vertex shader receives two uniform buffer slots: - Slot 0: scene uniforms (MVP, model, light_vp) - Slot 1: joint matrices array ```hlsl cbuffer JointUniforms : register(b1, space1) { column_major float4x4 joint_mats[MAX_JOINTS]; }; /* Compute skin matrix from 4 weighted joints */ float4x4 skin_mat = input.weights.x * joint_mats[input.joints.x] + input.weights.y * joint_mats[input.joints.y] + input.weights.z * joint_mats[input.joints.z] + input.weights.w * joint_mats[input.joints.w]; float4 world = mul(skin_mat, float4(input.pos, 1.0)); ``` ## Animation evaluation `forge_gltf_load()` parses all animation channels from the glTF file. At runtime, `forge_gltf_anim_apply()` handles channel evaluation, then `forge_gltf_compute_world_transforms()` propagates the hierarchy: ```c #include "gltf/forge_gltf.h" #include "gltf/forge_gltf_anim.h" /* Each frame: evaluate channels, rebuild hierarchy, compute joints */ anim_time += dt * ANIM_SPEED; if (scene.animation_count > 0) { forge_gltf_anim_apply(&scene.animations[0], scene.nodes, scene.node_count, anim_time, true); } mat4 identity = mat4_identity(); for (int i = 0; i < scene.root_node_count; i++) forge_gltf_compute_world_transforms(&scene, scene.root_nodes[i], &identity); /* Then compute joint matrices and push to GPU uniform slot 1 */ ``` ## Pipeline setup ```c /* Skinned vertex shader needs 2 uniform buffers. */ SDL_GPUShaderCreateInfo info; info.num_uniform_buffers = 2; /* slot 0: scene, slot 1: joints */ /* Push both uniform slots per draw call. */ SDL_PushGPUVertexUniformData(cmd, 0, &scene_uniforms, sizeof(scene_uniforms)); SDL_PushGPUVertexUniformData(cmd, 1, &joint_uniforms, sizeof(joint_uniforms)); ``` ## Shadow pass Use a separate `shadow_skin.vert.hlsl` that applies the same skin matrix before projecting into light clip space. Both passes share the same joint uniform data — no extra CPU work for skinned shadows. ## Common mistakes - **glTF quaternion order** — glTF stores quaternions as `[x, y, z, w]`, but the forge math library uses `quat_create(w, x, y, z)` with `w` first. Swapping the order silently produces incorrect rotations. - **Missing inverse mesh world** — The simplified formula `world * inverseBindMatrix` only works when the mesh node sits at the scene root. The full glTF formula is `inverse(meshWorld) * jointWorld * inverseBindMatrix`. CesiumMan has `Z_UP` and `Armature` parent nodes, so skipping the inverse mesh world produces distorted results. - **Forgetting shadow skinning** — The shadow pass must apply the same skin matrix in its vertex shader. Without this, the shadow stays in the bind pose while the mesh animates. - **Joint count mismatch** — The `MAX_JOINTS` constant in the shader must match the C-side `JointUniforms` struct. A mismatch silently corrupts uniform data. - **Weight normalization** — glTF requires weights to sum to 1.0, but some exporters produce slightly denormalized weights. The linear blend still works acceptably, but be aware of this if you see subtle artifacts. ## glTF skin data The `forge_gltf.h` parser provides: - `ForgeGltfSkin`: joints array, inverse bind matrices, skeleton root - `ForgeGltfPrimitive`: joint_indices (USHORT4), weights (FLOAT4) - `ForgeGltfNode`: skin_index for nodes referencing a skin Load with `forge_gltf_load()` — skins are parsed automatically.
GitHub에서 보기