在做Leetcode的799. 香槟塔题目时,发现这个题目很有趣,于是就准备动手在Unity下面实现其动态过程,目的是为了更清晰的看清其动态过程,首先来一个GIF看下效果:
在做这个的过程中,主要步骤有以下几点:
算法的步骤,首先需要画出一个半圆的杯子侧壁,然后在画香槟液体的容积,利用一张正方形的image对象,然后根据其UV坐标,设定对应的像素值,这里需要透明度,所以Shaderlab 的Tag 中,RenderType 设置为 Transparent, 然后在Pass中启用透明度混合,也就是Blend SrcAlpha OneMinusSrcAlpha;第三个要素就是使用SDF画出香槟侧壁和香槟液体的液面具体代码如下所示, 有关SDF更加详细的介绍可以看这里:
Shader "Unlit/s_galss"
{Properties{_MainTex ("Texture", 2D) = "white" {}_WaterColor("waterColor", COLOR) = (1,1,1,1)_EdgeColor("edgeColor", COLOR) = (0,0,0,1)_BackGroundColor("background_color", COLOR) = (0,0,0,1)_WaterHight("waterHight", Range(0, 1.0) ) = 0.5_GlassThickness("glassThickness", Range(0, 0.2) ) = 0.05}SubShader{Tags { "RenderType"="Transparent" }LOD 100Pass{Blend SrcAlpha OneMinusSrcAlphaCGPROGRAM#pragma vertex vert#pragma fragment frag// make fog work#pragma multi_compile_fog#include "UnityCG.cginc"struct appdata{float4 vertex : POSITION;float2 uv : TEXCOORD0;};struct v2f{float2 uv : TEXCOORD0;UNITY_FOG_COORDS(1)float4 vertex : SV_POSITION;};sampler2D _MainTex;float4 _MainTex_ST;float4 _WaterColor;float4 _EdgeColor;float4 _BackGroundColor;float _WaterHight;float _GlassThickness;v2f vert (appdata v){v2f o;o.vertex = UnityObjectToClipPos(v.vertex);o.uv = TRANSFORM_TEX(v.uv, _MainTex);UNITY_TRANSFER_FOG(o,o.vertex);return o;}float sdfCircle(float2 tex, float2 center, float radius){return -length(float2(tex - center)) + radius;}float sdfWater(float2 tex, float2 center, float radius, float h){float dis0 = length(float2(tex - center));float dis1 = center.y - h;float2 p1 = tex - center;float dis2 = dot(p1, float2(0,-1));float rate = step(dis0, radius);return step(dis1, dis2 ) * rate;}fixed4 frag (v2f i) : SV_Target{// sample the texturefloat radius = 0.5;float edge_width = _GlassThickness;float4 BgColor = _BackGroundColor;float1x2 center = (0.5, 0.5);fixed4 col = tex2D(_MainTex, i.uv);float2 uv = i.uv;float d = sdfCircle(uv, center, radius);float anti = fwidth(d);col = lerp(BgColor, _EdgeColor, smoothstep(-anti, anti, d ));col.a = lerp(0, col.a, smoothstep(-anti, anti, d ));float d1 = sdfCircle(uv, center, radius - edge_width);float anti1 = fwidth(d1);float edge_alpha = smoothstep(-anti1, anti1, d1);col = lerp(col, BgColor, edge_alpha);//col.a = lerp(col.a, 0, edge_alpha);// water 颜色float d_water = sdfWater(uv, center, radius - edge_width, _WaterHight * (radius - edge_width) + edge_width);col = lerp(col, _WaterColor, d_water);col = lerp(col, BgColor, 1.0 - step(uv.y, 0.5)); // 不显示半圆之上的部分float a = lerp(col.a, 0, 1.0 - step(uv.y, 0.5));col.a = a;return col;}ENDCG}}
}
假设倒香槟是匀速的,比如1秒1000ml,然后随着时间增加,总的体积就是 V = speed * T,此时我们需要计算出香槟杯子地面水平线到液面的高度h,如下图所示:
而香槟的容积可以看做是一个截球体,然后半径为R,则其体积公式为: V=Pi∗h2×(R−h3)V = Pi*h^2\times(R - \frac{h}{3})V=Pi∗h2×(R−3h)
那么时间t和高度h的数学表达式就可以表示为: speed×t=Pi∗h2×(R−h3)speed\times t = Pi*h^2\times(R - \frac{h}{3})speed×t=Pi∗h2×(R−3h)
这个等式不能简单的表示为h=f(t)h = f(t)h=f(t)的形式,在这种情况中,我们在给定一个体积,也就是一个时间t之后,需要计算出高度h,所以就得想办法去计算,网上有对应的公式,但是也有点复杂,所以就想用简单一点的方法去做,二分法,这里情况比较特殊,首先我们知道h的取值范围是(0, R),所以我们可以采用二分的方式,在可取值的范围之内找出一个在一定误差范围之内的值,具体的算法函数如下所示:
public float GetHeightByVolumeAsync(float volume) // 输入一个体积值{int num = 20;float start = 0;float end = radius;float R = radius;float cur_h = 0;float mid = radius / 2;float res = Mathf.PI * mid * mid * (R - mid / 3.0f);while (Mathf.Abs(volume - res) > 100.0f && start < end ){mid = (start + end) / 2.0f;if (volume < res){end = mid;}else{start = mid;}cur_h = (start + end) / 2.0f;res = Mathf.PI * cur_h * cur_h * (R - cur_h / 3.0f);}return cur_h / radius; // 最后做归一化处理,返回一个0-1的值}
这里用到的算法如下所示,主要思路就是从上往下计算对应的值,真正的源头是总共的香槟体积。
List curList = new List() { totalVolume }; // totalVolume 是总共的香槟体积,随着时间递增for (int i =1; i<=rowNum; ++i){List nextList = new List();for(int k=0; knextList.Add(0);}for(int j = 0; j < i; ++j){float num = curList[j];float _addVal = Mathf.Max(0, num - maxUnitVolume) / 2.0f;if(glassDic.ContainsKey(i)){nextList[j] += _addVal;glassDic[i][j].Volume = nextList[j];glassDic[i][j].Refresh(Time.deltaTime);nextList[j + 1] += _addVal;glassDic[i][j + 1].Volume = nextList[j + 1];glassDic[i][j + 1].Refresh(Time.deltaTime);}}curList = nextList;}
演示所有的剩余详细脚本如下所示:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System.Threading.Tasks;public class GObject
{public GameObject glass;public GameObject leftFlowWater;public GameObject RightFlowWater;public float height;float radius;float total_time;float maxVolume;private float volume;public GObject(GameObject obj, float _height) { glass = obj;leftFlowWater = obj.transform.Find("leftflowWater").gameObject;RightFlowWater = obj.transform.Find("rightflowWater").gameObject;leftFlowWater.SetActive(false);RightFlowWater.SetActive(false);Material mat_left = new Material(leftFlowWater.GetComponent().material);leftFlowWater.GetComponent().material = mat_left;Material mat_right = new Material(RightFlowWater.GetComponent().material);RightFlowWater.GetComponent().material = mat_right;height = _height; radius = _height / 2.0f;maxVolume = 2 / 3.0f * Mathf.PI * radius * radius * radius;Volume = 0.0f;total_time = 0.0f;SetVolume(Volume);}public float Volume{get { return volume; }set { volume = value; }}public void SetParent(GameObject parent_obj){glass.transform.SetParent(parent_obj.transform);}public void SetHeight(float h){Material mat = glass.GetComponent().material; // 获取材质mat.SetFloat("_WaterHight", h); // 设置 Shader 中某 Color 变量的值}public void SetPosition(float posX, float PosY){glass.GetComponent().anchoredPosition = new Vector3(posX, PosY, 0f);}public void SetSize(float width, float height){glass.GetComponent().sizeDelta = new Vector2(width, height);}public void SetVolume(float val){volume = val;if(val > maxVolume){leftFlowWater.SetActive(true);RightFlowWater.SetActive(true);leftFlowWater.GetComponent().material.SetFloat("_StartVal", total_time);RightFlowWater.GetComponent().material.SetFloat("_StartVal", total_time);Debug.Log($"total_time = {total_time}");SetHeight(1.0f);}else if(val < 0.01f){SetHeight(0.0f);}else{var _h = GetHeightByVolumeAsync(val);SetHeight(_h);}}public float GetHeightByVolumeAsync(float volume){float start = 0;float end = radius;float R = radius;float cur_h = 0;float mid = radius / 2;float res = Mathf.PI * mid * mid * (R - mid / 3.0f);while (Mathf.Abs(volume - res) > 100.0f && start < end ){mid = (start + end) / 2.0f;if (volume < res){end = mid;}else{start = mid;}cur_h = (start + end) / 2.0f;res = Mathf.PI * cur_h * cur_h * (R - cur_h / 3.0f);}return cur_h / radius;}public void Refresh(float dt){if(Volume > maxVolume){total_time += dt;}SetVolume(Volume);}}public class Champagne : MonoBehaviour
{// Start is called before the first frame updateGameObject glass;Vector2 startPos;Dictionary> glassDic;int rowNum;int width;int height;int extraHeight;float totalVolume;float maxUnitVolume;public float speed;float total_time;private void Awake(){startPos = new Vector2(0, -50);rowNum = 5;width = height = 100;extraHeight = 30;glassDic = new Dictionary>();speed = 500000;totalVolume = 0.0f;float radius = height / 2;maxUnitVolume = 2 / 3.0f * Mathf.PI * radius * radius * radius;total_time = 0.0f;}void Start(){InitChampagneGlass();}void InitChampagneGlass(){for(int i =0; iglass = Resources.Load("prefab/Image1");glass = Instantiate(glass);Material mat = new Material(glass.GetComponent().material);glass.GetComponent().material = mat;float posY = startPos.y - (height / 2.0f + extraHeight) * i;float posX = startPos.x - i * (3 / 4.0f * width) + j * (3 / 2.0f * width);GObject gObject = new GObject(glass, height);gObject.SetParent(gameObject);gObject.SetPosition(posX, posY);gObject.SetSize(width, height);if (! glassDic.ContainsKey(i)){var cur_dic = new Dictionary() { { j, gObject } };glassDic[i] = cur_dic;}else{var cur_dic = glassDic[i];cur_dic.Add(j, gObject);}}}// Update is called once per framevoid Update(){float addVal = Time.deltaTime * speed;totalVolume += addVal;glassDic[0][0].Volume = totalVolume;glassDic[0][0].Refresh(Time.deltaTime);List curList = new List() { totalVolume };for (int i =1; i<=rowNum; ++i){List nextList = new List();for(int k=0; knextList.Add(0);}for(int j = 0; j < i; ++j){float num = curList[j];float _addVal = Mathf.Max(0, num - maxUnitVolume) / 2.0f;if(glassDic.ContainsKey(i)){nextList[j] += _addVal;glassDic[i][j].Volume = nextList[j];glassDic[i][j].Refresh(Time.deltaTime);nextList[j + 1] += _addVal;glassDic[i][j + 1].Volume = nextList[j + 1];glassDic[i][j + 1].Refresh(Time.deltaTime);}}curList = nextList;}}}
Shader "Unlit/s_flow_water"
{Properties{_MainTex ("Texture", 2D) = "white" {}_WaterColor("waterColor", COLOR) = (1,0,0,1)_StartVal ("startValue", Range(0, 1.0)) = 0.0}SubShader{Tags { "RenderType"="Transparent" }LOD 100Pass{Blend SrcAlpha OneMinusSrcAlphaCGPROGRAM#pragma vertex vert#pragma fragment frag// make fog work#pragma multi_compile_fog#include "UnityCG.cginc"struct appdata{float4 vertex : POSITION;float2 uv : TEXCOORD0;};struct v2f{float2 uv : TEXCOORD0;UNITY_FOG_COORDS(1)float4 vertex : SV_POSITION;};sampler2D _MainTex;float4 _MainTex_ST;float4 _WaterColor;float _StartVal;float curVal = 0.0f;v2f vert (appdata v){v2f o;o.vertex = UnityObjectToClipPos(v.vertex);o.uv = TRANSFORM_TEX(v.uv, _MainTex);UNITY_TRANSFER_FOG(o,o.vertex);return o;}fixed4 frag (v2f i) : SV_Target{// sample the texturefixed4 col = _WaterColor;// apply fogfloat2 uv = i.uv;float a = lerp(0, 1, step( 1.0f, uv.y + _StartVal / 0.3f));col.a = a;return col;}ENDCG}}
}