设计自己的软渲染器2-构建3D世界到2D屏幕显示的基本变换

说明

在这一节中,我们将一步步的从基础构起,完成由3D物体坐标转换为到屏幕上所看到的图像的变换流程,最终反映在程序中便是我们输入的三位点依据我们设置的观察方式投影到了屏幕上。这部分内容可以参考《计算机图形学》第四版。

 

首先明白几个概念。

1.    模型坐标系,在此坐标系下构建我们的3D物体(一般以物体几何中心为坐标中心)。

2.    世界坐标系,拥有所有3D物体的整个3D世界。

3.    相机坐标系,该坐标系是以观察用相机(我们眼睛)为中心。

4.    裁剪空间,我理解为我们所划定的能观察到的相机坐标系中的部分,其实还在相机坐标系中

5.    标准设备空间,我理解为我们最终投影到的位置在【-1,1】之间。

6.    屏幕空间,将【-1,1】映射到屏幕的大小下。

 

整个转换流程是

 

其中裁剪空间我们在透视矩阵中即可设置,表现为一个可视截楔形台。

具体看下面的流程

我们此时所处为相机空间,也就是说当一个点x==0,y==0时应在屏幕中心,但是由于人眼的透视效果,我们不可只用x与y来确定一个物体应放在屏幕哪部分,所以使用到相机的距离Z,当一点x,y坐标相等,我们依据Z来判断哪一个更应放在屏幕中。

 

 

借助图形,在投影前,蓝色为世界坐标系中物体,红色部分为相机视角下能看见的地方。

 

对物体使用透视矩阵后

每一个锥台都是一个原先的立方体透视变换后,也就是符合近大远小的原则。

实际看来就是下面样子

 

 

代码简介

Vector4D(PointerD),Mat(4*4矩阵),这两个类来进行基本数学表示,其中若用到Point2D,则采用Point4D来模拟。并附有加减乘除,向量积与数量积等运算。(基本线性代数)

 

Camera:

观察相机相关,设置视图矩阵

 

Transform:

包含从模型到世界到相机再到投影平面变换过程的综合变换矩阵。

在这里设置透视矩阵(推导参考重要矩阵部分)

 

Device:

综合渲染过程。

 

 

部分系统调用代码(窗口创建,显示framebuffer等),这一部分不必深究,因为其实它不是我们的软渲染器的核心部分,只是辅助而已,我们只要会访问其中的显示缓存framebuffer即可。

 

代码执行流程为:

 

Main {

       初始化模型mesh;

       设置windows窗口;

       实例化设备并与窗口的显示缓存绑定;

       初始化相机。

       设置模型变换的变换矩阵的视图矩阵与透视矩阵;

      

While {

       窗口相关;

       设备显示内存清空;

       设备渲染模型;

       调整模型姿态;

       更新窗口画面;

       Sleep(1);

}

}

 

Code:

All1.h

Main.cpp

 

PS:

之前看计算机图形学的时候,那时感觉这一套变换流程不是很难嘛,但是今天具体实现时发现许多细节要注意。

 

 

重要矩阵

平移旋转缩放矩阵

变换矩阵(可以是平移、旋转、缩放矩阵或者他们的组合)

使用方式:变换矩阵 * 某一点 = 变换后的点

 

 

平移矩阵:

 

 

缩放矩阵:

 

x, y, z 分别表示希望x, y,z 上的缩放倍数。

 

旋转矩阵:

 

对于任意轴p时,可将旋转分解为

1.将坐标轴旋转,使旋转轴p与Z轴重合

2.将点w绕Z轴旋转theta度

3.再将坐标轴旋转回原位。

即:

 

 

Point_after = Rz(ψ)*Ry(ϕ)*Rz(θ)*Ry(−ϕ)*Rz(−ψ)*Point_before

其中借助旋转矩阵作为正交矩阵的特点用 R(−α)=(R−1)(α)=RT(α)

简化为

