当前位置:   article > 正文

threejs!可视化数字城市效果的实现_three.js 城市

three.js 城市
灵感图

image.png

现在随着城市的发展,越来越多的智慧摄像头,都被互联网公司布到城市的各个角落,举一个例子,一个大楼上上下下都被布置了智能摄像头,用于监控火势,人员进出,工装工牌佩戴等监控,这时候为了美化项目,大公司都会将城市的区域作为对象,进行3d可视化交互,接下来的内容,就是基于以上元素,开发的一款城市数据可视化的demo,包含楼宇特效,飞线,特定视角,动画等交互,

用到的技术栈 vite + typescript + threejs

白模
下载白模

搜索关键词:city

压缩包包含的内容

image.png

模型加载

下面是具体代码

  1. export function loadGltf(url: string) {
  2. return new Promise<Object>((resolve, reject) => {
  3. gltfLoader.load(url, function (gltf) {
  4. console.log('gltf',gltf)
  5. resolve(gltf)
  6. });
  7. })
  8. }
处理模型

模型上有一些咱们用不到的模型,进行删除,还有一些用的到的模型,但是名称不友好,所以进行整理

  1. loadGltf('./models/scene.gltf').then((gltf: any) => {
  2. const group = gltf.scene
  3. const scale = 10
  4. group.scale.set(scale, scale, scale)
  5. // 删除多余模型
  6. const mesh1 = group.getObjectByName('Text_test-base_0')
  7. if (mesh1 && mesh1.parent) mesh1.parent.remove(mesh1)
  8. const mesh2 = group.getObjectByName('Text_text_0')
  9. if (mesh2 && mesh2.parent) mesh2.parent.remove(mesh2)
  10. // 重命名模型
  11. // 环球金融中心
  12. const hqjrzx = group.getObjectByName('02-huanqiujinrongzhongxin_huanqiujinrongzhongxin_0')
  13. if (hqjrzx) hqjrzx.name = 'hqjrzx'
  14. // 上海中心
  15. const shzx = group.getObjectByName('01-shanghaizhongxindasha_shanghaizhongxindasha_0')
  16. if (shzx) shzx.name = 'shzx'
  17. // 金茂大厦
  18. const jmds = group.getObjectByName('03-jinmaodasha_jjinmaodasha_0')
  19. if (jmds) jmds.name = 'jmds'
  20. // 东方明珠塔
  21. const dfmzt = group.getObjectByName('04-dongfangmingzhu_dongfangmingzhu_0')
  22. if (dfmzt) dfmzt.name = 'dfmzt'
  23. T.scene.add(group)
  24. T.toSceneCenter(group)
  25. T.ray(group.children, (meshList) => {
  26. console.log('meshList', meshList);
  27. })
  28. T.animate()
  29. })

T是场景的构建函数,主要创建了场景,镜头,控制器,灯光等基础信息,并且监听控制器变化时修改灯光位置

image.png

在使用第三方模型的时候,总有一些不尽人意的地方,比如模型加载后,模型中心并不在3d世界的中心位置,所以就需要调整一下模型整体的位置,toSceneCenter 方法是自定义的一个让模型居中的方法,通过BOX3获取到模型的包围盒,获取到模型的中心点坐标信息,再取反,就会得到模型中心点在3d世界的位置信息

  1. // 获取包围盒
  2. getBoxInfo(mesh) {
  3. const box3 = new THREE.Box3()
  4. box3.expandByObject(mesh)
  5. const size = new THREE.Vector3()
  6. const center = new THREE.Vector3()
  7. // 获取包围盒的中心点和尺寸
  8. box3.getCenter(center)
  9. box3.getSize(size)
  10. return {
  11. size, center
  12. }
  13. }
  14. toSceneCenter(mesh) {
  15. const { center, size } = this.getBoxInfo(mesh)
  16. // 将Y轴置为0
  17. mesh.position.copy(center.negate().setY(0))
  18. }
飞线
收集飞线的点

没有3d设计师的支持,所有的数据都来自于模型,所以利用现有条件,收集飞线经过的点位,原理就是使用到的鼠标射线,点击模型上的某个位置并记录下来,提供给后期使用

