【Unity】用2D流体实现在水中添加染料的效果

最终效果图

2D流体简单介绍

2D流体相关的算法很多,这篇文章主要讲应用层面,因此不对算法做较深的说明。

本文流体相关的代码叫Stable Fluids,来自SIGGRAPH的一篇论文。

该代码使用的算法为MAC(Marker and Ceil) Grid,它将一个平面分成多个网格,在网格里面计算流体的密度,在网格的边缘计算流体的向量场。

我们使用这个算法时,只需要添加染料进密度场,然后交互扰动向量场,密度场随之变化,最后我们再读取密度场的信息,就是水体里染料的最终效果。

准备内容

  1. unity standard assets(主要是水体shader)
  2. 2D流体代码  Stable Fluids (见附件)
  3. 最基本的shader知识储备

代码实现

  • 首先将MAC Grid算法移植到unity中,原算法是C++实现,更改为C#也不是很困难,注意将C++的指针转换成C#对应的内容,以及将C++宏定义都正确移植就好。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


public class MAC_Grid
{
    private int N;
    private float[] u;
    private float[] v;
    private float[] u0;
    private float[] v0;
    private float visc;
    private float dt;

    public MAC_Grid(int N)
    {
        this.N = N;
    }

    public int IX(int i, int j)
    {
        return i + (N + 2) * j;
    }

    private void swap<T>(ref T x0, ref T x)
    {
        T temp = x0;
        x0 = x;
        x = temp;
    }

    private void add_source(float[] x, float[] s, float dt)
    {
        int i, size = (N + 2) * (N + 2);
        for (i = 0; i < size; i++)
        {
            x[i] += dt * s[i];
        }
    }

    private void set_bnd(int b, float[] x)
    {
        int i;

        for (i = 1; i <= N; i++)
        {
            x[IX(0, i)] = b == 1 ? -x[IX(1, i)] : x[IX(1, i)];
            x[IX(N + 1, i)] = b == 1 ? -x[IX(N, i)] : x[IX(N, i)];
            x[IX(i, 0)] = b == 2 ? -x[IX(i, 1)] : x[IX(i, 1)];
            x[IX(i, N + 1)] = b == 2 ? -x[IX(i, N)] : x[IX(i, N)];
        }
        x[IX(0, 0)] = 0.5f * (x[IX(1, 0)] + x[IX(0, 1)]);
        x[IX(0, N + 1)] = 0.5f * (x[IX(1, N + 1)] + x[IX(0, N)]);
        x[IX(N + 1, 0)] = 0.5f * (x[IX(N, 0)] + x[IX(N + 1, 1)]);
        x[IX(N + 1, N + 1)] = 0.5f * (x[IX(N, N + 1)] + x[IX(N + 1, N)]);
    }

    private void lin_solve(int b, float[] x, float[] x0, float a, float c)
    {
        for (int k = 0; k < 20; k++)
        {
            for (int i = 1; i <= N; i++)
            {
                for (int j = 1; j <= N; j++)
                {
                    x[IX(i, j)] = (x0[IX(i, j)] + a * (x[IX(i - 1, j)] + x[IX(i + 1, j)] + x[IX(i, j - 1)] + x[IX(i, j + 1)])) / c;
                }
            }
            set_bnd(b, x);
        }
    }

    private void diffuse(int b, float[] x, float[] x0, float diff, float dt)
    {
        float a = dt * diff * N * N;
        lin_solve(b, x, x0, a, 1 + 4 * a);
    }

