Shadow cast algorithm
This commit is contained in:
parent
3bc89af23b
commit
0077def410
|
|
@ -1,4 +1,5 @@
|
|||
import { clamp } from "@common/utils";
|
||||
import { shadowCast } from "./fov";
|
||||
|
||||
interface Point {
|
||||
x: number;
|
||||
|
|
@ -19,9 +20,9 @@ interface BresenhamOptions {
|
|||
directions?: 4 | 8;
|
||||
}
|
||||
|
||||
export interface BresenhamLineOptions extends BresenhamOptions {}
|
||||
export interface BresenhamLineOptions extends BresenhamOptions { }
|
||||
|
||||
interface BresenhamCircleBaseOptions<T extends boolean | 'fov'> extends BresenhamOptions {
|
||||
interface BresenhamCircleBaseOptions<T extends boolean | 'fov' | 'shadow'> extends BresenhamOptions {
|
||||
/**
|
||||
* - `false` (default) — outline only.
|
||||
* - `true` — filled disc (span-from-outline scanline).
|
||||
|
|
@ -31,7 +32,7 @@ interface BresenhamCircleBaseOptions<T extends boolean | 'fov'> extends Bresenha
|
|||
}
|
||||
|
||||
type BresenhamCircleFillOptions = BresenhamCircleBaseOptions<boolean>;
|
||||
interface BresenhamCircleFovOptions extends BresenhamCircleBaseOptions<'fov'> {
|
||||
interface BresenhamCircleFovOptions extends BresenhamCircleBaseOptions<'fov' | 'shadow'> {
|
||||
/**
|
||||
* A callback that is called for each point on the ray casted from the start
|
||||
* point to the current point. If the callback returns `true`, the ray is
|
||||
|
|
@ -323,6 +324,14 @@ export function* bresenhamCircleGen(
|
|||
return;
|
||||
}
|
||||
|
||||
if (options?.fill === 'shadow') {
|
||||
yield* shadowCast(x, y, r, (x, y) => {
|
||||
if (!inBounds(x, y)) return true;
|
||||
return options?.breaker?.(x, y) ?? false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (fill) {
|
||||
// Span-from-outline: use the midpoint algorithm to find the exact
|
||||
// horizontal extent for each row, then fill between those extents.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
export function* shadowCast(
|
||||
x: number,
|
||||
y: number,
|
||||
r: number,
|
||||
breaker: (x: number, y: number) => boolean
|
||||
): Generator<{ x: number; y: number }> {
|
||||
if (r < 0) return;
|
||||
|
||||
const radiusSq = r * r;
|
||||
const seen = new Set<string>();
|
||||
const keyOf = (px: number, py: number) => `${px},${py}`;
|
||||
|
||||
// Yield the source tile itself if it is not blocked.
|
||||
if (!breaker(x, y)) {
|
||||
seen.add(keyOf(x, y));
|
||||
yield { x, y };
|
||||
}
|
||||
|
||||
type Frame = {
|
||||
octant: number;
|
||||
row: number;
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
|
||||
// 8 octants around the origin.
|
||||
const OCTANTS: ReadonlyArray<readonly [number, number, number, number]> = [
|
||||
[1, 0, 0, 1],
|
||||
[0, 1, 1, 0],
|
||||
[0, 1, -1, 0],
|
||||
[1, 0, 0, -1],
|
||||
[-1, 0, 0, -1],
|
||||
[0, -1, -1, 0],
|
||||
[0, -1, 1, 0],
|
||||
[-1, 0, 0, 1],
|
||||
];
|
||||
|
||||
const stack: Frame[] = [];
|
||||
for (let octant = 7; octant >= 0; octant--) {
|
||||
stack.push({ octant, row: 1, start: 1.0, end: 0.0 });
|
||||
}
|
||||
|
||||
while (stack.length > 0) {
|
||||
const frame = stack.pop()!;
|
||||
if (frame.start < frame.end) continue;
|
||||
|
||||
const [xx, xy, yx, yy] = OCTANTS[frame.octant];
|
||||
|
||||
let start = frame.start;
|
||||
let blocked = false;
|
||||
let newStart = start;
|
||||
|
||||
for (let row = frame.row; row <= r; row++) {
|
||||
let dx = -row - 1;
|
||||
const dy = -row;
|
||||
|
||||
while (dx <= 0) {
|
||||
dx++;
|
||||
|
||||
const mapX = x + dx * xx + dy * xy;
|
||||
const mapY = y + dx * yx + dy * yy;
|
||||
|
||||
const leftSlope = (dx - 0.5) / (dy + 0.5);
|
||||
const rightSlope = (dx + 0.5) / (dy - 0.5);
|
||||
|
||||
if (start < rightSlope) continue;
|
||||
if (frame.end > leftSlope) break;
|
||||
|
||||
const opaque = breaker(mapX, mapY);
|
||||
|
||||
if (dx * dx + dy * dy <= radiusSq && !opaque) {
|
||||
const key = keyOf(mapX, mapY);
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
yield { x: mapX, y: mapY };
|
||||
}
|
||||
}
|
||||
|
||||
if (blocked) {
|
||||
if (opaque) {
|
||||
newStart = rightSlope;
|
||||
continue;
|
||||
}
|
||||
|
||||
blocked = false;
|
||||
start = newStart;
|
||||
} else if (opaque && row < r) {
|
||||
blocked = true;
|
||||
stack.push({
|
||||
octant: frame.octant,
|
||||
row: row + 1,
|
||||
start,
|
||||
end: leftSlope,
|
||||
});
|
||||
newStart = rightSlope;
|
||||
}
|
||||
}
|
||||
|
||||
if (blocked) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { createCanvas } from "@common/display/canvas";
|
||||
import { gameLoop } from "@common/game";
|
||||
import { bresenhamCircleGen } from "@common/navigation/bresenham";
|
||||
import { circleGen } from "@common/navigation/bresenham";
|
||||
import Physics from "@common/physics";
|
||||
import { mapNumber } from "@common/utils";
|
||||
|
||||
|
|
@ -162,7 +162,7 @@ const frame = (dt: number, state: State) => {
|
|||
|
||||
function fillCircle(ctx: CanvasRenderingContext2D, xc: number, yc: number, r: number) {
|
||||
// because default circle fill is antialiased
|
||||
for (const { x, y } of bresenhamCircleGen(xc, yc, r, { fill: true })) {
|
||||
for (const { x, y } of circleGen(xc, yc, r, { fill: true })) {
|
||||
ctx.fillRect(x, y, 1, 1);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { bresenhamCircleGen } from "@common/navigation/bresenham";
|
|||
|
||||
const S = 20;
|
||||
|
||||
const breaker = (x: number, y: number) => Math.hypot(x - S * 0.3, y - S * 0.3) <= S / 16;
|
||||
|
||||
export default async function main() {
|
||||
const canvas = createCanvas(S, S);
|
||||
|
||||
|
|
@ -10,12 +12,11 @@ export default async function main() {
|
|||
if (!ctx) return;
|
||||
|
||||
ctx.fillStyle = 'black';
|
||||
for (const fill of [true, false, 'fov'] as const) {
|
||||
for (const fill of ['fov', 'shadow'] as const) {
|
||||
let i = 0;
|
||||
|
||||
console.time(`fill=${fill}`);
|
||||
// @ts-ignore
|
||||
for (const { x, y } of bresenhamCircleGen(S / 2, S / 2, S * 0.4, { fill })) {
|
||||
for (const { x, y } of bresenhamCircleGen(S / 2, S / 2, S * 0.4, { fill, breaker })) {
|
||||
i++;
|
||||
ctx.fillRect(x, y, 1, 1);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue