DirectX11 With Windows SDK--23 立方體映射:動態天空盒的實現

来源:https://www.cnblogs.com/X-Jun/archive/2018/11/03/9900694.html
-Advertisement-
Play Games

前言 上一章的靜態天空盒已經可以滿足絕大部分日常使用了。但對於自帶反射/折射屬性的物體來說,它需要依賴天空盒進行繪製,但靜態天空盒並不會記錄周邊的物體,更不用說正在其周圍運動的物體了。因此我們需要在運行期間構建動態天空盒,將周邊物體繪製入當前的動態天空盒。 沒瞭解過靜態天空盒的讀者請先移步到下麵的鏈 ...


前言

上一章的靜態天空盒已經可以滿足絕大部分日常使用了。但對於自帶反射/折射屬性的物體來說,它需要依賴天空盒進行繪製,但靜態天空盒並不會記錄周邊的物體,更不用說正在其周圍運動的物體了。因此我們需要在運行期間構建動態天空盒,將周邊物體繪製入當前的動態天空盒。

沒瞭解過靜態天空盒的讀者請先移步到下麵的鏈接

章節回顧
22 立方體映射:靜態天空盒的讀取與實現

DirectX11 With Windows SDK完整目錄

Github項目源碼

歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什麼問題也可以在這裡彙報。

動態天空盒

現在如果我們要讓擁有反射/折射屬性的物體映射其周圍的物體和天空盒的話,就需要在每一幀重建動態天空盒,具體做法為:在每一幀將攝像機放置在待反射/折射物體中心,然後沿著各個坐標軸渲染除了自己以外的所有物體及靜態天空盒共六次,一次對應紋理立方體的一個面。這樣繪製好的動態天空盒就會記錄下當前幀各物體所在的位置了。

但是這樣做會帶來非常大的性能開銷,加上動態天空盒後,現在一個場景就要渲染七次,對應七個不同的渲染目標!如果要使用的話,儘可能減少所需要用到的動態天空盒數目。對於多個物體來說,你可以只對比較重要,關註度較高的反射/折射物體使用動態天空盒,其餘的仍使用靜態天空盒,甚至不用。畢竟動態天空盒也不是用在場景繪製,而是在物體上,可以不需要跟靜態天空盒那樣大的解析度,通常情況下設置到256x256即可.

資源視圖(Resource Views)回顧

由於動態天空盒的實現同時要用到渲染目標視圖(Render Target View)深度模板視圖(Depth Stencil View)著色器資源視圖(Shader Resource View),這裡再進行一次回顧。

由於資源(ID3D11Resource)本身的類型十分複雜,比如一個ID3D11Texture2D本身既可以是一個紋理,也可以是一個紋理數組,但紋理數組在元素個數為6時有可能會被用作立方體紋理,就這樣直接綁定到渲染管線上是無法確定它本身究竟要被用作什麼樣的類型的。比如說作為著色器資源,它可以是Texture2D, Texture2DArray, TextureCube的任意一種。

因此,我們需要用到一種叫資源視圖(Resource Views)的類型,它主要有下麵4種功能:

  1. 綁定要使用的資源
  2. 解釋該資源具體會被用作什麼類型
  3. 指定該資源的元素範圍,以及紋理的子資源範圍
  4. 說明該資源最終在渲染管線上的用途

渲染目標視圖用於將渲染管線的運行結果輸出給其綁定的資源,即僅能設置給輸出合併階段。這意味著該資源主要用於寫入,但是在進行混合操作時還需要讀取該資源。通常渲染目標是一個二維的紋理,但它依舊可能會綁定其餘類型的資源。這裡不做討論。

深度/模板視圖同樣用於設置給輸出合併階段,但是它用於深度測試和模板測試,決定了當前像素是通過還是會被拋棄,並更新深度/模板值。它允許一個資源同時綁定到深度模板視圖和著色器資源視圖,但是兩個資源視圖此時都是只讀的,深度/模板視圖也無法對其進行修改,這樣該紋理就還可以綁定到任意允許的可編程著色器階段上。如果要允許深度/模板緩衝區進行寫入,則應該取消綁定在著色器的資源視圖。

著色器資源視圖提供了資源的讀取許可權,可以用於渲染管線的所有可編程著色器階段中。通常該視圖多用於像素著色器階段,但要註意無法通過著色器寫入該資源。

DynamicSkyRender類

該類繼承自上一章的SkyRender類,用以支持動態天空盒的相關操作。


class DynamicSkyRender : public SkyRender
{
public:
    DynamicSkyRender(ComPtr<ID3D11Device> device,
        ComPtr<ID3D11DeviceContext> deviceContext,
        const std::wstring& cubemapFilename,
        float skySphereRadius,      // 天空球半徑
        int dynamicCubeSize,        // 立方體棱長
        bool generateMips = false); // 預設不為靜態天空盒生成mipmaps
                                    // 動態天空盒必然生成mipmaps

    DynamicSkyRender(ComPtr<ID3D11Device> device,
        ComPtr<ID3D11DeviceContext> deviceContext,
        const std::vector<std::wstring>& cubemapFilenames,
        float skySphereRadius,      // 天空球半徑
        int dynamicCubeSize,        // 立方體棱長
        bool generateMips = false); // 預設不為靜態天空盒生成mipmaps
                                    // 動態天空盒必然生成mipmaps


    // 緩存當前渲染目標視圖
    void Cache(ComPtr<ID3D11DeviceContext> deviceContext, BasicEffect& effect);

    // 指定天空盒某一面開始繪製,需要先調用Cache方法
    void BeginCapture(ComPtr<ID3D11DeviceContext> deviceContext, BasicEffect& effect, D3D11_TEXTURECUBE_FACE face,
        const DirectX::XMFLOAT3& pos, float nearZ = 1e-3f, float farZ = 1e3f);

    // 恢復渲染目標視圖及攝像機,並綁定當前動態天空盒
    void Restore(ComPtr<ID3D11DeviceContext> deviceContext, BasicEffect& effect, const Camera& camera);

    // 獲取動態天空盒
    // 註意:該方法只能在
    ComPtr<ID3D11ShaderResourceView> GetDynamicTextureCube();

    // 獲取當前用於捕獲的天空盒
    const Camera& GetCamera() const;

private:
    void InitResource(ComPtr<ID3D11Device> device, int dynamicCubeSize);

private:
    ComPtr<ID3D11RenderTargetView>      mCacheRTV;      // 臨時緩存的後備緩衝區
    ComPtr<ID3D11DepthStencilView>      mCacheDSV;      // 臨時緩存的深度/模板緩衝區
    
    FirstPersonCamera                   mCamera;                // 捕獲當前天空盒其中一面的攝像機
    ComPtr<ID3D11DepthStencilView>      mDynamicCubeMapDSV;     // 動態天空盒渲染對應的深度/模板視圖
    ComPtr<ID3D11ShaderResourceView>    mDynamicCubeMapSRV;     // 動態天空盒對應的著色器資源視圖
    ComPtr<ID3D11RenderTargetView>      mDynamicCubeMapRTVs[6]; // 動態天空盒每個面對應的渲染目標視圖
    
};

構造函數在完成靜態天空盒的初始化後,就會調用DynamicSkyRender::InitResource方法來初始化動態天空盒。

Render-To-Texture 技術

因為之前的個人教程把計算著色器給跳過了,Render-To-Texture剛好又在龍書里的這章,只好把它帶到這裡來講了。

在我們之前的程式中,我們都是渲染到後備緩衝區里。經過了這麼多的章節,應該可以知道它的類型是ID3D11Texture2D,僅僅是一個2D紋理罷了。在d3dApp類里可以看到這部分的代碼:

