Start using the sdf-2d library

This commit is contained in:
schmelczerandras 2020-09-21 09:42:00 +02:00
parent 99f47e2961
commit 1b7dee4be0
56 changed files with 314 additions and 2119 deletions

24
frontend/custom.d.ts vendored
View file

@ -1,24 +0,0 @@
declare module '*.glsl' {
const content: string;
export default content;
}
/*
declare module '*.png' {
import { ResponsiveImage } from 'src/framework/model/misc';
const content: ResponsiveImage;
export default content;
}
declare module '*.jpg' {
import { ResponsiveImage } from 'src/framework/model/misc';
const content: ResponsiveImage;
export default content;
}
declare module '*.jpeg' {
import { ResponsiveImage } from 'src/framework/model/misc';
const content: ResponsiveImage;
export default content;
}
*/

View file

@ -7,10 +7,10 @@
"scripts": { "scripts": {
"start": "webpack-dev-server --mode development", "start": "webpack-dev-server --mode development",
"lint": "npx eslint --fix \"src/**/*.ts\" && npx prettier --write \"src/**/*.ts\"", "lint": "npx eslint --fix \"src/**/*.ts\" && npx prettier --write \"src/**/*.ts\"",
"build": "webpack && find dist -type f -not -name '*.html' | xargs rm" "build": "webpack --mode production && find dist -type f -not -name '*.html' | xargs rm"
}, },
"keywords": [], "keywords": [],
"author": "András Schmelczer", "author": "András Schmelczer <andras@schmelczer.dev> (https://schmelczer.dev/)",
"postcss": { "postcss": {
"plugins": { "plugins": {
"autoprefixer": {} "autoprefixer": {}
@ -22,9 +22,6 @@
"sideEffects": [ "sideEffects": [
"*.scss" "*.scss"
], ],
"optimization": {
"usedExports": true
},
"devDependencies": { "devDependencies": {
"@types/gl-matrix": "^2.4.5", "@types/gl-matrix": "^2.4.5",
"@types/uuid": "^8.0.0", "@types/uuid": "^8.0.0",
@ -35,6 +32,7 @@
"css-loader": "^3.5.2", "css-loader": "^3.5.2",
"cssnano": "^4.1.10", "cssnano": "^4.1.10",
"eslint": "^7.2.0", "eslint": "^7.2.0",
"sdf-2d": "^0.1.0-alpha",
"eslint-config-prettier": "^6.11.0", "eslint-config-prettier": "^6.11.0",
"eslint-plugin-import": "^2.21.2", "eslint-plugin-import": "^2.21.2",
"eslint-plugin-prettier": "^3.1.4", "eslint-plugin-prettier": "^3.1.4",

View file

@ -1,13 +1,27 @@
import { glMatrix, vec2 } from 'gl-matrix'; import { glMatrix } from 'gl-matrix';
import { Game } from './scripts/game'; import { Game } from './scripts/game';
import { applyArrayPlugins } from './scripts/helper/array';
import { Random } from './scripts/helper/random'; import { Random } from './scripts/helper/random';
import TunnelShape from './scripts/shapes/types/tunnel-shape';
import './styles/main.scss'; import './styles/main.scss';
glMatrix.setMatrixArrayType(Array); glMatrix.setMatrixArrayType(
applyArrayPlugins(); Array
); /*
}
}
zeroes.forEach((z) => {
const blueIndex = z.y * (width * 4) + z.x * 4 + 2;
imageData.data[blueIndex] = 255;
imageData.data[blueIndex - 1] = 255;
imageData.data[blueIndex - 2] = 255;
});
console.log(errors);
ctx.putImageData(imageData, 0, 0);
};*/ //testSDF(); // extract as new image (data-uri)
/*
const testSDF = () => { const testSDF = () => {
Random.seed = 44; Random.seed = 44;
@ -24,19 +38,32 @@ const testSDF = () => {
res = -Math.log2(res) / k; res = -Math.log2(res) / k;
return -res; return -res;
*/ */ /*let min = 1000;
/*let min = 1000;
for (const t of objects) { for (const t of objects) {
min = Math.min(min, t.distance(position)); min = Math.min(min, t.distance(position));
} }
if (min < 0) { if (min < 0) {
// min = Math.min(min, vec2.distance(position, vec2.fromValues(40, 80 - 31.62)) - 31.62); // min = Math.min(min, vec2.distance(position, vec2.fromValues(40, 80 - 31.62)) - 31.62);
} }
return -min;*/ return -min;*/ /*
const tree = new BoundingBoxTree([
new BoundingBox(300, 550, 150, 550, 'A'),
new BoundingBox(400, 800, 50, 200, 'B'),
new BoundingBox(450, 500, 175, 185, 'C'),
new BoundingBox(100, 200, 100, 500, 'D'),
new BoundingBox(750, 950, 450, 600, 'E'),
new BoundingBox(940, 1000, -2, 180, 'F'),
new BoundingBox(960, 1050, 50, 190, 'G'),
new BoundingBox(150, 900, 0, 575, 'H'),
new BoundingBox(-10000, 10000, -10000, 10000, 'I'),
]);
tree.print();
console.log(tree.findIntersecting(new BoundingBox(960, 1050, 50, 190, 'G')));
*/ /*
return -vec2.distance(position, vec2.fromValues(40, 40)) + 30; return -vec2.distance(position, vec2.fromValues(40, 40)) + 30;
}; };
/*
const width = 80; const width = 80;
const height = 100; const height = 100;
@ -92,43 +119,7 @@ const testSDF = () => {
/*if (zeroes.find((z) => vec2.distance(z, position) < dist) !== undefined) { /*if (zeroes.find((z) => vec2.distance(z, position) < dist) !== undefined) {
const blueIndex = y * (width * 4) + x * 4 + 2; const blueIndex = y * (width * 4) + x * 4 + 2;
imageData.data[blueIndex] = 255; imageData.data[blueIndex] = 255;
}*/ }*/ const main = async () => {
}
}
zeroes.forEach((z) => {
const blueIndex = z.y * (width * 4) + z.x * 4 + 2;
imageData.data[blueIndex] = 255;
imageData.data[blueIndex - 1] = 255;
imageData.data[blueIndex - 2] = 255;
});
console.log(errors);
ctx.putImageData(imageData, 0, 0);
};
//testSDF();
// extract as new image (data-uri)
/*
const tree = new BoundingBoxTree([
new BoundingBox(300, 550, 150, 550, 'A'),
new BoundingBox(400, 800, 50, 200, 'B'),
new BoundingBox(450, 500, 175, 185, 'C'),
new BoundingBox(100, 200, 100, 500, 'D'),
new BoundingBox(750, 950, 450, 600, 'E'),
new BoundingBox(940, 1000, -2, 180, 'F'),
new BoundingBox(960, 1050, 50, 190, 'G'),
new BoundingBox(150, 900, 0, 575, 'H'),
new BoundingBox(-10000, 10000, -10000, 10000, 'I'),
]);
tree.print();
console.log(tree.findIntersecting(new BoundingBox(960, 1050, 50, 190, 'G')));
*/
const main = async () => {
try { try {
Random.seed = 42; Random.seed = 42;
await new Game().start(); await new Game().start();

View file

@ -1,9 +1,8 @@
import { vec3 } from 'gl-matrix';
import { CircleLight, compile, Flashlight, Renderer } from 'sdf-2d';
import { CommandBroadcaster } from './commands/command-broadcaster'; import { CommandBroadcaster } from './commands/command-broadcaster';
import { BeforeRenderCommand } from './graphics/commands/before-render';
import { RenderCommand } from './graphics/commands/render'; import { RenderCommand } from './graphics/commands/render';
import { IRenderer } from './graphics/i-renderer'; import { prettyPrint } from './helper/pretty-print';
import { WebGl2Renderer } from './graphics/rendering/webgl2-renderer';
import { timeIt } from './helper/timing';
import { IGame } from './i-game'; import { IGame } from './i-game';
import { KeyboardListener } from './input/keyboard-listener'; import { KeyboardListener } from './input/keyboard-listener';
import { MouseListener } from './input/mouse-listener'; import { MouseListener } from './input/mouse-listener';
@ -12,13 +11,14 @@ import { GameObject } from './objects/game-object';
import { Objects } from './objects/objects'; import { Objects } from './objects/objects';
import { Camera } from './objects/types/camera'; import { Camera } from './objects/types/camera';
import { Character } from './objects/types/character'; import { Character } from './objects/types/character';
import { InfoText } from './objects/types/info-text';
import { createDungeon } from './objects/world/create-dungeon'; import { createDungeon } from './objects/world/create-dungeon';
import { MoveToCommand } from './physics/commands/move-to'; import { MoveToCommand } from './physics/commands/move-to';
import { StepCommand } from './physics/commands/step'; import { StepCommand } from './physics/commands/step';
import { TeleportToCommand } from './physics/commands/teleport-to'; import { TeleportToCommand } from './physics/commands/teleport-to';
import { Physics } from './physics/physics'; import { Physics } from './physics/physics';
import { BoundingBoxBase } from './shapes/bounding-box-base'; import { BoundingBoxBase } from './shapes/bounding-box-base';
import { CircleShape } from './shapes/types/circle-shape';
import { TunnelShape } from './shapes/types/tunnel-shape';
export class Game implements IGame { export class Game implements IGame {
public readonly objects = new Objects(); public readonly objects = new Objects();
@ -26,14 +26,13 @@ export class Game implements IGame {
public readonly camera = new Camera(); public readonly camera = new Camera();
private previousTime?: DOMHighResTimeStamp = null; private previousTime?: DOMHighResTimeStamp = null;
private previousFpsValues: Array<number> = []; private previousFpsValues: Array<number> = [];
private infoText = new InfoText();
private character: Character; private character: Character;
private renderer: IRenderer; private renderer: Renderer;
private initializeRendererPromise: Promise<void>; private rendererPromise: Promise<Renderer>;
private overlay: HTMLElement = document.querySelector('#overlay');
constructor() { constructor() {
const canvas: HTMLCanvasElement = document.querySelector('canvas#main'); const canvas: HTMLCanvasElement = document.querySelector('canvas#main');
const overlay: HTMLElement = document.querySelector('#overlay');
document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this)); document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
@ -46,14 +45,31 @@ export class Game implements IGame {
[this.objects] [this.objects]
); );
this.renderer = new WebGl2Renderer(canvas, overlay); this.rendererPromise = compile(
this.initializeRendererPromise = this.renderer.initialize(); canvas,
[
CircleShape.descriptor,
TunnelShape.descriptor,
Flashlight.descriptor,
CircleLight.descriptor,
],
[
vec3.fromValues(0, 0, 0),
vec3.fromValues(224 / 255, 96 / 255, 126 / 255),
vec3.fromValues(119 / 255, 173 / 255, 120 / 255),
]
);
this.initializeScene(); this.initializeScene();
this.physics.start(); this.physics.start();
} }
public async start(): Promise<void> { public async start(): Promise<void> {
await this.initializeRendererPromise; this.renderer = await this.rendererPromise;
this.renderer.setRuntimeSettings({
isWorldInverted: true,
ambientLight: vec3.fromValues(0.35, 0.1, 0.45),
shadowLength: 300,
});
requestAnimationFrame(this.gameLoop.bind(this)); requestAnimationFrame(this.gameLoop.bind(this));
} }
@ -70,8 +86,6 @@ export class Game implements IGame {
} }
private initializeScene() { private initializeScene() {
this.objects.addObject(this.infoText);
const start = createDungeon(this.objects, this.physics); const start = createDungeon(this.objects, this.physics);
createDungeon(this.objects, this.physics); createDungeon(this.objects, this.physics);
@ -90,7 +104,6 @@ export class Game implements IGame {
} }
} }
@timeIt()
private gameLoop(time: DOMHighResTimeStamp) { private gameLoop(time: DOMHighResTimeStamp) {
if (this.previousTime === null) { if (this.previousTime === null) {
this.previousTime = time; this.previousTime = time;
@ -98,44 +111,26 @@ export class Game implements IGame {
const deltaTime = time - this.previousTime; const deltaTime = time - this.previousTime;
this.previousTime = time; this.previousTime = time;
this.calculateFps(deltaTime);
this.objects.sendCommand(new StepCommand(deltaTime)); this.objects.sendCommand(new StepCommand(deltaTime));
this.camera.sendCommand(new MoveToCommand(this.character.position)); this.camera.sendCommand(new MoveToCommand(this.character.position));
this.renderer.startFrame(deltaTime); this.camera.sendCommand(new RenderCommand(this.renderer));
this.camera.sendCommand(new BeforeRenderCommand(this.renderer));
const shouldBeDrawn = this.physics const shouldBeDrawn = this.physics
.findIntersecting(this.camera.viewArea) .findIntersecting(this.camera.viewArea)
.map((b) => b.shape?.gameObject); .map((b) => b.shape?.gameObject);
for (const object of shouldBeDrawn) { for (const object of shouldBeDrawn) {
object?.sendCommand(new BeforeRenderCommand(this.renderer));
object?.sendCommand(new RenderCommand(this.renderer)); object?.sendCommand(new RenderCommand(this.renderer));
} }
this.character.sendCommand(new RenderCommand(this.renderer)); this.character.sendCommand(new RenderCommand(this.renderer));
this.infoText.sendCommand(new RenderCommand(this.renderer)); this.renderer.renderDrawables();
this.renderer.finishFrame();
this.overlay.innerText = prettyPrint(this.renderer.insights);
localStorage.setItem('character-position', JSON.stringify(this.character.position)); localStorage.setItem('character-position', JSON.stringify(this.character.position));
window.requestAnimationFrame(this.gameLoop.bind(this)); requestAnimationFrame(this.gameLoop.bind(this));
}
private calculateFps(deltaTime: number) {
this.previousFpsValues.push(1000 / deltaTime);
if (this.previousFpsValues.length > 30) {
this.previousFpsValues.sort();
const min = this.previousFpsValues[0];
const median = this.previousFpsValues[
Math.floor(this.previousFpsValues.length / 2)
];
InfoText.modifyRecord('FPS', { min, median });
this.previousFpsValues = [];
}
} }
} }

View file

@ -1,12 +0,0 @@
import { Command } from '../../commands/command';
import { IRenderer } from '../i-renderer';
export class BeforeRenderCommand extends Command {
public constructor(public readonly renderer?: IRenderer) {
super();
}
public get type(): string {
return 'BeforeRenderCommand';
}
}

View file

@ -1,8 +1,8 @@
import { Renderer } from 'sdf-2d';
import { Command } from '../../commands/command'; import { Command } from '../../commands/command';
import { IRenderer } from '../../graphics/i-renderer';
export class RenderCommand extends Command { export class RenderCommand extends Command {
public constructor(public readonly renderer?: IRenderer) { public constructor(public readonly renderer?: Renderer) {
super(); super();
} }

View file

@ -1,32 +0,0 @@
import { mat2d, vec2 } from 'gl-matrix';
import { Blob } from '../../shapes/types/blob';
import { settings } from '../settings';
import { IDrawable } from './i-drawable';
import { IDrawableDescriptor } from './i-drawable-descriptor';
export class DrawableBlob extends Blob implements IDrawable {
public static descriptor: IDrawableDescriptor = {
uniformName: 'blobs',
countMacroName: 'blobCount',
shaderCombinationSteps: settings.shaderCombinations.blobSteps,
empty: new DrawableBlob(vec2.fromValues(0, 0)),
};
public serializeToUniforms(uniforms: any, scale: number, transform: mat2d): void {
const { uniformName } = DrawableBlob.descriptor;
if (!Object.prototype.hasOwnProperty.call(uniforms, uniformName)) {
uniforms[uniformName] = [];
}
uniforms[uniformName].push({
headCenter: vec2.transformMat2d(vec2.create(), this.head.center, transform),
leftFootCenter: vec2.transformMat2d(vec2.create(), this.leftFoot.center, transform),
rightFootCenter: vec2.transformMat2d(
vec2.create(),
this.rightFoot.center,
transform
),
headRadius: this.headRadius * scale,
footRadius: this.footRadius * scale,
});
}
}

View file

@ -1,28 +0,0 @@
import { mat2d, vec2 } from 'gl-matrix';
import TunnelShape from '../../shapes/types/tunnel-shape';
import { settings } from '../settings';
import { IDrawable } from './i-drawable';
import { IDrawableDescriptor } from './i-drawable-descriptor';
export class DrawableTunnel extends TunnelShape implements IDrawable {
public static descriptor: IDrawableDescriptor = {
uniformName: 'lines',
countMacroName: 'lineCount',
shaderCombinationSteps: settings.shaderCombinations.lineSteps,
empty: new DrawableTunnel(vec2.fromValues(0, 0), vec2.fromValues(0, 0), 0, 0),
};
public serializeToUniforms(uniforms: any, scale: number, transform: mat2d): void {
const { uniformName } = DrawableTunnel.descriptor;
if (!Object.prototype.hasOwnProperty.call(uniforms, uniformName)) {
uniforms[uniformName] = [];
}
uniforms[uniformName].push({
from: vec2.transformMat2d(vec2.create(), this.from, transform),
toFromDelta: vec2.scale(vec2.create(), this.toFromDelta, scale),
fromRadius: this.fromRadius * scale,
toRadius: this.toRadius * scale,
});
}
}

View file

@ -1,8 +0,0 @@
import { IDrawable } from './i-drawable';
export interface IDrawableDescriptor {
uniformName: string;
countMacroName: string;
shaderCombinationSteps: Array<number>;
readonly empty: IDrawable;
}

View file

@ -1,6 +0,0 @@
import { vec2, mat2d } from 'gl-matrix';
export interface IDrawable {
distance(target: vec2): number;
serializeToUniforms(uniforms: any, scale: number, transform: mat2d): void;
}

View file

@ -1,46 +0,0 @@
import { mat2d, vec2, vec3 } from 'gl-matrix';
import { settings } from '../../settings';
import { IDrawableDescriptor } from '../i-drawable-descriptor';
import { ILight } from './i-light';
export class CircleLight implements ILight {
public static descriptor: IDrawableDescriptor = {
uniformName: 'circleLights',
countMacroName: 'circleLightCount',
shaderCombinationSteps: settings.shaderCombinations.circleLightSteps,
empty: new CircleLight(vec2.fromValues(0, 0), 0, vec3.fromValues(0, 0, 0), 0),
};
constructor(
public center: vec2,
public lightDrop: number,
public color: vec3,
public lightness: number
) {}
public distance(_: vec2): number {
return 0;
}
public serializeToUniforms(uniforms: any, scale: number, transform: mat2d): void {
const { uniformName } = CircleLight.descriptor;
if (!Object.prototype.hasOwnProperty.call(uniforms, uniformName)) {
uniforms[uniformName] = [];
}
uniforms[uniformName].push({
center: vec2.transformMat2d(vec2.create(), this.center, transform),
lightDrop: this.lightDrop,
value: this.value,
});
}
public get value(): vec3 {
return vec3.scale(
vec3.create(),
vec3.normalize(this.color, this.color),
this.lightness
);
}
}

View file

@ -1,50 +0,0 @@
import { mat2d, vec2, vec3 } from 'gl-matrix';
import { settings } from '../../settings';
import { IDrawableDescriptor } from '../i-drawable-descriptor';
import { ILight } from './i-light';
export class Flashlight implements ILight {
public static descriptor: IDrawableDescriptor = {
uniformName: 'flashlights',
countMacroName: 'flashlightCount',
shaderCombinationSteps: settings.shaderCombinations.flashlightSteps,
empty: new Flashlight(
vec2.fromValues(0, 0),
vec2.fromValues(0, 0),
0,
vec3.fromValues(0, 0, 0),
0
),
};
public constructor(
public center: vec2,
public direction: vec2,
public lightDrop: number,
public color: vec3,
public lightness: number
) {}
public distance(_: vec2): number {
return 0;
}
public serializeToUniforms(uniforms: any, scale: number, transform: mat2d): void {
const listName = Flashlight.descriptor.uniformName;
if (!Object.prototype.hasOwnProperty.call(uniforms, listName)) {
uniforms[listName] = [];
}
uniforms[listName].push({
center: vec2.transformMat2d(vec2.create(), this.center, transform),
direction: this.direction,
lightDrop: this.lightDrop,
value: this.value,
});
}
public get value(): vec3 {
return vec3.scale(vec3.create(), this.color, this.lightness);
}
}

View file

@ -1,3 +0,0 @@
import { IDrawable } from '../i-drawable';
export type ILight = IDrawable;

View file

@ -1,13 +0,0 @@
import { checkShader } from './check-shader';
export const checkProgram = (gl: WebGL2RenderingContext, program: WebGLProgram) => {
const success = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!success) {
gl.getAttachedShaders(program).forEach((s) => {
checkShader(gl, s);
});
throw new Error(gl.getProgramInfoLog(program));
}
};

View file

@ -1,7 +0,0 @@
export const checkShader = (gl: WebGL2RenderingContext, shader: WebGLShader) => {
const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!success) {
throw new Error(gl.getShaderInfoLog(shader));
}
};

View file

@ -1,44 +0,0 @@
import { waitWhileFalse } from '../../../helper/wait-while-false';
import { tryEnableExtension } from '../helper/enable-extension';
import { checkProgram } from './check-program';
import { createShader } from './create-shader';
export const createProgram = async (
gl: WebGL2RenderingContext,
vertexShaderSource: string,
fragmentShaderSource: string,
substitutions: { [name: string]: string }
): Promise<WebGLProgram> => {
const program = gl.createProgram();
const extension = tryEnableExtension(gl, 'KHR_parallel_shader_compile');
const vertexShader = createShader(
gl,
gl.VERTEX_SHADER,
vertexShaderSource,
substitutions
);
gl.attachShader(program, vertexShader);
const fragmentShader = createShader(
gl,
gl.FRAGMENT_SHADER,
fragmentShaderSource,
substitutions
);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (extension) {
const checkCompileStatus = () =>
gl.getProgramParameter(program, extension.COMPLETION_STATUS_KHR);
await waitWhileFalse(checkCompileStatus);
}
checkProgram(gl, program);
return program;
};

View file

@ -1,17 +0,0 @@
export const createShader = (
gl: WebGL2RenderingContext,
type: GLenum,
source: string,
substitutions: { [name: string]: string }
): WebGLShader => {
source = source.replace(/{(.+)}/gm, (_, name: string): string => {
const value = substitutions[name];
return Number.isInteger(value) ? `${value}.0` : value;
});
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
return shader;
};

View file

@ -1,19 +0,0 @@
import { FrameBuffer } from './frame-buffer';
export class DefaultFrameBuffer extends FrameBuffer {
constructor(gl: WebGL2RenderingContext) {
super(gl);
this.frameBuffer = null;
this.setSize();
}
public setSize(): boolean {
const hasChanged = super.setSize();
if (hasChanged) {
this.gl.canvas.width = this.size.x;
this.gl.canvas.height = this.size.y;
}
return hasChanged;
}
}

View file

@ -1,44 +0,0 @@
import { vec2 } from 'gl-matrix';
import { settings } from '../../settings';
export abstract class FrameBuffer {
public renderScale = 1;
public enableHighDpiRendering = settings.enableHighDpiRendering;
protected size = vec2.create();
protected frameBuffer: WebGLFramebuffer;
constructor(protected gl: WebGL2RenderingContext) {}
public bindAndClear(colorInput?: WebGLTexture) {
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.frameBuffer);
if (colorInput !== null) {
this.gl.bindTexture(this.gl.TEXTURE_2D, colorInput);
}
this.gl.viewport(0, 0, this.size.x, this.size.y);
this.gl.clearColor(0, 0, 0, 0);
this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
}
public setSize(): boolean {
const realToCssPixels =
(this.enableHighDpiRendering ? window.devicePixelRatio : 1) * this.renderScale;
const canvasWidth = (this.gl.canvas as HTMLCanvasElement).clientWidth;
const canvasHeight = (this.gl.canvas as HTMLCanvasElement).clientHeight;
const displayWidth = Math.floor(canvasWidth * realToCssPixels);
const displayHeight = Math.floor(canvasHeight * realToCssPixels);
const oldSize = vec2.clone(this.getSize());
this.size = vec2.fromValues(displayWidth, displayHeight);
return this.size.x != oldSize.x || this.size.y != oldSize.y;
}
public getSize(): vec2 {
return this.size;
}
}

View file

@ -1,89 +0,0 @@
import { enableExtension } from '../helper/enable-extension';
import { FrameBuffer } from './frame-buffer';
export class IntermediateFrameBuffer extends FrameBuffer {
private frameTexture: WebGLTexture;
private floatLinearEnabled = false;
constructor(gl: WebGL2RenderingContext) {
super(gl);
enableExtension(gl, 'EXT_color_buffer_float');
try {
enableExtension(gl, 'OES_texture_float_linear');
this.floatLinearEnabled = true;
} catch {
// it's okay
}
this.frameTexture = this.gl.createTexture();
this.configureTexture();
this.frameBuffer = this.gl.createFramebuffer();
this.configureFrameBuffer();
this.setSize();
}
public get colorTexture(): WebGLTexture {
return this.frameTexture;
}
public setSize(): boolean {
const hasChanged = super.setSize();
if (hasChanged) {
this.gl.bindTexture(this.gl.TEXTURE_2D, this.frameTexture);
this.gl.texImage2D(
this.gl.TEXTURE_2D,
0,
this.gl.RG16F,
this.size.x,
this.size.y,
0,
this.gl.RG,
this.gl.FLOAT,
null
);
}
return hasChanged;
}
private configureTexture() {
this.gl.bindTexture(this.gl.TEXTURE_2D, this.frameTexture);
this.gl.texParameteri(
this.gl.TEXTURE_2D,
this.gl.TEXTURE_MAG_FILTER,
this.floatLinearEnabled ? this.gl.LINEAR : this.gl.NEAREST
);
this.gl.texParameteri(
this.gl.TEXTURE_2D,
this.gl.TEXTURE_MIN_FILTER,
this.floatLinearEnabled ? this.gl.LINEAR : this.gl.NEAREST
);
this.gl.texParameteri(
this.gl.TEXTURE_2D,
this.gl.TEXTURE_WRAP_S,
this.gl.CLAMP_TO_EDGE
);
this.gl.texParameteri(
this.gl.TEXTURE_2D,
this.gl.TEXTURE_WRAP_T,
this.gl.CLAMP_TO_EDGE
);
}
private configureFrameBuffer() {
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.frameBuffer);
const attachmentPoint = this.gl.COLOR_ATTACHMENT0;
this.gl.framebufferTexture2D(
this.gl.FRAMEBUFFER,
attachmentPoint,
this.gl.TEXTURE_2D,
this.frameTexture,
0
);
}
}

View file

@ -1,41 +0,0 @@
import { InfoText } from '../../../objects/types/info-text';
const extensions: Map<string, any> = new Map();
const printExtensions = () => {
const values = {};
for (const [k, v] of extensions.entries()) {
values[k] = v !== null;
}
InfoText.modifyRecord('extensions', values);
};
export const tryEnableExtension = (
gl: WebGL2RenderingContext,
name: string
): any | null => {
if (extensions.has(name)) {
return extensions.get(name);
}
let extension = null;
if (gl.getSupportedExtensions().indexOf(name) != -1) {
extension = gl.getExtension(name);
}
extensions.set(name, extension);
printExtensions();
return extension;
};
export const enableExtension = (gl: WebGL2RenderingContext, name: string): any => {
const extension = tryEnableExtension(gl, name);
if (extension === null) {
throw new Error(`Unsupported extension ${name}`);
}
return extension;
};

View file

@ -1,9 +0,0 @@
export const getWebGl2Context = (canvas: HTMLCanvasElement): WebGL2RenderingContext => {
const gl = canvas.getContext('webgl2');
if (!gl) {
throw new Error('WebGl2 is not supported');
}
return gl;
};

View file

@ -1,82 +0,0 @@
import { mat3, ReadonlyVec3, vec2, vec3 } from 'gl-matrix';
const loaderMat3 = mat3.create();
export const loadUniform = (
gl: WebGL2RenderingContext,
value: any,
type: GLenum,
location: WebGLUniformLocation
): any => {
const converters: Map<
GLenum,
(gl: WebGL2RenderingContext, value: any, location: WebGLUniformLocation) => void
> = new Map();
{
converters.set(WebGL2RenderingContext.FLOAT, (gl, v, l) => {
if (v instanceof Array) {
if (v.length == 0) {
return;
}
gl.uniform1fv(l, new Float32Array(v));
} else {
gl.uniform1f(l, v);
}
});
converters.set(WebGL2RenderingContext.FLOAT_VEC2, (gl, v: vec2 | Array<vec2>, l) => {
if (v.length == 0) {
return;
}
if (v[0] instanceof Array) {
const result = new Float32Array(v.length * 2);
for (let i = 0; i < v.length; i++) {
result[2 * i] = (v[i] as Array<number>).x;
result[2 * i + 1] = (v[i] as Array<number>).y;
}
gl.uniform2fv(l, result);
} else {
gl.uniform2fv(l, v as vec2);
}
});
converters.set(
WebGL2RenderingContext.FLOAT_VEC3,
(gl, v: ReadonlyVec3 | Array<vec3>, l) => {
if (v.length == 0) {
return;
}
if (v[0] instanceof Array) {
const result = new Float32Array(v.length * 3);
for (let i = 0; i < v.length; i++) {
result[3 * i] = (v[i] as Array<number>)[0];
result[3 * i + 1] = (v[i] as Array<number>)[1];
result[3 * i + 2] = (v[i] as Array<number>)[2];
}
gl.uniform3fv(l, result);
} else {
gl.uniform3fv(l, v as vec3);
}
}
);
converters.set(WebGL2RenderingContext.FLOAT_MAT3, (gl, v, l) => {
gl.uniformMatrix3fv(l, true, mat3.fromMat2d(loaderMat3, v));
});
converters.set(WebGL2RenderingContext.BOOL, (gl, v, l) => gl.uniform1i(l, v));
if (!converters.has(type)) {
throw new Error(`Unimplemented webgl type: ${type}`);
}
converters.get(type)(gl, value, location);
}
};

View file

@ -1,50 +0,0 @@
import { InfoText } from '../../../objects/types/info-text';
import { enableExtension } from './enable-extension';
// https://www.khronos.org/registry/webgl/extensions/EXT_disjoint_timer_query_webgl2/
export class WebGlStopwatch {
private timerExtension: any;
private timerQuery: WebGLQuery;
private isReady = true;
private resultsInNanoSeconds: number;
constructor(private gl: WebGL2RenderingContext) {
this.timerExtension = enableExtension(gl, 'EXT_disjoint_timer_query_webgl2');
}
public start() {
if (this.isReady) {
this.timerQuery = this.gl.createQuery();
this.gl.beginQuery(this.timerExtension.TIME_ELAPSED_EXT, this.timerQuery);
}
}
public stop() {
if (this.isReady) {
this.gl.endQuery(this.timerExtension.TIME_ELAPSED_EXT);
this.isReady = false;
}
const available = this.gl.getQueryParameter(
this.timerQuery,
this.gl.QUERY_RESULT_AVAILABLE
);
const disjoint = this.gl.getParameter(this.timerExtension.GPU_DISJOINT_EXT);
if (available && !disjoint) {
this.resultsInNanoSeconds = this.gl.getQueryParameter(
this.timerQuery,
this.gl.QUERY_RESULT
);
InfoText.modifyRecord('Draw time', `${this.resultsInMilliSeconds.toFixed(2)} ms`);
this.isReady = true;
}
}
public get resultsInMilliSeconds(): number {
return this.resultsInNanoSeconds / 1000 / 1000;
}
}

View file

@ -1,48 +0,0 @@
import Program from './program';
export class FragmentShaderOnlyProgram extends Program {
private vao: WebGLVertexArrayObject;
constructor(
gl: WebGL2RenderingContext,
sources: [string, string],
substitutions: { [name: string]: string }
) {
super(gl, sources, substitutions);
}
public async initialize(): Promise<void> {
await super.initialize();
this.prepareScreenQuad('vertexPosition');
}
public bind() {
super.bind();
this.gl.bindVertexArray(this.vao);
}
public draw() {
this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
}
private prepareScreenQuad(attributeName: string) {
const positionAttributeLocation = this.gl.getAttribLocation(
this.program,
attributeName
);
const positionBuffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, positionBuffer);
this.gl.bufferData(
this.gl.ARRAY_BUFFER,
new Float32Array([-1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0]),
this.gl.STATIC_DRAW
);
this.vao = this.gl.createVertexArray();
this.gl.bindVertexArray(this.vao);
this.gl.enableVertexAttribArray(positionAttributeLocation);
this.gl.vertexAttribPointer(positionAttributeLocation, 2, this.gl.FLOAT, false, 0, 0);
}
}

View file

@ -1,9 +0,0 @@
import { vec2 } from 'gl-matrix';
export interface IProgram {
initialize(): Promise<void>;
setDrawingRectangleUV(bottomLeft: vec2, size: vec2): void;
bindAndSetUniforms(values: { [name: string]: any }): void;
draw(): void;
delete(): void;
}

View file

@ -1,101 +0,0 @@
import { mat2d, vec2 } from 'gl-matrix';
import { settings } from '../../settings';
import { createProgram } from '../compiling/create-program';
import { loadUniform } from '../helper/load-uniform';
import { IProgram } from './i-program';
export default abstract class Program implements IProgram {
protected program: WebGLProgram;
private programPromise: Promise<WebGLProgram>;
private modelTransform = mat2d.identity(mat2d.create());
private readonly ndcToUv = mat2d.fromValues(0.5, 0, 0, 0.5, 0.5, 0.5);
private uniforms: Array<{
name: Array<string>;
location: WebGLUniformLocation;
type: GLenum;
}> = [];
constructor(
protected gl: WebGL2RenderingContext,
[vertexShaderSource, fragmentShaderSource]: [string, string],
substitutions: { [name: string]: string }
) {
substitutions = { ...settings.shaderMacros, ...substitutions };
this.programPromise = createProgram(
this.gl,
vertexShaderSource,
fragmentShaderSource,
substitutions
);
}
public async initialize(): Promise<void> {
this.program = await this.programPromise;
this.queryUniforms();
}
public bindAndSetUniforms(values: { [name: string]: any }) {
this.bind();
this.setUniforms({ modelTransform: this.modelTransform, ...values });
}
public setDrawingRectangleUV(bottomLeft: vec2, size: vec2) {
mat2d.invert(this.modelTransform, this.ndcToUv);
mat2d.translate(this.modelTransform, this.modelTransform, bottomLeft);
mat2d.scale(this.modelTransform, this.modelTransform, size);
mat2d.multiply(this.modelTransform, this.modelTransform, this.ndcToUv);
}
public setUniforms(values: { [name: string]: any }) {
this.uniforms.forEach(({ name, location, type }) => {
const value = name.reduce(
(prev, prop) => (prev !== null && prop in prev ? prev[prop] : null),
values
);
if (value !== null) {
loadUniform(this.gl, value, type, location);
}
});
}
public delete() {
this.gl.getAttachedShaders(this.program).forEach((s) => this.gl.deleteShader(s));
this.gl.deleteProgram(this.program);
}
public abstract draw(): void;
protected bind() {
this.gl.useProgram(this.program);
}
private queryUniforms() {
const uniformCount = this.gl.getProgramParameter(
this.program,
WebGL2RenderingContext.ACTIVE_UNIFORMS
);
for (let i = 0; i < uniformCount; i++) {
const glUniform = this.gl.getActiveUniform(this.program, i);
this.uniforms.push({
name: glUniform.name.split(/\[|\]\.|\]|\./).filter((p) => p !== ''),
location: this.gl.getUniformLocation(this.program, glUniform.name),
type: glUniform.type,
});
}
this.uniforms.map((u1) => {
const isSingle =
this.uniforms.filter((u2) => u2.name.includes(u1.name[0])).length == 1;
if (u1.name.includes('0') && isSingle) {
u1.name = u1.name.slice(0, -1);
}
return u1;
});
}
}

View file

@ -1,90 +0,0 @@
import { mat2d, vec2 } from 'gl-matrix';
import { getCombinations } from '../../../helper/get-combinations';
import { last } from '../../../helper/last';
import { IDrawableDescriptor } from '../../drawables/i-drawable-descriptor';
import { FragmentShaderOnlyProgram } from './fragment-shader-only-program';
import { IProgram } from './i-program';
export class UniformArrayAutoScalingProgram implements IProgram {
private programs: Array<{
program: FragmentShaderOnlyProgram;
values: Array<number>;
}> = [];
private current: FragmentShaderOnlyProgram;
private drawingRectangleBottomLeft = vec2.fromValues(0, 0);
private drawingRectangleSize = vec2.fromValues(1, 1);
constructor(
private gl: WebGL2RenderingContext,
shaderSources: [string, string],
private descriptors: Array<IDrawableDescriptor>
) {
const names = descriptors.map((o) => o.countMacroName);
for (const combination of getCombinations(
descriptors.map((o) => o.shaderCombinationSteps)
)) {
this.createProgram(names, combination, shaderSources);
}
}
public async initialize(): Promise<void> {
await Promise.all(this.programs.map((p) => p.program.initialize()));
}
public bindAndSetUniforms(uniforms: { [name: string]: any }): void {
const values = this.descriptors.map((d) =>
uniforms[d.uniformName] ? uniforms[d.uniformName].length : 0
);
const closest = this.programs.find((p) => p.values.every((v, i) => v >= values[i]));
this.current = closest ? closest.program : last(this.programs).program;
if (closest) {
this.descriptors.map((d, i) => {
const difference = closest.values[i] - values[i];
for (let i = 0; i < difference; i++) {
d.empty.serializeToUniforms(uniforms, 0, mat2d.create());
}
});
}
this.current.setDrawingRectangleUV(
this.drawingRectangleBottomLeft,
this.drawingRectangleSize
);
this.current.bindAndSetUniforms(uniforms);
}
public setDrawingRectangleUV(bottomLeft: vec2, size: vec2) {
this.drawingRectangleBottomLeft = bottomLeft;
this.drawingRectangleSize = size;
}
public draw(): void {
this.current.draw();
}
public delete(): void {
this.programs.forEach((p) => p.program.delete());
}
private createProgram(
names: Array<string>,
combination: Array<number>,
shaderSources: [string, string]
): FragmentShaderOnlyProgram {
const substitutions = {};
names.forEach((v, i) => (substitutions[v] = combination[i].toString()));
const program = new FragmentShaderOnlyProgram(this.gl, shaderSources, substitutions);
this.programs.push({
program,
values: combination,
});
return program;
}
}

View file

@ -1,19 +0,0 @@
import { vec2 } from 'gl-matrix';
import { BoundingBoxBase } from '../shapes/bounding-box-base';
import { IDrawable } from './drawables/i-drawable';
import { ILight } from './drawables/lights/i-light';
export interface IRenderer {
initialize(): Promise<void>;
startFrame(deltaTime: DOMHighResTimeStamp): void;
finishFrame(): void;
drawShape(drawable: IDrawable): void;
drawLight(light: ILight): void;
drawInfoText(text: string): void;
readonly canvasSize: vec2;
setViewArea(viewArea: BoundingBoxBase): void;
setCursorPosition(position: vec2): void;
}

View file

@ -1,45 +0,0 @@
import { Autoscaler as AutoScaler } from '../../helper/autoscaler';
import { exponentialDecay } from '../../helper/exponential-decay';
import { settings } from '../settings';
export class FpsAutoscaler extends AutoScaler {
private timeSinceLastAdjusment = 0;
private exponentialDecayedDeltaTime = 0.0;
constructor(setters: { [key: string]: (value: number | boolean) => void }) {
super(
setters,
settings.qualityScaling.qualityTargets,
settings.qualityScaling.startingTargetIndex,
settings.qualityScaling.scalingOptions
);
}
public autoscale(lastDeltaTime: DOMHighResTimeStamp) {
this.timeSinceLastAdjusment += lastDeltaTime;
if (
this.timeSinceLastAdjusment >= settings.qualityScaling.adjusmentRateInMilliseconds
) {
this.timeSinceLastAdjusment = 0;
this.exponentialDecayedDeltaTime = exponentialDecay(
this.exponentialDecayedDeltaTime,
lastDeltaTime,
settings.qualityScaling.deltaTimeResponsiveness
);
if (
this.exponentialDecayedDeltaTime <=
settings.qualityScaling.targetDeltaTimeInMilliseconds -
settings.qualityScaling.deltaTimeError
) {
this.increase();
} else if (
this.exponentialDecayedDeltaTime >
settings.qualityScaling.targetDeltaTimeInMilliseconds +
settings.qualityScaling.deltaTimeError
) {
this.decrease();
}
}
}
}

View file

@ -1,86 +0,0 @@
import { vec2 } from 'gl-matrix';
import { IDrawable } from '../drawables/i-drawable';
import { IDrawableDescriptor } from '../drawables/i-drawable-descriptor';
import { FrameBuffer } from '../graphics-library/frame-buffer/frame-buffer';
import { UniformArrayAutoScalingProgram } from '../graphics-library/program/uniform-array-autoscaling-program';
import { settings } from '../settings';
export class RenderingPass {
private drawables: Array<IDrawable> = [];
private program: UniformArrayAutoScalingProgram;
constructor(
gl: WebGL2RenderingContext,
shaderSources: [string, string],
drawableDescriptors: Array<IDrawableDescriptor>,
private frame: FrameBuffer
) {
this.program = new UniformArrayAutoScalingProgram(
gl,
shaderSources,
drawableDescriptors
);
}
public async initialize(): Promise<void> {
await this.program.initialize();
}
public addDrawable(drawable: IDrawable) {
this.drawables.push(drawable);
}
public render(commonUniforms: any, inputTexture?: WebGLTexture) {
this.frame.bindAndClear(inputTexture);
const stepsInUV = 1 / settings.tileMultiplier;
const worldR =
0.5 *
vec2.length(vec2.scale(vec2.create(), commonUniforms.worldAreaInView, stepsInUV));
const radiusInNDC = worldR * commonUniforms.scaleWorldLengthToNDC;
const stepsInNDC = 2 * stepsInUV;
for (let x = -1; x < 1; x += stepsInNDC) {
for (let y = -1; y < 1; y += stepsInNDC) {
const uniforms = { ...commonUniforms, maxMinDistance: 0.0 };
const ndcBottomLeft = vec2.fromValues(x, y);
this.program.setDrawingRectangleUV(
[(ndcBottomLeft.x + 1) / 2, (ndcBottomLeft.y + 1) / 2],
vec2.fromValues(stepsInUV, stepsInUV)
);
const tileCenterWorldCoordinates = vec2.transformMat2d(
vec2.create(),
vec2.add(
vec2.create(),
[(ndcBottomLeft.x + 1) / 2, (ndcBottomLeft.y + 1) / 2],
vec2.fromValues(stepsInUV / 2, stepsInUV / 2)
),
uniforms.uvToWorld
);
const primitivesNearTile = this.drawables.filter(
(d) => d.distance(tileCenterWorldCoordinates) < 2 * worldR
);
primitivesNearTile.forEach((p) =>
p.serializeToUniforms(
uniforms,
uniforms.scaleWorldLengthToNDC,
uniforms.transformWorldToNDC
)
);
this.program.bindAndSetUniforms(uniforms);
this.program.draw();
}
}
this.drawables = [];
}
}

View file

@ -1,95 +0,0 @@
import { mat2d, vec2 } from 'gl-matrix';
import { BoundingBoxBase } from '../../shapes/bounding-box-base';
export class UniformsProvider {
private scaleWorldLengthToNDC = 1;
private transformWorldToNDC = mat2d.create();
private viewAreaBottomLeft = vec2.create();
private worldAreaInView = vec2.create();
private squareToAspectRatio: vec2;
private uvToWorld: mat2d;
private cursorPosition = vec2.create();
public softShadowsEnabled: boolean;
public constructor(private gl: WebGL2RenderingContext) {}
public getUniforms(uniforms: any): any {
const cursorPosition = this.uvToWorldCoordinate(this.cursorPosition);
return {
...uniforms,
cursorPosition,
uvToWorld: this.uvToWorld,
worldAreaInView: this.worldAreaInView,
squareToAspectRatio: this.squareToAspectRatio,
scaleWorldLengthToNDC: this.scaleWorldLengthToNDC,
transformWorldToNDC: this.transformWorldToNDC,
squareToAspectRatioTimes2: vec2.scale(vec2.create(), this.squareToAspectRatio, 2),
softShadowsEnabled: this.softShadowsEnabled,
};
}
private getScreenToWorldTransform(screenSize: vec2) {
const transform = mat2d.fromTranslation(mat2d.create(), this.viewAreaBottomLeft);
mat2d.scale(
transform,
transform,
vec2.divide(vec2.create(), this.worldAreaInView, screenSize)
);
mat2d.translate(transform, transform, vec2.fromValues(0.5, 0.5));
return transform;
}
public uvToWorldCoordinate(screenUvPosition: vec2): vec2 {
const resolution = vec2.fromValues(this.gl.canvas.width, this.gl.canvas.height);
return vec2.transformMat2d(
vec2.create(),
vec2.multiply(vec2.create(), screenUvPosition, resolution),
this.getScreenToWorldTransform(resolution)
);
}
public setViewArea(viewArea: BoundingBoxBase) {
this.worldAreaInView = viewArea.size;
// world coordinates
this.viewAreaBottomLeft = vec2.add(
vec2.create(),
viewArea.topLeft,
vec2.fromValues(0, -viewArea.size.y)
);
const scaleLongerEdgeTo1 =
1 / Math.max(this.worldAreaInView.x, this.worldAreaInView.y);
this.squareToAspectRatio = vec2.fromValues(
this.worldAreaInView.x * scaleLongerEdgeTo1,
this.worldAreaInView.y * scaleLongerEdgeTo1
);
this.scaleWorldLengthToNDC = scaleLongerEdgeTo1 * 2;
mat2d.fromScaling(
this.transformWorldToNDC,
vec2.fromValues(this.scaleWorldLengthToNDC, this.scaleWorldLengthToNDC)
);
mat2d.translate(
this.transformWorldToNDC,
this.transformWorldToNDC,
vec2.fromValues(
-this.viewAreaBottomLeft.x - 0.5 * this.worldAreaInView.x,
-this.viewAreaBottomLeft.y - 0.5 * this.worldAreaInView.y
)
);
this.uvToWorld = mat2d.fromTranslation(mat2d.create(), this.viewAreaBottomLeft);
mat2d.scale(this.uvToWorld, this.uvToWorld, this.worldAreaInView);
}
public setCursorPosition(position: vec2): void {
this.cursorPosition = position;
}
}

View file

@ -1,129 +0,0 @@
import { vec2 } from 'gl-matrix';
import { BoundingBoxBase } from '../../shapes/bounding-box-base';
import { DrawableBlob } from '../drawables/drawable-blob';
import { DrawableTunnel } from '../drawables/drawable-tunnel';
import { IDrawable } from '../drawables/i-drawable';
import { CircleLight } from '../drawables/lights/circle-light';
import { Flashlight } from '../drawables/lights/flashlight';
import { ILight } from '../drawables/lights/i-light';
import { DefaultFrameBuffer } from '../graphics-library/frame-buffer/default-frame-buffer';
import { IntermediateFrameBuffer } from '../graphics-library/frame-buffer/intermediate-frame-buffer';
import { getWebGl2Context } from '../graphics-library/helper/get-webgl2-context';
import { WebGlStopwatch } from '../graphics-library/helper/stopwatch';
import { IRenderer } from '../i-renderer';
import distanceFragmentShader from '../shaders/distance-fs.glsl';
import distanceVertexShader from '../shaders/distance-vs.glsl';
import lightsFragmentShader from '../shaders/shading-fs.glsl';
import lightsVertexShader from '../shaders/shading-vs.glsl';
import { FpsAutoscaler } from './fps-autoscaler';
import { RenderingPass } from './rendering-pass';
import { UniformsProvider } from './uniforms-provider';
export class WebGl2Renderer implements IRenderer {
private gl: WebGL2RenderingContext;
private stopwatch?: WebGlStopwatch;
private uniformsProvider: UniformsProvider;
private distanceFieldFrameBuffer: IntermediateFrameBuffer;
private lightingFrameBuffer: DefaultFrameBuffer;
private distancePass: RenderingPass;
private lightingPass: RenderingPass;
private autoscaler: FpsAutoscaler;
private initializePromise: Promise<[void, void]> = null;
constructor(private canvas: HTMLCanvasElement, private overlay: HTMLElement) {
this.gl = getWebGl2Context(canvas);
this.distanceFieldFrameBuffer = new IntermediateFrameBuffer(this.gl);
this.lightingFrameBuffer = new DefaultFrameBuffer(this.gl);
this.distancePass = new RenderingPass(
this.gl,
[distanceVertexShader, distanceFragmentShader],
[DrawableTunnel.descriptor, DrawableBlob.descriptor],
this.distanceFieldFrameBuffer
);
this.lightingPass = new RenderingPass(
this.gl,
[lightsVertexShader, lightsFragmentShader],
[CircleLight.descriptor, Flashlight.descriptor],
this.lightingFrameBuffer
);
this.initializePromise = Promise.all([
this.distancePass.initialize(),
this.lightingPass.initialize(),
]);
this.uniformsProvider = new UniformsProvider(this.gl);
this.autoscaler = new FpsAutoscaler({
distanceRenderScale: (v) =>
(this.distanceFieldFrameBuffer.renderScale = v as number),
finalRenderScale: (v) => (this.lightingFrameBuffer.renderScale = v as number),
softShadowsEnabled: (v) =>
(this.uniformsProvider.softShadowsEnabled = v as boolean),
});
try {
this.stopwatch = new WebGlStopwatch(this.gl);
} catch {
// no problem
}
}
public async initialize(): Promise<void> {
await this.initializePromise;
}
public drawShape(shape: IDrawable) {
this.distancePass.addDrawable(shape);
}
public drawLight(light: ILight) {
this.lightingPass.addDrawable(light);
}
public startFrame(deltaTime: DOMHighResTimeStamp) {
this.autoscaler.autoscale(deltaTime);
this.stopwatch?.start();
this.distanceFieldFrameBuffer.setSize();
this.lightingFrameBuffer.setSize();
}
public finishFrame() {
const common = {
distanceNdcPixelSize: 2 / Math.max(...this.distanceFieldFrameBuffer.getSize()),
shadingNdcPixelSize: 2 / Math.max(...this.distanceFieldFrameBuffer.getSize()),
};
this.distancePass.render(this.uniformsProvider.getUniforms(common));
this.lightingPass.render(
this.uniformsProvider.getUniforms(common),
this.distanceFieldFrameBuffer.colorTexture
);
this.stopwatch?.stop();
}
public setViewArea(viewArea: BoundingBoxBase) {
this.uniformsProvider.setViewArea(viewArea);
}
public setCursorPosition(position: vec2) {
this.uniformsProvider.setCursorPosition(position);
}
public get canvasSize(): vec2 {
return vec2.fromValues(this.canvas.clientWidth, this.canvas.clientHeight);
}
public drawInfoText(text: string) {
if (this.overlay.innerText != text) {
this.overlay.innerText = text;
}
}
}

View file

@ -1,59 +0,0 @@
export const settings = {
enableHighDpiRendering: true,
qualityScaling: {
targetDeltaTimeInMilliseconds: 20,
deltaTimeError: 2,
deltaTimeResponsiveness: 1 / 16,
adjusmentRateInMilliseconds: 300,
qualityTargets: [
{
distanceRenderScale: 0.1,
finalRenderScale: 0.2,
softShadowsEnabled: false,
},
{
distanceRenderScale: 0.1,
finalRenderScale: 0.6,
softShadowsEnabled: false,
},
{
distanceRenderScale: 0.3,
finalRenderScale: 1.0,
softShadowsEnabled: false,
},
{
distanceRenderScale: 0.3,
finalRenderScale: 1.0,
softShadowsEnabled: true,
},
/*{
distanceRenderScale: 1.0,
finalRenderScale: 1.5,
softShadowsEnabled: true,
},
{
distanceRenderScale: 1.25,
finalRenderScale: 1.75,
softShadowsEnabled: true,
},
{
distanceRenderScale: 2,
finalRenderScale: 2,
softShadowsEnabled: true,
},*/
],
startingTargetIndex: 2,
scalingOptions: {
additiveIncrease: 0.2,
multiplicativeDecrease: 1.05,
},
},
tileMultiplier: 8,
shaderMacros: {},
shaderCombinations: {
lineSteps: [0, 1, 2, 4, 8, 16, 128],
blobSteps: [0, 1, 2, 8],
circleLightSteps: [0, 1],
flashlightSteps: [0, 1],
},
};

View file

@ -1,109 +0,0 @@
#version 300 es
precision lowp float;
#define LINE_COUNT {lineCount}
#define BLOB_COUNT {blobCount}
#define SURFACE_OFFSET 0.001
uniform float maxMinDistance;
uniform float distanceNdcPixelSize;
in vec2 position;
float smoothMin(float a, float b)
{
const float k = 2.0;
a = pow(a, k);
b = pow(b, k);
return pow((a * b) / (a + b), 1.0 / k);
}
#if LINE_COUNT > 0
uniform struct {
vec2 from;
vec2 toFromDelta;
float fromRadius;
float toRadius;
}[LINE_COUNT] lines;
void lineMinDistance(inout float minDistance, inout float color) {
float myMinDistance = maxMinDistance;
for (int i = 0; i < LINE_COUNT; i++) {
vec2 targetFromDelta = position - lines[i].from;
vec2 toFromDelta = lines[i].toFromDelta;
float h = clamp(
dot(targetFromDelta, toFromDelta) / dot(toFromDelta, toFromDelta),
0.0, 1.0
);
float lineDistance = -mix(
lines[i].fromRadius, lines[i].toRadius, h
) + distance(
targetFromDelta, toFromDelta * h
);
myMinDistance = min(myMinDistance, lineDistance);
}
color = mix(0.0, color, step(distanceNdcPixelSize + SURFACE_OFFSET, -myMinDistance));
minDistance = -myMinDistance;
}
#endif
#if BLOB_COUNT > 0
uniform struct {
vec2 headCenter;
vec2 leftFootCenter;
vec2 rightFootCenter;
float headRadius;
float footRadius;
float k;
}[BLOB_COUNT] blobs;
float circleMinDistance(vec2 circleCenter, float radius) {
return distance(position, circleCenter) - radius;
}
void blobMinDistance(inout float minDistance, inout float color) {
for (int i = 0; i < BLOB_COUNT; i++) {
float headDistance = circleMinDistance(blobs[i].headCenter, blobs[i].headRadius);
float leftFootDistance = circleMinDistance(blobs[i].leftFootCenter, blobs[i].footRadius);
float rightFootDistance = circleMinDistance(blobs[i].rightFootCenter, blobs[i].footRadius);
float res = min(
smoothMin(headDistance, leftFootDistance),
smoothMin(headDistance, rightFootDistance)
);
res = min(100.0, headDistance);
res = min(res, leftFootDistance);
res = min(res, rightFootDistance);
//color = mix(2.0, color, step(distanceUvPixelSize + SURFACE_OFFSET, res));
minDistance = min(minDistance, res);
color = mix(2.0, color, step(distanceNdcPixelSize + SURFACE_OFFSET, res));
}
}
#endif
out vec2 fragmentColor;
void main() {
float minDistance = -maxMinDistance;
float color = 0.0;
#if LINE_COUNT > 0
lineMinDistance(minDistance, color);
#endif
#if BLOB_COUNT > 0
blobMinDistance(minDistance, color);
#endif
// minDistance / 2.0: NDC to UV scale
fragmentColor = vec2((minDistance - SURFACE_OFFSET) / 2.0, color);
}

View file

@ -1,15 +0,0 @@
#version 300 es
precision lowp float;
uniform mat3 modelTransform;
uniform vec2 squareToAspectRatio;
in vec4 vertexPosition;
out vec2 position;
void main() {
vec3 vertexPosition2D = vec3(vertexPosition.xy, 1.0) * modelTransform;
gl_Position = vec4(vertexPosition2D.xy, 0.0, 1.0);
position = vertexPosition2D.xy * squareToAspectRatio;
}

View file

@ -1,176 +0,0 @@
#version 300 es
precision lowp float;
#define INFINITY 1000.0
#define LIGHT_DROP_INSIDE_RATIO 0.3
#define AMBIENT_LIGHT vec3(0.25, 0.15, 0.25)
#define SHADOW_HARDNESS 150.0
#define CIRCLE_LIGHT_COUNT {circleLightCount}
#define FLASHLIGHT_COUNT {flashlightCount}
uniform bool softShadowsEnabled;
uniform vec2 squareToAspectRatioTimes2;
uniform float shadingNdcPixelSize;
uniform sampler2D distanceTexture;
in vec2 position;
in vec2 uvCoordinates;
vec3[3] colors = vec3[](
vec3(0.4, 0.35, 0.6), // cave color
vec3(0.0, 1.0, 0.0),
vec3(0.0, 0.0, 1.0)
);
float getDistance(in vec2 target, out vec3 color) {
vec4 values = texture(distanceTexture, target);
color = colors[int(values[1])];
return values[0];
}
float getDistance(in vec2 target) {
return texture(distanceTexture, target)[0];
}
float softShadowTransparency(float startingDistance, float lightCenterDistance, vec2 direction) {
float rayLength = startingDistance;
float q = 1.0 / SHADOW_HARDNESS;
for (int j = 0; j < 128; j++) {
float minDistance = getDistance(uvCoordinates + direction * rayLength);
q = min(q, minDistance / rayLength);
rayLength += minDistance / 2.5;
if (rayLength >= lightCenterDistance) {
return q * SHADOW_HARDNESS;
}
}
return 0.0;
}
float hardShadowTransparency(float startingDistance, float lightCenterDistance, vec2 direction) {
float rayLength = startingDistance;
for (int j = 0; j < 32; j++) {
rayLength += getDistance(uvCoordinates + direction * rayLength);
}
return step(lightCenterDistance, rayLength);
}
float shadowTransparency(float startingDistance, float lightCenterDistance, vec2 direction) {
return softShadowsEnabled ?
softShadowTransparency(startingDistance, lightCenterDistance, direction) :
hardShadowTransparency(startingDistance, lightCenterDistance, direction);
}
#if CIRCLE_LIGHT_COUNT > 0
uniform struct CircleLight {
vec2 center;
float lightDrop;
vec3 value;
}[CIRCLE_LIGHT_COUNT] circleLights;
in vec2[CIRCLE_LIGHT_COUNT] circleLightDirections;
vec3 colorInPosition(CircleLight light, out float lightCenterDistance) {
lightCenterDistance = distance(light.center, position);
return light.value / pow(
lightCenterDistance / light.lightDrop + 1.0, 2.0
);
}
vec3 colorInPositionInside(CircleLight light) {
float lightCenterDistance = distance(light.center, position);
return light.value / pow(
lightCenterDistance / (light.lightDrop * LIGHT_DROP_INSIDE_RATIO) + 1.0, 2.0
);
}
#endif
#if FLASHLIGHT_COUNT > 0
uniform struct Flashlight {
vec2 center;
vec2 direction;
float lightDrop;
vec3 value;
}[FLASHLIGHT_COUNT] flashlights;
in vec2[FLASHLIGHT_COUNT] flashlightDirections;
float intensityInDirection(vec2 lightDirection, vec2 targetDirection) {
return smoothstep(0.0, 1.0, 10.0 * max(0.0, dot(targetDirection, lightDirection) - 0.9));
}
vec3 colorInPosition(Flashlight light, vec2 positionDirection, out float lightCenterDistance) {
lightCenterDistance = distance(light.center, position);
return intensityInDirection(light.direction, positionDirection) * light.value / pow(
lightCenterDistance / light.lightDrop + 1.0, 2.0
);
}
vec3 colorInPositionInside(Flashlight light, vec2 positionDirection) {
float lightCenterDistance = distance(light.center, position);
return intensityInDirection(light.direction, positionDirection) * light.value / pow(
lightCenterDistance / (light.lightDrop * LIGHT_DROP_INSIDE_RATIO) + 1.0, 2.0
);
}
#endif
out vec4 fragmentColor;
void main() {
vec3 lighting = AMBIENT_LIGHT;
vec3 colorAtPosition;
float startingDistance = getDistance(uvCoordinates, colorAtPosition);
if (startingDistance < 0.0) {
#if CIRCLE_LIGHT_COUNT > 0
for (int i = 0; i < CIRCLE_LIGHT_COUNT; i++) {
lighting += colorInPositionInside(circleLights[i]);
}
#endif
#if FLASHLIGHT_COUNT > 0
for (int i = 0; i < FLASHLIGHT_COUNT; i++) {
lighting += colorInPositionInside(flashlights[i], normalize(flashlightDirections[i]));
}
#endif
} else {
colorAtPosition = vec3(1.0);
#if CIRCLE_LIGHT_COUNT > 0
for (int i = 0; i < CIRCLE_LIGHT_COUNT; i++) {
vec2 direction = normalize(circleLightDirections[i]) / squareToAspectRatioTimes2;
float lightCenterDistance;
vec3 lightColorAtPosition = colorInPosition(circleLights[i], lightCenterDistance);
lighting += lightColorAtPosition * shadowTransparency(startingDistance, lightCenterDistance, direction);
}
#endif
#if FLASHLIGHT_COUNT > 0
for (int i = 0; i < FLASHLIGHT_COUNT; i++) {
vec2 originalDirection = normalize(flashlightDirections[i]);
vec2 direction = originalDirection / squareToAspectRatioTimes2;
float lightCenterDistance;
vec3 lightColorAtPosition = colorInPosition(flashlights[i], originalDirection, lightCenterDistance);
if (length(lightColorAtPosition) < 0.01) {
continue;
}
lighting += lightColorAtPosition * shadowTransparency(startingDistance, lightCenterDistance, direction);
}
#endif
}
fragmentColor = vec4(colorAtPosition * lighting, 1.0);
}

View file

@ -1,59 +0,0 @@
#version 300 es
precision lowp float;
#define CIRCLE_LIGHT_COUNT {circleLightCount}
#define FLASHLIGHT_COUNT {flashlightCount}
uniform mat3 modelTransform;
in vec4 vertexPosition;
out vec2 position;
out vec2 uvCoordinates;
uniform vec2 squareToAspectRatio;
#if CIRCLE_LIGHT_COUNT > 0
uniform struct CircleLight {
vec2 center;
float lightDrop;
vec3 value;
}[CIRCLE_LIGHT_COUNT] circleLights;
out vec2[CIRCLE_LIGHT_COUNT] circleLightDirections;
#endif
#if FLASHLIGHT_COUNT > 0
uniform struct Flashlight {
vec2 center;
vec2 direction;
float lightDrop;
vec3 value;
}[FLASHLIGHT_COUNT] flashlights;
out vec2[FLASHLIGHT_COUNT] flashlightDirections;
#endif
void main() {
vec3 vertexPosition2D = vec3(vertexPosition.xy, 1.0) * modelTransform;
gl_Position = vec4(vertexPosition2D.xy, 0.0, 1.0);
position = vertexPosition2D.xy * squareToAspectRatio;
uvCoordinates = (vertexPosition2D * mat3(
0.5, 0.0, 0.5,
0.0, 0.5, 0.5,
0.0, 0.0, 1.0
)).xy;
#if CIRCLE_LIGHT_COUNT > 0
for (int i = 0; i < CIRCLE_LIGHT_COUNT; i++) {
circleLightDirections[i] = circleLights[i].center - position;
}
#endif
#if FLASHLIGHT_COUNT > 0
for (int i = 0; i < FLASHLIGHT_COUNT; i++) {
flashlightDirections[i] = flashlights[i].center - position;
}
#endif
}

View file

@ -1,49 +0,0 @@
declare global {
interface Array<T> {
x: number;
y: number;
}
interface Float32Array {
x: number;
y: number;
}
}
export const applyArrayPlugins = () => {
Object.defineProperty(Array.prototype, 'x', {
get() {
return this[0];
},
set(value) {
this[0] = value;
},
});
Object.defineProperty(Array.prototype, 'y', {
get() {
return this[1];
},
set(value) {
this[1] = value;
},
});
Object.defineProperty(Float32Array.prototype, 'x', {
get() {
return this[0];
},
set(value) {
this[0] = value;
},
});
Object.defineProperty(Float32Array.prototype, 'y', {
get() {
return this[1];
},
set(value) {
this[1] = value;
},
});
};

View file

@ -1,60 +0,0 @@
import { InfoText } from '../objects/types/info-text';
import { clamp } from './clamp';
import { mix } from './mix';
export class Autoscaler {
// can have fractions
private index: number;
constructor(
private setters: { [key: string]: (value: number | boolean) => void },
private targets: Array<{ [key: string]: number | boolean }>,
startingIndex: number,
private scalingOptions: {
additiveIncrease: number;
multiplicativeDecrease: number;
}
) {
this.index = startingIndex;
this.applyScaling();
}
public increase() {
this.index += this.scalingOptions.additiveIncrease;
this.applyScaling();
}
public decrease() {
this.index /= this.scalingOptions.multiplicativeDecrease;
this.applyScaling();
}
private applyScaling() {
this.index = clamp(this.index, 0, this.targets.length - 1);
const floor = Math.floor(this.index);
const fract = this.index - floor;
const previousTarget = this.targets[floor];
const nextTarget =
floor + 1 == this.targets.length ? previousTarget : this.targets[floor + 1];
const result = {};
for (const key in this.setters) {
const previous = previousTarget[key];
const next = nextTarget[key];
let current: number | boolean;
if (typeof previous == 'number') {
current = mix(previous, next as number, fract);
} else {
current = next;
}
result[key] = current;
this.setters[key](current);
}
InfoText.modifyRecord('quality', result);
}
}

View file

@ -0,0 +1,23 @@
export class DeltaTimeCalculator {
private previousTime: DOMHighResTimeStamp | null = null;
constructor() {
document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
}
public getNextDeltaTime(currentTime: DOMHighResTimeStamp): DOMHighResTimeStamp {
if (this.previousTime === null) {
this.previousTime = currentTime;
}
const delta = currentTime - this.previousTime;
this.previousTime = currentTime;
return delta;
}
private handleVisibilityChange() {
if (!document.hidden) {
this.previousTime = null;
}
}
}

View file

@ -0,0 +1,4 @@
export const prettyPrint = (o: any): string =>
JSON.stringify(o, (_, v) => (v.toFixed ? Number(v.toFixed(3)) : v), ' ')
.replace(/("|,|{|^\n)/g, '')
.replace(/(\W*}\n?)+/g, '\n\n');

View file

@ -1,35 +0,0 @@
import { InfoText } from '../objects/types/info-text';
import { last } from './last';
export function timeIt(interval = 60) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
let i = 0;
let previousTimes: Array<DOMHighResTimeStamp> = [];
const targetFunction = descriptor.value;
descriptor.value = function (...values) {
const start = performance.now();
targetFunction.bind(this)(...values);
const end = performance.now();
previousTimes.push(end - start);
if (i++ % interval == 0) {
previousTimes = previousTimes.sort();
const text = `Max: ${last(previousTimes).toFixed(
2
)} ms\n\tMedian: ${previousTimes[Math.floor(previousTimes.length / 2)].toFixed(
2
)} ms`;
InfoText.modifyRecord(propertyKey, text);
previousTimes = [];
}
};
return descriptor;
};
}

