从零开始手敲次世代游戏引擎(七十九)

北城余情 提交于 2020-05-07 16:15:49

一晃又一年过去了。。。

即便这样,如我在系列开头所说,这个系列我是不会放弃的,虽然可能会很慢,但是我会努力继续下去。

因此,今天更新一篇。

其实在这一年当中我对图形部分在不断进行重构。其中一个重点就是对应DX12/Vulkan/Metal的PSO(Pipeline State Object)概念。虽然3个图形API的PSO概念在实现细节方面并不是完全一致,但是大致的概念是共通的:将GPU的管道状态,包括

  1. 渲染管道入口处的,附属在vertex(顶点)处的属性layout;
  2. 渲染管道出口处的RT/DT的格式
  3. 固定功能(Fix Function)的设置,如
    1. 深度测试是否有效,深度测试的比较函数(>, >=, ==, <, <=, always true, always false)
    2. Stencil测试是否有效,相关测试
  4. Shader的绑定

这些主要的概念。

其实,这基本上就对应现代GPGPU当中的Context,也就是上下文的概念。在以往,特别是诸如OpenGL这样的图形API当中,这些状态设置是混杂在绘图命令当中的,并没有单独分开进行存储。这就导致在需要改变渲染管道状态的时候,需要一系列的命令来对状态进行设置,并且:

  1. 要么每次把所有状态都重新设置一遍
  2. 要么需要在引擎当中自己跟踪状态的变化,计算出需要改变哪些设置

而DX12/Vulkan/Metal提供了PSO对象,该对象将渲染管道的状态与绘图指令完全分开,使得我们可以提前创建所需要的各种状态。我们在需要的时候从预先创建的PSO对象当中选出要使用的,只需要一次API的调用就可以完成所有状态的设置。

因此,我将之前的IShaderManager重构成了IPipelineStateManager,并且定义了一个引擎内部的PipelineState,来抽象不同图形API的PSO所需的信息:

#pragma once
#include "IRuntimeModule.hpp"
#include "portable.hpp"
#include "cbuffer.h"
#include <memory>
#include <string>

namespace My {
    ENUM(DEPTH_TEST_MODE)
    {
        NONE,
        LARGE,
        LARGE_EQUAL,
        EQUAL,
        LESS_EQUAL,
        LESS,
        NOT_EQUAL,
        NEVER,
        ALWAYS
    };

    ENUM(STENCIL_TEST_MODE)
    {
        NONE
    };

    ENUM(CULL_FACE_MODE)
    {
        NONE,
        FRONT,
        BACK
    };

    ENUM(PIPELINE_TYPE)
    {
        GRAPHIC,
        COMPUTE
    };

    struct PipelineState
    {
        std::string pipelineStateName;
        PIPELINE_TYPE pipelineType;

        std::string vertexShaderName;
        std::string pixelShaderName;
        std::string computeShaderName;
        std::string geometryShaderName;
        std::string tessControlShaderName;
        std::string tessEvaluateShaderName;
        std::string meshShaderName;

        DEPTH_TEST_MODE depthTestMode;
        bool    bDepthWrite;
        STENCIL_TEST_MODE stencilTestMode;
        CULL_FACE_MODE  cullFaceMode;

        A2V_TYPES a2vType;

        virtual ~PipelineState() = default;
    }; 

    Interface IPipelineStateManager : inheritance IRuntimeModule
    {
    public:
        virtual bool RegisterPipelineState(PipelineState& pipelineState) = 0;
        virtual void UnregisterPipelineState(PipelineState& pipelineState) = 0;
        virtual void Clear() = 0;

        [[nodiscard]] virtual const std::shared_ptr<PipelineState> GetPipelineState(std::string name) const = 0;
    };

    extern IPipelineStateManager* g_pPipelineStateManager;
}

这个工作还在继续,接下来预计还会有很多的变化,比如我准备移除其中的tesselation相关的部分,因为目前最新的趋势说tesselation会被mesh shader取代,而mesh shader今后在各个平台会有比较统一的支持,而不是像tesselation那样,各个平台支持情况不一,各个图形API的支持方法也不一,非常头疼。

从这个接口出发,首先实现出一个名为PipelineStateManager的类,用来实现各个图形API无关的部分,也就是共通的部分:

#include "PipelineStateManager.hpp"

using namespace My;
using namespace std;

#define VS_BASIC_SOURCE_FILE "basic.vert"
#define PS_BASIC_SOURCE_FILE "basic.frag"
#define VS_PBR_SOURCE_FILE "pbr.vert"
#define PS_PBR_SOURCE_FILE "pbr.frag"
#define CS_PBR_BRDF_SOURCE_FILE "integrateBRDF.comp"
#define VS_SHADOWMAP_SOURCE_FILE "shadowmap.vert"
#define PS_SHADOWMAP_SOURCE_FILE "shadowmap.frag"
#define VS_OMNI_SHADOWMAP_SOURCE_FILE "shadowmap_omni.vert"
#define PS_OMNI_SHADOWMAP_SOURCE_FILE "shadowmap_omni.frag"
#define GS_OMNI_SHADOWMAP_SOURCE_FILE "shadowmap_omni.geom"
#define DEBUG_VS_SHADER_SOURCE_FILE "debug.vert"
#define DEBUG_PS_SHADER_SOURCE_FILE "debug.frag"
#define VS_PASSTHROUGH_SOURCE_FILE "passthrough.vert"
#define PS_TEXTURE_SOURCE_FILE "texture.frag"
#define PS_TEXTURE_ARRAY_SOURCE_FILE "texturearray.frag"
#define VS_PASSTHROUGH_CUBEMAP_SOURCE_FILE "passthrough_cube.vert"
#define PS_CUBEMAP_SOURCE_FILE "cubemap.frag"
#define PS_CUBEMAP_ARRAY_SOURCE_FILE "cubemaparray.frag"
#define VS_SKYBOX_SOURCE_FILE "skybox.vert"
#define PS_SKYBOX_SOURCE_FILE "skybox.frag"
#define VS_TERRAIN_SOURCE_FILE "terrain.vert"
#define PS_TERRAIN_SOURCE_FILE "terrain.frag"
#define TESC_TERRAIN_SOURCE_FILE "terrain.tesc"
#define TESE_TERRAIN_SOURCE_FILE "terrain.tese"

PipelineStateManager::~PipelineStateManager()
{
    Clear();
}

bool PipelineStateManager::RegisterPipelineState(PipelineState& pipelineState)
{
    PipelineState* pPipelineState;
    pPipelineState = &pipelineState;
    if (InitializePipelineState(&pPipelineState))
    {
        m_pipelineStates.emplace(pipelineState.pipelineStateName, pPipelineState);
        return true;
    }

    return false;
}

void PipelineStateManager::UnregisterPipelineState(PipelineState& pipelineState)
{
    const auto& it = m_pipelineStates.find(pipelineState.pipelineStateName);
    if (it != m_pipelineStates.end())
    {
        DestroyPipelineState(*it->second);
    }
    m_pipelineStates.erase(it);
}

void PipelineStateManager::Clear()
{
    for (auto it = m_pipelineStates.begin(); it != m_pipelineStates.end(); it++)
    {
        if (it != m_pipelineStates.end())
        {
            DestroyPipelineState(*it->second);
        }
        m_pipelineStates.erase(it);
    }

    assert(m_pipelineStates.empty());
}

const std::shared_ptr<PipelineState> PipelineStateManager::GetPipelineState(std::string name) const
{
    const auto& it = m_pipelineStates.find(name);
    if (it != m_pipelineStates.end())
    {
        return it->second;
    }
    else
    {
        assert(!m_pipelineStates.empty());
        return m_pipelineStates.begin()->second;
    }
}

