Axiom3D:Ogre射线与点,线,面相交,鼠标操作3维空间.

  在第一篇网络分解成点,线,面.第二篇分别点以球形,线以圆柱,面分别以MergerBatch整合批次显示.因为整合批次显示后,相应的点,线,面不能以Ogre本身的射线来选取,因为整合后,以点举例,多个点显示虽然不在一起,但是是一个Mesh.Ogre本身的检测只能检测到这里,在我们这不满足要求,相应的点,线,面检测都需要自己来计算.

  在讲解本文之前,先看下射线的相关生成代码,只有先明白射线如何生成,生成最后是相对什么空间.

        [OgreVersion( 1, 7, 2790, "Slightly different" )]
        public void GetCameraToViewportRay( float screenX, float screenY, out Ray ray )
        {
            var inverseVP = ( _projectionMatrix*_viewMatrix ).Inverse();

#if !AXIOM_NO_VIEWPORT_ORIENTATIONMODE
            // We need to convert screen point to our oriented viewport (temp solution)
            Real tX = screenX;
            Real a = (int)OrientationMode*System.Math.PI*0.5f;
            screenX = System.Math.Cos( a )*( tX - 0.5f ) + System.Math.Sin( a )*( screenY - 0.5f ) + 0.5f;
            screenY = System.Math.Sin( a )*( tX - 0.5f ) + System.Math.Cos( a )*( screenY - 0.5f ) + 0.5f;
            if ( ( ( (int)OrientationMode ) & 1 ) == 1 )
            {
                screenY = 1.0f - screenY;
            }
#endif

            Real nx = ( 2.0f*screenX ) - 1.0f;
            Real ny = 1.0f - ( 2.0f*screenY );
            var nearPoint = new Vector3( nx, ny, -1.0f );
            // Use midPoint rather than far point to avoid issues with infinite projection
            var midPoint = new Vector3( nx, ny, 0.0f );

            // Get ray origin and ray target on near plane in world space

            var rayOrigin = inverseVP*nearPoint;
            var rayTarget = inverseVP*midPoint;

            var rayDirection = rayTarget - rayOrigin;
            rayDirection.Normalize();

            ray = new Ray( rayOrigin, rayDirection );
        }

Ogre 射线生成

  这个方法在摄像机类,主要因为摄像机内整合了视图矩阵与透视矩阵,再说下视图矩阵与透视矩阵的作用,视图矩阵相当于把世界坐标变换成摄像机坐标系,我们平常用Camear.Lookat来完成,一个参数是当前位置,一个是向上向量,一个是指向目标向量.这三个参数分别用来指定视图坐标系的圆点,指向目标向量是Z轴,向上向量是Y轴,根据YZ向量的叉积生成X轴,然后X轴与Z轴的叉积会重新生成Y轴,为什么Y轴后面会重新生成,因为我们目标向量可能一直在变,本来不可能一直垂直Y轴,而在我们视图坐标系中,三轴垂直,Y轴更多用来辅助生成X轴.比如我们不设置Camear.Lookat,那么默认视图坐标系与世界坐标对应关系如下,目录在原点,而Z轴是Vector3.zero-Vector3.Z,就是负Z轴,Y轴还是Vector3.Y,根据z与y生成x轴对应的是-Vector3.X,然后生成Y轴,和原来一样,总的来说,就是一个和世界坐标系同原点,但是x,z是负方向,y的原来一样的坐标系.

  而透视矩阵是在视图矩阵后,把视图里视椎体(一般四个参数,近截而距离,远截面距离,上下视角,左右比例)映射成一个长宽高各为(-1,1)的立方体,在Ogre与opengl都是如此,在dx中是(0,1)的立方体,这个关系不大.这里不说具体如何生成矩阵,只讲这个矩阵的含义.在经过透视矩阵后生成的x,y,z三个方向各为(-1,1)的立方体中,x方向的(-1,1)对应的屏幕的宽度,而y方向对应是屏幕的高度,z对应的是深度,主要用来判断那个物体在前,那个物体在后,根据深度可以添加很多特殊效果.

  物体一般的变换大致如下,如果物体本身有个局部坐标系,需要先从局部坐标系换成世界坐标系,再从世界坐标系换成屏幕坐标系,最后经过透视矩阵转换成屏幕上的点.

  那么我们用鼠标点击一下,就是屏幕上的点,如何换算成3D里面的坐标系了,Ogre里的GetCameraToViewportRay就给我们做了一个很好的演视,或者说是把上面的过程反推回去,首先根据点在视图中的位置x/width,y/hight生成0-1的浮点值,在前面我们知道,透视矩阵后的长宽都是-1到1的,所以先要把这里的0,1的值映射成-1,1,很简单2*x-1.需要注意的是,在屏幕里,Y是从上向下,而我们世界坐标系Y是从下向上,所以首先把y倒过来取负.x与y确定后,z值不确定.这很正常,二维变成三维,本来就少一维的信息,在这里我们生成射线就好说了,分别是近点z=-1,和远点z=1生成射线就行,这边没取z=1,而是z=0,代码上有解释,中间的点比最远的的那点好,避免无限远投影的问题.那么在这我们就生成了原经过透视矩阵后的坐标,如果把这坐标换成世界坐标了,很简单来,原来是世界坐标经过视图与透视矩阵后成透视坐标,我们只要把对应透视坐标剩以视图与透视矩阵的逆矩阵就成了世界坐标系中的位置.我们知道矩阵剩以他自己的逆矩阵等于1.根据这两点最后生成射线表示法的一种.P(t) = P0 + t*D.P0是原点,D是方向,t是沿方向D前进的长度.

  经过上面我们知道,通过GetCameraToViewportRay生成的射线是世界坐标系下的,相应的运算在同一坐标系下算才有意义.下面是点选择的效果.

  如图上,八个点是一个Mesh,可以分别设置大小,颜色等,具体设计请看上一篇.点的选择比较简单,把点当然一个球形,用Ogre自带的射线与球相交检查就可.

        public void HitTest(Ray ray)
        {
            foreach (var p in this)
            {
                Sphere sphere = new Sphere(this.Parent.PointNode.FullTransform * p.VertexInfo.Position, this.Scale);
                var result = Utility.Intersects(ray, sphere);
                if (result.Hit)
                {
                    this.SelectPoint = p;
                    break;
                }
            }
        }

