@aethervtt/shard-render 0.0.1 → 0.1.0

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 (134) hide show
  1. package/dist/assets.d.ts +3 -0
  2. package/dist/assets.js +43 -2
  3. package/dist/atmosphere-nodes.d.ts +9 -0
  4. package/dist/atmosphere-nodes.js +57 -18
  5. package/dist/atmosphere-plugin.js +8 -2
  6. package/dist/baseline/atmosphere.d.ts +16 -0
  7. package/dist/baseline/atmosphere.js +301 -0
  8. package/dist/baseline/data-texture.d.ts +36 -0
  9. package/dist/baseline/data-texture.js +181 -0
  10. package/dist/baseline/environment.d.ts +14 -0
  11. package/dist/baseline/environment.js +368 -0
  12. package/dist/baseline/exposure.d.ts +25 -0
  13. package/dist/baseline/exposure.js +120 -0
  14. package/dist/baseline/lights.d.ts +33 -0
  15. package/dist/baseline/lights.js +133 -0
  16. package/dist/baseline/textures.d.ts +97 -0
  17. package/dist/baseline/textures.js +203 -0
  18. package/dist/camera.d.ts +2 -0
  19. package/dist/camera.js +4 -0
  20. package/dist/culling-shaders.js +21 -2
  21. package/dist/culling.d.ts +11 -2
  22. package/dist/culling.js +44 -18
  23. package/dist/cutaway-plugin.d.ts +48 -0
  24. package/dist/cutaway-plugin.js +364 -0
  25. package/dist/cutaway.d.ts +27 -0
  26. package/dist/cutaway.js +49 -0
  27. package/dist/data-store.d.ts +69 -0
  28. package/dist/data-store.js +105 -0
  29. package/dist/debug-shaders.js +17 -3
  30. package/dist/debug-views.js +4 -2
  31. package/dist/deferred-shaders.js +2 -0
  32. package/dist/deferred.js +24 -3
  33. package/dist/deform.d.ts +5 -4
  34. package/dist/deform.js +4 -9
  35. package/dist/describe-plugin.d.ts +4 -2
  36. package/dist/describe-plugin.js +75 -3
  37. package/dist/display-nodes.js +15 -4
  38. package/dist/dynamic-resolution.d.ts +2 -0
  39. package/dist/dynamic-resolution.js +6 -1
  40. package/dist/environment-plugin.js +17 -2
  41. package/dist/environment-state.d.ts +7 -3
  42. package/dist/environment-state.js +20 -9
  43. package/dist/environment.js +7 -1
  44. package/dist/features.d.ts +49 -0
  45. package/dist/features.js +106 -0
  46. package/dist/forward.d.ts +38 -2
  47. package/dist/forward.js +582 -184
  48. package/dist/fxaa.js +12 -2
  49. package/dist/gizmos-plugin.js +7 -0
  50. package/dist/gizmos.d.ts +3 -3
  51. package/dist/gizmos.js +11 -13
  52. package/dist/gpu-assets.d.ts +58 -4
  53. package/dist/gpu-assets.js +191 -8
  54. package/dist/graph.d.ts +11 -0
  55. package/dist/graph.js +30 -5
  56. package/dist/health.d.ts +58 -0
  57. package/dist/health.js +112 -0
  58. package/dist/index.d.ts +30 -11
  59. package/dist/index.js +286 -9
  60. package/dist/instances.d.ts +119 -8
  61. package/dist/instances.js +387 -78
  62. package/dist/interior-plugin.d.ts +121 -0
  63. package/dist/interior-plugin.js +465 -0
  64. package/dist/interior.d.ts +24 -0
  65. package/dist/interior.js +13 -0
  66. package/dist/layers.d.ts +35 -0
  67. package/dist/layers.js +49 -0
  68. package/dist/lens-plugin.d.ts +43 -0
  69. package/dist/lens-plugin.js +304 -0
  70. package/dist/lens.d.ts +41 -0
  71. package/dist/lens.js +99 -0
  72. package/dist/lights.d.ts +50 -5
  73. package/dist/lights.js +101 -22
  74. package/dist/material-pipelines.d.ts +37 -4
  75. package/dist/material-pipelines.js +102 -5
  76. package/dist/materials.d.ts +23 -0
  77. package/dist/materials.js +44 -1
  78. package/dist/outline-plugin.d.ts +48 -0
  79. package/dist/outline-plugin.js +529 -0
  80. package/dist/outline.d.ts +15 -0
  81. package/dist/outline.js +42 -0
  82. package/dist/overlays.js +1 -0
  83. package/dist/picking.d.ts +17 -0
  84. package/dist/picking.js +40 -11
  85. package/dist/pixel-perfect-plugin.js +10 -2
  86. package/dist/plugin.d.ts +58 -6
  87. package/dist/plugin.js +191 -24
  88. package/dist/post-common.d.ts +2 -0
  89. package/dist/post-common.js +3 -0
  90. package/dist/post-nodes.d.ts +2 -1
  91. package/dist/post-nodes.js +111 -19
  92. package/dist/post-plugin.js +5 -3
  93. package/dist/post-shaders.js +51 -6
  94. package/dist/projection.d.ts +24 -0
  95. package/dist/projection.js +87 -0
  96. package/dist/readback.d.ts +1 -0
  97. package/dist/readback.js +1 -0
  98. package/dist/render-scale.js +1 -0
  99. package/dist/screen-effects.d.ts +97 -0
  100. package/dist/screen-effects.js +192 -0
  101. package/dist/shaders.d.ts +13 -0
  102. package/dist/shaders.js +131 -29
  103. package/dist/shadow-catcher.d.ts +9 -0
  104. package/dist/shadow-catcher.js +133 -0
  105. package/dist/shadows.d.ts +25 -1
  106. package/dist/shadows.js +97 -19
  107. package/dist/standard-fields.d.ts +1 -0
  108. package/dist/standard-fields.js +5 -0
  109. package/dist/standard.js +12 -0
  110. package/dist/stats.d.ts +45 -1
  111. package/dist/stats.js +91 -3
  112. package/dist/surface-model.d.ts +57 -0
  113. package/dist/surface-model.js +289 -0
  114. package/dist/surface-shaders.d.ts +3 -0
  115. package/dist/surface-shaders.js +160 -0
  116. package/dist/surface.d.ts +40 -0
  117. package/dist/surface.js +86 -0
  118. package/dist/target.d.ts +1 -13
  119. package/dist/target.js +1 -26
  120. package/dist/testing.d.ts +10 -0
  121. package/dist/testing.js +33 -1
  122. package/dist/tier.d.ts +13 -0
  123. package/dist/tier.js +14 -0
  124. package/dist/timer.d.ts +2 -0
  125. package/dist/timer.js +7 -1
  126. package/dist/variants.d.ts +37 -0
  127. package/dist/variants.js +104 -0
  128. package/dist/view-visibility-plugin.d.ts +61 -0
  129. package/dist/view-visibility-plugin.js +267 -0
  130. package/dist/view-visibility.d.ts +28 -0
  131. package/dist/view-visibility.js +46 -0
  132. package/dist/view.d.ts +29 -1
  133. package/dist/view.js +57 -34
  134. package/package.json +15 -11
package/dist/forward.js CHANGED
@@ -1,4 +1,11 @@
1
- import { defineResource, defineSystem, Last, PostUpdate } from "@aethervtt/shard-core";
1
+ import { assetServer } from "@aethervtt/shard-assets";
2
+ import {
3
+ defineResource,
4
+ defineSystem,
5
+ Last,
6
+ PostUpdate,
7
+ PreUpdate
8
+ } from "@aethervtt/shard-core";
2
9
  import { GpuBuffer } from "@aethervtt/shard-gpu";
3
10
  import { definePlugin, LogResource, Time } from "@aethervtt/shard-runtime";
4
11
  import { setTextureCapabilities, Textures } from "@aethervtt/shard-texture";
@@ -23,11 +30,15 @@ import {
23
30
  MAX_LIGHTS_PER_CLUSTER,
24
31
  ViewLightList
25
32
  } from "./clusters.js";
26
- import { Culler, cullTransparent, GpuCuller } from "./culling.js";
33
+ import { Culler, cullGround, cullTransparent, GpuCuller } from "./culling.js";
34
+ import { Cutaway, CutawayPath, CutawayView } from "./cutaway.js";
35
+ import { DataStore, dataEntry } from "./data-store.js";
27
36
  import { addDisplayNodes } from "./display-nodes.js";
28
37
  import { Environments, environmentParams } from "./environment-state.js";
38
+ import { addRenderFeatures } from "./features.js";
29
39
  import { GpuAssets, GpuAssetsResource } from "./gpu-assets.js";
30
40
  import { RenderPhase } from "./graph.js";
41
+ import { clearHealthIssue, MaterialFallbacks, raiseHealthIssue } from "./health.js";
31
42
  import {
32
43
  DeformPath,
33
44
  InstanceData,
@@ -44,10 +55,14 @@ import {
44
55
  NotShadowReceiver,
45
56
  observeInstanceRemovals,
46
57
  prepareInstances,
58
+ ShadowWhenHidden,
47
59
  SkinnedMesh,
48
60
  VisibilityRange
49
61
  } from "./instances.js";
62
+ import { INTERIOR_BINDINGS, InteriorPath } from "./interior.js";
50
63
  import { observeOriginShifts } from "./large-world.js";
64
+ import { GroundLayer, RenderLayers } from "./layers.js";
65
+ import { expireLensFields, Lens, LensFields, LensPath } from "./lens.js";
51
66
  import {
52
67
  AmbientLight,
53
68
  DirectionalLight,
@@ -69,8 +84,9 @@ import {
69
84
  variantCull
70
85
  } from "./material-pipelines.js";
71
86
  import { isTransparent } from "./materials.js";
87
+ import { Outline, OutlinePath, observeOutlinesWithoutPass } from "./outline.js";
72
88
  import { PixelPerfect, PixelPerfectPath, PixelTargets } from "./pixel-perfect.js";
73
- import { Gpu, Graph, RenderSet, Shaders, Views } from "./plugin.js";
89
+ import { Gpu, Graph, RenderDescribers, RenderSet, Shaders, Views } from "./plugin.js";
74
90
  import {
75
91
  Antialiasing,
76
92
  AutoExposure,
@@ -84,6 +100,13 @@ import {
84
100
  Vignette
85
101
  } from "./post.js";
86
102
  import { RenderScale } from "./render-scale.js";
103
+ import {
104
+ describeScreenEffects,
105
+ expireScreenEffects,
106
+ runScreenEffects,
107
+ ScreenEffectHandlers,
108
+ ScreenEffects
109
+ } from "./screen-effects.js";
87
110
  import { ViewUniform, viewLayout } from "./shaders.js";
88
111
  import {
89
112
  assignLocalShadows,
@@ -92,15 +115,20 @@ import {
92
115
  fitCascades,
93
116
  fitLocalShadows,
94
117
  LocalShadows,
118
+ markShadowDrawn,
95
119
  packShadowData,
96
120
  SHADOW_DATA_FLOATS,
97
121
  ShadowPassUniforms,
98
122
  ShadowsResource,
123
+ shadowRedraw,
99
124
  shadowViewLayout
100
125
  } from "./shadows.js";
101
126
  import { SkinAssetType, Skins } from "./skin-asset.js";
102
127
  import { GpuMemory, RenderCounters, RenderStats } from "./stats.js";
128
+ import { bindingDimension } from "./tier.js";
129
+ import { addShaderVariantSource, materialVariants, ShaderVariantSources } from "./variants.js";
103
130
  import {
131
+ CameraMoved,
104
132
  Cameras,
105
133
  cameraOf,
106
134
  DeferredPath,
@@ -109,6 +137,12 @@ import {
109
137
  Tonemapping,
110
138
  ViewSettings
111
139
  } from "./view.js";
140
+ import {
141
+ HiddenSetsResource,
142
+ NO_HIDDEN,
143
+ observeViewVisibilityWithoutPlugin,
144
+ ViewVisibility
145
+ } from "./view-visibility.js";
112
146
  import { ComputedVisibility, computeVisibility, Visibility } from "./visibility.js";
113
147
  const ForwardStateResource = defineResource("render/ForwardState");
114
148
  const State = ForwardStateResource;
@@ -161,7 +195,8 @@ const cameraCull = {
161
195
  lodScale: 1,
162
196
  orthographic: false,
163
197
  lodState: void 0,
164
- updateLod: true
198
+ updateLod: true,
199
+ layers: 4294967295
165
200
  };
166
201
  const shadowCull = {
167
202
  planes: null,
@@ -170,19 +205,23 @@ const shadowCull = {
170
205
  lodScale: 1,
171
206
  orthographic: false,
172
207
  lodState: void 0,
173
- updateLod: false
208
+ updateLod: false,
209
+ layers: 4294967295
174
210
  };
175
211
  function cameraParams(cam, out) {
176
212
  out.planes = cam.frozenFrustum ?? cam.frustum;
177
213
  out.eye = cam.frozenPosition ?? cam.position;
178
214
  out.orthographic = cam.orthographic;
215
+ out.layers = cam.layers;
216
+ out.hidden = cam.hidden?.bits;
217
+ out.hiddenBase = cam.hidden ? cam.hidden.base : NO_HIDDEN;
179
218
  out.lodScale = cam.orthographic ? 2 / cam.orthoHeight : 1 / Math.tan(cam.fovY / 2);
180
219
  if (cam.lodState.length < cam.lodCapacity)
181
220
  cam.lodState = new Uint8Array(cam.lodCapacity).fill(LOD_UNSET);
182
221
  out.lodState = cam.lodState;
183
222
  return out;
184
223
  }
185
- const queue = defineSystem({
224
+ const forwardQueue = defineSystem({
186
225
  name: "render/forward-queue",
187
226
  description: "Culls instances per camera and shadow view (on the GPU when it can), clusters lights, and writes view uniforms.",
188
227
  run: (_, world) => {
@@ -197,7 +236,8 @@ const queue = defineSystem({
197
236
  stats.clear();
198
237
  state.frame++;
199
238
  ensureLayouts(gpu, state, world.resource(GpuAssetsResource), store);
200
- state.pipelines.beginFrame(state.frame);
239
+ chooseInterior(gpu, world, state);
240
+ state.pipelines.beginFrame(state.frame, world.resource(Shaders).revision);
201
241
  shadows.uniforms.reset();
202
242
  culler.beginFrame();
203
243
  const gpuCull = culler.active;
@@ -219,8 +259,19 @@ const queue = defineSystem({
219
259
  if (gpuCull) {
220
260
  culler.add(store, cam.draws, params, culler.lodCamera(cam.entity), forwardOnly);
221
261
  cullTransparent(store, cam.transparent, params, cam.forward);
262
+ cullGround(store, cam.ground, params, cam.overlay);
222
263
  } else {
223
- store.cullCpu(cam.draws, params, void 0, cam.transparent, cam.forward, forwardOnly);
264
+ store.cullCpu(
265
+ cam.draws,
266
+ params,
267
+ void 0,
268
+ cam.transparent,
269
+ cam.forward,
270
+ forwardOnly,
271
+ false,
272
+ cam.ground,
273
+ cam.overlay
274
+ );
224
275
  }
225
276
  store.orderNearFirst(cam.draws, cam.position);
226
277
  if (view.order < mainOrder) {
@@ -228,14 +279,18 @@ const queue = defineSystem({
228
279
  mainOrder = view.order;
229
280
  }
230
281
  }
231
- const shadowParams = (cam, planes) => {
282
+ const shadowParams = (cam, planes, own) => {
232
283
  cameraParams(cam, shadowCull);
233
284
  shadowCull.planes = planes;
234
285
  shadowCull.updateLod = false;
286
+ if (!own || !cam.hidden?.shadows) {
287
+ shadowCull.hidden = void 0;
288
+ shadowCull.hiddenBase = NO_HIDDEN;
289
+ }
235
290
  return shadowCull;
236
291
  };
237
- const cullShadow = (cam, draws, planes, box) => {
238
- const params = shadowParams(cam, planes);
292
+ const cullShadow = (cam, draws, planes, box, own = false) => {
293
+ const params = shadowParams(cam, planes, own);
239
294
  if (gpuCull) {
240
295
  culler.add(store, draws, params, culler.lodCamera(cam.entity));
241
296
  if (box) store.casterBounds(box);
@@ -246,16 +301,19 @@ const queue = defineSystem({
246
301
  };
247
302
  const local = shadows.local;
248
303
  fitLocalShadows(local, settings.shadowMapSize);
304
+ store.checkMeshVersions();
249
305
  if (main) {
250
306
  for (let i = 0; i < local.spots.length; i++) {
251
307
  const view = local.spotViews[i];
252
308
  cullShadow(main, view.draws, view.frustum);
253
309
  view.offset = shadows.uniforms.push(view.viewProj);
310
+ view.redraw = shadowRedraw(view, local.spots[i]?.cachedShadow ?? false, store);
254
311
  }
255
312
  for (let i = 0; i < local.points.length * 6; i++) {
256
313
  const view = local.pointViews[i];
257
314
  cullShadow(main, view.draws, view.frustum);
258
315
  view.offset = shadows.uniforms.push(view.viewProj);
316
+ view.redraw = shadowRedraw(view, local.points[i / 6 | 0]?.cachedShadow ?? false, store);
259
317
  }
260
318
  }
261
319
  for (const view of world.resource(Views).list) {
@@ -264,7 +322,20 @@ const queue = defineSystem({
264
322
  const pv = viewGpu(gpu, state, view.name);
265
323
  pv.ao = void 0;
266
324
  pv.lightList.build(cam, lights.records, lights.high);
267
- pv.clusters.upload(pv.lightList, cam, settings.clusterFar);
325
+ if (pv.clusters) pv.clusters.upload(pv.lightList, cam, settings.clusterFar);
326
+ else {
327
+ const b = pv.baseline;
328
+ state.baseline.binBaselineLights(
329
+ b,
330
+ pv.lightList,
331
+ lights.data,
332
+ cam,
333
+ settings.clusterFar,
334
+ settings.baselineMaxLights
335
+ );
336
+ if (b.dropped !== b.reported)
337
+ reportLightBudget(world, view.name, b, settings.baselineMaxLights);
338
+ }
268
339
  const sun = lights.shadowSun;
269
340
  if (sun) {
270
341
  fitCascades(
@@ -272,11 +343,12 @@ const queue = defineSystem({
272
343
  cam,
273
344
  sun,
274
345
  settings.cascadeMapSize,
275
- (v, planes, box) => cullShadow(cam, v.draws, planes, box)
346
+ (v, planes, box) => cullShadow(cam, v.draws, planes, box, true)
276
347
  );
277
348
  for (let i = 0; i < pv.cascades.count; i++) {
278
349
  const v = pv.cascades.views[i];
279
350
  v.offset = shadows.uniforms.push(v.viewProj);
351
+ v.redraw = shadowRedraw(v, sun.cached, store);
280
352
  }
281
353
  } else {
282
354
  pv.cascades.count = 0;
@@ -302,8 +374,8 @@ const queue = defineSystem({
302
374
  st.visible += cam.forwardOnly.visible;
303
375
  st.drawCalls += cam.forwardOnly.cullView >= 0 ? culler.drawCounts.get(cam.forwardOnly) ?? 0 : cam.forwardOnly.length;
304
376
  }
305
- st.visible += cam.transparent.visible;
306
- st.drawCalls += cam.transparent.length;
377
+ st.visible += cam.transparent.visible + cam.ground.visible;
378
+ st.drawCalls += cam.transparent.length + cam.ground.length;
307
379
  stats.set(view.name, st);
308
380
  }
309
381
  }
@@ -318,10 +390,11 @@ function viewGpu(gpu, state, name) {
318
390
  size: viewLayout.size
319
391
  }),
320
392
  lightList: new ViewLightList(),
321
- clusters: new ClusterBuffers(gpu, name),
322
- shadowData: new GpuBuffer(gpu, {
393
+ clusters: state.baseline ? void 0 : new ClusterBuffers(gpu, name),
394
+ baseline: state.baseline?.baselineViewLights(gpu, name),
395
+ shadowData: new DataStore(gpu, {
323
396
  label: `${name}/shadows`,
324
- usage: GPUBufferUsage.STORAGE,
397
+ kind: "uniform",
325
398
  size: SHADOW_DATA_FLOATS * 4
326
399
  }),
327
400
  cascades: new Cascades(),
@@ -351,6 +424,14 @@ const ambientScratch = new Float32Array(3);
351
424
  const envScratch = new Float32Array(4);
352
425
  const jitterScratch = new Float32Array(4);
353
426
  const noTransmittance = new Float32Array(16).fill(1);
427
+ const pixelScaleScratch = new Float32Array(4);
428
+ function viewPixelScale(cam, out) {
429
+ out[0] = cam.pixelRatio * cam.width / Math.max(1, cam.displayWidth);
430
+ out[1] = cam.orthographic ? cam.orthoHeight / Math.max(1, cam.height) : 2 * Math.tan(cam.fovY / 2) / Math.max(1, cam.height);
431
+ out[2] = cam.orthographic ? 1 : 0;
432
+ out[3] = 0;
433
+ return out;
434
+ }
354
435
  function writeViewUniform(world, state, pv, cam, ambient, clusterFar) {
355
436
  environmentParams(world.resource(Environments), cam, envScratch);
356
437
  const [near, far] = clusterRange(cam, clusterFar);
@@ -381,7 +462,8 @@ function writeViewUniform(world, state, pv, cam, ambient, clusterFar) {
381
462
  viewProjNoJitter: cam.viewProjNoJitter,
382
463
  prevViewProj: cam.prevViewProj,
383
464
  jitter: jitterScratch,
384
- sunTransmittance: world.tryResource(Atmospheres)?.cameras.get(cam.entity)?.sunTransmittance ?? noTransmittance
465
+ sunTransmittance: world.tryResource(Atmospheres)?.cameras.get(cam.entity)?.sunTransmittance ?? noTransmittance,
466
+ pixelScale: viewPixelScale(cam, pixelScaleScratch)
385
467
  });
386
468
  pv.uniform.write(new Float32Array(state.viewBytes.buffer, 0, viewLayout.size / 4));
387
469
  }
@@ -392,7 +474,7 @@ const upload = defineSystem({
392
474
  const store = world.resource(Instances);
393
475
  const culler = world.resource(Culler);
394
476
  if (culler.active) {
395
- culler.prepare(store);
477
+ culler.prepare(store, world.tryResource(HiddenSetsResource));
396
478
  store.gpuVisible = culler.visible;
397
479
  }
398
480
  store.finishFrame();
@@ -408,15 +490,33 @@ const VERTEX_BUFFERS = [
408
490
  { arrayStride: 16, attributes: [{ shaderLocation: 4, offset: 0, format: "float32x4" }] }
409
491
  ];
410
492
  const FORWARD_DEFINES = [
411
- { PREMULTIPLY: false, MASK: false },
412
- { PREMULTIPLY: true, MASK: false },
413
- { PREMULTIPLY: false, MASK: true },
414
- { PREMULTIPLY: true, MASK: true }
493
+ { PREMULTIPLY: false, MASK: false, OPAQUE: false },
494
+ { PREMULTIPLY: true, MASK: false, OPAQUE: false },
495
+ { PREMULTIPLY: false, MASK: true, OPAQUE: false },
496
+ { PREMULTIPLY: true, MASK: true, OPAQUE: false },
497
+ { PREMULTIPLY: false, MASK: false, OPAQUE: true },
498
+ { PREMULTIPLY: true, MASK: false, OPAQUE: true },
499
+ { PREMULTIPLY: false, MASK: true, OPAQUE: true },
500
+ { PREMULTIPLY: true, MASK: true, OPAQUE: true }
415
501
  ];
416
- function materialLayout(gpu, state, assets, type, store) {
502
+ const CUT_KEY = 2 ** 40;
503
+ const INTERIOR_KEY = 2 ** 41;
504
+ const CUTAWAY_DEFINES = FORWARD_DEFINES.map((d) => ({ ...d, CUTAWAY: true }));
505
+ const INTERIOR_DEFINES = [
506
+ void 0,
507
+ { SKY_VISIBILITY: true },
508
+ { BLOCKED_LIGHTS: true },
509
+ { SKY_VISIBILITY: true, BLOCKED_LIGHTS: true }
510
+ ];
511
+ const LIT_DEFINES = INTERIOR_DEFINES.map(
512
+ (extra) => [FORWARD_DEFINES, CUTAWAY_DEFINES].map(
513
+ (set) => extra ? set.map((d) => ({ ...d, ...extra })) : set
514
+ )
515
+ );
516
+ function materialLayout(gpu, state, assets, type, store, cut) {
417
517
  return gpu.layouts.pipelineLayout({
418
- label: `forward/${type.name}`,
419
- bindGroupLayouts: [state.layouts.view, assets.layoutOf(type), store.layout]
518
+ label: cut ? `forward/${type.name}/cutaway` : `forward/${type.name}`,
519
+ bindGroupLayouts: cut ? [state.layouts.view, assets.layoutOf(type), store.layout, cut] : [state.layouts.view, assets.layoutOf(type), store.layout]
420
520
  });
421
521
  }
422
522
  const PASS_OPAQUE = 0;
@@ -424,6 +524,11 @@ const PASS_TRANSPARENT = 1;
424
524
  const PASS_GBUFFER = 3;
425
525
  const PASS_PREPASS = 5;
426
526
  const PASS_PICK = 7;
527
+ const PASS_GROUND = 9;
528
+ const PASS_PICK_GROUND = 11;
529
+ const PASS_DEPTH = 13;
530
+ const GROUND_DEPTH_BIAS = 8;
531
+ const GROUND_SLOPE_BIAS = 2;
427
532
  const PICK_TARGETS = [
428
533
  { format: "r32uint" },
429
534
  { format: "rgba32float" }
@@ -439,6 +544,9 @@ function drawMaterials(ctx, state, pv, cam, draws, pass) {
439
544
  const args = ctx.world.resource(Culler).args.buffer;
440
545
  renderPass.setBindGroup(0, viewBindGroup(gpu, ctx.world, pv, cam));
441
546
  renderPass.setBindGroup(2, instances);
547
+ const cutaway = ctx.world.tryResource(CutawayPath);
548
+ const cutGroup = cutaway?.bindGroup(gpu, cam.entity);
549
+ if (cutGroup) renderPass.setBindGroup(3, cutGroup);
442
550
  let current;
443
551
  let boundMaterial;
444
552
  let boundPositions;
@@ -447,52 +555,15 @@ function drawMaterials(ctx, state, pv, cam, draws, pass) {
447
555
  let switches = 0;
448
556
  for (let d = 0; d < draws.length; d++) {
449
557
  const item = draws.items[d];
450
- const material = item.batch.material;
451
- const type = material.type;
452
- const variant = materialVariant(material);
453
- const blend = variantBlend(variant);
454
- const gbuffer = pass === PASS_GBUFFER;
455
- const prepass = pass === PASS_PREPASS;
456
- const pick = pass === PASS_PICK;
457
- if (gbuffer && !type.standard) continue;
458
- const msaa = gbuffer || prepass || pick ? 1 : cam.msaa;
459
- const key = ((pass * 1024 + typeOrdinal(type)) * 16 + variant) * 8 + msaa;
460
- let pipeline = state.pipelines.cached(key);
558
+ const own = item.batch.material;
559
+ if (!inPass(own.type, pass)) continue;
560
+ const cut = cutGroup !== void 0 && item.batch.cutaway > 0 && cutaway.cuts(cam.entity, item.batch.index) ? cutaway : void 0;
561
+ const fallback = state.pipelines.failing(own.type);
562
+ const material = fallback ? state.pipelines.proxy(own) : own;
563
+ const pipeline = materialPipeline(ctx, state, cam, store, assets, material, pass, true, cut);
461
564
  if (!pipeline) {
462
- const premultiply = blend === "premultiplied";
463
- const mask = blend === "mask";
464
- const slot = pick ? 40 : prepass ? 32 + (type.standard ? 0 : 4) + (mask ? 2 : 0) : (gbuffer ? 16 : type.standard ? 0 : 4) + (premultiply ? 1 : 0) + (mask ? 2 : 0);
465
- const module = state.pipelines.module(
466
- ctx.world,
467
- gpu,
468
- type,
469
- slot,
470
- pick ? "shard::pick" : prepass ? type.standard ? "shard::prepass" : "shard::prepass::plain" : gbuffer ? "shard::pbr::gbuffer_pass" : type.standard ? "shard::pbr::forward" : "shard::unlit::forward",
471
- FORWARD_DEFINES[slot & 3]
472
- );
473
- if (!module) {
474
- gpu.pipelines.skipped++;
475
- continue;
476
- }
477
- const transparent = isTransparent(blend) && !pick;
478
- pipeline = state.pipelines.create(gpu, key, {
479
- label: `${pick ? "pick" : prepass ? "prepass" : gbuffer ? "gbuffer" : "forward"}/${type.name}/${blend}/x${msaa}/${variantCull(variant)}`,
480
- layout: materialLayout(gpu, state, assets, type, store),
481
- vertex: { module, entryPoint: "vs", buffers: VERTEX_BUFFERS },
482
- fragment: {
483
- module,
484
- entryPoint: "fs",
485
- targets: pick ? PICK_TARGETS : prepass ? PREPASS_TARGETS : gbuffer ? state.gbufferTargets : [{ format: "rgba16float", blend: blendState(blend) }]
486
- },
487
- primitive: { topology: "triangle-list", cullMode: variantCull(variant), frontFace: "ccw" },
488
- depthStencil: {
489
- format: "depth32float",
490
- depthWriteEnabled: !transparent,
491
- depthCompare: transparent ? "greater-equal" : "greater"
492
- },
493
- multisample: { count: msaa }
494
- });
495
- if (!pipeline) continue;
565
+ if (!fallback && state.pipelines.failing(own.type)) d--;
566
+ continue;
496
567
  }
497
568
  const mat = assets.material(ctx.world, material);
498
569
  if (!mat?.bindGroup) continue;
@@ -529,14 +600,127 @@ function drawMaterials(ctx, state, pv, cam, draws, pass) {
529
600
  renderPass.draw(gm.count, item.count, 0, item.first);
530
601
  }
531
602
  }
603
+ if (draws.cullView < 0) warmPipelines(ctx, state, cam, store, assets, pass, cutGroup && cutaway);
532
604
  return switches;
533
605
  }
606
+ function inPass(type, pass) {
607
+ if (pass === PASS_GBUFFER) return type.standard;
608
+ if (pass === PASS_PICK || pass === PASS_PICK_GROUND) return type.pickable;
609
+ return true;
610
+ }
611
+ function materialPipeline(ctx, state, cam, store, assets, material, pass, draw, cut) {
612
+ const gpu = ctx.gpu;
613
+ const type = material.type;
614
+ const variant = materialVariant(material);
615
+ const blend = variantBlend(variant);
616
+ const gbuffer = pass === PASS_GBUFFER;
617
+ const depthOnly = pass === PASS_DEPTH;
618
+ const prepass = pass === PASS_PREPASS || depthOnly;
619
+ const pick = pass === PASS_PICK || pass === PASS_PICK_GROUND;
620
+ const ground = pass === PASS_GROUND || pass === PASS_PICK_GROUND;
621
+ const msaa = gbuffer || prepass || pick ? 1 : cam.msaa;
622
+ const mode = gbuffer || prepass || pick ? 0 : state.interiorMode;
623
+ const key = ((pass * 1024 + typeOrdinal(type)) * 16 + variant) * 8 + msaa + (cut ? CUT_KEY : 0) + mode * INTERIOR_KEY;
624
+ let pipeline = state.pipelines.cached(key);
625
+ if (!pipeline) {
626
+ const premultiply = blend === "premultiplied";
627
+ const mask = blend === "mask";
628
+ const opaque = !gbuffer && !prepass && !pick && !isTransparent(blend);
629
+ const slot = (cut ? 64 : 0) + (pick ? 40 : prepass ? 32 + (type.standard ? 0 : 4) + (mask ? 2 : 0) : (gbuffer ? 16 : opaque ? 48 : 0) + (type.standard ? 0 : 4) + (premultiply ? 1 : 0) + (mask ? 2 : 0));
630
+ const module = state.pipelines.module(
631
+ ctx.world,
632
+ gpu,
633
+ type,
634
+ slot,
635
+ pick ? "shard::pick" : prepass ? type.standard ? "shard::prepass" : "shard::prepass::plain" : gbuffer ? "shard::pbr::gbuffer_pass" : type.standard ? "shard::pbr::forward" : "shard::unlit::forward",
636
+ // Only what the slot tells apart: the prepass and picking don't premultiply.
637
+ LIT_DEFINES[mode][cut ? 1 : 0][pick ? 0 : prepass ? mask ? 2 : 0 : (premultiply ? 1 : 0) | (mask ? 2 : 0) | (opaque ? 4 : 0)]
638
+ );
639
+ if (!module) {
640
+ if (draw && !state.pipelines.failing(type)) gpu.pipelines.skipped++;
641
+ return void 0;
642
+ }
643
+ const transparent = isTransparent(blend) && !pick;
644
+ pipeline = state.pipelines.create(
645
+ gpu,
646
+ key,
647
+ {
648
+ label: `${pick ? "pick" : prepass ? "prepass" : gbuffer ? "gbuffer" : ground ? "ground" : "forward"}/${type.name}/${blend}/x${msaa}/${variantCull(variant)}${cut ? "/cutaway" : ""}`,
649
+ layout: materialLayout(gpu, state, assets, type, store, cut?.layout(gpu)),
650
+ vertex: { module, entryPoint: "vs", buffers: VERTEX_BUFFERS },
651
+ fragment: {
652
+ module,
653
+ entryPoint: depthOnly ? "fs_depth" : "fs",
654
+ targets: pick ? PICK_TARGETS : depthOnly ? [] : prepass ? PREPASS_TARGETS : gbuffer ? state.gbufferTargets : [{ format: "rgba16float", blend: blendState(blend) }]
655
+ },
656
+ primitive: {
657
+ topology: "triangle-list",
658
+ cullMode: variantCull(variant),
659
+ frontFace: "ccw"
660
+ },
661
+ depthStencil: {
662
+ format: "depth32float",
663
+ // Ground bands never write depth (so they never fight); picking them writes it, so the
664
+ // nearest band wins at equal depth.
665
+ depthWriteEnabled: pick || !transparent && !ground,
666
+ depthCompare: transparent || ground ? "greater-equal" : "greater",
667
+ // Ground bands lie on their floor: a few ULPs and a slope term keep them in front of it
668
+ // at any angle (reversed Z: toward the camera). Bands don't write depth, so they never
669
+ // fight each other; the order does that.
670
+ depthBias: ground ? GROUND_DEPTH_BIAS : 0,
671
+ depthBiasSlopeScale: ground ? GROUND_SLOPE_BIAS : 0
672
+ },
673
+ multisample: { count: msaa }
674
+ },
675
+ type
676
+ );
677
+ if (!pipeline) return void 0;
678
+ }
679
+ return pipeline;
680
+ }
681
+ function warmPipelines(ctx, state, cam, store, assets, pass, cut) {
682
+ const slot = pass * 8 + cam.msaa + (cut ? CUT_KEY : 0) + state.interiorMode * INTERIOR_KEY;
683
+ if (state.pipelines.warmed(slot, store.structureVersion, ctx.gpu.generation)) return;
684
+ const ground = pass === PASS_GROUND || pass === PASS_PICK_GROUND;
685
+ const batches = store.batches;
686
+ for (let i = 0; i < batches.length; i++) {
687
+ const batch = batches[i];
688
+ if (batch.count === 0 || batch.ground !== ground) continue;
689
+ if (pass === PASS_TRANSPARENT ? !batch.transparent : batch.transparent && pass !== PASS_PICK) {
690
+ continue;
691
+ }
692
+ if (pass === PASS_OPAQUE && cam.deferred && batch.deferrable) continue;
693
+ const own = batch.material;
694
+ if (!inPass(own.type, pass)) continue;
695
+ const material = state.pipelines.failing(own.type) ? state.pipelines.proxy(own) : own;
696
+ materialPipeline(ctx, state, cam, store, assets, material, pass, false);
697
+ if (cut && batch.cutaway > 0)
698
+ materialPipeline(ctx, state, cam, store, assets, material, pass, false, cut);
699
+ assets.material(ctx.world, material);
700
+ }
701
+ if (ctx.gpu.pipelines.pending === 0) {
702
+ state.pipelines.markWarmed(slot, store.structureVersion, ctx.gpu.generation);
703
+ }
704
+ }
534
705
  function recordSwitches(ctx, switches) {
535
706
  const stats = ctx.world.resource(RenderStats).get(ctx.view.name);
536
707
  if (stats) stats.pipelineSwitches = Math.max(stats.pipelineSwitches ?? 0, switches);
537
708
  }
538
709
  const isCamera = (view) => cameraOf(view) !== void 0;
539
710
  const msaaOf = (view) => cameraOf(view)?.msaa ?? 1;
711
+ function reportLightBudget(world, view, b, max) {
712
+ b.reported = b.dropped;
713
+ if (b.dropped === 0) {
714
+ clearHealthIssue(world, "render/light-budget", view);
715
+ return;
716
+ }
717
+ raiseHealthIssue(world, {
718
+ code: "render/light-budget",
719
+ severity: "degraded",
720
+ ref: view,
721
+ message: `${b.inView} point and spot lights are in view, over the baseline tier's ${Math.min(max, 128)}: the ${b.dropped} farthest aren't shaded`
722
+ });
723
+ }
540
724
  function whiteTexture(gpu, state) {
541
725
  if (!state.white || state.white.generation !== gpu.generation) {
542
726
  const texture = gpu.device.createTexture({
@@ -557,7 +741,8 @@ function emptyShadow(gpu, state) {
557
741
  label: "shadows/empty",
558
742
  size: [1, 1, 1],
559
743
  format: "depth32float",
560
- usage: GPUTextureUsage.TEXTURE_BINDING
744
+ usage: GPUTextureUsage.TEXTURE_BINDING,
745
+ ...bindingDimension(gpu, "2d-array")
561
746
  }),
562
747
  generation: gpu.generation
563
748
  };
@@ -604,29 +789,35 @@ function viewBindGroup(gpu, world, pv, cam) {
604
789
  const source = baked?.sourceView ?? envs.emptyView;
605
790
  const sh = baked?.sh ?? envs.emptySh;
606
791
  const ao = pv.ao ?? whiteTexture(gpu, state);
607
- const key = `${idOf(ao)}/${gpu.generation}/${pv.uniform.version}/${lights.buffer.version}/${pv.clusters.clusters.version}/${lights.directional.version}/${pv.shadowData.version}/${idOf(cascades)}/${idOf(spots)}/${idOf(points)}/${baked ? idOf(baked.source) : 0}/${sh.version}/${state.globals.version}`;
792
+ const b = pv.baseline;
793
+ const lightsVersion = b ? b.lights.version : lights.buffer.version;
794
+ const clustersVersion = b ? b.bits.version : pv.clusters.clusters.version;
795
+ const interior = state.interiorMode !== 0 ? world.tryResource(InteriorPath) : void 0;
796
+ const key = `${idOf(ao)}/${gpu.generation}/${pv.uniform.version}/${lightsVersion}/${clustersVersion}/${lights.directional.version}/${pv.shadowData.version}/${idOf(cascades)}/${idOf(spots)}/${idOf(points)}/${baked ? idOf(baked.source) : 0}/${sh.version}/${state.globals.version}${interior ? `/${state.interiorMode}/${interior.version}` : ""}`;
608
797
  if (!pv.bindGroup || pv.bound !== key) {
798
+ const entries = [
799
+ { binding: 0, resource: { buffer: pv.uniform.buffer } },
800
+ { binding: 1, resource: b ? b.lights.resource() : { buffer: lights.buffer.buffer } },
801
+ { binding: 2, resource: b ? b.bits.resource() : { buffer: pv.clusters.clusters.buffer } },
802
+ { binding: 3, resource: lights.directional.resource() },
803
+ { binding: 4, resource: pv.shadowData.resource() },
804
+ { binding: 5, resource: cascades.createView({ dimension: "2d-array" }) },
805
+ { binding: 6, resource: spots.createView({ dimension: "2d-array" }) },
806
+ { binding: 7, resource: points.createView({ dimension: "2d-array" }) },
807
+ { binding: 8, resource: state.shadowSampler },
808
+ { binding: 9, resource: specular },
809
+ { binding: 10, resource: envs.lut.createView() },
810
+ { binding: 11, resource: envs.sampler },
811
+ { binding: 12, resource: sh.resource() },
812
+ { binding: 13, resource: source },
813
+ { binding: 14, resource: { buffer: state.globals.buffer } },
814
+ { binding: 15, resource: ao.createView() }
815
+ ];
816
+ if (interior) entries.push(...interior.entries(gpu));
609
817
  pv.bindGroup = gpu.device.createBindGroup({
610
818
  label: "forward/view",
611
819
  layout: state.layouts.view,
612
- entries: [
613
- { binding: 0, resource: { buffer: pv.uniform.buffer } },
614
- { binding: 1, resource: { buffer: lights.buffer.buffer } },
615
- { binding: 2, resource: { buffer: pv.clusters.clusters.buffer } },
616
- { binding: 3, resource: { buffer: lights.directional.buffer } },
617
- { binding: 4, resource: { buffer: pv.shadowData.buffer } },
618
- { binding: 5, resource: cascades.createView({ dimension: "2d-array" }) },
619
- { binding: 6, resource: spots.createView({ dimension: "2d-array" }) },
620
- { binding: 7, resource: points.createView({ dimension: "2d-array" }) },
621
- { binding: 8, resource: state.shadowSampler },
622
- { binding: 9, resource: specular },
623
- { binding: 10, resource: envs.lut.createView() },
624
- { binding: 11, resource: envs.sampler },
625
- { binding: 12, resource: { buffer: sh.buffer } },
626
- { binding: 13, resource: source },
627
- { binding: 14, resource: { buffer: state.globals.buffer } },
628
- { binding: 15, resource: ao.createView() }
629
- ]
820
+ entries
630
821
  });
631
822
  pv.bound = key;
632
823
  }
@@ -649,6 +840,40 @@ function forwardNode(state) {
649
840
  }
650
841
  };
651
842
  }
843
+ function groundNode(state) {
844
+ return {
845
+ kind: "render",
846
+ phase: RenderPhase.Ground,
847
+ enabled: (view) => (cameraOf(view)?.ground.length ?? 0) > 0,
848
+ reads: ["clusters", "shadow-cascades", "shadow-local", "environment", "culled", "ssao"],
849
+ writes: ["scene-color", "hdr"],
850
+ color: (view) => sceneColor(view),
851
+ depth: { resource: "scene-depth", readOnly: true },
852
+ run: (ctx) => {
853
+ const cam = cameraOf(ctx.view);
854
+ const pv = state.views.get(ctx.view.name);
855
+ if (!pv) return;
856
+ drawMaterials(ctx, state, pv, cam, cam.ground, PASS_GROUND);
857
+ }
858
+ };
859
+ }
860
+ function overlayNode(state) {
861
+ return {
862
+ kind: "render",
863
+ phase: RenderPhase.Overlay3d,
864
+ enabled: (view) => (cameraOf(view)?.overlay.length ?? 0) > 0,
865
+ reads: ["clusters", "shadow-cascades", "shadow-local", "environment", "culled", "ssao"],
866
+ writes: ["scene-color", "hdr"],
867
+ color: (view) => sceneColor(view),
868
+ depth: { resource: "scene-depth", readOnly: true },
869
+ run: (ctx) => {
870
+ const cam = cameraOf(ctx.view);
871
+ const pv = state.views.get(ctx.view.name);
872
+ if (!pv) return;
873
+ drawMaterials(ctx, state, pv, cam, cam.overlay, PASS_GROUND);
874
+ }
875
+ };
876
+ }
652
877
  function transparentNode(state) {
653
878
  return {
654
879
  kind: "render",
@@ -670,11 +895,12 @@ function clusterNode(state) {
670
895
  return {
671
896
  kind: "raw",
672
897
  phase: RenderPhase.Setup,
673
- enabled: isCamera,
898
+ // Baseline bins on the CPU (baseline/lights.ts): no compute.
899
+ enabled: (view) => isCamera(view) && !state.baseline,
674
900
  writes: ["clusters"],
675
901
  run: (ctx) => {
676
902
  const pv = state.views.get(ctx.view.name);
677
- if (!pv) return;
903
+ if (!pv?.clusters) return;
678
904
  const gpu = ctx.gpu;
679
905
  const module = ctx.world.resource(Shaders).module(gpu, { root: "shard::lighting::cluster" });
680
906
  if (!module) {
@@ -732,6 +958,18 @@ function clusterNode(state) {
732
958
  }
733
959
  };
734
960
  }
961
+ let skippedBefore = 0;
962
+ function beginShadowView(ctx, view) {
963
+ if (!view.redraw && view.drawn) return false;
964
+ skippedBefore = ctx.gpu.pipelines.skipped;
965
+ ctx.world.resource(RenderStats).current.shadowMapsRendered++;
966
+ return true;
967
+ }
968
+ function endShadowView(ctx, view) {
969
+ if (ctx.gpu.pipelines.skipped === skippedBefore)
970
+ markShadowDrawn(view, ctx.world.resource(Instances));
971
+ else view.drawn = false;
972
+ }
735
973
  function cascadeNode(state) {
736
974
  return {
737
975
  kind: "raw",
@@ -745,16 +983,19 @@ function cascadeNode(state) {
745
983
  const size = ctx.world.resource(LightingSettings).cascadeMapSize;
746
984
  const texture = pv.cascades.ensureTexture(ctx.gpu, size, ctx.view.name);
747
985
  for (let i = 0; i < pv.cascades.count; i++) {
986
+ const view = pv.cascades.views[i];
987
+ if (!beginShadowView(ctx, view)) continue;
748
988
  drawShadowCasters(
749
989
  ctx,
750
990
  texture.createView({ dimension: "2d", baseArrayLayer: i, arrayLayerCount: 1 }),
751
- pv.cascades.views[i],
752
- pv.cascades.views[i].offset,
991
+ view,
992
+ view.offset,
753
993
  shadows.uniforms,
754
994
  state.pipelines,
755
995
  state.layouts.shadowView,
756
996
  `shadows/cascade${i}`
757
997
  );
998
+ endShadowView(ctx, view);
758
999
  }
759
1000
  }
760
1001
  };
@@ -776,6 +1017,7 @@ function localShadowNode(state) {
776
1017
  const layer = (texture, i) => texture.createView({ dimension: "2d", baseArrayLayer: i, arrayLayerCount: 1 });
777
1018
  for (let i = 0; i < local.spots.length; i++) {
778
1019
  const view = local.spotViews[i];
1020
+ if (!beginShadowView(ctx, view)) continue;
779
1021
  drawShadowCasters(
780
1022
  ctx,
781
1023
  layer(local.spotTexture, i),
@@ -786,9 +1028,11 @@ function localShadowNode(state) {
786
1028
  state.layouts.shadowView,
787
1029
  `shadows/spot${i}`
788
1030
  );
1031
+ endShadowView(ctx, view);
789
1032
  }
790
1033
  for (let i = 0; i < local.points.length * 6; i++) {
791
1034
  const view = local.pointViews[i];
1035
+ if (!beginShadowView(ctx, view)) continue;
792
1036
  drawShadowCasters(
793
1037
  ctx,
794
1038
  layer(local.pointTexture, i),
@@ -799,6 +1043,7 @@ function localShadowNode(state) {
799
1043
  state.layouts.shadowView,
800
1044
  `shadows/point${i}`
801
1045
  );
1046
+ endShadowView(ctx, view);
802
1047
  }
803
1048
  }
804
1049
  };
@@ -907,13 +1152,31 @@ function sceneColor(view, clear) {
907
1152
  if (clear === void 0) return msaa ? MSAA_LOAD : PLAIN_LOAD;
908
1153
  return [{ resource: "scene-color", clear, resolve: msaa ? "hdr" : void 0 }];
909
1154
  }
910
- function depthResolveNode() {
1155
+ function depthPrepassNode(state) {
1156
+ return {
1157
+ kind: "render",
1158
+ phase: RenderPhase.Prepass,
1159
+ enabled: (view) => state.baseline !== void 0 && msaaOf(view) > 1,
1160
+ reads: ["culled"],
1161
+ writes: ["depth"],
1162
+ depth: { resource: "depth", clear: 0 },
1163
+ run: (ctx) => {
1164
+ const pv = state.views.get(ctx.view.name);
1165
+ const cam = cameraOf(ctx.view);
1166
+ if (!pv || !cam) return;
1167
+ drawMaterials(ctx, state, pv, cam, cam.draws, PASS_DEPTH);
1168
+ if (cam.deferred) drawMaterials(ctx, state, pv, cam, cam.forwardOnly, PASS_DEPTH);
1169
+ }
1170
+ };
1171
+ }
1172
+ function depthResolveNode(state) {
911
1173
  let bindGroups = /* @__PURE__ */ new WeakMap();
912
1174
  let generation = -1;
913
1175
  return {
914
1176
  kind: "render",
915
1177
  phase: RenderPhase.Resolve,
916
- enabled: (view) => msaaOf(view) > 1,
1178
+ // Baseline draws `depth` in its own prepass instead (depthPrepassNode).
1179
+ enabled: (view) => state.baseline === void 0 && msaaOf(view) > 1,
917
1180
  reads: ["scene-depth"],
918
1181
  writes: ["depth"],
919
1182
  depth: { resource: "depth", clear: 0 },
@@ -961,39 +1224,36 @@ function depthResolveNode() {
961
1224
  }
962
1225
  };
963
1226
  }
964
- function createLayouts(gpu, assets, store) {
1227
+ function viewEntries(gpu) {
965
1228
  const F = GPUShaderStage.FRAGMENT;
966
- const storage = (binding) => ({
967
- binding,
968
- visibility: F,
969
- buffer: { type: "read-only-storage" }
970
- });
971
1229
  const depthArray = (binding) => ({
972
1230
  binding,
973
1231
  visibility: F,
974
1232
  texture: { sampleType: "depth", viewDimension: "2d-array" }
975
1233
  });
976
- const view = gpu.layouts.bindGroupLayout({
977
- label: "forward/view",
978
- entries: [
979
- { binding: 0, visibility: GPUShaderStage.VERTEX | F, buffer: { type: "uniform" } },
980
- storage(1),
981
- storage(2),
982
- storage(3),
983
- storage(4),
984
- depthArray(5),
985
- depthArray(6),
986
- depthArray(7),
987
- { binding: 8, visibility: F, sampler: { type: "comparison" } },
988
- { binding: 9, visibility: F, texture: { sampleType: "float", viewDimension: "cube" } },
989
- { binding: 10, visibility: F, texture: { sampleType: "float" } },
990
- { binding: 11, visibility: F, sampler: { type: "filtering" } },
991
- storage(12),
992
- { binding: 13, visibility: F, texture: { sampleType: "float", viewDimension: "cube" } },
993
- { binding: 14, visibility: GPUShaderStage.VERTEX | F, buffer: { type: "uniform" } },
994
- { binding: 15, visibility: F, texture: { sampleType: "float" } }
995
- ]
996
- });
1234
+ return [
1235
+ { binding: 0, visibility: GPUShaderStage.VERTEX | F, buffer: { type: "uniform" } },
1236
+ // Storage on the full tier; on baseline (0064) lights, directional lights, shadow data and SH
1237
+ // are uniform blocks and the cluster bits a data texture.
1238
+ dataEntry(gpu, 1, F, "uniform"),
1239
+ dataEntry(gpu, 2, F, "texture"),
1240
+ dataEntry(gpu, 3, F, "uniform"),
1241
+ dataEntry(gpu, 4, F, "uniform"),
1242
+ depthArray(5),
1243
+ depthArray(6),
1244
+ depthArray(7),
1245
+ { binding: 8, visibility: F, sampler: { type: "comparison" } },
1246
+ { binding: 9, visibility: F, texture: { sampleType: "float", viewDimension: "cube" } },
1247
+ { binding: 10, visibility: F, texture: { sampleType: "float" } },
1248
+ { binding: 11, visibility: F, sampler: { type: "filtering" } },
1249
+ dataEntry(gpu, 12, F, "uniform"),
1250
+ { binding: 13, visibility: F, texture: { sampleType: "float", viewDimension: "cube" } },
1251
+ { binding: 14, visibility: GPUShaderStage.VERTEX | F, buffer: { type: "uniform" } },
1252
+ { binding: 15, visibility: F, texture: { sampleType: "float" } }
1253
+ ];
1254
+ }
1255
+ function createLayouts(gpu, assets, store) {
1256
+ const view = gpu.layouts.bindGroupLayout({ label: "forward/view", entries: viewEntries(gpu) });
997
1257
  const C = GPUShaderStage.COMPUTE;
998
1258
  const cluster = gpu.layouts.bindGroupLayout({
999
1259
  label: "light-clusters",
@@ -1008,6 +1268,8 @@ function createLayouts(gpu, assets, store) {
1008
1268
  const shadowView = shadowViewLayout(gpu);
1009
1269
  return {
1010
1270
  view,
1271
+ viewBase: view,
1272
+ viewInterior: void 0,
1011
1273
  pipeline: gpu.layouts.pipelineLayout({
1012
1274
  label: "forward",
1013
1275
  bindGroupLayouts: [view, assets.materialLayout, store.layout]
@@ -1024,6 +1286,29 @@ function createLayouts(gpu, assets, store) {
1024
1286
  })
1025
1287
  };
1026
1288
  }
1289
+ function interiorViewLayout(gpu, state) {
1290
+ const layouts = state.layouts;
1291
+ if (layouts.viewInterior) return layouts.viewInterior;
1292
+ const F = GPUShaderStage.FRAGMENT;
1293
+ layouts.viewInterior = gpu.layouts.bindGroupLayout({
1294
+ label: "forward/view/interior",
1295
+ entries: [
1296
+ ...viewEntries(gpu),
1297
+ {
1298
+ binding: INTERIOR_BINDINGS.data,
1299
+ visibility: F,
1300
+ texture: { sampleType: "uint", viewDimension: "2d-array" }
1301
+ },
1302
+ { binding: INTERIOR_BINDINGS.table, visibility: F, buffer: { type: "uniform" } }
1303
+ ]
1304
+ });
1305
+ return layouts.viewInterior;
1306
+ }
1307
+ function chooseInterior(gpu, world, state) {
1308
+ const mode = world.tryResource(InteriorPath)?.mode ?? 0;
1309
+ state.interiorMode = mode;
1310
+ state.layouts.view = mode !== 0 ? interiorViewLayout(gpu, state) : state.layouts.viewBase;
1311
+ }
1027
1312
  function ensureLayouts(gpu, state, assets, store) {
1028
1313
  if (state.layoutGeneration === gpu.generation) return;
1029
1314
  state.layoutGeneration = gpu.generation;
@@ -1039,6 +1324,7 @@ function forwardCorePlugin(options = {}) {
1039
1324
  provides: [
1040
1325
  // core
1041
1326
  Environments,
1327
+ MaterialFallbacks,
1042
1328
  DeferredPath,
1043
1329
  DeformPath,
1044
1330
  PostFeatures,
@@ -1065,6 +1351,7 @@ function forwardCorePlugin(options = {}) {
1065
1351
  MorphWeights,
1066
1352
  NotShadowCaster,
1067
1353
  NotShadowReceiver,
1354
+ ShadowWhenHidden,
1068
1355
  SkinnedMesh,
1069
1356
  VisibilityRange,
1070
1357
  AmbientLight,
@@ -1075,6 +1362,14 @@ function forwardCorePlugin(options = {}) {
1075
1362
  SpotLight,
1076
1363
  ComputedVisibility,
1077
1364
  Visibility,
1365
+ // per-view visibility and cutaways (0070)
1366
+ ViewVisibility,
1367
+ HiddenSetsResource,
1368
+ Cutaway,
1369
+ CutawayView,
1370
+ CutawayPath,
1371
+ // interior lighting (0069)
1372
+ InteriorPath,
1078
1373
  Cameras,
1079
1374
  RenderPath,
1080
1375
  Tonemapping,
@@ -1087,6 +1382,12 @@ function forwardCorePlugin(options = {}) {
1087
1382
  SkinAssetType,
1088
1383
  Skins,
1089
1384
  RenderScale,
1385
+ // tabletop (0057)
1386
+ GroundLayer,
1387
+ RenderLayers,
1388
+ Outline,
1389
+ OutlinePath,
1390
+ CameraMoved,
1090
1391
  // shadows
1091
1392
  ShadowsResource,
1092
1393
  // post
@@ -1102,11 +1403,21 @@ function forwardCorePlugin(options = {}) {
1102
1403
  // pixel-perfect
1103
1404
  PixelPerfect,
1104
1405
  PixelPerfectPath,
1105
- PixelTargets
1406
+ PixelTargets,
1407
+ // lens fields
1408
+ Lens,
1409
+ LensFields,
1410
+ LensPath,
1411
+ // screen effects
1412
+ ScreenEffects,
1413
+ ScreenEffectHandlers,
1414
+ // shard shaders bake (0064)
1415
+ ShaderVariantSources
1106
1416
  ],
1107
1417
  dependencies: ["render", "core/transform"],
1108
1418
  build(app) {
1109
1419
  const w = app.world;
1420
+ addShaderVariantSource(w, materialVariants);
1110
1421
  w.initResource(Meshes);
1111
1422
  w.initResource(Skins);
1112
1423
  w.initResource(Materials);
@@ -1119,98 +1430,185 @@ function forwardCorePlugin(options = {}) {
1119
1430
  w.initResource(Environments);
1120
1431
  w.initResource(ViewSettings).msaa = options.msaa ?? 4;
1121
1432
  Object.assign(w.initResource(RenderScale), options.renderScale);
1433
+ w.initResource(LensFields);
1434
+ w.initResource(ScreenEffects);
1435
+ w.initResource(ScreenEffectHandlers);
1436
+ w.initResource(RenderDescribers).set("screenEffects", describeScreenEffects);
1122
1437
  observeInstanceRemovals(w);
1438
+ observeOutlinesWithoutPass(w);
1439
+ observeViewVisibilityWithoutPlugin(w);
1123
1440
  observeLightRemovals(w);
1124
1441
  observeOriginShifts(w);
1125
- app.addSystems(PostUpdate, computeVisibility.after(TransformSystems), applyPhysicalCameras).addSystems(
1442
+ app.addSystems(PreUpdate, runScreenEffects).addSystems(PostUpdate, computeVisibility.after(TransformSystems), applyPhysicalCameras).addSystems(
1126
1443
  Last,
1444
+ expireLensFields.inSet(RenderSet.Begin),
1445
+ expireScreenEffects.inSet(RenderSet.Begin),
1127
1446
  extractCameras.inSet(RenderSet.Extract),
1128
1447
  extractLights.inSet(RenderSet.Extract),
1129
1448
  prepareInstances.inSet(RenderSet.Prepare),
1130
1449
  prepareLights.inSet(RenderSet.Prepare),
1131
- queue.inSet(RenderSet.Queue),
1450
+ forwardQueue.inSet(RenderSet.Queue),
1132
1451
  upload.inSet(RenderSet.Upload)
1133
1452
  );
1134
1453
  },
1135
- ready(app) {
1454
+ async ready(app) {
1136
1455
  const gpu = app.world.resource(Gpu);
1137
- setTextureCapabilities({
1138
- bc: gpu.features.has("texture-compression-bc"),
1139
- astc: gpu.features.has("texture-compression-astc"),
1140
- etc2: gpu.features.has("texture-compression-etc2")
1141
- });
1142
- const assets = new GpuAssets(gpu, app.world.initResource(GpuMemory));
1143
- const store = new InstanceStore(gpu);
1144
- app.insertResource(GpuAssetsResource, assets);
1145
- app.insertResource(Instances, store);
1146
- app.insertResource(Culler, new GpuCuller(gpu));
1147
- app.insertResource(
1148
- Lights,
1149
- new LightStore(gpu, app.world.resource(LightingSettings).maxLights)
1150
- );
1151
- app.insertResource(ShadowsResource, {
1152
- local: new LocalShadows(),
1153
- uniforms: new ShadowPassUniforms(gpu)
1456
+ const [baseline, baselineTextures] = gpu.tier === "baseline" ? await Promise.all([import("./baseline/lights.js"), import("./baseline/textures.js")]) : [void 0, void 0];
1457
+ app.scoped(() => {
1458
+ setTextureCapabilities({
1459
+ bc: gpu.features.has("texture-compression-bc"),
1460
+ astc: gpu.features.has("texture-compression-astc"),
1461
+ etc2: gpu.features.has("texture-compression-etc2")
1462
+ });
1463
+ const assets = new GpuAssets(gpu, app.world.initResource(GpuMemory));
1464
+ if (baselineTextures) {
1465
+ assets.baseline = baselineTextures;
1466
+ app.world.initResource(RenderDescribers).set("twins", () => assets.describeTwins());
1467
+ }
1468
+ assets.counts = app.world.initResource(RenderStats).current;
1469
+ const store = new InstanceStore(gpu);
1470
+ app.insertResource(GpuAssetsResource, assets);
1471
+ unwatch.set(app, assets.watch(assetServer(app.world)));
1472
+ app.world.owners.addDescriber("gpu", (owner) => {
1473
+ const server = assetServer(app.world);
1474
+ const total = { buffers: 0, textures: 0, bytes: 0 };
1475
+ for (const path of server.leasesOf(owner)) {
1476
+ const item = server.item(path);
1477
+ const o = assets.objectsOf(item);
1478
+ total.buffers += o.buffers;
1479
+ total.textures += o.textures;
1480
+ total.bytes += o.bytes;
1481
+ }
1482
+ return total;
1483
+ });
1484
+ app.insertResource(Instances, store);
1485
+ app.insertResource(Culler, new GpuCuller(gpu));
1486
+ app.insertResource(
1487
+ Lights,
1488
+ new LightStore(gpu, app.world.resource(LightingSettings).maxLights)
1489
+ );
1490
+ app.insertResource(ShadowsResource, {
1491
+ local: new LocalShadows(),
1492
+ uniforms: new ShadowPassUniforms(gpu)
1493
+ });
1494
+ const state = {
1495
+ views: /* @__PURE__ */ new Map(),
1496
+ viewBytes: new DataView(new ArrayBuffer(viewLayout.size)),
1497
+ shadowFloats: new Float32Array(SHADOW_DATA_FLOATS),
1498
+ layouts: createLayouts(gpu, assets, store),
1499
+ layoutGeneration: gpu.generation,
1500
+ emptyShadow: void 0,
1501
+ white: void 0,
1502
+ shadowSampler: void 0,
1503
+ frame: 0,
1504
+ warned: { budget: "", overflow: "" },
1505
+ globals: new GpuBuffer(gpu, {
1506
+ label: "globals",
1507
+ usage: GPUBufferUsage.UNIFORM,
1508
+ size: 16
1509
+ }),
1510
+ globalsData: new Float32Array(4),
1511
+ pipelines: new MaterialPipelines(),
1512
+ baseline,
1513
+ cullerActive: false,
1514
+ interiorMode: 0,
1515
+ gbufferTargets: [
1516
+ { format: "rgba8unorm-srgb" },
1517
+ { format: "rgba16float" },
1518
+ { format: gbufferEmissiveFormat(gpu) }
1519
+ ]
1520
+ };
1521
+ app.insertResource(MaterialFallbacks, {
1522
+ count: () => state.pipelines.failureCount,
1523
+ list: () => state.pipelines.failures()
1524
+ });
1525
+ store.batchKey = (b) => typeOrdinal(b.material.type) * 16 + materialVariant(b.material);
1526
+ app.insertResource(State, state);
1527
+ const graph = app.world.resource(Graph);
1528
+ graph.declare({ name: "scene-color", format: "rgba16float", sampleCount: msaaOf });
1529
+ graph.declare({ name: "scene-depth", format: "depth32float", sampleCount: msaaOf });
1530
+ graph.declare({ name: "hdr", format: "rgba16float" });
1531
+ graph.declare({ name: "depth", format: "depth32float" });
1532
+ graph.declare({ name: "ldr", format: "view" });
1533
+ addRenderFeatures(app.world, {
1534
+ name: "render/forward",
1535
+ description: "Opaque, ground and transparent mesh passes, and the MSAA depth resolve.",
1536
+ nodes: [
1537
+ "forward-opaque",
1538
+ "forward-ground",
1539
+ "forward-transparent",
1540
+ "forward-overlay",
1541
+ "depth-resolve",
1542
+ "depth-prepass"
1543
+ ],
1544
+ baseline: {
1545
+ strategy: "Instances, visibility and deform data in data textures; MSAA depth from a single-sample depth prepass"
1546
+ }
1547
+ });
1548
+ addRenderFeatures(app.world, {
1549
+ name: "render/culling",
1550
+ description: "GPU frustum, range and LOD culling with indirect draws.",
1551
+ nodes: ["instance-cull"],
1552
+ baseline: { strategy: "CPU culling and direct draws" }
1553
+ });
1554
+ addRenderFeatures(app.world, {
1555
+ name: "render/light-clusters",
1556
+ description: "Clustered light binning (Forward+).",
1557
+ nodes: ["light-clusters"],
1558
+ baseline: { strategy: "CPU binning into a 128-light bitmask texture" }
1559
+ });
1560
+ addRenderFeatures(app.world, {
1561
+ name: "render/shadows",
1562
+ description: "Cascaded directional shadows, and spot and point shadow layers.",
1563
+ nodes: ["shadows/cascades", "shadows/local"],
1564
+ baseline: { strategy: "The same render passes" }
1565
+ });
1566
+ graph.addNode("instance-cull", cullNode(state));
1567
+ graph.addNode("light-clusters", clusterNode(state));
1568
+ graph.addNode("shadows/cascades", cascadeNode(state));
1569
+ graph.addNode("shadows/local", localShadowNode(state));
1570
+ graph.addNode("forward-opaque", forwardNode(state));
1571
+ graph.addNode("forward-ground", groundNode(state));
1572
+ graph.addNode("forward-overlay", overlayNode(state));
1573
+ graph.addNode("forward-transparent", transparentNode(state));
1574
+ graph.addNode("depth-resolve", depthResolveNode(state));
1575
+ graph.addNode("depth-prepass", depthPrepassNode(state));
1576
+ addDisplayNodes(app.world);
1154
1577
  });
1155
- const state = {
1156
- views: /* @__PURE__ */ new Map(),
1157
- viewBytes: new DataView(new ArrayBuffer(viewLayout.size)),
1158
- shadowFloats: new Float32Array(SHADOW_DATA_FLOATS),
1159
- layouts: createLayouts(gpu, assets, store),
1160
- layoutGeneration: gpu.generation,
1161
- emptyShadow: void 0,
1162
- white: void 0,
1163
- shadowSampler: void 0,
1164
- frame: 0,
1165
- warned: { budget: "", overflow: "" },
1166
- globals: new GpuBuffer(gpu, { label: "globals", usage: GPUBufferUsage.UNIFORM, size: 16 }),
1167
- globalsData: new Float32Array(4),
1168
- pipelines: new MaterialPipelines(),
1169
- cullerActive: false,
1170
- gbufferTargets: [
1171
- { format: "rgba8unorm-srgb" },
1172
- { format: "rgba16float" },
1173
- { format: gbufferEmissiveFormat(gpu) }
1174
- ]
1175
- };
1176
- store.batchKey = (b) => typeOrdinal(b.material.type) * 16 + materialVariant(b.material);
1177
- app.insertResource(State, state);
1178
- const graph = app.world.resource(Graph);
1179
- graph.declare({ name: "scene-color", format: "rgba16float", sampleCount: msaaOf });
1180
- graph.declare({ name: "scene-depth", format: "depth32float", sampleCount: msaaOf });
1181
- graph.declare({ name: "hdr", format: "rgba16float" });
1182
- graph.declare({ name: "depth", format: "depth32float" });
1183
- graph.declare({ name: "ldr", format: "view" });
1184
- graph.addNode("instance-cull", cullNode(state));
1185
- graph.addNode("light-clusters", clusterNode(state));
1186
- graph.addNode("shadows/cascades", cascadeNode(state));
1187
- graph.addNode("shadows/local", localShadowNode(state));
1188
- graph.addNode("forward-opaque", forwardNode(state));
1189
- graph.addNode("forward-transparent", transparentNode(state));
1190
- graph.addNode("depth-resolve", depthResolveNode());
1191
- addDisplayNodes(app.world);
1578
+ },
1579
+ dispose(app) {
1580
+ unwatch.get(app)?.();
1581
+ unwatch.delete(app);
1192
1582
  }
1193
1583
  });
1194
1584
  }
1585
+ const unwatch = /* @__PURE__ */ new WeakMap();
1195
1586
  function gbufferEmissiveFormat(gpu) {
1196
1587
  return gpu.features.has("rg11b10ufloat-renderable") ? "rg11b10ufloat" : "rgba16float";
1197
1588
  }
1198
1589
  export {
1199
1590
  ForwardStateResource,
1591
+ INTERIOR_DEFINES,
1200
1592
  Mesh3d,
1201
1593
  MeshMaterial,
1594
+ PASS_DEPTH,
1202
1595
  PASS_GBUFFER,
1596
+ PASS_GROUND,
1203
1597
  PASS_OPAQUE,
1204
1598
  PASS_PICK,
1599
+ PASS_PICK_GROUND,
1205
1600
  PASS_PREPASS,
1206
1601
  PASS_TRANSPARENT,
1207
1602
  PICK_TARGETS,
1603
+ VERTEX_BUFFERS,
1208
1604
  drawMaterials,
1209
1605
  forwardCorePlugin,
1606
+ forwardQueue,
1210
1607
  gbufferEmissiveFormat,
1211
1608
  isCamera,
1212
1609
  sceneColor,
1213
1610
  skyNode,
1214
1611
  upload,
1215
- viewBindGroup
1612
+ viewBindGroup,
1613
+ viewPixelScale
1216
1614
  };