手搓一个小型渲染器(一)
bresenham算法
实现一
bresenham算法主要是用于在网格中的两点之间绘制一条直线,对于绘制一条经过 ( x 0 , y 0 ) , ( x 1 , y 1 ) (x_0,y_0),(x_1, y_1) (x0,y0),(x1,y1)的一条直线,我们可以通过下面的代码实现
void line(int x0, int y0, int x1, int y1, TGAImage& image, TGAColor color) {
//使用for循环逐步从起始点逼近终点
for (float t = 0; t < 1; t += 0.01)
{
int x = x0 + (x1 - x0) * t;
int y = y0 + (y1 - y0) * t;
image.set(x, y, color);
}
}
int main()
{
TGAImage image(300, 300, TGAImage::RGB);
TGAColor linecolor = TGAColor{ 255, 255, 255, 255 };
line(0, 0, 100, 200 , image, linecolor);
image.write_tga_file("output.tga");
}
上述的实现会产生一个问题,当我们将for循环中t的步长从0.01调整到0.1时,绘制出的直线如下图:
上述的实现会产生一个问题,当我们将for循环中t的步长从0.01调整到0.1时,绘制出的直线如下图:
实现二
因此,针对上面的情况,我们可以将t由x0与x1组成,其中代码(实现方式是错误的)如下:
void line(int x0, int y0, int x1, int y1, TGAImage& image, TGAColor color) {
for (int x = x0; x <= x1; x++)
{
//注意分母取float,因为int和int除法还是会得到int,绘制只能得到一个点
float t = (x - x0) / (float)(x1 - x0);
int y = y0 * (1. - t) + y1 * t;
image.set(x, y, color);
}
}
int main()
{
TGAImage image(100, 100, TGAImage::RGB);
const TGAColor white = TGAColor{ 255, 255, 255, 255 };
const TGAColor red = TGAColor{ 0, 0, 255, 255 };
line(13, 20, 80, 40, image, white);
line(20, 13, 40, 80, image, red);
line(80, 40, 13, 20, image, red);
image.write_tga_file("output.tga");
}
用上述结果运行的时候,第一条线是好的,第二条线则是有一个洞,然后第三条线没有出现。这里我们的第一条线与第三条线使用了不同方向不同颜色绘制同一条线,我们期望得到的是将下面这一条白线转换为红色的,但是之前的代码无法实现,这是对称性测试,即绘制线段的结果不取决于两点的顺序。 ( a , b ) (a,b) (a,b)线段应该与 ( b , a ) (b, a) (b,a)线段完全相同。
实现三
第二条红线出现的空洞的原因在于我们使用(x0, x1)进行y的绘制,其高度大于宽度,因此就会出现线空洞的状态
因此我们可以比较x之间的差与y之间差的绝对值,更大的一方进行绘制。
bool steep = false;
if (std::abs(x0 - x1) < std::abs(y0 - y1))
{
std::swap(x0, y0);
std::swap(x1, y1);
steep = true;
}
选择更平缓(更长)的部分来绘制。
另外一个问题就是对称性的问题,这部分也非常简单,我们只需要判断以下x0与x1的大小,如果x0 > x1,则将双方交换一下。
if (x0 > x1)
{
std::swap(x0, x1);
std::swap(y0, y1);
}
最终代码如下:
void line(int x0, int y0, int x1, int y1, TGAImage& image, TGAColor color) {
bool steep = false;
//判断x与y哪一方更平缓,防止出现空洞的情况
if (std::abs(x0 - x1) < std::abs(y0 - y1))
{
std::swap(x0, y0);
std::swap(x1, y1);
steep = true;
}
//满足line的对称性
if (x0 > x1)
{
std::swap(x0, x1);
std::swap(y0, y1);
}
for (int x = x0; x <= x1; x++)
{
//注意分母取float,因为int和int除法还是会得到int,绘制只能得到一个点
float t = (x - x0) / (float)(x1 - x0);
int y = y0 * (1. - t) + y1 * t;
//当y更平缓的时候我们在绘制的时候需要注意此时x和y已经交换了位置
if (steep)
{
image.set(y, x, color);
}
else
{
image.set(x, y, color);
}
}
}
int main()
{
TGAImage image(100, 100, TGAImage::RGB);
const TGAColor white = TGAColor{ 255, 255, 255, 255 };
const TGAColor red = TGAColor{ 0, 0, 255, 255 };
line(13, 20, 80, 40, image, white);
line(20, 13, 40, 80, image, red);
line(80, 40, 13, 20, image, red);
image.write_tga_file("output.tga");
}
最终的结果如下:
可以看到我们修复了之前遇到的问题,同时对称性也满足了。
实现四
上述实现还会存在一个问题,就是代码在运行时的优化问题,为了测试,我们调用10000次上面的三个line函数
int main()
{
TGAImage image(100, 100, TGAImage::RGB);
const TGAColor white = TGAColor{ 255, 255, 255, 255 };
const TGAColor red = TGAColor{ 0, 0, 255, 255 };
for (int i = 0; i < 10000; i++) {
line(13, 20, 80, 40, image, white);
line(20, 13, 40, 80, image, red);
line(80, 40, 13, 20, image, red);
}
image.flip_vertically();
image.write_tga_file("output.tga");
}
结果如下:我们使用VS的性能生成可以看到,main函数中花费时间最长的时line函数(这个功能需要留意C盘大小,之前的分析直接吃了12G的内存= =)
在line中每一个for循环中,相除的数是固定的,因此我们可以将其放在loop外,并使用导数的概念
对于两个点,
(
x
0
,
y
0
)
(x_0, y_0)
(x0,y0)和
(
x
1
,
y
1
)
(x_1,y_1)
(x1,y1),我们可以得到两者的增量:
d
y
=
y
1
−
y
0
d
x
=
x
1
−
x
0
d
y
d
x
=
y
1
−
y
0
x
1
−
x
0
dy=y_1-y_0 \\ dx = x_1 - x_0 \\ \frac{dy}{dx} = \frac{y_1-y_0}{x_1 - x_0}
dy=y1−y0dx=x1−x0dxdy=x1−x0y1−y0
我们可以将其看作这条直线的一个很小的增量,因此我们可以通过这个增量进行累加。代码如下:
void line(int x0, int y0, int x1, int y1, TGAImage& image, TGAColor color) {
bool steep = false;
if (std::abs(x0 - x1) < std::abs(y0 - y1))
{
std::swap(x0, y0);
std::swap(x1, y1);
steep = true;
}
if (x0 > x1)
{
std::swap(x0, x1);
std::swap(y0, y1);
}
int dy = y1 - y0;
int dx = x1 - x0;
//计算dy/dx
float derror = std::abs(dy / (float)dx);
float error = 0;
int y = y0;
for (int x = x0; x <= x1; x++)
{
if (steep)
{
image.set(y, x, color);
}
else
{
image.set(x, y, color);
}
//对误差进行累加
error += derror;
if (error > .5)
{
y += y0 < y1 ? 1 : -1;
error -= 1;
}
}
}
int main()
{
TGAImage image(100, 100, TGAImage::RGB);
const TGAColor white = TGAColor{ 255, 255, 255, 255 };
const TGAColor red = TGAColor{ 0, 0, 255, 255 };
long t1 = GetTickCount64();
for (int i = 0; i < 10000; i++) {
line(13, 20, 80, 40, image, white);
line(20, 13, 40, 80, image, red);
line(80, 40, 13, 20, image, red);
}
long t2 = GetTickCount64();
std::cout << "Time for line: " << (t2 - t1) << std::endl;
//image.flip_vertically();
image.write_tga_file("output.tga");
}
模型的线框渲染
现在我们需要建立一个模型的渲染,我们首先需要对obj格式的模型进行读取,最主要的是需要从文件中读取一个类似下面的vector:
v 0.608654 -0.568839 -0.416318
这三个值代表着模型一个顶点(vertex)的 ( x , y , z ) (x,y,z) (x,y,z)的坐标
f 1193/1240/1193 1180/1227/1180 1179/1226/1179
f代表模型的一个面,我们需要关注的则是/最后的数字,上面代表这是由1193,1180,1179三个顶点构成的一个面。
首先我们需要读取obj格式数据,其中face代表着一个三角面三个顶点的索引,而vertex代表各个顶点的坐标
在此之前,我们需要定义二维向量与三维向量,以及该类向量的基础运算。
/*
* geometry.h
*/
#include <cmath>
//基本的向量运算
//2维向量
template<class t> struct Vec2 {
union
{
struct { t u, v; }; //uv贴图坐标
struct { t x, y; }; //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) {
return Vec2<t>(u + V.u, v + V.v);
}
inline Vec2<t> operator - (const Vec2<t> &V) {
return Vec2<t>(u - V.u, v - V.v);
}
inline Vec2<t> operator* (const float f) {
return Vec2<t> (u* f, v* f);
}
template <class > friend std::ostream& operator<<(std::ostream& s, Vec2<t>& v);
};
//3维向量
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;
}
读取模型
/*
* model.h
*/
#include <vector>
#include <string>
#include <vector>
#include "geometry.h"
#include "tgaimage.h"
//Model用于读取obj文件
class Model {
private:
std::vector<Vec3f> verts_;
std::vector<std::vector<int>> faces_;
public:
Model(const char* filename);
~Model();
//返回顶点的个数
int nverts();
//返回三角形的个数
int nfaces();
//返回具体顶点的坐标向量
Vec3f vert(int i);
//返回具体的三角形顶点的索引值
std::vector<int> face(int index);
};
/*
* model.cpp
*/
#include <iostream>
#include <sstream>
#include "model.h"
//读取obj文件
Model::Model(const char* filename): verts_(), faces_() {
std::ifstream in;
in.open(filename, std::ifstream::in);
if (in.fail()) return;
std::string line;
while (!in.eof())
{
std::getline(in, line);
std::istringstream iss(line.c_str());
char trash;
if (!line.compare(0, 2, "v ")) {
iss >> trash;
Vec3f v;
for (int i = 0; i < 3; i++) iss >> v.raw[i];
verts_.push_back(v);
}
else if (!line.compare(0, 2, "f ")) {
std::vector<int> f;
int itrash, idx;
iss >> trash;
while (iss >> idx >> trash >> itrash >> trash >> itrash) {
idx--; // in wavefront obj all indices start at 1, not zero
f.push_back(idx);
}
faces_.push_back(f);
}
}
std::cerr << "# v# " << verts_.size() << " f# " << faces_.size() << std::endl;
}
Model::~Model() {}
int Model::nverts() {
return (int)verts_.size();
}
int Model::nfaces() {
return (int)faces_.size();
}
Vec3f Model::vert(int index) {
return verts_[index];
}
std::vector<int> Model::face(int index) {
return faces_[index];
}
main.cpp
#include <windows.h>
#include <iostream>
#include <vector>
#include "tgaimage.h"
#include "model.h"
void line(int x0, int y0, int x1, int y1, TGAImage& image, TGAColor color) {
bool steep = false;
if (std::abs(x0 - x1) < std::abs(y0 - y1)) {
std::swap(x0, y0);
std::swap(x1, y1);
steep = true;
}
if (x0 > x1) {
std::swap(x0, x1);
std::swap(y0, y1);
}
int dx = x1 - x0;
int dy = y1 - y0;
int derror2 = std::abs(dy) * 2;
int error2 = 0;
int y = y0;
for (int x = x0; x <= x1; x++) {
if (steep) {
image.set(y, x, color);
}
else {
image.set(x, y, color);
}
error2 += derror2;
if (error2 > dx) {
y += (y1 > y0 ? 1 : -1);
error2 -= dx * 2;
}
}
}
int main()
{
Model* head = new Model("./obj/african_head/african_head.obj");
int width = 1024;
int height = 1024;
TGAImage image(width, height, TGAImage::RGB);
const TGAColor white = TGAColor{ 255, 255, 255, 255 };
const TGAColor red = TGAColor{ 0, 0, 255, 255 };
for (int i = 0; i < head->nfaces(); i++)
{
std::vector<int> face = head->face(i);
for (int j = 0; j < 3; j++)
{
//face中存储着一个三角面中三个顶点的索引
//之后我们两两选取顶点画线
Vec3f v0 = head->vert(face[j]);
Vec3f v1 = head->vert(face[(j + 1) % 3]);
//作归一化,让顶点坐标在图像的中间
int x0 = (v0.x + 1.) * width / 2.;
int y0 = (v0.y + 1.) * height / 2.;
int x1 = (v1.x + 1.) * width / 2.;
int y1 = (v1.y + 1.) * height / 2.;
line(x0, y0, x1, y1, image, white);
}
}
image.write_tga_file("output.tga");
delete head;
}
在main函数中,我们只考虑了x和y坐标,z坐标之后会通过z-buffer进行存储。
最终结果如下: