Metal框架詳細解析(二十八) —— 高級技術之使用專用函數的LOD(一)

版本記錄

版本號 時間
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框架詳細解析(二十七) —— 高級技術之圖層選擇的反射(一)

Overview - 概覽

演示如何使用專用函數根據動態條件選擇細節級別。

高品質的游戲體驗必須在卓越的圖形和卓越的性能之間進行權衡。 高質量的模型看起來很棒,但它們的復雜性需要大量的處理能力。 通過增加或減少模型的細節水平(level of detail - LOD),游戲可以選擇性地管理圖形和性能。

游戲可以在運行時基于某些模型視圖條件在一系列LOD之間動態地選擇,而不是在構建時選擇固定LOD。 例如,焦點前景模型可以具有高LOD,而快速移動的背景模型可以具有低LOD。

此示例演示了消防車模型的動態LOD選擇,基于其與場景攝像機的距離。 當模型離相機較近時,渲染器使用較高的LOD;當模型離相機較遠時,渲染器使用較低的LOD。


GPU Branch Statements - GPU分支聲明

與CPU代碼不同,圖形處理單元(GPU)分支語句(如if和else)非常昂貴。 GPU的大規模并行架構不是特別適合處理具有許多分支的GPU函數。更多分支導致更多的寄存器分配,從而減少可以并發執行的GPU線程的數量。然而,分支語句是有用的編程結構,特別是對于共享大量代碼的函數。實際上,共享代碼的圖形函數的一個常見問題是如何處理僅在繪制調用之間而不是在單個繪制調用中執行的各個線程之間的分支條件。

傳統上,繪制調用之間不同的分支可以通過以下方式之一進行緩解:

  • 編寫每個分支函數。每個分支都寫為完整且獨立的函數,渲染循環確定在運行時使用哪個函數。這種方法極大地增加了代碼重復,因為每個分支條件的所有可能結果都需要它們自己的獨立函數。例如,單個if語句需要一個函數用于true結果,另一個函數用于false結果。

  • 使用預處理程序指令。函數可以使用#if預處理程序指令,而不是使用常規的if語句,該指令在評估其分支條件后有選擇地編譯函數。這種方法避免了代碼重復,但降低了預編譯的Metal著色語言代碼的性能優勢。由于分支條件只能在運行時進行評估,因此無法在構建時預編譯這些函數。

Metal的函數專用功能可降低分支性能成本,避免代碼重復,并利用構建時編譯。 函數專業化允許您創建單個源函數的多個可執行版本。 您可以通過在Metal著色語言代碼中聲明函數常量并在運行時設置它們來創建專用函數。 這樣做允許前端編譯器在構建時預編譯源函數,并允許后端編譯器在創建管道時在運行時編譯專用函數。


Define Your LOD Selection Criteria - 定義您的LOD選擇標準

此示例通過為不同的LOD創建不同的渲染管道來演示函數特化。所有管道共享相同的源函數,但函數常量確定每個管道的LOD特定路徑和輸入。具體而言,該示例演示了消防車模型的動態LOD選擇,基于其與場景攝像機的距離。當消防車靠近相機時,它在屏幕上占據更多像素;因此,該示例使用高質量的渲染管道。當消防車離相機很遠時,它在屏幕上占用的像素較少;因此,該示例使用低質量的渲染管道。

此示例中的消防車模型使用多種類型的紋理,例如反照率,法線,金屬,粗糙度,環境遮擋和輻照度(albedo, normal, metallic, roughness, ambient occlusion, and irradiance)。當模型遠離相機時,從這些紋理中的每一個中采樣都太浪費了,因為沒有看到由完整紋理組合提供的細節。該示例使用各種函數常量值來創建從更多或更少紋理中采樣的特殊函數,具體取決于所選的LOD。此外,從較少紋理中采樣的專用函數也執行較不復雜的計算,從而產生更快的渲染管道。

isTexturedProperty:atQualityLevel:方法控制是通過從紋理采樣還是通過讀取常量值來設置材質屬性。

+ (BOOL)isTexturedProperty:(AAPLFunctionConstant)propertyIndex atQualityLevel:(AAPLQualityLevel)quality
{
    AAPLQualityLevel minLevelForProperty = AAPLQualityLevelHigh;
    
    switch(propertyIndex)
    {
        case AAPLFunctionConstantBaseColorMapIndex:
        case AAPLFunctionConstantIrradianceMapIndex:
            minLevelForProperty = AAPLQualityLevelMedium;
            break;
        default:
            break;
    }
    
    return quality <= minLevelForProperty;
}

Implement Specialized Functions - 實施專業函數

該示例使用六個函數常量來控制fragmentLighting片段函數可用的各種輸入。

