如何将3D点转换为2D透视投影?

时间:2009-04-07 05:23:05

标签: java graphics 3d bezier

我目前正在使用Bezier曲线和曲面来绘制着名的犹他州茶壶。使用16个控制点的Bezier贴片,我已经能够绘制茶壶并使用“世界到相机”功能显示它,该功能可以旋转生成的茶壶,目前正在使用正交投影。

结果是我有一个'扁平'茶壶,预计正投影的目的是保留平行线。

但是,我想使用透视投影来给出茶壶深度。我的问题是,如何从“世界到相机”函数返回3D xyz顶点,并将其转换为2D坐标。我想在z = 0时使用投影平面,并允许用户使用键盘上的箭头键确定焦距和图像大小。

我在java中编程并设置了所有输入事件处理程序,并且还编写了一个处理基本矩阵乘法的矩阵类。我已经阅读了维基百科和其他资源一段时间,但我无法完全了解如何执行此转换。

10 个答案:

答案 0 :(得分:86)

我觉得这个问题有点陈旧,但无论如何我决定通过搜索找到这个问题的人给出答案。
现在表示2D / 3D变换的标准方法是使用齐次坐标。 2D的 [x,y,w] ,3D的 [x,y,z,w] 。由于您在3D和平移中都有三个轴,因此该信息完全适合4x4变换矩阵。我将在此解释中使用列主矩阵表示法。除非另有说明,否则所有矩阵均为4x4 从3D点到栅格化点,线或多边形的阶段如下所示:

  1. 使用逆相机矩阵转换3D点,然后进行所需的任何转换。如果你有曲面法线,也可以将它们变换,但是将w设置为零,因为你不想平移法线。您使用法线变换的矩阵必须是各向同性;缩放和剪切会使法线变形。
  2. 使用剪辑空间矩阵转换点。该矩阵使用视场和纵横比来缩放x和y,通过近和远剪裁平面缩放z,并将'旧'z插入到w中。转换后,应将x,y和z除以w。这称为透视划分
  3. 现在您的顶点位于剪辑空间中,并且您希望执行剪裁,因此您不会渲染视口范围之外的任何像素。 Sutherland-Hodgeman剪辑是最常用的裁剪算法。
  4. 相对于w以及半宽和半高变换x和y。您的x和y坐标现在位于视口坐标中。 w被丢弃,但通常保存1 / w和z,因为在多边形表面上进行透视校正插值需要1 / w,z存储在z缓冲区中并用于深度测试。
  5. 此阶段是实际投影,因为z不再用作位置中的组件。

    算法:

    视野的计算

    这会计算视野。 tan是否需要弧度或度数是无关紧要的,但 angle 必须匹配。请注意,当 angle 接近180度时,结果会达到无穷大。这是一个奇点,因为不可能有一个广泛的焦点。如果您想要数值稳定性,请保持角度小于或等于179度。

    fov = 1.0 / tan(angle/2.0)
    

    还要注意1.0 / tan(45)= 1.这里的其他人建议除以z。结果很清楚。您将获得90度FOV和1:1的宽高比。使用像这样的齐次坐标还有其他几个优点;例如,我们可以对近处和远处的平面执行裁剪,而不将其视为特殊情况。

    剪辑矩阵的计算

    这是剪辑矩阵的布局。 aspectRatio 是宽度/高度。因此,基于y的FOV来缩放x分量的FOV。远近是系数,它们是近剪裁平面和远剪裁平面的距离。

    [fov * aspectRatio][        0        ][        0              ][        0       ]
    [        0        ][       fov       ][        0              ][        0       ]
    [        0        ][        0        ][(far+near)/(far-near)  ][        1       ]
    [        0        ][        0        ][(2*near*far)/(near-far)][        0       ]
    

    屏幕投影

    剪裁后,这是获取屏幕坐标的最终转换。

    new_x = (x * Width ) / (2.0 * w) + halfWidth;
    new_y = (y * Height) / (2.0 * w) + halfHeight;
    

    C ++中的简单示例实现

    #include <vector>
    #include <cmath>
    #include <stdexcept>
    #include <algorithm>
    
    struct Vector
    {
        Vector() : x(0),y(0),z(0),w(1){}
        Vector(float a, float b, float c) : x(a),y(b),z(c),w(1){}
    
        /* Assume proper operator overloads here, with vectors and scalars */
        float Length() const
        {
            return std::sqrt(x*x + y*y + z*z);
        }
    
        Vector Unit() const
        {
            const float epsilon = 1e-6;
            float mag = Length();
            if(mag < epsilon){
                std::out_of_range e("");
                throw e;
            }
            return *this / mag;
        }
    };
    
    inline float Dot(const Vector& v1, const Vector& v2)
    {
        return v1.x*v2.x + v1.y*v2.y + v1.z*v2.z;
    }
    
    class Matrix
    {
        public:
        Matrix() : data(16)
        {
            Identity();
        }
        void Identity()
        {
            std::fill(data.begin(), data.end(), float(0));
            data[0] = data[5] = data[10] = data[15] = 1.0f;
        }
        float& operator[](size_t index)
        {
            if(index >= 16){
                std::out_of_range e("");
                throw e;
            }
            return data[index];
        }
        Matrix operator*(const Matrix& m) const
        {
            Matrix dst;
            int col;
            for(int y=0; y<4; ++y){
                col = y*4;
                for(int x=0; x<4; ++x){
                    for(int i=0; i<4; ++i){
                        dst[x+col] += m[i+col]*data[x+i*4];
                    }
                }
            }
            return dst;
        }
        Matrix& operator*=(const Matrix& m)
        {
            *this = (*this) * m;
            return *this;
        }
    
        /* The interesting stuff */
        void SetupClipMatrix(float fov, float aspectRatio, float near, float far)
        {
            Identity();
            float f = 1.0f / std::tan(fov * 0.5f);
            data[0] = f*aspectRatio;
            data[5] = f;
            data[10] = (far+near) / (far-near);
            data[11] = 1.0f; /* this 'plugs' the old z into w */
            data[14] = (2.0f*near*far) / (near-far);
            data[15] = 0.0f;
        }
    
        std::vector<float> data;
    };
    
    inline Vector operator*(const Vector& v, const Matrix& m)
    {
        Vector dst;
        dst.x = v.x*m[0] + v.y*m[4] + v.z*m[8 ] + v.w*m[12];
        dst.y = v.x*m[1] + v.y*m[5] + v.z*m[9 ] + v.w*m[13];
        dst.z = v.x*m[2] + v.y*m[6] + v.z*m[10] + v.w*m[14];
        dst.w = v.x*m[3] + v.y*m[7] + v.z*m[11] + v.w*m[15];
        return dst;
    }
    
    typedef std::vector<Vector> VecArr;
    VecArr ProjectAndClip(int width, int height, float near, float far, const VecArr& vertex)
    {
        float halfWidth = (float)width * 0.5f;
        float halfHeight = (float)height * 0.5f;
        float aspect = (float)width / (float)height;
        Vector v;
        Matrix clipMatrix;
        VecArr dst;
        clipMatrix.SetupClipMatrix(60.0f * (M_PI / 180.0f), aspect, near, far);
        /*  Here, after the perspective divide, you perform Sutherland-Hodgeman clipping 
            by checking if the x, y and z components are inside the range of [-w, w].
            One checks each vector component seperately against each plane. Per-vertex
            data like colours, normals and texture coordinates need to be linearly
            interpolated for clipped edges to reflect the change. If the edge (v0,v1)
            is tested against the positive x plane, and v1 is outside, the interpolant
            becomes: (v1.x - w) / (v1.x - v0.x)
            I skip this stage all together to be brief.
        */
        for(VecArr::iterator i=vertex.begin(); i!=vertex.end(); ++i){
            v = (*i) * clipMatrix;
            v /= v.w; /* Don't get confused here. I assume the divide leaves v.w alone.*/
            dst.push_back(v);
        }
    
        /* TODO: Clipping here */
    
        for(VecArr::iterator i=dst.begin(); i!=dst.end(); ++i){
            i->x = (i->x * (float)width) / (2.0f * i->w) + halfWidth;
            i->y = (i->y * (float)height) / (2.0f * i->w) + halfHeight;
        }
        return dst;
    }
    

    如果您仍在思考这个问题,那么OpenGL规范对于所涉及的数学来说是一个非常好的参考。 http://www.devmaster.net/上的DevMaster论坛也有很多与软件光栅化器有关的好文章。

答案 1 :(得分:12)

我认为this可能会回答你的问题。这是我在那里写的:

  

这是一个非常一般的答案。假设相机处于(Xc,Yc,Zc)并且您要投影的点是P =(X,Y,Z)。从摄像机到投影到的2D平面的距离为F(因此平面的方程为Z-Zc = F)。投影到平面上的P的2D坐标是(X',Y')。

     

然后,非常简单:

     

X'=((X-Xc)*(F / Z))+ Xc

     

Y'=((Y-Yc)*(F / Z))+ Yc

     

如果你的相机是原点,那么这简化为:

     

X'= X *(F / Z)

     

Y'= Y *(F / Z)

答案 2 :(得分:6)

您可以使用:Commons Math: The Apache Commons Mathematics Library仅使用两个类在2D中投影3D点。

Java Swing的示例。

import org.apache.commons.math3.geometry.euclidean.threed.Plane;
import org.apache.commons.math3.geometry.euclidean.threed.Vector3D;


Plane planeX = new Plane(new Vector3D(1, 0, 0));
Plane planeY = new Plane(new Vector3D(0, 1, 0)); // Must be orthogonal plane of planeX

void drawPoint(Graphics2D g2, Vector3D v) {
    g2.drawLine(0, 0,
            (int) (world.unit * planeX.getOffset(v)),
            (int) (world.unit * planeY.getOffset(v)));
}

