mirror of
https://github.com/CNCKitchen/stlTexturizer.git
synced 2026-04-07 22:11:32 +00:00
feat(preview): mask-type-dependent falloff tinting and uniform shading
- Track whether each boundary is from user masking or angle masking via a new boundaryMaskTypeAttr vertex attribute (0=user, 1=angle) - Pass vUserMask and vMaskType varyings to the fragment shader - Use consistent teal base color for all surfaces so lighting is uniform across masked and textured areas (fixes dark halo artifact) - Tint the falloff gradient warm red-orange near user-painted masks and neutral grey near angle-masked boundaries
This commit is contained in:
+39
-8
@@ -1536,6 +1536,9 @@ function computeBoundaryFalloffAttr(geometry, userMaskArr) {
|
|||||||
|
|
||||||
if (falloff <= 0) {
|
if (falloff <= 0) {
|
||||||
geometry.setAttribute('boundaryFalloffAttr', new THREE.Float32BufferAttribute(falloffArr, 1));
|
geometry.setAttribute('boundaryFalloffAttr', new THREE.Float32BufferAttribute(falloffArr, 1));
|
||||||
|
const defaultType = new Float32Array(posCount);
|
||||||
|
defaultType.fill(1.0);
|
||||||
|
geometry.setAttribute('boundaryMaskTypeAttr', new THREE.Float32BufferAttribute(defaultType, 1));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1543,9 +1546,10 @@ function computeBoundaryFalloffAttr(geometry, userMaskArr) {
|
|||||||
// Mirrors the vertex shader logic so the preview boundary matches export.
|
// Mirrors the vertex shader logic so the preview boundary matches export.
|
||||||
const faceNrmAttr = geometry.attributes.faceNormal;
|
const faceNrmAttr = geometry.attributes.faceNormal;
|
||||||
const faceMask = new Float32Array(triCount); // 0 = masked, 1 = textured
|
const faceMask = new Float32Array(triCount); // 0 = masked, 1 = textured
|
||||||
|
const isUserMasked = new Uint8Array(triCount); // 1 if user-excluded
|
||||||
for (let t = 0; t < triCount; t++) {
|
for (let t = 0; t < triCount; t++) {
|
||||||
const userVal = userMaskArr[t * 3]; // same for all 3 verts of this face
|
const userVal = userMaskArr[t * 3]; // same for all 3 verts of this face
|
||||||
if (userVal < 0.5) { faceMask[t] = 0; continue; }
|
if (userVal < 0.5) { faceMask[t] = 0; isUserMasked[t] = 1; continue; }
|
||||||
|
|
||||||
let angleMask = 1.0;
|
let angleMask = 1.0;
|
||||||
if (faceNrmAttr) {
|
if (faceNrmAttr) {
|
||||||
@@ -1571,6 +1575,7 @@ function computeBoundaryFalloffAttr(geometry, userMaskArr) {
|
|||||||
const posFromKey = new Map(); // posKey → [x, y, z]
|
const posFromKey = new Map(); // posKey → [x, y, z]
|
||||||
// Per-position: [maskedArea, totalArea] to find boundary vertices
|
// Per-position: [maskedArea, totalArea] to find boundary vertices
|
||||||
const maskFracMap = new Map();
|
const maskFracMap = new Map();
|
||||||
|
const userMaskAreaMap = new Map(); // posKey → area of user-masked faces
|
||||||
const tmpV = new THREE.Vector3();
|
const tmpV = new THREE.Vector3();
|
||||||
const vA = new THREE.Vector3(), vB = new THREE.Vector3(), vC = new THREE.Vector3();
|
const vA = new THREE.Vector3(), vB = new THREE.Vector3(), vC = new THREE.Vector3();
|
||||||
const e1 = new THREE.Vector3(), e2 = new THREE.Vector3(), fn = new THREE.Vector3();
|
const e1 = new THREE.Vector3(), e2 = new THREE.Vector3(), fn = new THREE.Vector3();
|
||||||
@@ -1596,19 +1601,31 @@ function computeBoundaryFalloffAttr(geometry, userMaskArr) {
|
|||||||
} else {
|
} else {
|
||||||
maskFracMap.set(k, [masked ? area : 0, area]);
|
maskFracMap.set(k, [masked ? area : 0, area]);
|
||||||
}
|
}
|
||||||
|
// Track user-mask area per position to classify boundary type
|
||||||
|
if (isUserMasked[t]) {
|
||||||
|
const prev = userMaskAreaMap.get(k) || 0;
|
||||||
|
userMaskAreaMap.set(k, prev + area);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Boundary positions: shared between masked and non-masked faces
|
// Boundary positions: shared between masked and non-masked faces.
|
||||||
|
// Each entry: [x, y, z, maskType] where maskType 0 = user, 1 = angle.
|
||||||
const boundaryPositions = [];
|
const boundaryPositions = [];
|
||||||
for (const [k, pos] of posFromKey) {
|
for (const [k, pos] of posFromKey) {
|
||||||
const mf = maskFracMap.get(k);
|
const mf = maskFracMap.get(k);
|
||||||
const frac = mf[1] > 0 ? mf[0] / mf[1] : 0;
|
const frac = mf[1] > 0 ? mf[0] / mf[1] : 0;
|
||||||
if (frac > 0 && frac < 1) boundaryPositions.push(pos);
|
if (frac > 0 && frac < 1) {
|
||||||
|
const userArea = userMaskAreaMap.get(k) || 0;
|
||||||
|
boundaryPositions.push([pos[0], pos[1], pos[2], userArea > 0 ? 0 : 1]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (boundaryPositions.length === 0) {
|
if (boundaryPositions.length === 0) {
|
||||||
geometry.setAttribute('boundaryFalloffAttr', new THREE.Float32BufferAttribute(falloffArr, 1));
|
geometry.setAttribute('boundaryFalloffAttr', new THREE.Float32BufferAttribute(falloffArr, 1));
|
||||||
|
const defaultType = new Float32Array(posCount);
|
||||||
|
defaultType.fill(1.0);
|
||||||
|
geometry.setAttribute('boundaryMaskTypeAttr', new THREE.Float32BufferAttribute(defaultType, 1));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1644,15 +1661,21 @@ function computeBoundaryFalloffAttr(geometry, userMaskArr) {
|
|||||||
const searchY = Math.ceil(falloff / gDy);
|
const searchY = Math.ceil(falloff / gDy);
|
||||||
const searchZ = Math.ceil(falloff / gDz);
|
const searchZ = Math.ceil(falloff / gDz);
|
||||||
|
|
||||||
// Compute per-unique-position falloff factor
|
// Compute per-unique-position falloff factor and mask type
|
||||||
const falloffCache = new Map(); // posKey → factor [0,1]
|
const falloffCache = new Map(); // posKey → factor [0,1]
|
||||||
|
const maskTypeCache = new Map(); // posKey → 0 (user mask) or 1 (angle mask)
|
||||||
for (const [k, pos] of posFromKey) {
|
for (const [k, pos] of posFromKey) {
|
||||||
const mf = maskFracMap.get(k);
|
const mf = maskFracMap.get(k);
|
||||||
const frac = mf[1] > 0 ? mf[0] / mf[1] : 0;
|
const frac = mf[1] > 0 ? mf[0] / mf[1] : 0;
|
||||||
if (frac >= 1) continue; // fully masked vertex — keep 1.0 (mask zeroes it anyway)
|
if (frac >= 1) continue; // fully masked vertex — keep 1.0 (mask zeroes it anyway)
|
||||||
// Boundary vertices (shared between masked and unmasked faces) are AT
|
// Boundary vertices (shared between masked and unmasked faces) are AT
|
||||||
// the boundary → distance 0 → falloff factor 0.
|
// the boundary → distance 0 → falloff factor 0.
|
||||||
if (frac > 0) { falloffCache.set(k, 0); continue; }
|
if (frac > 0) {
|
||||||
|
falloffCache.set(k, 0);
|
||||||
|
const userArea = userMaskAreaMap.get(k) || 0;
|
||||||
|
maskTypeCache.set(k, userArea > 0 ? 0 : 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const px = pos[0], py = pos[1], pz = pos[2];
|
const px = pos[0], py = pos[1], pz = pos[2];
|
||||||
const cix = Math.max(0, Math.min(gRes - 1, Math.floor((px - gMinX) / gDx)));
|
const cix = Math.max(0, Math.min(gRes - 1, Math.floor((px - gMinX) / gDx)));
|
||||||
@@ -1660,6 +1683,7 @@ function computeBoundaryFalloffAttr(geometry, userMaskArr) {
|
|||||||
const ciz = Math.max(0, Math.min(gRes - 1, Math.floor((pz - gMinZ) / gDz)));
|
const ciz = Math.max(0, Math.min(gRes - 1, Math.floor((pz - gMinZ) / gDz)));
|
||||||
|
|
||||||
let minDist2 = falloff * falloff;
|
let minDist2 = falloff * falloff;
|
||||||
|
let nearestType = 1; // default: angle mask
|
||||||
for (let dix = -searchX; dix <= searchX; dix++) {
|
for (let dix = -searchX; dix <= searchX; dix++) {
|
||||||
const nix = cix + dix;
|
const nix = cix + dix;
|
||||||
if (nix < 0 || nix >= gRes) continue;
|
if (nix < 0 || nix >= gRes) continue;
|
||||||
@@ -1674,24 +1698,31 @@ function computeBoundaryFalloffAttr(geometry, userMaskArr) {
|
|||||||
for (const bp of cell) {
|
for (const bp of cell) {
|
||||||
const dx = px - bp[0], dy = py - bp[1], dz = pz - bp[2];
|
const dx = px - bp[0], dy = py - bp[1], dz = pz - bp[2];
|
||||||
const d2 = dx * dx + dy * dy + dz * dz;
|
const d2 = dx * dx + dy * dy + dz * dz;
|
||||||
if (d2 < minDist2) minDist2 = d2;
|
if (d2 < minDist2) { minDist2 = d2; nearestType = bp[3]; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const dist = Math.sqrt(minDist2);
|
const dist = Math.sqrt(minDist2);
|
||||||
const factor = Math.min(1, dist / falloff);
|
const factor = Math.min(1, dist / falloff);
|
||||||
if (factor < 1) falloffCache.set(k, factor);
|
if (factor < 1) {
|
||||||
|
falloffCache.set(k, factor);
|
||||||
|
maskTypeCache.set(k, nearestType);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write per-vertex attribute
|
// Write per-vertex attributes
|
||||||
|
const maskTypeArr = new Float32Array(posCount);
|
||||||
|
maskTypeArr.fill(1.0); // default: angle mask (grey)
|
||||||
for (let i = 0; i < posCount; i++) {
|
for (let i = 0; i < posCount; i++) {
|
||||||
tmpV.fromBufferAttribute(posAttr, i);
|
tmpV.fromBufferAttribute(posAttr, i);
|
||||||
const k = posKey(tmpV.x, tmpV.y, tmpV.z);
|
const k = posKey(tmpV.x, tmpV.y, tmpV.z);
|
||||||
if (falloffCache.has(k)) falloffArr[i] = falloffCache.get(k);
|
if (falloffCache.has(k)) falloffArr[i] = falloffCache.get(k);
|
||||||
|
if (maskTypeCache.has(k)) maskTypeArr[i] = maskTypeCache.get(k);
|
||||||
}
|
}
|
||||||
|
|
||||||
geometry.setAttribute('boundaryFalloffAttr', new THREE.Float32BufferAttribute(falloffArr, 1));
|
geometry.setAttribute('boundaryFalloffAttr', new THREE.Float32BufferAttribute(falloffArr, 1));
|
||||||
|
geometry.setAttribute('boundaryMaskTypeAttr', new THREE.Float32BufferAttribute(maskTypeArr, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+20
-1
@@ -203,12 +203,15 @@ const vertexShader = /* glsl */`
|
|||||||
attribute vec3 faceNormal;
|
attribute vec3 faceNormal;
|
||||||
attribute float faceMask;
|
attribute float faceMask;
|
||||||
attribute float boundaryFalloffAttr;
|
attribute float boundaryFalloffAttr;
|
||||||
|
attribute float boundaryMaskTypeAttr;
|
||||||
|
|
||||||
varying vec3 vModelPos; // ORIGINAL model-space position → UV computation in fragment
|
varying vec3 vModelPos; // ORIGINAL model-space position → UV computation in fragment
|
||||||
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 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 vMaskType; // boundary mask type (0 = user mask, 1 = angle mask)
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
vec3 safeN = length(normal) > 1e-6 ? normalize(normal) : vec3(0.0, 0.0, 1.0);
|
vec3 safeN = length(normal) > 1e-6 ? normalize(normal) : vec3(0.0, 0.0, 1.0);
|
||||||
@@ -226,6 +229,8 @@ const vertexShader = /* glsl */`
|
|||||||
angleMask = min(angleMask, surfaceAngle > topAngleLimit ? 1.0 : 0.0);
|
angleMask = min(angleMask, surfaceAngle > topAngleLimit ? 1.0 : 0.0);
|
||||||
float totalMask = angleMask * faceMask * boundaryFalloffAttr;
|
float totalMask = angleMask * faceMask * boundaryFalloffAttr;
|
||||||
vFaceMask = totalMask;
|
vFaceMask = totalMask;
|
||||||
|
vUserMask = faceMask;
|
||||||
|
vMaskType = boundaryMaskTypeAttr;
|
||||||
|
|
||||||
if (useDisplacement == 1) {
|
if (useDisplacement == 1) {
|
||||||
float h = computeHeightAtPoint(position, safeN, safeN);
|
float h = computeHeightAtPoint(position, safeN, safeN);
|
||||||
@@ -262,6 +267,8 @@ const fragmentShader = /* glsl */`
|
|||||||
varying vec3 vViewPos;
|
varying vec3 vViewPos;
|
||||||
varying vec3 vNormal;
|
varying vec3 vNormal;
|
||||||
varying float vFaceMask;
|
varying float vFaceMask;
|
||||||
|
varying float vUserMask;
|
||||||
|
varying float vMaskType;
|
||||||
|
|
||||||
// Fragment-only wrapper: compute face-stable projection normal via dFdx
|
// Fragment-only wrapper: compute face-stable projection normal via dFdx
|
||||||
// then delegate to the shared height function.
|
// then delegate to the shared height function.
|
||||||
@@ -334,7 +341,19 @@ const fragmentShader = /* glsl */`
|
|||||||
vec3 bumpN = length(bumpVec) > 1e-6 ? normalize(bumpVec) : N;
|
vec3 bumpN = length(bumpVec) > 1e-6 ? normalize(bumpVec) : N;
|
||||||
|
|
||||||
// ── Shading ───────────────────────────────────────────────────────────
|
// ── Shading ───────────────────────────────────────────────────────────
|
||||||
vec3 baseColor = mix(vec3(0.50, 0.50, 0.50), vec3(0.22, 0.68, 0.68), maskBlend);
|
// 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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
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));
|
||||||
|
|||||||
Reference in New Issue
Block a user