手搓一个小型渲染器(六)

渲染器的着色器(Shader)

重构代码

在之前,我们将相机移动以及三角渲染的算法都放在了main.cpp中,这会使得main中看起来很庞大,我们可以构建属于我们自己的gl库,在OpenGL中,gl.cpp是由其生成的二进制文件,而现在我们可以构建属于我们自己的GL库,那么我们需要思考在这里面需要处理什么。答案很明显,ModelView,Viewport和Projection矩阵,初始化函数以及三角形光栅化。

main.cpp

#include <vector>
#include <string>
#include <iostream>
#include "tgaimage.h"
#include "model.h"
#include "gl.h"

Model* model = NULL;
const int width = 800;
const int height = 800;

Vec3f light_dir = {1., 1., 1.};
Vec3f eye = { 0, 0, 20 };
Vec3f center{ 0, 0, 0 };
Vec3f up = { 0, 1, 0 };

Matrix ModelView{};
Matrix Viewport{};
Matrix Projection{};

class GouraudShader : public IShader {
private:
    Vec3f varying_intensity;

public:
    virtual Vec4f vertex(int iface, int nthvert) {
        //转换为齐次坐标
        Vec4f gl_Vertex = embed<4>(model->vert(iface, nthvert));
        gl_Vertex = Viewport * Projection * ModelView * gl_Vertex;
        varying_intensity[nthvert] = std::max(0.f, model->normal(iface, nthvert) * light_dir);
        return gl_Vertex;
    }

    virtual bool fragment(Vec3f bar, TGAColor& color) {
        float intensity = varying_intensity * bar;
        color = TGAColor{ unsigned char(255 * intensity), unsigned char(255 * intensity), unsigned char(255 * intensity) };
        return false;
    }
};

int main() {
    model = new Model("./obj/african_head/african_head.obj");

    lookat(eye, center, up);
    viewport(width / 8, height / 8, width * 3 / 4, height * 3 / 4);
    projection(1. / (eye - center).norm());
    light_dir.normalize();

    TGAImage image(width, height, TGAImage::RGB);
    TGAImage Pimage(width, height, TGAImage::RGB);
    //把zbuffer也存成和image同样大小的灰度图
    TGAImage zbuffer(width, height, TGAImage::GRAYSCALE);
    
    GouraudShader shader;
    PhoneShader Pshader;
    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);
        }
        triangle(screen_coords, shader, image, zbuffer);
        
    }

    image.write_tga_file("output.tga");
    zbuffer.write_tga_file("zbuffer.tga");

    delete model;
    return 0;
}

让我们分析一下上面的main函数主要做了什么,首先我们忽略最上方对矩阵的初始化,在main中,我们主要做了下面这三个行为:

  • 读取obj文件
  • ModelView、Projection和Viewport矩阵的初始化(这些矩阵的实际初始化是放在gl.cpp中的)
  • 迭代模型中所有三角形,并将它们光栅化

最后一步则是通过两个遍历,外部遍历所有的三角形面,而内部的遍历则是对三角形的每个顶点待用顶点着色器,**顶点着色器的主要目的有两个,一个是变换顶点的坐标,从空间坐标转换到齐次坐标,并且乘上投影矩阵,另一个是为片元(像素)着色器准备数据。**之后我们就调用了光栅化的程序,在光栅化程序中,我们判断每个三角形的包围框,判断像素元素是否在三角形内部,之后调用片元着色器。

片元着色器的主要目标是确定当前像素的颜色。次要目标-我们可以通过返回true来丢弃当前像素。

下面是OpenGL2的渲染管线:

img

我们不能涉及的所有阶段都显示为蓝色,而我们的回调显示为橙色。事实上,我们的main()函数-是原始处理例程。它调用顶点着色器。我们这里没有原始装配,因为我们只绘制哑三角形(在我们的代码中,它与原始处理合并)。triangle()函数-是光栅化器,它为三角形内的每个点调用片段着色器,然后执行深度检查(z-buffer)等。

Gouraud Shader

