iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0

今天是太陽的部分,主要註解跟太陽有關的實作。

// 定義 Vertex Shader 傳給 Pixel Shader 的資料
struct VSOutput
{
    // 儲存頂點的裁切空間位置
    float4 position : SV_POSITION;

    // 儲存對應的螢幕 UV 座標
    float2 uv : TEXCOORD0;
};

// 定義太陽相關參數並綁定到 b0
cbuffer SunConstants : register(b0)
{
    // 儲存太陽在螢幕上的位置
    float2 sunPosition;

    // 儲存太陽圓盤半徑
    float sunRadius;

    // 儲存太陽亮度強度
    float sunIntensity;

    // 儲存太陽顏色
    float3 sunColor;

    // 儲存螢幕寬高比
    float aspectRatio;
};

// 根據 Vertex ID 建立 Fullscreen Triangle
VSOutput VSMain(uint vertexID : SV_VertexID)
{
    // 建立 Vertex Shader 輸出資料
    VSOutput output;

    // 定義覆蓋整個畫面的三角形頂點位置
    float2 positions[3] =
    {
        float2(-1.0f, -1.0f),
        float2(-1.0f, 3.0f),
        float2(3.0f, -1.0f)
    };

    // 取得目前 Vertex ID 對應的頂點位置
    float2 position = positions[vertexID];

    // 將二維位置轉換成裁切空間座標
    output.position = float4(position, 0.0f, 1.0f);

    // 將 NDC 座標轉換成 UV 座標並反轉 Y 軸
    output.uv = position * float2(0.5f, -0.5f) + 0.5f;

    // 回傳 Vertex Shader 輸出
    return output;
}

// 計算太陽圓盤與外圍光暈顏色
float4 PSMain(VSOutput input) : SV_TARGET
{
    // 將 UV 從 [0, 1] 轉換成 [-1, 1] 的螢幕座標
    float2 screenPosition = input.uv * 2.0f - 1.0f;

    // 計算目前 Pixel 相對於太陽中心的位置差
    float2 delta = screenPosition - sunPosition;

    // 依照視窗比例修正水平距離,避免太陽在寬螢幕上被拉伸
    delta.x *= aspectRatio;

    // 計算目前 Pixel 到太陽中心的距離
    float distanceFromSun = length(delta);

    // 設定太陽圓盤邊緣的柔化範圍
    float edgeSoftness = 0.005f;
	
    // 建立具有柔和邊緣的太陽圓盤遮罩
    float sunDisc = 1.0f - smoothstep(sunRadius, sunRadius + edgeSoftness, distanceFromSun);
	
    // 建立從太陽邊緣向外逐漸衰減的光暈遮罩
    float glow = 1.0f - smoothstep(sunRadius, sunRadius * 4.0f, distanceFromSun);
	
    // 結合太陽圓盤與較弱的光暈計算最終 RGB 顏色
    float3 finalColor = sunColor * (sunDisc * sunIntensity + glow * sunIntensity * 0.2f);
	
    // 根據太陽圓盤與光暈強度計算透明度
    float alpha = saturate(sunDisc + glow * 0.15f);
	
    // 輸出太陽的最終顏色與透明度
    return float4(finalColor, alpha);
}
#include <cstdlib>
#include <exception>
#include <stdexcept>
#include <directx/d3dx12_core.h>

#include "constant_buffer.h"
#include "graphics_engine.h"
#include "render_context.h"
#include "skyline_debugger.h"
#include "system.h"
#include "math/vector.h"

namespace
{
    // 定義傳給太陽 Shader 的 Constant Buffer 資料
    struct SunConstants
    {
        // 太陽在螢幕座標中的中心位置
        Vector2 sunPosition;

        // 太陽圓盤半徑
        float sunRadius;

        // 太陽亮度強度
        float sunIntensity;

        // 太陽顏色
        Vector3 sunColor;

        // 畫面寬高比,用來修正太陽形狀
        float aspectRatio;
    };

    // 確認 C++ 結構大小與 HLSL Constant Buffer layout 一致
    static_assert(sizeof(SunConstants) == 32, "SunConstants must match the HLSL constant-buffer layout.");

    void initRootSignature(RootSignature& rs)
    {
        rs.init(D3D12_FILTER_MIN_MAG_MIP_LINEAR,
            D3D12_TEXTURE_ADDRESS_MODE_WRAP,
            D3D12_TEXTURE_ADDRESS_MODE_WRAP,
            D3D12_TEXTURE_ADDRESS_MODE_WRAP);
    }

    D3D12_GRAPHICS_PIPELINE_STATE_DESC createSkyPipelineStateDescription(
        ID3D12RootSignature* rootSignature,
        ID3DBlob* vertexShader,
        ID3DBlob* pixelShader)
    {
        if (rootSignature == nullptr)
            throw std::invalid_argument("SkyPipeline: Root signature is required.");
        if (vertexShader == nullptr)
            throw std::invalid_argument("SkyPipeline: Vertex shader is required.");
        if (pixelShader == nullptr)
            throw std::invalid_argument("SkyPipeline: Pixel shader is required.");

        D3D12_GRAPHICS_PIPELINE_STATE_DESC description{};

        description.InputLayout = {nullptr, 0};

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

        description.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
        description.DepthStencilState.DepthEnable = FALSE;
        description.DepthStencilState.DepthWriteMask = 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;
    }

    // 建立繪製太陽使用的 Graphics Pipeline State 設定
    D3D12_GRAPHICS_PIPELINE_STATE_DESC createLightStateDescription(
        ID3D12RootSignature* rootSignature,
        ID3DBlob* vertexShader,
        ID3DBlob* pixelShader)
    {
        if (rootSignature == nullptr)
            throw std::invalid_argument("LightPipeline: Root signature is required.");
        if (vertexShader == nullptr)
            throw std::invalid_argument("LightPipeline: Vertex shader is required.");
        if (pixelShader == nullptr)
            throw std::invalid_argument("LightPipeline: Pixel shader is required.");

        D3D12_GRAPHICS_PIPELINE_STATE_DESC description{};

        // Fullscreen Triangle 由 SV_VertexID 產生,因此不需要 Vertex Buffer Input Layout
        description.InputLayout = {nullptr, 0};

        description.pRootSignature = rootSignature;

        // 設定太陽使用的 Vertex Shader 與 Pixel Shader
        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;

        // 使用 Alpha Blending 讓太陽與光暈疊加在天空背景上
        description.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);

        // 取得第一個 Render Target 的 Blend 設定
        auto& rt = description.BlendState.RenderTarget[0];

        // 啟用顏色混合
        rt.BlendEnable = TRUE;

        // Source 顏色乘上 Pixel Shader 輸出的 Alpha
        rt.SrcBlend = D3D12_BLEND_SRC_ALPHA;

        // 背景顏色乘上 1 - Source Alpha
        rt.DestBlend = D3D12_BLEND_INV_SRC_ALPHA;

        // 將 Source 與 Destination 顏色相加
        rt.BlendOp = D3D12_BLEND_OP_ADD;

        // Alpha 輸出直接使用 Source Alpha
        rt.SrcBlendAlpha = D3D12_BLEND_ONE;

        // 不混合 Destination Alpha
        rt.DestBlendAlpha = D3D12_BLEND_ZERO;

        // 使用加法產生最終 Alpha
        rt.BlendOpAlpha = D3D12_BLEND_OP_ADD;

        // 允許寫入 RGBA 所有 Color Channel
        rt.RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;

        description.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
        description.DepthStencilState.DepthEnable = FALSE;
        description.DepthStencilState.DepthWriteMask = 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;
    }

    void initPipelineState(PipelineState& pipelineState, RootSignature& rs, Shader& vs, Shader& ps)
    {
        const D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc =
            createSkyPipelineStateDescription(rs.get(), vs.getCompiledBlob(), ps.getCompiledBlob());
        pipelineState.init(psoDesc);
    }

    // 初始化太陽專用的 Pipeline State
    void initLightPipelineState(PipelineState& pipelineState, RootSignature& rs, Shader& vs, Shader& ps)
    {
        // 建立包含 Alpha Blending 設定的太陽 Pipeline 描述
        const D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc =
            createLightStateDescription(rs.get(), vs.getCompiledBlob(), ps.getCompiledBlob());

        // 建立太陽使用的 Pipeline State Object
        pipelineState.init(psoDesc);
    }
}

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow)
{
    try
    {
        SkylineDebugger::Initialize("DayX_Light");

        SKYLINE_LOG(INFO) << "[DayX_Light] Initializing window...";
        initWindow(hInstance, hPrevInstance, lpCmdLine, nCmdShow, TEXT("Game"));
        if (g_hWnd == nullptr)
            throw std::runtime_error("DayX_Light: Failed to create the application window.");

        SKYLINE_LOG(INFO) << "[DayX_Light] Initializing graphics engine...";
        GraphicsEngine graphicsEngine;
        graphicsEngine.init(g_hWnd, FRAME_BUFFER_W, FRAME_BUFFER_H);

        // 設定太陽初始位置、大小、亮度、顏色與畫面比例
        SunConstants sunConstants{
            Vector2{-0.55f, 0.25f},
            0.08f,
            1.5f,
            Vector3{1.0f, 0.85f, 0.55f},
            static_cast<float>(FRAME_BUFFER_W) / static_cast<float>(FRAME_BUFFER_H)
        };

        // 建立儲存 SunConstants 的 Constant Buffer
        ConstantBuffer sunConstantBuffer;

        // 將太陽初始參數寫入 Constant Buffer
        sunConstantBuffer.init(sizeof(SunConstants), &sunConstants);

        SkylineDebugger::ConfigureD3D12(graphicsEngine.getD3DDevice());

        SKYLINE_LOG(INFO) << "[DayX_Light] Initializing root signature...";
        RootSignature rootSignature;
        initRootSignature(rootSignature);

        SKYLINE_LOG(INFO) << "[DayX_Light] Compiling shaders...";
        Shader skyVS, skyPS;
        skyVS.loadVS("assets/shaders/sky_gradient.hlsl", "VSMain");
        skyPS.loadPS("assets/shaders/sky_gradient.hlsl", "PSMain");

        // 建立太陽使用的 Vertex Shader 與 Pixel Shader
        Shader sunVS, sunPS;

        // 載入產生太陽 Fullscreen Triangle 的 Vertex Shader
        sunVS.loadVS("assets/shaders/sun_2D.hlsl", "VSMain");

        // 載入計算太陽圓盤與光暈的 Pixel Shader
        sunPS.loadPS("assets/shaders/sun_2D.hlsl", "PSMain");

        SKYLINE_LOG(INFO) << "[DayX_Light] Creating pipeline state...";
        PipelineState skyPipelineState;
        initPipelineState(skyPipelineState, rootSignature, skyVS, skyPS);

        // 建立太陽專用的 Pipeline State
        PipelineState sunPipelineState;

        // 使用太陽 Shader 與 Alpha Blending 設定初始化 Pipeline
        initLightPipelineState(sunPipelineState, rootSignature, sunVS, sunPS);

        SKYLINE_LOG(INFO) << "[DayX_Light] Initialization completed.";
        RenderContext& renderContext = graphicsEngine.getRenderContext();

        bool isFirstFrame = true;
        while (dispatchWindowMessage())
        {
            if (isFirstFrame)
                SKYLINE_LOG(INFO) << "[DayX_Light] Rendering first frame...";

            graphicsEngine.beginRender();

            // 每幀取得目前 Frame Buffer 的實際寬高比
            sunConstants.aspectRatio =
                static_cast<float>(graphicsEngine.getFrameBufferWidth()) /
                static_cast<float>(graphicsEngine.GetFrameBufferHeight());

            // 將更新後的太陽參數同步到 GPU
            sunConstantBuffer.copyToVRAM(sunConstants);

            renderContext.setRootSignature(rootSignature);
            renderContext.setPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

            renderContext.setPipelineState(skyPipelineState);
            renderContext.draw(3);

            // 切換成支援 Alpha Blending 的太陽 Pipeline
            renderContext.setPipelineState(sunPipelineState);

            // 將 SunConstants 綁定到 Root Parameter 0 對應的 Constant Buffer
            renderContext.setGraphicsRootConstantBufferView(
                0,
                sunConstantBuffer.getGPUVirtualAddress());

            // 繪製 Fullscreen Triangle,並由 Pixel Shader 決定太陽實際顯示區域
            renderContext.draw(3);

            graphicsEngine.endRender();

            SkylineDebugger::LogD3D12Messages(graphicsEngine.getD3DDevice());
            if (isFirstFrame)
            {
                SKYLINE_LOG(INFO) << "[DayX_Light] First frame presented.";
                isFirstFrame = false;
            }
        }

        SkylineDebugger::Shutdown();
        return EXIT_SUCCESS;
    }
    catch (const std::exception& exception)
    {
        SkylineDebugger::ShowFatalError("DayX_Light initialization failed", exception);
        SkylineDebugger::Shutdown();
        return EXIT_FAILURE;
    }
    catch (...)
    {
        SkylineDebugger::ShowFatalError("DayX_Light initialization failed", "An unknown fatal error occurred.");
        SkylineDebugger::Shutdown();
        return EXIT_FAILURE;
    }
}

結果


上一篇
Day 13 : 天空背景
下一篇
Day 15:Model View Projection
系列文
因為 AI 看不懂老舊程式,只好乖乖從零開始學 DirectX 12 與 HLSL23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言