0


threejs实战数字孪生园区开源(threejs+vue3+vite)

Hello大家好,我是日拱一卒的攻城师不浪,专注前端、后端、AI学习、2D3D、GIS等学习沉淀,这是2024年输出的第10/100篇文章,欢迎志同道合的朋友一起学习交流;

公众号:攻城师不浪
绿泡泡:brown_7778

视频效果

threejs数字孪生园区

前言

近两年,

web3D

的势头逐渐兴起。

例如得物的

VR穿戴

,贝壳的

VR游览

,高德地图的3D白模建筑以及

VR导航

,懂车帝的

汽车3D展示

等等,这些功能都需要具备一定的

3D

开发能力。

web3D最直接的需求就是前几年兴起的

数字孪生

概念,也有很多大厂单独成立了数字孪生部门去抢赛道。

可能有的同学还不知道数字孪生的概念,我将用最通俗易懂的语言去解释:

数字孪生:就好比你有一个双胞胎兄弟,你们长得一模一样,但一个是活在现实里的真人,另一个是活在电脑里的

虚拟

人。这个虚拟的兄弟,就是你的“数字孪生”。在现实世界中,数字孪生通常指的是通过各种数据和先进的技术手段,创建一个真实物体或系统的

虚拟副本

。这个副本不仅外观和原型一样,而且还能模拟和反映原型在现实世界中的行为和状态。比如,一座大楼、一辆汽车,甚至是整个城市,都可以有自己的数字孪生。

OK,言归正传,写这篇文章的起因,也是因为前段时间,公司有这方面的需求,需要给公司的园区做一个

数字孪生

,放在公司的展厅进行展示。

做数字孪生肯定就要涉及3D,在对比了众多3D开发引擎(

unreal

,

unity

,

Babylonjs

,

threejs

)之后,发现

threejs

的开发成本是最低的。

所以我就被

委以重任

,从零开始学习threejs开发。一开始是先在官网上学基础,例如

相机视角

矩阵

法线

射线交叉

3D坐标系

光线

Shader材质

模型加载

等等。

说实话,对于我这一直写2D的选手来说,确实一脸懵逼,这还没涉及到写更有难度的

粒子效果GLSL

呢。。。

所以就想着去网上能不能找到相关实战代码,但是网上关于threejs实战开发的案例实在是少之又少,而且大部分都要收费,废了九牛二虎之力才找到了一个实战开发

threejs-park

项目(由于项目过去很久了,开源的找不到了,找到的同学可以帮忙在评论区@一下),感觉还不错,因此就仿照着他的项目,自己一边熟悉一边梳理,最终改成了

threejs+vue3+vite

的一个项目。

项目概览

可以看出项目的核心是封装了一些Threejs常用到的基类,这些都封装好之后,再理解一下3D里的一些概念类的东西,剩下的就是纯JS逻辑了。

Viewer视角

这个是threejs里最重要的一个元素,俗称视角,也就是说我们在3D场景里,肯定是需要一个视角去观看场景的,所以它是最基础且必不可少的。

exportdefaultclassViewer{/**
   * 
   * @param {*} id 场景容器id
   */constructor(id){
    Cache.enabled =true// 开启缓存this.id = id
    this.renderer =undefinedthis.scene =undefinedthis.camera =undefinedthis.controls =undefinedthis.animateEventList =[]this.#initViewer()}#initViewer(){this.#initRenderer()this.#initCamera()this.#initScene()this.#initControl()this.#initSkybox()this.#initLight()constanimate=()=>{requestAnimationFrame(animate)this.#updateDom()this.#renderDom()// 全局的公共动画函数,添加函数可同步执行this.animateEventList.forEach(event=>{
          event.fun && event.content && event.fun(event.content)})}animate()}/**
   * 创建初始化场景界面
   */#initRenderer(){// 获取画布domthis.viewerDom = document.getElementById(this.id)// 初始化渲染器this.renderer =newWebGLRenderer({// logarithmicDepthBuffer: true, // true/false 表示是否使用对数深度缓冲,true性能不好antialias:true,// true/false表示是否开启反锯齿alpha:true,// true/false 表示是否可以设置背景色透明precision:"highp",// highp/mediump/lowp 表示着色精度选择premultipliedAlpha:true,// true/false 表示是否可以设置像素深度(用来度量图像的分辨率)})this.renderer.clearDepth();// 设置深度缓冲区this.renderer.shadowMap.enabled =true// 场景中的阴影自动更新this.viewerDom.appendChild(this.renderer.domElement)// 将渲染器添加到画布中// 二维标签this.labelRenderer =newCSS2DRenderer()// 标签渲染器this.labelRenderer.domElement.style.zIndex =2this.labelRenderer.domElement.style.position ='absolute'this.labelRenderer.domElement.style.top ='0px'this.labelRenderer.domElement.style.left ='0px'this.labelRenderer.domElement.style.pointerEvents ='none'// 避免HTML标签遮挡三维场景的鼠标事件this.viewerDom.appendChild(this.labelRenderer.domElement)// 三维标签this.css3DRenderer =newCSS3DRenderer()// 标签渲染器this.css3DRenderer.domElement.style.zIndex =0this.css3DRenderer.domElement.style.position ='absolute'this.css3DRenderer.domElement.style.top ='0px'this.css3DRenderer.domElement.style.left ='0px'this.css3DRenderer.domElement.style.pointerEvents ='none'// 避免HTML标签遮挡三维场景的鼠标事件this.viewerDom.appendChild(this.css3DRenderer.domElement)}/**
   * 渲染相机
   */#initCamera(){this.camera =newPerspectiveCamera(45, window.innerWidth / window.innerHeight,0.1,500000)// 透视相机this.camera.position.set(50,0,50)// 相机位置this.camera.lookAt(0,0,0)// 设置相机方向}/**
   * 渲染场景
   */#initScene(){this.scene =newScene()this.css3dScene =newScene()this.scene.background =newColor('rgb(5,24,38)')}}

