Files
skills/shader-dev/techniques/texture-sampling.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

13 KiB

IMPORTANT - GLSL Type Strictness:

  • GLSL is a strongly-typed language and does not support the string type (you cannot define string var)
  • vec2/vec3/vec4 are vector types and cannot be directly assigned a float (e.g., vec2 a = 1.0 must be vec2 a = vec2(1.0))
  • Array indices must be integer constants or uniform variables; runtime-computed floats cannot be used
  • Avoid uninitialized variables — GLSL default values are undefined

Texture Sampling

Use Cases

  • Post-processing effects: Blur, bloom, dispersion, chromatic aberration
  • Procedural noise: FBM layering from noise textures to generate terrain, clouds, fire
  • PBR/IBL: Cubemap environment lighting, BRDF LUT lookup
  • Simulation/feedback systems: Reaction-diffusion, fluid simulation multi-buffer feedback
  • Data storage: Textures used as structured data (game state, keyboard input)
  • Temporal accumulation: TAA, motion blur, previous frame reading

Core Principles

Function Coordinate Type Filtering Typical Use
texture(sampler, uv) Float UV [0,1] Hardware bilinear General texture reading
textureLod(sampler, uv, lod) Float UV + LOD Specified mip level Control blur level / avoid auto mip
texelFetch(sampler, ivec2, lod) Integer pixel coordinates No filtering Exact pixel data reading

Key mathematics:

  1. Hardware bilinear interpolation: texture() automatically linearly blends between 4 adjacent texels
  2. Quintic Hermite smoothing: u = f^3(6f^2 - 15f + 10), C2 continuous (eliminates hardware linear interpolation seams)
  3. LOD control: textureLod third parameter selects mipmap level, lod=0 is original resolution, each +1 halves resolution
  4. Coordinate wrapping: fract(uv) implements torus boundary, equivalent to GL_REPEAT

Implementation Steps

Step 1: Basic Sampling and UV Normalization

vec2 uv = fragCoord / iResolution.xy;
vec4 col = texture(iChannel0, uv);

Step 2: textureLod for Mipmap Control

// In ray marching: force LOD 0 to avoid artifacts
vec3 groundCol = textureLod(iChannel2, groundUv * 0.05, 0.0).rgb;

// Depth of field blur: LOD varies with distance
float focus = mix(maxBlur - coverage, minBlur, smoothstep(.1, .2, coverage));
vec3 col = textureLod(iChannel0, uv + normal, focus).rgb;

// Bloom: sample high mip levels
#define BLOOM_LOD_A 4.0  // adjustable: bloom first mip level
#define BLOOM_LOD_B 5.0
#define BLOOM_LOD_C 6.0
vec3 bloom = vec3(0.0);
bloom += textureLod(iChannel0, uv + off * exp2(BLOOM_LOD_A), BLOOM_LOD_A).rgb;
bloom += textureLod(iChannel0, uv + off * exp2(BLOOM_LOD_B), BLOOM_LOD_B).rgb;
bloom += textureLod(iChannel0, uv + off * exp2(BLOOM_LOD_C), BLOOM_LOD_C).rgb;
bloom /= 3.0;

Step 3: texelFetch for Exact Pixel Reading

// Data storage addresses
const ivec2 txBallPosVel = ivec2(0, 0);
const ivec2 txPaddlePos  = ivec2(1, 0);
const ivec2 txPoints     = ivec2(2, 0);
const ivec2 txState      = ivec2(3, 0);

vec4 loadValue(in ivec2 addr) {
    return texelFetch(iChannel0, addr, 0);
}

void storeValue(in ivec2 addr, in vec4 val, inout vec4 fragColor, in ivec2 fragPos) {
    fragColor = (fragPos == addr) ? val : fragColor;
}

// Keyboard input
float key = texelFetch(iChannel1, ivec2(KEY_SPACE, 0), 0).x;

Step 4: Manual Bilinear + Quintic Hermite Smoothing

float noise(vec2 x) {
    vec2 p = floor(x);
    vec2 f = fract(x);
    vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0); // C2 continuous

    #define TEX_RES 1024.0  // adjustable: noise texture resolution
    float a = texture(iChannel0, (p + vec2(0.0, 0.0)) / TEX_RES).x;
    float b = texture(iChannel0, (p + vec2(1.0, 0.0)) / TEX_RES).x;
    float c = texture(iChannel0, (p + vec2(0.0, 1.0)) / TEX_RES).x;
    float d = texture(iChannel0, (p + vec2(1.0, 1.0)) / TEX_RES).x;

    return a + (b - a) * u.x + (c - a) * u.y + (a - b - c + d) * u.x * u.y;
}

Step 5: FBM Texture Noise

#define FBM_OCTAVES 5       // adjustable: number of layers
#define FBM_PERSISTENCE 0.5 // adjustable: amplitude decay rate

float fbm(vec2 x) {
    float v = 0.0;
    float a = 0.5;
    float totalWeight = 0.0;
    for (int i = 0; i < FBM_OCTAVES; i++) {
        v += a * noise(x);
        totalWeight += a;
        x *= 2.0;
        a *= FBM_PERSISTENCE;
    }
    return v / totalWeight;
}

Step 6: Separable Gaussian Blur

#define BLUR_RADIUS 4  // adjustable: blur radius

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = fragCoord / iResolution.xy;
    vec2 d = vec2(1.0 / iResolution.x, 0.0); // horizontal pass; for vertical pass change to vec2(0, 1/iResolution.y)
    float w[9] = float[9](0.05, 0.09, 0.12, 0.15, 0.16, 0.15, 0.12, 0.09, 0.05);

    vec4 col = vec4(0.0);
    for (int i = -4; i <= 4; i++) {
        col += w[i + 4] * texture(iChannel0, fract(uv + float(i) * d));
    }
    col /= 0.98;
    fragColor = col;
}

Step 7: Dispersion Sampling

#define DISP_SAMPLES 64  // adjustable: sample count

vec3 sampleWeights(float i) {
    return vec3(i * i, 46.6666 * pow((1.0 - i) * i, 3.0), (1.0 - i) * (1.0 - i));
}

vec3 sampleDisp(sampler2D tex, vec2 uv, vec2 disp) {
    vec3 col = vec3(0.0);
    vec3 totalWeight = vec3(0.0);
    for (int i = 0; i < DISP_SAMPLES; i++) {
        float t = float(i) / float(DISP_SAMPLES);
        vec3 w = sampleWeights(t);
        col += w * texture(tex, fract(uv + disp * t)).rgb;
        totalWeight += w;
    }
    return col / totalWeight;
}

Step 8: IBL Environment Sampling

#define MAX_LOD 7.0     // adjustable: cubemap max mip level
#define DIFFUSE_LOD 6.5 // adjustable: diffuse sampling LOD

vec3 getSpecularLightColor(vec3 N, float roughness) {
    vec3 raw = textureLod(iChannel0, N, roughness * MAX_LOD).rgb;
    return pow(raw, vec3(4.5)) * 6.5; // HDR approximation
}

vec3 getDiffuseLightColor(vec3 N) {
    return textureLod(iChannel0, N, DIFFUSE_LOD).rgb;
}

// BRDF LUT lookup
vec2 brdf = texture(iChannel3, vec2(NdotV, roughness)).rg;
vec3 specular = envColor * (F * brdf.x + brdf.y);

Complete Code Template

iChannel0 bound to a noise texture (e.g., "Gray Noise Medium"), with mipmap enabled.

// === Texture Sampling Comprehensive Demo ===
// iChannel0: noise texture (requires mipmap enabled)

#define TEX_RES 256.0
#define FBM_OCTAVES 6
#define FBM_PERSISTENCE 0.5
#define CLOUD_LAYERS 4
#define CLOUD_SPEED 0.02
#define DOF_MAX_BLUR 5.0
#define DOF_FOCUS_DIST 0.5
#define BLOOM_STRENGTH 0.3
#define BLOOM_LOD 4.0