Point Hit

  这个算是比较简单的,只需要注意一点的,原来点是局部坐标系下的,需要剩以本节点下的局部坐标下,转换成世界坐标再与我们的Ray比较.

  下面是线的选择,同样虽然是多条线,可以不同的颜色,但是只有一个Batch.先看下效果图.

  具体算法见如下代码:

        /// <summary>
        /// 3D数学基础:图形与游戏开发 里的算法,二射线相交检测
        /// </summary>
        /// <param name="ray1"></param>
        /// <param name="ray2"></param>
        /// <param name="deviation">ray1与ray2最近距离误差范围</param>
        /// <returns></returns>
        public static Tuple<bool, Real, Real, Vector3, Vector3> RayIntersectsRay(this Ray ray1, Ray ray2, float deviation)
        {
            var OfO = ray2.Origin - ray1.Origin;
            var DxD = ray1.Direction.Cross(ray2.Direction);
            //平行
            if (DxD == Vector3.Zero)
            {
                //1重合,2不重合,重合也相当于hit.
                float limit = 0.00001f;
                var t = ray2.Origin.x - ray1.Origin.x;
                var v = ray1.Origin + ray1.Direction * t;
                //查看ray2.Origin是否在ray1.Origin射线中
                if (v.DifferenceLessThan(ray2.Origin, limit))
                {
                    return Tuple.Create(true, (Real)t, (Real)0, v, ray2.Origin);
                }
                return Tuple.Create(false, (Real)0, (Real)0, Vector3.Invalid, Vector3.Invalid);
            }
            else
            {
                var t1 = OfO.Cross(ray2.Direction).Dot(DxD) / DxD.Dot(DxD);
                var t2 = OfO.Cross(ray1.Direction).Dot(DxD) / DxD.Dot(DxD);

                var p1 = ray1.Origin + ray1.Direction * t1;
                var p2 = ray2.Origin + ray2.Direction * t2;
                bool bHit = (p1 - p2).Length < deviation;
                return Tuple.Create(bHit, t1, t2, p1, p2);
            }
        }

