@downdraft/engine 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/app/src/renderer/bootstrap.ts +0 -6
  2. package/app/src/renderer/feature-log.ts +1 -9
  3. package/app/src/renderer/mcp-harness.ts +8 -11
  4. package/app/src/renderer/standard-automation-tools.ts +8 -145
  5. package/core/src/module/host.ts +3 -6
  6. package/core/src/module/index.ts +1 -1
  7. package/core/src/module/module.ts +4 -47
  8. package/core/src/physics/interface.ts +5 -0
  9. package/core/src/platform/render-surface.ts +13 -0
  10. package/core/src/render/canvas-resize-watcher.ts +1 -10
  11. package/core/src/render/game-renderer.ts +40 -18
  12. package/core/src/render/index.ts +0 -2
  13. package/core/src/render/input-manager.ts +7 -7
  14. package/core/src/render/renderer-module-host.ts +2 -4
  15. package/core/src/ui-font-scale.ts +5 -38
  16. package/core/src/worker/rpc.ts +65 -0
  17. package/dist/.dd-manifest.json +859 -884
  18. package/dist/app/src/renderer/bootstrap.js +0 -4
  19. package/dist/app/src/renderer/feature-log.js +1 -10
  20. package/dist/app/src/renderer/mcp-harness.js +8 -11
  21. package/dist/app/src/renderer/standard-automation-tools.js +6 -138
  22. package/dist/core/src/module/host.js +3 -6
  23. package/dist/core/src/render/canvas-resize-watcher.js +1 -10
  24. package/dist/core/src/render/game-renderer.js +44 -18
  25. package/dist/core/src/render/index.js +0 -1
  26. package/dist/core/src/render/input-manager.js +7 -7
  27. package/dist/core/src/render/renderer-module-host.js +2 -4
  28. package/dist/core/src/ui-font-scale.js +5 -38
  29. package/dist/core/src/worker/rpc.js +59 -0
  30. package/dist/libraries/blitz-ui/src/native-module.js +8 -1
  31. package/dist/libraries/devtools/src/native-providers.js +0 -2
  32. package/dist/libraries/entities/src/model-renderer.js +87 -27
  33. package/dist/libraries/entities/src/shaders/model.wgsl +13 -4
  34. package/dist/libraries/networking/src/index.js +1 -0
  35. package/dist/libraries/networking/src/ws-server.js +387 -0
  36. package/dist/libraries/physics-rapier/src/api.js +15 -0
  37. package/dist/libraries/physics-rapier/src/backend.js +15 -0
  38. package/dist/libraries/physics-rapier/src/rapier-backend.js +5 -0
  39. package/dist/libraries/postfx/src/post-process-stack.js +1 -3
  40. package/dist/libraries/sand/src/materials.js +2 -32
  41. package/dist/libraries/sand/src/sand-world.js +0 -3
  42. package/dist/modules/devtools/src/api.js +8 -23
  43. package/dist/modules/devtools/src/data-bridge.js +0 -11
  44. package/dist/modules/devtools/src/index.js +0 -6
  45. package/dist/modules/devtools/src/init.js +8 -42
  46. package/dist/modules/devtools/src/scene-inspector.js +1 -4
  47. package/dist/modules/devtools/src/worker-sync.js +3 -11
  48. package/dist/test/src/vision-test.js +1 -3
  49. package/libraries/blitz-ui/src/native-module.ts +8 -1
  50. package/libraries/devtools/src/native-providers.ts +0 -2
  51. package/libraries/entities/src/model-renderer.ts +88 -27
  52. package/libraries/entities/src/shaders/model.wgsl +13 -4
  53. package/libraries/networking/src/index.ts +4 -0
  54. package/libraries/networking/src/ws-server.ts +397 -0
  55. package/libraries/physics-rapier/src/api.ts +15 -0
  56. package/libraries/physics-rapier/src/backend.ts +15 -0
  57. package/libraries/physics-rapier/src/rapier-backend.ts +6 -0
  58. package/libraries/postfx/src/post-process-stack.ts +1 -2
  59. package/libraries/sand/src/materials.ts +2 -32
  60. package/libraries/sand/src/sand-world.ts +0 -4
  61. package/modules/devtools/README.md +1 -3
  62. package/modules/devtools/src/api.ts +10 -34
  63. package/modules/devtools/src/data-bridge.ts +0 -16
  64. package/modules/devtools/src/index.ts +1 -11
  65. package/modules/devtools/src/init.ts +8 -51
  66. package/modules/devtools/src/scene-inspector.ts +2 -6
  67. package/modules/devtools/src/types.ts +5 -90
  68. package/modules/devtools/src/worker-sync.ts +3 -13
  69. package/package.json +4 -5
  70. package/test/src/vision-test.ts +2 -4
  71. package/core/src/render/sab-canvas-overlay.ts +0 -122
  72. package/dist/core/src/render/sab-canvas-overlay.js +0 -83
  73. package/dist/modules/devtools/src/debug-overlay.js +0 -181
  74. package/dist/modules/devtools/src/doctor-panel.js +0 -163
  75. package/dist/modules/devtools/src/label-overlay.js +0 -129
  76. package/dist/modules/devtools/src/material-stats-panel.js +0 -171
  77. package/dist/modules/devtools/src/sim-stats-panel.js +0 -250
  78. package/modules/devtools/src/debug-overlay.ts +0 -238
  79. package/modules/devtools/src/doctor-panel.ts +0 -182
  80. package/modules/devtools/src/label-overlay.ts +0 -154
  81. package/modules/devtools/src/material-stats-panel.ts +0 -188
  82. package/modules/devtools/src/sim-stats-panel.ts +0 -272
@@ -5,10 +5,10 @@
5
5
  // 1. Creating an IDevToolsDataRenderer adapter
6
6
  // 2. Instantiating DevToolsDataBridge (or BaseSceneInspector)
7
7
  // 3. Calling init()
8
- // 4. Wiring sim stats provider, panel extensions, overlay toggles, etc.
8
+ // 4. Wiring sim stats provider, debug overlay provider, etc.
9
9
  //
10
- // Also merges globally-registered panels/feeds/commands from the `devtools`
11
- // singleton (registered by plugins via ctx.devtools.registerPanel() etc.)
10
+ // Also merges globally-registered feeds/commands from the `devtools`
11
+ // singleton (registered by plugins via ctx.devtools.registerDataFeed() etc.)
12
12
  // and from worker manifests (via syncWorkerManifests).
13
13
  //
14
14
  // Returns the bridge instance for games that need post-init calls
@@ -22,7 +22,7 @@ import { BaseSceneInspector } from "./scene-inspector.js";
22
22
  import { syncWorkerManifests } from "./worker-sync.js";
23
23
  /**
24
24
  * Initialize DevTools with one call. Creates the bridge, wires all providers,
25
- merges globally-registered panels/feeds/commands, syncs worker manifests,
25
+ * merges globally-registered feeds/commands, syncs worker manifests,
26
26
  * and exposes the API on window.__sceneInspector.
27
27
  *
28
28
  * Returns the bridge instance.
@@ -34,19 +34,7 @@ export async function initDevTools(renderer, options = {}) {
34
34
  const useSceneInspector = options.sceneInspector
35
35
  || !!options.bridgeClass
36
36
  || isSceneRenderer(renderer);
37
- // 3. Register game-declared panels/toggles into the global registry
38
- // (these merge with plugin-registered ones)
39
- if (options.panels) {
40
- options.panels.forEach((panel) => {
41
- devtools.registerPanel(panel);
42
- });
43
- }
44
- if (options.overlayToggles) {
45
- options.overlayToggles.forEach((toggle) => {
46
- devtools.registerOverlayToggle(toggle);
47
- });
48
- }
49
- // 4. Sync worker manifests (fetches panels/feeds/commands from sim workers)
37
+ // 4. Sync worker manifests (fetches feeds/commands from sim workers)
50
38
  if (options.workerHosts && options.workerHosts.length > 0) {
51
39
  await syncWorkerManifests(options.workerHosts);
52
40
  }
@@ -85,10 +73,8 @@ export async function initDevTools(renderer, options = {}) {
85
73
  if (options.performanceMetricsProvider) {
86
74
  wirePerformanceMetricsProvider(bridge, options.performanceMetricsProvider);
87
75
  }
88
- // 7. Merge globally-registered panels/toggles/feeds/commands into the bridge
89
- // The bridge's getPanelExtensions()/getOverlayToggles() already returns
90
- // game-declared ones; we augment the __sceneInspector API with
91
- // plugin-registered data feeds and commands.
76
+ // 7. Merge globally-registered feeds/commands/SAB stats into the bridge —
77
+ // augments the __sceneInspector API with plugin-registered methods.
92
78
  mergeGlobalRegistrations(bridge);
93
79
  // 8. Initialize the profiling system (if enabled)
94
80
  if (options.profiling) {
@@ -134,35 +120,15 @@ function wirePerformanceMetricsProvider(bridge, provider) {
134
120
  bridge.getPerformanceMetricsProvider = () => provider;
135
121
  }
136
122
  /**
137
- * Merge globally-registered panels, data feeds, and commands from the `devtools`
123
+ * Merge globally-registered data feeds and commands from the `devtools`
138
124
  * singleton into the bridge's __sceneInspector API.
139
125
  *
140
- * - Panels: merged into getPanelExtensions() (game panels + plugin panels)
141
126
  * - Data feeds: added as methods on __sceneInspector (called synchronously)
142
127
  * - Commands: added as methods on __sceneInspector (forwarded to worker or called directly)
143
128
  * - SAB stats: added as methods on __sceneInspector (read from SAB)
144
129
  */
