Three.js - 用100行javascript代码创建一座城市

来源:互联网 发布:淘宝开店实名认证失败 编辑:程序博客网 时间:2024/04/30 07:21

翻译有删改

原文链接:

http://learningthreejs.com/blog/2013/08/02/how-to-do-a-procedural-city-in-100lines/

 

算法评价

在深入细节之前先有一个全局的概念总是好的。该算法实现的整座城市都是动态建立的,而不是实现下载好的模型。算法写的非常优雅,创建一个3D城市仅仅用了100行代码。概括来讲:每栋建筑都是一个立方体,且它们的大小和位置随机。

从性能表现角度来说,所有的建筑都融合成一个单一的几何形状,有着单一的材质。没有材质上的变换和单一的绘图调用使得程序非常高效。

为了提高真实度,我们用一个简单的办法来模拟环境光的遮挡效果——使用vertexColor。在城市中,街道层有来自其他建筑物的阴影。所以建筑物的底部比顶部更暗。我们能通过设置vertexColor,使得建筑物底部顶点比顶部的颜色更暗,从而再现这个效果。

 

开始

我们一步步分析这100行代码:首先,我们为建筑物制造基本的几何形状;然后,我们使用这个几何形状来确定在城市的哪里放置建筑物,使用vertexColor来实现环境光遮挡的效果;然后,我们整合所有的建筑物来形成一个城市。因此,绘制整座城市只需要一个单一的绘制调用。最后,我们详细讲一下在渐进生成过程中建筑物的纹理。

让我们开始吧~!

 

为建筑物制造基本的几何形状

我们创建一个简单的立方体作为基本形状。

 

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;

在这里很多个Math.random()的连乘改变了结果的统计分布,使其更接近于0.现在,建筑物的位置、旋转和大小已经都设置好了。接下来设置它们的颜色和阴影仿真。

使用VertexColor模拟环境光遮挡

在graphic programming里面,环境光遮挡(ambientocclusion)可以被应用到很多个方面。

首先我们分别定义接收光源部分和阴影部分的基础色。这对于每个建筑物都是常量。

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和底部顶点的shaddow。以此来做简单的环境光遮挡效果。

 

// set topColor/bottom vertexColors as adjustement of baseColorvar topColor  = baseColor.clone().multiply( light );var bottomColor = baseColor.clone().multiply( shadow );// set .vertexColors for each facevar 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 ];  }}

现在单独的建筑物已经完全设置好了~!

 

用所有的建筑物组合成一座城市

为了制造我们的城市,我们需要整合20000个建筑物。所以我们用一个循环并且把循环中的建筑物都做以上处理。因为现在所有的建筑物都使用同样的材质,所以我们准备将它们整合到一个几何形状里。

 

var cityGeometry= new THREE.Geometry();for( var i = 0; i < 20000; i ++ ){  // set the position/rotation/color the building in the city  // ...   // merge it with cityGeometry - very important for performance  THREE.GeometryUtils.merge( cityGeometry, buildingMesh );}

现在我们得到了城市的一整个几何形体,然后为这个大几何形体创建一个网格。

 

// build the meshvar material  = new THREE.MeshLambertMaterial({  map           : texture,  vertexColors  : THREE.VertexColors});var mesh = new THREE.Mesh(cityGeometry, material );

这个网格就是整座城市的模型。太棒啦~!接下来是最后一步,我们会讲解如何制作贴图。

 

建筑物贴图的渐进生成(procedural generation)

这里我们想要生成每个建筑物侧面的纹理。简单的说,这会展示出楼层的真实感和多样性。所以它在窗户行和楼层行之间交替进行。窗户行是带着微小噪音的黑色来模拟每间房间的光线变化。然后我们小心地将纹理升级以避免滤波。

首先创建一个小的canvas画布。

 

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 );  }}

现在我们得到的纹理很小,为了放大后不模糊,我们关闭了.imageSmoothedEnabled效果。下面是代码:首先创建一个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。将anisotropie设置成一个较大的值以得到更好的效果。

 

var texture   = new THREE.Texture( generateTexture() );texture.anisotropy  = renderer.getMaxAnisotropy();texture.needsUpdate = true;

完整代码

// build the base geometry for each buildingvar geometry = new THREE.CubeGeometry( 1, 1, 1 );// translate the geometry to place the pivot point at the bottom instead of the centergeometry.applyMatrix( new THREE.Matrix4().makeTranslation( 0, 0.5, 0 ) );// get rid of the bottom face - it is never seengeometry.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 );// buildMeshvar buildingMesh= new THREE.Mesh( geometry );// base colors for vertexColors. light is for vertices at the top, shaddow is for the ones at the bottomvar 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 texturevar texture       = new THREE.Texture( generateTexture() );texture.anisotropy = renderer.getMaxAnisotropy();texture.needsUpdate    = true;// build the meshvar 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 );  // return the just built canvas2  return canvas2;}

threex.proceduralcity扩展

这段代码被集成到一个易于复用的threex包里面:threex.proceduralcity。使用起来非常简单。

var city  = new THREEx.ProceduralCity()scene.add(city)



0 0