class GouraudShader : public IShader {
private:
    //每个顶点上的光照强度
    Vec3f varying_intensity;

public:
    virtual Vec4f vertex(int iface, int nthvert) {
        //转换为齐次坐标
        Vec4f gl_Vertex = embed<4>(model->vert(iface, nthvert));
        gl_Vertex = Viewport * Projection * ModelView * gl_Vertex;
        varying_intensity[nthvert] = std::max(0.f, model->normal(iface, nthvert) * light_dir);
        return gl_Vertex;
    }

    virtual bool fragment(Vec3f bar, TGAColor& color) {
        float intensity = varying_intensity * bar;
        color = TGAColor{ unsigned char(255 * intensity), unsigned char(255 * intensity), unsigned char(255 * intensity) };
        return false;
    }
};

vertex shader

 Vec3f varying_intensity;

virtual Vec4f vertex(int iface, int nthvert) {
    //转换为齐次坐标
    Vec4f gl_Vertex = embed<4>(model->vert(iface, nthvert));
    gl_Vertex = Viewport * Projection * ModelView * gl_Vertex;
    varying_intensity[nthvert] = std::max(0.f, model->normal(iface, nthvert) * light_dir);
    return gl_Vertex;
}

variation是GLSL语言中的一个保留关键字,使用varying_intensity作为名称来显示对应关系。在不同的变量中,我们将要插值的数据存储在三角形内部,片元着色器将获得插值。

fragment shader:

    virtual bool fragment(Vec3f bar, TGAColor& color) {
        float intensity = varying_intensity * bar;
        color = TGAColor(255, 255, 255)*intensity;
        return false;
    }

fragment shader的输入为重心坐标中三个顶点的重心权重,而intensity则是三个顶点的光照强度,分别相乘并将其作为系数乘上白色光,最终得到颜色值。

TGAColor color;
    bool discard = shader.fragment(c, color);
    if (!discard) {
        zbuffer.set(P.x, P.y, TGAColor(P.z));
        image.set(P.x, P.y, color);
    }

片元着色器可以丢弃当前像素的绘制,然后光栅化器简单地跳过它。如果我们想创建二进制掩码或任何你想要的东西。

在这里插入图片描述

对shader的修改

public:
    virtual Vec4f vertex(int iface, int nthvert) {
        //转换为齐次坐标
        Vec4f gl_Vertex = embed<4>(model->vert(iface, nthvert));
        gl_Vertex = Viewport * Projection * ModelView * gl_Vertex;
        varying_intensity[nthvert] = std::max(0.f, model->normal(iface, nthvert) * light_dir);
        return gl_Vertex;
    }

    virtual bool fragment(Vec3f bar, TGAColor& color) {
        float intensity = varying_intensity * bar;

        if (intensity > 0.85) intensity = 1.;
        else if (intensity > 0.6) intensity = 0.85;
        else if (intensity > 0.45) intensity = 0.6;
        else if (intensity > 0.3) intensity = 0.45;
        else if (intensity > 0.15) intensity = 0.3;
        else intensity = 0.;
        color = TGAColor(255, 155, 0) * intensity;
        //color = TGAColor{ unsigned char(255 * intensity), unsigned char(255 * intensity), unsigned char(255 * intensity) };
        return false;
    }
};

我们将结果归类于下面六个范围,最终结果如下:
在这里插入图片描述

Texture Shader

class TextureShader :public IShader {
private:
    Vec3f          varying_intensity; // written by vertex shader, read by fragment shader
    //我们将顶点的贴图存在一个2*3的矩阵中,每一列代表各个顶点的uv值
    mat<2, 3, float> varying_uv;

public:
    virtual Vec4f vertex(int iface, int nthvert) {
        varying_uv.set_col(nthvert, model->uv(iface, nthvert));
        varying_intensity[nthvert] = std::max(0.f, model->normal(iface, nthvert) * light_dir);
        //转换为齐次坐标
        Vec4f gl_Vertex = embed<4>(model->vert(iface, nthvert));
        gl_Vertex = Viewport * Projection * ModelView * gl_Vertex;
        return gl_Vertex;
    }

