max unity 方向_Unity中实现瓶中液体晃动的效果(从建模开始)上

本文介绍了在Unity中实现瓶中液体晃动效果的建模和Shader实现过程。首先在3ds Max中创建并优化瓶子模型,然后导出为FBX。接着,使用Shader实现双Pass渲染,控制液体的液面高度,通过Box Collider计算液面适应瓶子的形状。最后,通过C#脚本获取Collider顶点,实现液体体积的动态变化。
摘要由CSDN通过智能技术生成

11a04d24ff2a62445ba4e89313c8bd63.png

动态效果如下:

6a07610f2539c5ced84f8983f84a883c.gif

之前写过一个模拟血瓶效果的文章。

Lucifer:在Unity中完善一下Unreal模拟伪液体血瓶的效果​zhuanlan.zhihu.com
92903d7b041da894d391bf55b75bc3a1.png

那篇主要是为了模拟正确的液面,好进行比较正确的反射和投影等效果。这次的液体,从基本原理上来说,实现思路应该差不多。不过这次,因为瓶子已经不是规则形状了,所以,继续计算正确的液面应该就不现实了。这次还要配合瓶子的运动状态,液体的形态也要做出相应的正确改变。

先从实现思路上捋一下基本实现步骤:

1、内部液体的基本呈现:液体是要配合瓶子形状的,所以基本的实现思路就是将瓶子双Pass渲染,先将瓶子进行必要的顶点缩放,渲染内部液体,然后再渲染瓶子本身。

2、瓶内液面的位置设定:液体因为受重力影响,所以液面也总是会垂直于重力方向。所以对液体进行裁剪时,需要按照世界空间的高度进行裁剪。

所以其他先不考虑的情况下,我们可以先使用Shader,将上面两步实现出来。

一、建模,打开3dsMax软件(༼ つ ◕_◕ ༽つ程序员来学Max建模了)。

c3ec1f2cdf69272ba71c9c70896b2f08.png

1、alt-W,将透视图(默认软件最右下角视图)最大化。

1f23e5de1b630500460d08017b8caae9.png

2、基本视图快捷键操作和基本建模操作。

视图:z,最大化视图。

p,f,t,l,透,前,顶,左视图。

鼠标中键,左右平移视图。

alt+鼠标中键,以选中物体为重心旋转视图。

鼠标滚轮:缩放视图。

操作:左键点选物体。

q、w、e、r,选取,移动,旋转,缩放物体。

鼠标右键:悬浮菜单。

3、创建一个简单的柱体。

依次在右侧主工具栏选择创建、模型、标准原型、柱体。

9e6258126958e319c1ef0a5f37dd5b3a.png

鼠标变为下图符号后:

51615a055674a2f3c1a1f7655183d5b8.png

在主视图左键按住,并拖动鼠标,创建圆片。然后松手,继续拖动鼠标拉出高度,等高度合适后,再次点击左键,创建出圆柱体。点击右键,取消创建命令。

452b60a60b5140870a6d4de6c290a9f3.png

4、调节圆柱体参数。

左键点击选中柱体,按F4,显示模型网格。移动模型到0,0,0位置。并在右侧修改菜单中,设置柱体的分段参数。如图:

8a998a534b1eef3040ad365a9034ad33.png

调节完毕之后,右键将模型转为可编辑多边形。

cbfca9de4d06c19ac4dfba031d5bdffb.png

5、造瓶体

建模方式有很多,我按照我习惯来说:

851db474510ff3727aa0eff962b88ee8.png

选择模型顶上的面,delete,删除。

4887a550d4ba2e5714c98e5ea740e59a.png
顶面删除

选择体,并选择模型,进行一下缩放到符合一般酒瓶子的宽高比例。

4ace00bd5ed7a3f8c0a386219d2a4ba0.png
选择整个体

然后选择上面的封闭线,按住shift,左键向上拉出并将拉出的边做一下缩放。

4c81a184e08b96b56ffbd62b33a5ef5a.png
按住shift拉出

da1e61594c484a109c1057d7ca2b5662.png
缩放一下,来制作酒瓶上方较窄的形状

重复以上步骤,直到拉出以下形体。

5b02bc55c6cb93e07af1788ec7bec426.png
中途如果需要调整,可以切换到顶点模式,按F3,仅显示网格,并选中需要调整的顶点进行调整

6、优化瓶口

不讲究的话,上面的瓶子就能导出成Fbx使用了,如果想做好点,就继续优化一下瓶口,同样使用上面的方法,调整成下面的形状。

8b5343fc0646cf6ab66ab15ea51a7352.png
就是不断的按住shift网上拉面,调整距离和缩放

7、导出Fbx

养成良好习惯,导出前,重置 一下模型的矩阵信息。

9f99cd8face3703673d68ca7136c462a.png

选择菜单、导出,并选择Fbx格式,导出模型即可。

f0b3d1b80f50abc7ef22a889bab1d2e0.png

Fbx导出设置这里请参考下图:

