写点什么

手把手教你:轻松打造沉浸感十足的动态漫反射全局光照

作者:HMS Core
  • 2022 年 9 月 19 日
    贵州
  • 本文字数:5737 字

    阅读完需:约 19 分钟

一个沉浸感十足的游戏,其场景中的全局光照效果一定功不可没。


动态漫反射全局光照(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.soADD_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 2void 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 最新技术资讯~

发布于: 刚刚阅读数: 3
用户头像

HMS Core

关注

HMS Core技术团队。 2022.06.16 加入

分享最新的技术干货,带来最全的能力应用场景,更新热门开发者圈子活动。与开发者一起,同成长,共精彩。

评论

发布
暂无评论
手把手教你:轻松打造沉浸感十足的动态漫反射全局光照_移动开发_HMS Core_InfoQ写作社区