mirror of
https://github.com/CNCKitchen/stlTexturizer.git
synced 2026-04-07 22:11:32 +00:00
Enhance displacement calculation and UV mapping; add gizmo visualization in viewer
This commit is contained in:
+33
-9
@@ -26,8 +26,15 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett
|
||||
const newPos = new Float32Array(count * 3);
|
||||
const newNrm = new Float32Array(count * 3);
|
||||
|
||||
const tmpPos = new THREE.Vector3();
|
||||
const tmpNrm = new THREE.Vector3();
|
||||
const tmpPos = new THREE.Vector3();
|
||||
const tmpNrm = new THREE.Vector3();
|
||||
// Reusable vectors for per-face normal computation
|
||||
const vA = new THREE.Vector3();
|
||||
const vB = new THREE.Vector3();
|
||||
const vC = new THREE.Vector3();
|
||||
const edge1 = new THREE.Vector3();
|
||||
const edge2 = new THREE.Vector3();
|
||||
const faceNrm = new THREE.Vector3();
|
||||
|
||||
const REPORT_EVERY = 5000;
|
||||
|
||||
@@ -35,7 +42,24 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett
|
||||
tmpPos.fromBufferAttribute(posAttr, i);
|
||||
tmpNrm.fromBufferAttribute(nrmAttr, i);
|
||||
|
||||
const uvResult = computeUV(tmpPos, tmpNrm, settings.mappingMode, settings, bounds);
|
||||
// Compute a stable face normal from the triangle's own vertex positions.
|
||||
// The subdivider deduplicates vertices by position only, so shared corner
|
||||
// vertices pick up whichever face's normal happened to be stored first.
|
||||
// For hard-edged meshes (e.g. a cube) this corrupts the stored normals at
|
||||
// edges/corners. Recomputing from the triangle geometry is always correct
|
||||
// for the flat-shaded STL source data and gives the right normal for both
|
||||
// displacement direction and UV projection.
|
||||
const base = Math.floor(i / 3) * 3;
|
||||
vA.fromBufferAttribute(posAttr, base);
|
||||
vB.fromBufferAttribute(posAttr, base + 1);
|
||||
vC.fromBufferAttribute(posAttr, base + 2);
|
||||
edge1.subVectors(vB, vA);
|
||||
edge2.subVectors(vC, vA);
|
||||
faceNrm.crossVectors(edge1, edge2);
|
||||
// Fall back to the stored vertex normal for degenerate triangles
|
||||
const useNrm = faceNrm.lengthSq() > 1e-10 ? faceNrm.normalize() : tmpNrm;
|
||||
|
||||
const uvResult = computeUV(tmpPos, useNrm, settings.mappingMode, settings, bounds);
|
||||
|
||||
let grey;
|
||||
if (uvResult.triplanar) {
|
||||
@@ -50,13 +74,13 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett
|
||||
|
||||
const disp = grey * settings.amplitude;
|
||||
|
||||
newPos[i*3] = tmpPos.x + tmpNrm.x * disp;
|
||||
newPos[i*3+1] = tmpPos.y + tmpNrm.y * disp;
|
||||
newPos[i*3+2] = tmpPos.z + tmpNrm.z * disp;
|
||||
newPos[i*3] = tmpPos.x + useNrm.x * disp;
|
||||
newPos[i*3+1] = tmpPos.y + useNrm.y * disp;
|
||||
newPos[i*3+2] = tmpPos.z + useNrm.z * disp;
|
||||
|
||||
newNrm[i*3] = tmpNrm.x;
|
||||
newNrm[i*3+1] = tmpNrm.y;
|
||||
newNrm[i*3+2] = tmpNrm.z;
|
||||
newNrm[i*3] = useNrm.x;
|
||||
newNrm[i*3+1] = useNrm.y;
|
||||
newNrm[i*3+2] = useNrm.z;
|
||||
|
||||
if (onProgress && i % REPORT_EVERY === 0) onProgress(i / count);
|
||||
}
|
||||
|
||||
+1
-1
@@ -245,7 +245,7 @@ async function handleSTL(file) {
|
||||
|
||||
// Default edge length = 1/100 of the largest bounding box dimension
|
||||
const maxDim = Math.max(bounds.size.x, bounds.size.y, bounds.size.z);
|
||||
const defaultEdge = Math.max(0.1, Math.min(5.0, +(maxDim / 100).toFixed(2)));
|
||||
const defaultEdge = Math.max(0.1, Math.min(5.0, +(maxDim / 200).toFixed(2)));
|
||||
settings.refineLength = defaultEdge;
|
||||
refineLenSlider.value = defaultEdge;
|
||||
refineLenVal.textContent = `${defaultEdge.toFixed(2)} mm`;
|
||||
|
||||
+3
-3
@@ -78,9 +78,9 @@ export function computeUV(pos, normal, mode, settings, bounds) {
|
||||
const az = Math.abs(normal.z);
|
||||
let uRaw, vRaw;
|
||||
if (ax >= ay && ax >= az) {
|
||||
// ±X dominant → project onto YZ
|
||||
uRaw = (pos.y - min.y) / Math.max(size.y, 1e-6);
|
||||
vRaw = (pos.z - min.z) / Math.max(size.z, 1e-6);
|
||||
// ±X dominant → project onto ZY (U=Z, V=Y keeps texture upright on side faces)
|
||||
uRaw = (pos.z - min.z) / Math.max(size.z, 1e-6);
|
||||
vRaw = (pos.y - min.y) / Math.max(size.y, 1e-6);
|
||||
} else if (ay >= ax && ay >= az) {
|
||||
// ±Y dominant → project onto XZ
|
||||
uRaw = (pos.x - min.x) / Math.max(size.x, 1e-6);
|
||||
|
||||
@@ -106,8 +106,8 @@ const fragmentShader = /* glsl */`
|
||||
// Picks the single planar projection whose axis is most aligned with the face normal.
|
||||
vec3 absN = abs(MN);
|
||||
if (absN.x >= absN.y && absN.x >= absN.z) {
|
||||
// ±X dominant → project onto YZ plane
|
||||
return sampleMap((pos.yz - boundsMin.yz) / max(boundsSize.yz, vec2(1e-4)));
|
||||
// ±X dominant → project onto ZY plane (U=Z, V=Y keeps texture upright on side faces)
|
||||
return sampleMap((pos.zy - boundsMin.zy) / max(boundsSize.zy, vec2(1e-4)));
|
||||
} else if (absN.y >= absN.x && absN.y >= absN.z) {
|
||||
// ±Y dominant → project onto XZ plane
|
||||
return sampleMap((pos.xz - boundsMin.xz) / max(boundsSize.xz, vec2(1e-4)));
|
||||
|
||||
@@ -3,6 +3,57 @@ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||
|
||||
let renderer, camera, scene, controls, meshGroup, ambientLight, dirLight1, dirLight2, grid;
|
||||
let currentMesh = null;
|
||||
let gizmoScene, gizmoCamera;
|
||||
|
||||
const GIZMO_PX = 90; // gizmo viewport size in CSS pixels
|
||||
const GIZMO_MARGIN = 14;
|
||||
|
||||
function buildGizmo() {
|
||||
gizmoScene = new THREE.Scene();
|
||||
gizmoCamera = new THREE.OrthographicCamera(-1.6, 1.6, 1.6, -1.6, 0.1, 10);
|
||||
gizmoCamera.position.set(0, 0, 3);
|
||||
|
||||
const addAxis = (dir, hex, label) => {
|
||||
// Shaft line
|
||||
const shaft = new THREE.BufferGeometry().setFromPoints([
|
||||
new THREE.Vector3(0, 0, 0),
|
||||
dir.clone().multiplyScalar(0.78),
|
||||
]);
|
||||
gizmoScene.add(new THREE.Line(
|
||||
shaft,
|
||||
new THREE.LineBasicMaterial({ color: hex, depthTest: false }),
|
||||
));
|
||||
|
||||
// Arrow-head cone
|
||||
const cone = new THREE.Mesh(
|
||||
new THREE.ConeGeometry(0.10, 0.24, 8),
|
||||
new THREE.MeshBasicMaterial({ color: hex, depthTest: false }),
|
||||
);
|
||||
cone.position.copy(dir.clone().multiplyScalar(0.92));
|
||||
cone.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
|
||||
gizmoScene.add(cone);
|
||||
|
||||
// Text label sprite
|
||||
const c = document.createElement('canvas');
|
||||
c.width = c.height = 64;
|
||||
const ctx = c.getContext('2d');
|
||||
ctx.fillStyle = `#${hex.toString(16).padStart(6, '0')}`;
|
||||
ctx.font = 'bold 46px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(label, 32, 32);
|
||||
const sprite = new THREE.Sprite(
|
||||
new THREE.SpriteMaterial({ map: new THREE.CanvasTexture(c), depthTest: false }),
|
||||
);
|
||||
sprite.position.copy(dir.clone().multiplyScalar(1.26));
|
||||
sprite.scale.set(0.42, 0.42, 1);
|
||||
gizmoScene.add(sprite);
|
||||
};
|
||||
|
||||
addAxis(new THREE.Vector3(1, 0, 0), 0xff4040, 'X');
|
||||
addAxis(new THREE.Vector3(0, 1, 0), 0x44dd44, 'Y');
|
||||
addAxis(new THREE.Vector3(0, 0, 1), 0x5599ff, 'Z');
|
||||
}
|
||||
|
||||
export function initViewer(canvas) {
|
||||
// Renderer
|
||||
@@ -54,6 +105,8 @@ export function initViewer(canvas) {
|
||||
controls.maxDistance = 3000;
|
||||
controls.screenSpacePanning = true;
|
||||
|
||||
buildGizmo();
|
||||
|
||||
// Resize observer
|
||||
const resizeObserver = new ResizeObserver(() => onResize());
|
||||
resizeObserver.observe(canvas.parentElement);
|
||||
@@ -63,7 +116,29 @@ export function initViewer(canvas) {
|
||||
(function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
controls.update();
|
||||
|
||||
const cw = renderer.domElement.clientWidth;
|
||||
const ch = renderer.domElement.clientHeight;
|
||||
|
||||
// 1. Main scene — full viewport
|
||||
renderer.setScissorTest(false);
|
||||
renderer.setViewport(0, 0, cw, ch);
|
||||
renderer.render(scene, camera);
|
||||
|
||||
// 2. Gizmo overlay — upper-right corner
|
||||
// WebGL y=0 is at bottom, so upper-right means large y.
|
||||
const gx = cw - GIZMO_PX - GIZMO_MARGIN;
|
||||
const gy = ch - GIZMO_PX - GIZMO_MARGIN;
|
||||
gizmoCamera.quaternion.copy(camera.quaternion);
|
||||
renderer.setScissorTest(true);
|
||||
renderer.setScissor(gx, gy, GIZMO_PX, GIZMO_PX);
|
||||
renderer.setViewport(gx, gy, GIZMO_PX, GIZMO_PX);
|
||||
renderer.autoClear = false;
|
||||
renderer.clearDepth();
|
||||
renderer.render(gizmoScene, gizmoCamera);
|
||||
renderer.autoClear = true;
|
||||
renderer.setScissorTest(false);
|
||||
renderer.setViewport(0, 0, cw, ch);
|
||||
})();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user