View file

@ -1,5 +1,5 @@
import { vec2 } from 'gl-matrix'; import { vec2 } from 'gl-matrix';
import { BeforeRenderCommand } from '../../graphics/commands/before-render'; import { RenderCommand } from '../../graphics/commands/render';
import { CursorMoveCommand } from '../../input/commands/cursor-move-command'; import { CursorMoveCommand } from '../../input/commands/cursor-move-command';
import { ZoomCommand } from '../../input/commands/zoom'; import { ZoomCommand } from '../../input/commands/zoom';
import { MoveToCommand } from '../../physics/commands/move-to'; import { MoveToCommand } from '../../physics/commands/move-to';
@ -17,7 +17,7 @@ export class Camera extends GameObject {
this._viewArea = new BoundingBox(null); this._viewArea = new BoundingBox(null);
this.addCommandExecutor(BeforeRenderCommand, this.draw.bind(this)); this.addCommandExecutor(RenderCommand, this.draw.bind(this));
this.addCommandExecutor(MoveToCommand, this.moveTo.bind(this)); this.addCommandExecutor(MoveToCommand, this.moveTo.bind(this));
this.addCommandExecutor(CursorMoveCommand, this.setCursorPosition.bind(this)); this.addCommandExecutor(CursorMoveCommand, this.setCursorPosition.bind(this));
this.addCommandExecutor(ZoomCommand, this.zoom.bind(this)); this.addCommandExecutor(ZoomCommand, this.zoom.bind(this));
@ -27,7 +27,7 @@ export class Camera extends GameObject {
return this._viewArea; return this._viewArea;
} }
private draw(c: BeforeRenderCommand) { private draw(c: RenderCommand) {
const canvasAspectRatio = c.renderer.canvasSize.x / c.renderer.canvasSize.y; const canvasAspectRatio = c.renderer.canvasSize.x / c.renderer.canvasSize.y;
this.viewArea.size = vec2.fromValues( this.viewArea.size = vec2.fromValues(
@ -35,8 +35,8 @@ export class Camera extends GameObject {
Math.sqrt(this.inViewAreaSize / canvasAspectRatio) Math.sqrt(this.inViewAreaSize / canvasAspectRatio)
); );
c.renderer.setViewArea(this._viewArea); c.renderer.setViewArea(this._viewArea.topLeft, this.viewArea.size);
c.renderer.setCursorPosition(this.cursorPosition); //c.renderer.setCursorPosition(this.cursorPosition);
} }
private moveTo(c: MoveToCommand) { private moveTo(c: MoveToCommand) {

View file

@ -1,7 +1,6 @@
import { vec2, vec3 } from 'gl-matrix'; import { vec2, vec3 } from 'gl-matrix';
import { Flashlight } from 'sdf-2d';
import { RenderCommand } from '../../graphics/commands/render'; import { RenderCommand } from '../../graphics/commands/render';
import { DrawableBlob } from '../../graphics/drawables/drawable-blob';
import { Flashlight } from '../../graphics/drawables/lights/flashlight';
import { IGame } from '../../i-game'; import { IGame } from '../../i-game';
import { KeyDownCommand } from '../../input/commands/key-down'; import { KeyDownCommand } from '../../input/commands/key-down';
import { KeyUpCommand } from '../../input/commands/key-up'; import { KeyUpCommand } from '../../input/commands/key-up';
@ -9,19 +8,18 @@ import { SwipeCommand } from '../../input/commands/swipe';
import { StepCommand } from '../../physics/commands/step'; import { StepCommand } from '../../physics/commands/step';
import { TeleportToCommand } from '../../physics/commands/teleport-to'; import { TeleportToCommand } from '../../physics/commands/teleport-to';
import { IShape } from '../../shapes/i-shape'; import { IShape } from '../../shapes/i-shape';
import { Blob } from '../../shapes/types/blob'; import { BlobShape } from '../../shapes/types/blob-shape';
import { GameObject } from '../game-object'; import { GameObject } from '../game-object';
export class Character extends GameObject { export class Character extends GameObject {
private keysDown: Set<string> = new Set(); private keysDown: Set<string> = new Set();
private light = new Flashlight( private light = new Flashlight(
vec2.create(), vec2.create(),
vec2.fromValues(-1, 0),
0.7,
vec3.fromValues(1, 0.6, 0.45), vec3.fromValues(1, 0.6, 0.45),
1.5 1.5,
vec2.fromValues(-1, 0)
); );
private shape = new DrawableBlob(vec2.create()); private shape = new BlobShape(vec2.create());
private static speed = 1.5; private static speed = 1.5;
constructor(private game: IGame) { constructor(private game: IGame) {
@ -41,8 +39,8 @@ export class Character extends GameObject {
} }
private draw(c: RenderCommand) { private draw(c: RenderCommand) {
c.renderer.drawShape(this.shape); c.renderer.addDrawable(this.shape);
c.renderer.drawLight(this.light); c.renderer.addDrawable(this.light);
} }
private tryMoving(delta: vec2, isFirstIteration = true) { private tryMoving(delta: vec2, isFirstIteration = true) {
@ -98,14 +96,14 @@ export class Character extends GameObject {
} }
} }
private getNearShapesTo(shape: Blob): Array<{ shape: IShape; distance: number }> { private getNearShapesTo(shape: BlobShape): Array<{ shape: IShape; distance: number }> {
return this.game return this.game
.findIntersecting(shape.boundingBox) .findIntersecting(shape.boundingBox)
.filter((b) => b.shape) .filter((b) => b.shape)
.map((b) => ({ .map((b) => ({
shape: b.shape, shape: b.shape,
// TODO: fix this // TODO: fix this
distance: b.shape.distance(shape.center) + shape.radius - 20, distance: b.shape.minDistance(shape.center) + shape.radius - 20,
})) }))
.sort((e) => e.distance); .sort((e) => e.distance);
} }

View file

@ -1,36 +0,0 @@
import { RenderCommand } from '../../graphics/commands/render';
import { GameObject } from '../game-object';
export class InfoText extends GameObject {
private static MinRowLength = 60;
constructor() {
super();
this.addCommandExecutor(RenderCommand, this.draw.bind(this));
}
private static records: Map<string, string> = new Map();
public static modifyRecord(key: string, value: string | any) {
if (typeof value == 'string') {
value = ' ' + value;
} else {
value = JSON.stringify(
value,
(_, v) => (v.toFixed ? Number(v.toFixed(2)) : v),
' '
);
}
InfoText.records.set(key, value);
}
private draw(e: RenderCommand) {
let text = '';
InfoText.records.forEach(
(v, k) => (text += `${k}\n${v.padEnd(InfoText.MinRowLength)}\n`)
);
e.renderer.drawInfoText(text);
}
}

View file

@ -1,23 +1,23 @@
import { vec2, vec3 } from 'gl-matrix'; import { vec2, vec3 } from 'gl-matrix';
import { CircleLight } from 'sdf-2d';
import { RenderCommand } from '../../graphics/commands/render'; import { RenderCommand } from '../../graphics/commands/render';
import { CircleLight } from '../../graphics/drawables/lights/circle-light';
import { MoveToCommand } from '../../physics/commands/move-to'; import { MoveToCommand } from '../../physics/commands/move-to';
import { GameObject } from '../game-object'; import { GameObject } from '../game-object';
export class Lamp extends GameObject { export class Lamp extends GameObject {
private light: CircleLight; private light: CircleLight;
constructor(center: vec2, radius: number, color: vec3, lightness: number) { constructor(center: vec2, color: vec3, lightness: number) {
super(); super();
this.light = new CircleLight(center, radius, color, lightness); this.light = new CircleLight(center, color, lightness);
this.addCommandExecutor(RenderCommand, this.draw.bind(this)); this.addCommandExecutor(RenderCommand, this.draw.bind(this));
this.addCommandExecutor(MoveToCommand, this.moveTo.bind(this)); this.addCommandExecutor(MoveToCommand, this.moveTo.bind(this));
} }
private draw(c: RenderCommand) { private draw(c: RenderCommand) {
c.renderer.drawLight(this.light); c.renderer.addDrawable(this.light);
} }
private moveTo(c: MoveToCommand) { private moveTo(c: MoveToCommand) {

View file

@ -1,11 +1,11 @@
import { vec2 } from 'gl-matrix'; import { vec2 } from 'gl-matrix';
import { RenderCommand } from '../../graphics/commands/render'; import { RenderCommand } from '../../graphics/commands/render';
import { DrawableTunnel } from '../../graphics/drawables/drawable-tunnel';
import { Physics } from '../../physics/physics'; import { Physics } from '../../physics/physics';
import { TunnelShape } from '../../shapes/types/tunnel-shape';
import { GameObject } from '../game-object'; import { GameObject } from '../game-object';
export class Tunnel extends GameObject { export class Tunnel extends GameObject {
private shape: DrawableTunnel; private shape: TunnelShape;
constructor( constructor(
physics: Physics, physics: Physics,
@ -16,12 +16,12 @@ export class Tunnel extends GameObject {
) { ) {
super(); super();
this.shape = new DrawableTunnel(from, to, fromRadius, toRadius, this); this.shape = new TunnelShape(from, to, fromRadius, toRadius, this);
physics.addStaticBoundingBox(this.shape.boundingBox); physics.addStaticBoundingBox(this.shape.boundingBox);
this.addCommandExecutor(RenderCommand, this.draw.bind(this)); this.addCommandExecutor(RenderCommand, this.draw.bind(this));
} }
private draw(c: RenderCommand) { private draw(c: RenderCommand) {
c.renderer.drawShape(this.shape); c.renderer.addDrawable(this.shape);
} }
} }

View file

@ -1,15 +1,12 @@
import { vec2 } from 'gl-matrix'; import { vec2 } from 'gl-matrix';
import { BoundingBox } from './bounding-box';
import { GameObject } from '../objects/game-object'; import { GameObject } from '../objects/game-object';
import { BoundingBox } from './bounding-box';
export interface IShape { export interface IShape {
readonly isInverted: boolean;
readonly boundingBox: BoundingBox; readonly boundingBox: BoundingBox;
readonly gameObject?: GameObject; readonly gameObject?: GameObject;
distance(target: vec2): number; minDistance(target: vec2): number;
normal(from: vec2): vec2; normal(from: vec2): vec2;
clone(): IShape; clone(): IShape;
} }

View file

@ -0,0 +1,127 @@
import { mat2d, vec2 } from 'gl-matrix';
import { Circle, Drawable, DrawableDescriptor } from 'sdf-2d';
import { GameObject } from '../../objects/game-object';
import { BoundingBox } from '../bounding-box';
import { IShape } from '../i-shape';
import { CircleShape } from './circle-shape';
export class BlobShape extends Drawable implements IShape {
public static descriptor: DrawableDescriptor = {
sdf: {
shader: `
uniform struct {
vec2 headCenter;
vec2 leftFootCenter;
vec2 rightFootCenter;
float headRadius;
float footRadius;
float k;
}[BLOB_COUNT] blobs;
float smoothMin(float a, float b)
{
const float k = 80.0;
float res = exp2( -k*a ) + exp2( -k*b );
return -log2( res )/k;
}
float circleDistance(vec2 circleCenter, float radius) {
return distance(position, circleCenter) - radius;
}
void blobMinDistance(inout float minDistance, inout float color) {
for (int i = 0; i < BLOB_COUNT; i++) {
float headDistance = circleDistance(blobs[i].headCenter, blobs[i].headRadius);
float leftFootDistance = circleDistance(blobs[i].leftFootCenter, blobs[i].footRadius);
float rightFootDistance = circleDistance(blobs[i].rightFootCenter, blobs[i].footRadius);
float res = min(
smoothMin(headDistance, leftFootDistance),
smoothMin(headDistance, rightFootDistance)
);
minDistance = min(minDistance, res);
color = mix(1.0, color, step(distanceNdcPixelSize + SURFACE_OFFSET, res));
}
}
`,
distanceFunctionName: 'blobMinDistance',
},
uniformName: 'blobs',
uniformCountMacroName: 'BLOB_COUNT',
shaderCombinationSteps: [1],
empty: new BlobShape(vec2.fromValues(0, 0)),
};
public readonly boundingCircleRadius = 100;
protected readonly headRadius = 40;
protected readonly footRadius = 15;
private readonly headOffset = vec2.fromValues(0, -15);
private readonly leftFootOffset = vec2.fromValues(-12, -60);
private readonly rightFootOffset = vec2.fromValues(12, -60);
public readonly isInverted = false;
protected boundingCircle = new CircleShape(vec2.create(), this.boundingCircleRadius);
protected head = new Circle(vec2.create(), this.headRadius);
protected leftFoot = new Circle(vec2.create(), this.footRadius);
protected rightFoot = new Circle(vec2.create(), this.footRadius);
public constructor(center: vec2, public readonly gameObject: GameObject = null) {
super();
this.position = center;
}
public set position(value: vec2) {
vec2.copy(this.boundingCircle.center, value);
vec2.add(this.head.center, value, this.headOffset);
vec2.add(this.leftFoot.center, value, this.leftFootOffset);
vec2.add(this.rightFoot.center, value, this.rightFootOffset);
}
public get center(): vec2 {
return this.boundingCircle.center;
}
public get radius(): number {
return this.boundingCircle.radius;
}
public minDistance(target: vec2): number {
return this.boundingCircle.minDistance(target);
}
public normal(from: vec2): vec2 {
return this.boundingCircle.normal(from);
}
public get boundingBox(): BoundingBox {
return this.boundingCircle.boundingBox;
}
public isInside(target: vec2): boolean {
return this.minDistance(target) < 0;
}
public clone(): BlobShape {
return new BlobShape(this.boundingCircle.center, this.gameObject);
}
protected getObjectToSerialize(transform2d: mat2d, transform1d: number): any {
return {
headCenter: vec2.transformMat2d(vec2.create(), this.head.center, transform2d),
leftFootCenter: vec2.transformMat2d(
vec2.create(),
this.leftFoot.center,
transform2d
),
rightFootCenter: vec2.transformMat2d(
vec2.create(),
this.rightFoot.center,
transform2d
),
headRadius: this.headRadius * transform1d,
footRadius: this.footRadius * transform1d,
};
}
}

View file

@ -1,60 +0,0 @@
import { vec2 } from 'gl-matrix';
import { GameObject } from '../../objects/game-object';
import { BoundingBox } from '../bounding-box';
import { IShape } from '../i-shape';
import { Circle } from './circle';
export class Blob implements IShape {
public readonly boundingCircleRadius = 100;
protected readonly headRadius = 40;
protected readonly footRadius = 15;
private readonly headOffset = vec2.fromValues(0, -15);
private readonly leftFootOffset = vec2.fromValues(-12, -60);
private readonly rightFootOffset = vec2.fromValues(12, -60);
public readonly isInverted = false;
protected boundingCircle = new Circle(vec2.create(), this.boundingCircleRadius);
protected head = new Circle(vec2.create(), this.headRadius);
protected leftFoot = new Circle(vec2.create(), this.footRadius);
protected rightFoot = new Circle(vec2.create(), this.footRadius);
public constructor(center: vec2, public readonly gameObject: GameObject = null) {
this.position = center;
}
public set position(value: vec2) {
vec2.copy(this.boundingCircle.center, value);
vec2.add(this.head.center, value, this.headOffset);
vec2.add(this.leftFoot.center, value, this.leftFootOffset);
vec2.add(this.rightFoot.center, value, this.rightFootOffset);
}
public get center(): vec2 {
return this.boundingCircle.center;
}
public get radius(): number {
return this.boundingCircle.radius;
}
public distance(target: vec2): number {
return this.boundingCircle.distance(target);
}
public normal(from: vec2): vec2 {
return this.boundingCircle.normal(from);
}
public get boundingBox(): BoundingBox {
return this.boundingCircle.boundingBox;
}
public isInside(target: vec2): boolean {
return this.distance(target) < 0;
}
public clone(): Blob {
return new Blob(this.boundingCircle.center, this.gameObject);
}
}

View file

@ -1,16 +1,19 @@
import { vec2 } from 'gl-matrix'; import { vec2 } from 'gl-matrix';
import { Circle } from 'sdf-2d';
import { GameObject } from '../../objects/game-object'; import { GameObject } from '../../objects/game-object';
import { BoundingBox } from '../bounding-box'; import { BoundingBox } from '../bounding-box';
import { IShape } from '../i-shape'; import { IShape } from '../i-shape';
export class Circle implements IShape { export class CircleShape extends Circle implements IShape {
public readonly isInverted = false; public readonly isInverted = false;
public constructor( public constructor(
public center = vec2.create(), center = vec2.create(),
public radius = 0, radius = 0,
public readonly gameObject: GameObject = null public readonly gameObject: GameObject = null
) {} ) {
super(center, radius);
}
public distance(target: vec2): number { public distance(target: vec2): number {
return vec2.distance(this.center, target) - this.radius; return vec2.distance(this.center, target) - this.radius;
@ -35,12 +38,12 @@ export class Circle implements IShape {
return this.distance(target) < 0; return this.distance(target) < 0;
} }
public areIntersecting(other: Circle): boolean { public areIntersecting(other: CircleShape): boolean {
const distance = vec2.distance(this.center, other.center); const distance = vec2.distance(this.center, other.center);
return distance < this.radius + other.radius; return distance < this.radius + other.radius;
} }
public clone(): Circle { public clone(): CircleShape {
return new Circle(vec2.clone(this.center), this.radius, this.gameObject); return new CircleShape(vec2.clone(this.center), this.radius, this.gameObject);
} }
} }

View file

@ -1,24 +1,20 @@
import { vec2 } from 'gl-matrix'; import { vec2 } from 'gl-matrix';
import { InvertedTunnel } from 'sdf-2d';
import { clamp01 } from '../../helper/clamp'; import { clamp01 } from '../../helper/clamp';
import { mix } from '../../helper/mix';
import { rotate90Deg } from '../../helper/rotate-90-deg'; import { rotate90Deg } from '../../helper/rotate-90-deg';
import { GameObject } from '../../objects/game-object'; import { GameObject } from '../../objects/game-object';
import { BoundingBox } from '../bounding-box'; import { BoundingBox } from '../bounding-box';
import { IShape } from '../i-shape'; import { IShape } from '../i-shape';
export default class TunnelShape implements IShape { export class TunnelShape extends InvertedTunnel implements IShape {
public readonly isInverted = true;
public readonly toFromDelta: vec2;
constructor( constructor(
public readonly from: vec2, readonly from: vec2,
public readonly to: vec2, readonly to: vec2,
public readonly fromRadius: number, readonly fromRadius: number,
public readonly toRadius: number, readonly toRadius: number,
public readonly gameObject: GameObject = null public readonly gameObject: GameObject = null
) { ) {
this.toFromDelta = vec2.subtract(vec2.create(), to, from); super(from, to, fromRadius, toRadius);
} }
public get boundingBox(): BoundingBox { public get boundingBox(): BoundingBox {
@ -33,9 +29,10 @@ export default class TunnelShape implements IShape {
public normal(target: vec2): vec2 { public normal(target: vec2): vec2 {
const targetFromDelta = vec2.subtract(vec2.create(), target, this.from); const targetFromDelta = vec2.subtract(vec2.create(), target, this.from);
const toFromDelta = vec2.subtract(vec2.create(), this.to, this.from);
const h = clamp01( const h = clamp01(
vec2.dot(targetFromDelta, this.toFromDelta) / vec2.dot(targetFromDelta, toFromDelta) / vec2.dot(toFromDelta, toFromDelta)
vec2.dot(this.toFromDelta, this.toFromDelta)
); );
let diff = vec2.create(); let diff = vec2.create();
@ -46,10 +43,10 @@ export default class TunnelShape implements IShape {
vec2.subtract(diff, target, this.from); vec2.subtract(diff, target, this.from);
} else { } else {
const side = Math.sign( const side = Math.sign(
this.toFromDelta.x * targetFromDelta.y - this.toFromDelta.y * targetFromDelta.x toFromDelta.x * targetFromDelta.y - toFromDelta.y * targetFromDelta.x
); );
const normal = rotate90Deg(this.toFromDelta); const normal = rotate90Deg(toFromDelta);
vec2.normalize(normal, normal); vec2.normalize(normal, normal);
const translatedFrom = vec2.add( const translatedFrom = vec2.add(
@ -72,20 +69,6 @@ export default class TunnelShape implements IShape {
return vec2.normalize(diff, diff); return vec2.normalize(diff, diff);
} }
public distance(target: vec2): number {
const targetFromDelta = vec2.subtract(vec2.create(), target, this.from);
const h = clamp01(
vec2.dot(targetFromDelta, this.toFromDelta) /
vec2.dot(this.toFromDelta, this.toFromDelta)
);
return (
vec2.distance(targetFromDelta, vec2.scale(vec2.create(), this.toFromDelta, h)) -
mix(this.fromRadius, this.toRadius, h)
);
}
public clone(): TunnelShape { public clone(): TunnelShape {
return new TunnelShape( return new TunnelShape(
vec2.clone(this.from), vec2.clone(this.from),

View file

@ -1,3 +1,19 @@
$breakpoint: 800px;
* {
margin: 0;
box-sizing: border-box;
color: white;
}
html {
background-color: linear-gradient(45deg, #103783, #9bafd9);
@media (max-width: $breakpoint) {
font-size: 0.7rem;
}
}
html, html,
body, body,
canvas#main { canvas#main {
@ -6,31 +22,16 @@ canvas#main {
} }
body { body {
position: relative;
margin: 0;
#overlay { #overlay {
font-family: Helvetica, Arial, sans-serif; margin: 0.5rem 1.25rem;
font-size: 0.75em;
margin: 0.75em 1em;
user-select: none;
pointer-events: none;
position: absolute; position: absolute;
right: 0; right: 0;
$outline-width: 0.5px; font-size: 0.75rem;
text-shadow: -$outline-width -$outline-width 0 #000,
$outline-width -$outline-width 0 #000, -$outline-width $outline-width 0 #000,
$outline-width $outline-width 0 #000;
color: white;
white-space: pre; white-space: pre;
font-family: 'Lucida Console', Monaco, monospace;
@media (max-width: 800px) { @media (max-width: $breakpoint) {
font-size: 0.65em; font-size: 0.6rem;
} }
} }
canvas#main {
width: 100%;
background-color: hotpink;
}
} }

View file

@ -3,12 +3,12 @@
"outDir": "./dist/", "outDir": "./dist/",
"sourceMap": true, "sourceMap": true,
"noImplicitAny": false, "noImplicitAny": false,
"target": "es5", "target": "es6",
"downlevelIteration": true, "downlevelIteration": true,
"allowJs": true, "allowJs": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"moduleResolution": "Node", "moduleResolution": "Node",
"module": "commonjs", "module": "es6",
"lib": ["es2015", "dom"] "lib": ["es2015", "dom"]
} }
} }

View file

@ -7,23 +7,34 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin'); const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin');
const Sass = require('sass'); const Sass = require('sass');
const isProduction = process.env.NODE_ENV == 'production'; const PATHS = {
const isDevelopment = !isProduction; entryPoint: path.resolve(__dirname, 'src/index.ts'),
bundles: path.resolve(__dirname, 'dist'),
};
module.exports = { module.exports = {
entry: {
index: PATHS.entryPoint,
},
target: 'web',
output: {
filename: '[name].[contenthash].js',
path: PATHS.bundles,
},
devtool: 'source-map',
watchOptions: { watchOptions: {
ignored: /node_modules/, ignored: /node_modules/,
}, },
devtool: 'inline-source-map',
devServer: { devServer: {
host: '0.0.0.0', host: '0.0.0.0',
disableHostCheck: true, disableHostCheck: true,
}, },
optimization: { optimization: {
minimize: true, minimize: true,
usedExports: true,
minimizer: [ minimizer: [
new TerserJSPlugin({ new TerserJSPlugin({
sourceMap: isDevelopment, sourceMap: true,
cache: true, cache: true,
test: /\.ts$/i, test: /\.ts$/i,
terserOptions: { terserOptions: {
@ -66,25 +77,8 @@ module.exports = {
chunkFilename: '[id].[contenthash].css', chunkFilename: '[id].[contenthash].css',
}), }),
], ],
entry: {
index: './src/index.ts',
},
module: { module: {
rules: [ rules: [
{
test: /\.(glsl)$/,
use: {
loader: 'raw-loader',
},
},
{
test: /\.svg$/,
loader: 'svg-url-loader',
options: {
limit: 10 * 1024,
noquotes: true,
},
},
{ {
test: /\.ico$/i, test: /\.ico$/i,
use: { use: {
@ -116,17 +110,6 @@ module.exports = {
}, },
], ],
}, },
{
test: /\.(woff2?|ttf|eot|svg)(?:[?#].+)?$/,
use: {
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'static/fonts/',
},
},
include: /fonts/,
},
{ {
test: /\.ts$/, test: /\.ts$/,
use: { use: {
@ -137,10 +120,6 @@ module.exports = {
], ],
}, },
resolve: { resolve: {
extensions: ['.ts', '.js', '.glsl'], extensions: ['.ts', '.js'],
},
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
}, },
}; };