@threenative/procedural-animals 0.0.0-stage → 0.1.1
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/LICENSES/Procedural-Animals-MIT.txt +21 -0
- package/README.md +43 -2
- package/dist/build.d.ts +53 -0
- package/dist/build.js +367 -0
- package/dist/format-BRFFv-ws.d.ts +44 -0
- package/dist/index.d.ts +95 -0
- package/dist/index.js +815 -0
- package/package.json +51 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
import { Matrix4, BufferGeometry, BufferAttribute, Vector3, Group, Material, Mesh, Euler, Bone, DataTexture, RGBAFormat, FloatType, NearestFilter, Quaternion, Matrix3, Sphere, Box3 } from 'three';
|
|
2
|
+
|
|
3
|
+
// src/format.ts
|
|
4
|
+
var ANIMAL_DONOR_REVISION = "c95ae49346aa8e140a924376cec6cf0073d99512";
|
|
5
|
+
var ANIMAL_ADAPTER_VERSION = 1;
|
|
6
|
+
var ANIMAL_LIMITS = Object.freeze({
|
|
7
|
+
bytes: 64 * 1024 * 1024,
|
|
8
|
+
vertices: 25e4,
|
|
9
|
+
indices: 15e5,
|
|
10
|
+
bones: 256,
|
|
11
|
+
headerBytes: 1024 * 1024
|
|
12
|
+
});
|
|
13
|
+
var ANIMAL_ARRAYS = {
|
|
14
|
+
pos: [Float32Array, 3],
|
|
15
|
+
nrm: [Float32Array, 3],
|
|
16
|
+
index: [Uint32Array, 0],
|
|
17
|
+
skinIndex: [Uint16Array, 4],
|
|
18
|
+
skinWeight: [Float32Array, 4],
|
|
19
|
+
comb: [Float32Array, 3],
|
|
20
|
+
tint: [Float32Array, 4],
|
|
21
|
+
coat: [Float32Array, 4],
|
|
22
|
+
pat: [Float32Array, 4],
|
|
23
|
+
surf: [Float32Array, 4]
|
|
24
|
+
};
|
|
25
|
+
function bakeIntegrity(buffer) {
|
|
26
|
+
const view = new DataView(buffer);
|
|
27
|
+
const headerBytes = view.getUint32(8, true);
|
|
28
|
+
const start = Math.ceil((12 + headerBytes) / 8) * 8;
|
|
29
|
+
const bytes = new Uint8Array(buffer);
|
|
30
|
+
const header = JSON.parse(new TextDecoder().decode(bytes.subarray(12, 12 + headerBytes)));
|
|
31
|
+
const { integrity: _integrity, ...metadata } = header.threenative;
|
|
32
|
+
let hash = 2166136261;
|
|
33
|
+
for (const section of [
|
|
34
|
+
new TextEncoder().encode(JSON.stringify({ ...header, threenative: metadata })),
|
|
35
|
+
bytes.subarray(start)
|
|
36
|
+
])
|
|
37
|
+
for (const byte of section) hash = Math.imul(hash ^ byte, 16777619) >>> 0;
|
|
38
|
+
return hash.toString(16).padStart(8, "0");
|
|
39
|
+
}
|
|
40
|
+
var validatedBakes = /* @__PURE__ */ new WeakSet();
|
|
41
|
+
function requireValidatedBake(bake) {
|
|
42
|
+
if (!validatedBakes.has(bake))
|
|
43
|
+
throw animalError("VALIDATION", "parse the complete bake before allocating resources");
|
|
44
|
+
validateArrayData(bake, bake.nV, bake.bones.length);
|
|
45
|
+
}
|
|
46
|
+
function animalError(code, detail) {
|
|
47
|
+
return new Error(`TN_ANIMAL_${code}: ${detail}`);
|
|
48
|
+
}
|
|
49
|
+
function record(value, name2) {
|
|
50
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
51
|
+
throw animalError("HEADER", `${name2} must be an object`);
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
function integer(value, min, max, name2) {
|
|
56
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < min || value > max) {
|
|
57
|
+
throw animalError("LIMIT", `${name2} must be an integer in [${min}, ${max}]`);
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
function name(value, label) {
|
|
62
|
+
if (typeof value !== "string" || !/^[a-zA-Z][a-zA-Z0-9_-]{0,127}$/u.test(value)) {
|
|
63
|
+
throw animalError("RIG", `${label} must be a joint or bone name`);
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
function joints(value, label) {
|
|
68
|
+
const entries = Object.entries(record(value, label));
|
|
69
|
+
integer(entries.length, 2, 4096, `${label} count`);
|
|
70
|
+
for (const [key, point] of entries) {
|
|
71
|
+
name(key, label);
|
|
72
|
+
if (!Array.isArray(point) || point.length !== 3 || !point.every(Number.isFinite)) {
|
|
73
|
+
throw animalError("RIG", `${label}.${key} must be a finite point`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
function validateHeader(header) {
|
|
79
|
+
if (header.version !== 1 || header.species !== "wolf" || header.quality !== "high" && header.quality !== "crowd") {
|
|
80
|
+
throw animalError("REVISION", "expected version 1, wolf, and high or crowd geometry");
|
|
81
|
+
}
|
|
82
|
+
integer(header.seed, 0, 4294967295, "seed");
|
|
83
|
+
integer(header.nV, 3, ANIMAL_LIMITS.vertices, "vertex count");
|
|
84
|
+
const metadata = record(header.threenative, "threenative");
|
|
85
|
+
if (metadata.donorRevision !== ANIMAL_DONOR_REVISION || metadata.adapterVersion !== ANIMAL_ADAPTER_VERSION) {
|
|
86
|
+
throw animalError("REVISION", "unknown donor revision or adapter version");
|
|
87
|
+
}
|
|
88
|
+
if (typeof metadata.buildRuntime !== "string" || metadata.buildRuntime !== "node-v20.19.6" || typeof metadata.cacheKey !== "string" || !/^[a-f0-9]{64}$/u.test(metadata.cacheKey) || typeof metadata.integrity !== "string" || !/^[a-f0-9]{8}$/u.test(metadata.integrity)) {
|
|
89
|
+
throw animalError("HEADER", "missing pinned build runtime or SHA-256 cache key");
|
|
90
|
+
}
|
|
91
|
+
const params = record(header.params, "params");
|
|
92
|
+
if (typeof params.size !== "number" || !Number.isFinite(params.size) || params.size <= 0) {
|
|
93
|
+
throw animalError("RIG", "params.size must be finite and positive");
|
|
94
|
+
}
|
|
95
|
+
if (params.seed !== void 0 && params.seed !== header.seed)
|
|
96
|
+
throw animalError("OPTIONS", "individual seed contradicts bake seed");
|
|
97
|
+
const points = joints(header.joints, "joints");
|
|
98
|
+
joints(header.refJoints, "refJoints");
|
|
99
|
+
if (!Array.isArray(header.bones)) throw animalError("RIG", "bones must be an array");
|
|
100
|
+
integer(header.bones.length, 1, ANIMAL_LIMITS.bones, "bone count");
|
|
101
|
+
const parents = /* @__PURE__ */ new Map();
|
|
102
|
+
for (const value of header.bones) {
|
|
103
|
+
const bone = record(value, "bone");
|
|
104
|
+
const key = name(bone.name, "bone.name");
|
|
105
|
+
if (parents.has(key)) throw animalError("RIG", `duplicate bone ${key}`);
|
|
106
|
+
const parent = bone.parent === null ? null : name(bone.parent, `${key}.parent`);
|
|
107
|
+
parents.set(key, parent);
|
|
108
|
+
const headName = name(bone.headJ, `${key}.headJ`);
|
|
109
|
+
const tailName = name(bone.tailJ, `${key}.tailJ`);
|
|
110
|
+
const head = Object.hasOwn(points, headName) ? points[headName] : void 0;
|
|
111
|
+
const tail = Object.hasOwn(points, tailName) ? points[tailName] : void 0;
|
|
112
|
+
const length = head && tail ? Math.hypot(head[0] - tail[0], head[1] - tail[1], head[2] - tail[2]) : Number.NaN;
|
|
113
|
+
if (!Number.isFinite(length) || length < 1e-8) {
|
|
114
|
+
throw animalError("RIG", `${key} has missing joints or zero length`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const key of parents.keys()) {
|
|
118
|
+
const seen = /* @__PURE__ */ new Set();
|
|
119
|
+
let current = key;
|
|
120
|
+
while (current !== null) {
|
|
121
|
+
if (seen.has(current) || !parents.has(current))
|
|
122
|
+
throw animalError("RIG", `${key} has a cyclic or missing parent`);
|
|
123
|
+
seen.add(current);
|
|
124
|
+
current = parents.get(current) ?? null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function validateArrayData(arrays, vertices, boneCount) {
|
|
129
|
+
for (const key of Object.keys(ANIMAL_ARRAYS)) {
|
|
130
|
+
const array = arrays[key];
|
|
131
|
+
const [Constructor, width] = ANIMAL_ARRAYS[key];
|
|
132
|
+
if (!(array instanceof Constructor) || !array || (key === "index" ? array.length < 3 || array.length > ANIMAL_LIMITS.indices || array.length % 3 !== 0 : array.length !== vertices * width))
|
|
133
|
+
throw animalError("SECTIONS", `${key} changed after validation`);
|
|
134
|
+
for (const value of array) {
|
|
135
|
+
if (!Number.isFinite(value))
|
|
136
|
+
throw animalError("FINITE", `${key} contains a non-finite value`);
|
|
137
|
+
if (key === "index" && value >= vertices)
|
|
138
|
+
throw animalError("INDEX", `vertex index ${value} exceeds vertex count`);
|
|
139
|
+
if (key === "skinIndex" && value >= boneCount)
|
|
140
|
+
throw animalError("BONE_INDEX", `bone index ${value} exceeds bone count`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const weights = arrays.skinWeight;
|
|
144
|
+
for (let i = 0; i < weights.length; i += 4) {
|
|
145
|
+
const group = weights.subarray(i, i + 4);
|
|
146
|
+
if (group.some((weight) => weight < 0 || weight > 1) || Math.abs(group.reduce((a, b) => a + b, 0) - 1) > 1e-4) {
|
|
147
|
+
throw animalError("WEIGHTS", `vertex ${i / 4} has non-normalized skin weights`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function parseAnimalBake(buffer) {
|
|
152
|
+
if (!(buffer instanceof ArrayBuffer) || buffer.byteLength < 16 || buffer.byteLength > ANIMAL_LIMITS.bytes) {
|
|
153
|
+
throw animalError("LIMIT", `payload must contain 16..${ANIMAL_LIMITS.bytes} bytes`);
|
|
154
|
+
}
|
|
155
|
+
const bytes = new Uint8Array(buffer);
|
|
156
|
+
const view = new DataView(buffer);
|
|
157
|
+
if (view.getUint32(0, false) !== 1346457165) throw animalError("MAGIC", "expected PANM");
|
|
158
|
+
if (view.getUint32(4, true) !== 1) throw animalError("REVISION", "unknown PANM version");
|
|
159
|
+
const headerBytes = integer(
|
|
160
|
+
view.getUint32(8, true),
|
|
161
|
+
8,
|
|
162
|
+
ANIMAL_LIMITS.headerBytes,
|
|
163
|
+
"header bytes"
|
|
164
|
+
);
|
|
165
|
+
const start = Math.ceil((12 + headerBytes) / 8) * 8;
|
|
166
|
+
if (headerBytes % 8 !== 0 || start > buffer.byteLength)
|
|
167
|
+
throw animalError("SECTIONS", "truncated or unaligned header");
|
|
168
|
+
let header;
|
|
169
|
+
try {
|
|
170
|
+
header = record(
|
|
171
|
+
JSON.parse(
|
|
172
|
+
new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(12, 12 + headerBytes))
|
|
173
|
+
),
|
|
174
|
+
"header"
|
|
175
|
+
);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
throw animalError("HEADER", `invalid UTF-8/JSON header: ${String(error)}`);
|
|
178
|
+
}
|
|
179
|
+
const pending = [{ value: header, depth: 0 }];
|
|
180
|
+
let visited = 0;
|
|
181
|
+
while (pending.length > 0) {
|
|
182
|
+
if (++visited > 1e5)
|
|
183
|
+
throw animalError("LIMIT", "header exceeds the structural traversal limit");
|
|
184
|
+
const entry = pending.pop();
|
|
185
|
+
if (!entry) break;
|
|
186
|
+
const { value, depth } = entry;
|
|
187
|
+
if (depth > 64) throw animalError("LIMIT", "header nesting exceeds 64 levels");
|
|
188
|
+
if (typeof value === "number" && !Number.isFinite(value))
|
|
189
|
+
throw animalError("FINITE", "header contains a non-finite number");
|
|
190
|
+
if (value !== null && typeof value === "object")
|
|
191
|
+
for (const child of Object.values(value)) pending.push({ value: child, depth: depth + 1 });
|
|
192
|
+
}
|
|
193
|
+
validateHeader(header);
|
|
194
|
+
if (!Array.isArray(header.arrays) || header.arrays.length !== Object.keys(ANIMAL_ARRAYS).length) {
|
|
195
|
+
throw animalError("SECTIONS", "expected every supported array exactly once");
|
|
196
|
+
}
|
|
197
|
+
const arrays = {};
|
|
198
|
+
const ranges = [];
|
|
199
|
+
for (const value of header.arrays) {
|
|
200
|
+
const section = record(value, "section");
|
|
201
|
+
if (typeof section.key !== "string")
|
|
202
|
+
throw animalError("SECTIONS", "section key must be a string");
|
|
203
|
+
const key = section.key;
|
|
204
|
+
if (!Object.hasOwn(ANIMAL_ARRAYS, key) || arrays[key] !== void 0)
|
|
205
|
+
throw animalError("SECTIONS", `unknown or duplicate section ${String(key)}`);
|
|
206
|
+
const [Constructor, width] = ANIMAL_ARRAYS[key];
|
|
207
|
+
if (section.type !== Constructor.name)
|
|
208
|
+
throw animalError("SECTIONS", `${key} must contain ${Constructor.name}`);
|
|
209
|
+
const length = integer(
|
|
210
|
+
section.length,
|
|
211
|
+
1,
|
|
212
|
+
key === "index" ? ANIMAL_LIMITS.indices : ANIMAL_LIMITS.vertices * width,
|
|
213
|
+
`${key} length`
|
|
214
|
+
);
|
|
215
|
+
if (key === "index" && length % 3 !== 0 || key !== "index" && length !== Number(header.nV) * width) {
|
|
216
|
+
throw animalError("SECTIONS", `${key} length contradicts the vertex or triangle count`);
|
|
217
|
+
}
|
|
218
|
+
const offset = integer(section.offset, 0, ANIMAL_LIMITS.bytes, `${key} offset`);
|
|
219
|
+
const end2 = start + offset + Math.ceil(length * Constructor.BYTES_PER_ELEMENT / 8) * 8;
|
|
220
|
+
if (offset % 8 !== 0 || end2 > buffer.byteLength)
|
|
221
|
+
throw animalError("SECTIONS", `${key} is unaligned or truncated`);
|
|
222
|
+
ranges.push([start + offset, end2]);
|
|
223
|
+
arrays[key] = new Constructor(buffer, start + offset, length);
|
|
224
|
+
}
|
|
225
|
+
let end = start;
|
|
226
|
+
for (const range of ranges.sort((a, b) => a[0] - b[0])) {
|
|
227
|
+
if (range[0] !== end) throw animalError("SECTIONS", "sections overlap or contain gaps");
|
|
228
|
+
end = range[1];
|
|
229
|
+
}
|
|
230
|
+
if (end !== buffer.byteLength)
|
|
231
|
+
throw animalError("SECTIONS", "byte length contradicts section extents");
|
|
232
|
+
validateArrayData(arrays, Number(header.nV), header.bones.length);
|
|
233
|
+
if (bakeIntegrity(buffer) !== header.threenative.integrity)
|
|
234
|
+
throw animalError("INTEGRITY", "baked header or array bytes changed");
|
|
235
|
+
for (const key of Object.keys(arrays)) arrays[key] = arrays[key]?.slice();
|
|
236
|
+
const { arrays: _sections, ...data } = header;
|
|
237
|
+
const objects = [data];
|
|
238
|
+
while (objects.length > 0) {
|
|
239
|
+
const object = objects.pop();
|
|
240
|
+
if (!object) break;
|
|
241
|
+
for (const value of Object.values(object))
|
|
242
|
+
if (value !== null && typeof value === "object") objects.push(value);
|
|
243
|
+
Object.freeze(object);
|
|
244
|
+
}
|
|
245
|
+
const bake = Object.freeze({ ...data, ...arrays });
|
|
246
|
+
validatedBakes.add(bake);
|
|
247
|
+
return bake;
|
|
248
|
+
}
|
|
249
|
+
function checkCancelled(signal) {
|
|
250
|
+
if (signal?.aborted) throw animalError("ABORTED", "animal load cancelled");
|
|
251
|
+
}
|
|
252
|
+
function abortable(operation, signal, cancel) {
|
|
253
|
+
if (!signal) return operation;
|
|
254
|
+
return new Promise((resolve, reject) => {
|
|
255
|
+
const onAbort = () => {
|
|
256
|
+
cancel?.();
|
|
257
|
+
signal.removeEventListener("abort", onAbort);
|
|
258
|
+
reject(animalError("ABORTED", "animal load cancelled"));
|
|
259
|
+
};
|
|
260
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
261
|
+
operation.then(
|
|
262
|
+
(value) => {
|
|
263
|
+
signal.removeEventListener("abort", onAbort);
|
|
264
|
+
if (signal.aborted) onAbort();
|
|
265
|
+
else resolve(value);
|
|
266
|
+
},
|
|
267
|
+
(error) => {
|
|
268
|
+
signal.removeEventListener("abort", onAbort);
|
|
269
|
+
if (signal.aborted) onAbort();
|
|
270
|
+
else reject(error);
|
|
271
|
+
}
|
|
272
|
+
);
|
|
273
|
+
if (signal.aborted) onAbort();
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
async function readBoundedBody(response, signal) {
|
|
277
|
+
if (!response.body) {
|
|
278
|
+
const bytes = await abortable(response.arrayBuffer(), signal);
|
|
279
|
+
if (bytes.byteLength > ANIMAL_LIMITS.bytes)
|
|
280
|
+
throw animalError("LIMIT", "decoded body exceeds the payload byte limit");
|
|
281
|
+
return bytes;
|
|
282
|
+
}
|
|
283
|
+
const reader = response.body.getReader();
|
|
284
|
+
const chunks = [];
|
|
285
|
+
let length = 0;
|
|
286
|
+
const cancel = () => {
|
|
287
|
+
void reader.cancel().catch(() => void 0);
|
|
288
|
+
};
|
|
289
|
+
try {
|
|
290
|
+
for (; ; ) {
|
|
291
|
+
checkCancelled(signal);
|
|
292
|
+
const result = await abortable(reader.read(), signal, cancel);
|
|
293
|
+
checkCancelled(signal);
|
|
294
|
+
if (result.done) break;
|
|
295
|
+
length += result.value.byteLength;
|
|
296
|
+
if (length > ANIMAL_LIMITS.bytes)
|
|
297
|
+
throw animalError("LIMIT", "decoded body exceeds the payload byte limit");
|
|
298
|
+
chunks.push(result.value);
|
|
299
|
+
}
|
|
300
|
+
const buffer = new ArrayBuffer(length);
|
|
301
|
+
const bytes = new Uint8Array(buffer);
|
|
302
|
+
let offset = 0;
|
|
303
|
+
for (const chunk of chunks) {
|
|
304
|
+
bytes.set(chunk, offset);
|
|
305
|
+
offset += chunk.byteLength;
|
|
306
|
+
}
|
|
307
|
+
return buffer;
|
|
308
|
+
} catch (error) {
|
|
309
|
+
cancel();
|
|
310
|
+
checkCancelled(signal);
|
|
311
|
+
throw error;
|
|
312
|
+
} finally {
|
|
313
|
+
reader.releaseLock();
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
async function loadAnimalBake(assets, logicalPath, options = {}) {
|
|
317
|
+
checkCancelled(options.signal);
|
|
318
|
+
const urls = await abortable(assets.resolve(logicalPath), options.signal);
|
|
319
|
+
checkCancelled(options.signal);
|
|
320
|
+
if (urls.length === 0) throw animalError("LOAD", `${logicalPath} resolved to no asset`);
|
|
321
|
+
const failures = [];
|
|
322
|
+
for (const url of urls) {
|
|
323
|
+
let response;
|
|
324
|
+
try {
|
|
325
|
+
response = await fetch(url, { signal: options.signal });
|
|
326
|
+
} catch (error) {
|
|
327
|
+
checkCancelled(options.signal);
|
|
328
|
+
failures.push(`${url}: ${String(error)}`);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (options.signal?.aborted) {
|
|
332
|
+
await response.body?.cancel().catch(() => void 0);
|
|
333
|
+
checkCancelled(options.signal);
|
|
334
|
+
}
|
|
335
|
+
if (!response.ok) {
|
|
336
|
+
await response.body?.cancel().catch(() => void 0);
|
|
337
|
+
failures.push(`${url}: HTTP ${response.status}`);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
const announced = response.headers.get("content-length");
|
|
341
|
+
if (announced !== null && Number(announced) > ANIMAL_LIMITS.bytes) {
|
|
342
|
+
await response.body?.cancel().catch(() => void 0);
|
|
343
|
+
throw animalError("LIMIT", `${logicalPath} exceeds the payload byte limit`);
|
|
344
|
+
}
|
|
345
|
+
const buffer = await readBoundedBody(response, options.signal);
|
|
346
|
+
checkCancelled(options.signal);
|
|
347
|
+
return parseAnimalBake(buffer);
|
|
348
|
+
}
|
|
349
|
+
throw animalError("LOAD", `${logicalPath} failed: ${failures.join("; ")}`);
|
|
350
|
+
}
|
|
351
|
+
function createAnimalGeometry(buffer) {
|
|
352
|
+
const bake = parseAnimalBake(buffer);
|
|
353
|
+
return { bake, geometry: geometryFromValidatedBake(bake) };
|
|
354
|
+
}
|
|
355
|
+
function geometryFromValidatedBake(bake) {
|
|
356
|
+
requireValidatedBake(bake);
|
|
357
|
+
const geometry = new BufferGeometry();
|
|
358
|
+
for (const [name2, data, size] of [
|
|
359
|
+
["position", bake.pos, 3],
|
|
360
|
+
["normal", bake.nrm, 3],
|
|
361
|
+
["skinIndex", bake.skinIndex, 4],
|
|
362
|
+
["skinWeight", bake.skinWeight, 4],
|
|
363
|
+
["aComb", bake.comb, 3],
|
|
364
|
+
["aTint", bake.tint, 4],
|
|
365
|
+
["aCoat", bake.coat, 4],
|
|
366
|
+
["aPat", bake.pat, 4],
|
|
367
|
+
["aSurf", bake.surf, 4]
|
|
368
|
+
])
|
|
369
|
+
geometry.setAttribute(name2, new BufferAttribute(data.slice(), size));
|
|
370
|
+
geometry.setIndex(new BufferAttribute(bake.index.slice(), 1));
|
|
371
|
+
return geometry;
|
|
372
|
+
}
|
|
373
|
+
function float32Norm(x, y, z, w = 0) {
|
|
374
|
+
return Math.sqrt(x * x + y * y + z * z + w * w);
|
|
375
|
+
}
|
|
376
|
+
function createAnimalBounds(bake, geometry) {
|
|
377
|
+
requireValidatedBake(bake);
|
|
378
|
+
let radius = 0;
|
|
379
|
+
let minimumFirstWeight = 1;
|
|
380
|
+
let maximumWeightSum = 0;
|
|
381
|
+
let previousPosition;
|
|
382
|
+
let previousWeights;
|
|
383
|
+
let previousIndices;
|
|
384
|
+
let positionVersion = -1;
|
|
385
|
+
let weightVersion = -1;
|
|
386
|
+
let indexVersion = -1;
|
|
387
|
+
const measure = () => {
|
|
388
|
+
const attribute = (name2, size) => {
|
|
389
|
+
if (!geometry) return void 0;
|
|
390
|
+
const value = geometry.getAttribute(name2);
|
|
391
|
+
if (!(value instanceof BufferAttribute) || value.itemSize !== size || value.count !== bake.nV)
|
|
392
|
+
throw animalError(
|
|
393
|
+
"BOUNDS",
|
|
394
|
+
"edited geometry must retain ordinary position/skin attributes and vertex count"
|
|
395
|
+
);
|
|
396
|
+
return value;
|
|
397
|
+
};
|
|
398
|
+
const position = attribute("position", 3);
|
|
399
|
+
const weights = attribute("skinWeight", 4);
|
|
400
|
+
const indices = attribute("skinIndex", 4);
|
|
401
|
+
if (weights?.normalized || indices?.normalized)
|
|
402
|
+
throw animalError("BOUNDS", "normalized edited skin attributes are unsupported");
|
|
403
|
+
if (position === previousPosition && weights === previousWeights && indices === previousIndices && position?.version === positionVersion && weights?.version === weightVersion && indices?.version === indexVersion)
|
|
404
|
+
return;
|
|
405
|
+
radius = 0;
|
|
406
|
+
minimumFirstWeight = 1;
|
|
407
|
+
maximumWeightSum = 0;
|
|
408
|
+
for (let v = 0; v < bake.nV; v++) {
|
|
409
|
+
const x = position?.getX(v) ?? bake.pos[v * 3] ?? 0;
|
|
410
|
+
const y = position?.getY(v) ?? bake.pos[v * 3 + 1] ?? 0;
|
|
411
|
+
const z = position?.getZ(v) ?? bake.pos[v * 3 + 2] ?? 0;
|
|
412
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z))
|
|
413
|
+
throw animalError("BOUNDS", "edited bind positions must be finite");
|
|
414
|
+
radius = Math.max(radius, Math.hypot(x, y, z));
|
|
415
|
+
let sum = 0;
|
|
416
|
+
for (let influence = 0; influence < 4; influence++) {
|
|
417
|
+
const weight = weights ? weights.array[v * 4 + influence] : bake.skinWeight[v * 4 + influence];
|
|
418
|
+
const index = indices ? indices.array[v * 4 + influence] : bake.skinIndex[v * 4 + influence];
|
|
419
|
+
if (weight === void 0 || !Number.isFinite(weight) || weight < 0 || weight > 1 || index === void 0 || !Number.isInteger(index) || index < 0 || index >= bake.bones.length)
|
|
420
|
+
throw animalError("BOUNDS", "edited skin weights/indices must remain valid");
|
|
421
|
+
if (influence === 0) minimumFirstWeight = Math.min(minimumFirstWeight, weight);
|
|
422
|
+
sum += weight;
|
|
423
|
+
}
|
|
424
|
+
if (Math.abs(sum - 1) > 1e-4)
|
|
425
|
+
throw animalError("BOUNDS", "edited skin weights must remain normalized");
|
|
426
|
+
maximumWeightSum = Math.max(maximumWeightSum, sum);
|
|
427
|
+
}
|
|
428
|
+
previousPosition = position;
|
|
429
|
+
previousWeights = weights;
|
|
430
|
+
previousIndices = indices;
|
|
431
|
+
positionVersion = previousPosition?.version ?? -1;
|
|
432
|
+
weightVersion = previousWeights?.version ?? -1;
|
|
433
|
+
indexVersion = previousIndices?.version ?? -1;
|
|
434
|
+
};
|
|
435
|
+
measure();
|
|
436
|
+
const sphere = new Sphere(new Vector3(), 0);
|
|
437
|
+
const box = new Box3();
|
|
438
|
+
return {
|
|
439
|
+
sphere,
|
|
440
|
+
box,
|
|
441
|
+
update(data) {
|
|
442
|
+
if (geometry) measure();
|
|
443
|
+
sphere.center.set(0, 0, 0);
|
|
444
|
+
if (data.length !== bake.bones.length * 20)
|
|
445
|
+
throw animalError("BOUNDS", "missing or non-finite pose packet");
|
|
446
|
+
for (let p = 0; p < data.length; p++)
|
|
447
|
+
if (!Number.isFinite(data[p]))
|
|
448
|
+
throw animalError("BOUNDS", "missing or non-finite pose packet");
|
|
449
|
+
let scaledRadius = 0;
|
|
450
|
+
let dualRadius = 0;
|
|
451
|
+
let minimumQuaternionLength = Number.POSITIVE_INFINITY;
|
|
452
|
+
const norm = data instanceof Float32Array ? float32Norm : Math.hypot;
|
|
453
|
+
for (let o = 0; o < data.length; o += 20) {
|
|
454
|
+
minimumQuaternionLength = Math.min(
|
|
455
|
+
minimumQuaternionLength,
|
|
456
|
+
norm(data[o] ?? 0, data[o + 1] ?? 0, data[o + 2] ?? 0, data[o + 3] ?? 0)
|
|
457
|
+
);
|
|
458
|
+
dualRadius = Math.max(
|
|
459
|
+
dualRadius,
|
|
460
|
+
norm(data[o + 4] ?? 0, data[o + 5] ?? 0, data[o + 6] ?? 0, data[o + 7] ?? 0)
|
|
461
|
+
);
|
|
462
|
+
let normSquared = 0;
|
|
463
|
+
for (let row = 8; row <= 16; row += 4)
|
|
464
|
+
for (let column = 0; column < 3; column++)
|
|
465
|
+
normSquared += (data[o + row + column] ?? 0) ** 2;
|
|
466
|
+
const offset = norm(data[o + 11] ?? 0, data[o + 15] ?? 0, data[o + 19] ?? 0);
|
|
467
|
+
scaledRadius = Math.max(scaledRadius, Math.sqrt(normSquared) * radius + offset);
|
|
468
|
+
}
|
|
469
|
+
const denominator = Math.max(minimumFirstWeight * minimumQuaternionLength, 1e-6);
|
|
470
|
+
const bound = scaledRadius + 2 * dualRadius * maximumWeightSum / denominator;
|
|
471
|
+
if (!Number.isFinite(bound)) throw animalError("BOUNDS", "unbounded pose envelope");
|
|
472
|
+
sphere.radius = bound * (1 + 64 * 2 ** -23) + 64 * 2 ** -23;
|
|
473
|
+
sphere.getBoundingBox(box);
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
function bindFrame(head, tail) {
|
|
478
|
+
const origin = new Vector3().fromArray(head);
|
|
479
|
+
const y = new Vector3().fromArray(tail).sub(origin).normalize();
|
|
480
|
+
const x = new Vector3(1, 0, 0).addScaledVector(y, -y.x);
|
|
481
|
+
if (x.lengthSq() < 1e-10) x.set(0, 0, 1).addScaledVector(y, -y.z);
|
|
482
|
+
x.normalize();
|
|
483
|
+
return new Matrix4().makeBasis(x, y, new Vector3().crossVectors(x, y)).setPosition(origin);
|
|
484
|
+
}
|
|
485
|
+
function createAnimalPose(bake) {
|
|
486
|
+
requireValidatedBake(bake);
|
|
487
|
+
const bind = bake.bones.map((bone) => {
|
|
488
|
+
const head = bake.joints[bone.headJ];
|
|
489
|
+
const tail = bake.joints[bone.tailJ];
|
|
490
|
+
if (!head || !tail) throw animalError("RIG", "validated joint disappeared");
|
|
491
|
+
return bindFrame(head, tail);
|
|
492
|
+
});
|
|
493
|
+
const bones = bind.map((matrix, i) => {
|
|
494
|
+
const bone = new Bone();
|
|
495
|
+
bone.name = bake.bones[i]?.name ?? "";
|
|
496
|
+
bone.matrixAutoUpdate = bone.matrixWorldAutoUpdate = false;
|
|
497
|
+
bone.matrixWorld.copy(matrix);
|
|
498
|
+
return bone;
|
|
499
|
+
});
|
|
500
|
+
const skeleton = { bones, bind, inverses: bind.map((matrix) => matrix.clone().invert()) };
|
|
501
|
+
const data = new Float32Array(bones.length * 20);
|
|
502
|
+
const pending = new Float32Array(data.length);
|
|
503
|
+
const texture = new DataTexture(data, bones.length * 5, 1, RGBAFormat, FloatType);
|
|
504
|
+
texture.minFilter = texture.magFilter = NearestFilter;
|
|
505
|
+
texture.generateMipmaps = false;
|
|
506
|
+
const bindRotation = [];
|
|
507
|
+
const bindPosition = [];
|
|
508
|
+
const bindAxes = [];
|
|
509
|
+
const position = new Vector3();
|
|
510
|
+
const rotation = new Quaternion();
|
|
511
|
+
const scale = new Vector3();
|
|
512
|
+
for (const matrix of bind) {
|
|
513
|
+
matrix.decompose(position, rotation, scale);
|
|
514
|
+
bindPosition.push(position.clone());
|
|
515
|
+
bindRotation.push(rotation.clone().invert());
|
|
516
|
+
bindAxes.push(new Matrix3().setFromMatrix4(new Matrix4().makeRotationFromQuaternion(rotation)));
|
|
517
|
+
}
|
|
518
|
+
const local = new Matrix4();
|
|
519
|
+
const affine = new Matrix3();
|
|
520
|
+
const scaling = new Matrix3();
|
|
521
|
+
const transpose = new Matrix3();
|
|
522
|
+
const translation = new Vector3();
|
|
523
|
+
let disposed = false;
|
|
524
|
+
return {
|
|
525
|
+
skeleton,
|
|
526
|
+
data,
|
|
527
|
+
texture,
|
|
528
|
+
update(worldToLocal) {
|
|
529
|
+
if (disposed) throw animalError("DISPOSED", "pose is disposed");
|
|
530
|
+
for (let i = 0; i < bones.length; i++) {
|
|
531
|
+
const bone = bones[i];
|
|
532
|
+
const inverse = bindRotation[i];
|
|
533
|
+
const origin = bindPosition[i];
|
|
534
|
+
const axes = bindAxes[i];
|
|
535
|
+
if (!bone || !inverse || !origin || !axes) throw animalError("RIG", "missing pose bone");
|
|
536
|
+
local.multiplyMatrices(worldToLocal, bone.matrixWorld);
|
|
537
|
+
for (let j = 0; j < local.elements.length; j++)
|
|
538
|
+
if (!Number.isFinite(local.elements[j]))
|
|
539
|
+
throw animalError("POSE", `${bone.name} has non-finite matrix`);
|
|
540
|
+
local.decompose(position, rotation, scale);
|
|
541
|
+
if (!(Math.abs(scale.x) > 1e-8) || !(Math.abs(scale.y) > 1e-8) || !(Math.abs(scale.z) > 1e-8))
|
|
542
|
+
throw animalError("POSE", `${bone.name} has singular scale`);
|
|
543
|
+
rotation.multiply(inverse);
|
|
544
|
+
translation.copy(origin).applyQuaternion(rotation).negate().add(position);
|
|
545
|
+
const o = i * 20;
|
|
546
|
+
const { x, y, z, w } = rotation;
|
|
547
|
+
const tx = translation.x;
|
|
548
|
+
const ty = translation.y;
|
|
549
|
+
const tz = translation.z;
|
|
550
|
+
pending[o] = x;
|
|
551
|
+
pending[o + 1] = y;
|
|
552
|
+
pending[o + 2] = z;
|
|
553
|
+
pending[o + 3] = w;
|
|
554
|
+
pending[o + 4] = 0.5 * (w * tx + ty * z - tz * y);
|
|
555
|
+
pending[o + 5] = 0.5 * (w * ty + tz * x - tx * z);
|
|
556
|
+
pending[o + 6] = 0.5 * (w * tz + tx * y - ty * x);
|
|
557
|
+
pending[o + 7] = -0.5 * (tx * x + ty * y + tz * z);
|
|
558
|
+
affine.multiplyMatrices(axes, scaling.set(scale.x, 0, 0, 0, scale.y, 0, 0, 0, scale.z)).multiply(transpose.copy(axes).transpose());
|
|
559
|
+
const e = affine.elements;
|
|
560
|
+
pending[o + 8] = e[0];
|
|
561
|
+
pending[o + 9] = e[3];
|
|
562
|
+
pending[o + 10] = e[6];
|
|
563
|
+
pending[o + 11] = origin.x - (e[0] * origin.x + e[3] * origin.y + e[6] * origin.z);
|
|
564
|
+
pending[o + 12] = e[1];
|
|
565
|
+
pending[o + 13] = e[4];
|
|
566
|
+
pending[o + 14] = e[7];
|
|
567
|
+
pending[o + 15] = origin.y - (e[1] * origin.x + e[4] * origin.y + e[7] * origin.z);
|
|
568
|
+
pending[o + 16] = e[2];
|
|
569
|
+
pending[o + 17] = e[5];
|
|
570
|
+
pending[o + 18] = e[8];
|
|
571
|
+
pending[o + 19] = origin.z - (e[2] * origin.x + e[5] * origin.y + e[8] * origin.z);
|
|
572
|
+
}
|
|
573
|
+
for (let p = 0; p < pending.length; p++)
|
|
574
|
+
if (!Number.isFinite(pending[p]))
|
|
575
|
+
throw animalError("POSE", "pose packet exceeds float range");
|
|
576
|
+
data.set(pending);
|
|
577
|
+
texture.needsUpdate = true;
|
|
578
|
+
},
|
|
579
|
+
dispose() {
|
|
580
|
+
if (disposed) return;
|
|
581
|
+
disposed = true;
|
|
582
|
+
texture.dispose();
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// src/follow.ts
|
|
588
|
+
var identity = new Matrix4();
|
|
589
|
+
function requireIdentityParent(object) {
|
|
590
|
+
if (!object) return;
|
|
591
|
+
if (!object.matrix.elements.every(Number.isFinite))
|
|
592
|
+
throw animalError("PARENT_TRANSFORM", "parent local matrix must be finite");
|
|
593
|
+
object.updateWorldMatrix(true, false);
|
|
594
|
+
if (object.matrixWorld.elements.some(
|
|
595
|
+
(value, index) => !Number.isFinite(value) || Math.abs(value - (identity.elements[index] ?? 0)) > 1e-9
|
|
596
|
+
))
|
|
597
|
+
throw animalError(
|
|
598
|
+
"PARENT_TRANSFORM",
|
|
599
|
+
"the first animal adapter requires an identity parent world transform"
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
function finiteVector(value) {
|
|
603
|
+
if (!value || ![value.x, value.y, value.z].every(Number.isFinite))
|
|
604
|
+
throw animalError("FOLLOW_STATE", "accepted vectors must be finite");
|
|
605
|
+
}
|
|
606
|
+
function cleanupOwned(callbacks, failure) {
|
|
607
|
+
const errors = failure === void 0 ? [] : [failure];
|
|
608
|
+
for (const callback of callbacks) {
|
|
609
|
+
try {
|
|
610
|
+
callback();
|
|
611
|
+
} catch (error) {
|
|
612
|
+
errors.push(error);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (errors.length === 1) throw errors[0];
|
|
616
|
+
if (errors.length > 1) throw new AggregateError(errors, errors.map(String).join("; "));
|
|
617
|
+
}
|
|
618
|
+
function createAnimalActor(bake, options) {
|
|
619
|
+
requireValidatedBake(bake);
|
|
620
|
+
requireIdentityParent(options.parent ?? null);
|
|
621
|
+
const originOffset = new Vector3().copy(options.visualOriginOffset ?? { x: 0, y: 0, z: 0 });
|
|
622
|
+
finiteVector(originOffset);
|
|
623
|
+
if (typeof options.ground !== "function" || typeof options.motion !== "function" || typeof options.material !== "function")
|
|
624
|
+
throw animalError("OPTIONS", "ground, game motion and material factories are required");
|
|
625
|
+
const object = new Group();
|
|
626
|
+
const root = new Vector3();
|
|
627
|
+
const velocity = new Vector3();
|
|
628
|
+
const foot = new Vector3();
|
|
629
|
+
const geometry = geometryFromValidatedBake(bake);
|
|
630
|
+
const pose = createAnimalPose(bake);
|
|
631
|
+
const bounds = createAnimalBounds(bake, geometry);
|
|
632
|
+
const worldToLocal = new Matrix4();
|
|
633
|
+
let heading = 0;
|
|
634
|
+
let groundBias = 0;
|
|
635
|
+
let disposed = false;
|
|
636
|
+
let paused = false;
|
|
637
|
+
const actions = /* @__PURE__ */ new Set();
|
|
638
|
+
const ground = (x, z) => {
|
|
639
|
+
const height = options.ground(x, z);
|
|
640
|
+
if (!Number.isFinite(height))
|
|
641
|
+
throw animalError("GROUND", `no finite physics/navigation ground at ${x},${z}`);
|
|
642
|
+
return height + groundBias;
|
|
643
|
+
};
|
|
644
|
+
const createMotion = () => options.motion({
|
|
645
|
+
data: bake,
|
|
646
|
+
skeleton: pose.skeleton,
|
|
647
|
+
ground,
|
|
648
|
+
position: foot.clone(),
|
|
649
|
+
heading,
|
|
650
|
+
emit: () => void 0
|
|
651
|
+
});
|
|
652
|
+
let material;
|
|
653
|
+
let motion;
|
|
654
|
+
let motionOwned = false;
|
|
655
|
+
try {
|
|
656
|
+
material = options.material(pose);
|
|
657
|
+
if (!(material instanceof Material))
|
|
658
|
+
throw animalError("MATERIAL", "game factory must return a material");
|
|
659
|
+
motion = createMotion();
|
|
660
|
+
motionOwned = true;
|
|
661
|
+
} catch (error) {
|
|
662
|
+
cleanupOwned(
|
|
663
|
+
[
|
|
664
|
+
() => {
|
|
665
|
+
if (material instanceof Material) material.dispose();
|
|
666
|
+
},
|
|
667
|
+
() => geometry.dispose(),
|
|
668
|
+
() => pose.dispose()
|
|
669
|
+
],
|
|
670
|
+
error
|
|
671
|
+
);
|
|
672
|
+
throw error;
|
|
673
|
+
}
|
|
674
|
+
if (!material) throw animalError("MATERIAL", "missing material");
|
|
675
|
+
const ownedMaterial = material;
|
|
676
|
+
const mesh = new Mesh(geometry, ownedMaterial);
|
|
677
|
+
object.add(mesh);
|
|
678
|
+
geometry.boundingSphere = bounds.sphere;
|
|
679
|
+
geometry.boundingBox = bounds.box;
|
|
680
|
+
const requireLive = () => {
|
|
681
|
+
if (disposed) throw animalError("DISPOSED", "animal actor is disposed");
|
|
682
|
+
};
|
|
683
|
+
const settle = (result) => {
|
|
684
|
+
for (const done of [...actions]) done(result);
|
|
685
|
+
};
|
|
686
|
+
const dispose = () => {
|
|
687
|
+
if (disposed) return;
|
|
688
|
+
disposed = true;
|
|
689
|
+
settle("disposed");
|
|
690
|
+
cleanupOwned([
|
|
691
|
+
() => {
|
|
692
|
+
if (motionOwned) {
|
|
693
|
+
motionOwned = false;
|
|
694
|
+
motion.dispose();
|
|
695
|
+
}
|
|
696
|
+
},
|
|
697
|
+
() => object.removeFromParent(),
|
|
698
|
+
() => object.clear(),
|
|
699
|
+
() => geometry.dispose(),
|
|
700
|
+
() => ownedMaterial.dispose(),
|
|
701
|
+
() => pose.dispose()
|
|
702
|
+
]);
|
|
703
|
+
};
|
|
704
|
+
const nextFoot = new Vector3();
|
|
705
|
+
const rootEuler = new Euler();
|
|
706
|
+
const up = new Vector3(0, 1, 0);
|
|
707
|
+
const following = { position: foot, velocity, heading: 0 };
|
|
708
|
+
const assign = (state) => {
|
|
709
|
+
finiteVector(state.position);
|
|
710
|
+
finiteVector(state.velocity);
|
|
711
|
+
if (!Number.isFinite(state.heading))
|
|
712
|
+
throw animalError("FOLLOW_STATE", "accepted heading must be finite");
|
|
713
|
+
requireIdentityParent(object.parent);
|
|
714
|
+
nextFoot.copy(originOffset).applyAxisAngle(up, state.heading).add(state.position);
|
|
715
|
+
finiteVector(nextFoot);
|
|
716
|
+
const sampled = options.ground(nextFoot.x, nextFoot.z);
|
|
717
|
+
if (!Number.isFinite(sampled)) throw animalError("GROUND", "missing ground at accepted root");
|
|
718
|
+
root.copy(state.position);
|
|
719
|
+
velocity.copy(state.velocity);
|
|
720
|
+
heading = state.heading;
|
|
721
|
+
object.position.copy(root);
|
|
722
|
+
object.quaternion.setFromEuler(rootEuler.set(0, heading, 0));
|
|
723
|
+
foot.copy(nextFoot);
|
|
724
|
+
groundBias = foot.y - sampled;
|
|
725
|
+
motion.input.speed = 0;
|
|
726
|
+
motion.input.target = null;
|
|
727
|
+
motion.input.heading = heading;
|
|
728
|
+
following.heading = heading;
|
|
729
|
+
motion.input.follow = following;
|
|
730
|
+
};
|
|
731
|
+
const upload = () => {
|
|
732
|
+
object.updateMatrix();
|
|
733
|
+
worldToLocal.copy(object.matrix).invert();
|
|
734
|
+
pose.update(worldToLocal);
|
|
735
|
+
bounds.update(pose.data);
|
|
736
|
+
};
|
|
737
|
+
try {
|
|
738
|
+
options.parent?.add(object);
|
|
739
|
+
motion.update(1e-4);
|
|
740
|
+
upload();
|
|
741
|
+
} catch (error) {
|
|
742
|
+
cleanupOwned([dispose], error);
|
|
743
|
+
throw error;
|
|
744
|
+
}
|
|
745
|
+
return {
|
|
746
|
+
object,
|
|
747
|
+
mesh,
|
|
748
|
+
pose,
|
|
749
|
+
bounds,
|
|
750
|
+
/** Measurement survives the explicit origin override. */
|
|
751
|
+
visualOriginOffset: originOffset.clone(),
|
|
752
|
+
get paused() {
|
|
753
|
+
return paused;
|
|
754
|
+
},
|
|
755
|
+
set paused(value) {
|
|
756
|
+
requireLive();
|
|
757
|
+
paused = value;
|
|
758
|
+
},
|
|
759
|
+
follow(state, dt) {
|
|
760
|
+
requireLive();
|
|
761
|
+
if (!Number.isFinite(dt) || dt < 0 || dt > 1 / 20)
|
|
762
|
+
throw animalError("FIXED_STEP", "follow requires a finite fixed step in [0, 1/20]");
|
|
763
|
+
assign(state);
|
|
764
|
+
if (paused || dt === 0) {
|
|
765
|
+
bounds.update(pose.data);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
motion.update(dt);
|
|
769
|
+
upload();
|
|
770
|
+
},
|
|
771
|
+
teleport(state) {
|
|
772
|
+
requireLive();
|
|
773
|
+
assign(state);
|
|
774
|
+
settle("interrupted");
|
|
775
|
+
try {
|
|
776
|
+
motionOwned = false;
|
|
777
|
+
motion.dispose();
|
|
778
|
+
velocity.set(0, 0, 0);
|
|
779
|
+
motion = createMotion();
|
|
780
|
+
motionOwned = true;
|
|
781
|
+
following.heading = heading;
|
|
782
|
+
motion.input.follow = following;
|
|
783
|
+
motion.update(1e-4);
|
|
784
|
+
upload();
|
|
785
|
+
} catch (error) {
|
|
786
|
+
cleanupOwned([dispose], error);
|
|
787
|
+
throw error;
|
|
788
|
+
}
|
|
789
|
+
},
|
|
790
|
+
play(name2) {
|
|
791
|
+
requireLive();
|
|
792
|
+
if (!["stand", "sit", "lie"].includes(name2))
|
|
793
|
+
throw animalError("ACTION", `unsupported first-slice action ${name2}`);
|
|
794
|
+
return new Promise((resolve, reject) => {
|
|
795
|
+
const done = (result) => {
|
|
796
|
+
if (!actions.delete(done)) return;
|
|
797
|
+
resolve(result);
|
|
798
|
+
};
|
|
799
|
+
actions.add(done);
|
|
800
|
+
try {
|
|
801
|
+
motion.play(name2).then(done, (error) => {
|
|
802
|
+
actions.delete(done);
|
|
803
|
+
reject(error);
|
|
804
|
+
});
|
|
805
|
+
} catch (error) {
|
|
806
|
+
actions.delete(done);
|
|
807
|
+
reject(error);
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
},
|
|
811
|
+
dispose
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
export { createAnimalActor, createAnimalGeometry, loadAnimalBake, parseAnimalBake };
|