int PipelineStateManager::Initialize()
{
    PipelineState pipelineState;
    pipelineState.pipelineStateName = "BASIC";
    pipelineState.pipelineType = PIPELINE_TYPE::GRAPHIC;
    pipelineState.vertexShaderName = VS_BASIC_SOURCE_FILE;
    pipelineState.pixelShaderName  = PS_BASIC_SOURCE_FILE;
    pipelineState.depthTestMode = DEPTH_TEST_MODE::LESS_EQUAL;
    pipelineState.stencilTestMode = STENCIL_TEST_MODE::NONE;
    pipelineState.cullFaceMode = CULL_FACE_MODE::BACK;
    pipelineState.a2vType = A2V_TYPES::A2V_TYPES_SIMPLE;
    RegisterPipelineState(pipelineState);

    pipelineState.pipelineStateName = "PBR";
    pipelineState.vertexShaderName = VS_PBR_SOURCE_FILE;
    pipelineState.pixelShaderName  = PS_PBR_SOURCE_FILE;
    pipelineState.a2vType = A2V_TYPES::A2V_TYPES_FULL;
    RegisterPipelineState(pipelineState);

    pipelineState.pipelineStateName = "PBR BRDF CS";
    pipelineState.pipelineType = PIPELINE_TYPE::COMPUTE;
    pipelineState.vertexShaderName.clear();
    pipelineState.pixelShaderName.clear();
    pipelineState.computeShaderName = CS_PBR_BRDF_SOURCE_FILE;
    pipelineState.a2vType = A2V_TYPES::A2V_TYPES_NONE;
    RegisterPipelineState(pipelineState);

    pipelineState.pipelineStateName = "Omni Light Shadow Map";
    pipelineState.pipelineType = PIPELINE_TYPE::GRAPHIC;
    pipelineState.vertexShaderName = VS_OMNI_SHADOWMAP_SOURCE_FILE;
    pipelineState.pixelShaderName = PS_OMNI_SHADOWMAP_SOURCE_FILE;
    pipelineState.geometryShaderName = GS_OMNI_SHADOWMAP_SOURCE_FILE;
    pipelineState.computeShaderName.clear(); 
    pipelineState.cullFaceMode = CULL_FACE_MODE::FRONT;
    pipelineState.a2vType = A2V_TYPES::A2V_TYPES_POS_ONLY;
    RegisterPipelineState(pipelineState);

    pipelineState.pipelineStateName = "Spot Light Shadow Map";
    pipelineState.vertexShaderName = VS_SHADOWMAP_SOURCE_FILE;
    pipelineState.pixelShaderName = PS_SHADOWMAP_SOURCE_FILE;
    pipelineState.geometryShaderName.clear();
    RegisterPipelineState(pipelineState);

    pipelineState.pipelineStateName = "Area Light Shadow Map";
    RegisterPipelineState(pipelineState);

    pipelineState.pipelineStateName = "Sun Light Shadow Map";
    RegisterPipelineState(pipelineState);

    pipelineState.pipelineStateName = "Texture Debug Output";
    pipelineState.vertexShaderName = VS_PASSTHROUGH_SOURCE_FILE;
    pipelineState.pixelShaderName = PS_TEXTURE_SOURCE_FILE;
    pipelineState.cullFaceMode = CULL_FACE_MODE::BACK;
    RegisterPipelineState(pipelineState);

    pipelineState.pipelineStateName = "Texture Array Debug Output";
    pipelineState.vertexShaderName = VS_PASSTHROUGH_SOURCE_FILE;
    pipelineState.pixelShaderName = PS_TEXTURE_ARRAY_SOURCE_FILE;
    RegisterPipelineState(pipelineState);

    pipelineState.pipelineStateName = "CubeMap Debug Output";
    pipelineState.vertexShaderName = VS_PASSTHROUGH_CUBEMAP_SOURCE_FILE;
    pipelineState.pixelShaderName = PS_CUBEMAP_SOURCE_FILE;
    pipelineState.a2vType = A2V_TYPES::A2V_TYPES_CUBE;
    RegisterPipelineState(pipelineState);

    pipelineState.pipelineStateName = "CubeMap Array Debug Output";
    pipelineState.vertexShaderName = VS_PASSTHROUGH_CUBEMAP_SOURCE_FILE;
    pipelineState.pixelShaderName = PS_CUBEMAP_ARRAY_SOURCE_FILE;
    RegisterPipelineState(pipelineState);

    pipelineState.pipelineStateName = "SkyBox";
    pipelineState.vertexShaderName = VS_SKYBOX_SOURCE_FILE;
    pipelineState.pixelShaderName = PS_SKYBOX_SOURCE_FILE;
    pipelineState.depthTestMode = DEPTH_TEST_MODE::LESS_EQUAL;
    RegisterPipelineState(pipelineState);

    pipelineState.pipelineStateName = "Terrain";
    pipelineState.vertexShaderName = VS_TERRAIN_SOURCE_FILE;
    pipelineState.pixelShaderName = PS_TERRAIN_SOURCE_FILE;
    pipelineState.tessControlShaderName = TESC_TERRAIN_SOURCE_FILE;
    pipelineState.tessEvaluateShaderName = TESE_TERRAIN_SOURCE_FILE;
    pipelineState.depthTestMode = DEPTH_TEST_MODE::LESS_EQUAL;
    pipelineState.a2vType = A2V_TYPES::A2V_TYPES_POS_ONLY;
    RegisterPipelineState(pipelineState);

    return 0;
}

可以看到,这个类主要是实现引擎内的PSO对象的创建和登记(以及注销和销毁)。这个同样目前只是一个中间状态,可以看到Shader是通过#define的方式硬性编码在里面。将来,Shader应该是通过读取外部的配置文件,或者材质库得到。

这个类包含两个纯虚函数,在注册和销毁PSO的时候,会调用这两个函数。

        protected:
            virtual bool InitializePipelineState(PipelineState** ppPipelineState) = 0;
            virtual void DestroyPipelineState(PipelineState& pipelineState) = 0;

这两个函数就是更为下层的RHI当中来生成/销毁平台相关的PSO使用的。比如对于DX12:

namespace My {
    struct D3d12PipelineState : public PipelineState
    {
        D3D12_SHADER_BYTECODE vertexShaderByteCode;
        D3D12_SHADER_BYTECODE pixelShaderByteCode;
        D3D12_SHADER_BYTECODE geometryShaderByteCode;
        D3D12_SHADER_BYTECODE computeShaderByteCode;
        int32_t psoIndex{-1};

