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

語言: 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密度和數量,儘量不要超過10 10 10。以上建議以最終的呈現結果為主。

開發步驟

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 最新技術資訊~