4 合成器
合成器是 Ogre 2.0 中的核心和关键组件。在1.x中,它只是用于花哨的后期处理效果。在2.0中,这是告诉Ogre你想要如何渲染场景的方式。如果不进行设置,Ogre 将无法呈现到屏幕。
使用合成器,用户不再需要处理设置视口,渲染目标,每帧更新这些渲染目标等。
相反,用户现在必须设置节点和工作区。工作区是顶级系统,用户在其中指示要使用的节点、连接节点的方式、将声明哪些全局渲染纹理(同一工作区中的所有节点都可以看到)、渲染最终输出的位置(即 RenderWindow,屏幕外的 RenderTexture)以及要使用的 SceneManager。用户可以同时激活多个工作区。
新的合成器系统深受Blender的合成器系统的启发。图片来自Blender:
合成器脚本语法没有太大变化,这应该使移植它们变得非常容易。然而,在内部,新系统是从头开始编写的(同时重用和审查一些现有代码);因为以前的合成器是基于堆栈的,而新的合成器是基于节点的。
因此,如果您曾经直接从C++操作合成器;移植工作可能会大大增加。
4.1 Nodes
合成器节点很可能类似于 1.x 中以前的"合成器"。
以下节点清除 RT 并绘制渲染队列中的所有内容
compositor_node MyNode
{
in 0 Input_as_MyLocaName // Take input texture #0 and use the local name "Input_as_MyLocaName" for reference
target Input_as_MyLocaName
{
//Clear to violet
pass clear
{
colour_value 1 0 1 1
}
pass render_scene
{
visibility_mask 0xffffffff //Viewport's visibility mask
rq_first 50 //Inclusive
rq_last 51 //Not inclusive
}
}
out 0 Input_as_MyLocaName
}
"Input_as_MyLocaName"的定义在哪里?它的分辨率是多少?它的位深度?RT 来自输入pass,因此答案是它取决于工作区将如何连接此节点。工作区可以传递在上一个节点中声明的本地 RTT,也可以传递 RenderWindow。
4.1.1输入输出pass和RTT
节点的 RT 可能来自三个不同的源:
- 它是在本地声明的
- 它来自输入pass。
- 它是在工作区中声明的全局纹理。全局纹理必须使用global_前缀
4.1.1.1 局部声明的纹理
以下脚本声明分辨率为 800x600 的局部纹理,将其清除为紫色,并将其置于输出pass#0 中(以便其他合成器节点可以将其用作输入):
compositor_node MyNode
{
texture rt0 800 600 PF_R8G8B8
target Input_as_MyLocaName
{
//Clear to violet
pass clear
{
colour_value 1 0 1 1
}
}
out 0 rt0
}
您可能已经注意到,用于声明 RTT 的语法与 Ogre 1.x 中的语法几乎完全相同。
有关它的更多信息,请参阅 Ogre 的 1.9 文档。
有一些小的变化:
- 新参数"no_gamma":在1.x中,如果全局伽玛设置打开,则默认情况下,HW gamma将处于打开状态;无法将其关闭。但可以强制始终使用关键字"gamma"。在 Ogre 2.x 中,将选项留空将使用系统的设置,写入关键字"gamma"强制打开,然后使用关键字 “no_gamma” 将其关闭。
- 参数"scope"不再可用。
- 参数"pooled"不再可用。
4.1.1.2它来自输入pass。
输入pass是有编号的。必须为输入pass指定一个名称,以便所有目标传递都可以在节点作用域本地引用它们。不能有任何间隙(即使用pass 0 和 2,但不使用pass 1)
输出pass也经过编号,可以分配RTT。工作区稍后将使用此编号来执行连接。
工作区将负责将节点 A 的输出pass与节点 B 的输入pass连接起来。换句话说,pass是在节点之间发送、接收和共享 RTT 的一种方式。
唯一的限制是全局纹理既不能用作输入也不能用作输出(全局纹理直接引用)。反之亦然,输入pass可能比输出pass多,并且可能没有输入pass或输出pass(即单独使用全局纹理时)。
以下(相当无用的)代码段获取pass #0 并将其发送到输出pass #1,获取输入pass #1 并通过输出pass #0 发送(换句话说,它会翻转pass),并将本地创建的未初始化纹理发送到pass #2:
compositor_node MyNode
{
in 0 myFirstInput
in 1 mySecondInput
texture rt0 target_width_scaled 0.25 target_height_scaled 0.25 PF_R8G8B8
out 0 mySecondInput
out 1 myFirstInput
out 2 rt0
}
从Blender的合成器系统中汲取灵感,左边的小点是输入pass,而右边的点将是输出纹理pass:
4.1.1.3 它是一个全局纹理
全局纹理在工作区范围内声明,并且具有与本地纹理相同的语法(与 Ogre 1.x 的语法相同)。有一些限制:
- 全局纹理名称必须包含global_前缀。相反,任何试图使用以"global_"开头的名称的本地纹理或输入pass都是非法的。
- 它们不能在输入或输出pass中使用。
- 属于同一工作区的任何节点都可以看到全局纹理,但它们看不到属于不同工作区的全局纹理。
- 如果节点使用工作区未声明的全局纹理,则工作区的执行将失败。
全局纹理与C++全局变量一样强大/危险。它们存在的主要原因是许多节点可能使用临时rtt来执行其工作,并且在可以共享和重用时在每个节点上声明这些中间rtt是非常浪费的。
共享和重用也可以通过输入和输出pass实现,但是对于临时rtt(或非常频繁访问的rtt,即延迟着色器的Gbuffer),它会导致连接地狱;因此,全局纹理更适合。
- in <channel_id> <local_texture_name>
channel_id是范围 [0; inf) 中的数字,但必须是连续的和连续的(无间隙,即定义pass 0、2,但没有 1)。loca_texture_name不能以"global_"开头。节点定义可能没有输入。 - out <channel_id> <local_texture_name>
channel_id是范围 [0; inf) 中的数字,但必须是连续的和连续的(无间隙,即定义pass 0、2,但不是 1)。loca_texture_name不能以"global_"开头。节点定义可能没有输出。 - in_buffer<channel_id><buffer_name>
对于UAV缓冲区。与常规纹理相同,只是您可以引用全局缓冲区,并且全局缓冲区不必以"global_"开头。如果本地缓冲区和全局缓冲区具有相同的名称,则本地缓冲区优先。 - out_buffer<channel_id><buffer_name>
对于UAV缓冲区连接。请参阅in_buffer。 - custom_id<字符串>
将进行哈希处理以标识此节点定义的自定义字符串。用于将节点分类为类别。
4.1.1.4 主渲染目标
创建 Workspace 实例时,C++代码将要求 RT,该 RT 应为最终目标(即 RenderWindow)。这个RT非常重要,因为像"target_width_scaled"这样的关键字和像fsaa和hw gamma这样的设置将基于这个主RT的属性。此功能将在"工作区"部分中更详细地看到。
注意#1!
默认情况下,您不能使用主 RenderTarget 作为纹理(因为它通常是 RenderWindow,而 D3D 和 OpenGL 不允许这样做),这样做可能会导致崩溃。
可以手动调用 node->connectFinalRT 并提供可以绑定的纹理指针(即,如果最终的 RenderTarget 是 RenderTexture)。尚未实现执行此操作的自动方法。
4.1.2 Target
Target包括多个Pass。它们的主要用途是定义pass将渲染到哪个渲染目标。
值得注意的是,目标接受可选参数"slice"。
slice 参数是可选的,它是指要从给定纹理使用的 3D 纹理(或立方体贴图或 2D 数组)中的切片或面。有效值可以是数字或提示"+X"。"-X"、"+Y"、"-Y"、"+Z"和"-Z"。
注意:"slice"一词不要写。只需编写"target myTexture +X"即可工作。
注意: 当目标是 3D/立方体贴图/数组纹理时,如果slice超出边界,将引发异常。如果目标是 2D 或 1D 纹理,则会以静默方式忽略此值。默认值:slice = 0
4.1.3 Passes
Pass与ogre 1.x中的Pass相同。在撰写本文时,有6种类型的Pass:
- clear(PASS_CLEAR)
- generate_mipmaps (PASS_MIPMAP)
- quad (PASS_QUAD)
- resollve (PASS_RESOLVE)
- render_scene (PASS_SCENE)
- stencil (PASS_STENCIL)
- uav_queue (PASS_UAV)
- custom (PASS_CUSTOM)
计划了更多pass,包括用户能够使用过去在 1.x 中存在的自定义凭证进行扩展。
计划pass包括:
- 抛物面映射pass(用于高效环境映射和点光影贴图)
- N pass(优化 Ogre 数据以进行两次或多次传递,即 Z press-pass,在引擎中开销最小,因为剔除数据相同)
所有传递都支持以下脚本参数:
- pass <type>[customId]
‘type’ 必须是受支持的类型之一:clear、quad、resolve、render_scene、stencil、custom。
customId 参数是可选的,由custom pass 使用,以便为注册的custom pass 提供程序提供识别多种类型的方法(以防存在多种类型的自定义pass)。 - num_initial<number>
将执行此操作的次数。默认值为 -1,表示始终执行。当执行计数达到该值时,在 d3d 设备重置、调整大小或重新创建工作区之前,它不会再次执行(执行计数被重置并再次执行 N 次)
此参数替换 Ogre 1.x 中的"only_initial"参数。 - identifier<number>
一个任意用户定义的数字 ID,用于标识C++代码中的各个传递。 - execution_mask <hex number>
8 位十六进制值。指定执行掩码。有关详细信息,请参阅立体声和分屏渲染。默认值为0xFF但清除pass除外,清除pass默认为0x01。 - viewport_modifier_mask <hex number>
8 位十六进制值。指定视口修饰符掩码。有关详细信息,请参阅立体声和分屏渲染。默认值为0xFF但清除pass除外,清除pass默认为0x00。 - colour_write <off | on>
禁用颜色写入。适用于 Z pre-pass;或输出到 UAV 而不是常规渲染目标(如渲染纹理)的像素着色器。
默认值:开。
4.1.3.1 clear
clear pass的语法与 1.x 相同;除了默认情况下,现在模板也被清除。这原因是因为性能,因为 Z 缓冲区与stencil缓冲区绑定的 GPU 体系结构,仅清除 Z 缓冲区会阻止驱动程序完全丢弃缓冲区或使用快速 Z 清除。
此外,所有pass都可以定义它们将要处理的视口区域,这意味着现在可以清除特定区域。
4.1.3.2generate_mipmaps
Generate_mipmaps除了仍然相关的共享参数(即标识符)之外,没有其他特殊参数。它们对于在完成渲染后显式填充较低的 mip 级别非常有用。
此传递在声明纹理时不需要automipmap关键字。"自动贴图"选项允许您完全跳过使用generate_mipmaps。这可能很诱人,但此选项的问题在于,如果您多次写入和读取纹理,但只想在整个过程结束后生成 mipmap,则每次需要再次读取纹理后,automipmap 关键字都会生成 mipmap。
禁用自automipmap 并使用generate_mipmaps允许您显式控制何时生成 mipmap;没有隐藏的消耗GPU性能。
- mipmap_method [api_default | compute | compute_hq]
默认值为api_default 这将要求 API 或驱动程序为你生成它们。如果 API 不支持它(例如 DX12),则将使用计算生成。
计算(实验)使用计算着色器。计算要求纹理为 UAV。某些格式可能不起作用,例如 sRGB 格式,因此纹理必须使用no_gamma修饰符。
compute_hq(实验)使用高质量的高斯滤波器。适用于快速和高质量的mipmap生成。 - kernel_radius <8>
整数值。默认值为8。必须是正数,偶数。定义计算高斯滤波器的核半径。 - gauss_deviation <0,5>
高斯滤波器的标准差。默认值为 0,5。
注意#1!
generate_mipmaps在纹理级别工作。如果传递 Cubemap,它将为所有face生成 mipmap。如果传递 3D 或 2D 阵列纹理,它将为所有切片生成 mipmap。
RenderWindow 没有纹理,因此尝试在其上生成 mipmap 将引发异常。
注意#2!
为了使基于计算的 mipmap 生成正常工作,Ogre 必须在 JSON 支持下进行编译,并且用户必须将 Ogre 存储库中包含的资源包含在 Samples/Media/2.0/scripts/materials/Common 中。
4.1.3.3 quad
quad pass 具有与 1.x 相同的语法;此外,还添加了以下关键字:
- use_quad [yes | no ]
缺省值为否。当否时 ;合成器将绘制一个全屏三角形。由于现代GPU的工作方式,使用两个矩形会浪费对角线边框中的GPU处理能力,因为像素至少在2x2块中处理;并且必须丢弃三角形中像素的结果。单个三角形效率更高,因为所有块都填满了视口区域,当矩形离开视口时,GPU 会有效地剪辑它。
当视口不是 0 0 1 1 时;此值强制为yes。下图显示了一个全屏三角形:
插值将导致有效 UV 坐标在视口区域内时处于 [0; 1] 范围内。
使用camera_far_corners_world_space也会强制使用四边形而不是三边形(但camera_far_corners_view_space适用于三角形)
要解释为什么这是性能优化,请参阅Fabien Giesen的"优化基本光栅器"。
- expose <textureName>
低级材质可以通过旧的"content_type合成器"设置访问本地和全局纹理,Hlms 材质可以通过调用 SceneManager::getCompositorTextures 来访问它们。但在你这样做之前,你需要让他们暴露在pass上。这是必要的,因此 Ogre 可以知道在传递过程中可以或将要使用哪些纹理,以便可以在 DX12 和 Vulkan 等显式 API 中发出资源转换和障碍。
以下设置在 Ogre 1.x 中可用;但没有记录:
- quad_normals [camera_far_corners_view_space | camera_far_corners_view_space_normalized | camera_far_corners_view_space_normalized_lh | camera_far_corners_world_space | camera_far_corners_world_space_centered | camera_direction]
通过"NORMALS"语义发送相机在世界空间或视图空间中的视锥角。这对于仅使用深度和拐角有效地重建位置特别有用。camera_direction选项发送穿过视锥体的方向,并且与camera_far_corners_world_space_centered相同,但归一化(值在[0;1]范围内),这对于天空渲染和大气散射很有用。请参阅TutorialSky_Postprocess示例。
有趣的阅读: 从深度重建位置第一部分,第二部分,第三部分camera_far_corners_view_space_normalized就像camera_far_corners_view_space,但将整个向量除以远平面;导致 dir.z 始终为 -1(但向量本身不是单位长度)。带有_lh后缀的那个是左手系变体(即dir.z = 1而不是-1)
从 Ogre 2.0 开始,UV 坐标始终以传递四边形的形式发送到顶点着色器。
注意!
在ogre2.x中;您需要应用wold-view-proj 矩阵,以便绘制的pass补偿 Direct3D9 中的纹素到像素对齐读取。如果不这样做,不仅会导致上述对齐问题,而且当视口不是 0 0 1 1 时,还会导致故障
在 Ogre 1.x 中,在 OpenGL 中渲染到 FBO 时,只需要 proj 矩阵来解决纹理翻转问题。
4.1.3.4 resolve
注意:在撰写本文时,解析传递尚未完全实现
待定
当渲染系统不支持显式resolve(或创建纹理时没有 msaa 设置)时,纹理将被视为隐式resolve,并且所有resolve pass都将被忽略。
有关详细信息,请参阅MSAA:显式与隐式解析部分。
4.1.3.5 render_scene
语法也类似于1.x;但是有几个修改:
- rq_first、
替换first_render_queue。默认值为 0。必须是介于 0 和 255 之间的值。该值具有包容性 - rq_last<id>
替换last_render_queue。默认值为"max",这是一个特殊参数,表示最后一个活动的渲染队列 ID。如果为数字,则值必须介于 0 和 255 之间。该值是不包含。 - viewport <left><top> <width> <height> [<scissor_left> <scissor_top> <scissor_width> <scissor_height>]
指定视口。所有其他pass(即 clear 和 quads)也支持,默认值为"0 0 1 1",覆盖整个屏幕。值应介于 0 和 1 之间。
当 4 个参数被替换时,剪裁框与视口匹配。所有8个参数都允许设置自定义剪刀盒。注意:剪刀式测试必须由Hlms Macroblock启用才能工作,我们只是在这里设置大小。
合成器将自动在不同pass之间共享指向同一 RenderTarget 的视口指针(即使对于不同的节点),只要它们共享完全相同的参数即可。 - visibility_mask<number>
pass的视口使用的可见性蒙版。那些未通过测试’entityMask & visibility_mask’ 的实体将不会被渲染。Ogre 1.x 没有重大更改,除了脚本编译器现在接受带有 0x 前缀的十六进制值;而不仅仅是十进制值。 - shadows <off | shadow_node_name> <reuse | recalculate | first>
默认情况下处于关闭状态。指定用于使用阴影贴图进行渲染的阴影节点。有关详细信息,请参阅有关影子节点的部分。当提供影子节点的名称时,第二个参数默认为"first"。 - overlays <off | on>
是否从叠加系统组件叠加。对于常规节点,默认情况下为"打开",对于影子节点,默认情况下为"关闭"。目标是最终叠加层像其他所有东西一样服从RenderQueue ID,但是移植起来太难了(叠加系统有点复杂…),所以创建了这个黑客/标志。它最终将被删除。 - camera <camera_name>
如果未指定,则默认摄像机用于渲染pass(从C++实例化工作空间时,将指定此默认摄像机)。
当给出名称时,合成器将查找此相机并使用它。对于反射pass(镜子,水)非常有用,用户希望控制相机,而合成器与之相关联。摄像机必须由用户在实例化工作空间之前创建,并且在销毁工作空间之前一直保持有效。 - lod_camera<camera_name>
LOD计算将基于的相机视角(即对于阴影映射很有用,这需要LOD与用户相机的LOD相匹配)。当提供空字符串时,Ogre 将假定 lod 摄像机与当前摄像机相同,但阴影节点除外,在阴影节点中,它将假定它是影子节点连接到的正常pass的lod_camera。默认值:空字符串。 - lod_update_list [yes | no ]
如果为否(或为 false),则不会更新 LOD 列表,并将使用由上一次传递计算的 LOD 列表。这节省了宝贵的 CPU 时间。对于使用相同lod_camera的多个pass非常有用(中间没有一个具有不同lod_camera的pass会覆盖缓存的 LOD 列表)。如果您的应用程序非常受 CPU 限制,因此不需要 LOD,则在所有传递中将此设置设置为 false 将有效地关闭 lodding(并缓解 CPU 故障)。默认值:是; 除了属于影子节点的传递,除非lod_camera是非空字符串,否则将强制为 false。 - lod_bias<bias>
将偏差乘数应用于 lod。有效值在 [0;Inf)。较高的 lod 偏差会导致 LOD 更快地弹出。默认值:1.0 - camera_cubemap_reorient [yes|no]
如果为是,则摄像机将重新定向以渲染立方体贴图,具体取决于我们要渲染到的渲染目标的哪个切片(仅限 3D、立方体贴图和 2D 数组纹理)。其原始方向将在pass完成后恢复。旋转是相对于其原始方向的,如果相机未设置为标识(除非这是所需的效果),则可能会产生违反直觉的结果。请参阅"pass"部分,了解如何指示应渲染到哪个切片。默认值:否。
注意:如果目标不是立方体贴图,Ogre 仍会尝试旋转摄像机,通常为意外角度。 - enable_forward3d [yes | no]
如果是,此pass将使用 Forward3D(必须首先由开发人员通过C++启用,请参阅 Forward3D 示例)。如果不是,则Forward3D将不会用于此pass,这可以提高CPU和GPU端的性能(但许多灯可能不会被绘制或使用)。默认值:是。
详:CPU端,灯光不会被剔除到摄像机上(只有当F3D在同一帧中还没有缓存,具有完全相同的摄像机和角度时才节省)。GPU端,像素着色器会更轻。 - expose <textureName>
低级材质可以通过旧的"content_type合成器"设置访问本地和全局纹理,Hlms 材质可以通过调用 SceneManager::getCompositorTextures 来访问它们。但在你这样做之前,你需要让他们expose pass上。这是必要的,因此 Ogre 可以知道在传递过程中可以或将要使用哪些纹理,以便可以在 DX12 和 Vulkan 等显式 API 中发出资源转换和障碍。
4.1.3.6 stencil
stencil pass 比 Ogre 1.x 中灵活一点。请始终记住在离开节点之前还原stencil pass,否则将执行的下一个节点可能会使用意外的stencil设置。
最相关的更改是,现在可以更灵活地定义双面模板(它不再是布尔值),并且语法略有变化以适应此更改:
pass stencil
{
check true
mask 0xff
read_mask 0xff
both
{
fail_op keep
depth_fail_op increment
pass_op decrement_wrap
}
}
“read_mask” 是新的,现在 fail_op, depth_fail_op和 pass_op 必须在括号里。
有效值为 “both”, “font”, 和"back"。 “both”只是一个快捷方式,用于只需要较少的输入就可以同时定义 front和back。
4.1.3.7 uav_queue
这是 Ogre 2.1 中引入的一项新功能。s 代表Unordered Access Views,在 D3D 的行话中。OpenGL用户知道uva是"图像"纹理(imageLoad,imageStore)和SSBO(着色器存储缓冲区对象)的功能组合。uva机是强大的功能,因为它们允许从着色器随机读取和写入访问,甚至支持原子操作。正确使用它们可以达到令人难以置信的效果,如果没有uav就无法做到,但不当使用会严重损害性能。
D3D11和OpenGL在如何从API接口角度对待uav方面存在很大差异。D3D11 uav等于渲染目标;而OpenGL则更像纹理。
实际上,绑定uva的 D3D11 API 调用必须同时设置 RenderTargets。没有 API 调用来仅设置uav。D3D11迫使uav与RenderTargets共享插槽;总共最多有8个插槽(在Windows 8.1上使用D3D11.1时为64个),这让事情变得更难。这意味着如果您使用的是具有3个目标的MRT,则只剩下5个插槽用于uav。
我们猜测可以这样性能改进:D3D11可以在设置RT和UAV时检查危险(即确保您没有绑定与RT和UAV相同的资源),同时它们仍然使用与纹理相同的危险检查来检查您没有绑定纹理,同时将其绑定为RT / UAV。
如果UAV等于纹理,如OpenGL;每次纹理更改时,他们都必须根据纹理检查纹理,这是O(n)的复杂性;也是一个非常常见的操作。考虑到过去的经验,我们猜测OpenGL只是简单地跳过检查并让危险发生(当有硬件扩展允许您同时读取/写入这些资源时,只要您遵守某些规则,这很酷)。
由于 D3D11 比 OpenGL 更具限制性,因此我们的接口类似于 D3D11。
- starting_slot <number>
所有UAV插槽的偏移量。例如,如果您将UVA绑定到插槽3,并且起始插槽为2;UAV实际上将被绑定到UAV插槽5。设置为 255 时,将忽略插槽偏移量,并保留上次所做的设置。
默认值:255(默认情况下,Ogre 将其设置为 1)。 - uav <slot> <texture_name> [mrt #] [pixel_format] [ #]
设置在当前合成器作用域中可见的纹理(即全局纹理、输入纹理、局部纹理)。插槽、名称以及至少必须存在读取或写入标志。其他的都是可选的,默认为0(mrt和mipmap)和PF_UNKNOWN(格式,PF_UNKNOWN意味着UAV将使用原始纹理的像素格式,而不是尝试重新解释数据)。
如果指定 mipmap 级别,则必须存在关键字 mipmap。
例如,假设 starting_slot 为 1:
uav 0 global_myTexture 2 read write mipmap 5
将全局纹理"global_myTexture"的mrt切片#2绑定到插槽11,将具有读写访问权限,并使用mipmap级别5。
如果仅指定了插槽,则将清除给定插槽中的任何UAV。
- uav_external
与UAV完全相同。但是,该名称不是从 Compositor 作用域中按名称获取纹理,而是引用可通过 TextureManager::getByName 访问的纹理。 - uav_buffer <slot> <bufferName> <read> <write> [offsetBytes] [sizeBytes]
设置在当前合成器作用域中可见的 UAV 缓冲区(即全局缓冲区、输入缓冲区、本地缓冲区)。插槽、名称以及至少必须存在读取或写入标志。其他选项是可选的,默认为 0。当大小字节 = 0 时;我们假设您要从偏移量绑定到缓冲区结束。
例如,假设 starting_slot 为 1:
uav_buffer 0 myUavBuffer read write 256 512
请注意,对于您指定的偏移量,可能存在硬件对齐限制。256字节的倍数是一个安全的选择。
请注意,uav_buffer插槽与UAV纹理共享。将两者绑定到同一插槽索引只会导致其中一个可用。
如果仅指定了插槽,则将清除给定插槽中的任何UAV。
- keep_previous_uavs [true|false]
如果为 false,则所有插槽中的所有先前UAV都将被清除。如果为 true,则只有此传递修改的 UAV 插槽才会受到影响。默认值:true。
同步
UAV对读取和写入的顺序几乎没有保证。通常需要设置内存屏障才能获得正确的渲染。
OpenGL使用粗略屏障(影响将用作特定类型的所有资源);而 D3D12 使用精细屏障(每个资源)。因此,我们需要采用D3D12的方法。
合成器可以检测UAV何时在PASS_QUAD传递中用作纹理,因此它将自动插入内存屏障。但是,它无法检测到pass是否会使用刚刚用于写入的UAV作为UAV进行读取,因此用户必须手动插入障碍物。(待办事项:还没有接口可以做到这一点!)
还可以检测到显式设置为要用作纹理的pass(即在 CompositorPassScene 中)可见的合成器纹理,并将自动放置内存屏障。
待办事项: !!!接口正在进行中。代码可能无法按本节所述工作!!!
注意:在撰写本文时,内存障碍是一项正在进行的工作。文档可能会更改!
4.1.3.8 compute
compute pass允许您运行计算作业。它可以读取纹理,读/写UAV纹理,读/写UVA缓冲区。
- job <job_name>
设置要运行的计算作业的名称(HlmsComputeJob)。 - uav <slot> <texture_name> [mrt #] [pixel_format] [ #] [allow_write_after_write]
请参阅uav_queue的说明。allow_write_after_write的意味着合成器不会在连续传递之间插入屏障,该pass在对UAV只有写操作,没有读取操作。 - uav_buffer <slot> <bufferName> <read> <write> [offsetBytes] [sizeBytes] [allow_write_after_write]
请参阅uav_queue的说明。allow_write_after_write的意味着合成器不会在连续传递之间插入屏障,该pass在对UAV只有写操作,没有读取操作。 - input <slot> <texture_name> [mrt #]
将纹理绑定到纹理单元。语法与pass_quad相同。插槽不与UAV共享。
compute pass实际上并不属于渲染目标。但是,由于合成器的设计,必须在渲染目标中指定它们。您可以在有效的渲染目标中执行此操作:
compositor_node MyNode
{
in 0 rt_renderwindow
texture myUavTexture target_width target_height PF_R8G8B8A8 depth_pool 0 no_gamma uav
buffer myUavBuffer 1024 4
target rt_renderwindow
{
//Run compute job with myUavTexture & myUavBuffer bound.
pass compute
{
job myComputeJobName
uav 0 myUavTexture read write
uav_buffer 1 myUavBuffer read write
}
//Clear rt_renderwindow to violet
pass clear
{
colour_value 1 0 1 1
}
}
out 0 myUavTexture
out_buffer 0 myUavBuffer
}
或者到一个几乎不占用内存的虚拟渲染目标:
compositor_node MyNode
{
texture nullDummy target_width target_height PF_NULL
texture myUavTexture target_width target_height PF_R8G8B8A8 depth_pool 0 no_gamma uav
buffer myUavBuffer 1024 4
target nullDummy
{
//Run compute job with myUavTexture bound.
pass compute
{
job myComputeJobName
uav 0 myUavTexture read write
uav_buffer 1 myUavBuffer read write
}
}
out 0 myUavTexture
out_buffer 0 myUavBuffer
}
注意#1!
不要直接将 UAV 缓冲区设置为ComputeJob(类 HlmsComputeJob)。合成器需要评估内存障碍和资源转换。保留不一致的内存障碍可能会导致某些 API 中出现危险/争用情况。如果有疑问,请改为更改 CompositorPassComputeDef。
此外,将纹理设置为渲染目标也是危险的。对于 RenderTargets,请改为更改 CompositorPassComputeDef。
注意#2!
不要交错compute和graphics pass。为获得最佳性能,请尝试将所有内容一起批处理。
4.1.4 纹理
4.1.4.1MSAA:显式与隐式解析
不久前,MSAA 是自动支持的,并且与前向渲染器完美配合,无需后处理。Direct3D 9 和 OpenGL 根本无法从着色器访问单个 MSAA 子采样。
快进到现在,MSAA解析应该在HDR之后执行,以避免边缘周围的光晕,并且延迟着色无法解析G-Buffer,否则混叠只会变得更糟。
Direct3D10 和 GL 3.2 引入了从着色器访问 MSAA 子采样的功能,还提供了编写自定义解析的功能。
对于那些不知道"解析MSAA"是什么意思的人;一个非常简短的解释是,当使用2xMSAA渲染时,我们实际上是渲染到分辨率的两倍的RT。"解析"是将分辨率缩小到实际RT的行为(即想想Photoshop或Gimp的缩小滤镜模式)。请参阅末尾的 参考资料 部分,获取有关 MSAA 工作原理的详细说明的链接。
在不破坏与D3D9和较旧的GL渲染系统的兼容性的情况下干净地处理此新功能,同时能够毫不费力地打开和关闭MSAA;添加了"显式"和"隐式"解析的概念。
隐式解析
默认情况下,所有 RTT 都是隐式解析的。隐式解析纹理的行为模仿 Ogre 1.x(Ogre 1.x 中的实现和设计问题除外,这些问题可能导致 RTT 在每帧不必要地解析多次)
RTT 有一个内部标志,表示"dirty"。渲染到纹理时会变dirty;当它被渲染时,它就不再是dirty的了。
当您尝试将dirty的RTT绑定为纹理时,您正在强制Ogre解析它。这意味着您应该尝试尽可能多地延迟使用RTT作为纹理,直到您完成对它的渲染。
否则,如果渲染到 RTT,RTT 可能会解析多次:渲染(变dirty)、将其用作纹理、再次渲染(再次变dirty),然后再次用作纹理(全部在同一帧中)。在某些情况下,这是不可避免的,但通常不是。
显式解析
当您想要实现 API 的默认值以外的自定义解析时,将使用显式解析;或者您希望直接通过着色器访问 MSAA 子采样。
与隐式解析一样,RTT 也有一个dirty标志。然而:
- 尝试将dirty的RTT绑定为纹理将导致Ogre发送MSAA缓冲区;授予着色器访问子采样的能力。
- 尝试将非dirty dirty RTT 绑定为纹理将导致 Ogre 发送解析的缓冲区。着色器将无法访问子采样。
总之,着色器可以在纹理dirty时访问子采样。要清除dirty标志并在 RTT 上执行解析,请使用PASS_RESOLVE pass。这就是为什么它们被称为"显式"解析的原因;因为您必须明确告诉 Ogre解析msaa 目标并取消设置dirty标志2。
在不支持显式解析的 RenderSystems 上,所有纹理都将被视为隐式解析,并且PASS_RESOLVE传递将被忽略;这应该工作简单明了,除了一些特殊的情况外,没有问题。
使用RSC_EXPLICIT_FSAA_RESOLVE渲染系统功能标志检查 API 是否支持显式解析。
资源
4.1.4.2 深度纹理
自ogre2.1以来;支持深度纹理。很长一段时间以来,它一直是Ogre所缺少的功能。
深度纹理有点特别,因为它们可能不"拥有"深度缓冲区。它们只是一个空渲染目标,在现有深度缓冲区上有一个"视图"。但。。。这是什么意思?
深度缓冲区可能很棘手。假设以下示例:
compositor_node Example
{
texture myDepthTexture 512 512 PF_D32_FLOAT
texture finalSSAO 512 512 PF_R8G8B8
//Draw the depth
target myDepthTexture
{
pass clear {}
pass render_scene
{
}
}
target finalSSAO
{
pass clear {}
pass render_quad
{
material DepthAnalysis
input 0 myDepthTexture
}
}
out 0 finalSSAO
}
它只是做"渲染深度只传递到myDepthTexture;并使用渲染四边形读取深度缓冲区内容,并将结果存储在称为’finalSSAO’的颜色RTT中"
那很容易。但是这个呢?
compositor_node Example2
{
texture firstPass 512 512 PF_R8G8B8
texture finalColour 512 512 PF_R8G8B8
//Draw everything, colour and depth
target firstPass
{
pass clear {}
pass render_scene
{
}
}
target finalColour
{
pass clear {}
pass render_quad
{
material SSAO
input 0 ??? // Depth, firstPass' depth texture?
input 1 firstPass
}
}
out 0 finalColour
}
第一个pass是包含颜色和深度的pass。第二个,我们只想将深度和颜色缓冲区分别作为 SSAO 材质pass的输入纹理。
但是我们如何获取深度缓冲区呢?为此,我们需要执行两个步骤:
- 请求要使用深度纹理的原始 RTT。
- 创建一个深度纹理,该纹理将是深度缓冲区的"视图"。深度缓冲区共享系统应为RTT和深度纹理"视图"分配相同的深度缓冲区。
解决方案如下:
compositor_node Example2_fixed
{
//Instruct we want to use a depth texture (32-bit float). The “depth_texture” keyword is necessary. Specifying The depth format is optional and so is the depth pool. However recommended to specify them to avoid surprises.
texture firstPass 512 512 PF_R8G8B8 depth_format PF_D32_FLOAT depth_texture depth_pool 1
//Declare the depth texture view (which becomes so by using PF_D32_FLOAT as format). Settings MUST match (depth format, pools, resolution). Specifying the depth pool is necessary, otherwise the depth texture will get its own depth buffer, instead of becoming a view.
texture firstPassDepthTexture 512 512 PF_D32_FLOAT depth_pool 1
texture finalColour 512 512 PF_R8G8B8
//Draw everything, colour and depth
target firstPass
{
pass clear {}
pass render_scene
{
}
}
target finalColour
{
pass clear {}
pass render_quad
{
material SSAO
input 0 firstPassDepthTexture
input 1 firstPass
}
}
out 0 finalColour
}
注意#1!
在许多硬件上,深度缓冲区是压缩的(请参阅Depth In Depth和ATI Radeon HD 2000编程指南)。在 AMD 的 GCN Taihiti 硬件 (AMD Radeon R9 280) 之前,深度缓冲区在作为深度纹理进行采样时需要解压缩。尝试再次使用深度纹理作为深度缓冲区而不清除它将由于缺乏压缩而降级。
如果您希望使用一个深度纹理进行采样,另一个用于继续渲染,则建议将深度纹理复制到另一个深度纹理,以最大限度地提高性能。
NVIDIA和Intel的深度压缩(解压缩)细节尚不清楚,但它们很可能与类似的问题有关。
TODO:合成器接口,可自动将深度纹理复制到另一个深度纹理。
4.2 阴影节点
在 Ogre 中拥有阴影的唯一方法是通过阴影节点。
模板阴影和"纹理阴影"已从Ogre 2.0中删除;仅支持深度阴影贴图。
阴影节点是一种特殊类型的 Node(实际上,该类继承自 CompositorNode),它在常规节点(通常为 render_scene 传递)内执行,而不是连接到其他节点。
但是,可以将阴影节点的输出连接到常规节点以进行进一步的后处理(即用于实时全局照明的反射阴影贴图),但阴影节点不能有输入。在撰写本文时,此特定功能(输出到常规节点)仍在进行中,因为确保在执行影子节点后执行常规节点可能有点棘手。
4.2.1设置阴影节点
阴影节点的工作方式与常规节点非常相似。也许它们最明显的区别是如何定义RTT。以下关键字应该在阴影节点范围内:
- technique <uniform|planeoptimal|focused|lispsm|pssm>
指定要用于后续影子映射声明的影子技术。默认值为uniform。
注意:在撰写本文时,lispsm技术已损坏并且具有可怕的伪影。这个错误似乎影响了Ogre 1.9和2.0。请改用专注的技术。
平面优化也尚未实现。
- use_aggressive_focus_region <true|false>
由 Focused、LispSM 和 PSSM techniques 使用。默认值为 true。如果出现阴影映射故障,请尝试将此值设置为 false。但请注意,禁用它的质量下降可能会很明显。 - optimal_adjust_factor <factor>
LispSM 和 PSSM 技术使用。默认值为 5。较低的值可提高更接近相机的阴影贴图的质量,而较高的值可减少"投影偏斜",这是lispsm的关键特征;从而导致质量恢复,看起来更像聚焦。
提示: 当此值为负数时,lispsm 将恢复为聚焦阴影贴图技术。这对于想要将 PSSM 技术与集中设置而不是 LispSM 一起使用的用户非常有用。 - light_direction_threshold <angle_in_degrees>
由 LispSM 和 PSSM 技术使用。默认值为 25。最初,LispSM 会在相机方向与光线方向平行时产生伪像。当相机方向和光线之间的角度低于阈值时,Ogre开始逐渐增加optimal_adjust_factor,使其恢复到对焦设置,从而摆脱这些伪影。
注意:当optimal_adjust_factor为负数(即 pssm + focusd)时,此设置将被忽略。 - num_splits <num_splits>
仅由 PSSM 技术使用。指定每盏灯的分割次数。每个阴影贴图可能有所不同。splits必须大于 2。默认值为 3。 - pssm_lambda <lambda>
仅由 PSSM 技术使用。值通常在 0 和 1 之间。默认值为 0.95。PSSM 的 lambda 是指数插值和每次拆分之间的线性分隔的权重值。较高的 lambda 将使用指数分布,因此更近的阴影将提高质量。较低的 lambda 将使用线性分布,进一步推动分割,从而提高远处阴影的质量。 - shadow_atlas <name> light <light index> [split <split index>]
请参阅阴影图集部分。
shadow_map [Depth] [] [] [gamma] [fsaa <fsaa_level>] [depth_texture] [depth_pool ] light [split ] [uav] [2d_array|3d|cubemap] [mipmaps ] [automipmaps]
阴影映射声明顺序很重要。声明的第一个阴影映射成为阴影映射 #0;声明的第二个阴影映射成为#1;等等。大多数设置与常规纹理相同。因此,将仅描述新设置或行为不同的设置:
- <Name>
与常规纹理一样,必须指定本地唯一的名称(并且不能以全局前缀开头)。为了避免混淆,强烈建议您根据阴影贴图的数量来命名它们。例如,.将第一个阴影贴图命名为“0”,将第二个阴影贴图命名为“1” - <Depth>
用于2d_array 和3d纹理。指定其深度/切片数。2d纹理自动强制为1,立方体贴图自动强制为6。 - <fsaa>
无论系统设置如何,默认值始终为0。将此设置为更高的值将激活fsaa。此行为不同于常规纹理声明。 - <gamma>
如果不存在,则无论系统设置如何,阴影贴图都不会使用硬件 gamma 转换。当存在此设置时,纹理将为您执行自动硬件伽玛转换(当硬件支持时)。此行为不同于常规纹理声明。 - <depth_texture>
如果不存在,阴影贴图将使用常规深度缓冲区。如果存在,阴影贴图将使用深度缓冲区,该缓冲区可用作作为纹理进行读取。当使用深度像素格式(如PF_D24_UNORM_X8) - light <index>
指示将与此阴影贴图关联的光源索引。即,阴影映射 #0 可能包含第 N 个最接近实体的阴影映射光,而不一定是第一个。 - split <split index>
默认值:0;仅在使用 PSSM 技术时才需要。指示此阴影映射所引用的split - uav
当存在时,纹理可以用作无人机。 - [2d_array|3d|cubemap]
当存在时,纹理将被创建为2d_array,3d或立方体贴图。主要与uav相关,但对渲染也很有用。请参阅目标切片参数。 - mipmaps <num Mipmaps>
默认值:0;指示要使用的 mipmap 数。0 表示无。使用 -1 填充所有 mipmap,直到 1x1 - [automipmaps]
如果存在,则使用自动生成mipmap。每次更改RTT(例如,通过渲染到它,通过清除它等),它都会被标记为dirty。当它再次用作纹理时,mipmap将自动生成。在某些情况下,如果涉及乒乓RTT,并且您只想在最后生成mipmap,这可能会有问题。在这种情况下,请考虑改用PASS_来手动生成它们。
shadow_map <shadowMapName0> <shadowMapName1> {}
声明阴影映射是不够的。你需要告诉ogre你想给它渲染什么。为此,您需要render_scene pass。
阴影节点可以使用常规的"target {传递render_scene {} }"语法编写。但是,当您有 6 个具有相同精确pass设置的阴影贴图时,编写 6 次pass会很麻烦。相反,"shadow_map"关键字会为您重复传递。
4.2.2 示例
以下是一个基本脚本,该脚本将使用集中设置设置单个阴影贴图:
compositor_node_shadow myShadowNode
{
technique focused
shadow_map 0 2048 2048 PF_FLOAT16_R light 0
//Render shadow map "0"
shadow_map 0
{
pass clear { colour_value 1 1 1 1 }
pass render_scene
{
rq_first 0
rq_last max
}
}
}
典型的设置是有一个太阳方向光,然后是多个点光源或聚光灯。这意味着定向光应使用PSSM设置以获得最佳质量,而点光和聚光灯阴影贴图可以使用聚焦或均匀。
以下脚本为 3 个 PSSM 分割创建 3 个阴影贴图,并为其余光源创建 3 个附加阴影贴图:
compositor_node_shadow myShadowNode
{
technique pssm
//Render 1st closest light, splits 0 1 & 2
shadow_map myStringName 2048 2048 PF_FLOAT16_R light 0 split 0
shadow_map 1 1024 1024 PF_FLOAT16_R light 0 split 1
shadow_map 2 1024 1024 PF_FLOAT16_R light 0 split 2
//Change to focused from now on
technique focused
shadow_map 3 1024 1024 PF_FLOAT16_R light 1
shadow_map 4 1024 1024 PF_FLOAT16_R light 2
shadow_map 5 512 512 PF_FLOAT16_R light 3
//Render shadow maps "myStringName", "1", "2", "3", "4" and "5"
shadow_map myStringName 1 2 3 4 5
{
pass clear { colour_value 1 1 1 1 }
pass render_scene
{
rq_first 0
rq_last max
}
}
}
展示阴影节点的全部灵活性:以下示例有点大,但表明阴影地图渲染可以按任何顺序完成,使用不同的设置,仅其中一个地图的自定义渲染队列等。用户在定义如何渲染阴影贴图方面拥有很大的权力:
compositor_node_shadow myShadowNode
{
technique pssm
//Render 1st closest light, splits 0 1 & 2
shadow_map myStringName 2048 2048 PF_FLOAT16_R light 0 split 0
shadow_map 1 1024 1024 PF_FLOAT16_R light 0 split 1
shadow_map 2 1024 1024 PF_FLOAT16_R light 0 split 2
//Change to focused from now on
//(we can also change optimal_adjust_factor, etc)
technique focused
//Render 2nd closest light in the 4th shadow map
//(could be directional, point, spot)
shadow_map 3 1024 1024 PF_FLOAT16_R light 1
//Render 3rd closest light in the 5th shadow map
shadow_map 4 1024 1024 PF_FLOAT16_R light 2
//Don't use aggressive focus region on shadow map 5 (and from anything
//declared from now on, we can also turn it back on later) just for
//showcasing how this works
use_aggressive_focus_region false
shadow_map 5 target_width_scaled 0.25 1024 PF_FLOAT16_R light 3
//Render shadow maps "myStringName", "1", "2", and "5"
shadow_map myStringName 1 2 5
{
pass clear { colour_value 1 1 1 1 }
pass render_scene
{
visibility_mask 0xffffffff
rq_first 0
rq_last max //Special value implying maximum
}
}
//Render shadow map "4" with a different setting
shadow_map 4
{
pass clear { colour_value 1 1 1 1 }
pass render_scene
{
shadow_map_idx 4 //Note this is needed!
visibility_mask 0xfaffffff
rq_first 40
rq_last 51
}
}
//Regular way of declaring passes also works.
target 3
{
pass clear { colour_value 1 1 1 1 }
pass render_scene
{
shadow_map_idx 3 //Note this is needed!
visibility_mask 0xfaffffff
rq_first 42
rq_last 51
}
}
//Send this RTT to the output channel. Other nodes can now use it.
out 0 myStringName
}
设置阴影节点非常灵活且功能强大。实现环境映射pass时,应该能够实现可完全渲染其 360° 环境的点光源。
4.2.3阴影地图集
警告:以下功能是实验性的,可能会崩溃,无法按预期工作,并且可能会发生更改。
新的合成器支持阴影地图集。现在无需为每个 RTT 编写一个阴影贴图,而是可以将多个阴影贴图渲染到同一个 RTT 中,只是不同的非重叠视口区域。
首先要做的是声明常规阴影贴图。然后需要使用"shadow_atlas"关键字声明后续的阴影映射:
compositor_node_shadow myShadowNode
{
technique pssm
//Render 1st closest light, splits 0 1 & 2, and 2nd light into myAtlas
shadow_map myAtlas 2048 2048 PF_FLOAT16_R light 0 split 0 viewport 0 0 0.5 0.5
shadow_atlas myAtlas light 0 split 1 viewport 0 0.5 0.5 0.5
shadow_atlas myAtlas light 0 split 2 viewport 0.5 0 0.5 0.5
technique focused
shadow_atlas myAtlas light 1 viewport 0.5 0.5 0.5 0.5
/* ... */
}
现在我们只需要声明render_scene通过。我们将避免使用方便的shadow_map,以便我们可以执行一个完全清除而不是4个。
/* ... */
target myAtlas
{
pass clear { colour_value 1 1 1 1 }
pass render_scene { viewport 0.0 0.0 0.5 0.5 }
pass render_scene { viewport 0.5 0.0 0.5 0.5 }
pass render_scene { viewport 0.0 0.5 0.5 0.5 }
pass render_scene { viewport 0.5 0.5 0.5 0.5 }
}
/* ... */
合成器节点应根据视口坐标自动检测您引用的阴影贴图。如果视口设置与任何已声明的阴影地图集都不匹配,则应引发错误。
- 提示:您可能希望探索 Ogre 强大的脚本功能(即抽象pass),而不是复制粘贴每个render_scenepass。
在接收器对象上,纹理投影矩阵应已缩放到视口范围内的点。
4.2.4 重用、重计算和优先
来自常规节点的每个PASS_SCENE都有三个设置:
- SHADOW_NODE_REUSE
- SHADOW_NODE_RECALCULATE
- SHADOW_NODE_FIRST_ONLY
这会影响阴影节点的执行时间以及它们缓存结果的方式。默认值为"SHADOW_NODE_FIRST_ONLY";这意味着ogre应该自动管理这一点;但是,有时SHADOW_NODE_REUSE可能很有用。
用示例解释它们的作用更容易。
假设用户有两个render_scene pass,两者都具有相同的关联阴影节点:
- 一个用于不透明几何。
- 另一个是透明几何形状,
如果使用SHADOW_NODE_FIRST_ONLY,则在执行第一次传递(不透明几何)时,Ogre将首先执行阴影节点,更新阴影贴图;,然后渲染不透明的几何图形。
当执行第二次传递(透明几何)时,阴影节点将不会执行,因为阴影贴图应该是最新的;因此,透明几何图形将重用结果。
另一个示例:假设用户有三个pass:
- 一个用于不透明几何。
- 另一个是反射,从不同的相机看到。
- 透明几何体的最后一个pass,使用与不透明几何体相同的照相机进行渲染。
如果使用SHADOW_NODE_FIRST_ONLY;阴影节点将在不透明几何通过之前执行。
然后反射的通过来了。它使用不同的摄像机,这意味着可能会有一组不同的灯光将用于阴影投射(因为某些technique将阴影摄像机相对于渲染摄像机设置以获得最佳质量,pssm分割变得过时,一些灯光更接近此摄像机而不是玩家的摄像机,等等)。 Ogre别无选择,只能重新计算并再次执行影子节点,更新阴影映射。
当第三次pass开始时,摄像机再次改变;因此,我们需要执行影子节点…再一次!
Ogre 会在检测到次优的合成器设置(如此设置)时记录警告。更具体地说,Ogre 检测到第 3次pass使用的结果与第1 次传递相同的结果,但影子节点被迫在第三次传递中重新计算,而不是重用。
有几种方法可以解决此问题:
- **首先渲染反射:**这也许是最明显的一个。如果没有数据依赖关系;首先执行反射pass,然后执行不透明和透明pass;因此阴影节点执行两次而不是三次。
- **使用两个阴影节点:**当第一个选项不可行时(即存在数据依赖关系),使用两个阴影节点将保证结果不会被覆盖。但是,此选项需要更多的 VRAM。
- **在反射render_scenepass中使用SHADOW_NODE_REUSE:**这将强制 Ogre 不执行影子节点。这假设您知道自己在做什么,否则您可能会遇到故障(即,pssm分割在具有不同位置的相机上无法完全使用)。但是,如果您希望在正在使用的光源列表中保持一致性,这将非常有用(因为重新计算可能会导致将不同的光源集用于阴影贴图,因为它取决于与活动摄像机的接近程度)。强制重用的另一个原因可能是性能:影子节点只执行一次。
"SHADOW_NODE_RECALCULATE"设置迫使ogre总是重新计算。如果 Ogre 检测到您的节点设置由于使用SHADOW_NODE_RECALCULATE而变得次优,则不会发出警告。
只有当应用程序在 Ogre 无法检测到的pass之间对摄像机进行相关更改(即通过侦听器更改位置或方向)时,强制重新计算才有意义。
4.2.5 阴影映射设置类型
Ogre 支持 5 种深度阴影映射技术。虽然它们与Ogre 1.4或更早版本一样古老,但它们从未在手册中被提及,并且假设读者对原始论文非常熟悉,那么doxygen文档也非常隐晦。这里解释了每种技术。
4.2.5.1统一阴影映射
最古老的阴影映射形式,也是最简单的一种。这是非常基本的,因此可能没有故障。但是,即使在高分辨率下,它的质量也非常糟糕。
用户需要调用 SceneManager::setShadowDirectionalLightExtrusionDistance & SceneManager::getShadowFarDistance 让 Ogre 知道定向光应该离相机多远(因为理论上它们是无限远的)。如果值太低,则不会包含某些投射器,因此不会投射阴影。太高会导致质量会迅速下降。
很可能只对测试着色器是否正常工作有用,并且阴影未正确显示不是ogre错误或场景(即具有无限abbs的交互可能会给聚焦技术带来麻烦)。
4.2.5.2 聚焦
在均匀阴影映射上改进的形式。该技术使用AABB封闭所有投射器,AABB封闭当前摄像机和摄像机视锥体可见的所有接收器来构建hull(这是所有三者的交叉点,也称为"交叉体B")。利用这个主要的信息,聚焦阴影映射能够推断出最佳的拉伸距离(不需要像在均匀的阴影映射中那样设置它),并创建一个更紧密的近远平面,从而产生更优越的质量。
SceneManager::getShadowFarDistance 仍在使用,它可以引起重大的质量改进,因为用于构建hull的摄像机视锥体首先在阴影远处剪切(而不是使用摄像机的远平面)
大多数情况下,这是常规阴影映射的最佳选择之一。
4.2.5.3垃圾信息
LispSM 继承自聚焦阴影相机设置,因此适用于聚焦阴影贴图的所有内容也适用于 LispSM。
LispSM 更进一步,修改投影矩阵,对渲染的图像应用一些"倾斜"。这样可以改善靠近相机的阴影质量,但可能会引入一些伪像。optimal_adjust_factor可以控制歪斜的强度,从而在伪影和质量之间实现融合。
注意:在撰写本文时,lispsm技术已损坏并且具有可怕的伪影。这个错误似乎影响了Ogre 1.9和2.0。
4.2.5.4PSSM / CSM
PSSM 代表 并行拆分阴影映射,又名。级联阴影贴图。
阴影贴图分为"级联"或"分裂";以提高质量。因此,用户不是每盏灯获得一个RTT,而是每盏灯获得多个RTT。通常,相机空间中的深度是决定使用哪种级联/分割的因素。
Ogre 的 PSSM 继承自 LispSM,因此对每个拆分都有单独的 lispsm 设置。由于导致 LispSM 充满强伪影的错误,建议用户将所有拆分的optimal_adjust_factor设置为负值,从而导致 PSSM 使用集中设置。
互联网上有很多关于PSSM / CSM的资源:
最初的技术由Fan Zhang, Hanqiu Sun, Leilei Xu & Lee Kit Lun 介绍。
4.2.5.5平面最优
待定
4.2.6 编写着色器
编写必要的着色器以获得深度阴影映射工作可能很困难,因为需要考虑的因素很多,而且 Ogre 提供了灵活性。
通常最好使用着色器生成器或不太灵活但允许更容易设置阴影贴图的材质系统,如RTSS或Shiny
话虽如此,为了使阴影映射着色器工作,以下清单将派上用场:
编写接收器的顶点和像素着色器,以使用深度阴影映射 | |
材质使用右对接收器顶点着色器和施法器顶点着色器 | |
Caster 顶点着色器的数学与接收器顶点着色器的数学匹配(即蒙皮、实例化) | |
VTF 实例化:texture_unit设置为对脚轮使用 VTF 纹理。 |
待定
4.3 工作空间
如果不设置工作区,节点是无用的。
工作区定义将使用哪些节点以及如何连接它们。他们还需要声明全局纹理。声明顺序非常重要。
指定节点的连接时,将自动使用节点。
connect <Node Name 1> [<output ch #>] [<output ch #>] <Node Name 2> [<input ch #>] [<input ch #>]
将节点"节点名称 1"输出pass连接到"节点名称 2"输入pass。这隐含地意味着工作区将使用和执行"节点名称 1"和"节点名称 2"(即使它们是隔离的并且永远不会到达屏幕)
- <Node Name 1>
将在"节点名称2"之前执行的节点的名称 - [<output ch #>] [<output ch #>] … [<output ch #>]
来自"节点名称 1"的输出pass的pass号,该pass将连接到"节点名称 2"。 - <Node Name 2>
将在"节点名称1"之后执行的节点的名称 - [<input ch #>] [<input ch #>] … [<input ch #>]
来自"节点名称 2"的pass号输入将从"节点名称 1"绑定连接的pass。
例子:
//Connect nodeA to nodeB
//A's output channel 0 ==> B's input channel 1
//A's output channel 1 ==> B's input channel 2
//A's output channel 2 ==> B's input channel 0
connect nodeA 0 1 2 nodeB 1 2 0
//Connect nodeA to nodeB
//A's output channel 0 ==> B's input channel 0
//A's output channel 2 ==> B's input channel 1
connect nodeA 0 2 nodeB 0 1
//Connect nodeC to nodeB
//C's output channel 3 ==> B's input channel 1
connect nodeC 3 nodeB 1
并非所有输出pass都必须使用。请记住,如果根本不使用输出,它仍然需要CPU和GPU处理时间。MRT(多渲染目标)纹理设计为通过单个pass。
注意#1!
所有节点都必须连接其输入pass。如果节点具有断开连接的输入pass,工作区将无法初始化并引发警告。
注意#2!
无论声明顺序如何,没有输入pass的节点都将是第一个被执行的节点(但是没有首先声明的输入pass的节点应该在后面声明没有输入pass的节点之前运行)。如果您计划使用全局纹理作为传递信息的手段(通常是一个非常糟糕的主意),请记住这一点。connect_external <external channel #> <input channel #>
将最终的渲染器目标(即渲染窗口)从节点连接到指定的输入pass。将隐式地使用和执行该节点。外部pass #0 中的呈现器目标始终用作target_width、target_width_scaled和基于外部 RTT 的所有其他参数的参考。您可以根据需要多次使用connect_external。外部呈现器目标在初始化工作区时以C++代码形式传递。
工作区可能不使用此变量(尽管相当无意义)
- <external channel #>
外部 UAV 缓冲区的索引传递给 addWorkspace。 - <Node Name 1>
外部 UAV 缓冲区的索引传递给 addWorkspace。 - <input channel #>
节点名称 1中的输入pass的编号。
例子
//Pass the external texture to nodeA through channel #0
connect_external 0 nodeA 0
//Pass the external texture to nodeB through channel #0
connect_external 0 nodeB 0
//Pass a second external texture to nodeB through channel #1
connect_external 1 nodeB 1
connect_output <Node Name> <input channel #>
它与connect_external 0 <Node Name> <input channel #>.
出于兼容性原因和方便性而提供。最初只允许使用一个connect_output,但现在您可以根据需要随时使用它。
alias <Node Name> <Aliased Name>
通常,节点始终被重用。所以,如果节点"A"连接到B和C;D 连接到 A;它总是和我们所说的节点 A 是一样的。该定义仅实例化一次。
但是,在某些情况下,您可能希望拥有同一节点定义的多个实例(即,因为您想要唯一的本地纹理,或者因为您希望在一组不同的节点上重复一个过程),因此这就是节点别名的作用。声明别名后,将使用不同的名称(其别名)实例化节点,并且可以与其建立连接。
- <Node Name>
原始实例的名称 - <Aliased Name>
要提供给此单独实例的别名。别名在整个工作区中必须是唯一的,并且在原始节点定义的名称中也必须是唯一的。
例子
workspace MyWorkspace
{
alias nodeA UniqueNode1 //Instantiate nodeA, calling it UniqueNode1
connect nodeA 0 UniqueNode1 0
connect nodeA 0 nodeB 0
connect UniqueNode1 0 nodeB 1
}
buffer <buffer_name> <num_elements> <bytes_per_element> [target_width] [target_width_scaled] [target_height] [target_height_scaled]
创建uav缓冲区
- <buffer_name>
缓冲区的名称。与纹理不同,没有命名限制(即没有"global_"前缀)。如果节点本地缓冲区和全局缓冲区具有相同的名称,则本地缓冲区优先,并记录警告。 - <num_elements>
uav中的元素数。必须是大于 0 的数字。 - <bytes_per_element>
每个元素的字节数。必须是大于 0 的数字。 - [target_width] [target_height] [target_width_scaled] [target_height_scaled]
它们的工作方式与纹理对应物相同,并且当存在时,将与元素的数量成倍增加。
uav缓冲区的大小计算如下:
finalNumElements = numElements * bytesPerElement;
if( widthFactor > 0 )
finalNumElements *= (widthFactor * width);
if( heightFactor > 0 )
finalNumElements *= (heightFactor * height);
例如,如果你想做512 x高度;只需将数字元素设置为 512 和 target_height 或 target_height_scaled 1。
由于没有像素格式,bytesPerElement 控制此类内容(例如,RGBA8888 为 4 个字节)。
UAV 缓冲区不仅用于存储连续的纹理数据。例如,如果运行收集所有灯光的计算着色器,则应将以下内容存储在 UAV 缓冲区中:
struct Lights
{
float3 position;
float3 direction;
float3 diffuse;
};
RWStructuredBuffer<Lights> myLights;
在这种情况下,numElements = 16意味着我们可以定位到 myLights[15];和每个元素字节 = 36。
bytesPerElement 必须根据 HLSL 规则(4 x 4 x 3 => 4 浮点数 x 大小(浮点数) x 3)来考虑填充。
由于手动计算字节PerElement可能会变得非常棘手(或者可能在运行时动态更改),因此,如果uav是在C++中创建的,并通过connect_buffer_external传递到工作区,则复杂情况是最好的。
为什么考虑使用 UAV 缓冲区进行纹理操作?
常规纹理具有优化的布局,可适应大多数栅格化情况(过滤、拉伸)。通常,这些布局是旋转的或平铺的(即将数据存储为RRRR GGGG BBBB AAAA,或按morton顺序存储像素)。有时它们甚至可能被GPU无损压缩。
当您使用计算着色器(例如,用于后处理)并且不需要筛选时,您的访问模式可能是平面的、线性的和连续的,因此通过使用 UAV 缓冲区可以获得更高的性能。
这不是经验法则。您需要在计算着色器中试验 UAV 纹理和 UAV 缓冲区,以了解什么能为您提供最佳性能。
connect_buffer <Node Name 1> [<output ch #>] [<output ch #>] <Node Name 2> [<input ch #>] [<input ch #>]
与连接完全相同,但它连接uav缓冲区而不是纹理。
例:
//Connect nodeA to nodeB
//A's output channel 0 ==> B's input channel 1
//A's output channel 1 ==> B's input channel 2
//A's output channel 2 ==> B's input channel 0
connect_buffer nodeA 0 1 2 nodeB 1 2 0
//Connect nodeA to nodeB
//A's output channel 0 ==> B's input channel 0
//A's output channel 2 ==> B's input channel 1
connect_buffer nodeA 0 2 nodeB 0 1
//Connect nodeC to nodeB
//C's output channel 3 ==> B's input channel 1
connect_buffer nodeC 3 nodeB 1
connect_buffer_external <external channel #> <Node Name><input channel #>
连接多个外部uav缓冲区。在C++中通过 addWorkspace 实例化工作区时,会提供外部 UAV 缓冲区。
工作区可能不使用此变量(尽管相当无意义)
- <external channel #>
外部 UAV 缓冲区的索引传递给 addWorkspace。 - <Node Name>
将接收外部uav的节点的名称 - <input channel #>
节点名称中的输入pass的编号。
例子
//Pass the external UAV to nodeA through channel #0
connect_buffer_external 0 nodeA 0
//Pass the external UAV to nodeB through channel #0
connect_buffer_external 0 nodeB 0
//Pass a second external UAV to nodeB through channel #1
connect_buffer_external 1 nodeB 1
4.3.1 节点之间的数据依赖关系和循环依赖关系
合成器将根据需要求解数据依赖关系并重新排序节点执行。它还将检测一些循环依赖关系(即节点A连接到A;连接到 B 和连接到 A 的 B) 报告错误并拒绝初始化工作区,但它可能无法检测到更复杂的情况(即节点 A 连接到 B、B 到 C、C 到 D、D 到 B),并且尝试执行可能会导致崩溃或图形故障。
如果您碰巧遇到 Ogre 未报告的循环依赖关系,我们会有兴趣了解更多有关它的信息。您可以向JIRA提交错误报告
4.4 设置代码
4.4.1初始化工作区
要创建工作区,只需使用工作区的名称调用以下函数:
CompositorManager2 *compositorManager = mRoot->getCompositorManager2();
compositorManager->addWorkspace( mSceneMgr, mWindow, mCamera, "MyOwnWorkspace", true );
可以有同一工作区定义的多个工作区实例。如果您尝试渲染到两个或多个不同的RT(即两个渲染窗口,一个渲染窗口和一个屏幕外RTT等),或者如果您想使用完全不同的SceneManagers,这非常有用。
4.4.2 初学者的简单引导
如果您是不想处理合成器节点的用户,或者您是初学者,或者您很着急,那么有一个实用程序功能可以帮助您设置基本工作区和合成器节点来渲染整个场景:
const IdString workspaceName( "MyOwnWorkspace" );
CompositorManager2 *compositorManager = mRoot->getCompositorManager2();
if( !compositorManager->hasWorkspaceDefinition( workspaceName ) )
compositorManager->createBasicWorkspaceDef( workspaceName, ColourValue( 0.6f, 0.0f, 0.6f ) );
compositorManager->addWorkspace( mSceneMgr, mWindow, mCamera, workspaceName, true );
实用程序函数创建的工作空间等效于以下复合器脚本:
compositor_node MyOwnWorkspace_Node
{
in 0 renderwindow
target renderwindow
{
pass clear
{
colour_value 0.6 0 0.6 1
}
pass render_scene
{
rq_first 0
rq_last max
}
}
}
workspace MyOwnWorkspace
{
connect_output MyOwnWorkspace_Node 0
}
4.4.3高级C++用户
高级C++想要直接处理 CompositorManager2 的用户可能会发现本节中的信息很有用。
CompositorManager2 使用一种C++模式,其中存在一个对象定义和一个实例。例如;有一个名为CompositorPassSceneDef 的类和一个名为CompositorPassScene的类。前者是定义,而后者是实例。
所有实例共享相同的定义,并且仅具有对它们的读取访问权限。在存在活动实例时修改共享定义是未定义的,并且可能发生从用户预期到故障、崩溃或内存泄漏的任何情况。只有通过分析代码,才能确定哪些更改可能是"安全的"(例如更改可见性掩码),以及哪些更改需要销毁和重新创建实例。
合成器脚本的语法几乎以 1:1 转换为定义,而不是实例。可能最显着的区别是NodeDef包含CompositorTargetDef,而这些包含CompositorPassDef;而实例、目标和pass连接在一起,因此节点直接包含组合pass。
由于 CompositorManager2 还很新,我们承认对节点(尤其是pass连接)的实时更改处理起来可能有点麻烦,除非销毁所有内容并重新创建它,这对于实时编辑节点来说可能不是最佳的。
我们很乐意在论坛上听到您的开发人员关于实时编辑节点和进一步改进合成器的反馈。
4.5立体和分屏渲染
在 Stereo ala Occulus Rift™(或将屏幕拆分为多个部分以进行多人游戏)的渲染通过引入执行和视口蒙版变得更加容易。
通常,渲染到屏幕的左侧,然后渲染到右侧;您需要创建一个清晰pass来清除整个渲染目标,并且两个passrender_scene具有不同视口设置的pass,每个眼睛一个。
使用执行和视口修饰符蒙版,您不再需要复制要绘制到的每个屏幕区域的传递次数。不过,您必须为每个区域创建一个工作区(即每只眼睛一个工作区)。
4.5.1每个工作空间的偏移和缩放
每个工作空间都包含要应用于每个pass的偏移量和比例。作为Vector4传递给CompositorManager2::addWorkspace。XY 分量包含偏移量,ZW 分量包含刻度。
在每次pass时,其最终视口的计算方式如下:
Real left = mDefinition->mVpLeft + vpModifier.x;
Real top = mDefinition->mVpTop + vpModifier.y;
Real width = mDefinition->mVpWidth * vpModifier.z;
Real height = mDefinition->mVpHeight * vpModifier.w;
这意味着要渲染到左眼,你需要指定Vector4(0.0f,0.0f,0.5f,1.0f),要渲染到右眼,你需要指定Vector4(0.5f,0.0f,0.5f , 1.0f )。
4.5.2视口修饰符掩码
您不希望修饰符影响所有pass。视口修饰符掩码是每次传递的 8 位值,与工作区的掩码一起使用 AND。如果结果不为零,则应用偏移量和小数位数。
例如,您可以将后处理pass应用于整个屏幕,而不仅仅是一只眼睛。
此掩码最常见的用途是清除:GPU 更喜欢一次性清除整个缓冲区,而不是两次部分清除。因此,您可以使用蒙版来防止清除的视口受到影响,并最终影响整个屏幕。
但是仍然存在一个问题:您有两个工作区(每只眼睛一个)。第一个工作区将按预期工作。但是,工作区将再次执行清除,并删除绘制到左眼的内容。执行掩码解决了这个问题。
4.5.3 执行掩码
执行掩码是每次传递的 8 位值,该值与工作区的执行掩码一起使用 AND。当为零时,将跳过pass;当非零时,将执行pass。
继续上一节中的示例,您可以使用执行掩码使清除仅在渲染第一个左眼时执行;并且在渲染右眼时不会执行清晰传递。
再举一个例子,您可以使用两个render_pass来执行浮雕3D,即左眼的红色色调,右眼的青色色调。您可以将视口修饰符蒙版设置为 0,以便它不受工作空间的偏移和比例的影响。但设置执行蒙版,以便仅对左眼的工作空间执行红色色调pass,并且仅对右眼的工作空间执行青色传递。
4.5.4默认值
默认情况下,执行和视口掩码默认为 0xFF,但"clear pass"除外,其中默认值为:
//Override so that it only gets executed on the first execution on the
//whole screen (i.e. clear the whole viewport during the left eye pass)
mExecutionMask = 0x01;
mViewportModifierMask = 0x00;
这假设您的第一个工作区(即左眼/分屏中的第一个玩家)将执行蒙版设置为1;而其他工作区则具有该掩码的第一个未设置位。
使用默认值,以下示例将屏幕拆分为4个用于多人游戏(即马里奥赛车™游戏和类似游戏),或者也可用于在4-POV建模应用程序中进行编辑;并且清除pass将应用于具有第一个工作区的整个屏幕:
for( int i=0; i<4; ++i )
{
Vector4 vpModifier( (i % 2) * 0.5f, (i >> 1) * 0.5f, 0.25f, 0.25f );
m_workspace[i] = mgr->addWorkspace( sceneManager, renderTarget,
playerCam[i], "MainWorkspace", true,
-1, vpModifier,
(1 << i), (1 << i) );
}
- starting_slot + slot = 1 + 0 = 1 ↩︎
- 注意:允许您永久保留显式解析的纹理dirty(即,如果您的主要目的是始终访问fsaa子采样,则从不解析) ↩︎