        D3d12PipelineState(PipelineState& state) : PipelineState(state)
        {
        }
    };

可以看到通过结构体的继承,我们将DX12所专有的部分追加在了末尾。利用C++的多态性,我们在D3DRHI的InitializePipelineState当中,用这个拓展版的PSO替换上层传下来的PSO:

bool D3d12PipelineStateManager::InitializePipelineState(PipelineState** ppPipelineState)
{
    D3d12PipelineState* pState = new D3d12PipelineState(**ppPipelineState);

    loadShaders(pState);

    *ppPipelineState = pState;

    return true;
}

而在上层,因为所有的PSO是通过智能指针的方式放入容器:

        protected:
            std::map<std::string, std::shared_ptr<PipelineState>> m_pipelineStates;

所以这种替换对上层是透明的。

类似地,我们对于Metal有如下定义:

namespace My {
    struct MetalPipelineState : public PipelineState
    {
        MetalPipelineState(PipelineState& rhs) : PipelineState(rhs) {}
        MetalPipelineState(PipelineState&& rhs) : PipelineState(std::move(rhs)) {}

        id<MTLRenderPipelineState> mtlRenderPipelineState;
        id<MTLComputePipelineState> mtlComputePipelineState;
        id<MTLDepthStencilState> depthState;
    };
}

同样是在Metal的InitializePipelineState当中,将上层传下来的PSO进行复制然后替换掉。

bool MetalPipelineStateManager::InitializePipelineState(PipelineState** ppPipelineState)
{
    MetalPipelineState* pState = new MetalPipelineState(**ppPipelineState);

    MTLVertexDescriptor* mtlVertexDescriptor = [[MTLVertexDescriptor alloc] init];

    initMtlVertexDescriptor(mtlVertexDescriptor, pState);

    // Load all the shader files with a metallib 
    id <MTLDevice> _device = MTLCreateSystemDefaultDevice();
    NSString *libraryFile = [[NSBundle mainBundle] pathForResource:@"Main" ofType:@"metallib"];
    NSError *error = Nil;
    id <MTLLibrary> myLibrary = [_device newLibraryWithFile:libraryFile error:&error];
    if (!myLibrary) {
        NSLog(@"Library error: %@", error);
    }

    switch (pState->pipelineType)
    {
        case PIPELINE_TYPE::GRAPHIC:
        {
            // Create pipeline state
            id<MTLFunction> vertexFunction = [myLibrary newFunctionWithName:shaderFileName2MainFuncName(pState->vertexShaderName)];
            id<MTLFunction> fragmentFunction = [myLibrary newFunctionWithName:shaderFileName2MainFuncName(pState->pixelShaderName)];

            MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
            pipelineStateDescriptor.label = [NSString stringWithCString:pState->pipelineStateName.c_str()
                                                    encoding:[NSString defaultCStringEncoding]];
            pipelineStateDescriptor.sampleCount = 4;
            pipelineStateDescriptor.vertexFunction = vertexFunction;
            pipelineStateDescriptor.fragmentFunction = fragmentFunction;
            pipelineStateDescriptor.vertexDescriptor = mtlVertexDescriptor;
            pipelineStateDescriptor.colorAttachments[0].pixelFormat = MTLPixelFormatRGBA8Unorm;
            pipelineStateDescriptor.depthAttachmentPixelFormat = MTLPixelFormatDepth32Float_Stencil8;

            pState->mtlRenderPipelineState = 
                [_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor error:&error];
            if (!pState->mtlRenderPipelineState)
            {
                NSLog(@"Failed to created render pipeline state %@, error %@", pipelineStateDescriptor.label, error);
            }
        }
        break;
        case PIPELINE_TYPE::COMPUTE:
        {
            id<MTLFunction> compFunction = [myLibrary newFunctionWithName:shaderFileName2MainFuncName(pState->computeShaderName)];

            pState->mtlComputePipelineState =
                [_device newComputePipelineStateWithFunction:compFunction error:&error];
            if (!pState->mtlComputePipelineState)
            {
                NSLog(@"Failed to created compute pipeline state, error %@", error);
            }
        }
        break;
        default:
            assert(0);
    }

    MTLDepthStencilDescriptor *depthStateDesc = [[MTLDepthStencilDescriptor alloc] init];

    switch(pState->depthTestMode)
    {
        case DEPTH_TEST_MODE::NONE:
            depthStateDesc.depthCompareFunction = MTLCompareFunctionAlways;
            break;
        case DEPTH_TEST_MODE::LARGE:
            depthStateDesc.depthCompareFunction = MTLCompareFunctionGreater;
            break;
        case DEPTH_TEST_MODE::LARGE_EQUAL:
            depthStateDesc.depthCompareFunction = MTLCompareFunctionGreaterEqual;
            break;
        case DEPTH_TEST_MODE::LESS:
            depthStateDesc.depthCompareFunction = MTLCompareFunctionLess;
            break;
        case DEPTH_TEST_MODE::LESS_EQUAL:
            depthStateDesc.depthCompareFunction = MTLCompareFunctionLessEqual;
            break;
        case DEPTH_TEST_MODE::EQUAL:
            depthStateDesc.depthCompareFunction = MTLCompareFunctionEqual;
            break;
        case DEPTH_TEST_MODE::NOT_EQUAL:
            depthStateDesc.depthCompareFunction = MTLCompareFunctionNotEqual;
            break;
        case DEPTH_TEST_MODE::NEVER:
            depthStateDesc.depthCompareFunction = MTLCompareFunctionNever;
            break;
        case DEPTH_TEST_MODE::ALWAYS:
            depthStateDesc.depthCompareFunction = MTLCompareFunctionAlways;
            break;
        default:
            assert(0);
    }

    if(pState->bDepthWrite)
    {
        depthStateDesc.depthWriteEnabled = YES;
    }
    else
    {
        depthStateDesc.depthWriteEnabled = NO;
    }

    pState->depthState = [_device newDepthStencilStateWithDescriptor:depthStateDesc];

    *ppPipelineState = pState;

    return true;
}

而对于OpenGL,因为没有对应的PSO概念,所以我们基本沿用上层的PSO对象,只是对Shader进行预编译,然后存储编译之后的ShaderProgram的ID

namespace My {
    struct OpenGLPipelineState : public PipelineState
    {
        uint32_t shaderProgram = 0;
        OpenGLPipelineState(PipelineState& rhs) : PipelineState(rhs) {}
        OpenGLPipelineState(PipelineState&& rhs) : PipelineState(std::move(rhs)) {}
    };

bool OpenGLPipelineStateManagerCommonBase::InitializePipelineState(PipelineState** ppPipelineState)
{
    bool result;
    OpenGLPipelineState* pnew_state = new OpenGLPipelineState(**ppPipelineState);

    ShaderSourceList list; 

    if(!(*ppPipelineState)->vertexShaderName.empty())
    {
        list.emplace_back(GL_VERTEX_SHADER, (*ppPipelineState)->vertexShaderName);
    }

    if(!(*ppPipelineState)->pixelShaderName.empty())
    {
        list.emplace_back(GL_FRAGMENT_SHADER, (*ppPipelineState)->pixelShaderName);
    }

    if(!(*ppPipelineState)->geometryShaderName.empty())
    {
        list.emplace_back(GL_GEOMETRY_SHADER, (*ppPipelineState)->geometryShaderName);
    }

    if(!(*ppPipelineState)->computeShaderName.empty())
    {
        list.emplace_back(GL_COMPUTE_SHADER, (*ppPipelineState)->computeShaderName);
    }

    if(!(*ppPipelineState)->tessControlShaderName.empty())
    {
        list.emplace_back(GL_TESS_CONTROL_SHADER, (*ppPipelineState)->tessControlShaderName);
    }

    if(!(*ppPipelineState)->tessEvaluateShaderName.empty())
    {
        list.emplace_back(GL_TESS_EVALUATION_SHADER, (*ppPipelineState)->tessEvaluateShaderName);
    }

    result = LoadShaderProgram(list, pnew_state->shaderProgram);

    *ppPipelineState = pnew_state;

    return result;
}

因为实际上不同图形API对应不同的Shading Language,所以我们需要一套机制,从一种Shading Language生成其它的版本。我选用的是HLSL,在CMake当中,通过如下的自定义命令,按照一定的命名规则,生成其它版本。

add_custom_target(Engine_Asset_Shaders
        COMMAND echo "Start processing Engine Shaders"
    )

set(SHADER_SOURCES basic.vert basic.frag
            debug.vert debug.frag
            cubemap.frag cubemaparray.frag
            pbr.vert pbr.frag
            skybox.vert skybox.frag
            shadowmap.vert shadowmap.frag
            shadowmap_omni.vert shadowmap_omni.frag
            terrain.vert terrain.frag
            texture.frag texturearray.frag
            passthrough.vert passthrough_cube.vert
            integrateBRDF.comp
        )

IF(WIN32)
    set(GLSL_VALIDATOR ${PROJECT_SOURCE_DIR}/External/Windows/bin/glslangValidator.exe)
    set(SPIRV_CROSS    ${PROJECT_SOURCE_DIR}/External/Windows/bin/SPIRV-Cross.exe)
ELSE(WIN32)
	set(GLSL_VALIDATOR ${PROJECT_SOURCE_DIR}/External/${MYGE_TARGET_PLATFORM}/bin/glslangValidator)
	set(SPIRV_CROSS    ${PROJECT_SOURCE_DIR}/External/${MYGE_TARGET_PLATFORM}/bin/spirv-cross)
ENDIF(WIN32)

foreach(SHADER IN LISTS SHADER_SOURCES)
    # Convert HLSL to Others
    string(REPLACE "." ";" arguments ${SHADER})
    list(GET arguments 0 part1)
    list(GET arguments 1 part2)