protected void paintComponent(Graphics g) {
    super.paintComponent(g);

    drawPoint(g2, new Vector3D(2, 1, 0));
    drawPoint(g2, new Vector3D(0, 2, 0));
    drawPoint(g2, new Vector3D(0, 0, 2));
    drawPoint(g2, new Vector3D(1, 1, 1));
}

现在你只需要更新planeXplaneY来更改透视投影,就可以得到这样的结果:

enter image description here enter image description here

答案 3 :(得分:5)

要获得经过视角校正的坐标,只需除以z坐标:

xc = x / z
yc = y / z

上述工作假设相机位于(0, 0, 0)并且您正在z = 1投影到飞机上 - 否则您需要相对于相机转换合作。

曲线存在一些复杂性,因为投影3D贝塞尔曲线的点通常不会给出与通过投影点绘制2D贝塞尔曲线相同的点。

答案 4 :(得分:2)

enter image description here

从顶部看屏幕,得到x和z轴。
从侧面看屏幕,得到y轴和z轴。

使用三角法计算顶视图和侧视图的焦距,三角测量是眼睛和屏幕中间之间的距离,由屏幕视野决定。 这使得两个直角三角形的形状背靠背。

hw = screen_width / 2

hh = screen_height / 2

fl_top = hw / tan(θ/ 2)

fl_side = hh / tan(θ/ 2)


然后取平均焦距。

fl_average =(fl_top + fl_side)/ 2


现在用基本算法计算新的x和新y,因为由3d点和眼点构成的较大的直角三角形与由2d点和眼点产生的较小的三角形一致。

x'=(x * fl_top)/(z + fl_top)

y'=(y * fl_top)/(z + fl_top)


或者你可以简单地设置

x'= x /(z + 1)

y'= y /(z + 1)

答案 5 :(得分:1)

我不确定你问这个问题的级别。听起来好像你已经在网上找到了这些公式,并且只是想了解它的作用。在阅读你的问题时,我提供:

  • 想象一下来自观察者的光线(在V点)直接朝向投影平面的中心(称之为C)。
  • 想象一下从观察者到图像中的一个点(P)的第二条光线,它也在某个点(Q)与投影平面相交
  • 观察者和视平面上的两个交点形成一个三角形(VCQ);两侧是两条射线和平面内各点之间的线。
  • 公式使用此三角形来查找Q的坐标,这是投影像素的位置

答案 6 :(得分:1)

所有答案都解决了标题中提出的问题。但是,我想在文本中添加隐含的警告。 Bézier贴片用于表示曲面,但您不能仅仅修改贴片的点并将贴片细分为多边形,因为这会导致几何体扭曲。但是,您可以使用转换的屏幕容差将贴片首先镶嵌到多边形中,然后转换多边形,或者将Bézier贴片转换为合理的Bézier贴片,然后使用屏幕空间容差对这些贴片进行细分。前者更容易,但后者更适合生产系统。

我怀疑你想要更轻松的方式。为此,您可以通过逆透视变换的雅可比规范来缩放屏幕容差,并使用它来确定模型空间中所需的镶嵌量(可能更容易计算前向雅可比矩阵,将其反转,然后采取常态)。请注意,此规范与位置有关,您可能希望在多个位置对此进行评估,具体取决于透视图。还要记住,由于投影变换是合理的,你需要应用商规则来计算导数。

答案 7 :(得分:0)

我知道这是一个古老的主题,但你的插图不正确,源代码设置剪辑矩阵正确。

[fov * aspectRatio][        0        ][        0              ][        0       ]
[        0        ][       fov       ][        0              ][        0       ]
[        0        ][        0        ][(far+near)/(far-near)  ][(2*near*far)/(near-far)]
[        0        ][        0        ][        1              ][        0       ]

你的东西的一些补充:

如果您想要添加相机移动和旋转,则此剪辑矩阵仅适用于在静态2D平面上投影:

viewMatrix = clipMatrix * cameraTranslationMatrix4x4 * cameraRotationMatrix4x4;

这使您可以旋转2D平面并将其移动..-

答案 8 :(得分:0)

您可能希望使用spheres调试系统,以确定您是否具有良好的视野。如果它太宽,则在屏幕边缘变形的球体变成更椭圆形的形状,指向框架的中心。这个问题的解决方案是放大帧,通过将三维点的x和y坐标乘以标量,然后将对象或世界缩小一个相似的因子。然后你会在整个画面中得到漂亮均匀的圆形球。

我几乎感到很尴尬,我花了一整天时间才想出这个,并且我几乎确信这里有一些怪异的神秘几何现象需要采用不同的方法。

然而,通过渲染球体来校准缩放视帧系数的重要性不容小觑。如果你不知道你的宇宙的“可居住区”在哪里,你将最终走在太阳下并废弃该项目。您希望能够在视图框架中的任何位置渲染球体,使其显示为圆形。在我的项目中,与我所描述的区域相比,单位范围是巨大的。

此外,必须的维基百科条目: Spherical Coordinate System

答案 9 :(得分:-1)

感谢@Mads Elvenheim提供正确的示例代码。我修复了代码中的次要语法错误(只有少数 const 问题和明显丢失的运算符)。此外, near far 在vs中具有截然不同的含义。

为了您的荣幸,这是可编译的(MSVC2013)版本。玩得开心。 请注意,我已使NEAR_Z和FAR_Z保持不变。你可能不喜欢那样。

<!DOCTYPE html>
<html lang="en">
<meta name="robots" content="index, follow">
<meta http-equiv="imagetoolbar" content="no">
<meta http-equiv="language" content="en">

<head>
    <!-- MenuBar - below 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- ./3 meta tags -->
<title>CENTERED</title>	
    <!-- MenuBar - Bootstrap Core CSS (ddmenu.js, jquery.js & bootstrap.js are the bottom for faster loading)-->
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">	
   <!-- MenuBar & Mine Custom CSS -->
    <link href="css-mb/main-SOLVED.css" rel="stylesheet">
    <!-- /.custom css -->  
</head>

<body>
    <!-- Navigation -->
    <nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
        <div class="container">
            <!-- Logo and responsive toggle -->
            <div class="navbar-header"> <!--This div creates a navigation button visible on smaller screens -->
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#navbar">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span><!--These tags create the standard 3-lin button logo on top right corner -->
                    <span class="icon-bar"></span><!--Put the page less than full-screen to see this behavior -->
                    <span class="icon-bar"></span>
                </button>
            </div>
            <div class="collapse navbar-collapse" id="navbar">
                <ul class="nav navbar-nav">
                    <li><a href="#">Home</a></li>
                    <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown1<span class="caret"></span></a><!--Requires the JavaScript files linked at the end-->
                        <ul class="dropdown-menu">
                            <li><a href="#">Item1</a></li>
                            <li><a href="#">Item2</a></li>
                            <li><a href="#">Item3</a></li>
                        </ul>
                    </li>
                    <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown2<span class="caret"></span></a>
                        <ul class="dropdown-menu">
                            <li><a href="#">AnotherItem</a></li>
                            <li role="separator" class="divider"></li>
                            <li class="dropdown-header">Header:</li>
                            <li><a href="#">MoreItems</a></li>
                            <li><a href="#">MoreItems</a></li>
                            <li><a href="#">MoreItems</a></li>
                        </ul>
                    </li>
                    <li><a href="#">Tutorials</a></li>
                    <li><a href="#">About</a></li>
                </ul>
            </div>
            <!-- /.navbar-collapse -->
        </div>
        <!-- /.container -->
    </nav>	
		
		
<section class="image">
<img src="http://oi64.tinypic.com/k3sz9x.jpg" width="840" height="166">
</section>


			<div id="footer">
		<center>
<TABLE BORDER=0><TR id="sl">
<TD><a href="#" target="_blank" class="tm-social-link"><i class="fa fa-1x fa-facebook"></i>FB</a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</TD>
<TD><a href="#" target="_blank" class="tm-social-link"><i class="fa fa-1x fa-youtube"></i>YT</a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</TD>
<TD><a href="#"<span class="st_sharethis_custom" st_via="XXX" st_msg="#XXX"><i class="fa fa-1x fa-share-alt"></i>SH</a></span></TD>
</TR></TABLE>
</center>
<div class="foot">FOOTER TEXT</div><!--class foot-->
		</div><!--.footer-->


<!-- MenuBar - jQuery & JavaScript are required for the dropdown menu. Placed at the end of the document so the pages load faster -->
<script src="http://m.uploadedit.com/ba3s/1492400858966.txt" type="text/javascript"></script>
<!-- jQuery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.0/jquery.js"></script>
<!-- Bootstrap core JavaScript-->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.js"></script>
</body>
</html>