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