threejs开发可视化数字城市效果
灵感图现在随着城市的发展,越来越多的智慧摄像头,都被互联网公司布到城市的各个角落,举一个例子,一个大楼上上下下都被布置了智能摄像头,用于监控火势,人员进出,工装工牌佩戴等监控,这时候为了美化项目,大公司都会将城市的区域作为对象,进行3d可视化交互,接下来的内容,就是基于以上元素,开发的一款城市数据可视化的demo,包含楼宇特效,飞线,特定视角,动画等交互,希望可以给大家带来一 neinei 的帮助,话不多说,开整
本文比较长,每个阶段代码都提供tag,可以分步骤查看,请耐心品尝
用到的技术栈 vite + typescript + threejs
白模下载白模模型下载网站 [上海模型](City- Shanghai-Sandboxie - Download Free 3D model by Michael Zhang (@beyond.zht) [3eab443] (sketchfab.com))
搜索关键词:city
压缩包包含的内容
模型加载模型下载的是gltf格式,所以要用到threejs 提供的 # GLTFLoader,下面是具体代码
exportfunctionloadGltf(url:string){
returnnewPromiseObject((resolve,reject)={
gltfLoader.load(url,function(gltf){
console.log('gltf',gltf)
resolve(gltf)
});
})
}
处理模型模型上有一些咱们用不到的模型,进行删除,还有一些用的到的模型,但是名称不友好,所以进行整理
loadGltf('./models/scene.gltf').then((gltf:any)={
constgroup=gltf.scene
constscale=10
group.scale.set(scale,scale,scale)
//删除多余模型
constmesh1=group.getObjectByName('Text_test-base_0')
if(mesh1mesh1.parent)mesh1.parent.remove(mesh1)
constmesh2=group.getObjectByName('Text_text_0')
if(mesh2mesh2.parent)mesh2.parent.remove(mesh2)
//重命名模型
//环球金融中心
consthqjrzx=group.getObjectByName('02-huanqiujinrongzhongxin_huanqiujinrongzhongxin_0')
if(hqjrzx)hqjrzx.name='hqjrzx'
//上海中心
constshzx=group.getObjectByName('01-shanghaizhongxindasha_shanghaizhongxindasha_0')
if(shzx)shzx.name='shzx'
//金茂大厦
constjmds=group.getObjectByName('03-jinmaodasha_jjinmaodasha_0')
if(jmds)jmds.name='jmds'
//东方明珠塔
constdfmzt=group.getObjectByName('04-dongfangmingzhu_dongfangmingzhu_0')
if(dfmzt)dfmzt.name='dfmzt'
T.scene.add(group)
T.toSceneCenter(group)
T.ray(group.children,(meshList)={
console.log('meshList',meshList);
})
T.animate()
})
T是场景的构建函数,具体可以查看 gitee中的文件,这里就不赘述了,主要创建了场景,镜头,控制器,灯光等基础信息,并且监听控制器变化时修改灯光位置
在使用第三方模型的时候,总有一些不尽人意的地方,比如模型加载后,模型中心并不在3d世界的中心位置,所以就需要调整一下模型整体的位置,toSceneCenter 方法是自定义的一个让模型居中的方法,通过# Box3获取到模型的包围盒,获取到模型的中心点坐标信息,再取反,就会得到模型中心点在3d世界的位置信息
//获取包围盒
getBoxInfo(mesh){
constbox3=newTHREE.Box3()
box3.expandByObject(mesh)
constsize=newTHREE.Vector3()
constcenter=newTHREE.Vector3()
//获取包围盒的中心点和尺寸
box3.getCenter(center)
box3.getSize(size)
return{
size,center
}
}
toSceneCenter(mesh){
const{center,size}=this.getBoxInfo(mesh)
//将Y轴置为0
mesh.position.copy(center.negate().setY(0))
}
阶段代码以上代码地址 城市加载白模 v2.0.1
飞线收集飞线的点没有3d设计师的支持,所有的数据都来自于模型,所以利用现有条件,收集飞线经过的点位,原理就是使用到的鼠标射线,点击模型上的某个位置并记录下来,提供给后期使用
众所周知,click的调用过程是忽略mousedown的,mouseup时候就会调用,如果单纯的想要改变视角,鼠标抬起时候也会调用click事件,所以要加一个鼠标是否移动的判断,利用控制器监听start和end时的镜头位置变化来区分鼠标是否移动
控制器部分代码:
this.controls.addEventListener('start',()={
this.controlsStartPos.copy(this.camera.position)
})
this.controls.addEventListener('end',()={
this.controlsMoveFlag=this.controlsStartPos.distanceToSquared(this.camera.position)===0
})
控制器开始变化的时候记录camera位置,跟结束时的camera的位置相减,如果为0,则表示鼠标没晃动,单纯的点击,如果不为0,说明镜头位置变化了,这时,鼠标的click回调将不会调用
射线部分代码:
ray(children:THREE.Object3D[],callback:(mesh:THREE.IntersectionTHREE.Object3DTHREE.Event[])=void){
letmouse=newTHREE.Vector2();//鼠标位置
varraycaster=newTHREE.Raycaster();
window.addEventListener("click",(event)={
mouse.x=(event.clientX/document.body.offsetWidth)*2-1;
mouse.y=-(event.clientY/document.body.offsetHeight)*2+1;
raycaster.setFromCamera(mouse,this.camera);
constrallyist=raycaster.intersectObjects(children);
if(this.controlsMoveFlag){
callbackcallback(rallyist)
}
}
射线的回调:
letarr=[]
T.ray(group.children,(meshList)={
console.log('meshList',meshList);
arr.push(...meshList[0].point.toArray())
console.log(JSON.stringify(arr));
})
收集后的顶点信息:
这部分的工作和之前写 # threejs 打造 world.ipanda.com 同款3D首页时候收集熊猫基地的点位是一样的,只不过判断鼠标是否移动的部分不一样而已。
细化顶点有了飞线具体经过的点位时候,要将这些点位细化,这时就要讲飞线的大致原理了,两点确定一条线段,获取线段上的100个点,每条飞线占用20个点位,每个点位创建一个着色器,用于绘制飞线的组成部分,当更新时候,飞线的首个点向下一个点前进,一次往后20个点都往前前进一次,循环往复一直到飞线的最后一个组成部分到达线段的最后一个点,飞线占用的点位数量决定飞线的长度,将线段分为多少个顶点,决定飞线的疏密程度,像图中这样的疏密度,就是单个线段的点位分少了,这个可以优化的,vector3.distanceTo(vector3)即可判断两个线段的长度,通过不同的长度,决定细化线段的点,当然,线段的顶点信息越多,对gpu的消耗越大
flyLineData.forEach((data:number[])={
constpoints:THREE.Vector2[]=[]
for(leti=0;idata.length/3;i++){
constx=data[i*3]
constz=data[i*3+2]
constpoint=newTHREE.Vector2(x,z)
points.push(point)
}
constcurve=newTHREE.SplineCurve(points);
//此处决定飞线每个点的疏密程度,数值越大,对gpu的压力越大
constcurvePoints=curve.getPoints(100);
constflyPoints=curvePoints.map((curveP:THREE.Vector2)=newTHREE.Vector3(curveP.x,0,curveP.y))
//constl=points.length-1
constflyGroup=T._Fly.setFly({
index:Math.random()0.5?50:20,
num:20,
points:flyPoints,
spaced:50,//要将曲线划分为的分段数。默认是 5
starColor:newTHREE.Color(Math.random()*0xffffff),
endColor:newTHREE.Color(Math.random()*0xffffff),
size:0.5
})
flyLineGroup.add(flyGroup)
})
setFly参数
interfaceSetFly{
index:number,//截取起点
num:number,//截取长度//要小于length
points:Vector3[],
spaced:number//要将曲线划分为的分段数。默认是 5
starColor:Color,
endColor:Color,
size:number
}
endColor和starColor目前不好用,做不出渐变,不知道是不是长度不够,暂时先放放
flyLine创建flyLine做成了一个类,开箱即用,也可以加入自己的想法,调整内容,
创建flyLine之后要在render中调用
render(){
this.controls.update()
this.renderer.render(this.scene,this.camera);
this._Flythis._Fly.upDate()
}
new Fly() 方法详见飞线fly.ts
可配置参数有尺寸,透明度,颜色等
varcolor1=params.starColor;//轨迹线颜色青色
varcolor2=params.endColor;//黄色
varcolor=color1.lerp(color2,i/newPoints2.length)
colorArr.push(color.r,color.g,color.b);
这里是引用渐变色的位置,需要再调整一下
阶段代码以上代码地址 城市飞线 v2.0.2
线稿将模型绘制出线稿,并添加到原有模型上,这里用到LineBasicMaterial基础线条材质,和MeshLambertMaterial基础网格材质,调节颜色和不透明度。
材质代码:
//建筑材质
exportconstotherBuildingMaterial=(color:THREE.Color,opacity=1)={
returnnewTHREE.MeshLambertMaterial({
color,
transparent:true,
opacity
}
//建筑线条材质
exportconstotherBuildingLineMaterial=(color:THREE.Color,opacity=1)={
returnnewTHREE.LineBasicMaterial(
{
color,
depthTest:true,
transparent:true,
opacity
}
)
}
以下代码是之前对模型改造时写的对模型重命名的方法,现在我们来改造一下
//重命名模型
//环球金融中心
consthqjrzx=group.getObjectByName('02-huanqiujinrongzhongxin_huanqiujinrongzhongxin_0')
if(hqjrzx){
hqjrzx.name='hqjrzx'
changeModelMaterial(hqjrzx,otherBuildingMaterial(buildColor,buildOpacity),otherBuildingLineMaterial(buildLineColor,buildLineOpacity),buildLineDeg)
}
//上海中心
constshzx=group.getObjectByName('01-shanghaizhongxindasha_shanghaizhongxindasha_0')
if(shzx){
shzx.name='shzx'
changeModelMaterial(shzx,otherBuildingMaterial(buildColor,buildOpacity),otherBuildingLineMaterial(buildLineColor,buildLineOpacity),buildLineDeg)
}
//金茂大厦
constjmds=group.getObjectByName('03-jinmaodasha_jjinmaodasha_0')
if(jmds){
jmds.name='jmds'
changeModelMaterial(jmds,otherBuildingMaterial(buildColor,buildOpacity),otherBuildingLineMaterial(buildLineColor,buildLineOpacity),buildLineDeg)
}
//东方明珠塔
constdfmzt=group.getObjectByName('04-dongfangmingzhu_dongfangmingzhu_0')
if(dfmzt){
dfmzt.name='dfmzt'
changeModelMaterial(dfmzt,otherBuildingMaterial(buildColor,buildOpacity),otherBuildingLineMaterial(buildLineColor,buildLineOpacity),buildLineDeg)
}
T.scene.add(group)
T.toSceneCenter(group)
group.traverse((mesh:any)={
meshasTHREE.Mesh
if(mesh.isMesh(mesh.name.indexOf('Shanghai')!==-1||mesh.name.indexOf('Object')!==-1)){
if(mesh.name.indexOf('Floor')!==-1){
mesh.material=floorMaterial
}elseif(mesh.name.indexOf('River')!==-1){
}else{
changeModelMaterial(mesh,otherBuildingMaterial(otherBuildColor,0.8),otherBuildingLineMaterial(otherBuildLineColor,0.4),buildLineDeg)
}
}
})
changeModelMaterial这个方法就是创建模型相对应的线条的方法,获取到模型的geometry,这里存着模型所有的顶点信息,索引和法向量,以此创建一个# 边缘几何体(EdgesGeometry);通过边缘几何体的信息创建 # 线段(LineSegments);并将创建出来的线段添加到原有模型中,因为我们的线段不需要单独处理,所以这里写的方法会比之前在# threejs渲染高级感可视化涡轮模型 一文中写的简化的很多,如果需要单独对线段处理的同学,可以采用这篇文章里的 changeModelMaterial 方法
/**
*
*@paramobject模型
*@paramlineGroup线组
*@parammeshMaterial模型材质
*@paramlineMaterial线材质
*/
exportconstchangeModelMaterial=(mesh:THREE.Mesh,meshMaterial:THREE.MeshBasicMaterial,lineMaterial:THREE.LineBasicMaterial,deg=1):any={
if(mesh.isMesh){
if(meshMaterial)mesh.material=meshMaterial
//以模型顶点信息创建线条
constline=getLine(mesh,deg,lineMaterial)
constname=mesh.name+'_line'
line.name=name
mesh.add(line)
}
}
//通过模型创建线条
exportconstgetLine=(object:THREE.Mesh,thresholdAngle=1,lineMaterial:THREE.LineBasicMaterial):THREE.LineSegments={
//创建线条,参数为几何体模型,相邻面的法线之间的角度,
varedges=newTHREE.EdgesGeometry(object.geometry,thresholdAngle);
varline=newTHREE.LineSegments(edges);
if(lineMaterial)line.material=lineMaterial
returnline;
}
关于颜色对于我这种野生前端开发,没有UI和UE的支持,只能在网上找案例,那么就需要图片中的颜色,这里不得不提到一个工具色輪、調色盤產生器 | Adobe Color
色彩这里可以根据一个颜色,调出互补色、相似色、单色等色彩信息
取色这个工具也可以根据一张图片,提取出主题色,包含主色、辅助色等信息
阶段代码以上代码地址 城市线稿轮廓
预设镜头位置
预埋点位预埋的点位坐标信息获取和飞线点位获取一样的方法,标记采用的是# CSS2DRenderer,将创建的element节点渲染到3d世界,3drender和2drender不在同一个图层内,所以需要新建一个dom节点,专门存放css2d的dom信息,
divid="css2dRender"/div
加载css2drendercreateScene 文件
+renderCss2D:CSS2DRenderer
createRenderer(){
...
this.renderCss2D=newCSS2DRenderer({element:this.css2dDom
this.renderCss2D.setSize(this.width,this.height);
...
}
render(){
...
this.renderCss2D.render(this.scene,this.camera);
...
}
根据数据创建dom节点exportinterfaceCameraPosInfo{
pos:number[],//预设摄像机位置信息
target:number[],//控制器目标位置
name:string,//预埋标记点或其他信息
tagPos?:number[],//预埋标记点的位置信息
}
接下来就是要根据信息创建节点,遍历这些信息,并创建节点,这里有一个点需要提一下,2d图层和3d图层的关系,这里要是不介绍清楚,后面没法进行
从图中可以看出,2d图层始终保持在3d图层的上层,然而我们在创建控制器的时候,第二个参数使用的是3d的图层,this.controls = new OrbitControls(this.camera, this.renderer.domElement),因为这一层被覆盖了,所以控制器失效了。
有两种解决方案,第一种是 new OrbitControls时,将第二个参数改为this.renderCss2D.domElement,还有一种方式,也就是本文采用的方式,将2d图层的css属性改变一下,忽略这个图层的任何事件。
#css2dRender{
/*一定要加这个属性,不然2D内容点击没效果*/
pointer-events:none;
}
由于pointer-events属性是可以继承的,2d图层内所有的元素都不响应事件,所以要将咱们创建的建筑tag的样式改一下
.build_tag{
/*一定要加这个属性,不然2D内容点击没效果*/
pointer-events:
}
第一次写这方面的代码的时候,也是头疼了好久,慢慢摸索才摸索出来的。
//创建建筑标记
functioncreateTag(){
constbuildTagGroup=newTHREE.Group()
T.scene.add(buildTagGroup)
presetsCameraPos.forEach((cameraPos:CameraPosInfo,i:number)={
if(cameraPos.tagPos){
//渲染2d文字
constelement=document.createElement('li');
//将信息存入dom节点中,如果是react或者vue写的,不用这么存,直接存data或者state
element.setAttribute('data-cameraPosInfo',JSON.stringify(cameraPos))
element.classList.add('build_tag')
element.innerText=`${i+1}`
//将初始化好的dom节点渲染成CSS2DObject,并在scene场景中渲染
consttag=newCSS2DObject(element);
consttagPos=newTHREE.Vector3().fromArray(cameraPos.tagPos)
tag.position.copy(tagPos)
buildTagGroup.add(tag)
}
})
}
镜头动画这里通过事件代理,点击到相应的建筑tag,从dom节点上获取到data-cameraPosInfo属性,然后通过tween动画处理器修改控制器的taget和镜头的position。事件代理是js基础内容,这里就不赘述了
if(css2dDom){
css2dDom.addEventListener('click',function(e){
if(e.target){
if(e.target.nodeName==='LI'){
console.dir(e);
constcameraPosInfo=e.target.getAttribute('data-cameraPosInfo')
if(cameraPosInfo){
const{pos,target}=JSON.parse(cameraPosInfo)
T.controls.target.set(...target)
T.handleCameraPos(pos)
}
}
}
}
handleCameraPos的代码
handleCameraPos(end:number[]){
//结束时候相机位置
constendV3=newTHREE.Vector3().fromArray(end)
//目前相机到目标位置的距离,根据不同的位置判断运动的时间长度,从而保证速度不变
constlength=this.camera.position.distanceTo(endV3)
//如果位置相同,不运行动画
if(length===0)return
newthis._TWEEN.Tween(this.camera.position)
.to(endV3,Math.sqrt(length)*400)
.start()
//.onUpdate((value)={
//console.log(value)
//})
.onComplete(()={
//动画结束的回调,可以展示建筑信息或其他操作
})
}
阶段代码以上代码地址 建筑镜头动画 v2.0.4
场景背景渲染scene的场景不仅支持颜色和texture纹理,还支持canvas,上面的黑色背景太单调了,所以利用canvas绘制一个圆渐变填充到scene.background
createScene(){
...
constdrawingCanvas=document.createElement('canvas');
constcontext=drawingCanvas.getContext('2d');
if(context){
//设置canvas的尺寸
drawingCanvas.width=this.width;
drawingCanvas.height=this.height;
//创建渐变
constgradient=context.createRadialGradient(this.width/2,this.height,0,this.width/2,this.height/2,Math.max(this.width,this.height));
//为渐变添加颜色
gradient.addColorStop(0,'#0b171f');
gradient.addColorStop(0.6,'#000000');
//使用渐变填充矩形
context.fillStyle=gradient;
context.fillRect(0,0,drawingCanvas.width,drawingCanvas.height);
this.scene.background=newTHREE.CanvasTexture(drawingCanvas)
...
}
其他风格
完整代码地址
历史文章# threejs渲染高级感可视化涡轮模型# 写一个高德地图巡航功能的小DEMO
# three.js 打造游戏小场景(拾取武器、领取任务、刷怪)
# threejs 打造 world.ipanda.com 同款3D首页
# three.js——物理引擎
# three.js——镜头跟踪
# threejs 笔记 03 —— 轨道控制器
# Javascript基础之有趣的文字效果
# Javascript基础之写一个好玩的点击效果
# Javascript基础之鼠标拖拉拽
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线