    virtual bool fragment(Vec3f bar, TGAColor& color) {
        float intensity = varying_intensity * bar;
        //2 * 3 * 3 * 2,重心坐标直接转换为矩阵运算
        Vec2f uv = varying_uv * bar;
        //color = model->diffuse(uv) * intensity;
        color = model->diffuse(uv);
        return false;
    }
};

结果如下:
在这里插入图片描述

Normal Mapping

现在我们有了纹理坐标,实际上我们可以在纹理图像中存放任何我们想要的,它可以是颜色、方向、温度等等。如果我们将RGB值解释为在xyz中的方向,那么上面的贴图将会为每个像素提供法向量,而不是向之前的model那样在obj文件中提供顶点的法向量。
在这里插入图片描述

上面这幅图像实在切线空间中给出法向量。

#include <vector>
#include <string>
#include <iostream>
#include "tgaimage.h"
#include "model.h"
#include "gl.h"

Model* model = NULL;
const int width = 800;
const int height = 800;


Vec3f light_dir = {1., 1., 1.};
Vec3f eye = { 5, 3, 5 };
Vec3f center{ 0, 0, 0 };
Vec3f up = { 0, 1, 0 };

Matrix ModelView{};
Matrix Viewport{};
Matrix Projection{};

class GouraudShader : public IShader {
private:
    //每个顶点上的光照强度
    Vec3f varying_intensity;

public:
    virtual Vec4f vertex(int iface, int nthvert) {
        //转换为齐次坐标
        Vec4f gl_Vertex = embed<4>(model->vert(iface, nthvert));
        gl_Vertex = Viewport * Projection * ModelView * gl_Vertex;
        varying_intensity[nthvert] = std::max(0.f, model->normal(iface, nthvert) * light_dir);
        return gl_Vertex;
    }

    virtual bool fragment(Vec3f bar, TGAColor& color) {
        float intensity = varying_intensity * bar;

        if (intensity > 0.85) intensity = 1.;
        else if (intensity > 0.6) intensity = 0.85;
        else if (intensity > 0.45) intensity = 0.6;
        else if (intensity > 0.3) intensity = 0.45;
        else if (intensity > 0.15) intensity = 0.3;
        else intensity = 0.;
        color = TGAColor(255, 155, 0) * intensity;
        //color = TGAColor{ unsigned char(255 * intensity), unsigned char(255 * intensity), unsigned char(255 * intensity) };
        return false;
    }
};

class TextureShader :public IShader {
private:
    Vec3f          varying_intensity; // written by vertex shader, read by fragment shader
    //我们将顶点的贴图存在一个2*3的矩阵中,每一列代表各个顶点的uv值
    mat<2, 3, float> varying_uv;

public:
    virtual Vec4f vertex(int iface, int nthvert) {
        varying_uv.set_col(nthvert, model->uv(iface, nthvert));
        varying_intensity[nthvert] = std::max(0.f, model->normal(iface, nthvert) * light_dir);
        //转换为齐次坐标
        Vec4f gl_Vertex = embed<4>(model->vert(iface, nthvert));
        gl_Vertex = Viewport * Projection * ModelView * gl_Vertex;
        return gl_Vertex;
    }

    virtual bool fragment(Vec3f bar, TGAColor& color) {
        float intensity = varying_intensity * bar;
        //2 * 3 * 3 * 2,重心坐标直接转换为矩阵运算
        Vec2f uv = varying_uv * bar;
        //color = model->diffuse(uv) * intensity;
        color = model->diffuse(uv);
        return false;
    }
};

class NormalShader : public IShader {
public:
    Vec3f          varying_intensity; // written by vertex shader, read by fragment shader
    //我们将顶点的贴图存在一个2*3的矩阵中,每一列代表各个顶点的uv值
    mat<2, 3, float> varying_uv;
    mat<4, 4, float> uniform_M; 
    mat<4, 4, float> uniform_MIT;

