@thi.ng/webgl
Version:
WebGL & GLSL abstraction layer
206 lines (205 loc) • 6.11 kB
JavaScript
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
};