I want to make a simple liquid something like these gifs:
1 Answer
I will explain the concept in shadertoy then I will convert it to Unity because learning with shadertoy is easier.
Shadertoy uses multiple passes, one per "Buffer". As the name indicates, this passes store the results in a buffer. A buffer is just a texture. I am sure Unity will let you render to textures too.
Simple Liquid
Buffer1: Painting
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
fragColor = vec4(distance(iMouse.xy, fragCoord) < 10.0);
fragColor += texture(iChannel0, fragCoord.xy / iResolution.xy);
}
Buffer 2: Scaling Everything From Buffer 1
float s = 0.;
vec4 Move(vec2 U,vec2 dir) {
// smoothing gives a circle
s += 1./ length(dir);
// using texelFetch ( if speed always refer to an integer number of pixels ) would make your life a bit easier
vec4 result = texelFetch( iChannel1, ivec2(U+dir) , 0 );
result /= length(dir);
return result;
}
void mainImage( out vec4 O, vec2 U )
{
O = vec4(0);
// with 8-neighborhood you have square
O += Move( U, vec2( 1, 0) )
+ Move( U, vec2(-1, 0) )
+ Move( U, vec2( 0, 1) )
+ Move( U, vec2( 0,-1) )
+ Move( U, vec2( 1, 1) )
+ Move( U, vec2( 1,-1) )
+ Move( U, vec2(-1, 1) )
+ Move( U, vec2(-1,-1) );
O /= s;
O += texelFetch(iChannel0, ivec2(U), 0);
}
Simple Liquid + Lighting
Normal Calculation Using dFdx and dFdy
dFdx and dFdy return the partial derivative of expression p in x and y, respectively. Deviatives are calculated using local differencing. Expressions that imply higher order derivatives such as dFdx(dFdx(n)) have undefined results, as do mixed-order derivatives such as dFdx(dFdy(n)). It is assumed that the expression p is continuous and therefore, expressions evaluated via non-uniform control flow may be undefined. https://docs.gl/el3/dFdx
Calculating Diffuse
diffuse = max(dot(N, L) + 1., 0.);
Calculating Specular
specular = clamp(dot(reflect(L, N),vec3(0, 0, -1)), 0., 1.0);
Final Result
Using shader in unity
- Main Camera -> Position (0,0,0)
- 3D Object -> Quad -> Position (0,0,1)
- Add script to quad and assign shader. Play.
- Use left mouse to paint
LiquidPaint script:
using UnityEngine;
public class LiquidPaint : MonoBehaviour
{
public int Resolution = 1024;
public Shader LiquidPaintShader;
Material material;
RenderTexture RTA1, RTA2, RTB1, RTB2;
bool swap = true;
void Blit(RenderTexture source, RenderTexture destination, Material mat, string name, int pass)
{
RenderTexture.active = destination;
mat.SetTexture(name, source);
GL.PushMatrix();
GL.LoadOrtho();
GL.invertCulling = true;
mat.SetPass(pass);
GL.Begin(GL.QUADS);
GL.MultiTexCoord2(0, 0.0f, 0.0f);
GL.Vertex3(0.0f, 0.0f, 0.0f);
GL.MultiTexCoord2(0, 1.0f, 0.0f);
GL.Vertex3(1.0f, 0.0f, 0.0f);
GL.MultiTexCoord2(0, 1.0f, 1.0f);
GL.Vertex3(1.0f, 1.0f, 0.0f);
GL.MultiTexCoord2(0, 0.0f, 1.0f);
GL.Vertex3(0.0f, 1.0f, 0.0f);
GL.End();
GL.invertCulling = false;
GL.PopMatrix();
}
void Start ()
{
material = new Material(LiquidPaintShader);
RTA1 = new RenderTexture(Resolution, Resolution, 0, RenderTextureFormat.ARGBFloat); //buffer must be floating point RT
RTA2 = new RenderTexture(Resolution, Resolution, 0, RenderTextureFormat.ARGBFloat); //buffer must be floating point RT
RTB1 = new RenderTexture(Resolution, Resolution, 0, RenderTextureFormat.ARGBFloat); //buffer must be floating point RT
RTB2 = new RenderTexture(Resolution, Resolution, 0, RenderTextureFormat.ARGBFloat); //buffer must be floating point RT
GetComponent<Renderer>().material = material;
}
void Update ()
{
RaycastHit hit;
if (Input.GetMouseButton(0))
{
if (Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition) , out hit))
material.SetVector("iMouse", new Vector4(
hit.textureCoord.x * Resolution, hit.textureCoord.y * Resolution, 1.0f, 1.0f));
}
else
{
material.SetVector("iMouse", new Vector4(-1000.0f, -1000.0f, -1.0f, -1.0f));
}
material.SetInt("iFrame",Time.frameCount);
material.SetVector("iResolution", new Vector4(Resolution,Resolution,0.0f,0.0f));
if (swap)
{
material.SetTexture("_BufferA", RTA1);
Blit(RTA1, RTA2, material, "_BufferA", 0);
material.SetTexture("_BufferA", RTA2);
material.SetTexture("_BufferB", RTB1);
Blit(RTB1, RTB2, material, "_BufferB", 1);
material.SetTexture("_BufferB", RTB2);
}
else
{
material.SetTexture("_BufferA", RTA2);
Blit(RTA2, RTA1, material, "_BufferA", 0);
material.SetTexture("_BufferA", RTA1);
material.SetTexture("_BufferB", RTB2);
Blit(RTB2, RTB1, material, "_BufferB", 1);
material.SetTexture("_BufferB", RTB1);
}
swap = !swap;
}
void OnDestroy ()
{
RTA1.Release();
RTA2.Release();
RTB1.Release();
RTB2.Release();
}
}
Liquid Paint Shader
Shader "LiquidPaint"
{
SubShader
{
//------Common-------------------------------------------------------------------------------------------
CGINCLUDE
#pragma vertex VSMain
#pragma fragment PSMain
Texture2D<float4> _BufferA;
Texture2D<float4> _BufferB;
SamplerState _LinearClamp;
int iFrame;
float4 iMouse;
float4 iResolution;
void VSMain (inout float4 vertex:POSITION, inout float2 uv:TEXCOORD0)
{
vertex = UnityObjectToClipPos(vertex);
}
ENDCG
//------Buffer A-------------------------------------------------------------------------------------------
Pass
{
CGPROGRAM
void PSMain (float4 vertex:POSITION, float2 uv:TEXCOORD0, out float4 fragColor:SV_TARGET)
{
float2 fragCoord = uv * iResolution.xy;
fragColor = (distance(iMouse.xy, fragCoord) < 10.0).xxxx;
fragColor += _BufferA.Sample(_LinearClamp, fragCoord.xy / iResolution.xy);
}
ENDCG
}
//------Buffer B-------------------------------------------------------------------------------------------
Pass
{
CGPROGRAM
#define Move(U, dir) _BufferB.Load( int3(U + dir, 0) )
void PSMain (float4 vertex:POSITION, float2 uv:TEXCOORD0, out float4 O:SV_TARGET)
{
float2 U = uv * iResolution.xy;
O = float4(0,0,0,0);
float s = 0.;
for (int i=0; i<9; i+= i==3 ? 2 : 1 )
{
float2 D = float2( i%3-1, i/3-1);
O += Move( U, D ) / length(D);
s += 1./ length(D);
}
O /= s;
O += _BufferA.Load( int3(U, 0) );
}
ENDCG
}
//------Image-------------------------------------------------------------------------------------------
Pass
{
CGPROGRAM
float diffuse;
float specular;
void calculateLighting(float2 uv)
{
float l = length(_BufferB.Sample(_LinearClamp, uv).xyz);
float dx=ddx(l)*iResolution.x;
float dy=ddy(l)*iResolution.y;
// Calculating Normal by dx and dy
float3 N =normalize(float3(dx,dy,100.0));
// Light Direction
float3 L = normalize(float3(1.0, 1.0, 2.0));
// Calculating Diffuse
diffuse = max(dot(N, L) + 1., 0.);
// Calculating Specular
specular = clamp(dot(reflect(L, N),float3(0, 0, -1)), 0., 1.0);
specular = pow(specular, 12.0);
}
void PSMain (float4 vertex:POSITION, float2 uv:TEXCOORD0, out float4 fragColor:SV_TARGET)
{
float2 fragCoord = uv * iResolution.xy;
// Normalized pixel coordinates (from 0 to 1)
calculateLighting(uv);
// Get Liquid From Buffer B
float3 liquid = _BufferB.Sample(_LinearClamp, uv).rgb;
// Normalizing Liquid
liquid = clamp(liquid,0.,0.5);
// Cream Color
float3 color = float3(1.,0.99,0.81);
float3 finalColor = liquid * color * diffuse + specular;
fragColor = float4(finalColor, 1.0);
}
ENDCG
}
//-------------------------------------------------------------------------------------------
}
}