145
130
  function mergeGlobalRegistrations(bridge) {
146
131
  const manifest = devtools.getManifest();
147
- // Augment getPanelExtensions to merge global registry panels
148
- const originalGetPanels = bridge.getPanelExtensions?.bind(bridge);
149
- bridge.getPanelExtensions = () => {
150
- const gamePanels = originalGetPanels?.() ?? [];
151
- // Merge: game panels take precedence on id collisions
152
- const merged = new Map();
153
- manifest.panels.forEach((p) => { merged.set(p.id, p); ; });
154
- gamePanels.forEach((p) => { merged.set(p.id, p); ; });
155
- return Array.from(merged.values()).sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
156
- };
157
- // Augment getOverlayToggles to merge global registry toggles
158
- const originalGetToggles = bridge.getOverlayToggles?.bind(bridge);
159
- bridge.getOverlayToggles = () => {
160
- const gameToggles = originalGetToggles?.() ?? [];
161
- const merged = new Map();
162
- manifest.toggles.forEach((t) => { merged.set(t.id, t); ; });
163
- gameToggles.forEach((t) => { merged.set(t.id, t); ; });
164
- return Array.from(merged.values());
165
- };
166
132
  // Add data feed methods to the __sceneInspector API
167
133
  // These are called synchronously by the panel via callInspector()
168
134
  const api = window.__sceneInspector;
@@ -27,7 +27,6 @@ export class BaseSceneInspector extends DevToolsDataBridge {
27
27
  getDebugOverlayProvider() {
28
28
  return null;
29
29
  }
30
- // getPanelExtensions() and getOverlayToggles() inherited from DevToolsDataBridge.
31
30
  // --- Init ---
32
31
  init(renderer) {
33
32
  super.init(renderer);
@@ -510,9 +509,7 @@ export class BaseSceneInspector extends DevToolsDataBridge {
510
509
  const cosY = Math.cos(angleY), sinY = Math.sin(angleY);
511
510
  const cosX = Math.cos(angleX), sinX = Math.sin(angleX);
512
511
  const THUMB_SIZE = 192;
513
- const canvas = document.createElement("canvas");
514
- canvas.width = THUMB_SIZE;
515
- canvas.height = THUMB_SIZE;
512
+ const canvas = new OffscreenCanvas(THUMB_SIZE, THUMB_SIZE);
516
513
  const ctx = canvas.getContext("2d");
517
514
  if (!ctx)
518
515
  return null;
@@ -1,14 +1,13 @@
1
1
  // ============================================================================
2
2
  // Renderer-side worker manifest sync — fetches the devtools manifest from
3
- // one or more sim workers via worker RPC, then wires the worker-registered panels,
4
- // data feeds, and commands into the renderer-side __sceneInspector API.
3
+ // one or more sim workers via worker RPC, then wires the worker-registered
4
+ // data feeds and commands into the renderer-side __sceneInspector API.
5
5
  //
6
6
  // Data feeds from workers are read from the devtools SAB (zero-copy).
7
7
  // Commands are forwarded via worker RPC.
8
- // Panel declarations are merged into the global registry.
9
8
  // ============================================================================
10
9
  import { createLogger } from "@downdraft/engine/util/logger";
11
- import { _devtoolsImpl, devtools } from "./api.js";
10
+ import { _devtoolsImpl } from "./api.js";
12
11
  const log = createLogger("info");
13
12
  /**
14
13
  * Sync manifests from one or more workers into the renderer-side devtools
@@ -54,13 +53,6 @@ async function syncOneWorker(entry) {
54
53
  // Worker may not have a devtools SAB (older workers)
55
54
  }
56
55
  }
57
- // Register panels (namespaced by prefix to avoid collisions)
58
- manifest.panels.forEach((panel) => {
59
- const namespaced = prefix
60
- ? { ...panel, id: `${prefix}:${panel.id}` }
61
- : panel;
62
- devtools.registerPanel(namespaced);
63
- });
64
56
  // Register data feed readers — these read from the SAB synchronously
65
57
  // We register them as main-realm data feeds that read from the worker SAB
66
58
  if (sab) {
@@ -155,9 +155,7 @@ export class VisionTest {
155
155
  }
156
156
  async screenshotToDataURL(texture, width, height) {
157
157
  const data = await this.captureTexture(texture, width, height);
158
- const canvas = document.createElement("canvas");
159
- canvas.width = width;
160
- canvas.height = height;
158
+ const canvas = new OffscreenCanvas(width, height);
161
159
  const ctx = canvas.getContext("2d");
162
160
  const imageData = ctx.createImageData(width, height);
163
161
  imageData.data.set(data);
@@ -169,7 +169,14 @@ export function createBlitzUiNativeModule<S>(options: BlitzUiNativeOptions<S>):
169
169
  const device = ctx.getDevice();
170
170
  const view = ensureTexture(device);
171
171
  const context = ctx.getSurface().getContext("webgpu");
172
- const targetTex = context?.getCurrentTexture();
172
+ // Join the in-flight frame, never start one: on a frame the
173
+ // game skipped (dirty-tracking), acquiring the surface here
174
+ // would present the UI blit over a blank swapchain image.
175
+ const peek = (context as { peekCurrentTexture?: () => GPUTexture | null } | null)
176
+ ?.peekCurrentTexture;
177
+ const targetTex = typeof peek === "function"
178
+ ? peek.call(context)
179
+ : context?.getCurrentTexture() ?? null;
173
180
  const target = targetTex?.createView();
174
181
  if (!view || !target || !targetTex) return;
175
182
 
@@ -525,8 +525,6 @@ async function collectWorkers(ctx: EngineProviderContext): Promise<PanelSnapshot
525
525
  kind: "kv",
526
526
  name: `Worker manifest (v${m.manifestVersion ?? "?"})`,
527
527
  rows: [
528
- kv("Panels", (m.panels ?? []).map((p: any) => p.id ?? p.name ?? "?").join(", ") || "(none)"),
529
- kv("Toggles", (m.toggles ?? []).map((t: any) => t.id ?? "?").join(", ") || "(none)"),
530
528
  kv("Data feeds", (m.dataFeeds ?? []).map((f: any) => `${f.name}@${f.writeRateHz}hz`).join(", ") || "(none)"),
531
529
  kv("Commands", (m.commands ?? []).join(", ") || "(none)"),
532
530
  ],
@@ -86,9 +86,11 @@ export class ModelRenderer {
86
86
  private reusableUniformsU32 = new Uint32Array(this.reusableUniforms.buffer);
87
87
  private textureLoadVersion = new Map<string, number>();
88
88
 
89
- // Skinning: a shared storage buffer of bone matrices (MAX_BONES * mat4),
90
- // updated per-frame via updateSkinMatrices. Bound at @group(1) for skinned
91
- // draws only.
89
+ // Skinning: a shared storage buffer of bone matrices (mat4 each), updated
90
+ // per-frame via updateSkinMatrices/updateSkinMatricesFor. Bound at @group(1)
91
+ // for skinned draws only. Each skinned nodeId owns a region of the buffer —
92
+ // the draw's `boneBase` uniform selects it so multiple skinned models can be
93
+ // animated independently in one frame.
92
94
  private skinMatrixBuffer: GPUBuffer | null = null;
93
95
  private skinBindGroupLayout: GPUBindGroupLayout | null = null;
94
96
  private skinBindGroup: GPUBindGroup | null = null;
@@ -96,6 +98,16 @@ export class ModelRenderer {
96
98
  private skinBindGroupSetThisFrame = false;
97
99
  /** Current bone capacity (number of mat4s the skin buffer can hold). Grows on demand. */
98
100
  private boneCapacity = ModelRenderer.INITIAL_BONE_CAPACITY;
101
+ /** nodeId → reserved bone region (offset+count in bones). */
102
+ private skinRegions = new Map<string, { offset: number; count: number }>();
103
+ /** nodeId → bone base index used by the skinning shader for that draw. */
104
+ private skinBaseByNode = new Map<string, number>();
105
+ /** Freed bone regions available for reuse (first-fit). */
106
+ private skinFreeList: Array<{ offset: number; count: number }> = [];
107
+ /** Next never-allocated bone index (high-water mark). */
108
+ private nextBoneOffset = 0;
109
+ /** Legacy caller region — updateSkinMatrices(m) without a nodeId. */
110
+ private static readonly DEFAULT_SKIN_NODE = "__skin_default";
99
111
 
100
112
  // Bindless deps
101
113
  private bindless: ModelRendererBindlessDeps | null = null;
@@ -822,6 +834,7 @@ export class ModelRenderer {
822
834
  }
823
835
 
824
836
  removeModel(nodeId: string): void {
837
+ this.releaseSkinRegion(nodeId);
825
838
  const resources = this.modelResources.get(nodeId);
826
839
  if (resources) {
827
840
  for (let i = 0; i < resources.length; i++) {
@@ -1082,43 +1095,85 @@ export class ModelRenderer {
1082
1095
  }
1083
1096
 
1084
1097
  /**
1085
- * Upload bone skin matrices for the current frame. The `matrices` buffer is
1086
- * a flat Float32Array of boneCount * 16 floats (column-major mat4s). It is
1087
- * copied into the shared skin matrix storage buffer. If the buffer is too
1088
- * small for the current skeleton, it is recreated (grown) automatically.
1089
- * Call once per frame before render() for skinned models.
1098
+ * Upload bone skin matrices for the current frame into the shared default
1099
+ * region. Kept for single-avatar callers — equivalent to
1100
+ * updateSkinMatricesFor("__skin_default", matrices).
1090
1101
  */
1091
1102
  updateSkinMatrices(matrices: Float32Array): void {
1103
+ this.updateSkinMatricesFor(ModelRenderer.DEFAULT_SKIN_NODE, matrices);
1104
+ }
1105
+
1106
+ /**
1107
+ * Upload bone skin matrices for one nodeId's reserved region. The region is
1108
+ * (re)allocated automatically — freed regions from removeModel() are reused
1109
+ * first-fit. Render() looks up the node's region offset and passes it to the
1110
+ * skinning shader via the `boneBase` uniform, so multiple skinned models
1111
+ * animate independently.
1112
+ */
1113
+ updateSkinMatricesFor(nodeId: string, matrices: Float32Array): void {
1092
1114
  if (!this.skinMatrixBuffer) return;
1093
1115
 
1094
1116
  const requiredBones = matrices.length / 16;
1095
- if (requiredBones > this.boneCapacity) {
1096
- // Grow the skin matrix buffer to fit the larger skeleton.
1097
- // Round up to the next power of 2 to reduce reallocations.
1098
- const newCapacity = Math.max(requiredBones, this.boneCapacity * 2);
1099
- this.skinMatrixBuffer.destroy();
1100
- this.skinMatrixBuffer = this.device.createBuffer({
1101
- size: newCapacity * 64,
1102
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
1103
- });
1104
- this.boneCapacity = newCapacity;
1105
- // Recreate the bind group with the new buffer
1106
- if (this.skinBindGroupLayout) {
1107
- this.skinBindGroup = this.device.createBindGroup({
1108
- layout: this.skinBindGroupLayout,
1109
- entries: [{ binding: 0, resource: { buffer: this.skinMatrixBuffer } }],
1110
- });
1111
- }
1112
- this.skinBindGroupSetThisFrame = false; // force re-bind
1117
+ let region = this.skinRegions.get(nodeId);
1118
+ if (!region || region.count < requiredBones) {
1119
+ if (region) this.skinFreeList.push(region);
1120
+ region = this.allocSkinRegion(requiredBones);
1121
+ this.skinRegions.set(nodeId, region);
1113
1122
  }
1123
+ this.skinBaseByNode.set(nodeId, region.offset);
1114
1124
 
1115
1125
  this.device.queue.writeBuffer(
1116
1126
  this.skinMatrixBuffer,
1117
- 0,
1127
+ region.offset * 64,
1118
1128
  matrices as Float32Array<ArrayBuffer>,
1119
1129
  );
1120
1130
  }
1121
1131
 
1132
+ /** Allocate a bone region for `count` bones — free-list first, then bump
1133
+ * allocation; grows the shared buffer when the capacity is exceeded. */
1134
+ private allocSkinRegion(count: number): { offset: number; count: number } {
1135
+ for (let i = 0; i < this.skinFreeList.length; i++) {
1136
+ const f = this.skinFreeList[i];
1137
+ if (f.count >= count) {
1138
+ this.skinFreeList.splice(i, 1);
1139
+ return { offset: f.offset, count: f.count };
1140
+ }
1141
+ }
1142
+ const needed = this.nextBoneOffset + count;
1143
+ if (needed > this.boneCapacity) this.growSkinBuffer(needed);
1144
+ const region = { offset: this.nextBoneOffset, count };
1145
+ this.nextBoneOffset += count;
1146
+ return region;
1147
+ }
1148
+
1149
+ /** Recreate the skin matrix buffer at a larger capacity and rebind group(1).
1150
+ * Region offsets stay valid — callers rewrite their matrices next frame. */
1151
+ private growSkinBuffer(minBones: number): void {
1152
+ const newCapacity = Math.max(minBones, this.boneCapacity * 2);
1153
+ this.skinMatrixBuffer?.destroy();
1154
+ this.skinMatrixBuffer = this.device.createBuffer({
1155
+ size: newCapacity * 64,
1156
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
1157
+ });
1158
+ this.boneCapacity = newCapacity;
1159
+ if (this.skinBindGroupLayout) {
1160
+ this.skinBindGroup = this.device.createBindGroup({
1161
+ layout: this.skinBindGroupLayout,
1162
+ entries: [{ binding: 0, resource: { buffer: this.skinMatrixBuffer } }],
1163
+ });
1164
+ }
1165
+ this.skinBindGroupSetThisFrame = false; // force re-bind
1166
+ }
1167
+
1168
+ /** Release a node's bone region back to the free list. */
1169
+ releaseSkinRegion(nodeId: string): void {
1170
+ const region = this.skinRegions.get(nodeId);
1171
+ if (!region) return;
1172
+ this.skinRegions.delete(nodeId);
1173
+ this.skinBaseByNode.delete(nodeId);
1174
+ this.skinFreeList.push(region);
1175
+ }
1176
+
1122
1177
  render(
1123
1178
  passEncoder: GPURenderPassEncoder,
1124
1179
  nodeId: string,
@@ -1221,6 +1276,12 @@ export class ModelRenderer {
1221
1276
  // fs_outline). Defaults: 0 width, black color.
1222
1277
  uniforms[39] = 0.0;
1223
1278
  uniforms[40] = 0.0; uniforms[41] = 0.0; uniforms[42] = 0.0;
1279
+ // boneBase (u32, slot 44) — index of this node's bone region inside the
1280
+ // shared skinMatrices storage buffer. Only read by vs_skinned.
1281
+ this.reusableUniformsU32[44] =
1282
+ this.skinBaseByNode.get(nodeId)
1283
+ ?? this.skinBaseByNode.get(ModelRenderer.DEFAULT_SKIN_NODE)
1284
+ ?? 0;
1224
1285
 
1225
1286
  this.device.queue.writeBuffer(
1226
1287
  this.uniformBuffer,
@@ -22,6 +22,14 @@ struct Uniforms {
22
22
  // Inverted-hull outline parameters (used by vs_outline / fs_outline only).
23
23
  outlineWidth: f32,
24
24
  outlineColor: vec3<f32>,
25
+ // Base index of this draw's bone region inside the shared skinMatrices
26
+ // storage buffer (group(1)). Written by ModelRenderer from the node's
27
+ // reserved region — lets multiple skinned models animate independently.
28
+ // Only read by vs_skinned.
29
+ boneBase: u32,
30
+ _padSkin0: u32,
31
+ _padSkin1: u32,
32
+ _padSkin2: u32,
25
33
  };
26
34
 
27
35
  @group(0) @binding(0) var<uniform> uniforms: Uniforms;
@@ -165,10 +173,11 @@ fn qrotate(q: vec4<f32>, v: vec3<f32>) -> vec3<f32> {
165
173
  // If all weights are zero (vertex has no bone assignment), return identity so
166
174
  // the vertex renders at its baked position instead of collapsing to the origin.
167
175
  fn skinMatrix(j: vec4<u32>, w: vec4<f32>) -> mat4x4<f32> {
168
- let m = skinMatrices[j.x] * w.x
169
- + skinMatrices[j.y] * w.y
170
- + skinMatrices[j.z] * w.z
171
- + skinMatrices[j.w] * w.w;
176
+ let base = uniforms.boneBase;
177
+ let m = skinMatrices[base + j.x] * w.x
178
+ + skinMatrices[base + j.y] * w.y
179
+ + skinMatrices[base + j.z] * w.z
180
+ + skinMatrices[base + j.w] * w.w;
172
181
  let wsum = w.x + w.y + w.z + w.w;
173
182
  if (wsum < 0.001) {
174
183
  return mat4x4<f32>(
@@ -43,3 +43,7 @@ export type { NetworkingLibConfig } from "./library";
43
43
 
44
44
  export { LanDiscovery, MdnsBrowser } from "./discovery";
45
45
  export type { LanBeacon, LanDiscoveryOptions, LanPeer, MdnsBrowseOptions, MdnsService } from "./discovery";
46
+
47
+ export { createWebSocketServer } from "./ws-server";
48
+ export type { WebSocketServerHandlers, WebSocketServerOptions, WsServer, WsServerConnection } from "./ws-server";
49
+