three.js開發我的世界minecraft(一)地形的生成
我的世界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 );