@aethervtt/shard-render 0.0.1 → 0.2.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 (170) hide show
  1. package/dist/ablation.d.ts +74 -0
  2. package/dist/ablation.js +117 -0
  3. package/dist/assets.d.ts +3 -0
  4. package/dist/assets.js +43 -2
  5. package/dist/atmosphere-nodes.d.ts +9 -0
  6. package/dist/atmosphere-nodes.js +157 -30
  7. package/dist/atmosphere-plugin.js +8 -2
  8. package/dist/atmosphere-shaders.js +17 -5
  9. package/dist/baseline/atmosphere.d.ts +16 -0
  10. package/dist/baseline/atmosphere.js +301 -0
  11. package/dist/baseline/data-texture.d.ts +36 -0
  12. package/dist/baseline/data-texture.js +181 -0
  13. package/dist/baseline/environment.d.ts +14 -0
  14. package/dist/baseline/environment.js +368 -0
  15. package/dist/baseline/exposure.d.ts +25 -0
  16. package/dist/baseline/exposure.js +133 -0
  17. package/dist/baseline/lights.d.ts +33 -0
  18. package/dist/baseline/lights.js +133 -0
  19. package/dist/baseline/textures.d.ts +97 -0
  20. package/dist/baseline/textures.js +203 -0
  21. package/dist/camera.d.ts +2 -0
  22. package/dist/camera.js +4 -0
  23. package/dist/crowd.d.ts +21 -0
  24. package/dist/crowd.js +121 -0
  25. package/dist/culling-shaders.js +21 -2
  26. package/dist/culling.d.ts +11 -2
  27. package/dist/culling.js +44 -18
  28. package/dist/cutaway-plugin.d.ts +48 -0
  29. package/dist/cutaway-plugin.js +364 -0
  30. package/dist/cutaway.d.ts +27 -0
  31. package/dist/cutaway.js +49 -0
  32. package/dist/data-store.d.ts +69 -0
  33. package/dist/data-store.js +105 -0
  34. package/dist/debug-shaders.js +17 -3
  35. package/dist/debug-views.js +4 -2
  36. package/dist/decals-plugin.d.ts +5 -0
  37. package/dist/decals-plugin.js +54 -0
  38. package/dist/decals.d.ts +69 -0
  39. package/dist/decals.js +326 -0
  40. package/dist/deferred-shaders.js +2 -0
  41. package/dist/deferred.js +28 -4
  42. package/dist/deform.d.ts +5 -4
  43. package/dist/deform.js +4 -9
  44. package/dist/describe-plugin.d.ts +4 -2
  45. package/dist/describe-plugin.js +75 -3
  46. package/dist/display-nodes.js +37 -13
  47. package/dist/dynamic-resolution.d.ts +2 -0
  48. package/dist/dynamic-resolution.js +6 -1
  49. package/dist/environment-plugin.js +17 -2
  50. package/dist/environment-state.d.ts +7 -3
  51. package/dist/environment-state.js +20 -9
  52. package/dist/environment.js +7 -1
  53. package/dist/features.d.ts +49 -0
  54. package/dist/features.js +106 -0
  55. package/dist/foliage-budget.d.ts +64 -0
  56. package/dist/foliage-budget.js +108 -0
  57. package/dist/foliage-path.d.ts +19 -0
  58. package/dist/foliage-path.js +7 -0
  59. package/dist/foliage-shaders.d.ts +11 -0
  60. package/dist/foliage-shaders.js +504 -0
  61. package/dist/foliage.d.ts +194 -0
  62. package/dist/foliage.js +1027 -0
  63. package/dist/forward.d.ts +38 -2
  64. package/dist/forward.js +608 -186
  65. package/dist/fxaa.js +12 -2
  66. package/dist/gizmos-plugin.js +7 -0
  67. package/dist/gizmos.d.ts +3 -3
  68. package/dist/gizmos.js +11 -13
  69. package/dist/gpu-assets.d.ts +58 -4
  70. package/dist/gpu-assets.js +191 -8
  71. package/dist/graph.d.ts +45 -0
  72. package/dist/graph.js +169 -19
  73. package/dist/health.d.ts +58 -0
  74. package/dist/health.js +112 -0
  75. package/dist/index.d.ts +40 -11
  76. package/dist/index.js +343 -9
  77. package/dist/instances.d.ts +119 -8
  78. package/dist/instances.js +387 -78
  79. package/dist/interior-plugin.d.ts +121 -0
  80. package/dist/interior-plugin.js +465 -0
  81. package/dist/interior.d.ts +24 -0
  82. package/dist/interior.js +13 -0
  83. package/dist/layers.d.ts +35 -0
  84. package/dist/layers.js +49 -0
  85. package/dist/lens-plugin.d.ts +43 -0
  86. package/dist/lens-plugin.js +304 -0
  87. package/dist/lens.d.ts +41 -0
  88. package/dist/lens.js +99 -0
  89. package/dist/lights.d.ts +50 -5
  90. package/dist/lights.js +101 -22
  91. package/dist/material-pipelines.d.ts +37 -4
  92. package/dist/material-pipelines.js +102 -5
  93. package/dist/materials.d.ts +23 -0
  94. package/dist/materials.js +44 -1
  95. package/dist/outline-plugin.d.ts +48 -0
  96. package/dist/outline-plugin.js +529 -0
  97. package/dist/outline.d.ts +15 -0
  98. package/dist/outline.js +42 -0
  99. package/dist/overlay-registry.d.ts +3 -1
  100. package/dist/overlay-registry.js +3 -1
  101. package/dist/overlays.js +6 -2
  102. package/dist/perf-overlay.d.ts +9 -0
  103. package/dist/perf-overlay.js +125 -0
  104. package/dist/picking.d.ts +17 -0
  105. package/dist/picking.js +40 -11
  106. package/dist/pixel-perfect-plugin.js +10 -2
  107. package/dist/plugin.d.ts +69 -7
  108. package/dist/plugin.js +223 -25
  109. package/dist/post-common.d.ts +22 -2
  110. package/dist/post-common.js +82 -2
  111. package/dist/post-nodes.d.ts +7 -1
  112. package/dist/post-nodes.js +235 -111
  113. package/dist/post-plugin.js +5 -3
  114. package/dist/post-scene.d.ts +130 -0
  115. package/dist/post-scene.js +164 -0
  116. package/dist/post-shaders.js +85 -62
  117. package/dist/post.js +11 -1
  118. package/dist/projection.d.ts +24 -0
  119. package/dist/projection.js +87 -0
  120. package/dist/readback.d.ts +1 -0
  121. package/dist/readback.js +1 -0
  122. package/dist/render-scale.js +1 -0
  123. package/dist/ribbons-plugin.d.ts +5 -0
  124. package/dist/ribbons-plugin.js +59 -0
  125. package/dist/ribbons.d.ts +44 -0
  126. package/dist/ribbons.js +288 -0
  127. package/dist/scenario.d.ts +70 -0
  128. package/dist/scenario.js +125 -0
  129. package/dist/screen-effects.d.ts +97 -0
  130. package/dist/screen-effects.js +192 -0
  131. package/dist/shaders.d.ts +13 -0
  132. package/dist/shaders.js +189 -34
  133. package/dist/shadow-catcher.d.ts +9 -0
  134. package/dist/shadow-catcher.js +133 -0
  135. package/dist/shadows.d.ts +27 -2
  136. package/dist/shadows.js +100 -20
  137. package/dist/standard-fields.d.ts +1 -0
  138. package/dist/standard-fields.js +5 -0
  139. package/dist/standard.js +18 -0
  140. package/dist/stats.d.ts +45 -1
  141. package/dist/stats.js +91 -3
  142. package/dist/surface-model.d.ts +57 -0
  143. package/dist/surface-model.js +289 -0
  144. package/dist/surface-shaders.d.ts +3 -0
  145. package/dist/surface-shaders.js +160 -0
  146. package/dist/surface.d.ts +40 -0
  147. package/dist/surface.js +86 -0
  148. package/dist/target.d.ts +1 -13
  149. package/dist/target.js +1 -26
  150. package/dist/testing.d.ts +11 -0
  151. package/dist/testing.js +39 -1
  152. package/dist/tier.d.ts +13 -0
  153. package/dist/tier.js +14 -0
  154. package/dist/timer.d.ts +18 -1
  155. package/dist/timer.js +73 -20
  156. package/dist/variants.d.ts +37 -0
  157. package/dist/variants.js +104 -0
  158. package/dist/vfx-draw.d.ts +37 -0
  159. package/dist/vfx-draw.js +291 -0
  160. package/dist/vfx-state.d.ts +42 -0
  161. package/dist/vfx-state.js +81 -0
  162. package/dist/vfx.d.ts +69 -0
  163. package/dist/vfx.js +103 -0
  164. package/dist/view-visibility-plugin.d.ts +61 -0
  165. package/dist/view-visibility-plugin.js +267 -0
  166. package/dist/view-visibility.d.ts +28 -0
  167. package/dist/view-visibility.js +46 -0
  168. package/dist/view.d.ts +29 -1
  169. package/dist/view.js +57 -34
  170. package/package.json +23 -11
