Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,8 @@
]
},
"tutorials/image/cosmos/cosmos-predict2-t2i",
"tutorials/image/omnigen/omnigen2"
"tutorials/image/omnigen/omnigen2",
"tutorials/image/glsl/glsl-shaders"
]
},
{
Expand Down
300 changes: 300 additions & 0 deletions tutorials/image/glsl/glsl-shaders.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
---
title: "Writing Custom GLSL Shaders"
description: "Learn how to write custom GLSL ES 3.00 fragment shaders for the GLSLShader node in ComfyUI, including available uniforms, multi-pass ping-pong rendering, and multiple render targets."
sidebarTitle: "GLSL Shaders"
---

The **GLSL Shader** node lets you write custom fragment shaders in **GLSL ES 3.00** (WebGL 2.0 compatible) to process images directly on the GPU. You can create image effects like blurs, color grading, film grain, glow, and much more - all running at GPU speed.

<Note>
The GLSL Shader node is currently marked as **experimental**, so the node may be updated and extended in future releases.
</Note>

## Minimal Shader

The simplest possible shader - a passthrough that outputs the input image unchanged:

```glsl
#version 300 es
precision highp float;

uniform sampler2D u_image0;

in vec2 v_texCoord;
layout(location = 0) out vec4 fragColor0;

void main() {
fragColor0 = texture(u_image0, v_texCoord);
}
```

<Note>
**Why GLSL ES 3.00?** Shaders need to run in two environments: the **browser** (via WebGL 2.0, which only supports GLSL ES 3.00) for live preview in the ComfyUI frontend, and the **Python backend** (via desktop OpenGL) when the workflow executes. GLSL ES 3.00 is the common denominator that works in both places.
</Note>

## Available Uniforms

These uniforms are automatically set by ComfyUI. You don't need to declare all of them - only declare the ones you use.

### Images

| Uniform | Type | Description |
|---------|------|-------------|
| `u_image0` – `u_image4` | `sampler2D` | Input images (up to 5). Sampled with `texture(u_image0, v_texCoord)`. Images are RGBA float textures with linear filtering and clamp-to-edge wrapping. |

### Floats

| Uniform | Type | Description |
|---------|------|-------------|
| `u_float0` – `u_float19` | `float` | Up to 20 user-controlled float values. Mapped from the **floats** input group on the node. |

### Integers

| Uniform | Type | Description |
|---------|------|-------------|
| `u_int0` – `u_int19` | `int` | Up to 20 user-controlled integer values. Mapped from the **ints** input group on the node. |

<Note>
**Using int uniforms as dropdowns:** Int uniforms pair well with the **Custom Combo** node's index output - users pick an option from a dropdown and the shader receives the selected item's index.

```glsl
const int BLEND_SCREEN = 0;
const int BLEND_OVERLAY = 1;
const int BLEND_MULTIPLY = 2;

// ...

if (u_int0 == BLEND_SCREEN) {
// ...
}
```
</Note>

### Booleans

| Uniform | Type | Description |
|---------|------|-------------|
| `u_bool0` – `u_bool9` | `bool` | Up to 10 user-controlled boolean values. Mapped from the **bools** input group on the node. |

### Curves (1D LUTs)

| Uniform | Type | Description |
|---------|------|-------------|
| `u_curve0` – `u_curve3` | `sampler2D` | Up to 4 user-editable curve LUTs from the **curves** input group. Each curve is a 1D lookup table stored as a single-row texture. |

<Note>
**Using curve uniforms:** Curves let users draw arbitrary tone-mapping graphs in the UI (e.g. for contrast, gamma, per-channel grading, or any custom `input → output` remap). Sample the curve using your input value as the X coordinate - remember to clamp it to `[0, 1]` first:

```glsl
float applyCurve(sampler2D curve, float value) {
return texture(curve, vec2(clamp(value, 0.0, 1.0), 0.5)).r;
}

// Usage: remap each RGB channel through a master curve
color.r = applyCurve(u_curve0, color.r);
color.g = applyCurve(u_curve0, color.g);
color.b = applyCurve(u_curve0, color.b);
```

Common uses: master RGB curves, per-channel R/G/B curves, luminance-driven remaps, custom gamma, and any effect where you want the user to shape a response curve visually.
</Note>

### Resolution

| Uniform | Type | Description |
|---------|------|-------------|
| `u_resolution` | `vec2` | **Output** framebuffer dimensions in pixels (`width, height`). This is the size you're writing to, which may differ from any input image's size when `size_mode` is `"custom"`. |

<Note>
**Computing texel size for sampling:** Don't use `1.0 / u_resolution` to step one pixel in an input texture. `u_resolution` is the *output* size, which may not match the input's size. Instead use `textureSize()` on the actual texture you're sampling:

```glsl
vec2 texel = 1.0 / vec2(textureSize(u_image0, 0));
```

Use `u_resolution` only when you need the output framebuffer dimensions themselves (e.g. computing `gl_FragCoord.xy / u_resolution` to get screen-space UVs).
</Note>

### Multi-Pass

| Uniform | Type | Description |
|---------|------|-------------|
| `u_pass` | `int` | Current pass index (0-based). Only meaningful when using `#pragma passes` - see [Multi-Pass Ping-Pong Rendering](#multi-pass-ping-pong-rendering) for details. |

### Vertex Shader Output

| Varying | Type | Description |
|---------|------|-------------|
| `v_texCoord` | `vec2` | Texture coordinates ranging from (0,0) at bottom-left to (1,1) at top-right. |

## Multiple Outputs (MRT)

The node supports up to **4 simultaneous outputs** using Multiple Render Targets. Declare additional outputs with explicit locations:

```glsl
#version 300 es
precision highp float;

uniform sampler2D u_image0;

in vec2 v_texCoord;
layout(location = 0) out vec4 fragColor0;
layout(location = 1) out vec4 fragColor1;
layout(location = 2) out vec4 fragColor2;
layout(location = 3) out vec4 fragColor3;

void main() {
vec4 color = texture(u_image0, v_texCoord);
fragColor0 = vec4(vec3(color.r), 1.0); // Red channel
fragColor1 = vec4(vec3(color.g), 1.0); // Green channel
fragColor2 = vec4(vec3(color.b), 1.0); // Blue channel
fragColor3 = vec4(vec3(color.a), 1.0); // Alpha channel
}
```

Each `fragColor` maps to the corresponding `IMAGE` output on the node. ComfyUI auto-detects which outputs you use - unused outputs will be black.

## Multi-Pass Ping-Pong Rendering

Some effects (like separable blur) need multiple passes over the image. Use the `#pragma passes N` directive to enable this:

```glsl
#version 300 es
#pragma passes 2
precision highp float;

uniform sampler2D u_image0;
uniform float u_float0; // Blur radius
uniform int u_pass;

in vec2 v_texCoord;
layout(location = 0) out vec4 fragColor0;

void main() {
vec2 texel = 1.0 / vec2(textureSize(u_image0, 0));
int radius = int(ceil(u_float0));

// Pass 0 = horizontal blur, Pass 1 = vertical blur
vec2 dir = (u_pass == 0) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);

vec4 color = vec4(0.0);
float total = 0.0;

for (int i = -radius; i <= radius; i++) {
vec2 offset = dir * float(i) * texel;
float w = 1.0; // box blur weight
color += texture(u_image0, v_texCoord + offset) * w;
total += w;
}

fragColor0 = color / total;
}
```

### How ping-pong works

1. **Pass 0**: Reads from the original `u_image0` input, writes to an internal ping-pong texture.
2. **Pass 1–N**: Reads from the *previous pass output* via `u_image0` (the binding is swapped automatically), writes to the other ping-pong texture.
3. **Final pass**: Writes to the actual output framebuffer (`fragColor0`).

<Note>
When using multi-pass with MRT (multiple outputs), only the first output (`fragColor0`) participates in ping-pong. The final pass writes all outputs.
</Note>

## Examples

### Grayscale Conversion

```glsl
#version 300 es
precision highp float;

uniform sampler2D u_image0;
in vec2 v_texCoord;
layout(location = 0) out vec4 fragColor0;

void main() {
vec4 color = texture(u_image0, v_texCoord);
float gray = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722));
fragColor0 = vec4(vec3(gray), color.a);
}
```

### Image Blending

Blend two input images using a float parameter as the mix factor:

```glsl
#version 300 es
precision highp float;

uniform sampler2D u_image0;
uniform sampler2D u_image1;
uniform float u_float0; // mix factor [0.0 – 1.0]

in vec2 v_texCoord;
layout(location = 0) out vec4 fragColor0;

void main() {
vec4 a = texture(u_image0, v_texCoord);
vec4 b = texture(u_image1, v_texCoord);
fragColor0 = mix(a, b, clamp(u_float0, 0.0, 1.0));
}
```

## Using an LLM to Generate Shaders

You can use any LLM (Claude, ChatGPT, etc.) to write GLSL shaders for you. Copy the following prompt and fill in your desired effect:

````markdown
Write a GLSL ES 3.00 fragment shader for ComfyUI's GLSLShader node.

**Effect I want:** [DESCRIBE YOUR DESIRED EFFECT HERE]

**Requirements:**
- Must start with `#version 300 es`
- Use `precision highp float;`
- The vertex shader provides `in vec2 v_texCoord` (0–1 UV coordinates, bottom-left origin)
- Output to `layout(location = 0) out vec4 fragColor0` (RGBA)
- Additional outputs available: `fragColor1`, `fragColor2`, `fragColor3` at locations 1–3

**Template to follow (minimal passthrough shader - use this exact structure):**

```glsl
#version 300 es
precision highp float;

uniform sampler2D u_image0;

in vec2 v_texCoord;
layout(location = 0) out vec4 fragColor0;

void main() {
fragColor0 = texture(u_image0, v_texCoord);
}
```

**Available uniforms (declare only what you use):**
- `uniform sampler2D u_image0;` through `u_image4` - up to 5 input images (RGBA float, linear filtering, clamp-to-edge)
- `uniform vec2 u_resolution;` - **output framebuffer** width and height in pixels. This is NOT the input texture size (they can differ when the user sets a custom output size). **To step one pixel in an input texture, use `vec2 texel = 1.0 / vec2(textureSize(u_image0, 0));` - do NOT use `1.0 / u_resolution` for this.**
- `uniform float u_float0;` through `u_float19;` - up to 20 user-controlled float parameters
- `uniform int u_int0;` through `u_int19;` - up to 20 user-controlled integer parameters. Tip: int uniforms can be wired from a **Custom Combo** node's index output, so users select from a dropdown and the shader receives the index. Use this for mode selection (e.g. blend mode, blur type). Define named constants for each option and branch on those - do NOT compare against raw integer literals. For example: `const int BLUR_GAUSSIAN = 0; const int BLUR_BOX = 1; ... if (u_int0 == BLUR_GAUSSIAN) { ... }`.
- `uniform bool u_bool0;` through `u_bool9;` - up to 10 user-controlled boolean parameters (use for feature toggles)
- `uniform sampler2D u_curve0;` through `u_curve3;` - up to 4 user-editable 1D LUT curves. Sample with `texture(u_curve0, vec2(clamp(x, 0.0, 1.0), 0.5)).r` where `x` is 0–1. Use for tone curves, remapping, etc.
- `uniform int u_pass;` - current pass index (when using multi-pass)

**Multi-pass rendering:**
- Add `#pragma passes N` on the second line to enable N passes
- On pass 0, `u_image0` is the original input; on subsequent passes it contains the previous pass output
- Use `u_pass` to vary behavior per pass (e.g., horizontal vs. vertical blur)

**Important constraints:**
- GLSL ES 3.00 only - no GLSL 1.x `varying`/`attribute`, no `gl_FragColor`
- No `#include`, no external textures, no custom vertex shader
- Document each uniform with a comment showing its purpose and expected range
````

### Example prompt fill-in

> **Effect I want:** A chromatic aberration effect that splits RGB channels outward from the center of the image. u_float0 controls the strength of the offset (0 = no effect, 10 = extremely strong). The offset should scale with distance from the center.

Loading