diff --git a/src/graphics/graphics-library/helper/enable-context-lost-simulator.ts b/src/graphics/graphics-library/helper/enable-context-lost-simulator.ts new file mode 100644 index 0000000..c84ae48 --- /dev/null +++ b/src/graphics/graphics-library/helper/enable-context-lost-simulator.ts @@ -0,0 +1,27 @@ +let isEnabled = false; + +export const enableContextLostSimulator = (canvas: HTMLCanvasElement) => { + if (isEnabled) { + return; + } + + isEnabled = true; + + const gl = canvas.getContext('webgl2')!; + const ext = gl.getExtension('WEBGL_lose_context'); + + const simulateContextLost = () => { + ext!.loseContext(); + console.log('lost'); + + const restoreTimeout = Math.random() * 500; + setTimeout(() => { + ext!.restoreContext(); + console.log('restored'); + }, restoreTimeout); + + setTimeout(() => simulateContextLost(), restoreTimeout + Math.random() * 500); + }; + + setTimeout(simulateContextLost, 1000); +}; diff --git a/src/graphics/graphics-library/helper/enable-extension.ts b/src/graphics/graphics-library/helper/enable-extension.ts index 5e1fd4f..ac7fd0a 100644 --- a/src/graphics/graphics-library/helper/enable-extension.ts +++ b/src/graphics/graphics-library/helper/enable-extension.ts @@ -1,32 +1,16 @@ import { Insights } from '../../rendering/insights'; import { UniversalRenderingContext } from '../universal-rendering-context'; -const extensions: Map = new Map(); - -const logExtensions = () => { - const values: any = {}; - for (const [k, v] of extensions.entries()) { - values[k] = v !== null; - } - Insights.setValue('extensions', values); -}; - export const tryEnableExtension = ( gl: UniversalRenderingContext, name: string ): any | null => { - if (extensions.has(name)) { - return extensions.get(name); - } - let extension = null; if (gl.getSupportedExtensions()!.indexOf(name) != -1) { extension = gl.getExtension(name); } - extensions.set(name, extension); - - logExtensions(); + Insights.setValue(['extensions', name], extension !== null); return extension; }; diff --git a/src/graphics/graphics-library/parallel-compiler.ts b/src/graphics/graphics-library/parallel-compiler.ts index cb7df2e..81b37c4 100644 --- a/src/graphics/graphics-library/parallel-compiler.ts +++ b/src/graphics/graphics-library/parallel-compiler.ts @@ -12,17 +12,17 @@ type CompilingProgram = { type ShaderWithSource = WebGLShader & { source: string }; -export abstract class ParallelCompiler { - private static extension?: any; - private static gl: UniversalRenderingContext; - private static programs: Array = []; +export class ParallelCompiler { + private extension?: any; + private gl: UniversalRenderingContext; + private programs: Array = []; - public static initialize(gl: UniversalRenderingContext) { - ParallelCompiler.gl = gl; - ParallelCompiler.extension = tryEnableExtension(gl, 'KHR_parallel_shader_compile'); + public constructor(gl: UniversalRenderingContext) { + this.gl = gl; + this.extension = tryEnableExtension(gl, 'KHR_parallel_shader_compile'); } - public static createProgram( + public createProgram( vertexShaderSource: string, fragmentShaderSource: string, substitutions: { [name: string]: string } @@ -31,23 +31,23 @@ export abstract class ParallelCompiler { const promise = new Promise((r) => (resolvePromise = r)); // can only return null on lost context - const program = ParallelCompiler.gl.createProgram()!; + const program = this.gl.createProgram()!; - const vertexShader = ParallelCompiler.compileShader( + const vertexShader = this.compileShader( vertexShaderSource, - ParallelCompiler.gl.VERTEX_SHADER, + this.gl.VERTEX_SHADER, program, substitutions ); - const fragmentShader = ParallelCompiler.compileShader( + const fragmentShader = this.compileShader( fragmentShaderSource, - ParallelCompiler.gl.FRAGMENT_SHADER, + this.gl.FRAGMENT_SHADER, program, substitutions ); - ParallelCompiler.programs.push({ + this.programs.push({ program, resolvePromise, vertexShader, @@ -58,18 +58,18 @@ export abstract class ParallelCompiler { } @Insights.measure('compile programs') - public static async compilePrograms(): Promise { - ParallelCompiler.programs.forEach((p) => ParallelCompiler.gl.linkProgram(p.program)); + public async compilePrograms(): Promise { + this.programs.forEach((p) => this.gl.linkProgram(p.program)); - Insights.setValue('program count', ParallelCompiler.programs.length); + Insights.setValue('program count', this.programs.length); - while (ParallelCompiler.programs.length > 0) { - ParallelCompiler.resolveFinishedPrograms(); + while (this.programs.length > 0) { + this.resolveFinishedPrograms(); await wait(0); } } - private static compileShader( + private compileShader( source: string, type: GLenum, program: WebGLProgram, @@ -87,7 +87,7 @@ export abstract class ParallelCompiler { } while (replaceHappened); // can only return null on lost context - const shader = ParallelCompiler.gl.createShader(type)!; + const shader = this.gl.createShader(type)!; this.gl.shaderSource(shader, processedSource); this.gl.compileShader(shader); @@ -99,48 +99,40 @@ export abstract class ParallelCompiler { return result; } - private static resolveFinishedPrograms() { + private resolveFinishedPrograms() { const done: Array = []; - ParallelCompiler.programs.forEach((p) => { + this.programs.forEach((p) => { if ( - !ParallelCompiler.extension || - ParallelCompiler.gl.getProgramParameter( - p.program, - ParallelCompiler.extension.COMPLETION_STATUS_KHR - ) + !this.extension || + this.gl.getProgramParameter(p.program, this.extension.COMPLETION_STATUS_KHR) ) { - ParallelCompiler.checkProgram(p); + this.checkProgram(p); done.push(p); p.resolvePromise!(p.program); } }); - ParallelCompiler.programs = ParallelCompiler.programs.filter( - (p1) => done.findIndex((p2) => p2 === p1) == -1 - ); + this.programs = this.programs.filter((p1) => done.findIndex((p2) => p2 === p1) == -1); } - private static checkProgram(program: CompilingProgram) { - const success = ParallelCompiler.gl.getProgramParameter( - program.program, - ParallelCompiler.gl.LINK_STATUS - ); + private checkProgram(program: CompilingProgram) { + const success = this.gl.getProgramParameter(program.program, this.gl.LINK_STATUS); - if (!success && !ParallelCompiler.gl.isContextLost()) { - ParallelCompiler.prettyPrintErrorsIfThereAreAny(program.vertexShader); - ParallelCompiler.prettyPrintErrorsIfThereAreAny(program.fragmentShader); + if (!success) { + this.prettyPrintErrorsIfThereAreAny(program.vertexShader); + this.prettyPrintErrorsIfThereAreAny(program.fragmentShader); - throw new Error(ParallelCompiler.gl.getProgramInfoLog(program.program)!); + throw new Error(this.gl.getProgramInfoLog(program.program)!); } this.gl.deleteShader(program.vertexShader); this.gl.deleteShader(program.fragmentShader); } - private static prettyPrintErrorsIfThereAreAny(shader: ShaderWithSource) { + private prettyPrintErrorsIfThereAreAny(shader: ShaderWithSource) { try { - ParallelCompiler.checkShader(shader); + this.checkShader(shader); } catch (e) { for (const match of e.toString().matchAll(/ERROR: 0:(\d+): (.*)$/gm)) { const line = Number.parseInt(match[1]); @@ -155,14 +147,11 @@ export abstract class ParallelCompiler { } } - private static checkShader(shader: WebGLShader) { - const success = ParallelCompiler.gl.getShaderParameter( - shader, - ParallelCompiler.gl.COMPILE_STATUS - ); + private checkShader(shader: WebGLShader) { + const success = this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS); - if (!success && !ParallelCompiler.gl.isContextLost()) { - throw new Error(ParallelCompiler.gl.getShaderInfoLog(shader)!); + if (!success) { + throw new Error(this.gl.getShaderInfoLog(shader)!); } } } diff --git a/src/graphics/graphics-library/program/fragment-shader-only-program.ts b/src/graphics/graphics-library/program/fragment-shader-only-program.ts index a08a7b6..33036e0 100644 --- a/src/graphics/graphics-library/program/fragment-shader-only-program.ts +++ b/src/graphics/graphics-library/program/fragment-shader-only-program.ts @@ -1,4 +1,5 @@ import { enableExtension } from '../helper/enable-extension'; +import { ParallelCompiler } from '../parallel-compiler'; import { UniversalRenderingContext } from '../universal-rendering-context'; import Program from './program'; @@ -14,9 +15,10 @@ export class FragmentShaderOnlyProgram extends Program { public async initialize( sources: [string, string], - substitutions: { [name: string]: string } + substitutions: { [name: string]: string }, + compiler: ParallelCompiler ): Promise { - await super.initialize(sources, substitutions); + await super.initialize(sources, substitutions, compiler); this.prepareScreenQuad('vertexPosition'); } diff --git a/src/graphics/graphics-library/program/program.ts b/src/graphics/graphics-library/program/program.ts index 04651a4..574ee7f 100644 --- a/src/graphics/graphics-library/program/program.ts +++ b/src/graphics/graphics-library/program/program.ts @@ -19,11 +19,12 @@ export default abstract class Program implements IProgram { public async initialize( [vertexShaderSource, fragmentShaderSource]: [string, string], - substitutions: { [name: string]: string } + substitutions: { [name: string]: string }, + compiler: ParallelCompiler ): Promise { substitutions = { ...substitutions }; - this.program = await ParallelCompiler.createProgram( + this.program = await compiler.createProgram( vertexShaderSource, fragmentShaderSource, substitutions diff --git a/src/graphics/graphics-library/program/uniform-array-autoscaling-program.ts b/src/graphics/graphics-library/program/uniform-array-autoscaling-program.ts index 28b28d0..dc5b7ea 100644 --- a/src/graphics/graphics-library/program/uniform-array-autoscaling-program.ts +++ b/src/graphics/graphics-library/program/uniform-array-autoscaling-program.ts @@ -2,6 +2,7 @@ import { mat2d, vec2 } from 'gl-matrix'; import { DrawableDescriptor } from '../../../drawables/drawable-descriptor'; import { getCombinations } from '../../../helper/get-combinations'; import { last } from '../../../helper/last'; +import { ParallelCompiler } from '../parallel-compiler'; import { UniversalRenderingContext } from '../universal-rendering-context'; import { FragmentShaderOnlyProgram } from './fragment-shader-only-program'; import { IProgram } from './i-program'; @@ -22,7 +23,8 @@ export class UniformArrayAutoScalingProgram implements IProgram { public async initialize( shaderSources: [string, string], descriptors: Array, - substitutions: { [name: string]: any } + substitutions: { [name: string]: any }, + compiler: ParallelCompiler ): Promise { this.descriptors = descriptors; @@ -32,7 +34,13 @@ export class UniformArrayAutoScalingProgram implements IProgram { descriptors.map((o) => o.shaderCombinationSteps) )) { promises.push( - this.createProgram(descriptors, substitutions, combination, shaderSources) + this.createProgram( + descriptors, + substitutions, + combination, + shaderSources, + compiler + ) ); } @@ -56,7 +64,7 @@ export class UniformArrayAutoScalingProgram implements IProgram { const closest = this.programs.find((p) => p.values.every((v, i) => v >= values[i])); - this.current = closest ? closest.program : last(this.programs)?.program; + this.current = (closest ? closest : last(this.programs))?.program; if (closest) { this.descriptors!.map((d, i) => { @@ -67,12 +75,12 @@ export class UniformArrayAutoScalingProgram implements IProgram { }); } - this.current!.setDrawingRectangleUV( + this.current?.setDrawingRectangleUV( this.drawingRectangleBottomLeft, this.drawingRectangleSize ); - this.current!.draw(uniforms); + this.current?.draw(uniforms); } public destroy(): void { @@ -83,7 +91,8 @@ export class UniformArrayAutoScalingProgram implements IProgram { descriptors: Array, substitutions: { [name: string]: any }, combination: Array, - shaderSources: [string, string] + shaderSources: [string, string], + compiler: ParallelCompiler ): Promise { const processedSubstitutions = { macroDefinitions: this.getMacroDefinitions(combination, descriptors), @@ -93,7 +102,7 @@ export class UniformArrayAutoScalingProgram implements IProgram { }; const program = new FragmentShaderOnlyProgram(this.gl); - await program.initialize(shaderSources, processedSubstitutions); + await program.initialize(shaderSources, processedSubstitutions, compiler); this.programs.push({ program, @@ -150,7 +159,7 @@ export class UniformArrayAutoScalingProgram implements IProgram { ${ descriptors[i].sdf?.isInverted ? `step(-distanceNdcPixelSize, -objectMinDistance)` - : `step(distanceNdcPixelSize, objectMinDistance)` + : `step(1.0 * distanceNdcPixelSize, objectMinDistance)` } ); diff --git a/src/graphics/graphics-library/universal-rendering-context.ts b/src/graphics/graphics-library/universal-rendering-context.ts index ace3e99..44bc520 100644 --- a/src/graphics/graphics-library/universal-rendering-context.ts +++ b/src/graphics/graphics-library/universal-rendering-context.ts @@ -29,5 +29,32 @@ export const getUniversalRenderingContext = ( } Insights.setValue('using WebGL2', result.isWebGL2); - return result; + + let isDestroyed = false; + + const handleContextLost = (e: Event) => { + e.preventDefault(); + isDestroyed = true; + canvas.removeEventListener('webglcontextlost', handleContextLost, false); + }; + canvas.addEventListener('webglcontextlost', handleContextLost, false); + + const contextLostHandler = { + get: function (target: UniversalRenderingContext, prop: string) { + if (isDestroyed || target.isContextLost()) { + isDestroyed = true; + throw new Error('Context lost'); + } + + const value = (target as any)[prop]; + + if (typeof value === 'function') { + return value.bind(target); + } + + return value; + }, + }; + + return new Proxy(result, contextLostHandler); }; diff --git a/src/graphics/rendering/fps-autoscaler.ts b/src/graphics/rendering/fps-autoscaler.ts index 739ee50..6326a80 100644 --- a/src/graphics/rendering/fps-autoscaler.ts +++ b/src/graphics/rendering/fps-autoscaler.ts @@ -18,7 +18,7 @@ const settings = { finalRenderScale: 0.6, }, { - distanceRenderScale: 0.3, + distanceRenderScale: 0.5, finalRenderScale: 1.0, }, { diff --git a/src/graphics/rendering/render-pass/render-pass.ts b/src/graphics/rendering/render-pass/render-pass.ts index d45988b..50c224c 100644 --- a/src/graphics/rendering/render-pass/render-pass.ts +++ b/src/graphics/rendering/render-pass/render-pass.ts @@ -1,5 +1,6 @@ import { DrawableDescriptor } from '../../../drawables/drawable-descriptor'; import { FrameBuffer } from '../../graphics-library/frame-buffer/frame-buffer'; +import { ParallelCompiler } from '../../graphics-library/parallel-compiler'; import { UniformArrayAutoScalingProgram } from '../../graphics-library/program/uniform-array-autoscaling-program'; import { UniversalRenderingContext } from '../../graphics-library/universal-rendering-context'; @@ -13,9 +14,10 @@ export abstract class RenderPass { public async initialize( shaderSources: [string, string], descriptors: Array, - substitutions: { [name: string]: any } = {} + substitutions: { [name: string]: any } = {}, + compiler: ParallelCompiler ): Promise { - await this.program.initialize(shaderSources, descriptors, substitutions); + await this.program.initialize(shaderSources, descriptors, substitutions, compiler); } public abstract render(commonUniforms: any, inputTexture?: WebGLTexture): void; diff --git a/src/graphics/rendering/renderer/context-aware-renderer.ts b/src/graphics/rendering/renderer/context-aware-renderer.ts new file mode 100644 index 0000000..939aad5 --- /dev/null +++ b/src/graphics/rendering/renderer/context-aware-renderer.ts @@ -0,0 +1,154 @@ +import { vec2 } from 'gl-matrix'; +import { Drawable, DrawableDescriptor } from '../../../main'; +import { RuntimeSettings } from '../settings/runtime-settings'; +import { StartupSettings } from '../settings/startup-settings'; +import { Renderer } from './renderer'; +import { RendererImplementation } from './renderer-implementation'; + +export class ContextAwareRenderer implements Renderer { + private renderer?: RendererImplementation; + private isRendererReady = false; + private readyPromise?: Promise; + private runtimeOverrides: Partial = {}; + private ignoreWebGL2?: boolean; + private previousViewAreaTopLeft?: vec2; + private previousViewAreaSize?: vec2; + + private contextRestoredHandler = this.handleContextRestored.bind(this); + private contextLostHandler = this.handleContextLost.bind(this); + + constructor( + private canvas: HTMLCanvasElement, + private descriptors: Array, + private settingsOverrides: Partial + ) { + canvas.addEventListener('webglcontextrestored', this.contextRestoredHandler, false); + + if (Object.prototype.hasOwnProperty.call(settingsOverrides, 'ignoreWebGL2')) { + this.ignoreWebGL2 = settingsOverrides.ignoreWebGL2 as boolean; + } + + this.createRenderer(); + } + + public get initializedPromise(): Promise { + return this.readyPromise!; + } + + private createRenderer() { + try { + this.renderer = new RendererImplementation( + this.canvas, + this.descriptors, + this.ignoreWebGL2 + ); + this.readyPromise = this.renderer.initialize(this.settingsOverrides); + this.waitForRenderer(); + } catch (e) { + if (!e.message.includes('Context lost')) { + throw e; + } + console.warn(e.message); + } + } + + private async waitForRenderer() { + try { + await this.readyPromise; + this.isRendererReady = true; + } catch (e) { + if (!e.message.includes('Context lost')) { + throw e; + } + console.warn(e.message); + } + + this.setRuntimeSettings(this.runtimeOverrides); + if (this.previousViewAreaTopLeft && this.previousViewAreaSize) { + this.setViewArea(this.previousViewAreaTopLeft, this.previousViewAreaSize); + } + } + + private handleContextLost(event: Event) { + this.isRendererReady = false; + event.preventDefault(); + console.warn('Context lost'); + } + + private handleContextRestored(event: Event) { + event.preventDefault(); + console.info('Context restored'); + + this.createRenderer(); + } + + private handle(f: () => T, defaultValue: T): T { + if (this.isRendererReady) { + try { + return f(); + } catch (e) { + if (e.message.includes('Context lost')) { + this.isRendererReady = false; + console.warn(e.message); + return defaultValue; + } + throw e; + } + } else { + return defaultValue; + } + } + + public get canvasSize(): vec2 { + return this.handle(() => this.renderer!.canvasSize, vec2.create()); + } + + public get viewAreaSize(): vec2 { + return this.handle(() => this.renderer!.viewAreaSize, vec2.create()); + } + + public get insights(): any { + return this.handle(() => this.renderer!.insights, {}); + } + + public setViewArea(topLeft: vec2, size: vec2): void { + this.previousViewAreaTopLeft = topLeft; + this.previousViewAreaSize = size; + return this.handle(() => this.renderer!.setViewArea(topLeft, size), undefined); + } + + public setRuntimeSettings(overrides: Partial): void { + this.runtimeOverrides = { + ...this.runtimeOverrides, + ...overrides, + }; + + return this.handle(() => this.renderer!.setRuntimeSettings(overrides), undefined); + } + + public addDrawable(drawable: Drawable): void { + return this.handle(() => this.renderer!.addDrawable(drawable), undefined); + } + + public autoscaleQuality(deltaTime: number): void { + return this.handle(() => this.renderer!.autoscaleQuality(deltaTime), undefined); + } + + public renderDrawables(): void { + return this.handle(() => this.renderer!.renderDrawables(), undefined); + } + + public destroy(): void { + this.canvas.removeEventListener( + 'webglcontextrestored', + this.contextRestoredHandler, + false + ); + + this.canvas.removeEventListener('webglcontextlost', this.contextLostHandler, false); + + this.isRendererReady = false; + + this.handle(() => this.renderer!.destroy(), undefined); + } +} diff --git a/src/graphics/rendering/renderer-implementation.ts b/src/graphics/rendering/renderer/renderer-implementation.ts similarity index 69% rename from src/graphics/rendering/renderer-implementation.ts rename to src/graphics/rendering/renderer/renderer-implementation.ts index acb3c1f..cca6164 100644 --- a/src/graphics/rendering/renderer-implementation.ts +++ b/src/graphics/rendering/renderer/renderer-implementation.ts @@ -1,33 +1,34 @@ import { vec2 } from 'gl-matrix'; -import { Drawable } from '../../drawables/drawable'; -import { DrawableDescriptor } from '../../drawables/drawable-descriptor'; -import { LightDrawable } from '../../drawables/lights/light-drawable'; -import { msToString } from '../../helper/ms-to-string'; -import { DefaultFrameBuffer } from '../graphics-library/frame-buffer/default-frame-buffer'; -import { IntermediateFrameBuffer } from '../graphics-library/frame-buffer/intermediate-frame-buffer'; -import { WebGlStopwatch } from '../graphics-library/helper/stopwatch'; -import { PaletteTexture } from '../graphics-library/palette-texture'; -import { ParallelCompiler } from '../graphics-library/parallel-compiler'; +import { Drawable } from '../../../drawables/drawable'; +import { DrawableDescriptor } from '../../../drawables/drawable-descriptor'; +import { LightDrawable } from '../../../drawables/lights/light-drawable'; +import { msToString } from '../../../helper/ms-to-string'; +import { DefaultFrameBuffer } from '../../graphics-library/frame-buffer/default-frame-buffer'; +import { IntermediateFrameBuffer } from '../../graphics-library/frame-buffer/intermediate-frame-buffer'; +import { WebGlStopwatch } from '../../graphics-library/helper/stopwatch'; +import { PaletteTexture } from '../../graphics-library/palette-texture'; +import { ParallelCompiler } from '../../graphics-library/parallel-compiler'; import { getUniversalRenderingContext, UniversalRenderingContext, -} from '../graphics-library/universal-rendering-context'; -import { FpsAutoscaler } from './fps-autoscaler'; -import { Insights } from './insights'; -import { DistanceRenderPass } from './render-pass/distance-render-pass'; -import { LightsRenderPass } from './render-pass/lights-render-pass'; +} from '../../graphics-library/universal-rendering-context'; +import { FpsAutoscaler } from '../fps-autoscaler'; +import { Insights } from '../insights'; +import { DistanceRenderPass } from '../render-pass/distance-render-pass'; +import { LightsRenderPass } from '../render-pass/lights-render-pass'; +import { defaultStartupSettings } from '../settings/default-startup-settings'; +import { RuntimeSettings } from '../settings/runtime-settings'; +import { StartupSettings } from '../settings/startup-settings'; +import distanceFragmentShader100 from '../shaders/distance-fs-100.glsl'; +import distanceFragmentShader from '../shaders/distance-fs.glsl'; +import distanceVertexShader100 from '../shaders/distance-vs-100.glsl'; +import distanceVertexShader from '../shaders/distance-vs.glsl'; +import lightsFragmentShader100 from '../shaders/shading-fs-100.glsl'; +import lightsFragmentShader from '../shaders/shading-fs.glsl'; +import lightsVertexShader100 from '../shaders/shading-vs-100.glsl'; +import lightsVertexShader from '../shaders/shading-vs.glsl'; +import { UniformsProvider } from '../uniforms-provider'; import { Renderer } from './renderer'; -import { RuntimeSettings } from './settings/runtime-settings'; -import { StartupSettings } from './settings/startup-settings'; -import distanceFragmentShader100 from './shaders/distance-fs-100.glsl'; -import distanceFragmentShader from './shaders/distance-fs.glsl'; -import distanceVertexShader100 from './shaders/distance-vs-100.glsl'; -import distanceVertexShader from './shaders/distance-vs.glsl'; -import lightsFragmentShader100 from './shaders/shading-fs-100.glsl'; -import lightsFragmentShader from './shaders/shading-fs.glsl'; -import lightsVertexShader100 from './shaders/shading-vs-100.glsl'; -import lightsVertexShader from './shaders/shading-vs.glsl'; -import { UniformsProvider } from './uniforms-provider'; export class RendererImplementation implements Renderer { private readonly gl: UniversalRenderingContext; @@ -41,7 +42,7 @@ export class RendererImplementation implements Renderer { private autoscaler: FpsAutoscaler; private applyRuntimeSettings: { - [key: string]: (value: any) => void; + [key in keyof RuntimeSettings]: (value: any) => void; } = { enableHighDpiRendering: (v) => { this.distanceFieldFrameBuffer.enableHighDpiRendering = v; @@ -49,30 +50,29 @@ export class RendererImplementation implements Renderer { }, tileMultiplier: (v) => (this.distancePass.tileMultiplier = v), isWorldInverted: (v) => (this.distancePass.isWorldInverted = v), - shadowLength: (v) => (this.uniformsProvider.shadowLength = v), + backgroundColor: (v) => (this.uniformsProvider.backgroundColor = v), ambientLight: (v) => (this.uniformsProvider.ambientLight = v), lightCutoffDistance: (v) => (this.lightsPass.lightCutoffDistance = v), colorPalette: (v) => this.palette!.setPalette(v), }; - private static defaultStartupSettings: StartupSettings = { - shadowTraceCount: 16, - paletteSize: 4, - }; - setRuntimeSettings(overrides: Partial): void { Object.entries(overrides).forEach(([k, v]) => { - this.applyRuntimeSettings[(k as unknown) as string](v); + this.applyRuntimeSettings[k as keyof RuntimeSettings](v); }); } constructor( private canvas: HTMLCanvasElement, - private descriptors: Array + private descriptors: Array, + ignoreWebGL2?: boolean ) { - this.gl = getUniversalRenderingContext(canvas); + this.gl = getUniversalRenderingContext( + canvas, + ignoreWebGL2 !== undefined ? ignoreWebGL2 : defaultStartupSettings.ignoreWebGL2 + ); - ParallelCompiler.initialize(this.gl); + this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE); this.distanceFieldFrameBuffer = new IntermediateFrameBuffer(this.gl); this.lightingFrameBuffer = new DefaultFrameBuffer(this.gl); @@ -82,6 +82,11 @@ export class RendererImplementation implements Renderer { this.uniformsProvider = new UniformsProvider(this.gl); + this.setViewArea( + vec2.fromValues(0, canvas.clientHeight), + vec2.fromValues(canvas.clientWidth, canvas.clientHeight) + ); + this.queryPrecisions(); this.autoscaler = new FpsAutoscaler({ @@ -93,13 +98,15 @@ export class RendererImplementation implements Renderer { public async initialize(settingsOverrides: Partial): Promise { const settings = { - ...RendererImplementation.defaultStartupSettings, + ...defaultStartupSettings, ...settingsOverrides, }; this.palette = new PaletteTexture(this.gl, settings.paletteSize); const promises: Array> = []; + const compiler = new ParallelCompiler(this.gl); + promises.push( this.distancePass.initialize( this.gl.isWebGL2 @@ -108,7 +115,8 @@ export class RendererImplementation implements Renderer { this.descriptors.filter(RendererImplementation.hasSdf), { paletteSize: settings.paletteSize, - } + }, + compiler ) ); promises.push( @@ -119,12 +127,12 @@ export class RendererImplementation implements Renderer { this.descriptors.filter((d) => !RendererImplementation.hasSdf(d)), { shadowTraceCount: settings.shadowTraceCount.toString(), - } + }, + compiler ) ); - ParallelCompiler.compilePrograms(); - + await compiler.compilePrograms(); await Promise.all(promises); try { @@ -200,17 +208,20 @@ export class RendererImplementation implements Renderer { // texture units distanceTexture: 0, palette: 1, - // regular uniforms + distanceNdcPixelSize: 2 / Math.max(...this.distanceFieldFrameBuffer.getSize()), shadingNdcPixelSize: 2 / Math.max(...this.distanceFieldFrameBuffer.getSize()), }; this.distancePass.render(this.uniformsProvider.getUniforms(common)); + + this.gl.enable(this.gl.BLEND); this.lightsPass.render( this.uniformsProvider.getUniforms(common), this.distanceFieldFrameBuffer.colorTexture, this.palette!.colorTexture ); + this.gl.disable(this.gl.BLEND); if (this.stopwatch?.isRunning) { this.stopwatch?.stop(); diff --git a/src/graphics/rendering/renderer.ts b/src/graphics/rendering/renderer/renderer.ts similarity index 77% rename from src/graphics/rendering/renderer.ts rename to src/graphics/rendering/renderer/renderer.ts index 9fc5904..3689d51 100644 --- a/src/graphics/rendering/renderer.ts +++ b/src/graphics/rendering/renderer/renderer.ts @@ -1,6 +1,6 @@ import { vec2 } from 'gl-matrix'; -import { Drawable } from '../../drawables/drawable'; -import { RuntimeSettings } from './settings/runtime-settings'; +import { Drawable } from '../../../drawables/drawable'; +import { RuntimeSettings } from '../settings/runtime-settings'; export interface Renderer { readonly canvasSize: vec2; diff --git a/src/graphics/rendering/uniforms-provider.ts b/src/graphics/rendering/uniforms-provider.ts index 636ba03..2ff852f 100644 --- a/src/graphics/rendering/uniforms-provider.ts +++ b/src/graphics/rendering/uniforms-provider.ts @@ -1,9 +1,9 @@ -import { mat2d, vec2, vec3 } from 'gl-matrix'; +import { mat2d, vec2, vec3, vec4 } from 'gl-matrix'; import { UniversalRenderingContext } from '../graphics-library/universal-rendering-context'; export class UniformsProvider { public ambientLight = vec3.fromValues(0.25, 0.15, 0.25); - public shadowLength = 400; + public _backgroundColor = vec4.fromValues(1, 1, 1, 1); private scaleWorldLengthToNDC = 1; private transformWorldToNDC = mat2d.create(); @@ -18,7 +18,7 @@ export class UniformsProvider { return { ...uniforms, ambientLight: this.ambientLight, - shadowLength: this.shadowLength, + backgroundColor: this._backgroundColor, uvToWorld: this.uvToWorld, worldAreaInView: this.worldAreaInView, squareToAspectRatio: this.squareToAspectRatio, @@ -28,6 +28,14 @@ export class UniformsProvider { }; } + public set backgroundColor(value: vec3 | vec4) { + if (value.length === 3) { + this.backgroundColor = vec4.fromValues(value[0], value[1], value[2], 1); + } else { + this._backgroundColor = value as vec4; + } + } + public getViewArea(): vec2 { return this.worldAreaInView; } diff --git a/src/main.ts b/src/main.ts index 1f5445b..79b9489 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,7 +1,7 @@ import { DrawableDescriptor } from './drawables/drawable-descriptor'; import { Insights } from './graphics/rendering/insights'; -import { Renderer } from './graphics/rendering/renderer'; -import { RendererImplementation } from './graphics/rendering/renderer-implementation'; +import { ContextAwareRenderer } from './graphics/rendering/renderer/context-aware-renderer'; +import { Renderer } from './graphics/rendering/renderer/renderer'; import { StartupSettings } from './graphics/rendering/settings/startup-settings'; import { applyArrayPlugins } from './helper/array'; @@ -12,7 +12,7 @@ export { Flashlight } from './drawables/lights/flashlight'; export { Circle } from './drawables/shapes/circle'; export { InvertedTunnel } from './drawables/shapes/inverted-tunnel'; export { Tunnel } from './drawables/shapes/tunnel'; -export { Renderer } from './graphics/rendering/renderer'; +export { Renderer } from './graphics/rendering/renderer/renderer'; declare global { interface Array { @@ -31,11 +31,13 @@ applyArrayPlugins(); export async function compile( canvas: HTMLCanvasElement, descriptors: Array, - settings: Partial = {} + settingsOverrides: Partial = {} ): Promise { + // enableContextLostSimulator(canvas); + return Insights.measureFunction('startup', async () => { - const renderer = new RendererImplementation(canvas, descriptors); - await renderer.initialize(settings); + const renderer = new ContextAwareRenderer(canvas, descriptors, settingsOverrides); + await renderer.initializedPromise; return renderer; }); }