line hit

  这个算法是参照3D数学基础:图形与游戏开发里的算法,二射线相交检测.前面平行(分二种,一种重合,一种不重合)的情况没碰到,也就没测,我们主要看不平行的情况,也有二种,一种是在同一平面,一种是不在同一平面.在同一平面的情况不多,更多的是不在同一平面的情况,这种的话并不是就说相交不到,因为我们本来选择的是线段,在这以圆柱画出的,只要检测二射线相离最近的二点在一个范围内就算相交,这个具体的推导算法就不解释了,大家有兴趣可以去看下3D数学基础:图形与游戏开发,里面有完整的推导过程.不过上面明显还不完善,还要加上如下代码.  

       public void HitTest(Ray ray)
        {
            foreach (var line in this)
            {
                var p1 = this.Parent.LineNode.FullTransform * this.Parent[line.Index[0]];
                var p2 = this.Parent.LineNode.FullTransform * this.Parent[line.Index[1]];
                //3D数学基础:图形与游戏开发 里的算法,二射线相交检测
                //s1 ray s2 (p1 + (p2-p1)*t)
                Ray ray0 = new Ray(p1, (p2 - p1).ToNormalized());

                var hitResult = ray0.RayIntersectsRay(ray, this.Scale);
                if (hitResult.Item1)
                {
                    var t = hitResult.Item2;

                    if (t >= 0 && t <= (p2 - p1).Length)
                    {
                        this.SelectLine = line;
                        break;
                    }
                }
            }
        }

line hit seg

  因为我们选择的是线段,所以不仅要检测相交,还要检测交点是否在有效范围内.最后同上面,对应的点应该转化到世界坐标系下.

  最后是面,面就不发图了,直接放代码,流程和上面差不多.

       public void HitTest(Ray ray)
        {
            foreach (var surface in this)
            {
                foreach (var tri in surface.Triangles)
                {
                    var matrix = this.Parent.LineNode.FullTransform;
                    var p1 = matrix * this.Parent[tri.sharedVertIndex[0]];
                    var p2 = matrix * this.Parent[tri.sharedVertIndex[1]];
                    var p3 = matrix * this.Parent[tri.sharedVertIndex[2]];

                    var result = ray.RayIntersectsTriangle(p1, p2, p3);
                    if (result.Item1)
                    {
                        this.SelectSurface = surface;
                        break;
                    }
                }
            }
        }

       public static System.Tuple<bool, Vector3> RayIntersectsTriangle(this Ray ray, Vector3 a, Vector3 b, Vector3 c)
        {
            // Place the end beyond any conceivable triangle, 1000 meters away
            var start = ray.Origin;
            var end = start + ray.Direction * 1000000f;
            var pq = end - start;
            var pa = a - start;
            var pb = b - start;
            var pc = c - start;
            // Test if pq is inside the edges bc, ca and ab. Done by testing
            // that the signed tetrahedral volumes, computed using scalar triple
            // products, are all positive
            var result = Tuple.Create(false, Vector3.Invalid);
            float u = pq.Cross(pc).Dot(pb);
            if (u < 0.0f)
            {
                return result;
            }
            float v = pq.Cross(pa).Dot(pc);
            if (v < 0.0f)
            {
                return result;
            }
            float w = pq.Cross(pb).Dot(pa);
            if (w < 0.0f)
            {
                return result;
            }
            var denom = 1.0f / (u + v + w);
            // Finally fill in the intersection point
            var where = (u * a + v * b + w * c) * denom;
            return Tuple.Create(true, where);
        }

surface hit

  RayIntersectsTriangle算法是Axiom3D论坛里有人提供的,大家可以搜索下.

  经过上面点,线,面选择后,我们来完善前面没有完善的地方,在上面截图控件上方,手掌一样的东东用来移动模型,最开始直接用鼠标移动来对应物体的x,y移动,当然效果烂的要命,网上搜了一下,都没昨讲.没有办法,只有自己动手了,前面我们看到Ray的生成过程,鼠标点击后,这点映射成三维里的,x,y好确定,唯一没想通如何确定z值.后面在脑海里把视截体想了下,如果能确定我们要移动的目标在视截体距离近截面和远截面的位置(后面发现只要用到与近截面的距离,具体后面再来说),不就可以根据这个比例映射在ray上的长度,就是前面P(t) = P0 + t*D.这里面的t值.

  下面是相关代码,鼠标左键点下后,我们确定与近截面的距离,viewD.

if (EngineCore.Instance.ActionType != ActionType.None)
{
    EngineCore.Instance.Select = true;
    //hitD = this.Camera;
    this.viewZ = this.Camera.FrustumPlanes[0].GetDistance(node.DerivedPosition);

}

