G. Böhm
G. Böhm

Reputation: 51

smooth terrain from height map three js

I am currently trying to create some smooth terrain using the PlaneBufferGeometry of three.js from a height map I got from Google Images:

https://forums.unrealengine.com/filedata/fetch?id=1192062&d=1471726925

but the result is kinda choppy..

(Sorry, this is my first question and evidently I need 10 reputation to post images, otherwise I would.. but here's an even better thing: a live demo! left click + drag to rotate, scroll to zoom)

I want, like i said, a smooth terrain, so am I doing something wrong or is this just the result and i need to smoothen it afterwards somehow?

Also here is my code:

const IMAGE_SRC = 'terrain2.png';
const SIZE_AMPLIFIER = 5;
const HEIGHT_AMPLIFIER = 10;

var WIDTH;
var HEIGHT;


var container = jQuery('#wrapper');
var scene, camera, renderer, controls;
var data, plane;

image();
// init();

function image() {
    var image = new Image();
    image.src = IMAGE_SRC;
    image.onload = function() {
        WIDTH = image.width;
        HEIGHT = image.height;

        var canvas = document.createElement('canvas');
        canvas.width = WIDTH;
        canvas.height = HEIGHT;
        var context = canvas.getContext('2d');


        console.log('image loaded');
        context.drawImage(image, 0, 0);
        data = context.getImageData(0, 0, WIDTH, HEIGHT).data;

        console.log(data);

        init();
    }
}

function init() {

    // initialize camera
    camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, .1, 100000);
    camera.position.set(0, 1000, 0);

    // initialize scene
    scene = new THREE.Scene();

    // initialize directional light (sun)
    var sun = new THREE.DirectionalLight(0xFFFFFF, 1.0);
    sun.position.set(300, 400, 300);
    sun.distance = 1000;
    scene.add(sun);

    var frame = new THREE.SpotLightHelper(sun);
    scene.add(frame);

    // initialize renderer
    renderer = new THREE.WebGLRenderer();
    renderer.setClearColor(0x000000);
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(window.innerWidth, window.innerHeight);
    container.append(renderer.domElement);

    // initialize controls
    controls = new THREE.OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.dampingFactor = .05;
    controls.rotateSpeed = .1;

    // initialize plane
    plane = new THREE.PlaneBufferGeometry(WIDTH * SIZE_AMPLIFIER, HEIGHT * SIZE_AMPLIFIER, WIDTH - 1, HEIGHT - 1);
    plane.castShadow = true;
    plane.receiveShadow = true;

    var vertices = plane.attributes.position.array;
    // apply height map to vertices of plane
    for(i=0, j=2; i < data.length; i += 4, j += 3) {
        vertices[j] = data[i] * HEIGHT_AMPLIFIER;
    }

    var material = new THREE.MeshPhongMaterial({color: 0xFFFFFF, side: THREE.DoubleSide, shading: THREE.FlatShading});

    var mesh = new THREE.Mesh(plane, material);
    mesh.rotation.x = - Math.PI / 2;
    mesh.matrixAutoUpdate  = false;
    mesh.updateMatrix();

    plane.computeFaceNormals();
    plane.computeVertexNormals();

    scene.add(mesh);

    animate();
}

function animate() {
    requestAnimationFrame(animate);

    renderer.render(scene, camera);
    controls.update();
}

Upvotes: 3

Views: 4121

Answers (2)

Martin Schuhfu&#223;
Martin Schuhfu&#223;

Reputation: 6986

One thing you could do to smooth things out a bit is to sample more than just a single pixel from the heightmap. Right now, the vertex indices directly correspond to the pixel position in the data-array. And you just update the z-value from the image.

for(i=0, j=2; i < data.length; i += 4, j += 3) {
  vertices[j] = data[i] * HEIGHT_AMPLIFIER;
}

Instead you could do things like this:

  • get multiple samples with certain offsets along the x/y axes
  • compute an (weighted) average value from the samples

That way you would get some smoothing at the borders of the same-height areas.

The second option is to use something like a blur-kernel (gaussian blur is horribly expensive, but maybe something like a fast box-blur would work for you).

As you are very limited in resolution due to just using a single byte, you should convert that image to float32 first:

const highResData = new Float32Array(data.length / 4);
for (let i = 0; i < highResData.length; i++) {
  highResData[i] = data[4 * i] / 255;
}

Now the data is in a format that allows for far higher numeric resolution, so we can smooth that now. You could either adjust something like the StackBlur for the float32 use-case, use ndarrays and ndarray-gaussian-filter or implement something simple yourself. The basic idea is to find an average value for all the values in those uniformly colored plateaus.

Hope that helps, good luck :)

Upvotes: 0

Matey
Matey

Reputation: 1210

The result is jagged because the height map has low color depth. I took the liberty of coloring a portion of the height map (Paint bucket in Photoshop, 0 tolerance, non-continuous) so you can see for yourself how large are the areas which have the same color value, i.e. the same height.

The areas of the same color will create a plateau in your terrain. That's why you have plateaus and sharp steps in your terrain.

Colored height map

What you can do is either smooth out the Z values of the geometry or use a height map which utilizes 16bits or event 32bits for height information. The current height map only uses 8bits, i.e. 256 values.

Upvotes: 1

Related Questions