Stylized Water Shader
On this tutorial I’ll clarify step-by-step how one can create a good looking stylized water shader in Unity utilizing Shader Graph. The aim is to not create a bodily correct end result, however slightly to attain a real-time, controllable and handsome stylized water shader.
⚠️ This tutorial has been made with Unity 2022.2.6f1 and Common RP 14.0.6.
Earlier than beginning this tutorial, there are some things that should be arrange.
Jump to heading
#
Render settings
Be certain to allow the depth and opaque textures within the URP pipeline asset.
Jump to heading
#
Shader and materials
Create a brand new Unlit Shader Graph shader. The shader materials must be set the Unlit and the floor kind to Opaque.
You possibly can then create a cloth that makes use of this shader.
Choose this materials and within the Superior Choices, change the Render Queue to be Clear.
Jump to heading
#
Scene
For the scene, I’m utilizing The Illustrated Nature by Artkovski however you should utilize any scene you need.
Within the scene, I created a airplane and assigned it the fabric that we created earlier than.
Now let’s get began!
???? Throughout this tutorial, newly added nodes will likely be marked in inexperienced so you possibly can simply observe alongside to create the shader your self from the bottom up.
Jump to heading
#
2. Determining the depth of the water
The one most essential step in creating our water shader is determining the depth of the water. We’ll use the depth worth it to drive many different results of our water similar to coloration, opacity, refraction and foam.
Jump to heading
#
Digicam-relative depth
Most water shader tutorials use some variation of the next node setup to calculate a depth fade worth that goes from shallow (1, white) to deep (0, black).
On this node setup, the Scene Depth (Eye) node returns the gap (in world house models, meters) between the digicam and the objects beneath the water. You possibly can think about it by tracing an imaginary ray from the digicam in direction of the water that stops when it first hits an object below the water floor. The gap of this ray is what’s returned by the node.
What we really care about is just not the gap between the digicam and the objects below the water, however the gap between the floor of the water and the objects below the water. For this, we will use the alpha element of the Display screen Place (Uncooked) node which supplies us the gap between the digicam and the water floor. We will then Subtract the two distances to get the specified distance which represents the Water Depth. That is visualized within the diagram above.
Lastly we Divide by a depth vary/distance management parameter, Saturate the output (clamp between 0 and 1) after which carry out a One Minus operation to get a white worth close to the shore and a black worth within the deep components of the water.
It is rather essential to notice that this calculated depth worth is not the vertical depth of the water. If you happen to have been wanting on the water and taking pictures an invisible ray out of your eye in direction of a degree of the water, the gap that the ray would journey between hitting the floor of the water and hitting the bottom beneath the water floor is the gap that you just get right here from these nodes. Which means that when wanting on the identical level on the water floor, the returned depth worth will depend on how shallow the angle is below which you’re looking on the water floor.
This impact/artifact may be seen within the video beneath when shifting across the digicam. You possibly can particularly see it on the rocks the place the identical spots on the water get both a black or a white coloration primarily based on on shallow the viewing angle is.
Jump to heading
#
World-space depth
Personally I’m not a fan of how the depth impact appears utilizing the earlier technique. The perceived depth values change when shifting round your digicam and I might slightly have a continuing depth worth that’s unbiased of digicam place.
???? That is only a private desire and you may preserve utilizing the camera-relative depth calculation when you like.
To ‘repair’ this camera-relative depth, I’ve discovered an alternate option to calculate the depth which occurs in world house.
This node setup offers you the depth of the water as when you would put a measuring tape vertically into the water and measure the gap from the floor of the water to the seabed. When shifting the digicam round, the calculated depth values don’t change in look.
Jump to heading
#
Depth Fade subgraph
It’s a good suggestion to place all the nodes we simply created right into a Depth Fade subgraph. This can make our most important graph extra clear and arranged.
On this part we are going to add coloration to our water floor utilizing the depth values we simply calculated.
Jump to heading
#
Shallow and deep
Now that we all know the depth of our water, we will use it to drive the colours and transparency of our water. We will merely use the output of our Depth Fade subgraph (which is a worth between 0 and 1) to Lerp between a shallow water coloration and a deep water coloration.
Lerping between a shallow and a deep water coloration already offers us a pleasant impact for our water.
Going the additional mile: HSV lerping
As defined by Alan Zucconi in this great article about colour interpolation, we will enhance the looks of the colour of our water by lerping in HSV house as a substitute of RGB house. To do that in Shader Graph, a customized operate node can be utilized that converts our RGB colours to HSV, lerps in HSV house after which converts them again to RGB.
half3 RGBToHSV(half3 In)
{
half4 Okay = half4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
half4 P = lerp(half4(In.bg, Okay.wz), half4(In.gb, Okay.xy), step(In.b, In.g));
half4 Q = lerp(half4(P.xyw, In.r), half4(In.r, P.yzx), step(P.x, In.r));
half D = Q.x - min(Q.w, Q.y);
half E = 1e-10;
return half3(abs(Q.z + (Q.w - Q.y)/(6.0 * D + E)), D / (Q.x + E), Q.x);
}half3 HSVToRGB(half3 In)
{
half4 Okay = half4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
half3 P = abs(frac(In.xxx + Okay.xyz) * 6.0 - Okay.www);
return In.z * lerp(Okay.xxx, saturate(P - Okay.xxx), In.y);
}
void HSVLerp_half(half4 A, half4 B, half T, out half4 Out)
{
A.xyz = RGBToHSV(A.xyz);
B.xyz = RGBToHSV(B.xyz);
half t = T;
half hue;
half d = B.x - A.x;
if(A.x > B.x)
{
half temp = B.x;
B.x = A.x;
A.x = temp;
d = -d;
T = 1-T;
}
if(d > 0.5)
{
A.x = A.x + 1;
hue = (A.x + T * (B.x - A.x)) % 1;
}
if(d <= 0.5) hue = A.x + T * d;
half sat = A.y + T * (B.y - A.y);
half val = A.z + T * (B.z - A.z);
half alpha = A.w + t * (B.w - A.w);
half3 rgb = HSVToRGB(half3(hue,sat,val));
Out = half4(rgb, alpha);
}
The customized operate node then matches in like this with the remainder of our nodes.
When wanting on the water, the intermediate colours between shallow and deep will seem extra vibrant.
Going the additional mile: Posterize
A cool and straightforward impact so as to add is posterization. We merely add the Posterize node and add a property to regulate the variety of steps.
A great instance of the place such a posterization method was used is within the sport A Short Hike. Within the picture beneath you possibly can see the totally different coloration bands that are a results of the posterization.
Bonus tip: Use a gradient
If you need much more management over the colours of your water, you may make use of a gradient texture to drive the colour. Within the instance beneath I’ve a gradient that I flip right into a 256×1 texture that I then pattern utilizing the [0,1] depth worth. The step to transform from a gradient to a texture is required as a result of Shader Graph doesn’t assist gradient properties.
This setup lets you do each arduous and tender transitions of colours.
Under is the code I used to transform from a gradient to a texture.
utilizing UnityEngine;
#if UNITY_EDITOR
utilizing UnityEditor;
#endif
utilizing System.IO;public static class GradientTextureMaker
{
public static int width = 128;
public static int peak = 4;
public static Texture2D CreateGradientTexture(Materials targetMaterial, Gradient gradient)
{
Texture2D gradientTexture = new Texture2D(width, peak, TextureFormat.ARGB32, false, false)
{
title = "_gradient",
filterMode = FilterMode.Level,
wrapMode = TextureWrapMode.Clamp
};
for (int j = 0; j < peak; j++)
{
for (int i = 0; i < width; i++) gradientTexture.SetPixel(i, j, gradient.Consider((float)i / (float)width));
}
gradientTexture.Apply(false);
gradientTexture = SaveAndGetTexture(targetMaterial, gradientTexture);
return gradientTexture;
}
personal static Texture2D SaveAndGetTexture(Materials targetMaterial, Texture2D sourceTexture)
{
string targetFolder = AssetDatabase.GetAssetPath(targetMaterial);
targetFolder = targetFolder.Substitute(targetMaterial.title + ".mat", string.Empty);
targetFolder += "Gradient Textures/";
if (!Listing.Exists(targetFolder))
{
Listing.CreateDirectory(targetFolder);
AssetDatabase.Refresh();
}
string path = targetFolder + targetMaterial.title + sourceTexture.title + ".png";
File.WriteAllBytes(path, sourceTexture.EncodeToPNG());
AssetDatabase.Refresh();
AssetDatabase.ImportAsset(path, ImportAssetOptions.Default);
sourceTexture = (Texture2D)AssetDatabase.LoadAssetAtPath(path, typeof(Texture2D));
return sourceTexture;
}
}
Jump to heading
#
Horizon coloration
Subsequent, we are going to add a coloration to the components of the water on the horizon. For this, we are going to use a Fresnel Impact node with a horizon coloration and horizon distance parameter. We’ll use the the output of the Depth Shade from the earlier part and lerp between that and the Horizon Shade.
If you take a look at the water at a really sharp angle, the horizon coloration will present up within the distance.
Jump to heading
#
Underwater coloration
Proper now we’re instantly setting the colour of the water however as a result of we do that, the colours of the objects beneath the water get form misplaced. To repair this, we are going to take the underwater coloration into consideration when shading the floor of the water.
We pattern the Scene Shade node which returns the colour of the geometry beneath the water floor. Then the place we set the opposite colours to be clear, we are going to use the underwater coloration as a substitute. That is executed utilizing the One Minus node. Lastly we add the colours we already needed to the underwater coloration to get the ultimate coloration.
By altering the alpha parts of the shallow and deep water colours, we will management how a lot the underwater coloration is added.
Let’s add a cool impact, refraction!
Jump to heading
#
Refracted UVs
Many nodes that now we have already used just like the Scene Shade and Scene Depth nodes have a UV enter parameter that we left to the default up till now. We’ll begin by including a UV parameter to the Depth Fade subgraph and join it as much as the Scene Depth node within the subgraph.
Now that we will management the UVs which the Depth Fade subgraph makes use of, we will distort these UVs so as to add a refraction impact. Let’s use the next node setup to create refracted UVs.
Within the node setup above, we tile and transfer some Gradient Noise that we then Remap to a [-1,1] vary, Multiply with a refraction power parameter after which add to the Display screen Place (default) (that are the common, undistorted UVs).
It’s a good suggestion to place these nodes in their very own Refracted UV subgraph to make the graph clear and arranged. We will then use this refracted UVs and join them to the UV enter of the Depth Fade subgraph in addition to the UV enter of the Scene Shade node that we used for the underwater coloration.
Now we get a pleasant refraction impact!
Going the additional mile: Fixing refraction artifacts
As you may need seen within the earlier clip, the refraction now we have proper now’s flawed. When an object stands proud of the water you will see that the refraction impact is current in locations the place it shouldn’t be.
One option to remedy that is to carry out a depth verify to see if we should always use the distorted or the undistorted UVS. The Scene Place subgraph is a subgraph I created that accommodates the nodes to calculate the world house scene place that was proven within the first part about calculating the world house depth.
With this repair carried out, the impact appears a lot better and solely reveals up the place you possibly can really see the article within the water.
The following step is so as to add foam to the water.
Jump to heading
#
Floor foam
We’ll begin by including foam that will get drawn on prime of the water floor.
Panning UVs
We’ll add floor foam to the shader step-by-step. We’ll begin by organising the UVs that we’ll use to pattern a floor foam texture. Let’s create a subgraph referred to as Panning UVs and add the next nodes.
This subgraph takes in UVs after which provides motion to them in addition to some Tiling and an Offset.
The nodes on the left are used to transform the course parameter which is a worth between 0 and 1 right into a course vector for the UV motion. You could possibly as nicely have only a vector enter parameter and set the course your self, however this strategy lets you work with a single course slider between 0 and 1 and management the total 360° vary of motion. For efficiency causes, you may depart this out and simply instantly set the course.
Distorted UVs
Subsequent, we are going to take the panning UVs and warp them utilizing some sine capabilities. For this, we are going to use a Customized Perform node as a result of it will be a problem to recreate the mathematics in nodes.
We take the output of the Panning UV subgraph and plug it right into a Customized Perform node. For the customized operate, we use the next code.
void DistortUV_float(float2 UV, float Quantity, out float2 Out)
{
float time = _Time.y;UV.y += Quantity * 0.01 * (sin(UV.x * 3.5 + time * 0.35) + sin(UV.x * 4.8 + time * 1.05) + sin(UV.x * 7.3 + time * 0.45)) / 3.0;
UV.x += Quantity * 0.12 * (sin(UV.y * 4.0 + time * 0.50) + sin(UV.y * 6.8 + time * 0.75) + sin(UV.y * 11.3 + time * 0.2)) / 3.0;
UV.y += Quantity * 0.12 * (sin(UV.x * 4.2 + time * 0.64) + sin(UV.x * 6.3 + time * 1.65) + sin(UV.x * 8.2 + time * 0.45)) / 3.0;
Out = UV;
}
This code provides a distortion to the UVs utilizing sine capabilities to make the froth a bit extra fascinating when shifting round. It appears like this.
Sampling the froth texture
Now that now we have shifting and distorted UVs, we will use them to pattern a foam texture and add it to the floor of our water.
We merely use a Pattern Texture 2D node to pattern the froth texture after which use a Step node so as to add a cutoff. Lastly we multiply by a foam coloration property.
???? You could possibly additionally use a Smoothstep node right here to doubtlessly get a smoother sampling of the froth texture.
We may merely add the floor foam to the water coloration utilizing the Add node nonetheless I imagine we will do a bit higher by mixing it utilizing an Overlay subgraph that we will create. This subgraph accommodates the next nodes.
We will then use this Overlay subgraph to mix between the water coloration we already had (Base) and the output of the floor foam nodes (Overlay).
Utilizing this, the floor foam can get blended extra properly with the water by making an allowance for the bottom coloration of the water beneath the froth. It’s refined, however fairly good.
Jump to heading
#
Intersection foam
Subsequent we are going to add foam that will get drawn on the edges of an object the place it intersects the water floor.
Intersection foam masks
We’ll begin by making a masks that may outline the place the intersection foam ought to present up. For this, we will likely be utilizing the Depth Fade subgraph we created earlier than.
We use the Depth Fade subgraph to create a depth-based masks and use the Intersection Foam Fade parameter to regulate the hardness of the masks. When simply displaying this intersection masks, it appears like this.
Sampling the intersection foam
For the intersection foam, the setup is much like the floor foam. Once more we use the Panning UV subgraph and use these UVs to pattern an Intersection Foam Texture. We use an Intersection Foam Cutoff parameter to regulate the place we minimize off the froth texture utilizing a Step node. We multiply this Intersection Foam Cutoff parameter by the output of the Depth Fade subgraph that we used within the nodes for the depth-based masks. The reasoning is that we would like the froth to be absolutely shaped close to the sides of the article, however disintegrate because it goes additional away from the shore.
The opposite nodes are used to set the colour of the intersection foam. We additionally multiply the alpha (transparency) of the intersection foam by the output of the nodes of the intersection foam masks that we created earlier than. This makes certain that the intersection foam solely reveals up the place we would like it to (within the masks).
The sampled intersection foam texture may for instance appear like this.
Including the intersection foam
Identical to for the floor foam, we use the Overlay subgraph we created to properly mix the intersection foam with the remainder of the colours.
Now now we have added a pleasant intersection foam impact to the water!
An essential characteristic of the water shader is how lighting interacts with it. We’ll go for a stylized look as a substitute of a bodily correct one.
Jump to heading
#
Floor normals
First, we are going to generate floor normals for the water by sampling a Normals Texture. We’ll use a standard trick which is to pattern the feel twice utilizing barely totally different sampling properties. We then mix these 2 samples through the use of the Regular Mix node.
Discover that we solely use a single worth for the Normals Scale and Normals Pace. We simply barely modify them earlier than sampling the Normals Texture for a second time. Once more, we use the Panning UV subgraph we created earlier to maneuver the normals textures.
I’ve put the nodes from the screenshot above in a subgraph referred to as Blended Normals. We will then use the output of those normals and apply a power to them utilizing the Regular Power node. Lastly we remodel them to world house utilizing the Rework node.
We now have shifting normals for our water that we will use the place we will modify the pace, scale and power.
Jump to heading
#
Lighting calculations
Subsequent, we are going to use the normals we simply created to generate lighting results. Once more, we are going to two Customized Perform nodes. Our customized operate nodes absorb a traditional vector, place and consider course (all in world house). The nodes then output a Specular lighting time period. For the normals, we use the output of our world-space reworked normals from earlier. For the place and consider course, we will use the Place and View Route nodes respectively (each in world house). We’ve one customized operate node for Foremost Lighting and one other one for Further Lights. This can make it doable for our mild to react to the primary mild in addition to further level lights.
We will put all the code for the lighting in a single file. The code appears like this.
float LightingSpecular(float3 L, float3 N, float3 V, float smoothness)
{
float3 H = SafeNormalize(float3(L) + float3(V));
float NdotH = saturate(dot(N, H));
return pow(NdotH, smoothness);
}void MainLighting_float(float3 normalWS, float3 positionWS, float3 viewWS, float smoothness, out float specular)
{
specular = 0.0;
#ifndef SHADERGRAPH_PREVIEW
smoothness = exp2(10 * smoothness + 1);
normalWS = normalize(normalWS);
viewWS = SafeNormalize(viewWS);
Gentle mainLight = GetMainLight(TransformWorldToShadowCoord(positionWS));
specular = LightingSpecular(mainLight.course, normalWS, viewWS, smoothness);
#endif
}
void AdditionalLighting_float(float3 normalWS, float3 positionWS, float3 viewWS, float smoothness, float hardness, out float3 specular)
{
specular = 0;
#ifndef SHADERGRAPH_PREVIEW
smoothness = exp2(10 * smoothness + 1);
normalWS = normalize(normalWS);
viewWS = SafeNormalize(viewWS);
int pixelLightCount = GetAdditionalLightsCount();
for (int i = 0; i < pixelLightCount; ++i)
{
Gentle mild = GetAdditionalLight(i, positionWS);
float3 attenuatedLight = mild.coloration * mild.distanceAttenuation * mild.shadowAttenuation;
float specular_soft = LightingSpecular(mild.course, normalWS, viewWS, smoothness);
float specular_hard = smoothstep(0.005,0.01,specular_soft);
float specular_term = lerp(specular_soft, specular_hard, hardness);
specular += specular_term * attenuatedLight;
}
#endif
}
We will then mix the Foremost Lighting and Further Lighting by first working the primary lighting via a Step node to get arduous lighting and we then multiply by a Specular Shade
Lastly we will fairly actually Add the lighting to the present output of the graph.
We now have fairly advanced lighting results for our water, permitting us to change between easy/tough water surfaces, arduous/tender lighting, assist for the primary mild and assist for added level lights.
Bonus tip: Utilizing floor normals to affect refraction
As a substitute of utilizing gradient noise to generate the refraction like we did earlier than, it is perhaps a greater concept to make use of the floor normals to affect the power of the refraction. This appears higher visually. For this, you’ll have to remodel the generated normals from Tangent house to View house, then Multiply by a Refraction Power parameter after which add the end result to the Display screen Place to generate the refracted UVs.
A giant a part of the look of our water shader is now full so let’s add some motion.
Jump to heading
#
Vertex displacement
We will likely be including wave motion by displacing the vertices of the water airplane within the vertex shader. For this to work properly, it is advisable to be sure that your water airplane has a excessive sufficient vertex density in order that there are sufficient vertices to displace.
The nodes beneath present a really fundamental instance of vertex displacement. We take the unique world place of the vertex and add an offset (0,0,0 on this case). We then convert from World Area to Object house and hyperlink it up with the vertex place slot.
Jump to heading
#
Gerstner waves
There are a lot of ranges of simulating wave displacement. You could possibly begin by including easy sine waves or go all in and create a FFT wave simulation. We’ll use one thing between the 2 when it comes to graphical constancy: Gerstner Waves. There are a lot of good tutorials about Gerstner Waves. I’ll simply clarify how I take advantage of them in my shader. If you need extra data, I like to recommend this tutorial about waves by Catlike Coding.
As a result of the code for the waves is math-heavy, we once more use a Customized Perform node and add the output as an offset to the world place like we did within the earlier part.
The next code generates 4 waves in complete after which provides them collectively.
float3 GerstnerWave(float3 place, float steepness, float wavelength, float pace, float course, inout float3 tangent, inout float3 binormal)
{
course = course * 2 - 1;
float2 d = normalize(float2(cos(3.14 * course), sin(3.14 * course)));
float ok = 2 * 3.14 / wavelength;
float f = ok * (dot(d, place.xz) - pace * _Time.y);
float a = steepness / ok;tangent += float3(
-d.x * d.x * (steepness * sin(f)),
d.x * (steepness * cos(f)),
-d.x * d.y * (steepness * sin(f))
);
binormal += float3(
-d.x * d.y * (steepness * sin(f)),
d.y * (steepness * cos(f)),
-d.y * d.y * (steepness * sin(f))
);
return float3(
d.x * (a * cos(f)),
a * sin(f),
d.y * (a * cos(f))
);
}
void GerstnerWaves_float(float3 place, float steepness, float wavelength, float pace, float4 instructions, out float3 Offset, out float3 regular)
{
Offset = 0;
float3 tangent = float3(1, 0, 0);
float3 binormal = float3(0, 0, 1);
Offset += GerstnerWave(place, steepness, wavelength, pace, instructions.x, tangent, binormal);
Offset += GerstnerWave(place, steepness, wavelength, pace, instructions.y, tangent, binormal);
Offset += GerstnerWave(place, steepness, wavelength, pace, instructions.z, tangent, binormal);
Offset += GerstnerWave(place, steepness, wavelength, pace, instructions.w, tangent, binormal);
regular = normalize(cross(binormal, tangent));
}
The customized operate takes in values for the Steepness, Wavelength and Pace of the waves in addition to 4 Route values between [0,1] which may every management a person wave. The conventional vector is calculated as nicely.
???? Be at liberty so as to add much more waves, however I believe 4 waves is already a pleasant start line.
Our waves look easy, however it’s already fairly good.
Bonus tip: Utilizing wave peak to drive coloration
A further factor you are able to do is use the Y element of the Offset output of the Gerstner Waves customized operate node to affect the colours of the water. This fashion you may give the tops of the waves a barely totally different coloration.
Buoyancy is an enormous and complex matter if you wish to obtain a practical simulation. Nonetheless, I wished to share how one can go about including fundamental buoyancy to your water.
Jump to heading
#
Simulation on the CPU
At the moment we’re simulating the waves on the GPU via the vertex shader of our water. To create a buoyancy simulation, we are going to recreate the wave motion on the CPU in a C# script. This fashion we will use this CPU simulation to create buoyancy results for floating objects. The setup appears fairly much like what we already did in our shader. We create a operate GetWaveDisplacement which takes able and a few wave parameters, It then returns an offset which is generated by including 4 waves collectively. I added the script beneath. The aim is to have the very same factor as we did within the vertex shader, however then working on the CPU.
Script: GerstnerWaveDisplacement.cs
utilizing UnityEngine;public static class GerstnerWaveDisplacement
{
personal static Vector3 GerstnerWave(Vector3 place, float steepness, float wavelength, float pace, float course)
{
course = course * 2 - 1;
Vector2 d = new Vector2(Mathf.Cos(Mathf.PI * course), Mathf.Sin(Mathf.PI * course)).normalized;
float ok = 2 * Mathf.PI / wavelength;
float a = steepness / ok;
float f = ok * (Vector2.Dot(d, new Vector2(place.x, place.z)) - pace * Time.time);
return new Vector3(d.x * a * Mathf.Cos(f), a * Mathf.Sin(f), d.y * a * Mathf.Cos(f));
}
public static Vector3 GetWaveDisplacement(Vector3 place, float steepness, float wavelength, float pace, float[] instructions)
{
Vector3 offset = Vector3.zero;
offset += GerstnerWave(place, steepness, wavelength, pace, instructions[0]);
offset += GerstnerWave(place, steepness, wavelength, pace, instructions[1]);
offset += GerstnerWave(place, steepness, wavelength, pace, instructions[2]);
offset += GerstnerWave(place, steepness, wavelength, pace, instructions[3]);
return offset;
}
}
Jump to heading
#
Buoyancy script
Subsequent, we need to use the CPU simulation of the waves we did to really make an object float on the water floor. For this I’ll use an strategy that works utilizing buoyancy effectors. These are factors on an object the place the wave offset is sampled and an acceptable power is added to the article. An object can have a number of effectors positioned across the floor of the article.
In my scene, I simply have these effectors as empty gameobjects as kids of the article that I need to simulate buoyancy for.
We will then create a script referred to as BuoyantObject which can absorb a reference to those effectors and apply power to them.
Script: BuoyantObject.cs
[RequireComponent(typeof(Rigidbody))]
public class BuoyantObject : MonoBehaviour
{
personal readonly Shade purple = new(0.92f, 0.25f, 0.2f);
personal readonly Shade inexperienced = new(0.2f, 0.92f, 0.51f);
personal readonly Shade blue = new(0.2f, 0.67f, 0.92f);
personal readonly Shade orange = new(0.97f, 0.79f, 0.26f);[Header("Water")]
[SerializeField] personal float waterHeight = 0.0f;
[Header("Waves")]
[SerializeField] float steepness;
[SerializeField] float wavelength;
[SerializeField] float pace;
[SerializeField] float[] instructions = new float[4];
[Header("Buoyancy")]
[Range(1, 5)] public float power = 1f;
[Range(0.2f, 5)] public float objectDepth = 1f;
public float velocityDrag = 0.99f;
public float angularDrag = 0.5f;
[Header("Effectors")]
public Rework[] effectors;
personal Rigidbody rb;
personal Vector3[] effectorProjections;
personal void Awake()
{
rb = GetComponent<Rigidbody>();
rb.useGravity = false;
effectorProjections = new Vector3[effectors.Length];
for (var i = 0; i < effectors.Size; i++) effectorProjections[i] = effectors[i].place;
}
personal void OnDisable()
{
rb.useGravity = true;
}
personal void FixedUpdate()
{
var effectorAmount = effectors.Size;
for (var i = 0; i < effectorAmount; i++)
{
var effectorPosition = effectors[i].place;
effectorProjections[i] = effectorPosition;
effectorProjections[i].y = waterHeight + GerstnerWaveDisplacement.GetWaveDisplacement(effectorPosition, steepness, wavelength, pace, instructions).y;
rb.AddForceAtPosition(Physics.gravity / effectorAmount, effectorPosition, ForceMode.Acceleration);
var waveHeight = effectorProjections[i].y;
var effectorHeight = effectorPosition.y;
if (!(effectorHeight < waveHeight)) proceed;
var submersion = Mathf.Clamp01(waveHeight - effectorHeight) / objectDepth;
var buoyancy = Mathf.Abs(Physics.gravity.y) * submersion * power;
rb.AddForceAtPosition(Vector3.up * buoyancy, effectorPosition, ForceMode.Acceleration);
rb.AddForce(-rb.velocity * (velocityDrag * Time.fixedDeltaTime), ForceMode.VelocityChange);
rb.AddTorque(-rb.angularVelocity * (angularDrag * Time.fixedDeltaTime), ForceMode.Impulse);
}
}
personal void OnDrawGizmos()
{
if (effectors == null) return;
for (var i = 0; i < effectors.Size; i++)
{
if (!Software.isPlaying && effectors[i] != null)
{
Gizmos.coloration = inexperienced;
Gizmos.DrawSphere(effectors[i].place, 0.06f);
}
else
{
if (effectors[i] == null) return;
Gizmos.coloration = effectors[i].place.y < effectorProjections[i].y ? purple : inexperienced;
Gizmos.DrawSphere(effectors[i].place, 0.06f);
Gizmos.coloration = orange;
Gizmos.DrawSphere(effectorProjections[i], 0.06f);
Gizmos.coloration = blue;
Gizmos.DrawLine(effectors[i].place, effectorProjections[i]);
}
}
}
}
What’s essential for the BuoyantObject script is that the wave parameters ought to match those that you just use in your water shader. In my very own tasks I normally make it so the BuoyantObject script holds a reference to the water after which simply reads out these properties as a substitute having to set them manually.
This offers us some easy however good buoyancy!
Jump to heading
#
Caustics
Caustics are a very nice impact which you can add to your water. When you have loved this tutorial thus far, you possibly can take a look at my caustics asset which may be added to any water shader and has a number of good options. Here’s a video displaying the impact.
Help could be enormously appreciated! ❤️
Bonus tip: Reflections
Planar reflections may be added through the use of the following script (use at your individual danger, has not been examined shortly ????). Basically it renders every part the other way up to a texture referred to as _PlanarReflectionTexture. You possibly can then pattern this texture in your shader utilizing the next nodes.
Bonus tip: World house UVs
As a substitute of utilizing common UVs for issues like sampling your textures, you may make use of world house UVs. This can allow you to make use of water tiles that you just place subsequent to one another, after which all the results will line up properly!
Bonus tip: Fog
Help for fog may be simply added like this.
Bonus tip: Water trails
I hope you preferred this tutorial. Please let me know what you assume over on Twitter!
You may get the shader recordsdata right here.
https://www.alanzucconi.com/2019/09/13/believable-caustics-reflections/
https://www.patreon.com/posts/making-water-24192529
https://catlikecoding.com/unity/tutorials/flow/waves/