版本记录
版本号 | 时间 |
---|---|
V1.0 | 2018.10.10 星期三 |
前言
很多做视频和图像的,相信对这个框架都不是很陌生,它渲染高级3D图形,并使用GPU执行数据并行计算。接下来的几篇我们就详细的解析这个框架。感兴趣的看下面几篇文章。
1. Metal框架详细解析(一)—— 基本概览
2. Metal框架详细解析(二) —— 器件和命令(一)
3. Metal框架详细解析(三) —— 渲染简单的2D三角形(一)
4. Metal框架详细解析(四) —— 关于GPU Family 4(一)
5. Metal框架详细解析(五) —— 关于GPU Family 4之关于Imageblocks(二)
6. Metal框架详细解析(六) —— 关于GPU Family 4之关于Tile Shading(三)
7. Metal框架详细解析(七) —— 关于GPU Family 4之关于光栅顺序组(四)
8. Metal框架详细解析(八) —— 关于GPU Family 4之关于增强的MSAA和Imageblock采样覆盖控制(五)
9. Metal框架详细解析(九) —— 关于GPU Family 4之关于线程组共享(六)
10. Metal框架详细解析(十) —— 基本组件(一)
11. Metal框架详细解析(十一) —— 基本组件之器件选择 - 图形渲染的器件选择(二)
12. Metal框架详细解析(十二) —— 基本组件之器件选择 - 计算处理的设备选择(三)
13. Metal框架详细解析(十三) —— 计算处理(一)
14. Metal框架详细解析(十四) —— 计算处理之你好,计算(二)
15. Metal框架详细解析(十五) —— 计算处理之关于线程和线程组(三)
16. Metal框架详细解析(十六) —— 计算处理之计算线程组和网格大小(四)
17. Metal框架详细解析(十七) —— 工具、分析和调试(一)
18. Metal框架详细解析(十八) —— 工具、分析和调试之Metal GPU Capture(二)
19. Metal框架详细解析(十九) —— 工具、分析和调试之GPU活动监视器(三)
20. Metal框架详细解析(二十) —— 工具、分析和调试之关于Metal着色语言文件名扩展名、使用Metal的命令行工具构建库和标记Metal对象和命令(四)
21. Metal框架详细解析(二十一) —— 基本课程之基本缓冲区(一)
22. Metal框架详细解析(二十二) —— 基本课程之基本纹理(二)
23. Metal框架详细解析(二十三) —— 基本课程之CPU和GPU同步(三)
24. Metal框架详细解析(二十四) —— 基本课程之参数缓冲 - 基本参数缓冲(四)
25. Metal框架详细解析(二十五) —— 基本课程之参数缓冲 - 带有数组和资源堆的参数缓冲区(五)
26. Metal框架详细解析(二十六) —— 基本课程之参数缓冲 - 具有GPU编码的参数缓冲区(六)
27. Metal框架详细解析(二十七) —— 高级技术之图层选择的反射(一)
28. Metal框架详细解析(二十八) —— 高级技术之使用专用函数的LOD(一)
29. Metal框架详细解析(二十九) —— 高级技术之具有参数缓冲区的动态地形(一)
Deferred Lighting - 延迟照明
演示如何实现利用独特Metal功能的延迟照明渲染器。
此示例演示了延迟照明渲染器,该渲染器使用阴影贴图实现阴影,并使用模板缓冲区剔除光量。
与前向照明相比,延迟照明可以更容易地渲染大量灯光。 例如,对于前向照明,在具有许多光源的场景中,每个片段计算每个光的贡献是不可行的。 必须实施复杂的排序和分级算法,以限制仅对影响每个片段的那些光的光贡献的计算。 通过延迟照明,可以轻松地将多个灯光应用于场景。
Review Important Concepts - 回顾重要概念
在开始使用示例应用程序之前,请查看这些概念,以便更好地了解延迟照明渲染器的关键细节以及一些独特的Metal功能。
1. Traditional Deferred Lighting Renderer - 传统的延迟照明渲染器
传统的延迟光照渲染器通常分为两个渲染过程:
First pass: G-buffer rendering - 第一遍:G缓冲区渲染。渲染器绘制并转换场景的模型,片段函数将结果渲染为称为几何缓冲区或G缓冲区
(geometry buffer or G-buffer)
的纹理集合。 G缓冲区包含模型中的材质颜色,以及每个片段的法线,阴影和深度值。Second pass: Deferred lighting and composition - 第二关:延迟照明和构图。渲染器绘制每个光量,使用G缓冲区数据重建每个片段的位置并应用光照计算。在绘制灯光时,每个灯光的输出会混合在之前的灯光输出之上。最后,渲染器通过执行全屏四边形或计算内核将其他数据(例如阴影和定向照明)合成到场景上。
注意:
macOS GPU
具有立即模式渲染(IMR)
架构。 在IMR GPU上,延迟照明渲染器只能在至少两个渲染过程中实现。 因此,该示例为应用程序的macOS版本实现了two-pass
延迟照明算法。
2. Single-Pass Deferred Lighting on iOS and tvOS GPUs - iOS和tvOS GPU上的单通道延迟照明
iOS和tvOS GPU具有基于图块的延迟渲染(TBDR)
架构,允许它们将数据渲染到GPU内的tile
内存。通过渲染到tile
内存,该设备避免了GPU和系统存储器之间潜在的昂贵的往返(通过带宽受限的存储器总线)。 GPU是否将tile
内存写入系统内存取决于以下配置:
- 应用程序渲染命令编码器的存储操作。
- 应用程序纹理的存储模式。
当MTLStoreActionStore
设置为存储操作时,渲染过程的渲染目标的输出数据将从tile内存写入系统内存,其中渲染目标由纹理支持。如果此数据随后用于后续渲染过程,则将来自这些纹理的输入数据从系统存储器读取到GPU中的纹理高速缓存中。因此,访问系统存储器的传统延迟照明渲染器要求G缓冲区数据在第一和第二渲染通道之间存储在系统存储器中。
但是,由于其TBDR
架构,iOS和tvOS GPU也可以在任何给定时间从tile
内存中读取数据。这允许片段着色器在将此数据再次写入tile内存之前从tile内存中的渲染目标读取并执行计算。此功能允许样本避免在第一次和第二次渲染过程之间将G缓冲区数据存储在系统内存中;因此,延迟的照明渲染器可以用单个渲染过程实现。
G缓冲区数据由单个渲染过程中的GPU(而不是CPU)专门生成和使用。因此,在渲染过程开始之前,不会从系统内存加载此数据,也不会在渲染过程完成后将其存储在系统内存中。光照片段不是从系统存储器中的纹理读取G缓冲区数据,而是从G缓冲区读取数据,同时它仍然作为渲染目标附加到渲染通道。因此,不需要为G缓冲区纹理分配系统内存,并且可以使用MTLStorageModeMemoryless
存储模式声明这些纹理中的每一个。
注意:允许
TBDR GPU
从片段函数中的附加渲染目标读取的功能也称为可编程混合(programmable blending)
。
3. Deferred Lighting with Raster Order Groups - 使用光栅顺序组的延迟照明
默认情况下,当片段着色器将数据写入像素时,GPU会等待着色器完全写入该像素,然后再开始执行该像素的另一个片段着色器。
光栅顺序组允许应用程序增加GPU片段着色器的并行化。 对于栅格顺序组,片段函数可以将渲染目标分成不同的执行组。 这种分离允许GPU在片段着色器的前一个实例完成将数据写入另一个组中的像素之前,从一个组中的渲染目标读取并执行计算。
在此示例中,某些光照片段函数使用以下栅格顺序组:
Raster order group 0 - 光栅顺序组0。
AAPLLightingROG
用于包含光照计算结果的渲染目标。Raster order group 1 - 光栅顺序组1。
AAPLGBufferROG
用于照明函数中的G缓冲区数据。
这些栅格顺序组允许GPU在片段着色器中读取G缓冲区并执行光照计算,然后从片段着色器的先前实例的光照计算完成写入其输出数据。
Render a Deferred Lighting Frame - 渲染延迟照明帧
该示例通过按以下顺序呈现这些阶段来呈现每个完整帧:
Shadow map
G-buffer
Directional light
Light mask
Point lights
Skybox
Fairy lights
示例的iOS和tvOS渲染器生成G缓冲区,并在单个渲染过程中执行所有后续阶段。 由于iOS和tvOS GPU的TBDR
架构,这种单通道实现是可能的,它允许设备从tile
存储器中的渲染目标读取G缓冲区数据。
id <MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:_viewRenderPassDescriptor];
[super drawGBuffer:renderEncoder];
[self drawDirectionalLight:renderEncoder];
[super drawPointLightMask:renderEncoder];
[self drawPointLights:renderEncoder];
[super drawSky:renderEncoder];
[super drawFairies:renderEncoder];
[renderEncoder endEncoding];
示例的macOS
渲染器在一个渲染过程中生成G缓冲区,然后在另一个渲染过程中执行所有后续阶段。 由于macOS GPU
的IMR
架构,这种双通道实现是必要的,这需要设备从视频存储器中的纹理中采样G缓冲区数据。
id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:_GBufferRenderPassDescriptor];
[super drawGBuffer:renderEncoder];
[renderEncoder endEncoding];
id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:_finalRenderPassDescriptor];
[self drawDirectionalLight:renderEncoder];
[super drawPointLightMask:renderEncoder];
[self drawPointLights:renderEncoder];
[super drawSky:renderEncoder];
[super drawFairies:renderEncoder];
[renderEncoder endEncoding];
Render the Shadow Map - 渲染阴影贴图
该示例通过从灯光的角度渲染模型,为场景中的单个方向光(太阳)渲染阴影贴图。
阴影贴图的渲染管道具有顶点函数但不具有片段函数;因此,示例可以确定写入阴影贴图的屏幕空间深度值,而无需执行渲染管线的其他阶段。 (另外,因为渲染管道没有片段函数,所以它的执行速度比没有片段函数时要快得多。)
MTLRenderPipelineDescriptor *renderPipelineDescriptor = [MTLRenderPipelineDescriptor new];
renderPipelineDescriptor.label = @"Shadow Gen";
renderPipelineDescriptor.vertexDescriptor = nil;
renderPipelineDescriptor.vertexFunction = shadowVertexFunction;
renderPipelineDescriptor.fragmentFunction = nil;
renderPipelineDescriptor.depthAttachmentPixelFormat = MTLPixelFormatDepth32Float;
_shadowGenPipelineState = [_device newRenderPipelineStateWithDescriptor:renderPipelineDescriptor
error:&error];
在为阴影贴图绘制几何图形之前,示例会设置深度偏差值以减少阴影瑕疵。
[encoder setDepthBias:0.015 slopeScale:7 clamp:0.02];
然后,在G缓冲阶段的片段函数中,示例测试片段是否被遮挡和阴影。
float shadow_sample = shadowMap.sample_compare(shadowSampler, in.shadow_coord.xy, in.shadow_coord.z);
该示例将sample_compare
函数的结果存储在normal_shadow
渲染目标的w
组件中。
gBuffer.normal_shadow = half4(eye_normal.xyz, shadow_sample);
在定向光和点光组成阶段,示例从G缓冲区读取阴影值并将其应用于片段。
Render the G-Buffer - 渲染G缓冲区
示例的G缓冲区包含以下纹理:
-
albedo_specular_GBuffer
,用于存储反照率和镜面反射数据。 反照率数据存储在x
,y
和z
分量中;镜面数据存储在w
组件中。 -
normal_shadow_GBuffer
,存储正常和阴影数据。 普通数据存储在x,y和z分量中;阴影数据存储在w组件中。 -
depth_GBuffer
,用于存储眼睛空间中的深度值。
当示例呈现G缓冲区时,iOS和tvOS渲染器以及macOS渲染器将所有G缓冲区纹理附加为渲染过程的渲染目标。 但是,由于iOS和tvOS设备都可以渲染G缓冲区并在单个渲染过程中从中读取,因此该示例使用无记忆存储模式创建iOS和tvOS纹理,这表明系统内存未分配给这些纹理。 相反,在渲染过程的持续时间内,这些纹理仅在tile内存中分配和填充。
该示例在常见的drawableSizeWillChange:withGBufferStorageMode:
方法中创建G缓冲区纹理,但iOS和tvOS渲染器将storageMode
变量设置为MTLStorageModeMemoryless
,而macOS
渲染器将其设置为MTLStorageModePrivate
。
GBufferTextureDesc.storageMode = storageMode;
GBufferTextureDesc.pixelFormat = _albedo_specular_GBufferFormat;
_albedo_specular_GBuffer = [_device newTextureWithDescriptor:GBufferTextureDesc];
GBufferTextureDesc.pixelFormat = _normal_shadow_GBufferFormat;
_normal_shadow_GBuffer = [_device newTextureWithDescriptor:GBufferTextureDesc];
GBufferTextureDesc.pixelFormat = _depth_GBufferFormat;
_depth_GBuffer = [_device newTextureWithDescriptor:GBufferTextureDesc];
对于macOS
渲染器,在示例完成将数据写入G缓冲区纹理后,它会调用endEncoding方法来完成G缓冲区渲染过程。 由于渲染命令编码器的存储操作设置为MTLStoreActionStore
,因此当编码器完成执行时,GPU会将每个渲染目标纹理写入视频内存。 这允许样本在随后的延迟光照和合成渲染过程中从视频存储器中读取这些纹理。
对于iOS和tvOS渲染器,在示例完成将数据写入G缓冲区纹理后,示例不会最终确定渲染命令编码器,而是继续将其用于后续阶段。
Apply the Directional Lighting and Shadows - 应用定向光照和阴影
该示例将定向光照和阴影应用于指向显示的drawable
。
macOS
渲染器从设置为片段函数参数的纹理中读取G缓冲区数据。
fragment half4
deferred_directional_lighting_fragment(QuadInOut in [[ stage_in ]],
constant AAPLUniforms & uniforms [[ buffer(AAPLBufferIndexUniforms) ]],
texture2d<half> albedo_specular_GBuffer [[ texture(AAPLRenderTargetAlbedo) ]],
texture2d<half> normal_shadow_GBuffer [[ texture(AAPLRenderTargetNormal) ]],
texture2d<float> depth_GBuffer [[ texture(AAPLRenderTargetDepth) ]])
iOS和tvOS渲染器从附加到渲染过程的渲染目标中读取G缓冲区数据。
struct GBufferData
{
half4 lighting [[color(AAPLRenderTargetLighting), raster_order_group(AAPLLightingROG)]];
half4 albedo_specular [[color(AAPLRenderTargetAlbedo), raster_order_group(AAPLGBufferROG)]];
half4 normal_shadow [[color(AAPLRenderTargetNormal), raster_order_group(AAPLGBufferROG)]];
float depth [[color(AAPLRenderTargetDepth), raster_order_group(AAPLGBufferROG)]];
};
fragment AccumLightBuffer
deferred_directional_lighting_fragment(QuadInOut in [[ stage_in ]],
constant AAPLUniforms & uniforms [[ buffer(AAPLBufferIndexUniforms) ]],
GBufferData GBuffer)
虽然这些片段函数具有不同的输入,但它们在deferred_directional_lighting_fragment_common
片段函数中共享一个共同的实现。 此函数执行以下操作:
- 从G缓冲区正常数据重建法线以计算扩散项。
- 从G缓冲深度数据重建眼睛空间位置以应用镜面高光。
- 使用G缓冲区阴影数据使片段变暗并将阴影应用于场景。
因为这是渲染到drawable
的第一个阶段,iOS和tvOS渲染器在早期G缓冲阶段之前获得一个drawable
,以便drawable
可以与后面阶段的输出合并。 然而,macOS
渲染器延迟获得可绘制直到G缓冲阶段完成之后和定向光阶段开始之前。 此延迟减少了应用程序保留在drawable
上的时间,从而提高了性能。
注意:由于
_directionLightDepthStencilState
的状态,deferred_directional_lighting_fragment
函数仅对应该点亮的片段执行。 此优化很简单但很重要,并且可以节省许多片段着色器执行周期。
Cull the Light Volumes
该示例创建了一个模板掩码,用于避免对许多片段执行昂贵的光照计算。 它通过使用来自G缓冲区传递的深度缓冲区和模板缓冲区来创建此模板蒙版,以跟踪光量是否与任何几何体相交。 (如果没有,那么它就不会对任何东西施光。)
在drawPointLightMask:
实现中,该示例设置_lightMaskPipelineState
渲染管道并对实例化绘制调用进行编码,以仅绘制二十面体的背面,其包含点光源的体积。 如果此绘制调用中的片段未通过深度测试,则此结果表明二十面体的背面位于某些几何体后面。
[renderEncoder setRenderPipelineState:_lightMaskPipelineState];
[renderEncoder setDepthStencilState:_lightMaskDepthStencilState];
[renderEncoder setStencilReferenceValue:128];
[renderEncoder setCullMode:MTLCullModeFront];
[renderEncoder setVertexBuffer:self.uniformBuffers[self.currentBufferIndex] offset:0 atIndex:AAPLBufferIndexUniforms];
[renderEncoder setFragmentBuffer:self.uniformBuffers[self.currentBufferIndex] offset:0 atIndex:AAPLBufferIndexUniforms];
[renderEncoder setVertexBuffer:self.lightsData offset:0 atIndex:AAPLBufferIndexLightsData];
[renderEncoder setVertexBuffer:self.lightPositions[self.currentBufferIndex] offset:0 atIndex:AAPLBufferIndexLightsPosition];
MTKMeshBuffer *vertexBuffer = self.icosahedronMesh.vertexBuffers[AAPLBufferIndexMeshPositions];
[renderEncoder setVertexBuffer:vertexBuffer.buffer offset:vertexBuffer.offset atIndex:AAPLBufferIndexMeshPositions];
MTKSubmesh *icosahedronSubmesh = self.icosahedronMesh.submeshes[0];
[renderEncoder drawIndexedPrimitives:icosahedronSubmesh.primitiveType
indexCount:icosahedronSubmesh.indexCount
indexType:icosahedronSubmesh.indexType
indexBuffer:icosahedronSubmesh.indexBuffer.buffer
indexBufferOffset:icosahedronSubmesh.indexBuffer.offset
instanceCount:AAPLNumLights];
_lightMaskPipelineState
没有片段函数,因此不会从此渲染管道中写入颜色数据。但是,由于set _lightMaskDepthStencilState
深度和模板状态,任何未通过深度测试的片段都会增加该片段的模板缓冲区。包含几何体的片段的起始深度值为128,样本在G缓冲阶段中设置。因此,设置_lightMaskDepthStencilState
时未通过深度测试的任何片段会将深度值增加到大于128。(因为正面剔除已启用,未通过深度测试并且值大于128的片段表示至少返回二十面体的一半落后于所有几何体。)
在下一个绘制调用中,在drawPointLightsCommon
实现中,该示例将点光源的贡献应用于drawable
。该示例测试二十面体的前半部分是否在所有几何体的前面,这确定体积是否与某些几何体相交,因此是否应该点亮该片段。如果片段的模板值大于参考值128,则为此绘制调用设置的深度和模板状态_pointLightDepthStencilState
仅执行片段函数。(因为模板测试值设置为MTLCompareFunctionLess
,所以示例通过测试仅当参考值128小于模板缓冲区中的值时。)
[renderEncoder setDepthStencilState:_pointLightDepthStencilState];
[renderEncoder setStencilReferenceValue:128];
[renderEncoder setCullMode:MTLCullModeBack];
[renderEncoder setVertexBuffer:self.uniformBuffers[self.currentBufferIndex] offset:0 atIndex:AAPLBufferIndexUniforms];
[renderEncoder setVertexBuffer:self.lightsData offset:0 atIndex:AAPLBufferIndexLightsData];
[renderEncoder setVertexBuffer:self.lightPositions[self.currentBufferIndex] offset:0 atIndex:AAPLBufferIndexLightsPosition];
[renderEncoder setFragmentBuffer:self.uniformBuffers[self.currentBufferIndex] offset:0 atIndex:AAPLBufferIndexUniforms];
[renderEncoder setFragmentBuffer:self.lightsData offset:0 atIndex:AAPLBufferIndexLightsData];
[renderEncoder setFragmentBuffer:self.lightPositions[self.currentBufferIndex] offset:0 atIndex:AAPLBufferIndexLightsPosition];
MTKMeshBuffer *vertexBuffer = self.icosahedronMesh.vertexBuffers[AAPLBufferIndexMeshPositions];
[renderEncoder setVertexBuffer:vertexBuffer.buffer offset:vertexBuffer.offset atIndex:AAPLBufferIndexMeshPositions];
MTKSubmesh *icosahedronSubmesh = self.icosahedronMesh.submeshes[0];
[renderEncoder drawIndexedPrimitives:icosahedronSubmesh.primitiveType
indexCount:icosahedronSubmesh.indexCount
indexType:icosahedronSubmesh.indexType
indexBuffer:icosahedronSubmesh.indexBuffer.buffer
indexBufferOffset:icosahedronSubmesh.indexBuffer.offset
instanceCount:AAPLNumLights];
因为drawPointLightMask
中的绘制调用会增加任何几何体后面的片段的模板值,所以样本执行片段函数的唯一片段是满足以下两个条件的片段:
- 正面通过深度测试并位于某些几何体前面的碎片。
- 背面未通过深度测试且位于某些几何体后面的碎片。
下图显示了使用此模板掩码算法的渲染帧与不使用此模板掩码算法的渲染帧之间的片段覆盖范围的差异。 当启用算法时,绿色像素是执行点光片段功能的像素。
当禁用该算法时,绿色和红色的像素是执行点光片段功能的像素。
Render the Skybox and Fairy Lights - 渲染Skybox 和 Fairy Lights
在最后的照明阶段,示例将更简单的照明技术应用于场景。
该示例将深度测试应用于天空盒,与模型的几何体相对应,因此渲染器仅渲染到未被某些几何体填充的可绘制区域。
[renderEncoder setRenderPipelineState:_skyboxPipelineState];
[renderEncoder setDepthStencilState:_dontWriteDepthStencilState];
[renderEncoder setCullMode:MTLCullModeFront];
[renderEncoder setVertexBuffer:_uniformBuffers[_currentBufferIndex] offset:0 atIndex:AAPLBufferIndexUniforms];
[renderEncoder setFragmentTexture:_skyMap atIndex:AAPLTextureIndexBaseColor];
// Set mesh's vertex buffers
for (NSUInteger bufferIndex = 0; bufferIndex < _skyMesh.vertexBuffers.count; bufferIndex++)
{
__unsafe_unretained MTKMeshBuffer *vertexBuffer = _skyMesh.vertexBuffers[bufferIndex];
if((NSNull*)vertexBuffer != [NSNull null])
{
[renderEncoder setVertexBuffer:vertexBuffer.buffer
offset:vertexBuffer.offset
atIndex:bufferIndex];
}
}
MTKSubmesh *sphereSubmesh = _skyMesh.submeshes[0];
[renderEncoder drawIndexedPrimitives:sphereSubmesh.primitiveType
indexCount:sphereSubmesh.indexCount
indexType:sphereSubmesh.indexType
indexBuffer:sphereSubmesh.indexBuffer.buffer
indexBufferOffset:sphereSubmesh.indexBuffer.offset];
该示例将fairy lights
渲染到drawable
上作为2D圆圈,并使用纹理来确定其片段的alpha混合因子。
half4 c = colorMap.sample(linearSampler, float2(in.tex_coord));
half3 fragColor = in.color * c.x;
return half4(fragColor, c.x);
后记
本篇主要讲述了延迟照明,感兴趣的给个赞或者关注~~~