constant bool has_base_color_map        [[ function_constant(AAPLFunctionConstantBaseColorMapIndex) ]];
constant bool has_normal_map            [[ function_constant(AAPLFunctionConstantNormalMapIndex) ]];
constant bool has_metallic_map          [[ function_constant(AAPLFunctionConstantMetallicMapIndex) ]];
constant bool has_roughness_map         [[ function_constant(AAPLFunctionConstantRoughnessMapIndex) ]];
constant bool has_ambient_occlusion_map [[ function_constant(AAPLFunctionConstantAmbientOcclusionMapIndex) ]];
constant bool has_irradiance_map        [[ function_constant(AAPLFunctionConstantIrradianceMapIndex) ]];

該示例還聲明了一個派生函數常量has_any_map,它在vertexTransform頂點函數中使用。 此值確定渲染管道是否需要頂點函數將紋理坐標輸出到ColorInOut.texCoord返回值。

constant bool has_any_map = (has_base_color_map        ||
                             has_normal_map            ||
                             has_metallic_map          ||
                             has_roughness_map         ||
                             has_ambient_occlusion_map ||
                             has_irradiance_map);

has_any_map的值為false時,頂點函數不會將值寫入texCoord成員。

if (has_any_map)
{
    out.texCoord = in.texCoord;
}

函數常量控制參數的來源到calculateParameters()函數中的光照計算。 使用[[function_constant(index)]]屬性時,此函數可以確定是否應從紋理中進行采樣。 如果屬性指示存在紋理參數,則該函數僅從紋理中采樣;否則,它從materialUniforms緩沖區讀取一個統一值。

LightingParameters calculateParameters(ColorInOut in,
                                       constant AAPLUniforms         & uniforms,
                                       constant AAPLMaterialUniforms & materialUniforms,
                                       texture2d<float>   baseColorMap        [[ function_constant(has_base_color_map) ]],
                                       texture2d<float>   normalMap           [[ function_constant(has_normal_map) ]],
                                       texture2d<float>   metallicMap         [[ function_constant(has_metallic_map) ]],
                                       texture2d<float>   roughnessMap        [[ function_constant(has_roughness_map) ]],
                                       texture2d<float>   ambientOcclusionMap [[ function_constant(has_ambient_occlusion_map) ]],
                                       texturecube<float> irradianceMap       [[ function_constant(has_irradiance_map) ]])

片段函數的相應輸入也使用相同的函數常量。

fragment float4
fragmentLighting(ColorInOut in [[stage_in]],
                 constant AAPLUniforms         & uniforms         [[ buffer(AAPLBufferIndexUniforms) ]],
                 constant AAPLMaterialUniforms & materialUniforms [[ buffer(AAPLBufferIndexMaterialUniforms) ]],
                 texture2d<float>   baseColorMap         [[ texture(AAPLTextureIndexBaseColor),        function_constant(has_base_color_map) ]],
                 texture2d<float>   normalMap            [[ texture(AAPLTextureIndexNormal),           function_constant(has_normal_map) ]],
                 texture2d<float>   metallicMap          [[ texture(AAPLTextureIndexMetallic),         function_constant(has_metallic_map) ]],
                 texture2d<float>   roughnessMap         [[ texture(AAPLTextureIndexRoughness),        function_constant(has_roughness_map) ]],
                 texture2d<float>   ambientOcclusionMap  [[ texture(AAPLTextureIndexAmbientOcclusion), function_constant(has_ambient_occlusion_map) ]],
                 texturecube<float> irradianceMap        [[ texture(AAPLTextureIndexIrradianceMap),    function_constant(has_irradiance_map)]])

Create Different Pipelines - 創建不同的管道

此示例使用三個不同的MTLRenderPipelineState對象,每個對象代表不同的LOD。 專用函數和構建管道很昂貴,因此示例在啟動渲染循環之前異步執行這些任務。 初始化AAPLRenderer對象時,將使用調度組,完成處理程序和通知塊異步創建每個LOD管道。

該示例總共創建了六個專用函數:三個LOD中的每一個都有一個頂點和一個片段函數。 此任務由specializationGroup調度組監視,并且每個函數都通過調用newFunctionWithName:constantValues:completionHandler:方法來專門化。

for (uint qualityLevel = 0; qualityLevel < AAPLNumQualityLevels; qualityLevel++)
{
    dispatch_group_enter(specializationGroup);

    MTLFunctionConstantValues* constantValues = [self functionConstantsForQualityLevel:qualityLevel];

    [defaultLibrary newFunctionWithName:@"fragmentLighting" constantValues:constantValues
                      completionHandler:^(id<MTLFunction> newFunction, NSError *error )
     {
         if (!newFunction)
         {
             NSLog(@"Failed to specialize function, error %@", error);
         }

         self->_fragmentFunctions[qualityLevel] = newFunction;
         dispatch_group_leave(specializationGroup);
     }];

    dispatch_group_enter(specializationGroup);

    [defaultLibrary newFunctionWithName:@"vertexTransform" constantValues:constantValues
                      completionHandler:^(id<MTLFunction> newFunction, NSError *error )
     {
         if (!newFunction)
         {
             NSLog(@"Failed to specialize function, error %@", error);
         }

         self->_vertexFunctions[qualityLevel] = newFunction;
         dispatch_group_leave(specializationGroup);
     }];
}

