Files
Webklar.com/node_modules/ogl/src/core/Program.js
Basilosaurusrex f027651f9b main repo
2025-11-24 18:09:40 +01:00

330 lines
12 KiB
JavaScript

// TODO: upload empty texture if null ? maybe not
// TODO: upload identity matrix if null ?
// TODO: sampler Cube
let ID = 1;
// cache of typed arrays used to flatten uniform arrays
const arrayCacheF32 = {};
export class Program {
constructor(
gl,
{
vertex,
fragment,
uniforms = {},
transparent = false,
cullFace = gl.BACK,
frontFace = gl.CCW,
depthTest = true,
depthWrite = true,
depthFunc = gl.LEQUAL,
} = {}
) {
if (!gl.canvas) console.error('gl not passed as first argument to Program');
this.gl = gl;
this.uniforms = uniforms;
this.id = ID++;
if (!vertex) console.warn('vertex shader not supplied');
if (!fragment) console.warn('fragment shader not supplied');
// Store program state
this.transparent = transparent;
this.cullFace = cullFace;
this.frontFace = frontFace;
this.depthTest = depthTest;
this.depthWrite = depthWrite;
this.depthFunc = depthFunc;
this.blendFunc = {};
this.blendEquation = {};
this.stencilFunc = {};
this.stencilOp = {}
// set default blendFunc if transparent flagged
if (this.transparent && !this.blendFunc.src) {
if (this.gl.renderer.premultipliedAlpha) this.setBlendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA);
else this.setBlendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
}
// Create empty shaders and attach to program
this.vertexShader = gl.createShader(gl.VERTEX_SHADER);
this.fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
this.program = gl.createProgram();
gl.attachShader(this.program, this.vertexShader);
gl.attachShader(this.program, this.fragmentShader);
// Compile shaders with source
this.setShaders({ vertex, fragment });
}
setShaders({ vertex, fragment }) {
if (vertex) {
// compile vertex shader and log errors
this.gl.shaderSource(this.vertexShader, vertex);
this.gl.compileShader(this.vertexShader);
if (this.gl.getShaderInfoLog(this.vertexShader) !== '') {
console.warn(`${this.gl.getShaderInfoLog(this.vertexShader)}\nVertex Shader\n${addLineNumbers(vertex)}`);
}
}
if (fragment) {
// compile fragment shader and log errors
this.gl.shaderSource(this.fragmentShader, fragment);
this.gl.compileShader(this.fragmentShader);
if (this.gl.getShaderInfoLog(this.fragmentShader) !== '') {
console.warn(`${this.gl.getShaderInfoLog(this.fragmentShader)}\nFragment Shader\n${addLineNumbers(fragment)}`);
}
}
// compile program and log errors
this.gl.linkProgram(this.program);
if (!this.gl.getProgramParameter(this.program, this.gl.LINK_STATUS)) {
return console.warn(this.gl.getProgramInfoLog(this.program));
}
// Get active uniform locations
this.uniformLocations = new Map();
let numUniforms = this.gl.getProgramParameter(this.program, this.gl.ACTIVE_UNIFORMS);
for (let uIndex = 0; uIndex < numUniforms; uIndex++) {
let uniform = this.gl.getActiveUniform(this.program, uIndex);
this.uniformLocations.set(uniform, this.gl.getUniformLocation(this.program, uniform.name));
// split uniforms' names to separate array and struct declarations
const split = uniform.name.match(/(\w+)/g);
uniform.uniformName = split[0];
uniform.nameComponents = split.slice(1);
}
// Get active attribute locations
this.attributeLocations = new Map();
const locations = [];
const numAttribs = this.gl.getProgramParameter(this.program, this.gl.ACTIVE_ATTRIBUTES);
for (let aIndex = 0; aIndex < numAttribs; aIndex++) {
const attribute = this.gl.getActiveAttrib(this.program, aIndex);
const location = this.gl.getAttribLocation(this.program, attribute.name);
// Ignore special built-in inputs. eg gl_VertexID, gl_InstanceID
if (location === -1) continue;
locations[location] = attribute.name;
this.attributeLocations.set(attribute, location);
}
this.attributeOrder = locations.join('');
}
setBlendFunc(src, dst, srcAlpha, dstAlpha) {
this.blendFunc.src = src;
this.blendFunc.dst = dst;
this.blendFunc.srcAlpha = srcAlpha;
this.blendFunc.dstAlpha = dstAlpha;
if (src) this.transparent = true;
}
setBlendEquation(modeRGB, modeAlpha) {
this.blendEquation.modeRGB = modeRGB;
this.blendEquation.modeAlpha = modeAlpha;
}
setStencilFunc(func, ref, mask) {
this.stencilRef = ref;
this.stencilFunc.func = func;
this.stencilFunc.ref = ref;
this.stencilFunc.mask = mask;
}
setStencilOp(stencilFail, depthFail, depthPass) {
this.stencilOp.stencilFail = stencilFail;
this.stencilOp.depthFail = depthFail;
this.stencilOp.depthPass = depthPass;
}
applyState() {
if (this.depthTest) this.gl.renderer.enable(this.gl.DEPTH_TEST);
else this.gl.renderer.disable(this.gl.DEPTH_TEST);
if (this.cullFace) this.gl.renderer.enable(this.gl.CULL_FACE);
else this.gl.renderer.disable(this.gl.CULL_FACE);
if (this.blendFunc.src) this.gl.renderer.enable(this.gl.BLEND);
else this.gl.renderer.disable(this.gl.BLEND);
if (this.cullFace) this.gl.renderer.setCullFace(this.cullFace);
this.gl.renderer.setFrontFace(this.frontFace);
this.gl.renderer.setDepthMask(this.depthWrite);
this.gl.renderer.setDepthFunc(this.depthFunc);
if (this.blendFunc.src) this.gl.renderer.setBlendFunc(this.blendFunc.src, this.blendFunc.dst, this.blendFunc.srcAlpha, this.blendFunc.dstAlpha);
this.gl.renderer.setBlendEquation(this.blendEquation.modeRGB, this.blendEquation.modeAlpha);
if(this.stencilFunc.func || this.stencilOp.stencilFail) this.gl.renderer.enable(this.gl.STENCIL_TEST)
else this.gl.renderer.disable(this.gl.STENCIL_TEST)
this.gl.renderer.setStencilFunc(this.stencilFunc.func, this.stencilFunc.ref, this.stencilFunc.mask)
this.gl.renderer.setStencilOp(this.stencilOp.stencilFail, this.stencilOp.depthFail, this.stencilOp.depthPass)
}
use({ flipFaces = false } = {}) {
let textureUnit = -1;
const programActive = this.gl.renderer.state.currentProgram === this.id;
// Avoid gl call if program already in use
if (!programActive) {
this.gl.useProgram(this.program);
this.gl.renderer.state.currentProgram = this.id;
}
// Set only the active uniforms found in the shader
this.uniformLocations.forEach((location, activeUniform) => {
let uniform = this.uniforms[activeUniform.uniformName];
for (const component of activeUniform.nameComponents) {
if (!uniform) break;
if (component in uniform) {
uniform = uniform[component];
} else if (Array.isArray(uniform.value)) {
break;
} else {
uniform = undefined;
break;
}
}
if (!uniform) {
return warn(`Active uniform ${activeUniform.name} has not been supplied`);
}
if (uniform && uniform.value === undefined) {
return warn(`${activeUniform.name} uniform is missing a value parameter`);
}
if (uniform.value.texture) {
textureUnit = textureUnit + 1;
// Check if texture needs to be updated
uniform.value.update(textureUnit);
return setUniform(this.gl, activeUniform.type, location, textureUnit);
}
// For texture arrays, set uniform as an array of texture units instead of just one
if (uniform.value.length && uniform.value[0].texture) {
const textureUnits = [];
uniform.value.forEach((value) => {
textureUnit = textureUnit + 1;
value.update(textureUnit);
textureUnits.push(textureUnit);
});
return setUniform(this.gl, activeUniform.type, location, textureUnits);
}
setUniform(this.gl, activeUniform.type, location, uniform.value);
});
this.applyState();
if (flipFaces) this.gl.renderer.setFrontFace(this.frontFace === this.gl.CCW ? this.gl.CW : this.gl.CCW);
}
remove() {
this.gl.deleteProgram(this.program);
}
}
function setUniform(gl, type, location, value) {
value = value.length ? flatten(value) : value;
const setValue = gl.renderer.state.uniformLocations.get(location);
// Avoid redundant uniform commands
if (value.length) {
if (setValue === undefined || setValue.length !== value.length) {
// clone array to store as cache
gl.renderer.state.uniformLocations.set(location, value.slice(0));
} else {
if (arraysEqual(setValue, value)) return;
// Update cached array values
setValue.set ? setValue.set(value) : setArray(setValue, value);
gl.renderer.state.uniformLocations.set(location, setValue);
}
} else {
if (setValue === value) return;
gl.renderer.state.uniformLocations.set(location, value);
}
switch (type) {
case 5126:
return value.length ? gl.uniform1fv(location, value) : gl.uniform1f(location, value); // FLOAT
case 35664:
return gl.uniform2fv(location, value); // FLOAT_VEC2
case 35665:
return gl.uniform3fv(location, value); // FLOAT_VEC3
case 35666:
return gl.uniform4fv(location, value); // FLOAT_VEC4
case 35670: // BOOL
case 5124: // INT
case 35678: // SAMPLER_2D
case 36306: // U_SAMPLER_2D
case 35680: // SAMPLER_CUBE
case 36289: // SAMPLER_2D_ARRAY
return value.length ? gl.uniform1iv(location, value) : gl.uniform1i(location, value); // SAMPLER_CUBE
case 35671: // BOOL_VEC2
case 35667:
return gl.uniform2iv(location, value); // INT_VEC2
case 35672: // BOOL_VEC3
case 35668:
return gl.uniform3iv(location, value); // INT_VEC3
case 35673: // BOOL_VEC4
case 35669:
return gl.uniform4iv(location, value); // INT_VEC4
case 35674:
return gl.uniformMatrix2fv(location, false, value); // FLOAT_MAT2
case 35675:
return gl.uniformMatrix3fv(location, false, value); // FLOAT_MAT3
case 35676:
return gl.uniformMatrix4fv(location, false, value); // FLOAT_MAT4
}
}
function addLineNumbers(string) {
let lines = string.split('\n');
for (let i = 0; i < lines.length; i++) {
lines[i] = i + 1 + ': ' + lines[i];
}
return lines.join('\n');
}
function flatten(a) {
const arrayLen = a.length;
const valueLen = a[0].length;
if (valueLen === undefined) return a;
const length = arrayLen * valueLen;
let value = arrayCacheF32[length];
if (!value) arrayCacheF32[length] = value = new Float32Array(length);
for (let i = 0; i < arrayLen; i++) value.set(a[i], i * valueLen);
return value;
}
function arraysEqual(a, b) {
if (a.length !== b.length) return false;
for (let i = 0, l = a.length; i < l; i++) {
if (a[i] !== b[i]) return false;
}
return true;
}
function setArray(a, b) {
for (let i = 0, l = a.length; i < l; i++) {
a[i] = b[i];
}
}
let warnCount = 0;
function warn(message) {
if (warnCount > 100) return;
console.warn(message);
warnCount++;
if (warnCount > 100) console.warn('More than 100 program warnings - stopping logs.');
}