diff --git a/frontend/src/scripts/drawing/drawer.ts b/frontend/src/scripts/drawing/drawer.ts index 95f4e31..692e7f3 100644 --- a/frontend/src/scripts/drawing/drawer.ts +++ b/frontend/src/scripts/drawing/drawer.ts @@ -1,12 +1,14 @@ import { vec2 } from 'gl-matrix'; export interface Drawer { - startFrame(): void; + startFrame(deltaTime: DOMHighResTimeStamp): void; finishFrame(): void; giveUniforms(uniforms: any): void; + appendToUniformList(listName: string, ...values: Array): void; setCameraPosition(position: vec2): void; setCursorPosition(position: vec2): void; setInViewArea(size: number): vec2; + screenUvToWorldCoordinate(mousePosition: vec2): vec2; drawInfoText(text: string): void; isOnScreen(position: vec2): boolean; } diff --git a/frontend/src/scripts/drawing/graphics-library/load-uniform.ts b/frontend/src/scripts/drawing/graphics-library/load-uniform.ts index 8aed086..9a2a16d 100644 --- a/frontend/src/scripts/drawing/graphics-library/load-uniform.ts +++ b/frontend/src/scripts/drawing/graphics-library/load-uniform.ts @@ -1,4 +1,4 @@ -import { mat3, vec2 } from 'gl-matrix'; +import { mat3, ReadonlyVec3, vec2, vec3 } from 'gl-matrix'; const loaderMat3 = mat3.create(); @@ -18,10 +18,15 @@ export const loadUniform = ( > = new Map(); { converters.set(WebGL2RenderingContext.FLOAT, (gl, v, l) => { - if (v.length == 0) { - return; + if (v instanceof Array) { + if (v.length == 0) { + return; + } + + gl.uniform1fv(l, new Float32Array(v)); + } else { + gl.uniform1f(l, v); } - gl.uniform1fv(l, new Float32Array(v)); }); converters.set( @@ -39,13 +44,36 @@ export const loadUniform = ( result[2 * i + 1] = (v[i] as Array).y; } - gl.uniform2fv(l, new Float32Array(result)); + gl.uniform2fv(l, result); } else { gl.uniform2fv(l, v as vec2); } } ); + converters.set( + WebGL2RenderingContext.FLOAT_VEC3, + (gl, v: ReadonlyVec3 | Array, 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)[0]; + result[3 * i + 1] = (v[i] as Array)[1]; + result[3 * i + 2] = (v[i] as Array)[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)); }); @@ -55,7 +83,7 @@ export const loadUniform = ( ); if (!converters.has(type)) { - throw new Error('Unimplemented webgl type'); + throw new Error(`Unimplemented webgl type: ${type}`); } converters.get(type)(gl, value, location); diff --git a/frontend/src/scripts/drawing/webgl2-renderer.ts b/frontend/src/scripts/drawing/webgl2-renderer.ts index b66236f..da179c6 100644 --- a/frontend/src/scripts/drawing/webgl2-renderer.ts +++ b/frontend/src/scripts/drawing/webgl2-renderer.ts @@ -34,15 +34,15 @@ export class WebGl2Renderer implements Drawer { new FragmentShaderOnlyProgram(this.gl, ...shaderSources[1]), ]); - this.distanceFieldFrameBuffer.renderScale = 0.5; - this.lightingFrameBuffer.renderScale = 1; + this.distanceFieldFrameBuffer.renderScale = 0.1; + this.lightingFrameBuffer.renderScale = 0.2; try { this.stopwatch = new WebGlStopwatch(this.gl); } catch {} } - public startFrame(): void { + public startFrame(deltaTime: DOMHighResTimeStamp): void { this.stopwatch?.start(); this.uniforms = {}; this.distanceFieldFrameBuffer.setSize(); @@ -76,8 +76,6 @@ export class WebGl2Renderer implements Drawer { mat2d.scale(ndcToWorld, ndcToWorld, vec2.fromValues(0.5, 0.5)); mat2d.translate(ndcToWorld, ndcToWorld, vec2.fromValues(1, 1)); - const screenToWorld = this.getScreenToWorldTransform(resolution); - const worldToDistanceUV = mat2d.scale( mat2d.create(), distanceScreenToWorld, @@ -85,11 +83,7 @@ export class WebGl2Renderer implements Drawer { ); mat2d.invert(worldToDistanceUV, worldToDistanceUV); - const cursorPosition = vec2.transformMat2d( - vec2.create(), - vec2.multiply(vec2.create(), this.cursorPosition, resolution), - screenToWorld - ); + const cursorPosition = this.screenUvToWorldCoordinate(this.cursorPosition); this.giveUniforms({ distanceScreenToWorld, @@ -115,6 +109,16 @@ export class WebGl2Renderer implements Drawer { return transform; } + public screenUvToWorldCoordinate(screenUvPosition: vec2): vec2 { + const resolution = vec2.fromValues(this.canvas.width, this.canvas.height); + + return vec2.transformMat2d( + vec2.create(), + vec2.multiply(vec2.create(), screenUvPosition, resolution), + this.getScreenToWorldTransform(resolution) + ); + } + public setCameraPosition(position: vec2) { this.viewBox.topLeft = position; } @@ -123,6 +127,16 @@ export class WebGl2Renderer implements Drawer { this.cursorPosition = position; } + public appendToUniformList(listName: string, ...values: any[]): void { + if (!this.uniforms.hasOwnProperty(listName)) { + this.uniforms[listName] = []; + } + + for (let value of values) { + this.uniforms[listName].push(value); + } + } + public giveUniforms(uniforms: any): void { this.uniforms = { ...this.uniforms, ...uniforms }; } @@ -144,7 +158,7 @@ export class WebGl2Renderer implements Drawer { } } - isOnScreen(position: vec2): boolean { + public isOnScreen(position: vec2): boolean { return this.viewBox.isInside(position); } } diff --git a/frontend/src/scripts/game.ts b/frontend/src/scripts/game.ts index eb885cd..050acb3 100644 --- a/frontend/src/scripts/game.ts +++ b/frontend/src/scripts/game.ts @@ -13,9 +13,9 @@ import { KeyboardListener } from './input/keyboard-listener'; import { MouseListener } from './input/mouse-listener'; import { TouchListener } from './input/touch-listener'; import { ObjectContainer } from './objects/object-container'; -import { Character } from './objects/types/character'; -import { Dungeon } from './objects/types/dungeon'; import { InfoText } from './objects/types/info-text'; +import { createCharacter } from './objects/world/create-character'; +import { createDungeon } from './objects/world/create-dungeon'; export class Game { private previousTime: DOMHighResTimeStamp = 0; @@ -46,9 +46,9 @@ export class Game { } private initializeScene() { - this.objects.addObject(new Character(this.objects)); this.objects.addObject(new InfoText()); - this.objects.addObject(new Dungeon()); + createCharacter(this.objects); + createDungeon(this.objects); } @timeIt() @@ -59,7 +59,7 @@ export class Game { this.objects.sendCommand(new StepCommand(deltaTime)); - this.renderer.startFrame(); + this.renderer.startFrame(deltaTime); this.objects.sendCommand(new BeforeDrawCommand(this.renderer)); this.objects.sendCommand(new DrawCommand(this.renderer)); this.renderer.finishFrame(); diff --git a/frontend/src/scripts/objects/game-object.ts b/frontend/src/scripts/objects/game-object.ts index 305f86e..a186716 100644 --- a/frontend/src/scripts/objects/game-object.ts +++ b/frontend/src/scripts/objects/game-object.ts @@ -28,6 +28,10 @@ export abstract class GameObject extends Typed implements CommandReceiver { this.commandExecutors[commandType.name] = handler; } + public reactsToCommand(commandType: new () => T): boolean { + return this.commandExecutors.hasOwnProperty(commandType.name); + } + public sendCommand(command: Command) { const commandType = command.constructor.name; diff --git a/frontend/src/scripts/objects/types/character.ts b/frontend/src/scripts/objects/types/character.ts index 84dbf44..6e95578 100644 --- a/frontend/src/scripts/objects/types/character.ts +++ b/frontend/src/scripts/objects/types/character.ts @@ -5,19 +5,16 @@ import { MoveToCommand } from '../../commands/types/move-to'; import { StepCommand } from '../../commands/types/step'; import { SwipeCommand } from '../../commands/types/swipe'; import { GameObject } from '../game-object'; -import { ObjectContainer } from '../object-container'; import { Camera } from './camera'; export class Character extends GameObject { private keysDown: Set = new Set(); - private camera = new Camera(); private static speed = 0.5; - constructor(objects: ObjectContainer) { + constructor(private camera: Camera) { super(); - objects.addObject(this.camera); this.addCommandExecutor(StepCommand, this.stepHandler.bind(this)); this.addCommandExecutor(KeyDownCommand, (c) => this.keysDown.add(c.key)); this.addCommandExecutor(KeyUpCommand, (c) => this.keysDown.delete(c.key)); diff --git a/frontend/src/scripts/objects/types/circle-light.ts b/frontend/src/scripts/objects/types/circle-light.ts new file mode 100644 index 0000000..b403f93 --- /dev/null +++ b/frontend/src/scripts/objects/types/circle-light.ts @@ -0,0 +1,25 @@ +import { vec2, vec3 } from 'gl-matrix'; +import { DrawCommand } from '../../commands/types/draw'; +import { GameObject } from '../game-object'; + +export class CircleLight extends GameObject { + constructor( + private center: vec2, + private radius: number, + private value: vec3 + ) { + super(); + + this.addCommandExecutor(DrawCommand, this.draw.bind(this)); + } + + private draw(c: DrawCommand) { + if (c.drawer.isOnScreen(this.center)) { + c.drawer.appendToUniformList('lights', { + center: this.center, + radius: this.radius, + value: this.value, + }); + } + } +} diff --git a/frontend/src/scripts/objects/types/cursor-light.ts b/frontend/src/scripts/objects/types/cursor-light.ts new file mode 100644 index 0000000..b5458a6 --- /dev/null +++ b/frontend/src/scripts/objects/types/cursor-light.ts @@ -0,0 +1,30 @@ +import { vec2, vec3 } from 'gl-matrix'; +import { CursorMoveCommand } from '../../commands/types/cursor-move-command'; +import { DrawCommand } from '../../commands/types/draw'; +import { GameObject } from '../game-object'; + +export class CursorLight extends GameObject { + private mousePosition = vec2.create(); + + constructor(private radius: number, private value: vec3) { + super(); + + this.addCommandExecutor(DrawCommand, this.draw.bind(this)); + this.addCommandExecutor(CursorMoveCommand, this.setPosition.bind(this)); + } + + private draw(c: DrawCommand) { + const center = c.drawer.screenUvToWorldCoordinate(this.mousePosition); + if (c.drawer.isOnScreen(center)) { + c.drawer.appendToUniformList('lights', { + center, + radius: this.radius, + value: this.value, + }); + } + } + + private setPosition(c: CursorMoveCommand) { + this.mousePosition = c.position; + } +} diff --git a/frontend/src/scripts/objects/types/dungeon.ts b/frontend/src/scripts/objects/types/dungeon.ts deleted file mode 100644 index ff66718..0000000 --- a/frontend/src/scripts/objects/types/dungeon.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { DrawCommand } from '../../commands/types/draw'; -import { GameObject } from '../game-object'; - -export interface Line { - start: vec2; - end: vec2; - radiusFrom: number; - radiusTo: number; -} - -export class Dungeon extends GameObject { - private lines: Array = []; - - constructor() { - super(); - - this.addCommandExecutor(DrawCommand, this.draw.bind(this)); - - let previousRadius = 350; - let previousEnd = vec2.create(); - - for (let i = 0; i < 500000; i += 500) { - const height = previousEnd.y + (Math.random() - 0.5) * 2000; - const currentEnd = vec2.fromValues(i, height); - const currentToRadius = Math.random() * 300 + 150; - - this.lines.push({ - start: previousEnd, - end: currentEnd, - radiusFrom: previousRadius, - radiusTo: currentToRadius, - }); - - previousEnd = currentEnd; - previousRadius = currentToRadius; - } - } - - private draw(c: DrawCommand) { - const lines: Array = []; - const radii: Array = []; - - for (let line of this.lines) { - if (c.drawer.isOnScreen(line.start) || c.drawer.isOnScreen(line.end)) { - lines.push(line.start); - lines.push(line.end); - radii.push(line.radiusFrom); - radii.push(line.radiusTo); - } - } - - c.drawer.giveUniforms({ lines, radii }); - } -} diff --git a/frontend/src/scripts/objects/types/tunnel.ts b/frontend/src/scripts/objects/types/tunnel.ts new file mode 100644 index 0000000..c8140f3 --- /dev/null +++ b/frontend/src/scripts/objects/types/tunnel.ts @@ -0,0 +1,25 @@ +import { vec2 } from 'gl-matrix'; +import { DrawCommand } from '../../commands/types/draw'; +import { GameObject } from '../game-object'; + +export interface Line {} + +export class Tunnel extends GameObject { + constructor( + private from: vec2, + private to: vec2, + private radiusFrom: number, + private radiusTo: number + ) { + super(); + + this.addCommandExecutor(DrawCommand, this.draw.bind(this)); + } + + private draw(c: DrawCommand) { + if (c.drawer.isOnScreen(this.from) || c.drawer.isOnScreen(this.to)) { + c.drawer.appendToUniformList('lines', this.from, this.to); + c.drawer.appendToUniformList('radii', this.radiusFrom, this.radiusTo); + } + } +} diff --git a/frontend/src/scripts/objects/world/create-character.ts b/frontend/src/scripts/objects/world/create-character.ts new file mode 100644 index 0000000..dab8c2b --- /dev/null +++ b/frontend/src/scripts/objects/world/create-character.ts @@ -0,0 +1,12 @@ +import { vec3 } from 'gl-matrix'; +import { ObjectContainer } from '../object-container'; +import { Camera } from '../types/camera'; +import { Character } from '../types/character'; +import { CursorLight } from '../types/cursor-light'; + +export const createCharacter = (objects: ObjectContainer) => { + const camera = new Camera(); + objects.addObject(camera); + objects.addObject(new Character(camera)); + objects.addObject(new CursorLight(40, vec3.fromValues(0.67, 0.67, 0.33))); +}; diff --git a/frontend/src/scripts/objects/world/create-dungeon.ts b/frontend/src/scripts/objects/world/create-dungeon.ts new file mode 100644 index 0000000..4024e1a --- /dev/null +++ b/frontend/src/scripts/objects/world/create-dungeon.ts @@ -0,0 +1,37 @@ +import { vec2, vec3 } from 'gl-matrix'; +import { ObjectContainer } from '../object-container'; +import { CircleLight } from '../types/circle-light'; +import { Tunnel } from '../types/tunnel'; + +export const createDungeon = (objects: ObjectContainer) => { + let previousRadius = 350; + let previousEnd = vec2.create(); + + for (let i = 0; i < 500000; i += 500) { + const deltaHeight = (Math.random() - 0.5) * 2000; + const height = previousEnd.y + deltaHeight; + const currentEnd = vec2.fromValues(i, height); + const currentToRadius = Math.random() * 300 + 150; + + objects.addObject( + new Tunnel(previousEnd, currentEnd, previousRadius, currentToRadius) + ); + + if (deltaHeight > 0) { + objects.addObject( + new CircleLight( + currentEnd, + Math.random() * 20 + 30, + vec3.scale( + vec3.create(), + vec3.normalize(vec3.create(), vec3.random(vec3.create())), + Math.random() * 0.5 + 0.5 + ) + ) + ); + } + + previousEnd = currentEnd; + previousRadius = currentToRadius; + } +}; diff --git a/frontend/src/shaders/cave-distance-fs.glsl b/frontend/src/shaders/cave-distance-fs.glsl index 002f3e7..4718efb 100644 --- a/frontend/src/shaders/cave-distance-fs.glsl +++ b/frontend/src/shaders/cave-distance-fs.glsl @@ -1,12 +1,10 @@ #version 300 es -precision lowp float; +precision mediump float; - -#define INFINITY 10000.0 +#define INFINITY 200.0 #define LINE_COUNT 50 - uniform vec2[LINE_COUNT * 2] lines; uniform float[LINE_COUNT * 2] radii; @@ -18,8 +16,9 @@ float lineDistance( in float radiusTo ) { vec2 pa = target - start, ba = end - start; - float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); - return distance(pa, ba * h) - mix(radiusFrom, radiusTo, h); + float baLength = length(ba); + float h = clamp(dot(pa / baLength, ba / baLength), 0.0, 1.0); + return length(pa - ba * h) - mix(radiusFrom, radiusTo, h); } float getDistance(in vec2 target) { diff --git a/frontend/src/shaders/lights-shading-fs.glsl b/frontend/src/shaders/lights-shading-fs.glsl index a59ccb9..e282508 100644 --- a/frontend/src/shaders/lights-shading-fs.glsl +++ b/frontend/src/shaders/lights-shading-fs.glsl @@ -3,20 +3,18 @@ precision mediump float; #define INFINITY 10000.0 -#define LIGHT_COUNT 3 +#define LIGHT_COUNT 5 #define AMBIENT_LIGHT vec3(0.05) #define LIGHT_DROP 800.0 #define SHADOW_BIAS 0.01 -struct Light { +uniform struct Light { vec2 center; float radius; vec3 value; }[LIGHT_COUNT] lights; uniform sampler2D distanceTexture; -uniform mat3 worldToDistanceUV; -uniform vec2 cursorPosition; uniform vec2 viewBoxSize; float square(in float a) { @@ -24,26 +22,13 @@ float square(in float a) { } float getDistance(in vec2 target, out vec3 color) { - // should avoid this matrix multiplication - vec2 targetUV = (vec3(target.xy, 1.0) * worldToDistanceUV).xy; - - vec4 values = texture(distanceTexture, targetUV); + vec4 values = texture(distanceTexture, target); color = values.rgb; return values.w * 32.0; } float getDistance(in vec2 target) { - // should avoid this matrix multiplication - vec2 targetUV = (vec3(target.xy, 1.0) * worldToDistanceUV).xy; - - vec4 values = texture(distanceTexture, targetUV); - return values.w * 32.0; -} - -void createWorld() { - //lights[0] = Light(vec2(600, 700), 40.5, normalize(vec3(1.0)) * 2.0); - //lights[1] = Light(vec2(100.0, 350.0), 52.5, normalize(vec3(2.0, 1.0, 0.25)) * 0.5); - lights[2] = Light(cursorPosition, 52.5, normalize(vec3(0.93, 0.25, 0.5)) * 1.0); + return texture(distanceTexture, target).w * 32.0; } float getFractionOfLightArriving( @@ -53,23 +38,25 @@ float getFractionOfLightArriving( in float lightDistance, in float lightRadius ) { - float q = INFINITY; - float rayLength = 0.0; - + float q = 1.0; + float rayLength = startingDistance; float movingAverageMeanDistance = startingDistance; + direction /= viewBoxSize; + for (int j = 0; j < 64; j++) { float minDistance = getDistance(target + direction * rayLength); movingAverageMeanDistance = movingAverageMeanDistance / 2.0 + minDistance / 2.0; q = min(q, movingAverageMeanDistance / rayLength); - rayLength = min(lightDistance, rayLength + max(0.0001, minDistance)); + rayLength = min(lightDistance, rayLength + max(1.0, minDistance)); } + return smoothstep(0.0, 1.0, (q - SHADOW_BIAS) * (lightDistance + lightRadius) / lightRadius); } vec3 getPixelColor(in vec2 worldCoordinates, in vec2 uvCoordinates) { vec3 colorAtPosition; - float startingDistance = getDistance(worldCoordinates, colorAtPosition); + float startingDistance = getDistance(uvCoordinates, colorAtPosition); vec3 result = colorAtPosition * AMBIENT_LIGHT; @@ -81,7 +68,7 @@ vec3 getPixelColor(in vec2 worldCoordinates, in vec2 uvCoordinates) { vec2 lightDirection = normalize(light.center - worldCoordinates); float fractionOfLightArriving = getFractionOfLightArriving( - worldCoordinates, lightDirection, startingDistance, + uvCoordinates, lightDirection, startingDistance, max(0.0, lightDistance), light.radius ); @@ -91,13 +78,10 @@ vec3 getPixelColor(in vec2 worldCoordinates, in vec2 uvCoordinates) { return clamp(result, 0.0, 1.0); } - in vec2 worldCoordinates; in vec2 uvCoordinates; out vec4 fragmentColor; void main() { - createWorld(); - fragmentColor = vec4(getPixelColor(worldCoordinates, uvCoordinates), 1.0); }