|
@@ -19,8 +19,8 @@ import {
|
|
|
UNDERSIDE_DARKEN,
|
|
UNDERSIDE_DARKEN,
|
|
|
UNDERSIDE_DEPTH,
|
|
UNDERSIDE_DEPTH,
|
|
|
} from '../config';
|
|
} from '../config';
|
|
|
-import { sampleBilinear, signedDistance } from './distance';
|
|
|
|
|
-import { hash2, makeThicknessTile, makeTile, scaledTileIndex, tileIndex } from './textures';
|
|
|
|
|
|
|
+import { sampleBilinear, signedDistance, smooth121 } from './distance';
|
|
|
|
|
+import { makeThicknessTile, makeTile, scaledTileIndex, tileIndex } from './textures';
|
|
|
|
|
|
|
|
interface ChunkSprite {
|
|
interface ChunkSprite {
|
|
|
ctx: CanvasRenderingContext2D;
|
|
ctx: CanvasRenderingContext2D;
|
|
@@ -72,6 +72,10 @@ export class TerrainView {
|
|
|
private readonly shadeK = new Float32Array(CHUNK_SIZE * CHUNK_SIZE);
|
|
private readonly shadeK = new Float32Array(CHUNK_SIZE * CHUNK_SIZE);
|
|
|
private readonly shadeW = new Float32Array(CHUNK_SIZE * CHUNK_SIZE);
|
|
private readonly shadeW = new Float32Array(CHUNK_SIZE * CHUNK_SIZE);
|
|
|
private readonly cavityK = new Float32Array(CHUNK_SIZE * CHUNK_SIZE);
|
|
private readonly cavityK = new Float32Array(CHUNK_SIZE * CHUNK_SIZE);
|
|
|
|
|
+ /** Output of blend(). */
|
|
|
|
|
+ private br = 0;
|
|
|
|
|
+ private bg = 0;
|
|
|
|
|
+ private bb = 0;
|
|
|
|
|
|
|
|
constructor(
|
|
constructor(
|
|
|
private readonly bitmap: TerrainBitmap,
|
|
private readonly bitmap: TerrainBitmap,
|
|
@@ -206,6 +210,8 @@ export class TerrainView {
|
|
|
return y * width + x;
|
|
return y * width + x;
|
|
|
};
|
|
};
|
|
|
signedDistance((i) => data[at(i)] !== MATERIAL_EMPTY, WIN, WIN, this.sd, M);
|
|
signedDistance((i) => data[at(i)] !== MATERIAL_EMPTY, WIN, WIN, this.sd, M);
|
|
|
|
|
+ // Irons out the little waves a pixel staircase leaves in the contour.
|
|
|
|
|
+ smooth121(this.sd, WIN, WIN);
|
|
|
for (let i = 0; i < WIN * WIN; i++) this.orig[i] = original[at(i)] !== MATERIAL_EMPTY ? 1 : 0;
|
|
for (let i = 0; i < WIN * WIN; i++) this.orig[i] = original[at(i)] !== MATERIAL_EMPTY ? 1 : 0;
|
|
|
|
|
|
|
|
// Per-pixel lighting from column scans: rim light on top faces, darker undersides, cavity shade.
|
|
// Per-pixel lighting from column scans: rim light on top faces, darker undersides, cavity shade.
|
|
@@ -240,16 +246,12 @@ export class TerrainView {
|
|
|
const texH = (y1 - y0) * S;
|
|
const texH = (y1 - y0) * S;
|
|
|
for (let ty = 0; ty < texH; ty++) {
|
|
for (let ty = 0; ty < texH; ty++) {
|
|
|
const py = ty / S;
|
|
const py = ty / S;
|
|
|
- const y = y0 + Math.floor(py);
|
|
|
|
|
// Window coordinates of the texel center.
|
|
// Window coordinates of the texel center.
|
|
|
const v = py + 0.5 / S + M;
|
|
const v = py + 0.5 / S + M;
|
|
|
for (let tx = 0; tx < texW; tx++) {
|
|
for (let tx = 0; tx < texW; tx++) {
|
|
|
const px = tx / S;
|
|
const px = tx / S;
|
|
|
- const x = x0 + Math.floor(px);
|
|
|
|
|
const u = px + 0.5 / S + M;
|
|
const u = px + 0.5 / S + M;
|
|
|
- const p = (y - y0) * CHUNK_SIZE + (x - x0);
|
|
|
|
|
const o = (ty * TEX + tx) * 4;
|
|
const o = (ty * TEX + tx) * 4;
|
|
|
- const i = y * width + x;
|
|
|
|
|
|
|
|
|
|
// Background layer: the inside of the cake where it was dug, and the drop shadow.
|
|
// Background layer: the inside of the cake where it was dug, and the drop shadow.
|
|
|
let bgR = 0;
|
|
let bgR = 0;
|
|
@@ -258,17 +260,12 @@ export class TerrainView {
|
|
|
let bgA = 0;
|
|
let bgA = 0;
|
|
|
const cav = Math.min(1, Math.max(0, (sampleBilinear(origCov, WIN, WIN, u, v) - 0.5) / TERRAIN_AA + 0.5));
|
|
const cav = Math.min(1, Math.max(0, (sampleBilinear(origCov, WIN, WIN, u, v) - 0.5) / TERRAIN_AA + 0.5));
|
|
|
const sh = shadowA * Math.min(1, Math.max(0, sampleBilinear(sd, WIN, WIN, u - DROP_SHADOW_DX, v - DROP_SHADOW_DY) / DROP_SHADOW_SOFTNESS + 0.5));
|
|
const sh = shadowA * Math.min(1, Math.max(0, sampleBilinear(sd, WIN, WIN, u - DROP_SHADOW_DX, v - DROP_SHADOW_DY) / DROP_SHADOW_SOFTNESS + 0.5));
|
|
|
- if (cav > 0) {
|
|
|
|
|
- let om = original[i];
|
|
|
|
|
- if (om === MATERIAL_EMPTY) om = this.nearbyMaterial(original, x, y);
|
|
|
|
|
- if (om !== MATERIAL_EMPTY) {
|
|
|
|
|
- const c = this.tiles[om]![scaledTileIndex(x0 * S + tx, y0 * S + ty, S)];
|
|
|
|
|
- const k = cavityK[p] * (1 - sh * 0.55);
|
|
|
|
|
- bgR = (c >> 16) * k;
|
|
|
|
|
- bgG = ((c >> 8) & 0xff) * k * 0.95;
|
|
|
|
|
- bgB = (c & 0xff) * k * 0.9;
|
|
|
|
|
- bgA = cav;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (cav > 0 && this.blend(original, x0, y0, px, py, x0 * S + tx, y0 * S + ty)) {
|
|
|
|
|
+ const k = sampleBilinear(cavityK, CHUNK_SIZE, CHUNK_SIZE, px, py) * (1 - sh * 0.55);
|
|
|
|
|
+ bgR = this.br * k;
|
|
|
|
|
+ bgG = this.bg * k * 0.95;
|
|
|
|
|
+ bgB = this.bb * k * 0.9;
|
|
|
|
|
+ bgA = cav;
|
|
|
}
|
|
}
|
|
|
if (sh > 0 && bgA < 1) {
|
|
if (sh > 0 && bgA < 1) {
|
|
|
const a = sh * (1 - bgA);
|
|
const a = sh * (1 - bgA);
|
|
@@ -282,28 +279,22 @@ export class TerrainView {
|
|
|
// Terrain layer: textured fill inside a felt-pen outline.
|
|
// Terrain layer: textured fill inside a felt-pen outline.
|
|
|
const d = sampleBilinear(sd, WIN, WIN, u, v);
|
|
const d = sampleBilinear(sd, WIN, WIN, u, v);
|
|
|
const fa = coverage(d);
|
|
const fa = coverage(d);
|
|
|
- let fr = 0;
|
|
|
|
|
- let fg = 0;
|
|
|
|
|
- let fb = 0;
|
|
|
|
|
|
|
+ let fr = OR;
|
|
|
|
|
+ let fg = OG;
|
|
|
|
|
+ let fb = OB;
|
|
|
if (fa > 0) {
|
|
if (fa > 0) {
|
|
|
- const t = this.thickness[tileIndex(x, y)];
|
|
|
|
|
|
|
+ const t = this.thickness[tileIndex(x0 + Math.floor(px), y0 + Math.floor(py))];
|
|
|
const ink = 1 - coverage(d - t);
|
|
const ink = 1 - coverage(d - t);
|
|
|
- // Felt pen: slightly uneven ink.
|
|
|
|
|
- const inkK = 1 + (hash2(x >> 1, y >> 1, 99) - 0.5) * 0.35;
|
|
|
|
|
- fr = OR * inkK;
|
|
|
|
|
- fg = OG * inkK;
|
|
|
|
|
- fb = OB * inkK;
|
|
|
|
|
- const mat = data[i];
|
|
|
|
|
- if (ink < 1 && mat !== MATERIAL_EMPTY) {
|
|
|
|
|
- const c = this.tiles[mat]![scaledTileIndex(x0 * S + tx, y0 * S + ty, S)];
|
|
|
|
|
- const k = shadeK[p];
|
|
|
|
|
- const w = shadeW[p];
|
|
|
|
|
- const r = (c >> 16) * k;
|
|
|
|
|
- const g = ((c >> 8) & 0xff) * k;
|
|
|
|
|
- const b = (c & 0xff) * k;
|
|
|
|
|
- fr = (r + (255 - r) * w) * (1 - ink) + fr * ink;
|
|
|
|
|
- fg = (g + (255 - g) * w) * (1 - ink) + fg * ink;
|
|
|
|
|
- fb = (b + (255 - b) * w) * (1 - ink) + fb * ink;
|
|
|
|
|
|
|
+ if (ink < 1 && this.blend(data, x0, y0, px, py, x0 * S + tx, y0 * S + ty)) {
|
|
|
|
|
+ // Lighting interpolated between pixel centers: no steps in the gradients.
|
|
|
|
|
+ const k = sampleBilinear(shadeK, CHUNK_SIZE, CHUNK_SIZE, px, py);
|
|
|
|
|
+ const w = sampleBilinear(shadeW, CHUNK_SIZE, CHUNK_SIZE, px, py);
|
|
|
|
|
+ const r = this.br * k;
|
|
|
|
|
+ const g = this.bg * k;
|
|
|
|
|
+ const b = this.bb * k;
|
|
|
|
|
+ fr = (r + (255 - r) * w) * (1 - ink) + OR * ink;
|
|
|
|
|
+ fg = (g + (255 - g) * w) * (1 - ink) + OG * ink;
|
|
|
|
|
+ fb = (b + (255 - b) * w) * (1 - ink) + OB * ink;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -320,26 +311,62 @@ export class TerrainView {
|
|
|
chunk.texture.source.update();
|
|
chunk.texture.source.update();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** A material of the original map next to (x, y), for antialiased cavity edges. */
|
|
|
|
|
- private nearbyMaterial(original: Uint8Array, x: number, y: number): number {
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Material color at a point (chunk-local pixel coordinates px, py; texel tx, ty), mixed
|
|
|
|
|
+ * bilinearly between the four nearest pixels so that boundaries between materials are soft
|
|
|
|
|
+ * instead of pixel steps. Empty pixels don't count. Result in br/bg/bb; false if all empty.
|
|
|
|
|
+ */
|
|
|
|
|
+ private blend(grid: Uint8Array, x0: number, y0: number, px: number, py: number, tx: number, ty: number): boolean {
|
|
|
const { width, height } = this.bitmap;
|
|
const { width, height } = this.bitmap;
|
|
|
- for (const [dx, dy] of [
|
|
|
|
|
- [1, 0],
|
|
|
|
|
- [-1, 0],
|
|
|
|
|
- [0, 1],
|
|
|
|
|
- [0, -1],
|
|
|
|
|
- [1, 1],
|
|
|
|
|
- [-1, -1],
|
|
|
|
|
- [1, -1],
|
|
|
|
|
- [-1, 1],
|
|
|
|
|
- ]) {
|
|
|
|
|
- const nx = x + dx;
|
|
|
|
|
- const ny = y + dy;
|
|
|
|
|
- if (nx < 0 || ny < 0 || nx >= width || ny >= height) continue;
|
|
|
|
|
- const m = original[ny * width + nx];
|
|
|
|
|
- if (m !== MATERIAL_EMPTY) return m;
|
|
|
|
|
|
|
+ const fx = px - 0.5;
|
|
|
|
|
+ const fy = py - 0.5;
|
|
|
|
|
+ const ix = Math.floor(fx);
|
|
|
|
|
+ const iy = Math.floor(fy);
|
|
|
|
|
+ const ax = fx - ix;
|
|
|
|
|
+ const ay = fy - iy;
|
|
|
|
|
+ const t = scaledTileIndex(tx, ty, S);
|
|
|
|
|
+ let r = 0;
|
|
|
|
|
+ let g = 0;
|
|
|
|
|
+ let b = 0;
|
|
|
|
|
+ let wsum = 0;
|
|
|
|
|
+ let first = -1;
|
|
|
|
|
+ let same = true;
|
|
|
|
|
+ for (let k = 0; k < 4; k++) {
|
|
|
|
|
+ const x = Math.max(0, Math.min(width - 1, x0 + ix + (k & 1)));
|
|
|
|
|
+ const y = Math.max(0, Math.min(height - 1, y0 + iy + (k >> 1)));
|
|
|
|
|
+ const m = grid[y * width + x];
|
|
|
|
|
+ if (first === -1) first = m;
|
|
|
|
|
+ else if (m !== first) same = false;
|
|
|
|
|
+ if (m === MATERIAL_EMPTY) continue;
|
|
|
|
|
+ const w = (k & 1 ? ax : 1 - ax) * (k >> 1 ? ay : 1 - ay);
|
|
|
|
|
+ if (w <= 0) continue;
|
|
|
|
|
+ const c = this.tiles[m]![t];
|
|
|
|
|
+ r += (c >> 16) * w;
|
|
|
|
|
+ g += ((c >> 8) & 0xff) * w;
|
|
|
|
|
+ b += (c & 0xff) * w;
|
|
|
|
|
+ wsum += w;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (same && first !== MATERIAL_EMPTY) {
|
|
|
|
|
+ const c = this.tiles[first]![t];
|
|
|
|
|
+ this.br = c >> 16;
|
|
|
|
|
+ this.bg = (c >> 8) & 0xff;
|
|
|
|
|
+ this.bb = c & 0xff;
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (wsum === 0) {
|
|
|
|
|
+ // Only the zero-weight corner is solid (texel right on a pixel center line): use any solid neighbor.
|
|
|
|
|
+ const m = grid[Math.max(0, Math.min(height - 1, y0 + Math.floor(py))) * width + Math.max(0, Math.min(width - 1, x0 + Math.floor(px)))];
|
|
|
|
|
+ if (m === MATERIAL_EMPTY) return false;
|
|
|
|
|
+ const c = this.tiles[m]![t];
|
|
|
|
|
+ this.br = c >> 16;
|
|
|
|
|
+ this.bg = (c >> 8) & 0xff;
|
|
|
|
|
+ this.bb = c & 0xff;
|
|
|
|
|
+ return true;
|
|
|
}
|
|
}
|
|
|
- return MATERIAL_EMPTY;
|
|
|
|
|
|
|
+ this.br = r / wsum;
|
|
|
|
|
+ this.bg = g / wsum;
|
|
|
|
|
+ this.bb = b / wsum;
|
|
|
|
|
+ return true;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** Fills the per-column distance tables for column x over rows [y0, y1). */
|
|
/** Fills the per-column distance tables for column x over rows [y0, y1). */
|