著色器示例:視差貼圖
互動演示
片段著色器
編輯著色器後按 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);
}