移动的目标与近截面的距离

  鼠标移动的代码:

        public override void MouseMove(MouseEventArgs e)
        {
            int offsetX = e.X - prePoint.X;
            int offsetY = e.Y - prePoint.Y;
            if (offsetX == 0 && offsetY == 0)
                return;
            if (Control.MouseButtons == MouseButtons.Left)
            {
                if (EngineCore.Instance.Select)
                {
                    var node = EngineCore.Instance.ViewNode;
                    if (EngineCore.Instance.ActionType == ActionType.Translate)
                    {
                        var ray = this.CreateViewportRay(e.X, e.Y);

                        var pos = ray.Origin + ray.Direction * this.viewZ;
                        node.DerivedPosition = pos;
                    }
                    else if (EngineCore.Instance.ActionType == ActionType.Rotate)
                    {
                        node.Yaw((Real)(new Degree((Real)(offsetX * 0.15f))));
                        node.Pitch((Real)(new Degree((Real)(offsetY * 0.15f))));

                    }
                    else if (EngineCore.Instance.ActionType == ActionType.Scale)
                    {

                    }
                }
                else
                {
                    Real dist = (this.camera.Position - EngineCore.Instance.ViewNode.DerivedPosition).Length;

                    this.camera.Position = EngineCore.Instance.ViewNode.DerivedPosition;
                    this.camera.Yaw((Real)(new Degree((Real)(-offsetX * 0.25f))));
                    this.camera.Pitch((Real)(new Degree((Real)(-offsetY * 0.25f))));
                    this.camera.MoveRelative(new Vector3(0, 0, dist));

                    EngineCore.Instance.AxisNode.Yaw((Real)(new Degree((Real)(-offsetX * 0.25f))));
                    EngineCore.Instance.AxisNode.Pitch((Real)(new Degree((Real)(-offsetY * 0.25f))));

                }
            }
            this.renderWindow.Update();
            prePoint = e.Location;

            base.MouseMove(e);
        }

鼠标移动事件

  结合前面讲解Ray的生成过程,我们知道,ray.Origin是鼠标点击的近截面上的点,而Direction是方向,因为已经归一化,也就是说这个是单位向量,那么我们只需要知道移动的目标与近截面的距离,就能得到我们点击z值了.

  效果就是鼠标左键按下是确定与近截面的距离,然后移动的时候,在视截体里,x,y是变化的,z固定的.测试了一下,效果不错,很满意,如我视点移到Y轴上,与x,z面垂直,就能保证移动物体在x,z面,根据不同的视角移动,现实感还是很强,也没有感觉不合理的位置.

  旋转就比较简单了,直接把鼠标的二维坐标映射过去就成,效果也还行.代码也在上面,不单独说了,缩放感觉放鼠标滚轮处理比较好,暂时不管了,就个很容易.最后是没选择物体是,摄像机会围绕模型旋转,具体实现大家看下代码就明白了.

  新的一年第一天,确定今年主要自学内容,C++与Ogre,在这个项目完成后,主要重新啃C++,最好能用Ogre实现一些功能.

  

时间: 2024-10-11 01:06:40

Axiom3D:Ogre射线与点,线,面相交,鼠标操作3维空间.的相关文章

Axiom3D:Ogre中Mesh网格分解成点线面。

这个需求可能比较古怪,一般Mesh我们组装好顶点,索引数据后,直接放入索引缓冲渲染就好了.但是如果有些特殊需要,如需要标注出Mesh的顶点,线,面这些信息,以及特殊显示这些信息. 最开始我想的是自己分析Mesh里的VertexData与IndexData,分析顶点时查找源码发现Ogre里本身有相关的类,这里Axiom3D与Ogre的源码有些区别,不过大致意思相同. 主要用到的类:EdgeListBuilder,CommonVertexList,EdgeData. 流程很简单,EdgeListBu

Axiom3D:Ogre地形代码解析

大致流程. 这里简单介绍下,Axiom中采用的Ogre的地形组件的一些概念与如何生成地形. 先说下大致流程,然后大家再往下看.(只说如何生成地形与LOD,除高度纹理图外别的纹理暂时不管.) 1.生成TerrainGroup,增加Request与Response处理,设置大小,高度图. 比较重要的属性是DefaultImportSettings(ImportData),包含地形的大小,分块最大与最小值, 2.TerrainGroup生成与设置各地形Terrain块的大小,高度图,ImportDat

