More than 1 year has passed since last update.

今週も司会・進行は大学生プログラマのうーぴょん(さすらいうさぎ)がお送りします!
そろそろブロックの破壊や設置したいねってことで先週から頑張っているわけですが、今週もその準備の第2弾!今度は視線の先のブロックを取得してみようと思います!これができると、やっとブロックの破壊や設置の準備が整うわけですね!そんなわけで今週もやっていきましょう!

アジェンダ的な!

  1. ブロックに枠線を引く
  2. 視線が通る場所の計算
  3. あとは頑張る!

Let's Try!

ブロックに枠線を引く

STEP1 枠線を引くメソッドを作成

描画系なので、Renderingクラスに任せてしまいましょう!全部書くのはあれなので抜粋します。次のメソッドを追加しましょう!

js/Rendering.js(抜粋)
  showSelectedBlockMarker(x, y, z) {
    if(this.selectedBlockMarker != undefined) {
      this.scene.remove(this.selectedBlockMarker);
    }

    this.selectedBlockMarker = new THREE.EdgesHelper(this.meshes[x][z][y], 0x000000);
    this.scene.add(this.selectedBlockMarker);
  }

  removeSelectedBlockMarker() {
    if(this.selectedBlockMarker != undefined) {
      this.scene.remove(this.selectedBlockMarker);
    }
  }

枠線を引くにはTHREE.EdgesHelperというものを使うと簡単にできるようです。(公式ドキュメントには載ってない、なぜだ・・・)
とりあえず動くかを確かめるため、application.jsから呼び出します。

js/application.js(超抜粋)
rendering.showSelectedBlock(0, 64, 0);

適当に入れちゃってください!(常識の範囲内で)
こんな感じになります!
スクリーンショット 2016-10-14 10.15.11.png

視線が通る場所の計算

視線ベクトルの起点を(u, v, w)として、ここから視線ベクトル(p, q, w)が突き抜けるブロックのうち、自分に最も近いものを選べばいいわけです。周辺のブロックを近そうなやつから片っ端調べていくのは、効率が良くないのとスマートじゃないのでしっかりと考えます。
次のようなアルゴリズムを用いると、(多分)視線ベクトルが突き抜ける最もカメラに近いブロックが取得できます。

  1. block = (起点にあるブロック)とします
  2. blockがBlockAirのインスタンスでなければblockを返して終了します
  3. blockと視線ベクトルが交わる2点を取得します。
  4. その点を元に次のblockを決定し2に戻ります。(視線ベクトルが突き抜けるblockの面に接するブロックを取ればいいのです)

それを実装したものがこちらです。

js/application.js(抜粋)
let getPositionWhen = (axis, v) => {
    let t = 0;
    switch(axis) {
      case 0:
        t = (v - rendering.posX) / rendering.sightX;
        return [v, rendering.posY + 1.6 + t * rendering.sightY, rendering.posZ + t * rendering.sightZ];
      case 1:
        t = (v - rendering.posY - 1.6) / rendering.sightY;
        return [rendering.posX + t * rendering.sightX, v, rendering.posZ + t * rendering.sightZ];
      case 2:
        t = (v - rendering.posZ) / rendering.sightZ;
        return [rendering.posX + t * rendering.sightX, rendering.posY + 1.6 + t * rendering.sightY, v];
      default:
        return null;
    }
  }
  let getPenetratingPosOnBlock = (x, y, z) => {
    let result = [];
    result.push(getPositionWhen(1, y + 0.5));
    result.push(getPositionWhen(0, x + 0.5));
    result.push(getPositionWhen(2, z + 0.5));
    result.push(getPositionWhen(0, x - 0.5));
    result.push(getPositionWhen(2, z - 0.5));
    result.push(getPositionWhen(1, y - 0.5));

    return result.map(pos => (pos[0] >= x - 0.5 && pos[0] <= x + 0.5 && pos[1] >= y - 0.5  && pos[1] <= y + 0.5 && pos[2] >= z - 0.5 && pos[2] <= z + 0.5) ? pos : null);
  }
  let getTargetBlockPos = (x, y, z) => {
    let pos = getPenetratingPosOnBlock(x, y, z);
    let getDistance = (a, b) => Math.sqrt(Math.pow(a[0] - b[0], 2) + Math.pow(a[1] - b[1], 2) + Math.pow(a[2] - b[2], 2));

    if(x < 0 || y < 0 || z < 0) {
      return null;
    }

    if(getDistance([rendering.posX, rendering.posY + 1.6, rendering.posZ], [x, y, z]) > 10) {
      return null;
    }

    if(world.getBlock(x, y, z) != Blocks.air) return [x, y, z];

    if(pos[0] != null && rendering.sightY > 0) {
      return getTargetBlockPos(x, y + 1, z);
    } else if(pos[1] != null && rendering.sightX > 0) {
      return getTargetBlockPos(x + 1, y, z);
    } else if(pos[2] != null && rendering.sightZ > 0) {
      return getTargetBlockPos(x, y, z + 1);
    } else if(pos[3] != null && rendering.sightX < 0) {
      return getTargetBlockPos(x - 1, y, z);
    } else if(pos[4] != null && rendering.sightZ < 0) {
      return getTargetBlockPos(x, y, z - 1);
    } else if(pos[5] != null && rendering.sightY < 0) {
      return getTargetBlockPos(x, y - 1, z);
    }

    return null;
  }

あまりにも遠く離れたブロックは取得しないようにしています。
視点が動くたびに計算されれば良いので、次の場所から呼び出しましょう!

js/application.js
document.onkeydown = (e) => {
    if(e.keyCode == 87) {
      rendering.move(0.3 * moveX, 0, 0.3 * moveZ);
    } else if(e.keyCode == 65) {
      rendering.move(0.3 * moveZ, 0, -0.3 * moveX);
    } else if(e.keyCode == 83) {
      rendering.move(-0.3 * moveX, 0, -0.3 * moveZ);
    } else if(e.keyCode == 68) {
      rendering.move(-0.3 * moveZ, 0, 0.3 * moveX);
    } else if(e.keyCode == 32 && e.shiftKey) {
      rendering.move(0, -0.3, 0);
    } else if(e.keyCode == 32) {
      rendering.move(0, 0.3, 0);
    }

    console.log(getTargetBlockPos(Math.floor(rendering.posX), Math.floor(rendering.posY + 1.6), Math.floor(rendering.posZ)));
  }

  document.onmousemove = (e) => {
    let deltaX = e.momentX || e.webkitMovementX || e.mozMovementX || e.movementX;
    let deltaY = e.momentY || e.webkitMovementY || e.mozMovementY || e.movementY;
    const radian = Math.PI / 180;

    yaw += deltaX / 0.8;
    pitch -= deltaY / 0.8;

    if(pitch > 90) {
      pitch = 90;
    } else if(pitch < -90) {
      pitch = -90;
    }

    rendering.rotate(Math.cos(radian * pitch) * Math.cos(radian * yaw), Math.sin(radian* pitch), Math.cos(radian * pitch) * Math.sin(radian * yaw));
    [moveX, moveZ] = [Math.cos(radian * yaw), Math.sin(radian * yaw)];
    console.log(getTargetBlockPos(Math.floor(rendering.posX), Math.floor(rendering.posY + 1.6), Math.floor(rendering.posZ)));
  }

あとは頑張る!

目線の先のブロックの座標を取得できたので、あとは頑張ってマーカを表示させます。

