2

I'm working on a hex-tile based planet generator- think Civ5 terrain projected onto a geodesic sphere.

The image that follows is the top half of such a shape: Figure

I am looking to flatten these individual hexagons and pentagons into a plane, albeit an irregular one, so as to apply standard, normal terrain generation techniques onto it, before reversing the transformation and reforming the spheroid.

I would be generating two planes, northern and southern, given that it is inherently impossible to flatten any true sphere.

Given that it IS possible to display a hexgrid in 2d space however, I see no reason that such a grid couldn't be made- though with some distortion along the pentagons.

My steps, in my mind, are as follows.

  1. Generate each tile as it's own THREE.Object3D, which itself contains a mesh with 7 vertices and 6 faces ( or 6 and 5 in the case of the 12 pentagons )
  2. Normalize the latitude in world coordinates of each tile (Object3D) to zero (omitting those which would fall onto the 'southern hemisphere- that would be a separate but identical step)
  3. Rotate each Object such that it's internal mesh is aligned with the horizon of the world- the Object and the vertices of the mesh inside are independent, so this is tricky. Basically, I need the y-value of each of the mesh's vertices to be 0, without distorting the shape itself.
  4. Reposition each Object3d along the world's horizontal plane such that the tiles are appropriately spaced, and resemble a hexgrid
  5. Stitch each vertex of each tile into a new geometry, a single contiguous plane

Once I have that, I would apply whatever world building I choose to implement, 'pickup' each transformed vertex onto it's original tile, and untransform it. Voila- a hex world.

Building the hexsphere is pretty easy. I'm using Rob Scanlon's Hexasphere library for THREE.js. The exact code for how I implement the tiles will follow at the bottom of this post.

Also, normalizing the latitudinal position of each tile is fairly simple also. Just set it's position and done.

The problem I'm facing right now is rotating the hexagons onto the horizon. I tried using standard trigonometric operations. The vertices of the tiles are relative to it's center point, not to the world center. As such, if I take the vertex with the greatest x-distance from the tile's center I can calculate the angle in the x-plane that the tile (itself being essentially a flat plane) forms relative to the horizon.

# If center-point of tile is ( 0, 0, 0 ) relative to itself
# and
# Vertex with maximum x distance from center is ( x, y, z )
# then
# the x-distance from the center is x, and the y-distance is y, z-distance is z
# therefor
# I will always have two sides, the opposite and the adjacent, of any triangle I'm forming with sed point
#
# Using normal trig, I can calculate the angle

let rotationX = Math.atan( vertex.y / vertex.x )

The I just apply that rotation in reverse to the object3D (or it's internal mesh I suppose?)

This hasn't been working and I can't shape the feeling that I'm missing at least one key part of the question. I wouldn't be surprised if I'm totally off the mark on this one.

Next would be spacing the tiles would properly, such that their xz positions would resemble 2d coordinates instead of 3d coordinates. This, also, I'm at a loss for.

I realize that this may be moot. I suppose I could simply perform my world building in-place on the sphere, but I'm having trouble bending my mind into the third dimension.

Code is as follows.

AppAbstract.js - Contains the Scene, Camera, etc.

class AppAbstract {

    constructor( positional, parameters ) {

        this.setupScene( parameters );

    };

    setupScene( parameters ) {

        this._scene = new THREE.Scene();

        this._camera = new THREE.PerspectiveCamera( 50, 2, 1, 100000 );
        this._camera.position.z = 2700;
        this._camera.position.y = 0;

        this._renderer = new THREE.WebGLRenderer( {
            canvas: parameters[ 'canvas' ],
            antialias: false, 
            alpha: true,
        } );

        this.renderer.setSize( window.innerWidth, window.innerWidth / 2 );
        this.renderer.setPixelRatio( window.devicePixelRatio );
        this.renderer.sortObjects = false;

        window.addEventListener( 'resize', () => {
            this.renderer.setSize( window.innerWidth, window.innerWidth / 2 );
        }, false );

        this.ambientLight = new THREE.AmbientLight( 0xffffff, 1 );
        this._scene.add( this.ambientLight );

        this._controls = new THREE.OrbitControls( this._camera, this._renderer.domElement );
        this.controls.enableDamping = true;
        this.controls.dampingFactor = 0.1;
        this.controls.rotateSpeed = 0.1;
        this.controls.autoRotate = false;
        this.controls.autoRotateSpeed = 0.01;
        this.controls.zoomSpeed = 0.1;

    };

    get scene() {

        return this._scene;

    };

    get camera() {

        return this._camera;

    };

    get renderer() {

        return this._renderer;

    };

    get controls() {

        return this._controls;

    };

};

PlanetApp.js -

    class PlanetApp extends AppAbstract {

    constructor( positional, parameters ) {
        super( positional, parameters );

        this.planet = new Planet( this, positional, parameters );
        this.scene.add( this.planet );

    };

};

Planet.js

class Planet extends THREE.Object3D {

    constructor( app, positional, parameters ) {

        this.generateMaterials();
        this.generateTiles( positional[ 0 ] );

    };

    generateMaterials() {

        this.material = new THREE.MeshBasicMaterial( {
            color: 0x6495ED,
        } );

        this.wireframe = new THREE.MeshBasicMaterial( {
            color: 0x000000,
            wireframe: true,
        } );

    };

    generateTiles( subdivisions ) {

        let hexasphere = new Hexasphere( 1000, subdivisions, 0.99 );

        hexasphere.tiles.map( ( tile, index ) => {

            this.tiles.push( new Tile( tile, index ) );
            this.add( this.tiles[ index ] );

        } );

    };

}

Tile.js

class Tile extends THREE.Object3D {

    constructor( tile, index ) { super();

        this.index = index;

        this.getCenterPoint( tile );
        this.getOrdinalLatitude();

        this.generateMaterials();
        this.generateGeometry( tile );
        this.generateObjects();

    };

    getCenterPoint( tile ) {

        let [ xPositions, yPositions, zPositions ] = [ [], [], [] ];

        for ( let index = 0; index < tile.boundary.length; index++ ) {

            let vertex = tile.boundary[ index ];

            xPositions.push( parseFloat( vertex.x ) );
            yPositions.push( parseFloat( vertex.y ) );
            zPositions.push( parseFloat( vertex.z ) );

        };

        let centerX = xPositions.reduce( ( a, b ) => a + b, 0 );
        centerX = centerX / xPositions.length;

        let centerY = yPositions.reduce( ( a, b ) => a + b, 0 );
        centerY = centerY / yPositions.length;

        let centerZ = zPositions.reduce( ( a, b ) => a + b, 0 );
        centerZ = centerZ / zPositions.length;

        this.position.set( centerX, centerY, centerZ );

    };

    getOrdinalLatitude() {

        let boolean = this.position.y >= -10;

        let ordinal = ( boolean ) ? 'northern' : 'southern';
        this.ordinalLatitude = ordinal;

    };

    generateMaterials() {

        this.material = new THREE.MeshStandardMaterial( {
            color: 0x0077be,
        } );

        this.wireframe = new THREE.MeshStandardMaterial( {
            color: 0x000000,
            wireframe: true,
        } );

    };

    generateVertices( tile ) {

        this.geometry.vertices.push( new THREE.Vector3( 0, 0, 0 ) );

        tile.boundary.map( ( point, index ) => {

            let xPosition = point.x - this.position.x;
            let yPosition = point.y - this.position.y;
            let zPosition = point.z - this.position.z;

            let vertex = new THREE.Vector3( 
                xPosition, yPosition, zPosition
             );

            this.geometry.vertices.push( vertex );

        } );

    };

    generateFaces() {

        this.geometry.faces.push( new THREE.Face3( 0, 1, 2 ) );
        this.geometry.faces.push( new THREE.Face3( 0, 2, 3 ) );
        this.geometry.faces.push( new THREE.Face3( 0, 3, 4 ) );
        this.geometry.faces.push( new THREE.Face3( 0, 4, 5 ) );

        if ( this.geometry.vertices.length == 6 ) {
            this.geometry.faces.push( new THREE.Face3( 0, 5, 1 ) );

        };

        if ( this.geometry.vertices.length == 7 ) {
            this.geometry.faces.push( new THREE.Face3( 0, 5, 6 ) );
            this.geometry.faces.push( new THREE.Face3( 0, 6, 1 ) );
        };

    };

    generateGeometry( tile ) {

        this.geometry = new THREE.Geometry();
        this.generateVertices( tile );
        this.generateFaces();

    };

    generateObjects( tile ) {

        var mesh = new THREE.Mesh( this.geometry, this.material );
        var frame = new THREE.Mesh( this.geometry, this.wireframe );
        this.add( mesh ); this.add( frame );

    };

    normalizeLatitude() {

        this.position.set( this.position.x, 0, this.position.z );

    };

    normalizeXRotation() {

        let furthestVertex = this.geometry.vertices.reduce( 
            ( a, b ) => a.x > b.x ? a : b
        );

        let radians = Math.atan( furthestVertex.z / furthestVertex.x );
        this.rotateX( -radians );

    };

    normalizeYRotation() {

        let furthestVertex = this.geometry.vertices.reduce( 
            ( a, b ) => a.y > b.y ? a : b
        );

        let radians = Math.atan( furthestVertex.z / furthestVertex.y );
        this.rotateY( -radians );

    };

    normalizeZRotation() {

        let furthestVertex = this.geometry.vertices.reduce( 
            ( a, b ) => a.z > b.z ? a : b
        );

        let radians = Math.atan( furthestVertex.z / furthestVertex.x );
        this.rotateZ( -radians );

    };

    normalizeRotation() {

        // this.normalizeXRotation();
        // this.normalizeYRotation();
        // this.normalizeZRotation();

    };

};

0 Answers0