Skip to content
This repository was archived by the owner on Jun 19, 2025. It is now read-only.

Commit f0ddb2d

Browse files
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'}) ```
1 parent ad080d0 commit f0ddb2d

File tree

3 files changed

+233
-1
lines changed

3 files changed

+233
-1
lines changed

packages/draw/index.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ export * from './animate.mjs';
22
export * from './color.mjs';
33
export * from './draw.mjs';
44
export * from './pianoroll.mjs';
5+
export * from './shader.mjs';
56
export * from './spiral.mjs';
67
export * from './pitchwheel.mjs';

packages/draw/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
},
3030
"homepage": "https://github.com/tidalcycles/strudel#readme",
3131
"dependencies": {
32-
"@strudel/core": "workspace:*"
32+
"@strudel/core": "workspace:*",
33+
"picogl": "^0.17.9"
3334
},
3435
"devDependencies": {
3536
"vite": "^5.0.10"

packages/draw/shader.mjs

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
/*
2+
shader.mjs - implements the `loadShader` helper and `shader` pattern function
3+
Copyright (C) 2024 Strudel contributors
4+
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/>.
5+
*/
6+
7+
import { PicoGL } from "picogl";
8+
import { register, logger } from '@strudel/core';
9+
10+
// The standard fullscreen vertex shader.
11+
const vertexShader = `#version 300 es
12+
precision highp float;
13+
layout(location=0) in vec2 position;
14+
void main() {
15+
gl_Position = vec4(position, 1, 1);
16+
}
17+
`;
18+
19+
// Make the fragment source, similar to the one from shadertoy.
20+
function mkFragmentShader(code) {
21+
return `#version 300 es
22+
precision highp float;
23+
out vec4 oColor;
24+
uniform float iTime;
25+
uniform vec2 iResolution;
26+
27+
${code}
28+
29+
void main(void) {
30+
mainImage(oColor, gl_FragCoord.xy);
31+
}
32+
`
33+
}
34+
35+
// Modulation helpers.
36+
const hardModulation = () => {
37+
let val = 0;
38+
return {
39+
get: () => val,
40+
set: (v) => { val = v },
41+
}
42+
}
43+
44+
const decayModulation = (decay) => {
45+
let val = 0;
46+
let desired = 0
47+
return {
48+
get: (ts) => {
49+
val += (desired - val) / decay
50+
return val
51+
},
52+
set: (v) => { desired = val + v },
53+
}
54+
}
55+
56+
// Set an uniform value (from a pattern).
57+
function setUniform(instance, name, value, position) {
58+
const uniform = instance.uniforms[name]
59+
if (uniform) {
60+
if (uniform.count == 0) {
61+
// This is a single value
62+
uniform.mod.set(value)
63+
} else {
64+
// This is an array
65+
const idx = position % uniform.mod.length
66+
uniform.mod[idx].set(value)
67+
}
68+
} else {
69+
logger('[shader] unknown uniform: ' + name)
70+
}
71+
72+
// Ensure the instance is drawn
73+
instance.age = 0
74+
if (!instance.drawing) {
75+
instance.drawing = requestAnimationFrame(instance.update)
76+
}
77+
}
78+
79+
// Update the uniforms for a given drawFrame call.
80+
function updateUniforms(drawFrame, elapsed, uniforms) {
81+
Object.values(uniforms).forEach((uniform) => {
82+
const value = uniform.count == 0
83+
? uniform.mod.get(elapsed)
84+
: uniform.value.map((_, i) => uniform.mod[i].get(elapsed))
85+
// Send the value to the GPU
86+
drawFrame.uniform(uniform.name, value)
87+
})
88+
}
89+
90+
// Setup the instance's uniform after shader compilation.
91+
function setupUniforms(uniforms, program) {
92+
Object.entries(program.uniforms).forEach(([name, uniform]) => {
93+
if (name != "iTime" && name != "iResolution") {
94+
// remove array suffix
95+
const uname = name.replace("[0]", "")
96+
const count = uniform.count | 0
97+
if (!uniforms[uname] || uniforms[uname].count != count) {
98+
// TODO: keep the previous value when the count change...
99+
uniforms[uname] = {
100+
name,
101+
count,
102+
value: count == 0 ? 0 : new Float32Array(count),
103+
mod: count == 0 ? decayModulation(50) : new Array(count).fill().map(() => decayModulation(50))
104+
}
105+
}
106+
}
107+
})
108+
// TODO: remove previous uniform that are no longer used...
109+
return uniforms
110+
}
111+
112+
// Setup the canvas and return the WebGL context.
113+
function setupCanvas(name) {
114+
// TODO: support custom size
115+
const width = 400; const height = 300;
116+
const canvas = document.createElement('canvas');
117+
canvas.id = "cnv-" + name
118+
canvas.width = width
119+
canvas.height = height
120+
const top = 60 + Object.keys(_instances).length * height
121+
canvas.style = `pointer-events:none;width:${width}px;height:${height}px;position:fixed;top:${top}px;right:23px`;
122+
document.body.append(canvas)
123+
return canvas.getContext("webgl2")
124+
}
125+
126+
// Setup the shader instance
127+
async function initializeShaderInstance(name, code) {
128+
// Setup PicoGL app
129+
const ctx = setupCanvas(name)
130+
console.log(ctx)
131+
const app = PicoGL.createApp(ctx);
132+
app.resize(400, 300)
133+
134+
// Setup buffers
135+
const resolution = new Float32Array([ctx.canvas.width, ctx.canvas.height])
136+
137+
// Two triangle to cover the whole canvas
138+
const positionBuffer = app.createVertexBuffer(PicoGL.FLOAT, 2, new Float32Array([
139+
-1, -1, -1, 1, 1, 1,
140+
1, 1, 1, -1, -1, -1,
141+
]))
142+
143+
// Setup the arrays
144+
const arrays = app.createVertexArray().vertexAttributeBuffer(0, positionBuffer);
145+
146+
return app.createPrograms([vertexShader, code]).then(([program]) => {
147+
const drawFrame = app.createDrawCall(program, arrays)
148+
const instance = {app, code, program, arrays, drawFrame, uniforms: setupUniforms({}, program)}
149+
150+
// Render frame logic
151+
let prev = performance.now() / 1000;
152+
instance.age = 0
153+
instance.update = () => {
154+
const now = performance.now() / 1000;
155+
const elapsed = now - prev
156+
prev = now
157+
// console.log("drawing!")
158+
app.clear()
159+
instance.drawFrame
160+
.uniform("iResolution", resolution)
161+
.uniform("iTime", now)
162+
163+
updateUniforms(instance.drawFrame, elapsed, instance.uniforms)
164+
165+
instance.drawFrame.draw()
166+
if (instance.age++ < 100)
167+
requestAnimationFrame(instance.update)
168+
else
169+
instance.drawing = false
170+
}
171+
return instance
172+
}).catch((err) => {
173+
ctx.canvas.remove()
174+
throw err
175+
})
176+
}
177+
178+
// Update the instance program
179+
async function reloadShaderInstanceCode(instance, code) {
180+
return instance.app.createPrograms([vertexShader, code]).then(([program]) => {
181+
instance.program.delete()
182+
instance.program = program
183+
instance.uniforms = setupUniforms(instance.uniforms, program)
184+
instance.draw = instance.app.createDrawCall(program, instance.arrays)
185+
})
186+
}
187+
188+
// Keep track of the running shader instances
189+
let _instances = {}
190+
export async function loadShader(code = '', name = 'default') {
191+
if (code) {
192+
code = mkFragmentShader(code)
193+
}
194+
if (!_instances[name]) {
195+
_instances[name] = await initializeShaderInstance(name, code)
196+
logger('[shader] ready')
197+
} else if (_instances[name].code != code) {
198+
await reloadShaderInstanceCode(_instances[name], code)
199+
logger('[shader] reloaded')
200+
}
201+
}
202+
203+
export const shader = register('shader', (options, pat) => {
204+
// Keep track of the pitches value: Map String Int
205+
const pitches = {_count: 0};
206+
207+
return pat.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
208+
const instance = _instances[options.instance || "default"]
209+
if (!instance) {
210+
logger('[shader] not loaded yet', 'warning')
211+
return
212+
}
213+
214+
const value = options.gain || 1.0;
215+
if (options.pitch !== undefined) {
216+
const note = hap.value.note || hap.value.s;
217+
if (pitches[note] === undefined) {
218+
// Assign new value, the first note gets 0, then 1, then 2, ...
219+
pitches[note] = Object.keys(pitches).length
220+
}
221+
setUniform(instance, options.pitch, value, pitches[note])
222+
} else if (options.seq !== undefined) {
223+
setUniform(instance, options.seq, value, pitches._count++)
224+
} else if (options.uniform !== undefined) {
225+
setUniform(instance, options.uniform, value)
226+
} else {
227+
console.error("Unknown shader options, need either pitch or uniform", options)
228+
}
229+
}, false)
230+
})

0 commit comments

Comments
 (0)