Programming Exercises 1:Simplified CPU-based rasterized renderer

So far I have learned how to transform the objects in the virtual world to the screen. Now it's time to get some programming exercises.

Task:

Given three points under 3D points,

v0(2.0, 0.0, -2.0), v1(0.0, 2.0, -2.0), v2(-2.0, 0.0, -2.0)

you need to,

  • Transform the coordinates of these three points to screen coordinates and draw the corresponding wireframe triangle on the screen
  • Implement keyboard interaction. When the user presses A or D, the model rotates clockwise or counterclockwise around the Z(or X,Y,N)-axis accordingly

In short, we need to perform model, view, projection, viewport and other transformations to display the triangle on the screen.


1 Get Model Transformation Matrix

1.1 Rotate around the Z-axis

As an exercise, this function returns the transformation matrix that makes the model rotate around the Z-axis.

According to our introduction in CG01:Model Transformation In Computer Graphics, transformation matrix that makes the model rotate around the Z-axis can be defined as follows,

\large \begin{array}{l} \mathbf{R}_{z}(\alpha)=\left(\begin{array}{cccc} \cos \alpha & -\sin \alpha & 0 & 0 \\ \sin \alpha & \cos \alpha & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{array}\right) \end{array}

 So we can easily implement the code,

// rotation_angle is the angle to rotate around z
Eigen::Matrix4f get_model_matrixws(float rotation_angle)
{
    Eigen::Matrix4f rotation;
    float angle = rotation_angle;
 
    angle = angle * MY_PI / 180.f; // angle to radius
    rotation << cos(angle),-sin(angle),0,0, // rotation matrix around z
                sin(angle),cos(angle),0,0,
                0,0,1,0,
                0,0,0,1;
    
    // No translation
    Eigen::Matrix4f translate = Eigen::Matrix4f::Identity();
 
    return translate * rotation;  // output
}
 

1.2 Rotate around the given N-axis

According to our introduction in CG01:Model Transformation In Computer Graphics, transformation matrix that makes the model rotate around the given N-axis can be defined as follows,

\large \mathbf{R}(\mathbf{n}, \alpha)=\cos (\alpha) \mathbf{I}+(1-\cos (\alpha)) \mathbf{n} \mathbf{n}^{T}+\sin (\alpha) \underbrace{\left(\begin{array}{ccc} 0 & -n_{z} & n_{y} \\ n_{z} & 0 & -n_{x} \\ -n_{y} & n_{x} & 0 \end{array}\right)}_{\mathbf{N}}

 So we can easily implement the code,

Eigen::Matrix4f get_rotation(Vector3f axis, float angle)
{
    Eigen::Matrix4f model = Eigen::Matrix4f::Identity();
    Eigen::Matrix3f temp = Eigen::Matrix3f::Identity();
    float ag = angle/180*MY_PI;
    Eigen::Matrix3f tr;
    Eigen::Matrix3f N;

    N <<   0,        -axis[2], axis[1],
           axis[2],  0,        -axis[0],
           -axis[1], axis[0],  0;

    tr = cos(ag)*temp + (1-cos(ag))*axis*axis.adjoint() + N*sin(ag);

    model << tr(0,0), tr(0,1), tr(0,2), 0,
             tr(1,0), tr(1,1), tr(1,2), 0,
             tr(2,0), tr(2,1), tr(2,2), 0,
             0,       0,       0,       1;

    return model;
}

Get View/Camera Transformation Matrix

View/Camera transformation matrix aim to move the object and the camera simultaneously to make camera locate at the origin, up at Y, look at -Z.

According to our introduction in CG02:View/Camera, Projection And Viewport Transformation In Computer Graphics, it can be defined as follows,

 

 

 So we can implement the code,

Eigen::Matrix4f get_view_matrix(Eigen::Matrix3f eye_dir, Eigen::Vector3f eye_pos)
{
    Eigen::Matrix4f view = Eigen::Matrix4f::Identity();

    Eigen::Matrix4f R_view;
    R_view << eye_dir(0,0),  eye_dir(0,1),  eye_dir(0,2),  0,
              eye_dir(1,0),  eye_dir(1,1),  eye_dir(1,2),  0,
              eye_dir(2,0),  eye_dir(2,1),  eye_dir(2,2),  0,
              0,             0,             0,             1;

    Eigen::Matrix4f translate;
    translate << 1, 0, 0, -eye_pos[0], 
                 0, 1, 0, -eye_pos[1], 
                 0, 0, 1, -eye_pos[2], 
                 0, 0, 0, 1;

    view = view * R_view * translate;

    return view;
}

