@aethervtt/shard-render 0.0.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 (139) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/dist/assets.d.ts +104 -0
  4. package/dist/assets.js +213 -0
  5. package/dist/atmosphere-model.d.ts +96 -0
  6. package/dist/atmosphere-model.js +370 -0
  7. package/dist/atmosphere-nodes.d.ts +55 -0
  8. package/dist/atmosphere-nodes.js +780 -0
  9. package/dist/atmosphere-plugin.d.ts +5 -0
  10. package/dist/atmosphere-plugin.js +42 -0
  11. package/dist/atmosphere-shaders.d.ts +7 -0
  12. package/dist/atmosphere-shaders.js +595 -0
  13. package/dist/atmosphere-state.d.ts +72 -0
  14. package/dist/atmosphere-state.js +18 -0
  15. package/dist/atmosphere.d.ts +191 -0
  16. package/dist/atmosphere.js +950 -0
  17. package/dist/camera.d.ts +83 -0
  18. package/dist/camera.js +163 -0
  19. package/dist/clusters.d.ts +68 -0
  20. package/dist/clusters.js +288 -0
  21. package/dist/culling-shaders.d.ts +2 -0
  22. package/dist/culling-shaders.js +203 -0
  23. package/dist/culling.d.ts +84 -0
  24. package/dist/culling.js +498 -0
  25. package/dist/debug-shaders.d.ts +3 -0
  26. package/dist/debug-shaders.js +207 -0
  27. package/dist/debug-views.d.ts +60 -0
  28. package/dist/debug-views.js +197 -0
  29. package/dist/deferred-plugin.d.ts +5 -0
  30. package/dist/deferred-plugin.js +20 -0
  31. package/dist/deferred-shaders.d.ts +2 -0
  32. package/dist/deferred-shaders.js +151 -0
  33. package/dist/deferred.d.ts +22 -0
  34. package/dist/deferred.js +296 -0
  35. package/dist/deform.d.ts +80 -0
  36. package/dist/deform.js +213 -0
  37. package/dist/describe-plugin.d.ts +5 -0
  38. package/dist/describe-plugin.js +19 -0
  39. package/dist/display-nodes.d.ts +5 -0
  40. package/dist/display-nodes.js +189 -0
  41. package/dist/dynamic-resolution.d.ts +47 -0
  42. package/dist/dynamic-resolution.js +191 -0
  43. package/dist/environment-plugin.d.ts +6 -0
  44. package/dist/environment-plugin.js +44 -0
  45. package/dist/environment-shaders.d.ts +2 -0
  46. package/dist/environment-shaders.js +260 -0
  47. package/dist/environment-state.d.ts +71 -0
  48. package/dist/environment-state.js +144 -0
  49. package/dist/environment.d.ts +70 -0
  50. package/dist/environment.js +529 -0
  51. package/dist/forward.d.ts +123 -0
  52. package/dist/forward.js +1216 -0
  53. package/dist/fxaa.d.ts +2 -0
  54. package/dist/fxaa.js +65 -0
  55. package/dist/gizmo-font.d.ts +21 -0
  56. package/dist/gizmo-font.js +132 -0
  57. package/dist/gizmos-plugin.d.ts +5 -0
  58. package/dist/gizmos-plugin.js +34 -0
  59. package/dist/gizmos.d.ts +156 -0
  60. package/dist/gizmos.js +772 -0
  61. package/dist/gpu-assets.d.ts +114 -0
  62. package/dist/gpu-assets.js +494 -0
  63. package/dist/graph.d.ts +204 -0
  64. package/dist/graph.js +417 -0
  65. package/dist/index.d.ts +53 -0
  66. package/dist/index.js +540 -0
  67. package/dist/instances.d.ts +349 -0
  68. package/dist/instances.js +1335 -0
  69. package/dist/large-world.d.ts +13 -0
  70. package/dist/large-world.js +20 -0
  71. package/dist/lights.d.ts +160 -0
  72. package/dist/lights.js +468 -0
  73. package/dist/material-noise.d.ts +19 -0
  74. package/dist/material-noise.js +7 -0
  75. package/dist/material-pipelines.d.ts +46 -0
  76. package/dist/material-pipelines.js +166 -0
  77. package/dist/materials.d.ts +108 -0
  78. package/dist/materials.js +219 -0
  79. package/dist/noise.d.ts +76 -0
  80. package/dist/noise.js +286 -0
  81. package/dist/overlay-registry.d.ts +18 -0
  82. package/dist/overlay-registry.js +30 -0
  83. package/dist/overlays.d.ts +35 -0
  84. package/dist/overlays.js +344 -0
  85. package/dist/paths.d.ts +20 -0
  86. package/dist/paths.js +46 -0
  87. package/dist/picking-plugin.d.ts +2 -0
  88. package/dist/picking-plugin.js +20 -0
  89. package/dist/picking.d.ts +94 -0
  90. package/dist/picking.js +456 -0
  91. package/dist/pixel-perfect-plugin.d.ts +9 -0
  92. package/dist/pixel-perfect-plugin.js +135 -0
  93. package/dist/pixel-perfect.d.ts +27 -0
  94. package/dist/pixel-perfect.js +70 -0
  95. package/dist/plugin.d.ts +111 -0
  96. package/dist/plugin.js +182 -0
  97. package/dist/pool.d.ts +19 -0
  98. package/dist/pool.js +43 -0
  99. package/dist/post-common.d.ts +51 -0
  100. package/dist/post-common.js +148 -0
  101. package/dist/post-nodes.d.ts +39 -0
  102. package/dist/post-nodes.js +849 -0
  103. package/dist/post-plugin.d.ts +5 -0
  104. package/dist/post-plugin.js +26 -0
  105. package/dist/post-shaders.d.ts +8 -0
  106. package/dist/post-shaders.js +698 -0
  107. package/dist/post.d.ts +175 -0
  108. package/dist/post.js +453 -0
  109. package/dist/readback.d.ts +4 -0
  110. package/dist/readback.js +95 -0
  111. package/dist/render-scale.d.ts +53 -0
  112. package/dist/render-scale.js +63 -0
  113. package/dist/shaders.d.ts +122 -0
  114. package/dist/shaders.js +1168 -0
  115. package/dist/shadows.d.ts +119 -0
  116. package/dist/shadows.js +562 -0
  117. package/dist/skin-asset.d.ts +26 -0
  118. package/dist/skin-asset.js +25 -0
  119. package/dist/skinning-plugin.d.ts +5 -0
  120. package/dist/skinning-plugin.js +16 -0
  121. package/dist/skinning.d.ts +19 -0
  122. package/dist/skinning.js +343 -0
  123. package/dist/standard-fields.d.ts +67 -0
  124. package/dist/standard-fields.js +73 -0
  125. package/dist/standard.d.ts +7 -0
  126. package/dist/standard.js +37 -0
  127. package/dist/stats.d.ts +25 -0
  128. package/dist/stats.js +18 -0
  129. package/dist/target.d.ts +55 -0
  130. package/dist/target.js +70 -0
  131. package/dist/testing.d.ts +34 -0
  132. package/dist/testing.js +126 -0
  133. package/dist/timer.d.ts +28 -0
  134. package/dist/timer.js +136 -0
  135. package/dist/view.d.ts +117 -0
  136. package/dist/view.js +389 -0
  137. package/dist/visibility.d.ts +10 -0
  138. package/dist/visibility.js +84 -0
  139. package/package.json +49 -0
