Android OpenGL ES 2 的阴影映射
简单和 PCF 阴影映射算法(奖励文章 - Android 通配符类别)。
引言
本教程的源代码可以在这里找到:https://github.com/pogi-b/OpenGLShadowDemo
您可以从这里下载应用程序本身 (apk):https://github.com/pogi-b/OpenGLShadowDemo/releases
阴影映射是动态阴影的解决方案。很多时候,它们的计算成本太高,尤其是在手机上,所以了解它们在简单情况下的表现可能很有用。
在本教程中,我将展示基本的阴影映射和 PCF (百分比关闭过滤) 技术,并提供可调节的阴影贴图尺寸和偏置类型,以便您了解它们在 Android 上的表现。简单的算法速度更快,但每个像素有两个输出(阴影/无阴影),因此边缘通常会有锯齿。PCF 可以实现平滑的阴影,因为它将阴影值计算为周围像素的平均值,但很多时候它对于实时阴影来说太慢了。
简单的阴影映射 |
PCF 阴影映射 |
阴影映射算法有更多可能的变体,请随时尝试并随意组合它们。您可以在此处找到一些优秀的教程,这些教程也是我在此演示应用程序中使用的来源。
- OpenGL 教程:http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-16-shadow-mapping/
- Fabian Sanglard 的网站:http://fabiensanglard.net/ (阴影映射、PCF、VSM)
也要感谢 Shayan Javed - Android 上 OpenGL ES 2.0 着色器的入门指南。
背景
在本教程中,我不会涵盖 OpenGL、OpenGL ES 2.0 和 Android 开发的基础知识。但是,您可以在 Learn OpenGL ES 教程 和这个 关于阴影映射的 OpenGL 教程 中找到所有这些背景知识。
渲染阴影贴图
阴影映射的基本原理是,我们首先像光源是相机一样渲染场景。为此,我们创建两个视图矩阵和两个投影矩阵,一个用于光源,一个用于相机。在第一步中,我们将光源 MVP 矩阵传递给着色器。
从这一步开始,我们只需要物体到光源的距离,这就是所谓的阴影贴图。为了以后使用它,我们将这些深度值存储在纹理中。在某些 Android 设备上,无法直接将深度值渲染到纹理(没有 OES_depth_texture OpenGL 扩展的 GPU),因此我们必须将深度值打包到 RGBA 分量中,然后稍后解包。要决定使用哪种方法
// Test OES_depth_texture extension
String extensions = GLES20.glGetString(GLES20.GL_EXTENSIONS);
if (extensions.contains("OES_depth_texture"))
mHasDepthTextureExtension = true;
使用或不使用 OES_depth_texture,顶点和片段着色器也不同。一组以“depth_tex_”为前缀,另一组则没有。为了方便切换着色器程序,我使用了一个单独的类 (RenderProgram.java) 来编译、链接和存储 OpenGL 程序句柄(基于此解决方案)。
用于渲染阴影贴图的着色器
- (depth_tex_)v_depth_map.glsl
- (depth_tex_)f_shadow_map.glsl
为了清楚起见:如果您的设备支持该扩展,只有更简单的着色器会在没有打包和解包的情况下运行,因此您可以从检查这些着色器开始,因为它们更容易理解。
这里唯一不直观的着色器是需要打包到 RGBA 的片段着色器。
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;
// from Fabien Sangalard's DEngine
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,
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() {
// the depth
float normalizedDistance = vPosition.z / vPosition.w;
// scale -1.0;1.0 to 0.0;1.0
normalizedDistance = (normalizedDistance + 1.0) / 2.0;
// pack value into 32-bit RGBA texture
gl_FragColor = pack(normalizedDistance);
}
这里发生的事情是将深度值(Z 坐标)编码为 4 个分量。如果您想了解数学解释,可以在源代码中找到:https://github.com/fabiensanglard/dEngine/blob/master/data/shaders/f_shadowMapGenerator.glsl
渲染场景
在获得深度贴图后,我们可以利用这些信息来决定一个像素是否处于阴影中。为了计算这个,我们为每个片段计算
- 从光源角度看其坐标(我们需要 lightMVP 将其作为 uniform 传递给着色器)
vShadowCoord = uShadowProjMatrix * aPosition;
- 属于该点的深度贴图上的深度值是多少
vec4 shadowMapPosition = vShadowCoord / vShadowCoord.w; float distanceFromLight = texture2D(uShadowTexture, shadowMapPosition.st).z;
- 片段是否比深度图上的值离光源更远?如果是,则片段处于阴影中。
//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);
不同设置下的阴影
在演示应用程序中,您可以在选项菜单中更改阴影算法的阴影类型和偏置类型。我可以将所有算法放在一个着色器中,通过 uniforms 传递并在 if 条件下决定使用哪种算法。这种方法的缺点是,由于 GPU 的并行计算,条件的两种情况都会被评估,导致性能低下,无法比较速度。另一种解决方案是使用 #ifdef 并使用不同的 #define 语句编译着色器。
固定/动态偏置
消除阴影痤疮的常见解决方案是在将深度值与片段到光源的距离进行比较之前,为其添加一个小的误差裕度。
//add bias to reduce shadow acne (error margin)
float bias = 0.005;
//1.0 = not in shadow (fragmant is closer to light than the value stored in shadow map)
//0.0 = in shadow
return float(distanceFromLight + bias > shadowMapPosition.z);
添加固定偏置后,可以看到阴影痤疮消失了,但出现了另一个问题,称为“Peter Panning”,因为地面上的物体看起来像在飞行。
您会注意到,当表面与光源的角度较小时,阴影痤疮更有可能出现。这导致了另一种解决方案,即根据表面的法线向量调整偏置。
//Calculate variable bias - from http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-16-shadow-mapping
float calcBias()
{
float bias;
vec3 n = normalize( vNormal );
// Direction of the light (from the fragment to the light)
vec3 l = normalize( uLightPos );
// Cosine of the angle between the normal and the light direction,
// clamped above 0
// - light is at the vertical of the triangle -> 1
// - light is perpendiular to the triangle -> 0
// - light is behind the triangle -> 0
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;
}
无偏置 / 固定偏置 (0.005) / 动态偏置
阴影贴图尺寸
您可以在菜单中更改阴影贴图尺寸
- 0.5 displayWidth x 0.5 displayHeight
- 1.0 displayWidth x 1.0 displayHeight
- 1.5 displayWidth x 1.5 displayHeight
- 2.0 displayWidth x 2.0 displayHeight
更大的阴影贴图纹理可以改善阴影边缘,但在某个点之后不会产生显著更好的结果,因此将其做得比屏幕分辨率大很多并不值得(特别是它会使算法变慢)。
简单的阴影映射 / PCF 阴影映射
PCF 算法基于对当前片段位置周围的深度贴图进行多次采样。这意味着如果我们使用 4x4 的窗口,阴影的值可以有 16 种不同的值。这会产生柔和的阴影和更少的锯齿边缘。这种方法的缺点是,我们将不得不查找深度贴图 16 次,进行 16 次比较,您也可以从 FPS 下降的结果中识别这一点。
![]() |
![]() |
用漫射光覆盖阴影痤疮
许多文章都写过如何通过添加偏置来解决阴影痤疮。我使用了一个基于漫射光组件的解决方案:如果顶点不朝向光源(从光源的角度看是 gl_BackFace),我只在片段着色器中跳过阴影计算。
// Shadow
float shadow = 1.0;
// If fragment doesn't face light source
if (diffuseComponent < 0.01)
{
shadow = 1.0;
}
else
{
//if the fragment is not behind light view frustum
if (vShadowCoord.w > 0.0) {
shadow = shadowSimple();
//scale 0.0-1.0 to 0.2-1.0
//otherways everything in shadow would be black
//shadow = (shadow * 0.8) + 0.2;
}
}
// Final output color with shadow and lighting
gl_FragColor = (vColor * (diffuseComponent + ambientComponent * shadow));
您可以在这里看到使用无偏置的效果。
感谢阅读!
请发送您的反馈或评论。