手把手教你:輕鬆打造沉浸感十足的動態漫反射全域性光照

語言: CN / TW / HK

一個沉浸感十足的遊戲,其場景中的全域性光照效果一定功不可沒。

動態漫反射全域性光照(DDGI)帶來的光影變化,是細膩延展的視覺語言,讓場景中每種顏色都有了“五彩斑斕”的詮釋,場景佈局光影,物體關係立顯,環境溫度降臨,拓展了畫面資訊傳達的層次,點睛之筆。

直接光渲染 VS 動態漫反射全域性光照

細膩的光照視覺語言帶來的技術挑戰不小。不同材質表面與光照產生的呈現效果千差萬別,漫反射(Diffuse)將光照資訊均勻散射,光線的強弱、光照動勢、物體表面材質的變換等,面對這些浮動的變數,平臺效能和算力備受考驗。

針對全域性光照需克服的複雜“症狀”,HMS Core圖形引擎服務提供了一套實時動態漫反射全域性光照(DDGI)技術,面向移動端,可擴充套件到全平臺,無需預烘培。基於光照探針(Light Probe)管線,在Probe更新和著色時提出改進演算法,降低原有管線的計算負載。實現多次反射資訊的全域性光照,提升了渲染真實感,滿足移動終端裝置實時性、互動性要求。

並且,實現一個沉浸感滿滿的動態漫反射全域性光照,幾步就能輕鬆搞定!

Demo示例

開發指南

步驟說明

1.初始化階段:設定Vulkan執行環境,初始化DDGIAPI類。

2.準備階段:

建立用於儲存DDGI渲染結果的兩張Texture,並將Texture的資訊傳遞給DDGI。

準備DDGI外掛需要的Mesh、Material、Light、Camera、解析度等資訊,並將其傳遞給DDGI。

設定DDGI引數。

3.渲染階段

如果場景的Mesh變換矩陣、Light、Camera資訊有變化,則同步更新到DDGI側。

呼叫Render()函式,DDGI的渲染結果儲存在準備階段建立的Texture中。

將DDGI的結果融入到著色計算中。

美術限制

1.對於想要使能DDGI效果的場景,DDGI的origin引數應該設定為場景的中心,並設定相應步長和Probe數量使得DDGI Volume能覆蓋整個場景。

2.為了讓DDGI獲得合適的遮擋效果,請避免用沒有厚度的牆;如果牆的厚度相對於Probe的密度顯得太薄了,會出現漏光(light leaking)現象。同時,構成牆的平面最好是單面(single-sided)的,也即牆是由兩個單面平面組成。

3.由於是移動端的DDGI方案,因此從效能和功耗角度出發,有以下建議:①控制傳到SDK側的幾何數量(建議5萬頂點以內),比如只將場景中的會產生間接光的主體結構傳到SDK;②儘量使用合適的Probe密度和數量,儘量不要超過101010。以上建議以最終的呈現結果為主。

開發步驟

1、下載外掛的SDK包,解壓後獲取DDGI SDK相關檔案,其中包括1個頭檔案和2個so檔案。Android平臺使用的so庫檔案下載地址請參見:動態漫反射全域性光照外掛

2、該外掛支援Android平臺,使用CMake進行構建。以下是CMakeLists.txt部分片段僅供參考:

cmake_minimum_required(VERSION 3.4.1 FATAL_ERROR)
set(NAME DDGIExample)
project(${NAME})

set(PROJ_ROOT ${CMAKE_CURRENT_SOURCE_DIR})
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14 -O2 -DNDEBUG -DVK_USE_PLATFORM_ANDROID_KHR")
file(GLOB EXAMPLE_SRC "${PROJ_ROOT}/src/*.cpp") # 引入開發者主程式程式碼。
include_directories(${PROJ_ROOT}/include) # 引入標頭檔案,可以將DDGIAPI.h標頭檔案放在此目錄。