    virtual Vec4f vertex(int iface, int nthvert) {
        varying_uv.set_col(nthvert, model->uv(iface, nthvert));
        varying_intensity[nthvert] = std::max(0.f, model->normal(iface, nthvert) * light_dir);
        //转换为齐次坐标
        Vec4f gl_Vertex = embed<4>(model->vert(iface, nthvert));
        gl_Vertex = Viewport * Projection * ModelView * gl_Vertex;
        return gl_Vertex;
    }

    virtual bool fragment(Vec3f bar, TGAColor& color) {
        
        //2 * 3 * 3 * 2,重心坐标直接转换为矩阵运算
        Vec2f uv = varying_uv * bar;

        Vec3f n = proj<3>(uniform_MIT * embed<4>(model->normal(uv))).normalize();
        Vec3f l = proj<3>(uniform_M * embed<4>(light_dir)).normalize();

        float intensity = std::max(0.f, n * l);
        color = model->diffuse(uv) * intensity;
        return false;
    }
};

int main() {

    model = new Model("./obj/african_head/african_head.obj");

    lookat(eye, center, up);
    viewport(width / 8, height / 8, width * 3 / 4, height * 3 / 4);
    projection(1. / (eye - center).norm());
    light_dir.normalize();

    TGAImage image(width, height, TGAImage::RGB);
    TGAImage Pimage(width, height, TGAImage::RGB);
    //把zbuffer也存成和image同样大小的灰度图
    TGAImage zbuffer(width, height, TGAImage::GRAYSCALE);
    
    GouraudShader shader;
    TextureShader Tshader;
    NormalShader Nshader;

    Nshader.uniform_M = Projection * ModelView;
    Nshader.uniform_MIT = (Projection * ModelView).invert_transpose();

    for (int i = 0; i < model->nfaces(); ++i) {
        Vec4f screen_coords[3];
        for (int j = 0; j < 3; ++j){
            screen_coords[j] = Nshader.vertex(i, j);
        }
        triangle(screen_coords, Nshader, image, zbuffer);
        
    }

    image.write_tga_file("NormalMapping.tga");
    zbuffer.write_tga_file("zbuffer.tga");
    delete model;
    return 0;

}

结果如下:
在这里插入图片描述

Blinn-Phong

为了欺骗视觉,我们使用了Phong对照明模型的近似。Phong建议将最终照明视为三种光强度的(加权)总和:环境照明(每个场景恒定)、漫射照明和镜面照明。

img

我们将漫反射照明计算为法线向量和灯光方向向量之间的角度的余弦。我的意思是,这假设光在所有方向上都被均匀反射。有光泽的表面会发生什么?在极限情况下(反射镜),当且仅当我们可以看到被该像素反射的光源时,像素才被照亮

img

class BPhongShader : public IShader {
public:
    mat<2, 3, float> varying_uv;
    mat<4, 4, float> uniform_M;
    mat<4, 4, float> uniform_MIT;

    virtual Vec4f vertex(int iface, int nthvert) {
        varying_uv.set_col(nthvert, model->uv(iface, nthvert));
        //转换为齐次坐标
        Vec4f gl_Vertex = embed<4>(model->vert(iface, nthvert));
        gl_Vertex = Viewport * Projection * ModelView * gl_Vertex;
        return gl_Vertex;
    }

    virtual bool fragment(Vec3f bar, TGAColor& color) {

        //2 * 3 * 3 * 2,重心坐标直接转换为矩阵运算
        Vec2f uv = varying_uv * bar;

        Vec3f n = proj<3>(uniform_MIT * embed<4>(model->normal(uv))).normalize();
        Vec3f l = proj<3>(uniform_M * embed<4>(light_dir)).normalize();
		
        //光照和观察向量得到的半程向量
        Vec3f r = (n * (n * l * 2.f) - l).normalize();
        //镜面反射,用的是模型有的镜面贴图
        float spec = pow(std::max(r.z, 0.0f), model->specular(uv));
        //漫反射
        float diff = std::max(0.f, n * l);
        TGAColor c = model->diffuse(uv);
        color = c;
        for (int i = 0; i < 3; i++) color[i] = std::min<float>(5 + c[i] * (diff + .6 * spec), 255);

        return false;
    }
};

结果如下:
在这里插入图片描述
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值