@ifc-lite/renderer 4.2.0 → 5.0.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 (74) hide show
  1. package/dist/edge-params.d.ts +43 -0
  2. package/dist/edge-params.d.ts.map +1 -0
  3. package/dist/edge-params.js +63 -0
  4. package/dist/edge-params.js.map +1 -0
  5. package/dist/edge-pass.d.ts +38 -0
  6. package/dist/edge-pass.d.ts.map +1 -0
  7. package/dist/edge-pass.js +111 -0
  8. package/dist/edge-pass.js.map +1 -0
  9. package/dist/index.d.ts +29 -14
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +56 -34
  12. package/dist/index.js.map +1 -1
  13. package/dist/instanced-render.d.ts +3 -3
  14. package/dist/instanced-render.js +3 -3
  15. package/dist/mesh-material.d.ts +26 -0
  16. package/dist/mesh-material.d.ts.map +1 -0
  17. package/dist/mesh-material.js +52 -0
  18. package/dist/mesh-material.js.map +1 -0
  19. package/dist/mesh-rte-uniforms.d.ts +1 -0
  20. package/dist/mesh-rte-uniforms.d.ts.map +1 -1
  21. package/dist/mesh-rte-uniforms.js +1 -0
  22. package/dist/mesh-rte-uniforms.js.map +1 -1
  23. package/dist/overlay-theme-uniforms.d.ts +54 -0
  24. package/dist/overlay-theme-uniforms.d.ts.map +1 -0
  25. package/dist/overlay-theme-uniforms.js +89 -0
  26. package/dist/overlay-theme-uniforms.js.map +1 -0
  27. package/dist/overlay-theme.d.ts +73 -0
  28. package/dist/overlay-theme.d.ts.map +1 -0
  29. package/dist/overlay-theme.js +24 -0
  30. package/dist/overlay-theme.js.map +1 -0
  31. package/dist/pipeline.d.ts +2 -0
  32. package/dist/pipeline.d.ts.map +1 -1
  33. package/dist/pipeline.js +10 -11
  34. package/dist/pipeline.js.map +1 -1
  35. package/dist/post-pass-chain.d.ts +1 -1
  36. package/dist/post-pass-chain.d.ts.map +1 -1
  37. package/dist/post-pass-chain.js +19 -13
  38. package/dist/post-pass-chain.js.map +1 -1
  39. package/dist/renderer-overlays.d.ts +8 -7
  40. package/dist/renderer-overlays.d.ts.map +1 -1
  41. package/dist/renderer-overlays.js +11 -14
  42. package/dist/renderer-overlays.js.map +1 -1
  43. package/dist/scene.d.ts +10 -1
  44. package/dist/scene.d.ts.map +1 -1
  45. package/dist/scene.js.map +1 -1
  46. package/dist/section-plane.d.ts +3 -0
  47. package/dist/section-plane.d.ts.map +1 -1
  48. package/dist/section-plane.js +15 -24
  49. package/dist/section-plane.js.map +1 -1
  50. package/dist/shaders/color-transfer.wgsl.d.ts +5 -3
  51. package/dist/shaders/color-transfer.wgsl.d.ts.map +1 -1
  52. package/dist/shaders/color-transfer.wgsl.js +5 -3
  53. package/dist/shaders/color-transfer.wgsl.js.map +1 -1
  54. package/dist/shaders/edges.wgsl.d.ts +2 -0
  55. package/dist/shaders/edges.wgsl.d.ts.map +1 -0
  56. package/dist/shaders/edges.wgsl.js +158 -0
  57. package/dist/shaders/edges.wgsl.js.map +1 -0
  58. package/dist/shaders/main.wgsl.d.ts +1 -1
  59. package/dist/shaders/main.wgsl.d.ts.map +1 -1
  60. package/dist/shaders/main.wgsl.js +81 -77
  61. package/dist/shaders/main.wgsl.js.map +1 -1
  62. package/dist/shaders/specular.wgsl.d.ts +34 -0
  63. package/dist/shaders/specular.wgsl.d.ts.map +1 -0
  64. package/dist/shaders/specular.wgsl.js +134 -0
  65. package/dist/shaders/specular.wgsl.js.map +1 -0
  66. package/dist/visual-enhancement.d.ts +10 -3
  67. package/dist/visual-enhancement.d.ts.map +1 -1
  68. package/dist/visual-enhancement.js +9 -3
  69. package/dist/visual-enhancement.js.map +1 -1
  70. package/package.json +2 -2
  71. package/dist/post-processor.d.ts +0 -46
  72. package/dist/post-processor.d.ts.map +0 -1
  73. package/dist/post-processor.js +0 -295
  74. package/dist/post-processor.js.map +0 -1