    set(VULKAN_SOURCE_DIR ${PROJECT_BINARY_DIR}/Asset/Shaders/Vulkan)
    set(GLSL_SOURCE_DIR ${PROJECT_BINARY_DIR}/Asset/Shaders/OpenGL)
    set(METAL_SOURCE_DIR ${PROJECT_BINARY_DIR}/Asset/Shaders/Metal)

    add_custom_command(TARGET Engine_Asset_Shaders PRE_BUILD
        COMMAND ${CMAKE_COMMAND} -E make_directory ${VULKAN_SOURCE_DIR}
        COMMAND ${CMAKE_COMMAND} -E make_directory ${GLSL_SOURCE_DIR}
        COMMAND ${CMAKE_COMMAND} -E make_directory ${METAL_SOURCE_DIR}
    )

    add_custom_command(TARGET Engine_Asset_Shaders PRE_BUILD
        COMMENT "HLSL --> SPIR-V"
	    COMMAND ${GLSL_VALIDATOR} -V -I. -I${PROJECT_SOURCE_DIR}/Framework/Common -o ${VULKAN_SOURCE_DIR}/${part1}.${part2}.spv -e ${part1}_${part2}_main ${PROJECT_SOURCE_DIR}/Asset/Shaders/HLSL/${part1}.${part2}.hlsl
        
        COMMENT "SPIR-V --> Desktop GLSL"
	    COMMAND ${SPIRV_CROSS} --version 420 --remove-unused-variables --output ${GLSL_SOURCE_DIR}/${part1}.${part2}.glsl ${PROJECT_BINARY_DIR}/Asset/Shaders/Vulkan/${part1}.${part2}.spv
        WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}

