Nimbus SDK:用于 C++ 实时体积云渲染、动画和变形的微型框架





5.00/5 (42投票s)
一个可重用的 Visual C++ 框架,用于实时体积云渲染、动画和变形
目录
2.1 光线追踪、光线投射和光线步进
2.2 体积渲染
2.3 水蒸气模拟
2.4 伪球面
3.2.1 主函数
3.2.2 渲染函数
3.2.3 模拟风
3.2.4 结果
3.4.1 线性插值
3.4.2 主函数
3.4.3 渲染函数
3.4.4 结果
4. 性能测试
5. 结论与局限性
6. 延伸阅读与参考文献
7. 扩展许可证
8. 下载与文档
9. 演示视频
1. 引言
本文是对我的博士论文《基于增强物理-数学抽象方法的高性能实时 GPGPU 体积云渲染算法》的评述。该论文于 2019 年 10 月在西班牙国家远程教育大学 (UNED) 答辩,并获得最高荣誉。本文旨在阐述 Nimbus SDK 在研究期间开发的主要功能。
对于缺乏数学/物理知识的新手开发者来说,实时体积云渲染是一项复杂的任务。对于没有先进 3D 硬件功能的传统计算机来说,这也是一个挑战。因此,现有的 Nimbus SDK 为低性能的英伟达显卡(如英伟达 GT1030 和英伟达 GTX 1050)提供了一个高效的基础框架。
该框架可应用于电脑游戏、虚拟现实、建筑设计的户外景观、飞行模拟器、环境科学应用、气象学等领域。
首先,我将解释当前最新的技术和计算机图形学背景,以便理解本文阐述的主要原理。最后,将提供 SDK 用法的完整描述并举例说明。
2. 理论背景
2.1 光线追踪、光线投射和光线步进
SDK 的核心基于光线追踪原理。基本上,光线追踪是指从位于帧缓冲区的摄像机视角发射直线(光线)到目标场景:通常是球体、立方体等,如图 1 所示。
|
图 1. 光线追踪布局。
|
直线的数学原理是其欧几里得方程。目标是确定该直线与先前提到的基本对象之间的碰撞。一旦确定了碰撞,我们就可以评估颜色和其他材质特性,以生成 2D 帧缓冲区上的像素颜色。
由于光线追踪是一种暴力技术,因此在过去几十年中已经开发了其他高效的解决方案。例如,在光线投射方法中,通过几何计算分析计算交点。这种方法通常与体素网格和空间划分算法等其他结构一起使用。该方法通常用于科学和医学可视化中的直接体积渲染,以获得磁共振成像 (MRI) 和计算机断层扫描 (CT) 中的一组 2D 切片图像。
在先进的实时计算机图形学中,一种广泛使用的光线追踪和光线投射的简化称为光线步进。该方法是光线投射的轻量级版本,其中沿着一条线采样
以离散方式检测与 3D 体积的碰撞。
2.2 体积渲染
许多视觉效果本质上是体积的,很难用几何图元进行建模,包括流体、云、火、烟、雾和尘埃。体积渲染对于需要可视化三维数据集的医学和工程应用至关重要。有两种体积渲染方法:基于纹理的技术和基于光线投射的技术。在基于纹理的体积渲染技术中,通过渲染体积内的 2D 几何图元集来执行采样和合成步骤,如图 2 所示。
|
图 2. 视对齐切片,带有三个采样平面。
|
每个图元都分配了用于采样体积纹理的纹理坐标。代理几何体按后到前或前到后的顺序栅格化并混合到帧缓冲区中。在片段着色阶段,插值纹理坐标用于数据纹理查找步骤。然后,插值数据值作为纹理坐标,用于对传递函数纹理进行依赖查找。光照技术可以在结果颜色发送到管道的合成阶段之前修改其颜色。[Iki+04]。
在具有光线投射的体积可视化中,可以渲染固体的实拍高质量图像,这使得可视化三维空间数据(如流体或医学成像)的采样函数成为可能。大多数光线投射方法基于 Blinn/Kajiya 模型,如图 3 所示。
沿着光线上的每个点都计算来自光源的光照 I(t)。让 P 为一个相位函数,用于计算沿着光线的散射光,D(t) 为体积的局部密度。从距离 t 处沿着 R 散射的光照为
其中 Θ 是视点和光源之间的角度。
在需要内部阴影的应用中,包含从点 (x,y,z) 到光源的线积分可能很有用。
|
图 3. 沿 3D 体积标量函数的光线投射。
|
由于密度函数沿光线的衰减可以计算为公式 2。
最后,到达眼睛的光强度沿方向 R 由于沿着光线的所有元素定义在公式 3 中
过程,通常会包含以下一种或多种优化过程
- 包围盒
- 分层空间枚举 (Octrees, Quadtrees, KD-Trees)
2.3 水蒸气模拟
为了模拟水蒸气滴,通过光线步进对 3D 噪声纹理进行采样,如图 4 所示。我们通常可以使用 Perlin 噪声或均匀随机噪声来生成 fBm(分形布朗运动)。该 SDK 的实现大量使用 fBm 噪声作为加权均匀噪声的总和。因此,设 w 为倍频程尺度因子,s 为噪声采样因子,i 为倍频程索引,fBm 方程定义为
其中 w = 1/2 且 s = 2。
|
图 4. 彩色比例尺图中的均匀噪声显示了光线追踪超纹理云中水滴的密度不规则。
|
2.4 伪球面
Nimbus 框架提供的一个新颖特性是云的不规则逼真形状,这得益于使用伪球面。基本上,伪球面是一个半径由噪声函数调制的球体,如图 5 所示。
|
图 5. 半径的噪声调制。
|
在光线步进期间,球体的半径遵循方程 5
(Eq. 5)
Nimbus 框架
3.1 类图
以下超链接显示了 SDK 的类图以及十九个相关类和两个接口的集合。
在接下来的章节中,我将逐步解释这些类的用法。
3.2 如何创建高斯积云
3.2.1 主函数
创建高斯云的类遵循公式 6 的密度函数。
(Eq.6)
它通常会生成一个类似于下图的 3D 图。
|
图 6. 3D 高斯图。
|
创建场景的代码如下所示。
// Entry point for cumulus rendering
#ifdef CUMULUS
void main(int argc, char* argv[])
{
// Initialize FreeGLUT and Glew
initGL(argc, argv);
initGLEW();
try {
// Cameras setup
cameraFrame.setProjectionRH(30.0f, SCR_W / SCR_H, 0.1f, 2000.0f);
cameraAxis.setProjectionRH(30.0f, SCR_W / SCR_H, 0.1f, 2000.0f);
cameraFrame.setViewport(0, 0, SCR_W, SCR_H);
cameraFrame.setLookAt(glm::vec3(0, 0, -SCR_Z), glm::vec3(0, 0, SCR_Z));
cameraFrame.translate(glm::vec3(-SCR_W / 2.0, -SCR_H / 2.0, -SCR_Z));
userCameraPos = glm::vec3(0.0, 0.4, 0.0);
// Create fragment shader canvas
canvas.create(SCR_W, SCR_H);
// Create cloud base texture
nimbus::Cloud::createTexture(TEXTSIZ);
#ifdef MOUNT
mountain.create(800.0, false); // Create mountain
#endif
axis.create(); // Create 3D axis
// Create cumulus clouds
myCloud.create(35, 2.8f, glm::vec3(0.0, 5.0, 0.0), 0.0f, 3.0f,
0.0f, 1.9f, 0.0f, 3.0f, true, false);
// Calculate guide points for cumulus
myCloud.setGuidePoint(nimbus::Winds::EAST);
// Load shaders
// Main shader
shaderCloud.loadShader(GL_VERTEX_SHADER, "../Nube/x64/data/shaders/canvasCloud.vert");
#ifdef MOUNT
// Mountains shader for cumulus
shaderCloud.loadShader(GL_FRAGMENT_SHADER,
"../Nube/x64/data/shaders/clouds_CUMULUS_MOUNT.frag");
#endif
#ifdef SEA
// Sea shader for cumulus
shaderCloud.loadShader(GL_FRAGMENT_SHADER,
"../Nube/x64/data/shaders/clouds_CUMULUS_SEA.frag");
#endif
// Axis shaders
shaderAxis.loadShader(GL_VERTEX_SHADER, "../Nube/x64/data/shaders/axis.vert");
shaderAxis.loadShader(GL_FRAGMENT_SHADER, "../Nube/x64/data/shaders/axis.frag");
// Create shader programs
shaderCloud.createShaderProgram();
shaderAxis.createShaderProgram();
#ifdef MOUNT
mountain.getUniforms(shaderCloud);
#endif
canvas.getUniforms(shaderCloud);
nimbus::Cloud::getUniforms(shaderCloud);
nimbus::Cumulus::getUniforms(shaderCloud);
axis.getUniforms(shaderAxis);
// Start main loop
glutMainLoop();
}
catch (nimbus::NimbusException& exception)
{
exception.printError();
system("pause");
}
// Free texture
nimbus::Cloud::freeTexture();
}
#endif
基本上,上面的代码初始化了 OpenGL 和 Glew 库,并定位了帧缓冲区和坐标轴相机。之后,我们定义了云纹理大小,创建了坐标轴、可选的山脉,最后使用 Cumulus::create()
创建了一个高斯积云。然后,我们使用 Cumulus::setGuidePoint(windDirection)
函数定义了风向的引导点。在渲染循环开始之前,我们使用 Shader::loadShader(shader)
、Shader::createShaderProgram()
和 ::getUniforms()
方法加载并将统一变量分配到 GLSL 着色器中。最后,我们启动渲染循环。不要忘记在关闭应用程序之前调用 nimbus::Cloud::freeTexture()
来释放纹理。
3.2.2 渲染函数
我们刚刚初始化了 OpenGL (FreeGLUT) 和 Glew,并准备了一个积云。然后我们告诉 FreeGLUT 绘制响应风平流和阴影预计算的场景或显示函数。该函数实现如下。
#ifdef CUMULUS
// Render function
void displayGL()
{
if (onPlay)
{
// Clear back-buffer
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
//////////////// CLOUDS ///////////////////
// Change coordinate system
glm::vec2 mouseScale = mousePos / glm::vec2(SCR_W, SCR_H);
// Rotate camera
glm::vec3 userCameraRotatePos = glm::vec3(sin(mouseScale.x*3.0),
mouseScale.y, cos(mouseScale.x*3.0));
glDisable(GL_DEPTH_TEST);
shaderCloud.useProgram();
if (firstPass) // If first iteration
{
// Render cloud base texture
nimbus::Cloud::renderTexture();
#ifdef MOUNT
// Render mountain
mountain.render();
#endif
nimbus::Cloud::renderFirstTime(SCR_W, SCR_H);
}
// Render cloud base class uniforms
nimbus::Cloud::render(mousePos, timeDelta, cloudDepth,
skyTurn, cloudDepth * userCameraRotatePos, debug);
// Calculate and apply wind
(parallel) ? myCloud.computeWind(&windGridCUDA) :
myCloud.computeWind(&windGridCPU);
// Wind setup
nimbus::Cumulus::setWind(windDirection);
// Render cumulus class
nimbus::Cumulus::render(shaderCloud);
if (precomputeTimeOut >= PRECOMPTIMEOUT) // Check for regular
// precompute light (shading)
{
clock_t start = clock();
if (parallel)
{
if (skyTurn == 0) // If morning sun is near else sun is far (sunset)
nimbus::Cumulus::precomputeLight(precompCUDA, sunDir, 100.0f, 0.2f);
else nimbus::Cumulus::precomputeLight
(precompCUDA, sunDir, 10000.0f, 1e-6f);
cudaDeviceSynchronize();
}
else
{
if (skyTurn == 0)
nimbus::Cumulus::precomputeLight(precompCPU, sunDir, 100.0f, 0.2f);
else nimbus::Cumulus::precomputeLight
(precompCPU, sunDir, 10000.0f, 1e-6f);
}
clock_t end = clock();
nimbus::Cloud::renderVoxelTexture(0);
precomputeTimeOut = 0;
float msElapsed = static_cast<float>(end - start);
std::cout << "PRECOMPUTING LIGHT TIME ="
<< msElapsed / CLOCKS_PER_SEC << std::endl;
}
if (totalTime > TOTALTIME)
{
timeDelta += nimbus::Cumulus::getTimeDir();
totalTime = 0;
}
totalTime++;
precomputeTimeOut++;
// User camera setup
cameraSky.setLookAt(cloudDepth * userCameraRotatePos, userCameraPos);
cameraSky.translate(userCameraPos);
canvas.render(cameraFrame, cameraSky);
/////////////// AXIS ////////////////////
shaderAxis.useProgram();
// Render axis
cameraAxis.setViewport(SCR_W / 3, SCR_H / 3, SCR_W, SCR_H);
cameraAxis.setLookAt(10.0f * userCameraRotatePos, userCameraPos);
cameraAxis.setPosition(userCameraPos);
axis.render(cameraAxis);
// Restore landscape viewport
cameraFrame.setViewport(0, 0, SCR_W, SCR_H);
glutSwapBuffers();
glEnable(GL_DEPTH_TEST);
// Calculate FPS
calculateFPS();
firstPass = false; // First pass ended
}
}
#endif
该函数的第一步是更新帧缓冲区相机并计算响应鼠标移动的主场景相机。此相机通过鼠标用作平移相机,但我们可以使用键盘箭头键进行平移。我们必须通过调用主 Shader::useProgram()
来启动渲染函数。在第一次迭代中,必须按以下顺序调用以下方法:nimbus::Cloud::renderTexture()
、可选的 Mountain::render()
,最后是 nimbus::Cloud::renderFirstTime(SCR_W, SCR_H)
。在渲染循环的正常运行期间,我们必须调用 nimbus::Cloud::render()
来将值传递给着色器统一变量。现在,是时候使用以下调用通过流体引擎计算风了:nimbus::Cumulus::setWind(windDirection)
和 Cumulus::computeWind()
,具体取决于设备选择。下一步是使用 nimbus::Cumulus::precomputeLight()
定期预计算阴影,具体取决于是否选择了 CPU 或 GPU,使用特定的预计算器对象。最后一步是通过调用 nimbus::Cloud::renderVoxelTexture(cloudIndex)
来渲染其计算的纹理,以渲染阴影云纹理。
3.2.3 模拟风
NimbuSDK 流体引擎基于内部的 U、V、W 风力 3D 网格,该网格作用于每个云的自动选择的引导点,如图 7 所示。
|
图 7. 3D 流体网格内的引导点示例。
|
我们将使用 OpenGL (FreeGLUT) 空闲函数来重新计算风,如下面的代码所示。
// Idle function
void idleGL()
{
if (!onPlay) return;
syncFPS();
#ifdef CUMULUS
simcont++;
if (simcont > FLUIDLIMIT) // Simulate fluid
{
applyWind();
clock_t start = clock();
if (parallel)
{
windGridCUDA.sendData();
windGridCUDA.sim();
windGridCUDA.receiveData();
cudaDeviceSynchronize(); // For clock_t usage
} else windGridCPU.sim();
clock_t end = clock();
float msElapsed = static_cast<float>(end - start);
std::cout << "FLUID SIMULATION TIME = " << msElapsed / CLOCKS_PER_SEC << std::endl;
simcont = 0;
}
#endif
glutPostRedisplay();
}
基本上,这个空闲函数调用 FluidCUDA::sendData()
和 FluidCUDA::sim()
来将 3D 网格数据发送到设备,然后再执行 CUDA 模拟。在 CUDA 处理之后,必须调用 FluidCUDA::receiveData()
来检索最后处理的流体数据。对于 CPU 情况,不需要将数据发送到设备,我们必须直接调用 FluidCPU::sim()
。
#ifdef CUMULUS
void applyWind() // Apply wind force
{
if (parallel)
{
windGridCUDA.clearUVW(); // Clear fluid internal data
myCloud.applyWind(windForce, &windGridCUDA);
} else
{
windGridCPU.clearUVW(); // Clear fluid internal data
myCloud.applyWind(windForce, &windGridCPU);
}
}
#endif
上面的代码必须由 OpenGL 空闲函数调用才能将风应用于云。
3.2.4 结果
前面解释的代码将产生以下图像。
|
图 8. 实时云渲染。
|
3.3 包围盒
为了避免光线步进期间的过度追踪,该框架将不同的云包裹在称为包围盒的 march cubes 中。使用此技术,可以通过追踪摄像机视锥体下的云并排除摄像机视图后方和外部的所有云来渲染大量云。此方法如图 9 所示。
|
图 9. 两个矩形包围盒,其中包含正在处理光线的云。
|
3.4 创建变形效果
Nimbus SDK 中最有趣的功能之一是与云变形效果相关的 C++ 类。已实现的算法要求在动画循环前对 3D 网格进行 90% 的抽取,可以使用 Blender 或任何其他 3D 商业编辑器。以下部分将解释技术背景,以理解算法基础。
3.4.1 线性插值
两个 3D 线框网格的转换是通过移动每个伪椭球的质心(即顶点)从源形状到目标形状,通过线性插值完成的,如下面的 GLSL 方程所述。
(Eq.7)
其中可能出现两种情况:
A) 源中的质心 > 目标网格中的质心:在这种情况下,我们将源和目标中的质心按迭代顺序直接对应。源中的多余质心通过将它们与它们质心数量的模数计算重叠来随机分布到目标质心上,如图 10 所示。
|
图 10. 情况 A. 六边形中的多余质心被随机分布并重叠在三角形质心上。
|
B) 目标中的质心 > 源网格中的质心:相反的操作意味着对多余的源质心进行随机重新选择以复制它们,并生成新的插值运动到目标网格,如图 11 所示。
|
图 11. 情况 B. 将所需质心添加到三角形中,以随机重新选择到六边形质心。
|
3.4.2 主函数
// Entry point for mesh morphing
#ifdef MODEL
void main(int argc, char* argv[])
{
// Initialize FreeGLUT and Glew
initGL(argc, argv);
initGLEW();
try {
// Cameras setup
cameraFrame.setProjectionRH(30.0f, SCR_W / SCR_H, 0.1f, 2000.0f);
cameraAxis.setProjectionRH(30.0f, SCR_W / SCR_H, 0.1f, 2000.0f);
cameraFrame.setViewport(0, 0, SCR_W, SCR_H);
cameraFrame.setLookAt(glm::vec3(0, 0, -SCR_Z), glm::vec3(0, 0, SCR_Z));
cameraFrame.translate(glm::vec3(-SCR_W / 2.0, -SCR_H / 2.0, -SCR_Z));
userCameraPos = glm::vec3(0.0, 0.4, 0.0);
// Create fragment shader canvas
canvas.create(SCR_W, SCR_H);
// Create cloud base texture
nimbus::Cloud::createTexture(TEXTSIZ);
#ifdef MOUNT
mountain.create(300.0, false); // Create mountain
#endif
axis.create(); // Create 3D axis
model1.create(glm::vec3(-1.0, 7.0, 0.0), MESH1, 1.1f); // Create mesh 1
model2.create(glm::vec3(1.0, 7.0, -3.0), MESH2, 1.1f); // Create mesh 2
morphing.setModels(&model1, &model2, EVOLUTE); // Setup modes for morphing
// Load shaders
// Main shader
shaderCloud.loadShader
(GL_VERTEX_SHADER, "../Nube/x64/data/shaders/canvasCloud.vert");
#ifdef MOUNT
// Mountains shader for 3D meshes based clouds
shaderCloud.loadShader(GL_FRAGMENT_SHADER,
"../Nube/x64/data/shaders/clouds_MORPH_MOUNT.frag");
#endif
#ifdef SEA
// Sea shader for 3D meshes based clouds
shaderCloud.loadShader(GL_FRAGMENT_SHADER,
"../Nube/x64/data/shaders/clouds_MORPH_SEA.frag");
#endif
// Axis shaders
shaderAxis.loadShader(GL_VERTEX_SHADER, "../Nube/x64/data/shaders/axis.vert");
shaderAxis.loadShader(GL_FRAGMENT_SHADER, "../Nube/x64/data/shaders/axis.frag");
// Create shader programs
shaderCloud.createShaderProgram();
shaderAxis.createShaderProgram();
// Locate uniforms
nimbus::Cloud::getUniforms(shaderCloud);
#ifdef MOUNT
mountain.getUniforms(shaderCloud);
#endif
canvas.getUniforms(shaderCloud);
nimbus::Model::getUniforms(shaderCloud);
axis.getUniforms(shaderAxis);
// Start main loop
glutMainLoop();
}
catch (nimbus::NimbusException& exception)
{
exception.printError();
system("pause");
}
// Free texture
nimbus::Cloud::freeTexture();
}
#endif
初始化函数与积云部分所述相同。还必须使用 Canvas::create()
创建帧缓冲区画布,并调用 nimbus::Cloud::createTexture()
创建云纹理。完成此操作后,我们通过调用 Model::create()
并将 3D 位置、OBJ 文件路径和比例作为参数传递来创建源网格和目标网格模型。最后,我们通过调用 Morphing::setModels()
来指定是首选演化(向前)还是退化(向后)。其余代码加载 GLSL 着色器并定位统一变量,方法与之前相同。
3.4.3 渲染函数
#ifdef MODEL
void displayGL()
{
if (onPlay)
{
// Clear back-buffer
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Change coordinate system
glm::vec2 mouseScale = mousePos / glm::vec2(SCR_W, SCR_H);
// Rotate camera
glm::vec3 userCameraRotatePos = glm::vec3(sin(mouseScale.x*3.0),
mouseScale.y, cos(mouseScale.x*3.0));
//////////////// MORPHING ///////////////////
glDisable(GL_DEPTH_TEST);
shaderCloud.useProgram();
if (firstPass) // If first iteration
{
nimbus::Cloud::renderFirstTime(SCR_W, SCR_H);
clock_t start = clock();
#ifdef CUDA
// Precompute light for meshes
nimbus::Model::precomputeLight(precompCUDA, sunDir,
100.0f, 1e-6f, model1.getNumEllipsoids(), model2.getNumEllipsoids());
#else
nimbus::Model::precomputeLight(precompCPU, sunDir, 100.0f, 1e-6f,
model1.getNumEllipsoids(), model2.getNumEllipsoids());
#endif
clock_t end = clock();
std::cout << "PRECOMPUTE TIME LIGHT = " << end - start << std::endl;
// Prepare for morphing
(EVOLUTE) ? morphing.prepareMorphEvolute() : morphing.prepareMorphInvolute();
alpha = alphaDir = 0.01f;
// First morphing render
nimbus::Model::renderFirstTime(model2.getNumEllipsoids(), EVOLUTE);
// Render cloud base texture
nimbus::Cloud::renderTexture();
#ifdef MOUNT
// Render mountain
mountain.render();
#endif
// Render clouds precomputed light textures
for (int i = 0; i < nimbus::Cloud::getNumClouds(); i++)
nimbus::Cloud::renderVoxelTexture(i);
}
// Render cloud base class uniforms
nimbus::Cloud::render(mousePos, timeDelta, cloudDepth, skyTurn,
cloudDepth * userCameraRotatePos, debug);
static bool totalTimePass = false;
if (totalTime > TOTALTIME) // Check time for morphing animation
{
totalTimePass = true;
timeDelta += timeDir;
if (alpha < 1.0 && alpha > 0.0)
{
alpha += alphaDir; // Animate morphing
(EVOLUTE) ? morphing.morphEvolute(alpha) : morphing.morphInvolute(alpha);
morphing.morph(0.1f); // Animation speed
}
totalTime = 0;
}
totalTime++;
// Mesh renderer
nimbus::Model::render(shaderCloud, (totalTimePass) ?
morphing.getCloudPosRDst() : nimbus::Model::getCloudPosR(),
morphing.getCloudPosDst(), alpha);
// User camera setup
cameraSky.setLookAt(cloudDepth * userCameraRotatePos, userCameraPos);
cameraSky.translate(userCameraPos);
canvas.render(cameraFrame, cameraSky);
/////////////// AXIS ////////////////////
shaderAxis.useProgram();
// Render axis
cameraAxis.setViewport(SCR_W / 3, SCR_H / 3, SCR_W, SCR_H);
cameraAxis.setLookAt(10.0f * userCameraRotatePos, userCameraPos);
cameraAxis.setPosition(userCameraPos);
axis.render(cameraAxis);
// Restore landscape viewport
cameraFrame.setViewport(0, 0, SCR_W, SCR_H);
glutSwapBuffers();
glEnable(GL_DEPTH_TEST);
// Calculate FPS
calculateFPS();
firstPass = false; // First pass ended
}
}
#endif
OpenGL 渲染函数的主要部分与积云部分所见类似,因此我将不再赘述。主要区别在于 nimbus::Model::precomputeLight()
函数,该函数仅预计算一次光照。然后,我们必须根据先前选择的选项调用 Morph::prepareMorphEvolute()
或 Morph::prepareMorphInvolute()
。在初始化线性插值变量计数器:alpha
用于递增,alphaDir
用于正或负递增之后,我们调用 nimbus::Model::renderFirstTime()
并传入目标椭球的数量和首选的进度选项。在第一个传递迭代结束之前,需要调用 nimbus::Cloud::renderVoxelTexture(meshIndex)
将阴影体素纹理传递给着色器。然后,在正常的动画循环中,我们将通过调用 Morph::morphEvolute(alpha)
或 Morph::morphInvolute(alpha)
(取决于所选选项)来为变形效果提供数据。如上代码所示,递增动画计数器非常简单,通常取决于我们需要的速度。
3.4.4 结果
图 12 至 18 说明了手变成兔子的变形过程。
| |
图 12. 第一步。
| 图 13. 第二步。
|
| |
图 13. 第三步。
| 图 14. 第四步。
|
| |
图 15. 第五步。
| 图 16. 第六步。
|
| |
图 17. 第七步。
| 图 18. 第八步。
|
4. 性能测试
本节介绍了在英伟达 GTX 1050 非 Ti(640 核/Pascal)和 GTX 1070 非 Ti(1920 核/Pascal)显卡以及 64 位 i-Core 7 CPU 860@2.80 GHz(第一代,2009 年)配 6 GB RAM 的环境下,使用 GPU/CPU 进行的性能测试。积云动力学测试是在具有真实天空函数的移动海景上进行的,场景中有 4 片云,每片云包含 35 个球体(共 140 个球体)。为每块显卡定义了测试版本:一个 103 的预计算光照网格大小,以及一个 100 x 20 x 40 的流体体积;以及一个 403 的预计算光照网格大小,以及一个 100 x 40 x 40 的流体体积。
|
图 19. 77.7% 的样本高于 30 FPS。
|
|
图 20. 100% 的样本高于 30 FPS。
|
|
图 21. 英伟达 GTX 1050 非 Ti 上光照预计算和流体模拟的 CUDA 加速。
|
|
图 22. 英伟达 GTX 1070 非 Ti 上光照预计算和流体模拟的 CUDA 加速。
|
5. 结论与局限性
我们可以说,这些算法是实时云渲染的极佳选择,具有增强的性能,这得益于对大气物理-数学复杂性的抽象,并利用 GPU/CPU 多核并行技术来加速计算。Nimbus SDK v1.0 处于 Beta 阶段,因此在您的软件运行过程中可能会出现一些错误。该框架的主要功能尚不完整,并且容易改进,尤其是那些可以适应更好的面向对象编程和设计模式的方面。
6. 延伸阅读与参考文献
本文内容摘自我的博士论文,可在以下网址找到:
https://www.educacion.gob.es/teseo/mostrarRef.do?ref=1821765
以及以下期刊文章:
- Jiménez de Parga, C.; Gómez Palomo, S.R. Efficient Algorithms for Real-Time GPU Volumetric Cloud Rendering with Enhanced Geometry. Symmetry 2018, 10, 125. https://doi.org/10.3390/sym10040125 (*)
- Parallel Algorithms for Real-Time GPGPU Volumetric Cloud Dynamics and Morphing. Carlos Jiménez de Parga and Sebastián Rubén Gómez Palomo. Journal of Applied Computer Science & Mathematics. Issue 1/2019, Pages 25-30, ISSN: 2066-4273. https://doi.org/10.4316/JACSM.201901004
(*) 如果您的研究发现本工作有用,请不要忘记在您的期刊论文中引用前面的参考文献。
本文使用的其他参考文献
- [Iki+04] M. Ikits et al. GPU Gems. Addison-Wesley, 2004。
- [Paw97] J. Pawasauskas. “Volume Visualization With Ray Casting”. In: CS563 - Advanced Topics
in Computer Graphics Proceedings. 1997 年 2 月。
7. 扩展许可证
- 非物理基于大气散射的 CC BY-NC-SA 3.0,作者 robobo1221。
- Elevated 是 CC BY-NC-SA,作者 Iñigo Quilez。
- Seascape 是 CC BY-NC-SA,作者 Alexander Alekseev aka TDM。
8. 下载与文档
API 文档和 Visual C++ SDK 可在以下网址阅读和下载: http://www.isometrica.net/thesis
和
https://github.com/maddanio/Nube