mirror of
https://github.com/CNCKitchen/stlTexturizer.git
synced 2026-04-07 22:11:32 +00:00
feat: add texture aspect correction for non-square textures in UV mapping
This commit is contained in:
+24
-7
@@ -36,6 +36,14 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett
|
|||||||
const edge2 = new THREE.Vector3();
|
const edge2 = new THREE.Vector3();
|
||||||
const faceNrm = new THREE.Vector3();
|
const faceNrm = new THREE.Vector3();
|
||||||
|
|
||||||
|
// Texture aspect correction so non-square textures keep their proportions.
|
||||||
|
// The shorter axis gets aspect > 1 so it tiles faster, making each tile
|
||||||
|
// proportionally shorter in world-space to match the texture's content.
|
||||||
|
const tmax = Math.max(imgWidth, imgHeight, 1);
|
||||||
|
const aspectU = tmax / Math.max(imgWidth, 1);
|
||||||
|
const aspectV = tmax / Math.max(imgHeight, 1);
|
||||||
|
const settingsWithAspect = { ...settings, textureAspectU: aspectU, textureAspectV: aspectV };
|
||||||
|
|
||||||
const QUANT = 1e4;
|
const QUANT = 1e4;
|
||||||
const posKey = (x, y, z) =>
|
const posKey = (x, y, z) =>
|
||||||
`${Math.round(x * QUANT)}_${Math.round(y * QUANT)}_${Math.round(z * QUANT)}`;
|
`${Math.round(x * QUANT)}_${Math.round(y * QUANT)}_${Math.round(z * QUANT)}`;
|
||||||
@@ -203,15 +211,21 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett
|
|||||||
const rotRad = (settings.rotation ?? 0) * Math.PI / 180;
|
const rotRad = (settings.rotation ?? 0) * Math.PI / 180;
|
||||||
let grey = 0;
|
let grey = 0;
|
||||||
if (za[0] > 0) { // X-dominant zone → YZ projection
|
if (za[0] > 0) { // X-dominant zone → YZ projection
|
||||||
const uv = _cubicUV((tmpPos.y-bounds.min.y)/md, (tmpPos.z-bounds.min.z)/md, settings, rotRad);
|
let rawU = (tmpPos.y-bounds.min.y)/md;
|
||||||
|
if (sn[0] < 0) rawU = -rawU; // flip U for -X faces
|
||||||
|
const uv = _cubicUV(rawU, (tmpPos.z-bounds.min.z)/md, settings, rotRad, aspectU, aspectV);
|
||||||
grey += sampleBilinear(imageData.data, imgWidth, imgHeight, uv.u, uv.v) * (za[0]/total);
|
grey += sampleBilinear(imageData.data, imgWidth, imgHeight, uv.u, uv.v) * (za[0]/total);
|
||||||
}
|
}
|
||||||
if (za[1] > 0) { // Y-dominant zone → XZ projection
|
if (za[1] > 0) { // Y-dominant zone → XZ projection
|
||||||
const uv = _cubicUV((tmpPos.x-bounds.min.x)/md, (tmpPos.z-bounds.min.z)/md, settings, rotRad);
|
let rawU = (tmpPos.x-bounds.min.x)/md;
|
||||||
|
if (sn[1] > 0) rawU = -rawU; // flip U for +Y faces
|
||||||
|
const uv = _cubicUV(rawU, (tmpPos.z-bounds.min.z)/md, settings, rotRad, aspectU, aspectV);
|
||||||
grey += sampleBilinear(imageData.data, imgWidth, imgHeight, uv.u, uv.v) * (za[1]/total);
|
grey += sampleBilinear(imageData.data, imgWidth, imgHeight, uv.u, uv.v) * (za[1]/total);
|
||||||
}
|
}
|
||||||
if (za[2] > 0) { // Z-dominant zone → XY projection
|
if (za[2] > 0) { // Z-dominant zone → XY projection
|
||||||
const uv = _cubicUV((tmpPos.x-bounds.min.x)/md, (tmpPos.y-bounds.min.y)/md, settings, rotRad);
|
let rawU = (tmpPos.x-bounds.min.x)/md;
|
||||||
|
if (sn[2] < 0) rawU = -rawU; // flip U for -Z faces
|
||||||
|
const uv = _cubicUV(rawU, (tmpPos.y-bounds.min.y)/md, settings, rotRad, aspectU, aspectV);
|
||||||
grey += sampleBilinear(imageData.data, imgWidth, imgHeight, uv.u, uv.v) * (za[2]/total);
|
grey += sampleBilinear(imageData.data, imgWidth, imgHeight, uv.u, uv.v) * (za[2]/total);
|
||||||
}
|
}
|
||||||
dispCache.set(k, grey);
|
dispCache.set(k, grey);
|
||||||
@@ -221,7 +235,7 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett
|
|||||||
|
|
||||||
tmpNrm.set(sn[0], sn[1], sn[2]);
|
tmpNrm.set(sn[0], sn[1], sn[2]);
|
||||||
|
|
||||||
const uvResult = computeUV(tmpPos, tmpNrm, settings.mappingMode, settings, bounds);
|
const uvResult = computeUV(tmpPos, tmpNrm, settings.mappingMode, settingsWithAspect, bounds);
|
||||||
let grey;
|
let grey;
|
||||||
if (uvResult.triplanar) {
|
if (uvResult.triplanar) {
|
||||||
grey = 0;
|
grey = 0;
|
||||||
@@ -321,6 +335,9 @@ function sampleBilinear(data, w, h, u, v) {
|
|||||||
// Ensure [0,1) — guard against floating-point edge cases
|
// Ensure [0,1) — guard against floating-point edge cases
|
||||||
u = ((u % 1) + 1) % 1;
|
u = ((u % 1) + 1) % 1;
|
||||||
v = ((v % 1) + 1) % 1;
|
v = ((v % 1) + 1) % 1;
|
||||||
|
// Flip V to match WebGL/Three.js texture convention (flipY=true means
|
||||||
|
// v=0 is the bottom of the image, but ImageData row 0 is the top).
|
||||||
|
v = 1 - v;
|
||||||
|
|
||||||
const fx = u * (w - 1);
|
const fx = u * (w - 1);
|
||||||
const fy = v * (h - 1);
|
const fy = v * (h - 1);
|
||||||
@@ -345,9 +362,9 @@ function sampleBilinear(data, w, h, u, v) {
|
|||||||
|
|
||||||
/** Apply scale/offset/rotation to raw UV for cubic projection.
|
/** Apply scale/offset/rotation to raw UV for cubic projection.
|
||||||
* Mirrors the private applyTransform helper in mapping.js. */
|
* Mirrors the private applyTransform helper in mapping.js. */
|
||||||
function _cubicUV(rawU, rawV, settings, rotRad) {
|
function _cubicUV(rawU, rawV, settings, rotRad, aspectU, aspectV) {
|
||||||
let u = rawU / settings.scaleU + settings.offsetU;
|
let u = (rawU * aspectU) / settings.scaleU + settings.offsetU;
|
||||||
let v = rawV / settings.scaleV + settings.offsetV;
|
let v = (rawV * aspectV) / settings.scaleV + settings.offsetV;
|
||||||
if (rotRad !== 0) {
|
if (rotRad !== 0) {
|
||||||
const c = Math.cos(rotRad), s = Math.sin(rotRad);
|
const c = Math.cos(rotRad), s = Math.sin(rotRad);
|
||||||
u -= 0.5; v -= 0.5;
|
u -= 0.5; v -= 0.5;
|
||||||
|
|||||||
+12
-1
@@ -1423,7 +1423,18 @@ function getEffectiveMapEntry() {
|
|||||||
function updatePreview() {
|
function updatePreview() {
|
||||||
if (!currentGeometry || !currentBounds) return;
|
if (!currentGeometry || !currentBounds) return;
|
||||||
|
|
||||||
const fullSettings = { ...settings, bounds: currentBounds };
|
// Texture aspect correction so non-square textures keep their proportions.
|
||||||
|
// A 512×279 texture needs aspectV = 512/279 ≈ 1.84 so V tiles faster (more
|
||||||
|
// repetitions), making each tile shorter in world-space to match the texture's
|
||||||
|
// wider-than-tall content. The wider axis gets aspect = 1 (unchanged).
|
||||||
|
const tw = activeMapEntry?.width ?? 1, th = activeMapEntry?.height ?? 1;
|
||||||
|
const tmax = Math.max(tw, th, 1);
|
||||||
|
const fullSettings = {
|
||||||
|
...settings,
|
||||||
|
bounds: currentBounds,
|
||||||
|
textureAspectU: tmax / Math.max(tw, 1),
|
||||||
|
textureAspectV: tmax / Math.max(th, 1),
|
||||||
|
};
|
||||||
|
|
||||||
if (!activeMapEntry) {
|
if (!activeMapEntry) {
|
||||||
// No map yet — plain material
|
// No map yet — plain material
|
||||||
|
|||||||
+28
-7
@@ -104,7 +104,13 @@ export function getCubicBlendWeights(normal, blend, seamBandWidth = 0.35) {
|
|||||||
*/
|
*/
|
||||||
export function computeUV(pos, normal, mode, settings, bounds) {
|
export function computeUV(pos, normal, mode, settings, bounds) {
|
||||||
const { min, size, center } = bounds;
|
const { min, size, center } = bounds;
|
||||||
const { scaleU, scaleV, offsetU, offsetV } = settings;
|
// Compensate for non-square textures: divide scale by aspect correction
|
||||||
|
// so equal world-space distances produce equal physical texture distances.
|
||||||
|
const aU = settings.textureAspectU ?? 1;
|
||||||
|
const aV = settings.textureAspectV ?? 1;
|
||||||
|
const scaleU = (settings.scaleU) / aU;
|
||||||
|
const scaleV = (settings.scaleV) / aV;
|
||||||
|
const { offsetU, offsetV } = settings;
|
||||||
const rotRad = (settings.rotation ?? 0) * Math.PI / 180;
|
const rotRad = (settings.rotation ?? 0) * Math.PI / 180;
|
||||||
const maxDim = Math.max(size.x, size.y, size.z);
|
const maxDim = Math.max(size.x, size.y, size.z);
|
||||||
const md = Math.max(maxDim, 1e-6);
|
const md = Math.max(maxDim, 1e-6);
|
||||||
@@ -230,9 +236,17 @@ export function computeUV(pos, normal, mode, settings, bounds) {
|
|||||||
|
|
||||||
case MODE_CUBIC: {
|
case MODE_CUBIC: {
|
||||||
const weights = getCubicBlendWeights(normal, settings.mappingBlend ?? 0.0, settings.seamBandWidth ?? 0.35);
|
const weights = getCubicBlendWeights(normal, settings.mappingBlend ?? 0.0, settings.seamBandWidth ?? 0.35);
|
||||||
const tYZ = applyTransform((pos.y - min.y) / md, (pos.z - min.z) / md, scaleU, scaleV, offsetU, offsetV, rotRad);
|
// Flip U based on normal sign so opposite faces show correct (non-mirrored) text.
|
||||||
const tXZ = applyTransform((pos.x - min.x) / md, (pos.z - min.z) / md, scaleU, scaleV, offsetU, offsetV, rotRad);
|
// Derived from camera right = view_dir × up_dir for each face orientation (Z-up).
|
||||||
const tXY = applyTransform((pos.x - min.x) / md, (pos.y - min.y) / md, scaleU, scaleV, offsetU, offsetV, rotRad);
|
let yzU = (pos.y - min.y) / md;
|
||||||
|
if (normal.x < 0) yzU = -yzU;
|
||||||
|
let xzU = (pos.x - min.x) / md;
|
||||||
|
if (normal.y > 0) xzU = -xzU;
|
||||||
|
let xyU = (pos.x - min.x) / md;
|
||||||
|
if (normal.z < 0) xyU = -xyU;
|
||||||
|
const tYZ = applyTransform(yzU, (pos.z - min.z) / md, scaleU, scaleV, offsetU, offsetV, rotRad);
|
||||||
|
const tXZ = applyTransform(xzU, (pos.z - min.z) / md, scaleU, scaleV, offsetU, offsetV, rotRad);
|
||||||
|
const tXY = applyTransform(xyU, (pos.y - min.y) / md, scaleU, scaleV, offsetU, offsetV, rotRad);
|
||||||
|
|
||||||
if (weights.x > 0.999) return tYZ;
|
if (weights.x > 0.999) return tYZ;
|
||||||
if (weights.y > 0.999) return tXZ;
|
if (weights.y > 0.999) return tXZ;
|
||||||
@@ -263,18 +277,25 @@ export function computeUV(pos, normal, mode, settings, bounds) {
|
|||||||
const wy = by / sum;
|
const wy = by / sum;
|
||||||
const wz = bz / sum;
|
const wz = bz / sum;
|
||||||
|
|
||||||
|
// Flip U based on normal sign so opposite faces show correct (non-mirrored) text.
|
||||||
|
let yzU = (pos.y - min.y) / md;
|
||||||
|
if (normal.x < 0) yzU = -yzU;
|
||||||
|
let xzU = (pos.x - min.x) / md;
|
||||||
|
if (normal.y > 0) xzU = -xzU;
|
||||||
|
let xyU = (pos.x - min.x) / md;
|
||||||
|
if (normal.z < 0) xyU = -xyU;
|
||||||
const uvXY = {
|
const uvXY = {
|
||||||
u: (pos.x - min.x) / md,
|
u: xyU,
|
||||||
v: (pos.y - min.y) / md,
|
v: (pos.y - min.y) / md,
|
||||||
w: wz,
|
w: wz,
|
||||||
};
|
};
|
||||||
const uvXZ = {
|
const uvXZ = {
|
||||||
u: (pos.x - min.x) / md,
|
u: xzU,
|
||||||
v: (pos.z - min.z) / md,
|
v: (pos.z - min.z) / md,
|
||||||
w: wy,
|
w: wy,
|
||||||
};
|
};
|
||||||
const uvYZ = {
|
const uvYZ = {
|
||||||
u: (pos.y - min.y) / md,
|
u: yzU,
|
||||||
v: (pos.z - min.z) / md,
|
v: (pos.z - min.z) / md,
|
||||||
w: wx,
|
w: wx,
|
||||||
};
|
};
|
||||||
|
|||||||
+25
-8
@@ -38,6 +38,7 @@ const sharedGLSL = /* glsl */`
|
|||||||
uniform float capAngle;
|
uniform float capAngle;
|
||||||
uniform int symmetricDisplacement;
|
uniform int symmetricDisplacement;
|
||||||
uniform int useDisplacement;
|
uniform int useDisplacement;
|
||||||
|
uniform vec2 textureAspect;
|
||||||
|
|
||||||
const float PI = 3.14159265358979;
|
const float PI = 3.14159265358979;
|
||||||
const float TWO_PI = 6.28318530717959;
|
const float TWO_PI = 6.28318530717959;
|
||||||
@@ -81,9 +82,9 @@ const sharedGLSL = /* glsl */`
|
|||||||
return blendedWeights / (dot(blendedWeights, vec3(1.0)) + 1e-6);
|
return blendedWeights / (dot(blendedWeights, vec3(1.0)) + 1e-6);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sample after applying scale + tiling
|
// Sample after applying scale + tiling (aspect-corrected)
|
||||||
float sampleMap(vec2 rawUV) {
|
float sampleMap(vec2 rawUV) {
|
||||||
vec2 uv = rawUV / scaleUV + offsetUV;
|
vec2 uv = (rawUV * textureAspect) / scaleUV + offsetUV;
|
||||||
float c = cos(rotation); float s = sin(rotation);
|
float c = cos(rotation); float s = sin(rotation);
|
||||||
uv -= 0.5;
|
uv -= 0.5;
|
||||||
uv = vec2(c * uv.x - s * uv.y, s * uv.x + c * uv.y);
|
uv = vec2(c * uv.x - s * uv.y, s * uv.x + c * uv.y);
|
||||||
@@ -159,15 +160,29 @@ const sharedGLSL = /* glsl */`
|
|||||||
vec3 blend = abs(projN);
|
vec3 blend = abs(projN);
|
||||||
blend = pow(blend, vec3(4.0));
|
blend = pow(blend, vec3(4.0));
|
||||||
blend /= dot(blend, vec3(1.0)) + 1e-4;
|
blend /= dot(blend, vec3(1.0)) + 1e-4;
|
||||||
float hXY = sampleMap(vec2((pos.x - boundsMin.x) / md, (pos.y - boundsMin.y) / md));
|
// Flip U based on normal sign so opposite faces show correct (non-mirrored) text.
|
||||||
float hXZ = sampleMap(vec2((pos.x - boundsMin.x) / md, (pos.z - boundsMin.z) / md));
|
float yzU = (pos.y - boundsMin.y) / md;
|
||||||
float hYZ = sampleMap(vec2((pos.y - boundsMin.y) / md, (pos.z - boundsMin.z) / md));
|
if (projN.x < 0.0) yzU = -yzU;
|
||||||
|
float xzU = (pos.x - boundsMin.x) / md;
|
||||||
|
if (projN.y > 0.0) xzU = -xzU;
|
||||||
|
float xyU = (pos.x - boundsMin.x) / md;
|
||||||
|
if (projN.z < 0.0) xyU = -xyU;
|
||||||
|
float hXY = sampleMap(vec2(xyU, (pos.y - boundsMin.y) / md));
|
||||||
|
float hXZ = sampleMap(vec2(xzU, (pos.z - boundsMin.z) / md));
|
||||||
|
float hYZ = sampleMap(vec2(yzU, (pos.z - boundsMin.z) / md));
|
||||||
return hXY * blend.z + hXZ * blend.y + hYZ * blend.x;
|
return hXY * blend.z + hXZ * blend.y + hYZ * blend.x;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
float hYZ = sampleMap(vec2((pos.y - boundsMin.y) / md, (pos.z - boundsMin.z) / md));
|
// Flip U based on normal sign so opposite faces show correct (non-mirrored) text.
|
||||||
float hXZ = sampleMap(vec2((pos.x - boundsMin.x) / md, (pos.z - boundsMin.z) / md));
|
float yzU = (pos.y - boundsMin.y) / md;
|
||||||
float hXY = sampleMap(vec2((pos.x - boundsMin.x) / md, (pos.y - boundsMin.y) / md));
|
if (projN.x < 0.0) yzU = -yzU;
|
||||||
|
float xzU = (pos.x - boundsMin.x) / md;
|
||||||
|
if (projN.y > 0.0) xzU = -xzU;
|
||||||
|
float xyU = (pos.x - boundsMin.x) / md;
|
||||||
|
if (projN.z < 0.0) xyU = -xyU;
|
||||||
|
float hYZ = sampleMap(vec2(yzU, (pos.z - boundsMin.z) / md));
|
||||||
|
float hXZ = sampleMap(vec2(xzU, (pos.z - boundsMin.z) / md));
|
||||||
|
float hXY = sampleMap(vec2(xyU, (pos.y - boundsMin.y) / md));
|
||||||
vec3 bN = blendN;
|
vec3 bN = blendN;
|
||||||
vec3 absFaceN = abs(projN);
|
vec3 absFaceN = abs(projN);
|
||||||
float facePrimary = max(absFaceN.x, max(absFaceN.y, absFaceN.z));
|
float facePrimary = max(absFaceN.x, max(absFaceN.y, absFaceN.z));
|
||||||
@@ -355,6 +370,7 @@ export function updateMaterial(material, displacementTexture, settings) {
|
|||||||
u.capAngle.value = settings.capAngle ?? 20.0;
|
u.capAngle.value = settings.capAngle ?? 20.0;
|
||||||
u.symmetricDisplacement.value = settings.symmetricDisplacement ? 1 : 0;
|
u.symmetricDisplacement.value = settings.symmetricDisplacement ? 1 : 0;
|
||||||
u.useDisplacement.value = settings.useDisplacement ? 1 : 0;
|
u.useDisplacement.value = settings.useDisplacement ? 1 : 0;
|
||||||
|
u.textureAspect.value.set(settings.textureAspectU ?? 1, settings.textureAspectV ?? 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Internal ──────────────────────────────────────────────────────────────────
|
// ── Internal ──────────────────────────────────────────────────────────────────
|
||||||
@@ -382,6 +398,7 @@ function buildUniforms(tex, settings) {
|
|||||||
capAngle: { value: settings.capAngle ?? 20.0 },
|
capAngle: { value: settings.capAngle ?? 20.0 },
|
||||||
symmetricDisplacement: { value: settings.symmetricDisplacement ? 1 : 0 },
|
symmetricDisplacement: { value: settings.symmetricDisplacement ? 1 : 0 },
|
||||||
useDisplacement: { value: settings.useDisplacement ? 1 : 0 },
|
useDisplacement: { value: settings.useDisplacement ? 1 : 0 },
|
||||||
|
textureAspect: { value: new THREE.Vector2(settings.textureAspectU ?? 1, settings.textureAspectV ?? 1) },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user