float noise(vec2 x) {
    vec2 p = floor(x);
    vec2 f = fract(x);
    vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);

    float a = textureLod(iChannel0, (p + vec2(0.0, 0.0)) / TEX_RES, 0.0).x;
    float b = textureLod(iChannel0, (p + vec2(1.0, 0.0)) / TEX_RES, 0.0).x;
    float c = textureLod(iChannel0, (p + vec2(0.0, 1.0)) / TEX_RES, 0.0).x;
    float d = textureLod(iChannel0, (p + vec2(1.0, 1.0)) / TEX_RES, 0.0).x;

    return a + (b - a) * u.x + (c - a) * u.y + (a - b - c + d) * u.x * u.y;
}

float fbm(vec2 x) {
    float v = 0.0;
    float a = 0.5;
    float w = 0.0;
    for (int i = 0; i < FBM_OCTAVES; i++) {
        v += a * noise(x);
        w += a;
        x *= 2.0;
        a *= FBM_PERSISTENCE;
    }
    return v / w;
}

float cloudLayer(vec2 uv, float height, float time) {
    vec2 offset = vec2(time * CLOUD_SPEED * (1.0 + height), 0.0);
    float n = fbm((uv + offset) * (2.0 + height * 3.0));
    return smoothstep(0.4, 0.7, n);
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = fragCoord / iResolution.xy;
    float aspect = iResolution.x / iResolution.y;

    // 1. Procedural sky
    vec3 sky = mix(vec3(0.1, 0.15, 0.4), vec3(0.5, 0.7, 1.0), uv.y);

    // 2. FBM cloud layers
    vec3 col = sky;
    for (int i = 0; i < CLOUD_LAYERS; i++) {
        float h = float(i) / float(CLOUD_LAYERS);
        float density = cloudLayer(vec2(uv.x * aspect, uv.y), h, iTime);
        vec3 cloudCol = mix(vec3(0.8, 0.85, 0.9), vec3(1.0), h);
        col = mix(col, cloudCol, density * (0.3 + 0.7 * h));
    }

    // 3. textureLod depth of field blur
    float dist = abs(uv.y - DOF_FOCUS_DIST);
    float lod = dist * DOF_MAX_BLUR;
    vec3 blurred = textureLod(iChannel0, uv, lod).rgb;
    col = mix(col, blurred * 0.5 + col * 0.5, 0.3);

    // 4. Bloom
    vec3 bloom = textureLod(iChannel0, uv, BLOOM_LOD).rgb;
    bloom += textureLod(iChannel0, uv, BLOOM_LOD + 1.0).rgb;
    bloom += textureLod(iChannel0, uv, BLOOM_LOD + 2.0).rgb;
    bloom /= 3.0;
    col += bloom * BLOOM_STRENGTH;

    // 5. Post-processing
    col = (col * (6.2 * col + 0.5)) / (col * (6.2 * col + 1.7) + 0.06);
    col *= 0.5 + 0.5 * pow(16.0 * uv.x * uv.y * (1.0 - uv.x) * (1.0 - uv.y), 0.2);

    fragColor = vec4(col, 1.0);
}

Common Variants

Variant 1: Anisotropic Flow-Field Blur

#define BLUR_ITERATIONS 32  // adjustable: number of samples along flow field
#define BLUR_STEP 0.008     // adjustable: UV offset per step

vec3 flowBlur(vec2 uv) {
    vec3 col = vec3(0.0);
    float acc = 0.0;
    for (int i = 0; i < BLUR_ITERATIONS; i++) {
        float h = float(i) / float(BLUR_ITERATIONS);
        float w = 4.0 * h * (1.0 - h);
        col += w * texture(iChannel0, uv).rgb;
        acc += w;
        vec2 dir = texture(iChannel1, uv).xy * 2.0 - 1.0;
        uv += BLUR_STEP * dir;
    }
    return col / acc;
}

Variant 2: Buffer-as-Data Storage

const ivec2 txPosition = ivec2(0, 0);
const ivec2 txVelocity = ivec2(1, 0);
const ivec2 txState    = ivec2(2, 0);

