手搓一个小型渲染器(四)

透视投影

在之前的渲染过程中,我们在渲染过程中只是简单的省略掉z坐标轴,完成了正交渲染,而为了更好的呈现渲染效果,我们需要完成透视投影中完成渲染。

二维平面

线性变换

平面上的线性变换可以用相应的矩阵来表示。如果我们取一个点(x,y),那么它的变换可以写成如下:
[ a b c d ] [ x y ] = [ a x + b y c x + d y ] \left[\begin{array}{l} a&b \\ c&d \end{array}\right] \left[\begin{array}{l} x \\ y \end{array}\right] =\left[\begin{array}{l} ax + by \\ cx+dy \end{array}\right] [acbd][xy]=[ax+bycx+dy]
其中最简单的就是变换就是单位变换,它不移动任何点:
[ 1 0 0 1 ] [ x y ] = [ x y ] \left[\begin{array}{l} 1&0 \\ 0&1 \end{array}\right] \left[\begin{array}{l} x \\ y \end{array}\right] =\left[\begin{array}{l} x \\ y \end{array}\right] [1001][xy]=[xy]
如果我们只改变对角线上矩阵的元素,代表对原物体进行缩放:
[ 3 2 0 0 3 2 ] [ x y ] = [ 3 2 x 3 2 y ] \left[\begin{array}{l} \frac{3}{2}&0 \\ 0&\frac{3}{2} \end{array}\right] \left[\begin{array}{l} x \\ y \end{array}\right] =\left[\begin{array}{l} \frac{3}{2}x \\ \frac{3}{2}y \end{array}\right] [230023][xy]=[23x23y]
使用矩阵,我们可以将对象的变换直接转换为矩阵的乘法:
[ 3 2 0 0 3 2 ] [ − 1 1 1 0 − 1 − 1 − 1 0 1 1 ] = [ − 3 2 3 2 3 2 0 − 3 2 − 3 2 − 3 2 0 3 2 3 2 ] \left[\begin{array}{l} \frac{3}{2}&0 \\ 0&\frac{3}{2} \end{array}\right] \left[\begin{array}{l} -1&1&1&0&-1 \\ -1&-1&0&1&1 \end{array}\right] =\left[\begin{array}{l} -\frac{3}{2}&\frac{3}{2}&\frac{3}{2}&0&-\frac{3}{2} \\ -\frac{3}{2}&-\frac{3}{2}&0&\frac{3}{2}&\frac{3}{2} \end{array}\right] [230023][1111100111]=[232323232300232323]
上式右侧矩阵是正方体对象的顶点,我们取一个数组,乘上变换矩阵,得到变换后的对象。

如果我们对左侧矩阵中除对角线元素进行改变时,得到的图形将会如何变换呢?对于下面的变换,结果如下:
[ 1 1 3 0 1 ] [ x y ] = [ x + y 3      y ] \left[\begin{array}{l} 1&\frac{1}{3} \\ 0&1 \end{array}\right] \left[\begin{array}{l} x \\ y \end{array}\right]= \left[\begin{array}{l} x + \frac{y}{3} \\ \ \ \ \ y \end{array}\right] [10311][xy]=[x+3y    y]
img

这时切变,即垂直度坐标不变(y坐标不变),最下边的偏移量为0,当y = 1时,最上边的偏移量为 1 3 \frac{1}{3} 31,事实证明,任何旋转(围绕原点)都可以表示为三个剪切的合成动作,这里白色物体被转换为红色物体,然后转换为绿色物体,最后转换为蓝色物体:

img

为了简化上面的旋转过程,我们可以直接给出旋转矩阵:
[ c o s ( α ) − s i n ( α ) s i n ( α ) − c o s ( α ) ] [ x y ] = [ x cos ⁡ ( α ) − y sin ⁡ ( α ) x   s i n ( α ) + y   c o s ( α ) ] \left[\begin{array}{l} cos(\alpha) & -sin(\alpha) \\ sin(\alpha) & -cos(\alpha) \end{array}\right] \left[\begin{array}{l} x \\ y \end{array}\right] = \left[\begin{array}{l} x\cos(\alpha)- y\sin(\alpha)\\ x\ sin(\alpha) + y\ cos(\alpha) \end{array}\right] [cos(α)sin(α)sin(α)cos(α)][xy]=[xcos(α)ysin(α)x sin(α)+y cos(α)]