// 重設交換鏈並且重新創建渲染目標視圖
ComPtr<ID3D11Texture2D> backBuffer;
HR(mSwapChain->ResizeBuffers(1, mClientWidth, mClientHeight, DXGI_FORMAT_R8G8B8A8_UNORM, 0));   // 使用了Direcr2D交互時則為DXGI_FORMAT_B8G8R8A8_UNORM
HR(mSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast<void**>(backBuffer.GetAddressOf())));
HR(md3dDevice->CreateRenderTargetView(backBuffer.Get(), 0, mRenderTargetView.GetAddressOf()));
backBuffer.Reset();

這裡渲染目標視圖綁定的是重新調整過大小的後備緩衝區。然後把該視圖交給輸出合併階段:

// 將渲染目標視圖和深度/模板緩衝區結合到管線
md3dImmediateContext->OMSetRenderTargets(1, mRenderTargetView.GetAddressOf(), mDepthStencilView.Get());

這樣經過一次繪製指令後就會將管線的運行結果輸出到該視圖綁定的後備緩衝區上,待所有繪製完成後,再調用IDXGISwapChain::Present方法來交換前/後臺以達到畫面更新的效果。

如果渲染目標視圖綁定的是新建的2D紋理,而非後備緩衝區的話,那麼渲染結果將會輸出到該紋理上,並且不會直接在屏幕上顯示出來。然後我們就可以使用該紋理做一些別的事情,比如綁定到著色器資源視圖供可編程著色器使用,又或者將結果保存到文件等等。

雖然這個技術並不高深,但它的應用非常廣泛:

  1. 小地圖的實現
  2. 陰影映射(Shadow mapping)
  3. 屏幕空間環境光遮蔽(Screen Space Ambient Occlusion)
  4. 利用天空盒實現動態反射/折射(Dynamic reflections/refractions with cube maps)

DynamicSkyRender::InitResource方法--初始化動態紋理立方體資源

創建動態紋理立方體和對應渲染目標視圖、著色器資源視圖

在更新動態天空盒的時候,該紋理將會被用做渲染目標;而完成渲染後,它將用作著色器資源視圖用於球體反射/折射的渲染。因此它需要在BindFlag設置D3D11_BIND_RENDER_TARGETD3D11_BIND_SHADER_RESOURCE