9f74b49064ae070a1ecaa8c0f921793b.png
这样即可,其他设置在这里并不是重点。

下面提供一下测试用模型(我知道你们第一次建模,结果肯定惨不忍睹,就一个字:丑(●'◡'●)):

c3db4e11b03fe57eb33f549c642648ac.png
Bottle.FBX
37.6K
·
百度网盘

二、Shader

按上面的思路,就是液体,瓶子渲染两遍就好了,没啥特别的。

Shader "Unlit/BottleLiquid_Test"
{
	Properties
	{
		_LiquidColor ("LiquidColor", Color) = (0.2,0.1,0.1,0.9)
		_BottleColor("BottleColor", Color) = (0.55,0.95,0.45,0.5)
	}
	SubShader
	{
		Tags { "RenderType"="Transparent" "Queue" = "Transparent"}
		LOD 100

		Pass//第一个pass先渲染瓶中的液体
		{
			Blend SrcAlpha OneMinusSrcAlpha
                        //这里要双面渲染液体
			Cull off
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag			
			#include "UnityCG.cginc"

			struct appdata
			{
				float4 vertex : POSITION;
			};

			struct v2f
			{
				float4 vertex : SV_POSITION;	
			};

			float4 _LiquidColor;
			
			v2f vert (appdata v)
			{
				v2f o;
                                //这里就是对瓶子进行缩放,比例决定瓶子的厚度,后面加了一个向上的小偏移,因为瓶底儿应该更厚点儿。
				float4 localPos = float4(0.9, 0.9, 0.98, 1) * v.vertex + float4(0, 0, 0.01, 0);
				o.vertex = UnityObjectToClipPos(localPos);
				return o;
			}
			
			fixed4 frag(v2f i) : SV_Target
			{
				return _LiquidColor;
			}
			ENDCG
		}

		Pass
		{
			Blend SrcAlpha OneMinusSrcAlpha
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "UnityCG.cginc"

			struct appdata
			{
				float4 vertex : POSITION;
			};

			struct v2f
			{
				float4 vertex : SV_POSITION;
			};

			float4 _BottleColor;

			v2f vert(appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				return o;
			}

			fixed4 frag(v2f i) : SV_Target
			{
				return _BottleColor;
			}
			ENDCG
		}
	}
}

然后我们就得到了:

23d87d67700821e48397f3ff4525da42.png

嗯,然后下一步就是控制液体的液面高度了。

猛地一想,貌似只要把高度超过一个定值的液体剪裁掉就可以了。

所以我们可以猛地先这么一写:

Shader中:

//增加属性液面高度_WaterLevel
_WaterLevel ("WaterLevel", range(0.1, 1.5)) = 1.0

struct v2f
{
    float4 vertex : SV_POSITION;
    //顶点输出中增加世界空间坐标
    float4 worldPos : TEXCOORD0;
};

float _WaterLevel;

//VS中增加关于世界坐标的计算
o.worldPos = mul(unity_ObjectToWorld, localPos);

//PS中增加液体剪裁,高于设置的高度的液体就直接裁剪掉
clip(_WaterLevel- i.worldPos.y);

嗯,调一下Level貌似有效:

aab1db5a0ba4e82f239cca69f2871d38.gif

但是,移动瓶子的时候,液体不跟着瓶子运动啊。。。。

bf5edc190449aed4076774406ee2514e.gif
十年之前,我不认识你,你不属于我 ( ̄_, ̄ )

呃,好吧,还需要考虑瓶子本身的世界坐标,然后根据高度差来决定液面高度。

我们先这么猛地改一下试试:

//改写VS中获取世界坐标的计算,用来获取坐标差
//o.worldPos = mul(unity_ObjectToWorld, localPos);
o.worldPos = mul(unity_ObjectToWorld, localPos) - mul(unity_ObjectToWorld, float4(0,0,0,1));

哎,貌似可以了哎:

b25d4f17e508058e7fbd9c3e16b7f7bd.gif

但是,一旋转就露馅了:

448373ece647ada6f4048b3c9b23e735.gif

瓶子里面的液体不能保持体积啊。本来半瓶的液体,瓶子横过来,液体就满了。要是现实中的快乐水能这样,那真是ƪ(˘⌣˘)ʃ。

c3367934af76d26703de315e8d56ae93.png

三、体积

好吧,终于还是到了这一步了,我们怎么才能让液体保持一个相对稳定的体积呢?瓶子可是不规则形状的,明显不能根据瓶子所处的状态,来硬性计算液面的相对高度啊。

4ea6b3084f423e358b509d948213f137.png

那,如果把瓶子近似成一个规则形状呢,比如box?那我们在瓶子上加一个boxcollider试试?

a2b679c9029e3aeb8d87a79459fa1b75.png

添加Box Collider组件,然后调整一下刚好套住瓶子。这里为什么我把collider往下面调了一下,低于瓶口了呢?原因你们自己考虑,哈哈。

