Files
skills/shader-dev/techniques/matrix-transform.md
shihao 6487becf60 Initial commit: add all skills files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:52:49 +08:00

12 KiB

Matrix Transforms & Camera

Use Cases

  • Camera systems in 3D scenes (orbit camera, fly camera, path camera)
  • SDF object domain transforms via translation, rotation, and scale matrices
  • Generating 3D rays from screen pixels (perspective / orthographic projection)
  • Hierarchical rotation transforms for joint animation
  • Rotation in noise domain warping, IFS fractal iterations

Core Principles

The essence of matrix transforms is coordinate system transformation. In a ray marching pipeline:

  1. Camera matrix: Screen pixels → world-space ray direction (view-to-world)
  2. Object transform matrix: World-space sample point → object local space (world-to-local, domain transform)

Key Formulas

2D Rotation R(θ) = [[cosθ, -sinθ], [sinθ, cosθ]]

3D Rotation Around Y-Axis Ry(θ) = [[cosθ, 0, sinθ], [0, 1, 0], [-sinθ, 0, cosθ]]

Rodrigues (Arbitrary Axis k, Angle θ): R = cosθ·I + (1-cosθ)·k⊗k + sinθ·K

LookAt Camera:

forward = normalize(target - eye)
right   = normalize(cross(forward, worldUp))
up      = cross(right, forward)
viewMatrix = mat3(right, up, forward)

Perspective Ray: rd = normalize(camMatrix * vec3(uv, focalLength))

Implementation Steps

Step 1: Screen Coordinate Normalization

// Range [-aspect, aspect] x [-1, 1]
vec2 uv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;

Step 2: Rotation Matrices

// 2D rotation (mat2)
mat2 rot2D(float a) {
    float c = cos(a), s = sin(a);
    return mat2(c, s, -s, c);
}

// 3D single-axis rotation (mat3)
mat3 rotX(float a) {
    float s = sin(a), c = cos(a);
    return mat3(1, 0, 0,  0, c, s,  0, -s, c);
}
mat3 rotY(float a) {
    float s = sin(a), c = cos(a);
    return mat3(c, 0, s,  0, 1, 0,  -s, 0, c);
}
mat3 rotZ(float a) {
    float s = sin(a), c = cos(a);
    return mat3(c, s, 0,  -s, c, 0,  0, 0, 1);
}

// Euler angles → mat3 (yaw/pitch/roll)
mat3 fromEuler(vec3 ang) {
    vec2 a1 = vec2(sin(ang.x), cos(ang.x));
    vec2 a2 = vec2(sin(ang.y), cos(ang.y));
    vec2 a3 = vec2(sin(ang.z), cos(ang.z));
    mat3 m;
    m[0] = vec3( a1.y*a3.y + a1.x*a2.x*a3.x,
                  a1.y*a2.x*a3.x + a3.y*a1.x,
                 -a2.y*a3.x);
    m[1] = vec3(-a2.y*a1.x, a1.y*a2.y, a2.x);
    m[2] = vec3( a3.y*a1.x*a2.x + a1.y*a3.x,
                  a1.x*a3.x - a1.y*a3.y*a2.x,
                  a2.y*a3.y);
    return m;
}

// Rodrigues arbitrary-axis rotation (mat3)
mat3 rotationMatrix(vec3 axis, float angle) {
    axis = normalize(axis);
    float s = sin(angle), c = cos(angle), oc = 1.0 - c;
    return mat3(
        oc*axis.x*axis.x + c,          oc*axis.x*axis.y - axis.z*s, oc*axis.z*axis.x + axis.y*s,
        oc*axis.x*axis.y + axis.z*s,   oc*axis.y*axis.y + c,        oc*axis.y*axis.z - axis.x*s,
        oc*axis.z*axis.x - axis.y*s,   oc*axis.y*axis.z + axis.x*s, oc*axis.z*axis.z + c
    );
}

Step 3: LookAt Camera

// Classic setCamera, cr = camera roll
mat3 setCamera(in vec3 ro, in vec3 ta, float cr) {
    vec3 cw = normalize(ta - ro);
    vec3 cp = vec3(sin(cr), cos(cr), 0.0);
    vec3 cu = normalize(cross(cw, cp));
    vec3 cv = normalize(cross(cu, cw));
    return mat3(cu, cv, cw);
}

// mat4 LookAt (with translation, for homogeneous coordinate scenes)
mat4 LookAt(vec3 pos, vec3 target, vec3 up) {
    vec3 dir = normalize(target - pos);
    vec3 x = normalize(cross(dir, up));
    vec3 y = cross(x, dir);
    return mat4(vec4(x, 0), vec4(y, 0), vec4(dir, 0), vec4(pos, 1));
}

Step 4: Perspective Ray Generation

// mat3 camera — focalLength controls FOV: 1.0≈90°, 2.0≈53°, 4.0≈28°
#define FOCAL_LENGTH 2.0
mat3 cam = setCamera(ro, ta, 0.0);
vec3 rd = cam * normalize(vec3(uv, FOCAL_LENGTH));

// Manual basis vector composition
#define FOV 1.0
vec3 rd = normalize(camDir + (uv.x * camRight + uv.y * camUp) * FOV);

// mat4 homogeneous coordinates
mat4 viewToWorld = LookAt(camPos, camTarget, camUp);
vec3 rd = (viewToWorld * normalize(vec4(uv, 1.0, 0.0))).xyz;

Step 5: Mouse-Interactive Camera

// Spherical coordinate orbit camera
#define CAM_DIST 5.0
#define CAM_HEIGHT 1.0

vec2 mouse = iMouse.xy / iResolution.xy;
float angleH = mouse.x * 6.2832;
float angleV = mouse.y * 3.1416 - 1.5708;

if (iMouse.z <= 0.0) {
    angleH = iTime * 0.5;
    angleV = 0.3;
}

vec3 ro = vec3(
    CAM_DIST * cos(angleH) * cos(angleV),
    CAM_DIST * sin(angleV) + CAM_HEIGHT,
    CAM_DIST * sin(angleH) * cos(angleV)
);
vec3 ta = vec3(0.0);

Step 6: SDF Domain Transforms

// Translation
float d = sdSphere(p - vec3(2.0, 0.0, 0.0), 1.0);

// Rotation (orthogonal matrix inverse = transpose)
float d = sdBox(rotY(0.5) * p, vec3(1.0));

// Scale (divide by scale factor, multiply back into distance)
#define SCALE 2.0
float d = sdSphere(p / SCALE, 1.0) * SCALE;

// mat4 SRT composition
mat4 Loc4(vec3 d) {
    d *= -1.0;
    return mat4(1,0,0,d.x, 0,1,0,d.y, 0,0,1,d.z, 0,0,0,1);
}

mat4 transposeM4(in mat4 m) {
    return mat4(
        vec4(m[0].x, m[1].x, m[2].x, m[3].x),
        vec4(m[0].y, m[1].y, m[2].y, m[3].y),
        vec4(m[0].z, m[1].z, m[2].z, m[3].z),
        vec4(m[0].w, m[1].w, m[2].w, m[3].w));
}

vec3 opTx(vec3 p, mat4 m) {
    return (transposeM4(m) * vec4(p, 1.0)).xyz;
}

// First translate to (3,0,0), then rotate 45° around Y-axis
mat4 xform = Rot4Y(0.785) * Loc4(vec3(3.0, 0.0, 0.0));
float d = sdBox(opTx(p, xform), vec3(1.0));

Step 7: Quaternion Rotation

vec4 axisAngleToQuat(vec3 axis, float angleDeg) {
    float half_angle = angleDeg * 3.14159265 / 360.0;
    vec2 sc = sin(vec2(half_angle, half_angle + 1.5707963));
    return vec4(normalize(axis) * sc.x, sc.y);
}

vec3 quatRotate(vec3 pos, vec3 axis, float angleDeg) {
    vec4 q = axisAngleToQuat(axis, angleDeg);
    return pos + 2.0 * cross(q.xyz, cross(q.xyz, pos) + q.w * pos);
}

// Hierarchical rotation in joint animation
vec3 limbPos = quatRotate(p - shoulderOffset, vec3(1,0,0), swingAngle);
float d = sdEllipsoid(limbPos, limbSize);

Complete Code Template

Can be run directly in ShaderToy, demonstrating LookAt camera + multi-object domain transforms + mouse interaction.

