[Unity/URP] 簡単な水の浮力エフェクトを作成する

簡単な水の浮力生成

この効果は、ステーション B の特定のアップ オーナーの方法を参照しています。実装は比較的単純で、パフォーマンスは比較的平均的です。主なアイデアは、オブジェクトの喫水量を計算することによって、対応する喫水量の上向き浮力を計算することです。ただし、ここでは回転エフェクトは追加されておらず、上下に浮いているだけです。

計算方法:アルキメデスの原理 忘れたら高校の教科書を読み返してください~~

まず、オブジェクトの体積を見つけるメソッドが必要です。

    //下面是一个方法的创建,这一个方法可以计算出水面上的不规则物体的体积,思路:向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(): フレームごとに実行され、コードは 1 秒間に N 回実行されますが、コンピュータの各フレームの時間が安定していないため、各実行の時間間隔は異なります。

-->FixedUpdate() (物理フレーム実行): 実行順序は Update の後で、フレームごとに 1 秒あたり N 回実行されますが、各フレームの時間は固定されています。つまり、各フレーム間の時間は同じです。

したがって、Update()を使用してオブジェクトの物理的な関係(衝突など) を記述する場合、オブジェクトが各フレームで不均一な力を受けると問題が発生します (2D 衝突により画像のジッターが発生するなど) 物理的な関係に関しては、FixedUpdate を使用してください。() の方が良いです。

以下は 2 つの関数のコードです。

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 を適切な高さに調整して浮力効果を確認します。

密度を調整すると、オブジェクトを浮かせたり、沈めたりすることができます。

他の機能へのいくつかの追加

Rigidbody.AddForce

Rigidbody.AddForceAtPosition

おすすめ

転載: blog.csdn.net/2201_75303014/article/details/129368064