然后我们就可以根据BoxCollider的8个顶点坐标,来求某个体积的液体的液面了:

0f325f7df2ee57f383bbf7b7eca88bfa.png

可是,这么想想,好复杂啊。能不能再简化一下啊?

a180784a14a1b2778eca051ab2934cdb.png

最终简化:

直接通过根据collider的8个顶点的最高最低点的高度插值,来确定液体的液面高度。

嗯嗯,OK,就这么搞。

创建C#脚本,用来获取box collider的顶点,并将最高最低点坐标传入shader:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(BoxCollider))]
public class BoundingBox : MonoBehaviour {

    private Material bottleLiquidMat;
    private BoxCollider bottleBox;
    //用来存储8个顶点的Local坐标
    private Vector4[] localPos = new Vector4[8];

    //x_最大值,y_最小值
    private Vector3 volum;
    static readonly int _Volum = Shader.PropertyToID("_Volum");

    //获取collider的顶点
    void initializeVolum()
    {
	volum = Vector3.zero;
	bottleBox = GetComponent<BoxCollider>();
	Vector4 centerPos = bottleBox.center;
	centerPos.w = 1.0f;
	Vector4 boxSize = bottleBox.size;
	boxSize.w = 0.0f;
	localPos[0] = centerPos + 0.5f * boxSize;
	localPos[1] = centerPos - 0.5f * boxSize;
	localPos[2] = centerPos + 0.5f * new Vector4(-boxSize.x, boxSize.y, boxSize.z, boxSize.w);
	localPos[3] = centerPos + 0.5f * new Vector4(-boxSize.x, -boxSize.y, boxSize.z, boxSize.w);
	localPos[4] = centerPos + 0.5f * new Vector4(-boxSize.x, boxSize.y, -boxSize.z, boxSize.w);
	localPos[5] = centerPos + 0.5f * new Vector4(boxSize.x, -boxSize.y, boxSize.z, boxSize.w);
	localPos[6] = centerPos + 0.5f * new Vector4(boxSize.x, -boxSize.y, -boxSize.z, boxSize.w);
	localPos[7] = centerPos + 0.5f * new Vector4(boxSize.x, boxSize.y, -boxSize.z, boxSize.w);
    }
    //计算collider的最低和最高的顶点世界坐标
    Vector3 calculateVolum()
    {
	Vector3 Volum;
	Matrix4x4 localToWorld = transform.localToWorldMatrix;
	Volum.x = -9999999; 
	Volum.y = 9999999;
	Vector4 worldPos;
        //判断一下最高最低点
	for (int i = 0; i < 8; i++)
	{
	    worldPos = localToWorld * localPos[i];
	    if (worldPos.y > Volum.x)
	        Volum.x = worldPos.y;
	    if (worldPos.y < Volum.y)
	        Volum.y = worldPos.y;
	}
	Volum.z = 0;
	return Volum;
    }
    //初始和结束都将体积计算并赋值一下。
    private void OnEnable()
    {
	bottleLiquidMat = GetComponent<Renderer>().material;
	if (bottleLiquidMat.HasProperty("_Volum"))
	{
	    initializeVolum();
	    volum = calculateVolum();
	    bottleLiquidMat.SetVector(_Volum, volum);
	}
    }
    private void OnDisable()
    {
	if (bottleLiquidMat.HasProperty("_Volum"))
	{
	    initializeVolum();
	    volum = calculateVolum();
	    bottleLiquidMat.SetVector(_Volum, volum);
	}
    }
    //每帧判断瓶子是否移动了,重新给shader传入最高最低点
    void Update () {
	if (transform.hasChanged && bottleLiquidMat.HasProperty("_Volum"))
	{
	    volum = calculateVolum();
	    bottleLiquidMat.SetVector(_Volum, volum);
	    transform.hasChanged = false;
	}		
    }
}

然后把脚本直接丢到瓶子身上。我们再在Shader中设置变量,接收一下:

//改一下level的数值范围。
_WaterLevel ("WaterLevel", range(0.2, 0.9)) = 0.5

//找个地方,声明体积变量
uniform float4 _Volum;

//前面都不需要修改,PS中增加一些代码
fixed4 frag(v2f i) : SV_Target
{
    //取到最高,最低点
    float heightMax = _Volum.x;
    float heightMin = _Volum.y;
    //直接根据设置的液面高度,对最低和最高顶点进行插值一下,求出液面实际高度
    float waterLevel = heightMin + _WaterLevel * (heightMax - heightMin);
    clip(waterLevel - i.worldPos.y);
    return _LiquidColor;
}

OK,到这里,瓶中液体的液面就能根据液体体积进行自动适应了。运行看一下,是不是像下面一样了?

1fdd06c64bbe4775153efd4348513b3f.gif
快乐水横竖都不会变多了,没有快乐了 ̄へ ̄

嗯,然后就是最后一步,加入液面的摇晃就完成了。先到这里,休息休息一下。

e6eb8fa166c67f2366a030753ed40197.png
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值