文章目录
- Unity演示Leetcode开香槟过程
- 示意图一:
- 示意图二(速度变为上图的5倍)
- 主要步骤与难点
- C#脚本代码:
- 香槟杯子液体页面变化以及杯子边缘的绘画Shader代码
- 杯子边缘液体流出的效果的Shader代码:
Unity演示Leetcode开香槟过程
在做Leetcode的799. 香槟塔题目时,发现这个题目很有趣,于是就想自己动手在Unity下面实现其动态过程,使其过程一幕了然,首先来一个GIF看下效果:
示意图一:
示意图二(速度变为上图的5倍)
主要步骤与难点
在做这个的过程中,主要步骤有以下几点:
- 香槟杯子的动态图变化需要用Shader实现
- 由于设计到求体积(严谨一点,和实际相贴近),为了计算简单,所以把杯子当作球面,然后根据容积不变,求出给定一个时间t,我们需要求出杯子底部到液体液面的距离h,这里会涉及到一元三次方程的求根问题,然后我的解决方式是用二分的方式,求一个比较接近的近似解。
- 香槟塔的对应关系,也就是leetcode题目中,也就是当倾倒了非负整数杯香槟后,返回第 i 行 j 个玻璃杯所盛放的香槟占玻璃杯容积的比例,然后用官方题解的算法即可。
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)
{
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 )
{
num--;
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_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
}
}
}
杯子边缘液体流出的效果的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
}
}
}
今天有点晚了,后续介绍算法介绍…