二维平面上的仿射变换

由上面的实验我们可以了解,任何平面上的线性变换都可以由缩放、平移和旋转组合而成,具体的公式如下:
[ a b c d ] [ x y ] + [ e f ] = [ a x + b y + e c x + d y + f ] \left[\begin{array}{} a&b \\ c&d \end{array}\right] \left[\begin{array}{} x \\ y \end{array}\right] + \left[\begin{array}{} e \\ f \end{array}\right] = \left[\begin{array}{} ax + by + e \\ cx + dy + f \end{array}\right] [acbd][xy]+[ef]=[ax+by+ecx+dy+f]
我们可以通过这个方程完成平移,缩放和旋转,进而可以完成一切二维平面上的仿射变换。

齐次坐标

在前面我们得到了仿射变换的具体变换过程,但是对于上面的公式我们认为还是太复杂了,对于我们操作物体进行上百次或者上千次变换后,上面的式子就会变得复杂而臃肿,我们希望用一种简洁的方式表达。我们可以使用齐次坐标完成对上面式子的简化,我们在坐标中增加一维,得到下式:
[ a b e c d f 0 0 1 ] [ x y 1 ] = [ a x + b y + e c x + d y + f 1 ] \left[\begin{array}{} a&b&e \\ c&d&f \\ 0&0&1 \end{array}\right] \left[\begin{array}{} x \\ y \\ 1 \end{array}\right] = \left[\begin{array}{} ax + by + e \\ cx + dy + f \\ 1 \end{array}\right] ac0bd0ef1 xy1 = ax+by+ecx+dy+f1
事实上,这个想法真的很简单。平行平移在二维空间中不是线性的。因此,我们将2D嵌入到3D空间中(只需为第三个组件添加1)。这意味着我们的2D空间是3D空间中的平面z=1。然后我们进行线性三维变换,并将结果投影到我们的二维物理平面上。

我们如何将3D投影回2D平面?简单地除以三维分量:
[ x y z ] ⟶ [ x / z y / z ] \left[\begin{array}{} x \\ y \\ z \end{array}\right] \longrightarrow \left[\begin{array}{} x/z \\ y/z \end{array}\right] xyz [x/zy/z]
我们引入了齐次坐标后,我们可以将上面的变换拆分,即对物体进行旋转、平移和缩放,这称之为Model矩阵:
M = [ 1 0 x 0 0 1 y 0 0 0 1 ] [ c o s ( α ) − s i n ( α ) 0 s i n ( α ) − c o s ( α ) 0 0 0 1 ] [ 1 0 − x 0 0 1 − y 0 0 0 1 ] M= \left[\begin{array}{} 1&0&x_0 \\ 0&1&y_0 \\ 0&0&1 \end{array}\right] \left[\begin{array}{l}cos(\alpha)&-sin(\alpha)&0 \\ sin(\alpha)&-cos(\alpha)&0 \\ 0&0&1\end{array}\right] \left[\begin{array}{} 1&0&-x_0 \\ 0&1&-y_0 \\ 0&0&1 \end{array}\right] M= 100010x0y01 cos(α)sin(α)0sin(α)cos(α)0001 100010x0y01

那么对于变换矩阵中的最后一行代表什么呢?当我们使用下面这个变换矩阵时:
[ 1 0 0 0 1 0 − 1 5 0 1 ] \left[\begin{array}{} 1&0&0 \\ 0&1&0 \\ -\frac{1}{5}&0&1 \end{array}\right] 1051010001
生成结果如下:
在这里插入图片描述

我们在原图(白色方形框中)使用类似之前的y-buffer的想法,我们想要将物体投影到垂直线x=0处,并且我们的相机在(0,5)的位置并且指向原点,为了找到投影,我们需要在相机和要投影的点之间绘制直线(黄色),并找到与屏幕线的交点(白色垂直)

img

之后替换原先的物体,但不改变之前绘制的黄线:

img

如果我们使用标准正交投影将红色物体投影到屏幕上,那么我们会找到完全相同的点!让我们仔细看看变换是如何工作的:所有垂直片段都被变换为垂直片段,但靠近相机的片段被拉伸,远离相机的片段则被收缩。

