移动相机以适应 3D 场景

Posted

技术标签:

【中文标题】移动相机以适应 3D 场景【英文标题】:Move camera to fit 3D scene 【发布时间】:2011-02-21 10:06:33 【问题描述】:

我正在寻找一种算法来适应视口内的边界框(在我的例子中是 DirectX 场景)。我知道在正交相机中将边界球定心的算法,但对于边界框和透视相机需要相同的算法。我不能只更改 FOV,因为这个应用程序将 FOV 作为用户可编辑变量,所以它必须移动相机。

我掌握了大部分数据:

我有相机的向上矢量 我有边界框的中心点 我有从相机点到框中心的观察向量(方向和距离) 我已将这些点投影到垂直于相机的平面上,并检索了描述最大/最小 X 和 Y 坐标在观察平面内或外的系数。

我遇到的问题:

边界框的中心不一定在视口的中心(即投影后的边界矩形)。 由于视野“倾斜”投影(请参阅 http://en.wikipedia.org/wiki/File:Perspective-foreshortening.svg),我不能简单地使用系数作为比例因子来移动相机,因为它会超出/低于所需的相机位置

如何找到相机位置,使其尽可能完美地填充视口(如果纵横比远非 1.0,则只需填充屏幕轴之一)?

我尝试了其他一些方法:

使用边界球体和切线来找到移动相机的比例因子。这效果不好,因为它没有考虑透视投影,其次球体不适合我使用,因为我有很多扁平和长的几何形状。 重复调用函数以获得越来越小的相机位置误差。这有点奏效,但有时我会遇到奇怪的边缘情况,即相机位置过冲过多并且误差因素增加。此外,在执行此操作时,我没有根据边界矩形的位置重新定位模型。我找不到可靠、可靠的方法来做到这一点。

请帮忙!

【问题讨论】:

解决了类似的问题(在three.js中),使用实时代码sn-p:***.com/questions/37923651/… 【参考方案1】:

有许多可能的相机位置+方向,其中边界框适合视锥体。但是任何程序都会选择一个特定的相机位置和方向。

如果您考虑使用边界球,一种解决方案可能是

首先改变方向以查看边界球体中心 然后充分向后移动(负向观察方向)以使边界球体适合截锥体

使用边界框,您可以考虑先将相机放置在垂直于最大(或最小,无论您喜欢)立方体面的中心的位置。

我没有使用 DirectX 的经验,但是移动和更改相机的观察方向以使某个点居中应该很容易。 困难的部分是计算确定要移动多远才能查看对象。

数学

如果您知道对象在世界坐标中的边界大小s(我们对像素或相机坐标不感兴趣,因为它们取决于您的距离)从相机的方向,您可以计算所需的距离如果您知道透视投影的 x 和 y 视场角a,则相机的d 到边界形状。

     frustum      ------            
            ------    *****          -  
       -----          *   *          |
   -===     ) FOV a   *bounding box  | BB size s
camera -----          *   *          |
            ------    *****          -
                  ------
  
  |-------------------|
        distance d

所以,math 是 tan(a/2) = (s/2) / d => d = (s/2) / tan(a/2) 这将为您提供相机应放置在最近边界表面的距离。

【讨论】:

澄清一下:S 是沿最长轴的尺寸,A 是同一轴的 FOV(因为 FOV 因纵横比而异)? 另外:如何最简单地使边界矩形居中?与边界球不同,查看边界框的中心不会产生居中的边界矩形,并且由于透视,仅测量边界矩形与屏幕坐标的偏移量并不能为我提供移动相机的良好标量. 第一个问题:是的,没错。第二个问题:这就是为什么我建议首先将相机直接垂直于其中一个 BB 表面。从那个位置开始,那个表面(即使是透视图)将是约束 BB 形状。【参考方案2】:

我知道上面有一些很好的答案,但我想添加一个非常简单的解决方案来适应相机视锥体内的边界球体。它假设您希望保持相机 Target 和 Forward 矢量相同,并简单地调整相机与目标的距离。

注意,这不会给你最好的拟合,但它会给你一个近似的拟合,显示所有的几何图形,而且只需要几行代码,并且没有屏幕到世界的转换

// Compute camera radius to fit bounding sphere
// Implementation in C#
// 

// Given a bounding box around your scene
BoundingBox bounds = new BoundingBox();

// Compute the centre point of the bounding box
// NOTE: The implementation for this is to take the mid-way point between 
// two opposing corners of the bounding box
Vector3 center = bounds.Center;

// Find the corner of the bounding box which is maximum distance from the 
// centre of the bounding box. Vector3.Distance computes the distance between 
// two vectors. Select is just nice syntactic sugar to loop 
// over Corners and find the max distance.
double boundSphereRadius = bounds.Corners.Select(x => Vector3.Distance(x, bounds.Center)).Max();

// Given the camera Field of View in radians
double fov = Math3D.DegToRad(FieldOfView);

// Compute the distance the camera should be to fit the entire bounding sphere
double camDistance = (boundSphereRadius * 2.0) / Math.Tan(fov / 2.0);

// Now, set camera.Target to bounds.Center
// set camera.Radius to camDistance
// Keep current forward vector the same

在 C# 中 BoundingBox 的实现如下所示。重点是 Center 和 Corners 属性。 Vector3 是一个非常标准的 3 分量 (X,Y,Z) 向量实现

public struct BoundingBox
        
    public Vector3 Vec0;
    public Vector3 Vec1;

    public BoundingBox(Vector3 vec0, Vector3 vec1)
    
        Vec0 = vec0;
        Vec1 = vec1;
    

    public Vector3 Center
    
        get  return (Vec0 + Vec1)*0.5; 
    

    public IList<Vector3> Corners
    
        get
        
            Vector3[] corners = new[]
            
                new Vector3( Vec0.X, Vec0.Y, Vec0.Z ), 
                new Vector3( Vec1.X, Vec0.Y, Vec0.Z ), 
                new Vector3( Vec0.X, Vec1.Y, Vec0.Z ), 
                new Vector3( Vec0.X, Vec0.Y, Vec1.Z ), 
                new Vector3( Vec1.X, Vec1.Y, Vec0.Z ), 
                new Vector3( Vec1.X, Vec0.Y, Vec1.Z ), 
                new Vector3( Vec0.X, Vec1.Y, Vec1.Z ), 
                new Vector3( Vec1.X, Vec1.Y, Vec1.Z ), 
            ;

            return corners;
        
     

【讨论】:

我认为在计算“camDistance”时不应将“boundSphereRadius”乘以 2。【参考方案3】:

既然你有一个边界框,你应该有一个描述它的方向的基础。似乎您想将相机放置在与描述盒子最小尺寸的基向量一致的线上,然后滚动相机以使最大尺寸是水平的(假设您有 OBB 而不是 AABB)。这假设纵横比大于 1.0;如果不是,您将要使用垂直尺寸。

我会尝试什么:

    找出最小的盒子尺寸。 找到相关的基向量。 将基向量缩放为距相机应位于框中心的距离。这个距离就是boxWidth / (2 * tan(horizontalFov / 2))。注意boxWidth是盒子最大尺寸的宽度。 将摄像头放在boxCenter + scaledBasis 并注视boxCenter。 如有必要,滚动相机以将相机的向上矢量与相应的框基矢量对齐。

编辑:

因此,我认为您的意思是,您将相机放在任意位置看某处,而 AABB 在另一个位置。在不将相机移动到盒子的一侧的情况下,您想要:

查看框的中心 沿其外观矢量平移相机,以使框占据最大的屏幕空间

如果是这种情况,您将需要做更多的工作;这是我的建议:

    旋转相机以查看边界框的中心。 将框的所有点投影到屏幕空间中,并在屏幕空间中找到最小/最大边界框(您已经有了这个)。 现在Unproject 屏幕空间边界框的两个相对角进入世界空间。对于 Z 值,请使用您的 AABB 到相机的最近世界空间点。 这应该会为您提供一个面向相机的世界空间平面,位于 AABB 上最靠近相机的点。 现在使用我们现有的侧向方法将相机移动到适当的位置,将此平面视为盒子的一侧。

【讨论】:

这些盒子是 AABB,但这并不重要。我将边界矩形投影到垂直于相机的平面上。似乎您正试图通过“隐藏”透视矩阵完成的变形来围绕该问题进行设计。我对吗?该解决方案对我不起作用。我需要以与“缩放以适应”命令之前相同的角度查看它。我正在寻找的是当我沿着 Camera-Z 移动时如何考虑边界矩形的非线性缩放。 我的答案与 catchmeifyoutry 的答案基本相同(我们的距离数学完全相同)。使用我们的方法,通过将相机放置在距离盒子侧面适当的距离来处理透视。 我同意 Ron 的观点,我们基本上提出了相同的方法。调整方法是可能的,例如计算与相机坐标系对齐的新 BB,或者 Ron 稍后建议的方法(+1)。 我喜欢您将当前 AABB 转换为相机空间并构建相机轴对齐 BB 的想法;这将重用现有的 AABB 代码,并且不需要在屏幕空间/返回之间反复跳跃。 是的,看起来编辑中的第 3 步就是我想要的。如果我需要使边界矩形居中(即通过平移相机),这不应该干扰 Unproject,对吗?下周我将在办公室进行测试,如果可行,将其标记为正确答案。【参考方案4】:

我现在手头没有,但你想要的书是http://www.amazon.com/Jim-Blinns-Corner-Graphics-Pipeline/dp/1558603875/ref=ntt_at_ep_dpi_1

他在这方面有一整章

【讨论】:

感谢您的意见。我有一个粗略的实现,但会研究他的版本,看看它是否更优雅。【参考方案5】:

这是直接从我的引擎复制而来的,它创建了 6 个平面,分别代表 futsum 的六个侧面。 我希望它有用。

internal class BoundingFrustum
    
        private readonly float4x4 matrix = new float4x4(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
        private readonly Plane[] planes;

        internal BoundingFrustum(float4x4 value)
        
            planes = new Plane[6];
            for (int i = 0; i < 6; i++)
                planes[i] = new Plane();
            Setfloat4x4(value);
        

        private void Setfloat4x4(float4x4 value)
        
            planes[2].Normal.X = -value.M14 - value.M11;
            planes[2].Normal.Y = -value.M24 - value.M21;
            planes[2].Normal.Z = -value.M34 - value.M31;
            planes[2].D = -value.M44 - value.M41;
            planes[3].Normal.X = -value.M14 + value.M11;
            planes[3].Normal.Y = -value.M24 + value.M21;
            planes[3].Normal.Z = -value.M34 + value.M31;
            planes[3].D = -value.M44 + value.M41;
            planes[4].Normal.X = -value.M14 + value.M12;
            planes[4].Normal.Y = -value.M24 + value.M22;
            planes[4].Normal.Z = -value.M34 + value.M32;
            planes[4].D = -value.M44 + value.M42;
            planes[5].Normal.X = -value.M14 - value.M12;
            planes[5].Normal.Y = -value.M24 - value.M22;
            planes[5].Normal.Z = -value.M34 - value.M32;
            planes[5].D = -value.M44 - value.M42;
            planes[0].Normal.X = -value.M13;
            planes[0].Normal.Y = -value.M23;
            planes[0].Normal.Z = -value.M33;
            planes[0].D = -value.M43;
            planes[1].Normal.X = -value.M14 + value.M13;
            planes[1].Normal.Y = -value.M24 + value.M23;
            planes[1].Normal.Z = -value.M34 + value.M33;
            planes[1].D = -value.M44 + value.M43;
            for (int i = 0; i < 6; i++)
            
                float num2 = planes[i].Normal.Length();
                planes[i].Normal = planes[i].Normal / num2;
                planes[i].D /= num2;
            
        

        internal Plane Bottom
        
            get  return planes[5]; 
        
        internal Plane Far
        
            get  return planes[1]; 
        
        internal Plane Left
        
            get  return planes[2]; 
        
        internal Plane Near
        
            get  return planes[0]; 
        
        internal Plane Right
        
            get  return planes[3]; 
        
        internal Plane Top
        
            get  return planes[4]; 
        
    

【讨论】:

【参考方案6】:

如果其他人对更精确的解决方案感兴趣,我为 3ds Max 相机做了这个。在相机视图上适合任意数量的对象。可以看到 Maxscript 代码,因为 seudo-code 易于阅读,并且有一些有用的 cmets。

https://github.com/piXelicidio/pxMaxScript/tree/master/CameraZoomExtents

我所做的简化是在相机空间上工作。获取对象顶点或边界框顶点并在两个 2D 平面上投影。

第一个就像从顶视图看到你的相机(水平 FOV) 第二个是从侧面看(垂直 FOV)

在第一个平面上投影所有顶点(顶视图) 现在从摄像机位置取两条线,代表摄像机 FOV,一条代表左侧,另一条代表右侧。我们只需要这条线的方向。

现在我们需要找到一个点(顶点),如果我们在其上画右线,所有其他点都会落在左侧。 (找到图上的红点)

然后找到另一个点,如果左线越过它,所有其他点都落在该线的右侧。 (蓝点)

有了这些到点,然后我们截取通过这两个点的两条线(我们仍然在 2D 中)。

生成的截距是仅考虑水平 FOV 的适合场景的最佳摄像机位置。

接下来对垂直 FOV 执行相同操作。

这两个位置将为您提供所需的一切,以决定是从侧面贴合还是从顶部和底部贴合。

让相机从场景中移开更多平移的那个是获得“完美契合”的那个,另一个将有更多的空房间然后你需要找到中心......这也是计算的上面链接上的脚本!

抱歉,不能继续解释现在需要睡觉;)如果有人感兴趣,请询问,我会尝试扩展答案。

【讨论】:

+1 这对我来说是最精确的解决方案,它在一次拍摄中找到了完美的相机位置,根本不需要迭代。虽然我在 3D 空间工作过,但无需暂时转换为 2D 空间。请参阅 Unity/C# 中的实现:github.com/yasirkula/UnityRuntimePreviewGenerator/blob/…【参考方案7】:

检查此链接 https://msdn.microsoft.com/en-us/library/bb197900.aspx

浮动距离 = sphere.radius / sin(fov / 2);

float3 eyePoint = sphere.centerPoint - 距离 * camera.frontVector;

【讨论】:

以上是关于移动相机以适应 3D 场景的主要内容,如果未能解决你的问题,请参考以下文章

unity3D 将面片始终面向摄像机怎么弄?

unity3D:游戏分解之角色移动和相机跟随

Unity无缝循环世界实现

unity3d 新人求助 按钮控制同场景多个相机切换的问题

javascript / threejs - 围绕中心 y 轴(在 3D 空间中)在圆圈中移动对象的方程

使用WPF实现3D场景[二]