Third-Person Planet Exploration with Three.js

I should share my experiments more often on this blog, and maybe not only the 3D javascript game development that I’m just having fun with. But anyway, here is the last one in date : a third person planet exloration thing.

It’s not exactly “work in progress” since I don’t really plan on improving any of it from here, so just consider this work bare and unfinished. You can try it out here : battle-royale.webmaestro.fr.

3D models of trees

The 3D models used, such as the trees and stones, are from Poly by Google.

The planet is generated when page loads. Noise is applied to the shere vertices length to create the terrain, and “biomes” (materials and 3D models) are set according to the elevation and latitude.

3D Planet from a Noise Sphere

Controls are the classic W, A, S, D and mouse. I had to adapt the “third-person” logic to rotate rather than translate over space.

There is a day and night cycle that depends on where the player is positionned on the globe. The sun and the moon are casting light on opposite sides while turning around.

Day and Night Cycle

The water is… just ugly. There is no collision detection. And the character is a simple cone.

Oh, and there is no server to make it a multi-player shooter, even though that was the ispiration. The idea came when a friend showed me the very entertaining Fortnite. We thought it would be fun to turn this “Battle Royale” island into a planet. Instead of a “storm” shrinking toward the gathered players, we could simply reduce the radius of the spherical terrain… That was the concept.

Maybe I could post details about the code if whoever is interested. In the meantime I have other things to focus on !

3D Noise Sphere Geometry with Three.js

This extended Three.js geometry applies noise elevation over a sphere.

class NoiseSphereGeometry extends THREE.SphereGeometry {
    constructor(radius, widthSegments, heightSegments, {seed, noiseWidth, noiseHeight}) {
        super(radius, widthSegments, heightSegments);
        const getNoise = (vertice) => ImprovedNoise.noise(
                seed + vertice.x / noiseWidth,
                seed + vertice.y / noiseWidth,
                seed + vertice.z / noiseWidth
            ),
            noiseMap = this
                .vertices
                .map(getNoise),
            noiseMax = Math.max(...noiseMap),
            noiseMin = -Math.min(...noiseMap);
        for (const v in this.vertices) {
            if (noiseMap[v] > 0) {
                this
                    .vertices[v]
                    .elevation = noiseMap[v] / noiseMax;
            } else {
                this
                    .vertices[v]
                    .elevation = noiseMap[v] / noiseMin;
            }
            this
                .vertices[v]
                .multiplyScalar(1 + this.vertices[v].elevation * noiseHeight / radius);
        }
    }
}

Make sure to import the ImprovedNoise function from the Three.js examples.

<script src="three/examples/js/ImprovedNoise.js"></script>

Get Distance Between Two Points

from math import sqrt

def get_distance(origin, destination):
    '''Distance Between Two Points'''
    (o_x, o_y) = origin
    (d_x, d_y) = destination
    return sqrt((d_x - o_x)**2.0 + (d_y - o_y)**2.0)

Synchronize $remote and $local Files

function sync_files( $remote, $local )
{
    if ( ! is_file( $local ) ) { return copy( $remote, $local ); }

    $handle = fopen( $remote, 'r' );
    if ( ! $handle ) { die( "Could not open {$remote}." ); }
    $meta = stream_get_meta_data( $handle );
    fclose( $handle );

    foreach( $meta['wrapper_data'] as $response )
    {
        // Redirection
        if ( substr( strtolower( $response ), 0, 10 ) === 'location: ' ) {
            return sync_files( substr( $response, 10 ), $local );
        }

        // Compare sizes
        if ( substr( strtolower( $response ), 0, 16 ) === 'content-length: ' )
        {
            if ( (int) filesize( $local ) !== (int) substr( $response, 16 ) ) {
                return copy( $remote, $local );
            }
            continue;
        }

        // Compare dates
        if ( substr( strtolower( $response ), 0, 15 ) === 'last-modified: ' )
        {
            if ( (int) filemtime( $local ) < (int) strtotime( substr( $response, 15 ) ) ) {
                return copy( $remote, $local );
            }
            continue;
        }
    }

    return false;
}

Remove Nodes (Comments, Script Tags) from $html String

function remove_nodes( $html, array $selectors = array( '//comment()', '//script' ) )
{
    $dom = new DOMDocument;
    $dom->loadHtml( strval( $html ) );
    $dom->preserveWhiteSpace = false;
    $dom->formatOutput = true;
    $xpath = new DOMXPath( $dom );
    foreach( $selectors as $selector ) {
        while ( $node = $xpath->query( $selector )->item( 0 ) ) {
            // Remove selected tag from html string
            $node->parentNode->removeChild( $node );
        }
    }
    return $dom->saveHTML();
}

Sanitize Key : Lowercase Accentless $string, Special Characters $replacement

function sanitize_key( $string, $replacement = '_' )
{
    // Lowercase and remove accents
    $string = htmlentities( trim( strtolower( strval( $string ) ) ), ENT_NOQUOTES );
    $string = preg_replace( '/&([a-z])(?:acute|cedil|caron|circ|grave|orn|ring|slash|th|tilde|uml);/', '$1', $string );
    $string = preg_replace( '/&([a-z]{2})(?:lig);/', '$1', $string );
    $string = preg_replace( '/&[^;]+;/', $replacement, $string );
    // Replace non-alphanumeric characters
    return preg_replace( '/[^a-z0-9]+/', $replacement, $string );
}

Sanitize $value According to its $type

function sanitize( $value, $type = null )
{
    // Recursive sanitation for arrays and objects
    if ( is_array( $value ) || is_object( $value ) ) {
        $output = array();
        foreach ( (array) $value as $k => $v ) {
            $k = sanitize_key( $k );
            $output[$k] = sanitize( $v, $type );
        }
        return $output;
    }

    // Default to value's actual type
    if ( ! $type ) { $type = gettype( $value ); }

    switch( strtolower( $type ) )
    {
        case 'boolean':
            return boolval( $value );
        case 'integer':
        case 'number':
            return intval( $value );
        case 'double':
        case 'float':
            return floatval( $value );
        case 'null':
            return null;
        case 'resource':
            return $value;
        case 'url':
            return filter_var( rtrim( $value, '/' ), FILTER_SANITIZE_URL );
        case 'email':
            return filter_var( substr( $value, 0, 254 ), FILTER_SANITIZE_EMAIL );
        default:
            // String
            return filter_var( $value, FILTER_SANITIZE_STRING );
    }
}