寫到一半才意識到要畫寫實的雲前,需要先有現實的物理演算才能算出對應的效果。而前面基本上都沒介紹這部分,在這裡先帶過。
雖然前面有太陽的 shader,但其實那個只是單純的看起來有那效果而已,而這裡就是要更徹底的把相關的數學帶進來,而首先要介紹的就是 PBR。
PBR 是 Physically Based Rendering 的簡稱,直翻就是物理基礎渲染。因為視覺效果呈現可以有非常多種的方式,依照古典物理來去繪出整個世界的樣子是其中一種方法。而這次因為想要畫出逼真的雲的效果,所以以此為基礎。
應該在高中時期上物理課時蠻常看見這樣的圖,光從甚麼角度打到物體就從甚麼角度反射回去。

其中打中物體後又會有間接光在物體間反射,而這也是之後雲會用到的相關知識之一。

其中在遊戲裡常見的光的實作方式有三種
不過這裡主要集中 Direction Light,也就是太陽光的部分。
//light.hlsl
// 定義圓周率常數供光照公式使用
static const float PI = 3.14159265359f;
cbuffer PbrConstants : register(b0)
{
// 物件從 Local Space 轉換到 World Space 的矩陣
row_major float4x4 world;
// 物件從 Local Space 轉換到 Clip Space 的 World View Projection 矩陣
row_major float4x4 worldViewProjection;
// 相機在 World Space 中的位置
float3 cameraPosition;
// 用來符合 Constant Buffer 記憶體對齊需求的填充值
float padding0;
// 材質的基礎顏色
float3 baseColor;
// 材質的金屬度
float metallic;
// 太陽光在 World Space 中的方向
float3 sunDirection;
// 材質的粗糙度
float roughness;
// 太陽光顏色
float3 sunColor;
// 太陽光強度
float sunIntensity;
};
struct VSInput
{
// 頂點在 Local Space 中的位置
float3 position : POSITION;
// 頂點在 Local Space 中的法線
float3 normal : NORMAL;
// 頂點的 UV 座標
float2 uv : TEXCOORD;
};
struct VSOutput
{
// 頂點在 Clip Space 中的位置
float4 position : SV_POSITION;
// 頂點在 World Space 中的位置
float3 worldPosition : POSITION0;
// 頂點在 World Space 中的法線
float3 worldNormal : NORMAL;
};
// Vertex Shader 主函式
VSOutput VSMain(VSInput input)
{
// 建立 Vertex Shader 的輸出資料
VSOutput output;
// 將頂點位置從 Local Space 轉換到 World Space
const float4 worldPosition = mul(float4(input.position, 1.0f), world);
// 將頂點位置轉換到 Clip Space
output.position = mul(float4(input.position, 1.0f), worldViewProjection);
// 儲存 World Space 頂點位置供 Pixel Shader 計算光照
output.worldPosition = worldPosition.xyz;
// 將法線轉換到 World Space 並正規化
output.worldNormal = normalize(mul(input.normal, (float3x3) world));
// 回傳轉換完成的頂點資料
return output;
}
// 計算 GGX Normal Distribution Function
float DistributionGGX(float3 normal, float3 halfVector, float alpha)
{
// 計算 alpha 的平方
const float alphaSquared = alpha * alpha;
// 計算法線與 Half Vector 的內積並限制在 0 到 1
const float normalDotHalf = saturate(dot(normal, halfVector));
// 計算 GGX Distribution 的分母中間項
const float denominator = normalDotHalf * normalDotHalf * (alphaSquared - 1.0f) + 1.0f;
// 回傳 GGX Normal Distribution 並避免分母過小
return alphaSquared / max(PI * denominator * denominator, 0.0001f);
}
// 使用 Schlick-GGX 近似計算單一方向的 Geometry Term
float GeometrySchlickGGX(float normalDotDirection, float roughnessValue)
{
// 根據粗糙度計算 Schlick-GGX 使用的 k 參數
const float k = ((roughnessValue + 1.0f) * (roughnessValue + 1.0f)) / 8.0f;
// 回傳 Geometry Term 並避免分母過小
return normalDotDirection / max(normalDotDirection * (1.0f - k) + k, 0.0001f);
}
// 使用 Schlick Approximation 計算 Fresnel 反射率
float3 FresnelSchlick(float cosine, float3 reflectance)
{
// 根據入射角度從基礎反射率逼近 Fresnel Effect
return reflectance + (1.0f - reflectance) * pow(1.0f - saturate(cosine), 5.0f);
}
// Pixel Shader 主函式
float4 PSMain(VSOutput input) : SV_TARGET
{
// 正規化插值後的 World Space 法線
const float3 normal = normalize(input.worldNormal);
// 計算表面指向相機的 View Direction
const float3 viewDirection = normalize(cameraPosition - input.worldPosition);
// 正規化光源方向
const float3 lightDirection = normalize(sunDirection);
// 計算 View Direction 與 Light Direction 中間的 Half Vector
const float3 halfVector = normalize(viewDirection + lightDirection);
// 計算法線與光源方向的內積
const float normalDotLight = saturate(dot(normal, lightDirection));
// 計算法線與視線方向的內積
const float normalDotView = saturate(dot(normal, viewDirection));
// 根據金屬度在非金屬預設反射率與 Base Color 之間插值
const float3 f0 = lerp(0.04f.xxx, baseColor, metallic);
// 將 Roughness 平方轉換為 GGX 使用的 alpha 並設定最小值
const float alpha = max(roughness * roughness, 0.0025f);
// 計算 GGX Normal Distribution
const float distribution = DistributionGGX(normal, halfVector, alpha);
// 分別計算 View 與 Light 方向的 Geometry Term 後相乘
const float geometry = GeometrySchlickGGX(normalDotView, roughness) *
GeometrySchlickGGX(normalDotLight, roughness);
// 計算目前角度下的 Fresnel 反射率
const float3 fresnel = FresnelSchlick(dot(halfVector, viewDirection), f0);
// 使用 Cook-Torrance BRDF 計算 Specular Reflection
const float3 specular = distribution * geometry * fresnel /
max(4.0f * normalDotView * normalDotLight, 0.0001f);
// 計算 Diffuse Reflection 所占比例並排除金屬材質的 Diffuse 成分
const float3 diffuseWeight = (1.0f - fresnel) * (1.0f - metallic);
// 根據光源顏色與強度計算入射 Radiance
const float3 radiance = sunColor * sunIntensity;
// 結合 Lambert Diffuse 與 Cook-Torrance Specular 計算直接光照
const float3 directLighting = (diffuseWeight * baseColor / PI + specular) * radiance * normalDotLight;
// 加入簡單的環境光並降低金屬材質的環境 Diffuse
const float3 ambient = baseColor * (0.025f * (1.0f - metallic));
// 合併直接光照與環境光得到 HDR 顏色
const float3 mappedColor = directLighting + ambient;
// 使用 Reinhard Tone Mapping 將 HDR 顏色映射到較低動態範圍
const float3 toneMapped = mappedColor / (mappedColor + 1.0f);
// 執行 Gamma Correction 後輸出不透明顏色
return float4(pow(toneMapped, 1.0f / 2.2f), 1.0f);
}
//main.cpp
#include <array>
#include <cstdlib>
#include <exception>
#include <filesystem>
#include <stdexcept>
#include <string>
#include <DirectXMath.h>
#include <directx/d3dx12_core.h>
#include "constant_buffer.h"
#include "graphics_engine.h"
#include "mesh.h"
#include "pipeline_state.h"
#include "render_context.h"
#include "render_pass.h"
#include "root_signature.h"
#include "shader.h"
#include "skyline_debugger.h"
#include "system.h"
namespace
{
using namespace DirectX;
constexpr XMFLOAT3 cameraPosition{0.0f, 4.0f, -10.0f};
constexpr XMFLOAT3 cameraTarget{0.0f, 1.0f, 0.0f};
constexpr XMFLOAT3 sunDirection{0.45f, 0.72f, 0.53f};
constexpr XMFLOAT3 sunColor{1.0f, 0.91f, 0.78f};
constexpr float sunIntensity = 5.0f;
struct SunConstants
{
XMFLOAT4X4 inverseView;
XMFLOAT4X4 inverseProjection;
XMFLOAT3 direction;
float radius;
XMFLOAT3 color;
float intensity;
};
struct PbrConstants
{
XMFLOAT4X4 world;
XMFLOAT4X4 worldViewProjection;
XMFLOAT3 camera;
float padding0;
XMFLOAT3 baseColor;
float metallic;
XMFLOAT3 direction;
float roughness;
XMFLOAT3 lightColor;
float lightIntensity;
};
static_assert(sizeof(SunConstants) == 160);
static_assert(sizeof(PbrConstants) == 192);
std::filesystem::path getExecutableDirectory()
{
wchar_t executablePath[MAX_PATH]{};
const DWORD pathLength = GetModuleFileNameW(nullptr, executablePath, _countof(executablePath));
if (pathLength == 0 || pathLength == _countof(executablePath))
throw std::runtime_error("DayX_PBR: Failed to determine the executable directory.");
return std::filesystem::path(executablePath).parent_path();
}
XMMATRIX createViewMatrix()
{
return XMMatrixLookAtLH(XMLoadFloat3(&cameraPosition), XMLoadFloat3(&cameraTarget), XMVectorSet(0, 1, 0, 0));
}
XMMATRIX createProjectionMatrix(UINT width, UINT height)
{
if (width == 0 || height == 0)
throw std::runtime_error("DayX_PBR: Frame-buffer dimensions must be non-zero.");
return XMMatrixPerspectiveFovLH(XMConvertToRadians(55.0f), static_cast<float>(width) / height, 0.1f, 100.0f);
}
D3D12_GRAPHICS_PIPELINE_STATE_DESC createPipelineDescription(ID3D12RootSignature* rootSignature,
ID3DBlob* vertexShader, ID3DBlob* pixelShader,
bool meshPipeline)
{
if (rootSignature == nullptr || vertexShader == nullptr || pixelShader == nullptr)
throw std::invalid_argument("DayX_PBR: Pipeline shaders and root signature are required.");
static constexpr D3D12_INPUT_ELEMENT_DESC inputLayout[] = {
{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
{"NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
{"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
};
D3D12_GRAPHICS_PIPELINE_STATE_DESC description{};
if (meshPipeline)
description.InputLayout = {inputLayout, _countof(inputLayout)};
description.pRootSignature = rootSignature;
description.VS = CD3DX12_SHADER_BYTECODE(vertexShader);
description.PS = CD3DX12_SHADER_BYTECODE(pixelShader);
description.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
description.RasterizerState.CullMode = D3D12_CULL_MODE_NONE;
description.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
if (!meshPipeline)
{
D3D12_RENDER_TARGET_BLEND_DESC& renderTarget = description.BlendState.RenderTarget[0];
renderTarget.BlendEnable = TRUE;
renderTarget.SrcBlend = D3D12_BLEND_SRC_ALPHA;
renderTarget.DestBlend = D3D12_BLEND_INV_SRC_ALPHA;
renderTarget.BlendOp = D3D12_BLEND_OP_ADD;
renderTarget.SrcBlendAlpha = D3D12_BLEND_ONE;
renderTarget.DestBlendAlpha = D3D12_BLEND_ZERO;
renderTarget.BlendOpAlpha = D3D12_BLEND_OP_ADD;
renderTarget.RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;
}
description.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
description.DepthStencilState.DepthEnable = meshPipeline ? TRUE : FALSE;
description.DepthStencilState.DepthWriteMask = meshPipeline ? D3D12_DEPTH_WRITE_MASK_ALL : D3D12_DEPTH_WRITE_MASK_ZERO;
description.DepthStencilState.StencilEnable = FALSE;
description.SampleMask = UINT_MAX;
description.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
description.NumRenderTargets = 1;
description.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
description.DSVFormat = DXGI_FORMAT_D32_FLOAT;
description.SampleDesc.Count = 1;
return description;
}
class SunPass final : public RenderPass
{
public:
void init(const RenderPassInitContext& context) override
{
m_vertexShader.loadVS("assets/shaders/sun.hlsl", "VSMain");
m_pixelShader.loadPS("assets/shaders/sun.hlsl", "PSMain");
m_pipelineState.init(createPipelineDescription(context.rootSignature.get(), m_vertexShader.getCompiledBlob(),
m_pixelShader.getCompiledBlob(), false));
updateConstants(context.graphicsEngine.getFrameBufferWidth(), context.graphicsEngine.GetFrameBufferHeight());
m_constantBuffer.init(sizeof(m_constants), &m_constants);
}
void execute(const RenderFrameContext& context) override
{
updateConstants(context.width, context.height);
m_constantBuffer.copyToVRAM(m_constants);
context.renderContext.setPipelineState(m_pipelineState);
context.renderContext.setGraphicsRootConstantBufferView(0, m_constantBuffer.getGPUVirtualAddress());
context.renderContext.draw(3);
}
private:
void updateConstants(UINT width, UINT height)
{
const XMMATRIX inverseView = XMMatrixInverse(nullptr, createViewMatrix());
const XMMATRIX inverseProjection = XMMatrixInverse(nullptr, createProjectionMatrix(width, height));
XMStoreFloat4x4(&m_constants.inverseView, inverseView);
XMStoreFloat4x4(&m_constants.inverseProjection, inverseProjection);
m_constants.direction = sunDirection;
m_constants.radius = 0.035f;
m_constants.color = sunColor;
m_constants.intensity = 1.5f;
}
Shader m_vertexShader;
Shader m_pixelShader;
PipelineState m_pipelineState;
ConstantBuffer m_constantBuffer;
SunConstants m_constants{};
};
class PbrPass final : public RenderPass
{
public:
void init(const RenderPassInitContext& context) override
{
//使用 light.hlsl
m_vertexShader.loadVS("assets/shaders/light.hlsl", "VSMain");
m_pixelShader.loadPS("assets/shaders/light.hlsl", "PSMain");
m_pipelineState.init(createPipelineDescription(context.rootSignature.get(), m_vertexShader.getCompiledBlob(),
m_pixelShader.getCompiledBlob(), true));
//使用導入的幾個 fbx 檔
const std::filesystem::path assetDirectory = getExecutableDirectory() / "assets" / "fbx";
m_meshes[0].initFromFbxFile((assetDirectory / "Plane.fbx").string());
m_meshes[1].initFromFbxFile((assetDirectory / "Cube.fbx").string());
m_meshes[2].initFromFbxFile((assetDirectory / "Sphere.fbx").string());
updateConstants(context.graphicsEngine.getFrameBufferWidth(), context.graphicsEngine.GetFrameBufferHeight());
for (std::size_t index = 0; index < m_buffers.size(); ++index)
m_buffers[index].init(sizeof(PbrConstants), &m_constants[index]);
}
void execute(const RenderFrameContext& context) override
{
updateConstants(context.width, context.height);
context.renderContext.setPipelineState(m_pipelineState);
for (std::size_t index = 0; index < m_meshes.size(); ++index)
{
m_buffers[index].copyToVRAM(m_constants[index]);
context.renderContext.setGraphicsRootConstantBufferView(0, m_buffers[index].getGPUVirtualAddress());
m_meshes[index].draw(context.renderContext);
}
}
private:
void updateConstants(UINT width, UINT height)
{
const XMMATRIX viewProjection = createViewMatrix() * createProjectionMatrix(width, height);
const std::array<XMMATRIX, 3> worlds = {
XMMatrixScaling(6.0f, 1.0f, 6.0f),
XMMatrixRotationY(XMConvertToRadians(-20.0f)) * XMMatrixTranslation(-1.7f, 1.0f, 0.0f),
XMMatrixTranslation(1.7f, 1.0f, 0.0f),
};
constexpr std::array<XMFLOAT3, 3> colors = {
XMFLOAT3{0.38f, 0.42f, 0.46f}, XMFLOAT3{0.92f, 0.24f, 0.08f}, XMFLOAT3{0.95f, 0.71f, 0.20f}};
constexpr std::array<float, 3> metallic = {0.0f, 0.15f, 1.0f};
constexpr std::array<float, 3> roughness = {0.72f, 0.30f, 0.16f};
for (std::size_t index = 0; index < m_constants.size(); ++index)
{
XMStoreFloat4x4(&m_constants[index].world, worlds[index]);
XMStoreFloat4x4(&m_constants[index].worldViewProjection, worlds[index] * viewProjection);
m_constants[index].camera = cameraPosition;
m_constants[index].baseColor = colors[index];
m_constants[index].metallic = metallic[index];
m_constants[index].direction = sunDirection;
m_constants[index].roughness = roughness[index];
m_constants[index].lightColor = sunColor;
m_constants[index].lightIntensity = sunIntensity;
}
}
Shader m_vertexShader;
Shader m_pixelShader;
PipelineState m_pipelineState;
std::array<Mesh, 3> m_meshes;
std::array<ConstantBuffer, 3> m_buffers;
std::array<PbrConstants, 3> m_constants{};
};
} // namespace
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR commandLine, int showCommand)
{
try
{
SkylineDebugger::Initialize("DayX_PBR");
SKYLINE_LOG(INFO) << "[DayX_PBR] Initializing window...";
initWindow(hInstance, hPrevInstance, commandLine, showCommand, TEXT("DayX PBR"));
if (g_hWnd == nullptr)
throw std::runtime_error("DayX_PBR: Failed to create the application window.");
SKYLINE_LOG(INFO) << "[DayX_PBR] Initializing graphics engine...";
GraphicsEngine graphicsEngine;
graphicsEngine.init(g_hWnd, FRAME_BUFFER_W, FRAME_BUFFER_H);
SkylineDebugger::ConfigureD3D12(graphicsEngine.getD3DDevice());
SKYLINE_LOG(INFO) << "[DayX_PBR] Initializing render passes...";
RootSignature rootSignature;
rootSignature.init(D3D12_FILTER_MIN_MAG_MIP_LINEAR, D3D12_TEXTURE_ADDRESS_MODE_WRAP,
D3D12_TEXTURE_ADDRESS_MODE_WRAP, D3D12_TEXTURE_ADDRESS_MODE_WRAP);
RenderPassInitContext initContext{graphicsEngine, rootSignature};
SunPass sunPass;
PbrPass pbrPass;
sunPass.init(initContext);
pbrPass.init(initContext);
SKYLINE_LOG(INFO) << "[DayX_PBR] Initialization completed.";
RenderContext& renderContext = graphicsEngine.getRenderContext();
bool isFirstFrame = true;
while (dispatchWindowMessage())
{
if (isFirstFrame)
SKYLINE_LOG(INFO) << "[DayX_PBR] Rendering first frame...";
graphicsEngine.beginRender();
renderContext.setRootSignature(rootSignature);
renderContext.setPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
RenderFrameContext frameContext{renderContext, graphicsEngine, graphicsEngine.getFrameBufferWidth(),
graphicsEngine.GetFrameBufferHeight()};
sunPass.execute(frameContext);
pbrPass.execute(frameContext);
graphicsEngine.endRender();
SkylineDebugger::LogD3D12Messages(graphicsEngine.getD3DDevice());
if (isFirstFrame)
{
SKYLINE_LOG(INFO) << "[DayX_PBR] First frame presented.";
isFirstFrame = false;
}
}
SkylineDebugger::Shutdown();
return EXIT_SUCCESS;
}
catch (const std::exception& exception)
{
SkylineDebugger::ShowFatalError("DayX_PBR initialization failed", exception);
SkylineDebugger::Shutdown();
return EXIT_FAILURE;
}
catch (...)
{
SkylineDebugger::ShowFatalError("DayX_PBR initialization failed", "An unknown fatal error occurred.");
SkylineDebugger::Shutdown();
return EXIT_FAILURE;
}
}

An Introduction to Physically Based Rendering | Maxime Garcia