首页 > 其他分享 >TinyRender学习笔记

TinyRender学习笔记

时间:2024-05-05 23:46:17浏览次数:36  
标签:Vec3f const pt int image 笔记 学习 coords TinyRender

通过手写软光栅渲染器加深对计算机图形学基本原理的理解,并练习C++面向对象程序设计。

github链接:blackbird2003/blackbirdTinyRenderer (github.com)

该项目主要参考Home · ssloy/tinyrenderer Wiki (github.com)编写

推荐先过一下GAMES101

Lesson 0 Getting Started

Using TGA image format

使用这个基本框架来生成TGA格式图像:
ssloy/tinyrenderer at 909fe20934ba5334144d2c748805690a1fa4c89f (github.com)

只需 #include "tgaimage.h" ,并在编译时链接tgaimage.cpp即可。

例:在屏幕上将像素(52,41)设置为红色

#include "tgaimage.h"
//Set color with RGB
const TGAColor white = TGAColor(255, 255, 255, 255);
const TGAColor red   = TGAColor(255, 0,   0,   255);
int main(int argc, char** argv) {
    	//Set image size
        TGAImage image(100, 100, TGAImage::RGB);
    	//Set pixel color
        image.set(52, 41, red);
        //To have the origin at the left bottom corner of the image
        image.flip_vertically(); 
        image.write_tga_file("output.tga");
        return 0;
}

个人推荐的环境:Clion + CMake。(因为VsCode CMake调试功能实在搞不懂=.=)

涉及导入模型,需要将工作目录设置为工程文件夹

image-20240505230016266

image-20240505230043985

但我的Clion存在tga图像无法加载的bug。在设置->编辑器->文件类型中去掉.tga,然后选择用本地程序打开即可。

Lesson 1 Bresenham’s Line Drawing Algorithm

使用Bresenham算法绘制线段。

原理:https://en.wikipedia.org/wiki/Bresenham's_line_algorithm

实现参考:https://rosettacode.org/wiki/Bitmap/Bresenham's_line_algorithm#C++

建议绘制斜率小于-1,-1到0,0到1,大于1,以及水平和垂直的直线来检验算法正确性。

#include <cmath>

#include "tgaimage.h"

const TGAColor white = TGAColor(255, 255, 255, 255);
const TGAColor red = TGAColor(255, 0, 0, 255);
const TGAColor blue = TGAColor(0, 0, 255, 255);

void line(int x1, int y1, int x2, int y2, TGAImage& image, TGAColor color)
{
    //Ensure that slope in (0, 1)
    const bool steep = (std::abs(y2 - y1) > std::abs(x2 - x1));
    if (steep) {
        std::swap(x1, y1);
        std::swap(x2, y2);
    }
    if (x1 > x2) {
        std::swap(x1, x2);
        std::swap(y1, y2);
    }

    const float dx = x2 - x1;
    const float dy = fabs(y2 - y1);

    float error = dx / 2.0f;
    const int ystep = (y1 < y2) ? 1 : -1;
    int y = (int)y1;

    const int maxX = (int)x2;

    for (int x = (int)x1; x <= maxX; x++) {
        if (steep) {
            image.set(y, x, color);
        } else {
            image.set(x, y, color);
        }

        error -= dy;
        if (error < 0) {
            y += ystep;
            error += dx;
        }
    }
}

int main(int argc, char** argv)
{
    TGAImage image(100, 100, TGAImage::RGB);
    line(13, 20, 80, 40, image, red);
    line(55, 33, 22, 66, image, blue);
    line(33, 33, 66, 66, image, white);
    line(44, 20, 44, 80, image, white);
    line(20, 44, 80, 44, image, white);
    image.flip_vertically();
    image.write_tga_file("output.tga");
    return 0;
}

效果

image-20240419183705501

Lesson 2: Triangle rasterization and back face culling

三维物体模型通常以三角形为基础。为了方便表示点、向量、多边形,写geometry.h。

#pragma once

template <class t> struct Vec2 {
    union {
        struct {t u, v;};
        struct {t x, y;};
        t raw[2];
    };
    Vec2() : u(0), v(0) {}
    Vec2(t _u, t _v) : u(_u),v(_v) {}
    inline Vec2<t> operator +(const Vec2<t> &V) const { return Vec2<t>(u+V.u, v+V.v); }
    inline Vec2<t> operator -(const Vec2<t> &V) const { return Vec2<t>(u-V.u, v-V.v); }
    inline Vec2<t> operator *(float f)          const { return Vec2<t>(u*f, v*f); }
    template <class > friend std::ostream& operator<<(std::ostream& s, Vec2<t>& v);
};

