From 2cf77b05c0c2bc76066fcc5a0f7573eee45f4f0e Mon Sep 17 00:00:00 2001 From: Tristan de Cacqueray Date: Sun, 27 Oct 2024 08:50:59 -0400 Subject: [PATCH] Remove uniform pattern function and expose the shader instance --- packages/shader/index.mjs | 1 - packages/shader/shader.mjs | 145 ++++++++++++---------- packages/shader/uniform.mjs | 125 ------------------- test/__snapshots__/examples.test.mjs.snap | 30 ----- test/runtime.mjs | 3 - 5 files changed, 81 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..ed4a3992a 100644 --- a/packages/shader/shader.mjs +++ b/packages/shader/shader.mjs @@ -34,88 +34,104 @@ 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) { + const newUniforms = new Set(); + 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, () => { + // Start the drawing loop + instance.age = 0; + if (!instance.drawing) { + instance.drawing = requestAnimationFrame(instance.update); + } + }); + else instance.uniforms[uname]._resize(count); } }); - // 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 +172,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 +205,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); }); } @@ -207,4 +223,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; };