🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

Feeling the ice beneath my feet

posted in The Berg for project The Berg
Published September 15, 2018
Advertisement

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;
        _this.pickingScene.add( mapMesh );


        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 {
                point: pRay.add( p ),
                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....

 

1 likes 0 comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement
Advertisement