这篇文章解释了如何使用代码来编写一座3D立体“城市”。这个代码是由@ mrdoob最新发布的演示Demo。我发现这个演示的算法很优雅,是一个简单而有效的解决方案,所以我发了一个帖子解释它。
关于算法的一些评论
在我们将关注焦点置于问题的细节之前,把握下问题的整体和全局是很有帮助的。这个3D虚拟城市所使用的算法是完全由程序所生成的,这意味着整个城市 是动态建立,而不参考任何模板。这个算法相当优雅,且不超过100行javascript代码。这个算法的原理是怎么样的呢?简而言之,每一个建筑是一个 立方体,他们得到随机的大小和位置。足够简单吗?听起来好像不切实际,但事实就是这样的,当你从城市底部往上看时就会发现这个秘密。
从性能的角度来看,所有的建筑都合并成一个单一的几何形状,用一个单一的材料。这是做法是非常有效的,因为没有着色器切换和绘图调用命令。
为了提高真实感,通过模拟自然光使用了vertexColor
小把戏。在这个城市,在街道级别里你可以看到来自其他建筑物的阴影,所以建筑物的底部比顶部暗,我们可以采用vertexColor
来重现这样的效果。我们采取建筑物的底部顶点,并使其比顶部更暗。
让我们开始吧
我们将逐步解释那100行代码:(1)生成建筑的基础几何形状 ;(2)在城市的合适位置放置建筑物;(3)使用vertexColor技巧模拟环境光和阴影;(4)合并所有的建筑物,这样整个城市可以在一次性绘制。不多说,让我们开始吧!
生成建筑的基础几何形状
我们首先需要建立构建城市建筑的基础几何形状,它会重复使用多次,然后构建起整个城市。所以我们建立了一个简单的CubeGeometry
对象。
var geometry = new THREE.CubeGeometry( 1, 1, 1 );
我们将参考点设置在立方体的底部,而不是它的中心,以便我们进行平移操作。
geometry.applyMatrix( new THREE.Matrix4().makeTranslation( 0, 0.5, 0 ) );
然后我们去掉立方体的底面,这是一个优化小技巧。因为建筑物的底面是不可见的,因为它始终是在地面上。所以它是无用的,我们将其删除。
geometry.faces.splice( 3, 1 );
现在我们修复顶面的UV映射,我们将它们设置为单一的坐标(0,0),这样屋顶将和地板颜色相同,且建筑物的各面纹理共用,这样使得我们可以可以在单一的绘图过程中完成绘制。这也是优化绘制的小技巧。
geometry.faceVertexUvs[0][2][0].set( 0, 0 );
geometry.faceVertexUvs[0][2][1].set( 0, 0 );
geometry.faceVertexUvs[0][2][2].set( 0, 0 );
geometry.faceVertexUvs[0][2][3].set( 0, 0 );
好了,现在我们得到了一个单体建筑的几何形状,让我们绘制更多的建筑物,让它看起来更像一个城市!
在城市的合适位置放置建筑物
嗯……说实话,我们可以把他们放在任何地方。全部是随机。但是要小心,因为建筑和建筑之间会发生碰撞。但不管怎样,我们先把建筑放在随机位置。
buildingMesh.position.x = Math.floor( Math.random() * 200 - 100 ) * 10;
buildingMesh.position.z = Math.floor( Math.random() * 200 - 100 ) * 10;
然后我们在Y方向做一个随机的旋转:
buildingMesh.rotation.y = Math.random()*Math.PI*2;
然后,我们通过设置mesh.scale
属性来改变建筑的大小。首先是如何建筑的宽度和深度。
buildingMesh.scale.x = Math.random()*Math.random()*Math.random()*Math.random() * 50 + 10;
buildingMesh.scale.z = buildingMesh.scale.x
然后是建筑物的高度:
buildingMesh.scale.y = (Math.random() * Math.random() * Math.random() * buildingMesh.scale.x) * 8 + 8;
我们设置好了建筑物的位置/旋转/缩放等属性。现在让我们设置它的颜色,以及如何使用它来模拟阴影。
使用vertexColor技巧模拟环境光和阴影
在建筑丛生的大城市,建筑的底部往往比顶端更暗。这是因为太阳光线照射到建筑物顶部比底部更容易,而且在建筑物底部往往由来自其它建筑物的阴影,这 是在图形编程中称之为环境光遮蔽(Ambient Occlusion)。使用ThreeJs,使得我们可以很轻易分配一种给定颜色给一个顶点,这最终将改变表面的最终颜色。我们要去利用这个特性来模拟建 筑物的底部的阴影。首先我们定义的向光面和背光面的基本色。
var light = new THREE.Color( 0xffffff )
var shadow = new THREE.Color( 0x303050 )
这些设置对于每个建筑来说都是基本的常数。现在我们需要根据这个常数来为每个建筑得到一些随机和特殊的颜色。
var value = 1 - Math.random() * Math.random();
var baseColor = new THREE.Color().setRGB( value + Math.random() * 0.1, value, value + Math.random() * 0.1 );
现在我们需要给每个面的每个顶点指定 vertexColor
的属性值。如果这个面是顶面,那么就使用该建筑的baseColor
。如果是侧面,那么使用baseColor
乘上light
作为上方顶点的颜色,使用baseColor
乘上shaddow
作为下方顶点的颜色来模拟环境光遮蔽的效果。
// 以baseColor作为参考设置上方顶点和下方顶点的颜色
var topColor = baseColor.clone().multiply( light );
var bottomColor = baseColor.clone().multiply( shadow );
// 每个面的每个顶点指定vertexColor的属性值
var geometry = buildingMesh.geometry;
for ( var j = 0, jl = geometry.faces.length; j < jl; j ++ ) {
if ( j === 2 ) {
// 如果这个面是顶面
geometry.faces[ j ].vertexColors = [ baseColor, baseColor, baseColor, baseColor ];
} else {
//如果这个面是侧面
geometry.faces[ j ].vertexColors = [ topColor, bottomColor, bottomColor, topColor ];
}
}
合并所有的建筑物
为了构建我们的城市,我们需要合并20000个建筑物在一起,为每个建筑物应用之前描述的过程。我们已经看到,降低绘图命令调用可以获取很好的性能。在这里,所有建筑物共用相同的材料,我们可以采用一个单一的几何物体来合并这些建筑。
var cityGeometry= new THREE.Geometry();
for( var i = 0; i < 20000; i ++ ){
// 为每个建筑设置位置、旋转、大小和颜色
// ...
// 合并所有建筑为单一的cityGeometry,可以有力的提升性能
THREE.GeometryUtils.merge( cityGeometry, buildingMesh );
}
现在,我们得到了整个城市的一个大几何形状,让我们采用它来构建Mesh对象。
// build the mesh
var material = new THREE.MeshLambertMaterial({
map : texture,
vertexColors : THREE.VertexColors
});
var mesh = new THREE.Mesh(cityGeometry, material );
这个Mesh对象就是我们想要构建的城市。还差一步,我们还需要添加纹理。
用程序生成纹理对象
在这里,我们要为每个建筑物的侧面生成纹理,以添加建筑的真实感。它采用交替的窗户和楼层进行。每个窗户通过不同的噪声模拟每个房间的光线明暗变化。
首先,你建一个Canvas画布,不需要很大,32×64就够了。
var canvas = document.createElement( 'canvas' );
canvas.width = 32;
canvas.height = 64;
var context = canvas.getContext( '2d' );
将它绘制成白色
context.fillStyle = '#ffffff';
context.fillRect( 0, 0, 32, 64 );
现在,我们需要在这个白色的表面进行绘制,,一排窗户,一排地板,如此循环。事实上,我们只需要绘制窗户就好了。要绘制窗户,我们添加一些随机明暗变化来模拟每个窗户的灯光变化。
for( var y = 2; y < 64; y += 2 ){
for( var x = 0; x < 32; x += 2 ){
var value = Math.floor( Math.random() * 64 );
context.fillStyle = 'rgb(' + [value, value, value].join( ',' ) + ')';
context.fillRect( x, y, 2, 1 );
}
}
现在我们已经有纹理了 32* 64 ,我们需要增加它的分辨率。首先,让我们创建一个更大的画布,1024*512。
var canvas2 = document.createElement( 'canvas' );
canvas2.width = 512;
canvas2.height = 1024;
var context = canvas2.getContext( '2d' );
关掉默认的平滑处理:
context.imageSmoothingEnabled = false;
context.webkitImageSmoothingEnabled = false;
context.mozImageSmoothingEnabled = false;
复制小画布的纹理到大的画布里去:
context.drawImage( canvas, 0, 0, canvas2.width, canvas2.height );
剩下的要做的就是根据这个大画布来创建真正的THREE.Texture
对象,
var texture = new THREE.Texture( generateTexture() );
texture.anisotropy = renderer.getMaxAnisotropy();
texture.needsUpdate = true;
这就是最后一步了!现在,你知道如何绘制一个虚拟的3D城市了,爽!下面是完整的代码。
// build the base geometry for each building
var geometry = new THREE.CubeGeometry( 1, 1, 1 );
// translate the geometry to place the pivot point at the bottom instead of the center
geometry.applyMatrix( new THREE.Matrix4().makeTranslation( 0, 0.5, 0 ) );
// get rid of the bottom face - it is never seen
geometry.faces.splice( 3, 1 );
geometry.faceVertexUvs[0].splice( 3, 1 );
// change UVs for the top face
// - it is the roof so it wont use the same texture as the side of the building
// - set the UVs to the single coordinate 0,0. so the roof will be the same color
// as a floor row.
geometry.faceVertexUvs[0][2][0].set( 0, 0 );
geometry.faceVertexUvs[0][2][1].set( 0, 0 );
geometry.faceVertexUvs[0][2][2].set( 0, 0 );
geometry.faceVertexUvs[0][2][3].set( 0, 0 );
// buildMesh
var buildingMesh= new THREE.Mesh( geometry );
// base colors for vertexColors. light is for vertices at the top, shaddow is for the ones at the bottom
var light = new THREE.Color( 0xffffff )
var shadow = new THREE.Color( 0x303050 )
var cityGeometry= new THREE.Geometry();
for( var i = 0; i < 20000; i ++ ){
// put a random position
buildingMesh.position.x = Math.floor( Math.random() * 200 - 100 ) * 10;
buildingMesh.position.z = Math.floor( Math.random() * 200 - 100 ) * 10;
// put a random rotation
buildingMesh.rotation.y = Math.random()*Math.PI*2;
// put a random scale
buildingMesh.scale.x = Math.random() * Math.random() * Math.random() * Math.random() * 50 + 10;
buildingMesh.scale.y = (Math.random() * Math.random() * Math.random() * buildingMesh.scale.x) * 8 + 8;
buildingMesh.scale.z = buildingMesh.scale.x
// establish the base color for the buildingMesh
var value = 1 - Math.random() * Math.random();
var baseColor = new THREE.Color().setRGB( value + Math.random() * 0.1, value, value + Math.random() * 0.1 );
// set topColor/bottom vertexColors as adjustement of baseColor
var topColor = baseColor.clone().multiply( light );
var bottomColor = baseColor.clone().multiply( shadow );
// set .vertexColors for each face
var geometry = buildingMesh.geometry;
for ( var j = 0, jl = geometry.faces.length; j < jl; j ++ ) {
if ( j === 2 ) {
// set face.vertexColors on root face
geometry.faces[ j ].vertexColors = [ baseColor, baseColor, baseColor, baseColor ];
} else {
// set face.vertexColors on sides faces
geometry.faces[ j ].vertexColors = [ topColor, bottomColor, bottomColor, topColor ];
}
}
// merge it with cityGeometry - very important for performance
THREE.GeometryUtils.merge( cityGeometry, buildingMesh );
}
// generate the texture
var texture = new THREE.Texture( generateTexture() );
texture.anisotropy = renderer.getMaxAnisotropy();
texture.needsUpdate = true;
// build the mesh
var material = new THREE.MeshLambertMaterial({
map : texture,
vertexColors : THREE.VertexColors
});
var cityMesh = new THREE.Mesh(cityGeometry, material );
function generateTexture() {
// build a small canvas 32x64 and paint it in white
var canvas = document.createElement( 'canvas' );
canvas.width = 32;
canvas.height = 64;
var context = canvas.getContext( '2d' );
// plain it in white
context.fillStyle = '#ffffff';
context.fillRect( 0, 0, 32, 64 );
// draw the window rows - with a small noise to simulate light variations in each room
for( var y = 2; y < 64; y += 2 ){
for( var x = 0; x < 32; x += 2 ){
var value = Math.floor( Math.random() * 64 );
context.fillStyle = 'rgb(' + [value, value, value].join( ',' ) + ')';
context.fillRect( x, y, 2, 1 );
}
}
// build a bigger canvas and copy the small one in it
// This is a trick to upscale the texture without filtering
var canvas2 = document.createElement( 'canvas' );
canvas2.width = 512;
canvas2.height = 1024;
var context = canvas2.getContext( '2d' );
// disable smoothing
context.imageSmoothingEnabled = false;
context.webkitImageSmoothingEnabled = false;
context.mozImageSmoothingEnabled = false;
// then draw the image
context.drawImage( canvas, 0, 0, canvas2.width, canvas2.height );
相关推荐
而Three.js是另一个非常流行的JavaScript库,专门用于在Web上构建3D图形。将Cesium与Three.js整合,可以利用Cesium的地理空间功能和Three.js的丰富3D图形渲染能力,为用户提供更加生动、复杂的地球可视化体验。 ...
这个“threejs案例源码资源”是利用Three.js技术实现的一个VR眼镜Demo,通过浏览器即可体验到3D虚拟现实效果。本文将深入探讨这个Demo的实现原理,帮助你理解Three.js在VR领域的应用。 首先,我们要了解VR(Virtual...
【标题】"Three.js实现的三维城市和穿梭的光线动画场景特效源码"是一个使用JavaScript库Three.js创建的项目,该库专门用于在Web浏览器中进行3D图形编程。Three.js提供了一个简单易用的API,使开发者能够利用 WebGL ...
在这个特定的项目中,开发者可能使用了Three.js来创建一个三维城市模型。这可能涉及到了以下步骤: 1. **创建场景**:定义3D空间中的场景,所有物体都将在这个场景中展示。 2. **创建相机**:设置一个虚拟相机,...
其中包含Three.js开发入门、three.js环境搭建、WebGPU与WGSL入门与原理、blender基础进阶、Cesium入门等,结合大量实战案例,比如全景看房与科技展馆案例、智慧城市案例、Cesium智慧广州项目实战案例、海景酒店日夜...
在本项目"flying_arround_city:使用THREE.js飞行Arround城市"中,开发者运用了JavaScript中的THREE.js库来构建一个3D的城市环境,让用户可以自由地通过鼠标操作在虚拟城市中穿梭飞行。THREE.js是WebGL的一个强大框架...
为了创建这样一个城市环境,建模师会使用Blender中的各种工具,如网格建模、雕刻工具、UV展开和映射、纹理绘制以及光照设置等。同时,为了优化性能和加载速度,还需要考虑模型的简化、LOD(Level of Detail)层次...
在3D-mago3djs.zip这个压缩包中,包含的是一个名为“mago3djs-master”的项目,这是一个基于JavaScript的3D渲染库,专门用于实现高效且交互性强的3D场景构建。 一、3D建模基础 1. 模型类型:3D模型有多种类型,包括...
"三维skyline二次开发多条线路流水效果效果"是一个关于如何在三维场景中利用特定技术实现动态流水动画的课题。这个课题的核心是Skyline,它可能指的是Autodesk的3ds Max Skyline插件或者某种自定义的三维引擎,用于...
Three.js、Babylon.js等JavaScript库提供的经典示例进一步证明了三维可视化的强大潜力。 总而言之,大数据可视化是将复杂数据转化为直观图形的过程,其涵盖探索式分析、实时数据展现和三维空间应用。这一技术通过...
Web3D主要依赖于如WebGL这样的JavaScript API,它是OpenGL的一个子集,专门设计用来在HTML5 Canvas元素上绘制高性能的3D图形。通过WebGL,开发者可以创建复杂的3D模型、动画和场景,并与网页的其他元素无缝集成。 ...