我正在研究一个基于六角瓷砖的行星生成器——想想投影到测地线球体上的 Civ5 地形。
下图是这种形状的上半部分:图
我希望将这些单独的六边形和五边形压平成一个平面,尽管是一个不规则的平面,以便在反转转换和重组椭球体之前对其应用标准、正常的地形生成技术。
我将生成两个平面,北部和南部,因为本质上不可能展平任何真正的球体。
然而,鉴于可以在二维空间中显示六角网格,我认为没有理由不能制作这样的网格——尽管沿五边形有一些变形。
在我看来,我的步骤如下。
- 将每个图块生成为它自己的 THREE.Object3D,它本身包含一个具有 7 个顶点和 6 个面的网格(或者在 12 个五边形的情况下为 6 个和 5 个)
- 将每个图块(Object3D)的世界坐标中的纬度归一化为零(省略那些会落在“南半球”的纬度 - 这将是一个单独但相同的步骤)
- 旋转每个对象,使其内部网格与世界的地平线对齐——对象和内部网格的顶点是独立的,所以这很棘手。基本上,我需要每个网格顶点的 y 值为 0,而不会扭曲形状本身。
- 沿世界的水平面重新定位每个 Object3d,以使图块具有适当的间距,并且类似于六边形网格
- 将每个图块的每个顶点缝合成一个新的几何图形,一个连续的平面
一旦我有了它,我将应用我选择实施的任何世界建筑,将每个转换后的顶点“拾取”到它的原始图块上,然后取消转换它。瞧——一个十六进制世界。
构建六边形非常容易。我正在为 THREE.js 使用Rob Scanlon 的 Hexasphere 库。我如何实现图块的确切代码将在这篇文章的底部出现。
此外,标准化每个图块的纬度位置也相当简单。只需设置它的位置并完成。
我现在面临的问题是将六边形旋转到地平线上。我尝试使用标准三角运算。瓦片的顶点相对于它的中心点,而不是世界中心。因此,如果我取离瓦片中心的 x 距离最大的顶点,我可以计算瓦片(本身本质上是一个平面)相对于地平线形成的 x 平面中的角度。
# 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 )
我只是将该旋转反向应用于object3D(或者我想它是内部网格?)
这一直没有奏效,我无法塑造我错过了问题的至少一个关键部分的感觉。如果我在这件事上完全不合时宜,我不会感到惊讶。
接下来是适当地间隔瓷砖,这样它们的 xz 位置将类似于 2d 坐标而不是 3d 坐标。这一点,我也不知所措。
我意识到这可能没有实际意义。我想我可以简单地在球体上就地构建我的世界,但我很难将我的思想弯曲到第三维度。
代码如下。
AppAbstract.js - 包含场景、相机等。
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 );
};
};
行星.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();
};
};