// === Matrix Transforms & Camera - Complete Template ===

#define PI 3.14159265
#define MAX_STEPS 128
#define MAX_DIST 50.0
#define SURF_DIST 0.001
#define FOCAL_LENGTH 2.0
#define CAM_DIST 6.0
#define AUTO_SPEED 0.4

// ---------- Rotation Matrix Utilities ----------

mat2 rot2D(float a) {
    float c = cos(a), s = sin(a);
    return mat2(c, s, -s, c);
}

mat3 rotX(float a) {
    float s = sin(a), c = cos(a);
    return mat3(1,0,0, 0,c,s, 0,-s,c);
}

mat3 rotY(float a) {
    float s = sin(a), c = cos(a);
    return mat3(c,0,s, 0,1,0, -s,0,c);
}

mat3 rotZ(float a) {
    float s = sin(a), c = cos(a);
    return mat3(c,s,0, -s,c,0, 0,0,1);
}

mat3 rotAxis(vec3 axis, float angle) {
    axis = normalize(axis);
    float s = sin(angle), c = cos(angle), oc = 1.0 - c;
    return mat3(
        oc*axis.x*axis.x+c,         oc*axis.x*axis.y-axis.z*s, oc*axis.z*axis.x+axis.y*s,
        oc*axis.x*axis.y+axis.z*s,  oc*axis.y*axis.y+c,        oc*axis.y*axis.z-axis.x*s,
        oc*axis.z*axis.x-axis.y*s,  oc*axis.y*axis.z+axis.x*s, oc*axis.z*axis.z+c
    );
}

// ---------- LookAt Camera ----------

mat3 setCamera(vec3 ro, vec3 ta, float cr) {
    vec3 cw = normalize(ta - ro);
    vec3 cp = vec3(sin(cr), cos(cr), 0.0);
    vec3 cu = normalize(cross(cw, cp));
    vec3 cv = normalize(cross(cu, cw));
    return mat3(cu, cv, cw);
}

// ---------- SDF Primitives ----------

float sdSphere(vec3 p, float r) { return length(p) - r; }

float sdBox(vec3 p, vec3 b) {
    vec3 q = abs(p) - b;
    return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0);
}

float sdTorus(vec3 p, vec2 t) {
    vec2 q = vec2(length(p.xz) - t.x, p.y);
    return length(q) - t.y;
}

// ---------- Scene (Domain Transform Demo) ----------

float map(vec3 p) {
    float d = p.y + 1.0; // Ground plane

    // Static sphere
    d = min(d, sdSphere(p, 0.5));

    // Rotating box (spinning around Y-axis)
    vec3 p2 = p - vec3(2.5, 0.0, 0.0);
    p2 = rotY(iTime * 0.8) * p2;
    d = min(d, sdBox(p2, vec3(0.6)));

    // Arbitrary-axis rotating torus
    vec3 p3 = p - vec3(-2.5, 0.5, 0.0);
    p3 = rotAxis(vec3(1,1,0), iTime * 0.6) * p3;
    d = min(d, sdTorus(p3, vec2(0.6, 0.2)));

    // Scaled + rotated sphere
    vec3 p4 = p - vec3(0.0, 0.5, 2.5);
    p4 = rotZ(iTime * 1.2) * rotX(iTime * 0.7) * p4;
    float scale = 1.5;
    d = min(d, sdSphere(p4 / scale, 0.4) * scale);

    return d;
}

// ---------- Normal ----------

vec3 calcNormal(vec3 p) {
    vec2 e = vec2(0.001, 0.0);
    return normalize(vec3(
        map(p + e.xyy) - map(p - e.xyy),
        map(p + e.yxy) - map(p - e.yxy),
        map(p + e.yyx) - map(p - e.yyx)
    ));
}

// ---------- Ray March ----------

float rayMarch(vec3 ro, vec3 rd) {
    float t = 0.0;
    for (int i = 0; i < MAX_STEPS; i++) {
        vec3 p = ro + rd * t;
        float d = map(p);
        if (d < SURF_DIST) break;
        t += d;
        if (t > MAX_DIST) break;
    }
    return t;
}

// ---------- Main Function ----------

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;

    // Mouse-interactive orbit camera
    float angleH, angleV;
    if (iMouse.z > 0.0) {
        vec2 m = iMouse.xy / iResolution.xy;
        angleH = m.x * 2.0 * PI;
        angleV = (m.y - 0.5) * PI;
    } else {
        angleH = iTime * AUTO_SPEED;
        angleV = 0.35;
    }

    vec3 ro = vec3(
        CAM_DIST * cos(angleH) * cos(angleV),
        CAM_DIST * sin(angleV) + 1.0,
        CAM_DIST * sin(angleH) * cos(angleV)
    );
    vec3 ta = vec3(0.0);

    mat3 cam = setCamera(ro, ta, 0.0);
    vec3 rd = cam * normalize(vec3(uv, FOCAL_LENGTH));

    float t = rayMarch(ro, rd);

    vec3 col = vec3(0.0);
    if (t < MAX_DIST) {
        vec3 p = ro + rd * t;
        vec3 n = calcNormal(p);
        vec3 lightDir = normalize(vec3(1.0, 2.0, -1.0));
        float diff = max(dot(n, lightDir), 0.0);
        col = vec3(0.8, 0.85, 0.9) * (diff + 0.15);
        if (p.y < -0.99) {
            float checker = mod(floor(p.x) + floor(p.z), 2.0);
            col *= 0.5 + 0.3 * checker;
        }
    } else {
        col = vec3(0.4, 0.6, 0.9) - rd.y * 0.3;
    }

    col = pow(col, vec3(0.4545));
    fragColor = vec4(col, 1.0);
}

Common Variants

Orthographic Projection Camera

#define ORTHO_SIZE 5.0
mat3 cam = setCamera(ro, ta, 0.0);
vec3 rd = cam * vec3(0.0, 0.0, 1.0);   // Fixed direction
ro += cam * vec3(uv * ORTHO_SIZE, 0.0); // Offset origin

Euler Angle Full Rotation Camera

vec3 ang = vec3(pitch, yaw, roll);
mat3 rot = fromEuler(ang);
vec3 ori = vec3(0.0, 0.0, 3.0) * rot;
vec3 rd = normalize(vec3(uv, -2.0)) * rot;

Quaternion Joint Rotation

vec3 legP = quatRotate(p - hipOffset, vec3(1,0,0), legAngle);
float dLeg = sdEllipsoid(legP, vec3(0.2, 0.6, 0.25));

mat4 SRT Pipeline

mat4 Rot4Y(float a) {
    float c = cos(a), s = sin(a);
    return mat4(c,0,s,0, 0,1,0,0, -s,0,c,0, 0,0,0,1);
}

mat4 xform = Rot4Y(angle) * Loc4(vec3(3.0, 0.0, 0.0));
float d = sdBox(opTx(p, xform), boxSize);

Path Camera (Animated Flight)

vec2 pathCenter(float z) {
    return vec2(sin(z * 0.17) * 3.0, sin(z * 0.1 + 4.0) * 2.0);
}

float z_offset = iTime * 10.0;
vec3 camPos = vec3(pathCenter(z_offset), 0.0);
vec3 camTarget = vec3(pathCenter(z_offset + 5.0), 5.0);
mat4 viewToWorld = LookAt(camPos, camTarget, camUp);
vec3 rd = (viewToWorld * normalize(vec4(uv, 1.0, 0.0))).xyz;

Performance & Composition

Performance:

  • Compute sin/cos of the same angle only once: vec2 sc = sin(vec2(a, a + 1.5707963));
  • Use mat3 instead of mat4 for pure rotation (saves 7 multiply-adds)
  • Inverse of orthogonal rotation matrix = transpose; use transpose(m) or v * m
  • Pre-compute matrices that don't depend on p outside map()
  • Pre-multiply multiple rotations into a single matrix

Composition:

  • SDF / Ray Marching: Camera generates rays + domain transforms place objects (fundamental pipeline)
  • Noise / fBm: Rotate sampling coordinates to break axis-aligned regularity fbm(rot * p)
  • Fractals / IFS: Embed rotation in iterations to create complex geometry
  • Lighting: Normal transform for pure rotation matrices is the same as vertex transform
  • Post-Processing: FOV for depth of field; mat2 for chromatic aberration/motion blur direction

Further Reading

For complete step-by-step tutorials, mathematical derivations, and advanced usage, see reference