射线和三角形的相交检测(ray triangle intersection test)

【判断三维空间中射线是否与三角形相交】 本文主要讲解计算机图形学光线追踪中的【判断三维空间中射线是否与三角形相交】传统方法和 Möller-Trumbore 算法,重点介绍了 Möller-Trumbore 的原理以及实现,并贴出C++代码实现思路。 阅读详情

目录

1. 概述

2. 理论部分

2.1. 一个直观的方法

2.2. 本文的方法

3. 代码部分

3.1. 参数说明

3.2. 代码说明

3.3. 交点坐标

3.4. osg中的应用

4. 后记 

5. 附录


1. 概述

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

 

2. 理论部分

2.1. 一个直观的方法

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

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

2.2. 本文的方法

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

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

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

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

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

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

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

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

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

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

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

根据混合积公式

上式可以改写成

令

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

3. 代码部分

      理论部分阐述完毕,开始上代码,这份代码来自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;
}

3.1. 参数说明

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

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

3.2. 代码说明

变量的命名方式:为了方便阅读,代码中的变量命名与上面公式中的变量保持一致,如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的最终值。

3.3. 交点坐标

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

O + Dt

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

3.4. osg中的应用

           在osg的LineSegmentIntersector.cpp文件的如下函数也用到上述的算法,参见3.2节解释去理解即可:

 void intersect(const osg::Vec3& v0, const osg::Vec3& v1, const osg::Vec3& v2)
    {
        if (_settings->_limitOneIntersection && _hit) return;

        // const StartEnd startend = _startEndStack.back();
        // const osg::Vec3& ls = startend.first;
        // const osg::Vec3& le = startend.second;

        Vec3 T = _start - v0;
        Vec3 E2 = v2 - v0;
        Vec3 E1 = v1 - v0;

        Vec3 P =  _d ^ E2;

        value_type det = P * E1;

        value_type r,r0,r1,r2;

        const value_type epsilon = 1e-10;
        if (det>epsilon)
        {
            value_type u = (P*T);
            if (u<0.0 || u>det)
            {
                return;
            }

            osg::Vec3 Q = T ^ E1;
            value_type v = (Q*_d);
            if (v<0.0 || v>det)
            {
                return;
            }

            if ((u + v) > det)
            {
                return;
            }

            value_type inv_det = 1.0/det;
            value_type t = (Q*E2)*inv_det;
            if (t<0.0 || t>_length) return;

            u *= inv_det;
            v *= inv_det;

            r0 = 1.0-u-v;
            r1 = u;
            r2 = v;
            r = t * _inverse_length;
        }
        else if (det<-epsilon)
        {
            value_type u = (P*T);
            if (u>0.0 || u<det) return;

            Vec3 Q = T ^ E1;
            value_type v = (Q*_d);
            if (v>0.0 || v<det) return;

            if ((u+v) < det) return;

            value_type inv_det = 1.0/det;
            value_type t = (Q*E2)*inv_det;
            if (t<0.0 || t>_length) return;

            u *= inv_det;
            v *= inv_det;

            r0 = 1.0-u-v;
            r1 = u;
            r2 = v;
            r = t * _inverse_length;
        }
        else
        {
            return;
        }

        // Remap ratio into the range of LineSegment
        const osg::Vec3d& lsStart = _settings->_lineSegIntersector->getStart();
        const osg::Vec3d& lsEnd = _settings->_lineSegIntersector->getEnd();
        double remap_ratio =  ((_start - lsStart).length() + r*_length)/(lsEnd - lsStart).length();

        Vec3 in = lsStart*(1.0 - remap_ratio) + lsEnd*remap_ratio; // == v0*r0 + v1*r1 + v2*r2;
        Vec3 normal = E1^E2;
        normal.normalize();

        LineSegmentIntersector::Intersection hit;
        hit.ratio = remap_ratio;
        hit.matrix = _settings->_iv->getModelMatrix();
        hit.nodePath = _settings->_iv->getNodePath();
        hit.drawable = _settings->_drawable;
        hit.primitiveIndex = _primitiveIndex;

        hit.localIntersectionPoint = in;
        hit.localIntersectionNormal = normal;

        if (_settings->_vertices.valid())
        {
            const osg::Vec3* first = &(_settings->_vertices->front());
            hit.indexList.reserve(3);
            hit.ratioList.reserve(3);

            if (r0!=0.0f)
            {
                hit.indexList.push_back(&v0-first);
                hit.ratioList.push_back(r0);
            }

            if (r1!=0.0f)
            {
                hit.indexList.push_back(&v1-first);
                hit.ratioList.push_back(r1);
            }

            if (r2!=0.0f)
            {
                hit.indexList.push_back(&v2-first);
                hit.ratioList.push_back(r2);
            }
        }

        _settings->_lineSegIntersector->insertIntersection(hit);
        _hit = true;
    }

4. 后记 

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

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

5. 附录

        本文转自:射线和三角形的相交检测(ray triangle intersection test)

检测两个三角形是否有重叠面积/相交的方法 为什么要写这篇文章呢? 在宾夕法尼亚大学的路径规划公开课第二周的课程中, 宾夕法尼亚大学Coursera运动规划公开课学习有感之二提到的那一周的作业用到的知识. 这个问题看似简单, 但是着实花了很长时间去想. 而且, 自己拍脑袋想的东西不一定完善, 方法总是有漏洞. 所以, 最后还是去Stack Overflow上寻找的答案, 选择了一个高票答案自己研究了一下分享在这里. 阅读详情

相关推荐

【信息科学与工程学】【数据科学】数据科学领域——第三篇 数学08 几何学00总篇

编号英文名称中文名称简要说明/归属1Foundations of Geometry几何基础公理化方法, Hilbert《几何基础》2Euclidean Geometry (Synthetic)欧几里得几何(综合法)《几何原本》传统3Analytic Geometry解析几何坐标方法, Descartes, Fermat4Plane Geometry平面几何三角形、圆、共点共线等经典问题5Solid Geometry立体几何三维空间中的图形与度量6Non-Euclidean Geometry非欧几何平行公设不

weixin_49199313的博客 200

射线与三角形相交检测