    private void advect(int b, float[] d, float[] d0, float[] u, float[] v, float dt)
    {
        int i0, j0, i1, j1;
        float x, y, s0, t0, s1, t1, dt0;
        dt0 = dt * N;
        for (int i = 1; i <= N; i++)
        {
            for (int j = 1; j <= N; j++)
            {
                x = i - dt0 * u[IX(i, j)]; y = j - dt0 * v[IX(i, j)];
                if (x < 0.5f) x = 0.5f; if (x > N + 0.5f) x = N + 0.5f; i0 = (int)x; i1 = i0 + 1;
                if (y < 0.5f) y = 0.5f; if (y > N + 0.5f) y = N + 0.5f; j0 = (int)y; j1 = j0 + 1;
                s1 = x - i0; s0 = 1 - s1; t1 = y - j0; t0 = 1 - t1;
                d[IX(i, j)] = s0 * (t0 * d0[IX(i0, j0)] + t1 * d0[IX(i0, j1)]) +
                             s1 * (t0 * d0[IX(i1, j0)] + t1 * d0[IX(i1, j1)]);
            }
        }

        set_bnd(b, d);
    }

    private void project(float[] u, float[] v, float[] p, float[] div)
    {
        for (int i = 1; i <= N; i++)
        {
            for (int j = 1; j <= N; j++)
            {
                div[IX(i, j)] = -0.5f * (u[IX(i + 1, j)] - u[IX(i - 1, j)] + v[IX(i, j + 1)] - v[IX(i, j - 1)]) / N;
                p[IX(i, j)] = 0;
            }
        }
        set_bnd(0, div); set_bnd(0, p);
        lin_solve(0, p, div, 1, 4);

        for (int i = 1; i <= N; i++)
        {
            for (int j = 1; j <= N; j++)
            {
                u[IX(i, j)] -= 0.5f * N * (p[IX(i + 1, j)] - p[IX(i - 1, j)]);
                v[IX(i, j)] -= 0.5f * N * (p[IX(i, j + 1)] - p[IX(i, j - 1)]);
            }
        }

        set_bnd(1, u); set_bnd(2, v);
    }

    public void dens_step(float[] x,float[] x0,float[] u,float[] v,float diff,float dt)
    {
        add_source(x, x0, dt);
        swap(ref x0, ref x); diffuse(0, x, x0, diff, dt);
        swap(ref x0, ref x); advect(0, x, x0, u, v, dt);
    }

