diff --git a/package.json b/package.json index 639ddf6..18cbf02 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "simple-query-string": "^1.3.2", "solarlunar": "^2.0.7", "store": "^2.0.12", + "three": "^0.153.0", "uuid": "^3.3.2" }, "peerDependencies": { @@ -58,6 +59,7 @@ "@types/react": "^17.0.2", "@types/react-dom": "^17.0.2", "@types/store": "^2.0.5", + "@types/three": "^0.152.1", "@umijs/plugin-access": "2.4.2", "@umijs/plugin-dva": "^0.13.0", "@umijs/plugin-initial-state": "^2.4.0", diff --git a/public/images/cloud.png b/public/images/cloud.png new file mode 100644 index 0000000..017eb90 Binary files /dev/null and b/public/images/cloud.png differ diff --git a/public/images/earth_bg.png b/public/images/earth_bg.png new file mode 100644 index 0000000..558d06f Binary files /dev/null and b/public/images/earth_bg.png differ diff --git a/public/images/ground.png b/public/images/ground.png new file mode 100644 index 0000000..cdb610c Binary files /dev/null and b/public/images/ground.png differ diff --git a/public/images/sky.png b/public/images/sky.png new file mode 100644 index 0000000..5df64bc Binary files /dev/null and b/public/images/sky.png differ diff --git a/public/images/starflake1.png b/public/images/starflake1.png new file mode 100644 index 0000000..5040794 Binary files /dev/null and b/public/images/starflake1.png differ diff --git a/public/images/starflake2.png b/public/images/starflake2.png new file mode 100644 index 0000000..f959684 Binary files /dev/null and b/public/images/starflake2.png differ diff --git a/public/models/datacenter.glb b/public/models/datacenter.glb new file mode 100644 index 0000000..52ca978 Binary files /dev/null and b/public/models/datacenter.glb differ diff --git a/src/lib/three/Viewer/index.ts b/src/lib/three/Viewer/index.ts new file mode 100644 index 0000000..67b7388 --- /dev/null +++ b/src/lib/three/Viewer/index.ts @@ -0,0 +1,100 @@ +import { AxesHelper, Camera, PerspectiveCamera, Scene, WebGLRenderer } from "three"; +import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"; + +export type Animate = { + fun: (arg: any) => any; + content: any; +}; +export default class Viewer { + public id: string; + public viewerDom!: HTMLElement; + public renderer!: WebGLRenderer; + public scene!: Scene; + public camera!: PerspectiveCamera; + public controls!: OrbitControls; + public isDestroy = false; // 是否销毁 + public animateEventList = new Map(); + + constructor(id: string) { + this.id = id; + this.initViewer(); + } + + private initViewer() { + this.initRender(); + this.initScene(); + this.initCamera(); + this.initControl(); + + const animate = () => { + if (this.isDestroy) return; + requestAnimationFrame(animate); + this.updateDom(); + this.renderDom(); + + // 全局的公共动画函数,添加函数可同步执行 + this.animateEventList.forEach((event) => { + if (event.fun && event.content) event.fun(event.content); + }); + }; + animate(); + } + + private initRender() { + // 获取画布dom + this.viewerDom = document.getElementById(this.id) as HTMLElement; + this.renderer = new WebGLRenderer({ antialias: true }); + + this.viewerDom.appendChild(this.renderer.domElement); + } + + // 创建场景 + private initScene() { + this.scene = new Scene(); + } + + private initControl() { + this.controls = new OrbitControls(this.camera as Camera, this.renderer?.domElement); + + this.controls.enableDamping = false; + this.controls.screenSpacePanning = false; + this.controls.minDistance = 2; + this.controls.maxDistance = 1000; + } + + // 初始化相机 + public initCamera() { + this.camera = new PerspectiveCamera(75, this.viewerDom.clientWidth / this.viewerDom.clientHeight, 0.1, 1000); + this.camera.position.set(5, 5, 10); + this.camera.lookAt(0, 0, 0); + } + + //创建坐标轴辅助对象 + public addAxis() { + const axis = new AxesHelper(1000); + this.scene?.add(axis); + } + + // 添加动画事件 + public addAnimate(id: string, animate: Animate) { + this.animateEventList.set(id, animate); + } + + // 根据传入的id,删除动画事件列表中的对应事件 + public removeAnimate(id: string) { + this.animateEventList?.delete(id); + } + + // 更新参数 + public updateDom() { + this.camera.aspect = this.viewerDom.clientWidth / this.viewerDom.clientHeight; // 摄像机视锥体的长宽比,通常是使用画布的宽/画布的高 + this.camera.updateProjectionMatrix(); // 更新摄像机投影矩阵。在任何参数被改变以后必须被调用,来使得这些改变生效 + this.renderer.setSize(this.viewerDom.clientWidth, this.viewerDom.clientHeight); + this.renderer.setPixelRatio(window.devicePixelRatio); // 设置设备像素比 + } + + // 渲染dom + public renderDom() { + this.renderer?.render(this.scene as Scene, this.camera as Camera); + } +} diff --git a/src/pages/demo/template/index.less b/src/pages/demo/template/index.less new file mode 100644 index 0000000..2860d28 --- /dev/null +++ b/src/pages/demo/template/index.less @@ -0,0 +1,12 @@ +.threetemplate_container { + width: 100%; + height: 100vh; + position: relative; +} + +:global { + #TEMPLATE_CONTAINER { + width: 100%; + height: 100%; + } +} diff --git a/src/pages/demo/template/index.tsx b/src/pages/demo/template/index.tsx new file mode 100644 index 0000000..a6d72fc --- /dev/null +++ b/src/pages/demo/template/index.tsx @@ -0,0 +1,44 @@ +import React, { useEffect, useRef } from "react"; +import * as THREE from "three"; +import Viewer from "@/lib/three/Viewer"; +import BoxGeometry from "./modules/BoxGeometry"; +import styles from "./index.less"; + +interface OwnProps {} + +type Props = OwnProps; + +const PAGE_ID = "TEMPLATE_CONTAINER"; +const Template: React.FC = (props) => { + const viewerRef = useRef(); + let boxGeometry: BoxGeometry; + useEffect(() => { + init(); + return () => {}; + }, []); + const init = () => { + viewerRef.current = new Viewer(PAGE_ID); + const viewer = viewerRef.current; + const clock = new THREE.Clock(); + + viewer.addAxis(); + boxGeometry = new BoxGeometry(viewer); + // boxGeometry.addBoxGeometry(); + const fnOnj = { + fun: () => { + const elapsedTime = clock.getElapsedTime(); + boxGeometry.cube.rotation.y = elapsedTime * Math.PI; + }, + content: viewer + }; + viewer?.addAnimate("cubeCraze", fnOnj); + }; + + return ( +
+
+
+ ); +}; + +export default Template; diff --git a/src/pages/demo/template/modules/BoxGeometry/index.ts b/src/pages/demo/template/modules/BoxGeometry/index.ts new file mode 100644 index 0000000..f9a760f --- /dev/null +++ b/src/pages/demo/template/modules/BoxGeometry/index.ts @@ -0,0 +1,57 @@ +import type Viewer from "@/lib/three/Viewer"; +import * as THREE from "three"; +import { AmbientLight, DirectionalLight, Mesh } from "three"; + +// 添加立方体 +export default class BoxGeometry { + protected viewer: Viewer; + public cube!: Mesh; + public directionalLight!: DirectionalLight; + + constructor(viewer: Viewer) { + this.viewer = viewer; + this.addBoxGeometry(); + this.initLight(); + this.initPlane(); + } + + private initLight() { + const material = new THREE.MeshStandardMaterial({ color: 0xff0000 }); + // 添加环境光 + const ambientLight = new AmbientLight(0xffffff, 0.4); + this.viewer.scene.add(ambientLight); + + this.directionalLight = new THREE.DirectionalLight(0xffffff, 1); + this.directionalLight.position.set(10, 0, 10); + this.viewer.scene.add(this.directionalLight); + } + + private initPlane() { + this.viewer.renderer.shadowMap.enabled = true; // 1. 渲染器能够渲染阴影效果 + this.directionalLight.castShadow = true; // 2. 该方向会投射阴影效果 + this.cube.castShadow = true; + + const planeGeometry = new THREE.PlaneGeometry(20, 20); + const planeMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff }); + const planeMesh = new THREE.Mesh(planeGeometry, planeMaterial); + planeMesh.rotation.x = -0.5 * Math.PI; + planeMesh.position.set(0, -3, 0); + planeMesh.receiveShadow = true; + this.viewer.scene.add(planeMesh); + + const directionalLightHelper = new THREE.DirectionalLightHelper(this.directionalLight); + this.viewer.scene.add(directionalLightHelper); + } + + public addBoxGeometry() { + // 创建长宽高都为4的立方几何体; + // 立方体的表明颜色为红色的材质; + // 将集合体和材质组合为Mesh对象; + // 将Mesh对象添加到场景中。 + const geometry = new THREE.BoxGeometry(4, 4, 4); + const material = new THREE.MeshBasicMaterial({ color: 0xff0000 }); + this.cube = new THREE.Mesh(geometry, material); + + this.viewer.scene.add(this.cube); + } +} diff --git a/src/pages/demo/threeLogin/index.less b/src/pages/demo/threeLogin/index.less new file mode 100644 index 0000000..db07c7e --- /dev/null +++ b/src/pages/demo/threeLogin/index.less @@ -0,0 +1,24 @@ +.login_container { + width: 100%; + height: 100vh; + position: relative; + + .login_ground { + position: absolute; + z-index: 9998; + width: 100%; + height: 400px; + background-image: url("../../../../public/images/ground.png"); + background-repeat: no-repeat; + background-size: 100% 100%; + bottom: 0; + left: 0; + } +} + +:global { + #LOGIN_CONTAINER { + width: 100%; + height: 100%; + } +} diff --git a/src/pages/demo/threeLogin/index.tsx b/src/pages/demo/threeLogin/index.tsx new file mode 100644 index 0000000..fb01bab --- /dev/null +++ b/src/pages/demo/threeLogin/index.tsx @@ -0,0 +1,34 @@ +import React, { FC, useEffect, useRef } from "react"; +import Viewer from "./modules/Viewer"; +import Earth from "./modules/earth"; +import styles from "./index.less"; + +interface OwnProps {} + +type Props = OwnProps; + +const PAGE_ID = "LOGIN_CONTAINER"; +const ThreeJSIndex: FC = (props) => { + const viewerRef = useRef(); + useEffect(() => { + init(); + return () => viewerRef.current?.destroy(); + }, []); + // 加载 + const init = () => { + viewerRef.current = new Viewer(PAGE_ID); + const viewer = viewerRef.current; + viewer.addStats(); + viewer.initSphereModal(); + const earthLoader = new Earth(viewer); + }; + + return ( +
+
+
+
+ ); +}; + +export default ThreeJSIndex; diff --git a/src/pages/demo/threeLogin/modules/Viewer/index.ts b/src/pages/demo/threeLogin/modules/Viewer/index.ts new file mode 100644 index 0000000..a140fb3 --- /dev/null +++ b/src/pages/demo/threeLogin/modules/Viewer/index.ts @@ -0,0 +1,212 @@ +import * as THREE from "three"; +import { Camera, Mesh, PerspectiveCamera, Raycaster, Scene, SphereGeometry, SRGBColorSpace, Vector2, WebGLRenderer } from "three"; +import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; +import { CSS2DRenderer } from "three/examples/jsm/renderers/CSS2DRenderer.js"; +import Stats from "three/examples/jsm/libs/stats.module.js"; + +export type Animate = { + fun: (arg: any) => any; + content: any; +}; +export default class Viewer { + public id: string; + public depth: number; + public zAxisNumber!: number; + public viewerDom!: HTMLElement; + public renderer!: WebGLRenderer; + public scene!: Scene; + public camera!: PerspectiveCamera; + public controls!: OrbitControls; + public css2Renderer: CSS2DRenderer | undefined; + public raycaster!: Raycaster; + public mouse!: Vector2; + public isDestroy = false; // 是否销毁 + public animateEventList = new Map(); + public statsControls!: Stats; // 声明球体几何 + public sphereGeometry!: SphereGeometry; // 声明完整球 + public sphere!: Mesh; + public Sphere_Group!: THREE.Group; + + constructor(id: string) { + this.id = id; + this.depth = 1440; + this.initViewer(); + } + + private initViewer() { + this.initRenderer(); //创建渲染器 + this.initScene(); //创建场景 + this.initSceneBg(); //初始化背景 + this.initLight(); // 创建光源 + this.initCamera(); // 创建相机 + this.initControl(); // 创建控制器 + this.initCss2Renderer(); // 创建css2渲染器 + // + // this.raycaster = new Raycaster(); // 创建射线 + // this.mouse = new Vector2(); // 创建鼠标位置 + // + const animate = () => { + if (this.isDestroy) return; + requestAnimationFrame(animate); + this.renderSphereRotate(); //渲染星球的自转 + this.css2Renderer?.render(this.scene, this.camera); + this.updateDom(); + this.renderDom(); + + // 全局的公共动画函数,添加函数可同步执行 + this.animateEventList.forEach((event) => { + // console.log("animateEventList"); + if (event.fun && event.content) event.fun(event.content); + }); + }; + animate(); + } + + private initRenderer() { + // 获取画布dom + this.viewerDom = document.getElementById(this.id) as HTMLElement; + // 初始化渲染器 + this.renderer = new WebGLRenderer({ + logarithmicDepthBuffer: true, //true/false用于启用或禁用对数深度缓冲区。 + antialias: true, // true/false表示是否开启反锯齿 + alpha: true, // true/false 表示是否可以设置背景色透明 + precision: "mediump", // highp/mediump/lowp 表示着色精度选择 + premultipliedAlpha: true // true/false 表示是否可以设置像素深度(用来度量图像的分辨率) + // preserveDrawingBuffer: false, // true/false 表示是否保存绘图缓冲 + // physicallyCorrectLights: true, // true/false 表示是否开启物理光照 + }); + this.renderer.clearDepth(); //清除深度缓冲区。在渲染之前,这通常用于重置深度缓冲区,以确保正确的深度测试 + + // 开启模型对象的局部剪裁平面功能 + // 如果不设置为true,设置剪裁平面的模型不会被剪裁 + this.renderer.localClippingEnabled = true; + + this.renderer.shadowMap.enabled = true; + this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; + this.renderer.outputColorSpace = SRGBColorSpace; // 可以看到更亮的材质,同时这也影响到环境贴图。 + this.viewerDom.appendChild(this.renderer.domElement); + } + + private initScene() { + this.scene = new Scene(); + // 在场景中添加雾的效果,Fog参数分别代表‘雾的颜色’、‘开始雾化的视线距离’、刚好雾化至看不见的视线距离’ + this.scene.fog = new THREE.Fog(0x000000, 0, 10000); + } + + // 初始化背景(盒模型背景,视角在盒子里面,看到的是盒子内部) + private initSceneBg() { + new THREE.TextureLoader().load("/images/sky.png", (texture) => { + const geometry = new THREE.BoxGeometry(this.viewerDom.clientWidth, this.viewerDom.clientHeight, this.depth); // 创建一个球形几何体 SphereGeometry + const material = new THREE.MeshBasicMaterial({ map: texture, side: THREE.BackSide }); // 创建基础为网格基础材料 + const mesh = new THREE.Mesh(geometry, material); + this.scene.add(mesh); + }); + } + + private initLight() { + const ambientLight = new THREE.AmbientLight(0xffffff, 1); + // 右下角点光源 + const light_rightBottom = new THREE.PointLight(0x0655fd, 5, 0); + light_rightBottom.position.set(0, 100, -200); + this.scene.add(light_rightBottom); + this.scene.add(ambientLight); + } + + private initCamera() { + const distance = this.viewerDom.clientWidth / 2 / Math.tan(Math.PI / 12); + this.zAxisNumber = Math.floor(distance - this.depth / 2); + // 渲染相机 + this.camera = new PerspectiveCamera(15, this.viewerDom.clientWidth / this.viewerDom.clientHeight, 1, 30000); + this.camera.position.set(0, 0, this.zAxisNumber); + this.camera.lookAt(new THREE.Vector3(0, 0, 0)); + } + + private initControl() { + this.controls = new OrbitControls(this.camera as Camera, this.renderer?.domElement); + this.controls.enabled = false; // enabled设置为true是可以使用鼠标控制视角 + // this.controls.enableDamping = false; + // this.controls.screenSpacePanning = false; // 定义平移时如何平移相机的位置 控制不上下移动 + this.controls.minDistance = 2; + this.controls.maxDistance = 2000; + this.controls.addEventListener("change", () => { + this.renderer.render(this.scene, this.camera); + }); + } + + private initCss2Renderer() { + this.css2Renderer = new CSS2DRenderer(); + } + + public updateDom() { + this.controls.update(); + // // 更新参数 + this.camera.aspect = this.viewerDom.clientWidth / this.viewerDom.clientHeight; // 摄像机视锥体的长宽比,通常是使用画布的宽/画布的高 + this.camera.updateProjectionMatrix(); // 更新摄像机投影矩阵。在任何参数被改变以后必须被调用,来使得这些改变生效 + this.renderer.setSize(this.viewerDom.clientWidth, this.viewerDom.clientHeight); + this.renderer.setPixelRatio(window.devicePixelRatio); // 设置设备像素比 + } + + // 渲染dom + public renderDom() { + this.renderer?.render(this.scene as Scene, this.camera as Camera); + } + + private statsUpdate(statsControls: any) { + statsControls.update(); + } + + // 初始化球体模型 + public initSphereModal() { + //材质 + let material = new THREE.MeshPhongMaterial(); + material.map = new THREE.TextureLoader().load("/images/earth_bg.png"); + material.blendDstAlpha = 1; + //几何体 + this.sphereGeometry = new THREE.SphereGeometry(50, 64, 32); + //模型 + this.sphere = new THREE.Mesh(this.sphereGeometry, material); + this.Sphere_Group = new THREE.Group(); + this.Sphere_Group.add(this.sphere); + this.Sphere_Group.position.x = -400; + this.Sphere_Group.position.y = 200; + this.Sphere_Group.position.z = -200; + this.scene.add(this.Sphere_Group); + } + + // 渲染星球的自转 + private renderSphereRotate() { + if (this.sphere) { + this.Sphere_Group.rotateY(0.001); + } + } + + public addAnimate(id: string, animate: Animate) { + this.animateEventList.set(id, animate); + } + + public addStats() { + if (!this.statsControls) this.statsControls = new Stats(); + this.statsControls.dom.style.position = "absolute"; + this.viewerDom.appendChild(this.statsControls.dom); + + // 添加到动画 + this.addAnimate("stats", { fun: this.statsUpdate, content: this.statsControls }); + } + + public destroy() { + this.scene.traverse((child: any) => { + if (child.material) { + child.material.dispose(); + } + if (child.geometry) { + child.geometry.dispose(); + } + child = null; + }); + this.renderer.forceContextLoss(); + this.renderer.dispose(); + this.scene.clear(); + + this.isDestroy = true; + } +} diff --git a/src/pages/demo/threeLogin/modules/earth/index.ts b/src/pages/demo/threeLogin/modules/earth/index.ts new file mode 100644 index 0000000..90be647 --- /dev/null +++ b/src/pages/demo/threeLogin/modules/earth/index.ts @@ -0,0 +1,240 @@ +import type Viewer from "../Viewer"; +import { GUI } from "three/examples/jsm/libs/lil-gui.module.min"; +import * as THREE from "three"; + +class Params { + x: number = 0; + y: number = 0; + z: number = 100; + size: number = 3; + length: number = 10; + radius: number = 16; + visible: boolean = true; + widthSegments: number = 64; + heightSegments: number = 32; + color: string = "#000"; +} + +export default class Earth { + protected viewer: Viewer; + public particles_init_position!: number; + public zprogress!: number; // 声明点在z轴上移动的进度 + public zprogress_second!: number; // 声明同上(第二个几何点) + public particles_first!: any[]; // 声明粒子1 + public particles_second!: any[]; // 声明粒子1 + public parameters!: any; // 声明点的参数 + public materials!: any[]; // 声明点材质 + public cloudParameter_first!: any; // 声明流动的云对象1(包含路径、云实例) + public cloudParameter_second!: any; // 声明流动的云对象2(包含路径、云实例) + public renderCloudMove_first!: any; // 声明云流动的渲染函数1 + public renderCloudMove_second!: any; // 声明云流动的渲染函数1 + public gui!: any; // 声明调试工具 + + constructor(viewer: Viewer) { + this.viewer = viewer; + this.materials = []; + this.gui = new GUI(); + this.particles_init_position = -this.viewer.zAxisNumber - this.viewer.depth / 2; + this.zprogress = this.particles_init_position; + this.zprogress_second = this.particles_init_position * 2; + this.particles_first = this.initSceneStar(this.particles_init_position); + this.particles_second = this.initSceneStar(this.zprogress_second); + this.cloudParameter_first = this.initTubeRoute( + [ + new THREE.Vector3(-this.viewer.viewerDom.clientWidth / 10, 0, -this.viewer.depth / 2), + new THREE.Vector3(-this.viewer.viewerDom.clientWidth / 4, this.viewer.viewerDom.clientHeight / 8, 0), + new THREE.Vector3(-this.viewer.viewerDom.clientWidth / 4, 0, this.viewer.zAxisNumber) + ], + 400, + 200 + ); + this.cloudParameter_second = this.initTubeRoute( + [ + new THREE.Vector3(this.viewer.viewerDom.clientWidth / 8, this.viewer.viewerDom.clientHeight / 8, -this.viewer.depth / 2), + new THREE.Vector3(this.viewer.viewerDom.clientWidth / 8, this.viewer.viewerDom.clientHeight / 8, this.viewer.zAxisNumber) + ], + 200, + 100 + ); + this.renderCloudMove_first = this.initCloudMove(this.cloudParameter_first, 0.0002); + this.renderCloudMove_second = this.initCloudMove(this.cloudParameter_second, 0.0008, 0.001); + this.initGUI(); + + const animate = () => { + if (this.viewer.isDestroy) return; + requestAnimationFrame(animate); + this.renderStarMove(); + this.renderCloudMove_first(); + this.renderCloudMove_second(); + this.viewer.updateDom(); + this.viewer.renderDom(); + }; + animate(); + } + + // 初始化gui + public initGUI = () => { + const params = new Params(); + this.gui.add(params, "x", -1500, 1500).onChange((x: number) => { + //点击颜色面板,e为返回的10进制颜色 + this.viewer.Sphere_Group.position.x = x; + }); + this.gui.add(params, "y", -50, 1500).onChange((y: number) => { + //点击颜色面板,e为返回的10进制颜色 + this.viewer.Sphere_Group.position.y = y; + }); + this.gui.add(params, "z", -200, 1000).onChange((z: number) => { + //点击颜色面板,e为返回的10进制颜色 + this.viewer.Sphere_Group.position.z = z; + }); + // this.gui.add(params, "widthSegments", 0, 64).onChange((widthSegments: number) => { + // //点击颜色面板,e为返回的10进制颜色 + // // this.viewer.sphereGeometry.parameters.widthSegments = widthSegments; + // }); + // this.gui.add(params, "heightSegments", 0, 32).onChange((heightSegments: number) => { + // //点击颜色面板,e为返回的10进制颜色 + // // this.viewer.sphereGeometry.parameters.heightSegments = heightSegments; + // }); + // this.gui.add(params, "radius", 5, 30).onChange((radius: number) => { + // //点击颜色面板,e为返回的10进制颜色 + // // this.viewer.sphereGeometry.parameters.radius = radius; + // this.viewer.renderer.render(this.viewer.scene, this.viewer.camera); + // }); + // this.gui.add(params, "visible").onChange((e: any) => { + // //这是一个单选框,因为params.visible是一个布尔值,e返回所选布尔值 + // // points.visible = e + // }); + // this.gui.addColor(params, "color").onChange((e: any) => { + // //点击颜色面板,e为返回的10进制颜色 + // // pointsMaterial.color.set(e) + // }); + }; + + private initSceneStar(initZposition: number) { + const geometry = new THREE.BufferGeometry(); + const vertices: number[] = []; + const pointsGeometry: any[] = []; + const textureLoader = new THREE.TextureLoader(); + const sprite1 = textureLoader.load("/images/starflake1.png"); + const sprite2 = textureLoader.load("/images/starflake2.png"); + this.parameters = [ + [[0.6, 100, 0.75], sprite1, 50], + [[0, 0, 1], sprite2, 20] + ]; + // 初始化500个节点 + for (let i = 0; i < 500; i++) { + /** + * const x: number = Math.random() * 2 * width - width + * 等价 + * THREE.MathUtils.randFloatSpread(width) + */ + const x: number = THREE.MathUtils.randFloatSpread(this.viewer.viewerDom.clientWidth); + const y: number = _.random(0, this.viewer.viewerDom.clientHeight / 2); + const z: number = _.random(-this.viewer.depth / 2, this.viewer.zAxisNumber); + vertices.push(x, y, z); + } + + geometry.setAttribute("position", new THREE.Float32BufferAttribute(vertices, 3)); + // 创建2种不同的材质的节点(500 * 2) + for (let i = 0; i < this.parameters.length; i++) { + const color = this.parameters[i][0]; + const sprite = this.parameters[i][1]; + const size = this.parameters[i][2]; + + this.materials[i] = new THREE.PointsMaterial({ + size, + map: sprite, + blending: THREE.AdditiveBlending, + depthTest: true, + transparent: true + }); + this.materials[i].color.setHSL(color[0], color[1], color[2]); + const particles = new THREE.Points(geometry, this.materials[i]); + particles.rotation.x = Math.random() * 0.2 - 0.15; + particles.rotation.z = Math.random() * 0.2 - 0.15; + particles.rotation.y = Math.random() * 0.2 - 0.15; + particles.position.setZ(initZposition); + pointsGeometry.push(particles); + this.viewer.scene.add(particles); + } + return pointsGeometry; + } + + // 渲染星星的运动 + public renderStarMove() { + const time = Date.now() * 0.00005; + this.zprogress += 1; + this.zprogress_second += 1; + + if (this.zprogress >= this.viewer.zAxisNumber + this.viewer.depth / 2) { + this.zprogress = this.particles_init_position; + } else { + this.particles_first.forEach((item) => { + item.position.setZ(this.zprogress); + }); + } + if (this.zprogress_second >= this.viewer.zAxisNumber + this.viewer.depth / 2) { + this.zprogress_second = this.particles_init_position; + } else { + this.particles_second.forEach((item) => { + item.position.setZ(this.zprogress_second); + }); + } + + for (let i = 0; i < this.materials.length; i++) { + const color = this.parameters[i][0]; + + const h = ((360 * (color[0] + time)) % 360) / 360; + this.materials[i].color.setHSL(color[0], color[1], parseFloat(h.toFixed(2))); + } + } + + // 初始化云的运动函数 + public initCloudMove = (cloudParameter: any, speed: number, scaleSpeed = 0.0006, maxScale = 1, startScale = 0) => { + let cloudProgress = 0; + return () => { + if (startScale < maxScale) { + startScale += scaleSpeed; + cloudParameter.cloud.scale.setScalar(startScale); + } + if (cloudProgress > 1) { + cloudProgress = 0; + startScale = 0; + } else { + cloudProgress += speed; + if (cloudParameter.curve) { + const point = cloudParameter.curve.getPoint(cloudProgress); + if (point && point.x) { + cloudParameter.cloud.position.set(point.x, point.y, point.z); + } + } + } + }; + }; + + // 初始化流动路径 + private initTubeRoute(route?: any, geometryWidth?: number, geometryHeigh?: number) { + const curve = new THREE.CatmullRomCurve3(route, false); + const tubeGeometry = new THREE.TubeGeometry(curve, 100, 2, 50, false); + const tubeMaterial = new THREE.MeshBasicMaterial({ + // color: '0x4488ff', + opacity: 0, + transparent: true + }); + const tube = new THREE.Mesh(tubeGeometry, tubeMaterial); + this.viewer.scene.add(tube); + + const clondGeometry = new THREE.PlaneGeometry(geometryWidth, geometryHeigh); + const textureLoader = new THREE.TextureLoader(); + const cloudTexture = textureLoader.load("/images/cloud.png"); + const clondMaterial = new THREE.MeshBasicMaterial({ + map: cloudTexture, + blending: THREE.AdditiveBlending, + depthTest: false, + transparent: true + }); + const cloud = new THREE.Mesh(clondGeometry, clondMaterial); + this.viewer.scene.add(cloud); + return { cloud, curve }; + } +} diff --git a/src/pages/demo/threejs/index.less b/src/pages/demo/threejs/index.less new file mode 100644 index 0000000..c57bbc5 --- /dev/null +++ b/src/pages/demo/threejs/index.less @@ -0,0 +1,12 @@ +.threejs_container { + width: 100%; + height: 100vh; + position: relative; +} + +:global { + #DEMO_CONTAINER { + width: 100%; + height: 100%; + } +} diff --git a/src/pages/demo/threejs/index.tsx b/src/pages/demo/threejs/index.tsx new file mode 100644 index 0000000..31e6384 --- /dev/null +++ b/src/pages/demo/threejs/index.tsx @@ -0,0 +1,135 @@ +import React, { FC, useEffect, useRef, useState } from "react"; +import { observer } from "mobx-react"; +import { useMobStore } from "@/hooks"; +import * as THREE from "three"; +import Viewer from "./modules/Viewer"; +import ModelLoader from "./modules/ModelLoder"; +import BoxHelperWrap from "./modules/BoxHelperWrap"; +import { checkNameIncludes } from "@/utils/three"; +import Floors from "./modules/Floors"; +import { Object3DExtends } from "@/types"; +import styles from "./index.less"; + +interface OwnProps {} + +type Props = OwnProps; + +const PAGE_ID = "DEMO_CONTAINER"; +const ThreeJSIndex: FC = observer((props) => { + const store = useMobStore("threeStore"); + const [rackList, setRackList] = useState([]); //架子 + const [chairList, setChairList] = useState([]); //椅子 + const viewerRef = useRef(); + let modelLoader: ModelLoader; + let boxHelperWrap: BoxHelperWrap; + useEffect(() => { + init(); + initModel(); + return () => viewerRef.current?.destroy(); + }, []); + /** 需要监听rackInfoList更新监听点击事件的函数 */ + useEffect(() => { + if (!viewerRef.current) return; + if (_.isEmpty(store?.raycasterObjects)) return; + const viewer = viewerRef.current; + onMouseClick(store?.raycasterObjects); + }, [store?.raycasterObjects, viewerRef, viewerRef.current?.scene]); + // 加载 + const init = () => { + viewerRef.current = new Viewer(PAGE_ID); + const viewer = viewerRef.current; + viewer.addAxis(); + viewer.addStats(); + viewer.initRaycaster(); + + modelLoader = new ModelLoader(viewer); + const floors = new Floors(viewer); + floors.addGird(8, 25, 0x004444, 0x004444); + // boxHelperWrap = new BoxHelperWrap(viewer); + }; + // 加载模型 + const initModel = () => { + modelLoader.loadModelToScene("/models/datacenter.glb", (baseModel) => { + // 设置基础模型的缩放比例 + baseModel.setScalc(0.15); + // 暂时注释掉旋转代码 + // baseModel.object.rotation.y = Math.PI / 2; + // 获取实际的模型对象 + const model = baseModel.gltf.scene; + model.position.set(0, 0, 0.3); + // 为模型设置名称 + model.name = "机房"; + model.uuid = "机房"; + // 启用基础模型的投射阴影功能 + baseModel.openCastShadow(); + let rackList: Object3DExtends[] = []; // 机架列表 + let allList: Object3DExtends[] = []; // 所有的物体 + let chairList: Object3DExtends[] = []; // 椅子列表 + model.traverse((item) => { + if (checkIsRack(item)) { + rackList.push(item); + } + if (checkIsChair(item)) { + chairList.push(item); + } + allList.push(item); + if (item instanceof THREE.Mesh) { + // 保存原始颜色数据,以及警告颜色 + if (item.isMesh) { + item.material.warningColor = { r: 1, g: 0, b: 0, isColor: true }; + // 保存旧的材质 + (item as Object3DExtends).oldMaterial = item.material; + } + } + }); + // dispatchDeviceListData({ type: "INIT", initData: rackList }); + setRackList(rackList); + setChairList(chairList); + const viewer = viewerRef.current; + // 将 rackList 中的机架设置为 viewer 的射线检测对象 + viewer?.setRaycasterObjects([...allList]); + }); + }; + const checkIsRack = (obj: THREE.Object3D): boolean => { + return checkNameIncludes(obj, "rack"); + }; + const checkIsChair = (obj: THREE.Object3D): boolean => { + return checkNameIncludes(obj, "chair"); + }; + const onClickChair = (selectedObject: THREE.Object3D) => { + if (!checkNameIncludes(selectedObject, "chair")) return; + const viewer = viewerRef.current; + + viewer?.addCameraTween(new THREE.Vector3(0.05, 0.66, -2.54), 1000, () => { + console.log("动画完成"); + console.log(viewer?.scene?.children); + const sceneModels = viewer?.scene?.children; + // const model = sceneModels?.find((model) => { + // return model.name === "机房"; + // })!; + // const floorModel = sceneModels?.find((model) => { + // return model.name === "机房"; + // }) + viewer?.setRaycasterObjects([]); // + sceneModels?.forEach((model) => { + viewer?.scene.remove(model); + }); + + // createRoom("room", new THREE.Vector3(0, 0, 0)); + }); + }; + const onMouseClick = (intersects: THREE.Intersection[]) => { + if (!intersects.length) return; + const selectedObject = intersects?.[0].object || {}; + // onClickRack(selectedObject); + onClickChair(selectedObject); + }; + + return ( +
+
+
+ ); +}); + +export default ThreeJSIndex; diff --git a/src/pages/demo/threejs/modules/BaseModel/index.ts b/src/pages/demo/threejs/modules/BaseModel/index.ts new file mode 100644 index 0000000..7428e88 --- /dev/null +++ b/src/pages/demo/threejs/modules/BaseModel/index.ts @@ -0,0 +1,48 @@ +import type { Material } from "three"; +import * as THREE from "three"; +import type Viewer from "../Viewer"; +import type { GLTF } from "three/examples/jsm/loaders/GLTFLoader.js"; +import { Object3DExtends } from "@/types"; + +export default class BaseModel { + protected viewer: Viewer; + public gltf: GLTF; // 模型 + public object: THREE.Group; + /**模型原始材质 */ + public originMaterials: Material[] = []; + public isSaveMaterial = false; + public animaIndex = -1; + public mixer!: THREE.AnimationMixer; + public clock: THREE.Clock; + + constructor(gltf: GLTF, viewer: Viewer) { + this.gltf = gltf; + this.viewer = viewer; + this.object = gltf.scene || gltf; + + this.clock = new THREE.Clock(); // 时钟 + } + + /** + * 设置模型比例 + * @param x 可以只填写一个参数 + * @param y 纵轴缩放 + * @param z 横轴缩放 + */ + public setScalc(x: number, y?: number, z?: number) { + this.object.scale.set(x, y || x, z || x); + } + + /** + * 开启模型阴影 数组中移除阴影 + */ + public openCastShadow(names = []) { + this.gltf.scene.traverse((model: Object3DExtends) => { + if (model.isMesh && !names.includes(model.name as never)) { + //它会在渲染对象之前检查每一帧对象是否位于相机的视锥体中。 如果设置为 false,则即使对象不在相机的视锥体中,也会在每一帧进行渲染。 + model.frustumCulled = false; + model.castShadow = true; //对象是否渲染成阴影贴图。 + } + }); + } +} diff --git a/src/pages/demo/threejs/modules/BoxHelperWrap/index.ts b/src/pages/demo/threejs/modules/BoxHelperWrap/index.ts new file mode 100644 index 0000000..3d8dbb0 --- /dev/null +++ b/src/pages/demo/threejs/modules/BoxHelperWrap/index.ts @@ -0,0 +1,20 @@ +import { BoxHelper, Color, Object3D } from "three"; +import type Viewer from "../Viewer"; +// 通过 BoxHelper 可以实现简单的鼠标选中的特效。 +// 也可以通过 OutlinePass 实现发光的特效。 +export default class BoxHelperWrap { + protected viewer: Viewer; + public boxHelper: BoxHelper; + + constructor(viewer: Viewer, color?: number) { + this.viewer = viewer; + const boxColor = color === undefined ? 0x00ffff : color; + this.boxHelper = new BoxHelper(new Object3D(), new Color(boxColor)); + + this.initBoxHelperWrap(); // 初始化 + } + + private initBoxHelperWrap() { + this.viewer.scene.add(this.boxHelper); + } +} diff --git a/src/pages/demo/threejs/modules/Floors/index.ts b/src/pages/demo/threejs/modules/Floors/index.ts new file mode 100644 index 0000000..0f5ec1a --- /dev/null +++ b/src/pages/demo/threejs/modules/Floors/index.ts @@ -0,0 +1,26 @@ +import * as THREE from "three"; +import type Viewer from "../Viewer"; + +export default class Floors { + protected viewer: Viewer; + public planeWidth = 1000; + public planeHeight = 1000; + + constructor(viewer: Viewer) { + this.viewer = viewer; + this.initFlooer(); + } + + private initFlooer() { + const ground = new THREE.Mesh(new THREE.PlaneGeometry(this.planeWidth, this.planeHeight), new THREE.MeshPhongMaterial({ color: 0xbbbbbb, depthWrite: false })); + ground.rotation.x = -Math.PI / 2; + ground.receiveShadow = true; + this.viewer.scene.add(ground); + } + + /**网格辅助线 */ + public addGird(size = 1000, divisions = 20, colorCenterLine = 0x888888, colorGrid = 0x888888) { + const grid = new THREE.GridHelper(size, divisions, colorCenterLine, colorGrid); + this.viewer.scene.add(grid); + } +} diff --git a/src/pages/demo/threejs/modules/ModelLoder/index.ts b/src/pages/demo/threejs/modules/ModelLoder/index.ts new file mode 100644 index 0000000..013d259 --- /dev/null +++ b/src/pages/demo/threejs/modules/ModelLoder/index.ts @@ -0,0 +1,38 @@ +import type Viewer from "../Viewer"; +import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; +import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js"; +import BaseModel from "../BaseModel"; + +type LoadModelCallbackFn = (arg: T) => any; + +/**模型加载器 */ +export default class ModelLoder { + protected viewer: Viewer; + private gltfLoader: GLTFLoader; + private readonly dracoLoader: DRACOLoader; + + constructor(viewer: Viewer, dracolPath = "/draco/") { + this.viewer = viewer; + this.gltfLoader = new GLTFLoader(); + this.dracoLoader = new DRACOLoader(); + // 提供一个DracLoader实例来解码压缩网格数据 + // 没有这个会报错 dracolPath 默认放在public文件夹当中 + this.dracoLoader.setDecoderPath(dracolPath); + this.gltfLoader.setDRACOLoader(this.dracoLoader); + } + + private loadModel(url: string, callback: LoadModelCallbackFn) { + this.gltfLoader.load(url, (gltf) => { + const baseModel = new BaseModel(gltf, this.viewer); + callback && callback(baseModel); + }); + } + + /**模型加载到场景 */ + public loadModelToScene(url: string, callback: LoadModelCallbackFn) { + this.loadModel(url, (model) => { + this.viewer.scene.add(model.object); + callback && callback(model); + }); + } +} diff --git a/src/pages/demo/threejs/modules/Viewer/Events.ts b/src/pages/demo/threejs/modules/Viewer/Events.ts new file mode 100644 index 0000000..619fa5d --- /dev/null +++ b/src/pages/demo/threejs/modules/Viewer/Events.ts @@ -0,0 +1,20 @@ +export default { + animate: "animate", + dispose: "dispose", + orbitChange: "orbitChange", + load: { + start: "load:start", + processing: "load:processing", + finish: "load:finish", + }, + click: { + raycaster: "click:raycaster", + }, + dblclick: { + raycaster: "dblclick:raycaster", + }, + mousemove: { + raycaster: "mousemove:raycaster", + }, + resize: "resize", +}; diff --git a/src/pages/demo/threejs/modules/Viewer/index.ts b/src/pages/demo/threejs/modules/Viewer/index.ts new file mode 100644 index 0000000..e7bb875 --- /dev/null +++ b/src/pages/demo/threejs/modules/Viewer/index.ts @@ -0,0 +1,247 @@ +import * as THREE from "three"; +import { AmbientLight, AxesHelper, Camera, PerspectiveCamera, Raycaster, Scene, SRGBColorSpace, Vector2, WebGLRenderer } from "three"; +import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; +import { CSS2DRenderer } from "three/examples/jsm/renderers/CSS2DRenderer.js"; +import { Tween } from "three/examples/jsm/libs/tween.module.js"; +import Stats from "three/examples/jsm/libs/stats.module.js"; +import Events from "./Events"; +import { threeStore } from "@/store"; + +export type Animate = { + fun: (arg: any) => any; + content: any; +}; +export default class Viewer { + public id: string; + public viewerDom!: HTMLElement; + public renderer!: WebGLRenderer; + public scene!: Scene; + public camera!: PerspectiveCamera; + public controls!: OrbitControls; + public css2Renderer: CSS2DRenderer | undefined; + public raycaster!: Raycaster; + public mouse!: Vector2; + public isDestroy = false; // 是否销毁 + public animateEventList = new Map(); + public statsControls!: Stats; // 性能监测 + public raycasterObjects: THREE.Object3D[] = []; + public tween!: Tween; + public mouseEvent: MouseEvent | undefined; + + constructor(id: string) { + this.id = id; + this.initViewer(); + } + + private initViewer() { + this.initRenderer(); //创建渲染器 + this.initScene(); //创建场景 + this.initLight(); // 创建光源 + this.initCamera(); // 创建相机 + this.initControl(); // 创建控制器 + // this.initCss2Renderer(); // 创建css2渲染器 + + this.raycaster = new Raycaster(); // 创建射线 + this.mouse = new Vector2(); // 创建鼠标位置 + + const animate = () => { + if (this.isDestroy) return; + requestAnimationFrame(animate); + // TWEEN.update(); // 更新动画 + // this.css2Renderer?.render(this.scene, this.camera); + this.updateDom(); + this.renderDom(); + + // 全局的公共动画函数,添加函数可同步执行 + this.animateEventList.forEach((event) => { + // console.log("animateEventList"); + if (event.fun && event.content) event.fun(event.content); + }); + }; + animate(); + } + + private initRenderer() { + // 获取画布dom + this.viewerDom = document.getElementById(this.id) as HTMLElement; + // 初始化渲染器 + this.renderer = new WebGLRenderer({ + logarithmicDepthBuffer: true, //true/false用于启用或禁用对数深度缓冲区。 + antialias: true, // true/false表示是否开启反锯齿 + alpha: true, // true/false 表示是否可以设置背景色透明 + precision: "mediump", // highp/mediump/lowp 表示着色精度选择 + premultipliedAlpha: true // true/false 表示是否可以设置像素深度(用来度量图像的分辨率) + // preserveDrawingBuffer: false, // true/false 表示是否保存绘图缓冲 + // physicallyCorrectLights: true, // true/false 表示是否开启物理光照 + }); + this.renderer.clearDepth(); //清除深度缓冲区。在渲染之前,这通常用于重置深度缓冲区,以确保正确的深度测试 + + // 开启模型对象的局部剪裁平面功能 + // 如果不设置为true,设置剪裁平面的模型不会被剪裁 + this.renderer.localClippingEnabled = true; + + this.renderer.shadowMap.enabled = true; + this.renderer.outputColorSpace = SRGBColorSpace; // 可以看到更亮的材质,同时这也影响到环境贴图。 + this.viewerDom.appendChild(this.renderer.domElement); + } + + private initScene() { + this.scene = new Scene(); + } + + private initLight() { + const ambient = new AmbientLight(0xffffff, 0.6); + this.scene.add(ambient); + + const light = new THREE.DirectionalLight(0xffffff); + light.position.set(0, 200, 100); + light.castShadow = true; + + light.shadow.camera.top = 180; + light.shadow.camera.bottom = -100; + light.shadow.camera.left = -120; + light.shadow.camera.right = 400; + light.shadow.camera.near = 0.1; + light.shadow.camera.far = 400; + // 设置mapSize属性可以使阴影更清晰,不那么模糊 + light.shadow.mapSize.set(1024, 1024); + light.name = "initLight"; + + this.scene.add(light); + } + + private initCamera() { + // 渲染相机 + this.camera = new PerspectiveCamera(25, this.viewerDom.clientWidth / this.viewerDom.clientHeight, 1, 2000); + //设置相机位置 + // this.camera.position.set(5, 1, -5); + this.camera.position.set(4, 2, -3); + //设置相机方向 + this.camera.lookAt(0, 0, 0); + } + + private initControl() { + this.controls = new OrbitControls(this.camera as Camera, this.renderer?.domElement); + this.controls.enabled = true; // 控制器关 + this.controls.enableDamping = false; + this.controls.screenSpacePanning = false; // 定义平移时如何平移相机的位置 控制不上下移动 + this.controls.minDistance = 2; + this.controls.maxDistance = 1000; + this.controls.addEventListener("change", () => { + this.renderer.render(this.scene, this.camera); + }); + } + + private initCss2Renderer() { + this.css2Renderer = new CSS2DRenderer(); + } + + private updateDom() { + this.controls.update(); + // // 更新参数 + this.camera.aspect = this.viewerDom.clientWidth / this.viewerDom.clientHeight; // 摄像机视锥体的长宽比,通常是使用画布的宽/画布的高 + this.camera.updateProjectionMatrix(); // 更新摄像机投影矩阵。在任何参数被改变以后必须被调用,来使得这些改变生效 + this.renderer.setSize(this.viewerDom.clientWidth, this.viewerDom.clientHeight); + this.renderer.setPixelRatio(window.devicePixelRatio); // 设置设备像素比 + } + + // 渲染dom + private renderDom() { + this.renderer?.render(this.scene as Scene, this.camera as Camera); + } + + private statsUpdate(statsControls: any) { + statsControls.update(); + } + + /**坐标轴辅助 */ + public addAxis() { + const axis = new AxesHelper(1000); //创建坐标轴辅助对象 + this.scene?.add(axis); + } + + public addAnimate(id: string, animate: Animate) { + this.animateEventList.set(id, animate); + } + + public addStats() { + if (!this.statsControls) this.statsControls = new Stats(); + this.statsControls.dom.style.position = "absolute"; + this.viewerDom.appendChild(this.statsControls.dom); + + // 添加到动画 + this.addAnimate("stats", { fun: this.statsUpdate, content: this.statsControls }); + } + + /**自定义鼠标事件触发的范围,给定一个模型组,对给定的模型组鼠标事件才生效 */ + public setRaycasterObjects(objList: THREE.Object3D[]): void { + this.raycasterObjects = objList; + } + + /**注册鼠标事件监听 */ + public initRaycaster() { + this.raycaster = new Raycaster(); + + const initRaycasterEvent: Function = (eventName: keyof HTMLElementEventMap): void => { + //这里的container就是画布所在的div,也就是说,这个是要拿整个scene所在的容器来界定的 + let getBoundingClientRect = this.viewerDom.getBoundingClientRect(); + let offsetWidth = this.viewerDom.offsetWidth; + let offsetHeight = this.viewerDom.offsetHeight; + const funWrap = _.throttle((event: any) => { + this.mouseEvent = { + ...event, //真正的鼠标相对于画布的位置 + x: event.clientX - getBoundingClientRect.left, + y: event.clientY - getBoundingClientRect.top + }; + this.mouse.x = ((event.clientX - getBoundingClientRect.left) / offsetWidth) * 2 - 1; + this.mouse.y = -((event.clientY - getBoundingClientRect.top) / offsetHeight) * 2 + 1; + threeStore.setRaycasterIntersectObjects(this.getRaycasterIntersectObjects()); + }, 100); + this.viewerDom.addEventListener(eventName, funWrap, false); + }; + + initRaycasterEvent("click"); + // initRaycasterEvent("dblclick"); + // initRaycasterEvent("mousemove"); + } + + // 获取射线与物体相交的物体 + private getRaycasterIntersectObjects(): THREE.Intersection[] { + // 如果射线与物体相交的数组为空,则返回空数组 + if (!this.raycasterObjects.length) return []; + // 设置射线从相机发出,经过鼠标位置 + this.raycaster.setFromCamera(this.mouse, this.camera); + // 返回射线与物体相交的数组 + return this.raycaster.intersectObjects(this.raycasterObjects, true); + } + + public addCameraTween(targetPosition = new THREE.Vector3(1, 1, 1), duration = 1000, onComplete: () => void) { + this.initCameraTween(); + this.tween.to(targetPosition, duration).start().onComplete(onComplete); + } + + /** + * 初始化补间动画库tween + */ + public initCameraTween() { + if (!this.camera) return; + this.tween = new Tween(this.camera.position); + } + + public destroy() { + this.scene.traverse((child: any) => { + if (child.material) { + child.material.dispose(); + } + if (child.geometry) { + child.geometry.dispose(); + } + child = null; + }); + this.renderer.forceContextLoss(); + this.renderer.dispose(); + this.scene.clear(); + + this.isDestroy = true; + } +} diff --git a/src/store/index.js b/src/store/index.js index 377d856..e305057 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -2,12 +2,14 @@ import { appStore } from "./AppStore"; import { calculateStore } from "./CalculateStore"; import { baseLayoutStore } from "@/layouts/BaseLayout/Store"; import { hiprintStore } from "@/store/HiprintStore"; +import { threeStore } from "./threeStore"; -export { baseLayoutStore, appStore, calculateStore, hiprintStore }; +export { baseLayoutStore, appStore, calculateStore, hiprintStore, threeStore }; export default { baseLayoutStore, calculateStore, appStore, - hiprintStore + hiprintStore, + threeStore }; diff --git a/src/store/threeStore.ts b/src/store/threeStore.ts new file mode 100644 index 0000000..4892eea --- /dev/null +++ b/src/store/threeStore.ts @@ -0,0 +1,13 @@ +import { action, makeObservable, observable } from "mobx"; +import { Intersection } from "three"; + +export class ThreeStore { + constructor() { + makeObservable(this); + } + + @observable raycasterObjects: Intersection[] = []; + @action setRaycasterIntersectObjects = (objects: Intersection[]) => (this.raycasterObjects = objects); +} + +export const threeStore = new ThreeStore(); diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..0a256c3 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,18 @@ +import type { Object3D, Material, Color } from "three"; +export interface MaterialExtends extends Material { + color?: Color; + warningColor?: Color; +} +export type ModelExtendsData = { + warn?: boolean; + name: string; + [key: string]: any; +}; +export interface Object3DExtends extends Object3D { + isGroup?: boolean; + isMesh?: boolean; + material?: MaterialExtends; + oldMaterial?: MaterialExtends; + addData?: ModelExtendsData; + dom?: HTMLElement; +} diff --git a/src/utils/three.ts b/src/utils/three.ts new file mode 100644 index 0000000..d1ccc4c --- /dev/null +++ b/src/utils/three.ts @@ -0,0 +1,5 @@ +import type { Object3D } from "three"; + +export function checkNameIncludes(obj: Object3D, str: string): boolean { + return obj.name.includes(str); +}