@@ -0,0 +1,296 @@
1
+ import { ShardError } from "@aethervtt/shard-core";
2
+ import { GpuBuffer } from "@aethervtt/shard-gpu";
3
+ import { Culler } from "./culling.js";
4
+ import {
5
+ drawMaterials,
6
+ ForwardStateResource,
7
+ gbufferEmissiveFormat,
8
+ PASS_GBUFFER,
9
+ PASS_OPAQUE,
10
+ sceneColor,
11
+ viewBindGroup
12
+ } from "./forward.js";
13
+ import { RenderPhase } from "./graph.js";
14
+ import { Gpu, Graph, RenderDescribers, Shaders, Views } from "./plugin.js";
15
+ import { Cameras, cameraOf } from "./view.js";
16
+ const GBUFFER_CHANNELS = ["albedo", "normal", "roughness", "metallic", "emissive"];
17
+ const deferredView = (view) => cameraOf(view)?.deferred === true;
18
+ const needsGBuffer = (view) => {
19
+ const cam = cameraOf(view);
20
+ return cam !== void 0 && (cam.deferred || cam.gbufferDebug >= 0);
21
+ };
22
+ const GBUFFER_CLEAR = [
23
+ { resource: "gbuffer0", clear: { r: 0, g: 0, b: 0, a: 1 } },
24
+ { resource: "gbuffer1", clear: { r: 0, g: 0, b: 0, a: 0 } },
25
+ { resource: "gbuffer2", clear: { r: 0, g: 0, b: 0, a: 0 } }
26
+ ];
27
+ function fillGBuffer(ctx) {
28
+ const state = ctx.world.resource(ForwardStateResource);
29
+ const cam = cameraOf(ctx.view);
30
+ const pv = state.views.get(ctx.view.name);
31
+ if (pv) drawMaterials(ctx, state, pv, cam, cam.draws, PASS_GBUFFER);
32
+ }
33
+ const gbufferNode = {
34
+ kind: "render",
35
+ phase: RenderPhase.Opaque,
36
+ enabled: deferredView,
37
+ writes: ["gbuffer0", "gbuffer1", "gbuffer2", "scene-depth"],
38
+ color: GBUFFER_CLEAR,
39
+ depth: { resource: "scene-depth", clear: 0 },
40
+ run: fillGBuffer
41
+ };
42
+ const gbufferFillNode = {
43
+ kind: "render",
44
+ phase: RenderPhase.Opaque,
45
+ enabled: (view) => needsGBuffer(view) && !deferredView(view),
46
+ writes: [
47
+ "gbuffer0",
48
+ "gbuffer1",
49
+ "gbuffer2",
50
+ { name: "gbuffer-depth", format: "depth32float" }
51
+ ],
52
+ color: GBUFFER_CLEAR,
53
+ depth: { resource: "gbuffer-depth", clear: 0 },
54
+ run: fillGBuffer
55
+ };
56
+ function lightingNode() {
57
+ let layout;
58
+ let layoutGeneration = -1;
59
+ const bindGroups = /* @__PURE__ */ new Map();
60
+ return {
61
+ kind: "render",
62
+ phase: RenderPhase.Lighting,
63
+ enabled: deferredView,
64
+ reads: [
65
+ "gbuffer0",
66
+ "gbuffer1",
67
+ "gbuffer2",
68
+ "scene-depth",
69
+ "clusters",
70
+ "environment",
71
+ "shadow-cascades",
72
+ "shadow-local",
73
+ "ssao"
74
+ ],
75
+ writes: ["scene-color", "hdr"],
76
+ color: (view) => sceneColor(view, cameraOf(view)?.clear),
77
+ run: (ctx) => {
78
+ const gpu = ctx.gpu;
79
+ const state = ctx.world.resource(ForwardStateResource);
80
+ const cam = cameraOf(ctx.view);
81
+ const pv = state.views.get(ctx.view.name);
82
+ if (!pv) return;
83
+ const module = ctx.world.resource(Shaders).module(gpu, { root: "shard::pbr::deferred_lighting" });
84
+ if (!module) {
85
+ gpu.pipelines.skipped++;
86
+ return;
87
+ }
88
+ if (!layout || layoutGeneration !== gpu.generation) {
89
+ const F = GPUShaderStage.FRAGMENT;
90
+ layout = gpu.layouts.bindGroupLayout({
91
+ label: "deferred/gbuffer",
92
+ entries: [
93
+ { binding: 0, visibility: F, texture: { sampleType: "float" } },
94
+ { binding: 1, visibility: F, texture: { sampleType: "unfilterable-float" } },
95
+ { binding: 2, visibility: F, texture: { sampleType: "float" } },
96
+ { binding: 3, visibility: F, texture: { sampleType: "depth" } }
97
+ ]
98
+ });
99
+ layoutGeneration = gpu.generation;
100
+ bindGroups.clear();
101
+ }
102
+ const pipeline = gpu.pipelines.render({
103
+ label: "deferred/lighting",
104
+ layout: gpu.layouts.pipelineLayout({
105
+ label: "deferred/lighting",
106
+ bindGroupLayouts: [state.layouts.view, layout]
107
+ }),
108
+ vertex: { module, entryPoint: "vs" },
109
+ fragment: { module, entryPoint: "fs", targets: [{ format: "rgba16float" }] }
110
+ });
111
+ if (!pipeline) return;
112
+ const g0 = ctx.texture("gbuffer0");
113
+ const g1 = ctx.texture("gbuffer1");
114
+ const g2 = ctx.texture("gbuffer2");
115
+ const depth = ctx.texture("scene-depth");
116
+ const key = `${idOf(g0)}/${idOf(g1)}/${idOf(g2)}/${idOf(depth)}`;
117
+ let bg = bindGroups.get(ctx.view.name);
118
+ if (!bg || bg.key !== key) {
119
+ bg = {
120
+ key,
121
+ group: gpu.device.createBindGroup({
122
+ label: "deferred/gbuffer",
123
+ layout,
124
+ entries: [
125
+ { binding: 0, resource: g0.createView() },
126
+ { binding: 1, resource: g1.createView() },
127
+ { binding: 2, resource: g2.createView() },
128
+ { binding: 3, resource: depth.createView() }
129
+ ]
130
+ })
131
+ };
132
+ bindGroups.set(ctx.view.name, bg);
133
+ }
134
+ const pass = ctx.renderPass;
135
+ pass.setPipeline(pipeline);
136
+ pass.setBindGroup(0, viewBindGroup(gpu, ctx.world, pv, cam));
137
+ pass.setBindGroup(1, bg.group);
138
+ pass.draw(3);
139
+ }
140
+ };
141
+ }
142
+ const forwardOnlyNode = {
143
+ kind: "render",
144
+ phase: RenderPhase.Lighting + 10,
145
+ enabled: (view) => deferredView(view) && cameraOf(view).forwardOnly.length > 0,
146
+ reads: ["clusters", "shadow-cascades", "shadow-local", "environment", "ssao"],
147
+ writes: ["scene-color", "scene-depth", "hdr"],
148
+ color: (view) => sceneColor(view),
149
+ depth: { resource: "scene-depth" },
150
+ run: (ctx) => {
151
+ const state = ctx.world.resource(ForwardStateResource);
152
+ const cam = cameraOf(ctx.view);
153
+ const pv = state.views.get(ctx.view.name);
154
+ if (pv) drawMaterials(ctx, state, pv, cam, cam.forwardOnly, PASS_OPAQUE);
155
+ }
156
+ };
157
+ function gbufferDebugNode() {
158
+ const buffers = /* @__PURE__ */ new Map();
159
+ const channel = new Uint32Array(4);
160
+ return {
161
+ kind: "render",
162
+ phase: RenderPhase.Debug,
163
+ enabled: (view) => (cameraOf(view)?.gbufferDebug ?? -1) >= 0,
164
+ reads: ["gbuffer0", "gbuffer1", "gbuffer2"],
165
+ writes: [{ name: "gbuffer-debug", format: "rgba8unorm" }],
166
+ sideEffects: true,
167
+ color: [{ resource: "gbuffer-debug", clear: { r: 0, g: 0, b: 0, a: 1 } }],
168
+ run: (ctx) => {
169
+ const gpu = ctx.gpu;
170
+ const cam = cameraOf(ctx.view);
171
+ const module = ctx.world.resource(Shaders).module(gpu, { root: "shard::debug::gbuffer" });
172
+ if (!module) {
173
+ gpu.pipelines.skipped++;
174
+ return;
175
+ }
176
+ const F = GPUShaderStage.FRAGMENT;
177
+ const layout = gpu.layouts.bindGroupLayout({
178
+ label: "debug/gbuffer",
179
+ entries: [
180
+ { binding: 0, visibility: F, texture: { sampleType: "float" } },
181
+ { binding: 1, visibility: F, texture: { sampleType: "unfilterable-float" } },
182
+ { binding: 2, visibility: F, texture: { sampleType: "float" } },
183
+ { binding: 3, visibility: F, buffer: { type: "uniform" } }
184
+ ]
185
+ });
186
+ const pipeline = gpu.pipelines.render({
187
+ label: "debug/gbuffer",
188
+ layout: gpu.layouts.pipelineLayout({ label: "debug/gbuffer", bindGroupLayouts: [layout] }),
189
+ vertex: { module, entryPoint: "vs" },
190
+ fragment: { module, entryPoint: "fs", targets: [{ format: "rgba8unorm" }] }
191
+ });
192
+ if (!pipeline) return;
193
+ let buffer = buffers.get(ctx.view.name);
194
+ if (!buffer) {
195
+ buffer = new GpuBuffer(gpu, {
196
+ label: "debug/gbuffer",
197
+ usage: GPUBufferUsage.UNIFORM,
198
+ size: 16
199
+ });
200
+ buffers.set(ctx.view.name, buffer);
201
+ }
202
+ channel[0] = cam.gbufferDebug;
203
+ buffer.write(channel);
204
+ const pass = ctx.renderPass;
205
+ pass.setPipeline(pipeline);
206
+ pass.setBindGroup(
207
+ 0,
208
+ gpu.device.createBindGroup({
209
+ layout,
210
+ entries: [
211
+ { binding: 0, resource: ctx.texture("gbuffer0").createView() },
212
+ { binding: 1, resource: ctx.texture("gbuffer1").createView() },
213
+ { binding: 2, resource: ctx.texture("gbuffer2").createView() },
214
+ { binding: 3, resource: { buffer: buffer.buffer } }
215
+ ]
216
+ })
217
+ );
218
+ pass.draw(3);
219
+ }
220
+ };
221
+ }
222
+ const textureIds = /* @__PURE__ */ new WeakMap();
223
+ let nextTextureId = 1;
224
+ function idOf(t) {
225
+ let id = textureIds.get(t);
226
+ if (id === void 0) {
227
+ id = nextTextureId++;
228
+ textureIds.set(t, id);
229
+ }
230
+ return id;
231
+ }
232
+ async function captureGBuffer(world, camera, channel) {
233
+ const cam = world.resource(Cameras).get(camera);
234
+ if (!cam) {
235
+ throw new ShardError("render/unknown-camera", `Entity ${camera} is not a rendered camera`);
236
+ }
237
+ const index = GBUFFER_CHANNELS.indexOf(channel);
238
+ if (index < 0) {
239
+ throw new ShardError("render/unknown-buffer", `Unknown G-buffer channel "${channel}"`, {
240
+ hint: `Channels: ${GBUFFER_CHANNELS.join(", ")}, or depth.`
241
+ });
242
+ }
243
+ cam.gbufferDebug = index;
244
+ return await world.resource(Graph).capture(`camera:${camera}`, "gbuffer-debug");
245
+ }
246
+ function forwardReason(cam, world) {
247
+ const reasons = {};
248
+ const gpuCounts = cam.forwardOnly.cullView >= 0 ? world.resource(Culler).batchCounts.get(cam.forwardOnly) : void 0;
249
+ for (let d = 0; d < cam.forwardOnly.length; d++) {
250
+ const item = cam.forwardOnly.items[d];
251
+ const type = item.batch.material.type;
252
+ const why = type.standard ? "not deferrable" : `custom lighting (${type.name})`;
253
+ const n = gpuCounts ? gpuCounts[item.batch.index] ?? 0 : item.count;
254
+ if (n > 0) reasons[why] = (reasons[why] ?? 0) + n;
255
+ }
256
+ if (cam.transparent.visible > 0) reasons.transparent = cam.transparent.visible;
257
+ return reasons;
258
+ }
259
+ function describeDeferred(world) {
260
+ const gpu = world.tryResource(Gpu);
261
+ if (!gpu) return void 0;
262
+ const views = {};
263
+ const emissive = gbufferEmissiveFormat(gpu) === "rg11b10ufloat" ? 4 : 8;
264
+ for (const view of world.resource(Views).list) {
265
+ const cam = cameraOf(view);
266
+ if (!cam) continue;
267
+ const pixels = cam.width * cam.height;
268
+ views[view.name] = cam.deferred ? {
269
+ path: "deferred",
270
+ gbufferBytes: pixels * (4 + 8 + emissive + 4),
271
+ deferredMeshes: cam.draws.visible,
272
+ forwardMeshes: cam.forwardOnly.visible + cam.transparent.visible,
273
+ forwardReasons: forwardReason(cam, world)
274
+ } : { path: "forward", msaa: cam.msaa };
275
+ }
276
+ return { views };
277
+ }
278
+ function addDeferredNodes(app) {
279
+ const gpu = app.world.resource(Gpu);
280
+ const graph = app.world.resource(Graph);
281
+ graph.declare({ name: "gbuffer0", format: "rgba8unorm-srgb" });
282
+ graph.declare({ name: "gbuffer1", format: "rgba16float" });
283
+ graph.declare({ name: "gbuffer2", format: gbufferEmissiveFormat(gpu) });
284
+ graph.addNode("deferred-gbuffer", gbufferNode);
285
+ graph.addNode("gbuffer-fill", gbufferFillNode);
286
+ graph.addNode("deferred-lighting", lightingNode());
287
+ graph.addNode("deferred-forward", forwardOnlyNode);
288
+ graph.addNode("gbuffer-debug", gbufferDebugNode());
289
+ app.world.initResource(RenderDescribers).set("deferred", (world) => describeDeferred(world));
290
+ }
291
+ export {
292
+ GBUFFER_CHANNELS,
293
+ addDeferredNodes,
294
+ captureGBuffer,
295
+ describeDeferred
296
+ };
@@ -0,0 +1,80 @@
1
+ import { GpuBuffer, type GpuContext } from '@aethervtt/shard-gpu';
2
+ import type { Mesh } from '@aethervtt/shard-mesh';
3
+ /**
4
+ * Per-slot deform record, 12 words (mirrors `Deform` in `shard::mesh` and `shard::cull`):
5
+ *
6
+ * 0-3 world bounding sphere (skinned slots; radius 0 means "use the mesh bounds")
7
+ * 4 first joint matrix (vec4 index into poses; 3 rows per joint) 5 joint count
8
+ * 6 first word of the mesh's skin data (joints, weights) 7 first morph delta word
9
+ * 8 vertex count 9 active morph targets (≤ 8) 10 their (index, weight)
10
+ * pairs (vec4 index)
11
+ * 11 unused
12
+ */
13
+ export declare const DEFORM_WORDS = 12;
14
+ /** Words of skin data per vertex: joints packed as two u16 pairs, then four f32 weights. */
15
+ export declare const SKIN_WORDS = 6;
16
+ /** Floats per vertex per morph target: position, normal, and tangent deltas. */
17
+ export declare const MORPH_WORDS = 9;
18
+ /** Where a mesh's static deform data sits in the shared vertex-data buffer. */
19
+ export interface MeshDeform {
20
+ version: number;
21
+ /** First word of skin data, or -1 without joints and weights. */
22
+ skinBase: number;
23
+ /** First word of morph deltas, or -1 without targets. */
24
+ morphBase: number;
25
+ vertexCount: number;
26
+ targetCount: number;
27
+ /** Per joint: the farthest vertex it mainly moves, in joint space (bounds padding); by skin. */
28
+ radii: Float32Array | undefined;
29
+ radiiFor: object | undefined;
30
+ /** Frame last used, so meshes that went away are dropped when the buffer repacks. */
31
+ used: number;
32
+ }
33
+ /**
34
+ * GPU data for skinned and morphed instances: per-slot records, this frame's joint matrices and
35
+ * morph weights (`poses`), and every deforming mesh's joints, weights, and morph deltas, packed
36
+ * into one buffer the vertex stage indexes by vertex. Meshes stay in the instanced path; a slot
37
+ * with `InstanceFlags.Skinned` or `Morph` reads its record.
38
+ */
39
+ export declare class DeformStore {
40
+ records: Uint32Array<ArrayBuffer>;
41
+ recordF32: Float32Array<ArrayBuffer>;
42
+ /** vec4s: joint matrix rows and morph (index, weight) pairs, written every frame. */
43
+ poses: Float32Array<ArrayBuffer>;
44
+ poseCount: number;
45
+ private vertexData;
46
+ private vertexF32;
47
+ private vertexWords;
48
+ private wasted;
49
+ readonly meshes: Map<Mesh, MeshDeform>;
50
+ readonly recordBuffer: GpuBuffer;
51
+ readonly poseBuffer: GpuBuffer;
52
+ readonly vertexBuffer: GpuBuffer;
53
+ private recordLo;
54
+ private recordHi;
55
+ private vertexDirty;
56
+ private generation;
57
+ private frame;
58
+ private readonly gpu;
59
+ constructor(gpu: GpuContext);
60
+ /** Slots the record array holds. */
61
+ get capacity(): number;
62
+ ensureSlots(slots: number): void;
63
+ beginFrame(): void;
64
+ /** Reserves `n` vec4s of this frame's poses; returns the first index. */
65
+ allocPoses(n: number): number;
66
+ /** Marks a slot's record for upload (after writing `records` at slot * DEFORM_WORDS). */
67
+ touch(slot: number): void;
68
+ /** Clears a slot's record (no joints, no targets, no sphere). */
69
+ clear(slot: number): void;
70
+ /** Packs a mesh's joints, weights, and morph deltas (once per mesh version). */
71
+ meshData(mesh: Mesh): MeshDeform;
72
+ private wordsOf;
73
+ private reserve;
74
+ private pack;
75
+ private repack;
76
+ /** Uploads changed records, this frame's poses, and (when it changed) the vertex data. */
77
+ upload(): void;
78
+ /** Bytes of deform data on the GPU (for describe). */
79
+ get vertexBytes(): number;
80
+ }
package/dist/deform.js ADDED
@@ -0,0 +1,213 @@
1
+ import { GpuBuffer } from "@aethervtt/shard-gpu";
2
+ const DEFORM_WORDS = 12;
3
+ const DEFORM_BYTES = DEFORM_WORDS * 4;
4
+ const SKIN_WORDS = 6;
5
+ const MORPH_WORDS = 9;
6
+ class DeformStore {
7
+ records = new Uint32Array(DEFORM_WORDS * 256);
8
+ recordF32 = new Float32Array(this.records.buffer);
9
+ /** vec4s: joint matrix rows and morph (index, weight) pairs, written every frame. */
10
+ poses = new Float32Array(4 * 1024);
11
+ poseCount = 0;
12
+ vertexData = new Uint32Array(1024);
13
+ vertexF32 = new Float32Array(this.vertexData.buffer);
14
+ vertexWords = 0;
15
+ wasted = 0;
16
+ meshes = /* @__PURE__ */ new Map();
17
+ recordBuffer;
18
+ poseBuffer;
19
+ vertexBuffer;
20
+ recordLo = Number.POSITIVE_INFINITY;
21
+ recordHi = -1;
22
+ vertexDirty = true;
23
+ generation;
24
+ frame = 0;
25
+ gpu;
26
+ constructor(gpu) {
27
+ this.gpu = gpu;
28
+ this.generation = gpu.generation;
29
+ const usage = GPUBufferUsage.STORAGE;
30
+ this.recordBuffer = new GpuBuffer(gpu, {
31
+ label: "deform/records",
32
+ usage,
33
+ size: DEFORM_BYTES * 256
34
+ });
35
+ this.poseBuffer = new GpuBuffer(gpu, { label: "deform/poses", usage, size: 16 * 1024 });
36
+ this.vertexBuffer = new GpuBuffer(gpu, { label: "deform/vertices", usage, size: 4096 });
37
+ }
38
+ /** Slots the record array holds. */
39
+ get capacity() {
40
+ return this.records.length / DEFORM_WORDS;
41
+ }
42
+ ensureSlots(slots) {
43
+ if (slots <= this.capacity) return;
44
+ let n = this.capacity;
45
+ while (n < slots) n *= 2;
46
+ const grown = new Uint32Array(n * DEFORM_WORDS);
47
+ grown.set(this.records);
48
+ this.records = grown;
49
+ this.recordF32 = new Float32Array(grown.buffer);
50
+ this.recordLo = 0;
51
+ this.recordHi = this.capacity - 1;
52
+ }
53
+ beginFrame() {
54
+ this.poseCount = 0;
55
+ this.frame++;
56
+ }
57
+ /** Reserves `n` vec4s of this frame's poses; returns the first index. */
58
+ allocPoses(n) {
59
+ const base = this.poseCount;
60
+ if ((base + n) * 4 > this.poses.length) {
61
+ let size = this.poses.length * 2;
62
+ while (size < (base + n) * 4) size *= 2;
63
+ const grown = new Float32Array(size);
64
+ grown.set(this.poses.subarray(0, base * 4));
65
+ this.poses = grown;
66
+ }
67
+ this.poseCount = base + n;
68
+ return base;
69
+ }
70
+ /** Marks a slot's record for upload (after writing `records` at slot * DEFORM_WORDS). */
71
+ touch(slot) {
72
+ if (slot < this.recordLo) this.recordLo = slot;
73
+ if (slot > this.recordHi) this.recordHi = slot;
74
+ }
75
+ /** Clears a slot's record (no joints, no targets, no sphere). */
76
+ clear(slot) {
77
+ if (slot >= this.capacity) return;
78
+ this.records.fill(0, slot * DEFORM_WORDS, (slot + 1) * DEFORM_WORDS);
79
+ this.touch(slot);
80
+ }
81
+ /** Packs a mesh's joints, weights, and morph deltas (once per mesh version). */
82
+ meshData(mesh) {
83
+ let entry = this.meshes.get(mesh);
84
+ if (entry && entry.version === mesh.version) {
85
+ entry.used = this.frame;
86
+ return entry;
87
+ }
88
+ if (entry) this.wasted += this.wordsOf(entry);
89
+ const n = mesh.vertexCount;
90
+ entry = {
91
+ version: mesh.version,
92
+ skinBase: -1,
93
+ morphBase: -1,
94
+ vertexCount: n,
95
+ targetCount: mesh.targets?.length ?? 0,
96
+ radii: void 0,
97
+ radiiFor: void 0,
98
+ used: this.frame
99
+ };
100
+ this.meshes.set(mesh, entry);
101
+ if (this.wasted > 1 << 16 && this.wasted > this.vertexWords / 2) this.repack();
102
+ else this.pack(mesh, entry);
103
+ return entry;
104
+ }
105
+ wordsOf(entry) {
106
+ return (entry.skinBase >= 0 ? entry.vertexCount * SKIN_WORDS : 0) + entry.targetCount * entry.vertexCount * MORPH_WORDS;
107
+ }
108
+ reserve(words) {
109
+ const base = this.vertexWords;
110
+ if (base + words > this.vertexData.length) {
111
+ let size = this.vertexData.length * 2;
112
+ while (size < base + words) size *= 2;
113
+ const grown = new Uint32Array(size);
114
+ grown.set(this.vertexData.subarray(0, base));
115
+ this.vertexData = grown;
116
+ this.vertexF32 = new Float32Array(grown.buffer);
117
+ }
118
+ this.vertexWords = base + words;
119
+ this.vertexDirty = true;
120
+ return base;
121
+ }
122
+ pack(mesh, entry) {
123
+ const n = mesh.vertexCount;
124
+ if (mesh.joints && mesh.weights) {
125
+ const base = this.reserve(n * SKIN_WORDS);
126
+ entry.skinBase = base;
127
+ const j = mesh.joints;
128
+ const w = mesh.weights;
129
+ const u = this.vertexData;
130
+ const f = this.vertexF32;
131
+ for (let v = 0; v < n; v++) {
132
+ const o = base + v * SKIN_WORDS;
133
+ u[o] = (j[v * 4] | j[v * 4 + 1] << 16) >>> 0;
134
+ u[o + 1] = (j[v * 4 + 2] | j[v * 4 + 3] << 16) >>> 0;
135
+ const sum = w[v * 4] + w[v * 4 + 1] + w[v * 4 + 2] + w[v * 4 + 3];
136
+ const k = sum > 1e-6 ? 1 / sum : 0;
137
+ for (let c = 0; c < 4; c++) f[o + 2 + c] = w[v * 4 + c] * k;
138
+ if (sum <= 1e-6) f[o + 2] = 1;
139
+ }
140
+ }
141
+ const targets = mesh.targets;
142
+ if (targets?.length) {
143
+ const base = this.reserve(targets.length * n * MORPH_WORDS);
144
+ entry.morphBase = base;
145
+ const f = this.vertexF32;
146
+ for (let t = 0; t < targets.length; t++) {
147
+ const target = targets[t];
148
+ for (let v = 0; v < n; v++) {
149
+ const o = base + (t * n + v) * MORPH_WORDS;
150
+ for (let c = 0; c < 3; c++) {
151
+ f[o + c] = target.positions[v * 3 + c];
152
+ f[o + 3 + c] = target.normals ? target.normals[v * 3 + c] : 0;
153
+ f[o + 6 + c] = target.tangents ? target.tangents[v * 3 + c] : 0;
154
+ }
155
+ }
156
+ }
157
+ }
158
+ }
159
+ repack() {
160
+ this.vertexWords = 0;
161
+ this.wasted = 0;
162
+ for (const [mesh, entry] of this.meshes) {
163
+ if (entry.used < this.frame - 600 && mesh.version === entry.version) {
164
+ this.meshes.delete(mesh);
165
+ continue;
166
+ }
167
+ entry.version = mesh.version;
168
+ entry.vertexCount = mesh.vertexCount;
169
+ entry.targetCount = mesh.targets?.length ?? 0;
170
+ entry.skinBase = -1;
171
+ entry.morphBase = -1;
172
+ entry.radii = void 0;
173
+ entry.radiiFor = void 0;
174
+ this.pack(mesh, entry);
175
+ }
176
+ this.vertexDirty = true;
177
+ }
178
+ /** Uploads changed records, this frame's poses, and (when it changed) the vertex data. */
179
+ upload() {
180
+ if (this.generation !== this.gpu.generation) {
181
+ this.generation = this.gpu.generation;
182
+ this.vertexDirty = true;
183
+ this.recordLo = 0;
184
+ this.recordHi = this.capacity - 1;
185
+ }
186
+ if (this.recordBuffer.ensureCapacity(this.records.byteLength)) {
187
+ this.recordLo = 0;
188
+ this.recordHi = this.capacity - 1;
189
+ }
190
+ if (this.recordHi >= this.recordLo) {
191
+ const lo = this.recordLo * DEFORM_WORDS;
192
+ const n = (this.recordHi - this.recordLo + 1) * DEFORM_WORDS;
193
+ this.recordBuffer.write(this.records, lo * 4, lo, n);
194
+ }
195
+ this.recordLo = Number.POSITIVE_INFINITY;
196
+ this.recordHi = -1;
197
+ if (this.poseCount > 0) this.poseBuffer.write(this.poses, 0, 0, this.poseCount * 4);
198
+ if (this.vertexDirty) {
199
+ this.vertexDirty = false;
200
+ if (this.vertexWords > 0) this.vertexBuffer.write(this.vertexData, 0, 0, this.vertexWords);
201
+ }
202
+ }
203
+ /** Bytes of deform data on the GPU (for describe). */
204
+ get vertexBytes() {
205
+ return this.vertexWords * 4;
206
+ }
207
+ }
208
+ export {
209
+ DEFORM_WORDS,
210
+ DeformStore,
211
+ MORPH_WORDS,
212
+ SKIN_WORDS
213
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * The lighting, culling, and renderScale sections of `render.describe`, for agents and the editor.
3
+ * Only introspection: an app that ships without it renders the same.
4
+ */
5
+ export declare const renderDescribePlugin: import("@aethervtt/shard-runtime").Plugin;
@@ -0,0 +1,19 @@
1
+ import { definePlugin } from "@aethervtt/shard-runtime";
2
+ import { describeCulling, describeLighting } from "./debug-views.js";
3
+ import { RenderDescribers } from "./plugin.js";
4
+ import { describeRenderScale } from "./render-scale.js";
5
+ const renderDescribePlugin = definePlugin({
6
+ name: "render/describe",
7
+ dependencies: ["render/forward"],
8
+ build() {
9
+ },
10
+ ready(app) {
11
+ const describers = app.world.initResource(RenderDescribers);
12
+ describers.set("lighting", (world) => describeLighting(world));
13
+ describers.set("culling", (world) => describeCulling(world));
14
+ describers.set("renderScale", (world) => describeRenderScale(world));
15
+ }
16
+ });
17
+ export {
18
+ renderDescribePlugin
19
+ };
@@ -0,0 +1,5 @@
1
+ import type { World } from '@aethervtt/shard-core';
2
+ /** White balance as LMS channel gains (Unity's method, from temperature and tint in −1..1). */
3
+ export declare function whiteBalance(temperature: number, tint: number, out: Float32Array): Float32Array;
4
+ /** Adds the display stage (tonemap, upscale) to the graph. */
5
+ export declare function addDisplayNodes(world: World): void;