我的世界minecraft地形生成
官网提供了一个minecraft案例:examples/webgl_geometry_minecraft.html
生成的效果,中间是一个直径10的小球,我作为参考用的,这里面的立方体在three.js中的单位是100。
案例中的地形生成,主要是采用Perlin Noise随机生成。也可以采用灰度图来生成。难点是理解Perlin Noise算法。
Perlin Noise (柏林噪声)
柏林噪声是由Ken Perlin于1983年提出的一种梯度噪声(Gradient Noise,通常由计算机模拟得到的一组噪声,相较于传统的离散数值噪声value noise要更加连续平滑)他在1985年的SIGGRAPH会议上,做了一场以“An Image Synthesizer”为题的学术报告,正式提出他的这一发现。
柏林噪声的应用非常广泛: 合成地形高度图、生成物体表面的复杂纹理、火焰烟雾特效、波动效果的模拟等等。
Ken Perlin的个人主页:http://mrl.nyu.edu/~perlin/(纽约大学-媒体研究实验室 nyu Media Research Lab)
看了一上午,这个perlin noise 的坑有点大,还是直接跳过,不跳进去了,免得出不来。泪奔啊,这几天每天上午写bug,下午找bug,一天时间就浪费了。
基本单位立方体
我的世界,是由“像素块”组成的,以一个立方体为单位。我想以立方体为基本模型单位来生成整个地形。但是研究官方的代码之后,发现人家更高明。一个立方体是有5个面组成的,底部的面直接省略掉,牛。
案例中修改了uv的坐标,这样的好处是使用一张图就可以贴出不同的效果。下面是未修改uv的贴图情况。关于attributes.uv.array在文档中没有找到说明。自己的理解就是 平面有四个顶点,对于的uv坐标是8个值,存在数组中,顺序是上左,上右,下左,下右,先x后y。这样就可以生成侧面和顶面的纹理了。
原贴图文件很小,16X32像素。放大后呈现方块化效果,正符合我的世界的风格。主要是设置了magFilter属性,指定了纹理的放大方式,nearestFilter最邻近过滤。
// 右边
var pxGeometry = new THREE.PlaneBufferGeometry( 100, 100 );
pxGeometry.attributes.uv.array[ 1 ] = 0.5;
pxGeometry.attributes.uv.array[ 3 ] = 0.5;
pxGeometry.rotateY( Math.PI / 2 );
pxGeometry.translate( 50, 0, 0 );
//左边
var nxGeometry = new THREE.PlaneBufferGeometry( 100, 100 );
nxGeometry.attributes.uv.array[ 1 ] = 0.5;
nxGeometry.attributes.uv.array[ 3 ] = 0.5;
nxGeometry.rotateY( - Math.PI / 2 );
nxGeometry.translate( - 50, 0, 0 );
//上面
var pyGeometry = new THREE.PlaneBufferGeometry( 100, 100 );
pyGeometry.attributes.uv.array[ 5 ] = 0.5;
pyGeometry.attributes.uv.array[ 7 ] = 0.5;
pyGeometry.rotateX( - Math.PI / 2 );
pyGeometry.translate( 0, 50, 0 );
//前面
var pzGeometry = new THREE.PlaneBufferGeometry( 100, 100 );
pzGeometry.attributes.uv.array[ 1 ] = 0.5;
pzGeometry.attributes.uv.array[ 3 ] = 0.5;
pzGeometry.translate( 0, 0, 50 );
//后面
var nzGeometry = new THREE.PlaneBufferGeometry( 100, 100 );
nzGeometry.attributes.uv.array[ 1 ] = 0.5;
nzGeometry.attributes.uv.array[ 3 ] = 0.5;
nzGeometry.rotateY( Math.PI );
nzGeometry.translate( 0, 0, -50 );
地形生成
部分代码,先定义了地图的长宽。然后是生成的noise数组,放在data中。
var worldWidth = 128, worldDepth = 128,
worldHalfWidth = worldWidth / 2, worldHalfDepth = worldDepth / 2,
data = generateHeight( worldWidth, worldDepth );
function generateHeight( width, height ) {
var data = [], perlin = new ImprovedNoise(),
size = width * height, quality = 2, z = Math.random() * 100;
for ( var j = 0; j < 4; j ++ ) {
if ( j === 0 ) for ( var i = 0; i < size; i ++ ) data[ i ] = 0;
for ( var i = 0; i < size; i ++ ) {
var x = i % width, y = ( i / width ) | 0;
data[ i ] += perlin.noise( x / quality, y / quality, z ) * quality;
}
quality *= 4;
}
return data;
}
function getY( x, z ) {
return ( data[ x + z * worldWidth ] * 0.2 ) | 0;
}
var matrix = new THREE.Matrix4();
var geometries = [];
for ( var z = 0; z < worldDepth; z ++ ) {
for ( var x = 0; x < worldWidth; x ++ ) {
var h = getY( x, z );
matrix.makeTranslation(
x * 100 - worldHalfWidth * 100,
h * 100,
z * 100 - worldHalfDepth * 100
);
var px = getY( x + 1, z );
var nx = getY( x - 1, z );
var pz = getY( x, z + 1 );
var nz = getY( x, z - 1 );
geometries.push( pyGeometry.clone().applyMatrix( matrix ) );
if ( ( px !== h && px !== h + 1 ) || x === 0 ) {
geometries.push( pxGeometry.clone().applyMatrix( matrix ) );
}
if ( ( nx !== h && nx !== h + 1 ) || x === worldWidth - 1 ) {
geometries.push( nxGeometry.clone().applyMatrix( matrix ) );
}
if ( ( pz !== h && pz !== h + 1 ) || z === worldDepth - 1 ) {
geometries.push( pzGeometry.clone().applyMatrix( matrix ) );
}
if ( ( nz !== h && nz !== h + 1 ) || z === 0 ) {
geometries.push( nzGeometry.clone().applyMatrix( matrix ) );
}
}
}
var geometry = THREE.BufferGeometryUtils.mergeBufferGeometries( geometries );
geometry.computeBoundingSphere();
var texture = new THREE.TextureLoader().load( 'minecraft/atlas.png' );
texture.magFilter = THREE.NearestFilter;
var mesh = new THREE.Mesh( geometry, new THREE.MeshLambertMaterial( { map: texture, side: THREE.DoubleSide } ) );
scene.add( mesh );