Jump to content
Sign in to follow this  
  • entries
    5
  • comments
    0
  • views
    173

Project: The Berg

Feeling the ice beneath my feet

Greedy Goblin

562 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;
        _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....

 




0 Comments


Recommended Comments

There are no comments to display.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!