Recap of CG Basics

引言

GAMES202 高质量实时渲染是由闫令琪老师教授,本节主要复习之前讲过的基础知识,如果之前上过 GAMES101 会觉得难度并不大。

Announcement

Recap of CG Basics

Basic GPU hardware pipeline

Graphics Pipeline

渲染管线在 GAMES101 里面说得非常清楚了,渲染一开始是一个 3D 模型,最后是一个图,中间经历的过程就是渲染管线(或是渲染流水线)。这个管线的设计就是现代 GPU 在完成的工作,所以之前 GAMES101 学了一套背后的工作原理,今天我们再来回顾一下。

首先各种各样的三维物体在空间中会表示成一堆点和它的连接关系,任何的点都会经过顶点的处理变成屏幕上的那个点。然后它们的连接关系并没有改变,所以三角形还是不变,只不过投影到屏幕上去了。

在三角形投影到屏幕上之后,我们来做光栅化:把原本连续表示的一个三角形连起来,离散化成屏幕空间的一堆像素/微元,打散成像素的过程中涉及到一些关于遮挡的处理。

当我确定了这些可见的 Fragment 之后可以做着色。着色自然是计算应该长什么样,之前也提到过包括 Blinn-Phong 着色模型,也是大家要在作业0中实现的内容。 整体的图形学渲染管线是这样的。

MVP transforms

第一步是对每一个顶点规定 MVP 变换,详见 GAMES101

Sampling triangle coverage

如果我考虑把一个三角形离散成若干不同的像素,这一步发生在光栅化的部分,通过计算三角形占据哪些像素把三角形真正画在屏幕上。

还有另外一些地方,比如判断这些三角形各自的遮挡关系,哪些近哪些远,因此有了深度缓存。

着色部分我们提到过 Blinn-Phong 模型,它是根据观察不同的物体的类型对光照的反应的一个经验式的着色模型。

它的缺憾在于即使有环境光项,仍然是全局光照或间接光照的近似做法。

在着色过程中当然还可以给三角形内部或物体的任何一个地方的纹理坐标,把图片贴上去。

这就是整个硬件的渲染管线的知识。

OpenGL

首先 OpenGL 是一系列的 API,在 GPU 端执行并负责调度 CPU。所以 OpenGL 本身拿什么语言写的本身是一点关系也没有,我们更关心的是 GPU 怎么去执行而不是 CPU 怎么让 GPU 去执行。

OpenGL 当然也有不好的地方,它的版本非常碎片化,相比较下 DirectX 每个版本的特性都是非常清楚的。另一个问题是 OpenGL 写的时候作为 CPU 端的 API,它的风格是 C 风格的代码,完完全全没有面向对象一说,编程人员写起来非常不方便,甚至若干年前 OpenGL 是非常难调试的。

我们把整个渲染过程理解为画油画的过程:

  • 把物体/模特放置好
  • 画架放置好
  • 画架上放一块画布
  • 在画布上绘画
  • 如果想再画更多的画,需要把这块画布扯下来换另外一副画布

我们为什么会提到这个比喻呢?因为 OpenGL 本身就是在做这么一个事情。

A. Place objects/models

假设我需要渲染一个三维的场景,类比绘画的第一步要先把物体和模型给放在一个地方。当然,这涉及到两个问题:

  • 放什么物体?
  • 如何摆放

这个时候外面需要 Vertex buffer object (VBO),它是 GPU 中间用于存储你的模型的一片区域,与 .obj 类似存储顶点位置、法线以及连接方式等等。

B. Set up an easel

C. Attach a canvas to the easel

D. Paint to the canvas

我们最需要关心的实际问题是自己怎么去写顶点和片段的着色器,这套流程其他的东西更多都是可封装的,也是为什么不用刻意学习 OpenGL 的语法。

一个简单的总结:OpenGL 就是告诉 GPU 需要做什么。

OpenGL Shading Language (GLSL)

Shading Languages

着色语言定义了顶点/片段的着色该如何去做,这些语言看起来都比较像 C 语言。

关于着色语言有非常久远的历史:

  • 上古时代人们在 GPU 上写汇编
  • 迫切的需求促使人们开发了最早的标准着色语言
  • 目前主流的着色语言是 DirectX 的 HLSL
Shader Setup

Shader Initialization Code (FYI)
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 ;
}
Linking Shader Program (FYI)
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 ;
}
Phong Shader in Assignment 0

Debugging Shaders

怎么去调试 Shader 呢?大家现在是非常幸运的,因为若干年前的调试需要使用 NVIDIA Nsight,它是与 Visual Studio 一起运作调试 GLSL,但它需要多个 GPU,而 HLSL 必须在软件模拟的条件下才可以调试,非常不方便。

现在新版本有很多办法做 Shader 的调试:

  • Nsight Graphics:与 Visual Studio 分开并且跨平台,只支持 NVIDIA 的 GPU
  • RenderDoc:跨平台且没有对 GPU 平台的限制

在没有调试工具的年代总结出一些经验:

  • 打印出来
  • 如何去做呢?
  • 把值当成颜色显示出来!

The Rendering Equation

The Rendering Equation

渲染方程在 GAMES101 里扮演着非常重要的角色,整个路径追踪体系就是建立在其基础上。它是一个正确的描述光线追踪传播的一个方法,我们看到任何一个点 P 的 radiance 等于这个点本身发出来的 radiance(emission)加上所有其他打到这一点的 radiance 乘以 BRDF。

简单来讲,任何一个点如果只考虑它的反射,自然而然要考虑这个点从四面八方接收的光线并且反射到观测方向的能量。

在实时渲染里(RTR)有两个区别:

  • 我们提到 BRDF 时有可能指 BRDF 有可能指 BRDF 与余弦项的总和(cosine-weighted)
  • 人们会考虑当前的 shading point 是否会遮挡,引入 visibility 的概念
Environment Lighting

实时渲染这样理解的好处在于处理环境光照:

  • 光照强度定义在一个 cube map 或 sphere map 上
  • 我们会在这门课上介绍一个新的表示方法(八面体)