[Unity/URP]制作一个简单的水体浮力效果

简易的水体浮力制作

这一个效果参考自B站某up主的方法,实现比较简单,性能上也比较一般,思路主要是通过计算物体在水中的吃水体积,来算出对应的吃水体积的一个向上的浮力大小,但是我这里还没有加入旋转的效果,只有上下的浮动:

计算方法:阿基米德原理,忘记了可以回去看看高中课本~~

首先我们需要一个方法来得出物体的体积大小:

    //下面是一个方法的创建,这一个方法可以计算出水面上的不规则物体的体积,思路:向Mesh的包围盒里面打出多个点,最后是很多万个,之后统计留在Mesh里面的点占总的打出去的点的
    //百分之多少,就可以计算出物体的体积!!;
    //我们要将当前的gameobject设置为这一个物体,并创建一个矩阵和数组将在模型空间中进行计算,之后再转换世界坐标中并传入到数组中去;
    //同时不要忘了获取包围盒的中心;
    public float CalculateVolumn()
    {
        GameObject object1 = this.gameObject;//首先将场景中的模型设置为当前的模型;
        MeshCollider object1_mc = object1.GetComponent<MeshCollider>();//此处获取到顶点的组件;
        Vector3 object1_center = object1_mc.bounds.center;//获取到组件的顶点包围盒的中心;
        Matrix4x4 localToWorld = object1_mc.transform.localToWorldMatrix;//获取到物体将顶点转换到世界空间中的一个矩阵;
        Vector3[] vertices_object1 = object1_mc.GetComponent<MeshFilter>().mesh.vertices;//将获取到的顶点组件的顶点信息传入到数组当中;
        //这里会创建三个顶点个数长度的数组,并将顶点的xyz经过矩阵变换后移动到世界空间;
        float[] x = new float[vertices_object1.Length];
        float[] y = new float[vertices_object1.Length];
        float[] z = new float[vertices_object1.Length];
        
        //下面进行矩阵从模型空间转移到世界空间的转换;
        for (int i = 0; i < vertices_object1.Length; i++)
        {
            Vector3 world_v = localToWorld.MultiplyPoint3x4(vertices_object1[i]);//这一步将数组中的每一个顶点通过变换转移到世界空间中;
            //下面会将顶点的xyz传入到我们上面创建的三个float数组当中去;
            x[i] = world_v.x;
            y[i] = world_v.y;
            z[i] = world_v.z;
        }

        //下面要计算包围盒的长度,首先要进行大小的排序;
        Array.Sort(x);
        Array.Sort(y);
        Array.Sort(z);
        //大小的排序结束之后就可以直接进行包围盒长度的计算;
        float x_length = x[x.Length - 1] - x[0];
        float y_length = y[y.Length - 1] - y[0];
        float z_length = z[z.Length - 1] - z[0];
        
        //计算步长;
        float lerp_x = x_length / SamplerCount;
        float lerp_y = y_length / SamplerCount;
        float lerp_z = z_length / SamplerCount;
        //下面要计算打出的点中在Mesh中的点的个数;
        int pointCount = 0;
        for (int i = 0; i < SamplerCount; i++)
        {
            for (int j = 0; j < SamplerCount; j++)
            {
                for (int k = 0; k < SamplerCount; k++)
                {
                    //创建samplerDot来接受每一个打出的点;
                    Vector3 samplerDot = new Vector3(x[0] + i * lerp_x, y[0] + lerp_y * j, z[0] + lerp_z * k);
                    if (IsInCollider(object1_mc, object1_center, samplerDot))
                    {
                        pointCount++;//如果打出的点在包围盒的内部则将点数加一;
                    }
                }
            }
            
        }
        //跳出循环,下面是物体体积的计算;
        //计算模型的体积,单位为立方体;
        float volumn = (float)pointCount / (SamplerCount * SamplerCount * SamplerCount) * (x_length * y_length * z_length);
        Debug.Log("Volumn:"+volumn);//弄一个Log;
        return volumn;//最后返回体积;
    }

