• entries
9
3
• views
1252

# Feeling the ice beneath my feet

977 views

Ok, so far the terrain looks pretty good, but I can't walk on it.  How the heck do I do that?

Two options as I see it...

1. Try and use the heightmap to figure out what the height is at my current position and just get the camera to follow that.
2. Generate collision geometry from the heightmap and then do ray-triangle based collision detection.

Yeah yeah, I'm sure I could just find some free resources/JS library on t'interweb that will do all this gubbins for me and give me a full blown phsyics engine to boot... but where's the fun in that?!  Oh yeah, by the way, did I mention I'm nuts.

After much thought and general mucking around I opted for the 2nd option.  To create a geometry object that would contain all the vertices/faces for the terrain.  I don't need to render it so no need to create a mesh (can you imagine the insanity of rendering a mesh of this scale in the browser on a low-end device... madness I tell you, madness!).  I could potentially, at a future date, even stream the bit of collision geometry that I need from the Node back-end on the fly to speed things up further.

For now though I'll just create the geometry in memory on the client-side and use that.  It's just an array of vertices after all.

    this.generateCollisionMesh = function () {
// Render the height map texture off-screen and read the pixel data into an array
let texSize = _this.heightMap.image.width;
_this.pickingScene = new THREE.Scene();
_this.renderTarget = new THREE.WebGLRenderTarget( texSize, texSize );
_this.pickingCamera = new THREE.OrthographicCamera( 0, texSize, 0, texSize, 0.1, 10 );
_this.renderTarget.texture.minFilter = THREE.NearestFilter;
_this.renderTarget.texture.magFilter = THREE.NearestFilter;

let mapGeom = new THREE.PlaneBufferGeometry( texSize, texSize, 1, 1 );
let mapMaterial = new THREE.MeshBasicMaterial( {
color: 0xffffff,
side: THREE.DoubleSide
} );
mapMaterial.map = _this.heightMap;
let mapMesh = new THREE.Mesh( mapGeom, mapMaterial );
mapMesh.position.set( texSize / 2, texSize / 2, -1 );
mapMesh.rotation.x = Math.PI;

let pixelData = new Uint8Array( 4 * Math.pow( texSize, 2 ) );
_this.renderer.render( _this.pickingScene, _this.pickingCamera, _this.renderTarget );
let ctx = _this.renderer.getContext();
ctx.readPixels( 0, 0, texSize, texSize, ctx.RGBA, ctx.UNSIGNED_BYTE, pixelData );

// Create a plane geometry that we can modify to create the collision mesh
let tSubdivisions = ( _this.mapSize / _this.viewSize ) * _this.subdivisions;
_this.collisionGeom = new THREE.PlaneGeometry( _this.mapSize, _this.mapSize, tSubdivisions, tSubdivisions );
let mapHalfSize = _this.mapSize / 2;

for ( let i = 0; i < _this.collisionGeom.vertices.length; i++ ) {
let v = _this.collisionGeom.vertices[i];
let tx = ( ( ( v.x + mapHalfSize ) / _this.mapSize ) * texSize );
let ty = ( ( ( v.y + mapHalfSize ) / _this.mapSize ) * texSize );
let pdIdx = Math.clamp( Math.floor( tx + ( ty * texSize ) ) * 4, 0, pixelData.length );
if ( isNaN( pdIdx ) || isNaN( pixelData[pdIdx] ) ) {
continue;
}
v.z = ( pixelData[pdIdx] / 255 ) * _this.maxHeight;
}
_this.collisionGeom.verticesNeedUpdate = true;
_this.collisionGeom.normalsNeedUpdate = true;
_this.collisionGeom.computeVertexNormals();
_this.collisionGeom.computeFaceNormals();
_this.collisionGeom.computeBoundingBox();
}

I don't even know if I need those 5 lines at the end but I'll leave them in for good measure.

p.s. It took me a few days to get this right as the collision geometry just wasn't quite matching up to the actual rendered mesh.  Turns out it was because I'd not set:

        _this.renderTarget.texture.minFilter = THREE.NearestFilter;
_this.renderTarget.texture.magFilter = THREE.NearestFilter;

Oh my wasted life!!! 😡

Terrain collision geometry now working so I created a little function to test the height at a given point...

    this.heightAt = function ( x, y ) {
function rayTriangleIntersection( p, ray, v1, v2, v3 ) {
let ab = new THREE.Vector3().subVectors( v2, v1 );
let ac = new THREE.Vector3().subVectors( v3, v1 );

let n = new THREE.Vector3().crossVectors( ab, ac );
let d = ray.dot( n );
if ( d <= 0 ) return false;

let ap = new THREE.Vector3().subVectors( p, v1 );
let t = -ap.dot( n );
if ( t < 0 ) return false;

let e = new THREE.Vector3().crossVectors( ray, ap );
let u, v, w;
v = ac.dot( e );
if ( v < 0 || v > d ) return false;

w = -ab.dot( e );
if ( w < 0 || v + w > d ) return false;

let ood = 1.0 / d;
t *= ood;
v *= ood;
w *= ood;
u = 1.0 - v - w;
let pRay = ray.multiplyScalar( t );
return {
normal: new THREE.Vector3( u, v, w )
};
}

let halfSize = _this.collisionGeom.boundingBox.max.x;
let mapSubdiv = _this.mapSize / _this.viewSize * _this.subdivisions;
let localPos = new THREE.Vector3( x, y, 0 );
localPos.add( new THREE.Vector3( halfSize, halfSize, 0 ) );
let t = localPos.divideScalar( halfSize * 2 );
t.multiplyScalar( mapSubdiv );
// Determine a 'grid' position from the local position
let g = new THREE.Vector2( Math.floor( t.x ), Math.floor( t.y ) );
// And determine which half of the grid square the co-ords are in
let faceOffset = ( 1 - Math.frac( t.y ) <= Math.frac( t.x ) ) ? 1 : 0;
// Use this to calculate the face array index.
let idx = ( g.x + ( g.y * mapSubdiv ) ) * 2 + faceOffset;
let face = _this.collisionGeom.faces[idx];
if ( !face )
return;
// Then use the associated vertices to calc the intersection
let v1 = _this.collisionGeom.vertices[face.a];
let v2 = _this.collisionGeom.vertices[face.b];
let v3 = _this.collisionGeom.vertices[face.c];

let p = rayTriangleIntersection( new THREE.Vector3( x, -y, 0 ), new THREE.Vector3( 0, 0, 1 ), v1, v2, v3 );
return p === false ? p : { h: p.point.z, normal: face.normal };
}

I'm sure I could code that better.... but really, I can't be bothered.... it works....

### Images

View the entire The Berg album

There are no comments to display.

## Create an account

Register a new account

• ### What is your GameDev Story?

In 2019 we are celebrating 20 years of GameDev.net! Share your GameDev Story with us.