(正交证明:http://blog.csdn.net/zhang11wu4/article/details/49761121

Rz(ψ)*Ry(ϕ)*Rz(θ)*RyT(ϕ)*RzT(ψ)

计算后可得:(x,y,z)为旋转轴单位向量,theta为弧度制旋转角

 

对一个点做变换,大部分为平移旋转与缩放的集合,所以可以将变换矩阵利用矩阵性质组合起来,即:Mat = translation_mat * rotate_mat * scale_mat

Point_after = Mat * Point_before  (先缩放,后旋转,然后平移)

Reference:

http://blog.csdn.net/csxiaoshui/article/details/65446125

视图矩阵

在这里的是

1、首先我们来求得N = eye – lookat(/*其实这里依据左手坐标系,应为lookat-eye,但因为下图中画出的方向一致性,这里写为eye –lookat,最终使用时记得换回来*/),并把N归一化。

2、up和N差积得到U, U= up X N,归一化U。

3、然后N和U差积得到V

 

 

 

 

假设一开始相机坐标系和世界坐标系重合,它先进行一个旋转变化,然后再进行一个平移,得到现在是相机位置和方位。则此时进行的矩阵变化为

,其中T是平移变化,R是旋转变化,而相机变换是相机本身变换的逆变换。

 

T的逆矩阵为:

 

当相机完成自身坐标系原点移至世界坐标系原点一步之后,相机的原点和世界原点就重合了,也就是处理完了关于平移的变换。

   我们要把一个世界坐标系点K(Kx, Ky, Kz),表示成(U,V,P)坐标系的点(假设此时,已经经过平移操作,摄像机在世界坐标系的原点),则其公式为:

Lx = Kx * Ux + Ky * Uy +Kz * Uz;

Ly = Kx * Vx + Ky * Vy +Kz * Vz;

Lz = Kx * Px + Ky * Py +Kz * Pz;

即:

 

 

 

Reference:

https://blog.csdn.net/tangguotupaopao/article/details/26477533

http://www.cnblogs.com/mikewolf2002/archive/2012/03/11/2390669.html

http://blog.csdn.net/augusdi/article/details/20450065

http://www.cnblogs.com/mikewolf2002/archive/2012/11/25/2787636.html

 

Code:

 

void Mat::Set_As_Rotate(float x, float y,floatz, float theta) {

    //设置为旋转矩阵

    //theta是弧度值

    float qsin = (float)sin(theta);

    float qcos = (float)cos(theta);

    float one_qcos = 1 -qcos;

    Vector4D vi(x,y,z, 1);

    vi.Normalize();

    float X = vi.x, Y = vi.y,Z = vi.z;

    m[0][0]= qcos + X*X*one_qcos;

    m[0][1]= X*Y*one_qcos - Z*qsin;

    m[0][2]= X*Z*one_qcos + Y*qsin;

    m[0][3]= 0.0f;

    m[1][0]= Y*X*one_qcos + Z*qsin;

    m[1][1]= qcos + Y*Y*one_qcos;

    m[1][2]= Y*Z*one_qcos - X*qsin;

    m[1][3]= 0.0f;

    m[2][0]= Z*X*one_qcos - Y*qsin;

    m[2][1]= Z*Y*one_qcos + X*qsin;

    m[2][2]= qcos + Z*Z*one_qcos;

    m[2][3]= 0.0f;

    m[3][0]= 0;

    m[3][1]= 0;

    m[3][2]= 0;

    m[3][3]= 1.0f;

}

 

 

 

 

 

透视矩阵

 

说了这么多,透视矩阵到底怎么做的?

视锥体:

视锥体是一个三维体,他的位置和摄像机相关,视锥体的形状决定了模型如何从camera space投影到屏幕上。最常见的投影类型-透视投影,使得离摄像机近的物体投影后较大,而离摄像机较远的物体投影后较小。透视投影使用棱锥作为视锥体,摄像机位于棱锥的椎顶。该棱锥被前后两个平面截断,形成一个棱台,叫做View Frustum,只有位于Frustum内部的模型才是可见的。

 

透视投影的目的:

透视投影的目的就是将上面的棱台转换为一个立方体(cuboid),转换后,棱台的前剪裁平面的右上角点变为立方体的前平面的中心(下图中弧线所示)。由图可知,这个变换的过程是将棱台较小的部分放大,较大的部分缩小,以形成最终的立方体。这就是投影变换会产生近大远小的效果的原因。变换后的x坐标范围是[-1, 1],y坐标范围是[-1, 1],z坐标范围是[-1, 1]。

 

 

透视投影矩阵推导:

那么透视投影到底做了什么工作呢?

我们可以将整个投影过程分为两个部分,第一部分是从Frustum内一点投影到近剪裁平面的过程,第二部分是由近剪裁平面缩放的过程。假设Frustum内一点P(x,y,z)在近剪裁平面上的投影是P'(x',y',z'),而P'经过缩放后的最终坐标设为P''(x",y",z")。假设所求的投影矩阵为M,那么根据矩阵乘法可知,如下等式成立。

 

