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
This commit is contained in:
CNCKitchen
2026-04-06 16:10:50 +02:00
parent 5eb8264f78
commit 027c57a6a9
2 changed files with 38 additions and 23 deletions
+4 -6
View File
@@ -1104,12 +1104,10 @@ function refreshExclusionOverlay() {
const overlayFaceSet = usePrecision ? precisionExcludedFaces : excludedFaces; const overlayFaceSet = usePrecision ? precisionExcludedFaces : excludedFaces;
_falloffDirty = true; _falloffDirty = true;
if (selectionMode) {
const maskGeo = buildExclusionOverlayGeo(overlayGeo, overlayFaceSet, true); // Never show the flat-coloured MeshLambertMaterial overlay — the custom
setExclusionOverlay(maskGeo, 0x8ab4d4, 0.96); // shader handles mask visualisation with smooth, view-dependent shading.
} else { setExclusionOverlay(null);
setExclusionOverlay(buildExclusionOverlayGeo(overlayGeo, overlayFaceSet), 0xff6600);
}
const n = usePrecision ? precisionExcludedFaces.size : excludedFaces.size; const n = usePrecision ? precisionExcludedFaces.size : excludedFaces.size;
exclCount.textContent = selectionMode exclCount.textContent = selectionMode
? t(n === 1 ? 'excl.faceSelected' : 'excl.facesSelected', { n: n.toLocaleString() }) ? t(n === 1 ? 'excl.faceSelected' : 'excl.facesSelected', { n: n.toLocaleString() })
+34 -17
View File
@@ -209,6 +209,7 @@ const vertexShader = /* glsl */`
varying vec3 vModelNormal; // model-space face normal → stable UV blending varying vec3 vModelNormal; // model-space face normal → stable UV blending
varying vec3 vViewPos; // view-space position (possibly displaced) → TBN & specular varying vec3 vViewPos; // view-space position (possibly displaced) → TBN & specular
varying vec3 vNormal; // view-space normal → lighting 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 vFaceMask; // combined mask (angle + user exclusion + boundary falloff)
varying float vUserMask; // raw user-exclusion mask (0 = user-excluded, 1 = included) varying float vUserMask; // raw user-exclusion mask (0 = user-excluded, 1 = included)
varying float vMaskType; // boundary mask type (0 = user mask, 1 = angle mask) 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); vec4 mvPos = modelViewMatrix * vec4(pos, 1.0);
vViewPos = mvPos.xyz; vViewPos = mvPos.xyz;
vNormal = normalize(normalMatrix * fN); vNormal = normalize(normalMatrix * fN);
vec3 sN = length(smoothNormal) > 1e-6 ? normalize(smoothNormal) : safeN;
vSmoothNormal = normalize(normalMatrix * sN);
gl_Position = projectionMatrix * mvPos; gl_Position = projectionMatrix * mvPos;
} }
`; `;
@@ -266,6 +269,7 @@ const fragmentShader = /* glsl */`
varying vec3 vModelNormal; varying vec3 vModelNormal;
varying vec3 vViewPos; varying vec3 vViewPos;
varying vec3 vNormal; varying vec3 vNormal;
varying vec3 vSmoothNormal;
varying float vFaceMask; varying float vFaceMask;
varying float vUserMask; varying float vUserMask;
varying float vMaskType; varying float vMaskType;
@@ -340,20 +344,20 @@ const fragmentShader = /* glsl */`
vec3 bumpVec = N - bumpStr * (dhx * T + dhy * B); vec3 bumpVec = N - bumpStr * (dhx * T + dhy * B);
vec3 bumpN = length(bumpVec) > 1e-6 ? normalize(bumpVec) : N; vec3 bumpN = length(bumpVec) > 1e-6 ? normalize(bumpVec) : N;
// ── Shading ─────────────────────────────────────────────────────────── // On fully masked faces the bump derivatives are zero, so bumpN falls
// Use consistent teal base for all areas so lighting looks uniform. // back to the flat face normal → faceted/static look. Blend toward
// Mask type determines the tint colour for masked/falloff regions: // the smooth interpolated normal so masked areas get smooth shading.
// user mask (vMaskType ≈ 0) → warm red-orange vec3 smoothN = normalize(vSmoothNormal) * (gl_FrontFacing ? 1.0 : -1.0);
// angle mask (vMaskType ≈ 1) → neutral grey bumpN = mix(smoothN, bumpN, maskBlend);
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);
float maskEffect = 1.0 - maskBlend; // 0 = fully textured, 1 = fully masked // ── Shading ───────────────────────────────────────────────────────────
// On user-excluded faces (vUserMask ≈ 0) force user tint regardless of vMaskType // Compute lighting identically for ALL surfaces using the teal base so
float effectiveMaskType = mix(vMaskType, 0.0, step(0.5, 1.0 - vUserMask)); // that specular highlights, diffuse response, and view-dependent shading
vec3 maskTint = mix(userMaskTint, angleMaskTint, effectiveMaskType); // are perfectly consistent everywhere. Mask tinting is applied AFTER
vec3 baseColor = mix(tealBase, maskTint, maskEffect * 0.6); // 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 L1 = normalize(vec3( 0.5, 0.8, 1.0));
vec3 L2 = normalize(vec3(-0.5, -0.2, -0.6)); vec3 L2 = normalize(vec3(-0.5, -0.2, -0.6));
@@ -365,10 +369,23 @@ const fragmentShader = /* glsl */`
vec3 H1 = normalize(L1 + V); vec3 H1 = normalize(L1 + V);
float spec = pow(max(dot(bumpN, H1), 0.0), 64.0) * 0.60; float spec = pow(max(dot(bumpN, H1), 0.0), 64.0) * 0.60;
vec3 color = baseColor * 0.55 // ambient // Lit teal (identical for textured and masked surfaces)
+ baseColor * diff1 * vec3(1.00, 0.96, 0.88) * 0.55 // key light vec3 litTeal = tealBase * 0.55
+ baseColor * diff2 * vec3(0.80, 0.60, 0.50) * 0.15 // warm fill + tealBase * diff1 * vec3(1.00, 0.96, 0.88) * 0.55
+ vec3(spec); // specular + 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); gl_FragColor = vec4(color, 1.0);
} }