template <class t> struct Vec3 {
    union {
        struct {t x, y, z;};
        struct { t ivert, iuv, inorm; };
        t raw[3];
    };
    Vec3() : x(0), y(0), z(0) {}
    Vec3(t _x, t _y, t _z) : x(_x),y(_y),z(_z) {}
    inline Vec3<t> operator ^(const Vec3<t> &v) const { return Vec3<t>(y*v.z-z*v.y, z*v.x-x*v.z, x*v.y-y*v.x); }
    inline Vec3<t> operator +(const Vec3<t> &v) const { return Vec3<t>(x+v.x, y+v.y, z+v.z); }
    inline Vec3<t> operator -(const Vec3<t> &v) const { return Vec3<t>(x-v.x, y-v.y, z-v.z); }
    inline Vec3<t> operator *(float f)          const { return Vec3<t>(x*f, y*f, z*f); }
    inline t       operator *(const Vec3<t> &v) const { return x*v.x + y*v.y + z*v.z; }
    float norm () const { return std::sqrt(x*x+y*y+z*z); }
    Vec3<t> & normalize(t l=1) { *this = (*this)*(l/norm()); return *this; }
    template <class > friend std::ostream& operator<<(std::ostream& s, Vec3<t>& v);
};

typedef Vec2<float> Vec2f;
typedef Vec2<int>   Vec2i;
typedef Vec3<float> Vec3f;
typedef Vec3<int>   Vec3i;

template <class t> std::ostream& operator<<(std::ostream& s, Vec2<t>& v) {
    s << "(" << v.x << ", " << v.y << ")\n";
    return s;
}

template <class t> std::ostream& operator<<(std::ostream& s, Vec3<t>& v) {
    s << "(" << v.x << ", " << v.y << ", " << v.z << ")\n";
    return s;
}

如何画出实心的三角形?一般来说,有扫描线和边界函数两种算法。

对于多线程的CPU,采用边界函数法更为高效:先找到三角形的矩形包围盒,再逐点判断是否在三角形中

triangle(vec2 points[3]) { 
    vec2 bbox[2] = find_bounding_box(points); 
    for (each pixel in the bounding box) { 
        if (inside(points, pixel)) { 
            put_pixel(pixel); 
        } 
    } 
}

因此,问题变成了给定三角形的三个点,如何判断点是否在三角形内部

一种最好的办法是,计算给定点关于给定三角形的重心坐标(或者叫面积坐标)。

维基百科:https://zh.wikipedia.org/wiki/重心坐标

简单来说,它表示一个点所对的三条边形成的三角形面积比。如果点在三角形外部,则有一个维度是负的。

image-20240419185822971

由于tinyrenderer的作者写得有些丑陋,我在geometry.h里直接加入了polygon和triangle类,来实现重心坐标计算和点在三角形内的检测

template <class T>
class Polygon2D {
public:
    int n;
    std::vector<Vec2<T>> pt;
    Polygon2D(int _n, std::vector<Vec2<T>> _pt): n(_n), pt(_pt) {}
};

template <class T>
class Triangle2D: public Polygon2D<T> {
public:
    using Polygon2D<T>::pt;
    Triangle2D(std::vector<Vec2<T>> _pt): Polygon2D<T>(3, _pt) {}
    Vec3f baryCentric(Vec2i P) {
        Vec3f u = Vec3f(pt[2][0]-pt[0][0], pt[1][0]-pt[0][0], pt[0][0]-P[0])^Vec3f(pt[2][1]-pt[0][1], pt[1][1]-pt[0][1], pt[0][1]-P[1]);
        /* `pts` and `P` has integer value as coordinates
           so `abs(u[2])` < 1 means `u[2]` is 0, that means
           triangle is degenerate, in this case return something with negative coordinates */
        if (std::abs(u.z)<1) return Vec3f(-1,1,1);
        return Vec3f(1.f-(u.x+u.y)/u.z, u.y/u.z, u.x/u.z);
    }

    bool inInside(Vec2i P) {
        auto bc = baryCentric(P);
        if (bc.x<0 || bc.y<0 || bc.z<0) return false;
        return true;
    }
};

在main.cpp里绘制实心三角形

//Iterate all points in the rectangular bounding box of triangle, draw if the point is inside
void drawSolidTriangle(Triangle2D<int> tri, TGAImage &image, TGAColor color) {
    Vec2i bboxmin(image.get_width()-1,  image.get_height()-1);
    Vec2i bboxmax(0, 0);
    Vec2i clamp(image.get_width()-1, image.get_height()-1);
    for (int i=0; i<3; i++) {
        bboxmin.x = std::max(0, std::min(bboxmin.x, tri.pt[i].x));
        bboxmin.y = std::max(0, std::min(bboxmin.y, tri.pt[i].y));

        bboxmax.x = std::min(clamp.x, std::max(bboxmax.x, tri.pt[i].x));
        bboxmax.y = std::min(clamp.y, std::max(bboxmax.y, tri.pt[i].y));
    }
    Vec2i P;
    for (P.x=bboxmin.x; P.x<=bboxmax.x; P.x++) {
        for (P.y=bboxmin.y; P.y<=bboxmax.y; P.y++) {
            Vec3f bc_screen  = tri.baryCentric(P);
            if (bc_screen.x<0 || bc_screen.y<0 || bc_screen.z<0) continue;
            image.set(P.x, P.y, color);
        }
    }
};

