Lesson 6 重构代码

Lesson 6

重构源代码,下面将按照光栅器运行的顺序来介绍代码

读取模型

读取模型就是读取OBJ文件里的一系列数据,OBJ文件用文本方式打开就可以看见

image-20220908182119965

#ifndef __MODEL_H__
#define __MODEL_H__
#include <vector>
#include <string>
#include "geometry.h"
#include "tgaimage.h"

class Model {
private:
    std::vector<Vec3f> verts_; //储存三角形顶点数据,v开头
    std::vector<std::vector<Vec3i> > faces_; //面数据,Vec3i保存的是 v/vt/vn
    std::vector<Vec3f> norms_; //储存顶点的法向量 vn开头
    std::vector<Vec2f> uv_;    //储存纹理坐标 vt开头
    TGAImage diffusemap_;
    TGAImage normalmap_;
    TGAImage specularmap_;
    //加载纹理
    void load_texture(std::string filename, const char* suffix, TGAImage& img);
public:
    Model(const char* filename);
    ~Model();
    int nverts(); //返回顶点数量
    int nfaces(); //返回面数量
    Vec3f normal(int iface, int nthvert); //返回顶点法向量序号
    Vec3f normal(Vec2f uv);
    Vec3f vert(int i); //通过序号,返回顶点
    Vec3f vert(int iface, int nthvert);//返回顶点序号
    Vec2f uv(int iface, int nthvert); //返回纹理坐标序号
    TGAColor diffuse(Vec2f uv);
    float specular(Vec2f uv);
    std::vector<int> face(int idx); //返回一个面的三个顶点
};
#endif //__MODEL_H__
#include <iostream>
#include <fstream>
#include <sstream>
#include "model.h"

Model::Model(const char* filename) : verts_(), faces_(), norms_(), uv_(), diffusemap_(), normalmap_(), specularmap_() {
    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[i];
            verts_.push_back(v);
        }
        else if (!line.compare(0, 3, "vn ")) {			//读入顶点法向量数据
            iss >> trash >> trash;
            Vec3f n;
            for (int i = 0; i < 3; i++) iss >> n[i];
            norms_.push_back(n);
        }
        else if (!line.compare(0, 3, "vt ")) {			//读入纹理坐标数据
            iss >> trash >> trash;
            Vec2f uv;
            for (int i = 0; i < 2; i++) iss >> uv[i];
            uv_.push_back(uv);
        }
        else if (!line.compare(0, 2, "f ")) {			//读入面数据
            std::vector<Vec3i> f;
            Vec3i tmp;
            iss >> trash;
            while (iss >> tmp[0] >> trash >> tmp[1] >> trash >> tmp[2]) {
                for (int i = 0; i < 3; i++) tmp[i]--; // in wavefront obj all indices start at 1, not zero
                f.push_back(tmp);
            }
            faces_.push_back(f);
        }
    }
    std::cerr << "# v# " << verts_.size() << " f# " << faces_.size() << " vt# " << uv_.size() << " vn# " << norms_.size() << std::endl;
    //加载纹理
    load_texture(filename, "_diffuse.tga", diffusemap_);
    load_texture(filename, "_nm.tga", normalmap_);
    load_texture(filename, "_spec.tga", specularmap_);
}

Model::~Model() {}

int Model::nverts() {
    return (int)verts_.size();
}

int Model::nfaces() {
    return (int)faces_.size();
}

std::vector<int> Model::face(int idx) {
    std::vector<int> face;
    for (int i = 0; i < (int)faces_[idx].size(); i++) face.push_back(faces_[idx][i][0]);
    return face;
}

Vec3f Model::vert(int i) {
    return verts_[i];
}

Vec3f Model::vert(int iface, int nthvert) {
    return verts_[faces_[iface][nthvert][0]];
}

void Model::load_texture(std::string filename, const char* suffix, TGAImage& img) {
    std::string texfile(filename);
    size_t dot = texfile.find_last_of(".");
    if (dot != std::string::npos) {
        texfile = texfile.substr(0, dot) + std::string(suffix);
        std::cerr << "texture file " << texfile << " loading " << (img.read_tga_file(texfile.c_str()) ? "ok" : "failed") << std::endl;
        img.flip_vertically();
    }
}

TGAColor Model::diffuse(Vec2f uvf) {
    Vec2i uv(uvf[0] * diffusemap_.get_width(), uvf[1] * diffusemap_.get_height());
    return diffusemap_.get(uv[0], uv[1]);
}

Vec3f Model::normal(Vec2f uvf) {
    Vec2i uv(uvf[0] * normalmap_.get_width(), uvf[1] * normalmap_.get_height());
    TGAColor c = normalmap_.get(uv[0], uv[1]);
    Vec3f res;
    for (int i = 0; i < 3; i++)
        res[2 - i] = (float)c[i] / 255.f * 2.f - 1.f;
    return res;
}

