diff --git a/packages/shader/index.mjs b/packages/shader/index.mjs index 541e86fde..41aa0e1de 100644 --- a/packages/shader/index.mjs +++ b/packages/shader/index.mjs @@ -1,234 +1,2 @@ -/* -shader.mjs - implements the `loadShader` helper and `shader` pattern function -Copyright (C) 2024 Strudel contributors -This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . -*/ - -import { PicoGL } from 'picogl'; -import { register, logger } from '@strudel/core'; - -// The standard fullscreen vertex shader. -const vertexShader = `#version 300 es -precision highp float; -layout(location=0) in vec2 position; -void main() { - gl_Position = vec4(position, 1, 1); -} -`; - -// Make the fragment source, similar to the one from shadertoy. -function mkFragmentShader(code) { - return `#version 300 es -precision highp float; -out vec4 oColor; -uniform float iTime; -uniform vec2 iResolution; - -${code} - -void main(void) { - mainImage(oColor, gl_FragCoord.xy); -} -`; -} - -// Modulation helpers. -const hardModulation = () => { - let val = 0; - return { - get: () => val, - set: (v) => { - val = v; - }, - }; -}; - -const decayModulation = (decay) => { - let val = 0; - let desired = 0; - return { - get: (ts) => { - val += (desired - val) / decay; - return val; - }, - set: (v) => { - desired = val + v; - }, - }; -}; - -// Set an uniform value (from a pattern). -function setUniform(instance, name, value, position) { - const uniform = instance.uniforms[name]; - if (uniform) { - if (uniform.count == 0) { - // This is a single value - uniform.mod.set(value); - } else { - // This is an array - const idx = position % uniform.mod.length; - uniform.mod[idx].set(value); - } - } else { - logger('[shader] unknown uniform: ' + name); - } - - // Ensure the instance is drawn - instance.age = 0; - if (!instance.drawing) { - instance.drawing = requestAnimationFrame(instance.update); - } -} - -// Update the uniforms for a given drawFrame call. -function updateUniforms(drawFrame, elapsed, uniforms) { - Object.values(uniforms).forEach((uniform) => { - const value = - uniform.count == 0 ? uniform.mod.get(elapsed) : uniform.value.map((_, i) => uniform.mod[i].get(elapsed)); - // Send the value to the GPU - drawFrame.uniform(uniform.name, value); - }); -} - -// Setup the instance's uniform after shader compilation. -function setupUniforms(uniforms, program) { - Object.entries(program.uniforms).forEach(([name, uniform]) => { - if (name != 'iTime' && name != 'iResolution') { - // remove array suffix - const uname = name.replace('[0]', ''); - const count = uniform.count | 0; - if (!uniforms[uname] || uniforms[uname].count != count) { - // TODO: keep the previous value when the count change... - uniforms[uname] = { - name, - count, - value: count == 0 ? 0 : new Float32Array(count), - mod: count == 0 ? decayModulation(50) : new Array(count).fill().map(() => decayModulation(50)), - }; - } - } - }); - // TODO: remove previous uniform that are no longer used... - return uniforms; -} - -// Setup the canvas and return the WebGL context. -function setupCanvas(name) { - // TODO: support custom size - const width = 400; - const height = 300; - const canvas = document.createElement('canvas'); - canvas.id = 'cnv-' + name; - canvas.width = width; - canvas.height = height; - const top = 60 + Object.keys(_instances).length * height; - canvas.style = `pointer-events:none;width:${width}px;height:${height}px;position:fixed;top:${top}px;right:23px`; - document.body.append(canvas); - return canvas.getContext('webgl2'); -} - -// Setup the shader instance -async function initializeShaderInstance(name, code) { - // Setup PicoGL app - const ctx = setupCanvas(name); - console.log(ctx); - const app = PicoGL.createApp(ctx); - app.resize(400, 300); - - // Setup buffers - const resolution = new Float32Array([ctx.canvas.width, ctx.canvas.height]); - - // Two triangle to cover the whole canvas - const positionBuffer = app.createVertexBuffer( - PicoGL.FLOAT, - 2, - new Float32Array([-1, -1, -1, 1, 1, 1, 1, 1, 1, -1, -1, -1]), - ); - - // Setup the arrays - const arrays = app.createVertexArray().vertexAttributeBuffer(0, positionBuffer); - - return app - .createPrograms([vertexShader, code]) - .then(([program]) => { - const drawFrame = app.createDrawCall(program, arrays); - const instance = { app, code, program, arrays, drawFrame, uniforms: setupUniforms({}, program) }; - - // Render frame logic - let prev = performance.now() / 1000; - instance.age = 0; - instance.update = () => { - const now = performance.now() / 1000; - const elapsed = now - prev; - prev = now; - // console.log("drawing!") - app.clear(); - instance.drawFrame.uniform('iResolution', resolution).uniform('iTime', now); - - updateUniforms(instance.drawFrame, elapsed, instance.uniforms); - - instance.drawFrame.draw(); - if (instance.age++ < 100) requestAnimationFrame(instance.update); - else instance.drawing = false; - }; - return instance; - }) - .catch((err) => { - ctx.canvas.remove(); - throw err; - }); -} - -// Update the instance program -async function reloadShaderInstanceCode(instance, code) { - return instance.app.createPrograms([vertexShader, code]).then(([program]) => { - instance.program.delete(); - instance.program = program; - instance.uniforms = setupUniforms(instance.uniforms, program); - instance.drawFrame = instance.app.createDrawCall(program, instance.arrays); - }); -} - -// Keep track of the running shader instances -let _instances = {}; -export async function loadShader(code = '', name = 'default') { - if (code) { - code = mkFragmentShader(code); - } - if (!_instances[name]) { - _instances[name] = await initializeShaderInstance(name, code); - logger('[shader] ready'); - } else if (_instances[name].code != code) { - await reloadShaderInstanceCode(_instances[name], code); - logger('[shader] reloaded'); - } -} - -export const shader = register('shader', (options, pat) => { - // Keep track of the pitches value: Map String Int - const pitches = { _count: 0 }; - - return pat.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => { - const instance = _instances[options.instance || 'default']; - if (!instance) { - logger('[shader] not loaded yet', 'warning'); - return; - } - - const value = options.gain || 1.0; - if (options.pitch !== undefined) { - const note = hap.value.note || hap.value.s; - if (pitches[note] === undefined) { - // Assign new value, the first note gets 0, then 1, then 2, ... - pitches[note] = Object.keys(pitches).length; - } - setUniform(instance, options.pitch, value, pitches[note]); - } else if (options.seq !== undefined) { - setUniform(instance, options.seq, value, pitches._count++); - } else if (options.uniform !== undefined) { - setUniform(instance, options.uniform, value); - } else { - console.error('Unknown shader options, need either pitch or uniform', options); - } - }, false); -}); +export {loadShader} from './shader.mjs'; +export * from './uniform.mjs'; diff --git a/packages/shader/shader.mjs b/packages/shader/shader.mjs new file mode 100644 index 000000000..e81a0cf50 --- /dev/null +++ b/packages/shader/shader.mjs @@ -0,0 +1,211 @@ +/* +shader.mjs - implements the `loadShader` function +Copyright (C) 2024 Strudel contributors +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ + +import { PicoGL } from 'picogl'; +import { logger } from '@strudel/core'; + +// The standard fullscreen vertex shader. +const vertexShader = `#version 300 es +precision highp float; +layout(location=0) in vec2 position; +void main() { + gl_Position = vec4(position, 1, 1); +} +`; + +// Make the fragment source, similar to the one from shadertoy. +function mkFragmentShader(code) { + return `#version 300 es +precision highp float; +out vec4 oColor; +uniform float iTime; +uniform vec2 iResolution; + +${code} + +void main(void) { + mainImage(oColor, gl_FragCoord.xy); +} +`; +} + +// Modulation helpers. +const hardModulation = () => { + let val = 0; + return { + get: () => val, + set: (v) => { + val = v; + }, + }; +}; + +const decayModulation = (decay) => { + let val = 0; + let desired = 0; + return { + get: (ts) => { + val += (desired - val) / decay; + return val; + }, + set: (v) => { + desired = val + v; + }, + }; +}; + +// Set an uniform value (from a pattern). +export function setUniform(instanceName, name, value, position) { + const instance = _instances[instanceName || 'default']; + if (!instance) { + logger('[shader] not loaded yet', 'warning'); + return; + } + + const uniform = instance.uniforms[name]; + if (uniform) { + if (uniform.count == 0) { + // This is a single value + uniform.mod.set(value); + } else { + // This is an array + const idx = position % uniform.mod.length; + uniform.mod[idx].set(value); + } + } else { + logger('[shader] unknown uniform: ' + name); + } + + // Ensure the instance is drawn + instance.age = 0; + if (!instance.drawing) { + instance.drawing = requestAnimationFrame(instance.update); + } +} + +// Update the uniforms for a given drawFrame call. +function updateUniforms(drawFrame, elapsed, uniforms) { + Object.values(uniforms).forEach((uniform) => { + const value = + uniform.count == 0 ? uniform.mod.get(elapsed) : uniform.value.map((_, i) => uniform.mod[i].get(elapsed)); + // Send the value to the GPU + drawFrame.uniform(uniform.name, value); + }); +} + +// Setup the instance's uniform after shader compilation. +function setupUniforms(uniforms, program) { + Object.entries(program.uniforms).forEach(([name, uniform]) => { + if (name != 'iTime' && name != 'iResolution') { + // remove array suffix + const uname = name.replace('[0]', ''); + const count = uniform.count | 0; + if (!uniforms[uname] || uniforms[uname].count != count) { + // TODO: keep the previous value when the count change... + uniforms[uname] = { + name, + count, + value: count == 0 ? 0 : new Float32Array(count), + mod: count == 0 ? decayModulation(50) : new Array(count).fill().map(() => decayModulation(50)), + }; + } + } + }); + // TODO: remove previous uniform that are no longer used... + return uniforms; +} + +// Setup the canvas and return the WebGL context. +function setupCanvas(name) { + // TODO: support custom size + const width = 400; + const height = 300; + const canvas = document.createElement('canvas'); + canvas.id = 'cnv-' + name; + canvas.width = width; + canvas.height = height; + const top = 60 + Object.keys(_instances).length * height; + canvas.style = `pointer-events:none;width:${width}px;height:${height}px;position:fixed;top:${top}px;right:23px`; + document.body.append(canvas); + return canvas.getContext('webgl2'); +} + +// Setup the shader instance +async function initializeShaderInstance(name, code) { + // Setup PicoGL app + const ctx = setupCanvas(name); + console.log(ctx); + const app = PicoGL.createApp(ctx); + app.resize(400, 300); + + // Setup buffers + const resolution = new Float32Array([ctx.canvas.width, ctx.canvas.height]); + + // Two triangle to cover the whole canvas + const positionBuffer = app.createVertexBuffer( + PicoGL.FLOAT, + 2, + new Float32Array([-1, -1, -1, 1, 1, 1, 1, 1, 1, -1, -1, -1]), + ); + + // Setup the arrays + const arrays = app.createVertexArray().vertexAttributeBuffer(0, positionBuffer); + + return app + .createPrograms([vertexShader, code]) + .then(([program]) => { + const drawFrame = app.createDrawCall(program, arrays); + const instance = { app, code, program, arrays, drawFrame, uniforms: setupUniforms({}, program) }; + + // Render frame logic + let prev = performance.now() / 1000; + instance.age = 0; + instance.update = () => { + const now = performance.now() / 1000; + const elapsed = now - prev; + prev = now; + // console.log("drawing!") + app.clear(); + instance.drawFrame.uniform('iResolution', resolution).uniform('iTime', now); + + updateUniforms(instance.drawFrame, elapsed, instance.uniforms); + + instance.drawFrame.draw(); + if (instance.age++ < 100) requestAnimationFrame(instance.update); + else instance.drawing = false; + }; + return instance; + }) + .catch((err) => { + ctx.canvas.remove(); + throw err; + }); +} + +// Update the instance program +async function reloadShaderInstanceCode(instance, code) { + return instance.app.createPrograms([vertexShader, code]).then(([program]) => { + instance.program.delete(); + instance.program = program; + instance.uniforms = setupUniforms(instance.uniforms, program); + instance.drawFrame = instance.app.createDrawCall(program, instance.arrays); + }); +} + +// Keep track of the running shader instances +let _instances = {}; +export async function loadShader(code = '', name = 'default') { + if (code) { + code = mkFragmentShader(code); + } + if (!_instances[name]) { + _instances[name] = await initializeShaderInstance(name, code); + logger('[shader] ready'); + } else if (_instances[name].code != code) { + await reloadShaderInstanceCode(_instances[name], code); + logger('[shader] reloaded'); + } +} diff --git a/packages/shader/uniform.mjs b/packages/shader/uniform.mjs new file mode 100644 index 000000000..e1ae7f732 --- /dev/null +++ b/packages/shader/uniform.mjs @@ -0,0 +1,33 @@ +/* +uniform.mjs - implements the `uniform` pattern function +Copyright (C) 2024 Strudel contributors +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ + +import { register, logger } from '@strudel/core'; +import { setUniform } from './shader.mjs'; + +export const uniform = register('uniform', (options, pat) => { + // Keep track of the pitches value: Map String Int + const pitches = { _count: 0 }; + + return pat.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => { + const instance = options.instance; + + const value = options.gain || 1.0; + if (options.pitch !== undefined) { + const note = hap.value.note || hap.value.s; + if (pitches[note] === undefined) { + // Assign new value, the first note gets 0, then 1, then 2, ... + pitches[note] = Object.keys(pitches).length; + } + setUniform(instance, options.pitch, value, pitches[note]); + } else if (options.seq !== undefined) { + setUniform(instance, options.seq, value, pitches._count++); + } else if (options.uniform !== undefined) { + setUniform(instance, options.uniform, value); + } else { + console.error('Unknown shader options, need either pitch or uniform', options); + } + }, false); +});