Creating fast water reflection

Written by Stepan Pazderka on .

Creating realistic water reflection


When we were working on The Admiral, it become clear very soon, that we will have to deal with water reflection. As obvious, ocean reflection is quite complex, and although rendering reflection in Arnold is quite fast, it will never catch up with speed and efficiency of doing such a thing at compositing phase.

Water reflection can get quite complex. Sure, we can have mirror-like specular reflection on perfectly smooth surfaces (which a still water is), however that is very rare the case on open sea. On ocean, water consists of big waves, as well as small waves. Lets take a look at some ocean reflection to get ourselves in the picture (literally).


A typical smooth surface produces specular reflection. That is out of our focus right now.


Specular reflectivity on smooth surface with large waves.


Complex water waves causing noise reflection. That is the type of reflection we were hunting in The Admiral

As we can see above, the reflection that we are going after is quite complex. There are a big waves distorting the reflection. The waves are getting close each other towards the ship. On top of that, there is also some smaller waves around. We would like to simulate both of these inside of nuke and save some to our 3D department.

From what we are seeing the process is quite straight forward. First of all we need a distortion map with which we are going to simulate a large waves reflective distortion. Typical IDistort presented in both Nuke and NukeX will be absolutely fine for that. We need to have just a kind of some map, that repeats lines with bigger frequency.

Oh yeah. Sinusoid have came to our minds right? Where would the humanity would be, without sinusoids.

Here is some typical sinusoid map inside of nuke. I am using regular expression node to create some map.


Note the expression I am calling inside Alpha channel:


I am calculating sinusoid on y value. On each pixel of image I am asking it's position in y coordinate. And then I am calculating sinus for each different y value on that position.

Important: Unlike most of the calculators you will get in touch with, sin function inside nuke is expecting radians and not degrees. So result for sin(90) in "practically" any calculator app on both Windows, OS X, iOS etc. will be 1, but inside nuke, it presuming you mean 90 radians and result will be around 0.9.

As you can see there, I am also dividing that number. That is because the numbers are changing pretty wildly. On he first row, row number 0, result of sin operation for 0 is 0, on row number 1, we would get 0.8, on row number 2 we would get 0.9, and on number 3, we would get somewhere around 0.14. That is one white line per four pixels. For our purposes, that is too much. We can get less frequent lines by dividing y.

I am however, more used to multiplying something with a number smaller than one. It is in my opinion more readable. Diving y with 20, or multiplying it with 0.05 would give us of course, exactly same result.


Awesome! Same result, but (at least in my opinion) more readable expression.


Changing the looks of these lines is quite simple. We can easily use a simple grade node, turn off black clamp and start to grade this. We can create thinner or fatter lines. Exactly matching our needs. That can be pretty handy right?

Creating motion

However, what we are not getting is any motion. We can use a simple frame variable. Whenever called, frame returns the number of frame playhead is currently looking at. When we combine frame with our map, we can create a motion.


Imagine that by adding frame value to the y, we are pushing entire y map up and therefor we are creating motion.

For sake of good organization, lets name this expression node "Waves_Calculation"

Alright, this is very essential to what we want to achieve. But there is still some work to be done. What we are going after is little bit more advanced. We need the size of the waves to increase gradualy. We need smaller waves at the top and bigger, wider waves at the bottom. But how can we achieve this? Well that is not so difficult as it might seems at first. So far we are just computing sinus function over linear gradient from top to bottom. What we need to do is just to change that linear gradient into some unlinear one. But how?

Well the most forward going solution for this would be to create a simple ramp in some color channel first. We would use that gradient to calculate the sinus function on it. Lets create a simple gradient in red channel first.

Lets create a new expression node named "Red_Gradient" and put following expression into red channel.


If you take a look at the new expression's node red channel, this is what you should get.


That is not too complicated, is it? A simple expression which assign each pixel its y position value divided by height of entire raster. That creates a perfect linear gradient. Exactly what we are looking for.

Now the fun part. Lets connect our first expression "Waves_Calculation" node to the second expression "Red_Gradient".

Keeping good organization in your project is essential to work well. Not just because of you, but going forward, it would be easier for your colleagues to easier understand how something works in your script.

So now we have a linear gradient created in Red_Gradient node and Waves_Calculation in second node, we can combine these two nodes to get effect we are after. We can simply call sinus function on red channel which already contains perfect gradient, there for our result would be pretty much similar to what we already have.

Lets change Waves_Calculation expression to:


Expression {

 expr3 sin((frame*0.2)+(r*100))

 name Waves_Calculation


Ha, almost the same result as before. But the advantage of this is, that the map, we are using to calculate the sinus on is separated in its own node. Why is that important? Well that is handy a lot, because we can now change it’s gamma, and therefor make in unlinear. Pretty cool ha? Just add a simple grade node below Red_Gradient node, and move gamma slider, if you take a look at alpha channel in Waves_Calculation, we are just creating a beautiful perspective waves.

Expression {

 expr0 y/height

 name Red_Gradient


Grade {

 gamma 0.445

 name Grade1


Expression {

 expr3 sin((frame*0.2)+(r*100))

 name Waves_Calculation


This is result in alpha channel of our script:

waves preview

Alright, let there be waves! Now you can use this to distort the image with IDistort, and therefor simulate a pretty nice water reflection.

OK. So far so good? As we can see on the images at the top, these big waves are only one part of the problem. We are also going after the small waves. There are much more chaotic, there does not seems to be any system about them. We somehow need to take some pixels of the image and change their position randomly.

Creating noise

Have you ever heard about STmapping?

Well let me introduce you to this topic little bit. STmapping is a procedure often known by Direct3D or OpenGL programmers.

Well STmap is a simple node in nuke, that maps each pixel of image to the new position described in special map called UV map. ST map is a surface map and UV map is a texture map. However since we are mapping onto simple 2D space it is not important to talk a lot about what is ST map. Lets focus on UV map instead. Here is what a standard UV map might look like.


UV map like this is formed from two simple gradients each in different color channel. One in red and one in green. It is not hard to see them already.

Red channel of UV map describes X (or more specifically U) coordinates. While green channel Y (logically V) coordinates. The understanding of what is UV is pretty simple, XY coordinates describes position on specific raster of specific size changable size. When you say, something is on x position of 220, you naturally expect that raster is at least wide of 220 pixels. However UV coordinates describes positions parametically. It is not important to now what size is UV map going to be, because what is absolutely right in U is always going to be 1, and what is totally left in U is always going to be 0. What is between those two points is interpolated between those two extremes.

That map you see above is default UVmap that is not going to change anything. It is pretty simple. For X position we use red channel from 0 to 1. Similarly for Y. Here is a simple expression node that will render you same map.

Expression {

 expr0 x/width

 expr1 y/height

 name Expression1


If you use this map to calculate STmaping you will see almost the same image as without STmaping. But you will also defocus the original image however. That is natural and standard behaviour since STmaping is happening and while it changes positions of pixels, it has the need to filter them. If you need to maintain sharpnes, set STmaping node a filter to Impulse (Impulse does not blend pixels across image and moves pixels in absolutes coordinates only)

Here is a example of STmap construction. Try it in your nuke.

Expression {

 expr0 x/width

 expr1 y/height

 name Expression2

 selected true

 xpos -2340

 ypos 31


CheckerBoard2 {

 inputs 0

 name CheckerBoard2

 selected true

 xpos -2236

 ypos -59


STMap {

 inputs 2

 uv rgb

 name STMap2

 selected true

 xpos -2236

 ypos 60


Now to the funny part. Still on track? We tried to introduce a little chaotic exchange of some pixels randomly across image. With STmapping that is absolutely possible. What we just need to do is to add into this map some pixels of different color. We can use a simple noise node to do that. :-)

Noise {

 output { -rgba.alpha}

 size {2.1 1}

 zoffset {{frame/10}}

 lacunarity 4.65

 gain 0.204

 gamma 0.26

 center {1295 540}

 name Noise3


And now we need to separate only hight value pixels, so we get a large portions of black space around them. A simple clamp node is better than any expression for this.

Clamp {

 minimum 0.355

 MinClampTo_enable true

 MaxClampTo_enable true

 name Clamp2


But for those, who like expression as much as me, we can also use this

Expression {

 expr0 "r<0.2 ? 0 : r*2"

 expr1 "g<0.2 ? 0 : g*2"

 name Expression3

 selected true

 xpos 2360

 ypos -1209


We can now plus these values onto UV map and is that UV map for STmapping process onto our 2D render. And voila, we have a smaller waves. :-)

I there are also some other tweaks which I we can use to polish this, however instead of me trying to explain everything here, I think it will be much more productive to just share with you entire script from which you can see everything what is happening. Please pay attention to black clamps turned off on various grade nodes and specifically set up clamp nodes since I am often working in negative values.

Here is entire script:



0 # Sen Haerens 2015-04-11 03:53
The download link for the script is not working. It points to an FTP server.
+1 # Stepan Pazderka 2015-04-11 11:18
Hi, just fixed it. :)
+4 # Ambar Narkar 2015-04-29 13:53
may I request you to show us 1 still for an example please ?

I mean in which shot this process is used... :)

0 # NO NO 2016-10-07 17:36
Hello, FTP link requires authentication. ...

You have no rights to post comments

We have 3677 guests and 91 members online