        COMMENT "SPIR-V --> Metal"
        COMMAND ${SPIRV_CROSS} --msl --msl-version 020101 --remove-unused-variables --output ${METAL_SOURCE_DIR}/${part1}.${part2}.metal ${PROJECT_BINARY_DIR}/Asset/Shaders/Vulkan/${part1}.${part2}.spv
        WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
    )
endforeach(SHADER)

IF(WIN32)
    # Compile HLSL shader sources
    set(SHADER_BIN_DIR ${PROJECT_BINARY_DIR}/Asset/Shaders/HLSL)

    add_custom_command(TARGET Engine_Asset_Shaders PRE_BUILD
        COMMAND ${CMAKE_COMMAND} -E make_directory ${SHADER_BIN_DIR}
    )

    foreach(SHADER IN LISTS SHADER_SOURCES)
        set(SHADER_BIN ${SHADER_BIN_DIR}/${SHADER}.cso)
        if (SHADER MATCHES "^([a-zA-Z0-9_]*)\.vert$")
            add_custom_command(TARGET Engine_Asset_Shaders POST_BUILD
                COMMAND fxc /T vs_5_1 /E ${CMAKE_MATCH_1}_vert_main /I${PROJECT_SOURCE_DIR} /I ${PROJECT_SOURCE_DIR}/Framework/Common ${PROJECT_SOURCE_DIR}/Asset/Shaders/HLSL/${SHADER}.hlsl /Fo ${SHADER_BIN}
                COMMENT "Compile ${SHADER}.hlsl --> ${SHADER}.cso"
                DEPENDS ${PROJECT_SOURCE_DIR}/Asset/Shaders/HLSL/${SHADER}.hlsl
            )
        elseif (SHADER MATCHES "^([a-zA-Z0-9_]*)\.frag$")
            add_custom_command(TARGET Engine_Asset_Shaders POST_BUILD
                COMMAND fxc /T ps_5_1 /E ${CMAKE_MATCH_1}_frag_main /I${PROJECT_SOURCE_DIR} /I ${PROJECT_SOURCE_DIR}/Framework/Common ${PROJECT_SOURCE_DIR}/Asset/Shaders/HLSL/${SHADER}.hlsl /Fo ${SHADER_BIN}
                COMMENT "Compile ${SHADER}.hlsl --> ${SHADER}.cso"
                DEPENDS ${PROJECT_SOURCE_DIR}/Asset/Shaders/HLSL/${SHADER}.hlsl
            )
        elseif (SHADER MATCHES "^([a-zA-Z0-9_]*)\.comp$")
            add_custom_command(TARGET Engine_Asset_Shaders POST_BUILD
                COMMAND fxc /T cs_5_1 /E ${CMAKE_MATCH_1}_comp_main /I${PROJECT_SOURCE_DIR} /I ${PROJECT_SOURCE_DIR}/Framework/Common ${PROJECT_SOURCE_DIR}/Asset/Shaders/HLSL/${SHADER}.hlsl /Fo ${SHADER_BIN}
                COMMENT "Compile ${SHADER}.hlsl --> ${SHADER}.cso"
                DEPENDS ${PROJECT_SOURCE_DIR}/Asset/Shaders/HLSL/${SHADER}.hlsl
            )
        elseif (SHADER MATCHES "^([a-zA-Z0-9_]*)\.geom$")
            add_custom_command(TARGET Engine_Asset_Shaders POST_BUILD
                COMMAND fxc /T gs_5_1 /E ${CMAKE_MATCH_1}_geom_main /I${PROJECT_SOURCE_DIR} /I ${PROJECT_SOURCE_DIR}/Framework/Common ${PROJECT_SOURCE_DIR}/Asset/Shaders/HLSL/${SHADER}.hlsl /Fo ${SHADER_BIN}
                COMMENT "Compile ${SHADER}.hlsl --> ${SHADER}.cso"
                DEPENDS ${PROJECT_SOURCE_DIR}/Asset/Shaders/HLSL/${SHADER}.hlsl
            )
        endif ()
    endforeach(SHADER)
ENDIF(WIN32)

这里面使用了glslang和spirv-cross这两个外部的工具。

最后,对于Metal,还需要将Shader进行库化。这个同样通过CMake自定义编译命令实现:

