分形地形算法
24 Oct 2012好久没写东西,这个算法是去年就看到别人有用THREE.JS写过,自己也玩一下。
关于分形地形请参考这篇文章:http://gameprogrammer.com/fractal.html
(function() {
function generateTerrain(width, height, smoothness) {
var smoothness = typeof smoothness === 'undefined' ? 1 : smoothness;
var size = smallestPowerOfTwoAfter(Math.max(width, height));
var squareTerrain = generateSquareTerrain(size, smoothness);
var terrain = [];
for (var i = 0; i <= height; ++i) {
terrain.push(squareTerrain[i].slice(0, width + 1));
}
return terrain;
}
function smallestPowerOfTwoAfter(n) {
var ret = 1;
while (ret < n) {
ret <<= 1;
}
return ret;
}
function generateSquareTerrain(size, smoothness) {
if (size & (size - 1)) {
throw new Error('Expected terrain size to be a power of 2, received ' +
size + ' instead.');
}
var mat = generateMatrix(size + 1);
iterate(mat, smoothness);
return mat;
}
function generateMatrix(size) {
var matrix = [];
for (var i = 0; i < size; i++) {
var row = [];
for (var j = 0; j < size; ++j) {
row.push(0);
}
matrix.push(row);
};
return matrix;
}
function iterate(matrix, smoothness) {
var counter = 0;
var numIteration = Math.log(matrix.length - 1) / Math.LN2;
while (counter++ < numIteration) {
diamond(matrix, counter, smoothness);
square(matrix, counter, smoothness);
}
}
function diamond(matrix, depth, smoothness) {
var len = matrix.length;
var terrainSize = len - 1;
var numSegs = 1 << (depth - 1);
var span = terrainSize / numSegs;
var half = span / 2;
for (var x = 0; x < terrainSize; x += span) {
for (var y = 0; y < terrainSize; y += span) {
// (x, y)
// \
// a---b---c
// | | |
// d---e---f
// | | |
// g---h---i
//
// \___ ___/
// V
// span
//
var va = [x, y];
var vc = [x + span, y];
var ve = [x + half, y + half];
var vg = [x, y + span];
var vi = [x + span, y + span];
var heights = [va, vc, vg, vi].map(function(v) {
return matrix[v[1]][v[0]];
});
var avg = average(heights);
var offset = getH(smoothness, depth);
matrix[ve[1]][ve[0]] = avg + offset;
}
}
}
function square(matrix, depth, smoothness) {
var len = matrix.length
var terrainSize = len - 1;
var numSegs = 1 << (depth - 1);
var span = terrainSize / numSegs;
var half = span / 2;
for (var x = 0; x < terrainSize; x += span) {
for (var y = 0; y < terrainSize; y += span) {
// h = avg(g, c, i, m) + random;
// f = avg(a, g, k, i) + random;
// j = f;
//
// (x, y)
// \
// a---b---c---d---e
// | \ | / | \ | / |
// f---g---h---i---j
// | / | \ | / | \ |
// k---l---m---n---o
// | \ | / | \ | / |
// p---q---r---s---t
// | / | \ | / | \ |
// u---v---w---x---y
//
// \___ ___/
// V
// span
//
var va = [x, y];
var vb = [x + half, y];
var vc = [x + span, y];
var vf = [x, y + half];
var vg = [x + half, y + half];
var vh = [x + span, y + half];
var vk = [x, y + span];
var vl = [x + half, y + span];
var vm = [x + span, y + span];
var vhr = [x + half * 3, y + half];
if (vhr[0] > terrainSize) vhr[0] = half;
var vfl = [x - half, y + half]
if (vfl[0] < 0) vfl[0] = terrainSize - half;
var vlu = [x + half, y + half * 3];
if (vlu[1] > terrainSize) vlu[1] = half;
var vba = [x + half, y - half]
if (vba[1] < 0) vba[1] = terrainSize - half;
squareHelper(matrix, depth, smoothness, va, vg, vk, vfl, vf);
squareHelper(matrix, depth, smoothness, va, vba, vc, vg, vb);
squareHelper(matrix, depth, smoothness, vc, vhr, vm, vg, vh);
squareHelper(matrix, depth, smoothness, vk, vg, vm, vlu, vl);
}
}
for (var y = 0; y < terrainSize; y += span) {
matrix[y][terrainSize] = matrix[y][0];
}
for (var x = 0; x < terrainSize; x += span) {
matrix[terrainSize][x] = matrix[0][x];
}
}
function squareHelper(matrix, depth, smoothness, a, b, c, d, t) {
var heights = [a, b, c, d].map(function(v) {
return matrix[v[1]][v[0]];
});
var avg = average(heights);
var offset = getH(smoothness, depth);
matrix[t[1]][t[0]] = avg + offset;
}
function getH(smoothness, depth) {
var sign = Math.random() > 0.5 ? 1 : -1;
var reduce = 1;
for (var i = 0; i < depth; ++i) {
reduce *= Math.pow(2, -smoothness);
}
return sign * Math.random() * reduce;
}
function average(numbers) {
var sum = 0;
numbers.forEach(function(v) {
sum += v;
});
return sum / numbers.length;
}
var root;
if (typeof exports !== 'undefined' && exports !== null) {
root = exports;
} else {
root = window;
}
root.generateTerrain = generateTerrain;
}).call(this);
var RandomPool = (function() {
var origRandom = Math.random;
var pool = [];
var counter = 0;
function next() {
var rand;
if (counter >= pool.length) {
rand = origRandom();
pool.push(rand);
} else {
rand = pool[counter];
}
counter++;
return rand;
};
function seek(index) {
counter = Math.min(index, pool.length);
}
function reset() {
counter = 0;
pool = [];
}
function hook() {
Math.random = next;
}
function unhook() {
Math.random = origRandom;
}
return {
next : next,
seek : seek,
reset : reset,
hook : hook,
unhook : unhook
};
})();
var TerrainModel = (function(){
var model;
function update(opts){
var size = opts.size || 32;
var smoothness = opts.smoothness || 1.0;
var zScale = opts.zScale || 200;
model = generateTerrain(size, size, smoothness);
updateZ(zScale);
}
function updateZ(z){
var width = model[0].length;
var height = model.length;
var terrain = [];
var i, j;
var row;
for(i = 0; i < height; ++i){
row = [];
for(j = 0; j < width; ++j){
row.push(model[i][j] * z);
}
terrain.push(row);
}
$.publish('terrain-update',[terrain]);
}
return {
update : update,
updateZ : updateZ
};
})();
var TerrainView = (function(){
var renderer;
var scene;
var camera;
var mesh;
function init(container){
var $container = $(container);
var width = $container.width();
var height = $container.height();
var ratio = width / height;
var near = 1;
var far = 6000;
var fov = 45;
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(fov, ratio, near, far);
camera.position.y = 400;
if(!Detector.webgl){
renderer = new THREE.CanvasRenderer();
}else{
renderer = new THREE.WebGLRenderer({
clearColor: 0xf1f1f1,
clearAlpha: 1,
antialias: true
});
}
renderer.setSize(width, height);
renderer.shadowCameraNear = 3;
renderer.shadowCameraFar = camera.far;
renderer.shadowCameraFov = 50;
renderer.shadowMapBias = 0.0039;
renderer.shadowMapDarkness = 0.5;
renderer.shadowMapEnabled = true;
renderer.shadowMapSoft = true;
setupLights();
$container.append(renderer.domElement);
}
function setupLights(){
var ambientLight = new THREE.AmbientLight(0xffffff, 0.9);
scene.add(ambientLight);
var mainLight = new THREE.SpotLight(0xffffff, 1.0);
mainLight.position.set(500, 500, 500);
mainLight.castShadow = true;
scene.add(mainLight);
var auxLight = new THREE.SpotLight(0xffffff, 1.0);
auxLight.position.set(-300, 500, -400);
auxLight.castShadow = true;
scene.add(auxLight);
}
function getTerrainMesh(model){
var modelWidth = model[0].length - 1;
var modelHeight = model.length - 1;
var segLength = getOptimalSegLength(modelWidth, modelHeight);
var height = (model.length - 1) * segLength;
var width = (model[0].length - 1) * segLength;
var mesh = new THREE.Mesh(
new THREE.PlaneGeometry(width, height, model.length - 1, model.length - 1),
new THREE.MeshLambertMaterial({
color: 0xbb3355,
wireframe: true,
})
);
mesh.rotation.x = -Math.PI / 2;
mesh.castShadow = true;
mesh.receiveShadow = true;
var vertices = mesh.geometry.vertices;
for(var i = 0; i< model.length; ++i){
for(var j = 0; j < model.length; ++j){
vertices[i * model.length + j].position.z = model[i][j];
}
}
return mesh;
}
function getOptimalSegLength(width, height){
return ~~(640 / Math.max(width, height));
}
function drawCoordinate(center, length){
var orthogonal = [
[new THREE.Vector3(length, 0, 0), 0xff0000],
[new THREE.Vector3(0, length, 0), 0x00ff00],
[new THREE.Vector3(0, 0, length), 0x0000ff]
];
for(var i = 0; i< orthogonal.length; ++i){
var v = orthogonal[i][0];
color = orthogonal[i][1];
var geometry = new THREE.Geometry();
geometry.vertices.push(new THREE.Vertex(center));
geometry.vertices.push(new THREE.Vertex(center.clone().addSelf(v)));
var line = new THREE.Line(
geometry,
new THREE.LineBasicMaterial({
color: color,
opacity: 1,
linewidth: 3
})
);
scene.add(line);
}
}
function getTexture(model, width, height){
var modelWidth = model[0].length - 1;
var modelHeight = model.length;
var ratio = width / modelWidth;
var canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
var context = canvas.getContext('2d');
var imageData = context.getImageData(0, 0, width, height);
var pixels = imageData.data;
var i = 0;
var xx = 0;
var yy = 0;
var rgba;
for(var y = 0; y < height; ++y){
for(var x = 0; x < width; ++x){
yy = ~~(y / ratio);
xx = ~~(x / ratio);
rgba = elevation2RGBA(modelp[yy][xx]);
pixels[i++] = rgba[0];
pixels[i++] = rgba[1];
pixels[i++] = rgba[2];
pixels[i++] = rgba[3];
}
}
context.putImageData(imageData, 0, 0);
texture = new THREE.Texture(
canvas,
new THREE.UVMapping(),
THREE.ClampToEdgeWrapping,
THREE.ClampToEdgeWrapping
);
texture.needsUpdate = true;
return texture;
}
function elevation2RGBA(elevation) {
if (elevation > 0.5) {
return [255, 255, 255, 255];
} else if (elevation > 0){
return [104, 53, 20, 255];
} else {
return [58, 131, 21, 255];
}
}
function update(evt, model){
scene.remove(mesh);
mesh = getTerrainMesh(model);
scene.add(mesh);
}
function animate (interval) {
var timer = new Date().getTime() * 0.0001;
camera.position.x = Math.cos(timer) * 800;
camera.position.z = Math.sin(timer) * 800;
camera.lookAt(scene.position);
requestAnimationFrame(animate, interval);
renderer.render(scene, camera);
}
return {
init : init,
update : update,
animate : animate,
drawCoordinate : drawCoordinate
};
})();
var TerrainController = (function(){
var view = TerrainView;
var model = TerrainModel;
function init () {
RandomPool.hook();
initUI();
animate();
bindEvents();
update();
}
function initUI(){
view.init('#container');
}
function animate () {
view.animate(30);
}
function bindEvents(){
$.subscribe('terrain-update', view.update);
}
function reset(){
RandomPool.reset();
update();
}
function update () {
RandomPool.seek(0);
var opts = {
size : 64,
smoothness : 1.0,
zScale : 220
};
model.update(opts);
}
function updateZ(){
var zScale = 220;
model.updateZ(zScale);
}
return { init : init };
})();