Get Projection Transformation Matrix

We use perspective projection. According to our introduction in CG02:View/Camera, Projection And Viewport Transformation In Computer Graphics, it can be defined as follows,

\LARGE M_{\text {persp } \rightarrow \text { ortho }}=\left(\begin{array}{cccc} n & 0 & 0 & 0 \\ 0 & n & 0 & 0 \\ 0 & 0 & n+f & -fn \\ 0 & 0 & 1 & 0 \end{array}\right)

\LARGE M_{persp} = M_{ortho}M_{persp\rightarrow ortho}

  So we can implement the code,

// eye_fov: Vertical Perspective
// aspect_ratio: Width-to-Height Ratio
Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio,
                                      float zNear, float zFar)
{
    Eigen::Matrix4f projection = Eigen::Matrix4f::Identity();
    
    // projection-to-orthogonal matrix
    Eigen::Matrix4f M_p = Eigen::Matrix4f::Identity();
    M_p << zNear, 0,     0,          0,
           0,     zNear, 0,          0,
           0,     0,     zNear+zFar, (-1.0*zNear*zFar),
           0,     0,     1,          0;

    //[l,r]   [b,t]    [f,n]
    float angle = eye_fov*MY_PI/180; // Calculating Perspective
    float t = tan(angle/2)*-zNear;   // Calculating top coordinate
    float b = -1.0*t;                // Calculating bottom coordinate
    float r = t*aspect_ratio;        // Calculating reft coordinate based on aspect ratio
    float l = -1.0*r;                // Calculating left coordinate
    
    // scaling
    Eigen::Matrix4f M_s = Eigen::Matrix4f::Identity();
    M_s << 2/(r-l), 0,       0,              0,
           0,       2/(t-b), 0,              0,
           0,       0,       2/(zNear-zFar), 0,
           0,       0,       0,              1;
    
    // translating
    Eigen::Matrix4f M_t = Eigen::Matrix4f::Identity();
    M_t << 1,  0,  0,  (-1.0)*(r+l)/2,
           0,  1,  0,  (-1.0)*(t+b)/2,
           0,  0,  1,  (-1.0)*(zNear+zFar)/2,
           0,  0,  0,  1;

    projection = M_s*M_t*M_p*projection;
    return projection;
}

Program Framework

Then we need to introduce two c++ files provided with the assignment,

First is the rasterizer.cpp. It aims to achieve a rasterized pipeline of applications for each point of the input 3D space.

// clang-format off
//
// Created by goksu on 4/6/19.
//

#include <algorithm>
#include <vector>
#include "rasterizer.hpp"
#include <opencv2/opencv.hpp>
#include <math.h>


rst::pos_buf_id rst::rasterizer::load_positions(const std::vector<Eigen::Vector3f> &positions)
{
    auto id = get_next_id();
    pos_buf.emplace(id, positions);

    return {id};
}

rst::ind_buf_id rst::rasterizer::load_indices(const std::vector<Eigen::Vector3i> &indices)
{
    auto id = get_next_id();
    ind_buf.emplace(id, indices);

    return {id};
}

rst::col_buf_id rst::rasterizer::load_colors(const std::vector<Eigen::Vector3f> &cols)
{
    auto id = get_next_id();
    col_buf.emplace(id, cols);

    return {id};
}

auto to_vec4(const Eigen::Vector3f& v3, float w = 1.0f)
{
    return Vector4f(v3.x(), v3.y(), v3.z(), w);
}