三维平面

同样的对于三位平面,我们也是用齐次坐标 ( x , y , z , 1 ) (x,y,z,1) (x,y,z,1)扩充点 ( x , y , z ) (x,y,z) (x,y,z),然后我们在4D中进行变换并将其投影回3D。转换公式如下:
[ 1 0 0 0 0 1 0 0 0 0 1 0 0 0 r 1 ] [ x y z 1 ] = [ x y z r z + 1 ] \left[\begin{array}{} 1&0&0&0 \\ 0&1&0&0 \\ 0&0&1&0 \\ 0&0&r&1 \end{array} \right] \left[\begin{array}{} x \\ y \\ z \\ 1 \end{array}\right] = \left[\begin{array}{} x \\ y \\ z \\ rz + 1 \end{array}\right] 10000100001r0001 xyz1 = xyzrz+1

[ x y z r z + 1 ] ⟶ [ x r z + 1 y r z + 1 z r z + 1 ] \left[\begin{array}{} x \\ y \\ z \\ rz + 1 \end{array}\right] \longrightarrow \left[\begin{array}{} \frac{x}{rz + 1} \\ \frac{y}{rz+1} \\ \frac{z}{rz+1} \end{array}\right] xyzrz+1 rz+1xrz+1yrz+1z

我们在3D中给出一个点p的坐标 ( x , y , z ) (x,y,z) (x,y,z),我们想要将其投影到平面z上,同时相机在z轴上(0,0,c)处。
在这里插入图片描述

通过相似三角形的等比关系,我们可以推出下面两个等式:
x ′ = x 1 − z / c y ′ = y 1 − z / c x'=\frac{x}{1-z/c}\\ y'=\frac{y}{1-z/c} x=1z/cxy=1z/cy
最终我们可以将上面所有变换组合起来
[ x y z ] ⟶ [ x y z 1 ] [ 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 ] [ x y z 1 ] = [ x y z 1 − z / c ] [ x y z 1 − z / c ] ⟶ [ x 1 − z / c y 1 − z / c z 1 − z / c ] \left[\begin{array}{} x \\ y \\ z \end{array}\right] \longrightarrow \left[\begin{array}{} x \\ y \\ z \\ 1 \end{array}\right] \\ \\ \left[\begin{array}{} 1&0&0&0 \\ 0&1&0&0 \\ 0&0&1&0 \\ 0&0&0&1 \end{array}\right] \left[\begin{array}{} x \\ y \\ z \\ 1 \end{array}\right] = \left[\begin{array}{} x \\ y \\ z \\ 1-z/c \end{array}\right] \\ \\ \left[\begin{array}{} x \\ y \\ z \\ 1 - z/c \end{array}\right] \longrightarrow \left[\begin{array}{} \frac{x}{1 - z/c} \\ \frac{y}{1 - z/c} \\ \frac{z}{1 - z/c} \end{array}\right] xyz xyz1 1000010000100001 xyz1 = xyz1z/c xyz1z/c 1z/cx1z/cy1z/cz

#include <vector>
#include <cmath>
#include <limits>
#include "tgaimage.h"
#include "model.h"
#include "geometry.h"

const int width = 800;
const int height = 800;
const int depth = 255;

Model* model = NULL;
int* zbuffer = NULL;
Vec3f light_dir(0, 0, -1);
Vec3f camera(0, 0, 3);

Vec3f m2v(Matrix m) {
    return Vec3f(m[0][0] / m[3][0], m[1][0] / m[3][0], m[2][0] / m[3][0]);
}

Matrix v2m(Vec3f v) {
    Matrix m(4, 1);
    m[0][0] = v.x;
    m[1][0] = v.y;
    m[2][0] = v.z;
    m[3][0] = 1.f;
    return m;
}

Matrix viewport(int x, int y, int w, int h) {
    Matrix m = Matrix::identity(4);
    m[0][3] = x + w / 2.f;
    m[1][3] = y + h / 2.f;
    m[2][3] = depth / 2.f;

    m[0][0] = w / 2.f;
    m[1][1] = h / 2.f;
    m[2][2] = depth / 2.f;
    return m;
}