vec4 load(ivec2 addr) { return texelFetch(iChannel0, addr, 0); }

void store(ivec2 addr, vec4 val, inout vec4 fragColor, ivec2 fragPos) {
    fragColor = (fragPos == addr) ? val : fragColor;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    ivec2 p = ivec2(fragCoord);
    fragColor = texelFetch(iChannel0, p, 0);
    vec4 pos = load(txPosition);
    vec4 vel = load(txVelocity);
    // ... update logic ...
    store(txPosition, pos + vel * 0.016, fragColor, p);
    store(txVelocity, vel, fragColor, p);
}

Variant 3: Dispersion Effect

#define DISP_SAMPLES 64     // adjustable: sample count
#define DISP_STRENGTH 0.05  // adjustable: dispersion strength

vec3 dispersion(vec2 uv, vec2 displacement) {
    vec3 col = vec3(0.0);
    vec3 w_total = vec3(0.0);
    for (int i = 0; i < DISP_SAMPLES; i++) {
        float t = float(i) / float(DISP_SAMPLES);
        vec3 w = vec3(t * t, 46.666 * pow((1.0 - t) * t, 3.0), (1.0 - t) * (1.0 - t));
        col += w * texture(iChannel0, fract(uv + displacement * t * DISP_STRENGTH)).rgb;
        w_total += w;
    }
    return col / w_total;
}

Variant 4: Triplanar Texture Mapping

#define TRIPLANAR_SHARPNESS 2.0  // adjustable: blend sharpness

vec3 triplanarSample(sampler2D tex, vec3 pos, vec3 normal, float scale) {
    vec3 w = pow(abs(normal), vec3(TRIPLANAR_SHARPNESS));
    w /= (w.x + w.y + w.z);
    vec3 xSample = texture(tex, pos.yz * scale).rgb;
    vec3 ySample = texture(tex, pos.xz * scale).rgb;
    vec3 zSample = texture(tex, pos.xy * scale).rgb;
    return xSample * w.x + ySample * w.y + zSample * w.z;
}

Variant 5: Temporal Reprojection (TAA)

#define TAA_BLEND 0.9  // adjustable: history frame blend ratio

vec3 temporalBlend(vec2 currUv, vec2 prevUv, vec3 currColor) {
    vec3 history = textureLod(iChannel0, prevUv, 0.0).rgb;
    vec3 minCol = currColor - 0.1;
    vec3 maxCol = currColor + 0.1;
    history = clamp(history, minCol, maxCol);
    return mix(currColor, history, TAA_BLEND);
}

Performance & Composition

Performance Tips:

  • Heavy sampling (e.g., 64 dispersion samples) is a bandwidth bottleneck — reduce sample count + use smart weight compensation; use textureLod with high LOD to reduce cache misses
  • 2D Gaussian blur uses separable two-pass (O(N^2) -> O(2N)), leveraging hardware bilinear for (N+1)/2 samples to achieve N-tap
  • Must use textureLod(..., 0.0) inside ray marching — the GPU cannot correctly estimate screen-space derivatives
  • Manual Hermite interpolation is ~4x slower than hardware — only use for the first two FBM octaves, fall back to texture() for higher frequencies
  • Each multi-buffer feedback adds one frame of latency — merge operations into the same pass; use texelFetch to avoid filtering overhead

Composition Tips:

  • + SDF Ray Marching: Noise textures for displacement maps/materials; use textureLod(..., 0.0) inside ray marching
  • + Procedural Noise: Hermite + FBM driving domain warping to generate terrain/clouds/fire; texture noise is faster than pure mathematical noise
  • + Post-Processing Pipeline: Multi-LOD bloom → separable DOF → dispersion → tone mapping, chaining a complete post-processing pipeline
  • + PBR/IBL: textureLod samples cubemap by roughness + BRDF LUT lookup = split-sum IBL
  • + Simulation/Feedback: Multi-buffer reaction-diffusion/fluid; Buffer A state, B/C separable blur diffusion, Image visualization; fract() torus boundary

Further Reading

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