先看第一部分,为了简化问题,我们考虑YOZ平面上的投影情况,见下图。设P(x, y, z)是Frustum内一点,它在近剪裁平面上的投影是P'(x', y', z')。(注意:D3D以近剪裁平面作为投影平面),设视锥体与Z轴夹角。

 

 由上图可知,三角形OP'Q'与三角形OPQ相似,于是有如下等式成立。

 

又因为投影平面的宽长比为Aspect,所以

 即:

 

由W/H = Aspect

 

此图问题应为除

最后看z'',当Frustum内的点投影到近剪裁平面的时候,实际上这个z'值已经没有意义了,因为所有位于近剪裁平面上的点,其z'值都是n,看起来我们甚至可以抛弃这个z'值,可以么?当然不行!别忘了后面还有深度测试呢。由第一幅图可知,所有位于线段p'p上的点,最终都会投影到p'点,那么如果这条线段上真的有多个点,如何确定最终保留哪一个呢?当然是离观察这最近的这个了,也就是深度值(z值)最小的。所以z'坐标可以直接保存p点的z值。因为在光栅化之前,我们需要对z坐标的倒数进行插值(原因请参见Mathematics for 3D Game Programming and Computer Grahpics 3rdsection 5.4),所以可以将z''写成

 

 

 

将X”Y”Z”代入最开始矩阵乘法等式

 

由上式可见,x'',y'',z''都除以了Pz,于是我们将他们再乘以Pz(这并不该变齐次坐标的大小),得到如下等式。

 

注意这里,x即Px,y即Py,z即Pz,解矩阵的每一列得到

 

于是所求矩阵为

 

注意,这里推得的透视变换矩阵是右乘用 即Vec_before*Mat= Vec_after

 

Reference:

https://blog.csdn.net/tangguotupaopao/article/details/26477533

http://www.cnblogs.com/caster99/p/4783386.html

http://www.cnblogs.com/graphics/

http://www.cnblogs.com/zhangbaochong/p/5388792.html

Code:

void Transform::Init(int width, int height) {

    // 初始化,设置屏幕长宽

 

    world.Set_Identity();

    view.Set_Identity();

    w= (float)width;

    h= (float)height;

}

 

void Transform::Set_Perspective(float fovy, float aspect,floatzn, float zf) {

    //设置透视矩阵,相当于D3DXMatrixPerspectiveFovLH

    //fovy = view frustum 与Z轴夹角弧度制

    //aspect 投影面宽长比(显示区宽长比)

    //zn 相机到近裁剪平面距离,zf相机到远裁剪平面距离

 

    float fax = 1.0f / (float)tan(fovy * 0.5f);

    projection.Set_Zero();

    projection.m[0][0]= (float)(fax /aspect);

    projection.m[1][1]= (float)(fax);

    projection.m[2][2]= zf / (zf -zn);

    projection.m[2][3]= zn * zf / (zn - zf);

    projection.m[3][2]= 1;

    /*projection.m[3][2] = zn * zf / (zn - zf);

    projection.m[2][3]= 1;*/

}

 

 

void Transform::Update() {

    // 矩阵更新,计算 transform =   projection * view * world

    static Mat m;

    m.Set_Identity();

    view.Mul(world,m);

    projection.Mul(m,transform);

}

void Transform::Apply(Vector4D &op,Vector4D &re) {

    //old  将矢量 op进行 project

    //此操作后,re里的x,y即为在屏幕中显示的位置,z留作深度测试

    transform.Mul_Vec(op, re);

 

}

void Transform::Homogenize(Vector4D &op,Vector4D &re) {

    // 归一化,得到屏幕坐标

    float rhw = 1.0f / op.w;

    re.x = (op.x * rhw + 1.0f) * w* 0.5f;

    re.y = (1.0f - op.y * rhw) * h *0.5f;

    re.z = op.z * rhw;

    re.w = 1.0f;

}

 

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值