久等了。再次來彙報一下進度。

目前已經把除了陰影貼圖之外的PBR+IBL渲染部分在Metal上面實現了。

在這個過程當中,我對代碼首先進行重構的地方是Draw Pass的概念。

之前我們聲明了一個IDrawPass介面,支持一個Draw()方法,然後將陰影貼圖的繪製、場景物體(幾何體)的繪製、天空球的繪製等都作為IDrawPass的一個實現進行編碼。但是在Metal當中,所謂的Draw Pass是指對某個表面(Render Target)的一次完整繪製。也就是在Draw Pass開始的時候會對Render Target進行清除操作。

因此,如果沿用我們原本的設計,那麼在繪製完陰影貼圖之後,在我們開始繪製場景之前,會有一次對RT的清除操作。這不要緊,因為我們原本就是要將陰影貼圖繪製在一個專用的RT上,然後稍後將這個RT作為一個貼圖使用。而我們在繪製場景的時候會使用一個不同的RT,在實際繪製場景之前對這個RT進行清除是我們所期待的。

然而,在繪製完成場景物體之後,開始天空球繪製之前,因為我們將其定義為兩個不同的Draw Pass,又會產生一次對RT的清除操作。這會導致最後我們在畫面上只能看到天空球,而在天空球之前繪製的內容都會被清除而看不到。這顯然不是我們想要的結果。

所以,場景物體(幾何體)的繪製與天空球的繪製之間,並不是Draw Pass之間的關係。它們都是對同一個RT的累積式的繪製,因此是屬於一個Draw Pass的。

因此,我又增加了一個IDrawPhase的介面,作為同一個Draw Pass當中的子階段過程的封裝。不同的Draw Phase共用同一個RT,並且Draw Phase的切換不產生對RT的Clear操作。一個Draw Pass由1個或者1個以上的Draw Phase聚合而成,Draw Pass的Draw事件實際上就是依次調用其聚合的所有Draw Phase的Draw事件:

#include "BasePass.hpp"

using namespace My;

void BasePass::Draw(Frame& frame)
{
for (const auto& pPhase : m_DrawPhases)
{
pPhase->BeginPhase();
pPhase->Draw(frame);
pPhase->EndPhase();
}
}

而GraphicsManager則聚合了1個或多個Draw Pass,其Draw事件實際上就是依次調用其聚合的所有Draw Pass的Draw事件:

void GraphicsManager::Draw()
{
auto& frame = m_Frames[m_nFrameIndex];

for (auto& pDrawPass : m_DrawPasses)
{
BeginPass();
pDrawPass->Draw(frame);
EndPass();
}
}

接下來一個重要的重構就是統一C++層面和Shader(HLSL)層面之間的重要數據結構。通過使用一些宏定義和條件編譯命令,我們可以在同一個文件當中定義可以用於這兩種不同編程語言的數據結構:

Framework/Common/cbuffer.h

#ifndef __STDCBUFFER_H__
#define __STDCBUFFER_H__

#define MAX_LIGHTS 100

#ifdef __cplusplus
#include "portable.hpp"
#include "geommath.hpp"
#include "Guid.hpp"
#include "SceneObjectLight.hpp"
using namespace My;
#define SEMANTIC(a)
#define REGISTER(x)
#define unistruct struct
#define SamplerState void

namespace My {
enum LightType {
Omni = 0,
Spot = 1,
Infinity = 2,
Area = 3
};
#else
#define SEMANTIC(a) : a
#define REGISTER(x) : register(x)
#define unistruct cbuffer
#define uint32_t uint
#define Guid uint4
#define Vector2f float2
#define Vector3f float3
#define Vector4f float4
#define Matrix2X2f row_major float2x2
#define Matrix3X3f row_major float3x3
#define Matrix4X4f row_major float4x4
#define LightType uint
#define AttenCurveType uint
#endif

struct Light{
float lightIntensity; // 4 bytes
LightType lightType; // 4 bytes
int lightCastShadow; // 4 bytes
int lightShadowMapIndex; // 4 bytes
AttenCurveType lightAngleAttenCurveType; // 4 bytes
AttenCurveType lightDistAttenCurveType; // 4 bytes
Vector2f lightSize; // 8 bytes
Guid lightGuid; // 16 bytes
Vector4f lightPosition; // 16 bytes
Vector4f lightColor; // 16 bytes
Vector4f lightDirection; // 16 bytes
Vector4f lightDistAttenCurveParams[2]; // 32 bytes
Vector4f lightAngleAttenCurveParams[2]; // 32 bytes
Matrix4X4f lightVP; // 64 bytes
Vector4f padding[2]; // 32 bytes
}; // totle 256 bytes

unistruct PerFrameConstants REGISTER(b10)
{
Matrix4X4f viewMatrix; // 64 bytes
Matrix4X4f projectionMatrix; // 64 bytes
Vector4f camPos; // 16 bytes
uint32_t numLights; // 4 bytes
};

unistruct PerBatchConstants REGISTER(b11)
{
Matrix4X4f modelMatrix; // 64 bytes
};

unistruct LightInfo REGISTER(b12)
{
struct Light lights[MAX_LIGHTS];
};

#ifdef __cplusplus
const size_t kSizePerFrameConstantBuffer = ALIGN(sizeof(PerFrameConstants), 256); // CB size is required to be 256-byte aligned.
const size_t kSizePerBatchConstantBuffer = ALIGN(sizeof(PerBatchConstants), 256); // CB size is required to be 256-byte aligned.
const size_t kSizeLightInfo = ALIGN(sizeof(LightInfo), 256); // CB size is required to be 256-byte aligned.
#endif

struct a2v
{
Vector3f inputPosition SEMANTIC(POSITION);
Vector3f inputNormal SEMANTIC(NORMAL);
Vector2f inputUV SEMANTIC(TEXCOORD);
Vector3f inputTangent SEMANTIC(TANGENT);
Vector3f inputBiTangent SEMANTIC(BITANGENT);
};

struct a2v_pos_only
{
Vector3f inputPosition SEMANTIC(POSITION);
};

#ifdef __cplusplus
struct material_textures
{
int32_t diffuseMap = -1;
int32_t normalMap = -1;
int32_t metallicMap = -1;
int32_t roughnessMap = -1;
int32_t aoMap = -1;
int32_t heightMap = -1;
};

struct global_textures
{
int32_t brdfLUT;
};

struct frame_textures
{
int32_t shadowMap = -1;
int32_t shadowMapCount = 0;

int32_t globalShadowMap = -1;
int32_t globalShadowMapCount = 0;

int32_t cubeShadowMap = -1;
int32_t cubeShadowMapCount = 0;

int32_t skybox = -1;
int32_t terrainHeightMap = -1;
};
#else
Texture2D diffuseMap REGISTER(t0);
Texture2D normalMap REGISTER(t1);
Texture2D metallicMap REGISTER(t2);
Texture2D roughnessMap REGISTER(t3);
Texture2D aoMap REGISTER(t4);
Texture2D heightMap REGISTER(t5);
Texture2D brdfLUT REGISTER(t6);
Texture2DArray shadowMap REGISTER(t7);
Texture2DArray globalShadowMap REGISTER(t8);
TextureCubeArray cubeShadowMap REGISTER(t9);
TextureCubeArray skybox REGISTER(t10);
Texture2D terrainHeightMap REGISTER(t11);

// samplers
SamplerState samp0 REGISTER(s0);
#endif

#ifdef __cplusplus
} // namespace My
#endif
#endif // !__STDCBUFFER_H__

因為渲染子系統的主要任務其實就是將CPU這邊的場景結構傳遞給GPU,所以保證CPU端和GPU端的數據結構類型一致就是非常重要的一個事情。通常,我們會分別在C++代碼和Shader代碼當中分別定義這些數據結構,但是這樣一來就很容易發生修改了一邊而忘記修改另外一邊的情況,從而導致各種各樣奇怪的問題。

在這個基礎上,由於我們想要讓GraphicsManager支持多線程渲染,那麼我們就需要封裝渲染所需的數據。由於渲染是按幀組織,渲染所需的數據其實就可以被看作是場景在某一幀時的一個快照。當然我們並不需要記錄場景當中的所有數據,因為我們的目的是渲染畫面,所以我們只需要保存需要傳遞給GPU的相關數據,以及一些緊密相關的上下文即可。

