@ifc-lite/renderer 6.0.0 → 6.3.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.
- package/README.md +21 -0
- package/dist/ao-pass.d.ts +0 -16
- package/dist/ao-pass.d.ts.map +1 -1
- package/dist/ao-pass.js +2 -0
- package/dist/ao-pass.js.map +1 -1
- package/dist/camera-animation.d.ts +1 -1
- package/dist/camera-animation.d.ts.map +1 -1
- package/dist/camera-animation.js +11 -11
- package/dist/camera-animation.js.map +1 -1
- package/dist/camera-inertia.d.ts +27 -0
- package/dist/camera-inertia.d.ts.map +1 -0
- package/dist/camera-inertia.js +33 -0
- package/dist/camera-inertia.js.map +1 -0
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +3 -0
- package/dist/camera.js.map +1 -1
- package/dist/deviation/deviation-computer.d.ts +5 -1
- package/dist/deviation/deviation-computer.d.ts.map +1 -1
- package/dist/deviation/deviation-computer.js +13 -6
- package/dist/deviation/deviation-computer.js.map +1 -1
- package/dist/deviation/deviation-readback.d.ts +16 -3
- package/dist/deviation/deviation-readback.d.ts.map +1 -1
- package/dist/deviation/deviation-readback.js +46 -36
- package/dist/deviation/deviation-readback.js.map +1 -1
- package/dist/deviation/deviation-statistics-kernel.d.ts +111 -0
- package/dist/deviation/deviation-statistics-kernel.d.ts.map +1 -0
- package/dist/deviation/deviation-statistics-kernel.js +303 -0
- package/dist/deviation/deviation-statistics-kernel.js.map +1 -0
- package/dist/deviation/deviation-statistics.d.ts +100 -0
- package/dist/deviation/deviation-statistics.d.ts.map +1 -0
- package/dist/deviation/deviation-statistics.js +100 -0
- package/dist/deviation/deviation-statistics.js.map +1 -0
- package/dist/device-recovery.d.ts.map +1 -1
- package/dist/device-recovery.js +5 -0
- package/dist/device-recovery.js.map +1 -1
- package/dist/device.d.ts +0 -3
- package/dist/device.d.ts.map +1 -1
- package/dist/device.js +5 -1
- package/dist/device.js.map +1 -1
- package/dist/edge-pass.d.ts +0 -12
- package/dist/edge-pass.d.ts.map +1 -1
- package/dist/edge-pass.js +3 -0
- package/dist/edge-pass.js.map +1 -1
- package/dist/edl-pass.d.ts +0 -21
- package/dist/edl-pass.d.ts.map +1 -1
- package/dist/edl-pass.js +23 -0
- package/dist/edl-pass.js.map +1 -1
- package/dist/frame-timing-gpu.d.ts +11 -15
- package/dist/frame-timing-gpu.d.ts.map +1 -1
- package/dist/frame-timing-gpu.js +57 -26
- package/dist/frame-timing-gpu.js.map +1 -1
- package/dist/gpu-counters.d.ts +30 -0
- package/dist/gpu-counters.d.ts.map +1 -0
- package/dist/gpu-counters.js +84 -0
- package/dist/gpu-counters.js.map +1 -0
- package/dist/gpu-static-upload.d.ts.map +1 -1
- package/dist/gpu-static-upload.js +2 -0
- package/dist/gpu-static-upload.js.map +1 -1
- package/dist/index.d.ts +33 -21
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +70 -59
- package/dist/index.js.map +1 -1
- package/dist/picker-depth-sample.d.ts +9 -0
- package/dist/picker-depth-sample.d.ts.map +1 -0
- package/dist/picker-depth-sample.js +63 -0
- package/dist/picker-depth-sample.js.map +1 -0
- package/dist/picker-readbacks.d.ts +8 -1
- package/dist/picker-readbacks.d.ts.map +1 -1
- package/dist/picker-readbacks.js +18 -1
- package/dist/picker-readbacks.js.map +1 -1
- package/dist/picker.d.ts +2 -0
- package/dist/picker.d.ts.map +1 -1
- package/dist/picker.js +70 -94
- package/dist/picker.js.map +1 -1
- package/dist/picking-manager.d.ts +5 -2
- package/dist/picking-manager.d.ts.map +1 -1
- package/dist/picking-manager.js +25 -8
- package/dist/picking-manager.js.map +1 -1
- package/dist/pipeline.d.ts +8 -0
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +22 -0
- package/dist/pipeline.js.map +1 -1
- package/dist/pointcloud/point-cloud-keyed-chunks.d.ts +23 -0
- package/dist/pointcloud/point-cloud-keyed-chunks.d.ts.map +1 -0
- package/dist/pointcloud/point-cloud-keyed-chunks.js +44 -0
- package/dist/pointcloud/point-cloud-keyed-chunks.js.map +1 -0
- package/dist/pointcloud/point-cloud-node.d.ts +5 -1
- package/dist/pointcloud/point-cloud-node.d.ts.map +1 -1
- package/dist/pointcloud/point-cloud-node.js +8 -4
- package/dist/pointcloud/point-cloud-node.js.map +1 -1
- package/dist/pointcloud/point-cloud-placement.d.ts +6 -0
- package/dist/pointcloud/point-cloud-placement.d.ts.map +1 -1
- package/dist/pointcloud/point-cloud-placement.js +8 -0
- package/dist/pointcloud/point-cloud-placement.js.map +1 -1
- package/dist/pointcloud/point-cloud-ray-transform.d.ts +17 -8
- package/dist/pointcloud/point-cloud-ray-transform.d.ts.map +1 -1
- package/dist/pointcloud/point-cloud-ray-transform.js +17 -13
- package/dist/pointcloud/point-cloud-ray-transform.js.map +1 -1
- package/dist/pointcloud/point-cloud-renderer.d.ts +10 -8
- package/dist/pointcloud/point-cloud-renderer.d.ts.map +1 -1
- package/dist/pointcloud/point-cloud-renderer.js +20 -31
- package/dist/pointcloud/point-cloud-renderer.js.map +1 -1
- package/dist/pointcloud/point-cloud-stream-lifecycle.d.ts +8 -1
- package/dist/pointcloud/point-cloud-stream-lifecycle.d.ts.map +1 -1
- package/dist/pointcloud/point-cloud-stream-lifecycle.js +17 -2
- package/dist/pointcloud/point-cloud-stream-lifecycle.js.map +1 -1
- package/dist/pointcloud/point-cloud-uniforms.d.ts +11 -0
- package/dist/pointcloud/point-cloud-uniforms.d.ts.map +1 -1
- package/dist/pointcloud/point-cloud-uniforms.js +8 -0
- package/dist/pointcloud/point-cloud-uniforms.js.map +1 -1
- package/dist/post-pass-chain.d.ts +2 -0
- package/dist/post-pass-chain.d.ts.map +1 -1
- package/dist/post-pass-chain.js +1 -1
- package/dist/post-pass-chain.js.map +1 -1
- package/dist/raycast-point-cloud-query.d.ts.map +1 -1
- package/dist/raycast-point-cloud-query.js +11 -1
- package/dist/raycast-point-cloud-query.js.map +1 -1
- package/dist/renderer-frame-timing.d.ts +21 -0
- package/dist/renderer-frame-timing.d.ts.map +1 -0
- package/dist/renderer-frame-timing.js +104 -0
- package/dist/renderer-frame-timing.js.map +1 -0
- package/dist/renderer-viewport.d.ts +13 -7
- package/dist/renderer-viewport.d.ts.map +1 -1
- package/dist/renderer-viewport.js +15 -7
- package/dist/renderer-viewport.js.map +1 -1
- package/dist/scene-batch-rebuild.d.ts +7 -0
- package/dist/scene-batch-rebuild.d.ts.map +1 -1
- package/dist/scene-batch-rebuild.js +12 -0
- package/dist/scene-batch-rebuild.js.map +1 -1
- package/dist/scene-contents.d.ts +3 -2
- package/dist/scene-contents.d.ts.map +1 -1
- package/dist/scene-flush.d.ts +43 -0
- package/dist/scene-flush.d.ts.map +1 -0
- package/dist/scene-flush.js +103 -0
- package/dist/scene-flush.js.map +1 -0
- package/dist/scene-geometry.d.ts.map +1 -1
- package/dist/scene-geometry.js +2 -0
- package/dist/scene-geometry.js.map +1 -1
- package/dist/scene-precision.d.ts +1 -1
- package/dist/scene-precision.d.ts.map +1 -1
- package/dist/scene-precision.js +11 -2
- package/dist/scene-precision.js.map +1 -1
- package/dist/scene.d.ts +10 -27
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +38 -107
- package/dist/scene.js.map +1 -1
- package/dist/selection-mask-pass.d.ts +2 -27
- package/dist/selection-mask-pass.d.ts.map +1 -1
- package/dist/selection-mask-pass.js +44 -39
- package/dist/selection-mask-pass.js.map +1 -1
- package/dist/selection-mask-pipelines.d.ts +3 -3
- package/dist/selection-mask-pipelines.d.ts.map +1 -1
- package/dist/selection-mask-pipelines.js +11 -8
- package/dist/selection-mask-pipelines.js.map +1 -1
- package/dist/shaders/depth-nudge.wgsl.d.ts +67 -0
- package/dist/shaders/depth-nudge.wgsl.d.ts.map +1 -0
- package/dist/shaders/depth-nudge.wgsl.js +130 -0
- package/dist/shaders/depth-nudge.wgsl.js.map +1 -0
- package/dist/shaders/ghost.wgsl.d.ts +2 -0
- package/dist/shaders/ghost.wgsl.d.ts.map +1 -0
- package/dist/shaders/ghost.wgsl.js +46 -0
- package/dist/shaders/ghost.wgsl.js.map +1 -0
- package/dist/shaders/main.wgsl.d.ts +1 -1
- package/dist/shaders/main.wgsl.d.ts.map +1 -1
- package/dist/shaders/main.wgsl.js +6 -4
- package/dist/shaders/main.wgsl.js.map +1 -1
- package/dist/shaders/section-2d-overlay.wgsl.d.ts +1 -16
- package/dist/shaders/section-2d-overlay.wgsl.d.ts.map +1 -1
- package/dist/shaders/section-2d-overlay.wgsl.js +6 -6
- package/dist/shaders/section-2d-overlay.wgsl.js.map +1 -1
- package/dist/shaders/selection-mask.wgsl.d.ts +2 -10
- package/dist/shaders/selection-mask.wgsl.d.ts.map +1 -1
- package/dist/shaders/selection-mask.wgsl.js +9 -42
- package/dist/shaders/selection-mask.wgsl.js.map +1 -1
- package/dist/shaders/symbolic-overlay.wgsl.d.ts +1 -1
- package/dist/shaders/symbolic-overlay.wgsl.d.ts.map +1 -1
- package/dist/shaders/symbolic-overlay.wgsl.js +5 -4
- package/dist/shaders/symbolic-overlay.wgsl.js.map +1 -1
- package/dist/shadow-pass.d.ts +0 -19
- package/dist/shadow-pass.d.ts.map +1 -1
- package/dist/shadow-pass.js +19 -0
- package/dist/shadow-pass.js.map +1 -1
- package/dist/test/frame-timing-device.d.ts +17 -0
- package/dist/test/frame-timing-device.d.ts.map +1 -0
- package/dist/test/frame-timing-device.js +70 -0
- package/dist/test/frame-timing-device.js.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/xray-alpha.d.ts +8 -0
- package/dist/xray-alpha.d.ts.map +1 -1
- package/dist/xray-alpha.js +23 -0
- package/dist/xray-alpha.js.map +1 -1
- package/package.json +4 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"depth-nudge.wgsl.js","sourceRoot":"","sources":["../../src/shaders/depth-nudge.wgsl.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAE/D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AAEH,0DAA0D;AAC1D,MAAM,CAAC,MAAM,gCAAgC,GAAG,IAAI,CAAC;AAErD,kGAAkG;AAClG,MAAM,CAAC,MAAM,kCAAkC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;AAE/D,iFAAiF;AACjF,MAAM,CAAC,MAAM,mCAAmC,GAAG,KAAK,CAAC;AAEzD,uDAAuD;AACvD,MAAM,CAAC,MAAM,oBAAoB,GAAG,GAAG,CAAC;AAExC,0FAA0F;AAC1F,MAAM,CAAC,MAAM,8BAA8B,GAAG,IAAI,CAAC;AAEnD,MAAM,cAAc,GAAG;0DACmC,kCAAkC;2DACjC,mCAAmC;4CAClD,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2B/D,CAAC;AAEF,MAAM,CAAC,MAAM,cAAc,GAAG;UACpB,cAAc;wDACgC,gCAAgC;;;;;;;;;;CAUvF,CAAC;AAEF,MAAM,CAAC,MAAM,oBAAoB,GAAG;UAC1B,cAAc;sDAC8B,8BAA8B;;;;;;;;;;;;;;;CAenF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ghost.wgsl.d.ts","sourceRoot":"","sources":["../../src/shaders/ghost.wgsl.ts"],"names":[],"mappings":"AAkDA,eAAO,MAAM,iBAAiB,QAAsC,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
* Fragment shading for X-Ray context ghosts (`ghostExceptIds`), derived from
|
|
6
|
+
* main.wgsl.ts the way textured.wgsl.ts is. A ghost keeps the main shader's
|
|
7
|
+
* discards, flat face normal and diffuse light rig, so it reads as the same
|
|
8
|
+
* surface faded, and returns before the specular term, cast shadows, the
|
|
9
|
+
* selection tint and the entity colour override. At the default ghost alpha
|
|
10
|
+
* none of those show, but blended geometry gets no hidden-surface removal, so
|
|
11
|
+
* every ghosted layer under a pixel paid for them.
|
|
12
|
+
*/
|
|
13
|
+
import { mainShaderSource } from './main.wgsl.js';
|
|
14
|
+
function indexOfOnce(src, find, label) {
|
|
15
|
+
const idx = src.indexOf(find);
|
|
16
|
+
if (idx === -1) {
|
|
17
|
+
throw new Error(`ghost.wgsl: anchor "${label}" not found — main.wgsl.ts changed; update the ghost-shader derivation.`);
|
|
18
|
+
}
|
|
19
|
+
if (src.indexOf(find, idx + find.length) !== -1) {
|
|
20
|
+
throw new Error(`ghost.wgsl: anchor "${label}" is not unique — tighten the derivation.`);
|
|
21
|
+
}
|
|
22
|
+
return idx;
|
|
23
|
+
}
|
|
24
|
+
const GHOST_TAIL = ' var color = baseColor * irradiance;\n' +
|
|
25
|
+
' var ghost: FragmentOutput;\n' +
|
|
26
|
+
' ghost.color = vec4<f32>(\n' +
|
|
27
|
+
' linearToSrgb(clamp(neutralCompress(color), vec3<f32>(0.0), vec3<f32>(1.0))),\n' +
|
|
28
|
+
' input.color.a * TRANSLUCENT_OPACITY_SCALE,\n' +
|
|
29
|
+
' );\n' +
|
|
30
|
+
' ghost.objectIdEncoded = encodeId24(input.entityId);\n' +
|
|
31
|
+
' return ghost;\n';
|
|
32
|
+
function deriveGhostShader(src) {
|
|
33
|
+
const shadow = 'let sunShadow = sunShadowFactor(input.eyePos, N, input.position.xy);';
|
|
34
|
+
const shadowAt = indexOfOnce(src, shadow, 'sun shadow');
|
|
35
|
+
const s = src.slice(0, shadowAt) + 'let sunShadow = 1.0;' + src.slice(shadowAt + shadow.length);
|
|
36
|
+
// Cut fs_main from the diffuse colour to its closing brace, so no dead code
|
|
37
|
+
// is left for the compiler to warn about. fs_main must be the last function.
|
|
38
|
+
const tailStart = indexOfOnce(s, ' var color = baseColor * irradiance;\n', 'diffuse colour');
|
|
39
|
+
const tailEnd = s.lastIndexOf('\n }');
|
|
40
|
+
if (tailEnd < tailStart || s.slice(tailStart, tailEnd).includes('\n fn ')) {
|
|
41
|
+
throw new Error('ghost.wgsl: fs_main is no longer the last function in main.wgsl.ts; update the ghost-shader derivation.');
|
|
42
|
+
}
|
|
43
|
+
return s.slice(0, tailStart) + GHOST_TAIL + s.slice(tailEnd + 1);
|
|
44
|
+
}
|
|
45
|
+
export const ghostShaderSource = deriveGhostShader(mainShaderSource);
|
|
46
|
+
//# sourceMappingURL=ghost.wgsl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ghost.wgsl.js","sourceRoot":"","sources":["../../src/shaders/ghost.wgsl.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAE/D;;;;;;;;GAQG;AACH,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,SAAS,WAAW,CAAC,GAAW,EAAE,IAAY,EAAE,KAAa;IAC3D,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,IAAI,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,uBAAuB,KAAK,yEAAyE,CAAC,CAAC;IACzH,CAAC;IACD,IAAI,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;QAChD,MAAM,IAAI,KAAK,CAAC,uBAAuB,KAAK,2CAA2C,CAAC,CAAC;IAC3F,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,GACd,iDAAiD;IACjD,wCAAwC;IACxC,sCAAsC;IACtC,4FAA4F;IAC5F,0DAA0D;IAC1D,gBAAgB;IAChB,iEAAiE;IACjE,2BAA2B,CAAC;AAE9B,SAAS,iBAAiB,CAAC,GAAW;IACpC,MAAM,MAAM,GAAG,sEAAsE,CAAC;IACtF,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,CAAC,CAAC;IACxD,MAAM,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,GAAG,sBAAsB,GAAG,GAAG,CAAC,KAAK,CAAC,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IAChG,4EAA4E;IAC5E,6EAA6E;IAC7E,MAAM,SAAS,GAAG,WAAW,CAAC,CAAC,EAAE,iDAAiD,EAAE,gBAAgB,CAAC,CAAC;IACtG,MAAM,OAAO,GAAG,CAAC,CAAC,WAAW,CAAC,aAAa,CAAC,CAAC;IAC7C,IAAI,OAAO,GAAG,SAAS,IAAI,CAAC,CAAC,KAAK,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC;QACjF,MAAM,IAAI,KAAK,CAAC,yGAAyG,CAAC,CAAC;IAC7H,CAAC;IACD,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,GAAG,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,CAAC,MAAM,iBAAiB,GAAG,iBAAiB,CAAC,gBAAgB,CAAC,CAAC"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const mainShaderSource = "\n \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, w unused (0) since #5746 dropped the derivative edges\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 overrideParams: vec4<u32>, // x = draw's id anchor, y = OVERRIDE_* bits (entity-color-table.ts, #6076)\n }\n @binding(0) @group(0) var<uniform> uniforms: Uniforms;\n const RTE_DRAWABLE_FLAG: u32 = 65536u;\n\n // The fragment position the section/crop tests run in: camera-relative\n // for RTE drawables (the packer writes plane and box in that frame),\n // world space otherwise.\n fn clipSpacePos(worldPos: vec3<f32>, eyePos: vec3<f32>) -> vec3<f32> {\n return select(worldPos, eyePos, (uniforms.flags.x & RTE_DRAWABLE_FLAG) != 0u);\n }\n\n // True when p is cut away: ABOVE the section plane (flags.y bit 0 =\n // enabled, bit 1 = flipped) or OUTSIDE the clip box (bit 2).\n fn sectionClipped(p: vec3<f32>) -> bool {\n if ((uniforms.flags.y & 1u) == 1u) {\n let side = select(1.0, -1.0, (uniforms.flags.y & 2u) == 2u);\n if ((dot(p, uniforms.sectionPlane.xyz) - uniforms.sectionPlane.w) * side > 0.0) { return true; }\n }\n if ((uniforms.flags.y & 4u) != 0u) {\n if (any(p < uniforms.clipBoxMin.xyz) || any(p > uniforms.clipBoxMax.xyz)) { return true; }\n }\n return false;\n }\n\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 @binding(5) @group(1) var<storage, read> entityColorTable: array<u32>;\n const ENTITY_COLOR_EMPTY: u32 = 4294967295u;\n const ENTITY_COLOR_HEADER: u32 = 4u;\n const OVERRIDE_PAINT: u32 = 1u;\n const OVERRIDE_EMPHASIZE: u32 = 2u;\n\n fn entityColorSlotHash(key: u32) -> u32 {\n var h = key;\n h = h ^ (h >> 16u);\n h = h * 0x85ebca6bu;\n h = h ^ (h >> 13u);\n h = h * 0xc2b2ae35u;\n h = h ^ (h >> 16u);\n return h;\n }\n\n // Full scene id behind a 24-bit lane, given the draw's smallest id.\n fn resolveLaneEntityId(anchor: u32, lane: u32) -> u32 {\n return anchor + ((lane - anchor) & 0x00FFFFFFu);\n }\n\n // The override colour for this fragment's entity; alpha < 0 = none.\n fn entityOverrideColor(lane: u32) -> vec4<f32> {\n let none = vec4<f32>(0.0, 0.0, 0.0, -1.0);\n if ((uniforms.overrideParams.y & OVERRIDE_PAINT) == 0u || entityColorTable[2] == 0u) {\n return none;\n }\n let capacity = entityColorTable[0];\n let maxProbe = entityColorTable[1];\n let colorBase = entityColorTable[3];\n let id = resolveLaneEntityId(uniforms.overrideParams.x, lane);\n let mask = capacity - 1u;\n var slot = entityColorSlotHash(id) & mask;\n for (var i = 0u; i <= maxProbe && i < capacity; i = i + 1u) {\n let key = entityColorTable[ENTITY_COLOR_HEADER + slot];\n if (key == id) {\n let c = colorBase + slot * 4u;\n return vec4<f32>(\n bitcast<f32>(entityColorTable[c]),\n bitcast<f32>(entityColorTable[c + 1u]),\n bitcast<f32>(entityColorTable[c + 2u]),\n bitcast<f32>(entityColorTable[c + 3u])\n );\n }\n if (key == ENTITY_COLOR_EMPTY) {\n break;\n }\n slot = (slot + 1u) & mask;\n }\n return none;\n }\n\n // Composite an override over the fragment's own encoded colour exactly\n // as the retired equal-depth overlay pass blended it (src-alpha over\n // the base draw): the override albedo lit by the same irradiance, no\n // specular, the same roll-off and edge darkening, mixed by its alpha.\n // Emphasized overrides are unlit, faintly faceted and opaque (#1277).\n fn paintEntityOverride(base: vec4<f32>, ov: vec4<f32>, irradiance: vec3<f32>, N: vec3<f32>) -> vec4<f32> {\n let albedo = srgbToLinear(ov.rgb);\n var lit = albedo * irradiance;\n var a = ov.a;\n if ((uniforms.overrideParams.y & OVERRIDE_EMPHASIZE) != 0u) {\n lit = albedo * (0.85 + 0.15 * abs(dot(N, normalize(vec3<f32>(0.3, 1.0, 0.2)))));\n a = 1.0;\n }\n let encoded = linearToSrgb(clamp(neutralCompress(lit), vec3<f32>(0.0), vec3<f32>(1.0)));\n return vec4<f32>(mix(base.rgb, encoded, a), a + (1.0 - a) * base.a);\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 // 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 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 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 every redraw of\n // the same geometry with a different draw colour (the selection\n // highlight's greater-equal pass) computes the SAME nudge as its\n // batch. At 1e-6 per step the max world-space offset is <3mm at 10m.\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 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 never redraws an occurrence with a second draw\n // colour, so the 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 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 = clipSpacePos(input.worldPos, input.eyePos);\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 / clip box (mesh-uniforms.wgsl.ts, shared with the\n // selection mask so an outline is cut where its surface is).\n if (sectionClipped(fragmentPos)) { discard; }\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 bit 0 (value 1) = isSelected \u2192 selection highlight, forced opaque.\n // bit 1 (value 2) = isOverlay \u2192 legacy overlay pipeline callers;\n // preserve alpha and skip specular for their blended draw.\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 // Public getOverlayPipeline() callers can still use the emphasized\n // overlay bit even though internal colour overrides now use a table.\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 // Selected objects and emphasized overlays draw fully opaque.\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 or legacy overlay (it would wash out\n // their colours); a table override is composited without it below. The\n // sun lobe is scaled exactly like the diffuse sun term, and both\n // terms like irradiance, so a preset's highlights and diffuse 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 // #5984: an instanced occurrence's own authored finish rides its\n // flags lane (instanced-render.ts packInstanceFinish); the pass's\n // uniform row is the shared default.\n var metallicRoughness = vec2<f32>(uniforms.metallicRoughness.x, uniforms.metallicRoughness.y);\n if (instancedPass && (input.instSelected & 4u) != 0u) {\n metallicRoughness.x = f32((input.instSelected >> 16u) & 255u) / 255.0;\n }\n if (instancedPass && (input.instSelected & 8u) != 0u) {\n metallicRoughness.y = f32((input.instSelected >> 24u) & 255u) / 255.0;\n }\n let metallic = clamp(metallicRoughness.x, 0.0, 1.0);\n let spec = surfaceSpecular(\n N,\n normalize(-input.eyePos),\n baseColor,\n metallic,\n 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 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 // Colour override from the per-entity table (#6076): only draws the\n // renderer marks (overrideParams.y), i.e. depth-writing opaque ones.\n let entityOverride = entityOverrideColor(input.entityId);\n if (entityOverride.a >= 0.0 && !isSelected) {\n out.color = paintEntityOverride(out.color, entityOverride, irradiance, N);\n }\n out.objectIdEncoded = encodeId24(input.entityId);\n return out;\n }\n ";
|
|
1
|
+
export declare const mainShaderSource = "\n \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, w unused (0) since #5746 dropped the derivative edges\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 overrideParams: vec4<u32>, // x = draw's id anchor, y = OVERRIDE_* bits (entity-color-table.ts, #6076)\n }\n @binding(0) @group(0) var<uniform> uniforms: Uniforms;\n const RTE_DRAWABLE_FLAG: u32 = 65536u;\n\n // The fragment position the section/crop tests run in: camera-relative\n // for RTE drawables (the packer writes plane and box in that frame),\n // world space otherwise.\n fn clipSpacePos(worldPos: vec3<f32>, eyePos: vec3<f32>) -> vec3<f32> {\n return select(worldPos, eyePos, (uniforms.flags.x & RTE_DRAWABLE_FLAG) != 0u);\n }\n\n // True when p is cut away: ABOVE the section plane (flags.y bit 0 =\n // enabled, bit 1 = flipped) or OUTSIDE the clip box (bit 2).\n fn sectionClipped(p: vec3<f32>) -> bool {\n if ((uniforms.flags.y & 1u) == 1u) {\n let side = select(1.0, -1.0, (uniforms.flags.y & 2u) == 2u);\n if ((dot(p, uniforms.sectionPlane.xyz) - uniforms.sectionPlane.w) * side > 0.0) { return true; }\n }\n if ((uniforms.flags.y & 4u) != 0u) {\n if (any(p < uniforms.clipBoxMin.xyz) || any(p > uniforms.clipBoxMax.xyz)) { return true; }\n }\n return false;\n }\n\n \n \n const ORTHOGRAPHIC_DEPTH_NUDGE_PER_LEVEL: f32 = 4.76837158203125e-7;\n const ORTHOGRAPHIC_MAX_DEPTH_NUDGE_METRES: f32 = 0.025;\n const MAX_DEPTH_NUDGE_HASH: f32 = 255.0;\n\n // An orthographic view-projection leaves w at 1: its bottom row is exactly\n // (0, 0, 0, 1), because the projection's and the view's bottom rows are\n // both (0, 0, 0, 1). A perspective one copies the forward axis into that\n // row, a unit vector, so it is never zero. Every caller passes the\n // camera's world view-projection, which each draw packs whether or not\n // it renders camera-relative.\n fn isOrthographicProjection(viewProj: mat4x4<f32>) -> bool {\n return viewProj[0][3] == 0.0 && viewProj[1][3] == 0.0 && viewProj[2][3] == 0.0 && viewProj[3][3] == 1.0;\n }\n\n // Orthographic only: how many hash levels fit the metre bound. The z row\n // of the view-projection has length 1 / (far - near).\n fn orthographicNudgeLevels(viewProj: mat4x4<f32>) -> u32 {\n let depthRange = 1.0 / length(vec3<f32>(viewProj[0][2], viewProj[1][2], viewProj[2][2]));\n let steps = ORTHOGRAPHIC_MAX_DEPTH_NUDGE_METRES / (depthRange * ORTHOGRAPHIC_DEPTH_NUDGE_PER_LEVEL);\n return u32(clamp(floor(steps), 0.0, MAX_DEPTH_NUDGE_HASH)) + 1u;\n }\n\n // Orthographic only: clip z moved `ndcOffset` toward the camera without\n // changing which side of a clip plane the vertex is on. A vertex beyond\n // either plane, or exactly on the far plane, stays where it is.\n fn orthographicOffsetInsideDepthRange(clip: vec4<f32>, ndcOffset: f32) -> f32 {\n if (!(clip.z > 0.0 && clip.z < clip.w)) { return clip.z; }\n return min(clip.z + ndcOffset * clip.w, clip.w);\n }\n\n const PERSPECTIVE_DEPTH_NUDGE_PER_STEP: f32 = 0.000001;\n\n // Clip-space z with the entity's depth nudge applied (depth-nudge.wgsl.ts).\n fn nudgedClipZ(clip: vec4<f32>, zHash: u32, viewProj: mat4x4<f32>) -> f32 {\n if (isOrthographicProjection(viewProj)) {\n let level = (zHash * orthographicNudgeLevels(viewProj)) >> 8u;\n return orthographicOffsetInsideDepthRange(clip, f32(level) * ORTHOGRAPHIC_DEPTH_NUDGE_PER_LEVEL);\n }\n return clip.z * (1.0 + f32(zHash) * PERSPECTIVE_DEPTH_NUDGE_PER_STEP);\n }\n\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 @binding(5) @group(1) var<storage, read> entityColorTable: array<u32>;\n const ENTITY_COLOR_EMPTY: u32 = 4294967295u;\n const ENTITY_COLOR_HEADER: u32 = 4u;\n const OVERRIDE_PAINT: u32 = 1u;\n const OVERRIDE_EMPHASIZE: u32 = 2u;\n\n fn entityColorSlotHash(key: u32) -> u32 {\n var h = key;\n h = h ^ (h >> 16u);\n h = h * 0x85ebca6bu;\n h = h ^ (h >> 13u);\n h = h * 0xc2b2ae35u;\n h = h ^ (h >> 16u);\n return h;\n }\n\n // Full scene id behind a 24-bit lane, given the draw's smallest id.\n fn resolveLaneEntityId(anchor: u32, lane: u32) -> u32 {\n return anchor + ((lane - anchor) & 0x00FFFFFFu);\n }\n\n // The override colour for this fragment's entity; alpha < 0 = none.\n fn entityOverrideColor(lane: u32) -> vec4<f32> {\n let none = vec4<f32>(0.0, 0.0, 0.0, -1.0);\n if ((uniforms.overrideParams.y & OVERRIDE_PAINT) == 0u || entityColorTable[2] == 0u) {\n return none;\n }\n let capacity = entityColorTable[0];\n let maxProbe = entityColorTable[1];\n let colorBase = entityColorTable[3];\n let id = resolveLaneEntityId(uniforms.overrideParams.x, lane);\n let mask = capacity - 1u;\n var slot = entityColorSlotHash(id) & mask;\n for (var i = 0u; i <= maxProbe && i < capacity; i = i + 1u) {\n let key = entityColorTable[ENTITY_COLOR_HEADER + slot];\n if (key == id) {\n let c = colorBase + slot * 4u;\n return vec4<f32>(\n bitcast<f32>(entityColorTable[c]),\n bitcast<f32>(entityColorTable[c + 1u]),\n bitcast<f32>(entityColorTable[c + 2u]),\n bitcast<f32>(entityColorTable[c + 3u])\n );\n }\n if (key == ENTITY_COLOR_EMPTY) {\n break;\n }\n slot = (slot + 1u) & mask;\n }\n return none;\n }\n\n // Composite an override over the fragment's own encoded colour exactly\n // as the retired equal-depth overlay pass blended it (src-alpha over\n // the base draw): the override albedo lit by the same irradiance, no\n // specular, the same roll-off and edge darkening, mixed by its alpha.\n // Emphasized overrides are unlit, faintly faceted and opaque (#1277).\n fn paintEntityOverride(base: vec4<f32>, ov: vec4<f32>, irradiance: vec3<f32>, N: vec3<f32>) -> vec4<f32> {\n let albedo = srgbToLinear(ov.rgb);\n var lit = albedo * irradiance;\n var a = ov.a;\n if ((uniforms.overrideParams.y & OVERRIDE_EMPHASIZE) != 0u) {\n lit = albedo * (0.85 + 0.15 * abs(dot(N, normalize(vec3<f32>(0.3, 1.0, 0.2)))));\n a = 1.0;\n }\n let encoded = linearToSrgb(clamp(neutralCompress(lit), vec3<f32>(0.0), vec3<f32>(1.0)));\n return vec4<f32>(mix(base.rgb, encoded, a), a + (1.0 - a) * base.a);\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 // 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 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 = nudgedClipZ(output.position, zHash, uniforms.viewProj);\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 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 every redraw of\n // the same geometry with a different draw colour (the selection\n // highlight's greater-equal pass) computes the SAME nudge as its\n // batch. The step size per projection is in depth-nudge.wgsl.ts.\n let colorSalt = (input.entityId >> 24u) * 2654435761u;\n let zHash = (((input.entityId & 0x00FFFFFFu) ^ colorSalt) * 2654435761u) & 255u;\n output.position.z = nudgedClipZ(output.position, zHash, uniforms.viewProj);\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 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 never redraws an occurrence with a second draw\n // colour, so the raw picking id is enough to separate coplanar entities.\n let zHash = ((inst.instEntityId & 0x00FFFFFFu) * 2654435761u) & 255u;\n output.position.z = nudgedClipZ(output.position, zHash, uniforms.viewProj);\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 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 = clipSpacePos(input.worldPos, input.eyePos);\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 / clip box (mesh-uniforms.wgsl.ts, shared with the\n // selection mask so an outline is cut where its surface is).\n if (sectionClipped(fragmentPos)) { discard; }\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 bit 0 (value 1) = isSelected \u2192 selection highlight, forced opaque.\n // bit 1 (value 2) = isOverlay \u2192 legacy overlay pipeline callers;\n // preserve alpha and skip specular for their blended draw.\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 // Public getOverlayPipeline() callers can still use the emphasized\n // overlay bit even though internal colour overrides now use a table.\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 // Selected objects and emphasized overlays draw fully opaque.\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 or legacy overlay (it would wash out\n // their colours); a table override is composited without it below. The\n // sun lobe is scaled exactly like the diffuse sun term, and both\n // terms like irradiance, so a preset's highlights and diffuse 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 // #5984: an instanced occurrence's own authored finish rides its\n // flags lane (instanced-render.ts packInstanceFinish); the pass's\n // uniform row is the shared default.\n var metallicRoughness = vec2<f32>(uniforms.metallicRoughness.x, uniforms.metallicRoughness.y);\n if (instancedPass && (input.instSelected & 4u) != 0u) {\n metallicRoughness.x = f32((input.instSelected >> 16u) & 255u) / 255.0;\n }\n if (instancedPass && (input.instSelected & 8u) != 0u) {\n metallicRoughness.y = f32((input.instSelected >> 24u) & 255u) / 255.0;\n }\n let metallic = clamp(metallicRoughness.x, 0.0, 1.0);\n let spec = surfaceSpecular(\n N,\n normalize(-input.eyePos),\n baseColor,\n metallic,\n 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 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 // Colour override from the per-entity table (#6076): only draws the\n // renderer marks (overrideParams.y), i.e. depth-writing opaque ones.\n let entityOverride = entityOverrideColor(input.entityId);\n if (entityOverride.a >= 0.0 && !isSelected) {\n out.color = paintEntityOverride(out.color, entityOverride, irradiance, N);\n }\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":"
|
|
1
|
+
{"version":3,"file":"main.wgsl.d.ts","sourceRoot":"","sources":["../../src/shaders/main.wgsl.ts"],"names":[],"mappings":"AA4CA,eAAO,MAAM,gBAAgB,qq2CAsetB,CAAC"}
|
|
@@ -14,6 +14,7 @@ import { meshUniformsWgsl } from './mesh-uniforms.wgsl.js';
|
|
|
14
14
|
import { relativeToEyeWgsl } from './relative-to-eye.wgsl.js';
|
|
15
15
|
import { specularWgsl } from './specular.wgsl.js';
|
|
16
16
|
import { entityColorTableWgsl } from './entity-color-table.wgsl.js';
|
|
17
|
+
import { depthNudgeWgsl } from './depth-nudge.wgsl.js';
|
|
17
18
|
/**
|
|
18
19
|
* Translucent surfaces draw at this fraction of their alpha, so interiors
|
|
19
20
|
* read through windows and X-Ray ghosts stay faint. A viewer choice that
|
|
@@ -39,6 +40,7 @@ const TRANSLUCENT_OPACITY_SCALE = 0.7;
|
|
|
39
40
|
const IRRADIANCE_CALIBRATION = 1.82;
|
|
40
41
|
export const mainShaderSource = `
|
|
41
42
|
${meshUniformsWgsl}
|
|
43
|
+
${depthNudgeWgsl}
|
|
42
44
|
${relativeToEyeWgsl}
|
|
43
45
|
${mainRteWgsl}
|
|
44
46
|
// Shared group(1) lighting; packing matches packEnvironmentUniforms().
|
|
@@ -158,7 +160,7 @@ export const mainShaderSource = `
|
|
|
158
160
|
// Anti z-fighting depth nudge — see vs_main's comment.
|
|
159
161
|
let colorSalt = (entityId >> 24u) * 2654435761u;
|
|
160
162
|
let zHash = (((entityId & 0x00FFFFFFu) ^ colorSalt) * 2654435761u) & 255u;
|
|
161
|
-
output.position.z
|
|
163
|
+
output.position.z = nudgedClipZ(output.position, zHash, uniforms.viewProj);
|
|
162
164
|
output.worldPos = worldPos.xyz;
|
|
163
165
|
output.normal = normalize((uniforms.model * vec4<f32>(localNormal, 0.0)).xyz);
|
|
164
166
|
output.entityId = entityId;
|
|
@@ -195,10 +197,10 @@ export const mainShaderSource = `
|
|
|
195
197
|
// OWN colour, NOT the per-draw baseColor uniform — so every redraw of
|
|
196
198
|
// the same geometry with a different draw colour (the selection
|
|
197
199
|
// highlight's greater-equal pass) computes the SAME nudge as its
|
|
198
|
-
// batch.
|
|
200
|
+
// batch. The step size per projection is in depth-nudge.wgsl.ts.
|
|
199
201
|
let colorSalt = (input.entityId >> 24u) * 2654435761u;
|
|
200
202
|
let zHash = (((input.entityId & 0x00FFFFFFu) ^ colorSalt) * 2654435761u) & 255u;
|
|
201
|
-
output.position.z
|
|
203
|
+
output.position.z = nudgedClipZ(output.position, zHash, uniforms.viewProj);
|
|
202
204
|
output.worldPos = worldPos.xyz;
|
|
203
205
|
output.normal = normalize((uniforms.model * vec4<f32>(input.normal, 0.0)).xyz);
|
|
204
206
|
output.entityId = input.entityId;
|
|
@@ -226,7 +228,7 @@ export const mainShaderSource = `
|
|
|
226
228
|
// instanced path never redraws an occurrence with a second draw
|
|
227
229
|
// colour, so the raw picking id is enough to separate coplanar entities.
|
|
228
230
|
let zHash = ((inst.instEntityId & 0x00FFFFFFu) * 2654435761u) & 255u;
|
|
229
|
-
output.position.z
|
|
231
|
+
output.position.z = nudgedClipZ(output.position, zHash, uniforms.viewProj);
|
|
230
232
|
output.worldPos = worldPos.xyz;
|
|
231
233
|
output.normal = normalize((instMat * vec4<f32>(input.normal, 0.0)).xyz);
|
|
232
234
|
output.entityId = inst.instEntityId;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.wgsl.js","sourceRoot":"","sources":["../../src/shaders/main.wgsl.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAE/D;;;;;;GAMG;AACH,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"main.wgsl.js","sourceRoot":"","sources":["../../src/shaders/main.wgsl.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAE/D;;;;;;GAMG;AACH,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD;;;;GAIG;AACH,MAAM,yBAAyB,GAAG,GAAG,CAAC;AAEtC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,sBAAsB,GAAG,IAAI,CAAC;AAEpC,MAAM,CAAC,MAAM,gBAAgB,GAAG;UACtB,gBAAgB;UAChB,cAAc;UACd,iBAAiB;UACjB,WAAW;;;;;;;;;;;;;;;;;;;;;;8CAsByB,sBAAsB;iDACnB,yBAAyB;UAChE,iBAAiB;UACjB,YAAY;UACZ,oBAAoB;;UAEpB,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAscjB,CAAC"}
|
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WGSL shaders for the section 2D overlay (cut-cap fills + overlay lines).
|
|
3
|
-
*
|
|
4
|
-
* Two pipelines share this file because they deliberately share ONE 160-byte
|
|
5
|
-
* uniform LAYOUT: the fill shader reads `viewProj … params2` (the first 144 B)
|
|
6
|
-
* and the line shader reads `viewProj`, `planeOffset` and the appended
|
|
7
|
-
* `lineColor` at byte offset 144. They share the layout, not the storage — each
|
|
8
|
-
* draw site gets its own record in one buffer, addressed by a dynamic
|
|
9
|
-
* bind-group offset; see {@link SECTION_2D_UNIFORM_SLOT_INDEX}.
|
|
10
|
-
* The two `struct Uniforms` declarations below
|
|
11
|
-
* must therefore stay field-for-field compatible, and every TypeScript writer
|
|
12
|
-
* must address the buffer through `SECTION_2D_UNIFORM_SLOTS` rather than a
|
|
13
|
-
* hand-written index. `section-2d-uniform-layout.test.ts` parses both structs
|
|
14
|
-
* out of these sources and fails if either drifts from the slot table.
|
|
15
|
-
*/
|
|
16
1
|
/**
|
|
17
2
|
* Float32 indices of each field in the shared uniform buffer.
|
|
18
3
|
*
|
|
@@ -115,5 +100,5 @@ export declare const SECTION_2D_CAP_FILL_WGSL = "\n struct Uniforms {\n
|
|
|
115
100
|
* Line shader for the section-cut outline and the standalone world-space line
|
|
116
101
|
* overlays (annotation / alignment / grid / DXF / clash box).
|
|
117
102
|
*/
|
|
118
|
-
export declare const SECTION_2D_OVERLAY_LINE_WGSL = "\n // Mirrors the fill shader's uniform layout so both pipelines can share\n // one buffer; the line shader only reads viewProj, planeOffset and the\n // appended lineColor (byte offset 144). capFillColor/\u2026 are unused here but\n // declared for correct field offsets.\n struct Uniforms {\n viewProj: mat4x4<f32>,\n planeOffset: vec4<f32>,\n capFillColor: vec4<f32>,\n capStrokeColor: vec4<f32>,\n params: vec4<f32>,\n params2: vec4<f32>,\n lineColor: vec4<f32>,\n rteViewProj: mat4x4<f32>,\n originDeltaHigh: vec4<f32>,\n originDeltaLow: vec4<f32>,\n }\n @binding(0) @group(0) var<uniform> uniforms: Uniforms;\n\n struct VertexInput {\n @location(0) position: vec3<f32>,\n }\n\n struct VertexOutput {\n @builtin(position) position: vec4<f32>,\n }\n\n @vertex\n fn vs_main(input: VertexInput) -> VertexOutput {\n var output: VertexOutput;\n let offsetPos = input.position + uniforms.planeOffset.xyz;\n let global = uniforms.viewProj * vec4<f32>(offsetPos, 1.0);\n let relative = (offsetPos + uniforms.originDeltaHigh.xyz) + uniforms.originDeltaLow.xyz;\n let clip = select(global, uniforms.rteViewProj * vec4<f32>(relative, 1.0), uniforms.originDeltaHigh.w == 1.0);\n // Reverse-Z decal
|
|
103
|
+
export declare const SECTION_2D_OVERLAY_LINE_WGSL = "\n // Mirrors the fill shader's uniform layout so both pipelines can share\n // one buffer; the line shader only reads viewProj, planeOffset and the\n // appended lineColor (byte offset 144). capFillColor/\u2026 are unused here but\n // declared for correct field offsets.\n struct Uniforms {\n viewProj: mat4x4<f32>,\n planeOffset: vec4<f32>,\n capFillColor: vec4<f32>,\n capStrokeColor: vec4<f32>,\n params: vec4<f32>,\n params2: vec4<f32>,\n lineColor: vec4<f32>,\n rteViewProj: mat4x4<f32>,\n originDeltaHigh: vec4<f32>,\n originDeltaLow: vec4<f32>,\n }\n @binding(0) @group(0) var<uniform> uniforms: Uniforms;\n \n \n const ORTHOGRAPHIC_DEPTH_NUDGE_PER_LEVEL: f32 = 4.76837158203125e-7;\n const ORTHOGRAPHIC_MAX_DEPTH_NUDGE_METRES: f32 = 0.025;\n const MAX_DEPTH_NUDGE_HASH: f32 = 255.0;\n\n // An orthographic view-projection leaves w at 1: its bottom row is exactly\n // (0, 0, 0, 1), because the projection's and the view's bottom rows are\n // both (0, 0, 0, 1). A perspective one copies the forward axis into that\n // row, a unit vector, so it is never zero. Every caller passes the\n // camera's world view-projection, which each draw packs whether or not\n // it renders camera-relative.\n fn isOrthographicProjection(viewProj: mat4x4<f32>) -> bool {\n return viewProj[0][3] == 0.0 && viewProj[1][3] == 0.0 && viewProj[2][3] == 0.0 && viewProj[3][3] == 1.0;\n }\n\n // Orthographic only: how many hash levels fit the metre bound. The z row\n // of the view-projection has length 1 / (far - near).\n fn orthographicNudgeLevels(viewProj: mat4x4<f32>) -> u32 {\n let depthRange = 1.0 / length(vec3<f32>(viewProj[0][2], viewProj[1][2], viewProj[2][2]));\n let steps = ORTHOGRAPHIC_MAX_DEPTH_NUDGE_METRES / (depthRange * ORTHOGRAPHIC_DEPTH_NUDGE_PER_LEVEL);\n return u32(clamp(floor(steps), 0.0, MAX_DEPTH_NUDGE_HASH)) + 1u;\n }\n\n // Orthographic only: clip z moved `ndcOffset` toward the camera without\n // changing which side of a clip plane the vertex is on. A vertex beyond\n // either plane, or exactly on the far plane, stays where it is.\n fn orthographicOffsetInsideDepthRange(clip: vec4<f32>, ndcOffset: f32) -> f32 {\n if (!(clip.z > 0.0 && clip.z < clip.w)) { return clip.z; }\n return min(clip.z + ndcOffset * clip.w, clip.w);\n }\n\n const PERSPECTIVE_OVERLAY_DEPTH_LIFT: f32 = 0.00005;\n\n // Clip-space z raised above any nudged mesh face (depth-nudge.wgsl.ts).\n // A multiple of clip.w is a constant NDC offset after the w-divide, which\n // under reverse-Z reads as \"slightly closer\". Orthographic keeps at least\n // the perspective lift, and two levels above the highest nudge level\n // when that is more (all 256 levels in use), so the text pipeline's\n // constant depthBias of -4 units is covered too.\n fn overlayLiftedClipZ(clip: vec4<f32>, viewProj: mat4x4<f32>) -> f32 {\n if (isOrthographicProjection(viewProj)) {\n let levelsLift = f32(orthographicNudgeLevels(viewProj) + 1u) * ORTHOGRAPHIC_DEPTH_NUDGE_PER_LEVEL;\n return orthographicOffsetInsideDepthRange(clip, max(PERSPECTIVE_OVERLAY_DEPTH_LIFT, levelsLift));\n }\n return clip.z + PERSPECTIVE_OVERLAY_DEPTH_LIFT * clip.w;\n }\n\n\n struct VertexInput {\n @location(0) position: vec3<f32>,\n }\n\n struct VertexOutput {\n @builtin(position) position: vec4<f32>,\n }\n\n @vertex\n fn vs_main(input: VertexInput) -> VertexOutput {\n var output: VertexOutput;\n let offsetPos = input.position + uniforms.planeOffset.xyz;\n let global = uniforms.viewProj * vec4<f32>(offsetPos, 1.0);\n let relative = (offsetPos + uniforms.originDeltaHigh.xyz) + uniforms.originDeltaLow.xyz;\n let clip = select(global, uniforms.rteViewProj * vec4<f32>(relative, 1.0), uniforms.originDeltaHigh.w == 1.0);\n // Reverse-Z decal lift for lines coplanar with model faces\n // (issue #812). WebGPU forbids depthStencil.depthBias on non-\n // triangle topologies, so we do the equivalent in clip space \u2014\n // enough to beat MSAA jitter and the mesh depth nudge on annotation\n // lines that ride exactly on a wall/floor.\n output.position = vec4<f32>(clip.x, clip.y, overlayLiftedClipZ(clip, uniforms.viewProj), clip.w);\n return output;\n }\n\n struct FragOutLine {\n @location(0) color: vec4<f32>,\n @location(1) objectId: vec4<f32>,\n }\n\n @fragment\n fn fs_main(input: VertexOutput) -> FragOutLine {\n var out: FragOutLine;\n out.color = uniforms.lineColor; // consumer-themeable (defaults black)\n out.objectId = vec4<f32>(0.0, 0.0, 0.0, 0.0);\n return out;\n }\n ";
|
|
119
104
|
//# sourceMappingURL=section-2d-overlay.wgsl.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"section-2d-overlay.wgsl.d.ts","sourceRoot":"","sources":["../../src/shaders/section-2d-overlay.wgsl.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"section-2d-overlay.wgsl.d.ts","sourceRoot":"","sources":["../../src/shaders/section-2d-overlay.wgsl.ts"],"names":[],"mappings":"AAsBA;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB;IACnC,+CAA+C;;IAE/C,+CAA+C;;IAE/C,+CAA+C;;IAE/C,gDAAgD;;IAEhD,gFAAgF;;IAEhF,oEAAoE;;IAEpE,mFAAmF;;IAEnF,oEAAoE;;IAEpE,8DAA8D;;;CAGtD,CAAC;AAEX,uEAAuE;AACvE,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAE5C,6CAA6C;AAC7C,eAAO,MAAM,wBAAwB,QAAgC,CAAC;AAEtE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH;;;;;;;;GAQG;AACH,eAAO,MAAM,8BAA8B,MAAM,CAAC;AAElD,eAAO,MAAM,6BAA6B;IACxC,6EAA6E;;;;;;;;;CASrE,CAAC;AAEX,mEAAmE;AACnE,eAAO,MAAM,6BAA6B,QAAyC,CAAC;AAEpF;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAIlE;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,wBAAwB,k7LA8H9B,CAAC;AAER;;;GAGG;AACH,eAAO,MAAM,4BAA4B,ikKAwDlC,CAAC"}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* hand-written index. `section-2d-uniform-layout.test.ts` parses both structs
|
|
17
17
|
* out of these sources and fails if either drifts from the slot table.
|
|
18
18
|
*/
|
|
19
|
+
import { overlayDepthLiftWgsl } from './depth-nudge.wgsl.js';
|
|
19
20
|
/**
|
|
20
21
|
* Float32 indices of each field in the shared uniform buffer.
|
|
21
22
|
*
|
|
@@ -266,6 +267,7 @@ export const SECTION_2D_OVERLAY_LINE_WGSL = /* wgsl */ `
|
|
|
266
267
|
originDeltaLow: vec4<f32>,
|
|
267
268
|
}
|
|
268
269
|
@binding(0) @group(0) var<uniform> uniforms: Uniforms;
|
|
270
|
+
${overlayDepthLiftWgsl}
|
|
269
271
|
|
|
270
272
|
struct VertexInput {
|
|
271
273
|
@location(0) position: vec3<f32>,
|
|
@@ -282,14 +284,12 @@ export const SECTION_2D_OVERLAY_LINE_WGSL = /* wgsl */ `
|
|
|
282
284
|
let global = uniforms.viewProj * vec4<f32>(offsetPos, 1.0);
|
|
283
285
|
let relative = (offsetPos + uniforms.originDeltaHigh.xyz) + uniforms.originDeltaLow.xyz;
|
|
284
286
|
let clip = select(global, uniforms.rteViewProj * vec4<f32>(relative, 1.0), uniforms.originDeltaHigh.w == 1.0);
|
|
285
|
-
// Reverse-Z decal
|
|
287
|
+
// Reverse-Z decal lift for lines coplanar with model faces
|
|
286
288
|
// (issue #812). WebGPU forbids depthStencil.depthBias on non-
|
|
287
|
-
// triangle topologies, so we do the equivalent in clip space
|
|
288
|
-
//
|
|
289
|
-
// constant after the w-divide, which under reverse-Z means
|
|
290
|
-
// "slightly closer" — enough to beat MSAA jitter on annotation
|
|
289
|
+
// triangle topologies, so we do the equivalent in clip space —
|
|
290
|
+
// enough to beat MSAA jitter and the mesh depth nudge on annotation
|
|
291
291
|
// lines that ride exactly on a wall/floor.
|
|
292
|
-
output.position = vec4<f32>(clip.x, clip.y, clip
|
|
292
|
+
output.position = vec4<f32>(clip.x, clip.y, overlayLiftedClipZ(clip, uniforms.viewProj), clip.w);
|
|
293
293
|
return output;
|
|
294
294
|
}
|
|
295
295
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"section-2d-overlay.wgsl.js","sourceRoot":"","sources":["../../src/shaders/section-2d-overlay.wgsl.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAE/D;;;;;;;;;;;;;;GAcG;AAEH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG;IACtC,+CAA+C;IAC/C,QAAQ,EAAE,CAAC;IACX,+CAA+C;IAC/C,WAAW,EAAE,EAAE;IACf,+CAA+C;IAC/C,YAAY,EAAE,EAAE;IAChB,gDAAgD;IAChD,cAAc,EAAE,EAAE;IAClB,gFAAgF;IAChF,MAAM,EAAE,EAAE;IACV,oEAAoE;IACpE,OAAO,EAAE,EAAE;IACX,mFAAmF;IACnF,SAAS,EAAE,EAAE;IACb,oEAAoE;IACpE,WAAW,EAAE,EAAE;IACf,8DAA8D;IAC9D,eAAe,EAAE,EAAE;IACnB,cAAc,EAAE,EAAE;CACV,CAAC;AAEX,uEAAuE;AACvE,MAAM,CAAC,MAAM,yBAAyB,GAAG,EAAE,CAAC;AAE5C,6CAA6C;AAC7C,MAAM,CAAC,MAAM,wBAAwB,GAAG,yBAAyB,GAAG,CAAC,CAAC;AAEtE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,8BAA8B,GAAG,GAAG,CAAC;AAElD,MAAM,CAAC,MAAM,6BAA6B,GAAG;IAC3C,6EAA6E;IAC7E,UAAU,EAAE,CAAC;IACb,UAAU,EAAE,CAAC;IACb,SAAS,EAAE,CAAC,GAAG,8BAA8B;IAC7C,IAAI,EAAE,CAAC,GAAG,8BAA8B,GAAG,CAAC;IAC5C,GAAG,EAAE,CAAC,GAAG,8BAA8B,GAAG,CAAC;IAC3C,OAAO,EAAE,CAAC,GAAG,8BAA8B,GAAG,CAAC;IAC/C,UAAU,EAAE,CAAC,GAAG,8BAA8B,GAAG,CAAC;IAClD,QAAQ,EAAE,CAAC,GAAG,8BAA8B,GAAG,CAAC;CACxC,CAAC;AAEX,mEAAmE;AACnE,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,GAAG,8BAA8B,GAAG,CAAC,CAAC;AAEpF;;;;;;;GAOG;AACH,MAAM,UAAU,wBAAwB,CAAC,MAAiB;IACxD,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,+BAA+B,IAAI,GAAG,CAAC;IACxE,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC;IAC3E,OAAO,IAAI,CAAC,IAAI,CAAC,wBAAwB,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;AAC3D,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8H5C,CAAC;AAER;;;GAGG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAG,UAAU,CAAC
|
|
1
|
+
{"version":3,"file":"section-2d-overlay.wgsl.js","sourceRoot":"","sources":["../../src/shaders/section-2d-overlay.wgsl.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAE/D;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAE7D;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG;IACtC,+CAA+C;IAC/C,QAAQ,EAAE,CAAC;IACX,+CAA+C;IAC/C,WAAW,EAAE,EAAE;IACf,+CAA+C;IAC/C,YAAY,EAAE,EAAE;IAChB,gDAAgD;IAChD,cAAc,EAAE,EAAE;IAClB,gFAAgF;IAChF,MAAM,EAAE,EAAE;IACV,oEAAoE;IACpE,OAAO,EAAE,EAAE;IACX,mFAAmF;IACnF,SAAS,EAAE,EAAE;IACb,oEAAoE;IACpE,WAAW,EAAE,EAAE;IACf,8DAA8D;IAC9D,eAAe,EAAE,EAAE;IACnB,cAAc,EAAE,EAAE;CACV,CAAC;AAEX,uEAAuE;AACvE,MAAM,CAAC,MAAM,yBAAyB,GAAG,EAAE,CAAC;AAE5C,6CAA6C;AAC7C,MAAM,CAAC,MAAM,wBAAwB,GAAG,yBAAyB,GAAG,CAAC,CAAC;AAEtE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,8BAA8B,GAAG,GAAG,CAAC;AAElD,MAAM,CAAC,MAAM,6BAA6B,GAAG;IAC3C,6EAA6E;IAC7E,UAAU,EAAE,CAAC;IACb,UAAU,EAAE,CAAC;IACb,SAAS,EAAE,CAAC,GAAG,8BAA8B;IAC7C,IAAI,EAAE,CAAC,GAAG,8BAA8B,GAAG,CAAC;IAC5C,GAAG,EAAE,CAAC,GAAG,8BAA8B,GAAG,CAAC;IAC3C,OAAO,EAAE,CAAC,GAAG,8BAA8B,GAAG,CAAC;IAC/C,UAAU,EAAE,CAAC,GAAG,8BAA8B,GAAG,CAAC;IAClD,QAAQ,EAAE,CAAC,GAAG,8BAA8B,GAAG,CAAC;CACxC,CAAC;AAEX,mEAAmE;AACnE,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,GAAG,8BAA8B,GAAG,CAAC,CAAC;AAEpF;;;;;;;GAOG;AACH,MAAM,UAAU,wBAAwB,CAAC,MAAiB;IACxD,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,+BAA+B,IAAI,GAAG,CAAC;IACxE,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC;IAC3E,OAAO,IAAI,CAAC,IAAI,CAAC,wBAAwB,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;AAC3D,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8H5C,CAAC;AAER;;;GAGG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;UAkB7C,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAsCvB,CAAC"}
|
|
@@ -1,16 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bind-group index of the scene depth texture in the mask pipeline layout:
|
|
3
|
-
* group 0 is the mesh uniform that `vs_main` reads, so depth is group 1.
|
|
4
|
-
* `selection-mask-pass.ts` builds its layout from this same constant.
|
|
5
|
-
*/
|
|
6
|
-
export declare const SELECTION_MASK_DEPTH_GROUP = 1;
|
|
7
1
|
/**
|
|
8
2
|
* Bind-group index of the hovered-entity uniform (`vec4<u32>`, x = id) in the
|
|
9
3
|
* INSTANCED mask pipeline layout only (#5745). The non-instanced variant
|
|
10
4
|
* selects its meshes on the CPU and declares no such binding.
|
|
11
5
|
*/
|
|
12
|
-
export declare const SELECTION_MASK_HOVER_GROUP =
|
|
13
|
-
|
|
14
|
-
export declare const MASK_DEPTH_REL_TOLERANCE = 0.002;
|
|
15
|
-
export declare function selectionMaskFragmentSource(multisampled: boolean, instanced?: boolean): string;
|
|
6
|
+
export declare const SELECTION_MASK_HOVER_GROUP = 1;
|
|
7
|
+
export declare function selectionMaskFragmentSource(instanced?: boolean): string;
|
|
16
8
|
//# sourceMappingURL=selection-mask.wgsl.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"selection-mask.wgsl.d.ts","sourceRoot":"","sources":["../../src/shaders/selection-mask.wgsl.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"selection-mask.wgsl.d.ts","sourceRoot":"","sources":["../../src/shaders/selection-mask.wgsl.ts"],"names":[],"mappings":"AAwCA;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,IAAI,CAAC;AAuB5C,wBAAgB,2BAA2B,CAAC,SAAS,UAAQ,GAAG,MAAM,CAuCrE"}
|
|
@@ -10,12 +10,10 @@
|
|
|
10
10
|
* fragment entry point with no declared inputs is interface-compatible
|
|
11
11
|
* with any vertex stage, since `@builtin(position)` is always available.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* single-sample regardless of the scene's MSAA sample count, with no
|
|
18
|
-
* resolve step of their own.
|
|
13
|
+
* Visible masks share the scene's MSAA sample count and read-only depth
|
|
14
|
+
* attachment. Hardware greater-equal testing compares the same raster sample,
|
|
15
|
+
* without treating neighbouring or genuinely occluded surfaces as coincident.
|
|
16
|
+
* The mask pass resolves coverage after its selected and hovered draws.
|
|
19
17
|
*
|
|
20
18
|
* Every output first drops fragments the section plane or clip box cuts
|
|
21
19
|
* away (`sectionClipped`, shared with `fs_main`), so an outline never traces
|
|
@@ -25,17 +23,7 @@
|
|
|
25
23
|
* Three outputs, at most one per pipeline:
|
|
26
24
|
* - `fs_mask_selected_visible` / `fs_mask_hover_visible`: pass only where
|
|
27
25
|
* this fragment is at least as close as the stored scene depth
|
|
28
|
-
* (reverse-Z:
|
|
29
|
-
* additive blend leaves the target at 0 there. The comparison is NOT
|
|
30
|
-
* exact: the stored depth is sample 0 of an MSAA attachment (not the
|
|
31
|
-
* pixel centre) and was usually written by the batch draw, whose
|
|
32
|
-
* positions may be quantized, so the same surface lands a slope-sized
|
|
33
|
-
* step away. `isVisible` therefore allows this fragment's own depth
|
|
34
|
-
* slope (`fwidth`, taken before any branch so it stays in uniform
|
|
35
|
-
* control flow) plus `MASK_DEPTH_REL_TOLERANCE` of its depth; reverse-Z
|
|
36
|
-
* depth is proportional to 1/distance, so that is a relative distance
|
|
37
|
-
* tolerance. A fixed 1e-7 epsilon failed on every pixel of a real
|
|
38
|
-
* WebGPU frame, leaving the visible mask empty.
|
|
26
|
+
* (reverse-Z: greater-equal), using the scene depth attachment.
|
|
39
27
|
* - `fs_mask_selected_all`: always passes (drawn "through occluders").
|
|
40
28
|
*
|
|
41
29
|
* Instanced variant (#5745, `instanced = true`): the vertex stage is
|
|
@@ -47,22 +35,13 @@
|
|
|
47
35
|
* (flag bit 1) are dropped, as `fs_main` drops them. The section/clip cut and
|
|
48
36
|
* the depth test are the same code as the non-instanced variant.
|
|
49
37
|
*/
|
|
50
|
-
import { depthTextureWgsl } from './depth-reconstruct.wgsl.js';
|
|
51
38
|
import { meshUniformsWgsl } from './mesh-uniforms.wgsl.js';
|
|
52
|
-
/**
|
|
53
|
-
* Bind-group index of the scene depth texture in the mask pipeline layout:
|
|
54
|
-
* group 0 is the mesh uniform that `vs_main` reads, so depth is group 1.
|
|
55
|
-
* `selection-mask-pass.ts` builds its layout from this same constant.
|
|
56
|
-
*/
|
|
57
|
-
export const SELECTION_MASK_DEPTH_GROUP = 1;
|
|
58
39
|
/**
|
|
59
40
|
* Bind-group index of the hovered-entity uniform (`vec4<u32>`, x = id) in the
|
|
60
41
|
* INSTANCED mask pipeline layout only (#5745). The non-instanced variant
|
|
61
42
|
* selects its meshes on the CPU and declares no such binding.
|
|
62
43
|
*/
|
|
63
|
-
export const SELECTION_MASK_HOVER_GROUP =
|
|
64
|
-
/** Relative depth slack for "the same surface" (reverse-Z: ~0.2 % of the distance). */
|
|
65
|
-
export const MASK_DEPTH_REL_TOLERANCE = 2e-3;
|
|
44
|
+
export const SELECTION_MASK_HOVER_GROUP = 1;
|
|
66
45
|
/**
|
|
67
46
|
* Which occurrences each output keeps. The non-instanced pass is handed only
|
|
68
47
|
* the meshes it should draw, so every fragment qualifies; the instanced pass
|
|
@@ -83,7 +62,7 @@ function occurrenceFilterWgsl(instanced) {
|
|
|
83
62
|
return (input.instSelected & 2u) == 0u && input.entityId == maskHover.x;
|
|
84
63
|
}`;
|
|
85
64
|
}
|
|
86
|
-
export function selectionMaskFragmentSource(
|
|
65
|
+
export function selectionMaskFragmentSource(instanced = false) {
|
|
87
66
|
const instanceVaryings = instanced
|
|
88
67
|
? `
|
|
89
68
|
@location(2) @interpolate(flat) entityId: u32,
|
|
@@ -91,7 +70,6 @@ export function selectionMaskFragmentSource(multisampled, instanced = false) {
|
|
|
91
70
|
: '';
|
|
92
71
|
return `
|
|
93
72
|
${meshUniformsWgsl}
|
|
94
|
-
${depthTextureWgsl(0, multisampled, SELECTION_MASK_DEPTH_GROUP)}
|
|
95
73
|
|
|
96
74
|
// The subset of the vertex stage's VertexOutput the mask reads (locations match).
|
|
97
75
|
struct MaskInput {
|
|
@@ -105,26 +83,15 @@ export function selectionMaskFragmentSource(multisampled, instanced = false) {
|
|
|
105
83
|
return sectionClipped(clipSpacePos(input.worldPos, input.eyePos));
|
|
106
84
|
}
|
|
107
85
|
|
|
108
|
-
const MASK_DEPTH_REL_TOLERANCE: f32 = ${MASK_DEPTH_REL_TOLERANCE};
|
|
109
|
-
|
|
110
|
-
// depthSlope = fwidth(fragPos.z), taken by the caller in uniform control flow.
|
|
111
|
-
fn isVisible(fragPos: vec4<f32>, depthSlope: f32) -> bool {
|
|
112
|
-
let ip = vec2<i32>(fragPos.xy);
|
|
113
|
-
let slack = depthSlope + fragPos.z * MASK_DEPTH_REL_TOLERANCE;
|
|
114
|
-
return fragPos.z >= loadDepth(ip) - slack;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
86
|
@fragment
|
|
118
87
|
fn fs_mask_selected_visible(input: MaskInput) -> @location(0) vec4<f32> {
|
|
119
|
-
|
|
120
|
-
if (!isSelectedOccurrence(input) || isCut(input) || !isVisible(input.fragPos, depthSlope)) { discard; }
|
|
88
|
+
if (!isSelectedOccurrence(input) || isCut(input)) { discard; }
|
|
121
89
|
return vec4<f32>(1.0, 0.0, 0.0, 0.0);
|
|
122
90
|
}
|
|
123
91
|
|
|
124
92
|
@fragment
|
|
125
93
|
fn fs_mask_hover_visible(input: MaskInput) -> @location(0) vec4<f32> {
|
|
126
|
-
|
|
127
|
-
if (!isHoveredOccurrence(input) || isCut(input) || !isVisible(input.fragPos, depthSlope)) { discard; }
|
|
94
|
+
if (!isHoveredOccurrence(input) || isCut(input)) { discard; }
|
|
128
95
|
return vec4<f32>(0.0, 1.0, 0.0, 0.0);
|
|
129
96
|
}
|
|
130
97
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"selection-mask.wgsl.js","sourceRoot":"","sources":["../../src/shaders/selection-mask.wgsl.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAE/D
|
|
1
|
+
{"version":3,"file":"selection-mask.wgsl.js","sourceRoot":"","sources":["../../src/shaders/selection-mask.wgsl.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAE/D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAE3D;;;;GAIG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC;AAE5C;;;;;GAKG;AACH,SAAS,oBAAoB,CAAC,SAAkB;IAC9C,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO;;0EAE+D,CAAC;IACzE,CAAC;IACD,OAAO;iBACQ,0BAA0B;;;;;UAKjC,CAAC;AACX,CAAC;AAED,MAAM,UAAU,2BAA2B,CAAC,SAAS,GAAG,KAAK;IAC3D,MAAM,gBAAgB,GAAG,SAAS;QAChC,CAAC,CAAC;;6DAEuD;QACzD,CAAC,CAAC,EAAE,CAAC;IACP,OAAO;UACC,gBAAgB;;;;;;2CAMiB,gBAAgB;;UAEjD,oBAAoB,CAAC,SAAS,CAAC;;;;;;;;;;;;;;;;;;;;;;;CAuBxC,CAAC;AACF,CAAC"}
|