下面是对其他变量的设置:

    //先声明一些变量,包括在水里1阻力,旋转的阻力,空中的阻力,空中旋转的阻力,以及密度水面的高度等等;
    private int SamplerCount = 100;//这里是每一个方向会选取100个点,总共会选取100万个点;
    public float density = 1f;//物体的密度;
    public float volumn;//体积大小;
    public float underWaterDrag = 3f;//水里的阻力大小;
    public float underWaterAngularDrag = 1f;//旋转的阻力;
    public float airDrag = 0f;//空气的阻力;
    public float airAngularDrag = 0.05f;//空气中旋转的阻力;
    public float waterHeight = 0f; //水面的高度;
    
    Rigidbody m_Rigidbody;//创建一个Rigidbody对象,用来获取到场景中的物体;
    bool underWater;//用来确定是否在水下;
    MeshRenderer mesh;//创建mesh来获取到场景中物体的所有面;

下面是Start以及FixedUpdate函数,这里重点讲解以下FixedUpdate函数:

Update 和 FixedUpdate的区别:

-->Update():每帧执行,代码每秒执行N次,但是由于电脑每帧的时间不是稳定的所以每次执行的时间间隔是不同的

-->FixedUpdate()(物理帧执行):执行顺序在Update之后,每帧执行,每秒执行N次,但每一帧的时间都是固定的,即每一帧相隔的时间都是相同的

因此,如果使用Update()来写物体的物理关系的时候(例如碰撞等),物体的每帧受力不均匀就会出现一些问题(例如2D碰撞造成图片抖动),所以涉及到物理关系使用FixedUpdate()更好一些.

下面是两个函数的代码:

void Start()
    {
        m_Rigidbody = GetComponent<Rigidbody>();//获取到场景对象并传入到m_Rigidbody对象中;
        mesh = GetComponent<MeshRenderer>();//获取到物体的面,并存储到mesh对象里面;
        volumn = CalculateVolumn();//通过下面定义的CalculateVolumn方法来获取到物体的体积;
    }

    void FixedUpdate()
    {
        //每一帧的时间相同,可以使得物理受力更加的均匀,这里主要是进行浮力相关的计算;
        //计算m = p*V;mass即为m;
        m_Rigidbody.mass = volumn * Mathf.Clamp(density, 0, density);//m = pV的计算,此处将变量density限制在了0到density之间;
        var difference = transform.position.y - mesh.bounds.size.y / 2 - waterHeight;
        if (difference < 0)
        {
            //给物体一个向上的浮力;
            m_Rigidbody.AddForceAtPosition(Vector3.up * volumn * 9.81f * Mathf.Clamp(Mathf.Abs(difference),0,mesh.bounds.size.y),transform.position,ForceMode.Force);
            //判断是否在水下:
            if (!underWater)
            {
                underWater = true;
                SwitchState(true);
            }
            else if(underWater)
            {
                underWater = false;
                SwitchState(false);
            }
        }
    }

还有一些补充的函数:

 void SwitchState(bool isUnderWater)
    {
        if (isUnderWater)
        {
            //修改阻力参数为水下参数;
            m_Rigidbody.drag = underWaterDrag;
            m_Rigidbody.angularDrag = underWaterAngularDrag;
        }
        else
        {
            //如果不在水下则修改成为空气的阻力参数;
            m_Rigidbody.drag = airDrag;
            m_Rigidbody.angularDrag = airAngularDrag;
        }
    }

    //用于判断打出的点是否在包围盒的内部,如果是则返回True;
    //这里传入的是顶点的组件,包围盒的中心,以及每一个打出的顶点位置;
    bool IsInCollider(MeshCollider other, Vector3 center, Vector3 point)
    {
        //这一个函数会对场景中的所有物体进行获取并投射射线,如果透射出的射线属于other则返回false,因为如果物体打出的点是在内部则不会有射线;

        Vector3 direction = center - point;//获取射线方向;
        RaycastHit[] hits = Physics.RaycastAll(point, direction);//获取所有场景中的射线并传入到数组hits当中;
        foreach (RaycastHit hit in hits)
        {
            if (hit.collider == other)
            {
                //对于射线的数组如果判断为改other对象的射线,则返回false;
                return false;
            }
        }

        return true;//否则返回false;
    }