void DynamicSkyRender::InitResource(ComPtr<ID3D11Device> device, int dynamicCubeSize)
{
    //
    // 1. 創建紋理數組
    //

    ComPtr<ID3D11Texture2D> texCube;
    D3D11_TEXTURE2D_DESC texDesc;

    texDesc.Width = dynamicCubeSize;
    texDesc.Height = dynamicCubeSize;
    texDesc.MipLevels = 0;
    texDesc.ArraySize = 6;
    texDesc.SampleDesc.Count = 1;
    texDesc.SampleDesc.Quality = 0;
    texDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    texDesc.Usage = D3D11_USAGE_DEFAULT;
    texDesc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;
    texDesc.CPUAccessFlags = 0;
    texDesc.MiscFlags = D3D11_RESOURCE_MISC_GENERATE_MIPS | D3D11_RESOURCE_MISC_TEXTURECUBE;
    
    // 現在texCube用於新建紋理
    HR(device->CreateTexture2D(&texDesc, nullptr, texCube.ReleaseAndGetAddressOf()));

    // ...

MipLevels設置為0是要說明該紋理將會在後面生成完整的mipmap鏈,但不代表創建紋理後立即就會生成,需要在後續通過GenerateMips方法才會生成出來。為此,還需要在MiscFlags設置D3D11_RESOURCE_MISC_GENERATE_MIPS。當然,把該紋理用作天空盒的D3D11_RESOURCE_MISC_TEXTURECUBE標簽也不能漏掉。

接下來就是創建渲染目標視圖的部分,紋理數組中的每個紋理都需要綁定一個渲染目標視圖:

    //
    // 2. 創建渲染目標視圖
    //

    D3D11_RENDER_TARGET_VIEW_DESC rtvDesc;
    rtvDesc.Format = texDesc.Format;
    rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2DARRAY;
    rtvDesc.Texture2DArray.MipSlice = 0;
    // 一個視圖只對應一個紋理數組元素
    rtvDesc.Texture2DArray.ArraySize = 1;

    // 每個元素創建一個渲染目標視圖
    for (int i = 0; i < 6; ++i)
    {
        rtvDesc.Texture2DArray.FirstArraySlice = i;
        HR(device->CreateRenderTargetView(
            texCube.Get(),
            &rtvDesc,
            mDynamicCubeMapRTVs[i].GetAddressOf()));
    }
    
    // ...

最後就是為整個紋理數組以天空盒的形式創建著色器資源視圖:

    //
    // 3. 創建著色器目標視圖
    //

    D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc;
    srvDesc.Format = texDesc.Format;
    srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURECUBE;
    srvDesc.TextureCube.MostDetailedMip = 0;
    srvDesc.TextureCube.MipLevels = -1;     // 使用所有的mip等級

    HR(device->CreateShaderResourceView(
        texCube.Get(),
        &srvDesc,
        mDynamicCubeMapSRV.GetAddressOf()));

到這裡還沒有結束。

為動態天空盒創建深度緩衝區和視口

通常天空盒的面解析度和後備緩衝區的解析度不一致,這意味著我們還需要創建一個和天空盒錶面解析度一致的深度緩衝區(無模板測試):

    //
    // 4. 創建深度/模板緩衝區與對應的視圖
    //

    texDesc.Width = dynamicCubeSize;
    texDesc.Height = dynamicCubeSize;
    texDesc.MipLevels = 0;
    texDesc.ArraySize = 1;
    texDesc.SampleDesc.Count = 1;
    texDesc.SampleDesc.Quality = 0;
    texDesc.Format = DXGI_FORMAT_D32_FLOAT;
    texDesc.Usage = D3D11_USAGE_DEFAULT;
    texDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
    texDesc.CPUAccessFlags = 0;
    texDesc.MiscFlags = 0;

    ComPtr<ID3D11Texture2D> depthTex;
    device->CreateTexture2D(&texDesc, nullptr, depthTex.GetAddressOf());

    D3D11_DEPTH_STENCIL_VIEW_DESC dsvDesc;
    dsvDesc.Format = texDesc.Format;
    dsvDesc.Flags = 0;
    dsvDesc.ViewDimension = D3D11_DSV_DIMENSION_TEXTURE2D;
    dsvDesc.Texture2D.MipSlice = 0;

    HR(device->CreateDepthStencilView(
        depthTex.Get(),
        &dsvDesc,
        mDynamicCubeMapDSV.GetAddressOf()));

同樣,視口也需要經過適配。不過之前的攝像機類可以幫我們簡化一下:

    //
    // 5. 初始化視口
    //

    mCamera.SetViewPort(0.0f, 0.0f, static_cast<float>(dynamicCubeSize), static_cast<float>(dynamicCubeSize));
}

動態天空盒的繪製

講完了初始化的事,就要開始留意幀與幀之間的動態天空盒渲染操作了。除了繪製部分以外的操作都交給了DynamicSkyRender類來完成。總結如下(粗體部分為該方法完成的任務):

  1. 緩存設備上下文綁定的後備緩衝區、深度/模板緩衝區
  2. 清空設置在像素著色器的著色器資源視圖(綁定了動態天空盒資源)
  3. 對準某一個坐標軸,以90度垂直視野(FOV),1.0f的寬高比架設攝像機,並調整視口
  4. 清理當前天空盒面對應的紋理和深度緩衝區,並綁定到設備上下文
  5. 和往常一樣繪製物體和靜態天空盒
  6. 回到步驟3,繼續下一個面的繪製,直到6個面都完成渲染
  7. 為設備上下文恢復後備緩衝區、深度/模板緩衝區並釋放內部緩存(防止交換鏈ResizeBuffer時因為引用的遺留出現問題)
  8. 讓動態天空盒生成mipmap鏈,並將其綁定到像素著色器
  9. 利用動態天空盒繪製反射/折射物體,和往常一樣繪製剩餘物體,並利用靜態天空盒繪製天空

DynamicSkyRender::Cache方法--緩存渲染目標視圖

該方法對應上面所說的第1,2步:

void DynamicSkyRender::Cache(ComPtr<ID3D11DeviceContext> deviceContext, BasicEffect& effect)
{
    deviceContext->OMGetRenderTargets(1, mCacheRTV.GetAddressOf(), mCacheDSV.GetAddressOf());

    // 清掉綁定在著色器的動態天空盒,需要立即生效
    effect.SetTextureCube(nullptr);
    effect.Apply(deviceContext);
}

DynamicSkyRender::BeginCapture方法--指定天空盒某一面開始繪製

該方法對應上面所說的第3,4步:

void DynamicSkyRender::BeginCapture(ComPtr<ID3D11DeviceContext> deviceContext, BasicEffect& effect, D3D11_TEXTURECUBE_FACE face,
    const XMFLOAT3& pos, float nearZ, float farZ)
{
    static XMVECTORF32 ups[6] = {
        {{ 0.0f, 1.0f, 0.0f, 0.0f }},   // +X
        {{ 0.0f, 1.0f, 0.0f, 0.0f }},   // -X
        {{ 0.0f, 0.0f, -1.0f, 0.0f }},  // +Y
        {{ 0.0f, 0.0f, 1.0f, 0.0f }},   // -Y
        {{ 0.0f, 1.0f, 0.0f, 0.0f }},   // +Z
        {{ 0.0f, 1.0f, 0.0f, 0.0f }}    // -Z
    };

    static XMVECTORF32 looks[6] = {
        {{ 1.0f, 0.0f, 0.0f, 0.0f }},   // +X
        {{ -1.0f, 0.0f, 0.0f, 0.0f }},  // -X
        {{ 0.0f, 1.0f, 0.0f, 0.0f }},   // +Y
        {{ 0.0f, -1.0f, 0.0f, 0.0f }},  // -Y
        {{ 0.0f, 0.0f, 1.0f, 0.0f }},   // +Z
        {{ 0.0f, 0.0f, -1.0f, 0.0f }},  // -Z
    };
    
    // 設置天空盒攝像機
    mCamera.LookTo(XMLoadFloat3(&pos) , looks[face].v, ups[face].v);
    mCamera.UpdateViewMatrix();
    // 這裡儘可能捕獲近距離物體
    mCamera.SetFrustum(XM_PIDIV2, 1.0f, nearZ, farZ);

    // 應用觀察矩陣、投影矩陣
    effect.SetViewMatrix(mCamera.GetViewXM());
    effect.SetProjMatrix(mCamera.GetProjXM());

    // 清空緩衝區
    deviceContext->ClearRenderTargetView(mDynamicCubeMapRTVs[face].Get(), reinterpret_cast<const float*>(&Colors::Black));
    deviceContext->ClearDepthStencilView(mDynamicCubeMapDSV.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);
    // 設置渲染目標和深度模板視圖
    deviceContext->OMSetRenderTargets(1, mDynamicCubeMapRTVs[face].GetAddressOf(), mDynamicCubeMapDSV.Get());
    // 設置視口
    deviceContext->RSSetViewports(1, &mCamera.GetViewPort());
}

在調用該方法後,就可以開始繪製到天空盒的指定面了,直到下一次DynamicSkyRender::BeginCaptureDynamicSkyRender::Restore被調用。

DynamicSkyRender::Restore方法--恢復之前綁定的資源並清空緩存

該方法對應上面所說的第7,8步:

void DynamicSkyRender::Restore(ComPtr<ID3D11DeviceContext> deviceContext, BasicEffect& effect, const Camera & camera)
{
    // 恢復預設設定
    deviceContext->RSSetViewports(1, &camera.GetViewPort());
    deviceContext->OMSetRenderTargets(1, mCacheRTV.GetAddressOf(), mCacheDSV.Get());

    // 生成動態天空盒後必須要生成mipmap鏈
    deviceContext->GenerateMips(mDynamicCubeMapSRV.Get());

    effect.SetViewMatrix(camera.GetViewXM());
    effect.SetProjMatrix(camera.GetProjXM());
    // 恢復綁定的動態天空盒
    effect.SetTextureCube(mDynamicCubeMapSRV);

    // 清空臨時緩存的渲染目標視圖和深度模板視圖
    mCacheDSV.Reset();
    mCacheRTV.Reset();
}

GameApp::DrawScene方法

在GameApp類多了這樣一個重載的成員函數:

void GameApp::DrawScene(bool drawCenterSphere);

該方法額外添加了一個參數,僅用於控制中心球是否要繪製,而其餘的物體不管怎樣都是要繪製出來的。使用該重載方法有利於減少代碼重覆,這裡面的大部分物體都需要繪製7次。

假如只考慮Daylight天空盒的話,無形參的GameApp::DrawScene方法關於3D場景的繪製可以簡化成這樣:


void GameApp::DrawScene()
{
    // ******************
    // 生成動態天空盒
    //
    
    // 保留當前繪製的渲染目標視圖和深度模板視圖
    mDaylight->Cache(md3dImmediateContext, mBasicEffect);
    
    // 繪製動態天空盒的每個面(以球體為中心)
    for (int i = 0; i < 6; ++i)
    {
        mDaylight->BeginCapture(md3dImmediateContext, mBasicEffect, 
            XMFLOAT3(), static_cast<D3D11_TEXTURECUBE_FACE>(i));

        // 不繪製中心球
        DrawScene(false);
    }
    
    // 恢復之前的繪製設定
    mDaylight->Restore(md3dImmediateContext, mBasicEffect, *mCamera);
    
    // ******************
    // 繪製場景
    //

    // 預先清空
    md3dImmediateContext->ClearRenderTargetView(mRenderTargetView.Get(), reinterpret_cast<const float*>(&Colors::Black));
    md3dImmediateContext->ClearDepthStencilView(mDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);
    
    // 繪製中心球
    DrawScene(true);
    
    // 省略文字繪製部分...
}

至於有形參的GameApp::DrawScene方法就不在這裡給出,可以在項目源碼看到。

使用幾何著色器的動態天空盒

這部分內容並沒有融入到項目中,因此只是簡單地提及一下。

在上面的內容中,我們對一個場景繪製了6次,從而生成動態天空盒。為了減少繪製調用,這裡可以使用幾何著色器來使得只需要進行1次繪製調用就可以生成整個動態天空盒。

首先,創建一個渲染目標視圖綁定整個紋理數組:

D3D11_RENDER_TARGET_VIEW_DESC rtvDesc;
rtvDesc.Format = texDesc.Format;
rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2DARRAY;
rtvDesc.Texture2DArray.FirstArraySlice = 0;
rtvDesc.Texture2DArray.ArraySize = 6;
rtvDesc.Texture2DArray.MipSlice = 0;
HR(device->CreateRenderTargetView(
    texCube.Get(),
    &rtvDesc,
    mDynamicCubeMapRTV.GetAddressOf()));

rtvDesc.

緊接著,就是要創建一個深度緩衝區數組(一個對應立方體面,元素個數為6):

