In a project earlier this year at work, we rendered a lot of oceans using Houdini’s Mantra renderer. This is the obvious choice, as it natively supports rendering the Tessendorf wave-based ocean spectra you can generate with Houdini. The usual approach in dealing with these for other renderers, such as Arnold, is to export the ocean as a tileable texture, which obviously has a lot of drawbacks.

As a longtime Arnold user, I wanted to see whether it might be possible to render these ocean spectra directly using Arnold. Internally, the ocean tools are just VEX code generating a displacement. It had also already built the technical basis for this a few years ago: a shader to run VEX directly in Arnold.

This post roughly describes how the resulting approach of directly rendering Houdini oceans in Arnold works.

You can grab the code on GitHub. It is licensed under the MIT license, which means use in commercial works is completely possible, the only restriction being that you need to keep the credit when sharing the code.

Background: Tessendorf Waves

Houdini’s ocean spectra are based on Tessendorf waves. These allow creation of realistic-looking oceans with some simple inputs such as wind direction and depth. These produce a spectrum, basically describing the distribution/size of waves statistically, which can be sampled using the Fast Fourier Transform. For details, see the paper: Simulating Ocean Water.

How it Works

The code uses the HDK, Houdini’s SDK, to execute the VEX code for evaluating the ocean spectra. This is wrapped inside an Arnold shader, which passes the result to Arnold’s vector displacement. A first step in building the shader was figuring out how to evaluate VEX and how to pass data on to CVEX shaders - a process that is not really documented, except for a few sample code files in the Houdini distribution. Another issue was avoiding crashes and deadlocks stemming from how Arnold’s shaders work in contrast to how VEX evaluation works.

In the end, we use Houdini’s HDK tools (using the hcustom tool) to compile the Arnold shader - this particular combination is to get both Arnold and Houdini/VEX to play nice together. It might also be possible to convert everything to CMake, which would provide a somewhat more robust and future-proof way to compile it.

The Arnold shader UI

The generated shader looks much like Houdini’s own Ocean Sample Layers node:


Shading Network

The shader is integrated in a shading network mostly in the usual way:

tangent space disabled

The main gotcha in the shading network is that we need to disable tangent displacement, as the shader provides raw displacement values:


Render Settings

Rendering the ocean requires some adjustment to the render settings. The actual object to be rendered is - same as with Houdini’s native oceans - usually a large plane of the size you want to render in.

Adaptive Subdivision

To achieve the same result as with Mantra, we need to subdivide this plane quite a lot - in fact so often that you end up with about one polygon per pixel. Just doing this is likely to lead only to a very busy computer with no resulting render. Imagine even a 10 polygon by 10 polygon plane: each subdivision doubles the polygons in each direction, so you and up with 10x10x(2+2)^s polygons, which for 11 subdivisions equates to roughly 400 million. Might be possible on a very beefy computer, but definitely not desirable.

The solution is adaptive subdivision. This way, you get less subdivision further away from the camera, but retain a similar level of detail in screen space. The settings shown here are intended to produce one polygon per screen pixel (which is approximately what Mantra does, though in a technically somewhat different way). Note how I’ve set Adaptive Metric to Edge Length and [subdivision] Error to 1 (pixel).

In terms of displacement settings, we need some bounds padding, as usual in Arnold. Unfortunately the approach so far does not work well with Autobump, so we disable it.

Technically obvious, but worth mentioning for completeness: of course you need to assign the ocean material, including displacement, to your object.

Frustum Culling

To reduce unnecessary work, we also enable Frustum Culling on the Arnold node. This disables the displacement outside the camera’s field of view.


The Optimization that Wasn’t

An idea I had for optimizing the process was the following. However, when I tested this, there were not any consistent rendertime improvements, so this is mostly for curiosity and because it provides users with some additional flexibility.

Big drivers of displacement performance in Arnold are the displacement iterations and also bounds padding. The lower we can keep both, the faster our render will start in general.

For optimization, we pre-subdivide and displace the grid a few times before the actual render. The file loaded in is exactly the same file passed to the Ai Ocean Evaluate shader. The 5 subdivisions we do here don’t need to be done during rendertime any more, so we can reduce our subdiv iterations from 11 to 6.

To reduce the necessary bounds padding, we can run some of the displacement and subdivisions before the render, directly in Houdini, using the Ocean Evaluate node. The Arnold shader also lets you supply a rest attribute for this purpose. If that attribute is present, the shader output displacement is the residual displacement after the difference between P and rest is taken into account.

As the SOP-level displacement takes care of the majority of the offset, we can then reduce our bounds padding significantly.


Performance and Limitations

As explained above, Arnold’s displacement works differently from Mantra. Thus, the render startup time is significantly longer, usually in the range of a few several minutes. Also, so far the performance in Arnold even after the initial stages of the render was relatively low, so there is most likely some room for optimization.

As far as I could find out, displacement in Arnold is officially multithreaded, while subdivision within the same object is (possibly) not. In any case, there is a fairly long phase at the beginning of rendering where only one CPU core is in heavy use. Even in some tests where I split the ocean geometry among different pieces, this initial single-threaded phase occured, so there is probably some potential for optimization in a future Arnold version or with some tweak on how we pass the surface to Arnold.

For preview renders during lighting, the optimization described in the previous section might still be helpful, as it allows disabling (or limiting) subdivision while providing a fairly good idea of the end result quickly.

Certain aspects of the Mantra ocean_samplelayers node are not supported at all:

  • the Anti-Aliasing Blur parameter doesn’t really apply in Arnold. To get a similar effect, simply reduce the maximum number of subdivision iterations to use
  • velocity and cuspdir are not available since Arnold does not support multiple shader outputs. This also means that motion blur is not properly supported on the generated ocean and the best bet is to just disable it on the ocean surface by enabling velocity motion blur. Again, the above approach of partially subdividing in SOP-level could prove useful as this (using the trail SOP) would allow you to calculate the velocity on the mesh before the rendertime subdivisions/displacements. While not perfectly accurate you could probably get very close to the “exact” result.

For the cusp, instead of being exposed as a separate output, it is accessible through the alpha channel of the single ai_ocean_samplelayers RGBA output.

Examples & Comparison

You can usually get exactly almost exactly the same image out of Mantra as out of Arnold. Depending on the situation, this project allows you to use either render engine easily without being tied to Mantra.

To see how close the results can get between Mantra and Arnold, here is a comparison render of the same scene with the same spectrum:

Getting it

You can get the code (which is MIT-licensed and can thus be used in your commercial projects) in the repository here on GitHub. I’m happy to provide some basic support/help in getting it to run.