全部的代码如下:

using System;
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;


[RequireComponent(typeof(MeshCollider))]
[RequireComponent(typeof(Rigidbody))]
public class buoyancy : MonoBehaviour
{
    //先声明一些变量,包括在水里1阻力,旋转的阻力,空中的阻力,空中旋转的阻力,以及密度水面的高度等等;
    private int SamplerCount = 100;//这里是每一个方向会选取100个点,总共会选取100万个点;
    public float density = 1f;//物体的密度;
    public float volumn;//体积大小;
    public float underWaterDrag = 3f;//水里的阻力大小;
    public float underWaterAngularDrag = 1f;//旋转的阻力;
    public float airDrag = 0f;//空气的阻力;
    public float airAngularDrag = 0.05f;//空气中旋转的阻力;
    public float waterHeight = 0f; 
    
    Rigidbody m_Rigidbody;//创建一个Rigidbody对象,用来获取到场景中的物体;
    bool underWater;//用来确定是否在水下;
    MeshRenderer mesh;//创建mesh来获取到场景中物体的所有面;

    void Start()
    {
        m_Rigidbody = GetComponent<Rigidbody>();//获取到场景对象并传入到m_Rigidbody对象中;
        mesh = GetComponent<MeshRenderer>();//获取到物体的面,并存储到mesh对象里面;
        volumn = CalculateVolumn();//通过下面定义的CalculateVolumn方法来获取到物体的体积;
    }

    void FixedUpdate()
    {
        //每一帧里面都会进行调用,这里主要是进行浮力相关的计算;
        //计算m = p*V;mass即为m;
        m_Rigidbody.mass = volumn * Mathf.Clamp(density, 0, density);//m = pV的计算,此处将变量density限制在了0到density之间;
        var difference = transform.position.y - mesh.bounds.size.y / 2 - waterHeight;
        if (difference < 0)
        {
            //给物体一个向上的浮力;
            m_Rigidbody.AddForceAtPosition(Vector3.up * volumn * 9.81f * Mathf.Clamp(Mathf.Abs(difference),0,mesh.bounds.size.y),transform.position,ForceMode.Force);
            if (!underWater)
            {
                underWater = true;
                SwitchState(true);
            }
            else if(underWater)
            {
                underWater = false;
                SwitchState(false);
            }
        }
    }

    void SwitchState(bool isUnderWater)
    {
        if (isUnderWater)
        {
            //修改阻力参数为水下参数;
            m_Rigidbody.drag = underWaterDrag;
            m_Rigidbody.angularDrag = underWaterAngularDrag;
        }
        else
        {
            //如果不在水下则修改成为空气的阻力参数;
            m_Rigidbody.drag = airDrag;
            m_Rigidbody.angularDrag = airAngularDrag;
        }
    }

    //用于判断打出的点是否在包围盒的内部,如果是则返回True;
    //这里传入的是顶点的组件,包围盒的中心,以及每一个打出的顶点位置;
    bool IsInCollider(MeshCollider other, Vector3 center, Vector3 point)
    {
        //这一个函数会对场景中的所有物体进行获取并投射射线,如果透射出的射线属于other则放回false,因为如果物体打出的点是在内部则不会有射线;

        Vector3 direction = center - point;//获取射线方向;
        RaycastHit[] hits = Physics.RaycastAll(point, direction);//获取所有场景中的射线并传入到数组hits当中;
        foreach (RaycastHit hit in hits)
        {
            if (hit.collider == other)
            {
                //对于射线的数组如果判断为改other对象,则返回false;
                return false;
            }
        }

        return true;//否则返回false;
    }


