Open this repo in Codevre (free browser editor – no signup or setup)
Or start with a minimal ShaderMate template
ShaderMate is a lightweight and powerful JavaScript library designed to simplify WebGL shader development. Whether you're transitioning from platforms like ShaderToy, familiar with WebGL frameworks like TWGL.js, or just starting your journey into the world of shaders, ShaderMate provides a streamlined environment for creating stunning visual effects.
It handles much of the boilerplate WebGL setup, allowing you to focus on writing your GLSL code. With support for both WebGL1 and WebGL2, multi-pass rendering, and automatic uniform injection, you can quickly prototype and build complex shader experiences.
To begin using ShaderMate, you'll typically set up a project with four core files: index.html
, sketch.js
, style.css
, and your primary fragment shader, frag.glsl
.
You can quickly include ShaderMate in any HTML page using the CDN:
<script src="https://cdn.jsdelivr.net/gh/Tezumie/ShaderMate@main/src/shaderMate.js"></script>
This is your main HTML file. It sets up the basic page structure, includes your JavaScript and CSS files, and provides the <canvas>
element where your shaders will be rendered.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ShaderMate</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<canvas id="glcanvas"></canvas>
<script src="https://cdn.jsdelivr.net/gh/Tezumie/ShaderMate@main/src/shaderMate.js"></script>
<script src="sketch.js"></script>
</body>
</html>
Key Points:
<canvas id="glcanvas"></canvas>
: This is the HTML element that ShaderMate will use to create the WebGL context and render your shaders. You can give it any ID, butglcanvas
is the default that ShaderMate looks for. If you use a different ID, you'll need to specify it in thestartShaderMate
options.path/to/shaderMate.js
: Replace this with the actual path to the ShaderMate library JavaScript file. It should be loaded before yoursketch.js
file.sketch.js
: Your application logic and ShaderMate setup will reside here.style.css
: Used for basic styling, typically to make the canvas fill the screen.
A minimal CSS file to ensure your canvas occupies the entire viewport:
html,
body {
margin: 0;
height: 100%;
overflow: hidden;
background: #000
}
canvas {
display: block;
width: 100%;
height: 100%
}
This is where you'll write your GLSL fragment shader code. For a basic single-pass setup, your shader will typically contain a mainImage
function, similar to ShaderToy:
// frag.glsl
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord.xy / iResolution.xy;
fragColor = vec4(uv, 0.5 + 0.5 * sin(iTime), 1.0);
}
This JavaScript file is where you initiate ShaderMate and define your shader passes.
// sketch.js
document.addEventListener('DOMContentLoaded', () => {
// Start ShaderMate with a single pass
ShaderMate.startShaderMate('frag.glsl');
});
This minimal setup will load frag.glsl
and render it to the screen. The DOMContentLoaded
listener ensures the canvas is ready before startShaderMate
is called.
ShaderMate organizes your shader effects into passes. Each pass represents a single render operation using a specific fragment shader. Passes can render to the screen or to an offscreen texture (Framebuffer Object or FBO), allowing for complex multi-pass effects.
A pass is defined as an object with various properties:
{
name: 'A', // (Optional) A unique name for the pass (e.g., 'A', 'B', 'BufferA')
src: 'fragA.glsl', // Path to the fragment shader file, or a direct GLSL string
size: 'screen', // Output size: 'screen', 'half', or [width, height]
screen: false, // If true, this pass renders directly to the canvas
pingpong: false, // If true, enables double buffering for feedback effects
channels: [], // Array of input textures/buffers for this pass (iChannel0, iChannel1, etc.)
uniforms: {}, // Custom uniforms you want to send to this shader
defines: [], // Custom GLSL #define directives for this pass
float: false, // If true, the FBO texture will be high precision (float/half-float)
wrap: 'clamp', // Texture wrap mode for FBO: 'clamp' (CLAMP_TO_EDGE) or 'repeat' (REPEAT)
filter: 'linear', // Texture filter for FBO: 'linear' (LINEAR) or 'nearest' (NEAREST)
depth: false // If true, the FBO will have a depth attachment (WebGL2 only)
}
For simpler effects, you might only need one shader that renders directly to the screen.
Example: sketch.js
for single pass
// sketch.js
document.addEventListener('DOMContentLoaded', () => {
ShaderMate.startShaderMate('frag.glsl');
});
Here, frag.glsl
will be the only shader. By default, if only one pass is provided as a string, ShaderMate assumes it's a screen-rendering pass.
Alternatively, you can specify it as an array of passes:
// sketch.js
document.addEventListener('DOMContentLoaded', () => {
ShaderMate.startShaderMate([
{
name: 'MainPass',
src: 'frag.glsl',
screen: true, // Explicitly render to screen
size: 'screen'
}
]);
});
Multi-pass rendering is crucial for advanced effects like blurring, post-processing, simulations, and feedback loops. In ShaderMate, you define an array of passes, where each pass can render to an offscreen buffer (FBO) which can then be used as input for subsequent passes.
Example: sketch.js
for multi-pass (Buffer A to Screen)
// sketch.js
document.addEventListener('DOMContentLoaded', () => {
ShaderMate.startShaderMate([
{
name: 'BufferA',
src: 'bufferA.glsl',
size: 'screen', // Render BufferA to an offscreen texture the size of the screen
channels: [], // No input textures for now
screen: false // Don't render this directly to screen
},
{
name: 'Image',
src: 'image.glsl',
size: 'screen',
screen: true, // Render Image pass to the screen
channels: [
{ id: 'BufferA' } // Use the output of BufferA as iChannel0
]
}
]);
});
In this setup:
bufferA.glsl
will compute something and render it to an internal texture managed by ShaderMate.image.glsl
will then take the output ofBufferA
as itsiChannel0
and render the final result to the canvas.
For feedback effects (like blurring over time, or dynamic simulations), you often need to read from the previous frame's output and write to the current frame's output. ShaderMate simplifies this with the pingpong: true
option on a pass.
When pingpong
is enabled, ShaderMate creates two internal buffers for that pass. In each frame, it swaps them: the iChannel0
input will receive the previous frame's output, and the current frame's rendering will go into the other buffer.
Example: sketch.js
with a ping-pong buffer
// sketch.js
document.addEventListener('DOMContentLoaded', () => {
ShaderMate.startShaderMate([
{
name: 'FeedbackBuffer',
src: 'feedback.glsl',
size: 'screen',
pingpong: true, // Enable ping-pong buffering
channels: [
{ id: 'FeedbackBuffer' } // iChannel0 will receive the *previous* frame's output from this pass
],
screen: false
},
{
name: 'RenderToScreen',
src: 'render.glsl',
size: 'screen',
screen: true,
channels: [
{ id: 'FeedbackBuffer' } // iChannel0 will receive the *current* output of FeedbackBuffer
]
}
]);
});
🔄 Common Gotcha: Ping-Pong Starts Black and Stays Black
When using pingpong: true
, the very first frame's buffer is all zeros (black). If your shader reads the previous frame and does:
fragColor = texture(iChannel0, uv) * 0.97;
…you're just multiplying black forever. If nothing ever adds color (e.g., on mouse input), your output stays black indefinitely.
-
Seed the buffer on the first frame:
if (iFrame < 2) { fragColor = vec4(noise(uv) * 0.01, 0.0, 0.0, 1.0); }
-
Or add a small bias:
fragColor += vec4(0.01);
-
Or inject color conditionally, e.g. on mouse press:
if (iMouse.z > 0.0) { fragColor += vec4(0.5, 0.2, 1.0, 1.0); }
Inside feedback.glsl
:
// feedback.glsl
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord.xy / iResolution.xy;
vec4 prevFrame = texture2D(iChannel0, uv); // Read from the previous frame
// Simple feedback: blend previous frame with a new color based on time
fragColor = mix(prevFrame, vec4(uv.x, uv.y, 0.0, 1.0), 0.01);
fragColor.rgb += 0.01 * sin(iTime + uv.x * 10.0); // Add some animation
}
This shader reads from iChannel0
(which is automatically connected to the "previous" frame's output of the same FeedbackBuffer
pass) and mixes it with a new value, creating a trailing effect.
ShaderMate automatically injects common GLSL code into your fragment shaders to provide a consistent and convenient development experience, especially for those familiar with ShaderToy. This includes version declarations, precision qualifiers, built-in uniform definitions, and a mainImage
function wrapper.
When autoSetup
is true
(which is the default) or inject
options are configured, ShaderMate prepends the following (or similar, depending on WebGL version) to your shader code:
#version 300 es // Only for WebGL2
precision highp float;
precision highp int;
#if __VERSION__ >= 300
#define texture2D(s,u) texture(s,u)
#define textureCube(s,u) texture(s,u)
#define TEX(s,u) texture(s,u)
#define TEX_LOD(s,u,l) textureLod(s,u,l)
#else
#define TEX(s,u) texture2D(s,u)
#define TEX_LOD(s,u,l) texture2D(s,u)
#endif
out vec4 outColor; // Only for WebGL2
Explanation of Injected Code:
#version 300 es
: Declares the GLSL ES 3.00 version, required for WebGL2. If using WebGL1, this line is omitted.precision highp float; precision highp int;
: Sets the default precision for floats and integers tohighp
. This is generally recommended for visual quality.#define texture2D(...)
/#define textureCube(...)
/#define TEX(...)
/#define TEX_LOD(...)
: These macros provide compatibility for texture sampling between WebGL1 (GLSL ES 1.00) and WebGL2 (GLSL ES 3.00).- In GLSL ES 1.00, you use
texture2D
andtextureCube
. - In GLSL ES 3.00, the generic
texture
function is used for all texture types. TEX(s, u)
andTEX_LOD(s, u, l)
provide a unified way to sample textures regardless of the WebGL version. It's recommended to useTEX
for basic sampling andTEX_LOD
for explicit level-of-detail sampling.
- In GLSL ES 1.00, you use
out vec4 outColor;
: For WebGL2, fragment shaders output to anout
variable (namedoutColor
by default). In WebGL1, you write togl_FragColor
.
If your shader defines a mainImage
function (common in ShaderToy) instead of void main()
, ShaderMate automatically wraps it. This allows you to write shaders in the familiar ShaderToy style while still integrating with the standard WebGL entry point (void main
).
Example of mainImage
(your GLSL):
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
// Your shader logic
fragColor = vec4(1.0);
}
How it's wrapped (conceptually, by ShaderMate):
// ... injected header ...
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
// Your shader logic
fragColor = vec4(1.0);
}
void main() {
vec4 c;
mainImage(c, gl_FragCoord.xy);
// For WebGL2:
outColor = c;
// For WebGL1:
// gl_FragColor = c;
}
This wrapper transforms gl_FragCoord.xy
(the pixel coordinate) and assigns the output of mainImage
to outColor
(WebGL2) or gl_FragColor
(WebGL1).
You have fine-grained control over what ShaderMate injects:
-
autoSetup: false
: Disables all automatic GLSL injection. You will need to provide the#version
,precision
,out vec4 outColor;
(for WebGL2), and your ownvoid main()
function that directly setsgl_FragColor
(WebGL1) oroutColor
(WebGL2). This gives you full control but requires more boilerplate.ShaderMate.startShaderMate('myShader.glsl', { autoSetup: false });
If
autoSetup: false
, yourmyShader.glsl
would need:#version 300 es // Or omit for WebGL1 precision highp float; precision highp int; out vec4 outColor; // Or gl_FragColor for WebGL1 void main() { // Your shader logic using gl_FragCoord.xy directly outColor = vec4(1.0); }
-
inject: { header: true, defines: false, mainWrap: false, builtins: true }
: Allows you to enable or disable specific parts of the injection.header
: Controls#version
andprecision
lines.defines
: Controls injection of custom#define
directives you provide inoptions.defines
orpass.defines
.mainWrap
: Controls themainImage
tomain()
wrapper.builtins
: Controls thetexture2D
/texture
macros andout vec4 outColor;
declaration.
This flexibility allows you to customize the environment to your preferences, whether you want a ShaderToy-like experience or full manual control.
ShaderMate automatically provides several useful uniforms to your fragment shaders, similar to ShaderToy. These are automatically updated each frame.
Uniform Name | Type | Description |
---|---|---|
uniform vec3 iResolution; |
vec3 |
The resolution of the rendering target (e.g., (canvas.width, canvas.height, 0.0) ). z component is always 0. |
uniform float iTime; |
float |
Current time in seconds since the shader started. |
uniform float iTimeDelta; |
float |
Time elapsed in seconds since the last frame (delta time). |
uniform float iFrameRate; |
float |
Current frames per second (FPS). |
uniform int iFrame; |
int |
Current frame number. |
uniform vec4 iMouse; |
vec4 |
Mouse coordinates and click state. (x, y) are the current mouse position (in pixels, origin bottom-left). z and w store the (x, y) coordinates of the last mouse click when the mouse button was pressed. |
uniform vec4 iDate; |
vec4 |
Current date and time: (year, month, day, seconds_since_midnight) . |
These uniforms are available when you load textures or buffers into your shader's channels.
Uniform Name | Type | Description |
---|---|---|
uniform sampler2D iChannel0; |
sampler2D |
Texture sampler for input channel 0. Connected to the channels[0] definition in your pass. |
uniform sampler2D iChannel1; |
sampler2D |
Texture sampler for input channel 1. Connected to the channels[1] definition. |
uniform sampler2D iChannel2; |
sampler2D |
Texture sampler for input channel 2. Connected to the channels[2] definition. |
uniform sampler2D iChannel3; |
sampler2D |
Texture sampler for input channel 3. Connected to the channels[3] definition. |
uniform vec3 iChannelResolution[4]; |
vec3[4] |
An array of vec3 where iChannelResolution[N].xy provides the width and height of iChannelN . z is always 0. This is useful for normalizing texture coordinates (uv = fragCoord.xy / iChannelResolution[N].xy; ). |
uniform float iChannelTime[4]; |
float[4] |
An array of float where iChannelTime[N] provides the time in seconds since a video or audio channel (iChannelN ) started playing. |
💡 Tip: ShaderMate only sets uniforms like iTime
, iResolution
, iChannel0
, etc. if you declare them. Forgetting to declare them silently breaks your shader. Use:
uniform sampler2D iChannel0;
uniform float iTime;
uniform vec3 iResolution;
ShaderMate provides convenient ways to load various types of external assets as textures or use the output of other passes as textures.
You specify input channels within the channels
array of a pass configuration. The order in the array corresponds to iChannel0
, iChannel1
, iChannel2
, and iChannel3
.
Load a standard 2D image from a URL.
// In your sketch.js pass definition:
channels: [
{ url: 'assets/texture.png', filter: 'linear', wrap: 'repeat', flipY: true }
]
Options:
url
(string, required): The URL to the image file.filter
('linear' or 'nearest', default 'linear'): How the texture is sampled when magnified or minified.wrap
('clamp' or 'repeat', default 'clamp'): How texture coordinates outside the 0-1 range behave.flipY
(boolean, defaultfalse
): Whether to flip the image vertically on load. Useful for images where(0,0)
is top-left, but WebGL's origin is bottom-left.
Load a cube map texture for skyboxes or environment mapping. You provide an array of 6 URLs for the faces.
// In your sketch.js pass definition:
channels: [
{
cubemap: [
'assets/posx.jpg', 'assets/negx.jpg',
'assets/posy.jpg', 'assets/negy.jpg',
'assets/posz.jpg', 'assets/negz.jpg'
],
filter: 'linear'
}
]
Options:
cubemap
(array of 6 strings, required): URLs for the cube map faces in the order: positive X, negative X, positive Y, negative Y, positive Z, negative Z.filter
('linear' or 'nearest', default 'linear'): Filtering for the cube map.wrap
('clamp' or 'repeat', default 'clamp'): Wrap mode (usuallyclamp
for cube maps).
Stream a video file and use each frame as a texture input.
// In your sketch.js pass definition:
channels: [
{ video: 'assets/myvideo.mp4', filter: 'linear', flipY: true }
]
Options:
video
(string, required): The URL to the video file.filter
('linear' or 'nearest', default 'linear'): Filtering for the video texture.wrap
('clamp' or 'repeat', default 'clamp'): Wrap mode.flipY
(boolean, defaulttrue
): Videos typically need to be flipped.
Process an audio file and get its frequency spectrum (FFT) as a 1D texture. The texture's width will be fftSize
and height 1.
// In your sketch.js pass definition:
channels: [
{ audioFFT: 'assets/myaudio.mp3', fftSize: 1024 }
]
Options:
audio
oraudioFFT
(string, required): The URL to the audio file.fftSize
(number, default 512): The FFT size, which determines the resolution of the frequency data (must be a power of 2, e.g., 32, 64, 128, ..., 2048).
To connect the output of one pass as an input to another, use the id
property, referencing the name
of the source pass.
// In your sketch.js pass definition:
channels: [
{ id: 'BufferA' }, // Use the output of the pass named 'BufferA' as iChannel0
{ id: 'FeedbackBuffer' } // Use the output of 'FeedbackBuffer' as iChannel1
]
Important Notes for Channels:
iChannelResolution[N]
: When you use any channel, theiChannelResolution[N]
uniform will be populated with its dimensions, allowing you to correctly normalize UVs.iChannelTime[N]
: For video and audio channels,iChannelTime[N]
will be available.- Order Matters: The order of objects in the
channels
array directly maps toiChannel0
,iChannel1
,iChannel2
,iChannel3
. - Referencing Ping-Pong Buffers: When a pass has
pingpong: true
, and you reference it by itsid
in another pass'schannels
array, you will always get the most recently rendered frame from that ping-pong pair. If the ping-pong pass references itself as a channel, it will get the previous frame's output.
You can define your own custom uniforms in your shader and update them from JavaScript.
-
Declare in GLSL:
// frag.glsl uniform float u_myValue; uniform vec3 u_myColor; uniform mat4 u_transformMatrix; void mainImage(out vec4 fragColor, in vec2 fragCoord) { fragColor = vec4(u_myColor * u_myValue, 1.0); }
-
Define in JavaScript (in your pass config):
// sketch.js ShaderMate.startShaderMate([ { name: 'MyShader', src: 'frag.glsl', screen: true, uniforms: { u_myValue: { type: '1f', value: 0.5 }, u_myColor: { type: '3f', value: [1.0, 0.0, 0.0] }, u_transformMatrix: { type: 'Matrix4fv', value: someFloat32ArrayOfMatrixData, transpose: false } } } ]);
Uniform type
options:
Type | GLSL Equivalent | Value Format (JavaScript) | Description |
---|---|---|---|
'1f' |
float |
number |
Single float. |
'2f' |
vec2 |
[number, number] |
Two floats. |
'3f' |
vec3 |
[number, number, number] |
Three floats. |
'4f' |
vec4 |
[number, number, number, number] |
Four floats. |
'1i' |
int |
number |
Single integer. |
'2i' |
ivec2 |
[number, number] |
Two integers. |
'3i' |
ivec3 |
[number, number, number] |
Three integers. |
'4i' |
ivec4 |
[number, number, number, number] |
Four integers. |
'1fv' |
float[] |
Float32Array or number[] |
Array of floats (for uniform float myArray[N] ). |
'2fv' |
vec2[] |
Float32Array or number[] (e.g., [x1,y1,x2,y2] ) |
Array of vec2s. |
'3fv' |
vec3[] |
Float32Array or number[] (e.g., [x1,y1,z1,x2,y2,z2] ) |
Array of vec3s. |
'4fv' |
vec4[] |
Float32Array or number[] (e.g., [x1,y1,z1,w1,x2,y2,z2,w2] ) |
Array of vec4s. |
'Matrix3fv' |
mat3 |
Float32Array or number[] (9 elements) |
3x3 matrix. Requires transpose: true/false . |
'Matrix4fv' |
mat4 |
Float32Array or number[] (16 elements) |
4x4 matrix. Requires transpose: true/false . |
Updating Custom Uniforms:
To update custom uniforms dynamically, you'll need to use the on
event listener for beforeFrame
.
// sketch.js
let customColor = [1.0, 0.0, 0.0];
ShaderMate.startShaderMate([
{
name: 'MyShader',
src: 'frag.glsl',
screen: true,
uniforms: {
u_dynamicColor: { type: '3f', value: customColor } // Initial value
}
}
]);
ShaderMate.on('beforeFrame', ({ time }) => {
// Update the color based on time
customColor[0] = 0.5 + 0.5 * Math.sin(time);
customColor[1] = 0.5 + 0.5 * Math.cos(time);
customColor[2] = 0.5 + 0.5 * Math.sin(time + Math.PI);
// ShaderMate automatically picks up changes to array values used in uniforms
});
The ShaderMate
object exposes a simple API for starting the shader graph and handling events.
This is the main function to initialize and run your shader graph.
-
passesOrPath
:string
: A path to a single fragment shader file (e.g.,'frag.glsl'
). This will be treated as a single screen-rendering pass.Array<Object>
: An array of pass configuration objects (see "Shader Passes" section for details). This is used for multi-pass setups.
-
options
(Object, optional): Configuration options for the entire ShaderMate instance.canvas
(HTMLElement, optional): The<canvas>
element to use for rendering. Defaults todocument.getElementById('glcanvas')
.autoSetup
(boolean, defaulttrue
): Iffalse
, disables all automatic GLSL injection. You must manually add#version
,precision
,out vec4 outColor;
(WebGL2), andvoid main()
in your shaders.inject
(Object, optional): Fine-grained control over GLSL injection. Only relevant ifautoSetup
istrue
(or if you want to partially override the defaultautoSetup
behavior).header
(boolean, defaulttrue
): Inject#version
andprecision
lines.defines
(boolean, defaulttrue
): Inject custom#define
directives.mainWrap
(boolean, defaulttrue
): WrapmainImage
invoid main()
.builtins
(boolean, defaulttrue
): Injecttexture2D
/texture
macros andout vec4 outColor;
(WebGL2).
defines
(Array<string | Object>, optional): Global GLSL#define
directives applied to all passes. Can be an array of strings (e.g.,['MY_DEFINE']
) or an array of objects ([{ MY_VALUE: 10 }]
).includes
(Object, optional): A map of include paths to their content. Use this to pre-load GLSL include files without network requests. Example:{ 'my_utils.glsl': 'vec3 colorize(vec3 c) { return c * 2.0; }' }
.showExpandedOnError
(boolean, defaulttrue
): Iftrue
, compilation errors will show the full, preprocessed shader source with line numbers, highlighting the error line. Set tofalse
for a more concise error log.startPaused
(boolean, defaultfalse
): Iftrue
, the animation will not start automatically. You'll need to callShaderMate.play()
to begin animation.fixedDelta
(number, default0
): If greater than 0,iTimeDelta
will be fixed to this value (in seconds) instead of reflecting actual frame time. Useful for deterministic simulations.timeScale
(number, default1.0
): Multiplier foriTime
. A value of0.5
makes time run half as fast;2.0
makes it twice as fast.
Registers an event listener.
eventName
(string): The name of the event to listen for.'beforeFrame'
: Fired before each frame is rendered. The callback receives an object{ time: float, frame: int, dt: float, fps: float }
.
callback
(function): The function to call when the event fires.
Removes an event listener previously registered with ShaderMate.on()
.
Starts the animation loop if it's currently paused.
Pauses the animation loop.
Returns the current accumulated time in seconds (iTime
).
Returns the current frame number (iFrame
).
ShaderMate provides detailed error reporting for GLSL compilation and linking errors. When an error occurs, it attempts to translate the logical line number reported by WebGL (which refers to the preprocessed code) back to the physical line number in your original source file, and will log the expanded source for easier debugging.
- Organize Your Code: For multi-pass setups, keep each pass's GLSL in its own file (e.g.,
bufferA.glsl
,image.glsl
). - Use Includes: Break down complex GLSL functions or common utilities into separate
.glsl
files and use#include "my_utils.glsl"
directives to manage your shader code. ShaderMate will load these files for you. - Performance:
- Be mindful of
float
(high precision) textures on older devices, as they can be slower or unsupported. - Minimize
texture2D
calls inside loops. - Consider
size: 'half'
for passes that don't require full screen resolution (e.g., blurs).
- Be mindful of
- Cross-Origin Issues: If loading images, videos, or audio from different domains, ensure they are served with appropriate CORS headers (
Access-Control-Allow-Origin
). Otherwise, your textures might not load or might cause security errors. - Memory Management: ShaderMate handles WebGL resource cleanup (textures, FBOs, programs) automatically via its
RefPool
when objects are no longer referenced. However, be aware that holding onto large textures or creating many passes can consume significant GPU memory.
This project is licensed under the GNU GPL v3.
- Stats overlay (FPS, frame count, ms) toggled with a key (e.g.
F1
).
- Optional uniform inspector overlay (dat.gui / lil-gui via CDN) to tweak uniforms in real time.
- More sizing modes (custom width/height, aspect locks) and clearer docs for each.
- Corrected mouse mapping for centered/letterboxed canvases with odd aspect ratios (helper to convert DOM coords → shader coords).