/** * WGSL for the point cloud splat pipeline. * * Draws each point as an instanced quad (6 verts per point, triangle-list * topology) so we can give it a real on-screen size — `point-list` has * no `gl_PointSize` equivalent in WebGPU, which is why a 1-px point cloud * looks like a halftone screen as you zoom in. * * Three size modes (uniforms.sizing.x): * 0 = fixed-px — every point renders at `pointSizePx` pixels * 1 = adaptive-world — splat covers `worldRadius` metres, projected * 2 = attenuated — adaptive but clamped to [1, pointSizePx] px * * Color modes (uniforms.colorModeAndExtras.x): * 0 = per-vertex RGB, 1 = classification, 2 = intensity ramp, * 3 = height ramp, 4 = fixed override. * * Round shape: fragment discards corners outside the unit disc, so * splats render as circles (not squares) at any size > ~3 px. */ export declare const pointShaderSource = "\n struct PointUniforms {\n viewProj: mat4x4,\n model: mat4x4,\n colorOverride: vec4,\n // x = colorMode, y = pointSizePx, z = heightMin, w = heightMax\n colorModeAndExtras: vec4,\n // x = sizeMode, y = worldRadius (m), z = viewportWidth, w = viewportHeight\n sizing: vec4,\n sectionPlane: vec4,\n // x = assetExpressId (federation-aware globalId), y = sectionEnabled,\n // z = roundShape, w = reserved (was the 32-bit class mask before\n // classMask below took over the full 0..255 LAS range, #1783)\n flags: vec4,\n // x = previewStride (1 = render every point, N = render every\n // Nth instance \u2014 used by the section-plane drag preview path).\n // yzw reserved for future per-frame state.\n extras: vec4,\n // x = deviation centerOffset (m), y = deviation halfRange (m).\n // Used by colorMode 5 (BIM\u2194scan deviation heatmap).\n deviationRange: vec4,\n // 256-bit LAS class-visibility bitmask packed as 8 u32 words\n // (two vec4s). Bit (i % 32) of word (i / 32) set \u2192 class i shown.\n classMask: array, 2>,\n }\n @binding(0) @group(0) var uniforms: PointUniforms;\n\n struct VertexInput {\n @location(0) position: vec3,\n @location(1) rgbAndClass: vec4, // unorm8x4 \u2192 0..1 each\n @location(2) intensityPacked: u32, // low 16 bits = intensity\n @location(3) entityId: u32,\n // BIM\u2194scan signed distance, populated by the deviation compute\n // pass. Zero when the user hasn't computed yet (or when no\n // mesh is loaded). Bound from a separate vertex buffer so the\n // existing 24-byte-per-point layout stays unchanged.\n @location(4) deviation: f32,\n }\n\n struct VertexOutput {\n @builtin(position) position: vec4,\n @location(0) color: vec4,\n @location(1) worldPos: vec3,\n @location(2) @interpolate(flat) entityId: u32,\n @location(3) quadUv: vec2,\n }\n\n fn classification_color(class_id: u32) -> vec3 {\n switch (class_id) {\n case 0u, 1u: { return vec3(0.65, 0.65, 0.65); }\n case 2u: { return vec3(0.55, 0.40, 0.25); }\n case 3u: { return vec3(0.55, 0.85, 0.45); }\n case 4u: { return vec3(0.30, 0.75, 0.30); }\n case 5u: { return vec3(0.10, 0.45, 0.15); }\n case 6u: { return vec3(0.95, 0.55, 0.20); }\n case 7u: { return vec3(0.95, 0.20, 0.20); }\n case 8u: { return vec3(0.20, 0.85, 0.95); }\n case 9u: { return vec3(0.20, 0.40, 0.95); }\n case 10u: { return vec3(0.55, 0.20, 0.85); }\n case 11u: { return vec3(0.30, 0.30, 0.30); }\n case 13u: { return vec3(0.95, 0.85, 0.20); }\n case 14u: { return vec3(0.95, 0.95, 0.50); }\n case 15u: { return vec3(0.20, 0.20, 0.55); }\n case 16u: { return vec3(0.30, 0.65, 0.65); }\n case 17u: { return vec3(0.85, 0.70, 0.50); }\n case 18u: { return vec3(0.95, 0.20, 0.20); }\n default: { return vec3(0.65, 0.65, 0.65); }\n }\n }\n\n // Diverging blue \u2192 white \u2192 red ramp for the BIM\u2194scan deviation\n // heatmap. t is in [-1, 1] where \u22121 = scan-far on the negative\n // side of the surface, 0 = exactly on surface, +1 = scan-far on\n // the positive (outward-normal) side. Negative side (typically\n // \"inside\" / \"before\" the wall) is blue; positive (\"outside\" /\n // \"past\" the wall) is red.\n fn deviation_ramp(t: f32) -> vec3 {\n let s = clamp(t, -1.0, 1.0);\n if (s < 0.0) {\n // Cool side: deep blue \u2192 white as |t| \u2192 0.\n let k = s + 1.0; // [-1..0] \u2192 [0..1]\n return mix(vec3(0.10, 0.30, 0.85), vec3(0.95, 0.95, 0.95), k);\n }\n // Warm side: white \u2192 red as t \u2192 1.\n return mix(vec3(0.95, 0.95, 0.95), vec3(0.85, 0.20, 0.10), s);\n }\n\n fn height_ramp(t: f32) -> vec3 {\n let s = clamp(t, 0.0, 1.0);\n if (s < 0.25) {\n let k = s / 0.25;\n return mix(vec3(0.10, 0.20, 0.85), vec3(0.10, 0.85, 0.85), k);\n } else if (s < 0.5) {\n let k = (s - 0.25) / 0.25;\n return mix(vec3(0.10, 0.85, 0.85), vec3(0.20, 0.85, 0.20), k);\n } else if (s < 0.75) {\n let k = (s - 0.5) / 0.25;\n return mix(vec3(0.20, 0.85, 0.20), vec3(0.95, 0.95, 0.20), k);\n } else {\n let k = (s - 0.75) / 0.25;\n return mix(vec3(0.95, 0.95, 0.20), vec3(0.95, 0.20, 0.10), k);\n }\n }\n\n @vertex\n fn vs_main(\n input: VertexInput,\n @builtin(vertex_index) vId: u32,\n @builtin(instance_index) iId: u32,\n ) -> VertexOutput {\n // Preview-density stride cull. UI sets extras.x to e.g. 4\n // while the user drags a section-plane slider so we render\n // every 4th point and the drag stays responsive on huge scans.\n // stride <= 1 is the no-op default.\n let stride = max(1u, uniforms.extras.x);\n if (stride > 1u && (iId % stride) != 0u) {\n var skipped: VertexOutput;\n // Push behind the near plane so the rasteriser drops it.\n skipped.position = vec4(0.0, 0.0, -2.0, 1.0);\n skipped.color = vec4(0.0);\n skipped.worldPos = vec3(0.0);\n skipped.entityId = 0u;\n skipped.quadUv = vec2(0.0);\n return skipped;\n }\n\n // Quad corners (two triangles, CCW) in unit disc coords:\n // tri 1: (-1,-1)(1,-1)(1,1)\n // tri 2: (-1,-1)(1, 1)(-1,1)\n var corners = array, 6>(\n vec2(-1.0, -1.0),\n vec2( 1.0, -1.0),\n vec2( 1.0, 1.0),\n vec2(-1.0, -1.0),\n vec2( 1.0, 1.0),\n vec2(-1.0, 1.0),\n );\n let corner = corners[vId];\n\n let worldPos4 = uniforms.model * vec4(input.position, 1.0);\n var clipPos = uniforms.viewProj * worldPos4;\n\n // Compute splat half-extent in pixels for the active size mode.\n let sizeMode = u32(uniforms.sizing.x);\n let worldRadius = uniforms.sizing.y;\n let viewport = uniforms.sizing.zw;\n let pointSizePx = uniforms.colorModeAndExtras.y;\n\n // halfPx is the splat RADIUS in pixels. The user-facing\n // pointSizePx is the diameter (\"8 px point\"), so divide by 2\n // when feeding it to the pipeline. Without this the fixed and\n // attenuated branches render splats at ~2x their requested size.\n var halfPx: f32;\n if (sizeMode == 0u) {\n halfPx = max(0.5, pointSizePx * 0.5);\n } else {\n // Project a world-radius offset to clip space, take pixel delta.\n // worldRadius is already a radius \u2014 no /2 needed here.\n let edgePos = uniforms.viewProj * (worldPos4 + vec4(worldRadius, 0.0, 0.0, 0.0));\n let centerNdcX = clipPos.x / max(abs(clipPos.w), 1e-6);\n let edgeNdcX = edgePos.x / max(abs(edgePos.w), 1e-6);\n let projectedPx = abs(edgeNdcX - centerNdcX) * 0.5 * viewport.x;\n if (sizeMode == 2u) {\n halfPx = clamp(projectedPx, 0.5, max(0.5, pointSizePx * 0.5));\n } else {\n halfPx = max(0.5, projectedPx);\n }\n }\n\n // Convert pixel offset to clip-space offset. Multiply by clipPos.w\n // because the GPU divides by w during the perspective divide.\n let halfClip = vec2(halfPx) / max(viewport, vec2(1.0)) * 2.0 * abs(clipPos.w);\n clipPos.x = clipPos.x + corner.x * halfClip.x;\n clipPos.y = clipPos.y + corner.y * halfClip.y;\n\n // Color selection\n let mode = u32(uniforms.colorModeAndExtras.x);\n let intensity01 = f32(input.intensityPacked & 0xffffu) / 65535.0;\n let classId = u32(round(input.rgbAndClass.a * 255.0));\n\n // Per-class visibility \u2014 classMask is a 256-bit mask covering\n // every LAS classification code, including user-defined 64..255\n // (#1783). Hidden classes get pushed behind the near plane via a\n // degenerate clipPos so they're culled before rasterisation;\n // cheaper than fragment-stage discard.\n let maskWordIdx = classId >> 5u; // classId is 0..255 \u2192 word 0..7\n let maskWord = uniforms.classMask[maskWordIdx >> 2u][maskWordIdx & 3u];\n if (((maskWord >> (classId & 31u)) & 1u) == 0u) {\n var output: VertexOutput;\n output.position = vec4(0.0, 0.0, -2.0, 1.0); // outside [0,1] reverse-Z \u2192 culled\n output.color = vec4(0.0);\n output.worldPos = vec3(0.0);\n output.entityId = 0u;\n output.quadUv = vec2(0.0);\n return output;\n }\n let heightT =\n (worldPos4.y - uniforms.colorModeAndExtras.z) /\n max(1e-6, uniforms.colorModeAndExtras.w - uniforms.colorModeAndExtras.z);\n\n var rgb: vec3;\n switch (mode) {\n case 0u: { rgb = input.rgbAndClass.rgb; }\n case 1u: { rgb = classification_color(classId); }\n case 2u: { rgb = vec3(intensity01, intensity01, intensity01); }\n case 3u: { rgb = height_ramp(heightT); }\n case 4u: { rgb = uniforms.colorOverride.rgb; }\n case 5u: {\n // Deviation: shift by centerOffset so a non-zero baseline\n // can be re-zeroed (handy when a scan has a global offset\n // from the model). halfRange = 0 falls through to white.\n let center = uniforms.deviationRange.x;\n let half = max(uniforms.deviationRange.y, 1e-6);\n let dt = (input.deviation - center) / half;\n rgb = deviation_ramp(dt);\n }\n default: { rgb = input.rgbAndClass.rgb; }\n }\n\n var output: VertexOutput;\n output.position = clipPos;\n output.color = vec4(rgb, 1.0);\n output.worldPos = worldPos4.xyz;\n output.entityId = input.entityId;\n output.quadUv = corner;\n return output;\n }\n\n struct FragmentOutput {\n @location(0) color: vec4,\n @location(1) objectId: vec4,\n }\n\n @fragment\n fn fs_main(input: VertexOutput) -> FragmentOutput {\n // Round shape \u2014 discard corners outside the unit disc.\n if (uniforms.flags.z == 1u) {\n if (dot(input.quadUv, input.quadUv) > 1.0) {\n discard;\n }\n }\n\n // Section-plane clipping\n if (uniforms.flags.y == 1u) {\n let d = dot(uniforms.sectionPlane.xyz, input.worldPos) - uniforms.sectionPlane.w;\n if (d > 0.0) {\n discard;\n }\n }\n\n var output: FragmentOutput;\n output.color = input.color;\n // Prefer the asset-level expressId from the uniform when it's set\n // (federation needs to relabel post-stream, so we can't rely on\n // the per-vertex attribute that was baked at upload time).\n // flags.x == 0 \u2192 fall back to per-vertex value to preserve the\n // legacy contract during the upload-only rendering window.\n let id = select(input.entityId, uniforms.flags.x, uniforms.flags.x != 0u);\n output.objectId = vec4(\n f32((id >> 0u) & 0xffu) / 255.0,\n f32((id >> 8u) & 0xffu) / 255.0,\n f32((id >> 16u) & 0xffu) / 255.0,\n f32((id >> 24u) & 0xffu) / 255.0,\n );\n return output;\n }\n"; //# sourceMappingURL=point-shader.wgsl.d.ts.map