本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com
我们将通过 three.js 技术打造 3d 隧道监测可视化项目,隧道监测项目将涵盖照明,风机的运行情况,控制车道指示灯关闭,情报板、火灾报警告警、消防安全、车行横洞、风向仪、隧道紧急逃生出口的控制以及事故模拟等!那先来看看我们的初步成果!因为作者也是在边学习边做的情况,效果有些丑陋,希望不要见笑!!!three.js 基础知识还是基本涵盖了,入门还是很有参考价值的!
Three.js 基础元素
我们将通过一个基本的 three.js 模板代码更好的概况我们的基础元素
import React, { useEffect } from 'react';import * as THREE from 'three';// eslint-disable-next-line import/extensionsimport { 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-define threeStart(); }, []); 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 构成。在这里就不过多研究点线面了,我们最重要的知道的是一个网络模型就是一个物体穿上了衣服,没有穿衣服的皇帝不会让别人揭穿和笑话,但是我们的老板才是皇帝,所以尽量给我们的模型套件衣服吧!如果你想换件漂亮的衣服你可以看看我以前写的这篇关于材质贴图 [1] 的文章。
同理定义一个全局变量 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-assign t.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 坐标的强大类!原理可以看这篇文章原理和推导过程 [2]
我们监听屏幕点击事件
const pcanvas = document.getElementById('threeMain');// 监听点击事件,pcanvaspcanvas.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);}如果你对这部分有很多疑问的话,你可以参考这篇文章 [3]
debugger 模式 这节主要涉及 gui,并且补充一下阴影的知识。gui 是一个图形用户界面工具, 我们可以通过这个工具实现对属性进行动态的操作,很方便。下面标红的就是我们的界面工具
我们通过增加点光源来举个例子。
- 首先我们初始化全局变量 gui 并且赋值
// debuggerlet 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-reassign obj.castShadow = true; // 开启被投射阴影 // eslint-disable-next-line no-param-reassign obj.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/extensionsimport { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';// eslint-disable-next-line import/extensionsimport { 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/extensionsimport { 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; // debugger let 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-define threeStart(); }, []); 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); // }); // 平行光 // 创建平行光,颜色为白色,强度为 10 const 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); // 平行光debugger const 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-assign t.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 sizes sizes.width = window.innerWidth; sizes.height = window.innerHeight; // Update camera camera.aspect = sizes.width / sizes.height; camera.updateProjectionMatrix(); // Update renderer renderer.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-reassign obj.castShadow = true; // 开启被投射阴影 // eslint-disable-next-line no-param-reassign obj.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); // 模型debugger const 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-reassign obj.castShadow = true; // 开启被投射阴影 // eslint-disable-next-line no-param-reassign obj.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' }} />;}参考资料
[1]
材质贴图: _https://juejin.cn/post/7129065605461884964_
[2]
原理和推导过程: _https://www.cnblogs.com/smedas/p/12445201.html_
[3]
后处理 (发光描边 OutlinePass): _http://www.webgl3d.cn/pages/e1e75d/_
作者:柳杉
链接:https://juejin.cn/post/7273987266523136056
感谢您的阅读
在看点赞 好文不断