diff --git a/frontend/custom.d.ts b/frontend/custom.d.ts index eae9908..7c29b64 100644 --- a/frontend/custom.d.ts +++ b/frontend/custom.d.ts @@ -1,9 +1,4 @@ -declare module '*.frag' { - const content: string; - export default content; -} - -declare module '*.vert' { +declare module '*.glsl' { const content: string; export default content; } diff --git a/frontend/package.json b/frontend/package.json index 3e3af8c..f5e3552 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -52,7 +52,6 @@ "uuid": "^8.2.0", "webpack": "^4.43.0", "webpack-cli": "^3.3.11", - "webpack-dev-server": "^3.10.3", - "webpack-glsl-minify": "^1.4.0" + "webpack-dev-server": "^3.10.3" } } diff --git a/frontend/src/scripts/commands/types/before-draw.ts b/frontend/src/scripts/commands/types/before-draw.ts new file mode 100644 index 0000000..ae3b3f4 --- /dev/null +++ b/frontend/src/scripts/commands/types/before-draw.ts @@ -0,0 +1,8 @@ +import { Drawer } from '../../drawing/drawer'; +import { Command } from '../command'; + +export class BeforeDrawCommand extends Command { + public constructor(public readonly drawer?: Drawer) { + super(); + } +} diff --git a/frontend/src/scripts/commands/types/move-to.ts b/frontend/src/scripts/commands/types/move-to.ts index da0f29b..eebf714 100644 --- a/frontend/src/scripts/commands/types/move-to.ts +++ b/frontend/src/scripts/commands/types/move-to.ts @@ -1,4 +1,3 @@ -import { Drawer } from '../../drawing/drawer'; import { Command } from '../command'; import { Vec2 } from '../../math/vec2'; diff --git a/frontend/src/scripts/commands/types/zoom.ts b/frontend/src/scripts/commands/types/zoom.ts new file mode 100644 index 0000000..1fc9fab --- /dev/null +++ b/frontend/src/scripts/commands/types/zoom.ts @@ -0,0 +1,7 @@ +import { Command } from '../command'; + +export class ZoomCommand extends Command { + public constructor(public readonly factor?: number) { + super(); + } +} diff --git a/frontend/src/scripts/drawing/drawer.ts b/frontend/src/scripts/drawing/drawer.ts index 6fcbae0..bd3da83 100644 --- a/frontend/src/scripts/drawing/drawer.ts +++ b/frontend/src/scripts/drawing/drawer.ts @@ -1,9 +1,12 @@ import { Vec2 } from '../math/vec2'; +import { Rectangle } from '../math/rectangle'; export interface Drawer { - startFrame(); - finishFrame(); - setCameraPosition(position: Vec2); + startFrame(): void; + finishFrame(): void; + giveUniforms(uniforms: any): void; + setCameraPosition(position: Vec2): void; setInViewArea(size: number): Vec2; - drawInfoText(text: string); + drawInfoText(text: string): void; + isOnScreen(position: Vec2): boolean; } diff --git a/frontend/src/scripts/drawing/graphics-library/create-program.ts b/frontend/src/scripts/drawing/graphics-library/create-program.ts deleted file mode 100644 index 9b6ad6e..0000000 --- a/frontend/src/scripts/drawing/graphics-library/create-program.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createShader } from './create-shader'; - -export const createProgram = ( - gl: WebGL2RenderingContext, - vertexShader: string, - fragmentShader: string -): WebGLProgram => { - const program = gl.createProgram(); - - gl.attachShader(program, createShader(gl, gl.VERTEX_SHADER, vertexShader)); - gl.attachShader( - program, - createShader(gl, gl.FRAGMENT_SHADER, fragmentShader) - ); - gl.linkProgram(program); - - const success = gl.getProgramParameter(program, gl.LINK_STATUS); - if (success) { - return program; - } - - console.log(gl.getProgramInfoLog(program)); - gl.deleteProgram(program); -}; diff --git a/frontend/src/scripts/drawing/graphics-library/create-shader.ts b/frontend/src/scripts/drawing/graphics-library/create-shader.ts index 37c82db..70cbaa5 100644 --- a/frontend/src/scripts/drawing/graphics-library/create-shader.ts +++ b/frontend/src/scripts/drawing/graphics-library/create-shader.ts @@ -7,10 +7,10 @@ export const createShader = ( gl.shaderSource(shader, source); gl.compileShader(shader); const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS); - if (success) { - return shader; + + if (!success) { + throw new Error(gl.getShaderInfoLog(shader)); } - console.log(gl.getShaderInfoLog(shader)); - gl.deleteShader(shader); + return shader; }; diff --git a/frontend/src/scripts/drawing/graphics-library/fragment-shader-only-program.ts b/frontend/src/scripts/drawing/graphics-library/fragment-shader-only-program.ts new file mode 100644 index 0000000..9def980 --- /dev/null +++ b/frontend/src/scripts/drawing/graphics-library/fragment-shader-only-program.ts @@ -0,0 +1,121 @@ +import passthroughVertexShader from '../../../shaders/passthrough-vs.glsl'; +import { createShader } from './create-shader'; +import { loadUniform } from './load-uniform'; + +export class FragmentShaderOnlyProgram { + program: WebGLProgram; + shaders: Array = []; + + private vao: WebGLVertexArrayObject; + private uniforms: Array<{ + name: Array; + location: WebGLUniformLocation; + type: GLenum; + }> = []; + + constructor( + private gl: WebGL2RenderingContext, + fragmentShaderSource: string + ) { + this.createProgram(fragmentShaderSource); + this.prepareScreenQuad('a_position'); + this.queryUniforms(); + } + + public bind() { + this.gl.useProgram(this.program); + this.gl.bindVertexArray(this.vao); + } + + public draw() { + this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4); + } + + public setUniforms(values: 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); + } + }); + } + + 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, + }); + } + } + + private createProgram(fragmentShaderSource: string) { + this.program = this.gl.createProgram(); + + const vertexShader = createShader( + this.gl, + this.gl.VERTEX_SHADER, + passthroughVertexShader + ); + this.gl.attachShader(this.program, vertexShader); + this.shaders.push(vertexShader); + + const fragmentShader = createShader( + this.gl, + this.gl.FRAGMENT_SHADER, + fragmentShaderSource + ); + this.gl.attachShader(this.program, fragmentShader); + this.shaders.push(fragmentShader); + + this.gl.linkProgram(this.program); + + const success = this.gl.getProgramParameter( + this.program, + this.gl.LINK_STATUS + ); + if (!success) { + throw new Error(this.gl.getProgramInfoLog(this.program)); + } + } + + 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 + ); + } +} diff --git a/frontend/src/scripts/drawing/graphics-library/load-uniform.ts b/frontend/src/scripts/drawing/graphics-library/load-uniform.ts new file mode 100644 index 0000000..6b0699e --- /dev/null +++ b/frontend/src/scripts/drawing/graphics-library/load-uniform.ts @@ -0,0 +1,41 @@ +import { Vec2 } from '../../math/vec2'; +import { Mat3 } from '../../math/mat3'; + +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) => + gl.uniform1f(l, v) + ); + + converters.set(WebGL2RenderingContext.FLOAT_VEC2, (gl, v: Vec2, l) => + gl.uniform2fv(l, new Float32Array(v.list)) + ); + + converters.set(WebGL2RenderingContext.FLOAT_MAT3, (gl, v: Mat3, l) => + gl.uniformMatrix3fv(l, false, new Float32Array(v.transposedFlat)) + ); + + converters.set(WebGL2RenderingContext.BOOL, (gl, v, l) => + gl.uniform1i(l, v) + ); + + if (!converters.has(type)) { + throw new Error('Unimplemented webgl type'); + } + + converters.get(type)(gl, value, location); + } +}; diff --git a/frontend/src/scripts/drawing/graphics-library/prepare-screen-quad.ts b/frontend/src/scripts/drawing/graphics-library/prepare-screen-quad.ts deleted file mode 100644 index 4a33466..0000000 --- a/frontend/src/scripts/drawing/graphics-library/prepare-screen-quad.ts +++ /dev/null @@ -1,26 +0,0 @@ -export const prepareScreenQuad = ( - gl: WebGL2RenderingContext, - program: WebGLProgram, - attributeName: string -): WebGLVertexArrayObject => { - const positionAttributeLocation = gl.getAttribLocation( - program, - attributeName - ); - - const positionBuffer = gl.createBuffer(); - - gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); - gl.bufferData( - gl.ARRAY_BUFFER, - new Float32Array([-1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0]), - gl.STATIC_DRAW - ); - const vao = gl.createVertexArray(); - - gl.bindVertexArray(vao); - gl.enableVertexAttribArray(positionAttributeLocation); - gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0); - - return vao; -}; diff --git a/frontend/src/scripts/drawing/graphics-library/stopwatch.ts b/frontend/src/scripts/drawing/graphics-library/stopwatch.ts new file mode 100644 index 0000000..93cb365 --- /dev/null +++ b/frontend/src/scripts/drawing/graphics-library/stopwatch.ts @@ -0,0 +1,64 @@ +import { WebGl2Renderer } from '../webgl2-renderer'; +import { InfoText } from '../../objects/types/info-text'; + +// 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) { + if ( + this.gl + .getSupportedExtensions() + .indexOf('EXT_disjoint_timer_query_webgl2') == -1 + ) { + throw new Error('Unsupported extension'); + } + + this.timerExtension = this.gl.getExtension( + '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; + } +} diff --git a/frontend/src/scripts/drawing/pipeline.ts b/frontend/src/scripts/drawing/pipeline.ts deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/src/scripts/drawing/renderer.ts b/frontend/src/scripts/drawing/webgl2-renderer.ts similarity index 51% rename from frontend/src/scripts/drawing/renderer.ts rename to frontend/src/scripts/drawing/webgl2-renderer.ts index d2e7f2a..87a3cba 100644 --- a/frontend/src/scripts/drawing/renderer.ts +++ b/frontend/src/scripts/drawing/webgl2-renderer.ts @@ -1,32 +1,37 @@ import { Drawer } from './drawer'; import { Vec2 } from '../math/vec2'; -import { createProgram } from './graphics-library/create-program'; -import { prepareScreenQuad } from './graphics-library/prepare-screen-quad'; import { Mat3 } from '../math/mat3'; +import { FragmentShaderOnlyProgram } from './graphics-library/fragment-shader-only-program'; +import { WebGlStopwatch } from './graphics-library/stopwatch'; +import { Rectangle } from '../math/rectangle'; export class WebGl2Renderer implements Drawer { private gl: WebGL2RenderingContext; - private program: WebGLProgram; - private vao: WebGLVertexArrayObject; + private program: FragmentShaderOnlyProgram; + private stopwatch: WebGlStopwatch; public enableHighDpiRendering = false; - public renderScale = 0.33; + public renderScale = 0.5; - private cameraPosition: Vec2; - private viewBoxSize: Vec2; + private viewBox: Rectangle = new Rectangle(); + + private nextFrameUniforms: any; constructor( private canvas: HTMLCanvasElement, private overlay: HTMLElement, - shaderSources: [string, string] + shaderSources: Array ) { this.gl = this.canvas.getContext('webgl2'); if (!this.gl) { throw new Error('WebGl2 is not supported'); } - this.program = createProgram(this.gl, ...shaderSources); - this.vao = prepareScreenQuad(this.gl, this.program, 'a_position'); + this.program = new FragmentShaderOnlyProgram(this.gl, shaderSources[0]); + + try { + this.stopwatch = new WebGlStopwatch(this.gl); + } catch {} } private handleResize() { @@ -49,51 +54,56 @@ export class WebGl2Renderer implements Drawer { } public startFrame() { + this.stopwatch?.start(); + this.handleResize(); + this.gl.clearColor(0, 0, 0, 0); this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT); - this.gl.useProgram(this.program); - this.gl.bindVertexArray(this.vao); + + this.program.bind(); + this.nextFrameUniforms = {}; } - finishFrame() { + public finishFrame() { const resolution = new Vec2(this.canvas.width, this.canvas.height); - const transform = Mat3.translateMatrix(new Vec2(0.5, 0.5)) - .times(Mat3.scaleMatrix(this.viewBoxSize.divide(resolution))) - .times(Mat3.translateMatrix(this.cameraPosition)); + this.nextFrameUniforms.transform = Mat3.translateMatrix(new Vec2(0.5, 0.5)) + .times(Mat3.scaleMatrix(this.viewBox.size.divide(resolution))) + .times(Mat3.translateMatrix(this.viewBox.topLeft)); - const viewBoxSizeUniformLocation = this.gl.getUniformLocation( - this.program, - 'transform' - ); - this.gl.uniformMatrix3fv( - viewBoxSizeUniformLocation, - false, - new Float32Array(transform.transposedFlat) - ); + this.program.setUniforms(this.nextFrameUniforms); + this.program.draw(); - this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4); + this.stopwatch?.stop(); } - setCameraPosition(position: Vec2) { - this.cameraPosition = position; + public setCameraPosition(position: Vec2) { + this.viewBox.topLeft = position; } - setInViewArea(size: number): Vec2 { + public giveUniforms(uniforms: any): void { + this.nextFrameUniforms = { ...this.nextFrameUniforms, ...uniforms }; + } + + public setInViewArea(size: number): Vec2 { const canvasAspectRatio = this.canvas.clientWidth / this.canvas.clientHeight; - this.viewBoxSize = new Vec2( + this.viewBox.size = new Vec2( Math.sqrt(size * canvasAspectRatio), Math.sqrt(size / canvasAspectRatio) ); - return this.viewBoxSize; + return this.viewBox.size; } - drawInfoText(text: string) { + public drawInfoText(text: string) { if (this.overlay.innerText != text) { this.overlay.innerText = text; } } + + isOnScreen(position: Vec2): boolean { + return this.viewBox.isInside(position); + } } diff --git a/frontend/src/scripts/game.ts b/frontend/src/scripts/game.ts index 05ae664..1c4b14b 100644 --- a/frontend/src/scripts/game.ts +++ b/frontend/src/scripts/game.ts @@ -1,4 +1,4 @@ -import { WebGl2Renderer } from './drawing/renderer'; +import { WebGl2Renderer } from './drawing/webgl2-renderer'; import { KeyboardListener } from './input/keyboard-listener'; import { MouseListener } from './input/mouse-listener'; import { TouchListener } from './input/touch-listener'; @@ -11,16 +11,15 @@ import { Character } from './objects/types/character'; import { InfoText } from './objects/types/info-text'; import { timeIt } from './helper/timing'; -import { GlslShader, GlslVariable, GlslVariableMap } from 'webpack-glsl-minify'; - -let passthroughVertexShader = require('../shaders/passthrough-vs.glsl') as GlslShader; -let distanceFragmentShader = require('../shaders/cave-fs.glsl') as GlslShader; +import caveFragmentShader from '../shaders/cave-fs.glsl'; +import { Dungeon } from './objects/types/dungeon'; +import { BeforeDrawCommand } from './commands/types/before-draw'; export class Game { private previousTime: DOMHighResTimeStamp = 0; private objects: ObjectContainer = new ObjectContainer(); private renderer: WebGl2Renderer; - private frameCount = 0; + private previousFpsValues: Array = []; constructor() { const canvas: HTMLCanvasElement = document.querySelector('canvas#main'); @@ -35,12 +34,7 @@ export class Game { [this.objects] ); - console.log(distanceFragmentShader); - - this.renderer = new WebGl2Renderer(canvas, overlay, [ - passthroughVertexShader.sourceCode, - distanceFragmentShader.sourceCode, - ]); + this.renderer = new WebGl2Renderer(canvas, overlay, [caveFragmentShader]); this.initializeScene(); requestAnimationFrame(this.gameLoop.bind(this)); @@ -49,23 +43,37 @@ export class Game { private initializeScene() { this.objects.addObject(new Character(this.objects)); this.objects.addObject(new InfoText()); + this.objects.addObject(new Dungeon()); } @timeIt() private gameLoop(time: DOMHighResTimeStamp) { const deltaTime = time - this.previousTime; - if (this.frameCount++ % 30 == 0) { - InfoText.modifyRecord('FPS', (1000 / deltaTime).toFixed(1)); - } - this.previousTime = time; + this.calculateFps(deltaTime); this.objects.sendCommand(new StepCommand(deltaTime)); this.renderer.startFrame(); + this.objects.sendCommand(new BeforeDrawCommand(this.renderer)); this.objects.sendCommand(new DrawCommand(this.renderer)); this.renderer.finishFrame(); window.requestAnimationFrame(this.gameLoop.bind(this)); } + + private calculateFps(deltaTime: number) { + this.previousFpsValues.push(1000 / deltaTime); + if (this.previousFpsValues.length > 30) { + const text = `Min: ${this.previousFpsValues[0].toFixed( + 2 + )}\n\tMedian: ${this.previousFpsValues[ + Math.floor(this.previousFpsValues.length / 2) + ].toFixed(2)}`; + + InfoText.modifyRecord('FPS', text); + + this.previousFpsValues = []; + } + } } diff --git a/frontend/src/scripts/helper/last.ts b/frontend/src/scripts/helper/last.ts new file mode 100644 index 0000000..5c319b0 --- /dev/null +++ b/frontend/src/scripts/helper/last.ts @@ -0,0 +1,3 @@ +export function last(a: Array): T | null { + return a.length > 0 ? a[a.length - 1] : null; +} diff --git a/frontend/src/scripts/input/mouse-listener.ts b/frontend/src/scripts/input/mouse-listener.ts index 9eee4b7..c57ad49 100644 --- a/frontend/src/scripts/input/mouse-listener.ts +++ b/frontend/src/scripts/input/mouse-listener.ts @@ -3,6 +3,7 @@ import { PrimaryActionCommand } from '../commands/types/primary-action'; import { SecondaryActionCommand } from '../commands/types/secondary-action'; import { Vec2 } from '../math/vec2'; import { SwipeCommand } from '../commands/types/swipe'; +import { ZoomCommand } from '../commands/types/zoom'; export class MouseListener extends CommandGenerator { private previousPosition: Vec2 = null; @@ -45,6 +46,10 @@ export class MouseListener extends CommandGenerator { const position = this.positionFromEvent(event); this.sendCommand(new SecondaryActionCommand(position)); }); + + target.addEventListener('wheel', (event: MouseWheelEvent) => { + this.sendCommand(new ZoomCommand(event.deltaY > 0 ? 1.3 : 1 / 1.3)); + }); } private positionFromEvent(event: MouseEvent): Vec2 { diff --git a/frontend/src/scripts/math/line.ts b/frontend/src/scripts/math/line.ts deleted file mode 100644 index 86c0924..0000000 --- a/frontend/src/scripts/math/line.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Vec2 } from './vec2'; - -export interface Line { - a: Vec2; - b: Vec2; - normal: Vec2; -} diff --git a/frontend/src/scripts/math/rectangle.ts b/frontend/src/scripts/math/rectangle.ts new file mode 100644 index 0000000..0d68b62 --- /dev/null +++ b/frontend/src/scripts/math/rectangle.ts @@ -0,0 +1,21 @@ +import { Typed } from '../transport/serializable'; +import { Vec2 } from './vec2'; + +export class Rectangle extends Typed { + public constructor( + public topLeft: Vec2 = new Vec2(), + public size: Vec2 = new Vec2() + ) { + super(); + } + + public isInside(position: Vec2): boolean { + const translated = position.subtract(this.topLeft); + return ( + 0 <= translated.x && + translated.x < this.size.x && + 0 <= translated.y && + translated.y < this.size.y + ); + } +} diff --git a/frontend/src/scripts/objects/types/camera.ts b/frontend/src/scripts/objects/types/camera.ts index 9908ccc..87ad7da 100644 --- a/frontend/src/scripts/objects/types/camera.ts +++ b/frontend/src/scripts/objects/types/camera.ts @@ -1,6 +1,7 @@ import { GameObject } from '../game-object'; -import { DrawCommand } from '../../commands/types/draw'; import { MoveToCommand } from '../../commands/types/move-to'; +import { ZoomCommand } from '../../commands/types/zoom'; +import { BeforeDrawCommand } from '../../commands/types/before-draw'; export class Camera extends GameObject { private inViewArea = 1920 * 1080; @@ -8,11 +9,12 @@ export class Camera extends GameObject { constructor() { super(); - this.addCommandExecutor(DrawCommand, this.draw.bind(this)); + this.addCommandExecutor(BeforeDrawCommand, this.draw.bind(this)); this.addCommandExecutor(MoveToCommand, this.moveTo.bind(this)); + this.addCommandExecutor(ZoomCommand, this.zoom.bind(this)); } - private draw(c: DrawCommand) { + private draw(c: BeforeDrawCommand) { c.drawer.setCameraPosition(this.position); this._boundingBoxSize = c.drawer.setInViewArea(this.inViewArea); } @@ -20,4 +22,8 @@ export class Camera extends GameObject { private moveTo(c: MoveToCommand) { this._position = c.position; } + + private zoom(c: ZoomCommand) { + this.inViewArea *= c.factor; + } } diff --git a/frontend/src/scripts/objects/types/dungeon.ts b/frontend/src/scripts/objects/types/dungeon.ts new file mode 100644 index 0000000..b1883dd --- /dev/null +++ b/frontend/src/scripts/objects/types/dungeon.ts @@ -0,0 +1,80 @@ +import { GameObject } from '../game-object'; +import { DrawCommand } from '../../commands/types/draw'; +import { Vec2 } from '../../math/vec2'; +import { last } from '../../helper/last'; + +export interface Line { + from: Vec2; + to: Vec2; + normal: Vec2; + isLineEnd: boolean; +} + +export class Dungeon extends GameObject { + private lines: Array = []; + + constructor() { + super(); + + this.addCommandExecutor(DrawCommand, this.draw.bind(this)); + + let previousHeight = 0; + let previousPoint = { + from: new Vec2(), + to: new Vec2(-10000, 0), + }; + + for (let i = 0; i < 5000; i += 50) { + const height = previousHeight + (Math.random() - 0.5) * 200; + previousHeight = height; + const current = { + from: previousPoint.to, + to: new Vec2(i, height), + normal: new Vec2(1.0), + isLineEnd: false, + }; + this.lines.push(current); + previousPoint = current; + } + + last(this.lines).to = last(this.lines).to.add(new Vec2(10000, 0)); + last(this.lines).isLineEnd = true; + + const delta = new Vec2(200, 400); + this.lines = [ + ...this.lines, + ...this.lines.map(({ from, to, normal, isLineEnd }) => ({ + normal: normal.scale(-1), + from: from.add(delta), + to: to.add(delta), + isLineEnd, + })), + ]; + + this.calculateNormals(); + } + + private calculateNormals() { + this.lines.forEach((l) => { + const tangent = l.to.subtract(l.from); + l.normal = new Vec2( + -l.normal.x * tangent.y, + l.normal.x * tangent.x + ).normalized; + }); + } + + private draw(c: DrawCommand) { + const linesToBeDrawn: Array = []; + + for (let line of this.lines) { + if (c.drawer.isOnScreen(line.from) || c.drawer.isOnScreen(line.to)) { + linesToBeDrawn.push(line); + } else if (line.isLineEnd && last(linesToBeDrawn) != null) { + last(linesToBeDrawn).isLineEnd = true; + } + } + + c.drawer.giveUniforms({ lines: linesToBeDrawn }); + } +} diff --git a/frontend/src/shaders/cave-fs.glsl b/frontend/src/shaders/cave-fs.glsl index 187833c..528478d 100644 --- a/frontend/src/shaders/cave-fs.glsl +++ b/frontend/src/shaders/cave-fs.glsl @@ -2,25 +2,25 @@ precision mediump float; -const float smoothing = 10.0; -const float inf = 1000000.0; -const float pi = 3.141592654; +#define SMOOTHING 10.0 +#define INFINITY 10000.0; +#define LINE_COUNT 100 + float interpolate(float from, float to, float quotient) { - float steppedQ = sin(quotient * pi - pi * 0.5) * 0.5 + 0.5; - return from + (to - from) * clamp(steppedQ, 0.0, 1.0); + return from + (to - from) * smoothstep(0.0, 1.0, quotient); } vec2 rotate90deg(in vec2 vector) { return vec2(-vector.y, vector.x); } -struct Line { +uniform struct Line { vec2 from; vec2 to; vec2 normal; bool isLineEnd; -}[16] lines; +}[LINE_COUNT] lines; float lineDistance(in vec2 position, in Line line, out float h) { vec2 pa = position - line.from, ba = line.to - line.from; @@ -31,85 +31,44 @@ float lineDistance(in vec2 position, in Line line, out float h) { return length(delta) * side; } -Line endDummyLineFromLine(Line line) { - return Line(line.to, line.to + rotate90deg(line.normal), line.normal, false); -} - float getDistance(in vec2 target) { - float minDistance = inf; + float positiveMinDistance = INFINITY; + float negativeMaxDistance = -INFINITY; float leftJoinAcuteness = 0.0; vec2 splitterLineNormalStart = vec2(-1.0, 0.0); - bool skipDistanceToPrevious = true; - for (int i = 0; i < lines.length(); i++) { - Line current = lines[i]; - - Line next; - if (current.isLineEnd || i + 1 == lines.length()) { - next = endDummyLineFromLine(current); - } else { - next = lines[i + 1]; - } - - vec2 splitterLineNormalEnd = rotate90deg(normalize(current.normal + next.normal)); + for (int i = 0; i < LINE_COUNT - 1; i++) { + vec2 splitterLineNormalEnd = rotate90deg(normalize(lines[i].normal + lines[i + 1].normal)); float h; - float distanceToCurrent = lineDistance(target, current, h); - float rightJoinAcuteness = dot(next.to - current.from, next.normal - current.normal); + float distanceToCurrent = lineDistance(target, lines[i], h); + float rightJoinAcuteness = dot(lines[i + 1].to - lines[i].from, lines[i + 1].normal - lines[i].normal); distanceToCurrent -= interpolate( - sign(leftJoinAcuteness) * smoothing, - sign(rightJoinAcuteness) * smoothing, h + sign(leftJoinAcuteness) * SMOOTHING, + sign(rightJoinAcuteness) * SMOOTHING, h ); leftJoinAcuteness = rightJoinAcuteness; if ( !( - dot(target - current.from, splitterLineNormalStart * -sign(dot(current.to - current.from, splitterLineNormalStart))) > 0.0 - || dot(target - current.to, splitterLineNormalEnd * sign(dot(current.from - current.to, splitterLineNormalEnd))) <= 0.0 - ) && abs(distanceToCurrent) < abs(minDistance) + dot(target - lines[i].from, splitterLineNormalStart * -sign(dot(lines[i].to - lines[i].from, splitterLineNormalStart))) > 0.0 + || dot(target - lines[i].to, splitterLineNormalEnd * sign(dot(lines[i].from - lines[i].to, splitterLineNormalEnd))) <= 0.0 + ) ) { - minDistance = distanceToCurrent; + float distanceToCurrentSign = sign(distanceToCurrent); + positiveMinDistance = min(positiveMinDistance, 1.0 / (0.5 + distanceToCurrentSign / 2.0) * distanceToCurrent); + negativeMaxDistance = max(negativeMaxDistance, -1.0 / (0.5 - distanceToCurrentSign / 2.0) * distanceToCurrent); } splitterLineNormalStart = splitterLineNormalEnd; } - return minDistance; -} - -void createWorld() { - lines[0] = Line(vec2(0.0, 300.0), vec2(550.0, 140.0), vec2(1.0), false); - lines[1] = Line(vec2(550.0, 140.0), vec2(750.0, 130.0), vec2(1.0), false); - lines[2] = Line(vec2(750.0, 130.0), vec2(650.0, 230.0), vec2(1.0), false); - lines[3] = Line(vec2(650.0, 230.0), vec2(850.0, 230.0), vec2(1.0), false); - lines[4] = Line(vec2(850.0, 230.0), vec2(800.0, 150.0), vec2(1.0), false); - lines[5] = Line(vec2(800.0, 150.0), vec2(1000.0, 120.0), vec2(1.0), false); - lines[6] = Line(vec2(1000.0, 120.0), vec2(1150, 120.0), vec2(1.0), false); - lines[7] = Line(vec2(1150, 120.0), vec2(10200, 350.0), vec2(1.0), true); - lines[8] = Line(vec2(0.0, 600.0), vec2(550.0, 440.0), vec2(-1.0), false); - lines[9] = Line(vec2(550.0, 440.0), vec2(750.0, 430.0), vec2(-1.0), false); - lines[10] = Line(vec2(750.0, 430.0), vec2(650.0, 530.0), vec2(-1.0), false); - lines[11] = Line(vec2(650.0, 530.0), vec2(850.0, 530.0), vec2(-1.0), false); - lines[12] = Line(vec2(850.0, 530.0), vec2(820.0, 450.0), vec2(-1.0), false); - lines[13] = Line(vec2(820.0, 450.0), vec2(1000.0, 420.0), vec2(-1.0), false); - lines[14] = Line(vec2(1000.0, 420.0), vec2(1150, 420.0), vec2(-1.0), false); - lines[15] = Line(vec2(1150, 420.0), vec2(10200, 650.0), vec2(-1.0), true); - - for (int i = 0; i < lines.length(); i++) { - vec2 tangent = lines[i].to - lines[i].from; - lines[i].normal = normalize( - vec2(-lines[i].normal.x * tangent.y, lines[i].normal.x * tangent.x) - ); - } + return positiveMinDistance < negativeMaxDistance ? positiveMinDistance : -negativeMaxDistance; } uniform mat3 transform; out vec4 fragmentColor; void main() { - createWorld(); - - vec3 projectiveXY = vec3(gl_FragCoord.xy, 1.0); - vec2 position = (projectiveXY * transform).xy; - //fragmentColor = getDistance(position) > 0.0 ? vec4(0.0, 0.0, 0.0, 0.0) : vec4(0.0, 0.0, 0.0, 1.0); + vec2 position = (vec3(gl_FragCoord.xy, 1.0) * transform).xy; fragmentColor = vec4(vec3(1.0) * clamp(0.0, 1.0, getDistance(position)), 1.0); } diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js index 097a724..3374635 100644 --- a/frontend/webpack.config.js +++ b/frontend/webpack.config.js @@ -72,16 +72,7 @@ module.exports = { { test: /\.(glsl)$/, use: { - loader: 'webpack-glsl-minify', - options: { - output: 'object', - esModule: false, - stripVersion: false, - preserveDefines: false, - preserveUniforms: true, - preserveVariables: true, - disableMangle: false, - }, + loader: 'raw-loader', }, }, {