Refactor rendering

This commit is contained in:
schmelczerandras 2020-07-30 13:28:10 +02:00
parent 9b47d56d8f
commit c892ca2d01
38 changed files with 511 additions and 429 deletions

View file

@ -13,11 +13,12 @@ A good-looking 2D adventure.
## Todo ## Todo
- host script
- https://github.com/ottomatica/slim
- rancher os
- Frontend nginx disable logging - Frontend nginx disable logging
- procedural piano - procedural piano
- subtract cameraPosition
- cross browser testing
- nginx log monitor
- pass befejezése
- lightweight object storage - lightweight object storage
- local dev env - local dev env
- CI pipeline: - CI pipeline:

View file

@ -1,5 +1,5 @@
import { Command } from '../command'; import { Command } from '../command';
import { IRenderer } from '../../drawing/rendering/i-renderer'; import { IRenderer } from '../../drawing/i-renderer';
export class BeforeRenderCommand extends Command { export class BeforeRenderCommand extends Command {
public constructor(public readonly renderer?: IRenderer) { public constructor(public readonly renderer?: IRenderer) {

View file

@ -1,5 +1,5 @@
import { Command } from '../command'; import { Command } from '../command';
import { IRenderer } from '../../drawing/rendering/i-renderer'; import { IRenderer } from '../../drawing/i-renderer';
export class RenderCommand extends Command { export class RenderCommand extends Command {
public constructor(public readonly renderer?: IRenderer) { public constructor(public readonly renderer?: IRenderer) {

View file

@ -0,0 +1,5 @@
export interface IDrawableDescriptor {
uniformName: string;
countMacroName: string;
shaderCombinationSteps: Array<number>;
}

View file

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

View file

@ -1,8 +1,14 @@
import { ILight } from './i-light'; import { ILight } from './i-light';
import { vec2, vec3 } from 'gl-matrix'; import { vec2, vec3 } from 'gl-matrix';
import { settings } from '../../settings';
import { IDrawableDescriptor } from '../i-drawable-descriptor';
export class CircleLight implements ILight { export class CircleLight implements ILight {
public static uniformName = 'lights'; public static descriptor: IDrawableDescriptor = {
uniformName: 'lights',
countMacroName: 'lightCount',
shaderCombinationSteps: settings.shaderCombinations.circleLightSteps,
};
constructor( constructor(
public center: vec2, public center: vec2,
@ -11,8 +17,16 @@ export class CircleLight implements ILight {
public lightness: number public lightness: number
) {} ) {}
distance(target: vec2): number {
return 0;
}
minimumDistance(target: vec2): number {
return 0;
}
serializeToUniforms(uniforms: any): void { serializeToUniforms(uniforms: any): void {
const listName = CircleLight.uniformName; const listName = CircleLight.descriptor.uniformName;
if (!uniforms.hasOwnProperty(listName)) { if (!uniforms.hasOwnProperty(listName)) {
uniforms[listName] = []; uniforms[listName] = [];

View file

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

View file

@ -9,6 +9,12 @@ export class PointLight implements ILight {
public color: vec3, public color: vec3,
public lightness: number public lightness: number
) {} ) {}
distance(target: vec2): number {
throw new Error('Method not implemented.');
}
minimumDistance(target: vec2): number {
throw new Error('Method not implemented.');
}
serializeToUniforms(uniforms: any): void { serializeToUniforms(uniforms: any): void {
const listName = PointLight.uniformName; const listName = PointLight.uniformName;

View file

@ -0,0 +1,3 @@
import { IDrawable } from '../i-drawable';
export interface IPrimitive extends IDrawable {}

View file

@ -1,12 +1,17 @@
import { vec2 } from 'gl-matrix'; import { vec2 } from 'gl-matrix';
import { clamp01 } from '../../helper/clamp'; import { clamp01 } from '../../../helper/clamp';
import { mix } from '../../helper/mix'; import { mix } from '../../../helper/mix';
import { Circle } from './circle'; import { Circle } from './circle';
import { IPrimitive } from './i-primitive'; import { IPrimitive } from './i-primitive';
import { IDrawableDescriptor } from '../i-drawable-descriptor';
import { settings } from '../../settings';
export class TunnelShape implements IPrimitive { export class TunnelShape implements IPrimitive {
public static uniformNameLines = 'lines'; public static descriptor: IDrawableDescriptor = {
public static uniformNameRadii = 'radii'; uniformName: 'lines',
countMacroName: 'lineCount',
shaderCombinationSteps: settings.shaderCombinations.lineSteps,
};
public readonly toFromDelta: vec2; public readonly toFromDelta: vec2;
private toFromDeltaLength: number; private toFromDeltaLength: number;
@ -29,18 +34,16 @@ export class TunnelShape implements IPrimitive {
} }
serializeToUniforms(uniforms: any): void { serializeToUniforms(uniforms: any): void {
if (!uniforms.hasOwnProperty(TunnelShape.uniformNameLines)) { if (!uniforms.hasOwnProperty(TunnelShape.descriptor.uniformName)) {
uniforms[TunnelShape.uniformNameLines] = []; uniforms[TunnelShape.descriptor.uniformName] = [];
} }
if (!uniforms.hasOwnProperty(TunnelShape.uniformNameRadii)) { uniforms[TunnelShape.descriptor.uniformName].push({
uniforms[TunnelShape.uniformNameRadii] = []; from: this.from,
} toFromDelta: this.toFromDelta,
fromRadius: this.fromRadius,
uniforms[TunnelShape.uniformNameLines].push(this.from); toRadius: this.toRadius,
uniforms[TunnelShape.uniformNameLines].push(this.toFromDelta); });
uniforms[TunnelShape.uniformNameRadii].push(this.fromRadius);
uniforms[TunnelShape.uniformNameRadii].push(this.toRadius);
} }
public distance(target: vec2): number { public distance(target: vec2): number {

View file

@ -1,15 +1,16 @@
import { settings } from '../../settings';
export const createShader = ( export const createShader = (
gl: WebGL2RenderingContext, gl: WebGL2RenderingContext,
type: GLenum, type: GLenum,
source: string, source: string,
substitutions: { [name: string]: string } substitutions: { [name: string]: string }
): WebGLShader => { ): WebGLShader => {
const allSubstitutions = { ...settings.shaderMacros, ...substitutions };
source = source.replace(/{(.+)}/gm, (_, name: string): string => { source = source.replace(/{(.+)}/gm, (_, name: string): string => {
const value = substitutions[name]; const value = allSubstitutions[name];
if (Number.isInteger(value)) { return Number.isInteger(value) ? `${value}.0` : value;
return `${value}.0`;
}
return value;
}); });
const shader = gl.createShader(type); const shader = gl.createShader(type);

View file

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

View file

@ -5,11 +5,10 @@ export class FragmentShaderOnlyProgram extends Program {
constructor( constructor(
gl: WebGL2RenderingContext, gl: WebGL2RenderingContext,
vertexShaderSource: string, shaderSources: [string, string],
fragmentShaderSource: string,
substitutions: { [name: string]: string } substitutions: { [name: string]: string }
) { ) {
super(gl, vertexShaderSource, fragmentShaderSource, substitutions); super(gl, shaderSources, substitutions);
this.prepareScreenQuad('vertexPosition'); this.prepareScreenQuad('vertexPosition');
} }

View file

@ -1,8 +1,8 @@
import { vec2 } from 'gl-matrix'; import { vec2 } from 'gl-matrix';
export interface IProgram { export interface IProgram {
bindAndSetUniforms(values: { [name: string]: any }): void;
setDrawingRectangle(topLeft: vec2, size: vec2): void; setDrawingRectangle(topLeft: vec2, size: vec2): void;
bindAndSetUniforms(values: { [name: string]: any }): void;
draw(): void; draw(): void;
delete(): void; delete(): void;
} }

View file

@ -17,8 +17,7 @@ export abstract class Program implements IProgram {
constructor( constructor(
protected gl: WebGL2RenderingContext, protected gl: WebGL2RenderingContext,
vertexShaderSource: string, [vertexShaderSource, fragmentShaderSource]: [string, string],
fragmentShaderSource: string,
substitutions: { [name: string]: string } substitutions: { [name: string]: string }
) { ) {
this.createProgram(vertexShaderSource, fragmentShaderSource, substitutions); this.createProgram(vertexShaderSource, fragmentShaderSource, substitutions);

View file

@ -1,11 +1,14 @@
import { vec2 } from 'gl-matrix'; import { vec2 } from 'gl-matrix';
import { FragmentShaderOnlyProgram } from './fragment-shader-only-program'; import { FragmentShaderOnlyProgram } from './fragment-shader-only-program';
import { IProgram } from './i-program'; import { IProgram } from './i-program';
import { last } from '../../../helper/last';
import { getCombinations } from '../../../helper/get-combinations';
import { IDrawableDescriptor } from '../../drawables/i-drawable-descriptor';
export class UniformArrayAutoScalingProgram implements IProgram { export class UniformArrayAutoScalingProgram implements IProgram {
private programs: Array<{ private programs: Array<{
program: FragmentShaderOnlyProgram; program: FragmentShaderOnlyProgram;
value: number; values: Array<number>;
}> = []; }> = [];
private current: FragmentShaderOnlyProgram; private current: FragmentShaderOnlyProgram;
@ -14,45 +17,33 @@ export class UniformArrayAutoScalingProgram implements IProgram {
constructor( constructor(
private gl: WebGL2RenderingContext, private gl: WebGL2RenderingContext,
private vertexShaderSource: string, shaderSources: [string, string],
private fragmentShaderSource: string, private options: Array<IDrawableDescriptor>
private substitutions: { [name: string]: any },
private options: {
getValueFromUniforms: (values: { [name: string]: any }) => number;
uniformArraySizeName: string;
enablingMacro: string;
startingValue: number;
steps: number;
maximumValue: number;
}
) { ) {
for ( const names = options.map((o) => o.countMacroName);
let i = options.startingValue; for (let combination of getCombinations(
i < options.maximumValue; options.map((o) => o.shaderCombinationSteps)
i += options.steps )) {
) { this.createProgram(names, combination, shaderSources);
this.createProgram(i);
} }
} }
public bindAndSetUniforms(values: { [name: string]: any }): void { public bindAndSetUniforms(uniforms: { [name: string]: any }): void {
let value = this.options.getValueFromUniforms(values); let values = this.options.map((o) =>
value = Math.min(this.options.maximumValue, value); uniforms[o.uniformName] ? uniforms[o.uniformName].length : 0
const closest = this.programs.find(
(p) => value < p.value && value + this.options.steps >= p.value
); );
if (closest) {
this.current = closest.program; const closest = this.programs.find((p) =>
} else { p.values.every((v, i) => v > values[i])
this.current = this.createProgram(value + this.options.steps); );
}
this.current = closest ? closest.program : last(this.programs).program;
this.current.setDrawingRectangle( this.current.setDrawingRectangle(
this.drawingRectangleTopLeft, this.drawingRectangleTopLeft,
this.drawingRectangleSize this.drawingRectangleSize
); );
this.current.bindAndSetUniforms(values); this.current.bindAndSetUniforms(uniforms);
} }
public setDrawingRectangle(topLeft: vec2, size: vec2) { public setDrawingRectangle(topLeft: vec2, size: vec2) {
@ -68,21 +59,23 @@ export class UniformArrayAutoScalingProgram implements IProgram {
this.programs.forEach((p) => p.program.delete()); this.programs.forEach((p) => p.program.delete());
} }
private createProgram(arraySize: number): FragmentShaderOnlyProgram { 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( const program = new FragmentShaderOnlyProgram(
this.gl, this.gl,
this.vertexShaderSource, shaderSources,
this.fragmentShaderSource, substitutions
{
[this.options.uniformArraySizeName]: Math.floor(arraySize).toString(),
[this.options.enablingMacro]: arraySize > 0 ? '1' : '0',
...this.substitutions,
}
); );
this.programs.push({ this.programs.push({
program, program,
value: arraySize, values: combination,
}); });
return program; return program;

View file

@ -1,6 +1,6 @@
import { vec2 } from 'gl-matrix'; import { vec2 } from 'gl-matrix';
import { IPrimitive } from '../primitives/i-primitive'; import { IPrimitive } from './drawables/primitives/i-primitive';
import { ILight } from '../lights/i-light'; import { ILight } from './drawables/lights/i-light';
export interface IRenderer { export interface IRenderer {
startFrame(deltaTime: DOMHighResTimeStamp): void; startFrame(deltaTime: DOMHighResTimeStamp): void;

View file

@ -1,3 +0,0 @@
export interface ILight {
serializeToUniforms(uniforms: any): void;
}

View file

@ -0,0 +1,54 @@
import { Autoscaler } from '../../helper/autoscaler';
import { settings } from '../settings';
import { exponentialDecay } from '../../helper/exponential-decay';
import { InfoText } from '../../objects/types/info-text';
import { FrameBuffer } from '../graphics-library/frame-buffer/frame-buffer';
import { toPercent } from '../../helper/to-percent';
export class FpsAutoscaler extends Autoscaler {
private timeSinceLastAdjusment = 0;
private exponentialDecayedDeltaTime = 0.0;
constructor(private frameBuffers: Array<FrameBuffer>) {
super(
frameBuffers.map((f) => (v) => (f.renderScale = v)),
settings.qualityScaling.scaleTargets,
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();
}
}
InfoText.modifyRecord(
'quality',
this.frameBuffers.map((f) => toPercent(f.renderScale)).join(', ')
);
}
}

View file

@ -1,8 +1,105 @@
import { IProgram } from '../graphics-library/program/i-program';
import { FrameBuffer } from '../graphics-library/frame-buffer/frame-buffer'; import { FrameBuffer } from '../graphics-library/frame-buffer/frame-buffer';
import { IDrawable } from '../drawables/i-drawable';
import { settings } from '../settings';
import { vec2 } from 'gl-matrix';
import { Circle } from '../drawables/primitives/circle';
import { InfoText } from '../../objects/types/info-text';
import { UniformArrayAutoScalingProgram } from '../graphics-library/program/uniform-array-autoscaling-program';
import { IDrawableDescriptor } from '../drawables/i-drawable-descriptor';
export class RenderingPass { export class RenderingPass {
constructor(private program: IProgram, private frame: FrameBuffer) {} private drawables: Array<IDrawable> = [];
private program: UniformArrayAutoScalingProgram;
public render(uniforms: any, inputTexture?: WebGLTexture) {} constructor(
gl: WebGL2RenderingContext,
shaderSources: [string, string],
private drawableDescriptors: Array<IDrawableDescriptor>,
private frame: FrameBuffer
) {
this.program = new UniformArrayAutoScalingProgram(
gl,
shaderSources,
drawableDescriptors
);
}
public addDrawable(drawable: IDrawable) {
this.drawables.push(drawable);
}
public render(
commonUniforms: any,
viewCircle: Circle,
inputTexture?: WebGLTexture
) {
this.frame.bindAndClear(inputTexture);
const q = 1 / settings.tileMultiplier;
const tileUvSize = vec2.fromValues(q, q);
const possiblyOnScreenDrawables = this.drawables.filter(
(p) => p.minimumDistance(viewCircle.center) < viewCircle.radius
);
const origin = vec2.transformMat2d(
vec2.create(),
vec2.fromValues(0, 0),
commonUniforms.uvToWorld
);
const firstCenter = vec2.transformMat2d(
vec2.create(),
vec2.fromValues(q, q),
commonUniforms.uvToWorld
);
vec2.subtract(firstCenter, firstCenter, origin);
const worldR = vec2.length(firstCenter);
let sumLineCount = 0;
for (let x = 0; x < 1; x += q) {
for (let y = 0; y < 1; y += q) {
const uniforms = { ...commonUniforms };
uniforms.maxMinDistance = 2 * worldR;
const uvBottomLeft = vec2.fromValues(x, y);
this.program.setDrawingRectangle(uvBottomLeft, tileUvSize);
const tileCenterWorldCoordinates = vec2.transformMat2d(
vec2.create(),
vec2.add(vec2.create(), uvBottomLeft, vec2.fromValues(q, q)),
uniforms.uvToWorld
);
const primitivesNearTile = possiblyOnScreenDrawables.filter(
(p) => p.distance(tileCenterWorldCoordinates) < 2 * worldR
);
sumLineCount += primitivesNearTile.length;
primitivesNearTile.forEach((p) => p.serializeToUniforms(uniforms));
this.program.bindAndSetUniforms(uniforms);
this.program.draw();
}
}
this.drawables = [];
InfoText.modifyRecord(
'nearby ' + this.drawableDescriptors[0].countMacroName,
possiblyOnScreenDrawables.length.toFixed(2)
);
InfoText.modifyRecord(
'drawn ' + this.drawableDescriptors[0].countMacroName,
(
sumLineCount /
settings.tileMultiplier /
settings.tileMultiplier
).toFixed(2)
);
}
} }

View file

@ -1,24 +1,28 @@
import { mat2d, vec2 } from 'gl-matrix'; import { mat2d, vec2 } from 'gl-matrix';
import { InfoText } from '../../objects/types/info-text'; import caveFragmentShader from '../shaders/cave-distance-fs.glsl';
import lightsFragmentShader from '../shaders/lights-shading-fs.glsl';
import caveVertexShader from '../shaders/passthrough-distance-vs.glsl';
import lightsVertexShader from '../shaders/passthrough-shading-vs.glsl';
// import lightsShader from '../shaders/rainbow-shading-fs.glsl';
import { DefaultFrameBuffer } from '../graphics-library/frame-buffer/default-frame-buffer'; import { DefaultFrameBuffer } from '../graphics-library/frame-buffer/default-frame-buffer';
import { IntermediateFrameBuffer } from '../graphics-library/frame-buffer/intermediate-frame-buffer'; import { IntermediateFrameBuffer } from '../graphics-library/frame-buffer/intermediate-frame-buffer';
import { WebGlStopwatch } from '../graphics-library/helper/stopwatch'; import { WebGlStopwatch } from '../graphics-library/helper/stopwatch';
import { IProgram } from '../graphics-library/program/i-program'; import { IProgram } from '../graphics-library/program/i-program';
import { UniformArrayAutoScalingProgram } from '../graphics-library/program/uniform-array-autoscaling-program'; import { UniformArrayAutoScalingProgram } from '../graphics-library/program/uniform-array-autoscaling-program';
import { IRenderer } from './i-renderer'; import { IRenderer } from '../i-renderer';
import { Circle } from '../primitives/circle'; import { Circle } from '../drawables/primitives/circle';
import { IPrimitive } from '../primitives/i-primitive'; import { IPrimitive } from '../drawables/primitives/i-primitive';
import { Rectangle } from '../primitives/rectangle'; import { Rectangle } from '../drawables/primitives/rectangle';
import { TunnelShape } from '../primitives/tunnel-shape'; import { ILight } from '../drawables/lights/i-light';
import { Autoscaler } from './autoscaler'; import { settings } from '../settings';
import { ILight } from '../lights/i-light'; import { FpsAutoscaler } from './fps-autoscaler';
import { toPercent } from '../../helper/to-percent'; import { getWebGl2Context } from '../graphics-library/helper/get-webgl2-context';
import { exponentialDecay } from '../../helper/exponential-decay'; import { RenderingPass } from './rendering-pass';
import { settings } from './settings'; import { TunnelShape } from '../drawables/primitives/tunnel-shape';
import { CircleLight } from '../drawables/lights/circle-light';
export class WebGl2Renderer implements IRenderer { export class WebGl2Renderer implements IRenderer {
private gl: WebGL2RenderingContext; private gl: WebGL2RenderingContext;
private qualityScaler: Autoscaler;
private stopwatch?: WebGlStopwatch; private stopwatch?: WebGlStopwatch;
private viewBox: Rectangle = new Rectangle(); private viewBox: Rectangle = new Rectangle();
@ -28,131 +32,51 @@ export class WebGl2Renderer implements IRenderer {
private distanceFieldFrameBuffer: IntermediateFrameBuffer; private distanceFieldFrameBuffer: IntermediateFrameBuffer;
private lightingFrameBuffer: DefaultFrameBuffer; private lightingFrameBuffer: DefaultFrameBuffer;
private distanceProgram: IProgram; private distancePass: RenderingPass;
private lightingProgram: IProgram; private lightingPass: RenderingPass;
private primitives: Array<IPrimitive>; private autoscaler: FpsAutoscaler;
private lights: Array<ILight>;
constructor( constructor(private canvas: HTMLCanvasElement, private overlay: HTMLElement) {
private canvas: HTMLCanvasElement, this.gl = getWebGl2Context(canvas);
private overlay: HTMLElement,
shaderSources: Array<[string, string]> this.distanceFieldFrameBuffer = new IntermediateFrameBuffer(this.gl);
) { this.lightingFrameBuffer = new DefaultFrameBuffer(this.gl);
this.getContext();
this.createPipeline(shaderSources); this.distancePass = new RenderingPass(
this.setupAutoscaling(); this.gl,
[caveVertexShader, caveFragmentShader],
[TunnelShape.descriptor],
this.distanceFieldFrameBuffer
);
this.lightingPass = new RenderingPass(
this.gl,
[lightsVertexShader, lightsFragmentShader],
[CircleLight.descriptor],
this.lightingFrameBuffer
);
this.autoscaler = new FpsAutoscaler([
this.lightingFrameBuffer,
this.distanceFieldFrameBuffer,
]);
try { try {
this.stopwatch = new WebGlStopwatch(this.gl); this.stopwatch = new WebGlStopwatch(this.gl);
} catch {} } catch {}
} }
private getContext() {
this.gl = this.canvas.getContext('webgl2');
if (!this.gl) {
throw new Error('WebGl2 is not supported');
}
}
private createPipeline(shaderSources: Array<[string, string]>) {
const distanceScale = settings.shaderUniforms.distanceScale;
this.distanceFieldFrameBuffer = new IntermediateFrameBuffer(this.gl);
this.distanceProgram = new UniformArrayAutoScalingProgram(
this.gl,
shaderSources[0][0],
shaderSources[0][1],
{ ...settings.shaderUniforms },
{
getValueFromUniforms: (v) => (v.lines ? v.lines.length / 2 : 0),
uniformArraySizeName: 'lineCount',
startingValue: 0,
enablingMacro: 'linesEnabled',
steps: 1,
maximumValue: 15,
}
);
this.lightingFrameBuffer = new DefaultFrameBuffer(this.gl);
this.lightingProgram = new UniformArrayAutoScalingProgram(
this.gl,
shaderSources[1][0],
shaderSources[1][1],
{ ...settings.shaderUniforms },
{
getValueFromUniforms: (v) => (v.lights ? v.lights.length : 0),
uniformArraySizeName: 'lightCount',
startingValue: 1,
enablingMacro: null,
steps: 1,
maximumValue: 8,
}
);
}
private setupAutoscaling() {
this.qualityScaler = new Autoscaler(
[
(v) => (this.lightingFrameBuffer.renderScale = v),
(v) => (this.distanceFieldFrameBuffer.renderScale = v),
],
settings.qualityScaling.scaleTargets,
settings.qualityScaling.startingTargetIndex,
settings.qualityScaling.scalingOptions
);
}
private timeSinceLastAdjusment = 0;
private exponentialDecayedDeltaTime = 0.0;
private configureQuality(deltaTime: DOMHighResTimeStamp) {
this.timeSinceLastAdjusment += deltaTime;
if (
this.timeSinceLastAdjusment >=
settings.qualityScaling.adjusmentRateInMilliseconds
) {
this.timeSinceLastAdjusment = 0;
this.exponentialDecayedDeltaTime = exponentialDecay(
this.exponentialDecayedDeltaTime,
deltaTime,
settings.qualityScaling.deltaTimeResponsiveness
);
if (
this.exponentialDecayedDeltaTime <=
settings.qualityScaling.targetDeltaTimeInMilliseconds -
settings.qualityScaling.deltaTimeError
) {
this.qualityScaler.increase();
} else if (
this.exponentialDecayedDeltaTime >
settings.qualityScaling.targetDeltaTimeInMilliseconds +
settings.qualityScaling.deltaTimeError
) {
this.qualityScaler.decrease();
}
}
InfoText.modifyRecord(
'quality',
`${toPercent(this.distanceFieldFrameBuffer.renderScale)}, ${toPercent(
this.lightingFrameBuffer.renderScale
)}`
);
}
public drawPrimitive(primitive: IPrimitive) { public drawPrimitive(primitive: IPrimitive) {
this.primitives.push(primitive); this.distancePass.addDrawable(primitive);
} }
public drawLight(light: ILight) { public drawLight(light: ILight) {
this.lights.push(light); this.lightingPass.addDrawable(light);
} }
public startFrame(deltaTime: DOMHighResTimeStamp): void { public startFrame(deltaTime: DOMHighResTimeStamp): void {
this.configureQuality(deltaTime); this.autoscaler.autoscale(deltaTime);
this.primitives = [];
this.lights = [];
this.stopwatch?.start(); this.stopwatch?.start();
this.distanceFieldFrameBuffer.setSize(); this.distanceFieldFrameBuffer.setSize();
@ -160,84 +84,13 @@ export class WebGl2Renderer implements IRenderer {
} }
public finishFrame() { public finishFrame() {
const uniforms: any = this.calculateOwnUniforms(); const uniforms = this.calculateOwnUniforms();
this.distancePass.render(uniforms, this.viewCircle);
this.lights.forEach((l) => l.serializeToUniforms(uniforms)); this.lightingPass.render(
uniforms,
this.distanceFieldFrameBuffer.bindAndClear(); this.viewCircle,
const q = 1 / settings.tileMultiplier;
const uvSize = vec2.fromValues(q, q);
const possiblyOnScreenPrimitives = this.primitives.filter(
(p) => p.minimumDistance(this.viewCircle.center) < this.viewCircle.radius
) as Array<TunnelShape>;
InfoText.modifyRecord(
'nearby lines',
possiblyOnScreenPrimitives.length.toString()
);
const origin = vec2.transformMat2d(
vec2.create(),
vec2.fromValues(0, 0),
uniforms.uvToWorld
);
const firstCenter = vec2.transformMat2d(
vec2.create(),
vec2.fromValues(q, q),
uniforms.uvToWorld
);
vec2.subtract(firstCenter, firstCenter, origin);
const worldR = vec2.length(firstCenter);
uniforms.maxMinDistance = 2 * worldR;
let sumLineCount = 0;
for (let x = 0; x < 1; x += q) {
for (let y = 0; y < 1; y += q) {
const uvBottomLeft = vec2.fromValues(x, y);
this.distanceProgram.setDrawingRectangle(uvBottomLeft, uvSize);
const tileCenterWorldCoordinates = vec2.transformMat2d(
vec2.create(),
vec2.add(vec2.create(), uvBottomLeft, vec2.fromValues(q, q)),
uniforms.uvToWorld
);
const primitivesNearTile = possiblyOnScreenPrimitives.filter(
(p) => p.distance(tileCenterWorldCoordinates) < 2 * worldR
);
sumLineCount += primitivesNearTile.length;
uniforms.lines = [];
uniforms.radii = [];
primitivesNearTile.forEach((p) => p.serializeToUniforms(uniforms));
this.distanceProgram.bindAndSetUniforms(uniforms);
this.distanceProgram.draw();
}
}
InfoText.modifyRecord(
'lines',
(
sumLineCount /
settings.tileMultiplier /
settings.tileMultiplier
).toFixed(2)
);
this.lightingFrameBuffer.bindAndClear(
this.distanceFieldFrameBuffer.colorTexture this.distanceFieldFrameBuffer.colorTexture
); );
this.lightingProgram.bindAndSetUniforms(uniforms);
this.lightingProgram.draw();
this.stopwatch?.stop(); this.stopwatch?.stop();
} }

View file

@ -20,8 +20,12 @@ export const settings = {
}, },
}, },
tileMultiplier: 5, tileMultiplier: 5,
shaderUniforms: { shaderMacros: {
distanceScale: 64, distanceScale: 64,
edgeSmoothing: 10, edgeSmoothing: 10,
}, },
shaderCombinations: {
lineSteps: [0, 1, 2, 3, 4, 8, 16, 32],
circleLightSteps: [0, 1, 2, 3],
},
}; };

View file

@ -2,7 +2,6 @@
precision mediump float; precision mediump float;
#define LINES_ENABLED {linesEnabled}
#define LINE_COUNT {lineCount} #define LINE_COUNT {lineCount}
#define CAVE_COLOR vec3(0.36, 0.38, 0.76) #define CAVE_COLOR vec3(0.36, 0.38, 0.76)
#define AIR_COLOR vec3(0.7) #define AIR_COLOR vec3(0.7)
@ -11,27 +10,30 @@ precision mediump float;
uniform float maxMinDistance; uniform float maxMinDistance;
#if LINES_ENABLED #if LINE_COUNT > 0
// start, end - start uniform struct Line {
uniform vec2[LINE_COUNT * 2] lines; vec2 from;
// startRadius, endRadius vec2 toFromDelta;
uniform float[LINE_COUNT * 2] radii; float fromRadius;
float toRadius;
}[LINE_COUNT] lines;
#endif #endif
in vec2 worldCoordinates; in vec2 worldCoordinates;
out vec4 fragmentColor; out vec4 fragmentColor;
void main() { void main() {
#if LINES_ENABLED #if LINE_COUNT > 0
float minDistance = maxMinDistance; float minDistance = maxMinDistance;
for (int i = 0; i < LINE_COUNT; i++) { for (int i = 0; i < LINE_COUNT; i++) {
vec2 pa = worldCoordinates - lines[2 * i]; Line line = lines[i];
vec2 ba = lines[2 * i + 1]; vec2 pa = worldCoordinates - line.from;
vec2 ba = line.toFromDelta;
float baLength = length(ba); float baLength = length(ba);
// todo: do we really want this dot(pa / baLength, ba / baLength) // todo: do we really want this dot(pa / baLength, ba / baLength)
float h = clamp(dot(pa / baLength, ba / baLength), 0.0, 1.0); float h = clamp(dot(pa / baLength, ba / baLength), 0.0, 1.0);
float lineDistance = distance(pa, ba * h) - mix(radii[2 * i], radii[2 * i + 1], h); float lineDistance = distance(pa, ba * h) - mix(line.fromRadius, line.toRadius, h);
minDistance = min(minDistance, lineDistance); minDistance = min(minDistance, lineDistance);
} }

View file

@ -0,0 +1,100 @@
#version 300 es
precision mediump float;
#define INFINITY 1000.0
#define LIGHT_DROP 500.0
#define MIN_STEP 1.0
#define AMBIENT_LIGHT vec3(0.15)
#define LIGHT_COUNT {lightCount}
#define DISTANCE_SCALE {distanceScale}
#define EDGE_SMOOTHING {edgeSmoothing}
#if LIGHT_COUNT > 0
uniform struct Light {
vec2 center;
float radius;
vec3 value;
}[LIGHT_COUNT] lights;
uniform sampler2D distanceTexture;
uniform vec2 viewBoxSize;
in vec2[LIGHT_COUNT] directions;
float getDistance(in vec2 target, out vec3 color) {
vec4 values = texture(distanceTexture, target);
color = values.rgb;
return values.w * DISTANCE_SCALE;
}
float getDistance(in vec2 target) {
return texture(distanceTexture, target).w * DISTANCE_SCALE;
}
float getFractionOfLightArriving(
in vec2 target,
in vec2 direction,
in float startingDistance,
in float lightDistance,
in float lightRadius
) {
float q = 1.0;
float rayLength = startingDistance;
float movingAverageMeanDistance = startingDistance;
direction /= viewBoxSize;
for (int j = 0; j < 48; j++) {
float minDistance = getDistance(target + direction * rayLength);
movingAverageMeanDistance = movingAverageMeanDistance / 2.0 + minDistance / 2.0;
q = min(q, movingAverageMeanDistance / rayLength);
rayLength += max(MIN_STEP, minDistance);
if (rayLength > lightDistance) {
return clamp(q * (lightDistance + lightRadius) / lightRadius, 0.0, 1.0);
}
}
return 0.0;
}
#endif
in vec2 worldCoordinates;
in vec2 uvCoordinates;
out vec4 fragmentColor;
void main() {
#if LIGHT_COUNT > 0
vec3 colorAtPosition;
float startingDistance = getDistance(uvCoordinates, colorAtPosition);
vec3 ligthing = vec3(0.0);
for (int i = 0; i < LIGHT_COUNT; i++) {
Light light = lights[i];
float lightDistance = distance(light.center, worldCoordinates);
float r = (lightDistance + light.radius) / LIGHT_DROP + 1.0;
vec3 lightColorAtPosition = light.value / (r * r);
float fractionOfLightArriving = getFractionOfLightArriving(
uvCoordinates, normalize(directions[i]), startingDistance,
lightDistance, light.radius
);
ligthing += lightColorAtPosition * fractionOfLightArriving;
}
fragmentColor = vec4(
colorAtPosition * (AMBIENT_LIGHT +
step(EDGE_SMOOTHING / 2.0, clamp(startingDistance, 0.0, EDGE_SMOOTHING)) * ligthing),
1.0
);
#else
fragmentColor = vec4(1.0);
#endif
}

View file

@ -0,0 +1,39 @@
#version 300 es
precision mediump float;
#define LIGHT_COUNT {lightCount}
uniform mat3 modelTransform;
uniform mat3 ndcToUv;
uniform mat3 uvToWorld;
in vec4 vertexPosition;
out vec2 worldCoordinates;
out vec2 uvCoordinates;
#if LIGHT_COUNT > 0
uniform struct Light {
vec2 center;
float radius;
vec3 value;
}[LIGHT_COUNT] lights;
out vec2[LIGHT_COUNT] directions;
#endif
void main() {
vec3 vertexPosition2D = vec3(vertexPosition.xy, 1.0) * modelTransform;
vec3 uvCoordinates1 = vertexPosition2D * ndcToUv;
worldCoordinates = (uvCoordinates1 * uvToWorld).xy;
uvCoordinates = (uvCoordinates1).xy;
#if LIGHT_COUNT > 0
for (int i = 0; i < LIGHT_COUNT; i++) {
directions[i] = lights[i].center - worldCoordinates;
}
#endif
gl_Position = vec4(vertexPosition2D.xy, 0.0, 1.0);
}

View file

@ -2,7 +2,6 @@
precision mediump float; precision mediump float;
vec3 rainbow(float level) { vec3 rainbow(float level) {
float r = float(level <= 2.0) + float(level > 4.0) * 0.5; float r = float(level <= 2.0) + float(level > 4.0) * 0.5;
float g = max(1.0 - abs(level - 2.0) * 0.5, 0.0); float g = max(1.0 - abs(level - 2.0) * 0.5, 0.0);

View file

@ -1,8 +1,3 @@
import caveFragmentShader from '../shaders/cave-distance-fs.glsl';
import lightsShader from '../shaders/lights-shading-fs.glsl';
import caveVertexShader from '../shaders/passthrough-distance-vs.glsl';
import lightsVertexShader from '../shaders/passthrough-shading-vs.glsl';
// import lightsShader from '../shaders/rainbow-shading-fs.glsl';
import { CommandBroadcaster } from './commands/command-broadcaster'; import { CommandBroadcaster } from './commands/command-broadcaster';
import { BeforeRenderCommand } from './commands/types/before-render'; import { BeforeRenderCommand } from './commands/types/before-render';
import { RenderCommand } from './commands/types/draw'; import { RenderCommand } from './commands/types/draw';
@ -41,10 +36,7 @@ export class Game {
[this.objects] [this.objects]
); );
this.renderer = new WebGl2Renderer(canvas, overlay, [ this.renderer = new WebGl2Renderer(canvas, overlay);
[caveVertexShader, caveFragmentShader],
[lightsVertexShader, lightsShader],
]);
this.initializeScene(); this.initializeScene();
requestAnimationFrame(this.gameLoop.bind(this)); requestAnimationFrame(this.gameLoop.bind(this));

View file

@ -1,5 +1,5 @@
import { mix } from '../../helper/mix'; import { mix } from './mix';
import { clamp } from '../../helper/clamp'; import { clamp } from './clamp';
export class Autoscaler { export class Autoscaler {
// can have fractions // can have fractions

View file

@ -0,0 +1,31 @@
export const getCombinations = (
values: Array<Array<number>>
): Array<Array<number>> => {
if (!values.every((a) => a.length > 0)) {
return [];
}
const result: Array<Array<number>> = [];
const counters = values.map((_) => 0);
const increaseCounter = (i: number) => {
if (i >= counters.length) {
return false;
}
counters[i]++;
if (counters[i] >= values[i].length) {
counters[i] = 0;
return increaseCounter(i + 1);
}
return true;
};
do {
result.push(values.map((v, i) => v[counters[i]]));
} while (increaseCounter(0));
return result;
};

View file

@ -2,7 +2,7 @@ import { vec2, vec3 } from 'gl-matrix';
import { RenderCommand } from '../../commands/types/draw'; import { RenderCommand } from '../../commands/types/draw';
import { MoveToCommand } from '../../commands/types/move-to'; import { MoveToCommand } from '../../commands/types/move-to';
import { GameObject } from '../game-object'; import { GameObject } from '../game-object';
import { CircleLight } from '../../drawing/lights/circle-light'; import { CircleLight } from '../../drawing/drawables/lights/circle-light';
const range = 2000; const range = 2000;

View file

@ -1,6 +1,6 @@
import { vec2 } from 'gl-matrix'; import { vec2 } from 'gl-matrix';
import { RenderCommand } from '../../commands/types/draw'; import { RenderCommand } from '../../commands/types/draw';
import { TunnelShape } from '../../drawing/primitives/tunnel-shape'; import { TunnelShape } from '../../drawing/drawables/primitives/tunnel-shape';
import { GameObject } from '../game-object'; import { GameObject } from '../game-object';
export interface Line {} export interface Line {}

View file

@ -17,7 +17,7 @@ export const createDungeon = (objects: ObjectContainer) => {
new Tunnel(previousEnd, currentEnd, previousRadius, currentToRadius) new Tunnel(previousEnd, currentEnd, previousRadius, currentToRadius)
); );
if (deltaHeight > 0 && Math.random() > 0.8) { /*if (deltaHeight > 0 && Math.random() > 0.8) {
objects.addObject( objects.addObject(
new Lamp( new Lamp(
currentEnd, currentEnd,
@ -30,7 +30,7 @@ export const createDungeon = (objects: ObjectContainer) => {
1 1
) )
); );
} }*/
previousEnd = currentEnd; previousEnd = currentEnd;
previousRadius = currentToRadius; previousRadius = currentToRadius;

View file

@ -1,91 +0,0 @@
#version 300 es
precision mediump float;
#define INFINITY 1000.0
#define AMBIENT_LIGHT vec3(0.15)
#define LIGHT_DROP 500.0
#define MIN_STEP 1.0
#define LIGHT_COUNT 1
#define DISTANCE_SCALE {distanceScale}
#define EDGE_SMOOTHING {edgeSmoothing}
uniform struct Light {
vec2 center;
float radius;
vec3 value;
}[LIGHT_COUNT] lights;
uniform sampler2D distanceTexture;
uniform vec2 viewBoxSize;
in vec2[LIGHT_COUNT] directions;
float getDistance(in vec2 target, out vec3 color) {
vec4 values = texture(distanceTexture, target);
color = values.rgb;
return values.w * DISTANCE_SCALE;
}
float getDistance(in vec2 target) {
return texture(distanceTexture, target).w * DISTANCE_SCALE;
}
float getFractionOfLightArriving(
in vec2 target,
in vec2 direction,
in float startingDistance,
in float lightDistance,
in float lightRadius
) {
float q = 1.0;
float rayLength = startingDistance;
float movingAverageMeanDistance = startingDistance;
direction /= viewBoxSize;
for (int j = 0; j < 32; j++) {
float minDistance = getDistance(target + direction * rayLength);
movingAverageMeanDistance = movingAverageMeanDistance / 2.0 + minDistance / 2.0;
q = min(q, movingAverageMeanDistance / rayLength);
rayLength += max(MIN_STEP, minDistance);
if (rayLength > lightDistance) {
return clamp(q * (lightDistance + lightRadius) / lightRadius, 0.0, 1.0);
}
}
return 0.0;
}
in vec2 worldCoordinates;
in vec2 uvCoordinates;
out vec4 fragmentColor;
void main() {
vec3 colorAtPosition;
float startingDistance = getDistance(uvCoordinates, colorAtPosition);
vec3 ligthing = vec3(0.0);
for (int i = 0; i < LIGHT_COUNT; i++) {
Light light = lights[i];
float lightDistance = distance(light.center, worldCoordinates);
float r = (lightDistance + light.radius) / LIGHT_DROP + 1.0;
vec3 lightColorAtPosition = light.value / (r * r);
float fractionOfLightArriving = getFractionOfLightArriving(
uvCoordinates, normalize(directions[i]), startingDistance,
lightDistance, light.radius
);
ligthing += lightColorAtPosition * fractionOfLightArriving;
}
fragmentColor = vec4(
colorAtPosition * (AMBIENT_LIGHT +
step(EDGE_SMOOTHING / 2.0, clamp(startingDistance, 0.0, EDGE_SMOOTHING)) * ligthing),
1.0
);
}

View file

@ -1,33 +0,0 @@
#version 300 es
precision mediump float;
#define LIGHT_COUNT 1
uniform mat3 ndcToUv;
uniform mat3 uvToWorld;
in vec4 vertexPosition;
out vec2 worldCoordinates;
out vec2 uvCoordinates;
uniform struct Light {
vec2 center;
float radius;
vec3 value;
}[LIGHT_COUNT] lights;
out vec2[LIGHT_COUNT] directions;
void main() {
vec3 vertexPosition2D = vec3(vertexPosition.xy, 1.0);
vec3 uvCoordinates1 = vertexPosition2D * ndcToUv;
worldCoordinates = (uvCoordinates1 * uvToWorld).xy;
uvCoordinates = (uvCoordinates1).xy;
for (int i = 0; i < LIGHT_COUNT; i++) {
directions[i] = lights[i].center - worldCoordinates;
}
gl_Position = vertexPosition;
}