判断三角面片与空间中球体是否相交

文章介绍了如何判断3D空间中一个三角面片与球体是否有相交部分的问题。主要涉及三种情况:1)三角形至少一个顶点在球体内;2)三角形边与球体相交;3)三角形平面与球体相交。通过计算顶点到球心的距离、线段与球体的关系以及平面到球心的距离来确定交点存在性。文章提供了Unity3D中的C#代码实现。


​ 在做项目时遇到了一个数学问题,即,如何判断给定一个三角面片与空间中某个球体有相交部分?这个问题看似简单,实际处理起来需要一些方法和手段。

一、问题描述

已知空间中球体的球心位置center,半径为r,三角形三个顶点分别为v1,v2,v3。判断该三角形与球体是否有交点?

二、解题思路

​ 通过写写画画,我们可以大致将球体和三角形的位置分为以下三种情况:

(一)三角形三个顶点至少有一个在球体内部

image-20230305153905184

​ 这种情况相对来说比较简单,只需要判断三角形三个顶点到球心的最小距离<半径 r 即可。

(二)三个顶点都在球体外部,但至少有一条线段与球体相交

image-20230305154203013

​ 计算三边所在直线到球心的距离,最短距离 < r 即可。

​ 但是同时需要确保 ∠ O A C \angle OAC OAC ∠ O C A \angle OCA OCA 均< ∠ O D A \angle ODA ODA,否则会出现以下错误的情况:

image-20230305154357287

​ 根本原因是,我们求的是线段与球体相交,而不是一条直线。

(三)三个顶点、三条边都在球体外部,但平面与球体相交

image-20230305154528284

​ 同理,计算球心到平面的距离 < r 即可。

​ 一样的,需要避免以下的情况,原因一样,我们求的是三角形与球体是否有交点,而不是平面:

image-20230305154638704

​ 判断方式:作球心 O 到平面 ABC 的投影 P,看 P 是否在三角形内部:

image-20230305154737066
  • 方法一: 面积法。算出三角形 ABP、BCP、CAP 的面积和,与 ABC 面积进行比较。如果相同,则 P 在内部

  • 方法二: 矢量法。若 P 在三角形内部,则:

    • 对 BC 而言,P、A 在同一侧
    • 对 CA 而言,P、B 在同一侧
    • 对 AB 而言,P、C 在同一侧

    可以用矢量的叉乘判断两点是否在线段的同一侧。

    即,对于 BC 而言, B P → × B C → \overrightarrow{BP}\times\overrightarrow{BC} BP ×BC B P → × B A → \overrightarrow{BP}\times\overrightarrow{BA} BP ×BA 同向,则 P、A 在同一侧。

实现代码:

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

public class Segment
{
    public Vector3 start; // 起始位置
    public Vector3 end;   // 终点位置

    /// <summary>
    /// 线段向量
    /// </summary>
    public Vector3 vector => end - start;

    /// <summary>
    /// 单位方向
    /// </summary>
    public Vector3 direction => vector.normalized;

    /// <summary>
    /// 线段长度
    /// </summary>
    public float length => vector.magnitude;

    public Segment(Vector3 start, Vector3 end) {
        this.start = start;
        this.end   = end;
    }

    /// <summary>
    /// 计算点到线段所在直线的距离
    /// </summary>
    /// <param name="point">点</param>
    /// <returns></returns>
    public float LineDistance(Vector3 point) {
        float theta = Vector3.Angle(point - start, vector);
        return (point - start).magnitude * Mathf.Sin(Mathf.Deg2Rad * theta);
    }

    /// <summary>
    /// 线段是否穿过球体
    /// </summary>
    /// <param name="center">球心</param>
    /// <param name="radius">半径</param>
    /// <returns></returns>
    public bool CrossSphere(Vector3 center, float radius) {
        // 如果线段两端点其中一个在球体内部,直接返回 true
        if (Vector3.Distance(center, start) < radius || Vector3.Distance(center, end) < radius) return true;

        // 球心到线段距离大于半径,则返回 false
        float d = LineDistance(center);
        if (d >= radius) return false;

        // 计算球心与线段的两个夹角,以判断线段是否穿过球体
        float theta = Mathf.Asin(d / radius) * Mathf.Rad2Deg;
        float a1    = Vector3.Angle(center - start, vector);
        float a2    = Vector3.Angle(center - end,   -vector);
        return a1 <= theta && a2 <= theta;
    }

