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,
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,
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,
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: 现代计算机图形学入门