众所周知,click的调用过程是忽略mousedown的,mouseup时候就会调用,如果单纯的想要改变视角,鼠标抬起时候也会调用click事件,所以要加一个鼠标是否移动的判断,利用控制器监听start和end时的镜头位置变化来区分鼠标是否移动

控制器部分代码:

  1. this.controls.addEventListener('start', () => {
  2. this.controlsStartPos.copy(this.camera.position)
  3. })
  4. this.controls.addEventListener('end', () => {
  5. this.controlsMoveFlag = this.controlsStartPos.distanceToSquared(this.camera.position) === 0
  6. })

控制器开始变化的时候记录camera位置,跟结束时的camera的位置相减,如果为0,则表示鼠标没晃动,单纯的点击,如果不为0,说明镜头位置变化了,这时,鼠标的click回调将不会调用

射线部分代码:

  1. ray(children: THREE.Object3D[], callback: (mesh: THREE.Intersection<THREE.Object3D<THREE.Event>>[]) => void) {
  2. let mouse = new THREE.Vector2(); //鼠标位置
  3. var raycaster = new THREE.Raycaster();
  4. window.addEventListener("click", (event) => {
  5. mouse.x = (event.clientX / document.body.offsetWidth) * 2 - 1;
  6. mouse.y = -(event.clientY / document.body.offsetHeight) * 2 + 1;
  7. raycaster.setFromCamera(mouse, this.camera);
  8. const rallyist = raycaster.intersectObjects(children);
  9. if (this.controlsMoveFlag) {
  10. callback && callback(rallyist)
  11. }
  12. });
  13. }

射线的回调:

  1. let arr = []
  2. T.ray(group.children, (meshList) => {
  3. console.log('meshList', meshList);
  4. arr.push(...meshList[0].point.toArray())
  5. console.log(JSON.stringify(arr));
  6. })

收集后的顶点信息:

image.png

这部分的工作只不过判断鼠标是否移动的部分不一样而已。

细化顶点

有了飞线具体经过的点位时候,要将这些点位细化,这时就要讲飞线的大致原理了,两点确定一条线段,获取线段上的100个点,每条飞线占用20个点位,每个点位创建一个着色器,用于绘制飞线的组成部分,当更新时候,飞线的首个点向下一个点前进,一次往后20个点都往前前进一次,循环往复一直到飞线的最后一个组成部分到达线段的最后一个点,飞线占用的点位数量决定飞线的长度,将线段分为多少个顶点,决定飞线的疏密程度,像图中这样的疏密度,就是单个线段的点位分少了,这个可以优化的,vector3.distanceTo(vector3)即可判断两个线段的长度,通过不同的长度,决定细化线段的点,当然,线段的顶点信息越多,对gpu的消耗越大

  1. flyLineData.forEach((data: number[]) => {
  2. const points: THREE.Vector2[] = []
  3. for (let i = 0; i < data.length / 3; i++) {
  4. const x = data[i * 3]
  5. const z = data[i * 3 + 2]
  6. const point = new THREE.Vector2(x, z)
  7. points.push(point)
  8. }
  9. const curve = new THREE.SplineCurve(points);
  10. // 此处决定飞线每个点的疏密程度,数值越大,对gpu的压力越大
  11. const curvePoints = curve.getPoints(100);
  12. const flyPoints = curvePoints.map((curveP: THREE.Vector2) => new THREE.Vector3(curveP.x, 0, curveP.y))
  13. // const l = points.length - 1
  14. const flyGroup = T._Fly.setFly({
  15. index: Math.random() > 0.5 ? 50 : 20,
  16. num: 20,
  17. points: flyPoints,
  18. spaced: 50, // 要将曲线划分为的分段数。默认是 5
  19. starColor: new THREE.Color(Math.random() * 0xffffff),
  20. endColor: new THREE.Color(Math.random() * 0xffffff),
  21. size: 0.5
  22. })
  23. flyLineGroup.add(flyGroup)
  24. })

setFly参数

  1. interface SetFly {
  2. index: number, // 截取起点
  3. num: number, // 截取长度 // 要小于length
  4. points: Vector3[],
  5. spaced: number // 要将曲线划分为的分段数。默认是 5
  6. starColor: Color,
  7. endColor: Color,
  8. size: number
  9. }

