murow 0.0.71 → 0.0.73
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 +7 -5
- package/dist/cjs/core/driver/drivers/raf.js +1 -1
- package/dist/cjs/core/sparse-batcher/sparse-batcher.js +1 -1
- package/dist/cjs/renderer/{base-2d-renderer.js → base/renderer-2d.js} +1 -1
- package/dist/cjs/renderer/base/renderer-3d.js +1 -0
- package/dist/cjs/renderer/gltf/helpers.js +1 -0
- package/dist/cjs/renderer/gltf/parser.js +1 -0
- package/dist/cjs/renderer/gltf/skeletal-animation.js +1 -0
- package/dist/cjs/renderer/gltf/skin-parser.js +1 -0
- package/dist/cjs/renderer/index.js +1 -1
- package/dist/cjs/renderer/math.js +1 -0
- package/dist/cjs/renderer/prefab-bucket/concrete.js +1 -0
- package/dist/cjs/renderer/prefab-bucket/index.js +1 -0
- package/dist/cjs/renderer/prefab-bucket/parsers.js +1 -0
- package/dist/cjs/renderer/prefab-bucket/specs.js +1 -0
- package/dist/cjs/renderer/spritesheet/helpers.js +1 -0
- package/dist/cjs/renderer/spritesheet/parser.js +1 -0
- package/dist/cjs/renderer/types.js +1 -1
- package/dist/esm/core/driver/drivers/raf.js +1 -1
- package/dist/esm/core/sparse-batcher/sparse-batcher.js +1 -1
- package/dist/esm/renderer/base/renderer-2d.js +1 -0
- package/dist/esm/renderer/base/renderer-3d.js +1 -0
- package/dist/esm/renderer/gltf/helpers.js +1 -0
- package/dist/esm/renderer/gltf/parser.js +1 -0
- package/dist/esm/renderer/gltf/skeletal-animation.js +1 -0
- package/dist/esm/renderer/gltf/skin-parser.js +1 -0
- package/dist/esm/renderer/index.js +1 -1
- package/dist/esm/renderer/math.js +1 -0
- package/dist/esm/renderer/prefab-bucket/concrete.js +1 -0
- package/dist/esm/renderer/prefab-bucket/index.js +1 -0
- package/dist/esm/renderer/prefab-bucket/parsers.js +1 -0
- package/dist/esm/renderer/prefab-bucket/specs.js +0 -0
- package/dist/esm/renderer/spritesheet/helpers.js +1 -0
- package/dist/esm/renderer/spritesheet/parser.js +1 -0
- package/dist/types/core/driver/drivers/raf.d.ts +13 -2
- package/dist/types/renderer/{base-2d-renderer.d.ts → base/renderer-2d.d.ts} +2 -2
- package/dist/types/renderer/{base-3d-renderer.d.ts → base/renderer-3d.d.ts} +3 -3
- package/dist/types/renderer/{base-renderer.d.ts → base/renderer.d.ts} +1 -1
- package/dist/types/renderer/gltf/helpers.d.ts +43 -0
- package/dist/types/renderer/gltf/parser.d.ts +49 -0
- package/dist/{webgpu/types/3d → types/renderer/gltf}/skeletal-animation.d.ts +8 -2
- package/dist/{webgpu/types/3d/gltf-skin-parser.d.ts → types/renderer/gltf/skin-parser.d.ts} +11 -5
- package/dist/types/renderer/index.d.ts +14 -3
- package/dist/types/renderer/prefab-bucket/concrete.d.ts +95 -0
- package/dist/types/renderer/prefab-bucket/index.d.ts +123 -0
- package/dist/types/renderer/prefab-bucket/parsers.d.ts +8 -0
- package/dist/types/renderer/prefab-bucket/specs.d.ts +209 -0
- package/dist/types/renderer/spritesheet/helpers.d.ts +38 -0
- package/dist/types/renderer/spritesheet/parser.d.ts +21 -0
- package/dist/types/renderer/types.d.ts +19 -7
- package/dist/webgpu/cjs/index.js +1021 -1188
- package/dist/webgpu/esm/index.js +1016 -1174
- package/dist/webgpu/types/2d/renderer.d.ts +34 -3
- package/dist/webgpu/types/2d/sprite-accessor.d.ts +1 -1
- package/dist/webgpu/types/3d/clip-resync-coordinator.d.ts +20 -0
- package/dist/webgpu/types/3d/renderer.d.ts +133 -15
- package/dist/webgpu/types/3d/skeletal-animation-compute/index.d.ts +1 -1
- package/dist/webgpu/types/3d/skeletal-animation-compute/kernel.d.ts +19 -2
- package/dist/webgpu/types/3d/skeletal-animation-compute/packer.d.ts +1 -1
- package/dist/webgpu/types/camera/camera-2d.d.ts +1 -1
- package/dist/webgpu/types/camera/camera-3d.d.ts +1 -1
- package/dist/webgpu/types/index.d.ts +15 -12
- package/dist/webgpu/types/particle/emitter.d.ts +1 -1
- package/dist/webgpu/types/spritesheet/spritesheet.d.ts +5 -34
- package/package.json +1 -1
- package/dist/cjs/renderer/base-3d-renderer.js +0 -1
- package/dist/esm/renderer/base-2d-renderer.js +0 -1
- package/dist/esm/renderer/base-3d-renderer.js +0 -1
- /package/dist/cjs/renderer/{base-renderer.js → base/renderer.js} +0 -0
- /package/dist/esm/renderer/{base-renderer.js → base/renderer.js} +0 -0
- /package/dist/{webgpu/types/core → types/renderer}/math.d.ts +0 -0
package/dist/webgpu/cjs/index.js
CHANGED
|
@@ -95,7 +95,6 @@ __export(src_exports, {
|
|
|
95
95
|
STATIC_OFFSET_UV_MAX_Y: () => STATIC_OFFSET_UV_MAX_Y,
|
|
96
96
|
STATIC_OFFSET_UV_MIN_X: () => STATIC_OFFSET_UV_MIN_X,
|
|
97
97
|
STATIC_OFFSET_UV_MIN_Y: () => STATIC_OFFSET_UV_MIN_Y,
|
|
98
|
-
SkeletalAnimation: () => SkeletalAnimation,
|
|
99
98
|
SkinnedStaticMesh: () => SkinnedStaticMesh,
|
|
100
99
|
SpriteAccessor: () => SpriteAccessor,
|
|
101
100
|
SpriteUniforms: () => SpriteUniforms,
|
|
@@ -105,14 +104,11 @@ __export(src_exports, {
|
|
|
105
104
|
StaticSprite: () => StaticSprite,
|
|
106
105
|
WebGPU2DRenderer: () => WebGPU2DRenderer,
|
|
107
106
|
WebGPU3DRenderer: () => WebGPU3DRenderer,
|
|
108
|
-
computeGridUVs: () => computeGridUVs,
|
|
109
|
-
computeTexturePackerUVs: () => computeTexturePackerUVs,
|
|
110
107
|
createGeometryDataLayout: () => createGeometryDataLayout,
|
|
111
108
|
createTextureFromBitmap: () => createTextureFromBitmap,
|
|
112
109
|
d: () => d,
|
|
113
110
|
getFieldFloats: () => getFieldFloats,
|
|
114
111
|
inverseLerp: () => inverseLerp,
|
|
115
|
-
loadImage: () => loadImage,
|
|
116
112
|
remap: () => remap,
|
|
117
113
|
resolveBuiltInGeometry: () => resolveBuiltInGeometry,
|
|
118
114
|
rotate2d: () => rotate2d,
|
|
@@ -133,7 +129,7 @@ var std = { ..._std };
|
|
|
133
129
|
var import_typegpu6 = __toESM(require("typegpu"), 1);
|
|
134
130
|
var d4 = __toESM(require("typegpu/data"), 1);
|
|
135
131
|
var import_free_list2 = require("murow/core/free-list");
|
|
136
|
-
var
|
|
132
|
+
var import_renderer2 = require("murow/renderer");
|
|
137
133
|
|
|
138
134
|
// src/core/constants.ts
|
|
139
135
|
var DYNAMIC_OFFSET_PREV_X = 0;
|
|
@@ -652,6 +648,7 @@ function createSpriteFragment(_spriteLayout, textureLayout) {
|
|
|
652
648
|
}
|
|
653
649
|
|
|
654
650
|
// src/spritesheet/spritesheet.ts
|
|
651
|
+
var import_renderer = require("murow/renderer");
|
|
655
652
|
var Spritesheet = class {
|
|
656
653
|
constructor(id, texture, textureView, sampler, uvs, width, height) {
|
|
657
654
|
this.id = id;
|
|
@@ -676,41 +673,6 @@ var Spritesheet = class {
|
|
|
676
673
|
return this._height;
|
|
677
674
|
}
|
|
678
675
|
};
|
|
679
|
-
function computeGridUVs(imageWidth, imageHeight, frameWidth, frameHeight) {
|
|
680
|
-
const cols = Math.floor(imageWidth / frameWidth);
|
|
681
|
-
const rows = Math.floor(imageHeight / frameHeight);
|
|
682
|
-
const uvs = [];
|
|
683
|
-
for (let row = 0; row < rows; row++) {
|
|
684
|
-
for (let col = 0; col < cols; col++) {
|
|
685
|
-
uvs.push({
|
|
686
|
-
minX: col * frameWidth / imageWidth,
|
|
687
|
-
minY: row * frameHeight / imageHeight,
|
|
688
|
-
maxX: (col + 1) * frameWidth / imageWidth,
|
|
689
|
-
maxY: (row + 1) * frameHeight / imageHeight
|
|
690
|
-
});
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
return uvs;
|
|
694
|
-
}
|
|
695
|
-
function computeTexturePackerUVs(data) {
|
|
696
|
-
const { w, h } = data.meta.size;
|
|
697
|
-
const uvs = [];
|
|
698
|
-
for (const key of Object.keys(data.frames)) {
|
|
699
|
-
const frame = data.frames[key].frame;
|
|
700
|
-
uvs.push({
|
|
701
|
-
minX: frame.x / w,
|
|
702
|
-
minY: frame.y / h,
|
|
703
|
-
maxX: (frame.x + frame.w) / w,
|
|
704
|
-
maxY: (frame.y + frame.h) / h
|
|
705
|
-
});
|
|
706
|
-
}
|
|
707
|
-
return uvs;
|
|
708
|
-
}
|
|
709
|
-
async function loadImage(url) {
|
|
710
|
-
const response = await fetch(url);
|
|
711
|
-
const blob = await response.blob();
|
|
712
|
-
return createImageBitmap(blob);
|
|
713
|
-
}
|
|
714
676
|
function createTextureFromBitmap(device, bitmap) {
|
|
715
677
|
const texture = device.createTexture({
|
|
716
678
|
size: [bitmap.width, bitmap.height, 1],
|
|
@@ -725,6 +687,9 @@ function createTextureFromBitmap(device, bitmap) {
|
|
|
725
687
|
return { texture, view: texture.createView() };
|
|
726
688
|
}
|
|
727
689
|
|
|
690
|
+
// src/2d/renderer.ts
|
|
691
|
+
var import_renderer3 = require("murow/renderer");
|
|
692
|
+
|
|
728
693
|
// src/geometry/geometry-builder.ts
|
|
729
694
|
var import_typegpu2 = __toESM(require("typegpu"), 1);
|
|
730
695
|
|
|
@@ -2203,9 +2168,23 @@ var ComputeBuilder = class {
|
|
|
2203
2168
|
};
|
|
2204
2169
|
|
|
2205
2170
|
// src/2d/renderer.ts
|
|
2206
|
-
var
|
|
2171
|
+
var prefab2DHandles = /* @__PURE__ */ new WeakMap();
|
|
2172
|
+
function isPrefab2D(value) {
|
|
2173
|
+
return value.type === "spritesheet";
|
|
2174
|
+
}
|
|
2175
|
+
function resolveSpritePrefabHandle(prefab) {
|
|
2176
|
+
const h = prefab2DHandles.get(prefab);
|
|
2177
|
+
if (!h) {
|
|
2178
|
+
throw new Error(
|
|
2179
|
+
`Prefab '${prefab.id}' has no GPU handle \u2014 has the renderer's init() been called with this bucket?`
|
|
2180
|
+
);
|
|
2181
|
+
}
|
|
2182
|
+
return h;
|
|
2183
|
+
}
|
|
2184
|
+
var WebGPU2DRenderer = class extends import_renderer2.Base2DRenderer {
|
|
2207
2185
|
constructor(canvas, options) {
|
|
2208
|
-
|
|
2186
|
+
const resolvedMaxSprites = options.maxSprites ?? options.maxInstances ?? 1024;
|
|
2187
|
+
super(canvas, { ...options, maxSprites: resolvedMaxSprites });
|
|
2209
2188
|
this.staticDirty = false;
|
|
2210
2189
|
// Per-sheet bind groups
|
|
2211
2190
|
this.sheetBindGroups = /* @__PURE__ */ new Map();
|
|
@@ -2214,12 +2193,13 @@ var WebGPU2DRenderer = class extends import_base_2d_renderer.Base2DRenderer {
|
|
|
2214
2193
|
this.uniformData = new Float32Array(20);
|
|
2215
2194
|
this.resizeObserver = null;
|
|
2216
2195
|
this.resizeCallbacks = [];
|
|
2196
|
+
this._prefabs = options.prefabs ?? null;
|
|
2217
2197
|
this.camera = new Camera2D(canvas.width || 800, canvas.height || 600);
|
|
2218
|
-
this.freeList = new import_free_list2.FreeList(
|
|
2219
|
-
this.batcher = new import_sparse_batcher.SparseBatcher(
|
|
2220
|
-
this.dynamicData = new Float32Array(
|
|
2221
|
-
this.staticData = new Float32Array(
|
|
2222
|
-
this.slotIndexData = new Uint32Array(
|
|
2198
|
+
this.freeList = new import_free_list2.FreeList(resolvedMaxSprites);
|
|
2199
|
+
this.batcher = new import_sparse_batcher.SparseBatcher(resolvedMaxSprites);
|
|
2200
|
+
this.dynamicData = new Float32Array(resolvedMaxSprites * DYNAMIC_FLOATS_PER_SPRITE);
|
|
2201
|
+
this.staticData = new Float32Array(resolvedMaxSprites * STATIC_FLOATS_PER_SPRITE);
|
|
2202
|
+
this.slotIndexData = new Uint32Array(resolvedMaxSprites);
|
|
2223
2203
|
}
|
|
2224
2204
|
get device() {
|
|
2225
2205
|
return this._device;
|
|
@@ -2273,9 +2253,25 @@ var WebGPU2DRenderer = class extends import_base_2d_renderer.Base2DRenderer {
|
|
|
2273
2253
|
this.rawStaticBuffer = this.root.unwrap(this.staticBuffer);
|
|
2274
2254
|
this.rawSlotIndexBuffer = this.root.unwrap(this.slotIndexBuffer);
|
|
2275
2255
|
this.rawUniformBuffer = this.root.unwrap(this.uniformBuffer);
|
|
2256
|
+
if (this._prefabs) {
|
|
2257
|
+
this.uploadPrefabBucket(this._prefabs);
|
|
2258
|
+
}
|
|
2276
2259
|
this.setupResizeObserver();
|
|
2277
2260
|
this._initialized = true;
|
|
2278
2261
|
}
|
|
2262
|
+
/**
|
|
2263
|
+
* Upload every prefab in the bucket to the GPU and stash the resulting
|
|
2264
|
+
* SpritesheetHandle on each prefab so `bucket.get(id)` returns something
|
|
2265
|
+
* usable as a sprite source.
|
|
2266
|
+
*/
|
|
2267
|
+
uploadPrefabBucket(bucket) {
|
|
2268
|
+
for (const prefab of bucket.entries()) {
|
|
2269
|
+
if (prefab.type === "spritesheet") {
|
|
2270
|
+
const handle = this.uploadParsedSpritesheet(prefab.parsed);
|
|
2271
|
+
prefab2DHandles.set(prefab, handle);
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2279
2275
|
setupResizeObserver() {
|
|
2280
2276
|
const supportsDevicePixelBox = (() => {
|
|
2281
2277
|
try {
|
|
@@ -2329,24 +2325,22 @@ var WebGPU2DRenderer = class extends import_base_2d_renderer.Base2DRenderer {
|
|
|
2329
2325
|
this.resizeCallbacks.push(callback);
|
|
2330
2326
|
}
|
|
2331
2327
|
async loadSpritesheet(source) {
|
|
2332
|
-
const
|
|
2333
|
-
|
|
2328
|
+
const parsed = await (0, import_renderer3.parseSpritesheet)(source);
|
|
2329
|
+
return this.uploadParsedSpritesheet(parsed);
|
|
2330
|
+
}
|
|
2331
|
+
/**
|
|
2332
|
+
* Upload a previously-parsed spritesheet to the GPU. Returns a SpritesheetHandle.
|
|
2333
|
+
* Splitting parse (CPU) from upload (GPU) lets callers parse spritesheets in parallel
|
|
2334
|
+
* before a renderer exists.
|
|
2335
|
+
*/
|
|
2336
|
+
uploadParsedSpritesheet(parsed) {
|
|
2337
|
+
const { texture, view } = createTextureFromBitmap(this._device, parsed.bitmap);
|
|
2334
2338
|
const sampler = this._device.createSampler({
|
|
2335
2339
|
magFilter: "nearest",
|
|
2336
2340
|
minFilter: "nearest"
|
|
2337
2341
|
});
|
|
2338
|
-
let uvs;
|
|
2339
|
-
if (source.data) {
|
|
2340
|
-
const resp = await fetch(source.data);
|
|
2341
|
-
const json = await resp.json();
|
|
2342
|
-
uvs = computeTexturePackerUVs(json);
|
|
2343
|
-
} else if (source.frameWidth && source.frameHeight) {
|
|
2344
|
-
uvs = computeGridUVs(bitmap.width, bitmap.height, source.frameWidth, source.frameHeight);
|
|
2345
|
-
} else {
|
|
2346
|
-
uvs = [{ minX: 0, minY: 0, maxX: 1, maxY: 1 }];
|
|
2347
|
-
}
|
|
2348
2342
|
const id = this.nextSheetId++;
|
|
2349
|
-
const sheet = new Spritesheet(id, texture, view, sampler, uvs,
|
|
2343
|
+
const sheet = new Spritesheet(id, texture, view, sampler, parsed.uvs, parsed.width, parsed.height);
|
|
2350
2344
|
this.sheets.set(id, sheet);
|
|
2351
2345
|
const bindGroup = this._device.createBindGroup({
|
|
2352
2346
|
layout: this.rawTextureLayout,
|
|
@@ -2364,18 +2358,20 @@ var WebGPU2DRenderer = class extends import_base_2d_renderer.Base2DRenderer {
|
|
|
2364
2358
|
throw new Error(`Max sprites (${this.maxSprites}) reached`);
|
|
2365
2359
|
const dynBase = slot * DYNAMIC_FLOATS_PER_SPRITE;
|
|
2366
2360
|
const statBase = slot * STATIC_FLOATS_PER_SPRITE;
|
|
2367
|
-
const
|
|
2368
|
-
const
|
|
2369
|
-
this.dynamicData[dynBase + DYNAMIC_OFFSET_PREV_X] =
|
|
2370
|
-
this.dynamicData[dynBase + DYNAMIC_OFFSET_PREV_Y] =
|
|
2371
|
-
this.dynamicData[dynBase + DYNAMIC_OFFSET_CURR_X] =
|
|
2372
|
-
this.dynamicData[dynBase + DYNAMIC_OFFSET_CURR_Y] =
|
|
2361
|
+
const sheet = isPrefab2D(opts.sheet) ? resolveSpritePrefabHandle(opts.sheet) : opts.sheet;
|
|
2362
|
+
const [px, py] = opts.position ?? [0, 0];
|
|
2363
|
+
this.dynamicData[dynBase + DYNAMIC_OFFSET_PREV_X] = px;
|
|
2364
|
+
this.dynamicData[dynBase + DYNAMIC_OFFSET_PREV_Y] = py;
|
|
2365
|
+
this.dynamicData[dynBase + DYNAMIC_OFFSET_CURR_X] = px;
|
|
2366
|
+
this.dynamicData[dynBase + DYNAMIC_OFFSET_CURR_Y] = py;
|
|
2373
2367
|
const rotation = opts.rotation ?? 0;
|
|
2374
2368
|
this.dynamicData[dynBase + DYNAMIC_OFFSET_PREV_ROTATION] = rotation;
|
|
2375
2369
|
this.dynamicData[dynBase + DYNAMIC_OFFSET_CURR_ROTATION] = rotation;
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2370
|
+
const s = opts.scale;
|
|
2371
|
+
const [sx, sy] = typeof s === "number" ? [s, s] : s ?? [1, 1];
|
|
2372
|
+
this.staticData[statBase + STATIC_OFFSET_SCALE_X] = sx;
|
|
2373
|
+
this.staticData[statBase + STATIC_OFFSET_SCALE_Y] = sy;
|
|
2374
|
+
const uv = sheet.getUV(opts.sprite ?? 0);
|
|
2379
2375
|
this.staticData[statBase + STATIC_OFFSET_UV_MIN_X] = uv.minX;
|
|
2380
2376
|
this.staticData[statBase + STATIC_OFFSET_UV_MIN_Y] = uv.minY;
|
|
2381
2377
|
this.staticData[statBase + STATIC_OFFSET_UV_MAX_X] = uv.maxX;
|
|
@@ -2390,12 +2386,12 @@ var WebGPU2DRenderer = class extends import_base_2d_renderer.Base2DRenderer {
|
|
|
2390
2386
|
this.staticData[statBase + STATIC_OFFSET_TINT_B] = tint[2];
|
|
2391
2387
|
this.staticData[statBase + STATIC_OFFSET_TINT_A] = tint[3];
|
|
2392
2388
|
this.staticDirty = true;
|
|
2393
|
-
this.batcher.add(opts.layer ?? 0,
|
|
2389
|
+
this.batcher.add(opts.layer ?? 0, sheet.id, slot);
|
|
2394
2390
|
return new SpriteAccessor(
|
|
2395
2391
|
this.dynamicData,
|
|
2396
2392
|
this.staticData,
|
|
2397
2393
|
slot,
|
|
2398
|
-
|
|
2394
|
+
sheet.id,
|
|
2399
2395
|
() => {
|
|
2400
2396
|
this.staticDirty = true;
|
|
2401
2397
|
}
|
|
@@ -2517,9 +2513,112 @@ var WebGPU2DRenderer = class extends import_base_2d_renderer.Base2DRenderer {
|
|
|
2517
2513
|
}
|
|
2518
2514
|
};
|
|
2519
2515
|
|
|
2516
|
+
// src/2d/animation.ts
|
|
2517
|
+
var AnimationController = class {
|
|
2518
|
+
constructor() {
|
|
2519
|
+
this.clips = [];
|
|
2520
|
+
this.clipsByName = /* @__PURE__ */ new Map();
|
|
2521
|
+
}
|
|
2522
|
+
/**
|
|
2523
|
+
* Register an animation clip. Returns the clip ID.
|
|
2524
|
+
*/
|
|
2525
|
+
loadClip(config) {
|
|
2526
|
+
const id = this.clips.length;
|
|
2527
|
+
const clip = {
|
|
2528
|
+
id,
|
|
2529
|
+
name: config.name,
|
|
2530
|
+
frames: new Uint16Array(config.frames),
|
|
2531
|
+
durations: new Float32Array(config.durations),
|
|
2532
|
+
frameCount: config.frames.length,
|
|
2533
|
+
totalDuration: config.durations.reduce((sum, d7) => sum + d7, 0),
|
|
2534
|
+
loop: config.loop
|
|
2535
|
+
};
|
|
2536
|
+
this.clips.push(clip);
|
|
2537
|
+
this.clipsByName.set(config.name, id);
|
|
2538
|
+
return id;
|
|
2539
|
+
}
|
|
2540
|
+
/**
|
|
2541
|
+
* Get a clip by name.
|
|
2542
|
+
*/
|
|
2543
|
+
getClipId(name) {
|
|
2544
|
+
const id = this.clipsByName.get(name);
|
|
2545
|
+
if (id === void 0)
|
|
2546
|
+
throw new Error(`Animation clip "${name}" not found`);
|
|
2547
|
+
return id;
|
|
2548
|
+
}
|
|
2549
|
+
/**
|
|
2550
|
+
* Get a clip by ID.
|
|
2551
|
+
*/
|
|
2552
|
+
getClip(id) {
|
|
2553
|
+
return this.clips[id];
|
|
2554
|
+
}
|
|
2555
|
+
/**
|
|
2556
|
+
* Create a new animation state for an entity. This is the per-entity data
|
|
2557
|
+
* that tracks playback progress. Store it however you like (flat array, component, etc).
|
|
2558
|
+
*/
|
|
2559
|
+
createState(clipId, speed = 1, playing = true) {
|
|
2560
|
+
return { clipId, frame: 0, time: 0, speed, playing };
|
|
2561
|
+
}
|
|
2562
|
+
/**
|
|
2563
|
+
* Advance an animation state by deltaTime (in seconds).
|
|
2564
|
+
* Returns the current sprite frame ID, or -1 if not playing.
|
|
2565
|
+
* Zero allocations.
|
|
2566
|
+
*/
|
|
2567
|
+
update(state, deltaTime) {
|
|
2568
|
+
if (!state.playing) {
|
|
2569
|
+
return this.clips[state.clipId].frames[state.frame];
|
|
2570
|
+
}
|
|
2571
|
+
const clip = this.clips[state.clipId];
|
|
2572
|
+
state.time += deltaTime * state.speed * 1e3;
|
|
2573
|
+
while (state.time >= clip.durations[state.frame]) {
|
|
2574
|
+
state.time -= clip.durations[state.frame];
|
|
2575
|
+
state.frame++;
|
|
2576
|
+
if (state.frame >= clip.frameCount) {
|
|
2577
|
+
if (clip.loop) {
|
|
2578
|
+
state.frame = 0;
|
|
2579
|
+
} else {
|
|
2580
|
+
state.frame = clip.frameCount - 1;
|
|
2581
|
+
state.playing = false;
|
|
2582
|
+
break;
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
return clip.frames[state.frame];
|
|
2587
|
+
}
|
|
2588
|
+
/**
|
|
2589
|
+
* Play a different clip on an existing state. Resets frame and time.
|
|
2590
|
+
*/
|
|
2591
|
+
play(state, clipId, speed) {
|
|
2592
|
+
state.clipId = clipId;
|
|
2593
|
+
state.frame = 0;
|
|
2594
|
+
state.time = 0;
|
|
2595
|
+
state.playing = true;
|
|
2596
|
+
if (speed !== void 0)
|
|
2597
|
+
state.speed = speed;
|
|
2598
|
+
}
|
|
2599
|
+
/**
|
|
2600
|
+
* Stop playback.
|
|
2601
|
+
*/
|
|
2602
|
+
stop(state) {
|
|
2603
|
+
state.playing = false;
|
|
2604
|
+
}
|
|
2605
|
+
/**
|
|
2606
|
+
* Resume playback.
|
|
2607
|
+
*/
|
|
2608
|
+
resume(state) {
|
|
2609
|
+
state.playing = true;
|
|
2610
|
+
}
|
|
2611
|
+
/**
|
|
2612
|
+
* Number of loaded clips.
|
|
2613
|
+
*/
|
|
2614
|
+
get clipCount() {
|
|
2615
|
+
return this.clips.length;
|
|
2616
|
+
}
|
|
2617
|
+
};
|
|
2618
|
+
|
|
2520
2619
|
// src/3d/renderer.ts
|
|
2521
2620
|
var import_typegpu9 = __toESM(require("typegpu"), 1);
|
|
2522
|
-
var
|
|
2621
|
+
var import_renderer4 = require("murow/renderer");
|
|
2523
2622
|
var import_free_list3 = require("murow/core/free-list");
|
|
2524
2623
|
var import_sparse_batcher2 = require("murow/core/sparse-batcher");
|
|
2525
2624
|
|
|
@@ -3164,754 +3263,101 @@ function createSkinnedMeshVertex(layout) {
|
|
|
3164
3263
|
});
|
|
3165
3264
|
}
|
|
3166
3265
|
|
|
3167
|
-
// src/
|
|
3168
|
-
|
|
3169
|
-
const m = new Float32Array(16);
|
|
3170
|
-
m[0] = 1;
|
|
3171
|
-
m[5] = 1;
|
|
3172
|
-
m[10] = 1;
|
|
3173
|
-
m[15] = 1;
|
|
3174
|
-
return m;
|
|
3175
|
-
}
|
|
3176
|
-
function trsToMat4(tx, ty, tz, qx, qy, qz, qw, sx, sy, sz, dst, offset) {
|
|
3177
|
-
const xx = qx * qx, yy = qy * qy, zz = qz * qz;
|
|
3178
|
-
const xy = qx * qy, xz = qx * qz, yz = qy * qz;
|
|
3179
|
-
const wx = qw * qx, wy = qw * qy, wz = qw * qz;
|
|
3180
|
-
dst[offset] = (1 - 2 * (yy + zz)) * sx;
|
|
3181
|
-
dst[offset + 1] = 2 * (xy + wz) * sx;
|
|
3182
|
-
dst[offset + 2] = 2 * (xz - wy) * sx;
|
|
3183
|
-
dst[offset + 3] = 0;
|
|
3184
|
-
dst[offset + 4] = 2 * (xy - wz) * sy;
|
|
3185
|
-
dst[offset + 5] = (1 - 2 * (xx + zz)) * sy;
|
|
3186
|
-
dst[offset + 6] = 2 * (yz + wx) * sy;
|
|
3187
|
-
dst[offset + 7] = 0;
|
|
3188
|
-
dst[offset + 8] = 2 * (xz + wy) * sz;
|
|
3189
|
-
dst[offset + 9] = 2 * (yz - wx) * sz;
|
|
3190
|
-
dst[offset + 10] = (1 - 2 * (xx + yy)) * sz;
|
|
3191
|
-
dst[offset + 11] = 0;
|
|
3192
|
-
dst[offset + 12] = tx;
|
|
3193
|
-
dst[offset + 13] = ty;
|
|
3194
|
-
dst[offset + 14] = tz;
|
|
3195
|
-
dst[offset + 15] = 1;
|
|
3196
|
-
}
|
|
3197
|
-
function nodeToMat4(node) {
|
|
3198
|
-
const m = new Float32Array(16);
|
|
3199
|
-
if (node.matrix) {
|
|
3200
|
-
m.set(node.matrix);
|
|
3201
|
-
return m;
|
|
3202
|
-
}
|
|
3203
|
-
const t = node.translation ?? [0, 0, 0];
|
|
3204
|
-
const r = node.rotation ?? [0, 0, 0, 1];
|
|
3205
|
-
const s = node.scale ?? [1, 1, 1];
|
|
3206
|
-
trsToMat4(t[0], t[1], t[2], r[0], r[1], r[2], r[3], s[0], s[1], s[2], m, 0);
|
|
3207
|
-
return m;
|
|
3208
|
-
}
|
|
3209
|
-
var _mulTemp = new Float32Array(16);
|
|
3210
|
-
function mat4Mul(a, aO, b, bO, dst, dO) {
|
|
3211
|
-
for (let i = 0; i < 4; i++) {
|
|
3212
|
-
for (let j = 0; j < 4; j++) {
|
|
3213
|
-
_mulTemp[j * 4 + i] = a[aO + i] * b[bO + j * 4] + a[aO + 4 + i] * b[bO + j * 4 + 1] + a[aO + 8 + i] * b[bO + j * 4 + 2] + a[aO + 12 + i] * b[bO + j * 4 + 3];
|
|
3214
|
-
}
|
|
3215
|
-
}
|
|
3216
|
-
dst.set(_mulTemp, dO);
|
|
3217
|
-
}
|
|
3218
|
-
function mat4MulNew(a, b) {
|
|
3219
|
-
const o = new Float32Array(16);
|
|
3220
|
-
for (let i = 0; i < 4; i++) {
|
|
3221
|
-
for (let j = 0; j < 4; j++) {
|
|
3222
|
-
o[j * 4 + i] = a[i] * b[j * 4] + a[4 + i] * b[j * 4 + 1] + a[8 + i] * b[j * 4 + 2] + a[12 + i] * b[j * 4 + 3];
|
|
3223
|
-
}
|
|
3224
|
-
}
|
|
3225
|
-
return o;
|
|
3226
|
-
}
|
|
3266
|
+
// src/3d/renderer.ts
|
|
3267
|
+
var import_renderer5 = require("murow/renderer");
|
|
3227
3268
|
|
|
3228
|
-
// src/3d/
|
|
3229
|
-
function
|
|
3230
|
-
const
|
|
3231
|
-
const
|
|
3232
|
-
const
|
|
3233
|
-
const
|
|
3234
|
-
const
|
|
3235
|
-
const
|
|
3236
|
-
const
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
const
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
}
|
|
3269
|
+
// src/3d/skeletal-animation-compute/packer.ts
|
|
3270
|
+
function packAnimationData(packed) {
|
|
3271
|
+
const numSkins = packed.skins.length;
|
|
3272
|
+
const numClips = packed.clips.length;
|
|
3273
|
+
const numChannels = packed.channels.length;
|
|
3274
|
+
const SKIN_ENTRY = 10;
|
|
3275
|
+
const CLIP_ENTRY = 4;
|
|
3276
|
+
const CH_ENTRY = 4;
|
|
3277
|
+
const skinEnd = numSkins * SKIN_ENTRY;
|
|
3278
|
+
const parentEnd = skinEnd + packed.parentIndices.length;
|
|
3279
|
+
const topoEnd = parentEnd + packed.topoOrder.length;
|
|
3280
|
+
const clipEnd = topoEnd + numClips * CLIP_ENTRY;
|
|
3281
|
+
const channelEnd = clipEnd + numChannels * CH_ENTRY;
|
|
3282
|
+
const jointLookupEnd = channelEnd + packed.jointChannelLookup.length;
|
|
3283
|
+
const clipTableOffset = topoEnd;
|
|
3284
|
+
const channelTableOffset = clipEnd;
|
|
3285
|
+
const jointLookupOffset = channelEnd;
|
|
3286
|
+
const skelI32 = new Int32Array(jointLookupEnd || 1);
|
|
3287
|
+
for (let s = 0; s < numSkins; s++) {
|
|
3288
|
+
const sk = packed.skins[s];
|
|
3289
|
+
const base = s * SKIN_ENTRY;
|
|
3290
|
+
skelI32[base + 0] = sk.jointCount;
|
|
3291
|
+
skelI32[base + 1] = skinEnd + sk.parentOffset;
|
|
3292
|
+
skelI32[base + 2] = parentEnd + sk.topoOffset;
|
|
3293
|
+
skelI32[base + 3] = sk.ibmOffset;
|
|
3294
|
+
skelI32[base + 4] = sk.restTRSOffset * 10;
|
|
3295
|
+
skelI32[base + 5] = sk.skelRootMatIndex;
|
|
3296
|
+
skelI32[base + 6] = sk.clipOffset;
|
|
3297
|
+
skelI32[base + 7] = jointLookupOffset + sk.jointLookupStart;
|
|
3298
|
+
skelI32[base + 8] = 0;
|
|
3299
|
+
skelI32[base + 9] = 0;
|
|
3260
3300
|
}
|
|
3261
|
-
let
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
for (let i = ancestorChain.length - 1; i >= 0; i--) {
|
|
3274
|
-
const nodeMat = nodeToMat4(gltf.nodes[ancestorChain[i]]);
|
|
3275
|
-
skeletonRootMatrix = mat4MulNew(skeletonRootMatrix, nodeMat);
|
|
3276
|
-
}
|
|
3277
|
-
}
|
|
3278
|
-
break;
|
|
3301
|
+
for (let i = 0; i < packed.parentIndices.length; i++)
|
|
3302
|
+
skelI32[skinEnd + i] = packed.parentIndices[i];
|
|
3303
|
+
for (let i = 0; i < packed.topoOrder.length; i++)
|
|
3304
|
+
skelI32[parentEnd + i] = packed.topoOrder[i];
|
|
3305
|
+
const dv = new DataView(skelI32.buffer);
|
|
3306
|
+
for (let c = 0; c < numClips; c++) {
|
|
3307
|
+
const cl = packed.clips[c];
|
|
3308
|
+
const base = topoEnd + c * CLIP_ENTRY;
|
|
3309
|
+
skelI32[base + 0] = cl.channelStart;
|
|
3310
|
+
skelI32[base + 1] = cl.channelCount;
|
|
3311
|
+
dv.setFloat32(base * 4 + 8, cl.duration, true);
|
|
3312
|
+
skelI32[base + 3] = cl.looping;
|
|
3279
3313
|
}
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
}
|
|
3288
|
-
function parseAnimations(gltf, skinData, getAccessorData) {
|
|
3289
|
-
if (!gltf.animations?.length)
|
|
3290
|
-
return [];
|
|
3291
|
-
const nodeToJoint = /* @__PURE__ */ new Map();
|
|
3292
|
-
for (let j = 0; j < skinData.jointCount; j++) {
|
|
3293
|
-
nodeToJoint.set(skinData.jointNodeIndices[j], j);
|
|
3294
|
-
}
|
|
3295
|
-
const clips = [];
|
|
3296
|
-
for (const anim of gltf.animations) {
|
|
3297
|
-
const channels = [];
|
|
3298
|
-
let maxTime = 0;
|
|
3299
|
-
for (const channel of anim.channels) {
|
|
3300
|
-
const targetNode = channel.target.node;
|
|
3301
|
-
const jointIndex = nodeToJoint.get(targetNode);
|
|
3302
|
-
if (jointIndex === void 0)
|
|
3303
|
-
continue;
|
|
3304
|
-
const path = channel.target.path;
|
|
3305
|
-
if (path !== "translation" && path !== "rotation" && path !== "scale")
|
|
3306
|
-
continue;
|
|
3307
|
-
const sampler = anim.samplers[channel.sampler];
|
|
3308
|
-
const interpolation = sampler.interpolation ?? "LINEAR";
|
|
3309
|
-
if (interpolation !== "LINEAR" && interpolation !== "STEP")
|
|
3310
|
-
continue;
|
|
3311
|
-
const inputAccess = getAccessorData(sampler.input);
|
|
3312
|
-
const timestamps = new Float32Array(inputAccess.data);
|
|
3313
|
-
const outputAccess = getAccessorData(sampler.output);
|
|
3314
|
-
const values = new Float32Array(outputAccess.data);
|
|
3315
|
-
if (timestamps.length > 0) {
|
|
3316
|
-
const lastTime = timestamps[timestamps.length - 1];
|
|
3317
|
-
if (lastTime > maxTime)
|
|
3318
|
-
maxTime = lastTime;
|
|
3319
|
-
}
|
|
3320
|
-
channels.push({
|
|
3321
|
-
jointIndex,
|
|
3322
|
-
path,
|
|
3323
|
-
timestamps,
|
|
3324
|
-
values,
|
|
3325
|
-
interpolation
|
|
3326
|
-
});
|
|
3327
|
-
}
|
|
3328
|
-
if (channels.length > 0) {
|
|
3329
|
-
clips.push({
|
|
3330
|
-
name: anim.name ?? `animation_${clips.length}`,
|
|
3331
|
-
duration: maxTime,
|
|
3332
|
-
channels
|
|
3333
|
-
});
|
|
3334
|
-
}
|
|
3314
|
+
for (let c = 0; c < numChannels; c++) {
|
|
3315
|
+
const ch = packed.channels[c];
|
|
3316
|
+
const base = clipEnd + c * CH_ENTRY;
|
|
3317
|
+
skelI32[base + 0] = ch.jointIndex;
|
|
3318
|
+
skelI32[base + 1] = ch.pathAndInterp;
|
|
3319
|
+
skelI32[base + 2] = ch.keyframeCount;
|
|
3320
|
+
skelI32[base + 3] = ch.dataOffset;
|
|
3335
3321
|
}
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
function parsePrimitiveSkinAttributes(primitive, getAccessorData) {
|
|
3339
|
-
if (primitive.attributes.JOINTS_0 === void 0 || primitive.attributes.WEIGHTS_0 === void 0) {
|
|
3340
|
-
return null;
|
|
3322
|
+
for (let i = 0; i < packed.jointChannelLookup.length; i++) {
|
|
3323
|
+
skelI32[channelEnd + i] = packed.jointChannelLookup[i];
|
|
3341
3324
|
}
|
|
3342
|
-
const
|
|
3343
|
-
const
|
|
3344
|
-
const
|
|
3345
|
-
let
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
} else if (weightsAccess.data instanceof Uint16Array) {
|
|
3354
|
-
weights = new Float32Array(weightsAccess.data.length);
|
|
3355
|
-
for (let i = 0; i < weightsAccess.data.length; i++) {
|
|
3356
|
-
weights[i] = weightsAccess.data[i] / 65535;
|
|
3357
|
-
}
|
|
3358
|
-
} else {
|
|
3359
|
-
weights = new Float32Array(weightsAccess.data);
|
|
3325
|
+
const keyframeDataSize = packed.keyframeData.length;
|
|
3326
|
+
const restTRSOffset = keyframeDataSize;
|
|
3327
|
+
const animF32 = new Float32Array(keyframeDataSize + packed.restTRS.length || 1);
|
|
3328
|
+
for (let i = 0; i < keyframeDataSize; i++)
|
|
3329
|
+
animF32[i] = packed.keyframeData[i];
|
|
3330
|
+
for (let i = 0; i < packed.restTRS.length; i++)
|
|
3331
|
+
animF32[keyframeDataSize + i] = packed.restTRS[i];
|
|
3332
|
+
const totalIBM = packed.ibmData.length / 16;
|
|
3333
|
+
for (let s = 0; s < numSkins; s++) {
|
|
3334
|
+
skelI32[s * SKIN_ENTRY + 5] += totalIBM;
|
|
3335
|
+
skelI32[s * SKIN_ENTRY + 4] += restTRSOffset;
|
|
3360
3336
|
}
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
trs[3] = 0;
|
|
3370
|
-
trs[4] = 0;
|
|
3371
|
-
trs[5] = 0;
|
|
3372
|
-
trs[6] = 1;
|
|
3373
|
-
trs[7] = 1;
|
|
3374
|
-
trs[8] = 1;
|
|
3375
|
-
trs[9] = 1;
|
|
3376
|
-
return trs;
|
|
3377
|
-
}
|
|
3378
|
-
const t = node.translation ?? [0, 0, 0];
|
|
3379
|
-
const r = node.rotation ?? [0, 0, 0, 1];
|
|
3380
|
-
const s = node.scale ?? [1, 1, 1];
|
|
3381
|
-
trs[0] = t[0];
|
|
3382
|
-
trs[1] = t[1];
|
|
3383
|
-
trs[2] = t[2];
|
|
3384
|
-
trs[3] = r[0];
|
|
3385
|
-
trs[4] = r[1];
|
|
3386
|
-
trs[5] = r[2];
|
|
3387
|
-
trs[6] = r[3];
|
|
3388
|
-
trs[7] = s[0];
|
|
3389
|
-
trs[8] = s[1];
|
|
3390
|
-
trs[9] = s[2];
|
|
3391
|
-
return trs;
|
|
3392
|
-
}
|
|
3393
|
-
function createPackedAnimationData() {
|
|
3394
|
-
return {
|
|
3395
|
-
channels: [],
|
|
3396
|
-
keyframeData: [],
|
|
3397
|
-
clips: [],
|
|
3398
|
-
skins: [],
|
|
3399
|
-
parentIndices: [],
|
|
3400
|
-
topoOrder: [],
|
|
3401
|
-
ibmData: [],
|
|
3402
|
-
restTRS: [],
|
|
3403
|
-
skelRootMats: [],
|
|
3404
|
-
jointChannelLookup: []
|
|
3405
|
-
};
|
|
3337
|
+
const totalSkelRoot = packed.skelRootMats.length / 16;
|
|
3338
|
+
const totalMats = totalIBM + totalSkelRoot || 1;
|
|
3339
|
+
const matFloats = new Float32Array(totalMats * 16);
|
|
3340
|
+
for (let i = 0; i < packed.ibmData.length; i++)
|
|
3341
|
+
matFloats[i] = packed.ibmData[i];
|
|
3342
|
+
for (let i = 0; i < packed.skelRootMats.length; i++)
|
|
3343
|
+
matFloats[totalIBM * 16 + i] = packed.skelRootMats[i];
|
|
3344
|
+
return { skelI32, animF32, matFloats, clipTableOffset, channelTableOffset, jointLookupOffset, totalMats };
|
|
3406
3345
|
}
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
const
|
|
3412
|
-
const
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
visited[j] = 1;
|
|
3423
|
-
const parent = skinData.parentJointIndices[j];
|
|
3424
|
-
if (parent !== -1)
|
|
3425
|
-
visit(parent);
|
|
3426
|
-
packed.topoOrder.push(j);
|
|
3427
|
-
};
|
|
3428
|
-
for (let j = 0; j < jc; j++)
|
|
3429
|
-
visit(j);
|
|
3430
|
-
for (let i = 0; i < skinData.inverseBindMatrices.length; i++) {
|
|
3431
|
-
packed.ibmData.push(skinData.inverseBindMatrices[i]);
|
|
3432
|
-
}
|
|
3433
|
-
for (let j = 0; j < jc; j++) {
|
|
3434
|
-
const trs = getNodeTRS(gltfNodes[skinData.jointNodeIndices[j]]);
|
|
3435
|
-
for (let k = 0; k < 10; k++)
|
|
3436
|
-
packed.restTRS.push(trs[k]);
|
|
3437
|
-
}
|
|
3438
|
-
if (skinData.skeletonRootMatrix) {
|
|
3439
|
-
for (let i = 0; i < 16; i++)
|
|
3440
|
-
packed.skelRootMats.push(skinData.skeletonRootMatrix[i]);
|
|
3441
|
-
} else {
|
|
3442
|
-
const id = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
3443
|
-
for (let i = 0; i < 16; i++)
|
|
3444
|
-
packed.skelRootMats.push(id[i]);
|
|
3445
|
-
}
|
|
3446
|
-
const clipOffset = packed.clips.length;
|
|
3447
|
-
const jointLookupStart = packed.jointChannelLookup.length;
|
|
3448
|
-
packed.skins.push({ jointCount: jc, parentOffset, topoOffset, ibmOffset, restTRSOffset, skelRootMatIndex, clipOffset, jointLookupStart });
|
|
3449
|
-
const nodeToJoint = /* @__PURE__ */ new Map();
|
|
3450
|
-
for (let j = 0; j < jc; j++) {
|
|
3451
|
-
nodeToJoint.set(skinData.jointNodeIndices[j], j);
|
|
3452
|
-
}
|
|
3453
|
-
for (const clip of clips) {
|
|
3454
|
-
const channelStart = packed.channels.length;
|
|
3455
|
-
const sortedChannels = [...clip.channels].sort((a, b) => a.jointIndex - b.jointIndex);
|
|
3456
|
-
for (const ch of sortedChannels) {
|
|
3457
|
-
const pathCode = ch.path === "translation" ? 0 : ch.path === "rotation" ? 1 : 2;
|
|
3458
|
-
const isStep = ch.interpolation === "STEP" ? 4 : 0;
|
|
3459
|
-
const dataOffset = packed.keyframeData.length;
|
|
3460
|
-
const n = ch.timestamps.length;
|
|
3461
|
-
const compCount = ch.path === "rotation" ? 4 : 3;
|
|
3462
|
-
for (let i = 0; i < n; i++)
|
|
3463
|
-
packed.keyframeData.push(ch.timestamps[i]);
|
|
3464
|
-
for (let i = 0; i < n * compCount; i++)
|
|
3465
|
-
packed.keyframeData.push(ch.values[i]);
|
|
3466
|
-
packed.channels.push({
|
|
3467
|
-
jointIndex: ch.jointIndex,
|
|
3468
|
-
pathAndInterp: pathCode | isStep,
|
|
3469
|
-
keyframeCount: n,
|
|
3470
|
-
dataOffset
|
|
3471
|
-
});
|
|
3472
|
-
}
|
|
3473
|
-
const clipChannelCount = sortedChannels.length;
|
|
3474
|
-
let ci = 0;
|
|
3475
|
-
for (let j = 0; j < jc; j++) {
|
|
3476
|
-
const lookupStart = ci;
|
|
3477
|
-
while (ci < clipChannelCount && sortedChannels[ci].jointIndex === j)
|
|
3478
|
-
ci++;
|
|
3479
|
-
packed.jointChannelLookup.push(channelStart + lookupStart);
|
|
3480
|
-
packed.jointChannelLookup.push(ci - lookupStart);
|
|
3481
|
-
}
|
|
3482
|
-
packed.clips.push({
|
|
3483
|
-
channelStart,
|
|
3484
|
-
channelCount: clipChannelCount,
|
|
3485
|
-
duration: clip.duration,
|
|
3486
|
-
looping: 1
|
|
3487
|
-
});
|
|
3488
|
-
}
|
|
3489
|
-
return skinIndex;
|
|
3490
|
-
}
|
|
3491
|
-
|
|
3492
|
-
// src/3d/skeletal-animation.ts
|
|
3493
|
-
var SkeletalAnimation = class {
|
|
3494
|
-
// 16 floats, identity if no non-joint ancestors
|
|
3495
|
-
constructor(skinData, clips, gltfNodes) {
|
|
3496
|
-
this.clips = [];
|
|
3497
|
-
this.clipsByName = /* @__PURE__ */ new Map();
|
|
3498
|
-
this._scratchVec3 = new Float32Array(3);
|
|
3499
|
-
this._scratchQuat = new Float32Array(4);
|
|
3500
|
-
this.skinData = skinData;
|
|
3501
|
-
const jc = skinData.jointCount;
|
|
3502
|
-
this.localMatrices = new Float32Array(jc * 16);
|
|
3503
|
-
this.worldMatrices = new Float32Array(jc * 16);
|
|
3504
|
-
this.blendScratch = new Float32Array(jc * 16);
|
|
3505
|
-
this.originalRestPoseTRS = new Float32Array(jc * 10);
|
|
3506
|
-
this.currentTRS = new Float32Array(jc * 10);
|
|
3507
|
-
for (let j = 0; j < jc; j++) {
|
|
3508
|
-
const nodeTRS = getNodeTRS(gltfNodes[skinData.jointNodeIndices[j]]);
|
|
3509
|
-
this.originalRestPoseTRS.set(nodeTRS, j * 10);
|
|
3510
|
-
}
|
|
3511
|
-
this.skelRootMat = new Float32Array(16);
|
|
3512
|
-
if (skinData.skeletonRootMatrix) {
|
|
3513
|
-
this.skelRootMat.set(skinData.skeletonRootMatrix);
|
|
3514
|
-
} else {
|
|
3515
|
-
this.skelRootMat[0] = 1;
|
|
3516
|
-
this.skelRootMat[5] = 1;
|
|
3517
|
-
this.skelRootMat[10] = 1;
|
|
3518
|
-
this.skelRootMat[15] = 1;
|
|
3519
|
-
}
|
|
3520
|
-
this.topoOrder = new Uint16Array(jc);
|
|
3521
|
-
const visited = new Uint8Array(jc);
|
|
3522
|
-
let writeIdx = 0;
|
|
3523
|
-
const visit = (j) => {
|
|
3524
|
-
if (visited[j])
|
|
3525
|
-
return;
|
|
3526
|
-
visited[j] = 1;
|
|
3527
|
-
const parent = skinData.parentJointIndices[j];
|
|
3528
|
-
if (parent !== -1)
|
|
3529
|
-
visit(parent);
|
|
3530
|
-
this.topoOrder[writeIdx++] = j;
|
|
3531
|
-
};
|
|
3532
|
-
for (let j = 0; j < jc; j++)
|
|
3533
|
-
visit(j);
|
|
3534
|
-
for (const clip of clips) {
|
|
3535
|
-
this.loadClip(clip);
|
|
3536
|
-
}
|
|
3537
|
-
}
|
|
3538
|
-
loadClip(clip, loop = true) {
|
|
3539
|
-
const id = this.clips.length;
|
|
3540
|
-
this.clips.push({
|
|
3541
|
-
id,
|
|
3542
|
-
name: clip.name,
|
|
3543
|
-
duration: clip.duration,
|
|
3544
|
-
channels: clip.channels,
|
|
3545
|
-
loop
|
|
3546
|
-
});
|
|
3547
|
-
this.clipsByName.set(clip.name, id);
|
|
3548
|
-
return id;
|
|
3549
|
-
}
|
|
3550
|
-
getClipId(name) {
|
|
3551
|
-
const id = this.clipsByName.get(name);
|
|
3552
|
-
if (id === void 0)
|
|
3553
|
-
throw new Error(`Skeletal clip "${name}" not found`);
|
|
3554
|
-
return id;
|
|
3555
|
-
}
|
|
3556
|
-
getClipNames() {
|
|
3557
|
-
return this.clips.map((c) => c.name);
|
|
3558
|
-
}
|
|
3559
|
-
getClip(id) {
|
|
3560
|
-
return this.clips[id] ?? null;
|
|
3561
|
-
}
|
|
3562
|
-
get clipCount() {
|
|
3563
|
-
return this.clips.length;
|
|
3564
|
-
}
|
|
3565
|
-
createState(clipIdOrName, speed = 1, playing = true) {
|
|
3566
|
-
const clipId = typeof clipIdOrName === "string" ? this.getClipId(clipIdOrName) : clipIdOrName;
|
|
3567
|
-
return { clipId, time: 0, speed, playing, loop: true, prevClipId: -1, prevTime: 0, prevSpeed: 1, blendWeight: 1, blendDuration: 0, onEnd: () => null };
|
|
3568
|
-
}
|
|
3569
|
-
play(state, clipIdOrName, opts) {
|
|
3570
|
-
const newClipId = typeof clipIdOrName === "string" ? this.getClipId(clipIdOrName) : clipIdOrName;
|
|
3571
|
-
const crossfade = opts?.crossfade ?? 0;
|
|
3572
|
-
if (crossfade > 0 && state.playing) {
|
|
3573
|
-
state.prevClipId = state.clipId;
|
|
3574
|
-
state.prevTime = state.time;
|
|
3575
|
-
state.prevSpeed = state.speed;
|
|
3576
|
-
state.blendWeight = 0;
|
|
3577
|
-
state.blendDuration = crossfade;
|
|
3578
|
-
} else {
|
|
3579
|
-
state.prevClipId = -1;
|
|
3580
|
-
state.blendWeight = 1;
|
|
3581
|
-
state.blendDuration = 0;
|
|
3582
|
-
}
|
|
3583
|
-
state.onEnd = opts?.onEnd ?? (() => null);
|
|
3584
|
-
state.loop = opts?.loop ?? true;
|
|
3585
|
-
state.clipId = newClipId;
|
|
3586
|
-
state.time = 0;
|
|
3587
|
-
state.playing = true;
|
|
3588
|
-
if (opts?.speed !== void 0)
|
|
3589
|
-
state.speed = opts.speed;
|
|
3590
|
-
}
|
|
3591
|
-
stop(state) {
|
|
3592
|
-
state.playing = false;
|
|
3593
|
-
}
|
|
3594
|
-
resume(state) {
|
|
3595
|
-
state.playing = true;
|
|
3596
|
-
}
|
|
3597
|
-
/**
|
|
3598
|
-
* Advance time and compute bone matrices.
|
|
3599
|
-
* Writes jointCount mat4s into `output` starting at float index `outputOffset`.
|
|
3600
|
-
* Handles crossfade blending automatically.
|
|
3601
|
-
*/
|
|
3602
|
-
update(state, deltaTime, output, outputOffset = 0) {
|
|
3603
|
-
const jc = this.skinData.jointCount;
|
|
3604
|
-
if (state.playing) {
|
|
3605
|
-
this.advanceTime(state, deltaTime);
|
|
3606
|
-
}
|
|
3607
|
-
const blending = state.prevClipId !== -1 && state.blendDuration > 0 && state.blendWeight < 1;
|
|
3608
|
-
if (blending) {
|
|
3609
|
-
state.blendWeight += deltaTime / state.blendDuration;
|
|
3610
|
-
if (state.blendWeight >= 1) {
|
|
3611
|
-
state.blendWeight = 1;
|
|
3612
|
-
state.prevClipId = -1;
|
|
3613
|
-
state.blendDuration = 0;
|
|
3614
|
-
}
|
|
3615
|
-
}
|
|
3616
|
-
if (blending && state.prevClipId !== -1) {
|
|
3617
|
-
const prevClip = this.clips[state.prevClipId];
|
|
3618
|
-
if (prevClip) {
|
|
3619
|
-
state.prevTime += deltaTime * state.prevSpeed;
|
|
3620
|
-
if (prevClip.loop && state.prevTime >= prevClip.duration) {
|
|
3621
|
-
state.prevTime %= prevClip.duration;
|
|
3622
|
-
}
|
|
3623
|
-
}
|
|
3624
|
-
this.evaluateClip(state.prevClipId, state.prevTime, this.blendScratch, 0);
|
|
3625
|
-
this.evaluateClip(state.clipId, state.time, output, outputOffset);
|
|
3626
|
-
const w = state.blendWeight;
|
|
3627
|
-
const oneMinusW = 1 - w;
|
|
3628
|
-
const scratch = this.blendScratch;
|
|
3629
|
-
const floatCount = jc * 16;
|
|
3630
|
-
for (let i = 0; i < floatCount; i++) {
|
|
3631
|
-
output[outputOffset + i] = scratch[i] * oneMinusW + output[outputOffset + i] * w;
|
|
3632
|
-
}
|
|
3633
|
-
} else {
|
|
3634
|
-
this.evaluateClip(state.clipId, state.time, output, outputOffset);
|
|
3635
|
-
}
|
|
3636
|
-
}
|
|
3637
|
-
/** Advance clip time with looping/clamping. */
|
|
3638
|
-
advanceTime(state, deltaTime) {
|
|
3639
|
-
const clip = this.clips[state.clipId];
|
|
3640
|
-
if (!clip || clip.duration <= 0)
|
|
3641
|
-
return;
|
|
3642
|
-
state.time += deltaTime * state.speed;
|
|
3643
|
-
if (state.time >= clip.duration) {
|
|
3644
|
-
if (clip.loop)
|
|
3645
|
-
state.time %= clip.duration;
|
|
3646
|
-
else {
|
|
3647
|
-
state.time = clip.duration - 1e-4;
|
|
3648
|
-
state.playing = false;
|
|
3649
|
-
}
|
|
3650
|
-
}
|
|
3651
|
-
if (state.time < 0) {
|
|
3652
|
-
if (clip.loop)
|
|
3653
|
-
state.time = clip.duration + state.time % clip.duration;
|
|
3654
|
-
else {
|
|
3655
|
-
state.time = 0;
|
|
3656
|
-
state.playing = false;
|
|
3657
|
-
}
|
|
3658
|
-
}
|
|
3659
|
-
}
|
|
3660
|
-
/** Evaluate a clip at a specific time and write bone matrices. */
|
|
3661
|
-
evaluateClip(clipId, time, output, outputOffset) {
|
|
3662
|
-
const clip = this.clips[clipId];
|
|
3663
|
-
if (!clip)
|
|
3664
|
-
return;
|
|
3665
|
-
const skin = this.skinData;
|
|
3666
|
-
const jc = skin.jointCount;
|
|
3667
|
-
const local = this.localMatrices;
|
|
3668
|
-
const world = this.worldMatrices;
|
|
3669
|
-
const cur = this.currentTRS;
|
|
3670
|
-
cur.set(this.originalRestPoseTRS);
|
|
3671
|
-
for (const ch of clip.channels) {
|
|
3672
|
-
const j = ch.jointIndex;
|
|
3673
|
-
if (j >= jc)
|
|
3674
|
-
continue;
|
|
3675
|
-
const ro = j * 10;
|
|
3676
|
-
const sampled = this.sampleChannel(ch, time);
|
|
3677
|
-
if (ch.path === "translation") {
|
|
3678
|
-
cur[ro] = sampled[0];
|
|
3679
|
-
cur[ro + 1] = sampled[1];
|
|
3680
|
-
cur[ro + 2] = sampled[2];
|
|
3681
|
-
} else if (ch.path === "rotation") {
|
|
3682
|
-
cur[ro + 3] = sampled[0];
|
|
3683
|
-
cur[ro + 4] = sampled[1];
|
|
3684
|
-
cur[ro + 5] = sampled[2];
|
|
3685
|
-
cur[ro + 6] = sampled[3];
|
|
3686
|
-
} else if (ch.path === "scale") {
|
|
3687
|
-
cur[ro + 7] = sampled[0];
|
|
3688
|
-
cur[ro + 8] = sampled[1];
|
|
3689
|
-
cur[ro + 9] = sampled[2];
|
|
3690
|
-
}
|
|
3691
|
-
}
|
|
3692
|
-
for (let j = 0; j < jc; j++) {
|
|
3693
|
-
const ro = j * 10;
|
|
3694
|
-
trsToMat4(
|
|
3695
|
-
cur[ro],
|
|
3696
|
-
cur[ro + 1],
|
|
3697
|
-
cur[ro + 2],
|
|
3698
|
-
cur[ro + 3],
|
|
3699
|
-
cur[ro + 4],
|
|
3700
|
-
cur[ro + 5],
|
|
3701
|
-
cur[ro + 6],
|
|
3702
|
-
cur[ro + 7],
|
|
3703
|
-
cur[ro + 8],
|
|
3704
|
-
cur[ro + 9],
|
|
3705
|
-
local,
|
|
3706
|
-
j * 16
|
|
3707
|
-
);
|
|
3708
|
-
}
|
|
3709
|
-
const topo = this.topoOrder;
|
|
3710
|
-
const srm = this.skelRootMat;
|
|
3711
|
-
for (let i = 0; i < jc; i++) {
|
|
3712
|
-
const j = topo[i];
|
|
3713
|
-
const parentJ = skin.parentJointIndices[j];
|
|
3714
|
-
if (parentJ === -1) {
|
|
3715
|
-
mat4Mul(srm, 0, local, j * 16, world, j * 16);
|
|
3716
|
-
} else {
|
|
3717
|
-
mat4Mul(world, parentJ * 16, local, j * 16, world, j * 16);
|
|
3718
|
-
}
|
|
3719
|
-
}
|
|
3720
|
-
const ibm = skin.inverseBindMatrices;
|
|
3721
|
-
for (let j = 0; j < jc; j++) {
|
|
3722
|
-
mat4Mul(world, j * 16, ibm, j * 16, output, outputOffset + j * 16);
|
|
3723
|
-
}
|
|
3724
|
-
}
|
|
3725
|
-
/**
|
|
3726
|
-
* Compute bone matrices for the rest/bind pose (no animation).
|
|
3727
|
-
*/
|
|
3728
|
-
computeRestPose(output, outputOffset = 0) {
|
|
3729
|
-
const skin = this.skinData;
|
|
3730
|
-
const jc = skin.jointCount;
|
|
3731
|
-
const local = this.localMatrices;
|
|
3732
|
-
const world = this.worldMatrices;
|
|
3733
|
-
const rest = this.originalRestPoseTRS;
|
|
3734
|
-
for (let j = 0; j < jc; j++) {
|
|
3735
|
-
const ro = j * 10;
|
|
3736
|
-
trsToMat4(
|
|
3737
|
-
rest[ro],
|
|
3738
|
-
rest[ro + 1],
|
|
3739
|
-
rest[ro + 2],
|
|
3740
|
-
rest[ro + 3],
|
|
3741
|
-
rest[ro + 4],
|
|
3742
|
-
rest[ro + 5],
|
|
3743
|
-
rest[ro + 6],
|
|
3744
|
-
rest[ro + 7],
|
|
3745
|
-
rest[ro + 8],
|
|
3746
|
-
rest[ro + 9],
|
|
3747
|
-
local,
|
|
3748
|
-
j * 16
|
|
3749
|
-
);
|
|
3750
|
-
}
|
|
3751
|
-
const topo = this.topoOrder;
|
|
3752
|
-
const srm = this.skelRootMat;
|
|
3753
|
-
for (let i = 0; i < jc; i++) {
|
|
3754
|
-
const j = topo[i];
|
|
3755
|
-
const parentJ = skin.parentJointIndices[j];
|
|
3756
|
-
if (parentJ === -1) {
|
|
3757
|
-
mat4Mul(srm, 0, local, j * 16, world, j * 16);
|
|
3758
|
-
} else {
|
|
3759
|
-
mat4Mul(world, parentJ * 16, local, j * 16, world, j * 16);
|
|
3760
|
-
}
|
|
3761
|
-
}
|
|
3762
|
-
const ibm = skin.inverseBindMatrices;
|
|
3763
|
-
for (let j = 0; j < jc; j++) {
|
|
3764
|
-
mat4Mul(world, j * 16, ibm, j * 16, output, outputOffset + j * 16);
|
|
3765
|
-
}
|
|
3766
|
-
}
|
|
3767
|
-
// --- Private helpers (zero-alloc, operate on Float32Array sub-regions) ---
|
|
3768
|
-
/** Sample an animation channel at time t. Returns 3 or 4 floats. */
|
|
3769
|
-
sampleChannel(ch, t) {
|
|
3770
|
-
const ts = ch.timestamps;
|
|
3771
|
-
const vals = ch.values;
|
|
3772
|
-
const compCount = ch.path === "rotation" ? 4 : 3;
|
|
3773
|
-
if (t <= ts[0])
|
|
3774
|
-
return vals.subarray(0, compCount);
|
|
3775
|
-
if (t >= ts[ts.length - 1])
|
|
3776
|
-
return vals.subarray((ts.length - 1) * compCount, ts.length * compCount);
|
|
3777
|
-
let lo = 0, hi = ts.length - 1;
|
|
3778
|
-
while (lo < hi - 1) {
|
|
3779
|
-
const mid = lo + hi >> 1;
|
|
3780
|
-
if (ts[mid] <= t)
|
|
3781
|
-
lo = mid;
|
|
3782
|
-
else
|
|
3783
|
-
hi = mid;
|
|
3784
|
-
}
|
|
3785
|
-
if (ch.interpolation === "STEP") {
|
|
3786
|
-
return vals.subarray(lo * compCount, lo * compCount + compCount);
|
|
3787
|
-
}
|
|
3788
|
-
const t0 = ts[lo], t1 = ts[hi];
|
|
3789
|
-
const f = t1 > t0 ? (t - t0) / (t1 - t0) : 0;
|
|
3790
|
-
const a = lo * compCount;
|
|
3791
|
-
const b = hi * compCount;
|
|
3792
|
-
if (ch.path === "rotation") {
|
|
3793
|
-
return this.nlerpQuat(vals, a, vals, b, f);
|
|
3794
|
-
}
|
|
3795
|
-
const out = this._scratchVec3;
|
|
3796
|
-
out[0] = vals[a] + (vals[b] - vals[a]) * f;
|
|
3797
|
-
out[1] = vals[a + 1] + (vals[b + 1] - vals[a + 1]) * f;
|
|
3798
|
-
out[2] = vals[a + 2] + (vals[b + 2] - vals[a + 2]) * f;
|
|
3799
|
-
return out;
|
|
3800
|
-
}
|
|
3801
|
-
/** Normalized lerp for quaternions (nlerp). */
|
|
3802
|
-
nlerpQuat(a, ao, b, bo, t) {
|
|
3803
|
-
const out = this._scratchQuat;
|
|
3804
|
-
let dot2 = a[ao] * b[bo] + a[ao + 1] * b[bo + 1] + a[ao + 2] * b[bo + 2] + a[ao + 3] * b[bo + 3];
|
|
3805
|
-
const sign = dot2 < 0 ? -1 : 1;
|
|
3806
|
-
const oneMinusT = 1 - t;
|
|
3807
|
-
out[0] = oneMinusT * a[ao] + t * b[bo] * sign;
|
|
3808
|
-
out[1] = oneMinusT * a[ao + 1] + t * b[bo + 1] * sign;
|
|
3809
|
-
out[2] = oneMinusT * a[ao + 2] + t * b[bo + 2] * sign;
|
|
3810
|
-
out[3] = oneMinusT * a[ao + 3] + t * b[bo + 3] * sign;
|
|
3811
|
-
const len = Math.sqrt(out[0] * out[0] + out[1] * out[1] + out[2] * out[2] + out[3] * out[3]);
|
|
3812
|
-
if (len > 0) {
|
|
3813
|
-
const inv = 1 / len;
|
|
3814
|
-
out[0] *= inv;
|
|
3815
|
-
out[1] *= inv;
|
|
3816
|
-
out[2] *= inv;
|
|
3817
|
-
out[3] *= inv;
|
|
3818
|
-
}
|
|
3819
|
-
return out;
|
|
3820
|
-
}
|
|
3821
|
-
};
|
|
3822
|
-
|
|
3823
|
-
// src/3d/skeletal-animation-compute/packer.ts
|
|
3824
|
-
function packAnimationData(packed) {
|
|
3825
|
-
const numSkins = packed.skins.length;
|
|
3826
|
-
const numClips = packed.clips.length;
|
|
3827
|
-
const numChannels = packed.channels.length;
|
|
3828
|
-
const SKIN_ENTRY = 10;
|
|
3829
|
-
const CLIP_ENTRY = 4;
|
|
3830
|
-
const CH_ENTRY = 4;
|
|
3831
|
-
const skinEnd = numSkins * SKIN_ENTRY;
|
|
3832
|
-
const parentEnd = skinEnd + packed.parentIndices.length;
|
|
3833
|
-
const topoEnd = parentEnd + packed.topoOrder.length;
|
|
3834
|
-
const clipEnd = topoEnd + numClips * CLIP_ENTRY;
|
|
3835
|
-
const channelEnd = clipEnd + numChannels * CH_ENTRY;
|
|
3836
|
-
const jointLookupEnd = channelEnd + packed.jointChannelLookup.length;
|
|
3837
|
-
const clipTableOffset = topoEnd;
|
|
3838
|
-
const channelTableOffset = clipEnd;
|
|
3839
|
-
const jointLookupOffset = channelEnd;
|
|
3840
|
-
const skelI32 = new Int32Array(jointLookupEnd || 1);
|
|
3841
|
-
for (let s = 0; s < numSkins; s++) {
|
|
3842
|
-
const sk = packed.skins[s];
|
|
3843
|
-
const base = s * SKIN_ENTRY;
|
|
3844
|
-
skelI32[base + 0] = sk.jointCount;
|
|
3845
|
-
skelI32[base + 1] = skinEnd + sk.parentOffset;
|
|
3846
|
-
skelI32[base + 2] = parentEnd + sk.topoOffset;
|
|
3847
|
-
skelI32[base + 3] = sk.ibmOffset;
|
|
3848
|
-
skelI32[base + 4] = sk.restTRSOffset * 10;
|
|
3849
|
-
skelI32[base + 5] = sk.skelRootMatIndex;
|
|
3850
|
-
skelI32[base + 6] = sk.clipOffset;
|
|
3851
|
-
skelI32[base + 7] = jointLookupOffset + sk.jointLookupStart;
|
|
3852
|
-
skelI32[base + 8] = 0;
|
|
3853
|
-
skelI32[base + 9] = 0;
|
|
3854
|
-
}
|
|
3855
|
-
for (let i = 0; i < packed.parentIndices.length; i++)
|
|
3856
|
-
skelI32[skinEnd + i] = packed.parentIndices[i];
|
|
3857
|
-
for (let i = 0; i < packed.topoOrder.length; i++)
|
|
3858
|
-
skelI32[parentEnd + i] = packed.topoOrder[i];
|
|
3859
|
-
const dv = new DataView(skelI32.buffer);
|
|
3860
|
-
for (let c = 0; c < numClips; c++) {
|
|
3861
|
-
const cl = packed.clips[c];
|
|
3862
|
-
const base = topoEnd + c * CLIP_ENTRY;
|
|
3863
|
-
skelI32[base + 0] = cl.channelStart;
|
|
3864
|
-
skelI32[base + 1] = cl.channelCount;
|
|
3865
|
-
dv.setFloat32(base * 4 + 8, cl.duration, true);
|
|
3866
|
-
skelI32[base + 3] = cl.looping;
|
|
3867
|
-
}
|
|
3868
|
-
for (let c = 0; c < numChannels; c++) {
|
|
3869
|
-
const ch = packed.channels[c];
|
|
3870
|
-
const base = clipEnd + c * CH_ENTRY;
|
|
3871
|
-
skelI32[base + 0] = ch.jointIndex;
|
|
3872
|
-
skelI32[base + 1] = ch.pathAndInterp;
|
|
3873
|
-
skelI32[base + 2] = ch.keyframeCount;
|
|
3874
|
-
skelI32[base + 3] = ch.dataOffset;
|
|
3875
|
-
}
|
|
3876
|
-
for (let i = 0; i < packed.jointChannelLookup.length; i++) {
|
|
3877
|
-
skelI32[channelEnd + i] = packed.jointChannelLookup[i];
|
|
3878
|
-
}
|
|
3879
|
-
const keyframeDataSize = packed.keyframeData.length;
|
|
3880
|
-
const restTRSOffset = keyframeDataSize;
|
|
3881
|
-
const animF32 = new Float32Array(keyframeDataSize + packed.restTRS.length || 1);
|
|
3882
|
-
for (let i = 0; i < keyframeDataSize; i++)
|
|
3883
|
-
animF32[i] = packed.keyframeData[i];
|
|
3884
|
-
for (let i = 0; i < packed.restTRS.length; i++)
|
|
3885
|
-
animF32[keyframeDataSize + i] = packed.restTRS[i];
|
|
3886
|
-
const totalIBM = packed.ibmData.length / 16;
|
|
3887
|
-
for (let s = 0; s < numSkins; s++) {
|
|
3888
|
-
skelI32[s * SKIN_ENTRY + 5] += totalIBM;
|
|
3889
|
-
skelI32[s * SKIN_ENTRY + 4] += restTRSOffset;
|
|
3890
|
-
}
|
|
3891
|
-
const totalSkelRoot = packed.skelRootMats.length / 16;
|
|
3892
|
-
const totalMats = totalIBM + totalSkelRoot || 1;
|
|
3893
|
-
const matFloats = new Float32Array(totalMats * 16);
|
|
3894
|
-
for (let i = 0; i < packed.ibmData.length; i++)
|
|
3895
|
-
matFloats[i] = packed.ibmData[i];
|
|
3896
|
-
for (let i = 0; i < packed.skelRootMats.length; i++)
|
|
3897
|
-
matFloats[totalIBM * 16 + i] = packed.skelRootMats[i];
|
|
3898
|
-
return { skelI32, animF32, matFloats, clipTableOffset, channelTableOffset, jointLookupOffset, totalMats };
|
|
3899
|
-
}
|
|
3900
|
-
|
|
3901
|
-
// src/3d/skeletal-animation-compute/kernel.ts
|
|
3902
|
-
var WORKGROUP_SIZE = 64;
|
|
3903
|
-
function buildAnimationKernel(root, packed, maxInstances, maxTotalBones) {
|
|
3904
|
-
const pb = packAnimationData(packed);
|
|
3905
|
-
const kernel = new ComputeBuilder("skeletal-animation", { workgroupSize: WORKGROUP_SIZE }, root).buffers({
|
|
3906
|
-
uniforms: { uniform: AnimComputeUniforms },
|
|
3907
|
-
instances: { storage: d.arrayOf(InstanceAnimStateGPU, maxInstances) },
|
|
3908
|
-
skelI32: { storage: d.arrayOf(d.i32, pb.skelI32.length) },
|
|
3909
|
-
animF32: { storage: d.arrayOf(d.f32, pb.animF32.length) },
|
|
3910
|
-
matrices: { storage: d.arrayOf(d.mat4x4f, pb.totalMats) },
|
|
3911
|
-
boneMatrices: { storage: d.arrayOf(d.mat4x4f, maxTotalBones), readwrite: true }
|
|
3912
|
-
}).shader(({ uniforms, instances, skelI32, animF32, matrices, boneMatrices }, { globalId }) => {
|
|
3913
|
-
const idx = globalId.x;
|
|
3914
|
-
if (idx >= uniforms.instanceCount) {
|
|
3346
|
+
|
|
3347
|
+
// src/3d/skeletal-animation-compute/kernel.ts
|
|
3348
|
+
var WORKGROUP_SIZE = 64;
|
|
3349
|
+
function buildAnimationKernel(root, packed, maxInstances, maxTotalBones, budgets) {
|
|
3350
|
+
const pb = packAnimationData(packed);
|
|
3351
|
+
const kernel = new ComputeBuilder("skeletal-animation", { workgroupSize: WORKGROUP_SIZE }, root).buffers({
|
|
3352
|
+
uniforms: { uniform: AnimComputeUniforms },
|
|
3353
|
+
instances: { storage: d.arrayOf(InstanceAnimStateGPU, maxInstances) },
|
|
3354
|
+
skelI32: { storage: d.arrayOf(d.i32, budgets.skelI32Capacity) },
|
|
3355
|
+
animF32: { storage: d.arrayOf(d.f32, budgets.animF32Capacity) },
|
|
3356
|
+
matrices: { storage: d.arrayOf(d.mat4x4f, budgets.matricesCapacity) },
|
|
3357
|
+
boneMatrices: { storage: d.arrayOf(d.mat4x4f, maxTotalBones), readwrite: true }
|
|
3358
|
+
}).shader(({ uniforms, instances, skelI32, animF32, matrices, boneMatrices }, { globalId }) => {
|
|
3359
|
+
const idx = globalId.x;
|
|
3360
|
+
if (idx >= uniforms.instanceCount) {
|
|
3915
3361
|
return;
|
|
3916
3362
|
}
|
|
3917
3363
|
const inst = instances[idx];
|
|
@@ -4249,13 +3695,50 @@ function buildAnimationKernel(root, packed, maxInstances, maxTotalBones) {
|
|
|
4249
3695
|
}
|
|
4250
3696
|
}
|
|
4251
3697
|
}).build();
|
|
4252
|
-
|
|
4253
|
-
kernel
|
|
4254
|
-
const matBuffer = kernel.getBuffer("matrices");
|
|
4255
|
-
const rawMatBuffer = root.unwrap(matBuffer);
|
|
4256
|
-
root.device.queue.writeBuffer(rawMatBuffer, 0, pb.matFloats);
|
|
4257
|
-
return { kernel, packedBuffers: pb };
|
|
3698
|
+
uploadPackedToKernel(root, kernel, pb);
|
|
3699
|
+
return { kernel, packedBuffers: pb, budgets };
|
|
4258
3700
|
}
|
|
3701
|
+
function uploadPackedToKernel(root, kernel, pb) {
|
|
3702
|
+
const queue = root.device.queue;
|
|
3703
|
+
const skelBuf = root.unwrap(kernel.getBuffer("skelI32"));
|
|
3704
|
+
queue.writeBuffer(skelBuf, 0, pb.skelI32);
|
|
3705
|
+
const animBuf = root.unwrap(kernel.getBuffer("animF32"));
|
|
3706
|
+
queue.writeBuffer(animBuf, 0, pb.animF32);
|
|
3707
|
+
const matBuf = root.unwrap(kernel.getBuffer("matrices"));
|
|
3708
|
+
queue.writeBuffer(matBuf, 0, pb.matFloats);
|
|
3709
|
+
}
|
|
3710
|
+
|
|
3711
|
+
// src/3d/clip-resync-coordinator.ts
|
|
3712
|
+
var GltfClipResyncCoordinator = class {
|
|
3713
|
+
constructor(bucket) {
|
|
3714
|
+
this.bucket = bucket;
|
|
3715
|
+
this.skinIndexByPrefabId = /* @__PURE__ */ new Map();
|
|
3716
|
+
this._pending = /* @__PURE__ */ new Set();
|
|
3717
|
+
this.bucket.events.on("clips-changed", ({ prefabId }) => {
|
|
3718
|
+
const skinIndex = this.skinIndexByPrefabId.get(prefabId);
|
|
3719
|
+
if (skinIndex !== void 0)
|
|
3720
|
+
this._pending.add(skinIndex);
|
|
3721
|
+
});
|
|
3722
|
+
}
|
|
3723
|
+
/** Map a prefab id to its index in the renderer's `skinnedModels` list. */
|
|
3724
|
+
registerSkin(prefabId, skinIndex) {
|
|
3725
|
+
this.skinIndexByPrefabId.set(prefabId, skinIndex);
|
|
3726
|
+
}
|
|
3727
|
+
/** Skin indices whose clip set has changed since the last `clear()`. */
|
|
3728
|
+
get pending() {
|
|
3729
|
+
return this._pending;
|
|
3730
|
+
}
|
|
3731
|
+
clear() {
|
|
3732
|
+
this._pending.clear();
|
|
3733
|
+
}
|
|
3734
|
+
/** Unsubscribe from the bucket and reset its animation clip sets. */
|
|
3735
|
+
dispose() {
|
|
3736
|
+
this.bucket.events.clear("clips-changed");
|
|
3737
|
+
this.bucket.resetAnimations();
|
|
3738
|
+
this.skinIndexByPrefabId.clear();
|
|
3739
|
+
this._pending.clear();
|
|
3740
|
+
}
|
|
3741
|
+
};
|
|
4259
3742
|
|
|
4260
3743
|
// src/3d/renderer.ts
|
|
4261
3744
|
var DYN_PREV_PX = 0;
|
|
@@ -4283,9 +3766,45 @@ var SSTAT_CR = 3;
|
|
|
4283
3766
|
var SSTAT_CG = 4;
|
|
4284
3767
|
var SSTAT_CB = 5;
|
|
4285
3768
|
var SSTAT_BONE_OFFSET = 6;
|
|
4286
|
-
var
|
|
3769
|
+
var prefabHandles = /* @__PURE__ */ new WeakMap();
|
|
3770
|
+
function isPrefab3D(value) {
|
|
3771
|
+
const t = value.type;
|
|
3772
|
+
return t === "gltf" || t === "grid" || t === "cube" || t === "composite";
|
|
3773
|
+
}
|
|
3774
|
+
function resolveTransform(opts) {
|
|
3775
|
+
const [px, py, pz] = opts.position ?? [0, 0, 0];
|
|
3776
|
+
const [rx, ry, rz] = opts.rotation ?? [0, 0, 0];
|
|
3777
|
+
const s = opts.scale;
|
|
3778
|
+
const [sx, sy, sz] = typeof s === "number" ? [s, s, s] : s ?? [1, 1, 1];
|
|
3779
|
+
const [cr, cg, cb] = opts.color ?? [1, 1, 1];
|
|
3780
|
+
return { px, py, pz, rx, ry, rz, sx, sy, sz, cr, cg, cb };
|
|
3781
|
+
}
|
|
3782
|
+
function resolvePrefabHandle(prefab) {
|
|
3783
|
+
const h = prefabHandles.get(prefab);
|
|
3784
|
+
if (!h) {
|
|
3785
|
+
throw new Error(
|
|
3786
|
+
`Prefab '${prefab.id}' has no GPU handle \u2014 has the renderer's init() been called with this bucket?`
|
|
3787
|
+
);
|
|
3788
|
+
}
|
|
3789
|
+
return h;
|
|
3790
|
+
}
|
|
3791
|
+
function computeBucketStats(bucket) {
|
|
3792
|
+
let maxSkinnedParts = 0;
|
|
3793
|
+
let maxJointCount = 0;
|
|
3794
|
+
for (const prefab of bucket.entries()) {
|
|
3795
|
+
if (prefab.type === "gltf") {
|
|
3796
|
+
if (prefab.skinnedPartCount > maxSkinnedParts)
|
|
3797
|
+
maxSkinnedParts = prefab.skinnedPartCount;
|
|
3798
|
+
if (prefab.jointCount > maxJointCount)
|
|
3799
|
+
maxJointCount = prefab.jointCount;
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
3802
|
+
return { maxSkinnedParts, maxJointCount };
|
|
3803
|
+
}
|
|
3804
|
+
var WebGPU3DRenderer = class extends import_renderer4.Base3DRenderer {
|
|
4287
3805
|
constructor(canvas, options) {
|
|
4288
|
-
|
|
3806
|
+
const resolvedMaxInstances = options.maxInstances ?? (options.prefabs ? options.prefabs.size + 16 : 32);
|
|
3807
|
+
super(canvas, { ...options, maxInstances: resolvedMaxInstances });
|
|
4289
3808
|
this.resizeObserver = null;
|
|
4290
3809
|
this.resizeCallbacks = [];
|
|
4291
3810
|
// layer=0, sheetId=modelId
|
|
@@ -4295,11 +3814,15 @@ var WebGPU3DRenderer = class extends import_base_3d_renderer.Base3DRenderer {
|
|
|
4295
3814
|
this.nextModelId = 0;
|
|
4296
3815
|
// Skinned model data
|
|
4297
3816
|
this.skinnedModels = [];
|
|
3817
|
+
// Null when constructed without a `prefabs` bucket; lazy load/unload requires it as event source.
|
|
3818
|
+
this.clipResync = null;
|
|
4298
3819
|
this.boneMatrixDirty = true;
|
|
4299
3820
|
// GPU animation compute
|
|
4300
|
-
this.packedAnimData = createPackedAnimationData();
|
|
3821
|
+
this.packedAnimData = (0, import_renderer5.createPackedAnimationData)();
|
|
4301
3822
|
this.animComputeKernel = null;
|
|
4302
3823
|
this.animComputeNeedsRebuild = false;
|
|
3824
|
+
// Capacities the active kernel was built with — used to gate the in-place upload path on resync.
|
|
3825
|
+
this.animKernelBudgets = null;
|
|
4303
3826
|
this.animClipTableOffset = 0;
|
|
4304
3827
|
this.animChannelTableOffset = 0;
|
|
4305
3828
|
this.animJointLookupOffset = 0;
|
|
@@ -4307,27 +3830,37 @@ var WebGPU3DRenderer = class extends import_base_3d_renderer.Base3DRenderer {
|
|
|
4307
3830
|
this.skinnedStaticDirty = false;
|
|
4308
3831
|
this.skinnedAnimStates = [];
|
|
4309
3832
|
this.nextBoneOffset = 0;
|
|
3833
|
+
/** Reusable bone-offset blocks per skinIndex. Pushed on remove, popped on add. Indexed by skinIndex (low cardinality), so a Map of lists is fine. */
|
|
3834
|
+
this.freedBoneOffsets = /* @__PURE__ */ new Map();
|
|
4310
3835
|
// Frustum planes (6 planes × 4 floats each), extracted from VP matrix
|
|
4311
3836
|
this.frustumPlanes = new Float32Array(24);
|
|
4312
3837
|
this.uniformData = new Float32Array(24);
|
|
4313
3838
|
// mat4x4 (16) + alpha (1) + lightDir (3) + padding (4)
|
|
4314
3839
|
this.lastRenderTime = 0;
|
|
4315
3840
|
this.camera = new Camera3D();
|
|
4316
|
-
this.
|
|
4317
|
-
|
|
3841
|
+
this._prefabs = options.prefabs ?? null;
|
|
3842
|
+
const SKINNED_PARTS_PER_INSTANCE_DEFAULT_CAP = 3;
|
|
3843
|
+
const bucketStats = this._prefabs ? computeBucketStats(this._prefabs) : null;
|
|
3844
|
+
this.maxSkinnedInstances = options.maxSkinnedInstances ?? (bucketStats ? resolvedMaxInstances * Math.max(1, Math.min(bucketStats.maxSkinnedParts, SKINNED_PARTS_PER_INSTANCE_DEFAULT_CAP)) : 5e3);
|
|
3845
|
+
this.maxBonesPerSkin = options.maxBonesPerSkin ?? (bucketStats ? Math.max(1, bucketStats.maxJointCount) : 64);
|
|
3846
|
+
const cullDist = options.animationCullDistance ?? 50;
|
|
3847
|
+
this.animationCullDistanceSq = cullDist * cullDist;
|
|
4318
3848
|
this.maxTotalBones = this.maxSkinnedInstances * this.maxBonesPerSkin * 2;
|
|
4319
3849
|
this.updatedBoneOffsets = new Uint8Array(this.maxTotalBones);
|
|
4320
|
-
this.
|
|
4321
|
-
this.
|
|
4322
|
-
this.
|
|
4323
|
-
this.
|
|
4324
|
-
this.
|
|
4325
|
-
this.
|
|
3850
|
+
this.boneOffsetRefcount = new Uint32Array(this.maxTotalBones);
|
|
3851
|
+
this.boneOffsetSkinIndex = new Uint32Array(this.maxTotalBones);
|
|
3852
|
+
this.freeList = new import_free_list3.FreeList(resolvedMaxInstances);
|
|
3853
|
+
this.batcher = new import_sparse_batcher2.SparseBatcher(resolvedMaxInstances);
|
|
3854
|
+
this.dynamicData = new Float32Array(resolvedMaxInstances * DYNAMIC_MESH_FLOATS);
|
|
3855
|
+
this.staticData = new Float32Array(resolvedMaxInstances * STATIC_MESH_FLOATS);
|
|
3856
|
+
this.slotIndexData = new Uint32Array(resolvedMaxInstances);
|
|
3857
|
+
this.instanceModelIds = new Uint8Array(resolvedMaxInstances);
|
|
4326
3858
|
const msi = this.maxSkinnedInstances;
|
|
4327
3859
|
this.skinnedFreeList = new import_free_list3.FreeList(msi);
|
|
4328
3860
|
this.skinnedBatcher = new import_sparse_batcher2.SparseBatcher(msi);
|
|
4329
3861
|
this.skinnedDynamicData = new Float32Array(msi * DYNAMIC_MESH_FLOATS);
|
|
4330
3862
|
this.skinnedStaticData = new Float32Array(msi * SKINNED_STATIC_MESH_FLOATS);
|
|
3863
|
+
this.skinnedStaticDV = new DataView(this.skinnedStaticData.buffer);
|
|
4331
3864
|
this.skinnedSlotIndexData = new Uint32Array(msi);
|
|
4332
3865
|
this.skinnedInstanceModelIds = new Uint8Array(msi);
|
|
4333
3866
|
this.skinnedInstanceBoneOffsets = new Uint32Array(msi);
|
|
@@ -4338,7 +3871,19 @@ var WebGPU3DRenderer = class extends import_base_3d_renderer.Base3DRenderer {
|
|
|
4338
3871
|
this.gpuInstDV = new DataView(this.gpuInstData.buffer);
|
|
4339
3872
|
}
|
|
4340
3873
|
async init() {
|
|
4341
|
-
|
|
3874
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
3875
|
+
if (!adapter)
|
|
3876
|
+
throw new Error("WebGPU3DRenderer: no GPU adapter available");
|
|
3877
|
+
const a = adapter.limits;
|
|
3878
|
+
const requiredLimits = {
|
|
3879
|
+
maxBufferSize: a.maxBufferSize,
|
|
3880
|
+
maxStorageBufferBindingSize: a.maxStorageBufferBindingSize,
|
|
3881
|
+
maxStorageBuffersPerShaderStage: a.maxStorageBuffersPerShaderStage,
|
|
3882
|
+
maxComputeWorkgroupStorageSize: a.maxComputeWorkgroupStorageSize,
|
|
3883
|
+
maxComputeInvocationsPerWorkgroup: a.maxComputeInvocationsPerWorkgroup
|
|
3884
|
+
};
|
|
3885
|
+
const device = await adapter.requestDevice({ requiredLimits });
|
|
3886
|
+
this.root = import_typegpu9.default.initFromDevice({ device });
|
|
4342
3887
|
this.device = this.root.device;
|
|
4343
3888
|
this.context = this.canvas.getContext("webgpu");
|
|
4344
3889
|
this.format = navigator.gpu.getPreferredCanvasFormat();
|
|
@@ -4355,7 +3900,7 @@ var WebGPU3DRenderer = class extends import_base_3d_renderer.Base3DRenderer {
|
|
|
4355
3900
|
format: "depth24plus",
|
|
4356
3901
|
usage: GPUTextureUsage.RENDER_ATTACHMENT
|
|
4357
3902
|
});
|
|
4358
|
-
this.meshLayout = createMeshLayout(this.
|
|
3903
|
+
this.meshLayout = createMeshLayout(this.maxInstances);
|
|
4359
3904
|
const depthStencil = {
|
|
4360
3905
|
format: "depth24plus",
|
|
4361
3906
|
depthWriteEnabled: true,
|
|
@@ -4402,10 +3947,10 @@ var WebGPU3DRenderer = class extends import_base_3d_renderer.Base3DRenderer {
|
|
|
4402
3947
|
primitive,
|
|
4403
3948
|
depthStencil
|
|
4404
3949
|
});
|
|
4405
|
-
this.dynamicBuffer = this.root.createBuffer(d.arrayOf(DynamicMesh, this.
|
|
4406
|
-
this.staticBuffer = this.root.createBuffer(d.arrayOf(StaticMesh, this.
|
|
3950
|
+
this.dynamicBuffer = this.root.createBuffer(d.arrayOf(DynamicMesh, this.maxInstances)).$usage("storage");
|
|
3951
|
+
this.staticBuffer = this.root.createBuffer(d.arrayOf(StaticMesh, this.maxInstances)).$usage("storage");
|
|
4407
3952
|
this.uniformBuffer = this.root.createBuffer(MeshUniforms).$usage("uniform");
|
|
4408
|
-
this.slotIndexBuffer = this.root.createBuffer(d.arrayOf(d.u32, this.
|
|
3953
|
+
this.slotIndexBuffer = this.root.createBuffer(d.arrayOf(d.u32, this.maxInstances)).$usage("storage");
|
|
4409
3954
|
this.rawDynamicBuffer = this.root.unwrap(this.dynamicBuffer);
|
|
4410
3955
|
this.rawStaticBuffer = this.root.unwrap(this.staticBuffer);
|
|
4411
3956
|
this.rawUniformBuffer = this.root.unwrap(this.uniformBuffer);
|
|
@@ -4478,9 +4023,41 @@ var WebGPU3DRenderer = class extends import_base_3d_renderer.Base3DRenderer {
|
|
|
4478
4023
|
{ binding: 4, resource: { buffer: this.rawBoneMatrixBuffer } }
|
|
4479
4024
|
]
|
|
4480
4025
|
});
|
|
4026
|
+
if (this._prefabs) {
|
|
4027
|
+
this.uploadPrefabBucket(this._prefabs);
|
|
4028
|
+
}
|
|
4481
4029
|
this.setupResizeObserver();
|
|
4482
4030
|
this._initialized = true;
|
|
4483
4031
|
}
|
|
4032
|
+
/**
|
|
4033
|
+
* Upload every prefab in the bucket to the GPU and stash the handle on
|
|
4034
|
+
* each prefab so `bucket.get(id)` resolves to a usable model. Also
|
|
4035
|
+
* subscribes the resync coordinator to the bucket's `clips-changed`
|
|
4036
|
+
* channel for lazy load/unload.
|
|
4037
|
+
*/
|
|
4038
|
+
uploadPrefabBucket(bucket) {
|
|
4039
|
+
this.clipResync = new GltfClipResyncCoordinator(bucket);
|
|
4040
|
+
for (const prefab of bucket.entries()) {
|
|
4041
|
+
if (prefab.type === "gltf") {
|
|
4042
|
+
const beforeSkinCount = this.skinnedModels.length;
|
|
4043
|
+
const model = this.uploadParsedGltf(prefab.parsed);
|
|
4044
|
+
prefabHandles.set(prefab, model);
|
|
4045
|
+
if (this.skinnedModels.length > beforeSkinCount) {
|
|
4046
|
+
this.clipResync.registerSkin(prefab.id, beforeSkinCount);
|
|
4047
|
+
}
|
|
4048
|
+
} else if (prefab.type === "grid") {
|
|
4049
|
+
const model = this.createGrid({
|
|
4050
|
+
size: prefab.size,
|
|
4051
|
+
step: prefab.step,
|
|
4052
|
+
lineWidth: prefab.lineWidth
|
|
4053
|
+
});
|
|
4054
|
+
prefabHandles.set(prefab, model);
|
|
4055
|
+
} else if (prefab.type === "cube") {
|
|
4056
|
+
const model = this.createCube({ size: prefab.size });
|
|
4057
|
+
prefabHandles.set(prefab, model);
|
|
4058
|
+
}
|
|
4059
|
+
}
|
|
4060
|
+
}
|
|
4484
4061
|
setupResizeObserver() {
|
|
4485
4062
|
const supportsDevicePixelBox = (() => {
|
|
4486
4063
|
try {
|
|
@@ -4542,6 +4119,36 @@ var WebGPU3DRenderer = class extends import_base_3d_renderer.Base3DRenderer {
|
|
|
4542
4119
|
createCompute(name, options) {
|
|
4543
4120
|
return new ComputeBuilder(name, options, this.root);
|
|
4544
4121
|
}
|
|
4122
|
+
/**
|
|
4123
|
+
* Set the maximum distance (in world units) at which the renderer keeps
|
|
4124
|
+
* computing skeletal animation. Skinned instances farther than this are
|
|
4125
|
+
* still drawn, but with their last-computed bone matrices instead of
|
|
4126
|
+
* fresh ones, which saves GPU compute work. Their internal animation
|
|
4127
|
+
* clocks keep ticking on CPU, so when they come back into range they
|
|
4128
|
+
* resume in sync.
|
|
4129
|
+
*
|
|
4130
|
+
* Lower values trade visual smoothness on distant characters for FPS.
|
|
4131
|
+
* Pass `Infinity` to disable culling entirely (always animate).
|
|
4132
|
+
*
|
|
4133
|
+
* Safe to call any time; takes effect on the next frame.
|
|
4134
|
+
*
|
|
4135
|
+
* @param distance Max distance to animate at, in world units.
|
|
4136
|
+
*/
|
|
4137
|
+
setAnimationCullDistance(distance) {
|
|
4138
|
+
this.animationCullDistanceSq = distance * distance;
|
|
4139
|
+
}
|
|
4140
|
+
/** Current animation cull distance (in world units). See `setAnimationCullDistance`. */
|
|
4141
|
+
get animationCullDistance() {
|
|
4142
|
+
return Math.sqrt(this.animationCullDistanceSq);
|
|
4143
|
+
}
|
|
4144
|
+
/**
|
|
4145
|
+
* Max skinned instances the renderer was sized for at construction.
|
|
4146
|
+
* Independent budget from `maxInstances` since skinned characters use a
|
|
4147
|
+
* separate set of GPU buffers. Read-only.
|
|
4148
|
+
*/
|
|
4149
|
+
get maxSkinned() {
|
|
4150
|
+
return this.maxSkinnedInstances;
|
|
4151
|
+
}
|
|
4545
4152
|
/**
|
|
4546
4153
|
* Create a flat grid mesh on the XZ plane at Y=0.
|
|
4547
4154
|
*
|
|
@@ -4573,6 +4180,245 @@ var WebGPU3DRenderer = class extends import_base_3d_renderer.Base3DRenderer {
|
|
|
4573
4180
|
indices: new Uint16Array(indices)
|
|
4574
4181
|
});
|
|
4575
4182
|
}
|
|
4183
|
+
/**
|
|
4184
|
+
* Create a unit-cube mesh centered at the origin. Pass `size` to scale the
|
|
4185
|
+
* edge length, or keep size = 1 and scale at the instance level.
|
|
4186
|
+
*
|
|
4187
|
+
* ```ts
|
|
4188
|
+
* const cube = renderer.createCube();
|
|
4189
|
+
* renderer.addInstance({ model: cube, color: [1, 0.5, 0.2], scale: 2 });
|
|
4190
|
+
* ```
|
|
4191
|
+
*/
|
|
4192
|
+
createCube(opts = {}) {
|
|
4193
|
+
const h = (opts.size ?? 1) / 2;
|
|
4194
|
+
const positions = new Float32Array([
|
|
4195
|
+
// +Z
|
|
4196
|
+
-h,
|
|
4197
|
+
-h,
|
|
4198
|
+
h,
|
|
4199
|
+
h,
|
|
4200
|
+
-h,
|
|
4201
|
+
h,
|
|
4202
|
+
h,
|
|
4203
|
+
h,
|
|
4204
|
+
h,
|
|
4205
|
+
-h,
|
|
4206
|
+
-h,
|
|
4207
|
+
h,
|
|
4208
|
+
h,
|
|
4209
|
+
h,
|
|
4210
|
+
h,
|
|
4211
|
+
-h,
|
|
4212
|
+
h,
|
|
4213
|
+
h,
|
|
4214
|
+
// -Z
|
|
4215
|
+
h,
|
|
4216
|
+
-h,
|
|
4217
|
+
-h,
|
|
4218
|
+
-h,
|
|
4219
|
+
-h,
|
|
4220
|
+
-h,
|
|
4221
|
+
-h,
|
|
4222
|
+
h,
|
|
4223
|
+
-h,
|
|
4224
|
+
h,
|
|
4225
|
+
-h,
|
|
4226
|
+
-h,
|
|
4227
|
+
-h,
|
|
4228
|
+
h,
|
|
4229
|
+
-h,
|
|
4230
|
+
h,
|
|
4231
|
+
h,
|
|
4232
|
+
-h,
|
|
4233
|
+
// +Y
|
|
4234
|
+
-h,
|
|
4235
|
+
h,
|
|
4236
|
+
h,
|
|
4237
|
+
h,
|
|
4238
|
+
h,
|
|
4239
|
+
h,
|
|
4240
|
+
h,
|
|
4241
|
+
h,
|
|
4242
|
+
-h,
|
|
4243
|
+
-h,
|
|
4244
|
+
h,
|
|
4245
|
+
h,
|
|
4246
|
+
h,
|
|
4247
|
+
h,
|
|
4248
|
+
-h,
|
|
4249
|
+
-h,
|
|
4250
|
+
h,
|
|
4251
|
+
-h,
|
|
4252
|
+
// -Y
|
|
4253
|
+
-h,
|
|
4254
|
+
-h,
|
|
4255
|
+
-h,
|
|
4256
|
+
h,
|
|
4257
|
+
-h,
|
|
4258
|
+
-h,
|
|
4259
|
+
h,
|
|
4260
|
+
-h,
|
|
4261
|
+
h,
|
|
4262
|
+
-h,
|
|
4263
|
+
-h,
|
|
4264
|
+
-h,
|
|
4265
|
+
h,
|
|
4266
|
+
-h,
|
|
4267
|
+
h,
|
|
4268
|
+
-h,
|
|
4269
|
+
-h,
|
|
4270
|
+
h,
|
|
4271
|
+
// +X
|
|
4272
|
+
h,
|
|
4273
|
+
-h,
|
|
4274
|
+
h,
|
|
4275
|
+
h,
|
|
4276
|
+
-h,
|
|
4277
|
+
-h,
|
|
4278
|
+
h,
|
|
4279
|
+
h,
|
|
4280
|
+
-h,
|
|
4281
|
+
h,
|
|
4282
|
+
-h,
|
|
4283
|
+
h,
|
|
4284
|
+
h,
|
|
4285
|
+
h,
|
|
4286
|
+
-h,
|
|
4287
|
+
h,
|
|
4288
|
+
h,
|
|
4289
|
+
h,
|
|
4290
|
+
// -X
|
|
4291
|
+
-h,
|
|
4292
|
+
-h,
|
|
4293
|
+
-h,
|
|
4294
|
+
-h,
|
|
4295
|
+
-h,
|
|
4296
|
+
h,
|
|
4297
|
+
-h,
|
|
4298
|
+
h,
|
|
4299
|
+
h,
|
|
4300
|
+
-h,
|
|
4301
|
+
-h,
|
|
4302
|
+
-h,
|
|
4303
|
+
-h,
|
|
4304
|
+
h,
|
|
4305
|
+
h,
|
|
4306
|
+
-h,
|
|
4307
|
+
h,
|
|
4308
|
+
-h
|
|
4309
|
+
]);
|
|
4310
|
+
const normals = new Float32Array([
|
|
4311
|
+
0,
|
|
4312
|
+
0,
|
|
4313
|
+
1,
|
|
4314
|
+
0,
|
|
4315
|
+
0,
|
|
4316
|
+
1,
|
|
4317
|
+
0,
|
|
4318
|
+
0,
|
|
4319
|
+
1,
|
|
4320
|
+
0,
|
|
4321
|
+
0,
|
|
4322
|
+
1,
|
|
4323
|
+
0,
|
|
4324
|
+
0,
|
|
4325
|
+
1,
|
|
4326
|
+
0,
|
|
4327
|
+
0,
|
|
4328
|
+
1,
|
|
4329
|
+
0,
|
|
4330
|
+
0,
|
|
4331
|
+
-1,
|
|
4332
|
+
0,
|
|
4333
|
+
0,
|
|
4334
|
+
-1,
|
|
4335
|
+
0,
|
|
4336
|
+
0,
|
|
4337
|
+
-1,
|
|
4338
|
+
0,
|
|
4339
|
+
0,
|
|
4340
|
+
-1,
|
|
4341
|
+
0,
|
|
4342
|
+
0,
|
|
4343
|
+
-1,
|
|
4344
|
+
0,
|
|
4345
|
+
0,
|
|
4346
|
+
-1,
|
|
4347
|
+
0,
|
|
4348
|
+
1,
|
|
4349
|
+
0,
|
|
4350
|
+
0,
|
|
4351
|
+
1,
|
|
4352
|
+
0,
|
|
4353
|
+
0,
|
|
4354
|
+
1,
|
|
4355
|
+
0,
|
|
4356
|
+
0,
|
|
4357
|
+
1,
|
|
4358
|
+
0,
|
|
4359
|
+
0,
|
|
4360
|
+
1,
|
|
4361
|
+
0,
|
|
4362
|
+
0,
|
|
4363
|
+
1,
|
|
4364
|
+
0,
|
|
4365
|
+
0,
|
|
4366
|
+
-1,
|
|
4367
|
+
0,
|
|
4368
|
+
0,
|
|
4369
|
+
-1,
|
|
4370
|
+
0,
|
|
4371
|
+
0,
|
|
4372
|
+
-1,
|
|
4373
|
+
0,
|
|
4374
|
+
0,
|
|
4375
|
+
-1,
|
|
4376
|
+
0,
|
|
4377
|
+
0,
|
|
4378
|
+
-1,
|
|
4379
|
+
0,
|
|
4380
|
+
0,
|
|
4381
|
+
-1,
|
|
4382
|
+
0,
|
|
4383
|
+
1,
|
|
4384
|
+
0,
|
|
4385
|
+
0,
|
|
4386
|
+
1,
|
|
4387
|
+
0,
|
|
4388
|
+
0,
|
|
4389
|
+
1,
|
|
4390
|
+
0,
|
|
4391
|
+
0,
|
|
4392
|
+
1,
|
|
4393
|
+
0,
|
|
4394
|
+
0,
|
|
4395
|
+
1,
|
|
4396
|
+
0,
|
|
4397
|
+
0,
|
|
4398
|
+
1,
|
|
4399
|
+
0,
|
|
4400
|
+
0,
|
|
4401
|
+
-1,
|
|
4402
|
+
0,
|
|
4403
|
+
0,
|
|
4404
|
+
-1,
|
|
4405
|
+
0,
|
|
4406
|
+
0,
|
|
4407
|
+
-1,
|
|
4408
|
+
0,
|
|
4409
|
+
0,
|
|
4410
|
+
-1,
|
|
4411
|
+
0,
|
|
4412
|
+
0,
|
|
4413
|
+
-1,
|
|
4414
|
+
0,
|
|
4415
|
+
0,
|
|
4416
|
+
-1,
|
|
4417
|
+
0,
|
|
4418
|
+
0
|
|
4419
|
+
]);
|
|
4420
|
+
return this.loadModel({ positions, normals });
|
|
4421
|
+
}
|
|
4576
4422
|
/**
|
|
4577
4423
|
* Register a model. Returns a handle for addInstance().
|
|
4578
4424
|
*
|
|
@@ -4814,108 +4660,20 @@ var WebGPU3DRenderer = class extends import_base_3d_renderer.Base3DRenderer {
|
|
|
4814
4660
|
* ```
|
|
4815
4661
|
*/
|
|
4816
4662
|
async loadGltf(url, opts) {
|
|
4817
|
-
const
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
const chunkType = new Uint32Array(arrayBuffer, offset + 4, 1)[0];
|
|
4828
|
-
offset += 8;
|
|
4829
|
-
if (chunkType === 1313821514) {
|
|
4830
|
-
const jsonBytes = new Uint8Array(arrayBuffer, offset, chunkLength);
|
|
4831
|
-
gltf = JSON.parse(new TextDecoder().decode(jsonBytes));
|
|
4832
|
-
} else if (chunkType === 5130562) {
|
|
4833
|
-
glbBinaryChunk = arrayBuffer.slice(offset, offset + chunkLength);
|
|
4834
|
-
}
|
|
4835
|
-
offset += chunkLength;
|
|
4836
|
-
}
|
|
4837
|
-
if (!gltf)
|
|
4838
|
-
throw new Error(`Invalid GLB: no JSON chunk in ${url}`);
|
|
4839
|
-
} else {
|
|
4840
|
-
gltf = JSON.parse(new TextDecoder().decode(arrayBuffer));
|
|
4841
|
-
}
|
|
4842
|
-
if (!gltf.meshes?.length)
|
|
4843
|
-
throw new Error(`No meshes found in ${url}`);
|
|
4844
|
-
const buffers = [];
|
|
4845
|
-
for (let i = 0; i < (gltf.buffers?.length ?? 0); i++) {
|
|
4846
|
-
const buf = gltf.buffers[i];
|
|
4847
|
-
if (glbBinaryChunk && (!buf.uri || buf.uri === "")) {
|
|
4848
|
-
buffers.push(glbBinaryChunk);
|
|
4849
|
-
} else if (buf.uri) {
|
|
4850
|
-
const r = await fetch(baseUrl + buf.uri);
|
|
4851
|
-
buffers.push(await r.arrayBuffer());
|
|
4852
|
-
}
|
|
4853
|
-
}
|
|
4854
|
-
const getAccessorData = (accessorIndex) => {
|
|
4855
|
-
const accessor = gltf.accessors[accessorIndex];
|
|
4856
|
-
const bufferView = gltf.bufferViews[accessor.bufferView];
|
|
4857
|
-
const buffer = buffers[bufferView.buffer];
|
|
4858
|
-
const typeMap = { 5120: Int8Array, 5121: Uint8Array, 5122: Int16Array, 5123: Uint16Array, 5125: Uint32Array, 5126: Float32Array };
|
|
4859
|
-
const byteSizeMap = { 5120: 1, 5121: 1, 5122: 2, 5123: 2, 5125: 4, 5126: 4 };
|
|
4860
|
-
const sizeMap = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4, MAT2: 4, MAT3: 9, MAT4: 16 };
|
|
4861
|
-
const TypedArray = typeMap[accessor.componentType];
|
|
4862
|
-
const componentBytes = byteSizeMap[accessor.componentType];
|
|
4863
|
-
const elementSize = sizeMap[accessor.type] ?? 1;
|
|
4864
|
-
const baseOffset = (bufferView.byteOffset ?? 0) + (accessor.byteOffset ?? 0);
|
|
4865
|
-
const stride = bufferView.byteStride ?? componentBytes * elementSize;
|
|
4866
|
-
const tightStride = componentBytes * elementSize;
|
|
4867
|
-
if (stride === tightStride) {
|
|
4868
|
-
const data = new TypedArray(buffer, baseOffset, accessor.count * elementSize);
|
|
4869
|
-
return { data, count: accessor.count, elementSize };
|
|
4870
|
-
}
|
|
4871
|
-
const out = new TypedArray(accessor.count * elementSize);
|
|
4872
|
-
const src = new Uint8Array(buffer);
|
|
4873
|
-
const dst = new Uint8Array(out.buffer);
|
|
4874
|
-
for (let i = 0; i < accessor.count; i++) {
|
|
4875
|
-
const srcOff = baseOffset + i * stride;
|
|
4876
|
-
const dstOff = i * tightStride;
|
|
4877
|
-
for (let b = 0; b < tightStride; b++) {
|
|
4878
|
-
dst[dstOff + b] = src[srcOff + b];
|
|
4879
|
-
}
|
|
4880
|
-
}
|
|
4881
|
-
return { data: out, count: accessor.count, elementSize };
|
|
4882
|
-
};
|
|
4883
|
-
const textureCache = /* @__PURE__ */ new Map();
|
|
4884
|
-
const loadTexture = async (imageIndex) => {
|
|
4885
|
-
if (textureCache.has(imageIndex))
|
|
4886
|
-
return textureCache.get(imageIndex);
|
|
4887
|
-
const image = gltf.images?.[imageIndex];
|
|
4888
|
-
if (!image)
|
|
4889
|
-
return void 0;
|
|
4890
|
-
let blob;
|
|
4891
|
-
if (image.bufferView !== void 0) {
|
|
4892
|
-
const bv = gltf.bufferViews[image.bufferView];
|
|
4893
|
-
const buf = buffers[bv.buffer];
|
|
4894
|
-
const data = new Uint8Array(buf, bv.byteOffset ?? 0, bv.byteLength);
|
|
4895
|
-
blob = new Blob([data], { type: image.mimeType ?? "image/png" });
|
|
4896
|
-
} else if (image.uri) {
|
|
4897
|
-
const imgUrl = image.uri.startsWith("data:") ? image.uri : baseUrl + image.uri;
|
|
4898
|
-
blob = await (await fetch(imgUrl)).blob();
|
|
4899
|
-
}
|
|
4900
|
-
if (blob) {
|
|
4901
|
-
const bmp = await createImageBitmap(blob);
|
|
4902
|
-
textureCache.set(imageIndex, bmp);
|
|
4903
|
-
return bmp;
|
|
4904
|
-
}
|
|
4905
|
-
return void 0;
|
|
4906
|
-
};
|
|
4907
|
-
const meshNodeIndex = gltf.nodes?.findIndex((n) => n.mesh !== void 0) ?? -1;
|
|
4908
|
-
const skinIndex = meshNodeIndex !== -1 ? gltf.nodes?.[meshNodeIndex]?.skin : void 0;
|
|
4909
|
-
let skinData = null;
|
|
4910
|
-
let animClips = [];
|
|
4663
|
+
const parsed = await (0, import_renderer5.parseGltf)(url, opts);
|
|
4664
|
+
return this.uploadParsedGltf(parsed);
|
|
4665
|
+
}
|
|
4666
|
+
/**
|
|
4667
|
+
* Upload a previously-parsed glTF to the GPU. Returns a GltfModel handle.
|
|
4668
|
+
* Splitting parse (CPU) from upload (GPU) lets callers parse models in parallel
|
|
4669
|
+
* before a renderer exists and inspect joint counts / vertex totals to size the
|
|
4670
|
+
* renderer appropriately.
|
|
4671
|
+
*/
|
|
4672
|
+
uploadParsedGltf(parsed) {
|
|
4911
4673
|
let skinnedModelSkinIndex = -1;
|
|
4912
|
-
if (
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
if (opts?.animations) {
|
|
4916
|
-
animClips = animClips.filter((clip) => opts.animations.includes(clip.name));
|
|
4917
|
-
}
|
|
4918
|
-
const animation = new SkeletalAnimation(skinData, animClips, gltf.nodes);
|
|
4674
|
+
if (parsed.skin) {
|
|
4675
|
+
const { data: skinData, animClips } = parsed.skin;
|
|
4676
|
+
const animation = new import_renderer5.SkeletalAnimation(skinData, animClips);
|
|
4919
4677
|
const ibm = skinData.inverseBindMatrices;
|
|
4920
4678
|
let maxRadSq = 0;
|
|
4921
4679
|
for (let j = 0; j < skinData.jointCount; j++) {
|
|
@@ -4929,91 +4687,34 @@ var WebGPU3DRenderer = class extends import_base_3d_renderer.Base3DRenderer {
|
|
|
4929
4687
|
this.skinnedModels.push({
|
|
4930
4688
|
animation,
|
|
4931
4689
|
jointCount: skinData.jointCount,
|
|
4932
|
-
boundingRadius: skinnedRadius
|
|
4690
|
+
boundingRadius: skinnedRadius,
|
|
4691
|
+
parsedSkin: parsed.skin
|
|
4933
4692
|
});
|
|
4934
|
-
packSkinAndAnimations(this.packedAnimData, skinData, animClips
|
|
4693
|
+
(0, import_renderer5.packSkinAndAnimations)(this.packedAnimData, skinData, animClips);
|
|
4935
4694
|
this.animComputeNeedsRebuild = true;
|
|
4936
4695
|
}
|
|
4937
4696
|
const handles = [];
|
|
4938
|
-
const
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
const positions = new Float32Array(posAccess.data);
|
|
4960
|
-
let normals;
|
|
4961
|
-
if (primitive.attributes.NORMAL !== void 0) {
|
|
4962
|
-
normals = new Float32Array(getAccessorData(primitive.attributes.NORMAL).data);
|
|
4963
|
-
}
|
|
4964
|
-
let uvs;
|
|
4965
|
-
if (primitive.attributes.TEXCOORD_0 !== void 0) {
|
|
4966
|
-
uvs = new Float32Array(getAccessorData(primitive.attributes.TEXCOORD_0).data);
|
|
4967
|
-
}
|
|
4968
|
-
let indices;
|
|
4969
|
-
if (primitive.indices !== void 0) {
|
|
4970
|
-
const idxAccess = getAccessorData(primitive.indices);
|
|
4971
|
-
indices = idxAccess.data.length > 65535 ? new Uint32Array(idxAccess.data) : new Uint16Array(idxAccess.data);
|
|
4972
|
-
}
|
|
4973
|
-
if (thisMeshNodeMatrix && !isSkinned) {
|
|
4974
|
-
const meshNodeMatrix = thisMeshNodeMatrix;
|
|
4975
|
-
const mm = meshNodeMatrix;
|
|
4976
|
-
const vertexCount = positions.length / 3;
|
|
4977
|
-
for (let v = 0; v < vertexCount; v++) {
|
|
4978
|
-
const o = v * 3;
|
|
4979
|
-
const px = positions[o], py = positions[o + 1], pz = positions[o + 2];
|
|
4980
|
-
positions[o] = mm[0] * px + mm[4] * py + mm[8] * pz + mm[12];
|
|
4981
|
-
positions[o + 1] = mm[1] * px + mm[5] * py + mm[9] * pz + mm[13];
|
|
4982
|
-
positions[o + 2] = mm[2] * px + mm[6] * py + mm[10] * pz + mm[14];
|
|
4983
|
-
if (normals) {
|
|
4984
|
-
const nx = normals[o], ny = normals[o + 1], nz = normals[o + 2];
|
|
4985
|
-
const tnx = mm[0] * nx + mm[4] * ny + mm[8] * nz;
|
|
4986
|
-
const tny = mm[1] * nx + mm[5] * ny + mm[9] * nz;
|
|
4987
|
-
const tnz = mm[2] * nx + mm[6] * ny + mm[10] * nz;
|
|
4988
|
-
const len = Math.sqrt(tnx * tnx + tny * tny + tnz * tnz);
|
|
4989
|
-
if (len > 0) {
|
|
4990
|
-
normals[o] = tnx / len;
|
|
4991
|
-
normals[o + 1] = tny / len;
|
|
4992
|
-
normals[o + 2] = tnz / len;
|
|
4993
|
-
}
|
|
4994
|
-
}
|
|
4995
|
-
}
|
|
4996
|
-
}
|
|
4997
|
-
let texture;
|
|
4998
|
-
if (primitive.material !== void 0) {
|
|
4999
|
-
const material = gltf.materials?.[primitive.material];
|
|
5000
|
-
const texIndex = material?.pbrMetallicRoughness?.baseColorTexture?.index;
|
|
5001
|
-
if (texIndex !== void 0 && gltf.textures?.[texIndex]) {
|
|
5002
|
-
texture = await loadTexture(gltf.textures[texIndex].source);
|
|
5003
|
-
}
|
|
5004
|
-
}
|
|
5005
|
-
if (isSkinned) {
|
|
5006
|
-
const skinAttrs = parsePrimitiveSkinAttributes(primitive, getAccessorData);
|
|
5007
|
-
if (skinAttrs) {
|
|
5008
|
-
handles.push(this.loadSkinnedModel(
|
|
5009
|
-
{ positions, normals, uvs, indices, texture },
|
|
5010
|
-
skinAttrs,
|
|
5011
|
-
skinnedModelSkinIndex
|
|
5012
|
-
));
|
|
5013
|
-
continue;
|
|
5014
|
-
}
|
|
5015
|
-
}
|
|
5016
|
-
handles.push(this.loadModel({ positions, normals, uvs, indices, texture }));
|
|
4697
|
+
for (const prim of parsed.primitives) {
|
|
4698
|
+
if (prim.skinned && prim.skinAttrs) {
|
|
4699
|
+
handles.push(this.loadSkinnedModel(
|
|
4700
|
+
{
|
|
4701
|
+
positions: prim.positions,
|
|
4702
|
+
normals: prim.normals,
|
|
4703
|
+
uvs: prim.uvs,
|
|
4704
|
+
indices: prim.indices,
|
|
4705
|
+
texture: prim.texture
|
|
4706
|
+
},
|
|
4707
|
+
prim.skinAttrs,
|
|
4708
|
+
skinnedModelSkinIndex
|
|
4709
|
+
));
|
|
4710
|
+
} else {
|
|
4711
|
+
handles.push(this.loadModel({
|
|
4712
|
+
positions: prim.positions,
|
|
4713
|
+
normals: prim.normals,
|
|
4714
|
+
uvs: prim.uvs,
|
|
4715
|
+
indices: prim.indices,
|
|
4716
|
+
texture: prim.texture
|
|
4717
|
+
}));
|
|
5017
4718
|
}
|
|
5018
4719
|
}
|
|
5019
4720
|
let totalVertexCount = 0;
|
|
@@ -5025,7 +4726,7 @@ var WebGPU3DRenderer = class extends import_base_3d_renderer.Base3DRenderer {
|
|
|
5025
4726
|
totalVertexCount,
|
|
5026
4727
|
skinned: handles.some((h) => h.skinned),
|
|
5027
4728
|
animations: animNames,
|
|
5028
|
-
src:
|
|
4729
|
+
src: parsed.src
|
|
5029
4730
|
};
|
|
5030
4731
|
}
|
|
5031
4732
|
/**
|
|
@@ -5033,7 +4734,10 @@ var WebGPU3DRenderer = class extends import_base_3d_renderer.Base3DRenderer {
|
|
|
5033
4734
|
* with another instance (e.g., when spawning all parts of a character).
|
|
5034
4735
|
*/
|
|
5035
4736
|
addInstance(opts) {
|
|
5036
|
-
|
|
4737
|
+
if (isPrefab3D(opts.model) && opts.model.type === "composite") {
|
|
4738
|
+
return this.addCompositeInstance(opts, opts.model);
|
|
4739
|
+
}
|
|
4740
|
+
const modelOrGltf = isPrefab3D(opts.model) ? resolvePrefabHandle(opts.model) : opts.model;
|
|
5037
4741
|
if ("parts" in modelOrGltf) {
|
|
5038
4742
|
return this.addGltfInstance(opts, modelOrGltf);
|
|
5039
4743
|
}
|
|
@@ -5044,36 +4748,38 @@ var WebGPU3DRenderer = class extends import_base_3d_renderer.Base3DRenderer {
|
|
|
5044
4748
|
}
|
|
5045
4749
|
const slot = this.freeList.allocate();
|
|
5046
4750
|
if (slot === -1)
|
|
5047
|
-
throw new Error(`Max instances (${this.
|
|
4751
|
+
throw new Error(`Max instances (${this.maxInstances}) reached`);
|
|
5048
4752
|
const dynBase = slot * DYNAMIC_MESH_FLOATS;
|
|
5049
4753
|
const statBase = slot * STATIC_MESH_FLOATS;
|
|
5050
|
-
const
|
|
5051
|
-
this.dynamicData[dynBase + DYN_PREV_PX] =
|
|
5052
|
-
this.dynamicData[dynBase + DYN_PREV_PY] =
|
|
5053
|
-
this.dynamicData[dynBase + DYN_PREV_PZ] =
|
|
5054
|
-
this.dynamicData[dynBase + DYN_CURR_PX] =
|
|
5055
|
-
this.dynamicData[dynBase + DYN_CURR_PY] =
|
|
5056
|
-
this.dynamicData[dynBase + DYN_CURR_PZ] =
|
|
5057
|
-
|
|
5058
|
-
this.dynamicData[dynBase +
|
|
5059
|
-
this.dynamicData[dynBase +
|
|
5060
|
-
this.dynamicData[dynBase +
|
|
5061
|
-
this.dynamicData[dynBase +
|
|
5062
|
-
this.dynamicData[dynBase +
|
|
5063
|
-
this.
|
|
5064
|
-
this.staticData[statBase +
|
|
5065
|
-
this.staticData[statBase +
|
|
5066
|
-
this.staticData[statBase +
|
|
5067
|
-
|
|
5068
|
-
this.staticData[statBase +
|
|
5069
|
-
this.staticData[statBase + STAT_CG] = color[1];
|
|
5070
|
-
this.staticData[statBase + STAT_CB] = color[2];
|
|
4754
|
+
const t = resolveTransform(opts);
|
|
4755
|
+
this.dynamicData[dynBase + DYN_PREV_PX] = t.px;
|
|
4756
|
+
this.dynamicData[dynBase + DYN_PREV_PY] = t.py;
|
|
4757
|
+
this.dynamicData[dynBase + DYN_PREV_PZ] = t.pz;
|
|
4758
|
+
this.dynamicData[dynBase + DYN_CURR_PX] = t.px;
|
|
4759
|
+
this.dynamicData[dynBase + DYN_CURR_PY] = t.py;
|
|
4760
|
+
this.dynamicData[dynBase + DYN_CURR_PZ] = t.pz;
|
|
4761
|
+
this.dynamicData[dynBase + DYN_PREV_RX] = t.rx;
|
|
4762
|
+
this.dynamicData[dynBase + DYN_PREV_RY] = t.ry;
|
|
4763
|
+
this.dynamicData[dynBase + DYN_PREV_RZ] = t.rz;
|
|
4764
|
+
this.dynamicData[dynBase + DYN_CURR_RX] = t.rx;
|
|
4765
|
+
this.dynamicData[dynBase + DYN_CURR_RY] = t.ry;
|
|
4766
|
+
this.dynamicData[dynBase + DYN_CURR_RZ] = t.rz;
|
|
4767
|
+
this.staticData[statBase + STAT_SX] = t.sx;
|
|
4768
|
+
this.staticData[statBase + STAT_SY] = t.sy;
|
|
4769
|
+
this.staticData[statBase + STAT_SZ] = t.sz;
|
|
4770
|
+
this.staticData[statBase + STAT_CR] = t.cr;
|
|
4771
|
+
this.staticData[statBase + STAT_CG] = t.cg;
|
|
4772
|
+
this.staticData[statBase + STAT_CB] = t.cb;
|
|
5071
4773
|
this.staticDirty = true;
|
|
5072
4774
|
this.instanceModelIds[slot] = modelHandle.id;
|
|
5073
4775
|
this.batcher.add(0, modelHandle.id, slot);
|
|
5074
4776
|
const dynamicData = this.dynamicData;
|
|
5075
4777
|
const staticData = this.staticData;
|
|
5076
|
-
|
|
4778
|
+
const self = this;
|
|
4779
|
+
let destroyed = false;
|
|
4780
|
+
const handle = {
|
|
4781
|
+
slot,
|
|
4782
|
+
modelId: modelHandle.id,
|
|
5077
4783
|
skinned: false,
|
|
5078
4784
|
setPosition(nx, ny, nz) {
|
|
5079
4785
|
dynamicData[dynBase + DYN_CURR_PX] = nx;
|
|
@@ -5089,8 +4795,19 @@ var WebGPU3DRenderer = class extends import_base_3d_renderer.Base3DRenderer {
|
|
|
5089
4795
|
staticData[statBase + STAT_SX] = nx;
|
|
5090
4796
|
staticData[statBase + STAT_SY] = ny;
|
|
5091
4797
|
staticData[statBase + STAT_SZ] = nz;
|
|
4798
|
+
},
|
|
4799
|
+
destroy() {
|
|
4800
|
+
if (destroyed)
|
|
4801
|
+
return;
|
|
4802
|
+
destroyed = true;
|
|
4803
|
+
self.batcher.remove(0, modelHandle.id, slot);
|
|
4804
|
+
self.freeList.free(slot);
|
|
4805
|
+
dynamicData.fill(0, dynBase, dynBase + DYNAMIC_MESH_FLOATS);
|
|
4806
|
+
staticData.fill(0, statBase, statBase + STATIC_MESH_FLOATS);
|
|
4807
|
+
self.staticDirty = true;
|
|
5092
4808
|
}
|
|
5093
4809
|
};
|
|
4810
|
+
return handle;
|
|
5094
4811
|
}
|
|
5095
4812
|
addGltfInstance(opts, gltf) {
|
|
5096
4813
|
const childHandles = [];
|
|
@@ -5128,7 +4845,71 @@ var WebGPU3DRenderer = class extends import_base_3d_renderer.Base3DRenderer {
|
|
|
5128
4845
|
} : void 0,
|
|
5129
4846
|
stop: skinnedHandle?.stop ? () => {
|
|
5130
4847
|
skinnedHandle.stop();
|
|
5131
|
-
} : void 0
|
|
4848
|
+
} : void 0,
|
|
4849
|
+
destroy() {
|
|
4850
|
+
for (const h of childHandles)
|
|
4851
|
+
h.destroy();
|
|
4852
|
+
}
|
|
4853
|
+
};
|
|
4854
|
+
}
|
|
4855
|
+
/**
|
|
4856
|
+
* Spawn a composite prefab by spawning each of its parts at the composed
|
|
4857
|
+
* (instance + offset) transform. The returned handle broadcasts subsequent
|
|
4858
|
+
* `setPosition` / `setRotation` to every child, keeping each child's
|
|
4859
|
+
* baked offset applied on top of the new value.
|
|
4860
|
+
*/
|
|
4861
|
+
addCompositeInstance(opts, composite) {
|
|
4862
|
+
const bucket = this._prefabs;
|
|
4863
|
+
if (!bucket) {
|
|
4864
|
+
throw new Error(
|
|
4865
|
+
`addInstance: composite '${composite.id}' requires the renderer to be constructed with the bucket (\`prefabs\`).`
|
|
4866
|
+
);
|
|
4867
|
+
}
|
|
4868
|
+
const basePos = opts.position ?? [0, 0, 0];
|
|
4869
|
+
const baseRot = opts.rotation ?? [0, 0, 0];
|
|
4870
|
+
const offsets = composite.parts.map((p) => ({
|
|
4871
|
+
px: p.offset?.position?.[0] ?? 0,
|
|
4872
|
+
py: p.offset?.position?.[1] ?? 0,
|
|
4873
|
+
pz: p.offset?.position?.[2] ?? 0,
|
|
4874
|
+
rx: p.offset?.rotation?.[0] ?? 0,
|
|
4875
|
+
ry: p.offset?.rotation?.[1] ?? 0,
|
|
4876
|
+
rz: p.offset?.rotation?.[2] ?? 0
|
|
4877
|
+
}));
|
|
4878
|
+
const childHandles = [];
|
|
4879
|
+
for (let i = 0; i < composite.parts.length; i++) {
|
|
4880
|
+
const part = composite.parts[i];
|
|
4881
|
+
const off = offsets[i];
|
|
4882
|
+
const partPrefab = bucket.get(part.partId);
|
|
4883
|
+
const partOpts = {
|
|
4884
|
+
...opts,
|
|
4885
|
+
model: partPrefab,
|
|
4886
|
+
position: [basePos[0] + off.px, basePos[1] + off.py, basePos[2] + off.pz],
|
|
4887
|
+
rotation: [baseRot[0] + off.rx, baseRot[1] + off.ry, baseRot[2] + off.rz]
|
|
4888
|
+
};
|
|
4889
|
+
childHandles.push(this.addInstance(partOpts));
|
|
4890
|
+
}
|
|
4891
|
+
return {
|
|
4892
|
+
skinned: childHandles.some((h) => h.skinned),
|
|
4893
|
+
setPosition(x, y, z) {
|
|
4894
|
+
for (let i = 0; i < childHandles.length; i++) {
|
|
4895
|
+
const o = offsets[i];
|
|
4896
|
+
childHandles[i].setPosition(x + o.px, y + o.py, z + o.pz);
|
|
4897
|
+
}
|
|
4898
|
+
},
|
|
4899
|
+
setRotation(x, y, z) {
|
|
4900
|
+
for (let i = 0; i < childHandles.length; i++) {
|
|
4901
|
+
const o = offsets[i];
|
|
4902
|
+
childHandles[i].setRotation(x + o.rx, y + o.ry, z + o.rz);
|
|
4903
|
+
}
|
|
4904
|
+
},
|
|
4905
|
+
setScale(x, y, z) {
|
|
4906
|
+
for (const h of childHandles)
|
|
4907
|
+
h.setScale(x, y, z);
|
|
4908
|
+
},
|
|
4909
|
+
destroy() {
|
|
4910
|
+
for (const h of childHandles)
|
|
4911
|
+
h.destroy();
|
|
4912
|
+
}
|
|
5132
4913
|
};
|
|
5133
4914
|
}
|
|
5134
4915
|
addSkinnedInstance(opts, modelHandle, skinIndex, linkedSlot) {
|
|
@@ -5142,43 +4923,53 @@ var WebGPU3DRenderer = class extends import_base_3d_renderer.Base3DRenderer {
|
|
|
5142
4923
|
if (linkedSlot !== void 0) {
|
|
5143
4924
|
boneOffset = this.skinnedInstanceBoneOffsets[linkedSlot];
|
|
5144
4925
|
animState = this.skinnedAnimStates[linkedSlot];
|
|
4926
|
+
this.boneOffsetRefcount[boneOffset]++;
|
|
5145
4927
|
} else {
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
4928
|
+
const pool = this.freedBoneOffsets.get(skinIndex);
|
|
4929
|
+
if (pool && pool.length > 0) {
|
|
4930
|
+
boneOffset = pool.pop();
|
|
4931
|
+
} else {
|
|
4932
|
+
boneOffset = this.nextBoneOffset + jointCount;
|
|
4933
|
+
this.nextBoneOffset += jointCount * 2;
|
|
4934
|
+
}
|
|
4935
|
+
this.boneOffsetRefcount[boneOffset] = 1;
|
|
4936
|
+
this.boneOffsetSkinIndex[boneOffset] = skinIndex;
|
|
4937
|
+
const restOffsetFloats = boneOffset * 16;
|
|
4938
|
+
const restLengthFloats = jointCount * 16;
|
|
4939
|
+
skinModel.animation.computeRestPose(this.boneMatrixData, restOffsetFloats);
|
|
4940
|
+
this.device.queue.writeBuffer(
|
|
4941
|
+
this.rawBoneMatrixBuffer,
|
|
4942
|
+
restOffsetFloats * 4,
|
|
4943
|
+
this.boneMatrixData.buffer,
|
|
4944
|
+
this.boneMatrixData.byteOffset + restOffsetFloats * 4,
|
|
4945
|
+
restLengthFloats * 4
|
|
4946
|
+
);
|
|
5150
4947
|
animState = skinModel.animation.clipCount > 0 ? skinModel.animation.createState(0, 1, true) : null;
|
|
5151
4948
|
}
|
|
5152
4949
|
this.skinnedInstanceBoneOffsets[slot] = boneOffset;
|
|
5153
4950
|
this.skinnedAnimStates[slot] = animState;
|
|
5154
4951
|
const dynBase = slot * DYNAMIC_MESH_FLOATS;
|
|
5155
4952
|
const statBase = slot * SKINNED_STATIC_MESH_FLOATS;
|
|
5156
|
-
const
|
|
5157
|
-
this.skinnedDynamicData[dynBase + DYN_PREV_PX] =
|
|
5158
|
-
this.skinnedDynamicData[dynBase + DYN_PREV_PY] =
|
|
5159
|
-
this.skinnedDynamicData[dynBase + DYN_PREV_PZ] =
|
|
5160
|
-
this.skinnedDynamicData[dynBase + DYN_CURR_PX] =
|
|
5161
|
-
this.skinnedDynamicData[dynBase + DYN_CURR_PY] =
|
|
5162
|
-
this.skinnedDynamicData[dynBase + DYN_CURR_PZ] =
|
|
5163
|
-
|
|
5164
|
-
this.skinnedDynamicData[dynBase +
|
|
5165
|
-
this.skinnedDynamicData[dynBase +
|
|
5166
|
-
this.skinnedDynamicData[dynBase +
|
|
5167
|
-
this.skinnedDynamicData[dynBase +
|
|
5168
|
-
this.skinnedDynamicData[dynBase +
|
|
5169
|
-
this.
|
|
5170
|
-
this.skinnedStaticData[statBase +
|
|
5171
|
-
this.skinnedStaticData[statBase +
|
|
5172
|
-
this.skinnedStaticData[statBase +
|
|
5173
|
-
|
|
5174
|
-
this.skinnedStaticData[statBase +
|
|
5175
|
-
this.
|
|
5176
|
-
this.skinnedStaticData[statBase + SSTAT_CB] = color[2];
|
|
5177
|
-
new DataView(this.skinnedStaticData.buffer).setUint32(
|
|
5178
|
-
(statBase + SSTAT_BONE_OFFSET) * 4,
|
|
5179
|
-
boneOffset,
|
|
5180
|
-
true
|
|
5181
|
-
);
|
|
4953
|
+
const t = resolveTransform(opts);
|
|
4954
|
+
this.skinnedDynamicData[dynBase + DYN_PREV_PX] = t.px;
|
|
4955
|
+
this.skinnedDynamicData[dynBase + DYN_PREV_PY] = t.py;
|
|
4956
|
+
this.skinnedDynamicData[dynBase + DYN_PREV_PZ] = t.pz;
|
|
4957
|
+
this.skinnedDynamicData[dynBase + DYN_CURR_PX] = t.px;
|
|
4958
|
+
this.skinnedDynamicData[dynBase + DYN_CURR_PY] = t.py;
|
|
4959
|
+
this.skinnedDynamicData[dynBase + DYN_CURR_PZ] = t.pz;
|
|
4960
|
+
this.skinnedDynamicData[dynBase + DYN_PREV_RX] = t.rx;
|
|
4961
|
+
this.skinnedDynamicData[dynBase + DYN_PREV_RY] = t.ry;
|
|
4962
|
+
this.skinnedDynamicData[dynBase + DYN_PREV_RZ] = t.rz;
|
|
4963
|
+
this.skinnedDynamicData[dynBase + DYN_CURR_RX] = t.rx;
|
|
4964
|
+
this.skinnedDynamicData[dynBase + DYN_CURR_RY] = t.ry;
|
|
4965
|
+
this.skinnedDynamicData[dynBase + DYN_CURR_RZ] = t.rz;
|
|
4966
|
+
this.skinnedStaticData[statBase + SSTAT_SX] = t.sx;
|
|
4967
|
+
this.skinnedStaticData[statBase + SSTAT_SY] = t.sy;
|
|
4968
|
+
this.skinnedStaticData[statBase + SSTAT_SZ] = t.sz;
|
|
4969
|
+
this.skinnedStaticData[statBase + SSTAT_CR] = t.cr;
|
|
4970
|
+
this.skinnedStaticData[statBase + SSTAT_CG] = t.cg;
|
|
4971
|
+
this.skinnedStaticData[statBase + SSTAT_CB] = t.cb;
|
|
4972
|
+
this.skinnedStaticDV.setUint32((statBase + SSTAT_BONE_OFFSET) * 4, boneOffset, true);
|
|
5182
4973
|
this.skinnedStaticDirty = true;
|
|
5183
4974
|
this.skinnedInstanceModelIds[slot] = modelHandle.id;
|
|
5184
4975
|
this.skinnedBatcher.add(0, modelHandle.id, slot);
|
|
@@ -5186,6 +4977,10 @@ var WebGPU3DRenderer = class extends import_base_3d_renderer.Base3DRenderer {
|
|
|
5186
4977
|
const staticData = this.skinnedStaticData;
|
|
5187
4978
|
const animStates = this.skinnedAnimStates;
|
|
5188
4979
|
const animation = skinModel.animation;
|
|
4980
|
+
const self = this;
|
|
4981
|
+
const capturedBoneOffset = boneOffset;
|
|
4982
|
+
const capturedSkinIndex = skinIndex;
|
|
4983
|
+
let destroyed = false;
|
|
5189
4984
|
return {
|
|
5190
4985
|
slot,
|
|
5191
4986
|
modelId: modelHandle.id,
|
|
@@ -5214,19 +5009,149 @@ var WebGPU3DRenderer = class extends import_base_3d_renderer.Base3DRenderer {
|
|
|
5214
5009
|
const state = animStates[slot];
|
|
5215
5010
|
if (state)
|
|
5216
5011
|
animation.stop(state);
|
|
5012
|
+
},
|
|
5013
|
+
destroy() {
|
|
5014
|
+
if (destroyed)
|
|
5015
|
+
return;
|
|
5016
|
+
destroyed = true;
|
|
5017
|
+
self.skinnedBatcher.remove(0, modelHandle.id, slot);
|
|
5018
|
+
self.skinnedFreeList.free(slot);
|
|
5019
|
+
dynamicData.fill(0, dynBase, dynBase + DYNAMIC_MESH_FLOATS);
|
|
5020
|
+
staticData.fill(0, statBase, statBase + SKINNED_STATIC_MESH_FLOATS);
|
|
5021
|
+
animStates[slot] = null;
|
|
5022
|
+
self.skinnedStaticDirty = true;
|
|
5023
|
+
if (--self.boneOffsetRefcount[capturedBoneOffset] === 0) {
|
|
5024
|
+
let pool = self.freedBoneOffsets.get(capturedSkinIndex);
|
|
5025
|
+
if (!pool) {
|
|
5026
|
+
pool = [];
|
|
5027
|
+
self.freedBoneOffsets.set(capturedSkinIndex, pool);
|
|
5028
|
+
}
|
|
5029
|
+
pool.push(capturedBoneOffset);
|
|
5030
|
+
}
|
|
5031
|
+
}
|
|
5032
|
+
};
|
|
5033
|
+
}
|
|
5034
|
+
/**
|
|
5035
|
+
* Drain pending resyncs from the coordinator. Per affected skin: rebuild
|
|
5036
|
+
* its `SkeletalAnimation` clip list densely, remap in-flight animStates,
|
|
5037
|
+
* then full-repack `packedAnimData`. Falls back to a kernel rebuild only
|
|
5038
|
+
* when the new data exceeds the kernel's allocated budgets.
|
|
5039
|
+
*
|
|
5040
|
+
* The dense renumbering is load-bearing: the kernel reads each clip by
|
|
5041
|
+
* its per-skin index, which must equal `SkeletalAnimation`'s clip id.
|
|
5042
|
+
*/
|
|
5043
|
+
syncLazyAnimationChanges() {
|
|
5044
|
+
const resync = this.clipResync;
|
|
5045
|
+
if (!resync || resync.pending.size === 0)
|
|
5046
|
+
return;
|
|
5047
|
+
const remapsBySkin = /* @__PURE__ */ new Map();
|
|
5048
|
+
for (const skinIndex of resync.pending) {
|
|
5049
|
+
const sm = this.skinnedModels[skinIndex];
|
|
5050
|
+
if (!sm)
|
|
5051
|
+
continue;
|
|
5052
|
+
remapsBySkin.set(skinIndex, sm.animation.replaceClips(sm.parsedSkin.animClips));
|
|
5053
|
+
}
|
|
5054
|
+
resync.clear();
|
|
5055
|
+
for (let slot = 0; slot < this.maxSkinnedInstances; slot++) {
|
|
5056
|
+
const animState = this.skinnedAnimStates[slot];
|
|
5057
|
+
if (!animState)
|
|
5058
|
+
continue;
|
|
5059
|
+
const modelId = this.skinnedInstanceModelIds[slot];
|
|
5060
|
+
const model = this.models[modelId];
|
|
5061
|
+
if (!model || model.skinIndex < 0)
|
|
5062
|
+
continue;
|
|
5063
|
+
const remap3 = remapsBySkin.get(model.skinIndex);
|
|
5064
|
+
if (!remap3)
|
|
5065
|
+
continue;
|
|
5066
|
+
if (animState.clipId >= 0 && animState.clipId < remap3.length) {
|
|
5067
|
+
const next = remap3[animState.clipId];
|
|
5068
|
+
if (next < 0) {
|
|
5069
|
+
animState.clipId = -1;
|
|
5070
|
+
animState.playing = false;
|
|
5071
|
+
} else {
|
|
5072
|
+
animState.clipId = next;
|
|
5073
|
+
}
|
|
5074
|
+
}
|
|
5075
|
+
if (animState.prevClipId >= 0 && animState.prevClipId < remap3.length) {
|
|
5076
|
+
animState.prevClipId = remap3[animState.prevClipId];
|
|
5217
5077
|
}
|
|
5078
|
+
}
|
|
5079
|
+
this.packedAnimData = (0, import_renderer5.createPackedAnimationData)();
|
|
5080
|
+
for (const sm of this.skinnedModels) {
|
|
5081
|
+
(0, import_renderer5.packSkinAndAnimations)(this.packedAnimData, sm.parsedSkin.data, sm.parsedSkin.animClips);
|
|
5082
|
+
}
|
|
5083
|
+
if (this.tryUploadInPlace())
|
|
5084
|
+
return;
|
|
5085
|
+
this.animComputeNeedsRebuild = true;
|
|
5086
|
+
}
|
|
5087
|
+
/** Doubling-growth budgets for kernel storage buffers, with sensible floors. */
|
|
5088
|
+
growBudgetsForPacked(packed, previous) {
|
|
5089
|
+
const pb = packAnimationData(packed);
|
|
5090
|
+
const grow = (cur, prev, floor) => Math.max(cur * 2, prev, floor);
|
|
5091
|
+
return {
|
|
5092
|
+
skelI32Capacity: grow(pb.skelI32.length, previous?.skelI32Capacity ?? 0, 256),
|
|
5093
|
+
animF32Capacity: grow(pb.animF32.length, previous?.animF32Capacity ?? 0, 4096),
|
|
5094
|
+
matricesCapacity: grow(pb.totalMats, previous?.matricesCapacity ?? 0, 8)
|
|
5218
5095
|
};
|
|
5219
5096
|
}
|
|
5097
|
+
/** Upload `packedAnimData` to the existing kernel iff it fits the current budgets. Returns false to force a rebuild. */
|
|
5098
|
+
tryUploadInPlace() {
|
|
5099
|
+
const kernel = this.animComputeKernel;
|
|
5100
|
+
const budgets = this.animKernelBudgets;
|
|
5101
|
+
if (!kernel || !budgets)
|
|
5102
|
+
return false;
|
|
5103
|
+
const pb = packAnimationData(this.packedAnimData);
|
|
5104
|
+
if (pb.skelI32.length > budgets.skelI32Capacity)
|
|
5105
|
+
return false;
|
|
5106
|
+
if (pb.animF32.length > budgets.animF32Capacity)
|
|
5107
|
+
return false;
|
|
5108
|
+
if (pb.totalMats > budgets.matricesCapacity)
|
|
5109
|
+
return false;
|
|
5110
|
+
uploadPackedToKernel(this.root, kernel, pb);
|
|
5111
|
+
this.animClipTableOffset = pb.clipTableOffset;
|
|
5112
|
+
this.animChannelTableOffset = pb.channelTableOffset;
|
|
5113
|
+
this.animJointLookupOffset = pb.jointLookupOffset;
|
|
5114
|
+
return true;
|
|
5115
|
+
}
|
|
5116
|
+
/**
|
|
5117
|
+
* Returns true if a skinned instance's bone-matrix compute should be
|
|
5118
|
+
* dispatched this frame. Combines frustum culling (scaled bounding sphere)
|
|
5119
|
+
* and a configurable distance cull from the camera. Callers pass the
|
|
5120
|
+
* camera position + cullDistSq once outside the loop to avoid re-reading.
|
|
5121
|
+
*/
|
|
5122
|
+
shouldDispatchSkinning(slot, model, camX, camY, camZ, cullDistSq) {
|
|
5123
|
+
const skinModel = model && model.skinIndex >= 0 ? this.skinnedModels[model.skinIndex] : null;
|
|
5124
|
+
const baseRadius = skinModel?.boundingRadius ?? 10;
|
|
5125
|
+
const base = slot * DYNAMIC_MESH_FLOATS;
|
|
5126
|
+
const sBase = slot * SKINNED_STATIC_MESH_FLOATS;
|
|
5127
|
+
const cx = this.skinnedDynamicData[base + DYN_CURR_PX];
|
|
5128
|
+
const cy = this.skinnedDynamicData[base + DYN_CURR_PY];
|
|
5129
|
+
const cz = this.skinnedDynamicData[base + DYN_CURR_PZ];
|
|
5130
|
+
const sx = Math.abs(this.skinnedStaticData[sBase + SSTAT_SX]);
|
|
5131
|
+
const sy = Math.abs(this.skinnedStaticData[sBase + SSTAT_SY]);
|
|
5132
|
+
const sz = Math.abs(this.skinnedStaticData[sBase + SSTAT_SZ]);
|
|
5133
|
+
const maxScale = sx > sy ? sx > sz ? sx : sz : sy > sz ? sy : sz;
|
|
5134
|
+
if (!this.isInFrustum(cx, cy, cz, baseRadius * maxScale))
|
|
5135
|
+
return false;
|
|
5136
|
+
const dxv = cx - camX, dyv = cy - camY, dzv = cz - camZ;
|
|
5137
|
+
if (dxv * dxv + dyv * dyv + dzv * dzv > cullDistSq)
|
|
5138
|
+
return false;
|
|
5139
|
+
return true;
|
|
5140
|
+
}
|
|
5220
5141
|
updateAnimations(deltaTime) {
|
|
5142
|
+
this.syncLazyAnimationChanges();
|
|
5221
5143
|
if (this.animComputeNeedsRebuild && this.packedAnimData.clips.length > 0) {
|
|
5222
5144
|
this.animComputeKernel?.destroy();
|
|
5145
|
+
const budgets = this.growBudgetsForPacked(this.packedAnimData, null);
|
|
5223
5146
|
const { kernel, packedBuffers } = buildAnimationKernel(
|
|
5224
5147
|
this.root,
|
|
5225
5148
|
this.packedAnimData,
|
|
5226
5149
|
this.maxSkinnedInstances,
|
|
5227
|
-
this.maxTotalBones
|
|
5150
|
+
this.maxTotalBones,
|
|
5151
|
+
budgets
|
|
5228
5152
|
);
|
|
5229
5153
|
this.animComputeKernel = kernel;
|
|
5154
|
+
this.animKernelBudgets = budgets;
|
|
5230
5155
|
this.animClipTableOffset = packedBuffers.clipTableOffset;
|
|
5231
5156
|
this.animChannelTableOffset = packedBuffers.channelTableOffset;
|
|
5232
5157
|
this.animJointLookupOffset = packedBuffers.jointLookupOffset;
|
|
@@ -5243,11 +5168,22 @@ var WebGPU3DRenderer = class extends import_base_3d_renderer.Base3DRenderer {
|
|
|
5243
5168
|
{ binding: 4, resource: { buffer: rawBoneBuffer } }
|
|
5244
5169
|
]
|
|
5245
5170
|
});
|
|
5171
|
+
this.device.queue.writeBuffer(
|
|
5172
|
+
this.rawBoneMatrixBuffer,
|
|
5173
|
+
0,
|
|
5174
|
+
this.boneMatrixData.buffer,
|
|
5175
|
+
this.boneMatrixData.byteOffset,
|
|
5176
|
+
this.boneMatrixData.byteLength
|
|
5177
|
+
);
|
|
5178
|
+
this.boneMatrixDirty = false;
|
|
5246
5179
|
this.animComputeNeedsRebuild = false;
|
|
5247
5180
|
}
|
|
5248
5181
|
this.updatedBoneOffsets.fill(0);
|
|
5249
5182
|
let count = 0;
|
|
5250
5183
|
const dv = this.gpuInstDV;
|
|
5184
|
+
const camPos = this.camera.position;
|
|
5185
|
+
const camX = camPos[0], camY = camPos[1], camZ = camPos[2];
|
|
5186
|
+
const cullDistSq = this.animationCullDistanceSq;
|
|
5251
5187
|
for (let slot = 0; slot < this.maxSkinnedInstances; slot++) {
|
|
5252
5188
|
const animState = this.skinnedAnimStates[slot];
|
|
5253
5189
|
if (!animState)
|
|
@@ -5286,6 +5222,8 @@ var WebGPU3DRenderer = class extends import_base_3d_renderer.Base3DRenderer {
|
|
|
5286
5222
|
const modelId = this.skinnedInstanceModelIds[slot];
|
|
5287
5223
|
const model = this.models[modelId];
|
|
5288
5224
|
const skinIdx = model?.skinIndex ?? 0;
|
|
5225
|
+
if (!this.shouldDispatchSkinning(slot, model, camX, camY, camZ, cullDistSq))
|
|
5226
|
+
continue;
|
|
5289
5227
|
const off = count * 32;
|
|
5290
5228
|
dv.setInt32(off, animState.clipId, true);
|
|
5291
5229
|
dv.setFloat32(off + 4, animState.time, true);
|
|
@@ -5330,14 +5268,12 @@ var WebGPU3DRenderer = class extends import_base_3d_renderer.Base3DRenderer {
|
|
|
5330
5268
|
}
|
|
5331
5269
|
}
|
|
5332
5270
|
}
|
|
5271
|
+
/**
|
|
5272
|
+
* Free an instance's renderer slot. Equivalent to `handle.destroy()` -
|
|
5273
|
+
* kept as a convenience for direct lookup. Safe to call multiple times.
|
|
5274
|
+
*/
|
|
5333
5275
|
removeInstance(handle) {
|
|
5334
|
-
|
|
5335
|
-
this.freeList.free(handle.slot);
|
|
5336
|
-
const dynBase = handle.slot * DYNAMIC_MESH_FLOATS;
|
|
5337
|
-
const statBase = handle.slot * STATIC_MESH_FLOATS;
|
|
5338
|
-
this.dynamicData.fill(0, dynBase, dynBase + DYNAMIC_MESH_FLOATS);
|
|
5339
|
-
this.staticData.fill(0, statBase, statBase + STATIC_MESH_FLOATS);
|
|
5340
|
-
this.staticDirty = true;
|
|
5276
|
+
handle.destroy();
|
|
5341
5277
|
}
|
|
5342
5278
|
storePreviousState() {
|
|
5343
5279
|
this.camera.storePrevious();
|
|
@@ -5643,6 +5579,8 @@ var WebGPU3DRenderer = class extends import_base_3d_renderer.Base3DRenderer {
|
|
|
5643
5579
|
this.resizeObserver?.disconnect();
|
|
5644
5580
|
this.resizeObserver = null;
|
|
5645
5581
|
this.resizeCallbacks.length = 0;
|
|
5582
|
+
this.clipResync?.dispose();
|
|
5583
|
+
this.clipResync = null;
|
|
5646
5584
|
this.dynamicBuffer?.destroy();
|
|
5647
5585
|
this.staticBuffer?.destroy();
|
|
5648
5586
|
this.uniformBuffer?.destroy();
|
|
@@ -5790,10 +5728,8 @@ var ParticleEmitter = class {
|
|
|
5790
5728
|
const sprite = this.renderer.addSprite({
|
|
5791
5729
|
sheet: this.config.sheet,
|
|
5792
5730
|
sprite: this.config.sprite ?? 0,
|
|
5793
|
-
x,
|
|
5794
|
-
|
|
5795
|
-
scaleX: size,
|
|
5796
|
-
scaleY: size,
|
|
5731
|
+
position: [x, y],
|
|
5732
|
+
scale: size,
|
|
5797
5733
|
opacity: 1,
|
|
5798
5734
|
tint: this.config.color,
|
|
5799
5735
|
layer: 255
|
|
@@ -5845,109 +5781,6 @@ var ParticleEmitter = class {
|
|
|
5845
5781
|
}
|
|
5846
5782
|
};
|
|
5847
5783
|
|
|
5848
|
-
// src/2d/animation.ts
|
|
5849
|
-
var AnimationController = class {
|
|
5850
|
-
constructor() {
|
|
5851
|
-
this.clips = [];
|
|
5852
|
-
this.clipsByName = /* @__PURE__ */ new Map();
|
|
5853
|
-
}
|
|
5854
|
-
/**
|
|
5855
|
-
* Register an animation clip. Returns the clip ID.
|
|
5856
|
-
*/
|
|
5857
|
-
loadClip(config) {
|
|
5858
|
-
const id = this.clips.length;
|
|
5859
|
-
const clip = {
|
|
5860
|
-
id,
|
|
5861
|
-
name: config.name,
|
|
5862
|
-
frames: new Uint16Array(config.frames),
|
|
5863
|
-
durations: new Float32Array(config.durations),
|
|
5864
|
-
frameCount: config.frames.length,
|
|
5865
|
-
totalDuration: config.durations.reduce((sum, d7) => sum + d7, 0),
|
|
5866
|
-
loop: config.loop
|
|
5867
|
-
};
|
|
5868
|
-
this.clips.push(clip);
|
|
5869
|
-
this.clipsByName.set(config.name, id);
|
|
5870
|
-
return id;
|
|
5871
|
-
}
|
|
5872
|
-
/**
|
|
5873
|
-
* Get a clip by name.
|
|
5874
|
-
*/
|
|
5875
|
-
getClipId(name) {
|
|
5876
|
-
const id = this.clipsByName.get(name);
|
|
5877
|
-
if (id === void 0)
|
|
5878
|
-
throw new Error(`Animation clip "${name}" not found`);
|
|
5879
|
-
return id;
|
|
5880
|
-
}
|
|
5881
|
-
/**
|
|
5882
|
-
* Get a clip by ID.
|
|
5883
|
-
*/
|
|
5884
|
-
getClip(id) {
|
|
5885
|
-
return this.clips[id];
|
|
5886
|
-
}
|
|
5887
|
-
/**
|
|
5888
|
-
* Create a new animation state for an entity. This is the per-entity data
|
|
5889
|
-
* that tracks playback progress. Store it however you like (flat array, component, etc).
|
|
5890
|
-
*/
|
|
5891
|
-
createState(clipId, speed = 1, playing = true) {
|
|
5892
|
-
return { clipId, frame: 0, time: 0, speed, playing };
|
|
5893
|
-
}
|
|
5894
|
-
/**
|
|
5895
|
-
* Advance an animation state by deltaTime (in seconds).
|
|
5896
|
-
* Returns the current sprite frame ID, or -1 if not playing.
|
|
5897
|
-
* Zero allocations.
|
|
5898
|
-
*/
|
|
5899
|
-
update(state, deltaTime) {
|
|
5900
|
-
if (!state.playing) {
|
|
5901
|
-
return this.clips[state.clipId].frames[state.frame];
|
|
5902
|
-
}
|
|
5903
|
-
const clip = this.clips[state.clipId];
|
|
5904
|
-
state.time += deltaTime * state.speed * 1e3;
|
|
5905
|
-
while (state.time >= clip.durations[state.frame]) {
|
|
5906
|
-
state.time -= clip.durations[state.frame];
|
|
5907
|
-
state.frame++;
|
|
5908
|
-
if (state.frame >= clip.frameCount) {
|
|
5909
|
-
if (clip.loop) {
|
|
5910
|
-
state.frame = 0;
|
|
5911
|
-
} else {
|
|
5912
|
-
state.frame = clip.frameCount - 1;
|
|
5913
|
-
state.playing = false;
|
|
5914
|
-
break;
|
|
5915
|
-
}
|
|
5916
|
-
}
|
|
5917
|
-
}
|
|
5918
|
-
return clip.frames[state.frame];
|
|
5919
|
-
}
|
|
5920
|
-
/**
|
|
5921
|
-
* Play a different clip on an existing state. Resets frame and time.
|
|
5922
|
-
*/
|
|
5923
|
-
play(state, clipId, speed) {
|
|
5924
|
-
state.clipId = clipId;
|
|
5925
|
-
state.frame = 0;
|
|
5926
|
-
state.time = 0;
|
|
5927
|
-
state.playing = true;
|
|
5928
|
-
if (speed !== void 0)
|
|
5929
|
-
state.speed = speed;
|
|
5930
|
-
}
|
|
5931
|
-
/**
|
|
5932
|
-
* Stop playback.
|
|
5933
|
-
*/
|
|
5934
|
-
stop(state) {
|
|
5935
|
-
state.playing = false;
|
|
5936
|
-
}
|
|
5937
|
-
/**
|
|
5938
|
-
* Resume playback.
|
|
5939
|
-
*/
|
|
5940
|
-
resume(state) {
|
|
5941
|
-
state.playing = true;
|
|
5942
|
-
}
|
|
5943
|
-
/**
|
|
5944
|
-
* Number of loaded clips.
|
|
5945
|
-
*/
|
|
5946
|
-
get clipCount() {
|
|
5947
|
-
return this.clips.length;
|
|
5948
|
-
}
|
|
5949
|
-
};
|
|
5950
|
-
|
|
5951
5784
|
// src/shaders/utils.ts
|
|
5952
5785
|
var import_typegpu11 = __toESM(require("typegpu"), 1);
|
|
5953
5786
|
var d6 = __toESM(require("typegpu/data"), 1);
|