    foreach(SHADER IN LISTS SHADER_SOURCES)
        add_custom_command(OUTPUT ${SHADER}.air
            COMMAND xcrun -sdk macosx metal -g -MO -c ${PROJECT_BINARY_DIR}/Asset/Shaders/Metal/${SHADER}.metal -o ${SHADER}.air
            COMMENT "Compile ${SHADER}.metal --> ${SHADER}.air"
            DEPENDS Engine_Asset_Shaders
            )

        list(APPEND AIRS ${SHADER}.air)
    endforeach(SHADER)

    add_custom_command(OUTPUT Main.metalar
            COMMAND xcrun -sdk macosx metal-ar rcv Main.metalar ${AIRS}
            COMMENT "Archive ${AIRS} --> Main.metalar"
            DEPENDS ${AIRS}
        )

    add_custom_command(OUTPUT Main.metallib
            COMMAND xcrun -sdk macosx metallib Main.metalar -o Main.metallib
            COMMENT "Compile Main.metalar --> Main.metallib"
            DEPENDS Main.metalar
        )

当然,目前所有shader代码名也是硬性编码在CMake当中的。这部分将来需要以配置文件(如JSON)独立出来。其实关于Shader的组织,涉及到如何设计和组织材质(Material)的问题。所以这部分目前暂且如此。

有了这些事先创建的PSO对象之后,对于不同的渲染Pass,只需要找到并激活它就可以了。比如,对于基于PBR的前向渲染,我们只需要找到名字为“PBR”的PSO,然后将其激活:

void ForwardRenderPhase::Draw(Frame& frame)
{
    auto& pPipelineState = g_pPipelineStateManager->GetPipelineState("PBR");

    // Set the color shader as the current shader program and set the matrices that it will use for rendering.
    g_pGraphicsManager->SetPipelineState(pPipelineState);
    g_pGraphicsManager->SetShadowMaps(frame);
    g_pGraphicsManager->DrawBatch(frame.batchContexts);
}

这个PSO一层一层传递到下面的RHI之后,对于DX12,是下面这样:

void D3d12GraphicsManager::SetPipelineState(const std::shared_ptr<PipelineState>& pipelineState)
{
    if (pipelineState)
    {
        std::shared_ptr<D3d12PipelineState> state = dynamic_pointer_cast<D3d12PipelineState>(pipelineState);
        if (state->psoIndex == -1)
        {
            CreatePSO(*state);
        }

        m_pCommandList[m_nFrameIndex]->SetPipelineState(m_pPipelineStates[state->psoIndex]);
    }
}

而对于Metal,是下面这样:

void Metal2GraphicsManager::SetPipelineState(const std::shared_ptr<PipelineState>& pipelineState)
{
    const std::shared_ptr<MetalPipelineState> pState = dynamic_pointer_cast<MetalPipelineState>(pipelineState);
    [m_pRenderer setPipelineState:*pState];
}

至于OpenGL,因为没有PSO概念,所以fallback成为一组API调用:

oid OpenGLGraphicsManagerCommonBase::SetPipelineState(const std::shared_ptr<PipelineState>& pipelineState)
{
    const std::shared_ptr<const OpenGLPipelineState> pPipelineState = dynamic_pointer_cast<const OpenGLPipelineState>(pipelineState);
    m_CurrentShader = pPipelineState->shaderProgram;

    // Set the color shader as the current shader program and set the matrices that it will use for rendering.
    glUseProgram(m_CurrentShader);

    // Prepare & Bind per frame constant buffer
    uint32_t blockIndex = glGetUniformBlockIndex(m_CurrentShader, "PerFrameConstants");

    if (blockIndex != GL_INVALID_INDEX)
    {
        glUniformBlockBinding(m_CurrentShader, blockIndex, 10);
        glBindBufferBase(GL_UNIFORM_BUFFER, 10, m_uboDrawFrameConstant[m_nFrameIndex]);
    }

    // Prepare per batch constant buffer binding point
    blockIndex = glGetUniformBlockIndex(m_CurrentShader, "PerBatchConstants");

    if (blockIndex != GL_INVALID_INDEX)
    {
        glUniformBlockBinding(m_CurrentShader, blockIndex, 11);
        glBindBufferBase(GL_UNIFORM_BUFFER, 11, m_uboDrawBatchConstant[m_nFrameIndex]);
    }

    // Prepare & Bind light info
    blockIndex = glGetUniformBlockIndex(m_CurrentShader, "LightInfo");

    if (blockIndex != GL_INVALID_INDEX)
    {
        glUniformBlockBinding(m_CurrentShader, blockIndex, 12);
        glBindBufferBase(GL_UNIFORM_BUFFER, 12, m_uboLightInfo[m_nFrameIndex]);
    }

    // Bind LUT table
    auto brdf_lut = GetTexture("BRDF_LUT");
    setShaderParameter("SPIRV_Cross_CombinedbrdfLUTsamp0", 6);
    glActiveTexture(GL_TEXTURE6);
    if (brdf_lut > 0) {
        glBindTexture(GL_TEXTURE_2D, brdf_lut);
    }
    else {
        glBindTexture(GL_TEXTURE_2D, 0);
    }

    // Set Sky Box
    auto texture_id = (uint32_t) m_Frames[m_nFrameIndex].skybox;
    if (texture_id >= 0)
    {
        setShaderParameter("SPIRV_Cross_Combinedskyboxsamp0", 10);
        glActiveTexture(GL_TEXTURE10);
        GLenum target;
    #if defined(OS_WEBASSEMBLY)
        target = GL_TEXTURE_2D_ARRAY;
    #else
        target = GL_TEXTURE_CUBE_MAP_ARRAY;
    #endif
        glBindTexture(target, texture_id);
    }

    // Set Terrain
    texture_id = (uint32_t) m_Frames[m_nFrameIndex].terrainHeightMap;
    if (texture_id >= 0)
    {
        setShaderParameter("SPIRV_Cross_CombinedterrainHeightMapsamp0", 11);
        glActiveTexture(GL_TEXTURE11);
        glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, texture_id);
    }

