Unity中的离轴投影

请注意,在底部图像中,对象的几何形状是如何倾斜的。 这就是我们想要的效果。 通常,我们正面朝上观看监视器,因此轴向投影最有意义。 但是,请尝试将头移到显示器的上方和右侧,然后再次查看底部图像(您可能必须离显示器很近才能使效果起作用)。 突然,几何形状再次看起来正常,因为投影补偿了您的观看位置。 那么我们怎样才能做到这一点呢?

投影平面

首先,我想要一种便捷的方法来可视化和修改我们正在查看的投影平面(虚拟屏幕/窗口)。 我创建了ProjectionPlane组件,以使其更容易将相机代码与投影平面分开。 该类带有[ExecuteInEditMode]属性,因此我们可以看到编辑器中发生了什么。 我公开了一些属性:

现在让我们集中讨论“大小”和“长宽比”。 离轴投影只有在与正在显示的监视器的纵横比匹配时才看起来正确。 以下代码在Update()函数中运行,以便我们可以设置正在查看的平面的大小,并添加一些纵横比锁定以方便使用。

 如果(LockAspectRatio) 
{
if(AspectRatio.x!= previousAspectRatio.x)
{
Size.y = Size.x / AspectRatio.x * AspectRatio.y;
//如果两者都更改,则X优先
previousAspectRatio.y = AspectRatio.y;
}

if(AspectRatio.y!= previousAspectRatio.y)
{
Size.x = Size.y / AspectRatio.y * AspectRatio.x;
}

if(Size.x!= previousSize.x)
{
Size.y = Size.x / AspectRatio.x * AspectRatio.y;
//如果两者都改变​​,则X优先
previousSize.y = Size.y;
}

if(Size.y!= previousSize.y)
{
Size.x = Size.y / AspectRatio.y * AspectRatio.x;
}
}

//确保我们不会崩溃
Size.x = Mathf.Max(1,Size.x);
Size.y = Mathf.Max(1,Size.y);
AspectRatio.x = Mathf.Max(1,AspectRatio.x);
AspectRatio.y = Mathf.Max(1,AspectRatio.y);
  previousSize =大小; 
previousAspectRatio = AspectRatio;

您可以通过将SizeAspectRatio的值设置得太低(最终有效地打破投影矩阵)来最终使Unity崩溃,因此我对此有所防范。

接下来是我们需要传递给相机的矢量和矩阵(同样在Update()函数中)

  BottomLeft = transform.TransformPoint( 
新的Vector3(-Size.x,-Size.y)* 0.5f);
BottomRight = transform.TransformPoint(
新的Vector3(Size.x,-Size.y)* 0.5f);
TopLeft = transform.TransformPoint(
新的Vector3(-Size.x,Size.y)* 0.5f);
TopRight = transform.TransformPoint(
新的Vector3(Size.x,Size.y)* 0.5f);
  DirRight =(BottomRight-BottomLeft).normalized; 
DirUp =(TopLeft-BottomLeft).normalized;
DirNormal = -Vector3.Cross(DirRight,DirUp).normalized;

m = Matrix4x4.zero;
m [0,0] = DirRight.x;
m [0,1] = DirRight.y;
m [0,2] = DirRight.z;

m [1,0] = DirUp.x;
m [1,1] = DirUp.y;
m [1,2] = DirUp.z;

m [2,0] = DirNormal.x;
m [2,1] = DirNormal.y;
m [2,2] = DirNormal.z;

m [3,3] = 1.0f;

我基本上是按照Kooima的文章来设置向量和矩阵,稍后我们将在相机代码中使用它们。 请注意,我将点从局部空间转换为世界空间。 还要注意,我计算了所有四个边界,这些边界在下面的Gizmo可视化中使用(在OnDrawGizmos()

  Gizmos.color = Color.red; 
Gizmos.DrawLine(BottomLeft,BottomRight);
Gizmos.DrawLine(BottomLeft,TopLeft);
Gizmos.DrawLine(TopRight,BottomRight);
Gizmos.DrawLine(TopLeft,TopRight);

//向眼睛绘制方向
Gizmos.color = Color.cyan;
var planeCenter = BottomLeft +(((TopRight-BottomLeft)* 0.5f);
Gizmos.DrawLine(planeCenter,planeCenter + DirNormal);

我还添加了一条朝相机应该所在的平面一侧绘制的小线。

投影相机

接下来是相机类。

  [ExecuteInEditMode] 
[RequireComponent(typeof(Camera))]
公共课ProjectionPlaneCamera:MonoBehaviour

我们需要能够看到相机在编辑器中的行为,因此可以看到[ExecuteInEditMode] 。 我们还确实需要一个Camera来在屏幕上显示某些内容,因此需要[RequireComponent(typeof(Camera)]属性,然后公开一些属性。

我们需要对ProjectionPlane的引用,这是我们相机的自定义投影和worldToCamera矩阵的基础。 然后我们来了解它的LateUpdate() ,即在LateUpdate()函数中设置的自定义矩阵。

  if(ProjectionScreen!= null) 
{
Vector3 pa = ProjectionScreen.BottomLeft;
Vector3 pb = ProjectionScreen.BottomRight;
Vector3 pc = ProjectionScreen.TopLeft;
Vector3 pd = ProjectionScreen.TopRight;

Vector3 vr = ProjectionScreen.DirRight;
Vector3 vu = ProjectionScreen.DirUp;
Vector3 vn = ProjectionScreen.DirNormal;
  Matrix4x4 M = ProjectionScreen.M; 

eyePos = transform.position;

//从眼睛到投影屏幕的各个角落
va = pa-eyePos;
vb = pb-eyePos;
vc = pc-eyePos;
vd = pd-eyePos;

viewDir = eyePos + va + vb + vc + vd;

//从眼睛到投影屏幕平面的距离
float d = -Vector3.Dot(va,vn);
如果(ClampNearPlane)
cam.nearClipPlane = d;
n = cam.nearClipPlane;
f = cam.farClipPlane;

浮动nearOverDist = n / d;
l = Vector3.Dot(vr,va)* nearOverDist;
r = Vector3.Dot(vr,vb)* nearOverDist;
b = Vector3.Dot(vu,va)* nearOverDist;
t = Vector3.Dot(vu,vc)* nearOverDist;
Matrix4x4 P = Matrix4x4.Frustum(l,r,b,t,n,f);

//平移到眼睛位置
Matrix4x4 T = Matrix4x4.Translate(-eyePos);


Matrix4x4 R = Matrix4x4.Rotate(
四元数反转(transform.rotation)* ProjectionScreen.transform.rotation);

cam.worldToCameraMatrix = M * R * T;

cam.projectionMatrix = P;
}

同样,我只是实现Kooima文章中的代码,并抛出一个观察方向向量以可视化场景中的焦点。 通过设置cam.worldToMatrix我们可以确保阴影计算正确无误,并且,作为额外的好处,相机的默认视锥Gizmo可以按预期工作。 ClampNearPlane标志设置我们是否将相机的近平面钳制为与投影平面完全相同。 大多数时候,这是最有用的近机。 但是,如果要在某些东西弹出屏幕的地方做一些立体效果,则可以关闭夹紧功能,并在相机和投影平面之间放置几何形状。 这种几何形状的行为就像在用户和屏幕之间一样,但前提是您有一种有效的立体显示方法来显示它。

设置层次结构

为方便起见,我将相机设置为投影平面的子代。 这样的好处是能够相对于平面移动摄像机,并且可以在连接摄像机的情况下左右移动投影平面。 将摄像机移动到默认位置是一个很好的经验法则,只需将其沿Z方向从投影平面偏移(沿投影平面Gizmo上的辅助线)即可。 这样,默认视图(在将运动应用于相机之前)是轴上投影。

校准和校准立方体

您可能已经注意到,在投影平面属性中,我创建了一个名为Alignment的小节,其中包含对对齐立方体的引用。 除非设置了“ 显示对齐多维数据集”标志,否则在应用程序启动时将创建并隐藏此对齐多维数据集。 设置它的代码位于ProjectionPlane类的Start()中。

 如果(Application.isPlaying) 
{
alignmentCube = new GameObject(“ AlignmentCube”);
alignmentCube.transform.SetParent(transform,false);

alignmentCube.transform.localPosition = Vector3.zero;
alignmentCube.transform.rotation = transform.rotation;

GameObject back = CreateAlignmentQuad();
backTrans = back.transform;
剩下的GameObject = CreateAlignmentQuad();
leftTrans = left.transform;
GameObject权限= CreateAlignmentQuad();
rightTrans = right.transform;
GameObject top = CreateAlignmentQuad();
topTrans = top.transform;
GameObject bottom = CreateAlignmentQuad();
bottomTrans = bottom.transform;

}

这仅创建5个四边形并存储其变换。 我还将材质设置为具有以下纹理的不发光纹理(值得注意的是,网格周围有白色边框)

每次更新都会对Alignment多维数据集进行缩放,以确保可以调整投影平面的大小,并且仍将多维数据集与摄影机视图对齐。 以下代码在Update()运行

 公共无效UpdateAlignmentCube() 
{
Vector2 halfSize =大小* 0.5f;
UpdateAlignmentQuad(backTrans,
新的Vector3(0,0,AlignmentDepth),
新的Vector3(Size.x,Size.y),Quaternion.identity);
UpdateAlignmentQuad(leftTrans,
新的Vector3(-halfSize.x,0,AlignmentDepth * 0.5f),
新的Vector3(AlignmentDepth,Size.y,0),
Quaternion.Euler(0,-90,0));
UpdateAlignmentQuad(rightTrans,
新的Vector3(halfSize.x,0,AlignmentDepth * 0.5f),
新的Vector3(AlignmentDepth,Size.y,0),
Quaternion.Euler(0,90,0));
UpdateAlignmentQuad(topTrans,
新的Vector3(0,halfSize.y,AlignmentDepth * 0.5f),
新的Vector3(Size.x,AlignmentDepth,0),
四元数.Euler(-90,0,0));

UpdateAlignmentQuad(bottomTrans,
新的Vector3(0,-halfSize.y,AlignmentDepth * 0.5f),
新的Vector3(Size.x,AlignmentDepth,0),
Quaternion.Euler(90,0,0));
}

private void UpdateAlignmentQuad(变换t,Vector3 pos,Vector3比例尺,四元数旋转)
{
t.localPosition = pos;
t.localScale =规模;
t.localRotation =旋转;
}

那么,我们将这个多维数据集用于什么呢? 好吧,这对于可能会有偏移的跟踪器(我们将在后面讨论)很有帮助。 对于我们的安装,我们将4个杆连接到显示器的角部,以便当您从各个角度看到对准立方体时,透视线应始终与直接指向显示器外部的角杆对齐(例如显示器的法线) 。

追踪器和相机运动

为了测试相机的移动和投影,我为跟踪器添加了一个基类,如下所示

 公共类TrackerBase:MonoBehaviour 
{
[HideInInspector]
公共布尔IsTracking {get; 保护集; }
[HideInInspector]
公共ulong TrackedId {get; 保护集; }
[HideInInspector]
公共Vector3翻译{get => translation; }
[HideInInspector]
公众持票数SecondsHasBeenTracked {get; 保护集; }

Vector3受保护的翻译;
}

我还通过以下课程对相机进行了快速移动,以进行测试

 公共类AutoMoveTracker:TrackerBase 
{
[最小(0.001f)]
公共浮动BoundsSize = 2;

[Range(0,1)]
公众持股XMovement = 0.5f;
[Range(0,1)]
公众持股量YMovement = 0.3f;
[Range(0,1)]
公众持股量ZMovement = 0;

私人浮点HalfBoundSize => BoundsSize * 0.5f;

无效Start()
{
IsTracking = true;
}

无效Update()
{
if(Input.GetKeyUp(KeyCode.A)&&(Input.GetKey(KeyCode.LeftControl)|| Input.GetKey(KeyCode.RightControl)))
{
IsTracking =!IsTracking;
SecondsHasBeenTracked = 0;
}

if(IsTracking)
{
SecondsHasBeenTracked + = Time.deltaTime;
float xSize = XMovement * HalfBoundSize;
浮动ySize = YMovement * HalfBoundSize;
float zSize = ZMovement * HalfBoundSize;
translation.x = Mathf.Sin(SecondsHasBeenTracked)* xSize;
translation.y = Mathf.Sin(SecondsHasBeenTracked-(Mathf.PI * 2/3))* ySize;
translation.z = Mathf.Sin(SecondsHasBeenTracked)* zSize;
}

}
}

最后,我创建了一个小组件,该组件根据BaseTracker的输入来移动摄像机并将其添加到我的摄像机游戏对象中

  [RequireComponent(typeof(ProjectionPlaneCamera))] 
公共课BasicMovement:MonoBehaviour
{
公共跟踪器基本跟踪器;

私人ProjectionPlaneCamera projectionCamera;
私有Vector3 initialLocalPosition;

无效Start()
{
projectionCamera = GetComponent ();
initialLocalPosition = projectionCamera.transform.localPosition;
}

无效Update()
{
如果(Tracker == null)
返回;

if(Tracker.IsTracking)
{
projectionCamera.transform.localPosition = initialLocalPosition + Tracker.Translation;
}
}
}

该运动管理器有很大的改进空间,在最终版本的安装中使用的运动管理器具有处理代码。

  • 当跟踪一个以上的人时会发生什么
  • 当有人进入或离开跟踪区域时会发生什么
  • 平滑输入

注意事项和陷阱

我会提到在此过程中发现的一些陷阱。 希望您会在这里忽略我的所有建议,并亲自发现您的行为。

  • 天空盒-请勿使用它们。 在场景中使用几何作为背景。
  • 避免头痛,不要缩放ProjectionPlane变换。 它弄乱了相机的局部坐标空间,使跟踪变得不必要地复杂。
  • 当前,自定义投影矩阵不适用于新的Postprocessing v2堆栈。 查看https://docs.unity3d.com/ScriptReference/Camera-nonJitteredProjectionMatrix.html了解更多信息

下次

接下来,我想告诉您一些有关如何实现Kinect跟踪器的信息