Three.js 搭建3D隧道监测
Three.js 搭建3D隧道监测
- Three.js 基础元素
- 场景scene
- 相机carema
- 网络模型Mesh
- 光源light
- 渲染器renderer
- 控制器controls
- 实现3d隧道监测基础
- 实现道路
- 实现隧道
- 实现多个摄像头
- 点击模型进行属性操作
- 实现点击模型发光效果
- 性能监视器stats
- 引入
- 使用
- 总结
- 完整代码
我们将通过three.js技术打造3d隧道监测可视化项目,隧道监测项目将涵盖照明,风机的运行情况,控制车道指示灯关闭,情报板、火灾报警告警、消防安全、车行横洞、风向仪、隧道紧急逃生出口的控制以及事故模拟等!那先来看看我们的初步成果!因为作者也是在边学习边做的情况,效果有些丑陋,希望不要见笑!!!three.js基础知识还是基本涵盖了,入门还是很有参考价值的!
Three.js 基础元素
我们将通过一个基本的three.js模板代码更好的概况我们的基础元素
import React, { useEffect } from 'react';
import * as THREE from 'three';
// eslint-disable-next-line import/extensions
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';export default function ThreeVisual() {// 场景let scene;// 相机let camera;// 控制器let controls;// 网络模型let mesh;// 渲染器let renderer;// debugger属性const debugObject = {light: {amlight: {color: 0xffffff,},directionalLight: {color: 0xffffff,position: {x: 0,y: 400,z: 1800,},},pointLight: {color: 0xff0000,position: {x: 0,y: 400,z: 1800,},},},};const sizes = {width: window.innerWidth,height: window.innerHeight,};useEffect(() => {// eslint-disable-next-line no-use-before-definethreeStart();}, []);const initThree = () => {const width = document.getElementById('threeMain').clientWidth;const height = document.getElementById('threeMain').clientHeight;renderer = new THREE.WebGLRenderer({antialias: true,logarithmicDepthBuffer: true,});renderer.shadowMap.enabled = true;renderer.setSize(width, height);document.getElementById('threeMain').appendChild(renderer.domElement);};const initCamera = (width, height) => {camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 10000);camera.position.x = 0;camera.position.y = 500;camera.position.z = 1300;camera.up.x = 0;camera.up.y = 1;camera.up.z = 0;camera.lookAt({x: 0,y: 0,z: 0,});// 创建相机视锥体辅助对象// const cameraPerspectiveHelper = new THREE.CameraHelper(camera);// scene.add(cameraPerspectiveHelper);};const initScene = () => {scene = new THREE.Scene();scene.background = new THREE.Color(0xbfd1e5);};const initLight = () => {// 环境光const amlight = new THREE.AmbientLight(debugObject.light.amlight.color);amlight.position.set(1000, 1000, 1000);scene.add(amlight);};const initObject = () => {const geometry = new THREE.BoxGeometry(3000, 6, 2400);const material = new THREE.MeshBasicMaterial({color: 0xcccccc});geometry.position = new THREE.Vector3(0, 0, 0);mesh = new THREE.Mesh(geometry, [material, material, material, material, material, material]);mesh.receiveShadow = true; // cast投射,方块投射阴影scene.add(mesh);}const initControl = () => {// 将renderer关联到container,这个过程类似于获取canvas元素const pcanvas = document.getElementById('threeMain');controls = new OrbitControls(camera, pcanvas);// 如果使用animate方法时,将此函数删除// controls.addEventListener( 'change', render );// 使动画循环使用时阻尼或自转 意思是否有惯性controls.enableDamping = true;// 动态阻尼系数 就是鼠标拖拽旋转灵敏度// controls.dampingFactor = 0.25;// 是否可以缩放controls.enableZoom = true;// 是否自动旋转// controls.autoRotate = true;controls.autoRotateSpeed = 0.5;// 设置相机距离原点的最近距离// controls.minDistance = 10;// 设置相机距离原点的最远距离controls.maxDistance = 10000;// 是否开启右键拖拽controls.enablePan = true;};function animation() {renderer.render(scene, camera);// mesh.rotateY(0.01);requestAnimationFrame(animation);}function initHelper() {const axesHelper = new THREE.AxesHelper(3000);scene.add(axesHelper);}function threeStart() {initThree();initScene();initCamera(sizes.width, sizes.height);initHelper();initObject();initLight();initControl();animation();}return <div id="threeMain" style={{ width: '100vw', height: '100vh' }} />;
}
场景scene
是一个三维空间,相当于我们html中的body,所有节点的容器,相当于一个空房间,承载所有的物品!所以我们定义一个全局变量scene。
初始化我们可以这样:
const initScene = () => {scene = new THREE.Scene();scene.background = new THREE.Color(0xbfd1e5);};
相机carema
打个比方,就是你买了一个1万元的相机出门拍风景,你总是想要抓住最美的风景,那你便要调好相机最精确的位置、角度、焦距等,相机看到的内容就是我们最终在屏幕上看到的内容。在这个例子中我们用的是像我们眼睛的透视相机PerspectiveCamera。
还有一个常用的相机是正交相机OrthographicCamera,它看到的范围不会受距离影响!
我们也定义了一个全局变量camera,
初始化我们可以这样:
const initCamera = (width, height) => {camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 10000);camera.position.x = 0;camera.position.y = 500;camera.position.z = 1300;camera.up.x = 0;camera.up.y = 1;camera.up.z = 0;camera.lookAt({x: 0,y: 0,z: 0,});// 创建相机视锥体辅助对象// const cameraPerspectiveHelper = new THREE.CameraHelper(camera);// scene.add(cameraPerspectiveHelper);};
网络模型Mesh
在介绍它之前我们需要先了解点模型Points、线模型Line。点线面,面就是Mesh模型。点模型Points、线模型Line、网格网格模型Mesh都是由几何体Geometry和材质Material构成。在这里就不过多研究点线面了,我们最重要的知道的是一个网络模型就是一个物体穿上了衣服,没有穿衣服的皇帝不会让别人揭穿和笑话,但是我们的老板才是皇帝,所以尽量给我们的模型套件衣服吧!
同理定义一个全局变量mesh
初始化我们可以这样:
const geometry = new THREE.BoxGeometry(3000, 6, 2400);
const material = new THREE.MeshBasicMaterial({color: 0xcccccc});
geometry.position = new THREE.Vector3(0, 0, 0);
mesh = new THREE.Mesh(geometry, [material, material, material, material, material, material]);
mesh.receiveShadow = true; // cast投射,方块投射阴影
scene.add(mesh);
光源light
没有光世界便是黑暗的!同理假如没有光,摄像机看不到任何东西。所以我们需要为我们的场景加上不同光照效果。我们先从最基础的环境光AmbientLight开始。环境光意思就是哪个角度、哪个位置的光照亮度强度都一样。因为光不需要重复使用,所以我们没必要定义全局变量,所以我们初始化可以这样:
const initLight = () => {// 环境光const amlight = new THREE.AmbientLight(debugObject.light.amlight.color);amlight.position.set(1000, 1000, 1000);scene.add(amlight);
};
渲染器renderer
就相当于现实生活中你带着相机,现在去了一个美丽的地方,你需要一个相片承载下这个美丽的景色,对于threejs而言,如果你需要这张相片,就需要一个新的对象,也就是WebGL渲染器WebGLRenderer,把这些承载。
同理我们定义一个全局变量renderer,初始化我们可以这样:
renderer = new THREE.WebGLRenderer({... //属性配置
});
渲染器还需要补充几点,就是如何和我们的dom节点关联起来:
渲染器WebGLRenderer通过属性domElement可以获得渲染方法render()生成的Canvas画布,domElement本质上就是一个HTML元素:Canvas画布。我们也可以通过setSize()来设置尺寸。
定义一个html元素
return <div id="threeMain" style={{ width: '100vw', height: '100vh' }} />;
html元素和渲染器关联,那就给div增加一个子节点(canvas)
const initThree = () => {const width = document.getElementById('threeMain').clientWidth;const height = document.getElementById('threeMain').clientHeight;renderer = new THREE.WebGLRenderer({... //属性配置});renderer.setSize(width, height); //设置画布宽高document.getElementById('threeMain').appendChild(renderer.domElement); // 把画布加入dom节点
};
渲染器和我们的threejs元素关联, 那渲染器渲染方法.render(),把我们的场景和相机记录进来了!
renderer.render(scene, camera);
控制器controls
就是相当于可以通过我们的键盘和鼠标来控制我们的场景,使其有了交互功能!控制器种类有很多,但这里我们只说轨道控制器OrbitControls。它可以使得相机围绕目标进行轨道运动。打个比方(地球围绕太阳一样运动)。
同理我们定义一个全局变量controls,初始化我们可以这样:
controls = new OrbitControls(camera, pcanvas);
关联操作和属性介绍:
const initControl = () => {// 将renderer关联到container,这个过程类似于获取canvas元素const pcanvas = document.getElementById('threeMain');controls = new OrbitControls(camera, pcanvas);// 如果使用animate方法时,将此函数删除// controls.addEventListener( 'change', render );// 使动画循环使用时阻尼或自转 意思是否有惯性controls.enableDamping = true;// 动态阻尼系数 就是鼠标拖拽旋转灵敏度// controls.dampingFactor = 0.25;// 是否可以缩放controls.enableZoom = true;// 是否自动旋转// controls.autoRotate = true;controls.autoRotateSpeed = 0.5;// 设置相机距离原点的最近距离// controls.minDistance = 10;// 设置相机距离原点的最远距离controls.maxDistance = 10000;// 是否开启右键拖拽controls.enablePan = true;
};
到此,我们已经把threejs基础元素介绍的差不多了,在这里还需要补充一些很容易遗漏的地方!
动画和及时更新
function animation() {controls.update()renderer.render(scene, camera);// mesh.rotateY(0.01);requestAnimationFrame(animation);
}
补充一个知识点:
requestAnimationFrame
实现3d隧道监测基础
实现道路
如图,我们首先实现发光这部分。
这部分主要涉及的知识是给一个平面(plane)贴图,具体的知识我在代码块相应位置已经标注。
// 图加载器
const loader = new THREE.TextureLoader();
// 加载
const texture = loader.load('/model/route.png', function(t) {// eslint-disable-next-line no-param-reassign,no-multi-assignt.wrapS = t.wrapT = THREE.RepeatWrapping; //是否重复渲染和css中的背景属性渲染方式很像t.repeat.set(1, 1);
});// 平面
const geometryRoute = new THREE.PlaneGeometry(1024, 2400);
const materialRoute = new THREE.MeshStandardMaterial({map: texture, // 使用纹理贴图side: THREE.BackSide, // 背面渲染
});
const plane = new THREE.Mesh(geometryRoute, materialRoute);
plane.receiveShadow = true;
plane.position.set(0, 8, 0);
plane.rotateX(Math.PI / 2);
scene.add(plane);
实现隧道
现在我们实现发光这部分
这部分主要涉及的知识是引入一个obj模型,并给模型贴上贴图(这里的材质是一个mtl)
补充知识点:
- OBJ是一种3D模型文件,因此不包含动画、材质特性、贴图路径、动力学、粒子等信息 我们拿到一个隧道obj模型的文件打开看看,里面是什么
- mtl文件(Material Library File)是材质库文件,描述的是物体的材质信息,ASCII存储,任何文本编辑器可以将其打开和编辑。同理我们也可以打开看看,是个什么东西
- 从obj文件看出我们需要tunnelWall.mtl材质,从mtl文件,看出我们需要suidao.jpg图片(需要和模型放在同一级),其实到这里我们还是回到了引入道路的那部分,模型+贴图环节。
但是还是有一些不同的地方的,首先使用的加载器不同
const mtlLoader = new MTLLoader();
const loader = new OBJLoader(); // 在init函数中,创建loader变量,用于导入模型
其次我们的模型是属于建模自己构造的,可能你引入进来很大可能是加载不出来的!所以你需要打印对象,从中分析具体原因。
// 模型对象公共变量
const modelsObj = {tunnelWall: {mtl: '/model/tunnelWall.mtl',obj: '/model/tunnelWall.obj',mesh: null,},camera: {mtl: '/model/camera/摄像头方.mtl',obj: '/model/camera/摄像头方.obj',mesh: null,},
};
mtlLoader.load(modelsObj.tunnelWall.mtl, material => {material.preload();// 设置材质的透明度// mtl文件中的材质设置到obj加载器loader.setMaterials(material);loader.load(modelsObj.tunnelWall.obj, object => {// 设置模型大小和中心点object.children[0].geometry.computeBoundingBox();object.children[0].geometry.center();modelsObj.tunnelWall.mesh = object;scene.add(object);});
});
实现多个摄像头
现在我们实现摄像头部分
这里其实和实现隧道大相径庭,只不过我们是多个,而隧道是单个。所以我们需要引入组(group)和克隆(clone)的概念。
知识点补充:
- 组对象group:相当于一个身体有胳膊、头、腿,组成一个组。每个人组合可以再次分一个组。
- 克隆clone:字面意思就是克隆一个一模一样的你。但是需要和copy分开。
// 加载摄像头模型
const loadCameraModel = () => {const mtlLoader = new MTLLoader();const loader = new OBJLoader(); // 在init函数中,创建loader变量,用于导入模型mtlLoader.load(modelsObj.camera.mtl, material => {material.preload();// 设置材质的透明度// mtl文件中的材质设置到obj加载器loader.setMaterials(material);loader.load(modelsObj.camera.obj, object => {console.log(object);// 设置模型大小object.children[0].geometry.computeBoundingBox();object.children[0].geometry.center();modelsObj.camera.mesh = object;cloneCameraModel(4, 60, 180);cloneCameraModel(4, -200, 180);});});
};
// 克隆摄像头模型
const cloneCameraModel = (cameraSize, lrInterval, baInterval) => {const group = new THREE.Group();for (let i = 0; i <= cameraSize; i += 1) {modelsObj[`camera${i}`] = modelsObj.camera.mesh.clone();modelsObj[`camera${i}`].position.set(lrInterval, 180, baInterval * (i % 2 === 0 ? -i : i));modelsObj[`camera${i}`].scale.set(1, 1, 1);group.add(modelsObj[`camera${i}`])}scene.add(group);
};
点击模型进行属性操作
这块我们需要涉及的知识点是点击操作(Raycaster)、发光部分(效果合成器,shader渲染使用)、debugger模式(gui)
首先我们实现对模型进行的点击,我们需要使用raycaster 定义全局变量mouse初始化鼠标,光线追踪。可以这样定义:
// 获取鼠标坐标 处理点击某个模型的事件
const mouse = new THREE.Vector2(); // 初始化一个2D坐标用于存储鼠标位置
const raycaster = new THREE.Raycaster(); // 初始化光线追踪
知识点补充:
光线投射raycaster:可以向特定方向投射光线,并测试哪些对象与其相交,由鼠标点击转为世界坐标的过程。就是把一个2d坐标转变成3d坐标的强大类!
我们监听屏幕点击事件
const pcanvas = document.getElementById('threeMain');
// 监听点击事件,pcanvas
pcanvas.addEventListener('click', e => onmodelclick(e)); // 监听点击
计算点击坐标,屏幕坐标系转换成世界坐标系的过程。并赋值全局变量点击模型clickModel。
const onmodelclick = event => {console.log(event);// 获取鼠标点击位置mouse.x = (event.clientX / sizes.width) * 2 - 1;mouse.y = -(event.clientY / sizes.height) * 2 + 1;console.log(mouse);raycaster.setFromCamera(mouse, camera);const intersects = raycaster.intersectObjects(scene.children); // 获取点击到的模型的数组,从近到远排列// const worldPosition = new THREE.Vector3(); // 初始化一个3D坐标,用来记录模型的世界坐标if (intersects.length > 0) {clickModel = intersects[0].object; outlinePass.selectedObjects = [];outlinePass.selectedObjects = [clickModel];}
};
实现点击模型发光效果
threejs提供了一个扩展库EffectComposer.js,通过这个我们可以实现一些后期处理效果。所谓后期处理,就像ps一样,对threejs的渲染结果进行后期处理,比如添加发光效果。我们结合高亮发光描边可以实现下图发光效果。
- 引入相关类
import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer';
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass';
import { FXAAShader } from 'three/examples/jsm/shaders/FXAAShader';
import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass';
- 初始化三个全局变量
let composer;
let effectFXAA;
let outlinePass;
- 赋值选中发光模型
const onmodelclick = event => {
...if (intersects.length > 0) {outlinePass.selectedObjects = [];outlinePass.selectedObjects = [clickModel];}
};
- 初始化加载发光效果
// 效果合成器,shader渲染使用
const initEffectComposer = () => {// 处理模型闪烁问题【优化展示网格闪烁】// const parameters = { format: THREE.RGBAFormat };// const size = renderer.getDrawingBufferSize(new THREE.Vector2());// const renderTarget = new THREE.WebGLMultipleRenderTargets(size.width, size.height, parameters);composer = new EffectComposer(renderer);const renderPass = new RenderPass(scene, camera);composer.addPass(renderPass);outlinePass = new OutlinePass(new THREE.Vector2(sizes.width, sizes.height), scene, camera);outlinePass.visibleEdgeColor.set(255, 255, 0);outlinePass.edgeStrength = 1.0; // 边框的亮度outlinePass.edgeGlow = 1; // 光晕[0,1]outlinePass.usePatternTexture = false; // 是否使用父级的材质outlinePass.edgeThickness = 1.0; // 边框宽度outlinePass.downSampleRatio = 1; // 边框弯曲度composer.addPass(outlinePass);const outputPass = new OutputPass();composer.addPass(outputPass);effectFXAA = new ShaderPass(FXAAShader);effectFXAA.uniforms.resolution.value.set(1 / sizes.width, 1 / sizes.height);composer.addPass(effectFXAA);
};
- 渲染循环执行
function animation() {stats.update();renderer.render(scene, camera);composer.render();// mesh.rotateY(0.01);requestAnimationFrame(animation);
}
debugger模式 这节主要涉及gui,并且补充一下阴影的知识。gui是一个图形用户界面工具,我们可以通过这个工具实现对属性进行动态的操作,很方便。下面标红的就是我们的界面工具
我们通过增加点光源来举个例子。
- 首先我们初始化全局变量gui并且赋值
// debugger
let gui;
function initDebugger() {gui = new GUI();
}
- 定义全局变量debugObject需要改变的属性。
// debugger属性
const debugObject = {light: {pointLight: {color: 0xff0000,position: {x: 0,y: 400,z: 1800,},},},
};
- 定义点光源,对点光源的位置和颜色属性动态切换
// 点光源
const pointLight = new THREE.PointLight(debuggerPointLight.color, 1);
pointLight.castShadow = true;
pointLight.position.set(100, 100, 300);
scene.add(pointLight);
const pointLightFolder = lightFolder.addFolder('点光源');
pointLightFolder.addColor(debuggerPointLight, 'color').onChange(function(value) {pointLight.color.set(value);
});
// 点光源位置
pointLightFolder.add(debuggerPointLight.position, 'x', -1000, 1000).onChange(function(value) {pointLight.position.x = value;pointLightHelper.update();
});
pointLightFolder.add(debuggerPointLight.position, 'y', -1000, 1000).onChange(function(value) {pointLight.position.y = value;pointLightHelper.update();
});
pointLightFolder.add(debuggerPointLight.position, 'z', -1000, 1000).onChange(function(value) {pointLight.position.z = value;pointLightHelper.update();
});
实现效果如图
- 开启阴影
阴影渲染
renderer = new THREE.WebGLRenderer({...
});
renderer.shadowMap.enabled = true;
点光源投射光影
const pointLight = new THREE.PointLight(debuggerPointLight.color, 1);
pointLight.castShadow = true;
模型和道路接受阴影和投射阴影
plane.receiveShadow = true;
loader.load(modelsObj.tunnelWall.obj, object => {object.traverse(obj => {if (obj.castShadow !== undefined) {// 开启投射影响// eslint-disable-next-line no-param-reassignobj.castShadow = true;// 开启被投射阴影// eslint-disable-next-line no-param-reassignobj.receiveShadow = true;}});
性能监视器stats
一个计算渲染分辨率FPS的工具,在这里提一下。
引入
import Stats from 'three/examples/jsm/libs/stats.module';
使用
// 性能监视器
let stats;document.getElementById('threeMain').appendChild(stats.domElement);function initStats() {stats = new Stats();stats.showPanel(1); // 0: fps, 1: ms, 2: mb, 3+: custom
}function animation() {stats.update();renderer.render(scene, camera);composer.render();// mesh.rotateY(0.01);requestAnimationFrame(animation);
}
总结
这是我们实现目标的一个小小起点,属于冰山一角,前路漫漫,还需要阅读很多知识文档和试错阶段,如果你对后续感兴趣的话,可以跟进一下呀!谢谢!
完整代码
import React, { useEffect } from 'react';
import * as THREE from 'three';
// eslint-disable-next-line import/extensions
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
// eslint-disable-next-line import/extensions
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader';
import Stats from 'three/examples/jsm/libs/stats.module';
// eslint-disable-next-line import/extensions
import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer';
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass';
import { FXAAShader } from 'three/examples/jsm/shaders/FXAAShader';
import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass';export default function ThreeVisual() {// 场景let scene;// 相机let camera;// 控制器let controls;// 网络模型let mesh;// 渲染器let renderer;// 性能监视器let stats;// debuggerlet gui;// 当前点击模型let clickModel;// 当前点击需要使用的let composer;let effectFXAA;let outlinePass;// debugger属性const debugObject = {light: {amlight: {color: 0xffffff,},directionalLight: {color: 0xffffff,position: {x: 0,y: 400,z: 1800,},},pointLight: {color: 0xff0000,position: {x: 0,y: 400,z: 1800,},},},model: {wall: {position: {x: 0,y: 210,z: 0,},scale: 0.12,opacity: {wallTopOpa: 0.4,wallSideOpa: 1,},},camera: {position: {x: 100,y: 100,z: 100,},scale: 1,},},};// 模型对象const modelsObj = {tunnelWall: {mtl: '/model/tunnelWall.mtl',obj: '/model/tunnelWall.obj',mesh: null,},camera: {mtl: '/model/camera/摄像头方.mtl',obj: '/model/camera/摄像头方.obj',mesh: null,},};const sizes = {width: window.innerWidth,height: window.innerHeight,};// 获取鼠标坐标 处理点击某个模型的事件const mouse = new THREE.Vector2(); // 初始化一个2D坐标用于存储鼠标位置const raycaster = new THREE.Raycaster(); // 初始化光线追踪useEffect(() => {// eslint-disable-next-line no-use-before-definethreeStart();}, []);const initThree = () => {const width = document.getElementById('threeMain').clientWidth;const height = document.getElementById('threeMain').clientHeight;renderer = new THREE.WebGLRenderer({antialias: true,logarithmicDepthBuffer: true,});renderer.shadowMap.enabled = true;renderer.setSize(width, height);document.getElementById('threeMain').appendChild(renderer.domElement);// renderer.setClearColor(0xFFFFFF, 1.0);document.getElementById('threeMain').appendChild(stats.domElement);};const initCamera = (width, height) => {camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 10000);camera.position.x = 0;camera.position.y = 500;camera.position.z = 1300;camera.up.x = 0;camera.up.y = 1;camera.up.z = 0;camera.lookAt({x: 0,y: 0,z: 0,});// 创建相机视锥体辅助对象// const cameraPerspectiveHelper = new THREE.CameraHelper(camera);// scene.add(cameraPerspectiveHelper);};const initScene = () => {scene = new THREE.Scene();scene.background = new THREE.Color(0xbfd1e5);};const initLight = () => {const lightFolder = gui.addFolder('光');const {directionalLight: debuggerDirectionalLight,pointLight: debuggerPointLight,} = debugObject.light;// 环境光// const amlight = new THREE.AmbientLight(debugObject.light.amlight.color);// amlight.position.set(1000, 1000, 1000);// scene.add(amlight);// // 环境光debugger// const amlightFolder=lightFolder.addFolder("环境光")// amlightFolder.addColor(debugObject.light.amlight, 'color').onChange(function(value){// amlight.color.set(value);// });// 平行光// 创建平行光,颜色为白色,强度为 10const directionalLight = new THREE.DirectionalLight(debuggerDirectionalLight.color, 1);// 设置平行光的方向directionalLight.position.set(0, 400, 1000);directionalLight.castShadow = true;const directonalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 20);// scene.add(directonalLightHelper);scene.add(directionalLight);// 平行光debuggerconst directionalLightFolder = lightFolder.addFolder('平行光');directionalLightFolder.addColor(debuggerDirectionalLight, 'color').onChange(function(value) {directionalLight.color.set(value);});// 平行光位置directionalLightFolder.add(debuggerDirectionalLight.position, 'x', -1000, 1000).onChange(function(value) {directionalLight.position.x = value;directonalLightHelper.update();});directionalLightFolder.add(debuggerDirectionalLight.position, 'y', -1000, 1000).onChange(function(value) {directionalLight.position.y = value;directonalLightHelper.update();});directionalLightFolder.add(debuggerDirectionalLight.position, 'z', -1000, 1000).onChange(function(value) {directionalLight.position.z = value;directonalLightHelper.update();});// 点光源const pointLight = new THREE.PointLight(debuggerPointLight.color, 1);pointLight.castShadow = true;pointLight.position.set(100, 100, 300);const sphereSize = 10;const pointLightHelper = new THREE.PointLightHelper(pointLight, sphereSize);scene.add(pointLight);scene.add(pointLightHelper);const pointLightFolder = lightFolder.addFolder('点光源');pointLightFolder.addColor(debuggerPointLight, 'color').onChange(function(value) {pointLight.color.set(value);});// 点光源位置pointLightFolder.add(debuggerPointLight.position, 'x', -1000, 1000).onChange(function(value) {pointLight.position.x = value;pointLightHelper.update();});pointLightFolder.add(debuggerPointLight.position, 'y', -1000, 1000).onChange(function(value) {pointLight.position.y = value;pointLightHelper.update();});pointLightFolder.add(debuggerPointLight.position, 'z', -1000, 1000).onChange(function(value) {pointLight.position.z = value;pointLightHelper.update();});};const initObject = () => {const geometry = new THREE.BoxGeometry(3000, 6, 2400);const loader = new THREE.TextureLoader();const texture = loader.load('/model/route.png', function(t) {// eslint-disable-next-line no-param-reassign,no-multi-assignt.wrapS = t.wrapT = THREE.RepeatWrapping;t.repeat.set(1, 1);});const material = new THREE.MeshBasicMaterial({ color: 0xcccccc });geometry.position = new THREE.Vector3(0, 0, 0);mesh = new THREE.Mesh(geometry, [material, material, material, material, material, material]);mesh.receiveShadow = true; // cast投射,方块投射阴影scene.add(mesh);// 平面const geometryRoute = new THREE.PlaneGeometry(1024, 2400);const materialRoute = new THREE.MeshStandardMaterial({map: texture, // 使用纹理贴图side: THREE.BackSide, // 两面都渲染});const plane = new THREE.Mesh(geometryRoute, materialRoute);plane.receiveShadow = true;plane.position.set(0, 8, 0);plane.rotateX(Math.PI / 2);scene.add(plane);};const initControl = () => {// 将renderer关联到container,这个过程类似于获取canvas元素const pcanvas = document.getElementById('threeMain');controls = new OrbitControls(camera, pcanvas);// 如果使用animate方法时,将此函数删除// controls.addEventListener( 'change', render );// 使动画循环使用时阻尼或自转 意思是否有惯性controls.enableDamping = true;// 动态阻尼系数 就是鼠标拖拽旋转灵敏度// controls.dampingFactor = 0.25;// 是否可以缩放controls.enableZoom = true;// 是否自动旋转// controls.autoRotate = true;controls.autoRotateSpeed = 0.5;// 设置相机距离原点的最近距离// controls.minDistance = 10;// 设置相机距离原点的最远距离controls.maxDistance = 10000;// 是否开启右键拖拽controls.enablePan = true;};const onmodelclick = event => {console.log(event);// 获取鼠标点击位置mouse.x = (event.clientX / sizes.width) * 2 - 1;mouse.y = -(event.clientY / sizes.height) * 2 + 1;console.log(mouse);raycaster.setFromCamera(mouse, camera);const intersects = raycaster.intersectObjects(scene.children); // 获取点击到的模型的数组,从近到远排列// const worldPosition = new THREE.Vector3(); // 初始化一个3D坐标,用来记录模型的世界坐标if (intersects.length > 0) {clickModel = intersects[0].object;outlinePass.selectedObjects = [];outlinePass.selectedObjects = [clickModel];// intersects[0].object.getWorldPosition(worldPosition); // 将点中的3D模型坐标记录到worldPosition中// const texture = new THREE.TextureLoader().load("/model/route.png");// const spriteMaterial = new THREE.SpriteMaterial({// map: texture,// 设置精灵纹理贴图// });// const sprite = new THREE.Sprite(spriteMaterial); // 精灵模型,不管从哪个角度看都可以一直面对你// scene.add(sprite);// sprite.scale.set(40,40,40);// sprite.position.set(worldPosition.x, worldPosition.y + 8, worldPosition.z); // 根据刚才获取的世界坐标设置精灵模型位置,高度加了3,是为了使精灵模型显示在点击模型的上方}};const initEvent = () => {window.addEventListener('resize', () => {// Update sizessizes.width = window.innerWidth;sizes.height = window.innerHeight;// Update cameracamera.aspect = sizes.width / sizes.height;camera.updateProjectionMatrix();// Update rendererrenderer.setSize(sizes.width, sizes.height);composer.setSize(sizes.width, sizes.height);renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));effectFXAA.uniforms.resolution.value.set(1 / sizes.width, 1 / sizes.height);});const pcanvas = document.getElementById('threeMain');// 监听点击事件pcanvas.addEventListener('click', e => onmodelclick(e)); // 监听点击};const loadModel = () => {const mtlLoader = new MTLLoader();const loader = new OBJLoader(); // 在init函数中,创建loader变量,用于导入模型mtlLoader.load(modelsObj.tunnelWall.mtl, material => {material.preload();// 设置材质的透明度// mtl文件中的材质设置到obj加载器loader.setMaterials(material);loader.load(modelsObj.tunnelWall.obj, object => {object.traverse(obj => {if (obj.castShadow !== undefined) {// 开启投射影响// eslint-disable-next-line no-param-reassignobj.castShadow = true;// 开启被投射阴影// eslint-disable-next-line no-param-reassignobj.receiveShadow = true;}});// 设置模型大小object.children[0].geometry.computeBoundingBox();object.children[0].geometry.center();// debugger模型属性const { scale, position, opacity } = debugObject.model.wall;// 模型本有属性const {scale: changeScale,position: changePositon,material: changeMaterial,} = object.children[0];changeScale.set(scale, scale, scale);changePositon.set(position.x, position.y, position.z);changeMaterial[0].transparent = true;changeMaterial[0].opacity = opacity.wallTopOpa;changeMaterial[1].transparent = true;changeMaterial[1].opacity = opacity.wallSideOpa;modelsObj.tunnelWall.mesh = object;scene.add(object);// 模型debuggerconst modelFolder = gui.addFolder('模型');const wallFolder = modelFolder.addFolder('墙');wallFolder.add(position, 'x', -100, 300).step(0.5).onChange(function(value) {changePositon.x = value;});wallFolder.add(position, 'y', -100, 300).step(0.5).onChange(function(value) {changePositon.y = value;});wallFolder.add(position, 'z', -100, 300).step(0.5).onChange(function(value) {changePositon.z = value;});wallFolder.add(debugObject.model.wall, 'scale', 0.01, 0.3).step(0.001).onChange(function(value) {changeScale.set(value, value, value);});wallFolder.add(opacity, 'wallTopOpa', 0, 1).step(0.01).onChange(function(value) {changeMaterial[0].opacity = value;});wallFolder.add(opacity, 'wallSideOpa', 0, 1).step(0.01).onChange(function(value) {changeMaterial[1].opacity = value;});});});};// 克隆摄像头模型const cloneCameraModel = (cameraSize, lrInterval, baInterval) => {const group = new THREE.Group();for (let i = 0; i <= cameraSize; i += 1) {modelsObj[`camera${i}`] = modelsObj.camera.mesh.clone();modelsObj[`camera${i}`].position.set(lrInterval, 180, baInterval * (i % 2 === 0 ? -i : i));modelsObj[`camera${i}`].scale.set(1, 1, 1);group.add(modelsObj[`camera${i}`])}scene.add(group);};// 加载摄像头模型const loadCameraModel = () => {const mtlLoader = new MTLLoader();const loader = new OBJLoader(); // 在init函数中,创建loader变量,用于导入模型mtlLoader.load(modelsObj.camera.mtl, material => {material.preload();// 设置材质的透明度// mtl文件中的材质设置到obj加载器loader.setMaterials(material);loader.load(modelsObj.camera.obj, object => {object.traverse(obj => {if (obj.castShadow !== undefined) {// 开启投射影响// eslint-disable-next-line no-param-reassignobj.castShadow = true;// 开启被投射阴影// eslint-disable-next-line no-param-reassignobj.receiveShadow = true;}});console.log(object);// 设置模型大小object.children[0].geometry.computeBoundingBox();object.children[0].geometry.center();// debugger模型属性object.children[0].scale.set(1, 1, 1);object.children[0].position.set(100, 100, 100);modelsObj.camera.mesh = object;cloneCameraModel(4, 60, 180);cloneCameraModel(4, -200, 180);});});};// 效果合成器,shader渲染使用const initEffectComposer = () => {// 处理模型闪烁问题【优化展示网格闪烁】// const parameters = { format: THREE.RGBAFormat };// const size = renderer.getDrawingBufferSize(new THREE.Vector2());// const renderTarget = new THREE.WebGLMultipleRenderTargets(size.width, size.height, parameters);composer = new EffectComposer(renderer);const renderPass = new RenderPass(scene, camera);composer.addPass(renderPass);outlinePass = new OutlinePass(new THREE.Vector2(sizes.width, sizes.height), scene, camera);outlinePass.visibleEdgeColor.set(255, 255, 0);outlinePass.edgeStrength = 1.0; // 边框的亮度outlinePass.edgeGlow = 1; // 光晕[0,1]outlinePass.usePatternTexture = false; // 是否使用父级的材质outlinePass.edgeThickness = 1.0; // 边框宽度outlinePass.downSampleRatio = 1; // 边框弯曲度composer.addPass(outlinePass);const outputPass = new OutputPass();composer.addPass(outputPass);effectFXAA = new ShaderPass(FXAAShader);effectFXAA.uniforms.resolution.value.set(1 / sizes.width, 1 / sizes.height);composer.addPass(effectFXAA);};function animation() {stats.update();renderer.render(scene, camera);composer.render();// mesh.rotateY(0.01);requestAnimationFrame(animation);}function initHelper() {// const axesHelper = new THREE.AxesHelper(3000);// scene.add(axesHelper);}function initStats() {stats = new Stats();stats.showPanel(1); // 0: fps, 1: ms, 2: mb, 3+: custom}function initDebugger() {gui = new GUI();}function threeStart() {initEvent();initStats();initDebugger();initThree();initScene();initCamera(sizes.width, sizes.height);initHelper();initLight();initControl();initObject();loadModel();loadCameraModel();initEffectComposer();animation();}return <div id="threeMain" style={{ width: '100vw', height: '100vh' }} />;
}
相关文章:

Three.js 搭建3D隧道监测
Three.js 搭建3D隧道监测 Three.js 基础元素场景scene相机carema网络模型Mesh光源light渲染器renderer控制器controls 实现3d隧道监测基础实现道路实现隧道实现多个摄像头点击模型进行属性操作实现点击模型发光效果 性能监视器stats引入使用 总结完整代码 我们将通过three.js技…...

「IDE」集成开发环境专栏目录大纲
✨博客主页何曾参静谧的博客📌文章专栏「IDE」集成开发环境📚全部专栏「Win」Windows程序设计「IDE」集成开发环境「UG/NX」BlockUI集合「C/C」C/C程序设计「DSA」数据结构与算法「UG/NX」NX二次开发「QT」QT5程序设计「File」数据文件格式「UG/NX」NX定…...

MySQL-初识数据库
目录 一、数据库基础概念 1、SQL 2、数据(Data) 3、数据库(DB) 4、数据库管理系统DBMS 5、数据库系统DBS 6、关系模型(Relational Model) 7、E-R图 8、常见的数据库 9、数据库基本操作 一、数据库…...

初始 html
html 文件结构 html 标签是整个 html 文件的根标签(最顶层标签) head 标签中写页面的属性. body 标签中写的是页面上显示的内容 title 标签中写的是页面的标题 <html><head><title>这是一个标题</title></head><body></body> <…...

前端 call、bind、apply的实际使用
目录 一、call 1、继承的子类可以使用父类的方法 2、可以接收任意参数 二、call、apply、bind比较 1、案例一 2、案例二 三、总结 这个三个方法都是改变函数的this指向的方法。 一、call 代码: const obj{uname:"pink"}function fn(){console.log…...
非关系型数据库NoSQL的类型与优缺点对比
NoSQL数据库根据数据模型和应用场景主要分为四种类型:键值型、列族型、文档型和图形型。以下是对每种类型的详细描述,包括其应用场景、优缺点的比较: 1. 键值型数据库 (Key-Value Store) 典型代表 RedisMemcachedAmazon DynamoDB 应用场景…...

面试击穿mysql
Mysql三大范式: 第一范式(1NF): 不符合第一范式的典型情况是在一个字段中存放多种不同类型的详细信息。例如,在商品表中,若将商品名称、价格和类型都存储在同一个字段中,会带来诸多弊端。首先,在…...

PyQt5超详细教程终篇
PyQt5超详细教程 前言 接: [【Python篇】PyQt5 超详细教程——由入门到精通(序篇)](【Python篇】PyQt5 超详细教程——由入门到精通(序篇)-CSDN博客) 建议把代码复制到pycahrm等IDE上面看实际效果,方便理…...

Android OpenGL ES详解——纹理:纹理过滤GL_NEAREST和GL_LINEAR的区别
目录 一、概念 1、纹理过滤 2、邻近过滤 3、线性过滤 二、邻近过滤和线性过滤的区别 三、源码下载 一、概念 1、纹理过滤 当纹理被应用到三维物体上时,随着物体表面的形状和相机视角的变化,会导致纹理在渲染过程中出现一些问题,如锯齿…...
Elasticsearch实战应用:从入门到精通
在当今这个数据爆炸的时代,如何快速、有效地从海量数据中检索信息,已经成为了许多企业和开发者面临的挑战。Elasticsearch,作为一个基于Lucene的搜索引擎,以其强大的全文搜索能力、分布式特性以及易用性,成为了解决这一…...

axios平替!用浏览器自带的fetch处理AJAX(兼容表单/JSON/文件上传)
fetch 是啥? fetch 函数是 JavaScript 中用于发送网络请求的内置 API,可以替代传统的 XMLHttpRequest。它可以发送 HTTP 请求(如 GET、POST 等),并返回一个 Promise,从而简化异步操作 基本用法 /* 下面是…...

【优选算法 — 滑动窗口】水果成篮 找到字符串中所有字母异位词
水果成篮 水果成篮 题目描述 因为只有两个篮子,每个篮子装的水果种类相同,如果从 0 开始摘,则只能摘 0 和 1 两个种类 ; 因为当我们在两个果篮都装有水果的情况下,如果再走到下一颗果树,果树的水果种类…...
Go 数据库查询与结构体映射
下面是关于如何使用 Go 进行数据库查询并映射数据到结构体的教程,重点讲解 结构体字段导出 和 db 标签 的使用。 Go 数据库查询与结构体映射教程 在 Go 中,我们可以使用 database/sql 或 sqlx 等库与数据库进行交互。为了方便地将数据库查询结果映射到结…...

Wi-Fi背后的工作原理与技术发展历程介绍【无线通信小百科】
1个视频说清楚WIFI:频段/历程/技术参数/常用模块 智能手机拥有率越来越高的今天,大家已经习惯了通过无线网络上网的方式。除了在外面需要用手机流量,我们通常在家里或者机场,商场都可以通过Wi-Fi连接上网。本期文章将为大家介绍Wi…...

2024 年(第 7 届)“泰迪杯”数据分析技能赛B 题 特殊医学用途配方食品数据分析 完整代码 结果 可视化分享
一、背景特殊医学用途配方食品简称特医食品,是指为满足进食受限、消化吸收障碍、代谢素乱或者特定疾病状态人群对营养素或者膳食的特殊需要,专门加工配置而成的配方食品,包括0月龄至12月龄的特殊医学用途婴儿配方食品和适用于1岁以上的特殊医…...

STM32学习笔记------编程驱动蜂鸣器实现音乐播放
1. 硬件准备 STM32开发板:STM32F407系列蜂鸣器:常见的蜂鸣器分为两类:有源蜂鸣器和无源蜂鸣器。若使用有源蜂鸣器,只需提供电源和控制信号即可;若使用无源蜂鸣器,则需要控制频率。外接电源(可选…...

ubuntu18.04 安装与卸载NCCL conda环境安装PaddlePaddle
cuda版本11.2 说明PaddlePaddle需要安装NCCL 1、Log in | NVIDIA Developer 登录官网 找到对应版本 官方提供了多种安装方式,本文使用Local installers (x86)本地安装 点击对应的版本下载如: nccl-local-repo-ubuntu1804-2.8.4-cuda11.2_1.0-1_amd6…...

AI有鼻子了,还能远程传输气味,图像生成香水
众所周知,图像、音乐能用AI生成,但出乎意料的是,气味也行。最近,一个名叫Osmo的初创公司宣布,他们成功地将气味数字化了。第一个成功的案例是“新鲜的夏季李子”,而且复现出的味道“闻起来”很不错。整个过…...

学习配置dify过程记录
最近在学习安装 Dify 并集成 Ollama 和 Xinference,学习过程中遇到很多问题,所以我都记录下来。 本人电脑环境:MacBook Pro 15.1系统 基本是基于B站教程一步步搭建: 【Dify快速入门 | 本地部署Dify基于Llama 3.1和OpenAI创建聊天机器人与知…...
简易抽奖器源码以及打包操作
import wx import random import time# 定义Myframe类,继承Frame class Myframe(wx.Frame):# 奖品rewards [桥本香奈, 二代CC, NaNa, 情深叉]# 构造方法def __init__(self):# 父类初始化super().__init__(None, title主界面, size(500, 400), pos(500, 200))# 创建面板&#x…...
挑战杯推荐项目
“人工智能”创意赛 - 智能艺术创作助手:借助大模型技术,开发能根据用户输入的主题、风格等要求,生成绘画、音乐、文学作品等多种形式艺术创作灵感或初稿的应用,帮助艺术家和创意爱好者激发创意、提高创作效率。 - 个性化梦境…...

深入浅出Asp.Net Core MVC应用开发系列-AspNetCore中的日志记录
ASP.NET Core 是一个跨平台的开源框架,用于在 Windows、macOS 或 Linux 上生成基于云的新式 Web 应用。 ASP.NET Core 中的日志记录 .NET 通过 ILogger API 支持高性能结构化日志记录,以帮助监视应用程序行为和诊断问题。 可以通过配置不同的记录提供程…...

TDengine 快速体验(Docker 镜像方式)
简介 TDengine 可以通过安装包、Docker 镜像 及云服务快速体验 TDengine 的功能,本节首先介绍如何通过 Docker 快速体验 TDengine,然后介绍如何在 Docker 环境下体验 TDengine 的写入和查询功能。如果你不熟悉 Docker,请使用 安装包的方式快…...
椭圆曲线密码学(ECC)
一、ECC算法概述 椭圆曲线密码学(Elliptic Curve Cryptography)是基于椭圆曲线数学理论的公钥密码系统,由Neal Koblitz和Victor Miller在1985年独立提出。相比RSA,ECC在相同安全强度下密钥更短(256位ECC ≈ 3072位RSA…...

【OSG学习笔记】Day 18: 碰撞检测与物理交互
物理引擎(Physics Engine) 物理引擎 是一种通过计算机模拟物理规律(如力学、碰撞、重力、流体动力学等)的软件工具或库。 它的核心目标是在虚拟环境中逼真地模拟物体的运动和交互,广泛应用于 游戏开发、动画制作、虚…...
DeepSeek 赋能智慧能源:微电网优化调度的智能革新路径
目录 一、智慧能源微电网优化调度概述1.1 智慧能源微电网概念1.2 优化调度的重要性1.3 目前面临的挑战 二、DeepSeek 技术探秘2.1 DeepSeek 技术原理2.2 DeepSeek 独特优势2.3 DeepSeek 在 AI 领域地位 三、DeepSeek 在微电网优化调度中的应用剖析3.1 数据处理与分析3.2 预测与…...

对WWDC 2025 Keynote 内容的预测
借助我们以往对苹果公司发展路径的深入研究经验,以及大语言模型的分析能力,我们系统梳理了多年来苹果 WWDC 主题演讲的规律。在 WWDC 2025 即将揭幕之际,我们让 ChatGPT 对今年的 Keynote 内容进行了一个初步预测,聊作存档。等到明…...

华为OD机试-食堂供餐-二分法
import java.util.Arrays; import java.util.Scanner;public class DemoTest3 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseint a in.nextIn…...
【决胜公务员考试】求职OMG——见面课测验1
2025最新版!!!6.8截至答题,大家注意呀! 博主码字不易点个关注吧,祝期末顺利~~ 1.单选题(2分) 下列说法错误的是:( B ) A.选调生属于公务员系统 B.公务员属于事业编 C.选调生有基层锻炼的要求 D…...

网络编程(UDP编程)
思维导图 UDP基础编程(单播) 1.流程图 服务器:短信的接收方 创建套接字 (socket)-----------------------------------------》有手机指定网络信息-----------------------------------------------》有号码绑定套接字 (bind)--------------…...