wmc/terrain.js
2021-12-20 01:25:40 -08:00

151 lines
4.0 KiB
JavaScript

const smoothstep = x => x*x*(3 - 2*x);
function interpolate(a, b, x, f = smoothstep) {
const val = a + f(x) * (b - a);
return val;
}
const sigmoid = (a, b, f = smoothstep) => x => {
if (x < a) return 0;
if (x > b) return 1;
return f((x - a) / (b - a));
};
function ghettoPerlinNoise(seed, x, y, gridSize = 16) {
const dot = (vx, vy) => vx[0] * vy[0] + vx[1] * vy[1];
// super ghetto random
const xorshift = (x) => {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
return x;
};
const randGrad = (x0, y0) => {
const rand = xorshift(1337 * x0 + seed + 80085 * y0);
return [Math.sin(rand), Math.cos(rand)];
};
const x0 = Math.floor(x / gridSize);
const y0 = Math.floor(y / gridSize);
const sx = x / gridSize - x0;
const sy = y / gridSize - y0;
const n0 = dot(randGrad(x0, y0), [sx, sy]);
const n1 = dot(randGrad(x0 + 1, y0), [sx - 1, sy]);
const n2 = dot(randGrad(x0, y0 + 1), [sx, sy - 1]);
const n3 = dot(randGrad(x0 + 1, y0 + 1), [sx - 1, sy - 1]);
return 1.5 * interpolate(interpolate(n0, n1, sx), interpolate(n2, n3, sx), sy);
}
function cliffPerlin(seed, x, y) {
const noise1 = ghettoPerlinNoise(seed, x, y);
const noise2 = ghettoPerlinNoise(seed+1, x, y);
const softerEdge = x => {
if (x < 0.1) return 0.5 - 5 * x;
if (x > 0.8) return 1.0 - 2.5 * (x - 0.8);
return (x - 0.1) / 0.8;
};
return interpolate(-1, 1, 0.5 * (1 + Math.atan2(noise1, noise2) / Math.PI), softerEdge);
}
export function makeTerrain(x, y) {
const seed = 1337;
const lacunarity = 2.1;
const persistence = 0.35;
const noiseMap = (x, y) => cliffPerlin(seed, x / 2, y / 2);
const fractalNoise = (x, y) => {
let value = 0;
let power = 0.6;
let scale = 0.1;
const noises = [ghettoPerlinNoise, ghettoPerlinNoise, cliffPerlin, cliffPerlin];
for (const noiseFun of noises) {
const noise = noiseFun(seed, scale * x, scale * y);
value += noise * power;
power *= persistence;
scale *= lacunarity;
}
return interpolate(-0.2, 0.6, value);//, x=>x*x); // between 0 and 1
}
const scaledNoise = (x, y) => noiseMap(x / 1, y / 1);
const outputNoise = fractalNoise;
// const outputNoise = scaledNoise;
const terrain = new Uint8Array(16 * 16);
for (let i = 0; i < 16; i++) {
for (let j = 0; j < 16; j++) {
terrain[i * 16 + j] = Math.floor(64 + 64 * outputNoise(x + i, y + j));
}
}
return terrain;
}
function colorInterp(x) {
const ocean = [0.0, 0.0, 0.5];
const grass = [0.2, 0.7, 0.2];
const mountain = [0.4, 0.3, 0.1];
const snow = [0.9, 0.9, 0.9];
const grassHeight = 75 / 255;
const mountainHeight = 100 / 255;
const interp = dim => {
return x => {
if (x < grassHeight) {
return interpolate(ocean[dim], grass[dim], x / grassHeight, sigmoid(0.6, 0.95));
} else if (x < mountainHeight) {
return interpolate(grass[dim], mountain[dim], (x - grassHeight) / (mountainHeight - grassHeight));
} else {
return interpolate(mountain[dim], snow[dim], (x - mountainHeight) / (1.0 - mountainHeight), sigmoid(0, 0.2));
}
};
};
return {
r: 255 * interp(0)(x/255),
g: 255 * interp(1)(x/255),
b: 255 * interp(2)(x/255),
};
}
function main() {
const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'gray';
ctx.fill();
for (let i = 0; i < canvas.width; i += 16) {
for (let j = 0; j < canvas.height; j += 16) {
const imgdat = ctx.createImageData(16, 16);
const dat = imgdat.data;
const chunk = makeTerrain(i - canvas.width / 2, j - canvas.height / 2);
for (let k = 0; k < 16; k++) {
for (let l = 0; l < 16; l++) {
const offset = 4 * (16 * (15 - l) + k);
const val = chunk[16 * k + l];
dat[offset + 0] = colorInterp(val).r;
dat[offset + 1] = colorInterp(val).g;
dat[offset + 2] = colorInterp(val).b;
dat[offset + 3] = 255;
}
}
ctx.putImageData(imgdat, i, canvas.height - 16 - j);
}
}
}
//window.onload = main;