简体中文
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);
}