These appendices collect details that are useful while writing shaders. The chapters explain why the operations work; this page helps you recognize the expression you need and the mistake beside it.
A. The GLSL we need
Read each row from input to result. Throughout the book, vectors are columns, matrix constructors receive columns, and a forward transform is M * p. A vector's type does not tell you its space or units; its name and the surrounding code must do that.
| Input | Meaning | Result | Common mistake |
|---|---|---|---|
float, int, bool | Real value, integer, condition. | 1.0 / 2.0 is 0.5. | 1 / 2 performs integer division: zero. |
vec2(0.5)vec3(p, 1.0) | Repeat a scalar; assemble components. | (0.5, 0.5); append 1 to a vec2. | A constructor does not normalize a direction. |
p.yx, color.rgb | Select and reorder components. | A new vector of the selected size. | Mixing component families, such as p.xr. |
uniform vec2 uResolution; | A host-supplied value shared by shader invocations. | Read the current drawing-buffer size. | Expecting the declaration to measure the canvas automatically. |
radians(90.0) | Convert degrees for trigonometry. | π/2; sin and cos take radians. | Passing a degree slider directly to sin. |
length(b - a) | Measure a displacement. | Nonnegative scalar distance. | Measuring uncorrected UV coordinates on a rectangular viewport. |
normalize(v) | Keep direction, set length to one. | Unit vector when v is nonzero. | Normalizing zero; guard squared length first. |
mix(a, b, t) | Compute (1-t)*a + t*b. | Interpolation, or extrapolation outside 0…1. | Assuming mix clamps t. |
dot(a, b) | Sum matching component products. | Scalar; cosine only for two unit vectors. | Treating every dot product as an angle. |
cross(a, b) | Perpendicular to two vec3 inputs. | Vector with magnitude equal to parallelogram area. | Assuming unit length, or swapping inputs without reversing the sign. |
reflect(I, N) | I - 2*dot(N,I)*N. | Reflected incident direction for unit N. | Using a tangent as the normal or reversing the incident direction. |
smoothstep(a,b,x) | Smooth a threshold between ordered edges. | 0…1; 1-smoothstep reverses the mask. | Supplying a >= b; the result is undefined. |
mat2(axisX, axisY) | Pack two columns; M[0] selects the first. | M*p = p.x*axisX + p.y*axisY. | Reading the constructor as rows. mat2(1.0) is identity. |
R * S * p | Compose forward transforms. | Scale first, then rotate. | Reading the actions from left to right. |
inverse(M) | Undo an invertible matrix. | Backward map from world to local. | Inverting a singular or numerically unstable matrix. |
determinant(M) | Signed area/volume factor. | Zero means collapse for a square matrix. | Treating every tiny nonzero determinant as a safe inverse. |
transpose(M) | Exchange rows and columns. | Used in transpose(inverse(A)) for normals. | Assuming transpose always equals inverse. |
matrixCompMult(A,B) | Multiply corresponding entries. | Component-wise matrix product. | Using it to compose transforms instead of A * B. |
For a custom helper, declare its return type and parameter types, then return that type: float circleField(vec2 p, float r) { return length(p) - r; }. A helper name does not change the coordinate contract: this distance is measured in the units of p.
B. From the browser to AGSL
Start with the circle from chapter 03. Both versions below place its center at (0.25, 0.15), with radius 0.35 and equal X/Y units. The offset makes a mistaken Y flip visible: the circle belongs above and right of center.
AGSL participates in Android's drawing pipeline. Its entry function receives local Canvas coordinates and returns a color; it is not a replacement for the vertex/fragment mesh pipeline in chapters 20–22. The default Canvas origin is upper left, while WebGL's gl_FragCoord origin is lower left. Local shader matrices or a parent shader can change the coordinates received. See Android's AGSL/GLSL differences.
The complete WebGL fragment shader
Paste this into the chapter 03 live editor and run it. The existing host supplies uResolution and the fullscreen vertex shader. This small version uses fixed center/radius values; the lab's controls and reference readouts still describe its original example.
#version 300 es
precision highp float;
uniform vec2 uResolution;
out vec4 fragColor;
void main() {
vec2 p = (2.0 * gl_FragCoord.xy - uResolution)
/ uResolution.y;
float d = length(p - vec2(0.25, 0.15));
float aa = 2.0 / uResolution.y;
float coverage = 1.0 - smoothstep(0.35 - aa, 0.35 + aa, d);
vec3 background = vec3(0.08, 0.12, 0.20);
vec3 tint = vec3(0.20, 0.80, 0.65);
fragColor = vec4(mix(background, tint, coverage), 1.0);
}
The complete AGSL shader
Remove the GLSL version directive and output declaration. AGSL accepts float2 and half4; its language reference lists supported syntax. Flip pixel Y before applying the same coordinate formula. The two color uniforms below are opaque.
uniform float2 uResolution;
layout(color) uniform half4 uBackground;
layout(color) uniform half4 uTint;
half4 main(float2 fragCoord) {
float2 pixel = float2(fragCoord.x, uResolution.y - fragCoord.y);
float2 p = (2.0 * pixel - uResolution) / uResolution.y;
float d = length(p - float2(0.25, 0.15));
float aa = 2.0 / uResolution.y;
float coverage = 1.0 - smoothstep(0.35 - aa, 0.35 + aa, d);
return mix(uBackground, uTint, half(coverage));
}
A minimal Compose host
Pass the AGSL source above as the agsl string. This composable targets Android 13 / API 33 or later. Call it only on a supported device, with an ordinary Canvas fallback on older versions; the annotation does not perform that runtime check. The Compose shader-brush guide documents this hosting model.
import android.graphics.Color as AndroidColor
import android.graphics.RuntimeShader
import androidx.annotation.RequiresApi
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ShaderBrush
import androidx.compose.ui.unit.dp
@RequiresApi(33)
@Composable
fun AgslCircle(agsl: String, modifier: Modifier = Modifier) {
val shader = remember(agsl) {
RuntimeShader(agsl).apply {
setColorUniform("uBackground",
AndroidColor.valueOf(0.08f, 0.12f, 0.20f, 1f))
setColorUniform("uTint",
AndroidColor.valueOf(0.20f, 0.80f, 0.65f, 1f))
}
}
val brush = remember(shader) { ShaderBrush(shader) }
Canvas(modifier.fillMaxWidth().height(240.dp)) {
if (size.width > 0f && size.height > 0f) {
shader.setFloatUniform("uResolution", size.width, size.height)
drawRect(brush = brush)
}
}
}
The AGSL usage guide shows shader construction and uniform setters. Here, remember retains the compiled shader and brush; each draw supplies the current pixel dimensions. Do not pass dp as the size uniform.
Values and children: initialize numeric uniforms with the matching float/int setter. Replace a GLSL sampler2D/texture(image, uv) pair with a declared uniform shader image;, bind it using setInputShader, and evaluate image.eval(pixelPosition). A bitmap child uses local pixel coordinates, not normalized UV. The RuntimeShader API reference also distinguishes setInputBuffer for data that must bypass color conversion.
Transparency and color: AGSL output must be premultiplied: return half4(rgb * alpha, alpha) for straight RGB. Do not multiply an already premultiplied child result again. Use layout(color) with setColorUniform for colors, allowing conversion into the destination's working space. For linear-light calculations, convert unpremultiplied RGB with toLinearSrgb, do the math, then use fromLinearSrgb before premultiplying the result. These rules are covered by the same RuntimeShader reference. Matching geometry does not promise identical browser/Android color output.
C. A shader math debugging checklist
Freeze animation, reset controls, and choose one pixel or point. Write its expected intermediate value before inspecting the final color. Then walk through these checks:
- Name the units and space. Pixel, UV, local, world, view, or clip? Color coordinates before coloring the shape. Recover equal units with chapter 01; use the final project's space contract for mesh values.
- Check zero before direction. Measure
dot(v,v)before division. Coincident points have no direction. Use an intentional fallback or mark the state undefined, as in chapter 04. - Check unit length. A cosine, reflection normal, or orthonormal camera axis needs the appropriate normalization. Inspect length independently of color; revisit dot products and reflection.
- Read the columns aloud. Multiply by (1,0), then (0,1). Each result should equal the corresponding axis. This catches transposed constructors in chapter 10.
- Trace the order with an asymmetric point. Compare
R*S*pandS*R*p. For affine transforms, inspect the origin too: its path reveals an unintentionally rotated translation. See chapter 13. - Separate geometry from lookup. Forward positions use
M*p; a fragment asking where to sample uses the inverse. Check a round trip with chapter 14. A misplaced procedural center often comes from adding the sampling offset. - Check invertibility before calling inverse. Collapsed axes lose information; nearly parallel axes amplify error. Compare the mapped grid and determinant in chapter 15. Do not conceal collapse by adding epsilon to matrix entries.
- Keep lighting in one space. Form
Lfrom two world positions and transform the normal with the inverse transpose. Checkdot(T,N)near zero after non-uniform scale, as in chapter 21. - Inspect clip coordinates before division. Check depth, FOV, aspect, and near/far planes. Give
gl_Positionthe clip vector; let the GPU clip and divide. Use chapter 20. - Compare CSS size with the drawing buffer. Convert pointer fractions from the displayed rectangle to buffer pixels before applying a pixel-space formula. Use the actual buffer dimensions, including any resolution cap. For a borderless, untransformed canvas:
const rect = canvas.getBoundingClientRect();
const u = (event.clientX - rect.left) / rect.width;
const v = (event.clientY - rect.top) / rect.height;
const pixelX = u * canvas.width;
const pixelY = (1 - v) * canvas.height;
// These match WebGL's lower-left framebuffer convention.
Require nonzero dimensions and update the viewport after resize. CSS rotation, borders, or a different content box need an appropriate pointer conversion. The coordinate lab and final project's pointer mapping show the two distinct contracts: pixel measurement and normalized tilt control.
D. Where to go next
Choose one extension and keep its first version measurable. These are projects to explore, not additional completed chapters.
- Texture distortion. After 02, 05, and 14, replace a procedural checker with an image. Build a draggable lens by displacing lookup coordinates. First verify that zero displacement reproduces the image; then choose an explicit edge-sampling behavior.
- Signed distance fields. After 03, 07, and 14, combine a circle and a capsule into an icon with a controllable outline. Test inside/outside signs. Non-uniform transforms require care: a local distance is not automatically a world-space distance.
- Ray marching. After 17, 19, 20, and the SDF project, render one sphere with an orbiting camera. Visualize step count before adding light. Use a conservative distance bound, a travel limit, and an iteration limit.
- Barycentric coordinates. After 05, 09, and 20, make a triangle whose vertices carry different colors. Show three weights and their sum. Then compare affine screen interpolation with perspective-correct interpolation; handle a collapsed triangle explicitly.
- Deeper linear algebra. After 10, 15, and 18, plot a unit circle under a 2×2 matrix. Explore singular values as its maximum/minimum stretch factors, and condition number as their ratio. Approach collapse and measure round-trip error instead of hiding it.
For each project, keep a plain debug view beside the finished effect. The most reusable result from this book is knowing which number to inspect when the picture surprises you.