Friday, July 27, 2012

Fencing My Three.js Avatar in with Collisions

‹prev | My Chain | next›

Up tonight, I hope to put my Three.js collision detection skills to good use. Specifically, I would like to finally replace the simple boundary limit detection that I have keeping my avatar on the island:


The boundary checking that prevents the avatar from taking that next step could not be simpler:
function render() {
  // ...
  if (controls.object.position.z >  ISLAND_HALF) controls.moveLeft = false;
  if (controls.object.position.z < -ISLAND_HALF) controls.moveRight = false;
  if (controls.object.position.x >  ISLAND_HALF) controls.moveBackward = false;
  if (controls.object.position.x < -ISLAND_HALF) controls.moveForward = false;
  // ...
}
The controls for the avatar are prevented from moving left if the avatar is half way from the center of the island. Similarly, the controls are prevented from moving forward if the avatar (controls.object) is already at the topmost half of the island.

As an aside, I am not quite certain why Three.js' FirstPersonControls rotate the coordinate system like this. Normally, the positive z-axis comes out of the screen; here it is to the left. Normally, the positive x-axis is to the right; here it is coming out of the screen. In other words, the orientation is rotated 90° clockwise. No doubt, FirstPersonControls does this for a reason (it seems related to a target), but it proves a bit of an annoyance.

Anyhow, I need an invisible fence around my island. Last night I made a wall with something like:
  var wallGeometry = new THREE.CubeGeometry(ISLAND_WIDTH, 1000, 100)
    , wallMaterial = new THREE.MeshBasicMaterial({wireframe: true})
    , wallMesh = new THREE.Mesh(wallGeometry, wallMaterial);
  wallMesh.position.z = ISLAND_HALF;
  scene.add(wallMesh);
With the wireframe option, this renders:


Instead of building four individual walls, it seems easier to place a cube around then entire island. I establish my invisible (well, not-so-invisible now) fence with:
  var fenceGeometry = new THREE.CubeGeometry(ISLAND_WIDTH, 1000, ISLAND_WIDTH, 3, 1, 3)
    , fenceMaterial = new THREE.MeshBasicMaterial({wireframe: true});
  fence = new THREE.Mesh(fenceGeometry, fenceMaterial);
  fence.flipSided = true;
  scene.add(fence);
To detect if the avatar intersects, I call my detectCollisions() function from the render() method:
function render() {
 // ...
 detectCollision();
 // ...
}

function detectCollision() {
  var vector = controls.target.clone().subSelf( controls.object.position ).normalize();
  var ray = new THREE.Ray(controls.object.position, vector);
  var intersects = ray.intersectObject(fence);

  if (intersects.length > 0) {
    if (intersects[0].distance < 5) {
      console.log(intersects);
    }
  }
}
The problem with the current detectCollisions() is that the vector is always pointing in the same direction. This results in collisions only being detected when I reach the maximum X distance.

Instead I need to calculate the vector direction based on the direction in which the avatar is currently travelling:
function detectCollision() {
  var x, z;
  if (controls.moveLeft) z = 1;
  if (controls.moveRight) z = -1;
  if (controls.moveBackward) x = 1;
  if (controls.moveForward) x = -1;

  var vector = new THREE.Vector3( x, 0, z );
  var ray = new THREE.Ray(controls.object.position, vector);
  var intersects = ray.intersectObject(fence);

  if (intersects.length > 0) {
    if (intersects[0].distance < 5) {
      console.log(intersects);
    }
  }
}
It is a bit of a pain to calculate the X's and Z's manually like this, but it does the trick. When I move through any of the walls, I see the desired console.log() output:


The last thing remaining is to prevent the avatar from moving past the wall. For that, I need to do a little more than logging the collision. I need to determine the direction in which I am moving and stop. I just stored the direction information in x and z variable so I reuse those. And, based on the direction, I disable the corresponding controls.move* boolean:
function detectCollision() {
  var x, z;
  if (controls.moveLeft) z = 1;
  if (controls.moveRight) z = -1;
  if (controls.moveBackward) x = 1;
  if (controls.moveForward) x = -1;

  var vector = new THREE.Vector3( x, 0, z );
  var ray = new THREE.Ray(controls.object.position, vector);
  var intersects = ray.intersectObject(fence);

  if (intersects.length > 0) {
    if (intersects[0].distance < 50) {
      if (z ==  1) controls.moveLeft = false;
      if (z == -1) controls.moveRight = false;
      if (x ==  1) controls.moveBackward = false;
      if (x == -1) controls.moveForward = false;
    }
  }
}
And, that actually works! I am now stuck on the island:


I am not 100% sold on that implementation, but it does the trick.

Day #460

No comments:

Post a Comment