Unity:圆底烧瓶中液体液面升降变化的效果

一、效果展示

请添加图片描述

二、实现的原理

1、从image的filled模式说起

image的filled模式,适合用来做进度条:
在这里插入图片描述
请添加图片描述

2、能否为一个3D object实现一个image filled 的shader ?

Shader "Custom/FilledImageEffect"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Color ("Color", Color) = (1, 1, 1, 1)
        _FillAmount ("Fill Amount", Range(0, 1)) = 1
    }

    SubShader
    {
        Tags {"Queue"="Transparent" "RenderType"="Transparent"}
        LOD 100

        CGPROGRAM
        #pragma surface surf Lambert

        sampler2D _MainTex;
        float4 _Color;
        float _FillAmount;

        struct Input
        {
            float2 uv_MainTex;
        };

        void surf (Input IN, inout SurfaceOutput o)
        {
            float2 uv = IN.uv_MainTex;
            float4 c = tex2D(_MainTex, uv) * _Color;

            if (uv.x > _FillAmount)
            {
                c.a = 0;
            }

            o.Albedo = c.rgb;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

3、液面变化的Shader

Shader "Custom/LiquidBottle"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _MainColor("MainColor",Color) = (1, 1, 1, 1)
        _TopColor("TopColor",Color) = (1, 1, 0, 1)
        _FillAmount("FillAmout",Range(-1 , 2)) = 0
        _EdgeWidth("EdgeWidth",Range(0 , 1)) = 0.2

        _BottleWidth("BottleWidth",Range(0,1)) = 0.2
        _BottleColor("BottleColor",Color) = (1,1,1,1)

        _RimColor("RimColor",Color)=(1,1,1,1)
        _RimWidth("RimWidth",float)=0.2

    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            Cull OFF
            AlphaToMask on
            //ZWrite On
            Blend SrcAlpha OneMinusSrcAlpha
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float3 normal : NORMAL;
               
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
                float fillEdge : TEXCOORD1;
                float3 viewDir : COLOR;
                float3 normal : COLOR2;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            float4 _MainColor;
            float _FillAmount;
            float4 _TopColor;
            float _EdgeWidth;

            float4 _RimColor;
            float _RimWidth;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                o.fillEdge=mul(unity_ObjectToWorld,v.vertex.xyz).y+_FillAmount;
                o.normal=v.normal;
                o.viewDir=normalize(ObjSpaceViewDir(v.vertex));
                return o;
            }

            fixed4 frag (v2f i,fixed facing : VFace) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv) * _MainColor;
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);

                float dotProduct = 1-pow(dot(i.normal, i.viewDir),_RimWidth);
                float4 rimCol=_RimColor*smoothstep(0.5,1,dotProduct);

                fixed4 edgeVal=step(i.fillEdge,0.5)-step(i.fillEdge,0.5-_EdgeWidth);
                fixed4 edgeCol=edgeVal *= _TopColor*0.9;

                fixed4 finalVal=step(i.fillEdge,0.5)-edgeVal;
                fixed4 finalCol=finalVal*col;
                finalCol+=edgeCol+rimCol;

                 fixed4 topCol=_TopColor * (edgeVal+finalVal);
                // float dotVal = 1- pow(dot(i.normal, i.viewDir),0.3);
                // return float4(dotVal,dotVal,dotVal,1);
                return facing > 0 ? finalCol : topCol;
            }
            ENDCG
        }

        Pass
        {
            //Cull Front
            Blend SrcAlpha OneMinusSrcAlpha
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float4 normal : NORMAL;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
                float3 viewDir : COLOR;
                float3 normal :NORMAL;
            };

            float4 _BottleColor;
            float _BottleWidth;

            float4 _RimColor;
            float _RimWidth;

            v2f vert (appdata v)
            {
                v2f o;
                v.vertex.xyz+=v.normal.xyz*_BottleWidth;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                UNITY_TRANSFER_FOG(o,o.vertex);
                o.normal=v.normal.xyz;
                o.viewDir=normalize(ObjSpaceViewDir(v.vertex));

                return o;
            }

            fixed4 frag (v2f i,fixed facing : VFace) : SV_Target
            {
                // sample the texture
                fixed4 col = _BottleColor;
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);

                float dotProduct = 1-pow(dot(i.normal, i.viewDir),_RimWidth);//1-pow(dot(i.normal, i.viewDir),_RimWidth/10);
                float4 rimCol=_RimColor*smoothstep(0.2,1,dotProduct);
              
                return rimCol;
            }
            ENDCG
        }
    }
}

三、服用方法

  • 1、创建一个液面效果的材质
    在这里插入图片描述

  • 2、把这个材质挂载到瓶子中的【液体】模型上

  • 3、在面板上拖动FillAmout

  • 4、在脚本中设置mat的该属性
    mat.SetFloat(“_FillAmount”, currentValue);

四、附录:C#调用代码

1、液面升降动画的异步方法

/// <summary>
/// 容器的液面变化:化学实验中,量筒中倒入液体或者被吸取液体后,液面会升降
/// 材质用shader,控制其fillAmout参数
/// </summary>
/// <param name="mat">操作的材质</param>
/// <param name="levelStart">起始液面位置</param>
/// <param name="levelEnd">结束页面位置</param>
/// <param name="duration">动画的时间</param>
/// <returns></returns>
public static async UniTask DoLiquidLevel(Material mat, float levelStart, float levelEnd, float duration)
{
    float currentValue = levelStart;
    float timeElapsed = 0f;
    while (timeElapsed < duration)
    {
        currentValue = Mathf.Lerp(levelStart, levelEnd, timeElapsed / duration);
        Debug.Log(currentValue);
        mat.SetFloat("_FillAmount", currentValue);      //变量名字确保与Shader源码中的变量一致。
        await UniTask.Yield();
        timeElapsed += Time.deltaTime;
    }
    mat.SetFloat("_FillAmount", levelEnd);              //确保最终结果准确
}

2、monobehaviour示例脚本

using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
using static txlib;//包含 DoLiquidLevel()

/// <summary>
/// 容器的液面升降控制
/// </summary>
public class ChangeLiquidLevel : MonoBehaviour
{
    /// <summary>
    /// 物体
    /// </summary>
    public GameObject obj;

    /// <summary>
    /// 起始液面
    /// </summary>
    public float levelStart;

    /// <summary>
    /// 终止液面
    /// </summary>
    public float levelEnd;

    /// <summary>
    /// 动画耗时
    /// </summary>
    public float duration;
    
    /// <summary>
    /// 材质
    /// </summary>
    private Material mat;

    [ContextMenu("测试液面")]
    async UniTask Test()
    {
        mat = obj.GetComponent<Renderer>().sharedMaterial;
        while (true)
        {
            await DoLiquidLevel(mat, levelStart, levelEnd, duration);
            await DoLiquidLevel(mat, levelEnd, levelStart, duration);
            await UniTask.Delay(TimeSpan.FromSeconds(0.8f));
        }
    }
}