package/dist/shaders.js CHANGED
@@ -26,7 +26,13 @@ const ViewUniform = defineComponent("render/ViewUniform", {
26
26
  * Per directional light (column i): the atmosphere's transmittance (rgb) between the camera and
27
27
  * that sun, applied to its light (spec 0044). All ones without an atmosphere.
28
28
  */
29
- sunTransmittance: t.mat4
29
+ sunTransmittance: t.mat4,
30
+ /**
31
+ * Screen scale (0057): render pixels per CSS pixel (x); world units per render pixel at one
32
+ * unit of view depth, or everywhere when orthographic (y); 1 when orthographic (z). A CSS-pixel
33
+ * width w at view depth d is w · x · y · d world units (w · x · y orthographic).
34
+ */
35
+ pixelScale: t.vec4
30
36
  });
31
37
  const viewLayout = wgslLayout(ViewUniform);
32
38
  const materialLayout = wgslLayout(StandardMaterial);
@@ -77,9 +83,9 @@ const FLAG_VISIBLE: u32 = 1u;
77
83
  const FLAG_CASTER: u32 = 2u;
78
84
  const FLAG_RECEIVER: u32 = 4u;
79
85
 
80
- @group(2) @binding(0) var<storage, read> instances: array<Instance>;
86
+ @data @group(2) @binding(0) var<storage, read> instances: array<Instance>;
81
87
  /** The view's culled slots; a draw's instance_index indexes this. */
82
- @group(2) @binding(1) var<storage, read> visible: array<u32>;
88
+ @data @group(2) @binding(1) var<storage, read> visible: array<u32>;
83
89
 
84
90
  /** Visible-list entries: slot in the low 28 bits, LOD level in the top 4. */
85
91
  const SLOT_MASK: u32 = 0x0fffffffu;
@@ -98,7 +104,7 @@ fn instance_world(inst: Instance, position: vec3f) -> vec3f {
98
104
  }
99
105
 
100
106
  /** Last frame's transform of each slot (three affine rows), for motion vectors. */
101
- @group(2) @binding(2) var<storage, read> previous: array<vec4f>;
107
+ @data @group(2) @binding(2) var<storage, read> previous: array<vec4f>;
102
108
 
103
109
  /** Where an object-space position was last frame, in world space. */
