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/esm/index.js
CHANGED
|
@@ -8,7 +8,7 @@ var std = { ..._std };
|
|
|
8
8
|
import tgpu4 from "typegpu";
|
|
9
9
|
import * as d4 from "typegpu/data";
|
|
10
10
|
import { FreeList as FreeList2 } from "murow/core/free-list";
|
|
11
|
-
import { Base2DRenderer } from "murow/renderer
|
|
11
|
+
import { Base2DRenderer } from "murow/renderer";
|
|
12
12
|
|
|
13
13
|
// src/core/constants.ts
|
|
14
14
|
var DYNAMIC_OFFSET_PREV_X = 0;
|
|
@@ -527,6 +527,11 @@ function createSpriteFragment(_spriteLayout, textureLayout) {
|
|
|
527
527
|
}
|
|
528
528
|
|
|
529
529
|
// src/spritesheet/spritesheet.ts
|
|
530
|
+
import {
|
|
531
|
+
computeGridUVs,
|
|
532
|
+
computeTexturePackerUVs,
|
|
533
|
+
loadImage
|
|
534
|
+
} from "murow/renderer";
|
|
530
535
|
var Spritesheet = class {
|
|
531
536
|
constructor(id, texture, textureView, sampler, uvs, width, height) {
|
|
532
537
|
this.id = id;
|
|
@@ -551,41 +556,6 @@ var Spritesheet = class {
|
|
|
551
556
|
return this._height;
|
|
552
557
|
}
|
|
553
558
|
};
|
|
554
|
-
function computeGridUVs(imageWidth, imageHeight, frameWidth, frameHeight) {
|
|
555
|
-
const cols = Math.floor(imageWidth / frameWidth);
|
|
556
|
-
const rows = Math.floor(imageHeight / frameHeight);
|
|
557
|
-
const uvs = [];
|
|
558
|
-
for (let row = 0; row < rows; row++) {
|
|
559
|
-
for (let col = 0; col < cols; col++) {
|
|
560
|
-
uvs.push({
|
|
561
|
-
minX: col * frameWidth / imageWidth,
|
|
562
|
-
minY: row * frameHeight / imageHeight,
|
|
563
|
-
maxX: (col + 1) * frameWidth / imageWidth,
|
|
564
|
-
maxY: (row + 1) * frameHeight / imageHeight
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
return uvs;
|
|
569
|
-
}
|
|
570
|
-
function computeTexturePackerUVs(data) {
|
|
571
|
-
const { w, h } = data.meta.size;
|
|
572
|
-
const uvs = [];
|
|
573
|
-
for (const key of Object.keys(data.frames)) {
|
|
574
|
-
const frame = data.frames[key].frame;
|
|
575
|
-
uvs.push({
|
|
576
|
-
minX: frame.x / w,
|
|
577
|
-
minY: frame.y / h,
|
|
578
|
-
maxX: (frame.x + frame.w) / w,
|
|
579
|
-
maxY: (frame.y + frame.h) / h
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
return uvs;
|
|
583
|
-
}
|
|
584
|
-
async function loadImage(url) {
|
|
585
|
-
const response = await fetch(url);
|
|
586
|
-
const blob = await response.blob();
|
|
587
|
-
return createImageBitmap(blob);
|
|
588
|
-
}
|
|
589
559
|
function createTextureFromBitmap(device, bitmap) {
|
|
590
560
|
const texture = device.createTexture({
|
|
591
561
|
size: [bitmap.width, bitmap.height, 1],
|
|
@@ -600,6 +570,9 @@ function createTextureFromBitmap(device, bitmap) {
|
|
|
600
570
|
return { texture, view: texture.createView() };
|
|
601
571
|
}
|
|
602
572
|
|
|
573
|
+
// src/2d/renderer.ts
|
|
574
|
+
import { parseSpritesheet } from "murow/renderer";
|
|
575
|
+
|
|
603
576
|
// src/geometry/geometry-builder.ts
|
|
604
577
|
import tgpu2 from "typegpu";
|
|
605
578
|
|
|
@@ -2078,9 +2051,23 @@ var ComputeBuilder = class {
|
|
|
2078
2051
|
};
|
|
2079
2052
|
|
|
2080
2053
|
// src/2d/renderer.ts
|
|
2054
|
+
var prefab2DHandles = /* @__PURE__ */ new WeakMap();
|
|
2055
|
+
function isPrefab2D(value) {
|
|
2056
|
+
return value.type === "spritesheet";
|
|
2057
|
+
}
|
|
2058
|
+
function resolveSpritePrefabHandle(prefab) {
|
|
2059
|
+
const h = prefab2DHandles.get(prefab);
|
|
2060
|
+
if (!h) {
|
|
2061
|
+
throw new Error(
|
|
2062
|
+
`Prefab '${prefab.id}' has no GPU handle \u2014 has the renderer's init() been called with this bucket?`
|
|
2063
|
+
);
|
|
2064
|
+
}
|
|
2065
|
+
return h;
|
|
2066
|
+
}
|
|
2081
2067
|
var WebGPU2DRenderer = class extends Base2DRenderer {
|
|
2082
2068
|
constructor(canvas, options) {
|
|
2083
|
-
|
|
2069
|
+
const resolvedMaxSprites = options.maxSprites ?? options.maxInstances ?? 1024;
|
|
2070
|
+
super(canvas, { ...options, maxSprites: resolvedMaxSprites });
|
|
2084
2071
|
this.staticDirty = false;
|
|
2085
2072
|
// Per-sheet bind groups
|
|
2086
2073
|
this.sheetBindGroups = /* @__PURE__ */ new Map();
|
|
@@ -2089,12 +2076,13 @@ var WebGPU2DRenderer = class extends Base2DRenderer {
|
|
|
2089
2076
|
this.uniformData = new Float32Array(20);
|
|
2090
2077
|
this.resizeObserver = null;
|
|
2091
2078
|
this.resizeCallbacks = [];
|
|
2079
|
+
this._prefabs = options.prefabs ?? null;
|
|
2092
2080
|
this.camera = new Camera2D(canvas.width || 800, canvas.height || 600);
|
|
2093
|
-
this.freeList = new FreeList2(
|
|
2094
|
-
this.batcher = new SparseBatcher(
|
|
2095
|
-
this.dynamicData = new Float32Array(
|
|
2096
|
-
this.staticData = new Float32Array(
|
|
2097
|
-
this.slotIndexData = new Uint32Array(
|
|
2081
|
+
this.freeList = new FreeList2(resolvedMaxSprites);
|
|
2082
|
+
this.batcher = new SparseBatcher(resolvedMaxSprites);
|
|
2083
|
+
this.dynamicData = new Float32Array(resolvedMaxSprites * DYNAMIC_FLOATS_PER_SPRITE);
|
|
2084
|
+
this.staticData = new Float32Array(resolvedMaxSprites * STATIC_FLOATS_PER_SPRITE);
|
|
2085
|
+
this.slotIndexData = new Uint32Array(resolvedMaxSprites);
|
|
2098
2086
|
}
|
|
2099
2087
|
get device() {
|
|
2100
2088
|
return this._device;
|
|
@@ -2148,9 +2136,25 @@ var WebGPU2DRenderer = class extends Base2DRenderer {
|
|
|
2148
2136
|
this.rawStaticBuffer = this.root.unwrap(this.staticBuffer);
|
|
2149
2137
|
this.rawSlotIndexBuffer = this.root.unwrap(this.slotIndexBuffer);
|
|
2150
2138
|
this.rawUniformBuffer = this.root.unwrap(this.uniformBuffer);
|
|
2139
|
+
if (this._prefabs) {
|
|
2140
|
+
this.uploadPrefabBucket(this._prefabs);
|
|
2141
|
+
}
|
|
2151
2142
|
this.setupResizeObserver();
|
|
2152
2143
|
this._initialized = true;
|
|
2153
2144
|
}
|
|
2145
|
+
/**
|
|
2146
|
+
* Upload every prefab in the bucket to the GPU and stash the resulting
|
|
2147
|
+
* SpritesheetHandle on each prefab so `bucket.get(id)` returns something
|
|
2148
|
+
* usable as a sprite source.
|
|
2149
|
+
*/
|
|
2150
|
+
uploadPrefabBucket(bucket) {
|
|
2151
|
+
for (const prefab of bucket.entries()) {
|
|
2152
|
+
if (prefab.type === "spritesheet") {
|
|
2153
|
+
const handle = this.uploadParsedSpritesheet(prefab.parsed);
|
|
2154
|
+
prefab2DHandles.set(prefab, handle);
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2154
2158
|
setupResizeObserver() {
|
|
2155
2159
|
const supportsDevicePixelBox = (() => {
|
|
2156
2160
|
try {
|
|
@@ -2204,24 +2208,22 @@ var WebGPU2DRenderer = class extends Base2DRenderer {
|
|
|
2204
2208
|
this.resizeCallbacks.push(callback);
|
|
2205
2209
|
}
|
|
2206
2210
|
async loadSpritesheet(source) {
|
|
2207
|
-
const
|
|
2208
|
-
|
|
2211
|
+
const parsed = await parseSpritesheet(source);
|
|
2212
|
+
return this.uploadParsedSpritesheet(parsed);
|
|
2213
|
+
}
|
|
2214
|
+
/**
|
|
2215
|
+
* Upload a previously-parsed spritesheet to the GPU. Returns a SpritesheetHandle.
|
|
2216
|
+
* Splitting parse (CPU) from upload (GPU) lets callers parse spritesheets in parallel
|
|
2217
|
+
* before a renderer exists.
|
|
2218
|
+
*/
|
|
2219
|
+
uploadParsedSpritesheet(parsed) {
|
|
2220
|
+
const { texture, view } = createTextureFromBitmap(this._device, parsed.bitmap);
|
|
2209
2221
|
const sampler = this._device.createSampler({
|
|
2210
2222
|
magFilter: "nearest",
|
|
2211
2223
|
minFilter: "nearest"
|
|
2212
2224
|
});
|
|
2213
|
-
let uvs;
|
|
2214
|
-
if (source.data) {
|
|
2215
|
-
const resp = await fetch(source.data);
|
|
2216
|
-
const json = await resp.json();
|
|
2217
|
-
uvs = computeTexturePackerUVs(json);
|
|
2218
|
-
} else if (source.frameWidth && source.frameHeight) {
|
|
2219
|
-
uvs = computeGridUVs(bitmap.width, bitmap.height, source.frameWidth, source.frameHeight);
|
|
2220
|
-
} else {
|
|
2221
|
-
uvs = [{ minX: 0, minY: 0, maxX: 1, maxY: 1 }];
|
|
2222
|
-
}
|
|
2223
2225
|
const id = this.nextSheetId++;
|
|
2224
|
-
const sheet = new Spritesheet(id, texture, view, sampler, uvs,
|
|
2226
|
+
const sheet = new Spritesheet(id, texture, view, sampler, parsed.uvs, parsed.width, parsed.height);
|
|
2225
2227
|
this.sheets.set(id, sheet);
|
|
2226
2228
|
const bindGroup = this._device.createBindGroup({
|
|
2227
2229
|
layout: this.rawTextureLayout,
|
|
@@ -2239,18 +2241,20 @@ var WebGPU2DRenderer = class extends Base2DRenderer {
|
|
|
2239
2241
|
throw new Error(`Max sprites (${this.maxSprites}) reached`);
|
|
2240
2242
|
const dynBase = slot * DYNAMIC_FLOATS_PER_SPRITE;
|
|
2241
2243
|
const statBase = slot * STATIC_FLOATS_PER_SPRITE;
|
|
2242
|
-
const
|
|
2243
|
-
const
|
|
2244
|
-
this.dynamicData[dynBase + DYNAMIC_OFFSET_PREV_X] =
|
|
2245
|
-
this.dynamicData[dynBase + DYNAMIC_OFFSET_PREV_Y] =
|
|
2246
|
-
this.dynamicData[dynBase + DYNAMIC_OFFSET_CURR_X] =
|
|
2247
|
-
this.dynamicData[dynBase + DYNAMIC_OFFSET_CURR_Y] =
|
|
2244
|
+
const sheet = isPrefab2D(opts.sheet) ? resolveSpritePrefabHandle(opts.sheet) : opts.sheet;
|
|
2245
|
+
const [px, py] = opts.position ?? [0, 0];
|
|
2246
|
+
this.dynamicData[dynBase + DYNAMIC_OFFSET_PREV_X] = px;
|
|
2247
|
+
this.dynamicData[dynBase + DYNAMIC_OFFSET_PREV_Y] = py;
|
|
2248
|
+
this.dynamicData[dynBase + DYNAMIC_OFFSET_CURR_X] = px;
|
|
2249
|
+
this.dynamicData[dynBase + DYNAMIC_OFFSET_CURR_Y] = py;
|
|
2248
2250
|
const rotation = opts.rotation ?? 0;
|
|
2249
2251
|
this.dynamicData[dynBase + DYNAMIC_OFFSET_PREV_ROTATION] = rotation;
|
|
2250
2252
|
this.dynamicData[dynBase + DYNAMIC_OFFSET_CURR_ROTATION] = rotation;
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2253
|
+
const s = opts.scale;
|
|
2254
|
+
const [sx, sy] = typeof s === "number" ? [s, s] : s ?? [1, 1];
|
|
2255
|
+
this.staticData[statBase + STATIC_OFFSET_SCALE_X] = sx;
|
|
2256
|
+
this.staticData[statBase + STATIC_OFFSET_SCALE_Y] = sy;
|
|
2257
|
+
const uv = sheet.getUV(opts.sprite ?? 0);
|
|
2254
2258
|
this.staticData[statBase + STATIC_OFFSET_UV_MIN_X] = uv.minX;
|
|
2255
2259
|
this.staticData[statBase + STATIC_OFFSET_UV_MIN_Y] = uv.minY;
|
|
2256
2260
|
this.staticData[statBase + STATIC_OFFSET_UV_MAX_X] = uv.maxX;
|
|
@@ -2265,12 +2269,12 @@ var WebGPU2DRenderer = class extends Base2DRenderer {
|
|
|
2265
2269
|
this.staticData[statBase + STATIC_OFFSET_TINT_B] = tint[2];
|
|
2266
2270
|
this.staticData[statBase + STATIC_OFFSET_TINT_A] = tint[3];
|
|
2267
2271
|
this.staticDirty = true;
|
|
2268
|
-
this.batcher.add(opts.layer ?? 0,
|
|
2272
|
+
this.batcher.add(opts.layer ?? 0, sheet.id, slot);
|
|
2269
2273
|
return new SpriteAccessor(
|
|
2270
2274
|
this.dynamicData,
|
|
2271
2275
|
this.staticData,
|
|
2272
2276
|
slot,
|
|
2273
|
-
|
|
2277
|
+
sheet.id,
|
|
2274
2278
|
() => {
|
|
2275
2279
|
this.staticDirty = true;
|
|
2276
2280
|
}
|
|
@@ -2392,9 +2396,112 @@ var WebGPU2DRenderer = class extends Base2DRenderer {
|
|
|
2392
2396
|
}
|
|
2393
2397
|
};
|
|
2394
2398
|
|
|
2399
|
+
// src/2d/animation.ts
|
|
2400
|
+
var AnimationController = class {
|
|
2401
|
+
constructor() {
|
|
2402
|
+
this.clips = [];
|
|
2403
|
+
this.clipsByName = /* @__PURE__ */ new Map();
|
|
2404
|
+
}
|
|
2405
|
+
/**
|
|
2406
|
+
* Register an animation clip. Returns the clip ID.
|
|
2407
|
+
*/
|
|
2408
|
+
loadClip(config) {
|
|
2409
|
+
const id = this.clips.length;
|
|
2410
|
+
const clip = {
|
|
2411
|
+
id,
|
|
2412
|
+
name: config.name,
|
|
2413
|
+
frames: new Uint16Array(config.frames),
|
|
2414
|
+
durations: new Float32Array(config.durations),
|
|
2415
|
+
frameCount: config.frames.length,
|
|
2416
|
+
totalDuration: config.durations.reduce((sum, d7) => sum + d7, 0),
|
|
2417
|
+
loop: config.loop
|
|
2418
|
+
};
|
|
2419
|
+
this.clips.push(clip);
|
|
2420
|
+
this.clipsByName.set(config.name, id);
|
|
2421
|
+
return id;
|
|
2422
|
+
}
|
|
2423
|
+
/**
|
|
2424
|
+
* Get a clip by name.
|
|
2425
|
+
*/
|
|
2426
|
+
getClipId(name) {
|
|
2427
|
+
const id = this.clipsByName.get(name);
|
|
2428
|
+
if (id === void 0)
|
|
2429
|
+
throw new Error(`Animation clip "${name}" not found`);
|
|
2430
|
+
return id;
|
|
2431
|
+
}
|
|
2432
|
+
/**
|
|
2433
|
+
* Get a clip by ID.
|
|
2434
|
+
*/
|
|
2435
|
+
getClip(id) {
|
|
2436
|
+
return this.clips[id];
|
|
2437
|
+
}
|
|
2438
|
+
/**
|
|
2439
|
+
* Create a new animation state for an entity. This is the per-entity data
|
|
2440
|
+
* that tracks playback progress. Store it however you like (flat array, component, etc).
|
|
2441
|
+
*/
|
|
2442
|
+
createState(clipId, speed = 1, playing = true) {
|
|
2443
|
+
return { clipId, frame: 0, time: 0, speed, playing };
|
|
2444
|
+
}
|
|
2445
|
+
/**
|
|
2446
|
+
* Advance an animation state by deltaTime (in seconds).
|
|
2447
|
+
* Returns the current sprite frame ID, or -1 if not playing.
|
|
2448
|
+
* Zero allocations.
|
|
2449
|
+
*/
|
|
2450
|
+
update(state, deltaTime) {
|
|
2451
|
+
if (!state.playing) {
|
|
2452
|
+
return this.clips[state.clipId].frames[state.frame];
|
|
2453
|
+
}
|
|
2454
|
+
const clip = this.clips[state.clipId];
|
|
2455
|
+
state.time += deltaTime * state.speed * 1e3;
|
|
2456
|
+
while (state.time >= clip.durations[state.frame]) {
|
|
2457
|
+
state.time -= clip.durations[state.frame];
|
|
2458
|
+
state.frame++;
|
|
2459
|
+
if (state.frame >= clip.frameCount) {
|
|
2460
|
+
if (clip.loop) {
|
|
2461
|
+
state.frame = 0;
|
|
2462
|
+
} else {
|
|
2463
|
+
state.frame = clip.frameCount - 1;
|
|
2464
|
+
state.playing = false;
|
|
2465
|
+
break;
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
return clip.frames[state.frame];
|
|
2470
|
+
}
|
|
2471
|
+
/**
|
|
2472
|
+
* Play a different clip on an existing state. Resets frame and time.
|
|
2473
|
+
*/
|
|
2474
|
+
play(state, clipId, speed) {
|
|
2475
|
+
state.clipId = clipId;
|
|
2476
|
+
state.frame = 0;
|
|
2477
|
+
state.time = 0;
|
|
2478
|
+
state.playing = true;
|
|
2479
|
+
if (speed !== void 0)
|
|
2480
|
+
state.speed = speed;
|
|
2481
|
+
}
|
|
2482
|
+
/**
|
|
2483
|
+
* Stop playback.
|
|
2484
|
+
*/
|
|
2485
|
+
stop(state) {
|
|
2486
|
+
state.playing = false;
|
|
2487
|
+
}
|
|
2488
|
+
/**
|
|
2489
|
+
* Resume playback.
|
|
2490
|
+
*/
|
|
2491
|
+
resume(state) {
|
|
2492
|
+
state.playing = true;
|
|
2493
|
+
}
|
|
2494
|
+
/**
|
|
2495
|
+
* Number of loaded clips.
|
|
2496
|
+
*/
|
|
2497
|
+
get clipCount() {
|
|
2498
|
+
return this.clips.length;
|
|
2499
|
+
}
|
|
2500
|
+
};
|
|
2501
|
+
|
|
2395
2502
|
// src/3d/renderer.ts
|
|
2396
2503
|
import tgpu6 from "typegpu";
|
|
2397
|
-
import { Base3DRenderer } from "murow/renderer
|
|
2504
|
+
import { Base3DRenderer } from "murow/renderer";
|
|
2398
2505
|
import { FreeList as FreeList3 } from "murow/core/free-list";
|
|
2399
2506
|
import { SparseBatcher as SparseBatcher2 } from "murow/core/sparse-batcher";
|
|
2400
2507
|
|
|
@@ -3039,750 +3146,102 @@ function createSkinnedMeshVertex(layout) {
|
|
|
3039
3146
|
});
|
|
3040
3147
|
}
|
|
3041
3148
|
|
|
3042
|
-
// src/
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
return m;
|
|
3050
|
-
}
|
|
3051
|
-
function trsToMat4(tx, ty, tz, qx, qy, qz, qw, sx, sy, sz, dst, offset) {
|
|
3052
|
-
const xx = qx * qx, yy = qy * qy, zz = qz * qz;
|
|
3053
|
-
const xy = qx * qy, xz = qx * qz, yz = qy * qz;
|
|
3054
|
-
const wx = qw * qx, wy = qw * qy, wz = qw * qz;
|
|
3055
|
-
dst[offset] = (1 - 2 * (yy + zz)) * sx;
|
|
3056
|
-
dst[offset + 1] = 2 * (xy + wz) * sx;
|
|
3057
|
-
dst[offset + 2] = 2 * (xz - wy) * sx;
|
|
3058
|
-
dst[offset + 3] = 0;
|
|
3059
|
-
dst[offset + 4] = 2 * (xy - wz) * sy;
|
|
3060
|
-
dst[offset + 5] = (1 - 2 * (xx + zz)) * sy;
|
|
3061
|
-
dst[offset + 6] = 2 * (yz + wx) * sy;
|
|
3062
|
-
dst[offset + 7] = 0;
|
|
3063
|
-
dst[offset + 8] = 2 * (xz + wy) * sz;
|
|
3064
|
-
dst[offset + 9] = 2 * (yz - wx) * sz;
|
|
3065
|
-
dst[offset + 10] = (1 - 2 * (xx + yy)) * sz;
|
|
3066
|
-
dst[offset + 11] = 0;
|
|
3067
|
-
dst[offset + 12] = tx;
|
|
3068
|
-
dst[offset + 13] = ty;
|
|
3069
|
-
dst[offset + 14] = tz;
|
|
3070
|
-
dst[offset + 15] = 1;
|
|
3071
|
-
}
|
|
3072
|
-
function nodeToMat4(node) {
|
|
3073
|
-
const m = new Float32Array(16);
|
|
3074
|
-
if (node.matrix) {
|
|
3075
|
-
m.set(node.matrix);
|
|
3076
|
-
return m;
|
|
3077
|
-
}
|
|
3078
|
-
const t = node.translation ?? [0, 0, 0];
|
|
3079
|
-
const r = node.rotation ?? [0, 0, 0, 1];
|
|
3080
|
-
const s = node.scale ?? [1, 1, 1];
|
|
3081
|
-
trsToMat4(t[0], t[1], t[2], r[0], r[1], r[2], r[3], s[0], s[1], s[2], m, 0);
|
|
3082
|
-
return m;
|
|
3083
|
-
}
|
|
3084
|
-
var _mulTemp = new Float32Array(16);
|
|
3085
|
-
function mat4Mul(a, aO, b, bO, dst, dO) {
|
|
3086
|
-
for (let i = 0; i < 4; i++) {
|
|
3087
|
-
for (let j = 0; j < 4; j++) {
|
|
3088
|
-
_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];
|
|
3089
|
-
}
|
|
3090
|
-
}
|
|
3091
|
-
dst.set(_mulTemp, dO);
|
|
3092
|
-
}
|
|
3093
|
-
function mat4MulNew(a, b) {
|
|
3094
|
-
const o = new Float32Array(16);
|
|
3095
|
-
for (let i = 0; i < 4; i++) {
|
|
3096
|
-
for (let j = 0; j < 4; j++) {
|
|
3097
|
-
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];
|
|
3098
|
-
}
|
|
3099
|
-
}
|
|
3100
|
-
return o;
|
|
3101
|
-
}
|
|
3149
|
+
// src/3d/renderer.ts
|
|
3150
|
+
import {
|
|
3151
|
+
createPackedAnimationData,
|
|
3152
|
+
packSkinAndAnimations,
|
|
3153
|
+
parseGltf,
|
|
3154
|
+
SkeletalAnimation
|
|
3155
|
+
} from "murow/renderer";
|
|
3102
3156
|
|
|
3103
|
-
// src/3d/
|
|
3104
|
-
function
|
|
3105
|
-
const
|
|
3106
|
-
const
|
|
3107
|
-
const
|
|
3108
|
-
const
|
|
3109
|
-
const
|
|
3110
|
-
const
|
|
3111
|
-
const
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
const
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
}
|
|
3135
|
-
}
|
|
3136
|
-
let skeletonRootMatrix = null;
|
|
3137
|
-
for (let j = 0; j < jointCount; j++) {
|
|
3138
|
-
if (parentJointIndices[j] !== -1)
|
|
3139
|
-
continue;
|
|
3140
|
-
let ancestorNode = nodeParent.get(joints[j]);
|
|
3141
|
-
const ancestorChain = [];
|
|
3142
|
-
while (ancestorNode !== void 0 && !nodeToJoint.has(ancestorNode)) {
|
|
3143
|
-
ancestorChain.push(ancestorNode);
|
|
3144
|
-
ancestorNode = nodeParent.get(ancestorNode);
|
|
3145
|
-
}
|
|
3146
|
-
if (ancestorChain.length > 0) {
|
|
3147
|
-
skeletonRootMatrix = mat4IdentityNew();
|
|
3148
|
-
for (let i = ancestorChain.length - 1; i >= 0; i--) {
|
|
3149
|
-
const nodeMat = nodeToMat4(gltf.nodes[ancestorChain[i]]);
|
|
3150
|
-
skeletonRootMatrix = mat4MulNew(skeletonRootMatrix, nodeMat);
|
|
3151
|
-
}
|
|
3152
|
-
}
|
|
3153
|
-
break;
|
|
3157
|
+
// src/3d/skeletal-animation-compute/packer.ts
|
|
3158
|
+
function packAnimationData(packed) {
|
|
3159
|
+
const numSkins = packed.skins.length;
|
|
3160
|
+
const numClips = packed.clips.length;
|
|
3161
|
+
const numChannels = packed.channels.length;
|
|
3162
|
+
const SKIN_ENTRY = 10;
|
|
3163
|
+
const CLIP_ENTRY = 4;
|
|
3164
|
+
const CH_ENTRY = 4;
|
|
3165
|
+
const skinEnd = numSkins * SKIN_ENTRY;
|
|
3166
|
+
const parentEnd = skinEnd + packed.parentIndices.length;
|
|
3167
|
+
const topoEnd = parentEnd + packed.topoOrder.length;
|
|
3168
|
+
const clipEnd = topoEnd + numClips * CLIP_ENTRY;
|
|
3169
|
+
const channelEnd = clipEnd + numChannels * CH_ENTRY;
|
|
3170
|
+
const jointLookupEnd = channelEnd + packed.jointChannelLookup.length;
|
|
3171
|
+
const clipTableOffset = topoEnd;
|
|
3172
|
+
const channelTableOffset = clipEnd;
|
|
3173
|
+
const jointLookupOffset = channelEnd;
|
|
3174
|
+
const skelI32 = new Int32Array(jointLookupEnd || 1);
|
|
3175
|
+
for (let s = 0; s < numSkins; s++) {
|
|
3176
|
+
const sk = packed.skins[s];
|
|
3177
|
+
const base = s * SKIN_ENTRY;
|
|
3178
|
+
skelI32[base + 0] = sk.jointCount;
|
|
3179
|
+
skelI32[base + 1] = skinEnd + sk.parentOffset;
|
|
3180
|
+
skelI32[base + 2] = parentEnd + sk.topoOffset;
|
|
3181
|
+
skelI32[base + 3] = sk.ibmOffset;
|
|
3182
|
+
skelI32[base + 4] = sk.restTRSOffset * 10;
|
|
3183
|
+
skelI32[base + 5] = sk.skelRootMatIndex;
|
|
3184
|
+
skelI32[base + 6] = sk.clipOffset;
|
|
3185
|
+
skelI32[base + 7] = jointLookupOffset + sk.jointLookupStart;
|
|
3186
|
+
skelI32[base + 8] = 0;
|
|
3187
|
+
skelI32[base + 9] = 0;
|
|
3154
3188
|
}
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
for (let j = 0; j < skinData.jointCount; j++) {
|
|
3168
|
-
nodeToJoint.set(skinData.jointNodeIndices[j], j);
|
|
3169
|
-
}
|
|
3170
|
-
const clips = [];
|
|
3171
|
-
for (const anim of gltf.animations) {
|
|
3172
|
-
const channels = [];
|
|
3173
|
-
let maxTime = 0;
|
|
3174
|
-
for (const channel of anim.channels) {
|
|
3175
|
-
const targetNode = channel.target.node;
|
|
3176
|
-
const jointIndex = nodeToJoint.get(targetNode);
|
|
3177
|
-
if (jointIndex === void 0)
|
|
3178
|
-
continue;
|
|
3179
|
-
const path = channel.target.path;
|
|
3180
|
-
if (path !== "translation" && path !== "rotation" && path !== "scale")
|
|
3181
|
-
continue;
|
|
3182
|
-
const sampler = anim.samplers[channel.sampler];
|
|
3183
|
-
const interpolation = sampler.interpolation ?? "LINEAR";
|
|
3184
|
-
if (interpolation !== "LINEAR" && interpolation !== "STEP")
|
|
3185
|
-
continue;
|
|
3186
|
-
const inputAccess = getAccessorData(sampler.input);
|
|
3187
|
-
const timestamps = new Float32Array(inputAccess.data);
|
|
3188
|
-
const outputAccess = getAccessorData(sampler.output);
|
|
3189
|
-
const values = new Float32Array(outputAccess.data);
|
|
3190
|
-
if (timestamps.length > 0) {
|
|
3191
|
-
const lastTime = timestamps[timestamps.length - 1];
|
|
3192
|
-
if (lastTime > maxTime)
|
|
3193
|
-
maxTime = lastTime;
|
|
3194
|
-
}
|
|
3195
|
-
channels.push({
|
|
3196
|
-
jointIndex,
|
|
3197
|
-
path,
|
|
3198
|
-
timestamps,
|
|
3199
|
-
values,
|
|
3200
|
-
interpolation
|
|
3201
|
-
});
|
|
3202
|
-
}
|
|
3203
|
-
if (channels.length > 0) {
|
|
3204
|
-
clips.push({
|
|
3205
|
-
name: anim.name ?? `animation_${clips.length}`,
|
|
3206
|
-
duration: maxTime,
|
|
3207
|
-
channels
|
|
3208
|
-
});
|
|
3209
|
-
}
|
|
3189
|
+
for (let i = 0; i < packed.parentIndices.length; i++)
|
|
3190
|
+
skelI32[skinEnd + i] = packed.parentIndices[i];
|
|
3191
|
+
for (let i = 0; i < packed.topoOrder.length; i++)
|
|
3192
|
+
skelI32[parentEnd + i] = packed.topoOrder[i];
|
|
3193
|
+
const dv = new DataView(skelI32.buffer);
|
|
3194
|
+
for (let c = 0; c < numClips; c++) {
|
|
3195
|
+
const cl = packed.clips[c];
|
|
3196
|
+
const base = topoEnd + c * CLIP_ENTRY;
|
|
3197
|
+
skelI32[base + 0] = cl.channelStart;
|
|
3198
|
+
skelI32[base + 1] = cl.channelCount;
|
|
3199
|
+
dv.setFloat32(base * 4 + 8, cl.duration, true);
|
|
3200
|
+
skelI32[base + 3] = cl.looping;
|
|
3210
3201
|
}
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3202
|
+
for (let c = 0; c < numChannels; c++) {
|
|
3203
|
+
const ch = packed.channels[c];
|
|
3204
|
+
const base = clipEnd + c * CH_ENTRY;
|
|
3205
|
+
skelI32[base + 0] = ch.jointIndex;
|
|
3206
|
+
skelI32[base + 1] = ch.pathAndInterp;
|
|
3207
|
+
skelI32[base + 2] = ch.keyframeCount;
|
|
3208
|
+
skelI32[base + 3] = ch.dataOffset;
|
|
3216
3209
|
}
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
const joints = jointsAccess.data instanceof Uint16Array ? jointsAccess.data : new Uint16Array(jointsAccess.data);
|
|
3220
|
-
let weights;
|
|
3221
|
-
if (weightsAccess.data instanceof Float32Array) {
|
|
3222
|
-
weights = weightsAccess.data;
|
|
3223
|
-
} else if (weightsAccess.data instanceof Uint8Array) {
|
|
3224
|
-
weights = new Float32Array(weightsAccess.data.length);
|
|
3225
|
-
for (let i = 0; i < weightsAccess.data.length; i++) {
|
|
3226
|
-
weights[i] = weightsAccess.data[i] / 255;
|
|
3227
|
-
}
|
|
3228
|
-
} else if (weightsAccess.data instanceof Uint16Array) {
|
|
3229
|
-
weights = new Float32Array(weightsAccess.data.length);
|
|
3230
|
-
for (let i = 0; i < weightsAccess.data.length; i++) {
|
|
3231
|
-
weights[i] = weightsAccess.data[i] / 65535;
|
|
3232
|
-
}
|
|
3233
|
-
} else {
|
|
3234
|
-
weights = new Float32Array(weightsAccess.data);
|
|
3210
|
+
for (let i = 0; i < packed.jointChannelLookup.length; i++) {
|
|
3211
|
+
skelI32[channelEnd + i] = packed.jointChannelLookup[i];
|
|
3235
3212
|
}
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
trs[6] = 1;
|
|
3248
|
-
trs[7] = 1;
|
|
3249
|
-
trs[8] = 1;
|
|
3250
|
-
trs[9] = 1;
|
|
3251
|
-
return trs;
|
|
3252
|
-
}
|
|
3253
|
-
const t = node.translation ?? [0, 0, 0];
|
|
3254
|
-
const r = node.rotation ?? [0, 0, 0, 1];
|
|
3255
|
-
const s = node.scale ?? [1, 1, 1];
|
|
3256
|
-
trs[0] = t[0];
|
|
3257
|
-
trs[1] = t[1];
|
|
3258
|
-
trs[2] = t[2];
|
|
3259
|
-
trs[3] = r[0];
|
|
3260
|
-
trs[4] = r[1];
|
|
3261
|
-
trs[5] = r[2];
|
|
3262
|
-
trs[6] = r[3];
|
|
3263
|
-
trs[7] = s[0];
|
|
3264
|
-
trs[8] = s[1];
|
|
3265
|
-
trs[9] = s[2];
|
|
3266
|
-
return trs;
|
|
3267
|
-
}
|
|
3268
|
-
function createPackedAnimationData() {
|
|
3269
|
-
return {
|
|
3270
|
-
channels: [],
|
|
3271
|
-
keyframeData: [],
|
|
3272
|
-
clips: [],
|
|
3273
|
-
skins: [],
|
|
3274
|
-
parentIndices: [],
|
|
3275
|
-
topoOrder: [],
|
|
3276
|
-
ibmData: [],
|
|
3277
|
-
restTRS: [],
|
|
3278
|
-
skelRootMats: [],
|
|
3279
|
-
jointChannelLookup: []
|
|
3280
|
-
};
|
|
3281
|
-
}
|
|
3282
|
-
function packSkinAndAnimations(packed, skinData, clips, gltfNodes) {
|
|
3283
|
-
const skinIndex = packed.skins.length;
|
|
3284
|
-
const jc = skinData.jointCount;
|
|
3285
|
-
const parentOffset = packed.parentIndices.length;
|
|
3286
|
-
const topoOffset = packed.topoOrder.length;
|
|
3287
|
-
const ibmOffset = packed.ibmData.length / 16;
|
|
3288
|
-
const restTRSOffset = packed.restTRS.length / 10;
|
|
3289
|
-
const skelRootMatIndex = packed.skelRootMats.length / 16;
|
|
3290
|
-
for (let j = 0; j < jc; j++) {
|
|
3291
|
-
packed.parentIndices.push(skinData.parentJointIndices[j]);
|
|
3292
|
-
}
|
|
3293
|
-
const visited = new Uint8Array(jc);
|
|
3294
|
-
const visit = (j) => {
|
|
3295
|
-
if (visited[j])
|
|
3296
|
-
return;
|
|
3297
|
-
visited[j] = 1;
|
|
3298
|
-
const parent = skinData.parentJointIndices[j];
|
|
3299
|
-
if (parent !== -1)
|
|
3300
|
-
visit(parent);
|
|
3301
|
-
packed.topoOrder.push(j);
|
|
3302
|
-
};
|
|
3303
|
-
for (let j = 0; j < jc; j++)
|
|
3304
|
-
visit(j);
|
|
3305
|
-
for (let i = 0; i < skinData.inverseBindMatrices.length; i++) {
|
|
3306
|
-
packed.ibmData.push(skinData.inverseBindMatrices[i]);
|
|
3307
|
-
}
|
|
3308
|
-
for (let j = 0; j < jc; j++) {
|
|
3309
|
-
const trs = getNodeTRS(gltfNodes[skinData.jointNodeIndices[j]]);
|
|
3310
|
-
for (let k = 0; k < 10; k++)
|
|
3311
|
-
packed.restTRS.push(trs[k]);
|
|
3312
|
-
}
|
|
3313
|
-
if (skinData.skeletonRootMatrix) {
|
|
3314
|
-
for (let i = 0; i < 16; i++)
|
|
3315
|
-
packed.skelRootMats.push(skinData.skeletonRootMatrix[i]);
|
|
3316
|
-
} else {
|
|
3317
|
-
const id = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
3318
|
-
for (let i = 0; i < 16; i++)
|
|
3319
|
-
packed.skelRootMats.push(id[i]);
|
|
3320
|
-
}
|
|
3321
|
-
const clipOffset = packed.clips.length;
|
|
3322
|
-
const jointLookupStart = packed.jointChannelLookup.length;
|
|
3323
|
-
packed.skins.push({ jointCount: jc, parentOffset, topoOffset, ibmOffset, restTRSOffset, skelRootMatIndex, clipOffset, jointLookupStart });
|
|
3324
|
-
const nodeToJoint = /* @__PURE__ */ new Map();
|
|
3325
|
-
for (let j = 0; j < jc; j++) {
|
|
3326
|
-
nodeToJoint.set(skinData.jointNodeIndices[j], j);
|
|
3327
|
-
}
|
|
3328
|
-
for (const clip of clips) {
|
|
3329
|
-
const channelStart = packed.channels.length;
|
|
3330
|
-
const sortedChannels = [...clip.channels].sort((a, b) => a.jointIndex - b.jointIndex);
|
|
3331
|
-
for (const ch of sortedChannels) {
|
|
3332
|
-
const pathCode = ch.path === "translation" ? 0 : ch.path === "rotation" ? 1 : 2;
|
|
3333
|
-
const isStep = ch.interpolation === "STEP" ? 4 : 0;
|
|
3334
|
-
const dataOffset = packed.keyframeData.length;
|
|
3335
|
-
const n = ch.timestamps.length;
|
|
3336
|
-
const compCount = ch.path === "rotation" ? 4 : 3;
|
|
3337
|
-
for (let i = 0; i < n; i++)
|
|
3338
|
-
packed.keyframeData.push(ch.timestamps[i]);
|
|
3339
|
-
for (let i = 0; i < n * compCount; i++)
|
|
3340
|
-
packed.keyframeData.push(ch.values[i]);
|
|
3341
|
-
packed.channels.push({
|
|
3342
|
-
jointIndex: ch.jointIndex,
|
|
3343
|
-
pathAndInterp: pathCode | isStep,
|
|
3344
|
-
keyframeCount: n,
|
|
3345
|
-
dataOffset
|
|
3346
|
-
});
|
|
3347
|
-
}
|
|
3348
|
-
const clipChannelCount = sortedChannels.length;
|
|
3349
|
-
let ci = 0;
|
|
3350
|
-
for (let j = 0; j < jc; j++) {
|
|
3351
|
-
const lookupStart = ci;
|
|
3352
|
-
while (ci < clipChannelCount && sortedChannels[ci].jointIndex === j)
|
|
3353
|
-
ci++;
|
|
3354
|
-
packed.jointChannelLookup.push(channelStart + lookupStart);
|
|
3355
|
-
packed.jointChannelLookup.push(ci - lookupStart);
|
|
3356
|
-
}
|
|
3357
|
-
packed.clips.push({
|
|
3358
|
-
channelStart,
|
|
3359
|
-
channelCount: clipChannelCount,
|
|
3360
|
-
duration: clip.duration,
|
|
3361
|
-
looping: 1
|
|
3362
|
-
});
|
|
3213
|
+
const keyframeDataSize = packed.keyframeData.length;
|
|
3214
|
+
const restTRSOffset = keyframeDataSize;
|
|
3215
|
+
const animF32 = new Float32Array(keyframeDataSize + packed.restTRS.length || 1);
|
|
3216
|
+
for (let i = 0; i < keyframeDataSize; i++)
|
|
3217
|
+
animF32[i] = packed.keyframeData[i];
|
|
3218
|
+
for (let i = 0; i < packed.restTRS.length; i++)
|
|
3219
|
+
animF32[keyframeDataSize + i] = packed.restTRS[i];
|
|
3220
|
+
const totalIBM = packed.ibmData.length / 16;
|
|
3221
|
+
for (let s = 0; s < numSkins; s++) {
|
|
3222
|
+
skelI32[s * SKIN_ENTRY + 5] += totalIBM;
|
|
3223
|
+
skelI32[s * SKIN_ENTRY + 4] += restTRSOffset;
|
|
3363
3224
|
}
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
this.clipsByName = /* @__PURE__ */ new Map();
|
|
3373
|
-
this._scratchVec3 = new Float32Array(3);
|
|
3374
|
-
this._scratchQuat = new Float32Array(4);
|
|
3375
|
-
this.skinData = skinData;
|
|
3376
|
-
const jc = skinData.jointCount;
|
|
3377
|
-
this.localMatrices = new Float32Array(jc * 16);
|
|
3378
|
-
this.worldMatrices = new Float32Array(jc * 16);
|
|
3379
|
-
this.blendScratch = new Float32Array(jc * 16);
|
|
3380
|
-
this.originalRestPoseTRS = new Float32Array(jc * 10);
|
|
3381
|
-
this.currentTRS = new Float32Array(jc * 10);
|
|
3382
|
-
for (let j = 0; j < jc; j++) {
|
|
3383
|
-
const nodeTRS = getNodeTRS(gltfNodes[skinData.jointNodeIndices[j]]);
|
|
3384
|
-
this.originalRestPoseTRS.set(nodeTRS, j * 10);
|
|
3385
|
-
}
|
|
3386
|
-
this.skelRootMat = new Float32Array(16);
|
|
3387
|
-
if (skinData.skeletonRootMatrix) {
|
|
3388
|
-
this.skelRootMat.set(skinData.skeletonRootMatrix);
|
|
3389
|
-
} else {
|
|
3390
|
-
this.skelRootMat[0] = 1;
|
|
3391
|
-
this.skelRootMat[5] = 1;
|
|
3392
|
-
this.skelRootMat[10] = 1;
|
|
3393
|
-
this.skelRootMat[15] = 1;
|
|
3394
|
-
}
|
|
3395
|
-
this.topoOrder = new Uint16Array(jc);
|
|
3396
|
-
const visited = new Uint8Array(jc);
|
|
3397
|
-
let writeIdx = 0;
|
|
3398
|
-
const visit = (j) => {
|
|
3399
|
-
if (visited[j])
|
|
3400
|
-
return;
|
|
3401
|
-
visited[j] = 1;
|
|
3402
|
-
const parent = skinData.parentJointIndices[j];
|
|
3403
|
-
if (parent !== -1)
|
|
3404
|
-
visit(parent);
|
|
3405
|
-
this.topoOrder[writeIdx++] = j;
|
|
3406
|
-
};
|
|
3407
|
-
for (let j = 0; j < jc; j++)
|
|
3408
|
-
visit(j);
|
|
3409
|
-
for (const clip of clips) {
|
|
3410
|
-
this.loadClip(clip);
|
|
3411
|
-
}
|
|
3412
|
-
}
|
|
3413
|
-
loadClip(clip, loop = true) {
|
|
3414
|
-
const id = this.clips.length;
|
|
3415
|
-
this.clips.push({
|
|
3416
|
-
id,
|
|
3417
|
-
name: clip.name,
|
|
3418
|
-
duration: clip.duration,
|
|
3419
|
-
channels: clip.channels,
|
|
3420
|
-
loop
|
|
3421
|
-
});
|
|
3422
|
-
this.clipsByName.set(clip.name, id);
|
|
3423
|
-
return id;
|
|
3424
|
-
}
|
|
3425
|
-
getClipId(name) {
|
|
3426
|
-
const id = this.clipsByName.get(name);
|
|
3427
|
-
if (id === void 0)
|
|
3428
|
-
throw new Error(`Skeletal clip "${name}" not found`);
|
|
3429
|
-
return id;
|
|
3430
|
-
}
|
|
3431
|
-
getClipNames() {
|
|
3432
|
-
return this.clips.map((c) => c.name);
|
|
3433
|
-
}
|
|
3434
|
-
getClip(id) {
|
|
3435
|
-
return this.clips[id] ?? null;
|
|
3436
|
-
}
|
|
3437
|
-
get clipCount() {
|
|
3438
|
-
return this.clips.length;
|
|
3439
|
-
}
|
|
3440
|
-
createState(clipIdOrName, speed = 1, playing = true) {
|
|
3441
|
-
const clipId = typeof clipIdOrName === "string" ? this.getClipId(clipIdOrName) : clipIdOrName;
|
|
3442
|
-
return { clipId, time: 0, speed, playing, loop: true, prevClipId: -1, prevTime: 0, prevSpeed: 1, blendWeight: 1, blendDuration: 0, onEnd: () => null };
|
|
3443
|
-
}
|
|
3444
|
-
play(state, clipIdOrName, opts) {
|
|
3445
|
-
const newClipId = typeof clipIdOrName === "string" ? this.getClipId(clipIdOrName) : clipIdOrName;
|
|
3446
|
-
const crossfade = opts?.crossfade ?? 0;
|
|
3447
|
-
if (crossfade > 0 && state.playing) {
|
|
3448
|
-
state.prevClipId = state.clipId;
|
|
3449
|
-
state.prevTime = state.time;
|
|
3450
|
-
state.prevSpeed = state.speed;
|
|
3451
|
-
state.blendWeight = 0;
|
|
3452
|
-
state.blendDuration = crossfade;
|
|
3453
|
-
} else {
|
|
3454
|
-
state.prevClipId = -1;
|
|
3455
|
-
state.blendWeight = 1;
|
|
3456
|
-
state.blendDuration = 0;
|
|
3457
|
-
}
|
|
3458
|
-
state.onEnd = opts?.onEnd ?? (() => null);
|
|
3459
|
-
state.loop = opts?.loop ?? true;
|
|
3460
|
-
state.clipId = newClipId;
|
|
3461
|
-
state.time = 0;
|
|
3462
|
-
state.playing = true;
|
|
3463
|
-
if (opts?.speed !== void 0)
|
|
3464
|
-
state.speed = opts.speed;
|
|
3465
|
-
}
|
|
3466
|
-
stop(state) {
|
|
3467
|
-
state.playing = false;
|
|
3468
|
-
}
|
|
3469
|
-
resume(state) {
|
|
3470
|
-
state.playing = true;
|
|
3471
|
-
}
|
|
3472
|
-
/**
|
|
3473
|
-
* Advance time and compute bone matrices.
|
|
3474
|
-
* Writes jointCount mat4s into `output` starting at float index `outputOffset`.
|
|
3475
|
-
* Handles crossfade blending automatically.
|
|
3476
|
-
*/
|
|
3477
|
-
update(state, deltaTime, output, outputOffset = 0) {
|
|
3478
|
-
const jc = this.skinData.jointCount;
|
|
3479
|
-
if (state.playing) {
|
|
3480
|
-
this.advanceTime(state, deltaTime);
|
|
3481
|
-
}
|
|
3482
|
-
const blending = state.prevClipId !== -1 && state.blendDuration > 0 && state.blendWeight < 1;
|
|
3483
|
-
if (blending) {
|
|
3484
|
-
state.blendWeight += deltaTime / state.blendDuration;
|
|
3485
|
-
if (state.blendWeight >= 1) {
|
|
3486
|
-
state.blendWeight = 1;
|
|
3487
|
-
state.prevClipId = -1;
|
|
3488
|
-
state.blendDuration = 0;
|
|
3489
|
-
}
|
|
3490
|
-
}
|
|
3491
|
-
if (blending && state.prevClipId !== -1) {
|
|
3492
|
-
const prevClip = this.clips[state.prevClipId];
|
|
3493
|
-
if (prevClip) {
|
|
3494
|
-
state.prevTime += deltaTime * state.prevSpeed;
|
|
3495
|
-
if (prevClip.loop && state.prevTime >= prevClip.duration) {
|
|
3496
|
-
state.prevTime %= prevClip.duration;
|
|
3497
|
-
}
|
|
3498
|
-
}
|
|
3499
|
-
this.evaluateClip(state.prevClipId, state.prevTime, this.blendScratch, 0);
|
|
3500
|
-
this.evaluateClip(state.clipId, state.time, output, outputOffset);
|
|
3501
|
-
const w = state.blendWeight;
|
|
3502
|
-
const oneMinusW = 1 - w;
|
|
3503
|
-
const scratch = this.blendScratch;
|
|
3504
|
-
const floatCount = jc * 16;
|
|
3505
|
-
for (let i = 0; i < floatCount; i++) {
|
|
3506
|
-
output[outputOffset + i] = scratch[i] * oneMinusW + output[outputOffset + i] * w;
|
|
3507
|
-
}
|
|
3508
|
-
} else {
|
|
3509
|
-
this.evaluateClip(state.clipId, state.time, output, outputOffset);
|
|
3510
|
-
}
|
|
3511
|
-
}
|
|
3512
|
-
/** Advance clip time with looping/clamping. */
|
|
3513
|
-
advanceTime(state, deltaTime) {
|
|
3514
|
-
const clip = this.clips[state.clipId];
|
|
3515
|
-
if (!clip || clip.duration <= 0)
|
|
3516
|
-
return;
|
|
3517
|
-
state.time += deltaTime * state.speed;
|
|
3518
|
-
if (state.time >= clip.duration) {
|
|
3519
|
-
if (clip.loop)
|
|
3520
|
-
state.time %= clip.duration;
|
|
3521
|
-
else {
|
|
3522
|
-
state.time = clip.duration - 1e-4;
|
|
3523
|
-
state.playing = false;
|
|
3524
|
-
}
|
|
3525
|
-
}
|
|
3526
|
-
if (state.time < 0) {
|
|
3527
|
-
if (clip.loop)
|
|
3528
|
-
state.time = clip.duration + state.time % clip.duration;
|
|
3529
|
-
else {
|
|
3530
|
-
state.time = 0;
|
|
3531
|
-
state.playing = false;
|
|
3532
|
-
}
|
|
3533
|
-
}
|
|
3534
|
-
}
|
|
3535
|
-
/** Evaluate a clip at a specific time and write bone matrices. */
|
|
3536
|
-
evaluateClip(clipId, time, output, outputOffset) {
|
|
3537
|
-
const clip = this.clips[clipId];
|
|
3538
|
-
if (!clip)
|
|
3539
|
-
return;
|
|
3540
|
-
const skin = this.skinData;
|
|
3541
|
-
const jc = skin.jointCount;
|
|
3542
|
-
const local = this.localMatrices;
|
|
3543
|
-
const world = this.worldMatrices;
|
|
3544
|
-
const cur = this.currentTRS;
|
|
3545
|
-
cur.set(this.originalRestPoseTRS);
|
|
3546
|
-
for (const ch of clip.channels) {
|
|
3547
|
-
const j = ch.jointIndex;
|
|
3548
|
-
if (j >= jc)
|
|
3549
|
-
continue;
|
|
3550
|
-
const ro = j * 10;
|
|
3551
|
-
const sampled = this.sampleChannel(ch, time);
|
|
3552
|
-
if (ch.path === "translation") {
|
|
3553
|
-
cur[ro] = sampled[0];
|
|
3554
|
-
cur[ro + 1] = sampled[1];
|
|
3555
|
-
cur[ro + 2] = sampled[2];
|
|
3556
|
-
} else if (ch.path === "rotation") {
|
|
3557
|
-
cur[ro + 3] = sampled[0];
|
|
3558
|
-
cur[ro + 4] = sampled[1];
|
|
3559
|
-
cur[ro + 5] = sampled[2];
|
|
3560
|
-
cur[ro + 6] = sampled[3];
|
|
3561
|
-
} else if (ch.path === "scale") {
|
|
3562
|
-
cur[ro + 7] = sampled[0];
|
|
3563
|
-
cur[ro + 8] = sampled[1];
|
|
3564
|
-
cur[ro + 9] = sampled[2];
|
|
3565
|
-
}
|
|
3566
|
-
}
|
|
3567
|
-
for (let j = 0; j < jc; j++) {
|
|
3568
|
-
const ro = j * 10;
|
|
3569
|
-
trsToMat4(
|
|
3570
|
-
cur[ro],
|
|
3571
|
-
cur[ro + 1],
|
|
3572
|
-
cur[ro + 2],
|
|
3573
|
-
cur[ro + 3],
|
|
3574
|
-
cur[ro + 4],
|
|
3575
|
-
cur[ro + 5],
|
|
3576
|
-
cur[ro + 6],
|
|
3577
|
-
cur[ro + 7],
|
|
3578
|
-
cur[ro + 8],
|
|
3579
|
-
cur[ro + 9],
|
|
3580
|
-
local,
|
|
3581
|
-
j * 16
|
|
3582
|
-
);
|
|
3583
|
-
}
|
|
3584
|
-
const topo = this.topoOrder;
|
|
3585
|
-
const srm = this.skelRootMat;
|
|
3586
|
-
for (let i = 0; i < jc; i++) {
|
|
3587
|
-
const j = topo[i];
|
|
3588
|
-
const parentJ = skin.parentJointIndices[j];
|
|
3589
|
-
if (parentJ === -1) {
|
|
3590
|
-
mat4Mul(srm, 0, local, j * 16, world, j * 16);
|
|
3591
|
-
} else {
|
|
3592
|
-
mat4Mul(world, parentJ * 16, local, j * 16, world, j * 16);
|
|
3593
|
-
}
|
|
3594
|
-
}
|
|
3595
|
-
const ibm = skin.inverseBindMatrices;
|
|
3596
|
-
for (let j = 0; j < jc; j++) {
|
|
3597
|
-
mat4Mul(world, j * 16, ibm, j * 16, output, outputOffset + j * 16);
|
|
3598
|
-
}
|
|
3599
|
-
}
|
|
3600
|
-
/**
|
|
3601
|
-
* Compute bone matrices for the rest/bind pose (no animation).
|
|
3602
|
-
*/
|
|
3603
|
-
computeRestPose(output, outputOffset = 0) {
|
|
3604
|
-
const skin = this.skinData;
|
|
3605
|
-
const jc = skin.jointCount;
|
|
3606
|
-
const local = this.localMatrices;
|
|
3607
|
-
const world = this.worldMatrices;
|
|
3608
|
-
const rest = this.originalRestPoseTRS;
|
|
3609
|
-
for (let j = 0; j < jc; j++) {
|
|
3610
|
-
const ro = j * 10;
|
|
3611
|
-
trsToMat4(
|
|
3612
|
-
rest[ro],
|
|
3613
|
-
rest[ro + 1],
|
|
3614
|
-
rest[ro + 2],
|
|
3615
|
-
rest[ro + 3],
|
|
3616
|
-
rest[ro + 4],
|
|
3617
|
-
rest[ro + 5],
|
|
3618
|
-
rest[ro + 6],
|
|
3619
|
-
rest[ro + 7],
|
|
3620
|
-
rest[ro + 8],
|
|
3621
|
-
rest[ro + 9],
|
|
3622
|
-
local,
|
|
3623
|
-
j * 16
|
|
3624
|
-
);
|
|
3625
|
-
}
|
|
3626
|
-
const topo = this.topoOrder;
|
|
3627
|
-
const srm = this.skelRootMat;
|
|
3628
|
-
for (let i = 0; i < jc; i++) {
|
|
3629
|
-
const j = topo[i];
|
|
3630
|
-
const parentJ = skin.parentJointIndices[j];
|
|
3631
|
-
if (parentJ === -1) {
|
|
3632
|
-
mat4Mul(srm, 0, local, j * 16, world, j * 16);
|
|
3633
|
-
} else {
|
|
3634
|
-
mat4Mul(world, parentJ * 16, local, j * 16, world, j * 16);
|
|
3635
|
-
}
|
|
3636
|
-
}
|
|
3637
|
-
const ibm = skin.inverseBindMatrices;
|
|
3638
|
-
for (let j = 0; j < jc; j++) {
|
|
3639
|
-
mat4Mul(world, j * 16, ibm, j * 16, output, outputOffset + j * 16);
|
|
3640
|
-
}
|
|
3641
|
-
}
|
|
3642
|
-
// --- Private helpers (zero-alloc, operate on Float32Array sub-regions) ---
|
|
3643
|
-
/** Sample an animation channel at time t. Returns 3 or 4 floats. */
|
|
3644
|
-
sampleChannel(ch, t) {
|
|
3645
|
-
const ts = ch.timestamps;
|
|
3646
|
-
const vals = ch.values;
|
|
3647
|
-
const compCount = ch.path === "rotation" ? 4 : 3;
|
|
3648
|
-
if (t <= ts[0])
|
|
3649
|
-
return vals.subarray(0, compCount);
|
|
3650
|
-
if (t >= ts[ts.length - 1])
|
|
3651
|
-
return vals.subarray((ts.length - 1) * compCount, ts.length * compCount);
|
|
3652
|
-
let lo = 0, hi = ts.length - 1;
|
|
3653
|
-
while (lo < hi - 1) {
|
|
3654
|
-
const mid = lo + hi >> 1;
|
|
3655
|
-
if (ts[mid] <= t)
|
|
3656
|
-
lo = mid;
|
|
3657
|
-
else
|
|
3658
|
-
hi = mid;
|
|
3659
|
-
}
|
|
3660
|
-
if (ch.interpolation === "STEP") {
|
|
3661
|
-
return vals.subarray(lo * compCount, lo * compCount + compCount);
|
|
3662
|
-
}
|
|
3663
|
-
const t0 = ts[lo], t1 = ts[hi];
|
|
3664
|
-
const f = t1 > t0 ? (t - t0) / (t1 - t0) : 0;
|
|
3665
|
-
const a = lo * compCount;
|
|
3666
|
-
const b = hi * compCount;
|
|
3667
|
-
if (ch.path === "rotation") {
|
|
3668
|
-
return this.nlerpQuat(vals, a, vals, b, f);
|
|
3669
|
-
}
|
|
3670
|
-
const out = this._scratchVec3;
|
|
3671
|
-
out[0] = vals[a] + (vals[b] - vals[a]) * f;
|
|
3672
|
-
out[1] = vals[a + 1] + (vals[b + 1] - vals[a + 1]) * f;
|
|
3673
|
-
out[2] = vals[a + 2] + (vals[b + 2] - vals[a + 2]) * f;
|
|
3674
|
-
return out;
|
|
3675
|
-
}
|
|
3676
|
-
/** Normalized lerp for quaternions (nlerp). */
|
|
3677
|
-
nlerpQuat(a, ao, b, bo, t) {
|
|
3678
|
-
const out = this._scratchQuat;
|
|
3679
|
-
let dot2 = a[ao] * b[bo] + a[ao + 1] * b[bo + 1] + a[ao + 2] * b[bo + 2] + a[ao + 3] * b[bo + 3];
|
|
3680
|
-
const sign = dot2 < 0 ? -1 : 1;
|
|
3681
|
-
const oneMinusT = 1 - t;
|
|
3682
|
-
out[0] = oneMinusT * a[ao] + t * b[bo] * sign;
|
|
3683
|
-
out[1] = oneMinusT * a[ao + 1] + t * b[bo + 1] * sign;
|
|
3684
|
-
out[2] = oneMinusT * a[ao + 2] + t * b[bo + 2] * sign;
|
|
3685
|
-
out[3] = oneMinusT * a[ao + 3] + t * b[bo + 3] * sign;
|
|
3686
|
-
const len = Math.sqrt(out[0] * out[0] + out[1] * out[1] + out[2] * out[2] + out[3] * out[3]);
|
|
3687
|
-
if (len > 0) {
|
|
3688
|
-
const inv = 1 / len;
|
|
3689
|
-
out[0] *= inv;
|
|
3690
|
-
out[1] *= inv;
|
|
3691
|
-
out[2] *= inv;
|
|
3692
|
-
out[3] *= inv;
|
|
3693
|
-
}
|
|
3694
|
-
return out;
|
|
3695
|
-
}
|
|
3696
|
-
};
|
|
3697
|
-
|
|
3698
|
-
// src/3d/skeletal-animation-compute/packer.ts
|
|
3699
|
-
function packAnimationData(packed) {
|
|
3700
|
-
const numSkins = packed.skins.length;
|
|
3701
|
-
const numClips = packed.clips.length;
|
|
3702
|
-
const numChannels = packed.channels.length;
|
|
3703
|
-
const SKIN_ENTRY = 10;
|
|
3704
|
-
const CLIP_ENTRY = 4;
|
|
3705
|
-
const CH_ENTRY = 4;
|
|
3706
|
-
const skinEnd = numSkins * SKIN_ENTRY;
|
|
3707
|
-
const parentEnd = skinEnd + packed.parentIndices.length;
|
|
3708
|
-
const topoEnd = parentEnd + packed.topoOrder.length;
|
|
3709
|
-
const clipEnd = topoEnd + numClips * CLIP_ENTRY;
|
|
3710
|
-
const channelEnd = clipEnd + numChannels * CH_ENTRY;
|
|
3711
|
-
const jointLookupEnd = channelEnd + packed.jointChannelLookup.length;
|
|
3712
|
-
const clipTableOffset = topoEnd;
|
|
3713
|
-
const channelTableOffset = clipEnd;
|
|
3714
|
-
const jointLookupOffset = channelEnd;
|
|
3715
|
-
const skelI32 = new Int32Array(jointLookupEnd || 1);
|
|
3716
|
-
for (let s = 0; s < numSkins; s++) {
|
|
3717
|
-
const sk = packed.skins[s];
|
|
3718
|
-
const base = s * SKIN_ENTRY;
|
|
3719
|
-
skelI32[base + 0] = sk.jointCount;
|
|
3720
|
-
skelI32[base + 1] = skinEnd + sk.parentOffset;
|
|
3721
|
-
skelI32[base + 2] = parentEnd + sk.topoOffset;
|
|
3722
|
-
skelI32[base + 3] = sk.ibmOffset;
|
|
3723
|
-
skelI32[base + 4] = sk.restTRSOffset * 10;
|
|
3724
|
-
skelI32[base + 5] = sk.skelRootMatIndex;
|
|
3725
|
-
skelI32[base + 6] = sk.clipOffset;
|
|
3726
|
-
skelI32[base + 7] = jointLookupOffset + sk.jointLookupStart;
|
|
3727
|
-
skelI32[base + 8] = 0;
|
|
3728
|
-
skelI32[base + 9] = 0;
|
|
3729
|
-
}
|
|
3730
|
-
for (let i = 0; i < packed.parentIndices.length; i++)
|
|
3731
|
-
skelI32[skinEnd + i] = packed.parentIndices[i];
|
|
3732
|
-
for (let i = 0; i < packed.topoOrder.length; i++)
|
|
3733
|
-
skelI32[parentEnd + i] = packed.topoOrder[i];
|
|
3734
|
-
const dv = new DataView(skelI32.buffer);
|
|
3735
|
-
for (let c = 0; c < numClips; c++) {
|
|
3736
|
-
const cl = packed.clips[c];
|
|
3737
|
-
const base = topoEnd + c * CLIP_ENTRY;
|
|
3738
|
-
skelI32[base + 0] = cl.channelStart;
|
|
3739
|
-
skelI32[base + 1] = cl.channelCount;
|
|
3740
|
-
dv.setFloat32(base * 4 + 8, cl.duration, true);
|
|
3741
|
-
skelI32[base + 3] = cl.looping;
|
|
3742
|
-
}
|
|
3743
|
-
for (let c = 0; c < numChannels; c++) {
|
|
3744
|
-
const ch = packed.channels[c];
|
|
3745
|
-
const base = clipEnd + c * CH_ENTRY;
|
|
3746
|
-
skelI32[base + 0] = ch.jointIndex;
|
|
3747
|
-
skelI32[base + 1] = ch.pathAndInterp;
|
|
3748
|
-
skelI32[base + 2] = ch.keyframeCount;
|
|
3749
|
-
skelI32[base + 3] = ch.dataOffset;
|
|
3750
|
-
}
|
|
3751
|
-
for (let i = 0; i < packed.jointChannelLookup.length; i++) {
|
|
3752
|
-
skelI32[channelEnd + i] = packed.jointChannelLookup[i];
|
|
3753
|
-
}
|
|
3754
|
-
const keyframeDataSize = packed.keyframeData.length;
|
|
3755
|
-
const restTRSOffset = keyframeDataSize;
|
|
3756
|
-
const animF32 = new Float32Array(keyframeDataSize + packed.restTRS.length || 1);
|
|
3757
|
-
for (let i = 0; i < keyframeDataSize; i++)
|
|
3758
|
-
animF32[i] = packed.keyframeData[i];
|
|
3759
|
-
for (let i = 0; i < packed.restTRS.length; i++)
|
|
3760
|
-
animF32[keyframeDataSize + i] = packed.restTRS[i];
|
|
3761
|
-
const totalIBM = packed.ibmData.length / 16;
|
|
3762
|
-
for (let s = 0; s < numSkins; s++) {
|
|
3763
|
-
skelI32[s * SKIN_ENTRY + 5] += totalIBM;
|
|
3764
|
-
skelI32[s * SKIN_ENTRY + 4] += restTRSOffset;
|
|
3765
|
-
}
|
|
3766
|
-
const totalSkelRoot = packed.skelRootMats.length / 16;
|
|
3767
|
-
const totalMats = totalIBM + totalSkelRoot || 1;
|
|
3768
|
-
const matFloats = new Float32Array(totalMats * 16);
|
|
3769
|
-
for (let i = 0; i < packed.ibmData.length; i++)
|
|
3770
|
-
matFloats[i] = packed.ibmData[i];
|
|
3771
|
-
for (let i = 0; i < packed.skelRootMats.length; i++)
|
|
3772
|
-
matFloats[totalIBM * 16 + i] = packed.skelRootMats[i];
|
|
3773
|
-
return { skelI32, animF32, matFloats, clipTableOffset, channelTableOffset, jointLookupOffset, totalMats };
|
|
3225
|
+
const totalSkelRoot = packed.skelRootMats.length / 16;
|
|
3226
|
+
const totalMats = totalIBM + totalSkelRoot || 1;
|
|
3227
|
+
const matFloats = new Float32Array(totalMats * 16);
|
|
3228
|
+
for (let i = 0; i < packed.ibmData.length; i++)
|
|
3229
|
+
matFloats[i] = packed.ibmData[i];
|
|
3230
|
+
for (let i = 0; i < packed.skelRootMats.length; i++)
|
|
3231
|
+
matFloats[totalIBM * 16 + i] = packed.skelRootMats[i];
|
|
3232
|
+
return { skelI32, animF32, matFloats, clipTableOffset, channelTableOffset, jointLookupOffset, totalMats };
|
|
3774
3233
|
}
|
|
3775
3234
|
|
|
3776
3235
|
// src/3d/skeletal-animation-compute/kernel.ts
|
|
3777
3236
|
var WORKGROUP_SIZE = 64;
|
|
3778
|
-
function buildAnimationKernel(root, packed, maxInstances, maxTotalBones) {
|
|
3237
|
+
function buildAnimationKernel(root, packed, maxInstances, maxTotalBones, budgets) {
|
|
3779
3238
|
const pb = packAnimationData(packed);
|
|
3780
3239
|
const kernel = new ComputeBuilder("skeletal-animation", { workgroupSize: WORKGROUP_SIZE }, root).buffers({
|
|
3781
3240
|
uniforms: { uniform: AnimComputeUniforms },
|
|
3782
3241
|
instances: { storage: d.arrayOf(InstanceAnimStateGPU, maxInstances) },
|
|
3783
|
-
skelI32: { storage: d.arrayOf(d.i32,
|
|
3784
|
-
animF32: { storage: d.arrayOf(d.f32,
|
|
3785
|
-
matrices: { storage: d.arrayOf(d.mat4x4f,
|
|
3242
|
+
skelI32: { storage: d.arrayOf(d.i32, budgets.skelI32Capacity) },
|
|
3243
|
+
animF32: { storage: d.arrayOf(d.f32, budgets.animF32Capacity) },
|
|
3244
|
+
matrices: { storage: d.arrayOf(d.mat4x4f, budgets.matricesCapacity) },
|
|
3786
3245
|
boneMatrices: { storage: d.arrayOf(d.mat4x4f, maxTotalBones), readwrite: true }
|
|
3787
3246
|
}).shader(({ uniforms, instances, skelI32, animF32, matrices, boneMatrices }, { globalId }) => {
|
|
3788
3247
|
const idx = globalId.x;
|
|
@@ -4124,13 +3583,50 @@ function buildAnimationKernel(root, packed, maxInstances, maxTotalBones) {
|
|
|
4124
3583
|
}
|
|
4125
3584
|
}
|
|
4126
3585
|
}).build();
|
|
4127
|
-
|
|
4128
|
-
kernel
|
|
4129
|
-
const matBuffer = kernel.getBuffer("matrices");
|
|
4130
|
-
const rawMatBuffer = root.unwrap(matBuffer);
|
|
4131
|
-
root.device.queue.writeBuffer(rawMatBuffer, 0, pb.matFloats);
|
|
4132
|
-
return { kernel, packedBuffers: pb };
|
|
3586
|
+
uploadPackedToKernel(root, kernel, pb);
|
|
3587
|
+
return { kernel, packedBuffers: pb, budgets };
|
|
4133
3588
|
}
|
|
3589
|
+
function uploadPackedToKernel(root, kernel, pb) {
|
|
3590
|
+
const queue = root.device.queue;
|
|
3591
|
+
const skelBuf = root.unwrap(kernel.getBuffer("skelI32"));
|
|
3592
|
+
queue.writeBuffer(skelBuf, 0, pb.skelI32);
|
|
3593
|
+
const animBuf = root.unwrap(kernel.getBuffer("animF32"));
|
|
3594
|
+
queue.writeBuffer(animBuf, 0, pb.animF32);
|
|
3595
|
+
const matBuf = root.unwrap(kernel.getBuffer("matrices"));
|
|
3596
|
+
queue.writeBuffer(matBuf, 0, pb.matFloats);
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3599
|
+
// src/3d/clip-resync-coordinator.ts
|
|
3600
|
+
var GltfClipResyncCoordinator = class {
|
|
3601
|
+
constructor(bucket) {
|
|
3602
|
+
this.bucket = bucket;
|
|
3603
|
+
this.skinIndexByPrefabId = /* @__PURE__ */ new Map();
|
|
3604
|
+
this._pending = /* @__PURE__ */ new Set();
|
|
3605
|
+
this.bucket.events.on("clips-changed", ({ prefabId }) => {
|
|
3606
|
+
const skinIndex = this.skinIndexByPrefabId.get(prefabId);
|
|
3607
|
+
if (skinIndex !== void 0)
|
|
3608
|
+
this._pending.add(skinIndex);
|
|
3609
|
+
});
|
|
3610
|
+
}
|
|
3611
|
+
/** Map a prefab id to its index in the renderer's `skinnedModels` list. */
|
|
3612
|
+
registerSkin(prefabId, skinIndex) {
|
|
3613
|
+
this.skinIndexByPrefabId.set(prefabId, skinIndex);
|
|
3614
|
+
}
|
|
3615
|
+
/** Skin indices whose clip set has changed since the last `clear()`. */
|
|
3616
|
+
get pending() {
|
|
3617
|
+
return this._pending;
|
|
3618
|
+
}
|
|
3619
|
+
clear() {
|
|
3620
|
+
this._pending.clear();
|
|
3621
|
+
}
|
|
3622
|
+
/** Unsubscribe from the bucket and reset its animation clip sets. */
|
|
3623
|
+
dispose() {
|
|
3624
|
+
this.bucket.events.clear("clips-changed");
|
|
3625
|
+
this.bucket.resetAnimations();
|
|
3626
|
+
this.skinIndexByPrefabId.clear();
|
|
3627
|
+
this._pending.clear();
|
|
3628
|
+
}
|
|
3629
|
+
};
|
|
4134
3630
|
|
|
4135
3631
|
// src/3d/renderer.ts
|
|
4136
3632
|
var DYN_PREV_PX = 0;
|
|
@@ -4158,9 +3654,45 @@ var SSTAT_CR = 3;
|
|
|
4158
3654
|
var SSTAT_CG = 4;
|
|
4159
3655
|
var SSTAT_CB = 5;
|
|
4160
3656
|
var SSTAT_BONE_OFFSET = 6;
|
|
3657
|
+
var prefabHandles = /* @__PURE__ */ new WeakMap();
|
|
3658
|
+
function isPrefab3D(value) {
|
|
3659
|
+
const t = value.type;
|
|
3660
|
+
return t === "gltf" || t === "grid" || t === "cube" || t === "composite";
|
|
3661
|
+
}
|
|
3662
|
+
function resolveTransform(opts) {
|
|
3663
|
+
const [px, py, pz] = opts.position ?? [0, 0, 0];
|
|
3664
|
+
const [rx, ry, rz] = opts.rotation ?? [0, 0, 0];
|
|
3665
|
+
const s = opts.scale;
|
|
3666
|
+
const [sx, sy, sz] = typeof s === "number" ? [s, s, s] : s ?? [1, 1, 1];
|
|
3667
|
+
const [cr, cg, cb] = opts.color ?? [1, 1, 1];
|
|
3668
|
+
return { px, py, pz, rx, ry, rz, sx, sy, sz, cr, cg, cb };
|
|
3669
|
+
}
|
|
3670
|
+
function resolvePrefabHandle(prefab) {
|
|
3671
|
+
const h = prefabHandles.get(prefab);
|
|
3672
|
+
if (!h) {
|
|
3673
|
+
throw new Error(
|
|
3674
|
+
`Prefab '${prefab.id}' has no GPU handle \u2014 has the renderer's init() been called with this bucket?`
|
|
3675
|
+
);
|
|
3676
|
+
}
|
|
3677
|
+
return h;
|
|
3678
|
+
}
|
|
3679
|
+
function computeBucketStats(bucket) {
|
|
3680
|
+
let maxSkinnedParts = 0;
|
|
3681
|
+
let maxJointCount = 0;
|
|
3682
|
+
for (const prefab of bucket.entries()) {
|
|
3683
|
+
if (prefab.type === "gltf") {
|
|
3684
|
+
if (prefab.skinnedPartCount > maxSkinnedParts)
|
|
3685
|
+
maxSkinnedParts = prefab.skinnedPartCount;
|
|
3686
|
+
if (prefab.jointCount > maxJointCount)
|
|
3687
|
+
maxJointCount = prefab.jointCount;
|
|
3688
|
+
}
|
|
3689
|
+
}
|
|
3690
|
+
return { maxSkinnedParts, maxJointCount };
|
|
3691
|
+
}
|
|
4161
3692
|
var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
4162
3693
|
constructor(canvas, options) {
|
|
4163
|
-
|
|
3694
|
+
const resolvedMaxInstances = options.maxInstances ?? (options.prefabs ? options.prefabs.size + 16 : 32);
|
|
3695
|
+
super(canvas, { ...options, maxInstances: resolvedMaxInstances });
|
|
4164
3696
|
this.resizeObserver = null;
|
|
4165
3697
|
this.resizeCallbacks = [];
|
|
4166
3698
|
// layer=0, sheetId=modelId
|
|
@@ -4170,11 +3702,15 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
4170
3702
|
this.nextModelId = 0;
|
|
4171
3703
|
// Skinned model data
|
|
4172
3704
|
this.skinnedModels = [];
|
|
3705
|
+
// Null when constructed without a `prefabs` bucket; lazy load/unload requires it as event source.
|
|
3706
|
+
this.clipResync = null;
|
|
4173
3707
|
this.boneMatrixDirty = true;
|
|
4174
3708
|
// GPU animation compute
|
|
4175
3709
|
this.packedAnimData = createPackedAnimationData();
|
|
4176
3710
|
this.animComputeKernel = null;
|
|
4177
3711
|
this.animComputeNeedsRebuild = false;
|
|
3712
|
+
// Capacities the active kernel was built with — used to gate the in-place upload path on resync.
|
|
3713
|
+
this.animKernelBudgets = null;
|
|
4178
3714
|
this.animClipTableOffset = 0;
|
|
4179
3715
|
this.animChannelTableOffset = 0;
|
|
4180
3716
|
this.animJointLookupOffset = 0;
|
|
@@ -4182,27 +3718,37 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
4182
3718
|
this.skinnedStaticDirty = false;
|
|
4183
3719
|
this.skinnedAnimStates = [];
|
|
4184
3720
|
this.nextBoneOffset = 0;
|
|
3721
|
+
/** Reusable bone-offset blocks per skinIndex. Pushed on remove, popped on add. Indexed by skinIndex (low cardinality), so a Map of lists is fine. */
|
|
3722
|
+
this.freedBoneOffsets = /* @__PURE__ */ new Map();
|
|
4185
3723
|
// Frustum planes (6 planes × 4 floats each), extracted from VP matrix
|
|
4186
3724
|
this.frustumPlanes = new Float32Array(24);
|
|
4187
3725
|
this.uniformData = new Float32Array(24);
|
|
4188
3726
|
// mat4x4 (16) + alpha (1) + lightDir (3) + padding (4)
|
|
4189
3727
|
this.lastRenderTime = 0;
|
|
4190
3728
|
this.camera = new Camera3D();
|
|
4191
|
-
this.
|
|
4192
|
-
|
|
3729
|
+
this._prefabs = options.prefabs ?? null;
|
|
3730
|
+
const SKINNED_PARTS_PER_INSTANCE_DEFAULT_CAP = 3;
|
|
3731
|
+
const bucketStats = this._prefabs ? computeBucketStats(this._prefabs) : null;
|
|
3732
|
+
this.maxSkinnedInstances = options.maxSkinnedInstances ?? (bucketStats ? resolvedMaxInstances * Math.max(1, Math.min(bucketStats.maxSkinnedParts, SKINNED_PARTS_PER_INSTANCE_DEFAULT_CAP)) : 5e3);
|
|
3733
|
+
this.maxBonesPerSkin = options.maxBonesPerSkin ?? (bucketStats ? Math.max(1, bucketStats.maxJointCount) : 64);
|
|
3734
|
+
const cullDist = options.animationCullDistance ?? 50;
|
|
3735
|
+
this.animationCullDistanceSq = cullDist * cullDist;
|
|
4193
3736
|
this.maxTotalBones = this.maxSkinnedInstances * this.maxBonesPerSkin * 2;
|
|
4194
3737
|
this.updatedBoneOffsets = new Uint8Array(this.maxTotalBones);
|
|
4195
|
-
this.
|
|
4196
|
-
this.
|
|
4197
|
-
this.
|
|
4198
|
-
this.
|
|
4199
|
-
this.
|
|
4200
|
-
this.
|
|
3738
|
+
this.boneOffsetRefcount = new Uint32Array(this.maxTotalBones);
|
|
3739
|
+
this.boneOffsetSkinIndex = new Uint32Array(this.maxTotalBones);
|
|
3740
|
+
this.freeList = new FreeList3(resolvedMaxInstances);
|
|
3741
|
+
this.batcher = new SparseBatcher2(resolvedMaxInstances);
|
|
3742
|
+
this.dynamicData = new Float32Array(resolvedMaxInstances * DYNAMIC_MESH_FLOATS);
|
|
3743
|
+
this.staticData = new Float32Array(resolvedMaxInstances * STATIC_MESH_FLOATS);
|
|
3744
|
+
this.slotIndexData = new Uint32Array(resolvedMaxInstances);
|
|
3745
|
+
this.instanceModelIds = new Uint8Array(resolvedMaxInstances);
|
|
4201
3746
|
const msi = this.maxSkinnedInstances;
|
|
4202
3747
|
this.skinnedFreeList = new FreeList3(msi);
|
|
4203
3748
|
this.skinnedBatcher = new SparseBatcher2(msi);
|
|
4204
3749
|
this.skinnedDynamicData = new Float32Array(msi * DYNAMIC_MESH_FLOATS);
|
|
4205
3750
|
this.skinnedStaticData = new Float32Array(msi * SKINNED_STATIC_MESH_FLOATS);
|
|
3751
|
+
this.skinnedStaticDV = new DataView(this.skinnedStaticData.buffer);
|
|
4206
3752
|
this.skinnedSlotIndexData = new Uint32Array(msi);
|
|
4207
3753
|
this.skinnedInstanceModelIds = new Uint8Array(msi);
|
|
4208
3754
|
this.skinnedInstanceBoneOffsets = new Uint32Array(msi);
|
|
@@ -4213,7 +3759,19 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
4213
3759
|
this.gpuInstDV = new DataView(this.gpuInstData.buffer);
|
|
4214
3760
|
}
|
|
4215
3761
|
async init() {
|
|
4216
|
-
|
|
3762
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
3763
|
+
if (!adapter)
|
|
3764
|
+
throw new Error("WebGPU3DRenderer: no GPU adapter available");
|
|
3765
|
+
const a = adapter.limits;
|
|
3766
|
+
const requiredLimits = {
|
|
3767
|
+
maxBufferSize: a.maxBufferSize,
|
|
3768
|
+
maxStorageBufferBindingSize: a.maxStorageBufferBindingSize,
|
|
3769
|
+
maxStorageBuffersPerShaderStage: a.maxStorageBuffersPerShaderStage,
|
|
3770
|
+
maxComputeWorkgroupStorageSize: a.maxComputeWorkgroupStorageSize,
|
|
3771
|
+
maxComputeInvocationsPerWorkgroup: a.maxComputeInvocationsPerWorkgroup
|
|
3772
|
+
};
|
|
3773
|
+
const device = await adapter.requestDevice({ requiredLimits });
|
|
3774
|
+
this.root = tgpu6.initFromDevice({ device });
|
|
4217
3775
|
this.device = this.root.device;
|
|
4218
3776
|
this.context = this.canvas.getContext("webgpu");
|
|
4219
3777
|
this.format = navigator.gpu.getPreferredCanvasFormat();
|
|
@@ -4230,7 +3788,7 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
4230
3788
|
format: "depth24plus",
|
|
4231
3789
|
usage: GPUTextureUsage.RENDER_ATTACHMENT
|
|
4232
3790
|
});
|
|
4233
|
-
this.meshLayout = createMeshLayout(this.
|
|
3791
|
+
this.meshLayout = createMeshLayout(this.maxInstances);
|
|
4234
3792
|
const depthStencil = {
|
|
4235
3793
|
format: "depth24plus",
|
|
4236
3794
|
depthWriteEnabled: true,
|
|
@@ -4277,10 +3835,10 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
4277
3835
|
primitive,
|
|
4278
3836
|
depthStencil
|
|
4279
3837
|
});
|
|
4280
|
-
this.dynamicBuffer = this.root.createBuffer(d.arrayOf(DynamicMesh, this.
|
|
4281
|
-
this.staticBuffer = this.root.createBuffer(d.arrayOf(StaticMesh, this.
|
|
3838
|
+
this.dynamicBuffer = this.root.createBuffer(d.arrayOf(DynamicMesh, this.maxInstances)).$usage("storage");
|
|
3839
|
+
this.staticBuffer = this.root.createBuffer(d.arrayOf(StaticMesh, this.maxInstances)).$usage("storage");
|
|
4282
3840
|
this.uniformBuffer = this.root.createBuffer(MeshUniforms).$usage("uniform");
|
|
4283
|
-
this.slotIndexBuffer = this.root.createBuffer(d.arrayOf(d.u32, this.
|
|
3841
|
+
this.slotIndexBuffer = this.root.createBuffer(d.arrayOf(d.u32, this.maxInstances)).$usage("storage");
|
|
4284
3842
|
this.rawDynamicBuffer = this.root.unwrap(this.dynamicBuffer);
|
|
4285
3843
|
this.rawStaticBuffer = this.root.unwrap(this.staticBuffer);
|
|
4286
3844
|
this.rawUniformBuffer = this.root.unwrap(this.uniformBuffer);
|
|
@@ -4353,9 +3911,41 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
4353
3911
|
{ binding: 4, resource: { buffer: this.rawBoneMatrixBuffer } }
|
|
4354
3912
|
]
|
|
4355
3913
|
});
|
|
3914
|
+
if (this._prefabs) {
|
|
3915
|
+
this.uploadPrefabBucket(this._prefabs);
|
|
3916
|
+
}
|
|
4356
3917
|
this.setupResizeObserver();
|
|
4357
3918
|
this._initialized = true;
|
|
4358
3919
|
}
|
|
3920
|
+
/**
|
|
3921
|
+
* Upload every prefab in the bucket to the GPU and stash the handle on
|
|
3922
|
+
* each prefab so `bucket.get(id)` resolves to a usable model. Also
|
|
3923
|
+
* subscribes the resync coordinator to the bucket's `clips-changed`
|
|
3924
|
+
* channel for lazy load/unload.
|
|
3925
|
+
*/
|
|
3926
|
+
uploadPrefabBucket(bucket) {
|
|
3927
|
+
this.clipResync = new GltfClipResyncCoordinator(bucket);
|
|
3928
|
+
for (const prefab of bucket.entries()) {
|
|
3929
|
+
if (prefab.type === "gltf") {
|
|
3930
|
+
const beforeSkinCount = this.skinnedModels.length;
|
|
3931
|
+
const model = this.uploadParsedGltf(prefab.parsed);
|
|
3932
|
+
prefabHandles.set(prefab, model);
|
|
3933
|
+
if (this.skinnedModels.length > beforeSkinCount) {
|
|
3934
|
+
this.clipResync.registerSkin(prefab.id, beforeSkinCount);
|
|
3935
|
+
}
|
|
3936
|
+
} else if (prefab.type === "grid") {
|
|
3937
|
+
const model = this.createGrid({
|
|
3938
|
+
size: prefab.size,
|
|
3939
|
+
step: prefab.step,
|
|
3940
|
+
lineWidth: prefab.lineWidth
|
|
3941
|
+
});
|
|
3942
|
+
prefabHandles.set(prefab, model);
|
|
3943
|
+
} else if (prefab.type === "cube") {
|
|
3944
|
+
const model = this.createCube({ size: prefab.size });
|
|
3945
|
+
prefabHandles.set(prefab, model);
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
4359
3949
|
setupResizeObserver() {
|
|
4360
3950
|
const supportsDevicePixelBox = (() => {
|
|
4361
3951
|
try {
|
|
@@ -4417,6 +4007,36 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
4417
4007
|
createCompute(name, options) {
|
|
4418
4008
|
return new ComputeBuilder(name, options, this.root);
|
|
4419
4009
|
}
|
|
4010
|
+
/**
|
|
4011
|
+
* Set the maximum distance (in world units) at which the renderer keeps
|
|
4012
|
+
* computing skeletal animation. Skinned instances farther than this are
|
|
4013
|
+
* still drawn, but with their last-computed bone matrices instead of
|
|
4014
|
+
* fresh ones, which saves GPU compute work. Their internal animation
|
|
4015
|
+
* clocks keep ticking on CPU, so when they come back into range they
|
|
4016
|
+
* resume in sync.
|
|
4017
|
+
*
|
|
4018
|
+
* Lower values trade visual smoothness on distant characters for FPS.
|
|
4019
|
+
* Pass `Infinity` to disable culling entirely (always animate).
|
|
4020
|
+
*
|
|
4021
|
+
* Safe to call any time; takes effect on the next frame.
|
|
4022
|
+
*
|
|
4023
|
+
* @param distance Max distance to animate at, in world units.
|
|
4024
|
+
*/
|
|
4025
|
+
setAnimationCullDistance(distance) {
|
|
4026
|
+
this.animationCullDistanceSq = distance * distance;
|
|
4027
|
+
}
|
|
4028
|
+
/** Current animation cull distance (in world units). See `setAnimationCullDistance`. */
|
|
4029
|
+
get animationCullDistance() {
|
|
4030
|
+
return Math.sqrt(this.animationCullDistanceSq);
|
|
4031
|
+
}
|
|
4032
|
+
/**
|
|
4033
|
+
* Max skinned instances the renderer was sized for at construction.
|
|
4034
|
+
* Independent budget from `maxInstances` since skinned characters use a
|
|
4035
|
+
* separate set of GPU buffers. Read-only.
|
|
4036
|
+
*/
|
|
4037
|
+
get maxSkinned() {
|
|
4038
|
+
return this.maxSkinnedInstances;
|
|
4039
|
+
}
|
|
4420
4040
|
/**
|
|
4421
4041
|
* Create a flat grid mesh on the XZ plane at Y=0.
|
|
4422
4042
|
*
|
|
@@ -4448,6 +4068,245 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
4448
4068
|
indices: new Uint16Array(indices)
|
|
4449
4069
|
});
|
|
4450
4070
|
}
|
|
4071
|
+
/**
|
|
4072
|
+
* Create a unit-cube mesh centered at the origin. Pass `size` to scale the
|
|
4073
|
+
* edge length, or keep size = 1 and scale at the instance level.
|
|
4074
|
+
*
|
|
4075
|
+
* ```ts
|
|
4076
|
+
* const cube = renderer.createCube();
|
|
4077
|
+
* renderer.addInstance({ model: cube, color: [1, 0.5, 0.2], scale: 2 });
|
|
4078
|
+
* ```
|
|
4079
|
+
*/
|
|
4080
|
+
createCube(opts = {}) {
|
|
4081
|
+
const h = (opts.size ?? 1) / 2;
|
|
4082
|
+
const positions = new Float32Array([
|
|
4083
|
+
// +Z
|
|
4084
|
+
-h,
|
|
4085
|
+
-h,
|
|
4086
|
+
h,
|
|
4087
|
+
h,
|
|
4088
|
+
-h,
|
|
4089
|
+
h,
|
|
4090
|
+
h,
|
|
4091
|
+
h,
|
|
4092
|
+
h,
|
|
4093
|
+
-h,
|
|
4094
|
+
-h,
|
|
4095
|
+
h,
|
|
4096
|
+
h,
|
|
4097
|
+
h,
|
|
4098
|
+
h,
|
|
4099
|
+
-h,
|
|
4100
|
+
h,
|
|
4101
|
+
h,
|
|
4102
|
+
// -Z
|
|
4103
|
+
h,
|
|
4104
|
+
-h,
|
|
4105
|
+
-h,
|
|
4106
|
+
-h,
|
|
4107
|
+
-h,
|
|
4108
|
+
-h,
|
|
4109
|
+
-h,
|
|
4110
|
+
h,
|
|
4111
|
+
-h,
|
|
4112
|
+
h,
|
|
4113
|
+
-h,
|
|
4114
|
+
-h,
|
|
4115
|
+
-h,
|
|
4116
|
+
h,
|
|
4117
|
+
-h,
|
|
4118
|
+
h,
|
|
4119
|
+
h,
|
|
4120
|
+
-h,
|
|
4121
|
+
// +Y
|
|
4122
|
+
-h,
|
|
4123
|
+
h,
|
|
4124
|
+
h,
|
|
4125
|
+
h,
|
|
4126
|
+
h,
|
|
4127
|
+
h,
|
|
4128
|
+
h,
|
|
4129
|
+
h,
|
|
4130
|
+
-h,
|
|
4131
|
+
-h,
|
|
4132
|
+
h,
|
|
4133
|
+
h,
|
|
4134
|
+
h,
|
|
4135
|
+
h,
|
|
4136
|
+
-h,
|
|
4137
|
+
-h,
|
|
4138
|
+
h,
|
|
4139
|
+
-h,
|
|
4140
|
+
// -Y
|
|
4141
|
+
-h,
|
|
4142
|
+
-h,
|
|
4143
|
+
-h,
|
|
4144
|
+
h,
|
|
4145
|
+
-h,
|
|
4146
|
+
-h,
|
|
4147
|
+
h,
|
|
4148
|
+
-h,
|
|
4149
|
+
h,
|
|
4150
|
+
-h,
|
|
4151
|
+
-h,
|
|
4152
|
+
-h,
|
|
4153
|
+
h,
|
|
4154
|
+
-h,
|
|
4155
|
+
h,
|
|
4156
|
+
-h,
|
|
4157
|
+
-h,
|
|
4158
|
+
h,
|
|
4159
|
+
// +X
|
|
4160
|
+
h,
|
|
4161
|
+
-h,
|
|
4162
|
+
h,
|
|
4163
|
+
h,
|
|
4164
|
+
-h,
|
|
4165
|
+
-h,
|
|
4166
|
+
h,
|
|
4167
|
+
h,
|
|
4168
|
+
-h,
|
|
4169
|
+
h,
|
|
4170
|
+
-h,
|
|
4171
|
+
h,
|
|
4172
|
+
h,
|
|
4173
|
+
h,
|
|
4174
|
+
-h,
|
|
4175
|
+
h,
|
|
4176
|
+
h,
|
|
4177
|
+
h,
|
|
4178
|
+
// -X
|
|
4179
|
+
-h,
|
|
4180
|
+
-h,
|
|
4181
|
+
-h,
|
|
4182
|
+
-h,
|
|
4183
|
+
-h,
|
|
4184
|
+
h,
|
|
4185
|
+
-h,
|
|
4186
|
+
h,
|
|
4187
|
+
h,
|
|
4188
|
+
-h,
|
|
4189
|
+
-h,
|
|
4190
|
+
-h,
|
|
4191
|
+
-h,
|
|
4192
|
+
h,
|
|
4193
|
+
h,
|
|
4194
|
+
-h,
|
|
4195
|
+
h,
|
|
4196
|
+
-h
|
|
4197
|
+
]);
|
|
4198
|
+
const normals = new Float32Array([
|
|
4199
|
+
0,
|
|
4200
|
+
0,
|
|
4201
|
+
1,
|
|
4202
|
+
0,
|
|
4203
|
+
0,
|
|
4204
|
+
1,
|
|
4205
|
+
0,
|
|
4206
|
+
0,
|
|
4207
|
+
1,
|
|
4208
|
+
0,
|
|
4209
|
+
0,
|
|
4210
|
+
1,
|
|
4211
|
+
0,
|
|
4212
|
+
0,
|
|
4213
|
+
1,
|
|
4214
|
+
0,
|
|
4215
|
+
0,
|
|
4216
|
+
1,
|
|
4217
|
+
0,
|
|
4218
|
+
0,
|
|
4219
|
+
-1,
|
|
4220
|
+
0,
|
|
4221
|
+
0,
|
|
4222
|
+
-1,
|
|
4223
|
+
0,
|
|
4224
|
+
0,
|
|
4225
|
+
-1,
|
|
4226
|
+
0,
|
|
4227
|
+
0,
|
|
4228
|
+
-1,
|
|
4229
|
+
0,
|
|
4230
|
+
0,
|
|
4231
|
+
-1,
|
|
4232
|
+
0,
|
|
4233
|
+
0,
|
|
4234
|
+
-1,
|
|
4235
|
+
0,
|
|
4236
|
+
1,
|
|
4237
|
+
0,
|
|
4238
|
+
0,
|
|
4239
|
+
1,
|
|
4240
|
+
0,
|
|
4241
|
+
0,
|
|
4242
|
+
1,
|
|
4243
|
+
0,
|
|
4244
|
+
0,
|
|
4245
|
+
1,
|
|
4246
|
+
0,
|
|
4247
|
+
0,
|
|
4248
|
+
1,
|
|
4249
|
+
0,
|
|
4250
|
+
0,
|
|
4251
|
+
1,
|
|
4252
|
+
0,
|
|
4253
|
+
0,
|
|
4254
|
+
-1,
|
|
4255
|
+
0,
|
|
4256
|
+
0,
|
|
4257
|
+
-1,
|
|
4258
|
+
0,
|
|
4259
|
+
0,
|
|
4260
|
+
-1,
|
|
4261
|
+
0,
|
|
4262
|
+
0,
|
|
4263
|
+
-1,
|
|
4264
|
+
0,
|
|
4265
|
+
0,
|
|
4266
|
+
-1,
|
|
4267
|
+
0,
|
|
4268
|
+
0,
|
|
4269
|
+
-1,
|
|
4270
|
+
0,
|
|
4271
|
+
1,
|
|
4272
|
+
0,
|
|
4273
|
+
0,
|
|
4274
|
+
1,
|
|
4275
|
+
0,
|
|
4276
|
+
0,
|
|
4277
|
+
1,
|
|
4278
|
+
0,
|
|
4279
|
+
0,
|
|
4280
|
+
1,
|
|
4281
|
+
0,
|
|
4282
|
+
0,
|
|
4283
|
+
1,
|
|
4284
|
+
0,
|
|
4285
|
+
0,
|
|
4286
|
+
1,
|
|
4287
|
+
0,
|
|
4288
|
+
0,
|
|
4289
|
+
-1,
|
|
4290
|
+
0,
|
|
4291
|
+
0,
|
|
4292
|
+
-1,
|
|
4293
|
+
0,
|
|
4294
|
+
0,
|
|
4295
|
+
-1,
|
|
4296
|
+
0,
|
|
4297
|
+
0,
|
|
4298
|
+
-1,
|
|
4299
|
+
0,
|
|
4300
|
+
0,
|
|
4301
|
+
-1,
|
|
4302
|
+
0,
|
|
4303
|
+
0,
|
|
4304
|
+
-1,
|
|
4305
|
+
0,
|
|
4306
|
+
0
|
|
4307
|
+
]);
|
|
4308
|
+
return this.loadModel({ positions, normals });
|
|
4309
|
+
}
|
|
4451
4310
|
/**
|
|
4452
4311
|
* Register a model. Returns a handle for addInstance().
|
|
4453
4312
|
*
|
|
@@ -4689,108 +4548,20 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
4689
4548
|
* ```
|
|
4690
4549
|
*/
|
|
4691
4550
|
async loadGltf(url, opts) {
|
|
4692
|
-
const
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
const chunkType = new Uint32Array(arrayBuffer, offset + 4, 1)[0];
|
|
4703
|
-
offset += 8;
|
|
4704
|
-
if (chunkType === 1313821514) {
|
|
4705
|
-
const jsonBytes = new Uint8Array(arrayBuffer, offset, chunkLength);
|
|
4706
|
-
gltf = JSON.parse(new TextDecoder().decode(jsonBytes));
|
|
4707
|
-
} else if (chunkType === 5130562) {
|
|
4708
|
-
glbBinaryChunk = arrayBuffer.slice(offset, offset + chunkLength);
|
|
4709
|
-
}
|
|
4710
|
-
offset += chunkLength;
|
|
4711
|
-
}
|
|
4712
|
-
if (!gltf)
|
|
4713
|
-
throw new Error(`Invalid GLB: no JSON chunk in ${url}`);
|
|
4714
|
-
} else {
|
|
4715
|
-
gltf = JSON.parse(new TextDecoder().decode(arrayBuffer));
|
|
4716
|
-
}
|
|
4717
|
-
if (!gltf.meshes?.length)
|
|
4718
|
-
throw new Error(`No meshes found in ${url}`);
|
|
4719
|
-
const buffers = [];
|
|
4720
|
-
for (let i = 0; i < (gltf.buffers?.length ?? 0); i++) {
|
|
4721
|
-
const buf = gltf.buffers[i];
|
|
4722
|
-
if (glbBinaryChunk && (!buf.uri || buf.uri === "")) {
|
|
4723
|
-
buffers.push(glbBinaryChunk);
|
|
4724
|
-
} else if (buf.uri) {
|
|
4725
|
-
const r = await fetch(baseUrl + buf.uri);
|
|
4726
|
-
buffers.push(await r.arrayBuffer());
|
|
4727
|
-
}
|
|
4728
|
-
}
|
|
4729
|
-
const getAccessorData = (accessorIndex) => {
|
|
4730
|
-
const accessor = gltf.accessors[accessorIndex];
|
|
4731
|
-
const bufferView = gltf.bufferViews[accessor.bufferView];
|
|
4732
|
-
const buffer = buffers[bufferView.buffer];
|
|
4733
|
-
const typeMap = { 5120: Int8Array, 5121: Uint8Array, 5122: Int16Array, 5123: Uint16Array, 5125: Uint32Array, 5126: Float32Array };
|
|
4734
|
-
const byteSizeMap = { 5120: 1, 5121: 1, 5122: 2, 5123: 2, 5125: 4, 5126: 4 };
|
|
4735
|
-
const sizeMap = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4, MAT2: 4, MAT3: 9, MAT4: 16 };
|
|
4736
|
-
const TypedArray = typeMap[accessor.componentType];
|
|
4737
|
-
const componentBytes = byteSizeMap[accessor.componentType];
|
|
4738
|
-
const elementSize = sizeMap[accessor.type] ?? 1;
|
|
4739
|
-
const baseOffset = (bufferView.byteOffset ?? 0) + (accessor.byteOffset ?? 0);
|
|
4740
|
-
const stride = bufferView.byteStride ?? componentBytes * elementSize;
|
|
4741
|
-
const tightStride = componentBytes * elementSize;
|
|
4742
|
-
if (stride === tightStride) {
|
|
4743
|
-
const data = new TypedArray(buffer, baseOffset, accessor.count * elementSize);
|
|
4744
|
-
return { data, count: accessor.count, elementSize };
|
|
4745
|
-
}
|
|
4746
|
-
const out = new TypedArray(accessor.count * elementSize);
|
|
4747
|
-
const src = new Uint8Array(buffer);
|
|
4748
|
-
const dst = new Uint8Array(out.buffer);
|
|
4749
|
-
for (let i = 0; i < accessor.count; i++) {
|
|
4750
|
-
const srcOff = baseOffset + i * stride;
|
|
4751
|
-
const dstOff = i * tightStride;
|
|
4752
|
-
for (let b = 0; b < tightStride; b++) {
|
|
4753
|
-
dst[dstOff + b] = src[srcOff + b];
|
|
4754
|
-
}
|
|
4755
|
-
}
|
|
4756
|
-
return { data: out, count: accessor.count, elementSize };
|
|
4757
|
-
};
|
|
4758
|
-
const textureCache = /* @__PURE__ */ new Map();
|
|
4759
|
-
const loadTexture = async (imageIndex) => {
|
|
4760
|
-
if (textureCache.has(imageIndex))
|
|
4761
|
-
return textureCache.get(imageIndex);
|
|
4762
|
-
const image = gltf.images?.[imageIndex];
|
|
4763
|
-
if (!image)
|
|
4764
|
-
return void 0;
|
|
4765
|
-
let blob;
|
|
4766
|
-
if (image.bufferView !== void 0) {
|
|
4767
|
-
const bv = gltf.bufferViews[image.bufferView];
|
|
4768
|
-
const buf = buffers[bv.buffer];
|
|
4769
|
-
const data = new Uint8Array(buf, bv.byteOffset ?? 0, bv.byteLength);
|
|
4770
|
-
blob = new Blob([data], { type: image.mimeType ?? "image/png" });
|
|
4771
|
-
} else if (image.uri) {
|
|
4772
|
-
const imgUrl = image.uri.startsWith("data:") ? image.uri : baseUrl + image.uri;
|
|
4773
|
-
blob = await (await fetch(imgUrl)).blob();
|
|
4774
|
-
}
|
|
4775
|
-
if (blob) {
|
|
4776
|
-
const bmp = await createImageBitmap(blob);
|
|
4777
|
-
textureCache.set(imageIndex, bmp);
|
|
4778
|
-
return bmp;
|
|
4779
|
-
}
|
|
4780
|
-
return void 0;
|
|
4781
|
-
};
|
|
4782
|
-
const meshNodeIndex = gltf.nodes?.findIndex((n) => n.mesh !== void 0) ?? -1;
|
|
4783
|
-
const skinIndex = meshNodeIndex !== -1 ? gltf.nodes?.[meshNodeIndex]?.skin : void 0;
|
|
4784
|
-
let skinData = null;
|
|
4785
|
-
let animClips = [];
|
|
4551
|
+
const parsed = await parseGltf(url, opts);
|
|
4552
|
+
return this.uploadParsedGltf(parsed);
|
|
4553
|
+
}
|
|
4554
|
+
/**
|
|
4555
|
+
* Upload a previously-parsed glTF to the GPU. Returns a GltfModel handle.
|
|
4556
|
+
* Splitting parse (CPU) from upload (GPU) lets callers parse models in parallel
|
|
4557
|
+
* before a renderer exists and inspect joint counts / vertex totals to size the
|
|
4558
|
+
* renderer appropriately.
|
|
4559
|
+
*/
|
|
4560
|
+
uploadParsedGltf(parsed) {
|
|
4786
4561
|
let skinnedModelSkinIndex = -1;
|
|
4787
|
-
if (
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
if (opts?.animations) {
|
|
4791
|
-
animClips = animClips.filter((clip) => opts.animations.includes(clip.name));
|
|
4792
|
-
}
|
|
4793
|
-
const animation = new SkeletalAnimation(skinData, animClips, gltf.nodes);
|
|
4562
|
+
if (parsed.skin) {
|
|
4563
|
+
const { data: skinData, animClips } = parsed.skin;
|
|
4564
|
+
const animation = new SkeletalAnimation(skinData, animClips);
|
|
4794
4565
|
const ibm = skinData.inverseBindMatrices;
|
|
4795
4566
|
let maxRadSq = 0;
|
|
4796
4567
|
for (let j = 0; j < skinData.jointCount; j++) {
|
|
@@ -4804,91 +4575,34 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
4804
4575
|
this.skinnedModels.push({
|
|
4805
4576
|
animation,
|
|
4806
4577
|
jointCount: skinData.jointCount,
|
|
4807
|
-
boundingRadius: skinnedRadius
|
|
4578
|
+
boundingRadius: skinnedRadius,
|
|
4579
|
+
parsedSkin: parsed.skin
|
|
4808
4580
|
});
|
|
4809
|
-
packSkinAndAnimations(this.packedAnimData, skinData, animClips
|
|
4581
|
+
packSkinAndAnimations(this.packedAnimData, skinData, animClips);
|
|
4810
4582
|
this.animComputeNeedsRebuild = true;
|
|
4811
4583
|
}
|
|
4812
4584
|
const handles = [];
|
|
4813
|
-
const
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
const positions = new Float32Array(posAccess.data);
|
|
4835
|
-
let normals;
|
|
4836
|
-
if (primitive.attributes.NORMAL !== void 0) {
|
|
4837
|
-
normals = new Float32Array(getAccessorData(primitive.attributes.NORMAL).data);
|
|
4838
|
-
}
|
|
4839
|
-
let uvs;
|
|
4840
|
-
if (primitive.attributes.TEXCOORD_0 !== void 0) {
|
|
4841
|
-
uvs = new Float32Array(getAccessorData(primitive.attributes.TEXCOORD_0).data);
|
|
4842
|
-
}
|
|
4843
|
-
let indices;
|
|
4844
|
-
if (primitive.indices !== void 0) {
|
|
4845
|
-
const idxAccess = getAccessorData(primitive.indices);
|
|
4846
|
-
indices = idxAccess.data.length > 65535 ? new Uint32Array(idxAccess.data) : new Uint16Array(idxAccess.data);
|
|
4847
|
-
}
|
|
4848
|
-
if (thisMeshNodeMatrix && !isSkinned) {
|
|
4849
|
-
const meshNodeMatrix = thisMeshNodeMatrix;
|
|
4850
|
-
const mm = meshNodeMatrix;
|
|
4851
|
-
const vertexCount = positions.length / 3;
|
|
4852
|
-
for (let v = 0; v < vertexCount; v++) {
|
|
4853
|
-
const o = v * 3;
|
|
4854
|
-
const px = positions[o], py = positions[o + 1], pz = positions[o + 2];
|
|
4855
|
-
positions[o] = mm[0] * px + mm[4] * py + mm[8] * pz + mm[12];
|
|
4856
|
-
positions[o + 1] = mm[1] * px + mm[5] * py + mm[9] * pz + mm[13];
|
|
4857
|
-
positions[o + 2] = mm[2] * px + mm[6] * py + mm[10] * pz + mm[14];
|
|
4858
|
-
if (normals) {
|
|
4859
|
-
const nx = normals[o], ny = normals[o + 1], nz = normals[o + 2];
|
|
4860
|
-
const tnx = mm[0] * nx + mm[4] * ny + mm[8] * nz;
|
|
4861
|
-
const tny = mm[1] * nx + mm[5] * ny + mm[9] * nz;
|
|
4862
|
-
const tnz = mm[2] * nx + mm[6] * ny + mm[10] * nz;
|
|
4863
|
-
const len = Math.sqrt(tnx * tnx + tny * tny + tnz * tnz);
|
|
4864
|
-
if (len > 0) {
|
|
4865
|
-
normals[o] = tnx / len;
|
|
4866
|
-
normals[o + 1] = tny / len;
|
|
4867
|
-
normals[o + 2] = tnz / len;
|
|
4868
|
-
}
|
|
4869
|
-
}
|
|
4870
|
-
}
|
|
4871
|
-
}
|
|
4872
|
-
let texture;
|
|
4873
|
-
if (primitive.material !== void 0) {
|
|
4874
|
-
const material = gltf.materials?.[primitive.material];
|
|
4875
|
-
const texIndex = material?.pbrMetallicRoughness?.baseColorTexture?.index;
|
|
4876
|
-
if (texIndex !== void 0 && gltf.textures?.[texIndex]) {
|
|
4877
|
-
texture = await loadTexture(gltf.textures[texIndex].source);
|
|
4878
|
-
}
|
|
4879
|
-
}
|
|
4880
|
-
if (isSkinned) {
|
|
4881
|
-
const skinAttrs = parsePrimitiveSkinAttributes(primitive, getAccessorData);
|
|
4882
|
-
if (skinAttrs) {
|
|
4883
|
-
handles.push(this.loadSkinnedModel(
|
|
4884
|
-
{ positions, normals, uvs, indices, texture },
|
|
4885
|
-
skinAttrs,
|
|
4886
|
-
skinnedModelSkinIndex
|
|
4887
|
-
));
|
|
4888
|
-
continue;
|
|
4889
|
-
}
|
|
4890
|
-
}
|
|
4891
|
-
handles.push(this.loadModel({ positions, normals, uvs, indices, texture }));
|
|
4585
|
+
for (const prim of parsed.primitives) {
|
|
4586
|
+
if (prim.skinned && prim.skinAttrs) {
|
|
4587
|
+
handles.push(this.loadSkinnedModel(
|
|
4588
|
+
{
|
|
4589
|
+
positions: prim.positions,
|
|
4590
|
+
normals: prim.normals,
|
|
4591
|
+
uvs: prim.uvs,
|
|
4592
|
+
indices: prim.indices,
|
|
4593
|
+
texture: prim.texture
|
|
4594
|
+
},
|
|
4595
|
+
prim.skinAttrs,
|
|
4596
|
+
skinnedModelSkinIndex
|
|
4597
|
+
));
|
|
4598
|
+
} else {
|
|
4599
|
+
handles.push(this.loadModel({
|
|
4600
|
+
positions: prim.positions,
|
|
4601
|
+
normals: prim.normals,
|
|
4602
|
+
uvs: prim.uvs,
|
|
4603
|
+
indices: prim.indices,
|
|
4604
|
+
texture: prim.texture
|
|
4605
|
+
}));
|
|
4892
4606
|
}
|
|
4893
4607
|
}
|
|
4894
4608
|
let totalVertexCount = 0;
|
|
@@ -4900,7 +4614,7 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
4900
4614
|
totalVertexCount,
|
|
4901
4615
|
skinned: handles.some((h) => h.skinned),
|
|
4902
4616
|
animations: animNames,
|
|
4903
|
-
src:
|
|
4617
|
+
src: parsed.src
|
|
4904
4618
|
};
|
|
4905
4619
|
}
|
|
4906
4620
|
/**
|
|
@@ -4908,7 +4622,10 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
4908
4622
|
* with another instance (e.g., when spawning all parts of a character).
|
|
4909
4623
|
*/
|
|
4910
4624
|
addInstance(opts) {
|
|
4911
|
-
|
|
4625
|
+
if (isPrefab3D(opts.model) && opts.model.type === "composite") {
|
|
4626
|
+
return this.addCompositeInstance(opts, opts.model);
|
|
4627
|
+
}
|
|
4628
|
+
const modelOrGltf = isPrefab3D(opts.model) ? resolvePrefabHandle(opts.model) : opts.model;
|
|
4912
4629
|
if ("parts" in modelOrGltf) {
|
|
4913
4630
|
return this.addGltfInstance(opts, modelOrGltf);
|
|
4914
4631
|
}
|
|
@@ -4919,36 +4636,38 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
4919
4636
|
}
|
|
4920
4637
|
const slot = this.freeList.allocate();
|
|
4921
4638
|
if (slot === -1)
|
|
4922
|
-
throw new Error(`Max instances (${this.
|
|
4639
|
+
throw new Error(`Max instances (${this.maxInstances}) reached`);
|
|
4923
4640
|
const dynBase = slot * DYNAMIC_MESH_FLOATS;
|
|
4924
4641
|
const statBase = slot * STATIC_MESH_FLOATS;
|
|
4925
|
-
const
|
|
4926
|
-
this.dynamicData[dynBase + DYN_PREV_PX] =
|
|
4927
|
-
this.dynamicData[dynBase + DYN_PREV_PY] =
|
|
4928
|
-
this.dynamicData[dynBase + DYN_PREV_PZ] =
|
|
4929
|
-
this.dynamicData[dynBase + DYN_CURR_PX] =
|
|
4930
|
-
this.dynamicData[dynBase + DYN_CURR_PY] =
|
|
4931
|
-
this.dynamicData[dynBase + DYN_CURR_PZ] =
|
|
4932
|
-
|
|
4933
|
-
this.dynamicData[dynBase +
|
|
4934
|
-
this.dynamicData[dynBase +
|
|
4935
|
-
this.dynamicData[dynBase +
|
|
4936
|
-
this.dynamicData[dynBase +
|
|
4937
|
-
this.dynamicData[dynBase +
|
|
4938
|
-
this.
|
|
4939
|
-
this.staticData[statBase +
|
|
4940
|
-
this.staticData[statBase +
|
|
4941
|
-
this.staticData[statBase +
|
|
4942
|
-
|
|
4943
|
-
this.staticData[statBase +
|
|
4944
|
-
this.staticData[statBase + STAT_CG] = color[1];
|
|
4945
|
-
this.staticData[statBase + STAT_CB] = color[2];
|
|
4642
|
+
const t = resolveTransform(opts);
|
|
4643
|
+
this.dynamicData[dynBase + DYN_PREV_PX] = t.px;
|
|
4644
|
+
this.dynamicData[dynBase + DYN_PREV_PY] = t.py;
|
|
4645
|
+
this.dynamicData[dynBase + DYN_PREV_PZ] = t.pz;
|
|
4646
|
+
this.dynamicData[dynBase + DYN_CURR_PX] = t.px;
|
|
4647
|
+
this.dynamicData[dynBase + DYN_CURR_PY] = t.py;
|
|
4648
|
+
this.dynamicData[dynBase + DYN_CURR_PZ] = t.pz;
|
|
4649
|
+
this.dynamicData[dynBase + DYN_PREV_RX] = t.rx;
|
|
4650
|
+
this.dynamicData[dynBase + DYN_PREV_RY] = t.ry;
|
|
4651
|
+
this.dynamicData[dynBase + DYN_PREV_RZ] = t.rz;
|
|
4652
|
+
this.dynamicData[dynBase + DYN_CURR_RX] = t.rx;
|
|
4653
|
+
this.dynamicData[dynBase + DYN_CURR_RY] = t.ry;
|
|
4654
|
+
this.dynamicData[dynBase + DYN_CURR_RZ] = t.rz;
|
|
4655
|
+
this.staticData[statBase + STAT_SX] = t.sx;
|
|
4656
|
+
this.staticData[statBase + STAT_SY] = t.sy;
|
|
4657
|
+
this.staticData[statBase + STAT_SZ] = t.sz;
|
|
4658
|
+
this.staticData[statBase + STAT_CR] = t.cr;
|
|
4659
|
+
this.staticData[statBase + STAT_CG] = t.cg;
|
|
4660
|
+
this.staticData[statBase + STAT_CB] = t.cb;
|
|
4946
4661
|
this.staticDirty = true;
|
|
4947
4662
|
this.instanceModelIds[slot] = modelHandle.id;
|
|
4948
4663
|
this.batcher.add(0, modelHandle.id, slot);
|
|
4949
4664
|
const dynamicData = this.dynamicData;
|
|
4950
4665
|
const staticData = this.staticData;
|
|
4951
|
-
|
|
4666
|
+
const self = this;
|
|
4667
|
+
let destroyed = false;
|
|
4668
|
+
const handle = {
|
|
4669
|
+
slot,
|
|
4670
|
+
modelId: modelHandle.id,
|
|
4952
4671
|
skinned: false,
|
|
4953
4672
|
setPosition(nx, ny, nz) {
|
|
4954
4673
|
dynamicData[dynBase + DYN_CURR_PX] = nx;
|
|
@@ -4964,8 +4683,19 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
4964
4683
|
staticData[statBase + STAT_SX] = nx;
|
|
4965
4684
|
staticData[statBase + STAT_SY] = ny;
|
|
4966
4685
|
staticData[statBase + STAT_SZ] = nz;
|
|
4686
|
+
},
|
|
4687
|
+
destroy() {
|
|
4688
|
+
if (destroyed)
|
|
4689
|
+
return;
|
|
4690
|
+
destroyed = true;
|
|
4691
|
+
self.batcher.remove(0, modelHandle.id, slot);
|
|
4692
|
+
self.freeList.free(slot);
|
|
4693
|
+
dynamicData.fill(0, dynBase, dynBase + DYNAMIC_MESH_FLOATS);
|
|
4694
|
+
staticData.fill(0, statBase, statBase + STATIC_MESH_FLOATS);
|
|
4695
|
+
self.staticDirty = true;
|
|
4967
4696
|
}
|
|
4968
4697
|
};
|
|
4698
|
+
return handle;
|
|
4969
4699
|
}
|
|
4970
4700
|
addGltfInstance(opts, gltf) {
|
|
4971
4701
|
const childHandles = [];
|
|
@@ -5003,7 +4733,71 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
5003
4733
|
} : void 0,
|
|
5004
4734
|
stop: skinnedHandle?.stop ? () => {
|
|
5005
4735
|
skinnedHandle.stop();
|
|
5006
|
-
} : void 0
|
|
4736
|
+
} : void 0,
|
|
4737
|
+
destroy() {
|
|
4738
|
+
for (const h of childHandles)
|
|
4739
|
+
h.destroy();
|
|
4740
|
+
}
|
|
4741
|
+
};
|
|
4742
|
+
}
|
|
4743
|
+
/**
|
|
4744
|
+
* Spawn a composite prefab by spawning each of its parts at the composed
|
|
4745
|
+
* (instance + offset) transform. The returned handle broadcasts subsequent
|
|
4746
|
+
* `setPosition` / `setRotation` to every child, keeping each child's
|
|
4747
|
+
* baked offset applied on top of the new value.
|
|
4748
|
+
*/
|
|
4749
|
+
addCompositeInstance(opts, composite) {
|
|
4750
|
+
const bucket = this._prefabs;
|
|
4751
|
+
if (!bucket) {
|
|
4752
|
+
throw new Error(
|
|
4753
|
+
`addInstance: composite '${composite.id}' requires the renderer to be constructed with the bucket (\`prefabs\`).`
|
|
4754
|
+
);
|
|
4755
|
+
}
|
|
4756
|
+
const basePos = opts.position ?? [0, 0, 0];
|
|
4757
|
+
const baseRot = opts.rotation ?? [0, 0, 0];
|
|
4758
|
+
const offsets = composite.parts.map((p) => ({
|
|
4759
|
+
px: p.offset?.position?.[0] ?? 0,
|
|
4760
|
+
py: p.offset?.position?.[1] ?? 0,
|
|
4761
|
+
pz: p.offset?.position?.[2] ?? 0,
|
|
4762
|
+
rx: p.offset?.rotation?.[0] ?? 0,
|
|
4763
|
+
ry: p.offset?.rotation?.[1] ?? 0,
|
|
4764
|
+
rz: p.offset?.rotation?.[2] ?? 0
|
|
4765
|
+
}));
|
|
4766
|
+
const childHandles = [];
|
|
4767
|
+
for (let i = 0; i < composite.parts.length; i++) {
|
|
4768
|
+
const part = composite.parts[i];
|
|
4769
|
+
const off = offsets[i];
|
|
4770
|
+
const partPrefab = bucket.get(part.partId);
|
|
4771
|
+
const partOpts = {
|
|
4772
|
+
...opts,
|
|
4773
|
+
model: partPrefab,
|
|
4774
|
+
position: [basePos[0] + off.px, basePos[1] + off.py, basePos[2] + off.pz],
|
|
4775
|
+
rotation: [baseRot[0] + off.rx, baseRot[1] + off.ry, baseRot[2] + off.rz]
|
|
4776
|
+
};
|
|
4777
|
+
childHandles.push(this.addInstance(partOpts));
|
|
4778
|
+
}
|
|
4779
|
+
return {
|
|
4780
|
+
skinned: childHandles.some((h) => h.skinned),
|
|
4781
|
+
setPosition(x, y, z) {
|
|
4782
|
+
for (let i = 0; i < childHandles.length; i++) {
|
|
4783
|
+
const o = offsets[i];
|
|
4784
|
+
childHandles[i].setPosition(x + o.px, y + o.py, z + o.pz);
|
|
4785
|
+
}
|
|
4786
|
+
},
|
|
4787
|
+
setRotation(x, y, z) {
|
|
4788
|
+
for (let i = 0; i < childHandles.length; i++) {
|
|
4789
|
+
const o = offsets[i];
|
|
4790
|
+
childHandles[i].setRotation(x + o.rx, y + o.ry, z + o.rz);
|
|
4791
|
+
}
|
|
4792
|
+
},
|
|
4793
|
+
setScale(x, y, z) {
|
|
4794
|
+
for (const h of childHandles)
|
|
4795
|
+
h.setScale(x, y, z);
|
|
4796
|
+
},
|
|
4797
|
+
destroy() {
|
|
4798
|
+
for (const h of childHandles)
|
|
4799
|
+
h.destroy();
|
|
4800
|
+
}
|
|
5007
4801
|
};
|
|
5008
4802
|
}
|
|
5009
4803
|
addSkinnedInstance(opts, modelHandle, skinIndex, linkedSlot) {
|
|
@@ -5017,43 +4811,53 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
5017
4811
|
if (linkedSlot !== void 0) {
|
|
5018
4812
|
boneOffset = this.skinnedInstanceBoneOffsets[linkedSlot];
|
|
5019
4813
|
animState = this.skinnedAnimStates[linkedSlot];
|
|
4814
|
+
this.boneOffsetRefcount[boneOffset]++;
|
|
5020
4815
|
} else {
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
4816
|
+
const pool = this.freedBoneOffsets.get(skinIndex);
|
|
4817
|
+
if (pool && pool.length > 0) {
|
|
4818
|
+
boneOffset = pool.pop();
|
|
4819
|
+
} else {
|
|
4820
|
+
boneOffset = this.nextBoneOffset + jointCount;
|
|
4821
|
+
this.nextBoneOffset += jointCount * 2;
|
|
4822
|
+
}
|
|
4823
|
+
this.boneOffsetRefcount[boneOffset] = 1;
|
|
4824
|
+
this.boneOffsetSkinIndex[boneOffset] = skinIndex;
|
|
4825
|
+
const restOffsetFloats = boneOffset * 16;
|
|
4826
|
+
const restLengthFloats = jointCount * 16;
|
|
4827
|
+
skinModel.animation.computeRestPose(this.boneMatrixData, restOffsetFloats);
|
|
4828
|
+
this.device.queue.writeBuffer(
|
|
4829
|
+
this.rawBoneMatrixBuffer,
|
|
4830
|
+
restOffsetFloats * 4,
|
|
4831
|
+
this.boneMatrixData.buffer,
|
|
4832
|
+
this.boneMatrixData.byteOffset + restOffsetFloats * 4,
|
|
4833
|
+
restLengthFloats * 4
|
|
4834
|
+
);
|
|
5025
4835
|
animState = skinModel.animation.clipCount > 0 ? skinModel.animation.createState(0, 1, true) : null;
|
|
5026
4836
|
}
|
|
5027
4837
|
this.skinnedInstanceBoneOffsets[slot] = boneOffset;
|
|
5028
4838
|
this.skinnedAnimStates[slot] = animState;
|
|
5029
4839
|
const dynBase = slot * DYNAMIC_MESH_FLOATS;
|
|
5030
4840
|
const statBase = slot * SKINNED_STATIC_MESH_FLOATS;
|
|
5031
|
-
const
|
|
5032
|
-
this.skinnedDynamicData[dynBase + DYN_PREV_PX] =
|
|
5033
|
-
this.skinnedDynamicData[dynBase + DYN_PREV_PY] =
|
|
5034
|
-
this.skinnedDynamicData[dynBase + DYN_PREV_PZ] =
|
|
5035
|
-
this.skinnedDynamicData[dynBase + DYN_CURR_PX] =
|
|
5036
|
-
this.skinnedDynamicData[dynBase + DYN_CURR_PY] =
|
|
5037
|
-
this.skinnedDynamicData[dynBase + DYN_CURR_PZ] =
|
|
5038
|
-
|
|
5039
|
-
this.skinnedDynamicData[dynBase +
|
|
5040
|
-
this.skinnedDynamicData[dynBase +
|
|
5041
|
-
this.skinnedDynamicData[dynBase +
|
|
5042
|
-
this.skinnedDynamicData[dynBase +
|
|
5043
|
-
this.skinnedDynamicData[dynBase +
|
|
5044
|
-
this.
|
|
5045
|
-
this.skinnedStaticData[statBase +
|
|
5046
|
-
this.skinnedStaticData[statBase +
|
|
5047
|
-
this.skinnedStaticData[statBase +
|
|
5048
|
-
|
|
5049
|
-
this.skinnedStaticData[statBase +
|
|
5050
|
-
this.
|
|
5051
|
-
this.skinnedStaticData[statBase + SSTAT_CB] = color[2];
|
|
5052
|
-
new DataView(this.skinnedStaticData.buffer).setUint32(
|
|
5053
|
-
(statBase + SSTAT_BONE_OFFSET) * 4,
|
|
5054
|
-
boneOffset,
|
|
5055
|
-
true
|
|
5056
|
-
);
|
|
4841
|
+
const t = resolveTransform(opts);
|
|
4842
|
+
this.skinnedDynamicData[dynBase + DYN_PREV_PX] = t.px;
|
|
4843
|
+
this.skinnedDynamicData[dynBase + DYN_PREV_PY] = t.py;
|
|
4844
|
+
this.skinnedDynamicData[dynBase + DYN_PREV_PZ] = t.pz;
|
|
4845
|
+
this.skinnedDynamicData[dynBase + DYN_CURR_PX] = t.px;
|
|
4846
|
+
this.skinnedDynamicData[dynBase + DYN_CURR_PY] = t.py;
|
|
4847
|
+
this.skinnedDynamicData[dynBase + DYN_CURR_PZ] = t.pz;
|
|
4848
|
+
this.skinnedDynamicData[dynBase + DYN_PREV_RX] = t.rx;
|
|
4849
|
+
this.skinnedDynamicData[dynBase + DYN_PREV_RY] = t.ry;
|
|
4850
|
+
this.skinnedDynamicData[dynBase + DYN_PREV_RZ] = t.rz;
|
|
4851
|
+
this.skinnedDynamicData[dynBase + DYN_CURR_RX] = t.rx;
|
|
4852
|
+
this.skinnedDynamicData[dynBase + DYN_CURR_RY] = t.ry;
|
|
4853
|
+
this.skinnedDynamicData[dynBase + DYN_CURR_RZ] = t.rz;
|
|
4854
|
+
this.skinnedStaticData[statBase + SSTAT_SX] = t.sx;
|
|
4855
|
+
this.skinnedStaticData[statBase + SSTAT_SY] = t.sy;
|
|
4856
|
+
this.skinnedStaticData[statBase + SSTAT_SZ] = t.sz;
|
|
4857
|
+
this.skinnedStaticData[statBase + SSTAT_CR] = t.cr;
|
|
4858
|
+
this.skinnedStaticData[statBase + SSTAT_CG] = t.cg;
|
|
4859
|
+
this.skinnedStaticData[statBase + SSTAT_CB] = t.cb;
|
|
4860
|
+
this.skinnedStaticDV.setUint32((statBase + SSTAT_BONE_OFFSET) * 4, boneOffset, true);
|
|
5057
4861
|
this.skinnedStaticDirty = true;
|
|
5058
4862
|
this.skinnedInstanceModelIds[slot] = modelHandle.id;
|
|
5059
4863
|
this.skinnedBatcher.add(0, modelHandle.id, slot);
|
|
@@ -5061,6 +4865,10 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
5061
4865
|
const staticData = this.skinnedStaticData;
|
|
5062
4866
|
const animStates = this.skinnedAnimStates;
|
|
5063
4867
|
const animation = skinModel.animation;
|
|
4868
|
+
const self = this;
|
|
4869
|
+
const capturedBoneOffset = boneOffset;
|
|
4870
|
+
const capturedSkinIndex = skinIndex;
|
|
4871
|
+
let destroyed = false;
|
|
5064
4872
|
return {
|
|
5065
4873
|
slot,
|
|
5066
4874
|
modelId: modelHandle.id,
|
|
@@ -5089,19 +4897,149 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
5089
4897
|
const state = animStates[slot];
|
|
5090
4898
|
if (state)
|
|
5091
4899
|
animation.stop(state);
|
|
4900
|
+
},
|
|
4901
|
+
destroy() {
|
|
4902
|
+
if (destroyed)
|
|
4903
|
+
return;
|
|
4904
|
+
destroyed = true;
|
|
4905
|
+
self.skinnedBatcher.remove(0, modelHandle.id, slot);
|
|
4906
|
+
self.skinnedFreeList.free(slot);
|
|
4907
|
+
dynamicData.fill(0, dynBase, dynBase + DYNAMIC_MESH_FLOATS);
|
|
4908
|
+
staticData.fill(0, statBase, statBase + SKINNED_STATIC_MESH_FLOATS);
|
|
4909
|
+
animStates[slot] = null;
|
|
4910
|
+
self.skinnedStaticDirty = true;
|
|
4911
|
+
if (--self.boneOffsetRefcount[capturedBoneOffset] === 0) {
|
|
4912
|
+
let pool = self.freedBoneOffsets.get(capturedSkinIndex);
|
|
4913
|
+
if (!pool) {
|
|
4914
|
+
pool = [];
|
|
4915
|
+
self.freedBoneOffsets.set(capturedSkinIndex, pool);
|
|
4916
|
+
}
|
|
4917
|
+
pool.push(capturedBoneOffset);
|
|
4918
|
+
}
|
|
5092
4919
|
}
|
|
5093
4920
|
};
|
|
5094
4921
|
}
|
|
4922
|
+
/**
|
|
4923
|
+
* Drain pending resyncs from the coordinator. Per affected skin: rebuild
|
|
4924
|
+
* its `SkeletalAnimation` clip list densely, remap in-flight animStates,
|
|
4925
|
+
* then full-repack `packedAnimData`. Falls back to a kernel rebuild only
|
|
4926
|
+
* when the new data exceeds the kernel's allocated budgets.
|
|
4927
|
+
*
|
|
4928
|
+
* The dense renumbering is load-bearing: the kernel reads each clip by
|
|
4929
|
+
* its per-skin index, which must equal `SkeletalAnimation`'s clip id.
|
|
4930
|
+
*/
|
|
4931
|
+
syncLazyAnimationChanges() {
|
|
4932
|
+
const resync = this.clipResync;
|
|
4933
|
+
if (!resync || resync.pending.size === 0)
|
|
4934
|
+
return;
|
|
4935
|
+
const remapsBySkin = /* @__PURE__ */ new Map();
|
|
4936
|
+
for (const skinIndex of resync.pending) {
|
|
4937
|
+
const sm = this.skinnedModels[skinIndex];
|
|
4938
|
+
if (!sm)
|
|
4939
|
+
continue;
|
|
4940
|
+
remapsBySkin.set(skinIndex, sm.animation.replaceClips(sm.parsedSkin.animClips));
|
|
4941
|
+
}
|
|
4942
|
+
resync.clear();
|
|
4943
|
+
for (let slot = 0; slot < this.maxSkinnedInstances; slot++) {
|
|
4944
|
+
const animState = this.skinnedAnimStates[slot];
|
|
4945
|
+
if (!animState)
|
|
4946
|
+
continue;
|
|
4947
|
+
const modelId = this.skinnedInstanceModelIds[slot];
|
|
4948
|
+
const model = this.models[modelId];
|
|
4949
|
+
if (!model || model.skinIndex < 0)
|
|
4950
|
+
continue;
|
|
4951
|
+
const remap3 = remapsBySkin.get(model.skinIndex);
|
|
4952
|
+
if (!remap3)
|
|
4953
|
+
continue;
|
|
4954
|
+
if (animState.clipId >= 0 && animState.clipId < remap3.length) {
|
|
4955
|
+
const next = remap3[animState.clipId];
|
|
4956
|
+
if (next < 0) {
|
|
4957
|
+
animState.clipId = -1;
|
|
4958
|
+
animState.playing = false;
|
|
4959
|
+
} else {
|
|
4960
|
+
animState.clipId = next;
|
|
4961
|
+
}
|
|
4962
|
+
}
|
|
4963
|
+
if (animState.prevClipId >= 0 && animState.prevClipId < remap3.length) {
|
|
4964
|
+
animState.prevClipId = remap3[animState.prevClipId];
|
|
4965
|
+
}
|
|
4966
|
+
}
|
|
4967
|
+
this.packedAnimData = createPackedAnimationData();
|
|
4968
|
+
for (const sm of this.skinnedModels) {
|
|
4969
|
+
packSkinAndAnimations(this.packedAnimData, sm.parsedSkin.data, sm.parsedSkin.animClips);
|
|
4970
|
+
}
|
|
4971
|
+
if (this.tryUploadInPlace())
|
|
4972
|
+
return;
|
|
4973
|
+
this.animComputeNeedsRebuild = true;
|
|
4974
|
+
}
|
|
4975
|
+
/** Doubling-growth budgets for kernel storage buffers, with sensible floors. */
|
|
4976
|
+
growBudgetsForPacked(packed, previous) {
|
|
4977
|
+
const pb = packAnimationData(packed);
|
|
4978
|
+
const grow = (cur, prev, floor) => Math.max(cur * 2, prev, floor);
|
|
4979
|
+
return {
|
|
4980
|
+
skelI32Capacity: grow(pb.skelI32.length, previous?.skelI32Capacity ?? 0, 256),
|
|
4981
|
+
animF32Capacity: grow(pb.animF32.length, previous?.animF32Capacity ?? 0, 4096),
|
|
4982
|
+
matricesCapacity: grow(pb.totalMats, previous?.matricesCapacity ?? 0, 8)
|
|
4983
|
+
};
|
|
4984
|
+
}
|
|
4985
|
+
/** Upload `packedAnimData` to the existing kernel iff it fits the current budgets. Returns false to force a rebuild. */
|
|
4986
|
+
tryUploadInPlace() {
|
|
4987
|
+
const kernel = this.animComputeKernel;
|
|
4988
|
+
const budgets = this.animKernelBudgets;
|
|
4989
|
+
if (!kernel || !budgets)
|
|
4990
|
+
return false;
|
|
4991
|
+
const pb = packAnimationData(this.packedAnimData);
|
|
4992
|
+
if (pb.skelI32.length > budgets.skelI32Capacity)
|
|
4993
|
+
return false;
|
|
4994
|
+
if (pb.animF32.length > budgets.animF32Capacity)
|
|
4995
|
+
return false;
|
|
4996
|
+
if (pb.totalMats > budgets.matricesCapacity)
|
|
4997
|
+
return false;
|
|
4998
|
+
uploadPackedToKernel(this.root, kernel, pb);
|
|
4999
|
+
this.animClipTableOffset = pb.clipTableOffset;
|
|
5000
|
+
this.animChannelTableOffset = pb.channelTableOffset;
|
|
5001
|
+
this.animJointLookupOffset = pb.jointLookupOffset;
|
|
5002
|
+
return true;
|
|
5003
|
+
}
|
|
5004
|
+
/**
|
|
5005
|
+
* Returns true if a skinned instance's bone-matrix compute should be
|
|
5006
|
+
* dispatched this frame. Combines frustum culling (scaled bounding sphere)
|
|
5007
|
+
* and a configurable distance cull from the camera. Callers pass the
|
|
5008
|
+
* camera position + cullDistSq once outside the loop to avoid re-reading.
|
|
5009
|
+
*/
|
|
5010
|
+
shouldDispatchSkinning(slot, model, camX, camY, camZ, cullDistSq) {
|
|
5011
|
+
const skinModel = model && model.skinIndex >= 0 ? this.skinnedModels[model.skinIndex] : null;
|
|
5012
|
+
const baseRadius = skinModel?.boundingRadius ?? 10;
|
|
5013
|
+
const base = slot * DYNAMIC_MESH_FLOATS;
|
|
5014
|
+
const sBase = slot * SKINNED_STATIC_MESH_FLOATS;
|
|
5015
|
+
const cx = this.skinnedDynamicData[base + DYN_CURR_PX];
|
|
5016
|
+
const cy = this.skinnedDynamicData[base + DYN_CURR_PY];
|
|
5017
|
+
const cz = this.skinnedDynamicData[base + DYN_CURR_PZ];
|
|
5018
|
+
const sx = Math.abs(this.skinnedStaticData[sBase + SSTAT_SX]);
|
|
5019
|
+
const sy = Math.abs(this.skinnedStaticData[sBase + SSTAT_SY]);
|
|
5020
|
+
const sz = Math.abs(this.skinnedStaticData[sBase + SSTAT_SZ]);
|
|
5021
|
+
const maxScale = sx > sy ? sx > sz ? sx : sz : sy > sz ? sy : sz;
|
|
5022
|
+
if (!this.isInFrustum(cx, cy, cz, baseRadius * maxScale))
|
|
5023
|
+
return false;
|
|
5024
|
+
const dxv = cx - camX, dyv = cy - camY, dzv = cz - camZ;
|
|
5025
|
+
if (dxv * dxv + dyv * dyv + dzv * dzv > cullDistSq)
|
|
5026
|
+
return false;
|
|
5027
|
+
return true;
|
|
5028
|
+
}
|
|
5095
5029
|
updateAnimations(deltaTime) {
|
|
5030
|
+
this.syncLazyAnimationChanges();
|
|
5096
5031
|
if (this.animComputeNeedsRebuild && this.packedAnimData.clips.length > 0) {
|
|
5097
5032
|
this.animComputeKernel?.destroy();
|
|
5033
|
+
const budgets = this.growBudgetsForPacked(this.packedAnimData, null);
|
|
5098
5034
|
const { kernel, packedBuffers } = buildAnimationKernel(
|
|
5099
5035
|
this.root,
|
|
5100
5036
|
this.packedAnimData,
|
|
5101
5037
|
this.maxSkinnedInstances,
|
|
5102
|
-
this.maxTotalBones
|
|
5038
|
+
this.maxTotalBones,
|
|
5039
|
+
budgets
|
|
5103
5040
|
);
|
|
5104
5041
|
this.animComputeKernel = kernel;
|
|
5042
|
+
this.animKernelBudgets = budgets;
|
|
5105
5043
|
this.animClipTableOffset = packedBuffers.clipTableOffset;
|
|
5106
5044
|
this.animChannelTableOffset = packedBuffers.channelTableOffset;
|
|
5107
5045
|
this.animJointLookupOffset = packedBuffers.jointLookupOffset;
|
|
@@ -5118,11 +5056,22 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
5118
5056
|
{ binding: 4, resource: { buffer: rawBoneBuffer } }
|
|
5119
5057
|
]
|
|
5120
5058
|
});
|
|
5059
|
+
this.device.queue.writeBuffer(
|
|
5060
|
+
this.rawBoneMatrixBuffer,
|
|
5061
|
+
0,
|
|
5062
|
+
this.boneMatrixData.buffer,
|
|
5063
|
+
this.boneMatrixData.byteOffset,
|
|
5064
|
+
this.boneMatrixData.byteLength
|
|
5065
|
+
);
|
|
5066
|
+
this.boneMatrixDirty = false;
|
|
5121
5067
|
this.animComputeNeedsRebuild = false;
|
|
5122
5068
|
}
|
|
5123
5069
|
this.updatedBoneOffsets.fill(0);
|
|
5124
5070
|
let count = 0;
|
|
5125
5071
|
const dv = this.gpuInstDV;
|
|
5072
|
+
const camPos = this.camera.position;
|
|
5073
|
+
const camX = camPos[0], camY = camPos[1], camZ = camPos[2];
|
|
5074
|
+
const cullDistSq = this.animationCullDistanceSq;
|
|
5126
5075
|
for (let slot = 0; slot < this.maxSkinnedInstances; slot++) {
|
|
5127
5076
|
const animState = this.skinnedAnimStates[slot];
|
|
5128
5077
|
if (!animState)
|
|
@@ -5161,6 +5110,8 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
5161
5110
|
const modelId = this.skinnedInstanceModelIds[slot];
|
|
5162
5111
|
const model = this.models[modelId];
|
|
5163
5112
|
const skinIdx = model?.skinIndex ?? 0;
|
|
5113
|
+
if (!this.shouldDispatchSkinning(slot, model, camX, camY, camZ, cullDistSq))
|
|
5114
|
+
continue;
|
|
5164
5115
|
const off = count * 32;
|
|
5165
5116
|
dv.setInt32(off, animState.clipId, true);
|
|
5166
5117
|
dv.setFloat32(off + 4, animState.time, true);
|
|
@@ -5205,14 +5156,12 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
5205
5156
|
}
|
|
5206
5157
|
}
|
|
5207
5158
|
}
|
|
5159
|
+
/**
|
|
5160
|
+
* Free an instance's renderer slot. Equivalent to `handle.destroy()` -
|
|
5161
|
+
* kept as a convenience for direct lookup. Safe to call multiple times.
|
|
5162
|
+
*/
|
|
5208
5163
|
removeInstance(handle) {
|
|
5209
|
-
|
|
5210
|
-
this.freeList.free(handle.slot);
|
|
5211
|
-
const dynBase = handle.slot * DYNAMIC_MESH_FLOATS;
|
|
5212
|
-
const statBase = handle.slot * STATIC_MESH_FLOATS;
|
|
5213
|
-
this.dynamicData.fill(0, dynBase, dynBase + DYNAMIC_MESH_FLOATS);
|
|
5214
|
-
this.staticData.fill(0, statBase, statBase + STATIC_MESH_FLOATS);
|
|
5215
|
-
this.staticDirty = true;
|
|
5164
|
+
handle.destroy();
|
|
5216
5165
|
}
|
|
5217
5166
|
storePreviousState() {
|
|
5218
5167
|
this.camera.storePrevious();
|
|
@@ -5518,6 +5467,8 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
5518
5467
|
this.resizeObserver?.disconnect();
|
|
5519
5468
|
this.resizeObserver = null;
|
|
5520
5469
|
this.resizeCallbacks.length = 0;
|
|
5470
|
+
this.clipResync?.dispose();
|
|
5471
|
+
this.clipResync = null;
|
|
5521
5472
|
this.dynamicBuffer?.destroy();
|
|
5522
5473
|
this.staticBuffer?.destroy();
|
|
5523
5474
|
this.uniformBuffer?.destroy();
|
|
@@ -5665,10 +5616,8 @@ var ParticleEmitter = class {
|
|
|
5665
5616
|
const sprite = this.renderer.addSprite({
|
|
5666
5617
|
sheet: this.config.sheet,
|
|
5667
5618
|
sprite: this.config.sprite ?? 0,
|
|
5668
|
-
x,
|
|
5669
|
-
|
|
5670
|
-
scaleX: size,
|
|
5671
|
-
scaleY: size,
|
|
5619
|
+
position: [x, y],
|
|
5620
|
+
scale: size,
|
|
5672
5621
|
opacity: 1,
|
|
5673
5622
|
tint: this.config.color,
|
|
5674
5623
|
layer: 255
|
|
@@ -5720,109 +5669,6 @@ var ParticleEmitter = class {
|
|
|
5720
5669
|
}
|
|
5721
5670
|
};
|
|
5722
5671
|
|
|
5723
|
-
// src/2d/animation.ts
|
|
5724
|
-
var AnimationController = class {
|
|
5725
|
-
constructor() {
|
|
5726
|
-
this.clips = [];
|
|
5727
|
-
this.clipsByName = /* @__PURE__ */ new Map();
|
|
5728
|
-
}
|
|
5729
|
-
/**
|
|
5730
|
-
* Register an animation clip. Returns the clip ID.
|
|
5731
|
-
*/
|
|
5732
|
-
loadClip(config) {
|
|
5733
|
-
const id = this.clips.length;
|
|
5734
|
-
const clip = {
|
|
5735
|
-
id,
|
|
5736
|
-
name: config.name,
|
|
5737
|
-
frames: new Uint16Array(config.frames),
|
|
5738
|
-
durations: new Float32Array(config.durations),
|
|
5739
|
-
frameCount: config.frames.length,
|
|
5740
|
-
totalDuration: config.durations.reduce((sum, d7) => sum + d7, 0),
|
|
5741
|
-
loop: config.loop
|
|
5742
|
-
};
|
|
5743
|
-
this.clips.push(clip);
|
|
5744
|
-
this.clipsByName.set(config.name, id);
|
|
5745
|
-
return id;
|
|
5746
|
-
}
|
|
5747
|
-
/**
|
|
5748
|
-
* Get a clip by name.
|
|
5749
|
-
*/
|
|
5750
|
-
getClipId(name) {
|
|
5751
|
-
const id = this.clipsByName.get(name);
|
|
5752
|
-
if (id === void 0)
|
|
5753
|
-
throw new Error(`Animation clip "${name}" not found`);
|
|
5754
|
-
return id;
|
|
5755
|
-
}
|
|
5756
|
-
/**
|
|
5757
|
-
* Get a clip by ID.
|
|
5758
|
-
*/
|
|
5759
|
-
getClip(id) {
|
|
5760
|
-
return this.clips[id];
|
|
5761
|
-
}
|
|
5762
|
-
/**
|
|
5763
|
-
* Create a new animation state for an entity. This is the per-entity data
|
|
5764
|
-
* that tracks playback progress. Store it however you like (flat array, component, etc).
|
|
5765
|
-
*/
|
|
5766
|
-
createState(clipId, speed = 1, playing = true) {
|
|
5767
|
-
return { clipId, frame: 0, time: 0, speed, playing };
|
|
5768
|
-
}
|
|
5769
|
-
/**
|
|
5770
|
-
* Advance an animation state by deltaTime (in seconds).
|
|
5771
|
-
* Returns the current sprite frame ID, or -1 if not playing.
|
|
5772
|
-
* Zero allocations.
|
|
5773
|
-
*/
|
|
5774
|
-
update(state, deltaTime) {
|
|
5775
|
-
if (!state.playing) {
|
|
5776
|
-
return this.clips[state.clipId].frames[state.frame];
|
|
5777
|
-
}
|
|
5778
|
-
const clip = this.clips[state.clipId];
|
|
5779
|
-
state.time += deltaTime * state.speed * 1e3;
|
|
5780
|
-
while (state.time >= clip.durations[state.frame]) {
|
|
5781
|
-
state.time -= clip.durations[state.frame];
|
|
5782
|
-
state.frame++;
|
|
5783
|
-
if (state.frame >= clip.frameCount) {
|
|
5784
|
-
if (clip.loop) {
|
|
5785
|
-
state.frame = 0;
|
|
5786
|
-
} else {
|
|
5787
|
-
state.frame = clip.frameCount - 1;
|
|
5788
|
-
state.playing = false;
|
|
5789
|
-
break;
|
|
5790
|
-
}
|
|
5791
|
-
}
|
|
5792
|
-
}
|
|
5793
|
-
return clip.frames[state.frame];
|
|
5794
|
-
}
|
|
5795
|
-
/**
|
|
5796
|
-
* Play a different clip on an existing state. Resets frame and time.
|
|
5797
|
-
*/
|
|
5798
|
-
play(state, clipId, speed) {
|
|
5799
|
-
state.clipId = clipId;
|
|
5800
|
-
state.frame = 0;
|
|
5801
|
-
state.time = 0;
|
|
5802
|
-
state.playing = true;
|
|
5803
|
-
if (speed !== void 0)
|
|
5804
|
-
state.speed = speed;
|
|
5805
|
-
}
|
|
5806
|
-
/**
|
|
5807
|
-
* Stop playback.
|
|
5808
|
-
*/
|
|
5809
|
-
stop(state) {
|
|
5810
|
-
state.playing = false;
|
|
5811
|
-
}
|
|
5812
|
-
/**
|
|
5813
|
-
* Resume playback.
|
|
5814
|
-
*/
|
|
5815
|
-
resume(state) {
|
|
5816
|
-
state.playing = true;
|
|
5817
|
-
}
|
|
5818
|
-
/**
|
|
5819
|
-
* Number of loaded clips.
|
|
5820
|
-
*/
|
|
5821
|
-
get clipCount() {
|
|
5822
|
-
return this.clips.length;
|
|
5823
|
-
}
|
|
5824
|
-
};
|
|
5825
|
-
|
|
5826
5672
|
// src/shaders/utils.ts
|
|
5827
5673
|
import tgpu7 from "typegpu";
|
|
5828
5674
|
import * as d6 from "typegpu/data";
|
|
@@ -5944,7 +5790,6 @@ export {
|
|
|
5944
5790
|
STATIC_OFFSET_UV_MAX_Y,
|
|
5945
5791
|
STATIC_OFFSET_UV_MIN_X,
|
|
5946
5792
|
STATIC_OFFSET_UV_MIN_Y,
|
|
5947
|
-
SkeletalAnimation,
|
|
5948
5793
|
SkinnedStaticMesh,
|
|
5949
5794
|
SpriteAccessor,
|
|
5950
5795
|
SpriteUniforms,
|
|
@@ -5954,14 +5799,11 @@ export {
|
|
|
5954
5799
|
StaticSprite,
|
|
5955
5800
|
WebGPU2DRenderer,
|
|
5956
5801
|
WebGPU3DRenderer,
|
|
5957
|
-
computeGridUVs,
|
|
5958
|
-
computeTexturePackerUVs,
|
|
5959
5802
|
createGeometryDataLayout,
|
|
5960
5803
|
createTextureFromBitmap,
|
|
5961
5804
|
d,
|
|
5962
5805
|
getFieldFloats,
|
|
5963
5806
|
inverseLerp,
|
|
5964
|
-
loadImage,
|
|
5965
5807
|
remap,
|
|
5966
5808
|
resolveBuiltInGeometry,
|
|
5967
5809
|
rotate2d,
|