• Unity演示Leetcode开香槟过程


    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108

    匀速添加液体与液面高度的涉及到的一元三次方程球根

    假设倒香槟是匀速的,比如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的值
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    香槟塔液面高度关联算法

    这里用到的算法如下所示,主要思路就是从上往下计算对应的值,真正的源头是总共的香槟体积。

    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;
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

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

    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;
            }
        }
        
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235

    杯子边缘液体流出的效果的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
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68

    Github工程连接

  • 相关阅读:
    在Sora引爆视频生成时,Meta开始用Agent自动剪视频了
    面向对象编程(高级部分)——单例模式
    Java版企业电子招标采购系统源码Spring Cloud + Spring Boot +二次开发+ MybatisPlus + Redis
    git使用技巧
    探索Django路由规则(路由匹配、路由命名空间、HTML中的跳转与Django集成、路由传参以及后端重定向)
    CSS知识补充(2022)
    C++基础知识:冒泡排序(利用C++实现冒泡排序)
    PyTorch中的CUDA操作
    12. 【containerd】snapshot服务解析
    电脑换cpu要重装系统吗
  • 原文地址:https://blog.csdn.net/qq_41841073/article/details/128071602