js/application.js(抜粋)
  let setMarker = (pos) => {
    if(pos != null) {
      rendering.showSelectedBlockMarker(pos[0], pos[1], pos[2]);
    } else {
      rendering.removeSelectedBlockMarker();
    }
  }

  document.onkeydown = (e) => {
    if(e.keyCode == 87) {
      rendering.move(0.3 * moveX, 0, 0.3 * moveZ);
    } else if(e.keyCode == 65) {
      rendering.move(0.3 * moveZ, 0, -0.3 * moveX);
    } else if(e.keyCode == 83) {
      rendering.move(-0.3 * moveX, 0, -0.3 * moveZ);
    } else if(e.keyCode == 68) {
      rendering.move(-0.3 * moveZ, 0, 0.3 * moveX);
    } else if(e.keyCode == 32 && e.shiftKey) {
      rendering.move(0, -0.3, 0);
    } else if(e.keyCode == 32) {
      rendering.move(0, 0.3, 0);
    }

    setMarker(getTargetBlockPos(Math.floor(rendering.posX), Math.floor(rendering.posY + 1.6), Math.floor(rendering.posZ)));
  }

  document.onmousemove = (e) => {
    let deltaX = e.momentX || e.webkitMovementX || e.mozMovementX || e.movementX;
    let deltaY = e.momentY || e.webkitMovementY || e.mozMovementY || e.movementY;
    const radian = Math.PI / 180;

    yaw += deltaX / 0.8;
    pitch -= deltaY / 0.8;

    if(pitch > 90) {
      pitch = 90;
    } else if(pitch < -90) {
      pitch = -90;
    }

    rendering.rotate(Math.cos(radian * pitch) * Math.cos(radian * yaw), Math.sin(radian* pitch), Math.cos(radian * pitch) * Math.sin(radian * yaw));
    [moveX, moveZ] = [Math.cos(radian * yaw), Math.sin(radian * yaw)];
    setMarker(getTargetBlockPos(Math.floor(rendering.posX), Math.floor(rendering.posY + 1.6), Math.floor(rendering.posZ)));
  }

おまけ

その1

js/application.jsにいらない変数いたので削除します。

js/application.js(抜粋)
  let [posX, posY, posZ] = [0, 64, 0];//これいらない
  let [yaw, pitch] = [0, 0];
  let [sightX, sightY, sightZ] = [1, 0, 0];//これいらない

(違和感はありますが)Renderingクラスに移転したので削除しました。

その2

Renderingクラスでworldクラスのオブジェクトを引数に取るメソッドがあるし、この2つは密接に関係しているクラスなので、インスタンス変数としてもたせておきましょう!

js/Rendering.js(抜粋)
constructor(canvasId, targetWorld) {
    this.renderer = new THREE.WebGLRenderer({antialias: true});
    this.scene = new THREE.Scene();
    this.camera = new THREE.PerspectiveCamera(40, 800 / 480);
    this.renderer.setSize(800, 480);
    this.renderer.setClearColor(0xffffff);
    this.meshes = [];
    this.world = targetWorld;
    document.getElementById(canvasId).appendChild(this.renderer.domElement);
    this.init();
    this.update();
  } 
js/Rendering.js(抜粋)
resetWorld() {
    for(let x = 0; x < 16; ++x) {
      this.meshes[x] = [];
      for(let z = 0; z < 16; ++z) {
        this.meshes[x][z] = [];
        for(let y = 0; y < 256; ++y) {
          let block = new THREE.Mesh(this.cube, this.world.getBlock(x, y, z).getMaterial());

          block.position.set(x, y, z);
          this.meshes[x][z][y] = block;
          this.scene.add(block);
        }
      }
    }
  }
js/Rendering.js(抜粋)
updateMesh(x, y, z) {
    let block = new THREE.Mesh(this.cube, this.world.getBlock(x, y, z).getMaterial());

    block.position.set(x, y, z);
    this.scene.remove(this.meshes[x][z][y]);
    this.scene.add(block);
    this.meshes[x][z][y] = block;
  }

余計な引数は少ない方がいいですよね!

終わりに

座標計算大変・・・

第4週目はこちらから

週刊 WebでMinec◯aft的なものを作る! ~第4週目~

第6週目はこちらから

週刊 WebでMinec◯aft的なものを作る! ~第6週目~