release/2.19.1.2503.01-业务线个税

feature/threejs
lys 3 months ago
parent ed8f9a056b
commit 15446c6a6b

@ -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",

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 B

Binary file not shown.

@ -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);
}
}

@ -0,0 +1,12 @@
.threetemplate_container {
width: 100%;
height: 100vh;
position: relative;
}
:global {
#TEMPLATE_CONTAINER {
width: 100%;
height: 100%;
}
}

@ -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> = (props) => {
const viewerRef = useRef<Viewer>();
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 (
<div className={styles.threetemplate_container}>
<div id={PAGE_ID} />
</div>
);
};
export default Template;

@ -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);
}
}

@ -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%;
}
}

@ -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> = (props) => {
const viewerRef = useRef<Viewer>();
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 (
<div className={styles.login_container}>
<div id={PAGE_ID} />
<div className={styles.login_ground} />
</div>
);
};
export default ThreeJSIndex;

@ -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;
}
}

@ -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 };
}
}

@ -0,0 +1,12 @@
.threejs_container {
width: 100%;
height: 100vh;
position: relative;
}
:global {
#DEMO_CONTAINER {
width: 100%;
height: 100%;
}
}

@ -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<Props> = observer((props) => {
const store = useMobStore("threeStore");
const [rackList, setRackList] = useState<Object3DExtends[]>([]); //架子
const [chairList, setChairList] = useState<Object3DExtends[]>([]); //椅子
const viewerRef = useRef<Viewer>();
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 (
<div className={styles.threejs_container}>
<div id={PAGE_ID} />
</div>
);
});
export default ThreeJSIndex;

@ -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; //对象是否渲染成阴影贴图。
}
});
}
}

@ -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);
}
}

@ -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);
}
}

@ -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<T = any> = (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<BaseModel>) {
this.gltfLoader.load(url, (gltf) => {
const baseModel = new BaseModel(gltf, this.viewer);
callback && callback(baseModel);
});
}
/**模型加载到场景 */
public loadModelToScene(url: string, callback: LoadModelCallbackFn<BaseModel>) {
this.loadModel(url, (model) => {
this.viewer.scene.add(model.object);
callback && callback(model);
});
}
}

@ -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",
};

@ -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<THREE.Vector3>;
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;
}
}

@ -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
};

@ -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();

@ -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;
}

@ -0,0 +1,5 @@
import type { Object3D } from "three";
export function checkNameIncludes(obj: Object3D, str: string): boolean {
return obj.name.includes(str);
}
Loading…
Cancel
Save