直观方法:判断射线是否与三角形所在的平面相交,如果相交,计算出射线与平面交点,如果交点在三角形内,则射线与三角形相交 接上一篇射线与平面相交检测,在这里直接使用射线与平面相交计算出来的交点坐标,判断交点坐标是否在三角形内。 判断一个点是否在三角形内,需要使用到Barycentric Coordinates(质心坐标系) 质心坐标系的基本概念: 几何结构中,图形中的点相对各顶点的位置,三角形中各顶点由(1, 0, 0),(0, 1, 0),(0, 0, 1)分别表示,则三角形内所有点的坐标 (...

lq 1897

【信息科学与工程学】计算机科学与自动化——第三百零五篇 计算机系统中的各类运算操作02

编号类型领域运算操作运算类型算法算法的详细设计及数学分析参数列表及参数的数值设计应用场景301集合运算关系代数关系集合Feature Selection (Chi-Square)集合运算卡方检验目标函数:χ2=∑E(O−E)2​ 衡量分类特征与目标的相关性;资源:O(n·d);条件:离散特征,离散目标;组合约束:值越大相关性越强。 数据结构:列联表。 代码实现(C++): 特征列,目标列文本分类中的特征选择。302集合运算关系代数关系集合Feature Selection (ANOVA F

weixin_49199313的博客 309

射线和三角形的相交检测(ray triangle intersection test)【转】

本文以Fast, Minimum Storage Ray Triangle Intersection为参考,在此感谢原作者,大家也可以直接阅读原版。 概述 射线和三角形的相交检测是游戏程序设计中一个常见的问题,最典型的应用就是拾取(Picking),本文介绍一个最常见的方法,这个方法也是DirectX中采用的方法,该方法速度快,而且存储空间少。先讲述理论,然后给出对应的代码实现。 ...

weixin_30387799的博客 247

从打印攻击到视频重放:手把手用OULU-NPU训练你的第一个活体检测模型

本文详细介绍了如何使用OULU-NPU数据集训练活体检测模型,以应对打印攻击和视频重放等安全威胁。通过PyTorch框架,从环境准备、数据加载、预处理到模型架构设计,手把手教你构建一个高效的双流网络,结合CNN和LSTM技术,提升模型在多种攻击场景下的检测准确率。

weixin_29092031的博客 385

DICOM三维重建:基于VTK实现三种面绘制算法

版本:VS2019、VTK9.0.1 #define vtkRenderingCore_AUTOINIT 3(vtkInteractionStyle,vtkRenderingFreeType,vtkRenderingOpenGL2) #define vtkRenderingVolume_AUTOINIT 1(vtkRenderingVolumeOpenGL2) #include "vtkDICOMImageReader.h" #include "vtkRenderWindowInteractor.h" #i

一直在笑的博客 5923

SymPy 几何工具库深入解析:intersection、convex_hull、are_similar、centroid 与 idiff 的源码级实战指南

SymPy 的 `sympy.geometry.util` 模块提供了一组与几何实体(GeometryEntity)配合使用的顶层工具函数,用于完成相交计算、凸包求解、相似性判定、质心求解与隐函数求导等高频几何运算。本文以 [utils.rst](https://link.gitcode.com/i/9f2c2d7a621381ac75c1ec987c634750) 中收录的 5 个公开函数为主线

gitblog_01086的博客 564

从零到一:RF-DETR如何通过神经架构搜索重塑实时目标检测的边界

RF-DETR通过创新的权重共享神经架构搜索(NAS)机制,重新定义了实时目标检测的技术边界。这项技术在COCO等基准测试中表现卓越,成为首个AP超过60的实时检测器,同时保持跨领域稳定性和高效推理速度。RF-DETR的动态架构调整能力使其能够适应从嵌入式设备到云端服务器的各种硬件环境,为工业质检、自动驾驶等场景提供灵活解决方案。

weixin_29061531的博客 469

CLRS 计算几何基石:叉积、方向判定、极角排序与多边形问题(C33 33.1 习题全解)

《算法导论》(CLRS)第 33 章"计算几何"(Computational Geometry)是算法竞赛与图形学开发的必修内容,而 33.1 节"线段的性质"(Line-Segment Properties)则奠定了整章的基础:叉积(cross product)、方向判定(direction)、线段相交判定、极角排序、以及由这些原语衍生出的共线检测、凸多边形判定、点在多边形内判定与多边形面积计算

gitblog_01040的博客 464

STGCN实战:从零构建PyTorch时空图卷积网络预测城市交通流

本文详细介绍了如何使用PyTorch从零构建时空图卷积网络(STGCN)进行城市交通流预测。通过将交通网络建模为图结构,并融合时间卷积与空间图卷积,STGCN能有效捕捉交通流的时空依赖性。文章提供了从数据预处理、核心模块(TimeBlock与STGCNBlock)搭建、模型训练到实战技巧的完整指南,帮助开发者快速掌握这一前沿的交通速度预测技术。

wasm7browser的博客 249

Allegro 给元器件PCB封装库添加3D模型详细步骤

从上面视图可以看到模型跟PCB 库的方向是对了,但是位置偏下,且在PCB封装库后面,我们下面需要做的就是把模型往上移、往前移。首先将元器件3D模型文件放在封装库的目录下(也可以单独新建一个模型库文件夹),注意3D文件必须是Step格式。Transparent: 选择透视的对象,可以是模型、也可以是PCB封装,或者两种都透视。在左边搜索要添加的3D模型的文件(注意是在* 星号前面输入文件名),按回车即可找到。同样的,接下来需要往Y 负方向移,即左移,一边调整一边观察视图。

sinat_15677011的博客 3682

体素化核心原理与实现:从点云到三维网格的工程实践指南

在三维视觉与点云处理领域,体素化作为连接连续几何与离散数据结构的关键技术,广泛应用于机器人导航、碰撞检测与深度学习。其核心原理是将点云或Mesh转换为规则排列的体素网格,从而降低数据复杂度、提升计算稳定性。工程实现通常采用暴力遍历、八叉树递归或哈希表加速三种路径,其中哈希表因其内存效率与速度的平衡成为主流选择。体素尺寸的设定直接影响模型细节与内存占用,需结合实际数据密度与任务需求动态调整。无论是点云降采样、表面重建,还是稀疏卷积网络的输入构建,体素化都是不可绕过的基础环节。掌握其算法流程与参数调优技巧,能够

weixin_34195364的博客 314

【点云鸟瞰图BEV】点云鸟瞰图BEV原理与可视化

本节将介绍激光雷达等点云的鸟瞰图生成原理、代码及效果图。 1 BEV视图原理 点云BEV(Bird's Eye View)视图是指点云在垂直于高度方向的平面上的投影。通常,在获得bev视图前,会将空间分割成体素,利用体素对点云进行下采样,然后将每个体素作为一个点进行投影。体素是指将三维空间按照固定尺寸长方体进行划分时的长方体(Δl*Δw*Δh)。 体素投影时可以得到BEV视图的像素点坐标。每个像素点的特征取值则可以通过多种方式得到。 ...

叶子的博客 2万+

DirectX 12 Raytracing(DXR)实战示例全解析:DirectX-Graphics-Samples 中的教程与高级场景指南

本文围绕当前仓库中 [Samples/Desktop/D3D12Raytracing/readme.md](https://link.gitcode.com/i/b5b54df4f6e400f2120e3c09efa22ccf) 这一官方集合说明展开,系统梳理其中 7 个教程示例与 2 个高级示例的技术脉络,并深入剖析每个示例背后的 DXR 管线搭建、着色器表布局、Shader Execution

gitblog_00223的博客 235

JVM高频面试基本问题与知识点整理

JVM高频面试基本问题与知识点整理

曾经“等你生日那天”都遥远得像未来,如今却可欢愉的挥手说“下个十年见” 87万+

基于分水岭分割进行肺癌诊断

近年来,肺癌的发病率和病死率均迅速上升,目前已居所有癌症之首。随着肺癌病人数量的增加,医生对肺部 CT 图像进行研判的工作量也增加了不少,在这种情况下难免工作效率降低甚至会出现误诊。为了帮助医生减少重复性工作,对肺部 CT 图像进行计算机辅助检测的技术就被广泛应用于对肺癌的诊断和治疗过程中。医学 CT 图像处理主要是研究医学图像中的器官和组织之间的关系,并进行病理性分析。

在职AI算法工程师,擅长计算机视觉,YOLO目标检测、分割等,擅长web、pyqt界面可视化,好内容持续更新中,来这里跟大家一起学习,共同进步 1568

qradiant

我实在没有什么时间来整理好这个了,这一篇没有任何的技术含量,其实就是一些代码的描述罢了, 加之个人能力有限,理解不到家,有空再写清楚 关卡编辑器部分    关卡编辑器的代码分析, 在关卡编辑器开始时,使用mfc来初始化这个opengl这个驱动等等的处理 检测鼠标,键盘的输入,滑动等等 我自己自个划分的有 界面层 ,操作层,介质层 界面层 专门画显示界面的

tailiangliang的专栏 2067

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

上一篇: 空间直线与球面的相交算法
下一篇: osgUtil::LineSegmentIntersector类源码分析(二)
荆楚闲人
博客等级 码龄20年 3万+粉丝 473原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值