notifyBlock塊構建三個渲染管道。 此任務由_pipelineCreationGroup調度組監視,并且每個管道都是通過調用newRenderPipelineStateWithDescriptor:completionHandler:方法構建的。

dispatch_group_enter(_pipelineCreationGroup);

void (^notifyBlock)(void) = ^void()
{
    const id<MTLDevice> device  = self->_device;
    const dispatch_group_t pipelineCreationGroup = self->_pipelineCreationGroup;

    MTLRenderPipelineDescriptor *pipelineStateDescriptors[AAPLNumQualityLevels];

    dispatch_group_wait(specializationGroup, DISPATCH_TIME_FOREVER);

    for (uint qualityLevel = 0; qualityLevel < AAPLNumQualityLevels; qualityLevel++)
    {
        dispatch_group_enter(pipelineCreationGroup);

        pipelineStateDescriptors[qualityLevel] = [pipelineStateDescriptor copy];
        pipelineStateDescriptors[qualityLevel].fragmentFunction = self->_fragmentFunctions[qualityLevel];
        pipelineStateDescriptors[qualityLevel].vertexFunction = self->_vertexFunctions[qualityLevel];

        [device newRenderPipelineStateWithDescriptor:pipelineStateDescriptors[qualityLevel]
                                   completionHandler:^(id<MTLRenderPipelineState> newPipelineState, NSError *error )
         {
             if (!newPipelineState)
                 NSLog(@"Failed to create pipeline state, error %@", error);

             self->_pipelineStates[qualityLevel] = newPipelineState;
             dispatch_group_leave(pipelineCreationGroup);
         }];
    }

    dispatch_group_leave(pipelineCreationGroup);
};

dispatch_group_notify(specializationGroup, pipelineQueue, notifyBlock);

使用特定LOD渲染

在渲染循環的開頭,對于每個幀,該示例調用_calculateQualityAtDistance:方法來更新_currentQualityLevel值。 此值根據模型和相機之間的距離定義框架的LOD。 _calculateQualityAtDistance:方法還設置_globalMapWeight值,以在LOD邊界之間創建平滑過渡。

- (void)calculateQualityAtDistance:(float)distance
{
    static const float MediumQualityDepth     = 150.f;
    static const float LowQualityDepth        = 650.f;
    static const float TransitionDepthAmount  = 50.f;

    assert(distance >= 0.0f);
    if (distance < MediumQualityDepth)
    {
        static const float TransitionDepth = MediumQualityDepth - TransitionDepthAmount;
        if(distance > TransitionDepth)
        {
            _globalMapWeight = distance - TransitionDepth;
            _globalMapWeight /= TransitionDepthAmount;
            _globalMapWeight = 1.0 - _globalMapWeight;
        }
        else
        {
            _globalMapWeight = 1.0;
        }
        _currentQualityLevel = AAPLQualityLevelHigh;
    }
    else if (distance < LowQualityDepth)
    {
        static const float TransitionDepth = LowQualityDepth - TransitionDepthAmount;
        if(distance > TransitionDepth)
        {
            _globalMapWeight = distance - (TransitionDepth);
            _globalMapWeight /= TransitionDepthAmount;
            _globalMapWeight = 1.0 - _globalMapWeight;
        }
        else
        {
            _globalMapWeight = 1.0;
        }
        _currentQualityLevel = AAPLQualityLevelMedium;
    }
    else
    {
        _currentQualityLevel = AAPLQualityLevelLow;
        _globalMapWeight = 0.0;
    }
}

更新的_currentQualityLevel值用于為幀設置相應的MTLRenderPipelineState對象。

[renderEncoder setRenderPipelineState:_pipelineStates[_currentQualityLevel]];

更新后的`_globalMapWeight值用于在質量級別之間進行插值,并防止突然的LOD轉換。

[submesh computeTextureWeightsForQualityLevel:_currentQualityLevel
                          withGlobalMapWeight:_globalMapWeight];

最后,渲染循環使用特定的LOD管道繪制模型中的每個子網格。

[renderEncoder drawIndexedPrimitives:metalKitSubmesh.primitiveType
                          indexCount:metalKitSubmesh.indexCount
                           indexType:metalKitSubmesh.indexType
                         indexBuffer:metalKitSubmesh.indexBuffer.buffer
                   indexBufferOffset:metalKitSubmesh.indexBuffer.offset];

后記

本篇主要講述了使用專用函數的LOD,感興趣的給個贊或者關注~~~

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容