@@ -26,11 +26,20 @@ export class SectionPlaneRenderer {
26
26
  format;
27
27
  sampleCount;
28
28
  initialized = false;
29
+ // One accent tint for every axis and for face-picked planes (#5484):
30
+ // set by `Renderer.setOverlayTheme`. Reproduces the historic "down"-axis
31
+ // colour (#03A9F4) until a theme is pushed, so a caller that never calls
32
+ // `setOverlayTheme` sees no visual change.
33
+ planeColor = [0.012, 0.663, 0.957, 1];
29
34
  constructor(device, format, sampleCount = 4) {
30
35
  this.device = device;
31
36
  this.format = format;
32
37
  this.sampleCount = sampleCount;
33
38
  }
39
+ /** Set the accent tint used for the preview plane, every axis alike (#5484). RGBA 0..1. */
40
+ setPlaneColor(color) {
41
+ this.planeColor = color;
42
+ }
34
43
  init() {
35
44
  if (this.initialized)
36
45
  return;
@@ -274,30 +283,12 @@ export class SectionPlaneRenderer {
274
283
  else {
275
284
  uniforms.set(viewProj, SECTION_PLANE_UNIFORM_SLOTS.rteViewProj);
276
285
  }
277
- // Axis-specific colors for better identification.
278
- // down (Y) = light blue, front (Z) = green, side (X) = orange.
279
- // Custom (face-picked) planes pick up a violet that won't be confused
280
- // with any cardinal preset.
281
- if (hasExplicitPlane) {
282
- uniforms[SECTION_PLANE_UNIFORM_SLOTS.planeColor] = 0.612; // R - #9C6BDE (violet)
283
- uniforms[SECTION_PLANE_UNIFORM_SLOTS.planeColor + 1] = 0.420;
284
- uniforms[SECTION_PLANE_UNIFORM_SLOTS.planeColor + 2] = 0.871;
285
- }
286
- else if (axis === 'down') {
287
- uniforms[SECTION_PLANE_UNIFORM_SLOTS.planeColor] = 0.012; // R - #03A9F4
288
- uniforms[SECTION_PLANE_UNIFORM_SLOTS.planeColor + 1] = 0.663; // G
289
- uniforms[SECTION_PLANE_UNIFORM_SLOTS.planeColor + 2] = 0.957; // B
290
- }
291
- else if (axis === 'front') {
292
- uniforms[SECTION_PLANE_UNIFORM_SLOTS.planeColor] = 0.298; // R - #4CAF50
293
- uniforms[SECTION_PLANE_UNIFORM_SLOTS.planeColor + 1] = 0.686; // G
294
- uniforms[SECTION_PLANE_UNIFORM_SLOTS.planeColor + 2] = 0.314; // B
295
- }
296
- else {
297
- uniforms[SECTION_PLANE_UNIFORM_SLOTS.planeColor] = 1.0; // R - #FF9800
298
- uniforms[SECTION_PLANE_UNIFORM_SLOTS.planeColor + 1] = 0.596; // G
299
- uniforms[SECTION_PLANE_UNIFORM_SLOTS.planeColor + 2] = 0.0; // B
300
- }
286
+ // One accent tint for every axis, and for face-picked (custom) planes
287
+ // alike (#5484) — set by `Renderer.setOverlayTheme`, no more per-axis
288
+ // Material colours or the custom violet.
289
+ uniforms[SECTION_PLANE_UNIFORM_SLOTS.planeColor] = this.planeColor[0];
290
+ uniforms[SECTION_PLANE_UNIFORM_SLOTS.planeColor + 1] = this.planeColor[1];
291
+ uniforms[SECTION_PLANE_UNIFORM_SLOTS.planeColor + 2] = this.planeColor[2];
301
292
  // Preview mode opacity
302
293
  uniforms[SECTION_PLANE_UNIFORM_SLOTS.planeColor + 3] = 0.25;
303
294
  this.device.queue.writeBuffer(this.uniformBuffer, 0, uniforms);
@@ -1 +1 @@
1
- {"version":3,"file":"section-plane.js","sourceRoot":"","sources":["../src/section-plane.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"section-plane.js","sourceRoot":"","sources":["../src/section-plane.ts"],"names":[],"mappings":"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"}
@@ -16,9 +16,11 @@
16
16
  * irradiance, renders as authored; brighter colours roll off with their
17
17
  * hue preserved (pure white lands at 0.88, about 241/255, leaving
18
18
  * headroom for sunlit highlights). It is the Khronos PBR Neutral operator
19
- * without its toe: the toe subtracts up to 0.04 to cancel a specular F0
20
- * offset, and this renderer has no specular term, so keeping it would only
21
- * crush dark materials.
19
+ * without its toe: the toe subtracts up to 0.04 to cancel the specular F0
20
+ * offset a dielectric picks up under a unit-radiance environment. This
21
+ * renderer's sky is far dimmer, so its specular term (specular.wgsl.ts)
22
+ * lifts a dark matte surface by only about 0.01-0.02, and keeping the toe
23
+ * would crush dark materials below their authored value.
22
24
  * - `linearToSrgb` is the exact piecewise encode. The canvas is configured
23
25
  * with a non-sRGB format, so the shader must encode.
24
26
  */
@@ -1 +1 @@
1
- {"version":3,"file":"color-transfer.wgsl.d.ts","sourceRoot":"","sources":["../../src/shaders/color-transfer.wgsl.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,iBAAiB,8gCAwB7B,CAAC"}
1
+ {"version":3,"file":"color-transfer.wgsl.d.ts","sourceRoot":"","sources":["../../src/shaders/color-transfer.wgsl.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,iBAAiB,8gCAwB7B,CAAC"}
@@ -19,9 +19,11 @@
19
19
  * irradiance, renders as authored; brighter colours roll off with their
20
20
  * hue preserved (pure white lands at 0.88, about 241/255, leaving
21
21
  * headroom for sunlit highlights). It is the Khronos PBR Neutral operator
22
- * without its toe: the toe subtracts up to 0.04 to cancel a specular F0
23
- * offset, and this renderer has no specular term, so keeping it would only
24
- * crush dark materials.
22
+ * without its toe: the toe subtracts up to 0.04 to cancel the specular F0
23
+ * offset a dielectric picks up under a unit-radiance environment. This
24
+ * renderer's sky is far dimmer, so its specular term (specular.wgsl.ts)
25
+ * lifts a dark matte surface by only about 0.01-0.02, and keeping the toe
26
+ * would crush dark materials below their authored value.
25
27
  * - `linearToSrgb` is the exact piecewise encode. The canvas is configured
26
28
  * with a non-sRGB format, so the shader must encode.
27
29
  */
@@ -1 +1 @@
1
- {"version":3,"file":"color-transfer.wgsl.js","sourceRoot":"","sources":["../../src/shaders/color-transfer.wgsl.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAE/D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;CAwBhC,CAAC"}
1
+ {"version":3,"file":"color-transfer.wgsl.js","sourceRoot":"","sources":["../../src/shaders/color-transfer.wgsl.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAE/D;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;CAwBhC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function edgeShaderSource(multisampled: boolean): string;
2
+ //# sourceMappingURL=edges.wgsl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edges.wgsl.d.ts","sourceRoot":"","sources":["../../src/shaders/edges.wgsl.ts"],"names":[],"mappings":"AA0BA,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,OAAO,GAAG,MAAM,CAqI9D"}
@@ -0,0 +1,158 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+ /**
5
+ * Edge pass (#5385): a single fullscreen pass that darkens along entity-id
6
+ * changes, normal creases, and depth silhouettes, replacing the separation-
7
+ * line pass in `post-processor.ts`.
8
+ *
9
+ * View-space position and normal come from `depth-reconstruct.wgsl.ts`
10
+ * (shared with the ambient-occlusion pass, #5384: one reconstruction, not
11
+ * two). The entity-id attachment is decoded the same way the old
12
+ * separation-line pass did (`encodeId24`/`decodeId24` in `main.wgsl.ts`).
13
+ *
14
+ * Each of `directions` tap offsets (4 cardinal at `low`, +4 diagonal at
15
+ * `high`) votes 0/1 on three cues — id change, normal crease, depth
16
+ * silhouette — and the pass darkens by the AVERAGE vote across directions.
17
+ * That average is a coverage estimate, not a single hard sample, so a line
18
+ * that only grazes one direction fades instead of dashing on/off between
19
+ * frames of the same seam (the bug this pass replaces, `post-processor.ts`
20
+ * introduced its own version of this coverage trick for the id test alone;
21
+ * this generalises it to all three cues and both new tests).
22
+ */
23
+ import { EDGE_CREASE_COS, EDGE_MAX_DARKEN, EDGE_SILHOUETTE_REL_THRESHOLD } from '../edge-params.js';
24
+ import { depthReconstructWgsl, depthTextureWgsl } from './depth-reconstruct.wgsl.js';
25
+ export function edgeShaderSource(multisampled) {
26
+ // Multisampled and non-multisampled `textureLoad` share this call shape:
27
+ // the third argument is the sample index (MS) or the mip level (non-MS),
28
+ // both 0, so one call site covers both declarations (`post-processor.ts`
29
+ // used the same trick).
30
+ const idTexDecl = multisampled
31
+ ? '@group(0) @binding(1) var idTex: texture_multisampled_2d<f32>;'
32
+ : '@group(0) @binding(1) var idTex: texture_2d<f32>;';
33
+ return `
34
+ struct EdgeParams {
35
+ depthParams: vec4<f32>, // m10, m11, m14, m15
36
+ xyParams: vec4<f32>, // m0, m5, m12, m13
37
+ viewport: vec4<f32>, // full-res width, height, 1/width, 1/height
38
+ edge: vec4<f32>, // tap radius px, intensity, highQuality (0/1), _pad
39
+ }
40
+
41
+ ${depthTextureWgsl(0, multisampled)}
42
+ ${idTexDecl}
43
+ @group(0) @binding(2) var<uniform> params: EdgeParams;
44
+
45
+ ${depthReconstructWgsl}
46
+
47
+ const EDGE_CREASE_COS: f32 = ${EDGE_CREASE_COS.toFixed(6)};
48
+ const EDGE_SILHOUETTE_REL: f32 = ${EDGE_SILHOUETTE_REL_THRESHOLD.toFixed(6)};
49
+ const EDGE_MAX_DARKEN: f32 = ${EDGE_MAX_DARKEN.toFixed(3)};
50
+
51
+ struct VsOut {
52
+ @builtin(position) pos: vec4<f32>,
53
+ }
54
+
55
+ @vertex
56
+ fn vs_fullscreen(@builtin(vertex_index) v: u32) -> VsOut {
57
+ var p = array<vec2<f32>, 3>(
58
+ vec2<f32>(-1.0, -3.0),
59
+ vec2<f32>(-1.0, 1.0),
60
+ vec2<f32>( 3.0, 1.0)
61
+ );
62
+ var o: VsOut;
63
+ o.pos = vec4<f32>(p[v], 0.0, 1.0);
64
+ return o;
65
+ }
66
+
67
+ fn decodeId24(encoded: vec4<f32>) -> u32 {
68
+ let r = u32(round(encoded.r * 255.0)) & 255u;
69
+ let g = u32(round(encoded.g * 255.0)) & 255u;
70
+ let b = u32(round(encoded.b * 255.0)) & 255u;
71
+ return (r << 16u) | (g << 8u) | b;
72
+ }
73
+
74
+ fn loadId(ip: vec2<i32>, dims: vec2<i32>) -> u32 {
75
+ let c = clamp(ip, vec2<i32>(0), dims - 1);
76
+ return decodeId24(textureLoad(idTex, c, 0));
77
+ }
78
+
79
+ fn viewPosAt(ip: vec2<i32>, d: f32) -> vec3<f32> {
80
+ return viewPositionFromDepth(ndcFromPixel(ip, params.viewport.zw), d, params.depthParams, params.xyParams);
81
+ }
82
+
83
+ // One tap direction's edge vote in [0, 1]: 1 if the id changes, the
84
+ // local normal creases past the angle threshold, or the view-space
85
+ // depth steps past the silhouette threshold.
86
+ fn tapVote(p: vec2<i32>, q: vec2<i32>, dims: vec2<i32>, centerId: u32, centerP: vec3<f32>, centerN: vec3<f32>) -> f32 {
87
+ let qc = clamp(q, vec2<i32>(0), dims - 1);
88
+ let dq = loadDepth(qc);
89
+ if (dq <= BACKGROUND_DEPTH) {
90
+ return 0.0;
91
+ }
92
+ let neighbourId = loadId(qc, dims);
93
+ let idVote = f32(neighbourId != centerId && neighbourId != 0u && centerId != 0u);
94
+
95
+ let Pq = viewPosAt(qc, dq);
96
+ let depthRef = max(-centerP.z, 1e-4);
97
+ let silhouetteVote = f32(abs(Pq.z - centerP.z) / depthRef > EDGE_SILHOUETTE_REL);
98
+
99
+ // Neighbour normal from ITS OWN neighbours (not just the two
100
+ // points), so a grazing coplanar surface (large positional slope,
101
+ // near-zero normal change) does not misread as a crease.
102
+ let dLeft = loadDepth(clamp(qc + vec2<i32>(-1, 0), vec2<i32>(0), dims - 1));
103
+ let dRight = loadDepth(clamp(qc + vec2<i32>(1, 0), vec2<i32>(0), dims - 1));
104
+ let dUp = loadDepth(clamp(qc + vec2<i32>(0, -1), vec2<i32>(0), dims - 1));
105
+ let dDown = loadDepth(clamp(qc + vec2<i32>(0, 1), vec2<i32>(0), dims - 1));
106
+ let Nq = normalFromNeighbours(
107
+ Pq,
108
+ select(centerP, viewPosAt(qc + vec2<i32>(1, 0), dRight), dRight > BACKGROUND_DEPTH),
109
+ select(centerP, viewPosAt(qc + vec2<i32>(-1, 0), dLeft), dLeft > BACKGROUND_DEPTH),
110
+ select(centerP, viewPosAt(qc + vec2<i32>(0, 1), dDown), dDown > BACKGROUND_DEPTH),
111
+ select(centerP, viewPosAt(qc + vec2<i32>(0, -1), dUp), dUp > BACKGROUND_DEPTH),
112
+ );
113
+ let creaseVote = f32(dot(centerN, Nq) < EDGE_CREASE_COS);
114
+
115
+ return max(idVote, max(creaseVote, silhouetteVote));
116
+ }
117
+
118
+ @fragment
119
+ fn fs_edges(@builtin(position) fragPos: vec4<f32>) -> @location(0) vec4<f32> {
120
+ let dims = vec2<i32>(params.viewport.xy);
121
+ let p = vec2<i32>(fragPos.xy);
122
+ let d = loadDepth(p);
123
+ if (d <= BACKGROUND_DEPTH) {
124
+ return vec4<f32>(0.0, 0.0, 0.0, 0.0);
125
+ }
126
+ let centerId = loadId(p, dims);
127
+ let centerP = viewPosAt(p, d);
128
+ let centerN = normalFromNeighbours(
129
+ centerP,
130
+ viewPosAt(clamp(p + vec2<i32>(1, 0), vec2<i32>(0), dims - 1), loadDepth(p + vec2<i32>(1, 0))),
131
+ viewPosAt(clamp(p - vec2<i32>(1, 0), vec2<i32>(0), dims - 1), loadDepth(p - vec2<i32>(1, 0))),
132
+ viewPosAt(clamp(p + vec2<i32>(0, 1), vec2<i32>(0), dims - 1), loadDepth(p + vec2<i32>(0, 1))),
133
+ viewPosAt(clamp(p - vec2<i32>(0, 1), vec2<i32>(0), dims - 1), loadDepth(p - vec2<i32>(0, 1))),
134
+ );
135
+
136
+ let r = i32(round(params.edge.x));
137
+ var votes = 0.0;
138
+ votes = votes + tapVote(p, p + vec2<i32>( r, 0), dims, centerId, centerP, centerN);
139
+ votes = votes + tapVote(p, p + vec2<i32>(-r, 0), dims, centerId, centerP, centerN);
140
+ votes = votes + tapVote(p, p + vec2<i32>( 0, r), dims, centerId, centerP, centerN);
141
+ votes = votes + tapVote(p, p + vec2<i32>( 0, -r), dims, centerId, centerP, centerN);
142
+ var directions = 4.0;
143
+
144
+ if (params.edge.z > 0.5) {
145
+ votes = votes + tapVote(p, p + vec2<i32>( r, r), dims, centerId, centerP, centerN);
146
+ votes = votes + tapVote(p, p + vec2<i32>(-r, r), dims, centerId, centerP, centerN);
147
+ votes = votes + tapVote(p, p + vec2<i32>( r, -r), dims, centerId, centerP, centerN);
148
+ votes = votes + tapVote(p, p + vec2<i32>(-r, -r), dims, centerId, centerP, centerN);
149
+ directions = 8.0;
150
+ }
151
+
152
+ let coverage = votes / directions;
153
+ let darken = clamp(coverage * params.edge.y, 0.0, EDGE_MAX_DARKEN);
154
+ return vec4<f32>(0.0, 0.0, 0.0, darken);
155
+ }
156
+ `;
157
+ }
158
+ //# sourceMappingURL=edges.wgsl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edges.wgsl.js","sourceRoot":"","sources":["../../src/shaders/edges.wgsl.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAE/D;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,6BAA6B,EAAE,MAAM,mBAAmB,CAAC;AACpG,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAErF,MAAM,UAAU,gBAAgB,CAAC,YAAqB;IACpD,yEAAyE;IACzE,yEAAyE;IACzE,yEAAyE;IACzE,wBAAwB;IACxB,MAAM,SAAS,GAAG,YAAY;QAC5B,CAAC,CAAC,gEAAgE;QAClE,CAAC,CAAC,mDAAmD,CAAC;IAExD,OAAO;;;;;;;;UAQC,gBAAgB,CAAC,CAAC,EAAE,YAAY,CAAC;UACjC,SAAS;;;UAGT,oBAAoB;;uCAES,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC;2CACtB,6BAA6B,CAAC,OAAO,CAAC,CAAC,CAAC;uCAC5C,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2GhE,CAAC;AACF,CAAC"}
@@ -1,2 +1,2 @@
1
- export declare const mainShaderSource = "\n struct Uniforms {\n viewProj: mat4x4<f32>,\n model: mat4x4<f32>,\n baseColor: vec4<f32>,\n metallicRoughness: vec2<f32>, // x = metallic, y = roughness\n _padding1: vec2<f32>,\n sectionPlane: vec4<f32>, // xyz = plane normal, w = plane distance\n flags: vec4<u32>, // x = isSelected, y = section/clip bits, z = edgeEnabled, w = edgeIntensityMilli\n clipBoxMin: vec4<f32>, // xyz = clip-box min corner (world), w = pad\n clipBoxMax: vec4<f32>, // xyz = clip-box max corner (world), w = pad\n quantParams: vec4<f32>, // local min xyz, lattice step w\n rteViewProj: mat4x4<f32>, // appended frame; bit 16 selects it\n drawableDeltaHigh: vec4<f32>,\n drawableDeltaLow: vec4<f32>,\n rteCameraHigh: vec4<f32>,\n rteCameraLow: vec4<f32>,\n }\n @binding(0) @group(0) var<uniform> uniforms: Uniforms;\n const RTE_DRAWABLE_FLAG: u32 = 65536u;\n \nstruct RteFrameUniform {\n // Camera translation is intentionally absent. This projects positions\n // returned by rteWorldPosition, which are already relative to the eye.\n viewProj: mat4x4<f32>,\n}\n\nstruct RteDrawableUniform {\n // CPU computes (drawable origin - camera origin) in f64 and splits it.\n // This data must be refreshed whenever the RTE camera frame changes.\n drawableDeltaHigh: vec4<f32>,\n drawableDeltaLow: vec4<f32>,\n}\n\nfn rteWorldPosition(\n local: vec3<f32>,\n drawable: RteDrawableUniform,\n) -> vec4<f32> {\n // Do not subtract a camera origin here: this delta was formed in CPU f64.\n let highDelta = drawable.drawableDeltaHigh.xyz;\n let lowDelta = drawable.drawableDeltaLow.xyz;\n // A drawable template can span kilometres. Keep local with high first;\n // The local-plus-(high-plus-low) association is not equivalent in f32.\n return vec4<f32>((local + highDelta) + lowDelta, 1.0);\n}\n\n \n fn rtePosition(local: vec3<f32>) -> vec4<f32> {\n let linear = (uniforms.model * vec4<f32>(local, 0.0)).xyz;\n return rteWorldPosition(linear, RteDrawableUniform(\n uniforms.drawableDeltaHigh,\n uniforms.drawableDeltaLow,\n ));\n }\n fn rteInstancePosition(local: vec3<f32>, anchorHigh: vec3<f32>, anchorLow: vec3<f32>) -> vec4<f32> {\n return rteWorldPosition(local, RteDrawableUniform(\n vec4<f32>(anchorHigh, 0.0),\n vec4<f32>(anchorLow, 0.0),\n ));\n }\n // Shared group(1) lighting; packing matches packEnvironmentUniforms().\n struct Environment {\n sunDirection: vec3<f32>, // unit vector TOWARD the sun\n sunIntensity: f32,\n sunColor: vec3<f32>,\n ambientIntensity: f32,\n skyColor: vec3<f32>, // hemisphere-ambient sky tint\n exposure: f32,\n groundColor: vec3<f32>, // hemisphere-ambient ground tint\n fillIntensity: f32,\n rimIntensity: f32,\n sunSoftness: f32,\n _pad1: f32,\n _pad2: f32,\n }\n @binding(0) @group(1) var<uniform> env: Environment;\n const IRRADIANCE_CALIBRATION: f32 = 1.82;\n \n fn srgbToLinear(c: vec3<f32>) -> vec3<f32> {\n let lo = c / 12.92;\n let hi = pow((max(c, vec3<f32>(0.0)) + 0.055) / 1.055, vec3<f32>(2.4));\n return select(hi, lo, c <= vec3<f32>(0.04045));\n }\n\n fn linearToSrgb(c: vec3<f32>) -> vec3<f32> {\n let lo = c * 12.92;\n let hi = 1.055 * pow(max(c, vec3<f32>(0.0)), vec3<f32>(1.0 / 2.4)) - 0.055;\n return select(hi, lo, c <= vec3<f32>(0.0031308));\n }\n\n fn neutralCompress(cIn: vec3<f32>) -> vec3<f32> {\n let startCompression = 0.76;\n let desaturation = 0.15;\n let c = max(cIn, vec3<f32>(0.0));\n let peak = max(c.r, max(c.g, c.b));\n if (peak < startCompression) { return c; }\n let d = 1.0 - startCompression;\n let newPeak = 1.0 - d * d / (peak + d - startCompression);\n let g = 1.0 - 1.0 / (desaturation * (peak - newPeak) + 1.0);\n return mix(c * (newPeak / peak), vec3<f32>(newPeak), g);\n }\n\n\n \n @binding(1) @group(1) var shadowMap: texture_depth_2d;\n @binding(2) @group(1) var shadowCmp: sampler_comparison;\n struct Shadow { lightViewProj: mat4x4<f32>, params: vec4<f32>, params2: vec4<f32>, }\n @binding(3) @group(1) var<uniform> shadowU: Shadow;\n const SHADOW_POISSON = array<vec2<f32>, 12>(\n vec2<f32>(-0.326, -0.406), vec2<f32>(-0.840, -0.074), vec2<f32>(-0.696, 0.457),\n vec2<f32>(-0.203, 0.621), vec2<f32>( 0.962, -0.195), vec2<f32>( 0.473, -0.480),\n vec2<f32>( 0.519, 0.767), vec2<f32>( 0.185, -0.893), vec2<f32>( 0.507, 0.064),\n vec2<f32>( 0.896, 0.412), vec2<f32>(-0.322, -0.933), vec2<f32>(-0.792, -0.598),\n );\n fn sunShadowFactor(eyePos: vec3<f32>, N: vec3<f32>, fragCoord: vec2<f32>) -> f32 {\n if (shadowU.params.y < 0.5) { return 1.0; }\n let L = normalize(env.sunDirection);\n let Ns = N * select(-1.0, 1.0, dot(N, L) >= 0.0);\n let biased = eyePos + Ns * shadowU.params.z;\n let clip = shadowU.lightViewProj * vec4<f32>(biased, 1.0);\n let ndc = clip.xyz / clip.w;\n let uv = vec2<f32>(ndc.x * 0.5 + 0.5, ndc.y * -0.5 + 0.5);\n let inBounds = uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0 && ndc.z > 0.0;\n if (!inBounds) { return 1.0; }\n let NdotL = max(dot(Ns, L), 0.0);\n let slope = clamp(sqrt(max(1.0 - NdotL * NdotL, 0.0)) / max(NdotL, 0.1), 1.0, 12.0);\n let refDepth = ndc.z + shadowU.params2.x * slope * (1.0 + shadowU.params.w);\n let radius = shadowU.params.x * shadowU.params.w;\n let ign = fract(52.9829189 * fract(dot(fragCoord, vec2<f32>(0.06711056, 0.00583715))));\n let ang = ign * 6.2831853;\n let cr = cos(ang); let sr = sin(ang); var sum = 0.0;\n for (var i = 0; i < 12; i = i + 1) {\n let p = SHADOW_POISSON[i];\n let off = vec2<f32>(p.x * cr - p.y * sr, p.x * sr + p.y * cr) * radius;\n sum = sum + textureSampleCompareLevel(shadowMap, shadowCmp, uv + off, refDepth);\n }\n return mix(1.0, sum / 12.0, smoothstep(0.0, 0.3, NdotL));\n }\n\n struct VertexInput {\n @location(0) position: vec3<f32>,\n @location(1) normal: vec3<f32>,\n @location(2) entityId: u32,\n }\n\n struct VertexOutput {\n @builtin(position) position: vec4<f32>,\n @location(0) worldPos: vec3<f32>,\n @location(1) normal: vec3<f32>,\n @location(2) @interpolate(flat) entityId: u32,\n @location(3) viewPos: vec3<f32>, // For edge detection\n // Per-draw albedo carried from the vertex stage so the fragment shader\n // is shared by the flat path (vs_main writes uniforms.baseColor \u2014 the\n // per-batch / overlay-override colour) AND the instanced path\n // (vs_instanced writes the per-occurrence colour from the instance\n // buffer). For the flat path this is identical to reading\n // uniforms.baseColor directly (the value is constant across the draw).\n @location(4) color: vec4<f32>,\n // Per-occurrence selection flag for the instanced path (bit 0 = selected).\n // vs_main writes 0 (the flat path selects via uniforms.flags.x instead);\n // vs_instanced writes the per-instance flag from the instance buffer, so a\n // single selected occurrence highlights without re-drawing.\n @location(5) @interpolate(flat) instSelected: u32,\n // Camera-relative position for fragment operations that compare\n // geometry to section/crop boundaries or take derivatives. This is\n // intentionally separate from worldPos: shadow maps still consume\n // their established world-space light matrix until their depth pass\n // is migrated as one transaction.\n @location(6) eyePos: vec3<f32>,\n }\n\n // Per-instance vertex-buffer inputs (slot 1, stepMode 'instance') used by\n // vs_instanced. The mat4 arrives as four COLUMN vec4s (WGSL mat4x4 is\n // column-major), matching composeInstanceMatrix's column-major output +\n // the pipeline's slot-1 attribute offsets (0/16/32/48).\n //\n // Location namespaces: vertex-INPUT @location (this struct + VertexInput)\n // and inter-stage @location (VertexOutput) are INDEPENDENT in WGSL, so\n // InstanceInput.m1 @location(4) does NOT collide with VertexOutput.color\n // @location(4) \u2014 exactly as VertexInput.entityId and VertexOutput.entityId\n // already BOTH use @location(2). Within the INPUT namespace the per-vertex\n // inputs (0..2) and per-instance inputs (3..8) stay distinct.\n struct InstanceInput {\n @location(3) m0: vec4<f32>,\n @location(4) m1: vec4<f32>,\n @location(5) m2: vec4<f32>,\n @location(6) m3: vec4<f32>,\n @location(7) instEntityId: u32,\n @location(8) instColor: vec4<f32>,\n @location(9) instSelected: u32,\n @location(10) anchorHigh: vec4<f32>,\n @location(11) anchorLow: vec4<f32>,\n }\n\n // 12-byte quantized vertex (issue #1682 phase 6): uint16x4 (lattice\n // position xyz + packed octahedral normal in w as (u8x << 8 | u8y))\n // followed by the u32 entityId lane. Dequantization is BIT-EXACT for\n // the 2^-10 lattice: quantMin and q*step are both (integer)\u00B72^-10,\n // so coincident points across batches stay coincident (the shared-\n // origin z-fight guarantee survives quantization).\n struct QuantizedVertexInput {\n @location(0) q: vec4<u32>, // uint16x4: x, y, z, packedOct\n @location(2) entityId: u32,\n }\n\n fn octDecodeN(packed: u32) -> vec3<f32> {\n let ox = (f32((packed >> 8u) & 255u) / 255.0) * 2.0 - 1.0;\n let oy = (f32(packed & 255u) / 255.0) * 2.0 - 1.0;\n var n = vec3<f32>(ox, oy, 1.0 - abs(ox) - abs(oy));\n if (n.z < 0.0) {\n let tx = (1.0 - abs(oy)) * select(-1.0, 1.0, ox >= 0.0);\n let ty = (1.0 - abs(ox)) * select(-1.0, 1.0, oy >= 0.0);\n n = vec3<f32>(tx, ty, n.z);\n }\n return normalize(n);\n }\n\n // Shared flat-path vertex shading \u2014 vs_main and vs_main_quantized\n // differ ONLY in how position/normal are sourced.\n fn shadeFlatVertex(localPos: vec3<f32>, localNormal: vec3<f32>, entityId: u32) -> VertexOutput {\n var output: VertexOutput;\n let worldPos = uniforms.model * vec4<f32>(localPos, 1.0);\n let eyePos = rtePosition(localPos).xyz;\n let rte = (uniforms.flags.x & RTE_DRAWABLE_FLAG) != 0u;\n output.position = select(uniforms.viewProj * worldPos, uniforms.rteViewProj * vec4<f32>(eyePos, 1.0), rte);\n // Anti z-fighting depth nudge \u2014 see vs_main's comment.\n let colorSalt = (entityId >> 24u) * 2654435761u;\n let zHash = (((entityId & 0x00FFFFFFu) ^ colorSalt) * 2654435761u) & 255u;\n output.position.z *= 1.0 + f32(zHash) * 1e-6;\n output.worldPos = worldPos.xyz;\n output.normal = normalize((uniforms.model * vec4<f32>(localNormal, 0.0)).xyz);\n output.entityId = entityId;\n output.color = uniforms.baseColor;\n output.instSelected = 0u;\n output.eyePos = eyePos;\n output.viewPos = select((uniforms.viewProj * worldPos).xyz, (uniforms.rteViewProj * vec4<f32>(eyePos, 1.0)).xyz, rte);\n return output;\n }\n\n @vertex\n fn vs_main_quantized(input: QuantizedVertexInput) -> VertexOutput {\n let p = uniforms.quantParams.xyz\n + vec3<f32>(f32(input.q.x), f32(input.q.y), f32(input.q.z)) * uniforms.quantParams.w;\n return shadeFlatVertex(p, octDecodeN(input.q.w), input.entityId);\n }\n\n @vertex\n fn vs_main(input: VertexInput, @builtin(instance_index) instanceIndex: u32) -> VertexOutput {\n var output: VertexOutput;\n let worldPos = uniforms.model * vec4<f32>(input.position, 1.0);\n let eyePos = rtePosition(input.position).xyz;\n let rte = (uniforms.flags.x & RTE_DRAWABLE_FLAG) != 0u;\n output.position = select(uniforms.viewProj * worldPos, uniforms.rteViewProj * vec4<f32>(eyePos, 1.0), rte);\n // Anti z-fighting: deterministic depth nudge.\n // Knuth multiplicative hash spreads sequential IDs across 0-255 so\n // coplanar faces from different entities always get distinct depths.\n // Material-layer walls slice into one closed solid per layer, all\n // sharing the PARENT wall's expressId, so adjacent layers' coincident\n // interface caps would get the same entity nudge and z-fight into a\n // flickering comb (\"see inside the wall\"). To separate them we fold in\n // an 8-bit MATERIAL-COLOUR salt that mergeGeometry/interleaveTextured\n // baked into the HIGH 8 bits of the entityId lane (low 24 = picking id,\n // masked off by encodeId24). Crucially the salt comes from the mesh's\n // OWN colour, NOT the per-draw baseColor uniform \u2014 so the base opaque\n // pass and the lens/IDS/compare/4D OVERLAY pass (which redraws the same\n // geometry with a DIFFERENT draw colour) compute the SAME nudge, and\n // the overlay pipeline's depthCompare:'equal' matches instead of\n // rejecting every fragment. At 1e-6 per step the max world-space offset\n // is <3mm at 10m \u2014 invisible.\n let colorSalt = (input.entityId >> 24u) * 2654435761u;\n let zHash = (((input.entityId & 0x00FFFFFFu) ^ colorSalt) * 2654435761u) & 255u;\n output.position.z *= 1.0 + f32(zHash) * 1e-6;\n output.worldPos = worldPos.xyz;\n output.normal = normalize((uniforms.model * vec4<f32>(input.normal, 0.0)).xyz);\n output.entityId = input.entityId;\n output.color = uniforms.baseColor;\n output.instSelected = 0u; // flat path selects via uniforms.flags.x\n output.eyePos = eyePos;\n // Store view-space position for edge detection\n output.viewPos = select((uniforms.viewProj * worldPos).xyz, (uniforms.rteViewProj * vec4<f32>(eyePos, 1.0)).xyz, rte);\n return output;\n }\n\n // Instanced vertex entry \u2014 one template's geometry drawn once per\n // occurrence. The per-instance mat4 already folds SWAP * rel_k * T(origin)\n // (composed CPU-side, see instanced-render.ts), so it maps the template's\n // LOCAL vertex straight to WebGL Y-up world space \u2014 no uniforms.model.\n // rel_k and SWAP are rigid (no scale), so the same matrix transforms\n // normals. entityId + colour come per-occurrence from the instance buffer.\n @vertex\n fn vs_instanced(input: VertexInput, inst: InstanceInput) -> VertexOutput {\n var output: VertexOutput;\n let instMat = mat4x4<f32>(inst.m0, inst.m1, inst.m2, inst.m3);\n let worldPos = instMat * vec4<f32>(input.position, 1.0);\n let linearLocal = (instMat * vec4<f32>(input.position, 0.0)).xyz;\n let eyePos = rteInstancePosition(linearLocal, inst.anchorHigh.xyz, inst.anchorLow.xyz).xyz;\n output.position = uniforms.rteViewProj * vec4<f32>(eyePos, 1.0);\n // Same per-entity depth nudge as vs_main. No colour salt here: the\n // instanced path has no base-vs-overlay coincident redraw (yet), so the\n // raw picking id is enough to separate coplanar entities.\n let zHash = ((inst.instEntityId & 0x00FFFFFFu) * 2654435761u) & 255u;\n output.position.z *= 1.0 + f32(zHash) * 1e-6;\n output.worldPos = worldPos.xyz;\n output.normal = normalize((instMat * vec4<f32>(input.normal, 0.0)).xyz);\n output.entityId = inst.instEntityId;\n output.color = inst.instColor;\n output.instSelected = inst.instSelected;\n output.eyePos = eyePos;\n output.viewPos = (uniforms.rteViewProj * vec4<f32>(eyePos, 1.0)).xyz;\n return output;\n }\n\n // PBR helper functions\n fn fresnelSchlick(cosTheta: f32, F0: vec3<f32>) -> vec3<f32> {\n return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);\n }\n\n fn distributionGGX(NdotH: f32, roughness: f32) -> f32 {\n let a = roughness * roughness;\n let a2 = a * a;\n let NdotH2 = NdotH * NdotH;\n let num = a2;\n let denomBase = (NdotH2 * (a2 - 1.0) + 1.0);\n let denom = 3.14159265 * denomBase * denomBase;\n return num / max(denom, 0.0000001);\n }\n\n fn geometrySchlickGGX(NdotV: f32, roughness: f32) -> f32 {\n let r = (roughness + 1.0);\n let k = (r * r) / 8.0;\n let num = NdotV;\n let denom = NdotV * (1.0 - k) + k;\n return num / max(denom, 0.0000001);\n }\n\n fn geometrySmith(NdotV: f32, NdotL: f32, roughness: f32) -> f32 {\n let ggx2 = geometrySchlickGGX(NdotV, roughness);\n let ggx1 = geometrySchlickGGX(NdotL, roughness);\n return ggx1 * ggx2;\n }\n\n fn encodeId24(id: u32) -> vec4<f32> {\n let r = f32((id >> 16u) & 255u) / 255.0;\n let g = f32((id >> 8u) & 255u) / 255.0;\n let b = f32(id & 255u) / 255.0;\n return vec4<f32>(r, g, b, 1.0);\n }\n\n struct FragmentOutput {\n @location(0) color: vec4<f32>,\n @location(1) objectIdEncoded: vec4<f32>,\n }\n\n @fragment\n fn fs_main(input: VertexOutput) -> FragmentOutput {\n // The flat/quantized/textured mesh paths submit this in one\n // camera-relative frame. Do all camera-local fragment arithmetic in\n // that frame; otherwise the RTE vertex precision is thrown away at\n // section/crop/derivative ingress. Instanced geometry is not yet\n // anchored, so its worldPos remains the authoritative input.\n let fragmentPos = select(input.worldPos, input.eyePos, (uniforms.flags.x & RTE_DRAWABLE_FLAG) != 0u);\n // Per-instance hide/isolate: bit 1 of the instance flags lane marks a hidden\n // occurrence. Discard it so it neither draws nor writes depth (and the pick\n // pass applies the same discard, so it isn't pickable). vs_main writes\n // instSelected=0u for flat geometry, so this never affects the flat path.\n if ((input.instSelected & 2u) != 0u) {\n discard;\n }\n // Per-instance opacity routing (instanced passes only \u2014 flags.x bit 2). The\n // opaque instanced pass draws fully-opaque (or selected) occurrences; the\n // transparent instanced sub-pass (bit 3, alpha-blended) draws the rest. Discard\n // the occurrences belonging to the OTHER pass so each is drawn exactly once.\n // Lens-ghost / x-ray / compare write a low per-instance alpha into input.color.a.\n if ((uniforms.flags.x & 4u) != 0u) {\n let occOpaque = input.color.a >= 0.99 || (input.instSelected & 1u) != 0u;\n let transparentPass = (uniforms.flags.x & 8u) != 0u;\n if (transparentPass) {\n if (occOpaque) { discard; }\n } else {\n if (!occOpaque) { discard; }\n }\n }\n // Section plane clipping - discard fragments ABOVE the plane.\n // flags.y packs two bits: bit 0 = enabled, bit 1 = flipped.\n let sectionEnabled = (uniforms.flags.y & 1u) == 1u;\n if (sectionEnabled) {\n let planeNormal = uniforms.sectionPlane.xyz;\n let planeDistance = uniforms.sectionPlane.w;\n let flipped = (uniforms.flags.y & 2u) == 2u;\n let side = select(1.0, -1.0, flipped);\n let distToPlane = (dot(fragmentPos, planeNormal) - planeDistance) * side;\n if (distToPlane > 0.0) {\n discard;\n }\n }\n // Clip box (section / crop box): discard fragments OUTSIDE the AABB.\n // flags.y bit 2 = clip-box enabled.\n if ((uniforms.flags.y & 4u) != 0u) {\n let p = fragmentPos;\n if (any(p < uniforms.clipBoxMin.xyz) || any(p > uniforms.clipBoxMax.xyz)) {\n discard;\n }\n }\n\n // Compute normal via derivative-based flat shading.\n //\n // Industry-standard solution for BIM/CAD viewers \u2014 what\n // Three.js (material.flatShading = true), Autodesk Forge,\n // Speckle, and xeokit all do for opaque surfaces. Rationale:\n //\n // * BIM geometry is overwhelmingly flat surfaces (walls,\n // slabs, roofs, beams), and CSG operations (opening\n // subtraction, layer slicing) emit those surfaces as\n // dense strips of coplanar triangles. Per-vertex normal\n // averaging gives a SLIGHTLY-different normal at each\n // vertex due to f32 noise from boolean output; the\n // boundary between strips then reads as a visible darker/\n // brighter scar line \u2014 the horizontal striations on\n // walls, stripes on roofs, visible triangulation reports\n // across every CSG kernel we have tried (legacy BSP,\n // Manifold).\n // * cross(dpdx, dpdy) of world position evaluates to the\n // EXACT face normal in the fragment shader. Every\n // fragment on a flat face \u2014 across an arbitrarily-fine\n // triangulation \u2014 gets the IDENTICAL normal, so coplanar\n // splits become invisible by construction. No CPU-side\n // welding, smooth-grouping, or coplanar-face merging\n // fixes the symptom as cleanly.\n //\n // Trade-off: genuinely curved surfaces (cylinder tessellations,\n // BSpline approximations) shade with visible facets. For BIM\n // that's acceptable \u2014 curved surfaces are < 5 % of typical\n // model triangle count and the faceting matches CAD-tool\n // (Revit, ArchiCAD) on-screen behaviour at default quality.\n //\n // We still fall back to the vertex normal when derivatives\n // are unavailable (extreme polygon degeneracy where dpdx /\n // dpdy collapse to zero \u2014 practically never on real geometry).\n let faceN = cross(dpdx(fragmentPos), dpdy(fragmentPos));\n let fLen2 = dot(faceN, faceN);\n var N: vec3<f32>;\n if (fLen2 > 1e-10) {\n N = faceN * inverseSqrt(fLen2);\n } else {\n // Degenerate derivative \u2014 fall back to the vertex normal\n // if it's populated, else +Y.\n N = input.normal;\n let nLen2 = dot(N, N);\n if (nLen2 > 1e-6) {\n N = N * inverseSqrt(nLen2);\n } else {\n N = vec3<f32>(0.0, 1.0, 0.0);\n }\n }\n\n // Stabilize the SIGN of the derivative face normal with the vertex\n // normal. The screen-space cross product gives the exact face\n // normal DIRECTION for coplanar strips (the scar-line fix), but at\n // grazing angles its SIGN becomes numerically unstable per quad \u2014\n // hemisphere/rim lighting then band-flips across large regions of\n // flat walls/slabs (diagonal lighter/darker bands). The interpolated\n // vertex normal is quad-noise-free, so use it only to orient N.\n // Guard: skip when the vertex normal is missing or nearly\n // perpendicular to the face normal (unreliable witness).\n let vN = input.normal;\n let alignDot = dot(N, vN);\n if (alignDot * alignDot > 0.03 * dot(vN, vN)) {\n N = N * sign(alignDot);\n }\n\n // Lighting environment \u2014 sun/hemisphere/exposure come from the\n // global env uniform. The fill follows the sun (it bounces in from\n // the opposite side); the rim stays fixed in world space as a\n // stylistic shaping light.\n let sunLight = env.sunDirection;\n // Horizontal mirror of the sun, lifted slightly. An overhead sun\n // leaves (0, 0.25, 0), which normalizes to straight up.\n let fillLight = normalize(vec3<f32>(-sunLight.x, 0.25, -sunLight.z));\n let rimLight = normalize(vec3<f32>(0.0, 0.2, -1.0)); // Rim light for edge definition\n\n // Hemisphere ambient. This, not the sun, keeps faces turned away\n // from the sun readable (I-beam webs and flange undersides, #5382).\n let hemisphereFactor = N.y * 0.5 + 0.5;\n let ambient = mix(env.groundColor, env.skyColor, hemisphereFactor) * env.ambientIntensity;\n\n // One-sided sun, so a building has a lit side and a shaded side.\n // sunSoftness is the diffuse wrap (env uniform): 0 = crisp\n // terminator (hard shadows), larger = softer wrap-around (overcast).\n let NdotL = dot(N, sunLight);\n let wrap = env.sunSoftness;\n let diffuseSun = max((NdotL + wrap) / (1.0 + wrap), 0.0) * env.sunIntensity;\n\n // Fill light, one-sided like the sun.\n let NdotFill = max(dot(N, fillLight), 0.0);\n let diffuseFill = NdotFill * env.fillIntensity;\n\n // Rim light for edge definition\n let NdotRim = max(dot(N, rimLight), 0.0);\n let rim = pow(NdotRim, 4.0) * env.rimIntensity;\n\n // The authored colour is display-referred sRGB; light it in linear.\n // (textured.wgsl.ts anchors on the first line to multiply in the texel.)\n var baseColor = input.color.rgb;\n baseColor = srgbToLinear(baseColor);\n\n // Combine all lighting. Only the DIRECT sun term is occluded by cast\n // shadows (#2670); ambient/fill/rim are indirect and stay unshadowed.\n // Exposure scales the light, so the selection shade below and every\n // later stage see one exposed irradiance.\n let sunShadow = sunShadowFactor(input.eyePos, N, input.position.xy);\n let lightTerm = ambient + env.sunColor * (diffuseSun * sunShadow) + vec3<f32>(diffuseFill + rim);\n let irradiance = lightTerm * (env.exposure * IRRADIANCE_CALIBRATION);\n var color = baseColor * irradiance;\n\n // flags.x is a bitfield:\n // bit 0 (value 1) = isSelected \u2192 selection-highlight + force opaque\n // bit 1 (value 2) = isOverlay \u2192 color-override pass; preserve\n // baseColor.a (overlay pipeline has\n // src-alpha blending) AND skip the\n // glass-fresnel branch so low-alpha\n // ghost tints don't pick up the\n // near-white reflection tint meant\n // for real glass materials.\n // Selected via the per-draw flag (flat path) OR the per-occurrence flag\n // (instanced path \u2014 vs_instanced reads it from the instance buffer).\n let isSelected = ((uniforms.flags.x & 1u) == 1u) || ((input.instSelected & 1u) == 1u);\n let isOverlay = (uniforms.flags.x & 2u) == 2u;\n\n // Selection highlight \u2014 a blue albedo RE-LIT by the scene lighting.\n //\n // We override the material albedo with selection-blue and re-light\n // it with the SAME lightTerm used for unselected surfaces, then\n // discard the view-dependent (fresnel) term below. Two requirements\n // are in tension and this satisfies both:\n //\n // * No base-material bleed-through. The old fresnel-glow mix left\n // ~80 % of the lit object colour visible at face centres (the\n // green-site / red-roof wash-out). Here the base colour never\n // enters the result \u2014 only lightTerm (geometry/light, colour-\n // independent) modulates the constant blue albedo.\n // * Facet/crease structure must survive. A single FLAT colour\n // (the previous fix) collapsed every face to the same blue, so\n // internal edges \u2014 which read as the per-face shading STEP, not\n // just the faint screen-space edge line \u2014 disappeared on\n // selection. Re-lighting keeps that per-face brightness step, so\n // creases read on the highlight exactly as they do unselected.\n //\n // The luminance of the exposed irradiance is used as a multiplicative\n // gain (which preserves the per-face brightness RATIOS, so creases\n // read as strongly as on the unselected surface). The calibration\n // puts a sunlit face at 1.0, i.e. full selection-blue, and the\n // floor/ceiling clamp keeps shadowed faces only dimmed and bright\n // scenes from washing out.\n if (isSelected) {\n let shadeLum = dot(irradiance, vec3<f32>(0.299, 0.587, 0.114));\n let shade = clamp(shadeLum, 0.45, 1.2);\n color = srgbToLinear(vec3<f32>(0.3, 0.6, 1.0)) * shade;\n }\n\n // flags.x bit 5 (value 32) = EMPHASIZE overlay: render the colour\n // override FULLY UNLIT and saturated (no lighting attenuation, no\n // wash-to-white) so the focused clash pair reads as a solid, vivid,\n // distinct colour that pops against the lit model \u2014 like a clash tool.\n // A faint normal-based shade keeps the silhouette from going flat. (#1277)\n let emphasizedOverlay = isOverlay && (uniforms.flags.x & 32u) != 0u;\n if (emphasizedOverlay) {\n let facet = 0.85 + 0.15 * abs(dot(N, normalize(vec3<f32>(0.3, 1.0, 0.2))));\n color = baseColor * facet;\n }\n\n // Beautiful fresnel effect for transparent materials (glass)\n // Skip when selected \u2014 the glass shine and desaturation wash out the\n // blue highlight, making it appear white instead of blue.\n // Also force alpha to 1.0 for selected objects so the highlight is\n // fully opaque (the selection pipeline has no alpha blending).\n // Emphasized clash overlay paints a SOLID vivid fill (force opaque) so\n // it isn't blended down to a pale tint against the geometry beneath.\n var finalAlpha = select(input.color.a, 1.0, isSelected || emphasizedOverlay);\n if (finalAlpha < 0.99 && !isSelected && !isOverlay) {\n // Calculate view direction for fresnel\n let V = normalize(-fragmentPos);\n let NdotV = max(dot(N, V), 0.0);\n\n // Enhanced fresnel effect - stronger at edges (grazing angles)\n // Using Schlick's approximation for realistic glass reflection\n let fresnelPower = 1.5; // Higher = softer edge reflections\n let fresnel = pow(1.0 - NdotV, fresnelPower);\n\n // Glass reflection tint (sky/environment reflection at edges)\n let reflectionTint = vec3<f32>(0.92, 0.96, 1.0); // Cool sky reflection\n let reflectionStrength = fresnel * 0.6; // Strong edge reflections\n\n // Mix in reflection tint at edges\n color = mix(color, color * reflectionTint, reflectionStrength);\n\n // Add realistic glass shine - brighter at edges where light reflects.\n // Linear amount: 0.06 lifts a mid-tone about as much as the 0.12\n // the pre-linear pipeline added in display space.\n let glassShine = fresnel * 0.06;\n color += glassShine;\n\n // Slight desaturation at edges (glass reflects environment, not just color)\n let edgeDesaturation = fresnel * 0.25;\n let gray = dot(color, vec3<f32>(0.299, 0.587, 0.114));\n color = mix(color, vec3<f32>(gray), edgeDesaturation);\n\n // Make glass more transparent (reduce opacity by 30%)\n finalAlpha = finalAlpha * 0.7;\n }\n\n // Hue-preserving highlight roll-off (color-transfer.wgsl.ts). No\n // contrast curve and no saturation boost: an authored colour lit at\n // unit irradiance leaves here unchanged unless it is brighter than\n // the roll-off threshold.\n color = neutralCompress(color);\n\n // Subtle edge enhancement using screen-space derivatives.\n //\n // Use the SHADED normal (face normal from dpdx/dpdy above)\n // for the normal-gradient term, not the interpolated vertex\n // normal \u2014 otherwise we get spurious dark stripes on flat\n // surfaces whose vertex normals carry numerical noise from\n // CSG output (the visible scar-line symptom would just\n // resurface here even after the lit-normal fix). With the\n // face normal, coplanar adjacent triangles agree exactly \u2192\n // zero normal gradient \u2192 no false edge; only the genuine\n // creases between perpendicular faces produce a real\n // gradient and get the intended outline.\n let depthGradient = length(vec2<f32>(\n dpdx(input.viewPos.z),\n dpdy(input.viewPos.z)\n ));\n let normalGradient = length(vec2<f32>(\n length(dpdx(N)),\n length(dpdy(N))\n ));\n\n if (uniforms.flags.z == 1u) {\n // Threshold filters subtle normal discontinuities at internal\n // triangle edges between coplanar entities in the same batch.\n let edgeFactor = smoothstep(0.02, 0.12, depthGradient * 10.0 + normalGradient * 5.0);\n let edgeIntensity = f32(uniforms.flags.w) / 1000.0;\n let edgeDarkenStrength = clamp(0.25 * edgeIntensity, 0.0, 0.85);\n let edgeDarken = mix(1.0, 1.0 - edgeDarkenStrength, edgeFactor);\n color *= edgeDarken;\n }\n\n color = linearToSrgb(clamp(color, vec3<f32>(0.0), vec3<f32>(1.0)));\n\n var out: FragmentOutput;\n out.color = vec4<f32>(color, finalAlpha);\n out.objectIdEncoded = encodeId24(input.entityId);\n return out;\n }\n ";
1
+ export declare const mainShaderSource = "\n struct Uniforms {\n viewProj: mat4x4<f32>,\n model: mat4x4<f32>,\n baseColor: vec4<f32>,\n metallicRoughness: vec2<f32>, // x = metallic, y = roughness (mesh-material.ts)\n transmission: vec2<f32>, // x = 1: authored translucent, drawn as glass; y = pad\n sectionPlane: vec4<f32>, // xyz = plane normal, w = plane distance\n flags: vec4<u32>, // x = isSelected, y = section/clip bits, z = edgeEnabled, w = edgeIntensityMilli\n clipBoxMin: vec4<f32>, // xyz = clip-box min corner (world), w = pad\n clipBoxMax: vec4<f32>, // xyz = clip-box max corner (world), w = pad\n quantParams: vec4<f32>, // local min xyz, lattice step w\n rteViewProj: mat4x4<f32>, // appended frame; bit 16 selects it\n drawableDeltaHigh: vec4<f32>,\n drawableDeltaLow: vec4<f32>,\n rteCameraHigh: vec4<f32>,\n rteCameraLow: vec4<f32>,\n }\n @binding(0) @group(0) var<uniform> uniforms: Uniforms;\n const RTE_DRAWABLE_FLAG: u32 = 65536u;\n \nstruct RteFrameUniform {\n // Camera translation is intentionally absent. This projects positions\n // returned by rteWorldPosition, which are already relative to the eye.\n viewProj: mat4x4<f32>,\n}\n\nstruct RteDrawableUniform {\n // CPU computes (drawable origin - camera origin) in f64 and splits it.\n // This data must be refreshed whenever the RTE camera frame changes.\n drawableDeltaHigh: vec4<f32>,\n drawableDeltaLow: vec4<f32>,\n}\n\nfn rteWorldPosition(\n local: vec3<f32>,\n drawable: RteDrawableUniform,\n) -> vec4<f32> {\n // Do not subtract a camera origin here: this delta was formed in CPU f64.\n let highDelta = drawable.drawableDeltaHigh.xyz;\n let lowDelta = drawable.drawableDeltaLow.xyz;\n // A drawable template can span kilometres. Keep local with high first;\n // The local-plus-(high-plus-low) association is not equivalent in f32.\n return vec4<f32>((local + highDelta) + lowDelta, 1.0);\n}\n\n \n fn rtePosition(local: vec3<f32>) -> vec4<f32> {\n let linear = (uniforms.model * vec4<f32>(local, 0.0)).xyz;\n return rteWorldPosition(linear, RteDrawableUniform(\n uniforms.drawableDeltaHigh,\n uniforms.drawableDeltaLow,\n ));\n }\n fn rteInstancePosition(local: vec3<f32>, anchorHigh: vec3<f32>, anchorLow: vec3<f32>) -> vec4<f32> {\n return rteWorldPosition(local, RteDrawableUniform(\n vec4<f32>(anchorHigh, 0.0),\n vec4<f32>(anchorLow, 0.0),\n ));\n }\n // Shared group(1) lighting; packing matches packEnvironmentUniforms().\n struct Environment {\n sunDirection: vec3<f32>, // unit vector TOWARD the sun\n sunIntensity: f32,\n sunColor: vec3<f32>,\n ambientIntensity: f32,\n skyColor: vec3<f32>, // hemisphere-ambient sky tint\n exposure: f32,\n groundColor: vec3<f32>, // hemisphere-ambient ground tint\n fillIntensity: f32,\n rimIntensity: f32,\n sunSoftness: f32,\n _pad1: f32,\n _pad2: f32,\n }\n @binding(0) @group(1) var<uniform> env: Environment;\n // Selection highlight tint (#5484), set by Renderer.setOverlayTheme via\n // updateSelectionColor \u2014 written only on theme change, never per frame.\n // TRUE LINEAR-LIGHT RGB: re-lit by lightTerm below exactly like the WGSL\n // constant it replaces, then ACES-tonemapped + gamma-encoded on output.\n @binding(4) @group(1) var<uniform> selectionColor: vec4<f32>;\n const IRRADIANCE_CALIBRATION: f32 = 1.82;\n const TRANSLUCENT_OPACITY_SCALE: f32 = 0.7;\n \n fn srgbToLinear(c: vec3<f32>) -> vec3<f32> {\n let lo = c / 12.92;\n let hi = pow((max(c, vec3<f32>(0.0)) + 0.055) / 1.055, vec3<f32>(2.4));\n return select(hi, lo, c <= vec3<f32>(0.04045));\n }\n\n fn linearToSrgb(c: vec3<f32>) -> vec3<f32> {\n let lo = c * 12.92;\n let hi = 1.055 * pow(max(c, vec3<f32>(0.0)), vec3<f32>(1.0 / 2.4)) - 0.055;\n return select(hi, lo, c <= vec3<f32>(0.0031308));\n }\n\n fn neutralCompress(cIn: vec3<f32>) -> vec3<f32> {\n let startCompression = 0.76;\n let desaturation = 0.15;\n let c = max(cIn, vec3<f32>(0.0));\n let peak = max(c.r, max(c.g, c.b));\n if (peak < startCompression) { return c; }\n let d = 1.0 - startCompression;\n let newPeak = 1.0 - d * d / (peak + d - startCompression);\n let g = 1.0 - 1.0 / (desaturation * (peak - newPeak) + 1.0);\n return mix(c * (newPeak / peak), vec3<f32>(newPeak), g);\n }\n\n \n const MIN_SPECULAR_ROUGHNESS: f32 = 0.045;\n const DIELECTRIC_F0: f32 = 0.04;\n const PI: f32 = 3.14159265;\n\n fn fresnelSchlick(cosTheta: f32, F0: vec3<f32>) -> vec3<f32> {\n return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);\n }\n\n fn distributionGGX(NdotH: f32, roughness: f32) -> f32 {\n let a = roughness * roughness;\n let a2 = a * a;\n let NdotH2 = NdotH * NdotH;\n let denomBase = (NdotH2 * (a2 - 1.0) + 1.0);\n return a2 / max(PI * denomBase * denomBase, 0.0000001);\n }\n\n fn geometrySchlickGGX(NdotV: f32, roughness: f32) -> f32 {\n let r = (roughness + 1.0);\n let k = (r * r) / 8.0;\n return NdotV / max(NdotV * (1.0 - k) + k, 0.0000001);\n }\n\n fn geometrySmith(NdotV: f32, NdotL: f32, roughness: f32) -> f32 {\n return geometrySchlickGGX(NdotL, roughness) * geometrySchlickGGX(NdotV, roughness);\n }\n\n // Split-sum environment BRDF, analytic fit (Karis 2014).\n fn envBrdfApprox(F0: vec3<f32>, roughness: f32, NdotV: f32) -> vec3<f32> {\n let r = roughness * vec4<f32>(-1.0, -0.0275, -0.572, 0.022) + vec4<f32>(1.0, 0.0425, 1.04, -0.04);\n let a004 = min(r.x * r.x, exp2(-9.28 * NdotV)) * r.x + r.y;\n let ab = vec2<f32>(-1.04, 1.04) * a004 + r.zw;\n return F0 * ab.x + ab.y;\n }\n\n struct SurfaceSpecular {\n // Reflected light, in lightTerm units (unexposed, uncalibrated).\n light: vec3<f32>,\n // Directional albedo of the specular lobe for this view.\n reflectance: vec3<f32>,\n }\n\n // N and V are unit vectors; V points from the surface to the eye.\n // sunLight is the sun's diffuse-equivalent intensity: sunColor *\n // sunIntensity * sunShadow, exactly what scales the diffuse sun term.\n fn surfaceSpecular(\n N: vec3<f32>,\n V: vec3<f32>,\n albedo: vec3<f32>,\n metallic: f32,\n roughnessIn: f32,\n sunLight: vec3<f32>,\n ) -> SurfaceSpecular {\n var out: SurfaceSpecular;\n let NdotV = dot(N, V);\n // A face turned away from the eye is its back side: nothing it\n // reflects can reach the eye, and its diffuse keeps full weight.\n if (NdotV <= 0.0) {\n out.light = vec3<f32>(0.0);\n out.reflectance = vec3<f32>(0.0);\n return out;\n }\n let roughness = clamp(roughnessIn, MIN_SPECULAR_ROUGHNESS, 1.0);\n let F0 = mix(vec3<f32>(DIELECTRIC_F0), albedo, metallic);\n\n let L = env.sunDirection;\n let rawNdotL = dot(N, L);\n let NdotL = max(rawNdotL, 0.0);\n // A face the sun is behind (rawNdotL <= 0) contributes nothing to\n // the direct lobe anyway (NdotL clamps it to 0 below), but V + L\n // can be exactly zero there \u2014 the sun exactly opposite the eye\n // reflection, e.g. N = V = (0,-1,0), sun = (0,1,0), both satisfy\n // NdotV > 0 above. normalize(vec3(0)) is NaN in WGSL, and NaN * 0\n // is NaN, not 0, so it would poison sun and everything summed with\n // it instead of being multiplied away. Skip the half-vector\n // entirely in that case rather than relying on the NdotL factor.\n var sun = vec3<f32>(0.0);\n if (rawNdotL > 0.0) {\n let H = normalize(V + L);\n let D = distributionGGX(max(dot(N, H), 0.0), roughness);\n let G = geometrySmith(NdotV, NdotL, roughness);\n let F = fresnelSchlick(max(dot(V, H), 0.0), F0);\n // BRDF * NdotL * (pi * intensity); the NdotL cancels the BRDF's.\n sun = F * (D * G * PI / (4.0 * NdotV)) * sunLight;\n }\n\n // The hemisphere ambient, looked up along the reflection. A rough\n // lobe gathers from around the normal, so lean the lookup toward N.\n let R = reflect(-V, N);\n let Rd = normalize(mix(R, N, roughness * roughness));\n let sky = mix(env.groundColor, env.skyColor, Rd.y * 0.5 + 0.5) * env.ambientIntensity;\n let reflectance = envBrdfApprox(F0, roughness, NdotV);\n\n out.light = sun + sky * reflectance;\n out.reflectance = reflectance;\n return out;\n }\n\n\n \n @binding(1) @group(1) var shadowMap: texture_depth_2d;\n @binding(2) @group(1) var shadowCmp: sampler_comparison;\n struct Shadow { lightViewProj: mat4x4<f32>, params: vec4<f32>, params2: vec4<f32>, }\n @binding(3) @group(1) var<uniform> shadowU: Shadow;\n const SHADOW_POISSON = array<vec2<f32>, 12>(\n vec2<f32>(-0.326, -0.406), vec2<f32>(-0.840, -0.074), vec2<f32>(-0.696, 0.457),\n vec2<f32>(-0.203, 0.621), vec2<f32>( 0.962, -0.195), vec2<f32>( 0.473, -0.480),\n vec2<f32>( 0.519, 0.767), vec2<f32>( 0.185, -0.893), vec2<f32>( 0.507, 0.064),\n vec2<f32>( 0.896, 0.412), vec2<f32>(-0.322, -0.933), vec2<f32>(-0.792, -0.598),\n );\n fn sunShadowFactor(eyePos: vec3<f32>, N: vec3<f32>, fragCoord: vec2<f32>) -> f32 {\n if (shadowU.params.y < 0.5) { return 1.0; }\n let L = normalize(env.sunDirection);\n let Ns = N * select(-1.0, 1.0, dot(N, L) >= 0.0);\n let biased = eyePos + Ns * shadowU.params.z;\n let clip = shadowU.lightViewProj * vec4<f32>(biased, 1.0);\n let ndc = clip.xyz / clip.w;\n let uv = vec2<f32>(ndc.x * 0.5 + 0.5, ndc.y * -0.5 + 0.5);\n let inBounds = uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0 && ndc.z > 0.0;\n if (!inBounds) { return 1.0; }\n let NdotL = max(dot(Ns, L), 0.0);\n let slope = clamp(sqrt(max(1.0 - NdotL * NdotL, 0.0)) / max(NdotL, 0.1), 1.0, 12.0);\n let refDepth = ndc.z + shadowU.params2.x * slope * (1.0 + shadowU.params.w);\n let radius = shadowU.params.x * shadowU.params.w;\n let ign = fract(52.9829189 * fract(dot(fragCoord, vec2<f32>(0.06711056, 0.00583715))));\n let ang = ign * 6.2831853;\n let cr = cos(ang); let sr = sin(ang); var sum = 0.0;\n for (var i = 0; i < 12; i = i + 1) {\n let p = SHADOW_POISSON[i];\n let off = vec2<f32>(p.x * cr - p.y * sr, p.x * sr + p.y * cr) * radius;\n sum = sum + textureSampleCompareLevel(shadowMap, shadowCmp, uv + off, refDepth);\n }\n return mix(1.0, sum / 12.0, smoothstep(0.0, 0.3, NdotL));\n }\n\n struct VertexInput {\n @location(0) position: vec3<f32>,\n @location(1) normal: vec3<f32>,\n @location(2) entityId: u32,\n }\n\n struct VertexOutput {\n @builtin(position) position: vec4<f32>,\n @location(0) worldPos: vec3<f32>,\n @location(1) normal: vec3<f32>,\n @location(2) @interpolate(flat) entityId: u32,\n @location(3) viewPos: vec3<f32>, // For edge detection\n // Per-draw albedo carried from the vertex stage so the fragment shader\n // is shared by the flat path (vs_main writes uniforms.baseColor \u2014 the\n // per-batch / overlay-override colour) AND the instanced path\n // (vs_instanced writes the per-occurrence colour from the instance\n // buffer). For the flat path this is identical to reading\n // uniforms.baseColor directly (the value is constant across the draw).\n @location(4) color: vec4<f32>,\n // Per-occurrence selection flag for the instanced path (bit 0 = selected).\n // vs_main writes 0 (the flat path selects via uniforms.flags.x instead);\n // vs_instanced writes the per-instance flag from the instance buffer, so a\n // single selected occurrence highlights without re-drawing.\n @location(5) @interpolate(flat) instSelected: u32,\n // Camera-relative position for fragment operations that compare\n // geometry to section/crop boundaries or take derivatives. This is\n // intentionally separate from worldPos: shadow maps still consume\n // their established world-space light matrix until their depth pass\n // is migrated as one transaction.\n @location(6) eyePos: vec3<f32>,\n }\n\n // Per-instance vertex-buffer inputs (slot 1, stepMode 'instance') used by\n // vs_instanced. The mat4 arrives as four COLUMN vec4s (WGSL mat4x4 is\n // column-major), matching composeInstanceMatrix's column-major output +\n // the pipeline's slot-1 attribute offsets (0/16/32/48).\n //\n // Location namespaces: vertex-INPUT @location (this struct + VertexInput)\n // and inter-stage @location (VertexOutput) are INDEPENDENT in WGSL, so\n // InstanceInput.m1 @location(4) does NOT collide with VertexOutput.color\n // @location(4) \u2014 exactly as VertexInput.entityId and VertexOutput.entityId\n // already BOTH use @location(2). Within the INPUT namespace the per-vertex\n // inputs (0..2) and per-instance inputs (3..8) stay distinct.\n struct InstanceInput {\n @location(3) m0: vec4<f32>,\n @location(4) m1: vec4<f32>,\n @location(5) m2: vec4<f32>,\n @location(6) m3: vec4<f32>,\n @location(7) instEntityId: u32,\n @location(8) instColor: vec4<f32>,\n @location(9) instSelected: u32,\n @location(10) anchorHigh: vec4<f32>,\n @location(11) anchorLow: vec4<f32>,\n }\n\n // 12-byte quantized vertex (issue #1682 phase 6): uint16x4 (lattice\n // position xyz + packed octahedral normal in w as (u8x << 8 | u8y))\n // followed by the u32 entityId lane. Dequantization is BIT-EXACT for\n // the 2^-10 lattice: quantMin and q*step are both (integer)\u00B72^-10,\n // so coincident points across batches stay coincident (the shared-\n // origin z-fight guarantee survives quantization).\n struct QuantizedVertexInput {\n @location(0) q: vec4<u32>, // uint16x4: x, y, z, packedOct\n @location(2) entityId: u32,\n }\n\n fn octDecodeN(packed: u32) -> vec3<f32> {\n let ox = (f32((packed >> 8u) & 255u) / 255.0) * 2.0 - 1.0;\n let oy = (f32(packed & 255u) / 255.0) * 2.0 - 1.0;\n var n = vec3<f32>(ox, oy, 1.0 - abs(ox) - abs(oy));\n if (n.z < 0.0) {\n let tx = (1.0 - abs(oy)) * select(-1.0, 1.0, ox >= 0.0);\n let ty = (1.0 - abs(ox)) * select(-1.0, 1.0, oy >= 0.0);\n n = vec3<f32>(tx, ty, n.z);\n }\n return normalize(n);\n }\n\n // Shared flat-path vertex shading \u2014 vs_main and vs_main_quantized\n // differ ONLY in how position/normal are sourced.\n fn shadeFlatVertex(localPos: vec3<f32>, localNormal: vec3<f32>, entityId: u32) -> VertexOutput {\n var output: VertexOutput;\n let worldPos = uniforms.model * vec4<f32>(localPos, 1.0);\n let eyePos = rtePosition(localPos).xyz;\n let rte = (uniforms.flags.x & RTE_DRAWABLE_FLAG) != 0u;\n output.position = select(uniforms.viewProj * worldPos, uniforms.rteViewProj * vec4<f32>(eyePos, 1.0), rte);\n // Anti z-fighting depth nudge \u2014 see vs_main's comment.\n let colorSalt = (entityId >> 24u) * 2654435761u;\n let zHash = (((entityId & 0x00FFFFFFu) ^ colorSalt) * 2654435761u) & 255u;\n output.position.z *= 1.0 + f32(zHash) * 1e-6;\n output.worldPos = worldPos.xyz;\n output.normal = normalize((uniforms.model * vec4<f32>(localNormal, 0.0)).xyz);\n output.entityId = entityId;\n output.color = uniforms.baseColor;\n output.instSelected = 0u;\n output.eyePos = eyePos;\n output.viewPos = select((uniforms.viewProj * worldPos).xyz, (uniforms.rteViewProj * vec4<f32>(eyePos, 1.0)).xyz, rte);\n return output;\n }\n\n @vertex\n fn vs_main_quantized(input: QuantizedVertexInput) -> VertexOutput {\n let p = uniforms.quantParams.xyz\n + vec3<f32>(f32(input.q.x), f32(input.q.y), f32(input.q.z)) * uniforms.quantParams.w;\n return shadeFlatVertex(p, octDecodeN(input.q.w), input.entityId);\n }\n\n @vertex\n fn vs_main(input: VertexInput, @builtin(instance_index) instanceIndex: u32) -> VertexOutput {\n var output: VertexOutput;\n let worldPos = uniforms.model * vec4<f32>(input.position, 1.0);\n let eyePos = rtePosition(input.position).xyz;\n let rte = (uniforms.flags.x & RTE_DRAWABLE_FLAG) != 0u;\n output.position = select(uniforms.viewProj * worldPos, uniforms.rteViewProj * vec4<f32>(eyePos, 1.0), rte);\n // Anti z-fighting: deterministic depth nudge.\n // Knuth multiplicative hash spreads sequential IDs across 0-255 so\n // coplanar faces from different entities always get distinct depths.\n // Material-layer walls slice into one closed solid per layer, all\n // sharing the PARENT wall's expressId, so adjacent layers' coincident\n // interface caps would get the same entity nudge and z-fight into a\n // flickering comb (\"see inside the wall\"). To separate them we fold in\n // an 8-bit MATERIAL-COLOUR salt that mergeGeometry/interleaveTextured\n // baked into the HIGH 8 bits of the entityId lane (low 24 = picking id,\n // masked off by encodeId24). Crucially the salt comes from the mesh's\n // OWN colour, NOT the per-draw baseColor uniform \u2014 so the base opaque\n // pass and the lens/IDS/compare/4D OVERLAY pass (which redraws the same\n // geometry with a DIFFERENT draw colour) compute the SAME nudge, and\n // the overlay pipeline's depthCompare:'equal' matches instead of\n // rejecting every fragment. At 1e-6 per step the max world-space offset\n // is <3mm at 10m \u2014 invisible.\n let colorSalt = (input.entityId >> 24u) * 2654435761u;\n let zHash = (((input.entityId & 0x00FFFFFFu) ^ colorSalt) * 2654435761u) & 255u;\n output.position.z *= 1.0 + f32(zHash) * 1e-6;\n output.worldPos = worldPos.xyz;\n output.normal = normalize((uniforms.model * vec4<f32>(input.normal, 0.0)).xyz);\n output.entityId = input.entityId;\n output.color = uniforms.baseColor;\n output.instSelected = 0u; // flat path selects via uniforms.flags.x\n output.eyePos = eyePos;\n // Store view-space position for edge detection\n output.viewPos = select((uniforms.viewProj * worldPos).xyz, (uniforms.rteViewProj * vec4<f32>(eyePos, 1.0)).xyz, rte);\n return output;\n }\n\n // Instanced vertex entry \u2014 one template's geometry drawn once per\n // occurrence. The per-instance mat4 already folds SWAP * rel_k * T(origin)\n // (composed CPU-side, see instanced-render.ts), so it maps the template's\n // LOCAL vertex straight to WebGL Y-up world space \u2014 no uniforms.model.\n // rel_k and SWAP are rigid (no scale), so the same matrix transforms\n // normals. entityId + colour come per-occurrence from the instance buffer.\n @vertex\n fn vs_instanced(input: VertexInput, inst: InstanceInput) -> VertexOutput {\n var output: VertexOutput;\n let instMat = mat4x4<f32>(inst.m0, inst.m1, inst.m2, inst.m3);\n let worldPos = instMat * vec4<f32>(input.position, 1.0);\n let linearLocal = (instMat * vec4<f32>(input.position, 0.0)).xyz;\n let eyePos = rteInstancePosition(linearLocal, inst.anchorHigh.xyz, inst.anchorLow.xyz).xyz;\n output.position = uniforms.rteViewProj * vec4<f32>(eyePos, 1.0);\n // Same per-entity depth nudge as vs_main. No colour salt here: the\n // instanced path has no base-vs-overlay coincident redraw (yet), so the\n // raw picking id is enough to separate coplanar entities.\n let zHash = ((inst.instEntityId & 0x00FFFFFFu) * 2654435761u) & 255u;\n output.position.z *= 1.0 + f32(zHash) * 1e-6;\n output.worldPos = worldPos.xyz;\n output.normal = normalize((instMat * vec4<f32>(input.normal, 0.0)).xyz);\n output.entityId = inst.instEntityId;\n output.color = inst.instColor;\n output.instSelected = inst.instSelected;\n output.eyePos = eyePos;\n output.viewPos = (uniforms.rteViewProj * vec4<f32>(eyePos, 1.0)).xyz;\n return output;\n }\n\n fn encodeId24(id: u32) -> vec4<f32> {\n let r = f32((id >> 16u) & 255u) / 255.0;\n let g = f32((id >> 8u) & 255u) / 255.0;\n let b = f32(id & 255u) / 255.0;\n return vec4<f32>(r, g, b, 1.0);\n }\n\n struct FragmentOutput {\n @location(0) color: vec4<f32>,\n @location(1) objectIdEncoded: vec4<f32>,\n }\n\n @fragment\n fn fs_main(input: VertexOutput) -> FragmentOutput {\n // The flat/quantized/textured mesh paths submit this in one\n // camera-relative frame. Do all camera-local fragment arithmetic in\n // that frame; otherwise the RTE vertex precision is thrown away at\n // section/crop/derivative ingress. Instanced geometry is not yet\n // anchored, so its worldPos remains the authoritative input.\n let fragmentPos = select(input.worldPos, input.eyePos, (uniforms.flags.x & RTE_DRAWABLE_FLAG) != 0u);\n // Per-instance hide/isolate: bit 1 of the instance flags lane marks a hidden\n // occurrence. Discard it so it neither draws nor writes depth (and the pick\n // pass applies the same discard, so it isn't pickable). vs_main writes\n // instSelected=0u for flat geometry, so this never affects the flat path.\n if ((input.instSelected & 2u) != 0u) {\n discard;\n }\n // Per-instance opacity routing (instanced passes only \u2014 flags.x bit 2). The\n // opaque instanced pass draws fully-opaque (or selected) occurrences; the\n // transparent instanced sub-pass (bit 3, alpha-blended) draws the rest. Discard\n // the occurrences belonging to the OTHER pass so each is drawn exactly once.\n // Lens-ghost / x-ray / compare write a low per-instance alpha into input.color.a.\n if ((uniforms.flags.x & 4u) != 0u) {\n let occOpaque = input.color.a >= 0.99 || (input.instSelected & 1u) != 0u;\n let transparentPass = (uniforms.flags.x & 8u) != 0u;\n if (transparentPass) {\n if (occOpaque) { discard; }\n } else {\n if (!occOpaque) { discard; }\n }\n }\n // Section plane clipping - discard fragments ABOVE the plane.\n // flags.y packs two bits: bit 0 = enabled, bit 1 = flipped.\n let sectionEnabled = (uniforms.flags.y & 1u) == 1u;\n if (sectionEnabled) {\n let planeNormal = uniforms.sectionPlane.xyz;\n let planeDistance = uniforms.sectionPlane.w;\n let flipped = (uniforms.flags.y & 2u) == 2u;\n let side = select(1.0, -1.0, flipped);\n let distToPlane = (dot(fragmentPos, planeNormal) - planeDistance) * side;\n if (distToPlane > 0.0) {\n discard;\n }\n }\n // Clip box (section / crop box): discard fragments OUTSIDE the AABB.\n // flags.y bit 2 = clip-box enabled.\n if ((uniforms.flags.y & 4u) != 0u) {\n let p = fragmentPos;\n if (any(p < uniforms.clipBoxMin.xyz) || any(p > uniforms.clipBoxMax.xyz)) {\n discard;\n }\n }\n\n // Compute normal via derivative-based flat shading.\n //\n // Industry-standard solution for BIM/CAD viewers \u2014 what\n // Three.js (material.flatShading = true), Autodesk Forge,\n // Speckle, and xeokit all do for opaque surfaces. Rationale:\n //\n // * BIM geometry is overwhelmingly flat surfaces (walls,\n // slabs, roofs, beams), and CSG operations (opening\n // subtraction, layer slicing) emit those surfaces as\n // dense strips of coplanar triangles. Per-vertex normal\n // averaging gives a SLIGHTLY-different normal at each\n // vertex due to f32 noise from boolean output; the\n // boundary between strips then reads as a visible darker/\n // brighter scar line \u2014 the horizontal striations on\n // walls, stripes on roofs, visible triangulation reports\n // across every CSG kernel we have tried (legacy BSP,\n // Manifold).\n // * cross(dpdx, dpdy) of world position evaluates to the\n // EXACT face normal in the fragment shader. Every\n // fragment on a flat face \u2014 across an arbitrarily-fine\n // triangulation \u2014 gets the IDENTICAL normal, so coplanar\n // splits become invisible by construction. No CPU-side\n // welding, smooth-grouping, or coplanar-face merging\n // fixes the symptom as cleanly.\n //\n // Trade-off: genuinely curved surfaces (cylinder tessellations,\n // BSpline approximations) shade with visible facets. For BIM\n // that's acceptable \u2014 curved surfaces are < 5 % of typical\n // model triangle count and the faceting matches CAD-tool\n // (Revit, ArchiCAD) on-screen behaviour at default quality.\n //\n // We still fall back to the vertex normal when derivatives\n // are unavailable (extreme polygon degeneracy where dpdx /\n // dpdy collapse to zero \u2014 practically never on real geometry).\n let faceN = cross(dpdx(fragmentPos), dpdy(fragmentPos));\n let fLen2 = dot(faceN, faceN);\n var N: vec3<f32>;\n if (fLen2 > 1e-10) {\n N = faceN * inverseSqrt(fLen2);\n } else {\n // Degenerate derivative \u2014 fall back to the vertex normal\n // if it's populated, else +Y.\n N = input.normal;\n let nLen2 = dot(N, N);\n if (nLen2 > 1e-6) {\n N = N * inverseSqrt(nLen2);\n } else {\n N = vec3<f32>(0.0, 1.0, 0.0);\n }\n }\n\n // Stabilize the SIGN of the derivative face normal with the vertex\n // normal. The screen-space cross product gives the exact face\n // normal DIRECTION for coplanar strips (the scar-line fix), but at\n // grazing angles its SIGN becomes numerically unstable per quad \u2014\n // hemisphere/rim lighting then band-flips across large regions of\n // flat walls/slabs (diagonal lighter/darker bands). The interpolated\n // vertex normal is quad-noise-free, so use it only to orient N.\n // Guard: skip when the vertex normal is missing or nearly\n // perpendicular to the face normal (unreliable witness).\n let vN = input.normal;\n let alignDot = dot(N, vN);\n if (alignDot * alignDot > 0.03 * dot(vN, vN)) {\n N = N * sign(alignDot);\n }\n\n // Lighting environment \u2014 sun/hemisphere/exposure come from the\n // global env uniform. The fill follows the sun (it bounces in from\n // the opposite side); the rim stays fixed in world space as a\n // stylistic shaping light.\n let sunLight = env.sunDirection;\n // Horizontal mirror of the sun, lifted slightly. An overhead sun\n // leaves (0, 0.25, 0), which normalizes to straight up.\n let fillLight = normalize(vec3<f32>(-sunLight.x, 0.25, -sunLight.z));\n let rimLight = normalize(vec3<f32>(0.0, 0.2, -1.0)); // Rim light for edge definition\n\n // Hemisphere ambient. This, not the sun, keeps faces turned away\n // from the sun readable (I-beam webs and flange undersides, #5382).\n let hemisphereFactor = N.y * 0.5 + 0.5;\n let ambient = mix(env.groundColor, env.skyColor, hemisphereFactor) * env.ambientIntensity;\n\n // One-sided sun, so a building has a lit side and a shaded side.\n // sunSoftness is the diffuse wrap (env uniform): 0 = crisp\n // terminator (hard shadows), larger = softer wrap-around (overcast).\n let NdotL = dot(N, sunLight);\n let wrap = env.sunSoftness;\n let diffuseSun = max((NdotL + wrap) / (1.0 + wrap), 0.0) * env.sunIntensity;\n\n // Fill light, one-sided like the sun.\n let NdotFill = max(dot(N, fillLight), 0.0);\n let diffuseFill = NdotFill * env.fillIntensity;\n\n // Rim light for edge definition\n let NdotRim = max(dot(N, rimLight), 0.0);\n let rim = pow(NdotRim, 4.0) * env.rimIntensity;\n\n // The authored colour is display-referred sRGB; light it in linear.\n // (textured.wgsl.ts anchors on the first line to multiply in the texel.)\n var baseColor = input.color.rgb;\n baseColor = srgbToLinear(baseColor);\n\n // Combine all lighting. Only the DIRECT sun term is occluded by cast\n // shadows (#2670); ambient/fill/rim are indirect and stay unshadowed.\n // Exposure scales the light, so the selection shade below and every\n // later stage see one exposed irradiance.\n let sunShadow = sunShadowFactor(input.eyePos, N, input.position.xy);\n let lightTerm = ambient + env.sunColor * (diffuseSun * sunShadow) + vec3<f32>(diffuseFill + rim);\n let irradiance = lightTerm * (env.exposure * IRRADIANCE_CALIBRATION);\n var color = baseColor * irradiance;\n\n // flags.x is a bitfield:\n // bit 0 (value 1) = isSelected \u2192 selection-highlight + force opaque\n // bit 1 (value 2) = isOverlay \u2192 color-override pass; preserve\n // baseColor.a (overlay pipeline has\n // src-alpha blending) AND skip the\n // specular term, so an override\n // paints its colour, lit, and a\n // low-alpha ghost tint does not\n // pick up glass reflections.\n // Selected via the per-draw flag (flat path) OR the per-occurrence flag\n // (instanced path \u2014 vs_instanced reads it from the instance buffer).\n let isSelected = ((uniforms.flags.x & 1u) == 1u) || ((input.instSelected & 1u) == 1u);\n let isOverlay = (uniforms.flags.x & 2u) == 2u;\n\n // Selection highlight \u2014 a blue albedo RE-LIT by the scene lighting.\n //\n // We override the material albedo with selection-blue and re-light\n // it with the SAME lightTerm used for unselected surfaces, then\n // skip the view-dependent (specular) term below. Two requirements\n // are in tension and this satisfies both:\n //\n // * No base-material bleed-through. The old fresnel-glow mix left\n // ~80 % of the lit object colour visible at face centres (the\n // green-site / red-roof wash-out). Here the base colour never\n // enters the result \u2014 only lightTerm (geometry/light, colour-\n // independent) modulates the constant blue albedo.\n // * Facet/crease structure must survive. A single FLAT colour\n // (the previous fix) collapsed every face to the same blue, so\n // internal edges \u2014 which read as the per-face shading STEP, not\n // just the faint screen-space edge line \u2014 disappeared on\n // selection. Re-lighting keeps that per-face brightness step, so\n // creases read on the highlight exactly as they do unselected.\n //\n // The luminance of the exposed irradiance is used as a multiplicative\n // gain (which preserves the per-face brightness RATIOS, so creases\n // read as strongly as on the unselected surface). The calibration\n // puts a sunlit face at 1.0, i.e. full selection-blue, and the\n // floor/ceiling clamp keeps shadowed faces only dimmed and bright\n // scenes from washing out.\n if (isSelected) {\n let shadeLum = dot(irradiance, vec3<f32>(0.299, 0.587, 0.114));\n let shade = clamp(shadeLum, 0.45, 1.2);\n // selectionColor is already linear-light (set by Renderer.setOverlayTheme\n // from the app theme, #5484) \u2014 no srgbToLinear here, unlike the constant\n // it replaces.\n color = selectionColor.rgb * shade;\n }\n\n // flags.x bit 5 (value 32) = EMPHASIZE overlay: render the colour\n // override FULLY UNLIT and saturated (no lighting attenuation, no\n // wash-to-white) so the focused clash pair reads as a solid, vivid,\n // distinct colour that pops against the lit model \u2014 like a clash tool.\n // A faint normal-based shade keeps the silhouette from going flat. (#1277)\n let emphasizedOverlay = isOverlay && (uniforms.flags.x & 32u) != 0u;\n if (emphasizedOverlay) {\n let facet = 0.85 + 0.15 * abs(dot(N, normalize(vec3<f32>(0.3, 1.0, 0.2))));\n color = baseColor * facet;\n }\n\n // Force alpha to 1.0 for selected objects so the highlight is fully\n // opaque (the selection pipeline has no alpha blending). Emphasized\n // clash overlay paints a SOLID vivid fill (force opaque) so it isn't\n // blended down to a pale tint against the geometry beneath.\n var finalAlpha = select(input.color.a, 1.0, isSelected || emphasizedOverlay);\n\n // Specular (#5386; specular.wgsl.ts), after the diffuse above. Not\n // on the selection highlight (it would wash the blue out) nor on a\n // colour-override overlay. The sun lobe is scaled exactly like the\n // diffuse sun term, and both terms like irradiance, so a preset's\n // highlights and its diffuse light move together.\n if (!isSelected && !isOverlay) {\n // Glass is an authored translucent material (mesh-material.ts\n // also gives it its smooth roughness). An X-Ray or compare fade\n // only lowers the alpha and stays a fade. Instanced occurrences\n // are never authored translucent: prepareInstancedRender routes\n // those to the flat path, so the per-pass lane is ignored there.\n let instancedPass = (uniforms.flags.x & 4u) != 0u;\n let translucent = finalAlpha < 0.99;\n let glass = translucent && uniforms.transmission.x > 0.5 && !instancedPass;\n let metallic = clamp(uniforms.metallicRoughness.x, 0.0, 1.0);\n let spec = surfaceSpecular(\n N,\n normalize(-input.eyePos),\n baseColor,\n metallic,\n uniforms.metallicRoughness.y,\n env.sunColor * (env.sunIntensity * sunShadow),\n );\n // What the lobe reflects is not there to diffuse; a metal has no\n // diffuse at all.\n let diffuse = color * (1.0 - spec.reflectance) * (1.0 - metallic);\n let reflected = spec.light * (env.exposure * IRRADIANCE_CALIBRATION);\n if (translucent) {\n finalAlpha = finalAlpha * TRANSLUCENT_OPACITY_SCALE;\n }\n if (glass) {\n // The transparent pipelines blend straight alpha:\n // out = c * a + behind * (1 - a). A pane passes what is behind\n // it except what its body absorbs and its surface reflects, and\n // its reflection is not dimmed by the body's opacity, so solve\n // for (c, a). The reflectance rising at grazing angles is what\n // makes glass more opaque there; a bright sun glint raises the\n // alpha further so it is not clipped to the body's opacity.\n let body = finalAlpha;\n let reflectance = dot(spec.reflectance, vec3<f32>(0.299, 0.587, 0.114));\n let premultiplied = diffuse * body + reflected;\n let peak = max(premultiplied.r, max(premultiplied.g, premultiplied.b));\n finalAlpha = clamp(max(body + reflectance * (1.0 - body), peak), 0.0, 1.0);\n color = premultiplied / max(finalAlpha, 0.0001);\n } else {\n color = diffuse + reflected;\n }\n }\n\n // Hue-preserving highlight roll-off (color-transfer.wgsl.ts). No\n // contrast curve and no saturation boost: an authored colour lit at\n // unit irradiance leaves here unchanged unless it is brighter than\n // the roll-off threshold.\n color = neutralCompress(color);\n\n // Subtle edge enhancement using screen-space derivatives.\n //\n // Use the SHADED normal (face normal from dpdx/dpdy above)\n // for the normal-gradient term, not the interpolated vertex\n // normal \u2014 otherwise we get spurious dark stripes on flat\n // surfaces whose vertex normals carry numerical noise from\n // CSG output (the visible scar-line symptom would just\n // resurface here even after the lit-normal fix). With the\n // face normal, coplanar adjacent triangles agree exactly \u2192\n // zero normal gradient \u2192 no false edge; only the genuine\n // creases between perpendicular faces produce a real\n // gradient and get the intended outline.\n let depthGradient = length(vec2<f32>(\n dpdx(input.viewPos.z),\n dpdy(input.viewPos.z)\n ));\n let normalGradient = length(vec2<f32>(\n length(dpdx(N)),\n length(dpdy(N))\n ));\n\n if (uniforms.flags.z == 1u) {\n // Threshold filters subtle normal discontinuities at internal\n // triangle edges between coplanar entities in the same batch.\n let edgeFactor = smoothstep(0.02, 0.12, depthGradient * 10.0 + normalGradient * 5.0);\n let edgeIntensity = f32(uniforms.flags.w) / 1000.0;\n let edgeDarkenStrength = clamp(0.25 * edgeIntensity, 0.0, 0.85);\n let edgeDarken = mix(1.0, 1.0 - edgeDarkenStrength, edgeFactor);\n color *= edgeDarken;\n }\n\n color = linearToSrgb(clamp(color, vec3<f32>(0.0), vec3<f32>(1.0)));\n\n var out: FragmentOutput;\n out.color = vec4<f32>(color, finalAlpha);\n out.objectIdEncoded = encodeId24(input.entityId);\n return out;\n }\n ";
2
2
  //# sourceMappingURL=main.wgsl.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"main.wgsl.d.ts","sourceRoot":"","sources":["../../src/shaders/main.wgsl.ts"],"names":[],"mappings":"AAiCA,eAAO,MAAM,gBAAgB,y5kCA8iBtB,CAAC"}
1
+ {"version":3,"file":"main.wgsl.d.ts","sourceRoot":"","sources":["../../src/shaders/main.wgsl.ts"],"names":[],"mappings":"AA0CA,eAAO,MAAM,gBAAgB,04uCA0iBtB,CAAC"}