摄像头目标标记使能有什么用(Unity开发:两种屏幕外目标点标记的实现方法)

Posted

篇首语:日日行,不怕千万里;常常做,不怕千万事。本文由小常识网(cha138.com)小编为大家整理,主要介绍了摄像头目标标记使能有什么用(Unity开发:两种屏幕外目标点标记的实现方法)相关的知识,希望对你有一定的参考价值。

摄像头目标标记使能有什么用(Unity开发:两种屏幕外目标点标记的实现方法)

前言

近期在做个人项目的时候,需要实现一个提示目标点位置的标记 UI。本以为是个相对简单的任务,但研究后发现还是有不少隐藏路障。本文将介绍两种不同的屏幕外目标点标记的实现方式,分别对应《守望先锋》及大部分第一人称游戏。


项目演示链接:https://github.com/Quin7et/OffScreenObjectiveMarker

屏幕内标记

在 Unity 中,要将 UI 摆放在屏幕内标记的位置十分简单,用 Unity 相机自带的 WorldToScreenPoint()方法即可。一个非常简易的实现如下:

public class ObjectiveMarker : MonoBehaviour

public Transform TargetTransform;
public Image img;

private void LateUpdate()

img
.transform.position = Camera.main.WorldToScreenPoint(TargetTransform.position);

效果如图:

红色球为目标点,绿色方块为标记


这里的 Canvas 使用的是默认的 Screen Space Overlay,目标点标记用一个 Image 组件表示

WorldToScreenPoint(),顾名思义,输入值为 Vector3 世界坐标,输出为屏幕坐标。左下角为原点,1 单位代表 1 个像素,例如:1920*1080 分辨率下,屏幕中心点的坐标为[960, 540, z]。这里的 z 值是世界坐标到相机平面的距离。注意虽然返回值 1 单位对应 1 像素,但该值不一定是整数。

一行代码就能实现标记,但存在一个问题:背对目标点的时候,标记也会显示。如下图:


WorldToScreenPoint()的实现基本上可以概括为:将世界空间中的点先转换到相机空间,然后通过投射矩阵转换到模型空间——一个以原点为中心点的 2x2x2 的立方体。来自 unity 论坛的实现如下:

https://answers.unity.com/questions/1014337/calculation-behind-cameraworldtoscreenpoint.html

Vector3 manualWorldToScreenPoint(Vector3 wp) 
// calculate view-projection matrix
Matrix4x4 mat = cam.projectionMatrix * cam.worldToCameraMatrix;

// multiply world point by VP matrix
Vector4 temp = mat * new Vector4(wp.x, wp.y, wp.z, 1f);

if (temp.w == 0f)
// point is exactly on camera focus point, screen point is undefined
// unity handles this by returning 0,0,0
return Vector3.zero;
else
// convert x and y from clip space to window coordinates
temp
.x = (temp.x/temp.w + 1f)*.5f * cam.pixelWidth;
temp
.y = (temp.y/temp.w + 1f)*.5f * cam.pixelHeight;
return new Vector3(temp.x, temp.y, wp.z);

由于WorldToScreenPoint()并不在乎物体是否在 view frustum 中,不在屏幕上的物体也会被映射。从 projection matrix 可以看出:


其中注意:

temp.x = (temp.x/temp.w + 1f)*.5f * cam.pixelWidth;
temp
.y = (temp.y/temp.w + 1f)*.5f * cam.pixelHeight;

这两行将 projection matrix 转换后得到的 xy 值除以了 w 值(即-z)。这是为了让已经是齐次向量的 temp 落在前述模型空间内。由于除以 z 值将 xy 值的符号反转,上面的动图中可以看到以相机为中心点的中心对称效果。不想要标记在玩家背后出现的话,我们需要就 WorldToScreenPoint()z 值做一定处理。一种简单的做法是,在 z 值为负——也就是目标点在相机平面后方的时候,不进行位置更新:

if (newPos.z < 0) return;

这样,屏幕内标记就完成了。

屏幕外标记,方法 1

常见的屏幕外标记可以参考下图:

注意 UI 边缘的目标点提示。这些目标点均不在相机视野内。标记会根据目标点在视野外的方向调整其在屏幕边缘的位置。同时,标记会呈现在定义好的边界框中,不会覆盖其他 UI 元素。

有了上一节实现的屏幕内目标点,我们可以试试用Clamp()直接将标记坐标固定在边界框内。此处的 offset 均为像素值。

newPos.x = Mathf.Clamp(newPos.x, offsetLeft, Screen.width - offsetRight);
newPos
.y = Mathf.Clamp(newPos.y, offsetDown, Screen.height - offsetUp);

看起来似乎工作良好,但在接近边缘时,标记出现了一些奇怪的行为:

注意边缘处标记的上下移动


当相机和目标不在同一 y 平面上时,标记似乎会在屏幕边缘先靠近一个角落,再从屏幕对角出现。这是为什么呢?我们可以让相机围绕 y 轴旋转,log 一下 newPosClamp 前的值:


可以看到,随着相机旋转以及相机平面靠近目标点,z 值越来越小,屏幕投影的值越来越大。上一节提到,转换过程中 xy 值需要除以 w(即-z),当目标点过于接近相机平面,整个向量就需要除以 0。实际上,如果目标点完美处于相机平面上,newPos 将会返回零向量;但实际游戏中,玩家几乎不可能通过操作实现这一情景,所以我们可以不对此进行边缘处理。标记在对角而不是邻角出现(如右下到左上,而不是右下到左下)则是因为 z 值正负的翻转在除以 -z 时转移到了 xy

xy 过大时,Clamp 就只能将其限制在屏幕的一角,这不是我们想要的。我们想要的效果是,标记在屏幕边缘的位置指向视角需要旋转的方向。注意截图中,即便在接近 90°的位置,xy 依然保持了一个比例。观察下图:


我们希望屏幕边缘的标记经过屏幕中心和目标点屏幕空间坐标的连线,这样它就能正确表示玩家需要移动准星的方向。单纯使用 Clamp 无法达成此效果,需要计算连线和屏幕边缘(限制区边缘)的交点。此处可以使用线段交点算法,但由于限制区的四边都平行于坐标轴,且直线过屏幕中心,用斜率表示法比较直观。

private Vector3 KClamp(Vector3 newPos)

Vector2 center = new(Screen.width / 2, Screen.height / 2);
float k = (newPos.y - center.y) / (newPos.x - center.x);

if (newPos.y - center.y > 0)

newPos
.y = Screen.height - offsetUp;
newPos
.x = center.x + (newPos.y - center.y) / k;

else

newPos
.y = offsetDown;
newPos
.x = center.x + (newPos.y - center.y) / k;


if (newPos.x > Screen.width - offsetRight)

newPos
.x = Screen.width - offsetRight;
newPos
.y = center.y + (newPos.x - center.x) * k;

else if (newPos.x < offsetLeft)

newPos
.x = offsetLeft;
newPos
.y = center.y + (newPos.x - center.x) * k;


return newPos;

上述方法将任意点根据坐标与准星的相对方向限制在定义的边界框上。注意 offset 的值不要超过屏幕中心,因为该方法也会将边界框内部的点强行外推。此外,该方法仅在标记应该处在屏幕外的情况下使用。

现在来解决目标点在屏幕平面后方的情况。上文提到在 z=0 时,xy 值会翻转。一个绕过该问题的简单方法是,检测到目标在屏幕后时,将目标点投射到屏幕平面前方:

Vector3 delta = TargetTransform.position - camTransform.position;
float dot = Vector3.Dot(camTransform.forward, delta);

if (dot < 0)

Vector3 projectedPos = camTransform.position + (delta - camTransform.forward * Vector3.Dot(camTransform.forward, delta) * 1.01f);
newPos
= Camera.main.WorldToScreenPoint(projectedPos);

else

newPos
= Camera.main.WorldToScreenPoint(TargetTransform.position);

检测目标点是否在屏幕后方也可以用屏幕空间坐标的 z 值或者相机空间坐标的 z 值,不过,因为投射本身会用到点积,这里复用了点积的值,减少一次 WorldToScreenPoint()调用。

最终效果见下图:

屏幕外标记,方法 2

方法 1 较为常见,采用此方法的游戏包括《战地 2042》《彩虹六号:围攻》《使命召唤:现代战争》《耻辱》等许多第一人称游戏。但这种一定程度上近似表示准星最短移动路径的标记,可能更适合 3 轴旋转的飞行模拟游戏,而不是 2 轴旋转、且仰角固定在-90°~90°之间的第一人称游戏。

例如,在完全背对目标点时,标记可能处于屏幕上边缘或下边缘,但第一人称角色抬头或低头受限,仍然需要水平旋转镜头才能看到后方,而飞行类游戏则可以通过不受限的俯仰直接瞄准目标点。

《守望先锋 2》的标记则为第一人称游戏进行了特化。背对目标点时,标记的 y 坐标也会忠实表示准星需要处于的仰角,而不会游离在屏幕上下边缘。


要实现这一行为,我们需要一种新的投射方式。既然处于同一以相机为起点的射线上的坐标,其透视变换后的屏幕坐标都相同,那么我们可以直接根据目标点与相机的角度,将目标点标准化为角度相同的单位向量,然后根据需求来限定角度的范围。

private void Method2()

Transform camTransform = Camera.main.transform;

var vFov = Camera.main.fieldOfView;
var radHFov = 2 * Mathf.Atan(Mathf.Tan(vFov * Mathf.Deg2Rad / 2) * Camera.main.aspect);
var hFov = Mathf.Rad2Deg * radHFov;

Vector3 deltaUnitVec = (TargetTransform.position - camTransform.position).normalized;

/* How the angles work:
* vdegobj: objective vs xz plane (horizontal plane). Upright = -90, straight down = 90.
* vdegcam: camera forward vs xz plane. same as above.
* vdeg: obj -> cam. if obj is higher, value is negative.
*/


float vdegobj = Vector3.Angle(Vector3.up, deltaUnitVec) - 90f;
float vdegcam = Vector3.SignedAngle(Vector3.up, camTransform.forward, camTransform.right) - 90f;

float vdeg = vdegobj - vdegcam;

float hdeg = Vector3.SignedAngle(Vector3.ProjectOnPlane(camTransform.forward, Vector3.up), Vector3.ProjectOnPlane(deltaUnitVec, Vector3.up), Vector3.up);

vdeg
= Mathf.Clamp(vdeg, -89f, 89f);
hdeg
= Mathf.Clamp(hdeg, hFov * -0.5f, hFov * 0.5f);

Vector3 projectedPos = Quaternion.AngleAxis(vdeg, camTransform.right) * Quaternion.AngleAxis(hdeg, camTransform.up) * camTransform.forward;
Debug.DrawLine(camTransform.position, camTransform.position + projectedPos, Color.red);

Vector3 newPos = Camera.main.WorldToScreenPoint(camTransform.position + projectedPos);

if (newPos.x > Screen.width - offsetRight || newPos.x < offsetLeft || newPos.y > Screen.height - offsetUp || newPos.y < offsetDown)
newPos
= KClamp(newPos);

img
.transform.position = newPos;

此方法中,水平和垂直角度,分别是相机朝向与目标点朝向在世界 xz 平面和相机 yz 平面上的差值,依此投影出的目标点即可忠实地反映玩家准星需要进行的移动。效果如下图:

结语

以上就是本文介绍的两种屏幕外标记的实现方式。不同游戏在实现上会有些许不同,如《幽灵行动:断点》的标记限定范围使用的是椭圆而不是长方形(解出交点坐标即可实现),但基本原理几乎均为上文介绍的第一种方法。《守望先锋 2》属于少见的例外,不过由于其实现方式更符合(2 轴旋转)第一人称视角游戏的操作直觉,笔者认为有价值复现。

欢迎探讨!


*本文内容系作者独立观点,不代表 indienova 立场。未经授权允许,请勿转载。


<

相关参考

普通玻璃怎么印3c标志(每块玻璃上都有的标记 除了牌子LOGO外 还有什么有用的点?)

车上除了车头车尾有品牌LOGO,每块玻璃上也有品牌LOGO。除了品牌LOGO外,玻璃上还有一堆杂七杂八的信息。这些信息都是什么意思?了解一下有什么好处?在玻璃上,除了汽车厂家品牌以及玻璃厂家品牌外,还有一堆这样那样的...

摄像头里的水会自己干吗(Redmi K50 Ultra评测:国产屏幕反而成了最大加分)

...一台iPhone。或许五六千元的iPhone没有高刷屏幕、没有长焦摄像头,但iOS生态已经足以弥补这一切。不过Android厂商也在努力,开发一些价格不算太高,却能提供旗舰级体验的设备,RedmiK系列正是其中的佼佼者。今年初,K50/K50Pro发...

温控摄像机(2K高刷还护眼,这四款旗舰都有一块好屏幕)

...人用手机的时间越来越多,除手机外眼睛要么是盯着电脑屏幕看、要么是盯着电视看,给眼睛留下的休息时间越来越少。针对于此笔者挑选了四款高分辨率、高刷新率,同时性能也不错的手机推荐给大家,这些产品相比之下可以...

温控摄像机(2K高刷还护眼,这四款旗舰都有一块好屏幕)

...人用手机的时间越来越多,除手机外眼睛要么是盯着电脑屏幕看、要么是盯着电视看,给眼睛留下的休息时间越来越少。针对于此笔者挑选了四款高分辨率、高刷新率,同时性能也不错的手机推荐给大家,这些产品相比之下可以...

摄像机光学系统组成(怎么选动捕系统?选光学动捕还是惯性动捕?)

...数据处理服务器组成,摄像机被安装在一个工作空间内,摄像头追踪安装在人体身上的反射标记,通过多个不同位置的摄像机得到标记在人体身上的光点位置,推断在三维空间中的位

手机正常就是屏幕不亮(购机时如何判断屏幕清晰度,刷新率不是关键,主要看准以下4点)

手机屏幕是核心卖点之一,是继处理器和摄像头之后,厂商最喜欢宣传的卖点。毕竟影像配置是用户最看重的参数之一,一块清晰流畅的屏幕不仅能大大提升观感体验,质量过硬的话还可以直接裸机上路,无后顾之忧。所以如今...

智能语音机器人有用吗(ai智能外呼机器人好用吗?)

...碍沟通对答,通话过程同步录音记录,通话结束后可以对目标客户进行意向分类,让后续人员只需跟进回访,提升了整个公司的工作效率。更多详情咨询:15516191220同薇AI智能外呼机器人,适用于贷款、房地产、POS机、保险、电...

毛混地毯(想把猫毛变成数字化的毛绒地毯吗?Unity ArtEngine帮你实现)

“摄影制图法”乍一看令人望而却步,起初人们不了解,认为它很复杂、令人费解,但事实上,它可以成为艺术家的好帮手。摄影制图法是使用真实世界物体和环境的多张照片创作数字资源的过程。早在19世纪中期,它就已经出...

点读机对小学生有用吗

1、点读机对小学生还是很有用的,点读机可以迅速开发智力,锻炼大脑,激发潜能每个孩子都是天才!孩子的兴趣在于引导。2、点读机科学合理的方法,充分挖掘孩子的潜力,激发孩子的兴趣,让家教变成愉快的家庭活动!点读...

机器人仿真及视觉系统(基于虚幻引擎的机器人仿真开发)

随着机器人处理越来越复杂的任务,使用硬件原型开发机器人变得不切实际、危险或不可能。机器人专家越来越多地转向仿真,以获取有价值的数据,用于为其机器人设计硬件和软件组件,以及测试和验证其控制算法。模拟具有...