着色器示例:视差贴图
交互演示
片段着色器
编辑着色器后按 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);
}