import { useEffect, useMemo, useRef, useState } from "react"; import { Button, View } from "@tarojs/components"; import Taro, { FC } from "@tarojs/taro"; import { observer } from "mobx-react"; import { ROTATE_TYPE, renderModel, ShadowModel, ReticleModel, TagModelParams, TagModel, } from "../../models"; import { Clock, GammaEncoding, LinearFilter, Mesh, MeshStandardMaterial, PointLight, Vector2, Vector3, } from "three-platformize"; import { Easing, Tween } from "@tweenjs/tween.js"; import { cancelAnimationFrame, requestAnimationFrame } from "@tarojs/runtime"; import { CanvasAdapter } from "../../components"; import "./index.scss"; enum MODEL_STATE { DEFAULT = 0, ZOOM_UP = 1, ZOOM_UP_CLICK = 2, } interface TweenHandlerOptions { /** 当前属性 */ curProps: T; /** 目标属性 */ targetProps: T; /** 动画更新回调 */ onUpdate?: (e: T) => void; /** 动画完成回调 */ cb?: Function; /** 动画时长 默认 1000 ms */ delay?: number; } const DEFUALT_SCALE = 20; const DENGZHAO_MIN_ROTATE = -1.2; const DENGZHAO_MAX_ROTATE = -2.55; const system = Taro.getSystemInfoSync(); const IndexPage: FC = observer(() => { const clock = useRef(new Clock()); const bulbLight = useRef(); const cameraPosition = useRef(new Vector3(0, 100, 360)); /** * 底部阴影 */ const shadowPlan = useRef(); const [modelState, setModelState] = useState( MODEL_STATE.DEFAULT ); /** * 是否分解 */ const isSeparate = useMemo( () => modelState === MODEL_STATE.ZOOM_UP, [modelState] ); /** * 是否单独展示某个模型 */ const isSingleModel = useMemo( () => modelState === MODEL_STATE.ZOOM_UP_CLICK, [modelState] ); const hotSeparateAnimArr = useRef([]); const tagArr = useRef([]); const startMouse = useRef(new Vector2(0, 0)); const mouseV2 = useRef(new Vector2(0, 0)); useEffect(() => { setTimeout(async () => { await init(); animate(); }, 100); return renderModel.dispose; }, []); const init = async () => { await renderModel.init("#wgl"); // 创建点光源 bulbLight.current = new PointLight(16772744, 2.5, 50, 2); bulbLight.current.position.set(1.45, -2.95, -1.17); bulbLight.current.castShadow = true; bulbLight.current.visible = false; renderModel.scene.add(bulbLight.current); renderModel.setControlsStatus(true, true, false); handleControlsAngle(true); renderModel.camera.position.copy(cameraPosition.current); await renderModel.loadModel({ filePath: "https://houseoss.4dkankan.com/project/yzdyh-dadu/test/model.FBX", fileType: "fbx", }); renderModel.modelMaterialList.forEach((mesh) => { const meshStandardMaterial = new MeshStandardMaterial(); // 设置物体是否投射阴影 mesh.castShadow = true; // 设置物体是否接收阴影 mesh.receiveShadow = true; meshStandardMaterial.copy(mesh.material); // 设置材质透明度 meshStandardMaterial.opacity = 0.01; // 告诉 three 库该材质渲染为半透明 meshStandardMaterial.transparent = true; // 设置材质渲染面,2:双面渲染;1:只渲染正面;0:只渲染背面 meshStandardMaterial.side = 2; switch (mesh.name) { case "dengzhao1": case "dengzhao2": case "dengpan": setModelMaterial(meshStandardMaterial, "dengzhao"); break; case "dengguan": setModelMaterial(meshStandardMaterial, "dengguan"); break; case "tongniudengdizuo": setModelMaterial(meshStandardMaterial, "tongniudengdizuo"); break; } // 设置材质粗糙度 meshStandardMaterial.roughness = 0.9; // 设置材质金属度 meshStandardMaterial.metalness = 0.3; // 设置材质纹理过滤器 meshStandardMaterial.map!.minFilter = LinearFilter; meshStandardMaterial.map!.magFilter = LinearFilter; // 设置材质纹理编码方式 meshStandardMaterial.map!.encoding = GammaEncoding; mesh.material.dispose(); mesh.material = meshStandardMaterial; mesh.material.needsUpdate = true; }); setTimeout(() => { shadowPlaneShow(); modelChildRotation(5); tweenHandler({ curProps: { x: 0, }, targetProps: { x: 20, }, onUpdate: (e) => { renderModel.model?.scale.copy(new Vector3(e.x, e.x, e.x)); }, cb: () => { setTimeout(() => { renderModel.setAutoRotate(ROTATE_TYPE.TRUE); }, 800); }, }); }, 300); }; const setModelMaterial = ( meshStandardMaterial: MeshStandardMaterial, name: string ) => { renderModel.setModelMap( meshStandardMaterial, require(`./resource/${name}_D.jpg`) ); renderModel.setModelRoughness( meshStandardMaterial, require(`./resource/${name}_S.jpg`) ); renderModel.setModelMetalness( meshStandardMaterial, require(`./resource/${name}_R.jpg`) ); renderModel.setModelEmissive( meshStandardMaterial, require(`./resource/${name}_D.png`) ); }; /** * 相机动画 */ function tweenHandler(options: TweenHandlerOptions) { let animaId: number | NodeJS.Timeout = 0; const { curProps, targetProps, onUpdate, cb } = options; const tween = new Tween(curProps) .to(targetProps, options.delay || 1000) .onUpdate(onUpdate) .onComplete(() => { cancelAnimationFrame(animaId as number); cb && cb(); }) .easing(Easing.Quintic.Out) .start(); function animate(time?: number) { animaId = requestAnimationFrame(animate); tween.update(time); } animate(); } const modelChildRotation = (e: number) => { const dengzhao1Mesh = renderModel.model?.getObjectByName("dengzhao1"); if (dengzhao1Mesh) { dengzhao1Mesh.rotation.z = DENGZHAO_MIN_ROTATE + (DENGZHAO_MAX_ROTATE - DENGZHAO_MIN_ROTATE) * (e - 1) * 0.1; renderModel.modelMaterialList.forEach((mesh) => { if (e > 5) { const t = 1 - 10 * (e - 8) * (1 / 30); if (e >= 8) { mesh.material.opacity = t; } mesh.visible = !(t <= 0.01); } else { mesh.material.opacity = 1; mesh.visible = true; } }); } }; /** * 是否展示底部阴影 */ const shadowPlaneShow = (show = true) => { if (!shadowPlan.current && renderModel.model) { shadowPlan.current = new ShadowModel( "https://houseoss.4dkankan.com/project/yzdyh-dadu/test/shadow.png", 1024, 1024, { x: Math.PI / 2, y: Math.PI / 80, z: -Math.PI / 2 }, new Vector3(0, -2.35, 0), 0.0055, renderModel.model ).mesh; } shadowPlan.current!.visible = show; }; /** * 拆解/合并模型 */ const handleSeparate = () => { handleControlsAngle(isSeparate); renderModel.setAutoRotate(ROTATE_TYPE.FALSE); renderModel.setControlsStatus(false, false, false); if (!isSeparate) { handleControlsDistance(320); shadowPlaneShow(false); const pos = renderModel.camera.position.clone(); const lookAt = renderModel.controls.target.clone(); const targetPos = new Vector3(0, 0, 320); const targetLookAt = new Vector3(0, 0, 0); tweenHandler({ curProps: { x1: pos.x, y1: pos.y, z1: pos.z, x2: lookAt.x, y2: lookAt.y, z2: lookAt.z, }, targetProps: { x1: targetPos.x, y1: targetPos.y, z1: targetPos.z, x2: targetLookAt.x, y2: targetLookAt.y, z2: targetLookAt.z, }, onUpdate: (e) => { renderModel.camera.position.set(e.x1, e.y1, e.z1); renderModel.controls.target.set(e.x2, e.y2, e.z2); renderModel.controls.update(); }, cb: () => { handleModelSeparateAnimation(true, () => { renderModel.setControlsStatus(true, true, false, true, false); renderModel.setAutoRotate(ROTATE_TYPE.TRUE); setModelState(MODEL_STATE.ZOOM_UP); }); }, }); } else { handleModelSeparateAnimation(false, () => { revertModel(() => { renderModel.setControlsStatus(true, true, false); renderModel.setAutoRotate(ROTATE_TYPE.TRUE); shadowPlaneShow(true); setModelState(MODEL_STATE.DEFAULT); }); }); } }; /** * 恢复模型 */ const revertModel = (cb?: Function) => { const pos = renderModel.camera.position.clone(); const lookAt = renderModel.controls.target.clone(); const targetPos = new Vector3(0, 30, 372); const targetLookAt = new Vector3(0, 0, 0); tweenHandler({ curProps: { x1: pos.x, y1: pos.y, z1: pos.z, x2: lookAt.x, y2: lookAt.y, z2: lookAt.z, }, targetProps: { x1: targetPos.x, y1: targetPos.y, z1: targetPos.z, x2: targetLookAt.x, y2: targetLookAt.y, z2: targetLookAt.z, }, onUpdate: (e) => { renderModel.camera.position.set(e.x1, e.y1, e.z1); renderModel.controls.target.set(e.x2, e.y2, e.z2); renderModel.controls.update(); }, cb, }); }; const handleControlsDistance = (dis: number, maxDis = 470) => { renderModel.controls.minDistance = dis; renderModel.controls.maxDistance = maxDis; }; /** * 修改控制器垂直最大角度 */ const handleControlsAngle = (type: boolean) => { renderModel.controls.maxPolarAngle = type ? 0.59 * Math.PI : Math.PI; }; /** * 模型分离动画 * @param type 是否分离 */ const handleModelSeparateAnimation = (type: boolean, cb?: Function) => { let loadCount = 0; renderModel.modelMaterialList.forEach((item) => { if (type) { const pos = item.position.clone(); switch (item.name) { case "dengguan": pos.add(new Vector3(0, 1, 0)); break; case "dengpan": pos.add(new Vector3(0, -0.5, 0)); break; case "dengzhao1": pos.add(new Vector3(-0.3, 0.2, 0)); break; case "dengzhao2": pos.add(new Vector3(0.3, 0.2, 0)); break; case "tongniudengdizuo": pos.add(new Vector3(0, -1, 0)); } tweenHandler({ curProps: item.position, targetProps: pos, cb: () => { loadCount++; if (loadCount === 5) { initTag(true); initReticleMeshs(true); cb && cb(); } }, }); } else { initTag(false); initReticleMeshs(false); tweenHandler({ curProps: item.position, targetProps: item._position, cb, }); } }); }; const initTag = (visible: boolean) => { if (!tagArr.current.length) { const temp: TagModel[] = []; const tagStack: TagModelParams[] = [ { url: require("./resource/daoyanguan.png"), maskUrl: require("./resource/biaoqian_left.png"), width: 256, height: 36, position: new Vector3(1.1, 3.1, 0), scale: 0.007, rotation: { x: 0, y: 0, z: 0 }, }, { url: require("./resource/denggai.png"), maskUrl: require("./resource/biaoqian_left.png"), width: 256, height: 36, position: new Vector3(1.7, 2.4, 0), scale: 0.005, rotation: { x: 0, y: 0, z: 0 }, }, ]; tagStack.forEach((tag) => { const tagIns = new TagModel(tag); temp.push(tagIns); renderModel.model?.add(tagIns.mesh); }); tagArr.current.push(...temp); } tagArr.current.forEach((tag) => { visible ? tag.show() : tag.hide(); }); }; const initReticleMeshs = (visible: boolean) => { if (!hotSeparateAnimArr.current.length) { const temp: ReticleModel[] = []; const vectorStack = [ new Vector3(0, 2.7, 0.65), new Vector3(0, 0.9, 0.65), new Vector3(0, -0.7, 0.65), new Vector3(0, -2, 0.65), ]; vectorStack.forEach((vector) => { const reticle = new ReticleModel({ url: require("./resource/reticle-animation.png"), planeWidth: 150, tilesHorizontal: 50, tilesVertical: 1, tileDisplayDuration: 50, scale: 0.0022, position: vector, camera: renderModel.camera, }); renderModel.model?.add(reticle.reticleAnim); temp.push(reticle); }); hotSeparateAnimArr.current.push(...temp); } hotSeparateAnimArr.current.forEach((item) => { visible ? item.show() : item.hide(); }); }; const animate = () => { const time = clock.current.getDelta(); hotSeparateAnimArr.current.forEach((item) => { item.update(time); }); requestAnimationFrame(animate); }; const clickHandler = (x: number, y: number) => { const v2 = new Vector2(x, y); // 比较两个向量是否相等 if (v2.equals(startMouse.current)) { setCanvasPosition(x, y); const res = renderModel.mouseRaycaster(mouseV2.current); for (let i = 0; i < res.length; i++) { const name = res[i].object.name; if (!name) continue; console.log("click model:", name); if ( isSeparate && res[i].object.visible && res[i].object.parent?.visible ) { handleModelZoomUp(name); break; } } } }; /** * 单独展示某个模型 */ const handleModelZoomUp = (name: string) => { renderModel.setAutoRotate(ROTATE_TYPE.FALSE); modelSingleClick(name, () => { renderModel.setControlsStatus(false, true, false, true, false); setModelState(MODEL_STATE.ZOOM_UP_CLICK); }); }; const modelSingleClick = (name: string, cb: Function) => { let targetProps = { x: 0, y: 0, z: 0, scale: DEFUALT_SCALE, }; switch (name) { case "dengguan": targetProps = { x: 0, y: -68, z: 0, scale: 0.6 * DEFUALT_SCALE, }; break; } if (name) { renderModel.modelMaterialList.forEach((model) => { model.visible = model.name.includes(name); }); } initTag(false); initReticleMeshs(false); tweenHandler({ curProps: { x: renderModel.model!.position.x, y: renderModel.model!.position.y, z: renderModel.model!.position.z, scale: renderModel.model!.scale.x, }, targetProps, onUpdate: (e) => { renderModel.model?.position.set(e.x, e.y, e.z); renderModel.model?.scale.set(e.scale, e.scale, e.scale); }, cb: () => { if (name === "") { renderModel.modelMaterialList.forEach((model) => { model.visible = true; }); initTag(true); initReticleMeshs(true); } cb && cb(); }, }); }; const setCanvasPosition = (x: number, y: number) => { mouseV2.current.x = (x / system.windowWidth) * 2 - 1; mouseV2.current.y = (-y / system.windowHeight) * 2 + 1; }; /** * 返回分解模型 */ const backForSeparate = () => { modelSingleClick("", () => { renderModel.setControlsStatus(true, true, false, true, false); setModelState(MODEL_STATE.ZOOM_UP); }); }; return ( { // @ts-ignore startMouse.current = new Vector2(e.clientX, e.clientY); renderModel.setAutoRotate(ROTATE_TYPE.DELAY); }} onTouchEnd={(e) => { // @ts-ignore clickHandler(e.clientX, e.clientY); }} /> {isSingleModel ? ( ) : ( )} ); }); export default IndexPage;