Vec2f Model::uv(int iface, int nthvert) {
    return uv_[faces_[iface][nthvert][1]];
}

float Model::specular(Vec2f uvf) {
    Vec2i uv(uvf[0] * specularmap_.get_width(), uvf[1] * specularmap_.get_height());
    return specularmap_.get(uv[0], uv[1])[0] / 1.f;
}

Vec3f Model::normal(int iface, int nthvert) {
    int idx = faces_[iface][nthvert][2];
    return norms_[idx].normalize();
}

这就是导入模型数据的主要代码,其中涉及到TGA格式文件的使用,我也没太弄懂,但这不重要,我们的目的不是为了搞懂文件的具体编码,关注点应该放在渲染上面。

计算矩阵

模型导入完成后,现在需要对几个顶点着色器涉及到的矩阵进行计算处理

    lookat(eye, center, up);                                            //视图变换
    projection(-1.f / (eye - center).norm());                           //投影变换
    viewport(width / 8, height / 8, width * 3 / 4, height * 3 / 4);     //视角矩阵
    light_dir.normalize();                                              //光源

视图变换

视图变换的推导可以查看视图变换,视图变换的目的就是把世界空间变成摄像机空间,以摄像机为中心,这里变换的最终结果就是摄像机的聚焦点center移到了原点,摄像机位于-z方向看向原点,也就是看向center

void lookat(Vec3f eye, Vec3f center, Vec3f up) {
    Vec3f z = (eye - center).normalize();
    Vec3f x = cross(up, z).normalize();
    Vec3f y = cross(z, x).normalize();
    ModelView = Matrix::identity();
    Matrix translaition = Matrix::identity();
    Matrix rotation = Matrix::identity();
    for (int i = 0; i < 3; i++) {
        translaition[i][3] = -center[i];
    }
    for (int i = 0; i < 3; i++) {
        rotation[0][i] = x[i];
        rotation[1][i] = y[i];
        rotation[2][i] = z[i];
    }
    ModelView = rotation * translaition;
}

投影变换

这里采用的是透视投影,关于透视投影的推导可以查看投影

void projection(float coeff) {
    Projection = Matrix::identity();
    Projection[3][2] = coeff; //coeff = -1/c
}

视角矩阵

//视图矩阵,把模型坐标的(-1,1)转换成屏幕坐标的(100,700)
//zbuffer从(-1,1)转换成0~255
void viewport(int x, int y, int w, int h) {
    Viewport = Matrix::identity();
    Viewport[0][3] = x + w / 2.f;
    Viewport[1][3] = y + h / 2.f;
    Viewport[2][3] = 255.f / 2.f;
    Viewport[0][0] = w / 2.f;
    Viewport[1][1] = h / 2.f;
    Viewport[2][2] = 255.f / 2.f;
}

最后得到的 Viewport 矩阵是

300 0 0 400

0 300 0 400

0 0 255/2 255/2

0 0 0 0

当一个顶点(x ,y , z, 1)和这个视角矩阵相乘时,得到的结果是

(300x+400,300y+400,255/2*z+255/2,0)显然可以看出,模型坐标的(-1,1)转换成了屏幕坐标的(100,700),zbuffer从(-1,1)转换成0~255

初始化image和zbuffer

TGAImage image(width, height, TGAImage::RGB);
TGAImage zbuffer(width, height, TGAImage::GRAYSCALE);

同样,关于TGA格式文件的详细内容不会进行文字说明

初始化着色器

//这里采用高洛德着色
GouraudShader shader;
struct GouraudShader : public IShader {
    //顶点着色器会将数据写入varying_intensity
    //片元着色器从varying_intensity中读取数据
    Vec3f varying_intensity;
    mat<2, 3, float> varying_uv;
    //接受两个变量,(面序号,顶点序号)
    virtual Vec4f vertex(int iface, int nthvert) {
        //根据面序号和顶点序号读取模型对应顶点,并扩展为4维 
        Vec4f gl_Vertex = embed<4>(model->vert(iface, nthvert));
        varying_uv.set_col(nthvert, model->uv(iface, nthvert));
        //变换顶点坐标到屏幕坐标(视角矩阵*投影矩阵*变换矩阵*v)
        //先进行视图变换,把世界空间转换到摄像机空间,再进行投影变换,把三维空间变成二维空间,即图片
        mat<4, 4, float> uniform_M = Projection * ModelView;
        mat<4, 4, float> uniform_MIT = ModelView.invert_transpose();
        gl_Vertex = Viewport * uniform_M * gl_Vertex;
        //计算光照强度(顶点法向量*光照方向)
        Vec3f normal = proj<3>(embed<4>(model->normal(iface, nthvert))).normalize();
        varying_intensity[nthvert] = std::max(0.f, model->normal(iface, nthvert) * light_dir); // get diffuse lighting intensity
        return gl_Vertex;
    }
    //根据传入的质心坐标,颜色,以及varying_intensity计算出当前像素的颜色
    virtual bool fragment(Vec3f bar, TGAColor& color) {
        Vec2f uv = varying_uv * bar;
        TGAColor c = model->diffuse(uv);
        float intensity = varying_intensity * bar;
        color = c * intensity;
        return false; 
    }
};

