diff --git a/frontend/src/scripts/drawing/drawables/drawable-blob.ts b/frontend/src/scripts/drawing/drawables/drawable-blob.ts new file mode 100644 index 0000000..f27ea0b --- /dev/null +++ b/frontend/src/scripts/drawing/drawables/drawable-blob.ts @@ -0,0 +1,26 @@ +import { IDrawable } from './i-drawable'; +import { IDrawableDescriptor } from './i-drawable-descriptor'; +import { settings } from '../settings'; +import { Blob } from '../../shapes/types/blob'; + +export class DrawableBlob extends Blob implements IDrawable { + public static descriptor: IDrawableDescriptor = { + uniformName: 'blobs', + countMacroName: 'blobCount', + shaderCombinationSteps: settings.shaderCombinations.blobSteps, + }; + + public serializeToUniforms(uniforms: any): void { + const uniformName = DrawableBlob.descriptor.uniformName; + if (!uniforms.hasOwnProperty(uniformName)) { + uniforms[uniformName] = []; + } + + uniforms[uniformName].push({ + headCenter: this.head.center, + torsoCenter: this.torso.center, + leftFootCenter: this.leftFoot.center, + rightFootCenter: this.rightFoot.center, + }); + } +} diff --git a/frontend/src/scripts/drawing/graphics-library/frame-buffer/intermediate-frame-buffer.ts b/frontend/src/scripts/drawing/graphics-library/frame-buffer/intermediate-frame-buffer.ts index f9c2518..f8d10bc 100644 --- a/frontend/src/scripts/drawing/graphics-library/frame-buffer/intermediate-frame-buffer.ts +++ b/frontend/src/scripts/drawing/graphics-library/frame-buffer/intermediate-frame-buffer.ts @@ -1,11 +1,22 @@ import { FrameBuffer } from './frame-buffer'; +import { enableExtension } from '../helper/enable-extension'; export class IntermediateFrameBuffer extends FrameBuffer { private frameTexture: WebGLTexture; + private floatLinearEnabled = true; + constructor(gl: WebGL2RenderingContext) { super(gl); + enableExtension(gl, 'EXT_color_buffer_float'); + + try { + enableExtension(gl, 'OES_texture_float_linear'); + } catch { + this.floatLinearEnabled = false; + } + this.frameTexture = this.gl.createTexture(); this.configureTexture(); @@ -27,12 +38,12 @@ export class IntermediateFrameBuffer extends FrameBuffer { this.gl.texImage2D( this.gl.TEXTURE_2D, 0, - this.gl.RGBA, + this.gl.RG16F, this.size.x, this.size.y, 0, - this.gl.RGBA, - this.gl.UNSIGNED_BYTE, + this.gl.RG, + this.gl.FLOAT, null ); } @@ -42,12 +53,12 @@ export class IntermediateFrameBuffer extends FrameBuffer { this.gl.texParameteri( this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, - this.gl.LINEAR + this.floatLinearEnabled ? this.gl.LINEAR : this.gl.NEAREST ); this.gl.texParameteri( this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, - this.gl.LINEAR + this.floatLinearEnabled ? this.gl.LINEAR : this.gl.NEAREST ); this.gl.texParameteri( this.gl.TEXTURE_2D, diff --git a/frontend/src/scripts/drawing/graphics-library/helper/enable-extension.ts b/frontend/src/scripts/drawing/graphics-library/helper/enable-extension.ts new file mode 100644 index 0000000..0425e8e --- /dev/null +++ b/frontend/src/scripts/drawing/graphics-library/helper/enable-extension.ts @@ -0,0 +1,10 @@ +export const enableExtension = ( + gl: WebGL2RenderingContext, + name: string +): any => { + if (gl.getSupportedExtensions().indexOf(name) == -1) { + throw new Error(`Unsupported extension ${name}`); + } + + return gl.getExtension(name); +}; diff --git a/frontend/src/scripts/drawing/graphics-library/helper/stopwatch.ts b/frontend/src/scripts/drawing/graphics-library/helper/stopwatch.ts index 7eb3d10..cae7874 100644 --- a/frontend/src/scripts/drawing/graphics-library/helper/stopwatch.ts +++ b/frontend/src/scripts/drawing/graphics-library/helper/stopwatch.ts @@ -1,4 +1,5 @@ import { InfoText } from '../../../objects/types/info-text'; +import { enableExtension } from './enable-extension'; // https://www.khronos.org/registry/webgl/extensions/EXT_disjoint_timer_query_webgl2/ @@ -10,15 +11,8 @@ export class WebGlStopwatch { 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( + this.timerExtension = enableExtension( + gl, 'EXT_disjoint_timer_query_webgl2' ); } diff --git a/frontend/src/scripts/drawing/graphics-library/program/uniform-array-autoscaling-program.ts b/frontend/src/scripts/drawing/graphics-library/program/uniform-array-autoscaling-program.ts index 9b7a8dc..5e63510 100644 --- a/frontend/src/scripts/drawing/graphics-library/program/uniform-array-autoscaling-program.ts +++ b/frontend/src/scripts/drawing/graphics-library/program/uniform-array-autoscaling-program.ts @@ -34,7 +34,7 @@ export class UniformArrayAutoScalingProgram implements IProgram { ); const closest = this.programs.find((p) => - p.values.every((v, i) => v > values[i]) + p.values.every((v, i) => v >= values[i]) ); this.current = closest ? closest.program : last(this.programs).program; diff --git a/frontend/src/scripts/drawing/rendering/webgl2-renderer.ts b/frontend/src/scripts/drawing/rendering/webgl2-renderer.ts index 7f0fd0d..269727c 100644 --- a/frontend/src/scripts/drawing/rendering/webgl2-renderer.ts +++ b/frontend/src/scripts/drawing/rendering/webgl2-renderer.ts @@ -16,6 +16,8 @@ import { FpsAutoscaler } from './fps-autoscaler'; import { RenderingPass } from './rendering-pass'; import { IDrawable } from '../drawables/i-drawable'; import { DrawableTunnel } from '../drawables/drawable-tunnel'; +import { enableExtension } from '../graphics-library/helper/enable-extension'; +import { DrawableBlob } from '../drawables/drawable-blob'; export class WebGl2Renderer implements IRenderer { private gl: WebGL2RenderingContext; @@ -50,7 +52,7 @@ export class WebGl2Renderer implements IRenderer { this.distancePass = new RenderingPass( this.gl, [caveVertexShader, caveFragmentShader], - [DrawableTunnel.descriptor], + [DrawableTunnel.descriptor, DrawableBlob.descriptor], this.distanceFieldFrameBuffer ); diff --git a/frontend/src/scripts/drawing/settings.ts b/frontend/src/scripts/drawing/settings.ts index 31c11ed..c6d89a2 100644 --- a/frontend/src/scripts/drawing/settings.ts +++ b/frontend/src/scripts/drawing/settings.ts @@ -8,10 +8,11 @@ export const settings = { [0.2, 0.1], [0.6, 0.1], [1, 0.3], - [1.25, 0.75], + /*[1.25, 0.75], [1.5, 1], [1.75, 1.25], [1.75, 1.75], + [2, 2],*/ ], startingTargetIndex: 2, scalingOptions: { @@ -21,13 +22,15 @@ export const settings = { }, tileMultiplier: 5, shaderMacros: { - distanceScale: 64, - distanceOffset: 0.0, edgeSmoothing: 10, + headRadius: 5, + torsoRadius: 10, + footRadius: 4, }, shaderCombinations: { - lineSteps: [0, 1, 2, 3, 4, 8, 16, 128], - circleLightSteps: [0, 1, 2, 3], + lineSteps: [0, 1, 2, 4, 8, 16, 128], + blobSteps: [0, 1, 2, 8], + circleLightSteps: [0, 1], pointLightSteps: [0, 1, 2, 3], }, }; diff --git a/frontend/src/scripts/drawing/shaders/cave-distance-fs.glsl b/frontend/src/scripts/drawing/shaders/cave-distance-fs.glsl index 7d464cc..24fabe0 100644 --- a/frontend/src/scripts/drawing/shaders/cave-distance-fs.glsl +++ b/frontend/src/scripts/drawing/shaders/cave-distance-fs.glsl @@ -3,10 +3,7 @@ precision mediump float; #define LINE_COUNT {lineCount} -#define CAVE_COLOR vec3(0.36, 0.38, 0.76) -#define AIR_COLOR vec3(0.7) -#define DISTANCE_SCALE {distanceScale} -#define DISTANCE_OFFSET {distanceOffset} +#define BLOB_COUNT {blobCount} uniform float maxMinDistance; @@ -17,32 +14,76 @@ uniform float maxMinDistance; float fromRadius; float toRadius; }[LINE_COUNT] lines; -#endif -in vec2 worldCoordinates; -out vec4 fragmentColor; - -void main() { - float realDistance = 0.0; - - #if LINE_COUNT > 0 - float minDistance = maxMinDistance; + void lineMinDistance(vec2 worldCoordinates, inout float minDistance) { + minDistance = maxMinDistance; for (int i = 0; i < LINE_COUNT; i++) { vec2 targetFromDelta = worldCoordinates - lines[i].from; vec2 toFromDelta = lines[i].toFromDelta; - float h = clamp(dot(targetFromDelta, toFromDelta) / dot(toFromDelta, toFromDelta), 0.0, 1.0); - float lineDistance = distance(targetFromDelta, toFromDelta * h) - mix(lines[i].fromRadius, lines[i].toRadius, h); + + float h = clamp( + dot(targetFromDelta, toFromDelta) / dot(toFromDelta, toFromDelta), + 0.0, 1.0 + ); + + float lineDistance = ( + distance(targetFromDelta, toFromDelta * h) + - mix(lines[i].fromRadius, lines[i].toRadius, h) + ); minDistance = min(minDistance, lineDistance); } - realDistance = -minDistance; + minDistance *= -1.0; + } +#endif + +#if BLOB_COUNT > 0 + #define headRadius {headRadius} + #define torsoRadius {torsoRadius} + #define footRadius {footRadius} + + uniform struct Blob { + vec2 headCenter; + vec2 torsoCenter; + vec2 leftFootCenter; + vec2 rightFootCenter; + }[BLOB_COUNT] blobs; + + float circleMinDistance(vec2 worldCoordinates, vec2 circleCenter, float radius) { + return distance(worldCoordinates, circleCenter) - radius; + } + + void blobMinDistance(vec2 worldCoordinates, inout float minDistance) { + float k = 1.0; + + for (int i = 0; i < BLOB_COUNT; i++) { + float res = exp2(-k * circleMinDistance(worldCoordinates, blobs[i].headCenter, headRadius)); + res += exp2(-k * circleMinDistance(worldCoordinates, blobs[i].torsoCenter, torsoRadius)); + res += exp2(-k * circleMinDistance(worldCoordinates, blobs[i].leftFootCenter, footRadius)); + res += exp2(-k * circleMinDistance(worldCoordinates, blobs[i].rightFootCenter, footRadius)); + + minDistance = min(minDistance, -log2(res) / k); + } + } +#endif + + + +in vec2 worldCoordinates; +out vec2 fragmentColor; + +void main() { + float minDistance = -maxMinDistance; + + #if LINE_COUNT > 0 + lineMinDistance(worldCoordinates, minDistance); #endif - fragmentColor = vec4( - mix(CAVE_COLOR, AIR_COLOR, clamp(realDistance, 0.0, 1.0)), - (realDistance + DISTANCE_OFFSET) / DISTANCE_SCALE + #if BLOB_COUNT > 0 + blobMinDistance(worldCoordinates, minDistance); + #endif - ); + fragmentColor = vec2(minDistance, 0.0); } diff --git a/frontend/src/scripts/drawing/shaders/lights-shading-fs.glsl b/frontend/src/scripts/drawing/shaders/lights-shading-fs.glsl index 68860a9..c4d8cfa 100644 --- a/frontend/src/scripts/drawing/shaders/lights-shading-fs.glsl +++ b/frontend/src/scripts/drawing/shaders/lights-shading-fs.glsl @@ -8,21 +8,26 @@ precision mediump float; #define CIRCLE_LIGHT_COUNT {circleLightCount} #define POINT_LIGHT_COUNT {pointLightCount} -#define DISTANCE_SCALE {distanceScale} -#define DISTANCE_OFFSET {distanceOffset} #define EDGE_SMOOTHING {edgeSmoothing} uniform sampler2D distanceTexture; uniform vec2 viewBoxSize; +vec3[4] colors = vec3[4]( + vec3(0.5), + vec3(1.0, 0.0, 0.0), + 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 = values.rgb; - return (values.w - DISTANCE_OFFSET) * DISTANCE_SCALE; + color = colors[int(values[1])]; + return values[0]; } float getDistance(in vec2 target) { - return (texture(distanceTexture, target).w - DISTANCE_OFFSET) * DISTANCE_SCALE; + return texture(distanceTexture, target)[0]; } #if CIRCLE_LIGHT_COUNT > 0 @@ -58,10 +63,6 @@ void main() { for (int i = 0; i < CIRCLE_LIGHT_COUNT; i++) { float lightCenterDistance = distance(circleLights[i].center, worldCoordinates); - /*if (lightCenterDistance < circleLights[i].radius) { - fragmentColor = vec4(1.0, 1.0, 0.0, 1.0); - return; - }*/ vec3 lightColorAtPosition = circleLights[i].value / pow( lightCenterDistance / LIGHT_DROP + 1.0, 2.0 ); @@ -76,7 +77,7 @@ void main() { rayLength = lightCenterDistance - circleLights[i].radius; float minDistance = getDistance(uvCoordinates + direction * rayLength); exponentialDecayDistance = (exponentialDecayDistance + minDistance) / 2.0; - q = min(q, exponentialDecayDistance / rayLength); + q = min(q, minDistance / rayLength); lighting += lightColorAtPosition * clamp( q / circleLights[i].radius * (lightCenterDistance + 1.0), 0.0, 1.0 @@ -122,5 +123,5 @@ void main() { } #endif - fragmentColor = vec4(colorAtPosition * lighting * clamp(startingDistance, 0.0, 1.0), 1.0); + fragmentColor = vec4(colorAtPosition * lighting * step(0.0, startingDistance), 1.0); } diff --git a/frontend/src/scripts/objects/types/camera.ts b/frontend/src/scripts/objects/types/camera.ts index 418cca0..802d62b 100644 --- a/frontend/src/scripts/objects/types/camera.ts +++ b/frontend/src/scripts/objects/types/camera.ts @@ -13,11 +13,10 @@ export class Camera extends GameObject { private cursorPosition = vec2.create(); private boundingBox: BoundingBox; - constructor(physics: Physics, private light: Lamp) { + constructor() { super(); this.boundingBox = new BoundingBox(null); - //physics.addDynamicBoundingBox(this.boundingBox); this.addCommandExecutor(BeforeRenderCommand, this.draw.bind(this)); this.addCommandExecutor(MoveToCommand, this.moveTo.bind(this)); @@ -40,7 +39,6 @@ export class Camera extends GameObject { private moveTo(c: MoveToCommand) { this.boundingBox.topLeft = c.position; - this.light.sendCommand(c); } private zoom(c: ZoomCommand) { diff --git a/frontend/src/scripts/objects/types/character.ts b/frontend/src/scripts/objects/types/character.ts index 9327ad5..5a81fb0 100644 --- a/frontend/src/scripts/objects/types/character.ts +++ b/frontend/src/scripts/objects/types/character.ts @@ -1,5 +1,4 @@ import { vec2 } from 'gl-matrix'; -import { Circle } from '../../shapes/types/circle'; import { KeyDownCommand } from '../../input/commands/key-down'; import { KeyUpCommand } from '../../input/commands/key-up'; import { SwipeCommand } from '../../input/commands/swipe'; @@ -10,22 +9,28 @@ import { Physics } from '../../physics/physics'; import { GameObject } from '../game-object'; import { Camera } from './camera'; import { IShape } from '../../shapes/i-shape'; -import { rotate90Deg } from '../../helper/rotate-90-deg'; -import { max } from 'gl-matrix/src/gl-matrix/vec2'; +import { Blob } from '../../shapes/types/blob'; +import { RenderCommand } from '../../drawing/commands/render'; +import { DrawableBlob } from '../../drawing/drawables/drawable-blob'; +import { Lamp } from './lamp'; export class Character extends GameObject { private keysDown: Set = new Set(); - private shape: Circle; + private shape: DrawableBlob; private static speed = 1.5; - constructor(private physics: Physics, private camera: Camera) { + constructor( + private physics: Physics, + private camera: Camera, + private light: Lamp + ) { super(); - this.shape = new Circle(); - this.shape.radius = 80; + this.shape = new DrawableBlob(vec2.create()); this.addCommandExecutor(StepCommand, this.stepHandler.bind(this)); + this.addCommandExecutor(RenderCommand, this.draw.bind(this)); this.addCommandExecutor(TeleportToCommand, (c) => this.setPosition(c.position) ); @@ -38,6 +43,10 @@ export class Character extends GameObject { }); } + private draw(c: RenderCommand) { + c.renderer.drawShape(this.shape); + } + private tryMoving(delta: vec2, isFirstIteration = true) { const maxStep = 2; if (vec2.length(delta) > maxStep) { @@ -95,7 +104,7 @@ export class Character extends GameObject { } private getNearShapesTo( - shape: Circle + shape: Blob ): Array<{ shape: IShape; distance: number }> { return this.physics .findIntersecting(shape.boundingBox) @@ -108,8 +117,10 @@ export class Character extends GameObject { } private setPosition(value: vec2) { - this.shape.center = value; - this.camera.sendCommand(new MoveToCommand(this.shape.center)); + this.shape.position = value; + this.camera.sendCommand(new MoveToCommand(value)); + vec2.add(value, value, vec2.fromValues(80, 0)); + this.light.sendCommand(new MoveToCommand(value)); } public stepHandler(c: StepCommand) { diff --git a/frontend/src/scripts/objects/world/create-character.ts b/frontend/src/scripts/objects/world/create-character.ts index 8f0e1e0..56b32b1 100644 --- a/frontend/src/scripts/objects/world/create-character.ts +++ b/frontend/src/scripts/objects/world/create-character.ts @@ -17,8 +17,8 @@ export const createCharacter = ( 2 ); - const camera = new Camera(physics, light); - const character = new Character(physics, camera); + const camera = new Camera(); + const character = new Character(physics, camera, light); objects.addObject(light); objects.addObject(camera); objects.addObject(character); diff --git a/frontend/src/scripts/shapes/types/blob.ts b/frontend/src/scripts/shapes/types/blob.ts new file mode 100644 index 0000000..55c7969 --- /dev/null +++ b/frontend/src/scripts/shapes/types/blob.ts @@ -0,0 +1,73 @@ +import { vec2 } from 'gl-matrix'; +import { IShape } from '../i-shape'; +import { BoundingBox } from '../bounding-box'; +import { Circle } from './circle'; +import { settings } from '../../drawing/settings'; + +export class Blob implements IShape { + private static readonly boundingCircleRadius = 19; + private static readonly headOffset = vec2.fromValues(0, 15); + private static readonly torsoOffset = vec2.fromValues(0, 0); + private static readonly leftFootOffset = vec2.fromValues(-5, -10); + private static readonly rightFootOffset = vec2.fromValues(5, -10); + + public readonly isInverted = false; + + protected boundingCircle = new Circle( + vec2.create(), + Blob.boundingCircleRadius + ); + protected head = new Circle(vec2.create(), settings.shaderMacros.headRadius); + protected torso = new Circle( + vec2.create(), + settings.shaderMacros.torsoRadius + ); + protected leftFoot = new Circle( + vec2.create(), + settings.shaderMacros.footRadius + ); + protected rightFoot = new Circle( + vec2.create(), + settings.shaderMacros.footRadius + ); + + public constructor(center: vec2) { + this.position = center; + } + + public set position(value: vec2) { + vec2.copy(this.boundingCircle.center, value); + vec2.add(this.head.center, value, Blob.headOffset); + vec2.add(this.torso.center, value, Blob.torsoOffset); + vec2.add(this.leftFoot.center, value, Blob.leftFootOffset); + vec2.add(this.rightFoot.center, value, Blob.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); + } +}