endColor和starColor目前不好用,做不出渐变,不知道是不是长度不够,暂时先放放

flyLine

创建flyLine做成了一个类,开箱即用,也可以加入自己的想法,调整内容,

创建flyLine之后要在render中调用

  1. render() {
  2. this.controls.update()
  3. this.renderer.render(this.scene, this.camera);
  4. this._Fly && this._Fly.upDate()
  5. }

可配置参数有尺寸,透明度,颜色等

  1. var color1 = params.starColor; //轨迹线颜色 青色
  2. var color2 = params.endColor; //黄色
  3. var color = color1.lerp(color2, i / newPoints2.length)
  4. colorArr.push(color.r, color.g, color.b);

这里是引用渐变色的位置,需要再调整一下

2023-11-21 17.48.02.gif

阶段代码
线稿

将模型绘制出线稿,并添加到原有模型上,这里用到LineBasicMaterial基础线条材质,和MeshLambertMaterial基础网格材质,调节颜色和不透明度。

材质代码:

  1. // 建筑材质
  2. export const otherBuildingMaterial = (color: THREE.Color, opacity = 1) => {
  3. return new THREE.MeshLambertMaterial({
  4. color,
  5. transparent: true,
  6. opacity
  7. });
  8. }
  9. // 建筑线条材质
  10. export const otherBuildingLineMaterial = (color: THREE.Color, opacity = 1) => {
  11. return new THREE.LineBasicMaterial(
  12. {
  13. color,
  14. depthTest: true,
  15. transparent: true,
  16. opacity
  17. }
  18. )
  19. }

以下代码是之前对模型改造时写的对模型重命名的方法,现在我们来改造一下

  1. // 重命名模型
  2. // 环球金融中心
  3. const hqjrzx = group.getObjectByName('02-huanqiujinrongzhongxin_huanqiujinrongzhongxin_0')
  4. if (hqjrzx) {
  5. hqjrzx.name = 'hqjrzx'
  6. changeModelMaterial(hqjrzx, otherBuildingMaterial(buildColor, buildOpacity), otherBuildingLineMaterial(buildLineColor, buildLineOpacity),buildLineDeg)
  7. }
  8. // 上海中心
  9. const shzx = group.getObjectByName('01-shanghaizhongxindasha_shanghaizhongxindasha_0')
  10. if (shzx) {
  11. shzx.name = 'shzx'
  12. changeModelMaterial(shzx, otherBuildingMaterial(buildColor, buildOpacity), otherBuildingLineMaterial(buildLineColor, buildLineOpacity),buildLineDeg)
  13. }
  14. // 金茂大厦
  15. const jmds = group.getObjectByName('03-jinmaodasha_jjinmaodasha_0')
  16. if (jmds) {
  17. jmds.name = 'jmds'
  18. changeModelMaterial(jmds, otherBuildingMaterial(buildColor, buildOpacity), otherBuildingLineMaterial(buildLineColor, buildLineOpacity),buildLineDeg)
  19. }
  20. // 东方明珠塔
  21. const dfmzt = group.getObjectByName('04-dongfangmingzhu_dongfangmingzhu_0')
  22. if (dfmzt) {
  23. dfmzt.name = 'dfmzt'
  24. changeModelMaterial(dfmzt, otherBuildingMaterial(buildColor, buildOpacity), otherBuildingLineMaterial(buildLineColor, buildLineOpacity),buildLineDeg)
  25. }
  26. T.scene.add(group)
  27. T.toSceneCenter(group)
  28. group.traverse((mesh: any) => {
  29. mesh as THREE.Mesh
  30. if (mesh.isMesh && (mesh.name.indexOf('Shanghai') !== -1 || mesh.name.indexOf('Object') !== -1)) {
  31. if (mesh.name.indexOf('Floor') !== -1) {
  32. mesh.material = floorMaterial
  33. } else if (mesh.name.indexOf('River') !== -1) {
  34. } else {
  35. changeModelMaterial(mesh, otherBuildingMaterial(otherBuildColor,0.8), otherBuildingLineMaterial(otherBuildLineColor,0.4),buildLineDeg)
  36. }
  37. }
  38. })

