Summary
This case study demonstrates a boat wake and water splash system for Unreal Engine. The showcase includes multiple boat types moving across ocean scenes, with visual effects reacting to vessel motion and producing trailing foam, spray, and wake patterns.
System Breakdown
did you ever wondered how splash systems work in games ? you have couple of components there to consider :
we can narrow down every particle systems in some simple terms -> where, when and what to spawn !?
Lets dive a little bit deep :
When?
when do we want to spawn a splash particle ? of course when our object has interaction or collide with some liquids ( assume water ) - in most cases there is simple collision object that generate hit events and then you fire your particle system if it is your ocean surface or liquid object ! seems quite straight forward yes ?
Where?
Where do we want to spawn a splash particle ? we already solved the "When" with a simple solution but where to spawn is a little bit more complex than you think ! first rational idea is to just spawn at the collision object world position and thats exactly how it is done in most cases but what if you want more control and precision ? Of course we can create multiple collision objects and query each hit event and so on which is mostly how its done for characters ! since you already have collision objects for each section of the body (Physic asset in unreal engine)(Fortnite use the same system) you can just query those or you can have sockets ! this will solve part of our problem again and give us a little bit more control ! but again what if we want more control and precision !! imagine for a boat that is sailing we want to get the bow, side and stern and each part of the boat can generate splashes, how many collision box or sockets do we need for a cargo boat to behave realistically !?
What?
Now we have straight forward solution for "When" and we partially solved "Where" for smaller objects which is exactly what many games use ! with the collision object or socket solution you can also query some tags that are stored in those components ( or socket name ) and spawn your desired particle effect and trigger different emitters, in this stage we have two "What" -> What to spawn based on which part of the character or objects collided and what that object is collided with? is it water, mud?
In case of a boat you may want a hero splash for your boats bow, little splashes for the side of the boat and wakes splashes for the stern ?
Problem
Ok now we have some solutions for all of the equations! right ? we can just decorate our boat object with some collision components or sockets or just simply an array of vectors ( position in local space of the object which i will explain in a bit) so then we know "Where,When,What" to spawn, it is pretty maintainable for couple of boats and small objects also large objects if you combine it with some heavy foam textures like what we saw in assasins creed black flag which looks amazing btw-they also implemented very innovating piston system for the boat bow splash intensity! which is pretty cool! BUT what if we want to scale the system to 200 300 boats,ships, floatable objects ?
That was the problem we were facing that we needed a system that works automatically mostly ! needs as low as possible maintenance and asset preparation and is precise enough!
Common Solution
If we look a little bit closer to the common solutions that most games use we can see that the process is simple :
- A=array of positions ( component, sockets, nulls)
- B=ocean surface or any liquid surface each vertex position
- You do a check each frame if A<=B -> spawn a splash !
- Intensity can be adjusted by how much A less than B (How much it is submerged)
Approached Solution:
If you have a boat in your game you probably do have a buoyancy system too ! which can give some useful data for splash system -> in our case our buoyancy is based on objects simple collision mesh and submerged part of it ! great then we can fill our array "A" each frame -> yes but the data is on C++/CPU side we want the data to be in GPU for niagara to be able to use it
First prototype: I did send the Data each frame to Niagara with NiagaraDataChannel (NDC from now on) it did work great but there was a performance overhead! I tried NiagaraDataInterface (NDI) better performance than NDC and works pretty fine for couple of boats (under 1ms for the whole system for 4 boats - NDC was 2ms for 4 boats) but we have situations that there are 20 boats on screen ! and the NDC/NDI performance didnt scale well in our case so start search for alternatives.
Long story short :
- "A=Array of positions" : I settled with StaticMesh interface in niagara to get static mesh WS vertex positions
- "B=OceanSurface" : read the data from ocean render target
- Check A<=B each frame for each particle spawned (ParticleSpawnRate = StaticMesh Vertex Count)
- Some Magic HLSL and scratch modules to sort the data => Vertex Velocity, Vertex Normal, Particle Depth, Reject 0.0,0.0,1.0 Normals, Interpolate Vertices Length, etc
- Determine what particles is bow,side,stern ( as seen in the video -> Red=Bow, Green=Stern, Blue=Side) => as the boat is moving forward you can dot product vertex normal with ocean surface normal and find bow,side,stern !
And finally we have a Custom Splash system that is scalable, performant (20 boats under 1ms), precise and can automatically works with every object without that much tweaking per object !