104
110
  fn previous_world(instance_index: u32, position: vec3f) -> vec3f {
@@ -171,9 +177,9 @@ struct Deform {
171
177
  const FLAG_SKINNED: u32 = 32u;
172
178
  const FLAG_MORPH: u32 = 64u;
173
179
 
174
- @group(2) @binding(3) var<storage, read> deforms: array<Deform>;
175
- @group(2) @binding(4) var<storage, read> poses: array<vec4f>;
176
- @group(2) @binding(5) var<storage, read> deform_data: array<u32>;
180
+ @data @group(2) @binding(3) var<storage, read> deforms: array<Deform>;
181
+ @data @group(2) @binding(4) var<storage, read> poses: array<vec4f>;
182
+ @data @group(2) @binding(5) var<storage, read> deform_data: array<u32>;
177
183
 
178
184
  struct Deformed {
179
185
  position: vec3f,
@@ -360,6 +366,7 @@ import shard::view::view;
360
366
  import shard::pbr::types::VertexOutput;
361
367
  import shard::mesh::mesh_vertex_at;
362
368
  import shard::unlit::shading::shade;
369
+ @if(CUTAWAY) import shard::cutaway::cutaway_clip;
363
370
 
364
371
  @vertex fn vs(
365
372
  @builtin(instance_index) instance_index: u32,
@@ -376,9 +383,11 @@ import shard::unlit::shading::shade;
376
383
  }
377
384
 
378
385
  @fragment fn fs(in: VertexOutput) -> @location(0) vec4f {
386
+ @if(CUTAWAY) cutaway_clip(in.world_position, in.clip.xy, in.flags);
379
387
  let c = shade(in);
380
388
  @if(MASK) if (c.a < 0.5) { discard; }
381
- return vec4f(c.rgb * view.exposure, c.a);
389
+ @if(OPAQUE) return vec4f(c.rgb * view.exposure, 1.0);
390
+ @if(!OPAQUE) return vec4f(c.rgb * view.exposure, c.a);
382
391
  }`,
383
392
  "shard::pbr::brdf": `
384
393
  const PI: f32 = 3.14159265359;
@@ -416,8 +425,24 @@ struct Light {
416
425
  shadow_bias: f32,
417
426
  shadow_normal_bias: f32,
418
427
  shadow_softness: f32,
419
- _pad0: f32,
420
- _pad1: f32,
428
+ /** 0: physical; 1: tabletop (0057). */
429
+ falloff: f32,
430
+ /** Tabletop: full intensity inside this radius. */
431
+ bright: f32,
432
+ }
433
+
434
+ /**
435
+ * Distance attenuation of a point or spot light: physical inverse square windowed to zero at
436
+ * range, or tabletop (0057): full intensity inside bright, linear to zero at range.
437
+ */
438
+ fn light_falloff(light: Light, d: f32, d2: f32) -> f32 {
439
+ if (light.falloff > 0.5) {
440
+ return clamp((light.range - d) / max(light.range - light.bright, 1e-4), 0.0, 1.0);
441
+ }
442
+ let ratio = d / light.range;
443
+ let r4 = ratio * ratio * ratio * ratio;
444
+ let window = clamp(1.0 - r4, 0.0, 1.0);
445
+ return window * window / d2;
421
446
  }
422
447
 
423
448
  struct DirectionalLight {
@@ -446,10 +471,29 @@ const CLUSTER_Z: u32 = 24u;
446
471
  const CLUSTER_COUNT: u32 = 3456u;
447
472
  const MAX_PER_CLUSTER: u32 = 128u;
448
473
 
449
- @group(0) @binding(1) var<storage, read> lights: array<Light>;
474
+ @data(uniform, 128) @group(0) @binding(1) var<storage, read> lights: array<Light>;
450
475
  /** counts[CLUSTER_COUNT], then MAX_PER_CLUSTER light indices per cluster. */
451
- @group(0) @binding(2) var<storage, read> clusters: array<u32>;
452
- @group(0) @binding(3) var<storage, read> directional: DirectionalLights;`,
476
+ @if(!BASELINE) @group(0) @binding(2) var<storage, read> clusters: array<u32>;
477
+ /** Baseline (0064): a bit per packed light, 128 per cluster. */
478
+ @if(BASELINE) @data @group(0) @binding(2) var<storage, read> cluster_bits: array<vec4u>;
479
+ @data(uniform) @group(0) @binding(3) var<storage, read> directional: DirectionalLights;
480
+
481
+ /**
482
+ * Baseline (0064): takes the lowest light left in \`mask\` (a cluster's bits) into \`index\`;
483
+ * false once none are left.
484
+ */
485
+ @if(BASELINE) fn next_light(mask: ptr<function, vec4u>, index: ptr<function, u32>) -> bool {
486
+ for (var w = 0u; w < 4u; w++) {
487
+ let word = (*mask)[w];
488
+ if (word != 0u) {
489
+ let lowest = word & (~word + 1u);
490
+ (*mask)[w] = word ^ lowest;
491
+ *index = w * 32u + u32(round(log2(f32(lowest))));
492
+ return true;
493
+ }
494
+ }
495
+ return false;
496
+ }`,
453
497
  "shard::pbr::shadows": `
454
498
  import shard::color::ign;
455
499
 
@@ -473,7 +517,7 @@ struct ShadowData {
473
517
  spot_params: array<vec4f, 8>,
474
518
  }
475
519
 
476
- @group(0) @binding(4) var<storage, read> shadows: ShadowData;
520
+ @data(uniform) @group(0) @binding(4) var<storage, read> shadows: ShadowData;
477
521
  @group(0) @binding(5) var cascade_maps: texture_depth_2d_array;
478
522
  @group(0) @binding(6) var spot_maps: texture_depth_2d_array;
479
523
  @group(0) @binding(7) var point_maps: texture_depth_2d_array;
@@ -576,9 +620,11 @@ fn point_shadow(index: u32, world: vec3f, n: vec3f, bias: f32, normal_bias: f32,
576
620
  import shard::view::view;
577
621
  import shard::pbr::types::PbrInput;
578
622
  import shard::pbr::brdf::{ PI, d_ggx, v_smith_ggx_correlated, f_schlick };
579
- import shard::pbr::lights::{ lights, clusters, directional, LIGHT_SPOT, NO_SHADOW, CLUSTER_X, CLUSTER_Y, CLUSTER_Z, CLUSTER_COUNT, MAX_PER_CLUSTER };
623
+ import shard::pbr::lights::{ lights, clusters, cluster_bits, next_light, directional, light_falloff, LIGHT_SPOT, NO_SHADOW, CLUSTER_X, CLUSTER_Y, CLUSTER_Z, CLUSTER_COUNT, MAX_PER_CLUSTER };
580
624
  import shard::pbr::shadows::{ directional_shadow, spot_shadow, point_shadow, cascade_index };
581
625
  import shard::pbr::environment::environment_light;
626
+ @if(SKY_VISIBILITY) import shard::interior::interior_ambient;
627
+ @if(BLOCKED_LIGHTS) import shard::interior::{ wall_visibility, interior_receiver };
582
628
 
583
629
  const FLAG_RECEIVER: u32 = 4u;
584
630
 
@@ -618,6 +664,26 @@ fn heat(count: u32) -> vec3f {
618
664
  * Lighting stage: the only place lights are evaluated. Returns scene-referred radiance.
619
665
  * Directional lights (with cascaded shadows), then clustered point and spot lights, then ambient.
620
666
  */
667
+ /**
668
+ * SSAO at a full-resolution pixel, from the half-resolution AO and view depth (rg): the four
669
+ * nearest texels, weighted by how well their depth matches this pixel's. Without SSAO a white
670
+ * 1\xD71 texture is bound, which gives 1.
671
+ */
672
+ fn ssao_at(frag: vec2f, z: f32) -> f32 {
673
+ let half_size = vec2i(textureDimensions(ao_texture));
674
+ let base = vec2i(floor((frag - 1.0) * 0.5));
675
+ var sum = 0.0;
676
+ var weight = 0.0;
677
+ for (var k = 0; k < 4; k++) {
678
+ let q = clamp(base + vec2i(k & 1, k >> 1), vec2i(0), half_size - 1);
679
+ let s = textureLoad(ao_texture, q, 0);
680
+ let w = 1.0 / (1e-3 + abs(s.y - z) / max(z, 1e-3));
681
+ sum += s.x * w;
682
+ weight += w;
683
+ }
684
+ return sum / max(weight, 1e-6);
685
+ }
686
+
621
687
  fn apply_lighting(p: PbrInput, world_position: vec3f, frag_coord: vec4f, flags: u32) -> vec3f {
622
688
  let n = p.normal;
623
689
  let v = normalize(view.cameraPosition - world_position);
@@ -642,7 +708,39 @@ fn apply_lighting(p: PbrInput, world_position: vec3f, frag_coord: vec4f, flags:
642
708
  // Point and spot lights from this fragment's cluster: intensity (cd) / d\xB2 \xD7 window.
643
709
  let cluster = cluster_of(frag_coord, view_depth);
644
710
  var count = 0u;
645
- if (cluster >= 0) {
711
+ @if(BLOCKED_LIGHTS) let receiver = interior_receiver(world_position, n);
712
+ @if(BASELINE) if (cluster >= 0) {
713
+ // The cluster's lights, lowest first; keep the body in step with the loop below.
714
+ var mask = cluster_bits[cluster];
715
+ var index = 0u;
716
+ while (next_light(&mask, &index)) {
717
+ count++;
718
+ let light = lights[index];
719
+ let to_light = light.position - world_position;
720
+ let d2 = max(dot(to_light, to_light), 1e-4);
721
+ let d = sqrt(d2);
722
+ let l = to_light / d;
723
+ var attenuation = light_falloff(light, d, d2);
724
+ if (light.kind == LIGHT_SPOT) {
725
+ let cd = dot(-l, light.direction);
726
+ let spot = clamp(cd * light.spot_scale + light.spot_offset, 0.0, 1.0);
727
+ attenuation *= spot * spot;
728
+ }
729
+ if (attenuation <= 0.0) { continue; }
730
+ @if(BLOCKED_LIGHTS) attenuation *= wall_visibility(light, receiver);
731
+ if (receives && light.shadow != NO_SHADOW) {
732
+ if (light.kind == LIGHT_SPOT) {
733
+ attenuation *= spot_shadow(light.shadow, world_position, n, light.position, light.shadow_bias, light.shadow_normal_bias, light.shadow_softness, frag_coord.xy);
734
+ } else {
735
+ attenuation *= point_shadow(light.shadow, world_position, n, light.shadow_bias, light.shadow_normal_bias, light.shadow_softness, frag_coord.xy);
736
+ }
737
+ }
738
+ let a_light = clamp(a + light.radius / (2.0 * d), 0.0, 1.0);
739
+ let norm = (a / a_light) * (a / a_light);
740
+ color += brdf(n, v, l, a_light, diffuse_color, f0, norm) * light.color * attenuation;
741
+ }
742
+ }
743
+ @if(!BASELINE) if (cluster >= 0) {
646
744
  count = clusters[cluster];
647
745
  let base = CLUSTER_COUNT + u32(cluster) * MAX_PER_CLUSTER;
648
746
  for (var k = 0u; k < count; k++) {
@@ -651,16 +749,14 @@ fn apply_lighting(p: PbrInput, world_position: vec3f, frag_coord: vec4f, flags:
651
749
  let d2 = max(dot(to_light, to_light), 1e-4);
652
750
  let d = sqrt(d2);
653
751
  let l = to_light / d;
654
- let ratio = d / light.range;
655
- let r4 = ratio * ratio * ratio * ratio;
656
- let window = clamp(1.0 - r4, 0.0, 1.0);
657
- var attenuation = window * window / d2;
752
+ var attenuation = light_falloff(light, d, d2);
658
753
  if (light.kind == LIGHT_SPOT) {
659
754
  let cd = dot(-l, light.direction);
660
755
  let spot = clamp(cd * light.spot_scale + light.spot_offset, 0.0, 1.0);
661
756
  attenuation *= spot * spot;
662
757
  }
663
758
  if (attenuation <= 0.0) { continue; }
759
+ @if(BLOCKED_LIGHTS) attenuation *= wall_visibility(light, receiver);
664
760
  if (receives && light.shadow != NO_SHADOW) {
665
761
  if (light.kind == LIGHT_SPOT) {
666
762
  attenuation *= spot_shadow(light.shadow, world_position, n, light.position, light.shadow_bias, light.shadow_normal_bias, light.shadow_softness, frag_coord.xy);
@@ -678,14 +774,13 @@ fn apply_lighting(p: PbrInput, world_position: vec3f, frag_coord: vec4f, flags:
678
774
  // Ambient: image-based lighting from the environment, or the uniform AmbientLight (cd/m\xB2).
679
775
  // SSAO darkens it on opaque surfaces (it measures the depth buffer, which blended ones aren't in).
680
776
  var surface = p;
681
- if (p.alpha >= 0.999) {
682
- let dims = vec2i(textureDimensions(ao_texture));
683
- surface.occlusion *= textureLoad(ao_texture, min(vec2i(frag_coord.xy), dims - 1), 0).r;
684
- }
777
+ if (p.alpha >= 0.999) { surface.occlusion *= ssao_at(frag_coord.xy, view_depth); }
685
778
  if (view.envParams.w > 0.5) {
686
- color += environment_light(surface, n, v);
779
+ @if(!SKY_VISIBILITY) color += environment_light(surface, n, v);
780
+ @if(SKY_VISIBILITY) color += interior_ambient(environment_light(surface, n, v), diffuse_color * surface.occlusion, world_position, n);
687
781
  } else {
688
- color += diffuse_color * view.ambient * surface.occlusion;
782
+ @if(!SKY_VISIBILITY) color += diffuse_color * view.ambient * surface.occlusion;
783
+ @if(SKY_VISIBILITY) color += interior_ambient(diffuse_color * view.ambient * surface.occlusion, diffuse_color * surface.occlusion, world_position, n);
689
784
  }
690
785
 
691
786
  let debug = u32(view.clusterParams.w);
@@ -912,6 +1007,7 @@ import shard::mesh::mesh_vertex_at;
912
1007
  import shard::pbr::material::{ pbr_input, fragment_output };
913
1008
  import shard::pbr::standard::material;
914
1009
  import shard::pbr::lighting::apply_lighting;
1010
+ @if(CUTAWAY) import shard::cutaway::cutaway_clip;
915
1011
 
916
1012
  @vertex fn vs(
917
1013
  @builtin(instance_index) instance_index: u32,
@@ -928,12 +1024,15 @@ import shard::pbr::lighting::apply_lighting;
928
1024
  }
929
1025
 
930
1026
  @fragment fn fs(in: VertexOutput) -> @location(0) vec4f {
1027
+ @if(CUTAWAY) cutaway_clip(in.world_position, in.clip.xy, in.flags);
931
1028
  let p = pbr_input(in);
932
1029
  if (material.alphaMode == 1u && p.alpha < material.alphaCutoff) { discard; }
933
1030
  let color = (apply_lighting(p, in.world_position, in.clip, in.flags) + p.emissive) * view.exposure;
934
1031
  // Premultiplied blending expects color \xD7 alpha; additive and alpha blend multiply in hardware.
935
1032
  @if(PREMULTIPLY) return fragment_output(vec4f(color * p.alpha, p.alpha));
936
- @if(!PREMULTIPLY) return fragment_output(vec4f(color, p.alpha));
1033
+ // Opaque and masked surfaces cover their pixel, whatever their base color's alpha (0052).
1034
+ @if(OPAQUE) return vec4f(fragment_output(vec4f(color, p.alpha)).rgb, 1.0);
1035
+ @if(!PREMULTIPLY && !OPAQUE) return fragment_output(vec4f(color, p.alpha));
937
1036
  }`,
938
1037
  "shard::post::depth_resolve": `
939
1038
  @group(0) @binding(0) var depth_ms: texture_depth_multisampled_2d;
@@ -950,7 +1049,7 @@ import shard::pbr::types::PbrInput;
950
1049
  @group(0) @binding(10) var env_lut: texture_2d<f32>;
951
1050
  @group(0) @binding(11) var env_sampler: sampler;
952
1051
  /** SH9, convolved with the cosine lobe and divided by \u03C0: dot with the basis = irradiance / \u03C0. */
953
- @group(0) @binding(12) var<storage, read> env_sh: array<vec4f, 9>;
1052
+ @data(uniform) @group(0) @binding(12) var<storage, read> env_sh: array<vec4f, 9>;
954
1053
  @group(0) @binding(13) var env_source: texture_cube<f32>;
955
1054
 
956
1055
  const ENV_SPECULAR_MIPS: f32 = 5.0;
@@ -1001,10 +1100,21 @@ fn environment_light(p: PbrInput, n: vec3f, v: vec3f) -> vec3f {
1001
1100
  /** The environment's own radiance toward d (the background), in cd/m\xB2. */
1002
1101
  fn environment_background(d: vec3f) -> vec3f {
1003
1102
  return textureSampleLevel(env_source, env_sampler, env_rotate(d), 0.0).rgb * view.envParams.x;
1103
+ }`,
1104
+ "shard::post::glow": `
1105
+ /**
1106
+ * Bloom's first level over the image: mixed, conserving energy, without a threshold; added with
1107
+ * one. \`glow\`: intensity, the chain's level count, mix (1) or add (0), 0. The tonemap applies it
1108
+ * as it reads the image, and auto exposure meters the same mix.
1109
+ */
1110
+ fn add_glow(c: vec3f, level0: vec3f, glow: vec4f) -> vec3f {
1111
+ let g = level0 / glow.y;
1112
+ return select(c + g * glow.x, mix(c, g, glow.x), glow.z > 0.5);
1004
1113
  }`,
1005
1114
  "shard::post::tonemap": `
1006
1115
  import shard::color::{ linear_to_srgb, srgb_to_linear, ign, luminance };
1007
1116
  import shard::tonemap::tonemap;
1117
+ @if(BLOOM) import shard::post::glow::add_glow;
1008
1118
 
1009
1119
  struct TonemapParams {
1010
1120
  /** curve, dither, flags (1 grading, 2 vignette, 4 LUT, 8 sRGB LUT), 0. */
@@ -1020,12 +1130,16 @@ struct TonemapParams {
1020
1130
  vignette: vec4f,
1021
1131
  /** 1 / width, 1 / height of the view. */
1022
1132
  texel: vec4f,
1133
+ /** Bloom: intensity, level count, mix (1) or add (0), 0. */
1134
+ glow: vec4f,
1023
1135
  }
1024
1136
 
1025
1137
  @group(0) @binding(0) var hdr: texture_2d<f32>;
1026
1138
  @group(0) @binding(1) var<uniform> params: TonemapParams;
1027
1139
  @group(0) @binding(2) var lut: texture_2d<f32>;
1028
1140
  @group(0) @binding(3) var lut_sampler: sampler;
1141
+ /** Bloom's first level, composited here instead of in a pass of its own. */
1142
+ @if(BLOOM) @group(0) @binding(4) var bloom: texture_2d<f32>;
1029
1143
 
1030
1144
  const LIN_TO_LMS = mat3x3f(
1031
1145
  vec3f(3.90405e-1, 7.08416e-2, 2.31082e-2),
@@ -1063,8 +1177,19 @@ fn apply_lut(c: vec3f) -> vec3f {
1063
1177
  }
1064
1178
 
1065
1179
  @fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
1066
- let texel = textureLoad(hdr, vec2i(p.xy), 0);
1180
+ var texel = textureLoad(hdr, vec2i(p.xy), 0);
1181
+ @if(BLOOM) {
1182
+ let level0 = textureSampleLevel(bloom, lut_sampler, p.xy * params.texel.xy, 0.0).rgb;
1183
+ texel = vec4f(add_glow(texel.rgb, level0, params.glow), texel.a);
1184
+ }
1067
1185
  var hdr_color = max(texel.rgb, vec3f(0.0));
1186
+ @if(TRANSPARENT) var alpha = 1.0;
1187
+ @if(TRANSPARENT) {
1188
+ // Premultiplied (0052): light over less coverage raises alpha, so a glow shows over the page;
1189
+ // then the color is un-premultiplied for grading and the curve, and premultiplied again below.
1190
+ alpha = clamp(max(texel.a, max(hdr_color.r, max(hdr_color.g, hdr_color.b))), 0.0, 1.0);
1191
+ hdr_color = hdr_color / max(alpha, 1e-6);
1192
+ }
1068
1193
  let flags = params.mode.z;
1069
1194
  if ((flags & 1u) != 0u) { hdr_color = grade(hdr_color); }
1070
1195
  if ((flags & 2u) != 0u) {
@@ -1080,9 +1205,12 @@ fn apply_lut(c: vec3f) -> vec3f {
1080
1205
  let n = ign(p.xy) + ign(p.xy + vec2f(71.0, 13.0)) - 1.0;
1081
1206
  c = c + vec3f(n / 255.0);
1082
1207
  }
1208
+ // The page composites in display space, so premultiply there; it keeps rgb <= alpha.
1209
+ @if(TRANSPARENT) c = clamp(c, vec3f(0.0), vec3f(1.0)) * alpha;
1083
1210
  // sRGB targets encode in hardware: hand them linear values.
1084
1211
  @if(SRGB_TARGET) c = srgb_to_linear(clamp(c, vec3f(0.0), vec3f(1.0)));
1085
- return vec4f(c, 1.0);
1212
+ @if(TRANSPARENT) return vec4f(c, max(alpha, max(c.r, max(c.g, c.b))));
1213
+ @if(!TRANSPARENT) return vec4f(c, 1.0);
1086
1214
  }`,
1087
1215
  "shard::post::upscale": `
1088
1216
  struct Upscale {
@@ -1094,8 +1222,18 @@ struct Upscale {
1094
1222
  @group(0) @binding(1) var input_sampler: sampler;
1095
1223
  @group(0) @binding(2) var<uniform> upscale: Upscale;
1096
1224
 
1097
- fn at(uv: vec2f) -> vec3f {
1098
- return textureSampleLevel(input, input_sampler, uv, 0.0).rgb;
1225
+ @if(!TRANSPARENT) alias Texel = vec3f;
1226
+ @if(TRANSPARENT) alias Texel = vec4f;
1227
+
1228
+ fn at(uv: vec2f) -> Texel {
1229
+ @if(!TRANSPARENT) return textureSampleLevel(input, input_sampler, uv, 0.0).rgb;
1230
+ @if(TRANSPARENT) return textureSampleLevel(input, input_sampler, uv, 0.0);
1231
+ }
1232
+
1233
+ /** Opaque views write alpha 1; premultiplied ones keep rgb <= alpha after sharpening (0052). */
1234
+ fn out(c: Texel) -> vec4f {
1235
+ @if(!TRANSPARENT) return vec4f(c, 1.0);
1236
+ @if(TRANSPARENT) return vec4f(c.rgb, max(c.a, max(c.r, max(c.g, c.b))));
1099
1237
  }
1100
1238
 
1101
1239
  /**
@@ -1106,7 +1244,7 @@ fn at(uv: vec2f) -> vec3f {
1106
1244
  let uv = frag.xy * upscale.params.xy;
1107
1245
  let c = at(uv);
1108
1246
  let k = upscale.params.z;
1109
- if (k <= 0.0) { return vec4f(c, 1.0); }
1247
+ if (k <= 0.0) { return out(c); }
1110
1248
  let texel = 1.0 / vec2f(textureDimensions(input));
1111
1249
  let n = at(uv + vec2f(0.0, -texel.y));
1112
1250
  let s = at(uv + vec2f(0.0, texel.y));
@@ -1115,7 +1253,7 @@ fn at(uv: vec2f) -> vec3f {
1115
1253
  let lo = min(c, min(min(n, s), min(e, w)));
1116
1254
  let hi = max(c, max(max(n, s), max(e, w)));
1117
1255
  let sharp = c + (4.0 * c - (n + s + e + w)) * (0.25 * k);
1118
- return vec4f(clamp(sharp, lo, hi), 1.0);
1256
+ return out(clamp(sharp, lo, hi));
1119
1257
  }`,
1120
1258
  "shard::post::common": `
1121
1259
  import shard::view::view;
@@ -1132,6 +1270,23 @@ fn world_at(uv: vec2f, depth: f32) -> vec3f {
1132
1270
  return w.xyz / w.w;
1133
1271
  }
1134
1272
 
1273
+ /**
1274
+ * Rounds to nearest on a device that truncates when it stores rg11b10ufloat (Metal does): half a
1275
+ * step of each channel's own exponent (6 mantissa bits in red and green, 5 in blue) added before
1276
+ * the store. Without it every pass through the format loses half a step on average, blue twice
1277
+ * as much, and the image drifts warm.
1278
+ */
1279
+ fn rg11_round(c: vec3f) -> vec3f {
1280
+ let e = exp2(floor(log2(max(c, vec3f(6.2e-5)))));
1281
+ return c + e * vec3f(1.0 / 128.0, 1.0 / 128.0, 1.0 / 64.0);
1282
+ }
1283
+
1284
+ /** A post pass's output into the chain: rounded where it's rg11b10ufloat on a truncating device. */
1285
+ fn post_out(c: vec4f) -> vec4f {
1286
+ @if(RG11_ROUND) return vec4f(rg11_round(c.rgb), c.a);
1287
+ @if(!RG11_ROUND) return c;
1288
+ }
1289
+
1135
1290
  /** Distance along the view axis, in meters. */
1136
1291
  fn view_depth(world: vec3f) -> f32 {
1137
1292
  return -(view.view * vec4f(world, 1.0)).z;
@@ -0,0 +1,9 @@
1
+ export declare const SHADOW_CATCHER_SHADERS: Record<string, string>;
2
+ /**
3
+ * Invisible except for the shadows it receives (0052): color 0, alpha `opacity × (1 − visibility)`
4
+ * over every shadow-casting light. Premultiplied, so it darkens what's behind it by that alpha.
5
+ * Needs `shadowCatcherPlugin` (in `forwardPlugin`).
6
+ */
7
+ export declare const ShadowCatcher: import("./materials.js").MaterialType;
8
+ /** The ShadowCatcher material's shader. */
9
+ export declare const shadowCatcherPlugin: import("@aethervtt/shard-runtime").Plugin;
@@ -0,0 +1,133 @@
1
+ import { t } from "@aethervtt/shard-core";
2
+ import { definePlugin } from "@aethervtt/shard-runtime";
3
+ import { defineMaterial } from "./materials.js";
4
+ import { Shaders } from "./plugin.js";
5
+ import { registerShaders } from "./shaders.js";
6
+ const SHADOW_CATCHER_SHADERS = {
7
+ "shard::pbr::catcher": `
8
+ import shard::view::view;
9
+ import shard::pbr::types::VertexOutput;
10
+ import shard::pbr::lights::{ lights, clusters, cluster_bits, next_light, directional, light_falloff, LIGHT_SPOT, NO_SHADOW, CLUSTER_COUNT, MAX_PER_CLUSTER };
11
+ import shard::pbr::shadows::{ directional_shadow, spot_shadow, point_shadow };
12
+ import shard::pbr::lighting::cluster_of;
13
+
14
+ const FLAG_RECEIVER: u32 = 4u;
15
+
16
+ fn brightest(c: vec3f) -> f32 { return max(c.r, max(c.g, c.b)); }
17
+
18
+ /**
19
+ * How much of the shadow-casting light reaches the surface: the light of every shadow-casting
20
+ * light with shadows, over that light without them. 1 when none cast shadows, or when the mesh
21
+ * doesn't receive them. Shadow catchers (and the dice tray, 0054) turn 1 \u2212 this into alpha.
22
+ */
23
+ fn shadow_visibility(in: VertexOutput) -> f32 {
24
+ if ((in.flags & FLAG_RECEIVER) == 0u) { return 1.0; }
25
+ let n = normalize(in.world_normal);
26
+ let p = in.world_position;
27
+ let view_depth = -(view.view * vec4f(p, 1.0)).z;
28
+ var lit = 0.0;
29
+ var total = 0.0;
30
+ for (var i = 0u; i < directional.count; i++) {
31
+ let light = directional.lights[i];
32
+ if (light.shadowed == 0u) { continue; }
33
+ let e = max(dot(n, normalize(light.direction)), 0.0) * brightest(light.color);
34
+ total += e;
35
+ lit += e * directional_shadow(p, n, view_depth, in.clip.xy);
36
+ }
37
+ let cluster = cluster_of(in.clip, view_depth);
38
+ // Baseline (0064): the cluster's bits; keep the body in step with the loop below.
39
+ @if(BASELINE) if (cluster >= 0) {
40
+ var mask = cluster_bits[cluster];
41
+ var index = 0u;
42
+ while (next_light(&mask, &index)) {
43
+ let light = lights[index];
44
+ if (light.shadow == NO_SHADOW) { continue; }
45
+ let to_light = light.position - p;
46
+ let d2 = max(dot(to_light, to_light), 1e-4);
47
+ let d = sqrt(d2);
48
+ let l = to_light / d;
49
+ var attenuation = light_falloff(light, d, d2);
50
+ if (light.kind == LIGHT_SPOT) {
51
+ let spot = clamp(dot(-l, light.direction) * light.spot_scale + light.spot_offset, 0.0, 1.0);
52
+ attenuation *= spot * spot;
53
+ }
54
+ let e = max(dot(n, l), 0.0) * attenuation * brightest(light.color);
55
+ if (e <= 0.0) { continue; }
56
+ var shadow = 1.0;
57
+ if (light.kind == LIGHT_SPOT) {
58
+ shadow = spot_shadow(light.shadow, p, n, light.position, light.shadow_bias, light.shadow_normal_bias, light.shadow_softness, in.clip.xy);
59
+ } else {
60
+ shadow = point_shadow(light.shadow, p, n, light.shadow_bias, light.shadow_normal_bias, light.shadow_softness, in.clip.xy);
61
+ }
62
+ total += e;
63
+ lit += e * shadow;
64
+ }
65
+ }
66
+ @if(!BASELINE) if (cluster >= 0) {
67
+ let count = clusters[cluster];
68
+ let base = CLUSTER_COUNT + u32(cluster) * MAX_PER_CLUSTER;
69
+ for (var k = 0u; k < count; k++) {
70
+ let light = lights[clusters[base + k]];
71
+ if (light.shadow == NO_SHADOW) { continue; }
72
+ let to_light = light.position - p;
73
+ let d2 = max(dot(to_light, to_light), 1e-4);
74
+ let d = sqrt(d2);
75
+ let l = to_light / d;
76
+ var attenuation = light_falloff(light, d, d2);
77
+ if (light.kind == LIGHT_SPOT) {
78
+ let spot = clamp(dot(-l, light.direction) * light.spot_scale + light.spot_offset, 0.0, 1.0);
79
+ attenuation *= spot * spot;
80
+ }
81
+ let e = max(dot(n, l), 0.0) * attenuation * brightest(light.color);
82
+ if (e <= 0.0) { continue; }
83
+ var shadow = 1.0;
84
+ if (light.kind == LIGHT_SPOT) {
85
+ shadow = spot_shadow(light.shadow, p, n, light.position, light.shadow_bias, light.shadow_normal_bias, light.shadow_softness, in.clip.xy);
86
+ } else {
87
+ shadow = point_shadow(light.shadow, p, n, light.shadow_bias, light.shadow_normal_bias, light.shadow_softness, in.clip.xy);
88
+ }
89
+ total += e;
90
+ lit += e * shadow;
91
+ }
92
+ }
93
+ return select(1.0, lit / total, total > 0.0);
94
+ }`,
95
+ "shard::material::shadow_catcher": `
96
+ import shard::pbr::types::VertexOutput;
97
+ import shard::pbr::catcher::shadow_visibility;
98
+ import material::shadow_catcher::ShadowCatcher;
99
+
100
+ /** No color; alpha opacity \xD7 (1 \u2212 visibility) over every shadow-casting light. */
101
+ override fn shade(in: VertexOutput) -> vec4f {
102
+ return vec4f(0.0, 0.0, 0.0, ShadowCatcher.opacity * (1.0 - shadow_visibility(in)));
103
+ }`
104
+ };
105
+ const ShadowCatcher = defineMaterial("render/ShadowCatcher", {
106
+ extends: "none",
107
+ blend: "premultiplied",
108
+ fields: {
109
+ opacity: t.f32({
110
+ default: 0.6,
111
+ min: 0,
112
+ max: 1,
113
+ description: "Alpha of full shadow. Partial shadow (penumbra, one of two lights) is less."
114
+ })
115
+ },
116
+ shader: "shard::material::shadow_catcher",
117
+ description: "Invisible except for the shadows it receives: under a camera clearing to alpha 0 they fall on the page (a dice tray floor). Give its mesh NotShadowCaster."
118
+ });
119
+ const shadowCatcherPlugin = definePlugin({
120
+ name: "render/shadow-catcher",
121
+ dependencies: ["render/forward"],
122
+ provides: [ShadowCatcher],
123
+ build() {
124
+ },
125
+ ready(app) {
126
+ registerShaders(app.world.resource(Shaders), SHADOW_CATCHER_SHADERS);
127
+ }
128
+ });
129
+ export {
130
+ SHADOW_CATCHER_SHADERS,
131
+ ShadowCatcher,
132
+ shadowCatcherPlugin
133
+ };
package/dist/shadows.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { type World } from '@aethervtt/shard-core';
2
2
  import { GpuBuffer, type GpuContext } from '@aethervtt/shard-gpu';
3
+ import type { FoliageSupport } from './foliage-path.js';
3
4
  import type { NodeContext } from './graph.js';
4
- import { type DrawList } from './instances.js';
5
+ import { type DrawList, type InstanceStore } from './instances.js';
5
6
  import { type LightRecord, type LightStore } from './lights.js';
6
7
  import { type MaterialPipelines } from './material-pipelines.js';
7
8
  import type { CameraData } from './view.js';
@@ -15,10 +16,28 @@ export declare function pointTanHalf(size: number): number;
15
16
  export interface ShadowViewDraw {
16
17
  viewProj: Float32Array;
17
18
  frustum: Float32Array;
19
+ /** What casters are culled against: the frustum, but open toward the light for cascades. */
20
+ cullPlanes: Float32Array;
18
21
  draws: DrawList;
19
22
  /** Dynamic offset of this view's matrix in the frame's shadow uniforms. */
20
23
  offset: number;
24
+ /** Whether its layer holds a complete draw (0055: cached shadows keep it). */
25
+ drawn: boolean;
26
+ /** The matrix and `InstanceStore.shadowEpoch` it was drawn with. */
27
+ drawnViewProj: Float32Array;
28
+ drawnEpoch: number;
29
+ /** Set by the queue: draw it this frame. */
30
+ redraw: boolean;
21
31
  }
32
+ /**
33
+ * Whether a shadow view must draw this frame (0055). Views of `shadowUpdate: 'always'` lights
34
+ * always do. A cached view draws when its layer isn't complete, its matrix changed (the light or
35
+ * the cascade's fit moved), what casts shadows changed (`shadowEpoch`), poses deform, or a
36
+ * caster inside its cull planes moved.
37
+ */
38
+ export declare function shadowRedraw(view: ShadowViewDraw, cached: boolean, store: InstanceStore): boolean;
39
+ /** Records that a shadow view's layer now holds a complete draw of this frame's state. */
40
+ export declare function markShadowDrawn(view: ShadowViewDraw, store: InstanceStore): void;
22
41
  /** A camera's cascades: fitted each frame, one depth layer each. */
23
42
  export declare class Cascades {
24
43
  count: number;
@@ -98,11 +117,17 @@ export declare class ShadowPassUniforms {
98
117
  bindGroup: GPUBindGroup | undefined;
99
118
  private bound;
100
119
  private readonly gpu;
120
+ /** What the buffer holds, so unchanged shadow views write nothing (0055). */
121
+ private sent;
122
+ private sentCount;
123
+ private sentVersion;
124
+ private sentGeneration;
101
125
  constructor(gpu: GpuContext);
102
126
  reset(): void;
103
127
  /** Adds a view matrix; returns its dynamic offset. */
104
128
  push(viewProj: Float32Array): number;
105
129
  upload(layout: GPUBindGroupLayout, globals: GpuBuffer): void;
130
+ private unchanged;
106
131
  }
107
132
  export declare function shadowViewLayout(gpu: GpuContext): GPUBindGroupLayout;
108
133
  /**
@@ -110,7 +135,7 @@ export declare function shadowViewLayout(gpu: GpuContext): GPUBindGroupLayout;
110
135
  * `vertex_position` displacement moves shadows too). Opaque materials are vertex-only; masked
111
136
  * standard materials run the surface stage to discard.
112
137
  */
113
- export declare function drawShadowCasters(ctx: NodeContext, target: GPUTextureView, view: ShadowViewDraw, offset: number, uniforms: ShadowPassUniforms, pipelines: MaterialPipelines, shadowView: GPUBindGroupLayout, label: string): void;
138
+ export declare function drawShadowCasters(ctx: NodeContext, target: GPUTextureView, view: ShadowViewDraw, offset: number, uniforms: ShadowPassUniforms, pipelines: MaterialPipelines, shadowView: GPUBindGroupLayout, label: string, foliage?: FoliageSupport, cam?: CameraData, cascade?: number): void;
114
139
  /** Culls instances that cast shadows into a shadow view, and unions their world bounds. */
115
140
  export declare function cullCasters(world: World, view: ShadowViewDraw, planes: Float32Array, box?: Float32Array): boolean;
116
141
  export declare const ShadowsResource: import("@aethervtt/shard-core").ResourceDef<{