UNPKG

@thi.ng/webgl

Version:

WebGL & GLSL abstraction layer

206 lines (205 loc) 6.11 kB
import { assert } from "@thi.ng/errors/assert"; import { S2D, S3D, V2, V4 } from "@thi.ng/shader-ast/api/types"; import { assign } from "@thi.ng/shader-ast/ast/assign"; import { defMain } from "@thi.ng/shader-ast/ast/function"; import { INT0, ivec2 } from "@thi.ng/shader-ast/ast/lit"; import { $xy } from "@thi.ng/shader-ast/ast/swizzle"; import { texelFetch } from "@thi.ng/shader-ast/builtin/texture"; import { mapIndexed } from "@thi.ng/transducers"; import { assocObj } from "@thi.ng/transducers/assoc-obj"; import { map } from "@thi.ng/transducers/map"; import { range } from "@thi.ng/transducers/range"; import { some } from "@thi.ng/transducers/some"; import { transduce } from "@thi.ng/transducers/transduce"; import { TextureFilter, TextureRepeat, TextureTarget } from "./api/texture.js"; import { compileModel } from "./buffer.js"; import { isFloatTexture, isGL2Context } from "./checks.js"; import { draw } from "./draw.js"; import { defFBO } from "./fbo.js"; import { defQuadModel } from "./geo/quad.js"; import { defShader } from "./shader.js"; import { PASSTHROUGH_VS } from "./shaders/pipeline.js"; import { defTexture } from "./texture.js"; const defMultiPass = (opts) => { const gl = opts.gl; const numPasses = opts.passes.length; assert(numPasses > 0, "require at least one shader pass"); const useMainBuffer = !opts.passes[numPasses - 1].outputs.length; const textures = __initTextures(opts); const passes = __initPasses(opts, textures); const fbos = __initBuffers(opts, textures, useMainBuffer); const drawPass = (i, time, isFBO = true) => { isFBO && fbos[i].bind(); const spec = opts.passes[i]; const pass = passes[i]; const shader = pass.shader; const size = spec.outputs.length ? textures[spec.outputs[0]].size : [gl.drawingBufferWidth, gl.drawingBufferHeight]; shader.uniforms.resolution && (pass.uniforms.resolution = size); shader.uniforms.time && (pass.uniforms.time = time); gl.viewport(0, 0, size[0], size[1]); draw(pass); isFBO && fbos[i].unbind(); }; const update = (time) => { for (let i = 0; i < fbos.length; i++) { drawPass(i, time); } useMainBuffer && drawPass(numPasses - 1, time, false); }; const updateRAF = () => { update((Date.now() - t0) * 1e-3); active && (rafID = requestAnimationFrame(updateRAF)); }; let active; let t0 = Date.now(); let rafID; const instance = { start() { t0 = Date.now(); active = true; rafID = requestAnimationFrame(updateRAF); }, stop() { if (active) { active = false; cancelAnimationFrame(rafID); } }, update(time) { update(time); }, singlePass(i, time) { drawPass(i, time, i < fbos.length); }, passes: opts.passes, fbos, models: passes, textures }; return instance; }; const __initPasses = (opts, textures) => { const gl = opts.gl; const model = compileModel(gl, defQuadModel({ uv: false })); return opts.passes.map((pass) => { const m = pass.model ? compileModel(gl, pass.model) : { ...model }; m.shader = __initShader(gl, pass, textures); m.uniforms = { ...pass.uniformVals }; pass.inputs.length > 0 && (m.textures = pass.inputs.map((id) => textures[id])); return m; }); }; const TEX_TYPE_MAP = { [TextureTarget.TEXTURE_2D]: S2D, [TextureTarget.TEXTURE_3D]: S3D, [TextureTarget.TEXTURE_CUBE_MAP]: "samplerCube" }; const __initShader = (gl, pass, textures) => { const isGL2 = isGL2Context(gl); const numOuts = pass.outputs.length; const ext = { ...pass.ext }; const spec = { vs: pass.vs || PASSTHROUGH_VS, fs: pass.fs, attribs: pass.attribs || { position: V2 }, varying: pass.varying, uniforms: { ...pass.uniforms, ...transduce( mapIndexed( (i, id) => [ `input${i}`, [TEX_TYPE_MAP[textures[id].target], i] ] ), assocObj(), pass.inputs ) }, outputs: numOuts ? transduce( map( (i) => [`output${i}`, [V4, i]] ), assocObj(), range(numOuts) ) : void 0, state: pass.state, pre: pass.pre, post: pass.post, replacePrelude: pass.replacePrelude, generateDecls: pass.generateDecls, ext }; const floatIn = some((id) => isFloatTexture(textures[id]), pass.inputs); const floatOut = some((id) => isFloatTexture(textures[id]), pass.outputs); if (!isGL2) { floatIn && (ext.OES_texture_float = "require"); numOuts > 1 && (ext.WEBGL_draw_buffers = "require"); } if (floatOut) { ext[isGL2 ? "EXT_color_buffer_float" : "WEBGL_color_buffer_float"] = "require"; isGL2 && (ext["EXT_float_blend"] = "require"); } return defShader(gl, spec); }; const __initTextures = (opts) => Object.keys(opts.textures).reduce((acc, id) => { acc[id] = defTexture(opts.gl, { width: opts.width, height: opts.height, depth: opts.depth, filter: TextureFilter.NEAREST, wrap: TextureRepeat.CLAMP, image: null, ...opts.textures[id] }); return acc; }, {}); const __initBuffers = (opts, textures, useMainBuffer) => (useMainBuffer ? opts.passes.slice(0, opts.passes.length - 1) : opts.passes).map( (pass) => defFBO(opts.gl, { tex: pass.outputs.map((id) => textures[id]) }) ); const passCopy = (src, dest) => { assert( src.length === dest.length, `require same number of in/out textures` ); return { fs: (gl, unis, _, outs) => [ defMain(() => [ ...map( (i) => assign( outs[`output${i}`], texelFetch( unis[`input${i}`], ivec2($xy(gl.gl_FragCoord)), INT0 ) ), range(src.length) ) ]) ], inputs: src, outputs: dest }; }; const passCopyMain = (src) => ({ fs: (gl, unis, _, outs) => [ defMain(() => [ assign( outs.fragColor, texelFetch(unis.input0, ivec2($xy(gl.gl_FragCoord)), INT0) ) ]) ], inputs: [src], outputs: [] }); export { defMultiPass, passCopy, passCopyMain };