因此,很容易想到我們只需要在上面的cbuffer.h所定義的CPU:GPU介面數據結構的基礎上進行一些必要的擴展即可形成我們圖形模塊所需要記錄的幀元數據結構:

Framework/Common/FrameStructure.hpp

#pragma once
#include <vector>
#include "Scene.hpp"
#include "cbuffer.h"

namespace My {
struct DrawFrameContext : PerFrameConstants, frame_textures {
};

struct DrawBatchContext : PerBatchConstants {
uint32_t batchIndex;
std::shared_ptr<SceneGeometryNode> node;
material_textures material;

virtual ~DrawBatchContext() = default;
};

struct Frame : global_textures {
DrawFrameContext frameContext;
std::vector<std::shared_ptr<DrawBatchContext>> batchContexts;
LightInfo lightInfo;
};
}

而對於各種具體的圖形API,上面的這些通用數據結構會進一步轉變成為具體的圖形API所要求的格式,所以我們在RHI當中,需要對這個結構進行進一步的特化,補充圖形API調用所需的數據成員。比如對於OpenGL、我們需要記住保存了每個DrawCall所需的頂點和索引數據的VAO對象的名字、頂點的拓撲結構、索引的字寬以及個數等:

struct OpenGLDrawBatchContext : public DrawBatchContext {
GLuint vao;
GLenum mode;
GLenum type;
GLsizei count;
};

而對於Metal2,類似地我們需要記住每個DrawCall的索引個數,索引在索引Buffer當中的偏移量,頂點的拓撲結構、索引的字寬、頂點屬性的個數以及頂點數據在頂點Buffer當中的偏移量等。

struct MtlDrawBatchContext : public DrawBatchContext {
uint32_t index_count;
uint32_t index_offset;
MTLPrimitiveType index_mode;
MTLIndexType index_type;
uint32_t property_count;
uint32_t property_offset;
};

通過將所有這些核心數據結構以及整個渲染管道按照具體圖形API特有的部分和可以通用的部分進行分離,並通過這樣的類泛化關係將其逐步抽象,我們可以找到比較能夠適合不同圖形API的通用的高層邏輯及數據結構封裝方法,也就是所謂的架構。下面給出了目前我們的GraphicsManager的一個大致的平台無關的功能劃分粒度:

namespace My {
class GraphicsManager : implements IRuntimeModule
{
public:
virtual ~GraphicsManager() {}

virtual int Initialize();
virtual void Finalize();

virtual void Tick();

virtual void Draw();
virtual void Present();

virtual void UseShaderProgram(const int32_t shaderProgram);

virtual void DrawBatch(const std::vector<std::shared_ptr<DrawBatchContext>>& batches);

virtual int32_t GenerateTexture(const char* id, const uint32_t width, const uint32_t height);
virtual void BeginRenderToTexture(int32_t& context, const int32_t texture, const uint32_t width, const uint32_t height);
virtual void EndRenderToTexture(int32_t& context);

virtual int32_t GenerateAndBindTextureForWrite(const char* id, const uint32_t width, const uint32_t height);
virtual void Dispatch(const uint32_t width, const uint32_t height, const uint32_t depth);

virtual int32_t GetTexture(const char* id);

virtual void DrawFullScreenQuad();

protected:
virtual void BeginScene(const Scene& scene);
virtual void EndScene();

virtual void BeginFrame();
virtual void EndFrame();

virtual void BeginPass();
virtual void EndPass();

virtual void BeginCompute();
virtual void EndCompute();

#ifdef DEBUG
virtual void RenderDebugBuffers();
#endif

private:
void InitConstants();
void CalculateCameraMatrix();
void CalculateLights();

void UpdateConstants();

virtual void SetLightInfo(const LightInfo& lightInfo);
virtual void SetPerFrameConstants(const DrawFrameContext& context);
virtual void SetPerBatchConstants(const std::vector<std::shared_ptr<DrawBatchContext>>& batches);

protected:
uint32_t m_nFrameIndex = 0;

std::vector<Frame> m_Frames;
std::vector<std::shared_ptr<IDispatchPass>> m_InitPasses;
std::vector<std::shared_ptr<IDrawPass>> m_DrawPasses;
};

推薦閱讀:

查看原文 >>
相关文章