@thi.ng/webgl
Version:
WebGL & GLSL abstraction layer
404 lines (403 loc) • 12.2 kB
JavaScript
import { deref } from "@thi.ng/api/deref";
import { asGLType } from "@thi.ng/api/typedarray";
import { existsAndNotNull } from "@thi.ng/checks/exists-not-null";
import { isArray } from "@thi.ng/checks/is-array";
import { isBoolean } from "@thi.ng/checks/is-boolean";
import { isFunction } from "@thi.ng/checks/is-function";
import { unsupported } from "@thi.ng/errors/unsupported";
import { doOnce } from "@thi.ng/memoize/do-once";
import { GLSLVersion } from "@thi.ng/shader-ast-glsl/api";
import { targetGLSL } from "@thi.ng/shader-ast-glsl/target";
import { program } from "@thi.ng/shader-ast/ast/scope";
import { input, output, sym, uniform } from "@thi.ng/shader-ast/ast/sym";
import { vals } from "@thi.ng/transducers/vals";
import {
GL_EXT_INFO
} from "./api/ext.js";
import {
DEFAULT_OUTPUT
} from "./api/shader.js";
import { getExtensions } from "./canvas.js";
import { isGL2Context } from "./checks.js";
import { error } from "./error.js";
import { LOGGER } from "./logger.js";
import { GLSL_HEADER, NO_PREFIXES, SYNTAX } from "./syntax.js";
import { UNIFORM_SETTERS } from "./uniforms.js";
const ERROR_REGEXP = /ERROR: \d+:(\d+): (.*)/;
class Shader {
gl;
program;
attribs;
uniforms;
state;
warnAttrib = doOnce(
(id) => LOGGER.warn(
`unknown attrib: ${id} (no further warnings will be shown...)`
)
);
warnUni = doOnce(
(id) => LOGGER.warn(
`unknown uniform: ${id} (no further warnings will be shown...)`
)
);
constructor(gl, program2, attribs, uniforms, state) {
this.gl = gl;
this.program = program2;
this.attribs = attribs;
this.uniforms = uniforms;
this.state = state || {};
}
/**
* Returns a shallow copy of this shader with its state config merged with
* given options (priority). Useful for re-using a shader, but applying
* different settings.
*
* @param state
*/
withState(state) {
return new Shader(this.gl, this.program, this.attribs, this.uniforms, {
...this.state,
...state
});
}
bind(spec) {
if (this.program) {
this.gl.useProgram(this.program);
this.bindAttribs(spec.attribs);
this.bindUniforms(spec.uniforms);
return true;
}
return false;
}
unbind() {
let shaderAttrib;
for (const id in this.attribs) {
if (shaderAttrib = this.attribs[id]) {
this.gl.disableVertexAttribArray(shaderAttrib.loc);
}
}
this.gl.useProgram(null);
return true;
}
release() {
if (this.program) {
this.gl.deleteProgram(this.program);
delete this.program;
return true;
}
return false;
}
bindAttribs(specAttribs) {
const gl = this.gl;
let shaderAttrib;
for (const id in specAttribs) {
if (shaderAttrib = this.attribs[id]) {
const attr = specAttribs[id];
attr.buffer.bind();
gl.enableVertexAttribArray(shaderAttrib.loc);
gl.vertexAttribPointer(
shaderAttrib.loc,
attr.size || 3,
asGLType(attr.type || gl.FLOAT),
attr.normalized || false,
attr.stride || 0,
attr.offset || 0
);
} else {
this.warnAttrib(id);
}
}
}
bindUniforms(specUnis = {}) {
const shaderUnis = this.uniforms;
for (const id in specUnis) {
const u = shaderUnis[id];
if (u) {
let val = specUnis[id];
val = isFunction(val) ? val(shaderUnis, specUnis) : deref(val);
u.setter(val);
} else {
this.warnUni(id);
}
}
for (const id in shaderUnis) {
if (shaderUnis.hasOwnProperty(id) && (!specUnis || !existsAndNotNull(specUnis[id]))) {
const u = shaderUnis[id];
const val = u.defaultFn ? u.defaultFn(shaderUnis, specUnis) : u.defaultVal;
u.setter(val);
}
}
}
prepareState(state = this.state) {
const gl = this.gl;
state.depth !== void 0 && this.setState(gl.DEPTH_TEST, state.depth);
if (state.cull !== void 0) {
this.setState(gl.CULL_FACE, state.cull);
state.cullMode && gl.cullFace(state.cullMode);
}
if (state.blend !== void 0) {
this.setState(gl.BLEND, state.blend);
state.blendFn && gl.blendFunc(state.blendFn[0], state.blendFn[1]);
state.blendEq !== void 0 && gl.blendEquation(state.blendEq);
}
if (state.stencil !== void 0) {
this.setState(gl.STENCIL_TEST, state.stencil);
state.stencilFn && gl.stencilFunc(
state.stencilFn[0],
state.stencilFn[1],
state.stencilFn[2]
);
state.stencilOp && gl.stencilOp(
state.stencilOp[0],
state.stencilOp[1],
state.stencilOp[2]
);
state.stencilMask !== void 0 && gl.stencilMask(state.stencilMask);
}
}
setState(id, val) {
if (val) {
this.gl.enable(id);
} else {
this.gl.disable(id);
}
}
}
const defShader = (gl, spec, opts) => {
const version = isGL2Context(gl) ? GLSLVersion.GLES_300 : GLSLVersion.GLES_100;
const srcVS = isFunction(spec.vs) ? shaderSourceFromAST(spec, "vs", version, opts) : prepareShaderSource(spec, "vs", version);
const srcFS = isFunction(spec.fs) ? shaderSourceFromAST(spec, "fs", version, opts) : prepareShaderSource(spec, "fs", version);
const logger = opts?.logger || LOGGER;
logger.debug(srcVS);
logger.debug(srcFS);
spec.ext && __initShaderExtensions(gl, spec.ext);
const vs = compileShader(gl, gl.VERTEX_SHADER, srcVS);
const fs = compileShader(gl, gl.FRAGMENT_SHADER, srcFS);
const program2 = gl.createProgram() || error("error creating shader program");
gl.attachShader(program2, vs);
gl.attachShader(program2, fs);
gl.linkProgram(program2);
if (gl.getProgramParameter(program2, gl.LINK_STATUS)) {
const attribs = __initAttributes(gl, program2, spec.attribs);
const uniforms = __initUniforms(gl, program2, spec.uniforms);
gl.deleteShader(vs);
gl.deleteShader(fs);
return new Shader(gl, program2, attribs, uniforms, spec.state);
}
throw new Error(`Error linking shader: ${gl.getProgramInfoLog(program2)}`);
};
const __compileVars = (attribs, syntax, prefixes) => {
let decls = [];
for (const id in attribs) {
if (attribs.hasOwnProperty(id)) {
decls.push(syntax(id, attribs[id], prefixes));
}
}
decls.push("");
return decls.join("\n");
};
const __compileExtensionPragma = (id, behavior, version) => {
const ext = GL_EXT_INFO[id];
const gl2 = version === GLSLVersion.GLES_300;
return ext && (!gl2 && ext.gl || gl2 && ext.gl2) ? `#extension ${ext && ext.alias || id} : ${isBoolean(behavior) ? behavior ? "enable" : "disable" : behavior}
` : "";
};
const __initShaderExtensions = (gl, exts) => {
for (const id in exts) {
const state = exts[id];
if (state === true || state === "require") {
getExtensions(gl, [id], state === "require");
}
}
};
const __compilePrelude = (spec, version) => {
let prelude = spec.pre ? spec.replacePrelude ? spec.pre : spec.pre + "\n" + GLSL_HEADER : GLSL_HEADER;
if (spec.ext) {
for (const id in spec.ext) {
prelude += __compileExtensionPragma(
id,
spec.ext[id],
version
);
}
}
return prelude;
};
const __compileIODecls = (decl, src, dest) => {
for (const id in src) {
const a = src[id];
dest[id] = isArray(a) ? decl(a[0], id, { loc: a[1] }) : decl(a, id);
}
};
const __varyingOpts = (v) => {
const [vtype, opts] = isArray(v) ? [v[0], { num: v[1] }] : [v, {}];
/(u?int|[ui]vec[234])/.test(vtype) && (opts.smooth = "flat");
return [vtype, opts];
};
const __compileVaryingDecls = (spec, decl, acc) => {
for (const id in spec.varying) {
const [vtype, opts] = __varyingOpts(spec.varying[id]);
acc[id] = decl(vtype, id, opts);
}
};
const __compileUniformDecls = (spec, acc) => {
for (const id in spec.uniforms) {
const u = spec.uniforms[id];
acc[id] = isArray(u) ? uniform(
u[0],
id,
u[0].indexOf("[]") > 0 ? { num: u[1] } : void 0
) : uniform(u, id);
}
};
const shaderSourceFromAST = (spec, type, version, opts = {}) => {
let prelude = __compilePrelude(spec, version);
const inputs = {};
const outputs = {};
const outputAliases = {};
const unis = {};
spec.uniforms && __compileUniformDecls(spec, unis);
if (type === "vs") {
__compileIODecls(input, spec.attribs, inputs);
spec.varying && __compileVaryingDecls(spec, output, outputs);
} else {
spec.varying && __compileVaryingDecls(spec, input, inputs);
const outs = spec.outputs || DEFAULT_OUTPUT;
if (version >= GLSLVersion.GLES_300) {
__compileIODecls(output, outs, outputs);
} else {
for (const id in outs) {
const o = outs[id];
if (isArray(o) && o[0] === "vec4") {
prelude += `#define ${id} gl_FragData[${o[1]}]
`;
outputAliases[id] = sym("vec4", id);
} else {
unsupported(`GLSL ${version} doesn't support output vars`);
}
}
}
}
const target = targetGLSL({
type,
version,
prelude,
prec: opts.prec
});
return target(
program([
...vals(unis),
...vals(inputs),
...vals(outputs),
...spec[type](target, unis, inputs, {
...outputs,
...outputAliases
})
])
) + (spec.post ? "\n" + spec.post : "");
};
const prepareShaderSource = (spec, type, version) => {
const syntax = SYNTAX[version];
const prefixes = { ...NO_PREFIXES, ...spec.declPrefixes };
const isVS = type === "vs";
let src = "";
src += `#version ${version}
`;
src += __compilePrelude(spec, version);
if (spec.generateDecls !== false) {
src += isVS ? __compileVars(spec.attribs, syntax.attrib, prefixes) : __compileVars(
spec.outputs || DEFAULT_OUTPUT,
syntax.output,
prefixes
);
src += __compileVars(spec.varying, syntax.varying[type], prefixes);
src += __compileVars(spec.uniforms, syntax.uniform, prefixes);
}
src += spec[type];
spec.post && (src += "\n" + spec.post);
return src;
};
const compileShader = (gl, type, src) => {
const shader = gl.createShader(type) || error("error creating shader");
gl.shaderSource(shader, src);
gl.compileShader(shader);
if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
return shader;
}
return __parseAndThrowShaderError(gl, shader, src);
};
const __parseAndThrowShaderError = (gl, shader, src) => {
const lines = src.split("\n");
const log = gl.getShaderInfoLog(shader).split("\n");
const errors = log.map((line) => {
const matches = ERROR_REGEXP.exec(line);
const ln = matches ? matches[1] : null;
if (ln) {
return `line ${ln}: ${matches[2]}
${lines[parseInt(ln) - 1]}`;
}
}).filter(existsAndNotNull).join("\n");
return error(`Error compiling shader:
${errors}`);
};
const __initAttributes = (gl, prog, attribs) => {
const res = {};
for (const id in attribs) {
const val = attribs[id];
const [type, loc] = isArray(val) ? val : [val, null];
const aid = id;
if (loc != null) {
gl.bindAttribLocation(prog, loc, aid);
res[id] = { type, loc };
} else {
res[id] = {
type,
loc: gl.getAttribLocation(prog, aid)
};
}
}
return res;
};
const __initUniforms = (gl, prog, uniforms = {}) => {
const res = {};
for (const id in uniforms) {
const val = uniforms[id];
let type;
let t1, t2, defaultVal, defaultFn;
if (isArray(val)) {
[type, t1, t2] = val;
defaultVal = type.indexOf("[]") < 0 ? t1 : t2;
if (isFunction(defaultVal)) {
defaultFn = defaultVal;
defaultVal = void 0;
}
} else {
type = val;
}
const loc = gl.getUniformLocation(prog, id);
if (loc != null) {
const setter = UNIFORM_SETTERS[type];
if (setter) {
res[id] = {
loc,
setter: setter(gl, loc, defaultVal),
defaultFn,
defaultVal,
type
};
} else {
error(`invalid uniform type: ${type}`);
}
} else {
LOGGER.warn(`unknown uniform: ${id}`);
}
}
return res;
};
export {
Shader,
compileShader,
defShader,
prepareShaderSource,
shaderSourceFromAST
};