这个类的一些核心代码,包括初始化

场景

相机

控制器

光线

等,代码里的注释很详细,我就不一一解释了。

Lights光线

一个3D场景,肯定是需要

光线

去照亮场景以及场景中的物体的,因此光线因素也是必不可少的。

而threejs中又把光线分成了

环境光

平行光

点光源

锥形光源

矩形光源

等诸多概念,这些我们都在项目中有实际使用到。

import SunLensflare from'./SunLensflare.js'import DirectionalLight from'./DirectionalLight.js'import AmbientLight from'./AmbientLight.js'import PointLight from'./PointLight.js'import SpotLight from'./SpotLight.js'import RectAreaLight from'./RectAreaLight.js'exportdefaultclassLights{constructor(viewer){this.viewer = viewer
    this.lightList =[]}/**
   * 添加平行光源
   * @param option
   */addDirectionalLight(position =[200,200,200], option ={color:'rgb(255,255,255)'}){const directionalLight =newDirectionalLight(this.viewer, position, option)this.lightList.push(directionalLight)return directionalLight
  }/**
   * 添加环境光源
   */addAmbientLight(){const ambientLight =newAmbientLight(this.viewer)this.lightList.push(ambientLight)return ambientLight
  }/**
   * 添加点状光源
   * @param option
   */addPointLight(position =[0,40,0], option ={color:'rgb(255,255,255)'}){const pointLight =newPointLight(this.viewer, position, option)this.lightList.push(pointLight)return pointLight
  }/**
   * 添加锥形光源
   * @param option
   */addSpotLight(position =[0,40,0], option ={color:'rgb(255,255,255)'}){const pointLight =newSpotLight(this.viewer, position, option)this.lightList.push(pointLight)return pointLight
  }/**
   * 添加矩形光源
   * @param option
   */addRectAreaLight(position =[0,40,0], option ={color:'rgb(255,255,255)'}){const rectAreaLight =newRectAreaLight(this.viewer, position, option)this.lightList.push(rectAreaLight)return rectAreaLight
  }/**
   * 添加炫光
   * @param x
   * @param y
   * @param z
   */addSunLensflare(x =200, y =200, z =200){this.sunLensflare =newSunLensflare(this.viewer)this.sunLensflare.addToScene(x, y, z)}/**
   * 移除灯光
   * @param light 灯光
   */removeLight(light){this.viewer.scene.remove(light)}}

Models模型

3D场景中,会由大大小小的各种3D模型组合而成,例如房子、道路、车、树、路灯等,这些都是加载3D模型渲染展示。

一些常见的3D模型格式

  • OBJ:这是一个非常普遍的3D模型格式,可以包含多种类型的数据,如顶点纹理坐标法线。OBJ文件通常用于交换数据,因为它们可以被多种3D软件读取和编辑。
  • FBX:Filmbox格式(FBX)是一个多功能的3D文件格式,广泛用于游戏开发和电影制作。它支持复杂的模型数据,包括动画骨骼纹理等。
  • BLEND:Blender的原生文件格式,Blender是一个开源的3D创建套件,支持全面的3D建模、动画、模拟等功能。
  • GLTFGLB:GL Transmission Format(GLTF)是一种用于3D场景和模型的高效跨平台的文件格式。它旨在为Web移动设备提供高性能的3D内容。
  • 3DS:3D Studio Max的原生格式,虽然主要用于Max软件,但也可以被其他3D软件导入和使用。

