mirror of
https://github.com/CNCKitchen/stlTexturizer.git
synced 2026-04-07 22:11:32 +00:00
feat: enhance displacement preview with face normal and mask attributes for improved rendering
This commit is contained in:
+230
-2
@@ -59,6 +59,7 @@ const settings = {
|
|||||||
// ── Displacement preview state ────────────────────────────────────────────────
|
// ── Displacement preview state ────────────────────────────────────────────────
|
||||||
let dispPreviewGeometry = null; // subdivided geometry with smoothNormal attribute
|
let dispPreviewGeometry = null; // subdivided geometry with smoothNormal attribute
|
||||||
let dispPreviewBusy = false; // true while async subdivision is running
|
let dispPreviewBusy = false; // true while async subdivision is running
|
||||||
|
let dispPreviewParentMap = null; // Int32Array: subdivided face → original face index
|
||||||
|
|
||||||
// ── DOM refs ──────────────────────────────────────────────────────────────────
|
// ── DOM refs ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -561,7 +562,15 @@ function pickTriangle(e) {
|
|||||||
_raycaster.setFromCamera(_canvasNDC(e), getCamera());
|
_raycaster.setFromCamera(_canvasNDC(e), getCamera());
|
||||||
const hits = _raycaster.intersectObject(mesh);
|
const hits = _raycaster.intersectObject(mesh);
|
||||||
const hit = getFrontFaceHit(hits, mesh);
|
const hit = getFrontFaceHit(hits, mesh);
|
||||||
return hit ? hit.faceIndex : -1;
|
if (!hit) return -1;
|
||||||
|
let fi = hit.faceIndex;
|
||||||
|
// When displacement preview is active the mesh uses the subdivided geometry,
|
||||||
|
// so the raycaster returns a subdivided face index. Map it back to the
|
||||||
|
// original face index so that excludedFaces always stores original indices.
|
||||||
|
if (dispPreviewGeometry && mesh.geometry === dispPreviewGeometry && dispPreviewParentMap) {
|
||||||
|
fi = dispPreviewParentMap[fi];
|
||||||
|
}
|
||||||
|
return fi;
|
||||||
}
|
}
|
||||||
|
|
||||||
function paintAt(e) {
|
function paintAt(e) {
|
||||||
@@ -572,7 +581,11 @@ function paintAt(e) {
|
|||||||
const hit = getFrontFaceHit(hits, mesh);
|
const hit = getFrontFaceHit(hits, mesh);
|
||||||
if (!hit) return;
|
if (!hit) return;
|
||||||
|
|
||||||
const triIdx = hit.faceIndex;
|
// Map subdivided → original face index when displacement preview is active
|
||||||
|
let triIdx = hit.faceIndex;
|
||||||
|
if (dispPreviewGeometry && mesh.geometry === dispPreviewGeometry && dispPreviewParentMap) {
|
||||||
|
triIdx = dispPreviewParentMap[triIdx];
|
||||||
|
}
|
||||||
|
|
||||||
if (brushIsRadius) {
|
if (brushIsRadius) {
|
||||||
const hitPt = hits[0].point;
|
const hitPt = hits[0].point;
|
||||||
@@ -607,6 +620,12 @@ function refreshExclusionOverlay() {
|
|||||||
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() })
|
||||||
: t(n === 1 ? 'excl.faceExcluded' : 'excl.facesExcluded', { n: n.toLocaleString() });
|
: t(n === 1 ? 'excl.faceExcluded' : 'excl.facesExcluded', { n: n.toLocaleString() });
|
||||||
|
|
||||||
|
// Update the faceMask attribute on the active preview geometry so the shader
|
||||||
|
// reflects user-painted exclusions in real time.
|
||||||
|
const activeGeo = (settings.useDisplacement && dispPreviewGeometry)
|
||||||
|
? dispPreviewGeometry : currentGeometry;
|
||||||
|
updateFaceMask(activeGeo);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateBrushCursor(e) {
|
function updateBrushCursor(e) {
|
||||||
@@ -865,6 +884,176 @@ function checkAmplitudeWarning() {
|
|||||||
amplitudeVal.classList.toggle('amp-danger', danger);
|
amplitudeVal.classList.toggle('amp-danger', danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set (or update) the `faceMask` vertex attribute on a geometry.
|
||||||
|
* 1.0 = textured, 0.0 = user-excluded. Angle masking stays in the shader.
|
||||||
|
*
|
||||||
|
* Always creates a fresh Float32BufferAttribute so that Three.js allocates a
|
||||||
|
* new WebGL buffer and uploads the current data. This avoids subtle buffer-
|
||||||
|
* caching issues where in-place array edits + needsUpdate could keep stale
|
||||||
|
* GPU data on some drivers.
|
||||||
|
*/
|
||||||
|
function updateFaceMask(geometry) {
|
||||||
|
if (!geometry) return;
|
||||||
|
const posCount = geometry.attributes.position.count;
|
||||||
|
const triCount = posCount / 3;
|
||||||
|
const maskArr = new Float32Array(posCount);
|
||||||
|
|
||||||
|
// Fast path: no user exclusion active
|
||||||
|
if (excludedFaces.size === 0 && !selectionMode) {
|
||||||
|
maskArr.fill(1.0);
|
||||||
|
} else {
|
||||||
|
const isDisp = (geometry === dispPreviewGeometry && dispPreviewParentMap);
|
||||||
|
for (let t = 0; t < triCount; t++) {
|
||||||
|
const origFace = isDisp ? dispPreviewParentMap[t] : t;
|
||||||
|
const excluded = selectionMode ? !excludedFaces.has(origFace) : excludedFaces.has(origFace);
|
||||||
|
const val = excluded ? 0.0 : 1.0;
|
||||||
|
maskArr[t * 3] = val;
|
||||||
|
maskArr[t * 3 + 1] = val;
|
||||||
|
maskArr[t * 3 + 2] = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
geometry.setAttribute('faceMask', new THREE.Float32BufferAttribute(maskArr, 1));
|
||||||
|
|
||||||
|
// Ensure faceNormal attribute exists (needed by shader for angle masking).
|
||||||
|
// For the original geometry normal == faceNormal; for subdivided geometry
|
||||||
|
// addFaceNormals() is called after subdivision, but guard here in case the
|
||||||
|
// attribute is still missing.
|
||||||
|
if (!geometry.attributes.faceNormal) {
|
||||||
|
addFaceNormals(geometry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a mapping from each subdivided face to its nearest original face
|
||||||
|
* using a grid-accelerated nearest-centroid lookup, with face normal
|
||||||
|
* tiebreaking to prevent boundary faces from being mapped to the wrong
|
||||||
|
* original face (e.g. a subdivided face on a cube edge mapped to the
|
||||||
|
* adjacent face instead of the correct one).
|
||||||
|
*/
|
||||||
|
function buildParentFaceMap(subdivGeo) {
|
||||||
|
if (!triangleCentroids || !currentGeometry) return null;
|
||||||
|
|
||||||
|
const origPos = currentGeometry.attributes.position.array;
|
||||||
|
const origTriCount = currentGeometry.attributes.position.count / 3;
|
||||||
|
const subPos = subdivGeo.attributes.position.array;
|
||||||
|
const subTriCount = subdivGeo.attributes.position.count / 3;
|
||||||
|
|
||||||
|
// Precompute original face normals
|
||||||
|
const origNormals = new Float32Array(origTriCount * 3);
|
||||||
|
const _e1 = new THREE.Vector3(), _e2 = new THREE.Vector3(), _fn = new THREE.Vector3();
|
||||||
|
for (let t = 0; t < origTriCount; t++) {
|
||||||
|
const b = t * 9;
|
||||||
|
_e1.set(origPos[b + 3] - origPos[b], origPos[b + 4] - origPos[b + 1], origPos[b + 5] - origPos[b + 2]);
|
||||||
|
_e2.set(origPos[b + 6] - origPos[b], origPos[b + 7] - origPos[b + 1], origPos[b + 8] - origPos[b + 2]);
|
||||||
|
_fn.crossVectors(_e1, _e2).normalize();
|
||||||
|
origNormals[t * 3] = _fn.x; origNormals[t * 3 + 1] = _fn.y; origNormals[t * 3 + 2] = _fn.z;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bounding box of original centroids
|
||||||
|
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
||||||
|
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
||||||
|
for (let i = 0; i < origTriCount; i++) {
|
||||||
|
const cx = triangleCentroids[i * 3], cy = triangleCentroids[i * 3 + 1], cz = triangleCentroids[i * 3 + 2];
|
||||||
|
if (cx < minX) minX = cx; if (cx > maxX) maxX = cx;
|
||||||
|
if (cy < minY) minY = cy; if (cy > maxY) maxY = cy;
|
||||||
|
if (cz < minZ) minZ = cz; if (cz > maxZ) maxZ = cz;
|
||||||
|
}
|
||||||
|
const pad = 1e-3;
|
||||||
|
minX -= pad; minY -= pad; minZ -= pad;
|
||||||
|
maxX += pad; maxY += pad; maxZ += pad;
|
||||||
|
|
||||||
|
const res = Math.max(4, Math.min(128, Math.ceil(Math.cbrt(origTriCount) * 2)));
|
||||||
|
const dx = (maxX - minX) / res || 1;
|
||||||
|
const dy = (maxY - minY) / res || 1;
|
||||||
|
const dz = (maxZ - minZ) / res || 1;
|
||||||
|
|
||||||
|
// Build spatial grid of original centroids
|
||||||
|
const grid = new Map();
|
||||||
|
const cellKey = (ix, iy, iz) => (ix * res + iy) * res + iz;
|
||||||
|
for (let i = 0; i < origTriCount; i++) {
|
||||||
|
const cx = triangleCentroids[i * 3], cy = triangleCentroids[i * 3 + 1], cz = triangleCentroids[i * 3 + 2];
|
||||||
|
const ix = Math.max(0, Math.min(res - 1, Math.floor((cx - minX) / dx)));
|
||||||
|
const iy = Math.max(0, Math.min(res - 1, Math.floor((cy - minY) / dy)));
|
||||||
|
const iz = Math.max(0, Math.min(res - 1, Math.floor((cz - minZ) / dz)));
|
||||||
|
const k = cellKey(ix, iy, iz);
|
||||||
|
const cell = grid.get(k);
|
||||||
|
if (cell) cell.push(i); else grid.set(k, [i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each subdivided face, find nearest original face by centroid distance
|
||||||
|
// with face-normal tiebreaking to resolve boundary ambiguity.
|
||||||
|
const parentMap = new Int32Array(subTriCount);
|
||||||
|
for (let st = 0; st < subTriCount; st++) {
|
||||||
|
const base = st * 9;
|
||||||
|
const sx = (subPos[base] + subPos[base + 3] + subPos[base + 6]) / 3;
|
||||||
|
const sy = (subPos[base + 1] + subPos[base + 4] + subPos[base + 7]) / 3;
|
||||||
|
const sz = (subPos[base + 2] + subPos[base + 5] + subPos[base + 8]) / 3;
|
||||||
|
|
||||||
|
// Subdivided face normal
|
||||||
|
_e1.set(subPos[base + 3] - subPos[base], subPos[base + 4] - subPos[base + 1], subPos[base + 5] - subPos[base + 2]);
|
||||||
|
_e2.set(subPos[base + 6] - subPos[base], subPos[base + 7] - subPos[base + 1], subPos[base + 8] - subPos[base + 2]);
|
||||||
|
_fn.crossVectors(_e1, _e2).normalize();
|
||||||
|
const snx = _fn.x, sny = _fn.y, snz = _fn.z;
|
||||||
|
|
||||||
|
const ix = Math.max(0, Math.min(res - 1, Math.floor((sx - minX) / dx)));
|
||||||
|
const iy = Math.max(0, Math.min(res - 1, Math.floor((sy - minY) / dy)));
|
||||||
|
const iz = Math.max(0, Math.min(res - 1, Math.floor((sz - minZ) / dz)));
|
||||||
|
|
||||||
|
let bestDist = Infinity, bestIdx = 0;
|
||||||
|
// Two-pass: prefer original faces whose normal aligns with the subdivided
|
||||||
|
// face (dot > 0.4 ≈ within ~66°), then among those pick the nearest
|
||||||
|
// centroid. This prevents boundary faces at sharp seams (cube edges etc.)
|
||||||
|
// from being mapped to the adjacent face even when that face's centroid
|
||||||
|
// happens to be closer. Falls back to pure nearest-centroid if no
|
||||||
|
// normal-matching candidate is found.
|
||||||
|
let bestDistAligned = Infinity, bestIdxAligned = -1;
|
||||||
|
for (let dix = -1; dix <= 1; dix++) {
|
||||||
|
for (let diy = -1; diy <= 1; diy++) {
|
||||||
|
for (let diz = -1; diz <= 1; diz++) {
|
||||||
|
const nix = ix + dix, niy = iy + diy, niz = iz + diz;
|
||||||
|
if (nix < 0 || nix >= res || niy < 0 || niy >= res || niz < 0 || niz >= res) continue;
|
||||||
|
const cell = grid.get(cellKey(nix, niy, niz));
|
||||||
|
if (!cell) continue;
|
||||||
|
for (const oi of cell) {
|
||||||
|
const cdx = sx - triangleCentroids[oi * 3];
|
||||||
|
const cdy = sy - triangleCentroids[oi * 3 + 1];
|
||||||
|
const cdz = sz - triangleCentroids[oi * 3 + 2];
|
||||||
|
const centroidDist = cdx * cdx + cdy * cdy + cdz * cdz;
|
||||||
|
if (centroidDist < bestDist) { bestDist = centroidDist; bestIdx = oi; }
|
||||||
|
const dot = snx * origNormals[oi * 3] + sny * origNormals[oi * 3 + 1] + snz * origNormals[oi * 3 + 2];
|
||||||
|
if (dot > 0.4 && centroidDist < bestDistAligned) {
|
||||||
|
bestDistAligned = centroidDist; bestIdxAligned = oi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the local grid search didn't find a normal-aligned original face
|
||||||
|
// (common for sparse original meshes like cubes where face centroids
|
||||||
|
// are far from the grid cell of a corner-adjacent subdivided face),
|
||||||
|
// fall back to a brute-force scan over ALL original faces.
|
||||||
|
if (bestIdxAligned < 0) {
|
||||||
|
for (let oi = 0; oi < origTriCount; oi++) {
|
||||||
|
const cdx = sx - triangleCentroids[oi * 3];
|
||||||
|
const cdy = sy - triangleCentroids[oi * 3 + 1];
|
||||||
|
const cdz = sz - triangleCentroids[oi * 3 + 2];
|
||||||
|
const centroidDist = cdx * cdx + cdy * cdy + cdz * cdz;
|
||||||
|
if (centroidDist < bestDist) { bestDist = centroidDist; bestIdx = oi; }
|
||||||
|
const dot = snx * origNormals[oi * 3] + sny * origNormals[oi * 3 + 1] + snz * origNormals[oi * 3 + 2];
|
||||||
|
if (dot > 0.4 && centroidDist < bestDistAligned) {
|
||||||
|
bestDistAligned = centroidDist; bestIdxAligned = oi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parentMap[st] = bestIdxAligned >= 0 ? bestIdxAligned : bestIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parentMap;
|
||||||
|
}
|
||||||
|
|
||||||
function updatePreview() {
|
function updatePreview() {
|
||||||
if (!currentGeometry || !currentBounds) return;
|
if (!currentGeometry || !currentBounds) return;
|
||||||
|
|
||||||
@@ -886,6 +1075,9 @@ function updatePreview() {
|
|||||||
? dispPreviewGeometry
|
? dispPreviewGeometry
|
||||||
: currentGeometry;
|
: currentGeometry;
|
||||||
|
|
||||||
|
// Ensure faceMask attribute is current before rendering
|
||||||
|
updateFaceMask(activeGeo);
|
||||||
|
|
||||||
if (!previewMaterial) {
|
if (!previewMaterial) {
|
||||||
previewMaterial = createPreviewMaterial(activeMapEntry.texture, fullSettings);
|
previewMaterial = createPreviewMaterial(activeMapEntry.texture, fullSettings);
|
||||||
loadGeometry(activeGeo, previewMaterial);
|
loadGeometry(activeGeo, previewMaterial);
|
||||||
@@ -898,6 +1090,35 @@ function updatePreview() {
|
|||||||
|
|
||||||
// ── Displacement preview ──────────────────────────────────────────────────────
|
// ── Displacement preview ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute and set flat geometric face normals as a `faceNormal` attribute.
|
||||||
|
* Unlike the `normal` attribute (which may be smooth/interpolated after
|
||||||
|
* subdivision), `faceNormal` is always the true per-triangle normal computed
|
||||||
|
* from the cross product of the triangle's edges. The shader uses this for
|
||||||
|
* angle-based masking so that smooth normals at edges don't cause mask bleeding.
|
||||||
|
*/
|
||||||
|
function addFaceNormals(geometry) {
|
||||||
|
const pos = geometry.attributes.position.array;
|
||||||
|
const count = geometry.attributes.position.count;
|
||||||
|
const fn = new Float32Array(count * 3);
|
||||||
|
const vA = new THREE.Vector3(), vB = new THREE.Vector3(), vC = new THREE.Vector3();
|
||||||
|
const e1 = new THREE.Vector3(), e2 = new THREE.Vector3(), n = new THREE.Vector3();
|
||||||
|
for (let i = 0; i < count; i += 3) {
|
||||||
|
vA.set(pos[i * 3], pos[i * 3 + 1], pos[i * 3 + 2]);
|
||||||
|
vB.set(pos[(i+1) * 3], pos[(i+1) * 3 + 1], pos[(i+1) * 3 + 2]);
|
||||||
|
vC.set(pos[(i+2) * 3], pos[(i+2) * 3 + 1], pos[(i+2) * 3 + 2]);
|
||||||
|
e1.subVectors(vB, vA);
|
||||||
|
e2.subVectors(vC, vA);
|
||||||
|
n.crossVectors(e1, e2).normalize();
|
||||||
|
for (let v = 0; v < 3; v++) {
|
||||||
|
fn[(i + v) * 3] = n.x;
|
||||||
|
fn[(i + v) * 3 + 1] = n.y;
|
||||||
|
fn[(i + v) * 3 + 2] = n.z;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
geometry.setAttribute('faceNormal', new THREE.Float32BufferAttribute(fn, 3));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute area-weighted smooth normals for a non-indexed geometry and store
|
* Compute area-weighted smooth normals for a non-indexed geometry and store
|
||||||
* them as a `smoothNormal` vec3 attribute. Every copy of the same position
|
* them as a `smoothNormal` vec3 attribute. Every copy of the same position
|
||||||
@@ -970,6 +1191,7 @@ async function toggleDisplacementPreview(enable) {
|
|||||||
// Revert to original geometry with bump-only shading.
|
// Revert to original geometry with bump-only shading.
|
||||||
if (currentGeometry && previewMaterial) {
|
if (currentGeometry && previewMaterial) {
|
||||||
updateMaterial(previewMaterial, activeMapEntry?.texture, { ...settings, bounds: currentBounds });
|
updateMaterial(previewMaterial, activeMapEntry?.texture, { ...settings, bounds: currentBounds });
|
||||||
|
updateFaceMask(currentGeometry);
|
||||||
setMeshGeometry(currentGeometry);
|
setMeshGeometry(currentGeometry);
|
||||||
}
|
}
|
||||||
// Dispose the subdivided preview geometry (no longer on the mesh)
|
// Dispose the subdivided preview geometry (no longer on the mesh)
|
||||||
@@ -977,6 +1199,7 @@ async function toggleDisplacementPreview(enable) {
|
|||||||
dispPreviewGeometry.dispose();
|
dispPreviewGeometry.dispose();
|
||||||
dispPreviewGeometry = null;
|
dispPreviewGeometry = null;
|
||||||
}
|
}
|
||||||
|
dispPreviewParentMap = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1003,11 +1226,16 @@ async function toggleDisplacementPreview(enable) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
addSmoothNormals(subdivided);
|
addSmoothNormals(subdivided);
|
||||||
|
addFaceNormals(subdivided);
|
||||||
|
|
||||||
// Dispose previous preview geometry if any
|
// Dispose previous preview geometry if any
|
||||||
if (dispPreviewGeometry) dispPreviewGeometry.dispose();
|
if (dispPreviewGeometry) dispPreviewGeometry.dispose();
|
||||||
dispPreviewGeometry = subdivided;
|
dispPreviewGeometry = subdivided;
|
||||||
|
|
||||||
|
// Build mapping from subdivided faces → original faces for exclusion masking
|
||||||
|
dispPreviewParentMap = buildParentFaceMap(subdivided);
|
||||||
|
updateFaceMask(subdivided);
|
||||||
|
|
||||||
// Force material recreation so it binds the new geometry with smoothNormal
|
// Force material recreation so it binds the new geometry with smoothNormal
|
||||||
if (previewMaterial) {
|
if (previewMaterial) {
|
||||||
previewMaterial.dispose();
|
previewMaterial.dispose();
|
||||||
|
|||||||
+38
-32
@@ -153,31 +153,37 @@ const vertexShader = /* glsl */`
|
|||||||
precision highp float;
|
precision highp float;
|
||||||
${sharedGLSL}
|
${sharedGLSL}
|
||||||
|
|
||||||
attribute vec3 smoothNormal;
|
attribute vec3 smoothNormal;
|
||||||
|
attribute vec3 faceNormal;
|
||||||
|
attribute float faceMask;
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
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);
|
||||||
|
// Use the true geometric face normal for angle masking so that
|
||||||
|
// smooth/interpolated normals from subdivision don't cause mask bleeding.
|
||||||
|
vec3 fN = length(faceNormal) > 1e-6 ? normalize(faceNormal) : safeN;
|
||||||
vec3 pos = position;
|
vec3 pos = position;
|
||||||
|
|
||||||
|
// Surface angle masking — hard per-face cutoff using flat face normal
|
||||||
|
float surfaceAngle = degrees(acos(clamp(abs(fN.z), 0.0, 1.0)));
|
||||||
|
float angleMask = 1.0;
|
||||||
|
if (fN.z < 0.0 && bottomAngleLimit >= 1.0)
|
||||||
|
angleMask = min(angleMask, surfaceAngle > bottomAngleLimit ? 1.0 : 0.0);
|
||||||
|
if (fN.z >= 0.0 && topAngleLimit >= 1.0)
|
||||||
|
angleMask = min(angleMask, surfaceAngle > topAngleLimit ? 1.0 : 0.0);
|
||||||
|
float totalMask = angleMask * faceMask;
|
||||||
|
vFaceMask = totalMask;
|
||||||
|
|
||||||
if (useDisplacement == 1) {
|
if (useDisplacement == 1) {
|
||||||
// Sample displacement texture using the same UV math as the fragment shader
|
|
||||||
float h = computeHeightAtPoint(position, safeN, safeN);
|
float h = computeHeightAtPoint(position, safeN, safeN);
|
||||||
if (symmetricDisplacement == 1) h = h - 0.5;
|
if (symmetricDisplacement == 1) h = h - 0.5;
|
||||||
|
h *= totalMask;
|
||||||
// Surface angle masking (same logic as fragment shader)
|
|
||||||
float surfaceAngle = degrees(acos(clamp(abs(safeN.z), 0.0, 1.0)));
|
|
||||||
float maskBlend = 1.0;
|
|
||||||
float FADE = 15.0;
|
|
||||||
if (safeN.z < 0.0 && bottomAngleLimit >= 1.0)
|
|
||||||
maskBlend = min(maskBlend, smoothstep(bottomAngleLimit, bottomAngleLimit + FADE, surfaceAngle));
|
|
||||||
if (safeN.z >= 0.0 && topAngleLimit >= 1.0)
|
|
||||||
maskBlend = min(maskBlend, smoothstep(topAngleLimit, topAngleLimit + FADE, surfaceAngle));
|
|
||||||
h = mix(0.0, h, maskBlend);
|
|
||||||
|
|
||||||
// Displace along smooth normal so all copies of the same position
|
// Displace along smooth normal so all copies of the same position
|
||||||
// arrive at the same point (watertight, no cracks).
|
// arrive at the same point (watertight, no cracks).
|
||||||
@@ -187,10 +193,10 @@ const vertexShader = /* glsl */`
|
|||||||
|
|
||||||
// Always pass the ORIGINAL position for UV computation in the fragment shader.
|
// Always pass the ORIGINAL position for UV computation in the fragment shader.
|
||||||
vModelPos = position;
|
vModelPos = position;
|
||||||
vModelNormal = safeN;
|
vModelNormal = fN;
|
||||||
vec4 mvPos = modelViewMatrix * vec4(pos, 1.0);
|
vec4 mvPos = modelViewMatrix * vec4(pos, 1.0);
|
||||||
vViewPos = mvPos.xyz;
|
vViewPos = mvPos.xyz;
|
||||||
vNormal = normalize(normalMatrix * safeN);
|
vNormal = normalize(normalMatrix * fN);
|
||||||
gl_Position = projectionMatrix * mvPos;
|
gl_Position = projectionMatrix * mvPos;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -199,10 +205,11 @@ const fragmentShader = /* glsl */`
|
|||||||
precision highp float;
|
precision highp float;
|
||||||
${sharedGLSL}
|
${sharedGLSL}
|
||||||
|
|
||||||
varying vec3 vModelPos;
|
varying vec3 vModelPos;
|
||||||
varying vec3 vModelNormal;
|
varying vec3 vModelNormal;
|
||||||
varying vec3 vViewPos;
|
varying vec3 vViewPos;
|
||||||
varying vec3 vNormal;
|
varying vec3 vNormal;
|
||||||
|
varying float vFaceMask;
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -220,20 +227,19 @@ const fragmentShader = /* glsl */`
|
|||||||
float h = getHeight();
|
float h = getHeight();
|
||||||
if (symmetricDisplacement == 1) h = h - 0.5;
|
if (symmetricDisplacement == 1) h = h - 0.5;
|
||||||
|
|
||||||
// ── Surface angle masking ─────────────────────────────────────────────
|
|
||||||
float surfaceAngle = degrees(acos(clamp(abs(vModelNormal.z), 0.0, 1.0)));
|
|
||||||
float maskBlend = 1.0;
|
|
||||||
float FADE = 15.0;
|
|
||||||
if (vModelNormal.z < 0.0 && bottomAngleLimit >= 1.0)
|
|
||||||
maskBlend = min(maskBlend, smoothstep(bottomAngleLimit, bottomAngleLimit + FADE, surfaceAngle));
|
|
||||||
if (vModelNormal.z >= 0.0 && topAngleLimit >= 1.0)
|
|
||||||
maskBlend = min(maskBlend, smoothstep(topAngleLimit, topAngleLimit + FADE, surfaceAngle));
|
|
||||||
h = mix(0.0, h, maskBlend);
|
|
||||||
|
|
||||||
// ── Bump mapping via screen-space height derivatives ──────────────────
|
// ── Bump mapping via screen-space height derivatives ──────────────────
|
||||||
|
// Compute derivatives on the RAW (unmasked) height so that screen-space
|
||||||
|
// 2×2 pixel quads spanning masked/unmasked boundaries don't produce
|
||||||
|
// large derivative spikes that bleed bump artifacts across the edge.
|
||||||
float dhx = dFdx(h);
|
float dhx = dFdx(h);
|
||||||
float dhy = dFdy(h);
|
float dhy = dFdy(h);
|
||||||
|
|
||||||
|
// ── Combined mask (angle + user exclusion) from vertex shader ────────
|
||||||
|
float maskBlend = vFaceMask;
|
||||||
|
h *= maskBlend;
|
||||||
|
dhx *= maskBlend;
|
||||||
|
dhy *= maskBlend;
|
||||||
|
|
||||||
vec3 dp1 = dFdx(vViewPos);
|
vec3 dp1 = dFdx(vViewPos);
|
||||||
vec3 dp2 = dFdy(vViewPos);
|
vec3 dp2 = dFdy(vViewPos);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user