177 lines
4.9 KiB
TypeScript
177 lines
4.9 KiB
TypeScript
import React, { useMemo, useRef, useEffect } from 'react';
|
|
import * as THREE from 'three';
|
|
import { useTexture } from '@react-three/drei';
|
|
|
|
// Mapping Types Enum
|
|
export const MAPPING_TYPES = {
|
|
UV: 0,
|
|
PLANAR: 1,
|
|
CYLINDER: 2,
|
|
SPHERE: 3,
|
|
BOX: 4,
|
|
};
|
|
|
|
const vertexShader = `
|
|
varying vec2 vUv;
|
|
varying vec3 vLocalPos;
|
|
varying vec3 vNormal;
|
|
varying vec3 vWorldPos;
|
|
|
|
void main() {
|
|
vUv = uv;
|
|
vLocalPos = position;
|
|
vNormal = normalize(normal);
|
|
vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
|
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
}
|
|
`;
|
|
|
|
const fragmentShader = `
|
|
uniform sampler2D uTexture;
|
|
uniform int uMappingType;
|
|
uniform float uScale;
|
|
uniform vec2 uTiling;
|
|
uniform vec2 uOffset;
|
|
uniform float uRotation;
|
|
uniform vec3 uBBoxMin;
|
|
uniform vec3 uBBoxMax;
|
|
|
|
varying vec2 vUv;
|
|
varying vec3 vLocalPos;
|
|
varying vec3 vNormal;
|
|
|
|
#ifndef PI
|
|
#define PI 3.14159265359
|
|
#endif
|
|
|
|
vec2 rotateUV(vec2 uv, float rotation) {
|
|
float mid = 0.5;
|
|
float cosAngle = cos(rotation);
|
|
float sinAngle = sin(rotation);
|
|
float dx = uv.x - mid;
|
|
float dy = uv.y - mid;
|
|
return vec2(
|
|
cosAngle * dx - sinAngle * dy + mid,
|
|
sinAngle * dx + cosAngle * dy + mid
|
|
);
|
|
}
|
|
|
|
void main() {
|
|
vec2 finalUv = vec2(0.0);
|
|
|
|
// Bounding box calculations for dynamic projections
|
|
vec3 boundsSize = uBBoxMax - uBBoxMin;
|
|
vec3 boundsPos = (vLocalPos - uBBoxMin) / (boundsSize + 0.00001);
|
|
vec3 center = (uBBoxMax + uBBoxMin) * 0.5;
|
|
vec3 dir = vLocalPos - center;
|
|
vec3 normDir = normalize(dir + 0.00001);
|
|
|
|
if (uMappingType == 0) {
|
|
// UV: Applies native geometry vUv
|
|
finalUv = vUv;
|
|
} else if (uMappingType == 1) {
|
|
// PLANAR (XZ / Top-Down)
|
|
finalUv = boundsPos.xz;
|
|
} else if (uMappingType == 2) {
|
|
// CYLINDER: u = angle around Y, v = normalized height
|
|
vec3 localRel = vLocalPos - center;
|
|
float u = (atan(localRel.z, localRel.x) / (2.0 * PI)) + 0.5;
|
|
float v = boundsPos.y;
|
|
finalUv = vec2(u, v);
|
|
} else if (uMappingType == 3) {
|
|
// SPHERE
|
|
float u = (atan(normDir.z, normDir.x) / (2.0 * PI)) + 0.5;
|
|
float v = (asin(clamp(normDir.y, -1.0, 1.0)) / PI) + 0.5;
|
|
finalUv = vec2(u, v);
|
|
} else if (uMappingType == 4) {
|
|
// BOX (Triplanar)
|
|
vec3 absNormal = abs(vNormal);
|
|
if (absNormal.x > absNormal.y && absNormal.x > absNormal.z) {
|
|
finalUv = boundsPos.zy;
|
|
} else if (absNormal.y > absNormal.x && absNormal.y > absNormal.z) {
|
|
finalUv = boundsPos.xz;
|
|
} else {
|
|
finalUv = boundsPos.xy;
|
|
}
|
|
}
|
|
|
|
// Apply Tiling, Scale, and Offset
|
|
finalUv = finalUv * uTiling * uScale + uOffset;
|
|
|
|
// Apply Rotation
|
|
if (uRotation != 0.0) {
|
|
finalUv = rotateUV(finalUv, uRotation);
|
|
}
|
|
|
|
vec4 texColor = texture2D(uTexture, finalUv);
|
|
gl_FragColor = texColor;
|
|
}
|
|
`;
|
|
|
|
export const UnifiedMappingMaterial = ({
|
|
textureUrl,
|
|
mappingType = 'UV',
|
|
uvScale = 1.0,
|
|
uvTiling = [1, 1],
|
|
uvOffset = [0, 0],
|
|
uvRotation = 0,
|
|
geometry
|
|
}) => {
|
|
const materialRef = useRef<THREE.ShaderMaterial>(null);
|
|
|
|
// Load texture
|
|
const texture = useTexture(textureUrl) as THREE.Texture;
|
|
|
|
useEffect(() => {
|
|
if (texture) {
|
|
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
|
|
texture.needsUpdate = true;
|
|
}
|
|
}, [texture]);
|
|
|
|
// Initialize Uniforms
|
|
const uniforms = useMemo(() => ({
|
|
uTexture: { value: texture },
|
|
uMappingType: { value: MAPPING_TYPES[mappingType as keyof typeof MAPPING_TYPES] || 0 },
|
|
uScale: { value: uvScale },
|
|
uTiling: { value: new THREE.Vector2(uvTiling[0], uvTiling[1]) },
|
|
uOffset: { value: new THREE.Vector2(uvOffset[0], uvOffset[1]) },
|
|
uRotation: { value: uvRotation * (Math.PI / 180) },
|
|
uBBoxMin: { value: new THREE.Vector3(-1, -1, -1) },
|
|
uBBoxMax: { value: new THREE.Vector3(1, 1, 1) }
|
|
}), [texture]);
|
|
|
|
// Real-time Uniform Updates
|
|
useEffect(() => {
|
|
if (materialRef.current) {
|
|
const u = materialRef.current.uniforms;
|
|
u.uMappingType.value = MAPPING_TYPES[mappingType as keyof typeof MAPPING_TYPES] || 0;
|
|
u.uScale.value = uvScale;
|
|
u.uTiling.value.set(uvTiling[0], uvTiling[1]);
|
|
u.uOffset.value.set(uvOffset[0], uvOffset[1]);
|
|
u.uRotation.value = uvRotation * (Math.PI / 180);
|
|
}
|
|
}, [mappingType, uvScale, uvTiling, uvOffset, uvRotation]);
|
|
|
|
// Bounding Box Update
|
|
useEffect(() => {
|
|
if (geometry && materialRef.current) {
|
|
geometry.computeBoundingBox();
|
|
const box = geometry.boundingBox || new THREE.Box3().setFromObject(new THREE.Mesh(geometry));
|
|
materialRef.current.uniforms.uBBoxMin.value.copy(box.min);
|
|
materialRef.current.uniforms.uBBoxMax.value.copy(box.max);
|
|
}
|
|
}, [geometry]);
|
|
|
|
return (
|
|
<shaderMaterial
|
|
ref={materialRef}
|
|
uniforms={uniforms}
|
|
vertexShader={vertexShader}
|
|
fragmentShader={fragmentShader}
|
|
side={THREE.DoubleSide}
|
|
transparent={true}
|
|
/>
|
|
);
|
|
};
|