您的位置:首页 > Web前端 > JavaScript

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

2017-09-29 15:29 302 查看
翻译有删改

原文链接:

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

 

算法评价

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

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

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

 

开始

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

让我们开始吧~!

 

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

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

 

[javascript] view
plain copy

var geometry = new THREE.CubeGeometry( 1, 1, 1 );  

我们将立方体的中轴点从中心变到底部。

 

[javascript] view
plain copy

geometry.applyMatrix( new THREE.Matrix4().makeTranslation( 0, 0.5, 0 ) );  

然后移除底部的面。这是一个优化的步骤,因为立方体底部的面永远不会被看到,所以可以移除。

 

[javascript] view
plain copy

geometry.faces.splice( 3, 1 );  

现在我们为顶部的面修整UV贴图。将它们放到单一坐标(0,0)中。这样屋顶和地板颜色就一样了。因为建筑物的每个面都用一张贴图,所以调用绘制函数一次就好。

 

[javascript] view
plain copy

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

好啦~现在我们有了单一建筑物的几何形状,接下来我们用建筑物来组合出一座城市吧~!

 

在城市的哪里放置建筑物

好吧,实际上我们是随机摆放它们的。虽然这样它们会发生冲突,但是在一个较低的位置漫游时看起来还好。

 

[javascript] view
plain copy

buildingMesh.position.x = Math.floor( Math.random() * 200 - 100 ) * 10;  

buildingMesh.position.z = Math.floor( Math.random() * 200 - 100 ) * 10;  

然后给Y轴加一个随机的旋转。

[javascript] view
plain copy

buildingMesh.rotation.y = Math.random()*Math.PI*2;  

然后我们通过改变mesh.scale来改变建筑物的大小。首先是宽度和深度。

 

[javascript] view
plain copy

buildingMesh.scale.x  = Math.random()*Math.random()*Math.random()*Math.random() * 50 + 10;  

buildingMesh.scale.z  = buildingMesh.scale.x  

然后是高度。

 

[javascript] view
plain copy

buildingMesh.scale.y  = (Math.random() * Math.random() * Math.random() * buildingMesh.scale.x) * 8 + 8;  

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

使用VertexColor模拟环境光遮挡

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

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

[javascript] view
plain copy

var light = new THREE.Color( 0xffffff )  

var shadow  = new THREE.Color( 0x303050 )  

接下来我们加一些随机值作为每个建筑物的变化色。

 

[javascript] view
plain copy

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。以此来做简单的环境光遮挡效果。

 

[javascript] view
plain copy

// 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个建筑物。所以我们用一个循环并且把循环中的建筑物都做以上处理。因为现在所有的建筑物都使用同样的材质,所以我们准备将它们整合到一个几何形状里。

 

[javascript] view
plain copy

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

}  

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

 

[javascript] view
plain copy

// build the mesh  

var material  = new THREE.MeshLambertMaterial({  

  map           : texture,  

  vertexColors  : THREE.VertexColors  

});  

var mesh = new THREE.Mesh(cityGeometry, material );  

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

 

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

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

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

 

[javascript] view
plain copy

var canvas  = document.createElement( 'canvas' );  

canvas.width  = 32;  

canvas.height = 64;  

var context = canvas.getContext( '2d' );  

然后染成白色。

 

[javascript] view
plain copy

context.fillStyle = '#ffffff';  

context.fillRect( 0, 0, 32, 64 );  

现在我们开始在这个白色的表面。我们准备在上面绘制地板。一个窗户行,一个地板行然后进行循环。实际上,当表面已经是白色的时候,我们只需要绘制窗户行就可以了。为了绘制窗户行,我们要加一些随机值以模拟窗户中的光线变化。

 

[javascript] view
plain copy

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的大画布。

 

[javascript] view
plain copy

var canvas2 = document.createElement( 'canvas' );  

canvas2.width = 512;  

canvas2.height  = 1024;  

var context = canvas2.getContext( '2d' );  

然后关闭平滑。

 

[javascript] view
plain copy

context.imageSmoothingEnabled   = false;  

context.webkitImageSmoothingEnabled = false;  

context.mozImageSmoothingEnabled  = false;  

现在把小的画布拷贝到大的里面。

 

[javascript] view
plain copy

context.drawImage( canvas, 0, 0, canvas2.width, canvas2.height );  

然后我们需要做的就是创建THREE.Texture。将anisotropie设置成一个较大的值以得到更好的效果。

 

[javascript] view
plain copy

var texture   = new THREE.Texture( generateTexture() );  

texture.anisotropy  = renderer.getMaxAnisotropy();  

texture.needsUpdate = true;  

完整代码

[javascript] view
plain copy

// 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。使用起来非常简单。

[javascript] view
plain copy

var city  = new THREEx.ProceduralCity()  

scene.add(city)  
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: