文档章节

小窥探 3D 中射线投射(raycast)

ylme
 ylme
发布于 2017/03/26 13:06
字数 1477
阅读 71
收藏 1

这个代码片段是在 Unity 中编写 FPS 小游戏,判断玩家射击是否击中目标。首先获取从摄像机到屏幕中心的射线,然后查看射线与哪个 GameObject 相交。虽然就短短几行代码,却隐藏了一个问题:如何在三维世界中判断在屏幕上点击是否触碰了某片区域。下面就在 OpengGL 中具体使用 raycast(射线投射)。

void Update() {
	if (Input.GetMouseButtonDown(0)) {
		Vector3 point = new Vector3(_camera.pixelWidth/2, _camera.pixelHeight/2, 0);
		Ray ray = _camera.ScreenPointToRay(point);
		RaycastHit hit;
		if (Physics.Raycast(ray, out hit)) {
			GameObject hitObject = hit.transform.gameObject;
			// do something
		}
	}
}

3D 世界中点击某个物体

在屏幕上点击可以得到一个屏幕坐标,这就是我们的点击点。现在要判断这一点是否与 3D 世界中某个物体相交需要如下步骤。

  • 首先把 2D 屏幕坐标转换到 3D 坐标空间中。要做到这点,我们要把点击点投射到一条射线上。小学生都知道两点决定一条直线。现在有了屏幕上的点击点,还需要一个起点来决定这条射线,而这一点就是摄像机的位置。在 OpengGL 进行坐标变换时,可选择把 World Space 中的点变换到 View Space (也叫 Camera Space) 中。如下图,摄像机观察角度是向前方俯视。
  • 根据摄像机位置和屏幕坐标表示射线。这里统一在 World Space 中进行处理。因此需要把屏幕坐标逆转换到 World Space 中。
  • 最后,检查射线是否与物体相交。

raycast

3D 世界中物体的移动

经过上面分析,我们可以判断是否点击到了物体。如何想进一步拖动物体进行移动,该如何处理呢。这里我们选择让物体在 3D 中指定平面进行移动。鼠标拖动物体时,同判断是否点击到物体一样,这里我们判断移动鼠标时,鼠标的屏幕坐标与该平面的交点。然后移动物体到此交点。

具体的实现

example

如图,一共绘制两个四边形,红色的是 _obj 物体,蓝色的是平面 _plane 。只有点击到 _obj 时才绘制 _plane 。然后拖动 _obj_plane 中移动。完整的代码实现在 blogspinnet/opengl/raycast 目录中

  • 绘制。类 color_shader 是绘制用到的 shader 。类 quad 是被绘制的几何体,负责产生顶点坐标。绘制时由于 _obj 会与 _plane 重叠造成颜色闪烁,所以代码中用深度值解决这个问题。
_colorshader.useprogram();
    
// 先绘制背景,并不写入深度缓冲中,现在缓冲中的值仍都是最小值 1.0f ,这样之后的绘制就会覆盖背景,防止重叠造成闪烁。
glDepthMask(GL_FALSE);
if (_params.ishint) {
	_colorshader.setuniform(mvp, 0.0f, 0.0f, 1.0f);
	_plane.draw();
}
glDepthMask(GL_TRUE);

_colorshader.setuniform(mvp, 1.0f, 0.0f, 0.0f);
_obj.draw();
  • 产生从摄像机到屏幕点的射线。先把屏幕坐标转换到 NDC 坐标空间,然后计算 View Space 和 Perspective Projection 的逆矩阵将 NDC 转换到 World Space 中。
// 将屏幕像素坐标转换成 NDC 坐标。屏幕坐标的原点是屏幕左上角。而 NDC 坐标原点是屏幕中心,y 轴指向屏幕上方。
static void
turn_screencoord_to_ndc(int width, int height, int x, int y, float *nx, float *ny) {
	  *nx = (float)x/(float)width * 2 - 1;
	  *ny = -((float)y/(float)height * 2 - 1); // reverte y axis
}

// 要想知道在窗口上点击时,是否点击到三维空间中的某个物体,需要先把窗口上点击时的二维坐标转换到三维空间,
// 具体会被转换成三维空间中的一条直线,然后判断该直线是否与物体相交。下面就是具体的实现。
// 把一个 2d ndc 转换成世界坐标空间中的一条射线。
static void
normalized2d_to_ray(float nx, float ny, glm::vec3 &raypos, glm::vec3 &raydir) {
	// 世界坐标 - 视图矩阵 - 透视矩阵 - 透视除法 - ndc
	// 要得到反转得到世界坐标,先需要视图矩阵和透视矩阵的反转矩阵

	// ndc 坐标系是左手坐标系,所以近平面的 z 坐标为远平面的 z 坐标要小
	glm::vec4 nearpoint_ndc(nx, ny, -1, 1);
	glm::vec4 nearpoint_world = _params.inverse_mvp * nearpoint_ndc;

	// 消除矩阵反转后,透视除法的影响
	nearpoint_world /= nearpoint_world.w;
	
	raypos = glm::vec3(nearpoint_world);
	raydir = raypos - _params.camerapos;
	raydir = glm::normalize(raydir);
}
  • 判断相交。类 quad 提供了相交判断函数。这里采用 GLM 数学库提供的函数,简单处理了相交判断。成员函数 isintersect 判断射线是否与 quad 相交。quad 就是由两个三角形绘制,因此判断是否与两个三角形相交。成员函数 intersect 判断射线与 quad 所在平面的交点。
bool
isintersect(const glm::vec3 &raypos, const glm::vec3 &raydir) {
	glm::vec3 barypos;
	if (glm::intersectRayTriangle(raypos, raydir, _vertices[0], _vertices[1], _vertices[2], barypos))
		return true;
	if (glm::intersectRayTriangle(raypos, raydir, _vertices[1], _vertices[3], _vertices[2], barypos))
		return true;
	return false;
}

bool
intersect(const glm::vec3 &raypos, const glm::vec3 &raydir, glm::vec3 &barypos) {
	float dist;
	glm::vec3 v1(_vertices[2] - _vertices[1]);
	glm::vec3 v2(_vertices[0] - _vertices[1]);
	glm::vec3 normal = glm::normalize(glm::cross(v1, v2));
	if (glm::intersectRayPlane(raypos, raydir, _center, normal, dist)) {
		barypos =  raypos + raydir * dist;
		return true;
	} else {
		return false;
	}
}

最后

之前写过一篇关于 OpenGL 坐标变换 的文章。本篇文章在现实中具体应用了坐标变换。如下坐标变换代码,透视投影时 frustum(视锥体)的中心点是位于原点,这和 View Space 中的摄像机位置是两码事情,View Space 中的坐标原点就是摄像机所在位置。因此选取射线的起点时应选择摄像机作为起点。

glm::mat4 viewmat = glm::lookAt(_params.camerapos, glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
glm::mat4 projmat = glm::perspective(45.0f, (float)w / (float)h, 1.0f, 100.0f);
glm::mat4 mvp = projmat * viewmat;

Unity 中的摄像机位置又是 frustum 的中心点,这点肯定是内部多了一层处理。所以不要把 Unity 中 frustum 的中心点与上面这个代码片段 frustum 的中心点搞混了。

© 著作权归作者所有

共有 人打赏支持
ylme
粉丝 10
博文 40
码字总数 41754
作品 0
广州
程序员
[Unity小技巧] 使用射线Raycast判断某个方向是否有碰撞体

我们做游戏开发的时候,可能会遇到类似这样的一个问题:一个物体加上了Rigidbody刚体受重力下降,在它下降的时候(还没与下方物体产生碰撞),我们需要提前知道它的下方是否有碰撞体(能接住...

qq_33000225
2017/02/15
0
0
58 Three.js 通过THREE.Raycaster给模型绑定点击事件

简介 由于浏览器是一个视口,而在里面显示的内容是场景,所以,现在有一个问题就是如何将视口的和坐标转换成场景中的坐标。好在已经有了解决相关问题的方案,那就是射线,用于鼠标拾取(计算...

qq_30100043
01/14
0
0
Unity Physics.Raycast 射线检测

在游戏中,我们常常要用到鼠标来控制物体的移动或是鼠标拾取某个物体。还有射击游戏里,子弹打中靶子......这些都需要Physics.Raycast和ray。可见他们的重要性。 首先说说射线; 射线是3D世界...

海灬未眠
2016/10/18
0
0
Unity 射线碰撞检测

项目起因: 前段时间项目需要完成一个点击,拖动,缩放功能。并没有使用Ngui或者Ugui自带的手势识别的功能,是直接在Update中处理了这些事务。但是后来根据项目需求添加了一个按钮(其实为一个...

Lohanry
2016/12/21
72
0
Hierarchy视图里的Transform和Camera组件

Hierarchy视图里的Transform和Camera组件 在Hierarchy视图里,选中Camera,然后在Inspector视图里查看其各组件,如图1-8所示。对于Transform和Camera组件,对于使用过Unity的读者来说再熟悉不...

大学霸
2015/04/10
0
0

没有更多内容

加载失败,请刷新页面

加载更多

VS code编辑器安装 php7.2 NTS 版本 Xdebug

本文使用的是 phpstudy 一键安装包(windows32位) php 版本是 php7.2 NTS 1、在 phpstudy 面板中开启 phpdebug 扩展 // 其他选项菜单-> php 扩展与设置-> php 扩展 2、官方下载 Xdebug // ...

削个椰子皮_给个梨
22分钟前
1
0
Swagger中配置了@ApiModelProperty的allowableValues属性但不显示的问题

现在用Swagger来生成API文档的例子已经非常多了,今天碰到开发同事问了一个问题,帮着看了一下,主要还是配置方法的问题,所以记录一下。如果您也碰到了同样的问题,希望本文对您有用。 问题...

程序猿DD
52分钟前
2
0
sql 命令

show variables like '%general%'; show variables like '%log_output%'; show variables like '%quer%'; show global status like '%slow%';...

JavaSon712
今天
2
0
Django修改默认数据库引擎

Django默认数据库引擎为sqlite3,除了sqlite3,还支持postgresql、mysql、oracle 配置如下:其中postgresql_psycopg2为postgresql的适配器。 'django.db.backends.postgresql' 'django.db.bac......

MichaelShu
今天
0
0
动画源码解析

目录介绍 1.Animation和Animator区别 2.Animation运行原理和源码分析 2.1 基本属性介绍 2.2 如何计算动画数据 2.3 什么是动画更新函数 2.4 动画数据如何存储 2.5 Animation的调用 3.Animator...

潇湘剑雨
今天
5
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部