    //下面是一个方法的创建,这一个方法可以计算出水面上的不规则物体的体积,思路:向Mesh的包围盒里面打出多个点,最后是很多万个,之后统计留在Mesh里面的点占总的打出去的点的
    //百分之多少,就可以计算出物体的体积!!;
    //我们要将当前的gameobject设置为这一个物体,并创建一个矩阵和数组将在模型空间中进行计算,之后再转换世界坐标中并传入到数组中去;
    //同时不要忘了获取包围盒的中心;
    public float CalculateVolumn()
    {
        GameObject object1 = this.gameObject;//首先将场景中的模型设置为当前的模型;
        MeshCollider object1_mc = object1.GetComponent<MeshCollider>();//此处获取到顶点的组件;
        Vector3 object1_center = object1_mc.bounds.center;//获取到组件的顶点包围盒的中心;
        Matrix4x4 localToWorld = object1_mc.transform.localToWorldMatrix;//获取到物体将顶点转换到世界空间中的一个矩阵;
        Vector3[] vertices_object1 = object1_mc.GetComponent<MeshFilter>().mesh.vertices;//将获取到的顶点组件的顶点信息传入到数组当中;
        //这里会创建三个顶点个数长度的数组,并将顶点的xyz经过矩阵变换后移动到世界空间;
        float[] x = new float[vertices_object1.Length];
        float[] y = new float[vertices_object1.Length];
        float[] z = new float[vertices_object1.Length];
        
        //下面进行矩阵从模型空间转移到世界空间的转换;
        for (int i = 0; i < vertices_object1.Length; i++)
        {
            Vector3 world_v = localToWorld.MultiplyPoint3x4(vertices_object1[i]);//这一步将数组中的每一个顶点通过变换转移到世界空间中;
            //下面会将顶点的xyz传入到我们上面创建的三个float数组当中去;
            x[i] = world_v.x;
            y[i] = world_v.y;
            z[i] = world_v.z;
        }

        //下面要计算包围盒的长度,首先要进行大小的排序;
        Array.Sort(x);
        Array.Sort(y);
        Array.Sort(z);
        //大小的排序结束之后就可以直接进行包围盒长度的计算;
        float x_length = x[x.Length - 1] - x[0];
        float y_length = y[y.Length - 1] - y[0];
        float z_length = z[z.Length - 1] - z[0];
        
        //计算步长;
        float lerp_x = x_length / SamplerCount;
        float lerp_y = y_length / SamplerCount;
        float lerp_z = z_length / SamplerCount;
        //下面要计算打出的点中在Mesh中的点的个数;

        int pointCount = 0;
        for (int i = 0; i < SamplerCount; i++)
        {
            for (int j = 0; j < SamplerCount; j++)
            {
                for (int k = 0; k < SamplerCount; k++)
                {
                    //创建samplerDot来接受每一个打出的点;
                    Vector3 samplerDot = new Vector3(x[0] + i * lerp_x, y[0] + lerp_y * j, z[0] + lerp_z * k);
                    if (IsInCollider(object1_mc, object1_center, samplerDot))
                    {
                        pointCount++;//如果打出的点在包围盒的内部则将点数加一;
                    }
                }
            }
            
        }
        //跳出循环,下面的体积的计算;
        //计算模型的体积,单位为立方体;
        float volumn = (float)pointCount / (SamplerCount * SamplerCount * SamplerCount) * (x_length * y_length * z_length);
        Debug.Log("Volumn:"+volumn);//弄一个Log;
        return volumn;//最后返回体积;
    }
}

最后在操作时为物体加上一个Rigidbody以及脚本,运行后调节waterHeight到合适的高度就可以看到浮力的效果,如图:

调节Density就可以使物体上浮和下沉.

一些其他函数的补充

Rigidbody.AddForce

Rigidbody.AddForceAtPosition

猜你喜欢

转载自blog.csdn.net/2201_75303014/article/details/129368064