UNPKG

@thi.ng/webgl

Version:

WebGL & GLSL abstraction layer

404 lines (403 loc) 12.2 kB
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 };