murow 0.0.71 → 0.0.72
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/renderer/{base-2d-renderer.js → base/renderer-2d.js} +1 -1
- package/dist/cjs/renderer/{base-3d-renderer.js → base/renderer-3d.js} +1 -1
- 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/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/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} +2 -2
- 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 +55 -0
- package/dist/types/renderer/prefab-bucket/index.d.ts +113 -0
- package/dist/types/renderer/prefab-bucket/parsers.d.ts +8 -0
- package/dist/types/renderer/prefab-bucket/specs.d.ts +166 -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 +17 -7
- package/dist/webgpu/cjs/index.js +479 -1082
- package/dist/webgpu/esm/index.js +485 -1079
- 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 +64 -14
- 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/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,661 +3146,13 @@ 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
|
-
}
|
|
3102
|
-
|
|
3103
|
-
// src/3d/gltf-skin-parser.ts
|
|
3104
|
-
function parseSkin(gltf, skinIndex, getAccessorData) {
|
|
3105
|
-
const skin = gltf.skins[skinIndex];
|
|
3106
|
-
const joints = skin.joints;
|
|
3107
|
-
const jointCount = joints.length;
|
|
3108
|
-
const ibmAccess = getAccessorData(skin.inverseBindMatrices);
|
|
3109
|
-
const inverseBindMatrices = new Float32Array(ibmAccess.data);
|
|
3110
|
-
const jointNodeIndices = new Uint16Array(joints);
|
|
3111
|
-
const nodeToJoint = /* @__PURE__ */ new Map();
|
|
3112
|
-
for (let j = 0; j < jointCount; j++) {
|
|
3113
|
-
nodeToJoint.set(joints[j], j);
|
|
3114
|
-
}
|
|
3115
|
-
const nodeParent = /* @__PURE__ */ new Map();
|
|
3116
|
-
for (let i = 0; i < gltf.nodes.length; i++) {
|
|
3117
|
-
const children = gltf.nodes[i].children;
|
|
3118
|
-
if (children) {
|
|
3119
|
-
for (const child of children) {
|
|
3120
|
-
nodeParent.set(child, i);
|
|
3121
|
-
}
|
|
3122
|
-
}
|
|
3123
|
-
}
|
|
3124
|
-
const parentJointIndices = new Int16Array(jointCount).fill(-1);
|
|
3125
|
-
for (let j = 0; j < jointCount; j++) {
|
|
3126
|
-
let parentNode = nodeParent.get(joints[j]);
|
|
3127
|
-
while (parentNode !== void 0) {
|
|
3128
|
-
const parentJoint = nodeToJoint.get(parentNode);
|
|
3129
|
-
if (parentJoint !== void 0) {
|
|
3130
|
-
parentJointIndices[j] = parentJoint;
|
|
3131
|
-
break;
|
|
3132
|
-
}
|
|
3133
|
-
parentNode = nodeParent.get(parentNode);
|
|
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;
|
|
3154
|
-
}
|
|
3155
|
-
return {
|
|
3156
|
-
jointCount,
|
|
3157
|
-
jointNodeIndices,
|
|
3158
|
-
inverseBindMatrices,
|
|
3159
|
-
parentJointIndices,
|
|
3160
|
-
skeletonRootMatrix
|
|
3161
|
-
};
|
|
3162
|
-
}
|
|
3163
|
-
function parseAnimations(gltf, skinData, getAccessorData) {
|
|
3164
|
-
if (!gltf.animations?.length)
|
|
3165
|
-
return [];
|
|
3166
|
-
const nodeToJoint = /* @__PURE__ */ new Map();
|
|
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
|
-
}
|
|
3210
|
-
}
|
|
3211
|
-
return clips;
|
|
3212
|
-
}
|
|
3213
|
-
function parsePrimitiveSkinAttributes(primitive, getAccessorData) {
|
|
3214
|
-
if (primitive.attributes.JOINTS_0 === void 0 || primitive.attributes.WEIGHTS_0 === void 0) {
|
|
3215
|
-
return null;
|
|
3216
|
-
}
|
|
3217
|
-
const jointsAccess = getAccessorData(primitive.attributes.JOINTS_0);
|
|
3218
|
-
const weightsAccess = getAccessorData(primitive.attributes.WEIGHTS_0);
|
|
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);
|
|
3235
|
-
}
|
|
3236
|
-
return { joints, weights };
|
|
3237
|
-
}
|
|
3238
|
-
function getNodeTRS(node) {
|
|
3239
|
-
const trs = new Float32Array(10);
|
|
3240
|
-
if (node.matrix) {
|
|
3241
|
-
trs[0] = node.matrix[12];
|
|
3242
|
-
trs[1] = node.matrix[13];
|
|
3243
|
-
trs[2] = node.matrix[14];
|
|
3244
|
-
trs[3] = 0;
|
|
3245
|
-
trs[4] = 0;
|
|
3246
|
-
trs[5] = 0;
|
|
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
|
-
});
|
|
3363
|
-
}
|
|
3364
|
-
return skinIndex;
|
|
3365
|
-
}
|
|
3366
|
-
|
|
3367
|
-
// src/3d/skeletal-animation.ts
|
|
3368
|
-
var SkeletalAnimation = class {
|
|
3369
|
-
// 16 floats, identity if no non-joint ancestors
|
|
3370
|
-
constructor(skinData, clips, gltfNodes) {
|
|
3371
|
-
this.clips = [];
|
|
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
|
-
};
|
|
3149
|
+
// src/3d/renderer.ts
|
|
3150
|
+
import {
|
|
3151
|
+
createPackedAnimationData,
|
|
3152
|
+
packSkinAndAnimations,
|
|
3153
|
+
parseGltf,
|
|
3154
|
+
SkeletalAnimation
|
|
3155
|
+
} from "murow/renderer";
|
|
3697
3156
|
|
|
3698
3157
|
// src/3d/skeletal-animation-compute/packer.ts
|
|
3699
3158
|
function packAnimationData(packed) {
|
|
@@ -3775,14 +3234,14 @@ function packAnimationData(packed) {
|
|
|
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,44 @@ 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
|
+
return value.type === "gltf" || value.type === "grid";
|
|
3660
|
+
}
|
|
3661
|
+
function resolveTransform(opts) {
|
|
3662
|
+
const [px, py, pz] = opts.position ?? [0, 0, 0];
|
|
3663
|
+
const [rx, ry, rz] = opts.rotation ?? [0, 0, 0];
|
|
3664
|
+
const s = opts.scale;
|
|
3665
|
+
const [sx, sy, sz] = typeof s === "number" ? [s, s, s] : s ?? [1, 1, 1];
|
|
3666
|
+
const [cr, cg, cb] = opts.color ?? [1, 1, 1];
|
|
3667
|
+
return { px, py, pz, rx, ry, rz, sx, sy, sz, cr, cg, cb };
|
|
3668
|
+
}
|
|
3669
|
+
function resolvePrefabHandle(prefab) {
|
|
3670
|
+
const h = prefabHandles.get(prefab);
|
|
3671
|
+
if (!h) {
|
|
3672
|
+
throw new Error(
|
|
3673
|
+
`Prefab '${prefab.id}' has no GPU handle \u2014 has the renderer's init() been called with this bucket?`
|
|
3674
|
+
);
|
|
3675
|
+
}
|
|
3676
|
+
return h;
|
|
3677
|
+
}
|
|
3678
|
+
function computeBucketStats(bucket) {
|
|
3679
|
+
let maxSkinnedParts = 0;
|
|
3680
|
+
let maxJointCount = 0;
|
|
3681
|
+
for (const prefab of bucket.entries()) {
|
|
3682
|
+
if (prefab.type === "gltf") {
|
|
3683
|
+
if (prefab.skinnedPartCount > maxSkinnedParts)
|
|
3684
|
+
maxSkinnedParts = prefab.skinnedPartCount;
|
|
3685
|
+
if (prefab.jointCount > maxJointCount)
|
|
3686
|
+
maxJointCount = prefab.jointCount;
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
return { maxSkinnedParts, maxJointCount };
|
|
3690
|
+
}
|
|
4161
3691
|
var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
4162
3692
|
constructor(canvas, options) {
|
|
4163
|
-
|
|
3693
|
+
const resolvedMaxModels = options.maxModels ?? (options.prefabs ? options.prefabs.size + 16 : 32);
|
|
3694
|
+
super(canvas, { ...options, maxModels: resolvedMaxModels });
|
|
4164
3695
|
this.resizeObserver = null;
|
|
4165
3696
|
this.resizeCallbacks = [];
|
|
4166
3697
|
// layer=0, sheetId=modelId
|
|
@@ -4170,11 +3701,15 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
4170
3701
|
this.nextModelId = 0;
|
|
4171
3702
|
// Skinned model data
|
|
4172
3703
|
this.skinnedModels = [];
|
|
3704
|
+
// Null when constructed without a `prefabs` bucket; lazy load/unload requires it as event source.
|
|
3705
|
+
this.clipResync = null;
|
|
4173
3706
|
this.boneMatrixDirty = true;
|
|
4174
3707
|
// GPU animation compute
|
|
4175
3708
|
this.packedAnimData = createPackedAnimationData();
|
|
4176
3709
|
this.animComputeKernel = null;
|
|
4177
3710
|
this.animComputeNeedsRebuild = false;
|
|
3711
|
+
// Capacities the active kernel was built with — used to gate the in-place upload path on resync.
|
|
3712
|
+
this.animKernelBudgets = null;
|
|
4178
3713
|
this.animClipTableOffset = 0;
|
|
4179
3714
|
this.animChannelTableOffset = 0;
|
|
4180
3715
|
this.animJointLookupOffset = 0;
|
|
@@ -4188,16 +3723,20 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
4188
3723
|
// mat4x4 (16) + alpha (1) + lightDir (3) + padding (4)
|
|
4189
3724
|
this.lastRenderTime = 0;
|
|
4190
3725
|
this.camera = new Camera3D();
|
|
4191
|
-
this.
|
|
4192
|
-
|
|
3726
|
+
this._prefabs = options.prefabs ?? null;
|
|
3727
|
+
const SKINNED_PARTS_PER_INSTANCE_DEFAULT_CAP = 3;
|
|
3728
|
+
const bucketStats = this._prefabs ? computeBucketStats(this._prefabs) : null;
|
|
3729
|
+
const maxInstances = options.maxInstances ?? resolvedMaxModels;
|
|
3730
|
+
this.maxSkinnedInstances = options.maxSkinnedInstances ?? (bucketStats ? maxInstances * Math.max(1, Math.min(bucketStats.maxSkinnedParts, SKINNED_PARTS_PER_INSTANCE_DEFAULT_CAP)) : 5e3);
|
|
3731
|
+
this.maxBonesPerSkin = options.maxBonesPerSkin ?? (bucketStats ? Math.max(1, bucketStats.maxJointCount) : 64);
|
|
4193
3732
|
this.maxTotalBones = this.maxSkinnedInstances * this.maxBonesPerSkin * 2;
|
|
4194
3733
|
this.updatedBoneOffsets = new Uint8Array(this.maxTotalBones);
|
|
4195
|
-
this.freeList = new FreeList3(
|
|
4196
|
-
this.batcher = new SparseBatcher2(
|
|
4197
|
-
this.dynamicData = new Float32Array(
|
|
4198
|
-
this.staticData = new Float32Array(
|
|
4199
|
-
this.slotIndexData = new Uint32Array(
|
|
4200
|
-
this.instanceModelIds = new Uint8Array(
|
|
3734
|
+
this.freeList = new FreeList3(resolvedMaxModels);
|
|
3735
|
+
this.batcher = new SparseBatcher2(resolvedMaxModels);
|
|
3736
|
+
this.dynamicData = new Float32Array(resolvedMaxModels * DYNAMIC_MESH_FLOATS);
|
|
3737
|
+
this.staticData = new Float32Array(resolvedMaxModels * STATIC_MESH_FLOATS);
|
|
3738
|
+
this.slotIndexData = new Uint32Array(resolvedMaxModels);
|
|
3739
|
+
this.instanceModelIds = new Uint8Array(resolvedMaxModels);
|
|
4201
3740
|
const msi = this.maxSkinnedInstances;
|
|
4202
3741
|
this.skinnedFreeList = new FreeList3(msi);
|
|
4203
3742
|
this.skinnedBatcher = new SparseBatcher2(msi);
|
|
@@ -4353,9 +3892,38 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
4353
3892
|
{ binding: 4, resource: { buffer: this.rawBoneMatrixBuffer } }
|
|
4354
3893
|
]
|
|
4355
3894
|
});
|
|
3895
|
+
if (this._prefabs) {
|
|
3896
|
+
this.uploadPrefabBucket(this._prefabs);
|
|
3897
|
+
}
|
|
4356
3898
|
this.setupResizeObserver();
|
|
4357
3899
|
this._initialized = true;
|
|
4358
3900
|
}
|
|
3901
|
+
/**
|
|
3902
|
+
* Upload every prefab in the bucket to the GPU and stash the handle on
|
|
3903
|
+
* each prefab so `bucket.get(id)` resolves to a usable model. Also
|
|
3904
|
+
* subscribes the resync coordinator to the bucket's `clips-changed`
|
|
3905
|
+
* channel for lazy load/unload.
|
|
3906
|
+
*/
|
|
3907
|
+
uploadPrefabBucket(bucket) {
|
|
3908
|
+
this.clipResync = new GltfClipResyncCoordinator(bucket);
|
|
3909
|
+
for (const prefab of bucket.entries()) {
|
|
3910
|
+
if (prefab.type === "gltf") {
|
|
3911
|
+
const beforeSkinCount = this.skinnedModels.length;
|
|
3912
|
+
const model = this.uploadParsedGltf(prefab.parsed);
|
|
3913
|
+
prefabHandles.set(prefab, model);
|
|
3914
|
+
if (this.skinnedModels.length > beforeSkinCount) {
|
|
3915
|
+
this.clipResync.registerSkin(prefab.id, beforeSkinCount);
|
|
3916
|
+
}
|
|
3917
|
+
} else if (prefab.type === "grid") {
|
|
3918
|
+
const model = this.createGrid({
|
|
3919
|
+
size: prefab.size,
|
|
3920
|
+
step: prefab.step,
|
|
3921
|
+
lineWidth: prefab.lineWidth
|
|
3922
|
+
});
|
|
3923
|
+
prefabHandles.set(prefab, model);
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
4359
3927
|
setupResizeObserver() {
|
|
4360
3928
|
const supportsDevicePixelBox = (() => {
|
|
4361
3929
|
try {
|
|
@@ -4689,108 +4257,20 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
4689
4257
|
* ```
|
|
4690
4258
|
*/
|
|
4691
4259
|
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 = [];
|
|
4260
|
+
const parsed = await parseGltf(url, opts);
|
|
4261
|
+
return this.uploadParsedGltf(parsed);
|
|
4262
|
+
}
|
|
4263
|
+
/**
|
|
4264
|
+
* Upload a previously-parsed glTF to the GPU. Returns a GltfModel handle.
|
|
4265
|
+
* Splitting parse (CPU) from upload (GPU) lets callers parse models in parallel
|
|
4266
|
+
* before a renderer exists and inspect joint counts / vertex totals to size the
|
|
4267
|
+
* renderer appropriately.
|
|
4268
|
+
*/
|
|
4269
|
+
uploadParsedGltf(parsed) {
|
|
4786
4270
|
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);
|
|
4271
|
+
if (parsed.skin) {
|
|
4272
|
+
const { data: skinData, animClips } = parsed.skin;
|
|
4273
|
+
const animation = new SkeletalAnimation(skinData, animClips);
|
|
4794
4274
|
const ibm = skinData.inverseBindMatrices;
|
|
4795
4275
|
let maxRadSq = 0;
|
|
4796
4276
|
for (let j = 0; j < skinData.jointCount; j++) {
|
|
@@ -4804,91 +4284,34 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
4804
4284
|
this.skinnedModels.push({
|
|
4805
4285
|
animation,
|
|
4806
4286
|
jointCount: skinData.jointCount,
|
|
4807
|
-
boundingRadius: skinnedRadius
|
|
4287
|
+
boundingRadius: skinnedRadius,
|
|
4288
|
+
parsedSkin: parsed.skin
|
|
4808
4289
|
});
|
|
4809
|
-
packSkinAndAnimations(this.packedAnimData, skinData, animClips
|
|
4290
|
+
packSkinAndAnimations(this.packedAnimData, skinData, animClips);
|
|
4810
4291
|
this.animComputeNeedsRebuild = true;
|
|
4811
4292
|
}
|
|
4812
4293
|
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 }));
|
|
4294
|
+
for (const prim of parsed.primitives) {
|
|
4295
|
+
if (prim.skinned && prim.skinAttrs) {
|
|
4296
|
+
handles.push(this.loadSkinnedModel(
|
|
4297
|
+
{
|
|
4298
|
+
positions: prim.positions,
|
|
4299
|
+
normals: prim.normals,
|
|
4300
|
+
uvs: prim.uvs,
|
|
4301
|
+
indices: prim.indices,
|
|
4302
|
+
texture: prim.texture
|
|
4303
|
+
},
|
|
4304
|
+
prim.skinAttrs,
|
|
4305
|
+
skinnedModelSkinIndex
|
|
4306
|
+
));
|
|
4307
|
+
} else {
|
|
4308
|
+
handles.push(this.loadModel({
|
|
4309
|
+
positions: prim.positions,
|
|
4310
|
+
normals: prim.normals,
|
|
4311
|
+
uvs: prim.uvs,
|
|
4312
|
+
indices: prim.indices,
|
|
4313
|
+
texture: prim.texture
|
|
4314
|
+
}));
|
|
4892
4315
|
}
|
|
4893
4316
|
}
|
|
4894
4317
|
let totalVertexCount = 0;
|
|
@@ -4900,7 +4323,7 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
4900
4323
|
totalVertexCount,
|
|
4901
4324
|
skinned: handles.some((h) => h.skinned),
|
|
4902
4325
|
animations: animNames,
|
|
4903
|
-
src:
|
|
4326
|
+
src: parsed.src
|
|
4904
4327
|
};
|
|
4905
4328
|
}
|
|
4906
4329
|
/**
|
|
@@ -4908,7 +4331,7 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
4908
4331
|
* with another instance (e.g., when spawning all parts of a character).
|
|
4909
4332
|
*/
|
|
4910
4333
|
addInstance(opts) {
|
|
4911
|
-
const modelOrGltf = opts.model;
|
|
4334
|
+
const modelOrGltf = isPrefab3D(opts.model) ? resolvePrefabHandle(opts.model) : opts.model;
|
|
4912
4335
|
if ("parts" in modelOrGltf) {
|
|
4913
4336
|
return this.addGltfInstance(opts, modelOrGltf);
|
|
4914
4337
|
}
|
|
@@ -4922,27 +4345,25 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
4922
4345
|
throw new Error(`Max instances (${this.maxModels}) reached`);
|
|
4923
4346
|
const dynBase = slot * DYNAMIC_MESH_FLOATS;
|
|
4924
4347
|
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];
|
|
4348
|
+
const t = resolveTransform(opts);
|
|
4349
|
+
this.dynamicData[dynBase + DYN_PREV_PX] = t.px;
|
|
4350
|
+
this.dynamicData[dynBase + DYN_PREV_PY] = t.py;
|
|
4351
|
+
this.dynamicData[dynBase + DYN_PREV_PZ] = t.pz;
|
|
4352
|
+
this.dynamicData[dynBase + DYN_CURR_PX] = t.px;
|
|
4353
|
+
this.dynamicData[dynBase + DYN_CURR_PY] = t.py;
|
|
4354
|
+
this.dynamicData[dynBase + DYN_CURR_PZ] = t.pz;
|
|
4355
|
+
this.dynamicData[dynBase + DYN_PREV_RX] = t.rx;
|
|
4356
|
+
this.dynamicData[dynBase + DYN_PREV_RY] = t.ry;
|
|
4357
|
+
this.dynamicData[dynBase + DYN_PREV_RZ] = t.rz;
|
|
4358
|
+
this.dynamicData[dynBase + DYN_CURR_RX] = t.rx;
|
|
4359
|
+
this.dynamicData[dynBase + DYN_CURR_RY] = t.ry;
|
|
4360
|
+
this.dynamicData[dynBase + DYN_CURR_RZ] = t.rz;
|
|
4361
|
+
this.staticData[statBase + STAT_SX] = t.sx;
|
|
4362
|
+
this.staticData[statBase + STAT_SY] = t.sy;
|
|
4363
|
+
this.staticData[statBase + STAT_SZ] = t.sz;
|
|
4364
|
+
this.staticData[statBase + STAT_CR] = t.cr;
|
|
4365
|
+
this.staticData[statBase + STAT_CG] = t.cg;
|
|
4366
|
+
this.staticData[statBase + STAT_CB] = t.cb;
|
|
4946
4367
|
this.staticDirty = true;
|
|
4947
4368
|
this.instanceModelIds[slot] = modelHandle.id;
|
|
4948
4369
|
this.batcher.add(0, modelHandle.id, slot);
|
|
@@ -5028,27 +4449,25 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
5028
4449
|
this.skinnedAnimStates[slot] = animState;
|
|
5029
4450
|
const dynBase = slot * DYNAMIC_MESH_FLOATS;
|
|
5030
4451
|
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.skinnedStaticData[statBase + SSTAT_CG] = color[1];
|
|
5051
|
-
this.skinnedStaticData[statBase + SSTAT_CB] = color[2];
|
|
4452
|
+
const t = resolveTransform(opts);
|
|
4453
|
+
this.skinnedDynamicData[dynBase + DYN_PREV_PX] = t.px;
|
|
4454
|
+
this.skinnedDynamicData[dynBase + DYN_PREV_PY] = t.py;
|
|
4455
|
+
this.skinnedDynamicData[dynBase + DYN_PREV_PZ] = t.pz;
|
|
4456
|
+
this.skinnedDynamicData[dynBase + DYN_CURR_PX] = t.px;
|
|
4457
|
+
this.skinnedDynamicData[dynBase + DYN_CURR_PY] = t.py;
|
|
4458
|
+
this.skinnedDynamicData[dynBase + DYN_CURR_PZ] = t.pz;
|
|
4459
|
+
this.skinnedDynamicData[dynBase + DYN_PREV_RX] = t.rx;
|
|
4460
|
+
this.skinnedDynamicData[dynBase + DYN_PREV_RY] = t.ry;
|
|
4461
|
+
this.skinnedDynamicData[dynBase + DYN_PREV_RZ] = t.rz;
|
|
4462
|
+
this.skinnedDynamicData[dynBase + DYN_CURR_RX] = t.rx;
|
|
4463
|
+
this.skinnedDynamicData[dynBase + DYN_CURR_RY] = t.ry;
|
|
4464
|
+
this.skinnedDynamicData[dynBase + DYN_CURR_RZ] = t.rz;
|
|
4465
|
+
this.skinnedStaticData[statBase + SSTAT_SX] = t.sx;
|
|
4466
|
+
this.skinnedStaticData[statBase + SSTAT_SY] = t.sy;
|
|
4467
|
+
this.skinnedStaticData[statBase + SSTAT_SZ] = t.sz;
|
|
4468
|
+
this.skinnedStaticData[statBase + SSTAT_CR] = t.cr;
|
|
4469
|
+
this.skinnedStaticData[statBase + SSTAT_CG] = t.cg;
|
|
4470
|
+
this.skinnedStaticData[statBase + SSTAT_CB] = t.cb;
|
|
5052
4471
|
new DataView(this.skinnedStaticData.buffer).setUint32(
|
|
5053
4472
|
(statBase + SSTAT_BONE_OFFSET) * 4,
|
|
5054
4473
|
boneOffset,
|
|
@@ -5092,16 +4511,102 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
5092
4511
|
}
|
|
5093
4512
|
};
|
|
5094
4513
|
}
|
|
4514
|
+
/**
|
|
4515
|
+
* Drain pending resyncs from the coordinator. Per affected skin: rebuild
|
|
4516
|
+
* its `SkeletalAnimation` clip list densely, remap in-flight animStates,
|
|
4517
|
+
* then full-repack `packedAnimData`. Falls back to a kernel rebuild only
|
|
4518
|
+
* when the new data exceeds the kernel's allocated budgets.
|
|
4519
|
+
*
|
|
4520
|
+
* The dense renumbering is load-bearing: the kernel reads each clip by
|
|
4521
|
+
* its per-skin index, which must equal `SkeletalAnimation`'s clip id.
|
|
4522
|
+
*/
|
|
4523
|
+
syncLazyAnimationChanges() {
|
|
4524
|
+
const resync = this.clipResync;
|
|
4525
|
+
if (!resync || resync.pending.size === 0)
|
|
4526
|
+
return;
|
|
4527
|
+
const remapsBySkin = /* @__PURE__ */ new Map();
|
|
4528
|
+
for (const skinIndex of resync.pending) {
|
|
4529
|
+
const sm = this.skinnedModels[skinIndex];
|
|
4530
|
+
if (!sm)
|
|
4531
|
+
continue;
|
|
4532
|
+
remapsBySkin.set(skinIndex, sm.animation.replaceClips(sm.parsedSkin.animClips));
|
|
4533
|
+
}
|
|
4534
|
+
resync.clear();
|
|
4535
|
+
for (let slot = 0; slot < this.maxSkinnedInstances; slot++) {
|
|
4536
|
+
const animState = this.skinnedAnimStates[slot];
|
|
4537
|
+
if (!animState)
|
|
4538
|
+
continue;
|
|
4539
|
+
const modelId = this.skinnedInstanceModelIds[slot];
|
|
4540
|
+
const model = this.models[modelId];
|
|
4541
|
+
if (!model || model.skinIndex < 0)
|
|
4542
|
+
continue;
|
|
4543
|
+
const remap3 = remapsBySkin.get(model.skinIndex);
|
|
4544
|
+
if (!remap3)
|
|
4545
|
+
continue;
|
|
4546
|
+
if (animState.clipId >= 0 && animState.clipId < remap3.length) {
|
|
4547
|
+
const next = remap3[animState.clipId];
|
|
4548
|
+
if (next < 0) {
|
|
4549
|
+
animState.clipId = -1;
|
|
4550
|
+
animState.playing = false;
|
|
4551
|
+
} else {
|
|
4552
|
+
animState.clipId = next;
|
|
4553
|
+
}
|
|
4554
|
+
}
|
|
4555
|
+
if (animState.prevClipId >= 0 && animState.prevClipId < remap3.length) {
|
|
4556
|
+
animState.prevClipId = remap3[animState.prevClipId];
|
|
4557
|
+
}
|
|
4558
|
+
}
|
|
4559
|
+
this.packedAnimData = createPackedAnimationData();
|
|
4560
|
+
for (const sm of this.skinnedModels) {
|
|
4561
|
+
packSkinAndAnimations(this.packedAnimData, sm.parsedSkin.data, sm.parsedSkin.animClips);
|
|
4562
|
+
}
|
|
4563
|
+
if (this.tryUploadInPlace())
|
|
4564
|
+
return;
|
|
4565
|
+
this.animComputeNeedsRebuild = true;
|
|
4566
|
+
}
|
|
4567
|
+
/** Doubling-growth budgets for kernel storage buffers, with sensible floors. */
|
|
4568
|
+
growBudgetsForPacked(packed, previous) {
|
|
4569
|
+
const pb = packAnimationData(packed);
|
|
4570
|
+
const grow = (cur, prev, floor) => Math.max(cur * 2, prev, floor);
|
|
4571
|
+
return {
|
|
4572
|
+
skelI32Capacity: grow(pb.skelI32.length, previous?.skelI32Capacity ?? 0, 256),
|
|
4573
|
+
animF32Capacity: grow(pb.animF32.length, previous?.animF32Capacity ?? 0, 4096),
|
|
4574
|
+
matricesCapacity: grow(pb.totalMats, previous?.matricesCapacity ?? 0, 8)
|
|
4575
|
+
};
|
|
4576
|
+
}
|
|
4577
|
+
/** Upload `packedAnimData` to the existing kernel iff it fits the current budgets. Returns false to force a rebuild. */
|
|
4578
|
+
tryUploadInPlace() {
|
|
4579
|
+
const kernel = this.animComputeKernel;
|
|
4580
|
+
const budgets = this.animKernelBudgets;
|
|
4581
|
+
if (!kernel || !budgets)
|
|
4582
|
+
return false;
|
|
4583
|
+
const pb = packAnimationData(this.packedAnimData);
|
|
4584
|
+
if (pb.skelI32.length > budgets.skelI32Capacity)
|
|
4585
|
+
return false;
|
|
4586
|
+
if (pb.animF32.length > budgets.animF32Capacity)
|
|
4587
|
+
return false;
|
|
4588
|
+
if (pb.totalMats > budgets.matricesCapacity)
|
|
4589
|
+
return false;
|
|
4590
|
+
uploadPackedToKernel(this.root, kernel, pb);
|
|
4591
|
+
this.animClipTableOffset = pb.clipTableOffset;
|
|
4592
|
+
this.animChannelTableOffset = pb.channelTableOffset;
|
|
4593
|
+
this.animJointLookupOffset = pb.jointLookupOffset;
|
|
4594
|
+
return true;
|
|
4595
|
+
}
|
|
5095
4596
|
updateAnimations(deltaTime) {
|
|
4597
|
+
this.syncLazyAnimationChanges();
|
|
5096
4598
|
if (this.animComputeNeedsRebuild && this.packedAnimData.clips.length > 0) {
|
|
5097
4599
|
this.animComputeKernel?.destroy();
|
|
4600
|
+
const budgets = this.growBudgetsForPacked(this.packedAnimData, null);
|
|
5098
4601
|
const { kernel, packedBuffers } = buildAnimationKernel(
|
|
5099
4602
|
this.root,
|
|
5100
4603
|
this.packedAnimData,
|
|
5101
4604
|
this.maxSkinnedInstances,
|
|
5102
|
-
this.maxTotalBones
|
|
4605
|
+
this.maxTotalBones,
|
|
4606
|
+
budgets
|
|
5103
4607
|
);
|
|
5104
4608
|
this.animComputeKernel = kernel;
|
|
4609
|
+
this.animKernelBudgets = budgets;
|
|
5105
4610
|
this.animClipTableOffset = packedBuffers.clipTableOffset;
|
|
5106
4611
|
this.animChannelTableOffset = packedBuffers.channelTableOffset;
|
|
5107
4612
|
this.animJointLookupOffset = packedBuffers.jointLookupOffset;
|
|
@@ -5118,6 +4623,14 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
5118
4623
|
{ binding: 4, resource: { buffer: rawBoneBuffer } }
|
|
5119
4624
|
]
|
|
5120
4625
|
});
|
|
4626
|
+
this.device.queue.writeBuffer(
|
|
4627
|
+
this.rawBoneMatrixBuffer,
|
|
4628
|
+
0,
|
|
4629
|
+
this.boneMatrixData.buffer,
|
|
4630
|
+
this.boneMatrixData.byteOffset,
|
|
4631
|
+
this.boneMatrixData.byteLength
|
|
4632
|
+
);
|
|
4633
|
+
this.boneMatrixDirty = false;
|
|
5121
4634
|
this.animComputeNeedsRebuild = false;
|
|
5122
4635
|
}
|
|
5123
4636
|
this.updatedBoneOffsets.fill(0);
|
|
@@ -5518,6 +5031,8 @@ var WebGPU3DRenderer = class extends Base3DRenderer {
|
|
|
5518
5031
|
this.resizeObserver?.disconnect();
|
|
5519
5032
|
this.resizeObserver = null;
|
|
5520
5033
|
this.resizeCallbacks.length = 0;
|
|
5034
|
+
this.clipResync?.dispose();
|
|
5035
|
+
this.clipResync = null;
|
|
5521
5036
|
this.dynamicBuffer?.destroy();
|
|
5522
5037
|
this.staticBuffer?.destroy();
|
|
5523
5038
|
this.uniformBuffer?.destroy();
|
|
@@ -5665,10 +5180,8 @@ var ParticleEmitter = class {
|
|
|
5665
5180
|
const sprite = this.renderer.addSprite({
|
|
5666
5181
|
sheet: this.config.sheet,
|
|
5667
5182
|
sprite: this.config.sprite ?? 0,
|
|
5668
|
-
x,
|
|
5669
|
-
|
|
5670
|
-
scaleX: size,
|
|
5671
|
-
scaleY: size,
|
|
5183
|
+
position: [x, y],
|
|
5184
|
+
scale: size,
|
|
5672
5185
|
opacity: 1,
|
|
5673
5186
|
tint: this.config.color,
|
|
5674
5187
|
layer: 255
|
|
@@ -5720,109 +5233,6 @@ var ParticleEmitter = class {
|
|
|
5720
5233
|
}
|
|
5721
5234
|
};
|
|
5722
5235
|
|
|
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
5236
|
// src/shaders/utils.ts
|
|
5827
5237
|
import tgpu7 from "typegpu";
|
|
5828
5238
|
import * as d6 from "typegpu/data";
|
|
@@ -5944,7 +5354,6 @@ export {
|
|
|
5944
5354
|
STATIC_OFFSET_UV_MAX_Y,
|
|
5945
5355
|
STATIC_OFFSET_UV_MIN_X,
|
|
5946
5356
|
STATIC_OFFSET_UV_MIN_Y,
|
|
5947
|
-
SkeletalAnimation,
|
|
5948
5357
|
SkinnedStaticMesh,
|
|
5949
5358
|
SpriteAccessor,
|
|
5950
5359
|
SpriteUniforms,
|
|
@@ -5954,14 +5363,11 @@ export {
|
|
|
5954
5363
|
StaticSprite,
|
|
5955
5364
|
WebGPU2DRenderer,
|
|
5956
5365
|
WebGPU3DRenderer,
|
|
5957
|
-
computeGridUVs,
|
|
5958
|
-
computeTexturePackerUVs,
|
|
5959
5366
|
createGeometryDataLayout,
|
|
5960
5367
|
createTextureFromBitmap,
|
|
5961
5368
|
d,
|
|
5962
5369
|
getFieldFloats,
|
|
5963
5370
|
inverseLerp,
|
|
5964
|
-
loadImage,
|
|
5965
5371
|
remap,
|
|
5966
5372
|
resolveBuiltInGeometry,
|
|
5967
5373
|
rotate2d,
|