转: 射线和三角形的相交检测

原文:https://www.cnblogs.com/graphics/archive/2010/08/09/1795348.html

概述

射线和三角形的相交检测是游戏程序设计中一个常见的问题,最典型的应用就是拾取(Picking),本文介绍一个最常见的方法,这个方法也是DirectX中采用的方法,该方法速度快,而且存储空间少。先讲述理论,然后给出对应的代码实现。

image
image

理论部分

一个直观的方法

我想大多数人在看到这个问题时,可能都会想到一个简单而直观的方法:首先判断射线是否与三角形所在的平面相交,如果相交,再判断交点是否在三角形内。

判断射线是否与平面相交

判断点是否在三角形内

但是,上面的方法效率并不很高,因为需要一个额外的计算,那就是计算出三角形所在的平面,而下面要介绍的方法则可以省去这个计算。

本文的方法

接下来会涉及到一些数学知识,不过没关系,我会详细解释每一个步骤,不至于太晦涩,只要您不觉得烦就行了,好了开始!

射线的参数方程如下,其中O是射线的起点,D是射线的方向。

image

我们可以这样理解射线,一个点从起点O开始,沿着方向D移动任意长度,得到终点R,根据t值的不同,得到的R值也不同,所有这些不同的R值便构成了整条射线,比如下面的射线,起点是P0,方向是u,p0 + tu也就构成了整条射线。

image

三角形的参数方程如下,其中V0,V1和V2是三角形的三个点,u, v是V1和V2的权重,1-u-v是V0的权重,并且满足u>=0, v >= 0,u+v<=1。

image

确切的说,上面的方程是三角形及其内部所有点的方程,因为三角形内任意一点都可以理解为从顶点V0开始,沿着边V0V1移动一段距离,然后再沿着边V0V2移动一段距离,然后求他们的和向量。至于移动多大距离,就是由参数u和v控制的。

image

于是,求射线与三角形的交点也就变成了解下面这个方程-其中t,u,v是未知数,其他都是已知的

image

移项并整理,将t,u,v提取出来作为未知数,得到下面的线性方程组

image

现在开始解这个方程组,这里要用到两个知识点,一是克莱姆法则,二是向量的混合积。

令E1 = V1 - V0,E2 = V2 - V0,T = O - V0上式可以改写成

image

根据克莱姆法则,可得到t,u,v的解分别是

image

将这三个解联合起来写就是

image

根据混合积公式

image

上式可以改写成

image

image

得到最终的公式,这便是下面代码中用到的最终公式了,之所以提炼出P和Q是为了避免重复计算

image

代码部分

理论部分阐述完毕,开始上代码,这份代码来自DirectX SDK中的Demo,名字叫做Picking(拾取),该函数位于文件Pick.cpp的最末尾。这个函数有一个特点,就是判断语句特别多,因为对于一个频繁被调用的函数来说,效率是最重要的,这么多判断就是为了在某个条件不满足时,及时返回,避免后续不必要的计算。

// Determine whether a ray intersect with a triangle
// Parameters
// orig: origin of the ray
// dir: direction of the ray
// v0, v1, v2: vertices of triangle
// t(out): weight of the intersection for the ray
// u(out), v(out): barycentric coordinate of intersection

bool IntersectTriangle(const Vector3& orig, const Vector3& dir,
    Vector3& v0, Vector3& v1, Vector3& v2,
    float* t, float* u, float* v)
{
    // E1
    Vector3 E1 = v1 - v0;

    // E2
    Vector3 E2 = v2 - v0;

    // P
    Vector3 P = dir.Cross(E2);

    // determinant
    float det = E1.Dot(P);

    // keep det > 0, modify T accordingly
    Vector3 T;
    if( det >0 )
    {
        T = orig - v0;
    }
    else
    {
        T = v0 - orig;
        det = -det;
    }

    // If determinant is near zero, ray lies in plane of triangle
    if( det < 0.0001f )
        return false;

    // Calculate u and make sure u <= 1
    *u = T.Dot(P);
    if( *u < 0.0f || *u > det )
        return false;

    // Q
    Vector3 Q = T.Cross(E1);

    // Calculate v and make sure u + v <= 1
    *v = dir.Dot(Q);
    if( *v < 0.0f || *u + *v > det )
        return false;

    // Calculate t, scale parameters, ray intersects triangle
    *t = E2.Dot(Q);

    float fInvDet = 1.0f / det;
    *t *= fInvDet;
    *u *= fInvDet;
    *v *= fInvDet;

    return true;
}

参数说明

输入参数:前两个参数orig和dir是射线的起点和方向,中间三个参数v0,v1和v2是三角形的三个顶点。

输出参数:t是交点对应的射线方程中的t值,u,v则是交点的纹理坐标值

代码说明

变量的命名方式:为了方便阅读,代码中的变量命名与上面公式中的变量保持一致,如E1,E2,T等。

变量det表示矩阵的行列式值

27-35行用来确保det>0,如果det<0则令det = -det,并对T做相应的调整,这样做是为了方便后续计算,否则的话需要分别处理det>0和det<0两种情况。

