前言:第 6 课,这一章超级长。为了静下心来我又翻译了这篇文章。
回想一下,我在这里的所有源代码都要与你的源代码进行比较。不要使用我的代码,写自己的代码。我是一个糟糕的程序员。请做最疯狂的着色器并发送给我图像,我会在这里发布。
首先,让我们检查一下源代码的当前状态
- geometry.cpp+.h — 218 lines
- model.cpp+.h — 139 lines
- our_gl.cpp+.h — 102 lines
- main.cpp — 66 lines
总共 525 行,正是我们想要的。请注意,负责实际渲染的唯一文件位于 our_gl. 和 main.cpp 中,共有 168 行*。
重构源代码(Refactoring the source code)
好的,我们的 main.cpp 太多了,让我们把它分成两部分:
our_gl.cpp + h 这部分程序员无法触及:粗略地说,它是 OpenGL 库文件
main.cpp - 在这里我们可以编程我们想要的一切。
现在,要把什么放入了 our_gl? ModelView,Viewport 和Projection 矩阵以及初始化函数和三角形光栅化器。就这些!
这是文件 our_gl.h 的内容(稍后我将介绍 IShader):
#include "tgaimage.h"
#include "geometry.h"
extern Matrix ModelView;
extern Matrix Viewport;
extern Matrix Projection;
void viewport(int x, int y, int w, int h);
void projection(float coeff=0.f); // coeff = -1/c
void lookat(Vec3f eye, Vec3f center, Vec3f up);
struct IShader {
virtual ~IShader();
virtual Vec3i vertex(int iface, int nthvert) = 0;
virtual bool fragment(Vec3f bar, TGAColor &color) = 0;
};
void triangle(Vec4f *pts, IShader &shader, TGAImage &image, TGAImage &zbuffer);
文件 main.cpp 现在只有 66 行,因此我完整地列了出来(不好意思啊,代码很长,但是我太喜欢这代码了,所以我要列出来):
#include
#include
#include "tgaimage.h"
#include "model.h"
#include "geometry.h"
#include "our_gl.h"
Model *model = NULL;
const int width = 800;
const int height = 800;
Vec3f light_dir(1,1,1);
Vec3f eye(1,1,3);
Vec3f center(0,0,0);
Vec3f up(0,1,0);
struct GouraudShader : public IShader {
Vec3f varying_intensity; // written by vertex shader, read by fragment shader
virtual Vec4f vertex(int iface, int nthvert) {
varying_intensity[nthvert] = std::max(0.f, model->normal(iface, nthvert)*light_dir); // get diffuse lighting intensity
Vec4f gl_Vertex = embed<4>(model->vert(iface, nthvert)); // read the vertex from .obj file
return Viewport*Projection*ModelView*gl_Vertex; // transform it to screen coordinates
}
virtual bool fragment(Vec3f bar, TGAColor &color) {
float intensity = varying_intensity*bar; // interpolate intensity for the current pixel
color = TGAColor(255, 255, 255)*intensity; // well duh
return false; // no, we do not discard this pixel
}
};
int main(int argc, char** argv) {
if (2==argc) {
model = new Model(argv[1]);
} else {
model = new Model("obj/african_head.obj");
}
lookat(eye, center, up);
viewport(width/8, height/8, width*3/4, height*3/4);
projection(-1.f/(eye-center).norm());
light_dir.normalize();
TGAImage image (width, height, TGAImage::RGB);
TGAImage zbuffer(width, height, TGAImage::GRAYSCALE);
GouraudShader shader;
for (int i=0; infaces(); 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. flip_vertically(); // to place the origin in the bottom left corner of the image
zbuffer.flip_vertically();
image. write_tga_file("output.tga");
zbuffer.write_tga_file("zbuffer.tga");
delete model;
return 0;
}
让我们看看它是如何工作的。跳过头文件,我们声明几个全局常量:屏幕尺寸,相机位置等。我将在下一段中解释 GouraudShader ,所以让我们跳过它。接下来就是实际的主函数:
- 转换 .obj 文件
- 初始化 ModelView, Projection and Viewport 矩阵。(回想一下,这些矩阵都是在 our_gl 模块中)
- 迭代模型的所有三角形和每个三角形的光栅化。
最后一步特别好玩。外循环遍历所有三角形。内循环遍历当前三角形的所有顶点,并用顶点着色器给每个顶点着色。
顶点着色器的主要目的是去转换顶点的坐标,次要目的是为像素着色器提供数据
那之后发生了什么?我们可以称之光栅化例程。光栅器里面发生了什么我不知道(好吧,其实我们知道,因为我们实现了它)我知道我们的光栅器调用了给每一个像素调用了一个函数。这个函数被叫做
像素着色器(the fragment shader)。对于每一个在三角形的像素来说,都会调用像素着色器
像素着色器的主要目的是决定当前像素的颜色,次要目的是通过返回 true 丢弃当前像素
OpenGL 2 的渲染管道可以由下图表示
由于我课程的限制,我缩减了 OpenGL 2 的管道,仅有片段着色器和顶点着色器。在较新的版本中,还有其他着色器
好的,在上面的图像中,我们无法触摸的所有阶段都以蓝色显示,而我们的回调以橙色显示。实际上,我们的 main() 函数(之前写的)是原始处理例程。它调用顶点着色器。在之前的代码中,我们没有「primitive assembly」。因为我们只是画出了哑三角形(在我们的代码中,它合并了「primitive processing 」)。triangle() 函数 - 是光栅器。对于在三角形内的每一点会调用像素着色器,然后用 zbuffer 检查
结束了!你知道着色器是什么现在你可以创建自己的着色器!
实现一个高洛德着色器(My implementation of shaders shown on Gouraud shading)
让我们检查一下我在 main.cpp 中列出的着色器。根据它的名字,它是一个 Gouraud 着色器。让我重新列出代码:
Vec3f varying_intensity; // written by vertex shader, read by fragment shader
virtual Vec4f vertex(int iface, int nthvert) {
varying_intensity[nthvert] = std::max(0.f, model->normal(iface, nthvert)*light_dir); // get diffuse lighting intensity
Vec4f gl_Vertex = embed<4>(model->vert(iface, nthvert)); // read the vertex from .obj file
return Viewport*Projection*ModelView*gl_Vertex; // transform it to screen coordinates
}
varying 是 GLSL 语言中保留的关键字。我使用了varying_intensity作为名字,与 GLSL 对应。在不同的变量中我们存储了数据用来在三角形中插入,像素着色器得到插入的值(给每一个像素)
让我们重新列出像素着色器:
Vec3f varying_intensity; // written by vertex shader, read by fragment shader
// [...]
virtual bool fragment(Vec3f bar, TGAColor &color) {
float intensity = varying_intensity*bar; // interpolate intensity for the current pixel
color = TGAColor(255, 255, 255)*intensity; // well duh
return false; // no, we do not discard this pixel
}
对于我们绘制的三角形中,每个像素调用此例程。它接收用于变换数据的内插的重心坐标作为输入。因此,插值强度可以计算为
varying_intensity[0]*bar[0]+varying_intensity[1]*bar[1]+varying_intensity[2]*bar[2]
或者仅仅作为两个向量之间的点积:varying_intensity * bar。当然,在真正的GLSL中,像素着色器接收准备插值。
请注意着色器返回 bool 值。如果我们查看光栅化器(our_gl.cpp,triangle() 函数),它很容易理解它的作用:
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);
}
像素着色器可以丢弃当前像素的绘制,然后光栅化器只是跳过它。如果我们想要创建二进制掩码或任何你想要的东西,这是很方便的(请查看第 9 课,丢弃像素是一个非常酷的例子)。
当然,光栅器无法画出你编程的所有奇怪的东西,因此无法使用着色器进行预编译。这里我们使用抽象类 IShader 作为两者之间的中间体。哇,我很少使用抽象类,但没有它我们会在这里受苦。功能指针很难看。
着色器的第一次修改(First modification of the shaders)
virtual bool fragment(Vec3f bar, TGAColor &color) {
float intensity = varying_intensity*bar;
if (intensity>.85) intensity = 1;
else if (intensity>.60) intensity = .80;
else if (intensity>.45) intensity = .60;
else if (intensity>.30) intensity = .45;
else if (intensity>.15) intensity = .30;
else intensity = 0;
color = TGAColor(255, 155, 0)*intensity;
return false;
}
简单修改Gourad着色,其中强度只允许6个值,结果如下:
纹理(Textures)
我会跳过 Phong 阴影,但看一下这篇文章。还记得我给你的纹理作业吗?我们必须插入uv坐标。所以,我创建了一个2x3矩阵。 u和v为2行,3列(每个顶点一列)。
struct Shader : public IShader {
Vec3f varying_intensity; // written by vertex shader, read by fragment shader
mat<2,3,float> varying_uv; // same as above
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); // get diffuse lighting intensity
Vec4f gl_Vertex = embed<4>(model->vert(iface, nthvert)); // read the vertex from .obj file
return Viewport*Projection*ModelView*gl_Vertex; // transform it to screen coordinates
}
virtual bool fragment(Vec3f bar, TGAColor &color) {
float intensity = varying_intensity*bar; // interpolate intensity for the current pixel
Vec2f uv = varying_uv*bar; // interpolate uv for the current pixel
color = model->diffuse(uv)*intensity; // well duh
return false; // no, we do not discard this pixel
}
};
结果长这样
法线贴图(Normalmapping)
好的,现在我们有纹理坐标。我们可以在纹理图像中存储什么?事实上,几乎任何事情。它可以是颜色,方向,温度等。让我们加载这个纹理:
[图片上传失败...(image-a19ab-1553651684120)]
如果我们将 RGB 值解释为 xyz 方向,则此图像为渲染的每个像素提供法线向量,而不是像以前那样为每个顶点提供法向向量。
顺便说一句,将此图像与另一个图像进行比较,它会提供完全相同的信息,但在另一个框架中:
其中一个图像在全局(笛卡尔)坐标系中给出法向矢量,在Darboux 框架(切线空间)中给出另一个矢量。在Darboux框架中,z - 向量垂直于物体,x - 主曲率方向和y - 它们的叉积。
第一个作业:
找出哪个图片属于 Darboux frame 哪个图片属于 global coordinate frame?
第二个作业:
哪个好?为什么。
镜面映射(Specular mapping)
所有的计算机图形科学都是欺骗的艺术。为了(廉价地)欺骗眼睛,我们使用了 Phong 对照明模型的近似。
Phong建议将最终照明视为三种光强度的(加权)总和:环境照明(每个场景的常数),漫射照明(我们计算到目前为止的那个)和镜面照明。 看看下面的图片,它说明了一切:
我们计算漫反射光照作为法向矢量和光方向矢量之间角度的余弦。我的意思是,这假设光线在所有方向上均匀地反射。有光泽的表面会发生什么?在极限情况下(镜像),当且仅当我们能够看到此像素反射的光源时,像素才会亮起来:
对于漫射照明,我们计算了矢量 n 和 l 之间的(余弦)角度,现在我们感兴趣的是矢量 r(反射光方向)和 v(视图方向)之间的角度(余弦)。
第三个作业:
给定向量 n 和 l,找到向量 r。
答案是:
如果n和l归一化,则 r = 2n<n,l> - l
对于漫反射来说我们计算余弦作为光照强度。但是一个光滑的表面在一个方向反射比其他方向要多得多。好的,那么,如果我们使用 10 倍的余弦会怎么样呢?回想一下,如果这样做,所有比 1 小的数字会减少。那意味着,10 倍的余弦将将导致光束更小的半径。而百分之一的光束半径要小得多。这个倍数藏在特殊的纹理中(镜面纹理 specular mapping texture),告诉我们每一个点是不是光滑的。
struct Shader : public IShader {
mat<2,3,float> varying_uv; // same as above
mat<4,4,float> uniform_M; // Projection*ModelView
mat<4,4,float> uniform_MIT; // (Projection*ModelView).invert_transpose()
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)); // read the vertex from .obj file
return Viewport*Projection*ModelView*gl_Vertex; // transform it to screen coordinates
}
virtual bool fragment(Vec3f bar, TGAColor &color) {
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(); // reflected light
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(5 + c[i]*(diff + .6*spec), 255);
return false;
}
};
我认为除了系数之外,我不需要在上面的代码中评论任何内容。
for (int i=0; i<3; i++) color[i] = std::min(5 + c[i]*(diff + .6*spec), 255);
周围部分为 5,漫反射部分为 1。0.6 作为镜面反射部分。你可以自己改变系数。不同的选择物体会有不同的样貌。通常是,画家去提供这些系数。
请记住,这些系数之和通常为 1。但是你知道,我喜欢创造光。
最后实现渲染器的代码