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(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 ( ); };