CONN BURANICZ
TECH ARTIST
GRAPHICS - SHADERS - CODING - TOOLS
Realtime Ocean in Unreal 4 Test Map
REAL-TIME OCEAN
When I joined Sanzaru in February 2016, Bill Spence told me my first task will be to make an ocean for the VR project that would be later titled "Asgards Wrath." This was very challenging for me on several fronts:: first off, this was my first time really diving into Unreal 4 (used Unreal 3 sparingly in college). Second, this was my time developing in a Physically-Based Lighting setting (instead of Blinn-Phong). I had to read articles on the Cook-Torrance Lighting Model (micro-facts what??) Third, this project was Oculus SDK2, rendering two stereoscopic scenes at 90 Hz each...performance was very important! Frankly, I was in over my head, and I ended up spending months on the ocean tech alone. I've recreated the Realtime-Ocean on my personal PC to show talk a little bit about some of the features.
I ended up leaving Sanzaru Games before the Kraken Boss battle even went into full production. Here is footage of the Boss Battle. Sadly, many of the features were cut, most likely for performance reasons. Sadly, the ocean surface doesnt change as the Player or Kraken interact with it. We had a solution with a RenderTarget to Mask off and Displace where huge characters (Loki, Tyr etc) intersected the surface. It wasnt perfect, and I guess Sanzaru chose an all-VFX solution for that. It starts around 3-Min mark. Happy for the team!
GERSTNER WAVES
GETTING STARTED
First off I want to thank Nick Kitten and Dan Halpern SO much for mentoring me throughout the ocean. Also, I want to deeply thank Evan Arnold, my Lead, for encouraging and being patient with me as I stumbled through and made way too many mistakes!
Anyhoo, the foundation of an Ocean sim is the waves. At the start, I went with what I was already familiar with:: waves were created by sampling a seamless displacement texture four times. Each sample used UVs scrolling in different directions. The averaged result displaced the vertices on World Up. I remember Dan H wasn't satisfied with this stylistic approach at all. It didn't look grounded or realistic enough at all to him. He pointed me to Gerstner Waves particularly because the surface would deform on all axis' and not just vertically.
Gerstner Waves look quite convincing, especially because of the peaks and troughs that seem to overlap. But looking closely...it's a bunch of repeating circular motions, nothing fancy. Yes, its the Shader author's favorite:: trigonometry, a series of averaged sin() and cos() calls!
Its the Tech Artist's favorite:: sin() and cos() functions! Amplitude, Frequency and Offset. Each Vertex's X,Y,Z component are fed through the function, with Vertex Position used as the seed. Chart sourced from Wikipedia
This GIF is the PERFECT visualization for Gerstner Waves (I wish I made this) As you can see its (sin/cos) circular path!
Its the Tech Artist's favorite:: sin() and cos() functions! Amplitude, Frequency and Offset. Each Vertex's X,Y,Z component are fed through the function, with Vertex Position used as the seed. Chart sourced from Wikipedia
CLUSTERS OF WAVES
In 2016, there were already several examples of the UDN Community sharing complex water solutions in Unreal 4, including Gerstner Waves. I found a video tutorial of how to implement Gerstner Waves in Unreal's Material system. That's the blessing/curse when working in Unreal. There's so many examples out there to help you implement features, but then that means most of your work is incredibly redundant. Anyhoo, I implemented Gerstner Waves into nested Material Functions. The YouTube video was utilizing two Sets of Gerstner Waves, one large set, one small set. Each "Set" comprised of eight Gerstner Wave Clusters given a specific direction. Totaling 16 Gerstner Waves (32 trig function calls) in the Vertex Shader. However, for "Asgard's Wrath" I refactored so we used still used 16 Gerstner Wave function calls, but divided into Four Sets of Gerstner Wave Clusters::
-
Huge Ocean Waves:: 2.0 Scale
-
Mid-Level Waves:: 1.0 Scale
-
Repeating Waves:: 0.5 Scale
-
Micro-Detail Waves:: 0.25 Scale
Using an Unreal Material Parameter Collection Asset, Bill Spence and Art Direction could control the global Amplitude, Frequency, Sharpness etc. The same values would be applied to each Gerstner Wave Set, but scaled respectively. Individual Layers were hand-tuned with const functions to look best for their purpose. For example, the direction of each Set of Gerstner Wave had four Layers, each of these Layer's Direction Vector was set to roughly one of the four cardinal directions. This makes the Ocean Sim feels like the waves are coming from all over, and not a specific direction. "Asgard's Wrath's" needed to look good and stormy, no matter which direction you were looking from.
Each Gerstner Wave Cluster is comprised of four Gerstner Wave Material Function Calls. Inside each Gerstner Wave is two Trig Function Calls (about other arithmetic logic)
CPU AND GPU
At first, I implemented the real-time Ocean strictly through the Material system. However, things got more complicated when the Leads wanted Ships, Debris, Barrels to float in the water and move with the waves 1:1. This meant I needed to implement the Gerstner Wave functions again, but in C++...and make sure that the GPU and CPU were completely in sync. At the start, there were alot of floating Barrels...couldn't get things to align! I ran into alot of gotchas::, for example, sin() and cos() complete cycles were different values between gameplay code and Material System. On the CPU -1.0 to 1.0 represented a complete period while 2 PI represented a complete period on the GPU. Rather than using the Material Time Node, we passed Time (and other parameters) via the OceanSimStruct so that CPU and GPU would have be in sync. This also gave us the ability to pause, fast-forward, and reverse the water. That was quite fun!
WAVE PARTICLES
Runtime-Generated texture that gives non-repeating micro-detail when the Camera is close to the Ocean surface
DERIVED FROM THE MASTERS
The Gerstner Waves are procedural, meaning they can be scaled up and down to any size. However, if you scale them down too much, a repetitive pattern becomes very apparent (not unlike if you repeat a texture too much). This was Not a problem in "God Mode" but repetition became very noticeable in "Mortal Mode" with the camera much closer to the ocean surface. Instead of adding more expensive Gerstner Wave Clusters to create the low level detail, we used Wave Particles! Wave Particles gives the sim a layer of micro-detail (close to the camera) that will not create a repeating pattern. In "Asgard's Wrath" the Ocean Actor is essentially attached to the camera. The Wave Particles Layer is projected onto the Ocean Surface essentially at LOD 0 distance.
Naughty Dog had fantastic presentation of their Ocean Tech for "Uncharted 3" on PS3. I followed Naughty Dog’s suite and added procedural wave particles by studying their GDC slides and video. In his GDC session Carlos Gonzalez Ochoa sourced a 2007 Siggraph presentation on Wave Particles.
http://www.cemyuksel.com/research/waveparticles/
Cem Yuskel’s Siggraph dissertation discussed spawning N number of Wave Particles from point sources ( such as objects falling into the water ). However, Carlos at Naughty Dog vied for just a random distribution of N wave particles ( at initialization ) to create ambient displacement. And so, I followed suite. Although, I will discuss later our implementation of Oculus Motion Controller interaction with the water surface ( to create ripples and waves ).
CPU AND GPU
The brunt of the work is generating the Wave Particle onto a RenderTexture. That’s all handled in the OceanManagement class in C++. The Material on the GPU side samples the RenderTexture for the offset and generates the Normal Map using derivative screenspace. Since Wave Particles are only portion of the ocean puzzle, the RenderTexture and “particle” count needs to be kept rather low.
RENDER TEXTURE
For our Unreal Ocean, I’m updating N number of Wave Particles every frame and writing them onto a TextureRenderTarget2D. The RenderTexture ends up looking like an undulating heightmap. The Ocean Material reads from the RenderTexture and adds it value to the World Position Offset and Normal Output of the material. Each Wave Particle only requires two variables; its XY position and XY velocity. So, in code, I’m storing all Wave Particles into a TArray.
Spawning 800, 16×16 WaveParticles rendered onto a 256×256 Heightmap. Looks alot like some sort of Perlin Noise function don't it!
RANDOM AND REPETITION
The Wave Particle simulation is completely deterministic; a particle’s position is 100% knowable at any point in time because the velocity is constant. If the randomized values are seeded the same, then the Wave Particle simulation will always turn out the same. Now, in the Siggraph paper, they were using upwards of 10,000 Wave Particles to generate “true to life” ocean waves….that’s a tad bit over Naughty Dog and Sanzaru rendering budget! Keeping the count below 1K still results in some pretty dynamic and varied looking small-detail waves.
As said before, we’re using Wave Particles instead of more Gerstner Waves because scaling down for small surface details would make the repeating pattern too noticeable…unless dozens more Gerstner Waves were used. As for Wave Particle repetition, there’s too much chaos to find a pattern with 800 Wave Particles, at 1/16th the size of the Texture.
Wave repetition is based mostly on the canvas size that the Ocean Material is sampling the Render Texture. Even repeating every 500 Units ( 5 meters ), its hard to see any “checkerboarding” with the Wave Particles, unlike the scaled down Gersnter Waves.
INITIALIZE
In C++, IntializeWaveParticles() is called at BeginPlay(). First, It creates and initializes the TextureRenderTarget2D; setting its size, clear color, and to grayscale compression. Second, it creates a TextureRenderTarget2DResource and Fcanvas; this is what will actually be “drawn” on. Lastly, the WaveParticleList TArray will be initialized, foreach item in the Array, a random start pos from ( 1 to TextureSize ) will be chosen. For the particles’ velocity, I wanted to ensure an even dispersal from all directions. So, the the first direction is randomly decided, and the following Wave Particles always increment between 2 and 5 degrees clockwise.
UPDATE
Both UpdateWaveParticles() and DrawWaveParticles() happen successively during OceanManagement’s Tick(). The Update function is only for incrementing the position of each Wave Particle. In the for loop, the current particle’s new position is added to by a vector ( direction * speed * deltaTime ). There’s four conditional statements checking if current particle has passed bounds of Canvas. If so, its current position is either subtracted or added by the Texture Size so that it wraps around.
DRAW
Once again, the WaveParticlesList TArray is put through a for loop, each particle is drawn on the canvas as a TileItem. The Tile Item is set to Additive so that Particle Tile pixels drawn on top of each other will become brighter. The Particle Tile Alpha is user-driven, but set to a very low number. By default, its set to 0.05 so that it will take 20 Particle Tile overlaps to reach max intensity ( height ). At the end, the Canvas is flushed so the Rendering Thread will know to draw the batched elements.
So there’s a really obtrusive, and amauter-looking wall of code in the Draw function…eight conditional statements ( each looking very similar ). This is for addressing the texture wrapping issue. When a Particle is reaching the bounds of the canvas, it has the potential to be four places at once ( if it hits a corner )!! Therefore, the eight conditional statements address all cardinal and intermediate directions. I wish I could make the wrapping code cleaner, but brute force is the only real solution for this matter.
MATERIAL SIDE
The OceanManagement Blueprint points the Wave Particle Render Texture to a Texture Object that the Ocean Material is using. I created a Wave Particle Function Material that outputs both the Displacement and Normal value. To get more bang for the buck; the function material samples the Render Texture three times, at 33%, 66%, and 100% the intended canvas size. This will further add Wave Particle detail and a fraction of the cost.
Collection Params are fed into the Wave Particle Function Materials
WAVE PARTICLE NORMALS
The wave particle Normal Map derives from it’s height map. There’s several ways to do this, and I tried two within the Material Editor. First, I tried to the partial difference derivative functions DDX and DDY. However, because they rely on screen-space coordinates, artifacts were popping up with extreme view angles from the camera to the water surface. These artifacts become especially severe in VR. So, I went with Unreal’s built in function material called “Normal From HeightMap.”
This approach produces the perfect result, but much more expensive. The function material simply uses arithmetic and inexpensive Dot and Cross Products. It becomes expensive because it needs to sample the heightmap three times ( for each axis ). In the end, the Wave Particle normal is added to the Gerstner and Texture Normals, but only the Red and Green channels.
Left == Off | Middle == Enabled | Right == Normal View