2021-12-28 16:45:31 +00:00
|
|
|
import { memoize } from "./memoize";
|
|
|
|
|
2021-12-20 09:25:40 +00:00
|
|
|
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));
|
|
|
|
};
|
|
|
|
|
2021-12-24 13:50:13 +00:00
|
|
|
// super ghetto random
|
|
|
|
function xorshift(x) {
|
|
|
|
x ^= x << 13;
|
|
|
|
x ^= x >> 7;
|
|
|
|
x ^= x << 17;
|
|
|
|
return x;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function random(seed, z, x) {
|
|
|
|
return xorshift(1337 * z + seed + 80085 * x);
|
|
|
|
}
|
|
|
|
|
2021-12-28 16:45:31 +00:00
|
|
|
export function random3d(seed, z, x, y) {
|
|
|
|
return xorshift(1337 * z + seed + 80085 * x + 13 * y);
|
|
|
|
}
|
|
|
|
|
2021-12-20 09:25:40 +00:00
|
|
|
function ghettoPerlinNoise(seed, x, y, gridSize = 16) {
|
|
|
|
const dot = (vx, vy) => vx[0] * vy[0] + vx[1] * vy[1];
|
|
|
|
|
|
|
|
const randGrad = (x0, y0) => {
|
2021-12-24 13:50:13 +00:00
|
|
|
const rand = random(seed, x0, y0);
|
2021-12-20 09:25:40 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2021-12-28 16:45:31 +00:00
|
|
|
function gpn3d(seed, x, y, z, gridSize = 16) {
|
|
|
|
const dot = (u, v) => u[0] * v[0] + u[1] * v[1] + u[2] * v[2];
|
|
|
|
|
|
|
|
const randGrad = (x0, y0, z0) => {
|
|
|
|
const rand0 = random3d(seed, x0, y0, z0);
|
|
|
|
const rand1 = random3d(seed+1, x0, y0, z0);
|
|
|
|
return [Math.cos(rand0), Math.sin(rand1) * Math.sin(rand0), Math.cos(rand1) * Math.sin(rand0)];
|
|
|
|
};
|
|
|
|
|
|
|
|
const x0 = Math.floor(x / gridSize);
|
|
|
|
const y0 = Math.floor(y / gridSize);
|
|
|
|
const z0 = Math.floor(z / gridSize);
|
|
|
|
const sx = x / gridSize - x0;
|
|
|
|
const sy = y / gridSize - y0;
|
|
|
|
const sz = z / gridSize - z0;
|
|
|
|
|
|
|
|
const n0 = dot(randGrad(x0, y0, z0), [sx, sy, sz]);
|
|
|
|
const n1 = dot(randGrad(x0 + 1, y0, z0), [sx - 1, sy, sz]);
|
|
|
|
const n2 = dot(randGrad(x0, y0 + 1, z0), [sx, sy - 1, sz]);
|
|
|
|
const n3 = dot(randGrad(x0 + 1, y0 + 1, z0), [sx - 1, sy - 1, sz]);
|
|
|
|
|
|
|
|
const n4 = dot(randGrad(x0, y0, z0 + 1), [sx, sy, sz - 1]);
|
|
|
|
const n5 = dot(randGrad(x0 + 1, y0, z0 + 1), [sx - 1, sy, sz - 1]);
|
|
|
|
const n6 = dot(randGrad(x0, y0 + 1, z0 + 1), [sx, sy - 1, sz - 1]);
|
|
|
|
const n7 = dot(randGrad(x0 + 1, y0 + 1, z0 + 1), [sx - 1, sy - 1, sz - 1]);
|
|
|
|
|
|
|
|
return 1.5 * interpolate(
|
|
|
|
interpolate(interpolate(n0, n1, sx), interpolate(n2, n3, sx), sy),
|
|
|
|
interpolate(interpolate(n4, n5, sx), interpolate(n6, n7, sx), sy),
|
|
|
|
sz,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-12-20 09:25:40 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2021-12-28 16:45:31 +00:00
|
|
|
function _fractalGpn3d(seed, x, y, z) {
|
|
|
|
const lacunarity = 3.4;
|
|
|
|
const persistence = 0.28;
|
|
|
|
|
|
|
|
let value = 0;
|
|
|
|
let power = 0.6;
|
|
|
|
let scale = 0.7;
|
|
|
|
const noises = [gpn3d, gpn3d, gpn3d];
|
|
|
|
|
|
|
|
for (const noiseFun of noises) {
|
|
|
|
const noise = noiseFun(seed, scale * x, scale * y, scale * z);
|
|
|
|
|
|
|
|
value += noise * power;
|
|
|
|
|
|
|
|
power *= persistence;
|
|
|
|
scale *= lacunarity;
|
|
|
|
}
|
|
|
|
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
const fractalGpn3d = memoize(_fractalGpn3d);
|
|
|
|
|
|
|
|
export function checkCave(seed, x, y, z) {
|
|
|
|
const nx = Math.floor(x / 8) * 8;
|
|
|
|
const ny = Math.floor(y / 4) * 4;
|
|
|
|
const nz = Math.floor(z / 8) * 8;
|
|
|
|
|
|
|
|
const sx = (x - nx) / 8;
|
|
|
|
const sy = (y - ny) / 4;
|
|
|
|
const sz = (z - nz) / 8;
|
|
|
|
|
|
|
|
const itp = interpolate;
|
|
|
|
const n = (x, y, z) => fractalGpn3d(seed, x / 3, y, z / 3);
|
|
|
|
|
|
|
|
const noise = itp(
|
|
|
|
itp(itp(n(nx, ny, nz), n(nx + 8, ny, nz), sx),
|
|
|
|
itp(n(nx, ny + 4, nz), n(nx + 8, ny + 4, nz), sx),
|
|
|
|
sy
|
|
|
|
),
|
|
|
|
itp(itp(n(nx, ny, nz + 8), n(nx + 8, ny, nz + 8), sx),
|
|
|
|
itp(n(nx, ny + 4, nz + 8), n(nx + 8, ny + 4, nz + 8), sx),
|
|
|
|
sy
|
|
|
|
),
|
|
|
|
sz);
|
|
|
|
|
|
|
|
//if (n(nx, ny, nz) > 0.3) throw Error('break!');
|
|
|
|
|
|
|
|
return noise > 0.28 && noise < 0.55;
|
|
|
|
}
|
|
|
|
|
2021-12-24 13:50:13 +00:00
|
|
|
export function makeTerrain(seed, x, y) {
|
2021-12-20 09:25:40 +00:00
|
|
|
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');
|
2021-12-24 13:50:13 +00:00
|
|
|
const seed = 1337;
|
2021-12-20 09:25:40 +00:00
|
|
|
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;
|
|
|
|
|
2021-12-24 13:50:13 +00:00
|
|
|
const chunk = makeTerrain(seed, i - canvas.width / 2, j - canvas.height / 2);
|
2021-12-20 09:25:40 +00:00
|
|
|
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;
|