Full Code of CatOnly/CrashNote for AI

master abf19e36725c cached
25 files
313.6 KB
148.5k tokens
1 requests
Download .txt
Showing preview only (462K chars total). Download the full file or copy to clipboard to get everything.
Repository: CatOnly/CrashNote
Branch: master
Commit: abf19e36725c
Files: 25
Total size: 313.6 KB

Directory structure:
gitextract_ek_1s3xw/

├── .gitignore
├── ComputerGraphics(OpenGL)/
│   ├── EXT0_GLBuffers&MultiSample.md
│   ├── EXT1_FileFormat.md
│   ├── EXT2_HardwareSupport.md
│   ├── EXT3_Platform.md
│   ├── Part0_Context&Pipeline.md
│   ├── Part1_Light&ShadowInGame.md
│   ├── Part2_PhysicalLight.md
│   ├── Part3_Texture.md
│   ├── Part4_Animation.md
│   ├── Part5_Trick.md
│   └── README.md
├── DigitalImageProcessing/
│   ├── Part0_Signals&Systems.md
│   ├── Part1_Filtering.md
│   ├── Part2_Colors.md
│   ├── Part3_PhotoShop.md
│   └── README.md
├── LinearAlgebra/
│   ├── Part0_Base.md
│   ├── Part1_Matrix.md
│   ├── Part2_Quaternion.md
│   ├── Part3_Triangles.md
│   └── README.md
├── README.md
└── UnrealEngine4/
    ├── Part0_Base.md
    └── README.md

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
.DS_Store
*.key
*/images/*.log
UnrealEngine4/Part3_AnimationSystem.md
UnrealEngine4/Part4_LandscapeSystem.md
UnrealEngine4/Part5_ParticleSystem.md


================================================
FILE: ComputerGraphics(OpenGL)/EXT0_GLBuffers&MultiSample.md
================================================
# 一、顶点信息传输

## 1. 立即模式 glBegin()/glEnd()

方式:立即绘制

优点:功能适配范围广,写法直观
缺点:频繁调用 OpenGL 函数,效率低,共享点使用次数多

例子:

1. 直接提交 OpenGL 命令到 GPU

```c
// Note that not all of OpenGL commands can be placed in between glBegin() and glEnd()
// Only a subset of commands can be used
// glVertex*(), glColor*(), glNormal*(), glTexCoord*(), glMaterial*(), glCallList(), etc.
glBegin(GL_TRIANGLES);
    glColor3f(1, 0, 0); // set vertex color to red
    glVertex3fv(v1);    // draw a triangle with v1, v2, v3
    glVertex3fv(v2);
    glVertex3fv(v3);
glEnd();
```

2. 将命令放到 DisplayList 后,批量一次传入到 GPU
   DisplayList 会将其中命令的所有资源存储到自己的内存中
   DisplayList 是服务端的状态,本身存储在 GPU 缓存中,只能存储与服务端有关的部分命令
   DisplayList 的命令和数据一旦上传便不可修改

```c
// create one display list
GLuint index = glGenLists(1);

// compile the display list, store a triangle in it
// Option: GL_COMPILE or GL_COMPILE_AND_EXECUTE(render)
glNewList(index, GL_COMPILE);
    glBegin(GL_TRIANGLES);
    glVertex3fv(v0);
    glVertex3fv(v1);
    glVertex3fv(v2);
    glEnd();
glEndList();
...

// draw the display list
glCallList(index);
...

// delete it if it is not used any more
glDeleteLists(index, 1);
```



## 2. VertexArray

方式:批量数据传入绘制

优点:数据以数组的形式**存储在应用缓存**,减少了 OpenGL 函数的频繁调用
缺点:每次绘制都要占用带宽上传到显存

例子:

```c
GLfloat vertices[] = {...}; // 36 of vertex coords

glUseProgram(progId);

// activate and specify pointer to vertex array
// 因为 vertices 存储在应用程序上,所以这里 enable client state
glEnableClientState(GL_VERTEX_ARRAY);
// 也可以用 
// glNormalPointer、glColorPointer、glIndexPointer、glTexCoordPointer、glEdgeFlagPointer
glVertexPointer(3, GL_FLOAT, 0, vertices);

// draw a cube
// 也可以用 glDrawElements、glDrawRangeElements
glDrawArrays(GL_TRIANGLES, 0, 36);

// deactivate vertex arrays after drawing
glDisableClientState(GL_VERTEX_ARRAY);
```



## 3. VertexBuffer

方式:批量数据传入绘制

优点:
1. 数据以数组的形式**存储在显卡高速缓存**,每次使用时不用重新上传,只需要在显卡绑定即可
2. 数据由于存储在显存,可以被应用程序在不同线程访问和修改

例子:

1. 创建和销毁

```c
GLuint vboId;                              // ID of VBO
GLfloat* vertices = new GLfloat[vCount*3]; // create vertex array

// generate a new VBO and get the associated ID
glGenBuffers(1, &vboId);

// bind VBO in order to use
// Option: GL_ARRAY_BUFFER or GL_ELEMENT_ARRAY_BUFFER
// This Option assists VBO to decide the most efficient locations of buffer objects
// For example, some systems may prefer indices in AGP or system memory, and vertices in video memory
// Once glBindBuffer() is first called, VBO initializes the buffer with a zero-sized memory buffer and set the initial VBO states, such as usage and access properties.
glBindBuffer(GL_ARRAY_BUFFER, vboId);

// upload data to VBO
// Option: glBufferSubData, GL_[STATIC/DYNAMIC/STREAM]_[DRAW/READ/COPY]
// GL_STATIC_DRAW 决定了数据的存储位置
// Static: 更新一次,使用多次
// Dynamic: 不断更新,使用多次
// Stream: 更新一次,最多使用几次
// Draw: application upload to GPU
// Read: GPU copy to application
// Copy: Draw and Read
glBufferData(GL_ARRAY_BUFFER, dataSize, vertices, GL_STATIC_DRAW);

// it is safe to delete after copying data to VBO
delete [] vertices;

// delete VBO when program terminated
glDeleteBuffers(1, &vboId);
```

2. 过去的使用方式:不同的 API  开启/关闭 不同的顶点属性
```c
glUseProgram(progId);

// bind VBOs for vertex array and index array
glBindBuffer(GL_ARRAY_BUFFER, vboId1);            // for vertex attributes
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboId2);    // for indices

glEnableClientState(GL_VERTEX_ARRAY);             // activate vertex position array
glEnableClientState(GL_NORMAL_ARRAY);             // activate vertex normal array
glEnableClientState(GL_TEXTURE_COORD_ARRAY);      // activate texture coord array

// do same as vertex array except pointer
glVertexPointer(3, GL_FLOAT, stride, offset1);    // last param is offset, not ptr
glNormalPointer(GL_FLOAT, stride, offset2);
glTexCoordPointer(2, GL_FLOAT, stride, offset3);

// draw 6 faces using offset of index array
glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_BYTE, 0);

glDisableClientState(GL_VERTEX_ARRAY);            // deactivate vertex position array
glDisableClientState(GL_NORMAL_ARRAY);            // deactivate vertex normal array
glDisableClientState(GL_TEXTURE_COORD_ARRAY);     // deactivate vertex tex coord array

// bind with 0, so, switch back to normal pointer operation
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
```

3. OpenGL 2.0 + 使用方式:同一个 API 开启/关闭 不同的顶点属性
```c
glUseProgram(progId);

// bind VBOs for vertex array and index array
glBindBuffer(GL_ARRAY_BUFFER, vboId1);            // for vertex coordinates
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboId2);    // for indices

glEnableVertexAttribArray(attribVertex);          // activate vertex position array
glEnableVertexAttribArray(attribNormal);          // activate vertex normal array
glEnableVertexAttribArray(attribTexCoord);        // activate texture coords array

// set vertex arrays with generic API
glVertexAttribPointer(attribVertex, 3, GL_FLOAT, false, stride, offset1);
glVertexAttribPointer(attribNormal, 3, GL_FLOAT, false, stride, offset2);
glVertexAttribPointer(attribTexCoord, 2, GL_FLOAT, false, stride, offset3);

// draw 6 faces using offset of index array
glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_BYTE, 0);

glDisableVertexAttribArray(attribVertex);         // deactivate vertex position
glDisableVertexAttribArray(attribNormal);         // deactivate vertex normal
glDisableVertexAttribArray(attribTexCoord);       // deactivate texture coords

// bind with 0, so, switch back to normal pointer operation
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
```

4. 更新 VAO
```c
// 方法 1:重新向 GPU 上传数据(缺点:应用程序和 GPU 要存储 2 份数据,每次更新都要占用带宽)
glBufferData(GL_ARRAY_BUFFER, dataSize, vertices, GL_STATIC_DRAW);

// 方法 2:通过映射 GPU 上缓存数据地址到应用程序的缓存地址
//        将应用程序对地址的操作用于对 GPU 缓存操作,达到在应用程序控制 GPU 缓存的效果

// bind then map the VBO
glBindBuffer(GL_ARRAY_BUFFER, vboId);

// Option: GL_READ_ONLY, GL_WRITE_ONLY, GL_READ_WRITE
// 如果 GPU 正在使用这个 buffer,将会返回 NULL
float* ptr = (float*)glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);

// if the pointer is valid(mapped), update VBO
if(ptr)
{
    updateMyVBO(ptr, ...);          // custom function modify buffer data
    glUnmapBuffer(GL_ARRAY_BUFFER); // unmap it after use it's return GLboolean
}
```



# 二、Pixel Buffer

## 1. 创建和使用

Pixel Buffer Object 由 Vertex Buffer Object 扩展而来,因此对 Pixel Buffer 操作的许多细节和接口都与 Vertex Buffer 保持一致,这里不在赘述

例子:

```c
GLuint pboIds[2];

// Create
glGenBuffers(2, pboIds);

// Bind
// Option: GL_PIXEL_UNPACK_BUFFER / GL_PIXEL_PACK_BUFFER
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[0]);
// load data rgba
glBufferData(GL_PIXEL_UNPACK_BUFFER, 720 * 1280 * 4, NULL, GL_STREAM_DRAW);
// unbind
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
...

// Note that glMapBuffer() causes sync issue
// If GPU is working with this buffer, glMapBuffer() will wait(stall)
// until GPU to finish its job. To avoid waiting (idle), you can call
// first glBufferData() with NULL pointer before glMapBuffer()
// If you do that, the previous data in PBO will be discarded and
// glMapBuffer() returns a new allocated pointer immediately
// even if GPU is still working with the previous data
glBufferData(GL_PIXEL_UNPACK_BUFFER, 720 * 1280 * 4, NULL, GL_STREAM_DRAW);

// Mapping PBO
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[0]);
GLubyte* ptr = (GLubyte*)glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY);
if(ptr)
{
  // Custom function: update data directly on the mapped buffer
  updatePixels(ptr, DATA_SIZE);
  // release pointer to mapping buffer
  glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
}

// Delete PBO
glDeleteBuffers(1, &pboIds);
```



## 2. PBO、FBO、Texture Object

![](./images/PBO_FBO_TO.png)

**Pack(OpenGL to Application)**
**glReadPixels**

1. 从 frame buffer 中读取数据
2. 将 frame buffer 数据写入 Pixel buffer



**Unpack(Application to OpenGL)**
**glDrawPixels**

1. 从 Pixel buffer 读取数据
2. 将 Pixel buffer 数据写入 frame buffer



## 3. Direct Memory Access

将数据转换到 Pixel Buffer Object 很快是由于:转到 PBO 的数据将会直接进入显卡缓存中,不通过 CPU 的调度

例如在加载纹理时:

1. 不使用 PBO
   在 CPU 的调度下,将图片资源加载到系统缓存,然后从系统缓存拷贝到 OpenGL 的纹理对象
2. 使用 PBO
   直接加载到 OpenGL 里的 PBO 下,然后拷贝到纹理对象
   整个过程由 GPU 完成,可以和 CPU 异步执行,效率提高

![](./images/loadTexture.png)

例子:

普通加载纹理的方式

```c
// Data is from CPU memory
glBindTexture(GL_TEXTURE_2D, textureId);
glTexImage2D(GL_TEXTURE_2D, 0, GL_BGRA, width, height, 0, GL_BGRA, GL_UNSIGNED_BYTE, data);
```



通过 PBO 加载纹理(PBO 创建时已经加载纹理数据)

```c
// bind the texture and PBO
glBindTexture(GL_TEXTURE_2D, textureId);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[index]);

// copy pixels from PBO to texture object so the last param is 0 not data
// Use offset instead of pointer
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_BGRA, GL_UNSIGNED_BYTE, 0);

// it is good idea to release PBOs with ID 0 after use
// Once bound with 0, all pixel operations are back to normal ways
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
```



通过 PBO 加载/读取 Frame Buffer

```c
// set the target framebuffer to read
glReadBuffer(GL_FRONT);

// read pixels from framebuffer to PBO
// glReadPixels() should return immediately.
glBindBuffer(GL_PIXEL_PACK_BUFFER, pboIds[index]);
glReadPixels(0, 0, WIDTH, HEIGHT, GL_BGRA, GL_UNSIGNED_BYTE, 0);
```





# 三、Frame Buffer

> 定义:framebuffer 是 OpenGL 一系列数据存储的集合

**浮点帧缓冲 (Floating Point Framebuffer)**
当一个帧缓冲的颜色缓冲的内部格式被设定成了 `GL_RGB16F`, `GL_RGBA16F`, `GL_RGB32F`  或者 `GL_RGBA32F` 时,这些帧缓冲被叫做,浮点帧缓冲可以存储超过 0.0 到 1.0 范围的浮点值

当帧缓冲使用了一个标准化的定点格式(像 `GL_RGB` )为其颜色缓冲的内部格式,OpenGL 会在将这些值存入帧缓冲前自动将其约束到 0.0 到 1.0 之间



## 1. 不同种类的 framebuffer

1. **Default framebuffer**
   本地窗口系统创建和使用的 framebuffer,一定会显示到屏幕上,是本地系统创建 Context 的一部分,当 `glBindFramebuffer(GL_FRAMEBUFFER, 0);` 时,绑定的就是当前窗口系统提供的 default framebuffer
   这种 framebuffer 有本地窗口系统 API 创建提供,由 OpenGL 将其作为自己的输出给本地窗口系统来使用
   包含:多个(至少一个)色彩缓冲、一个深度缓冲、一个模板缓冲、一个累积缓冲
  2. **Frame Buffer Object**
     OpenGL 创建和使用的 framebuffer,可以不显示到屏幕上
     提供一个 FBO 对象来供 OpenGL 操作,FBO 对象可以有**多个(至少一个)色彩缓冲**,一个深度缓冲,一个模板缓冲(没有累积缓冲)



## 2. 内部数据对象

> framebuffer 提供的内部数据都以 attach 方式来赋予,并非内部创建
> 因此 framebuffer 内部数据的切换都要比单独切换 framebuffer 本身要快

切换 texture:**glFramebufferTexture2D**

1. attach 的 textureid 为 0,之前的纹理将会从 frame buffer 上解绑
2. 删除纹理时会自动从当前绑定的 framebuffer 上解绑,但如果当前纹理并不会从其他已 attach 的**非当前绑定的** framebuffer 解绑



切换 renderbuffer:**glFramebufferRenderbuffer**
1. renderbuffer 主要用来存储一些逻辑数据,而非图像数据

2. 可以通过 glGetRenderbufferParameteriv 来获取 renderbuffer 里数据的一些属性

   ```c
   int width;
   // Option: 
   // GL_RENDERBUFFER_WIDTH
   // GL_RENDERBUFFER_HEIGHT
   // GL_RENDERBUFFER_INTERNAL_FORMAT
   // GL_RENDERBUFFER_RED_SIZE
   // GL_RENDERBUFFER_GREEN_SIZE
   // GL_RENDERBUFFER_BLUE_SIZE
   // GL_RENDERBUFFER_ALPHA_SIZE
   // GL_RENDERBUFFER_DEPTH_SIZE
   // GL_RENDERBUFFER_STENCIL_SIZE
   glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &width);
   ```

   

## 3. 创建和使用

例子:

```c
GLuint fboId;

// create a framebuffer object, you need to delete them when program exits.
glGenFramebuffers(1, &fboId);
glBindFramebuffer(GL_FRAMEBUFFER, fboId);

// create a renderbuffer object to store depth info
// NOTE: A depth renderable image should be attached the FBO for depth test.
// If we don't attach a depth renderable image to the FBO, then
// the rendering output will be corrupted because of missing depth test.
// If you also need stencil test for your rendering, then you must
// attach additional image to the stencil attachement point, too.
glGenRenderbuffers(1, &rboDepthId);
glBindRenderbuffer(GL_RENDERBUFFER, rboDepthId);
// allocat memory for renderbuffer
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, TEXTURE_WIDTH, TEXTURE_HEIGHT);
//glRenderbufferStorageMultisample(GL_RENDERBUFFER, fboSampleCount, GL_DEPTH_COMPONENT, TEXTURE_WIDTH, TEXTURE_HEIGHT);
glBindRenderbuffer(GL_RENDERBUFFER, 0);

// attach a texture to FBO color attachement point
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureId, 0);
//glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureId, 0);

// attach a renderbuffer to depth attachment point
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboDepthId);

//@@ disable color buffer if you don't attach any color buffer image,
//@@ for example, rendering the depth buffer only to a texture.
//@@ Otherwise, glCheckFramebufferStatus will not be complete.
//glDrawBuffer(GL_NONE);
//glReadBuffer(GL_NONE);

// trigger mipmaps generation explicitly
// NOTE: If GL_GENERATE_MIPMAP is set to GL_TRUE, then glCopyTexSubImage2D()
// triggers mipmap generation automatically. However, the texture attached
// onto a FBO should generate mipmaps manually via glGenerateMipmap().
glBindTexture(GL_TEXTURE_2D, textureId);
glGenerateMipmap(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, 0);
```



## 4. Multi Sample Anti Aliasing

多重采样抗锯齿功能**不会自动打开**

例子

```c
// Open MSAA
glEnable(GL_MULTISAMPLE); // default is enable

// create a 4x MSAA renderbuffer object for colorbuffer
int msaa = 4;
GLuint rboColorId;
glGenRenderbuffers(1, &rboColorId);
glBindRenderbuffer(GL_RENDERBUFFER, rboColorId);
glRenderbufferStorageMultisample(GL_RENDERBUFFER, msaa, GL_RGB8, width, height);

// create a 4x MSAA renderbuffer object for depthbuffer
GLuint rboDepthId;
glGenRenderbuffers(1, &rboDepthId);
glBindRenderbuffer(GL_RENDERBUFFER, rboDepthId);
// msaa: samples count
// get max count by use glGetIntegerv(GL_MAX_SAMPLES, &max_samples_count);
glRenderbufferStorageMultisample(GL_RENDERBUFFER, msaa, GL_DEPTH_COMPONENT, width, height);

// create a 4x MSAA framebuffer object
GLuint fboMsaaId;
glGenFramebuffers(1, &fboMsaaId);
glBindFramebuffer(GL_FRAMEBUFFER, fboMsaaId);

// attach colorbuffer image to FBO
glFramebufferRenderbuffer(GL_FRAMEBUFFER,       // 1. fbo target: GL_FRAMEBUFFER
                          GL_COLOR_ATTACHMENT0, // 2. color attachment point
                          GL_RENDERBUFFER,      // 3. rbo target: GL_RENDERBUFFER
                          rboColorId);          // 4. rbo ID

// attach depthbuffer image to FBO
glFramebufferRenderbuffer(GL_FRAMEBUFFER,       // 1. fbo target: GL_FRAMEBUFFER
                          GL_DEPTH_ATTACHMENT,  // 2. depth attachment point
                          GL_RENDERBUFFER,      // 3. rbo target: GL_RENDERBUFFER
                          rboDepthId);          // 4. rbo ID
```



### 4.1 多重采样 转 单采样

**多重采样后 framebuffer 的渲染结果不能直接使用,需要转换成 single-sample image 才能使用**

转换例子

```c
// copy rendered image from MSAA (multi-sample) to normal (single-sample)
// NOTE: The multi samples at a pixel in read buffer will be converted
// to a single sample at the target pixel in draw buffer.
glBindFramebuffer(GL_READ_FRAMEBUFFER, fboMsaaId); // src FBO (multi-sample)
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fboId);     // dst FBO (single-sample)

glBlitFramebuffer(0, 0, width, height,             // src rect
                  0, 0, width, height,             // dst rect
                  GL_COLOR_BUFFER_BIT,             // buffer mask(which buffers are copied)
                  GL_LINEAR);                      // scale filter
```



### 4.2 从 shader 里获取多重采样结果

```c
// shader 里自定义多重纹理采样

// 1. 使用 sampler2DMS 而不是 sampler2D
uniform sampler2DMS screenTextureMS; 

// 2. 使用 texelFetch
vec4 colorSample = texelFetch(screenTextureMS, TexCoords, 3);  // 取第4个子样本
```





## 5. 检查 framebuffer 的状态

例子

```c
bool checkFramebufferStatus(GLuint fbo)
{
    // check FBO status
    glBindFramebuffer(GL_FRAMEBUFFER, fbo); // bind
    GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    switch(status)
    {
    case GL_FRAMEBUFFER_COMPLETE:
        std::cout << "Framebuffer complete." << std::endl;
        return true;

    case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
        std::cout << "[ERROR] Framebuffer incomplete: Attachment is NOT complete." << std::endl;
        return false;

    case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:
        std::cout << "[ERROR] Framebuffer incomplete: No image is attached to FBO." << std::endl;
        return false;
/*
    case GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS:
        std::cout << "[ERROR] Framebuffer incomplete: Attached images have different dimensions." << std::endl;
        return false;

    case GL_FRAMEBUFFER_INCOMPLETE_FORMATS:
        std::cout << "[ERROR] Framebuffer incomplete: Color attached images have different internal formats." << std::endl;
        return false;
*/
    case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER:
        std::cout << "[ERROR] Framebuffer incomplete: Draw buffer." << std::endl;
        return false;

    case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER:
        std::cout << "[ERROR] Framebuffer incomplete: Read buffer." << std::endl;
        return false;

    case GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE:
        std::cout << "[ERROR] Framebuffer incomplete: Multisample." << std::endl;
        return false;

    case GL_FRAMEBUFFER_UNSUPPORTED:
        std::cout << "[ERROR] Framebuffer incomplete: Unsupported by FBO implementation." << std::endl;
        return false;

    default:
        std::cout << "[ERROR] Framebuffer incomplete: Unknown error." << std::endl;
        return false;
    }
    glBindFramebuffer(GL_FRAMEBUFFER, 0);   // unbind
}
```





# Reference

1. [OpenGL Vertex Buffer Object (VBO)](http://www.songho.ca/opengl/gl_vbo.html)
2. [How to choose between GL_STREAM_DRAW or GL_DYNAMIC_DRAW?](https://stackoverflow.com/questions/8281653/how-to-choose-between-gl-stream-draw-or-gl-dynamic-draw)
3. [OpenGL Pixel Buffer Object](http://www.songho.ca/opengl/gl_pbo.html)
4. [OpenGL Frame Buffer Object](http://www.songho.ca/opengl/gl_fbo.html)

================================================
FILE: ComputerGraphics(OpenGL)/EXT1_FileFormat.md
================================================
# 一、图片存储格式

## 1. BMP 文件

无损的图片格式,全称 Bitmap(无压缩,体积大)
可以直接存储图片数据,也可以采用索引表的存储方式。但即便采用了索引表的存储方式,也不能使体积缩小太多。图片的内存格式

适合:logos 等有明确边界的图片,程序的缓存文件(无压缩,可直接存储的特点)

格式:ARGB,BMP 文件的第一行数据是显示器的最后一行数据

内存排列:

- 位图文件头(bitmap-file header)

- 位图信息头(bitmap-informationheader)

- 颜色表(color table)

- 颜色点阵数据(bits data)

  

## 2. GIF 文件

无损的图片压缩格式,只能采用索引表的存储方式存储,因此最多能表示 256 种颜色
GIF 的图片善于做动画,并且支持 alpha 透明通道

适合:logos 等有明确边界的简单图片



## 3. PNG 文件

Portable Network Graphics
无损的图片压缩格式,不能做动画,支持 alpha 透明通道(透明效果优于 GIF)

- PNG-8:采用索引表的方式存储图片,最多能表示 256 种颜色(压缩后体积小于 GIF)
  适合:logos 等有明确边界的简单图片
- PNG-24:采用直接存储的方式存储图片,能存储上千种颜色(24 位存储一个像素)
  适合:兼顾好的压缩和效果的照片存储



## 4. JPEG 文件

Joint Photographic Experts Group
简称 jpg,有损的图片压缩格式,压缩后体积小(虽然有损,但不易被人眼察觉,24 位存储一个像素 RGB,**不支持透明**)
适合:只考虑体积照片的压缩

JPEG 格式图片是分为一个一个的段来存储的:
段的多少和长度并不是一定的。只要包含了足够的信息,该 JPEG 文件就能够被打开,呈现给人们

段的结构:

```c++
名称  字节数 数据  说明
------------------------------------------------------
段标识   1   FF   每个新段的开始标识
段类型   1        类型编码(称作“标记码”)
段长度   2        包括段内容和段长度本身,不包括段标识和段类型
段内容            ≤65533字节
```



JPEG 图片存储的段文件按顺序依次如下:

1. **SOI(文件头)**Start Of Image
   段标识:FF(标志新段的开始)
   段类型:D8(SOI 的段类型为 D8,表示文件头)
2. APP0(图像识别信息)Application data marker, type 0
   段标识:FF(标志新段的开始)
   段类型:E0(APP0 的段类型为 E0,定义交换格式和图像识别信息)
3. DQT(定义量化表)
4. SOF0(图像基本信息)
5. DHT(定义 Huffman 表)
6. DRI(定义重新开始间隔)
7. SOS(扫描行开始)
8. **EOI(文件尾)**End Of Image
   段标识:FF(标志新段的开始)
   段类型:D9(EOI 的段类型为 D9 表示文件尾)



## 5. SVG 文件

矢量的图片存储方案,内部存储的不是像素,而是曲线和线条
这使得即便是看起来很大的 SVG 图,存储起来会很小(前提是画面图像足够简单)
SVG 使用 XML 语法编写,并且可以在文本工具里修改(可以使用 JavaScript 快速修改 SVG 图片的颜色)

适合:logos 或 icons 等简单且需要适配不同尺寸的网站图片




## 6. TGA 文件

游戏中常用的图像格式

### 6.1 非压缩文件格式

![](./images/file_format_tga.jpg)

### 6.2 压缩文件格式







# 二、三维文件格式

三维软件之间互相导入导出一般会涉及到一些格式不兼容的问题,不同的格式有着不同的定位及用处,有开源的也有商业的

| 格式                                     | 功能                                                         | 详情                                                         |
| ---------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| **.abc**<br />Alembic                    | 动画、粒子、模型烘焙、流体                                   | 通用格式,有效地储存, 共享动画与特效场景<br />[官网](http://www.alembic.io/)<br />[为什么 CG 行业需要 Alembic(.abc) 通用格式](http://www.bgteach.com/article/131) |
| **.glTF**<br />GL Transmission Format    | 动画、场景、相机、网格、材质、纹理、着色器、着色程序         | json 格式描述<br />较少的冗余信息<br />[官网](https://www.khronos.org/gltf/)<br />[Github](https://github.com/KhronosGroup/glTF/blob/master/README.md) |
| **.fbx**<br />FilmBoX                    | 骨骼动画、材质、网格                                         | FilmBoX 这套软件所使用的格式,后改称 Motionbuilder<br />Autodesk 家族格式,在 3D Max、Maya、Softimage 等软件间进行**模型**、材质、**动作**和摄影机信息的互导,这样就可以发挥 3D Max 和 Maya 等软件的优势 |
| **.bvh**<br />BioVision                  | 骨骼动画                                                     | 对人体运动进行捕获后产生文件格式的文件扩展名,捕捉后的文件可以重复利用,应用在不同的角色骨骼驱动上制作动画 |
| **.obj**                                 | 主要支持多边形(Polygons)模型<br />不包含动画、材质特性、贴图路径、动力学、粒子等信息 | 几乎所有知名的 3D 软件都支持 OBJ 文件的读写                  |
| **.ply**<br />Polygon File Format        | 静态多边形模型,OBJ 格式的升级版<br />颜色、透明度、表面法向量、材质座标与资料可信度 | 改进了 Obj 格式所缺少的对任意属性及群组的扩充性<br />因此PLY格式发明了 "property" 及 "element" 这两个关键词,来概括 "顶点、面、相关资讯、群组" 的概念 |
| **.dae**<br />Data Acquisition Equipment | 骨骼动画、材质、网格                                         | xml 格式描述,3D Max、Maya,通过安装插件可导出<br />相比 FBX,对 dae 格式模型的载入有非常高的自由控制,是 FBX 格式代替品 |
| **.x3d**                                 | 多纹理、多遍绘制、支持 Shader 着色、支持多渲染目标、支持几何实例 | xml 格式描述,专为万维网而设计的三维图像标记语言             |
| **.stl**                                 | 三角面静态模型<br />只能描述三维物体的几何信息,不支持颜色材质等信息 | 计算机图形学处理 CG、数字几何处理如 CAD、 数字几何工业应用, 如三维打印机支持的最常见文件格式 |
| **.dxf**<br />Drawing Exchange File      | 三角面静态模型                                               | CAD 通用格式                                                 |
| **.3ds**                                 | 三角面静态模型                                               | 比较早的一种三维格式,三角面,最早游戏模型应用比较广泛<br />由于后期导入软件的不可编辑性、难以二次编辑现在逐渐的远离了我们的视线 |



# 三、三维软件

三维软件根据工作的功能分类为:

- 主体三维软件
  指能独立完成整个三维动画创作的平台性三维软件,具备建模、材质、灯光、渲染、动画、角色等一系列创作的需求,同时允许开发者对软件进行开发第三方插件以扩充软件主体的三维软件
  
  | 软件      | 简介                               | 功能                                                         |
  | --------- | ---------------------------------- | ------------------------------------------------------------ |
  | Blender   | 免费开源的三维软件                 | 建模、材质、灯光、渲染、雕刻、角色动画、纹理绘制、插件、摄影机跟踪、扣像、合成、游戏引擎 |
  | Maya      | 售价高昂,易学易用,制作效率高     | 建模、材质、灯光、渲染、角色动画、插件                       |
  | Cinema 4D | 许多一流艺术家和电影公司的首选     | 建模、材质、灯光、渲染、雕刻、角色动画、纹理绘制、插件       |
  | Houdini   | 完全是为电影而生                   | 建模、材质、灯光、渲染、特效、角色动画                       |
  | LightWave | 生物建模和角色动画方面功能异常强大 | 建模、材质、灯光、渲染、特效、角色动画                       |
  | Softimage | 2014 年三月,发布停产声明          | 建模、材质、灯光、渲染、特效、角色动画                       |

  
  
- 协助三维软件
  指能依赖三维主体软件存在,以强大的辅助完成高质量、高效率的流程,这种软件常常也属于单功能三维软件

  | 软件         | 简介                                                     | 功能                                                      |
  | ------------ | -------------------------------------------------------- | --------------------------------------------------------- |
  | Clarisse iFX | 以图像为核心<br />减少机器开始渲染最终图像所需的交互时间 | 导入模型、材质、灯光、即时渲染、特效、合成                |
  | Twinmotion   | 可以将项目导出为 .exe 可执行文件                         | 导入模型、材质、灯光、即时渲染、动画、展示、交互、VR、360 |
  | Lumion       | 实时3D可视化工具<br />用来制作电影和静帧作品             | 导入模型、材质、灯光、即时渲染、动画、展示、360           |
  | ZBrush       | 数字雕刻和绘画软件                                       | 雕刻、纹理                                                |




- 单功能三维软件
  一般在某个模块异常强大,由于着重解决流程中的一个环节,在效率上有着得天独厚的优势。 缺点也显而易见,需要主体三维软件的导入导出

  | 软件                 | 简介                                 | 功能                                                         |
  | -------------------- | ------------------------------------ | ------------------------------------------------------------ |
  | Marmoset Toolbag     | 实时材质编辑,渲染,动画编辑预览软件 | GPU、CPU 即时渲染器                                          |
  | Silo                 | 视频游戏及电影创建角色或建筑         | 建模、UV Mapping                                             |
  | Cycles Render Engine | blender 中的一种渲染引擎             | 基于光线追踪的渲染引擎,支持交互式渲染,内置一个新的光影节点系统、新的纹理工作流程和GPU加速,用户通过切换GPU渲染可以使渲染过程变得较为便捷 |





# Reference

- [What are the different usecases of PNG vs. GIF vs. JPEG vs. SVG?](https://stackoverflow.com/questions/2336522/what-are-the-different-usecases-of-png-vs-gif-vs-jpeg-vs-svg)

- [libpng](http://www.libpng.org/pub/png/libpng.html)

- [www.w3.org/Graphics](https://www.w3.org/Graphics/JPEG/itu-t81.pdf)

- [JPEG 解码器](https://zhuanlan.zhihu.com/p/27296876)

- [使用 libjpeg 进行图片压缩](https://zhuanlan.zhihu.com/p/126728039)

- [影像算法解析——JPEG 压缩算法](https://zhuanlan.zhihu.com/p/40356456)

- [TGA 文件格式解析](http://www.twinklingstar.cn/2013/471/tga-file-format/)

- [三维文件格式知多少 ](http://www.bgteach.com/article/132)

- [三维软件知多少](http://www.bgteach.com/article/40)

- [图片文件格式知多少 | jpeg、png、pdf、tga、tif、svg、esp、exr、hdr...](https://www.bgteach.com/article/133)

- [游戏制作行业为什么使用TGA格式的贴图而不使用PNG格式? - 韦易笑的回答 - 知乎 ](https://www.zhihu.com/question/340196227/answer/789538293)

  



================================================
FILE: ComputerGraphics(OpenGL)/EXT2_HardwareSupport.md
================================================
# 一、 GPU 的硬件架构

GPU 主要由 **显存 Device Memory** 和 **流多处理器 Stream Multiprocessors ** 组成(Stream Processors,SP 是 SM 中的一个 Core)



## 1. CPU vs GPU

相对于全面功能考虑的 CPU,GPU 有更多的 ALU(Arithmetic Logic Unit,逻辑运算单元),更少的逻辑控制单元和寄存器
GPU 的并行运算:与 CPU 上十几个线程的并行计算不同,GPU 的线程数可以达到上百万或更多

![](images/GPU_VS_CPU.jpg)

**缓存行 Cache-line**:缓存存储数据的最小单位

CPU 主要采用**三层**缓存(当今主流的 CPU 架构)

1. L1、L2 硬件缓存成为本地核心内缓存,即一个核一个
   如果是 4 核,那就是有 4 个 L1+4 个 L2
2. L3 硬件缓存是所有核共享的。即不管你的 CPU 是几核,这个 CPU 中只有一个 L3
3. L1 硬件缓存的大小是 64K,即 32K 指令缓存 +32K 数据缓存,L2 是 256K,L3 是 2M
   这不是绝对的,目前 Intel CPU 基本是这样的设计



GPU 主要采用**二层**缓存(缓存容量小,Cache 命中率低,延时较高)

1. L1 硬件缓存,速度最快,存储共享内存
   每个 SM 独立包含,一部分在 SM 内共用,一部分在每个 SP 内单独使用(用来交换共享内存)
2. L2 硬件缓存,速度低于 L1,存储只读常量、纹理
   多个 SM 共享使用
3. GPU 的全局内存最大为 12GB
   GPU 内存**不支持并发读取和并发写入**

![](./images/GPU.jpg)



## 2. Stream Multiprocessor

GPU 在 shader 中进行的向量运算采用 SIMD 或 MIMD 计算方式

- **SISD**(Single Instruction Single Data Stream,单指令单数据流):传统顺序执行计算机使用
- **MIMD**(Multiple Instruction Stream Multiple Data Stream,多指令多数据流):现代大多数计算机使用,使用多个控制器来异步地控制多个处理器,从而实现空间上的并行性。从硬件角度看,MIMD 需要消耗大量的晶体管数
- **SIMD**(Single Instruction Stream Multiple Data Stream,单指令多数据流):GPU 内置计算方式,CPU 里也有相应的实现方法
- **MISD**(Multiple Instruction Stream Single Data Stream,多指令单数据流)



早期的 GPU,顶点着色器和像素着色器的硬件结构是独立的(顶点和像素线程资源不能共享)
使用统一着色器架构 **Unified shader Architecture**,VS、PS、GS、CS 都可以使用相同的硬件资源(线程共享)Stream Multiprocessor

<p>
    Stream Multiprocessor,SM 为 Shader 执行的地方,如右图一个多流处理器包含
    <img src='./images/GPU_Stream_processor.png' style='float:right;'/>
</p>

1. **多边形引擎 PolyMorph Engine**:负责 attribute Setup、VertexFetch、曲面细分、栅格化
2. **多 ALU 逻辑运算 Core**(每个核代表一个线程,由 Warp 统一管理一组线程)
   Warp 中的多个线程是**单指令多线程**(相同的逻辑,不同的数据)
   当执行 if-else 语句、 for 循环次数 n 不是常量,或被 break 提前终止了但其他批次循环的内容还在执行时
   一个 Warp 线程组只会有部分满足逻辑条件的线程在执行,其他线程当前执行阶段会什么都不执行(被遮掩,但是仍然活跃)
   这相当于浪费了一部分线程资源,导致像 if-else 这样的语句是 false 的情况也占用了线程资源(占用了,但不使用)
3. **指令缓存 Instruction Cache**
4. **LD/ST(load/store)**:加载和存储数据
5. **SFU(Special function units)**:执行特殊数学运算(sin、cos、log 等)
6. **寄存器** 128KB
7. **L1 Cache**
   配合共享的 L2 Cache 做到 vertex-shader 和 pixel-shader 的数据通信
8. **Uniform Buffer** 的**部分**缓存
9. **Texture Cache 和 纹理读取单元**



## 3. GPU 处理 Shader

**Shader 处理流程**

1. CPU 端编译 Shader 源码为二进制(现代 GPU Shader 为二进制)
2. CPU 端将 shader 二进制指令经由 PCI-e 推送到 GPU 端
3. GPU 在执行代码时,会用 Context 将指令分成若干 Channel 推送到各个 SM 的存储空间



**Shader 的流水线多线程式处理**

多个 SP 里的多个运算单元同时运行同一个 Shader,但每个运算单元支撑的 1 个线程处理的数据又各不相同

![](./images/GPU_Shader_process.png)





# 二、CPU、GPU、显示器

## 1. <span id="gpu">CPU-GPU 异构系统</span>

![](./images/GPU_CPU_architecture.png)

**分离式**结构(左图)

- 结构:CPU 和 GPU 拥有各自的存储系统,两者通过 PCI-e 总线进行连接,可以共享一套虚拟地址空间,必要时会进行内存拷贝

- 缺点:PCI-e 相对于两者具有低带宽和高延迟,数据的**传输带宽**成了其中的性能瓶颈
- 应用的硬件设备:**PC**(CPU、GPU 存储**各自有存储系统**)、**移动端**(CPU、GPU **继承到了一个芯片,且共享物理内存**)
  很多 SoC (比如:移动端)都是集成了CPU 和 GPU,事实上这仅仅是在物理上进行了集成,并不意味着它们使用的(运行时)就是耦合式结构



**耦合式**结构(右图)

- 结构:CPU 和 GPU 集成到了一个芯片,GPU 没有独立的内存,与 GPU 共享系统内存,由 MMU 进行存储管理
- 应用的硬件设备:PS4



## 2. CPU-GPU Workflow

下图所示为 CPU-GPU 异构系统的工作流,当 CPU 遇到图像处理的需求时,会调用 GPU 进行处理,主要流程可以分为以下四步:

1. 将主存的处理数据 CPU 复制到 显存 GPU 中(可以通过 [DMA](./EXT0_GLBuffers&MultiSample.md) 跳过此步骤)
2. CPU 指令驱动 GPU
3. GPU 中的每个运算单元并行处理
4. GPU 将显存结果传回主存

![](./images/cuda_processing_flow.png)



## 3. CPU-GPU Data transmission

![](./images/GPU_Management_model.png)

1. **MMIO(Memory-Mapped I/O)**
   CPU 通过 MMIO 访问 GPU 的寄存器状态
   通过 MMIO 传送数据块传输命令,支持 DMA 的硬件可以实现块数据传输

2. **GPU Context**
   上下文表示 GPU 的计算状态,在 GPU 中占据部分虚拟地址空间
   多个活跃态下的上下文可以在 GPU 中并存(一个 Context 对应一个 SP)

3. **CPU Channel**
   来自 CPU 操作 GPU 的命令存储在内存中,并提交至 GPU channel 硬件单元
   每个 GPU 上下文可拥有多个 GPU Channel
   每个 GPU 上下文都包含 GPU channel 描述符(GPU 内存中的内存对象)
   每个 GPU Channel 描述符存储了channel 的配置,如:其所在的页表
   每个 GPU Channel 都有一个专用的命令缓冲区,该缓冲区分配在 GPU 内存中,通过 MMIO 对 CPU 可见

4. **GPU 页表**
   GPU 上下文使用 GPU 页表进行分配,该表将**虚拟地址**空间与**其他地址空间**隔离开来
   GPU 页表与 CPU 页表分离,其驻留在 GPU 内存中,物理地址位于 GPU 通道描述符中
   通过 GPU channel 提交的所有命令和程序都在对应的 GPU 虚拟地址空间中执行
   GPU 页表将 GPU 虚拟地址不仅转换为 GPU 设备物理地址,还转换为主机物理地址
   这使得 GPU 页面表能够将 GPU 存储器和主存储器统一到统一的 GPU 虚拟地址空间中,从而构成一个完成的虚拟地址空间

5. **PFIFO Engine**
   一个提交 GPU 命令的特殊引擎
   维护多个独立的命令队列,即 channel(带有 put 和 get 指针的环形缓冲器)
   会拦截多有对通道控制区域的访问以供执行
   GPU 驱动使用一个通道描述符来存储关联通道的设置

6. **Buffer Object** 缓冲对象
   一块内存,可以用来存储纹理,渲染对象,着色器代码等等



## 4. 显示器的显示

从 CPU 通过 GPU 到显示的流程如下:

1. CPU 计算好显示内容提交至 GPU
2. GPU 渲染完成后将渲染结果存入帧缓冲区
3. 视频控制器会按照 `VSync` 信号逐帧读取帧缓冲区的数据
4. 帧缓冲区数据 经过转换后 由显示器进行显示

![](./images/ios-renderIng-gpu-internal-structure.png)

**显示器显示工作流程**

1. 显示器逐行刷新,每一行刷新完后会发出一个水平同步信号(horizonal synchronization),简称 **HSync**
2. 绘制下一行
3. 显示器一帧画面绘制完成后会发出一个垂直同步信号(vertical synchronization),简称 **VSync**



**双缓冲机制**

- 使用场景:防止在快速运动场景下,由于**显卡运算速率 > 显示器运算速率**导致快速运动的动作割裂情况(画面撕裂)
- 方法:使用两个帧缓冲,一个负责 GPU 写入,一个负责 显示器 读取,用垂直同步确保写入完成后将写入和读取帧缓冲互换角色
- 缺点:开启垂直同步,画面会有延迟(无法达到显卡的最大运算速率),但并没有卡顿
  `显卡绘制一帧时间 > 显示器刷新一帧时间 ? 显示器刷新(显卡等待) : 显示器显示上一帧,等待显卡绘制完成(屏幕卡顿);`
- 解决方法:用**三重缓冲**代替垂直同步
  三重缓冲:在双缓冲的基础上加了一个缓冲
  在等待垂直同步时,来回交替渲染两个离屏的缓冲区
  垂直同步发生时,屏幕缓冲区和最近渲染完成的离屏缓冲区交换,实现充分利用硬件性能的目的

![](./images/ios-vsync-off.jpg)





# 三、测试 GPU 硬件信息

## 1. NV Shader 扩展功能

[NV shader thread group](https://www.opengl.org/registry/specs/NV/shader_thread_group.txt) 提供了 OpenGL 的扩展,可以查询 GPU 线程、Core、SM、Warp 等硬件相关的属性(需要支持 GLVersion 4.3+ 的硬件)

```c
// 开启扩展
#extension GL_NV_shader_thread_group : require     (or enable)