D3D11_DEPTH_STENCIL_VIEW_DESC dsvDesc;
dsvDesc.Format = DXGI_FORMAT_D32_FLOAT;
dsvDesc.ViewDimension = D3D11_DSV_DIMENSION_TEXTURE2DARRAY;
dsvDesc.Texture2DArray.FirstArraySlice = 0;
dsvDesc.Texture2DArray.ArraySize = 6;
dsvDesc.Texture2DArray.MipSlice = 0;
HR(device->CreateDepthStencilView(
    depthTexArray.Get(),
    &dsvDesc,
    mDynamicCubeMapDSV.GetAddressOf()));

在輸出合併階段這樣綁定到渲染管線:

deviceContext->OMSetRenderTargets(1, 
    mDynamicCubeMapRTV.Get(),
    mDynamicCubeMapDSV.Get());

這樣做會使得一次調用繪製可以同時向該渲染目標視圖對應的六個紋理進行渲染。

在HLSL,現在需要同時在常量緩衝區提供6個觀察矩陣。頂點著色階段將頂點直接傳遞給幾何著色器,然後幾何著色器重覆傳遞一個頂點六次,但區別在於每次將會傳遞給不同的渲染目標。這需要依賴系統值SV_RenderTargetArrayIndex來實現,它是一個整型索引值,並且只能由幾何著色器寫入來指定當前需要往渲染目標視圖所綁定的紋理數組中的哪一個紋理。該系統值只能用於綁定了紋理數組的視圖。

struct VertexPosTex
{
    float3 PosL : POSITION;
    float2 Tex : TEXCOORD;
};

struct VertexPosHTexRT
{
    float3 PosH : SV_POSITION;
    float2 Tex : TEXCOORD;
    uint RTIndex : SV_RenderTargetArrayIndex;
};


[maxvertexcount(18)]
void GS(trangle VertexPosTex input[3],
    inout TriangleStream<VertexPosTexRT> output)
{
     
    for (int i = 0; i < 6; ++i)
    {
        VertexPosTexRT vertex;
        // 指定該三角形到第i個渲染目標
        vertex.RTIndex = i;
        
        for (int j = 0; j < 3; ++j)
        {
            vertex.PosH = mul(input[j].PosL, mul(gViews[i], gProj));
            vertex.Tex = input[j].Tex;
            
            output.Append(vertex);
        }
        output.RestartStrip();
    }
}

上面的代碼是經過魔改的,至於與它相關的示例項目CubeMapGS只能在舊版的Microsoft DirectX SDK的Samples中看到了。

這種方法有兩點不那麼吸引人的原因:

  1. 它使用幾何著色器來輸出大量的數據。不過放眼現在的顯卡應該不會損失多大的性能。
  2. 在一個典型的場景中,一個三角形不會出現在兩個或以上的立方體錶面,不管怎樣,這5次繪製都沒法通過裁剪,顯得十分浪費。雖然在我們的項目中,一開始的做法也是將整個場景繪製到天空盒的一面,但是我們還可以使用視錐體裁剪技術來剔除掉那些不在視錐體的物體。使用幾何著色器的方法不能進行提前的裁剪。

但還有一種情況它的表現還算不俗。假如你現在有一個動態天空系統,這些雲層會移動,並且顏色隨著時間變化。因為天空正在實時變化,我們不能使用預先烘焙的天空盒紋理來進行反射/折射。使用幾何著色器繪製天空盒的方法在性能上不會損失太大。

模型的折射

dielectric(絕緣體?)是指能夠折射光線的透明材料,如下圖。當光束射到絕緣體錶面時,一部分光會被反射,還有一部分光會基於斯涅爾定律進行折射。公式如下:
\[n_{1}sinθ_{1} = n_{2}sinθ_{2}\]

其中n1和n2分半是兩個介質的折射率,θ1和θ2分別是入射光、折射光與界面法線的夾角,叫做入射角和折射角。

n1 = n2時,θ1 = θ2(無折射)
n2 > n1時,θ2 < θ1(光線向內彎折)
n1 > n2時,θ2 > θ1(光線向外彎折)

在物理上,光線在從絕緣體出來後還會進行一次彎折。但是在實時渲染中,通常只考慮第一次折射的情況。

HLSL提供了固有函數refract來幫助我們計算折射向量:

float3 refract(float3 incident, float3 normal, float eta);

incident指的是入射光向量
normal指的是交界面處的法向量(與入射光點乘的結果為負值)
eta指的是n1/n2,即介質之間的折射比

通常,空氣的折射率為1.0,水的折射率為1.33,玻璃的折射率為1.51.

之前的項目中Material::Reflect來調整反射顏色,現在你可以拿它來調整折射顏色。

在HLSL里,你只需要在像素著色器中加上這部分代碼,就可以實現折射效果了(gEta出現在常量緩衝區中):

// 折射
if (gRefractionEnabled)
{
    float3 incident = -toEyeW;
    float3 refractionVector = refract(incident, pIn.NormalW, gEta);
    float4 refractionColor = texCube.Sample(sam, refractionVector);

    litColor += gMaterial.Reflect * refractionColor;
}

項目演示

該項目實現了反射和折射

DirectX11 With Windows SDK完整目錄

Github項目源碼

歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什麼問題也可以在這裡彙報。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 模板類中,或模板函數中,若限定模板參數為數值類型,可以使用如下方式進行判斷. 以上代碼節選自muduo. 其中主要推斷方式是通過調用std::is_arithmetic<T>. 若 T 為算術類型(即整數類型或浮點類型)或其修飾類型(添加註入const等),則提供等於 true 的成員常量 valu ...
  • 變數來源於數學,是電腦語言中能儲存計算結果或能表示值抽象概念。變數可以通過變數名訪問。 ...
  • 一、介紹和使用 Lombok 是一個 java 庫,能以簡單的註解形式來簡化 java 代碼,提高開發人員的開發效率。 常見使用在開發過程中需要寫的 javabean,往往開發需要花時間去添加相應的 getter/setter,也許還要去寫構造器、equals等方法,而且需要維護,當屬性多時會出現大 ...
  • 目錄 一. 正則表達式 二. 特殊的元字元 三. python3的re模塊方法 四. python3的re模塊練習 五. 第一章課後練習題 六. re模塊綜合應用之計算器 一. 正則表達式 正則表達式是由一堆字元和特殊符號組成的字元串。它可以為我們提供高級的文本搜索,匹配,替換功能。當然,正則表達式 ...
  • ServerBootstrap與Bootstrap分別是netty中服務端與客戶端的引導類,主要負責服務端與客戶端初始化、配置及啟動引導等工作,接下來我們就通過netty源碼中的示例對ServerBootstrap與Bootstrap的源碼進行一個簡單的分析。首先我們知道這兩個類都繼承自Abstra ...
  • 迴圈是流程式控制制的又一重要結構,“白天-黑夜-白天-黑夜”屬於時間上的迴圈,古人“年復一年、日復一日”的“日出而作、日落而息”便是每天周而複始的生活。電腦程式處理迴圈結構時,給定一段每次都要執行的代碼塊,然後分別指定迴圈的開始條件和結束條件,就形成了常見的迴圈語句。最簡單的迴圈結構只需一個while ...
  • 《PHP核心技術與最佳實踐》是一本致力於為希望成為中高級PHP程式員的讀者提供高效而有針對性指導的經典著作。系統歸納和深刻解讀了PHP開發中的編程思想、底層原理、核心技術、開發技巧、編碼規範和最佳實踐。全書分為5個部分:第一部分(1~2章)從不同的角度闡述了面向對象軟體設計思想的核心概念、技術和原則 ...
  • T1 Adjoin 【問題描述】 定義一種合法的$0 1$串:串中任何一個數字都與$1$相鄰。例如長度為$ 3 的 0 1 $串中,$101$是非法的,因為兩邊的$1$沒有相鄰的$1,011$是合法的,因為三個數都有$1$相鄰。現在問,長度為$N$的$0 1$中有多少是合法的。 【輸入格式】 一行, ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...