    /// <summary>
    /// 判断两点是否处于线段同一侧
    /// </summary>
    /// <param name="p1">点 1</param>
    /// <param name="p2">点 2</param>
    /// <returns></returns>
    public bool SameSide(Vector3 p1, Vector3 p2) {
        Vector3 v1 = Vector3.Cross(p1 - start, vector);
        Vector3 v2 = Vector3.Cross(p2 - start, vector);
        return Vector3.Dot(v1, v2) >= 0;
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 三角形
/// </summary>
public class Triangle
{
    public Vector3 v1; // 顶点 1
    public Vector3 v2; // 顶点 2
    public Vector3 v3; // 顶点 3

    /// <summary>
    /// 线段 12
    /// </summary>
    public Segment l1 => new Segment(v1, v2);

    /// <summary>
    /// 线段 23
    /// </summary>
    public Segment l2 => new Segment(v2, v3);

    /// <summary>
    /// 线段 31
    /// </summary>
    public Segment l3 => new Segment(v3, v1);

    /// <summary>
    /// 所在平面
    /// </summary>
    public Plane plane => new Plane(v1, v2, v3);

    public Triangle(Vector3 v1, Vector3 v2, Vector3 v3) {
        SetInfo(v1, v2, v3);
    }

    /// <summary>
    /// 设置三角形位置
    /// </summary>
    /// <param name="v1">顶点 1</param>
    /// <param name="v2">顶点 2</param>
    /// <param name="v3">顶点 3</param>
    public void SetInfo(Vector3 v1, Vector3 v2, Vector3 v3) {
        this.v1 = v1;
        this.v2 = v2;
        this.v3 = v3;
    }

    /// <summary>
    /// 顶点到点 point 的最小距离
    /// </summary>
    /// <param name="point">点 point</param>
    /// <returns></returns>
    public float MinPointDistance(Vector3 point) {
        float   d1 = Vector3.Distance(v1, point);
        float   d2 = Vector3.Distance(v2, point);
        float   d3 = Vector3.Distance(v3, point);
        return Mathf.Min(d1, d2, d3);
    }

    /// <summary>
    /// 投影转化为平面三角形(大小不变)
    /// </summary>
    /// <param name="center">投影中心</param>
    /// <returns>平面三角形</returns>
    public Triangle2D CastTriangle2D(Vector3 center) {
        Vector3 origin  = plane.ClosestPointOnPlane(center);
        Vector3 vector1 = v1 - origin;
        Vector3 vector2 = v2 - origin;
        Vector3 vector3 = v3 - origin;

        float a12 = Vector3.SignedAngle(vector1, vector2, plane.normal) * Mathf.Deg2Rad;
        float a13 = Vector3.SignedAngle(vector1, vector3, plane.normal) * Mathf.Deg2Rad;

        Vector2 p1 = new Vector2(vector1.magnitude,                  0);
        Vector2 p2 = new Vector2(vector2.magnitude * Mathf.Cos(a12), vector2.magnitude * Mathf.Sin(a12));
        Vector2 p3 = new Vector2(vector3.magnitude * Mathf.Cos(a13), vector3.magnitude * Mathf.Sin(a13));

        return new Triangle2D(p1, p2, p3);
    }

    /// <summary>
    /// 判断三角形是否穿过球体
    /// </summary>
    /// <param name="center">球心</param>
    /// <param name="radius">半径</param>
    /// <returns></returns>
    public bool CrossSphere(Vector3 center, float radius) {
        // 如果最近的点在球体内部则返回 ture
        if (MinPointDistance(center) < radius) return true;

        // 如果有线段穿过了球体,则返回 true
        if (l1.CrossSphere(center, radius) || l2.CrossSphere(center, radius) || l3.CrossSphere(center, radius)) return true;

        // 否则投影球心到三角平面上,看其是否在三角形内
        float d = Mathf.Abs(plane.GetDistanceToPoint(center));
        if (d < radius) {                            // 球心到平面的距离小于半径才可能相交
            Triangle2D t2D = CastTriangle2D(center); // 进行投影
            return t2D.Contains(Vector2.zero);       // 判断
        }

        return false;
    }
}
using System;
using UnityEngine;

/// <summary>
/// 平面线段
/// </summary>
public class Segment2D
{
    public Vector2 start; // 起始位置
    public Vector2 end;   // 终点位置

    /// <summary>
    /// 线段向量
    /// </summary>
    public Vector2 vector => end - start;

    /// <summary>
    /// 单位方向
    /// </summary>
    public Vector2 direction => vector.normalized;

    public float length => vector.magnitude;

    /// <summary>
    /// 线段长度
    /// </summary>
    public Segment2D(Vector2 start, Vector2 end) {
        this.start = start;
        this.end   = end;
    }

    /// <summary>
    /// 判断两点是否处于线段同一侧
    /// </summary>
    /// <param name="p1">点 1</param>
    /// <param name="p2">点 2</param>
    /// <returns></returns>
    public bool SameSide(Vector2 p1, Vector2 p2) {
        Vector3 v1 = Vector3.Cross(p1 - start, vector);
        Vector3 v2 = Vector3.Cross(p2 - start, vector);
        return Vector3.Dot(v1, v2) >= 0;
    }
}
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Triangle2D 
{
    public Vector2 v1; // 顶点 1
    public Vector2 v2; // 顶点 2
    public Vector2 v3; // 顶点 3

    /// <summary>
    /// 线段 12
    /// </summary>
    public Segment2D l1 => new Segment2D(v1, v2);

    /// <summary>
    /// 线段 23
    /// </summary>
    public Segment2D l2 => new Segment2D(v2, v3);

    /// <summary>
    /// 线段 31
    /// </summary>
    public Segment2D l3 => new Segment2D(v3, v1);

    public Triangle2D(Vector2 v1, Vector2 v2, Vector2 v3) {
        SetInfo(v1, v2, v3);
    }

    /// <summary>
    /// 设置三角形位置
    /// </summary>
    /// <param name="v1">顶点 1</param>
    /// <param name="v2">顶点 2</param>
    /// <param name="v3">顶点 3</param>
    public void SetInfo(Vector2 v1, Vector2 v2, Vector2 v3) {
        this.v1 = v1;
        this.v2 = v2;
        this.v3 = v3;
    }

    /// <summary>
    /// 判断点 point 是否在三角形内部
    /// </summary>
    /// <param name="point">点</param>
    /// <returns></returns>
    public bool Contains(Vector2 point) {
        return l1.SameSide(point, v3) && l2.SameSide(point, v1) && l3.SameSide(point, v2);
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蔗理苦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值