    switch(pipelineState->depthTestMode)
    {
        case DEPTH_TEST_MODE::NONE:
            glDisable(GL_DEPTH_TEST);
            break;
        case DEPTH_TEST_MODE::LARGE:
            glEnable(GL_GREATER);
            break;
        case DEPTH_TEST_MODE::LARGE_EQUAL:
            glEnable(GL_DEPTH_TEST);
            glDepthFunc(GL_GEQUAL);
            break;
        case DEPTH_TEST_MODE::LESS:
            glEnable(GL_DEPTH_TEST);
            glDepthFunc(GL_LESS);
            break;
        case DEPTH_TEST_MODE::LESS_EQUAL:
            glEnable(GL_DEPTH_TEST);
            glDepthFunc(GL_LEQUAL);
            break;
        case DEPTH_TEST_MODE::EQUAL:
            glEnable(GL_DEPTH_TEST);
            glDepthFunc(GL_EQUAL);
            break;
        case DEPTH_TEST_MODE::NOT_EQUAL:
            glEnable(GL_DEPTH_TEST);
            glDepthFunc(GL_NOTEQUAL);
            break;
        case DEPTH_TEST_MODE::NEVER:
            glEnable(GL_DEPTH_TEST);
            glDepthFunc(GL_NEVER);
            break;
        case DEPTH_TEST_MODE::ALWAYS:
            glEnable(GL_DEPTH_TEST);
            glDepthFunc(GL_ALWAYS);
            break;
        default:
            assert(0);
    }

    if(pipelineState->bDepthWrite)
    {
        glDepthMask(GL_TRUE);
    }
    else
    {
        glDepthMask(GL_FALSE);
    }

    switch(pipelineState->cullFaceMode)
    {
        case CULL_FACE_MODE::NONE:
            glDisable(GL_CULL_FACE);
            break;
        case CULL_FACE_MODE::FRONT:
            glEnable(GL_CULL_FACE);
            glCullFace(GL_FRONT);
            break;
        case CULL_FACE_MODE::BACK:
            glEnable(GL_CULL_FACE);
            glCullFace(GL_BACK);
            break;
        default:
            assert(0);
    }
}

这种对比也让我明显看到DX12/Metal这种新一代API的好处:比起OpenGL,在切换渲染管道状态方面,明显更加快速。这不仅仅是因为API调用次数大大减少,而且是因为固定的PSO对象更加方便底层的GPU驱动进行优化,因为整个状态都清晰固定可见。而不是像OpenGL那样,驱动永远不知道后面是否还会有状态设置命令。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!