在3D图形渲染中,动态阴影是一个重要的效果,它能够显著提升场景的真实感。然而,动态阴影的计算成本通常较高,尤其是在移动设备上。本文将介绍如何在Android平台上实现基本的阴影映射技术,以及如何通过调整参数来优化性能。
阴影映射是一种用于动态阴影的技术。在许多情况下,这种技术计算成本过高,尤其是在移动电话上,因此了解其在简单情况下的表现是非常有用的。本文将展示基本的阴影映射和PCF(百分比接近过滤)技术,并允许调整阴影贴图大小和偏移类型,以便在Android上查看它们的性能。
简单阴影映射算法速度较快,但每个像素有两个输出(阴影/无阴影),因此边缘通常会出现锯齿。PCF通过计算周围像素的平均值来实现平滑的阴影,但这种方法往往速度较慢,无法实时生成阴影。
阴影映射的基础是首先以光源作为摄像机来渲染场景。为此,需要创建两个视图矩阵和两个投影矩阵,一个用于光源,一个用于摄像机。在第一步中,将光源的MVP矩阵传递给着色器。
从这一步中,只需要物体与光源的距离,这被称为阴影贴图。为了稍后使用,将这些深度值存储在纹理中。在某些Android设备上,不能直接将深度值渲染到纹理中(没有OES_depth_texture OpenGL扩展的GPU),因此必须将深度值打包到RGBA分量中,然后再解包它们。
String extensions = GLES20.glGetString(GLES20.GL_EXTENSIONS);
if (extensions.contains("OES_depth_texture")) {
mHasDepthTextureExtension = true;
}
用于渲染阴影贴图的着色器:
深度纹理着色器 (depth_tex_)v_depth_map.glsl
阴影贴图着色器 (depth_tex_)f_shadow_map.glsl
如果设备支持扩展,那么只有更简单的着色器会运行,不需要打包和解包,因此可以从检查这些着色器开始,因为它们更容易理解。
// Pixel shader to generate the Depth Map
// Used for shadow mapping - generates depth map from the light's viewpoint
precision highp float;
varying vec4 vPosition;
vec4 pack(float depth) {
const vec4 bitSh = vec4(256.0 * 256.0 * 256.0, 256.0 * 256.0, 256.0, 1.0);
const vec4 bitMsk = vec4(0.0, 1.0 / 256.0, 1.0 / 256.0, 1.0 / 256.0);
vec4 comp = fract(depth * bitSh);
comp -= comp.xxyz * bitMsk;
return comp;
}
void main() {
float normalizedDistance = vPosition.z / vPosition.w;
normalizedDistance = (normalizedDistance + 1.0) / 2.0;
gl_FragColor = pack(normalizedDistance);
}
有了深度图之后,可以使用这些信息来决定一个像素是否处于阴影中。为此,为每个片段计算:
//1.0 = not in shadow (fragmant is closer to light than the value stored in shadow map)
//0.0 = in shadow
return float(distanceFromLight > shadowMapPosition.z);
在演示应用程序中,可以在选项菜单中更改阴影类型和阴影算法的偏移类型。本可以将所有算法放在一个着色器中,通过uniform传递并使用if条件决定使用哪个算法。这种方法的问题在于,由于GPU的并行计算,两种条件的情况都会被评估,导致性能下降,使得速度比较变得不可能。另一种解决方案是使用#ifdef并编译带有不同#define语句的着色器。
消除阴影瑕疵的常见解决方案是在与光源的距离值比较之前添加一个小的误差范围。
float bias = 0.005;
return float(distanceFromLight + bias > shadowMapPosition.z);
float calcBias() {
float bias;
vec3 n = normalize(vNormal);
vec3 l = normalize(uLightPos);
float cosTheta = clamp(dot(n, l), 0.0, 1.0);
bias = 0.0001 * tan(acos(cosTheta));
bias = clamp(bias, 0.0, 0.01);
return bias;
}
可以在菜单中更改阴影贴图大小:
更大的阴影贴图纹理可以带来更好的阴影边缘,但在某个点之后,它不会带来显著更好的结果,因此不值得做得比屏幕分辨率大得多(特别是因为它会使算法变慢)。
PCF算法基于在当前片段位置周围多次采样深度图。这意味着如果使用4x4的窗口大小,阴影值可能有16个不同的值。这会产生柔和的阴影和较少的锯齿边缘。这种方法的问题是,将有16倍的深度图查找和16倍的比较,这也可以从FPS结果的下降中看出。
float shadow = 1.0;
if (diffuseComponent < 0.01) {
shadow = 1.0;
} else {
if (vShadowCoord.w > 0.0) {
shadow = shadowSimple();
}
}
gl_FragColor = (vColor * (diffuseComponent + ambientComponent * shadow));