@umicat/three-sdk 0.19.0 → 0.20.0
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/README.md +5 -0
- package/dist/SceneLoader3D.d.ts +3 -0
- package/dist/SceneLoader3D.js +51 -1
- package/dist/scene3d.d.ts +23 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,6 +73,11 @@ Each rule is a decision:
|
|
|
73
73
|
Guessing that every model calls its walk cycle `Walk` fails silently; an
|
|
74
74
|
independent review's cross-rig retarget returned zero matched bones and zero
|
|
75
75
|
tracks, which is the same class of failure, quieter.
|
|
76
|
+
- **What a character wears is authored on the entity**
|
|
77
|
+
(`attachments: [{ modelAssetId: 'sword' }]`). Because the character already
|
|
78
|
+
says where its sockets are and the prop already says which one it belongs in,
|
|
79
|
+
naming the prop is the whole declaration — a knight in armour is data, not
|
|
80
|
+
equip code written once per game.
|
|
76
81
|
- **A prop declares which socket it belongs in** (`socket: 'hand-right'` on the
|
|
77
82
|
sword, `sockets: { 'hand-right': {...} }` on the character). Both halves are
|
|
78
83
|
properties of the MODEL, not of any game — a cap goes on a head in every game
|
package/dist/SceneLoader3D.d.ts
CHANGED
|
@@ -30,6 +30,9 @@ export interface LoadedScene3D {
|
|
|
30
30
|
/** Model asset id → every clip that model shipped with. A game switching
|
|
31
31
|
* between idle and walk needs the clips, not just the one playing. */
|
|
32
32
|
clips: Map<string, THREE.AnimationClip[]>;
|
|
33
|
+
/** Entity id → its props, keyed by the attachment's `id` or, failing that,
|
|
34
|
+
* the socket it went into. How a game reaches the sword it authored. */
|
|
35
|
+
attachments: Map<string, Map<string, THREE.Object3D>>;
|
|
33
36
|
/** Entity id → rigid body, when physics was supplied. */
|
|
34
37
|
bodies: Map<string, any>;
|
|
35
38
|
world?: any;
|
package/dist/SceneLoader3D.js
CHANGED
|
@@ -3,6 +3,7 @@ import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
|
|
3
3
|
import { clone as cloneRigged } from 'three/examples/jsm/utils/SkeletonUtils.js';
|
|
4
4
|
import { validateScene, IDENTITY_QUAT, } from './scene3d.js';
|
|
5
5
|
import { applySkin } from './Skin.js';
|
|
6
|
+
import { attachToSocket } from './Sockets.js';
|
|
6
7
|
function toVec(v, d = 0) {
|
|
7
8
|
return new THREE.Vector3(v?.x ?? d, v?.y ?? d, v?.z ?? d);
|
|
8
9
|
}
|
|
@@ -115,6 +116,20 @@ export async function loadScene3D(scene3d, manifest, opts = {}) {
|
|
|
115
116
|
// a model must not download it twice.
|
|
116
117
|
const models = new Map((manifest.models ?? []).map((m) => [m.id, m]));
|
|
117
118
|
const needed = new Set(scene3d.entities.map((e) => e.modelAssetId).filter(Boolean));
|
|
119
|
+
// Props are referenced only from `attachments`, so a set built from
|
|
120
|
+
// `modelAssetId` alone leaves every sword unfetched and every attachment
|
|
121
|
+
// silently empty-handed.
|
|
122
|
+
for (const e of scene3d.entities) {
|
|
123
|
+
for (const a of e.attachments ?? []) {
|
|
124
|
+
// Checked HERE rather than at attach time so the message can name the
|
|
125
|
+
// wearer: the generic prefetch error fires first and only knows the id.
|
|
126
|
+
if (!models.has(a.modelAssetId)) {
|
|
127
|
+
throw new Error(`entity '${e.id}' attaches model '${a.modelAssetId}', which the manifest does not define. ` +
|
|
128
|
+
`It has: ${[...models.keys()].join(', ') || '(none)'}`);
|
|
129
|
+
}
|
|
130
|
+
needed.add(a.modelAssetId);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
118
133
|
const loaded = new Map();
|
|
119
134
|
if (needed.size) {
|
|
120
135
|
const loader = new GLTFLoader();
|
|
@@ -136,6 +151,7 @@ export async function loadScene3D(scene3d, manifest, opts = {}) {
|
|
|
136
151
|
// a caller that awaits `loadScene3D` gets a scene that is finished — not one
|
|
137
152
|
// whose cast pops from placeholder to costume a frame or two later.
|
|
138
153
|
const skinJobs = [];
|
|
154
|
+
const attachments = new Map();
|
|
139
155
|
for (const e of scene3d.entities) {
|
|
140
156
|
let obj;
|
|
141
157
|
if (e.modelAssetId) {
|
|
@@ -209,8 +225,42 @@ export async function loadScene3D(scene3d, manifest, opts = {}) {
|
|
|
209
225
|
}
|
|
210
226
|
}));
|
|
211
227
|
}
|
|
228
|
+
for (const a of e.attachments ?? []) {
|
|
229
|
+
const prop = models.get(a.modelAssetId); // prefetch already proved it exists
|
|
230
|
+
// The prop knows where it belongs; the scene only has to say so when
|
|
231
|
+
// it wants it somewhere else.
|
|
232
|
+
const socketName = a.socket ?? prop.socket;
|
|
233
|
+
if (!socketName) {
|
|
234
|
+
throw new Error(`entity '${e.id}' attaches '${a.modelAssetId}' with no socket, and that model ` +
|
|
235
|
+
`declares none of its own — give the attachment a 'socket', or the model one`);
|
|
236
|
+
}
|
|
237
|
+
const socket = asset.sockets?.[socketName];
|
|
238
|
+
if (!socket) {
|
|
239
|
+
throw new Error(`entity '${e.id}' wants socket '${socketName}' for '${a.modelAssetId}', but model ` +
|
|
240
|
+
`'${e.modelAssetId}' has: ${Object.keys(asset.sockets ?? {}).join(', ') || '(none)'}`);
|
|
241
|
+
}
|
|
242
|
+
const propGltf = loaded.get(a.modelAssetId);
|
|
243
|
+
// Always a clone: one prop asset can be worn by a whole squad, and the
|
|
244
|
+
// first wearer must not end up holding everyone's sword.
|
|
245
|
+
const propObj = isRigged(propGltf.scene)
|
|
246
|
+
? cloneRigged(propGltf.scene) : propGltf.scene.clone(true);
|
|
247
|
+
if (prop.importScale !== undefined)
|
|
248
|
+
propObj.scale.setScalar(prop.importScale);
|
|
249
|
+
propObj.userData.modelAssetId = a.modelAssetId;
|
|
250
|
+
propObj.name = a.id ?? a.modelAssetId;
|
|
251
|
+
attachToSocket(obj, socket, propObj);
|
|
252
|
+
let mine = attachments.get(e.id);
|
|
253
|
+
if (!mine) {
|
|
254
|
+
mine = new Map();
|
|
255
|
+
attachments.set(e.id, mine);
|
|
256
|
+
}
|
|
257
|
+
mine.set(a.id ?? socketName, propObj);
|
|
258
|
+
}
|
|
212
259
|
}
|
|
213
260
|
else {
|
|
261
|
+
if (e.attachments?.length) {
|
|
262
|
+
throw new Error(`entity '${e.id}' has attachments but no model to hang them on`);
|
|
263
|
+
}
|
|
214
264
|
obj = makePrimitive(e);
|
|
215
265
|
}
|
|
216
266
|
obj.name = e.name ?? e.id;
|
|
@@ -345,7 +395,7 @@ export async function loadScene3D(scene3d, manifest, opts = {}) {
|
|
|
345
395
|
? entities.get(scene3d.camera.target) : undefined;
|
|
346
396
|
const want = new THREE.Vector3();
|
|
347
397
|
return {
|
|
348
|
-
scene, camera, entities, mixers, mixerFor, clips, bodies, world,
|
|
398
|
+
scene, camera, entities, mixers, mixerFor, clips, attachments, bodies, world,
|
|
349
399
|
get cameraYaw() { return camYaw; },
|
|
350
400
|
get cameraPitch() { return camPitch; },
|
|
351
401
|
orbit(dYaw, dPitch) {
|
package/dist/scene3d.d.ts
CHANGED
|
@@ -59,6 +59,27 @@ export interface Collider3D {
|
|
|
59
59
|
/** Offset from the entity's own origin. */
|
|
60
60
|
offset?: Vec3;
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Something this entity wears or carries.
|
|
64
|
+
*
|
|
65
|
+
* The pairing that makes this short is already in the manifest from both ends
|
|
66
|
+
* — the character says where its sockets are, the prop says which one it
|
|
67
|
+
* belongs in — so `{ modelAssetId: 'sword' }` is usually the whole thing. A
|
|
68
|
+
* knight in armour becomes authored data rather than equip code written per
|
|
69
|
+
* game, which is the same reason `skin` and `animation.play` are data.
|
|
70
|
+
*/
|
|
71
|
+
export interface Attachment3D {
|
|
72
|
+
/** Manifest id of the prop. */
|
|
73
|
+
modelAssetId: string;
|
|
74
|
+
/**
|
|
75
|
+
* Which of the wearer's sockets. Omit to use the prop's own declared
|
|
76
|
+
* `socket` — override only to put something where it does not normally go
|
|
77
|
+
* (a shield strapped to the back, a torch in the off hand).
|
|
78
|
+
*/
|
|
79
|
+
socket?: string;
|
|
80
|
+
/** Optional name, so game code can find this one again to swap or drop it. */
|
|
81
|
+
id?: string;
|
|
82
|
+
}
|
|
62
83
|
export interface Entity3D {
|
|
63
84
|
/** Stable, authored, unique within the scene. */
|
|
64
85
|
id: string;
|
|
@@ -83,6 +104,8 @@ export interface Entity3D {
|
|
|
83
104
|
/** Which skin to wear, by name from the model's `skins` — see `SkinMap`.
|
|
84
105
|
* Omit to keep whatever texture the glb itself was built with. */
|
|
85
106
|
skin?: string;
|
|
107
|
+
/** Props worn or carried on this entity's sockets — see `Attachment3D`. */
|
|
108
|
+
attachments?: Attachment3D[];
|
|
86
109
|
/** Free-form, read by game code. The 2D SDK's `properties` equivalent. */
|
|
87
110
|
properties?: Record<string, unknown>;
|
|
88
111
|
visible?: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umicat/three-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Three.js runtime for Umicat games: the scene3d design format, its loader with physics, a kinematic character controller, and the Umicat platform via @umicat/platform-sdk.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|