@solidrt/3d 0.0.57 → 0.0.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +859 -528
- package/examples/README.md +6 -0
- package/examples/wireframe.tsx +107 -0
- package/package.json +2 -2
- package/src/geometry-gpu.ts +45 -13
- package/src/geometry.ts +186 -5
- package/src/gltf.ts +97 -16
- package/src/index.ts +2 -1
- package/src/input.ts +11 -2
- package/src/material.ts +26 -8
- package/src/mesh.ts +5 -6
- package/src/model.ts +65 -33
- package/src/node.ts +2 -1
- package/src/scene.ts +6 -3
- package/src/skeleton.ts +211 -0
package/src/skeleton.ts
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// Shared skeletons. Wardrobe pieces (a hood, a cape, cuffs) export with
|
|
2
|
+
// a skin over the body's joints - the same joint names, their own inverse
|
|
3
|
+
// binds - and no clips of their own, so a piece added beside an animated
|
|
4
|
+
// body holds its bind pose. bindSkeleton makes the piece's skin read the
|
|
5
|
+
// BODY's joints instead of its own: each matched joint's palette row is
|
|
6
|
+
// re-bound onto the body's node, with the piece's inverse bind as the
|
|
7
|
+
// row's post-multiply, so the frame's flush writes the body's pose into
|
|
8
|
+
// the piece's palette and no per-frame code exists anywhere. One
|
|
9
|
+
// skeleton, many skinned meshes: Three's SkinnedMesh.bind(skeleton),
|
|
10
|
+
// Unity's SkinnedMeshRenderer.bones, Godot's MeshInstance3D.skeleton.
|
|
11
|
+
//
|
|
12
|
+
// The piece's own node tree is deliberately NOT posed or composed:
|
|
13
|
+
// exporters truncate it above (a hat's table starts at the spine, the
|
|
14
|
+
// missing ancestors folded into its first local) or skip an ancestor in
|
|
15
|
+
// the middle (a cape parents Spine_1 straight to Root_0), and copying
|
|
16
|
+
// locals through such a tree hangs joints off the wrong place while
|
|
17
|
+
// looking right for every piece whose tree happens to match. A bone
|
|
18
|
+
// matrix needs the joint's WORLD matrix, which the body's node has, and
|
|
19
|
+
// the piece's inverse binds agree with the body's whenever the piece
|
|
20
|
+
// renders at rest beside it - checked at bind against the body's own.
|
|
21
|
+
// Joints with no body counterpart (a hat's internal bones) keep their own
|
|
22
|
+
// node, grafted under the nearest matched body joint so they ride the
|
|
23
|
+
// pose; so does any rigid part hanging off a matched node.
|
|
24
|
+
|
|
25
|
+
import * as spatial from "flux:spatial"
|
|
26
|
+
import type { TextureId } from "@solidrt/core/gpu"
|
|
27
|
+
import { add, remove, setTransform } from "./node.ts"
|
|
28
|
+
import type { SceneNode } from "./node.ts"
|
|
29
|
+
import type { Model } from "./model.ts"
|
|
30
|
+
|
|
31
|
+
/** Inverse-bind entries of a piece and its body differ by exporter
|
|
32
|
+
* rounding (1e-6 or so); a piece exported against another rest pose or
|
|
33
|
+
* scale is off by centimetres. The gate between the two. */
|
|
34
|
+
const BIND_TOLERANCE = 1e-3
|
|
35
|
+
|
|
36
|
+
/** A piece bound onto a body's skeleton: the piece, and its matched
|
|
37
|
+
* joints' influence boxes (in joint space) keyed by the body joint that
|
|
38
|
+
* now drives them - the body's cull-bounds union takes them in. */
|
|
39
|
+
export type WornPiece = {
|
|
40
|
+
piece: Model
|
|
41
|
+
boxes: { joint: SceneNode; box: Float32Array }[]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Drive `piece`'s skins from `body`'s skeleton: a wardrobe piece exported
|
|
46
|
+
* over the body's joints (matched by name, case-insensitive) with no
|
|
47
|
+
* clips of its own. After this the piece hangs under the body at the
|
|
48
|
+
* body's placement, its skinned parts follow every pose the body takes -
|
|
49
|
+
* mixer-driven or hand-posed - and its joints with no body counterpart
|
|
50
|
+
* ride the nearest matched body joint. The piece's own joint nodes are
|
|
51
|
+
* no longer posed by anything; socket items on the body's joints. Throws
|
|
52
|
+
* when the piece has no skin, shares no joint name with the body, or a
|
|
53
|
+
* shared joint's bind pose differs (a piece exported against another
|
|
54
|
+
* rest pose). A piece is taken off by disposing it; disposing the body
|
|
55
|
+
* disposes what it wears.
|
|
56
|
+
*/
|
|
57
|
+
export function bindSkeleton(body: Model, piece: Model): void {
|
|
58
|
+
if (piece === body) throw new Error("bindSkeleton: a model cannot wear itself")
|
|
59
|
+
if (piece._body !== null) throw new Error("bindSkeleton: the piece is already bound to a body")
|
|
60
|
+
if (body._body !== null) throw new Error("bindSkeleton: the body is itself a worn piece")
|
|
61
|
+
if (piece._skins.length === 0) throw new Error("bindSkeleton: the piece has no skin")
|
|
62
|
+
|
|
63
|
+
// Out of the scene first: the piece's rows must release before they
|
|
64
|
+
// re-bind under a different anchor (one anchor per palette texture),
|
|
65
|
+
// and the add() under the body re-enters everything below.
|
|
66
|
+
remove(piece)
|
|
67
|
+
|
|
68
|
+
let bodyByName = new Map<string, SceneNode>()
|
|
69
|
+
for (let n of body.nodes) {
|
|
70
|
+
let key = n.name.toLowerCase()
|
|
71
|
+
if (!bodyByName.has(key)) bodyByName.set(key, n.node)
|
|
72
|
+
}
|
|
73
|
+
let bodyBind = new Map<SceneNode, Float32Array>()
|
|
74
|
+
for (let s of body._skins) s.joints.forEach((j, k) => bodyBind.set(j, s.inverseBind.subarray(k * 16, k * 16 + 16)))
|
|
75
|
+
let match = new Map<SceneNode, SceneNode>()
|
|
76
|
+
let names = new Map<SceneNode, string>()
|
|
77
|
+
for (let n of piece.nodes) {
|
|
78
|
+
names.set(n.node, n.name)
|
|
79
|
+
let b = bodyByName.get(n.name.toLowerCase())
|
|
80
|
+
if (b !== undefined) match.set(n.node, b)
|
|
81
|
+
}
|
|
82
|
+
if (match.size === 0) throw new Error("bindSkeleton: no node of the piece is named like a node of the body")
|
|
83
|
+
|
|
84
|
+
// Matched rows move onto the body's joints. Identical skins share one
|
|
85
|
+
// texture, so its rows move with the first skin naming it; the check
|
|
86
|
+
// and the boxes run for every skin regardless.
|
|
87
|
+
let rebound = new Set<TextureId>()
|
|
88
|
+
let boxes: WornPiece["boxes"] = []
|
|
89
|
+
for (let s of piece._skins) {
|
|
90
|
+
let first = !rebound.has(s.texture)
|
|
91
|
+
rebound.add(s.texture)
|
|
92
|
+
s.joints.forEach((joint, k) => {
|
|
93
|
+
let b = match.get(joint)
|
|
94
|
+
if (b === undefined) return
|
|
95
|
+
let post = s.inverseBind.subarray(k * 16, k * 16 + 16)
|
|
96
|
+
let own = bodyBind.get(b)
|
|
97
|
+
if (own !== undefined && !sameMatrix(own, post)) {
|
|
98
|
+
throw new Error("bindSkeleton: joint '" + names.get(joint) + "' has a different bind pose on the piece than on the body")
|
|
99
|
+
}
|
|
100
|
+
let box = s.jointBounds.subarray(k * 6, k * 6 + 6)
|
|
101
|
+
if (validBox(box)) boxes.push({ joint: b, box: Float32Array.from(box) })
|
|
102
|
+
if (!first || joint._palettes === null) return
|
|
103
|
+
let at = joint._palettes.findIndex((p) => p.texture === s.texture)
|
|
104
|
+
if (at < 0) return
|
|
105
|
+
let row = joint._palettes.splice(at, 1)[0]!
|
|
106
|
+
if (joint._palettes.length === 0) joint._palettes = null
|
|
107
|
+
;(b._palettes ??= []).push({ texture: s.texture, row: k, post: row.post, anchor: body })
|
|
108
|
+
if (b._node !== null && body._node !== null) spatial.bindTextureSlot(b._node, s.texture, k, row.post, body._node)
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
// The rows that stay on the piece's own nodes (unmatched joints) share
|
|
112
|
+
// the texture's one anchor: the body root, an ancestor of every bound
|
|
113
|
+
// node once the piece and its grafts hang under the body.
|
|
114
|
+
for (let n of piece.nodes) {
|
|
115
|
+
if (n.node._palettes === null) continue
|
|
116
|
+
for (let p of n.node._palettes) if (p.anchor === piece) p.anchor = body
|
|
117
|
+
}
|
|
118
|
+
for (let part of piece.parts) {
|
|
119
|
+
let joints = part.mesh._cullJoints
|
|
120
|
+
if (joints !== null) part.mesh._cullJoints = joints.map((j) => match.get(j) ?? j)
|
|
121
|
+
}
|
|
122
|
+
// Graft: whatever hangs off a matched node that is not itself matched
|
|
123
|
+
// (an unmatched joint subtree, a rigid part) moves under the body's
|
|
124
|
+
// node with its local transform intact - the two nodes share a bind
|
|
125
|
+
// pose, so the local means the same thing there.
|
|
126
|
+
for (let [node, bodyNode] of match) {
|
|
127
|
+
for (let child of node.children.slice()) {
|
|
128
|
+
if (match.has(child)) continue
|
|
129
|
+
add(bodyNode, child)
|
|
130
|
+
piece._grafts.push(child)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
piece._body = body
|
|
135
|
+
body._worn.push({ piece, boxes })
|
|
136
|
+
refreshJointBounds(body)
|
|
137
|
+
// A piece's skinned vertices are in the body's model space, so it takes
|
|
138
|
+
// the body's placement and nothing else.
|
|
139
|
+
setTransform(piece, { position: [0, 0, 0], quaternion: [0, 0, 0, 1], scale: [1, 1, 1] })
|
|
140
|
+
add(body, piece)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** @internal Take a worn piece off its body: its rows leave the body's
|
|
144
|
+
* joints, its grafts detach, the body's joint boxes shrink back. The
|
|
145
|
+
* piece's own bindings are not restored - dispose is the one caller, and
|
|
146
|
+
* it destroys the piece's palette textures right after. */
|
|
147
|
+
export function unbindSkeleton(piece: Model): void {
|
|
148
|
+
let body = piece._body
|
|
149
|
+
if (body === null) return
|
|
150
|
+
let mine = new Set(piece._skins.map((s) => s.texture))
|
|
151
|
+
for (let n of body.nodes) {
|
|
152
|
+
let rows = n.node._palettes
|
|
153
|
+
if (rows === null) continue
|
|
154
|
+
let keep = rows.filter((p) => !mine.has(p.texture))
|
|
155
|
+
if (keep.length === rows.length) continue
|
|
156
|
+
if (n.node._node !== null) {
|
|
157
|
+
for (let p of rows) if (mine.has(p.texture)) spatial.unbindTextureSlot(n.node._node, p.texture)
|
|
158
|
+
}
|
|
159
|
+
n.node._palettes = keep.length > 0 ? keep : null
|
|
160
|
+
}
|
|
161
|
+
for (let g of piece._grafts) remove(g)
|
|
162
|
+
piece._grafts = []
|
|
163
|
+
body._worn.splice(body._worn.findIndex((w) => w.piece === piece), 1)
|
|
164
|
+
piece._body = null
|
|
165
|
+
refreshJointBounds(body)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** @internal Recompute every joint's culling box from the model's skins
|
|
169
|
+
* and the pieces it wears: the union, per joint, of each skin's joint-
|
|
170
|
+
* space influence box (a body/legs split and a worn hood all weight the
|
|
171
|
+
* head), applied to the live core node where it changed. A joint no skin
|
|
172
|
+
* reaches gets none and contributes nothing to a part's cull group. */
|
|
173
|
+
export function refreshJointBounds(model: Model): void {
|
|
174
|
+
let boxes = new Map<SceneNode, Float32Array>()
|
|
175
|
+
let grow = (joint: SceneNode, box: Float32Array): void => {
|
|
176
|
+
if (!validBox(box)) return
|
|
177
|
+
let have = boxes.get(joint)
|
|
178
|
+
if (have === undefined) {
|
|
179
|
+
boxes.set(joint, Float32Array.from(box))
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
for (let i = 0; i < 3; i++) {
|
|
183
|
+
have[i] = Math.min(have[i]!, box[i]!)
|
|
184
|
+
have[i + 3] = Math.max(have[i + 3]!, box[i + 3]!)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
for (let s of model._skins) s.joints.forEach((j, k) => grow(j, s.jointBounds.subarray(k * 6, k * 6 + 6)))
|
|
188
|
+
for (let w of model._worn) for (let b of w.boxes) grow(b.joint, b.box)
|
|
189
|
+
for (let n of model.nodes) {
|
|
190
|
+
let box = boxes.get(n.node) ?? null
|
|
191
|
+
if (sameBox(n.node._cullBounds, box)) continue
|
|
192
|
+
n.node._cullBounds = box
|
|
193
|
+
if (n.node._node !== null) spatial.setCullBounds(n.node._node, box)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** A joint influencing no vertex carries an inverted box (min > max). */
|
|
198
|
+
function validBox(box: Float32Array): boolean {
|
|
199
|
+
return box[0]! <= box[3]! && box[1]! <= box[4]! && box[2]! <= box[5]!
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function sameBox(a: Float32Array | null, b: Float32Array | null): boolean {
|
|
203
|
+
if (a === null || b === null) return a === b
|
|
204
|
+
for (let i = 0; i < 6; i++) if (a[i] !== b[i]) return false
|
|
205
|
+
return true
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function sameMatrix(a: Float32Array, b: Float32Array): boolean {
|
|
209
|
+
for (let i = 0; i < 16; i++) if (Math.abs(a[i]! - b[i]!) > BIND_TOLERANCE) return false
|
|
210
|
+
return true
|
|
211
|
+
}
|