# 匯入librtcore.so和libddgi.so
ADD_LIBRARY(rtcore SHARED IMPORTED)
SET_TARGET_PROPERTIES(rtcore
                PROPERTIES IMPORTED_LOCATION
                ${CMAKE_SOURCE_DIR}/src/main/libs/librtcore.so)

ADD_LIBRARY(ddgi SHARED IMPORTED)
SET_TARGET_PROPERTIES(ddgi
                PROPERTIES IMPORTED_LOCATION
                ${CMAKE_SOURCE_DIR}/src/main/libs/libddgi.so)

add_library(native-lib SHARED ${EXAMPLE_SRC})
target_link_libraries(
    native-lib
    ...
    ddgi # 連結ddgi庫。
    rtcore
    android
    log
    z
    ...
)

3、設定Vulkan環境,初始化DDGIAPI類。

// 設定DDGI SDK需要的Vulkan環境資訊。
// 包括logicalDevice, queue, queueFamilyIndex資訊。
void DDGIExample::SetupDDGIDeviceInfo()
{
    m_ddgiDeviceInfo.physicalDevice = physicalDevice;
    m_ddgiDeviceInfo.logicalDevice = device;
    m_ddgiDeviceInfo.queue = queue;
    m_ddgiDeviceInfo.queueFamilyIndex = vulkanDevice->queueFamilyIndices.graphics;    
}

void DDGIExample::PrepareDDGI()
{
    // 設定Vulkan環境資訊。
    SetupDDGIDeviceInfo();
    // 呼叫DDGI的初始化函式。
    m_ddgiRender->InitDDGI(m_ddgiDeviceInfo);
    ...
}

void DDGIExample::Prepare()
{
    ...
    // 建立DDGIAPI物件。
    std::unique_ptr<DDGIAPI> m_ddgiRender = make_unique<DDGIAPI>();
    ...
    PrepareDDGI();
    ...
}

4、建立兩張Texture,用於儲存相機視角的漫反射全域性光照和法線深度圖。為提高渲染效能,Texture支援降解析度的設定。解析度越小,渲染效能表現越好,但渲染結果的走樣,例如邊緣的鋸齒等問題可能會更加嚴重。

// 建立用於儲存渲染結果的Texture。
void DDGIExample::CreateDDGITexture()
{
    VkImageUsageFlags usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
    int ddgiTexWidth = width / m_shadingPara.ddgiDownSizeScale; // 紋理寬度。
    int ddgiTexHeight = height / m_shadingPara.ddgiDownSizeScale; // 紋理高度。
    glm::ivec2 size(ddgiTexWidth, ddgiTexHeight);
    // 建立儲存irradiance結果的Texture。
    m_irradianceTex.CreateAttachment(vulkanDevice,
                                     ddgiTexWidth,
                                     ddgiTexHeight,
                                     VK_FORMAT_R16G16B16A16_SFLOAT,
                                     usage,
                                     VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
                                     m_defaultSampler);
    // 建立儲存normal和depth結果的Texture。
    m_normalDepthTex.CreateAttachment(vulkanDevice,
                                      ddgiTexWidth,
                                      ddgiTexHeight,
                                      VK_FORMAT_R16G16B16A16_SFLOAT,
                                      usage,
                                      VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
                                      m_defaultSampler);
}
// 設定DDGIVulkanImage資訊。
void DDGIExample::PrepareDDGIOutputTex(const vks::Texture& tex, DDGIVulkanImage *texture) const
{
    texture->image = tex.image;
    texture->format = tex.format;
    texture->type = VK_IMAGE_TYPE_2D;
    texture->extent.width = tex.width;
    texture->extent.height = tex.height;
    texture->extent.depth = 1;
    texture->usage = tex.usage;
    texture->layout = tex.imageLayout;
    texture->layers = 1;
    texture->mipCount = 1;
    texture->samples = VK_SAMPLE_COUNT_1_BIT;
    texture->tiling = VK_IMAGE_TILING_OPTIMAL;
}