下面封装了一个threejs支持的比较好的加载模型GLTF及GLB的类:

import{ GLTFLoader }from'three/examples/jsm/loaders/GLTFLoader'import{ FBXLoader }from'three/examples/jsm/loaders/FBXLoader'import{ DRACOLoader }from'three/examples/jsm/loaders/DRACOLoader'import DsModel from'./DsModel'/**
 * 模型加载类(只能加载GLTF及GLB格式)
 */exportdefaultclassModelLoader{constructor(viewer){this.viewer = viewer
    this.scene = viewer.scene
    this.loaderGLTF =newGLTFLoader()// 加载gltf模型this.loaderFBX =newFBXLoader()// 加载fbx模型this.dracoLoader =newDRACOLoader()// 加载draco模型(加载基于Google Draco压缩格式的3D模型的类)this.dracoLoader.setDecoderPath('/js/draco/')// 设置draco模型解码器路径this.loaderGLTF.setDRACOLoader(this.dracoLoader)// 设置draco模型加载器}/**
      * 添加模型数据
      * @param url 模型的路径
      * @param callback 返回模型对象,常用一些功能挂接在模型对象上
      * @param progress 返回加载进度,还有问题,需要修改
      */loadModelToScene(url, callback, progress){this.loadModel(url,model=>{this.scene.add(model.object)// 加载模型
      callback?.(model)},num=>{
      progress?.(num)// 加载进度})}/**
    * 加载模型
    * @param url 模型路径
    * @param callback 回调模型
    * @param progress 返回加载进度
    */loadModel(url, callback, progress){let loader =this.loaderGLTF
    if(url.indexOf('.fbx')!==-1){
      loader =this.loaderFBX
    }
    loader.load(url,model=>{
      callback?.(newDsModel(model,this.viewer))},xhr=>{
      progress?.((xhr.loaded / xhr.total).toFixed(2))},(error)=>{
      console.error('模型渲染报错:', error)})}}

Event鼠标事件

再就是我们需要操作场景中的某些元素,例如:

楼体分层

,这时就需要鼠标事件去监听,点击的时候我需要计算出当前点击的具体楼层,需要用到

射线求交

等知识。

import*asTHREEfrom'three'import{ EffectComposer }from'three/examples/jsm/postprocessing/EffectComposer'exportdefaultclassThreeMouseEvent{constructor(viewer, isSelect, callback, type ='click'){this.viewer = viewer
    this.isSelect = isSelect
    this.callback = callback
    this.type = type
    this.composer =newEffectComposer(this.viewer.renderer)returnthis}startSelect(){this.stopSelect()this.bingEvent =this.#event.bind(this,this)this.viewer.renderer.domElement.addEventListener(this.type,this.bingEvent)}stopSelect(){this.viewer.renderer.domElement.removeEventListener(this.type,this.bingEvent)}#event(that, event){const raycaster =newTHREE.Raycaster()// 创建射线const mouse =newTHREE.Vector2()// 创建鼠标坐标
    mouse.x =(event.offsetX / that.viewer.renderer.domElement.clientWidth)*2-1
    mouse.y =-(event.offsetY / that.viewer.renderer.domElement.clientHeight)*2+1
    raycaster.setFromCamera(mouse, that.viewer.camera)// 设置射线的起点和终点// TODO: 第一个参数是否需要外部传入,减小监听范围const intersects = raycaster.intersectObject(that.viewer.scene,true)// 检测射线与模型是否相交if(intersects.length >0&& intersects[0]){
      that.callback(intersects[0].object, intersects[0].point)}}}

最后

以上,我只列出了实战项目里需要用到的一些关键代码,当这些基础的类封装好之后,那我们基本上就可以构建出一个简单的3D场景。

开源项目地址:https://github.com/day-day-dreamer/threejs-learning

将以上开源项目里的代码照着撸一遍,并理解其中的一些基础概念,对于threejs入门来说足够了,做一些小项目来说足以应对!

最后,如果觉得项目对你有帮助,希望可以随手点一个

star

,激励我去开源更多优秀的代码。

如果有在做

数字孪生

或者

GIS

方面的同学也可以加我交流:brown_7778,我有更多开源代码可以送你。

如果觉得文章对你有帮助,也欢迎一键三连👏👏👏,你的鼓励是支持我持续原创下去的动力~


本文转载自: https://blog.csdn.net/weixin_40580834/article/details/137480860
版权归原作者 攻城师不浪 所有, 如有侵权,请联系我们删除。

“threejs实战数字孪生园区开源(threejs+vue3+vite)”的评论:

还没有评论