1 图形渲染管线
- 输入:点和连接关系(比如.obj格式,就是一系列三维点的坐标和连接关系组成的)。
- 输入->顶点流:将点变换到屏幕空间(MVP变换)。
- 顶点流->三角形流:将三角形投影到屏幕空间。
- 三角形流->片段或像素流:光栅化,把原本连续表示的三角形离散化成屏幕上的片段(fragment),或称为像素(采样三角形)。
- 处理遮挡(Z-Buffer可见性测试)。
- 片段流->着色的片段流:对片段着色(blinn-phong着色模型、纹理映射/插值)。
- 输出:图像(pixels)。
对于传统渲染管线,
不足:难以处理阴影和光线弹射。
优点:并行度高,速度快。
2 OpenGL
OpenGL是一套从CPU调用GPU的API。
- 因此,语言不重要。
- 跨平台。
- 代替品(DirectX,Vulkan,etc)。
缺点
- 碎片化:许多不同的版本。不同的版本有不同的特性。
- C风格的代码,没有面向对象的概念,不好用。
- 难以debug。
理解
OpenGL的渲染管线可以与 GAMES101 中的软光栅化渲染器进行 1 对 1 映射。
2.1 使用OpenGL过程的比喻: 油画过程
A. Place objects/models 放置对象/模型。 Model变换
B. Set position of an easel 设置画架的位置。 View变换
C. Attach a canvas to the easel 将画布连接到画架(easel)。 设置framebuffer
D. Paint to the canvas 画到画布上。 深度测试、光栅化、着色
E. (Attach other canvases to the easel and continue painting)(将其他画布连接到画架上并继续绘画)。 可以指定很多输出
F. (Use previous paintings for reference)(参考以前的画作)。 存储中间结果到纹理上,应用有shadow mapping存储深度为中间结果
下面的内容对这几个步骤逐一说明。
2.2 Place objects/models
相当于渲染中的模型变换,要考虑两个问题。
- 放哪些模型?
- 模型怎么摆放?
放这些模型
用户指定对象的顶点、法线、纹理坐标并将它们作为顶点缓冲区对象 (VBO) 发送到 GPU。
- 与.obj格式类似
模型这么摆放
使用OpenGL函数来获取矩阵
- eg:glTranslate,glMultMatrix
- 不需要自己计算矩阵。
2.3 Set up an easel
- 对应渲染中的视图变换。
- 这里的画架easel对应OpenGL中的framebuffer。
- 同样无需复杂计算,只需确定fov等参数调用以下函数就可以生成视图矩阵。
2.4 Attach a canvas to the easel
用油画比喻:OpenGL能够使用相同的画架画出多幅图。
在OpenGL中可以一次渲染出很多纹理(只需渲染场景一次)。
- 指定要使用的framebuffer。
- 指定一个或多个纹理作为输出(shading, depth, etc.)。
- 渲染 (fragment shader 指定每个texture的内容)。
注:一般不会直接渲染到屏幕,由于可能渲染不完,会出现画面撕裂的情况。
解决撕裂问题的方法:垂直同步/双重缓存/三重缓存。
2.5 Paint to the canvas
对应渲染中的着色过程。
- 使用顶点着色器(vertex)和片段(fragment)着色器。
对每个并行顶点
- OpenGL 调用用户指定的vertex shader变换顶点 (使用ModelView, Projection等变换矩阵)。
对于每个图元(primitive)
- OpenGL为片段覆盖的每个像素生成一个片段(这个片段可以直接理解成像素)。
对每个片段(fragment)
- OpenGL调用用户指定的fragment shader。
- 对于深度测试,可以自己做深度测试或让OpenGL做。
fragment中的值可以当作之前被处理过的,就是说顶点着色器可能只是三个点的坐标或者颜色,但是到了片段着色器里面,像素肯定远远不止三个,而这中间的像素的属性,就可以根据顶点进行插值,这些插值的属性使用关键字varying来定义。
2.6 总结
OpenGL的使用步骤
- 指定物体,相机,MVP矩阵(MVP矩阵可以让OpenGL帮你算,然后将变换矩阵从CPU传给GPU)。
- 指定画架(fragment)和输入输出纹理。
- 指定vertex/fragment shaders(最重要的手写部分,决定渲染效果)。
- (确定把所用东西都放到了GPU里)就可以渲染了!
OpenGL是状态机模型:指定现在要用哪些东西,执行哪些操作。
Buffer可以认为是内存中的一块区域。
其他
- F. Multiple passes,OpenGL可以进行多次渲染!(使用之前的结果作为参考)。
3 OpenGL Shading Language(GLSL)
- GLSL是描述Vertex/Fragment shading的小程序(运行在GPU上)。
- 语法风格与C类型,但是有结构体。
3.1 着色语言的历史
- 上古时代:在GPU上写汇编。
- 出现了Stanford Real-Time Shading Language,在SGI上工作,减小了难度。
- 很久以前:NVIDIA的Cg着色语言。
- Direct中的HLSL(vertex + pixel)。High Level Shading Language。
- OpenGL中的GLSL(vertex + fragment)。
3.2 Shader的在OpenGL中的使用过程
- Create shader (Vertex and Fragment) 创建shader
- Compile shader 编译shader
- Attach shader to program 把shader绑定到程序
- Link program 链接程序
- Use program 使用程序(这些程序是整个应用程序的子程序,通过useprogram调用,调用了之后意味着drawVertex这种函数都会使用你当前use的着色器来渲染。)
Shader代码只是一系列字符串。编译shader与编译一个普通的程序相似。
例子
GLuint initshaders (GLenum type, const char *filename) {
// Using GLSL shaders, OpenGL book, page 679
GLuint shader = glCreateShader(type) ;
GLint compiled ;
string str = textFileRead (filename) ;
GLchar * cstr = new GLchar[str.size()+1] ;
const GLchar * cstr2 = cstr ; // Weirdness to get a const char
strcpy(cstr,str.c_str()) ;
glShaderSource (shader, 1, &cstr2, NULL) ;
glCompileShader (shader) ;
glGetShaderiv (shader, GL_COMPILE_STATUS, &compiled) ;
if (!compiled) {
shadererrors (shader) ;
throw 3 ;
}
return shader ;
}
GLuint initprogram (GLuint vertexshader, GLuint fragmentshader)
{
GLuint program = glCreateProgram() ;
GLint linked ;
glAttachShader(program, vertexshader) ;
glAttachShader(program, fragmentshader) ;
glLinkProgram(program) ;
glGetProgramiv(program, GL_LINK_STATUS, &linked) ;
if (linked) glUseProgram(program) ;
else {
programerrors(program) ;
throw 4 ;
}
return program ;
}
3.3 调试Shader
1、很多年前: NVIDIA Nsight with Visual Studio
- 需要多个 GPUs 调试 GLSL。
- 必须在 HLSL 中以软件模拟模式运行。
2、现在
- Nsight Graphics (cross platform, NVIDIA GPUs only) 与Visual Studio分离。
- RenderDoc (cross platform, no limitations on GPUs)。
- 无法调试WebGL。
调试经验
- 把值作为颜色输出,然后用取色器。
- 不能使用cout等输出。
4 The Rendering Equation 渲染方程
4.1 概览
L
o
(
p
,
ω
o
)
=
L
e
(
p
,
ω
o
)
+
∫
H
2
L
i
(
p
,
ω
i
)
f
r
(
p
,
ω
i
,
ω
o
)
cos
θ
i
d
ω
i
L_o(p,\omega_o)=L_e(p,\omega_o)+\int_{H^2}L_i(p,\omega_i)f_r(p,\omega_i,\omega_o)\cos\theta_i d\omega_i
Lo(p,ωo)=Le(p,ωo)+∫H2Li(p,ωi)fr(p,ωi,ωo)cosθidωi
理解:outgoing radiance(输出的radiance) = emission(自己发出的这个单位面积,这个方向的辐射) + incident radiance*brdf在立体角上的积分。
在实时渲染(real-time rendering RTR)中
- 可见性通常被显式考虑。
- BRDF通常和cosine项一起考虑。
所以渲染方程可以进一步表示为
4.2 Environment Lighting 环境光照
环境光照简单理解为任何一个方向过来的光照强度为多少,它代表来自各个方向的入射光。
- 通常表示为 cube map 或 sphere map(可以按照 θ ϕ \theta \phi θϕ展开) (texture)。
- 还有一种新的环境光表示(八面体,以后介绍)。
4.3 实时渲染中存在的问题
- 一个fragment的运算很复杂(把来自各个方向的光都加在一起)。
- 全局光照 = 直接光照 + 间接光照,间接光照难以计算(一般讨论的实时光照,只考虑弹射一次的间接光照)。