static bool insideTriangle(int x, int y, const Vector3f* _v)
{     
    Eigen::Vector2f p, a, b, c, AP, BP, CP;  
    p << x, y;	//.head(2)指这个点的前两个数值,即x,y
    a = _v[0].head(2) - _v[1].head(2);          //a = A - B  即B->A
    b = _v[1].head(2) - _v[2].head(2);          //b = B - C  即C->B
    c = _v[2].head(2) - _v[0].head(2);          //c = C - A  即A->C  
    AP = p - _v[0].head(2);
    BP = p - _v[1].head(2);
    CP = p - _v[2].head(2);
    return ((AP[0] * c[1] - AP[1] * c[0] > 0 && BP[0] * a[1] - BP[1] * a[0] > 0 && CP[0] * b[1] - CP[1] * b[0] > 0)||
    (AP[0] * c[1] - AP[1] * c[0] < 0 && BP[0] * a[1] - BP[1] * a[0] < 0 && CP[0] * b[1] - CP[1] * b[0] < 0));
}


static std::tuple<float, float, float> computeBarycentric2D(float x, float y, const Vector3f* v)
{
    float c1 = (x*(v[1].y() - v[2].y()) + (v[2].x() - v[1].x())*y + v[1].x()*v[2].y() - v[2].x()*v[1].y()) / (v[0].x()*(v[1].y() - v[2].y()) + (v[2].x() - v[1].x())*v[0].y() + v[1].x()*v[2].y() - v[2].x()*v[1].y());
    float c2 = (x*(v[2].y() - v[0].y()) + (v[0].x() - v[2].x())*y + v[2].x()*v[0].y() - v[0].x()*v[2].y()) / (v[1].x()*(v[2].y() - v[0].y()) + (v[0].x() - v[2].x())*v[1].y() + v[2].x()*v[0].y() - v[0].x()*v[2].y());
    float c3 = (x*(v[0].y() - v[1].y()) + (v[1].x() - v[0].x())*y + v[0].x()*v[1].y() - v[1].x()*v[0].y()) / (v[2].x()*(v[0].y() - v[1].y()) + (v[1].x() - v[0].x())*v[2].y() + v[0].x()*v[1].y() - v[1].x()*v[0].y());
    return {c1,c2,c3};
}

void rst::rasterizer::draw(pos_buf_id pos_buffer, ind_buf_id ind_buffer, col_buf_id col_buffer, Primitive type)
{
    auto& buf = pos_buf[pos_buffer.pos_id];
    auto& ind = ind_buf[ind_buffer.ind_id];
    auto& col = col_buf[col_buffer.col_id];

    float f1 = (50 - 0.1) / 2.0;
    float f2 = (50 + 0.1) / 2.0;

    Eigen::Matrix4f mvp = projection * view * model;
    for (auto& i : ind)
    {
        Triangle t;
        Eigen::Vector4f v[] = {
                mvp * to_vec4(buf[i[0]], 1.0f),
                mvp * to_vec4(buf[i[1]], 1.0f),
                mvp * to_vec4(buf[i[2]], 1.0f)
        };
        //Homogeneous division
        for (auto& vec : v) {
            vec /= vec.w();
        }
        //Viewport transformation
        for (auto & vert : v)
        {
            vert.x() = 0.5*width*(vert.x()+1.0);
            vert.y() = 0.5*height*(vert.y()+1.0);
            vert.z() = vert.z() * f1 + f2;
        }

        for (int i = 0; i < 3; ++i)
        {
            t.setVertex(i, v[i].head<3>());
            t.setVertex(i, v[i].head<3>());
            t.setVertex(i, v[i].head<3>());
        }

        auto col_x = col[i[0]];
        auto col_y = col[i[1]];
        auto col_z = col[i[2]];

        t.setColor(0, col_x[0], col_x[1], col_x[2]);
        t.setColor(1, col_y[0], col_y[1], col_y[2]);
        t.setColor(2, col_z[0], col_z[1], col_z[2]);

        rasterize_triangle(t);
    }
}

void rst::rasterizer::rasterize_triangle(const Triangle& t) {
    auto v = t.toVector4(); //v[0],v[1],v[2]分别为三角形的三个顶点,是四维向量

    //比较三个顶点的横纵坐标,确定包围盒的边界并取整
    double min_x = std::min(v[0][0], std::min(v[1][0], v[2][0]));
    double max_x = std::max(v[0][0], std::max(v[1][0], v[2][0]));
    double min_y = std::min(v[0][1], std::min(v[1][1], v[2][1]));
    double max_y = std::max(v[0][1], std::max(v[1][1], v[2][1]));
    min_x = static_cast<int>(std::floor(min_x));
    min_y = static_cast<int>(std::floor(min_y));
    max_x = static_cast<int>(std::ceil(max_x));
    max_y = static_cast<int>(std::ceil(max_y));
    
    //此处实现的是MSAA
    std::vector<Eigen::Vector2f> pos
    {                               //对一个像素分割四份 当然你还可以分成4x4 8x8等等甚至你还可以为了某种特殊情况设计成不规则的图形来分割单元
        {0.25,0.25},                //左下
        {0.75,0.25},                //右下
        {0.25,0.75},                //左上
        {0.75,0.75}                 //右上
    };

    for (int i = min_x; i <= max_x; ++i)
    {
        for (int j = min_y; j <= max_y; ++j)
        {
            int count = 0; //开始遍历四个小格子,获得平均值
            for (int MSAA_4 = 0; MSAA_4 < 4; ++MSAA_4)
            {
                if (insideTriangle(static_cast<float>(i+pos[MSAA_4][0]), static_cast<float>(j+pos[MSAA_4][1]),t.v))
                    ++count;
            }
            if(count) //至少有一个小格子在三角形内
            {
                //此处是框架中代码,获得z,见原程序注释:
                auto [alpha, beta, gamma] = computeBarycentric2D(static_cast<float>(i + 0.5), static_cast<float>(j + 0.5), t.v);
                float w_reciprocal = 1.0 / (alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
                float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
                z_interpolated *= w_reciprocal;
                //end
                if (depth_buf[get_index(i, j)] > z_interpolated)
                {
                    depth_buf[get_index(i, j)] = z_interpolated;//更新深度
                    //这里注意,虽然说明上说"反转了z,保证都是正数,并且越大表示离视点越远",
                    //但经过我的查看,实际上并没有反转,因此还是按照-z近大远小来做,当然也可以在上面补一个负号不过没必要
                    Eigen::Vector3f color = t.getColor() * (count / 4.0);//对颜色进行平均
                    Eigen::Vector3f point;
                    point << static_cast<float>(i), static_cast<float>(j), z_interpolated;
                    set_pixel(point, color);//设置颜色
                }
            }
        }
    }
}



void rst::rasterizer::set_model(const Eigen::Matrix4f& m)
{
    model = m;
}

void rst::rasterizer::set_view(const Eigen::Matrix4f& v)
{
    view = v;
}

void rst::rasterizer::set_projection(const Eigen::Matrix4f& p)
{
    projection = p;
}

void rst::rasterizer::clear(rst::Buffers buff)
{
    if ((buff & rst::Buffers::Color) == rst::Buffers::Color)
    {
        std::fill(frame_buf.begin(), frame_buf.end(), Eigen::Vector3f{0, 0, 0});
    }
    if ((buff & rst::Buffers::Depth) == rst::Buffers::Depth)
    {
        std::fill(depth_buf.begin(), depth_buf.end(), std::numeric_limits<float>::infinity());
    }
}

rst::rasterizer::rasterizer(int w, int h) : width(w), height(h)
{
    frame_buf.resize(w * h);
    depth_buf.resize(w * h);
}

int rst::rasterizer::get_index(int x, int y)
{
    return (height-1-y)*width + x;
}

void rst::rasterizer::set_pixel(const Eigen::Vector3f& point, const Eigen::Vector3f& color)
{
    //old index: auto ind = point.y() + point.x() * width;
    auto ind = (height-1-point.y())*width + point.x();
    frame_buf[ind] = color;

}

// clang-format on

 The second one is Triangle.cpp. It is used to store data and set parameters for drawing triangles

//
// Created by LEI XU on 4/11/19.
//

#include "Triangle.hpp"
#include <algorithm>
#include <array>
#include <stdexcept>

Triangle::Triangle()
{
    v[0] << 0, 0, 0;
    v[1] << 0, 0, 0;
    v[2] << 0, 0, 0;

    color[0] << 0.0, 0.0, 0.0;
    color[1] << 0.0, 0.0, 0.0;
    color[2] << 0.0, 0.0, 0.0;

    tex_coords[0] << 0.0, 0.0;
    tex_coords[1] << 0.0, 0.0;
    tex_coords[2] << 0.0, 0.0;
}

void Triangle::setVertex(int ind, Eigen::Vector3f ver) { v[ind] = ver; }

void Triangle::setNormal(int ind, Vector3f n) { normal[ind] = n; }

void Triangle::setColor(int ind, float r, float g, float b)
{
    if ((r < 0.0) || (r > 255.) || (g < 0.0) || (g > 255.) || (b < 0.0) ||
        (b > 255.)) {
        throw std::runtime_error("Invalid color values");
    }

    color[ind] = Vector3f((float)r / 255., (float)g / 255., (float)b / 255.);
    return;
}
void Triangle::setTexCoord(int ind, float s, float t)
{
    tex_coords[ind] = Vector2f(s, t);
}

std::array<Vector4f, 3> Triangle::toVector4() const
{
    std::array<Vector4f, 3> res;
    std::transform(std::begin(v), std::end(v), res.begin(), [](auto& vec) {
        return Vector4f(vec.x(), vec.y(), vec.z(), 1.f);
    });
    return res;
}

In the main loop of main.cpp, we need to call the functions defined earlier and concatenate them to implement the entire pipeline of transformations. 

When keys A and D are detected as being pressed, the angle is increased or decreased accordingly to the angle, respectively. Then in the next cycle the corresponding transformation matrix is generated and applied to the transformation pipeline.

int main(int argc, const char** argv)
{
    float angle = 0;
    bool command_line = false;
    std::string filename = "output.png";

    if (argc >= 3) {
        command_line = true;
        angle = std::stof(argv[2]); // -r by default
        if (argc == 4) {
            filename = std::string(argv[3]);
        }
        else
            return 0;
    }

    rst::rasterizer r(700, 700);

    Eigen::Vector3f eye_pos = {0, 0, 5};
    Eigen::Matrix3f eye_dir;
    eye_dir << 1, 0, 0,
               0, 1, 0,
               0, 0, 1;

    Eigen::Vector3f naxis = {0, 0, 1};

    std::vector<Eigen::Vector3f> pos{{2, 0, -2}, {0, 2, -2}, {-2, 0, -2}};

    std::vector<Eigen::Vector3i> ind{{0, 1, 2}};

    auto pos_id = r.load_positions(pos);
    auto ind_id = r.load_indices(ind);

    int key = 0;
    int frame_count = 0;

    if (command_line) {
        r.clear(rst::Buffers::Color | rst::Buffers::Depth);

        r.set_model(get_rotation(naxis,angle));
        r.set_view(get_view_matrix(eye_dir, eye_pos));
        r.set_projection(get_projection_matrix(45, 1, 0.1, 50));

        r.draw(pos_id, ind_id, rst::Primitive::Triangle);
        cv::Mat image(700, 700, CV_32FC3, r.frame_buffer().data());
        image.convertTo(image, CV_8UC3, 1.0f);

        cv::imwrite(filename, image);

        return 0;
    }

    while (key != 27) {
        r.clear(rst::Buffers::Color | rst::Buffers::Depth);

        r.set_model(get_rotation(naxis,angle));
        r.set_view(get_view_matrix(eye_dir, eye_pos));
        r.set_projection(get_projection_matrix(45, 1, 0.1, 50));

        r.draw(pos_id, ind_id, rst::Primitive::Triangle);

        cv::Mat image(700, 700, CV_32FC3, r.frame_buffer().data());
        image.convertTo(image, CV_8UC3, 1.0f);
        cv::imshow("image", image);
        key = cv::waitKey(10);

        std::cout << "frame count: " << frame_count++ << '\n';

        if (key == 'a') {
            angle += 10;
        }
        else if (key == 'd') {
            angle -= 10;
        }
    }

    return 0;
}

Test


Reference

[1] Marschner S , Shirley P. Fundamentals of Computer Graphics 4th

[2] Lingqi Yan,  GAMES101: 现代计算机图形学入门

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值