    public void vel_step(float[] u,float[] v, float[] u0, float[] v0, float visc, float dt)
    {
        add_source(u, u0, dt); add_source(v, v0, dt);
        swap(ref u0, ref u); diffuse(1, u, u0, visc, dt);
        swap(ref v0, ref v); diffuse(2, v, v0, visc, dt);
        project(u, v, u0, v0);
        swap(ref u0, ref u); swap(ref v0, ref v);
        advect(1, u, u0, u0, v0, dt); advect(2, v, v0, u0, v0, dt);
        project(u, v, u0, v0);
    }
}
  • Unity脚本,挂载为组件,用于获取MAC Grid所需的数据,以及每帧调用MAC Grid算法
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MeshWater : MonoBehaviour {
    MAC_Grid mac_grid;

    /* MAC_Grid variables */
    //int N;
    public int N = 62;
    public float diff = 0.0001f, visc = 0.0f;
    public float force = 5.0f, source = 100.0f;
    public float dt = 0.2f;
    static float[] u;
    static float[] v;
    static float[] u_prev;
    static float[] v_prev;
    public static float[] dens;
    static float[] dens_prev;
	// Use this for initialization
	void Start () {
        allocate_data();
        
        mac_grid = new MAC_Grid(N);
    }
	
	// Update is called once per frame
	void Update () {
        clearSource();
        mac_grid.vel_step(u, v, u_prev, v_prev, visc, dt);
        mac_grid.dens_step(dens, dens_prev, u, v, diff, dt);
        Debug.Log(dens[10]);
	}

    void free_data()
    {
        u = null;
        v = null;
        u_prev = null;
        v_prev = null;
        dens = null;
        dens_prev = null;        
    }

    void clear_data()
    {
        System.Array.Clear(u, 0, u.Length);
        System.Array.Clear(v, 0, v.Length);
        System.Array.Clear(u_prev, 0, u_prev.Length);
        System.Array.Clear(v_prev, 0, v_prev.Length);
        System.Array.Clear(dens, 0, dens.Length);
        System.Array.Clear(dens_prev, 0, dens_prev.Length);
    }

    void allocate_data()
    {
        int size = (N + 2) * (N + 2);

        u = new float[size];
        v = new float[size];
        u_prev = new float[size];
        v_prev = new float[size];
        dens = new float[size];
        dens_prev = new float[size];
    }

    public void add_source(Vector3 pos,float value)
    {
        float offsetX = pos.x - (transform.position.x - 5.0f);
        float offsetZ = pos.z - (transform.position.z - 5.0f);
        int GridOffsetX = (int)(offsetX / 10 * 64);
        int GridOffsetZ = (int)(offsetZ / 10 * 64);
        dens[mac_grid.IX(GridOffsetX, GridOffsetZ)] = value;
    }

    public void add_velocity(Vector3 pos,Vector3 direction)
    {
        float offsetX = pos.x - (transform.position.x - 5.0f);
        float offsetZ = pos.z - (transform.position.z - 5.0f);
        int GridOffsetX = (int)(offsetX / 10 * 64);
        int GridOffsetZ = (int)(offsetZ / 10 * 64);
        u[mac_grid.IX(GridOffsetX, GridOffsetZ)] = force * direction.normalized.x;
        v[mac_grid.IX(GridOffsetX, GridOffsetZ)] = force * direction.normalized.y;
    }

    public int getGridCoord(int i,int j)
    {
        return mac_grid.IX(i, j);
    }

    void clearSource()
    {
        int i, j, size = (N + 2) * (N + 2);

        for (i = 0; i < size; i++)
        {
            u_prev[i] = v_prev[i] = dens_prev[i] = 0.0f;
        }
    }
}
  • 更改水体shader的脚本,新建一张texture2D,将从MAC Grid算法读取到的密度场的值用setPixal方法赋值给texture2D的像素值中,这张2D贴图最终会传递给水体shader,并叠加在最终的效果上。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MeshShaderTest : MonoBehaviour {
    Texture2D waterColor;
    Material material;
    public Vector4 waterColorOffset;
    MeshWater meshWater;

    Vector3 mousePos, oldMousePos;
	// Use this for initialization
	void Start () {
        waterColor = new Texture2D(256, 256);

        material = gameObject.GetComponent<MeshRenderer>().material;
        material.SetTexture("_WaterColorMap", waterColor);
        meshWater = gameObject.GetComponent<MeshWater>();

        oldMousePos = Vector3.zero;
    }
	// Update is called once per frame
	void Update () {
        RaycastHit hitInfo;
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

        if (Physics.Raycast(ray, out hitInfo, 100))
        {
            mousePos = hitInfo.point;
            if(oldMousePos == Vector3.zero)
            {
                oldMousePos = hitInfo.point;
            }
            //当射线碰撞到plane并且鼠标左键按下时
            if (hitInfo.transform.name == "Plane" && Input.GetMouseButton(0))
            {
                float offsetX = hitInfo.point.x - (transform.position.x - 5.0f);
                float offsetZ = hitInfo.point.z - (transform.position.z - 5.0f);
                int TexOffsetX = (int)(offsetX / 10 * 256);
                int TexOffsetZ = (int)(offsetZ / 10 * 256);
                //waterColor.SetPixel(TexOffsetX, TexOffsetZ, new Color(1, 0, 0));
               // waterColor.Apply();
                meshWater.add_source(hitInfo.point, 127);
            }

            if (hitInfo.transform.name == "Plane" && Input.GetMouseButton(1))
            {
                Vector3 direction = mousePos - oldMousePos;
                meshWater.add_velocity(hitInfo.point, direction);
                oldMousePos = mousePos;
            }

            if(Input.GetMouseButtonUp(1))
            {
                oldMousePos = Vector3.zero;
            }
        }

        for (int i = 0; i < waterColor.width; i++)
        {
            for (int j = 0; j < waterColor.height; j++)
            {
                float color = MeshWater.dens[meshWater.getGridCoord(i / 4, j / 4)];
                waterColor.SetPixel(i, j, new Color(color, 0, 0));
            }
        }
        waterColor.Apply();
    }
}
  • 修改水体shader,使用standard assets中的shader FxWaterPro,建议新建shader并把原来的shader代码复制过来再做修改。修改部分主要是获取染料贴图,将贴图叠加在原本水体的效果上面。注意获取贴图uv时,要用scale以及offset设置一下,保证贴图uv正确。具体代码如下(修改部分已添加中文注释):
