Unity演示Leetcode开香槟过程

17 篇文章 2 订阅
16 篇文章 0 订阅

Unity演示Leetcode开香槟过程

在做Leetcode的799. 香槟塔题目时,发现这个题目很有趣,于是就准备动手在Unity下面实现其动态过程,目的是为了更清晰的看清其动态过程,首先来一个GIF看下效果:

示意图一:

在这里插入图片描述

示意图二(速度变为上图的0.5倍)

在这里插入图片描述

主要步骤与难点

在做这个的过程中,主要步骤有以下几点:

  1. 香槟杯子的动态图变化需要用Shader实现。
  2. 由于设计到求体积(严谨一点,和实际相贴近),为了计算简单,所以把杯子当作球面,然后根据容积不变,求出给定一个时间t,我们需要求出杯子底部到液体液面的距离h,这里会涉及到一元三次方程的求根问题,然后这里解决方式是用二分的方式,求一个比较接近的近似解。
  3. 香槟塔的对应关系,也就是leetcode题目中,也就是当倾倒了非负整数杯香槟后,返回第 i 行 j 个玻璃杯所盛放的香槟占玻璃杯容积的比例,然后用官方题解的算法即可。

香槟也液面变化的实现

算法的步骤,首先需要画出一个半圆的杯子侧壁,然后在画香槟液体的容积,利用一张正方形的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 100

        Pass
        {
            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;
            };

            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 texture
                float 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 = P i ∗ h 2 × ( R − h 3 ) V = Pi*h^2\times(R - \frac{h}{3}) V=Pih2×(R3h)
那么时间t和高度h的数学表达式就可以表示为: s p e e d × t = P i ∗ h 2 × ( R − h 3 ) speed\times t = Pi*h^2\times(R - \frac{h}{3}) speed×t=Pih2×(R3h)
这个等式不能简单的表示为 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<float> curList = new List<float>() { totalVolume }; // totalVolume 是总共的香槟体积,随着时间递增
        for (int i =1; i<=rowNum; ++i)
        {
            List<float> nextList = new List<float>();
            for(int k=0; k<i+1; ++k)
            {
                nextList.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;
        }

演示所有的剩余详细脚本如下所示:

C#脚本代码:

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<Image>().material);
        leftFlowWater.GetComponent<Image>().material = mat_left;
        Material mat_right = new Material(RightFlowWater.GetComponent<Image>().material);
        RightFlowWater.GetComponent<Image>().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<Image>().material; // 获取材质
        mat.SetFloat("_WaterHight", h); // 设置 Shader 中某 Color 变量的值
    }

    public void SetPosition(float posX, float PosY)
    {
        glass.GetComponent<RectTransform>().anchoredPosition = new Vector3(posX, PosY, 0f);
    }

    public void SetSize(float width, float height)
    {
        glass.GetComponent<RectTransform>().sizeDelta = new Vector2(width, height);
    }

    public void SetVolume(float val)
    {
        volume = val;
        
        if(val > maxVolume)
        {
            leftFlowWater.SetActive(true);
            RightFlowWater.SetActive(true);

            leftFlowWater.GetComponent<Image>().material.SetFloat("_StartVal", total_time);
            RightFlowWater.GetComponent<Image>().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 update

    GameObject glass;
    Vector2 startPos;
    Dictionary<int, Dictionary<int,GObject>> 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<int, Dictionary<int, GObject>>();
        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; i<rowNum; ++i)
            for(int j=0; j<i+1; ++j)
            {
                glass = Resources.Load<GameObject>("prefab/Image1");
                glass = Instantiate<GameObject>(glass);
                Material mat = new Material(glass.GetComponent<Image>().material);
                glass.GetComponent<Image>().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<int, GObject>() { { j, gObject } };
                    glassDic[i] = cur_dic;
                }
                else
                {
                    var cur_dic = glassDic[i];
                    cur_dic.Add(j, gObject);
                }

            }
    }

    // Update is called once per frame
    void Update()
    {
        float addVal = Time.deltaTime * speed;
        totalVolume += addVal;
        glassDic[0][0].Volume = totalVolume;
        glassDic[0][0].Refresh(Time.deltaTime);
        List<float> curList = new List<float>() { totalVolume };
        for (int i =1; i<=rowNum; ++i)
        {
            List<float> nextList = new List<float>();
            for(int k=0; k<i+1; ++k)
            {
                nextList.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代码:

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 100

        Pass
        {
            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;
            };

            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 texture
                fixed4 col = _WaterColor;
                // apply fog
                float2 uv = i.uv;
                float a = lerp(0, 1, step( 1.0f, uv.y + _StartVal / 0.3f));
                col.a = a;
                return col;
            }
            ENDCG
        }
    }
}

Github工程连接

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值