得到如图效果:

image-20240419195714296

三角形绘制完成后,可以尝试导入作者提供的由三角形构成的人脸模型。

.obj模型文件的格式如下

# List of geometric vertices, with (x, y, z, [w]) coordinates, w is optional and defaults to 1.0.
v 0.123 0.234 0.345 1.0
v ...
...
# List of texture coordinates, in (u, [v, w]) coordinates, these will vary between 0 and 1. v, w are optional and default to 0.
vt 0.500 1 [0]
vt ...
...
# List of vertex normals in (x,y,z) form; normals might not be unit vectors.
vn 0.707 0.000 0.707
vn ...
...
# Parameter space vertices in (u, [v, w]) form; free form geometry statement (see below)
vp 0.310000 3.210000 2.100000
vp ...
...
# Polygonal face element (see below)
f 1 2 3
f 3/1 4/2 5/3
f 6/4/1 3/5/3 7/6/5
f 7//1 8//2 9//3
f ...
...
# Line element (see below)
l 5 8 1 2 4 9

目前,我们暂时不关心模型的深度(z坐标),只是将模型正投影到XY平面上,则模型上的点对应的屏幕坐标可以这样简单的计算

screen_coords[j] = Vec2i((v.x+1.)*width/2., (v.y+1.)*height/2.);

假设光从正前方射向正后方,即光线方向(0,0,-1)。

在这里,我们使用一种简化的亮度计算方法:我们忽略面与光源之间的距离差异,认为正对着光源的面(法线与光线方向相同)最亮,这样就可以计算每个三角形面的单位法向量与光线方向的叉积来代表亮度。

int main(int argc, char** argv) {
    if (2==argc) {
        model = new Model(argv[1]);
    } else {
        model = new Model("obj/african_head.obj");
    }

    TGAImage image(width, height, TGAImage::RGB);
    Vec3f light_dir(0,0,-1);
    int cnt = 0;
    for (int i=0; i<model->nfaces(); i++) {
        std::vector<int> face = model->face(i);
        Vec2i screen_coords[3];
        Vec3f world_coords[3];
        for (int j=0; j<3; j++) {
            Vec3f v = model->vert(face[j]);
            screen_coords[j] = Vec2i((v.x+1.)*width/2., (v.y+1.)*height/2.);
            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) {
            printf("ok %d\n", ++cnt);
            drawSolidTriangle(Triangle2D<int>({screen_coords[0], screen_coords[1], screen_coords[2]}), image, TGAColor(intensity*255, intensity*255, intensity*255, 255));
        }
    }

    image.flip_vertically();
    image.write_tga_file("output.tga");
    delete model;
    return 0;
}

在这种简化下,得到的渲染结果如下:
image-20240419205641217

可以发现,位于口腔中的三角形遮住了嘴唇。下一节课中,我们将考虑深度测试,正确处理多边形的遮挡关系。

Lesson 3: Hidden faces removal (z buffer)

深度检测算法的基本原理是,引入一个大小为像素数量的Z-Buffer数组,初始化所有像素点深度为负无穷。

在遍历像素点时,比较当前三角形上点的深度是否小于Z-Buffer的数值,如果小于,则更新该像素并更新Z-Buffer。

为此,我们需要为屏幕坐标增加一维深度(对于上面的人脸设置为模型的z即可)。在drawSolidTriangle()中增加对深度缓冲区的判断。

//Iterate all points in the rectangular bounding box of triangle, draw if the point is inside
// 2024 04 26 2d->3d
void drawSolidTriangle(Triangle2D<float> tri, TGAImage &image, TGAColor color, float *zbuffer) {
    Vec2f bboxmin(image.get_width()-1,  image.get_height()-1);
    Vec2f bboxmax(0, 0);
    Vec2f clamp(image.get_width()-1, image.get_height()-1);
    for (int i=0; i<3; i++) {
        bboxmin.x = std::max((float)0, std::min(bboxmin.x, tri.pt[i].x));
        bboxmin.y = std::max((float)0, std::min(bboxmin.y, tri.pt[i].y));

        bboxmax.x = std::min(clamp.x, std::max(bboxmax.x, tri.pt[i].x));
        bboxmax.y = std::min(clamp.y, std::max(bboxmax.y, tri.pt[i].y));
    }
    Vec3i P;
    for (P.x=bboxmin.x; P.x<=bboxmax.x; P.x++) {
        for (P.y=bboxmin.y; P.y<=bboxmax.y; P.y++) {
            Vec3f bc_screen  = tri.baryCentric(P.toVec2());//toTriangle2D().baryCentric(P);
            if (bc_screen.x<0 || bc_screen.y<0 || bc_screen.z<0) continue;
            
            //bugfix
            P.z = tri.depth[0] * bc.x + tri.depth[1] * bc.y + tri.depth[2] * bc.z;

            int idx = P.x+P.y*width;
            if (zbuffer[idx]<P.z) {
                zbuffer[idx] = P.z;
                image.set(P.x, P.y, color);
            }
        }
    }
};




Vec3f worldToScreen(Vec3f v) {
    return Vec3f(int((v.x+1.)*width/2.+.5), int((v.y+1.)*height/2.+.5), v.z);
}

int main(int argc, char** argv) {
    if (2==argc) {
        model = new Model(argv[1]);
    } else {
        model = new Model("obj/african_head.obj");
    }

    float *zbuffer = new float[width * height];
    for (int i=width*height; i--; zbuffer[i] = -std::numeric_limits<float>::max());


    TGAImage image(width, height, TGAImage::RGB);
    Vec3f light_dir(0,0,-1);
    int cnt = 0;
    for (int i=0; i<model->nfaces(); i++) {
        std::vector<int> face = model->face(i);
        Vec3f screen_coords[3];
        Vec3f world_coords[3];
        for (int j=0; j<3; j++) {
            Vec3f v = model->vert(face[j]);
            //screen_coords[j] = Vec2i((v.x+1.)*width/2., (v.y+1.)*height/2.);
            world_coords[j]  = v;
            screen_coords[j] = worldToScreen(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) {
            printf("ok %d\n", ++cnt);
            drawSolidTriangle(Triangle2D<float>({screen_coords[0], screen_coords[1], screen_coords[2]}), image, TGAColor(intensity*255, intensity*255, intensity*255, 255), zbuffer);
        }
    }

    image.flip_vertically();
    image.write_tga_file("output.tga");
    delete model;
    return 0;
}

同时,在Triangle2D类中加入depth数组即可

template <class T>
class Triangle2D: public Polygon2D<T> {
public:
    using Polygon2D<T>::pt;
    std::vector<T> depth;
    
    Triangle2D(std::vector<Vec2<T>> _pt, std::vector<Vec2<T>> _depth = {0, 0, 0}): 
        Polygon2D<T>(3, _pt), 
        depth(_depth) {}
        
    Triangle2D(std::vector<Vec3<float>> _pt):
        Polygon2D<T>(3, {_pt[0].toVec2(), _pt[1].toVec2(), _pt[2].toVec2()}),
        depth({_pt[0].z, _pt[1].z, _pt[2].z}) {}
        
    Vec3f baryCentric(Vec2f P) {
        Vec3f u = Vec3f(pt[2][0]-pt[0][0], pt[1][0]-pt[0][0], pt[0][0]-P[0])^Vec3f(pt[2][1]-pt[0][1], pt[1][1]-pt[0][1], pt[0][1]-P[1]);
        /* `pts` and `P` has integer value as coordinates
           so `abs(u[2])` < 1 means `u[2]` is 0, that means
           triangle is degenerate, in this case return something with negative coordinates */
        if (std::abs(u.z)<1) return Vec3f(-1,1,1);
        return Vec3f(1.f-(u.x+u.y)/u.z, u.y/u.z, u.x/u.z);
    }

    bool inInside(Vec2i P) {
        auto bc = baryCentric(P);
        if (bc.x<0 || bc.y<0 || bc.z<0) return false;
        return true;
    }
};



效果如图所示:
image-20240426175739547

Bouns: Texture Mapping

在.obj文件中,有以“vt u v”开头的行,它们给出了一个纹理坐标数组。

The number in the middle (between the slashes) in the facet lines "f x/x/x x/x/x x/x/x" are the texture coordinates of this vertex of this triangle. Interpolate it inside the triangle, multiply by the width-height of the texture image and you will get the color to put in your render.

tinyrender作者提供了漫反射纹理: african_head_diffuse.tga

据此,我们可以给上述人脸模型添加纹理。此时,main函数中drawSolidTriangle函数里不需要再传入颜色,只需要传入intensity即可,另外需要传入当前三角形三个点的纹理坐标uv。

//Iterate all points in the rectangular bounding box of triangle, draw if the point is inside
// 2024 04 26 2d->3d, texture mapping
void drawSolidTriangle(Triangle2D<float> tri, Vec2i* uv, TGAImage &image, float intensity, float *zbuffer) {
    Vec2f bboxmin(image.get_width()-1,  image.get_height()-1);
    Vec2f bboxmax(0, 0);
    Vec2f clamp(image.get_width()-1, image.get_height()-1);
    for (int i=0; i<3; i++) {
        bboxmin.x = std::max((float)0, std::min(bboxmin.x, tri.pt[i].x));
        bboxmin.y = std::max((float)0, std::min(bboxmin.y, tri.pt[i].y));

        bboxmax.x = std::min(clamp.x, std::max(bboxmax.x, tri.pt[i].x));
        bboxmax.y = std::min(clamp.y, std::max(bboxmax.y, tri.pt[i].y));
    }
    Vec3i P;
    for (P.x=bboxmin.x; P.x<=bboxmax.x; P.x++) {
        for (P.y=bboxmin.y; P.y<=bboxmax.y; P.y++) {
            Vec3f bc  = tri.baryCentric(P.toVec2());//toTriangle2D().baryCentric(P);
            if (bc.x<0 || bc.y<0 || bc.z<0) continue;
            
            P.z = tri.depth[0] * bc.x + tri.depth[1] * bc.y + tri.depth[2] * bc.z;

            int idx = P.x+P.y*width;
            if (zbuffer[idx]<P.z) {
                zbuffer[idx] = P.z;

                Vec2i P_uv = uv[0] * bc.x + uv[1] * bc.y + uv[2] * bc.z;
                TGAColor color = model->diffuse(P_uv);
                image.set(P.x, P.y, color);
            }
        }
    }
};




Vec3f worldToScreen(Vec3f v) {
    return Vec3f(int((v.x+1.)*width/2.+.5), int((v.y+1.)*height/2.+.5), v.z);
}

int main(int argc, char** argv) {
    if (2==argc) {
        model = new Model(argv[1]);
    } else {
        model = new Model("obj/african_head.obj");
    }

    float *zbuffer = new float[width * height];
    for (int i=width*height; i--; zbuffer[i] = -std::numeric_limits<float>::max());


    TGAImage image(width, height, TGAImage::RGB);
    Vec3f light_dir(0,0,-1);
    int cnt = 0;
    for (int i=0; i<model->nfaces(); i++) {
        std::vector<int> face = model->face(i);
        Vec3f screen_coords[3];
        Vec3f world_coords[3];
        for (int j=0; j<3; j++) {
            Vec3f v = model->vert(face[j]);
            //screen_coords[j] = Vec2i((v.x+1.)*width/2., (v.y+1.)*height/2.);
            world_coords[j]  = v;
            screen_coords[j] = worldToScreen(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) {
            printf("ok %d\n", ++cnt);
            Vec2i uv[3];
            for (int j = 0; j < 3; j++) uv[j] = model->uv(i, j);
            drawSolidTriangle(Triangle2D<float>({screen_coords[0], screen_coords[1], screen_coords[2]}), uv, image, intensity, zbuffer);
        }
    }

    image.flip_vertically();
    image.write_tga_file("output.tga");
    delete model;
    return 0;
}

model.h和model.cpp需要修改以支持纹理。作者在lesson4的结尾放出了代码。

效果:

image-20240426190246539

这是一个平行投影的结果,损失了一部分真实感,例如,虽然耳朵旁边的头发在xoy平面上不与脸部重叠,但实际上应该被前边的皮肤遮挡,因为人眼/相机本身是“点光源”,而不是“平行光源”,物体发出的光线最终汇聚于一点,也就是所谓的“透视”。下面将引入透视投影:

Lesson 4: Perspective projection

齐次坐标

img

简单变换(图来自GAMES101)

image-20240505174927834

逆变换

image-20240505175454080

复合变换

image-20240505175019922

实现矩阵类:

const int DEFAULT_D = 4;
class Matrix {
    std::vector<std::vector<float>> m;
    int nrow, ncol;
public:
    Matrix(int r=DEFAULT_D, int c=DEFAULT_D) :
        m(std::vector<std::vector<float>> (r, std::vector<float>(c, 0.f))),
        nrow(r), ncol(c) {}

    int get_nrow() { return nrow; }
    int get_ncol() { return ncol; }

    static Matrix identity(int dimensions) {
        Matrix E(dimensions, dimensions);
        for (int i = 0; i < dimensions; i++)
            E[i][i] = 1;
        return E;
    }

    std::vector<float>& operator[](const int i) {
        assert(i >= 0 && i < nrow);
        return m[i];
    }

    const std::vector<float>& operator[](const int i) const {
        assert(i >= 0 && i < nrow);
        return m[i];
    }

    Matrix operator*(const Matrix& a) {
        assert(this->ncol == a.nrow);
        Matrix res(this->nrow, a.ncol);
        for (int i = 0; i < this->nrow; i++) {
            for (int j = 0; j < a.ncol; j++) {
                res.m[i][j] = 0;
                for (int k = 0; k < this->ncol; k++)
                    res.m[i][j] += this->m[i][k]*a.m[k][j];
            }
        }
        return res;
    }

    Matrix transpose() {
        Matrix res(ncol, nrow);
        for (int i = 0; i < ncol; i++)
            for (int j = 0; j < nrow; j++)
                res.m[i][j] = m[j][i];
        return res;
    }
    Matrix inverse() {
        assert(nrow==ncol);
        // augmenting the square matrix with the identity matrix of the same dimensions a => [ai]
        Matrix result(nrow, ncol*2);
        for(int i=0; i<nrow; i++)
            for(int j=0; j<ncol; j++)
                result[i][j] = m[i][j];
        for(int i=0; i<nrow; i++)
            result[i][i+ncol] = 1;
        // first pass
        for (int i=0; i<nrow-1; i++) {
            // normalize the first row
            for(int j=result.ncol-1; j>=0; j--)
                result[i][j] /= result[i][i];
            for (int k=i+1; k<nrow; k++) {
                float coeff = result[k][i];
                for (int j=0; j<result.ncol; j++) {
                    result[k][j] -= result[i][j]*coeff;
                }
            }
        }
        // normalize the last row
        for(int j=result.ncol-1; j>=nrow-1; j--)
            result[nrow-1][j] /= result[nrow-1][nrow-1];
        // second pass
        for (int i=nrow-1; i>0; i--) {
            for (int k=i-1; k>=0; k--) {
                float coeff = result[k][i];
                for (int j=0; j<result.ncol; j++) {
                    result[k][j] -= result[i][j]*coeff;
                }
            }
        }
        // cut the identity matrix back
        Matrix truncate(nrow, ncol);
        for(int i=0; i<nrow; i++)
            for(int j=0; j<ncol; j++)
                truncate[i][j] = result[i][j+ncol];
        return truncate;
    }

    friend std::ostream& operator<<(std::ostream& s, Matrix& m);
};

inline std::ostream& operator<<(std::ostream& s, Matrix& m) {
    for (int i = 0; i < m.nrow; i++)  {
        for (int j = 0; j < m.ncol; j++) {
            s << m[i][j];
            if (j<m.ncol-1) s << "\t";
        }
        s << "\n";
    }
    return s;
}

一个简单投影矩阵的推导:

假设相机位置为(0,0,c)成像平面为z=0,如图

img

根据三角形相似,x'/c = x/(c-z),即有

img

同理

img

为了实现z轴方向上靠近相机的线段被拉伸,远离相机的线段被压缩,投影矩阵具有这样的形式

img

根据齐次坐标的结果,得到对应的投影点坐标

img

根据上面的结果,可知r=-1/c。

我们可以得到一个简单情况下的投影矩阵,变换过程如图

img

在程序中,这个过程用如下方式实现:

screen_coords[j] = hc2v(viewportMatrix * projectionMatrix * v2hc(v));

(普通坐标 → 齐次坐标)

世界坐标 → (经投影变换)投影坐标 → (经视口变换)屏幕坐标

(齐次坐标 → 普通坐标)

这里的坐标包含位置(x,y)和深度z,深度交给z-buffer来处理

视口变化的目的是将投影区域映射到[-1,1]^3的立方体中,便于绘制

相关变化的实现:

//Transition between coordinates (vector type) and homogeneous coordinates (matrix type)
Matrix v2hc(const Vec3f &v) {
    Matrix hc(4, 1);
    hc[0][0] = v.x;
    hc[1][0] = v.y;
    hc[2][0] = v.z;
    hc[3][0] = 1;
    return hc;
}
Vec3f hc2v(const Matrix &hc) {
    return Vec3f(hc[0][0], hc[1][0], hc[2][0]) * (1.f / hc[3][0]);
}

Vec3f light_dir(0,0,-1);
Vec3f camera(0, 0, 3);
//project to z = 0
Matrix projection(const Vec3f &camera) {
    Matrix m = Matrix::identity(4);
    m[3][2] = -1.f/camera.z;
    return m;
}

//viewport(width / 8, height / 8, width * 0.75, height * 0.75);
//窗口边缘留出1/8空隙
Matrix viewport(int x, int y, int w, int h) {
    Matrix m = Matrix::identity(4);
    //Translation
    m[0][3] = x + w / 2.f;
    m[1][3] = y + h / 2.f;
    m[2][3] = depth / 2.f;
    //scale to [0, 1]
    m[0][0] = w / 2.f;
    m[1][1] = h / 2.f;
    m[2][2] = depth / 2.f;
    return m;
}

int main() {
	...

    Matrix projectionMatrix = projection(camera);
    Matrix viewportMatrix = viewport(width / 8, height / 8, width * 0.75, height * 0.75);

    ...
        
    for (int i=0; i<model->nfaces(); i++) {
        std::vector<int> face = model->face(i);
        Vec3f screen_coords[3];
        Vec3f world_coords[3];
        for (int j=0; j<3; j++) {
            Vec3f v = model->vert(face[j]);
            //world -> screen:
            //3d coordinate -> homogeneous coordinates
            //-> projection trans(camera at (0,0,c), project to plane z = 0)
            //-> viewport trans(to make central at (w/2,h/2,d/2))

            world_coords[j]  = v;
            screen_coords[j] = hc2v(viewportMatrix * projectionMatrix * v2hc(v));
        }

        //Still simplified light intensity
        Vec3f n = (world_coords[2]-world_coords[0])^(world_coords[1]-world_coords[0]);
        n.normalize();
        float intensity = n*light_dir;

        if (intensity>0) {
            printf("ok %d\n", ++cnt);
            Vec2i uv[3];
            for (int j = 0; j < 3; j++) uv[j] = model->uv(i, j);
            drawSolidTriangle(Triangle2D<float>({screen_coords[0], screen_coords[1], screen_coords[2]}), uv, image, intensity, zbuffer);
        }
    }
    ...
}

效果

image-20240504170937200

注:TinyRenderer的透视投影与GAMES101处理方式不同,GAMES101是把M[3][2]固定为1,求解M的第三行,而此处是固定第三行为(0 0 1 0),求解M[3][2]。

此处并没有“近平面”的概念,认为n=0,f=c。

下面是GAMES101给出的结果(第三行为0 0 A B):

image-20240505192224927 image-20240505192257807

Lesson 5: Moving the camera

之前,我们考虑了相机在(0,0,c),朝着-z方向看的情况。

对于任意的相机位置,需要三个向量来确定:相机坐标e,相机指向的点c,向上方向向量u,如图所示:

img

我们假定相机总是朝着-z方向看,而u朝向正y方向,据此就得到了一个新的坐标系x'y'z',

下面考虑如何将物体坐标[x,y,z]转化为新坐标系下的[x',y',z']。

img

首先回顾坐标[x,y,z]的定义,它是三个正交的单位向量i,j,k前面的系数

img

现在,我们有了新的单位向量i',j',k',那么一定存在矩阵M,使得

img

我们将OP写成OO'+O'P,与新的单位坐标建立联系:

img

将[i',j',k']用上面的式子表示,提出[i,j,k]:

img

左边用[x,y,z]的定义式替换,就得到了[x',y',z']与[x,y,z]的关系

img

关于look at的推导,此处写的有些混乱
建议参阅https://www.zhihu.com/question/447781866

下面是个人理解:

简单来说,设M是(0, 0, 0),[i,j,k]到eyepos, [i',j',k']的变换矩阵
则M=TR,先旋转后平移

其中旋转矩阵R根据单位向量左乘该矩阵得到新单位向量,很容易得到(此处r,u,v是i',j',k'在原坐标系下的坐标)

image-20240505220527743

而T则为原点平移到eye pos的平移矩阵 (C是eyepos)

image-20240505221319762

此为对坐标轴的变换矩阵,即,我们用M计算了新的单位向量在原坐标系下的坐标,而要得到原来单位向量在新坐标系下的坐标,显然应该左乘M的逆矩阵。这样,我们就求得了ModelView矩阵。

据此,编写lookup实现modelview的计算

Vec3f light_dir = Vec3f(0, 0, -1).normalize();
Vec3f eye(1, 1, 3);
Vec3f center(0, 0, 0);
Vec3f up(0, 1, 0);
//Vec3f camera(0, 0, 3);

//screen_coordinate = viewport * projection * modelview * world_coordinate
Matrix lookat(Vec3f eye, Vec3f center, Vec3f up) {
    Vec3f z = (eye - center).normalize();
    Vec3f x = (up ^ z).normalize();
    Vec3f y = (z ^ x).normalize();
    Matrix M_inv = Matrix::identity(4);
    Matrix T = Matrix::identity(4);
    //thanks https://www.zhihu.com/question/447781866
    for (int i = 0; i < 3; i++) {
        M_inv[0][i] = x[i];
        M_inv[1][i] = y[i];
        M_inv[2][i] = z[i];
        T[i][3] = -eye[i];
    }
    return M_inv * T;
}


Matrix projection(Vec3f eye, Vec3f center) {
    Matrix m = Matrix::identity(4);
    m[3][2] = -1.f / (eye - center).norm();
    //m[3][2] = -1.f / camera.z;
    return m;
}


int main() {
    ...
        
    Matrix modelviewMatrix = lookat(eye, center, up);
    Matrix projectionMatrix = projection(eye, center);
    Matrix viewportMatrix = viewport(width / 8, height / 8, width * 0.75, height * 0.75);

    ...
    screen_coords[j] = hc2v(viewportMatrix * projectionMatrix * modelviewMatrix * v2hc(v));
    ...
    
}

效果(目前有点bug)

image-20240505225818988

Bouns:Transformation of normal vectors

为了处理光照,我们将模型进行坐标变换后,如果模型提供了每个面的法向量,还需要将法向量也进行变换。

此处有一个结论:模型上的坐标通过矩阵M进行仿射变换,那么模型的法向量的变换矩阵是M的逆矩阵的转置。

证明:考虑平面方程 Ax+By+Cz=0,它的法向量是(A,B,C) ,写成矩阵形式为:

img

在两者之间插入M的逆和M:

img

由于坐标均为列向量,把左边写成转置形式:

img

因此,如果对坐标(x,y,z)做变换M,要满足原来的直线方程,对法向量的变换矩阵为M的逆矩阵的转置(或者转置再求逆,转置和求逆是可交换的,证明略)

标签:Vec3f,const,pt,int,image,笔记,学习,coords,TinyRender
From: https://www.cnblogs.com/vv123/p/18174106

相关文章

  • 《自动机理论、语言和计算导论》阅读笔记:p402-p427
    《自动机理论、语言和计算导论》学习第13天,p402-P427总结,总计26页。一、技术总结无。二、英语总结1.eludee--,assimilatedformofex-(out,away)+ludere(toplay,seeludicrous)。vt.ifsthyouwanteludesyou,youdonotsucceedinachievingit。p426,Mor......
  • sqlserver笔记
    明确的性能低的定义:在现有资源还没有达到最大吞吐量的前提下,系统如果不能满足合理的预期表现。最小化每个SQL的响应时间;合理增加吞吐量;减少网络延时优化磁盘IO、CPU能够协调、平衡的运行,合理的响应外部的请求,实现资源利用的最大化。影响性能的常见因素:1.数据库结构的设计--了解......
  • 如何用费曼技巧快速学习任何东西
    如何用费曼技巧快速学习任何东西为什么教学是理解的关键理查德·费曼是一位诺贝尔物理学奖得主,在量子力学、粒子物理等领域做出了重大贡献。他还开创了量子计算,引入了纳米技术的概念。他是康奈尔大学和加州理工学院的著名讲师。尽管取得了这些成就,费曼认为自己只是一个"努力......
  • 面试必问并发编程内存模型JMM与内存屏障剖析 学习
    总课程:1、JMM。每个线程会产生一个变量副本。如下图所示,第二个变量修改了变量initFlag,但线程1并不会退出,是因为每个线程产生了副本。----解决方法:volatileCPU缓存一致性协议:MESI机制,以及内存模型底层八大原子操作。Volatile缓存可见性实现原理:底层实现主要通过汇编lock前......
  • 深入学习和理解Django模板层:构建动态页面
    title:深入学习和理解Django模板层:构建动态页面date:2024/5/520:53:51updated:2024/5/520:53:51categories:后端开发tags:Django模板表单处理静态文件国际化性能优化安全防护部署实践第一章:模板语法基础Django模板语法介绍Django模板语法是一种简洁而......
  • filedownload-基于pikachu的学习
    Filedownload原理文件下载功能在很多web系统上都会出现,一般我们当点击下载链接,便会向后台发送一个下载请求,一般这个请求会包含一个需要下载的文件名称,后台在收到请求后会开始执行下载代码,将该文件名对应的文件response给浏览器,从而完成下载。如果后台在收到请求的文件名后,将......
  • 网络流学习笔记
    1.概述网络指的是一类特殊的有向图G=(V,E),与一般有向图不同的是有容量和源汇点对于网络G=(V,E),流是一个从边集E到整数集或实数集的函数,满足如下性质容量限制:对于每条边,该边流经的流量不得超过该边的容量流守恒性:除源汇点外,其余任何点的净流量为0,其中,我们定义节点u的净流......
  • 【学习方法】: 读书的方法
    【学习方法】:读书的方法    读书的方法。 读《历史》的方法。每段历史,都是一个场景;每段历史,都是一个抉择(选择做什么,选择不做什么)。每当我读历史类书籍的时候,置自己于历史之中。我想象自己在那段历史的环境中,自己会做什么不做什么;然后看看书中的人物做了什么......
  • m基于Yolov2深度学习网络的螺丝检测系统matlab仿真,带GUI界面
    1.算法仿真效果matlab2022a仿真结果如下:         2.算法涉及理论知识概要        基于YOLOv2(YouOnlyLookOnceversion2)深度学习网络的螺丝检测系统,是一种高效的目标检测方法,它在计算机视觉领域被广泛应用,尤其适合于实时检测和定位图像中的......
  • Redis基础篇笔记
    一、Redis入门1.认识NoSQL1.1 什么是NoSQLNoSQL最常见的解释是"non-relational",很多人也说它是"NotOnlySQL"NoSQL仅仅是一个概念,泛指非关系型的数据库区别于关系数据库,它们不保证关系数据的ACID特性NoSQL是一项全新的数据库革命性运动,提倡运用非关系型的数据存储,相对于......