@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
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Majid Manzarpour and procedural-animals contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,44 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @threenative/procedural-animals
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Optional bake-first wolf content. The build entry pins Procedural Animals at
|
|
4
|
+
`c95ae49346aa8e140a924376cec6cf0073d99512` and Node 20.19.6. Runtime imports contain
|
|
5
|
+
no generator, worker or Node dependency. Frozen installed browser/Linux correctness
|
|
6
|
+
runs passed; paired frame budgets and fifty GPU lifecycle cycles remain unqualified.
|
|
7
|
+
See the [owning PRD](../../docs/PRDs/done/PRD-procedural-animal-content.md).
|
|
8
|
+
|
|
9
|
+
The installed engine MCP discovers this optional package through
|
|
10
|
+
`engine_search_capabilities` and `engine_capability_detail`, using the ordinary
|
|
11
|
+
generated capability manifest. Runtime capabilities are `createAnimalActor`,
|
|
12
|
+
`createAnimalGeometry`, `loadAnimalBake` and `parseAnimalBake` from
|
|
13
|
+
`@threenative/procedural-animals`. Build capabilities are `animalBakePass`,
|
|
14
|
+
`bakeWolf` and `bakeWolfToFile` from `@threenative/procedural-animals/build`.
|
|
15
|
+
Each entry reports the optional dependency, an example and its constraints.
|
|
16
|
+
|
|
17
|
+
At build time, call `bakeWolfToFile({ seed: 7, tier: "crowd" }, destination)` from
|
|
18
|
+
`@threenative/procedural-animals/build`, then pass `animalBakePass()` to the normal
|
|
19
|
+
`@threenative/assets` compiler. High and crowd bakes share individual parameters.
|
|
20
|
+
Each source is validated before a same-directory atomic rename; the asset compiler
|
|
21
|
+
owns the hashed manifest. Timing statistics are excluded from deterministic bytes.
|
|
22
|
+
|
|
23
|
+
At runtime, `loadAnimalBake(ctx.assets, logicalPath, { signal })` resolves the cooked
|
|
24
|
+
file and validates the complete PANM payload before constructing resources. Limits:
|
|
25
|
+
64 MiB, 250,000 vertices, 1.5 million indices and 256 bones. Unknown revisions,
|
|
26
|
+
malformed offsets/indices, non-finite values and unnormalized weights fail by name.
|
|
27
|
+
The corruption checksum is not an authenticity signature.
|
|
28
|
+
|
|
29
|
+
`createAnimalActor(bake, { motion, material, ground })` owns instance geometry, pose,
|
|
30
|
+
bounds and lifetime. Supply editable game motion and a DQS material from `src/render/`.
|
|
31
|
+
Call `follow(acceptedState, dt)` in `afterPhysics`; accepted position/velocity and
|
|
32
|
+
heading are the only root authority. An identity parent is required. A named
|
|
33
|
+
`visualOriginOffset` accommodates centred physics capsules and remains reported.
|
|
34
|
+
Pause freezes the pose; teleport replaces motion history; disposal settles pending
|
|
35
|
+
actions and attempts every resource cleanup. The first action set is stand/sit/lie.
|
|
36
|
+
|
|
37
|
+
The example contains the pinned wolf's editable motion closure and a base-surface
|
|
38
|
+
TSL DQS material with normal/shadow skinning. Fur, fins, eyes, runtime generation,
|
|
39
|
+
other species and AnimationMixer/composer ownership are outside this slice.
|
|
40
|
+
|
|
41
|
+
The [pinned donor](https://github.com/majidmanzarpour/threejs-procedural-animals/tree/c95ae49346aa8e140a924376cec6cf0073d99512)
|
|
42
|
+
MIT notice is retained in `LICENSES/Procedural-Animals-MIT.txt`. Focused tests use
|
|
43
|
+
actual generation, the ordinary asset cook, real fixed-step Rapier, an independent
|
|
44
|
+
CPU DQS oracle and lifecycle fault controls. These are CPU evidence only.
|
package/dist/build.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { A as AnimalTier } from './format-BRFFv-ws.js';
|
|
2
|
+
|
|
3
|
+
declare const ANIMAL_BUILD_NODE = "20.19.6";
|
|
4
|
+
interface IWolfBakeOptions {
|
|
5
|
+
readonly seed: number;
|
|
6
|
+
readonly tier: AnimalTier;
|
|
7
|
+
readonly sex?: "male" | "female";
|
|
8
|
+
readonly age?: "adult" | "juvenile";
|
|
9
|
+
readonly variant?: "grey" | "black" | "pale" | "tawny";
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Bake the pinned wolf on the build machine, never on the portable runtime path.
|
|
13
|
+
* @situation bake high and crowd wolf geometry before the ordinary asset cook
|
|
14
|
+
* @constraint exact pinned Node runtime, donor revision, options and generated bytes are validated; no worker is created
|
|
15
|
+
* @requires npm i @threenative/procedural-animals
|
|
16
|
+
* @example const bytes = await bakeWolf({ seed: 7, tier: "crowd" });
|
|
17
|
+
*/
|
|
18
|
+
declare function bakeWolf(options: IWolfBakeOptions): Promise<ArrayBuffer>;
|
|
19
|
+
/**
|
|
20
|
+
* Stage a validated source bake with a same-directory atomic rename, then use compileAssets.
|
|
21
|
+
* @situation publish a generated wolf without replacing the last valid bake on interruption
|
|
22
|
+
* @constraint validation and generation complete before the destination is touched; temporary files are removed on failure
|
|
23
|
+
* @requires npm i @threenative/procedural-animals
|
|
24
|
+
* @example await bakeWolfToFile({ seed: 7, tier: "crowd" }, "assets/wolf-crowd.animal");
|
|
25
|
+
*/
|
|
26
|
+
declare function bakeWolfToFile(options: IWolfBakeOptions, destination: string): Promise<string>;
|
|
27
|
+
/**
|
|
28
|
+
* Validate staged PANM sources inside the existing asset compiler; it owns the hashed manifest.
|
|
29
|
+
* @situation cook staged animal bakes through the ordinary asset pipeline
|
|
30
|
+
* @constraint only .animal inputs are interpreted; corruption fails the cook, without regeneration
|
|
31
|
+
* @requires npm i @threenative/procedural-animals
|
|
32
|
+
* @example await compileAssets({ passes: [animalBakePass()] });
|
|
33
|
+
*/
|
|
34
|
+
declare function animalBakePass(): {
|
|
35
|
+
name: string;
|
|
36
|
+
appliesTo: readonly ["other"];
|
|
37
|
+
cacheKey: string;
|
|
38
|
+
apply(input: Buffer, logicalPath: string): Buffer<ArrayBufferLike> | {
|
|
39
|
+
buffer: Buffer<ArrayBufferLike>;
|
|
40
|
+
entry: {
|
|
41
|
+
animal: {
|
|
42
|
+
species: "wolf";
|
|
43
|
+
seed: number;
|
|
44
|
+
tier: AnimalTier;
|
|
45
|
+
vertices: number;
|
|
46
|
+
bones: number;
|
|
47
|
+
sha256: string;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export { ANIMAL_BUILD_NODE, type IWolfBakeOptions, animalBakePass, bakeWolf, bakeWolfToFile };
|
package/dist/build.js
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { randomUUID, createHash } from 'crypto';
|
|
2
|
+
import { readFile, mkdir, writeFile, rename, rm } from 'fs/promises';
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { clearBuildCache, buildAnimalData, writeBake } from 'procedural-animals';
|
|
6
|
+
import wolf from 'procedural-animals/species/wolf';
|
|
7
|
+
|
|
8
|
+
// src/build.ts
|
|
9
|
+
|
|
10
|
+
// src/format.ts
|
|
11
|
+
var ANIMAL_DONOR_REVISION = "c95ae49346aa8e140a924376cec6cf0073d99512";
|
|
12
|
+
var ANIMAL_ADAPTER_VERSION = 1;
|
|
13
|
+
var ANIMAL_LIMITS = Object.freeze({
|
|
14
|
+
bytes: 64 * 1024 * 1024,
|
|
15
|
+
vertices: 25e4,
|
|
16
|
+
indices: 15e5,
|
|
17
|
+
bones: 256,
|
|
18
|
+
headerBytes: 1024 * 1024
|
|
19
|
+
});
|
|
20
|
+
var ANIMAL_ARRAYS = {
|
|
21
|
+
pos: [Float32Array, 3],
|
|
22
|
+
nrm: [Float32Array, 3],
|
|
23
|
+
index: [Uint32Array, 0],
|
|
24
|
+
skinIndex: [Uint16Array, 4],
|
|
25
|
+
skinWeight: [Float32Array, 4],
|
|
26
|
+
comb: [Float32Array, 3],
|
|
27
|
+
tint: [Float32Array, 4],
|
|
28
|
+
coat: [Float32Array, 4],
|
|
29
|
+
pat: [Float32Array, 4],
|
|
30
|
+
surf: [Float32Array, 4]
|
|
31
|
+
};
|
|
32
|
+
function bakeIntegrity(buffer) {
|
|
33
|
+
const view = new DataView(buffer);
|
|
34
|
+
const headerBytes = view.getUint32(8, true);
|
|
35
|
+
const start = Math.ceil((12 + headerBytes) / 8) * 8;
|
|
36
|
+
const bytes = new Uint8Array(buffer);
|
|
37
|
+
const header = JSON.parse(new TextDecoder().decode(bytes.subarray(12, 12 + headerBytes)));
|
|
38
|
+
const { integrity: _integrity, ...metadata } = header.threenative;
|
|
39
|
+
let hash = 2166136261;
|
|
40
|
+
for (const section of [
|
|
41
|
+
new TextEncoder().encode(JSON.stringify({ ...header, threenative: metadata })),
|
|
42
|
+
bytes.subarray(start)
|
|
43
|
+
])
|
|
44
|
+
for (const byte of section) hash = Math.imul(hash ^ byte, 16777619) >>> 0;
|
|
45
|
+
return hash.toString(16).padStart(8, "0");
|
|
46
|
+
}
|
|
47
|
+
var validatedBakes = /* @__PURE__ */ new WeakSet();
|
|
48
|
+
function animalError(code, detail) {
|
|
49
|
+
return new Error(`TN_ANIMAL_${code}: ${detail}`);
|
|
50
|
+
}
|
|
51
|
+
function record(value, name2) {
|
|
52
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
53
|
+
throw animalError("HEADER", `${name2} must be an object`);
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
function integer(value, min, max, name2) {
|
|
58
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < min || value > max) {
|
|
59
|
+
throw animalError("LIMIT", `${name2} must be an integer in [${min}, ${max}]`);
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
function name(value, label) {
|
|
64
|
+
if (typeof value !== "string" || !/^[a-zA-Z][a-zA-Z0-9_-]{0,127}$/u.test(value)) {
|
|
65
|
+
throw animalError("RIG", `${label} must be a joint or bone name`);
|
|
66
|
+
}
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
function joints(value, label) {
|
|
70
|
+
const entries = Object.entries(record(value, label));
|
|
71
|
+
integer(entries.length, 2, 4096, `${label} count`);
|
|
72
|
+
for (const [key, point] of entries) {
|
|
73
|
+
name(key, label);
|
|
74
|
+
if (!Array.isArray(point) || point.length !== 3 || !point.every(Number.isFinite)) {
|
|
75
|
+
throw animalError("RIG", `${label}.${key} must be a finite point`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
function validateHeader(header) {
|
|
81
|
+
if (header.version !== 1 || header.species !== "wolf" || header.quality !== "high" && header.quality !== "crowd") {
|
|
82
|
+
throw animalError("REVISION", "expected version 1, wolf, and high or crowd geometry");
|
|
83
|
+
}
|
|
84
|
+
integer(header.seed, 0, 4294967295, "seed");
|
|
85
|
+
integer(header.nV, 3, ANIMAL_LIMITS.vertices, "vertex count");
|
|
86
|
+
const metadata = record(header.threenative, "threenative");
|
|
87
|
+
if (metadata.donorRevision !== ANIMAL_DONOR_REVISION || metadata.adapterVersion !== ANIMAL_ADAPTER_VERSION) {
|
|
88
|
+
throw animalError("REVISION", "unknown donor revision or adapter version");
|
|
89
|
+
}
|
|
90
|
+
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)) {
|
|
91
|
+
throw animalError("HEADER", "missing pinned build runtime or SHA-256 cache key");
|
|
92
|
+
}
|
|
93
|
+
const params = record(header.params, "params");
|
|
94
|
+
if (typeof params.size !== "number" || !Number.isFinite(params.size) || params.size <= 0) {
|
|
95
|
+
throw animalError("RIG", "params.size must be finite and positive");
|
|
96
|
+
}
|
|
97
|
+
if (params.seed !== void 0 && params.seed !== header.seed)
|
|
98
|
+
throw animalError("OPTIONS", "individual seed contradicts bake seed");
|
|
99
|
+
const points = joints(header.joints, "joints");
|
|
100
|
+
joints(header.refJoints, "refJoints");
|
|
101
|
+
if (!Array.isArray(header.bones)) throw animalError("RIG", "bones must be an array");
|
|
102
|
+
integer(header.bones.length, 1, ANIMAL_LIMITS.bones, "bone count");
|
|
103
|
+
const parents = /* @__PURE__ */ new Map();
|
|
104
|
+
for (const value of header.bones) {
|
|
105
|
+
const bone = record(value, "bone");
|
|
106
|
+
const key = name(bone.name, "bone.name");
|
|
107
|
+
if (parents.has(key)) throw animalError("RIG", `duplicate bone ${key}`);
|
|
108
|
+
const parent = bone.parent === null ? null : name(bone.parent, `${key}.parent`);
|
|
109
|
+
parents.set(key, parent);
|
|
110
|
+
const headName = name(bone.headJ, `${key}.headJ`);
|
|
111
|
+
const tailName = name(bone.tailJ, `${key}.tailJ`);
|
|
112
|
+
const head = Object.hasOwn(points, headName) ? points[headName] : void 0;
|
|
113
|
+
const tail = Object.hasOwn(points, tailName) ? points[tailName] : void 0;
|
|
114
|
+
const length = head && tail ? Math.hypot(head[0] - tail[0], head[1] - tail[1], head[2] - tail[2]) : Number.NaN;
|
|
115
|
+
if (!Number.isFinite(length) || length < 1e-8) {
|
|
116
|
+
throw animalError("RIG", `${key} has missing joints or zero length`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const key of parents.keys()) {
|
|
120
|
+
const seen = /* @__PURE__ */ new Set();
|
|
121
|
+
let current = key;
|
|
122
|
+
while (current !== null) {
|
|
123
|
+
if (seen.has(current) || !parents.has(current))
|
|
124
|
+
throw animalError("RIG", `${key} has a cyclic or missing parent`);
|
|
125
|
+
seen.add(current);
|
|
126
|
+
current = parents.get(current) ?? null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function validateArrayData(arrays, vertices, boneCount) {
|
|
131
|
+
for (const key of Object.keys(ANIMAL_ARRAYS)) {
|
|
132
|
+
const array = arrays[key];
|
|
133
|
+
const [Constructor, width] = ANIMAL_ARRAYS[key];
|
|
134
|
+
if (!(array instanceof Constructor) || !array || (key === "index" ? array.length < 3 || array.length > ANIMAL_LIMITS.indices || array.length % 3 !== 0 : array.length !== vertices * width))
|
|
135
|
+
throw animalError("SECTIONS", `${key} changed after validation`);
|
|
136
|
+
for (const value of array) {
|
|
137
|
+
if (!Number.isFinite(value))
|
|
138
|
+
throw animalError("FINITE", `${key} contains a non-finite value`);
|
|
139
|
+
if (key === "index" && value >= vertices)
|
|
140
|
+
throw animalError("INDEX", `vertex index ${value} exceeds vertex count`);
|
|
141
|
+
if (key === "skinIndex" && value >= boneCount)
|
|
142
|
+
throw animalError("BONE_INDEX", `bone index ${value} exceeds bone count`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const weights = arrays.skinWeight;
|
|
146
|
+
for (let i = 0; i < weights.length; i += 4) {
|
|
147
|
+
const group = weights.subarray(i, i + 4);
|
|
148
|
+
if (group.some((weight) => weight < 0 || weight > 1) || Math.abs(group.reduce((a, b) => a + b, 0) - 1) > 1e-4) {
|
|
149
|
+
throw animalError("WEIGHTS", `vertex ${i / 4} has non-normalized skin weights`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function parseAnimalBake(buffer) {
|
|
154
|
+
if (!(buffer instanceof ArrayBuffer) || buffer.byteLength < 16 || buffer.byteLength > ANIMAL_LIMITS.bytes) {
|
|
155
|
+
throw animalError("LIMIT", `payload must contain 16..${ANIMAL_LIMITS.bytes} bytes`);
|
|
156
|
+
}
|
|
157
|
+
const bytes = new Uint8Array(buffer);
|
|
158
|
+
const view = new DataView(buffer);
|
|
159
|
+
if (view.getUint32(0, false) !== 1346457165) throw animalError("MAGIC", "expected PANM");
|
|
160
|
+
if (view.getUint32(4, true) !== 1) throw animalError("REVISION", "unknown PANM version");
|
|
161
|
+
const headerBytes = integer(
|
|
162
|
+
view.getUint32(8, true),
|
|
163
|
+
8,
|
|
164
|
+
ANIMAL_LIMITS.headerBytes,
|
|
165
|
+
"header bytes"
|
|
166
|
+
);
|
|
167
|
+
const start = Math.ceil((12 + headerBytes) / 8) * 8;
|
|
168
|
+
if (headerBytes % 8 !== 0 || start > buffer.byteLength)
|
|
169
|
+
throw animalError("SECTIONS", "truncated or unaligned header");
|
|
170
|
+
let header;
|
|
171
|
+
try {
|
|
172
|
+
header = record(
|
|
173
|
+
JSON.parse(
|
|
174
|
+
new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(12, 12 + headerBytes))
|
|
175
|
+
),
|
|
176
|
+
"header"
|
|
177
|
+
);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
throw animalError("HEADER", `invalid UTF-8/JSON header: ${String(error)}`);
|
|
180
|
+
}
|
|
181
|
+
const pending = [{ value: header, depth: 0 }];
|
|
182
|
+
let visited = 0;
|
|
183
|
+
while (pending.length > 0) {
|
|
184
|
+
if (++visited > 1e5)
|
|
185
|
+
throw animalError("LIMIT", "header exceeds the structural traversal limit");
|
|
186
|
+
const entry = pending.pop();
|
|
187
|
+
if (!entry) break;
|
|
188
|
+
const { value, depth } = entry;
|
|
189
|
+
if (depth > 64) throw animalError("LIMIT", "header nesting exceeds 64 levels");
|
|
190
|
+
if (typeof value === "number" && !Number.isFinite(value))
|
|
191
|
+
throw animalError("FINITE", "header contains a non-finite number");
|
|
192
|
+
if (value !== null && typeof value === "object")
|
|
193
|
+
for (const child of Object.values(value)) pending.push({ value: child, depth: depth + 1 });
|
|
194
|
+
}
|
|
195
|
+
validateHeader(header);
|
|
196
|
+
if (!Array.isArray(header.arrays) || header.arrays.length !== Object.keys(ANIMAL_ARRAYS).length) {
|
|
197
|
+
throw animalError("SECTIONS", "expected every supported array exactly once");
|
|
198
|
+
}
|
|
199
|
+
const arrays = {};
|
|
200
|
+
const ranges = [];
|
|
201
|
+
for (const value of header.arrays) {
|
|
202
|
+
const section = record(value, "section");
|
|
203
|
+
if (typeof section.key !== "string")
|
|
204
|
+
throw animalError("SECTIONS", "section key must be a string");
|
|
205
|
+
const key = section.key;
|
|
206
|
+
if (!Object.hasOwn(ANIMAL_ARRAYS, key) || arrays[key] !== void 0)
|
|
207
|
+
throw animalError("SECTIONS", `unknown or duplicate section ${String(key)}`);
|
|
208
|
+
const [Constructor, width] = ANIMAL_ARRAYS[key];
|
|
209
|
+
if (section.type !== Constructor.name)
|
|
210
|
+
throw animalError("SECTIONS", `${key} must contain ${Constructor.name}`);
|
|
211
|
+
const length = integer(
|
|
212
|
+
section.length,
|
|
213
|
+
1,
|
|
214
|
+
key === "index" ? ANIMAL_LIMITS.indices : ANIMAL_LIMITS.vertices * width,
|
|
215
|
+
`${key} length`
|
|
216
|
+
);
|
|
217
|
+
if (key === "index" && length % 3 !== 0 || key !== "index" && length !== Number(header.nV) * width) {
|
|
218
|
+
throw animalError("SECTIONS", `${key} length contradicts the vertex or triangle count`);
|
|
219
|
+
}
|
|
220
|
+
const offset = integer(section.offset, 0, ANIMAL_LIMITS.bytes, `${key} offset`);
|
|
221
|
+
const end2 = start + offset + Math.ceil(length * Constructor.BYTES_PER_ELEMENT / 8) * 8;
|
|
222
|
+
if (offset % 8 !== 0 || end2 > buffer.byteLength)
|
|
223
|
+
throw animalError("SECTIONS", `${key} is unaligned or truncated`);
|
|
224
|
+
ranges.push([start + offset, end2]);
|
|
225
|
+
arrays[key] = new Constructor(buffer, start + offset, length);
|
|
226
|
+
}
|
|
227
|
+
let end = start;
|
|
228
|
+
for (const range of ranges.sort((a, b) => a[0] - b[0])) {
|
|
229
|
+
if (range[0] !== end) throw animalError("SECTIONS", "sections overlap or contain gaps");
|
|
230
|
+
end = range[1];
|
|
231
|
+
}
|
|
232
|
+
if (end !== buffer.byteLength)
|
|
233
|
+
throw animalError("SECTIONS", "byte length contradicts section extents");
|
|
234
|
+
validateArrayData(arrays, Number(header.nV), header.bones.length);
|
|
235
|
+
if (bakeIntegrity(buffer) !== header.threenative.integrity)
|
|
236
|
+
throw animalError("INTEGRITY", "baked header or array bytes changed");
|
|
237
|
+
for (const key of Object.keys(arrays)) arrays[key] = arrays[key]?.slice();
|
|
238
|
+
const { arrays: _sections, ...data } = header;
|
|
239
|
+
const objects = [data];
|
|
240
|
+
while (objects.length > 0) {
|
|
241
|
+
const object = objects.pop();
|
|
242
|
+
if (!object) break;
|
|
243
|
+
for (const value of Object.values(object))
|
|
244
|
+
if (value !== null && typeof value === "object") objects.push(value);
|
|
245
|
+
Object.freeze(object);
|
|
246
|
+
}
|
|
247
|
+
const bake = Object.freeze({ ...data, ...arrays });
|
|
248
|
+
validatedBakes.add(bake);
|
|
249
|
+
return bake;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// src/build.ts
|
|
253
|
+
var ANIMAL_BUILD_NODE = "20.19.6";
|
|
254
|
+
var donorRoot = path.dirname(
|
|
255
|
+
createRequire(import.meta.url).resolve("procedural-animals/package.json")
|
|
256
|
+
);
|
|
257
|
+
var digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
258
|
+
function normalized(options) {
|
|
259
|
+
if (!process.versions.node.startsWith(`${ANIMAL_BUILD_NODE.split(".")[0]}.`))
|
|
260
|
+
throw animalError(
|
|
261
|
+
"BUILD_RUNTIME",
|
|
262
|
+
`baking requires Node ${ANIMAL_BUILD_NODE.split(".")[0]}.x (reference ${ANIMAL_BUILD_NODE}); found ${process.versions.node}`
|
|
263
|
+
);
|
|
264
|
+
if (!options || typeof options !== "object" || Array.isArray(options))
|
|
265
|
+
throw animalError("OPTIONS", "wolf options must be an object");
|
|
266
|
+
for (const key of Object.keys(options))
|
|
267
|
+
if (!["seed", "tier", "sex", "age", "variant"].includes(key))
|
|
268
|
+
throw animalError("OPTIONS", `unknown wolf option ${key}`);
|
|
269
|
+
if (!Number.isSafeInteger(options.seed) || options.seed < 0 || options.seed > 4294967295)
|
|
270
|
+
throw animalError("OPTIONS", "seed must be an unsigned 32-bit integer");
|
|
271
|
+
if (options.tier !== "high" && options.tier !== "crowd")
|
|
272
|
+
throw animalError("OPTIONS", "tier must be high or crowd");
|
|
273
|
+
for (const [key, allowed] of [
|
|
274
|
+
["sex", ["male", "female"]],
|
|
275
|
+
["age", ["adult", "juvenile"]],
|
|
276
|
+
["variant", ["grey", "black", "pale", "tawny"]]
|
|
277
|
+
]) {
|
|
278
|
+
const value = options[key];
|
|
279
|
+
if (value !== void 0 && !allowed.includes(value))
|
|
280
|
+
throw animalError("OPTIONS", `unsupported ${key}`);
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
seed: options.seed,
|
|
284
|
+
tier: options.tier,
|
|
285
|
+
sex: options.sex,
|
|
286
|
+
age: options.age,
|
|
287
|
+
variant: options.variant
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
async function bakeWolf(options) {
|
|
291
|
+
const o = normalized(options);
|
|
292
|
+
const definitionHash = digest(await readFile(path.join(donorRoot, "src/species/wolf/index.js")));
|
|
293
|
+
const cacheKey = digest(
|
|
294
|
+
JSON.stringify({
|
|
295
|
+
donor: ANIMAL_DONOR_REVISION,
|
|
296
|
+
adapter: ANIMAL_ADAPTER_VERSION,
|
|
297
|
+
runtime: `node-v${ANIMAL_BUILD_NODE}`,
|
|
298
|
+
definitionHash,
|
|
299
|
+
species: "wolf",
|
|
300
|
+
options: o
|
|
301
|
+
})
|
|
302
|
+
);
|
|
303
|
+
clearBuildCache();
|
|
304
|
+
const generated = await buildAnimalData(wolf, {
|
|
305
|
+
seed: o.seed,
|
|
306
|
+
quality: o.tier,
|
|
307
|
+
sex: o.sex,
|
|
308
|
+
age: o.age,
|
|
309
|
+
variant: o.variant,
|
|
310
|
+
worker: false
|
|
311
|
+
});
|
|
312
|
+
const stats = { ...generated.stats };
|
|
313
|
+
for (const key of ["weightsMs", "coatMs", "buildMs"]) delete stats[key];
|
|
314
|
+
const metadata = {
|
|
315
|
+
donorRevision: ANIMAL_DONOR_REVISION,
|
|
316
|
+
adapterVersion: ANIMAL_ADAPTER_VERSION,
|
|
317
|
+
buildRuntime: `node-v${ANIMAL_BUILD_NODE}`,
|
|
318
|
+
cacheKey
|
|
319
|
+
};
|
|
320
|
+
const data = { ...generated, stats, threenative: metadata };
|
|
321
|
+
const integrity = bakeIntegrity(writeBake(data));
|
|
322
|
+
const bytes = writeBake({ ...data, threenative: { ...metadata, integrity } });
|
|
323
|
+
const bake = parseAnimalBake(bytes);
|
|
324
|
+
if (bake.quality === "crowd" && (bake.nV > 8e3 || bake.bones.length > 128))
|
|
325
|
+
throw animalError("CROWD_LIMIT", `wolf has ${bake.nV} vertices/${bake.bones.length} bones`);
|
|
326
|
+
return bytes;
|
|
327
|
+
}
|
|
328
|
+
async function bakeWolfToFile(options, destination) {
|
|
329
|
+
if (!destination.endsWith(".animal"))
|
|
330
|
+
throw animalError("OPTIONS", "destination must end in .animal");
|
|
331
|
+
const bytes = await bakeWolf(options);
|
|
332
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
333
|
+
const temporary = `${destination}.${process.pid}.${randomUUID()}.tmp`;
|
|
334
|
+
try {
|
|
335
|
+
await writeFile(temporary, new Uint8Array(bytes), { flag: "wx" });
|
|
336
|
+
await rename(temporary, destination);
|
|
337
|
+
} finally {
|
|
338
|
+
await rm(temporary, { force: true });
|
|
339
|
+
}
|
|
340
|
+
return digest(new Uint8Array(bytes));
|
|
341
|
+
}
|
|
342
|
+
function animalBakePass() {
|
|
343
|
+
return {
|
|
344
|
+
name: "animal-bake",
|
|
345
|
+
appliesTo: ["other"],
|
|
346
|
+
cacheKey: `${ANIMAL_DONOR_REVISION}:${ANIMAL_ADAPTER_VERSION}:node-v${ANIMAL_BUILD_NODE}`,
|
|
347
|
+
apply(input, logicalPath) {
|
|
348
|
+
if (!logicalPath.endsWith(".animal")) return input;
|
|
349
|
+
const bake = parseAnimalBake(Uint8Array.from(input).buffer);
|
|
350
|
+
return {
|
|
351
|
+
buffer: input,
|
|
352
|
+
entry: {
|
|
353
|
+
animal: {
|
|
354
|
+
species: bake.species,
|
|
355
|
+
seed: bake.seed,
|
|
356
|
+
tier: bake.quality,
|
|
357
|
+
vertices: bake.nV,
|
|
358
|
+
bones: bake.bones.length,
|
|
359
|
+
sha256: digest(input)
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export { ANIMAL_BUILD_NODE, animalBakePass, bakeWolf, bakeWolfToFile };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
declare const ANIMAL_DONOR_REVISION = "c95ae49346aa8e140a924376cec6cf0073d99512";
|
|
2
|
+
declare const ANIMAL_ADAPTER_VERSION = 1;
|
|
3
|
+
type AnimalTier = "high" | "crowd";
|
|
4
|
+
type AnimalPoint = readonly [number, number, number];
|
|
5
|
+
interface IAnimalBone {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly parent: string | null;
|
|
8
|
+
readonly headJ: string;
|
|
9
|
+
readonly tailJ: string;
|
|
10
|
+
readonly region?: string;
|
|
11
|
+
readonly group?: string;
|
|
12
|
+
}
|
|
13
|
+
interface IAnimalBake {
|
|
14
|
+
readonly version: 1;
|
|
15
|
+
readonly species: "wolf";
|
|
16
|
+
readonly seed: number;
|
|
17
|
+
readonly quality: AnimalTier;
|
|
18
|
+
readonly nV: number;
|
|
19
|
+
readonly bones: readonly IAnimalBone[];
|
|
20
|
+
readonly joints: Readonly<Record<string, AnimalPoint>>;
|
|
21
|
+
readonly refJoints: Readonly<Record<string, AnimalPoint>>;
|
|
22
|
+
readonly params: Readonly<Record<string, unknown>>;
|
|
23
|
+
readonly threenative: {
|
|
24
|
+
readonly donorRevision: typeof ANIMAL_DONOR_REVISION;
|
|
25
|
+
readonly adapterVersion: typeof ANIMAL_ADAPTER_VERSION;
|
|
26
|
+
readonly buildRuntime: string;
|
|
27
|
+
readonly cacheKey: string;
|
|
28
|
+
readonly integrity: string;
|
|
29
|
+
};
|
|
30
|
+
readonly pos: Float32Array;
|
|
31
|
+
readonly nrm: Float32Array;
|
|
32
|
+
readonly index: Uint32Array;
|
|
33
|
+
readonly skinIndex: Uint16Array;
|
|
34
|
+
readonly skinWeight: Float32Array;
|
|
35
|
+
readonly comb: Float32Array;
|
|
36
|
+
readonly tint: Float32Array;
|
|
37
|
+
readonly coat: Float32Array;
|
|
38
|
+
readonly pat: Float32Array;
|
|
39
|
+
readonly surf: Float32Array;
|
|
40
|
+
}
|
|
41
|
+
/** Validate all PANM bytes before a caller creates any Three.js/GPU object. */
|
|
42
|
+
declare function parseAnimalBake(buffer: ArrayBuffer): IAnimalBake;
|
|
43
|
+
|
|
44
|
+
export { type AnimalTier as A, type IAnimalBake as I, type IAnimalBone as a, parseAnimalBake as p };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { I as IAnimalBake } from './format-BRFFv-ws.js';
|
|
2
|
+
export { A as AnimalTier, a as IAnimalBone, p as parseAnimalBake } from './format-BRFFv-ws.js';
|
|
3
|
+
import * as three from 'three';
|
|
4
|
+
import { BufferGeometry, Bone, Matrix4, DataTexture, Vector3, Material, Object3D, Group, Mesh } from 'three';
|
|
5
|
+
|
|
6
|
+
interface IAnimalAssetResolver {
|
|
7
|
+
resolve(path: string): Promise<readonly string[]>;
|
|
8
|
+
}
|
|
9
|
+
/** Resolve a cooked logical asset with ctx.assets and load only a validated bake. */
|
|
10
|
+
declare function loadAnimalBake(assets: IAnimalAssetResolver, logicalPath: string, options?: {
|
|
11
|
+
readonly signal?: AbortSignal;
|
|
12
|
+
}): Promise<IAnimalBake>;
|
|
13
|
+
/** Create geometry only after the complete binary has passed validation. */
|
|
14
|
+
declare function createAnimalGeometry(buffer: ArrayBuffer): {
|
|
15
|
+
readonly bake: IAnimalBake;
|
|
16
|
+
readonly geometry: BufferGeometry;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
interface IAnimalSkeleton {
|
|
20
|
+
readonly bones: readonly Bone[];
|
|
21
|
+
readonly bind: readonly Matrix4[];
|
|
22
|
+
readonly inverses: readonly Matrix4[];
|
|
23
|
+
}
|
|
24
|
+
interface IAnimalPose {
|
|
25
|
+
readonly skeleton: IAnimalSkeleton;
|
|
26
|
+
readonly data: Float32Array;
|
|
27
|
+
readonly texture: DataTexture;
|
|
28
|
+
update(worldToLocal: Matrix4): void;
|
|
29
|
+
dispose(): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type AnimalActionResult = "done" | "interrupted" | "stopped" | "refused" | "disposed";
|
|
33
|
+
interface IAnimalMotion {
|
|
34
|
+
readonly input: {
|
|
35
|
+
speed: number;
|
|
36
|
+
target: unknown;
|
|
37
|
+
heading: number;
|
|
38
|
+
follow: {
|
|
39
|
+
position: Vector3;
|
|
40
|
+
velocity: Vector3;
|
|
41
|
+
heading: number;
|
|
42
|
+
} | null;
|
|
43
|
+
};
|
|
44
|
+
update(dt: number): void;
|
|
45
|
+
reset(position: Vector3, heading: number): void;
|
|
46
|
+
play(name: string): Promise<AnimalActionResult>;
|
|
47
|
+
dispose(): void;
|
|
48
|
+
}
|
|
49
|
+
interface IAnimalMotionContext {
|
|
50
|
+
readonly data: IAnimalBake;
|
|
51
|
+
readonly skeleton: IAnimalSkeleton;
|
|
52
|
+
readonly ground: (x: number, z: number) => number;
|
|
53
|
+
readonly position: Vector3;
|
|
54
|
+
readonly heading: number;
|
|
55
|
+
readonly emit: (name: string, data: unknown) => void;
|
|
56
|
+
}
|
|
57
|
+
interface IAnimalAcceptedState {
|
|
58
|
+
readonly position: Readonly<Pick<Vector3, "x" | "y" | "z">>;
|
|
59
|
+
readonly velocity: Readonly<Pick<Vector3, "x" | "y" | "z">>;
|
|
60
|
+
readonly heading: number;
|
|
61
|
+
}
|
|
62
|
+
interface IAnimalActorOptions {
|
|
63
|
+
readonly motion: (context: IAnimalMotionContext) => IAnimalMotion;
|
|
64
|
+
readonly material: (pose: IAnimalPose) => Material;
|
|
65
|
+
readonly ground: (x: number, z: number) => number;
|
|
66
|
+
readonly parent?: Object3D;
|
|
67
|
+
/** Game's rig origin relative to the physics origin (capsules are centred). Reported verbatim. */
|
|
68
|
+
readonly visualOriginOffset?: Readonly<Pick<Vector3, "x" | "y" | "z">>;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Own one actor's pose/lifetime, accepting the completed physics state as its only root writer.
|
|
72
|
+
* @situation follow a baked animal from accepted fixed-step Rapier state
|
|
73
|
+
* @constraint identity parent required; material and motion are editable game source; no move/Mixer/composer writer exists
|
|
74
|
+
* @requires npm i @threenative/procedural-animals
|
|
75
|
+
* @example const wolf = createAnimalActor(bake, { motion, material, ground });
|
|
76
|
+
*/
|
|
77
|
+
declare function createAnimalActor(bake: IAnimalBake, options: IAnimalActorOptions): {
|
|
78
|
+
object: Group<three.Object3DEventMap>;
|
|
79
|
+
mesh: Mesh<three.BufferGeometry<three.NormalBufferAttributes, three.BufferGeometryEventMap>, Material<three.MaterialEventMap>, three.Object3DEventMap>;
|
|
80
|
+
pose: IAnimalPose;
|
|
81
|
+
bounds: {
|
|
82
|
+
sphere: three.Sphere;
|
|
83
|
+
box: three.Box3;
|
|
84
|
+
update(data: Float32Array): void;
|
|
85
|
+
};
|
|
86
|
+
/** Measurement survives the explicit origin override. */
|
|
87
|
+
visualOriginOffset: Vector3;
|
|
88
|
+
paused: boolean;
|
|
89
|
+
follow(state: IAnimalAcceptedState, dt: number): void;
|
|
90
|
+
teleport(state: IAnimalAcceptedState): void;
|
|
91
|
+
play(name: "stand" | "sit" | "lie"): Promise<AnimalActionResult>;
|
|
92
|
+
dispose: () => void;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export { type AnimalActionResult, type IAnimalAcceptedState, type IAnimalActorOptions, type IAnimalAssetResolver, IAnimalBake, type IAnimalMotion, type IAnimalMotionContext, type IAnimalPose, type IAnimalSkeleton, createAnimalActor, createAnimalGeometry, loadAnimalBake };
|