Skip to content

A terrain map visualizer based from height maps using React and three.js

License

Notifications You must be signed in to change notification settings

supershaneski/react-three-terrain

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

29 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

react-three-terrain

This React project is a simple terrain map viewer using three.js, a cross-browser Javascript library to create/display 3D graphics in web browser, and bootstrapped using Vite.

Motivation

This is a coding exercise to explore three.js, making custom geometry, etc.

At first, I was thinking of making some procedural terrain map builder but I cannot find any interesting function to generate a relief map so I decided to make the terrain based on height maps instead.

I envision it to end up like an 3D elevation viewer for some geographic information system (GIS).

Application

screenshot

I am using @react-three/fiber as the React renderer for three.js and @react-three/drei for helper components.

I use the pixel data from the 2D image height map to create the relief in 3D. I extract it using the canvas API getImageData.

const canvas = document.createElement('canvas')
canvas.width = img.naturalWidth
canvas.height = img.naturalHeight

const ctx = canvas.getContext('2d')

ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight)

for(var y = 0; y < img.naturalHeight; y++){
    for(var x = 0; x < img.naturalWidth; x++){
        
        var pixeldata = ctx.getImageData(x, y, 1, 1).data
        // pixel data contains RGB data of the pixel

    }
}

By default, the relief height is calculated by grayscale value of each pixel.

// grayscale value based on the luminance equation
const gs = (0.21 * pixeldata[0]) + (0.72 * pixeldata[1]) + (0.07 * pixeldata[2])

The included height maps are all in black and white but it is possible to use colored images.

The images I used for the demo are located in the public directory so it is possible to add more or perhaps even allow upload from the user.

To get the best result, it is advisable to use black and white images. The black represents the lowest level while white the highest level. Sharp contrasts will become deep ridges so it is better to blur or soften the edges. Adjust the resulting height using the level slider in OPTIONS panel to make the output visually appealing.

From previous version, I rewrote the geometry creation part from using native Plane Geometry to creating everything on the fly. I found a nice example that outlines how to do it by scratch.

for (let yi = 0; yi < height; yi++) {
    for (let xi = 0; xi < width; xi++) {
        
        let x = sep * (xi - (width - 1) / 2)
        let y = sep * (yi - (height + 1) / 2)
        let z = 0

        positions.push(x, y, z)
        colors.push(1, 0, 0)
        normals.push(0, 0, 1)

    }
}

This will give us the vertices, vertex color and normals.

I used scaleLinear function from d3-scale to get the color values per vertices.

const colorScale = scaleLinear()
    .domain([0, 0.4, 1])
    .range(['#000000', '#006622', '#ff88aa'])]

...

const color = colorScale(d) // where d is the grayscale value

As for indices and uvs,

let indices = [], uvs = []

let i = 0

for (let yi = 0; yi < height - 1; yi++) {
    for (let xi = 0; xi < width - 1; xi++) {
        indices.push(i, i + 1, i + width + 1)
        indices.push(i + width + 1, i + width, i)
        i++
    }
    i++
}

for (let y = height - 1; y >= 0; y--) {
    for (let x = 0; x < width; x++) {
        const u = Math.round(10000 * x / width)/10000
        const v = Math.round(10000 * y / height)/10000
        uvs.push(u, v)
    }
}

This article explains how to compute for uv. Now, we then plug all the data it in our mesh

<mesh>
    <bufferGeometry>
        <bufferAttribute
        attach="attributes-position"
        array={positions}
        count={positions.length / 3}
        itemSize={3}
        />
        <bufferAttribute
        attach="attributes-normal"
        array={normals}
        count={normals.length / 3}
        itemSize={3}
        />
        <bufferAttribute
        attach="attributes-color"
        array={colors}
        count={colors.length / 3}
        itemSize={3}
        />
        <bufferAttribute
        attach="attributes-uv"
        array={uvs}
        count={uvs.length / 2}
        itemSize={2}
        />
        <bufferAttribute
        attach="attributes-index"
        array={indices}
        count={indices.length}
        itemSize={1}
        />
    </bufferGeometry>
    <meshStandardMaterial vertexColors flatShading side={DoubleSide} />
</mesh>

This will give you a vertical plane as a starting point.

We apply the grayscale data during vertex creation. To make this process simple, we make sure that the number of vertices and pixels are the same.

for (let yi = 0; yi < height; yi++) {
    for (let xi = 0; xi < width; xi++) {
        
        let d = data[k] // grayscale data
        
        let x = sep * (xi - (width - 1) / 2)
        let y = sep * (yi - (height + 1) / 2)
        let z = height * d.value / max

        positions.push(x, y, z)
        
        k++

    }
}

Please note that the sample height maps are taken from the web

Moving Forward

Craggy Terrain

After rewriting the code, it is now possible to show vertexColor. I removed several unnecessary custom options for simplicity.

One of the side effect of the update is, it is now necessary to press the Reload button in the Edit Options panel to show the changes you made.

Setup

Clone the repository and install the dependencies

$ git clone https://github.com/supershaneski/react-three-terrain.git myproject

$ cd myproject

$ npm install

$ npm run dev

Open your browser to http://localhost:5173/ to load the application page.

About

A terrain map visualizer based from height maps using React and three.js

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published