changeModelMaterial这个方法就是创建模型相对应的线条的方法,获取到模型的geometry,这里存着模型所有的顶点信息,索引和法向量,以此创建一个# 边缘几何体(EdgesGeometry)边缘几何体(EdgesGeometry)# 边缘几何体(EdgesGeometry);通过边缘几何体的信息创建 # 线段(LineSegments)线段(LineSegments) # 线段(LineSegments);并将创建出来的线段添加到原有模型中,因为我们的线段不需要单独处理,

  1. /**
  2. *
  3. * @param object 模型
  4. * @param lineGroup 线组
  5. * @param meshMaterial 模型材质
  6. * @param lineMaterial 线材质
  7. */
  8. export const changeModelMaterial = (mesh: THREE.Mesh, meshMaterial: THREE.MeshBasicMaterial, lineMaterial: THREE.LineBasicMaterial, deg = 1): any => {
  9. if (mesh.isMesh) {
  10. if (meshMaterial) mesh.material = meshMaterial
  11. // 以模型顶点信息创建线条
  12. const line = getLine(mesh, deg, lineMaterial)
  13. const name = mesh.name + '_line'
  14. line.name = name
  15. mesh.add(line)
  16. }
  17. }
  18. // 通过模型创建线条
  19. export const getLine = (object: THREE.Mesh, thresholdAngle = 1, lineMaterial: THREE.LineBasicMaterial): THREE.LineSegments => {
  20. // 创建线条,参数为 几何体模型,相邻面的法线之间的角度,
  21. var edges = new THREE.EdgesGeometry(object.geometry, thresholdAngle);
  22. var line = new THREE.LineSegments(edges);
  23. if (lineMaterial) line.material = lineMaterial
  24. return line;
  25. }

关于颜色

对于我这种野生前端开发,没有UI和UE的支持,只能在网上找案例,那么就需要图片中的颜色,这里不得不提到一个工具色輪、調色盤產生器 | Adobe Color

色彩

这里可以根据一个颜色,调出互补色、相似色、单色等色彩信息

image.png

取色

这个工具也可以根据一张图片,提取出主题色,包含主色、辅助色等信息

image.png

阶段代码
预设镜头位置

image.png

预埋点位

预埋的点位坐标信息获取和飞线点位获取一样的方法,标记采用的是CSS2DRenderer,将创建的element节点渲染到3d世界,3drender和2drender不在同一个图层内,所以需要新建一个dom节点,专门存放css2d的dom信息,

    <div id="css2dRender"></div>
加载css2drender

createScene 文件

  1. +renderCss2D: CSS2DRenderer
  2. createRenderer(){
  3. ...
  4. this.renderCss2D = new CSS2DRenderer({ element: this.css2dDom });
  5. this.renderCss2D.setSize(this.width, this.height);
  6. ...
  7. }
  8. render(){
  9. ...
  10. this.renderCss2D.render(this.scene, this.camera);
  11. ...
  12. }

 

根据数据创建dom节点

image.png

  1. export interface CameraPosInfo {
  2. pos: number[], // 预设摄像机位置信息
  3. target: number[], // 控制器目标位置
  4. name: string, // 预埋标记点或其他信息
  5. tagPos?: number[], // 预埋标记点的位置信息
  6. }

接下来就是要根据信息创建节点,遍历这些信息,并创建节点,这里有一个点需要提一下,2d图层和3d图层的关系

image.png

从图中可以看出,2d图层始终保持在3d图层的上层,然而我们在创建控制器的时候,第二个参数使用的是3d的图层, this.controls = new OrbitControls(this.camera, this.renderer.domElement),因为这一层被覆盖了,所以控制器失效了。

有两种解决方案,第一种是 new OrbitControls时,将第二个参数改为this.renderCss2D.domElement,还有一种方式,也就是本文采用的方式,将2d图层的css属性改变一下,忽略这个图层的任何事件。

  1. #css2dRender {
  2. /* 一定要加这个属性,不然2D内容点击没效果 */
  3. pointer-events: none;
  4. }

由于pointer-events属性是可以继承的,2d图层内所有的元素都不响应事件,所以要将咱们创建的建筑tag的样式改一下

  1. .build_tag {
  2. /* 一定要加这个属性,不然2D内容点击没效果 */
  3. pointer-events: all;
  4. }

 

  1. // 创建建筑标记
  2. function createTag() {
  3. const buildTagGroup = new THREE.Group()
  4. T.scene.add(buildTagGroup)
  5. presetsCameraPos.forEach((cameraPos: CameraPosInfo, i: number) => {
  6. if (cameraPos.tagPos) {
  7. // 渲染2d文字
  8. const element = document.createElement('li');
  9. // 将信息存入dom节点中,如果是react或者vue写的,不用这么存,直接存data或者state
  10. element.setAttribute('data-cameraPosInfo', JSON.stringify(cameraPos))
  11. element.classList.add('build_tag')
  12. element.innerText = `${i + 1}`
  13. // 将初始化好的dom节点渲染成CSS2DObject,并在scene场景中渲染
  14. const tag = new CSS2DObject(element);
  15. const tagPos = new THREE.Vector3().fromArray(cameraPos.tagPos)
  16. tag.position.copy(tagPos)
  17. buildTagGroup.add(tag)
  18. }
  19. })
  20. }

镜头动画

这里通过事件代理,点击到相应的建筑tag,从dom节点上获取到data-cameraPosInfo属性,然后通过tween动画处理器修改控制器的taget和镜头的position。事件代理是js基础内容,

 

  1. if (css2dDom) {
  2. css2dDom.addEventListener('click', function (e) {
  3. if (e.target) {
  4. if(e.target.nodeName=== 'LI') {
  5. console.dir(e);
  6. const cameraPosInfo = e.target.getAttribute('data-cameraPosInfo')
  7. if (cameraPosInfo) {
  8. const {pos,target} = JSON.parse(cameraPosInfo)
  9. T.controls.target.set(...target)
  10. T.handleCameraPos(pos)
  11. }
  12. }
  13. }
  14. });
  15. }

handleCameraPos的代码

  1. handleCameraPos(end: number[]) {
  2. // 结束时候相机位置
  3. const endV3 = new THREE.Vector3().fromArray(end)
  4. // 目前相机到目标位置的距离,根据不同的位置判断运动的时间长度,从而保证速度不变
  5. const length = this.camera.position.distanceTo(endV3)
  6. // 如果位置相同,不运行动画
  7. if(length===0) return
  8. new this._TWEEN.Tween(this.camera.position)
  9. .to(endV3, Math.sqrt(length) * 400)
  10. .start()
  11. // .onUpdate((value) => {
  12. // console.log(value)
  13. // })
  14. .onComplete(() => {
  15. // 动画结束的回调,可以展示建筑信息或其他操作
  16. })
  17. }

2023-11-22 18.22.12.gif

阶段代码
场景背景渲染

scene的场景不仅支持颜色和texture纹理,还支持canvas,上面的黑色背景太单调了,所以利用canvas绘制一个圆渐变填充到scene.background

  1. createScene(){
  2. ...
  3. const drawingCanvas = document.createElement('canvas');
  4. const context = drawingCanvas.getContext('2d');
  5. if (context) {
  6. // 设置canvas的尺寸
  7. drawingCanvas.width = this.width;
  8. drawingCanvas.height = this.height;
  9. // 创建渐变
  10. const gradient = context.createRadialGradient(this.width / 2, this.height, 0, this.width/2, this.height/2, Math.max(this.width, this.height));
  11. // 为渐变添加颜色
  12. gradient.addColorStop(0, '#0b171f');
  13. gradient.addColorStop(0.6, '#000000');
  14. // 使用渐变填充矩形
  15. context.fillStyle = gradient;
  16. context.fillRect(0, 0, drawingCanvas.width, drawingCanvas.height);
  17. this.scene.background = new THREE.CanvasTexture(drawingCanvas)
  18. ...
  19. }

 

其他风格

image.png

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/867812
推荐阅读
相关标签
  

闽ICP备14008679号