Add WebGL compatibility

This commit is contained in:
schmelczerandras 2020-09-22 16:41:59 +02:00
parent 3725f56c78
commit 1d4980ae28
29 changed files with 567 additions and 212 deletions

View file

@ -1,7 +1,8 @@
import { UniversalRenderingContext } from '../universal-rendering-context';
import { FrameBuffer } from './frame-buffer';
export class DefaultFrameBuffer extends FrameBuffer {
constructor(gl: WebGL2RenderingContext) {
constructor(gl: UniversalRenderingContext) {
super(gl);
this.frameBuffer = null;
this.setSize();

View file

@ -1,4 +1,5 @@
import { vec2 } from 'gl-matrix';
import { UniversalRenderingContext } from '../universal-rendering-context';
export abstract class FrameBuffer {
public renderScale = 1;
@ -9,7 +10,7 @@ export abstract class FrameBuffer {
// null means the default framebuffer
protected frameBuffer: WebGLFramebuffer | null = null;
constructor(protected gl: WebGL2RenderingContext) {}
constructor(protected gl: UniversalRenderingContext) {}
public bindAndClear(colorInput?: WebGLTexture) {
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.frameBuffer);

View file

@ -1,14 +1,17 @@
import { enableExtension } from '../helper/enable-extension';
import { UniversalRenderingContext } from '../universal-rendering-context';
import { FrameBuffer } from './frame-buffer';
export class IntermediateFrameBuffer extends FrameBuffer {
private frameTexture: WebGLTexture;
private floatLinearEnabled = false;
constructor(gl: WebGL2RenderingContext) {
constructor(gl: UniversalRenderingContext) {
super(gl);
enableExtension(gl, 'EXT_color_buffer_float');
if (gl.isWebGL2) {
enableExtension(gl, 'EXT_color_buffer_float');
}
try {
enableExtension(gl, 'OES_texture_float_linear');
@ -45,12 +48,12 @@ export class IntermediateFrameBuffer extends FrameBuffer {
this.gl.texImage2D(
this.gl.TEXTURE_2D,
0,
this.gl.RG16F,
this.gl.isWebGL2 ? this.gl.RG16F : this.gl.RGBA,
this.size.x,
this.size.y,
0,
this.gl.RG,
this.gl.FLOAT,
this.gl.isWebGL2 ? this.gl.RG : this.gl.RGBA,
this.gl.isWebGL2 ? this.gl.FLOAT : this.gl.UNSIGNED_BYTE,
null
);
}

View file

@ -1,4 +1,5 @@
import { Insights } from '../../rendering/insights';
import { UniversalRenderingContext } from '../universal-rendering-context';
const extensions: Map<string, any> = new Map();
@ -11,7 +12,7 @@ const logExtensions = () => {
};
export const tryEnableExtension = (
gl: WebGL2RenderingContext,
gl: UniversalRenderingContext,
name: string
): any | null => {
if (extensions.has(name)) {
@ -30,7 +31,7 @@ export const tryEnableExtension = (
return extension;
};
export const enableExtension = (gl: WebGL2RenderingContext, name: string): any => {
export const enableExtension = (gl: UniversalRenderingContext, name: string): any => {
const extension = tryEnableExtension(gl, name);
if (extension === null) {

View file

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

View file

@ -1,20 +1,21 @@
import { mat3, ReadonlyVec3, vec2, vec3 } from 'gl-matrix';
import { UniversalRenderingContext } from '../universal-rendering-context';
const loaderMat3 = mat3.create();
export const loadUniform = (
gl: WebGL2RenderingContext,
gl: UniversalRenderingContext,
value: any,
type: GLenum,
location: WebGLUniformLocation
): any => {
const converters: Map<
GLenum,
(gl: WebGL2RenderingContext, value: any, location: WebGLUniformLocation) => void
(gl: UniversalRenderingContext, value: any, location: WebGLUniformLocation) => void
> = new Map();
{
converters.set(WebGL2RenderingContext.FLOAT, (gl, v, l) => {
if (v instanceof Array) {
converters.set(WebGLRenderingContext.FLOAT, (gl, v, l) => {
if (v instanceof Array || v[0] instanceof Float32Array) {
if (v.length == 0) {
return;
}
@ -25,19 +26,18 @@ export const loadUniform = (
}
});
converters.set(WebGL2RenderingContext.FLOAT_VEC2, (gl, v: vec2 | Array<vec2>, l) => {
converters.set(WebGLRenderingContext.FLOAT_VEC2, (gl, v: vec2 | Array<vec2>, l) => {
if (v.length == 0) {
return;
}
if (v[0] instanceof Array) {
if (v[0] instanceof Array || v[0] instanceof Float32Array) {
const result = new Float32Array(v.length * 2);
for (let i = 0; i < v.length; i++) {
result[2 * i] = (v[i] as Array<number>).x;
result[2 * i + 1] = (v[i] as Array<number>).y;
}
gl.uniform2fv(l, result);
} else {
gl.uniform2fv(l, v as vec2);
@ -45,13 +45,13 @@ export const loadUniform = (
});
converters.set(
WebGL2RenderingContext.FLOAT_VEC3,
WebGLRenderingContext.FLOAT_VEC3,
(gl, v: ReadonlyVec3 | Array<vec3>, l) => {
if (v.length == 0) {
return;
}
if (v[0] instanceof Array) {
if (v[0] instanceof Array || v[0] instanceof Float32Array) {
const result = new Float32Array(v.length * 3);
for (let i = 0; i < v.length; i++) {
@ -67,11 +67,19 @@ export const loadUniform = (
}
);
converters.set(WebGL2RenderingContext.FLOAT_MAT3, (gl, v, l) => {
gl.uniformMatrix3fv(l, true, mat3.fromMat2d(loaderMat3, v));
converters.set(WebGLRenderingContext.FLOAT_MAT3, (gl, v, l) => {
if (gl.isWebGL2) {
gl.uniformMatrix3fv(l, true, mat3.fromMat2d(loaderMat3, v));
} else {
gl.uniformMatrix3fv(
l,
false,
mat3.transpose(mat3.create(), mat3.fromMat2d(loaderMat3, v))
);
}
});
converters.set(WebGL2RenderingContext.BOOL, (gl, v, l) => gl.uniform1i(l, v));
converters.set(WebGLRenderingContext.BOOL, (gl, v, l) => gl.uniform1i(l, v));
if (!converters.has(type)) {
throw new Error(`Unimplemented webgl type: ${type}`);

View file

@ -1,3 +1,4 @@
import { UniversalRenderingContext } from '../universal-rendering-context';
import { enableExtension } from './enable-extension';
enum StopwatchState {
@ -13,7 +14,7 @@ export class WebGlStopwatch {
private timerExtension: any;
private timerQuery?: WebGLQuery;
constructor(private gl: WebGL2RenderingContext) {
constructor(private gl: UniversalRenderingContext) {
this.timerExtension = enableExtension(gl, 'EXT_disjoint_timer_query_webgl2');
}

View file

@ -1,18 +1,23 @@
import { wait } from '../../helper/wait';
import { Insights } from '../rendering/insights';
import { tryEnableExtension } from './helper/enable-extension';
import { UniversalRenderingContext } from './universal-rendering-context';
type CompilingProgram = {
program: WebGLProgram;
resolvePromise: ((program: WebGLProgram) => void) | null;
vertexShader: ShaderWithSource;
fragmentShader: ShaderWithSource;
};
type ShaderWithSource = WebGLShader & { source: string };
export abstract class ParallelCompiler {
private static extension?: any;
private static gl: WebGL2RenderingContext;
private static gl: UniversalRenderingContext;
private static programs: Array<CompilingProgram> = [];
public static initialize(gl: WebGL2RenderingContext) {
public static initialize(gl: UniversalRenderingContext) {
ParallelCompiler.gl = gl;
ParallelCompiler.extension = tryEnableExtension(gl, 'KHR_parallel_shader_compile');
}
@ -28,14 +33,14 @@ export abstract class ParallelCompiler {
// can only return null on lost context
const program = ParallelCompiler.gl.createProgram()!;
ParallelCompiler.compileShader(
const vertexShader = ParallelCompiler.compileShader(
vertexShaderSource,
ParallelCompiler.gl.VERTEX_SHADER,
program,
substitutions
);
ParallelCompiler.compileShader(
const fragmentShader = ParallelCompiler.compileShader(
fragmentShaderSource,
ParallelCompiler.gl.FRAGMENT_SHADER,
program,
@ -45,6 +50,8 @@ export abstract class ParallelCompiler {
ParallelCompiler.programs.push({
program,
resolvePromise,
vertexShader,
fragmentShader,
});
return promise;
@ -67,7 +74,7 @@ export abstract class ParallelCompiler {
type: GLenum,
program: WebGLProgram,
substitutions: { [name: string]: string }
) {
): ShaderWithSource {
const processedSource = source.replace(/{(.+)}/gm, (_, name: string): string => {
const value = substitutions[name];
return Number.isInteger(value) ? `${value}.0` : value;
@ -79,7 +86,11 @@ export abstract class ParallelCompiler {
this.gl.shaderSource(shader, processedSource);
this.gl.compileShader(shader);
this.gl.attachShader(program, shader);
this.gl.deleteShader(shader);
const result = shader as ShaderWithSource;
result.source = processedSource;
return result;
}
private static resolveFinishedPrograms() {
@ -93,7 +104,7 @@ export abstract class ParallelCompiler {
ParallelCompiler.extension.COMPLETION_STATUS_KHR
)
) {
ParallelCompiler.checkProgram(p.program);
ParallelCompiler.checkProgram(p);
done.push(p);
p.resolvePromise!(p.program);
}
@ -104,18 +115,39 @@ export abstract class ParallelCompiler {
);
}
private static checkProgram(program: WebGLProgram) {
private static checkProgram(program: CompilingProgram) {
const success = ParallelCompiler.gl.getProgramParameter(
program,
program.program,
ParallelCompiler.gl.LINK_STATUS
);
if (!success && !ParallelCompiler.gl.isContextLost()) {
ParallelCompiler.gl.getAttachedShaders(program)?.forEach((s) => {
ParallelCompiler.checkShader(s);
});
ParallelCompiler.prettyPrintErrorsIfThereAreAny(program.vertexShader);
ParallelCompiler.prettyPrintErrorsIfThereAreAny(program.fragmentShader);
throw new Error(ParallelCompiler.gl.getProgramInfoLog(program)!);
throw new Error(ParallelCompiler.gl.getProgramInfoLog(program.program)!);
}
this.gl.deleteShader(program.vertexShader);
this.gl.deleteShader(program.fragmentShader);
}
private static prettyPrintErrorsIfThereAreAny(shader: ShaderWithSource) {
try {
ParallelCompiler.checkShader(shader);
} catch (e) {
for (const match of e
.toString()
.matchAll(/ERROR: 0:(?<line>\d+): (?<error>.*)$/gm)) {
const line = Number.parseInt(match.groups.line);
const error = match.groups.error;
console.error(
`Error: ${error}\nSource (line ${line}):\n${
shader.source.split('\n')[line - 1]
}`
);
}
throw new Error('Error while compiling shader');
}
}

View file

@ -1,10 +1,13 @@
import { enableExtension } from '../helper/enable-extension';
import { UniversalRenderingContext } from '../universal-rendering-context';
import Program from './program';
export class FragmentShaderOnlyProgram extends Program {
private vao?: WebGLVertexArrayObject;
constructor(gl: WebGL2RenderingContext) {
private vertexArrayExtension: any;
constructor(gl: UniversalRenderingContext) {
super(gl);
this.vertexArrayExtension = enableExtension(this.gl, 'OES_vertex_array_object');
}
public async initialize(
@ -17,7 +20,11 @@ export class FragmentShaderOnlyProgram extends Program {
public bind() {
super.bind();
this.gl.bindVertexArray(this.vao!);
if (this.gl.isWebGL2) {
this.gl.bindVertexArray(this.vao!);
} else {
this.vertexArrayExtension.createVertexArrayOES();
}
}
public draw(uniforms: { [name: string]: any }) {
@ -26,7 +33,12 @@ export class FragmentShaderOnlyProgram extends Program {
}
public destroy(): void {
this.gl.deleteVertexArray(this.vao!);
if (this.gl.isWebGL2) {
this.gl.deleteVertexArray(this.vao!);
} else {
this.vertexArrayExtension.deleteVertexArrayOES(this.vao!);
}
super.destroy();
}
@ -45,10 +57,15 @@ export class FragmentShaderOnlyProgram extends Program {
this.gl.STATIC_DRAW
);
// can only return null on lost context
this.vao = this.gl.createVertexArray()!;
if (this.gl.isWebGL2) {
// can only return null on lost context
this.vao = this.gl.createVertexArray()!;
this.gl.bindVertexArray(this.vao!);
} else {
this.vao = this.vertexArrayExtension.createVertexArrayOES();
this.vertexArrayExtension.bindVertexArrayOES(this.vao!);
}
this.gl.bindVertexArray(this.vao);
this.gl.enableVertexAttribArray(positionAttributeLocation);
this.gl.vertexAttribPointer(positionAttributeLocation, 2, this.gl.FLOAT, false, 0, 0);
}

View file

@ -1,6 +1,7 @@
import { mat2d, vec2 } from 'gl-matrix';
import { loadUniform } from '../helper/load-uniform';
import { ParallelCompiler } from '../parallel-compiler';
import { UniversalRenderingContext } from '../universal-rendering-context';
import { IProgram } from './i-program';
export default abstract class Program implements IProgram {
@ -14,7 +15,7 @@ export default abstract class Program implements IProgram {
type: GLenum;
}> = [];
constructor(protected gl: WebGL2RenderingContext) {}
constructor(protected gl: UniversalRenderingContext) {}
public async initialize(
[vertexShaderSource, fragmentShaderSource]: [string, string],
@ -67,7 +68,7 @@ export default abstract class Program implements IProgram {
private queryUniforms() {
const uniformCount = this.gl.getProgramParameter(
this.program!,
WebGL2RenderingContext.ACTIVE_UNIFORMS
WebGLRenderingContext.ACTIVE_UNIFORMS
);
for (let i = 0; i < uniformCount; i++) {

View file

@ -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 { UniversalRenderingContext } from '../universal-rendering-context';
import { FragmentShaderOnlyProgram } from './fragment-shader-only-program';
import { IProgram } from './i-program';
@ -16,7 +17,7 @@ export class UniformArrayAutoScalingProgram implements IProgram {
private drawingRectangleBottomLeft = vec2.fromValues(0, 0);
private drawingRectangleSize = vec2.fromValues(1, 1);
constructor(private gl: WebGL2RenderingContext) {}
constructor(private gl: UniversalRenderingContext) {}
public async initialize(
shaderSources: [string, string],
@ -44,9 +45,14 @@ export class UniformArrayAutoScalingProgram implements IProgram {
}
public draw(uniforms: { [name: string]: any }): void {
const values = this.descriptors!.map((d) =>
uniforms[d.uniformName] ? uniforms[d.uniformName].length : 0
);
const values = this.descriptors!.map((d) => {
const uniformNames = last(Object.entries(d.propertyUniformMapping));
if (!uniformNames) {
return 0;
}
const uniformName = uniformNames[1];
return uniforms[uniformName] ? uniforms[uniformName].length : 0;
});
const closest = this.programs.find((p) => p.values.every((v, i) => v >= values[i]));

View file

@ -0,0 +1,34 @@
import { Insights } from '../rendering/insights';
export type UniversalRenderingContext = WebGL2RenderingContext &
WebGLRenderingContext & { isWebGL2: boolean };
export const getUniversalRenderingContext = (
canvas: HTMLCanvasElement,
ignoreWebGL2 = false
): UniversalRenderingContext => {
let context: WebGL2RenderingContext | WebGLRenderingContext | null = ignoreWebGL2
? null
: canvas.getContext('webgl2');
let webgl2Support = true;
if (!context) {
context = canvas.getContext('webgl');
webgl2Support = false;
}
if (!context) {
context = canvas.getContext('experimental-webgl') as WebGLRenderingContext;
}
if (!context) {
throw new Error('Neither WebGL or WebGL2 is supported');
}
Insights.setValue('using WebGL2', webgl2Support);
const result = context as UniversalRenderingContext;
result.isWebGL2 = webgl2Support;
return result;
};