Initial commit of VIZ 3D project
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
import { useRef, useEffect, forwardRef, useMemo, useState } from "react";
|
||||
import * as THREE from "three";
|
||||
import { useFrame, useThree, createPortal } from "@react-three/fiber";
|
||||
import { MaterialFactory } from "../materials/MaterialFactory";
|
||||
import { useTexturePipeline } from "../hooks/useTexturePipeline";
|
||||
|
||||
interface ModelViewerProps {
|
||||
object: THREE.Object3D | null;
|
||||
sceneObjects: any[];
|
||||
}
|
||||
|
||||
interface MeshMaterialManagerProps {
|
||||
mesh: THREE.Mesh;
|
||||
sceneObj: any;
|
||||
textureCache: React.MutableRefObject<Map<string, THREE.Texture>>;
|
||||
}
|
||||
|
||||
const MeshMaterialManager = ({ mesh, sceneObj, textureCache }: MeshMaterialManagerProps) => {
|
||||
useEffect(() => {
|
||||
if (!mesh || !sceneObj.materialProps) return;
|
||||
|
||||
const props = sceneObj.materialProps;
|
||||
const mappingType = props.mappingType || 'uv';
|
||||
const uvScale = props.uvScale || 1;
|
||||
const uvTiling = props.uvTiling || [1, 1];
|
||||
const uvOffset = props.uvOffset || [0, 0];
|
||||
const uvRotation = props.uvRotation || 0;
|
||||
|
||||
const origMat = (Array.isArray(mesh.material) ? mesh.material[0] : mesh.material) as THREE.MeshPhysicalMaterial;
|
||||
|
||||
const baseParams: any = {
|
||||
color: origMat.color,
|
||||
roughness: origMat.roughness !== undefined ? origMat.roughness : 0.5,
|
||||
metalness: origMat.metalness !== undefined ? origMat.metalness : 0,
|
||||
map: origMat.map,
|
||||
normalMap: origMat.normalMap,
|
||||
roughnessMap: origMat.roughnessMap,
|
||||
metalnessMap: origMat.metalnessMap,
|
||||
aoMap: origMat.aoMap,
|
||||
emissiveMap: origMat.emissiveMap,
|
||||
emissive: origMat.emissive,
|
||||
emissiveIntensity: origMat.emissiveIntensity,
|
||||
envMapIntensity: origMat.envMapIntensity,
|
||||
alphaMap: origMat.alphaMap,
|
||||
transparent: origMat.transparent,
|
||||
opacity: origMat.opacity,
|
||||
side: origMat.side,
|
||||
alphaTest: origMat.alphaTest,
|
||||
};
|
||||
|
||||
// Use MaterialFactory to get reusable materials
|
||||
let activeMat: any;
|
||||
if (mappingType === 'triplanar') {
|
||||
activeMat = MaterialFactory.getInstance().getTriplanarMaterial(
|
||||
baseParams,
|
||||
mesh.uuid
|
||||
);
|
||||
|
||||
activeMat.triplanarUniforms.uTriplanarScale.value = uvScale;
|
||||
activeMat.triplanarUniforms.uBlendSharpness.value = 5.0; // Default or from props
|
||||
} else {
|
||||
activeMat = MaterialFactory.getInstance().getStandardUVMaterial(
|
||||
baseParams,
|
||||
mesh.uuid
|
||||
);
|
||||
|
||||
let modeIndex = 0;
|
||||
if (mappingType === 'planar') modeIndex = 1;
|
||||
else if (mappingType === 'box') modeIndex = 2;
|
||||
else if (mappingType === 'cylinder') modeIndex = 3;
|
||||
|
||||
activeMat.uvUniforms.uMappingMode.value = modeIndex;
|
||||
activeMat.updateUVTransform(uvOffset, [uvTiling[0] * uvScale, uvTiling[1] * uvScale], uvRotation * (Math.PI / 180));
|
||||
}
|
||||
|
||||
// Apply basic props
|
||||
if (props.color) activeMat.color.set(props.color);
|
||||
if (props.roughness !== undefined) activeMat.roughness = props.roughness;
|
||||
if (props.metalness !== undefined) activeMat.metalness = props.metalness;
|
||||
if (props.opacity !== undefined) activeMat.opacity = props.opacity;
|
||||
if (props.transmission !== undefined) activeMat.transmission = props.transmission;
|
||||
activeMat.transparent = (props.transmission > 0) || (props.opacity < 1) || !!props.alphaMap;
|
||||
|
||||
if (Array.isArray(mesh.material)) {
|
||||
mesh.material = mesh.material.map(() => activeMat);
|
||||
} else {
|
||||
mesh.material = activeMat;
|
||||
}
|
||||
|
||||
// Textures
|
||||
const textureTypes: ('map' | 'normalMap' | 'specularMap' | 'alphaMap')[] = ['map', 'normalMap', 'specularMap', 'alphaMap'];
|
||||
textureTypes.forEach(type => {
|
||||
const url = props[type];
|
||||
if (url) {
|
||||
if (!activeMat[type] || activeMat[type]!.name !== url) {
|
||||
if (textureCache.current.has(url)) {
|
||||
activeMat[type] = textureCache.current.get(url)!;
|
||||
if (mappingType === 'triplanar') activeMat.triplanarUniforms['t' + type.charAt(0).toUpperCase() + type.slice(1).replace('Map', '')].value = activeMat[type];
|
||||
activeMat.needsUpdate = true;
|
||||
} else {
|
||||
const loader = new THREE.TextureLoader();
|
||||
loader.load(url, (tex) => {
|
||||
tex.name = url;
|
||||
if (type === 'map') tex.colorSpace = THREE.SRGBColorSpace;
|
||||
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
|
||||
textureCache.current.set(url, tex);
|
||||
activeMat[type] = tex;
|
||||
if (mappingType === 'triplanar') activeMat.triplanarUniforms['t' + type.charAt(0).toUpperCase() + type.slice(1).replace('Map', '')].value = tex;
|
||||
activeMat.needsUpdate = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (url === null && activeMat[type]) {
|
||||
activeMat[type] = null;
|
||||
if (mappingType === 'triplanar') activeMat.triplanarUniforms['t' + type.charAt(0).toUpperCase() + type.slice(1).replace('Map', '')].value = null;
|
||||
activeMat.needsUpdate = true;
|
||||
}
|
||||
});
|
||||
|
||||
mesh.visible = sceneObj.visible;
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
mesh.frustumCulled = false;
|
||||
|
||||
// Sync individual mesh transform
|
||||
if (sceneObj.transformProps) {
|
||||
const { position, rotation, scale } = sceneObj.transformProps;
|
||||
mesh.position.set(position[0], position[1], position[2]);
|
||||
mesh.rotation.set(rotation[0], rotation[1], rotation[2]);
|
||||
mesh.scale.set(scale[0], scale[1], scale[2]);
|
||||
}
|
||||
|
||||
}, [sceneObj, mesh]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const FillMesh = ({ fillProps, originalMesh }: { fillProps: any, originalMesh: THREE.Mesh }) => {
|
||||
const materialRef = useRef<any>(null);
|
||||
const [clippingPlane] = useState(() => new THREE.Plane(new THREE.Vector3(0, -1, 0), 0));
|
||||
const { size: winSize } = useThree();
|
||||
|
||||
// Clone geometry and fix interpolation
|
||||
const geometry = useMemo(() => {
|
||||
const geo = originalMesh.geometry.clone();
|
||||
geo.computeVertexNormals();
|
||||
return geo;
|
||||
}, [originalMesh.geometry]);
|
||||
|
||||
// Update clipping plane based on height
|
||||
useEffect(() => {
|
||||
geometry.computeBoundingBox();
|
||||
const box = geometry.boundingBox || new THREE.Box3().setFromObject(originalMesh);
|
||||
const minY = box.min.y;
|
||||
const maxY = box.max.y;
|
||||
// height is 0 to 1
|
||||
const clipY = minY + (maxY - minY) * (fillProps.height ?? 0.5);
|
||||
// Since this plane is applied in world space by Three.js by default,
|
||||
// wait, localClippingEnabled = true means the plane is in LOCAL space!
|
||||
// If it's local space, clipY is exact.
|
||||
clippingPlane.constant = clipY;
|
||||
}, [geometry, fillProps.height, originalMesh]);
|
||||
|
||||
useFrame((state) => {
|
||||
if (materialRef.current && fillProps.type === 'liquid') {
|
||||
const mat = materialRef.current;
|
||||
if (mat.uTime !== undefined) mat.uTime = state.clock.elapsedTime;
|
||||
if (mat.uResolution && mat.uResolution.set) {
|
||||
mat.uResolution.set(winSize.width || 800, winSize.height || 600);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const fillNode = (
|
||||
<mesh geometry={geometry} scale={[0.995, 0.995, 0.995]}>
|
||||
{fillProps.type === 'liquid' ? (
|
||||
// @ts-ignore
|
||||
<liquidMaterialImpl
|
||||
ref={materialRef}
|
||||
uColor={new THREE.Color(fillProps.color)}
|
||||
transparent
|
||||
side={THREE.DoubleSide}
|
||||
clippingPlanes={[clippingPlane]}
|
||||
clipping={true}
|
||||
/>
|
||||
) : (
|
||||
<meshPhysicalMaterial
|
||||
color={fillProps.color}
|
||||
side={THREE.DoubleSide}
|
||||
clippingPlanes={[clippingPlane]}
|
||||
roughness={0.2}
|
||||
clearcoat={0.5}
|
||||
/>
|
||||
)}
|
||||
</mesh>
|
||||
);
|
||||
|
||||
return createPortal(fillNode, originalMesh);
|
||||
};
|
||||
|
||||
export const ModelViewer = forwardRef<THREE.Group, ModelViewerProps>(({ object, sceneObjects }, ref) => {
|
||||
const internalRef = useRef<THREE.Group>(null);
|
||||
const groupRef = (ref as any) || internalRef;
|
||||
const textureCache = useRef<Map<string, THREE.Texture>>(new Map());
|
||||
const meshCache = useRef<Map<string, THREE.Mesh>>(new Map());
|
||||
|
||||
useTexturePipeline(object);
|
||||
|
||||
// Cleanup textures on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
textureCache.current.forEach(tex => tex.dispose());
|
||||
textureCache.current.clear();
|
||||
meshCache.current.clear();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Build mesh cache when object changes
|
||||
useEffect(() => {
|
||||
meshCache.current.clear();
|
||||
if (object) {
|
||||
let mIdx = 0;
|
||||
object.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
const stableId = `mesh-${mIdx}`;
|
||||
meshCache.current.set(stableId, child);
|
||||
mIdx++;
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [object]);
|
||||
|
||||
// Main transform update for the core model
|
||||
useEffect(() => {
|
||||
if (object && sceneObjects) {
|
||||
const mainModel = sceneObjects.find(obj => obj.type === 'model');
|
||||
if (mainModel && mainModel.transformProps && groupRef.current) {
|
||||
const { position: p, rotation: r, scale: s } = mainModel.transformProps;
|
||||
groupRef.current.position.set(p[0], p[1], p[2]);
|
||||
groupRef.current.rotation.set(r[0], r[1], r[2]);
|
||||
groupRef.current.scale.set(s[0], s[1], s[2]);
|
||||
}
|
||||
}
|
||||
}, [object, sceneObjects]);
|
||||
|
||||
if (!object) return null;
|
||||
|
||||
const meshObjects = sceneObjects.filter(obj => obj.type === 'mesh');
|
||||
|
||||
return (
|
||||
<group ref={groupRef}>
|
||||
<primitive object={object} />
|
||||
{meshObjects.map(obj => {
|
||||
const mesh = meshCache.current.get(obj.id);
|
||||
if (!mesh) return null;
|
||||
return (
|
||||
<group key={obj.id}>
|
||||
<MeshMaterialManager
|
||||
mesh={mesh}
|
||||
sceneObj={obj}
|
||||
textureCache={textureCache}
|
||||
/>
|
||||
{obj.fillProps?.enabled && (
|
||||
<FillMesh fillProps={obj.fillProps} originalMesh={mesh} />
|
||||
)}
|
||||
</group>
|
||||
);
|
||||
})}
|
||||
</group>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user