void DDGIExample::PrepareDDGI()
{
    ...
    // 設定Texture解析度。
    m_ddgiRender->SetResolution(width / m_downScale, height / m_downScale);
    // 設定用於儲存渲染結果的DDGIVulkanImage資訊。
    PrepareDDGIOutputTex(m_irradianceTex, &m_ddgiIrradianceTex);
    PrepareDDGIOutputTex(m_normalDepthTex, &m_ddgiNormalDepthTex);
    m_ddgiRender->SetAdditionalTexHandler(m_ddgiIrradianceTex, AttachmentTextureType::DDGI_IRRADIANCE);
    m_ddgiRender->SetAdditionalTexHandler(m_ddgiNormalDepthTex, AttachmentTextureType::DDGI_NORMAL_DEPTH);
    ...
}

void DDGIExample::Prepare()
{
    ...
    CreateDDGITexture();
    ...
    PrepareDDGI();
    ...
}

5、準備DDGI渲染所需的網格、材質、光源、相機資料。

// mesh結構體,支援submesh。
struct DDGIMesh {
    std::string meshName;
    std::vector<DDGIVertex> meshVertex;
    std::vector<uint32_t> meshIndice;
    std::vector<DDGIMaterial> materials;
    std::vector<uint32_t> subMeshStartIndexes;
    ...
};

// 方向光結構體,當前僅支援1個方向光。
struct DDGIDirectionalLight {
    CoordSystem coordSystem = CoordSystem::RIGHT_HANDED;
    int lightId;
    DDGI::Mat4f localToWorld;
    DDGI::Vec4f color;
    DDGI::Vec4f dirAndIntensity;
};

// 主相機結構體。
struct DDGICamera {
    DDGI::Vec4f pos;
    DDGI::Vec4f rotation;
    DDGI::Mat4f viewMat;
    DDGI::Mat4f perspectiveMat;
};

// 設定DDGI的光源資訊。
void DDGIExample::SetupDDGILights()
{
    m_ddgiDirLight.color = VecInterface(m_dirLight.color);
    m_ddgiDirLight.dirAndIntensity = VecInterface(m_dirLight.dirAndPower);
    m_ddgiDirLight.localToWorld = MatInterface(inverse(m_dirLight.worldToLocal));
    m_ddgiDirLight.lightId = 0;
}

// 設定DDGI的相機資訊。
void DDGIExample::SetupDDGICamera()
{
    m_ddgiCamera.pos = VecInterface(m_camera.viewPos);
    m_ddgiCamera.rotation = VecInterface(m_camera.rotation, 1.0);
    m_ddgiCamera.viewMat = MatInterface(m_camera.matrices.view);
    glm::mat4 yFlip = glm::mat4(1.0f);
    yFlip[1][1] = -1;
    m_ddgiCamera.perspectiveMat = MatInterface(m_camera.matrices.perspective * yFlip);
}

// 準備DDGI需要的網格資訊。
// 以gltf格式的渲染場景為例。
void DDGIExample::PrepareDDGIMeshes()
{
    for (constauto& node : m_models.scene.linearNodes) {
        DDGIMesh tmpMesh;
        tmpMesh.meshName = node->name;
        if (node->mesh) {
            tmpMesh.meshName = node->mesh->name; // 網格的名稱。
            tmpMesh.localToWorld = MatInterface(node->getMatrix()); // 網格的變換矩陣。
            // 網格的骨骼蒙皮矩陣。
            if (node->skin) {
                tmpMesh.hasAnimation = true;
                for (auto& matrix : node->skin->inverseBindMatrices) {
                    tmpMesh.boneTransforms.emplace_back(MatInterface(matrix));
                }
            }
            // 網格的材質節點、頂點資訊。
            for (vkglTF::Primitive *primitive : node->mesh->primitives) {
                ...
            }
        }
        m_ddgiMeshes.emplace(std::make_pair(node->index, tmpMesh));
    }
}

