计算快速将3D物体投影到2D平面并计算表面积(用于空气动力学阻力)?

3
我正在尝试使用Unity(使用c#)模拟游泳,实际上是通过物体的运动产生阻力来推动物体穿过液体。
为此,我使用公式
F = -½ * C * d * velocity squared * A
其中C是阻力系数,d是液体密度,A是面对运动方向的物体表面积。通过将3D对象投影到垂直于速度向量的2D平面上来计算A。
这里有一张解释A的图片:https://www.real-world-physics-problems.com/images/drag_force_2.png 现在我怀疑Unity有一种内置方法来执行此类投影(因为每次场景中有相机时它都会执行该操作)。
我的问题是:
  1. 我该怎么做?搜索没有帮助我(除非你想用相机来做)。
  2. Unity中有内置函数吗?

  3. 这会消耗大量计算资源吗?我可能会同时对成千上万个对象进行操作。

我不需要非常准确的结果。我只是想让它更加逼真,所以我希望具有更大A值的物体比具有较低A值的物体更具阻力。轻微的差异并不重要。这些物体本身不会非常复杂,但某些物体根据方向可能具有非常不同的面积。例如,锥体可能会因移动方向的不同而产生相当大的变化。如果需要,我可以用简单的形状(如椭球形或矩形)来近似A。

如果这很消耗计算资源,我读过一篇论文,其中使用了一个很酷的方法来近似计算。他在物体内部创建了一个均匀分布的点网格(称为体素),有效地将物体分成相等大小的球体(其横截面表面积始终为圆形(易于计算)。然后他计算了每个球体的阻力,并将它们相加以找到总阻力(参见图像)。

Voxels calculated drag

来自于“实时基于物理的人形游泳者动画论文”,作者Jurgis Pamerneckas,2014年

链接 https://dspace.library.uu.nl/bitstream/handle/1874/298577/JP-PhysBAnimHumanSwim.pdf?sequence=2

这个方法可以成功地估算出他的阻力。但我发现一个问题,就是在物体深处的“体素”仍然会对阻力产生贡献,而只有靠近前缘的那些才应该产生贡献。

因此,我想到了一种可能性,即我可以将体素点投影到二维平面上(垂直于速度),然后找到一个边界形状或类似的东西,并以此进行近似。我怀疑投影几个点比投影整个三维对象要快。

这引发了一些更多的问题:

  1. 这似乎是一种更好的方法吗?
  2. 我该如何在Unity中创建体素?
  3. 它计算速度更快吗?
  4. 还有更好的想法吗?

我想到了一种方法是进行某种形式的光线投射,但我不知道如何做到这一点,也许可以沿速度向量平行排列一组光线并计算击中次数来近似面积?

更新

我已经通过手动输入A的值实现了基本的阻力,现在需要以某种方式近似A。即使是手动输入,它对于非常基本的“游泳者”也能出奇地有效。在下面的图像中,由于左臂更大(我给它两倍的A值),所以游泳者正确地向右旋转。 Simmers

更新2

根据@Pierre的评论,我尝试使用物体的顶点(也可以选择顶点上的几个点),将它们投影到平面上,并计算出所得多边形的总面积,来计算整体形状的A。然而,这只计算了物体的总阻力,没有计算由于物体的某些部分移动速度比其他部分快而引起的任何旋转阻力。例如,想象一下棒球棒的摆动,棒子的最远部分将会产生更多的阻力,因为它的摆动速度比手柄快。

这让我重新考虑“体素”的想法,因为我可以在物体的几个部分计算出本地阻力。

我正在尝试使用圆来估算体素的表面积。但是仍然存在一些问题,使这个估计相对准确。尽管不精确,但似乎效果相当好。

首先,我使用重投影来确定体素是否在速度方向上“可见”,以确定它是否在物体的前导面上。如果是这样,那么我就将体素的本地(圆形)表面积乘以圆的法线和局部速度矢量的点积。这基于它实际面向运动方向的程度来调整面积。

目前的不准确性是由于圆圈对于奇怪地形状的物体无法很好地估计局部表面积,距离越远的顶点,估计就越不准确。在这个方面得到任何帮助将不胜感激。
此外,我需要优化这个计算。目前,对每个顶点进行计算证明是相当昂贵的。我会随着进展不断更新,任何意见都将非常有帮助!一旦我进展一些,我会尽快发布一些代码。 更新3 我使用体素进行了相当精确的实现,手动放置在物体表面,并手动估算面对该体素时的局部面积。然后使用点积来估算该面积有多少面向运动方向。这效果很好。但问题是即使不在物体前沿的体素也会导致阻力。因此,我使用Physics.Raycasts从速度方向上弹出一小段距离,然后射线回到体素。如果这个射线撞到实际物体(而不是体素)的碰撞器,就意味着它在前沿。这非常有效,产生了令人惊讶的准确自然的阻力行为。奇形怪状的物体最终会旋转以最小化阻力,就像你期望的那样。然而,一旦我增加了体素的分辨率和/或在场景中添加了几个物体,我的帧率降至近3fps。分析器显示,计算的主要负担是由于射线投射步骤。我尝试想出其他方法来确定体素是否在前沿,但目前没有成功。所以简而言之,我成功地模拟了阻力,但计算速度不够快。

你肯定需要一些逼近方法来处理成千上万个对象。一个简单的形状(例如椭球体)逼近每个对象是否足够准确?否则,你可以将体素投影到平面上并对其进行光栅化(即在地图或类似结构中插入被体素覆盖的2D坐标)。最后,计算地图中有多少条目。 - Nico Schertler
是的,像椭球体这样的简单形状就可以了。我认为圆形可能会太简单了。我也可以用矩形来应付。 - Adam B
另外需要补充的是,这些对象本身并不会非常复杂,但有些可能根据方向而有非常不同的区域。例如锥体,A 可能会根据其移动方向而发生很大变化。我也在问题中加入了这一点。 - Adam B
如果您可以访问形状的顶点,可以尝试将所有顶点投影到平面上,然后取投影的凸包?这可以在O(n log n)时间内完成。对于像立方体、椭球体、圆锥体、圆柱体等简单形状,这应该非常有效。但是,如果您的全局形状是多个简单形状的联合,则要小心,因为凸包可能比实际投影更大。 - Pierre Baret
谢谢Pierre,我会尝试一下并看看效果如何。 - Adam B
显示剩余4条评论
1个回答

0
我从来没有找到加速计算的方法,但只要体素数量较低,模拟就可以很好地工作。
该模拟根据每个体素的速度计算阻力。它检查它是否在物体的前沿,并且如果是,则施加其阻力。
代码可能有点难以理解,但至少可以让您开始尝试。如果您有任何问题或需要澄清,请告诉我。
此代码是我上面更新#3的稍微整理过的版本。
在操作中: 在模拟开始时(物体向屏幕右下方直线移动)

start of drag

您可以看到添加的力箭头用于可视化以及表示体素的圆圈。力与体素大致代表的表面积成比例,并且仅形状的前沿有助于减拖。

随着模拟的继续,由于阻力,形状会正确地旋转到最具空气动力学性能的位置,并且后部区域停止贡献阻力。

farther in simulation


可拖动的形状类

将此类拖到主对象(刚体)上以启用拖动。您可以选择在球形周围创建体素的扩散,或加载自己定制的带有 Voxel 脚本附加的游戏对象作为体素,并将其设置为此对象的子级。

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

[RequireComponent (typeof(Rigidbody))]
public class DragEnabledShape : MonoBehaviour {

    const float EPSILON = 0.0001f;

    public Voxel voxelPrefab;
    public float C = 1f;
    public float d = 0.5f;
    public int resolutionFactor = 2;
    public float maxDistanceFromCenter = 10f;
    public bool displayDragVisualization = false;
    public float forceVisualizationMultiplier = 1f;
    public bool displayVoxels = false;
    public bool loadCustomVoxels = false;

    List<Voxel> voxels;
    Rigidbody rb;


    // Use this for initialization
    void Awake () {
        voxels = new List<Voxel> ();
        rb = GetComponent<Rigidbody> ();
    }

    void OnEnable () {

        if (loadCustomVoxels) {
            var customVoxels = GetComponentsInChildren<Voxel> ();
            voxels.AddRange (customVoxels);
            if (displayDragVisualization) {
                foreach (Voxel voxel in customVoxels) {
                    voxel.DisplayDrag (forceVisualizationMultiplier);
                }
            }
            if (displayVoxels) {
                foreach (Voxel voxel in customVoxels) {
                    voxel.Display ();
                }
            }
        }
        else {
            foreach (Transform child in GetComponentsInChildren<Transform> ()) {
                if (child.GetComponent<Collider> ()) {
                    //print ("creating voxels of " + child.gameObject.name);
                    CreateSurfaceVoxels (child);
                }
            }
        }
    }

    void CreateSurfaceVoxels (Transform body) {
        List<Vector3> directionList = new List<Vector3> ();
        for (float i = -1; i <= 1 + EPSILON; i += 2f / resolutionFactor) {
            for (float j = -1; j <= 1 + EPSILON; j += 2f / resolutionFactor) {
                for (float k = -1; k <= 1 + EPSILON; k += 2f / resolutionFactor) {
                    Vector3 v = new Vector3 (i, j, k);
                    directionList.Add (v);
                }
            }
        }
        //float runningTotalVoxelArea = 0;
        foreach (Vector3 direction in directionList) {
            Ray upRay = new Ray (body.position, direction).Reverse (maxDistanceFromCenter);
            RaycastHit[] hits = Physics.RaycastAll (upRay, maxDistanceFromCenter);
            if (hits.Length > 0) {
                //print ("Aiming for " + body.gameObject.name + "and hit count: " + hits.Length); 
                foreach (RaycastHit hit in hits) {

                    if (hit.collider == body.GetComponent<Collider> ()) {
                        //if (GetComponentsInParent<Transform> ().Contains (hit.transform)) {
                        //print ("hit " + body.gameObject.name);  
                        GameObject empty = new GameObject ();
                        empty.name = "Voxels";
                        empty.transform.parent = body;
                        empty.transform.localPosition = Vector3.zero;
                        GameObject newVoxelObject = Instantiate (voxelPrefab.gameObject, empty.transform);
                        Voxel newVoxel = newVoxelObject.GetComponent<Voxel> ();
                        voxels.Add (newVoxel);
                        newVoxel.transform.position = hit.point;
                        newVoxel.transform.rotation = Quaternion.LookRotation (hit.normal);
                        newVoxel.DetermineTotalSurfaceArea (hit.distance - maxDistanceFromCenter, resolutionFactor);
                        newVoxel.attachedToCollider = body.GetComponent<Collider> ();
                        if (displayDragVisualization) {
                            newVoxel.DisplayDrag (forceVisualizationMultiplier);
                        }
                        if (displayVoxels) {
                            newVoxel.Display ();
                        }
                        //runningTotalVoxelArea += vox.TotalSurfaceArea;
                        //newVoxel.GetComponent<FixedJoint> ().connectedBody = shape.GetComponent<Rigidbody> ();
                    }
                    else {
                        //print ("missed " + body.gameObject.name + "but hit " + hit.transform.gameObject.name); 
                    }
                }


            }

        }

    }

    void FixedUpdate () {
        foreach (Voxel voxel in voxels) {
            rb.AddForceAtPosition (voxel.GetDrag (), voxel.transform.position);
        }
    }



}

体素类

此脚本附加在放置在形状周围的小游戏对象上。它们代表计算拖动的位置。因此,对于复杂的形状,这些位置应该位于任何极端位置,并且应该相对分散地分布在物体上。体素对象的刚体质量应该近似于此体素所代表的物体部分。

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

public class Voxel : MonoBehaviour {

    Vector3 velocity;


    public Collider attachedToCollider;

    Vector3 drag;

    public Vector3 Drag {
        get {
            return drag;
        }
    }

    float dragMagnitude;

    public float DragMagnitude {
        get {
            return dragMagnitude;
        }
    }

    bool leadingEdge;

    public bool LeadingEdge {
        get {
            return leadingEdge;
        }
    }

    bool firstUpdate = true;
    public float localSurfaceArea;

    Vector3 prevPos;
    public VoxelForceVisualizer forceVisualizer;
    public VoxelVisualizer voxelVisualizer;

    const float AREA_COEFFICIENT = 1.1f;
    const float EPSILON = 0.001f;
    const float FAR_DISTANCE = 5f;
    const float MAX_FORCE = 100f;



    public void DetermineTotalSurfaceArea (float distanceFromCenter, float resolution) {
        float theta = (Mathf.PI / 4) / resolution;
        float localR = distanceFromCenter * Mathf.Tan (theta) * AREA_COEFFICIENT;// * (resolution / 0.01f);
        localSurfaceArea = Mathf.PI * localR * localR;
    }


    bool IsVisibleFromPlane () {
        if (attachedToCollider == null) {
            throw new MissingReferenceException ("attached to collider not set");
        }
        bool visibleFromPlane = false;

        //checks if this is leading edge of this part of object.
        Ray justOutsideSurface = new Ray (this.transform.position, velocity).Reverse (EPSILON);
        RaycastHit hit;
        if (Physics.Raycast (justOutsideSurface, out hit, EPSILON * 2f)) {
            if (hit.collider == attachedToCollider) {

                //checks if other parts of this object are in front, blocking airflow.
                //Ray wayOutsideSurface = new Ray (this.transform.position, velocity).Reverse (FAR_DISTANCE);
                //RaycastHit firstHit;
                //if (Physics.Raycast (wayOutsideSurface, out firstHit, FAR_DISTANCE * 2f)) {
                //if (firstHit.collider == attachedToCollider) {
                visibleFromPlane = true;
                //}
                //}

            }
        }

        //}
        leadingEdge = visibleFromPlane;
        return visibleFromPlane;
    }

    void FixedUpdate () {
        if (firstUpdate) {
            prevPos = transform.position;
            firstUpdate = false;
        }

        velocity = (transform.position - prevPos) / Time.deltaTime;
        prevPos = transform.position;
    }

    public Vector3 GetDrag () {
        if (IsVisibleFromPlane ()) {
            float alignment = Vector3.Dot (velocity, this.transform.forward);
            float A = alignment * localSurfaceArea;
            dragMagnitude = DragForce.Calculate (velocity.sqrMagnitude, A);

            //This clamp is necessary for imperfections in velocity calculation, especially with joint limits!
            //dragMagnitude = Mathf.Clamp (dragMagnitude, 0f, MAX_FORCE);

            drag = -velocity * dragMagnitude;
        }
        return drag;
    }

    public void Display () {
        voxelVisualizer.gameObject.SetActive (true);
    }

    public void TurnOffDisplay () {
        voxelVisualizer.gameObject.SetActive (false);
    }

    public void DisplayDrag (float forceMultiplier) {
        forceVisualizer.gameObject.SetActive (true);
        forceVisualizer.multiplier = forceMultiplier;
    }

    public void TurnOffDragDisplay () {
        forceVisualizer.gameObject.SetActive (false);
    }


}

VoxelForceVisualizer

这是一个附加到预制体的细箭头,我将其作为像素的子对象,以便在调试拖动力时绘制力箭头。

using UnityEngine;

public class VoxelForceVisualizer : MonoBehaviour {

    const float TINY_NUMBER = 0.00000001f;

    public Voxel voxel;
    public float drag;
    public float multiplier;

    void Start () {
        voxel = this.GetComponentInParent<Voxel> ();
    }
    // Update is called once per frame
    void Update () {
        Vector3 rescale;
        if (voxel.LeadingEdge && voxel.Drag != Vector3.zero) {
            this.transform.rotation = Quaternion.LookRotation (voxel.Drag);
            rescale = new Vector3 (1f, 1f, voxel.DragMagnitude * multiplier);

        }
        else {
            rescale = Vector3.zero;
        }
        this.transform.localScale = rescale;
        drag = voxel.DragMagnitude;
    }

}

VoxelVisualizer

这个脚本被附加到一个小球体对象上,作为体素空物体的子级。它只是用来查看体素的位置,并让上面的脚本在不禁用拖动力计算的情况下显示/隐藏体素。

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

public class VoxelVisualizer : MonoBehaviour {


}

DragForce

这个函数用于计算阻力力量。

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

public static class DragForce {

    const float EPSILON = 0.000001f;


    public static float Calculate (float coefficient, float density, float vsq, float A) {
        float f = coefficient * density * vsq * A;
        return f;
    }

    public static float Calculate (float vsq, float A) {
        return Calculate (1f, 1f, vsq, A);
    }


}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接