Skip to main content

skinning/
skinning.rs

1//! Procedural tentacles: `cargo run --example skinning --features 3d`.
2//! Escape quits. All visible geometry is built in code.
3use ponko::*;
4
5const JOINTS: usize = 8;
6const STEP: f32 = 0.5;
7struct Sway(f32);
8struct Bend(AnimationClip);
9
10// ANCHOR: skin_chain
11fn chain() -> Skeleton {
12    Skeleton::new(
13        (0..JOINTS)
14            .map(|i| Joint {
15                name: format!("joint_{i}"),
16                parent: i.checked_sub(1),
17                rest: Transform3D::at(Vec3::Y * if i == 0 { 0.0 } else { STEP }),
18            })
19            .collect(),
20    )
21}
22// ANCHOR_END: skin_chain
23
24// A cylinder (including caps), subdivided along +Y. Also used for the band.
25// ANCHOR: skin_geometry
26fn tube(bottom: f32, top: f32, radius: f32) -> MeshData {
27    const SLICES: usize = 12;
28    const ROWS: usize = 28;
29    let mut data = MeshData::default();
30    for row in 0..=ROWS {
31        let y = bottom + (top - bottom) * row as f32 / ROWS as f32;
32        let along = (y / STEP).clamp(0.0, (JOINTS - 1) as f32);
33        let lower = (along.floor() as usize).min(JOINTS - 2);
34        let blend = along - lower as f32;
35        for slice in 0..SLICES {
36            let angle = std::f32::consts::TAU * slice as f32 / SLICES as f32;
37            data.positions.push(vec3(radius * angle.cos(), y, radius * angle.sin()));
38            data.joints.push([lower as u16, (lower + 1) as u16, 0, 0]);
39            data.weights.push([1.0 - blend, blend, 0.0, 0.0]);
40        }
41    }
42    for row in 0..ROWS {
43        for slice in 0..SLICES {
44            let a = (row * SLICES + slice) as u32;
45            let b = (row * SLICES + (slice + 1) % SLICES) as u32;
46            let c = a + SLICES as u32;
47            let d = b + SLICES as u32;
48            data.indices.extend([a, c, b, b, c, d]);
49        }
50    }
51    for slice in 1..SLICES - 1 {
52        let last = (ROWS * SLICES) as u32;
53        data.indices.extend([0, slice as u32, (slice + 1) as u32]);
54        data.indices
55            .extend([last, last + (slice + 1) as u32, last + slice as u32]);
56    }
57    data
58}
59// ANCHOR_END: skin_geometry
60
61// ANCHOR: skin_pose
62fn sway(ctx: &mut Ctx) {
63    let time = ctx.time() as f32;
64    ctx.world.query::<(&mut Skeleton, &Sway)>().for_each(|(s, phase)| {
65        for (i, pose) in s.pose.iter_mut().enumerate() {
66            pose.rotation = Quat::from_rotation_z((time * 1.5 + phase.0 - i as f32 * 0.45).sin() * 0.16);
67        }
68    });
69    ctx.world.query::<(&mut Skeleton, &Bend)>().for_each(|(s, bend)| {
70        bend.0.sample_blended(time.rem_euclid(bend.0.duration()), 0.5, s);
71    });
72    ctx.world.query::<&mut Animator>().for_each(|a| {
73        a.set_bool(0, ((time / 3.0) as u32).is_multiple_of(2));
74    });
75    if ctx.key_pressed(Key::Escape) {
76        ctx.quit();
77    }
78}
79// ANCHOR_END: skin_pose
80
81// Compiled book example; the procedural demo never loads asset files.
82#[allow(dead_code)]
83fn load_character(ctx: &mut Ctx) {
84    // ANCHOR: skin_load
85    let mesh = ctx.load_mesh("models/character.glb");
86    let skeleton = ctx.load_skeleton("models/character.glb");
87    let hand = skeleton.find("hand_r").expect("model needs hand_r");
88    let character = ctx.world.spawn((Transform3D::default(), Model::new(mesh), skeleton));
89    let skeleton = ctx.world.get_mut::<Skeleton>(character).unwrap();
90    skeleton.pose[hand].rotation = Quat::from_rotation_y(0.3);
91    let local_hand = skeleton.joint_matrix(hand);
92    let transform = ctx.world.get::<Transform3D>(character).unwrap();
93    let world_hand = (transform.matrix() * local_hand).transform_point3(Vec3::ZERO);
94    println!("Hand at {world_hand}");
95    // ANCHOR_END: skin_load
96}
97
98// ANCHOR: clip_code
99fn bend_clip(skeleton: &Skeleton) -> AnimationClip {
100    let rotation = AnimationTrack::new(
101        vec![0.0, 1.0, 2.0],
102        [0.0, 0.3, 0.0]
103            .map(|angle| Vec4::from_array(Quat::from_rotation_z(angle).to_array()))
104            .to_vec(),
105        Interpolation::Linear,
106    );
107    AnimationClip::new(
108        "bend",
109        skeleton,
110        vec![JointAnimation {
111            joint: 1,
112            name: "joint_1".into(),
113            translation: None,
114            rotation: Some(rotation),
115            scale: None,
116        }],
117    )
118}
119// ANCHOR_END: clip_code
120
121#[allow(dead_code)]
122fn play_clips(ctx: &mut Ctx) {
123    // ANCHOR: clip_play
124    let mut skeleton = ctx.load_skeleton("models/character.glb");
125    let walk = ctx.load_clip("models/character.glb", "walk");
126    let run = ctx.load_clip("models/character.glb", "run");
127    let time = ctx.time() as f32;
128    let loop_time = |clip: &AnimationClip| {
129        if clip.duration() > 0.0 {
130            time.rem_euclid(clip.duration())
131        } else {
132            0.0
133        }
134    };
135    skeleton.reset_pose();
136    walk.sample(loop_time(&walk), &mut skeleton);
137    run.sample_blended(loop_time(&run), 0.5, &mut skeleton);
138    // Keep clips and the skeleton as components/resources for subsequent frames.
139    // ANCHOR_END: clip_play
140}
141
142// ANCHOR: animator_graph
143fn bend_animator(skeleton: &Skeleton) -> Animator {
144    let idle = AnimationClip::new("idle", skeleton, vec![]);
145    let bend = bend_clip(skeleton);
146    let states = vec![
147        AnimationState::new(Motion::Clip(idle)),
148        AnimationState::new(Motion::Clip(bend)),
149    ];
150    let transition = |from, to, active| AnimationTransition {
151        from: Some(from),
152        to,
153        conditions: vec![AnimationCondition::Bool(0, active)],
154        exit_time: None,
155        duration: 0.12,
156        offset: 0.0,
157    };
158    let graph = AnimationGraph::new(
159        vec![AnimationParameter::Bool(false)],
160        states,
161        vec![transition(0, 1, true), transition(1, 0, false)],
162        0,
163    );
164    Animator::new(graph)
165}
166// ANCHOR_END: animator_graph
167
168#[allow(dead_code)]
169fn animate_now() {
170    // ANCHOR: animator_update
171    let mut skeleton = chain();
172    let mut animator = bend_animator(&skeleton);
173    animator.manual = true;
174    animator.set_bool(0, true);
175    animator.update(0.016, &mut skeleton);
176    // Or spawn (Animator, Skeleton) together for automatic updates.
177    animator.crossfade(0, 0.12, 0.0);
178    animator.update(0.016, &mut skeleton);
179    for name in animator.drain_events() {
180        println!("Animation event: {name}");
181    }
182    // ANCHOR_END: animator_update
183}
184
185#[allow(dead_code)]
186fn layers_and_root_motion(skeleton: &Skeleton, wave: AnimationClip) -> Animator {
187    // ANCHOR: animator_layers
188    let mut animator = bend_animator(skeleton);
189    let waving = AnimationGraph::new(vec![], vec![AnimationState::new(Motion::Clip(wave))], vec![], 0);
190    let mut layer = AnimationLayer::new(waving);
191    layer.mask = JointMask::descendants(skeleton, "joint_3"); // that joint and its children
192    layer.weight = 0.8;
193    let wave = animator.add_layer(layer);
194    animator.layer_mut(wave).weight = 0.5; // fade it any time; its graph has its own parameters
195    // The clip's own steps move the entity, instead of sliding in place.
196    animator.root_joint = 0;
197    animator.apply_root_motion = true;
198    // ANCHOR_END: animator_layers
199    animator
200}
201
202fn main() {
203    Game::new("Crabby: skinned meshes", 1000, 700).system(sway).run(|ctx| {
204        ctx.clear(Color::hex(0xABCBDD));
205        ctx.camera3d.position = vec3(6.0, 4.0, 9.0);
206        ctx.camera3d.look_at(vec3(0.0, 1.7, 0.0));
207        let green = ctx.create_material(MaterialDesc::color(Color::hex(0x398F79)));
208        let gold = ctx.create_material(MaterialDesc::color(Color::hex(0xF2BD50)));
209        let white = ctx.default_material();
210        let ground = ctx.create_mesh(&MeshData::plane(20.0), white);
211        ctx.world.spawn((Transform3D::default(), Model::new(ground)));
212        // ANCHOR: skin_create
213        let skeleton = chain();
214        let mesh = ctx.create_skinned_mesh(&tube(0.0, 3.5, 0.22), green, &skeleton);
215        // A separate skeleton with matching names demonstrates name binding.
216        let band = ctx.create_skinned_mesh(&tube(2.2, 2.5, 0.25), gold, &chain());
217        // ANCHOR_END: skin_create
218        let ball = ctx.create_mesh(&MeshData::sphere(0.18, 16), gold);
219        let bend = bend_clip(&skeleton);
220        for i in 0..3 {
221            let tentacle = ctx.world.spawn((
222                Transform3D::at(vec3((i as f32 - 1.0) * 2.5, 0.0, 0.0)),
223                Model::new(mesh),
224                skeleton.clone(),
225                Sway(i as f32 * 1.8),
226                Bend(bend.clone()),
227            ));
228            if i == 2 {
229                ctx.world.insert(tentacle, bend_animator(&skeleton));
230            }
231            if i == 0 {
232                // ANCHOR: skin_parts
233                ctx.world.spawn((Model::new(band), SkinOf(tentacle)));
234                let tip = skeleton.find("joint_7").unwrap();
235                ctx.world.spawn((
236                    Transform3D::default(),
237                    Model::new(ball),
238                    AttachToJoint {
239                        skeleton: tentacle,
240                        joint: tip,
241                        offset: Transform3D::at(vec3(0.0, 0.12, 0.0)),
242                    },
243                ));
244                // ANCHOR_END: skin_parts
245            }
246        }
247    });
248}
249
250// ANCHOR: skin_ik
251// Register this with Game::new(...).late_system(procedural_pose).
252#[allow(dead_code)]
253fn procedural_pose(ctx: &mut Ctx) {
254    let target = vec3(0.7, 0.6, 0.2);
255    ctx.world
256        .query::<(&Transform3D, &mut Skeleton)>()
257        .for_each(|(world, skeleton)| {
258            two_bone_ik(skeleton, world, [0, 1, 2], target, Vec3::Z, 1.0);
259            // Apply parent first when distributing aim across spine/neck/head.
260            look_at_joint(skeleton, world, 5, Vec3::Y, target, 0.3, 0.2);
261            look_at_joint(skeleton, world, 6, Vec3::Y, target, 0.5, 0.5);
262            look_at_joint(skeleton, world, 7, Vec3::Y, target, 0.8, 1.0);
263        });
264}
265
266#[allow(dead_code)]
267fn debug_bones(ctx: &mut Ctx, character: Entity) {
268    ctx.draw_skeleton(character, Color::RED);
269}
270// ANCHOR_END: skin_ik