Update uniform value and add tests

This commit is contained in:
Tristan de Cacqueray
2024-10-20 13:03:26 -04:00
parent b85a5a9179
commit cad11e1e48
7 changed files with 102 additions and 62 deletions
+3 -1
View File
@@ -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
View File
@@ -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();
+3
View File
@@ -0,0 +1,3 @@
// Fix `ReferenceError: self is not defined`
// when importing picogl in tests
globalThis.self = {};
+38 -27
View File
@@ -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);
});
+22
View File
@@ -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 });
});
});
+3
View File
@@ -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'),
+4
View File
@@ -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==}