void DDGIExample::PrepareDDGI()
{
    ...
    // 轉換成DDGI需要的資料格式。
    SetupDDGILights();
    SetupDDGICamera();
    PrepareDDGIMeshes();
    ...
    // 向DDGI傳遞資料。
    m_ddgiRender->SetMeshs(m_ddgiMeshes);
    m_ddgiRender->UpdateDirectionalLight(m_ddgiDirLight);
    m_ddgiRender->UpdateCamera(m_ddgiCamera);
    ...
}

6、設定DDGI探針的位置、數量等引數。

// 設定DDGI演算法引數。
void DDGIExample::SetupDDGIParameters()
{
    m_ddgiSettings.origin = VecInterface(3.5f, 1.5f, 4.25f, 0.f);
    m_ddgiSettings.probeStep = VecInterface(1.3f, 0.55f, 1.5f, 0.f);
    ...
}
void DDGIExample::PrepareDDGI()
{
    ...
    SetupDDGIParameters();
    ...
    // 向DDGI傳遞資料。
    m_ddgiRender->UpdateDDGIProbes(m_ddgiSettings);
    ...
}

7、呼叫DDGI的Prepare()函式解析前面傳遞的資料。

void DDGIExample::PrepareDDGI()
{
    ...
    m_ddgiRender->Prepare();
}

8、呼叫DDGI的Render(),將場景的間接光資訊更新快取到步驟4中設定的兩張DDGI Texture中。

*說明

當前版本中,渲染結果為相機視角的漫反射間接光結果圖和法線深度圖,開發者使用雙邊濾波演算法結合法線深度圖將漫反射間接光結果進行上取樣操作,計算得到螢幕尺寸的漫反射全域性光照結果。

如果不呼叫Render()函式,那麼渲染結果為歷史幀的結果。

#define RENDER_EVERY_NUM_FRAME 2
void DDGIExample::Draw()
{
    ...
    // 每兩幀呼叫一次DDGIRender()。
    if (m_ddgiON && m_frameCnt % RENDER_EVERY_NUM_FRAME == 0) {
        m_ddgiRender->UpdateDirectionalLight(m_ddgiDirLight); // 更新光源資訊。
        m_ddgiRender->UpdateCamera(m_ddgiCamera); // 更新相機資訊。
        m_ddgiRender->DDGIRender(); // DDGI渲染(執行)一次,渲染結果儲存在步驟4建立的Texture中。
    }
    ...
}

void DDGIExample::Render()
{
    if (!prepared) {
        return;
    }
    SetupDDGICamera();
    if (!paused || m_camera.updated) {
        UpdateUniformBuffers();
    }
    Draw();
    m_frameCnt++;
}

9、疊加DDGI間接光結果,使用流程如下:

// 最終著色shader。

// 通過上取樣計算螢幕空間座標對應的DDGI值。
vec3 Bilateral(ivec2 uv, vec3 normal)
{
    ...
}

void main()
{
    ...
    vec3 result = vec3(0.0);
    result += DirectLighting();
    result += IndirectLighting();
    vec3 DDGIIrradiances = vec3(0.0);
    ivec2 texUV = ivec2(gl_FragCoord.xy);
    texUV.y = shadingPara.ddgiTexHeight - texUV.y;
    if (shadingPara.ddgiDownSizeScale == 1) { // 未降低解析度。
        DDGIIrradiances = texelFetch(irradianceTex, texUV, 0).xyz;
    } else { // 降低解析度。
        ivec2 inDirectUV = ivec2(vec2(texUV) / vec2(shadingPara.ddgiDownSizeScale));
        DDGIIrradiances = Bilateral(inDirectUV, N);
    }
    result += DDGILighting();
    ...
    Image = vec4(result_t, 1.0);
}

瞭解更多詳情>>

訪問華為開發者聯盟官網
獲取開發指導文件
華為移動服務開源倉庫地址:GitHubGitee

關注我們,第一時間瞭解 HMS Core 最新技術資訊~