开始使用Triton
Triton包括超过60,000行的代码,但只需要几行就可以将其集成到你的应用程序中。
本文为如何开始使用 Triton 中文教程第二篇,点击此处返回第一篇>>
渲染每一帧
如果你的相机包含了你的场景中含有水的区域,你就需要把你的海洋对象作为场景的一部分来渲染。这样做包括三个步骤:
更新Triton的照明条件
调用Triton::Environment::SetDirectionalLight()来指定阳光(或月光)的颜色和方向。这个信息将被用来在水中创建太阳或月亮的镜面反射。注意这是对太阳或月亮的方向,而不是来自太阳或月亮的方向--错误的方向将导致水的无效颜色。
Triton::Environment::SetAmbientLight()提供了用于照亮海泡石和水本身的环境天光,如果没有提供环境地图的话(参见将环境立方体地图与Triton集成)。
你可以将Triton与一个从动态时间效果中提供场景照明的系统结合起来使用,比如Sundog软件公司的SilverLining库(见https://www.dhorde.com/product/188.html),或者,这些照明值可以从你所使用的天空盒纹理中导出。
比如说:
// Position the sun 45 degrees up in the sky at full brightness:
Triton::Vector3 lightPosition(0, 1.0 / sqrt(2.0), 1.0 / sqrt(2.0));
environment->SetDirectionalLight(lightPosition, Triton::Vector3(1.0, 1.0, 1.0));
// Ambient color of the sky:
environment->SetAmbientLight(Triton::Vector3(0.6, 0.9, 0.9);
更新Triton的相机矩阵
由于Triton不依赖于固定的函数管道,你需要明确地告诉它你的相机矩阵是什么,以便我们可以将海洋与你的场景的其他部分一致地渲染。只要使用Triton::Environment::SetCameraMatrix()和Triton::Environment::SetProjectionMatrix()传递你的场景的模型视图和投影矩阵。这两个方法都接收了一个16个双数的数组。比如说:
double projection[16];
double modelview[16];
// How you retrieve your camera matrices will vary depending on the engine
// you're using, so we'll just postulate the existence of these methods:
GetProjectionMatrix(projection);
GetModelviewMatrix(modelview);
environment->SetCameraMatrix(modelview);
environment->SetProjectionMatrix(projection);
引擎有时会在它们是否暴露行为主或列为主的矩阵上有所不同。如果你的海洋不能正常渲染,可以尝试在传入矩阵之前将其转置。如果其他方法都失败了,可以尝试使用摄像机的位置、视场、剪辑平面和长宽比从头开始构建这些矩阵。请参考Triton附带的示例代码。
在C#中传递你的矩阵是有点困难的。下面是一个在XNA中创建投影矩阵并将其传入Triton的例子;对摄像机矩阵使用同样的技术。
projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, GraphicsDevice.Viewport.AspectRatio, 10.0f, 100000.0f);
double[] m = new double[16];
m[0] = projection.M11; m[1] = projection.M12; m[2] = projection.M13; m[3] = projection.M14;
m[4] = projection.M21; m[5] = projection.M22; m[6] = projection.M23; m[7] = projection.M24;
m[8] = projection.M31; m[9] = projection.M32; m[10] = projection.M33; m[11] = projection.M34;
m[12] = projection.M41; m[13] = projection.M42; m[14] = projection.M43; m[15] = projection.M44;
SWIGTYPE_p_double matrix4 = TritonEnvironment.new_double_array(16);
for (int i = 0; i < 16; i++)
{
TritonEnvironment.double_array_setitem(matrix4, i, m[i]);
}
environment.SetProjectionMatrix(matrix4);
TritonEnvironment.delete_double_array(matrix4);
渲染海洋
有了灯光和摄像机的信息,我们现在可以绘制我们的海洋。只要调用Triton::Ocean::Draw()就可以了。比如说:
// Draw the ocean for the current time sample
if (ocean) {
DWORD millis = timeGetTime();
ocean->Draw((double)millis * 0.001);
}
注意,一个明确的时间样本被传递进来,所以你可以随心所欲地操纵时间的流逝。由于海洋可能会画出透明的喷雾粒子,你通常会想在帧的最后调用Triton::Ocean::Draw()来进行适当的排序。如果你需要分别绘制海洋表面和粒子,你会在Ocean::Draw()上找到参数,让你这样做。
Ocean::Draw()渲染的是一个无限的海洋;要绘制用户定义的水面几何图形补丁,请参见渲染用户定义的几何图形补丁。
渲染用户定义的几何体斑块
除了用Triton::Ocean::Draw()方法绘制无限的海洋之外,Triton还可以用来为你自己的水体几何图形着色。这使得你可以将Triton用于较小的水体。当与Triton::Ocean::SetDepth()一起使用时,你可以用Triton来渲染池塘、湖泊和任何大小和深度的水。
OpenGLPatchSample、DirectX9PatchSample和DirectX11PatchSample说明了这种技术。你将使用Triton::Ocean::SetPatchShader()方法来设置绘制Triton的水所需的状态和着色器,然后在你希望水出现的地方绘制一个平面网格,最后调用Triton::Ocean::UnsetPatchShader()来恢复之前的状态。
由于你将会自己绘制,你需要注意为你的水面补丁设置适当的剔除状态。还要确保你的深度测试被设置为 "小于或等于 "以获得最佳效果。
如果你在你的场景中使用了体积贴花(见在水面上贴花),你还需要对贴花进行第二次处理。要做到这一点,在decalPass = true的情况下第二次调用SetPatchShader()。如果视图中存在贴花,SetPatchShader()将返回true,在这种情况下,你必须第二次渲染水的几何形状。然后在第二次调用SetPatchShader()时,用同样是decalPass = true的UnsetPatchShader来平衡。
下面是一个在OpenGL中渲染用户定义的水的几何图形的例子,支持贴花:
// Explicitly update the ocean simulation once per frame
environment->SetSeaLevel(5);
ocean->UpdateSimulation(time);
// Bind our vertex and index arrays to draw our mesh
glBindBuffer(GL_ARRAY_BUFFER, vboID);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, idxID);
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(4, GL_FLOAT, 0, 0);
glDepthFunc(GL_LEQUAL);
// Have Triton set all the state required to render our mesh as water
// with floating-point vertices with a stride of 16 bytes.
// We optionally pass in a 4x4 model matrix to translate the patch
ocean->SetPatchShader(time, 16, 0, false, modelMatrix, false);
// Draw our mesh - Triton will make it look like water...
glEnable(GL_CULL_FACE);
glFrontFace(GL_CW);
glDrawElements(GL_TRIANGLE_STRIP, nIndices, GL_UNSIGNED_INT, 0);
// Restore the previous state.
ocean->UnsetPatchShader();
// Do another pass for decals, if needed. Note the "true" being passed on
// the decalPass parameters below.
if (ocean->SetPatchShader(time, 16, 0, false, modelMatrix, true)) {
glEnable(GL_CULL_FACE);
glFrontFace(GL_CW);
glDrawElements(GL_TRIANGLE_STRIP, nIndices, GL_UNSIGNED_INT, 0);
}
ocean->UnsetPatchShader(0.0, 0, true);
每一帧的额外水斑可以用同样的方式绘制,但要确保Ocean::UpdateSimulation()每帧只被调用一次,以保持性能。如果你愿意,你可以从另一个线程调用Ocean::UpdateSimulation()。此外,注意每一次对Ocean::SetPatchShader()的调用都要与对Ocean::UnsetPatchShader()的调用相平衡。
如果你要在一个场景中绘制许多补丁,那么在绘制所有补丁之前调用Ocean::SetPatchShader()一次是最有效的,然后再调用Ocean::SetPatchMatrix()来定位每个单独的网格。这可以让你避免在每个单独的网格上调用SetPatchShader()的开销,而你所需要做的只是改变每个网格的位置。
更多的例子请参考为DirectX提供的样本代码。在地心坐标系和平地坐标系中都支持绘制用户定义的几何图形,但只适用于泰森多夫波。
如果你需要向Ocean::SetPatchShader()传递一个平移矩阵,C#用户可以参考更新Triton的相机矩阵,以了解从C#向Triton传递一个4x4矩阵的例子。TritonClassLibrary中的TritonOcean类也包括在该例子中使用的TritonEnvironment操作双数组的辅助函数。
如果你在使用时遇到问题,请联系 https://www.dhorde.com/ 在线客服,电话 023-62585653,或邮箱 sales@dhorde.com。
用Triton进行多线程渲染
在Triton 4.0之前,为了支持多线程环境,用户必须在配置文件中指定以下标志,并像往常一样进行绘制:
thread-safe = yes
然而,这是一个非常幼稚的方法,因为它实际上有一个顶层的mutex,迫使Triton为多个视图做工作,并以序列化的方式进行渲染。
从Triton 4.0开始,Triton支持DirectX 11和OpenGL的全功能多线程渲染。但是,为了利用这一点,必须仔细设计应用程序,并且正确地以正确的顺序使用新的API。
这最好用一个例子来解释。假设你有一个具有左右视图的VR应用,每个摄像头都是一个略微不同的地壳,对应于左眼和右眼。
第一件事是让Triton为海洋生成波浪的几何形状和尾流效果。这就需要对Ocean::UpdateSimulation进行调用。这个调用需要在主线程中进行,其中DirectX 11设备/即时上下文或OpenGL上下文是当前的。
如果不同视图之间的风况不同,局部海浪的外观可能会改变。但是对于VR来说,各视图之间的距离很近,就我们的目的而言,可以认为是在同一个位置--所以你可以不用每帧只调用Ocean::UpdateSimulation一次。UpdateSimulation()需要一个Camera参数;你可以在这里传入左或右视图的Camera对象,或者在两个视点之间创建一个假Camera。
在这之后,你需要分别为左边和右边的相机做Ocean::UpdateSimulation()和Ocean::DrawConcurrent()。这些调用可以在每个摄像机的完全独立的线程中发生。而且,你可以同时渲染多少个视图是没有限制的(这个例子只有两个。)
在这之后,你需要在主线程中调用Ocean::PostDrawConcurrent()。
因此,调用的顺序是:
Ocean::UpdateSimulation(time, main camera/viewing frustum) // in main thread
Ocean::UpdateSimulation(time, left camera)
Ocean::DrawConcurrent(left camera) // can happen in a completely different thread
Ocean::UpdateSimulation(time, right camera)
Ocean::DrawConcurrent(right camera) // can happen in a completely different thread
Ocean::PostDrawConcurrent() // in main thread, after both view threads are done calling DrawConcurrent()
如果你有完全不同的视图(例如,相隔数公里的独立窗口和视点),而且这些视点可能有不同的风况或不同的附近波浪,那么在你的视图之间共享一个海洋几何体的策略就不再有效了。你仍然可以从不同的线程中调用Ocean::DrawConcurrent(),使用唯一的Camera对象来模拟和渲染每个视图的海洋,但在这种情况下,你要为每个视图使用单独的海洋实例。
除了相机,我们还需要传递一个额外的 "上下文 "参数。根据我们使用的是OpenGL还是DirectX 11渲染器,这意味着不同的事情,接下来我们将对此进行描述。
DirectX 11的多线程渲染:
任何渲染的一个关键观察点是,为GPU生成要执行的命令,然后在GPU上实际执行这些命令的实际工作可以有效地解耦。
因此,每个不同视图的命令列表生成可以在完全不同的线程中进行。在每个视图的所有命令列表生成后,它们可以在主线程的GPU上快速执行。
为此,DirectX 11利用了 "延迟上下文 "和 "命令列表":
https://msdn.microsoft.com/en-us/library/windows/desktop/ff476892(v=vs.85).aspx
在DirectX 11中,你要为每个相关的视图/线程创建一个 "递延上下文/命令列表 "对,并将它们与相关的视图/线程的摄像机一起传递给Ocean::DrawConcurrent的调用。当Ocean::DrawConcurrent被指定使用有效的DirectX 11延迟上下文和摄像机时,Triton将生成正确的命令集,并将其附加到传入的上下文/命令列表中,但不会实际进行渲染。在这之后,你将使用适当的DirectX 11 API实际 "执行 "命令列表。
我们有一个功能齐全的例子DirectX11MultiThreadedSample,演示了DirectX11的多线程渲染。
OpenGL多线程渲染:
OpenGL渲染遵循与DirectX 11相同的范式。不幸的是,在这个时候,OpenGL规范并不包括命令列表。然而,供应商的特定API/扩展是可用的,其中最完整的是Nvidia的:
https://www.khronos.org/registry/OpenGL/extensions/NV/NV_command_list.txt
然而,我们不想将自己与特定的供应商/扩展捆绑在一起,因此我们按照DirectX 11上下文/命令列表API的思路创建了自己的抽象API。然而,在OpenGL中,"上下文 "的意思完全不同(指窗口/离屏缓冲区的实际上下文),所以我们把每个视图/线程的命令列表/上下文称为 "OpenGL流"。
与DirectX 11类似,我们用以下方法为每个视图/线程创建一个OpenGL Stream:Environment::CreateOpenGLStream。一旦流被创建(并且在任何绘图之前),你必须调用Ocean::Initialize,将流和有关的摄像机作为参数,表明你打算用这个流和摄像机来渲染。这个调用必须发生在当前有OpenGL上下文的地方。在这之后,流指针和摄像机将作为参数传递给Ocean::DrawConcurrent。这些调用可以在完全不同的线程中进行(在每个线程中甚至不需要实际的OpenGL上下文,因为Ocean::DrawConcurrent调用实际上只是将命令追加到OpenGL流中)。在这之后,我们使用Environment::ExecuteOpenGLStream来执行每一个数据流。同样,执行必须发生在有一个当前OpenGL上下文的地方。
我们有一个功能齐全的样本OpenGLMultiThreadedSample,演示了OpenGL的多线程渲染。你可以使用各种绘制策略。请参考SampleDeclares.h获取更多信息。包括Visual Studio 2015/2017/2019/2022的项目文件;对于Linux,请参考README_LINUX文件以获得构建指导。
需要注意的一件事是,你不想在多个线程中实际渲染/执行流,每个线程都有自己的OpenGL上下文,因为在内部,GPU无论如何都会将调用序列化,而且没有性能改进。如果有的话,也会因为OpenGL上下文切换等原因而出现性能下降。尽管如此,有些渲染引擎(比如OpenSceneGraph)确实支持多上下文的线程渲染,为了完整起见,我们提供了一个代码路径/绘制策略,演示了如何在实际存在多个OpenGL上下文时进行多线程渲染。请注意,我们目前只支持共享的OpenGL上下文,也就是说,这些上下文是共享资源的(纹理、顶点缓冲区等),这也是你想做的最好的方式。
最后,也是很重要的一点,这个标志:
thread-safe = yes
在使用Ocean::DrawConcurrent时,实际上是被忽略的。Triton内部是完全线程安全的,只要你创建/传递正确的DirectX 11递延上下文/OpenGL流,以及在DirectX 11和OpenGL样本中展示的有关线程的相机。你也想在你的应用代码中删除任何额外的互斥,并相应地调整对Triton的调用。
将环境立方体地图与Triton结合起来
默认情况下,Triton将只是在水中反射你用Triton::Environment::SetAmbientLight()传递的任何颜色。为了获得更真实的反射,你需要使用Triton::Environment::SetEnvironmentMap()设置一个环境立方体地图。这样做是可选的,但它会带来很大的不同。
传入这个方法的纹理参数的类型将取决于你使用的渲染器。OpenGL用户应该传入一个代表立方体贴图的纹理ID的GLuint。DirectX9用户应该传入一个LPDIRECT3DCUBETEXTURE9。 (C#用户可以从他们的纹理对象的pComPtr成员中获得这个参数。) DirectX11用户应该传入一个ID3D11ShaderResourceView的指针。
如果你发现反射似乎来自立方体贴图的错误面,你可以使用传递到Environment::SetEnvironmentMap()中的Matrix3参数来纠正这个问题。由于DirectX立方体贴图的左手惯例,这在DirectX中特别常见。例如,如果你似乎从你的环境贴图的底部而不是从顶部得到反射,而你的 "向上 "方向是正Y轴,那么传入一个缩放矩阵,将Y缩放为-1。
从磁盘加载和构建正确的立方体地图的例子可以在SDK样本代码的SkyBox类中找到。你也可以根据天空的物理模拟动态地生成这些立方体地图,比如Sundog软件公司的SilverLining库。我们网站上的Triton的演示程序正是这样做的。
关机
在关机时,C++用户只需按照创建时的相反顺序删除Triton对象--首先是你的Triton::Ocean,然后是你的Triton::Environment,最后是你的Triton::ResourceLoader。我们将清理我们的内存和资源。比如说:
// Clean up our resources
void Destroy()
{
if (ocean) delete ocean;
if (environment) delete environment;
if (resourceLoader) delete resourceLoader;
}
如果你在使用时遇到问题,请联系 https://www.dhorde.com/ 在线客服,电话 023-62585653,或邮箱 sales@dhorde.com。
本文为如何开始使用 Triton 中文教程第二篇,点击此处返回第一篇>>
渝公网安备50010702505508