Unity演示Leetcode开香槟过程
创始人
2024-04-14 01:15:40
0

文章目录

  • Unity演示Leetcode开香槟过程
    • 示意图一:
    • 示意图二(速度变为上图的0.5倍)
    • 主要步骤与难点
    • 香槟也液面变化的实现
    • 匀速添加液体与液面高度的涉及到的一元三次方程球根
    • 香槟塔液面高度关联算法
    • C#脚本代码:
    • 杯子边缘液体流出的效果的Shader代码:

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

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

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().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代码:

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}}
}

相关内容

热门资讯

AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
AWR报告解读 WORKLOAD REPOSITORY PDB report (PDB snapshots) AW...
AWS管理控制台菜单和权限 要在AWS管理控制台中创建菜单和权限,您可以使用AWS Identity and Access Ma...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
群晖外网访问终极解决方法:IP... 写在前面的话 受够了群晖的quickconnet的小水管了,急需一个新的解决方法&#x...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...
Azure构建流程(Power... 这可能是由于配置错误导致的问题。请检查构建流程任务中的“发布构建制品”步骤,确保正确配置了“Arti...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...