渲染器的着色器(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的渲染管线:
我们不能涉及的所有阶段都显示为蓝色,而我们的回调显示为橙色。事实上,我们的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建议将最终照明视为三种光强度的(加权)总和:环境照明(每个场景恒定)、漫射照明和镜面照明。
我们将漫反射照明计算为法线向量和灯光方向向量之间的角度的余弦。我的意思是,这假设光在所有方向上都被均匀反射。有光泽的表面会发生什么?在极限情况下(反射镜),当且仅当我们可以看到被该像素反射的光源时,像素才被照亮
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;
}
};
结果如下: