繁體中文
English 简体中文 繁體中文
Methodox 首頁

著色器示例:視差貼圖

互動演示

片段著色器

編輯著色器後按 Run(執行,或按 Ctrl+Enter)即可重新編譯。演示下方的原始碼就是互動演示正在執行的同一份文字。

從背面觀察表面

攝像機會繞場景轉滿一整圈,因此每轉一圈,有一半時間它位於平面的背面。這種情況需要謹慎處理,而且處理錯了很容易被誤認為是一種特性:本示例的早期版本從背面看時,會呈現出一種模糊、玻璃質感、幾乎像折射的表面。它看起來很有意思,但完全是錯的。

當觀察者轉到另一側時,有兩處會出問題。

視差偏移取決於視線向量法線分量的符號。 從正面向左傾時看起來向左移動的凸起,從背面向左傾時必須向右移動。偏移量要除以這個分量,因此用 max(z, 0.05) 對它進行鉗制——這是防止掠射角下除數趨近於零的常見做法——會丟掉符號,並代之以一個很小的正數。結果,每個背向像素得到的偏移方向都是反的,而且大小最多可達正確值的二十倍,幾乎是在整個表面上隨機取樣高度場。這種錯亂的尋找正是玻璃質感的來源。應當鉗制絕對值,並保留符號。

著色法線的朝向是反的。 normalFromHeight 根據高度梯度構建法線,因此法線總是位於 +z 一側。從背面看時,可見的面是同一浮雕的背面,其法線應取反。如果不把它翻轉過來,光照表現的是一回事,輪廓表現的又是另一回事。

應根據攝像機所在的一側來做這個判斷,而不是根據擾動後的法線。針對擾動後的法線使用 faceforward,還會把近側個別陡峭的凸起也翻轉過來,從而悄悄改變正面的檢視——修好了背面卻損壞了正面,就不算修復。

只要攝像機始終停留在一側,這兩個問題都不會顯現。正因如此,這類演示需要讓攝像機繞完整整一圈,而不是隻走一段舒適的弧線。

程式碼片段

Shadertoy

// Shadertoy: parallax mapping demo without textures
// Paste into https://www.shadertoy.com/ as the Image shader.

float heightMap(vec2 uv)
{
    // Procedural height field
    float h = 0.0;
    h += 0.50 * sin(uv.x * 10.0) * sin(uv.y * 10.0);
    h += 0.25 * sin(uv.x * 20.0 + 1.0) * sin(uv.y * 20.0 + 0.7);
    return h * 0.5 + 0.5;
}

vec3 normalFromHeight(vec2 uv)
{
    float e = 0.002;
    float h  = heightMap(uv);
    float hx = heightMap(uv + vec2(e, 0.0));
    float hy = heightMap(uv + vec2(0.0, e));

    vec3 dpdx = vec3(e, 0.0, hx - h);
    vec3 dpdy = vec3(0.0, e, hy - h);

    return normalize(cross(dpdx, dpdy));
}

vec2 parallaxOffset(vec2 uv, vec3 viewDirTS)
{
    float heightScale = 0.08;

    // Basic parallax mapping.
    //
    // viewDirTS.z is negative when the surface is seen from behind, and the
    // whole effect depends on its sign: a bump that shifts one way as you lean
    // left from the front must shift the other way from the back. Clamping it
    // with max(z, 0.05) threw that sign away and, worse, replaced it with a
    // near-zero divisor, so every back-facing pixel got an offset up to twenty
    // times too large pointing the wrong way. The height field was then sampled
    // almost at random across the surface, which is what produced the smeared,
    // glassy look - it was not refraction, it was a lookup running wild.
    //
    // Clamp the magnitude to keep the divisor away from zero at grazing angles;
    // keep the sign.
    float vz = viewDirTS.z;
    vz = (abs(vz) < 0.05) ? (vz < 0.0 ? -0.05 : 0.05) : vz;

    float h = heightMap(uv);
    return uv - viewDirTS.xy / vz * ((h - 0.5) * heightScale);
}

vec3 albedo(vec2 uv)
{
    // Checker + gradient for visibility
    vec2 g = floor(uv * 8.0);
    float checker = mod(g.x + g.y, 2.0);
    vec3 a = mix(vec3(0.15, 0.18, 0.22), vec3(0.75, 0.80, 0.90), checker);
    a *= 0.7 + 0.3 * heightMap(uv);
    return a;
}

mat3 camera(vec3 ro, vec3 ta)
{
    vec3 f = normalize(ta - ro);
    vec3 r = normalize(cross(vec3(0.0, 1.0, 0.0), f));
    vec3 u = cross(f, r);
    return mat3(r, u, f);
}

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

    // Camera
    float t = iTime * 0.8;
    vec3 ro = vec3(1.8 * cos(t), 0.9, 1.8 * sin(t));
    vec3 ta = vec3(0.0, 0.0, 0.0);
    mat3 cam = camera(ro, ta);

    vec3 rd = normalize(cam * vec3(p, 1.8));

    // Intersect plane z=0 in tangent/object space
    // Plane basis: x,y = tangent plane, z = height direction
    float planeZ = 0.0;
    float tt = (planeZ - ro.z) / rd.z;

    vec3 col = vec3(0.02, 0.03, 0.05);

    if (tt > 0.0)
    {
        vec3 hit = ro + rd * tt;
        vec2 uv = hit.xy;

        // Tile UVs
        uv *= 1.2;

        // View dir in tangent space for a flat XY plane
        vec3 viewDirTS = normalize(-rd);

        // Apply parallax
        vec2 puv = parallaxOffset(uv, viewDirTS);

        // Normal + lighting from displaced UV.
        //
        // normalFromHeight always returns a normal on the +z side, because it is
        // built from the height gradient. Seen from behind, the face actually
        // visible is the back of the same relief, so the normal has to be turned
        // to meet the viewer - otherwise the surface is lit as though the camera
        // were still in front of it and the shading contradicts the silhouette.
        // Which side of the plane the camera is on decides which face is visible.
        // Deciding it from the perturbed normal instead - with faceforward - also
        // flips individual steep bumps on the near side, which changes the front
        // view for no reason. The plane's geometry is what is two-sided here, not
        // each bump.
        float side = (ro.z >= 0.0) ? 1.0 : -1.0;
        vec3 n = normalFromHeight(puv) * side;
        vec3 l = normalize(vec3(0.6, 0.7, 0.5));
        vec3 v = normalize(-rd);
        vec3 h = normalize(l + v);

        float diff = max(dot(n, l), 0.0);
        float spec = pow(max(dot(n, h), 0.0), 48.0);

        vec3 base = albedo(puv);
        col = base * (0.15 + 0.85 * diff) + 0.35 * spec;

        // Slight vignette / fade
        col *= exp(-0.08 * tt);
    }

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