123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645 |
- 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<T extends object> {
- /** 当前属性 */
- 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<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 isSingleModel = useMemo(
- () => modelState === MODEL_STATE.ZOOM_UP_CLICK,
- [modelState]
- );
- const hotSeparateAnimArr = useRef<ReticleModel[]>([]);
- const tagArr = useRef<TagModel[]>([]);
- 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<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((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 (
- <View className="page">
- <CanvasAdapter
- onTouchStart={(e) => {
- // @ts-ignore
- startMouse.current = new Vector2(e.clientX, e.clientY);
- renderModel.setAutoRotate(ROTATE_TYPE.DELAY);
- }}
- onTouchEnd={(e) => {
- // @ts-ignore
- clickHandler(e.clientX, e.clientY);
- }}
- />
- <View className="toolbar">
- {isSingleModel ? (
- <Button className="btn" onClick={backForSeparate}>
- 返回
- </Button>
- ) : (
- <Button className="btn" onClick={handleSeparate}>
- {!isSeparate ? "拆解" : "合并"}
- </Button>
- )}
- </View>
- </View>
- );
- });
- export default IndexPage;
|