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

版本記錄

版本號 時間
V1.0 2018.10.10 星期三

前言

很多做視頻和圖像的,相信對這個框架都不是很陌生,它渲染高級3D圖形,并使用GPU執(zhí)行數(shù)據(jù)并行計算。接下來的幾篇我們就詳細的解析這個框架。感興趣的看下面幾篇文章。
1. Metal框架詳細解析(一)—— 基本概覽
2. Metal框架詳細解析(二) —— 器件和命令(一)
3. Metal框架詳細解析(三) —— 渲染簡單的2D三角形(一)
4. Metal框架詳細解析(四) —— 關(guān)于GPU Family 4(一)
5. Metal框架詳細解析(五) —— 關(guān)于GPU Family 4之關(guān)于Imageblocks(二)
6. Metal框架詳細解析(六) —— 關(guān)于GPU Family 4之關(guān)于Tile Shading(三)
7. Metal框架詳細解析(七) —— 關(guān)于GPU Family 4之關(guān)于光柵順序組(四)
8. Metal框架詳細解析(八) —— 關(guān)于GPU Family 4之關(guān)于增強的MSAA和Imageblock采樣覆蓋控制(五)
9. Metal框架詳細解析(九) —— 關(guān)于GPU Family 4之關(guān)于線程組共享(六)
10. Metal框架詳細解析(十) —— 基本組件(一)
11. Metal框架詳細解析(十一) —— 基本組件之器件選擇 - 圖形渲染的器件選擇(二)
12. Metal框架詳細解析(十二) —— 基本組件之器件選擇 - 計算處理的設(shè)備選擇(三)
13. Metal框架詳細解析(十三) —— 計算處理(一)
14. Metal框架詳細解析(十四) —— 計算處理之你好,計算(二)
15. Metal框架詳細解析(十五) —— 計算處理之關(guān)于線程和線程組(三)
16. Metal框架詳細解析(十六) —— 計算處理之計算線程組和網(wǎng)格大小(四)
17. Metal框架詳細解析(十七) —— 工具、分析和調(diào)試(一)
18. Metal框架詳細解析(十八) —— 工具、分析和調(diào)試之Metal GPU Capture(二)
19. Metal框架詳細解析(十九) —— 工具、分析和調(diào)試之GPU活動監(jiān)視器(三)
20. Metal框架詳細解析(二十) —— 工具、分析和調(diào)試之關(guān)于Metal著色語言文件名擴展名、使用Metal的命令行工具構(gòu)建庫和標記Metal對象和命令(四)
21. Metal框架詳細解析(二十一) —— 基本課程之基本緩沖區(qū)(一)
22. Metal框架詳細解析(二十二) —— 基本課程之基本紋理(二)
23. Metal框架詳細解析(二十三) —— 基本課程之CPU和GPU同步(三)
24. Metal框架詳細解析(二十四) —— 基本課程之參數(shù)緩沖 - 基本參數(shù)緩沖(四)
25. Metal框架詳細解析(二十五) —— 基本課程之參數(shù)緩沖 - 帶有數(shù)組和資源堆的參數(shù)緩沖區(qū)(五)
26. Metal框架詳細解析(二十六) —— 基本課程之參數(shù)緩沖 - 具有GPU編碼的參數(shù)緩沖區(qū)(六)
27. Metal框架詳細解析(二十七) —— 高級技術(shù)之圖層選擇的反射(一)

Overview - 概覽

演示如何使用專用函數(shù)根據(jù)動態(tài)條件選擇細節(jié)級別。

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

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

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


GPU Branch Statements - GPU分支聲明

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

傳統(tǒng)上,繪制調(diào)用之間不同的分支可以通過以下方式之一進行緩解:

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

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

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


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

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

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

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

+ (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 - 實施專業(yè)函數(shù)

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

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) ]];

該示例還聲明了一個派生函數(shù)常量has_any_map,它在vertexTransform頂點函數(shù)中使用。 此值確定渲染管道是否需要頂點函數(shù)將紋理坐標輸出到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時,頂點函數(shù)不會將值寫入texCoord成員。

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

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

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) ]])

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

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 - 創(chuàng)建不同的管道

此示例使用三個不同的MTLRenderPipelineState對象,每個對象代表不同的LOD。 專用函數(shù)和構(gòu)建管道很昂貴,因此示例在啟動渲染循環(huán)之前異步執(zhí)行這些任務。 初始化AAPLRenderer對象時,將使用調(diào)度組,完成處理程序和通知塊異步創(chuàng)建每個LOD管道。

該示例總共創(chuàng)建了六個專用函數(shù):三個LOD中的每一個都有一個頂點和一個片段函數(shù)。 此任務由specializationGroup調(diào)度組監(jiān)視,并且每個函數(shù)都通過調(diào)用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塊構(gòu)建三個渲染管道。 此任務由_pipelineCreationGroup調(diào)度組監(jiān)視,并且每個管道都是通過調(diào)用newRenderPipelineStateWithDescriptor:completionHandler:方法構(gòu)建的。

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渲染

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

- (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值用于為幀設(shè)置相應的MTLRenderPipelineState對象。

[renderEncoder setRenderPipelineState:_pipelineStates[_currentQualityLevel]];

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

[submesh computeTextureWeightsForQualityLevel:_currentQualityLevel
                          withGlobalMapWeight:_globalMapWeight];

最后,渲染循環(huán)使用特定的LOD管道繪制模型中的每個子網(wǎng)格。

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

后記

本篇主要講述了使用專用函數(shù)的LOD,感興趣的給個贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容