Add shadertoy-server example for WebGL shader rendering (#226)
Introduces a new MCP App example that renders ShaderToy-compatible GLSL fragment shaders in real-time using WebGL 2.0 and ShaderToyLite.js. Features: - Standard ShaderToy uniforms (`iTime`, `iResolution`, `iMouse`, etc.) - Multi-pass rendering with buffers A-D for feedback effects - Shader compilation error overlay Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b273279849
commit
ec3409aa91
5 changed files with 1641 additions and 1901 deletions
12
examples/shadertoy-server/src/global.css
Normal file
12
examples/shadertoy-server/src/global.css
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 1em;
|
||||
}
|
||||
12
examples/shadertoy-server/src/mcp-app.css
Normal file
12
examples/shadertoy-server/src/mcp-app.css
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
html, body {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
126
examples/shadertoy-server/src/mcp-app.ts
Normal file
126
examples/shadertoy-server/src/mcp-app.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
/**
|
||||
* ShaderToy renderer MCP App using ShaderToyLite.js
|
||||
*/
|
||||
import { App, type McpUiHostContext } from "@modelcontextprotocol/ext-apps";
|
||||
import "./global.css";
|
||||
import "./mcp-app.css";
|
||||
import ShaderToyLite, {
|
||||
type ShaderToyLiteInstance,
|
||||
} from "./vendor/ShaderToyLite.js";
|
||||
|
||||
interface ShaderInput {
|
||||
fragmentShader: string;
|
||||
common?: string;
|
||||
bufferA?: string;
|
||||
bufferB?: string;
|
||||
bufferC?: string;
|
||||
bufferD?: string;
|
||||
}
|
||||
|
||||
function isShaderInput(value: unknown): value is ShaderInput {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
typeof (value as Record<string, unknown>).fragmentShader === "string"
|
||||
);
|
||||
}
|
||||
|
||||
const log = {
|
||||
info: console.log.bind(console, "[APP]"),
|
||||
warn: console.warn.bind(console, "[APP]"),
|
||||
error: console.error.bind(console, "[APP]"),
|
||||
};
|
||||
|
||||
// Get element references
|
||||
const mainEl = document.querySelector(".main") as HTMLElement;
|
||||
const canvas = document.getElementById("canvas") as HTMLCanvasElement;
|
||||
// Resize canvas to fill viewport
|
||||
function resizeCanvas() {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
}
|
||||
resizeCanvas();
|
||||
window.addEventListener("resize", resizeCanvas);
|
||||
|
||||
// Handle host context changes (safe area insets)
|
||||
function handleHostContextChanged(ctx: McpUiHostContext) {
|
||||
if (ctx.safeAreaInsets) {
|
||||
mainEl.style.paddingTop = `${ctx.safeAreaInsets.top}px`;
|
||||
mainEl.style.paddingRight = `${ctx.safeAreaInsets.right}px`;
|
||||
mainEl.style.paddingBottom = `${ctx.safeAreaInsets.bottom}px`;
|
||||
mainEl.style.paddingLeft = `${ctx.safeAreaInsets.left}px`;
|
||||
}
|
||||
}
|
||||
|
||||
// ShaderToyLite instance
|
||||
let shaderToy: ShaderToyLiteInstance | null = null;
|
||||
|
||||
// Create app instance
|
||||
const app = new App({ name: "ShaderToy Renderer", version: "1.0.0" });
|
||||
|
||||
app.onteardown = async () => {
|
||||
log.info("App is being torn down");
|
||||
if (shaderToy) {
|
||||
shaderToy.pause();
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
app.ontoolinput = (params) => {
|
||||
log.info("Received shader input");
|
||||
|
||||
if (!isShaderInput(params.arguments)) {
|
||||
log.error("Invalid tool input");
|
||||
return;
|
||||
}
|
||||
|
||||
const { fragmentShader, common, bufferA, bufferB, bufferC, bufferD } =
|
||||
params.arguments;
|
||||
|
||||
// Initialize ShaderToyLite if needed
|
||||
if (!shaderToy) {
|
||||
shaderToy = new ShaderToyLite("canvas");
|
||||
}
|
||||
|
||||
// Set common code (shared across all shaders)
|
||||
shaderToy.setCommon(common || "");
|
||||
|
||||
// Set buffer shaders with self-feedback
|
||||
if (bufferA) {
|
||||
shaderToy.setBufferA({ source: bufferA, iChannel0: "A" });
|
||||
}
|
||||
if (bufferB) {
|
||||
shaderToy.setBufferB({ source: bufferB, iChannel1: "B" });
|
||||
}
|
||||
if (bufferC) {
|
||||
shaderToy.setBufferC({ source: bufferC, iChannel2: "C" });
|
||||
}
|
||||
if (bufferD) {
|
||||
shaderToy.setBufferD({ source: bufferD, iChannel3: "D" });
|
||||
}
|
||||
|
||||
// Set main Image shader with buffer inputs
|
||||
shaderToy.setImage({
|
||||
source: fragmentShader,
|
||||
iChannel0: bufferA ? "A" : undefined,
|
||||
iChannel1: bufferB ? "B" : undefined,
|
||||
iChannel2: bufferC ? "C" : undefined,
|
||||
iChannel3: bufferD ? "D" : undefined,
|
||||
});
|
||||
|
||||
shaderToy.play();
|
||||
log.info("Setup complete");
|
||||
};
|
||||
|
||||
app.onerror = log.error;
|
||||
|
||||
app.onhostcontextchanged = handleHostContextChanged;
|
||||
|
||||
// Connect to host
|
||||
app.connect().then(() => {
|
||||
log.info("Connected to host");
|
||||
const ctx = app.getHostContext();
|
||||
if (ctx) {
|
||||
handleHostContextChanged(ctx);
|
||||
}
|
||||
});
|
||||
113
examples/shadertoy-server/src/vendor/ShaderToyLite.d.ts
vendored
Normal file
113
examples/shadertoy-server/src/vendor/ShaderToyLite.d.ts
vendored
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
/**
|
||||
* Type declarations for ShaderToyLite.js
|
||||
* https://github.com/chipweinberger/ShaderToyLite.js
|
||||
*/
|
||||
|
||||
/** Valid buffer keys for channel inputs */
|
||||
type BufferKey = "A" | "B" | "C" | "D";
|
||||
|
||||
/** Configuration for shader passes (buffers and image) */
|
||||
interface ShaderConfig {
|
||||
/** GLSL fragment shader source code */
|
||||
source?: string;
|
||||
/** Buffer to use as iChannel0 input */
|
||||
iChannel0?: BufferKey;
|
||||
/** Buffer to use as iChannel1 input */
|
||||
iChannel1?: BufferKey;
|
||||
/** Buffer to use as iChannel2 input */
|
||||
iChannel2?: BufferKey;
|
||||
/** Buffer to use as iChannel3 input */
|
||||
iChannel3?: BufferKey;
|
||||
}
|
||||
|
||||
/** ShaderToyLite instance methods */
|
||||
interface ShaderToyLiteInstance {
|
||||
/**
|
||||
* Set common shader code shared across all passes.
|
||||
* @param source - GLSL source code to prepend to all shaders
|
||||
*/
|
||||
setCommon(source?: string | null): void;
|
||||
|
||||
/**
|
||||
* Set Buffer A shader pass.
|
||||
* @param config - Shader configuration or falsy to clear
|
||||
*/
|
||||
setBufferA(config?: ShaderConfig | null): void;
|
||||
|
||||
/**
|
||||
* Set Buffer B shader pass.
|
||||
* @param config - Shader configuration or falsy to clear
|
||||
*/
|
||||
setBufferB(config?: ShaderConfig | null): void;
|
||||
|
||||
/**
|
||||
* Set Buffer C shader pass.
|
||||
* @param config - Shader configuration or falsy to clear
|
||||
*/
|
||||
setBufferC(config?: ShaderConfig | null): void;
|
||||
|
||||
/**
|
||||
* Set Buffer D shader pass.
|
||||
* @param config - Shader configuration or falsy to clear
|
||||
*/
|
||||
setBufferD(config?: ShaderConfig | null): void;
|
||||
|
||||
/**
|
||||
* Set the main Image shader pass (renders to screen).
|
||||
* @param config - Shader configuration or falsy to clear
|
||||
*/
|
||||
setImage(config?: ShaderConfig | null): void;
|
||||
|
||||
/**
|
||||
* Set a callback to be called on each draw frame.
|
||||
* @param callback - Function to call each frame
|
||||
*/
|
||||
setOnDraw(callback?: (() => void) | null): void;
|
||||
|
||||
/**
|
||||
* Add a custom texture that can be used as a channel input.
|
||||
* @param texture - WebGL texture object
|
||||
* @param key - Buffer key to reference this texture (only A/B/C/D are recognized by iChannel bindings)
|
||||
*/
|
||||
addTexture(texture: WebGLTexture, key: BufferKey): void;
|
||||
|
||||
/**
|
||||
* Get the current playback time in seconds.
|
||||
* @returns Time elapsed since first draw
|
||||
*/
|
||||
time(): number;
|
||||
|
||||
/**
|
||||
* Check if the shader is currently playing.
|
||||
* @returns True if playing, false if paused
|
||||
*/
|
||||
isPlaying(): boolean;
|
||||
|
||||
/**
|
||||
* Reset playback to the beginning (time = 0, frame = 0).
|
||||
*/
|
||||
reset(): void;
|
||||
|
||||
/**
|
||||
* Pause shader playback.
|
||||
*/
|
||||
pause(): void;
|
||||
|
||||
/**
|
||||
* Start or resume shader playback.
|
||||
*/
|
||||
play(): void;
|
||||
}
|
||||
|
||||
/** ShaderToyLite constructor interface */
|
||||
interface ShaderToyLiteConstructor {
|
||||
/**
|
||||
* Create a new ShaderToyLite instance.
|
||||
* @param canvasId - ID of the canvas element to render to
|
||||
*/
|
||||
new (canvasId: string): ShaderToyLiteInstance;
|
||||
}
|
||||
|
||||
declare const ShaderToyLite: ShaderToyLiteConstructor;
|
||||
export default ShaderToyLite;
|
||||
export type { ShaderToyLiteInstance, ShaderConfig, BufferKey };
|
||||
437
examples/shadertoy-server/src/vendor/ShaderToyLite.js
vendored
Normal file
437
examples/shadertoy-server/src/vendor/ShaderToyLite.js
vendored
Normal file
|
|
@ -0,0 +1,437 @@
|
|||
/**
|
||||
* ShaderToyLite.js
|
||||
* https://github.com/chipweinberger/ShaderToyLite.js
|
||||
*
|
||||
* Copyright (c) Chip Weinberger
|
||||
* Licensed under the MIT License
|
||||
* https://github.com/chipweinberger/ShaderToyLite.js/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
function ShaderToyLite(canvasId) {
|
||||
|
||||
var hdr =
|
||||
`#version 300 es
|
||||
#ifdef GL_ES
|
||||
precision highp float;
|
||||
precision highp int;
|
||||
precision mediump sampler3D;
|
||||
#endif
|
||||
#define texture2D texture
|
||||
uniform vec3 iResolution; // viewport resolution (in pixels)
|
||||
uniform float iTime; // shader playback time (in seconds)
|
||||
uniform float iTimeDelta; // render time (in seconds)
|
||||
uniform float iFrameRate; // shader frame rate
|
||||
uniform int iFrame; // shader playback frame
|
||||
uniform float iChannelTime[4]; // channel playback time (in seconds)
|
||||
uniform vec3 iChannelResolution[4]; // channel resolution (in pixels)
|
||||
uniform vec4 iMouse; // mouse pixel coords. xy: current (if MLB down), zw: click
|
||||
uniform sampler2D iChannel0; // input channel 0
|
||||
uniform sampler2D iChannel1; // input channel 1
|
||||
uniform sampler2D iChannel2; // input channel 2
|
||||
uniform sampler2D iChannel3; // input channel 3
|
||||
uniform vec4 iDate; // (year, month, day, unixtime in seconds)
|
||||
uniform float iSampleRate; // sound sample rate (i.e., 44100)
|
||||
out vec4 frag_out_color;
|
||||
void mainImage( out vec4 c, in vec2 f );
|
||||
void main( void )
|
||||
{
|
||||
vec4 color = vec4(0.0,0.0,0.0,0.0);
|
||||
mainImage( color, gl_FragCoord.xy );
|
||||
frag_out_color = vec4(color);
|
||||
}
|
||||
`;
|
||||
|
||||
var basicFragShader =
|
||||
`void mainImage( out vec4 fragColor, in vec2 fragCoord ) {
|
||||
fragColor = texture2D(iChannel0, gl_FragCoord.xy / iResolution.xy);
|
||||
}
|
||||
`;
|
||||
|
||||
const basicVertexShader =
|
||||
`#version 300 es
|
||||
#ifdef GL_ES
|
||||
precision highp float;
|
||||
precision highp int;
|
||||
precision mediump sampler3D;
|
||||
#endif
|
||||
in vec2 vertexInPosition;
|
||||
void main() {
|
||||
gl_Position = vec4(vertexInPosition, 0.0, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const quadVertices = new Float32Array([
|
||||
-1.0, -1.0,
|
||||
1.0, -1.0,
|
||||
-1.0, 1.0,
|
||||
1.0, 1.0,
|
||||
-1.0, 1.0,
|
||||
1.0, -1.0
|
||||
]);
|
||||
|
||||
|
||||
var opts = { alpha: false,
|
||||
depth: false,
|
||||
stencil: false,
|
||||
premultipliedAlpha: false,
|
||||
antialias: true,
|
||||
preserveDrawingBuffer: false,
|
||||
powerPreference: "high-performance" };
|
||||
|
||||
var gl = document.getElementById(canvasId).getContext('webgl2', opts);
|
||||
|
||||
// timing
|
||||
var isPlaying = false;
|
||||
var firstDrawTime = 0;
|
||||
var prevDrawTime = 0;
|
||||
|
||||
// callback
|
||||
var onDrawCallback;
|
||||
|
||||
// uniforms
|
||||
var iFrame = 0;
|
||||
var iMouse = {x: 0, y: 0, clickX: 0, clickY: 0};
|
||||
|
||||
// shader common source
|
||||
var common = "";
|
||||
|
||||
// render passes variables. valid keys:
|
||||
// 'A', 'B', 'C', 'D', 'Image'
|
||||
var sourcecode = {};// fragment shader code
|
||||
var ichannels = {}; // texture inputs
|
||||
var atexture = {}; // front texture (input/output)
|
||||
var btexture = {}; // back texture (input/output)
|
||||
var aframebuf = {}; // front buffer (output)
|
||||
var bframebuf = {}; // back buffer (output)
|
||||
var program = {}; // webgl program
|
||||
var location = {}; // uniform location
|
||||
var flip = {}; // a b flip
|
||||
var quadBuffer; // two full screen triangles
|
||||
|
||||
|
||||
var setup = () => {
|
||||
gl.getExtension( 'OES_texture_float_linear');
|
||||
gl.getExtension( 'OES_texture_half_float_linear');
|
||||
gl.getExtension( 'EXT_color_buffer_float');
|
||||
gl.getExtension( 'WEBGL_debug_shaders');
|
||||
|
||||
['A', 'B', 'C', 'D', 'Image'].forEach((key) => {
|
||||
sourcecode[key] = "";
|
||||
ichannels[key] = {};
|
||||
program[key] = null;
|
||||
location[key] = {};
|
||||
if (key != 'Image') {
|
||||
atexture[key] = createTexture();
|
||||
btexture[key] = createTexture();
|
||||
aframebuf[key] = createFrameBuffer(atexture[key]);
|
||||
bframebuf[key] = createFrameBuffer(btexture[key]);
|
||||
flip[key] = false;
|
||||
}
|
||||
});
|
||||
|
||||
// bind the geometry
|
||||
quadBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, quadVertices, gl.STATIC_DRAW);
|
||||
|
||||
// Set viewport size
|
||||
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
||||
|
||||
var canvas = document.getElementById(canvasId);
|
||||
|
||||
window.addEventListener('resize', function() {
|
||||
gl.canvas.width = canvas.width;
|
||||
gl.canvas.height = canvas.height;
|
||||
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
||||
});
|
||||
|
||||
canvas.addEventListener("mousemove", (event) => {
|
||||
iMouse.x = event.offsetX;
|
||||
iMouse.y = canvas.height - event.offsetY;
|
||||
});
|
||||
|
||||
canvas.addEventListener("mousedown", (event) => {
|
||||
iMouse.clickX = event.offsetX;
|
||||
iMouse.clickY = canvas.height - event.offsetY;
|
||||
});
|
||||
|
||||
canvas.addEventListener("mouseup", () => {
|
||||
iMouse.clickX = 0;
|
||||
iMouse.clickY = 0;
|
||||
});
|
||||
}
|
||||
|
||||
var createTexture = () => {
|
||||
var texture = gl.createTexture();
|
||||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, gl.canvas.width, gl.canvas.height, 0, gl.RGBA, gl.FLOAT, null);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
return texture;
|
||||
}
|
||||
|
||||
var createFrameBuffer = (texture) => {
|
||||
var framebuffer = gl.createFramebuffer();
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
||||
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, null);
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||
return framebuffer;
|
||||
};
|
||||
|
||||
var compileProgram = (key) => {
|
||||
var vert = gl.createShader(gl.VERTEX_SHADER);
|
||||
gl.shaderSource(vert, basicVertexShader);
|
||||
gl.compileShader(vert);
|
||||
|
||||
if (!gl.getShaderParameter(vert, gl.COMPILE_STATUS)) {
|
||||
console.error('Vertex Shader compilation failed: ' + gl.getShaderInfoLog(vert));
|
||||
gl.deleteShader(vert);
|
||||
return null;
|
||||
}
|
||||
|
||||
var source = hdr + common + sourcecode[key];
|
||||
var frag = gl.createShader(gl.FRAGMENT_SHADER);
|
||||
gl.shaderSource(frag, source);
|
||||
gl.compileShader(frag);
|
||||
|
||||
if (!gl.getShaderParameter(frag, gl.COMPILE_STATUS)) {
|
||||
console.error('Fragment Shader compilation failed: ' + gl.getShaderInfoLog(frag));
|
||||
console.error(source);
|
||||
gl.deleteShader(frag);
|
||||
return null;
|
||||
}
|
||||
|
||||
var program = gl.createProgram();
|
||||
gl.attachShader(program, vert);
|
||||
gl.attachShader(program, frag);
|
||||
gl.linkProgram(program);
|
||||
|
||||
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
||||
console.error('Program initialization failed: ' + gl.getProgramInfoLog(program));
|
||||
return null;
|
||||
}
|
||||
|
||||
// uniform locations
|
||||
location[key]["iResolution"] = gl.getUniformLocation(program, "iResolution");
|
||||
location[key]["iTime"] = gl.getUniformLocation(program, "iTime");
|
||||
location[key]["iTimeDelta"] = gl.getUniformLocation(program, "iTimeDelta");
|
||||
location[key]["iFrameRate"] = gl.getUniformLocation(program, "iFrameRate");
|
||||
location[key]["iFrame"] = gl.getUniformLocation(program, "iFrame");
|
||||
location[key]["iChannelTime"] = gl.getUniformLocation(program, "iChannelTime[0]");
|
||||
location[key]["iChannelResolution"] = gl.getUniformLocation(program, "iChannelResolution[0]");
|
||||
location[key]["iChannel0"] = gl.getUniformLocation(program, "iChannel0");
|
||||
location[key]["iChannel1"] = gl.getUniformLocation(program, "iChannel1");
|
||||
location[key]["iChannel2"] = gl.getUniformLocation(program, "iChannel2");
|
||||
location[key]["iChannel3"] = gl.getUniformLocation(program, "iChannel3");
|
||||
location[key]["iMouse"] = gl.getUniformLocation(program, "iMouse");
|
||||
location[key]["iDate"] = gl.getUniformLocation(program, "iDate");
|
||||
location[key]["iSampleRate"] = gl.getUniformLocation(program, "iSampleRate");
|
||||
location[key]["vertexInPosition"] = gl.getAttribLocation(program, "vertexInPosition");
|
||||
|
||||
return program;
|
||||
};
|
||||
|
||||
var repeat = (times, arr) => {
|
||||
let result = [];
|
||||
for (let i = 0; i < times; i++) {
|
||||
result = [...result, ...arr];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
var setShader = (config, key) => {
|
||||
if (config) {
|
||||
if (config.source) {
|
||||
sourcecode[key] = config.source;
|
||||
program[key] = compileProgram(key);
|
||||
if (program[key] == null) {
|
||||
console.error("Failed to compile " + key);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < 4; i++) {
|
||||
var s = config[`iChannel${i}`];
|
||||
if (s == "A" || s == "B" || s == "C" || s == "D") {
|
||||
ichannels[key][i] = s;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sourcecode[key] = "";
|
||||
program[key] = null;
|
||||
}
|
||||
};
|
||||
|
||||
var draw = () => {
|
||||
|
||||
// current time
|
||||
var now = isPlaying ? Date.now() : prevDrawTime;
|
||||
var date = new Date(now);
|
||||
|
||||
// first draw?
|
||||
if (firstDrawTime == 0) {
|
||||
firstDrawTime = now;
|
||||
}
|
||||
|
||||
// call callback
|
||||
if (onDrawCallback) {
|
||||
onDrawCallback();
|
||||
}
|
||||
|
||||
// time difference between frames in seconds
|
||||
var iTimeDelta = (now - prevDrawTime) * 0.001;
|
||||
|
||||
// time in seconds
|
||||
var iTime = (now - firstDrawTime) * 0.001;
|
||||
var iDate = [date.getFullYear(), date.getMonth(), date.getDate(), date.getTime() * 0.001];
|
||||
|
||||
// channel uniforms
|
||||
var iChannelTimes = new Float32Array(repeat(4, [iTime]));
|
||||
var iChannelResolutions = new Float32Array(repeat(4, [gl.canvas.width, gl.canvas.height, 0]));
|
||||
|
||||
['A', 'B', 'C', 'D', 'Image'].forEach((key) => {
|
||||
|
||||
if (program[key]) {
|
||||
|
||||
// framebuffer
|
||||
if (key === "Image") {
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||
} else {
|
||||
var output = flip[key] ? bframebuf[key] : aframebuf[key];
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, output);
|
||||
}
|
||||
|
||||
// textures
|
||||
for (let i = 0; i < 4; i++) {
|
||||
var chkey = ichannels[key][i];
|
||||
if (chkey) {
|
||||
var input = flip[chkey] ? atexture[chkey] : btexture[chkey];
|
||||
gl.activeTexture(gl[`TEXTURE${i}`]);
|
||||
gl.bindTexture(gl.TEXTURE_2D, input);
|
||||
}
|
||||
}
|
||||
|
||||
// program
|
||||
gl.useProgram(program[key]);
|
||||
|
||||
// uniforms
|
||||
gl.uniform3f( location[key]["iResolution"], gl.canvas.width, gl.canvas.height, 1.0);
|
||||
gl.uniform1f( location[key]["iTime"], iTime);
|
||||
gl.uniform1f( location[key]["iTimeDelta"], iTimeDelta);
|
||||
gl.uniform1f( location[key]["iFrameRate"], 60);
|
||||
gl.uniform1i( location[key]["iFrame"], iFrame);
|
||||
gl.uniform1fv(location[key]["iChannelTime"], iChannelTimes);
|
||||
gl.uniform3fv(location[key]["iChannelResolution"], iChannelResolutions);
|
||||
gl.uniform1i( location[key]["iChannel0"], 0);
|
||||
gl.uniform1i( location[key]["iChannel1"], 1);
|
||||
gl.uniform1i( location[key]["iChannel2"], 2);
|
||||
gl.uniform1i( location[key]["iChannel3"], 3);
|
||||
gl.uniform4f( location[key]["iMouse"], iMouse.x, iMouse.y, iMouse.clickX, iMouse.clickY);
|
||||
gl.uniform4f( location[key]["iDate"], iDate[0], iDate[1], iDate[2], iDate[3]);
|
||||
gl.uniform1f( location[key]["iSampleRate"], 44100);
|
||||
|
||||
// viewport
|
||||
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
||||
|
||||
// vertexs
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer);
|
||||
gl.vertexAttribPointer(location[key]["vertexInPosition"], 2, gl.FLOAT, false, 0, 0);
|
||||
gl.enableVertexAttribArray(location[key]["vertexInPosition"]);
|
||||
|
||||
// draw
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
|
||||
flip[key] = !flip[key];
|
||||
}
|
||||
});
|
||||
|
||||
// time of last draw
|
||||
prevDrawTime = now;
|
||||
|
||||
// frame counter
|
||||
iFrame++;
|
||||
};
|
||||
|
||||
// Animation loop
|
||||
var animate = () => {
|
||||
if (isPlaying) {
|
||||
draw();
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
|
||||
this.setCommon = (source) => {
|
||||
if (source === undefined) {source = "";}
|
||||
if (source === null) {source = "";}
|
||||
common = source;
|
||||
['A', 'B', 'C', 'D', 'Image'].forEach((key) => {
|
||||
if (program[key]) {
|
||||
program[key] = compileProgram(key);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.setBufferA = (config) => {
|
||||
setShader(config, 'A');
|
||||
};
|
||||
|
||||
this.setBufferB = (config) => {
|
||||
setShader(config, 'B');
|
||||
};
|
||||
|
||||
this.setBufferC = (config) => {
|
||||
setShader(config, 'C');
|
||||
};
|
||||
|
||||
this.setBufferD = (config) => {
|
||||
setShader(config, 'D');
|
||||
};
|
||||
|
||||
this.setImage = (config) => {
|
||||
setShader(config, 'Image');
|
||||
};
|
||||
|
||||
this.setOnDraw = (callback) => {
|
||||
onDrawCallback = callback;
|
||||
}
|
||||
|
||||
this.addTexture = (texture, key) => {
|
||||
atexture[key] = texture;
|
||||
btexture[key] = texture;
|
||||
flip[key] = false;
|
||||
}
|
||||
|
||||
this.time = () => {
|
||||
return (prevDrawTime - firstDrawTime) * 0.001;
|
||||
}
|
||||
|
||||
this.isPlaying = () => isPlaying;
|
||||
|
||||
this.reset = () => {
|
||||
var now = new Date();
|
||||
firstDrawTime = now;
|
||||
prevDrawTime = now;
|
||||
iFrame = 0;
|
||||
draw();
|
||||
}
|
||||
|
||||
this.pause = () => {
|
||||
isPlaying = false;
|
||||
}
|
||||
|
||||
this.play = () => {
|
||||
if (!isPlaying) {
|
||||
isPlaying = true;
|
||||
var now = Date.now();
|
||||
var elapsed = prevDrawTime - firstDrawTime;
|
||||
firstDrawTime = now - elapsed;
|
||||
prevDrawTime = now;
|
||||
animate();
|
||||
}
|
||||
}
|
||||
|
||||
setup();
|
||||
}
|
||||
|
||||
export default ShaderToyLite;
|
||||
Loading…
Reference in a new issue