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

作者: u011077271
发布时间:2015-07-17 11:36:15

翻译有删改

原文链接:

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

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

 

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

为了制造我们的城市,我们需要整合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 mesh var 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 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 );   // return the just built canvas2   return canvas2; }

threex.proceduralcity扩展

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

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



标签: Java JavaScript
来源:http://blog.csdn.net/u011077271/article/details/40381749

推荐: