GLSL and WGSL Shaders skill for Codex

Shaders that compile on phones and look right. Codex identifies the shader dialect first, from WebGL2 GLSL and Three.js ShaderMaterial to Godot shading language and WebGPU WGSL, then writes effects with linear-space color, explicit precision, NaN-safe math and correctly aligned uniform buffers. It ports between dialects with a mapping reference, validates offline with glslangValidator or naga, and reports the GPU cost of each effect.

  • Game Development
  • $glsl-shaders
  • 167 lines
  • 3 files
  • ~2,050 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- glsl-shaders

Installs to ~/.agents/skills/glsl-shaders/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $glsl-shaders, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Writes and debugs GLSL and WGSL shaders for games and the web - WebGL2 GLSL ES 3.00, Three.js ShaderMaterial, Godot shading language, WebGPU WGSL with bind groups and uniform buffer alignment - covering fullscreen post effects, dissolve, outlines, toon ramps, scrolling and distortion, SDF shapes, color space and precision, plus offline validation with glslangValidator and naga. Use when writing a vertex or fragment shader, porting a Shadertoy or GLSL effect to WGSL or Godot, a shader compiles on desktop but fails on mobile, colors look washed out, or a shader is too slow.

Use it when

  • Writing vertex, fragment or post-processing shaders for a game or web scene
  • Porting GLSL between WebGL, WGSL, Godot or Shadertoy
  • Colors look washed out, banding appears or NaNs show as black pixels
  • A shader fails on mobile or costs too much GPU time

Use something else when

How to check the result

The shader validates with glslangValidator or naga, compiles in the engine on desktop and one mobile device, and stays NaN-free with uniforms at their extremes.

The mistake it prevents

Using mediump floats for time-based UV animation, which looks fine at first and then jitters or freezes on mobile GPUs once the time value grows.

~/.agents/skills/glsl-shaders/SKILL.md

--- name: glsl-shaders description: Writes and debugs GLSL and WGSL shaders for games and the web - WebGL2 GLSL ES 3.00, Three.js ShaderMaterial, Godot shading language, WebGPU WGSL… license: MIT ---

Done well means: the shader compiles on the weakest target (mobile WebGL2, the oldest supported GPU), math happens in linear color space with one conversion at output, every uniform has a clear unit and range, cost per pixel is known, and the effect was validated offline before it went into the engine.

Identify the dialect first

TargetLanguageNotes
WebGL2, rawGLSL ES 3.00 (#version 300 es)in/out, explicit out vec4 fragColor;
Three.js ShaderMaterialGLSL ES 3.00 with Three’s prefixThree injects projectionMatrix, modelViewMatrix, position, uv, normal; RawShaderMaterial injects nothing
Three.js WebGPU rendererTSL (node materials)GLSL ShaderMaterial is not supported there
Godot 4Godot shading language (GLSL-like)shader_type canvas_item;/spatial;, built-ins UV, COLOR, TIME, ALBEDO
WebGPU, wgpu, BevyWGSLExplicit types, bind groups, strict uniformity rules
UnityHLSL / Shader GraphOut of scope here except for porting math
ShadertoyGLSL with mainImage(out vec4, in vec2) and iTime, iResolutionNeeds a wrapper to run anywhere else

Read AGENTS.md, then find how shaders are loaded (inline strings, .glsl imports via a Vite plugin, .gdshader, .wgsl files) and follow it.

Fragment shader skeleton (WebGL2 / Three.js)

// Three.js ShaderMaterial fragment shader: dissolve with an edge glow
uniform sampler2D uMap;
uniform sampler2D uNoise;
uniform float uProgress;      // 0 = intact, 1 = fully dissolved
uniform vec3 uEdgeColor;      // linear RGB
uniform float uEdgeWidth;     // in noise units, e.g. 0.05

varying vec2 vUv;

void main() {
  vec4 base = texture2D(uMap, vUv);
  float n = texture2D(uNoise, vUv).r;
  float cut = n - uProgress;
  if (cut < 0.0) discard;
  float edge = 1.0 - smoothstep(0.0, uEdgeWidth, cut);
  vec3 color = mix(base.rgb, uEdgeColor * 4.0, edge);   // >1.0 feeds bloom in HDR pipelines
  gl_FragColor = vec4(color, base.a);
  #include <colorspace_fragment>                         // Three: linear -> output color space
}

Three.js maps texture2D, varying and gl_FragColor to GLSL3 equivalents. In raw GLSL ES 3.00 write texture, in/out and declare out vec4 fragColor;.

WGSL equivalent structure

struct Params {
  time: f32,
  progress: f32,
  resolution: vec2f,          // offset 8: vec2f aligns to 8 bytes
};
@group(0) @binding(0) var<uniform> params: Params;
@group(0) @binding(1) var colorTex: texture_2d<f32>;
@group(0) @binding(2) var colorSampler: sampler;

struct VsOut { @builtin(position) pos: vec4f, @location(0) uv: vec2f };

@vertex
fn vs_main(@builtin(vertex_index) i: u32) -> VsOut {
  // Fullscreen triangle; WebGPU texture space has v = 0 at the top, so flip y.
  var p = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));
  var out: VsOut;
  out.pos = vec4f(p[i], 0.0, 1.0);
  out.uv = vec2f(p[i].x * 0.5 + 0.5, 0.5 - p[i].y * 0.5);
  return out;
}

@fragment
fn fs_main(in: VsOut) -> @location(0) vec4f {
  let c = textureSample(colorTex, colorSampler, in.uv);   // must be in uniform control flow
  let vignette = smoothstep(0.9, 0.3, distance(in.uv, vec2f(0.5)));
  return vec4f(c.rgb * vignette, c.a);
}

For a GLSL to WGSL and GLSL to Godot translation table (types, built-ins, texture calls, coordinate conventions), read references/glsl-wgsl-mapping.md when porting a shader between dialects.

Rules that prevent most bugs

  • Color space. Do lighting and blending in linear space. Textures holding color (albedo, UI) are sRGB and must be decoded (engine texture settings or SRGB formats); data textures (normals, noise, masks) must not be. Convert to sRGB once at output. Washed-out or too-dark results almost always mean a double or missing conversion.
  • Precision. In GLSL ES fragment shaders, declare precision highp float; when computing positions, UV over large textures or time; mediump (often 16-bit on mobile) breaks UV scrolling after minutes of uTime. Wrap time: mod(uTime, 1000.0) or pass fract values.
  • Uniform layout. In std140 (GLSL UBOs) and WGSL uniform buffers, vec3 aligns to 16 bytes. Pack as vec4 or put a float right after a vec3 deliberately. Mismatched CPU-side layout gives garbage values with no error.
  • Undefined math. pow(x, y) with negative x, normalize(vec3(0)), sqrt of negative, division by zero, and atan(0, 0) produce NaN or implementation-defined values that differ per GPU. Clamp inputs (max(x, 0.0), max(len, 1e-5)).
  • Derivatives and texture sampling in branches. texture(), dFdx, fwidth inside non-uniform branches give undefined mip selection in GLSL and are a compile error in WGSL (textureSample requires uniform control flow). Sample before branching, or use textureSampleLevel / textureLod with an explicit level.
  • Premultiplied alpha. Know whether the pipeline expects premultiplied output. Dark fringes around sprites mean straight alpha blended as premultiplied (or the reverse).
  • Coordinate conventions. GL texture v=0 is the bottom; WebGPU/D3D/Metal v=0 is the top. Render-target sampling in one API is flipped in the other.

Performance

Cost driverGuidance
Overdraw (full-screen passes, layered transparency)Count passes; each 1080p full-screen pass is ~2M fragment invocations
Texture fetchesFewer, smaller, mipmapped textures; pack masks into RGBA channels
Dependent reads (UV computed from another texture)Fine on desktop, costly on old mobile; keep chains short
BranchingUniform branches (same for all pixels) are cheap; divergent ones run both sides
LoopsConstant bounds; avoid large loops per pixel (blur: separable two-pass, not N x N)
discardDisables early depth test on many GPUs; use alpha blending or alpha-to-coverage where possible

Move work up the pipeline: per-frame constants on the CPU, per-vertex values in the vertex shader (interpolated), per-pixel only what must be.

Debugging

  • Output intermediate values as color: fragColor = vec4(vec3(n), 1.0); to see a noise value, vec4(normal * 0.5 + 0.5, 1.0) for normals, bright magenta for NaN checks (if (isnan(v)) fragColor = vec4(1,0,1,1); in GLSL ES 3.00).
  • Frame captures: Spector.js (WebGL), browser WebGPU devtools, RenderDoc (desktop, Vulkan/D3D/GL).
  • Change one thing per reload; shader errors in engines often report the line of the generated code, not your file. Print the final source the engine compiled when lines do not match.

Offline validation

Both tools are offline once installed (installing needs network and may need approval):

glslangValidator shader.frag            # GLSL; needs a #version line; -S frag for unknown extensions
naga shader.wgsl                        # WGSL validation (cargo install naga-cli)
naga shader.frag shader.wgsl            # naga can also translate GLSL -> WGSL as a starting point

For Three.js or Godot shaders that rely on injected prefixes, validate in the engine: load the material in a test scene and check the console or Godot output for compile errors.

Verification checklist

  • Compiles in the target engine with zero warnings, on WebGL2 and on one mobile device
  • Looks correct in both linear and sRGB output paths the project uses
  • Uniforms at their extremes (0, 1, large time) produce no NaN, banding or popping
  • Measured: GPU frame time with and without the effect at target resolution
  • CPU-side uniform buffer layout matches the shader struct byte for byte (WGSL/std140)

Hand-off summary

**Shader:** dissolve.frag (Three.js ShaderMaterial, GLSL3) + dissolve.wgsl port for the WebGPU path
**Uniforms:** uProgress 0..1, uEdgeWidth 0.05, uEdgeColor linear RGB (HDR x4 for bloom)
**Checks:** glslangValidator and naga clean; Pixel 6 + desktop Chrome; no NaN at progress 0 and 1
**Cost:** +0.3 ms GPU at 1080p (1 extra texture fetch, discard on dissolved pixels)

What the zip contains

Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "GLSL and WGSL Shaders"
  short_description: "Shaders that compile on phones and look right"
  default_prompt: "$glsl-shaders Write a dissolve shader with a glowing edge for our Three.js enemies"
references/glsl-wgsl-mapping.md101 lines, 4.8 KB
# GLSL, WGSL and Godot shading language mapping

Read this when porting a shader between dialects. Translate structure first (inputs, outputs,
uniforms), then built-ins, then the math, then fix coordinate conventions last.

## Types

| GLSL | WGSL | Godot |
|---|---|---|
| `float` | `f32` | `float` |
| `int` / `uint` | `i32` / `u32` | `int` / `uint` |
| `vec2` `vec3` `vec4` | `vec2f` `vec3f` `vec4f` (`vec2<f32>`) | `vec2` `vec3` `vec4` |
| `ivec2` | `vec2i` | `ivec2` |
| `mat4` | `mat4x4f` | `mat4` |
| `sampler2D` | `texture_2d<f32>` + `sampler` (two bindings) | `sampler2D` with hints (`: source_color`, `: hint_normal`) |
| `bool` uniforms | Not allowed in uniform buffers; use `u32` | `bool` |

WGSL has no implicit conversions: `1` is an integer literal; write `1.0` for floats and wrap
with `f32(i)` when mixing.

## Declarations

| Concept | GLSL ES 3.00 | WGSL | Godot |
|---|---|---|---|
| Uniform value | `uniform float uTime;` | Field in a struct bound as `var<uniform>` | `uniform float speed = 1.0;` (default allowed) |
| Vertex input | `in vec3 position;` | `@location(0) position: vec3f` parameter | Built-in `VERTEX` |
| Varying | `out vec2 vUv;` / `in vec2 vUv;` | Struct field `@location(0) uv: vec2f` | `varying vec2 v_uv;` |
| Fragment output | `out vec4 fragColor;` | Return `@location(0) vec4f` | Write `COLOR` (canvas_item) or `ALBEDO`, `ALPHA` (spatial) |
| Clip position | `gl_Position` | Return field `@builtin(position)` | Engine computes; modify `VERTEX` |
| Pixel coordinate | `gl_FragCoord` | `@builtin(position)` input in fragment | `FRAGCOORD` |
| Vertex index | `gl_VertexID` | `@builtin(vertex_index)` | `VERTEX_ID` |
| Constants | `const float PI = 3.14159;` | `const PI: f32 = 3.14159;` | `const float PI = 3.14159;` |
| Mutable local | `float x = 1.0;` | `var x = 1.0;` (`let` is immutable) | `float x = 1.0;` |

## Functions

| GLSL | WGSL | Notes |
|---|---|---|
| `texture(s, uv)` | `textureSample(t, s, uv)` | WGSL requires uniform control flow |
| `textureLod(s, uv, l)` | `textureSampleLevel(t, s, uv, l)` | Allowed in non-uniform flow |
| `texelFetch(s, ivec2, 0)` | `textureLoad(t, vec2i, 0)` | No sampler needed |
| `mix` | `mix` | Same |
| `fract`, `floor`, `clamp`, `smoothstep`, `step` | Same names | Same semantics |
| `mod(x, y)` | `x - y * floor(x / y)` | WGSL `%` truncates toward zero, unlike GLSL `mod` for negatives |
| `atan(y, x)` | `atan2(y, x)` | |
| `inversesqrt` | `inverseSqrt` | |
| `dFdx`, `dFdy`, `fwidth` | `dpdx`, `dpdy`, `fwidth` | Uniform control flow in WGSL |
| `lessThan(a, b)` | `a < b` (component-wise vector of bool) | |
| `isnan` | No built-in; test `x != x` | Optimisers may remove it; prefer clamping inputs |

Godot uses GLSL function names (`texture`, `mix`, `mod`, `fract`), so most math ports directly.

## Built-in uniforms

| Shadertoy | Three.js (you provide) | Godot | WGSL |
|---|---|---|---|
| `iTime` | `uniform float uTime` updated per frame | `TIME` | Field in your uniform struct |
| `iResolution` | `uniform vec2 uResolution` | `SCREEN_PIXEL_SIZE` (1 / size) or `textureSize` | Field in your uniform struct |
| `fragCoord` | `gl_FragCoord.xy` | `FRAGCOORD.xy` | `@builtin(position)` .xy |
| `iChannel0` | `uniform sampler2D uChannel0` | `uniform sampler2D tex` | `texture_2d<f32>` + `sampler` |
| `iMouse` | `uniform vec4 uMouse` | Uniform you set from script | Uniform field |

Shadertoy wrapper for GLSL ES 3.00:

```glsl
#version 300 es
precision highp float;
uniform float iTime;
uniform vec3 iResolution;
out vec4 fragColor;
void mainImage(out vec4 c, in vec2 fragCoord);   // paste the Shadertoy body below
void main() { mainImage(fragColor, gl_FragCoord.xy); }
```

## Coordinate conventions

| Item | OpenGL / WebGL | WebGPU / Vulkan / D3D / Metal | Godot |
|---|---|---|---|
| NDC y | Up | Up | Engine handles |
| Clip-space depth | -1 to 1 | 0 to 1 | Engine handles (reverse-Z in 4.3+ on supported renderers) |
| Texture v = 0 | Bottom row | Top row | Top (Godot `UV` origin is top-left) |
| Framebuffer origin (`gl_FragCoord` / position) | Bottom-left | Top-left | `FRAGCOORD` top-left |

Projection matrices built for GL depth range are wrong in WebGPU without a correction; math
libraries usually offer a zero-to-one variant (for example `mat4.perspectiveZO` in gl-matrix).

## Uniform buffer alignment (WGSL and std140)

| Type | Size | Alignment |
|---|---|---|
| `f32`, `i32`, `u32` | 4 | 4 |
| `vec2f` | 8 | 8 |
| `vec3f` | 12 | 16 |
| `vec4f` | 16 | 16 |
| `mat4x4f` | 64 | 16 |
| Array element in a uniform buffer | Rounded up to 16 bytes stride | 16 |
| Struct | Rounded to multiple of its largest alignment | Largest member alignment |

Example: `struct { a: vec3f, b: f32 }` packs into 16 bytes (`b` fills the vec3 padding), but
`struct { a: f32, b: vec3f }` takes 32 bytes (`b` starts at offset 16). Mirror the layout on
the CPU with a `Float32Array` and explicit offsets, or use a layout helper library.

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem