Triton
Visual simulation library for ocean rendering.
Advanced topics

Integrating with your own resource manager

It's possible to derive your own Triton::ResourceLoader class to hook into your own resource manager. Implement your own Triton::ResourceLoader::LoadResource() and Triton::ResourceLoader::FreeResource() methods to grab Triton's shader and graphical resources, located in the resources directory of the SDK, from any resource database you might have. Then, pass your derived Triton::ResourceLoader into Triton::Environment::Initialize() and it will be called instead of our default one.

The DLL's within our resources directory will require special care, however. You'll need to keep the vc9 and/or vc10 subdirectories of the resources directory in place on disk, and continue to pass a valid path to the resources directory to the Triton::ResourceLoader constructor so Triton will know where to find our FFT DLL's. Failure to do so will force Triton to fall back on a CPU-only FFT transform, which will hurt performance in a big way.

You'll also need to ensure the third-party DLL's located in the resources/dll directory are distributed in a place where Triton will be able to load them as part of the DLL search path. Our default implementation of Triton::ResourceLoader::SetResourceDirPath() calls the Windows function SetDllDirectory to add this directory to the DLL search path. You'll want to emulate this behavior, or redistribute these DLL's alongside your application's executable or working directory so Triton will find them when loaded.

Integrating with your own memory manager

If you'd like to hook into your own memory manager instead of using ours (which is just based on the Windows functions HeapAlloc() and HeapFree()), it's possible to do so.

Extend your own Triton::Allocator class from the one defined in MemAlloc.h. Then, pass it in via the static Triton::Allocator::SetAllocator() method prior to calling any other Triton methods or instantiating any Triton objects.

Since Triton operates across DLL boundaries, you'll want to take care that a consistent heap is used, as we do. Licensed users have access to Triton's full source, and you may find it informative to examine our own implementation of Triton::Allocator before creating your own.

We take care to capture every usage of new, delete, malloc, and free within Triton, as well as hooking into any memory allocated from STL objects. Memory may be allocated by system functions and third party libraries (such as CUDA, OpenCL, and FFTSS) that is outside of our control, however.

Using bindless graphics in OpenGL 4.5

Triton has the ability to take advantage of "bindless graphics" that use native GPU addresses, instead of handles for resources that must be looked up and validated by the driver. This offers some performance benefit, and also gets us a closer to an architecture that's ready for Vulkan.

This feature is disabled by default; the relevant settings in the Resources/Triton.config file are:

use-gl-bindless-textures
use-gl-bindless-uniform-buffer-objects
use-gl-bindless-vertex-buffers

If set to "yes", these settings enable bindless textures, UBO's, and VBO's respectively. Their usage requires use of the OPENGL_4_5 or newer renderer type with Triton (specified in Environment::Initialize()), and bindless UBO's and VBO's rely on NVidia-specific extensions that may not be present on the systems you are targeting. That's why they are disabled by default. If however you control the configuration of the systems you are deploying your software to and they are compatible with NVidia's bindless extensions, you can test enabling these options in your deployments. Bindless textures rely on the availability of the GL_ARB_bindless_texture extension; bindless UBO's rely on GL_ARB_uniform_buffer_object, GL_NV_vertex_buffer_unified_memory and GL_NV_shader_buffer_load; and bindless VBO's rely on GL_NV_vertex_buffer_unified_memory and GL_NV_shader_buffer_load.

Adding your own effects to the water

You'll find Triton's shaders in plain text inside the Resources folder; .glsl files are for OpenGL and .fx are effect files for DirectX9 and DirectX11. You're free to make changes to these shaders as you see fit to customize your application, or integrate more tightly with your graphics engine. For example, you could introduce support for additional light sources.

If you are developing changes to Triton's shaders, it's a good idea to enable the enable-debug-messages setting in resources/Triton.config so you'll see any compliation errors in the shaders. Any error messages will appear in your debugger's output log.

Your modifications will likely require additional parameters to be passed in to the shaders. To enable this, we offer the Triton::Ocean::GetShaderObject() method. Depending on the renderer being used, this will return to you a GLhandle, a ID3DXEffect pointer, or a ID3DX11Effect pointer, which you can use to pass in uniform parameters of your own - for example, your own textures or matrices used to transform them as they are combined with the water. GetShaderObject() takes a parameter indicated which shader you wish to modify. For the ocean surface, be sure to apply your uniforms to both the WATER_SURFACE and WATER_SURFACE_PATCH programs. These shader programs draw the water when using a projected grid and a conventional mesh, respectively.

If you'd like to keep your modified shaders separate from Triton's stock shaders, you can use the config setting "shader-filename-prefix" in resources/Triton.config to add a prefix to the shader file names when they are loaded. For example, setting "shader-filename-prefix = my-" would result in the file "my-flat-fft.fx" being used instead of "flat-ffx.fx.".

We also offer the "user-functions.glsl" file, which offers easy to use hooks into the lighting of all of Triton's geometry. Refer to the comments in this file to learn how to override Triton's shading. By placing your custom shading code into user-functions.glsl, you can avoid modifying our shaders directly, which makes updating Triton easier. This also has the benefit of placing your custom shading code in one place, instead of worrying about extending all of Triton's different individual shader programs for geocentric, flat, projected grid, and conventional mesh rendering.

For deeper integration, we offer full source licenses in addition to binary licenses on our website (www.sundog-soft.com). However, even with a binary license you receive the shader sources and the ability to hook into them as described here.

Building Triton from source

Licensed customers of the full source Triton Ocean SDK will receive a special SDK installer that includes Triton's full source code, enabling you to modify Triton to meet your own special needs.

You'll find solution files for Visual Studio 2015-2019 (vc14) and 2022 (vc143) included at the top level directory of the SDK. Select the build flavors you need, for example "Triton-MT-DLL" for multi-threaded, non-debug, DLL runtimes, "Triton-MTD-DLL" for multi-threaded debug DLL runtimes, etc.

In order to compile the TritonCUDA DLL project, you'll need NVidia's CUDA Toolkit 11.1 installed if you are compiling our Visual Studio 2015-2019 libraries (the -vc14 solution and project files), or CUDA Toolkit 11.7 if you are compiling with the Visual Studio 2022 solution files (-vc143). If you are compiling for 32-bit targets, install CUDA Toolkit version 6.5, then install the appropriate version of 11. This complexity arises from the fact that the CUDA Toolkit dropped support for Visual Studio 2015 after version 11.1, and for 32-bit builds after version 6.5. Linux users may just install the most recent CUDA Toolkit that supports the OS and architecture you are developing on and targeting for.

The CUDA Toolkit is freely available from Nvidia's website, and specific versions are available through their CUDA Toolkit Archive Page (currently at developer.nvidia.com/cuda-toolkit-archive)

If you're building 32-bit targets and need CUDA support, you first need to install Visual Studio 2013 as well (the free community edition will do.) NVidia's CUDA Toolkit 6.5 (required for 32-bit support) does not support Visual Studio 2015 or newer, and so our TritonCUDA project is set up to use the Visual Studio 2013 (vc12) compiler in this case.

You'll find that the documentation included with the full source SDK includes references on all of Triton's internal classes, in addition to the public API's.

If you have any trouble building Triton, drop a note to suppo.nosp@m.rt@s.nosp@m.undog.nosp@m.-sof.nosp@m.t.com.

Integrating planar reflection maps with Triton

In addition to environment cube maps, you may also pass in planar reflection maps for use with Triton. This is useful for generating local reflections from ships and terrain. Use Triton::Environment::SetPlanarReflectionMap() for this effect.

The type of texture parameter passed into this method will vary depending on what renderer you're using. OpenGL users should pass in a GLuint representing the texture ID of the planar reflection texture. DirectX9 users should pass in a LPDIRECT3DTEXTURE2D. DirectX11 users should pass a ID3D11ShaderResourceView pointer.

Triton can use planar reflection and environment map textures together. If both are applied, the alpha channel of the planar reflection texture is used to blend between planar reflection and environment cube map reflections. This is a good way to get the "best of both worlds," with the cube map providing environmental reflections from steep wave angles, and the planar reflection map providing local reflections directly above the water surface.

Generation of planar reflection maps can be considered to be an advanced topic. It requires a render to texture pass to produce a proper reflection map. The scene rendered to such a texture has to be mirrored in the reflection plane by scaling the height values by -1 (this can be accomplished by multiplying a scaling matrix into the reflection camera's view matrix). Such scaling flips winding directions, so will be necessary to change the polygon winding order used for backface culling when rendering this reflection camera. Application of user clip planes to cut off pieces of scene models normally hiden below the water surface may be also required. All these topics are described in great detail in various documents available on the Internet. If you seek more info try searching the web for "Water Rendering" and you will surely find plenty of documentation on the topic of rendering mirror textures. The OpenSceneGraphReflection sample included with the SDK may also be used for reference.

Together with a planar reflection texture, Triton requires passing a texture matrix used to project Triton's computed reflection vector to texture coords. In the most common scenario, this is exactly the View*Projection matrix used to render the main camera, scaled and translated to 0..1 x 0..1 texture coord space. Usually, you construct the texture matrix for the reflection map lookup by first zeroing out any translation in your main scene's view matrix, then multiplying by the projection matrix, then multiplying by a transformation matrix that translates by (1, 1, 1) and scales by (.5, .5, .5) to get to texture coordinates. (DirectX users will want to translate by (1, 1, 0) and scale by (0.5, -0.5, 1.0) due to different conventions.) However, more complex scenarios aiming to optimize the use of reflection map space can adopt less intuitive View and Projection matrices. Such unusual TextureMatrices can be also passed to Triton::Environment::SetEnvironmentMap().

For example, here's some OpenSceneGraph code that computes the proper texture matrix to pass in for Environment::SetPlanarReflectionMap, assuming the texture was rendered from a camera that flipped the height coorinates using the same view and position as the main camera:

view.setTrans( 0, 0, 0 );
_textureProjectionMatrix->set( view *
projection *
osg::Matrix::translate( 1,1,1 ) *
osg::Matrix::scale( 0.5, 0.5, 0.5 ) );

Whatever matrix you pass in will be applied to the view vector in our fragment shaders, prior to projective texture mapping to the reflection texture. Our shaders are all located inside the Resources folder and complied at runtime, so if you need to modify them to support alternative reflection schemes you are free to do so.

Triton does provide a helper function to compute commonly used reflection and texture matrices for planar reflections with Triton::Ocean::ComputeReflectionMatrices(). This method will give you a 4x4 matrix to flip your scene about the local water surface, and a 3x3 matrix that can be used for texture lookups into your reflection texture. Here's some pseudo-code in OpenGL illustrating how to use it to create a valid reflection texture map and matrix suitable for Environment::SetPlanarReflectionMap().

// The following code renders models into a texture as part of the reflection map pass.
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
// Apply the camera position and rotation.
glLoadIdentity();
glRotated(pitch, 1, 0, 0);
glRotated(yaw, 0, 1, 0);
glTranslated(-camX, -camY, -camZ);
// Pass the camera info to Triton
double mv[16], proj[16];
glGetDoublev(GL_MODELVIEW_MATRIX, mv);
glGetDoublev(GL_PROJECTION_MATRIX, proj);
tritonEnvironment->SetCameraMatrix(mv);
tritonEnvironment->SetProjectionMatrix(proj);
// Ask Triton for reasonable matrices to use
Triton::Matrix3 reflectTexMatrix;
Triton::Matrix4 reflectionMatrix;
if (tritonOcean->ComputeReflectionMatrices(reflectionMatrix, reflectTexMatrix))
{
// Flip everything about the water surface
glMultMatrixf(reflectionMatrix.ToFloatArray());
// Activate a render to texture target for your reflection texture
// Setting up and using an FBO is a lot of code so we're not showing
// that here, contact support@sundog-soft.com if you need help.
frameBufferObject->SetupRenderPass();
// Flip the back face culling winding order, since we flipped everything
glFrontFace(GL_CW);
// Set up a user clipping plane to prevent reflection geometry under water
// There are also tricks for doing this by manipulating the near clip plane
// of your projection matrix if you want to look that up.
double planeEq[4] = {0, 1, 0, tritonEnvironment->GetSeaLevel()};
glClipPlane(GL_CLIP_PLANE0, planeEq);
glEnable(GL_CLIP_PLANE0);
// Draw anything you want reflected in the water. Note this is additive to
// any environmental cube map reflections you may have, so you can handle
// sky reflections separately and less frequently.
DrawModels(true);
glDisable(GL_CLIP_PLANE0);
// Close out your render texture and restore things the way they were
frameBufferObject->EndRenderPass();
glFrontFace(GL_CCW);
// Give it to Triton
tritonEnvironment->SetPlanarReflectionMap((Triton::TextureHandle)(frameBufferObject->GetTexture()),
reflectTexMatrix);
// Blend the reflection with other reflections 60%.
tritonOcean->SetPlanarReflectionBlend(0.6f);
}
glPopMatrix();
int TextureHandle
A renderer-agnostic handle for a texture.
Definition: TritonCommon.h:56
A simple 3x3 matrix class and its operations.
Definition: Matrix3.h:30
An implementation of a 4x4 matrix and some simple operations on it.
Definition: Matrix4.h:30
void TRITONAPI ToFloatArray(float val[16]) const
Populates a static array of 16 floats with the contents of the matrix.
Definition: Matrix4.h:96

Note that we also used Triton::Ocean::SetPlanarReflectionBlend() to control the strength of the planar reflections in the final output. You might use this to have stronger reflections in calm water, or to fade out reflections entirely as wave heights increase or prior to disabling your reflection pass.

Restricting Use of Computing Resources

Triton achieves its performance by using any parallel computing resources it can find, including your GPU's and multi-core CPU's. By default, it will typically max out usage of your GPU and CPU in order to render the water as fast as possible. In practice, this means more time is left each frame to render your other objects in the scene.

Some users find this behavior undesirable due to the power usage and heat generated from running your computer so hard, or perhaps it interferes with scheduling of other parallel processes in your application. We allow you to control how aggressively Triton uses your computing resources to mitigate these concerns.

If you need to preserve CPU resources, Triton::Environment::EnableOpenMP() may be used to prevent use of OpenMP to split up intensive loops across multiple cores. This will restrict Triton to using the core it was invoked on, unless Triton is falling back to CPU-based FFT computations.

If you need to preserve GPU resources, you may want to disable Triton's use of CUDA, OpenCL, and DirectX11 Compute Shaders to force all FFT computations to happen on the CPU instead. The config settings disable-cuda, disable-opencl, and disable-compute-shader in resources/triton.config may be used to achieve this.

Using linear color space with Triton

If your application renders its scene internally in linear color space, you'll need to tell Triton to correct its shaders to remove the effects of gamma correction as well.

The method Triton::Ocean::SetLinearColorSpace() may be used for this purpose. If set to true, the ocean surface colors will be raised to the power of 2.2 (which un-does gamma correction of 2.2.) If false, the ocean surface colors are left unchanged.