|
@@ -0,0 +1,533 @@
|
|
|
+import { useEffect, useMemo, useRef, useState } from "react";
|
|
|
+import { Button, Canvas, View } from "@tarojs/components";
|
|
|
+import { 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,
|
|
|
+ Vector3,
|
|
|
+} from "three-platformize";
|
|
|
+import { Easing, Tween } from "@tweenjs/tween.js";
|
|
|
+import { cancelAnimationFrame, requestAnimationFrame } from "@tarojs/runtime";
|
|
|
+import "./index.scss";
|
|
|
+
|
|
|
+enum MODEL_STATE {
|
|
|
+ DEFAULT = 0,
|
|
|
+ ZOOM_UP = 1,
|
|
|
+}
|
|
|
+
|
|
|
+interface TweenHandlerOptions<T extends object> {
|
|
|
+ /** 当前属性 */
|
|
|
+ curProps: T;
|
|
|
+ /** 目标属性 */
|
|
|
+ targetProps: T;
|
|
|
+ /** 动画更新回调 */
|
|
|
+ onUpdate?: (e: T) => void;
|
|
|
+ /** 动画完成回调 */
|
|
|
+ cb?: Function;
|
|
|
+ /** 动画时长 默认 1000 ms */
|
|
|
+ delay?: number;
|
|
|
+}
|
|
|
+
|
|
|
+const DENGZHAO_MIN_ROTATE = -1.2;
|
|
|
+const DENGZHAO_MAX_ROTATE = -2.55;
|
|
|
+
|
|
|
+const IndexPage: FC = observer(() => {
|
|
|
+ const clock = useRef(new Clock());
|
|
|
+ const bulbLight = useRef<PointLight>();
|
|
|
+ const cameraPosition = useRef(new Vector3(0, 100, 360));
|
|
|
+ /**
|
|
|
+ * 底部阴影
|
|
|
+ */
|
|
|
+ const shadowPlan = useRef<Mesh>();
|
|
|
+ const [modelState, setModelState] = useState<MODEL_STATE>(
|
|
|
+ MODEL_STATE.DEFAULT
|
|
|
+ );
|
|
|
+ /**
|
|
|
+ * 是否分解
|
|
|
+ */
|
|
|
+ const isSeparate = useMemo(
|
|
|
+ () => modelState === MODEL_STATE.ZOOM_UP,
|
|
|
+ [modelState]
|
|
|
+ );
|
|
|
+ const hotSeparateAnimArr = useRef<ReticleModel[]>([]);
|
|
|
+ const tagArr = useRef<TagModel[]>([]);
|
|
|
+
|
|
|
+ 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((material) => {
|
|
|
+ const meshStandardMaterial = new MeshStandardMaterial();
|
|
|
+ // TODO: 暂定 any
|
|
|
+ const mesh: any = renderModel.scene.getObjectByProperty(
|
|
|
+ "uuid",
|
|
|
+ material.uuid
|
|
|
+ );
|
|
|
+
|
|
|
+ // 设置物体是否投射阴影
|
|
|
+ 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 (material.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<T extends object>(options: TweenHandlerOptions<T>) {
|
|
|
+ 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((material) => {
|
|
|
+ // TODO: 暂定 any
|
|
|
+ const mesh: any = renderModel.scene.getObjectByProperty(
|
|
|
+ "uuid",
|
|
|
+ material.uuid
|
|
|
+ );
|
|
|
+
|
|
|
+ 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 handleClick = () => {
|
|
|
+ renderModel.setAutoRotate(ROTATE_TYPE.DELAY);
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 拆解/合并模型
|
|
|
+ */
|
|
|
+ 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.model?.traverse((item) => {
|
|
|
+ // @ts-ignore
|
|
|
+ if (item.isMesh) {
|
|
|
+ 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);
|
|
|
+ addReticleMeshs(true);
|
|
|
+ cb && cb();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ const model = renderModel.modelMaterialList.find(
|
|
|
+ (i) => i.name === item.name
|
|
|
+ );
|
|
|
+
|
|
|
+ if (model) {
|
|
|
+ initTag(false);
|
|
|
+ addReticleMeshs(false);
|
|
|
+ tweenHandler({
|
|
|
+ curProps: item.position,
|
|
|
+ targetProps: model.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 addReticleMeshs = (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);
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <View className="page" onClick={handleClick}>
|
|
|
+ <Canvas id="wgl" type="webgl" />
|
|
|
+
|
|
|
+ <View className="toolbar">
|
|
|
+ <Button className="btn" onClick={handleSeparate}>
|
|
|
+ {!isSeparate ? "拆解" : "合并"}
|
|
|
+ </Button>
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+ );
|
|
|
+});
|
|
|
+
|
|
|
+export default IndexPage;
|