WebGLCompute = {
const VARYING_REGEX = /[^\w](?:varying|out)\s+\w+\s+(\w+)\s*;/g;
const lineNumbers = (source, offset = 0) =>
source.replace(/^/gm, () => `${offset++}:`);
const getDataType = (data) => {
switch (data.constructor) {
case Float32Array:
return 5126;
case Int8Array:
return 5120;
case Int16Array:
return 5122;
case Int32Array:
return 5124;
case Uint8Array:
case Uint8ClampedArray:
return 5121;
case Uint16Array:
return 5123;
case Uint32Array:
return 5125;
default:
return null;
}
};
/**
* Constructs a WebGL compute program via transform feedback. Can be used to compute and serialize data from the GPU.
*/
class WebGLCompute {
constructor(gl = new OffscreenCanvas(1,1).getContext("webgl2")) {
this.gl = gl;
this._compiled = new Map();
this._fragmentShader = this.gl.createShader(this.gl.FRAGMENT_SHADER);
this.gl.shaderSource(
this._fragmentShader,
"#version 300 es\nvoid main(){}"
);
this.gl.compileShader(this._fragmentShader);
}
/**
* Compiles a transform feedback program from compute options.
*/
compile(options) {
let compiled = this._compiled.get(options);
if (compiled) {
this.gl.bindVertexArray(compiled.VAO);
for (const [name, buffer] of compiled.buffers) {
const input = options.inputs[name];
if (!input.needsUpdate) continue;
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer);
this.gl.bufferData(
this.gl.ARRAY_BUFFER,
input.data,
this.gl.DYNAMIC_READ
);
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);
input.needsUpdate = false;
}
this.gl.bindVertexArray(null);
return compiled;
}
// Parse outputs from shader source
const outputs = Array.from(options.compute.matchAll(VARYING_REGEX)).map(
([, varying]) => varying
);
// Compile shaders, configure output varyings
const program = this.gl.createProgram();
const vertexShader = this.gl.createShader(this.gl.VERTEX_SHADER);
this.gl.shaderSource(vertexShader, options.compute);
this.gl.compileShader(vertexShader);
this.gl.attachShader(program, vertexShader);
this.gl.attachShader(program, this._fragmentShader);
this.gl.transformFeedbackVaryings(
program,
outputs,
this.gl.SEPARATE_ATTRIBS
);
this.gl.linkProgram(program);
const shaderError = this.gl.getShaderInfoLog(vertexShader);
if (shaderError)
throw `Error compiling shader: ${shaderError}\n${lineNumbers(
options.compute
)}`;
const programError = this.gl.getProgramInfoLog(program);
if (programError)
throw `Error compiling program: ${this.gl.getProgramInfoLog(program)}`;
this.gl.detachShader(program, vertexShader);
this.gl.detachShader(program, this._fragmentShader);
this.gl.deleteShader(vertexShader);
// Init VAO state (input)
const VAO = this.gl.createVertexArray();
this.gl.bindVertexArray(VAO);
let length = 0;
const buffers = new Map();
for (const name in options.inputs) {
const input = options.inputs[name];
const buffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer);
this.gl.bufferData(
this.gl.ARRAY_BUFFER,
input.data,
this.gl.DYNAMIC_READ //this.gl.STATIC_READ
);
const location = this.gl.getAttribLocation(program, name);
const slots = Math.min(4, Math.max(1, Math.floor(input.size / 3)));
for (let i = 0; i < slots; i++) {
this.gl.enableVertexAttribArray(location + i);
if (input.data instanceof Float32Array) {
this.gl.vertexAttribPointer(
location,
input.size,
this.gl.FLOAT,
false,
0,
0
);
} else {
const dataType = getDataType(input.data);
this.gl.vertexAttribIPointer(location, input.size, dataType, 0, 0);
}
if (input.divisor)
this.gl.vertexAttribDivisor(location + i, input.divisor);
}
buffers.set(name, buffer);
length = Math.max(length, input.data.length / input.size);
input.needsUpdate = false;
}
this.gl.bindVertexArray(null);
// Init feedback state (output)
const transformFeedback = this.gl.createTransformFeedback();
this.gl.bindTransformFeedback(
this.gl.TRANSFORM_FEEDBACK,
transformFeedback
);
const containers = new Map();
for (let i = 0; i < outputs.length; i++) {
const output = outputs[i];
const data = new Float32Array(length * options.instances);
containers.set(output, data);
const buffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer);
this.gl.bufferData(this.gl.ARRAY_BUFFER, data, this.gl.STATIC_COPY);
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);
buffers.set(output, buffer);
this.gl.bindBufferBase(this.gl.TRANSFORM_FEEDBACK_BUFFER, i, buffer);
}
this.gl.bindTransformFeedback(this.gl.TRANSFORM_FEEDBACK, null);
compiled = {
program,
VAO,
transformFeedback,
buffers,
containers,
length,
};
this._compiled.set(options, compiled);
return compiled;
}
/**
* Runs and reads from the compute program.
*/
compute(options) {
return compute.call(this,options)
}
/**
* Disposes the compute pipeline from GPU memory.
*/
dispose() {
this.gl.deleteShader(this._fragmentShader);
for (const [, compiled] of this._compiled) {
this.gl.deleteProgram(compiled.program);
this.gl.deleteVertexArray(compiled.VAO);
this.gl.deleteTransformFeedback(compiled.transformFeedback);
compiled.buffers.forEach((buffer) => this.gl.deleteBuffer(buffer));
}
this._compiled.clear();
}
}
return WebGLCompute;
}