From 027c57a6a969432a6339fecca73883cbd930b135 Mon Sep 17 00:00:00 2001 From: CNCKitchen Date: Mon, 6 Apr 2026 16:10:50 +0200 Subject: [PATCH] fix(preview): smooth shading for all masked surfaces - Remove flat-shaded MeshLambertMaterial overlay that was covering the custom shader output on user-masked faces (root cause of static shading) - Pass smooth interpolated normal (vSmoothNormal) to fragment shader and blend toward it on masked faces so they get smooth view-dependent lighting instead of flat per-face shading - Brighten user-mask color to warm orange (0.85, 0.40, 0.15) for better visibility of lighting variation on masked surfaces - Shader now handles all mask visualization consistently: exclude mode (orange), include-only mode (orange for unselected), and angle mask (grey) all receive identical smooth shading --- js/main.js | 10 ++++----- js/previewMaterial.js | 51 ++++++++++++++++++++++++++++--------------- 2 files changed, 38 insertions(+), 23 deletions(-) diff --git a/js/main.js b/js/main.js index ca3c58e..b2c9a0a 100644 --- a/js/main.js +++ b/js/main.js @@ -1104,12 +1104,10 @@ function refreshExclusionOverlay() { const overlayFaceSet = usePrecision ? precisionExcludedFaces : excludedFaces; _falloffDirty = true; - if (selectionMode) { - const maskGeo = buildExclusionOverlayGeo(overlayGeo, overlayFaceSet, true); - setExclusionOverlay(maskGeo, 0x8ab4d4, 0.96); - } else { - setExclusionOverlay(buildExclusionOverlayGeo(overlayGeo, overlayFaceSet), 0xff6600); - } + + // Never show the flat-coloured MeshLambertMaterial overlay — the custom + // shader handles mask visualisation with smooth, view-dependent shading. + setExclusionOverlay(null); const n = usePrecision ? precisionExcludedFaces.size : excludedFaces.size; exclCount.textContent = selectionMode ? t(n === 1 ? 'excl.faceSelected' : 'excl.facesSelected', { n: n.toLocaleString() }) diff --git a/js/previewMaterial.js b/js/previewMaterial.js index 899bdb5..7cd3fe8 100644 --- a/js/previewMaterial.js +++ b/js/previewMaterial.js @@ -209,6 +209,7 @@ const vertexShader = /* glsl */` varying vec3 vModelNormal; // model-space face normal → stable UV blending varying vec3 vViewPos; // view-space position (possibly displaced) → TBN & specular varying vec3 vNormal; // view-space normal → lighting + varying vec3 vSmoothNormal; // view-space smooth normal → smooth shading on masked faces varying float vFaceMask; // combined mask (angle + user exclusion + boundary falloff) varying float vUserMask; // raw user-exclusion mask (0 = user-excluded, 1 = included) varying float vMaskType; // boundary mask type (0 = user mask, 1 = angle mask) @@ -249,6 +250,8 @@ const vertexShader = /* glsl */` vec4 mvPos = modelViewMatrix * vec4(pos, 1.0); vViewPos = mvPos.xyz; vNormal = normalize(normalMatrix * fN); + vec3 sN = length(smoothNormal) > 1e-6 ? normalize(smoothNormal) : safeN; + vSmoothNormal = normalize(normalMatrix * sN); gl_Position = projectionMatrix * mvPos; } `; @@ -266,6 +269,7 @@ const fragmentShader = /* glsl */` varying vec3 vModelNormal; varying vec3 vViewPos; varying vec3 vNormal; + varying vec3 vSmoothNormal; varying float vFaceMask; varying float vUserMask; varying float vMaskType; @@ -340,20 +344,20 @@ const fragmentShader = /* glsl */` vec3 bumpVec = N - bumpStr * (dhx * T + dhy * B); vec3 bumpN = length(bumpVec) > 1e-6 ? normalize(bumpVec) : N; - // ── Shading ─────────────────────────────────────────────────────────── - // Use consistent teal base for all areas so lighting looks uniform. - // Mask type determines the tint colour for masked/falloff regions: - // user mask (vMaskType ≈ 0) → warm red-orange - // angle mask (vMaskType ≈ 1) → neutral grey - vec3 tealBase = vec3(0.22, 0.68, 0.68); - vec3 userMaskTint = vec3(0.55, 0.22, 0.12); - vec3 angleMaskTint = vec3(0.45, 0.48, 0.50); + // On fully masked faces the bump derivatives are zero, so bumpN falls + // back to the flat face normal → faceted/static look. Blend toward + // the smooth interpolated normal so masked areas get smooth shading. + vec3 smoothN = normalize(vSmoothNormal) * (gl_FrontFacing ? 1.0 : -1.0); + bumpN = mix(smoothN, bumpN, maskBlend); - float maskEffect = 1.0 - maskBlend; // 0 = fully textured, 1 = fully masked - // On user-excluded faces (vUserMask ≈ 0) force user tint regardless of vMaskType - float effectiveMaskType = mix(vMaskType, 0.0, step(0.5, 1.0 - vUserMask)); - vec3 maskTint = mix(userMaskTint, angleMaskTint, effectiveMaskType); - vec3 baseColor = mix(tealBase, maskTint, maskEffect * 0.6); + // ── Shading ─────────────────────────────────────────────────────────── + // Compute lighting identically for ALL surfaces using the teal base so + // that specular highlights, diffuse response, and view-dependent shading + // are perfectly consistent everywhere. Mask tinting is applied AFTER + // lighting as a colour blend so masked areas keep the same glossy look. + vec3 tealBase = vec3(0.22, 0.68, 0.68); + vec3 userMaskColor = vec3(0.85, 0.40, 0.15); + vec3 angleMaskColor = vec3(0.45, 0.48, 0.50); vec3 L1 = normalize(vec3( 0.5, 0.8, 1.0)); vec3 L2 = normalize(vec3(-0.5, -0.2, -0.6)); @@ -365,10 +369,23 @@ const fragmentShader = /* glsl */` vec3 H1 = normalize(L1 + V); float spec = pow(max(dot(bumpN, H1), 0.0), 64.0) * 0.60; - vec3 color = baseColor * 0.55 // ambient - + baseColor * diff1 * vec3(1.00, 0.96, 0.88) * 0.55 // key light - + baseColor * diff2 * vec3(0.80, 0.60, 0.50) * 0.15 // warm fill - + vec3(spec); // specular + // Lit teal (identical for textured and masked surfaces) + vec3 litTeal = tealBase * 0.55 + + tealBase * diff1 * vec3(1.00, 0.96, 0.88) * 0.55 + + tealBase * diff2 * vec3(0.80, 0.60, 0.50) * 0.15 + + vec3(spec); + + // Mask tint: pick colour by mask type, compute same lighting with that base + float maskEffect = 1.0 - maskBlend; // 0 = fully textured, 1 = fully masked + float effectiveMaskType = mix(vMaskType, 0.0, step(0.5, 1.0 - vUserMask)); + vec3 maskBase = mix(userMaskColor, angleMaskColor, effectiveMaskType); + vec3 litMask = maskBase * 0.55 + + maskBase * diff1 * vec3(1.00, 0.96, 0.88) * 0.55 + + maskBase * diff2 * vec3(0.80, 0.60, 0.50) * 0.15 + + vec3(spec); + + // Blend: 100% mask colour at the boundary, fading to 0% at falloff distance + vec3 color = mix(litTeal, litMask, maskEffect); gl_FragColor = vec4(color, 1.0); }