void triangle(Vec3i t0, Vec3i t1, Vec3i t2, Vec2i uv0, Vec2i uv1, Vec2i uv2, TGAImage& image, float intensity, float* zbuffer) {
    if (t0.y == t1.y && t0.y == t2.y) return; 
    if (t0.y > t1.y) { std::swap(t0, t1); std::swap(uv0, uv1); }
    if (t0.y > t2.y) { std::swap(t0, t2); std::swap(uv0, uv2); }
    if (t1.y > t2.y) { std::swap(t1, t2); std::swap(uv1, uv2); }
	//这里直接使用画上下三角形的方法对三角形着色
    //自己写的时候是用重心坐标,但重心坐标在透视投影下的校正写的一直不对
    int total_height = t2.y - t0.y;
    for (int i = 0; i < total_height; i++) {
        bool second_half = i > t1.y - t0.y || t1.y == t0.y;
        int segment_height = second_half ? t2.y - t1.y : t1.y - t0.y;
        float alpha = (float)i / (float)total_height;
        float beta = (float)(i - (second_half ? t1.y - t0.y : 0)) / (float)segment_height; 
        Vec3i A = t0 + Vec3f(t2 - t0) * alpha;
        Vec3i B = second_half ? t1 + Vec3f(t2 - t1) * beta : t0 + Vec3f(t1 - t0) * beta;
        Vec2i uvA = uv0 + (uv2 - uv0) * alpha;
        Vec2i uvB = second_half ? uv1 + (uv2 - uv1) * beta : uv0 + (uv1 - uv0) * beta;
        if (A.x > B.x) { std::swap(A, B); std::swap(uvA, uvB); }
        for (int j = A.x; j <= B.x; j++) {
            float phi = B.x == A.x ? 1. : (float)(j - A.x) / (float)(B.x - A.x);
            Vec3f   P = Vec3f(A) + Vec3f(B - A) * phi;
            Vec2i uvP = uvA + (uvB - uvA) * phi;
            int idx = P.x + P.y * width;
            if (zbuffer[idx] < P.z) {
                zbuffer[idx] = P.z;
                TGAColor color = model->diffuse(uvP);
                TGAColor lightcolor = TGAColor(color[0] * intensity, color[1] * intensity, color[2] * intensity, 1);
                image.set(P.x, P.y, lightcolor);
            }
        }
    }
}

int main() {
    
    model = new Model("obj/african_head/african_head.obj");
    model->load_texture("obj/african_head/african_head_diffuse.tga");

    float* zbuffer = new float[width * height];
    for (int i = 0; i < width * height; i++) {
        zbuffer[i] = std::numeric_limits<float>::min();
    }

    { // draw the model
        Matrix Projection = Matrix::identity(4);
        Matrix ViewPort = viewport(width / 8, height / 8, width * 3 / 4, height * 3 / 4);
        Projection[3][2] = -1.f / camera.z;

        TGAImage image(width, height, TGAImage::RGB);
        for (int i = 0; i < model->nfaces(); i++) {
            std::vector<int> face = model->face(i);
            Vec3i screen_coords[3];
            Vec3f world_coords[3];
            for (int j = 0; j < 3; j++) {
                Vec3f v = model->vert(face[j]);
                screen_coords[j] = m2v(ViewPort * Projection * v2m(v));
                world_coords[j] = v;
            }
            Vec3f n = (world_coords[2] - world_coords[0]) ^ (world_coords[1] - world_coords[0]);
            n.normalize();
            float intensity = n * light_dir;
            if (intensity > 0) {
                Vec2i uv[3];
                for (int k = 0; k < 3; k++) {
                    uv[k] = model->uv(i, k);
                }
                triangle(screen_coords[0], screen_coords[1], screen_coords[2], uv[0], uv[1], uv[2], image, intensity, zbuffer);
            }
        }

        image.write_tga_file("translation.tga");
    }

    { // dump z-buffer (debugging purposes only)
        TGAImage zbimage(width, height, TGAImage::GRAYSCALE);
        for (int i = 0; i < width; i++) {
            for (int j = 0; j < height; j++) {
                TGAColor color = TGAColor(zbuffer[i + j * width], zbuffer[i + j * width], zbuffer[i + j * width], 1);
                zbimage.set(i, j, color);
            }
        }
        zbimage.write_tga_file("zbuffer.tga");
    }
    delete model;
    delete[] zbuffer;
    return 0;
}

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值