Axiom3D:Ogre公告板集与合并批次

在上文中,我们把Ogre里的网格分解成点线面后,我们要完成一个新的功能,在点上突出显示. 得到顶点位置后,这个功能也就是一个很简单的事,最开始是每个顶点添加一个子节点,节点上添加一个圆点. foreach (var vect in this.Points) { var cEntity = EntityBuilder.Instance.SphereX1.Clone(Guid.NewGuid().ToString()); cEntity.UserData = vect; var cNode = th

visio直线交叉相交跨线修改

在使用visio画流程图时,经常会遇到两条直线相交.下面讲如何修改使得相交点变成我们想要的方式. 可以设置如下: (1)  全局直线相交,设置跨线标志. (2)  对每条线进行相交跨线设置. (一) 全局设置. 首先,画上交叉线.如下图. 选择直线--格式--行为.如图 有如下选项: 按页上指定(指对当页生效,有跨线) 从不(全部无跨线) 始终(全部有跨线) 对于其他线是始终的(选中线无效,选中线以外其他线有跨线) 对两者都不(选中线,以及交叉线,均无跨线) (三) 也可以通过 "文件"

转:Ogre的八叉树场景管理器OctreeSceneManager

上面是我绘制的一张图. 关于八叉树场景管理器主要需要关注两个类,其一是松散八叉树的数据结构Ogre::Octree,其二是八叉树场景管理器Ogre::OctreeSceneManager. 下面摘录图片中的文字: 松散八叉树的数据结构. 属性:其中mBox为其包围盒,mHalfSize定义为包围盒大小的一半.mChildren是一个大小为8的静态数组,里面保存了8个Octree指针,由八叉树场景管理器创建,由本类管理.mNodes为挂接到当前八叉树上面的八叉树场景节点,mNumNodes保存了挂

三维空间中线与三角形相交判定

——读Computer Graphics Principles and Practice 3rd Edition第七章时遇见课文正文和代码中的错误,作记. 本文旨在阐释一种算法,用于在三维空间中寻找某一线(ray)与某一三角形的交点.此算法是计算机图形学中的基础算法之一. 1.预设概念 为了阐释此算法,必须先引入一组预设概念,借以使用计算机语言来描述三维空间中的线与三角形. 我们首先给出这些概念的定义及数据结构. 1.1 点(Point3D) 我们使用笛卡尔坐标系 (Cartesian coor

【Unity 3D】学习笔记四十:射线

射线 射线,类比的理解就是游戏中的子弹.是在3D世界里中一个点向一个方向发射的一条无终点的线.在发射的过程中,一旦与其它对象发生碰撞,就停止发射. 射线的原理 创建一个射线时,首先须要知道射线的起点和终点在3D世界里的坐标. using UnityEngine; using System.Collections; public class Script_06_08 : MonoBehaviour { void Update() { //创建射线,从零点发射到对象 Ray ray = new Ra

PCB走线角度选择 — PCB Layout 跳坑指南

PCB走线角度选择 - PCB Layout 跳坑指南 PCB设计技巧 by xfire PCB Layout 跳坑指南 现在但凡打开SoC原厂的PCB Layout Guide,都会提及到高速信号的走线的拐角角度问题,都会说高速信号不要以直角走线,要以45度角走线,并且会说走圆弧会比45度拐角更好.狮屎是不是这样?PCB走线角度该怎样设置,是走45度好还是走圆弧好?90度直角走线到底行不行?这是老wu经常看见广大 PCB Layout 拉线菌热议的话题. 大家开始纠结于pcb走线的拐角角度,也

ArcMAp对线要素进行平滑处(打断)

一:工具简单介绍 -- ArcMAp10.1的高级编辑工具中提供了对线/面要素进行概括/平滑处理的工具. 概括工具.平滑工具分别例如以下:(首先得开启编辑状态 --- 才干够对要素的属性进行更改).选中某一个要素(如某一条地铁线)(下图的左側箭头有误,更正例如以下:标注ArcCatelog 实际是  文件夹 .ArcCatelog  左边的是内容列表:标注 内容列表的实际是搜索. 其它的不变  ) 二:概括/平滑示意图例如以下 watermark/2/text/aHR0cDovL2Jsb2cuY