第38行,注意浮点数和0的比较,一般不用 == 0的方式,而是给定一个Epsilon值,并与这个值比较。

第43行,这里实际上u还没有计算完毕,此时的值是Dot(P,T),如果Dot(P,T) > det, 那么u > 1,无交点。

第51行,要确保u + v <= 1,也即 [Dot(P,T) + Dot(Q, D)] / det 必须不能大于1,否则无交点。

第57-60行,走到这里时,表明前面的条件都已经满足,开始计算t, u, v的最终值。

交点坐标

根据上面代码求出的t,u,v的值,交点的最终坐标可以用下面两种方法计算

O + Dt

(1 - u - v)V0 + uV1 + vV2

后记

在本文开头已经说了,射线和三角形的相交检测最典型的应用就是拾取,比如在一个三维场景中用鼠标选择某个物体。那么拾取是如何实现的呢?我们知道在物体的三维模型表示中,三角形是最小的几何图元,最小意味着不可再分,也就是说任何模型,无论它多么复杂,都可以由若干个三角形组合而成。拾取过程实际是判断拾取射线是否与模型相交,而这又可以转化为-只要射线与模型中的任何一个三角形相交即可。下面是模型的线框表示法,可见如果想要判断某条射线是否与这个茶壶相交,只要判断该射线是否与茶壶模型中某个三角形相交即可。

image

需要注意的是,虽然射线和三角形的相交检测可以用来实现拾取,但是大多数程序并不采用这个方法,原因是这个方法效率很低,我们可以设想,一个大型的3D在线游戏,它的模型数量以及复杂程度都是很高的,如果用这种方法来判断,需要对模型中每个三角形都做一次判断,效率极其低下,一种可行的方案是,用包围球或者包围盒来代替,计算出能容纳模型的最小球体或者矩形体,只要判断出射线与包围球或者包围盒相交,我们就认为射线与模型相交,这样效率会显著提高,只是精确度上会有一定误差,但是足以满足多数程序的需要。

image


以上为原文,现将原文中的代码部分翻译为C#:

Unity中C#代码

    // 得射线与三角形是否相交
    // Determine whether a ray intersect with a triangle
    // Parameters
    // orig: origin of the ray. 射线的起点pos
    // dir: direction of the ray. orig的方向向量
    // v0, v1, v2: vertices of triangle. 三角形的3个顶点
    // t(out): weight of the intersection for the ray. 射线与三角形的交点
    // u(out), v(out): barycentric coordinate of intersection. 交点在三角形内的重心位置
    public bool IntersectTriangle(Vector3 orig, Vector3 dir, Vector3 v0, Vector3 v1, Vector3 v2, out float t, out float u, out float v)
    {
        // E1,三角形中指向v1的方向向量
        Vector3 E1 = v1 - v0;

        // E2,三角形中指向v2的方向向量
        Vector3 E2 = v2 - v0;

        // P,垂直于E2的法向量
        Vector3 P = Vector3.Cross(dir, E2);

        // determinant, E2的法向量与E1的夹角
        float det = Vector3.Dot(E1, P);

        // keep det > 0, modify T accordingly
        Vector3 T;
        if (det > 0)
        {
            T = orig - v0;
        }
        else
        {
            T = v0 - orig;
            det = -det;
        }
        t = 0;
        u = 0;
        v = 0;

        // If determinant is near zero, ray lies in plane of triangle
        if (det < 0.00001f)
            return false;

        // Calculate u and make sure u <= 1
        u = Vector3.Dot(T, P);
        if (u < 0.0f || u - det > 0.00001f) // u > det
            return false;

        // Q
        Vector3 Q = Vector3.Cross(T, E1);

        // Calculate v and make sure u + v <= 1
        v = Vector3.Dot(dir, Q);
        if (v < 0.0f || (u + v - det > 0.00001f)) // u + v > det
            return false;

        // Calculate t, scale parameters, ray intersects triangle
        t = Vector3.Dot(E2, Q);

        float fInvDet = 1.0f / det;
        t *= fInvDet;
        u *= fInvDet;
        v *= fInvDet;

        return true;
    }

    // 测试相交点
    public void TestPos(float x, float z)
    {
        var mesh = this.GetComponent<MeshFilter>().mesh;
        var vertices = mesh.vertices;
        var startPos = new Vector3(x, 0.2f, z);
        var endPos = new Vector3(x, 10, z);
        var dir = Vector3.Normalize(endPos - startPos);
        for (int i = 0; i < mesh.triangles.Length; i += 3)
        {
            var v0 = vertices[mesh.triangles[i]];
            var v1 = vertices[mesh.triangles[i + 1]];
            var v2 = vertices[mesh.triangles[i + 2]];

            float t, u, v;
            var b = IntersectTriangle(startPos, dir, v0, v1, v2, out t, out u, out v);
            if (b)
            {
                var p = (1 - u - v) * v0 + u * v1 + v * v2;
            }
        }
    }

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343