12
\$\begingroup\$

I want to make a simple liquid something like these gifs:

enter image description here enter image description here

\$\endgroup\$

1 Answer 1

17
\$\begingroup\$

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

enter image description here

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

enter image description here

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); 
}

enter image description here

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

enter image description here

Calculating Diffuse

enter image description here

diffuse = max(dot(N, L)  + 1., 0.);

Calculating Specular

enter image description here

specular = clamp(dot(reflect(L, N),vec3(0, 0, -1)), 0., 1.0);

Final Result

enter image description here


Using shader in unity

  1. Main Camera -> Position (0,0,0)
  2. 3D Object -> Quad -> Position (0,0,1)
  3. Add script to quad and assign shader. Play.
  4. 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
        }

//-------------------------------------------------------------------------------------------
        
    }
}

Source is Available on Github

\$\endgroup\$
0

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .