mirror of
https://github.com/CNCKitchen/stlTexturizer.git
synced 2026-04-07 22:11:32 +00:00
feat: add symmetric displacement feature with UI integration and update displacement logic
This commit is contained in:
+10
-2
@@ -132,8 +132,8 @@
|
||||
</div>
|
||||
<div class="form-row slider-row">
|
||||
<label for="seam-blend" data-i18n="labels.seamBlend" data-i18n-title="tooltips.seamBlend" title="Softens the hard seam where projection faces meet. Effective for Cubic and Cylindrical modes.">Seam Blend ⓘ</label>
|
||||
<input type="range" id="seam-blend" min="0" max="1" step="0.01" value="0.20" />
|
||||
<input type="number" class="val" id="seam-blend-val" value="0.20" min="0" max="1" step="0.01" />
|
||||
<input type="range" id="seam-blend" min="0" max="1" step="0.01" value="1" />
|
||||
<input type="number" class="val" id="seam-blend-val" value="1" min="0" max="1" step="0.01" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -148,6 +148,14 @@
|
||||
<div id="amplitude-warning" class="amplitude-warning hidden" data-i18n="warnings.amplitudeOverlap">
|
||||
⚠ Amplitude exceeds 10% of the smallest model dimension — geometry overlaps may occur in the exported STL.
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="checkbox-label" for="symmetric-displacement"
|
||||
data-i18n-title="tooltips.symmetricDisplacement"
|
||||
title="When on, 50% grey = no displacement; white pushes out, black pushes in. Keeps part volume roughly constant.">
|
||||
<input type="checkbox" id="symmetric-displacement" />
|
||||
<span data-i18n="labels.symmetricDisplacement">Symmetric displacement ⓘ</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Transform -->
|
||||
|
||||
+19
-20
@@ -1,5 +1,5 @@
|
||||
import * as THREE from 'three';
|
||||
import { computeUV, getDominantCubicAxis } from './mapping.js';
|
||||
import { computeUV, getDominantCubicAxis, getCubicBlendWeights } from './mapping.js';
|
||||
|
||||
/**
|
||||
* Apply displacement to every vertex of a non-indexed BufferGeometry.
|
||||
@@ -7,7 +7,8 @@ import { computeUV, getDominantCubicAxis } from './mapping.js';
|
||||
* For each vertex:
|
||||
* 1. Compute UV with the same math used in the GLSL preview shader (mapping.js).
|
||||
* 2. Bilinear-sample the greyscale ImageData at that UV.
|
||||
* 3. Move the vertex along its normal by: grey * amplitude
|
||||
* 3. Move the vertex along its normal by: (grey − 0.5) × 2 × amplitude
|
||||
* so 50% grey = no displacement, white = outward, black = inward.
|
||||
*
|
||||
* @param {THREE.BufferGeometry} geometry – non-indexed (from subdivide())
|
||||
* @param {ImageData} imageData – raw pixel data from Canvas2D
|
||||
@@ -113,24 +114,21 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett
|
||||
const faceMasked = angleMasked;
|
||||
if (userExcluded && userExcludedFaces) userExcludedFaces[t / 3] = 1;
|
||||
|
||||
// For cubic mapping: assign this face's area to its single dominant zone (argmax).
|
||||
// Seam-edge vertices that border two zones still accumulate proportional blending
|
||||
// because those two different adjacent faces each contribute to their own zone.
|
||||
// Using argmax (instead of all-three-components) ensures that a face at exactly 45°
|
||||
// picks one projection consistently, eliminating the double-texture artefact.
|
||||
// For cubic mapping: distribute this face's area across projection zones
|
||||
// proportionally to its blend weights. When blend=0, getCubicBlendWeights
|
||||
// returns a one-hot vector (same as the old argmax), preserving sharp seams.
|
||||
// When blend>0, faces near a zone boundary contribute partial area to
|
||||
// adjacent zones, creating a smooth multi-vertex-wide gradient that matches
|
||||
// the preview shader. The old single-zone approach only blended at the
|
||||
// one-vertex-wide boundary, leaving an abrupt seam in the export.
|
||||
let czX = 0, czY = 0, czZ = 0;
|
||||
if (settings.mappingMode === 6 && faceArea > 1e-12) {
|
||||
switch (getDominantCubicAxis(faceNrm)) {
|
||||
case 'x':
|
||||
czX = faceArea;
|
||||
break;
|
||||
case 'y':
|
||||
czY = faceArea;
|
||||
break;
|
||||
default:
|
||||
czZ = faceArea;
|
||||
break;
|
||||
}
|
||||
const cubicBlend = settings.mappingBlend ?? 0;
|
||||
const unitFaceNrm = { x: faceNrm.x / faceArea, y: faceNrm.y / faceArea, z: faceNrm.z / faceArea };
|
||||
const w = getCubicBlendWeights(unitFaceNrm, cubicBlend);
|
||||
czX = w.x * faceArea;
|
||||
czY = w.y * faceArea;
|
||||
czZ = w.z * faceArea;
|
||||
}
|
||||
|
||||
for (let v = 0; v < 3; v++) {
|
||||
@@ -146,7 +144,7 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett
|
||||
} else {
|
||||
smoothNrmMap.set(k, [tmpNrm.x * faceArea, tmpNrm.y * faceArea, tmpNrm.z * faceArea]);
|
||||
}
|
||||
if (czX > 0 || czY > 0 || czZ > 0) {
|
||||
if (czX > 1e-12 || czY > 1e-12 || czZ > 1e-12) {
|
||||
const za = zoneAreaMap.get(k);
|
||||
if (za) { za[0] += czX; za[1] += czY; za[2] += czZ; }
|
||||
else { zoneAreaMap.set(k, [czX, czY, czZ]); }
|
||||
@@ -252,7 +250,8 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett
|
||||
const isSealedBoundary = !isFaceExcluded && excludedPosSet && excludedPosSet.has(k);
|
||||
const mf = maskedFracMap.get(k) || [0, 1];
|
||||
const maskedFrac = mf[1] > 0 ? mf[0] / mf[1] : 0;
|
||||
const disp = (isFaceExcluded || isSealedBoundary) ? 0 : (1 - maskedFrac) * grey * settings.amplitude;
|
||||
const centeredGrey = settings.symmetricDisplacement ? (grey - 0.5) * 2.0 : grey;
|
||||
const disp = (isFaceExcluded || isSealedBoundary) ? 0 : (1 - maskedFrac) * centeredGrey * settings.amplitude;
|
||||
|
||||
const newX = tmpPos.x + sn[0] * disp;
|
||||
const newY = tmpPos.y + sn[1] * disp;
|
||||
|
||||
+9
-1
@@ -92,6 +92,10 @@ export const TRANSLATIONS = {
|
||||
'excl.hintExclude': 'Excluded surfaces appear orange and will not receive displacement during export.',
|
||||
'excl.hintInclude': 'Selected surfaces appear green and will be the only ones to receive displacement during export.',
|
||||
|
||||
// Symmetric displacement
|
||||
'labels.symmetricDisplacement': 'Symmetric displacement \u24d8',
|
||||
'tooltips.symmetricDisplacement':'When on, 50% grey = no displacement; white pushes out, black pushes in. Keeps part volume roughly constant.',
|
||||
|
||||
// Amplitude overlap warning
|
||||
'warnings.amplitudeOverlap': '\u26a0 Amplitude exceeds 10% of the smallest model dimension \u2014 geometry overlaps may occur in the exported STL.',
|
||||
|
||||
@@ -221,8 +225,12 @@ export const TRANSLATIONS = {
|
||||
'excl.hintExclude': 'Ausgeschlossene Fl\u00e4chen erscheinen orange und erhalten beim Export keine Verschiebung.',
|
||||
'excl.hintInclude': 'Ausgew\u00e4hlte Fl\u00e4chen erscheinen gr\u00fcn und sind die einzigen, die beim Export eine Verschiebung erhalten.',
|
||||
|
||||
// Symmetric displacement
|
||||
'labels.symmetricDisplacement': 'Symmetrische Verschiebung \u24d8',
|
||||
'tooltips.symmetricDisplacement':'Wenn aktiv: 50% Grau = keine Verschiebung; Weiß nach außen, Schwarz nach innen. H\u00e4lt das Volumen des Teils in etwa konstant.',
|
||||
|
||||
// Amplitude overlap warning
|
||||
'warnings.amplitudeOverlap': '\u26a0 Amplitude überschreitet 10% der kleinsten Modellabmessung \u2014 beim Export k\u00f6nnen Geometrie\u00fcberschneidungen auftreten.',
|
||||
'warnings.amplitudeOverlap': '\u26a0 Amplitude \u00fcberschreitet 10% der kleinsten Modellabmessung \u2014 beim Export k\u00f6nnen Geometrie\u00fcberschneidungen auftreten.',
|
||||
|
||||
// Export section
|
||||
'sections.export': 'Export \u24d8',
|
||||
|
||||
+9
-3
@@ -50,7 +50,8 @@ const settings = {
|
||||
lockScale: true,
|
||||
bottomAngleLimit: 5,
|
||||
topAngleLimit: 0,
|
||||
mappingBlend: 0.2,
|
||||
mappingBlend: 1,
|
||||
symmetricDisplacement: false,
|
||||
};
|
||||
|
||||
// ── DOM refs ──────────────────────────────────────────────────────────────────
|
||||
@@ -99,6 +100,7 @@ const bottomAngleLimitVal = document.getElementById('bottom-angle-limit-val')
|
||||
const topAngleLimitVal = document.getElementById('top-angle-limit-val');
|
||||
const seamBlendSlider = document.getElementById('seam-blend');
|
||||
const seamBlendVal = document.getElementById('seam-blend-val');
|
||||
const symmetricDispToggle = document.getElementById('symmetric-displacement');
|
||||
|
||||
// ── Exclusion panel DOM refs ──────────────────────────────────────────────────
|
||||
const exclBrushBtn = document.getElementById('excl-brush-btn');
|
||||
@@ -310,6 +312,10 @@ function wireEvents() {
|
||||
linkSlider(bottomAngleLimitSlider, bottomAngleLimitVal, v => { settings.bottomAngleLimit = v; return v; });
|
||||
linkSlider(topAngleLimitSlider, topAngleLimitVal, v => { settings.topAngleLimit = v; return v; });
|
||||
linkSlider(seamBlendSlider, seamBlendVal, v => { settings.mappingBlend = v; return v.toFixed(2); });
|
||||
symmetricDispToggle.addEventListener('change', () => {
|
||||
settings.symmetricDisplacement = symmetricDispToggle.checked;
|
||||
updatePreview();
|
||||
});
|
||||
|
||||
// ── Export ──
|
||||
exportBtn.addEventListener('click', () => {
|
||||
@@ -723,8 +729,8 @@ async function handleSTL(file) {
|
||||
slider.value = value;
|
||||
valEl.value = value;
|
||||
};
|
||||
settings.scaleU = 1; scaleUSlider.value = scaleToPos(1); scaleUVal.value = 1;
|
||||
settings.scaleV = 1; scaleVSlider.value = scaleToPos(1); scaleVVal.value = 1;
|
||||
settings.scaleU = 0.5; scaleUSlider.value = scaleToPos(0.5); scaleUVal.value = 0.5;
|
||||
settings.scaleV = 0.5; scaleVSlider.value = scaleToPos(0.5); scaleVVal.value = 0.5;
|
||||
settings.offsetU = 0; resetVal(offsetUSlider, offsetUVal, 0);
|
||||
settings.offsetV = 0; resetVal(offsetVSlider, offsetVVal, 0);
|
||||
triLimitWarning.classList.add('hidden');
|
||||
|
||||
@@ -55,6 +55,7 @@ const fragmentShader = /* glsl */`
|
||||
uniform float bottomAngleLimit; // degrees from horizontal; 0 = disabled
|
||||
uniform float topAngleLimit; // degrees from horizontal; 0 = disabled
|
||||
uniform float mappingBlend; // 0 = sharp seams, 1 = fully blended
|
||||
uniform int symmetricDisplacement; // 1 = remap [0,1]→[-1,1] so 50% grey = no disp
|
||||
|
||||
varying vec3 vModelPos;
|
||||
varying vec3 vModelNormal;
|
||||
@@ -193,6 +194,7 @@ const fragmentShader = /* glsl */`
|
||||
// Flip normal for back faces so flipped-winding geometry still lights correctly.
|
||||
vec3 N = normalize(vNormal) * (gl_FrontFacing ? 1.0 : -1.0);
|
||||
float h = getHeight();
|
||||
if (symmetricDisplacement == 1) h = h * 2.0 - 1.0; // remap [0,1]→[-1,1]: 0.5 grey = zero
|
||||
|
||||
// ── Surface angle masking (FDM: suppress texture on near-horizontal faces) ────
|
||||
// Use a 15° smoothstep fade above the threshold so the bump tapers gradually
|
||||
@@ -204,7 +206,7 @@ const fragmentShader = /* glsl */`
|
||||
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.5, h, maskBlend); // blend toward neutral grey (zero-gradient → no bump)
|
||||
h = mix(0.0, h, maskBlend); // blend toward neutral (zero-gradient → no bump)
|
||||
|
||||
// ── Bump mapping via screen-space height derivatives ──────────────────
|
||||
float dhx = dFdx(h);
|
||||
@@ -287,7 +289,8 @@ export function updateMaterial(material, displacementTexture, settings) {
|
||||
}
|
||||
u.bottomAngleLimit.value = settings.bottomAngleLimit ?? 5.0;
|
||||
u.topAngleLimit.value = settings.topAngleLimit ?? 0.0;
|
||||
u.mappingBlend.value = settings.mappingBlend ?? 0.0;
|
||||
u.mappingBlend.value = settings.mappingBlend ?? 0.0;
|
||||
u.symmetricDisplacement.value = settings.symmetricDisplacement ? 1 : 0;
|
||||
}
|
||||
|
||||
// ── Internal ──────────────────────────────────────────────────────────────────
|
||||
@@ -310,7 +313,8 @@ function buildUniforms(tex, settings) {
|
||||
boundsCenter: { value: b.center.clone() },
|
||||
bottomAngleLimit: { value: settings.bottomAngleLimit ?? 5.0 },
|
||||
topAngleLimit: { value: settings.topAngleLimit ?? 0.0 },
|
||||
mappingBlend: { value: settings.mappingBlend ?? 0.0 },
|
||||
mappingBlend: { value: settings.mappingBlend ?? 0.0 },
|
||||
symmetricDisplacement: { value: settings.symmetricDisplacement ? 1 : 0 },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user