Shader "FX/Water" {
Properties {
	_WaveScale ("Wave scale", Range (0.02,0.15)) = 0.063
	_ReflDistort ("Reflection distort", Range (0,1.5)) = 0.44
	_RefrDistort ("Refraction distort", Range (0,1.5)) = 0.40
	_RefrColor ("Refraction color", COLOR)  = ( .34, .85, .92, 1)
	[NoScaleOffset] _Fresnel ("Fresnel (A) ", 2D) = "gray" {}
	[NoScaleOffset] _BumpMap ("Normalmap ", 2D) = "bump" {}
	WaveSpeed ("Wave speed (map1 x,y; map2 x,y)", Vector) = (19,9,-16,-7)
	[NoScaleOffset] _ReflectiveColor ("Reflective color (RGB) fresnel (A) ", 2D) = "" {}
	_HorizonColor ("Simple water horizon color", COLOR)  = ( .172, .463, .435, 1)
	[HideInInspector] _ReflectionTex ("Internal Reflection", 2D) = "" {}
	[HideInInspector] _RefractionTex ("Internal Refraction", 2D) = "" {}
    //添加贴图、缩放、位移变量
	_WaterColorMap ("Water Color Map", 2D) = ""{}
	_WaterColorMapScale("Scale",FLOAT) = 0.1
	_WaterColorMapOffset("Offset",Vector) = (0.5,0.5,0.0,0.0)
}


// -----------------------------------------------------------
// Fragment program cards


Subshader {
	Tags { "WaterMode"="Refractive" "RenderType"="Opaque" }
	Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fog
#pragma multi_compile WATER_REFRACTIVE WATER_REFLECTIVE WATER_SIMPLE

#if defined (WATER_REFLECTIVE) || defined (WATER_REFRACTIVE)
#define HAS_REFLECTION 1
#endif
#if defined (WATER_REFRACTIVE)
#define HAS_REFRACTION 1
#endif


#include "UnityCG.cginc"

uniform float4 _WaveScale4;
uniform float4 _WaveOffset;

#if HAS_REFLECTION
uniform float _ReflDistort;
#endif
#if HAS_REFRACTION
uniform float _RefrDistort;
#endif
//上述变量在此声明
sampler2D _WaterColorMap;
float _WaterColorMapScale;
float4 _WaterColorMapOffset;
struct appdata {
	float4 vertex : POSITION;
	float3 normal : NORMAL;
};

struct v2f {
	float4 pos : SV_POSITION;
	#if defined(HAS_REFLECTION) || defined(HAS_REFRACTION)
		float4 ref : TEXCOORD0;
		float2 bumpuv0 : TEXCOORD1;
		float2 bumpuv1 : TEXCOORD2;
		float3 viewDir : TEXCOORD3;
	#else
		float2 bumpuv0 : TEXCOORD0;
		float2 bumpuv1 : TEXCOORD1;
		float3 viewDir : TEXCOORD2;
	#endif
        //更改v2f结构体,添加水体颜色的uv
		float2 coloruv : TEXCOORD4;
	UNITY_FOG_COORDS(4)
};

v2f vert(appdata v)
{
	v2f o;
	o.pos = UnityObjectToClipPos(v.vertex);
	

	// scroll bump waves
	float4 temp;
	float4 wpos = mul (unity_ObjectToWorld, v.vertex);
	temp.xyzw = wpos.xzxz * _WaveScale4 + _WaveOffset;
	o.bumpuv0 = temp.xy;
	o.bumpuv1 = temp.wz;
	
	// object space view direction (will normalize per pixel)
	o.viewDir.xzy = WorldSpaceViewDir(v.vertex);
	
	#if defined(HAS_REFLECTION) || defined(HAS_REFRACTION)
	o.ref = ComputeNonStereoScreenPos(o.pos);
	#endif
    //从顶点数据中拿值,并且应用缩放和位移
	o.coloruv = v.vertex.xz * _WaterColorMapScale + _WaterColorMapOffset;
	UNITY_TRANSFER_FOG(o,o.pos);
	return o;
}

#if defined (WATER_REFLECTIVE) || defined (WATER_REFRACTIVE)
sampler2D _ReflectionTex;
#endif
#if defined (WATER_REFLECTIVE) || defined (WATER_SIMPLE)
sampler2D _ReflectiveColor;
#endif
#if defined (WATER_REFRACTIVE)
sampler2D _Fresnel;
sampler2D _RefractionTex;
uniform float4 _RefrColor;
#endif
#if defined (WATER_SIMPLE)
uniform float4 _HorizonColor;
#endif
sampler2D _BumpMap;

half4 frag( v2f i ) : SV_Target
{
	i.viewDir = normalize(i.viewDir);
	
	// combine two scrolling bumpmaps into one
	half3 bump1 = UnpackNormal(tex2D( _BumpMap, i.bumpuv0 )).rgb;
	half3 bump2 = UnpackNormal(tex2D( _BumpMap, i.bumpuv1 )).rgb;
	half3 bump = (bump1 + bump2) * 0.5;
	
	// fresnel factor
	half fresnelFac = dot( i.viewDir, bump );
	
	// perturb reflection/refraction UVs by bumpmap, and lookup colors
	
	#if HAS_REFLECTION
	float4 uv1 = i.ref; uv1.xy += bump * _ReflDistort;
	half4 refl = tex2Dproj( _ReflectionTex, UNITY_PROJ_COORD(uv1) );
	#endif
	#if HAS_REFRACTION
	float4 uv2 = i.ref; uv2.xy -= bump * _RefrDistort;
	half4 refr = tex2Dproj( _RefractionTex, UNITY_PROJ_COORD(uv2) ) * _RefrColor;
	#endif
	
	// final color is between refracted and reflected based on fresnel
	half4 color;
	
	#if defined(WATER_REFRACTIVE)
	half fresnel = UNITY_SAMPLE_1CHANNEL( _Fresnel, float2(fresnelFac,fresnelFac) );
	color = lerp( refr, refl, fresnel );
	#endif
	
	#if defined(WATER_REFLECTIVE)
	half4 water = tex2D( _ReflectiveColor, float2(fresnelFac,fresnelFac) );
	color.a = refl.a * water.a;
	#endif
	
	#if defined(WATER_SIMPLE)
	half4 water = tex2D( _ReflectiveColor, float2(fresnelFac,fresnelFac) );
	color.rgb = lerp( water.rgb, _HorizonColor.rgb, water.a );
	color.a = _HorizonColor.a;
	#endif
    //最后将贴图的颜色直接加到计算完水体效果的color上,注意要乘一个因子否则水体会太亮
	half4 waterColor = tex2D(_WaterColorMap, i.coloruv) * 0.2;
	color.rgb = color.rgb + waterColor;
	UNITY_APPLY_FOG(i.fogCoord, color);
	return color;
}
ENDCG

	}
}

}

开始制作

  • 新建一个Plane并按照standard assets里水体的预制体制作一块水体,这里是保证网格是矩形以便正确应用MAC Grid算法的结果。
  • 将水体材质的shader换成我们自己的shader
  • 运行看效果吧!

附件

unity工程包

MAC Grid算法源代码

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值