介绍
简单的画一条线
void line(int x0, int y0, int x1, int y1, TGAImage &image, TGAColor color) {
for (float t=0.; t<1.; t+=.01) {
int x = x0 + (x1-x0)*t;
int y = y0 + (y1-y0)*t;
image.set(x, y, color);
}
}
这个方法其实跟差值差不多,t 取 100 份,然后从起始点走平均的画一百个点到结束点,但这样对于很短的线段来说,就比较浪费,他们可能只需要10个点就能画出很流畅的线。
一百个点
按像素来画一条线
void line(int x0, int y0, int x1, int y1, TGAImage &image, TGAColor color) {
for (int x=x0; x<=x1; x++) {
float t = (x-x0)/(float)(x1-x0);
// int y = y0*(1.-t) + y1*t;
int y = y0 + (y1 - y0) * t;
image.set(x, y, color);
}
}
这个方法是从 x0 出发,然后在 x0+1 的时候,算出 y 的值,然后画一个点;然后在 x0+2 的时候算出 y 的值,再画一个点......这种方式对于相对是躺着的线来说比较友好(line1),因为他的 x 轴像素是连续的,但对于站着的线就不太 ok(line2)。而且对于从右往左走的线就根本画不出来(line3),因为这个 for 循环是递增的。
line(13, 20, 80, 40, image, white); // line1
line(20, 13, 40, 80, image, red); // line2
line(80, 40, 13, 20, image, red); // line3
line3 没有
修改按像素来画一条线的 bug
如果能将所有的线都变成上面的 line1 一样不就好了?代码如下:
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)) { // if the line is steep, we transpose the image
std::swap(x0, y0);
std::swap(x1, y1);
steep = true;
}
if (x0>x1) { // make it left−to−right
std::swap(x0, x1);
std::swap(y0, y1);
}
for (int x=x0; x<=x1; x++) {
float t = (x-x0)/(float)(x1-x0);
int y = y0*(1.-t) + y1*t;
if (steep) {
image.set(y, x, color); // if transposed, de−transpose
} else {
image.set(x, y, color);
}
}
}
第一个 if 是防止上面 line2 的情况,先判断线的斜率,以左下角为原点建立平面直角坐标系,直线的斜率大于 1 的话(也就是跟 x 轴的夹角大于 45°),就使它关于 y=x 这条线对称,在代码上来看就是交换起点的 x 和 y,交换终点的 x 和 y,把两个点给对称了,整条线自然是关于 y=x 对称的了。如果对称了需要把 steep 设为 true。
第二个 if 是防止上面 line3 的情况,此刻我们的线都是斜率小于 1 的了,但不能让 x1 大于 x0,所以如果 x1 大于 x0 了,那就交换终点和起点的位置,这个不需要记录,因为对后续不产生影响。
下面一个 for 循环跟上面一样,需要注意的就是如果 steep 为 true,说明之前是根据 y=x 对称过的了,需要给他对称回来。
优化(去除法)
作者是用的 Linux 平台下的一个性能分析工具 gprof
,分析出 70% 的时间都花费在了 line 函数上面,分析一下 line 函数里面还有什么地方可以优化,可以看到 x1-x0
其实是固定的,不需要每次都在 for 里面计算一次。所以单独提出:
int dx = x1-x0;
int dy = y1-y0;
float derror = std::abs(dy/float(dx));
derror 就是每次 x+1 之后,y 应该加的分量。比如说 derror = 0.4,那么在绘制完 (x0,y0) 这个像素之后,下一个绘制的点应该是 (x0+1,y0+0.4),假设 (x0,y0) 的坐标在 (0.5,0.5),那么 (x0+1,y0+0.4) 应该是 (1.5,0.9),可以看到 y 的坐标还在第一个像素点里面,所以 y 不变,也就是第二个点绘制 (1.5,0.5) 这个像素,然后来到第三个点,应该绘制 (2.5,1.3) ,可以看到 1.3 已经是属于下一个 y 轴的像素了,所以将 y+1,也就是绘制 (2.5,1.5) 这个点......所以可以看到当 y 大于 0.5,1.5,2.5,3.5...... 的时候需要进行 +1,或者 -1,那么在每次比较之后都减去 1 ,就可以每次都只和 0.5 进行比较了。完整代码如下:
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;
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 += (y1>y0?1:-1);
error -= 1.;
}
}
}
for 里面已经没有除法了,但是还有浮点数。
优化(去浮点数)
怎么去浮点数去掉呢,其实使用浮点数主要是因为有个 0.5 的比较,那直接将 0.5*2就可以啦,那相应的 error 也应该 *2,但是 error 是 derror 的和,所以将 derror *2 就可以了,然后就会发现 std::abs((2*dy)/float(dx))
依然是一个 浮点数,所以再乘分母,也就是 dx,最后结果就是:
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; // 原来的乘了 2dx
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) { // 原来的 0.5 乘了 2dx
y += (y1>y0?1:-1);
error2 -= dx*2; // 原来的 1 乘了 2dx
}
}
}
.obj 格式
具体可以参考:https://en.wikipedia.org/wiki/Wavefront_.obj_file
-
#
后面都是注释
# 这是一个注释
-
v
后面是一个顶点,可以是 xyz,也可以是 xyzw
v 0.123 0.234 0.345 1.0
-
vt
是 UV
vt 0.500 1
-
vn
是顶点法线,必须是单位向量
vn 0.707 0.000 0.707
-
f
是多边形面- 只有顶点的多边形面,顶点数量可以大于等于 3,比如说
f 1 2 3 4...
就是说,以刚刚 v 开头的顶点列表的第一个、第二个、第三个、第四个(注意索引是从 1 开始,不是 0)顶点坐标为顶点建立多边形面 - 有 UV 的多边形面,比如
f 1/5 2/6 3/6 4/5 ...
就是说用顶点列表的第一个、第二个、第三个、第四个建立多边形面,并且第一个顶点对应的 UV 是 UV 列表(就是刚刚以 vt 开头的那些)中的第五个坐标,第二个顶点对应的 UV 列表中的第六个坐标...... - 有 UV 和法线的多边形面,比如
f 1/5/7 2/6/8 3/6/9 4/5/10 ...
同理,第一个顶点的坐标是顶点列表中的第一个,UV 是 UV 列表中的第五个,法线是法线列表(也就是 vn 开头的一列)中的第七个。 - 只有法线,没有 UV 的多边形面,
f 1//7 2//8 3//9 4//10 ...
直接将 UV 删掉就好了。
- 只有顶点的多边形面,顶点数量可以大于等于 3,比如说
画网格
首先需要一个类去解析 .obj 文件,可以直接把下面的几个文件复制进自己的工程:
目录结构
// file_name:model.h
#ifndef __MODEL_H__
#define __MODEL_H__
#include <vector>
#include "geometry.h"
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 idx);
};
#endif //__MODEL_H__
// file_name :model.cpp
#include <iostream>
#include <string>
#include <fstream>
#include <sstream>
#include <vector>
#include "model.h"
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();
}
std::vector<int> Model::face(int idx) {
return faces_[idx];
}
Vec3f Model::vert(int i) {
return verts_[i];
}
// file_name :geometry.h
#ifndef __GEOMETRY_H__
#define __GEOMETRY_H__
#include <cmath>
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
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;
}
#endif //__GEOMETRY_H__
geometry.h
定义了四个类型:浮点型的二维向量(Vec2f),整型的二维向量(Vec2i),浮点型的三维向量(Vec3f),整型的三维向量(Vec3i),并且重载了一些常用的运行符,比如
+
-
*
^
<<
等,还有求膜,单位化向量等。
modle.h
将 .obj 文件解析,在构造函数中传入 .obj 文件的地址,然后将里面的顶点都放到
std::vector<Vec3f> verts_;
中(这里就默认不管 w 了), 将 .obj 文件中的多边形面都放到std::vector<std::vector<int> > faces_;
中,每一行都是一个std::vector<int>
,并且只留了顶点的信息,UV 和法线都被舍弃了,比如f 24/1/24 25/2/25 26/3/26
在 vector<int> 中的存储的就是23,24,25
(因为 obj 中索引是从 1 ,但是数组索引是从 0,所以都减了一)。
下面是 main:
#include <vector>
#include <cmath>
#include "tgaimage.h"
#include "model.h"
#include "geometry.h"
#include "iostream"
const TGAColor white = TGAColor(255, 255, 255, 255);
const TGAColor red = TGAColor(255, 0, 0, 255);
Model* model = NULL;
const int width = 800;
const int height = 800;
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);
}
for (int x = x0; x <= x1; x++) {
float t = (x - x0) / (float)(x1 - x0);
int y = y0 * (1. - t) + y1 * t;
if (steep) {
image.set(y, x, color);
}
else {
image.set(x, y, color);
}
}
}
int main(int argc, char** argv) {
if (2 == argc) { // 如果运行时有参数,就使用参数为 obj 的路径
model = new Model(argv[1]);
}
else { // 不然就使用 head.obj 为路径名
model = new Model("Garen.obj");
}
TGAImage image(width, height, TGAImage::RGB);
for (int i = 0; i < model->nfaces(); i++) {
std::vector<int> face = model->face(i);
for (int j = 0; j < 3; j++) {
Vec3f v0 = model->vert(face[j]);
Vec3f v1 = model->vert(face[(j + 1) % 3]); // 循环画线 0-1,1-2,2-0
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.flip_vertically(); // i want to have the origin at the left bottom corner of the image
image.write_tga_file("output.tga");
delete model;
return 0;
}
可能会有人对 int x0 = (v0.x + 1.) * width / 2.;
这系列操作有疑问,这是因为对于作者的 obj 文件,顶点是在 [-1,1] 之间的,所以他将其 +1,也就是把范围映射在了 [0,1] 之间,再 * width(height ) / 2 进行放大,这样的话,就会画在画布中间了。
但需要注意的是顶点是在 [-1,1] 之间并不是硬性规定,比如我自己在网上下的另一个模型,内容就是这样的:
当我直接使用
int x0 = v0.x;
int y0 = v0.y;
int x1 = v1.x;
int y1 = v1.y;
的方式去绘制时,结果是这样:
盖伦
完整画出来应该是这样:
完整的
所以如何将一个 obj 画在画布中间还是需要自己将范围进行映射。比如记录一下 x,y 的最大值,然后将坐标先除以最大值,这样都映射在了 [-1,1] 之间,再按照上面的方法绘制。
教程
https://github.com/ssloy/tinyrenderer/wiki/Lesson-1-Bresenham%E2%80%99s-Line-Drawing-Algorithm
网友评论