# 第八课:基本着色
# 第八课:基础光照模型
在第八课中,我们将学习光照模型的基础知识。包括:
- 物体离光源越近会越亮
- 直视反射光时会有高亮(镜面反射)
- 当光没有直接照射物体时,物体会更暗(漫反射)
- 用环境光简化计算
不包括:
- 阴影。这是个宽阔的主题,大到需要专题教程了。
- 类镜面反射(包括水)
- 任何复杂的光与物质的相互作用,像次表面散射(比如蜡)
- 各向异性材料(比如拉丝的金属)
- 追求真实感的,基于物理的光照模型
- 环境光遮蔽(在洞穴里会更黑)
- 颜色溢出(一块红色的地毯会映得白色天花板带红色)
- 透明度
- 任何种类的全局光照(它包括了上面的所有)
总而言之:只讲基础。
## 法向
过去的几个教程中我们一直在处理法向,但是并不知道法向到底是什么。
### 三角形法向
一个平面的法向是一个长度为1并且垂直于这个平面的向量。
一个三角形的法向是一个长度为1并且垂直于这个三角形的向量。通过简单地将三角形两条边进行叉乘计算(向量a和b的叉乘结果是一个同时垂直于a和b的向量,记得?),然后归一化:使长度为1。伪代码如下:
```
<pre class="calibre16">```
triangle <span class="token1">(</span> v1<span class="token1">,</span> v2<span class="token1">,</span> v3 <span class="token1">)</span>
edge1 <span class="token">=</span> v2<span class="token">-</span>v1
edge2 <span class="token">=</span> v3<span class="token">-</span>v1
triangle<span class="token1">.</span>normal <span class="token">=</span> <span class="token3">cross</span><span class="token1">(</span>edge1<span class="token1">,</span> edge2<span class="token1">)</span><span class="token1">.</span><span class="token3">normalize</span><span class="token1">(</span><span class="token1">)</span>
```
```
不要将法向(normal)和normalize()函数混淆。Normalize()函数是让一个向量(任意向量,不一定必须是normal)除以其长度,从而使新长度为1。法向(normal)则是某一类向量的名字。
### 顶点法向
引申开来:顶点的法向,是包含该顶点的所有三角形的法向的均值。这很方便——因为在顶点着色器中,我们处理顶点,而不是三角形;所以在顶点处有信息是很好的。并且在OpenGL中,我们没有任何办法获得三角形信息。伪代码如下:
```
<pre class="calibre16">```
vertex v1<span class="token1">,</span> v2<span class="token1">,</span> v3<span class="token1">,</span> <span class="token1">.</span><span class="token1">.</span><span class="token1">.</span><span class="token1">.</span>
triangle tr1<span class="token1">,</span> tr2<span class="token1">,</span> tr3 <span class="token2">// all share vertex v1</span>
v1<span class="token1">.</span>normal <span class="token">=</span> <span class="token3">normalize</span><span class="token1">(</span> tr1<span class="token1">.</span>normal <span class="token">+</span> tr2<span class="token1">.</span>normal <span class="token">+</span> tr3<span class="token1">.</span>normal <span class="token1">)</span>
```
```
### 在OpenGL中使用顶点法向
在OpenGL中使用法向很简单。法向是顶点的属性,就像位置,颜色,UV坐标等一样;按处理其他属性的方式处理即可。第七课的loadOBJ函数已经将它们从OBJ文件中读出来了。
```
<pre class="calibre16">```
GLuint normalbuffer<span class="token1">;</span>
<span class="token3">glGenBuffers</span><span class="token1">(</span><span class="token6">1</span><span class="token1">,</span> <span class="token">&</span>normalbuffer<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glBindBuffer</span><span class="token1">(</span>GL_ARRAY_BUFFER<span class="token1">,</span> normalbuffer<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glBufferData</span><span class="token1">(</span>GL_ARRAY_BUFFER<span class="token1">,</span> normals<span class="token1">.</span><span class="token3">size</span><span class="token1">(</span><span class="token1">)</span> <span class="token">*</span> <span class="token3">sizeof</span><span class="token1">(</span>glm<span class="token1">:</span><span class="token1">:</span>vec3<span class="token1">)</span><span class="token1">,</span> <span class="token">&</span>normals<span class="token1">[</span><span class="token6">0</span><span class="token1">]</span><span class="token1">,</span> GL_STATIC_DRAW<span class="token1">)</span><span class="token1">;</span>
```
```
和
```
<pre class="calibre16">```
<span class="token2">// 3rd attribute buffer : normals</span>
<span class="token3">glEnableVertexAttribArray</span><span class="token1">(</span><span class="token6">2</span><span class="token1">)</span><span class="token1">;</span>
<span class="token3">glBindBuffer</span><span class="token1">(</span>GL_ARRAY_BUFFER<span class="token1">,</span> normalbuffer<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glVertexAttribPointer</span><span class="token1">(</span>
<span class="token6">2</span><span class="token1">,</span> <span class="token2">// attribute</span>
<span class="token6">3</span><span class="token1">,</span> <span class="token2">// size</span>
GL_FLOAT<span class="token1">,</span> <span class="token2">// type</span>
GL_FALSE<span class="token1">,</span> <span class="token2">// normalized?</span>
<span class="token6">0</span><span class="token1">,</span> <span class="token2">// stride</span>
<span class="token1">(</span>void<span class="token">*</span><span class="token1">)</span><span class="token6">0</span> <span class="token2">// array buffer offset</span>
<span class="token1">)</span><span class="token1">;</span>
```
```
有这些准备就可以开始了。
## 漫反射部分
### 表面法向的重要性
当光源照射一个物体,其中重要的一部分光向各个方向反射。这就是“漫反射分量”。(我们不久将会看到光的其他部分去哪里了)
![](https://box.kancloud.cn/2015-11-02_5636f30535e9f.png)
当一定量的光线到达某表面,该表面根据光到达时的角度而不同程度地被照亮。
如果光线垂直于表面,它会聚在一小片表面上。如果它以一个倾斜角到达表面,相同的强度光照亮更大一片表面:
![](https://box.kancloud.cn/2015-11-02_5636f305417cc.png)
这意味着在斜射下,表面的点会较黑(但是记住,更多的点会被照射到,总光强度仍然是一样的)
也就是说,当计算像素的颜色时,入射光和表面法向的夹角很重要。因此有:
```
<pre class="calibre16">```
<span class="token2">// Cosine of the angle between the normal and the light direction,</span>
<span class="token2">// clamped above 0</span>
<span class="token2">// - light is at the vertical of the triangle -> 1</span>
<span class="token2">// - light is perpendicular to the triangle -> 0</span>
float cosTheta <span class="token">=</span> <span class="token3">dot</span><span class="token1">(</span> n<span class="token1">,</span>l <span class="token1">)</span><span class="token1">;</span>
color <span class="token">=</span> LightColor <span class="token">*</span> cosTheta<span class="token1">;</span>
```
```
在这段代码中,n是表面法向,l是从表面到光源的单位向量(和光线方向相反。虽然不直观,但能简化数学计算)。
### 注意正负
求cosTheta的公式有漏洞。如果光源在三角形后面,n和l方向相反,那么n.l是负值。这意味着colour=一个负数,没有意义。因此这种情况须用clamp()将cosTheta赋值为0:
```
<pre class="calibre16">```
<span class="token2">// Cosine of the angle between the normal and the light direction,</span>
<span class="token2">// clamped above 0</span>
<span class="token2">// - light is at the vertical of the triangle -> 1</span>
<span class="token2">// - light is perpendicular to the triangle -> 0</span>
<span class="token2">// - light is behind the triangle -> 0</span>
float cosTheta <span class="token">=</span> <span class="token3">clamp</span><span class="token1">(</span> <span class="token3">dot</span><span class="token1">(</span> n<span class="token1">,</span>l <span class="token1">)</span><span class="token1">,</span> <span class="token6">0</span><span class="token1">,</span><span class="token6">1</span> <span class="token1">)</span><span class="token1">;</span>
color <span class="token">=</span> LightColor <span class="token">*</span> cosTheta<span class="token1">;</span>
```
```
### 材质颜色
当然,输出颜色也依赖于材质颜色。在这幅图像中,白光由绿、红、蓝光组成。当光碰到红色材质时,绿光和蓝光被吸收,只有红光保留着。
![](https://box.kancloud.cn/2015-11-02_5636f305530bd.png)
我们可以通过一个简单的乘法来模拟:
```
<pre class="calibre16">```
color <span class="token">=</span> MaterialDiffuseColor <span class="token">*</span> LightColor <span class="token">*</span> cosTheta<span class="token1">;</span>
```
```
### 模拟光源
首先假设在空间中有一个点光源,它向所有方向发射光线,像蜡烛一样。
对于该光源,我们的表面收到的光通量依赖于表面到光源的距离:越远光越少。实际上,光通量与距离的平方成反比:
```
<pre class="calibre16">```
color <span class="token">=</span> MaterialDiffuseColor <span class="token">*</span> LightColor <span class="token">*</span> cosTheta <span class="token">/</span> <span class="token1">(</span>distance<span class="token">*</span>distance<span class="token1">)</span><span class="token1">;</span>
```
```
最后,需要另一个参数来控制光的强度。它可以被编码到LightColor中(将在随后的课程中讲到),但是现在暂且只一个颜色值(如白色)和一个强度(如60瓦)。
```
<pre class="calibre16">```
color <span class="token">=</span> MaterialDiffuseColor <span class="token">*</span> LightColor <span class="token">*</span> LightPower <span class="token">*</span> cosTheta <span class="token">/</span> <span class="token1">(</span>distance<span class="token">*</span>distance<span class="token1">)</span><span class="token1">;</span>
```
```
### 组合在一起
为了让这段代码运行,需要一些参数(各种颜色和强度)和更多代码。
MaterialDiffuseColor简单地从纹理中获取。
LightColor和LightPower通过GLSL的uniform变量在着色器中设置。
cosTheta由n和l决定。我们可以在任意坐标系中表示它们,因为都是一样的。这里选相机坐标系,是因为它计算光源位置简单:
```
<pre class="calibre16">```
<span class="token2">// Normal of the computed fragment, in camera space</span>
vec3 n <span class="token">=</span> <span class="token3">normalize</span><span class="token1">(</span> Normal_cameraspace <span class="token1">)</span><span class="token1">;</span>
<span class="token2">// Direction of the light (from the fragment to the light)</span>
vec3 l <span class="token">=</span> <span class="token3">normalize</span><span class="token1">(</span> LightDirection_cameraspace <span class="token1">)</span><span class="token1">;</span>
```
```
Normal\_cameraspace和LightDirection\_cameraspace在顶点着色器中计算,然后传给片断着色器:
```
<pre class="calibre16">```
<span class="token2">// Output position of the vertex, in clip space : MVP * position</span>
gl_Position <span class="token">=</span> MVP <span class="token">*</span> <span class="token3">vec4</span><span class="token1">(</span>vertexPosition_modelspace<span class="token1">,</span><span class="token6">1</span><span class="token1">)</span><span class="token1">;</span>
<span class="token2">// Position of the vertex, in worldspace : M * position</span>
Position_worldspace <span class="token">=</span> <span class="token1">(</span>M <span class="token">*</span> <span class="token3">vec4</span><span class="token1">(</span>vertexPosition_modelspace<span class="token1">,</span><span class="token6">1</span><span class="token1">)</span><span class="token1">)</span><span class="token1">.</span>xyz<span class="token1">;</span>
<span class="token2">// Vector that goes from the vertex to the camera, in camera space.</span>
<span class="token2">// In camera space, the camera is at the origin (0,0,0).</span>
vec3 vertexPosition_cameraspace <span class="token">=</span> <span class="token1">(</span> V <span class="token">*</span> M <span class="token">*</span> <span class="token3">vec4</span><span class="token1">(</span>vertexPosition_modelspace<span class="token1">,</span><span class="token6">1</span><span class="token1">)</span><span class="token1">)</span><span class="token1">.</span>xyz<span class="token1">;</span>
EyeDirection_cameraspace <span class="token">=</span> <span class="token3">vec3</span><span class="token1">(</span><span class="token6">0</span><span class="token1">,</span><span class="token6">0</span><span class="token1">,</span><span class="token6">0</span><span class="token1">)</span> <span class="token">-</span> vertexPosition_cameraspace<span class="token1">;</span>
<span class="token2">// Vector that goes from the vertex to the light, in camera space. M is ommited because it's identity.</span>
vec3 LightPosition_cameraspace <span class="token">=</span> <span class="token1">(</span> V <span class="token">*</span> <span class="token3">vec4</span><span class="token1">(</span>LightPosition_worldspace<span class="token1">,</span><span class="token6">1</span><span class="token1">)</span><span class="token1">)</span><span class="token1">.</span>xyz<span class="token1">;</span>
LightDirection_cameraspace <span class="token">=</span> LightPosition_cameraspace <span class="token">+</span> EyeDirection_cameraspace<span class="token1">;</span>
<span class="token2">// Normal of the the vertex, in camera space</span>
Normal_cameraspace <span class="token">=</span> <span class="token1">(</span> V <span class="token">*</span> M <span class="token">*</span> <span class="token3">vec4</span><span class="token1">(</span>vertexNormal_modelspace<span class="token1">,</span><span class="token6">0</span><span class="token1">)</span><span class="token1">)</span><span class="token1">.</span>xyz<span class="token1">;</span> <span class="token2">// Only correct if ModelMatrix does not scale the model ! Use its inverse transpose if not.</span>
```
```
这段代码看起来很牛,但它就是在第三课中学到的东西:矩阵。每个向量命名时,都嵌入了所在的空间名,这样在跟踪时更简单。 你也应该这样做。
M和V分别是模型和视图矩阵,并且是用与MVP完全相同的方式传给着色器。
### 运行时间
现在有了编写漫反射光源的一切必要条件。向前吧,刻苦努力地尝试 ![](https://box.kancloud.cn/2015-11-02_5636f3089e7d7.gif)
### 结果
只包含漫反射分量时,我们得到以下结果(再次为无趣的纹理道歉):
![](https://box.kancloud.cn/2015-11-02_5636f3056a2bc.png)
这次结果比之前好,但感觉仍少了一些东西。特别地,Suzanne的背后完全是黑色的,因为我们使用clamp()。
## 环境光分量
环境光分量是最华丽的优化。
我们期望的是Suzanne的背后有一点亮度,因为在现实生活中灯泡会照亮它背后的墙,而墙会反过来(微弱地)照亮物体的背后。
但计算它的代价大得可怕。
因此通常可以简单地做点假光源取巧。实际上,直接让三维模型发光,使它看起来不是完全黑即可。
可这样完成:
```
<pre class="calibre16">```
vec3 MaterialAmbientColor <span class="token">=</span> <span class="token3">vec3</span><span class="token1">(</span><span class="token6">0.1</span><span class="token1">,</span><span class="token6">0.1</span><span class="token1">,</span><span class="token6">0.1</span><span class="token1">)</span> <span class="token">*</span> MaterialDiffuseColor<span class="token1">;</span>
color <span class="token">=</span>
<span class="token2">// Ambient : simulates indirect lighting</span>
MaterialAmbientColor <span class="token">+</span>
<span class="token2">// Diffuse : "color" of the object</span>
MaterialDiffuseColor <span class="token">*</span> LightColor <span class="token">*</span> LightPower <span class="token">*</span> cosTheta <span class="token">/</span> <span class="token1">(</span>distance<span class="token">*</span>distance<span class="token1">)</span> <span class="token1">;</span>
```
```
来看看它的结果
### 结果
好的,效果更好些了。如果要更好的结果,可以调整(0.1, 0.1, 0.1)值。
![](https://box.kancloud.cn/2015-11-02_5636f30580083.png)
## 镜面反射分量
反射光的剩余部分就是镜面反射分量。这部分的光在表面有确定的反射方向。
![](https://box.kancloud.cn/2015-11-02_5636f30599852.png)
如图所示,它形成一种波瓣。在极端的情况下,漫反射分量可以为零,这样波瓣非常非常窄(所有的光从一个方向反射),这就是镜子。
*(的确可以调整参数值,得到镜面;但这个例子中,镜面唯一反射的只有光源,渲染结果看起来会很奇怪)*
```
<pre class="calibre16">```
<span class="token2">// Eye vector (towards the camera)</span>
vec3 E <span class="token">=</span> <span class="token3">normalize</span><span class="token1">(</span>EyeDirection_cameraspace<span class="token1">)</span><span class="token1">;</span>
<span class="token2">// Direction in which the triangle reflects the light</span>
vec3 R <span class="token">=</span> <span class="token3">reflect</span><span class="token1">(</span><span class="token">-</span>l<span class="token1">,</span>n<span class="token1">)</span><span class="token1">;</span>
<span class="token2">// Cosine of the angle between the Eye vector and the Reflect vector,</span>
<span class="token2">// clamped to 0</span>
<span class="token2">// - Looking into the reflection -> 1</span>
<span class="token2">// - Looking elsewhere -> < 1</span>
float cosAlpha <span class="token">=</span> <span class="token3">clamp</span><span class="token1">(</span> <span class="token3">dot</span><span class="token1">(</span> E<span class="token1">,</span>R <span class="token1">)</span><span class="token1">,</span> <span class="token6">0</span><span class="token1">,</span><span class="token6">1</span> <span class="token1">)</span><span class="token1">;</span>
color <span class="token">=</span>
<span class="token2">// Ambient : simulates indirect lighting</span>
MaterialAmbientColor <span class="token">+</span>
<span class="token2">// Diffuse : "color" of the object</span>
MaterialDiffuseColor <span class="token">*</span> LightColor <span class="token">*</span> LightPower <span class="token">*</span> cosTheta <span class="token">/</span> <span class="token1">(</span>distance<span class="token">*</span>distance<span class="token1">)</span> <span class="token1">;</span>
<span class="token2">// Specular : reflective highlight, like a mirror</span>
MaterialSpecularColor <span class="token">*</span> LightColor <span class="token">*</span> LightPower <span class="token">*</span> <span class="token3">pow</span><span class="token1">(</span>cosAlpha<span class="token1">,</span><span class="token6">5</span><span class="token1">)</span> <span class="token">/</span> <span class="token1">(</span>distance<span class="token">*</span>distance<span class="token1">)</span><span class="token1">;</span>
```
```
R是反射光的方向,E是视线的反方向(就像之前对“l”的假设);如果二者夹角很小,意味着视线与反射光线重合。
pow(cosAlpha,5)用来控制镜面反射的波瓣。可以增大5来获得更大的波瓣。
### 最终结果
![](https://box.kancloud.cn/2015-11-02_5636f305a6df9.png)
注意到镜面反射使鼻子和眉毛更亮。
这个光照模型因为简单,已被使用了很多年。但它有一些问题,所以被microfacet BRDF之类的基于物理的模型代替,后面将会讲到。
在下节课中,我们将学习怎么提高VBO的性能。将是第一节中级课程!