WARP_SIZE_NV	// 单个线程束的线程数量
WARPS_PER_SM_NV	// 单个SM的线程束数量
SM_COUNT_NV		// SM数量

uniform uint  gl_WarpSizeNV;	// 单个线程束的线程数量
uniform uint  gl_WarpsPerSMNV;	// 单个SM的线程束数量
uniform uint  gl_SMCountNV;		// SM数量

in uint  gl_WarpIDNV;		// 当前线程束id
in uint  gl_SMIDNV;			// 当前线程束所在的SM id,取值[0, gl_SMCountNV-1]
in uint  gl_ThreadInWarpNV;	// 当前线程id,取值[0, gl_WarpSizeNV-1]

in uint  gl_ThreadEqMaskNV;	// 是否等于当前线程id的位域掩码。
in uint  gl_ThreadGeMaskNV;	// 是否大于等于当前线程id的位域掩码。
in uint  gl_ThreadGtMaskNV;	// 是否大于当前线程id的位域掩码。
in uint  gl_ThreadLeMaskNV;	// 是否小于等于当前线程id的位域掩码。
in uint  gl_ThreadLtMaskNV;	// 是否小于当前线程id的位域掩码。

in bool  gl_HelperThreadNV;	// 当前线程是否协助型线程(Draw quad 时,不在当前图元上的像素所在的线程)
/**
   The variable gl_HelperThreadNV specifies if the current thread is a helper thread. In implementations supporting this extension, fragment shader invocations may be arranged in SIMD thread groups of 2x2 fragments called "quad".  When a fragment shader instruction is executed on a quad, it's possible that some fragments within the quad will execute the instruction even if they are not covered by the primitive.  Those threads are called helper threads.  Their outputs will be discarded and they will not execute global store functions, but the intermediate values they compute can still be used by thread group sharing functions or by fragment derivative functions like dFdx and dFdy.
*/
```



## 2. Sample Code

```C
// VS
#version 430 core
layout (location = 0) in vec3 aPos;

void main() {
	gl_Position = vec4(aPos, 1.0f);
}

// FS
#version 430 core
#extension GL_NV_shader_thread_group : require

uniform uint  gl_WarpSizeNV;	// 单个线程束的线程数量
uniform uint  gl_WarpsPerSMNV;	// 单个SM的线程束数量
uniform uint  gl_SMCountNV;		// SM数量

in uint  gl_WarpIDNV;		// 当前线程束id
in uint  gl_SMIDNV;			// 当前线程所在的SM id,取值[0, gl_SMCountNV-1]
in uint  gl_ThreadInWarpNV;	// 当前线程id,取值[0, gl_WarpSizeNV-1]

out vec4 FragColor;

void main() {
	// SM id
	float lightness0 = gl_SMIDNV / gl_SMCountNV;
    // warp id
    float lightness1 = gl_WarpIDNV / gl_WarpsPerSMNV;
    // thread id
	float lightness2 = gl_ThreadInWarpNV / gl_WarpSizeNV;
    
	FragColor = vec4(lightness0);
}
```



**SM id 的结果 lightness0 分析:**

1. 通过计算画面色阶总数得出 SM 的总数
2. 通过单个三角面内重复的像素块个数推算每个 SM 内的核心数
3. 不同三角形的接缝处出现断层,说明同一个像素块如果分属不同的三角形,就会分配到不同的 SM 进行处理
   由此推断,**相同面积的区域,如果所属的三角形越多,就会导致分配给 SM 的次数越多,消耗的渲染性能也越多**

![](./images/GPU_Test_SM.png)





# 四、GPU 硬件渲染模式

## 1. IMR

 **Immediate Mode Rendering 立即渲染模式**:PC 端

![](./images/immediate-demo.gif)

- 优点:顶点着色器和其它几何体相关着色器的输出数据能存储在 GPU 上(FIFO 缓冲区),直到管道中的下一阶段准备使用数据

- 缺点:由于根据图元划分绘制批次,GPU 对整个帧缓冲进行随机访问,帧缓冲只能存储在外部 DRAM 上,导致高分辨率时内存**带宽负载高**

- 优化:降带宽,将最近访问的帧缓冲存储排布在靠近 GPU 的位置来提高内存命中率
  
- 方法:**优先根据图元来划分绘制批次**
  
  ![](images/immediate_mode.svg)
  
  ```python
  # 每个顶点几何图形画完后直接做像素颜色,此时像素颜色不确定,需要多次读写 framebuffer(深度值的不同)
  for draw in renderPass:
      for primitive in draw:
          for vertex in primitive:
              execute_vertex_shader(vertex)
              
          if primitive not culled:
              for fragment in primitive:
                  execute_fragment_shader(fragment)
  ```
  



## 2. TB[D]R

**Tile Based [Deferred] Rendering 基于切片的[延迟]渲染**:移动端

![](./images/tile-based-demo.gif)

- 优点:
  可以将整个颜色、深度和模板的工作集存储在快速的 On-chip RAM 上(GPU 直连,不用带宽) 
  深度测试和混合透明像素所需帧缓冲数据也存储在 GPU 内部,通过提高缓存命中来降低了带宽消耗

- 缺点:
  GPU 必须将每个顶点的变化数据和 Tile 的中间状态存储到主内存中,着色阶段随后读取这些数据
  通过**延迟一帧**的方式降低了带宽

- 方法:**优先根据帧缓冲来划分绘制批次**,将帧缓冲切分为几个固定大小的 Tile,分别渲染每个 Tile 上的像素
  
  ![](images/tiled_mode.svg)
  
  ```python
  # Pass 1. 将所有几何图元属性处理后,逐个划分到对应的 Tile 中
  for draw in renderPass:
      for primitive in draw:
          for vertex in primitive:
              execute_vertex_shader(vertex)
          if primitive not culled:
              append_tile_list(primitive)
  
  # Pass 2. 根据当前 Tile 的图元属性绘制当前 Tile 所包含的所有像素
  for tile in renderPass:
      for primitive in tile:
          for fragment in primitive:
              execute_fragment_shader(fragment)
  ```
  





# 五、GPU 硬件新特性

## 1. Pixel Local Storage

支持平台:OpenGL ES、Metal、Vulkan、D3D

**Pixel Local Storage(PLS)**是一种数据存取方式,用 PLS 声明的数据将保存在 GPU 的 Tile buffer 上
应用:**延迟着色**所需的 **GBuffer** 数据一直处于 PLS 之中,最好解析后返回最终颜色,而**不需要将 GBuffer 写回系统显存**

```glsl
// 1. 光照累积
__pixel_localEXT FragData // 可读写数据
{
    layout(rgba8) highp vec4 Color;
    layout(rg16f) highp vec2 NormalXY;
    layout(rg16f) highp vec2 NormalZ_LightingB;
    layout(rg16f) highp vec2 LightingRG;
} gbuf;

void main() {
    vec3 Lighting = CalcLighting(gbuf.NormalXY, gbuf.NormalZ_LightingB.x);
    gbuf.LightingRG += Lighting.xy;
    gbuf.NormalZ_LightingB.y += Lighting.z;
}

// 2. 最终着色
// __pixel_local_outEXT 只读数据
__pixel_local_inEXT FragData // 只读数据
{
    layout(rgba8) highp vec4 Color;
    layout(rg16f) highp vec2 NormalXY;
    layout(rg16f) highp vec2 NormalZ_LightingB;
    layout(rg16f) highp vec2 LightingRG;
} gbuf;

out highp vec4 FragColor;

void main() {
    FragColor = resolve(gbuf.Color, gbuf.LightingRG, gbuf.NormalZ_LightingB.y);
}
```



## 2. Subpass

支持平台:Metal、Vulkan、D3D

SubPass 与 Pixel Local Storage 类似也是存储数据到 GPU 的 Tile buffer 上,借鉴了 TBDR 的延迟一帧的 1 Pass 拆成 图元处理 Pass 和 着色 Pass 这样两个 Subpass 的想法

![](./images/subpass.png)

限制:

- 所有 Subpass 必须在同一个 Render Pass 中(不能是上一个)
- 无法在超过 GPU Tile buffer 范围外进行采样

```c
// 读
// LOAD_OP_LOAD:从全局内存加载 Attachment 到 Tile
// LOAD_OP_CLEAR:清理Tile缓冲区的数据。
// LOAD_OP_DONT_CARE:不对 Tile 缓冲区的数据做任何操作,通常用于 Tile 内的数据会被全部重新,效率高于 LOAD_OP_CLEAR

// 写
// STORE_OP_STORE:将 Tile 内的数据存储到全局内存
// STORE_OP_DONT_CARE:不对 Tile 缓冲区的数据做任何存储操作

// 1. Attachment
VkAttachmentDescription colorAttachment = {};
colorAttachment.format = VK_FORMAT_B8G8R8A8_SRGB;
colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
// 标明 loadOp 为 DONT_CARE
colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
// 标明 storeOp 为 DONT_CARE
colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;

// 2. 为了让 Attachment 存储到 Tile 内,必须使用标记 TRANSIENT_ATTACHMENT 和 LAZILY_ALLOCATED
VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
imageInfo.flags		= flags;
imageInfo.imageType	= type;
imageInfo.format	= format;
imageInfo.extent	= extent;
imageInfo.samples	= sampleCount;
// Image 使用 TRANSIENT_ATTACHMENT 的标记
imageInfo.usage		= VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT;

VmaAllocation memory;
VmaAllocationCreateInfo memoryInfo{};
memoryInfo.usage		  = memoryUsage;
// Image 所在的内存使用 LAZILY_ALLOCATED 的标记
memoryInfo.preferredFlags = VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT;

// 创建 Image
auto result = vmaCreateImage(device.get_memory_allocator(), &imageInfo, memoryInfo, &handle, &memory, nullptr);
```





# 六、常见问题

## 1. PCIe BindWidth

显存的带宽比内存的大很多(显存的位宽大)

- **内存** 和 **显存** 之间的 PCIe 总线带宽过小是 CPU 和 GPU 交互的瓶颈
- 在移动端,由于其[耦合式的物理架构](#gpu),带宽是一种多设备(CPU、GPU、AUDIO 等)共享的资源,而且**处理器通过带宽对存储的访问很耗电**
- OpenGL 的**显示列表**,将一组绘制指令放到 GPU 上,CPU 只要发一条 "执行这个显示列表" 这些指令就执行,而不必每次渲染都发送大量指令到 GPU,从而节约 PCIe 带宽



移动设备的特点:不同于 PC 端的 CPU 和 GPU 纯粹地追求计算性能,移动端在尺寸、能耗、硬件性能等诸多方面都存在显著的差异

1. 性能(**P**erformance):移动设备的各类元件(CPU、带宽、内存、GPU等)的性能都只是PC设备的数十分之一
2. 能耗(**P**ower):为了满足足够长的续航和散热限制,必须严格控制移动设备的整机功率
   PC 设备通常可以安装散热风扇、甚至水冷系统,而移动设备不具备这些主动散热方式,只能靠热传导散热
   如果散热不当,CPU 和 GPU 都会<u>主动降频</u>,以非常有限的性能运行,以免设备元器件因过热而损毁
3. 面积(**A**rea):移动端的便携性就要求整机只能限制在非常小的体积之内



## 2. Pipeline Barrier

Vulkan、Metal、DX12 等现代图形 API 可以精确指定渲染管线屏障 Barrier 的等待阶段
避免 TBDR 这种 VS 和 FS 分两个 Pass 等待期间造成的 GPU 流水线并发率低,提高 Shader 在 GPU 运行的并发效果

![](./images/pipeline-barrier.png)



## 3. GPU 访问内存

由于 TBDR 的 Tile GPU 缓存拆分,GPU 在访问内存时,多组 ALU 计算单元核需要串行访问

![](./images/tile-based-memory.png)





# Reference

1. [NV extensions](https://www.khronos.org/registry/OpenGL/extensions/NV/)
2. [GDC Vault](https://gdcvault.com/browse/?categories=PgTaVr)
3. [Siggraph Conference Content](https://www.siggraph.org/learn/conference-content/)
4. [Rendering pipeline: The hardware side](https://slideplayer.com/slide/11059244/)
4. [Introduction to GPU Architecture](http://haifux.org/lectures/267/Introduction-to-GPUs.pdf)
4. [An Introduction to Modern GPU Architecture](http://download.nvidia.com/developer/cuda/seminar/TDCI_Arch.pdf)
4. [Revisting Co-Processing for Hash Joins on the Coupled CPU-GPU Architecture](https://www.slideshare.net/mohamedragabslideshare/p12-29046493)
4. [Understanding GPU caches](https://www.rastergrid.com/blog/gpu-tech/2021/01/understanding-gpu-caches/)
8. [Transitioning from OpenGL to Vulkan](https://developer.nvidia.com/transitioning-opengl-vulkan)
9. [Next Generation OpenGL Becomes Vulkan: Additional Details Released](https://www.anandtech.com/show/9038/next-generation-opengl-becomes-vulkan-additional-details-released)
10. [Bringing Fortnite to Mobile with Vulkan and OpenGL ES](https://www.khronos.org/assets/uploads/developers/library/2019-gdc/Vulkan-Bringing-Fortnite-to-Mobile-Samsung-GDC-Mar19.pdf)
5. [Graphics Processing Unit(GPU) Memory Hierarchy](http://meseec.ce.rit.edu/551-projects/spring2015/3-2.pdf)[Tile-Based Rendering](https://developer.arm.com/solutions/graphics-and-gaming/developer-guides/learn-the-basics/tile-based-rendering)
6. [Understanding Render Passes](https://developer.arm.com/solutions/graphics-and-gaming/developer-guides/learn-the-basics/understanding-render-passes)
6. [Asynchronous Shaders](http://developer.amd.com/wordpress/media/2012/10/Asynchronous-Shaders-White-Paper-FINAL.pdf)
7. [GameDev Best Practices](https://developer.samsung.com/galaxy-gamedev/best-practice.html)
8. [Accelerating Mobile XR](https://community.arm.com/cfs-file/__key/communityserver-blogs-components-weblogfiles/00-00-00-20-66/siggraph_2D00_2018_2D00_mmg_2D00_2_2D00_rob_2D00_xr.pdf)
9. [Google Developer Contributes Universal Bandwidth Compression To Freedreno Driver](https://www.phoronix.com/scan.php?page=news_item&px=Freedreno-UBWC-A6XX)
10. [Using pipeline barriers efficiently](https://github.com/KhronosGroup/Vulkan-Samples/blob/master/samples/performance/pipeline_barriers/pipeline_barriers_tutorial.md#the-sample)
11. [Graphics Shaders - Theory and Practice 2nd Edition](http://cs.uns.edu.ar/cg/clasespdf/GraphicShaders.pdf)
11. [Unreal Engine 4: Mobile Graphics on ARM CPU and GPU Architecture](https://armkeil.blob.core.windows.net/developer/Files/pdf/graphics-and-multimedia/Unreal Engine 4 Mobile Graphics on ARM CPU and GPU Architecture.pdf)
12. [渲染优化-从GPU的结构谈起](https://zhuanlan.zhihu.com/p/58694744)
12. [Render Graph 与现代图形 API](https://zhuanlan.zhihu.com/p/425830762)
7. [计算机那些事(8)——图形图像渲染原理](http://chuquan.me/2018/08/26/graphics-rending-principle-gpu/)
7. [移动游戏性能优化通用技法](https://www.cnblogs.com/timlly/p/10463467.html)
7. [写实大世界游戏渲染技术详解 GPU 优化](https://mp.weixin.qq.com/s/T1t7dQwmxoUfuz1ik2USVg)
7. [剖析虚幻渲染体系(12)- 移动端专题Part 2(GPU架构和机制)](https://www.cnblogs.com/timlly/p/15546797.html)



================================================
FILE: ComputerGraphics(OpenGL)/EXT3_Platform.md
================================================
# 零、代码 Debug

> 关于性能的衡量 帧率/单帧时间,**建议采用单帧时间**
>
> 由于 帧率 = 60 / 单帧时间,他们之间的关系不是线性的,例如在减少同样时间消耗的情况下:
> 低帧率区间进步缓慢,每秒 10fps 下,帧时间减少 2ms,帧率提高到 10.2fps
> 高帧率区间进步明显,每秒 25fps 下,帧时间减少 2ms,帧率提高到 26.3fps



第三方跨平台框架

- [Flutter外接纹理](https://zhuanlan.zhihu.com/p/42566807)



# 一、Android 平台

>图片有不清晰的地方,可以点开连接看大图

## 1. 数据的封装

### 1.1 Surface
内存中的一段绘图缓冲区,是对 framebuffer 的 Java 封装对象



### 1.2 SurfaceTexture
内存中的一段绘图缓冲区,对 EGL texture 的 Java 封装对象

数据源:android.hardware.camera2, MediaCodec, MediaPlayer 和 Allocation 这些类的目标视频数据输出对象

限制:API 11 存在,Android 3.0 及其后才能使用

特点:可以接收一个 EGL texture 的纹理 ID 来产生,可以做到**离线渲染**

关键方法:

- updateTexImage(从内容流中获取当前帧,使得内容流中的一些帧可以跳过)

- 通过 调用 getTransformMatrix 获取纹理的旋转情况

![](./images/surfaceTexture.png)



SurfaceTexture 使用流程

![](./images/processSurfaceTexture.png)



## 2. 数据的展示和刷新

### 2.1 SurfaceView

父类:View

限制:API 1 存在

回调方法运行线程:主线程

优点:有自己的 Surface 来刷新,可以做到**局部刷新,独立线程刷新数据**

缺点:不能进行 Transition,Rotation,Scale 等变换,这导致 SurfaceView 在由于刷新不受主线程控制,**滑动时可能有黑边**

关键方法:getHolder(获取 SurfaceHolder ,SurfaceHolder 持有 Surface 数据对象)



### 2.2 GLSurfaceView

父类:SurfaceView

限制:API 3 存在,Android 1.5 及其后才能使用

关键方法:

![](./images/glsurfaceview.png)



### 2.3 TextureView

父类:View

限制:API 14 存在,Android 4.0 及其后才能使用,只能针对用于硬件加速(没有 GPU 就无法使用)

回调方法运行线程:主线程(在Android 5.0 引入渲染线程后,它是在渲染线程中做的)

特点:没有自己的 Surface 来刷新,使用所在的 window 来**全局刷新**,可以进行 Transition,Rotation,Scale 等变换,**滑动时没有黑边**

关键方法:

- getSurfaceTexture(可能返回 null)

- setSurfaceTextureListener

  ```java
  public DrawTextureView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    this.setSurfaceTextureListener(this);
  }
  
  @Override
  public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
    mSurface = new Surface(surface);
  }
  
  @Override
  public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {}
  
  @Override
  public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {  
    return true;
  }
  
  @Override
  public void onSurfaceTextureUpdated(SurfaceTexture surface) {}
  ```
  <img src="./images/textureView.jpeg"  />
  
  

### 2.4 Android 5.0 引入渲染线程

![](./images/renderThread.jpeg)



## 3. EGL 环境配置

> 如果 OpenGL 是打印机,EGL 就是纸。EGL:作为 OpenGL ES 和本地窗口的桥梁,不同平台的 EGL 实现方式不一样


### 3.1 渲染同步问题


本地环境和客户端环境

- 本地窗口环境:Windows、X
- 客户端环境:3D 渲染器 OpenGL、2D 矢量图形渲染器 [OpenVG](https://baike.baidu.com/item/OpenVG/7922699?fr=aladdin)



同一个 surface 上可能 **同时异步** 执行了 <u>本地窗口环境</u> 和 <u>客户端环境</u> 的命令

- 本地环境等待客户端环境渲染完成,效果同 glFinish 或 vgFinish 一致
  [EGLBoolean eglWaitClient(void);](https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglWaitClient.xhtml)
- 客户端环境等待本地环境渲染完成
  [EGLBoolean eglWaitNative(EGLint engine);](https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglWaitNative.xhtml)



### 3.2 获取 EGL 版本信息

1. [EGLBoolean eglInitialize(EGLDisplay dpy, EGLint *major, EGLint *minor);](https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglInitialize.xhtml)
   本质是初始化函数,但也能通过返回 major,minor 来获取当前 display 硬件设备支持的 OpenGL ES 版本号
2. [const char* eglQueryString(EGLDisplay dpy, EGLint name);](https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglQueryString.xhtml)
   查询当前 display 硬件设备有哪些  EGL 的扩展支持,**调用前必须先通过 eglInitialize 初始化 EGL**



### 3.3 EGL 的 Context 和 Surface

EGL 主要作用是将渲染绘制到本地窗口上

EGL 可以销毁本地资源(各种 surface 类型)
只有一个方法,[EGLBoolean eglDestroySurface(EGLDisplay display, EGLSurface surface);](https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglDestroySurface.xhtml)

EGL 可以创建本地环境的资源(各种 surface 类型)格式有

1. pixel buffer(存储在显存上)
   OpenGL API 方面,[EGLSurface eglCreatePbufferSurface(...)](https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglCreatePbufferSurface.xhtml)
   OpenVG API 方面,[EGLSurface eglCreatePbufferFromClientBuffer(...)](https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglCreatePbufferFromClientBuffer.xhtml)
2. frame buffer(存储在显存上)
   本地环境 API 创建的 window 内的 surface,[EGLSurface eglCreateWindowSurface(...)](https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglCreateWindowSurface.xhtml)
   
3. 本地环境 API 创建的 [pixmap(常用做本地图像的内部存储)](https://franz.com/support/documentation/current/doc/cg/cg-pixmaps.htm),[EGLSurface eglCreatePixmapSurface(...);](https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglCreatePixmapSurface.xhtml)
	Pixmap:将图像以像素颜色数组的结构来存储的对象
	Bitmap:用 1 bit 来存储 1 个像素颜色的 Pixmap



**EGLContext 上下文创建步骤**

```c
#include <stdlib.h>
#include <unistd.h>
#include <EGL/egl.h>
#include <GLES/gl.h>

typedef ... NativeWindowType;
extern NativeWindowType createNativeWindow(void);

// 虽然是一维数组,但还是要采用 id, value, id, value ... 的存储方式
const EGLint attribute_list[] =
{
    EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT_KHR,
  	//EGL_WINDOW_BIT EGL_PBUFFER_BIT we will create a pixelbuffer surface
    EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
    EGL_RED_SIZE,   	8,
    EGL_GREEN_SIZE, 	8,
    EGL_BLUE_SIZE,    8,
    EGL_ALPHA_SIZE,   8, // if you need the alpha channel
    EGL_DEPTH_SIZE,   8, // if you need the depth buffer
    EGL_STENCIL_SIZE, 8,
    EGL_NONE
};

// EGL context attributes
const EGLint ctxAttr[] = {
    EGL_CONTEXT_CLIENT_VERSION, 2,
    EGL_NONE
};

void createGLESEnv()
{
    EGLint num_config;
    EGLint numConfigs;
    EGLint eglMajVers;
    EGLint eglMinVers;
  
    EGLConfig config;
    EGLDisplay m_eglDisplay;	// 关联系统物理屏幕,表示显示设备句柄
    EGLContext m_eglContext;
    EGLSurface m_eglSurface;  // EGLSurface 和 Java 的 Surface 没有关系,是两个独立的对象
    NativeWindowType native_window;

    // 1. get an EGL display connection
    m_eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
    if(EGL_NO_DISPLAY == m_eglDisplay) {
      Log.e("ERROR: get an EGL display connection");
    }

    // 2. initialize the EGL display connection
    if (!eglInitialize(m_eglDisplay, &eglMajVers, &eglMinVers)) {
			Log.e("ERROR: initialize the EGL display connection");
    }

    // 3. get an appropriate EGL frame buffer configuration
    if(!eglChooseConfig(m_eglDisplay, attribute_list, &config, 1, &num_config)) {
      Log.e("ERROR: get an appropriate EGL frame buffer configuration");
    }

    // 4. create an EGL rendering context
    m_eglContext = eglCreateContext(m_eglDisplay, config, EGL_NO_CONTEXT, ctxAttr);	
  	if (EGL_NO_CONTEXT == m_eglContext) {
      EGLint error = eglGetError();
      if(error == EGL_BAD_CONFIG) {
        Log.e("ERROR: create an EGL rendering context");
      }
		}

    // 5. create a native window
    native_window = createNativeWindow();

    // 6. create an EGL window surface
    m_eglSurface = eglCreateWindowSurface(m_eglDisplay, config, native_window, NULL);

    // 7. connect the context to the surface
    if (!eglMakeCurrent(m_eglDisplay, m_eglSurface, m_eglSurface, m_eglContext)) {
			Log.e("ERROR: connect the context to the surface");
    }
}

void destroyGlESEnv()
{
    if (m_eglDisplay != EGL_NO_DISPLAY) {
        eglMakeCurrent(m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
        eglDestroyContext(m_eglDisplay, m_eglContext);
        eglDestroySurface(m_eglDisplay, m_eglSurface);
        eglReleaseThread();
        eglTerminate(m_eglDisplay);
    }

    m_eglDisplay = EGL_NO_DISPLAY;
    m_eglSurface = EGL_NO_SURFACE;
    m_eglContext = EGL_NO_CONTEXT;
}

int main(int argc, char ** argv)
{
    createGLESEnv();

    glClearColor(1.0, 1.0, 0.0, 1.0);
    glClear(GL_COLOR_BUFFER_BIT);
    glFlush();

    // 所有的绘制步骤在后台绘制,当绘制完成时切换前后台缓冲,确保显示的一直是绘制完成的画面
    eglSwapBuffers(m_eglDisplay, m_eglSurface);
  
  	destroyGlESEnv();

    return EXIT_SUCCESS;
}
```



## 4. 平台问题

### 4.1 TEXTURE_EXTERNAL_OES

[TEXTURE_EXTERNAL_OES](https://www.khronos.org/registry/OpenGL/extensions/OES/OES_EGL_image_external.txt) 是 OpenGL ES 在 Android 上的扩展,在获取相机纹理时只有 TEXTURE_EXTERNAL_OES 类型的纹理

使用扩展纹理 TEXTURE_EXTERNAL_OES 步骤

1. 创建纹理时

   ```java
   // 注意 GLES11Ext
   GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId);
   
   GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
   GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
   GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP_TO_EDGE);
   GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP_TO_EDGE);
   ```

2. 在 fragment shader 里要提前声明使用的扩展

   ```c
   #extension GL_OES_EGL_image_external : require
   uniform samplerExternalOES u_texture; // 代替 sampler2D
   void main() {}
   ```

   

### 4.2 Java 成员变量和 C++ 指针的 JNI 层绑定

绑定指针

- Java 类中,声明一个为 long 的长整型类型的成员变量代表 C++ 指针的句柄

- C / C++ 文件中,每次获取 jlong 的成员变量时**强制转换为指针来使用**

  ```c
  #include <jni.h>
  
  jfieldID inline getHandleField(JNIEnv *env, jobject obj)
  {
      jclass c = env->GetObjectClass(obj);
      // J is the type signature for long:
      return env->GetFieldID(c, "nativeHandle", "J");
  }
  
  template <typename T>
  T *getHandle(JNIEnv *env, jobject obj)
  {
      jlong handle = env->GetLongField(obj, getHandleField(env, obj));
      return reinterpret_cast<T *>(handle);
  }
  
  template <typename T>
  void setHandle(JNIEnv *env, jobject obj, T *t)
  {
      jlong handle = reinterpret_cast<jlong>(t);
      env->SetLongField(obj, getHandleField(env, obj), handle);
  }
  
  // Use Example
  MyObject* ptr = new MyObject();
  setHandle<MyObject>(env, object, ptr);
  ptr = getHandle<MyObject>(env, object);
  ```

  

绑定智能指针

- Java 类中,声明一个为 long 的长整型类型的成员变量代表 C++ 指针的句柄

- C / C++ 文件中,每次获取 jlong 的成员变量时**强制转换为包含智能指针的对象的指针来使用**

  ```c
  template <typename T>
  class SmartPointerWrapper {
      std::shared_ptr<T> mObject;
  public:
      template <typename ...ARGS>
      explicit SmartPointerWrapper(ARGS... a) {
          mObject = std::make_shared<T>(a...);
      }
  
      explicit SmartPointerWrapper (std::shared_ptr<T> obj) {
          mObject = obj;
      }
  
      virtual ~SmartPointerWrapper() noexcept = default;
  
      void instantiate (JNIEnv *env, jobject instance) {
          setHandle<SmartPointerWrapper>(env, instance, this);
      }
  
      jlong instance() const {
          return reinterpret_cast<jlong>(this);
      }
  
      std::shared_ptr<T> get() const {
          return mObject;
      }
  
      static std::shared_ptr<T> object(JNIEnv *env, jobject instance) {
          return get(env, instance)->get();
      }
  
      static SmartPointerWrapper<T> *get(JNIEnv *env, jobject instance) {
          return getHandle<SmartPointerWrapper<T>>(env, instance);
      }
  
      static void dispose(JNIEnv *env, jobject instance) {
          auto obj = get(env,instance);
          delete obj;
          setHandle<SmartPointerWrapper>(env, instance, nullptr);
      }
  };
  
  // Use Example
  SmartPointerWrapper<Object> obj = new SmartPointerWrapper<Object>(arguments);
  obj->instantiate(env,instance);
  ```




## 5. Debug

1. [系统自带的 GPU 呈现分析](https://zhuanlan.zhihu.com/p/22334175)
2. [高通骁龙 Adreno GPU Profiler 调试工具(建议在 windows 下使用,mac 下测试无用)](https://gameinstitute.qq.com/community/detail/123051)
3. [GAPID 调试 Android 应用,需要 Android stuido 停用 adb 的使用](http://www.geeks3d.com/20171214/google-gapid-capture-vulkan-and-opengl-es-calls-on-android-windows-macos-and-linux/)
4. 部分 vivo 手机会出现安装 app 失败的问题,需要在 Android Studio 设置里的 Build > Instant Run > disable Instant Run





# 二、iOS 平台

## 1. 数据封装

### 1.1 内存与纹理建立映射关系

**CVOpenGLESTextureCacheCreateTextureFromImage**

- 从 **CVOpenGLESTextureCacheRef** 纹理缓存中获取一个新的纹理,或者符合传入参数的已创建的纹理
- 将一段内存与 **CVOpenGLESTextureCacheRef** 中的一个纹理建立映射关系

```objc
+ (CVReturn)createTextureFromPixelBuffer:(CVImageBufferRef __nonnull)pixelBuffer
                                   width:(int)width
                                  height:(int)height
                                   cache:(CVOpenGLESTextureCacheRef __nonnull)textureCache
                               cvTexture:(CVOpenGLESTextureRef* __nonnull)cvTexture
                            textureOuput:(unsigned int* __nullable)textureOuput
{
  CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
  CVReturn result = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
                                                                 textureCache,
                                                                 pixelBuffer,
                                                                 NULL,
                                                                 GL_TEXTURE_2D,
                                                                 GL_RGBA,
                                                                 width,
                                                                 height,
                                                                 GL_BGRA,
                                                                 GL_UNSIGNED_BYTE,
                                                                 0,
                                                                 cvTexture);
  CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
  
  if (NULL == textureOuput) {
    glBindTexture(CVOpenGLESTextureGetTarget(*cvTexture), CVOpenGLESTextureGetName(*cvTexture));
  } else  {
    *textureOuput = CVOpenGLESTextureGetName(*cvTexture);
    glBindTexture(CVOpenGLESTextureGetTarget(*cvTexture), *textureOuput);
  }
  
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  glBindTexture(GL_TEXTURE_2D, 0);
  
  return result;
}
```



## 2. 数据的展示和刷新

### 2.1 CAEAGLLayer

CAEAGLLayer  是 UIView 的 OpenGL 展示层,可以通过重写 UIView 的以下方法来创建 CAEAGLLayer 对象

```objective-c
+ (Class)layerClass
{
  return [CAEAGLLayer class];
}
```



CAEGALLayer 的配置

```objc
CAEAGLLayer *layer = (CAEAGLLayer *)self.layer;
layer.opaque = YES;
layer.drawableProperties = @{
  kEAGLDrawablePropertyRetainedBacking: [NSNumber numberWithBool:false],
  kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8
};
```



### 2.2 GLKViewController

GLKViewController 内含 GLKView,内部已经创建好了 OpenGL 的上下文,并且可以通过 `self.preferredFramesPerSecond` 来控制刷新帧率。详见 [Apple 官方文档](https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/OpenGLES_ProgrammingGuide/DrawingWithOpenGLES/DrawingWithOpenGLES.html#//apple_ref/doc/uid/TP40008793-CH503-SW1)



### 2.3 CADisplayLink

当系统屏幕刷新时,通过 CADisplayLink 绑定的回调函数获取屏幕刷新帧率

```objc
- (void)createDisplayLink {
    CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self
                                                             selector:@selector(step:)];
    [displayLink addToRunLoop:[NSRunLoop currentRunLoop]
                      forMode:NSRunLoopCommonModes];
}

- (void)step:(CADisplayLink *)sender {
    NSLog(@"%f", sender.targetTimestamp);
}
```





## 3. EGL 环境配置

### 3.1 EGL 的 Context 

[Configuring OpenGL ES Contexts](https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/OpenGLES_ProgrammingGuide/WorkingwithOpenGLESContexts/WorkingwithOpenGLESContexts.html#//apple_ref/doc/uid/TP40008793-CH2-SW1)

```objc
// 1. 创建上下文
EAGLContext *firstContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];

// 2. 使用共享上下文
//    把新建的上下文放到已有上下文的 EAGLSharegroup 中来确保 EAGLSharegroup 中的上下文进行资源共享
// 注:EAGLSharegroup 为不透明类,只读,不可自行创建和修改
EAGLContext* secondContext = [[EAGLContext alloc] initWithAPI:[firstContext API] 
                                                   sharegroup:[firstContext sharegroup]];

// 3. 设置上下文为当前上下文
if ([EAGLContext currentContext] != firstContext) {
  [EAGLContext setCurrentContext:firstContext];
}
```



## 4. 平台问题

### 4.1 证书问题

1. [苹果开发者账号申请流程完整版](https://www.jianshu.com/p/655380201685)



## 5. Debug

1. [使用Xcode GPU Frame Caputre教程](https://www.cnblogs.com/TracePlus/p/4093830.html)





# 三、QT 平台

## 1. 数据封装

### 1.1 QSurface

内存中的一段绘图缓冲区,对 framebuffer 的 Qt 封装对象
有 OpengLSurface、OpenVGSurface(2D)、RasterSurface等多种类型



### 1.2 QOffscreenSurface

不需要在创建 QWindow 的情况下创建,本地窗口内存中的一段绘图缓冲区
可以获取 QOffscreenSurface 中创建的 OpenGL 资源,但无法通过 read pixel 的形式获取像素数据



### 1.3 QGLFunctions

QGLFunctions 提供 OpenGL ES 2.0 头文件接口功能,内部成员函数都是 OpenGL 函数,如果要使用 Qt 提供的 OpenGL 函数需要:继承 QGLFunctions 

如果想使用 OpenGL ES 2.0 意外的 API,需要继承 QOpenGLFunctions_3_3[_Core/Compatibility] 类



### 1.2 QGLBuffer、QGLColormap、QGLPixelBuffer...

Qt 将 OpenGL 许多用 C 风格的写成的对象封装为 Qt 内部的对象以方便与 Qt 其他的对象交互





## 2. 数据的展示和刷新

### 2.1 QGLWidget

QGLWidget 继承自QWidget,绑定当前窗口的显存,内置 OpenGL 上下文

- void initializeGL():初始化 OpenGL 上下文
- void resizeGL(int w, int h):设置 OpenGL view port
- void paintGL():绘制 OpenGL 场景





## 3. GL 环境配置

### 3.1 链接库 QtOpenGL

使用 Qt 内部封装的 OpenGL 函数前,需要在 qmake 的 .pro 里添加 QT += opengl 库



### 3.2 配置 QOpenGLContext

QOpenGLContext 代表本地窗口的 OpenGL 上下文,渲染在 QSurface 上

```c++
void GLWidget::initializeGL()
{
    QOpenGLContext* context = QOpenGLContext::currentContext();
    QSurface* mainSurface = context->surface();

  	// 创建离屏渲染的 surface
    QOffscreenSurface* renderSurface = new QOffscreenSurface(nullptr, this);
	  // 设置 QSurfaceFormat
    renderSurface->setFormat(context->format()); 
    renderSurface->create();

  	// 设置当前 context 为 NULL
    context->doneCurrent();
  
  	// 设置当前 context 为 GLWidget 一开始默认的当前上下文
    context->makeCurrent(mainSurface);
}

int main(int argc, char *argv[]) {
    QSurfaceFormat format;
    format.setMajorVersion(3);
    format.setMinorVersion(3);
    format.setProfile(QSurfaceFormat::CoreProfile);
    format.setOption(QSurfaceFormat::DebugContext);
  
  	// 设置 Qt 的 QOpenGLContext, QWindow, QOpenGLWidget 中 QSurface 默认的格式
  	// 这个默认的全局通用格式会被 Qt 内部的函数设置的格式覆盖
    QSurfaceFormat::setDefaultFormat(format);
  
  	QApplication a(argc, argv);
    a.setWindowIcon(QIcon(":/resources/Editor.ico"));
 
    QMainWindow w;
    w.show();
  
    return a.exec();
}
```





## 4. 平台问题

如果使用的是不通平台的 OpenGL 原生的库,需要根据所在平台的不同在 qmake 的 .pro 里添加不同的库文件

```cmake
win32-g++ {
    LIBS += -lopengl32
}
win32-msvc*{
    LIBS += opengl32.lib
}

unix {
		
}
```





## 5. Debug

查看 OpenGL 上下文信息

```c++
// ViewRender 继承自 QOpenGLWidget 和 QOpenGLFunctions
void ViewRender::logCtxInfo()
{
    QOpenGLContext *ctx = QOpenGLContext::currentContext();
    QOpenGLContext *defaultCtx = context();
    GLint attributeNumber;
    glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &attributeNumber);
    qDebug() << "Has default context: " <<  (defaultCtx != nullptr ? "Yes" : "No") << endl
             << "Default context is current context: " << (defaultCtx == ctx ? "Yes" : "No")  << endl
             << "Renderer: " << (const char*)glGetString(GL_RENDERER) << endl
             << "Version:  " << (ctx->isOpenGLES() ? "OpenGL ES" : "OpenGL") << (const char*)glGetString(GL_VERSION) << endl
             << "Shader Version:" << (const char*)glGetString(GL_SHADING_LANGUAGE_VERSION) << endl
             << "R:" << ctx->format().redBufferSize() << endl
             << "G:" << ctx->format().greenBufferSize() << endl
             << "B:" << ctx->format().blueBufferSize() << endl
             << "A:" << ctx->format().alphaBufferSize() << endl
             << "Depth:   " << ctx->format().depthBufferSize() << endl
             << "Stencil: " << ctx->format().stencilBufferSize() << endl
             << "Pixel Radio: " << devicePixelRatio() << endl
             << "Support Attribute Number: " << attributeNumber << endl;
}
```



查看 OpenGL 错误信息

```c++
#ifndef QT_NO_DEBUG
#define glCheckError() glCheckError_(__FILE__, __LINE__, this)
#else
#define glCheckError()
#endif

GLenum glCheckError_(const char *file, int line, QAbstractOpenGLFunctions* obj) {
    GLenum errorCode;
    while ((errorCode = obj->glGetError()) != GL_NO_ERROR) {
        std::string error;
        switch (errorCode) {
            case GL_INVALID_ENUM:                  error = "INVALID_ENUM"; break;
            case GL_INVALID_VALUE:                 error = "INVALID_VALUE"; break;
            case GL_INVALID_OPERATION:             error = "INVALID_OPERATION"; break;
            case GL_STACK_OVERFLOW:                error = "STACK_OVERFLOW"; break;
            case GL_STACK_UNDERFLOW:               error = "STACK_UNDERFLOW"; break;
            case GL_OUT_OF_MEMORY:                 error = "OUT_OF_MEMORY"; break;
            case GL_INVALID_FRAMEBUFFER_OPERATION: error = "INVALID_FRAMEBUFFER_OPERATION"; break;
        }
        std::cout << "Error:" << error << " File:" << file <<  " Line:" << line << std::endl;
    }
    return errorCode;
}
```





# Reference

- [A C++ Smart Pointer wrapper for use with JNI](https://www.studiofuga.com/2017/03/10/a-c-smart-pointer-wrapper-for-use-with-jni/)
- [Android Graphics 官方文档](https://source.android.com/devices/graphics)
- [Android中的 EGL 扩展](http://ju.outofmemory.cn/entry/146313)
- [SurfaceView、SurfaceHolder、Surface](https://blog.csdn.net/holmofy/article/details/66578852)
- [TextureView、SurfaceTexture、Surface](https://blog.csdn.net/Holmofy/article/details/66583879)
- [SurfaceView、TextureView、SurfaceTexture 等的区别](https://www.cnblogs.com/wytiger/p/5693569.html)
- [OpenGL ES:EGL 接口解析与理解](https://blog.csdn.net/xuwei072/article/details/70049004)
- [OpenGL ES:EGL简介](https://blog.csdn.net/iEearth/article/details/71180457)
- [Android中 的 GraphicBuffer 同步机制 Fence](https://blog.csdn.net/jinzhuojun/article/details/39698317)
- [深入 Android 渲染机制](https://www.cnblogs.com/ldq2016/p/6668148.html)
- [Android Deeper(01) - Graphic Architecture](http://hukai.me/android-deeper-graphics-architecture/)
- [Apple OpenGL ES Programming Guide](https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/OpenGLES_ProgrammingGuide/Introduction/Introduction.html?language=objc#//apple_ref/doc/uid/TP40008793)
- [Android 相机开发中的尺寸和方向问题](https://glumes.com/post/android/android-camera-aspect-ratio--and-orientation/)
- [Google 官方相机 Demo](https://github.com/google/cameraview)
- [Android 音视频开发打怪升级](https://mp.weixin.qq.com/s/4Rn5Z54lu3O55c7MK3nNpg)



================================================
FILE: ComputerGraphics(OpenGL)/Part0_Context&Pipeline.md
================================================
# 一、OpenGL 简介

>  OpenGL 作为图形硬件标准,是最通用的图形管线版本
>  使用 OpenGL 自带的数据类型可以确保各平台中每一种类型的大小都是统一的
>
>  **OpenGL 只是一个标准/规范,具体的实现是由驱动开发商针对特定显卡来实现**



## 1. OpenGL Context

由于 OpenGL 内部是一个类似于全局变量的状态机

- 切换状态 `glEnable()`、 `glDisable()` 
- 查询状态 `glIsEnabled()` 
- 存储状态 `glPushAttrib()`:保存 OpenGL 当前属性状态信息到属性栈中
- 恢复之前存储的状态 `glPopAttrib()`:从属性栈中获取栈首的一系列属性值



**OpenGL Context 的接口和实现没有统一的标准,随着不同操作系统平台的不同而不同** 

OpenGL 命令执行的结果影响 OpenGL 状态(由 OpenGL context 保存,包括OpenGL 数据缓存)或 影响帧缓存

1. 使用 OpenGL 之前必须先创建 OpenGL Context,并 make current 将创建的 上下文作为当前线程的上下文

2. **OpenGL 标准并不定义如何创建 OpenGL Context,这个任务由其他标准定义**
   如GLX(linux)、WGL(windows)、EGL(一般在移动设备上用)

3. 上下文的描述类型有 **core profile (不包含任何弃用功能)** 或 **compatibility profile (包含任何弃用功能)** 两种
   如果创建的是 core profile OpenGL context,调用如 glBegin() 等兼容 API 将产生GL_INVALID_OPERATION 错误(用 glGetError() 查询)

4. 共享上下文

   一个窗口的 Context 可以有多个,在某个线程创建后,所有 OpenGL 的操作都会转到这个线程来操作
   两个线程同时 make current 到同一个绘制上下文,会导致程序崩溃 

   一个线程同一时间只能用一个上下文,一个线程可以切换多个上下文
   
   一般每个窗口都有一个上下文,可以保证上下文间的不互相影响
   通过**创建上下文时传入要共享的上下文**,多个窗口的上下文之间图形资源可以共享
   可以共享的:纹理、shader、Vertex Buffer 等,外部传入对象
   不可共享的:Frame Buffer Object、Vertex Array Object(内存)、Vertex Buffer Object(显存)、等 OpenGL 内置容器**对象**



## 2. OpenGL 的环境配置流程

### 2.1 动态获取 OpenGL 函数地址

OpenGL 只是一个标准/规范,具体的实现是由驱动开发商针对特定显卡来实现,而且 OpenGL 驱动版本众多,它大多数函数的位置都**无法在编译时确定下来,需要在运行时查询**
因此,在编写与 OpenGL 相关的程序时需要开发者自己来获取 OpenGL 函数地址

相关库可以提供 OpenGL 函数获取地址后的头文件:[GLAD](https://github.com/Dav1dde/glad)



### 2.2 创建上下文

OpenGL 创建上下文的操作在不同的操作(窗口)系统上是不同的,所以需要开发者自己处理:**窗口的创建、定义上下文、处理用户输入**

相关库可以摆脱平台的限制,提供一个较为统一的接口和窗口、上下文用来渲染:[GLUT](http://freeglut.sourceforge.net/)、SDL、SFML、[GLFW](http://www.glfw.org/download.html)



## 3. OpenGL 的执行模型(Client - Server 模型)

> 主函数在 CPU 上执行,图形渲染在 GPU 上执行
> 虽然 GPU 可以编程,但这样的程序也需要在 CPU 上执行来操作 GPU

基本执行模型:CPU 上 push command 命令,GPU 上执行命令的渲染操作

- **应用程序 和 GPU 的执行通常是异步的**
  OpenGL API 调用返回 != OpenGL 在 GPU 上执行完了相应命令,但保证按调用顺序执行
  同步方式:**glFlush()** 强制发出所有 OpenGL 命令并在此函数返回后的有限时间内执行完这些 OpenGL 命令
  异步方式:**glFinish()** 等待直到**此函数之前**的 OpenGL 命令执行完毕才返回

- **应用程序 和 OpenGL 可以在也可以不在同一台计算机上执行**
  一个网络渲染的例子是通过 Windows 远程桌面在远程计算机上启动 OpenGL 程序,应用程序在远程计算机执行,而 OpenGL 命令在本地计算机执行(**将几何数据**而不是将渲染结果图像通过网络传输)

  > 当 Client 和 Server 位于**同一台计算机**上时,也称 GPU 为 Device,CPU 为 Host
  > Device、Host 这两个术语通常在用 GPU 进行通用计算时使用

- **内存管理**
  CPU 上由程序准备的缓存数据(buffer、texture 等)存储在显存(video memory)中,这些数据从程序到缓存中拷贝,也可以再次拷贝到程序的缓存中
  
- **数据绑定发生在 OpenGL 命令调用时**
  应用程序传送给 GPU 的数据在 OpenGL API 调用时解释,在调用返回时完成
  例,指针指向的数据给 OpenGL 传送数据,如 glBufferData()  在此 API 调用返回后修改指针指向的数据将不再对 OpenGL 状态产生影响



## 4. OpenGL 的着色器程序 Shader

### 4.1 不同平台的 shader 编译

OpenGL 的 GLSL(OpenGL Shading Language)

- 跨平台
- 运行时,将 GLSL 源码交给 GPU 图形驱动厂商编译成汇编语言后由 GPU 执行



DirectX 的 HLSL(High Level Shading Language)

- 微软独占,可以提前编译成机器语言,在运行时直接在 GPU 执行

  

NVIDIA 的 CG(C for Graphic)

- 跨平台,根据平台的不同编译成相应的中间语言



### 4.2 Shader 接口一致性

> shader link 到 program 里可以 detached 后继续使用,这样便无法抓取 shader 查看

- Vertex Shader 的 输入 和 应用程序的顶点属性数据接口 一致
- Vertex Shader 的 输出 和 Fragment Shader 对应的 输入 一致
- Fragment Shader 的 输出 和 帧缓存的颜色缓存接口 一致



固定管线功能阶段需要的一些特定输入输出由着色器的内置输出输入变量定义,如下图

![](images/vertexToFragmentAPI.png)



### 4.3 GLSL 版本变化

通过**首行使用** `#version` 来说明当前 OpenGL Shader Language 版本



**GLSL 版本号对应 **

- OpenGL 和 OpenGL 的 Shading Language 版本对应
  | **Version OpenGL** | 2.0 | 2.1 | 3.0 | 3.1 | 3.2 | 3.3 | 4.0 | 4.1 | 4.2 | 4.3 |
  | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
  | **Version GLSL** | 110 | 120 | 130 | 140 | 150 | 330 | 400 | 410 | 420 | 430 |

- OpenGL ES 和 OpenGL ES 的 Shading Language 版本对应
  | **Version OpenGL ES** | 2.0 | 3.0 |
  | --------------------- | --- | --- |
  | **Version GLSL ES**   | 100 | 300 |



**GLSL 版本功能区别 **

1. GLSL 130+ 版本
   用 `in` 和  `out` 替换了 `attribute` 和 `varying`
2. GLSL 330+ 版本
   用 `texture` 替换了 `texture2D` 
   增加了 layout 内存布局功能
3. [其他版本重要功能变化](https://github.com/mattdesl/lwjgl-basics/wiki/glsl-versions)



### 4.4 编写 shader 的注意事项

精度问题

1. 颜色和单位向量用 lowp 精度
2. 减少对 highp 的使用



慎用分支和循环语句

1. GPU 使用了不同于 CPU 的技术来实现分支语句
2. 最坏情况下,花在一个分支上的时间相当于运行了所有的分支语句
3. 使用大量流程控制语句,shader 性能可能会成倍下降
4. 分支语句判断用的条件变量最好是常数
5. 每个分支中的操作指令数尽量少
6. 分支嵌套层数少



## 5. 渲染同步

### 5.1 同步异步的渲染方式 glFlush/glFinish

> 提交给 OpenGL 的指令并不是马上送到驱动程序里执行的,而是放到一个缓冲区里面,等这个缓冲区满了再一次过发到驱动程序里执行,glFlush 可以只接提交缓冲区的命令到驱动执行,而不需要在意缓冲区是否满了

同步方式:[void glFlush()](https://www.khronos.org/opengl/wiki/GLAPI/glFlush) 强制发出所有 OpenGL 命令并在此函数返回后的**有限时间**内执行这些 OpenGL 命令(这些命令可能没有执行完)
异步方式:[void glFinish()](https://www.khronos.org/opengl/wiki/GLAPI/glFinish) 等待直到**此函数之前**的 OpenGL 命令执行完毕才返回



### 5.2 垂直同步 vsync

由于显示器的刷新一般是逐行进行的,因此为了防止交换缓冲区的时候屏幕上下区域的图像分属于两个不同的帧,因此交换一般会等待显示器刷新完成的信号,在显示器两次刷新的间隔中进行交换,这个信号就被称为垂直同步信号,这个技术被称为垂直同步

定义:确保显卡的运算频率(GPU 一秒绘制的帧数)和 显示器刷新频率(硬件决定)一致,防止在快速运动场景下,由于**显卡运算速率大于显示器运算速率**导致快速运动的动作割裂情况(画面撕裂)

流程:`显卡绘制一帧时间 > 显示器刷新一帧时间 ? 显示器刷新(显卡等待) : 显示器显示上一帧,等待显卡绘制完成(屏幕卡顿);`

缺点:开启垂直同步,画面会有延迟(无法达到显卡的最大运算速率),但并没有卡顿

**规避缺点的方法**:用三重缓冲代替垂直同步(三重缓冲:在双缓冲的基础上加了一个缓冲,引入了三缓冲区技术,在等待垂直同步时,来回交替渲染两个离屏的缓冲区,而垂直同步发生时,屏幕缓冲区和最近渲染完成的离屏缓冲区交换,实现充分利用硬件性能的目的)



## 6. 高版本 Feature

### 6.1 Draw Indirect

Indirect:绘制数据直接从显存拿,非直接传输的关系,便于通过 compute shader 直接在 GPU 端直接修改 Buffer 内容

- **Draw Instanced(GPU Instance)**
  绘制多个相同的 Mesh,但它们的位置可以各部相同(如:绘制有多个相同士兵的军队)
  需要完全一样的顶点、索引、渲染状态和材质数据,只允许 Transform 不一样

  ```c
  // 普通绘制
  void glDrawArrays(GLenum mode, GLint first, GLsizei tCount);
  // 绘制多个相同的 Mesh 
  void glDrawArraysInstanced(GLenum mode, GLint first, GLsizei tCount, GLsizei meshCount);

- **Draw Instanced Indirect**
  一次只能绘制一个 Mesh
  Mesh 数据直接从显存里获取数据,并非直接传输关系因此称为 Indirect

  ```c++
  // 1. 创建显存 Mesh 数据等参数
  typedef struct {
  	GLuint vertexCount;
  	GLuint instanceCount; // 绘制 Mesh 个数
  	GLuint firstVertex;
  	GLuint baseInstance;
  } DrawArraysIndirectCommand;
  
  static const DrawArraysIndirectCommand arraysCommand = {
    3, // Three vertices in total, making one triangle
    1, // Draw one copy of this triangle
    0, // Starting index
    0  // Reserved
  }; 
  assert(sizeof(DrawArraysIndirectCommand) == 16);
  
  GLuint arrayCommandBuffer;
  glCreateBuffers(1, &arrayCommandBuffer);
  glNamedBufferData(arrayCommandBuffer, sizeof(DrawArraysIndirectCommand), &arraysCommand, GL_STATIC_DRAW);
  
  // 2. 直接获取显存数据绘制
  void glDrawArraysIndirect(
    GLenum mode,         // GL_TRIANGLES
    const void *indirect // NULL 因为已经 bind GPU buffer 了
  );
  ```

- **Multi Draw Indirect**
  绘制多个相同的 Mesh,Mesh 数据直接从显存里获取数据(一份数据有多个 Mesh 需要的 CMD)

  ```c
  // 1. 创建
  DrawArraysIndirectCommand draws[] =
  { // 一份数据
  	{
  		42, // Vertex count
  		1,  // Instance count
  		0,  // First vertex
  		0   // Base instance
  	},
  	{ /** ... */}
  };
  GLuint buffer;
  glGenBuffers(1, &buffer);
  glBindBuffer(GL_DRAW_INDIRECT_BUFFER, buffer);
  glBufferData(GL_DRAW_INDIRECT_BUFFER, sizeof(draws), draws, GL_STATIC_DRAW);
  
  // 2. 直接获取显存数据绘制
  void glMultiDrawElementsIndirect(
    GLenum mode,           // GL_TRIANGLES
    GLenum type,           // GL_UNSIGED_INT
    const void * indirect, // NULL 因为已经 bind GPU buffer 了
    GLsizei drawcount,     // sizeof(draws)/sizeof(draws[0])
    GLsizei stride         // 0
  );
  ```

  



# 二、渲染管线

> 所谓 OpenGL 管线(OpenGL pipeline),就是指 OpenGL 的渲染过程,即从输入数据到最终产生渲染结果数据所经过的通路及所经受的处理

真实生活中的流水线:

![](./images/pipeline_live.png)

 OpenGL 4.4 渲染管线

![](images/pipeline_gl4.4.png)

DirectX3D 12 渲染管线

![](./images/pipeline_DXD12.png)

![](./images/pipeline.png)

## 1. 应用阶段

应用阶段是开发者可以完全把控的阶段

**绘制物体的包围盒一共有两种,且同时使用**

1. 球体包围盒(用于快速碰撞检测):尺寸比其包含的对象要大
2. 箱体包围盒(更准确的碰撞检测):更接近于对象形状,但是计算量大



一般游戏引擎的绘制顺序

```c
// 每一帧:Draw layer > Draw Technique > Draw Pass
// 每一 Pass:相关 view、相关 shader、相关 material、相关 object
for each view {
    bind view resources					// camera, environment...
      
    for each shader {
        bind shader pipeline
        bind shader resources			// shader control values
          
			for each material {
        		bind material resources	// material params and textures
              
            for each object {
              	bind object reources	// object transforms
                draw object
                  
            } // object
          
        } // material
      
    } // shader
  
} // view
```

### 1.1 计算 Level Of Detail

作用:让近处物体的网格更细致(LOD 0),远处物体的网格更稀疏(LOD n),以便场景运行更流畅

方法:根据一定标准切换 LOD 等级,提前设置好 LOD 等级对应的标准
如果达到标准,就会替换当前 Mesh 为对应的 LOD 等级

常用的 LOD 判断标准有:

1. 视距 View Distance:物体球体包围盒,距离视点的距离
2. 屏占比 Screen Size:物体箱体包围盒,经过透视投影变换后,在当前屏幕渲染像素总数的占比
   比例范围 0.0 ~ 1.0,1.0 表示屏占比 100%



### 1.2 距离体积剔除

在场景里放入一个**箱体**,在箱体范围内通过设置绘制物体的 尺寸 与对应的 相机最大距离 来剔除物体

- 物体尺寸:指的是物体球体包围盒的**直径**大小
- 相机最大距离:物体与相机的距离大于这个距离后相应 物体尺寸的物体会被剔除,如果不设置(值为 0)将永远不会剔除与其配对的物体尺寸



### 1.3 视锥剔除

![](./images/culled_view_frustum.png)

> 在裁剪空间下更容易进行视锥剔除,详见 几何阶段的视锥剔除

在世界空间下的视锥剔除流程(剔除的是包围盒 Mesh 对象,而非单个三角面图元)

1. 计算包围要绘制物体的 AABB 盒
2. 获得视锥体六个面的平面方程
3. 判断 AABB 盒的最小点和最大点在六个面的内侧还是外侧
4. 剔除掉最小和最大点完全在外侧的物体

```c++
// 视锥体的六个平面方程,用于视锥剔除
// 所得的法向都是指向内部的(面向原点)
void GetViewingFrustumPlanesByProjM4(std::vector<glm::vec4> & result , const glm::mat4 &vp) {
	//左侧  
	result[0].x = vp[0][3] + vp[0][0];
	result[0].y = vp[1][3] + vp[1][0];
	result[0].z = vp[2][3] + vp[2][0];
	result[0].w = vp[3][3] + vp[3][0];
	//右侧
	result[1].x = vp[0][3] - vp[0][0];
	result[1].y = vp[1][3] - vp[1][0];
	result[1].z = vp[2][3] - vp[2][0];
	result[1].w = vp[3][3] - vp[3][0];
	//上侧
	result[2].x = vp[0][3] - vp[0][1];
	result[2].y = vp[1][3] - vp[1][1];
	result[2].z = vp[2][3] - vp[2][1];
	result[2].w = vp[3][3] - vp[3][1];
	//下侧
	result[3].x = vp[0][3] + vp[0][1];
	result[3].y = vp[1][3] + vp[1][1];
	result[3].z = vp[2][3] + vp[2][1];
	result[3].w = vp[3][3] + vp[3][1];
	//Near
	result[4].x = vp[0][3] + vp[0][2];
	result[4].y = vp[1][3] + vp[1][2];
	result[4].z = vp[2][3] + vp[2][2];
	result[4].w = vp[3][3] + vp[3][2];
	//Far
	result[5].x = vp[0][3] - vp[0][2];
	result[5].y = vp[1][3] - vp[1][2];
	result[5].z = vp[2][3] - vp[2][2];
	result[5].w = vp[3][3] - vp[3][2];
}

//点到平面距离 d =  Ax + By + Cz + D;
// d < 0 点在平面法向反方向所指的区域
// d > 0 点在平面法向所指的区域
// d = 0 在平面上
// d < 0为 false
bool Point2Plane(const glm::vec3 &v,const glm::vec4 &p) {
	return p.x * v.x + p.y * v.y + p.z * v.z + p.w >= 0;
}

std::vector<glm::vec4> ViewPlanes;
//构造函数中
ViewPlanes.resize(6, glm::vec4(0));

void UpdateViewPlanes() {
	ViewingFrustumPlanes(ViewPlanes,  ProjectMatrix * ViewMatrix);
}

bool ViewCull(const glm::vec4 &v1,const glm::vec4 &v2,const glm::vec4 &v3) {
	glm::vec3 minPoint, maxPoint;
	minPoint.x = min(v1.x, min(v2.x, v3.x));
	minPoint.y = min(v1.y, min(v2.y, v3.y));
	minPoint.z = min(v1.z, min(v2.z, v3.z));
	maxPoint.x = max(v1.x, max(v2.x, v3.x));
	maxPoint.y = max(v1.y, max(v2.y, v3.y));
	maxPoint.z = max(v1.z, max(v2.z, v3.z));
	// Near 和 Far 剔除时只保留完全在内的
	if (!Point2Plane(minPoint, ViewPlanes[4]) || !Point2Plane(maxPoint, ViewPlanes[4])) {
		return false;
	}
	if (!Point2Plane(minPoint, ViewPlanes[5]) || !Point2Plane(maxPoint, ViewPlanes[5])) {
		return false;
	}
	if (!Point2Plane(minPoint, ViewPlanes[0]) && !Point2Plane(maxPoint, ViewPlanes[0])) {
		return false;
	}
	if (!Point2Plane(minPoint, ViewPlanes[1]) && !Point2Plane(maxPoint, ViewPlanes[1])) {
		return false;
	}
	if (!Point2Plane(minPoint, ViewPlanes[2]) && !Point2Plane(maxPoint, ViewPlanes[2])) {
		return false;
	}
	if (!Point2Plane(minPoint, ViewPlanes[3]) && !Point2Plane(maxPoint, ViewPlanes[3])) {
		return false;
	}
	return true;
}
```



### 1.4 遮挡剔除

![](./images/culled_occlusion.png)

#### 1.4.1 遮挡查询 - 硬件

**一、CPU-Driven 的剔除**

以下方法均可以通过 CPU 读取深度,或者采用硬件提供的 Early-Z 功能在<u>片源着色执行前</u>从 GPU 读取深度

- **中小场景剔除**:将 Mesh 对象的包围盒(或最高 LOD 级别的模型)写入到 z-buffer 上,然后使用物体的包围盒传入到 GPU 进行遮挡测试
  从 GPU 回读数据到 CPU 通常很慢,因此通常会将得到的数据放在下一帧中作为剔除数据来使用,这样遮挡剔除其实是延迟一帧生效的(一般这种延迟影响不大)[OpenGL 的查询方式 Query Object](https://www.khronos.org/opengl/wiki/Query_Object),[DirectX 的查询方式 Predication queries](https://docs.microsoft.com/en-us/windows/win32/direct3d12/predication-queries)

- **大场景剔除 HZB**,Hierarchical z-buffer:多 Mip 层级的 z-buffer
  Pass 1:Level n 的 Mip 记录 Level n-1 中周围**四个像素**中**最远处**的深度值(这样只要符合高等级的 Mip 深度,就能覆盖到低等级)
  Pass 2:根据 Mesh 对象包围盒的大小判断选择哪个 Level 的 Mip,选定 Level 后通过比较 Mip 上的深度值来判断是否被遮挡

![](./images/culled_HZB.png)

**二、GPU-Driven 的剔除**

1. 创建 Indirect 指令队列,将所有待渲染物体的渲染指令录入
2. 对渲染物体进行遮挡剔除,将剔除结果写入到 buffer 中
3. 根据结果 GPU 会选择性执行录入的 Indirect 渲染指令,达到剔除的效果



#### 1.4.2 遮挡查询 - 软件(移动平台)

将 Mesh 对象的包围盒(或最高 LOD 级别的模型)软光栅到 CPU 内存中的 z-buffer 上,然后根据 z-buffer 中 的深度信息来判断那些 Mesh 对象需要剔除
支持任意大小的场景,CPU 端压力较大



#### 1.4.3 静态剔除 - 预计算(移动平台)

![](./images/culled_precomputed.png)

在场景里放入一个**箱体**,在箱体范围内将场景划分成一个个 Cell(适合中小型场景的性能优化)

- Precomputed Visibility Volumes 预计算可见性(无法剔除动态物体)
  每个 Cell 区域预计算出相机在这个 Cell 范围内所有可能看到的物体,并将信息存储下来
  在运行时直接查表来得到所有静态物体的可见信息
- [Portal-Culling](https://www.gamedeveloper.com/programming/sponsored-feature-next-generation-occlusion-culling)(可以剔除动态物体)
  每个 Cell 区域预计算出两个相邻 Cell 之间的连通性,并将信息存储下来
  在运行时根据相机所在的 Cell 间的连通性信息 和 相机观察方向快速计算出目标物体是否处于可见范围内





## 2. 几何阶段

几何阶段里的部分流程(从视锥剔除开始)开发者无法控制,由不同平台的系统驱动自行完成

![](./images/coordinate.png)

### 2.1 Vertex Shader

#### 2.1.1 观察/相机 空间

![](./images/camera_axes.png)

**LookAt 矩阵**:将世界空间坐标 乘以 lookat 矩阵 可以得到相机的 观察空间
$$
LookAt =
\begin{bmatrix}
\color{red}{R_x} & \color{red}{R_y} & \color{red}{R_z} & 0 \\
\color{green}{U_x} & \color{green}{U_y} & \color{green}{U_z} & 0 \\
\color{blue}{D_x} & \color{blue}{D_y} & \color{blue}{D_z} & 0 \\
0 & 0 & 0 & 1
\end{bmatrix}
\begin{bmatrix}
1 & 0 & 0 & \color{orange}{-P_x} \\
0 & 1 & 0 & \color{orange}{-P_y} \\
0 & 0 & 1 & \color{orange}{-P_z} \\
0 & 0 & 0 & 1
\end{bmatrix}
$$
相机对象和 LookAt 矩阵是两套不同的坐标系:
相机对象的坐标系要和自己所处的世界坐标的坐标系保持一致,而 LookAt 的坐标系必须与世界坐标系的 Z 轴方向相反

- 其中,P 为相机的位置、U 为相机的 Y 轴、R 为相机的 X 轴、D 为相机指向的方向和相机的 Z 轴相反
- 相机对象的 Z 轴和 LookAt 矩阵 D 相反,其他轴和 LookAt 矩阵的基坐标相同,同时也和世界坐标的基坐标相同
  因此,如果 相机对象为右手坐标系,LookAt 矩阵为左手坐标系,这样是方便其他方向的移动

```c
// 1. 计算 lookAt 矩阵的坐标系(根据指向的 目标坐标和相机坐标 求得)
glm::vec3 cameraFront = glm::normalize(cameraTarget - cameraPos);		// 指向目标物体
glm::vec3 lookAtDirection = glm::normalize(cameraPos - cameraTarget); 	// 指向相机

glm::vec3 WorldUp = glm::vec3(0.0f, 1.0f, 0.0f); 
glm::vec3 lookAtRight = glm::normalize(glm::cross(WorldUp, lookAtDirection)); // LookAt 为右手坐标系
glm::vec3 lookAtUp = glm::cross(lookAtDirection, lookAtRight);

// 2. 计算相机对象的坐标系(根据欧拉角 Yaw,Pich 求得)
glm::vec3 cameraFront;
cameraFront.x = cos(glm::radians(Yaw)) * cos(glm::radians(Pitch));
cameraFront.y = sin(glm::radians(Pitch));
cameraFront.z = sin(glm::radians(Yaw)) * cos(glm::radians(Pitch));
cameraFront = glm::normalize(cameraFront);

cameraRight = glm::normalize(glm::cross(cameraFront, WorldUp));  // 相机对象为左手坐标系
cameraUp    = glm::normalize(glm::cross(cameraRight, cameraFront));
```



#### 2.1.2 透视/正交 投影变换

将观察空间的坐标转换到投影空间(一个 Frustum 平截头体空间),详见 [矩阵变换,透视投影](../LinearAlgebra/Part1_Matrix.md)

<img src="./images/projection.png" style="zoom: 80%;" />

**OpenGL 的透视投影矩阵为**

- 列主序矩阵
- 相机坐标系为 **右手坐标系**
- Z 的标准设备空间范围限定为 [-w, w]

$$
M_{OpenGL} * P = 
\begin{bmatrix}
near \over right & 0 & 0 & 0 \\
0 & near \over top & 0 & 0\\
0 & 0 & -{{far+near} \over {far - near}} & -{2\cdot far \cdot near \over {far - near}}\\
0 & 0 & -1 & 0\\
\end{bmatrix}
\begin{bmatrix}
x \\ y \\ z \\ 1
\end{bmatrix}
= 
\begin{bmatrix}
{near \over right}x \\ {near \over top}y \\ {-{{far+near} \over {far - near}}}z -{2 \cdot far \cdot near \over {far - near}} \\ -z
\end{bmatrix}
$$

**DriectX  的透视投影矩阵为**

- 行主序矩阵
- 相机坐标系为 **左手坐标系**
- Z 的标准设备空间范围限定为 [0,w]

$$
P * M_{DriectX} = 
\begin{bmatrix}
x & y & z & 1
\end{bmatrix}
\begin{bmatrix}
near \over right & 0 & 0 & 0 \\
0 & near \over top & 0 & 0\\
0 & 0 & {far \over {far - near}} & 1 \\
0 & 0 & -{far \cdot near \over {far - near}} & 0\\
\end{bmatrix}
= 
\begin{bmatrix}
{near \over right}x & {near \over top}y & {{far \over {far - near}}}z -{far \cdot near \over {far - near}} & z
\end{bmatrix}
$$



### 2.2 Tessellation Shader

曲面细分阶段:降低带宽负载,通过较少的 Mesh,经过 曲面细分的算法计算可以生成更精细的 Mesh 网格(方便 Level Of Details 的实现)

1. Tessellation **control** shaders (**H**ull **S**haders) 
2. Tessellation **evaluation** shaders (**D**omain **S**haders)

![](./images/tesselation_pipeline.svg)



### 2.3 Geometry Shader

几何着色阶段:可以由已知的图元生成新的图元(点、线、面),一般多用于

- 由顶点产生的粒子特效
- 几何曲面细分
- [Shadow Volume 加速](https://developer.nvidia.com/gpugems/gpugems3/part-ii-light-and-shadows/chapter-11-efficient-and-robust-shadow-volumes-using)
- 只用一个 Pass 绘制 Cube map(6 个面)



### 2.4 裁剪(硬件实现)

#### 2.4.1 视锥剔除

一个面的三个顶点如果都被剔除,则当前**三角形**被剔除

```c
std::uint8_t checkViewCut(const glm::vec4& v)
{
    auto ret = (std::uint8_t)0;
    
    if      (v.x < -v.w) ret |= 1;
    else if (v.x >  v.w) ret |= 2;
    if      (v.y < -v.w) ret |= 4;
    else if (v.y >  v.w) ret |= 8;
    if      (v.z < -v.w) ret |= 16;
    else if (v.z >  v.w) ret |= 32;
    
    return ret;
}
```



#### 2.4.2 齐次坐标裁剪

![](./images/culled_GPU.png)

裁剪后三角网格的数量和连接方式可能会变(裁剪得到的顶点属性值会提前插值好)
假设 $P(x,y,z,w)$ 为投影空间内部的一个点,则
$$
-1 <= x/w <=1 \\
-1 <= y/w <=1 \\
-1 <= z/w <=1 \\

-w <= x <= w \\
-w <= y <= w \\
-w <= z <= w
$$
由 [线与面的关系判断](../LinearAlgebra/Part3_Triangles.md) 可知,如果线 $Q_1Q_2$ 与面交与点 $I$,则
$$
\begin{align}
Q_1&=(x_1,y_1,z_1,w_1) \\
Q_2&=(x_2,y_2,z_2,w_2) \\\\
I &= Q_1 + t(Q_2 - Q_1) \\
w_1 + t(w_2 - w_1) &= x_1 + t(x_2 - x_1) \\
t &= {{w_1 - x_1} \over (w_1 - x_1) - (w_2 - x_2)} 
\end{align}
$$
注意:为了防止透视除法除的 $w$ 为 0,这里裁剪的时候还要裁剪掉一个 $w=1e-5$ 这样一个极小数的平面

```c
enum AXIS {
    X = 0,
    Y = 1,
    Z = 2,
    W = 3
}

// EX: 0,1,2,3
//     3-0, 0-1, 1-2
void clipInHomoCoord(std::vector<Vertex>& vertIn, std::vector<Vertex>& vertOut, AXIS axis, bool isNegative)
{
    vertOut.clear();

    int preDot = -1;
    int curDot = -1;
    float w = 1.0f;
    float flag = isNegative ? -1.0f : 1.0f;
    for (int i = 0; i < vertIn.size(); ++i)
    {
        Vertex& preVert = vertIn[(i + vertIn.size() -1) % vertIn.size()];
        Vertex& curVert = vertIn[i];
        preDot = flag * preVert.position[axis] <= preVert.position.w ? 1 : -1;
        curDot = flag * curVert.position[axis] <= curVert.position.w ? 1 : -1;
        if (preDot * curDot < 0) // put intersection point first
        {
            w = preVert.position.w - flag * curVert.position[axis];
            w = w / (w - (curVert.position.w - flag * curVert.position[axis]));
            vertOut.push_back( lerp(preVert, curVert, w) ); 
        }
        if (curDot > 0)			// then put original point
        {
            vertOut.push_back(curVert);
        }
    }
}

// how to call clip function
std::vector<Vertex> vertIn;
std::vector<Vertex> vertOut;
clipInHomoCoord(vertIn, vertOut, X, true);	// clip on x axis
clipInHomoCoord(vertOut, vertIn, X, false);
clipInHomoCoord(vertIn, vertOut, Y, true); 	// clip on y axis
clipInHomoCoord(vertOut, vertIn, Y, false);
clipInHomoCoord(vertIn, vertOut, Z, true);	// clip on z axis
clipInHomoCoord(vertOut, vertIn, Z, false);

auto& vertexOut = vertIn;	// output

// draw point order must use trangles fan
GL_TRIANGLE_FAN
```



#### 2.4.3 透视除法和 NDC 空间

通过透视除法将 <u>齐次坐标</u> 转换为的 <u>非齐次坐标</u> 具体见 [矩阵变换,齐次空间](../LinearAlgebra/Part1_Matrix.md)
将坐标点从 投影空间 转换到 NDC (Normalized Device Coordinates  标准设备坐标系)空间

```c
// Scope: [-w, w]
glm::vec4 proj;

// 如果 w 为 0,表示一个三维坐标点,则 令 w 为 1.0,表示一组齐次坐标点
if (0 == proj.w) proj.w = 1e-5f;

// Scope: [-1, 1]
glm::vec4 ndc = glm::vec4(proj.x / proj.w, 
                          proj.y / proj.w, 
                          proj.z / proj.w,
                          1.0f);
```



#### 2.4.4 将坐标映射到屏幕上

**屏幕坐标和像素的映射关系**

- 屏幕坐标是 2D 纹理坐标
  归一化后的裁剪坐标转换到屏幕坐 标的矩阵
  $$
  \begin{bmatrix}
  {width \over 2} & 0 & 0 & {width \over 2} \\
  0 & {height \over 2} & 0 & {height \over 2} \\
  0 & 0 & 1 & 0 \\
  0 & 0 & 0 & 1
  \end{bmatrix}
  $$

- 屏幕坐标表示的是屏幕空间中的像素坐标

- OpenGL 和 DirectX 10 以后的版本认为 像素中心 对应 屏幕坐标的值为 0.5,例:
  屏幕分辨率为 400 X 300,则其屏幕坐标 x 的范围是 [0.5, 400.5],y 的范围是 [0.5, 300.5]
  $$
  Screen_x = (1 + x_{标准设备坐标}) \cdot {Pixel_{width} \over 2} \\
  Screen_y = (1 + y_{标准设备坐标}) \cdot {Pixel_{height} \over 2}
  $$
  



#### 2.4.5 背面剔除 2D

在绘制 Enclosed solid 物体时,有正面自遮挡了其背面的三角面的情况,这时候需要剔除不需要被看到的三角面

1. 在平台的坐标系下,根据三角形两边的叉乘可以知道三角面的法线朝向
2. 根据法线的朝向可以判断当前三角面是正面还是背面
3. 根据需求剔除正面或者背面的三角面,以减少不必要的 drawcall



#### 2.4.6 视口剔除 2D

光栅化时,根据设置的 Viewport 大小来限定光栅化范围(限定逐行光栅化的行数和列数)
以达到只有视口内部的数据被刷新了,避免了不必要的计算





## 3. 着色阶段

### 3.1 光栅化(硬件实现)

1. **三角形设置 Triangle Setup**
   计算三角网格表示数据(每条边上的像素坐标)
2. **三角遍历 Triangle Traversal**
   查找被三角形覆盖的像素,生成一个图元,这一过程又称扫描变换 Scan Conversion
3. **光栅化 Rasterization**
   在三角形中心坐标中,将顶点信息线性差值并根据渲染目标像素个数,对一个三角形图元信息做离散化处理



### 3.2 Early-Z / Pixel Shader

**Pre-Z**:应用阶段实现,需要单独 Pass,只写入深度,开启  Alpha 测试,用来剔除**透明像素**
**Early-Z**(Z-cull):需要硬件支持,只能用来剔除**不透明 像素块(相邻的 4 个像素)**
<u>提前在 Pixel Shader 着色前判断,防止 Pixel Shader 计算后在进行深度测试才被 discard 而带来的不必要计算</u>
使用 Early-Z 需要关闭深度写入,无法进行 Alpha 混合,每次使用的深度需要 Clear 一下
使用 Early-Z 需要**从前向后**渲染不透明物体,才能体现出它的优势



### 3.3 逐像素处理(可配置)

1. **Scissor 测试**
   裁剪测试,查看是否设置裁剪区域,如果有在裁剪区域外的像素会被剔除

1. **Alpha 测试**
   过滤掉那些透明的片段,让剩下的不透明片段来写入缓存
   **提前 Alpha 测试**,在 Pixel Shader 阶段提前根据像素的 alpha 值,使用 GLSL 的 discard 命令剔除像素
   例:一个草的方形图片其透明的部分像素应该被剔除掉
   
1. **Stencil 测试**
   模版测试,根据二值图的模版 buffer 在当前 color buffer 上填充其他颜色
   例:给一张白纸画一个红印章(或文字),这样会使得存储更少,不需要每个像素都存储印章的单一颜色
   ![](./images/stencil_test.png)
   
2. **深度测试**
   深度缓冲中每个像素(或超采样)都有对应的深度值(通过三角形顶点深度信息差值得到)
   深度测试通过,深度缓冲将会更新深度值
   因为每个像素都有深度,所以不会存在两个图元交叉的深度问题
   深度测试之后,Alpha 混合之前,会**更新 Occlusion query**
   ![](./images/depth_test.png)
   
2. **Alpha 混合**
   开启 alpha 混合会关闭深度写入(如果不关闭后面片元将会被踢出,无法进入到 alpha 混合缓解混合颜色)
   但是深度测试依旧可以进行,**深度值对 Alpha 混合来说是只读的**
   
   为了正确的做 alpha 混合,一般流程如下
   
   1. 确保混合的 alpha 物体是凸面体,将复杂的模型拆分成可独立排序的多个子模型
      从而防止循环重叠半透明物体出现
   2. 先渲染所有不透明物体,并且开启他们的深度测试和深度写入
   3. 开启深度测试,关闭深度写入(当前深度值已经确定)
      把半透明物体按照深度值依次排序,**从后向前渲染**(确保半透明物体被不透明物体遮挡)
      但这仍会存在两个图元交叉,导致从后向前渲染排序有误的问题。可以通过添加 alpha 缓存,或者使用排序无关的半透明混合方式(Depth peeling)





# 三、常见问题

## 1. Alpha 预乘

关闭 Alpha 预乘的混合方式(假设:透明物体 B 在 A 前面)
$$
\begin{align}
A &= (A_r,A_g,A_b, \alpha_A)\\
B &= (B_r,B_g,B_b, \alpha_B)\\
M_{rgb} &= \alpha_B B + (1-\alpha_B)\alpha_A A\\
\alpha_M &= \alpha_B + (1-\alpha_B)\alpha_A
\end{align}
$$

开启 Alpha 预乘的混合方式(假设:透明物体 B 在 A 前面)
透明图像边缘是黑色,为了防止在混合多个透明物体时 alpha 遮罩外的颜色由于不是黑色 0,而带来的混合颜色的色差
$$
  \begin{align}
  A' &= (\alpha_A A_r,\alpha_A A_g,\alpha_A A_b, \alpha_A)\\
  B' &= (\alpha_B B_r,\alpha_B B_g,\alpha_B B_b, \alpha_B)\\
  M'_{rgb} &= B' + (1-\alpha_B) A'\\
  \alpha_M &= \alpha_B + (1-\alpha_B)\alpha_A \\
  M_{rgb} &= M'_{rgb} / \alpha _M
  \end{align}
$$

![](./images/alpha_multiply.png)



## 2. Overshading / Quad overdraw

**四个相邻的 [Pixel  Quad](https://www.khronos.org/registry/OpenGL/extensions/NV/NV_shader_thread_group.txt) 是 Early-Z 的最小剔除单位**
**硬件原因**:一个 GPU 内 SM 的 Warp 同时处理相邻的 2x2 个像素(每个线程处理一个像素,但是同指令,不同数据)
四个像素中非当前图元数据的输出值会被丢弃,但其插值数据仍会被使用(例:dFx,dFy)

- 业务场景:由于三角形(相对于像素大小)过于密集同一 drawcall 里,多个图元之间对像素数据的重复绘制
- 解决方法:一般使用 Level Of Details 技术可以缓解

![](./images/Overshading.png)



## 3. GPU 指令依赖

GPU 指令组之间存在数据依赖关系,会拉长指令组之间的执行时间
下图蓝色椭圆气泡 bubble 的产生是为了解决 GPU 指令组之间的数据依赖,bubble 其实际上指空等待

![](./images/GPU_CMD.png)



## 4. Compute Shader

DirectX3D 12 Compute Shader pipeline

![](./images/pipeline_compute_DXD12.png)

[Compute Shader](https://en.wikipedia.org/wiki/Compute_kernel) 指一个过程:多用于卷积核 kernel 的并行加速计算 与 GPU 共享顶点着色器和像素着色器的执行单元,并不局限于图形渲染领域,多用于并行计算领域





# Reference

1. [SIGGRAPH courses](http://advances.realtimerendering.com/)
1. [OpenGL Loading Library](https://www.khronos.org/opengl/wiki/OpenGL_Loading_Library)
2. [OpenGL Tools](http://www.opengl-tutorial.org/miscellaneous/useful-tools-links/)
5. [GLSL Versions](https://github.com/mattdesl/lwjgl-basics/wiki/glsl-versions)
6. [learnopengl-Blending](https://learnopengl-cn.github.io/04 Advanced OpenGL/03 Blending/)
7. [TriangleRasterization](http://www.sunshine2k.de/coding/java/TriangleRasterization/TriangleRasterization.html#algo2)
8. [Platform-specific rendering differences](https://docs.unity3d.com/Manual/SL-PlatformDifferences.html)
9. [Stateless, layered, multi-threaded rendering](https://blog.molecular-matters.com/2014/11/06/stateless-layered-multi-threaded-rendering-part-1/)
10. [Graphics pipeline](https://en.wikipedia.org/wiki/Graphics_pipeline)
10. [Pipeline stall](https://en.wikipedia.org/wiki/Pipeline_stall)
11. [Game Programming Patterns](http://gameprogrammingpatterns.com/contents.html)
12. [Shader detached program](https://github.com/google/gapid/issues/398)
13. [Explaining Homogeneous Coordinates & Projective Geometry](https://www.tomdalling.com/blog/modern-opengl/explaining-homogenous-coordinates-and-projective-geometry/)
14. [Fast Extraction of Viewing Frustum Planes from the WorldView-Projection Matrix](http://link.zhihu.com/?target=http%3A//www8.cs.umu.se/kurser/5DV180/VT18/lab/plane_extraction.pdf)
15. [OpenGL Projection Matrix (songho.ca)](http://www.songho.ca/opengl/gl_projectionmatrix.html)
16. [3D Clipping in Homogeneous Coordinates. | Development Chaos Theory (chaosinmotion.com)](https://chaosinmotion.com/2016/05/22/3d-clipping-in-homogeneous-coordinates/)
17. [Clipping using homegeneous coordinates by James F. Blinn and Martin E. Newell](https://link.zhihu.com/?target=https%3A//fabiensanglard.net/polygon_codec/clippingdocument/p245-blinn.pdf)
18. [CLIPPING by Kenneth I. Joy](https://link.zhihu.com/?target=https%3A//fabiensanglard.net/polygon_codec/clippingdocument/Clipping.pdf)
19. [Clipping implementation](https://link.zhihu.com/?target=https%3A//fabiensanglard.net/polygon_codec/)
20. [Nvidia GPU Programming](https://developer.download.nvidia.cn/GPU_Programming_Guide/GPU_Programming_Guide_G80.pdf)
20. [Per-Sample Processing](https://www.khronos.org/opengl/wiki/Per-Sample_Processing)
20. [OGL-Community LOD Selection](https://community.khronos.org/t/lod-selection/49560)
20. [3D C/C++ tutorials](http://www.3dcpptutorials.sk/index.php)
24. [Developing a Software Renderer Part 1](https://trenki2.github.io/blog/2017/06/06/developing-a-software-renderer-part1/)
25. [Render Hell !!!!!](https://simonschreibt.de/gat/renderhell-book1/)
20. [移动游戏性能优化通用技法](https://www.cnblogs.com/timlly/p/10463467.html)
20. [水平同步 垂直同步](https://blog.csdn.net/hankern/article/details/90344384)
21. [Android 的 16ms 和垂直同步以及三重缓存](https://www.jianshu.com/p/3750db831aca)
21. [计算机图形学补充2:齐次空间裁剪(Homogeneous Space Clipping) - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/162190576)
22. [从零开始的软渲染器(2.5)- 再谈裁剪与剔除 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/97371838)
23. [深入剖析 GPU Early Z 优化](https://zhuanlan.zhihu.com/p/53092784)
24. [【Ogre编程入门与进阶】第十章 Ogre场景管理](https://blog.csdn.net/zhanghua1816/article/details/8130251)



================================================
FILE: ComputerGraphics(OpenGL)/Part1_Light&ShadowInGame.md
================================================
# 一、光

## 1. 颜色计算

常用的计算光照颜色的方法:

- 物体反射的颜色(我们感知到的颜色):光源的颜色 * 物体的颜色
- 多光源的情况下,一般都是将各个光源的颜色累加起来,最后得出最终的颜色
- 同一个光源的光衰减系数是一样的



屏幕上显示的物体颜色可以通过以下公式得出,其中:

- $K_\gamma$:显示屏幕的 gamma 矫正
- $K_{Cam}$:相机的配置(曝光、白平衡)
- $K_i$:当前光源的衰减
- $I_i$:当前像素接受光源能量的比例
- $\Phi_i$:当前光源放射的总能量
- $L_i$:当前光源的颜色

$$
Color_{屏幕} = K_\gamma K_{Cam} \sum_{i=0}^{L_n} K_i I_i \Phi_i L_i
$$



## 2. 光的衰减(体积)

**衰减** Attenuation

随着光线传播距离的增长逐渐削减光的强度,光的衰减的模拟公式(其中 $K_c$、$K_l$、$K_q$ 的取值都是经验值)

- $K_c$ 通常保持为 1.0,它的主要作用是保证分母永远不会比1小,否则的话在某些距离上它反而会增加强度,这肯定不是我们想要的效果
- $K_l$ 与距离值相乘,以线性的方式减少强度
- $K_q$ 与距离的平方相乘,让光源以二次递减的方式减少强度。二次项在距离比较小的时候影响会比一次项小很多,但当距离值比较大的时候它就会比一次项更大了

**光体积** Light Volume

光源能够达到片段的范围(通过光的衰减计算出<u>光的半径</u>),一般通过渲染光的衰减半径的球体来确定光源的影响范围
根据衰减公式可知,衰减值只能无限接近于 0,因此需要通过选定一个衰减的最小值来限定光的范围
一般 $L_{att} = {x \over 256}$,除以 256 是因为默认的 8-bit 帧缓冲可以每个分
$$
\begin{align}
L_{att} &= {1.0 \over K_c + K_l * d + K_q * d^2}\\
K_q * d^2 + K_l * d + K_c &= {1.0 \over L_{att}}\\
K_q * d^2 + K_l * d + K_c - {1.0 \over L_{att}} &= 0\\
d &= {-K_l + \sqrt{K_l^2 - 4*K_q*(K_c - {1.0 \over L_{att}})} \over 2 * K_q}
\end{align}
$$

实际的光的衰减的计算

1. 通过一张 256 * 1 的纹理作为查找表
   通过的**点到光源距离的平方** 来(为了避免开方操作)查找衰减值(Unity 内的光衰减纹理)
2. 使用简化后的数学公式计算衰减(Unity 内使用的计算公式)

$$
L_{att} = {1.0 \over 光源到着色物体的距离 d}
$$



## 3. 光源类型

光源类型,即投光物(Light Caster):将光**投射**(Cast)到物体的光源
在渲染方程中常常称一个具体的光源类型为 **精确光源 punctual lights**  



### 3.1 天光 Sky light

天光 (单位:$cd/m^2$):环境光,模拟场景中带有太阳或阴天的光照环境,不仅可以用于户外环境,也可以给室内环境带来环境光效果

1. 使用固定的场景贴图:多用天空球网格贴图
2. 实时从场景中捕捉生成 Cubemap(立方体图)来生成环境光



### 3.2 自发光 Emissive light

自发光表面 (单位:$cd/m^2$):直接由光源发射的光照,自发光一般为材质的颜色

- 实时渲染中自发光不会作为光源来照亮其他物体(不提供间接光照)
- 天光可以看做自发光的一种



### 3.3 平行光 Directional light

![](./images/irradiance.png)

平行光 (单位:$lux$),又称定向光:光源处于无限远处,所有光线有相同的方向

- 不考虑光源位置,只考虑光的方向
- 表示用方向:平行光从光源发出的方向
- 计算用方向:平行光从片段发出到光源的方向(与平行光的表示相反)



**光照度**物理光照计算公式(其中,$\Phi _e$ 表示光通量,为已知条件)
$$
E_e = {\Phi_e \over A\cos \theta}
$$
实际简化后的计算方式:
使用 Phong 光照模型里的兰伯特漫反射模型<u>乘以固定的系数</u>来计算

```glsl
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir)
{
    vec3 lightDir = normalize(-light.direction);
    
    // diffuse shading
    float diff = max(dot(normal, lightDir), 0.0);
    
    // specular shading
    vec3 reflectDir = reflect(-lightDir, normal);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
    
    // combine results
    vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
    vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
    vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
    
    return (ambient + diffuse + specular);
}

```



### 3.4 点光源 Point light

![](./images/light_point.png)

点光源 (单位:$cd$):光源处于世界中某一个位置的光源,它会朝着所有方向发光,但光线会随着距离逐渐衰减

- 计算用方向:平行光从片段发出到点光源的方向
- 衰减系数:点光源的最终结果需要乘以一个衰减系数



点光源环境下,[球坐标系](https://baike.baidu.com/item/球坐标系) $(r,\theta,\phi)$:
实际输出的光通量 $\Phi _e$ 与实际接收的光强度 $I_e$ 的转换比例为
$$
\begin{align}
\Phi_e &=
\int _0^{2\pi} \int_0^{\pi} sin\theta \space d\theta d\phi \space I_e
= 4\pi \space I_e \\\\
1 \space lm &=4\pi \space cd \\
&\approx  12.6 \space cd
\end{align}
$$

```glsl
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{
    vec3 lightDir = normalize(light.position - fragPos);
    
    // diffuse shading
    float diff = max(dot(normal, lightDir), 0.0);
    
    // specular shading, func reflect need inverse direction of input light
    vec3 reflectDir = reflect(-lightDir, normal);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
    
    // combine results
    vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
    vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
    vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
    
    // attenuation
    float distance = length(light.position - fragPos);
    float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));   

    return (ambient + diffuse + specular) * attenuation;
}
```



### 3.5 聚光灯 Spot light

![](./images/light_spotlight.png)

聚光灯 (单位:$cd$):只朝一个特定方向而不是所有方向照射光线,只有在聚光方向的特定半径内的物体才会被照亮,其它的物体都会保持黑暗

实际简化后的计算方式:

- LightDir:聚光照射到片元的方向

- SpotDir:聚光的方向

- $\phi$ 切光角:聚光的照在物体上光圈的半径大小

- $\theta$ LightDir 和 SpotDir 之间的夹角



**聚光的边缘软化**

- 聚光边缘强度变化:需要一个内切光角和一个外切光角,通过从内到外切光角的过渡来表示聚光强度的变化

- 强度计算公式,其中
  $I$ 为聚光强度,范围是 [0, 1] 
  $\theta$ 为 LightDir 和 SpotDir 之间的夹角
  $\phi$ 为外切光角,$\gamma$ 为内切光角($\phi$、$\gamma$ 一般作为聚光的属性,都是常数)

  $I = {\theta - \phi \over cos\gamma - cos\phi}$



聚光灯环境下,[球坐标系](https://baike.baidu.com/item/球坐标系) $(r,\theta,\phi)$:
实际输出的光通量 $\Phi _e$ 与实际接收的光强度 $I_e$ 的转换比例为
$$
\begin{align}
\Phi_e &=
\int _0^{2\pi} \int_0^x sin\theta \space d\theta d\phi \space I_e
= 2\pi(1-cosx) \space I_e , \space x\in[0,{\pi \over 2}]\\\\
1 \space lm &=2\pi(1-cosx) \space cd , \space x\in[0\degree,90\degree]\\
&\approx  6.3(1-cosx) \space cd
\end{align}
$$

```glsl
vec3 CalcSpotLight(SpotLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{
    vec3 lightDir = normalize(light.position - fragPos);
    
    // diffuse shading
    float diff = max(dot(normal, lightDir), 0.0);
    
    // specular shading
    vec3 reflectDir = reflect(-lightDir, normal);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
    
    // combine results
    vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
    vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
    vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
    
    // attenuation
    float distance = length(light.position - fragPos);
    float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));  

    // spotlight intensity
    float theta = dot(lightDir, normalize(-light.direction)); 
    float epsilon = light.cutOff - light.outerCutOff;
    float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);
    
    return (ambient + diffuse + specular) * attenuation * intensity;
}
```



### 3.6 面光源 Area light

![](./images/light_area.png)

面光源 (单位:$cd$):由空间中的矩形限定,在所有方向上均匀地在其表面区域上发出光,但仅从矩形的一侧发出,类似于方正的聚光灯

> 由于面光源会同时从几个不同的方向照亮对象,因此阴影比其他类型的光更柔和细微
> 可用于创建逼真的路灯或靠近播放器的一排灯
>
> 小面积的光源可以模拟较小的光源(例如室内照明),比点光源具有更逼真的效果



面光源环境下,[球坐标系](https://baike.baidu.com/item/球坐标系) $(r,\theta,\phi)$:
实际输出的光通量 $\Phi _e$ 与实际接收的光强度 $I_e$ 的转换比例为
$$
\begin{align}
\Phi_e &=
\int _0^{2\pi} \int_0^{\pi \over 2} sin\theta \space d\theta d\phi \space I_e
= \pi \space I_e\\\\
1 \space lm &=\pi \space cd \\
&\approx 3.14 \space cd
\end{align}
$$



### 3.7 光域光源 Photometric light

![](./images/light_IES.png)

光域光源:是一种用来描述光源辐射范围和强度的说明文件,可以模拟任何形状和强度的光源
用来模拟一些由于发光物体形状迥异和自身遮挡原因的光源

Illuminating Engineering Society (IES):一种被广泛应用的用来描述光域光源的标准格式 `.ies`

**实现方法**:一般会通过将描述光源辐射范围和强度的 [球坐标系](https://baike.baidu.com/item/球坐标系) $(r,\theta,\phi)$ 转化为笛卡尔坐标系,将 IES 文件转化为 (U, V) 分别为 $(\theta, \space \cos \phi)$ 的纹理

```c
float getIESProfileAttenuation ( float3 L, ShadowLightInfo light )
{
    // Sample direction into light space
    float3 iesSampleDirection = mul ( light . worldToLight , -L);

    // Cartesian to spherical
    // Texture encoded with cos( phi ), scale from -1->1 to 0->1
    float phiCoord = iesSampleDirection.z * 0.5f + 0.5f;
    float theta = atan2(iesSampleDirection.y, iesSampleDirection.x);
    float thetaCoord = theta * FB_INV_TWO_PI ;
    float3 texCoord = float3(thetaCoord, phiCoord, light.lightIndex);
    float iesProfileScale = iesTexture.SampleLevel(sampler, texCoord, 0).r;

    return iesProfileScale;
}

// ...

att *= getAngleAtt (L, lightForward , lightAngleScale , lightAngleOffset );
att *= getIESProfileAttenuation (L, light );
```





# 二、光的反射模型

> 材质反射模型是对 BRDF 光照模型进行简化和理想化后的经验模型

**着色(shading)**:计算某个观察方向出射度的过程,期间需要材质属性、光源信息 和 一个等式(这个等式也称为光照模型)
$$
基础材质模型 = 环境光 f_a + 漫反射 f_d + 镜面反射 f_s
$$


![](./images/material.png)

![](./images/material.jpg)



## 1. 环境光 Ambient

是一个全局光照,同一个场景中的所有物体都使用同样的环境光(一般为常量)

**泛光模型**
即只考虑环境光,这是最简单的**经验**模型,只会去考虑环境光的影响

- $K_a$ 代表物体表面对环境光的反射率
- $I_a$ 代表入射环境光的亮度

$$
I_{Env} = K_a I_a
$$



## 2. 漫反射 Diffuse

物体表面随机散射后反射的光照

### 2.1 Lambert 模型

![](./images/light_diffuse.png)

反射的光线强度 与 表面法线和光源方向夹角 的余弦值 成正比(物体背面的光照不会参与着色计算)

- 计算方法
  两个单位向量的点积  $\hat n \cdot I = |\hat n||I| \cos \theta = \cos \theta$
  反射的光线强度 和 **单位**表面法线和**单位**光源方向 的点积 成正比
- 实际计算公式
  max 函数防止出现表面法线 $n$ 和 光源方向 $I$ 夹角 $\theta$ 大于 90 度的情况(即,光源被物体遮挡的情况)

$$
Color_{diff} = Color_{light} \cdot Color_{材质强度} \max(0, \hat n \cdot I)
$$

### 2.2 Half Lambert 模型

基于 Lambert 模型(物体背面的光照会参与着色计算)

- 计算方法
  不通过限制余弦值的大小而是将余弦值的范围从 [-1, 1] 映射到 [0, 1]
- 实际计算公式

$$
Color_{diff} = Color_{light} \cdot Color_{材质强度} (0.5 + 0.5* \hat n \cdot I)
$$



## 3. 镜面反射 Specular

### 3.1 Phong 模型

> Phong 着色:使用 Phong 光照模型,在**片元着色器**逐像素的计算(使用的顶点法线在当前片面的插值)
> Gouraud 着色:使用 Phong 光照模型,在**顶点着色器**逐顶点的计算(计算量相对较小)

Phong 照模型只关心由光源发射,经过物体表面一次反射后**进入摄像机的光线**

**镜面反射 Specular**:

![](./images/light_reflect.png)



**计算反射方向** $r$,已知法线 $\hat n$ 是单位向量,$L$ 是入射光线 $I$ 到 $\hat n$ 的投影
$$
\begin{align}
|\hat n| &= 1 \\ \\
r + I &= 2 L\\
&= 2(|I|cos \theta) \\
&= 2(|\hat n||I|cos \theta) \\
&= 2(\hat n\cdot I) \\
r &= 2 (\hat n \cdot I) \hat n - I
\end{align}
$$

**高光反射**,已知 观察方向 $\hat v$ 是单位向量

![](./images/light_specular.png)
$$
Color_{spec} = Color_{light} \cdot Color_{高光强度} \max(0, \hat v \cdot r)^{Gloss}
$$
Gloss:光泽度,控制高光区域的亮点(光泽度越大,亮点越小)
max 函数防止出现 $v$ 和 $r$ 夹角 $\theta$ 大于 90 度的情况(即,光源在摄像头后侧的情况)



### 3.2 Blinn-Phong 模型

**Phong 光照的缺点**:
当物体的反光度非常小时,它产生的镜面高光半径足以让相反方向的光线对亮度产生足够大的影响。在这种情况下就不能忽略它们对镜面光分量的贡献了

![](./images/light_over_90.png)



Blinn-Phong 光照模型为了解决 Phong 光照的上述缺点,**计算高光强度的方法改为**计算 **半程向量** 与 法线向量 的夹角的方式

![](./images/light_blinn.png)

$$
\hat h = {I + \hat v \over |I + \hat v|}
$$
Blinn-Phong 的高光强度
$$
Color_{spec} = Color_{light} \cdot Color_{高光强度} \max(0, \hat h \cdot \hat n)^{Gloss}
$$


Blinn-Phong 较 Phong 具有更真实的光照效果

![](./images/light_comparrison.png)

![](./images/light_comparrison2.png)



**代码实现**


```glsl
// VS
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;

out vec3 FragPos;
out vec3 Normal;
out vec2 TexCoords;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main() {
    // Word coordinate
    FragPos = vec3(model * vec4(aPos, 1.0));
    
    // mode 4D to 3D for remove translate
    Normal = mat3(transpose(inverse(model))) * aNormal;  
    TexCoords = aTexCoords;
    
    gl_Position = projection * view * vec4(FragPos, 1.0);
}

// FS
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{
    // lightDir from frag to light
    vec3 lightDir = normalize(light.position - fragPos);
    
    // diffuse shading
    float diff = max(dot(normal, lightDir), 0.0);
    
    // specular shading
    vec3 halfwayDir = normalize(viewDir + lightDir);
    float spec = pow(max(dot(viewDir, halfwayDir), 0.0), material.shininess);
    
    // combine results
    vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
    vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
    vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
    
    // attenuation
    float distance = length(light.position - fragPos);
    float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance)); 

    return (ambient + diffuse + specular) * attenuation;
}
```



## 4. 微平面模型 Microfacet

现实当中大多数物体的表面都会有非常微小的缺陷:微小的凹槽,裂缝,几乎肉眼不可见的凸起,以及在正常情况下过于细小以至于难以使用 Normal map 去表现的细节。尽管这些微观的细节几乎是肉眼观察不到的,但是他们仍然影响着光的扩散和反射

- 平面越粗糙,这个平面上的微平面的排列就越混乱。当我们特指镜面光/镜面反射时,入射光线更趋向于向完全不同的方向发散 (Scatter) 开来
- 平面越光滑,光线大体上会更趋向于向同一个方向反射,造成更小更锐利的反射

![](./images/microfacets2.png)

### 4.1 Oren Nayarh 模型

Lambert 模型由于是理想环境下的光照模拟,不能正确体现物体微表面(特别是粗糙物体)的光照效果
Oren Nayarh 模型考虑到微小平面之间的 相互遮挡 和 互相反射照明,主要对粗糙表面的物体建模,比如石膏、沙石、陶瓷等

![](./images/oren_nayar.jpg)



### 4.2 GGX 模型

GGX 模型所解决的问题是,如何将微平面反射模型推广到表面粗糙的**半透明**材质,从而能够模拟类似于毛玻璃的粗糙表面的**透射**效果,它也提出了一种新的描述微平面**法线**方向分布的函数

![](./images/GGX.png)



## 5. 菲涅尔反射 Fresnel

**菲涅耳反射** Fresnel reflection(反射的光线所占折射和反射的比率)
观察方向和物体表面法线的夹角越大,反射效果越明显

模拟物理效果的近似公式,其中 $v$ 表示视角方向的**单位向量**,$n$ 表示物体表面**单位法线**

![](./images/light_fresenel_reflection.png)

### 5.1 Schlick 模型

Schlick 模型简化了 Phong 模型的镜面反射中的指数运算,它模拟的高光反射效果跟 pow 运算基本一致,且效率比 pow 运算高,采用以下公式替代
其中,$n_1$ 表示入射光线介质的折射率,$n_2$ 表示折射光线介质的折射率
$$
\begin{align}
F_0 &= ({n_1 - n_2 \over n_1 + n_2})^2\\
F   &= F_0+(1−F_o)(1−v \cdot n)^5 \\
\end{align}
$$

实时渲染中,常会用一些 经验公式 Empircial Formular 来代替,其中 bias,scale,power 是控制项
$$
F(v,n) = max(0,min(1, bias + scale * (1- v \cdot n)^{power}))
$$






# 三、 阴影

## 1. 阴影效果分析

**阴影具有近实(边缘锐利清晰),远虚(边缘模糊)的效果**

根据被遮挡程度,阴影的类型可分为:

1. lit 照亮:没有被遮挡
2. umbra 本影区:完全被遮挡
3. penumbra 半影区:部分被遮挡

![](./images/shadow_map.png)



## 2. 阴影映射 Shadow Mapping

注意:

- 由于阴影数据的精度问题,光源距离物体越远效果越好
- 点光源的阴影(透视投影)需要更高的精度和更小的竖直方向的视角
- 法线最好采用法线贴图,顶点法线生成的阴影在一些特殊视角会有阴影形变问题



整体思路

![](./images/shadow_map2.png)



方法:

1. **渲染深度贴图(阴影贴图)**
   以光的位置为视角进行渲染,我们能看到的东西都将被点亮,看不见的是阴影
   以光源的类型选择 正交投影 或者 透视投影
   
   ```c
   // 存储的是实际 Z 的深度值,没有标准化(这个时候的 Z 无法确定输入范围)
   GLuint depthMap;
   glGenTextures(1, &depthMap);
   glBindTexture(GL_TEXTURE_2D, depthMap);
   glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, 
                SHADOW_WIDTH, SHADOW_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); 
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
   ```
   
2. **深度贴图纹理坐标计算**
   世界空间坐标 -> 光源空间坐标 -> 裁切空间的标准化设备坐标-> 根据深度贴图和坐标求出阴影深度值

3. 计算片段是否在阴影之中:若当前坐标的 Z 值比深度贴图的值大,则物体在阴影后面,物体有阴影

   ```c
   // shadow 只能为 0 或 1
   // 阴影中只有环境光,没有高光反射和漫反射
   vec3 lighting = (ambient + (1.0 - shadow) * (diffuse + specular)) * color;
	```

   

重点:

1. 不使用颜色缓冲,不包含颜色缓冲的帧缓冲是不完整的,因此只能禁止颜色缓冲
   并且在片源着色器里什么都不干
   
   ```c
   glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
   glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthMap, 0);
   glDrawBuffer(GL_NONE);
   glReadBuffer(GL_NONE);
   glBindFramebuffer(GL_FRAMEBUFFER, 0);
   ```
   
2. 获取阴影贴图的值为透视投影下的非线性深度值

   **解决方案**:将非线性深度值通过透视投影的逆变换转换为线性深度,[投影矩阵](../LinearAlgebra/Part1_Matrix.md)
   $$
   \begin{align}
   Z_n &= {{far + near} \over {far - near}} +{2 \cdot far \cdot near \over {far - near}}{1 \over Z_{linear}} \\
   (far - near)Z_n &= (far + near) + 2 \cdot far \cdot near {1 \over Z_{linear}} \\
   {(far - near)Z_n - (far + near) \over 2 \cdot far \cdot near} &= {1 \over Z_{linear}} \\
   Z_{linear} &= {2 \cdot far \cdot near \over (far - near)Z_n - (far + near)}
   \end{align}
   $$
   
3. 在**距离光源比较远**时,多个片段会从深度贴图的同一个值中采样
   当光以一定角度朝向物体表面时,物体表面会产生明显的线条样式

   ![](./images/shadow_line.png)

   **解决方案**:阴影偏移(shadow bias)+ 深度纹理的线性采样 + 精度修正
   根据对阴影贴图应用一个**随 物体表面朝向和光线的角度**变化的偏移量

   ```c
   // 1. 阴影偏移
   float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);
   float shadow = currentDepth - bias > closestDepth  ? 1.0 : 0.0;
   
   // 2. 精度问题 
   // 2.1 精度打包
   vec4 bitShift = vec4(1.0, 256.0, 256.0 * 256.0, 256.0 * 256.0 * 256.0);
   const vec4 bitMask = vec4(1.0/256.0, 1.0/256.0, 1.0/256.0, 0.0);
   vec4 rgbaDepth = fract(gl_FragCoord.z * bitShift);
   rgbaDepth -= rgbaDepth.gbaa * bitMask;
   
   // 2.2 精度解包
   
   ```
   
   ![](./images/shadow_acne_bias.png)
   
   这样会带来一个问题 —— 悬浮
   
   ![](./images/shadow_peter_panning.png)
   
   解决悬浮的一种方法:通过在生成阴影深度贴图时采用正面剔除的方式,只保留实体物体背面阴影深度,这样阴影的深度更真实,由于偏移出现的部分多余的阴影也会由于阴影深度的更精确而消失,但是地板的深度会去掉
   
   
   
4. 阴影贴图有一定的范围,无法覆盖所有场景

   ![](./images/shadow_texture_scope.png)

   **解决方案**:让阴影贴图范围外的没有阴影

   1. <u>采样位置超出深度贴图边缘</u>
      将阴影贴图的纹理环绕选项设置为 `GL_CLAMP_TO_BORDER`,给边框设一个较亮的白色(最大深度 1)

   2. <u>深度 Z 的范围超过远平面的裁剪范围 -1.0 ~ 1.0</u>
      首先在片源着色器里判断深度值是否超出 1.0,如果超出,强制设置为无阴影

      

5. 阴影贴图受限于分辨率,画出的阴影有锯齿感

   ![](./images/shadow_soft.png)

   **解决方案**:PCF(percentage-closer filtering)
   计算阴影时,多次进行深度图的采样计算,给做一次 BoxBlur 均值滤波,来模糊阴影边缘的锯齿



## 3. 阴影类型

### 3.1 效率最高的阴影(PPS)

**平面投影阴影** Planar Projected Shadows

TODO: https://zhuanlan.zhihu.com/p/31504088



### 3.2 近实远虚的阴影(PCSS)

#### 3.2.1 Percentage-Closer Soft Shadows

PCF 由于采样区域是固定大小的,因此会在所有地方展示同样形状的软阴影。
为了做到**近实远虚**的效果,我们需要一个系数来控制 PCF 的步长,让近处 PCF步长短(清晰),远处 PCF 步长长(模糊)

![](./images/shadow_PCSS.png)


$$
\begin{align}
{W_{Penumbra} \over W_{Light}} &= {{(d_{Receiver} - d_{Blocker})} \over W_{Blocker}} \\
W_{Penumbra} &= {{(d_{Receiver} - d_{Blocker})}  W_{Light} \over W_{Blocker}}
\end{align}
$$

在计算平均深度时,可以使用 mipmap 来加速平均深度的计算,通过减少采样次数的方式来提高效率

```c++
#define BIAS 		5e-5
#define nSamples 	8

float findAVGBlocker(const vec3& coords, const float& bias)
{
    int blockerCount = 0;
    float totalDepth = 0;
    for (int i = 0; i < nSamples - 2; ++i) {
        vec2 uv = vec2(coords.x, coords.y) + u_offsets[i];
        float shadowMapDepth = sample2D(texDepth, uv);
        if (coord.z > (bias + shadowMapDepth)) {
            totalDepth += shadowMapDepth;
            blockCount += 1;
        }
    }
    
    if (0 == blockCount) {
        return -1.0f;
    } else if (nSamples - 2 == blockCount) {
        return 2.0f;
    } else {
        return totalDepth / float(blockCount);
    }
}

float PercentageCloserSoftShadows(
    const vec3& coords, 
    const vec3& normal, 
    const vec3& lightDir
)
{
    float bias = MAX(BIAS, BIAS * (1.0f - nomral.dot(lightDir)));
    
    // 1. avg blocker depth
    float zBlocker = findAVGBlocker(coords, bias);
    if (zBlocker > EPS) {
        return 1.0f;
    } else if (zBlocker > 1.0f + EPS) {
        return 0.0f;
    }
    
    // 2. penumbra size
    float penumbraScale = (coord.z - zBlocker) / zBlocker;
    
    // 3. filtering
    float sum = 0.0f;
    for (int i = 0; i < nSamples; ++i) {
        vec2 uv = vec2(coord.x, coord.y) + u_offsets[i] * penumbraScale;
        sum += (coord.z > sample2D(texDepth, uv) ? 0.0f : 1.0f);
    }
    
    return sum / nSamples;
}
```

另外还有通过影子都是水平的这个假设 + 概率方差的方式来给 PCSS 计算加速的 Variance Shadow Maps(VSM),以及修正 VSM 漏光问题的 Moment Shadow Mapping(MSM)方法,由于使用场景特定且实现方式复杂等问题,这里不再详述,具体可以看 [实时渲染|Shadow Map:PCF、PCSS、VSM、MSM - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/369710758)



### 3.3 大场景的阴影(CSM)

**阴影贴图**方法对于**大型场景**渲染显得力不从心,很容易出现**阴影抖动**和**锯齿边缘**现象

**级联式纹理映射** Cascaded Shadow Maps(CSM) 方法根据**对象**到**观察者**的距离提供**不同分辨率**的**深度纹理**来解决上述问题

1. 将**相机**的**视锥体**分割成若干部分,然后为分割的每一部分生成**独立**的**深度贴图**
2. 根据物体在场景中的位置对位置附近的两张深度贴图进行采样,根据 深度 距离来对两个采样进行线性插值



### 3.4 点光源阴影 Point Shadows

点光阴影,过去的名字是万向阴影贴图(omnidirectional shadow maps)技术

方法:

1. 渲染深度**立方体**贴图
   将立方体贴图 GL_TEXTURE_CUBE_MAP 绑定到 FBO 上,通过几何着色器,一次绘制 6 个面的贴图
   顶点着色器:将顶点变换到世界空间
   几何着色器:将所有世界空间的顶点变换到 6 个不同的光空间(输入:一个三角形的 3 个顶点)

   ```c
   // 几何着色器
   #version 330 core
   layout (triangles) in;
   layout (triangle_strip, max_vertices=18) out;
   
   uniform mat4 shadowMatrices[6];
   out vec4 FragPos; // FragPos from GS (output per emitvertex)
   
   void main() {
       for(int face = 0; face < 6; ++face) {
           gl_Layer = face; // built-in variable that specifies to which face we render.
           for(int i = 0; i < 3; ++i) { // for each triangle's vertices
               FragPos = gl_in[i].gl_Position;
               gl_Position = shadowMatrices[face] * FragPos;
               EmitVertex();
           }    
           EndPrimitive();
       }
   }
   
   // 片源着色器
   #version 330 core
   in vec4 FragPos;
   
   uniform vec3 lightPos;
   uniform float far_plane;
   
   void main() {
       // get distance between fragment and light source
       float lightDistance = length(FragPos.xyz - lightPos);
   
       // map to [0;1] range by dividing by far_plane
       lightDistance = lightDistance / far_plane;
   
       // write this as modified depth
       gl_FragDepth = lightDistance;
   }
   ```

   

2. 渲染场景
   为了确保 6 个面的深度贴图边缘都对齐,设置透视投影的视角为 90 度

   ```c
   float ShadowCalculation(vec3 fragPos) {
       // Get vector between fragment position and light position
       vec3 fragToLight = fragPos - lightPos;
       // Use the fragment to light vector to sample from the depth map    
       float closestDepth = texture(depthMap, fragToLight).r;
       // It is currently in linear range between [0,1]. 
       // Let's re-transform it back to original depth value
       closestDepth *= far_plane;
       // Now get current linear depth as the length between the fragment and light position
       float currentDepth = length(fragToLight);
       // Now test for shadows
       float bias = 0.05; 
       // We use a much larger bias since depth is now in [near_plane, far_plane] range
       float shadow = currentDepth -  bias > closestDepth ? 1.0 : 0.0;
   
       return shadow;
   }
   ```


![](./images/shadow_point.png)



### 3.5 透明物体的阴影





### 3.6 环境光遮蔽 SSAO

屏幕空间的环境光遮挡 (Screen Space Ambient Occlusion,SSAO)通过将褶皱、孔洞和非常靠近的墙面变暗的方法近似模拟出间接光照(常用来模拟大面积的光源对整个场景的光照 如,下图)

![](./images/light_ssao.png)

方法:在三维物体已经生成二维图片之后计算遮蔽因子

1. 几何阶段:准备输入数据
   **1.1 渲染当前相机范围的 顶点、法线、线性深度 到观察空间下的 G-Buffer(Geometry Buffer)**
   注意:纹理采样使用 `GL_CLAMP_TO_EDGE` 方法,防止采样到在屏幕空间中纹理默认坐标区域之外的深度值
   
   ```c
   // 几何着色器 VS
   #version 330 core
   layout (location = 0) in vec3 position;
   layout (location = 1) in vec3 normal;
   layout (location = 2) in vec2 texCoords;
   
   out vec3 FragPos;
   out vec2 TexCoords;
   out vec3 Normal;
   
   uniform mat4 model;
   uniform mat4 view;
   uniform mat4 projection;
   
   void main() {
       vec4 viewPos = view * model * vec4(position, 1.0f);
       FragPos = viewPos.xyz; // 观察空间
       gl_Position = projection * viewPos;
       TexCoords = texCoords;
       
       mat3 normalMatrix = transpose(inverse(mat3(view * model)));
       Normal = normalMatrix * normal; // 观察空间 -> 切线空间
   }
   
   // 几何着色器 FS
   #version 330 core
   layout (location = 0) out vec4 gPositionDepth;
   layout (location = 1) out vec3 gNormal;
   layout (location = 2) out vec4 gAlbedoSpec;
   
   in vec2 TexCoords;
   in vec3 FragPos;
   in vec3 Normal;
   
   const float NEAR = 0.1; // 投影矩阵的近平面
   const float FAR = 50.0f; // 投影矩阵的远平面
   float LinearizeDepth(float depth) {
       float z = depth * 2.0 - 1.0; // 回到NDC
       return (2.0 * NEAR * FAR) / (FAR + NEAR - z * (FAR - NEAR));    
   }
   
   void main() {    
       // 1. 储存片段的位置矢量到第一个 G 缓冲纹理
       gPositionDepth.xyz = FragPos;
       // 2. 储存线性深度到 gPositionDepth 的 alpha 分量
       gPositionDepth.a = LinearizeDepth(gl_FragCoord.z); 
       // 3. 储存法线信息到 G 缓冲
       gNormal = normalize(Normal);
       // 4. 储存漫反射颜色
       gAlbedoSpec.rgb = vec3(0.95);
   }
   ```
   
   **1.2 计算法向半球采样在切线空间的位置**
   在**切线空间**内,距离**每个片源**半球形范围内随机取固定数量的采样坐标,一般会将采样点靠近分布
   
      ```c
   // 在应用程序初始化中调用
   // 随机浮点数,范围0.0 - 1.0
   std::uniform_real_distribution<GLfloat> randomFloats(0.0, 1.0);
   std::default_random_engine generator;
   std::vector<glm::vec3> ssaoKernel;
   
   GLfloat lerp(GLfloat a, GLfloat b, GLfloat f) {
     return a + f * (b - a);
   }
   
   for (GLuint i = 0; i < 64; ++i) {
     // 半球采样点 x,y ~ [-1, 1], z ~ [0, 1]
     glm::vec3 sample(
       randomFloats(generator) * 2.0 - 1.0, 
       randomFloats(generator) * 2.0 - 1.0, 
       randomFloats(generator)
     );
     sample = glm::normalize(sample);
     sample *= randomFloats(generator);
     GLfloat scale = GLfloat(i) / 64.0;
     // 将更多的注意放在靠近真正片段的遮蔽上,也就是将核心样本靠近原点分布
     scale = lerp(0.1f, 1.0f, scale * scale);
     ssaoKernel.push_back(sample * scale);  
   }
      ```
   
   **1.3 创建随机核心旋转噪声纹理**
   半球内采样位置会被所有片源共享使用,需要通过随机转动来确保在较低采样数量的情况下有较好的采样效果
   由于,对场景中每一个片段创建一个随机旋转向量,会占用大量内存
   因此,创建一个小的随机旋转向量纹理(4X4)<u>像瓷砖一样反复平铺</u>在屏幕上
   
      ```c
   // 纹理生成
   std::vector<glm::vec3> ssaoNoise;
   for (GLuint i = 0; i < 16; i++) {
     glm::vec3 noise(
       randomFloats(generator) * 2.0 - 1.0, 
       randomFloats(generator) * 2.0 - 1.0, 
       0.0f); // 围绕 Z 轴偏移旋转,因此 Z 轴不需要有任何变化
     ssaoNoise.push_back(noise);
   }
      ```
   
2. 光照处理阶段:计算遮蔽因子

   **2.1 SSAO 阶段**
   SSAO  着色器在 2D 的铺屏四边形上运行,它对于每一个生成的片段计算遮蔽值(为了在最终的光照着色器中使用)。由于环境遮蔽的结果是一个灰度值,只需要纹理的红色分量,所以将颜色缓冲的内部格式设置为 `GL_RED`

   ```c
   #version 330 core
   out float FragColor;
   in vec2 TexCoords;
   
   uniform sampler2D gPositionDepth;
   uniform sampler2D gNormal;
   uniform sampler2D texNoise;
   
   uniform vec3 samples[64];
   uniform mat4 projection;
   
   // 最好设置为 uniform
   int kernelSize = 64;
   float radius = 1.0;
   
   void main() {
       // Get input for SSAO algorithm
       vec3 fragPos = texture(gPositionDepth, TexCoords).xyz;
       vec3 normal = texture(gNormal, TexCoords).rgb;
     
     	// 为了将 [0,1] 的屏幕纹理坐标转化为平铺的噪声纹理坐标 [0,1]
   		// 1. 获取随机旋转向量这里需要一个缩放值
   		const vec2 noiseScale = vec2(800.0f/4.0f, 600.0f/4.0f); // 屏幕 = 800x600
       vec3 randomVec = texture(texNoise, TexCoords * noiseScale).xyz;
     
       // 2. 根据随机旋转向量创建正交坐标
       vec3 tangent = normalize(randomVec - normal * dot(randomVec, normal));
       vec3 bitangent = cross(normal, tangent);
       mat3 TBN = mat3(tangent, bitangent, normal);
     
       // 3. 根据每个片源共用的半球体内采样数量计算遮蔽因子
       float occlusion = 0.0;
       for(int i = 0; i < kernelSize; ++i)
       {
           // 3.1 获取半球体内每个采样点位置(切线空间内)
           vec3 sample = TBN * samples[i];     // 切线 -> 观察空间
           sample = fragPos + sample * radius; // 根据偏移步长和方向,计算偏移后的采样点
           
           // 3.2 将观察空间的采样点投影到屏幕上
           vec4 offset = projection * vec4(sample, 1.0); // from view to clip-space
           offset.xyz /= offset.w; 											// perspective divide
           offset.xyz = offset.xyz * 0.5 + 0.5;          // transform to range 0.0 - 1.0
           
           // 3.3 获取采样点对应周围采样的深度值
           float sampleDepth = -texture(gPositionDepth, offset.xy).w;
           
           // 3.4 检测周围采样的深度如果在法向半球采样半径内,则被保留
           // 从而避免:当检测一个靠近表面边缘的片段时,它将会考虑测试表面之下的表面的深度值
           float rangeCheck = smoothstep(0.0, 1.0, radius / abs(fragPos.z - sampleDepth ));
         
           // 3.5 若周围采样的深度比当前观察深度大,则累加遮蔽因子的值
           occlusion += (sampleDepth >= sample.z ? 1.0 : 0.0) * rangeCheck;           
       }
     
       // 4. 均值化遮蔽因子
       occlusion = 1.0 - (occlusion / kernelSize);
       
       FragColor = occlusion;
   }
   ```

   **2.2 模糊环境遮蔽结果**
   重复的噪声纹理再上一步的图中清晰可见,为了创建一个光滑的环境遮蔽结果,需要用 box bluer 来模糊环境遮蔽纹理

   ```c
   // 需要额外创建 FBO 在存储这种后处理效果
   #version 330 core
   in vec2 TexCoords;
   
   out float fragColor;
   
   uniform sampler2D ssaoInput;
   const int blurSize = 4; // use size of noise texture (4x4)
   
   void main() {
      vec2 texelSize = 1.0 / vec2(textureSize(ssaoInput, 0));
      float result = 0.0;
      for (int x = 0; x < blurSize; ++x) {
         for (int y = 0; y < blurSize; ++y) {
            vec2 offset = (vec2(-2.0) + vec2(float(x), float(y))) * texelSize;
            result += texture(ssaoInput, TexCoords + offset).r;
         }
      }
    
      fragColor = result / float(blurSize * blurSize);
   }
   ```

   **2.3 应用遮蔽因子在光照计算中**
   光照模型中的环境光 = 原来的环境光常量 * 遮蔽因子(环境遮蔽纹理中)

   ```c
   #version 330 core
   out vec4 FragColor;
   in vec2 TexCoords;
   
   uniform sampler2D gPositionDepth;
   uniform sampler2D gNormal;
   uniform sampler2D gAlbedo;
   uniform sampler2D ssao;
   
   struct Light {
       vec3 Position;
       vec3 Color;
   
       float Linear;
       float Quadratic;
       float Radius;
   };
   uniform Light light;
   
   void main() {             
       // 从 G 缓冲中提取数据
       vec3 FragPos = texture(gPositionDepth, TexCoords).rgb;
       vec3 Normal = texture(gNormal, TexCoords).rgb;
       vec3 Diffuse = texture(gAlbedo, TexCoords).rgb;
   	  // BoxBlur 后的遮蔽因子
       float AmbientOcclusion = texture(ssao, TexCoords).r;
   
       // Blinn-Phong (观察空间中)
       vec3 ambient = vec3(0.3 * AmbientOcclusion); // 这里我们加上遮蔽因子
       vec3 lighting = ambient; 
       vec3 viewDir  = normalize(-FragPos); // Viewpos 为 (0.0.0),在观察空间中
       // 漫反射
       vec3 lightDir = normalize(light.Position - FragPos);
       vec3 diffuse = max(dot(Normal, lightDir), 0.0) * Diffuse * light.Color;
       // 镜面
       vec3 halfwayDir = normalize(lightDir + viewDir);  
       float spec = pow(max(dot(Normal, halfwayDir), 0.0), 8.0);
       vec3 specular = light.Color * spec;
       // 衰减
       float dist = length(light.Position - FragPos);
       float attenuation = 1.0 / (1.0 + light.Linear * dist + light.Quadratic * dist * dist);
       diffuse  *= attenuation;
       specular *= attenuation;
       lighting += diffuse + specular;
   
       FragColor = vec4(lighting, 1.0);
   }
   ```





# 四、游戏中的可移动性

通过对游戏中可移动性的分类,可以对游戏部分数据进行预先计算以提高游戏 Runtime 效率
从游戏 Runtime 的可移动性上,光源/阴影可以分为:

1. 静态 **Static**:不可移动,属性值(颜色、强度) **不变**
   通过离线烘焙(预计算)的方式来实现,直接使用已经计算好的贴图,运行时无需额外的计算
   例:Lightmap 的使用
2. 静止 **Stationary**:不可移动,属性值(颜色、强度) **可变**
   固定光源使用有范围的区域阴影
3. 可移动 **Movable**:可移动,属性值(颜色、强度) **可变**
   需要配置当前场景对应的阴影偏差,以便于实时计算





# 五、光照渲染路径 Rendering Path

渲染路径:**决定光照**如何应用到 shader 中,是当前渲染目标使用光照的流程

|                  | 前向渲染 Forward                     | 延迟渲染 Deferred                                       |
| ---------------- | ------------------------------------ | ------------------------------------------------------- |
| **场景复杂度**   | 简单                                 | 复杂                                                    |
| **光源支持数量** | 少量光源                             | 多光源                                                  |
| **时间复杂度**   | O(g⋅f⋅l)                             | O(f⋅l)                                                  |
| **显存和带宽**   | 较低                                 | 较高                                                    |
| **硬件要求**     | 低,几乎覆盖100%的硬件设备           | 较高,需 MRT 的支持,需要Shader Model 3.0+              |
| **后处理**       | 无法支持需要法线和深度等信息的后处理 | 支持需要法线和深度等信息的后处理,如 SSAO、SSR、SSGI 等 |
| **画质**         | 清晰,抗锯齿效果好                   | 模糊,抗锯齿效果打折扣                                  |
| **屏幕分辨率**   | 低                                   | 高                                                      |

<u>延迟渲染 比 前向渲染 模糊</u>:因为经历了两次光栅化(几何通道和光照通道),相当于执行了两次离散化,造成了两次信号的丢失,对后续的重建造成影响,以致最终画面的方差扩大



## 1. 前向渲染 Forward

前向渲染路径(Unity 默认渲染路径)

- 优点:实现简单;带宽消耗低;MSAA 和透明渲染支持较好
- 缺点:对大规模的光源支持不好;对需要深度和法线的后期处理算法支持不好

方法:对场景中的每个物体分别进行着色,在 VS 或 FS 对 每个光源逐个进行计算(世界坐标系)并累加到 frame buffer
优化:有些作用程度特别小的光源可以不进行考虑(Unity 中只考虑重要程度最大的前 4 个光源)

```c
// 复杂度 O(Objects * Pixels * lights)
void RenderForword() {
    // 1. 遍历场景中的每个物体
    // Unity3D 4.X 版本中,根据光照对物体的距离采用不同程度的计算
    // 根据距离由近到远,采用的光照计算方式在每个光源中均进行:逐像素计、顶点、求调和函数计算 Spherical Harmonic
    for each(object in ObjectsInScene)
	{
        // 2. 遍历像素(在一个 RT 中的像素) [PS]
        for each(pixel in RenderTargetPixels) {
            color = 0;    
            // 3. 遍历所有灯光,将每个灯光的光照计算结果累加到颜色中
            for each(light in Lights) {
                color += CalculateLightColor(light, pixelData);
            }
        }
        WriteColorToRenderTarget(color);
    } // ObjectsInScene
}
```



### 1.1 Forward+

前向渲染增强,又称 *Tiled Forward Rendering*

- 优点:带宽消耗低;MSAA 和透明渲染支持较好;**支持大规模光源**
- 缺点:需要一个 Pre-Z Pass,一个场景要渲染两遍;对需要法线、深度的后处理支持不好

![](./images/RenderingDeferredTiledBased.png)

方法:
1. <u>PrePass (Depth Only Pass / Early-Z Pass)</u>,用来渲染不透明物体的深度,只写入深度不写入颜色
2. <u>Light Culling Pass</u>,把 Z-Buffer 划分成许多四边形(大小一般是 2 的 N 次方,且长宽不一定相等,称为 Tile)
   根据屏幕划分的范围和深度信息构成 Tile 内场景的包围盒,根据 Tile 包围和光的包围盒求交来剔除无用的光源
3. <u>Shading Pass</u>,每个 Tile 根据各自拥有的 Light list 来计算光照



### 1.2 Cluster Forward

分簇前向渲染 *Cluster Forward Rendering*

方法:和 Tiled Forward 一致,只是在划分 Tile 的场景包围盒上更加精确(屏幕空间 + 深度),进而更精准地裁剪光源,避免深度不连续时的光源裁剪效率降低
主要的细分 tile 场景包围盒子的方法有隐式 Implicit 分簇法(绿色),显式 Explicit 分簇法(蓝色)
以下为 Tiled Forward 的划分方式(红色)与 Clustered 的对比图

![](./images/RenderingClusteredDeferred.png)

优化:*Volume Tiled Forward Rendering*,不使用深度(少了一个 Pass),根据相机投影信息和高度 H,计算出 Tile 的深度从而构成 Volume Tile 和光源求交。相对 Cluster Forward 更粗略但是能快速计算出当前 Tile 的光源影响列表





## 2. 延迟渲染 Deferred

延迟渲染 *Deferred Rendering*

- 优点:支持大规模的光源;针对需要深度、法线的后处理算法友好
- 缺点:带宽、显存消耗大;MSAA 和透明渲染支持不好;需要 MRT 硬件支持

![](./images/RenderingDeferred.png)

方法:将光照处理这一步放在三维物体已经生成二维图片之后进行处理(屏幕坐标系)

1. <u>几何 Pass</u>:渲染所有 几何/颜色 到 G-Buffer(Geometry Buffer)
   G-Buffer:用来存储每个像素对应的 Position,Normal,Diffuse Color 和其他 Material parameters(所有变量都在**世界坐标系**下,同一个场景会渲染多次产生多个 Render Target)

   ```c
   // 复杂度 O(Objects * Pixels)
   void RenderGeometryPass() {
       SetupGBuffer(); // 设置几何数据缓冲区
       
       // 1. 遍历场景(非半透明物体)
       for each(Object in OpaqueAndMaskedObjectsInScene) {
           SetUnlitMaterial(Object);    // 设置无光照的材质
           // 2. 渲染 Object 的几何信息到 GBuffer[VS + PS]
           DrawObjectToGBuffer(Object);
       }
   }
   ```
   
2. <u>光照 Pass</u>:使用 G-buffer 计算场景的光照渲染
   每个 light 需要画一个 light volume,以决定它会影响到哪些 pixel
   
   ```c
   // 复杂度 O(Pixels * lights)
   void RenderLightingPass() {
       BindGBuffer();       // 绑定几何数据缓冲区
       SetupRenderTarget(); // 设置渲染纹理
       
       // 1. 遍历像素(在一个 RT 中的像素) [PS]
       for each(pixel in RenderTargetPixels) {
           // 获取 GBuffer 数据
           pixelData = GetPixelDataFromGBuffer(pixel.uv);
           // 清空累计颜色
           color = 0;    
           // 2. 遍历所有灯光,将每个灯光的光照计算结果累加到颜色中
           for each(light in Lights) {
               color += CalculateLightColor(light, pixelData);
           }
           // 写入颜色到 RT
           WriteColorToRenderTarget(color);
       }
   }
   ```



**多渲染目标** *Multiple Render Targets*, MRT 技术:可以一次渲染完成对像素 位置、颜色、法线等对象信息到多个帧缓冲里

```c
// 1. 一个 FBO 绑定多个 buffer
GLuint gBuffer;
glGenFramebuffers(1, &gBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, gBuffer);
GLuint gPosition, gNormal, gColorSpec;

// - 位置颜色缓冲
glGenTextures(1, &gPosition);
glBindTexture(GL_TEXTURE_2D, gPosition);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, gPosition, 0);

// - 法线颜色缓冲
glGenTextures(1, &gNormal);
glBindTexture(GL_TEXTURE_2D, gNormal);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, gNormal, 0);

// - 颜色 + 镜面颜色缓冲
glGenTextures(1, &gAlbedoSpec);
glBindTexture(GL_TEXTURE_2D, gAlbedoSpec);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGBA, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, GL_TEXTURE_2D, gAlbedoSpec, 0);

// - 告诉OpenGL我们将要使用(帧缓冲的)哪种颜色附件来进行渲染
GLuint attachments[3] = { GL_COLOR_ATTACHMENT0,     GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2 };
glDrawBuffers(3, attachments);

// 2. 片源着色器绘制 buffer
#version 330 core
layout (location = 0) out vec3 gPosition;  // location = 0 和 frame buffer 的 GL_COLOR_ATTACHMENT0 对应
layout (location = 1) out vec3 gNormal;
layout (location = 2) out vec4 gAlbedoSpec;

in vec2 TexCoords;
in vec3 FragPos;
in vec3 Normal;

uniform sampler2D texture_diffuse1;
uniform sampler2D texture_specular1;

void main() {    
    // 存储第一个G缓冲纹理中的片段位置向量
    gPosition = FragPos;
    // 同样存储对每个逐片段法线到G缓冲中
    gNormal = normalize(Normal);
    // 和漫反射对每个逐片段颜色
    gAlbedoSpec.rgb = texture(texture_diffuse1, TexCoords).rgb;
    // 存储镜面强度到gAlbedoSpec的alpha分量
    gAlbedoSpec.a = texture(texture_specular1, TexCoords).r;
}
```



### 2.1 Deferred Lighting

延迟光照,又称 *Light Pre-Pass*

- 优点:相对于 Deferred 可以不需要 MRT 硬件支持;支持 MSAA
- 缺点:带宽、显存消耗大(相对于 Deferred,在几何 Pass 时减少,但 drawcall 多了一次);透明渲染支持不好

方法:

1. <u>几何 Pass</u>:只在 G-Buffer 中存储 深度信息、法线值 RGB 和 粗糙度 Alpha(只需要绘制到 1 个 RT,不需要 MRT)

2. <u>光照 Pass</u>:在 FS 阶段利用 G-Buffer 计算出所必须的 light properties
   Channel RGB 存储:根据 Normal,LightDir, LightColor, 光的衰减系数 得到的漫反射值
   Channel A 存储:根据 Normal,LightDir, LightColor, 光的衰减系数,以及高光强度,观察角度 得到的高光反射值
   高光反射的值本是一个三维数据,通过将颜色转换为亮度的公式转化为一维数据存储
   $$
   lum(x) = 0.2126 x_r + 0.7152x_g + 0.0722x_b
   $$

3.  <u>第二次几何 Pass</u>:将结果送到 forward rendering 渲染方式在 FS 里计算最后的光照效果



### 2.2 Tile-Based Deferred(TBDR)

基于瓦片的延迟渲染

- 优点:相对于 Deferred,在光照 Pass 时带宽消耗减少
- 缺点:显存消耗大;MSAA 和透明渲染支持不好;需要 MRT 硬件支持

![](./images/RenderingDeferredTiledBased.png)

方法:

1. <u>几何 Pass</u>:生成 G-Buffer,这一步和传统 deferred shading 一样
2. <u>光照 Pass</u>:把 G-Buffer 划分成许多四边形(大小一般是 2 的 N 次方,且长宽不一定相等,称为 Tile)
   根据屏幕划分 Tile,构成 Tile 的包围盒,并将其与光的包围盒求交来剔除无用的光源
   对于 G-Buffer 的每个 pixel,用它所在 Tile 的 light list 累加光照计算 shading



### 2.3 Clustered Deferred

分簇延迟渲染 *Clustered Deferred Rendering*

方法:和 TBDR 一致,只是在划分 Tile 的场景包围盒上更加精确(屏幕空间 + 深度),进而更精准地裁剪光源,避免深度不连续时的光源裁剪效率降低
主要的细分 tile 场景包围盒子的方法有隐式 Implicit 分簇法(绿色),显式 Explicit 分簇法(蓝色)
以下为 TBDR 的划分方式(红色)与 Clustered 的对比图

![](./images/RenderingClusteredDeferred.png)





# Reference

- [learnopengl-Lighting Advanced](https://learnopengl-cn.github.io/05 Advanced Lighting/01 Advanced Lighting/)
- [Schlick's approximation](https://en.wikipedia.org/wiki/Schlick's_approximation)
- [Everything has Fresnel](http://filmicworlds.com/blog/everything-has-fresnel/)
- [SIGGRAPH 2012 Course: Practical Physically Based Shading in Film and Game Production](https://blog.selfshadow.com/publications/s2012-shading-course/#course_content)
- [Unity_Shaders_Book](https://github.com/candycat1992/Unity_Shaders_Book)
- [MSDN-Cascaded Shadow Maps](https://docs.microsoft.com/zh-cn/windows/win32/dxtecharts/cascaded-shadow-maps?redirectedfrom=MSDN)
- [GDC: Deferred Shading](http://developer.amd.com/wordpress/media/2012/10/D3DTutorial_DeferredShading.pdf)
- [Deferred lighting approaches](http://www.realtimerendering.com/blog/deferred-lighting-approaches/)
- [Deferred Shading VS Deferred Lighting](https://blog.csdn.net/BugRunner/article/details/7436600)
- [Clustered Deferred and Forward Shading](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.592.3067&rep=rep1&type=pdf)
- [Decoupled deferred shading for hardware rasterization](https://cg.ivd.kit.edu/publications/p2012/shadingreuse/shadingreuse_preprint.pdf)
- [Microfacet Models for Refraction through Rough Surfaces](https://www.cs.cornell.edu/~srm/publications/EGSR07-btdf.pdf)
- [基于物理的渲染—更精确的微表面分布函数 GGX](https://blog.uwa4d.com/archives/1582.html)
- [UE4 BRDF 公式解析](https://zhuanlan.zhihu.com/p/35878843)
- [阴影渲染](https://zhuanlan.zhihu.com/p/102135703)
- [使用顶点投射的方法制作实时阴影](https://zhuanlan.zhihu.com/p/31504088)
- [弧长和曲面面积](https://blog.csdn.net/sunbobosun56801/article/details/78657455)
- [实时阴影技术总结 - xiaOp的博客 (xiaoiver.github.io)](https://xiaoiver.github.io/coding/2018/09/27/实时阴影技术总结.html)
- [Cascaded Shadow Maps(CSM)实时阴影的原理与实现](https://zhuanlan.zhihu.com/p/53689987)



================================================
FILE: ComputerGraphics(OpenGL)/Part2_PhysicalLight.md
================================================
# 一、概率与统计

**随机变量** $X$​:可能取很多不同值的变量

**随机变量分布函数** $X \sim p(x)$​​​​​​​:
连续的分布函数又称**概率密度函数** Probability Density Function(PDF),指不同概率事件下随机变量和概率的映射关系

某一个随机变量 $x$ 对应的概率 $P$​
$$
\begin{align}
离散:P &= p(x), & dx = 1\\
连续:P &= p(x)dx \\
\\
所有概率和:\sum p(x) &= 1
\end{align}
$$
**均值**:统计所有数据得到的结果

**期望** $E$​​:
抽取部分数据得到的**平均概率值**,无限接近于均值
$$
\begin{align}
\lim_{x \to \infty} E[X]&= \bar X \\
离散: E[X] &= \sum _{i=1}^{n} x_ip(x_i),p(x) \geq 0\\
连续: E[X] &= \int_1^n xp(x)dx \\
\\
对于随机变量X,Y \\
Y &= f(X) \\
E[Y] &= E[f(x)] \\
&= \int f(x)p(x)dx
\end{align}
$$
**方差 Variance**:
用来度量随机变量和其期望(即均值)之间的**分散程度**,波动越大,方差越大
$$
\begin{align}
Var(x) 
&= s^2 \\
&= \sum _{i=1}^n(x_i - \bar x)^2f(x) \\
&=E((x - \bar x)^2) \\
&=E(x^2 - 2x\bar x + \bar x^2) \\
&=E(x^2) - 2E(x \bar x) + E(\bar x^2) \\
&=E(x)^2 - 2 \sum x \bar x p(x) + \sum \bar x^2 p(x) \\
&=E(x)^2 - 2 \bar x \sum xp(x) + \bar x^2 \sum p(x) \\
&=E(x)^2 - 2 \bar xE(x) + \bar x^2 \\
&=E(x)^2 - 2E(x)E(x) + (E(x))^2 \\
&=E(x)^2 - (E(x))^2
\end{align}
$$
**协方差**:
衡量两个变量之间的变化方向关系
$$
cov(X,Y) = E(XY) - E(X)E(Y)
$$

## 1. 蒙特卡洛积分

**概率密度函数 PDF  (probability density function)**:随着连续随机变量样本在整个样本集上发生的<u>概率</u>
**累积分布函数 CDF (Cumulative Distribution Function)**:随着连续随机变量而变化的<u>概率积分值</u>,CDF 的导数是 PDF

**大数定理**:抽样检测一部分数据得出的结果虽然不能完全代表整个样品,但结果随着采样数量的增加而逐渐接近

**蒙特卡洛积分** 主要是统计和概率理论的组合。

蒙特卡洛可以帮助我们离散地解决人口统计问题,而不必考虑**所有**人
蒙特卡洛积分在计算机图形学中非常普遍,因为它是一种以高效的离散方式对连续的积分求近似而且非常直观的方法:对任何面积/体积进行采样——例如半球 Ω ——在该面积/体积内生成数量 N 的随机采样,权衡每个样本对最终结果的贡献并求和



## 2. 球协函数





# 二、物理理论

## 1. 光学现象

光线实际上可以被认为是一束没有耗尽就不停向前运动的能量,而光束是通过碰撞的方式来消耗能量

### 间接光照

也称反射照明,通过其他物体反射的光线照亮物体的光照效果
![](./images/light_indirect.png)



### 环境光遮蔽

常用来模拟大面积的光源对整个场景的光照
![](./images/light_AO.png)



### 反射 Reflection

指镜子会反射场景中一摸一样像的效果
![](./images/light_bounce.png)



### 折射 Refraction

光的折射是指光从一种介质斜射入另一种介质时,传播方向发生改变,从而使光线在不同介交界处发生的偏折



### 散射 Scattering

光经过透明物体的折射后聚焦在一定范围上的效果
![](./images/light_caustics.png)



## 2. 材质属性

### 粗糙度 Roughness

用统计学的方法来概略的估算微平面的粗糙程度
表示半程向量(Blinn-Phong 中)的方向与微平面平均取向方向一致的概率

微平面的取向方向与中间向量的方向越是一致,镜面反射的效果就越是强烈越是锐利

![](./images/microfacets.png)



### 透光性
即透明度,描述物质透过光线的程度
物体的透光性主要取决于物质内部结构对外来光子的吸收和散射
散射越多越不透明,散射越少越透明(例:水对光的散射少折射多,为透明的)



### 各向性 
描述物质任意一点的物理和化学等属性跟方向是否相关

**各向同性** isortropy:与方向**无关**
在计算机图形学,特别是实时渲染领域,通常将物体简化成均匀的,即各向同性的

**各向异性** anisortropy:与方向**有关**
金属面经过拉丝后会有各项异性的效果



### 导电性 / 金属度 Metallic
因为导体和非导体如此的不同,通常用金属度来控制一个材质是否为金属
金属度可以用灰度值,也可以用二值图来表示物体表面具有金属特性的位置

**绝缘体**:也称电介质,常见的绝缘体包含干燥的木材、塑料、橡胶、纸张等

**导体**:导电性强,常见的有金属、电解质、液体等
导体有更强的反射,导体的反射光颜色可能会与 Albedo(固有色,金属没有漫反射)不同



## 3. 能量守恒

![](./images/light_energy.jpg)

**能量守恒** Energy Conservation:出射光线的能量永远不能超过入射光线的能量(发光面除外)
入射光线的能量,一部分被镜面反射、(散射后)漫反射出去,一部分被吸收,最后一部分被透射(折射)出去
$$
E_{in} = E_{specular} + E_{diffuse} + E_{absorb} + E_{refract}
$$

根据能量守恒,在不同的材质下可以忽略其他微弱的能量消耗

- 对于光强度,反射 + 折射 = 1.0
- 对于光强度,镜面反射 + 漫反射 = 1.0
- 随着粗糙度的上升镜面反射区域的会增加,但是镜面反射的亮度却会下降






## 4. 辐射量

辐度学和色度学的单位是一一对应的,在 **游戏引擎** 和 **建筑照明** 设计里经常使用**色度学**单位。具体见 [从真实世界到渲染](https://zhuanlan.zhihu.com/p/118272193)
其他相关的辐照单位对应的色度学单位如下:

![](./images/light_equation.svg)

以下主要用辐度学来解释 辐射量

### $Q_e$ 辐射能 Energy

代表单个光子的辐射能,表示为 $Q_e = {hc \over \lambda}$,单位是焦耳 $J$
其中 $h$:Planck 常量(常数),$c$:光的速度(常数),$\lambda$:光的波长



### $\Phi_e$ 辐通量/光通量 $lumens/lm$

辐通量 Radiant flux 又称功率,代表<u>单位时间</u>内发射、接收或传输的能量 $\Phi_e = {dQ \over dt}$,单位是瓦特 $W$

<u>光源发射的全部能量</u>(单位时间内)
1700 lm = 100 W 灯泡的辐射能



### $E_e$ 辐照度/光照度 $lux/lx$
平行光的辐照度 Irradiance:**垂直于光线方向**的 **单位面积** 上 **单位时间** 内穿过的能量,单位是 $W/m^2$

<u>光源发射的能量到达某一表面上的强度</u>(单位时间内)

![](./images/irradiance.png)

与平行光线垂直的平面 $A^\bot$ 上的辐照度 $E_e = {\Phi_e \over A\cos \theta}$
与距离 (单位:米 $m$) 的平方成反比



### $I_e$ 辐强度/光强度 $cd/candela$
辐强度 Radiant intensity:表示**元立体角**内的辐通量大小 $I_e = {d\Phi \over d\Omega}$,单位是 $W/sr$

<u>光源从一个立体角发射出的能量</u>(单位时间)

![](./images/light_solidAngle.jpg)

- Angle 角,圆的弧长比半径

- Solid Angle 立体角,$\Omega = {A \over r^2}$,表示符号 $\Omega / \omega$, 单位 球面度 $sr$
  在[球坐标系](https://baike.baidu.com/item/球坐标系) $(r,\theta,\phi)$ 下,观测点为球心,构造一个单位球面:
  任意物体投影到该单位球面上的投影面积,即为该物体相对于该观测点的立体角

$$
\begin{align}
dA_2 &= rsin \theta d\phi * rd\theta = r^2(sin\theta d\phi d\theta) \\
d\Omega &= {dA_2 \over r^2} = sin\theta d \theta d \phi \\
\Omega &= \int d \Omega \\
\Omega_{总面积} &= \int _0^{2\pi} d\phi \int_0^{\pi} sin\theta d\theta = 4\pi
\end{align}
$$



### $L_e$ 辐亮度/光亮度 $nit(cd/m^2)$

辐亮度 Radiance:表示从**单位立体角**反射出去的**单位面积**上的辐通量,单位 $W/(m^2 \cdot sr)$

<u>光源从一个立体角发射出的能量到达某一个表面,反射出去的能量</u>(单位时间)

![](./images/light_radiance.png)

描述传感器(摄影机,人眼等)感受辐射最常用的量,辐亮度与距离无关
其中,
$\theta$ 入射光线与平面法线的夹角
$A$ 真实的平面面积,是 $A^{\bot}$ 的投影
$A^{\bot}$ 表示垂直光线方向的平面面积
$$
L_e = {d^2 \Phi_e \over d A^\bot \cdot d \Omega} = {d^2 \Phi_e \over cos\theta dA \cdot d\Omega}
$$






# 三、基于物理的渲染

## 2. 渲染方程

**渲染方程**(着色流程)

- 先有了一个渲染方程,才会有对应的材质类型
- 传统的渲染方程为每种材质写一个特定的 Shader
- 为了一个万能的 Shader 就可以渲染大部分类型的材质,需要基于物理的方式来渲染
  **Physically Based Rendering 基于物理**的渲染(非真实的物理渲染)是为了使用一种更符合物理学规律的方式来模拟光线
  <span style="color:red">PBR 并不需要追求和照片一样真实的效果,只是为了有一个近乎万能的渲染方法</span>

![](./images/render_equation.png)



### 2.1 入射光线总量(精确光源)

实际渲染方程在计算时,由于**光源的类型**确定(精确光源 Punctual light sources)可以进一步简化渲染方程
每一个光源的计算的统一公式为

- $f$:双向反射分布函数
- $v$:观察方向
- $n$:法线方向
- $l_i$:光源方向
- $C_i$:光源颜色

$$
L_o(v) = \pi f(l_i, v) C_i(n \cdot l_i)
$$



### 2.2 反射光线总量(BRDF)

**双向反射分布函数** BRDF(Bidirectional Reflectance Distribution Function)
表示有多少比例的光反射到了观察方向上(遵守能量守恒),可以近似的求出每束光线对一个给定了材质属性的平面上最终反射出来的光线所作出的**贡献程度**
$$
f_r = k_d\cdot f_{lambert} + k_s \cdot f_{reflection}
$$

其中,$k_d + k_s = 1$

- $k_d$ 折射光线能量比率

- $k_s$ 反射光线能量比率





## 3. PBR 材质

物理的材质模型遵循 **能量守恒** Energy Conservation:出射光线的能量永远不能超过入射光线的能量(发光面除外)

- 根据能量守恒,随着粗糙度的上升镜面反射区域的会增加,但是镜面反射的亮度却会下降
- 反射/镜面反射 + 折射/漫反射 = 1.0(光强度)



**PBR 材质贴图**

- <u>环境光遮蔽 Ambient Occlusion 贴图</u>(漫反射、高光反射)
  固定光源的辐照度贴图,多用于大场景的环境光
  为表面和周围潜在的几何图形指定了一个额外的阴影因子
  网格/表面的环境遮蔽贴图可以通过实时动态生成,或者由 3D 建模软件提前烘焙生成
- <u>反照率 Albedo 贴图</u>(漫反射)
  为每一个金属的纹素(Texel 纹理像素)指定表面颜色(固有色)或者基础反射率,只包含表面的颜色
  反照率贴图和漫反射贴图的区别,反照率贴图只有固有色没有其他的阴影细节纹理,漫反射贴图是一些细节纹理和固有色的集合
- <u>粗糙度 Roughness / 光滑度 Smoothness 贴图</u>(高光反射)
  微表平面模型的简化,以纹素为单位指定某个表面有多粗糙,粗糙度 = 1.0 – 光滑度
- <u>金属度 Metallic 贴图</u>(高光反射)
- <u>法线 Normal 贴图</u>(高光反射)
  计算反射光线强度时使用
  根据分布方向的 一致 / 非一致性,可模拟出 各项异性 Anisotropic / 各项同性 Isotropic 材质
  ![](./images/light_anisotropic.png)



### 3.1 漫反射

漫反射的**输入颜色**为 Albedo 贴图,它不收光照的影响,没有阴影为物体本来的颜色

1. 不考虑光源方向
   假设在所有方向观察亮度都是相同的,因此 $f_{lambert}$ 是常数

$$
\begin{align}
\int_{\Omega} f_{lambert} L_{Input} cos\theta d\omega' &= L_{Output}\\
L_{Input} f_{lambert} \int_{\Omega} cos\theta d\omega' &= L_{Output}\\
f_{lambert} \int_0^{2\pi} d\phi \int_0^{\pi\over 2} cos\theta d\theta &= 1\\
f_{lambert} \pi &= 1\\
f_{lambert} &= {color \over \pi}, \space color[0,1]
\end{align}
$$

2. 考虑光源方向
   根据入射精确光源计算漫反射分量,可见和简化的 lambert 光照模型计算方法一致

$$
\begin{align}
L_o(v) &= \pi f(l_i, v) C_i(n \cdot l_i) \\
L_{lambert} &= \pi f_{lambert} C_i(n \cdot l_i) \\
&= C_i(n \cdot l_i)
\end{align}
$$



### 3.2 高光反射(微平面模型)

高光反射模型 Cook-torrance,字母 D, F, G 分别代表着一种类型的函数,各个函数分别用来近似的计算出表面反射特性的一个特定部分
$$
f_{cook-torrance} = {DFG \over 4(\omega_o \cdot n)(\omega_i \cdot n)}
$$





**正态分布函数** Normal Distribution Function
用来估算微平面的主要函数,估算在受到表面**粗糙度**的影响下,取向方向与中间向量一致的微平面的数量
以下设给定向量 $h$,通过 NDF 函数 Trowbridge-Reitz GGX 计算与 $h$ 方向一致的概率
$h$:平面法向量 $n$ 和光线方向向量之间的中间向量
$\alpha$:表面的粗糙度(参数)
$$
NDF_{GGXTR}(n,h,\alpha) = {\alpha^2 \over \pi((n\cdot h)^2(\alpha^2-1)+1)^2}
$$
传统的微平面模型的问题:太过于平滑,不能表现小于一个像素的几何级的细节

![](./images/microfacet.jpg)



#### 3.2.2 G 微平面自成阴影

几何函数 Geometry Function:用来描述微平面自成阴影的属性
当一个平面相对比较粗糙的时候,平面表面上的微平面有可能挡住其他的微平面从而**减少表面所反射的光线**

1. 单纯的计算平面遮挡的几何函数可采用 GGX 与 Schlick-Beckmann 近似的结合体
   因此又称为 Schlick-GGX 
   $v$:观察方向
   $\alpha$:表面的粗糙度(参数)
   $k_{direct}$:直接光照
   $k_{IBL}$:IBL(Image based lighting)基于图像的光照

   - 其光源不是可分解的直接光源,而是将周围环境整体视为一个大光源
   - 通常使用(取自现实世界或从 3D 场景生成的)环境立方体贴图 (Cubemap) 
     我们可以将立方体贴图的每个像素视为光源,在渲染方程中直接使用它

   $$
   \begin{align}
   k_{direct} &= {(\alpha + 1)^2 \over 8} \\
   k_{IBL} &= {\alpha ^2 \over 2} \\
   G_{SchlickGGX}(n,v,k) &= {n \cdot v \over (n\cdot v)(1-k) + k}
   \end{align}
   $$

2. 将观察方向(几何遮蔽 Geometry Obstruction)和光线方向向量(几何阴影 Geometry Shadowing)都考虑进去后采用 Smith’s method 方法计算
   $v$:观察方向
   $l$:光线方向
   $$
   G_{Smith}(n,v,l,k) = G_{SchlickGGX}(n,v,k) \cdot G_{SchlickGGX}(n,l,k)
   $$



## 4. 全局光照 Global illumination

全局光照 GI (环境光):主要对以下生活中的现象进行模拟



**离线渲染方案**

1. 路径追踪 
2. 光子映射 Photon Mapping 
3. 辐射度 只能模拟漫反射现象



**实时渲染方案**

- 屏幕空间(SSGI)
  仅限于摄像机视图中的对象和光照,用于生成光照数据
  如果明亮的光源在视野之外或被场景内的物体阻挡,可能会导致不和谐的结果
  1. 屏幕环境光遮蔽
  2. 屏幕空间反射
- 世界空间
  1. 体素 Voxel Cone Tracing
  2. 距离场 Distance Field
  3. 实时光线追踪



**基于图像的照明 IBL**
通常使用(取自现实世界或从3D场景生成的)环境立方体贴图 (Cubemap) ,我们可以将立方体贴图的**每个像素视为光源**,在渲染方程中直接使用它
这种方式可以有效地捕捉环境的全局光照和氛围,使物体**更好地融入**其环境

根据渲染总方程(高光反射模型 Cook-torrance)

- 表示点 $p$ 在 $\omega_o$ 方向被反射出的**辐亮度**的**总和**
- 它包含以 $p$ 为球心的单位半球领域 $\Omega$ 内所有入射方向的 $d\omega_i$ 之和
- 其中
  $\omega_o$ 观察/出射方向
  $\omega_i$ 光线入射方向
  $n\cdot\omega_i$ 入射方向和法线的夹角 $cos\theta$ 值

$$
\begin{align}
L_o(p,\omega_o) &= \int_{\Omega} (k_d{color \over \pi} + k_s{DFG \over 4(\omega_o \cdot n)(\omega_i \cdot n)})L_i(p,w_i)n\cdot\omega_id\omega_i \\
&= \int_{\Omega} k_d{color \over \pi}L_i(p,w_i)n\cdot\omega_id\omega_i + \int_{\Omega} k_s{DFG \over 4(\omega_o \cdot n)(\omega_i \cdot n)}L_i(p,w_i)n\cdot\omega_id\omega_i \\
&= k_d{color \over \pi}\int_{\Omega} L_i(p,w_i)n\cdot\omega_id\omega_i + \int_{\Omega} k_s{DFG \over 4(\omega_o \cdot n)(\omega_i \cdot n)}L_i(p,w_i)n\cdot\omega_id\omega_i \\
&= L_{o 漫反射} + L_{o 镜面反射}
\end{align}
$$

可知

- 漫反射与 物体的位置 和 入射光线方向 有关
- 镜面反射与 物体的位置、入射光线方向、<u>出射光线方向</u> 有关



### 4.1 IBL 漫反射

#### 4.1.1 辐照度图

- 预先烘焙:根据环境贴图计算
  实时计算:预计算在一个<u>固定位置</u>下新的立方体贴图,它在每个采样方向(也就是纹素)中存储漫反射积分的结果,这些结果是通过卷积计算出来的
- 在图的每个像素上通过对光的辐射范围半球 $\Omega$ 上的大量方向进行离散采样并对其辐射度取平均值,来**计算每个输出采样方向的积分**



#### 4.1.2 反射探针(实时计算 IBL)

- 辐照度贴图是从<u>固定位置</u>获得的光照贴图,在不同的室内场景位置中我们会使用不同的辐照度贴图来达到环境光动态变化的效果
  这个固定位置我们称为反射探针
- 根据当前视点的辐照度为:与其距离最近的几个反射探针处辐照度的插值



**IBL 漫反射贴图 制作流程**

1. 读取 hdr 图(从球体投影到平面上的图),转换为距柱状投影图(Equirectangular Map)
   实际读取图片到 float texture 就可以

2. 等距柱状投影图 转换为 立方体贴图
   采用不同的观察空间,从柱状投影图逐个绘制纹理到对应的立方体贴图上(可以通过缩小立方图来提高效率)

   ```c
   #version 330 core
   out vec4 FragColor;
   in vec3 localPos; // 经过 VS 插值后的顶点坐标(模型空间)
   
   uniform sampler2D equirectangularMap;
   
   const vec2 invAtan = vec2(0.1591, 0.3183);
   // 球体 UV 坐标转 笛卡尔 uv 坐标
   vec2 SampleSphericalMap(vec3 v) {
       vec2 uv = vec2(atan(v.z, v.x), asin(v.y));
       uv *= invAtan;
       uv += 0.5;
       return uv;
   }
   
   void main() {       
       vec2 uv = SampleSphericalMap(normalize(localPos)); // make sure to normalize localPos
       vec3 color = texture(equirectangularMap, uv).rgb;
   
       FragColor = vec4(color, 1.0);
   }
   ```

3. 生成辐照度贴图
   计算立方体贴图的卷积,通过对**有限数量的所有方向**采样以近似求解(卷积,离散均匀采样积分)
   下图为球形坐标系
   ![](./images/sphericalcoordinates.png)

   球形坐标系 和 笛卡尔坐标系 的互相转换如下
   $$
   \begin{align}
   x &= rsin\theta cos\phi \\
   y &= rcos\theta \\
   z &= rsin\theta sin\phi \\\\
   r &= \sqrt {x^2 + y^2 + z^2} \\
   \theta &= cos^{-1}{y \over r} \\
   \phi &= tan^{-1}{z \over x} \\\\
   L_o(p,\omega_o) &= k_d{color \over \pi}\int_{\Omega} L_i(p,w_i)n\cdot\omega_id\omega_i \\
   L_o(p,\phi_o, \theta_o) &= k_d{color \over \pi} \int_{\phi = 0}^{2 \pi} \int_{\theta = 0}^{\pi \over 2} L_i(p,\phi_i, \theta_i) cos\theta sin\theta d\phi d\theta
   \end{align}
   $$

   ```c
   vec3 irradiance = vec3(0.0);  
   
   // 根据法线制作 TBN 切线坐标矩阵
   vec3 up    = vec3(0.0, 1.0, 0.0);
   vec3 right = cross(up, normal);
   up         = cross(normal, right);
   
   float sampleDelta = 0.025;
   float nrSamples = 0.0;
   // 半球采样:phi 绕 y 轴 360,theta 绕 z 轴 180
   for(float phi = 0.0; phi < 2.0 * PI; phi += sampleDelta) {
       for(float theta = 0.0; theta < 0.5 * PI; theta += sampleDelta) {
           // 球形坐标 转 笛卡尔坐标 (切线空间)
           vec3 tangentSample = vec3(sin(theta) * cos(phi),  
                                     sin(theta) * sin(phi), 
                                     cos(theta));
           // 切线空间转换为世界空间
           vec3 sampleVec = tangentSample.x * right + 
                            tangentSample.y * up + 
                            tangentSample.z * N; 
   
           irradiance += texture(environmentMap, sampleVec).rgb * cos(theta) * sin(theta);
           nrSamples++;
       }
   }
   irradiance = PI * irradiance * (1.0 / float(nrSamples));
   ```

4. 根据生成的辐照度贴图 模拟菲涅耳效应
   由于 IBL 的漫反射环境来自环境的所有方向,没有一个确定的方向来计算菲涅耳效应
   简化后,用法线和视线之间的夹角计算菲涅耳系数

   ```c
   vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness) {
       return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0);
   }  
   
   vec3 kS = fresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness); 
   ```



### 4.2 IBL 高光反射

**重要性采样**:只在某些区域生成采样向量,该区域围绕微表面半向量,受粗糙度限制

1. 通过低差异序列根据索引整数获得均匀的随机数
2. 根据粗糙度和微表面等属性进行重要性质采样



在实时状态下,对每种可能的 **入射光线** 和 出射光线 的组合预计算该积分是不可行的
**Epic Games 的分割求和近似法**将预计算分成两个单独的部分求解,再将两部分组合起来得到后文给出的预计算结果
$$
\begin{align}
f_r(p, w_i, w_o) &= k_s{DFG \over 4(\omega_o \cdot n)(\omega_i \cdot n)} \\
L_o(p, \omega_o) &= \int_{\Omega} k_s{DFG \over 4(\omega_o \cdot n)(\omega_i \cdot n)}L_i(p,w_i)n\cdot\omega_id\omega_i \\
&= \int_{\Omega} f_r(p, w_i, w_o)L_i(p,w_i)n\cdot\omega_id\omega_i \\
&= \int_{\Omega} L_i(p,w_i)d\omega_i * \int_{\Omega}f_r(p, w_i, w_o)n\cdot \omega_i d\omega_i \\\\
&= 预滤波环境贴图 * 镜面反射积分
\end{align}
$$

1. 制作**预滤波环境贴图**

   它类似于辐照度图,是预先计算的环境卷积贴图,但这次考虑了粗糙度。因为随着粗糙度的增加,参与环境贴图卷积的采样向量会更分散,导致反射更模糊,所以对于卷积的每个粗糙度级别,我们将按顺序把模糊后的结果存储在预滤波贴图的 mipmap 中,注意开启 `glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);  `  让立方体接缝过渡自然

   可以通过 [cmftStudio](https://github.com/dariomanesku/cmftStudio) 或 [IBLBaker](https://github.com/derkreature/IBLBaker) 等工具生成预计算贴图

   ![](./images/ibl_prefilter_map.png)

   ```c
   #version 330 core
   out vec4 FragColor;
   in vec3 WorldPos;
   
   uniform samplerCube environmentMap;
   uniform float roughness;
   
   const float PI = 3.14159265359;
   
   float DistributionGGX(vec3 N, vec3 H, float roughness) {
       float a = roughness*roughness;
       float a2 = a*a;
       float NdotH = max(dot(N, H), 0.0);
       float NdotH2 = NdotH*NdotH;
   
       float nom   = a2;
       float denom = (NdotH2 * (a2 - 1.0) + 1.0);
       denom = PI * denom * denom;
   
       return nom / denom;
   }
   
   // http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html
   // efficient VanDerCorpus calculation
   // 整数变小数:把十进制数字的二进制表示 镜像翻转 到小数点右边
   float RadicalInverse_VdC(uint bits) {
        bits = (bits << 16u) | (bits >> 16u);
        bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
        bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
        bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
        bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
        return float(bits) * 2.3283064365386963e-10; // / 0x100000000
   }
   
   // 低差异序列:根据索引来生成均匀随机数,避免伪随机带来的不均匀采样
   vec2 Hammersley(uint i, uint N) {
   	return vec2(float(i)/float(N), RadicalInverse_VdC(i));
   }
   
   // 重要性采样
   vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness) {
   	float a = roughness*roughness;
   	
     // 根据随机数获得随机角度
   	float phi = 2.0 * PI * Xi.x;
   	float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a*a - 1.0) * Xi.y));
   	float sinTheta = sqrt(1.0 - cosTheta*cosTheta);
   	
   	// 根据随机角度获得半角向量,从球坐标转换为笛卡尔坐标
   	vec3 H;
   	H.x = cos(phi) * sinTheta;
   	H.y = sin(phi) * sinTheta;
   	H.z = cosTheta;
   	
   	// 将半角向量从切线空间转换为世界空间
   	vec3 up        = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
   	vec3 tangent   = normalize(cross(up, N));
   	vec3 bitangent = cross(N, tangent);
   	
   	vec3 sampleVec = tangent * H.x + bitangent * H.y + N * H.z;
   	return normalize(sampleVec);
   }
   
   void main(){		
   	  // 在卷积环境贴图时事先不知道镜面反射方向, 因此假设镜面反射方向总是等于输出方向 w_o
   		// 这意味着掠角镜面反射效果不是很好
       vec3 N = normalize(WorldPos);
       vec3 R = N;
       vec3 V = R;
   
       const uint SAMPLE_COUNT = 1024u;
       vec3 prefilteredColor = vec3(0.0);
       float totalWeight = 0.0;
       
       for(uint i = 0u; i < SAMPLE_COUNT; ++i) {
           // 根据重要性采样随机生成半角向量 H
           vec2 Xi = Hammersley(i, SAMPLE_COUNT);
           vec3 H = ImportanceSampleGGX(Xi, N, roughness);
           vec3 L  = normalize(2.0 * dot(V, H) * H - V);
   
           float NdotL = max(dot(N, L), 0.0);
           if(NdotL > 0.0) {
               float D   = DistributionGGX(N, H, roughness);
               float NdotH = max(dot(N, H), 0.0);
               float HdotV = max(dot(H, V), 0.0);
               float pdf = D * NdotH / (4.0 * HdotV) + 0.0001; 
   
               float resolution = 512.0; // resolution of source cubemap (per face)
               float saTexel  = 4.0 * PI / (6.0 * resolution * resolution);
               float saSample = 1.0 / (float(SAMPLE_COUNT) * pdf + 0.0001);
   
               float mipLevel = roughness == 0.0 ? 0.0 : 0.5 * log2(saSample / saTexel); 
               
               // 根据纹理的 LOD 大小来加载纹理
               prefilteredColor += textureLod(environmentMap, L, mipLevel).rgb * NdotL;
               totalWeight      += NdotL;
           }
       }
   
       prefilteredColor = prefilteredColor / totalWeight;
       FragColor = vec4(prefilteredColor, 1.0);
   }
   ```

2. 制作 **BRDF 积分贴图**
   存储:入射角方向,建议存储为 512 x 512 大小的支持存储 mip 级别的 .dds 文件
   横坐标:BRDF 的输入 $n\cdot \omega_i$(范围在 0.0 和 1.0 之间,$\omega_i$ 为光源到片源方向,$\omega_o$ 为视点到片源方向)
   纵坐标:粗糙度
   将环绕模式设置为  `GL_CLAMP_TO_EDGE` 以防止边缘采样的伪像,并且在 NDC (译注:Normalized Device Coordinates) 屏幕空间四边形上绘制积分贴图

   ![](./images/ibl_brdf_lut.png)

   根据 $n \cdot \omega_o$、表面粗糙度、菲涅尔系数 $F_0$ 来计算 BRDF 方程的卷积
   并且假设在纯白的环境光或者辐射度恒定为 1,为了减少因变量的个数,我们做以下化简
   $$
   \begin{align}
   \int_{\Omega}f_r(p, w_i, w_o)n\cdot \omega_i d\omega_i &= 
   \int_{\Omega}f_r(p, w_i, w_o){F(\omega_o, h) \over F(\omega_o, h)}n\cdot \omega_i d\omega_i\\
   &=\int_{\Omega}{f_r(p, w_i, w_o) \over F(\omega_o, h)}F(\omega_o, h)n\cdot \omega_i d\omega_i\\
   &=\int_{\Omega}{f_r(p, w_i, w_o) \over F(\omega_o, h)}(F_0 + (1-F_0)(1-\omega_o \cdot h)^5)n\cdot \omega_i d\omega_i\\
   设 \space \alpha = (1 - \omega_o \cdot h)^5, \space 则:\\
   &=\int_{\Omega}{f_r(p, w_i, w_o) \over F(\omega_o, h)}n\cdot \omega_i (F_0 + (1-F_0)\alpha) d\omega_i\\
   &=\int_{\Omega}{f_r(p, w_i, w_o) \over F(\omega_o, h)}n\cdot \omega_i (F_0 + \alpha -F_0 *\alpha) d\omega_i\\
   &=\int_{\Omega}{f_r(p, w_i, w_o) \over F(\omega_o, h)}n\cdot \omega_i (F_0 * (1 -\alpha)+ \alpha) d\omega_i\\
   &=\int_{\Omega}{f_r(p, w_i, w_o) \over F(\omega_o, h)}n\cdot \omega_i (F_0 * (1 -\alpha)) d\omega_i + \int_{\Omega}{f_r(p, w_i, w_o) \over F(\omega_o, h)}n\cdot \omega_i \alpha d\omega_i\\
   &=F_0\int_{\Omega}{f_r(p, w_i, w_o) \over F(\omega_o, h)}n\cdot \omega_i (1 -\alpha) d\omega_i + \int_{\Omega}{f_r(p, w_i, w_o) \over F(\omega_o, h)}n\cdot \omega_i \alpha d\omega_i\\
   &=F_0\int_{\Omega}{f_r(p, w_i, w_o) \over F(\omega_o, h)}n\cdot \omega_i (1 -(1 - \omega_o \cdot h)^5) d\omega_i + \int_{\Omega}{f_r(p, w_i, w_o) \over F(\omega_o, h)}n\cdot \omega_i (1 - \omega_o \cdot h)^5 d\omega_i\\
   &=F_0 A + B\\
   \end{align}
   $$
   转换为代码为:

   ```glsl
   float GeometrySchlickGGX(float NdotV, float roughness) {
       // 不使用 IBL 
     	// float a = (roughness + 1.0);
       // float k = (a * a) / 8.0;
     
       // 使用 IBL 后和不用 IBL 这里公式略有不同
       float a = roughness;
       float k = (a * a) / 2.0; 
   
       float nom   = NdotV;
       float denom = NdotV * (1.0 - k) + k;
   
       return nom / denom;
   }
   
   float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) {
       float NdotV = max(dot(N, V), 0.0);
       float NdotL = max(dot(N, L), 0.0);
       float ggx2 = GeometrySchlickGGX(NdotV, roughness);
       float ggx1 = GeometrySchlickGGX(NdotL, roughness);
   
       return ggx1 * ggx2;
   }
   
   vec2 IntegrateBRDF(float NdotV, float roughness) {
       vec3 V;
       V.x = sqrt(1.0 - NdotV*NdotV);
       V.y = 0.0;
       V.z = NdotV;
   
       float A = 0.0;
       float B = 0.0;
   
       vec3 N = vec3(0.0, 0.0, 1.0);
   
       const uint SAMPLE_COUNT = 1024u;
       for(uint i = 0u; i < SAMPLE_COUNT; ++i) {
           // 根据重要性采样随机生成入射光线和反射光线的 半角向量
           vec2 Xi = Hammersley(i, SAMPLE_COUNT);
           vec3 H  = ImportanceSampleGGX(Xi, N, roughness);
           vec3 L  = normalize(2.0 * dot(V, H) * H - V);
   
           float NdotL = max(L.z, 0.0);
           float NdotH = max(H.z, 0.0);
           float VdotH = max(dot(V, H), 0.0);
   
           if(NdotL > 0.0) {
               float G = GeometrySmith(N, V, L, roughness);
               float G_Vis = (G * VdotH) / (NdotH * NdotV);
               float Fc = pow(1.0 - VdotH, 5.0);
   
               A += (1.0 - Fc) * G_Vis;
               B += Fc * G_Vis;
           }
       }
       A /= float(SAMPLE_COUNT);
       B /= float(SAMPLE_COUNT);
       return vec2(A, B);
   }
   
   void main()  {
       vec2 integratedBRDF = IntegrateBRDF(TexCoords.x, TexCoords.y);
       FragColor = integratedBRDF;
   }
   ```

3. 结合预滤波环境和 BRDF 积分贴图,完成 IBL 反射

   ```glsl
   uniform samplerCube prefilterMap; // 预滤波环境贴图
   uniform sampler2D   brdfLUT;  	  // BRDF 积分贴图
   
   void main() {
       [...]
       vec3 R = reflect(-V, N);   
       const float MAX_REFLECTION_LOD = 4.0;
       vec3 prefilteredColor = textureLod(prefilterMap, R,  roughness * MAX_REFLECTION_LOD).rgb;    
     
     	vec3 F = FresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness);
   
       vec3 kS = F;
       vec3 kD = 1.0 - kS;
       kD *= 1.0 - metallic;     
   
       vec3 irradiance = texture(irradianceMap, N).rgb;
       vec3 diffuse    = irradiance * albedo;
   
       const float MAX_REFLECTION_LOD = 4.0;
       vec3 prefilteredColor = textureLod(prefilterMap, R,  roughness * MAX_REFLECTION_LOD).rgb;   
       vec2 envBRDF  = texture(brdfLUT, vec2(max(dot(N, V), 0.0), roughness)).rg;
   	
       // specular 由于已经乘过了菲涅尔系数,所以这里不用乘以 kS
       vec3 specular = prefilteredColor * (F * envBRDF.x + envBRDF.y);
   
       vec3 ambient = (kD * diffuse + specular) * ao; 
       [...]
   }
   ```



### 4.3 屏幕空间反射和平面反射

![](D:/Github/CrashNote/ComputerGraphics(OpenGL)/images/PlanarReflection.png)

**屏幕空间反射 Screen Space Reflections:**

- 优点:效率高,只考虑屏幕内的反射光
- 缺点:可靠性差,无法反射画面外或被遮挡的物体



**平面反射 Planar Reflections:**

- 优点:反射保持了连贯和精准,平面反射能够无视摄像机视角,反射画面外的物体
- 缺点:染开销较高,因为平面反射实际上将从**反射方向**再次对整个场景进行渲染





## 5. PBR 代码实现

预计算的方法 **Precomputation-based methods**

```c
// 方法一:根据统一数据计算 FS
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
in vec3 WorldPos;
in vec3 Normal;

// material parameters
uniform vec3 albedo;
uniform float metallic;
uniform float roughness;
uniform float ao;

// lights
uniform vec3 lightPositions[4];
uniform vec3 lightColors[4];

uniform vec3 camPos;

const float PI = 3.14159265359;

// 3.1 正态分布函数:计算微表面粗糙度(高光区域)
float DistributionGGX(vec3 N, vec3 H, float roughness) {
    float a = roughness*roughness;
    float a2 = a*a;
    float NdotH = max(dot(N, H), 0.0);
    float NdotH2 = NdotH*NdotH;

    float nom   = a2;
    float denom = (NdotH2 * (a2 - 1.0) + 1.0);
    denom = PI * denom * denom;

	  // 避免在 NdotV=0.0 or NdotL=0.0 情况下出现除零错误
    return nom / max(denom, 0.0000001); 
}

// 3.2.1 几何函数:微表面自成阴影的程度
float GeometrySchlickGGX(float NdotV, float roughness) {
    float r = (roughness + 1.0);
    float k = (r*r) / 8.0;

    float nom   = NdotV;
    float denom = NdotV * (1.0 - k) + k;

    return nom / denom;
}
// 3.2.2 同时考虑观察方向和光源方向下的 几何函数值
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) {
    float NdotV = max(dot(N, V), 0.0);
    float NdotL = max(dot(N, L), 0.0);
    float ggx2 = GeometrySchlickGGX(NdotV, roughness);
    float ggx1 = GeometrySchlickGGX(NdotL, roughness);

    return ggx1 * ggx2;
}

// 3.3 菲涅尔方程:不同观察角下反射光线的强度
vec3 fresnelSchlick(float cosTheta, vec3 F0) {
    return F0 + (1.0 - F0) * pow(max(1.0 - cosTheta, 0.0), 5.0);
}

void main() {		
    vec3 N = normalize(Normal);
    vec3 V = normalize(camPos - WorldPos);

    // 1. 计算基础反照率:根据金属度来计算高光色是折射的固有色还是反射的高光色
    //    在菲涅尔反射中作为某类材质的固定参数使用
    vec3 F0 = vec3(0.04); 
    F0 = mix(F0, albedo, metallic);

    // 2. 前向渲染:使用双向反射分布函数 BRDF,累计处理每个光源的光照强度
    vec3 Lo = vec3(0.0);
    for(int i = 0; i < 4; ++i) {
      
        // 2.1 根据光体积,计算光源的光照强度
        vec3 L = normalize(lightPositions[i] - WorldPos);
        vec3 H = normalize(V + L);
        float distance = length(lightPositions[i] - WorldPos);
        float attenuation = 1.0 / (distance * distance);
        vec3 radiance = lightColors[i] * attenuation;

        // 2.2 计算双向反射分布函数的 Cook-Torrance
        float NDF = DistributionGGX(N, H, roughness);   
        float G   = GeometrySmith(N, V, L, roughness);      
        vec3  F   = fresnelSchlick(clamp(dot(H, V), 0.0, 1.0), F0);
           
        vec3 nominator    = NDF * G * F; 
        float denominator = 4 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0);
        // 避免在 NdotV=0.0 or NdotL=0.0 情况下出现除零错误
        vec3 specular = nominator / max(denominator, 0.001); 
        
        // 2.3 计算光的辐射率强度,不用 Blinn-Phone 因为它不遵循能量守恒,更像是 BRDF 的替代简化版
        float NdotL = max(dot(N, L), 0.0);  
      
        // 2.4 计算反射和折射系数
        // kS 镜面反射强度:源于菲涅尔方程
        vec3 kS = F;
        // kD 漫反射强度(折射强度):1.0 - 高光反射
        // 这个能量守恒总量是 1.0,要大于 1.0 除非是自发光物体
        vec3 kD = vec3(1.0) - kS;
        // kD 要考虑金属材质:因为金属不会折射光线,因此不会有漫反射
        kD *= 1.0 - metallic;	        

        // 4. 计算出射光的反射强度总量
        // Cook-Torrance 方程中的 F 就是 ks,因此方程的结果 specular 已经计入了 ks,不需要再次乘以 ks
        Lo += (kD * albedo / PI + specular) * radiance * NdotL;
    }   
    
    // 环境光照强度(将会被 IBL 基于图像的环境光代替)
    vec3 ambient = vec3(0.03) * albedo * ao;

    vec3 color = ambient + Lo;
    color = color / (color + vec3(1.0)); // HDR 色调映射
    color = pow(color, vec3(1.0/2.2)); 	 // gamma 矫正
  
    FragColor = vec4(color, 1.0);
}

// 方法二:根据贴图计算 FS
uniform sampler2D normalMap;
uniform sampler2D albedoMap;
uniform sampler2D metallicMap;
uniform sampler2D roughnessMap;
uniform sampler2D aoMap;

// ...

// 将法线向量从 切线空间 转换为 世界空间
vec3 getNormalFromMap() {
    vec3 tangentNormal = texture(normalMap, TexCoords).xyz * 2.0 - 1.0;

    vec3 Q1  = dFdx(WorldPos);
    vec3 Q2  = dFdy(WorldPos);
    vec2 st1 = dFdx(TexCoords);
    vec2 st2 = dFdy(TexCoords);

    vec3 N   = normalize(Normal);
    vec3 T  = normalize(Q1*st2.t - Q2*st1.t);
    vec3 B  = -normalize(cross(N, T));
    mat3 TBN = mat3(T, B, N);

    return normalize(TBN * tangentNormal);
}

// ...

void main() {		
    // 从纹理中获取多变的材质贴图
    // albedo 从贴图的非线性 sRGB 空间转化为线性的 RGB 空间
    vec3 albedo     = pow(texture(albedoMap, TexCoords).rgb, vec3(2.2));
    float metallic  = texture(metallicMap, TexCoords).r;
    float roughness = texture(roughnessMap, TexCoords).r;
    float ao        = texture(aoMap, TexCoords).r;

    vec3 N = getNormalFromMap();
    vec3 V = normalize(camPos - WorldPos);
    
    vec3 F0 = vec3(0.04); 
    F0 = mix(F0, albedo, metallic);

    // 反射方程
    vec3 Lo = vec3(0.0);
    for(int i = 0; i < 4; ++i) {
        // ...
    }   
    
    // ...
}
```





# 四、屏幕色彩校正(后处理)

**后处理体积** *Post Processing Volume*:为了能在三维空间能更清楚的限制后处理范围,通过设置后处理体积(和场景内物体求交,排除多余的物体)来提高后处理效率,明确后处理对象范围



## 1. 自动曝光(眼部适应)

曝光:单位时间接收到的光的辐通量
自动曝光:可再现人眼适应不同光照条件的体验,例如从昏暗的室内走到明亮的室外,或从室外走到室内

游戏引擎中的自动曝光分别有以下方式:

1. **基本的自动曝光方法**(根据当前屏幕画面实时测光)
   测光值:计算屏幕内场景亮度对数的平均值
   对输出到屏幕的纹理做下采样,取当前像素上下左右四个像素的值为平均值(边缘纹理拉伸采样)
   根据下采样纹理宽高各取一半继续下采样,一次次下采样下来最后得到一个 1x1 的纹理,这个纹理颜色就是当前场景内的平均亮度

2. **根据直方图计算自动曝光**(根据当前屏幕画面实时测光)
   测光值:计算屏幕内场景亮度对数的直方图,通过分析直方图得出亮度平均值
   原图像长宽各缩小一半后,通过 GPU 并发的统计这张图的直方图存成纹理,供 CPU 程序使用

3. **手动调节自动曝光**(不测光)
   通过手动设置:感光度 ISO、快门速度 Shutter Speed、光圈大小 Aperture(F-stop)、曝光补偿 Exposure compensation
   EV100:表示感光度为 100 时的曝光强度,是计算曝光的基准数值
   其中如果不考虑[物理相机曝光](../../Blog/base/Part3_Camera.md)的情况,EV100 的值取 0
   $$
   \begin{align}
   EV100 &= log_2{光圈^2 \over 快门速度} - log_2{ISO \over 100}\\
   曝光 &= {1 \over 2^{(EV100 + 曝光补偿)}} \\\\
   屏幕像素 &= 曝光 * 物体经过PBR后的表面亮度
   \end{align}
   $$
   

## 2. Gamma 矫正

![](./images/gamma_correction_gamma_workflow.png)

作用:我们在应用中配置的亮度和颜色是基于监视器所看到的,这样所有的配置实际上是非线性的亮度/颜色配置

**源起**:

1. 人眼看到的颜色亮度空间变化是**非线性**的
2. 我们用来记录/展示画面的媒介上,动态范围和灰阶预算是有限的。(无论**纸张**还是屏幕)
3. **韦伯定律**
   **人对自然界刺激的感知,是非线性的,外界以一定的比例加强刺激,对人来说,这个刺激是均匀增长的**

早期 CRT 阴极射线管显示器:显示的颜色亮度空间变化和人眼看到的基本相似,也是**非线性**的

不经过 Gamma 矫正的光照是不符合真实显示器的情况(左图 Gamma 矫正后的光照,右图没有经过后处理的原始光照)

![](./images/gamma_correction_light.png)



**1. Gamma 曲线就是把物理光强和美术灰度做了一个幂函数映射**
Gamma 曲线就是将在显示器选中的颜色经过矫正后成为线性的便于计算,最后通过显示器又显示出来
曲线如下图:

- 灰色点线:线性颜色/亮度值
- 红色虚线:gamma 矫正曲线
- 红色实线:人眼和 CRT 显示器看到的效果

![](./images/gamma_correction_gamma_curves.png)

**2. sRGB 纹理**

是一种非线性纹理,将线性空间的图片经过显示器一样的 gamma 处理后得到的图片
非线性纹理在进行线性混合时会出现混合错误,原因如下($x^{1 \over \gamma}$ 非线性纹理像素值)
$$
0.5 * (x^{1 \over \gamma} + y^{1 \over \gamma}) < (0.5 * (x+y))^{1 \over \gamma}
$$
![](./images/gamma_correction_blender.png)

使用方法:

1. 开启 OpenGL 自己的 sRGB 帧缓冲,在颜色存储到缓冲前会先 gamma 2.2 矫正 sRGB 颜色

   ```C++
   // 开启 sRGB 帧缓冲
   glEnable(GL_FRAMEBUFFER_SRGB);
   
   // 纹理格式设置为 sRGB,这样在读取 sRGB 图片的时候会首先做一个逆向的 gamma 矫正
   // 防止最后的 sRGB 缓冲统一 gamma 矫正的时候,在这个纹理上进行重复矫正
   glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
   ```

2. 在 frame shader 里自定义 gamma 矫正

   ```c++
   void main() {
       // do super fancy lighting 
       [...]
     
       float gamma = 2.2;
       // 1. 对于普通格式的纹理导入了 sRGB 图片,进行反向 gamma 矫正,防止 2. 统一 gamma 矫正时,做了重复的 gamma 矫正
   		vec3 diffuseColor = pow(texture(diffuseSRGB, texCoords).rgb, vec3(gamma));
       // 2. 对线性空间的颜色进行 gamma 矫正,让显示器显示的和实际计算的颜色一致
       fragColor.rgb = pow(fragColor.rgb, vec3(1.0/gamma));
   }
   ```



## 3. High Dynamic Range 高动态范围

**源起**:人眼的工作原理,当光线很弱的啥时候,人眼会自动调整从而使过暗和过亮的部分变得更清晰,就像人眼有一个能自动根据场景亮度调整的自动曝光滑块

HDR 渲染的真正优点在庞大和复杂的场景中应用复杂光照算法会被显示出来

1. 浮点帧缓冲:可以存储超过 0.0 到 1.0 范围的浮点值

   ```c++
    // GL_RGB16F 格式的浮点帧缓冲
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL);  
   ```

2. **色调映射** Tone Mapping:将所有的浮点颜色通过一些方法映射到 <u>Low Dynamic Range</u> 0.0 - 1.0 的范围中,是一种模拟胶片对光线反应的方法,该方法要符合[学院色彩编码系统(ACES)](http://www.oscars.org/science-technology/sci-tech-projects/aces)针对电视和电影设定的行业标准

   ```c++
   uniform float exposure; // 无确定范围,曝光值
                           // 越高:暗部细节越多
                           // 越低:亮部细节越多
   void main() {
       const float gamma = 2.2;
       vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb;
   
       // Tone Mapping 方法 1: Reinhard 色调映射
       // 分散整个 HDR 颜色值到 LDR 颜色值上,所有的值都有对应
       vec3 mapped = hdrColor / (hdrColor + vec3(1.0));
       // Tone Mapping 方法 2: 曝光色调映射
       vec3 mapped = vec3(1.0) - exp(-hdrColor * exposure);
   
       // Gamma 校正
       mapped = pow(mapped, vec3(1.0 / gamma));
   
       color = vec4(mapped, 1.0);
   }  
   ```



## 4. 颜色查找表 LUT

颜色查找表 ([LookUp Table](https://zhuanlan.zhihu.com/p/43241990)):将一种颜色映射为另一种颜色,可以提供更精细的色彩变换,从而可用于去饱和度之类的用途

注意:LUT 发生在低动态范围(LDR)中以及在 sRGB 空间中输出到显示器的最终图像颜色上,所以它只是与显示器支持对应的一张适时的快照,**不一定在它输出到的所有显示器上都呈现相同外观**

下图为 256 * 1 像素的渐进纹理和其对应的效果

![](./images/LookupTable.png)





# 五、离线渲染

## 1. 光线追踪 Ray Tracing

优点:真实,多用于离线渲染
缺点:计算量大

前提:

- **假设**光线近似直线传播

- **假设**光线交叉后仍然互不影响

- 光路可逆:从光源到人眼的路径 == 从人眼到光源

  

### 1.1 Whitted-Style Ray Tracing

方法

1. 从相机出发,向场景投射光线
2. 将场景进行合理分割,方便快速找到光线与物体的相交点
3. 判断光线与距离相机最近的地方相交(反射),在相交处计算物体颜色
4. 光线会折射多次,在每一次折射点计算颜色值
   ![](./images/ray_tracing.png)



### 1.2 渲染方程推导

![](./images/ray_tracing_rendering_equation.png)

折射点渲染方程推导:

1. 考虑**自发光物体 Emission** 的光照

2. 考虑多个光源的光照

3. 考虑到面光源,将**累加 sum** 替换为**积分 integral** 更准确

4. 考虑到其他物体反射的光线(**间接光照 inter reflection**)

5. 渲染方程化简
   $$
   \begin{align}
   设:\\
   E &= L_e(x, \omega_r)\\
   L &= L_r(x, \omega_r) =L_i(x, \omega_i)\\
   K &= \int_{\Omega}f(x,\omega_i, \omega_r) \cos \theta_i d\omega_i \\
   则 \space 渲染方程简化为:\\
   L &= E + KL \\
   L - KL &= E \\
   (I - K)L &= E \\
   L &= (I - K)^{-1} E \\
   L &= (I + K + K^2 + K^3 + ...)E \\
   L &= E + KE + K^2E+ K^3E + ... \\
   其中:\\
   直接光照 &= KE \\
   间接光照 &= K^2E \\
   二次间接光照 &= K^3E \\
   ...
   \end{align}
   $$



## 2. 路径追踪 Path Tracing







# Reference

- [概率密度函数(PDF)](https://www.jianshu.com/p/70b188d512aa)
- [Render Hell !!!!!](https://simonschreibt.de/gat/renderhell-book1/)
- [3D C/C++ tutorials](http://www.3dcpptutorials.sk/index.php)[3D C/C++ tutorials](http://www.3dcpptutorials.sk/index.php)
- [smallpt: Global Illumination in 99 lines of C++](http://www.kevinbeason.com/smallpt/)
- [Physically Based Rendering](http://www.codinglabs.net/article_physically_based_rendering.aspx)
- [Physically Based Rendering - Cook–Torrance](http://www.codinglabs.net/article_physically_based_rendering_cook_torrance.aspx)
- [Rendering the world of Far Cry 4](http://www.gdcvault.com/play/1022235/Rendering-the-World-of-Far)
- [SIGGRAPH 2014 Moving Frostbite to PBR - Frostbite](https://www.ea.com/frostbite/news/moving-frostbite-to-pb)
- [The Mathematics of Shading](https://www.scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/mathematics-of-shading)
- [Physically Based Shading and Image Based Lighting](https://www.trentreed.net/blog/physically-based-shading-and-image-based-lighting/)
- [Disney animation papers](https://www.disneyanimation.com/technology/publications/#papers)
- [PHYSICALLY-BASED RENDERING REVOLUTIONIZES PRODUCT DEVELOPMENT](https://pny.com/File Library/Unassigned/Moor-Whitepaper-Download.pdf)
- [A MultiAgent System for Physically based Rendering Optimization](http://www.weiss-gerhard.info/publications/D02.pdf)
- [Physically Based Shading on Mobile](https://medium.com/spaceapetech/physically-based-shading-on-mobile-d7d4e90bb4bd)
- [Applying Visual Analytics to Physically-Based Rendering](http://cg.ivd.kit.edu/publications/2018/visual_analytics_pbr/preprint.pdf)
- [An Inexpensive BRDF Model for Physically based Rendering](http://mathinfo.univ-reims.fr/IMG/pdf/An_inexpensive_BRDF_model_for_Physically-based_rendering_-_Schlick.pdf)
- [Optimizing PBR](https://community.arm.com/cfs-file/__key/communityserver-blogs-components-weblogfiles/00-00-00-20-66/siggraph2015_2D00_mmg_2D00_renaldas_2D00_slides.pdf)
- [SubSurface Profile Shading Model](https://docs.unrealengine.com/en-us/Engine/Rendering/Materials/LightingModels/SubSurfaceProfile)
- [Physically Based Materials in Unreal Engine 4](https://docs.unrealengine.com/en-us/Engine/Rendering/Materials/PhysicallyBased)
- [A Multi-Ink Color-Separation Algorithm Maximizing Color Constancy](https://pdfs.semanticscholar.org/9e56/8b13ea51ca3c669186624566f672eb547857.pdf)
- [Unidirectional Reflectance of Imperfectly Diffuse Surfaces](https://www.onacademic.com/detail/journal_1000035238254910_7744.html#)
- [Adopting a physically based shading model](https://seblagarde.wordpress.com/2011/08/17/hello-world/)
- [SaschaWillems / Vulkan-glTF-PBR](https://juejin.im/repo/5a8127a4f265da02d800abba)
- [The Beginner’s Guide to Physically Based Rendering in Unity](https://blog.teamtreehouse.com/beginners-guide-physically-based-rendering-unity)
- [Image Based Lighting](https://chetanjags.wordpress.com/2015/08/26/image-based-lighting/)
- [Using Image Based Lighting (IBL)](https://www.indiedb.com/features/using-image-based-lighting-ibl)
- [Converting a Cubemap into Equirectangular Panorama](https://stackoverflow.com/questions/34250742/converting-a-cubemap-into-equirectangular-panorama)
- [Does PBR incur a performance penalty by design?](https://computergraphics.stackexchange.com/questions/1568/does-pbr-incur-a-performance-penalty-by-design)
- [Lec 2: Shading Models](http://www.cs.cornell.edu/courses/cs5625/2013sp/lectures/Lec2ShadingModelsWeb.pdf)
- [Unity Blog !!](https://blog.unity.com/)
- [Unity_Shaders_Book](https://github.com/candycat1992/Unity_Shaders_Book)
- [使用顶点投射的方法制作实时阴影](https://zhuanlan.zhihu.com/p/31504088)
- [弧长和曲面面积](https://blog.csdn.net/sunbobosun56801/article/details/78657455)
- [深入浅出基于物理的渲染一](https://zhuanlan.zhihu.com/p/33630079)
- [Unity 手册/图形/图形概述](https://docs.unity3d.com/cn/current/Manual/RenderingPaths.html)
- [彻底看懂 PBR/BRDF 方程](https://zhuanlan.zhihu.com/p/158025828)
- [PBR 材质系统原理简介](https://blog.csdn.net/weixin_42660918/article/details/80989738)
- [BRDF 材质贴图](https://blog.csdn.net/mconreally/article/details/50629098)
- [BRDF(双向反射分布函数)](https://zhuanlan.zhihu.com/p/21376124)
- [PBR 材质基础概念,限制及未来发展](https://blog.csdn.net/qq_42145322/article/details/100621811)
- [【Unity】Compute Shader 计算 BRDF 存储到纹理](https://www.cnblogs.com/jaffhan/p/7389450.html)
- [Create icosphere mesh by code](http://blog.andreaskahler.com/2009/06/creating-icosphere-mesh-in-code.html)
- [低差异序列(一)- 常见序列的定义及性质](https://zhuanlan.zhihu.com/p/20197323?columnSlug=graphics)
- [【图形学】我理解的伽马校正(Gamma Correction)](https://blog.csdn.net/candycat1992/article/details/46228771/)
- [A Standard Default Color Space for the Internet - sRGB](https://www.w3.org/Graphics/Color/sRGB)
- [为什么线性渐变的填充,直方图的两头比中间高? - 黄一凯的回答 - 知乎](https://www.zhihu.com/question/61996849/answer/193452971)



================================================
FILE: ComputerGraphics(OpenGL)/Part3_Texture.md
================================================
# 一、颜色插值

根据三角形三个顶点的颜色来求整个三角面的插值颜色

## 1. 普通的插值方法

方法一:根据**高度比**来插值

![](./images/linear_interpolate_triangle.png)
$$
\begin{align}
f_i &= d_i / h_i \\
Color &= f_i * Color_i + f_j * Color_j + f_k * Color_k
\end{align}
$$


方法二:根据所占**三角形面积**来插值
$$
\begin{align}
f_i &= {area(x, x_j, x_k) \over area(x_i, x_j, x_k)} \\
Color &= f_i * Color_i + f_j * Color_j + f_k * Color_k
\end{align}
$$



## 2. 在三角形的重心坐标系下做插值

在三个顶点组成的平面内,方法如下:

1. 将普通笛卡尔坐标系转化为重心坐标系
2. 根据重心坐标计算**不受透视投影影响**的三个插值系数
   这三个插值系数可以对当前三个顶点的任意属性进行插值



### 2.1 计算重心坐标系

![](./images/barycentric.jpg)

设 P 为 2D 空间内三角形 ABC 内任意一点,求三角形 ABC 以 AB,AC 为坐标轴的重心坐标 u, v
$$
\begin{align}
\vec {AP} &= u \vec {AB} + v \vec {AC} \\
A-P &= u(A-B) + v(A-C) \\
P &= (1-u-v)A + uB + vC \\\\

\vec {AP} &= u \vec {AB} + v \vec {AC} \\
u \vec {AB} + v \vec {AC} + \vec {PA} &= 0 \\
u (\vec {AB})_x + v (\vec {AC})_x + (\vec {PA})_x &= 0 \\
u (\vec {AB})_y + v (\vec {AC})_y + (\vec {PA})_y &= 0 \\
\begin{bmatrix}u & v & 1 \end{bmatrix} 
\begin{bmatrix}(\vec {AB})_x \\ (\vec {AC})_x \\ (\vec {PA})_x \end{bmatrix} &= 0 \\
\begin{bmatrix}u & v & 1 \end{bmatrix} 
\begin{bmatrix}(\vec {AB})_y \\ (\vec {AC})_y \\ (\vec {PA})_y \end{bmatrix} &= 0 \\
\begin{bmatrix}(\vec {AB})_x \\ (\vec {AC})_x \\ (\vec {PA})_x \end{bmatrix} \times 
\begin{bmatrix}(\vec {AB})_y \\ (\vec {AC})_y \\ (\vec {PA})_y \end{bmatrix} &= \begin{bmatrix}u \\ v \\ 1 \end{bmatrix} \\\\

u \vec {AB} + v \vec {AC} + \vec {PA} &= 0 \\
a \vec {AB} + b \vec {AC} + c \vec {PA} &= 0 &u = {a \over c}, v ={b \over c} \\
P &= (1-u-v)A + uB + vC \\
P &= (1-{a \over c}-{b \over c})A + {a \over c}B + {b \over c}C
\end{align}
$$
编码为

```c
// 将屏幕上的笛卡尔坐标系转换为 ABC 三角形内的重心坐标系
vec3 barycentric(vec2 A, vec2 B, vec2 C, vec2 P) {
    vec3 s[2];
    for (int i=2; i--; ) {
        s[i][0] = B[i]-A[i];
        s[i][1] = C[i]-A[i];
        s[i][2] = A[i]-P[i];
    }
    vec3 u = cross(s[0], s[1]);
    if (0 == std::abs(u.z)) return vec3(-1,1,1);
        
    return vec3(1.f-(u.x+u.y)/u.z, u.x/u.z, u.y/u.z);
}
```



### 2.2 根据重心坐标系计算插值系数

**注意:**

- 以下使用的深度都是<u>线性深度</u>,实际存储的是非线性深度,需要转换一下
- 对于非线性深度的插值,必须是<u>非透视矫正</u>的插值系数



已知

- 透视投影后 2D 屏幕空间的 三角形 ABC 的深度为 $Z_{P'} = \alpha' Z_{A'} + \beta' Z_{B'} +  \gamma' Z_{C'}$
- $\alpha' + \beta' + \gamma' = 1$

求:透视投影前 3D 裁剪空间的 三角形 ABC 的深度为 $Z_P = \alpha Z_A + \beta Z_B +  \gamma Z_C$
$$
\begin{align}
1 &= \alpha' + \beta' + \gamma' \\
{Z_P \over Z_P} &= {Z_A \over Z_A}\alpha' + {Z_B \over Z_B}\beta' + {Z_C \over Z_C}\gamma' \\
{Z_P \over Z_P} &=
\begin{bmatrix}Z_A & Z_B & Z_C\end{bmatrix}
\begin{bmatrix}{1 \over Z_A}\alpha' \\ {1 \over Z_B}\beta' \\ {1 \over Z_C}\gamma' \end{bmatrix} \\
Z_P &=
\begin{bmatrix}Z_A & Z_B & Z_C\end{bmatrix}
\begin{bmatrix}{1 \over Z_A}\alpha' \\ {1 \over Z_B}\beta' \\ {1 \over Z_C}\gamma' \end{bmatrix} Z_P \\
Z_P &=
\begin{bmatrix}Z_A & Z_B & Z_C\end{bmatrix}
\begin{bmatrix}{Z_P \over Z_A}\alpha' \\ {Z_P \over Z_B}\beta' \\ {Z_P \over Z_C}\gamma' \end{bmatrix}\\
Z_P &=
\begin{bmatrix}Z_A & Z_B & Z_C\end{bmatrix}
\begin{bmatrix}\alpha \\ \beta \\ \gamma \end{bmatrix}\\
\\
\alpha + \beta + \gamma &= 1\\
{Z_P \over Z_A}\alpha' + {Z_P \over Z_B}\beta' + {Z_P \over Z_C}\gamma' &= 1\\
Z_P &= {1 \over {{\alpha' \over Z_A} + {\beta' \over Z_B} + {\gamma' \over Z_C}}}
\\

\end{align}
$$
要通过这些插值其他属性的值 I,则
$$
\begin{align}
I_P &= \begin{bmatrix} I_A & I_B & I_C \end{bmatrix}
\begin{bmatrix} \alpha \\ \beta \\ \gamma \end{bmatrix}\\
&= \begin{bmatrix} I_A & I_B & I_C \end{bmatrix}
\begin{bmatrix} {Z_P \over Z_A}\alpha' \\ {Z_P \over Z_B}\beta' \\ {Z_P \over Z_C}\gamma'\end{bmatrix}\\
&= \begin{bmatrix} {Z_P \over Z_A}I_A & {Z_P \over Z_B}I_B & {Z_P \over Z_C}I_C \end{bmatrix}
\begin{bmatrix} \alpha' \\ \beta' \\ \gamma' \end{bmatrix}\\
&= ({\alpha' \over Z_A}I_A + {\beta' \over Z_B}I_B + {\gamma' \over Z_C}I_C)Z_P \\
&= ({\alpha' \over Z_A}I_A + {\beta' \over Z_B}I_B + {\gamma' \over Z_C}I_C) / {1 \over Z_P} \\
&= { {\alpha' \over Z_A}I_A + {\beta' \over Z_B}I_B + {\gamma' \over Z_C}I_C \over {{\alpha' \over Z_A} + {\beta' \over Z_B} + {\gamma' \over Z_C}} }
\end{align}
$$



# 二、纹理基础

纹理材质的反光性质

- 各项异性:固定视角和光源方向旋转表面时,反射会发生任何改变
- 各项同性:固定视角和光源方向旋转表面时,反射不会发生任何改变



纹理映射坐标

- 所有的纹理尺寸都会映射在 [0, 1] 的范围
  顶点着色器使用的是 uv 纹理坐标(坐标值为原始图片大小的值)
  片段着色器使用的是 st 纹理坐标(坐标值为归一化以后的值)
- OpenGL、Unity 纹理坐标原点在 左下角
- DirectX 纹理坐标原点在 左上角



纹理的尺寸

- 长宽大小应该是 2 的幂
  非 2 的幂的纹理会占用更多的内存空间和读取时间,有些平台会不支持非 2 的幂尺寸的纹理
- 纹理可以是非正方形的



## 1. 纹理环绕(坐标包装)

> 当**纹理坐标超出默认范围**时,每种纹理环绕方式都有不同的视觉效果输出

OpenGL 设置纹理不同坐标轴的环绕方式

```c
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_NEAREST); //纹理坐标 s/u/x 轴的包装格式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_LINEAR);  //纹理坐标 t/v/y 轴的包装格式
```

![](images/texture_wrapping.png)

| 环绕方式           | 描述                                                         |
| ------------------ | ------------------------------------------------------------ |
| GL_REPEAT          | 对纹理的默认行为,重复纹理图像                               |
| GL_MIRRORED_REPEAT | 和 GL_REPEAT 一样,但每次重复图片是镜像放置的                |
| GL_CLAMP_TO_EDGE   | 纹理坐标会被约束在 0 ~ 1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果 |
| GL_CLAMP_TO_BORDER | 超出的坐标处的纹理为用户指定的边缘颜色                       |



## 2. 纹理过滤(采样)

> 纹素,纹理单元:在**纹理坐标系**中一个像素占用的纹理数据
>
> 当三维空间里面的多边形,变成二维屏幕上的一组像素的时候,对每个像素需要到相应纹理图像中进行采样一个像素 Pixel 对应 N 个纹素 Texel 的映射过程就称为纹理过滤 

**纹理过滤的两种情况**

- 纹理被缩小 `GL_TEXTURE_MIN_FILTER`:**一个像素对应多个纹素**
  现象:走样问题,摩尔纹(远) + 锯齿(近)
  解决方法:超采样、Mipmap、各向异性过滤 Anisotropic
  例,一个 8 X 8 的纹理贴到远处正方形上,最后在屏幕上占了 2 X 2 个像素矩阵
- 纹理被放大 `GL_TEXTURE_MAG_FILTER`:**一个纹素对应多个像素**
  现象:模糊 / 锯齿
  解决方法:插值,Nearest、Bilinear
  例,一个 2 X 2 的纹理贴到近处正方形上,最后在屏幕上占了 8 X 8 个像素矩阵


OpenGL 中针对放大和缩小的情况的设置

```c
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); //缩小
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);  //放大
```



### 2.1 邻近点采样 Nearest neighbor

优点:效率最高
缺点:效果最差

方法:选择最接近中心点纹理坐标的 **1 个纹理单元**采样

![](images/texture_nearest.png)



### 2.2 双线性过滤 Bilinear

优点:适于处理有一定精深的静态影像
缺点:不适用于绘制动态的物体,当三维物体很小时会产生深度赝样锯齿 (Depth Aliasing artifacts)

方法:选择最接近中心点纹理坐标的 2 X 2 纹理单元矩阵进行采样,取 **4 个纹理单元**采样的插值
另外还有种类似的方法 **Bicubic**,取附近 **16** 个纹理单元采样的插值

![](images/texture_linear.png)



### 2.3 多级渐远纹理过滤 Mipmap 

Migmap 用来对同一纹理生成多个不同尺寸的纹理,用 *Level of Detail* (**LOD**) 来规定纹理缩放的大小
LOD 0 为原始尺寸,从 LOD 1 开始,LOD n 的纹理宽高为 LOD n-1 的一半,直到纹理的大小缩放为 1 X 1 为止

距观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,即最适合物体的距离的那个。由于距离远,解析度不高也不会被用户注意到

优点:效果最好,适用于动态物体或景深很大的场景
缺点:效率低,会占用一定的空间,只能用于纹理被缩小的情况

![](./images/texture_mipmapping.png)

开启 Mipmap 下的纹理采样

![](./images/texture_mipmap.png)

例,三线性过滤 Trilinear 方法:

1. 取 Mipmap 纹理中距离与当前屏幕上尺寸相近的两个纹理

2. 将 1 中选取的纹理 选择最接近中心点纹理坐标的 2 X 2 纹理单元矩阵进行采样(线性过滤)

3. 将 2 中两次采样的结果进行加权平均(**8 个纹理单元**采样),得到最后的采样数据



**Mipmap Level 计算**
取 X 轴和 Y 轴的最大变化长度 L 作为查询的输入参数,通过 $d = log_2L$​​ 得到 Mipmap 的查询层级 Level d

**Mipmap 采样**
得到的层级 d 是个小数,分别向上和向下取整,得到两个不同 level 的 Mipmap 像素值,然后在根据 d 的小数部分做插值

<img src="./images/conpute_mipmaplevel.png" style="zoom:150%;" />



### 2.4 各向异性过滤 Anisotropic

> 之前提到的三种过滤方式,默认纹理在 x,y 轴方向上的缩放程度是一致的(纹理表面刚好正对着摄像机)
> 当纹理在 3D 场景中,纹理表面刚倾斜于虚拟屏幕平面时,出现一个轴的方向纹理放大,一个轴的方向纹理缩小的情况(**OpenGL 判定为纹理缩小**)需要使用各向异性过滤配合以上三种过滤方式来达到最佳的效果

优点:效果最好,使画面更加逼真
缺点:效率最低,由硬件实现

各向异性过滤包含会生成一张包含 Mipmap 的图 Ripmap,如下图(对角线的集合是 Mipmap)
各向异性过滤也只是覆盖了大部分情况,另一种方法 EWA filtering 多重椭圆形采样效果更好 

![](./images/ripmap.png)

方法:根据视角对梯形范围内的纹理采样

1. 确定 X、Y 方向的**采样比例(Ripmap 的采样范围 N x N)**
   ScaleX = 纹理的宽 / 屏幕上显示的纹理的宽
   ScaleY = 纹理的高 / 屏幕上显示的纹理的高
   异向程度 N = max(ScaleX, ScaleY) / min(ScaleX, ScaleY);

   例,64 X 64的纹理最后投影到屏幕上占了128 X 32 的像素矩阵
   ScaleX = 64.0 / 128.0 = 0.5;
   ScaleY = 64.0 / 32.0 = 2.0;
   异向程度 N = 2.0 / 0.5 = 4;

2. 根据采样比例分别在 X、Y 方向上采用 *三线性过滤* 或 *双线性过滤* 获得采样数据,**采样的范围由异向程度决定,不是原来的 2 X 2 像素矩阵**

   例,64 X 64 的纹理最后投影到屏幕上占了 128 X 32 的像素矩阵
   异向程度为 4,且在 缩放方面 X 轴 > Y 轴,所以 X 轴采样 2 个像素,Y 轴采样 2 * 异向程度 = 8 个像素
   采样范围为最接近中心点纹理坐标的 2 X 8 的像素矩阵

![](./images/texture_anisotropic.png)

OpenGL 中设置各向异性过滤

```c
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 异向程度);
```

各向异性对比三线性

![](images/texture_anisotropic.jpg)



### 2.4 多级渐远纹理过滤 Mipmap

![](./images/texture_mipmap.jpg)

Migmap 用来对同一纹理生成多个不同尺寸的纹理,用 *Level of Detail* (**LOD**) 来规定纹理缩放的大小
LOD 0 为原始尺寸,从 LOD 1 开始,LOD n 的纹理宽高为 LOD n-1 的一半,直到纹理的大小缩放为 1 X 1 为止

距观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,即最适合物体的距离的那个。由于距离远,解析度不高也不会被用户注意到

优点:效果最好,适用于动态物体或景深很大的场景
缺点:效率低,会占用一定的空间,只能用于纹理被缩小的情况

![](./images/texture_mipmapping.png)

开启 Mipmap 下的纹理采样

![](./images/texture_mipmap.png)

例,三线性过滤 Trilinear 方法:

1. 取 Mipmap 纹理中距离与当前屏幕上尺寸相近的两个纹理

2. 将 1 中选取的纹理 选择最接近中心点纹理坐标的 2 X 2 纹理单元矩阵进行采样(线性过滤)

3. 将 2 中两次采样的结果进行加权平均(**8 个纹理单元**采样),得到最后的采样数据



MipMap Level 计算

<img src="./images/conpute_mipmaplevel.png" style="zoom:150%;" />



### 2.5 圆内均匀随机采样

![](./images/sample.png)

1. 一般圆内随机采样的方式如图 2,其中 a,b 为均匀的随机数
   这种变换的局部性保持很差,如果两点在笛卡尔坐标系下连续,那么投影到圆后的两个点同样是连续的。但是,反过来就不一定成立了。

$$
\begin{align}
r &= \sqrt a \\
\theta &= 2 \pi \space b \\\\
(u, v) &= (r\cos \theta, r\sin \theta) \\
\end{align}
$$

2. 一种较好的改进方式如图 3,其中 a,b 为均匀的随机数
   具体论证见论文 [ A Low Distortion Map Between Disk and Square | Semantic Scholar](https://www.semanticscholar.org/paper/A-Low-Distortion-Map-Between-Disk-and-Square-Shirley-Chiu/43226a3916a85025acbb3a58c17f6dc0756b35ac)

$$
\begin{align}
r &= \sqrt a \\
\theta &= {\pi \space b  \over 4 \space a}
\end{align}
$$

3. [泊松圆盘采样 Poisson Disk](https://bost.ocks.org/mike/algorithms/#sampling)
   由于采样计算方法复杂,大家多用查表法来提前存储采样结果,然后再用旋转提前存储采样点的方式得到伪随机采样点的坐标![](./images/sample_poisson_disk.png)

   ```c++
   glm::vec2 sample(int numCandidates, const std::vector<glm::vec2>& samples) 
   {
     float bestDistance = 0;
     glm::vec2 bestCandidate;
     for (int i = 0; i < numCandidates; ++i) {
       glm::vec2 c(Math.random() * width, Math.random() * height);
       float d = glm::distance(findClosest(samples, c), c);
       if (d > bestDistance) {
         bestDistance = d;
         bestCandidate = c;
       }
     }
   
     return bestCandidate;
   }
   ```



## 3. 反走样 Anti-Aliasing

> 香农定理告诉我们,即便我们有无限的频率,无论对原信号采样多少次,总会在重建信号时有一些误差

通过增加采样质量来反走样,是适用于各个渲染场景的唯一通用方案


### 3.1 SSAA

超采样抗锯齿 Super Sample Anti-aliasing, SSAA

- 步骤:渲染一张比显示的纹理更高分辨率的帧缓冲,分辨率下采样到正常的分辨率
- 缺点:性能开销很大



### 3.2 MSAA

多重采样抗锯齿 Multisample Anti-aliasing, MSAA

- 步骤:将单一的采样点变为多个采样点(采样点的数量可以是任意的)
  不再使用像素中心的单一采样点,而是以特定图案排列的 4 个子采样点(Subsample)
  (4 个以上采样点的效果差别不大) 
  由顶点插值得到的像素颜色,会存储在**被图形遮盖住**的**每个**子采样点中,最终的像素颜色是子采样点的平均值
  如果不想以平均计算子采样颜色的方式,OpenGL 允许我们在 FS 阶段获取到每个子采样点的颜色并计算最终采样结果
- 缺点:颜色缓冲的大小会随着子采样点的增加而增加

![](./images/trick_anti_aliasing_sample.png)

### 3.3 FXAA





### 3.4 TAA







#  三、遮罩贴图

- 保护纹理的某些区域,使它们免于修改
- 主要用与控制光照,使同一个纹理的模型不同的角度拥有了不同的高光强度
- 一般为单通道纹理,不过有时候一张 RGBA 四通道的遮罩纹理可以控制 四种 表面属性的强度
- 使用方式为:物体的颜色 = 当前纹理坐标对应的遮罩纹理强度 * 光照计算后的颜色



## 1. Opacity 透明贴图

贴图的不透明度:黑色是透明的部分,白色为不透明的部分,灰色为半透明的部分



## 2. Ambient Occlusion 环境遮挡贴图

环境光遮蔽贴图属于**预计算的贴图类型**(预先计算好纹理效果,降低实时计算成本)
模拟物体之间所产生的阴影,在不打光的时候增加体积感
完全不考虑光线,单纯基于物体与其他物体越接近的区域,受到反射光线的照明越弱这一现象来模拟现实照明(的一部分)效果

白色表示应接受完全间接光照的区域,以黑色表示没有间接光照





# 四、微观几何形态存储



## 1. 凹凸贴图 Bump Map

又称高度贴图:使用一张高度纹理来模拟表面上下高度的位移,存储相对于顶点位置的偏移量(白色区域是高区域,黑色区域是低区域)

- 优点:非常直观,可以从高度纹理中明确的知道一个模型表面的凹凸情况
- 缺点:**不改变几何信息**,计算较复杂,不能直接得到表面法线,需要通过像素的灰度值计算得到
  如图,先通过凹凸贴图和顶点位置计算高度值,根据高度值计算表面切线,根据表面切线的垂线得到法线
  ![](./images/texture_bump_map.png)



## 2. 位移贴图 Displacement Map

![](./images/texture_displacement.png)

**和凹凸贴图一样**存储相对于顶点位置的偏移量值

- 优点:可直接使用凹凸贴图作为位移贴图,改变几何信息
- 缺点:相比上更逼真,要求模型足够细致,运算量更高
  DirectX 有 Dynamic 的插值法,对模型做插值,使得初始不用过于细致



## 3. 法线贴图 Normal map

法线贴图

- 直接存储表面法线
- 根据法线所在的坐标空间类型可分为
  模型空间的法线纹理 (object-space normal map):将修改后的**模型**空间表面的法线存储在一张纹理中
  切线空间的法线纹理 (tangent-space normal map):将修改后的**纹理**切线空间表面的法线存储在一张纹理中
  **一般使用切线空间的法线纹理**



法线的模型变换矩阵

- 在顶点坐标的模型变换中,当我们使用一个不等比缩放时,法线不会再垂直于对应的表面
  ![](./images/normal_transformation.png)

- 法线需要一个基于顶点坐标的模型变换的专门的 模型矩阵
  如果模型变换 $M_t$ 不是正交变换,则法线变换矩阵为:$M_{n} = (M_t^T)^{-1}$
  如果模型变换 $M_t$ 是正交变换,则法线变换矩阵为:$M_{n} = M_t$
  正交变换:旋转变换,[公式的推导过程](../LinearAlgebra/Part1_Matrix.md)
  由于位移对于法线方向没有影响,而逆变换计算量较大,因此一般采用没有位移的 3 X 3 矩阵来计算法线变换



### 3.1 使用流程

**I. 顶点信息补充**

   1. 由 模型变换 得到 法线的模型变换矩阵(逆矩阵耗时大,尽量放在 CPU 上算一次或者放在顶点着色器)
   2. 根据顶点位置和纹理坐标信息,计算**模型空间下的** 切线 和 副切线
   3. 每三个顶点构成一个平面,他们共享一组 切线 和 副切线



**II. 顶点着色器**

1. 将顶点数据中的 切线、副切线、法线坐标系位置经过 法线的模型变换矩阵 转换为
   **世界空间下的** 切线空间坐标,Gram-Schmidt 正交化后构建 切线空间矩阵
2. 计算世界空间下的光源在 切线空间 的坐标



**III. 片元着色器**

   1. 根据法线纹理对应的普通纹理的纹理坐标,从法线纹理读取切线空间下的法线数据(像素值)
   2. 将范围是 [0, 1] 的像素值,转换为范围是 [-1, 1] 的表面法线值:$normal = pixel*2.0 - 1.0$
   3. 将在**切线空间**下的光源和物体片元的坐标与法线计算得到片元颜色



### 3.2 切线空间

切线空间的坐标系,原点:模型的顶点

- Z 轴:N(Normal)法线方向(和 Z 轴的正方向始终保持一致)
- X 轴:T(Tagent)切线方向,和纹理坐标的 X 轴(U)一致
- Y 轴:B(Bitangent)副切线方向 ,和纹理坐标的 Y 轴(V)一致

![](./images/normal_mapping_tbn.png)



计算额外的顶点信息:纹理法线 **切线空间** 到 **模型空间** 的矩阵

- 已知切线空间法线纹理的切线 T 和 副切线 B 分别对应与法线纹理对应普通纹理的 U 和 V 坐标轴
  (此时 T、B 在模型空间下)且点 $P_1$、$P_2$、$P_3$ 与纹理坐标的对应关系如下图,
  求切线方向 T 和副切线方向 B

  ![](./images/normal_mapping_surface_edges.png)

- 则:
  $$
  \begin{align}
  E_1 &= \Delta U_1 T + \Delta V_1 B\\
  E_2 &= \Delta U_2 T + \Delta V_2 B\\
  \begin{bmatrix} E_1\\ E_2 \end{bmatrix}
  &= 
  \begin{bmatrix}
  \Delta U_1 & \Delta V_1\\
  \Delta U_2 & \Delta V_2
  \end{bmatrix}
  \begin{bmatrix} T\\ B \end{bmatrix} \\
  \begin{bmatrix}
  \Delta U_1 & \Delta V_1\\
  \Delta U_2 & \Delta V_2
  \end{bmatrix}^{-1}
  \begin{bmatrix} E_1\\ E_2 \end{bmatrix}
  &= 
  \begin{bmatrix} T\\ B \end{bmatrix} \\
  {1 \over \Delta U_1 \Delta V_2 - \Delta U_2 \Delta V_1}
  \begin{bmatrix}
  \Delta V_2 & -\Delta V_1\\
  -\Delta U_2 & \Delta U_1
  \end{bmatrix}
  \begin{bmatrix} E_1\\ E_2 \end{bmatrix}
  &= 
  \begin{bmatrix} T\\ B \end{bmatrix} \\
  {1 \over \Delta U_1 \Delta V_2 - \Delta U_2 \Delta V_1}
  \begin{bmatrix}
  \Delta V_2 E_1 -\Delta V_1 E_2\\
  -\Delta U_2 E_1 + \Delta U_1 E_2
  \end{bmatrix}
  &= 
  \begin{bmatrix} T\\ B \end{bmatrix}
  \end{align}
  $$



**Gram-Schmidt 正交化**

当在更大的网格上计算切线向量的时候,它们往往有很大数量的共享顶点,当法向贴图应用到这些表面时将切线向量平均化(一个三角面平均三个顶点的切向量)通常能获得更好更平滑的结果。但是这样做有个问题,就是TBN向量可能会不能互相垂直,这意味着 TBN 矩阵不再是正交矩阵了

这时需要在**顶点着色器**做正交化操作,让 TBN 回归到正交矩阵
$$
\begin{align}
N &= normalize(N) \\
T &= normalize(U - dot(U, N) * N) \\
B &= normalize(cross(N, T))
\end{align}
$$



### 3.3 不同坐标空间的比较

**模型空间**法线纹理的优点:

- 实现简单,更加直观
- 在纹理坐标的缝合处和尖锐的边角部分,可见的突变(缝隙)较少,边界过渡平滑
  模型空间的法线纹理存储的是同一坐标系下的法线信息,在边界可将法线通过插值,来实现平滑过渡
  切线空间的法线依靠纹理坐标的方向得到的结果,会在边缘处或尖锐的地方出现缝合现象



**切线空间**法线纹理的优点:

- 自由度高,可做 UV 纹理动画
  可映射到不同的网格上,而模型空间法线纹理只能用于创建他的网格
- 可以复用法线纹理
  一个砖块的 6 个面可以共用一张切线空间法线纹理
- 对于纹理使用的额外数据是 可压缩的
  可只存储额外的 切线 和 副切线 2 个方向,而模型空间的法线必须存储 3 个方向的值



## 4. Curvature 曲率贴图

存储凹凸信息:黑色的值代表了凹区域,白色的值代表了凸区域,灰度值代表中性/平地



## 5. Thickness 厚度贴图

辅助制作表面散射 SSS:黑色代表薄的地方、白色代表厚的地方



# 五、环境光贴图

环境光贴图 Environment Light Map,存储所有外部方向的环境光照信息(环境光来自无限远处,强度一致,只记录方向)

在游戏引擎里做**场景**地图的时候会用到
用于静态模型上的间接光照:将场景的光照结果烘培到模型贴图上,从而实现模拟现实光照效果,可以节省硬件资源

## 1. 球形环境贴图 Spherical Environment Map

存储的一个**镜子球反射**的环境光色彩
球形贴图扭曲拉伸问题严重,贴图存储的环境光照信息不均匀



## 2. 立方体贴图 Cube Map

贴图扭曲拉伸问题相较于球形环境贴图较轻,但计算量较大
立方体贴图 GL_TEXTURE_CUBE_MAP 

```c
// shader
uniform samplerCube skybox;

// CPU code
glBindTexture(GL_TEXTURE_CUBE_MAP, _texID);
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, ...);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
```

- 包含了 6 个 2D 纹理的纹理,每个 2D 纹理都组成了立方体的一个面,它通过一个方向向量来进行采样
  **方向向量的大小并不重要,只要提供了方向**

- 纹理坐标:一般为世界坐标系下的顶点坐标(范围 -1,1)
  处于世界坐标系下,是一个由立方体中心出发指向立方体面的三维向量
  贴图的顺序一般为:<u>右、左、上、下、前、后</u>

  ![](./images/texture_cube_map.png)

### 2.1 天空盒 Skybox

天空盒:包含了整个场景的(大)立方体,它包含周围环境的 6 个图像,让玩家以为他处在一个比实际大得多的环境当中

- 天空盒会跟随相机移动,从而让人认为天空盒的图像在无法到达的远方
- 使用提前深度测试将天空盒最后渲染以节省带宽
- 纹理环绕方式:超出采样部分取边界
- 通过将输出位置的 z 分量等于它的 w 分量,让 z 分量永远等于 1.0,使 z 在透视除法时,深度始终是最大的 1
  `gl_Position = pos.xyww;`

```glsl
// VS
#version 330 core
layout (location = 0) in vec3 aPos;

out vec3 TexCoords;

uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;

void main() {
    TexCoords = aPos;
    vec4 pos = projection * view * model * vec4(aPos, 1.0);
	  // 用 w 替换 z,让天空盒的深度值在进行深度除法后始终保持 1.0 的最大值,确保天空盒在最后绘制
    gl_Position = pos.xyww;
}

// FS
#version 330 core
out vec4 FragColor;

in vec3 TexCoords;

uniform samplerCube skybox;

void main() {
    FragColor = texture(skybox, TexCoords);
}
```



### 2.2 环境映射 Environment Mapping

环境映射:通过使用环境的立方体贴图(不仅仅是天空盒)我们可以给物体反射和折射的属性

动态环境贴图:通过帧缓冲,为物体的 6 个角度创建出场景纹理,并在每个渲染迭代中将它们储存到一个立方体贴图中。之后可以使用这个(动态生成的)立方体贴图来创建出更真实的,包含其它物体的,反射和折射表面了

**反射**:通过物体表面单位法线,观察方向来计算反射方向作为立方体贴图的纹理坐标

![](./images/texture_reflection.png)



**折射**:通过物体表面单位法线,观察方向,以及两个材质之间的折射率(Refractive Index)来求出折射方向,其中 OpenGL 输入的折射率 = $出发材质(空气)的折射率 \over 进入材质(水)的折射率$

折射法则通过 [斯涅尔定律 Snell's Law](https://en.wikipedia.org/wiki/Snell%27s_law) 来描述,其中 $\eta$ 为材质的折射率

$$
\eta_{入射角} \sin \theta_{入射角} = \eta_{出射角}  \sin \theta_{折射角}
$$

![](./images/texture_refraction.png)

```glsl
// VS
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;

out vec3 Normal;
out vec3 Position;

uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;

void main() {
    Normal = mat3(transpose(inverse(model))) * normal;
    Position = vec3(model * vec4(position, 1.0));
    gl_Position = projection * view * model * vec4(position, 1.0);
}

// FS
#version 330 core
out vec4 FragColor;

in vec3 Normal;
in vec3 Position;

uniform vec3 cameraPos;
uniform samplerCube skybox;

void main() {
    vec3 I = normalize(Position - cameraPos);
    vec3 R = reflect(I, normalize(Normal)); // 反射

    // 折射
    float ratio = 1.00 / 1.52;
    R = refract(I, normalize(Normal), ratio);
    FragColor = vec4(texture(skybox, R).rgb, 1.0);
}

```





# 六、高级纹理

## 1. 渲染目标纹理 RTT

渲染目标纹理(Render Target Texture)把整个三维场景渲染到中间缓冲中,而不是传统的帧缓冲或者后备缓冲(back buffer),与之相关的是多重渲染目标(Multiple Render Target,MRT)

应用:

- 场景中的镜子
- 场景中的透明玻璃



## 2. 程序纹理

程序纹理:由计算机生成的纹理,可以使用各种颜色以外参数来控制纹理的外观

### 2.1 Perlin 噪声纹理

常用于模拟水波纹,火和地形,生成二维 Perlin 噪声纹理的过程如下:

1. 晶格划分
   将二维空间划分为多个大小相等的晶格(矩形)例:1024px * 1024px 的噪声图,可以选择 64px 为晶格尺寸
   
   ```glsl
   p0 = floor(pos / size) * size;
   p1 = p0 + float2(1, 0) * size;
   p2 = p0 + float2(0, 1) * size;
   p3 = p0 + float2(1, 1) * size;
   
   posInGrid = (pos - p0) / size;
   ```
   
   ![](./images/texture_noise_lattice.png)
   
2. 伪随机梯度生成
   根据晶格的位置 P 与随机种子,对晶格的每个顶点生成一个伪随机梯度,表示为一个二维向量
   经过**随机函数 gold_noise** 生成的随机的 x, y 后再归一化,最后用 grad 表

   ```glsl
   #define PHI (1.61803398874989484820459 * 00000.1)
   #define PI (3.14159265358979323846264 * 00000.1)
   #define SQ2 (1.41421356237309504880169 * 10000.0)
   
   float gold_noise(float2 pos, float seed) {
     return frac(tan(distance(pos * (PHI + seed), float2(PHI, PI))) * SQ2) * 2 - 1;
   }
   ```

![](./images/texture_noise_lattice1.png)

3. 晶格内插值
   计算当前点 P 相对于晶格四个顶点的偏移量 delta

   ![](./images/texture_noise_lattice2.png)

   对 delta 和 伪随机梯度得到的 grad 进行点积得到 v,最后将四个顶点的 v 值插值为一个数值
   得到 Perlin 噪声的**最终值(范围 -1, 1)**

   插值系数的计算一般为:$k = 6t^5 - 15t^4 + 10t^3$ 或 $k = 3t^2 - 2t^3$

   ```glsl
   float smoothLerp(float a, float b, float t) {
       float k = pow(t, 5) * 6 - pow(t, 4) * 15 + pow(t, 3) * 10;
       return (1 - k) * a + k * b
   }
   
   v0 = dot(delta0, grad0);
   v1 = dot(delta1, grad1);
   v2 = dot(delta2, grad2);
   v3 = dot(delta3, grad3);
   
   // Lerp with x
   a = smoothLerp(v0, v1, posInGrid.x);
   b = smoothLerp(v2, v3, posInGrid.x);
   
   // Lerp with y
   return smoothLerp(a, b, posInGrid.y);
   ```

4. 分型噪声图

   仅通过晶格化随机梯度生成的二维噪声图难以模拟自然界中的噪声现象
   即便是缩小晶格尺寸,也只能徒增噪声图的 "颗粒感"
   需要通过:将多种不同晶格尺寸的噪声图**叠加**得到自相似的分形噪声图

   ```glsl
   // 噪声图的叠加过程也可以描述成一个 分形布朗运动(FBM)函数
   inline float fbm(float2 pos) {
       float value = 0;
       float amplitude = 0.5;
   
       for(int i = 0; i < _Iteration; i++) {
           // 由于 noise_function 返回值的范围在 -1 ~ 1,取绝对值后,可用于地形的生成
           // value += amplitude * abs(noise_function(pos));
           value += amplitude * noise_function(pos);
           pos *= 2;
           amplitude *= .5;
       }
     
       return value;
   }
   ```



### 2.2 Worley 噪声纹理

常用于模拟多孔噪声,如:石头、水、纸张



## 3. 虚拟纹理 Virtual Texture











# 八、纹理压缩







# Reference

1. [Render To Texture](http://www.paulsprojects.net/opengl/rtotex/rtotex.html)
2. [Implementing an anisotropic texture filter](https://www.sciencedirect.com/science/article/abs/pii/S0097849399001594)
3. [Lesson 2: Triangle rasterization and back face culling · ssloy/tinyrenderer Wiki (github.com)](https://github.com/ssloy/tinyrenderer/wiki/Lesson-2:-Triangle-rasterization-and-back-face-culling)
4. [(PDF) Accelerated Half-Space Triangle Rasterization (researchgate.net)](https://www.researchgate.net/publication/286441992_Accelerated_Half-Space_Triangle_Rasterization)
6. [learnopengl-法线贴图](https://learnopengl-cn.github.io/05%20Advanced%20Lighting/04%20Normal%20Mapping/)
7. [learnopengl-立方体贴图](https://learnopengl-cn.github.io/04 Advanced OpenGL/06 Cubemaps/#_7)
8. [Understanding Perlin Noise](https://flafla2.github.io/2014/08/09/perlinnoise.html)
9. [基于 ComputeShader 生成 Perlin Noise 噪声图](https://zhuanlan.zhihu.com/p/88518193)
10. [Unity_Shaders_Book : https://github.com/candycat1992/Unity_Shaders_Book](https://link.zhihu.com/?target=https%3A//github.com/candycat1992/Unity_Shaders_Book)
11. [Unity Manual: https://docs.unity3d.com/Manual/TextureTypes.html](https://link.zhihu.com/?target=https%3A//docs.unity3d.com/Manual/TextureTypes.html)
14. [Learning DirectX 12 – Lesson 4 – Textures](https://www.3dgep.com/learning-directx-12-4/)
15. [Unity GPU优化(Occlusion Culling 遮挡剔除,LOD 多细节层次,GI 全局光照)](https://gameinstitute.qq.com/community/detail/120912)
16. [《我所理解的 Cocos2d-x》秦春林](https://book.douban.com/subject/26214576/)
17. [《Unity Shader 入门精要》冯乐乐](https://book.douban.com/subject/26821639/)
18. [深入探索透视纹理映射(下)](https://blog.csdn.net/popy007/article/details/5570803)
17. [FXAA Whitepaper](http://developer.download.nvidia.com/assets/gamedev/files/sdk/11/FXAA_WhitePaper.pdf)



================================================
FILE: ComputerGraphics(OpenGL)/Part4_Animation.md
================================================
# 一、纹理动画

## 1. 序列帧动画

序列帧动画:

- 依次播放一系列关键帧图片,当播放速度达到一定数值时,看起来就是一个连续的动画
- 多用于游戏 2D 中做循环动作的角色,或者制作 3D 模型,做三渲二的序列帧效果



优点:灵活性强,不需要任何物理计算就可以达到非常细腻的效果

缺点:

- 效果固定,只能从一个角度观看
- 时间较长的序列帧动画会消耗大量的内存和显存,对于移动端来说,会造成一段时间的内存波动



## 2. Sprite 动画

生成 Sprite 序列帧动画的工具:[TexturePacker](https://www.codeandweb.com/texturepacker)

方法:多张序列帧图片合成一张纹理,每个序列帧在纹理中的大小是一样的,通过不断变换对合成纹理的局部采样位置来实现帧动画的切换

优点:多张序列帧图片合成一张纹理,减少纹理的读取次数,提高效率

缺点:

- 效果固定,只能从一个角度观看
- 时间较长的序列帧动画会消耗大量的内存和显存,对于移动端来说,会造成一段时间的内存波动





# 二、顶点动画

## 1. 广告牌

广告牌(Billboarding)根据视角方向旋转一个贴有纹理的多边形(通常是四边形)使得多边形总是面对着摄像机

**应用场景:烟雾,云朵,闪光效果**



和构建相机坐标系的方法类似,需要在广告牌的多边形上构建基坐标系:

1. 在多边形上选取坐标原点(一般为多边形的中心点)
2. 由于一般广告牌的**表面法向量为指向相机的方向**和世界坐标下竖直向上的方向为已知
   从而:设世界坐标下竖直向上的方向为 Y 轴(0,1,0),则垂直于 Y 轴和表面法线构成平面的为 X 轴
   $X = Y_{up} \times Normal$
3. Z 轴在 Y 轴和广告牌表面法线构成的平面内,但 Z 轴不一定是法线,需要重新计算
   Z 轴直于 X 轴和 Y 轴构成的平面 $Z = X \times Y_{up}$
4. 将 X、Y、Z 轴**归一化**后作为广告牌的模型矩阵
5. 根据实际广告牌的中心点在世界坐标系下的位置,将广告牌上的顶点逐个进行位移、MVP 变换,裁剪后绘制到屏幕上



## 2. Morph 动画

记录多个关键动作中的模型所有顶点的位置,通过在运行中对前后的两个关键动作的模型顶点做线性插值,来达到动画的效果

- 只存储模型网格变化的顶点
- 多用于人物表情等对动画细节要求较高的地方





## 3. 骨骼蒙皮动画

### 3.1 功能作用

> 刚性物体:物体在运动中形状不会发生改变

<u> 刚性阶层动画</u>

- 方法:动画不是一个整体的 Mesh, 而是分成很多部分 Mesh
  通过一个父子层次结构将这些分散的 Mesh 组织在一起,父 Mesh 带动其下子 Mesh 的运动
- 优点:刚性阶层动画将动画角色以树状的数据结构存储,制作灵活,美术工作量小,方便角色动画运动
- 缺点:由于各部分 Mesh 中的顶点是固定在其 Mesh 坐标系中的,所以关节处在运动时会产生裂缝,只适合机械或者皮影风格的角色



骨骼蒙皮动画

- 方法:通过动画关键帧数据控制骨架的变换,使骨架在两个姿态间的线性插值变化,从而带动绑定在骨架上模型顶点 Mesh 的运动,最终形成动画效果
- 优点:由于**每个顶点可以被多个骨骼控制**,关节在运动时没有裂缝
  多套 Mesh 可以共享一个骨骼的动画效果,节省资源



### 3.2 数据存储结构

**骨骼和关节的关系**:骨骼是一个坐标空间,关节是骨骼坐标空间的原点
骨骼没有长度,但在编辑器中为了方便展示,会给骨骼绘制长度

**骨架:有层次的关节组成的树形结构**

- 关节:包含多种顶点信息的树形结构中的节点
- **关节权重**:所有关节的权重和为 1
  顶点存储受哪些关节的影响(Unity 里 1 个顶点最多受 4 个关节的影响)并记录下受每个关节影响的权重
- [**插槽** Socket/Slot](https://www.52vr.com/extDoc/ue4/CHN/Engine/Content/Types/SkeletalMeshes/Sockets/index.html):一个骨骼拥有了插槽,说明这个骨骼可以挂载一个 Mesh 对象,让这个对象**相对于这个骨骼**旋转,位移等运动
  比如,一个人物模型的手骨骼上通常会有插槽,方便在手上放置不同的武器
- 根关节:可以通过平移和旋转根关节移动并确定整个骨架在世界空间中的位置和方向
- 父关节:自身运动可以影响所有子关节的运动
- 子关节:自身运动不会影响父关节,但处于父关节的坐标系中

![](./images/animation_spine.png)



**姿势:关节相对于某坐标系的位置、朝向、缩放**

- 绑定姿势:顶点网格在 <u>绑定骨骼前</u> 默认的姿势(又称参考姿势,一般为 T 字形)
- 局部姿势:关节相对于父关节的偏移,结构 TQS(位置、朝向、缩放)根关节的父节点是世界坐标系原点
- 全局姿势:关节相对于模型空间或者世界空间的姿势



### 3.3 骨骼蒙皮的计算

如下图,点 B 是骨骼 A 的子骨骼,点 p 为跟随 B 的网格上的一个顶点

![](./images/spine.png)

**隐含条件** 绑定姿势下的相对位置均已知

1. **绑定姿势下**:点 p 相对于 B 的位置表示为 PB0
   **目标姿势下**:点 p 相对于 B 的位置表示为 PB1
   由于 p 跟随 B 运动,相对于 B 时 p 是不动的,因此 PB0 == PB1,点 p 相对于 B 的相对位置可以表示为 PB

2. 顶点<u>关联一根</u>骨骼时
   **绑定姿势下(当前帧):**已知
   点 B 在模型空间<u>相对父骨骼</u>的位置为 $M_{B0}$
   点 p 在模型空间<u>相对父骨骼</u>的位置为 $M_{p0} = PB * M_{B0}$
   则,$PB = M_{p0} * M_{B0}^{-1}$
   
   **目标姿势下(下一帧):**
   已知:点 B 在模型空间<u>相对父骨骼</u>的位置为 $M_{B1}$
   求得:点 p 在模型空间<u>相对父骨骼</u>的位置为 $M_{p1} = PB * M_{B1} = M_{p0} * M_{B0}^{-1} * M_{B1}$
   
   其中,通过已知可求得的矩阵 $M_{sn} = M_{B0}^{-1} * M_{Bn}$ 称为蒙皮矩阵
   
3. 顶点<u>关联多根</u>骨骼时
   关联的所有骨骼的权重和为 $W_0 + W_1 + ... + W_n = 1$
   $M_{p1} = M_{p0} * (W_0*M_{s0} + W_1*M_{s1} + ... + W_n*M_{sn})$



### 3.4 线性混合蒙皮(Linear Blending Skinning,LBS)

<u>在三维空间的旋转动画中</u>
使用**线性插值矩阵**会有信息丢失,这个时候需要使用**线性插值对偶四元数**来达到平滑动画的效果
为了减少 CPU 的压力,这些计算可以放在 GPU 里来做

比如:两个手臂向相反方向旋转同样角度时,手肘关节的变化
$$
0.5 * 
\begin{bmatrix}
0 & -1 & 0 \\
1 & 0 & 0 \\
0 & 0 & 1
\end{bmatrix}
+ 
0.5 * 
\begin{bmatrix}
0 & 1 & 0 \\
-1 & 0 & 0 \\
0 & 0 & 1
\end{bmatrix}
= 
\begin{bmatrix}
0 & 0 & 0 \\
0 & 0 & 1 \\
0 & 0 & 1
\end{bmatrix}
$$

- 线性混合蒙皮算法因其原理为线性计算,有一个无法克服的缺陷::对于比较灵活的关节(如肩膀),当关节处旋转角度很大时,会产生皮肤失真的结果
  比如皮肤的塌陷、扭曲打结(裹糖纸)等现象
  ![](./images/animBoneLBS.png)
- 采用矩阵来计算旋转信息
  ![](./images/LinearBlendingSkinnig.gif)

- 采用对偶四元数来计算旋转信息
  ![](./images/DualQuaternionBlendingSkinning.gif)





# 三、动画混合

将两个或多个**动画片段**的当前**骨架姿态**以一定的方式通过程序进行实时混合

**注意:**骨骼之间的混合不能使用最终的矩阵来混合(矩阵无法做动画各个属性的线性计算),需要分别对生成矩阵的 SQT 进行单独混合





# 四、动画流水线

动画后处理:动画片段混合以外的骨架姿势修改。包括:IK 处理,物理效果(动画融合物理打击效果)

![](./images/Animation_Pipeline.png)

**骨骼**:一根骨骼实际上是一个点(在编辑器里常常会把骨骼间连成线段)
**骨骼 Pos**:基于骨架参考姿势的变换矩阵,是所有骨骼 Transform (位移+旋转+缩放)的集合
**蒙皮**:将网格顶点,根据设计好的蒙皮权重 和 上一步计算好的骨骼 Pos 计算后得到新网格顶点的过程





# Reference

- [OpenGEX 官网](http://www.opengex.org/)
- [骨骼动画原理](https://www.cnblogs.com/tandier/p/10087656.html)
- [骨骼蒙皮动画算法(Linear Blending Skinning)](https://www.cnblogs.com/shushen/p/5987280.html)
- [Skeletal Animation 理论与实践](https://zhuanlan.zhihu.com/p/27073261)
- [Skinned Mesh 原理解析和一个最简单的实现示例](https://blog.csdn.net/n5/article/details/3105872)
- [UE4 动画系统笔记](https://blog.csdn.net/hechao3225/article/details/113531847)
- [浅析 UE 动画系统](https://zhuanlan.zhihu.com/p/393884450)



================================================
FILE: ComputerGraphics(OpenGL)/Part5_Trick.md
================================================
# 一、后处理特效


## 1. HDR

高动态范围(High Dynamic Range,HDR)

- 亮的东西可以变得非常亮,暗的东西可以变得非常暗,而且充满细节

- 显示器被限制为只能显示值为 0.0 到 1.0 间的颜色,但是在光照方程中却没有这个限制

- 通过使片段的颜色超过1.0,我们有了一个更大的颜色范围



HDR 原本只是被运用在摄影上,摄影师对同一个场景采取不同曝光拍多张照片,捕捉大范围的色彩值。这些图片被合成为 HDR 图片,从而综合不同的曝光等级使得大范围的细节可见




## 2. 泛光 Bloom

方法:

1. 根据一个阈值提取出图像中较亮的区域,把它们存储在一张渲染纹理上
2. 利用高斯模糊对这张渲染纹理进行模糊处理,从而模拟光线扩散的泛光效果
3. 将模糊后的图像和原图像进行混合



## 3. 运动模糊

运动模糊:真实世界中的摄像机的一种效果(相机曝光时,拍摄场景发生变化,就会产生模糊效果)

方法一:

- 步骤:利用累积缓存(accumulation buffer)来混合多张连续的图像,将它们取平均值来作为最后的模糊图像
- 缺点:性能消耗很大,因为在同一帧里要渲染多次场景
- 优化:只保存上一帧的渲染效果,不断把上一帧图像和当前图像叠加(alpha 混合),从而产生运动轨迹的视觉效果



方法二:

- 步骤:利用速度缓存(velocity buffer)存储各个像素当前的运动速度,利用改值来决定模糊的方向和大小
- 优化:通过上一帧的相机位置和投影得到上一帧点的坐标与当前帧求深度位置差,即运动速度。最后,通过运动速度决定了 3 X 3 的均值滤波的各个方向的采样步长来做模糊



## 4. 全局雾化

雾的计算:$Color_{out} = f * Color_{fog} +(1-f) * Color_{origin}$

雾的系数 $f$ 计算:使用噪声纹理强度图,乘以雾的系数,让雾的系数变化更自然

1. 线性,$d_{max}$ 和 $d_{min}$ 分别表示受雾影响的最小距离和最大距离
   $f = {d_{max} - |z| \over d_{max} - d_{min}}$
2. 指数,$d$ 控制雾浓度的参数
   $f = e^{-d-|z|}$
3. 指数的平方,$d$ 控制雾浓度的参数
   $f = e^{-(d-|z|)^2}$





# 二、其他

## 1. 描边

### 1.1 向外扩张

1. 让背面面片在视角空间下把模型顶点沿着法线的方向**向外扩张**一段距离,让背部轮廓可见
   为了防止背面面片有 Z 轴方向内凹的模型,先给让背面面片尽可能平整
   让所有背面面片的法线 Z 轴统一为一个定值,然后在归一化法线

   ```glsl
   viewNormal.z = -0.5;
   viewNormal = normalize(viewNormal);
   viewPos = viewPos + viewNormal * outlineWidth;
   ```

2. 使用轮廓线的颜色渲染背面的面片

3. 渲染正面面片



### 1.2 检测轮廓线

1. 检测边是否为轮廓线
   通过判断两个相邻的三角片面是否一个朝正面,一个朝背面
   $(n_0 \cdot v) * (n_1 \cdot v) < 0$ 其中,$n_0$ 和 $n_1$ 为相邻两个三角面的法向量,$v$ 是从视角指向顶点的方向
2. 单独渲染轮廓线(可以进行风格化渲染,水磨笔触的描边)





## 2. 3D 拾取

### 2.1 颜色拾取

1. **绘制颜色索引**
   创建 FrameBuffer,附着一张颜色纹理 RGB,一张深度纹理
   根据物体的 ID,物体的绘制批次 来给物体离屏渲染着色
    ```c
   #version 330
   
   uniform uint gObjectIndex; // 绘制对象的 ID:随着对象的更新而更新
   uniform uint gDrawIndex;   // 绘制批次的 ID:对象的绘制批次
   
   out vec3 FragColor;
   
   void main()
   {
     // gl_PrimitiveID:默认不使用 GS 为 0,使用 GS 时会被赋值,每次 drawcall 会更新
     // gl_PrimitiveID + 1:为了区分背景色黑色和索引色
     FragColor = vec3(float(gObjectIndex), float(gDrawIndex), float(gl_PrimitiveID + 1));
   }
    ```

2. **拾取颜色**
   通过 glReadPixels 拾取点选的颜色值,根据颜色值判断点击的物体
   
    ```c
    BYTE bArray[3];
    glReadPixels(mp.x, mp.y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, bArray);
    ```



### 2.2 射线拾取

1. **确定射线**
   将屏幕的点击位置映射到近平面和远平面的点,两个点的连线就是 射线

   ![](./images/ray.jpg)

   ```c
   // 直接调用 glm 的 unProject 函数来确定射线
   glm::vec3 glm::unProject(glm::vec3 const& win, 
                            glm::mat4 const& model, 
                            glm::mat4 const& proj, 
                            glm::ivec4 const& viewport);
   
   // 以下为 glm unProject 的具体实现
   template<typename T, typename U, qualifier Q>
   GLM_FUNC_QUALIFIER vec<3, T, Q> unProject(vec<3, T, Q> const& win, 
                                             mat<4, 4, T, Q> const& model, 
	                                          mat<4, 4, T, Q> const& proj, 
                                             vec<4, U, Q> const& viewport)
   {
   #		if GLM_CONFIG_CLIP_CONTROL & GLM_CLIP_CONTROL_ZO_BIT
   	return unProjectZO(win, model, proj, viewport);
   #		else
   	return unProjectNO(win, model, proj, viewport);
   #		endif
   }
   
   template<typename T, typename U, qualifier Q>
   GLM_FUNC_QUALIFIER vec<3, T, Q> unProjectNO(vec<3, T, Q> const& win, 
                                               mat<4, 4, T, Q> const& model, 
                                               mat<4, 4, T, Q> const& proj, 
                                               vec<4, U, Q> const& viewport)
   {
     mat<4, 4, T, Q> Inverse = inverse(proj * model);
   
     vec<4, T, Q> tmp = vec<4, T, Q>(win, T(1));
     tmp.x = (tmp.x - T(viewport[0])) / T(viewport[2]);
     tmp.y = (tmp.y - T(viewport[1])) / T(viewport[3]);
     tmp = tmp * static_cast<T>(2) - static_cast<T>(1);
   
     vec<4, T, Q> obj = Inverse * tmp;
     obj /= obj.w;
   
     return vec<3, T, Q>(obj);
   }
   
   template<typename T, typename U, qualifier Q>
   GLM_FUNC_QUALIFIER vec<3, T, Q> unProjectZO(vec<3, T, Q> const& win, 
                                               mat<4, 4, T, Q> const& model, 
                                               mat<4, 4, T, Q> const& proj, 
                                               vec<4, U, Q> const& viewport)
   {
     mat<4, 4, T, Q> Inverse = inverse(proj * model);
     
     vec<4, T, Q> tmp = vec<4, T, Q>(win, T(1));
     tmp.x = (tmp.x - T(viewport[0])) / T(viewport[2]);
     tmp.y = (tmp.y - T(viewport[1])) / T(viewport[3]);
     tmp.x = tmp.x * static_cast<T>(2) - static_cast<T>(1);
     tmp.y = tmp.y * static_cast<T>(2) - static_cast<T>(1);
   
     vec<4, T, Q> obj = Inverse * tmp;
     obj /= obj.w;
   
     return vec<3, T, Q>(obj);
   }
   ```
   
2. **找到射线的碰撞**
   将每个物体的碰撞体设置为球体,求射线与球心最近的对象
   判断射线是否与最近的球体相交(具体方法见 [三维距离检测/点到直线最近点](../LinearAlgebra/Part3_Triangles.md))



## 3. 粒子系统





## 4. 地貌生成

### 4.1 高度贴图

地貌的高低起伏一般通过编辑器在 CPU 内处理为 Mesh 数据,其基本原理是

1. 通过绘制黑白的高度图来生成 3D 地貌顶点 Mesh 数据
   一个高度图上的像素对应一个顶点的高度数据分量
2. 高度图的 UV 表示 Mesh 的 XZ 坐标
3. 高度图的像素颜色可以通过范围映射将 [0, 255]  映射为 [-128, 128] 来表示 Mesh 的 Y 轴坐标 (高低起伏,只修改高度值,不会修改水平面坐标)
4. 根据 Mesh 在生成顶点法线等顶点属性

![](./images/landscape.png)



### 4.2 植被覆盖

植被可以是树、花、草,每一种绘制流程都相似,以草为例:
通过 Geometry Shader 来实时生成固定的草的 Mesh 顶点并绘制

1. 一个草的片面由 4 个顶点构成的 2 个三角形
2. 草可以有多种类型的片面
3. 需要一张对应高度贴图的草种类贴图来描述每个位置草的种类
4. 通过 GS,将一个地貌 Mesh 的顶点扩展为
   一个草的片面(像向日葵一样跟随相机朝向)
   三个草的片面(根据固定偏移随机旋转,让每个顶点不同,每一帧的每个顶点相同的随机值,相同的旋转)
5. 根据草种类纹理选择草的种类,绘制草的片面
6. 可以使用三角函数,通过整体上下周期偏移来制造草的高低起伏(做到风吹草浪的效果)



制作植被时,有以下情况还需要注意:

![](./images/foliage.png)





# 三、游戏引擎

## 1. Handle 的作用

**Handle 类似于指针,实际上是一个整数类型,不直接引用内存,可以有以下映射内存的方式**

- 作为索引直接引用
- 经过一系列加密方法映射到内存地址
  比如:用 8 位密码将 16 位索引加密。将 4 位类型、4 位权限、8 位密码、16 位加密索引之后打包成一个 32 位的整数作为 Handle



**Handle 的类型**

通过给 Handle 套上结构体,确保在内存占用不变的情况下让编译器区分 Handle 类型,将问题前置到编译阶段

```c
struct VertexBufferHandle { uint16_t idx; };
struct ProgramHandle { uint16_t idx; };
```



**设计接口时 Handle 比指针优势**

1. 指针作用太强,可做的事情太多
   接口设计中,功能刚刚好就够了,并非越多权限越好的,权限越多就越危险(不容易解耦)
2. Handle 只是个整数,里面实现可以隐藏起来
   假如直接暴露了指针,也就暴露了指针类型,用户就会看到更多的细节
3. 所有资源在内部管理,通过 Handle 作为中间层,可以有效判断 Handle 是否合法,而防止了野指针的情况
4. Handle 只是个整数,所有的语言都有整数这种类型,但并非所有语言都有指针
   接口只出现 Handle,方便将实现绑定到各种语言





# Reference

- [3D Picking](http://ogldev.atspace.co.uk/www/tutorial29/tutorial29.html)
- [light house / opengl-selection-tutorial](http://www.lighthouse3d.com/tutorials/opengl-selection-tutorial/)
- [learnopengl-Bloom](https://learnopengl-cn.github.io/05 Advanced Lighting/07 Bloom/)
- [learnopengl-HDR](https://learnopengl-cn.github.io/05 Advanced Lighting/06 HDR/)
- [learnopengl-AntiAliasing](https://learnopengl-cn.github.io/04 Advanced OpenGL/11 Anti Aliasing/)
- [HDR Tone Mapping](https://zhuanlan.zhihu.com/p/26254959)
- [OGL-Particle System using Transform Feedback](http://ogldev.atspace.co.uk/www/tutorial28/tutorial28.html)
- [Open Dynamics Engine](http://www.ode.org)
- [Open Dynamics Engine Doc](http://ode.org/ode-latest-userguide.html)
- [bgfx 学习笔记(5)- Handle 的作用和分配](https://zhuanlan.zhihu.com/p/63012167)
- [OGRE 的渲染流程分析](https://zhuanlan.zhihu.com/p/113368993)
- [Redundancy vs dependencies: which is worse?](http://yosefk.com/blog/redundancy-vs-dependencies-which-is-worse.html)
- [Multilayered Terrain](https://www.mbsoftworks.sk/tutorials/opengl3/21-multilayered-terrain/)
- [Terrain Pt. 2 - Waving Grass](https://www.mbsoftworks.sk/tutorials/opengl3/29-terrain-pt2-waving-grass/)
- [Creating a Stylized Chaparral Environment in UE4](https://80.lv/articles/creating-a-stylized-chaparral-environment-in-ue4/)



================================================
FILE: ComputerGraphics(OpenGL)/README.md
================================================
# Computer Graphics(OpenGL)Notes

This notes written in [Typora](https://www
Download .txt
gitextract_ek_1s3xw/

├── .gitignore
├── ComputerGraphics(OpenGL)/
│   ├── EXT0_GLBuffers&MultiSample.md
│   ├── EXT1_FileFormat.md
│   ├── EXT2_HardwareSupport.md
│   ├── EXT3_Platform.md
│   ├── Part0_Context&Pipeline.md
│   ├── Part1_Light&ShadowInGame.md
│   ├── Part2_PhysicalLight.md
│   ├── Part3_Texture.md
│   ├── Part4_Animation.md
│   ├── Part5_Trick.md
│   └── README.md
├── DigitalImageProcessing/
│   ├── Part0_Signals&Systems.md
│   ├── Part1_Filtering.md
│   ├── Part2_Colors.md
│   ├── Part3_PhotoShop.md
│   └── README.md
├── LinearAlgebra/
│   ├── Part0_Base.md
│   ├── Part1_Matrix.md
│   ├── Part2_Quaternion.md
│   ├── Part3_Triangles.md
│   └── README.md
├── README.md
└── UnrealEngine4/
    ├── Part0_Base.md
    └── README.md
Condensed preview — 25 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (477K chars).
[
  {
    "path": ".gitignore",
    "chars": 147,
    "preview": ".DS_Store\n*.key\n*/images/*.log\nUnrealEngine4/Part3_AnimationSystem.md\nUnrealEngine4/Part4_LandscapeSystem.md\nUnrealEngin"
  },
  {
    "path": "ComputerGraphics(OpenGL)/EXT0_GLBuffers&MultiSample.md",
    "chars": 17522,
    "preview": "# 一、顶点信息传输\n\n## 1. 立即模式 glBegin()/glEnd()\n\n方式:立即绘制\n\n优点:功能适配范围广,写法直观\n缺点:频繁调用 OpenGL 函数,效率低,共享点使用次数多\n\n例子:\n\n1. 直接提交 OpenGL 命"
  },
  {
    "path": "ComputerGraphics(OpenGL)/EXT1_FileFormat.md",
    "chars": 7223,
    "preview": "# 一、图片存储格式\n\n## 1. BMP 文件\n\n无损的图片格式,全称 Bitmap(无压缩,体积大)\n可以直接存储图片数据,也可以采用索引表的存储方式。但即便采用了索引表的存储方式,也不能使体积缩小太多。图片的内存格式\n\n适合:logo"
  },
  {
    "path": "ComputerGraphics(OpenGL)/EXT2_HardwareSupport.md",
    "chars": 15842,
    "preview": "# 一、 GPU 的硬件架构\n\nGPU 主要由 **显存 Device Memory** 和 **流多处理器 Stream Multiprocessors ** 组成(Stream Processors,SP 是 SM 中的一个 Core)"
  },
  {
    "path": "ComputerGraphics(OpenGL)/EXT3_Platform.md",
    "chars": 21961,
    "preview": "# 零、代码 Debug\n\n> 关于性能的衡量 帧率/单帧时间,**建议采用单帧时间**\n>\n> 由于 帧率 = 60 / 单帧时间,他们之间的关系不是线性的,例如在减少同样时间消耗的情况下:\n> 低帧率区间进步缓慢,每秒 10fps 下,"
  },
  {
    "path": "ComputerGraphics(OpenGL)/Part0_Context&Pipeline.md",
    "chars": 26271,
    "preview": "# 一、OpenGL 简介\n\n>  OpenGL 作为图形硬件标准,是最通用的图形管线版本\n>  使用 OpenGL 自带的数据类型可以确保各平台中每一种类型的大小都是统一的\n>\n>  **OpenGL 只是一个标准/规范,具体的实现是由驱"
  },
  {
    "path": "ComputerGraphics(OpenGL)/Part1_Light&ShadowInGame.md",
    "chars": 38862,
    "preview": "# 一、光\n\n## 1. 颜色计算\n\n常用的计算光照颜色的方法:\n\n- 物体反射的颜色(我们感知到的颜色):光源的颜色 * 物体的颜色\n- 多光源的情况下,一般都是将各个光源的颜色累加起来,最后得出最终的颜色\n- 同一个光源的光衰减系数是一"
  },
  {
    "path": "ComputerGraphics(OpenGL)/Part2_PhysicalLight.md",
    "chars": 36757,
    "preview": "# 一、概率与统计\n\n**随机变量** $X$​:可能取很多不同值的变量\n\n**随机变量分布函数** $X \\sim p(x)$​​​​​​​:\n连续的分布函数又称**概率密度函数** Probability Density Functio"
  },
  {
    "path": "ComputerGraphics(OpenGL)/Part3_Texture.md",
    "chars": 21623,
    "preview": "# 一、颜色插值\n\n根据三角形三个顶点的颜色来求整个三角面的插值颜色\n\n## 1. 普通的插值方法\n\n方法一:根据**高度比**来插值\n\n![](./images/linear_interpolate_triangle.png)\n$$\n\\b"
  },
  {
    "path": "ComputerGraphics(OpenGL)/Part4_Animation.md",
    "chars": 4464,
    "preview": "# 一、纹理动画\n\n## 1. 序列帧动画\n\n序列帧动画:\n\n- 依次播放一系列关键帧图片,当播放速度达到一定数值时,看起来就是一个连续的动画\n- 多用于游戏 2D 中做循环动作的角色,或者制作 3D 模型,做三渲二的序列帧效果\n\n\n\n优点"
  },
  {
    "path": "ComputerGraphics(OpenGL)/Part5_Trick.md",
    "chars": 7468,
    "preview": "# 一、后处理特效\n\n\n## 1. HDR\n\n高动态范围(High Dynamic Range,HDR)\n\n- 亮的东西可以变得非常亮,暗的东西可以变得非常暗,而且充满细节\n\n- 显示器被限制为只能显示值为 0.0 到 1.0 间的颜色,但"
  },
  {
    "path": "ComputerGraphics(OpenGL)/README.md",
    "chars": 196,
    "preview": "# Computer Graphics(OpenGL)Notes\n\nThis notes written in [Typora](https://www.typora.io/). It is also recommended to use "
  },
  {
    "path": "DigitalImageProcessing/Part0_Signals&Systems.md",
    "chars": 11104,
    "preview": "# 一、信号与系统\n\n信号可以来自于很多外部设备:录音机、温度计、相机\n\n**信号处理的方向**:并不关心数据以怎样自然规律产生,而是重点放在如何改变输入的信号数据\n\n- 模拟信号:连续不断的,针对具体外界做出的具体测量值,没有取值范围\n-"
  },
  {
    "path": "DigitalImageProcessing/Part1_Filtering.md",
    "chars": 10729,
    "preview": "# 一、图像处理基本\n\n\n## 1. 光谱即是电磁波谱\n\n- 波长:从红光到紫光,波长不断变短(对于可见光,波长不同,颜色不同)\n\n  ![](images/waveLength.png)\n\n- 灰度级:从黑到白的单色光度量范围\n  单色光"
  },
  {
    "path": "DigitalImageProcessing/Part2_Colors.md",
    "chars": 16407,
    "preview": "# 一、彩色图像处理\n\n人眼对明暗比颜色更加敏感,RGB 色光三原色中人眼对绿色比较敏感\n\n\n\n## 1. 颜色的特性\n\n### 1.1  颜色的心理学特征:人对光的感觉而产生的光的特性\n\n**色彩 Hue:**\n光谱(一定电磁频谱范围内)"
  },
  {
    "path": "DigitalImageProcessing/Part3_PhotoShop.md",
    "chars": 4398,
    "preview": "<h1><center>PS 中的图像处理技术</center></h1>\n\n\n\n# 一、混合透明模式\n\n**声明**\n\n以下公式描述中:\n\n- 0代表纯黑色 0x000\n  1代表纯白色 0xFFF\n- a 表示当前图层 (activit"
  },
  {
    "path": "DigitalImageProcessing/README.md",
    "chars": 377,
    "preview": "# [Digital Image Processing](https://www.amazon.cn/%E5%9B%BE%E4%B9%A6/dp/B00HNC8OYC) Notes\n\n- This notes written in [Typ"
  },
  {
    "path": "LinearAlgebra/Part0_Base.md",
    "chars": 14822,
    "preview": "[TOC]\n\n# 一、向量\n\n## 1. 概念\n\n- 起点是原点\n- 向量 = 方向 + 大小\n- **位置移动** 的数值记录\n- **线性变换** 的数值记录\n\n\n\n##2. 基本运算 \n\n- 向量的加减\n  ![](images/ve"
  },
  {
    "path": "LinearAlgebra/Part1_Matrix.md",
    "chars": 13848,
    "preview": "[TOC]\n\n# 一、矩阵变换\n\n## 1. 基础知识\n\n### 1.1 GPU 中矩阵间的计算方式\n\n> 注意:\n>\n> 1. 在矩阵乘法中,顺序很重要,变换的几何意义由 右 -> 左 变换\n> 2. 建议在组合矩阵时,先缩放,后旋转,最"
  },
  {
    "path": "LinearAlgebra/Part2_Quaternion.md",
    "chars": 15073,
    "preview": "[TOC]\n\n# 一、3D 中的方位与角位移\n\n**方位**:从上一方位旋转后的 结果值(单一状态,用欧拉角表示)\n**角位移**:相对于上一方位旋转后的 偏移量(用四元数、矩阵表示)\n\n\n\n## 1. 欧拉角 (Euler angles)"
  },
  {
    "path": "LinearAlgebra/Part3_Triangles.md",
    "chars": 8952,
    "preview": "[TOC]\n\n# 一、三角形\n\n## 1. 内心\n\n三角形内心(三角形内切圆的圆心):三角形内角平分线的交点\n\n已知三点坐标,求三角形内心坐标,[证明过程](https://www.zybang.com/question/272657890"
  },
  {
    "path": "LinearAlgebra/README.md",
    "chars": 681,
    "preview": "# [Linear Algebra](https://space.bilibili.com/88461692#/channel/detail?cid=9450) Notes\n\n1. This notes written in [Typora"
  },
  {
    "path": "README.md",
    "chars": 1171,
    "preview": "#  CRASH NOTES\n\nThis notes written in [Typora](https://www.typora.io/). It is also recommended to use it to read.\n\n\n\n## "
  },
  {
    "path": "UnrealEngine4/Part0_Base.md",
    "chars": 24723,
    "preview": "# 一、UE4 的开发流程\n\n> 这里以 Win 平台的开发流程为例子,其他的平台在 **UE 4.21.0** 源码的 readme 和 [UE 开发者文档](https://docs.unrealengine.com/4.26/zh-C"
  },
  {
    "path": "UnrealEngine4/README.md",
    "chars": 517,
    "preview": "# [Unreal Engine 4](https://www.unrealengine.com/) Notes\n\nThis note is only used to record the **application of UE4** ac"
  }
]

About this extraction

This page contains the full source code of the CatOnly/CrashNote GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 25 files (313.6 KB), approximately 148.5k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!