Unity我的世界地形生成算法
我的世界minecraft地形生成
目录
我的世界minecraft地形生成
Perlin Noise (柏林噪声)
基本单位立方体
地形生成
种树
添加玩家
添加阴影
官网提供了一个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 );种树
场景有点单调,我的得种上一点树。种树思路,把地形的坐标都保存在数组中,然后随机的生成树。百度一张树的图片,要左右对称。用作plane的贴图。两个相交效果就可以了。比我的世界的树要简单很多,当然效果也差很多。
// tree 种树 function drTree() { var length = planeXYZ.length; for (var i = 0; i < 100; i++) { var po = Math.round(Math.random() * length); tree(3, planeXYZ[po].xx, planeXYZ[po].xh + 50 + 3 * 90, planeXYZ[po].xz); } } function tree(h, x, y, z) { var texture = new THREE.TextureLoader().load('minecraft/tree1.png'); texture.magFilter = THREE.NearestFilter; var tGeometry = new THREE.PlaneBufferGeometry(120 * h, 180 * h); // tGeometry.rotateY( - Math.PI / 2 ); var rGeometry = new THREE.PlaneBufferGeometry(120 * h, 180 * h); rGeometry.rotateY(Math.PI / 2); var mater = new THREE.MeshPhongMaterial({ map: texture, transparent: true, depthWrite: false, side: THREE.DoubleSide, // blending: THREE.AdditiveBlending, //opacity: .9, }); var tree = new THREE.Mesh(tGeometry,mater); tree.add(new THREE.Mesh(rGeometry,mater)); tree.castShadow = true; tree.position.set(x, y, z); scene.add(tree); return tree }树种完,还要种花花草草的。原理是一样。但是,种的多了,随机的方法就会出现 花草叠加在一起的情况。想的可以用es6种的集合代替数组,可以避免位置重叠。
添加玩家
添加一个小人,用键盘wsad键控制移动。关机地方是移动的时候要适应地图的高度。
这个小球就代表玩家。可以移动。但是在上台阶和下台阶的时候有bug,我没有去处理判断,会悬空,会进入草地。暂时实现基本功能就行了。
代码很简单。创建小球,添加键盘事件,移动小球就ok。
runball = sphereDr(50,0, getY(worldHalfWidth, worldHalfDepth) * 100+100, 0); document.body.addEventListener("keydown", function (k) { if(k.key == "w"){ runball.position.z -= 20 ; runball.position.y = getY(worldHalfWidth+Math.round(runball.position.x/100), worldHalfDepth+Math.round(runball.position.z/100)) * 100+100 } if(k.key == "s"){ runball.position.z += 20 ; runball.position.y = getY(worldHalfWidth+Math.round(runball.position.x/100), worldHalfDepth+Math.round(runball.position.z/100)) * 100+100 } if(k.key == "a"){ runball.position.x -= 20 ; runball.position.y = getY(worldHalfWidth+Math.round(runball.position.x/100), worldHalfDepth+Math.round(runball.position.z/100)) * 100+100 } if(k.key == "d"){ runball.position.x += 20 ; runball.position.y = getY(worldHalfWidth+Math.round(runball.position.x/100), worldHalfDepth+Math.round(runball.position.z/100)) * 100+100 } });
添加阴影
这个看起来有必要,不然场景光线有点单一。添加阴影后还能模拟太阳东升西落的效果。场景单位很大,导致我添加阴影失败,原来需要设置一下阴影的默认值。spotlight.shadow.camera.far = 8000;值要大,配的上场景才行。下一部减少场景比例。阴影也耗性能,暂时可以考虑不添加。
添加鸡
鸡这个元素在游戏中很神奇,不能少。我得添加一些公鸡母鸡,在场景溜达,还能下金蛋。
添加小怪兽
四处游荡在场景中的小怪兽,天黑了小怪兽就出来了,会攻击玩家,会吃掉金蛋。
添加玩家交互
玩家用手中的工具,可以收集花草,可以攻击怪兽。
玩家自定义场景
玩家可以自己添加立方体,可以删除立方体。初始化的场景的地形是合并了的,不好修改。只能创建和消除自己建立的立方体了。