Three.js-100行javascriptコードで都市を作成

10714 ワード

翻訳に添削がある
テキストリンク:
http://learningthreejs.com/blog/2013/08/02/how-to-do-a-procedural-city-in-100lines/
 
アルゴリズム評価
細部に深く入り込む前に、まずグローバルな概念があるのはいつもいいです.このアルゴリズムによって実現される都市全体は、ダウンロードされたモデルを実現するのではなく、動的に構築されている.アルゴリズムは非常に優雅に書かれており、3 D都市を作成するには100行のコードしか使われていません.要約すると、各建物は立方体であり、大きさと位置はランダムです.
性能表現の観点から言えば、すべての建物は単一の幾何学的形状に融合し、単一の材質を持っている.マテリアル上の変換と単一の図面呼び出しがないため、プログラムは非常に効率的です.
リアリティを向上させるために,vertexColorを用いて環境光の遮蔽効果をシミュレートする簡単な方法を用いた.都市では、街の層に他の建物からの影があります.だから建物の底は上部より暗いです.vertexColorを設定することで、建物の下部の頂点が上部の色よりも暗くなり、この効果を再現することができます.
 
スタート
まず、建物の基本的な幾何学的形状を作成します.次に,この幾何学的形状を用いて都市のどこに建物を置くかを決定し,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)に配置します.これで屋根と床の色が同じになります.建物の各面に1枚のマップが使われているので、ペイント関数を1回呼び出すといいです.
 
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の建物を統合する必要があります.だから私たちは1つの循環を使って、循環中の建物をすべて以上の処理をします.現在、すべての建物が同じ材質を使用しているので、ジオメトリに統合するつもりです.
 
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 );

今、私たちはこの白い表面にいます.私たちは上に床を描くつもりです.1つの窓行、1つの床行を循環します.実際、表面が白いときは、窓を描くだけでいいです.窓の行を描くには、窓の光の変化をシミュレートするためにランダム値を追加します.
 
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)