From f0ddb2d595ea97744aeaa39f3a379bcf5dbb44e3 Mon Sep 17 00:00:00 2001 From: Tristan de Cacqueray Date: Sat, 28 Sep 2024 14:56:15 -0400 Subject: [PATCH 01/21] Add initial shader module This change adds the `loadShader` and `shader` function, to be used like this: ```strudel await loadShader` // The modulation targets uniform float iColor; void mainImage( out vec4 fragColor, in vec2 fragCoord ) { vec2 uv = fragCoord / iResolution.xy; vec3 col = 0.5 + 0.5*cos(iColor+uv.xyx+vec3(0,2,4)); fragColor = vec4(col, 0); } ` $: s("bd").shader({uniform: 'iColor'}) ``` --- packages/draw/index.mjs | 1 + packages/draw/package.json | 3 +- packages/draw/shader.mjs | 230 +++++++++++++++++++++++++++++++++++++ 3 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 packages/draw/shader.mjs diff --git a/packages/draw/index.mjs b/packages/draw/index.mjs index 506c6151d..98ac989af 100644 --- a/packages/draw/index.mjs +++ b/packages/draw/index.mjs @@ -2,5 +2,6 @@ export * from './animate.mjs'; export * from './color.mjs'; export * from './draw.mjs'; export * from './pianoroll.mjs'; +export * from './shader.mjs'; export * from './spiral.mjs'; export * from './pitchwheel.mjs'; diff --git a/packages/draw/package.json b/packages/draw/package.json index a6a88ecfa..b65817504 100644 --- a/packages/draw/package.json +++ b/packages/draw/package.json @@ -29,7 +29,8 @@ }, "homepage": "https://github.com/tidalcycles/strudel#readme", "dependencies": { - "@strudel/core": "workspace:*" + "@strudel/core": "workspace:*", + "picogl": "^0.17.9" }, "devDependencies": { "vite": "^5.0.10" diff --git a/packages/draw/shader.mjs b/packages/draw/shader.mjs new file mode 100644 index 000000000..a3dd6a81d --- /dev/null +++ b/packages/draw/shader.mjs @@ -0,0 +1,230 @@ +/* +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.draw = 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) +}) From 1b8998ac14cafa6c192830934de9c421f2d5de21 Mon Sep 17 00:00:00 2001 From: Tristan de Cacqueray Date: Fri, 18 Oct 2024 18:15:15 -0400 Subject: [PATCH 02/21] Fix shader reload The attribute for the draw call is called drawFrame, not draw. --- packages/draw/shader.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/draw/shader.mjs b/packages/draw/shader.mjs index a3dd6a81d..518548b8f 100644 --- a/packages/draw/shader.mjs +++ b/packages/draw/shader.mjs @@ -181,7 +181,7 @@ async function reloadShaderInstanceCode(instance, code) { instance.program.delete() instance.program = program instance.uniforms = setupUniforms(instance.uniforms, program) - instance.draw = instance.app.createDrawCall(program, instance.arrays) + instance.drawFrame = instance.app.createDrawCall(program, instance.arrays) }) } From 13168bb9bc753347988b367e7d8e0fde8e5e9b16 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 19 Oct 2024 01:15:26 +0200 Subject: [PATCH 03/21] codeformat --- packages/draw/shader.mjs | 198 ++++++++++++++++++++------------------- 1 file changed, 101 insertions(+), 97 deletions(-) diff --git a/packages/draw/shader.mjs b/packages/draw/shader.mjs index 518548b8f..541e86fde 100644 --- a/packages/draw/shader.mjs +++ b/packages/draw/shader.mjs @@ -4,7 +4,7 @@ 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 { PicoGL } from 'picogl'; import { register, logger } from '@strudel/core'; // The standard fullscreen vertex shader. @@ -29,7 +29,7 @@ ${code} void main(void) { mainImage(oColor, gl_FragCoord.xy); } -` +`; } // Modulation helpers. @@ -37,178 +37,182 @@ const hardModulation = () => { let val = 0; return { get: () => val, - set: (v) => { val = v }, - } -} + set: (v) => { + val = v; + }, + }; +}; const decayModulation = (decay) => { let val = 0; - let desired = 0 + let desired = 0; return { get: (ts) => { - val += (desired - val) / decay - return val + val += (desired - val) / decay; + return val; }, - set: (v) => { desired = val + v }, - } -} + set: (v) => { + desired = val + v; + }, + }; +}; // Set an uniform value (from a pattern). function setUniform(instance, name, value, position) { - const uniform = instance.uniforms[name] + const uniform = instance.uniforms[name]; if (uniform) { if (uniform.count == 0) { // This is a single value - uniform.mod.set(value) + uniform.mod.set(value); } else { // This is an array - const idx = position % uniform.mod.length - uniform.mod[idx].set(value) + const idx = position % uniform.mod.length; + uniform.mod[idx].set(value); } } else { - logger('[shader] unknown uniform: ' + name) + logger('[shader] unknown uniform: ' + name); } // Ensure the instance is drawn - instance.age = 0 + instance.age = 0; if (!instance.drawing) { - instance.drawing = requestAnimationFrame(instance.update) + 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)) + 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) - }) + 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") { + if (name != 'iTime' && name != 'iResolution') { // remove array suffix - const uname = name.replace("[0]", "") - const count = uniform.count | 0 + 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)) - } + mod: count == 0 ? decayModulation(50) : new Array(count).fill().map(() => decayModulation(50)), + }; } } - }) + }); // TODO: remove previous uniform that are no longer used... - return uniforms + return uniforms; } // Setup the canvas and return the WebGL context. function setupCanvas(name) { // TODO: support custom size - const width = 400; const height = 300; + 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.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") + 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 ctx = setupCanvas(name); + console.log(ctx); const app = PicoGL.createApp(ctx); - app.resize(400, 300) + app.resize(400, 300); // Setup buffers - const resolution = new Float32Array([ctx.canvas.width, ctx.canvas.height]) + 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, - ])) + 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 - }) + 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) - }) + 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 = {} +let _instances = {}; export async function loadShader(code = '', name = 'default') { if (code) { - code = mkFragmentShader(code) + code = mkFragmentShader(code); } if (!_instances[name]) { - _instances[name] = await initializeShaderInstance(name, code) - logger('[shader] ready') + _instances[name] = await initializeShaderInstance(name, code); + logger('[shader] ready'); } else if (_instances[name].code != code) { - await reloadShaderInstanceCode(_instances[name], code) - logger('[shader] reloaded') + 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}; + const pitches = { _count: 0 }; return pat.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => { - const instance = _instances[options.instance || "default"] + const instance = _instances[options.instance || 'default']; if (!instance) { - logger('[shader] not loaded yet', 'warning') - return + logger('[shader] not loaded yet', 'warning'); + return; } const value = options.gain || 1.0; @@ -216,15 +220,15 @@ export const shader = register('shader', (options, pat) => { 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 + pitches[note] = Object.keys(pitches).length; } - setUniform(instance, options.pitch, value, pitches[note]) + setUniform(instance, options.pitch, value, pitches[note]); } else if (options.seq !== undefined) { - setUniform(instance, options.seq, value, pitches._count++) + setUniform(instance, options.seq, value, pitches._count++); } else if (options.uniform !== undefined) { - setUniform(instance, options.uniform, value) + setUniform(instance, options.uniform, value); } else { - console.error("Unknown shader options, need either pitch or uniform", options) + console.error('Unknown shader options, need either pitch or uniform', options); } - }, false) -}) + }, false); +}); From 735d531cce7f4dd5760ad768b8d3f50e420b7e4b Mon Sep 17 00:00:00 2001 From: Tristan de Cacqueray Date: Sat, 19 Oct 2024 06:28:38 -0400 Subject: [PATCH 04/21] Move shader module into it's own package --- examples/codemirror-repl/main.js | 1 + packages/draw/index.mjs | 1 - packages/draw/package.json | 3 +- packages/repl/prebake.mjs | 1 + packages/shader/README.md | 3 ++ .../{draw/shader.mjs => shader/index.mjs} | 0 packages/shader/package.json | 39 +++++++++++++++++++ packages/shader/vite.config.js | 19 +++++++++ pnpm-lock.yaml | 21 ++++++++++ website/package.json | 1 + website/src/repl/util.mjs | 1 + 11 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 packages/shader/README.md rename packages/{draw/shader.mjs => shader/index.mjs} (100%) create mode 100644 packages/shader/package.json create mode 100644 packages/shader/vite.config.js diff --git a/examples/codemirror-repl/main.js b/examples/codemirror-repl/main.js index 5b5714fb7..2d360ce5d 100644 --- a/examples/codemirror-repl/main.js +++ b/examples/codemirror-repl/main.js @@ -29,6 +29,7 @@ const editor = new StrudelMirror({ import('@strudel/core'), import('@strudel/draw'), import('@strudel/mini'), + import('@strudel/shader'), import('@strudel/tonal'), import('@strudel/webaudio'), ); diff --git a/packages/draw/index.mjs b/packages/draw/index.mjs index 98ac989af..506c6151d 100644 --- a/packages/draw/index.mjs +++ b/packages/draw/index.mjs @@ -2,6 +2,5 @@ export * from './animate.mjs'; export * from './color.mjs'; export * from './draw.mjs'; export * from './pianoroll.mjs'; -export * from './shader.mjs'; export * from './spiral.mjs'; export * from './pitchwheel.mjs'; diff --git a/packages/draw/package.json b/packages/draw/package.json index b65817504..a6a88ecfa 100644 --- a/packages/draw/package.json +++ b/packages/draw/package.json @@ -29,8 +29,7 @@ }, "homepage": "https://github.com/tidalcycles/strudel#readme", "dependencies": { - "@strudel/core": "workspace:*", - "picogl": "^0.17.9" + "@strudel/core": "workspace:*" }, "devDependencies": { "vite": "^5.0.10" diff --git a/packages/repl/prebake.mjs b/packages/repl/prebake.mjs index 9fc1c8819..f0f72687f 100644 --- a/packages/repl/prebake.mjs +++ b/packages/repl/prebake.mjs @@ -12,6 +12,7 @@ export async function prebake() { import('@strudel/webaudio'), import('@strudel/codemirror'), import('@strudel/hydra'), + import('@strudel/shader'), import('@strudel/soundfonts'), import('@strudel/midi'), // import('@strudel/xen'), diff --git a/packages/shader/README.md b/packages/shader/README.md new file mode 100644 index 000000000..851cc7814 --- /dev/null +++ b/packages/shader/README.md @@ -0,0 +1,3 @@ +# @strudel/shader + +Helpers for drawing shader diff --git a/packages/draw/shader.mjs b/packages/shader/index.mjs similarity index 100% rename from packages/draw/shader.mjs rename to packages/shader/index.mjs diff --git a/packages/shader/package.json b/packages/shader/package.json new file mode 100644 index 000000000..55e1250e7 --- /dev/null +++ b/packages/shader/package.json @@ -0,0 +1,39 @@ +{ + "name": "@strudel/shader", + "version": "1.0.0", + "description": "Helpers for drawing shader", + "main": "index.mjs", + "type": "module", + "publishConfig": { + "main": "dist/index.mjs" + }, + "scripts": { + "build": "vite build", + "prepublishOnly": "npm run build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tidalcycles/strudel.git" + }, + "keywords": [ + "titdalcycles", + "strudel", + "pattern", + "livecoding", + "algorave", + "shader" + ], + "author": "Felix Roos ", + "license": "AGPL-3.0-or-later", + "bugs": { + "url": "https://github.com/tidalcycles/strudel/issues" + }, + "homepage": "https://github.com/tidalcycles/strudel#readme", + "dependencies": { + "@strudel/core": "workspace:*", + "picogl": "^0.17.9" + }, + "devDependencies": { + "vite": "^5.0.10" + } +} diff --git a/packages/shader/vite.config.js b/packages/shader/vite.config.js new file mode 100644 index 000000000..5df3edc1b --- /dev/null +++ b/packages/shader/vite.config.js @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import { dependencies } from './package.json'; +import { resolve } from 'path'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [], + build: { + lib: { + entry: resolve(__dirname, 'index.mjs'), + formats: ['es'], + fileName: (ext) => ({ es: 'index.mjs' })[ext], + }, + rollupOptions: { + external: [...Object.keys(dependencies)], + }, + target: 'esnext', + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a18f06e73..fb7fba514 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -406,6 +406,19 @@ importers: specifier: ^5.0.10 version: 5.4.9(@types/node@22.7.6)(terser@5.36.0) + packages/shader: + dependencies: + '@strudel/core': + specifier: workspace:* + version: link:../core + picogl: + specifier: ^0.17.9 + version: 0.17.9 + devDependencies: + vite: + specifier: ^5.0.10 + version: 5.4.9(@types/node@22.7.6)(terser@5.36.0) + packages/soundfonts: dependencies: '@strudel/core': @@ -629,6 +642,9 @@ importers: '@strudel/serial': specifier: workspace:* version: link:../packages/serial + '@strudel/shader': + specifier: workspace:* + version: link:../packages/shader '@strudel/soundfonts': specifier: workspace:* version: link:../packages/soundfonts @@ -6084,6 +6100,9 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picogl@0.17.9: + resolution: {integrity: sha512-TfqB7jlD5FTO4a/Rp9wnMhVjPA0XZ/xbtLS2f8eHtOmVtIOhFD7Wf0jUL6kDjww3bgF/REEV9oPreBeGvWFlUQ==} + picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} @@ -14789,6 +14808,8 @@ snapshots: picocolors@1.1.1: {} + picogl@0.17.9: {} + picomatch@2.3.1: {} picomatch@4.0.2: {} diff --git a/website/package.json b/website/package.json index 942fee9c6..176454660 100644 --- a/website/package.json +++ b/website/package.json @@ -35,6 +35,7 @@ "@strudel/mini": "workspace:*", "@strudel/osc": "workspace:*", "@strudel/serial": "workspace:*", + "@strudel/shader": "workspace:*", "@strudel/soundfonts": "workspace:*", "@strudel/tonal": "workspace:*", "@strudel/transpiler": "workspace:*", diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index 905e16b09..901f3323f 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -78,6 +78,7 @@ export function loadModules() { import('@strudel/codemirror'), import('@strudel/hydra'), import('@strudel/serial'), + import('@strudel/shader'), import('@strudel/soundfonts'), import('@strudel/csound'), import('@strudel/tidal'), From 5c983ceb8fab4807de1834792bab2db1f16a6faa Mon Sep 17 00:00:00 2001 From: Tristan de Cacqueray Date: Sat, 19 Oct 2024 06:43:50 -0400 Subject: [PATCH 05/21] Extract the uniform helper to its own module --- packages/shader/index.mjs | 236 +----------------------------------- packages/shader/shader.mjs | 211 ++++++++++++++++++++++++++++++++ packages/shader/uniform.mjs | 33 +++++ 3 files changed, 246 insertions(+), 234 deletions(-) create mode 100644 packages/shader/shader.mjs create mode 100644 packages/shader/uniform.mjs 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); +}); From bf3a0958522f67d680481c8519f23a1df4dd3023 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 19 Oct 2024 15:07:48 +0200 Subject: [PATCH 06/21] codeformat --- packages/shader/index.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shader/index.mjs b/packages/shader/index.mjs index 41aa0e1de..9358dedc8 100644 --- a/packages/shader/index.mjs +++ b/packages/shader/index.mjs @@ -1,2 +1,2 @@ -export {loadShader} from './shader.mjs'; +export { loadShader } from './shader.mjs'; export * from './uniform.mjs'; From b0149f8190e339f8d5408ef9e698f53416cb4a37 Mon Sep 17 00:00:00 2001 From: Tristan de Cacqueray Date: Sat, 19 Oct 2024 08:21:09 -0400 Subject: [PATCH 07/21] Refactor the uniform function and add documentation --- packages/shader/README.md | 15 +++++++- packages/shader/index.mjs | 2 +- packages/shader/shader.mjs | 6 ++-- packages/shader/uniform.mjs | 72 ++++++++++++++++++++++++++++++------- 4 files changed, 79 insertions(+), 16 deletions(-) diff --git a/packages/shader/README.md b/packages/shader/README.md index 851cc7814..42fc5a860 100644 --- a/packages/shader/README.md +++ b/packages/shader/README.md @@ -1,3 +1,16 @@ # @strudel/shader -Helpers for drawing shader +Helpers for drawing shader. + +## Todos + +Here are the things that needs to be implemented: + +- [ ] Shader source error reporting +- [ ] Shader import from url, like shadertoy or git +- [ ] Display attribution +- [ ] Compilation error reporting, e.g. to show the line number +- [ ] Multiple instance and custom canvas position +- [ ] Multiple program, to be swapped like a pattern +- [ ] Texture inputs +- [ ] Buffer inputs, e.g. to generate a texture diff --git a/packages/shader/index.mjs b/packages/shader/index.mjs index 41aa0e1de..931421e83 100644 --- a/packages/shader/index.mjs +++ b/packages/shader/index.mjs @@ -1,2 +1,2 @@ -export {loadShader} from './shader.mjs'; +export * from './shader.mjs'; export * from './uniform.mjs'; diff --git a/packages/shader/shader.mjs b/packages/shader/shader.mjs index e81a0cf50..8d9f0430c 100644 --- a/packages/shader/shader.mjs +++ b/packages/shader/shader.mjs @@ -59,7 +59,7 @@ const decayModulation = (decay) => { // Set an uniform value (from a pattern). export function setUniform(instanceName, name, value, position) { - const instance = _instances[instanceName || 'default']; + const instance = _instances[instanceName]; if (!instance) { logger('[shader] not loaded yet', 'warning'); return; @@ -104,7 +104,8 @@ function setupUniforms(uniforms, program) { 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... + // TODO: keep the previous values when the count change: + // if the count decreased, then drop the excess, else append new values uniforms[uname] = { name, count, @@ -174,6 +175,7 @@ async function initializeShaderInstance(name, code) { updateUniforms(instance.drawFrame, elapsed, instance.uniforms); instance.drawFrame.draw(); + // After sometime, if no update happened, stop the animation loop if (instance.age++ < 100) requestAnimationFrame(instance.update); else instance.drawing = false; }; diff --git a/packages/shader/uniform.mjs b/packages/shader/uniform.mjs index e1ae7f732..100b83b01 100644 --- a/packages/shader/uniform.mjs +++ b/packages/shader/uniform.mjs @@ -7,27 +7,75 @@ This program is free software: you can redistribute it and/or modify it under th 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 }; +/** + * Update a shader. The destination name consist of + * + * - the uniform name + * - optional array mapping, either a number or an assignment mode ('seq' or 'pitch') + * + * @name uniform + * @example + * s("bd").uniform("iColor") + * @example + * s("bd").uniform("iColors:seq") + * @example + * note("c3 e3").uniform("iMorph:pitch") + */ +function parseUniformTarget(name) { + if (typeof name === 'string') + return {name, mapping: 'single', position: 0} + else if (name.length == 2) { + const mapping = typeof name[1] === 'string' ? name[1] : 'single' + const position = typeof name[1] === 'string' ? null : name[1] + return { + name: name[0], + mapping, + position + } + } +} + +// Keep track of the pitches value per uniform +let _pitches = {} +export const uniform = register('uniform', (target, pat) => { + // TODO: support multiple shader instance + const instance = "default" + + // Decode the uniform defintion + const uniformTarget = parseUniformTarget(target) + + // Get the pitches + if (!_pitches[uniformTarget.name]) + _pitches[uniformTarget.name] = {_count: 0} + const pitches = _pitches[uniformTarget.name] return pat.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => { - const instance = options.instance; + // TODO: figure out how to get the desired value, e.g. is this pattern for pan, gain, velocity, ... + const value = hap.value ? (hap.value.gain || 1.0) : 1.0; - const value = options.gain || 1.0; - if (options.pitch !== undefined) { + // Get the uniform mapping position + let position = null + if (uniformTarget.mapping == 'pitch') { + // Assign one position per pitch 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); + position = pitches[note] + } else if (uniformTarget.mapping == 'seq') { + console.log("HERE", pitches) + // Assign a new position per event + position = pitches._count++ + } else if (uniformTarget.mapping == 'single') { + // Assign a fixed position + position = uniformTarget.position } else { - console.error('Unknown shader options, need either pitch or uniform', options); + console.error('Unknown uniform target', uniformTarget) } + + // Update the uniform + if (position !== null) + setUniform(instance, uniformTarget.name, value, position); }, false); }); From fcd550b5bd8f0332e74f51d3dcf50f7e49929acf Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 19 Oct 2024 15:13:21 +0200 Subject: [PATCH 08/21] codeformat --- packages/shader/uniform.mjs | 39 +++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/packages/shader/uniform.mjs b/packages/shader/uniform.mjs index 100b83b01..903988554 100644 --- a/packages/shader/uniform.mjs +++ b/packages/shader/uniform.mjs @@ -22,39 +22,37 @@ import { setUniform } from './shader.mjs'; * note("c3 e3").uniform("iMorph:pitch") */ function parseUniformTarget(name) { - if (typeof name === 'string') - return {name, mapping: 'single', position: 0} + if (typeof name === 'string') return { name, mapping: 'single', position: 0 }; else if (name.length == 2) { - const mapping = typeof name[1] === 'string' ? name[1] : 'single' - const position = typeof name[1] === 'string' ? null : name[1] + const mapping = typeof name[1] === 'string' ? name[1] : 'single'; + const position = typeof name[1] === 'string' ? null : name[1]; return { name: name[0], mapping, - position - } + position, + }; } } // Keep track of the pitches value per uniform -let _pitches = {} +let _pitches = {}; export const uniform = register('uniform', (target, pat) => { // TODO: support multiple shader instance - const instance = "default" + const instance = 'default'; // Decode the uniform defintion - const uniformTarget = parseUniformTarget(target) + const uniformTarget = parseUniformTarget(target); // Get the pitches - if (!_pitches[uniformTarget.name]) - _pitches[uniformTarget.name] = {_count: 0} - const pitches = _pitches[uniformTarget.name] + if (!_pitches[uniformTarget.name]) _pitches[uniformTarget.name] = { _count: 0 }; + const pitches = _pitches[uniformTarget.name]; return pat.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => { // TODO: figure out how to get the desired value, e.g. is this pattern for pan, gain, velocity, ... - const value = hap.value ? (hap.value.gain || 1.0) : 1.0; + const value = hap.value ? hap.value.gain || 1.0 : 1.0; // Get the uniform mapping position - let position = null + let position = null; if (uniformTarget.mapping == 'pitch') { // Assign one position per pitch const note = hap.value.note || hap.value.s; @@ -62,20 +60,19 @@ export const uniform = register('uniform', (target, pat) => { // Assign new value, the first note gets 0, then 1, then 2, ... pitches[note] = Object.keys(pitches).length; } - position = pitches[note] + position = pitches[note]; } else if (uniformTarget.mapping == 'seq') { - console.log("HERE", pitches) + console.log('HERE', pitches); // Assign a new position per event - position = pitches._count++ + position = pitches._count++; } else if (uniformTarget.mapping == 'single') { // Assign a fixed position - position = uniformTarget.position + position = uniformTarget.position; } else { - console.error('Unknown uniform target', uniformTarget) + console.error('Unknown uniform target', uniformTarget); } // Update the uniform - if (position !== null) - setUniform(instance, uniformTarget.name, value, position); + if (position !== null) setUniform(instance, uniformTarget.name, value, position); }, false); }); From 8d1180f87aeca51a1aa6b1e3f2d486697c273b86 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 19 Oct 2024 15:15:25 +0200 Subject: [PATCH 09/21] fix: test runtime for uniform function --- test/runtime.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/runtime.mjs b/test/runtime.mjs index 19b716274..75105fbe1 100644 --- a/test/runtime.mjs +++ b/test/runtime.mjs @@ -77,6 +77,9 @@ const toneHelpersMocked = { strudel.Pattern.prototype.osc = function () { return this; }; +strudel.Pattern.prototype.uniform = function () { + return this; +}; strudel.Pattern.prototype.csound = function () { return this; }; From 67c0ebaf54c46586f7bc648f3acdc0377acc9d72 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 19 Oct 2024 15:17:09 +0200 Subject: [PATCH 10/21] snapshot --- test/__snapshots__/examples.test.mjs.snap | 31 +++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index a3419e98d..4905690c6 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -8343,6 +8343,37 @@ exports[`runs examples > example "undegradeBy" example index 1 1`] = ` ] `; +exports[`runs examples > example "uniform" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | s:bd ]", + "[ 1/1 → 2/1 | s:bd ]", + "[ 2/1 → 3/1 | s:bd ]", + "[ 3/1 → 4/1 | s:bd ]", +] +`; + +exports[`runs examples > example "uniform" example index 1 1`] = ` +[ + "[ 0/1 → 1/1 | s:bd ]", + "[ 1/1 → 2/1 | s:bd ]", + "[ 2/1 → 3/1 | s:bd ]", + "[ 3/1 → 4/1 | s:bd ]", +] +`; + +exports[`runs examples > example "uniform" example index 2 1`] = ` +[ + "[ 0/1 → 1/2 | note:c3 ]", + "[ 1/2 → 1/1 | note:e3 ]", + "[ 1/1 → 3/2 | note:c3 ]", + "[ 3/2 → 2/1 | note:e3 ]", + "[ 2/1 → 5/2 | note:c3 ]", + "[ 5/2 → 3/1 | note:e3 ]", + "[ 3/1 → 7/2 | note:c3 ]", + "[ 7/2 → 4/1 | note:e3 ]", +] +`; + exports[`runs examples > example "unison" example index 0 1`] = ` [ "[ 0/1 → 1/12 | note:d s:supersaw unison:1 ]", From 9b4675f1ce576a1f982b7a30793904db14a74b5a Mon Sep 17 00:00:00 2001 From: Tristan de Cacqueray Date: Sat, 19 Oct 2024 09:24:01 -0400 Subject: [PATCH 11/21] Update the shader todos --- packages/shader/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/shader/README.md b/packages/shader/README.md index 42fc5a860..42dc3ba0f 100644 --- a/packages/shader/README.md +++ b/packages/shader/README.md @@ -6,11 +6,11 @@ Helpers for drawing shader. Here are the things that needs to be implemented: -- [ ] Shader source error reporting +- [ ] Shader compilation error reporting, e.g. to show the line number - [ ] Shader import from url, like shadertoy or git -- [ ] Display attribution -- [ ] Compilation error reporting, e.g. to show the line number -- [ ] Multiple instance and custom canvas position -- [ ] Multiple program, to be swapped like a pattern +- [ ] Display shader author attribution, e.g. to respect CC-BY +- [ ] Handle WebGL context lost by restoring the objects. +- [ ] Multiple instances and custom canvas positions +- [ ] Multiple programs, to be swapped like a pattern - [ ] Texture inputs - [ ] Buffer inputs, e.g. to generate a texture From b85a5a9179c0ef1e0c84673e686422876808ffce Mon Sep 17 00:00:00 2001 From: Tristan de Cacqueray Date: Sun, 20 Oct 2024 09:49:46 -0400 Subject: [PATCH 12/21] Use pat.withValue and add uniformTrigger --- packages/shader/shader.mjs | 7 +- packages/shader/uniform.mjs | 124 +++++++++++++++++++++++------------- 2 files changed, 83 insertions(+), 48 deletions(-) diff --git a/packages/shader/shader.mjs b/packages/shader/shader.mjs index 8d9f0430c..6b1864b38 100644 --- a/packages/shader/shader.mjs +++ b/packages/shader/shader.mjs @@ -24,6 +24,8 @@ out vec4 oColor; uniform float iTime; uniform vec2 iResolution; +#define STRUDEL 1 + ${code} void main(void) { @@ -65,6 +67,7 @@ export function setUniform(instanceName, name, value, position) { return; } + // console.log("setUniform: ", name, value, position) const uniform = instance.uniforms[name]; if (uniform) { if (uniform.count == 0) { @@ -91,6 +94,8 @@ 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)); + + // console.log("updateUniforms:", uniform.name, value) // Send the value to the GPU drawFrame.uniform(uniform.name, value); }); @@ -138,9 +143,7 @@ function setupCanvas(name) { 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]); diff --git a/packages/shader/uniform.mjs b/packages/shader/uniform.mjs index 903988554..12627aa68 100644 --- a/packages/shader/uniform.mjs +++ b/packages/shader/uniform.mjs @@ -7,72 +7,104 @@ This program is free software: you can redistribute it and/or modify it under th import { register, logger } from '@strudel/core'; import { setUniform } from './shader.mjs'; +function parseUniformTarget(name) { + if (typeof name === 'string') return { name, position: 0 }; + else if (name.length == 2) { + let position = null; + if (typeof name[1] === 'number') position = name[1]; + else if (name[1] == 'random') position = Math.floor(Math.random() * 1024); + else if (name[1] != 'seq') throw 'Unknown mapping: ' + name[1]; + return { name: name[0], position }; + } +} + +// Keep track of the pitches value per uniform +let _uniforms = {}; +function getNextPosition(name, value) { + // Initialize uniform state + if (!_uniforms[name]) _uniforms[name] = { _count: 0 }; + const uniform = _uniforms[name]; + + // Set a new position when the value changes + if (uniform._last != value) { + uniform._last = value; + uniform._count++; + } + return uniform._count; +} + /** * Update a shader. The destination name consist of * * - the uniform name - * - optional array mapping, either a number or an assignment mode ('seq' or 'pitch') + * - optional array mapping, either a number or an assignment mode ('seq' or 'random') * * @name uniform * @example - * s("bd").uniform("iColor") - * @example - * s("bd").uniform("iColors:seq") + * pan(sine.uniform("iColor")) * @example - * note("c3 e3").uniform("iMorph:pitch") + * gain("<.5 .3>".uniform("rotations:seq")) */ -function parseUniformTarget(name) { - if (typeof name === 'string') return { name, mapping: 'single', position: 0 }; - else if (name.length == 2) { - const mapping = typeof name[1] === 'string' ? name[1] : 'single'; - const position = typeof name[1] === 'string' ? null : name[1]; - return { - name: name[0], - mapping, - position, - }; +export const uniform = register('uniform', (target, pat) => { + // TODO: support multiple shader instance + const instance = 'default'; + + // Decode the uniform target defintion + const uniformTarget = parseUniformTarget(target); + + return pat.withValue((v) => { + // TODO: figure out why this is called repeatedly when changing values. For example, on first call, the last value is passed. + if (typeof v === 'number') { + const position = + uniformTarget.position === null ? getNextPosition(uniformTarget.name, v) : uniformTarget.position; + setUniform(instance, uniformTarget.name, v, position); + } else { + console.error('Uniform applied to a non number pattern'); + } + return v; + }); +}); + +function getNotePosition(name, value) { + // Initialize uniform state + if (!_uniforms[name]) _uniforms[name] = {}; + const uniform = _uniforms[name]; + + const note = value.note || value.n || value.sound || value.s; + if (uniform[note] === undefined) { + // Assign new value, the first note gets 0, then 1, then 2, ... + uniform[note] = Object.keys(uniform).length; } + return uniform[note]; } -// Keep track of the pitches value per uniform -let _pitches = {}; -export const uniform = register('uniform', (target, pat) => { +/** + * Update a shader with note-on event. The destination name consist of + * + * - the uniform name + * - optional array position, default to randomly assigned position based on the note or sound. + * + * @name uniformTrigger + * @example + * pan(sine.uniform("iColor")) + * @example + * gain("<.5 .3>".uniform("rotations:seq")) + */ +export const uniformTrigger = register('uniformTrigger', (target, pat) => { // TODO: support multiple shader instance const instance = 'default'; - // Decode the uniform defintion + // Decode the uniform target defintion const uniformTarget = parseUniformTarget(target); - // Get the pitches - if (!_pitches[uniformTarget.name]) _pitches[uniformTarget.name] = { _count: 0 }; - const pitches = _pitches[uniformTarget.name]; - return pat.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => { - // TODO: figure out how to get the desired value, e.g. is this pattern for pan, gain, velocity, ... - const value = hap.value ? hap.value.gain || 1.0 : 1.0; + const position = + uniformTarget.position === null ? getNotePosition(uniformTarget.name, hap.value) : uniformTarget.position; - // Get the uniform mapping position - let position = null; - if (uniformTarget.mapping == 'pitch') { - // Assign one position per pitch - 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; - } - position = pitches[note]; - } else if (uniformTarget.mapping == 'seq') { - console.log('HERE', pitches); - // Assign a new position per event - position = pitches._count++; - } else if (uniformTarget.mapping == 'single') { - // Assign a fixed position - position = uniformTarget.position; - } else { - console.error('Unknown uniform target', uniformTarget); - } + // TODO: support custom value + const value = 1.0; // Update the uniform - if (position !== null) setUniform(instance, uniformTarget.name, value, position); + setUniform(instance, uniformTarget.name, value, position); }, false); }); From cad11e1e4845fbd906c55edc541c5c222daba70b Mon Sep 17 00:00:00 2001 From: Tristan de Cacqueray Date: Sun, 20 Oct 2024 13:03:26 -0400 Subject: [PATCH 13/21] Update uniform value and add tests --- packages/shader/package.json | 4 +- packages/shader/shader.mjs | 65 +++++++++++++++----------------- packages/shader/testSetup.mjs | 3 ++ packages/shader/uniform.mjs | 65 +++++++++++++++++++------------- packages/shader/uniform.test.mjs | 22 +++++++++++ packages/shader/vite.config.js | 3 ++ pnpm-lock.yaml | 4 ++ 7 files changed, 103 insertions(+), 63 deletions(-) create mode 100644 packages/shader/testSetup.mjs create mode 100644 packages/shader/uniform.test.mjs diff --git a/packages/shader/package.json b/packages/shader/package.json index 55e1250e7..64496b424 100644 --- a/packages/shader/package.json +++ b/packages/shader/package.json @@ -8,6 +8,7 @@ "main": "dist/index.mjs" }, "scripts": { + "test": "vitest run", "build": "vite build", "prepublishOnly": "npm run build" }, @@ -34,6 +35,7 @@ "picogl": "^0.17.9" }, "devDependencies": { - "vite": "^5.0.10" + "vite": "^5.0.10", + "vitest": "^2.1.3" } } diff --git a/packages/shader/shader.mjs b/packages/shader/shader.mjs index 6b1864b38..f65b23f8a 100644 --- a/packages/shader/shader.mjs +++ b/packages/shader/shader.mjs @@ -34,50 +34,47 @@ void main(void) { `; } -// 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; - }, - }; -}; +// Modulation helpers to smooth the values. +class UniformValue { + constructor() { + this.value = 0; + this.desired = 0; + this.slow = 10; + } + + get(elapsed) { + // Adjust the value according to the rate of change + const offset = (this.desired - this.value) / (this.slow * Math.min(1, elapsed * 60)); + // Ignore small changes + if (Math.abs(offset) > 1e-3) this.value += offset; + return this.value; + } +} // Set an uniform value (from a pattern). -export function setUniform(instanceName, name, value, position) { +export function setUniform(instanceName, name, value, incr, position, slow) { const instance = _instances[instanceName]; if (!instance) { logger('[shader] not loaded yet', 'warning'); return; } - // console.log("setUniform: ", name, value, position) + // console.log('setUniform: ', name, value, position, slow); const uniform = instance.uniforms[name]; if (uniform) { + let uniformValue; if (uniform.count == 0) { // This is a single value - uniform.mod.set(value); + uniformValue = uniform.value; } else { // This is an array - const idx = position % uniform.mod.length; - uniform.mod[idx].set(value); + const idx = position % uniform.value.length; + uniformValue = uniform.value[idx]; } + uniformValue.slow = slow; + // TODO: handle direct assignment, this is incrementing by default + if (incr) uniformValue.desired += value; + else uniformValue.desired = value; } else { logger('[shader] unknown uniform: ' + name); } @@ -92,11 +89,10 @@ export function setUniform(instanceName, name, value, position) { // 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)); + const value = uniform.count == 0 ? uniform.value.get(elapsed) : uniform.value.map((v) => v.get(elapsed)); - // console.log("updateUniforms:", uniform.name, value) // Send the value to the GPU + // console.log('updateUniforms:', uniform.name, value); drawFrame.uniform(uniform.name, value); }); } @@ -114,8 +110,7 @@ function setupUniforms(uniforms, program) { uniforms[uname] = { name, count, - value: count == 0 ? 0 : new Float32Array(count), - mod: count == 0 ? decayModulation(50) : new Array(count).fill().map(() => decayModulation(50)), + value: count == 0 ? new UniformValue() : new Array(count).fill().map(() => new UniformValue()), }; } } @@ -169,7 +164,7 @@ async function initializeShaderInstance(name, code) { instance.age = 0; instance.update = () => { const now = performance.now() / 1000; - const elapsed = now - prev; + const elapsed = instance.age == 0 ? 1 / 60 : now - prev; prev = now; // console.log("drawing!") app.clear(); diff --git a/packages/shader/testSetup.mjs b/packages/shader/testSetup.mjs new file mode 100644 index 000000000..fb6963834 --- /dev/null +++ b/packages/shader/testSetup.mjs @@ -0,0 +1,3 @@ +// Fix `ReferenceError: self is not defined` +// when importing picogl in tests +globalThis.self = {}; diff --git a/packages/shader/uniform.mjs b/packages/shader/uniform.mjs index 12627aa68..4bf364967 100644 --- a/packages/shader/uniform.mjs +++ b/packages/shader/uniform.mjs @@ -7,18 +7,29 @@ This program is free software: you can redistribute it and/or modify it under th import { register, logger } from '@strudel/core'; import { setUniform } from './shader.mjs'; -function parseUniformTarget(name) { - if (typeof name === 'string') return { name, position: 0 }; - else if (name.length == 2) { - let position = null; - if (typeof name[1] === 'number') position = name[1]; - else if (name[1] == 'random') position = Math.floor(Math.random() * 1024); - else if (name[1] != 'seq') throw 'Unknown mapping: ' + name[1]; - return { name: name[0], position }; +// Parse a destination from the mini notation, e.g. `name` or `name:attr:value` +export function parseUniformDest(dest) { + let result = {}; + if (typeof dest === 'string') result.name = dest; + else if (dest.length >= 2) { + result.name = dest[0]; + // Parse the attr:value pairs + for (let i = 1; i < dest.length; i += 2) { + const k = dest[i]; + const v = dest[i + 1]; + const isNum = typeof v === 'number'; + if (k == 'index' && isNum) result.position = v; + else if (k == 'index' && v == 'random') result.position = Math.floor(Math.random() * 1024); + else if (k == 'index' && v == 'seq') result.position = null; + else if (k == 'gain' && isNum) result.gain = v; + else if (k == 'slow' && isNum) result.slow = v; + else throw 'Bad uniform param ' + k + ':' + v; + } } + return result; } -// Keep track of the pitches value per uniform +// Keep track of the last uniforms' array position let _uniforms = {}; function getNextPosition(name, value) { // Initialize uniform state @@ -37,27 +48,31 @@ function getNextPosition(name, value) { * Update a shader. The destination name consist of * * - the uniform name - * - optional array mapping, either a number or an assignment mode ('seq' or 'random') + * - optional 'index' to set array position, either a number or an assignment mode ('seq' or 'random') + * - optional 'gain' to adjust the value: 0 to silence, 2 to double + * - optional 'slow' to adjust the change speed: 1 for instant, 50 for slow changes, default to 10 * * @name uniform * @example * pan(sine.uniform("iColor")) * @example - * gain("<.5 .3>".uniform("rotations:seq")) + * gain("<.5 .3>".uniform("rotations:index:seq")) */ export const uniform = register('uniform', (target, pat) => { // TODO: support multiple shader instance const instance = 'default'; // Decode the uniform target defintion - const uniformTarget = parseUniformTarget(target); + const uniformDest = parseUniformDest(target); + // Set the first value by default + if (uniformDest.position === undefined) uniformDest.position = 0; return pat.withValue((v) => { // TODO: figure out why this is called repeatedly when changing values. For example, on first call, the last value is passed. if (typeof v === 'number') { - const position = - uniformTarget.position === null ? getNextPosition(uniformTarget.name, v) : uniformTarget.position; - setUniform(instance, uniformTarget.name, v, position); + const position = uniformDest.position === null ? getNextPosition(uniformDest.name, v) : uniformDest.position; + const value = v * (uniformDest.gain || 1); + setUniform(instance, uniformDest.name, value, false, position, uniformDest.slow || 10); } else { console.error('Uniform applied to a non number pattern'); } @@ -79,32 +94,28 @@ function getNotePosition(name, value) { } /** - * Update a shader with note-on event. The destination name consist of - * - * - the uniform name - * - optional array position, default to randomly assigned position based on the note or sound. + * Update a shader with note-on event. See the 'uniform' doc. * * @name uniformTrigger * @example - * pan(sine.uniform("iColor")) - * @example - * gain("<.5 .3>".uniform("rotations:seq")) + * s("bd sd").uniformTrigger("iColors:gain:2")) */ export const uniformTrigger = register('uniformTrigger', (target, pat) => { // TODO: support multiple shader instance const instance = 'default'; // Decode the uniform target defintion - const uniformTarget = parseUniformTarget(target); + const uniformDest = parseUniformDest(target); + // Assign pitch position by default + if (uniformDest.position === undefined) uniformDest.position = null; return pat.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => { const position = - uniformTarget.position === null ? getNotePosition(uniformTarget.name, hap.value) : uniformTarget.position; + uniformDest.position === null ? getNotePosition(uniformDest.name, hap.value) : uniformDest.position; - // TODO: support custom value - const value = 1.0; + const value = (hap.value.gain || 1) * (uniformDest.gain || 1); // Update the uniform - setUniform(instance, uniformTarget.name, value, position); + setUniform(instance, uniformDest.name, value, true, position, uniformDest.slow || 10); }, false); }); diff --git a/packages/shader/uniform.test.mjs b/packages/shader/uniform.test.mjs new file mode 100644 index 000000000..7751ebd56 --- /dev/null +++ b/packages/shader/uniform.test.mjs @@ -0,0 +1,22 @@ +/* +uniform.test.mjs - +Copyright (C) 2024 Strudel contributors - see +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 { describe, it, expect } from 'vitest'; +import { parseUniformDest } from './uniform.mjs'; + +describe('Uniform', () => { + it('Parse simple', () => { + expect(parseUniformDest('iColor')).toStrictEqual({ name: 'iColor' }); + }); + it('Parse param', () => { + expect(parseUniformDest(['iColor', 'index', 2])).toStrictEqual({ name: 'iColor', position: 2 }); + expect(parseUniformDest(['iColor', 'index', 'seq'])).toStrictEqual({ name: 'iColor', position: null }); + expect(parseUniformDest(['iColor', 'gain', 3])).toStrictEqual({ name: 'iColor', gain: 3 }); + }); + it('Parse multi param', () => { + expect(parseUniformDest(['iColor', 'index', 2, 'gain', 3])).toStrictEqual({ name: 'iColor', position: 2, gain: 3 }); + }); +}); diff --git a/packages/shader/vite.config.js b/packages/shader/vite.config.js index 5df3edc1b..b0e119c95 100644 --- a/packages/shader/vite.config.js +++ b/packages/shader/vite.config.js @@ -5,6 +5,9 @@ import { resolve } from 'path'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [], + test: { + setupFiles: './testSetup.mjs', + }, build: { lib: { entry: resolve(__dirname, 'index.mjs'), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fb7fba514..c12f2445e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -418,6 +418,9 @@ importers: vite: specifier: ^5.0.10 version: 5.4.9(@types/node@22.7.6)(terser@5.36.0) + vitest: + specifier: ^2.1.3 + version: 2.1.3(@types/node@22.7.6)(@vitest/ui@2.1.3)(terser@5.36.0) packages/soundfonts: dependencies: @@ -7759,6 +7762,7 @@ packages: workbox-google-analytics@7.0.0: resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==} + deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained workbox-navigation-preload@7.0.0: resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==} From 9e0b840afd0cbb6d686a8c88fada7dd338eec8ec Mon Sep 17 00:00:00 2001 From: Tristan de Cacqueray Date: Sun, 20 Oct 2024 15:13:49 -0400 Subject: [PATCH 14/21] Fix default uniformTrigger indexs --- packages/shader/uniform.mjs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/shader/uniform.mjs b/packages/shader/uniform.mjs index 4bf364967..0ce7896a2 100644 --- a/packages/shader/uniform.mjs +++ b/packages/shader/uniform.mjs @@ -106,12 +106,16 @@ export const uniformTrigger = register('uniformTrigger', (target, pat) => { // Decode the uniform target defintion const uniformDest = parseUniformDest(target); - // Assign pitch position by default - if (uniformDest.position === undefined) uniformDest.position = null; return pat.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => { const position = - uniformDest.position === null ? getNotePosition(uniformDest.name, hap.value) : uniformDest.position; + uniformDest.position === undefined + ? // Set the position based on the note by default + getNotePosition(uniformDest.name, hap.value) + : uniformDest.position === null + ? // The index is set to `seq` + getNextPosition(uniformDest.name, currentTime) + : uniformDest.position; const value = (hap.value.gain || 1) * (uniformDest.gain || 1); From 1baa6da370f2894f5c5b1d5be56948aafde9f9c9 Mon Sep 17 00:00:00 2001 From: Tristan de Cacqueray Date: Sun, 20 Oct 2024 19:54:16 -0400 Subject: [PATCH 15/21] Update uniform method to take an option object --- packages/shader/package.json | 1 - packages/shader/testSetup.mjs | 3 - packages/shader/uniform.mjs | 178 +++++++++------------- packages/shader/uniform.test.mjs | 22 --- packages/shader/vite.config.js | 3 - test/__snapshots__/examples.test.mjs.snap | 31 ++-- 6 files changed, 90 insertions(+), 148 deletions(-) delete mode 100644 packages/shader/testSetup.mjs delete mode 100644 packages/shader/uniform.test.mjs diff --git a/packages/shader/package.json b/packages/shader/package.json index 64496b424..45ea5c11c 100644 --- a/packages/shader/package.json +++ b/packages/shader/package.json @@ -8,7 +8,6 @@ "main": "dist/index.mjs" }, "scripts": { - "test": "vitest run", "build": "vite build", "prepublishOnly": "npm run build" }, diff --git a/packages/shader/testSetup.mjs b/packages/shader/testSetup.mjs deleted file mode 100644 index fb6963834..000000000 --- a/packages/shader/testSetup.mjs +++ /dev/null @@ -1,3 +0,0 @@ -// Fix `ReferenceError: self is not defined` -// when importing picogl in tests -globalThis.self = {}; diff --git a/packages/shader/uniform.mjs b/packages/shader/uniform.mjs index 0ce7896a2..3e33b68da 100644 --- a/packages/shader/uniform.mjs +++ b/packages/shader/uniform.mjs @@ -4,122 +4,94 @@ 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 { Pattern, reify, register, logger } from '@strudel/core'; import { setUniform } from './shader.mjs'; -// Parse a destination from the mini notation, e.g. `name` or `name:attr:value` -export function parseUniformDest(dest) { - let result = {}; - if (typeof dest === 'string') result.name = dest; - else if (dest.length >= 2) { - result.name = dest[0]; - // Parse the attr:value pairs - for (let i = 1; i < dest.length; i += 2) { - const k = dest[i]; - const v = dest[i + 1]; - const isNum = typeof v === 'number'; - if (k == 'index' && isNum) result.position = v; - else if (k == 'index' && v == 'random') result.position = Math.floor(Math.random() * 1024); - else if (k == 'index' && v == 'seq') result.position = null; - else if (k == 'gain' && isNum) result.gain = v; - else if (k == 'slow' && isNum) result.slow = v; - else throw 'Bad uniform param ' + k + ':' + v; - } - } - return result; -} - -// Keep track of the last uniforms' array position -let _uniforms = {}; -function getNextPosition(name, value) { - // Initialize uniform state - if (!_uniforms[name]) _uniforms[name] = { _count: 0 }; - const uniform = _uniforms[name]; - - // Set a new position when the value changes - if (uniform._last != value) { - uniform._last = value; - uniform._count++; - } - return uniform._count; -} - /** - * Update a shader. The destination name consist of - * - * - the uniform name - * - optional 'index' to set array position, either a number or an assignment mode ('seq' or 'random') - * - optional 'gain' to adjust the value: 0 to silence, 2 to double - * - optional 'slow' to adjust the change speed: 1 for instant, 50 for slow changes, default to 10 + * Update a shader. * * @name uniform * @example * pan(sine.uniform("iColor")) * @example - * gain("<.5 .3>".uniform("rotations:index:seq")) + * gain("<.5 .3>".uniform("rotations:seq")) + * @example + * s("bd sd").uniform({onTrigger: true, dest: "moveFWD"}) */ -export const uniform = register('uniform', (target, pat) => { - // TODO: support multiple shader instance - const instance = 'default'; +export const uniform = register('uniform', (options, pat) => { + // The shader instance name + const instance = options.instance || 'default'; - // Decode the uniform target defintion - const uniformDest = parseUniformDest(target); - // Set the first value by default - if (uniformDest.position === undefined) uniformDest.position = 0; + // Are the uniform updated on trigger + const onTrigger = options.onTrigger || false; - return pat.withValue((v) => { - // TODO: figure out why this is called repeatedly when changing values. For example, on first call, the last value is passed. - if (typeof v === 'number') { - const position = uniformDest.position === null ? getNextPosition(uniformDest.name, v) : uniformDest.position; - const value = v * (uniformDest.gain || 1); - setUniform(instance, uniformDest.name, value, false, position, uniformDest.slow || 10); - } else { - console.error('Uniform applied to a non number pattern'); - } - return v; + const setCtx = (uniformParam) => (ctx) => ({ + uniformParam, + onTrigger: () => {}, + dominantTrigger: true, + ...ctx, }); -}); -function getNotePosition(name, value) { - // Initialize uniform state - if (!_uniforms[name]) _uniforms[name] = {}; - const uniform = _uniforms[name]; + const pitches = { _count: 0 }; + const getPosition = (value, dest) => { + if (typeof dest === 'number') return dest; + else if (dest == 'seq') return pitches._count++; + else if (dest == 'random') return Math.floor(Math.random() * 1024); + else if (onTrigger) { + const note = value.note || value.n || value.sound || value.s; + if (pitches[note] === undefined) { + // Assign new value, the first note gets 0, then 1, then 2, ... + pitches[note] = Object.keys(pitches).length - 1; + } + return pitches[note]; + } else { + throw 'Invalid position' + dest; + } + }; + const getUniformPosition = (value, dest) => { + if (typeof dest === 'string') { + return [dest, 0]; + } else { + return [dest[0], getPosition(value, dest[1])]; + } + }; - const note = value.note || value.n || value.sound || value.s; - if (uniform[note] === undefined) { - // Assign new value, the first note gets 0, then 1, then 2, ... - uniform[note] = Object.keys(uniform).length; + const optionsPats = []; + if (Array.isArray(options) || typeof options === 'string') + optionsPats.push(reify(options).withContext(setCtx('dest'))); + else { + if (options.dest) optionsPats.push(reify(options.dest).withContext(setCtx('dest'))); + if (options.gain) optionsPats.push(reify(options.gain).withContext(setCtx('gain'))); + if (options.slow) optionsPats.push(reify(options.slow).withContext(setCtx('slow'))); } - return uniform[note]; -} - -/** - * Update a shader with note-on event. See the 'uniform' doc. - * - * @name uniformTrigger - * @example - * s("bd sd").uniformTrigger("iColors:gain:2")) - */ -export const uniformTrigger = register('uniformTrigger', (target, pat) => { - // TODO: support multiple shader instance - const instance = 'default'; - - // Decode the uniform target defintion - const uniformDest = parseUniformDest(target); - - return pat.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => { - const position = - uniformDest.position === undefined - ? // Set the position based on the note by default - getNotePosition(uniformDest.name, hap.value) - : uniformDest.position === null - ? // The index is set to `seq` - getNextPosition(uniformDest.name, currentTime) - : uniformDest.position; - - const value = (hap.value.gain || 1) * (uniformDest.gain || 1); - - // Update the uniform - setUniform(instance, uniformDest.name, value, true, position, uniformDest.slow || 10); - }, false); + return stack(pat, ...optionsPats).withHaps((haps) => { + let dest; + let gain = 1; + let slow = 10; + let source; + haps.forEach((hap) => { + if (hap.context.uniformParam == 'dest') { + dest = hap.value; + } else if (hap.context.uniformParam == 'gain') { + gain = hap.value; + } else if (hap.context.uniformParam == 'slow') { + slow = hap.value; + } else { + source = hap; + } + }); + if (dest && source) { + if (onTrigger) { + source.context.onTrigger = (_, hap) => { + const [uniform, position] = getUniformPosition(hap.value, dest); + setUniform(instance, uniform, (hap.value.gain || 1) * gain, true, position, slow); + }; + source.context.dominantTrigger = false; + } else { + const [uniform, position] = getUniformPosition(source.value, dest); + setUniform(instance, uniform, source.value * gain, false, position, slow); + } + } + return haps; + }); }); diff --git a/packages/shader/uniform.test.mjs b/packages/shader/uniform.test.mjs deleted file mode 100644 index 7751ebd56..000000000 --- a/packages/shader/uniform.test.mjs +++ /dev/null @@ -1,22 +0,0 @@ -/* -uniform.test.mjs - -Copyright (C) 2024 Strudel contributors - see -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 { describe, it, expect } from 'vitest'; -import { parseUniformDest } from './uniform.mjs'; - -describe('Uniform', () => { - it('Parse simple', () => { - expect(parseUniformDest('iColor')).toStrictEqual({ name: 'iColor' }); - }); - it('Parse param', () => { - expect(parseUniformDest(['iColor', 'index', 2])).toStrictEqual({ name: 'iColor', position: 2 }); - expect(parseUniformDest(['iColor', 'index', 'seq'])).toStrictEqual({ name: 'iColor', position: null }); - expect(parseUniformDest(['iColor', 'gain', 3])).toStrictEqual({ name: 'iColor', gain: 3 }); - }); - it('Parse multi param', () => { - expect(parseUniformDest(['iColor', 'index', 2, 'gain', 3])).toStrictEqual({ name: 'iColor', position: 2, gain: 3 }); - }); -}); diff --git a/packages/shader/vite.config.js b/packages/shader/vite.config.js index b0e119c95..5df3edc1b 100644 --- a/packages/shader/vite.config.js +++ b/packages/shader/vite.config.js @@ -5,9 +5,6 @@ import { resolve } from 'path'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [], - test: { - setupFiles: './testSetup.mjs', - }, build: { lib: { entry: resolve(__dirname, 'index.mjs'), diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 4905690c6..d06011d35 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -8345,32 +8345,31 @@ exports[`runs examples > example "undegradeBy" example index 1 1`] = ` exports[`runs examples > example "uniform" example index 0 1`] = ` [ - "[ 0/1 → 1/1 | s:bd ]", - "[ 1/1 → 2/1 | s:bd ]", - "[ 2/1 → 3/1 | s:bd ]", - "[ 3/1 → 4/1 | s:bd ]", + "[ ~show() { + return this.begin.show() + ' → ' + this.end.show(); + } | pan:0.4999999999999998 ]", ] `; exports[`runs examples > example "uniform" example index 1 1`] = ` [ - "[ 0/1 → 1/1 | s:bd ]", - "[ 1/1 → 2/1 | s:bd ]", - "[ 2/1 → 3/1 | s:bd ]", - "[ 3/1 → 4/1 | s:bd ]", + "[ 0/1 → 1/1 | gain:0.5 ]", + "[ 1/1 → 2/1 | gain:0.3 ]", + "[ 2/1 → 3/1 | gain:0.5 ]", + "[ 3/1 → 4/1 | gain:0.3 ]", ] `; exports[`runs examples > example "uniform" example index 2 1`] = ` [ - "[ 0/1 → 1/2 | note:c3 ]", - "[ 1/2 → 1/1 | note:e3 ]", - "[ 1/1 → 3/2 | note:c3 ]", - "[ 3/2 → 2/1 | note:e3 ]", - "[ 2/1 → 5/2 | note:c3 ]", - "[ 5/2 → 3/1 | note:e3 ]", - "[ 3/1 → 7/2 | note:c3 ]", - "[ 7/2 → 4/1 | note:e3 ]", + "[ 0/1 → 1/2 | s:bd ]", + "[ 1/2 → 1/1 | s:sd ]", + "[ 1/1 → 3/2 | s:bd ]", + "[ 3/2 → 2/1 | s:sd ]", + "[ 2/1 → 5/2 | s:bd ]", + "[ 5/2 → 3/1 | s:sd ]", + "[ 3/1 → 7/2 | s:bd ]", + "[ 7/2 → 4/1 | s:sd ]", ] `; From f8a241401c1748b091314b3825de57b87f1495c4 Mon Sep 17 00:00:00 2001 From: Tristan de Cacqueray Date: Sun, 20 Oct 2024 22:00:42 -0400 Subject: [PATCH 16/21] Add uniform pattern function doc --- packages/shader/shader.mjs | 3 +-- packages/shader/uniform.mjs | 52 ++++++++++++++++++++++++++++--------- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/packages/shader/shader.mjs b/packages/shader/shader.mjs index f65b23f8a..6e1e0fad9 100644 --- a/packages/shader/shader.mjs +++ b/packages/shader/shader.mjs @@ -44,7 +44,7 @@ class UniformValue { get(elapsed) { // Adjust the value according to the rate of change - const offset = (this.desired - this.value) / (this.slow * Math.min(1, elapsed * 60)); + const offset = (this.desired - this.value) / (this.slow * Math.max(1, elapsed * 60)); // Ignore small changes if (Math.abs(offset) > 1e-3) this.value += offset; return this.value; @@ -72,7 +72,6 @@ export function setUniform(instanceName, name, value, incr, position, slow) { uniformValue = uniform.value[idx]; } uniformValue.slow = slow; - // TODO: handle direct assignment, this is incrementing by default if (incr) uniformValue.desired += value; else uniformValue.desired = value; } else { diff --git a/packages/shader/uniform.mjs b/packages/shader/uniform.mjs index 3e33b68da..58169b993 100644 --- a/packages/shader/uniform.mjs +++ b/packages/shader/uniform.mjs @@ -4,19 +4,31 @@ 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 { Pattern, reify, register, logger } from '@strudel/core'; +import { reify, register, logger } from '@strudel/core'; import { setUniform } from './shader.mjs'; /** - * Update a shader. + * Update a shader uniform value. A uniform name consists of + * + * - a name + * - optional array position seperated by ':'. The position can be an index number, or 'seq' | 'random' to assign a different index per events. + * + * The uniform can also be configured with an object to pass extra options: * * @name uniform + * @param {string} name: the uniform name and optional position. + * @param {number} gain: the value multiplier - defaults to 1. + * @param {number} slow: the value change rate, set to 1 for instant update - defaults to 10. + * @param {boolean} onTrigger: update the uniform only when the pattern trigger. In that case, the uniform position is mapped to the event note/sound when it is not explicity set. + * * @example * pan(sine.uniform("iColor")) * @example - * gain("<.5 .3>".uniform("rotations:seq")) + * gain(".5 .3").uniform("mod:0 mod:1") * @example - * s("bd sd").uniform({onTrigger: true, dest: "moveFWD"}) + * dist("<.5 .3>".uniform("rotations:seq")) + * @example + * s("bd sd").uniform({name: 'rotations', gain: 0.2, slow: "<5 20>", onTrigger: true}) */ export const uniform = register('uniform', (options, pat) => { // The shader instance name @@ -25,13 +37,7 @@ export const uniform = register('uniform', (options, pat) => { // Are the uniform updated on trigger const onTrigger = options.onTrigger || false; - const setCtx = (uniformParam) => (ctx) => ({ - uniformParam, - onTrigger: () => {}, - dominantTrigger: true, - ...ctx, - }); - + // Helper to assign uniform position const pitches = { _count: 0 }; const getPosition = (value, dest) => { if (typeof dest === 'number') return dest; @@ -45,9 +51,10 @@ export const uniform = register('uniform', (options, pat) => { } return pitches[note]; } else { - throw 'Invalid position' + dest; + logger('Invalid position' + dest, 'error'); } }; + // Helper to decode the uniform position const getUniformPosition = (value, dest) => { if (typeof dest === 'string') { return [dest, 0]; @@ -56,15 +63,31 @@ export const uniform = register('uniform', (options, pat) => { } }; + // The option pattern context + const setCtx = (uniformParam) => (ctx) => ({ + // The option name + uniformParam, + // Disable event trigger + onTrigger: () => {}, + dominantTrigger: true, + ...ctx, + }); + + // Collect the option patterns const optionsPats = []; if (Array.isArray(options) || typeof options === 'string') optionsPats.push(reify(options).withContext(setCtx('dest'))); else { + // dest was the initial name, that can be removed if (options.dest) optionsPats.push(reify(options.dest).withContext(setCtx('dest'))); + if (options.name) optionsPats.push(reify(options.name).withContext(setCtx('dest'))); if (options.gain) optionsPats.push(reify(options.gain).withContext(setCtx('gain'))); if (options.slow) optionsPats.push(reify(options.slow).withContext(setCtx('slow'))); } + + // Run the base pattern along with all the options return stack(pat, ...optionsPats).withHaps((haps) => { + // Collect the current options let dest; let gain = 1; let slow = 10; @@ -80,18 +103,23 @@ export const uniform = register('uniform', (options, pat) => { source = hap; } }); + if (dest && source) { if (onTrigger) { + // Set the uniform when the source trigger source.context.onTrigger = (_, hap) => { const [uniform, position] = getUniformPosition(hap.value, dest); setUniform(instance, uniform, (hap.value.gain || 1) * gain, true, position, slow); }; source.context.dominantTrigger = false; } else { + // Set the uniform now. const [uniform, position] = getUniformPosition(source.value, dest); setUniform(instance, uniform, source.value * gain, false, position, slow); } } + + // The options haps are kept so that the current values are highlighted on screen return haps; }); }); From 36d4d8905238269ba264bd6b2fff003c9f4216ab Mon Sep 17 00:00:00 2001 From: Tristan de Cacqueray Date: Mon, 21 Oct 2024 09:23:31 -0400 Subject: [PATCH 17/21] Fix lint error --- packages/shader/uniform.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shader/uniform.mjs b/packages/shader/uniform.mjs index 58169b993..d7bf23c13 100644 --- a/packages/shader/uniform.mjs +++ b/packages/shader/uniform.mjs @@ -4,7 +4,7 @@ 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 { reify, register, logger } from '@strudel/core'; +import { stack, reify, register, logger } from '@strudel/core'; import { setUniform } from './shader.mjs'; /** From 65cc317382edba5342c413b242a44fc444269041 Mon Sep 17 00:00:00 2001 From: Tristan de Cacqueray Date: Mon, 21 Oct 2024 10:19:49 -0400 Subject: [PATCH 18/21] Automatically set onTrigger when the hap value is not a number This change makes the following demo works: `s("bd").uniform("rot")` --- packages/shader/uniform.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/shader/uniform.mjs b/packages/shader/uniform.mjs index d7bf23c13..6e08f361a 100644 --- a/packages/shader/uniform.mjs +++ b/packages/shader/uniform.mjs @@ -19,7 +19,7 @@ import { setUniform } from './shader.mjs'; * @param {string} name: the uniform name and optional position. * @param {number} gain: the value multiplier - defaults to 1. * @param {number} slow: the value change rate, set to 1 for instant update - defaults to 10. - * @param {boolean} onTrigger: update the uniform only when the pattern trigger. In that case, the uniform position is mapped to the event note/sound when it is not explicity set. + * @param {boolean} onTrigger: update the uniform only when the pattern trigger. In that case, the uniform position is mapped to the event note/sound when it is not explicity set. onTrigger is automatically set when the pattern value is not a number. * * @example * pan(sine.uniform("iColor")) @@ -28,7 +28,7 @@ import { setUniform } from './shader.mjs'; * @example * dist("<.5 .3>".uniform("rotations:seq")) * @example - * s("bd sd").uniform({name: 'rotations', gain: 0.2, slow: "<5 20>", onTrigger: true}) + * s("bd sd").uniform({name: 'rotations', gain: 0.2, slow: "<5 20>"}) */ export const uniform = register('uniform', (options, pat) => { // The shader instance name @@ -105,7 +105,7 @@ export const uniform = register('uniform', (options, pat) => { }); if (dest && source) { - if (onTrigger) { + if (typeof source.value !== 'number' || onTrigger) { // Set the uniform when the source trigger source.context.onTrigger = (_, hap) => { const [uniform, position] = getUniformPosition(hap.value, dest); From 77bbb9626aef084cf9226197aa97426a2278f177 Mon Sep 17 00:00:00 2001 From: Tristan de Cacqueray Date: Sun, 27 Oct 2024 08:50:59 -0400 Subject: [PATCH 19/21] Remove uniform pattern function and expose the shader instance --- packages/shader/index.mjs | 1 - packages/shader/shader.mjs | 146 ++++++++++++---------- packages/shader/uniform.mjs | 125 ------------------ test/__snapshots__/examples.test.mjs.snap | 30 ----- test/runtime.mjs | 3 - 5 files changed, 82 insertions(+), 223 deletions(-) delete mode 100644 packages/shader/uniform.mjs diff --git a/packages/shader/index.mjs b/packages/shader/index.mjs index 931421e83..8045a1724 100644 --- a/packages/shader/index.mjs +++ b/packages/shader/index.mjs @@ -1,2 +1 @@ export * from './shader.mjs'; -export * from './uniform.mjs'; diff --git a/packages/shader/shader.mjs b/packages/shader/shader.mjs index 6e1e0fad9..5fd811852 100644 --- a/packages/shader/shader.mjs +++ b/packages/shader/shader.mjs @@ -34,88 +34,105 @@ void main(void) { `; } -// Modulation helpers to smooth the values. +// Helper class to handle uniform updates class UniformValue { - constructor() { - this.value = 0; - this.desired = 0; - this.slow = 10; + constructor(name, count, draw) { + this.name = name; + this.draw = draw; + this.isArray = count > 0; + this.value = new Array(Math.max(1, count)).fill(0); + this.frameModifier = new Array(Math.max(1, count)).fill(null); } - get(elapsed) { - // Adjust the value according to the rate of change - const offset = (this.desired - this.value) / (this.slow * Math.max(1, elapsed * 60)); - // Ignore small changes - if (Math.abs(offset) > 1e-3) this.value += offset; - return this.value; - } -} - -// Set an uniform value (from a pattern). -export function setUniform(instanceName, name, value, incr, position, slow) { - const instance = _instances[instanceName]; - if (!instance) { - logger('[shader] not loaded yet', 'warning'); - return; + incr(value, pos = 0) { + const idx = pos % this.value.length; + this.value[idx] += value; + this.frameModifier[idx] = null; + this.draw(); } - // console.log('setUniform: ', name, value, position, slow); - const uniform = instance.uniforms[name]; - if (uniform) { - let uniformValue; - if (uniform.count == 0) { - // This is a single value - uniformValue = uniform.value; + set(value, pos = 0) { + const idx = pos % this.value.length; + if (typeof value === 'function') { + this.frameModifier[idx] = value; } else { - // This is an array - const idx = position % uniform.value.length; - uniformValue = uniform.value[idx]; + this.value[idx] = value; + this.frameModifier[idx] = null; } - uniformValue.slow = slow; - if (incr) uniformValue.desired += value; - else uniformValue.desired = value; - } else { - logger('[shader] unknown uniform: ' + name); + this.draw(); + } + + get(pos = 0) { + return this.value[pos % this.value.length]; + } + + _frameUpdate(elapsed) { + this.value = this.value.map((value, idx) => + this.frameModifier[idx] ? this.frameModifier[idx](value, elapsed) : value, + ); + return this.isArray ? this.value : this.value[0]; } - // Ensure the instance is drawn - instance.age = 0; - if (!instance.drawing) { - instance.drawing = requestAnimationFrame(instance.update); + _resize(count) { + if (count != this.count) { + this.isArray = count > 0; + count = Math.max(1, count); + resizeArray(this.value, count, 0); + resizeArray(this.frameModifier, count, null); + } } } -// Update the uniforms for a given drawFrame call. -function updateUniforms(drawFrame, elapsed, uniforms) { - Object.values(uniforms).forEach((uniform) => { - const value = uniform.count == 0 ? uniform.value.get(elapsed) : uniform.value.map((v) => v.get(elapsed)); +// Shrink or extend an array +function resizeArray(arr, size, defval) { + if (arr.length > size) arr.length = size; + else arr.push(...new Array(size - arr.length).fill(defval)); +} - // Send the value to the GPU - // console.log('updateUniforms:', uniform.name, value); - drawFrame.uniform(uniform.name, value); - }); +// Get the size of an uniform +function uniformSize(funcName) { + if (funcName == 'uniform3fv') return 3; + else if (funcName == 'uniform4fv') return 4; + return 1; } // Setup the instance's uniform after shader compilation. -function setupUniforms(uniforms, program) { - Object.entries(program.uniforms).forEach(([name, uniform]) => { +function setupUniforms(instance, resetDraw = false) { + const newUniforms = new Set(); + const draw = () => { + // Start the drawing loop + instance.age = 0; + if (!instance.drawing) { + instance.drawing = requestAnimationFrame(instance.update); + } + }; + Object.entries(instance.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 values when the count change: - // if the count decreased, then drop the excess, else append new values - uniforms[uname] = { - name, - count, - value: count == 0 ? new UniformValue() : new Array(count).fill().map(() => new UniformValue()), - }; - } + newUniforms.add(uname); + const count = (uniform.count | 0) * uniformSize(uniform.glFuncName); + if (!instance.uniforms[uname]) instance.uniforms[uname] = new UniformValue(name, count, draw); + else instance.uniforms[uname]._resize(count); + if (resetDraw) instance.uniforms[uname].draw = draw; } }); - // TODO: remove previous uniform that are no longer used... - return uniforms; + + // Remove deleted uniforms + Object.keys(instance.uniforms).forEach((name) => { + if (!newUniforms.has(name)) delete instance.uniforms[name]; + }); +} + +// Update the uniforms for a given drawFrame call. +function updateUniforms(drawFrame, elapsed, uniforms) { + Object.values(uniforms).forEach((uniform) => { + const value = uniform._frameUpdate(elapsed); + + // Send the value to the GPU + // console.log('updateUniforms:', uniform.name, value); + drawFrame.uniform(uniform.name, value); + }); } // Setup the canvas and return the WebGL context. @@ -156,8 +173,8 @@ async function initializeShaderInstance(name, code) { .createPrograms([vertexShader, code]) .then(([program]) => { const drawFrame = app.createDrawCall(program, arrays); - const instance = { app, code, program, arrays, drawFrame, uniforms: setupUniforms({}, program) }; - + const instance = { app, code, program, arrays, drawFrame, uniforms: {} }; + setupUniforms(instance); // Render frame logic let prev = performance.now() / 1000; instance.age = 0; @@ -189,8 +206,8 @@ 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); + setupUniforms(instance, true); }); } @@ -207,4 +224,5 @@ export async function loadShader(code = '', name = 'default') { await reloadShaderInstanceCode(_instances[name], code); logger('[shader] reloaded'); } + return _instances[name]; } diff --git a/packages/shader/uniform.mjs b/packages/shader/uniform.mjs deleted file mode 100644 index 6e08f361a..000000000 --- a/packages/shader/uniform.mjs +++ /dev/null @@ -1,125 +0,0 @@ -/* -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 { stack, reify, register, logger } from '@strudel/core'; -import { setUniform } from './shader.mjs'; - -/** - * Update a shader uniform value. A uniform name consists of - * - * - a name - * - optional array position seperated by ':'. The position can be an index number, or 'seq' | 'random' to assign a different index per events. - * - * The uniform can also be configured with an object to pass extra options: - * - * @name uniform - * @param {string} name: the uniform name and optional position. - * @param {number} gain: the value multiplier - defaults to 1. - * @param {number} slow: the value change rate, set to 1 for instant update - defaults to 10. - * @param {boolean} onTrigger: update the uniform only when the pattern trigger. In that case, the uniform position is mapped to the event note/sound when it is not explicity set. onTrigger is automatically set when the pattern value is not a number. - * - * @example - * pan(sine.uniform("iColor")) - * @example - * gain(".5 .3").uniform("mod:0 mod:1") - * @example - * dist("<.5 .3>".uniform("rotations:seq")) - * @example - * s("bd sd").uniform({name: 'rotations', gain: 0.2, slow: "<5 20>"}) - */ -export const uniform = register('uniform', (options, pat) => { - // The shader instance name - const instance = options.instance || 'default'; - - // Are the uniform updated on trigger - const onTrigger = options.onTrigger || false; - - // Helper to assign uniform position - const pitches = { _count: 0 }; - const getPosition = (value, dest) => { - if (typeof dest === 'number') return dest; - else if (dest == 'seq') return pitches._count++; - else if (dest == 'random') return Math.floor(Math.random() * 1024); - else if (onTrigger) { - const note = value.note || value.n || value.sound || value.s; - if (pitches[note] === undefined) { - // Assign new value, the first note gets 0, then 1, then 2, ... - pitches[note] = Object.keys(pitches).length - 1; - } - return pitches[note]; - } else { - logger('Invalid position' + dest, 'error'); - } - }; - // Helper to decode the uniform position - const getUniformPosition = (value, dest) => { - if (typeof dest === 'string') { - return [dest, 0]; - } else { - return [dest[0], getPosition(value, dest[1])]; - } - }; - - // The option pattern context - const setCtx = (uniformParam) => (ctx) => ({ - // The option name - uniformParam, - // Disable event trigger - onTrigger: () => {}, - dominantTrigger: true, - ...ctx, - }); - - // Collect the option patterns - const optionsPats = []; - if (Array.isArray(options) || typeof options === 'string') - optionsPats.push(reify(options).withContext(setCtx('dest'))); - else { - // dest was the initial name, that can be removed - if (options.dest) optionsPats.push(reify(options.dest).withContext(setCtx('dest'))); - if (options.name) optionsPats.push(reify(options.name).withContext(setCtx('dest'))); - if (options.gain) optionsPats.push(reify(options.gain).withContext(setCtx('gain'))); - if (options.slow) optionsPats.push(reify(options.slow).withContext(setCtx('slow'))); - } - - // Run the base pattern along with all the options - return stack(pat, ...optionsPats).withHaps((haps) => { - // Collect the current options - let dest; - let gain = 1; - let slow = 10; - let source; - haps.forEach((hap) => { - if (hap.context.uniformParam == 'dest') { - dest = hap.value; - } else if (hap.context.uniformParam == 'gain') { - gain = hap.value; - } else if (hap.context.uniformParam == 'slow') { - slow = hap.value; - } else { - source = hap; - } - }); - - if (dest && source) { - if (typeof source.value !== 'number' || onTrigger) { - // Set the uniform when the source trigger - source.context.onTrigger = (_, hap) => { - const [uniform, position] = getUniformPosition(hap.value, dest); - setUniform(instance, uniform, (hap.value.gain || 1) * gain, true, position, slow); - }; - source.context.dominantTrigger = false; - } else { - // Set the uniform now. - const [uniform, position] = getUniformPosition(source.value, dest); - setUniform(instance, uniform, source.value * gain, false, position, slow); - } - } - - // The options haps are kept so that the current values are highlighted on screen - return haps; - }); -}); diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index d06011d35..a3419e98d 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -8343,36 +8343,6 @@ exports[`runs examples > example "undegradeBy" example index 1 1`] = ` ] `; -exports[`runs examples > example "uniform" example index 0 1`] = ` -[ - "[ ~show() { - return this.begin.show() + ' → ' + this.end.show(); - } | pan:0.4999999999999998 ]", -] -`; - -exports[`runs examples > example "uniform" example index 1 1`] = ` -[ - "[ 0/1 → 1/1 | gain:0.5 ]", - "[ 1/1 → 2/1 | gain:0.3 ]", - "[ 2/1 → 3/1 | gain:0.5 ]", - "[ 3/1 → 4/1 | gain:0.3 ]", -] -`; - -exports[`runs examples > example "uniform" example index 2 1`] = ` -[ - "[ 0/1 → 1/2 | s:bd ]", - "[ 1/2 → 1/1 | s:sd ]", - "[ 1/1 → 3/2 | s:bd ]", - "[ 3/2 → 2/1 | s:sd ]", - "[ 2/1 → 5/2 | s:bd ]", - "[ 5/2 → 3/1 | s:sd ]", - "[ 3/1 → 7/2 | s:bd ]", - "[ 7/2 → 4/1 | s:sd ]", -] -`; - exports[`runs examples > example "unison" example index 0 1`] = ` [ "[ 0/1 → 1/12 | note:d s:supersaw unison:1 ]", diff --git a/test/runtime.mjs b/test/runtime.mjs index 75105fbe1..19b716274 100644 --- a/test/runtime.mjs +++ b/test/runtime.mjs @@ -77,9 +77,6 @@ const toneHelpersMocked = { strudel.Pattern.prototype.osc = function () { return this; }; -strudel.Pattern.prototype.uniform = function () { - return this; -}; strudel.Pattern.prototype.csound = function () { return this; }; From 0d67176a98ae38f0c9967ccbdc0b66388dabb32a Mon Sep 17 00:00:00 2001 From: Tristan de Cacqueray Date: Sun, 27 Oct 2024 11:41:59 -0400 Subject: [PATCH 20/21] Improve the API when using a frame modifier --- packages/shader/shader.mjs | 75 +++++++++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 5 deletions(-) diff --git a/packages/shader/shader.mjs b/packages/shader/shader.mjs index 5fd811852..02c68d722 100644 --- a/packages/shader/shader.mjs +++ b/packages/shader/shader.mjs @@ -4,6 +4,64 @@ 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 . */ +/* +/// Here is a feature demo +// Setup a shader +let truchetFTW = await fetch('https://raw.githubusercontent.com/TristanCacqueray/shaders/refs/heads/main/shaders/Truchet%20%2B%20Kaleidoscope%20FTW.glsl').then((res) => res.text()) +// This shader provides the following uniforms: +// uniform float icolor; +// uniform float moveFWD; +// uniform float rotations[4]; +// uniform float modulations[6]; + +// Start the instance and binds the uniforms +let {uniforms} = await loadShader(truchetFTW) + +setcpm(96) + +// A smoothing function that is called for each frame +let smooth = (desired, speed) => (value) => value + ((desired - value) / speed) + +// Each kick updates a different rotation value. +let rotationIndex = 0 +$: s("bd").bank("RolandTR808") + .gain(2).dist("<1 .7 .7 .7>") + .mask("<1@30 0@2>") + .onTrigger(() => uniforms.rotations.set( + cur => smooth(cur + 1, 20), rotationIndex++), + false) + +// Each hat increase the icolor value. +$: sound("hh*4").bank("RolandTR808") + .room(.3).gain(".25 .3 .4") + .mask("<0@8 1@32>") + .onTrigger(() => uniforms.icolor.incr(0.1), false) + +// The snare smoothly increase the moveFWD value +$: s("cp/8").bank("RolandTR808") + .hpf(500).hpa(.8).hpenv("<-3 -2 -3 -2 -1>/8") + .room(0.5).roomsize(7).rlp(5000).gain(.2) + .onTrigger(() => uniforms.moveFWD.set(cur => smooth(cur + 1, 30)), false) + +// Each piano note updates a different modulations value +let pianoPitches = {} +$: note("*[2,2.02]") + .clip(1.1) + .transpose("<-12 -24 -12 0>/8") + // .sound("sawtooth") + .sound("triangle") + .cutoff(perlin.slow(5).range(20,1200)) + .room(.8).roomsize(.6) + .gain(.4) + .onTrigger((_, hap) => { + const n = hap.value.note + // assign unique array position for each new notes + if (!pianoPitches[n]) pianoPitches[n] = Object.keys(pianoPitches).length + 1 + const idx = pianoPitches[n] + uniforms.modulations.set(cur => smooth(cur + .5, 55), idx) + }, false) +*/ + import { PicoGL } from 'picogl'; import { logger } from '@strudel/core'; @@ -51,10 +109,11 @@ class UniformValue { this.draw(); } + // The value can be a function that will be called for each rendering frame set(value, pos = 0) { const idx = pos % this.value.length; if (typeof value === 'function') { - this.frameModifier[idx] = value; + this.frameModifier[idx] = value(this.value[idx]); } else { this.value[idx] = value; this.frameModifier[idx] = null; @@ -127,11 +186,16 @@ function setupUniforms(instance, resetDraw = false) { // Update the uniforms for a given drawFrame call. function updateUniforms(drawFrame, elapsed, uniforms) { Object.values(uniforms).forEach((uniform) => { - const value = uniform._frameUpdate(elapsed); + try { + const value = uniform._frameUpdate(elapsed); - // Send the value to the GPU - // console.log('updateUniforms:', uniform.name, value); - drawFrame.uniform(uniform.name, value); + // Send the value to the GPU + // console.log('updateUniforms:', uniform.name, value); + drawFrame.uniform(uniform.name, value); + } catch (err) { + console.warn('uniform error'); + console.error(err); + } }); } @@ -207,6 +271,7 @@ async function reloadShaderInstanceCode(instance, code) { instance.program.delete(); instance.program = program; instance.drawFrame = instance.app.createDrawCall(program, instance.arrays); + instance.code = code; setupUniforms(instance, true); }); } From fc335c993020e31158971071b153c39a2c9d4c7e Mon Sep 17 00:00:00 2001 From: Tristan de Cacqueray Date: Sun, 22 Dec 2024 07:51:37 -0500 Subject: [PATCH 21/21] Remove PicoGL dependency --- packages/shader/shader.mjs | 236 +++++++++++++++++++++++-------------- 1 file changed, 145 insertions(+), 91 deletions(-) diff --git a/packages/shader/shader.mjs b/packages/shader/shader.mjs index 02c68d722..37d3995d3 100644 --- a/packages/shader/shader.mjs +++ b/packages/shader/shader.mjs @@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th /* /// Here is a feature demo // Setup a shader -let truchetFTW = await fetch('https://raw.githubusercontent.com/TristanCacqueray/shaders/refs/heads/main/shaders/Truchet%20%2B%20Kaleidoscope%20FTW.glsl').then((res) => res.text()) +let truchetFTW = fetch('https://raw.githubusercontent.com/TristanCacqueray/shaders/refs/heads/main/shaders/Truchet%20%2B%20Kaleidoscope%20FTW.glsl').then((res) => res.text()) // This shader provides the following uniforms: // uniform float icolor; // uniform float moveFWD; @@ -62,7 +62,6 @@ $: note("*[2,2.02]") }, false) */ -import { PicoGL } from 'picogl'; import { logger } from '@strudel/core'; // The standard fullscreen vertex shader. @@ -94,14 +93,13 @@ void main(void) { // Helper class to handle uniform updates class UniformValue { - constructor(name, count, draw) { - this.name = name; + constructor(count, draw) { this.draw = draw; - this.isArray = count > 0; - this.value = new Array(Math.max(1, count)).fill(0); - this.frameModifier = new Array(Math.max(1, count)).fill(null); + this.value = new Array(count).fill(0); + this.frameModifier = new Array(count).fill(null); } + // Helper to perform a simple increment incr(value, pos = 0) { const idx = pos % this.value.length; this.value[idx] += value; @@ -125,16 +123,17 @@ class UniformValue { return this.value[pos % this.value.length]; } + // This function is called for every frame, allowing to run a smooth modifier _frameUpdate(elapsed) { this.value = this.value.map((value, idx) => this.frameModifier[idx] ? this.frameModifier[idx](value, elapsed) : value, ); - return this.isArray ? this.value : this.value[0]; + return this.value; } + // When the shader is update, this function adjust the number of values, preserving the current one _resize(count) { if (count != this.count) { - this.isArray = count > 0; count = Math.max(1, count); resizeArray(this.value, count, 0); resizeArray(this.frameModifier, count, null); @@ -148,15 +147,8 @@ function resizeArray(arr, size, defval) { else arr.push(...new Array(size - arr.length).fill(defval)); } -// Get the size of an uniform -function uniformSize(funcName) { - if (funcName == 'uniform3fv') return 3; - else if (funcName == 'uniform4fv') return 4; - return 1; -} - // Setup the instance's uniform after shader compilation. -function setupUniforms(instance, resetDraw = false) { +function setupUniforms(instance) { const newUniforms = new Set(); const draw = () => { // Start the drawing loop @@ -165,17 +157,46 @@ function setupUniforms(instance, resetDraw = false) { instance.drawing = requestAnimationFrame(instance.update); } }; - Object.entries(instance.program.uniforms).forEach(([name, uniform]) => { - if (name != 'iTime' && name != 'iResolution') { - // remove array suffix - const uname = name.replace('[0]', ''); - newUniforms.add(uname); - const count = (uniform.count | 0) * uniformSize(uniform.glFuncName); - if (!instance.uniforms[uname]) instance.uniforms[uname] = new UniformValue(name, count, draw); - else instance.uniforms[uname]._resize(count); - if (resetDraw) instance.uniforms[uname].draw = draw; + + // Collect every available uniforms + let gl = instance.gl; + const numUniforms = instance.gl.getProgramParameter(instance.program, gl.ACTIVE_UNIFORMS); + for (let i = 0; i < numUniforms; ++i) { + const inf = gl.getActiveUniform(instance.program, i); + + // Arrays have a `[0]` suffix in their name, drop that + const name = inf.name.replace('[0]', ''); + + // Figure out how many values is this uniform, and how to update it. + let count = inf.size; + let updateFunc = 'uniform1fv'; + switch (inf.type) { + case gl.FLOAT_VEC2: + count *= 2; + updateFunc = 'uniform2fv'; + break; + case gl.FLOAT_VEC3: + count *= 3; + updateFunc = 'uniform3fv'; + break; + case gl.FLOAT_VEC4: + count *= 4; + updateFunc = 'uniform4fv'; + break; } - }); + + // This is a new uniform + if (!instance.uniforms[name]) instance.uniforms[name] = new UniformValue(count, draw); + // This is a known uniform, make sure it's size is correct + else instance.uniforms[name]._resize(count); + + // Record it's location for the 'updateUniforms' below. + instance.uniforms[name].loc = gl.getUniformLocation(instance.program, inf.name); + instance.uniforms[name].updateFunc = updateFunc; + + // Record the name so that unused uniform can be deleted below + newUniforms.add(name); + } // Remove deleted uniforms Object.keys(instance.uniforms).forEach((name) => { @@ -184,14 +205,19 @@ function setupUniforms(instance, resetDraw = false) { } // Update the uniforms for a given drawFrame call. -function updateUniforms(drawFrame, elapsed, uniforms) { - Object.values(uniforms).forEach((uniform) => { +function updateUniforms(gl, now, elapsed, uniforms) { + Object.entries(uniforms).forEach(([name, uniform]) => { try { - const value = uniform._frameUpdate(elapsed); - - // Send the value to the GPU - // console.log('updateUniforms:', uniform.name, value); - drawFrame.uniform(uniform.name, value); + if (name == 'iTime') { + gl.uniform1f(uniform.loc, now); + } else if (name == 'iResolution') { + gl.uniform2f(uniform.loc, gl.canvas.width, gl.canvas.height); + } else { + const value = uniform._frameUpdate(elapsed); + // Send the value to the GPU + // console.log('updateUniforms:', name, uniform.updateFunc, value); + gl[uniform.updateFunc](uniform.loc, value); + } } catch (err) { console.warn('uniform error'); console.error(err); @@ -214,79 +240,107 @@ function setupCanvas(name) { return canvas.getContext('webgl2'); } -// Setup the shader instance -async function initializeShaderInstance(name, code) { - // Setup PicoGL app - const ctx = setupCanvas(name); - const app = PicoGL.createApp(ctx); +function createProgram(gl, vertex, fragment) { + const compile = (type, source) => { + const shader = gl.createShader(type); + gl.shaderSource(shader, source); + gl.compileShader(shader); + const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS); + if (!success) { + const err = gl.getShaderInfoLog(shader); + gl.deleteShader(shader); + throw err; + } + return shader; + }; + const program = gl.createProgram(); + gl.attachShader(program, compile(gl.VERTEX_SHADER, vertex)); + gl.attachShader(program, compile(gl.FRAGMENT_SHADER, fragment)); + gl.linkProgram(program); + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + const err = gl.getProgramInfoLog(program); + gl.deleteProgram(program); + throw err; + } + gl.useProgram(program); + return program; +} - // Setup buffers - const resolution = new Float32Array([ctx.canvas.width, ctx.canvas.height]); +// Setup the shader instance +function initializeShaderInstance(name, code) { + const gl = setupCanvas(name); // 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(instance); - // Render frame logic - let prev = performance.now() / 1000; - instance.age = 0; - instance.update = () => { - const now = performance.now() / 1000; - const elapsed = instance.age == 0 ? 1 / 60 : 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(); - // After sometime, if no update happened, stop the animation loop - if (instance.age++ < 100) requestAnimationFrame(instance.update); - else instance.drawing = false; - }; - return instance; - }) - .catch((err) => { - ctx.canvas.remove(); - throw err; - }); + const mkPositionArray = () => { + const buf = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buf); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW); + const vao = gl.createVertexArray(); + gl.bindVertexArray(vao); + gl.enableVertexAttribArray(0); + gl.vertexAttribPointer(buf, 2, gl.FLOAT, false, 0, 0); + return vao; + }; + + try { + let array = mkPositionArray(); + let program = createProgram(gl, vertexShader, code); + const instance = { gl, code, program, array, uniforms: {} }; + setupUniforms(instance); + // Render frame logic + let prev = performance.now() / 1000; + instance.age = 0; + instance.update = () => { + const now = performance.now() / 1000; + const elapsed = instance.age == 0 ? 1 / 60 : now - prev; + prev = now; + // console.log('drawing!'); + + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + + // Clear the canvas + gl.clearColor(0, 0, 0, 0); + gl.clear(gl.COLOR_BUFFER_BIT); + gl.bindVertexArray(array); + + // Send the uniform values to the GPU + updateUniforms(instance.gl, now, elapsed, instance.uniforms); + + // Draw the quad + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + + // After sometime, if no update happened, stop the animation loop to save cpu cycles + if (instance.age++ < 100) requestAnimationFrame(instance.update); + else instance.drawing = false; + }; + instance.update(); + return instance; + } catch (err) { + gl.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.drawFrame = instance.app.createDrawCall(program, instance.arrays); - instance.code = code; - setupUniforms(instance, true); - }); +function reloadShaderInstanceCode(instance, code) { + const program = createProgram(instance.gl, vertexShader, code); + instance.gl.deleteProgram(instance.program); + instance.program = program; + instance.code = code; + setupUniforms(instance); } // Keep track of the running shader instances let _instances = {}; -export async function loadShader(code = '', name = 'default') { +export function loadShader(code = '', name = 'default') { if (code) { code = mkFragmentShader(code); } if (!_instances[name]) { - _instances[name] = await initializeShaderInstance(name, code); + _instances[name] = initializeShaderInstance(name, code); logger('[shader] ready'); } else if (_instances[name].code != code) { - await reloadShaderInstanceCode(_instances[name], code); + reloadShaderInstanceCode(_instances[name], code); logger('[shader] reloaded'); } return _instances[name];