Ogre中的动态阴影技术详解
1. 动态阴影简介
在3D场景渲染中,阴影是重要的空间线索,对场景的感知真实度和用户满意度有极大贡献。不过,3D图形的实时阴影计算即便有强大的硬件加速,仍然成本高昂。目前,Ogre支持两种主要的动态阴影生成类型:模板阴影和纹理阴影,每种类型又有调制和加法两种变体。
1.1 阴影技术概述
在使用Ogre的阴影技术前,需要了解以下几点:
- 场景中只能使用一种阴影技术,且应在渲染场景前设置(理想情况是创建场景管理器实例时)。
- 通过调用
SceneManager::setShadowTechnique()
设置阴影技术,可选参数有
SHADOWTYPE_STENCIL_ADDITIVE
、
SHADOWTYPE_STENCIL_MODULATIVE
、
SHADOWTYPE_TEXTURE_ADDITIVE
或
SHADOWTYPE_TEXTURE_MODULATIVE
。
- 阴影默认是禁用的。
- 对象的阴影投射和接收由其材质控制,也可在对象本身进行控制。默认情况下,启用阴影后所有对象都会投射阴影,半透明或透明对象默认不投射阴影,但可设置半透明材质投射阴影。
- 由于模板阴影算法的特性,半透明/透明对象要么投射完全实心的模板阴影,要么不投射阴影,除非使用纹理阴影(纹理阴影默认处理透明度)。
- 灯光可以设置为不投射阴影,不同灯光类型和阴影技术的兼容性如下表所示:
| 阴影技术 | 点光源 | 聚光灯 | 平行光 |
| ---- | ---- | ---- | ---- |
| 加法模板 | 是 | 是 | 是 |
| 调制模板 | 是 | 是 | 是 |
| 加法纹理 | 是
| 是 | 是 |
| 调制纹理 | 是
| 是 | 是 |
注:* 纹理阴影对点光源的支持通过聚光灯近似实现,使用纹理阴影时应避免将对象靠近点光源(也不要将点光源包围在对象内)。
2. 模板阴影
2.1 原理
模板阴影的概念很简单,场景中投射阴影的对象所产生的阴影形状由该对象从给定灯光视角的轮廓定义。Ogre提供了一个方便的调试功能,启用后可绘制阴影体积。
2.2 阴影体积
阴影体积是通过轮廓边缘挤出形成的空间体积,一端由模型界定,另一端根据以下规则界定:
- 如果有可编程图形硬件,使用顶点程序将阴影体积无限投射。
- 如果没有顶点程序,阴影体积的远端由灯光衰减设置(点光源和聚光灯)或
SceneManager::setShadowDirectionalLightExtrusionDistance()
方法界定。
注意 :如果没有顶点程序可用,Ogre必须使用有限的阴影体积,应避免让对象离光源过近,否则可能导致较宽的阴影体积无法正确对其内部的对象进行阴影处理。
阴影体积用于在屏幕空间创建模板,模板内的对象像素绘制为阴影,模板外的像素不绘制阴影。由于模板阴影体积的特性,模板阴影的边缘非常清晰,像素要么在阴影中,要么不在阴影中,没有中间状态。这使得模板阴影可以远距离投射而不失真,但也意味着阴影中通常会看到实际多边形的边缘(尤其是长阴影)。Ogre没有“使阴影变柔和”的开关,实际上很难使模板阴影变柔和,而纹理阴影的特性允许对其进行柔化处理。
2.3 模板阴影与GPU程序
模板阴影在可用时会利用可编程顶点GPU处理进行无限阴影体积挤出,这种行为不会影响你编写的任何GPU顶点程序,因为挤出仅在自动阴影生成阶段进行(此时你的程序不会被使用)。然而,由于模板体积生成是仅CPU的过程,无法根据GPU顶点程序中发生的几何变形简单调整该体积。对于硬件蒙皮,Ogre会在软件中重复顶点蒙皮(这很遗憾但不可避免)。对于顶点程序中实现的任意算法,Ogre无法知道变形后的几何形状是什么样的。因此,如果使用任何其他几何变形顶点程序,使用模板阴影时阴影可能无法准确反映几何形状的实际轮廓,这可能是选择使用纹理阴影的一个原因。
2.4 模板阴影优化
Ogre在渲染模板阴影时会进行一些通用优化:
- 可用时使用顶点程序加速阴影体积挤出。
- 能检测哪些灯光可能影响视锥体(基于方向和范围),避免构建不需要的阴影几何。
- 如果点光源或聚光灯没有覆盖整个视口,会使用剪刀测试以节省填充率。
- 支持双侧模板和模板环绕扩展,可防止不必要的图元设置和驱动程序开销。
- 当不需要Z-Fail算法时,使用成本较低的Z-Pass算法(Z-Fail算法在相机穿过阴影体积时最有用)。
不过,使用模板阴影时也需要注意以下问题:
- 阴影体积不受遮挡几何的影响,可能会看到本应被遮挡对象后面的阴影。
- 屏幕外的阴影投射器可以将阴影投射到视锥体中,如果这些投射器很远(如黎明或黄昏时投射阴影的对象),即使它们不在场景的可视范围内,也必须渲染产生这些阴影的几何。
- 如果目标硬件没有能够运行顶点程序的图形加速器,应避免让对象靠近或穿过光源。
3. 纹理阴影
3.1 原理
纹理阴影的计算方式与模板阴影完全不同,它通过从灯光的视角将场景渲染到纹理,然后在正常屏幕缓冲区渲染时将该纹理投射到场景中的阴影接收对象上。Ogre中模板阴影和纹理阴影的主要区别之一在于阴影投射器和接收对象的使用方式。固定功能纹理阴影投射器不能自阴影,对象要么是投射器,要么是接收对象,但不能同时是两者。如果一个对象不是阴影投射器,那么它就是阴影接收对象(除非在对象的材质中禁用了
receive_shadows
选项)。Ogre目前不支持一种名为深度阴影映射的替代纹理阴影技术,该技术可实现纹理阴影的自阴影。
3.2 优缺点
纹理阴影通常是模板阴影体积的更快替代方案,但主要代价是保真度。由于阴影渲染到纹理,它们具有固定的分辨率,对广泛的投影拉伸反应不佳,应用的阴影中容易出现锯齿边缘,虽然可以进行一定程度的过滤,但无法完全消除。不过,纹理阴影计算速度快,并且可以完全在硬件中计算,在许多情况下,性能的提升和将整个阴影处理过程卸载到GPU的能力可以弥补保真度的下降。
3.3 限制
性能提升的同时也存在一些限制。理论上可以有任意数量的纹理阴影,但纹理内存大小和填充率限制了一帧中阴影纹理的实际上限。Ogre允许使用
SceneManager::setShadowTextureCount()
方法管理一帧中活动的阴影纹理数量上限。由于Ogre在一帧中每个灯光使用一个纹理,它将按先到先得的原则使用这些纹理,实际上是将纹理分配给最近的n个灯光(n是
SceneManager::setShadowTextureCount()
中提供的纹理数量),帧中多余的灯光将被忽略。
3.4 优化调整
可以通过以下方法调整纹理阴影的视觉质量和美学效果:
- 如果无法增加纹理大小来改善拉长的纹理阴影的视觉质量,可以减小远投影距离,使阴影更靠近光源终止。缺点是阴影可能不太真实,但阴影质量可以得到很大改善。
- 可以相对于相机“移动”阴影。默认情况下,Ogre将阴影放置为使60%的阴影在相机视线“前方”。可以使用
SceneManager::setShadowDirLightTextureOffset
调整此值,但要注意如果设置过高,可能会无意中以极端角度渲染纹理边缘。
- 阴影不会突然结束,Ogre会对纹理阴影的边缘进行淡化处理,以防止从阴影到无阴影的突然变化。可以改变淡化开始和停止的半径,默认值分别为0.7和0.9(归一化)。由于径向灯光和方形纹理的“方枘圆凿”问题,应避免将淡化停止距离设置为1.0,否则纹理阴影将始终是圆形或椭圆形。
3.5 纹理阴影与GPU程序
纹理阴影在GPU上计算,因此在处理GPU变形几何的阴影时不会像模板阴影那样存在问题。但要使变形几何正确投射阴影,需要额外的步骤。Ogre可以很好地处理完全固定功能的阴影生成,但当在阴影投射器和/或接收对象的材质中涉及GPU顶点程序时,需要一些帮助。
Ogre通过
shadow_caster_vertex_program_ref
材质指令在阴影投射阶段复制几何变形。例如:
shadow_caster_vertex_program_ref skinningOneWeight_ShadowCaster {
param_named_auto world_view_proj_3x4 worldviewproj_matrix
param_named_auto ambientColor ambient_light_colour
}
以下是单权重硬件蒙皮程序的代码:
void hardwareSkinningOneWeight_vp(
float4 position : POSITION,
float3 normal : NORMAL,
float2 uv : TEXCOORD0,
float blendIdx : BLENDINDICES,
out float4 oPosition : POSITION,
out float2 oUv : TEXCOORD0,
out float4 colour : COLOR,
// Support up to 24 bones of float3x4
// vs_1_1 only supports 96 params so more than this is not feasible
uniform float3x4 worldMatrix3x4Array[24],
uniform float4x4 viewProjectionMatrix,
uniform float4 lightPos[2],
uniform float4 lightDiffuseColour[2],
uniform float4 ambient)
{
// transform by indexed matrix
float4 blendPos = float4(mul(worldMatrix3x4Array[blendIdx], position).xyz, 1.0);
// view / projection
oPosition = mul(viewProjectionMatrix, blendPos);
// transform normal
float3 norm = mul((float3x3)worldMatrix3x4Array[blendIdx], normal);
// Lighting - support point and directional
float3 lightDir0 = normalize(
lightPos[0].xyz - (blendPos.xyz * lightPos[0].w));
float3 lightDir1 = normalize(
lightPos[1].xyz - (blendPos.xyz * lightPos[1].w));
oUv = uv;
colour = ambient +
(saturate(dot(lightDir0, norm)) * lightDiffuseColour[0]) +
(saturate(dot(lightDir1, norm)) * lightDiffuseColour[1]);
}
/*
Single-weight-per-vertex hardware skinning, shadow-caster pass
*/
void hardwareSkinningOneWeightCaster_vp(
float4 position : POSITION,
float3 normal : NORMAL,
float blendIdx : BLENDINDICES,
out float4 oPosition : POSITION,
out float4 colour : COLOR,
// Support up to 24 bones of float3x4
// vs_1_1 only supports 96 params so more than this is not feasible
uniform float3x4 worldMatrix3x4Array[24],
uniform float4x4 viewProjectionMatrix,
uniform float4 ambient)
{
// transform by indexed matrix
float4 blendPos = float4(mul(worldMatrix3x4Array[blendIdx], position).xyz, 1.0);
// view / projection
oPosition = mul(viewProjectionMatrix, blendPos);
colour = ambient;
}
在上述代码中,注意程序签名(参数列表)是相同的,Ogre在渲染阴影纹理时会将标准程序替换为标记为阴影投射阶段的程序。阴影投射阶段只计算顶点位置并将环境颜色值传递给光栅化器,这使得阴影生成阶段能够准确反映顶点位置。环境颜色值是必需的,因为调制阴影技术需要一种颜色来绘制阴影。
如果阴影接收对象使用顶点程序,也需要特别注意。
shadow_receiver_vertex_program_ref
指令指定一个替代的阴影接收程序:
shadow_receiver_vertex_program_ref skinningOneWeight_ShadowReceiver {
param_named_auto world_view_proj_3x4 worldviewproj_matrix
param_named_auto texProjMatrix texture_viewproj_matrix
}
这个程序应该:
- 接受一个
texture_viewproj_matrix
自动参数,以便正确转换顶点纹理坐标。
- 正常变形顶点。
- 将转换后的纹理坐标放入纹理坐标集0和1(因为某些技术使用两个纹理单元)。
- 对于调制阴影,将输出颜色渲染为白色,以免改变阴影的颜色。
加法纹理阴影需要更进一步:由于阴影阶段渲染实际上是光照渲染,如果在对象上使用片段程序(例如,模拟对象表面扰动的视差/偏移映射逐像素片段程序),还需要一个特殊的阴影接收片段程序来模拟阴影中的这些扰动,该程序通过
shadow_receiver_fragment_program_ref
指令指定:
shadow_receiver_fragment_program_ref fragmentProgram_ShadowReceiver {
param_named_auto diffuse light_diffuse_color 0
}
投射的阴影坐标将来自接收顶点程序,阴影纹理将始终出现在纹理单元0(将任何其他纹理向上推一个)。
4. 调制阴影混合
调制阴影混合理论上很简单,在正常渲染场景中处于阴影中的所有颜色都乘以(调制)阴影颜色,以创建变暗的阴影颜色,这相当于从已渲染的场景中“减去”光线。但在实践中会出现一些问题,使用调制阴影时,阴影区域会均匀变暗,而不考虑该区域可能接收到的光线量,这是一种不准确的阴影技术,因为它没有考虑点光源或聚光灯照明固有的衰减(阴影接收表面无论离光源多远都会以相同的方式变暗),但与更准确的加法阴影遮罩相比,成本较低,能提供可接受的结果。此外,当多个灯光在同一区域投射阴影时,阴影区域的乘法效果可能导致阴影过暗,因此不建议对多个灯光使用调制阴影。不过,调制阴影与静态阴影映射(如离线计算的环境光遮蔽或预计算辐射传输)结合得很好。
5. 加法阴影遮罩
加法阴影技术与调制阴影不同,它“累积”多个阴影阶段的结果,每个阶段从单个灯光的视角进行渲染。由于每个灯光不会影响它看不到的阴影区域,因此可以在阴影区域累积来自其他灯光的光线,从而产生更“正确”的阴影,这是调制技术无法实现的。两种技术的主要区别在于,调制阴影仅影响处于阴影中的区域,而加法阴影仅影响被照亮(非阴影)的区域,这是一种更自然的光照技术。
5.1 成本
这种增强的视觉质量伴随着两个成本。首先是逻辑上的,加法阴影技术不能与调制“烘焙”光照阴影映射(如《雷神之锤3》等游戏中使用的技术)结合使用,因为无法去除使用调制技术生成的地图中已存在的光线,必须将加法作为静态地图和动态阴影的唯一阴影技术,当然也可以实现一些非常逼真的仅动态光照解决方案。其次是计算成本,进行加法阴影计算所需的阶段数为n + 2,其中n是场景中的灯光数量。Ogre将渲染阶段(无论是否为可编程或固定功能)分为三个阶段:
-
环境光阶段
:即使场景中没有未被照亮的对象,Ogre也会运行一个仅环境光的阶段,以设置深度缓冲区并将任何环境光和自发光颜色应用到场景中,此阶段不渲染纹理。
-
漫反射和镜面反射阶段
:此阶段每个灯光渲染一次,来自该灯光的阴影区域不受影响(被“遮罩”),未被遮罩的区域与场景的其余部分进行混合(
scene_blend add
),此阶段也不使用贴花纹理。
-
贴花阶段
:此阶段应用任何贴花纹理(
scene_blend modulate
),与前一阶段累积的颜色进行混合。
5.2 加法阴影与GPU程序
Ogre可以很好地为固定功能材质自行进行阶段分割,但当涉及可编程阶段时,需要一些帮助。实际上,需要为Ogre手动分割可编程加法阴影材质,并以特定方式标记它们,以便它知道如何对每个阶段进行分类。以下是一个材质脚本示例:
// Any number of lights, diffuse
material Examples/BumpMapping/MultiLight
{
// This is the preferred technique which uses both vertex and
// fragment programs, supports coloured lights
technique
{
// Base ambient pass
pass
{
// base colours, not needed for rendering, but as information
// to lighting pass categorisation routine
ambient 1 1 1
diffuse 0 0 0
specular 0 0 0 0
// Really basic vertex program
// NB we don't use fixed function here because GL does not like
// mixing fixed function and vertex programs, depth fighting can
// be an issue
vertex_program_ref Ogre/BasicVertexPrograms/AmbientOneTexture
{
param_named_auto worldViewProj worldviewproj_matrix
param_named_auto ambient ambient_light_colour
}
}
// Now do the lighting pass
// NB we don't do decal texture here because this is repeated per light
pass
{
// base colours, not needed for rendering, but as information
// to lighting pass categorisation routine
ambient 0 0 0
// do this for each light
iteration once_per_light
scene_blend add
// Vertex program reference
vertex_program_ref Examples/BumpMapVP
{
param_named_auto lightPosition light_position_object_space 0
param_named_auto worldViewProj worldviewproj_matrix
}
// Fragment program
fragment_program_ref Examples/BumpMapFP
{
param_named_auto lightDiffuse light_diffuse_colour 0
}
// Base bump map
texture_unit
{
texture NMBumpsOut.png
colour_op replace
}
// Normalisation cube map
texture_unit
{
cubic_texture nm.png combinedUVW
tex_coord_set 1
tex_address_mode clamp
}
}
// Decal pass
pass
{
// base colours, not needed for rendering, but as information
// to lighting pass categorisation routine
lighting off
// Really basic vertex program
// NB we don't use fixed function here because GL does not like
// mixing fixed function and vertex programs, depth fighting can
// be an issue
vertex_program_ref Ogre/BasicVertexPrograms/AmbientOneTexture
{
param_named_auto worldViewProj worldviewproj_matrix
param_named ambient float4 1 1 1 1
}
scene_blend dest_colour zero
texture_unit
{
texture RustedMetal.jpg
}
}
}
}
在上述材质脚本中,通过将漫反射和镜面反射颜色设置为黑色来标记仅环境光阶段,用于设置深度缓冲区。每个灯光的迭代阶段通过其黑色环境光颜色和
once_per_light
指令标记,该指令还告诉Ogre该阶段中的片段程序应针对每个灯光执行,如果没有该指令,Ogre会认为片段程序在执行贴花工作,从而不会为每个灯光运行该片段程序。最后,在贴花阶段,关闭光照以标记该阶段,并将纹理与场景的其余部分进行调制。
6. 总结
3D图形中的阴影实际上是一个由各种技巧和错觉组成的系统,最终能产生非常令人满意的结果。但这些技巧和错觉需要你了解大量的提示、技巧和注意事项,以平衡应用程序对逼真阴影的需求和可接受的帧率。掌握上述知识后,你应该能够更好地处理3D场景中的阴影问题,根据具体需求选择合适的阴影技术,并进行相应的优化和调整。
6.1 不同阴影技术的对比总结
为了更清晰地了解各种阴影技术的特点,我们将它们的关键特性进行对比,如下表所示:
| 阴影技术 | 边缘效果 | 保真度 | 性能 | 与GPU变形几何兼容性 | 多光源处理 | 与静态阴影映射结合 |
| ---- | ---- | ---- | ---- | ---- | ---- | ---- |
| 模板阴影 | 边缘清晰,无中间状态 | 高,远距离不失真 | 相对较低,CPU计算压力大 | 差,难以处理GPU变形几何 | 调制阴影多光源易过暗 | 调制阴影可结合 |
| 纹理阴影 | 可能有锯齿,可过滤 | 低,拉伸时效果差 | 高,可硬件计算 | 好,需额外设置 | 加法阴影更自然 | 加法阴影无法结合调制静态阴影 |
| 调制阴影混合 | 均匀变暗,不考虑光线衰减 | - | 低 | - | 多光源易过暗 | 可结合静态阴影映射 |
| 加法阴影遮罩 | 更自然的光照效果 | - | 较高,计算阶段多 | - | 可累积多光源光线 | 无法结合调制静态阴影 |
6.2 实际应用中的选择建议
在实际应用中,选择合适的阴影技术需要综合考虑多个因素,以下是一些建议:
-
性能优先
:如果应用程序对性能要求较高,如移动设备上的游戏或实时交互场景,纹理阴影通常是更好的选择。它可以利用GPU进行快速计算,并且通过调整纹理大小和投影距离等参数,可以在一定程度上平衡性能和视觉质量。
-
逼真度优先
:如果追求高度逼真的阴影效果,且性能不是主要限制因素,加法阴影遮罩技术可能更适合。它能够更准确地模拟光线的传播和阴影的累积,产生更自然的光照效果。
-
兼容性和简单性
:对于一些简单的场景或对兼容性要求较高的应用,模板阴影可能是一个不错的选择。它的原理相对简单,并且在大多数硬件上都能正常工作。不过需要注意处理好边缘效果和与GPU变形几何的兼容性问题。
-
静态与动态结合
:如果场景中既有静态元素又有动态元素,可以考虑将调制阴影与静态阴影映射结合使用。静态阴影映射可以提前计算并烘焙到纹理中,减少实时计算量,而调制阴影则用于处理动态对象的阴影。
6.3 优化策略总结
为了在保证阴影效果的同时提高性能,我们可以采取以下优化策略:
-
模板阴影优化
:
- 利用可编程顶点GPU处理进行无限阴影体积挤出,提高效率。
- 检测哪些灯光可能影响视锥体,避免构建不必要的阴影几何。
- 使用剪刀测试节省填充率,特别是对于点光源和聚光灯。
- 支持双侧模板和模板环绕扩展,减少不必要的开销。
- 在不需要Z - Fail算法时,使用成本较低的Z - Pass算法。
-
纹理阴影优化
:
- 合理设置阴影纹理数量上限,避免纹理内存和填充率成为瓶颈。
- 调整远投影距离和阴影偏移,改善拉长阴影的视觉质量。
- 对阴影边缘进行淡化处理,避免突然变化。
-
加法阴影优化
:
- 对于可编程材质,手动分割渲染阶段并正确标记,帮助Ogre进行阶段分类。
6.4 未来展望
随着硬件技术的不断发展,3D图形中的阴影处理技术也将不断进步。未来可能会出现更高效、更逼真的阴影算法,例如实时软阴影技术的普及,将使阴影效果更加自然和细腻。同时,与人工智能和机器学习的结合也可能为阴影处理带来新的突破,例如通过学习真实世界的光照和阴影模式,自动生成更符合实际情况的阴影效果。
6.5 总结流程图
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([开始选择阴影技术]):::startend --> B{性能要求高?}:::decision
B -->|是| C(纹理阴影):::process
B -->|否| D{追求高度逼真?}:::decision
D -->|是| E(加法阴影遮罩):::process
D -->|否| F{兼容性和简单性优先?}:::decision
F -->|是| G(模板阴影):::process
F -->|否| H{有静态与动态元素结合?}:::decision
H -->|是| I(调制阴影+静态阴影映射):::process
H -->|否| J(根据其他需求选择):::process
C --> K(优化纹理参数):::process
E --> L(优化加法阴影阶段):::process
G --> M(优化模板阴影计算):::process
I --> N(结合静态与动态阴影):::process
K --> O([结束选择]):::startend
L --> O
M --> O
N --> O
J --> O
7. 结语
3D场景中的阴影处理是一个复杂而又关键的领域,它直接影响着场景的真实感和用户体验。通过深入了解Ogre中提供的各种阴影技术,包括模板阴影、纹理阴影、调制阴影混合和加法阴影遮罩,我们可以根据不同的应用场景和需求选择合适的技术,并通过优化策略提高性能和视觉质量。同时,我们也应该关注阴影技术的发展趋势,为未来的应用做好准备。希望本文能够帮助你在3D图形开发中更好地处理阴影问题,创造出更加逼真和精彩的场景。
超级会员免费看
127

被折叠的 条评论
为什么被折叠?



