mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-23 05:33:13 -04:00
Update uniform value and add tests
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
+29
-34
@@ -34,50 +34,47 @@ void main(void) {
|
||||
`;
|
||||
}
|
||||
|
||||
// Modulation helpers.
|
||||
const hardModulation = () => {
|
||||
let val = 0;
|
||||
return {
|
||||
get: () => val,
|
||||
set: (v) => {
|
||||
val = v;
|
||||
},
|
||||
};
|
||||
};
|
||||
// Modulation helpers to smooth the values.
|
||||
class UniformValue {
|
||||
constructor() {
|
||||
this.value = 0;
|
||||
this.desired = 0;
|
||||
this.slow = 10;
|
||||
}
|
||||
|
||||
const decayModulation = (decay) => {
|
||||
let val = 0;
|
||||
let desired = 0;
|
||||
return {
|
||||
get: (ts) => {
|
||||
val += (desired - val) / decay;
|
||||
return val;
|
||||
},
|
||||
set: (v) => {
|
||||
desired = val + v;
|
||||
},
|
||||
};
|
||||
};
|
||||
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();
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// Fix `ReferenceError: self is not defined`
|
||||
// when importing picogl in tests
|
||||
globalThis.self = {};
|
||||
+38
-27
@@ -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);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
uniform.test.mjs - <short description TODO>
|
||||
Copyright (C) 2024 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/pattern.test.mjs>
|
||||
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 { 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 });
|
||||
});
|
||||
});
|
||||
@@ -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'),
|
||||
|
||||
Generated
+4
@@ -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==}
|
||||
|
||||
Reference in New Issue
Block a user