绘制

    for (int i = 0; i < model->nfaces(); i++) {
        Vec4f screen_coords[3];
        for (int j = 0; j < 3; j++) {
            screen_coords[j] = shader.vertex(i, j);//为三角形的每个顶点调用顶点着色器
        }
        Vec2i uv[3];
        for (int k = 0; k < 3; k++) {
            uv[k] = model->uv(i, k);
        }
        triangle(screen_coords, shader, image, zbuffer);
    }
    image.flip_vertically();
    image.write_tga_file("output.tga");

循环遍历每个面,再遍历每个面的三个顶点,为三个顶点调用顶点着色器进行处理,处理结束后,对这个三角形面再进行绘制

绘制三角形

void triangle(Vec4f* pts, IShader& shader, TGAImage& image, TGAImage& zbuffer) {
    //初始化三角形边界框
    Vec2f bboxmin(std::numeric_limits<float>::max(), std::numeric_limits<float>::max());
    Vec2f bboxmax(-std::numeric_limits<float>::max(), -std::numeric_limits<float>::max());
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 2; j++) {
            //这里pts除以了最后一个分量,实现了透视中的缩放,所以作为边界框
            bboxmin[j] = std::min(bboxmin[j], pts[i][j] / pts[i][3]);
            bboxmax[j] = std::max(bboxmax[j], pts[i][j] / pts[i][3]);
        }
    }
    //当前像素坐标P,颜色color
    Vec2i P;
    TGAColor color;
    //遍历边界框中的每一个像素
    for (P.x = bboxmin.x; P.x <= bboxmax.x; P.x++) {
        for (P.y = bboxmin.y; P.y <= bboxmax.y; P.y++) {
            //c为当前P对应的质心坐标
            //这里pts除以了最后一个分量,实现了透视中的缩放,所以用于判断P是否在三角形内
            Vec3f c = barycentric(proj<2>(pts[0] / pts[0][3]), proj<2>(pts[1] / pts[1][3]), proj<2>(pts[2] / pts[2][3]), proj<2>(P));
            //插值计算P的zbuffer
            //pts[i]为三角形的三个顶点
            //pts[i][2]为三角形的z信息(0~255)
            //pts[i][3]为三角形的投影系数(1-z/c)

            float z_P = (pts[0][2] / pts[0][3]) * c.x + (pts[0][2] / pts[1][3]) * c.y + (pts[0][2] / pts[2][3]) * c.z;
            int frag_depth = std::max(0, std::min(255, int(z_P + .5)));
            //P的任一质心分量小于0或者zbuffer小于已有zbuffer,不渲染
            if (c.x < 0 || c.y < 0 || c.z<0 || zbuffer.get(P.x, P.y)[0]>frag_depth) continue;
            //调用片元着色器计算当前像素颜色
            bool discard = shader.fragment(c, color);
            //通过判断片元着色器的返回值来丢弃当前像素
            if (!discard) {
                //zbuffer
                zbuffer.set(P.x, P.y, TGAColor(frag_depth));
                //为像素设置颜色
                image.set(P.x, P.y, color);
            }
        }
    }

}

到这里我们的基本的绘制就结束了,这就是绘制一个三角形面的步骤,对于一个模型,我们这只需要不断地重复这些步骤就行了。

接下来是一些功能函数

功能函数

计算重心坐标

计算重心坐标的推导可以看重心坐标

Vec3f barycentric(Vec2f A, Vec2f B, Vec2f C, Vec2f P) {
    Vec3f s[2];
    for (int i = 2; i--; ) {
        s[i][0] = C[i] - A[i];
        s[i][1] = B[i] - A[i];
        s[i][2] = A[i] - P[i];
    }
    Vec3f u = cross(s[0], s[1]);
    if (std::abs(u[2]) > 1e-2)
        return Vec3f(1.f - (u.x + u.y) / u.z, u.y / u.z, u.x / u.z);
    return Vec3f(-1, 1, 1);
}

着色器基类

struct IShader {
    virtual ~IShader();
    virtual Vec4f vertex(int iface, int nthvert) = 0;
    virtual bool fragment(Vec3f bar, TGAColor& color) = 0;
};

上面涉及到TGA文件的代码我没有讲解,因为源码较为繁杂,我也没咋看,并且不是此次课程的重点,只需要知道TGA的函数的功能即可,就略过。不了解TGA文件并不会影响渲染部分的代码的阅读。

复习一下步骤

读取模型数据->初始化矩阵->初始化着色器->对顶点进行顶点着色器处理->绘制处理完的顶点形成的面->重复

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值