Compare commits

...

24 Commits

Author SHA1 Message Date
Tristan de Cacqueray fc335c9930 Remove PicoGL dependency 2024-12-22 08:04:07 -05:00
Tristan de Cacqueray 0d67176a98 Improve the API when using a frame modifier 2024-10-27 11:46:49 -04:00
Tristan de Cacqueray 77bbb9626a Remove uniform pattern function and expose the shader instance 2024-10-27 10:17:41 -04:00
Tristan de Cacqueray 65cc317382 Automatically set onTrigger when the hap value is not a number
This change makes the following demo works: `s("bd").uniform("rot")`
2024-10-21 10:20:50 -04:00
Tristan de Cacqueray 36d4d89052 Fix lint error 2024-10-21 09:23:31 -04:00
Tristan de Cacqueray f8a241401c Add uniform pattern function doc 2024-10-20 22:06:41 -04:00
Tristan de Cacqueray 1baa6da370 Update uniform method to take an option object 2024-10-20 19:55:12 -04:00
Tristan de Cacqueray 9e0b840afd Fix default uniformTrigger indexs 2024-10-20 15:13:49 -04:00
Felix Roos e6fbef95f2 Merge branch 'main' into shader 2024-10-20 21:07:26 +02:00
Tristan de Cacqueray cad11e1e48 Update uniform value and add tests 2024-10-20 13:46:33 -04:00
Tristan de Cacqueray b85a5a9179 Use pat.withValue and add uniformTrigger 2024-10-20 09:58:10 -04:00
Tristan de Cacqueray 9b4675f1ce Update the shader todos 2024-10-19 09:24:01 -04:00
Felix Roos 67c0ebaf54 snapshot 2024-10-19 15:17:09 +02:00
Felix Roos 8d1180f87a fix: test runtime for uniform function 2024-10-19 15:15:25 +02:00
Felix Roos fcd550b5bd codeformat 2024-10-19 15:13:21 +02:00
Felix Roos d3e624f673 Merge branch 'shader' of https://github.com/TristanCacqueray/strudel into shader 2024-10-19 15:12:02 +02:00
Tristan de Cacqueray b0149f8190 Refactor the uniform function and add documentation 2024-10-19 09:08:33 -04:00
Felix Roos bf3a095852 codeformat 2024-10-19 15:07:48 +02:00
Tristan de Cacqueray 5c983ceb8f Extract the uniform helper to its own module 2024-10-19 06:43:50 -04:00
Tristan de Cacqueray 735d531cce Move shader module into it's own package 2024-10-19 06:29:19 -04:00
Felix Roos 13168bb9bc codeformat 2024-10-19 01:15:26 +02:00
Felix Roos 46b4aff0ef Merge remote-tracking branch 'origin/main' into shader 2024-10-19 01:14:06 +02:00
Tristan de Cacqueray 1b8998ac14 Fix shader reload
The attribute for the draw call is called drawFrame, not draw.
2024-10-18 18:15:15 -04:00
Tristan de Cacqueray f0ddb2d595 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'})
```
2024-10-18 11:40:09 -04:00
10 changed files with 452 additions and 0 deletions
+1
View File
@@ -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'),
);
+1
View File
@@ -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'),
+16
View File
@@ -0,0 +1,16 @@
# @strudel/shader
Helpers for drawing shader.
## Todos
Here are the things that needs to be implemented:
- [ ] Shader compilation error reporting, e.g. to show the line number
- [ ] Shader import from url, like shadertoy or git
- [ ] 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
+1
View File
@@ -0,0 +1 @@
export * from './shader.mjs';
+40
View File
@@ -0,0 +1,40 @@
{
"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 <flix91@gmail.com>",
"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",
"vitest": "^2.1.3"
}
}
+347
View File
@@ -0,0 +1,347 @@
/*
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 <https://www.gnu.org/licenses/>.
*/
/*
/// Here is a feature demo
// Setup a shader
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;
// 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("<C D G A Bb D C A G D Bb A>*[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 { 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;
#define STRUDEL 1
${code}
void main(void) {
mainImage(oColor, gl_FragCoord.xy);
}
`;
}
// Helper class to handle uniform updates
class UniformValue {
constructor(count, draw) {
this.draw = draw;
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;
this.frameModifier[idx] = null;
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.value[idx]);
} else {
this.value[idx] = value;
this.frameModifier[idx] = null;
}
this.draw();
}
get(pos = 0) {
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.value;
}
// When the shader is update, this function adjust the number of values, preserving the current one
_resize(count) {
if (count != this.count) {
count = Math.max(1, count);
resizeArray(this.value, count, 0);
resizeArray(this.frameModifier, count, null);
}
}
}
// 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));
}
// Setup the instance's uniform after shader compilation.
function setupUniforms(instance) {
const newUniforms = new Set();
const draw = () => {
// Start the drawing loop
instance.age = 0;
if (!instance.drawing) {
instance.drawing = requestAnimationFrame(instance.update);
}
};
// 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) => {
if (!newUniforms.has(name)) delete instance.uniforms[name];
});
}
// Update the uniforms for a given drawFrame call.
function updateUniforms(gl, now, elapsed, uniforms) {
Object.entries(uniforms).forEach(([name, uniform]) => {
try {
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);
}
});
}
// 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');
}
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 the shader instance
function initializeShaderInstance(name, code) {
const gl = setupCanvas(name);
// Two triangle to cover the whole canvas
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
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 function loadShader(code = '', name = 'default') {
if (code) {
code = mkFragmentShader(code);
}
if (!_instances[name]) {
_instances[name] = initializeShaderInstance(name, code);
logger('[shader] ready');
} else if (_instances[name].code != code) {
reloadShaderInstanceCode(_instances[name], code);
logger('[shader] reloaded');
}
return _instances[name];
}
+19
View File
@@ -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',
},
});
+25
View File
@@ -406,6 +406,22 @@ 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)
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:
'@strudel/core':
@@ -629,6 +645,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 +6103,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'}
@@ -7740,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==}
@@ -14789,6 +14812,8 @@ snapshots:
picocolors@1.1.1: {}
picogl@0.17.9: {}
picomatch@2.3.1: {}
picomatch@4.0.2: {}
+1
View File
@@ -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:*",
+1
View File
@@ -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'),