ITエンジニア勉強ブログ

自分が学んだことを共有していきます。

ビル群のプロシージャル生成 ver.0.1

ビル群をプロシージャル生成するプログラムをJavaScript+Three.jsで書いてみました。


See the Pen
[WIP] procedural generation for buildings
by Imai (@imai1)
on CodePen.

既製品の商用ライブラリにもっと凄いのがありますが、自分で作ってみたくなったので……。

この記事では、このプログラムについて解説していきます。

環境設定

まずは、CDNからThree.jsを読み込みます。

<script src="https://unpkg.com/three@0.142.0/build/three.min.js"></script>

JavaScriptコードの一番外側の処理はmain関数にまとめてあります。

function main() {
  const WIDTH = window.innerWidth;
  const HEIGHT = window.innerHeight;

  const renderer = new THREE.WebGLRenderer({antialias:true});
  renderer.setSize(WIDTH, HEIGHT);
  renderer.setClearColor(0xDDDDDD, 1);
  document.body.appendChild(renderer.domElement);

  const [scene, camera] = initializeScene();

  const width = 200;
  const height = 200;
  scene.add(makeBaseMesh(width, height));
  addAvenueMesh(scene, 6, 3);
  //scene.add(makeBuildingMesh(30, 30, 0.5, 4.0, 3.0, 8));

  /*
  function render() {
    requestAnimationFrame(render);
    renderer.render(scene, camera);
  }
  render();
  */
  renderer.render(scene, camera);
}

main();

WebGLRendererの設定や利用に関するごく普通のコードだと思います。詳細についてはThree.jsの解説記事をご覧ください。

カメラと光源はinitializeScene関数で設定しています。また、床面の大きな板はmakeBaseMesh関数で生成しています。

function initializeScene() {
  const scene = new THREE.Scene();
  
  scene.fog = new THREE.Fog(0x0, 120, 220);

  const camera = new THREE.OrthographicCamera(-120, +120, 70, -70, 1, 1000);
  camera.position.set(-90, -90, 60);
  //camera.rotation.set(Math.PI / 4, 0, -Math.PI / 4);
  camera.up.set(1, 1, 1);
  camera.lookAt(new THREE.Vector3(0, 0, 0));
  scene.add(camera);
  
  const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
  directionalLight.position.set(-10, 15, 50);
  scene.add(directionalLight);
  
  const directionalLightTarget = new THREE.Object3D();
  scene.add(directionalLightTarget);
  directionalLight.target = directionalLightTarget;

  const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
  scene.add(ambientLight);

  return [scene, camera];
}

function makeBaseMesh(xSize, ySize) {
  const geometry = new THREE.PlaneGeometry(xSize, ySize);
  const material = new THREE.MeshLambertMaterial({color: 0xEAEFF2});
  const mesh = new THREE.Mesh(geometry, material);
  return mesh;
}

複数のビル群の生成

一つのビルを生成する関数はmakeBuildingMeshですが、先に、これを利用してビル群を生成する部分を紹介します。

ビル群の生成はaddAvenueMesh関数で実施しています。

function addAvenueMesh(scene, xSize, ySize) {
  const xUnit = 20;
  const yUnit = 24;
  for (let y = 0; y < ySize; ++y) {
    for (let x = 0; x < xSize; ++x) {
      // Setup random parameters for a building.
      const widthOfBuilding = Math.random() * 2 + 18;
      const firstFloorLevel = Math.random() * 0.4 + 0.3;
      const floorHeight = Math.random() * 1.5 + 3;
      const ceilingHeight = Math.random() * 0.5 + 2.5;
      const numFloors = Math.floor(Math.random() * 10 + 2);

      // Generate a mesh of a building.
      const mesh = makeBuildingMesh(
        widthOfBuilding,
        widthOfBuilding,
        firstFloorLevel,
        floorHeight,
        ceilingHeight,
        numFloors
      );

      // Move the mesh.
      const px = x * xUnit - xSize * xUnit / 2;
      const py = y * yUnit - ySize * yUnit / 2;
      mesh.position.set(px, py, 0);
      scene.add(mesh);
    }
  }
}

パラメータをランダムに設定してmakeBuildingMeshを実行、という処理を碁盤状に座標を変えながら繰り返しているだけです。

ビルの生成

最後にmakeBuildingMeshの紹介です。

makeBuildingMesh関数の全体像

まずは、ビルの形状を実際に作る処理を後回しにして、先に関数の全体像を紹介します。

このコードでは、Three.jsのBufferGeometryクラスを利用して、柱や窓などの面を一つ一つ手作りしていく方法でビルを生成しました。makeBuildingMeshでは、BufferGeometryの初期化や、面を張るサブルーチンaddFace、直方体を置くサブルーチンaddCubeなどを定義し、そのあとビルの形を定義して、最後に各種データをまとめてメッシュ化します。

function makeBuildingMesh(xWidth, yWidth, firstFloorLevel, floorHeight, ceilingHeight, numFloors) {

  ///////////////////////////////////////////////////////////
  // Preprocess.
  ///////////////////////////////////////////////////////////

  const geometry = new THREE.BufferGeometry();
  const rawVertices = new Array();

  let numVertices = 0;
  function addFace(a, b, c, d, materialIndex) {
    rawVertices.push(...a.concat(b, c, c, d, a));
    geometry.addGroup(numVertices, 6, materialIndex);
    numVertices += 6;
  }

  function addCube(x, y, z, xWidth, yWidth, height, materialIndex) {
    const vertices = [
      [x, y, z],
      [x + xWidth, y, z],
      [x + xWidth, y + yWidth, z],
      [x, y + yWidth, z],
      [x, y, z + height],
      [x + xWidth, y, z + height],
      [x + xWidth, y + yWidth, z + height],
      [x, y + yWidth, z + height],
    ];
    addFace(vertices[3], vertices[2], vertices[1], vertices[0], materialIndex);
    addFace(vertices[0], vertices[1], vertices[5], vertices[4], materialIndex);
    addFace(vertices[1], vertices[2], vertices[6], vertices[5], materialIndex);
    addFace(vertices[2], vertices[3], vertices[7], vertices[6], materialIndex);
    addFace(vertices[3], vertices[0], vertices[4], vertices[7], materialIndex);
    addFace(vertices[4], vertices[5], vertices[6], vertices[7], materialIndex);
  }

  ///////////////////////////////////////////////////////////
  // Define the shape of a building.
  ///////////////////////////////////////////////////////////

  // ビルの形状を作る処理は後述

  ///////////////////////////////////////////////////////////
  // Postprocess.
  ///////////////////////////////////////////////////////////

  // itemSize = 3 because there are 3 values (components) per vertex
  const vertices = new Float32Array(rawVertices);
  geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
  geometry.computeVertexNormals();

  const wallMaterial = new THREE.MeshLambertMaterial({color: 0xEAEFF2});
  const windowMaterial = new THREE.MeshBasicMaterial({color: 0xBBBB00, side: THREE.DoubleSide});
  windowMaterial.opacity = 0.6;
  windowMaterial.transparent = true;
  const materials = [wallMaterial, windowMaterial];
  const mesh = new THREE.Mesh(geometry, materials);
  return mesh;
}

L-Systemについて

ビルの形状はL-Systemによって定義します。

Lindenmayerが提案したオリジナルのL-System以外にも様々な類似手法がL-Systemと呼ばれているため、その上でバシッと定義を説明せよと言われると私のような素人には難しいのですが、誤解を恐れずに大雑把に言えば、L-Systemとは記号の置換ルールの再帰適用によって複雑な形状やルールを生成するための手法です。

例えば、

街 とは { (1, 1)に家があって、(2, 1)に店があって、(3, 1)に道路があって、... }

(x, y)に家 とは { (x, y)に壁があって、(x+0.1, y)に家具があって、... }

(x, y)に店 とは { ... }

(x, y)に道路 とは { ... }

という風に定義して、「〇〇とは」を左辺から右辺に各々変換していくと、最終的に「街」

街とは { (1, 1)に壁があって、(1.1, 1)に家具があって、... }

という風に、ローレベルなオブジェクトの集合に変換することができます。

実際には、最終的に変換されるローレベルなオブジェクトがCGライブラリのオブジェクトに置き換えられるように各部品を定義してあげると、L-Systemによって3DCGモデルを生成することができます。CGライブラリのオブジェクトとは、今回でいうとThree.jsの面や頂点や、それをラップしたaddFace addCubeなどです。

また、「〇〇とは { ... }」は、プログラミング言語における関数と全く同型の構造になっていますので、各変換処理は関数定義によって表すことができます。

ビルの形状の定義

以上を踏まえて、実際のビル形状の定義を紹介します。

まず、建物の基礎(Base)はaddBaseによって定義します。

  function addBase(x, y, z, xWidth, yWidth, height, numFloors) {
    // add a cube for base.
    addCube(x, y, z, xWidth, yWidth, height, 0);

    // add floors.
    addFloor(x, y, height, xWidth, yWidth, floorHeight, ceilingHeight, numFloors);
  }

  addBase(-xWidth / 2, -yWidth / 2, 0, xWidth, yWidth, firstFloorLevel, numFloors);

基礎にあたる大きな直方体を一つ定義して、その上にフロアを積み重ねるだけです。

一つ一つのフロアはaddFloorによって定義します。かなり大きいので実際の実装はCodePenをご覧いただくとして、擬似コードで説明すると以下のようになります。

  // 複数の階で共通のパラメータを定義

  function addFloor(x0, y0, z0, xWidth, yWidth, floorHeight, ceilingHeight, numFloors) {
    // 柱や窓などを生成するサブルーチンを定義
    // (一部パラメータをクロージャで引き継ぐために内部関数として定義した)

    // 四隅の柱を定義

    if (windowPattern == 0) {
      // 四辺に窓を定義
    } else if (windowPattern == 1) {
      // 四辺に窓、柱、窓、柱、...のパターンを定義
    }

    // add a ceiling.
    addCube(x0, y0, z0 + ceilingHeight, xWidth, yWidth, floorHeight - ceilingHeight, 0);

    // add a next floor
    if (numFloors > 1) {
      addFloor(x0, y0, z0 + floorHeight, xWidth, yWidth, floorHeight, ceilingHeight, numFloors - 1);
    }

windowPatternとして新しいパターンを定義すれば、別の窓枠配置も追加することができます。

また、上の階はaddFloor再帰的に呼び出して生成するようになっています。for文で各階を定義するよう実装しなかったのは、L-Systemが元々再帰的なものだからというのもありますが、上の階をwindowPatternのようなやり方で別のデザインに変更できるよう将来的な拡張を考慮したためです。

将来課題

いくらでも改良できますが、例えば以下のような課題が考えられます。

  • 1Fの入り口パターンの追加
  • 窓枠のパターンの追加
  • 柱のパターンの追加(外壁に大きな柱のないビルなど)
  • 窓のない面を持つビルの追加
  • 屋上のパターンの追加
  • 外壁の色のパターンの追加
  • 窓の色のパターンの追加