mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-21 20:55:12 -04:00
Remove uniform pattern function and expose the shader instance
This commit is contained in:
@@ -1,2 +1 @@
|
||||
export * from './shader.mjs';
|
||||
export * from './uniform.mjs';
|
||||
|
||||
+84
-66
@@ -34,61 +34,100 @@ 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;
|
||||
incr(value, pos = 0) {
|
||||
const idx = pos % this.value.length;
|
||||
this.value[idx] += value;
|
||||
this.frameModifier[idx] = null;
|
||||
this.draw();
|
||||
}
|
||||
|
||||
set(value, pos = 0) {
|
||||
const idx = pos % this.value.length;
|
||||
if (typeof value === 'function') {
|
||||
this.frameModifier[idx] = value;
|
||||
} else {
|
||||
this.value[idx] = value;
|
||||
this.frameModifier[idx] = null;
|
||||
}
|
||||
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];
|
||||
}
|
||||
|
||||
_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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
// 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));
|
||||
}
|
||||
|
||||
// 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;
|
||||
} else {
|
||||
// This is an array
|
||||
const idx = position % uniform.value.length;
|
||||
uniformValue = uniform.value[idx];
|
||||
// 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) {
|
||||
const newUniforms = new Set();
|
||||
const draw = () => {
|
||||
// Start the drawing loop
|
||||
instance.age = 0;
|
||||
if (!instance.drawing) {
|
||||
instance.drawing = requestAnimationFrame(instance.update);
|
||||
}
|
||||
uniformValue.slow = slow;
|
||||
if (incr) uniformValue.desired += value;
|
||||
else uniformValue.desired = value;
|
||||
} else {
|
||||
logger('[shader] unknown uniform: ' + name);
|
||||
}
|
||||
};
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure the instance is drawn
|
||||
instance.age = 0;
|
||||
if (!instance.drawing) {
|
||||
instance.drawing = requestAnimationFrame(instance.update);
|
||||
}
|
||||
// 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.count == 0 ? uniform.value.get(elapsed) : uniform.value.map((v) => v.get(elapsed));
|
||||
const value = uniform._frameUpdate(elapsed);
|
||||
|
||||
// Send the value to the GPU
|
||||
// console.log('updateUniforms:', uniform.name, value);
|
||||
@@ -96,28 +135,6 @@ function updateUniforms(drawFrame, elapsed, uniforms) {
|
||||
});
|
||||
}
|
||||
|
||||
// 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 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()),
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
// 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
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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;
|
||||
});
|
||||
});
|
||||
@@ -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 ]",
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user