Exploring VDB's in Lightwave 3D

Updated: Sep 4

This tutorial is about experimentations and diverse views on using the openvdb toolkit in Lightwave 3D.


The Idea


The exploration here is about using openvdb to create landscapes. I'ts common to use the nodal displacement on an object with procedural textures until to achieve something like a landscape. This method requires a mesh with subdivisions to work properly, which can be very expensive to the render engine to calculate, and getting longer render times in the end.


The approach I propose is to use the mesh displacement as a base to create an openvdb SDF. In doing so, the 3D artist can "balance" the number of subdivisions in the mesh with the resolution of the openvdb to obtain, in the end, a more organic mesh with an optimized subdivision level and less memory consuming. Therefore, faster render times.


We can save the VDB mesh as an openvdb SDF. This will free the render engine from have to freeze and calculate the SDF before rendering. In my tests, I have got faster renders using this method: when I use the tradicional displacement on a mesh, the render time is always higher.


The Lightwave artist can be tempted to use the openvdb noise node to do this work directly on the vdb SDF. This is a huge mistake. Lightwave will have to do more calculations and the entire system will get a lot slower: as a result, the workflow will be impossible to execute. Instead, if the 3D artist uses the nodal mesh displacement as the driver for the SDF displacement, Lightwave will handle the SDF smoothly.


In this example, I'll create a terrain using nodal displacement on a mesh that will be transformed in an openvdb SDF. In this way, we'll get a more organic terrain than if we only used the traditional method. Next I'll show you the lookdev process, which uses a procedural workflow using Lightwave's native procedural textures. This include the optimization of the render settings. Following up, we'll see how to bake the textures to use on our VDB mesh (yes! we will not convert it to a lightwave object because this will trigger a longer render time). Then, we will apply the textures, render the scene, analyze the results and think about possible workflows. Let's dive in.


The beginning: how to set up the displacement correctly.


My displacement node.
My displacement node.

In my nodal network you can see a compound node called Displacement. Inside there's a nodal setup that it is not complex: it's only a node logic to use Zbrush displacements tweaked to a more general use. Let's understand it.

The displacement node inside.
The displacement node inside.

The subtract node is the main aspect: he subtracts 0.5 from the input. This is what we have to do when dealing with Zbrush displacements, because the zero point is actually mid-gray (0.5). In a common setup, we connect this result to the scalar socket of the vector scale node. In the vector input we connect the normals output and we have our displacement.

An usual displacement setup.
An usual displacement setup.

What I did was replace the normals input with a Mesh Info node because I was creating a compound node to be used in any other scene. Then, he provides the normals information of the mesh I'm using.

The Mesh Info node replaces the standard input node.
The Mesh Info node replaces the standard input node.

The logic node at the top verifies if the scalar input (displacement) is not zero. If not, he passes the value of the displacement output. If the value is zero, que passes the value of the color output. I did this to allow me to use a scalar node as input (like a procedural texture) or an image, for example. Note that I put a vector scalar node to convert the vector input from color to a scalar value. In this case, the vector scalar node was set up to pass the maximum value.

The logic node at the bottom verifies if the input (Value) is equal to zero. If true, he passes the value of 0.5; if not, he passes the value defined by the artist. The add node is only used as a placeholder to organize better the nodal graph: he is adding nothing. The multiply node is doing exactly this: multiplying the displacement value by a factor defined by the artist. The default value is one.

The last vector scalar node was used to output a scalar value. He outputs the maximum value. And this is my Displacement compound node that I saved it and I can use in any other scene I create.

All the Vector-Scalar nodes were set to Maximum.
All the Vector-Scalar nodes were set to Maximum.

The scene set up.


For a landscape we need a plane. I created the plane with the default settings, 10 meters by 10 meters and with 100 by 100 subdivisions. I enabled the nodal displacement and did a temporary basic nodal setup.



Creating the plane that will drive the SDF.
Creating the plane that will drive the SDF.


A basic nodal setup for testing. Soon, we'll go back to do the real stuff.
A basic nodal setup for testing. Soon, we'll go back to do the real stuff.

Next, I created a null and named it volume. Using the scene editor, I set the plane to be hidden in the viewport and to the render engine.

The null was set as an OpenVDB Evaluator.
The null was set as an OpenVDB Evaluator.
I hid the plane , camera and lights to clean up the viewport and easy the work.
I hid the plane , camera and lights to clean up the viewport and easy the work.

In the OpenVdb Evaluator node graph, select the mesh to volume node to convert the plane geometry to a openvdb SDF (Level set).

Well, something is wrong after we've connected the mesh to volume node to the grid output...

When this kind of thing happens, we have two options: either adjust the voxel size or use the filter node to dilate the vdb and create more mass. In this example I chose the latter.

I decided to smooth the vdb a little bit. For this, I added another filter node and chose laplacian flow to do a smoothing that preserves the volume of the vdb.

We have finished the basic setup. Now we will work on the displacement to do our terrain.



Building the terrain.


Going back to the plane nodal displacement, we have enable subpatch and increase the viewport subdivision levels. For now, make sure to set up a value that will not crash your system. I set up to 16 subdivision levels to viewport and the double for the render.

And this is my setup to start. I used a Hetero Terrain node. The other node is our already known displacement compound node.

The initial setup is also a time for exploration of different nodes.
The initial setup is also a time for exploration of different nodes.

Note that the displacement affects all plane. I don't want this. What I want is to create a circular falloff and the displacement to be higher at the center. For this, I'll select a Dots procedural node which will spread dots on the whole surface. I only need a big one, at center. We obtain this result by scaling the procedural to the size of the plane (10mX10m). The opacity value controls how high and how intense is the displacement.

Time to experiment with nodes.
Time to experiment with nodes.
Put the dots the size of the terrain and you'll have a single one.
Put the dots the size of the terrain and you'll have a single one.

To do the texture falloff in Lighwave is very easy because this function is built-in. In the falloff tab, I defined the range (20%) and the type to spherical. I recommend to try others setups: you can obtain very interesting results.

It's very easy to set up a texture falloff in Lightwave.
It's very easy to set up a texture falloff in Lightwave.

Now, we have our mask. Connect the Alpha output from the dots texture node to the Opacity slot of the Hetero Terrain texture node and Voilà!

A lot can be done here, but we stay simple. You're invited to do your own experimentations.


I'll spread a small displacement on the non-affected area. I used a Multifractal texture node and put it in the background color of the Hetero Terrain node. I also increased the contrast of the dots texture to 20%.

Adding a small displacement.
Adding a small displacement.

Again, I control the intensity with the opacity (10% in this case).
Again, I control the intensity with the opacity (10% in this case).
A small change in the dots texture (Contrast = 20%)
A small change in the dots texture (Contrast = 20%)

Lookdev.


The first step is to copy the dots texture node. We'll need it for the shading. Next, we have to improve our displacement. I'm looking for a kind of alien landscape, with a crystal like formation. For this, we need to give to our displacement an appearance of rock formation. We'll do this in the shading. The terrain looks coarse, then I'll first smooth the terrain.

Check Smoothing to smooth the terrain.
Check Smoothing to smooth the terrain.

I pasted the dots texure in the node editor and took a look. That's our mask. I'll use a RidgedMultiFractal texture to do the additional displacement of the terrain.

Masking.
Masking.

I'm going to stretch it on the Y axis. Gradients is the way to do this. The Alpha of the RidgedMultiFractal texture is the reference (input) for the gradient. Again, here I'll do a fast setup, but here we have room for many experimentations.


Configuring the first gradient color:


Configuring the second gradient color:


Stretching...


Fine tuning the mask...

Removing the falloff.
Removing the falloff.

Now, the displacement. We connect the Alpha output from the gradient to the displacement slot in our Displacement compound node; and the displacement output from this node to the displacement slot of the Surface node. I also increased the scale a little bit.

But we can't see this displacement yet, because this is a surface displacement. We must enable it in the object properties.

Surface displacement was enable in the Volume VDB properties.
Surface displacement was enable in the Volume VDB properties.

Now, when we use the VPR, a warning message will pop up. It's bringing attention to the quantity of voxels used in the object and asking if we want to cancel the operation. We'll always answer no. I also reduced the voxel size to have a more detailed SDF. This is not the finished setup yet. This displacement will going to be improved. But, for now, it's ok and we can fine tune our landscape.

I increased the plane subdivision a little bit and added a gradient to have a texture preview of the displacement.

Now we have something as a rock formation.
Now we have something as a rock formation.

It's time for the light setup. I used a HDRI that I made in Terragen.

And I tried to match as good as possible the position of the infinite light with the sun in the HDRI map.

I also used ACES tonemap. Unfortunately, Newtek didn't implement the proper support for OCIO in Lightwave. Tonemap is not the same. By the way, Terragen supports OCIO and the HDRI file I'm using is in acescg colorspace.

I'll use a transmissive material, then I need to adjust some render settings: I set Ray Recursion Limit and Refraction recursion Limit to 32. In doing so I'll have enough light bounces to render the transmission correctly.

The mountain is going to be a mixture of rock and a kind of crystal. First I'll setup the crystal material. Then, the rock material and mix them.

For the crystal material I used a Dielectric material node. The settings can be seen in the following picture.

I copied and pasted the RidgedMultiFractal node and the Dots texture node. The first RidgemultiFractal is going to be the Dielectric Albedo. The second one is going to be the Transmissive Color, and I changed some parameters in the Dots texture.

The Dielectric Albedo
The Dielectric Albedo.
The Transmissive Color.
The Transmissive Color.
The mask.
The mask.

And this was the result:


Again, I copied and pasted the Dots text