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 查看