Skinned meshes: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: rig (line 38), weights (line 50), mesh (line 57), live (line 107).
1/// A banner bent by two joints: `Skeleton` moves it on the device, `SkinBlend`
2/// mirrors the same arithmetic on the CPU so this page can check its own claim.
3///
4/// Quoted by `skinning.md` and shown whole in the Source tab.
5library;
7import 'dart:typed_data';
9import 'package:flutter3d/flutter3d.dart';
10import 'package:flutter3d_showcase/src/demo/demo.dart';
11import 'package:vector_math/vector_math.dart';
13final class SkinningDemo extends ShowcaseDemo {
14 double bend = 0.5;
16 static const double _depth = 2.4;
17 static const double _maxAngle = 1.4; // a little under a right angle
19 late final SceneNode _base;
20 late final SceneNode _tip;
21 late final Skeleton _skeleton;
22 late final MeshData _cpuMesh;
24 /// The vertex whose bind position sits halfway along the banner, where the
25 /// blend of the two joints moves it the most — see `skinning.md`.
26 late int _midVertex;
29 void configureView(DemoContext context) {
30 context.orbit
31 ..distance = 4.0
32 ..pitch = 0.15
33 ..yaw = 0.5;
34 }
37 Scene build(DemoContext context) {
38 _base = SceneNode(name: 'base')..setPosition(0.0, 0.0, -_depth / 2);
39 _tip = SceneNode(name: 'tip')..setPosition(0.0, 0.0, _depth);
40 _base.add(_tip);
42 final Matrix4 inverseBindBase = Matrix4.copy(_base.worldMatrix)..invert();
43 final Matrix4 inverseBindTip = Matrix4.copy(_tip.worldMatrix)..invert();
44 _skeleton = Skeleton(
45 joints: <SceneNode>[_base, _tip],
46 inverseBindMatrices: <Matrix4>[inverseBindBase, inverseBindTip],
47 name: 'flagpole',
48 );
50 _cpuMesh = const PlaneShape(
51 width: 0.7,
52 depth: _depth,
53 depthSegments: 8,
54 ).build().convertedTo(VertexLayout.skinned);
55 _paintWeights(_cpuMesh);
57 final Material fabric = Material(
58 name: 'banner',
59 baseColor: Vector4(0.82, 0.24, 0.2, 1.0),
60 roughness: 0.7,
61 doubleSided: true,
62 );
63 final MeshNode banner = MeshNode(
64 DeviceMesh.upload(context.device, _cpuMesh),
65 fabric,
66 name: 'banner',
67 )..skeleton = _skeleton;
69 return Scene()
70 ..add(banner)
71 ..add(
72 LightNode(name: 'sun', intensity: 3.0)
73 ..setLocalForward(Vector3(-0.3, -0.6, -0.7)),
74 );
75 }
77 /// Every vertex leans on the base joint near `z = -depth/2` and on the tip
78 /// joint near `z = +depth/2`, blending linearly between the two — the
79 /// ordinary two-joint paint a rope or a flagpole gets.
80 void _paintWeights(MeshData mesh) {
81 final VertexLayout layout = mesh.layout;
82 final int stride = layout.floatsPerVertex;
83 final int positionAt = layout.floatOffsetOf(VertexLayout.position.name);
84 final int jointsAt = layout.floatOffsetOf(VertexLayout.joints.name);
85 final int weightsAt = layout.floatOffsetOf(VertexLayout.weights.name);
86 final Float32List vertices = mesh.vertices;
87 var closestToMiddle = double.infinity;
89 for (var v = 0; v < mesh.vertexCount; v++) {
90 final int at = v * stride;
91 final double z = vertices[at + positionAt + 2];
92 final double t = ((z + _depth / 2) / _depth).clamp(0.0, 1.0);
93 vertices[at + jointsAt] = 0.0;
94 vertices[at + jointsAt + 1] = 1.0;
95 vertices[at + weightsAt] = 1.0 - t;
96 vertices[at + weightsAt + 1] = t;
98 if (z.abs() < closestToMiddle) {
99 closestToMiddle = z.abs();
100 _midVertex = v;
101 }
102 }
103 }
106 void update(DemoContext context, double dt) {
107 _tip.setRotation(
108 Quaternion.axisAngle(Vector3(1.0, 0.0, 0.0), bend * _maxAngle),
109 );
110 }
113 List<DemoControl> controls(DemoContext context) => <DemoControl>[
114 SliderControl(
115 'Bend',
116 min: 0,
117 max: 1,
118 value: () => bend,
119 onChanged: (double v) => bend = v,
120 ),
121 ];
124 void verify(Scene scene, FrameResult frame) {
125 if (frame.skinnedDraws < 1) {
126 throw StateError('the banner was not drawn through the skinned stage');
127 }
128 // The device already posed `_skeleton.matrices` for this frame; blending
129 // them again on the CPU is `SkinBlend`'s own job, and the tip of the
130 // banner should have left its bind position by as much as `bend` asked.
131 final SkinBlend blend = SkinBlend(_cpuMesh);
132 if (!blend.blend(_skeleton.matrices)) {
133 throw StateError('SkinBlend saw no change from the bind pose');
134 }
135 final int stride = _cpuMesh.layout.floatsPerVertex;
136 final int positionAt = _cpuMesh.layout.floatOffsetOf(
137 VertexLayout.position.name,
138 );
139 final double skinnedY =
140 blend.vertices[_midVertex * stride + positionAt + 1];
141 if (skinnedY.abs() < 1e-3) {
142 throw StateError('the banner did not follow the tip joint');
143 }
144 }
145}