[
  {
    "path": ".gitignore",
    "content": ".DS_Store\n*.key\n*/images/*.log\nUnrealEngine4/Part3_AnimationSystem.md\nUnrealEngine4/Part4_LandscapeSystem.md\nUnrealEngine4/Part5_ParticleSystem.md\n"
  },
  {
    "path": "ComputerGraphics(OpenGL)/EXT0_GLBuffers&MultiSample.md",
    "content": "# 一、顶点信息传输\n\n## 1. 立即模式 glBegin()/glEnd()\n\n方式：立即绘制\n\n优点：功能适配范围广，写法直观\n缺点：频繁调用 OpenGL 函数，效率低，共享点使用次数多\n\n例子：\n\n1. 直接提交 OpenGL 命令到 GPU\n\n```c\n// Note that not all of OpenGL commands can be placed in between glBegin() and glEnd()\n// Only a subset of commands can be used\n// glVertex*(), glColor*(), glNormal*(), glTexCoord*(), glMaterial*(), glCallList(), etc.\nglBegin(GL_TRIANGLES);\n    glColor3f(1, 0, 0); // set vertex color to red\n    glVertex3fv(v1);    // draw a triangle with v1, v2, v3\n    glVertex3fv(v2);\n    glVertex3fv(v3);\nglEnd();\n```\n\n2. 将命令放到 DisplayList 后，批量一次传入到 GPU\n   DisplayList 会将其中命令的所有资源存储到自己的内存中\n   DisplayList 是服务端的状态，本身存储在 GPU 缓存中，只能存储与服务端有关的部分命令\n   DisplayList 的命令和数据一旦上传便不可修改\n\n```c\n// create one display list\nGLuint index = glGenLists(1);\n\n// compile the display list, store a triangle in it\n// Option: GL_COMPILE or GL_COMPILE_AND_EXECUTE(render)\nglNewList(index, GL_COMPILE);\n    glBegin(GL_TRIANGLES);\n    glVertex3fv(v0);\n    glVertex3fv(v1);\n    glVertex3fv(v2);\n    glEnd();\nglEndList();\n...\n\n// draw the display list\nglCallList(index);\n...\n\n// delete it if it is not used any more\nglDeleteLists(index, 1);\n```\n\n\n\n## 2. VertexArray\n\n方式：批量数据传入绘制\n\n优点：数据以数组的形式**存储在应用缓存**，减少了 OpenGL 函数的频繁调用\n缺点：每次绘制都要占用带宽上传到显存\n\n例子：\n\n```c\nGLfloat vertices[] = {...}; // 36 of vertex coords\n\nglUseProgram(progId);\n\n// activate and specify pointer to vertex array\n// 因为 vertices 存储在应用程序上，所以这里 enable client state\nglEnableClientState(GL_VERTEX_ARRAY);\n// 也可以用 \n// glNormalPointer、glColorPointer、glIndexPointer、glTexCoordPointer、glEdgeFlagPointer\nglVertexPointer(3, GL_FLOAT, 0, vertices);\n\n// draw a cube\n// 也可以用 glDrawElements、glDrawRangeElements\nglDrawArrays(GL_TRIANGLES, 0, 36);\n\n// deactivate vertex arrays after drawing\nglDisableClientState(GL_VERTEX_ARRAY);\n```\n\n\n\n## 3. VertexBuffer\n\n方式：批量数据传入绘制\n\n优点：\n1. 数据以数组的形式**存储在显卡高速缓存**，每次使用时不用重新上传，只需要在显卡绑定即可\n2. 数据由于存储在显存，可以被应用程序在不同线程访问和修改\n\n例子：\n\n1. 创建和销毁\n\n```c\nGLuint vboId;                              // ID of VBO\nGLfloat* vertices = new GLfloat[vCount*3]; // create vertex array\n\n// generate a new VBO and get the associated ID\nglGenBuffers(1, &vboId);\n\n// bind VBO in order to use\n// Option: GL_ARRAY_BUFFER or GL_ELEMENT_ARRAY_BUFFER\n// This Option assists VBO to decide the most efficient locations of buffer objects\n// For example, some systems may prefer indices in AGP or system memory, and vertices in video memory\n// 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.\nglBindBuffer(GL_ARRAY_BUFFER, vboId);\n\n// upload data to VBO\n// Option: glBufferSubData, GL_[STATIC/DYNAMIC/STREAM]_[DRAW/READ/COPY]\n// GL_STATIC_DRAW 决定了数据的存储位置\n// Static: 更新一次，使用多次\n// Dynamic: 不断更新，使用多次\n// Stream: 更新一次，最多使用几次\n// Draw: application upload to GPU\n// Read: GPU copy to application\n// Copy: Draw and Read\nglBufferData(GL_ARRAY_BUFFER, dataSize, vertices, GL_STATIC_DRAW);\n\n// it is safe to delete after copying data to VBO\ndelete [] vertices;\n\n// delete VBO when program terminated\nglDeleteBuffers(1, &vboId);\n```\n\n2. 过去的使用方式：不同的 API  开启/关闭 不同的顶点属性\n```c\nglUseProgram(progId);\n\n// bind VBOs for vertex array and index array\nglBindBuffer(GL_ARRAY_BUFFER, vboId1);            // for vertex attributes\nglBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboId2);    // for indices\n\nglEnableClientState(GL_VERTEX_ARRAY);             // activate vertex position array\nglEnableClientState(GL_NORMAL_ARRAY);             // activate vertex normal array\nglEnableClientState(GL_TEXTURE_COORD_ARRAY);      // activate texture coord array\n\n// do same as vertex array except pointer\nglVertexPointer(3, GL_FLOAT, stride, offset1);    // last param is offset, not ptr\nglNormalPointer(GL_FLOAT, stride, offset2);\nglTexCoordPointer(2, GL_FLOAT, stride, offset3);\n\n// draw 6 faces using offset of index array\nglDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_BYTE, 0);\n\nglDisableClientState(GL_VERTEX_ARRAY);            // deactivate vertex position array\nglDisableClientState(GL_NORMAL_ARRAY);            // deactivate vertex normal array\nglDisableClientState(GL_TEXTURE_COORD_ARRAY);     // deactivate vertex tex coord array\n\n// bind with 0, so, switch back to normal pointer operation\nglBindBuffer(GL_ARRAY_BUFFER, 0);\nglBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);\n```\n\n3. OpenGL 2.0 + 使用方式：同一个 API 开启/关闭 不同的顶点属性\n```c\nglUseProgram(progId);\n\n// bind VBOs for vertex array and index array\nglBindBuffer(GL_ARRAY_BUFFER, vboId1);            // for vertex coordinates\nglBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboId2);    // for indices\n\nglEnableVertexAttribArray(attribVertex);          // activate vertex position array\nglEnableVertexAttribArray(attribNormal);          // activate vertex normal array\nglEnableVertexAttribArray(attribTexCoord);        // activate texture coords array\n\n// set vertex arrays with generic API\nglVertexAttribPointer(attribVertex, 3, GL_FLOAT, false, stride, offset1);\nglVertexAttribPointer(attribNormal, 3, GL_FLOAT, false, stride, offset2);\nglVertexAttribPointer(attribTexCoord, 2, GL_FLOAT, false, stride, offset3);\n\n// draw 6 faces using offset of index array\nglDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_BYTE, 0);\n\nglDisableVertexAttribArray(attribVertex);         // deactivate vertex position\nglDisableVertexAttribArray(attribNormal);         // deactivate vertex normal\nglDisableVertexAttribArray(attribTexCoord);       // deactivate texture coords\n\n// bind with 0, so, switch back to normal pointer operation\nglBindBuffer(GL_ARRAY_BUFFER, 0);\nglBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);\n```\n\n4. 更新 VAO\n```c\n// 方法 1：重新向 GPU 上传数据（缺点：应用程序和 GPU 要存储 2 份数据，每次更新都要占用带宽）\nglBufferData(GL_ARRAY_BUFFER, dataSize, vertices, GL_STATIC_DRAW);\n\n// 方法 2：通过映射 GPU 上缓存数据地址到应用程序的缓存地址\n//        将应用程序对地址的操作用于对 GPU 缓存操作，达到在应用程序控制 GPU 缓存的效果\n\n// bind then map the VBO\nglBindBuffer(GL_ARRAY_BUFFER, vboId);\n\n// Option: GL_READ_ONLY, GL_WRITE_ONLY, GL_READ_WRITE\n// 如果 GPU 正在使用这个 buffer，将会返回 NULL\nfloat* ptr = (float*)glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);\n\n// if the pointer is valid(mapped), update VBO\nif(ptr)\n{\n    updateMyVBO(ptr, ...);          // custom function modify buffer data\n    glUnmapBuffer(GL_ARRAY_BUFFER); // unmap it after use it's return GLboolean\n}\n```\n\n\n\n# 二、Pixel Buffer\n\n## 1. 创建和使用\n\nPixel Buffer Object 由 Vertex Buffer Object 扩展而来，因此对 Pixel Buffer 操作的许多细节和接口都与 Vertex Buffer 保持一致，这里不在赘述\n\n例子：\n\n```c\nGLuint pboIds[2];\n\n// Create\nglGenBuffers(2, pboIds);\n\n// Bind\n// Option: GL_PIXEL_UNPACK_BUFFER / GL_PIXEL_PACK_BUFFER\nglBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[0]);\n// load data rgba\nglBufferData(GL_PIXEL_UNPACK_BUFFER, 720 * 1280 * 4, NULL, GL_STREAM_DRAW);\n// unbind\nglBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);\n...\n\n// Note that glMapBuffer() causes sync issue\n// If GPU is working with this buffer, glMapBuffer() will wait(stall)\n// until GPU to finish its job. To avoid waiting (idle), you can call\n// first glBufferData() with NULL pointer before glMapBuffer()\n// If you do that, the previous data in PBO will be discarded and\n// glMapBuffer() returns a new allocated pointer immediately\n// even if GPU is still working with the previous data\nglBufferData(GL_PIXEL_UNPACK_BUFFER, 720 * 1280 * 4, NULL, GL_STREAM_DRAW);\n\n// Mapping PBO\nglBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[0]);\nGLubyte* ptr = (GLubyte*)glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY);\nif(ptr)\n{\n  // Custom function: update data directly on the mapped buffer\n  updatePixels(ptr, DATA_SIZE);\n  // release pointer to mapping buffer\n  glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);\n}\n\n// Delete PBO\nglDeleteBuffers(1, &pboIds);\n```\n\n\n\n## 2. PBO、FBO、Texture Object\n\n![](./images/PBO_FBO_TO.png)\n\n**Pack（OpenGL to Application）**\n**glReadPixels**\n\n1. 从 frame buffer 中读取数据\n2. 将 frame buffer 数据写入 Pixel buffer\n\n\n\n**Unpack（Application to OpenGL）**\n**glDrawPixels**\n\n1. 从 Pixel buffer 读取数据\n2. 将 Pixel buffer 数据写入 frame buffer\n\n\n\n## 3. Direct Memory Access\n\n将数据转换到 Pixel Buffer Object 很快是由于：转到 PBO 的数据将会直接进入显卡缓存中，不通过 CPU 的调度\n\n例如在加载纹理时：\n\n1. 不使用 PBO\n   在 CPU 的调度下，将图片资源加载到系统缓存，然后从系统缓存拷贝到 OpenGL 的纹理对象\n2. 使用 PBO\n   直接加载到 OpenGL 里的 PBO 下，然后拷贝到纹理对象\n   整个过程由 GPU 完成，可以和 CPU 异步执行，效率提高\n\n![](./images/loadTexture.png)\n\n例子：\n\n普通加载纹理的方式\n\n```c\n// Data is from CPU memory\nglBindTexture(GL_TEXTURE_2D, textureId);\nglTexImage2D(GL_TEXTURE_2D, 0, GL_BGRA, width, height, 0, GL_BGRA, GL_UNSIGNED_BYTE, data);\n```\n\n\n\n通过 PBO 加载纹理（PBO 创建时已经加载纹理数据）\n\n```c\n// bind the texture and PBO\nglBindTexture(GL_TEXTURE_2D, textureId);\nglBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[index]);\n\n// copy pixels from PBO to texture object so the last param is 0 not data\n// Use offset instead of pointer\nglTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_BGRA, GL_UNSIGNED_BYTE, 0);\n\n// it is good idea to release PBOs with ID 0 after use\n// Once bound with 0, all pixel operations are back to normal ways\nglBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);\n```\n\n\n\n通过 PBO 加载/读取 Frame Buffer\n\n```c\n// set the target framebuffer to read\nglReadBuffer(GL_FRONT);\n\n// read pixels from framebuffer to PBO\n// glReadPixels() should return immediately.\nglBindBuffer(GL_PIXEL_PACK_BUFFER, pboIds[index]);\nglReadPixels(0, 0, WIDTH, HEIGHT, GL_BGRA, GL_UNSIGNED_BYTE, 0);\n```\n\n\n\n\n\n# 三、Frame Buffer\n\n> 定义：framebuffer 是 OpenGL 一系列数据存储的集合\n\n**浮点帧缓冲 (Floating Point Framebuffer)**\n当一个帧缓冲的颜色缓冲的内部格式被设定成了 `GL_RGB16F`, `GL_RGBA16F`, `GL_RGB32F`  或者 `GL_RGBA32F` 时，这些帧缓冲被叫做，浮点帧缓冲可以存储超过 0.0 到 1.0 范围的浮点值\n\n当帧缓冲使用了一个标准化的定点格式(像 `GL_RGB` )为其颜色缓冲的内部格式，OpenGL 会在将这些值存入帧缓冲前自动将其约束到 0.0 到 1.0 之间\n\n\n\n## 1. 不同种类的 framebuffer\n\n1. **Default framebuffer**\n   本地窗口系统创建和使用的 framebuffer，一定会显示到屏幕上，是本地系统创建 Context 的一部分，当 `glBindFramebuffer(GL_FRAMEBUFFER, 0);` 时，绑定的就是当前窗口系统提供的 default framebuffer\n   这种 framebuffer 有本地窗口系统 API 创建提供，由 OpenGL 将其作为自己的输出给本地窗口系统来使用\n   包含：多个（至少一个）色彩缓冲、一个深度缓冲、一个模板缓冲、一个累积缓冲\n  2. **Frame Buffer Object**\n     OpenGL 创建和使用的 framebuffer，可以不显示到屏幕上\n     提供一个 FBO 对象来供 OpenGL 操作，FBO 对象可以有**多个（至少一个）色彩缓冲**，一个深度缓冲，一个模板缓冲（没有累积缓冲）\n\n\n\n## 2. 内部数据对象\n\n> framebuffer 提供的内部数据都以 attach 方式来赋予，并非内部创建\n> 因此 framebuffer 内部数据的切换都要比单独切换 framebuffer 本身要快\n\n切换 texture：**glFramebufferTexture2D**\n\n1. attach 的 textureid 为 0，之前的纹理将会从 frame buffer 上解绑\n2. 删除纹理时会自动从当前绑定的 framebuffer 上解绑，但如果当前纹理并不会从其他已 attach 的**非当前绑定的** framebuffer 解绑\n\n\n\n切换 renderbuffer：**glFramebufferRenderbuffer**\n1. renderbuffer 主要用来存储一些逻辑数据，而非图像数据\n\n2. 可以通过 glGetRenderbufferParameteriv 来获取 renderbuffer 里数据的一些属性\n\n   ```c\n   int width;\n   // Option: \n   // GL_RENDERBUFFER_WIDTH\n   // GL_RENDERBUFFER_HEIGHT\n   // GL_RENDERBUFFER_INTERNAL_FORMAT\n   // GL_RENDERBUFFER_RED_SIZE\n   // GL_RENDERBUFFER_GREEN_SIZE\n   // GL_RENDERBUFFER_BLUE_SIZE\n   // GL_RENDERBUFFER_ALPHA_SIZE\n   // GL_RENDERBUFFER_DEPTH_SIZE\n   // GL_RENDERBUFFER_STENCIL_SIZE\n   glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &width);\n   ```\n\n   \n\n## 3. 创建和使用\n\n例子：\n\n```c\nGLuint fboId;\n\n// create a framebuffer object, you need to delete them when program exits.\nglGenFramebuffers(1, &fboId);\nglBindFramebuffer(GL_FRAMEBUFFER, fboId);\n\n// create a renderbuffer object to store depth info\n// NOTE: A depth renderable image should be attached the FBO for depth test.\n// If we don't attach a depth renderable image to the FBO, then\n// the rendering output will be corrupted because of missing depth test.\n// If you also need stencil test for your rendering, then you must\n// attach additional image to the stencil attachement point, too.\nglGenRenderbuffers(1, &rboDepthId);\nglBindRenderbuffer(GL_RENDERBUFFER, rboDepthId);\n// allocat memory for renderbuffer\nglRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, TEXTURE_WIDTH, TEXTURE_HEIGHT);\n//glRenderbufferStorageMultisample(GL_RENDERBUFFER, fboSampleCount, GL_DEPTH_COMPONENT, TEXTURE_WIDTH, TEXTURE_HEIGHT);\nglBindRenderbuffer(GL_RENDERBUFFER, 0);\n\n// attach a texture to FBO color attachement point\nglFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureId, 0);\n//glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureId, 0);\n\n// attach a renderbuffer to depth attachment point\nglFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboDepthId);\n\n//@@ disable color buffer if you don't attach any color buffer image,\n//@@ for example, rendering the depth buffer only to a texture.\n//@@ Otherwise, glCheckFramebufferStatus will not be complete.\n//glDrawBuffer(GL_NONE);\n//glReadBuffer(GL_NONE);\n\n// trigger mipmaps generation explicitly\n// NOTE: If GL_GENERATE_MIPMAP is set to GL_TRUE, then glCopyTexSubImage2D()\n// triggers mipmap generation automatically. However, the texture attached\n// onto a FBO should generate mipmaps manually via glGenerateMipmap().\nglBindTexture(GL_TEXTURE_2D, textureId);\nglGenerateMipmap(GL_TEXTURE_2D);\nglBindTexture(GL_TEXTURE_2D, 0);\n```\n\n\n\n## 4. Multi Sample Anti Aliasing\n\n多重采样抗锯齿功能**不会自动打开**\n\n例子\n\n```c\n// Open MSAA\nglEnable(GL_MULTISAMPLE); // default is enable\n\n// create a 4x MSAA renderbuffer object for colorbuffer\nint msaa = 4;\nGLuint rboColorId;\nglGenRenderbuffers(1, &rboColorId);\nglBindRenderbuffer(GL_RENDERBUFFER, rboColorId);\nglRenderbufferStorageMultisample(GL_RENDERBUFFER, msaa, GL_RGB8, width, height);\n\n// create a 4x MSAA renderbuffer object for depthbuffer\nGLuint rboDepthId;\nglGenRenderbuffers(1, &rboDepthId);\nglBindRenderbuffer(GL_RENDERBUFFER, rboDepthId);\n// msaa: samples count\n// get max count by use glGetIntegerv(GL_MAX_SAMPLES, &max_samples_count);\nglRenderbufferStorageMultisample(GL_RENDERBUFFER, msaa, GL_DEPTH_COMPONENT, width, height);\n\n// create a 4x MSAA framebuffer object\nGLuint fboMsaaId;\nglGenFramebuffers(1, &fboMsaaId);\nglBindFramebuffer(GL_FRAMEBUFFER, fboMsaaId);\n\n// attach colorbuffer image to FBO\nglFramebufferRenderbuffer(GL_FRAMEBUFFER,       // 1. fbo target: GL_FRAMEBUFFER\n                          GL_COLOR_ATTACHMENT0, // 2. color attachment point\n                          GL_RENDERBUFFER,      // 3. rbo target: GL_RENDERBUFFER\n                          rboColorId);          // 4. rbo ID\n\n// attach depthbuffer image to FBO\nglFramebufferRenderbuffer(GL_FRAMEBUFFER,       // 1. fbo target: GL_FRAMEBUFFER\n                          GL_DEPTH_ATTACHMENT,  // 2. depth attachment point\n                          GL_RENDERBUFFER,      // 3. rbo target: GL_RENDERBUFFER\n                          rboDepthId);          // 4. rbo ID\n```\n\n\n\n### 4.1 多重采样 转 单采样\n\n**多重采样后 framebuffer 的渲染结果不能直接使用，需要转换成 single-sample image 才能使用**\n\n转换例子\n\n```c\n// copy rendered image from MSAA (multi-sample) to normal (single-sample)\n// NOTE: The multi samples at a pixel in read buffer will be converted\n// to a single sample at the target pixel in draw buffer.\nglBindFramebuffer(GL_READ_FRAMEBUFFER, fboMsaaId); // src FBO (multi-sample)\nglBindFramebuffer(GL_DRAW_FRAMEBUFFER, fboId);     // dst FBO (single-sample)\n\nglBlitFramebuffer(0, 0, width, height,             // src rect\n                  0, 0, width, height,             // dst rect\n                  GL_COLOR_BUFFER_BIT,             // buffer mask(which buffers are copied)\n                  GL_LINEAR);                      // scale filter\n```\n\n\n\n### 4.2 从 shader 里获取多重采样结果\n\n```c\n// shader 里自定义多重纹理采样\n\n// 1. 使用 sampler2DMS 而不是 sampler2D\nuniform sampler2DMS screenTextureMS; \n\n// 2. 使用 texelFetch\nvec4 colorSample = texelFetch(screenTextureMS, TexCoords, 3);  // 取第4个子样本\n```\n\n\n\n\n\n## 5. 检查 framebuffer 的状态\n\n例子\n\n```c\nbool checkFramebufferStatus(GLuint fbo)\n{\n    // check FBO status\n    glBindFramebuffer(GL_FRAMEBUFFER, fbo); // bind\n    GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);\n    switch(status)\n    {\n    case GL_FRAMEBUFFER_COMPLETE:\n        std::cout << \"Framebuffer complete.\" << std::endl;\n        return true;\n\n    case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT:\n        std::cout << \"[ERROR] Framebuffer incomplete: Attachment is NOT complete.\" << std::endl;\n        return false;\n\n    case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:\n        std::cout << \"[ERROR] Framebuffer incomplete: No image is attached to FBO.\" << std::endl;\n        return false;\n/*\n    case GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS:\n        std::cout << \"[ERROR] Framebuffer incomplete: Attached images have different dimensions.\" << std::endl;\n        return false;\n\n    case GL_FRAMEBUFFER_INCOMPLETE_FORMATS:\n        std::cout << \"[ERROR] Framebuffer incomplete: Color attached images have different internal formats.\" << std::endl;\n        return false;\n*/\n    case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER:\n        std::cout << \"[ERROR] Framebuffer incomplete: Draw buffer.\" << std::endl;\n        return false;\n\n    case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER:\n        std::cout << \"[ERROR] Framebuffer incomplete: Read buffer.\" << std::endl;\n        return false;\n\n    case GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE:\n        std::cout << \"[ERROR] Framebuffer incomplete: Multisample.\" << std::endl;\n        return false;\n\n    case GL_FRAMEBUFFER_UNSUPPORTED:\n        std::cout << \"[ERROR] Framebuffer incomplete: Unsupported by FBO implementation.\" << std::endl;\n        return false;\n\n    default:\n        std::cout << \"[ERROR] Framebuffer incomplete: Unknown error.\" << std::endl;\n        return false;\n    }\n    glBindFramebuffer(GL_FRAMEBUFFER, 0);   // unbind\n}\n```\n\n\n\n\n\n# Reference\n\n1. [OpenGL Vertex Buffer Object (VBO)](http://www.songho.ca/opengl/gl_vbo.html)\n2. [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)\n3. [OpenGL Pixel Buffer Object](http://www.songho.ca/opengl/gl_pbo.html)\n4. [OpenGL Frame Buffer Object](http://www.songho.ca/opengl/gl_fbo.html)"
  },
  {
    "path": "ComputerGraphics(OpenGL)/EXT1_FileFormat.md",
    "content": "# 一、图片存储格式\n\n## 1. BMP 文件\n\n无损的图片格式，全称 Bitmap（无压缩，体积大）\n可以直接存储图片数据，也可以采用索引表的存储方式。但即便采用了索引表的存储方式，也不能使体积缩小太多。图片的内存格式\n\n适合：logos 等有明确边界的图片，程序的缓存文件（无压缩，可直接存储的特点）\n\n格式：ARGB，BMP 文件的第一行数据是显示器的最后一行数据\n\n内存排列：\n\n- 位图文件头(bitmap-file header)\n\n- 位图信息头(bitmap-informationheader)\n\n- 颜色表(color table)\n\n- 颜色点阵数据(bits data)\n\n  \n\n## 2. GIF 文件\n\n无损的图片压缩格式，只能采用索引表的存储方式存储，因此最多能表示 256 种颜色\nGIF 的图片善于做动画，并且支持 alpha 透明通道\n\n适合：logos 等有明确边界的简单图片\n\n\n\n## 3. PNG 文件\n\nPortable Network Graphics\n无损的图片压缩格式，不能做动画，支持 alpha 透明通道（透明效果优于 GIF）\n\n- PNG-8：采用索引表的方式存储图片，最多能表示 256 种颜色（压缩后体积小于 GIF）\n  适合：logos 等有明确边界的简单图片\n- PNG-24：采用直接存储的方式存储图片，能存储上千种颜色（24 位存储一个像素）\n  适合：兼顾好的压缩和效果的照片存储\n\n\n\n## 4. JPEG 文件\n\nJoint Photographic Experts Group\n简称 jpg，有损的图片压缩格式，压缩后体积小（虽然有损，但不易被人眼察觉，24 位存储一个像素 RGB，**不支持透明**）\n适合：只考虑体积照片的压缩\n\nJPEG 格式图片是分为一个一个的段来存储的：\n段的多少和长度并不是一定的。只要包含了足够的信息，该 JPEG 文件就能够被打开，呈现给人们\n\n段的结构：\n\n```c++\n名称  字节数 数据  说明\n------------------------------------------------------\n段标识   1   FF   每个新段的开始标识\n段类型   1        类型编码（称作“标记码”）\n段长度   2        包括段内容和段长度本身,不包括段标识和段类型\n段内容            ≤65533字节\n```\n\n\n\nJPEG 图片存储的段文件按顺序依次如下：\n\n1. **SOI（文件头）**Start Of Image\n   段标识：FF（标志新段的开始）\n   段类型：D8（SOI 的段类型为 D8，表示文件头）\n2. APP0（图像识别信息）Application data marker, type 0\n   段标识：FF（标志新段的开始）\n   段类型：E0（APP0 的段类型为 E0，定义交换格式和图像识别信息）\n3. DQT（定义量化表）\n4. SOF0（图像基本信息）\n5. DHT（定义 Huffman 表）\n6. DRI（定义重新开始间隔）\n7. SOS（扫描行开始）\n8. **EOI（文件尾）**End Of Image\n   段标识：FF（标志新段的开始）\n   段类型：D9（EOI 的段类型为 D9 表示文件尾）\n\n\n\n## 5. SVG 文件\n\n矢量的图片存储方案，内部存储的不是像素，而是曲线和线条\n这使得即便是看起来很大的 SVG 图，存储起来会很小（前提是画面图像足够简单）\nSVG 使用 XML 语法编写，并且可以在文本工具里修改（可以使用 JavaScript 快速修改 SVG 图片的颜色）\n\n适合：logos 或 icons 等简单且需要适配不同尺寸的网站图片\n\n\n\n\n## 6. TGA 文件\n\n游戏中常用的图像格式\n\n### 6.1 非压缩文件格式\n\n![](./images/file_format_tga.jpg)\n\n### 6.2 压缩文件格式\n\n\n\n\n\n\n\n# 二、三维文件格式\n\n三维软件之间互相导入导出一般会涉及到一些格式不兼容的问题，不同的格式有着不同的定位及用处，有开源的也有商业的\n\n| 格式                                     | 功能                                                         | 详情                                                         |\n| ---------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |\n| **.abc**<br />Alembic                    | 动画、粒子、模型烘焙、流体                                   | 通用格式，有效地储存, 共享动画与特效场景<br />[官网](http://www.alembic.io/)<br />[为什么 CG 行业需要 Alembic（.abc） 通用格式](http://www.bgteach.com/article/131) |\n| **.glTF**<br />GL Transmission Format    | 动画、场景、相机、网格、材质、纹理、着色器、着色程序         | json 格式描述<br />较少的冗余信息<br />[官网](https://www.khronos.org/gltf/)<br />[Github](https://github.com/KhronosGroup/glTF/blob/master/README.md) |\n| **.fbx**<br />FilmBoX                    | 骨骼动画、材质、网格                                         | FilmBoX 这套软件所使用的格式，后改称 Motionbuilder<br />Autodesk 家族格式，在 3D Max、Maya、Softimage 等软件间进行**模型**、材质、**动作**和摄影机信息的互导，这样就可以发挥 3D Max 和 Maya 等软件的优势 |\n| **.bvh**<br />BioVision                  | 骨骼动画                                                     | 对人体运动进行捕获后产生文件格式的文件扩展名，捕捉后的文件可以重复利用，应用在不同的角色骨骼驱动上制作动画 |\n| **.obj**                                 | 主要支持多边形(Polygons)模型<br />不包含动画、材质特性、贴图路径、动力学、粒子等信息 | 几乎所有知名的 3D 软件都支持 OBJ 文件的读写                  |\n| **.ply**<br />Polygon File Format        | 静态多边形模型，OBJ 格式的升级版<br />颜色、透明度、表面法向量、材质座标与资料可信度 | 改进了 Obj 格式所缺少的对任意属性及群组的扩充性<br />因此PLY格式发明了 \"property\" 及 \"element\" 这两个关键词，来概括 \"顶点、面、相关资讯、群组\" 的概念 |\n| **.dae**<br />Data Acquisition Equipment | 骨骼动画、材质、网格                                         | xml 格式描述，3D Max、Maya，通过安装插件可导出<br />相比 FBX，对 dae 格式模型的载入有非常高的自由控制，是 FBX 格式代替品 |\n| **.x3d**                                 | 多纹理、多遍绘制、支持 Shader 着色、支持多渲染目标、支持几何实例 | xml 格式描述，专为万维网而设计的三维图像标记语言             |\n| **.stl**                                 | 三角面静态模型<br />只能描述三维物体的几何信息，不支持颜色材质等信息 | 计算机图形学处理 CG、数字几何处理如 CAD、 数字几何工业应用, 如三维打印机支持的最常见文件格式 |\n| **.dxf**<br />Drawing Exchange File      | 三角面静态模型                                               | CAD 通用格式                                                 |\n| **.3ds**                                 | 三角面静态模型                                               | 比较早的一种三维格式，三角面，最早游戏模型应用比较广泛<br />由于后期导入软件的不可编辑性、难以二次编辑现在逐渐的远离了我们的视线 |\n\n\n\n# 三、三维软件\n\n三维软件根据工作的功能分类为：\n\n- 主体三维软件\n  指能独立完成整个三维动画创作的平台性三维软件，具备建模、材质、灯光、渲染、动画、角色等一系列创作的需求，同时允许开发者对软件进行开发第三方插件以扩充软件主体的三维软件\n  \n  | 软件      | 简介                               | 功能                                                         |\n  | --------- | ---------------------------------- | ------------------------------------------------------------ |\n  | Blender   | 免费开源的三维软件                 | 建模、材质、灯光、渲染、雕刻、角色动画、纹理绘制、插件、摄影机跟踪、扣像、合成、游戏引擎 |\n  | Maya      | 售价高昂，易学易用，制作效率高     | 建模、材质、灯光、渲染、角色动画、插件                       |\n  | Cinema 4D | 许多一流艺术家和电影公司的首选     | 建模、材质、灯光、渲染、雕刻、角色动画、纹理绘制、插件       |\n  | Houdini   | 完全是为电影而生                   | 建模、材质、灯光、渲染、特效、角色动画                       |\n  | LightWave | 生物建模和角色动画方面功能异常强大 | 建模、材质、灯光、渲染、特效、角色动画                       |\n  | Softimage | 2014 年三月，发布停产声明          | 建模、材质、灯光、渲染、特效、角色动画                       |\n\n  \n  \n- 协助三维软件\n  指能依赖三维主体软件存在，以强大的辅助完成高质量、高效率的流程，这种软件常常也属于单功能三维软件\n\n  | 软件         | 简介                                                     | 功能                                                      |\n  | ------------ | -------------------------------------------------------- | --------------------------------------------------------- |\n  | Clarisse iFX | 以图像为核心<br />减少机器开始渲染最终图像所需的交互时间 | 导入模型、材质、灯光、即时渲染、特效、合成                |\n  | Twinmotion   | 可以将项目导出为 .exe 可执行文件                         | 导入模型、材质、灯光、即时渲染、动画、展示、交互、VR、360 |\n  | Lumion       | 实时3D可视化工具<br />用来制作电影和静帧作品             | 导入模型、材质、灯光、即时渲染、动画、展示、360           |\n  | ZBrush       | 数字雕刻和绘画软件                                       | 雕刻、纹理                                                |\n\n\n\n\n- 单功能三维软件\n  一般在某个模块异常强大，由于着重解决流程中的一个环节，在效率上有着得天独厚的优势。 缺点也显而易见，需要主体三维软件的导入导出\n\n  | 软件                 | 简介                                 | 功能                                                         |\n  | -------------------- | ------------------------------------ | ------------------------------------------------------------ |\n  | Marmoset Toolbag     | 实时材质编辑，渲染，动画编辑预览软件 | GPU、CPU 即时渲染器                                          |\n  | Silo                 | 视频游戏及电影创建角色或建筑         | 建模、UV Mapping                                             |\n  | Cycles Render Engine | blender 中的一种渲染引擎             | 基于光线追踪的渲染引擎，支持交互式渲染，内置一个新的光影节点系统、新的纹理工作流程和GPU加速，用户通过切换GPU渲染可以使渲染过程变得较为便捷 |\n\n\n\n\n\n# Reference\n\n- [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)\n\n- [libpng](http://www.libpng.org/pub/png/libpng.html)\n\n- [www.w3.org/Graphics](https://www.w3.org/Graphics/JPEG/itu-t81.pdf)\n\n- [JPEG 解码器](https://zhuanlan.zhihu.com/p/27296876)\n\n- [使用 libjpeg 进行图片压缩](https://zhuanlan.zhihu.com/p/126728039)\n\n- [影像算法解析——JPEG 压缩算法](https://zhuanlan.zhihu.com/p/40356456)\n\n- [TGA 文件格式解析](http://www.twinklingstar.cn/2013/471/tga-file-format/)\n\n- [三维文件格式知多少 ](http://www.bgteach.com/article/132)\n\n- [三维软件知多少](http://www.bgteach.com/article/40)\n\n- [图片文件格式知多少 | jpeg、png、pdf、tga、tif、svg、esp、exr、hdr...](https://www.bgteach.com/article/133)\n\n- [游戏制作行业为什么使用TGA格式的贴图而不使用PNG格式？ - 韦易笑的回答 - 知乎 ](https://www.zhihu.com/question/340196227/answer/789538293)\n\n  \n\n"
  },
  {
    "path": "ComputerGraphics(OpenGL)/EXT2_HardwareSupport.md",
    "content": "# 一、 GPU 的硬件架构\n\nGPU 主要由 **显存 Device Memory** 和 **流多处理器 Stream Multiprocessors ** 组成（Stream Processors，SP 是 SM 中的一个 Core）\n\n\n\n## 1. CPU vs GPU\n\n相对于全面功能考虑的 CPU，GPU 有更多的 ALU（Arithmetic Logic Unit，逻辑运算单元），更少的逻辑控制单元和寄存器\nGPU 的并行运算：与 CPU 上十几个线程的并行计算不同，GPU 的线程数可以达到上百万或更多\n\n![](images/GPU_VS_CPU.jpg)\n\n**缓存行 Cache-line**：缓存存储数据的最小单位\n\nCPU 主要采用**三层**缓存（当今主流的 CPU 架构）\n\n1. L1、L2 硬件缓存成为本地核心内缓存，即一个核一个\n   如果是 4 核，那就是有 4 个 L1+4 个 L2\n2. L3 硬件缓存是所有核共享的。即不管你的 CPU 是几核，这个 CPU 中只有一个 L3\n3. L1 硬件缓存的大小是 64K，即 32K 指令缓存 +32K 数据缓存，L2 是 256K，L3 是 2M\n   这不是绝对的，目前 Intel CPU 基本是这样的设计\n\n\n\nGPU 主要采用**二层**缓存（缓存容量小，Cache 命中率低，延时较高）\n\n1. L1 硬件缓存，速度最快，存储共享内存\n   每个 SM 独立包含，一部分在 SM 内共用，一部分在每个 SP 内单独使用（用来交换共享内存）\n2. L2 硬件缓存，速度低于 L1，存储只读常量、纹理\n   多个 SM 共享使用\n3. GPU 的全局内存最大为 12GB\n   GPU 内存**不支持并发读取和并发写入**\n\n![](./images/GPU.jpg)\n\n\n\n## 2. Stream Multiprocessor\n\nGPU 在 shader 中进行的向量运算采用 SIMD 或 MIMD 计算方式\n\n- **SISD**（Single Instruction Single Data Stream，单指令单数据流）：传统顺序执行计算机使用\n- **MIMD**（Multiple Instruction Stream Multiple Data Stream，多指令多数据流）：现代大多数计算机使用，使用多个控制器来异步地控制多个处理器，从而实现空间上的并行性。从硬件角度看，MIMD 需要消耗大量的晶体管数\n- **SIMD**（Single Instruction Stream Multiple Data Stream，单指令多数据流）：GPU 内置计算方式，CPU 里也有相应的实现方法\n- **MISD**（Multiple Instruction Stream Single Data Stream，多指令单数据流）\n\n\n\n早期的 GPU，顶点着色器和像素着色器的硬件结构是独立的（顶点和像素线程资源不能共享）\n使用统一着色器架构 **Unified shader Architecture**，VS、PS、GS、CS 都可以使用相同的硬件资源（线程共享）Stream Multiprocessor\n\n<p>\n    Stream Multiprocessor，SM 为 Shader 执行的地方，如右图一个多流处理器包含\n    <img src='./images/GPU_Stream_processor.png' style='float:right;'/>\n</p>\n\n1. **多边形引擎 PolyMorph Engine**：负责 attribute Setup、VertexFetch、曲面细分、栅格化\n2. **多 ALU 逻辑运算 Core**（每个核代表一个线程，由 Warp 统一管理一组线程）\n   Warp 中的多个线程是**单指令多线程**（相同的逻辑，不同的数据）\n   当执行 if-else 语句、 for 循环次数 n 不是常量，或被 break 提前终止了但其他批次循环的内容还在执行时\n   一个 Warp 线程组只会有部分满足逻辑条件的线程在执行，其他线程当前执行阶段会什么都不执行（被遮掩，但是仍然活跃）\n   这相当于浪费了一部分线程资源，导致像 if-else 这样的语句是 false 的情况也占用了线程资源（占用了，但不使用）\n3. **指令缓存 Instruction Cache**\n4. **LD/ST（load/store）**：加载和存储数据\n5. **SFU（Special function units）**：执行特殊数学运算（sin、cos、log 等）\n6. **寄存器** 128KB\n7. **L1 Cache**\n   配合共享的 L2 Cache 做到 vertex-shader 和 pixel-shader 的数据通信\n8. **Uniform Buffer** 的**部分**缓存\n9. **Texture Cache 和 纹理读取单元**\n\n\n\n## 3. GPU 处理 Shader\n\n**Shader 处理流程**\n\n1. CPU 端编译 Shader 源码为二进制（现代 GPU Shader 为二进制）\n2. CPU 端将 shader 二进制指令经由 PCI-e 推送到 GPU 端\n3. GPU 在执行代码时，会用 Context 将指令分成若干 Channel 推送到各个 SM 的存储空间\n\n\n\n**Shader 的流水线多线程式处理**\n\n多个 SP 里的多个运算单元同时运行同一个 Shader，但每个运算单元支撑的 1 个线程处理的数据又各不相同\n\n![](./images/GPU_Shader_process.png)\n\n\n\n\n\n# 二、CPU、GPU、显示器\n\n## 1. <span id=\"gpu\">CPU-GPU 异构系统</span>\n\n![](./images/GPU_CPU_architecture.png)\n\n**分离式**结构（左图）\n\n- 结构：CPU 和 GPU 拥有各自的存储系统，两者通过 PCI-e 总线进行连接，可以共享一套虚拟地址空间，必要时会进行内存拷贝\n\n- 缺点：PCI-e 相对于两者具有低带宽和高延迟，数据的**传输带宽**成了其中的性能瓶颈\n- 应用的硬件设备：**PC**（CPU、GPU 存储**各自有存储系统**）、**移动端**（CPU、GPU **继承到了一个芯片，且共享物理内存**）\n  很多 SoC （比如：移动端）都是集成了CPU 和 GPU，事实上这仅仅是在物理上进行了集成，并不意味着它们使用的（运行时）就是耦合式结构\n\n\n\n**耦合式**结构（右图）\n\n- 结构：CPU 和 GPU 集成到了一个芯片，GPU 没有独立的内存，与 GPU 共享系统内存，由 MMU 进行存储管理\n- 应用的硬件设备：PS4\n\n\n\n## 2. CPU-GPU Workflow\n\n下图所示为 CPU-GPU 异构系统的工作流，当 CPU 遇到图像处理的需求时，会调用 GPU 进行处理，主要流程可以分为以下四步：\n\n1. 将主存的处理数据 CPU 复制到 显存 GPU 中（可以通过 [DMA](./EXT0_GLBuffers&MultiSample.md) 跳过此步骤）\n2. CPU 指令驱动 GPU\n3. GPU 中的每个运算单元并行处理\n4. GPU 将显存结果传回主存\n\n![](./images/cuda_processing_flow.png)\n\n\n\n## 3. CPU-GPU Data transmission\n\n![](./images/GPU_Management_model.png)\n\n1. **MMIO（Memory-Mapped I/O）**\n   CPU 通过 MMIO 访问 GPU 的寄存器状态\n   通过 MMIO 传送数据块传输命令，支持 DMA 的硬件可以实现块数据传输\n\n2. **GPU Context**\n   上下文表示 GPU 的计算状态，在 GPU 中占据部分虚拟地址空间\n   多个活跃态下的上下文可以在 GPU 中并存（一个 Context 对应一个 SP）\n\n3. **CPU Channel**\n   来自 CPU 操作 GPU 的命令存储在内存中，并提交至 GPU channel 硬件单元\n   每个 GPU 上下文可拥有多个 GPU Channel\n   每个 GPU 上下文都包含 GPU channel 描述符（GPU 内存中的内存对象）\n   每个 GPU Channel 描述符存储了channel 的配置，如：其所在的页表\n   每个 GPU Channel 都有一个专用的命令缓冲区，该缓冲区分配在 GPU 内存中，通过 MMIO 对 CPU 可见\n\n4. **GPU 页表**\n   GPU 上下文使用 GPU 页表进行分配，该表将**虚拟地址**空间与**其他地址空间**隔离开来\n   GPU 页表与 CPU 页表分离，其驻留在 GPU 内存中，物理地址位于 GPU 通道描述符中\n   通过 GPU channel 提交的所有命令和程序都在对应的 GPU 虚拟地址空间中执行\n   GPU 页表将 GPU 虚拟地址不仅转换为 GPU 设备物理地址，还转换为主机物理地址\n   这使得 GPU 页面表能够将 GPU 存储器和主存储器统一到统一的 GPU 虚拟地址空间中，从而构成一个完成的虚拟地址空间\n\n5. **PFIFO Engine**\n   一个提交 GPU 命令的特殊引擎\n   维护多个独立的命令队列，即 channel（带有 put 和 get 指针的环形缓冲器）\n   会拦截多有对通道控制区域的访问以供执行\n   GPU 驱动使用一个通道描述符来存储关联通道的设置\n\n6. **Buffer Object** 缓冲对象\n   一块内存，可以用来存储纹理，渲染对象，着色器代码等等\n\n\n\n## 4. 显示器的显示\n\n从 CPU 通过 GPU 到显示的流程如下：\n\n1. CPU 计算好显示内容提交至 GPU\n2. GPU 渲染完成后将渲染结果存入帧缓冲区\n3. 视频控制器会按照 `VSync` 信号逐帧读取帧缓冲区的数据\n4. 帧缓冲区数据 经过转换后 由显示器进行显示\n\n![](./images/ios-renderIng-gpu-internal-structure.png)\n\n**显示器显示工作流程**\n\n1. 显示器逐行刷新，每一行刷新完后会发出一个水平同步信号（horizonal synchronization），简称 **HSync**\n2. 绘制下一行\n3. 显示器一帧画面绘制完成后会发出一个垂直同步信号（vertical synchronization），简称 **VSync**\n\n\n\n**双缓冲机制**\n\n- 使用场景：防止在快速运动场景下，由于**显卡运算速率 > 显示器运算速率**导致快速运动的动作割裂情况（画面撕裂）\n- 方法：使用两个帧缓冲，一个负责 GPU 写入，一个负责 显示器 读取，用垂直同步确保写入完成后将写入和读取帧缓冲互换角色\n- 缺点：开启垂直同步，画面会有延迟（无法达到显卡的最大运算速率），但并没有卡顿\n  `显卡绘制一帧时间 > 显示器刷新一帧时间 ? 显示器刷新(显卡等待) : 显示器显示上一帧，等待显卡绘制完成(屏幕卡顿);`\n- 解决方法：用**三重缓冲**代替垂直同步\n  三重缓冲：在双缓冲的基础上加了一个缓冲\n  在等待垂直同步时，来回交替渲染两个离屏的缓冲区\n  垂直同步发生时，屏幕缓冲区和最近渲染完成的离屏缓冲区交换，实现充分利用硬件性能的目的\n\n![](./images/ios-vsync-off.jpg)\n\n\n\n\n\n# 三、测试 GPU 硬件信息\n\n## 1. NV Shader 扩展功能\n\n[NV shader thread group](https://www.opengl.org/registry/specs/NV/shader_thread_group.txt) 提供了 OpenGL 的扩展，可以查询 GPU 线程、Core、SM、Warp 等硬件相关的属性（需要支持 GLVersion 4.3+ 的硬件）\n\n```c\n// 开启扩展\n#extension GL_NV_shader_thread_group : require     (or enable)\n\nWARP_SIZE_NV\t// 单个线程束的线程数量\nWARPS_PER_SM_NV\t// 单个SM的线程束数量\nSM_COUNT_NV\t\t// SM数量\n\nuniform uint  gl_WarpSizeNV;\t// 单个线程束的线程数量\nuniform uint  gl_WarpsPerSMNV;\t// 单个SM的线程束数量\nuniform uint  gl_SMCountNV;\t\t// SM数量\n\nin uint  gl_WarpIDNV;\t\t// 当前线程束id\nin uint  gl_SMIDNV;\t\t\t// 当前线程束所在的SM id，取值[0, gl_SMCountNV-1]\nin uint  gl_ThreadInWarpNV;\t// 当前线程id，取值[0, gl_WarpSizeNV-1]\n\nin uint  gl_ThreadEqMaskNV;\t// 是否等于当前线程id的位域掩码。\nin uint  gl_ThreadGeMaskNV;\t// 是否大于等于当前线程id的位域掩码。\nin uint  gl_ThreadGtMaskNV;\t// 是否大于当前线程id的位域掩码。\nin uint  gl_ThreadLeMaskNV;\t// 是否小于等于当前线程id的位域掩码。\nin uint  gl_ThreadLtMaskNV;\t// 是否小于当前线程id的位域掩码。\n\nin bool  gl_HelperThreadNV;\t// 当前线程是否协助型线程(Draw quad 时，不在当前图元上的像素所在的线程)\n/**\n   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.\n*/\n```\n\n\n\n## 2. Sample Code\n\n```C\n// VS\n#version 430 core\nlayout (location = 0) in vec3 aPos;\n\nvoid main() {\n\tgl_Position = vec4(aPos, 1.0f);\n}\n\n// FS\n#version 430 core\n#extension GL_NV_shader_thread_group : require\n\nuniform uint  gl_WarpSizeNV;\t// 单个线程束的线程数量\nuniform uint  gl_WarpsPerSMNV;\t// 单个SM的线程束数量\nuniform uint  gl_SMCountNV;\t\t// SM数量\n\nin uint  gl_WarpIDNV;\t\t// 当前线程束id\nin uint  gl_SMIDNV;\t\t\t// 当前线程所在的SM id，取值[0, gl_SMCountNV-1]\nin uint  gl_ThreadInWarpNV;\t// 当前线程id，取值[0, gl_WarpSizeNV-1]\n\nout vec4 FragColor;\n\nvoid main() {\n\t// SM id\n\tfloat lightness0 = gl_SMIDNV / gl_SMCountNV;\n    // warp id\n    float lightness1 = gl_WarpIDNV / gl_WarpsPerSMNV;\n    // thread id\n\tfloat lightness2 = gl_ThreadInWarpNV / gl_WarpSizeNV;\n    \n\tFragColor = vec4(lightness0);\n}\n```\n\n\n\n**SM id 的结果 lightness0 分析：**\n\n1. 通过计算画面色阶总数得出 SM 的总数\n2. 通过单个三角面内重复的像素块个数推算每个 SM 内的核心数\n3. 不同三角形的接缝处出现断层，说明同一个像素块如果分属不同的三角形，就会分配到不同的 SM 进行处理\n   由此推断，**相同面积的区域，如果所属的三角形越多，就会导致分配给 SM 的次数越多，消耗的渲染性能也越多**\n\n![](./images/GPU_Test_SM.png)\n\n\n\n\n\n# 四、GPU 硬件渲染模式\n\n## 1. IMR\n\n **Immediate Mode Rendering 立即渲染模式**：PC 端\n\n![](./images/immediate-demo.gif)\n\n- 优点：顶点着色器和其它几何体相关着色器的输出数据能存储在 GPU 上（FIFO 缓冲区），直到管道中的下一阶段准备使用数据\n\n- 缺点：由于根据图元划分绘制批次，GPU 对整个帧缓冲进行随机访问，帧缓冲只能存储在外部 DRAM 上，导致高分辨率时内存**带宽负载高**\n\n- 优化：降带宽，将最近访问的帧缓冲存储排布在靠近 GPU 的位置来提高内存命中率\n  \n- 方法：**优先根据图元来划分绘制批次**\n  \n  ![](images/immediate_mode.svg)\n  \n  ```python\n  # 每个顶点几何图形画完后直接做像素颜色，此时像素颜色不确定，需要多次读写 framebuffer（深度值的不同）\n  for draw in renderPass:\n      for primitive in draw:\n          for vertex in primitive:\n              execute_vertex_shader(vertex)\n              \n          if primitive not culled:\n              for fragment in primitive:\n                  execute_fragment_shader(fragment)\n  ```\n  \n\n\n\n## 2. TB[D]R\n\n**Tile Based [Deferred] Rendering 基于切片的[延迟]渲染**：移动端\n\n![](./images/tile-based-demo.gif)\n\n- 优点：\n  可以将整个颜色、深度和模板的工作集存储在快速的 On-chip RAM 上（GPU 直连，不用带宽） \n  深度测试和混合透明像素所需帧缓冲数据也存储在 GPU 内部，通过提高缓存命中来降低了带宽消耗\n\n- 缺点：\n  GPU 必须将每个顶点的变化数据和 Tile 的中间状态存储到主内存中，着色阶段随后读取这些数据\n  通过**延迟一帧**的方式降低了带宽\n\n- 方法：**优先根据帧缓冲来划分绘制批次**，将帧缓冲切分为几个固定大小的 Tile，分别渲染每个 Tile 上的像素\n  \n  ![](images/tiled_mode.svg)\n  \n  ```python\n  # Pass 1. 将所有几何图元属性处理后，逐个划分到对应的 Tile 中\n  for draw in renderPass:\n      for primitive in draw:\n          for vertex in primitive:\n              execute_vertex_shader(vertex)\n          if primitive not culled:\n              append_tile_list(primitive)\n  \n  # Pass 2. 根据当前 Tile 的图元属性绘制当前 Tile 所包含的所有像素\n  for tile in renderPass:\n      for primitive in tile:\n          for fragment in primitive:\n              execute_fragment_shader(fragment)\n  ```\n  \n\n\n\n\n\n# 五、GPU 硬件新特性\n\n## 1. Pixel Local Storage\n\n支持平台：OpenGL ES、Metal、Vulkan、D3D\n\n**Pixel Local Storage（PLS）**是一种数据存取方式，用 PLS 声明的数据将保存在 GPU 的 Tile buffer 上\n应用：**延迟着色**所需的 **GBuffer** 数据一直处于 PLS 之中，最好解析后返回最终颜色，而**不需要将 GBuffer 写回系统显存**\n\n```glsl\n// 1. 光照累积\n__pixel_localEXT FragData // 可读写数据\n{\n    layout(rgba8) highp vec4 Color;\n    layout(rg16f) highp vec2 NormalXY;\n    layout(rg16f) highp vec2 NormalZ_LightingB;\n    layout(rg16f) highp vec2 LightingRG;\n} gbuf;\n\nvoid main() {\n    vec3 Lighting = CalcLighting(gbuf.NormalXY, gbuf.NormalZ_LightingB.x);\n    gbuf.LightingRG += Lighting.xy;\n    gbuf.NormalZ_LightingB.y += Lighting.z;\n}\n\n// 2. 最终着色\n// __pixel_local_outEXT 只读数据\n__pixel_local_inEXT FragData // 只读数据\n{\n    layout(rgba8) highp vec4 Color;\n    layout(rg16f) highp vec2 NormalXY;\n    layout(rg16f) highp vec2 NormalZ_LightingB;\n    layout(rg16f) highp vec2 LightingRG;\n} gbuf;\n\nout highp vec4 FragColor;\n\nvoid main() {\n    FragColor = resolve(gbuf.Color, gbuf.LightingRG, gbuf.NormalZ_LightingB.y);\n}\n```\n\n\n\n## 2. Subpass\n\n支持平台：Metal、Vulkan、D3D\n\nSubPass 与 Pixel Local Storage 类似也是存储数据到 GPU 的 Tile buffer 上，借鉴了 TBDR 的延迟一帧的 1 Pass 拆成 图元处理 Pass 和 着色 Pass 这样两个 Subpass 的想法\n\n![](./images/subpass.png)\n\n限制：\n\n- 所有 Subpass 必须在同一个 Render Pass 中（不能是上一个）\n- 无法在超过 GPU Tile buffer 范围外进行采样\n\n```c\n// 读\n// LOAD_OP_LOAD：从全局内存加载 Attachment 到 Tile\n// LOAD_OP_CLEAR：清理Tile缓冲区的数据。\n// LOAD_OP_DONT_CARE：不对 Tile 缓冲区的数据做任何操作，通常用于 Tile 内的数据会被全部重新，效率高于 LOAD_OP_CLEAR\n\n// 写\n// STORE_OP_STORE：将 Tile 内的数据存储到全局内存\n// STORE_OP_DONT_CARE：不对 Tile 缓冲区的数据做任何存储操作\n\n// 1. Attachment\nVkAttachmentDescription colorAttachment = {};\ncolorAttachment.format = VK_FORMAT_B8G8R8A8_SRGB;\ncolorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;\n// 标明 loadOp 为 DONT_CARE\ncolorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;\n// 标明 storeOp 为 DONT_CARE\ncolorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;\n\n// 2. 为了让 Attachment 存储到 Tile 内，必须使用标记 TRANSIENT_ATTACHMENT 和 LAZILY_ALLOCATED\nVkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};\nimageInfo.flags\t\t= flags;\nimageInfo.imageType\t= type;\nimageInfo.format\t= format;\nimageInfo.extent\t= extent;\nimageInfo.samples\t= sampleCount;\n// Image 使用 TRANSIENT_ATTACHMENT 的标记\nimageInfo.usage\t\t= VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT;\n\nVmaAllocation memory;\nVmaAllocationCreateInfo memoryInfo{};\nmemoryInfo.usage\t\t  = memoryUsage;\n// Image 所在的内存使用 LAZILY_ALLOCATED 的标记\nmemoryInfo.preferredFlags = VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT;\n\n// 创建 Image\nauto result = vmaCreateImage(device.get_memory_allocator(), &imageInfo, memoryInfo, &handle, &memory, nullptr);\n```\n\n\n\n\n\n# 六、常见问题\n\n## 1. PCIe BindWidth\n\n显存的带宽比内存的大很多（显存的位宽大）\n\n- **内存** 和 **显存** 之间的 PCIe 总线带宽过小是 CPU 和 GPU 交互的瓶颈\n- 在移动端，由于其[耦合式的物理架构](#gpu)，带宽是一种多设备（CPU、GPU、AUDIO 等）共享的资源，而且**处理器通过带宽对存储的访问很耗电**\n- OpenGL 的**显示列表**，将一组绘制指令放到 GPU 上，CPU 只要发一条 \"执行这个显示列表\" 这些指令就执行，而不必每次渲染都发送大量指令到 GPU，从而节约 PCIe 带宽\n\n\n\n移动设备的特点：不同于 PC 端的 CPU 和 GPU 纯粹地追求计算性能，移动端在尺寸、能耗、硬件性能等诸多方面都存在显著的差异\n\n1. 性能（**P**erformance）：移动设备的各类元件（CPU、带宽、内存、GPU等）的性能都只是PC设备的数十分之一\n2. 能耗（**P**ower）：为了满足足够长的续航和散热限制，必须严格控制移动设备的整机功率\n   PC 设备通常可以安装散热风扇、甚至水冷系统，而移动设备不具备这些主动散热方式，只能靠热传导散热\n   如果散热不当，CPU 和 GPU 都会<u>主动降频</u>，以非常有限的性能运行，以免设备元器件因过热而损毁\n3. 面积（**A**rea）：移动端的便携性就要求整机只能限制在非常小的体积之内\n\n\n\n## 2. Pipeline Barrier\n\nVulkan、Metal、DX12 等现代图形 API 可以精确指定渲染管线屏障 Barrier 的等待阶段\n避免 TBDR 这种 VS 和 FS 分两个 Pass 等待期间造成的 GPU 流水线并发率低，提高 Shader 在 GPU 运行的并发效果\n\n![](./images/pipeline-barrier.png)\n\n\n\n## 3. GPU 访问内存\n\n由于 TBDR 的 Tile GPU 缓存拆分，GPU 在访问内存时，多组 ALU 计算单元核需要串行访问\n\n![](./images/tile-based-memory.png)\n\n\n\n\n\n# Reference\n\n1. [NV extensions](https://www.khronos.org/registry/OpenGL/extensions/NV/)\n2. [GDC Vault](https://gdcvault.com/browse/?categories=PgTaVr)\n3. [Siggraph Conference Content](https://www.siggraph.org/learn/conference-content/)\n4. [Rendering pipeline: The hardware side](https://slideplayer.com/slide/11059244/)\n4. [Introduction to GPU Architecture](http://haifux.org/lectures/267/Introduction-to-GPUs.pdf)\n4. [An Introduction to Modern GPU Architecture](http://download.nvidia.com/developer/cuda/seminar/TDCI_Arch.pdf)\n4. [Revisting Co-Processing for Hash Joins on the Coupled CPU-GPU Architecture](https://www.slideshare.net/mohamedragabslideshare/p12-29046493)\n4. [Understanding GPU caches](https://www.rastergrid.com/blog/gpu-tech/2021/01/understanding-gpu-caches/)\n8. [Transitioning from OpenGL to Vulkan](https://developer.nvidia.com/transitioning-opengl-vulkan)\n9. [Next Generation OpenGL Becomes Vulkan: Additional Details Released](https://www.anandtech.com/show/9038/next-generation-opengl-becomes-vulkan-additional-details-released)\n10. [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)\n5. [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)\n6. [Understanding Render Passes](https://developer.arm.com/solutions/graphics-and-gaming/developer-guides/learn-the-basics/understanding-render-passes)\n6. [Asynchronous Shaders](http://developer.amd.com/wordpress/media/2012/10/Asynchronous-Shaders-White-Paper-FINAL.pdf)\n7. [GameDev Best Practices](https://developer.samsung.com/galaxy-gamedev/best-practice.html)\n8. [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)\n9. [Google Developer Contributes Universal Bandwidth Compression To Freedreno Driver](https://www.phoronix.com/scan.php?page=news_item&px=Freedreno-UBWC-A6XX)\n10. [Using pipeline barriers efficiently](https://github.com/KhronosGroup/Vulkan-Samples/blob/master/samples/performance/pipeline_barriers/pipeline_barriers_tutorial.md#the-sample)\n11. [Graphics Shaders - Theory and Practice 2nd Edition](http://cs.uns.edu.ar/cg/clasespdf/GraphicShaders.pdf)\n11. [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)\n12. [渲染优化-从GPU的结构谈起](https://zhuanlan.zhihu.com/p/58694744)\n12. [Render Graph 与现代图形 API](https://zhuanlan.zhihu.com/p/425830762)\n7. [计算机那些事(8)——图形图像渲染原理](http://chuquan.me/2018/08/26/graphics-rending-principle-gpu/)\n7. [移动游戏性能优化通用技法](https://www.cnblogs.com/timlly/p/10463467.html)\n7. [写实大世界游戏渲染技术详解 GPU 优化](https://mp.weixin.qq.com/s/T1t7dQwmxoUfuz1ik2USVg)\n7. [剖析虚幻渲染体系（12）- 移动端专题Part 2（GPU架构和机制）](https://www.cnblogs.com/timlly/p/15546797.html)\n\n"
  },
  {
    "path": "ComputerGraphics(OpenGL)/EXT3_Platform.md",
    "content": "# 零、代码 Debug\n\n> 关于性能的衡量 帧率/单帧时间，**建议采用单帧时间**\n>\n> 由于 帧率 = 60 / 单帧时间，他们之间的关系不是线性的，例如在减少同样时间消耗的情况下：\n> 低帧率区间进步缓慢，每秒 10fps 下，帧时间减少 2ms，帧率提高到 10.2fps\n> 高帧率区间进步明显，每秒 25fps 下，帧时间减少 2ms，帧率提高到 26.3fps\n\n\n\n第三方跨平台框架\n\n- [Flutter外接纹理](https://zhuanlan.zhihu.com/p/42566807)\n\n\n\n# 一、Android 平台\n\n>图片有不清晰的地方，可以点开连接看大图\n\n## 1. 数据的封装\n\n### 1.1 Surface\n内存中的一段绘图缓冲区，是对 framebuffer 的 Java 封装对象\n\n\n\n### 1.2 SurfaceTexture\n内存中的一段绘图缓冲区，对 EGL texture 的 Java 封装对象\n\n数据源：android.hardware.camera2, MediaCodec, MediaPlayer 和 Allocation 这些类的目标视频数据输出对象\n\n限制：API 11 存在，Android 3.0 及其后才能使用\n\n特点：可以接收一个 EGL texture 的纹理 ID 来产生，可以做到**离线渲染**\n\n关键方法：\n\n- updateTexImage（从内容流中获取当前帧，使得内容流中的一些帧可以跳过）\n\n- 通过 调用 getTransformMatrix 获取纹理的旋转情况\n\n![](./images/surfaceTexture.png)\n\n\n\nSurfaceTexture 使用流程\n\n![](./images/processSurfaceTexture.png)\n\n\n\n## 2. 数据的展示和刷新\n\n### 2.1 SurfaceView\n\n父类：View\n\n限制：API 1 存在\n\n回调方法运行线程：主线程\n\n优点：有自己的 Surface 来刷新，可以做到**局部刷新，独立线程刷新数据**\n\n缺点：不能进行 Transition，Rotation，Scale 等变换，这导致 SurfaceView 在由于刷新不受主线程控制，**滑动时可能有黑边**\n\n关键方法：getHolder（获取 SurfaceHolder ，SurfaceHolder 持有 Surface 数据对象）\n\n\n\n### 2.2 GLSurfaceView\n\n父类：SurfaceView\n\n限制：API 3 存在，Android 1.5 及其后才能使用\n\n关键方法：\n\n![](./images/glsurfaceview.png)\n\n\n\n### 2.3 TextureView\n\n父类：View\n\n限制：API 14 存在，Android 4.0 及其后才能使用，只能针对用于硬件加速（没有 GPU 就无法使用）\n\n回调方法运行线程：主线程（在Android 5.0 引入渲染线程后，它是在渲染线程中做的）\n\n特点：没有自己的 Surface 来刷新，使用所在的 window 来**全局刷新**，可以进行 Transition，Rotation，Scale 等变换，**滑动时没有黑边**\n\n关键方法：\n\n- getSurfaceTexture（可能返回 null）\n\n- setSurfaceTextureListener\n\n  ```java\n  public DrawTextureView(Context context, AttributeSet attrs, int defStyleAttr) {\n    super(context, attrs, defStyleAttr);\n    this.setSurfaceTextureListener(this);\n  }\n  \n  @Override\n  public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {\n    mSurface = new Surface(surface);\n  }\n  \n  @Override\n  public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {}\n  \n  @Override\n  public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {  \n    return true;\n  }\n  \n  @Override\n  public void onSurfaceTextureUpdated(SurfaceTexture surface) {}\n  ```\n  <img src=\"./images/textureView.jpeg\"  />\n  \n  \n\n### 2.4 Android 5.0 引入渲染线程\n\n![](./images/renderThread.jpeg)\n\n\n\n## 3. EGL 环境配置\n\n> 如果 OpenGL 是打印机，EGL 就是纸。EGL：作为 OpenGL ES 和本地窗口的桥梁，不同平台的 EGL 实现方式不一样\n\n\n### 3.1 渲染同步问题\n\n\n本地环境和客户端环境\n\n- 本地窗口环境：Windows、X\n- 客户端环境：3D 渲染器 OpenGL、2D 矢量图形渲染器 [OpenVG](https://baike.baidu.com/item/OpenVG/7922699?fr=aladdin)\n\n\n\n同一个 surface 上可能 **同时异步** 执行了 <u>本地窗口环境</u> 和 <u>客户端环境</u> 的命令\n\n- 本地环境等待客户端环境渲染完成，效果同 glFinish 或 vgFinish 一致\n  [EGLBoolean eglWaitClient(void);](https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglWaitClient.xhtml)\n- 客户端环境等待本地环境渲染完成\n  [EGLBoolean eglWaitNative(EGLint engine);](https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglWaitNative.xhtml)\n\n\n\n### 3.2 获取 EGL 版本信息\n\n1. [EGLBoolean eglInitialize(EGLDisplay dpy, EGLint *major, EGLint *minor);](https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglInitialize.xhtml)\n   本质是初始化函数，但也能通过返回 major，minor 来获取当前 display 硬件设备支持的 OpenGL ES 版本号\n2. [const char* eglQueryString(EGLDisplay dpy, EGLint name);](https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglQueryString.xhtml)\n   查询当前 display 硬件设备有哪些  EGL 的扩展支持，**调用前必须先通过 eglInitialize 初始化 EGL**\n\n\n\n### 3.3 EGL 的 Context 和 Surface\n\nEGL 主要作用是将渲染绘制到本地窗口上\n\nEGL 可以销毁本地资源（各种 surface 类型）\n只有一个方法，[EGLBoolean eglDestroySurface(EGLDisplay display, EGLSurface surface);](https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglDestroySurface.xhtml)\n\nEGL 可以创建本地环境的资源（各种 surface 类型）格式有\n\n1. pixel buffer（存储在显存上）\n   OpenGL API 方面，[EGLSurface eglCreatePbufferSurface(...)](https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglCreatePbufferSurface.xhtml)\n   OpenVG API 方面，[EGLSurface eglCreatePbufferFromClientBuffer(...)](https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglCreatePbufferFromClientBuffer.xhtml)\n2. frame buffer（存储在显存上）\n   本地环境 API 创建的 window 内的 surface，[EGLSurface eglCreateWindowSurface(...)](https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglCreateWindowSurface.xhtml)\n   \n3. 本地环境 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)\n\tPixmap：将图像以像素颜色数组的结构来存储的对象\n\tBitmap：用 1 bit 来存储 1 个像素颜色的 Pixmap\n\n\n\n**EGLContext 上下文创建步骤**\n\n```c\n#include <stdlib.h>\n#include <unistd.h>\n#include <EGL/egl.h>\n#include <GLES/gl.h>\n\ntypedef ... NativeWindowType;\nextern NativeWindowType createNativeWindow(void);\n\n// 虽然是一维数组，但还是要采用 id, value, id, value ... 的存储方式\nconst EGLint attribute_list[] =\n{\n    EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT_KHR,\n  \t//EGL_WINDOW_BIT EGL_PBUFFER_BIT we will create a pixelbuffer surface\n    EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,\n    EGL_RED_SIZE,   \t8,\n    EGL_GREEN_SIZE, \t8,\n    EGL_BLUE_SIZE,    8,\n    EGL_ALPHA_SIZE,   8, // if you need the alpha channel\n    EGL_DEPTH_SIZE,   8, // if you need the depth buffer\n    EGL_STENCIL_SIZE, 8,\n    EGL_NONE\n};\n\n// EGL context attributes\nconst EGLint ctxAttr[] = {\n    EGL_CONTEXT_CLIENT_VERSION, 2,\n    EGL_NONE\n};\n\nvoid createGLESEnv()\n{\n    EGLint num_config;\n    EGLint numConfigs;\n    EGLint eglMajVers;\n    EGLint eglMinVers;\n  \n    EGLConfig config;\n    EGLDisplay m_eglDisplay;\t// 关联系统物理屏幕，表示显示设备句柄\n    EGLContext m_eglContext;\n    EGLSurface m_eglSurface;  // EGLSurface 和 Java 的 Surface 没有关系，是两个独立的对象\n    NativeWindowType native_window;\n\n    // 1. get an EGL display connection\n    m_eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);\n    if(EGL_NO_DISPLAY == m_eglDisplay) {\n      Log.e(\"ERROR: get an EGL display connection\");\n    }\n\n    // 2. initialize the EGL display connection\n    if (!eglInitialize(m_eglDisplay, &eglMajVers, &eglMinVers)) {\n\t\t\tLog.e(\"ERROR: initialize the EGL display connection\");\n    }\n\n    // 3. get an appropriate EGL frame buffer configuration\n    if(!eglChooseConfig(m_eglDisplay, attribute_list, &config, 1, &num_config)) {\n      Log.e(\"ERROR: get an appropriate EGL frame buffer configuration\");\n    }\n\n    // 4. create an EGL rendering context\n    m_eglContext = eglCreateContext(m_eglDisplay, config, EGL_NO_CONTEXT, ctxAttr);\t\n  \tif (EGL_NO_CONTEXT == m_eglContext) {\n      EGLint error = eglGetError();\n      if(error == EGL_BAD_CONFIG) {\n        Log.e(\"ERROR: create an EGL rendering context\");\n      }\n\t\t}\n\n    // 5. create a native window\n    native_window = createNativeWindow();\n\n    // 6. create an EGL window surface\n    m_eglSurface = eglCreateWindowSurface(m_eglDisplay, config, native_window, NULL);\n\n    // 7. connect the context to the surface\n    if (!eglMakeCurrent(m_eglDisplay, m_eglSurface, m_eglSurface, m_eglContext)) {\n\t\t\tLog.e(\"ERROR: connect the context to the surface\");\n    }\n}\n\nvoid destroyGlESEnv()\n{\n    if (m_eglDisplay != EGL_NO_DISPLAY) {\n        eglMakeCurrent(m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);\n        eglDestroyContext(m_eglDisplay, m_eglContext);\n        eglDestroySurface(m_eglDisplay, m_eglSurface);\n        eglReleaseThread();\n        eglTerminate(m_eglDisplay);\n    }\n\n    m_eglDisplay = EGL_NO_DISPLAY;\n    m_eglSurface = EGL_NO_SURFACE;\n    m_eglContext = EGL_NO_CONTEXT;\n}\n\nint main(int argc, char ** argv)\n{\n    createGLESEnv();\n\n    glClearColor(1.0, 1.0, 0.0, 1.0);\n    glClear(GL_COLOR_BUFFER_BIT);\n    glFlush();\n\n    // 所有的绘制步骤在后台绘制，当绘制完成时切换前后台缓冲，确保显示的一直是绘制完成的画面\n    eglSwapBuffers(m_eglDisplay, m_eglSurface);\n  \n  \tdestroyGlESEnv();\n\n    return EXIT_SUCCESS;\n}\n```\n\n\n\n## 4. 平台问题\n\n### 4.1 TEXTURE_EXTERNAL_OES\n\n[TEXTURE_EXTERNAL_OES](https://www.khronos.org/registry/OpenGL/extensions/OES/OES_EGL_image_external.txt) 是 OpenGL ES 在 Android 上的扩展，在获取相机纹理时只有 TEXTURE_EXTERNAL_OES 类型的纹理\n\n使用扩展纹理 TEXTURE_EXTERNAL_OES 步骤\n\n1. 创建纹理时\n\n   ```java\n   // 注意 GLES11Ext\n   GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId);\n   \n   GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);\n   GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);\n   GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP_TO_EDGE);\n   GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP_TO_EDGE);\n   ```\n\n2. 在 fragment shader 里要提前声明使用的扩展\n\n   ```c\n   #extension GL_OES_EGL_image_external : require\n   uniform samplerExternalOES u_texture; // 代替 sampler2D\n   void main() {}\n   ```\n\n   \n\n### 4.2 Java 成员变量和 C++ 指针的 JNI 层绑定\n\n绑定指针\n\n- Java 类中，声明一个为 long 的长整型类型的成员变量代表 C++ 指针的句柄\n\n- C / C++ 文件中，每次获取 jlong 的成员变量时**强制转换为指针来使用**\n\n  ```c\n  #include <jni.h>\n  \n  jfieldID inline getHandleField(JNIEnv *env, jobject obj)\n  {\n      jclass c = env->GetObjectClass(obj);\n      // J is the type signature for long:\n      return env->GetFieldID(c, \"nativeHandle\", \"J\");\n  }\n  \n  template <typename T>\n  T *getHandle(JNIEnv *env, jobject obj)\n  {\n      jlong handle = env->GetLongField(obj, getHandleField(env, obj));\n      return reinterpret_cast<T *>(handle);\n  }\n  \n  template <typename T>\n  void setHandle(JNIEnv *env, jobject obj, T *t)\n  {\n      jlong handle = reinterpret_cast<jlong>(t);\n      env->SetLongField(obj, getHandleField(env, obj), handle);\n  }\n  \n  // Use Example\n  MyObject* ptr = new MyObject();\n  setHandle<MyObject>(env, object， ptr)；\n  ptr = getHandle<MyObject>(env, object);\n  ```\n\n  \n\n绑定智能指针\n\n- Java 类中，声明一个为 long 的长整型类型的成员变量代表 C++ 指针的句柄\n\n- C / C++ 文件中，每次获取 jlong 的成员变量时**强制转换为包含智能指针的对象的指针来使用**\n\n  ```c\n  template <typename T>\n  class SmartPointerWrapper {\n      std::shared_ptr<T> mObject;\n  public:\n      template <typename ...ARGS>\n      explicit SmartPointerWrapper(ARGS... a) {\n          mObject = std::make_shared<T>(a...);\n      }\n  \n      explicit SmartPointerWrapper (std::shared_ptr<T> obj) {\n          mObject = obj;\n      }\n  \n      virtual ~SmartPointerWrapper() noexcept = default;\n  \n      void instantiate (JNIEnv *env, jobject instance) {\n          setHandle<SmartPointerWrapper>(env, instance, this);\n      }\n  \n      jlong instance() const {\n          return reinterpret_cast<jlong>(this);\n      }\n  \n      std::shared_ptr<T> get() const {\n          return mObject;\n      }\n  \n      static std::shared_ptr<T> object(JNIEnv *env, jobject instance) {\n          return get(env, instance)->get();\n      }\n  \n      static SmartPointerWrapper<T> *get(JNIEnv *env, jobject instance) {\n          return getHandle<SmartPointerWrapper<T>>(env, instance);\n      }\n  \n      static void dispose(JNIEnv *env, jobject instance) {\n          auto obj = get(env,instance);\n          delete obj;\n          setHandle<SmartPointerWrapper>(env, instance, nullptr);\n      }\n  };\n  \n  // Use Example\n  SmartPointerWrapper<Object> obj = new SmartPointerWrapper<Object>(arguments);\n  obj->instantiate(env,instance);\n  ```\n\n\n\n\n## 5. Debug\n\n1. [系统自带的 GPU 呈现分析](https://zhuanlan.zhihu.com/p/22334175)\n2. [高通骁龙 Adreno GPU Profiler 调试工具（建议在 windows 下使用，mac 下测试无用）](https://gameinstitute.qq.com/community/detail/123051)\n3. [GAPID 调试 Android 应用，需要 Android stuido 停用 adb 的使用](http://www.geeks3d.com/20171214/google-gapid-capture-vulkan-and-opengl-es-calls-on-android-windows-macos-and-linux/)\n4. 部分 vivo 手机会出现安装 app 失败的问题，需要在 Android Studio 设置里的 Build > Instant Run > disable Instant Run\n\n\n\n\n\n# 二、iOS 平台\n\n## 1. 数据封装\n\n### 1.1 内存与纹理建立映射关系\n\n**CVOpenGLESTextureCacheCreateTextureFromImage**\n\n- 从 **CVOpenGLESTextureCacheRef** 纹理缓存中获取一个新的纹理，或者符合传入参数的已创建的纹理\n- 将一段内存与 **CVOpenGLESTextureCacheRef** 中的一个纹理建立映射关系\n\n```objc\n+ (CVReturn)createTextureFromPixelBuffer:(CVImageBufferRef __nonnull)pixelBuffer\n                                   width:(int)width\n                                  height:(int)height\n                                   cache:(CVOpenGLESTextureCacheRef __nonnull)textureCache\n                               cvTexture:(CVOpenGLESTextureRef* __nonnull)cvTexture\n                            textureOuput:(unsigned int* __nullable)textureOuput\n{\n  CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);\n  CVReturn result = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,\n                                                                 textureCache,\n                                                                 pixelBuffer,\n                                                                 NULL,\n                                                                 GL_TEXTURE_2D,\n                                                                 GL_RGBA,\n                                                                 width,\n                                                                 height,\n                                                                 GL_BGRA,\n                                                                 GL_UNSIGNED_BYTE,\n                                                                 0,\n                                                                 cvTexture);\n  CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);\n  \n  if (NULL == textureOuput) {\n    glBindTexture(CVOpenGLESTextureGetTarget(*cvTexture), CVOpenGLESTextureGetName(*cvTexture));\n  } else  {\n    *textureOuput = CVOpenGLESTextureGetName(*cvTexture);\n    glBindTexture(CVOpenGLESTextureGetTarget(*cvTexture), *textureOuput);\n  }\n  \n  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);\n  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);\n  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);\n  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);\n  glBindTexture(GL_TEXTURE_2D, 0);\n  \n  return result;\n}\n```\n\n\n\n## 2. 数据的展示和刷新\n\n### 2.1 CAEAGLLayer\n\nCAEAGLLayer  是 UIView 的 OpenGL 展示层，可以通过重写 UIView 的以下方法来创建 CAEAGLLayer 对象\n\n```objective-c\n+ (Class)layerClass\n{\n  return [CAEAGLLayer class];\n}\n```\n\n\n\nCAEGALLayer 的配置\n\n```objc\nCAEAGLLayer *layer = (CAEAGLLayer *)self.layer;\nlayer.opaque = YES;\nlayer.drawableProperties = @{\n  kEAGLDrawablePropertyRetainedBacking: [NSNumber numberWithBool:false],\n  kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8\n};\n```\n\n\n\n### 2.2 GLKViewController\n\nGLKViewController 内含 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)\n\n\n\n### 2.3 CADisplayLink\n\n当系统屏幕刷新时，通过 CADisplayLink 绑定的回调函数获取屏幕刷新帧率\n\n```objc\n- (void)createDisplayLink {\n    CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self\n                                                             selector:@selector(step:)];\n    [displayLink addToRunLoop:[NSRunLoop currentRunLoop]\n                      forMode:NSRunLoopCommonModes];\n}\n\n- (void)step:(CADisplayLink *)sender {\n    NSLog(@\"%f\", sender.targetTimestamp);\n}\n```\n\n\n\n\n\n## 3. EGL 环境配置\n\n### 3.1 EGL 的 Context \n\n[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)\n\n```objc\n// 1. 创建上下文\nEAGLContext *firstContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];\n\n// 2. 使用共享上下文\n//    把新建的上下文放到已有上下文的 EAGLSharegroup 中来确保 EAGLSharegroup 中的上下文进行资源共享\n// 注：EAGLSharegroup 为不透明类，只读，不可自行创建和修改\nEAGLContext* secondContext = [[EAGLContext alloc] initWithAPI:[firstContext API] \n                                                   sharegroup:[firstContext sharegroup]];\n\n// 3. 设置上下文为当前上下文\nif ([EAGLContext currentContext] != firstContext) {\n  [EAGLContext setCurrentContext:firstContext];\n}\n```\n\n\n\n## 4. 平台问题\n\n### 4.1 证书问题\n\n1. [苹果开发者账号申请流程完整版](https://www.jianshu.com/p/655380201685)\n\n\n\n## 5. Debug\n\n1. [使用Xcode GPU Frame Caputre教程](https://www.cnblogs.com/TracePlus/p/4093830.html)\n\n\n\n\n\n# 三、QT 平台\n\n## 1. 数据封装\n\n### 1.1 QSurface\n\n内存中的一段绘图缓冲区，对 framebuffer 的 Qt 封装对象\n有 OpengLSurface、OpenVGSurface（2D）、RasterSurface等多种类型\n\n\n\n### 1.2 QOffscreenSurface\n\n不需要在创建 QWindow 的情况下创建，本地窗口内存中的一段绘图缓冲区\n可以获取 QOffscreenSurface 中创建的 OpenGL 资源，但无法通过 read pixel 的形式获取像素数据\n\n\n\n### 1.3 QGLFunctions\n\nQGLFunctions 提供 OpenGL ES 2.0 头文件接口功能，内部成员函数都是 OpenGL 函数，如果要使用 Qt 提供的 OpenGL 函数需要：继承 QGLFunctions \n\n如果想使用 OpenGL ES 2.0 意外的 API，需要继承 QOpenGLFunctions_3_3[_Core/Compatibility] 类\n\n\n\n### 1.2 QGLBuffer、QGLColormap、QGLPixelBuffer...\n\nQt 将 OpenGL 许多用 C 风格的写成的对象封装为 Qt 内部的对象以方便与 Qt 其他的对象交互\n\n\n\n\n\n## 2. 数据的展示和刷新\n\n### 2.1 QGLWidget\n\nQGLWidget 继承自QWidget，绑定当前窗口的显存，内置 OpenGL 上下文\n\n- void initializeGL()：初始化 OpenGL 上下文\n- void resizeGL(int w, int h)：设置 OpenGL view port\n- void paintGL()：绘制 OpenGL 场景\n\n\n\n\n\n## 3. GL 环境配置\n\n### 3.1 链接库 QtOpenGL\n\n使用 Qt 内部封装的 OpenGL 函数前，需要在 qmake 的 .pro 里添加 QT += opengl 库\n\n\n\n### 3.2 配置 QOpenGLContext\n\nQOpenGLContext 代表本地窗口的 OpenGL 上下文，渲染在 QSurface 上\n\n```c++\nvoid GLWidget::initializeGL()\n{\n    QOpenGLContext* context = QOpenGLContext::currentContext();\n    QSurface* mainSurface = context->surface();\n\n  \t// 创建离屏渲染的 surface\n    QOffscreenSurface* renderSurface = new QOffscreenSurface(nullptr, this);\n\t  // 设置 QSurfaceFormat\n    renderSurface->setFormat(context->format()); \n    renderSurface->create();\n\n  \t// 设置当前 context 为 NULL\n    context->doneCurrent();\n  \n  \t// 设置当前 context 为 GLWidget 一开始默认的当前上下文\n    context->makeCurrent(mainSurface);\n}\n\nint main(int argc, char *argv[]) {\n    QSurfaceFormat format;\n    format.setMajorVersion(3);\n    format.setMinorVersion(3);\n    format.setProfile(QSurfaceFormat::CoreProfile);\n    format.setOption(QSurfaceFormat::DebugContext);\n  \n  \t// 设置 Qt 的 QOpenGLContext, QWindow, QOpenGLWidget 中 QSurface 默认的格式\n  \t// 这个默认的全局通用格式会被 Qt 内部的函数设置的格式覆盖\n    QSurfaceFormat::setDefaultFormat(format);\n  \n  \tQApplication a(argc, argv);\n    a.setWindowIcon(QIcon(\":/resources/Editor.ico\"));\n \n    QMainWindow w;\n    w.show();\n  \n    return a.exec();\n}\n```\n\n\n\n\n\n## 4. 平台问题\n\n如果使用的是不通平台的 OpenGL 原生的库，需要根据所在平台的不同在 qmake 的 .pro 里添加不同的库文件\n\n```cmake\nwin32-g++ {\n    LIBS += -lopengl32\n}\nwin32-msvc*{\n    LIBS += opengl32.lib\n}\n\nunix {\n\t\t\n}\n```\n\n\n\n\n\n## 5. Debug\n\n查看 OpenGL 上下文信息\n\n```c++\n// ViewRender 继承自 QOpenGLWidget 和 QOpenGLFunctions\nvoid ViewRender::logCtxInfo()\n{\n    QOpenGLContext *ctx = QOpenGLContext::currentContext();\n    QOpenGLContext *defaultCtx = context();\n    GLint attributeNumber;\n    glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &attributeNumber);\n    qDebug() << \"Has default context: \" <<  (defaultCtx != nullptr ? \"Yes\" : \"No\") << endl\n             << \"Default context is current context: \" << (defaultCtx == ctx ? \"Yes\" : \"No\")  << endl\n             << \"Renderer: \" << (const char*)glGetString(GL_RENDERER) << endl\n             << \"Version:  \" << (ctx->isOpenGLES() ? \"OpenGL ES\" : \"OpenGL\") << (const char*)glGetString(GL_VERSION) << endl\n             << \"Shader Version:\" << (const char*)glGetString(GL_SHADING_LANGUAGE_VERSION) << endl\n             << \"R:\" << ctx->format().redBufferSize() << endl\n             << \"G:\" << ctx->format().greenBufferSize() << endl\n             << \"B:\" << ctx->format().blueBufferSize() << endl\n             << \"A:\" << ctx->format().alphaBufferSize() << endl\n             << \"Depth:   \" << ctx->format().depthBufferSize() << endl\n             << \"Stencil: \" << ctx->format().stencilBufferSize() << endl\n             << \"Pixel Radio: \" << devicePixelRatio() << endl\n             << \"Support Attribute Number: \" << attributeNumber << endl;\n}\n```\n\n\n\n查看 OpenGL 错误信息\n\n```c++\n#ifndef QT_NO_DEBUG\n#define glCheckError() glCheckError_(__FILE__, __LINE__, this)\n#else\n#define glCheckError()\n#endif\n\nGLenum glCheckError_(const char *file, int line, QAbstractOpenGLFunctions* obj) {\n    GLenum errorCode;\n    while ((errorCode = obj->glGetError()) != GL_NO_ERROR) {\n        std::string error;\n        switch (errorCode) {\n            case GL_INVALID_ENUM:                  error = \"INVALID_ENUM\"; break;\n            case GL_INVALID_VALUE:                 error = \"INVALID_VALUE\"; break;\n            case GL_INVALID_OPERATION:             error = \"INVALID_OPERATION\"; break;\n            case GL_STACK_OVERFLOW:                error = \"STACK_OVERFLOW\"; break;\n            case GL_STACK_UNDERFLOW:               error = \"STACK_UNDERFLOW\"; break;\n            case GL_OUT_OF_MEMORY:                 error = \"OUT_OF_MEMORY\"; break;\n            case GL_INVALID_FRAMEBUFFER_OPERATION: error = \"INVALID_FRAMEBUFFER_OPERATION\"; break;\n        }\n        std::cout << \"Error:\" << error << \" File:\" << file <<  \" Line:\" << line << std::endl;\n    }\n    return errorCode;\n}\n```\n\n\n\n\n\n# Reference\n\n- [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/)\n- [Android Graphics 官方文档](https://source.android.com/devices/graphics)\n- [Android中的 EGL 扩展](http://ju.outofmemory.cn/entry/146313)\n- [SurfaceView、SurfaceHolder、Surface](https://blog.csdn.net/holmofy/article/details/66578852)\n- [TextureView、SurfaceTexture、Surface](https://blog.csdn.net/Holmofy/article/details/66583879)\n- [SurfaceView、TextureView、SurfaceTexture 等的区别](https://www.cnblogs.com/wytiger/p/5693569.html)\n- [OpenGL ES：EGL 接口解析与理解](https://blog.csdn.net/xuwei072/article/details/70049004)\n- [OpenGL ES：EGL简介](https://blog.csdn.net/iEearth/article/details/71180457)\n- [Android中 的 GraphicBuffer 同步机制 Fence](https://blog.csdn.net/jinzhuojun/article/details/39698317)\n- [深入 Android 渲染机制](https://www.cnblogs.com/ldq2016/p/6668148.html)\n- [Android Deeper(01) - Graphic Architecture](http://hukai.me/android-deeper-graphics-architecture/)\n- [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)\n- [Android 相机开发中的尺寸和方向问题](https://glumes.com/post/android/android-camera-aspect-ratio--and-orientation/)\n- [Google 官方相机 Demo](https://github.com/google/cameraview)\n- [Android 音视频开发打怪升级](https://mp.weixin.qq.com/s/4Rn5Z54lu3O55c7MK3nNpg)\n\n"
  },
  {
    "path": "ComputerGraphics(OpenGL)/Part0_Context&Pipeline.md",
    "content": "# 一、OpenGL 简介\n\n>  OpenGL 作为图形硬件标准，是最通用的图形管线版本\n>  使用 OpenGL 自带的数据类型可以确保各平台中每一种类型的大小都是统一的\n>\n>  **OpenGL 只是一个标准/规范，具体的实现是由驱动开发商针对特定显卡来实现**\n\n\n\n## 1. OpenGL Context\n\n由于 OpenGL 内部是一个类似于全局变量的状态机\n\n- 切换状态 `glEnable()`、 `glDisable()` \n- 查询状态 `glIsEnabled()` \n- 存储状态 `glPushAttrib()`：保存 OpenGL 当前属性状态信息到属性栈中\n- 恢复之前存储的状态 `glPopAttrib()`：从属性栈中获取栈首的一系列属性值\n\n\n\n**OpenGL Context 的接口和实现没有统一的标准，随着不同操作系统平台的不同而不同** \n\nOpenGL 命令执行的结果影响 OpenGL 状态（由 OpenGL context 保存，包括OpenGL 数据缓存）或 影响帧缓存\n\n1. 使用 OpenGL 之前必须先创建 OpenGL Context，并 make current 将创建的 上下文作为当前线程的上下文\n\n2. **OpenGL 标准并不定义如何创建 OpenGL Context，这个任务由其他标准定义**\n   如GLX（linux）、WGL（windows）、EGL（一般在移动设备上用）\n\n3. 上下文的描述类型有 **core profile (不包含任何弃用功能)** 或 **compatibility profile (包含任何弃用功能)** 两种\n   如果创建的是 core profile OpenGL context，调用如 glBegin() 等兼容 API 将产生GL_INVALID_OPERATION 错误（用 glGetError() 查询）\n\n4. 共享上下文\n\n   一个窗口的 Context 可以有多个，在某个线程创建后，所有 OpenGL 的操作都会转到这个线程来操作\n   两个线程同时 make current 到同一个绘制上下文，会导致程序崩溃 \n\n   一个线程同一时间只能用一个上下文，一个线程可以切换多个上下文\n   \n   一般每个窗口都有一个上下文，可以保证上下文间的不互相影响\n   通过**创建上下文时传入要共享的上下文**，多个窗口的上下文之间图形资源可以共享\n   可以共享的：纹理、shader、Vertex Buffer 等，外部传入对象\n   不可共享的：Frame Buffer Object、Vertex Array Object（内存）、Vertex Buffer Object（显存）、等 OpenGL 内置容器**对象**\n\n\n\n## 2. OpenGL 的环境配置流程\n\n### 2.1 动态获取 OpenGL 函数地址\n\nOpenGL 只是一个标准/规范，具体的实现是由驱动开发商针对特定显卡来实现，而且 OpenGL 驱动版本众多，它大多数函数的位置都**无法在编译时确定下来，需要在运行时查询**\n因此，在编写与 OpenGL 相关的程序时需要开发者自己来获取 OpenGL 函数地址\n\n相关库可以提供 OpenGL 函数获取地址后的头文件：[GLAD](https://github.com/Dav1dde/glad)\n\n\n\n### 2.2 创建上下文\n\nOpenGL 创建上下文的操作在不同的操作(窗口)系统上是不同的，所以需要开发者自己处理：**窗口的创建、定义上下文、处理用户输入**\n\n相关库可以摆脱平台的限制，提供一个较为统一的接口和窗口、上下文用来渲染：[GLUT](http://freeglut.sourceforge.net/)、SDL、SFML、[GLFW](http://www.glfw.org/download.html)\n\n\n\n## 3. OpenGL 的执行模型（Client - Server 模型）\n\n> 主函数在 CPU 上执行，图形渲染在 GPU 上执行\n> 虽然 GPU 可以编程，但这样的程序也需要在 CPU 上执行来操作 GPU\n\n基本执行模型：CPU 上 push command 命令，GPU 上执行命令的渲染操作\n\n- **应用程序 和 GPU 的执行通常是异步的**\n  OpenGL API 调用返回 != OpenGL 在 GPU 上执行完了相应命令，但保证按调用顺序执行\n  同步方式：**glFlush()** 强制发出所有 OpenGL 命令并在此函数返回后的有限时间内执行完这些 OpenGL 命令\n  异步方式：**glFinish()** 等待直到**此函数之前**的 OpenGL 命令执行完毕才返回\n\n- **应用程序 和 OpenGL 可以在也可以不在同一台计算机上执行**\n  一个网络渲染的例子是通过 Windows 远程桌面在远程计算机上启动 OpenGL 程序，应用程序在远程计算机执行，而 OpenGL 命令在本地计算机执行（**将几何数据**而不是将渲染结果图像通过网络传输）\n\n  > 当 Client 和 Server 位于**同一台计算机**上时，也称 GPU 为 Device，CPU 为 Host\n  > Device、Host 这两个术语通常在用 GPU 进行通用计算时使用\n\n- **内存管理**\n  CPU 上由程序准备的缓存数据（buffer、texture 等）存储在显存（video memory）中，这些数据从程序到缓存中拷贝，也可以再次拷贝到程序的缓存中\n  \n- **数据绑定发生在 OpenGL 命令调用时**\n  应用程序传送给 GPU 的数据在 OpenGL API 调用时解释，在调用返回时完成\n  例，指针指向的数据给 OpenGL 传送数据，如 glBufferData()  在此 API 调用返回后修改指针指向的数据将不再对 OpenGL 状态产生影响\n\n\n\n## 4. OpenGL 的着色器程序 Shader\n\n### 4.1 不同平台的 shader 编译\n\nOpenGL 的 GLSL（OpenGL Shading Language）\n\n- 跨平台\n- 运行时，将 GLSL 源码交给 GPU 图形驱动厂商编译成汇编语言后由 GPU 执行\n\n\n\nDirectX 的 HLSL（High Level Shading Language）\n\n- 微软独占，可以提前编译成机器语言，在运行时直接在 GPU 执行\n\n  \n\nNVIDIA 的 CG（C for Graphic）\n\n- 跨平台，根据平台的不同编译成相应的中间语言\n\n\n\n### 4.2 Shader 接口一致性\n\n> shader link 到 program 里可以 detached 后继续使用，这样便无法抓取 shader 查看\n\n- Vertex Shader 的 输入 和 应用程序的顶点属性数据接口 一致\n- Vertex Shader 的 输出 和 Fragment Shader 对应的 输入 一致\n- Fragment Shader 的 输出 和 帧缓存的颜色缓存接口 一致\n\n\n\n固定管线功能阶段需要的一些特定输入输出由着色器的内置输出输入变量定义，如下图\n\n![](images/vertexToFragmentAPI.png)\n\n\n\n### 4.3 GLSL 版本变化\n\n通过**首行使用** `#version` 来说明当前 OpenGL Shader Language 版本\n\n\n\n**GLSL 版本号对应 **\n\n- OpenGL 和 OpenGL 的 Shading Language 版本对应\n  | **Version OpenGL** | 2.0 | 2.1 | 3.0 | 3.1 | 3.2 | 3.3 | 4.0 | 4.1 | 4.2 | 4.3 |\n  | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n  | **Version GLSL** | 110 | 120 | 130 | 140 | 150 | 330 | 400 | 410 | 420 | 430 |\n\n- OpenGL ES 和 OpenGL ES 的 Shading Language 版本对应\n  | **Version OpenGL ES** | 2.0 | 3.0 |\n  | --------------------- | --- | --- |\n  | **Version GLSL ES**   | 100 | 300 |\n\n\n\n**GLSL 版本功能区别 **\n\n1. GLSL 130+ 版本\n   用 `in` 和  `out` 替换了 `attribute` 和 `varying`\n2. GLSL 330+ 版本\n   用 `texture` 替换了 `texture2D` \n   增加了 layout 内存布局功能\n3. [其他版本重要功能变化](https://github.com/mattdesl/lwjgl-basics/wiki/glsl-versions)\n\n\n\n### 4.4 编写 shader 的注意事项\n\n精度问题\n\n1. 颜色和单位向量用 lowp 精度\n2. 减少对 highp 的使用\n\n\n\n慎用分支和循环语句\n\n1. GPU 使用了不同于 CPU 的技术来实现分支语句\n2. 最坏情况下，花在一个分支上的时间相当于运行了所有的分支语句\n3. 使用大量流程控制语句，shader 性能可能会成倍下降\n4. 分支语句判断用的条件变量最好是常数\n5. 每个分支中的操作指令数尽量少\n6. 分支嵌套层数少\n\n\n\n## 5. 渲染同步\n\n### 5.1 同步异步的渲染方式 glFlush/glFinish\n\n> 提交给 OpenGL 的指令并不是马上送到驱动程序里执行的，而是放到一个缓冲区里面，等这个缓冲区满了再一次过发到驱动程序里执行，glFlush 可以只接提交缓冲区的命令到驱动执行，而不需要在意缓冲区是否满了\n\n同步方式：[void glFlush()](https://www.khronos.org/opengl/wiki/GLAPI/glFlush) 强制发出所有 OpenGL 命令并在此函数返回后的**有限时间**内执行这些 OpenGL 命令（这些命令可能没有执行完）\n异步方式：[void glFinish()](https://www.khronos.org/opengl/wiki/GLAPI/glFinish) 等待直到**此函数之前**的 OpenGL 命令执行完毕才返回\n\n\n\n### 5.2 垂直同步 vsync\n\n由于显示器的刷新一般是逐行进行的，因此为了防止交换缓冲区的时候屏幕上下区域的图像分属于两个不同的帧，因此交换一般会等待显示器刷新完成的信号，在显示器两次刷新的间隔中进行交换，这个信号就被称为垂直同步信号，这个技术被称为垂直同步\n\n定义：确保显卡的运算频率（GPU 一秒绘制的帧数）和 显示器刷新频率（硬件决定）一致，防止在快速运动场景下，由于**显卡运算速率大于显示器运算速率**导致快速运动的动作割裂情况（画面撕裂）\n\n流程：`显卡绘制一帧时间 > 显示器刷新一帧时间 ? 显示器刷新(显卡等待) : 显示器显示上一帧，等待显卡绘制完成(屏幕卡顿);`\n\n缺点：开启垂直同步，画面会有延迟（无法达到显卡的最大运算速率），但并没有卡顿\n\n**规避缺点的方法**：用三重缓冲代替垂直同步（三重缓冲：在双缓冲的基础上加了一个缓冲，引入了三缓冲区技术，在等待垂直同步时，来回交替渲染两个离屏的缓冲区，而垂直同步发生时，屏幕缓冲区和最近渲染完成的离屏缓冲区交换，实现充分利用硬件性能的目的）\n\n\n\n## 6. 高版本 Feature\n\n### 6.1 Draw Indirect\n\nIndirect：绘制数据直接从显存拿，非直接传输的关系，便于通过 compute shader 直接在 GPU 端直接修改 Buffer 内容\n\n- **Draw Instanced（GPU Instance）**\n  绘制多个相同的 Mesh，但它们的位置可以各部相同（如：绘制有多个相同士兵的军队）\n  需要完全一样的顶点、索引、渲染状态和材质数据，只允许 Transform 不一样\n\n  ```c\n  // 普通绘制\n  void glDrawArrays(GLenum mode, GLint first, GLsizei tCount);\n  // 绘制多个相同的 Mesh \n  void glDrawArraysInstanced(GLenum mode, GLint first, GLsizei tCount, GLsizei meshCount);\n\n- **Draw Instanced Indirect**\n  一次只能绘制一个 Mesh\n  Mesh 数据直接从显存里获取数据，并非直接传输关系因此称为 Indirect\n\n  ```c++\n  // 1. 创建显存 Mesh 数据等参数\n  typedef struct {\n  \tGLuint vertexCount;\n  \tGLuint instanceCount; // 绘制 Mesh 个数\n  \tGLuint firstVertex;\n  \tGLuint baseInstance;\n  } DrawArraysIndirectCommand;\n  \n  static const DrawArraysIndirectCommand arraysCommand = {\n    3, // Three vertices in total, making one triangle\n    1, // Draw one copy of this triangle\n    0, // Starting index\n    0  // Reserved\n  }; \n  assert(sizeof(DrawArraysIndirectCommand) == 16);\n  \n  GLuint arrayCommandBuffer;\n  glCreateBuffers(1, &arrayCommandBuffer);\n  glNamedBufferData(arrayCommandBuffer, sizeof(DrawArraysIndirectCommand), &arraysCommand, GL_STATIC_DRAW);\n  \n  // 2. 直接获取显存数据绘制\n  void glDrawArraysIndirect(\n    GLenum mode,         // GL_TRIANGLES\n    const void *indirect // NULL 因为已经 bind GPU buffer 了\n  );\n  ```\n\n- **Multi Draw Indirect**\n  绘制多个相同的 Mesh，Mesh 数据直接从显存里获取数据（一份数据有多个 Mesh 需要的 CMD）\n\n  ```c\n  // 1. 创建\n  DrawArraysIndirectCommand draws[] =\n  { // 一份数据\n  \t{\n  \t\t42, // Vertex count\n  \t\t1,  // Instance count\n  \t\t0,  // First vertex\n  \t\t0   // Base instance\n  \t},\n  \t{ /** ... */}\n  };\n  GLuint buffer;\n  glGenBuffers(1, &buffer);\n  glBindBuffer(GL_DRAW_INDIRECT_BUFFER, buffer);\n  glBufferData(GL_DRAW_INDIRECT_BUFFER, sizeof(draws), draws, GL_STATIC_DRAW);\n  \n  // 2. 直接获取显存数据绘制\n  void glMultiDrawElementsIndirect(\n    GLenum mode,           // GL_TRIANGLES\n    GLenum type,           // GL_UNSIGED_INT\n    const void * indirect, // NULL 因为已经 bind GPU buffer 了\n    GLsizei drawcount,     // sizeof(draws)/sizeof(draws[0])\n    GLsizei stride         // 0\n  );\n  ```\n\n  \n\n\n\n# 二、渲染管线\n\n> 所谓 OpenGL 管线（OpenGL pipeline），就是指 OpenGL 的渲染过程，即从输入数据到最终产生渲染结果数据所经过的通路及所经受的处理\n\n真实生活中的流水线：\n\n![](./images/pipeline_live.png)\n\n OpenGL 4.4 渲染管线\n\n![](images/pipeline_gl4.4.png)\n\nDirectX3D 12 渲染管线\n\n![](./images/pipeline_DXD12.png)\n\n![](./images/pipeline.png)\n\n## 1. 应用阶段\n\n应用阶段是开发者可以完全把控的阶段\n\n**绘制物体的包围盒一共有两种，且同时使用**\n\n1. 球体包围盒（用于快速碰撞检测）：尺寸比其包含的对象要大\n2. 箱体包围盒（更准确的碰撞检测）：更接近于对象形状，但是计算量大\n\n\n\n一般游戏引擎的绘制顺序\n\n```c\n// 每一帧：Draw layer > Draw Technique > Draw Pass\n// 每一 Pass：相关 view、相关 shader、相关 material、相关 object\nfor each view {\n    bind view resources\t\t\t\t\t// camera, environment...\n      \n    for each shader {\n        bind shader pipeline\n        bind shader resources\t\t\t// shader control values\n          \n\t\t\tfor each material {\n        \t\tbind material resources\t// material params and textures\n              \n            for each object {\n              \tbind object reources\t// object transforms\n                draw object\n                  \n            } // object\n          \n        } // material\n      \n    } // shader\n  \n} // view\n```\n\n### 1.1 计算 Level Of Detail\n\n作用：让近处物体的网格更细致（LOD 0），远处物体的网格更稀疏（LOD n），以便场景运行更流畅\n\n方法：根据一定标准切换 LOD 等级，提前设置好 LOD 等级对应的标准\n如果达到标准，就会替换当前 Mesh 为对应的 LOD 等级\n\n常用的 LOD 判断标准有：\n\n1. 视距 View Distance：物体球体包围盒，距离视点的距离\n2. 屏占比 Screen Size：物体箱体包围盒，经过透视投影变换后，在当前屏幕渲染像素总数的占比\n   比例范围 0.0 ~ 1.0，1.0 表示屏占比 100%\n\n\n\n### 1.2 距离体积剔除\n\n在场景里放入一个**箱体**，在箱体范围内通过设置绘制物体的 尺寸 与对应的 相机最大距离 来剔除物体\n\n- 物体尺寸：指的是物体球体包围盒的**直径**大小\n- 相机最大距离：物体与相机的距离大于这个距离后相应 物体尺寸的物体会被剔除，如果不设置（值为 0）将永远不会剔除与其配对的物体尺寸\n\n\n\n### 1.3 视锥剔除\n\n![](./images/culled_view_frustum.png)\n\n> 在裁剪空间下更容易进行视锥剔除，详见 几何阶段的视锥剔除\n\n在世界空间下的视锥剔除流程（剔除的是包围盒 Mesh 对象，而非单个三角面图元）\n\n1. 计算包围要绘制物体的 AABB 盒\n2. 获得视锥体六个面的平面方程\n3. 判断 AABB 盒的最小点和最大点在六个面的内侧还是外侧\n4. 剔除掉最小和最大点完全在外侧的物体\n\n```c++\n// 视锥体的六个平面方程，用于视锥剔除\n// 所得的法向都是指向内部的（面向原点）\nvoid GetViewingFrustumPlanesByProjM4(std::vector<glm::vec4> & result , const glm::mat4 &vp) {\n\t//左侧  \n\tresult[0].x = vp[0][3] + vp[0][0];\n\tresult[0].y = vp[1][3] + vp[1][0];\n\tresult[0].z = vp[2][3] + vp[2][0];\n\tresult[0].w = vp[3][3] + vp[3][0];\n\t//右侧\n\tresult[1].x = vp[0][3] - vp[0][0];\n\tresult[1].y = vp[1][3] - vp[1][0];\n\tresult[1].z = vp[2][3] - vp[2][0];\n\tresult[1].w = vp[3][3] - vp[3][0];\n\t//上侧\n\tresult[2].x = vp[0][3] - vp[0][1];\n\tresult[2].y = vp[1][3] - vp[1][1];\n\tresult[2].z = vp[2][3] - vp[2][1];\n\tresult[2].w = vp[3][3] - vp[3][1];\n\t//下侧\n\tresult[3].x = vp[0][3] + vp[0][1];\n\tresult[3].y = vp[1][3] + vp[1][1];\n\tresult[3].z = vp[2][3] + vp[2][1];\n\tresult[3].w = vp[3][3] + vp[3][1];\n\t//Near\n\tresult[4].x = vp[0][3] + vp[0][2];\n\tresult[4].y = vp[1][3] + vp[1][2];\n\tresult[4].z = vp[2][3] + vp[2][2];\n\tresult[4].w = vp[3][3] + vp[3][2];\n\t//Far\n\tresult[5].x = vp[0][3] - vp[0][2];\n\tresult[5].y = vp[1][3] - vp[1][2];\n\tresult[5].z = vp[2][3] - vp[2][2];\n\tresult[5].w = vp[3][3] - vp[3][2];\n}\n\n//点到平面距离 d =  Ax + By + Cz + D;\n// d < 0 点在平面法向反方向所指的区域\n// d > 0 点在平面法向所指的区域\n// d = 0 在平面上\n// d < 0为 false\nbool Point2Plane(const glm::vec3 &v,const glm::vec4 &p) {\n\treturn p.x * v.x + p.y * v.y + p.z * v.z + p.w >= 0;\n}\n\nstd::vector<glm::vec4> ViewPlanes;\n//构造函数中\nViewPlanes.resize(6, glm::vec4(0));\n\nvoid UpdateViewPlanes() {\n\tViewingFrustumPlanes(ViewPlanes,  ProjectMatrix * ViewMatrix);\n}\n\nbool ViewCull(const glm::vec4 &v1,const glm::vec4 &v2,const glm::vec4 &v3) {\n\tglm::vec3 minPoint, maxPoint;\n\tminPoint.x = min(v1.x, min(v2.x, v3.x));\n\tminPoint.y = min(v1.y, min(v2.y, v3.y));\n\tminPoint.z = min(v1.z, min(v2.z, v3.z));\n\tmaxPoint.x = max(v1.x, max(v2.x, v3.x));\n\tmaxPoint.y = max(v1.y, max(v2.y, v3.y));\n\tmaxPoint.z = max(v1.z, max(v2.z, v3.z));\n\t// Near 和 Far 剔除时只保留完全在内的\n\tif (!Point2Plane(minPoint, ViewPlanes[4]) || !Point2Plane(maxPoint, ViewPlanes[4])) {\n\t\treturn false;\n\t}\n\tif (!Point2Plane(minPoint, ViewPlanes[5]) || !Point2Plane(maxPoint, ViewPlanes[5])) {\n\t\treturn false;\n\t}\n\tif (!Point2Plane(minPoint, ViewPlanes[0]) && !Point2Plane(maxPoint, ViewPlanes[0])) {\n\t\treturn false;\n\t}\n\tif (!Point2Plane(minPoint, ViewPlanes[1]) && !Point2Plane(maxPoint, ViewPlanes[1])) {\n\t\treturn false;\n\t}\n\tif (!Point2Plane(minPoint, ViewPlanes[2]) && !Point2Plane(maxPoint, ViewPlanes[2])) {\n\t\treturn false;\n\t}\n\tif (!Point2Plane(minPoint, ViewPlanes[3]) && !Point2Plane(maxPoint, ViewPlanes[3])) {\n\t\treturn false;\n\t}\n\treturn true;\n}\n```\n\n\n\n### 1.4 遮挡剔除\n\n![](./images/culled_occlusion.png)\n\n#### 1.4.1 遮挡查询 - 硬件\n\n**一、CPU-Driven 的剔除**\n\n以下方法均可以通过 CPU 读取深度，或者采用硬件提供的 Early-Z 功能在<u>片源着色执行前</u>从 GPU 读取深度\n\n- **中小场景剔除**：将 Mesh 对象的包围盒（或最高 LOD 级别的模型）写入到 z-buffer 上，然后使用物体的包围盒传入到 GPU 进行遮挡测试\n  从 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)\n\n- **大场景剔除 HZB**，Hierarchical z-buffer：多 Mip 层级的 z-buffer\n  Pass 1：Level n 的 Mip 记录 Level n-1 中周围**四个像素**中**最远处**的深度值（这样只要符合高等级的 Mip 深度，就能覆盖到低等级）\n  Pass 2：根据 Mesh 对象包围盒的大小判断选择哪个 Level 的 Mip，选定 Level 后通过比较 Mip 上的深度值来判断是否被遮挡\n\n![](./images/culled_HZB.png)\n\n**二、GPU-Driven 的剔除**\n\n1. 创建 Indirect 指令队列，将所有待渲染物体的渲染指令录入\n2. 对渲染物体进行遮挡剔除，将剔除结果写入到 buffer 中\n3. 根据结果 GPU 会选择性执行录入的 Indirect 渲染指令，达到剔除的效果\n\n\n\n#### 1.4.2 遮挡查询 - 软件（移动平台）\n\n将 Mesh 对象的包围盒（或最高 LOD 级别的模型）软光栅到 CPU 内存中的 z-buffer 上，然后根据 z-buffer 中 的深度信息来判断那些 Mesh 对象需要剔除\n支持任意大小的场景，CPU 端压力较大\n\n\n\n#### 1.4.3 静态剔除 - 预计算（移动平台）\n\n![](./images/culled_precomputed.png)\n\n在场景里放入一个**箱体**，在箱体范围内将场景划分成一个个 Cell（适合中小型场景的性能优化）\n\n- Precomputed Visibility Volumes 预计算可见性（无法剔除动态物体）\n  每个 Cell 区域预计算出相机在这个 Cell 范围内所有可能看到的物体，并将信息存储下来\n  在运行时直接查表来得到所有静态物体的可见信息\n- [Portal-Culling](https://www.gamedeveloper.com/programming/sponsored-feature-next-generation-occlusion-culling)（可以剔除动态物体）\n  每个 Cell 区域预计算出两个相邻 Cell 之间的连通性，并将信息存储下来\n  在运行时根据相机所在的 Cell 间的连通性信息 和 相机观察方向快速计算出目标物体是否处于可见范围内\n\n\n\n\n\n## 2. 几何阶段\n\n几何阶段里的部分流程（从视锥剔除开始）开发者无法控制，由不同平台的系统驱动自行完成\n\n![](./images/coordinate.png)\n\n### 2.1 Vertex Shader\n\n#### 2.1.1 观察/相机 空间\n\n![](./images/camera_axes.png)\n\n**LookAt 矩阵**：将世界空间坐标 乘以 lookat 矩阵 可以得到相机的 观察空间\n$$\nLookAt =\n\\begin{bmatrix}\n\\color{red}{R_x} & \\color{red}{R_y} & \\color{red}{R_z} & 0 \\\\\n\\color{green}{U_x} & \\color{green}{U_y} & \\color{green}{U_z} & 0 \\\\\n\\color{blue}{D_x} & \\color{blue}{D_y} & \\color{blue}{D_z} & 0 \\\\\n0 & 0 & 0 & 1\n\\end{bmatrix}\n\\begin{bmatrix}\n1 & 0 & 0 & \\color{orange}{-P_x} \\\\\n0 & 1 & 0 & \\color{orange}{-P_y} \\\\\n0 & 0 & 1 & \\color{orange}{-P_z} \\\\\n0 & 0 & 0 & 1\n\\end{bmatrix}\n$$\n相机对象和 LookAt 矩阵是两套不同的坐标系：\n相机对象的坐标系要和自己所处的世界坐标的坐标系保持一致，而 LookAt 的坐标系必须与世界坐标系的 Z 轴方向相反\n\n- 其中，P 为相机的位置、U 为相机的 Y 轴、R 为相机的 X 轴、D 为相机指向的方向和相机的 Z 轴相反\n- 相机对象的 Z 轴和 LookAt 矩阵 D 相反，其他轴和 LookAt 矩阵的基坐标相同，同时也和世界坐标的基坐标相同\n  因此，如果 相机对象为右手坐标系，LookAt 矩阵为左手坐标系，这样是方便其他方向的移动\n\n```c\n// 1. 计算 lookAt 矩阵的坐标系（根据指向的 目标坐标和相机坐标 求得）\nglm::vec3 cameraFront = glm::normalize(cameraTarget - cameraPos);\t\t// 指向目标物体\nglm::vec3 lookAtDirection = glm::normalize(cameraPos - cameraTarget); \t// 指向相机\n\nglm::vec3 WorldUp = glm::vec3(0.0f, 1.0f, 0.0f); \nglm::vec3 lookAtRight = glm::normalize(glm::cross(WorldUp, lookAtDirection)); // LookAt 为右手坐标系\nglm::vec3 lookAtUp = glm::cross(lookAtDirection, lookAtRight);\n\n// 2. 计算相机对象的坐标系（根据欧拉角 Yaw，Pich 求得）\nglm::vec3 cameraFront;\ncameraFront.x = cos(glm::radians(Yaw)) * cos(glm::radians(Pitch));\ncameraFront.y = sin(glm::radians(Pitch));\ncameraFront.z = sin(glm::radians(Yaw)) * cos(glm::radians(Pitch));\ncameraFront = glm::normalize(cameraFront);\n\ncameraRight = glm::normalize(glm::cross(cameraFront, WorldUp));  // 相机对象为左手坐标系\ncameraUp    = glm::normalize(glm::cross(cameraRight, cameraFront));\n```\n\n\n\n#### 2.1.2 透视/正交 投影变换\n\n将观察空间的坐标转换到投影空间（一个 Frustum 平截头体空间），详见 [矩阵变换，透视投影](../LinearAlgebra/Part1_Matrix.md)\n\n<img src=\"./images/projection.png\" style=\"zoom: 80%;\" />\n\n**OpenGL 的透视投影矩阵为**\n\n- 列主序矩阵\n- 相机坐标系为 **右手坐标系**\n- Z 的标准设备空间范围限定为 [-w, w]\n\n$$\nM_{OpenGL} * P = \n\\begin{bmatrix}\nnear \\over right & 0 & 0 & 0 \\\\\n0 & near \\over top & 0 & 0\\\\\n0 & 0 & -{{far+near} \\over {far - near}} & -{2\\cdot far \\cdot near \\over {far - near}}\\\\\n0 & 0 & -1 & 0\\\\\n\\end{bmatrix}\n\\begin{bmatrix}\nx \\\\ y \\\\ z \\\\ 1\n\\end{bmatrix}\n= \n\\begin{bmatrix}\n{near \\over right}x \\\\ {near \\over top}y \\\\ {-{{far+near} \\over {far - near}}}z -{2 \\cdot far \\cdot near \\over {far - near}} \\\\ -z\n\\end{bmatrix}\n$$\n\n**DriectX  的透视投影矩阵为**\n\n- 行主序矩阵\n- 相机坐标系为 **左手坐标系**\n- Z 的标准设备空间范围限定为 [0,w]\n\n$$\nP * M_{DriectX} = \n\\begin{bmatrix}\nx & y & z & 1\n\\end{bmatrix}\n\\begin{bmatrix}\nnear \\over right & 0 & 0 & 0 \\\\\n0 & near \\over top & 0 & 0\\\\\n0 & 0 & {far \\over {far - near}} & 1 \\\\\n0 & 0 & -{far \\cdot near \\over {far - near}} & 0\\\\\n\\end{bmatrix}\n= \n\\begin{bmatrix}\n{near \\over right}x & {near \\over top}y & {{far \\over {far - near}}}z -{far \\cdot near \\over {far - near}} & z\n\\end{bmatrix}\n$$\n\n\n\n### 2.2 Tessellation Shader\n\n曲面细分阶段：降低带宽负载，通过较少的 Mesh，经过 曲面细分的算法计算可以生成更精细的 Mesh 网格（方便 Level Of Details 的实现）\n\n1. Tessellation **control** shaders (**H**ull **S**haders) \n2. Tessellation **evaluation** shaders (**D**omain **S**haders)\n\n![](./images/tesselation_pipeline.svg)\n\n\n\n### 2.3 Geometry Shader\n\n几何着色阶段：可以由已知的图元生成新的图元（点、线、面），一般多用于\n\n- 由顶点产生的粒子特效\n- 几何曲面细分\n- [Shadow Volume 加速](https://developer.nvidia.com/gpugems/gpugems3/part-ii-light-and-shadows/chapter-11-efficient-and-robust-shadow-volumes-using)\n- 只用一个 Pass 绘制 Cube map（6 个面）\n\n\n\n### 2.4 裁剪（硬件实现）\n\n#### 2.4.1 视锥剔除\n\n一个面的三个顶点如果都被剔除，则当前**三角形**被剔除\n\n```c\nstd::uint8_t checkViewCut(const glm::vec4& v)\n{\n    auto ret = (std::uint8_t)0;\n    \n    if      (v.x < -v.w) ret |= 1;\n    else if (v.x >  v.w) ret |= 2;\n    if      (v.y < -v.w) ret |= 4;\n    else if (v.y >  v.w) ret |= 8;\n    if      (v.z < -v.w) ret |= 16;\n    else if (v.z >  v.w) ret |= 32;\n    \n    return ret;\n}\n```\n\n\n\n#### 2.4.2 齐次坐标裁剪\n\n![](./images/culled_GPU.png)\n\n裁剪后三角网格的数量和连接方式可能会变（裁剪得到的顶点属性值会提前插值好）\n假设 $P(x,y,z,w)$ 为投影空间内部的一个点，则\n$$\n-1 <= x/w <=1 \\\\\n-1 <= y/w <=1 \\\\\n-1 <= z/w <=1 \\\\\n\n-w <= x <= w \\\\\n-w <= y <= w \\\\\n-w <= z <= w\n$$\n由 [线与面的关系判断](../LinearAlgebra/Part3_Triangles.md) 可知，如果线 $Q_1Q_2$ 与面交与点 $I$，则\n$$\n\\begin{align}\nQ_1&=(x_1,y_1,z_1,w_1) \\\\\nQ_2&=(x_2,y_2,z_2,w_2) \\\\\\\\\nI &= Q_1 + t(Q_2 - Q_1) \\\\\nw_1 + t(w_2 - w_1) &= x_1 + t(x_2 - x_1) \\\\\nt &= {{w_1 - x_1} \\over (w_1 - x_1) - (w_2 - x_2)} \n\\end{align}\n$$\n注意：为了防止透视除法除的 $w$ 为 0，这里裁剪的时候还要裁剪掉一个 $w=1e-5$ 这样一个极小数的平面\n\n```c\nenum AXIS {\n    X = 0,\n    Y = 1,\n    Z = 2,\n    W = 3\n}\n\n// EX: 0,1,2,3\n//     3-0, 0-1, 1-2\nvoid clipInHomoCoord(std::vector<Vertex>& vertIn, std::vector<Vertex>& vertOut, AXIS axis, bool isNegative)\n{\n    vertOut.clear();\n\n    int preDot = -1;\n    int curDot = -1;\n    float w = 1.0f;\n    float flag = isNegative ? -1.0f : 1.0f;\n    for (int i = 0; i < vertIn.size(); ++i)\n    {\n        Vertex& preVert = vertIn[(i + vertIn.size() -1) % vertIn.size()];\n        Vertex& curVert = vertIn[i];\n        preDot = flag * preVert.position[axis] <= preVert.position.w ? 1 : -1；\n        curDot = flag * curVert.position[axis] <= curVert.position.w ? 1 : -1;\n        if (preDot * curDot < 0) // put intersection point first\n        {\n            w = preVert.position.w - flag * curVert.position[axis];\n            w = w / (w - (curVert.position.w - flag * curVert.position[axis]));\n            vertOut.push_back( lerp(preVert, curVert, w) ); \n        }\n        if (curDot > 0)\t\t\t// then put original point\n        {\n            vertOut.push_back(curVert);\n        }\n    }\n}\n\n// how to call clip function\nstd::vector<Vertex> vertIn;\nstd::vector<Vertex> vertOut;\nclipInHomoCoord(vertIn, vertOut, X, true);\t// clip on x axis\nclipInHomoCoord(vertOut, vertIn, X, false);\nclipInHomoCoord(vertIn, vertOut, Y, true); \t// clip on y axis\nclipInHomoCoord(vertOut, vertIn, Y, false);\nclipInHomoCoord(vertIn, vertOut, Z, true);\t// clip on z axis\nclipInHomoCoord(vertOut, vertIn, Z, false);\n\nauto& vertexOut = vertIn;\t// output\n\n// draw point order must use trangles fan\nGL_TRIANGLE_FAN\n```\n\n\n\n#### 2.4.3 透视除法和 NDC 空间\n\n通过透视除法将 <u>齐次坐标</u> 转换为的 <u>非齐次坐标</u> 具体见 [矩阵变换，齐次空间](../LinearAlgebra/Part1_Matrix.md)\n将坐标点从 投影空间 转换到 NDC （Normalized Device Coordinates  标准设备坐标系）空间\n\n```c\n// Scope: [-w, w]\nglm::vec4 proj;\n\n// 如果 w 为 0，表示一个三维坐标点，则 令 w 为 1.0，表示一组齐次坐标点\nif (0 == proj.w) proj.w = 1e-5f;\n\n// Scope: [-1, 1]\nglm::vec4 ndc = glm::vec4(proj.x / proj.w, \n                          proj.y / proj.w, \n                          proj.z / proj.w,\n                          1.0f);\n```\n\n\n\n#### 2.4.4 将坐标映射到屏幕上\n\n**屏幕坐标和像素的映射关系**\n\n- 屏幕坐标是 2D 纹理坐标\n  归一化后的裁剪坐标转换到屏幕坐 标的矩阵\n  $$\n  \\begin{bmatrix}\n  {width \\over 2} & 0 & 0 & {width \\over 2} \\\\\n  0 & {height \\over 2} & 0 & {height \\over 2} \\\\\n  0 & 0 & 1 & 0 \\\\\n  0 & 0 & 0 & 1\n  \\end{bmatrix}\n  $$\n\n- 屏幕坐标表示的是屏幕空间中的像素坐标\n\n- OpenGL 和 DirectX 10 以后的版本认为 像素中心 对应 屏幕坐标的值为 0.5，例：\n  屏幕分辨率为 400 X 300，则其屏幕坐标 x 的范围是 [0.5, 400.5]，y 的范围是 [0.5, 300.5]\n  $$\n  Screen_x = (1 + x_{标准设备坐标}) \\cdot {Pixel_{width} \\over 2} \\\\\n  Screen_y = (1 + y_{标准设备坐标}) \\cdot {Pixel_{height} \\over 2}\n  $$\n  \n\n\n\n#### 2.4.5 背面剔除 2D\n\n在绘制 Enclosed solid 物体时，有正面自遮挡了其背面的三角面的情况，这时候需要剔除不需要被看到的三角面\n\n1. 在平台的坐标系下，根据三角形两边的叉乘可以知道三角面的法线朝向\n2. 根据法线的朝向可以判断当前三角面是正面还是背面\n3. 根据需求剔除正面或者背面的三角面，以减少不必要的 drawcall\n\n\n\n#### 2.4.6 视口剔除 2D\n\n光栅化时，根据设置的 Viewport 大小来限定光栅化范围（限定逐行光栅化的行数和列数）\n以达到只有视口内部的数据被刷新了，避免了不必要的计算\n\n\n\n\n\n## 3. 着色阶段\n\n### 3.1 光栅化（硬件实现）\n\n1. **三角形设置 Triangle Setup**\n   计算三角网格表示数据（每条边上的像素坐标）\n2. **三角遍历 Triangle Traversal**\n   查找被三角形覆盖的像素，生成一个图元，这一过程又称扫描变换 Scan Conversion\n3. **光栅化 Rasterization**\n   在三角形中心坐标中，将顶点信息线性差值并根据渲染目标像素个数，对一个三角形图元信息做离散化处理\n\n\n\n### 3.2 Early-Z / Pixel Shader\n\n**Pre-Z**：应用阶段实现，需要单独 Pass，只写入深度，开启  Alpha 测试，用来剔除**透明像素**\n**Early-Z**（Z-cull）：需要硬件支持，只能用来剔除**不透明 像素块（相邻的 4 个像素）**\n<u>提前在 Pixel Shader 着色前判断，防止 Pixel Shader 计算后在进行深度测试才被 discard 而带来的不必要计算</u>\n使用 Early-Z 需要关闭深度写入，无法进行 Alpha 混合，每次使用的深度需要 Clear 一下\n使用 Early-Z 需要**从前向后**渲染不透明物体，才能体现出它的优势\n\n\n\n### 3.3 逐像素处理（可配置）\n\n1. **Scissor 测试**\n   裁剪测试，查看是否设置裁剪区域，如果有在裁剪区域外的像素会被剔除\n\n1. **Alpha 测试**\n   过滤掉那些透明的片段，让剩下的不透明片段来写入缓存\n   **提前 Alpha 测试**，在 Pixel Shader 阶段提前根据像素的 alpha 值，使用 GLSL 的 discard 命令剔除像素\n   例：一个草的方形图片其透明的部分像素应该被剔除掉\n   \n1. **Stencil 测试**\n   模版测试，根据二值图的模版 buffer 在当前 color buffer 上填充其他颜色\n   例：给一张白纸画一个红印章（或文字），这样会使得存储更少，不需要每个像素都存储印章的单一颜色\n   ![](./images/stencil_test.png)\n   \n2. **深度测试**\n   深度缓冲中每个像素（或超采样）都有对应的深度值（通过三角形顶点深度信息差值得到）\n   深度测试通过，深度缓冲将会更新深度值\n   因为每个像素都有深度，所以不会存在两个图元交叉的深度问题\n   深度测试之后，Alpha 混合之前，会**更新 Occlusion query**\n   ![](./images/depth_test.png)\n   \n2. **Alpha 混合**\n   开启 alpha 混合会关闭深度写入（如果不关闭后面片元将会被踢出，无法进入到 alpha 混合缓解混合颜色）\n   但是深度测试依旧可以进行，**深度值对 Alpha 混合来说是只读的**\n   \n   为了正确的做 alpha 混合，一般流程如下\n   \n   1. 确保混合的 alpha 物体是凸面体，将复杂的模型拆分成可独立排序的多个子模型\n      从而防止循环重叠半透明物体出现\n   2. 先渲染所有不透明物体，并且开启他们的深度测试和深度写入\n   3. 开启深度测试，关闭深度写入（当前深度值已经确定）\n      把半透明物体按照深度值依次排序，**从后向前渲染**（确保半透明物体被不透明物体遮挡）\n      但这仍会存在两个图元交叉，导致从后向前渲染排序有误的问题。可以通过添加 alpha 缓存，或者使用排序无关的半透明混合方式（Depth peeling）\n\n\n\n\n\n# 三、常见问题\n\n## 1. Alpha 预乘\n\n关闭 Alpha 预乘的混合方式（假设：透明物体 B 在 A 前面）\n$$\n\\begin{align}\nA &= (A_r,A_g,A_b, \\alpha_A)\\\\\nB &= (B_r,B_g,B_b, \\alpha_B)\\\\\nM_{rgb} &= \\alpha_B B + (1-\\alpha_B)\\alpha_A A\\\\\n\\alpha_M &= \\alpha_B + (1-\\alpha_B)\\alpha_A\n\\end{align}\n$$\n\n开启 Alpha 预乘的混合方式（假设：透明物体 B 在 A 前面）\n透明图像边缘是黑色，为了防止在混合多个透明物体时 alpha 遮罩外的颜色由于不是黑色 0，而带来的混合颜色的色差\n$$\n  \\begin{align}\n  A' &= (\\alpha_A A_r,\\alpha_A A_g,\\alpha_A A_b, \\alpha_A)\\\\\n  B' &= (\\alpha_B B_r,\\alpha_B B_g,\\alpha_B B_b, \\alpha_B)\\\\\n  M'_{rgb} &= B' + (1-\\alpha_B) A'\\\\\n  \\alpha_M &= \\alpha_B + (1-\\alpha_B)\\alpha_A \\\\\n  M_{rgb} &= M'_{rgb} / \\alpha _M\n  \\end{align}\n$$\n\n![](./images/alpha_multiply.png)\n\n\n\n## 2. Overshading / Quad overdraw\n\n**四个相邻的 [Pixel  Quad](https://www.khronos.org/registry/OpenGL/extensions/NV/NV_shader_thread_group.txt) 是 Early-Z 的最小剔除单位**\n**硬件原因**：一个 GPU 内 SM 的 Warp 同时处理相邻的 2x2 个像素（每个线程处理一个像素，但是同指令，不同数据）\n四个像素中非当前图元数据的输出值会被丢弃，但其插值数据仍会被使用（例：dFx，dFy）\n\n- 业务场景：由于三角形（相对于像素大小）过于密集同一 drawcall 里，多个图元之间对像素数据的重复绘制\n- 解决方法：一般使用 Level Of Details 技术可以缓解\n\n![](./images/Overshading.png)\n\n\n\n## 3. GPU 指令依赖\n\nGPU 指令组之间存在数据依赖关系，会拉长指令组之间的执行时间\n下图蓝色椭圆气泡 bubble 的产生是为了解决 GPU 指令组之间的数据依赖，bubble 其实际上指空等待\n\n![](./images/GPU_CMD.png)\n\n\n\n## 4. Compute Shader\n\nDirectX3D 12 Compute Shader pipeline\n\n![](./images/pipeline_compute_DXD12.png)\n\n[Compute Shader](https://en.wikipedia.org/wiki/Compute_kernel) 指一个过程：多用于卷积核 kernel 的并行加速计算 与 GPU 共享顶点着色器和像素着色器的执行单元，并不局限于图形渲染领域，多用于并行计算领域\n\n\n\n\n\n# Reference\n\n1. [SIGGRAPH courses](http://advances.realtimerendering.com/)\n1. [OpenGL Loading Library](https://www.khronos.org/opengl/wiki/OpenGL_Loading_Library)\n2. [OpenGL Tools](http://www.opengl-tutorial.org/miscellaneous/useful-tools-links/)\n5. [GLSL Versions](https://github.com/mattdesl/lwjgl-basics/wiki/glsl-versions)\n6. [learnopengl-Blending](https://learnopengl-cn.github.io/04 Advanced OpenGL/03 Blending/)\n7. [TriangleRasterization](http://www.sunshine2k.de/coding/java/TriangleRasterization/TriangleRasterization.html#algo2)\n8. [Platform-specific rendering differences](https://docs.unity3d.com/Manual/SL-PlatformDifferences.html)\n9. [Stateless, layered, multi-threaded rendering](https://blog.molecular-matters.com/2014/11/06/stateless-layered-multi-threaded-rendering-part-1/)\n10. [Graphics pipeline](https://en.wikipedia.org/wiki/Graphics_pipeline)\n10. [Pipeline stall](https://en.wikipedia.org/wiki/Pipeline_stall)\n11. [Game Programming Patterns](http://gameprogrammingpatterns.com/contents.html)\n12. [Shader detached program](https://github.com/google/gapid/issues/398)\n13. [Explaining Homogeneous Coordinates & Projective Geometry](https://www.tomdalling.com/blog/modern-opengl/explaining-homogenous-coordinates-and-projective-geometry/)\n14. [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)\n15. [OpenGL Projection Matrix (songho.ca)](http://www.songho.ca/opengl/gl_projectionmatrix.html)\n16. [3D Clipping in Homogeneous Coordinates. | Development Chaos Theory (chaosinmotion.com)](https://chaosinmotion.com/2016/05/22/3d-clipping-in-homogeneous-coordinates/)\n17. [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)\n18. [CLIPPING by Kenneth I. Joy](https://link.zhihu.com/?target=https%3A//fabiensanglard.net/polygon_codec/clippingdocument/Clipping.pdf)\n19. [Clipping implementation](https://link.zhihu.com/?target=https%3A//fabiensanglard.net/polygon_codec/)\n20. [Nvidia GPU Programming](https://developer.download.nvidia.cn/GPU_Programming_Guide/GPU_Programming_Guide_G80.pdf)\n20. [Per-Sample Processing](https://www.khronos.org/opengl/wiki/Per-Sample_Processing)\n20. [OGL-Community LOD Selection](https://community.khronos.org/t/lod-selection/49560)\n20. [3D C/C++ tutorials](http://www.3dcpptutorials.sk/index.php)\n24. [Developing a Software Renderer Part 1](https://trenki2.github.io/blog/2017/06/06/developing-a-software-renderer-part1/)\n25. [Render Hell !!!!!](https://simonschreibt.de/gat/renderhell-book1/)\n20. [移动游戏性能优化通用技法](https://www.cnblogs.com/timlly/p/10463467.html)\n20. [水平同步 垂直同步](https://blog.csdn.net/hankern/article/details/90344384)\n21. [Android 的 16ms 和垂直同步以及三重缓存](https://www.jianshu.com/p/3750db831aca)\n21. [计算机图形学补充2：齐次空间裁剪(Homogeneous Space Clipping) - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/162190576)\n22. [从零开始的软渲染器（2.5）- 再谈裁剪与剔除 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/97371838)\n23. [深入剖析 GPU Early Z 优化](https://zhuanlan.zhihu.com/p/53092784)\n24. [【Ogre编程入门与进阶】第十章 Ogre场景管理](https://blog.csdn.net/zhanghua1816/article/details/8130251)\n\n"
  },
  {
    "path": "ComputerGraphics(OpenGL)/Part1_Light&ShadowInGame.md",
    "content": "# 一、光\n\n## 1. 颜色计算\n\n常用的计算光照颜色的方法：\n\n- 物体反射的颜色（我们感知到的颜色）：光源的颜色 * 物体的颜色\n- 多光源的情况下，一般都是将各个光源的颜色累加起来，最后得出最终的颜色\n- 同一个光源的光衰减系数是一样的\n\n\n\n屏幕上显示的物体颜色可以通过以下公式得出，其中：\n\n- $K_\\gamma$：显示屏幕的 gamma 矫正\n- $K_{Cam}$：相机的配置（曝光、白平衡）\n- $K_i$：当前光源的衰减\n- $I_i$：当前像素接受光源能量的比例\n- $\\Phi_i$：当前光源放射的总能量\n- $L_i$：当前光源的颜色\n\n$$\nColor_{屏幕} = K_\\gamma K_{Cam} \\sum_{i=0}^{L_n} K_i I_i \\Phi_i L_i\n$$\n\n\n\n## 2. 光的衰减（体积）\n\n**衰减** Attenuation\n\n随着光线传播距离的增长逐渐削减光的强度，光的衰减的模拟公式（其中 $K_c$、$K_l$、$K_q$ 的取值都是经验值）\n\n- $K_c$ 通常保持为 1.0，它的主要作用是保证分母永远不会比1小，否则的话在某些距离上它反而会增加强度，这肯定不是我们想要的效果\n- $K_l$ 与距离值相乘，以线性的方式减少强度\n- $K_q$ 与距离的平方相乘，让光源以二次递减的方式减少强度。二次项在距离比较小的时候影响会比一次项小很多，但当距离值比较大的时候它就会比一次项更大了\n\n**光体积** Light Volume\n\n光源能够达到片段的范围（通过光的衰减计算出<u>光的半径</u>），一般通过渲染光的衰减半径的球体来确定光源的影响范围\n根据衰减公式可知，衰减值只能无限接近于 0，因此需要通过选定一个衰减的最小值来限定光的范围\n一般 $L_{att} = {x \\over 256}$，除以 256 是因为默认的 8-bit 帧缓冲可以每个分\n$$\n\\begin{align}\nL_{att} &= {1.0 \\over K_c + K_l * d + K_q * d^2}\\\\\nK_q * d^2 + K_l * d + K_c &= {1.0 \\over L_{att}}\\\\\nK_q * d^2 + K_l * d + K_c - {1.0 \\over L_{att}} &= 0\\\\\nd &= {-K_l + \\sqrt{K_l^2 - 4*K_q*(K_c - {1.0 \\over L_{att}})} \\over 2 * K_q}\n\\end{align}\n$$\n\n实际的光的衰减的计算\n\n1. 通过一张 256 * 1 的纹理作为查找表\n   通过的**点到光源距离的平方** 来（为了避免开方操作）查找衰减值（Unity 内的光衰减纹理）\n2. 使用简化后的数学公式计算衰减（Unity 内使用的计算公式）\n\n$$\nL_{att} = {1.0 \\over 光源到着色物体的距离 d}\n$$\n\n\n\n## 3. 光源类型\n\n光源类型，即投光物(Light Caster)：将光**投射**(Cast)到物体的光源\n在渲染方程中常常称一个具体的光源类型为 **精确光源 punctual lights**  \n\n\n\n### 3.1 天光 Sky light\n\n天光 (单位：$cd/m^2$)：环境光，模拟场景中带有太阳或阴天的光照环境，不仅可以用于户外环境，也可以给室内环境带来环境光效果\n\n1. 使用固定的场景贴图：多用天空球网格贴图\n2. 实时从场景中捕捉生成 Cubemap（立方体图）来生成环境光\n\n\n\n### 3.2 自发光 Emissive light\n\n自发光表面 (单位：$cd/m^2$)：直接由光源发射的光照，自发光一般为材质的颜色\n\n- 实时渲染中自发光不会作为光源来照亮其他物体（不提供间接光照）\n- 天光可以看做自发光的一种\n\n\n\n### 3.3 平行光 Directional light\n\n![](./images/irradiance.png)\n\n平行光 (单位：$lux$)，又称定向光：光源处于无限远处，所有光线有相同的方向\n\n- 不考虑光源位置，只考虑光的方向\n- 表示用方向：平行光从光源发出的方向\n- 计算用方向：平行光从片段发出到光源的方向（与平行光的表示相反）\n\n\n\n**光照度**物理光照计算公式（其中，$\\Phi _e$ 表示光通量，为已知条件）\n$$\nE_e = {\\Phi_e \\over A\\cos \\theta}\n$$\n实际简化后的计算方式：\n使用 Phong 光照模型里的兰伯特漫反射模型<u>乘以固定的系数</u>来计算\n\n```glsl\nvec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir)\n{\n    vec3 lightDir = normalize(-light.direction);\n    \n    // diffuse shading\n    float diff = max(dot(normal, lightDir), 0.0);\n    \n    // specular shading\n    vec3 reflectDir = reflect(-lightDir, normal);\n    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);\n    \n    // combine results\n    vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));\n    vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));\n    vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));\n    \n    return (ambient + diffuse + specular);\n}\n\n```\n\n\n\n### 3.4 点光源 Point light\n\n![](./images/light_point.png)\n\n点光源 (单位：$cd$)：光源处于世界中某一个位置的光源，它会朝着所有方向发光，但光线会随着距离逐渐衰减\n\n- 计算用方向：平行光从片段发出到点光源的方向\n- 衰减系数：点光源的最终结果需要乘以一个衰减系数\n\n\n\n点光源环境下，[球坐标系](https://baike.baidu.com/item/球坐标系) $(r,\\theta,\\phi)$：\n实际输出的光通量 $\\Phi _e$ 与实际接收的光强度 $I_e$ 的转换比例为\n$$\n\\begin{align}\n\\Phi_e &=\n\\int _0^{2\\pi} \\int_0^{\\pi} sin\\theta \\space d\\theta d\\phi \\space I_e\n= 4\\pi \\space I_e \\\\\\\\\n1 \\space lm &=4\\pi \\space cd \\\\\n&\\approx  12.6 \\space cd\n\\end{align}\n$$\n\n```glsl\nvec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir)\n{\n    vec3 lightDir = normalize(light.position - fragPos);\n    \n    // diffuse shading\n    float diff = max(dot(normal, lightDir), 0.0);\n    \n    // specular shading, func reflect need inverse direction of input light\n    vec3 reflectDir = reflect(-lightDir, normal);\n    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);\n    \n    // combine results\n    vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));\n    vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));\n    vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));\n    \n    // attenuation\n    float distance = length(light.position - fragPos);\n    float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));   \n\n    return (ambient + diffuse + specular) * attenuation;\n}\n```\n\n\n\n### 3.5 聚光灯 Spot light\n\n![](./images/light_spotlight.png)\n\n聚光灯 (单位：$cd$)：只朝一个特定方向而不是所有方向照射光线，只有在聚光方向的特定半径内的物体才会被照亮，其它的物体都会保持黑暗\n\n实际简化后的计算方式：\n\n- LightDir：聚光照射到片元的方向\n\n- SpotDir：聚光的方向\n\n- $\\phi$ 切光角：聚光的照在物体上光圈的半径大小\n\n- $\\theta$ LightDir 和 SpotDir 之间的夹角\n\n\n\n**聚光的边缘软化**\n\n- 聚光边缘强度变化：需要一个内切光角和一个外切光角，通过从内到外切光角的过渡来表示聚光强度的变化\n\n- 强度计算公式，其中\n  $I$ 为聚光强度，范围是 [0, 1] \n  $\\theta$ 为 LightDir 和 SpotDir 之间的夹角\n  $\\phi$ 为外切光角，$\\gamma$ 为内切光角（$\\phi$、$\\gamma$ 一般作为聚光的属性，都是常数）\n\n  $I = {\\theta - \\phi \\over cos\\gamma - cos\\phi}$\n\n\n\n聚光灯环境下，[球坐标系](https://baike.baidu.com/item/球坐标系) $(r,\\theta,\\phi)$：\n实际输出的光通量 $\\Phi _e$ 与实际接收的光强度 $I_e$ 的转换比例为\n$$\n\\begin{align}\n\\Phi_e &=\n\\int _0^{2\\pi} \\int_0^x sin\\theta \\space d\\theta d\\phi \\space I_e\n= 2\\pi(1-cosx) \\space I_e , \\space x\\in[0,{\\pi \\over 2}]\\\\\\\\\n1 \\space lm &=2\\pi(1-cosx) \\space cd , \\space x\\in[0\\degree,90\\degree]\\\\\n&\\approx  6.3(1-cosx) \\space cd\n\\end{align}\n$$\n\n```glsl\nvec3 CalcSpotLight(SpotLight light, vec3 normal, vec3 fragPos, vec3 viewDir)\n{\n    vec3 lightDir = normalize(light.position - fragPos);\n    \n    // diffuse shading\n    float diff = max(dot(normal, lightDir), 0.0);\n    \n    // specular shading\n    vec3 reflectDir = reflect(-lightDir, normal);\n    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);\n    \n    // combine results\n    vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));\n    vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));\n    vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));\n    \n    // attenuation\n    float distance = length(light.position - fragPos);\n    float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));  \n\n    // spotlight intensity\n    float theta = dot(lightDir, normalize(-light.direction)); \n    float epsilon = light.cutOff - light.outerCutOff;\n    float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);\n    \n    return (ambient + diffuse + specular) * attenuation * intensity;\n}\n```\n\n\n\n### 3.6 面光源 Area light\n\n![](./images/light_area.png)\n\n面光源 (单位：$cd$)：由空间中的矩形限定，在所有方向上均匀地在其表面区域上发出光，但仅从矩形的一侧发出，类似于方正的聚光灯\n\n> 由于面光源会同时从几个不同的方向照亮对象，因此阴影比其他类型的光更柔和细微\n> 可用于创建逼真的路灯或靠近播放器的一排灯\n>\n> 小面积的光源可以模拟较小的光源（例如室内照明），比点光源具有更逼真的效果\n\n\n\n面光源环境下，[球坐标系](https://baike.baidu.com/item/球坐标系) $(r,\\theta,\\phi)$：\n实际输出的光通量 $\\Phi _e$ 与实际接收的光强度 $I_e$ 的转换比例为\n$$\n\\begin{align}\n\\Phi_e &=\n\\int _0^{2\\pi} \\int_0^{\\pi \\over 2} sin\\theta \\space d\\theta d\\phi \\space I_e\n= \\pi \\space I_e\\\\\\\\\n1 \\space lm &=\\pi \\space cd \\\\\n&\\approx 3.14 \\space cd\n\\end{align}\n$$\n\n\n\n### 3.7 光域光源 Photometric light\n\n![](./images/light_IES.png)\n\n光域光源：是一种用来描述光源辐射范围和强度的说明文件，可以模拟任何形状和强度的光源\n用来模拟一些由于发光物体形状迥异和自身遮挡原因的光源\n\nIlluminating Engineering Society (IES)：一种被广泛应用的用来描述光域光源的标准格式 `.ies`\n\n**实现方法**：一般会通过将描述光源辐射范围和强度的 [球坐标系](https://baike.baidu.com/item/球坐标系) $(r,\\theta,\\phi)$ 转化为笛卡尔坐标系，将 IES 文件转化为 (U, V) 分别为 $(\\theta, \\space \\cos \\phi)$ 的纹理\n\n```c\nfloat getIESProfileAttenuation ( float3 L, ShadowLightInfo light )\n{\n    // Sample direction into light space\n    float3 iesSampleDirection = mul ( light . worldToLight , -L);\n\n    // Cartesian to spherical\n    // Texture encoded with cos( phi ), scale from -1->1 to 0->1\n    float phiCoord = iesSampleDirection.z * 0.5f + 0.5f;\n    float theta = atan2(iesSampleDirection.y, iesSampleDirection.x);\n    float thetaCoord = theta * FB_INV_TWO_PI ;\n    float3 texCoord = float3(thetaCoord, phiCoord, light.lightIndex);\n    float iesProfileScale = iesTexture.SampleLevel(sampler, texCoord, 0).r;\n\n    return iesProfileScale;\n}\n\n// ...\n\natt *= getAngleAtt (L, lightForward , lightAngleScale , lightAngleOffset );\natt *= getIESProfileAttenuation (L, light );\n```\n\n\n\n\n\n# 二、光的反射模型\n\n> 材质反射模型是对 BRDF 光照模型进行简化和理想化后的经验模型\n\n**着色（shading）**：计算某个观察方向出射度的过程，期间需要材质属性、光源信息 和 一个等式（这个等式也称为光照模型）\n$$\n基础材质模型 = 环境光 f_a + 漫反射 f_d + 镜面反射 f_s\n$$\n\n\n![](./images/material.png)\n\n![](./images/material.jpg)\n\n\n\n## 1. 环境光 Ambient\n\n是一个全局光照，同一个场景中的所有物体都使用同样的环境光（一般为常量）\n\n**泛光模型**\n即只考虑环境光，这是最简单的**经验**模型，只会去考虑环境光的影响\n\n- $K_a$ 代表物体表面对环境光的反射率\n- $I_a$ 代表入射环境光的亮度\n\n$$\nI_{Env} = K_a I_a\n$$\n\n\n\n## 2. 漫反射 Diffuse\n\n物体表面随机散射后反射的光照\n\n### 2.1 Lambert 模型\n\n![](./images/light_diffuse.png)\n\n反射的光线强度 与 表面法线和光源方向夹角 的余弦值 成正比（物体背面的光照不会参与着色计算）\n\n- 计算方法\n  两个单位向量的点积  $\\hat n \\cdot I = |\\hat n||I| \\cos \\theta = \\cos \\theta$\n  反射的光线强度 和 **单位**表面法线和**单位**光源方向 的点积 成正比\n- 实际计算公式\n  max 函数防止出现表面法线 $n$ 和 光源方向 $I$ 夹角 $\\theta$ 大于 90 度的情况（即，光源被物体遮挡的情况）\n\n$$\nColor_{diff} = Color_{light} \\cdot Color_{材质强度} \\max(0, \\hat n \\cdot I)\n$$\n\n### 2.2 Half Lambert 模型\n\n基于 Lambert 模型（物体背面的光照会参与着色计算）\n\n- 计算方法\n  不通过限制余弦值的大小而是将余弦值的范围从 [-1, 1] 映射到 [0, 1]\n- 实际计算公式\n\n$$\nColor_{diff} = Color_{light} \\cdot Color_{材质强度} (0.5 + 0.5* \\hat n \\cdot I)\n$$\n\n\n\n## 3. 镜面反射 Specular\n\n### 3.1 Phong 模型\n\n> Phong 着色：使用 Phong 光照模型，在**片元着色器**逐像素的计算（使用的顶点法线在当前片面的插值）\n> Gouraud 着色：使用 Phong 光照模型，在**顶点着色器**逐顶点的计算（计算量相对较小）\n\nPhong 照模型只关心由光源发射，经过物体表面一次反射后**进入摄像机的光线**\n\n**镜面反射 Specular**：\n\n![](./images/light_reflect.png)\n\n\n\n**计算反射方向** $r$，已知法线 $\\hat n$ 是单位向量，$L$ 是入射光线 $I$ 到 $\\hat n$ 的投影\n$$\n\\begin{align}\n|\\hat n| &= 1 \\\\ \\\\\nr + I &= 2 L\\\\\n&= 2(|I|cos \\theta) \\\\\n&= 2(|\\hat n||I|cos \\theta) \\\\\n&= 2(\\hat n\\cdot I) \\\\\nr &= 2 (\\hat n \\cdot I) \\hat n - I\n\\end{align}\n$$\n\n**高光反射**，已知 观察方向 $\\hat v$ 是单位向量\n\n![](./images/light_specular.png)\n$$\nColor_{spec} = Color_{light} \\cdot Color_{高光强度} \\max(0, \\hat v \\cdot r)^{Gloss}\n$$\nGloss：光泽度，控制高光区域的亮点（光泽度越大，亮点越小）\nmax 函数防止出现 $v$ 和 $r$ 夹角 $\\theta$ 大于 90 度的情况（即，光源在摄像头后侧的情况）\n\n\n\n### 3.2 Blinn-Phong 模型\n\n**Phong 光照的缺点**：\n当物体的反光度非常小时，它产生的镜面高光半径足以让相反方向的光线对亮度产生足够大的影响。在这种情况下就不能忽略它们对镜面光分量的贡献了\n\n![](./images/light_over_90.png)\n\n\n\nBlinn-Phong 光照模型为了解决 Phong 光照的上述缺点，**计算高光强度的方法改为**计算 **半程向量** 与 法线向量 的夹角的方式\n\n![](./images/light_blinn.png)\n\n$$\n\\hat h = {I + \\hat v \\over |I + \\hat v|}\n$$\nBlinn-Phong 的高光强度\n$$\nColor_{spec} = Color_{light} \\cdot Color_{高光强度} \\max(0, \\hat h \\cdot \\hat n)^{Gloss}\n$$\n\n\nBlinn-Phong 较 Phong 具有更真实的光照效果\n\n![](./images/light_comparrison.png)\n\n![](./images/light_comparrison2.png)\n\n\n\n**代码实现**\n\n\n```glsl\n// VS\n#version 330 core\nlayout (location = 0) in vec3 aPos;\nlayout (location = 1) in vec3 aNormal;\nlayout (location = 2) in vec2 aTexCoords;\n\nout vec3 FragPos;\nout vec3 Normal;\nout vec2 TexCoords;\n\nuniform mat4 model;\nuniform mat4 view;\nuniform mat4 projection;\n\nvoid main() {\n    // Word coordinate\n    FragPos = vec3(model * vec4(aPos, 1.0));\n    \n    // mode 4D to 3D for remove translate\n    Normal = mat3(transpose(inverse(model))) * aNormal;  \n    TexCoords = aTexCoords;\n    \n    gl_Position = projection * view * vec4(FragPos, 1.0);\n}\n\n// FS\nvec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir)\n{\n    // lightDir from frag to light\n    vec3 lightDir = normalize(light.position - fragPos);\n    \n    // diffuse shading\n    float diff = max(dot(normal, lightDir), 0.0);\n    \n    // specular shading\n    vec3 halfwayDir = normalize(viewDir + lightDir);\n    float spec = pow(max(dot(viewDir, halfwayDir), 0.0), material.shininess);\n    \n    // combine results\n    vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));\n    vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));\n    vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));\n    \n    // attenuation\n    float distance = length(light.position - fragPos);\n    float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance)); \n\n    return (ambient + diffuse + specular) * attenuation;\n}\n```\n\n\n\n## 4. 微平面模型 Microfacet\n\n现实当中大多数物体的表面都会有非常微小的缺陷：微小的凹槽，裂缝，几乎肉眼不可见的凸起，以及在正常情况下过于细小以至于难以使用 Normal map 去表现的细节。尽管这些微观的细节几乎是肉眼观察不到的，但是他们仍然影响着光的扩散和反射\n\n- 平面越粗糙，这个平面上的微平面的排列就越混乱。当我们特指镜面光/镜面反射时，入射光线更趋向于向完全不同的方向发散 (Scatter) 开来\n- 平面越光滑，光线大体上会更趋向于向同一个方向反射，造成更小更锐利的反射\n\n![](./images/microfacets2.png)\n\n### 4.1 Oren Nayarh 模型\n\nLambert 模型由于是理想环境下的光照模拟，不能正确体现物体微表面（特别是粗糙物体）的光照效果\nOren Nayarh 模型考虑到微小平面之间的 相互遮挡 和 互相反射照明，主要对粗糙表面的物体建模，比如石膏、沙石、陶瓷等\n\n![](./images/oren_nayar.jpg)\n\n\n\n### 4.2 GGX 模型\n\nGGX 模型所解决的问题是，如何将微平面反射模型推广到表面粗糙的**半透明**材质，从而能够模拟类似于毛玻璃的粗糙表面的**透射**效果，它也提出了一种新的描述微平面**法线**方向分布的函数\n\n![](./images/GGX.png)\n\n\n\n## 5. 菲涅尔反射 Fresnel\n\n**菲涅耳反射** Fresnel reflection（反射的光线所占折射和反射的比率）\n观察方向和物体表面法线的夹角越大，反射效果越明显\n\n模拟物理效果的近似公式，其中 $v$ 表示视角方向的**单位向量**，$n$ 表示物体表面**单位法线**\n\n![](./images/light_fresenel_reflection.png)\n\n### 5.1 Schlick 模型\n\nSchlick 模型简化了 Phong 模型的镜面反射中的指数运算，它模拟的高光反射效果跟 pow 运算基本一致，且效率比 pow 运算高，采用以下公式替代\n其中，$n_1$ 表示入射光线介质的折射率，$n_2$ 表示折射光线介质的折射率\n$$\n\\begin{align}\nF_0 &= ({n_1 - n_2 \\over n_1 + n_2})^2\\\\\nF   &= F_0+(1−F_o)(1−v \\cdot n)^5 \\\\\n\\end{align}\n$$\n\n实时渲染中，常会用一些 经验公式 Empircial Formular 来代替，其中 bias，scale，power 是控制项\n$$\nF(v,n) = max(0,min(1, bias + scale * (1- v \\cdot n)^{power}))\n$$\n\n\n\n\n\n\n# 三、 阴影\n\n## 1. 阴影效果分析\n\n**阴影具有近实（边缘锐利清晰），远虚（边缘模糊）的效果**\n\n根据被遮挡程度，阴影的类型可分为：\n\n1. lit 照亮：没有被遮挡\n2. umbra 本影区：完全被遮挡\n3. penumbra 半影区：部分被遮挡\n\n![](./images/shadow_map.png)\n\n\n\n## 2. 阴影映射 Shadow Mapping\n\n注意：\n\n- 由于阴影数据的精度问题，光源距离物体越远效果越好\n- 点光源的阴影（透视投影）需要更高的精度和更小的竖直方向的视角\n- 法线最好采用法线贴图，顶点法线生成的阴影在一些特殊视角会有阴影形变问题\n\n\n\n整体思路\n\n![](./images/shadow_map2.png)\n\n\n\n方法：\n\n1. **渲染深度贴图（阴影贴图）**\n   以光的位置为视角进行渲染，我们能看到的东西都将被点亮，看不见的是阴影\n   以光源的类型选择 正交投影 或者 透视投影\n   \n   ```c\n   // 存储的是实际 Z 的深度值，没有标准化（这个时候的 Z 无法确定输入范围）\n   GLuint depthMap;\n   glGenTextures(1, &depthMap);\n   glBindTexture(GL_TEXTURE_2D, depthMap);\n   glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, \n                SHADOW_WIDTH, SHADOW_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);\n   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);\n   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);\n   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); \n   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);\n   ```\n   \n2. **深度贴图纹理坐标计算**\n   世界空间坐标 -> 光源空间坐标 -> 裁切空间的标准化设备坐标-> 根据深度贴图和坐标求出阴影深度值\n\n3. 计算片段是否在阴影之中：若当前坐标的 Z 值比深度贴图的值大，则物体在阴影后面，物体有阴影\n\n   ```c\n   // shadow 只能为 0 或 1\n   // 阴影中只有环境光，没有高光反射和漫反射\n   vec3 lighting = (ambient + (1.0 - shadow) * (diffuse + specular)) * color;\n\t```\n\n   \n\n重点：\n\n1. 不使用颜色缓冲，不包含颜色缓冲的帧缓冲是不完整的，因此只能禁止颜色缓冲\n   并且在片源着色器里什么都不干\n   \n   ```c\n   glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);\n   glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthMap, 0);\n   glDrawBuffer(GL_NONE);\n   glReadBuffer(GL_NONE);\n   glBindFramebuffer(GL_FRAMEBUFFER, 0);\n   ```\n   \n2. 获取阴影贴图的值为透视投影下的非线性深度值\n\n   **解决方案**：将非线性深度值通过透视投影的逆变换转换为线性深度，[投影矩阵](../LinearAlgebra/Part1_Matrix.md)\n   $$\n   \\begin{align}\n   Z_n &= {{far + near} \\over {far - near}} +{2 \\cdot far \\cdot near \\over {far - near}}{1 \\over Z_{linear}} \\\\\n   (far - near)Z_n &= (far + near) + 2 \\cdot far \\cdot near {1 \\over Z_{linear}} \\\\\n   {(far - near)Z_n - (far + near) \\over 2 \\cdot far \\cdot near} &= {1 \\over Z_{linear}} \\\\\n   Z_{linear} &= {2 \\cdot far \\cdot near \\over (far - near)Z_n - (far + near)}\n   \\end{align}\n   $$\n   \n3. 在**距离光源比较远**时，多个片段会从深度贴图的同一个值中采样\n   当光以一定角度朝向物体表面时，物体表面会产生明显的线条样式\n\n   ![](./images/shadow_line.png)\n\n   **解决方案**：阴影偏移（shadow bias）+ 深度纹理的线性采样 + 精度修正\n   根据对阴影贴图应用一个**随 物体表面朝向和光线的角度**变化的偏移量\n\n   ```c\n   // 1. 阴影偏移\n   float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);\n   float shadow = currentDepth - bias > closestDepth  ? 1.0 : 0.0;\n   \n   // 2. 精度问题 \n   // 2.1 精度打包\n   vec4 bitShift = vec4(1.0, 256.0, 256.0 * 256.0, 256.0 * 256.0 * 256.0);\n   const vec4 bitMask = vec4(1.0/256.0, 1.0/256.0, 1.0/256.0, 0.0);\n   vec4 rgbaDepth = fract(gl_FragCoord.z * bitShift);\n   rgbaDepth -= rgbaDepth.gbaa * bitMask;\n   \n   // 2.2 精度解包\n   \n   ```\n   \n   ![](./images/shadow_acne_bias.png)\n   \n   这样会带来一个问题 —— 悬浮\n   \n   ![](./images/shadow_peter_panning.png)\n   \n   解决悬浮的一种方法：通过在生成阴影深度贴图时采用正面剔除的方式，只保留实体物体背面阴影深度，这样阴影的深度更真实，由于偏移出现的部分多余的阴影也会由于阴影深度的更精确而消失，但是地板的深度会去掉\n   \n   \n   \n4. 阴影贴图有一定的范围，无法覆盖所有场景\n\n   ![](./images/shadow_texture_scope.png)\n\n   **解决方案**：让阴影贴图范围外的没有阴影\n\n   1. <u>采样位置超出深度贴图边缘</u>\n      将阴影贴图的纹理环绕选项设置为 `GL_CLAMP_TO_BORDER`，给边框设一个较亮的白色（最大深度 1）\n\n   2. <u>深度 Z 的范围超过远平面的裁剪范围 -1.0 ～ 1.0</u>\n      首先在片源着色器里判断深度值是否超出 1.0，如果超出，强制设置为无阴影\n\n      \n\n5. 阴影贴图受限于分辨率，画出的阴影有锯齿感\n\n   ![](./images/shadow_soft.png)\n\n   **解决方案**：PCF（percentage-closer filtering）\n   计算阴影时，多次进行深度图的采样计算，给做一次 BoxBlur 均值滤波，来模糊阴影边缘的锯齿\n\n\n\n## 3. 阴影类型\n\n### 3.1 效率最高的阴影（PPS）\n\n**平面投影阴影** Planar Projected Shadows\n\nTODO: https://zhuanlan.zhihu.com/p/31504088\n\n\n\n### 3.2 近实远虚的阴影（PCSS）\n\n#### 3.2.1 Percentage-Closer Soft Shadows\n\nPCF 由于采样区域是固定大小的，因此会在所有地方展示同样形状的软阴影。\n为了做到**近实远虚**的效果，我们需要一个系数来控制 PCF 的步长，让近处 PCF步长短（清晰），远处 PCF 步长长（模糊）\n\n![](./images/shadow_PCSS.png)\n\n\n$$\n\\begin{align}\n{W_{Penumbra} \\over W_{Light}} &= {{(d_{Receiver} - d_{Blocker})} \\over W_{Blocker}} \\\\\nW_{Penumbra} &= {{(d_{Receiver} - d_{Blocker})}  W_{Light} \\over W_{Blocker}}\n\\end{align}\n$$\n\n在计算平均深度时，可以使用 mipmap 来加速平均深度的计算，通过减少采样次数的方式来提高效率\n\n```c++\n#define BIAS \t\t5e-5\n#define nSamples \t8\n\nfloat findAVGBlocker(const vec3& coords, const float& bias)\n{\n    int blockerCount = 0;\n    float totalDepth = 0;\n    for (int i = 0; i < nSamples - 2; ++i) {\n        vec2 uv = vec2(coords.x, coords.y) + u_offsets[i]；\n        float shadowMapDepth = sample2D(texDepth, uv);\n        if (coord.z > (bias + shadowMapDepth)) {\n            totalDepth += shadowMapDepth;\n            blockCount += 1;\n        }\n    }\n    \n    if (0 == blockCount) {\n        return -1.0f;\n    } else if (nSamples - 2 == blockCount) {\n        return 2.0f;\n    } else {\n        return totalDepth / float(blockCount);\n    }\n}\n\nfloat PercentageCloserSoftShadows(\n    const vec3& coords, \n    const vec3& normal, \n    const vec3& lightDir\n)\n{\n    float bias = MAX(BIAS, BIAS * (1.0f - nomral.dot(lightDir)));\n    \n    // 1. avg blocker depth\n    float zBlocker = findAVGBlocker(coords, bias);\n    if (zBlocker > EPS) {\n        return 1.0f;\n    } else if (zBlocker > 1.0f + EPS) {\n        return 0.0f;\n    }\n    \n    // 2. penumbra size\n    float penumbraScale = (coord.z - zBlocker) / zBlocker;\n    \n    // 3. filtering\n    float sum = 0.0f;\n    for (int i = 0; i < nSamples; ++i) {\n        vec2 uv = vec2(coord.x, coord.y) + u_offsets[i] * penumbraScale;\n        sum += (coord.z > sample2D(texDepth, uv) ? 0.0f : 1.0f);\n    }\n    \n    return sum / nSamples;\n}\n```\n\n另外还有通过影子都是水平的这个假设 + 概率方差的方式来给 PCSS 计算加速的 Variance Shadow Maps（VSM），以及修正 VSM 漏光问题的 Moment Shadow Mapping（MSM）方法，由于使用场景特定且实现方式复杂等问题，这里不再详述，具体可以看 [实时渲染｜Shadow Map：PCF、PCSS、VSM、MSM - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/369710758)\n\n\n\n### 3.3 大场景的阴影（CSM）\n\n**阴影贴图**方法对于**大型场景**渲染显得力不从心，很容易出现**阴影抖动**和**锯齿边缘**现象\n\n**级联式纹理映射** Cascaded Shadow Maps(CSM) 方法根据**对象**到**观察者**的距离提供**不同分辨率**的**深度纹理**来解决上述问题\n\n1. 将**相机**的**视锥体**分割成若干部分，然后为分割的每一部分生成**独立**的**深度贴图**\n2. 根据物体在场景中的位置对位置附近的两张深度贴图进行采样，根据 深度 距离来对两个采样进行线性插值\n\n\n\n### 3.4 点光源阴影 Point Shadows\n\n点光阴影，过去的名字是万向阴影贴图（omnidirectional shadow maps）技术\n\n方法：\n\n1. 渲染深度**立方体**贴图\n   将立方体贴图 GL_TEXTURE_CUBE_MAP 绑定到 FBO 上，通过几何着色器，一次绘制 6 个面的贴图\n   顶点着色器：将顶点变换到世界空间\n   几何着色器：将所有世界空间的顶点变换到 6 个不同的光空间（输入：一个三角形的 3 个顶点）\n\n   ```c\n   // 几何着色器\n   #version 330 core\n   layout (triangles) in;\n   layout (triangle_strip, max_vertices=18) out;\n   \n   uniform mat4 shadowMatrices[6];\n   out vec4 FragPos; // FragPos from GS (output per emitvertex)\n   \n   void main() {\n       for(int face = 0; face < 6; ++face) {\n           gl_Layer = face; // built-in variable that specifies to which face we render.\n           for(int i = 0; i < 3; ++i) { // for each triangle's vertices\n               FragPos = gl_in[i].gl_Position;\n               gl_Position = shadowMatrices[face] * FragPos;\n               EmitVertex();\n           }    \n           EndPrimitive();\n       }\n   }\n   \n   // 片源着色器\n   #version 330 core\n   in vec4 FragPos;\n   \n   uniform vec3 lightPos;\n   uniform float far_plane;\n   \n   void main() {\n       // get distance between fragment and light source\n       float lightDistance = length(FragPos.xyz - lightPos);\n   \n       // map to [0;1] range by dividing by far_plane\n       lightDistance = lightDistance / far_plane;\n   \n       // write this as modified depth\n       gl_FragDepth = lightDistance;\n   }\n   ```\n\n   \n\n2. 渲染场景\n   为了确保 6 个面的深度贴图边缘都对齐，设置透视投影的视角为 90 度\n\n   ```c\n   float ShadowCalculation(vec3 fragPos) {\n       // Get vector between fragment position and light position\n       vec3 fragToLight = fragPos - lightPos;\n       // Use the fragment to light vector to sample from the depth map    \n       float closestDepth = texture(depthMap, fragToLight).r;\n       // It is currently in linear range between [0,1]. \n       // Let's re-transform it back to original depth value\n       closestDepth *= far_plane;\n       // Now get current linear depth as the length between the fragment and light position\n       float currentDepth = length(fragToLight);\n       // Now test for shadows\n       float bias = 0.05; \n       // We use a much larger bias since depth is now in [near_plane, far_plane] range\n       float shadow = currentDepth -  bias > closestDepth ? 1.0 : 0.0;\n   \n       return shadow;\n   }\n   ```\n\n\n![](./images/shadow_point.png)\n\n\n\n### 3.5 透明物体的阴影\n\n\n\n\n\n### 3.6 环境光遮蔽 SSAO\n\n屏幕空间的环境光遮挡 （Screen Space Ambient Occlusion，SSAO）通过将褶皱、孔洞和非常靠近的墙面变暗的方法近似模拟出间接光照（常用来模拟大面积的光源对整个场景的光照 如，下图）\n\n![](./images/light_ssao.png)\n\n方法：在三维物体已经生成二维图片之后计算遮蔽因子\n\n1. 几何阶段：准备输入数据\n   **1.1 渲染当前相机范围的 顶点、法线、线性深度 到观察空间下的 G-Buffer（Geometry Buffer）**\n   注意：纹理采样使用 `GL_CLAMP_TO_EDGE` 方法，防止采样到在屏幕空间中纹理默认坐标区域之外的深度值\n   \n   ```c\n   // 几何着色器 VS\n   #version 330 core\n   layout (location = 0) in vec3 position;\n   layout (location = 1) in vec3 normal;\n   layout (location = 2) in vec2 texCoords;\n   \n   out vec3 FragPos;\n   out vec2 TexCoords;\n   out vec3 Normal;\n   \n   uniform mat4 model;\n   uniform mat4 view;\n   uniform mat4 projection;\n   \n   void main() {\n       vec4 viewPos = view * model * vec4(position, 1.0f);\n       FragPos = viewPos.xyz; // 观察空间\n       gl_Position = projection * viewPos;\n       TexCoords = texCoords;\n       \n       mat3 normalMatrix = transpose(inverse(mat3(view * model)));\n       Normal = normalMatrix * normal; // 观察空间 -> 切线空间\n   }\n   \n   // 几何着色器 FS\n   #version 330 core\n   layout (location = 0) out vec4 gPositionDepth;\n   layout (location = 1) out vec3 gNormal;\n   layout (location = 2) out vec4 gAlbedoSpec;\n   \n   in vec2 TexCoords;\n   in vec3 FragPos;\n   in vec3 Normal;\n   \n   const float NEAR = 0.1; // 投影矩阵的近平面\n   const float FAR = 50.0f; // 投影矩阵的远平面\n   float LinearizeDepth(float depth) {\n       float z = depth * 2.0 - 1.0; // 回到NDC\n       return (2.0 * NEAR * FAR) / (FAR + NEAR - z * (FAR - NEAR));    \n   }\n   \n   void main() {    \n       // 1. 储存片段的位置矢量到第一个 G 缓冲纹理\n       gPositionDepth.xyz = FragPos;\n       // 2. 储存线性深度到 gPositionDepth 的 alpha 分量\n       gPositionDepth.a = LinearizeDepth(gl_FragCoord.z); \n       // 3. 储存法线信息到 G 缓冲\n       gNormal = normalize(Normal);\n       // 4. 储存漫反射颜色\n       gAlbedoSpec.rgb = vec3(0.95);\n   }\n   ```\n   \n   **1.2 计算法向半球采样在切线空间的位置**\n   在**切线空间**内，距离**每个片源**半球形范围内随机取固定数量的采样坐标，一般会将采样点靠近分布\n   \n      ```c\n   // 在应用程序初始化中调用\n   // 随机浮点数，范围0.0 - 1.0\n   std::uniform_real_distribution<GLfloat> randomFloats(0.0, 1.0);\n   std::default_random_engine generator;\n   std::vector<glm::vec3> ssaoKernel;\n   \n   GLfloat lerp(GLfloat a, GLfloat b, GLfloat f) {\n     return a + f * (b - a);\n   }\n   \n   for (GLuint i = 0; i < 64; ++i) {\n     // 半球采样点 x，y ~ [-1, 1], z ~ [0, 1]\n     glm::vec3 sample(\n       randomFloats(generator) * 2.0 - 1.0, \n       randomFloats(generator) * 2.0 - 1.0, \n       randomFloats(generator)\n     );\n     sample = glm::normalize(sample);\n     sample *= randomFloats(generator);\n     GLfloat scale = GLfloat(i) / 64.0;\n     // 将更多的注意放在靠近真正片段的遮蔽上，也就是将核心样本靠近原点分布\n     scale = lerp(0.1f, 1.0f, scale * scale);\n     ssaoKernel.push_back(sample * scale);  \n   }\n      ```\n   \n   **1.3 创建随机核心旋转噪声纹理**\n   半球内采样位置会被所有片源共享使用，需要通过随机转动来确保在较低采样数量的情况下有较好的采样效果\n   由于，对场景中每一个片段创建一个随机旋转向量，会占用大量内存\n   因此，创建一个小的随机旋转向量纹理（4X4）<u>像瓷砖一样反复平铺</u>在屏幕上\n   \n      ```c\n   // 纹理生成\n   std::vector<glm::vec3> ssaoNoise;\n   for (GLuint i = 0; i < 16; i++) {\n     glm::vec3 noise(\n       randomFloats(generator) * 2.0 - 1.0, \n       randomFloats(generator) * 2.0 - 1.0, \n       0.0f); // 围绕 Z 轴偏移旋转，因此 Z 轴不需要有任何变化\n     ssaoNoise.push_back(noise);\n   }\n      ```\n   \n2. 光照处理阶段：计算遮蔽因子\n\n   **2.1 SSAO 阶段**\n   SSAO  着色器在 2D 的铺屏四边形上运行，它对于每一个生成的片段计算遮蔽值（为了在最终的光照着色器中使用）。由于环境遮蔽的结果是一个灰度值，只需要纹理的红色分量，所以将颜色缓冲的内部格式设置为 `GL_RED`\n\n   ```c\n   #version 330 core\n   out float FragColor;\n   in vec2 TexCoords;\n   \n   uniform sampler2D gPositionDepth;\n   uniform sampler2D gNormal;\n   uniform sampler2D texNoise;\n   \n   uniform vec3 samples[64];\n   uniform mat4 projection;\n   \n   // 最好设置为 uniform\n   int kernelSize = 64;\n   float radius = 1.0;\n   \n   void main() {\n       // Get input for SSAO algorithm\n       vec3 fragPos = texture(gPositionDepth, TexCoords).xyz;\n       vec3 normal = texture(gNormal, TexCoords).rgb;\n     \n     \t// 为了将 [0,1] 的屏幕纹理坐标转化为平铺的噪声纹理坐标 [0,1]\n   \t\t// 1. 获取随机旋转向量这里需要一个缩放值\n   \t\tconst vec2 noiseScale = vec2(800.0f/4.0f, 600.0f/4.0f); // 屏幕 = 800x600\n       vec3 randomVec = texture(texNoise, TexCoords * noiseScale).xyz;\n     \n       // 2. 根据随机旋转向量创建正交坐标\n       vec3 tangent = normalize(randomVec - normal * dot(randomVec, normal));\n       vec3 bitangent = cross(normal, tangent);\n       mat3 TBN = mat3(tangent, bitangent, normal);\n     \n       // 3. 根据每个片源共用的半球体内采样数量计算遮蔽因子\n       float occlusion = 0.0;\n       for(int i = 0; i < kernelSize; ++i)\n       {\n           // 3.1 获取半球体内每个采样点位置（切线空间内）\n           vec3 sample = TBN * samples[i];     // 切线 -> 观察空间\n           sample = fragPos + sample * radius; // 根据偏移步长和方向，计算偏移后的采样点\n           \n           // 3.2 将观察空间的采样点投影到屏幕上\n           vec4 offset = projection * vec4(sample, 1.0); // from view to clip-space\n           offset.xyz /= offset.w; \t\t\t\t\t\t\t\t\t\t\t// perspective divide\n           offset.xyz = offset.xyz * 0.5 + 0.5;          // transform to range 0.0 - 1.0\n           \n           // 3.3 获取采样点对应周围采样的深度值\n           float sampleDepth = -texture(gPositionDepth, offset.xy).w;\n           \n           // 3.4 检测周围采样的深度如果在法向半球采样半径内，则被保留\n           // 从而避免：当检测一个靠近表面边缘的片段时，它将会考虑测试表面之下的表面的深度值\n           float rangeCheck = smoothstep(0.0, 1.0, radius / abs(fragPos.z - sampleDepth ));\n         \n           // 3.5 若周围采样的深度比当前观察深度大，则累加遮蔽因子的值\n           occlusion += (sampleDepth >= sample.z ? 1.0 : 0.0) * rangeCheck;           \n       }\n     \n       // 4. 均值化遮蔽因子\n       occlusion = 1.0 - (occlusion / kernelSize);\n       \n       FragColor = occlusion;\n   }\n   ```\n\n   **2.2 模糊环境遮蔽结果**\n   重复的噪声纹理再上一步的图中清晰可见，为了创建一个光滑的环境遮蔽结果，需要用 box bluer 来模糊环境遮蔽纹理\n\n   ```c\n   // 需要额外创建 FBO 在存储这种后处理效果\n   #version 330 core\n   in vec2 TexCoords;\n   \n   out float fragColor;\n   \n   uniform sampler2D ssaoInput;\n   const int blurSize = 4; // use size of noise texture (4x4)\n   \n   void main() {\n      vec2 texelSize = 1.0 / vec2(textureSize(ssaoInput, 0));\n      float result = 0.0;\n      for (int x = 0; x < blurSize; ++x) {\n         for (int y = 0; y < blurSize; ++y) {\n            vec2 offset = (vec2(-2.0) + vec2(float(x), float(y))) * texelSize;\n            result += texture(ssaoInput, TexCoords + offset).r;\n         }\n      }\n    \n      fragColor = result / float(blurSize * blurSize);\n   }\n   ```\n\n   **2.3 应用遮蔽因子在光照计算中**\n   光照模型中的环境光 = 原来的环境光常量 * 遮蔽因子（环境遮蔽纹理中）\n\n   ```c\n   #version 330 core\n   out vec4 FragColor;\n   in vec2 TexCoords;\n   \n   uniform sampler2D gPositionDepth;\n   uniform sampler2D gNormal;\n   uniform sampler2D gAlbedo;\n   uniform sampler2D ssao;\n   \n   struct Light {\n       vec3 Position;\n       vec3 Color;\n   \n       float Linear;\n       float Quadratic;\n       float Radius;\n   };\n   uniform Light light;\n   \n   void main() {             \n       // 从 G 缓冲中提取数据\n       vec3 FragPos = texture(gPositionDepth, TexCoords).rgb;\n       vec3 Normal = texture(gNormal, TexCoords).rgb;\n       vec3 Diffuse = texture(gAlbedo, TexCoords).rgb;\n   \t  // BoxBlur 后的遮蔽因子\n       float AmbientOcclusion = texture(ssao, TexCoords).r;\n   \n       // Blinn-Phong (观察空间中)\n       vec3 ambient = vec3(0.3 * AmbientOcclusion); // 这里我们加上遮蔽因子\n       vec3 lighting = ambient; \n       vec3 viewDir  = normalize(-FragPos); // Viewpos 为 (0.0.0)，在观察空间中\n       // 漫反射\n       vec3 lightDir = normalize(light.Position - FragPos);\n       vec3 diffuse = max(dot(Normal, lightDir), 0.0) * Diffuse * light.Color;\n       // 镜面\n       vec3 halfwayDir = normalize(lightDir + viewDir);  \n       float spec = pow(max(dot(Normal, halfwayDir), 0.0), 8.0);\n       vec3 specular = light.Color * spec;\n       // 衰减\n       float dist = length(light.Position - FragPos);\n       float attenuation = 1.0 / (1.0 + light.Linear * dist + light.Quadratic * dist * dist);\n       diffuse  *= attenuation;\n       specular *= attenuation;\n       lighting += diffuse + specular;\n   \n       FragColor = vec4(lighting, 1.0);\n   }\n   ```\n\n\n\n\n\n# 四、游戏中的可移动性\n\n通过对游戏中可移动性的分类，可以对游戏部分数据进行预先计算以提高游戏 Runtime 效率\n从游戏 Runtime 的可移动性上，光源/阴影可以分为：\n\n1. 静态 **Static**：不可移动，属性值（颜色、强度） **不变**\n   通过离线烘焙（预计算）的方式来实现，直接使用已经计算好的贴图，运行时无需额外的计算\n   例：Lightmap 的使用\n2. 静止 **Stationary**：不可移动，属性值（颜色、强度） **可变**\n   固定光源使用有范围的区域阴影\n3. 可移动 **Movable**：可移动，属性值（颜色、强度） **可变**\n   需要配置当前场景对应的阴影偏差，以便于实时计算\n\n\n\n\n\n# 五、光照渲染路径 Rendering Path\n\n渲染路径：**决定光照**如何应用到 shader 中，是当前渲染目标使用光照的流程\n\n|                  | 前向渲染 Forward                     | 延迟渲染 Deferred                                       |\n| ---------------- | ------------------------------------ | ------------------------------------------------------- |\n| **场景复杂度**   | 简单                                 | 复杂                                                    |\n| **光源支持数量** | 少量光源                             | 多光源                                                  |\n| **时间复杂度**   | O(g⋅f⋅l)                             | O(f⋅l)                                                  |\n| **显存和带宽**   | 较低                                 | 较高                                                    |\n| **硬件要求**     | 低，几乎覆盖100%的硬件设备           | 较高，需 MRT 的支持，需要Shader Model 3.0+              |\n| **后处理**       | 无法支持需要法线和深度等信息的后处理 | 支持需要法线和深度等信息的后处理，如 SSAO、SSR、SSGI 等 |\n| **画质**         | 清晰，抗锯齿效果好                   | 模糊，抗锯齿效果打折扣                                  |\n| **屏幕分辨率**   | 低                                   | 高                                                      |\n\n<u>延迟渲染 比 前向渲染 模糊</u>：因为经历了两次光栅化（几何通道和光照通道），相当于执行了两次离散化，造成了两次信号的丢失，对后续的重建造成影响，以致最终画面的方差扩大\n\n\n\n## 1. 前向渲染 Forward\n\n前向渲染路径（Unity 默认渲染路径）\n\n- 优点：实现简单；带宽消耗低；MSAA 和透明渲染支持较好\n- 缺点：对大规模的光源支持不好；对需要深度和法线的后期处理算法支持不好\n\n方法：对场景中的每个物体分别进行着色，在 VS 或 FS 对 每个光源逐个进行计算（世界坐标系）并累加到 frame buffer\n优化：有些作用程度特别小的光源可以不进行考虑（Unity 中只考虑重要程度最大的前 4 个光源）\n\n```c\n// 复杂度 O(Objects * Pixels * lights)\nvoid RenderForword() {\n    // 1. 遍历场景中的每个物体\n    // Unity3D 4.X 版本中，根据光照对物体的距离采用不同程度的计算\n    // 根据距离由近到远，采用的光照计算方式在每个光源中均进行：逐像素计、顶点、求调和函数计算 Spherical Harmonic\n    for each(object in ObjectsInScene)\n\t{\n        // 2. 遍历像素(在一个 RT 中的像素) [PS]\n        for each(pixel in RenderTargetPixels) {\n            color = 0;    \n            // 3. 遍历所有灯光，将每个灯光的光照计算结果累加到颜色中\n            for each(light in Lights) {\n                color += CalculateLightColor(light, pixelData);\n            }\n        }\n        WriteColorToRenderTarget(color);\n    } // ObjectsInScene\n}\n```\n\n\n\n### 1.1 Forward+\n\n前向渲染增强，又称 *Tiled Forward Rendering*\n\n- 优点：带宽消耗低；MSAA 和透明渲染支持较好；**支持大规模光源**\n- 缺点：需要一个 Pre-Z Pass，一个场景要渲染两遍；对需要法线、深度的后处理支持不好\n\n![](./images/RenderingDeferredTiledBased.png)\n\n方法：\n1. <u>PrePass (Depth Only Pass / Early-Z Pass)</u>，用来渲染不透明物体的深度，只写入深度不写入颜色\n2. <u>Light Culling Pass</u>，把 Z-Buffer 划分成许多四边形（大小一般是 2 的 N 次方，且长宽不一定相等，称为 Tile）\n   根据屏幕划分的范围和深度信息构成 Tile 内场景的包围盒，根据 Tile 包围和光的包围盒求交来剔除无用的光源\n3. <u>Shading Pass</u>，每个 Tile 根据各自拥有的 Light list 来计算光照\n\n\n\n### 1.2 Cluster Forward\n\n分簇前向渲染 *Cluster Forward Rendering*\n\n方法：和 Tiled Forward 一致，只是在划分 Tile 的场景包围盒上更加精确（屏幕空间 + 深度），进而更精准地裁剪光源，避免深度不连续时的光源裁剪效率降低\n主要的细分 tile 场景包围盒子的方法有隐式 Implicit 分簇法（绿色），显式 Explicit 分簇法（蓝色）\n以下为 Tiled Forward 的划分方式（红色）与 Clustered 的对比图\n\n![](./images/RenderingClusteredDeferred.png)\n\n优化：*Volume Tiled Forward Rendering*，不使用深度（少了一个 Pass），根据相机投影信息和高度 H，计算出 Tile 的深度从而构成 Volume Tile 和光源求交。相对 Cluster Forward 更粗略但是能快速计算出当前 Tile 的光源影响列表\n\n\n\n\n\n## 2. 延迟渲染 Deferred\n\n延迟渲染 *Deferred Rendering*\n\n- 优点：支持大规模的光源；针对需要深度、法线的后处理算法友好\n- 缺点：带宽、显存消耗大；MSAA 和透明渲染支持不好；需要 MRT 硬件支持\n\n![](./images/RenderingDeferred.png)\n\n方法：将光照处理这一步放在三维物体已经生成二维图片之后进行处理（屏幕坐标系）\n\n1. <u>几何 Pass</u>：渲染所有 几何/颜色 到 G-Buffer（Geometry Buffer）\n   G-Buffer：用来存储每个像素对应的 Position，Normal，Diffuse Color 和其他 Material parameters（所有变量都在**世界坐标系**下，同一个场景会渲染多次产生多个 Render Target）\n\n   ```c\n   // 复杂度 O(Objects * Pixels)\n   void RenderGeometryPass() {\n       SetupGBuffer(); // 设置几何数据缓冲区\n       \n       // 1. 遍历场景(非半透明物体)\n       for each(Object in OpaqueAndMaskedObjectsInScene) {\n           SetUnlitMaterial(Object);    // 设置无光照的材质\n           // 2. 渲染 Object 的几何信息到 GBuffer[VS + PS]\n           DrawObjectToGBuffer(Object);\n       }\n   }\n   ```\n   \n2. <u>光照 Pass</u>：使用 G-buffer 计算场景的光照渲染\n   每个 light 需要画一个 light volume，以决定它会影响到哪些 pixel\n   \n   ```c\n   // 复杂度 O(Pixels * lights)\n   void RenderLightingPass() {\n       BindGBuffer();       // 绑定几何数据缓冲区\n       SetupRenderTarget(); // 设置渲染纹理\n       \n       // 1. 遍历像素(在一个 RT 中的像素) [PS]\n       for each(pixel in RenderTargetPixels) {\n           // 获取 GBuffer 数据\n           pixelData = GetPixelDataFromGBuffer(pixel.uv);\n           // 清空累计颜色\n           color = 0;    \n           // 2. 遍历所有灯光，将每个灯光的光照计算结果累加到颜色中\n           for each(light in Lights) {\n               color += CalculateLightColor(light, pixelData);\n           }\n           // 写入颜色到 RT\n           WriteColorToRenderTarget(color);\n       }\n   }\n   ```\n\n\n\n**多渲染目标** *Multiple Render Targets*, MRT 技术：可以一次渲染完成对像素 位置、颜色、法线等对象信息到多个帧缓冲里\n\n```c\n// 1. 一个 FBO 绑定多个 buffer\nGLuint gBuffer;\nglGenFramebuffers(1, &gBuffer);\nglBindFramebuffer(GL_FRAMEBUFFER, gBuffer);\nGLuint gPosition, gNormal, gColorSpec;\n\n// - 位置颜色缓冲\nglGenTextures(1, &gPosition);\nglBindTexture(GL_TEXTURE_2D, gPosition);\nglTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL);\nglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);\nglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);\nglFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, gPosition, 0);\n\n// - 法线颜色缓冲\nglGenTextures(1, &gNormal);\nglBindTexture(GL_TEXTURE_2D, gNormal);\nglTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL);\nglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);\nglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);\nglFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, gNormal, 0);\n\n// - 颜色 + 镜面颜色缓冲\nglGenTextures(1, &gAlbedoSpec);\nglBindTexture(GL_TEXTURE_2D, gAlbedoSpec);\nglTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGBA, GL_FLOAT, NULL);\nglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);\nglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);\nglFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, GL_TEXTURE_2D, gAlbedoSpec, 0);\n\n// - 告诉OpenGL我们将要使用(帧缓冲的)哪种颜色附件来进行渲染\nGLuint attachments[3] = { GL_COLOR_ATTACHMENT0,     GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2 };\nglDrawBuffers(3, attachments);\n\n// 2. 片源着色器绘制 buffer\n#version 330 core\nlayout (location = 0) out vec3 gPosition;  // location = 0 和 frame buffer 的 GL_COLOR_ATTACHMENT0 对应\nlayout (location = 1) out vec3 gNormal;\nlayout (location = 2) out vec4 gAlbedoSpec;\n\nin vec2 TexCoords;\nin vec3 FragPos;\nin vec3 Normal;\n\nuniform sampler2D texture_diffuse1;\nuniform sampler2D texture_specular1;\n\nvoid main() {    \n    // 存储第一个G缓冲纹理中的片段位置向量\n    gPosition = FragPos;\n    // 同样存储对每个逐片段法线到G缓冲中\n    gNormal = normalize(Normal);\n    // 和漫反射对每个逐片段颜色\n    gAlbedoSpec.rgb = texture(texture_diffuse1, TexCoords).rgb;\n    // 存储镜面强度到gAlbedoSpec的alpha分量\n    gAlbedoSpec.a = texture(texture_specular1, TexCoords).r;\n}\n```\n\n\n\n### 2.1 Deferred Lighting\n\n延迟光照，又称 *Light Pre-Pass*\n\n- 优点：相对于 Deferred 可以不需要 MRT 硬件支持；支持 MSAA\n- 缺点：带宽、显存消耗大（相对于 Deferred，在几何 Pass 时减少，但 drawcall 多了一次）；透明渲染支持不好\n\n方法：\n\n1. <u>几何 Pass</u>：只在 G-Buffer 中存储 深度信息、法线值 RGB 和 粗糙度 Alpha（只需要绘制到 1 个 RT，不需要 MRT）\n\n2. <u>光照 Pass</u>：在 FS 阶段利用 G-Buffer 计算出所必须的 light properties\n   Channel RGB 存储：根据 Normal，LightDir, LightColor, 光的衰减系数 得到的漫反射值\n   Channel A 存储：根据 Normal，LightDir, LightColor, 光的衰减系数，以及高光强度，观察角度 得到的高光反射值\n   高光反射的值本是一个三维数据，通过将颜色转换为亮度的公式转化为一维数据存储\n   $$\n   lum(x) = 0.2126 x_r + 0.7152x_g + 0.0722x_b\n   $$\n\n3.  <u>第二次几何 Pass</u>：将结果送到 forward rendering 渲染方式在 FS 里计算最后的光照效果\n\n\n\n### 2.2 Tile-Based Deferred（TBDR）\n\n基于瓦片的延迟渲染\n\n- 优点：相对于 Deferred，在光照 Pass 时带宽消耗减少\n- 缺点：显存消耗大；MSAA 和透明渲染支持不好；需要 MRT 硬件支持\n\n![](./images/RenderingDeferredTiledBased.png)\n\n方法：\n\n1. <u>几何 Pass</u>：生成 G-Buffer，这一步和传统 deferred shading 一样\n2. <u>光照 Pass</u>：把 G-Buffer 划分成许多四边形（大小一般是 2 的 N 次方，且长宽不一定相等，称为 Tile）\n   根据屏幕划分 Tile，构成 Tile 的包围盒，并将其与光的包围盒求交来剔除无用的光源\n   对于 G-Buffer 的每个 pixel，用它所在 Tile 的 light list 累加光照计算 shading\n\n\n\n### 2.3 Clustered Deferred\n\n分簇延迟渲染 *Clustered Deferred Rendering*\n\n方法：和 TBDR 一致，只是在划分 Tile 的场景包围盒上更加精确（屏幕空间 + 深度），进而更精准地裁剪光源，避免深度不连续时的光源裁剪效率降低\n主要的细分 tile 场景包围盒子的方法有隐式 Implicit 分簇法（绿色），显式 Explicit 分簇法（蓝色）\n以下为 TBDR 的划分方式（红色）与 Clustered 的对比图\n\n![](./images/RenderingClusteredDeferred.png)\n\n\n\n\n\n# Reference\n\n- [learnopengl-Lighting Advanced](https://learnopengl-cn.github.io/05 Advanced Lighting/01 Advanced Lighting/)\n- [Schlick's approximation](https://en.wikipedia.org/wiki/Schlick's_approximation)\n- [Everything has Fresnel](http://filmicworlds.com/blog/everything-has-fresnel/)\n- [SIGGRAPH 2012 Course: Practical Physically Based Shading in Film and Game Production](https://blog.selfshadow.com/publications/s2012-shading-course/#course_content)\n- [Unity_Shaders_Book](https://github.com/candycat1992/Unity_Shaders_Book)\n- [MSDN-Cascaded Shadow Maps](https://docs.microsoft.com/zh-cn/windows/win32/dxtecharts/cascaded-shadow-maps?redirectedfrom=MSDN)\n- [GDC: Deferred Shading](http://developer.amd.com/wordpress/media/2012/10/D3DTutorial_DeferredShading.pdf)\n- [Deferred lighting approaches](http://www.realtimerendering.com/blog/deferred-lighting-approaches/)\n- [Deferred Shading VS Deferred Lighting](https://blog.csdn.net/BugRunner/article/details/7436600)\n- [Clustered Deferred and Forward Shading](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.592.3067&rep=rep1&type=pdf)\n- [Decoupled deferred shading for hardware rasterization](https://cg.ivd.kit.edu/publications/p2012/shadingreuse/shadingreuse_preprint.pdf)\n- [Microfacet Models for Refraction through Rough Surfaces](https://www.cs.cornell.edu/~srm/publications/EGSR07-btdf.pdf)\n- [基于物理的渲染—更精确的微表面分布函数 GGX](https://blog.uwa4d.com/archives/1582.html)\n- [UE4 BRDF 公式解析](https://zhuanlan.zhihu.com/p/35878843)\n- [阴影渲染](https://zhuanlan.zhihu.com/p/102135703)\n- [使用顶点投射的方法制作实时阴影](https://zhuanlan.zhihu.com/p/31504088)\n- [弧长和曲面面积](https://blog.csdn.net/sunbobosun56801/article/details/78657455)\n- [实时阴影技术总结 - xiaOp的博客 (xiaoiver.github.io)](https://xiaoiver.github.io/coding/2018/09/27/实时阴影技术总结.html)\n- [Cascaded Shadow Maps(CSM)实时阴影的原理与实现](https://zhuanlan.zhihu.com/p/53689987)\n\n"
  },
  {
    "path": "ComputerGraphics(OpenGL)/Part2_PhysicalLight.md",
    "content": "# 一、概率与统计\n\n**随机变量** $X$​：可能取很多不同值的变量\n\n**随机变量分布函数** $X \\sim p(x)$​​​​​​​：\n连续的分布函数又称**概率密度函数** Probability Density Function(PDF)，指不同概率事件下随机变量和概率的映射关系\n\n某一个随机变量 $x$ 对应的概率 $P$​\n$$\n\\begin{align}\n离散：P &= p(x), & dx = 1\\\\\n连续：P &= p(x)dx \\\\\n\\\\\n所有概率和：\\sum p(x) &= 1\n\\end{align}\n$$\n**均值**：统计所有数据得到的结果\n\n**期望** $E$​​：\n抽取部分数据得到的**平均概率值**，无限接近于均值\n$$\n\\begin{align}\n\\lim_{x \\to \\infty} E[X]&= \\bar X \\\\\n离散： E[X] &= \\sum _{i=1}^{n} x_ip(x_i)，p(x) \\geq 0\\\\\n连续： E[X] &= \\int_1^n xp(x)dx \\\\\n\\\\\n对于随机变量X,Y \\\\\nY &= f(X) \\\\\nE[Y] &= E[f(x)] \\\\\n&= \\int f(x)p(x)dx\n\\end{align}\n$$\n**方差 Variance**：\n用来度量随机变量和其期望（即均值）之间的**分散程度**，波动越大，方差越大\n$$\n\\begin{align}\nVar(x) \n&= s^2 \\\\\n&= \\sum _{i=1}^n(x_i - \\bar x)^2f(x) \\\\\n&=E((x - \\bar x)^2) \\\\\n&=E(x^2 - 2x\\bar x + \\bar x^2) \\\\\n&=E(x^2) - 2E(x \\bar x) + E(\\bar x^2) \\\\\n&=E(x)^2 - 2 \\sum x \\bar x p(x) + \\sum \\bar x^2 p(x) \\\\\n&=E(x)^2 - 2 \\bar x \\sum xp(x) + \\bar x^2 \\sum p(x) \\\\\n&=E(x)^2 - 2 \\bar xE(x) + \\bar x^2 \\\\\n&=E(x)^2 - 2E(x)E(x) + (E(x))^2 \\\\\n&=E(x)^2 - (E(x))^2\n\\end{align}\n$$\n**协方差**：\n衡量两个变量之间的变化方向关系\n$$\ncov(X,Y) = E(XY) - E(X)E(Y)\n$$\n\n## 1. 蒙特卡洛积分\n\n**概率密度函数 PDF  (probability density function)**：随着连续随机变量样本在整个样本集上发生的<u>概率</u>\n**累积分布函数 CDF (Cumulative Distribution Function)**：随着连续随机变量而变化的<u>概率积分值</u>，CDF 的导数是 PDF\n\n**大数定理**：抽样检测一部分数据得出的结果虽然不能完全代表整个样品，但结果随着采样数量的增加而逐渐接近\n\n**蒙特卡洛积分** 主要是统计和概率理论的组合。\n\n蒙特卡洛可以帮助我们离散地解决人口统计问题，而不必考虑**所有**人\n蒙特卡洛积分在计算机图形学中非常普遍，因为它是一种以高效的离散方式对连续的积分求近似而且非常直观的方法：对任何面积/体积进行采样——例如半球 Ω ——在该面积/体积内生成数量 N 的随机采样，权衡每个样本对最终结果的贡献并求和\n\n\n\n## 2. 球协函数\n\n\n\n\n\n# 二、物理理论\n\n## 1. 光学现象\n\n光线实际上可以被认为是一束没有耗尽就不停向前运动的能量，而光束是通过碰撞的方式来消耗能量\n\n### 间接光照\n\n也称反射照明，通过其他物体反射的光线照亮物体的光照效果\n![](./images/light_indirect.png)\n\n\n\n### 环境光遮蔽\n\n常用来模拟大面积的光源对整个场景的光照\n![](./images/light_AO.png)\n\n\n\n### 反射 Reflection\n\n指镜子会反射场景中一摸一样像的效果\n![](./images/light_bounce.png)\n\n\n\n### 折射 Refraction\n\n光的折射是指光从一种介质斜射入另一种介质时，传播方向发生改变，从而使光线在不同介交界处发生的偏折\n\n\n\n### 散射 Scattering\n\n光经过透明物体的折射后聚焦在一定范围上的效果\n![](./images/light_caustics.png)\n\n\n\n## 2. 材质属性\n\n### 粗糙度 Roughness\n\n用统计学的方法来概略的估算微平面的粗糙程度\n表示半程向量（Blinn-Phong 中）的方向与微平面平均取向方向一致的概率\n\n微平面的取向方向与中间向量的方向越是一致，镜面反射的效果就越是强烈越是锐利\n\n![](./images/microfacets.png)\n\n\n\n### 透光性\n即透明度，描述物质透过光线的程度\n物体的透光性主要取决于物质内部结构对外来光子的吸收和散射\n散射越多越不透明，散射越少越透明（例：水对光的散射少折射多，为透明的）\n\n\n\n### 各向性 \n描述物质任意一点的物理和化学等属性跟方向是否相关\n\n**各向同性** isortropy：与方向**无关**\n在计算机图形学，特别是实时渲染领域，通常将物体简化成均匀的，即各向同性的\n\n**各向异性** anisortropy：与方向**有关**\n金属面经过拉丝后会有各项异性的效果\n\n\n\n### 导电性 / 金属度 Metallic\n因为导体和非导体如此的不同，通常用金属度来控制一个材质是否为金属\n金属度可以用灰度值，也可以用二值图来表示物体表面具有金属特性的位置\n\n**绝缘体**：也称电介质，常见的绝缘体包含干燥的木材、塑料、橡胶、纸张等\n\n**导体**：导电性强，常见的有金属、电解质、液体等\n导体有更强的反射，导体的反射光颜色可能会与 Albedo（固有色，金属没有漫反射）不同\n\n\n\n## 3. 能量守恒\n\n![](./images/light_energy.jpg)\n\n**能量守恒** Energy Conservation：出射光线的能量永远不能超过入射光线的能量（发光面除外）\n入射光线的能量，一部分被镜面反射、（散射后）漫反射出去，一部分被吸收，最后一部分被透射（折射）出去\n$$\nE_{in} = E_{specular} + E_{diffuse} + E_{absorb} + E_{refract}\n$$\n\n根据能量守恒，在不同的材质下可以忽略其他微弱的能量消耗\n\n- 对于光强度，反射 + 折射 = 1.0\n- 对于光强度，镜面反射 + 漫反射 = 1.0\n- 随着粗糙度的上升镜面反射区域的会增加，但是镜面反射的亮度却会下降\n\n\n\n\n\n\n## 4. 辐射量\n\n辐度学和色度学的单位是一一对应的，在 **游戏引擎** 和 **建筑照明** 设计里经常使用**色度学**单位。具体见 [从真实世界到渲染](https://zhuanlan.zhihu.com/p/118272193)\n其他相关的辐照单位对应的色度学单位如下：\n\n![](./images/light_equation.svg)\n\n以下主要用辐度学来解释 辐射量\n\n### $Q_e$ 辐射能 Energy\n\n代表单个光子的辐射能，表示为 $Q_e = {hc \\over \\lambda}$，单位是焦耳 $J$\n其中 $h$：Planck 常量（常数），$c$：光的速度（常数），$\\lambda$：光的波长\n\n\n\n### $\\Phi_e$ 辐通量/光通量 $lumens/lm$\n\n辐通量 Radiant flux 又称功率，代表<u>单位时间</u>内发射、接收或传输的能量 $\\Phi_e = {dQ \\over dt}$，单位是瓦特 $W$\n\n<u>光源发射的全部能量</u>（单位时间内）\n1700 lm = 100 W 灯泡的辐射能\n\n\n\n### $E_e$ 辐照度/光照度 $lux/lx$\n平行光的辐照度 Irradiance：**垂直于光线方向**的 **单位面积** 上 **单位时间** 内穿过的能量，单位是 $W/m^2$\n\n<u>光源发射的能量到达某一表面上的强度</u>（单位时间内）\n\n![](./images/irradiance.png)\n\n与平行光线垂直的平面 $A^\\bot$ 上的辐照度 $E_e = {\\Phi_e \\over A\\cos \\theta}$\n与距离 (单位：米 $m$) 的平方成反比\n\n\n\n### $I_e$ 辐强度/光强度 $cd/candela$\n辐强度 Radiant intensity：表示**元立体角**内的辐通量大小 $I_e = {d\\Phi \\over d\\Omega}$，单位是 $W/sr$\n\n<u>光源从一个立体角发射出的能量</u>（单位时间）\n\n![](./images/light_solidAngle.jpg)\n\n- Angle 角，圆的弧长比半径\n\n- Solid Angle 立体角，$\\Omega = {A \\over r^2}$，表示符号 $\\Omega / \\omega$， 单位 球面度 $sr$\n  在[球坐标系](https://baike.baidu.com/item/球坐标系) $(r,\\theta,\\phi)$ 下，观测点为球心，构造一个单位球面：\n  任意物体投影到该单位球面上的投影面积，即为该物体相对于该观测点的立体角\n\n$$\n\\begin{align}\ndA_2 &= rsin \\theta d\\phi * rd\\theta = r^2(sin\\theta d\\phi d\\theta) \\\\\nd\\Omega &= {dA_2 \\over r^2} = sin\\theta d \\theta d \\phi \\\\\n\\Omega &= \\int d \\Omega \\\\\n\\Omega_{总面积} &= \\int _0^{2\\pi} d\\phi \\int_0^{\\pi} sin\\theta d\\theta = 4\\pi\n\\end{align}\n$$\n\n\n\n### $L_e$ 辐亮度/光亮度 $nit（cd/m^2）$\n\n辐亮度 Radiance：表示从**单位立体角**反射出去的**单位面积**上的辐通量，单位 $W/(m^2 \\cdot sr)$\n\n<u>光源从一个立体角发射出的能量到达某一个表面，反射出去的能量</u>（单位时间）\n\n![](./images/light_radiance.png)\n\n描述传感器（摄影机，人眼等)感受辐射最常用的量，辐亮度与距离无关\n其中，\n$\\theta$ 入射光线与平面法线的夹角\n$A$ 真实的平面面积，是 $A^{\\bot}$ 的投影\n$A^{\\bot}$ 表示垂直光线方向的平面面积\n$$\nL_e = {d^2 \\Phi_e \\over d A^\\bot \\cdot d \\Omega} = {d^2 \\Phi_e \\over cos\\theta dA \\cdot d\\Omega}\n$$\n\n\n\n\n\n\n# 三、基于物理的渲染\n\n## 2. 渲染方程\n\n**渲染方程**（着色流程）\n\n- 先有了一个渲染方程，才会有对应的材质类型\n- 传统的渲染方程为每种材质写一个特定的 Shader\n- 为了一个万能的 Shader 就可以渲染大部分类型的材质，需要基于物理的方式来渲染\n  **Physically Based Rendering 基于物理**的渲染（非真实的物理渲染）是为了使用一种更符合物理学规律的方式来模拟光线\n  <span style=\"color:red\">PBR 并不需要追求和照片一样真实的效果，只是为了有一个近乎万能的渲染方法</span>\n\n![](./images/render_equation.png)\n\n\n\n### 2.1 入射光线总量（精确光源）\n\n实际渲染方程在计算时，由于**光源的类型**确定（精确光源 Punctual light sources）可以进一步简化渲染方程\n每一个光源的计算的统一公式为\n\n- $f$：双向反射分布函数\n- $v$：观察方向\n- $n$：法线方向\n- $l_i$：光源方向\n- $C_i$：光源颜色\n\n$$\nL_o(v) = \\pi f(l_i, v) C_i(n \\cdot l_i)\n$$\n\n\n\n### 2.2 反射光线总量（BRDF）\n\n**双向反射分布函数** BRDF（Bidirectional Reflectance Distribution Function）\n表示有多少比例的光反射到了观察方向上（遵守能量守恒），可以近似的求出每束光线对一个给定了材质属性的平面上最终反射出来的光线所作出的**贡献程度**\n$$\nf_r = k_d\\cdot f_{lambert} + k_s \\cdot f_{reflection}\n$$\n\n其中，$k_d + k_s = 1$\n\n- $k_d$ 折射光线能量比率\n\n- $k_s$ 反射光线能量比率\n\n\n\n\n\n## 3. PBR 材质\n\n物理的材质模型遵循 **能量守恒** Energy Conservation：出射光线的能量永远不能超过入射光线的能量（发光面除外）\n\n- 根据能量守恒，随着粗糙度的上升镜面反射区域的会增加，但是镜面反射的亮度却会下降\n- 反射/镜面反射 + 折射/漫反射 = 1.0（光强度）\n\n\n\n**PBR 材质贴图**\n\n- <u>环境光遮蔽 Ambient Occlusion 贴图</u>（漫反射、高光反射）\n  固定光源的辐照度贴图，多用于大场景的环境光\n  为表面和周围潜在的几何图形指定了一个额外的阴影因子\n  网格/表面的环境遮蔽贴图可以通过实时动态生成，或者由 3D 建模软件提前烘焙生成\n- <u>反照率 Albedo 贴图</u>（漫反射）\n  为每一个金属的纹素（Texel 纹理像素）指定表面颜色（固有色）或者基础反射率，只包含表面的颜色\n  反照率贴图和漫反射贴图的区别，反照率贴图只有固有色没有其他的阴影细节纹理，漫反射贴图是一些细节纹理和固有色的集合\n- <u>粗糙度 Roughness / 光滑度 Smoothness 贴图</u>（高光反射）\n  微表平面模型的简化，以纹素为单位指定某个表面有多粗糙，粗糙度 = 1.0 – 光滑度\n- <u>金属度 Metallic 贴图</u>（高光反射）\n- <u>法线 Normal 贴图</u>（高光反射）\n  计算反射光线强度时使用\n  根据分布方向的 一致 / 非一致性，可模拟出 各项异性 Anisotropic / 各项同性 Isotropic 材质\n  ![](./images/light_anisotropic.png)\n\n\n\n### 3.1 漫反射\n\n漫反射的**输入颜色**为 Albedo 贴图，它不收光照的影响，没有阴影为物体本来的颜色\n\n1. 不考虑光源方向\n   假设在所有方向观察亮度都是相同的，因此 $f_{lambert}$ 是常数\n\n$$\n\\begin{align}\n\\int_{\\Omega} f_{lambert} L_{Input} cos\\theta d\\omega' &= L_{Output}\\\\\nL_{Input} f_{lambert} \\int_{\\Omega} cos\\theta d\\omega' &= L_{Output}\\\\\nf_{lambert} \\int_0^{2\\pi} d\\phi \\int_0^{\\pi\\over 2} cos\\theta d\\theta &= 1\\\\\nf_{lambert} \\pi &= 1\\\\\nf_{lambert} &= {color \\over \\pi}, \\space color[0,1]\n\\end{align}\n$$\n\n2. 考虑光源方向\n   根据入射精确光源计算漫反射分量，可见和简化的 lambert 光照模型计算方法一致\n\n$$\n\\begin{align}\nL_o(v) &= \\pi f(l_i, v) C_i(n \\cdot l_i) \\\\\nL_{lambert} &= \\pi f_{lambert} C_i(n \\cdot l_i) \\\\\n&= C_i(n \\cdot l_i)\n\\end{align}\n$$\n\n\n\n### 3.2 高光反射（微平面模型）\n\n高光反射模型 Cook-torrance，字母 D, F, G 分别代表着一种类型的函数，各个函数分别用来近似的计算出表面反射特性的一个特定部分\n$$\nf_{cook-torrance} = {DFG \\over 4(\\omega_o \\cdot n)(\\omega_i \\cdot n)}\n$$\n\n\n\n\n\n**正态分布函数** Normal Distribution Function\n用来估算微平面的主要函数，估算在受到表面**粗糙度**的影响下，取向方向与中间向量一致的微平面的数量\n以下设给定向量 $h$，通过 NDF 函数 Trowbridge-Reitz GGX 计算与 $h$ 方向一致的概率\n$h$：平面法向量 $n$ 和光线方向向量之间的中间向量\n$\\alpha$：表面的粗糙度（参数）\n$$\nNDF_{GGXTR}(n,h,\\alpha) = {\\alpha^2 \\over \\pi((n\\cdot h)^2(\\alpha^2-1)+1)^2}\n$$\n传统的微平面模型的问题：太过于平滑，不能表现小于一个像素的几何级的细节\n\n![](./images/microfacet.jpg)\n\n\n\n#### 3.2.2 G 微平面自成阴影\n\n几何函数 Geometry Function：用来描述微平面自成阴影的属性\n当一个平面相对比较粗糙的时候，平面表面上的微平面有可能挡住其他的微平面从而**减少表面所反射的光线**\n\n1. 单纯的计算平面遮挡的几何函数可采用 GGX 与 Schlick-Beckmann 近似的结合体\n   因此又称为 Schlick-GGX \n   $v$：观察方向\n   $\\alpha$：表面的粗糙度（参数）\n   $k_{direct}$：直接光照\n   $k_{IBL}$：IBL（Image based lighting）基于图像的光照\n\n   - 其光源不是可分解的直接光源，而是将周围环境整体视为一个大光源\n   - 通常使用（取自现实世界或从 3D 场景生成的）环境立方体贴图 (Cubemap) \n     我们可以将立方体贴图的每个像素视为光源，在渲染方程中直接使用它\n\n   $$\n   \\begin{align}\n   k_{direct} &= {(\\alpha + 1)^2 \\over 8} \\\\\n   k_{IBL} &= {\\alpha ^2 \\over 2} \\\\\n   G_{SchlickGGX}(n,v,k) &= {n \\cdot v \\over (n\\cdot v)(1-k) + k}\n   \\end{align}\n   $$\n\n2. 将观察方向（几何遮蔽 Geometry Obstruction）和光线方向向量（几何阴影 Geometry Shadowing）都考虑进去后采用 Smith’s method 方法计算\n   $v$：观察方向\n   $l$：光线方向\n   $$\n   G_{Smith}(n,v,l,k) = G_{SchlickGGX}(n,v,k) \\cdot G_{SchlickGGX}(n,l,k)\n   $$\n\n\n\n## 4. 全局光照 Global illumination\n\n全局光照 GI (环境光)：主要对以下生活中的现象进行模拟\n\n\n\n**离线渲染方案**\n\n1. 路径追踪 \n2. 光子映射 Photon Mapping \n3. 辐射度 只能模拟漫反射现象\n\n\n\n**实时渲染方案**\n\n- 屏幕空间（SSGI）\n  仅限于摄像机视图中的对象和光照，用于生成光照数据\n  如果明亮的光源在视野之外或被场景内的物体阻挡，可能会导致不和谐的结果\n  1. 屏幕环境光遮蔽\n  2. 屏幕空间反射\n- 世界空间\n  1. 体素 Voxel Cone Tracing\n  2. 距离场 Distance Field\n  3. 实时光线追踪\n\n\n\n**基于图像的照明 IBL**\n通常使用（取自现实世界或从3D场景生成的）环境立方体贴图 (Cubemap) ，我们可以将立方体贴图的**每个像素视为光源**，在渲染方程中直接使用它\n这种方式可以有效地捕捉环境的全局光照和氛围，使物体**更好地融入**其环境\n\n根据渲染总方程（高光反射模型 Cook-torrance）\n\n- 表示点 $p$ 在 $\\omega_o$ 方向被反射出的**辐亮度**的**总和**\n- 它包含以 $p$ 为球心的单位半球领域 $\\Omega$ 内所有入射方向的 $d\\omega_i$ 之和\n- 其中\n  $\\omega_o$ 观察/出射方向\n  $\\omega_i$ 光线入射方向\n  $n\\cdot\\omega_i$ 入射方向和法线的夹角 $cos\\theta$ 值\n\n$$\n\\begin{align}\nL_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 \\\\\n&= \\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 \\\\\n&= 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 \\\\\n&= L_{o 漫反射} + L_{o 镜面反射}\n\\end{align}\n$$\n\n可知\n\n- 漫反射与 物体的位置 和 入射光线方向 有关\n- 镜面反射与 物体的位置、入射光线方向、<u>出射光线方向</u> 有关\n\n\n\n### 4.1 IBL 漫反射\n\n#### 4.1.1 辐照度图\n\n- 预先烘焙：根据环境贴图计算\n  实时计算：预计算在一个<u>固定位置</u>下新的立方体贴图，它在每个采样方向（也就是纹素）中存储漫反射积分的结果，这些结果是通过卷积计算出来的\n- 在图的每个像素上通过对光的辐射范围半球 $\\Omega$ 上的大量方向进行离散采样并对其辐射度取平均值，来**计算每个输出采样方向的积分**\n\n\n\n#### 4.1.2 反射探针（实时计算 IBL）\n\n- 辐照度贴图是从<u>固定位置</u>获得的光照贴图，在不同的室内场景位置中我们会使用不同的辐照度贴图来达到环境光动态变化的效果\n  这个固定位置我们称为反射探针\n- 根据当前视点的辐照度为：与其距离最近的几个反射探针处辐照度的插值\n\n\n\n**IBL 漫反射贴图 制作流程**\n\n1. 读取 hdr 图(从球体投影到平面上的图)，转换为距柱状投影图(Equirectangular Map)\n   实际读取图片到 float texture 就可以\n\n2. 等距柱状投影图 转换为 立方体贴图\n   采用不同的观察空间，从柱状投影图逐个绘制纹理到对应的立方体贴图上（可以通过缩小立方图来提高效率）\n\n   ```c\n   #version 330 core\n   out vec4 FragColor;\n   in vec3 localPos; // 经过 VS 插值后的顶点坐标（模型空间）\n   \n   uniform sampler2D equirectangularMap;\n   \n   const vec2 invAtan = vec2(0.1591, 0.3183);\n   // 球体 UV 坐标转 笛卡尔 uv 坐标\n   vec2 SampleSphericalMap(vec3 v) {\n       vec2 uv = vec2(atan(v.z, v.x), asin(v.y));\n       uv *= invAtan;\n       uv += 0.5;\n       return uv;\n   }\n   \n   void main() {       \n       vec2 uv = SampleSphericalMap(normalize(localPos)); // make sure to normalize localPos\n       vec3 color = texture(equirectangularMap, uv).rgb;\n   \n       FragColor = vec4(color, 1.0);\n   }\n   ```\n\n3. 生成辐照度贴图\n   计算立方体贴图的卷积，通过对**有限数量的所有方向**采样以近似求解（卷积，离散均匀采样积分）\n   下图为球形坐标系\n   ![](./images/sphericalcoordinates.png)\n\n   球形坐标系 和 笛卡尔坐标系 的互相转换如下\n   $$\n   \\begin{align}\n   x &= rsin\\theta cos\\phi \\\\\n   y &= rcos\\theta \\\\\n   z &= rsin\\theta sin\\phi \\\\\\\\\n   r &= \\sqrt {x^2 + y^2 + z^2} \\\\\n   \\theta &= cos^{-1}{y \\over r} \\\\\n   \\phi &= tan^{-1}{z \\over x} \\\\\\\\\n   L_o(p,\\omega_o) &= k_d{color \\over \\pi}\\int_{\\Omega} L_i(p,w_i)n\\cdot\\omega_id\\omega_i \\\\\n   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\n   \\end{align}\n   $$\n\n   ```c\n   vec3 irradiance = vec3(0.0);  \n   \n   // 根据法线制作 TBN 切线坐标矩阵\n   vec3 up    = vec3(0.0, 1.0, 0.0);\n   vec3 right = cross(up, normal);\n   up         = cross(normal, right);\n   \n   float sampleDelta = 0.025;\n   float nrSamples = 0.0;\n   // 半球采样：phi 绕 y 轴 360，theta 绕 z 轴 180\n   for(float phi = 0.0; phi < 2.0 * PI; phi += sampleDelta) {\n       for(float theta = 0.0; theta < 0.5 * PI; theta += sampleDelta) {\n           // 球形坐标 转 笛卡尔坐标 (切线空间)\n           vec3 tangentSample = vec3(sin(theta) * cos(phi),  \n                                     sin(theta) * sin(phi), \n                                     cos(theta));\n           // 切线空间转换为世界空间\n           vec3 sampleVec = tangentSample.x * right + \n                            tangentSample.y * up + \n                            tangentSample.z * N; \n   \n           irradiance += texture(environmentMap, sampleVec).rgb * cos(theta) * sin(theta);\n           nrSamples++;\n       }\n   }\n   irradiance = PI * irradiance * (1.0 / float(nrSamples));\n   ```\n\n4. 根据生成的辐照度贴图 模拟菲涅耳效应\n   由于 IBL 的漫反射环境来自环境的所有方向，没有一个确定的方向来计算菲涅耳效应\n   简化后，用法线和视线之间的夹角计算菲涅耳系数\n\n   ```c\n   vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness) {\n       return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0);\n   }  \n   \n   vec3 kS = fresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness); \n   ```\n\n\n\n### 4.2 IBL 高光反射\n\n**重要性采样**：只在某些区域生成采样向量，该区域围绕微表面半向量，受粗糙度限制\n\n1. 通过低差异序列根据索引整数获得均匀的随机数\n2. 根据粗糙度和微表面等属性进行重要性质采样\n\n\n\n在实时状态下，对每种可能的 **入射光线** 和 出射光线 的组合预计算该积分是不可行的\n**Epic Games 的分割求和近似法**将预计算分成两个单独的部分求解，再将两部分组合起来得到后文给出的预计算结果\n$$\n\\begin{align}\nf_r(p, w_i, w_o) &= k_s{DFG \\over 4(\\omega_o \\cdot n)(\\omega_i \\cdot n)} \\\\\nL_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 \\\\\n&= \\int_{\\Omega} f_r(p, w_i, w_o)L_i(p,w_i)n\\cdot\\omega_id\\omega_i \\\\\n&= \\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 \\\\\\\\\n&= 预滤波环境贴图 * 镜面反射积分\n\\end{align}\n$$\n\n1. 制作**预滤波环境贴图**\n\n   它类似于辐照度图，是预先计算的环境卷积贴图，但这次考虑了粗糙度。因为随着粗糙度的增加，参与环境贴图卷积的采样向量会更分散，导致反射更模糊，所以对于卷积的每个粗糙度级别，我们将按顺序把模糊后的结果存储在预滤波贴图的 mipmap 中，注意开启 `glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);  `  让立方体接缝过渡自然\n\n   可以通过 [cmftStudio](https://github.com/dariomanesku/cmftStudio) 或 [IBLBaker](https://github.com/derkreature/IBLBaker) 等工具生成预计算贴图\n\n   ![](./images/ibl_prefilter_map.png)\n\n   ```c\n   #version 330 core\n   out vec4 FragColor;\n   in vec3 WorldPos;\n   \n   uniform samplerCube environmentMap;\n   uniform float roughness;\n   \n   const float PI = 3.14159265359;\n   \n   float DistributionGGX(vec3 N, vec3 H, float roughness) {\n       float a = roughness*roughness;\n       float a2 = a*a;\n       float NdotH = max(dot(N, H), 0.0);\n       float NdotH2 = NdotH*NdotH;\n   \n       float nom   = a2;\n       float denom = (NdotH2 * (a2 - 1.0) + 1.0);\n       denom = PI * denom * denom;\n   \n       return nom / denom;\n   }\n   \n   // http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html\n   // efficient VanDerCorpus calculation\n   // 整数变小数：把十进制数字的二进制表示 镜像翻转 到小数点右边\n   float RadicalInverse_VdC(uint bits) {\n        bits = (bits << 16u) | (bits >> 16u);\n        bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);\n        bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);\n        bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);\n        bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);\n        return float(bits) * 2.3283064365386963e-10; // / 0x100000000\n   }\n   \n   // 低差异序列：根据索引来生成均匀随机数，避免伪随机带来的不均匀采样\n   vec2 Hammersley(uint i, uint N) {\n   \treturn vec2(float(i)/float(N), RadicalInverse_VdC(i));\n   }\n   \n   // 重要性采样\n   vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness) {\n   \tfloat a = roughness*roughness;\n   \t\n     // 根据随机数获得随机角度\n   \tfloat phi = 2.0 * PI * Xi.x;\n   \tfloat cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a*a - 1.0) * Xi.y));\n   \tfloat sinTheta = sqrt(1.0 - cosTheta*cosTheta);\n   \t\n   \t// 根据随机角度获得半角向量，从球坐标转换为笛卡尔坐标\n   \tvec3 H;\n   \tH.x = cos(phi) * sinTheta;\n   \tH.y = sin(phi) * sinTheta;\n   \tH.z = cosTheta;\n   \t\n   \t// 将半角向量从切线空间转换为世界空间\n   \tvec3 up        = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);\n   \tvec3 tangent   = normalize(cross(up, N));\n   \tvec3 bitangent = cross(N, tangent);\n   \t\n   \tvec3 sampleVec = tangent * H.x + bitangent * H.y + N * H.z;\n   \treturn normalize(sampleVec);\n   }\n   \n   void main(){\t\t\n   \t  // 在卷积环境贴图时事先不知道镜面反射方向, 因此假设镜面反射方向总是等于输出方向 w_o\n   \t\t// 这意味着掠角镜面反射效果不是很好\n       vec3 N = normalize(WorldPos);\n       vec3 R = N;\n       vec3 V = R;\n   \n       const uint SAMPLE_COUNT = 1024u;\n       vec3 prefilteredColor = vec3(0.0);\n       float totalWeight = 0.0;\n       \n       for(uint i = 0u; i < SAMPLE_COUNT; ++i) {\n           // 根据重要性采样随机生成半角向量 H\n           vec2 Xi = Hammersley(i, SAMPLE_COUNT);\n           vec3 H = ImportanceSampleGGX(Xi, N, roughness);\n           vec3 L  = normalize(2.0 * dot(V, H) * H - V);\n   \n           float NdotL = max(dot(N, L), 0.0);\n           if(NdotL > 0.0) {\n               float D   = DistributionGGX(N, H, roughness);\n               float NdotH = max(dot(N, H), 0.0);\n               float HdotV = max(dot(H, V), 0.0);\n               float pdf = D * NdotH / (4.0 * HdotV) + 0.0001; \n   \n               float resolution = 512.0; // resolution of source cubemap (per face)\n               float saTexel  = 4.0 * PI / (6.0 * resolution * resolution);\n               float saSample = 1.0 / (float(SAMPLE_COUNT) * pdf + 0.0001);\n   \n               float mipLevel = roughness == 0.0 ? 0.0 : 0.5 * log2(saSample / saTexel); \n               \n               // 根据纹理的 LOD 大小来加载纹理\n               prefilteredColor += textureLod(environmentMap, L, mipLevel).rgb * NdotL;\n               totalWeight      += NdotL;\n           }\n       }\n   \n       prefilteredColor = prefilteredColor / totalWeight;\n       FragColor = vec4(prefilteredColor, 1.0);\n   }\n   ```\n\n2. 制作 **BRDF 积分贴图**\n   存储：入射角方向，建议存储为 512 x 512 大小的支持存储 mip 级别的 .dds 文件\n   横坐标：BRDF 的输入 $n\\cdot \\omega_i$（范围在 0.0 和 1.0 之间，$\\omega_i$ 为光源到片源方向，$\\omega_o$ 为视点到片源方向）\n   纵坐标：粗糙度\n   将环绕模式设置为  `GL_CLAMP_TO_EDGE` 以防止边缘采样的伪像，并且在 NDC (译注：Normalized Device Coordinates) 屏幕空间四边形上绘制积分贴图\n\n   ![](./images/ibl_brdf_lut.png)\n\n   根据 $n \\cdot \\omega_o$、表面粗糙度、菲涅尔系数 $F_0$ 来计算 BRDF 方程的卷积\n   并且假设在纯白的环境光或者辐射度恒定为 1，为了减少因变量的个数，我们做以下化简\n   $$\n   \\begin{align}\n   \\int_{\\Omega}f_r(p, w_i, w_o)n\\cdot \\omega_i d\\omega_i &= \n   \\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\\\\\n   &=\\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\\\\\n   &=\\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\\\\\n   设 \\space \\alpha = (1 - \\omega_o \\cdot h)^5, \\space 则：\\\\\n   &=\\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\\\\\n   &=\\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\\\\\n   &=\\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\\\\\n   &=\\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\\\\\n   &=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\\\\\n   &=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\\\\\n   &=F_0 A + B\\\\\n   \\end{align}\n   $$\n   转换为代码为：\n\n   ```glsl\n   float GeometrySchlickGGX(float NdotV, float roughness) {\n       // 不使用 IBL \n     \t// float a = (roughness + 1.0);\n       // float k = (a * a) / 8.0;\n     \n       // 使用 IBL 后和不用 IBL 这里公式略有不同\n       float a = roughness;\n       float k = (a * a) / 2.0; \n   \n       float nom   = NdotV;\n       float denom = NdotV * (1.0 - k) + k;\n   \n       return nom / denom;\n   }\n   \n   float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) {\n       float NdotV = max(dot(N, V), 0.0);\n       float NdotL = max(dot(N, L), 0.0);\n       float ggx2 = GeometrySchlickGGX(NdotV, roughness);\n       float ggx1 = GeometrySchlickGGX(NdotL, roughness);\n   \n       return ggx1 * ggx2;\n   }\n   \n   vec2 IntegrateBRDF(float NdotV, float roughness) {\n       vec3 V;\n       V.x = sqrt(1.0 - NdotV*NdotV);\n       V.y = 0.0;\n       V.z = NdotV;\n   \n       float A = 0.0;\n       float B = 0.0;\n   \n       vec3 N = vec3(0.0, 0.0, 1.0);\n   \n       const uint SAMPLE_COUNT = 1024u;\n       for(uint i = 0u; i < SAMPLE_COUNT; ++i) {\n           // 根据重要性采样随机生成入射光线和反射光线的 半角向量\n           vec2 Xi = Hammersley(i, SAMPLE_COUNT);\n           vec3 H  = ImportanceSampleGGX(Xi, N, roughness);\n           vec3 L  = normalize(2.0 * dot(V, H) * H - V);\n   \n           float NdotL = max(L.z, 0.0);\n           float NdotH = max(H.z, 0.0);\n           float VdotH = max(dot(V, H), 0.0);\n   \n           if(NdotL > 0.0) {\n               float G = GeometrySmith(N, V, L, roughness);\n               float G_Vis = (G * VdotH) / (NdotH * NdotV);\n               float Fc = pow(1.0 - VdotH, 5.0);\n   \n               A += (1.0 - Fc) * G_Vis;\n               B += Fc * G_Vis;\n           }\n       }\n       A /= float(SAMPLE_COUNT);\n       B /= float(SAMPLE_COUNT);\n       return vec2(A, B);\n   }\n   \n   void main()  {\n       vec2 integratedBRDF = IntegrateBRDF(TexCoords.x, TexCoords.y);\n       FragColor = integratedBRDF;\n   }\n   ```\n\n3. 结合预滤波环境和 BRDF 积分贴图，完成 IBL 反射\n\n   ```glsl\n   uniform samplerCube prefilterMap; // 预滤波环境贴图\n   uniform sampler2D   brdfLUT;  \t  // BRDF 积分贴图\n   \n   void main() {\n       [...]\n       vec3 R = reflect(-V, N);   \n       const float MAX_REFLECTION_LOD = 4.0;\n       vec3 prefilteredColor = textureLod(prefilterMap, R,  roughness * MAX_REFLECTION_LOD).rgb;    \n     \n     \tvec3 F = FresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness);\n   \n       vec3 kS = F;\n       vec3 kD = 1.0 - kS;\n       kD *= 1.0 - metallic;     \n   \n       vec3 irradiance = texture(irradianceMap, N).rgb;\n       vec3 diffuse    = irradiance * albedo;\n   \n       const float MAX_REFLECTION_LOD = 4.0;\n       vec3 prefilteredColor = textureLod(prefilterMap, R,  roughness * MAX_REFLECTION_LOD).rgb;   \n       vec2 envBRDF  = texture(brdfLUT, vec2(max(dot(N, V), 0.0), roughness)).rg;\n   \t\n       // specular 由于已经乘过了菲涅尔系数，所以这里不用乘以 kS\n       vec3 specular = prefilteredColor * (F * envBRDF.x + envBRDF.y);\n   \n       vec3 ambient = (kD * diffuse + specular) * ao; \n       [...]\n   }\n   ```\n\n\n\n### 4.3 屏幕空间反射和平面反射\n\n![](D:/Github/CrashNote/ComputerGraphics(OpenGL)/images/PlanarReflection.png)\n\n**屏幕空间反射 Screen Space Reflections：**\n\n- 优点：效率高，只考虑屏幕内的反射光\n- 缺点：可靠性差，无法反射画面外或被遮挡的物体\n\n\n\n**平面反射 Planar Reflections：**\n\n- 优点：反射保持了连贯和精准，平面反射能够无视摄像机视角，反射画面外的物体\n- 缺点：染开销较高，因为平面反射实际上将从**反射方向**再次对整个场景进行渲染\n\n\n\n\n\n## 5. PBR 代码实现\n\n预计算的方法 **Precomputation-based methods**\n\n```c\n// 方法一：根据统一数据计算 FS\n#version 330 core\nout vec4 FragColor;\nin vec2 TexCoords;\nin vec3 WorldPos;\nin vec3 Normal;\n\n// material parameters\nuniform vec3 albedo;\nuniform float metallic;\nuniform float roughness;\nuniform float ao;\n\n// lights\nuniform vec3 lightPositions[4];\nuniform vec3 lightColors[4];\n\nuniform vec3 camPos;\n\nconst float PI = 3.14159265359;\n\n// 3.1 正态分布函数：计算微表面粗糙度（高光区域）\nfloat DistributionGGX(vec3 N, vec3 H, float roughness) {\n    float a = roughness*roughness;\n    float a2 = a*a;\n    float NdotH = max(dot(N, H), 0.0);\n    float NdotH2 = NdotH*NdotH;\n\n    float nom   = a2;\n    float denom = (NdotH2 * (a2 - 1.0) + 1.0);\n    denom = PI * denom * denom;\n\n\t  // 避免在 NdotV=0.0 or NdotL=0.0 情况下出现除零错误\n    return nom / max(denom, 0.0000001); \n}\n\n// 3.2.1 几何函数：微表面自成阴影的程度\nfloat GeometrySchlickGGX(float NdotV, float roughness) {\n    float r = (roughness + 1.0);\n    float k = (r*r) / 8.0;\n\n    float nom   = NdotV;\n    float denom = NdotV * (1.0 - k) + k;\n\n    return nom / denom;\n}\n// 3.2.2 同时考虑观察方向和光源方向下的 几何函数值\nfloat GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) {\n    float NdotV = max(dot(N, V), 0.0);\n    float NdotL = max(dot(N, L), 0.0);\n    float ggx2 = GeometrySchlickGGX(NdotV, roughness);\n    float ggx1 = GeometrySchlickGGX(NdotL, roughness);\n\n    return ggx1 * ggx2;\n}\n\n// 3.3 菲涅尔方程：不同观察角下反射光线的强度\nvec3 fresnelSchlick(float cosTheta, vec3 F0) {\n    return F0 + (1.0 - F0) * pow(max(1.0 - cosTheta, 0.0), 5.0);\n}\n\nvoid main() {\t\t\n    vec3 N = normalize(Normal);\n    vec3 V = normalize(camPos - WorldPos);\n\n    // 1. 计算基础反照率：根据金属度来计算高光色是折射的固有色还是反射的高光色\n    //    在菲涅尔反射中作为某类材质的固定参数使用\n    vec3 F0 = vec3(0.04); \n    F0 = mix(F0, albedo, metallic);\n\n    // 2. 前向渲染：使用双向反射分布函数 BRDF，累计处理每个光源的光照强度\n    vec3 Lo = vec3(0.0);\n    for(int i = 0; i < 4; ++i) {\n      \n        // 2.1 根据光体积，计算光源的光照强度\n        vec3 L = normalize(lightPositions[i] - WorldPos);\n        vec3 H = normalize(V + L);\n        float distance = length(lightPositions[i] - WorldPos);\n        float attenuation = 1.0 / (distance * distance);\n        vec3 radiance = lightColors[i] * attenuation;\n\n        // 2.2 计算双向反射分布函数的 Cook-Torrance\n        float NDF = DistributionGGX(N, H, roughness);   \n        float G   = GeometrySmith(N, V, L, roughness);      \n        vec3  F   = fresnelSchlick(clamp(dot(H, V), 0.0, 1.0), F0);\n           \n        vec3 nominator    = NDF * G * F; \n        float denominator = 4 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0);\n        // 避免在 NdotV=0.0 or NdotL=0.0 情况下出现除零错误\n        vec3 specular = nominator / max(denominator, 0.001); \n        \n        // 2.3 计算光的辐射率强度，不用 Blinn-Phone 因为它不遵循能量守恒，更像是 BRDF 的替代简化版\n        float NdotL = max(dot(N, L), 0.0);  \n      \n        // 2.4 计算反射和折射系数\n        // kS 镜面反射强度：源于菲涅尔方程\n        vec3 kS = F;\n        // kD 漫反射强度（折射强度）：1.0 - 高光反射\n        // 这个能量守恒总量是 1.0，要大于 1.0 除非是自发光物体\n        vec3 kD = vec3(1.0) - kS;\n        // kD 要考虑金属材质：因为金属不会折射光线，因此不会有漫反射\n        kD *= 1.0 - metallic;\t        \n\n        // 4. 计算出射光的反射强度总量\n        // Cook-Torrance 方程中的 F 就是 ks，因此方程的结果 specular 已经计入了 ks，不需要再次乘以 ks\n        Lo += (kD * albedo / PI + specular) * radiance * NdotL;\n    }   \n    \n    // 环境光照强度（将会被 IBL 基于图像的环境光代替）\n    vec3 ambient = vec3(0.03) * albedo * ao;\n\n    vec3 color = ambient + Lo;\n    color = color / (color + vec3(1.0)); // HDR 色调映射\n    color = pow(color, vec3(1.0/2.2)); \t // gamma 矫正\n  \n    FragColor = vec4(color, 1.0);\n}\n\n// 方法二：根据贴图计算 FS\nuniform sampler2D normalMap;\nuniform sampler2D albedoMap;\nuniform sampler2D metallicMap;\nuniform sampler2D roughnessMap;\nuniform sampler2D aoMap;\n\n// ...\n\n// 将法线向量从 切线空间 转换为 世界空间\nvec3 getNormalFromMap() {\n    vec3 tangentNormal = texture(normalMap, TexCoords).xyz * 2.0 - 1.0;\n\n    vec3 Q1  = dFdx(WorldPos);\n    vec3 Q2  = dFdy(WorldPos);\n    vec2 st1 = dFdx(TexCoords);\n    vec2 st2 = dFdy(TexCoords);\n\n    vec3 N   = normalize(Normal);\n    vec3 T  = normalize(Q1*st2.t - Q2*st1.t);\n    vec3 B  = -normalize(cross(N, T));\n    mat3 TBN = mat3(T, B, N);\n\n    return normalize(TBN * tangentNormal);\n}\n\n// ...\n\nvoid main() {\t\t\n    // 从纹理中获取多变的材质贴图\n    // albedo 从贴图的非线性 sRGB 空间转化为线性的 RGB 空间\n    vec3 albedo     = pow(texture(albedoMap, TexCoords).rgb, vec3(2.2));\n    float metallic  = texture(metallicMap, TexCoords).r;\n    float roughness = texture(roughnessMap, TexCoords).r;\n    float ao        = texture(aoMap, TexCoords).r;\n\n    vec3 N = getNormalFromMap();\n    vec3 V = normalize(camPos - WorldPos);\n    \n    vec3 F0 = vec3(0.04); \n    F0 = mix(F0, albedo, metallic);\n\n    // 反射方程\n    vec3 Lo = vec3(0.0);\n    for(int i = 0; i < 4; ++i) {\n        // ...\n    }   \n    \n    // ...\n}\n```\n\n\n\n\n\n# 四、屏幕色彩校正（后处理）\n\n**后处理体积** *Post Processing Volume*：为了能在三维空间能更清楚的限制后处理范围，通过设置后处理体积（和场景内物体求交，排除多余的物体）来提高后处理效率，明确后处理对象范围\n\n\n\n## 1. 自动曝光（眼部适应）\n\n曝光：单位时间接收到的光的辐通量\n自动曝光：可再现人眼适应不同光照条件的体验，例如从昏暗的室内走到明亮的室外，或从室外走到室内\n\n游戏引擎中的自动曝光分别有以下方式：\n\n1. **基本的自动曝光方法**（根据当前屏幕画面实时测光）\n   测光值：计算屏幕内场景亮度对数的平均值\n   对输出到屏幕的纹理做下采样，取当前像素上下左右四个像素的值为平均值（边缘纹理拉伸采样）\n   根据下采样纹理宽高各取一半继续下采样，一次次下采样下来最后得到一个 1x1 的纹理，这个纹理颜色就是当前场景内的平均亮度\n\n2. **根据直方图计算自动曝光**（根据当前屏幕画面实时测光）\n   测光值：计算屏幕内场景亮度对数的直方图，通过分析直方图得出亮度平均值\n   原图像长宽各缩小一半后，通过 GPU 并发的统计这张图的直方图存成纹理，供 CPU 程序使用\n\n3. **手动调节自动曝光**（不测光）\n   通过手动设置：感光度 ISO、快门速度 Shutter Speed、光圈大小 Aperture(F-stop)、曝光补偿 Exposure compensation\n   EV100：表示感光度为 100 时的曝光强度，是计算曝光的基准数值\n   其中如果不考虑[物理相机曝光](../../Blog/base/Part3_Camera.md)的情况，EV100 的值取 0\n   $$\n   \\begin{align}\n   EV100 &= log_2{光圈^2 \\over 快门速度} - log_2{ISO \\over 100}\\\\\n   曝光 &= {1 \\over 2^{(EV100 + 曝光补偿)}} \\\\\\\\\n   屏幕像素 &= 曝光 * 物体经过PBR后的表面亮度\n   \\end{align}\n   $$\n   \n\n## 2. Gamma 矫正\n\n![](./images/gamma_correction_gamma_workflow.png)\n\n作用：我们在应用中配置的亮度和颜色是基于监视器所看到的，这样所有的配置实际上是非线性的亮度/颜色配置\n\n**源起**：\n\n1. 人眼看到的颜色亮度空间变化是**非线性**的\n2. 我们用来记录/展示画面的媒介上，动态范围和灰阶预算是有限的。（无论**纸张**还是屏幕）\n3. **韦伯定律**\n   **人对自然界刺激的感知，是非线性的，外界以一定的比例加强刺激，对人来说，这个刺激是均匀增长的**\n\n早期 CRT 阴极射线管显示器：显示的颜色亮度空间变化和人眼看到的基本相似，也是**非线性**的\n\n不经过 Gamma 矫正的光照是不符合真实显示器的情况（左图 Gamma 矫正后的光照，右图没有经过后处理的原始光照）\n\n![](./images/gamma_correction_light.png)\n\n\n\n**1. Gamma 曲线就是把物理光强和美术灰度做了一个幂函数映射**\nGamma 曲线就是将在显示器选中的颜色经过矫正后成为线性的便于计算，最后通过显示器又显示出来\n曲线如下图：\n\n- 灰色点线：线性颜色/亮度值\n- 红色虚线：gamma 矫正曲线\n- 红色实线：人眼和 CRT 显示器看到的效果\n\n![](./images/gamma_correction_gamma_curves.png)\n\n**2. sRGB 纹理**\n\n是一种非线性纹理，将线性空间的图片经过显示器一样的 gamma 处理后得到的图片\n非线性纹理在进行线性混合时会出现混合错误，原因如下（$x^{1 \\over \\gamma}$ 非线性纹理像素值）\n$$\n0.5 * (x^{1 \\over \\gamma} + y^{1 \\over \\gamma}) < (0.5 * (x+y))^{1 \\over \\gamma}\n$$\n![](./images/gamma_correction_blender.png)\n\n使用方法：\n\n1. 开启 OpenGL 自己的 sRGB 帧缓冲，在颜色存储到缓冲前会先 gamma 2.2 矫正 sRGB 颜色\n\n   ```C++\n   // 开启 sRGB 帧缓冲\n   glEnable(GL_FRAMEBUFFER_SRGB);\n   \n   // 纹理格式设置为 sRGB，这样在读取 sRGB 图片的时候会首先做一个逆向的 gamma 矫正\n   // 防止最后的 sRGB 缓冲统一 gamma 矫正的时候，在这个纹理上进行重复矫正\n   glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);\n   ```\n\n2. 在 frame shader 里自定义 gamma 矫正\n\n   ```c++\n   void main() {\n       // do super fancy lighting \n       [...]\n     \n       float gamma = 2.2;\n       // 1. 对于普通格式的纹理导入了 sRGB 图片，进行反向 gamma 矫正，防止 2. 统一 gamma 矫正时，做了重复的 gamma 矫正\n   \t\tvec3 diffuseColor = pow(texture(diffuseSRGB, texCoords).rgb, vec3(gamma));\n       // 2. 对线性空间的颜色进行 gamma 矫正，让显示器显示的和实际计算的颜色一致\n       fragColor.rgb = pow(fragColor.rgb, vec3(1.0/gamma));\n   }\n   ```\n\n\n\n## 3. High Dynamic Range 高动态范围\n\n**源起**：人眼的工作原理，当光线很弱的啥时候，人眼会自动调整从而使过暗和过亮的部分变得更清晰，就像人眼有一个能自动根据场景亮度调整的自动曝光滑块\n\nHDR 渲染的真正优点在庞大和复杂的场景中应用复杂光照算法会被显示出来\n\n1. 浮点帧缓冲：可以存储超过 0.0 到 1.0 范围的浮点值\n\n   ```c++\n    // GL_RGB16F 格式的浮点帧缓冲\n    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL);  \n   ```\n\n2. **色调映射** Tone Mapping：将所有的浮点颜色通过一些方法映射到 <u>Low Dynamic Range</u> 0.0 - 1.0 的范围中，是一种模拟胶片对光线反应的方法，该方法要符合[学院色彩编码系统（ACES）](http://www.oscars.org/science-technology/sci-tech-projects/aces)针对电视和电影设定的行业标准\n\n   ```c++\n   uniform float exposure; // 无确定范围，曝光值\n                           // 越高：暗部细节越多\n                           // 越低：亮部细节越多\n   void main() {\n       const float gamma = 2.2;\n       vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb;\n   \n       // Tone Mapping 方法 1: Reinhard 色调映射\n       // 分散整个 HDR 颜色值到 LDR 颜色值上，所有的值都有对应\n       vec3 mapped = hdrColor / (hdrColor + vec3(1.0));\n       // Tone Mapping 方法 2: 曝光色调映射\n       vec3 mapped = vec3(1.0) - exp(-hdrColor * exposure);\n   \n       // Gamma 校正\n       mapped = pow(mapped, vec3(1.0 / gamma));\n   \n       color = vec4(mapped, 1.0);\n   }  \n   ```\n\n\n\n## 4. 颜色查找表 LUT\n\n颜色查找表 ([LookUp Table](https://zhuanlan.zhihu.com/p/43241990))：将一种颜色映射为另一种颜色，可以提供更精细的色彩变换，从而可用于去饱和度之类的用途\n\n注意：LUT 发生在低动态范围（LDR）中以及在 sRGB 空间中输出到显示器的最终图像颜色上，所以它只是与显示器支持对应的一张适时的快照，**不一定在它输出到的所有显示器上都呈现相同外观**\n\n下图为 256 * 1 像素的渐进纹理和其对应的效果\n\n![](./images/LookupTable.png)\n\n\n\n\n\n# 五、离线渲染\n\n## 1. 光线追踪 Ray Tracing\n\n优点：真实，多用于离线渲染\n缺点：计算量大\n\n前提：\n\n- **假设**光线近似直线传播\n\n- **假设**光线交叉后仍然互不影响\n\n- 光路可逆：从光源到人眼的路径 == 从人眼到光源\n\n  \n\n### 1.1 Whitted-Style Ray Tracing\n\n方法\n\n1. 从相机出发，向场景投射光线\n2. 将场景进行合理分割，方便快速找到光线与物体的相交点\n3. 判断光线与距离相机最近的地方相交(反射)，在相交处计算物体颜色\n4. 光线会折射多次，在每一次折射点计算颜色值\n   ![](./images/ray_tracing.png)\n\n\n\n### 1.2 渲染方程推导\n\n![](./images/ray_tracing_rendering_equation.png)\n\n折射点渲染方程推导：\n\n1. 考虑**自发光物体 Emission** 的光照\n\n2. 考虑多个光源的光照\n\n3. 考虑到面光源，将**累加 sum** 替换为**积分 integral** 更准确\n\n4. 考虑到其他物体反射的光线（**间接光照 inter reflection**）\n\n5. 渲染方程化简\n   $$\n   \\begin{align}\n   设:\\\\\n   E &= L_e(x, \\omega_r)\\\\\n   L &= L_r(x, \\omega_r) =L_i(x, \\omega_i)\\\\\n   K &= \\int_{\\Omega}f(x,\\omega_i, \\omega_r) \\cos \\theta_i d\\omega_i \\\\\n   则 \\space 渲染方程简化为：\\\\\n   L &= E + KL \\\\\n   L - KL &= E \\\\\n   (I - K)L &= E \\\\\n   L &= (I - K)^{-1} E \\\\\n   L &= (I + K + K^2 + K^3 + ...)E \\\\\n   L &= E + KE + K^2E+ K^3E + ... \\\\\n   其中：\\\\\n   直接光照 &= KE \\\\\n   间接光照 &= K^2E \\\\\n   二次间接光照 &= K^3E \\\\\n   ...\n   \\end{align}\n   $$\n\n\n\n## 2. 路径追踪 Path Tracing\n\n\n\n\n\n\n\n# Reference\n\n- [概率密度函数(PDF)](https://www.jianshu.com/p/70b188d512aa)\n- [Render Hell !!!!!](https://simonschreibt.de/gat/renderhell-book1/)\n- [3D C/C++ tutorials](http://www.3dcpptutorials.sk/index.php)[3D C/C++ tutorials](http://www.3dcpptutorials.sk/index.php)\n- [smallpt: Global Illumination in 99 lines of C++](http://www.kevinbeason.com/smallpt/)\n- [Physically Based Rendering](http://www.codinglabs.net/article_physically_based_rendering.aspx)\n- [Physically Based Rendering - Cook–Torrance](http://www.codinglabs.net/article_physically_based_rendering_cook_torrance.aspx)\n- [Rendering the world of Far Cry 4](http://www.gdcvault.com/play/1022235/Rendering-the-World-of-Far)\n- [SIGGRAPH 2014 Moving Frostbite to PBR - Frostbite](https://www.ea.com/frostbite/news/moving-frostbite-to-pb)\n- [The Mathematics of Shading](https://www.scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/mathematics-of-shading)\n- [Physically Based Shading and Image Based Lighting](https://www.trentreed.net/blog/physically-based-shading-and-image-based-lighting/)\n- [Disney animation papers](https://www.disneyanimation.com/technology/publications/#papers)\n- [PHYSICALLY-BASED RENDERING REVOLUTIONIZES PRODUCT DEVELOPMENT](https://pny.com/File Library/Unassigned/Moor-Whitepaper-Download.pdf)\n- [A MultiAgent System for Physically based Rendering Optimization](http://www.weiss-gerhard.info/publications/D02.pdf)\n- [Physically Based Shading on Mobile](https://medium.com/spaceapetech/physically-based-shading-on-mobile-d7d4e90bb4bd)\n- [Applying Visual Analytics to Physically-Based Rendering](http://cg.ivd.kit.edu/publications/2018/visual_analytics_pbr/preprint.pdf)\n- [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)\n- [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)\n- [SubSurface Profile Shading Model](https://docs.unrealengine.com/en-us/Engine/Rendering/Materials/LightingModels/SubSurfaceProfile)\n- [Physically Based Materials in Unreal Engine 4](https://docs.unrealengine.com/en-us/Engine/Rendering/Materials/PhysicallyBased)\n- [A Multi-Ink Color-Separation Algorithm Maximizing Color Constancy](https://pdfs.semanticscholar.org/9e56/8b13ea51ca3c669186624566f672eb547857.pdf)\n- [Unidirectional Reflectance of Imperfectly Diffuse Surfaces](https://www.onacademic.com/detail/journal_1000035238254910_7744.html#)\n- [Adopting a physically based shading model](https://seblagarde.wordpress.com/2011/08/17/hello-world/)\n- [SaschaWillems / Vulkan-glTF-PBR](https://juejin.im/repo/5a8127a4f265da02d800abba)\n- [The Beginner’s Guide to Physically Based Rendering in Unity](https://blog.teamtreehouse.com/beginners-guide-physically-based-rendering-unity)\n- [Image Based Lighting](https://chetanjags.wordpress.com/2015/08/26/image-based-lighting/)\n- [Using Image Based Lighting (IBL)](https://www.indiedb.com/features/using-image-based-lighting-ibl)\n- [Converting a Cubemap into Equirectangular Panorama](https://stackoverflow.com/questions/34250742/converting-a-cubemap-into-equirectangular-panorama)\n- [Does PBR incur a performance penalty by design?](https://computergraphics.stackexchange.com/questions/1568/does-pbr-incur-a-performance-penalty-by-design)\n- [Lec 2: Shading Models](http://www.cs.cornell.edu/courses/cs5625/2013sp/lectures/Lec2ShadingModelsWeb.pdf)\n- [Unity Blog !!](https://blog.unity.com/)\n- [Unity_Shaders_Book](https://github.com/candycat1992/Unity_Shaders_Book)\n- [使用顶点投射的方法制作实时阴影](https://zhuanlan.zhihu.com/p/31504088)\n- [弧长和曲面面积](https://blog.csdn.net/sunbobosun56801/article/details/78657455)\n- [深入浅出基于物理的渲染一](https://zhuanlan.zhihu.com/p/33630079)\n- [Unity 手册/图形/图形概述](https://docs.unity3d.com/cn/current/Manual/RenderingPaths.html)\n- [彻底看懂 PBR/BRDF 方程](https://zhuanlan.zhihu.com/p/158025828)\n- [PBR 材质系统原理简介](https://blog.csdn.net/weixin_42660918/article/details/80989738)\n- [BRDF 材质贴图](https://blog.csdn.net/mconreally/article/details/50629098)\n- [BRDF（双向反射分布函数）](https://zhuanlan.zhihu.com/p/21376124)\n- [PBR 材质基础概念，限制及未来发展](https://blog.csdn.net/qq_42145322/article/details/100621811)\n- [【Unity】Compute Shader 计算 BRDF 存储到纹理](https://www.cnblogs.com/jaffhan/p/7389450.html)\n- [Create icosphere mesh by code](http://blog.andreaskahler.com/2009/06/creating-icosphere-mesh-in-code.html)\n- [低差异序列（一）- 常见序列的定义及性质](https://zhuanlan.zhihu.com/p/20197323?columnSlug=graphics)\n- [【图形学】我理解的伽马校正（Gamma Correction）](https://blog.csdn.net/candycat1992/article/details/46228771/)\n- [A Standard Default Color Space for the Internet - sRGB](https://www.w3.org/Graphics/Color/sRGB)\n- [为什么线性渐变的填充，直方图的两头比中间高？ - 黄一凯的回答 - 知乎](https://www.zhihu.com/question/61996849/answer/193452971)\n\n"
  },
  {
    "path": "ComputerGraphics(OpenGL)/Part3_Texture.md",
    "content": "# 一、颜色插值\n\n根据三角形三个顶点的颜色来求整个三角面的插值颜色\n\n## 1. 普通的插值方法\n\n方法一：根据**高度比**来插值\n\n![](./images/linear_interpolate_triangle.png)\n$$\n\\begin{align}\nf_i &= d_i / h_i \\\\\nColor &= f_i * Color_i + f_j * Color_j + f_k * Color_k\n\\end{align}\n$$\n\n\n方法二：根据所占**三角形面积**来插值\n$$\n\\begin{align}\nf_i &= {area(x, x_j, x_k) \\over area(x_i, x_j, x_k)} \\\\\nColor &= f_i * Color_i + f_j * Color_j + f_k * Color_k\n\\end{align}\n$$\n\n\n\n## 2. 在三角形的重心坐标系下做插值\n\n在三个顶点组成的平面内，方法如下：\n\n1. 将普通笛卡尔坐标系转化为重心坐标系\n2. 根据重心坐标计算**不受透视投影影响**的三个插值系数\n   这三个插值系数可以对当前三个顶点的任意属性进行插值\n\n\n\n### 2.1 计算重心坐标系\n\n![](./images/barycentric.jpg)\n\n设 P 为 2D 空间内三角形 ABC 内任意一点，求三角形 ABC 以 AB，AC 为坐标轴的重心坐标 u, v\n$$\n\\begin{align}\n\\vec {AP} &= u \\vec {AB} + v \\vec {AC} \\\\\nA-P &= u(A-B) + v(A-C) \\\\\nP &= (1-u-v)A + uB + vC \\\\\\\\\n\n\\vec {AP} &= u \\vec {AB} + v \\vec {AC} \\\\\nu \\vec {AB} + v \\vec {AC} + \\vec {PA} &= 0 \\\\\nu (\\vec {AB})_x + v (\\vec {AC})_x + (\\vec {PA})_x &= 0 \\\\\nu (\\vec {AB})_y + v (\\vec {AC})_y + (\\vec {PA})_y &= 0 \\\\\n\\begin{bmatrix}u & v & 1 \\end{bmatrix} \n\\begin{bmatrix}(\\vec {AB})_x \\\\ (\\vec {AC})_x \\\\ (\\vec {PA})_x \\end{bmatrix} &= 0 \\\\\n\\begin{bmatrix}u & v & 1 \\end{bmatrix} \n\\begin{bmatrix}(\\vec {AB})_y \\\\ (\\vec {AC})_y \\\\ (\\vec {PA})_y \\end{bmatrix} &= 0 \\\\\n\\begin{bmatrix}(\\vec {AB})_x \\\\ (\\vec {AC})_x \\\\ (\\vec {PA})_x \\end{bmatrix} \\times \n\\begin{bmatrix}(\\vec {AB})_y \\\\ (\\vec {AC})_y \\\\ (\\vec {PA})_y \\end{bmatrix} &= \\begin{bmatrix}u \\\\ v \\\\ 1 \\end{bmatrix} \\\\\\\\\n\nu \\vec {AB} + v \\vec {AC} + \\vec {PA} &= 0 \\\\\na \\vec {AB} + b \\vec {AC} + c \\vec {PA} &= 0 &u = {a \\over c}, v ={b \\over c} \\\\\nP &= (1-u-v)A + uB + vC \\\\\nP &= (1-{a \\over c}-{b \\over c})A + {a \\over c}B + {b \\over c}C\n\\end{align}\n$$\n编码为\n\n```c\n// 将屏幕上的笛卡尔坐标系转换为 ABC 三角形内的重心坐标系\nvec3 barycentric(vec2 A, vec2 B, vec2 C, vec2 P) {\n    vec3 s[2];\n    for (int i=2; i--; ) {\n        s[i][0] = B[i]-A[i];\n        s[i][1] = C[i]-A[i];\n        s[i][2] = A[i]-P[i];\n    }\n    vec3 u = cross(s[0], s[1]);\n    if (0 == std::abs(u.z)) return vec3(-1,1,1);\n        \n    return vec3(1.f-(u.x+u.y)/u.z, u.x/u.z, u.y/u.z);\n}\n```\n\n\n\n### 2.2 根据重心坐标系计算插值系数\n\n**注意：**\n\n- 以下使用的深度都是<u>线性深度</u>，实际存储的是非线性深度，需要转换一下\n- 对于非线性深度的插值，必须是<u>非透视矫正</u>的插值系数\n\n\n\n已知\n\n- 透视投影后 2D 屏幕空间的 三角形 ABC 的深度为 $Z_{P'} = \\alpha' Z_{A'} + \\beta' Z_{B'} +  \\gamma' Z_{C'}$\n- $\\alpha' + \\beta' + \\gamma' = 1$\n\n求：透视投影前 3D 裁剪空间的 三角形 ABC 的深度为 $Z_P = \\alpha Z_A + \\beta Z_B +  \\gamma Z_C$\n$$\n\\begin{align}\n1 &= \\alpha' + \\beta' + \\gamma' \\\\\n{Z_P \\over Z_P} &= {Z_A \\over Z_A}\\alpha' + {Z_B \\over Z_B}\\beta' + {Z_C \\over Z_C}\\gamma' \\\\\n{Z_P \\over Z_P} &=\n\\begin{bmatrix}Z_A & Z_B & Z_C\\end{bmatrix}\n\\begin{bmatrix}{1 \\over Z_A}\\alpha' \\\\ {1 \\over Z_B}\\beta' \\\\ {1 \\over Z_C}\\gamma' \\end{bmatrix} \\\\\nZ_P &=\n\\begin{bmatrix}Z_A & Z_B & Z_C\\end{bmatrix}\n\\begin{bmatrix}{1 \\over Z_A}\\alpha' \\\\ {1 \\over Z_B}\\beta' \\\\ {1 \\over Z_C}\\gamma' \\end{bmatrix} Z_P \\\\\nZ_P &=\n\\begin{bmatrix}Z_A & Z_B & Z_C\\end{bmatrix}\n\\begin{bmatrix}{Z_P \\over Z_A}\\alpha' \\\\ {Z_P \\over Z_B}\\beta' \\\\ {Z_P \\over Z_C}\\gamma' \\end{bmatrix}\\\\\nZ_P &=\n\\begin{bmatrix}Z_A & Z_B & Z_C\\end{bmatrix}\n\\begin{bmatrix}\\alpha \\\\ \\beta \\\\ \\gamma \\end{bmatrix}\\\\\n\\\\\n\\alpha + \\beta + \\gamma &= 1\\\\\n{Z_P \\over Z_A}\\alpha' + {Z_P \\over Z_B}\\beta' + {Z_P \\over Z_C}\\gamma' &= 1\\\\\nZ_P &= {1 \\over {{\\alpha' \\over Z_A} + {\\beta' \\over Z_B} + {\\gamma' \\over Z_C}}}\n\\\\\n\n\\end{align}\n$$\n要通过这些插值其他属性的值 I，则\n$$\n\\begin{align}\nI_P &= \\begin{bmatrix} I_A & I_B & I_C \\end{bmatrix}\n\\begin{bmatrix} \\alpha \\\\ \\beta \\\\ \\gamma \\end{bmatrix}\\\\\n&= \\begin{bmatrix} I_A & I_B & I_C \\end{bmatrix}\n\\begin{bmatrix} {Z_P \\over Z_A}\\alpha' \\\\ {Z_P \\over Z_B}\\beta' \\\\ {Z_P \\over Z_C}\\gamma'\\end{bmatrix}\\\\\n&= \\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}\n\\begin{bmatrix} \\alpha' \\\\ \\beta' \\\\ \\gamma' \\end{bmatrix}\\\\\n&= ({\\alpha' \\over Z_A}I_A + {\\beta' \\over Z_B}I_B + {\\gamma' \\over Z_C}I_C)Z_P \\\\\n&= ({\\alpha' \\over Z_A}I_A + {\\beta' \\over Z_B}I_B + {\\gamma' \\over Z_C}I_C) / {1 \\over Z_P} \\\\\n&= { {\\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}} }\n\\end{align}\n$$\n\n\n\n# 二、纹理基础\n\n纹理材质的反光性质\n\n- 各项异性：固定视角和光源方向旋转表面时，反射会发生任何改变\n- 各项同性：固定视角和光源方向旋转表面时，反射不会发生任何改变\n\n\n\n纹理映射坐标\n\n- 所有的纹理尺寸都会映射在 [0, 1] 的范围\n  顶点着色器使用的是 uv 纹理坐标（坐标值为原始图片大小的值）\n  片段着色器使用的是 st 纹理坐标（坐标值为归一化以后的值）\n- OpenGL、Unity 纹理坐标原点在 左下角\n- DirectX 纹理坐标原点在 左上角\n\n\n\n纹理的尺寸\n\n- 长宽大小应该是 2 的幂\n  非 2 的幂的纹理会占用更多的内存空间和读取时间，有些平台会不支持非 2 的幂尺寸的纹理\n- 纹理可以是非正方形的\n\n\n\n## 1. 纹理环绕（坐标包装）\n\n> 当**纹理坐标超出默认范围**时，每种纹理环绕方式都有不同的视觉效果输出\n\nOpenGL 设置纹理不同坐标轴的环绕方式\n\n```c\nglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_NEAREST); //纹理坐标 s/u/x 轴的包装格式\nglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_LINEAR);  //纹理坐标 t/v/y 轴的包装格式\n```\n\n![](images/texture_wrapping.png)\n\n| 环绕方式           | 描述                                                         |\n| ------------------ | ------------------------------------------------------------ |\n| GL_REPEAT          | 对纹理的默认行为，重复纹理图像                               |\n| GL_MIRRORED_REPEAT | 和 GL_REPEAT 一样，但每次重复图片是镜像放置的                |\n| GL_CLAMP_TO_EDGE   | 纹理坐标会被约束在 0 ～ 1之间，超出的部分会重复纹理坐标的边缘，产生一种边缘被拉伸的效果 |\n| GL_CLAMP_TO_BORDER | 超出的坐标处的纹理为用户指定的边缘颜色                       |\n\n\n\n## 2. 纹理过滤（采样）\n\n> 纹素，纹理单元：在**纹理坐标系**中一个像素占用的纹理数据\n>\n> 当三维空间里面的多边形，变成二维屏幕上的一组像素的时候，对每个像素需要到相应纹理图像中进行采样一个像素 Pixel 对应 N 个纹素 Texel 的映射过程就称为纹理过滤 \n\n**纹理过滤的两种情况**\n\n- 纹理被缩小 `GL_TEXTURE_MIN_FILTER`：**一个像素对应多个纹素**\n  现象：走样问题，摩尔纹（远） + 锯齿（近）\n  解决方法：超采样、Mipmap、各向异性过滤 Anisotropic\n  例，一个 8 X 8 的纹理贴到远处正方形上，最后在屏幕上占了 2 X 2 个像素矩阵\n- 纹理被放大 `GL_TEXTURE_MAG_FILTER`：**一个纹素对应多个像素**\n  现象：模糊 / 锯齿\n  解决方法：插值，Nearest、Bilinear\n  例，一个 2 X 2 的纹理贴到近处正方形上，最后在屏幕上占了 8 X 8 个像素矩阵\n\n\nOpenGL 中针对放大和缩小的情况的设置\n\n```c\nglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); //缩小\nglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);  //放大\n```\n\n\n\n### 2.1 邻近点采样 Nearest neighbor\n\n优点：效率最高\n缺点：效果最差\n\n方法：选择最接近中心点纹理坐标的 **1 个纹理单元**采样\n\n![](images/texture_nearest.png)\n\n\n\n### 2.2 双线性过滤 Bilinear\n\n优点：适于处理有一定精深的静态影像\n缺点：不适用于绘制动态的物体，当三维物体很小时会产生深度赝样锯齿 (Depth Aliasing artifacts)\n\n方法：选择最接近中心点纹理坐标的 2 X 2 纹理单元矩阵进行采样，取 **4 个纹理单元**采样的插值\n另外还有种类似的方法 **Bicubic**，取附近 **16** 个纹理单元采样的插值\n\n![](images/texture_linear.png)\n\n\n\n### 2.3 多级渐远纹理过滤 Mipmap \n\nMigmap 用来对同一纹理生成多个不同尺寸的纹理，用 *Level of Detail* (**LOD**) 来规定纹理缩放的大小\nLOD 0 为原始尺寸，从 LOD 1 开始，LOD n 的纹理宽高为 LOD n-1 的一半，直到纹理的大小缩放为 1 X 1 为止\n\n距观察者的距离超过一定的阈值，OpenGL会使用不同的多级渐远纹理，即最适合物体的距离的那个。由于距离远，解析度不高也不会被用户注意到\n\n优点：效果最好，适用于动态物体或景深很大的场景\n缺点：效率低，会占用一定的空间，只能用于纹理被缩小的情况\n\n![](./images/texture_mipmapping.png)\n\n开启 Mipmap 下的纹理采样\n\n![](./images/texture_mipmap.png)\n\n例，三线性过滤 Trilinear 方法：\n\n1. 取 Mipmap 纹理中距离与当前屏幕上尺寸相近的两个纹理\n\n2. 将 1 中选取的纹理 选择最接近中心点纹理坐标的 2 X 2 纹理单元矩阵进行采样（线性过滤）\n\n3. 将 2 中两次采样的结果进行加权平均（**8 个纹理单元**采样），得到最后的采样数据\n\n\n\n**Mipmap Level 计算**\n取 X 轴和 Y 轴的最大变化长度 L 作为查询的输入参数，通过 $d = log_2L$​​ 得到 Mipmap 的查询层级 Level d\n\n**Mipmap 采样**\n得到的层级 d 是个小数，分别向上和向下取整，得到两个不同 level 的 Mipmap 像素值，然后在根据 d 的小数部分做插值\n\n<img src=\"./images/conpute_mipmaplevel.png\" style=\"zoom:150%;\" />\n\n\n\n### 2.4 各向异性过滤 Anisotropic\n\n> 之前提到的三种过滤方式，默认纹理在 x，y 轴方向上的缩放程度是一致的（纹理表面刚好正对着摄像机）\n> 当纹理在 3D 场景中，纹理表面刚倾斜于虚拟屏幕平面时，出现一个轴的方向纹理放大，一个轴的方向纹理缩小的情况（**OpenGL 判定为纹理缩小**）需要使用各向异性过滤配合以上三种过滤方式来达到最佳的效果\n\n优点：效果最好，使画面更加逼真\n缺点：效率最低，由硬件实现\n\n各向异性过滤包含会生成一张包含 Mipmap 的图 Ripmap，如下图（对角线的集合是 Mipmap）\n各向异性过滤也只是覆盖了大部分情况，另一种方法 EWA filtering 多重椭圆形采样效果更好 \n\n![](./images/ripmap.png)\n\n方法：根据视角对梯形范围内的纹理采样\n\n1. 确定 X、Y 方向的**采样比例（Ripmap 的采样范围 N x N）**\n   ScaleX = 纹理的宽 / 屏幕上显示的纹理的宽\n   ScaleY = 纹理的高 / 屏幕上显示的纹理的高\n   异向程度 N = max(ScaleX, ScaleY) / min(ScaleX, ScaleY);\n\n   例，64 X 64的纹理最后投影到屏幕上占了128 X 32 的像素矩阵\n   ScaleX = 64.0 / 128.0 = 0.5;\n   ScaleY = 64.0 / 32.0 = 2.0;\n   异向程度 N = 2.0 / 0.5 = 4;\n\n2. 根据采样比例分别在 X、Y 方向上采用 *三线性过滤* 或 *双线性过滤* 获得采样数据，**采样的范围由异向程度决定，不是原来的 2 X 2 像素矩阵**\n\n   例，64 X 64 的纹理最后投影到屏幕上占了 128 X 32 的像素矩阵\n   异向程度为 4，且在 缩放方面 X 轴 > Y 轴，所以 X 轴采样 2 个像素，Y 轴采样 2 * 异向程度 = 8 个像素\n   采样范围为最接近中心点纹理坐标的 2 X 8 的像素矩阵\n\n![](./images/texture_anisotropic.png)\n\nOpenGL 中设置各向异性过滤\n\n```c\nglTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 异向程度);\n```\n\n各向异性对比三线性\n\n![](images/texture_anisotropic.jpg)\n\n\n\n### 2.4 多级渐远纹理过滤 Mipmap\n\n![](./images/texture_mipmap.jpg)\n\nMigmap 用来对同一纹理生成多个不同尺寸的纹理，用 *Level of Detail* (**LOD**) 来规定纹理缩放的大小\nLOD 0 为原始尺寸，从 LOD 1 开始，LOD n 的纹理宽高为 LOD n-1 的一半，直到纹理的大小缩放为 1 X 1 为止\n\n距观察者的距离超过一定的阈值，OpenGL会使用不同的多级渐远纹理，即最适合物体的距离的那个。由于距离远，解析度不高也不会被用户注意到\n\n优点：效果最好，适用于动态物体或景深很大的场景\n缺点：效率低，会占用一定的空间，只能用于纹理被缩小的情况\n\n![](./images/texture_mipmapping.png)\n\n开启 Mipmap 下的纹理采样\n\n![](./images/texture_mipmap.png)\n\n例，三线性过滤 Trilinear 方法：\n\n1. 取 Mipmap 纹理中距离与当前屏幕上尺寸相近的两个纹理\n\n2. 将 1 中选取的纹理 选择最接近中心点纹理坐标的 2 X 2 纹理单元矩阵进行采样（线性过滤）\n\n3. 将 2 中两次采样的结果进行加权平均（**8 个纹理单元**采样），得到最后的采样数据\n\n\n\nMipMap Level 计算\n\n<img src=\"./images/conpute_mipmaplevel.png\" style=\"zoom:150%;\" />\n\n\n\n### 2.5 圆内均匀随机采样\n\n![](./images/sample.png)\n\n1. 一般圆内随机采样的方式如图 2，其中 a，b 为均匀的随机数\n   这种变换的局部性保持很差，如果两点在笛卡尔坐标系下连续，那么投影到圆后的两个点同样是连续的。但是，反过来就不一定成立了。\n\n$$\n\\begin{align}\nr &= \\sqrt a \\\\\n\\theta &= 2 \\pi \\space b \\\\\\\\\n(u, v) &= (r\\cos \\theta, r\\sin \\theta) \\\\\n\\end{align}\n$$\n\n2. 一种较好的改进方式如图 3，其中 a，b 为均匀的随机数\n   具体论证见论文 [ 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)\n\n$$\n\\begin{align}\nr &= \\sqrt a \\\\\n\\theta &= {\\pi \\space b  \\over 4 \\space a}\n\\end{align}\n$$\n\n3. [泊松圆盘采样 Poisson Disk](https://bost.ocks.org/mike/algorithms/#sampling)\n   由于采样计算方法复杂，大家多用查表法来提前存储采样结果，然后再用旋转提前存储采样点的方式得到伪随机采样点的坐标![](./images/sample_poisson_disk.png)\n\n   ```c++\n   glm::vec2 sample(int numCandidates, const std::vector<glm::vec2>& samples) \n   {\n     float bestDistance = 0;\n     glm::vec2 bestCandidate;\n     for (int i = 0; i < numCandidates; ++i) {\n       glm::vec2 c(Math.random() * width, Math.random() * height);\n       float d = glm::distance(findClosest(samples, c), c);\n       if (d > bestDistance) {\n         bestDistance = d;\n         bestCandidate = c;\n       }\n     }\n   \n     return bestCandidate;\n   }\n   ```\n\n\n\n## 3. 反走样 Anti-Aliasing\n\n> 香农定理告诉我们，即便我们有无限的频率，无论对原信号采样多少次，总会在重建信号时有一些误差\n\n通过增加采样质量来反走样，是适用于各个渲染场景的唯一通用方案\n\n\n### 3.1 SSAA\n\n超采样抗锯齿 Super Sample Anti-aliasing, SSAA\n\n- 步骤：渲染一张比显示的纹理更高分辨率的帧缓冲，分辨率下采样到正常的分辨率\n- 缺点：性能开销很大\n\n\n\n### 3.2 MSAA\n\n多重采样抗锯齿 Multisample Anti-aliasing, MSAA\n\n- 步骤：将单一的采样点变为多个采样点（采样点的数量可以是任意的）\n  不再使用像素中心的单一采样点，而是以特定图案排列的 4 个子采样点(Subsample)\n  (4 个以上采样点的效果差别不大) \n  由顶点插值得到的像素颜色，会存储在**被图形遮盖住**的**每个**子采样点中，最终的像素颜色是子采样点的平均值\n  如果不想以平均计算子采样颜色的方式，OpenGL 允许我们在 FS 阶段获取到每个子采样点的颜色并计算最终采样结果\n- 缺点：颜色缓冲的大小会随着子采样点的增加而增加\n\n![](./images/trick_anti_aliasing_sample.png)\n\n### 3.3 FXAA\n\n\n\n\n\n### 3.4 TAA\n\n\n\n\n\n\n\n#  三、遮罩贴图\n\n- 保护纹理的某些区域，使它们免于修改\n- 主要用与控制光照，使同一个纹理的模型不同的角度拥有了不同的高光强度\n- 一般为单通道纹理，不过有时候一张 RGBA 四通道的遮罩纹理可以控制 四种 表面属性的强度\n- 使用方式为：物体的颜色 = 当前纹理坐标对应的遮罩纹理强度 * 光照计算后的颜色\n\n\n\n## 1. Opacity 透明贴图\n\n贴图的不透明度：黑色是透明的部分，白色为不透明的部分，灰色为半透明的部分\n\n\n\n## 2. Ambient Occlusion 环境遮挡贴图\n\n环境光遮蔽贴图属于**预计算的贴图类型**（预先计算好纹理效果，降低实时计算成本）\n模拟物体之间所产生的阴影，在不打光的时候增加体积感\n完全不考虑光线，单纯基于物体与其他物体越接近的区域，受到反射光线的照明越弱这一现象来模拟现实照明（的一部分）效果\n\n白色表示应接受完全间接光照的区域，以黑色表示没有间接光照\n\n\n\n\n\n# 四、微观几何形态存储\n\n\n\n## 1. 凹凸贴图 Bump Map\n\n又称高度贴图：使用一张高度纹理来模拟表面上下高度的位移，存储相对于顶点位置的偏移量（白色区域是高区域，黑色区域是低区域）\n\n- 优点：非常直观，可以从高度纹理中明确的知道一个模型表面的凹凸情况\n- 缺点：**不改变几何信息**，计算较复杂，不能直接得到表面法线，需要通过像素的灰度值计算得到\n  如图，先通过凹凸贴图和顶点位置计算高度值，根据高度值计算表面切线，根据表面切线的垂线得到法线\n  ![](./images/texture_bump_map.png)\n\n\n\n## 2. 位移贴图 Displacement Map\n\n![](./images/texture_displacement.png)\n\n**和凹凸贴图一样**存储相对于顶点位置的偏移量值\n\n- 优点：可直接使用凹凸贴图作为位移贴图，改变几何信息\n- 缺点：相比上更逼真，要求模型足够细致，运算量更高\n  DirectX 有 Dynamic 的插值法，对模型做插值，使得初始不用过于细致\n\n\n\n## 3. 法线贴图 Normal map\n\n法线贴图\n\n- 直接存储表面法线\n- 根据法线所在的坐标空间类型可分为\n  模型空间的法线纹理 (object-space normal map)：将修改后的**模型**空间表面的法线存储在一张纹理中\n  切线空间的法线纹理 (tangent-space normal map)：将修改后的**纹理**切线空间表面的法线存储在一张纹理中\n  **一般使用切线空间的法线纹理**\n\n\n\n法线的模型变换矩阵\n\n- 在顶点坐标的模型变换中，当我们使用一个不等比缩放时，法线不会再垂直于对应的表面\n  ![](./images/normal_transformation.png)\n\n- 法线需要一个基于顶点坐标的模型变换的专门的 模型矩阵\n  如果模型变换 $M_t$ 不是正交变换，则法线变换矩阵为：$M_{n} = (M_t^T)^{-1}$\n  如果模型变换 $M_t$ 是正交变换，则法线变换矩阵为：$M_{n} = M_t$\n  正交变换：旋转变换，[公式的推导过程](../LinearAlgebra/Part1_Matrix.md)\n  由于位移对于法线方向没有影响，而逆变换计算量较大，因此一般采用没有位移的 3 X 3 矩阵来计算法线变换\n\n\n\n### 3.1 使用流程\n\n**I. 顶点信息补充**\n\n   1. 由 模型变换 得到 法线的模型变换矩阵（逆矩阵耗时大，尽量放在 CPU 上算一次或者放在顶点着色器）\n   2. 根据顶点位置和纹理坐标信息，计算**模型空间下的** 切线 和 副切线\n   3. 每三个顶点构成一个平面，他们共享一组 切线 和 副切线\n\n\n\n**II. 顶点着色器**\n\n1. 将顶点数据中的 切线、副切线、法线坐标系位置经过 法线的模型变换矩阵 转换为\n   **世界空间下的** 切线空间坐标，Gram-Schmidt 正交化后构建 切线空间矩阵\n2. 计算世界空间下的光源在 切线空间 的坐标\n\n\n\n**III. 片元着色器**\n\n   1. 根据法线纹理对应的普通纹理的纹理坐标，从法线纹理读取切线空间下的法线数据（像素值）\n   2. 将范围是 [0, 1] 的像素值，转换为范围是 [-1, 1] 的表面法线值：$normal = pixel*2.0 - 1.0$\n   3. 将在**切线空间**下的光源和物体片元的坐标与法线计算得到片元颜色\n\n\n\n### 3.2 切线空间\n\n切线空间的坐标系，原点：模型的顶点\n\n- Z 轴：N（Normal）法线方向（和 Z 轴的正方向始终保持一致）\n- X 轴：T（Tagent）切线方向，和纹理坐标的 X 轴（U）一致\n- Y 轴：B（Bitangent）副切线方向 ，和纹理坐标的 Y 轴（V）一致\n\n![](./images/normal_mapping_tbn.png)\n\n\n\n计算额外的顶点信息：纹理法线 **切线空间** 到 **模型空间** 的矩阵\n\n- 已知切线空间法线纹理的切线 T 和 副切线 B 分别对应与法线纹理对应普通纹理的 U 和 V 坐标轴\n  （此时 T、B 在模型空间下）且点 $P_1$、$P_2$、$P_3$ 与纹理坐标的对应关系如下图，\n  求切线方向 T 和副切线方向 B\n\n  ![](./images/normal_mapping_surface_edges.png)\n\n- 则：\n  $$\n  \\begin{align}\n  E_1 &= \\Delta U_1 T + \\Delta V_1 B\\\\\n  E_2 &= \\Delta U_2 T + \\Delta V_2 B\\\\\n  \\begin{bmatrix} E_1\\\\ E_2 \\end{bmatrix}\n  &= \n  \\begin{bmatrix}\n  \\Delta U_1 & \\Delta V_1\\\\\n  \\Delta U_2 & \\Delta V_2\n  \\end{bmatrix}\n  \\begin{bmatrix} T\\\\ B \\end{bmatrix} \\\\\n  \\begin{bmatrix}\n  \\Delta U_1 & \\Delta V_1\\\\\n  \\Delta U_2 & \\Delta V_2\n  \\end{bmatrix}^{-1}\n  \\begin{bmatrix} E_1\\\\ E_2 \\end{bmatrix}\n  &= \n  \\begin{bmatrix} T\\\\ B \\end{bmatrix} \\\\\n  {1 \\over \\Delta U_1 \\Delta V_2 - \\Delta U_2 \\Delta V_1}\n  \\begin{bmatrix}\n  \\Delta V_2 & -\\Delta V_1\\\\\n  -\\Delta U_2 & \\Delta U_1\n  \\end{bmatrix}\n  \\begin{bmatrix} E_1\\\\ E_2 \\end{bmatrix}\n  &= \n  \\begin{bmatrix} T\\\\ B \\end{bmatrix} \\\\\n  {1 \\over \\Delta U_1 \\Delta V_2 - \\Delta U_2 \\Delta V_1}\n  \\begin{bmatrix}\n  \\Delta V_2 E_1 -\\Delta V_1 E_2\\\\\n  -\\Delta U_2 E_1 + \\Delta U_1 E_2\n  \\end{bmatrix}\n  &= \n  \\begin{bmatrix} T\\\\ B \\end{bmatrix}\n  \\end{align}\n  $$\n\n\n\n**Gram-Schmidt 正交化**\n\n当在更大的网格上计算切线向量的时候，它们往往有很大数量的共享顶点，当法向贴图应用到这些表面时将切线向量平均化（一个三角面平均三个顶点的切向量）通常能获得更好更平滑的结果。但是这样做有个问题，就是TBN向量可能会不能互相垂直，这意味着 TBN 矩阵不再是正交矩阵了\n\n这时需要在**顶点着色器**做正交化操作，让 TBN 回归到正交矩阵\n$$\n\\begin{align}\nN &= normalize(N) \\\\\nT &= normalize(U - dot(U, N) * N) \\\\\nB &= normalize(cross(N, T))\n\\end{align}\n$$\n\n\n\n### 3.3 不同坐标空间的比较\n\n**模型空间**法线纹理的优点：\n\n- 实现简单，更加直观\n- 在纹理坐标的缝合处和尖锐的边角部分，可见的突变（缝隙）较少，边界过渡平滑\n  模型空间的法线纹理存储的是同一坐标系下的法线信息，在边界可将法线通过插值，来实现平滑过渡\n  切线空间的法线依靠纹理坐标的方向得到的结果，会在边缘处或尖锐的地方出现缝合现象\n\n\n\n**切线空间**法线纹理的优点：\n\n- 自由度高，可做 UV 纹理动画\n  可映射到不同的网格上，而模型空间法线纹理只能用于创建他的网格\n- 可以复用法线纹理\n  一个砖块的 6 个面可以共用一张切线空间法线纹理\n- 对于纹理使用的额外数据是 可压缩的\n  可只存储额外的 切线 和 副切线 2 个方向，而模型空间的法线必须存储 3 个方向的值\n\n\n\n## 4. Curvature 曲率贴图\n\n存储凹凸信息：黑色的值代表了凹区域，白色的值代表了凸区域，灰度值代表中性/平地\n\n\n\n## 5. Thickness 厚度贴图\n\n辅助制作表面散射 SSS：黑色代表薄的地方、白色代表厚的地方\n\n\n\n# 五、环境光贴图\n\n环境光贴图 Environment Light Map，存储所有外部方向的环境光照信息（环境光来自无限远处，强度一致，只记录方向）\n\n在游戏引擎里做**场景**地图的时候会用到\n用于静态模型上的间接光照：将场景的光照结果烘培到模型贴图上，从而实现模拟现实光照效果，可以节省硬件资源\n\n## 1. 球形环境贴图 Spherical Environment Map\n\n存储的一个**镜子球反射**的环境光色彩\n球形贴图扭曲拉伸问题严重，贴图存储的环境光照信息不均匀\n\n\n\n## 2. 立方体贴图 Cube Map\n\n贴图扭曲拉伸问题相较于球形环境贴图较轻，但计算量较大\n立方体贴图 GL_TEXTURE_CUBE_MAP \n\n```c\n// shader\nuniform samplerCube skybox;\n\n// CPU code\nglBindTexture(GL_TEXTURE_CUBE_MAP, _texID);\nglTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, ...);\nglTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);\n```\n\n- 包含了 6 个 2D 纹理的纹理，每个 2D 纹理都组成了立方体的一个面，它通过一个方向向量来进行采样\n  **方向向量的大小并不重要，只要提供了方向**\n\n- 纹理坐标：一般为世界坐标系下的顶点坐标（范围 -1，1）\n  处于世界坐标系下，是一个由立方体中心出发指向立方体面的三维向量\n  贴图的顺序一般为：<u>右、左、上、下、前、后</u>\n\n  ![](./images/texture_cube_map.png)\n\n### 2.1 天空盒 Skybox\n\n天空盒：包含了整个场景的（大）立方体，它包含周围环境的 6 个图像，让玩家以为他处在一个比实际大得多的环境当中\n\n- 天空盒会跟随相机移动，从而让人认为天空盒的图像在无法到达的远方\n- 使用提前深度测试将天空盒最后渲染以节省带宽\n- 纹理环绕方式：超出采样部分取边界\n- 通过将输出位置的 z 分量等于它的 w 分量，让 z 分量永远等于 1.0，使 z 在透视除法时，深度始终是最大的 1\n  `gl_Position = pos.xyww;`\n\n```glsl\n// VS\n#version 330 core\nlayout (location = 0) in vec3 aPos;\n\nout vec3 TexCoords;\n\nuniform mat4 projection;\nuniform mat4 view;\nuniform mat4 model;\n\nvoid main() {\n    TexCoords = aPos;\n    vec4 pos = projection * view * model * vec4(aPos, 1.0);\n\t  // 用 w 替换 z，让天空盒的深度值在进行深度除法后始终保持 1.0 的最大值，确保天空盒在最后绘制\n    gl_Position = pos.xyww;\n}\n\n// FS\n#version 330 core\nout vec4 FragColor;\n\nin vec3 TexCoords;\n\nuniform samplerCube skybox;\n\nvoid main() {\n    FragColor = texture(skybox, TexCoords);\n}\n```\n\n\n\n### 2.2 环境映射 Environment Mapping\n\n环境映射：通过使用环境的立方体贴图（不仅仅是天空盒）我们可以给物体反射和折射的属性\n\n动态环境贴图：通过帧缓冲，为物体的 6 个角度创建出场景纹理，并在每个渲染迭代中将它们储存到一个立方体贴图中。之后可以使用这个（动态生成的）立方体贴图来创建出更真实的，包含其它物体的，反射和折射表面了\n\n**反射**：通过物体表面单位法线，观察方向来计算反射方向作为立方体贴图的纹理坐标\n\n![](./images/texture_reflection.png)\n\n\n\n**折射**：通过物体表面单位法线，观察方向，以及两个材质之间的折射率（Refractive Index）来求出折射方向，其中 OpenGL 输入的折射率 = $出发材质（空气）的折射率 \\over 进入材质（水）的折射率$\n\n折射法则通过 [斯涅尔定律 Snell's Law](https://en.wikipedia.org/wiki/Snell%27s_law) 来描述，其中 $\\eta$ 为材质的折射率\n\n$$\n\\eta_{入射角} \\sin \\theta_{入射角} = \\eta_{出射角}  \\sin \\theta_{折射角}\n$$\n\n![](./images/texture_refraction.png)\n\n```glsl\n// VS\n#version 330 core\nlayout (location = 0) in vec3 position;\nlayout (location = 1) in vec3 normal;\n\nout vec3 Normal;\nout vec3 Position;\n\nuniform mat4 projection;\nuniform mat4 view;\nuniform mat4 model;\n\nvoid main() {\n    Normal = mat3(transpose(inverse(model))) * normal;\n    Position = vec3(model * vec4(position, 1.0));\n    gl_Position = projection * view * model * vec4(position, 1.0);\n}\n\n// FS\n#version 330 core\nout vec4 FragColor;\n\nin vec3 Normal;\nin vec3 Position;\n\nuniform vec3 cameraPos;\nuniform samplerCube skybox;\n\nvoid main() {\n    vec3 I = normalize(Position - cameraPos);\n    vec3 R = reflect(I, normalize(Normal)); // 反射\n\n    // 折射\n    float ratio = 1.00 / 1.52;\n    R = refract(I, normalize(Normal), ratio);\n    FragColor = vec4(texture(skybox, R).rgb, 1.0);\n}\n\n```\n\n\n\n\n\n# 六、高级纹理\n\n## 1. 渲染目标纹理 RTT\n\n渲染目标纹理（Render Target Texture）把整个三维场景渲染到中间缓冲中，而不是传统的帧缓冲或者后备缓冲（back buffer），与之相关的是多重渲染目标（Multiple Render Target，MRT）\n\n应用：\n\n- 场景中的镜子\n- 场景中的透明玻璃\n\n\n\n## 2. 程序纹理\n\n程序纹理：由计算机生成的纹理，可以使用各种颜色以外参数来控制纹理的外观\n\n### 2.1 Perlin 噪声纹理\n\n常用于模拟水波纹，火和地形，生成二维 Perlin 噪声纹理的过程如下：\n\n1. 晶格划分\n   将二维空间划分为多个大小相等的晶格（矩形）例：1024px * 1024px 的噪声图，可以选择 64px 为晶格尺寸\n   \n   ```glsl\n   p0 = floor(pos / size) * size;\n   p1 = p0 + float2(1, 0) * size;\n   p2 = p0 + float2(0, 1) * size;\n   p3 = p0 + float2(1, 1) * size;\n   \n   posInGrid = (pos - p0) / size;\n   ```\n   \n   ![](./images/texture_noise_lattice.png)\n   \n2. 伪随机梯度生成\n   根据晶格的位置 P 与随机种子，对晶格的每个顶点生成一个伪随机梯度，表示为一个二维向量\n   经过**随机函数 gold_noise** 生成的随机的 x, y 后再归一化，最后用 grad 表\n\n   ```glsl\n   #define PHI (1.61803398874989484820459 * 00000.1)\n   #define PI (3.14159265358979323846264 * 00000.1)\n   #define SQ2 (1.41421356237309504880169 * 10000.0)\n   \n   float gold_noise(float2 pos, float seed) {\n     return frac(tan(distance(pos * (PHI + seed), float2(PHI, PI))) * SQ2) * 2 - 1;\n   }\n   ```\n\n![](./images/texture_noise_lattice1.png)\n\n3. 晶格内插值\n   计算当前点 P 相对于晶格四个顶点的偏移量 delta\n\n   ![](./images/texture_noise_lattice2.png)\n\n   对 delta 和 伪随机梯度得到的 grad 进行点积得到 v，最后将四个顶点的 v 值插值为一个数值\n   得到 Perlin 噪声的**最终值（范围 -1, 1）**\n\n   插值系数的计算一般为：$k = 6t^5 - 15t^4 + 10t^3$ 或 $k = 3t^2 - 2t^3$\n\n   ```glsl\n   float smoothLerp(float a, float b, float t) {\n       float k = pow(t, 5) * 6 - pow(t, 4) * 15 + pow(t, 3) * 10;\n       return (1 - k) * a + k * b\n   }\n   \n   v0 = dot(delta0, grad0);\n   v1 = dot(delta1, grad1);\n   v2 = dot(delta2, grad2);\n   v3 = dot(delta3, grad3);\n   \n   // Lerp with x\n   a = smoothLerp(v0, v1, posInGrid.x);\n   b = smoothLerp(v2, v3, posInGrid.x);\n   \n   // Lerp with y\n   return smoothLerp(a, b, posInGrid.y);\n   ```\n\n4. 分型噪声图\n\n   仅通过晶格化随机梯度生成的二维噪声图难以模拟自然界中的噪声现象\n   即便是缩小晶格尺寸，也只能徒增噪声图的 \"颗粒感\"\n   需要通过：将多种不同晶格尺寸的噪声图**叠加**得到自相似的分形噪声图\n\n   ```glsl\n   // 噪声图的叠加过程也可以描述成一个 分形布朗运动（FBM）函数\n   inline float fbm(float2 pos) {\n       float value = 0;\n       float amplitude = 0.5;\n   \n       for(int i = 0; i < _Iteration; i++) {\n           // 由于 noise_function 返回值的范围在 -1 ~ 1，取绝对值后，可用于地形的生成\n           // value += amplitude * abs(noise_function(pos));\n           value += amplitude * noise_function(pos);\n           pos *= 2;\n           amplitude *= .5;\n       }\n     \n       return value;\n   }\n   ```\n\n\n\n### 2.2 Worley 噪声纹理\n\n常用于模拟多孔噪声，如：石头、水、纸张\n\n\n\n## 3. 虚拟纹理 Virtual Texture\n\n\n\n\n\n\n\n\n\n\n\n# 八、纹理压缩\n\n\n\n\n\n\n\n# Reference\n\n1. [Render To Texture](http://www.paulsprojects.net/opengl/rtotex/rtotex.html)\n2. [Implementing an anisotropic texture filter](https://www.sciencedirect.com/science/article/abs/pii/S0097849399001594)\n3. [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)\n4. [(PDF) Accelerated Half-Space Triangle Rasterization (researchgate.net)](https://www.researchgate.net/publication/286441992_Accelerated_Half-Space_Triangle_Rasterization)\n6. [learnopengl-法线贴图](https://learnopengl-cn.github.io/05%20Advanced%20Lighting/04%20Normal%20Mapping/)\n7. [learnopengl-立方体贴图](https://learnopengl-cn.github.io/04 Advanced OpenGL/06 Cubemaps/#_7)\n8. [Understanding Perlin Noise](https://flafla2.github.io/2014/08/09/perlinnoise.html)\n9. [基于 ComputeShader 生成 Perlin Noise 噪声图](https://zhuanlan.zhihu.com/p/88518193)\n10. [Unity_Shaders_Book : https://github.com/candycat1992/Unity_Shaders_Book](https://link.zhihu.com/?target=https%3A//github.com/candycat1992/Unity_Shaders_Book)\n11. [Unity Manual: https://docs.unity3d.com/Manual/TextureTypes.html](https://link.zhihu.com/?target=https%3A//docs.unity3d.com/Manual/TextureTypes.html)\n14. [Learning DirectX 12 – Lesson 4 – Textures](https://www.3dgep.com/learning-directx-12-4/)\n15. [Unity GPU优化(Occlusion Culling 遮挡剔除，LOD 多细节层次，GI 全局光照)](https://gameinstitute.qq.com/community/detail/120912)\n16. [《我所理解的 Cocos2d-x》秦春林](https://book.douban.com/subject/26214576/)\n17. [《Unity Shader 入门精要》冯乐乐](https://book.douban.com/subject/26821639/)\n18. [深入探索透视纹理映射（下）](https://blog.csdn.net/popy007/article/details/5570803)\n17. [FXAA Whitepaper](http://developer.download.nvidia.com/assets/gamedev/files/sdk/11/FXAA_WhitePaper.pdf)\n\n"
  },
  {
    "path": "ComputerGraphics(OpenGL)/Part4_Animation.md",
    "content": "# 一、纹理动画\n\n## 1. 序列帧动画\n\n序列帧动画：\n\n- 依次播放一系列关键帧图片，当播放速度达到一定数值时，看起来就是一个连续的动画\n- 多用于游戏 2D 中做循环动作的角色，或者制作 3D 模型，做三渲二的序列帧效果\n\n\n\n优点：灵活性强，不需要任何物理计算就可以达到非常细腻的效果\n\n缺点：\n\n- 效果固定，只能从一个角度观看\n- 时间较长的序列帧动画会消耗大量的内存和显存，对于移动端来说，会造成一段时间的内存波动\n\n\n\n## 2. Sprite 动画\n\n生成 Sprite 序列帧动画的工具：[TexturePacker](https://www.codeandweb.com/texturepacker)\n\n方法：多张序列帧图片合成一张纹理，每个序列帧在纹理中的大小是一样的，通过不断变换对合成纹理的局部采样位置来实现帧动画的切换\n\n优点：多张序列帧图片合成一张纹理，减少纹理的读取次数，提高效率\n\n缺点：\n\n- 效果固定，只能从一个角度观看\n- 时间较长的序列帧动画会消耗大量的内存和显存，对于移动端来说，会造成一段时间的内存波动\n\n\n\n\n\n# 二、顶点动画\n\n## 1. 广告牌\n\n广告牌（Billboarding）根据视角方向旋转一个贴有纹理的多边形（通常是四边形）使得多边形总是面对着摄像机\n\n**应用场景：烟雾，云朵，闪光效果**\n\n\n\n和构建相机坐标系的方法类似，需要在广告牌的多边形上构建基坐标系：\n\n1. 在多边形上选取坐标原点（一般为多边形的中心点）\n2. 由于一般广告牌的**表面法向量为指向相机的方向**和世界坐标下竖直向上的方向为已知\n   从而：设世界坐标下竖直向上的方向为 Y 轴（0，1，0），则垂直于 Y 轴和表面法线构成平面的为 X 轴\n   $X = Y_{up} \\times Normal$\n3. Z 轴在 Y 轴和广告牌表面法线构成的平面内，但 Z 轴不一定是法线，需要重新计算\n   Z 轴直于 X 轴和 Y 轴构成的平面 $Z = X \\times Y_{up}$\n4. 将 X、Y、Z 轴**归一化**后作为广告牌的模型矩阵\n5. 根据实际广告牌的中心点在世界坐标系下的位置，将广告牌上的顶点逐个进行位移、MVP 变换，裁剪后绘制到屏幕上\n\n\n\n## 2. Morph 动画\n\n记录多个关键动作中的模型所有顶点的位置，通过在运行中对前后的两个关键动作的模型顶点做线性插值，来达到动画的效果\n\n- 只存储模型网格变化的顶点\n- 多用于人物表情等对动画细节要求较高的地方\n\n\n\n\n\n## 3. 骨骼蒙皮动画\n\n### 3.1 功能作用\n\n> 刚性物体：物体在运动中形状不会发生改变\n\n<u> 刚性阶层动画</u>\n\n- 方法：动画不是一个整体的 Mesh, 而是分成很多部分 Mesh\n  通过一个父子层次结构将这些分散的 Mesh 组织在一起，父 Mesh 带动其下子 Mesh 的运动\n- 优点：刚性阶层动画将动画角色以树状的数据结构存储，制作灵活，美术工作量小，方便角色动画运动\n- 缺点：由于各部分 Mesh 中的顶点是固定在其 Mesh 坐标系中的，所以关节处在运动时会产生裂缝，只适合机械或者皮影风格的角色\n\n\n\n骨骼蒙皮动画\n\n- 方法：通过动画关键帧数据控制骨架的变换，使骨架在两个姿态间的线性插值变化，从而带动绑定在骨架上模型顶点 Mesh 的运动，最终形成动画效果\n- 优点：由于**每个顶点可以被多个骨骼控制**，关节在运动时没有裂缝\n  多套 Mesh 可以共享一个骨骼的动画效果，节省资源\n\n\n\n### 3.2 数据存储结构\n\n**骨骼和关节的关系**：骨骼是一个坐标空间，关节是骨骼坐标空间的原点\n骨骼没有长度，但在编辑器中为了方便展示，会给骨骼绘制长度\n\n**骨架：有层次的关节组成的树形结构**\n\n- 关节：包含多种顶点信息的树形结构中的节点\n- **关节权重**：所有关节的权重和为 1\n  顶点存储受哪些关节的影响（Unity 里 1 个顶点最多受 4 个关节的影响）并记录下受每个关节影响的权重\n- [**插槽** Socket/Slot](https://www.52vr.com/extDoc/ue4/CHN/Engine/Content/Types/SkeletalMeshes/Sockets/index.html)：一个骨骼拥有了插槽，说明这个骨骼可以挂载一个 Mesh 对象，让这个对象**相对于这个骨骼**旋转，位移等运动\n  比如，一个人物模型的手骨骼上通常会有插槽，方便在手上放置不同的武器\n- 根关节：可以通过平移和旋转根关节移动并确定整个骨架在世界空间中的位置和方向\n- 父关节：自身运动可以影响所有子关节的运动\n- 子关节：自身运动不会影响父关节，但处于父关节的坐标系中\n\n![](./images/animation_spine.png)\n\n\n\n**姿势：关节相对于某坐标系的位置、朝向、缩放**\n\n- 绑定姿势：顶点网格在 <u>绑定骨骼前</u> 默认的姿势（又称参考姿势，一般为 T 字形）\n- 局部姿势：关节相对于父关节的偏移，结构 TQS（位置、朝向、缩放）根关节的父节点是世界坐标系原点\n- 全局姿势：关节相对于模型空间或者世界空间的姿势\n\n\n\n### 3.3 骨骼蒙皮的计算\n\n如下图，点 B 是骨骼 A 的子骨骼，点 p 为跟随 B 的网格上的一个顶点\n\n![](./images/spine.png)\n\n**隐含条件** 绑定姿势下的相对位置均已知\n\n1. **绑定姿势下**：点 p 相对于 B 的位置表示为 PB0\n   **目标姿势下**：点 p 相对于 B 的位置表示为 PB1\n   由于 p 跟随 B 运动，相对于 B 时 p 是不动的，因此 PB0 == PB1，点 p 相对于 B 的相对位置可以表示为 PB\n\n2. 顶点<u>关联一根</u>骨骼时\n   **绑定姿势下（当前帧）：**已知\n   点 B 在模型空间<u>相对父骨骼</u>的位置为 $M_{B0}$\n   点 p 在模型空间<u>相对父骨骼</u>的位置为 $M_{p0} = PB * M_{B0}$\n   则，$PB = M_{p0} * M_{B0}^{-1}$\n   \n   **目标姿势下（下一帧）：**\n   已知：点 B 在模型空间<u>相对父骨骼</u>的位置为 $M_{B1}$\n   求得：点 p 在模型空间<u>相对父骨骼</u>的位置为 $M_{p1} = PB * M_{B1} = M_{p0} * M_{B0}^{-1} * M_{B1}$\n   \n   其中，通过已知可求得的矩阵 $M_{sn} = M_{B0}^{-1} * M_{Bn}$ 称为蒙皮矩阵\n   \n3. 顶点<u>关联多根</u>骨骼时\n   关联的所有骨骼的权重和为 $W_0 + W_1 + ... + W_n = 1$\n   $M_{p1} = M_{p0} * (W_0*M_{s0} + W_1*M_{s1} + ... + W_n*M_{sn})$\n\n\n\n### 3.4 线性混合蒙皮（Linear Blending Skinning，LBS）\n\n<u>在三维空间的旋转动画中</u>\n使用**线性插值矩阵**会有信息丢失，这个时候需要使用**线性插值对偶四元数**来达到平滑动画的效果\n为了减少 CPU 的压力，这些计算可以放在 GPU 里来做\n\n比如：两个手臂向相反方向旋转同样角度时，手肘关节的变化\n$$\n0.5 * \n\\begin{bmatrix}\n0 & -1 & 0 \\\\\n1 & 0 & 0 \\\\\n0 & 0 & 1\n\\end{bmatrix}\n+ \n0.5 * \n\\begin{bmatrix}\n0 & 1 & 0 \\\\\n-1 & 0 & 0 \\\\\n0 & 0 & 1\n\\end{bmatrix}\n= \n\\begin{bmatrix}\n0 & 0 & 0 \\\\\n0 & 0 & 1 \\\\\n0 & 0 & 1\n\\end{bmatrix}\n$$\n\n- 线性混合蒙皮算法因其原理为线性计算，有一个无法克服的缺陷:：对于比较灵活的关节（如肩膀），当关节处旋转角度很大时，会产生皮肤失真的结果\n  比如皮肤的塌陷、扭曲打结（裹糖纸）等现象\n  ![](./images/animBoneLBS.png)\n- 采用矩阵来计算旋转信息\n  ![](./images/LinearBlendingSkinnig.gif)\n\n- 采用对偶四元数来计算旋转信息\n  ![](./images/DualQuaternionBlendingSkinning.gif)\n\n\n\n\n\n# 三、动画混合\n\n将两个或多个**动画片段**的当前**骨架姿态**以一定的方式通过程序进行实时混合\n\n**注意：**骨骼之间的混合不能使用最终的矩阵来混合（矩阵无法做动画各个属性的线性计算），需要分别对生成矩阵的 SQT 进行单独混合\n\n\n\n\n\n# 四、动画流水线\n\n动画后处理：动画片段混合以外的骨架姿势修改。包括：IK 处理，物理效果（动画融合物理打击效果）\n\n![](./images/Animation_Pipeline.png)\n\n**骨骼**：一根骨骼实际上是一个点（在编辑器里常常会把骨骼间连成线段）\n**骨骼 Pos**：基于骨架参考姿势的变换矩阵，是所有骨骼 Transform （位移+旋转+缩放）的集合\n**蒙皮**：将网格顶点，根据设计好的蒙皮权重 和 上一步计算好的骨骼 Pos 计算后得到新网格顶点的过程\n\n\n\n\n\n# Reference\n\n- [OpenGEX 官网](http://www.opengex.org/)\n- [骨骼动画原理](https://www.cnblogs.com/tandier/p/10087656.html)\n- [骨骼蒙皮动画算法（Linear Blending Skinning）](https://www.cnblogs.com/shushen/p/5987280.html)\n- [Skeletal Animation 理论与实践](https://zhuanlan.zhihu.com/p/27073261)\n- [Skinned Mesh 原理解析和一个最简单的实现示例](https://blog.csdn.net/n5/article/details/3105872)\n- [UE4 动画系统笔记](https://blog.csdn.net/hechao3225/article/details/113531847)\n- [浅析 UE 动画系统](https://zhuanlan.zhihu.com/p/393884450)\n\n"
  },
  {
    "path": "ComputerGraphics(OpenGL)/Part5_Trick.md",
    "content": "# 一、后处理特效\n\n\n## 1. HDR\n\n高动态范围（High Dynamic Range，HDR）\n\n- 亮的东西可以变得非常亮，暗的东西可以变得非常暗，而且充满细节\n\n- 显示器被限制为只能显示值为 0.0 到 1.0 间的颜色，但是在光照方程中却没有这个限制\n\n- 通过使片段的颜色超过1.0，我们有了一个更大的颜色范围\n\n\n\nHDR 原本只是被运用在摄影上，摄影师对同一个场景采取不同曝光拍多张照片，捕捉大范围的色彩值。这些图片被合成为 HDR 图片，从而综合不同的曝光等级使得大范围的细节可见\n\n\n\n\n## 2. 泛光 Bloom\n\n方法：\n\n1. 根据一个阈值提取出图像中较亮的区域，把它们存储在一张渲染纹理上\n2. 利用高斯模糊对这张渲染纹理进行模糊处理，从而模拟光线扩散的泛光效果\n3. 将模糊后的图像和原图像进行混合\n\n\n\n## 3. 运动模糊\n\n运动模糊：真实世界中的摄像机的一种效果（相机曝光时，拍摄场景发生变化，就会产生模糊效果）\n\n方法一：\n\n- 步骤：利用累积缓存（accumulation buffer）来混合多张连续的图像，将它们取平均值来作为最后的模糊图像\n- 缺点：性能消耗很大，因为在同一帧里要渲染多次场景\n- 优化：只保存上一帧的渲染效果，不断把上一帧图像和当前图像叠加（alpha 混合），从而产生运动轨迹的视觉效果\n\n\n\n方法二：\n\n- 步骤：利用速度缓存（velocity buffer）存储各个像素当前的运动速度，利用改值来决定模糊的方向和大小\n- 优化：通过上一帧的相机位置和投影得到上一帧点的坐标与当前帧求深度位置差，即运动速度。最后，通过运动速度决定了 3 X 3 的均值滤波的各个方向的采样步长来做模糊\n\n\n\n## 4. 全局雾化\n\n雾的计算：$Color_{out} = f * Color_{fog} +(1-f) * Color_{origin}$\n\n雾的系数 $f$ 计算：使用噪声纹理强度图，乘以雾的系数，让雾的系数变化更自然\n\n1. 线性，$d_{max}$ 和 $d_{min}$ 分别表示受雾影响的最小距离和最大距离\n   $f = {d_{max} - |z| \\over d_{max} - d_{min}}$\n2. 指数，$d$ 控制雾浓度的参数\n   $f = e^{-d-|z|}$\n3. 指数的平方，$d$ 控制雾浓度的参数\n   $f = e^{-(d-|z|)^2}$\n\n\n\n\n\n# 二、其他\n\n## 1. 描边\n\n### 1.1 向外扩张\n\n1. 让背面面片在视角空间下把模型顶点沿着法线的方向**向外扩张**一段距离，让背部轮廓可见\n   为了防止背面面片有 Z 轴方向内凹的模型，先给让背面面片尽可能平整\n   让所有背面面片的法线 Z 轴统一为一个定值，然后在归一化法线\n\n   ```glsl\n   viewNormal.z = -0.5;\n   viewNormal = normalize(viewNormal);\n   viewPos = viewPos + viewNormal * outlineWidth;\n   ```\n\n2. 使用轮廓线的颜色渲染背面的面片\n\n3. 渲染正面面片\n\n\n\n### 1.2 检测轮廓线\n\n1. 检测边是否为轮廓线\n   通过判断两个相邻的三角片面是否一个朝正面，一个朝背面\n   $(n_0 \\cdot v) * (n_1 \\cdot v) < 0$ 其中，$n_0$ 和 $n_1$ 为相邻两个三角面的法向量，$v$ 是从视角指向顶点的方向\n2. 单独渲染轮廓线（可以进行风格化渲染，水磨笔触的描边）\n\n\n\n\n\n## 2. 3D 拾取\n\n### 2.1 颜色拾取\n\n1. **绘制颜色索引**\n   创建 FrameBuffer，附着一张颜色纹理 RGB，一张深度纹理\n   根据物体的 ID，物体的绘制批次 来给物体离屏渲染着色\n    ```c\n   #version 330\n   \n   uniform uint gObjectIndex; // 绘制对象的 ID：随着对象的更新而更新\n   uniform uint gDrawIndex;   // 绘制批次的 ID：对象的绘制批次\n   \n   out vec3 FragColor;\n   \n   void main()\n   {\n     // gl_PrimitiveID：默认不使用 GS 为 0，使用 GS 时会被赋值，每次 drawcall 会更新\n     // gl_PrimitiveID + 1：为了区分背景色黑色和索引色\n     FragColor = vec3(float(gObjectIndex), float(gDrawIndex), float(gl_PrimitiveID + 1));\n   }\n    ```\n\n2. **拾取颜色**\n   通过 glReadPixels 拾取点选的颜色值，根据颜色值判断点击的物体\n   \n    ```c\n    BYTE bArray[3];\n    glReadPixels(mp.x, mp.y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, bArray);\n    ```\n\n\n\n### 2.2 射线拾取\n\n1. **确定射线**\n   将屏幕的点击位置映射到近平面和远平面的点，两个点的连线就是 射线\n\n   ![](./images/ray.jpg)\n\n   ```c\n   // 直接调用 glm 的 unProject 函数来确定射线\n   glm::vec3 glm::unProject(glm::vec3 const& win, \n                            glm::mat4 const& model, \n                            glm::mat4 const& proj, \n                            glm::ivec4 const& viewport);\n   \n   // 以下为 glm unProject 的具体实现\n   template<typename T, typename U, qualifier Q>\n   GLM_FUNC_QUALIFIER vec<3, T, Q> unProject(vec<3, T, Q> const& win, \n                                             mat<4, 4, T, Q> const& model, \n\t                                          mat<4, 4, T, Q> const& proj, \n                                             vec<4, U, Q> const& viewport)\n   {\n   #\t\tif GLM_CONFIG_CLIP_CONTROL & GLM_CLIP_CONTROL_ZO_BIT\n   \treturn unProjectZO(win, model, proj, viewport);\n   #\t\telse\n   \treturn unProjectNO(win, model, proj, viewport);\n   #\t\tendif\n   }\n   \n   template<typename T, typename U, qualifier Q>\n   GLM_FUNC_QUALIFIER vec<3, T, Q> unProjectNO(vec<3, T, Q> const& win, \n                                               mat<4, 4, T, Q> const& model, \n                                               mat<4, 4, T, Q> const& proj, \n                                               vec<4, U, Q> const& viewport)\n   {\n     mat<4, 4, T, Q> Inverse = inverse(proj * model);\n   \n     vec<4, T, Q> tmp = vec<4, T, Q>(win, T(1));\n     tmp.x = (tmp.x - T(viewport[0])) / T(viewport[2]);\n     tmp.y = (tmp.y - T(viewport[1])) / T(viewport[3]);\n     tmp = tmp * static_cast<T>(2) - static_cast<T>(1);\n   \n     vec<4, T, Q> obj = Inverse * tmp;\n     obj /= obj.w;\n   \n     return vec<3, T, Q>(obj);\n   }\n   \n   template<typename T, typename U, qualifier Q>\n   GLM_FUNC_QUALIFIER vec<3, T, Q> unProjectZO(vec<3, T, Q> const& win, \n                                               mat<4, 4, T, Q> const& model, \n                                               mat<4, 4, T, Q> const& proj, \n                                               vec<4, U, Q> const& viewport)\n   {\n     mat<4, 4, T, Q> Inverse = inverse(proj * model);\n     \n     vec<4, T, Q> tmp = vec<4, T, Q>(win, T(1));\n     tmp.x = (tmp.x - T(viewport[0])) / T(viewport[2]);\n     tmp.y = (tmp.y - T(viewport[1])) / T(viewport[3]);\n     tmp.x = tmp.x * static_cast<T>(2) - static_cast<T>(1);\n     tmp.y = tmp.y * static_cast<T>(2) - static_cast<T>(1);\n   \n     vec<4, T, Q> obj = Inverse * tmp;\n     obj /= obj.w;\n   \n     return vec<3, T, Q>(obj);\n   }\n   ```\n   \n2. **找到射线的碰撞**\n   将每个物体的碰撞体设置为球体，求射线与球心最近的对象\n   判断射线是否与最近的球体相交（具体方法见 [三维距离检测/点到直线最近点](../LinearAlgebra/Part3_Triangles.md)）\n\n\n\n## 3. 粒子系统\n\n\n\n\n\n## 4. 地貌生成\n\n### 4.1 高度贴图\n\n地貌的高低起伏一般通过编辑器在 CPU 内处理为 Mesh 数据，其基本原理是\n\n1. 通过绘制黑白的高度图来生成 3D 地貌顶点 Mesh 数据\n   一个高度图上的像素对应一个顶点的高度数据分量\n2. 高度图的 UV 表示 Mesh 的 XZ 坐标\n3. 高度图的像素颜色可以通过范围映射将 [0, 255]  映射为 [-128, 128] 来表示 Mesh 的 Y 轴坐标 （高低起伏，只修改高度值，不会修改水平面坐标）\n4. 根据 Mesh 在生成顶点法线等顶点属性\n\n![](./images/landscape.png)\n\n\n\n### 4.2 植被覆盖\n\n植被可以是树、花、草，每一种绘制流程都相似，以草为例：\n通过 Geometry Shader 来实时生成固定的草的 Mesh 顶点并绘制\n\n1. 一个草的片面由 4 个顶点构成的 2 个三角形\n2. 草可以有多种类型的片面\n3. 需要一张对应高度贴图的草种类贴图来描述每个位置草的种类\n4. 通过 GS，将一个地貌 Mesh 的顶点扩展为\n   一个草的片面（像向日葵一样跟随相机朝向）\n   三个草的片面（根据固定偏移随机旋转，让每个顶点不同，每一帧的每个顶点相同的随机值，相同的旋转）\n5. 根据草种类纹理选择草的种类，绘制草的片面\n6. 可以使用三角函数，通过整体上下周期偏移来制造草的高低起伏（做到风吹草浪的效果）\n\n\n\n制作植被时，有以下情况还需要注意：\n\n![](./images/foliage.png)\n\n\n\n\n\n# 三、游戏引擎\n\n## 1. Handle 的作用\n\n**Handle 类似于指针，实际上是一个整数类型，不直接引用内存，可以有以下映射内存的方式**\n\n- 作为索引直接引用\n- 经过一系列加密方法映射到内存地址\n  比如：用 8 位密码将 16 位索引加密。将 4 位类型、4 位权限、8 位密码、16 位加密索引之后打包成一个 32 位的整数作为 Handle\n\n\n\n**Handle 的类型**\n\n通过给 Handle 套上结构体，确保在内存占用不变的情况下让编译器区分 Handle 类型，将问题前置到编译阶段\n\n```c\nstruct VertexBufferHandle { uint16_t idx; };\nstruct ProgramHandle { uint16_t idx; };\n```\n\n\n\n**设计接口时 Handle 比指针优势**\n\n1. 指针作用太强，可做的事情太多\n   接口设计中，功能刚刚好就够了，并非越多权限越好的，权限越多就越危险（不容易解耦）\n2. Handle 只是个整数，里面实现可以隐藏起来\n   假如直接暴露了指针，也就暴露了指针类型，用户就会看到更多的细节\n3. 所有资源在内部管理，通过 Handle 作为中间层，可以有效判断 Handle 是否合法，而防止了野指针的情况\n4. Handle 只是个整数，所有的语言都有整数这种类型，但并非所有语言都有指针\n   接口只出现 Handle，方便将实现绑定到各种语言\n\n\n\n\n\n# Reference\n\n- [3D Picking](http://ogldev.atspace.co.uk/www/tutorial29/tutorial29.html)\n- [light house / opengl-selection-tutorial](http://www.lighthouse3d.com/tutorials/opengl-selection-tutorial/)\n- [learnopengl-Bloom](https://learnopengl-cn.github.io/05 Advanced Lighting/07 Bloom/)\n- [learnopengl-HDR](https://learnopengl-cn.github.io/05 Advanced Lighting/06 HDR/)\n- [learnopengl-AntiAliasing](https://learnopengl-cn.github.io/04 Advanced OpenGL/11 Anti Aliasing/)\n- [HDR Tone Mapping](https://zhuanlan.zhihu.com/p/26254959)\n- [OGL-Particle System using Transform Feedback](http://ogldev.atspace.co.uk/www/tutorial28/tutorial28.html)\n- [Open Dynamics Engine](http://www.ode.org)\n- [Open Dynamics Engine Doc](http://ode.org/ode-latest-userguide.html)\n- [bgfx 学习笔记（5）- Handle 的作用和分配](https://zhuanlan.zhihu.com/p/63012167)\n- [OGRE 的渲染流程分析](https://zhuanlan.zhihu.com/p/113368993)\n- [Redundancy vs dependencies: which is worse?](http://yosefk.com/blog/redundancy-vs-dependencies-which-is-worse.html)\n- [Multilayered Terrain](https://www.mbsoftworks.sk/tutorials/opengl3/21-multilayered-terrain/)\n- [Terrain Pt. 2 - Waving Grass](https://www.mbsoftworks.sk/tutorials/opengl3/29-terrain-pt2-waving-grass/)\n- [Creating a Stylized Chaparral Environment in UE4](https://80.lv/articles/creating-a-stylized-chaparral-environment-in-ue4/)\n\n"
  },
  {
    "path": "ComputerGraphics(OpenGL)/README.md",
    "content": "# Computer Graphics（OpenGL）Notes\n\nThis notes written in [Typora](https://www.typora.io/). It is also recommended to use it to read.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n⚓️ Keep Reading , Keep Writing , Keep Coding.\n\n"
  },
  {
    "path": "DigitalImageProcessing/Part0_Signals&Systems.md",
    "content": "# 一、信号与系统\n\n信号可以来自于很多外部设备：录音机、温度计、相机\n\n**信号处理的方向**：并不关心数据以怎样自然规律产生，而是重点放在如何改变输入的信号数据\n\n- 模拟信号：连续不断的，针对具体外界做出的具体测量值，没有取值范围\n- 数字信号：离散采样的，具有时间和幅度两个分量，将模拟信号限定在一个**固定**的测量范围后，表示**局部模拟信号**波动的值，在传输时具有较强的抗干扰能力\n\n\n\n\n## 1. 信号的系统\n\n> 信号的系统指一系列处理信号的**方法**\n\n### 1.1 系统的分类\n\n以下系统类型可以组合为多个系统类型\n\n- **线性转换** Linear / **非线性转换** Non-linear\n  线性：输入信号与输出信号满足可加性和同质性（缩放一致）\n  $$\n  \\alpha \\cdot x(t) \\rarr [Linear \\space System] \\rarr \\alpha \\cdot y(t) \\\\\n  x_1(t) + x_2(t) \\rarr [Linear \\space System] \\rarr y_1(t) + y_2(t) \\\\\n  \\alpha \\cdot x_1(t) + \\beta \\cdot x_2(t) \n  \\rarr [Linear \\space System] \\rarr \n  \\alpha \\cdot y_1(t) + \\beta \\cdot y_2(t) \\\\\n  $$\n  \n- **时不变** time-inveriant / **时变** time-varying\n  时不变：同样的时间偏移，输入和输出的值是对应的\n$$\nx(t-t_0) \\rarr [Time-Invariant \\space System] \\rarr y(t-t_0)\\\\\n$$\n\n- 因果系统 Causal\n根据现在和过去的输入输出值推出输出值\n\n\n\n\n### 1.2 对信号分析和表达的作用域\n\n- 时域：从时间的角度分析信号\n- 频率域：从信号频率角度分析信号\n- 空间域：从信号发生空间位置角度分析信号\n\n\n\n## 2. 信号的分类\n\n> 信号在离散和连续时有相同也有不同的特性，比如：一个正弦信号，如果周期不是整数，那虽然在连续信号里它具有周期性，但是在离散信号里，没有周期性\n\n**脉冲信号**：一种**离散信号**（即，离散时间信号，指信号在时间这个轴上是离散的），波形之间在 Y 轴不连续，具有一定周期性\n\n\n\n### 2.1 单位脉冲信号\n\n一维信号中的单位脉冲信号\n\n![](./images/unit_impulse.png)\n\n$$\n\\begin{array}{rrl}\nOriginal & \\delta(n) =& \n\\begin{cases}\n1,& n=0\\\\\n0,& otherwise\n\\end{cases}\\\\\nOffset & \\delta(n-n') =& \n\\begin{cases}\n1,& n=n'\\\\\n0,& otherwise\n\\end{cases}\\\\\\\\\n2D \\space Discrete & \\delta(n) =& f_1(n_1)\\cdot f_2(n_2)\n\\end{array}\n$$\n\n\n\n二维信号中的单位脉冲信号\n\n![](./images/unit_impulse_2D.png)\n$$\n\\begin{array}{rrl}\nOriginal & \\delta(m,n) =& \n\\begin{cases}\n1,& m=n=0\\\\\n0,& otherwise\n\\end{cases}\\\\\nOffset & \\delta(m-m',n-n') =& \n\\begin{cases}\n1,& m=m',n=n'\\\\\n0,& otherwise\n\\end{cases}\n\\end{array}\n$$\n\n\n\n### 2.2 单位跃阶信号\n\n![](./images/unit_step.png)\n$$\n\\begin{array} {rrl}\nOriginal & u(n) =& \n\\begin{cases}\n1,& n\\geq0\\\\\n0,& otherwise\n\\end{cases}\\\\\nOffset & u(n-n') =& \n\\begin{cases}\n1,& n \\geq n'\\\\\n0,& otherwise\n\\end{cases}\\\\\\\\\n2D \\space Discrete & u(n_1, n_2) =& \nu_1(n_1)\\cdot u_2(n_2)\n\\end{array}\n$$\n\n\n\n### 2.3 正弦信号\n\nA 振幅，$\\omega_0$ 频率，$\\phi$ 相位（指偏移量），$T_0 = {2 \\pi \\over \\omega_0}$ 周期\n$$\nu(t) = A \\space cos(\\omega_0 t + \\phi)\n$$\n\n![](./images/cos.png)\n\n\n\n### 2.4 指数信号\n\n指数信号\n$$\nu(t) = C e^{at}，(C \\space 和 \\space a \\space 是实数)\n$$\n![](./images/exponent.png)\n\n\n\n复指数信号\n$$\n\\begin{align}\nu(t) &= C e^{at}，(C \\space 和 \\space a \\space 是复数)\\\\\nu(t) &= |C|e^{rt}e^{(j\\omega_0t+ \\theta)}\n\\end{align}\n$$\n\n![](./images/exponent2.png)\n\n\n\n\n\n\n# 二、卷积\n\n## 1. 信号的拆解\n\n> 一般来说，一个脉冲信号可以被分解为多个单位脉冲信号加权之和\n\n### 1.1 傅立叶级数\n\n傅立叶级数：任何周期函数，都可以用一系列的 sin 函数 和 cos 函数的和来表示\n\n\n\n### 1.2 离散信号序列的拆解\n\n以下图的一维 $x$ 信号序列为例：\n\n![](./images/impulse.png)\n\n已知单位脉冲信号序列 $\\delta[n]$：\n$$\n\\delta(n) =\n\\begin{cases}\n1,& n=0\\\\\n0,& otherwise\n\\end{cases}\\\\\n$$\n则脉冲信号序列 $x[n]$：\n$$\n\\begin{array}{lll}\nx[0] &= 2 &= 2 \\cdot \\delta[n - 0] &= x[0] \\cdot \\delta[n - 0] \\\\\nx[1] &= 3 &= 3 \\cdot \\delta[n - 1] &= x[1] \\cdot \\delta[n - 1] \\\\\nx[2] &= 1 &= 1 \\cdot \\delta[n - 2] &= x[2] \\cdot \\delta[n - 2] \\\\ \\\\\n\\end{array} \\\\\n\\begin{array}{llll}\n当 \\space n \\in [1,2,3] \\space 时 & x[n] &= x[0] + x[1] + x[2] \\\\\n& &= x[0] \\cdot \\delta[n - 0] + x[1] \\cdot \\delta[n - 1] + x[2] \\cdot \\delta[n - 2]\\\\ \\\\\n当 \\space n \\in (-\\infin, \\infin) \\space 时 & x[n] &= \\sum_{k = - \\infin}^\\infin x[k] \\cdot \\delta[n - k]\n\\end{array}\n$$\n\n\n\n同理，二维离散信号序列也可以拆解为：\n$$\n\\begin{array}{lll}\n当 \\space n \\in [1,2,3] \\space 时 & \nx[m][n] &= x[0][0] \\cdot \\delta[m][n] + x[1][2] \\cdot \\delta[m-1][n-2] + ... +  x[m][n]\\cdot \\delta[0][0]\\\\\n当 \\space n \\in (-\\infin, \\infin) \\space 时 &\nx[m][n] &= \n\\sum_{i = - \\infin}^\\infin\n\\sum_{j = - \\infin}^\\infin \nx[i][j] \\cdot \\delta[m - i][n - j]\n\\end{array}\n$$\n\n\n\n\n\n## 2. 单位脉冲响应\n\n**单位脉冲响应**：通过单位脉冲信号输入**线性时不变系统**后，得到的输出信号，一般用 $h[n]$ 表示\n一般作为卷积的参数之一，使得全部的信号能通过该方法处理\n\n![](./images/impulse_response01.png)\n\n### 2.1 单位脉冲响应的特性\n\n以下特性，适用于一维和二维等信号\n\n1. 系统以时间为变量时，输入单位脉冲会得到对应时间偏移的单位脉冲响应\n![](./images/impulse_response02.png)\n\n2. 系统为线性变换系统时，单位脉冲响应结果也满足对应输入的线性变换\n   ![](./images/impulse_response03.png)\n   \n3. 多个单位脉冲输入时，在经过系统处理后也会得到相应数目个单位响应脉冲\n   ![](./images/impulse_response04.png)\n\n\n\n结合**单位脉冲响应的性质**和**离散信号的拆解方式**后的处理过程，就是卷积\n\n![](./images/impulse_response05.png)\n\n\n\n### 2.2 二维的单位脉冲响应（卷积核）\n\n一般来说二维的脉冲响应（卷积核）多以中心为起点排列，如下图**（具体原理参考数值积分）**\n\n- 注意：输入信号的排列方式与二维脉冲响应信号的排列**无关**\n\n![](./images/kernel.png)\n\n\n\n**为了减少计算量**，我们可以通过将一个二维脉冲响应（卷积核）拆解为两个一维脉冲响应分别依次（顺序可以颠倒）做卷积\n这样假如原本为 3X3 的卷积核，**9 个输入**要做 9 X 9 = 81 次乘法，拆分后只需要做 9 + 9 =18 次乘法\n\n- 作用：**通过拆解来降低二维脉冲响应的计算量**\n\n- 例，其中 * 表示卷积：\n  $$\n  \\begin{array}{lrl}\n  \\because & y[m,n] =& x[m,n] * h[m,n] = \n  \\sum_{j = - \\infin}^\\infin\n  \\sum_{i = - \\infin}^\\infin\n  x[i,j] \\cdot h[m-i,n-j] \\\\\n  &=& h[m,n] * x[m,n] = \n  \\sum_{j = - \\infin}^\\infin\n  \\sum_{i = - \\infin}^\\infin\n  h[i,j] \\cdot x[m-i,n-j] \\\\\n  &=& (h_1[m] \\cdot h_2[n]) * x[m,n],(线性代数中对矩阵的向量拆分) \\\\\n  &=& \n  \\sum_{j = - \\infin}^\\infin\n  \\sum_{i = - \\infin}^\\infin\n  (h_1[i] \\cdot h_2[j]) \\cdot x[m-i,n-j] \\\\\n  &=& \n  \\sum_{j = - \\infin}^\\infin\n  h_2[j] \\cdot(\\sum_{i = - \\infin}^\\infin h_1[i] \\cdot x[m-i,n-j]) \\\\\n  &=& h_2[j] * (h_1[i] * x[m-i,n-j]) \\\\\\\\\n  or \n  &=& h_1[i] * (h_2[j] * x[m-i,n-j]) \\\\\n  \\\\\n  \\therefore &\n  \\begin{bmatrix}\n  A \\cdot a & A \\cdot b & A \\cdot c \\\\\n  B \\cdot a & B \\cdot b & B \\cdot c \\\\\n  C \\cdot a & C \\cdot b & C \\cdot c \n  \\end{bmatrix}\n  =& \n  \\begin{bmatrix}A \\\\ B \\\\ c\\end{bmatrix}\n  \\cdot \n  \\begin{bmatrix}a & b & c\\end{bmatrix} \\\\\n  &\n  x[m,n] * \n  \\begin{bmatrix}\n  A \\cdot a & A \\cdot b & A \\cdot c \\\\\n  B \\cdot a & B \\cdot b & B \\cdot c \\\\\n  C \\cdot a & C \\cdot b & C \\cdot c \n  \\end{bmatrix}\n  =& x[m,n] * \n  \\begin{pmatrix}\n  \\begin{bmatrix}A \\\\ B \\\\ c\\end{bmatrix}\n  \\cdot \n  \\begin{bmatrix}a & b & c\\end{bmatrix} \n  \\end{pmatrix}\\\\\n  &=&\n  \\begin{pmatrix} x[m,n] * \\begin{bmatrix}A \\\\ B \\\\ c\\end{bmatrix} \\end{pmatrix}\n  * \n  \\begin{bmatrix}a & b & c\\end{bmatrix} \n  \\end{array}\n  $$\n  \n\n\n\n## 3. 离散时阈下的卷积\n\n**卷积公式（一维信号）**，其中\ny 为输出信号序列（卷积结果）\nx 为输入信号序列\nh 当前输入信号对应的**脉冲响应结果**（信号系统处理后的到的值）\nn 当前输入信号的序列号\n$$\ny[n] = \\sum_{k = -\\infin}^\\infin x[k]\\cdot h[n - k]\n$$\n\n\n\n## 4. 卷积\n\n### 4.1 一维信号的卷积过程\n\n假设：\n\n信号序列 $x[n] = \\{ 3, 4, 5 \\}$ 当 n 取 0 - 2 之外的值时信号均为 0 \n单位脉冲信号随着时间变化后的**响应脉冲**为 $h[n] = \\{ 2, 1 \\}$ 当 n 取 0 - 1 之外的值时脉冲响应均为 0 \n\n求卷积：\n![](./images/convolution.png)\n$$\n\\begin{array}{lll}\ny[0] &= x[0]·h[0] & 需要采样 \\space x_0  \\\\\ny[1] &= x[1]·h[0] + x[0]·h[1] & 需要采样 \\space x_0,x_1 \\\\\ny[2] &= x[2]·h[0] + x[1]·h[1] & 需要采样 \\space x_1,x_2 \\\\\ny[3] &= x[3]·h[0] + x[2]·h[1] & 需要采样 \\space x_2,x_3 \\\\\n\\end{array}\n$$\n\n归纳可得，其中 i 表示 **信号序号**，k 表示 **响应脉冲序号**\n$$\ny[i] =\n\\begin{cases}\nx[0]·h[0] & k = 0 \\\\\nx[i]·h[0] + x[i - 1]·h[1] + x[i - 2]·h[2] + ...+x[i - (k-1)]·h[k-1] & k \\gt 0\n\\end{cases}\n$$\n\n\n\nCPU 代码计算示例\n\n- 注意：卷积后的结果可能会超出原来数值的范围，即使原来的数值都在范围内\n\n```c\n// 1D convolution\n// We assume input and kernel signal start from t=0.\nbool convolve1D(float* in, float* out, int dataSize, float* kernel, int kernelSize)\n{\n    int i, j, k;\n\n    // check validity of params\n    if(!in || !out || !kernel || dataSize <=0 || kernelSize <= 0) return false;\n\n    // start convolution from out[kernelSize-1] to out[dataSize-1] (last)\n    for(i = kernelSize-1; i < dataSize; ++i)\n    {\n        out[i] = 0;                             // init to 0 before accumulate\n\n        for(j = i, k = 0; k < kernelSize; --j, ++k)\n            out[i] += in[j] * kernel[k];\n    }\n\n    // convolution from out[0] to out[kernelSize-2]\n    for(i = 0; i < kernelSize - 1; ++i)\n    {\n        out[i] = 0;                             // init to 0 before sum\n\n        for(j = i, k = 0; j >= 0; --j, ++k)\n            out[i] += in[j] * kernel[k];\n    }\n\n    return true;\n}\n```\n\n\n\n\n### 4.2 二维信号的卷积过程\n\n二维信号的卷积是一维信号卷积的扩展，根据二维的单位脉冲响应和二维的信号拆解可得\n\n- 注意：二维响应信号（卷积核）在最后使用的时候和原来**左右上下都颠倒**\n\n![](./images/convolution1.png)\n\n\n\n例：在采样（输入）是（1，1）位置的卷积计算为\n\n![](./images/convolution2D.png)\n\n\n\n卷积的输入、卷积核、卷积输出如下\n\n![](./images/convolution2.png)\n\n\n\nCPU 代码计算示例\n\n- 注意：卷积后的结果可能会超出原来数值的范围，即使原来的数值都在范围内\n\n```c\nbool convolve2D(int* in, int* out, int dataSizeX, int dataSizeY, \n                float* kernel, int kernelSizeX, int kernelSizeY)\n{\n    int i, j, m, n;\n    int *inPtr, *inPtr2, *outPtr;\n    float *kPtr;\n    int kCenterX, kCenterY;\n    int rowMin, rowMax;                             // to check boundary of input array\n    int colMin, colMax;                             //\n    float sum;                                      // temp accumulation buffer\n\n    // check validity of params\n    if(!in || !out || !kernel) return false;\n    if(dataSizeX <= 0 || kernelSizeX <= 0) return false;\n\n    // find center position of kernel (half of kernel size)\n    kCenterX = kernelSizeX >> 1;\n    kCenterY = kernelSizeY >> 1;\n\n    // init working  pointers\n    // note that it is shifted (kCenterX, kCenterY)\n    inPtr = inPtr2 = &in[dataSizeX * kCenterY + kCenterX];  \n    outPtr = out;\n    kPtr = kernel;\n\n    // start convolution\n    for(i= 0; i < dataSizeY; ++i)           \t // number of rows\n    {\n        // compute the range of convolution\n        // the current row of kernel should be between these\n        rowMax = i + kCenterY;\n        rowMin = i - dataSizeY + kCenterY;\n\n        for(j = 0; j < dataSizeX; ++j)     \t\t// number of columns\n        {\n            // compute the range of convolution, \n            // the current column of kernel should be between these\n            colMax = j + kCenterX;\n            colMin = j - dataSizeX + kCenterX;\n\n            sum = 0;                           // set to 0 before accumulate\n\n            // flip the kernel and traverse all the kernel values\n            // multiply each kernel value with underlying input data\n            for(m = 0; m < kernelSizeY; ++m)   // kernel rows\n            {\n                // check if the index is out of bound of input array\n                if(m <= rowMax && m > rowMin)\n                {\n                    for(n = 0; n < kernelSizeX; ++n)\n                    {\n                        // check the boundary of array\n                        if(n <= colMax && n > colMin)\n                            sum += *(inPtr - n) * *kPtr;\n\n                        ++kPtr;            // next kernel\n                    }\n                }\n                else\n                    kPtr += kernelSizeX;   // out of bound, move to next row of kernel\n\n                inPtr -= dataSizeX;        // move input data 1 raw up\n            }\n\n            // convert integer number\n            if(sum >= 0) *outPtr = (int)(sum + 0.5f);\n            else *outPtr = (int)(sum - 0.5f);\n\n            kPtr = kernel;      // reset kernel to (0,0)\n            inPtr = ++inPtr2;   // next input\n            ++outPtr;           // next output\n        }\n    }\n\n    return true;\n}\n```\n\n\n\n\n\n# 三、傅立叶变换\n\n\n\n\n\n\n\n# Reference\n\n1. [正弦信号，指数信号](https://www.cnblogs.com/zhiyinglky/p/5805315.html)\n2. [跃阶信号](https://www.cnblogs.com/zhiyinglky/p/5805314.html)\n3. [单位脉冲响应、单位阶跃响应的作用是什么？](https://know.baidu.com/question/b83161b20e2bf9de79624dd98fa89b47c70e1cf)\n4. [Fundamentals of Digital Image and Video Processing](https://github.com/giosans/Fundamentals-of-Digital-Image-and-Video-Processing-course)\n5. [Signals and Systems](http://open.163.com/movie/2011/8/7/A/M8AROL7GG_M8AROT67A.html)\n6. [Convolution](http://www.songho.ca/dsp/convolution/convolution.html)\n7. [Proof of Separable Convolution 2D](http://www.songho.ca/dsp/convolution/convolution2d_separable.html)"
  },
  {
    "path": "DigitalImageProcessing/Part1_Filtering.md",
    "content": "# 一、图像处理基本\n\n\n## 1. 光谱即是电磁波谱\n\n- 波长：从红光到紫光，波长不断变短（对于可见光，波长不同，颜色不同）\n\n  ![](images/waveLength.png)\n\n- 灰度级：从黑到白的单色光度量范围\n  单色光：无色光，没有颜色的光\n  强度：即灰度，单色光的唯一属性\n\n- 发光强度：光源流出的能量总和\n\n- 光通量：观察者从光源感受到的能量，**单位：流明 lumen （LM）**\n\n- 亮度：物体表面的反光率，实际上不能度量，色彩的强度（人眼对亮度的敏感 > 色彩）\n\n\n\n\n## 2. 数字图像\n### 2.1 图像属性\n\n- 图像的本质：一个二维数组（矩阵）\n\n- 原点：左上角\n\n- 动态范围：**图中**最大灰度 / **图中**最小灰度\n\n- 噪声：图中多余的干扰信息，低于**图中**最小灰度，便会出现\n\n- 饱和度：即**图中**最大灰度，高于这个值，图中的灰度将会被剪裁掉（彩图中是灰度和色调的比例，0% 灰色 ～ 100% 完全饱和）\n\n- 对比度：**图中**最大灰度 **-** **图中**最小灰度\n\n- 图像分辨率（空间分辨率）：每单位距离的点个数 \n\n  > 相关单位：dpi([点每英寸](https://baike.baidu.com/item/%E7%82%B9%E6%AF%8F%E8%8B%B1%E5%AF%B8)）、lpi（线每英寸）和ppi（[像素每英寸](https://baike.baidu.com/item/%E5%83%8F%E7%B4%A0%E6%AF%8F%E8%8B%B1%E5%AF%B8))\n\n$$\nPPI = {\\sqrt{像素长^2 + 像素宽^2} \\over \\sqrt{屏幕长^2 + 屏幕宽^2}}\n$$\n\n### 2.2 像素\n\n- 邻接性：\n  有时候也用非矩形的邻域（比如：圆形），但矩形邻域是目前为止最好的邻域，因为它在计算上更为容易\n\n  ![](images/adjacency.png)\n\n- 像素通路：从起点像素到终点像素相邻像素的连线\n  必须保证唯一，创建通路前要确定使用哪种邻接\n\n- 像素间距离计算：例，求 $A(x_a, y_a)、B(x_b, y_b)$ 间的距离\n  欧式距离：最短距离 $D_e(A, B) = \\sqrt{(x_b - x_a)^2+ (y_b - y_a)^2}$\n  D4 距离：每次只能横、竖、走 4 邻接像素 $D_4(A,B) = |x_b - x_a|, |y_b - y_a|$\n  D8 距离：每次只能横、竖、**斜**走 8 邻接像素 $D_8(A,B) = max(|x_b - x_a|, |y_b - y_a|)$\n\n\n\n\n## 3. 图像处理\n\n### 3.1 图像的收缩和放大\n\n- 线性插值（nearest）：例，根据 $Q(x_0,y_0)，R(x_1,y_1)  \\Rightarrow  P_{插值后}(x, y)$\n  $$\n  {y - y_0 \\over x - x_0} = {y_1 - y_0 \\over x_1 - x_0}\n  $$\n\n- 双线性插值（bilinear）：**结果与插值的 X、Y 方向先后顺序无关**\n  缺点：对角线过渡不平滑，细节退化\n  计算方法：\n\n  1. X 方向线性插值：根据 4 个 Q 点分别求出 R1，R2\n  2. Y 方向线性插值：根据 R1，R2 求出 P\n  ![](images/interp2.png)\n  \n- 双三次内插值（bicubic）：双线性插值的三维拓展\n\n\n\n### 3.2 图像的线性操作\n\n- 阵列操作：表示图像的矩阵中每个对应元素之间的操作\n  例，**阵列**相乘\n  $$\n  \\begin{bmatrix}\n  \\color{red}{a_{11}} & \\color{red}{a_{21}} \\\\\n  a_{12} & a_{22} \\\\\n  \\end{bmatrix}\n  \\begin{bmatrix}\n  \\color{green}{b_{11}} & b_{21} \\\\\n  \\color{green}{b_{12}} & b_{22} \\\\\n  \\end{bmatrix}\n  =\n  \\begin{bmatrix}\n  \\color{red}{a_{11}}\\color{green}{b_{11}} & \\color{red}{a_{21}}b_{21} \\\\\n  a_{12}\\color{green}{b_{12}} & a_{22}b_{22} \\\\\n  \\end{bmatrix}\n  $$\n\n- 符合线性代数的条件下使用线性代数公式\n  例，**矩阵**相乘：行 X 列\n  $$\n  \\begin{bmatrix}\n  \\color{red}{a_{11}} & \\color{red}{a_{21}} \\\\\n  a_{12} & a_{22} \\\\\n  \\end{bmatrix}\n  \\begin{bmatrix}\n  \\color{green}{b_{11}} & b_{21} \\\\\n  \\color{green}{b_{12}} & b_{22} \\\\\n  \\end{bmatrix}\n  =\n  \\begin{bmatrix}\n  \\color{red}{a_{11}}\\color{green}{b_{11}}+\\color{red}{a_{21}}\\color{green}{b_{12}} &\n  \\color{red}{a_{11}}b_{21}+\\color{red}{a_{21}}b_{22} \\\\\n  a_{12}\\color{green}{b_{11}}+a_{22}\\color{green}{b_{12}} & a_{12}b_{21}+a_{22}b_{22} \\\\\n  \\end{bmatrix}\n  $$\n\n- [其他线性代数问题](https://github.com/CatOnly/CrashNote/blob/master/LinearAlgebra/Part0.md)\n\n\n\n### 3.3 图像的概率方法\n\n- 基本概率：概率 = 符合条件事件数 / 事件总数\n- 期望值：概率加权后的和\n  当事件总数不可知（或者无法求得），但是知道概率分布（符合条件的每个概率值），从而求得的整体的平均值\n- 方差：随机变量和期望的偏离程度\n  方差公式：$\\sigma^2 = {\\sum_1^n(X - \\mu)^2 \\over n}, \\sigma 标准差、X 随机变量、\\mu期望、n随机变量总量$\n- **概率密度函数（PDF）**：连续随机变量存在随机变量 $x$ 与其出现的概率 $f(x)$，求规定 $x$ 范围内的可能性，就是求概率密度函数 $f(x)$ 在这个范围的积分\n- **累积分布函数（CDF）**：概率密度函数的积分，能完整描述一个实随机变量 $X$ 的概率分布\n\n\n\n### 3.4 图像的灰度变换函数 \n\n- 图像反转：$ s = (L-1) -r $\n  突出图像暗区域中白色或灰色部分\n\n- 对数变换：$s = c \\cdot  log(1 + r)，c  常数$\n  主要用来压缩像素值变化较大的动态范围\n\n- 伽马变换：$s = c \\cdot r^\\gamma，c 常数$\n  又称幂律变换，当 c = 1 时，**显著**提高暗部亮度，可以提高亮部的亮度，如图下所示\n\n  ![](images/graphy0.png)\n\n\n\n### 3.5 图像的直方图处理\n\n作用：通过在单张灰度图中统计 0 - 255 每个灰度级出现的总次数（概率），作为图像增强或图像阈值判断标准\n限制：直方图只能测量一种强度值（R、G、B、A、Gray）的分布概率范围\n特点：暗的图像直方图主要分布在灰度级低的位置，高对比度的图像直方图分布均匀，低对比度的直方图主要分布在灰度级的中间位置\n\n直方图的生成\n\n```c\nint pixelCount = width * height;\nfor (int i = 0; i < pixelCount; ++i )\n{\n    histogram[imageData[i]]++; // 计算单张图中每个灰度级出现的总次数\n}\n```\n\n\n\n累加直方图：在单张灰度图中统计 0 - 255 每个灰度等级，小于等于该灰度的等级出现总次数之和\n\n生成累加直方图\n\n```c\n// build a cumulative histogram as LUT (Look Up Table)\nint sum = 0;\nfor (int i=0; i < HISTOGRAM_SIZE; ++i )\n{\n    sum += histogram[i];\n    sumHistogram[i] = sum;\n}\n```\n\n\n\n**直方图均衡**：利用图像直方图 **对** 对比度进行调整的**方法**\n\n- 作用：用于作为增强局部对比度而不影响整体对比度的自适应工具\n\n- 方法：原始直方图 => 累加直方图 =>将最大强度通过出现概率平滑过度到每个像素  => 均衡后的直方图\n\n  ```c\n  // 一、直接对图像进行直方图均衡\n  // transform image using sum histogram as a LUT (Look Up Table)\n  for (int i = 0; i < pixelCount; ++i )\n  {\n    \t// 通过这张图的最大强度和当前像素的累加出现概率来求出修改后的像素值\n      outImage[i] = sumHistogram[image[i]] * MAX_INTENSITY / pixelCount;\n  }\n  \n  // 二、由于 pixelCount 远大于 HISTOGRAM_SIZE 这种固定值\n  //    通过提前制作的查找表来做直方图均衡（减少计算量，提高效率）\n  // build a LUT containing scale factor\n  for (int i=0; i < HISTOGRAM_SIZE; ++i )\n  {\n      sum += histogram[i];\n      lut[i] = sum * MAX_INTENSITY / pixelCount;\n  }\n  \n  // transform image using sum histogram as a LUT\n  for (int i = 0; i < pixelCount; ++i )\n  {\n      outImage[i] = lut[image[i]];\n  }\n  ```\n\n- 效果对比：均衡后的累加直方图几乎是线性增长的\n\n  ![](./images/histoEqualization.png)\n\n  \n\n**直方图匹配**：处理图像使其得到指定的概率密度\n\n- 作用：通过得到**指定**概率密度的 灰度变换函数使图像局部增强\n- 方法：不需要求得 灰度变换函数，只需要存储 灰度变换函数对应的 输入输出表格用查表法来得到相应的数据\n\n\n\n\n# 二、空间域滤波\n\n> 空间域技术：直接在图像像素上操作，针对图像本身，**可用于非线性滤波，这在频率域中无法做到**\n>\n> 滤波：接受/拒绝 一定的频率分量\n> 低通滤波器：通过低频的滤波器处理结果模糊（平滑）的图像\n> 空间滤波器（也称为，空间掩膜、核、模版、窗口）\n\n## 1. 空间滤波基础\n\n- 空间滤波方法：通过中心像素和其邻域**执行特定的方法**生成新像素\n- 空间滤波器（模版）：通过特定的方法得到的对应像素的权重集合\n- 线性空间滤波器：执行特定的方法是线性操作的空间滤波器（对于边缘像素的计算结果，根据纹理的环绕方式的不同而不同）\n  ![](images/spaceFilter.png)\n  空间相关/卷积：滤波器移过图像，计算每个位置乘积之和得出对应像素值的处理方法\n\n> 离散单位冲激：包含单个 1 其余都是 0 的函数/矩阵\n\n- 处理方法，**空间相关**：\n    $$\n    输入图像\n    \\begin{bmatrix}\n    0 & 0 & 0 & 0 & 0\\\\\n    0 & 0 & 0 & 0 & 0\\\\\n    0 & 0 & \\color{red}1 & 0 & 0\\\\\n    0 & 0 & 0 & 0 & 0\\\\\n    0 & 0 & 0 & 0 & 0\n     \\end{bmatrix}\n     \\Rightarrow\n     滤波器 w\n     \\begin{bmatrix}\n    1 & 2 & 3\\\\\n    4 & 5 & 6\\\\\n    7 & 8 & 9\n     \\end{bmatrix}\n     {逐像素处理 \\over \\Rightarrow}\n     全部相关结果\n    \\begin{bmatrix}\n    0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0\\\\\n    0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0\\\\\n    0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0\\\\\n    0 & 0 & 0 & \\color{red}9 & \\color{red}8 & \\color{red}7 & 0 & 0 & 0\\\\\n    0 & 0 &0 & \\color{red}6 & \\color{red}5 & \\color{red}4 & 0 & 0 & 0\\\\\n    0 & 0 &0 & \\color{red}3 & \\color{red}2 & \\color{red}1 & 0 & 0 & 0\\\\\n    0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0\\\\\n    0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0\\\\\n    0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0\n     \\end{bmatrix}\n     { 剪裁 \\over \\Rightarrow}\n    输出图像\n    \\begin{bmatrix}\n    0 & 0 & 0 & 0 & 0\\\\\n    0 & \\color{red}9 & \\color{red}8 & \\color{red}7 & 0\\\\\n    0 & \\color{red}6 & \\color{red}5 & \\color{red}4 & 0\\\\\n    0 & \\color{red}3 & \\color{red}2 & \\color{red}1 & 0\\\\\n    0 & 0 & 0 & 0 & 0\n     \\end{bmatrix}\n    $$\n\n- 处理方法，**空间卷积**：\n  $$\n  输入图像\n  \\begin{bmatrix}\n  0 & 0 & 0 & 0 & 0\\\\\n  0 & 0 & 0 & 0 & 0\\\\\n  0 & 0 & \\color{red}1 & 0 & 0\\\\\n  0 & 0 & 0 & 0 & 0\\\\\n  0 & 0 & 0 & 0 & 0\n   \\end{bmatrix}\n   {180 度翻转 w  \\over \\Rightarrow}\n   滤波器 w^T\n   \\begin{bmatrix}\n  9 & 8 & 7\\\\\n  6 & 5 & 4\\\\\n  3 & 2 & 1\n   \\end{bmatrix}\n   {逐像素处理 \\over \\Rightarrow}\n   全部卷积结果\n  \\begin{bmatrix}\n  0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0\\\\\n  0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0\\\\\n  0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0\\\\\n  0 & 0 & 0 & \\color{red}1 & \\color{red}2 & \\color{red}3 & 0 & 0 & 0\\\\\n  0 & 0 & 0 & \\color{red}4 & \\color{red}5 & \\color{red}6 & 0 & 0 & 0\\\\\n  0 & 0 & 0 & \\color{red}7 & \\color{red}8 & \\color{red}9 & 0 & 0 & 0\\\\\n  0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0\\\\\n  0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0\\\\\n  0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0\n   \\end{bmatrix}\n   { 剪裁 \\over \\Rightarrow}\n  输出图像\n  \\begin{bmatrix}\n  0 & 0 & 0 & 0 & 0\\\\\n  0 & \\color{red}1 & \\color{red}2 & \\color{red}3 & 0\\\\\n  0 & \\color{red}4 & \\color{red}5 & \\color{red}6 & 0\\\\\n  0 & \\color{red}7 & \\color{red}8 & \\color{red}9 & 0\\\\\n  0 & 0 & 0 & 0 & 0\n   \\end{bmatrix}\n  $$\n  \n\n\n\n## 2. 平滑空间滤波器\n\n**作用：用于模糊处理，降低噪声**\n使用 3 X 3、5 X 5 或更大的空间滤波器，效果更明显\n\n### 2.1 平滑线性滤波器（均值滤波器）\n\n方法：中心像素的值为滤波器模版邻域内像素简单的加权平均值\n特点：滤波器个系数加权之和为 1\n缺点：出现边缘模糊\n例：\n$$\n\\begin{array}{rr}\n{ 1 \\over 9 } \\times\n \\begin{bmatrix}\n1 & 1 & 1\\\\\n1 & 1 & 1\\\\\n1 & 1 & 1\n \\end{bmatrix}\n &\n { 1 \\over 16 } \\times\n  \\begin{bmatrix}\n1 & 2 & 1\\\\\n2 & 4 & 2\\\\\n1 & 2 & 1\n \\end{bmatrix}\n\\\\\n均值滤波器 A & 均值滤波器 B\n\\end{array}\n$$\n\n### 2.2 统计排序（非线性）滤波器\n\n方法：中心像素的值为滤波器模版邻域内像素值安一定规则排序后，从统计排序结果中选取的\n\n缺点：非线性滤波器可能改变图像的性质，这在医学是不能接受的\n\n例：中值滤波，中心像素的值为滤波器模版邻域内像素值从小到大排列后，位置排在中间的像素的值\n\n\n\n## 3. 锐化空间滤波器\n\n**作用：突出灰度过渡的部分**\n锐化空间滤波器的关键数学工具：**微分**\n**微分**：函数  $f(x)$ 在 $dx$ 的微分是，函数  $f(x)$ 在 $dx$ 处的极限，具体意义如下\n\n- 直线 T 为 曲线 $f(x)$ 上 P 点的切线，是 ${df \\over dx}$ 的图像\n  曲线 L 为 曲线 $f(x)$ 的图像\n- 当增量 $dx$ 趋于无穷小时， $f(x) - df$ 的差别也趋于无穷小，所以**在 P 点附近**可以用 切线 T 来的代替曲线 L 的值\n\n![](images/differential.jpg)\n\n一维的微分公式\n- 一阶微分：${df \\over dx} = {f(x+1) - f(x) \\over (x+1) -x} = f(x+1) - f(x)$\n- 二阶微分：${d^2f \\over dx^2} = {d({df \\over dx})\\over dx} = f(x+1) +f(x-1) - 2f(x)$\n\n### 3.1 梯度（一阶微分）\n\n特点：边缘增强\n\n方法：\n\n- 具有旋转不变的特性（各向同性），根据对一阶微分 ${df \\over dx}$ 的计算不同而不同，基本思路是：\n  $g_{中心}(x,y) = \\sqrt{({df \\over dx})^2 + ({df \\over dy})^2}$\n- 具有旋转改变的特性（各向异性），为了方便计算：\n  $g_{中心}(x,y) \\approx |{df \\over dx}| + |{df \\over dy}|$\n- 为不影响其他像素的值，滤波器个系数加权和为 0\n\n### 3.2 拉普拉斯算子（二阶微分）\n\n特点：增强图像细节，增强灰度的突变，减弱灰度的缓慢变化区\n缺点：比梯度操作产生更多的噪点\n\n拉普拉斯算子是二维的二阶微分，公式为：$\\Delta^2f(x, y) = f(x+1,y) +f(x-1,y)+f(x,y+1) +f(x,y-1)-4f(x,y)$\n\n使用方法：\n\n- $g_{中心}(x,y) = f_{中心}(x,y)+滤波器中心系数的符号\\cdot \\Delta^2f(x,y)$\n- 为不影响其他像素的值，滤波器个系数加权和为 0\n\n### 3.3 非锐化掩蔽和高提升滤波（不用微分）\n\n方法：原图像 A ${模糊 \\over \\Rightarrow}$ 模糊图像 G ${A - G \\over \\Rightarrow}$ 差值图像 D ${A + D \\over \\Rightarrow}$ 锐化图像\n\n### 3.4 多种滤波的混合使用举例\n\n锐化图像：原图像 $\\rightarrow$ 梯度滤波 $\\rightarrow$ 平滑线性滤波 $\\rightarrow$ 拉普拉斯滤波 $\\rightarrow$ 锐化后的图像\n\n\n\n## 4. 模糊技术\n\n作用：为处理不严密信息提供了一种形式\n\n模糊集合：集合中的某元素 A 在集合中的隶属度（符合集合定义的程度，一般介于 0 ～ 1）组成的有序对\n\n集合操作与数学计算：\n\n- 集合 A   的补集：$1 - A$\n- 集合 AB 的并集：$max(A, B)$\n- 集合 AB 的交集： $min(A, B)$\n\n**方法**：\n\n1. 找到处理图像的**各个**影响因素和其对应的隶属度的关系 A、B、C...\n2. 将**各个**关键关系 A、B、C… 进行集合操作得到模糊集合\n3. 将模糊集合通过**一种计算方法**（以下几个例子都用了计算集合中心的方法）得出一个计算方程\n\n### 4.1 模糊技术处理灰度\n\n根据 黑、灰、白 和 其隶属度的关系 的模糊集合，使用重心的计算方法得到灰度处理方法\n$$\nv_{输出} = {\nf_黑(x_{隶属度}) \\times  v_黑 + g_灰(x_{隶属度}) \\times  v_灰 + h_白(x_{隶属度}) \\times  v_白 \n\\over \nf_黑(x_{隶属度})  + g_灰(x_{隶属度})  + h_白(x_{隶属度}) \n}\n$$\n\n### 4.2 模糊技术进行空间滤波\n\n假设 黑色 == 平滑，白色 == 边缘，则边缘检测如下\n\n图 4.2.A 为：灰度差为 0 和其集合隶属关系\n图 4.2.B 为：黑色 和 白色与其集合隶属关系\n根据图 4.2.A、B 构成的模糊集合通过边缘提取的计算方法（只要与中心像素 **4邻接**的两个**相邻**灰度差均为 0 时，中间点为白色）\n\n![](./images/fuzzySet.png)\n\n\n\n\n# 三、频率域滤波\n> 傅立叶概念：$任何周期函数 = (系数 1 \\times 正弦) 和/或 (系数2 \\times 余弦)$\n\n常用数学公式\n$$\n\\begin{align}\n复数 &= 实数_{实部} + 实数_{虚部}\\cdot i_{虚数单位}, (i_{虚数单位} = \\sqrt{-1} )\\\\\n复数的共轭 &= 实数_{实部} - 实数_{虚部}\\cdot i\\\\\nf(x)_{复数} &= R(x)_{实部} + J(x)_{虚部} \\cdot i\\\\\nf(x)_{复数的共轭} &= R(x)_{实部} - J(x)_{虚部} \\cdot i\\\\\n复数_{极坐标下} &= \\sqrt{实部^2 + 虚部^2}(cos\\theta + i \\cdot sin\\theta)\\\\\n复数_{极坐标下}  &= \\sqrt{实部^2 + 虚部^2}e^{i\\theta},(欧拉公式: e^{\\theta i} = cos\\theta + i\\cdot sin\\theta，e = 2.71828 …)\\\\\nf(x)_{傅立叶级数} &= \\sum_{n = -\\infty}^\\infty c_n e^{i{2\\pi n\\over T_{周期}}x},(i = \\sqrt{-1}, c_n = {1 \\over T}\\int_{-{T \\over 2}}^{T \\over 2} f(x)e^{-i{2\\pi n\\over T}x}dx, n = 0, \\pm1, \\pm2...)\n\\end{align}\n$$\n\n## 1. 频率域滤波基础\n### 1.1 基本概念\n\n### 1.2 傅立叶变换\n#### 1.2.1 取样函数的傅立叶变换\n#### 1.2.2 单变量的离散傅立叶变换(DTF)\n#### 1.2.3 二维离散傅里叶变换的性质\n\n## 2. 频率域滤波平滑图像\n## 3. 频率域滤波锐化图像\n\n## 4. 选择性滤波\n\n## 5. 滤波器实现\n\n\n\n# Reference\n\n1. [Histogram](http://www.songho.ca/dsp/histogram/histogram.html)"
  },
  {
    "path": "DigitalImageProcessing/Part2_Colors.md",
    "content": "# 一、彩色图像处理\n\n人眼对明暗比颜色更加敏感，RGB 色光三原色中人眼对绿色比较敏感\n\n\n\n## 1. 颜色的特性\n\n### 1.1  颜色的心理学特征：人对光的感觉而产生的光的特性\n\n**色彩 Hue：**\n光谱（一定电磁频谱范围内）中的主频率/主波长\n\n**纯度 Purity / 饱和度 Satruation：**\n一种颜色混合白色的比例（100% 是无白光混合，纯度最高），**纯度高了色彩会鲜亮**\n\n**色度 Chromaticity：**\n代表了光的纯度和色彩这两种特征的组合\n\n**亮度 Brightness：**\n光源的强度/振幅，物体表面的反光率（实际上不能度量，人眼对亮度的敏感 > 色彩）\n\n- Luminance(Y)：物理度量，没有具体的大小范围，是某个方向上行进的每单位面积光的发光强度\n- Luma(Y')：通过将物理度量的亮度伽马矫正后，具有固定大小范围的相对亮度\n\n\n\n### 1.2 直观的颜色概念\n\n**明暗 Shades：**\n纯色颜料里添加黑色颜料\n\n**色泽 Tints：**\n纯色颜料里添加白色颜料\n\n**色调 Tones：**\n纯色颜料里同时添加黑色和白色颜料\n\n**补色 Complementary Color：**\n两个颜色相加是白色，则互为补色（在色环上，与一个色调直接相对的另一端）\n让颜色**变亮**不是加白，是**减去补色**，这样**色调才不会改变**（用于增强图像暗区细节）\n\n\n\n\n\n## 2. 色彩模型\n\n> 颜色模型是在某种情况下对颜色的特征和行为的解释，没有哪种颜色模型能解决所有的颜色问题\n>\n> 较少的颜色比使用较多的颜色能产生更令人满意的显示，淡色和暗色的混合比纯色彩更柔和\n\n\n\n### 2.1 RGB 色彩模型\n\n特点：基于三刺激理论，适合色彩生成，适合硬件设备对于色彩的实现\n\n加色混色模型：颜色混在一起亮度增大，**R**ed、**G**reen、**B**lue 三种颜色的取值范围是 [0, 255]\n\n![](images/modelRGB.png)\n\n使用 RGB 色彩模型的应用：\n\n- [3D LUT 的 Unity 实现](https://zhuanlan.zhihu.com/p/43241990)\n- [3D LUT Creator Tutorials](https://www.3dlutcreator.com/3d-lut-creator---tutorials.html)\n\n\n\n### 2.2 CMY 和 CMYK 色彩模型 \t \n\n特点：适合色彩生成，适合硬件设备对于色彩的实现\n\n减色混色模型：颜色混在一起亮度降低，**C**yan (青)、**M**agenta (品红)、**Y**ellow(黄)、Blac**K**(黑)，加入黑色是因为打印时由品红、黄、青构成的黑色不够纯粹\n\n![](images/modelCMYK.png)\n\n#### 2.2.1 RGB 与 CMY 的转换\n\n- 公式：假设所有彩色值都归一化到了 [0, 1] 的范围内\n  ​\n  $$\n  \\begin{bmatrix}\n  C\\\\\n  M\\\\\n  Y\n  \\end{bmatrix}\n  =\n  \\begin{bmatrix}\n  1\\\\\n  1\\\\\n  1\n  \\end{bmatrix}\n  - \n  \\begin{bmatrix}\n  R\\\\\n  G\\\\\n  B\n  \\end{bmatrix}\n  $$\n\n\n\n\n\n\n### 2.3 HSI、HSL 和 HSV 色彩模型 \n\n特点：适合色彩描述、电脑绘画、图像算法的处理，人能观察的色彩并不是由 RGB 三种颜色混合而成，而是取决于颜色的亮度、色调、饱和度。**HSI、HSL、HSV 方便和RGB 进行互相转换**\n> HSI、HSL 和 HSV 更多的是一种色彩模型，并不是一个绝对不变的色彩空间，它的取值范围，取决于来自于 RGB 输入的取值范围\n\n#### 2.3.1 关键概念\n\n- **H**ue：色相，决定什么颜色，对应 **红 (0°) 绿(120°) 蓝(240°)** 首尾相接的色相环值，取值范围 [0, 360)\n- **S**aturation：饱和度，决定颜色浓淡，一种颜色混合白光的比例（100% 是无白光混合），物体反射的颜色，**饱和度高了色彩会鲜亮**\n- **V**alue：饱和度值\n- **L**ightness：明度，光源的明暗，人们所感知到的色彩明暗度\n- Brightness/Luminance/Intensity：亮度，光的振幅，物体表面的反光率，表面色彩白色的多少\n- Chroma：色度 = 色相(方向) + 饱和度(大小)，一种颜色混合白光的比例（100% 是无白光混合）\n\n#### 2.3.2 各自的特点\n\nHSI 主要用于方便处理图片色彩，而非用于修改和选择颜色\nHSL 和 HSV 有相同的灰度定义，但在 饱和度 和 亮度的定义方面是不同的：\n\n- 亮度：HSL 最大为 0.5，HSV 最大为 1\n- 饱和度：饱和度为 1 时，HSL 亮度为 0.5，HSV 亮度为 1\n\n\n明度和亮度：都决定照射在颜色上的白光有多亮，亮度与颜色的辐射能量有关，但能量高的颜色不一定明度高。例，蓝色的能量很高，但其明度却低 [more](https://en.wikipedia.org/wiki/HSL_and_HSV)\n\nHSL、HSV、HSI缺点：\n\n- 在高色度上的亮度相对于 YUV 偏离过多\n- 在选择颜色和配色方案上存在问题\n\n![](images/modelHSLV.png)\n\n#### 2.3.3 转换公式推导\n\n从 RGB 到 HSI 或 HSL 或 HSV 的 Hue 是由 RGB 的如下整投影得到的：\n\n- M：max(R, G, B) 、m：min(R, G, B)、C：色度 = M - m\n\n![](images/RGBToChroma.png)\n\n- I: HSI 里的 I，Y'：YUV 里的灰度值\n  ![](images/Luma.png)\n\n#### 2.3.4 RGB 与 HSL 的互相转换\n\n**RGB 转 HSL**\n\n- 公式：\n  如果 R = G = B 时，颜色是非彩色的，H (色相) 无定义，H = S = 0\n  max 是 R、G、B 中的最大值\n  min  是 R、G、B 中的最小值\n\n  $R \\in [0,1], G \\in [0,1], B \\in [0,1]$\n  $H \\in [0, 360), S \\in [0,1], L \\in [0,1]$\n  ​\n  $$\n  \\begin{aligned}\n  \n  H &= \n  \\begin{cases}\n  0, &\\text{if max = min}\\\\\n  {G - B \\over max - min} \\times 60  , &\\text{if max = R  and G $\\geq$ B} \\\\\n  {G - B \\over max - min} \\times 60  + 360, &\\text{if max = R  and G $\\lt$ B}\\\\\n  {B - R \\over max - min} \\times 60  + 120, &\\text{if max = G}\\\\\n  {R - G \\over max - min} \\times 60  + 240, &\\text{if max = B} \\\\\n  \\end{cases} \\\\\\\\\n  L &= \\frac 12(max + min) \\\\\\\\\n  S & = \n  \\begin{cases}\n  0 , &\\text{if max = min or L = 1}\\\\\n  {max - min \\over 1 - |2L - 1|}  , &\\text{otherwise}\n  \\end{cases} \n  \\end{aligned}\n  $$\n\n- 代码：\n\n  ```c\n  // GLSL\n  vec3 RGBToHSL(vec3 color) {\n      float minRGB = min(min(color.r, color.g), color.b);\n      float maxRGB = max(max(color.r, color.g), color.b);\n      float sum = maxRGB + minRGB;\n      float chroma = maxRGB - minRGB;\n      float luminance = 0.5 * sum;\n  \n      vec3 hsl = vec3(0.0, 0.0, luminance);\n   \n      if (chroma == 0.0) return hsl; // R = G = B, 颜色是非彩色的 S = 0，这是色相无定义 H = 0\n  \n      // Saturation\n      hsl.y = luminance == 1.0 ? 0.0 : chroma / (1.0 - abs(2.0 * luminance - 1.0));\n      \n      // Hue 原来范围是 [0, 360), 这里默认输入的范围是 [0, 1]\n      /** 尽管这样并不一定能真的提高效率，但是这个规避 if else 的思想值得思考 \n          vec3 comp;\n          comp.rg = vec2(equal(rgb.rg, vec2(maxRGB)));\n          float invertR = 1.0 - comp.r;       // 0 or 1\n          comp.g *= invertR;                  // g = invertR * g\n          comp.b  = invertR * (1.0 - comp.g); // b = invertR * invertG\n          hsl.x = dot(comp, vec3((color.g - color.b) / chroma, (color.b - color.r) / chroma + 2.0, (color.r - color.g) / chroma + 4.0));\n          hsl.x /= 6.0;\n      */\n      if (color.r == maxRGB) {\n          hsl.x =  (color.g - color.b) / chroma / 6.0;         \n      } else if (color.g == maxRGB) {\n          hsl.x = ((color.b - color.r) / chroma + 2.0) / 6.0; \n      } else {\n          hsl.x = ((color.r - color.g) / chroma + 4.0) / 6.0; \n      }\n  \t\n      // Optimize\n      // hsl.x += 1.0 - step(0.0, hsl.x);\n      if (hsl.x < 0.0) hsl.x += 1.0; \n      \n      return hsl;\n  }\n  ```\n\n**HSL 转 RGB**\n- 公式：\n  如果 S (饱和度)  = 0，则颜色是非彩色的。H (色相)无意义， R = G = B = L (亮度)\n  如果 S (饱和度) != 0，有如下公式\n\n  $R \\in [0,1], G \\in [0,1], B \\in [0,1]$\n  $H \\in [0, 360), S \\in [0,1], L \\in [0,1]$\n  > H' 取整数时，两边计算结果相同\n\n  $$\n  \\begin{aligned}\n  H' &= {H \\over 60}\\\\\n  C &= (1 - |2L - 1|) \\times S \\\\\n  X &=  (1 - |H' \\pmod 2 - 1|)\\times C \\\\\n  m &= L - \\frac 12 C \\\\\\\\\n  (R, G, B) &= \n  \\begin{cases}\n  (C + m, X + m,  m), &\\text{if $0 \\leq H' \\leq 1$}\\\\\n  (X + m, C + m,  m), &\\text{if $1 \\leq H' \\leq 2$} \\\\\n  (m, C + m,  X + m), &\\text{if $2 \\leq H' \\leq 3$} \\\\\n  (m, X + m,  C + m), &\\text{if $3 \\leq H' \\leq 4$} \\\\\n  (X + m, m,  C + m), &\\text{if $4 \\leq H' \\leq 5$} \\\\\n  (C + m, m,  X + m), &\\text{if $5 \\leq H' \\lt 6$} \\\\\n  \\end{cases}\\\\\n  \\end{aligned}\n  $$\n\n- 代码：\n```c\nvec3 HSLToRGB(vec3 color) {\n    if (color.y == 0.0) return vec3(color.z); // Luminance\n    \n    float hue = color.x;\n    hue *= 6.0; \t\t\t\t\t  // Hue 默认范围是 [0, 1]\n    float chroma = (1.0 - abs(2.0 * color.z - 1.0)) * color.y;\n    float m = color.z - 0.5 * chroma; // Lightness\n    float x = (1.0 - abs(mod(hue, 2.0) - 1.0)) * chroma;\n    \n    if (hue < 1.0){\n        return vec3(chroma + m, x + m, m);\n    } else if (hue < 2.0){\n        return vec3(x + m, chroma + m, m);\n    } else if (hue < 3.0){\n    \treturn vec3(m, chroma + m, x + m);\n    } else if (hue < 4.0){\n        return vec3(m, x + m, chroma + m);        \n    } else if (hue < 5.0){\n        return vec3(x + m, m, chroma + m);\n    } else {\n        return vec3(chroma + m, m, x + m);\n    }\n}\n```\n\n#### 2.3.5 RGB 与 HSV 的互相转换\n\n**RGB 转 HSV**\n\n- 公式：\n  如果 R = G = B 时，颜色是非彩色的，H (色相) 无定义，H = S = 0\n  max 是 R、G、B 中的最大值\n  min  是 R、G、B 中的最小值\n\n  $R \\in [0,1], G \\in [0,1], B \\in [0,1]$\n  $H \\in [0, 360), S \\in [0,1], V \\in [0,1]$\n\n$$\n\\begin{aligned}\n\nH &= \n\\begin{cases}\n0, &\\text{if max = min}\\\\\n{G - B \\over max - min} \\times 60  , &\\text{if max = R  and G $\\geq$ B} \\\\\n{G - B \\over max - min} \\times 60  + 360, &\\text{if max = R  and G $\\lt$ B}\\\\\n{B - R \\over max - min} \\times 60  + 120, &\\text{if max = G}\\\\\n{R - G \\over max - min} \\times 60  + 240, &\\text{if max = B} \\\\\n\\end{cases} \\\\\\\\\nS & = \n\\begin{cases}\n0 , &\\text{if max = 0}\\\\\n{max - min \\over max}, &\\text{if max $\\neq 0$}\n\\end{cases} \\\\\\\\\nV &= max\n\\end{aligned}\n$$\n\n- 代码：\n\n ```c\n// GLSL\nvec3 RGBToHSV(vec3 color) {\n\tfloat minRGB = min(min(color.r, color.g), color.b);\n    float maxRGB = max(max(color.r, color.g), color.b);\n    float sum = maxRGB + minRGB;\n    float chroma = maxRGB - minRGB;\n    \n    // V = maxRGB;\n    vec3 hsv = vec3(0.0, 0.0, maxRGB);\n    if (chroma == 0.0) return hsv;\n    \n    // Saturation\n    hsv.y = maxRGB == 0.0 ? 0.0 : chroma / maxRGB;\n    \n    // Hue 原来范围是 [0, 360), 这里默认输入的范围是 [0, 1]\n    if (color.r == maxRGB) {\n        hsv.x =  (color.g - color.b) / chroma / 6.0;         \n    } else if (color.g == maxRGB) {\n        hsv.x = ((color.b - color.r) / chroma + 2.0) / 6.0; \n    } else {\n        hsv.x = ((color.r - color.g) / chroma + 4.0) / 6.0; \n    }\n   \n    // Optimize\n    // hsv.x += 1.0 - step(0.0, hsv.x);\n    if (hsv.x < 0.0) hsv.x += 1.0; \n    \n    return hsv;\n}\n ```\n\n**HSV 转 RGB**\n\n- 公式：\n  如果 S (饱和度)  = 0，则颜色是非彩色的。H (色相)无意义， R = G = B = L (亮度)如果 S (饱和度) != 0，有如下公式\n\n  $R \\in [0,1], G \\in [0,1], B \\in [0,1]$\n  $H \\in [0, 360), S \\in [0,1], V \\in [0,1]$\n  \n  $$\n  \\begin{aligned}\n  H' &= {H \\over 60}\\\\\n  C &= V \\times S \\\\\n  X &=  (1 - |H' \\pmod 2 - 1|)\\times C \\\\\n  m &= V - C \\\\\\\\\n  (R, G, B) &= \n  \\begin{cases}\n  (C + m, X + m,  m), &\\text{if $0 \\leq H' \\leq 1$}\\\\\n  (X + m, C + m,  m), &\\text{if $1 \\leq H' \\leq 2$} \\\\\n  (m, C + m,  X + m), &\\text{if $2 \\leq H' \\leq 3$} \\\\\n  (m, X + m,  C + m), &\\text{if $3 \\leq H' \\leq 4$} \\\\\n  (X + m, m,  C + m), &\\text{if $4 \\leq H' \\leq 5$} \\\\\n  (C + m, m,  X + m), &\\text{if $5 \\leq H' \\lt 6$} \\\\\n  \\end{cases}\\\\\n  \\end{aligned}\n  $$\n  > **⌊ ⌋ Floor    向下取整：** 比自己小的最大整数，舍弃小数位\n  > **⌈ ⌉ Ceiling 向上取整：** 比自己大的最小整数，有小数位就进 1\n  > $\\equiv$ ：\n  >\n  > 1. 恒等号 一般用于一些参变量恒为一个常数或恒定表达式时，总等于关系与变量无关。例 $f(x) \\equiv k，f(x)$ 的值始终为 k 而与 x 无关\n  > 2. 同余符号，例 $a \\equiv b \\pmod c$ ，a 和 b 分别除以 c 得到的余数相同\n\n- 代码：\n ```c\n// GLSL\nvec3 HSVToRGB(vec3 color) { \n    if (color.y == 0.0) return vec3(color.z); // Luminance\n    \n    float hue = color.x;\n    hue *= 6.0; \t\t\t     // Hue 默认范围是 [0, 1]\n    float chroma = color.z * color.y;\n    float m = color.z - chroma;  // Lightness\n    float x = (1.0 - abs(mod(hue, 2.0) - 1.0)) * chroma;\n    \n    if (hue < 1.0){\n        return vec3(chroma + m, x + m, m);\n    } else if (hue < 2.0){\n        return vec3(x + m, chroma + m, m);\n    } else if (hue < 3.0){\n    \treturn vec3(m, chroma + m, x + m);\n    } else if (hue < 4.0){\n        return vec3(m, x + m, chroma + m);        \n    } else if (hue < 5.0){\n        return vec3(x + m, m, chroma + m);\n    } else {\n        return vec3(chroma + m, m, x + m);\n    }\n}\n ```\n\n#### 2.3.6 RGB 与 HSI 的互相转换\n\n**RGB 转 HSI**\n\n- 公式：\n  如果 R = G = B 时，颜色是非彩色的，H (色相) 无定义，H = S = 0\n  max 是 R、G、B 中的最大值\n  min  是 R、G、B 中的最小值\n\n  $R \\in [0,1], G \\in [0,1], B \\in [0,1]$\n  $H \\in [0, 360), S \\in [0,1], I \\in [0,1]$\n  \n  $$\n  \\begin{aligned}\n  H &= \n  \\begin{cases}\n  0, &\\text{if max = min}\\\\\n  {G - B \\over max - min} \\times 60  , &\\text{if max = R  and G $\\geq$ B} \\\\\n  {G - B \\over max - min} \\times 60  + 360, &\\text{if max = R  and G $\\lt$ B}\\\\\n  {B - R \\over max - min} \\times 60  + 120, &\\text{if max = G}\\\\\n  {R - G \\over max - min} \\times 60  + 240, &\\text{if max = B} \\\\\n  \\end{cases} \\\\\\\\\n  I &= \\frac 1 3 (R+G+B)\\\\\\\\\n  S & = \n  \\begin{cases}\n  0 , &\\text{if $I$ = 0}\\\\\n  1 - {min \\over I}, &\\text{if $I \\neq 0$}\n  \\end{cases} \\\\\n  \\end{aligned}\n  $$\n\n- 代码：\n```c\n// GLSL\nvec3 RGBToHSI(vec3 color) {\n\tfloat minRGB = min(min(color.r, color.g), color.b);\n    float maxRGB = max(max(color.r, color.g), color.b);\n    float sum = maxRGB + minRGB;\n    float chroma = maxRGB - minRGB;\n    \n    // Intensity;\n    float intensity = dot(vec3(0.3333), color);\n    vec3 hsi = vec3(0.0, 0.0, intensity);\n    if (chroma == 0.0) return hsi;\n    \n    // Saturation\n    hsi.y = intensity == 0.0 ? 0.0 : 1.0 - minRGB / intensity;\n    \n    // Hue 原来范围是 [0, 360), 这里默认输入的范围是 [0, 1]\n    if (color.r == maxRGB) {\n        hsi.x =  (color.g - color.b) / chroma / 6.0;         \n    } else if (color.g == maxRGB) {\n        hsi.x = ((color.b - color.r) / chroma + 2.0) / 6.0; \n    } else {\n        hsi.x = ((color.r - color.g) / chroma + 4.0) / 6.0; \n    }\n   \n    // Optimize\n    // hsi.x += 1.0 - step(0.0, hsi.x);\n    if (hsi.x < 0.0) hsi.x += 1.0; \n    \n    return hsi;\n}\n```\n\n**HSI 转 RGB**\n\n- 公式：\n  如果 S (饱和度)  = 0，则颜色是非彩色的。H (色相)无意义， R = G = B = L (亮度)\n  如果 S (饱和度) != 0，有如下公式\n\n  $R \\in [0,1], G \\in [0,1], B \\in [0,1]$\n  $H \\in [0, 360), S \\in [0,1], I \\in [0,1]$\n  ​\n  $$\n  \\begin{aligned}\n  H' &= {H \\over 60}\\\\\n  Z &= 1 - |H' \\pmod 2 - 1| \\\\\n  C &=  {3 \\cdot S \\cdot I \\over 1 + Z} \\\\\n  X &= C \\cdot Z\\\\\n  m &= (1 - S) \\cdot I \\\\\\\\\n  (R, G, B) &= \n  \\begin{cases}\n  (C + m, X + m,  m), &\\text{if $0 \\leq H' \\leq 1$}\\\\\n  (X + m, C + m,  m), &\\text{if $1 \\leq H' \\leq 2$} \\\\\n  (m, C + m,  X + m), &\\text{if $2 \\leq H' \\leq 3$} \\\\\n  (m, X + m,  C + m), &\\text{if $3 \\leq H' \\leq 4$} \\\\\n  (X + m, m,  C + m), &\\text{if $4 \\leq H' \\leq 5$} \\\\\n  (C + m, m,  X + m), &\\text{if $5 \\leq H' \\lt 6$} \\\\\n  \\end{cases}\\\\\n  \\end{aligned}\n  $$\n\n- 代码：\n```c\n// GLSL\nvec3 HSIToRGB(vec3 color) { \n    if (color.y == 0.0) return vec3(color.z); // Luminance\n    \n    float hue = color.x;\n    hue *= 6.0;\t\t\t\t\t\t   // Hue 默认范围是 [0, 1]\n    float tmp = 1.0 - abs(mod(hue, 2.0) - 1.0);\n    float chroma = 3.0 * color.y * color.z / (1.0 + tmp);\n    float x = chroma * tmp;\n    float m = (1 - chroma) * color.z;  // Lightness\n\n    if (hue < 1.0){\n        return vec3(chroma + m, x + m, m);\n    } else if (hue < 2.0){\n        return vec3(x + m, chroma + m, m);\n    } else if (hue < 3.0){\n    \treturn vec3(m, chroma + m, x + m);\n    } else if (hue < 4.0){\n        return vec3(m, x + m, chroma + m);        \n    } else if (hue < 5.0){\n        return vec3(x + m, m, chroma + m);\n    } else {\n        return vec3(chroma + m, m, x + m);\n    }\n}\n```\n## 3. 色彩编码 YUV 和 YCbCr \n\n> YUV 和 YCbCr 更多的是一种色彩模型，并不是一个绝对不变的色彩空间，它的取值范围，取决于来自于 RGB 输入的取值范围\n\n特点：适合电视系统，数码摄影的色彩**压缩和传输**\n\n### 3.1 区分 YUV、YCbCr、Y'CbCr \n\n**YUV、YCbCr、Y'CbCr 相似之处**\n\n- Y 表示亮度，Y' 表示经过伽马矫正后的亮度\n- U 和 Cb 代表蓝色与亮度差\n- V 和 Cr 代表红色与亮度差\n\n\n\n**YUV、YCbCr 不同之处**\n\n- YUV：处理**模拟信号**的数据格式\n  Y 的范围是 [0, 1]，UV 的范围是 [-0.5, 0.5]\n- YCbCr (又称 YPbPr)：处理**数字信号**的数据格式，**YUV 色彩空间被缩放和偏移后是 YCbCr**\n  当使用 8 位存储单个分量时，Y 的范围是 [16, 235]，UV 的范围是 [16, 240]\n\n\n\n### 3.2 YUV 的采样方式和存储格式\n\n> 数字信号通常被压缩以减少文件大小并节省传输时间。由于人类视觉系统对亮度的变化比颜色更敏感，因此，视频系统可以通过向亮度分量 Y，赋予更多的带宽来优化，而不是在色差分量UV中 [more](https://en.wikipedia.org/wiki/Chroma_subsampling)\n\n**采样方式**\n采样通常为YUV J:A:B 三部分的比率，采样都从图片的左上角第一行逐行开始\n\n- J：单行水平方向的采样像素总个数，通常为 4\n- A：在两行采样像素矩形中，每行采样像素中色彩样本的个数\n- B：在两行采样像素矩形中，每列竖直方向色彩样本不同个数之和\n  ![](images/JAB.png)\n\n**存储格式**\n\n- 打包格式（packed）：YUV 存储在一个数组中（YUV 数据相邻）\n- 平面格式（planar）：Y、U、V 或 Y 、UV 分别作为不同的平面存储（YUV 数据互相独立）\n\n**采样方式 + 存储方式，实例**\n以下存储，一个像素点对应一个 Y，四个像素点根据采样比例不同对应不同个数的 UV\n\n- 采样方式 4:4:4 每像素 32 位（打包格式）\n  ![](images/YUV444.png)\n\n- 采样方式 4:2:2 每像素 16 位（打包格式）\n  ![](images/YUV422.png)\n\n- 采样方式 4:2:0 每像素 16 位（平面格式）\n  IMC2：在同一行数组中，先有 V 数据，再有 U 数据\n  YV12：在同一行数组中，只有 V 数据，当所有的 V 数据读取完后，才会只有紧跟的 U 数据\n  ![](images/YUV420.png)\n\n\n\n假设一个分辨率为 6X4 的 YUV420 格式图像，采样方式和存储方式如下图\n\n![](images/YUV420.svg)\n\n假设一个分辨率为 8X4 的 YUV420 格式图像\n\n- YUV422p：Y(行X列) + U(行X列 / 2) + V(行X列 / 2)\n- I420（打包格式：YV12）：Y(行X列) + V(行X列 / 4) + U(行X列 / 4)\n- YUV420p：Y(行X列) + U(行X列 / 4) + V(行X列 / 4)\n  ![](images/YUV420p.png)\n- YUV420sp（打包格式：NV12）：Y(行X列) + UV(行X列 / 2) \n  ![](images/YUV420sp.png)\n\n\n\n### 3.3 RGB 与 YUV 的互相转换\n\n模拟信号格式的 YUV 转换基本公式，其中 $K_R+K_G+K_B = 1 $\n$$\n\\begin{align}\nY &= K_R \\cdot R + K_G \\cdot G + K_B \\cdot B \\\\\nC_B &= {1 \\over 2} \\cdot {B - Y \\over 1 - K_B}\\\\\nC_R &= {1 \\over 2} \\cdot {R - Y \\over 1 - K_R}\n\\end{align}\n$$\n$RGB \\in [0,1]、Y \\in [0,1]、U \\in [-0.436,0.436]、V \\in [-0.615,0.615]$，[more](https://en.wikipedia.org/wiki/YUV)\n\n- 以下转换为 RGB 到 *SDTV / BT.601* 的转换\n    ```c\n    vec3 RGBToYUVBT601(vec3 rgb) {\n        // mat3(列向量1，列向量2，列向量3)\n        return rgb * mat3(\n            0.299,    0.587,    0.114,\n           -0.14713, -0.2886,   0.436,\n            0.615,   -0.51499, -0.10001\n                   );\n    }\n    \n    vec3 YUVBT601ToRGB(vec3 yuv) {\n        return yuv * mat3(\n            1.0,      1.0,     1.0,\n            0.0,     -0.39465, 2.03211,\n            1.13983, -0.5806,  0.0\n        );\n    }\n    ```\n\n- 以下转换为 RGB 到 *HDTV / BT.709* 的转换\n    ```c\n    vec3 RGBToYUVBT709(vec3 rgb) {\n        // mat3(列向量1，列向量2，列向量3)\n        return rgb * mat3(\n            0.2126,   0.7152,   0.0722,\n           -0.09991, -0.33609,  0.436,\n            0.615,   -0.55861, -0.05639\n        );\n    }\n    \n    vec3 YUVBT709ToRGB(vec3 yuv) {\n        return yuv * mat3(\n            1.0,  0.0,      1.28033,\n            1.0, -0.21482, -0.38059,\n            1.0,  2.12798,  0.0 \n        );\n    }\n    ```\n\n### 3.4 更多色彩模型的转换\n\n- [YY Color Convertor](https://github.com/ibireme/yy_color_convertor)\n\n\n\n\n\n# Reference\n\n1. [Lumiance 计算性能优化](http://www.songho.ca/dsp/luminance/luminance.html)\n2. [Luminance 和 Luma 的区别](https://cs.stackexchange.com/questions/92569/what-is-the-difference-between-luma-and-luminance)\n3. [Fast branchless RGB to HSV conversion in GLSL](http://lolengine.net/blog/2013/07/27/rgb-to-hsv-in-glsl)\n4. [YUV 维基百科](https://en.wikipedia.org/wiki/YUV)\n5. [YUV 的采样方式](https://en.wikipedia.org/wiki/Chroma_subsampling)\n6. [YUV 的打包方式](https://msdn.microsoft.com/en-us/library/aa904813%28VS.80%29.aspx)\n7. [YUV 420 数据格式详解](http://www.cnblogs.com/azraelly/archive/2013/01/01/2841269.html)\n8. [IJKPlayer](https://github.com/bilibili/ijkplayer)\n\n"
  },
  {
    "path": "DigitalImageProcessing/Part3_PhotoShop.md",
    "content": "<h1><center>PS 中的图像处理技术</center></h1>\n\n\n\n# 一、混合透明模式\n\n**声明**\n\n以下公式描述中：\n\n- 0代表纯黑色 0x000\n  1代表纯白色 0xFFF\n- a 表示当前图层 (activity layer)，取值范围 0 ~ 1\n  b 表示背景图层 (background layer)，取值范围 0 ~ 1\n- a，b 图层如果为同一张图像，那么结果就是对同一图像的明暗做出了修改\n\n\n\n\n## 1. 普通 Normal\n\n### 1.1 正常 Normal\n\n公式：$f(a,b) = a$\n\n\n\n### 1.2 溶解 Dissolve\n\n上层中随机抽取一些像素作为透明，使其可以看到下层，随着上层透明度越低，可看到的下层区域越多（不是真正的溶解）\n\n公式：$f(a,b) = random(a,b)$\n\n\n\n\n\n## 2. 加深 Darken\n\n### 2.1 变暗 Darken\n\n比较上下层像素后取相对较暗的像素作为输出（与变暗 Lighten 效果相反）\nRGB 每个色彩通道分别比较取最小\n\n公式：$f(a,b) = min(a,b)$\n\n\n\n### 2.2 深色 Darker Color\n\n比较上下层像素后取相对较暗的像素作为输出（与浅色 Lighter Color 效果相反）\n比较 RGB 三个色彩通道数值之和取最小\n\n公式：$f(a,b) = min(a_r + a_g + a_b, \\space b_r + b_g + b_b)$\n\n\n\n### 2.3 正片叠底 Multiply\n\n该效果将两层像素的标准色彩值相乘后输出\n其效果可以形容成：两个幻灯片叠加在一起然后放映，透射光需要分别通过这两个幻灯片，从而被削弱了两次\n\n公式：$f(a,b) = ab$\n\n![](./images/multiply.png)\n\n\n\n### 2.3 颜色加深 Color Burn\n\n如果上层越暗，则下层获取的光越少（与颜色加深 Color Dodge 相反）\n\n- 上层为全黑色，则下层越黑\n\n- 上层为全白色，则根本不会影响下层：结果最亮的地方不会高于下层的像素值\n\n公式：$f(a,b) = 1-{1-b \\over a}$\n\n![](./images/color_burn.png)\n\n\n\n### 2.4 线性加深 Linear Burn\n\n如果上下层的像素值之和小于 1，输出结果将会是纯黑色（与线性减淡 Linear Dodge 相反）\n如果将上层反相，结果将是纯粹的数学减\n\n公式：$f(a,b) = a+b-1$\n\n![](./images/linear_burn.png)\n\n\n\n\n\n## 3. 减淡 Lighten\n\n### 3.1 变亮 Lighten\n\n比较上下层像素后取相对较亮的像素作为输出（与变暗 Darken 相反）\nRGB 每个色彩通道分别比较取最大\n\n公式：$f(a,b) = max(a,b)$\n\n\n\n### 3.5 浅色 Lighter Color\n\n比较上下层像素后取相对较暗的像素作为输出（与深色 Darker Color 效果相反）\n比较 RGB 三个色彩通道数值之和取最大\n\n公式：$f(a,b) = max(a_r + a_g + a_b, \\space b_r + b_g + b_b)$\n\n\n\n### 3.2 滤色 Screen\n\n上下层像素的标准色彩值反相后相乘后输出，输出结果比两者的像素值都将要亮\n就好像两台投影机分别对其中一个图层进行投影后，然后投射到同一个屏幕上\n\n如果两个图层反相后，采用 Multiply 模式混合，则将和对这两个图层采用 Screen 模式混合后反相的结果完全一样\n\n公式：$f(a,b)=1-(1-a)(1-b)\\\\ 1-f(a,b)=(1-a)(1-b)$\n\n![](./images/screen.png)\n\n\n\n### 3.3 颜色减淡 Color Dodge\n\n该模式下，上层的亮度决定了下层的暴露程度（与颜色加深 Color Burn 相反）\n如果上层越亮，下层获取的光越多，也就是越亮\n\n公式：$f(a,b) = {b \\over 1-a}$\n\n![](./images/color_dodge.png)\n\n\n\n### 3.4 线性减淡 Linear Dodge\n\n将上下层的色彩值相加，结果将更亮（与线性加深 Linear Burn 相反）\n\n公式：$f(a,b) = a+b$\n\n\n\n\n\n\n## 4. 对比 Contrast\n\n### 4.1 叠加 Overlay\n\n依据下层色彩值的不同，该模式可能是 Multiply，也可能是 Screen 模式\n上层决定了下层中间色调偏移的强度\n\n公式：$f(a,b)=\\begin{cases}2ab, & if \\space a < 0.5\\\\ 1-2(1-a)(1-b), & otherwise \\end{cases}$\n\n\n\n### 4.2 柔光 Soft Light\n\n叠加模式下，过上层的颜色高于 50% 灰，则下层越亮，反之越暗\n以 Gamma 值范围为 2.0 到 0.5 的方式来调制下层的色彩值，结果将是一个非常柔和的组合\n\n公式：$f(a,b) =\\begin{cases}2ab+a^2(1-2b), & if \\space a < 0.5\\\\ 2a(1-b)+ \\sqrt{a}(2b-1), & otherwise \\end{cases}$\n\n![](./images/soft_light.png)\n\n\n\n### 4.3 强光 Hard Light\n\n叠加模式下，过上层的颜色高于 50% 灰，则下层越亮，反之越暗\n\n公式：$f(a,b) =\\begin{cases}2ab, & if \\space a < 0.5\\\\ 1-2(1-a)(1-b), & otherwise \\end{cases}$\n\n![](./images/hard_light.png)\n\n\n\n### 4.4 亮光 Vivid Light\n\n**非常强烈**的增加了对比度，特别是在高亮和阴暗处\n阴暗处应用颜色加深 Color Burn\n高亮处应用颜色减淡 Color Dodge\n\n公式：$f(a,b) =\\begin{cases}1-{1-b \\over 2a}, & if \\space a \\leq 0.5\\\\ {b \\over 2(1-a)}, & otherwise \\end{cases}$\n\n\n\n### 4.5 线性光 Linear Light\n\n增加了对比度，特别是在高亮和阴暗处（**比亮光 Vivid Light 对比度增加的少**）\n类似于线性加深 Linear Burn，只不过是加深了上层的影响力\n\n公式：$f(a,b) = b+2a-1$\n\n![](./images/linear_light.png)\n\n\n\n### 4.6 点光 Pin Light\n\n中间调几乎是不变的下层，但是两边是变暗 Darken 和变亮 Light 模式的组合\n\n公式：$f(a,b) = \\begin{cases}2a-1, & if \\space 2a-1>b\\\\ b, & if \\space 2a-1<b<2a\\\\ 2a, & if \\space b>2a\\end{cases}$\n\n\n\n### 4.7 实色混合 Hard mix\n\n**最终结果仅包含 6 种基本颜色**，每个通道要么就是 0，要么就是 1\n\n公式：$f(a,b) =\\begin{cases}0, & if \\space a<1-b\\\\ 1, & if \\space a>1-b \\end{cases}$\n\n\n\n## 5. 差集 Inversion/Cancelation\n\n### 5.1 差值 Difference\n\n用于比较两个不同版本的图片：如果两者完全一样，则结果为全黑\n\n公式：$f(a,b) = abs(b-a)$\n\n\n\n### 5.2 排除 Exclusion\n\n亮的图片区域将导致另一层的反相，很暗的区域则将导致另一层完全没有改变\n\n公式：$f(a,b) = a+b-2ab$\n\n![](./images/exclusion.png)\n\n\n\n### 5.3 减去 Subtract\n\n公式：$f(a,b) = b-a$\n\n\n\n### 5.4 划分 Divide\n\n公式：$f(a,b) = b/a$\n\n\n\n## 6. 色彩模型 Component\n\n### 6.1 色相 Hue\n\n在 HSB 色彩模型中，使用当前图层的色相，使用背景图层的饱和度和明度\n\n公式：$H_cS_cB_c = \\color{red}{H_a}S_bB_b$\n\n\n\n### 6.2 颜色 Color\n\n在 HSB 色彩模型中，使用当前图层的色相和饱和度，使用背景图层的明度\n\n公式：$H_cS_cB_c = \\color{red}{H_aS_a}B_b$\n\n\n\n### 6.3 饱和度 Saturation\n\n在 HSB 色彩模型中，使用当前图层的饱和度，使用背景图层的色相和明度\n\n公式：$H_cS_cB_c = H_b\\color{red}{S_a}B_b$\n\n\n\n### 6.4 明度 Luminosity\n\n在 HSB 色彩模型中，使用当前图层的明度，使用背景图层的色相和饱和度（与颜色相反）\n\n公式：$H_cS_cB_c = H_bS_b\\color{red}{B_a}$\n\n\n\n\n\n# Reference\n\n- [Photoshop Blend Modes Explained](https://photoblogstop.com/photoshop/photoshop-blend-modes-explained)\n- [Wiki: Blend modes](https://en.wikipedia.org/wiki/Blend_modes)\n- [Adobe blending-modes](https://helpx.adobe.com/cn/photoshop/using/blending-modes.html)\n- [stackoverflow: how-does-photoshop-blend-two-images-together](https://link.jianshu.com/?t=http://stackoverflow.com/questions/5919663/how-does-photoshop-blend-two-images-together)\n"
  },
  {
    "path": "DigitalImageProcessing/README.md",
    "content": "# [Digital Image Processing](https://www.amazon.cn/%E5%9B%BE%E4%B9%A6/dp/B00HNC8OYC) Notes\n\n- This notes written in [Typora](https://www.typora.io/). It is also recommended to use it to read.\n- See [DrawEasy3D](https://github.com/CatOnly/DrawEasy3D) for the specific code implementation of Digital image processing.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n⚓️ Keep Reading , Keep Writing , Keep Coding.\n"
  },
  {
    "path": "LinearAlgebra/Part0_Base.md",
    "content": "[TOC]\n\n# 一、向量\n\n## 1. 概念\n\n- 起点是原点\n- 向量 = 方向 + 大小\n- **位置移动** 的数值记录\n- **线性变换** 的数值记录\n\n\n\n##2. 基本运算 \n\n- 向量的加减\n  ![](images/vectorOperator.png)\n  ​\n- 向量的缩放\n  ![](images/vectorScale.png)\n  ​\n\n\n## 3. 向量的基（单位向量）\n\n- 向量的长度（向量的模）：$|v| = \\sqrt{x^2 + y^2 +z^2}$\n- 向量的基（单位向量）：$\\hat{v} = {v \\over |v|} = ({x \\over |v|},{y \\over |v|},{z \\over |v|})$\n- 向量的值 **依赖** 于基\n  ![](images/vectorBase.png)\n- 向量空间的一个基是 **张成** 该空间的 **线性无关** 向量集\n\n\n\n## 4. 零向量\n\n概念：\n\n- 表示没有位移\n- 零向量没有方向\n- 每个维度都为 0 的向量\n  例：在三维空间的零向量为 (0, 0, 0)\n\n\n\n\n# 二、张成空间（向量的表示范围）\n\n## 1. 概念\n\n- 二维的张成空间\n  ![](images/span1.png)\n\n- 三维的张成空间\n  ![](images/span2.png)\n\n\n\n## 2. 线性相关\n\n- 线性相关：有多组向量构成张成空间，移除其中一个 **不减少** 张成空间的大小\n  线性无关：所有向量 给张成空间增添了新的维度\n- 线性相关：多组向量中，有向量 = 其他向量的线性组合\n  ![](images/linear.png)\n- 齐次向量：表示同一条直线的一类向量\n\n\n\n\n# 三、线性变换（矩阵）\n\n## 1. 概念\n\n- 实质：操作空间的一种手段\n\n- 原点固定\n\n- 直线在变换后保持直线\n\n- 网格保持 **平行** 且 **等距分布**\n  ![](images/linearTransform.png)\n\n\n\n## 2. 矩阵 * 向量（点的状态变化）\n\n- 实质：求该向量**基坐标变化后（矩阵）的值**\n\n- 线性变换后的基坐标 == 矩阵\n  ![](images/LinearTransform1.png)\n\n- 矩阵 * 向量 == 该向量对应 **基坐标的线性变换** 后的值\n  ![](images/LinearTransform2.png)\n\n- 向量 * 矩阵\n$$\n\\begin{bmatrix}\nx & y\n\\end{bmatrix}\n\\begin{bmatrix}\n\\color{green}{a} & \\color{red}{b}\\\\\n\\color{green}{c} & \\color{red}{d}\n\\end{bmatrix}\n=\n\\begin{bmatrix}\nx \\color{green}{a}+y \\color{red}{c} \n& \nx \\color{green}{b}+ y\\color{red}{d}\n\\end{bmatrix}\n$$\n\n\n\n## 3. 矩阵相乘（基坐标的状态变化）\n\n- 实质：先后切换**基坐标**的状态\n  例子：矩阵B * 矩阵A * 向量C == 将向量C **先按照 矩阵A 变换**，再按照矩阵B 变换\n  ![](images/matrix.png)\n\n- 矩阵相乘**不遵循**交换律\n\n  例子：矩阵B * **矩阵A** != **矩阵A** * 矩阵B\n\n- 矩阵相乘结合律：(AB) C == A (BC)\n  由于**都是从 最右边的 C 开始变换**，从几何角度看没有区别\n\n- 矩阵相乘公式\n  ![](images/matrix1.png)\n\n\n\n# 四、行列式（面积/体积）\n\n## 1. 概念\n\n- 二维表示：二维行列式中两个二维向量构成的 **面积**\n  **符号取决于** 区域是否发生了翻转变换（翻转为 负）\n  行列式的值与所选坐标系无关\n  ![](images/determinant.png)\n\n- 三维 表示：三维行列式中三个三维向量构成的 **体积**\n  **符号取决于** 构成矩阵的这三个向量**是否满足右手定则**（不满足为 负）\n  ![](images/determinant3.png)\n\n\n\n## 2. 特殊情况\n\n- 当线性变化后 区域 **面积** 为 0 时（点，线）\n  说明发生了由 **高维到低维** 的线性变换\n  ![](images/determinant1.png)\n\n- 当线性变化后 区域 **体积** 为 0 时（点，线，面）\n  ![](images/determinant4.png)\n\n- 当线性变化后 比例为 **负数**  时，平面翻转\n  ![](images/determinant2.png)\n\n\n\n## 3. 求行列式公式\n\n- 二阶行列式计算\n![](images/determinant5.png)\n\n- 三阶行列式计算\n  $$\n  \\begin{bmatrix}\n  a & b & c \\\\\n  d & e & g \\\\\n  h & i & j\n  \\end{bmatrix}\n  = a * e * j + d * i * c + b * g * h - c * e * h - b * d * j - a * g * i\n  $$\n\n\n\n# 五、矩阵和线性方程组（计算工具）\n\n## 1. 非方阵\n\n- 3 X 2 矩阵的几何意义：将 **二维** 空间映射到 **三维**空间上\n\n\n\n## 2. 线性方程组 转 矩阵\n\n- 求线性方程组的解，就是求线性变化的向量\n ![](images/equation.png)\n\n\n\n## 3. 列空间\n\n- **零向量** 一定在列空间中（因为线性变换必须保持原点不变）\n ![](images/columSpace.png)\n\n\n\n## 4. 秩\n\n- 实质：线性变换后空间的 **维数**\n- 定义：列空间的维数\n- 满秩：秩 == 列数\n\n\n\n## 5. 零空间\n\n- 当一个矩阵不是满秩的时候，会出现零空间\n- 实质：线性变换后落在原点的集合\n- 当线性方程组线性变换前 **V 向量为 0 时**，零空间就是该线性方程组 **解的集合**\n\n\n\n## 6. 单位矩阵\n\n- 实质：一个什么都不做的变换\n- 特点：$M_{单位} = M_{正交} = M^T = M^{-1}$\n\n\n\n## 7. 伴随矩阵\n\n- 实质：用来计算逆矩阵\n\n- 定义：各元素代数余子式构成的矩阵\n  $a_{ij}$ 的余子式 $M_{ij}$：对于矩阵 $A = (a_{ij})_{n \\times n}$ 将矩阵 A 中的元素 $a_{ij}$ 所在的 第 i 行 和 第 j 列都去掉后，剩余的 $(n-1)^2$ 个元素按照原来的排列顺序组成的 n-1 阶矩阵的**行列式值**\n  $a_{ij}$ 的代数余子式：$A_{ij} = (-1)^{i+j}M_{ij}$\n  $$\n  A^* = \n  \\begin{bmatrix}\n  A_{11} & A_{21} & \\cdots & A_{n1} \\\\\n  A_{12} & A_{22} & \\cdots & A_{n2} \\\\\n  \\vdots & \\vdots & \\ddots & \\vdots \\\\\n  A_{1n} & A_{2n} & \\cdots & A_{nn} \\\\\n  \\end{bmatrix}\n  $$\n\n- 二维矩阵的伴随矩阵快速求法：主对角线元素互换，副对角线元素加负号\n  $$\n  \\begin{bmatrix}\n  a & b \\\\\n  c & d\n  \\end{bmatrix}\n  ^*\n  =\n  \\begin{bmatrix}\n  d & -b \\\\\n  -c & a\n  \\end{bmatrix}\n  $$\n\n\n\n## 8. 逆矩阵（基坐标的逆向变换）\n\n- 前提：矩阵的行列式的值不为 0，这个矩阵存在逆矩阵\n\n- 实质：恒等变换，无论先后顺序，向量的基都不变\n  向量进行矩阵 A 的线性变换后，在进行矩阵 A 的逆矩阵的线性变换，向量不变，向量的基不变\n  $$\n  A^{-1}A = \n  \\begin{bmatrix}\n  1 & 0 \\\\\n  0 & 1\n  \\end{bmatrix}\n  $$\n\n- 应用：\n\n   1. [基坐标的逆向转换](#2. 不同坐标系的互相转换)\n   2. 利用矩阵的逆阵求解方程组\n\n- 求解：\n\n   1. 如果为正交矩阵，则 $M^{-1} = M^T$\n   2. **对于维数较低的矩阵才可用** $M^*$ 为伴随矩阵，|M| 为矩阵 M 的[行列式值](#四、行列式（面积/体积）)\n      $M^{-1} = {M^*  \\over |M| }$\n\n\n\n## 9. 转置矩阵\n\n- 定义：若 $V_1 \\times V_2 = M$ ，则 M 的转置矩阵 $M^T = V_2 \\times V_1$\n  例：\n  $$\n  \\begin{align}\n  \\begin{bmatrix}\n  1 & 2 & 3\\\\\n  4 & 5 & 6\\\\\n  7 & 8 & 9\\\\\n  10 & 11 & 12\n  \\end{bmatrix}^T &=\n  \\begin{bmatrix}\n  1 & 4 & 7 & 10\\\\\n  2 & 5 & 8 & 11\\\\\n  3 & 6 & 9 & 12\\\\\n  \\end{bmatrix} \\\\\n  \n  \\begin{bmatrix}\n  1 & 2 & 3\\\\\n  4 & 5 & 6\\\\\n  7 & 8 & 9\n  \\end{bmatrix}^T &= \n  \\begin{bmatrix}\n  1 & 4 & 7\\\\\n  2 & 5 & 8\\\\\n  3 & 6 & 9\n  \\end{bmatrix}\n  \\end{align}\n  $$\n\n\n\n## 10. 正交矩阵（转置 == 逆）\n\n- 定义：若 M 是正交矩阵，则 $MM^T = I_{单位矩阵} \\Rightarrow M^T = M^{-1}$\n\n- 几何意义：\n  矩阵每 行/列 向量**互相垂直**，矩阵每 行/列 向量都是**单位向量**，则为正交矩阵\n\n  > 正交基：互相垂直的基坐标\n  > 标准正交基：互相垂直 且 长度都为 1 的基坐标（构成正交矩阵）\n\n- 应用：\n\n  1. 三维变换中，经常用逆矩阵来求解反向变换的问题。逆矩阵的求解往往计算量很大，但转置矩阵却很容易求解\n  2. 为了避免计算转置矩阵而交换矩阵相乘的顺序\n     $M^Tv = vM$\n\n\n\n\n\n# 六、向量的点积（两向量的相似度）\n\n## 1. 概念\n\n- 数学计算，遵循乘法交换律\n  如果 a，b 都是列向量，则 $a \\cdot b = a^T * b$\n  $$\n  \\begin{bmatrix} \n  \\color{green}{1} \\\\  \\color{red}{2} \\\\ \n  \\end{bmatrix} \\cdot\n  \\begin{bmatrix} \n  \\color{green}{3} \\\\  \\color{red}{4} \\\\ \n  \\end{bmatrix}\n  =\n  \\begin{bmatrix} \n  \\color{green}{1} &  \\color{red}{2} \\\\ \n  \\end{bmatrix} *\n  \\begin{bmatrix} \n  \\color{green}{3} \\\\  \\color{red}{4} \\\\ \n  \\end{bmatrix}\n  = \\color{green}{1} \\cdot \\color{green}{3} + \\color{red}{2} \\cdot \\color{red}{4}\n  $$\n\n- **应用：求向量的夹角**\n  $|\\vec v| 是 \\vec v 的长度，如果 \\vec v是单位向量，|\\vec v| = 1$\n  $$\n  \\vec v \\cdot \\vec n = |\\vec v| \\cdot |\\vec n| \\cdot cos\\theta \\\\\n  \\theta = arccos({\\vec v \\cdot \\vec n \\over |\\vec v| \\space |\\vec n|})\n  $$\n\n- 几何解释\n  点积，表示两个向量的相似程度，点积的值越大，两个向量越相似\n\n  | v 和 n 点积 | v 和 n 的夹角 | v 和 n 的方向 |\n  | :---------: | :-----------: | :-----------: |\n  |    \\> 0     |   [ 0，90 )   |   基本相同    |\n  |     = 0     |      90       |   互相垂直    |\n  |     < 0     |  ( 90，180 ]  |   基本相反    |\n\n\n\n## 2. 点积和投影\n\n给定两个向量 v 和 n，能将 v 分解成两个分量： $v_{||}$ 平行于 n 和 $v_{\\bot}$ 垂直于 n \n\n![](images/vectorProject.png)\n\n证明，公式一：$n \\cdot v = |v_{在n上的投影}|n$ \n$$\n\\begin{align}\n已知： 单位向量 = {n \\over |n|} &= {v_{||} \\over |v_{||}|}, \\space \ncos \\theta = {|v_{||}| \\over |v|} \\\\\nv \\space 到 \\space n \\space 的投影：v_{||} \n&= {n \\over |n|}|v_{||}| \\\\\n&= {n \\over |n|}|v|cos \\theta \\\\\n&= n{|n||v|cos \\theta \\over |n|^2} \\\\\n&= n{n \\cdot v \\over |n|^2}\\\\\n&= {n \\over |n|}{n \\cdot v \\over |n|}\\\\\nv_{||} &= {n \\cdot v \\over |n|}\\\\\n由于 \\space n \\space 是单位向量，且和 \\space v_{||} \\space 方向相同，则 \\\\\nv_{||} &= n \\cdot v\\\\\n|v_{||}|v_{单位向量} &= n \\cdot v \\\\\nn \\cdot v &= |v_{||}|n\n\\end{align}\n$$\n\n\n![](images/dot1.png)\n\n\n\n证明，公式二：$n \\cdot v = |n||v|cos\\theta$\n\n![](./images/vectorProject2.png)\n$$\n\\begin{align}\n已知，单位向量：\\hat a \\cdot \\hat b \n&= \\hat b \\space 在 \\space \\hat a \\space 的投影\\\\\n&= {\\hat b \\space 在 \\space \\hat a \\space 的投影 \\over |\\hat b|} \\\\\n&= cos \\theta \\\\\\\\\n则：a \\cdot b \n&= (|a|\\hat a) \\cdot (|b|\\hat b) \\\\\n&= |a||b|(\\hat a \\cdot \\hat b) \\\\\n&= |a||b|cos\\theta \\\\\n\na \\cdot a &= |a|^2 \n\\end{align}\n$$\n\n\n\n## 3. 点积可以是一种线性变换\n\n1. 二维向量 线性变换到 一维向量\n    例：设，二维向量（4，3）【基向量为（1，1）】\n    经过基向量变化（1，-2）到一维向量上为 -2【基向量为（1，-2）】\n    则，点积的过程可以看作 维度由高到底的 线形变换\n$$\n  \\begin{align}\n  M_{1\\times 2 矩阵} \\cdot v_{前} &= N_{点积结果} \\\\\n  V_{点积用向量} \\cdot v_{前} &= N_{点积结果} \\\\\n  M_{1\\times 2 矩阵} &= V_{点积用向量}\n  \\end{align}\n$$\n  ![](images/dot2.png)\n\n2. 进一步说明\n    有时，为求由高维到低维的 1 X 2 变换矩阵 M $\\begin{bmatrix}u_x & u_y\\end{bmatrix}$\n    一维上的一个向量 u，到基坐标 j 的正交投影值 $u_y$ ，利用对称性可知：\n   $u_y$ 等价于 基坐标 j 到 向量 u 上的正交投影值，即\n   $u_y$ 等价于 就是变换矩阵M 的 y 值\n    ![](images/dot4.png)\n\n3. 结论\n\n  1 X 2 矩阵M 这样的由二维到一维的线性变换过程 可以用 向量 m 表示，并且 **矩阵M 和 向量m** 一一对应\n  ![](images/dot5.png)\n\n\n\n\n# 七、向量的叉积（基向量，向量组合的行列式）\n\n## 1. 概念\n\n- **几何解释**\n  二维空间：通过 2 个向量叉乘得到这 2 个向量构成的**面积**（面积也有负，取决于叉乘的顺序）\n  三维空间：通过 2 个向量叉乘得到 1 个**垂直于这两个向量平面的向量**\n  ![](images/cross2.png)\n\n- 叉乘的计算\n  仅限于二维空间：$\\vec v \\times \\vec w = |\\vec v||\\vec w| sin \\theta$，其中 $\\theta$ 是 v 和 w 的夹角\n  适用于二维和三维空间：两个向量组合成的矩阵的**行列式值 det**（矩阵的转置矩阵不改变行列式的值）\n\n  ![](images/cross1.png)\n\n  扩展：特殊的行列式求法，其中 $({\\hat x},{\\hat y},{\\hat z})$ 为基向量 （右手坐标系的叉乘公式）\n  $$\n  \\begin{bmatrix} \n  \\color{green}{v_1} \\\\  \\color{red}{v_2} \\\\ \\color{blue}{v_3}\\\\ \n  \\end{bmatrix} \\times\n  \\begin{bmatrix} \n  \\color{green}{w_1}  \\\\ \\color{red}{w_2} \\\\ \\color{blue}{w_3} \\\\ \n  \\end{bmatrix} = \n  \\begin{bmatrix} \n  \\color{red}{v_2} \\cdot  \\color{blue}{w_3} - \\color{blue}{v_3}\\cdot \\color{red}{w_2} \\\\\n  \\color{blue}{v_3} \\cdot  \\color{green}{w_1} - \\color{green}{v_1}\\cdot \\color{blue}{w_3} \\\\\n  \\color{green}{v_1} \\cdot  \\color{red}{w_2} - \\color{red}{v_2}\\cdot \\color{green}{w_1} \\\\\n  \\end{bmatrix}\n  \\\\ \\Downarrow \\\\\n  \\begin{bmatrix} \n  \\color{#F80}{v_1} \\\\  \\color{#F80}{v_2} \\\\ \\color{#F80}{v_3}\\\\ \n  \\end{bmatrix} \\times\n  \\begin{bmatrix} \n  \\color{#F0F}{w_1}  \\\\ \\color{#F0F}{w_2} \\\\ \\color{#F0F}{w_3} \\\\ \n  \\end{bmatrix} = \n  det\n  \\begin{pmatrix} \n  \\begin{bmatrix} \n  \\color{green}{\\hat x}  & \\color{#F80}{v_1} & \\color{#F0F}{w_1} \\\\\n  \\color{red}{\\hat y}  & \\color{#F80}{v_2} & \\color{#F0F}{w_2} \\\\ \n  \\color{blue}{\\hat z}  & \\color{#F80}{v_3} & \\color{#F0F}{w_3} \\\\ \n  \\end{bmatrix} \n  \\end{pmatrix}\n  \\\\ \\Downarrow \\\\\n  \\begin{align}\n  \\color{#F80}{\\overrightarrow V} \\times \\color{#F0F}{\\overrightarrow W}\n  &= \n  \\color{green}{\\hat x}(\\color{#F80}{v_2} \\cdot  \\color{#F0F}{w_3} - \\color{#F80}{v_3}\\cdot \\color{#F0F}{w_2}) + \n  \\color{red}{\\hat y}(\\color{#F80}{v_3} \\cdot  \\color{#F0F}{w_1} - \\color{#F80}{v_1}\\cdot \\color{#F0F}{w_3}) + \n  \\color{blue}{\\hat z}(\\color{#F80}{v_1} \\cdot  \\color{#F0F}{w_2} - \\color{#F80}{v_2}\\cdot \\color{#F0F}{w_1})\\\\\n  &=\n  \\color{green}{\\hat x}(\\color{#F80}{v_2} \\cdot  \\color{#F0F}{w_3} - \\color{#F80}{v_3}\\cdot \\color{#F0F}{w_2}) - \n  \\color{red}{\\hat y}(\\color{#F80}{v_1}\\cdot \\color{#F0F}{w_3} - \\color{#F80}{v_3} \\cdot  \\color{#F0F}{w_1}) + \n  \\color{blue}{\\hat z}(\\color{#F80}{v_1} \\cdot  \\color{#F0F}{w_2} - \\color{#F80}{v_2}\\cdot \\color{#F0F}{w_1})\\\\\n  &= \\overrightarrow S_{垂直于V 和 W 构成的平面}\n  \\end{align}\n  $$\n\n\n\n\n- 叉乘向量的方向（具体见 Part1 十、1.1.4）\n  **二维空间**\n  逆时针为正，由 $\\vec v \\times \\vec w = |\\vec v||\\vec w| sin \\theta$，可知 $ \\vec v \\times \\vec w = 0 $ 表示 v 平行于 w\n  ![](images/cross.png)\n  **三维空间**\n  在右手坐标系下，使用 [**右手定则**](https://en.wikipedia.org/wiki/Right-hand_rule) 判断叉乘的方向\n  在左手坐标系下，使用 **左手定则** 判断叉乘的方向\n- 叉乘向量的长度\n  几何意义\n  **二维空间**：叉乘的两个向量构成的平行四边行的面积\n  **三维空间**：叉乘后得到的新向量的长度\n  叉乘后得到新的向量长度大小计算：$|a \\times b| = ||a|\\space|b| sin\\theta|$\n\n![](images/vectorCross.png)\n\n$$\na \\times  b = - (b \\times a)\n$$\n\n\n\n\n## 2. 三维空间中叉乘的线性变换解释\n\n1. 构造 三维 到 一维的**线性变换**\n   函数：由第一列的向量的三个坐标做变量，v 和 w 向量值固定，得到一个行列式的值（三个向量构成的六面体的体积）\n\n   ![](images/cross5.png)\n\n2. 由于函数线性，就会存在一个 1 X 3 矩阵来代表这个变换\n\n\n$$\n\\begin{bmatrix} \\color{red}{p_1} &  \\color{red}{p_2} & \\color{red}{p_3}\\\\ \\end{bmatrix} \\cdot \n   \\begin{bmatrix} x \\\\ y \\\\ z\\\\ \\end{bmatrix} =  \n   det\n   \\begin{pmatrix} \n   \\begin{bmatrix} \n   x & \\color{green}{v_1} & \\color{#F80}{w_1} \\\\\n   y & \\color{green}{v_2} & \\color{#F80}{w_2} \\\\ \n   z & \\color{green}{v_3} & \\color{#F80}{w_3} \\\\ \n   \\end{bmatrix} \n   \\end{pmatrix}\n$$\n\n3. 从多维到一维的线性变换矩阵可以用向量代替（见 六.2）\n$$\n\\begin{bmatrix} \\color{red}{p_1}\\\\ \\color{red}{p_2}\\\\ \\color{red}{p_3}\\\\ \\end{bmatrix} \\cdot \n\\begin{bmatrix} x \\\\ y \\\\ z\\\\ \\end{bmatrix} =  \ndet\n\\begin{pmatrix} \n\\begin{bmatrix} \nx & \\color{green}{v_1} & \\color{#F80}{w_1} \\\\\ny & \\color{green}{v_2} & \\color{#F80}{w_2} \\\\ \nz & \\color{green}{v_3} & \\color{#F80}{w_3} \\\\ \n\\end{bmatrix} \n\\end{pmatrix} \n\\\\ \\Downarrow \\\\\n\\color{red}{p_1} \\cdot x +  \\color{red}{p_2} \\cdot y +  \\color{red}{p_3} \\cdot z = x(\\color{green}{v_2} \\cdot  \\color{#F80}{w_3} - \\color{green}{v_3}\\cdot \\color{#F80}{w_2}) + \n y(\\color{green}{v_3} \\cdot  \\color{#F80}{w_1} - \\color{green}{v_1}\\cdot \\color{#F80}{w_3}) + \n z(\\color{green}{v_1} \\cdot  \\color{#F80}{w_2} - \\color{green}{v_2}\\cdot \\color{#F80}{w_1})\n\\\\ \\Downarrow \\\\\n\\color{red}{p_1} = \\color{green}{v_2} \\cdot  \\color{#F80}{w_3} - \\color{green}{v_3}\\cdot \\color{#F80}{w_2} \\\\\n\\color{red}{p_2} = \\color{green}{v_3} \\cdot  \\color{#F80}{w_1} - \\color{green}{v_1}\\cdot \\color{#F80}{w_3} \\\\\n\\color{red}{p_3} = \\color{green}{v_1} \\cdot  \\color{#F80}{w_2} - \\color{green}{v_2}\\cdot \\color{#F80}{w_1}\n\\\\ \\Downarrow \\\\\n\\overrightarrow P = \\overrightarrow V \\times \\overrightarrow W\n$$\n\n4. 假设 空间内有一向量 P 垂直于 v 和 w 构成的平行四边形 F，且 P 的长度为 F 的面积\n$$\n  \\begin{align} \\\\\n    P \\cdot x  &= x 在 P 的投影 \\times P 的长度（见 六.1）\\\\\n    &= X 在 P 的投影 \\times F_{v 和 m 构成的平行四边形面积} \\\\\n    &= x, w, v 构成的六面体的体积\n    \\end{align}\n$$\n  ![](images/cross4.png)\n\n5. 由 3 得 P 为 向量 V 和 W 的叉乘结果\n   由 4 得 存在 P‘ 垂直于 向量 V 和 W 构成的平面\n   由 2 得 P 和 P’ 一一对应（见 六、2.3）\n\n\n\n# 八、基变换（坐标系转换）\n\n## 1. 概念\n\n- 基向量：**基向量的比值**可以表示坐标系中的任何向量\n- 变换前提：同一原点\n- 几何意义：实现 **同一个向量** 在不同 **基向量（坐标系）**下的转换\n\n  > 例，一个模型上的点都是基于模型上原点的偏移量 A（此时模型原点值是 M（ 0，0，0））\n  > 当把模型放到世界中，移动模型时，模型上的原点是基于世界原点的偏移量（此时模型原点值是 N（ x，y，z），基坐标已经变化）\n  > 此时求得：模型的点在世界坐标的值 = 偏移量 A + 模型原点在世界坐标的值 N\n\n\n\n## 2. 不同坐标系的互相转换\n\n本质：变换后的向量原来由基向量构成的线性组合不变，但使用新的基向量\n\n目标 1：A 坐标系的向量 v 转 B 坐标系的向量 w\n\n- 公式 1：矩阵左乘法（三、2）\n  向量 v：A 坐标系中的向量\n  向量 w：向量 v 在**B 坐标系中**对应的向量\n  矩阵 M：A 坐标系的基向量在 B 坐标系的表示\n  $$\n  \\begin{align}\n  \\overrightarrow M_{ab} \\cdot \\overrightarrow v_a &= \\overrightarrow w_b \\\\\n  \\overrightarrow v_a &= {\\overrightarrow M_{ab}}^{-1} \\cdot \\overrightarrow w_b\n  \\end{align}\n  $$\n\n目标 2： B 坐标系的线性变换（矩阵 P）转 A 坐标系的线性变换（矩阵 Q）\n\n- 公式 2：矩阵与矩阵相乘（三、3）\n  向量 v： A 坐标系中的向量\n  向量 w：向量 v 的在 B 坐标系线性变换后的在 A 坐标系的结果\n  矩阵 M：A 坐标系的基向量在 B 坐标系的表示\n  矩阵 P：向量 v 在 B 坐标系的线性变换\n  $$\n  \\begin{align}\n  \\overrightarrow M^{-1}_{ba} \\cdot \n  \\color{red}{\\overrightarrow P_b} \\cdot \n  \\color{red}{\\overrightarrow M_{ab}} \\cdot \n  \\overrightarrow v_a &= \\overrightarrow w_a \\\\\n  \\overrightarrow Q_a \\cdot \n  \\overrightarrow v_a &= \\overrightarrow w_a \\\\\n  \\end{align}\n  $$\n\n\n\n\n\n\n# 九、特征向量与特征值\n\n## 1. 概念\n\n- 几何意义：\n  特征向量：向量在 **线性变换前后** 向量的 **张成空间（二、1）不变**（与所选坐标系无关）\n  特征值：$特征值 = {特征向量线性变换后的长度 \\over 特征向量线性变换前的长度}$，向量翻转时特征值为负\n- 特殊情况：\n  部分旋转的线性变换 **没有特征值和特征向量**\n  属于单个特征值的特征向量可能不止在一条线上\n  ![](images/eigenvalue.png)\n- 公式：\n  矩阵 A：变换矩阵\n  矩阵 I：单位矩阵\n  向量 v：特征向量\n  lambda：特征值\n$$\n\\begin{align}\nA \\overrightarrow v &= \\lambda \\overrightarrow v \\\\\nA \\overrightarrow v &= \\lambda  \n\\begin{bmatrix} \n1 & 0 & 0\\\\\n0 & 1 & 0\\\\\n0 & 0 & 1\\\\\n\\end{bmatrix}\n\\cdot\n\\overrightarrow v \\\\\n(A - \n\\begin{bmatrix} \n\\lambda & 0 & 0\\\\\n0 & \\lambda & 0\\\\\n0 & 0 & \\lambda\\\\\n\\end{bmatrix}\n) \\cdot \\overrightarrow v &= 0 \\\\\n\\\\\n(A - \\lambda I) \\cdot \\overrightarrow v &= 0 \\\\\n\\end{align}\n$$\n\n - 求解：\n   特征向量 v 为 0，上述公式恒成立\n   特征向量 v 非 0，可知 **矩阵 [ A - lambda * I ] **的 **行列式** 值为 0（四、2），可以求出 特征值，然后代入公式求特征向量\n\n\n\n## 2. 应用\n\n- 特征向量作为旋转轴：**（特征值必须 == 1）**\n  由于特征向量在线性变换后不影响其方向，可以**作为旋转轴**\n- 对角矩阵：\n  除了对角元素以外其他元素均为 0 的矩阵\n  所有基向量 都是 特征向量，对角元素之积是 特征值\n  多次相乘对角矩阵容易计算\n- 特征基：\n  几个特征向量的张成空间是基向量的张成空间，这几个特征向量可以作为基向量使用\n- 降低矩阵计算量：\n  对于部分矩阵，如果特征基存在，通过基变换（八、2）选取特征基作为新的基向量，可以得出对角矩阵，在利用对角矩阵易计算的特性，降低计算量\n  例：\n  X 轴上的基向量由 (0, 1) 到 (0, 1)\n  Y 轴上的基向量由 (1, 0) 到 (-1, 1)\n  $$\n    { \\begin{bmatrix} \n    \\color{green}{1} & \\color{#FA0}{-1}\\\\ \n    \\color{green}{0} & \\color{#FA0}{1}\\\\\n    \\end{bmatrix} \n    }^{-1}\\cdot \n    \\begin{bmatrix} \n    \\color{green}{3} & \\color{red}{1}\\\\ \n    \\color{green}{0} & \\color{red}{2}\\\\\n    \\end{bmatrix} \n    \\cdot\n    \\begin{bmatrix} \n    \\color{green}{1} & \\color{#FA0}{-1}\\\\ \n    \\color{green}{0} & \\color{#FA0}{1}\\\\\n    \\end{bmatrix} \n    = \n    \\begin{bmatrix} \n    \\color{red}{3} & 0\\\\ \n    0 & \\color{red}{2}\\\\\n    \\end{bmatrix}\n  $$\n\n"
  },
  {
    "path": "LinearAlgebra/Part1_Matrix.md",
    "content": "[TOC]\n\n# 一、矩阵变换\n\n## 1. 基础知识\n\n### 1.1 GPU 中矩阵间的计算方式\n\n> 注意：\n>\n> 1. 在矩阵乘法中，顺序很重要，变换的几何意义由 右 -> 左 变换\n> 2. 建议在组合矩阵时，先缩放，后旋转，最后位移\n>    否则它们会（消极地）互相影响，比如：比如向某方向移动2米，2米也许会被缩放成1米\n\n- 阵列操作：图像的矩阵中每个对应元素之间的操作（矩阵间的 加、减，矩阵和数字的加、减、乘）\n  例，**矩阵**和数字相减\n  $$\n  \\begin{bmatrix}\n  \\color{red}{a_{11}} & \\color{red}{a_{21}} \\\\\n  a_{12} & a_{22} \\\\\n  \\end{bmatrix}\n  - 2\n  =\n  \\begin{bmatrix}\n  \\color{red}{a_{11}} - 2 & \\color{red}{a_{21}} - 2 \\\\\n  a_{12} - 2 & a_{22} - 2 \\\\\n  \\end{bmatrix}\n  $$\n\n- 符合线性代数的条件下使用线性代数公式\n  例，**矩阵**相乘：行 X 列\n  $$\n  \\begin{bmatrix}\n  \\color{red}{a_{11}} & \\color{red}{a_{21}} \\\\\n  a_{12} & a_{22} \\\\\n  \\end{bmatrix}\n  \\begin{bmatrix}\n  \\color{green}{b_{11}} & b_{21} \\\\\n  \\color{green}{b_{12}} & b_{22} \\\\\n  \\end{bmatrix}\n  =\n  \\begin{bmatrix}\n  \\color{red}{a_{11}}\\color{green}{b_{11}}+\\color{red}{a_{21}}\\color{green}{b_{12}} &\n  \\color{red}{a_{11}}b_{21}+\\color{red}{a_{21}}b_{22} \\\\\n  a_{12}\\color{green}{b_{11}}+a_{22}\\color{green}{b_{12}} & a_{12}b_{21}+a_{22}b_{22} \\\\\n  \\end{bmatrix}\n  $$\n  如果使用 **glsl** 的内置函数 `matrixcompmult`，矩阵之间也可以实现**阵列相乘**\n  $$\n  \\begin{align}\n  M_1 &= \\begin{bmatrix}\n  \\color{red}{a_{11}} & \\color{red}{a_{21}} \\\\\n  a_{12} & a_{22} \\\\\n  \\end{bmatrix} \\\\\n  M_2 &= \\begin{bmatrix}\n  \\color{green}{b_{11}} & b_{21} \\\\\n  \\color{green}{b_{12}} & b_{22} \\\\\n  \\end{bmatrix}\\\\\n  matrixcompmult(M_1, M_2) &=\n  \\begin{bmatrix}\n  \\color{red}{a_{11}}\\color{green}{b_{11}} & \\color{red}{a_{21}}b_{21} \\\\\n  a_{12}\\color{green}{b_{12}} & a_{22}b_{22} \\\\\n  \\end{bmatrix}\n  \\end{align}\n  $$\n\n\n\n### 1.2 矩阵变换组合\n\n![](./images/Matrix_memory.png)\n\nOpenGL，Unity，Unreal 默认为**列向量优先存储：矩阵由列向量构成**\n$$\n\\begin{align}\nv_{世界} &= M_{模型 \\to世界} \\cdot v_{模型} \\\\\nv_{视点} &= V_{世界 \\to 视点} \\cdot v_{世界}\\\\\nv_{屏幕} &= P_{视点 \\to 透视} \\cdot v_{视点}\\\\\n&= P_{视点 \\to 透视} \\cdot V_{世界 \\to 视点} \\cdot M_{模型 \\to 世界} \\cdot v_{模型}\n\\end{align}\n$$\n\n\n\n### 1.3 齐次空间\n\n齐次坐标：将一个原本是 n 维的向量用一个 n+1 维向量来表示\n\n齐次坐标的齐次性：**多个齐次坐标表示的是一个点**，例：下表中的齐次坐标都表示 (1/3, 2/3) 这一个点\n\n| 齐次坐标     | 非 齐次坐标                   |\n| ------------ | ----------------------------- |\n| (x, y, w)    | (x/w,  y/w)                   |\n| (1, 2, 3)    | (1/3,  2/3)                   |\n| (2, 4, 6)    | (2/6,  4/6) = (1/3,  2/3)     |\n| (1a, 2a, 3a) | (1a/3a,  2a/3a) = (1/3,  2/3) |\n\n几何意义：\n\n- 为了避免用 $\\infty$ 这样无法量化的符号来表示无限远\n- 在非齐次坐标空间，两条平行线不会相交\n  在齐次坐标空间，两条平行线在无限远处相交于一点\n- n 维向量在参与 与 n+1 维向量的运算时，通过引入齐次坐标，使 n 维向量变为 n+1 维向量与 n+1 维向量运算，由于齐次坐标的齐次性，所以这样的升维计算并不影响最终结果\n  例：由于矩阵的加法总比乘法需要的矩阵高一维，所以需要**引入齐次坐标来合并矩阵的乘法和加法**\n\n\n\n**齐次坐标表示两条平行线相交**：\n\n- 在笛卡尔坐标系中，对于以下方程\n  如果 C $\\neq$ D，方程无解\n  如果 C $=$ D，方程表示的两条线是同一条线\n  $$\n  \\begin{cases}\n  Ax+By+C=0\\\\\n  Ax+By+D=0\n  \\end{cases}\n  $$\n\n- 在投影空间中，对于以下方程\n  如果 C $\\neq$ D，由 (C - D)w = 0 得 w = 0，**即当 w = 0 时并非无意义，而是表示一个无穷远**，所以两条平行线相交于点 (x, y, 0)，这个点在无穷远处\n  如果 C $=$ D，方程表示的两条线是同一条线\n  $$\n  \\begin{cases}\n  A{x\\over w}+B{y\\over w}+C=0\\\\\n  A{x\\over w}+B{y\\over w}+D=0\n  \\end{cases}\n  \\rightarrow\n  \\begin{cases}\n  Ax+By+Cw=0\\\\\n  Ax+By+Dw=0\n  \\end{cases}\n  $$\n\n\n\n**齐次坐标区分点和向量**：\n\nI. 设基坐标为 (x, y, z)，原点为 O，则**点比向量需要额外的信息**\n\n- 向量 $\\vec V(v_1, v_2, v_3) = v_1x + v_2y + v_3z$\n- 点 $P(p_1, p_2, p_3) - O = p_1x + p_2y + p_3z \\rightarrow P(p_1, p_2, p_3) = p_1x + p_2y + p_3z + O$\n\nII. 在齐次坐标 (x, y, z, w) 中，w = 0 代表无穷远，即一个方向\n- 非齐次坐标 转 齐次坐标\n  点     (x, y, z) 转为 (x, y, z, 1)\n  向量 (x, y, z) 转为 (x, y, z, 0)\n\n- 齐次坐标 转 非齐次坐标\n  (x, y, z, 1) 转为 点     (x, y, z)\n  (x, y, z, 0) 转为 向量 (x, y, z)\n\nIII. 应用：深度缓冲\n\n\n\n\n\n\n### 1.4 左右手坐标系\n\n![](images/LRHandCoordinate.jpeg)\n\n判断叉乘后的方向\n\n- 在右手坐标系下，使用 **右手定则** 判断叉乘的方向\n- 在左手坐标系下，使用 **左手定则** 判断叉乘的方向\n\n![](images/cross3.png)\n\n\n\n### 1.5 惯性坐标系\n\n定义：原点在模型坐标系原点上，坐标轴平行于世界坐标轴\n作用：简化世界坐标系到模型坐标系的转换\n\n与其他坐标系的转化：物体坐标系 ${旋转 \\over \\to}$ 惯性坐标系 ${平移 \\over \\to}$ 世界坐标系\n\n\n\n## 2. 线形变换矩阵\n\n### 2.1 2D 变换矩阵\n![](images/2D_affine_transformation_matrix.svg)\n\n\n\n### 2.2 Scale\n\n![](images/scale.png)\n\n缩放比例为 K 在不同的坐标轴上的缩放比例不同，**这里假设 缩放方向 N 必过原点，且 N 为单位向量**\n\n推导：\n$$\n\\begin{align}\nv_{||} &= {n \\cdot v \\over ||n||} \\cdot {n \\over ||n||} = (v \\cdot n)n\\\\\nv'_{||} &= kv_{||}\\\\\nv'_{\\bot} &= v_{\\bot} (与缩放方向垂直的方向不受缩放的影响) \\\\\nv' &= v'_{||} + v'_{\\bot}\\\\\n&= k(v \\cdot n)n + (v - (v \\cdot n)n)\\\\\n&= v+(k -1)(v \\cdot n)n\n\\end{align}\n$$\n\n- 核心公式：将三个基向量 v 分别沿 n 向量方向缩放后的向量构成的列矩阵为沿向量 n 的缩放矩阵\n  $$\n  v_{缩放后} = v + (K_{比例} - 1)(v \\cdot n_{缩放方向})n_{缩放方向}\n  $$\n\n- 由 **基坐标** 构成的 **列向量** 变化矩阵\n    $$\n    \\begin{array}{cc}\n    \\begin{bmatrix}\n    K_x & 0 &0 \\\\\n    0 & K_y & 0\\\\\n    0 & 0 & K_z\n    \\end{bmatrix}&\n    \\begin{bmatrix}\n    1+(K-1)x^2 & (K-1)yx & (K-1)zx\\\\\n    (K-1)xy & 1+(K-1)y^2& (K-1)zy\\\\\n    (K-1)xz & (K-1)yz& 1+(K-1)z^2\n    \\end{bmatrix}\\\\\n    沿坐标轴缩放 & 沿任意向量N_{(x,y,z)}缩放K\n    \\end{array}\n    $$\n\n\n\n\n### 2.3 Reflect\n\n缩放比例为 -1 时，就是镜像，**这里假设 缩放方向 n 必过原点，且 N 为单位向量**\n\n- 核心公式：将三个基向量 v 分别沿 n 向量方向缩放 -1 （缩放核心公式的比例 K = -1）后的向量构成的列矩阵为沿向量 n 的镜像矩阵\n  $$\n  v_{镜像后} = v - 2(v \\cdot n_{镜像方向})n_{镜像方向}\n  $$\n\n- 由 **基坐标** 构成的 **列向量** 变化矩阵\n  $$\n  \\begin{array}{cccc}\n  \\begin{bmatrix}\n  -1 & 0 & 0\\\\\n  0 & 1 & 0\\\\\n  0 & 0 & 1\n  \\end{bmatrix} &\n  \\begin{bmatrix}\n  1 & 0 & 0\\\\\n  0 & -1 & 0\\\\\n  0 & 0 & 1\n  \\end{bmatrix} &\n  \\begin{bmatrix}\n  -1 & 0 & 0\\\\\n  0 & -1 & 0\\\\\n  0 & 0 & 1\n  \\end{bmatrix} &\n  \\begin{bmatrix}\n  1-2x^2 & -2yx & -2zx\\\\\n  -2xy & 1-2y^2 & -2zy\\\\\n  -2xz & -2yz & 1-2z^2\n  \\end{bmatrix}\n  \\\\沿 X 轴镜像 & 沿 Y 轴镜像 & 沿 y=-x 轴镜像 & 沿任意单位向量N_{(x,y,z)}镜像 \n  \\end{array}\n  $$\n\n\n\n### 2.4 Rotate\n\n设 在**右手坐标系**，旋转角**逆时针**为正方向，**缩放方向 n 必过原点，且 N 为单位向量**\n\n- 核心公式：将三个基向量 v 分别绕向量 n 旋转后（代入核心公式后）的向量构成的列矩阵为 绕向量 n 的旋转矩阵，[公式推导](#4.2.3 三维空间旋转的拆分)\n  $$\n  v_{旋转后} = v \\cdot cos\\theta  + (v \\cdot n_{旋转轴})n_{旋转轴}(1 - cos\\theta) + (v \\times n_{旋转轴})sin\\theta\n  $$\n\n- 由 **基坐标** 构成的 **列向量** 变化矩阵\n    $$\n    \\begin{array}{cccc}\n    \\begin{bmatrix}\n    1 & 0 & 0\\\\\n    0 & cos\\theta & sin\\theta\\\\\n    0 &-sin\\theta & cos\\theta\n    \\end{bmatrix} &\n    \\begin{bmatrix}\n    cos\\theta & 0 & -sin\\theta\\\\\n    0 & 1 & 0\\\\\n    sin\\theta & 0 & cos\\theta\n    \\end{bmatrix} &\n    \\begin{bmatrix}\n     cos\\theta & sin\\theta & 0\\\\\n    -sin\\theta & cos\\theta & 0\\\\\n    0 & 0 & 1\n    \\end{bmatrix} & = &\n    \\begin{bmatrix}\n    (1-cos\\theta)x^2 + cos\\theta & (1-cos\\theta)yx+sin\\theta z & (1-cos\\theta)zx - sin\\theta y\\\\\n    (1-cos\\theta)xy - sin\\theta z & (1-cos\\theta)y^2 + cos\\theta & -(1-cos\\theta)zy + sin\\theta x\\\\\n    (1-cos\\theta)xz + sin\\theta y & (1-cos\\theta)yz - sin\\theta x & (1-cos\\theta)z^2 + cos\\theta\n    \\end{bmatrix}\\\\\n    沿 X 轴旋转 & 沿 Y 轴旋转 & 沿 Z 轴旋转 & &沿任意向量N_{(x,y,z)}旋转 \\theta\n    \\end{array}\n    $$\n\n\n\n\n### 2.5 Shear\n![](images/shear.png)\n\n变化后体积和面积保持不变\n\n- 核心公式：x' = x + sy\n\n- 方法：将三个基向量 v 分别取出 x，y，z 中的任意一个值，乘以变换因子，在把它加到 x，y，z 中的其他轴的值上，例：取 x 乘以变换因子 K\n  $$\n  v_{切变后} =\n  \\begin{bmatrix}\n  x \\\\ y + x * K_y \\\\ z + x * K_z\n  \\end{bmatrix}\n  $$\n\n- 由 **基坐标** 构成的 **列向量** 变化矩阵\n  $$\n  \\begin{array}{cccc}\n  \\begin{bmatrix}\n  1 & K_y & K_z\\\\\n  0 & 1 & 0\\\\\n  0 & 0 & 1\n  \\end{bmatrix} &\n  \\begin{bmatrix}\n  1 & 0 & 0\\\\\n  K_x & 1 & K_z\\\\\n  0 & 0 & 1\n  \\end{bmatrix} &\n  \\begin{bmatrix}\n  1 & 0 & 0\\\\\n  0 & 1 & 0\\\\\n  K_x & K_y & 1\n  \\end{bmatrix}\n  \\\\沿 X 轴切变 & 沿 Y 轴切变  & 沿 Z 轴切变 \n  \\end{array}\n  $$\n\n\n\n## 3. 几何变换\n\n### 3.1 基础变换\n\n**可逆变换**\n\n- 可以**撤销**原来的变换\n- 变换矩阵是**非奇异**\n- 变换矩阵行**列式不为零**\n\n**等角变换**\n\n- 变换后向量的**夹角不变**\n- 包括：平移、旋转、均匀缩放（镜像不是）\n\n**正交变换**\n\n- 变换矩阵 列/行 互相保持垂直，切为单位向量\n- 包括：平移、旋转、镜像\n- 变换矩阵行列式为 $\\pm 1$\n- **可根据 正交变换矩阵 = 逆矩阵 求逆矩阵**\n\n**刚体变换**\n\n- 只改变位置和方向\n- 包括：平移、旋转（镜像不是）\n- 例子：渲染中视野相机的变换\n\n\n\n### 3.2 线性变换（可逆）\n\n定义：\n\n- 原点固定\n- 直线变换后保持直线\n- 网格保持 **平行** 且 **等距分布**\n\n实质：线形变换不会导致平移（原点位置不变）\n$$\nF(ka + b) = kF(a) + F(b)\n$$\n\n### 3.3 仿射变换（可逆）\n\n仿射变换：*用于改变模型的位置和形状*\n\n定义：\n- 直线变换后保持直线\n- 网格保持 **平行** 且 **等距分布**\n\n实质：\n- 仿射变换 = 线性变换 + 平移\n- 一个向量空间 变换为 另一个向量空间\n- 增加一个维度后可以同过 **高维度的线性变换** 代替 **低维度的仿射变换**\n\n变换矩阵：例子，平移变换\n$$\n\\begin{bmatrix}x & y & z & \\color{red}1\\end{bmatrix}\n\\cdot\n\\begin{bmatrix}\n1 & 0 & 0 & 0\\\\\n0 & 1 & 0 & 0\\\\\n0 & 0 & 1 & 0\\\\\n\\color{red}{\\Delta x} & \\color{red}{\\Delta y} & \\color{red}{\\Delta z} & \\color{red}{1}\\\\\n\\end{bmatrix}\n= \\begin{bmatrix}x + \\color{red}{\\Delta x} & y + \\color{red}{\\Delta y} & z + \\color{red}{\\Delta z} & \\color{red}1\\end{bmatrix}\n$$\n\n同过 **高维度的线性变换** 代替 **低维度的仿射变换**\n ![](images/affine.gif)\n\n### 3.4 投影变换（不可逆）\n\n#### 3.4.1 投影\n\n投影是降维操作，数学上的投影为了便于计算将投影后的物体和被投影的物体放在一侧\n\n![](./images/projection1.png)\n$$\n\\begin{align}\nP  &= (x,y,z) \\\\\nP' &= ({x \\over z}, {y \\over z}, z)\n\\end{align}\n$$\n\n\n#### 3.4.2 正交投影\n\nOpenGL、Unity 中的正交投影（相机坐标系为右手坐标系）\n\n![](images/orthogonal2.png)\n\n几何意义：\n- 图像远近大小相同\n- 点到投影后对应点的连线(投影线)与其他**投影线互相平行**\n- 在线形缩放的基础上，沿投影方向的缩放比例为 0，其他缩放比例不变\n\n\n\n简单的正交投影矩阵：\n$$\n\\begin{array}{cccc}\n\\begin{bmatrix}\n1 & 0 & 0\\\\\n0 & 1 & 0\\\\\n0 & 0 & 0\n\\end{bmatrix} &\n\\begin{bmatrix}\n1 & 0 & 0\\\\\n0 & 0 & 0\\\\\n0 & 0 & 1\n\\end{bmatrix} &\n\\begin{bmatrix}\n0 & 0 & 0\\\\\n0 & 1 & 0\\\\\n0 & 0 & 1\n\\end{bmatrix} &\n\\begin{bmatrix}\n1- x^2 & -yx & -zx\\\\\n-xy & 1- y^2 & -zy\\\\\n-xz & -yz & 1- z^2\n\\end{bmatrix} \n\\\\沿 Z 轴投影到 XY平面 & 沿 Y 轴投影到 XZ平面  & 沿 X 轴投影到 YZ平面 & 沿 N(x,y,z) 投影到垂直于 N 的平面上\n\\end{array}\n$$\n\n\n\n**OpenGL 中的正交投影矩阵** [推导过程](http://www.songho.ca/opengl/gl_projectionmatrix.html)\n\n![](images/orthogonal.png)\n$$\nM_{正交} = \n\\begin{bmatrix}\n2 \\over {right - left} & 0 & 0 & -{{right + left}\\over{right - left}}\\\\\n0 & 2 \\over {top - bottom} & 0 & -{{top + bottom}\\over{top - bottom}}\\\\\n0 & 0 & -2 \\over {far - near} & -{{far + near}\\over{far - near}}\\\\\n0 & 0 & 0 & 1\n\\end{bmatrix}\n{\n投影体对称\n\\over\n\\Longrightarrow\n}\n\\begin{bmatrix}\n1 \\over right & 0 & 0 & 0\\\\\n0 & 1 \\over top & 0 & 0\\\\\n0 & 0 & -2 \\over {far - near} & -{{far + near}\\over{far - near}}\\\\\n0 & 0 & 0 & 1\n\\end{bmatrix}\n$$\n\n\n\nDriectX  的正交投影矩阵为\n\n- **行主序矩阵**\n-  Z 的标准设备空间范围限定为 [0,1]\n- 相机坐标系为 **左手坐标系**\n\n$$\nM_{DriectX \\space 正交} = \n\\begin{bmatrix}\n1 \\over right & 0 & 0 & 0 \\\\\n0 & 1 \\over top & 0 & 0\\\\\n0 & 0 & {1 \\over {far - near}} & 0\\\\\n0 & 0 & -{ near \\over {far - near}} & 1\\\\\n\\end{bmatrix}\n$$\n\n\n\n**另一种表示 OpenGL、Unity 正交投影矩阵的方式**\n$$\nM_{正交} = \n\\begin{bmatrix}\n1 \\over Aspect \\cdot Size & 0 & 0 & 0\\\\\n0 & 1 \\over Size & 0 & 0\\\\\n0 & 0 & -2 \\over {far - near} & -{{far + near}\\over{far - near}}\\\\\n0 & 0 & 0 & 1\n\\end{bmatrix}\n$$\n\n\n\n**标准设备空间坐标的转换**\n\n1. 得到裁剪空间的坐标\n\n$$\nM_{正交}\n\\begin{bmatrix}\nx \\\\ y \\\\ z \\\\ 1\n\\end{bmatrix}\n= \n\\begin{bmatrix}\n{1 \\over right}x \\\\ {1 \\over top}y \\\\ {-2 \\over {far - near}}z -{{far + near}\\over{far - near}} \\\\ 1\n\\end{bmatrix}\n$$\n\n2. 裁剪：取裁剪空间坐标的 x, y, z 的值均在 [-1, 1] 范围内的值\n3. 此时 w 分量为 1，得到标准设备空间坐标\n\n\n\n#### 3.4.3 透视投影\n\nOpenGL、Unity 中的透视投影（相机坐标系为右手坐标系）\n\n![](images/projection.png)\n\n\n几何意义：\n\n- 图像近大远小\n- 点到投影后对应点的连线(投影线)与其他**投影线相交于一点**（投影中心）\n- 小孔成像：投影中心 在 投影平面 前\n\n\n\n**OpenGL 中透视投影矩阵**，[推导过程](http://www.songho.ca/opengl/gl_projectionmatrix.html)\n\nFOV：Field Of View (视场角) 决定视野范围，视场角越大，焦距越小\n\nAspect：横纵比，宽 : 高\n\n![](images/perspective.png)\n\n$$\nM_{透视} = \n\\begin{bmatrix}\n2 \\cdot near \\over {right - left} & 0 & {right + left}\\over{right - left} & 0\\\\\n0 & 2 \\cdot near \\over {top - bottom} & {top + bottom}\\over{top - bottom} & 0\\\\\n0 & 0 & -{{far + near} \\over {far - near}} & -{2 \\cdot far \\cdot near \\over {far - near}}\\\\\n0 & 0 & -1 & 0\n\\end{bmatrix}\n{\n投影体对称\n\\over\n\\Longrightarrow\n}\n\\begin{bmatrix}\nnear \\over right & 0 & 0 & 0\\\\\n0 & near \\over top & 0 & 0\\\\\n0 & 0 & -{{far + near} \\over {far - near}} & -{2 \\cdot far \\cdot near \\over {far - near}}\\\\\n0 & 0 & -1 & 0\n\\end{bmatrix}\n$$\n\n\n\nDriectX  的透视投影矩阵为\n\n- **行主序矩阵**\n-  Z 的标准设备空间范围限定为 [0, w]\n- 相机坐标系为 **左手坐标系**\n\n$$\nM_{DriectX \\space 透视} = \n\\begin{bmatrix}\nnear \\over right & 0 & 0 & 0 \\\\\n0 & near \\over top & 0 & 0\\\\\n0 & 0 & {far \\over {far - near}} & 1\\\\\n0 & 0 & -{far \\cdot near \\over {far - near}} & 0\\\\\n\\end{bmatrix}\n$$\n\n\n\n**另一种表示 OpenGL、Unity  投影矩阵的方式**\n$$\nHeight_{near} = 2 \\cdot near \\cdot \\tan{FOV \\over 2} \\\\\n\nM_{透视} = \n\\begin{bmatrix}\n\\cot{FOV \\over 2} \\over Aspect & 0 & 0 & 0 \\\\\n0 & \\cot{FOV \\over 2} & 0 & 0 \\\\\n0 & 0 & -{{far + near} \\over {far - near}} & -{2 \\cdot far \\cdot near \\over {far - near}} \\\\\n0 & 0 & -1 & 0 \\\\\n\\end{bmatrix}\n$$\n\n\n\n**标准设备空间坐标的转换**\n\n1. 得到裁剪空间的坐标\n\n$$\n\\begin{bmatrix}\nnear \\over right & 0 & 0 & 0\\\\\n0 & near \\over top & 0 & 0\\\\\n0 & 0 & -{{far + near} \\over {far - near}} & -{2 \\cdot far \\cdot near \\over {far - near}}\\\\\n0 & 0 & -1 & 0\n\\end{bmatrix}\n\\begin{bmatrix}\nx \\\\ y \\\\ z \\\\ 1\n\\end{bmatrix}\n= \n\\begin{bmatrix}\n{near \\over right}x \\\\ {near \\over top}y \\\\ {-{{far + near} \\over {far - near}}}z -{2 \\cdot far \\cdot near \\over {far - near}} \\\\ -z\n\\end{bmatrix}\n$$\n\n2. 裁剪：取裁剪空间坐标的 x, y, z 的值均在 [-z, z] 范围内的值\n3. 让坐标的 w 分量再次变为 1，得到标准设备空间坐标\n\n$$\n\\begin{bmatrix}\n{near \\over right}x \\\\ {near \\over top}y \\\\ {-{{far + near} \\over {far - near}}}z -{2 \\cdot far \\cdot near \\over {far - near}} \\\\ -z\n\\end{bmatrix}\n=\n\\begin{bmatrix}\n- {near \\over right}{x \\over z} \\\\ \n-{near \\over top}{y \\over z} \\\\ \n{{far + near} \\over {far - near}} +{2 \\cdot far \\cdot near \\over {far - near}}{1 \\over z} \\\\ 1\n\\end{bmatrix}\n$$\n\n\n\n### 3.5 法线变换\n\n法线 normal：\n\n- 垂直于一个平面\n- 非等比缩放的变换会使得变换后的法线不在垂直与原来的平面\n\n![](./images/normalVector.png)\n\n\n\n切线 tangent：\n\n- 垂直于法线\n- 一般由两个顶点的差计算得到切线向量，与纹理空间对齐\n- 切线的变换不受非等比缩放等变换的影响\n\n\n\n**由切线变换求法线变换**，其中\n\n- T 为切线向量，$M_t$ 为 切线变换矩阵\nN 为法线向量，$M_n$ 为对应切线变换的 法线变换矩阵\n- 平移变换不影响向量的方向，在法线变换时不需要考虑平移变换\n- 只有旋转矩阵是正交矩阵\n统一缩放会导致每一行/列的向量长度不为 1，从而不是正交矩阵\n- 根据点积计算公式，若 T、N 均为列向量，则 $T \\cdot N = T^T * N$\n\n$$\n\\begin{align}\nT\\cdot N &= T^T N = 0 \\\\\n(M_t T) \\cdot (M_n N) &= T^T N \\\\\n(M_t T)^T (M_n N) &= T^T N \\\\\nT^T M_t^T M_n N &= T^T N \\\\\nT^T (M_t^T M_n) N &= T^T N \\\\\n\\\\\nM_t^T M_n &= I \\\\\nM_n &= (M_t^T)^{-1} \\\\\n如果 \\space M_t \\space 为正交矩阵 \\space M_n&= M_t\n\\end{align}\n$$\n\n\n\n#### 3.5.1 Gram-Schmidt 格拉姆-施密特正交化\n\n标准正交化两个线性无关的向量 a 和 b，让它们变成 $q_1$ 和 $q_2$ \n\n![](./images/GramSchmidt.jpg)\n\n设 B 使得 $a \\bot B$， p 是 b 在 a 上的投影\n$$\n\\begin{align}\np &= normalize(a) \\cdot b * normalize(a) \\\\\nB &= b - p = b - normalize(a) \\cdot b * normalize(a)\n\\end{align}\n$$\n\n\n\n\n# 引用\n\n- [齐次坐标解释平行线相交](http://www.songho.ca/math/homogeneous/homogeneous.html)\n- [齐次坐标的说明](https://blog.csdn.net/business122/article/details/51916858)\n- [齐次坐标的理解](http://www.cnblogs.com/csyisong/archive/2008/12/09/1351372.html)\n- [投影矩阵的推导](http://www.songho.ca/opengl/gl_projectionmatrix.html)\n- [Depth Precision Visualized](https://developer.nvidia.com/content/depth-precision-visualized)\n- [线性代数20——格拉姆-施密特正交化](https://zhuanlan.zhihu.com/p/125646432)\n\n"
  },
  {
    "path": "LinearAlgebra/Part2_Quaternion.md",
    "content": "[TOC]\n\n# 一、3D 中的方位与角位移\n\n**方位**：从上一方位旋转后的 结果值（单一状态，用欧拉角表示）\n**角位移**：相对于上一方位旋转后的 偏移量（用四元数、矩阵表示）\n\n\n\n## 1. 欧拉角 (Euler angles)\n\n定义：\n\n- 欧拉角可以用来描述任意旋转，将一个角位移分解为三个互相垂直轴的**顺序旋转步骤**\n- 旋转后，原来互相垂直的轴可能不再垂直，**当前步骤只能影响下一个旋转步骤，不能影响之前的旋转步骤**，通过这个特性我们可以选择**适合的旋转方式 (如：YXZ）**来避免万像锁的发生\n- 这里**默认右手坐标系，逆时针为正**，任意三个轴可以作为旋转轴，下图仅为举例\nheading：绕**惯性坐标系**的 Y 轴旋转\nyaw：绕**模型坐标系**的 Y 轴旋转 \n\n![](images/rollPichYaw.png)\n\n优点：\n\n- 仅需要三个角度值，节省内存\n- 表示直观（便于显示和输入方位）\n- 任意三个数对于欧拉角都是合法的\n\n缺点：\n\n- 欧拉角之间求差值（角位移）困难\n- 欧拉角的方位表达方式不唯一（n = n + 360）\n- 由于三个角度不互相独立，可能产生：pich 135 = heading 180 + pich 45 + bank 180 的情况\n  （通过限制角的范围解决，heading 和 bank 限制在 -180 ～ 180，pitch 限制在 -90 ～90）\n- 万向锁问题（避免方法：**重新排列角度的旋转顺序**，但万向锁仍可能产生）\n\n[万向锁](http://v.youku.com/v_show/id_XNzkyOTIyMTI=.html)：\n\n- 当沿着任意角位移 90 度时，导致两个方向的旋转轴同线，导致三次旋转中有两次旋转的效果是一样的\n  即：少了一个维度的旋转变化\n- 在万向锁的情况下仍可以旋转到想要的位置，但必须同时旋转三个轴向，这时物体没有按照期望的方式旋转，下图的箭头如果是相机，则相机会发生抖动\n\n|![](images/gimbalLock.png)| ![](images/gimbalLock.gif) | ![](images/gimbalLockRight.png) |\n| :----: | :----: | :----: |\n| 万向锁 |**发生万向锁后**的旋转|期望的旋转|\n\n\n\n## 2. 四元数的相关知识\n\n### 2.1 复数\n\n[复数](https://en.wikipedia.org/wiki/Complex_number)是一种复合的数字，$C_{复数} = a + b \\cdot i$ ，其中 a、b 为实数，$i$ 为虚数，$i^2 = -1$\n\n- 复数通过 **实部 a 和 虚部 b** 构成的二维虚平面，将一维的数扩展到二维平面\n- $i$ 只是区分 实部  a 和 虚部 b 的标记，这样可以把复数用二维的向量表示\n  加法：$(a + bi) + (c + di) = (a + c) + (b+d)i$\n  乘法：$(a + bi) *(c + di) = (ac - bd) + (bc+ad)i$\n- 复数乘以 $i$ 的几何意义，在二维平面上逆时针旋转 90 度\n  $(a + b \\cdot i) * i = a*i + b*i^2 = -b + a \\cdot i$ \n\n![](images/complex_number.png)\n\n\n\n### 2.2 欧拉旋转定理\n\n![](images/EularRotate.png)\n\n这里 $a = cos \\phi, b = sin \\phi, \\phi$ 是从上一方位到当前方位的角位移，复数值表示当前方位 则 $c_{方位} =  a + b \\cdot i$ 在极坐标下表示为 $e_{方位} = cos \\phi + sin \\phi \\cdot i$\n在极坐标下，复数能够更方便的表示旋转角的变化：\n\n1. **连续的旋转可以用两个复数的乘积表示**\n   例： $e_1$ 为先旋转 $\\phi$，$e_2$ 再旋转 $\\theta$，则根据 $i^2 = -1$ 以及[和差公式](https://en.wikipedia.org/wiki/Trigonometric_functions)可得最后的方位\n   $$\n   \\begin{align}\n   e_1 &= cos \\phi + sin \\phi \\cdot i \\\\\n   e_2 &= cos \\theta + sin \\theta \\cdot i \\\\\n   e &= cos(\\phi + \\theta) + sin(\\phi + \\theta) \\cdot i \\\\\n   &= (cos \\phi \\cdot cos \\theta - sin \\phi \\cdot sin \\theta) +(sin \\phi \\cdot cos \\theta + cos \\phi \\cdot sin \\theta) \\cdot i \\\\\n   &= (cos \\phi + sin \\phi \\cdot i )(cos \\theta + sin \\theta \\cdot i) \\\\\n   &= e_1 \\cdot e_2\n   \\end{align}\n   $$\n\n2. 在不知道当前角位移的情况下，**可以通过当前方位+角位移计算出之后的方位**\n\n\n\n### 2.3 三维空间旋转的拆分\n\n四元数在表示三维空间旋转的方式时采用**轴角式（Axis-angle）**的旋转\n轴角式旋转方法如下图，v 绕过原点的方向向量 u 逆时针旋转 $\\theta$ 得到 v ’ （图中采用右手坐标系，逆时针旋转方向为正方向）\n\n![](images/axisAngle.png)\n\n拆分轴角式旋转：\n\n1. 如下图 A，假设 u 是单位向量（[求 v 到 u 的投影向量 v||](https://www.cnblogs.com/graphics/archive/2010/08/03/1791626.html)）\n   $$\n   \\begin{align}\n   v &= v_{||} + v_{\\bot}\\\\\n   v_{||} &= v_{||}'\\\\\n   v_{||} &= proj_u(v) = {u \\cdot v \\over ||u||} \\cdot {u \\over ||u||} = {(u \\cdot v)u \\over ||u||^2} = (u \\cdot v)u\\\\\n   v_{\\bot} &= v - v_{||} = v - (u \\cdot v)u\n   \\end{align}\n   $$\n\n2. 如下图 B，C：w 既垂直于 u 又垂直于 $v_{\\bot}$\n   $$\n   \\begin{align}\n   w &= u \\times v_{\\bot}\\\\\n   ||w|| &= ||u \\times v_{\\bot}|| \\\\\n   &= ||u|| \\cdot ||v_{\\bot}|| \\cdot sin{\\pi \\over 2}\\\\\n   &= ||v_{\\bot}|| \\\\\\\\\n   v_{\\bot}' &= v_v' + v_w'\\\\\n   &= v_{\\bot} \\cdot cos\\theta + w \\cdot sin\\theta \\\\\n   &= v_{\\bot} \\cdot cos\\theta + (u \\times v_{\\bot})sin\\theta\n   \\end{align}\n   $$\n\n3. 结合 1，2 可得\n   $$\n   v' = v \\cdot cos\\theta + (u\\cdot v)u(1 - cos\\theta)+(u \\times v)sin\\theta\n   $$\n\n\n\n\n![](images/axisAngleSplit.png)\n\n\n\n## 3. 四元数 (Quaternion)\n\n> 相对于复数的二维空间，为了解决三维空间的旋转变化问题，爱尔兰数学家 William Rowan Hamilton 把复数进行了推广，也就是四元数\n>\n> **四元数包含旋转方向**： 3D 中的一个旋转对应正向和反向旋转的两个四元数，不是一一对应的\n\n定义：**四元数表示角位移的大小**\n\n- 与复数类似，四元数由 1 个实部 和 3 个虚部构成。其中，a、b、c 、d 为实数，$i$ 为虚数\n  $\\vec Q_{四元数} = a + b \\cdot i_x + c \\cdot i_y + d \\cdot i_z$\n\n- $i$ 代表旋转，$-i$ 代表反向旋转\n  $$\n  \\begin{align}\n  i_x * i_y * i_z &= i_x * i_x = i_y * i_y = i_z * i_z = -1\\\\\n  i_x &= i_y * i_z  = - i_z * i_y = - i_x\\\\\n  -i_y &= i_x * i_z \\\\\n  \\end{align}\n  $$\n\n - 四元数的 3 个虚数 $i$ 之间的乘法与向量之间的点积结果形式很类似，于是四元数有了另外一种表示形式\n$$\n\\begin{align}\n\\vec Q_{四元数} &= w + x \\cdot i_x + y \\cdot i_y + z \\cdot i_z \\\\\n&= (x, y, z, w) \\\\\n&= (\\vec u, w)\n\\end{align}\n$$\n\n优点：\n\n- **平滑差值**：slerp 和 squad 提供了方位间的平滑差值**（矩阵和欧拉角都没有这个功能）**\n- 快速连接和角位移求逆：\n  多个角位移 ${四元数叉乘 \\over \\to}$ 单个角位移（比矩阵快）\n  反角位移 = 四元数的共轭（比矩阵快）\n\n缺点：\n\n- 四元数比欧拉角多存储一个数（当保存大量角位移时尤为明显，如存储动画数据）\n- 浮点数舍入误差和随意输入，会导致四元数不合法（可以通过四元数标准化解决，确保四元数为单位大小）\n- 难以直接使用\n\n\n\n### 3.1 四元数的运算\n\n- **乘法，合并两个四元数的偏移量，得到总的角位移**\n  四元数的乘法有很多种，其中最常用的一种是格拉丝曼积，与数学多项式乘法相同（与复数乘法概念相同）\n  乘法满足结合律：abc = (ab)c = a(bc)\n  但不符合交换律：ab != ba\n  $$\n  \\begin{align}\n  \\vec Q_1 \\vec Q_2 &= w_1w_2 - \\vec u_1 \\cdot \\vec u_2 + w_1 \\vec u_2 + w_2 \\vec u_1 + \\vec u_1 \\times \\vec u_2\\\\\n  &= ((w_1 \\vec u_2 + w_2 \\vec u_1 +  \\vec u_1 \\times \\vec u_2), (w_1w_2 - \\vec u_1 \\cdot \\vec u_2))\n  \\end{align}\n  $$\n\n- 四元数与标量相乘、点积、加法、叉乘、单位化，均与四维向量的点积相同，以点积为例\n  $$\n  \\begin{align}\n  \\vec Q_1 \\cdot \\vec Q_2 &= (w_1, x_1, y_1, z_1)(w_2, x_2, y_2, z_2) \\\\\n  &= w_1w_2 + x_1 x_2 + y_1y_2 + z_1z_2\n  \\end{align}\n  $$\n\n- 求模：代表一个四维的长度，与向量的求模方法一致\n  $$\n  ||\\vec Q|| = \\sqrt{w^2 + \\vec u \\cdot \\vec u} = \\sqrt{w^2 + x^2 + y^2 + z^2}\n  $$\n\n- 共轭：实部相同，虚部相反（与复数共轭概念相同）\n  $$\n  \\vec  Q^* \\vec  Q = (-\\vec u, w) (\\vec u,w) = ||\\vec Q||^2 = \\vec Q \\cdot \\vec Q\n  $$\n\n- 求逆：**为了计算四元数的 \"差\"**，例\n  给定方位 A 和 B，求 A 旋转到 B 的**角位移 d**，即：Ad = B，$d = A^{-1}B$\n  $$\n  \\begin{align}\n  \\vec Q\\vec Q^{-1} &= 1\\\\\n  \\vec Q^* \\vec Q\\vec Q^{-1} &= \\vec Q^*\\\\\n  ||\\vec Q||^2 \\cdot \\vec Q^{-1} &= \\vec Q^*\\\\\n  \\vec Q^{-1} &= {\\vec Q^* \\over ||\\vec Q||^2}\\\\\n  \\end{align}\n  $$\n\n- 单位四元数：任意四元数乘以单位四元数后保持不变，$(\\vec 0, \\pm 1)$，模为 1\n  **单位四元数的 逆 = 共轭**，由于共轭比逆好求出，一般用四元数的共轭代替逆使用\n  几何上存在两个单位四元数 -u 和 u，因为他们几何意义相同都表示没有位移，但数学上只有 u\n  $$\n  Q_{单位} = {Q \\over ||Q||}\\\\\n  Q_{单位}Q_1 = Q_1Q_{单位} = Q_1\\\\\n  ((w_1 \\vec u + w \\vec u_1 +  \\vec u_1 \\times \\vec u), (w_1w - \\vec u_1 \\cdot \\vec u)) = (\\vec u_1, w_1)\n  $$\n\n\n\n### 3.2 四元数默认在极坐标下\n\n**极坐标下的优势：使四元数的运算和向量的运算方法一致**\n\n由 4.2.1 复数在笛卡尔坐标和极坐标的转换方式可得，四元数在\n\n- 笛卡尔坐标下为\n  $\\vec Q = (x, y, z, w) = (\\vec u, w)$\n- 极坐标下为，其中 $\\theta$ 为绕 $\\vec u$ 旋转后的角位移（旋转方式见 4.2.3，[推导到极坐标](https://krasjet.github.io/quaternion/quaternion.pdf)）\n  $\\vec Q  = (x sin{\\theta \\over 2}, y sin{\\theta \\over 2},z sin{\\theta \\over 2}, cos{\\theta \\over 2}) = (\\vec u sin{\\theta \\over 2}, cos{\\theta \\over 2})$\n\n\n\n**只有旋转轴 u 为单位向量时，下面公式才成立**\n\n\n- 用指数代替四元数：根据旋转角位移 $ \\theta $ 和旋转轴 u 求出四元数\n  $$\n  e^{(\\vec u {\\theta \\over 2}, 0)} = (\\vec usin{\\theta \\over 2}, cos{\\theta \\over 2})\n  $$\n\n- 对数：根据四元数和旋转轴 u 求出旋转角位移 $\\theta$\n  $$\n  log_e(（\\vec u sin{\\theta \\over 2}, cos{\\theta \\over 2}）) = (\\vec u {\\theta \\over 2}, 0)\n  $$\n\n- 将点 P 绕 $ \\vec u $ 旋转 $ \\theta $ 度\n  $P_{旋转后} = aPa^{-1} = aPa^*, a = (\\vec u sin{\\theta \\over 2}, cos{\\theta \\over 2})$\n\n- 将点 P 绕 $ \\vec u $ 旋转 $ \\theta $ 度后再旋转 $\\alpha$ 度（方位的叠加是点乘）\n  $P_{旋转后} = baPa^{-1}b^{-1} = (ba)P(ba)^{-1},a = (\\vec u  sin{\\theta \\over 2},cos{\\theta \\over 2}),b = (\\vec u sin{\\alpha \\over 2},cos{\\alpha \\over 2})$\n\n- **四元数求幂**：四元数的 x 次幂等同于将它的旋转角缩放 x 倍\n  $$\n  (\\vec u sin{\\theta \\over 2}, cos{\\theta \\over 2})^x\n  = (\\vec u {sin({\\theta \\over 2}x)\\over sin{\\theta \\over 2}}, cos({\\theta \\over 2}x))\n  $$\n\n\n\n四元数 * 向量 = 旋转后的向量，例：\n设 用四元数 q = (u, w)，u 为单位向量，旋转三维向量 v，则\n$$\n\\begin{align}\np &= (v, 0)\\\\\np'&= qpq^{-1} = qpq^* = (u,w)(v,0)(-u,w)\\\\\n&= (2(u\\cdot v)u + (w^2 -u\\cdot v)v + 2w(u \\times v),...)\\\\\n&= (v',...)\n\\end{align}\n$$\n\n\n\n### 3.3 四元数的常用插值方法\n\n所有插值用的旋转四元数**都是单位四元数**\n插值要采用弧面最短路径\n\n- $Q$ 和 $-Q$ 代表不同的旋转角度得到的相同方位，在插值时会有不同的结果，如下图：可以将 q0 和 q1（蓝色的弧）插值，这会导致 3D 空间的向量旋转接近 360 度，而实际上这两个旋转相差并没有那么多，所以 q0 和 -q1（红色的弧）的插值才是插值的最短路径\n- 每次插值前要通过 $cos\\theta = q_0 \\cdot q_1$ 判断 q0 和 q1 的夹角是否为钝角，若为钝角将 q1 改为 -q1 来计算插值\n\n![](images/Interpolation.png)\n\n\n\n**线形插值**（Lerp：**L**inear Int**erp**olation）\n\n- 对四元数插值后，得到的结果不是单位四元数，插值的弧度越大缺点越明显\n- 公式：$Q_t = (1- t)Q_0 + t Q_1, t$ 为插值比例\n\n![](images/Lerp.png)\n\n\n\n**正规化线性插值**（Nlerp：**N**ormalized **L**inear Int**erp**olation）\n\n- 对四元数插值后，虽然把弦等分了，但是弦上对应的弧却不是等分的，插值的弧度越大缺点越明显\n- 公式：$Q_t = {{ (1- t)Q_0 + tQ_1}\\over{||(1- t)Q_0 + tQ_1||}}, t$ 为插值比例\n\n![](images/NLerp.png)\n\n**旋转角度球面线性插值**（Slerp：**S**pherical **L**inear Int**erp**olation）\n\n- 对单个角度做线形插值，做到固定角速度，无法平滑过渡连接不同方向的角度\n- 公式 1：这里用的四元数都是单位四元数，所以有 $Q^{-1} = Q^*,  t$ 为插值比例\n  $$\n  Q_t = Q_0(Q_0^{-1}Q_1)^t = Q_0(Q_0^* Q_1)^t​\n  $$\n\n- 公式 2：效率比公式 1 高，其中 $\\theta = cos^{-1}(Q_0\\cdot Q_1)$\n  $$\n  Q_t = {sin((1 - t)\\theta) \\over sin\\theta} Q_0 + {sin(t\\theta) \\over sin\\theta}Q_1\n  $$\n\n\n![](images/Slerp.png)\n\n**Slerp 和 Nlerp 的比较**\n\n- 效率上 Nlerp 比 Slerp 高\n  效果上 Nlerp 和 Slerp 在单位四元数之间的夹⻆ θ 非常小时差别不大，夹角越大 Nlerp 的效果越差\n- 单位四元数之间的夹⻆ θ 非常小，那么 sinθ 可能会由于浮点数的误差被近似为 0.0，从而导致除以 0 的错误。我们在实施 Slerp 之前，需要检查两个四元数的夹⻆是否过小一旦发现这种问题，我们就必须改用 Nlerp 对两个四元数进行插值，这时候 Nlerp 的误差非常小所以基本不会与真正的 Slerp 有什么区别 \n\n\n\n### 3.4 贝塞尔曲线和 Squad 插值\n\n**样条（Spline）**：在一个向量序列 $v_0,v_1,...,v_n$ 中分别对每对向量 $v_i,v_{i+1}$ 进行插值后互相连接得到的曲线\n\n**贝塞尔曲线（Bézier）**：通过不断在现有点的基础上添加控制点，使最终得到的曲线更加平滑\n\n![](images/bezier.png)\n\n三次贝塞尔曲线：\n\n![](images/bezier.gif)\n\n![](images/bezier2.png)\n\n![](images/bezier3.png)\n\n插值方式可以用 lerp、Slerp 等方式，上图采用 de Casteljau 算法构造贝塞尔曲线\n上图采用 lerp 方式插值，插值方程为：\n$$\nv_t = (1 − t)^3v_0 + 3(1 − t)^2tv_1 + 3(1 − t)t^2v_2 + t^3v_3\n$$\n$de Casteljau$ 算法构造贝塞尔曲线的过程为：\n\n- 第一次贝塞尔曲线，由相邻的基础点得到插值点 $v_{01}、v_{12}、v_{23}$\n- 第二次贝塞尔曲线，由上次贝塞尔的插值点得到本次的插值点 $v_{012}、v_{123}$\n- 第三次贝塞尔曲线，由上次贝塞尔的插值点得到本次的插值点 $v_{0123}$\n\n\n\n**球面四边形插值**（Squad：**S**pherical and **Quad**rangle）\n\n- 平滑过渡连接不同方向的旋转角，效率最低，效果最好\n- Squad 的插值方法类似贝塞尔曲线的构造过程，**由于取基础点的方式不同，效率比构造贝塞尔曲线要高**\n  Squad 的插值过程中可以用 lerp、Slerp 等方式插值\n  如果使用 lerp 插值，**插值参数为 2t(1-t)，而非 t**\n  插值方程为：\n\n$$\nv_t = (2t^2 − 2t + 1)(1 − t)v_0 + 2(1 − t)^2tv_1 + 2(1 − t)t^2v_2 + t(2t^2 − 2t + 1)v_3\n$$\n\n  插值步骤为：\n1. 由基础点得到插值点 $v_{12}，v_{03}$\n2. 根据上次的插值点得出本次的插值点 $v_{0312}$\n\n![](images/Squad.png)\n\n\n\n三次贝塞尔曲线和 Squad 插值构造的曲线对比：\n\n![](images/comparedBS.png)\n\n\n\n## 4 欧拉角、旋转矩阵、四元数的互相转换\n\n### 4.1 欧拉角和旋转矩阵\n\n[欧拉角](#4.1 欧拉角 (Euler angles)) $ \\to $ 旋转矩阵\n\n- 这里的旋转矩阵和 [2.4 Rotate](#2.4 Rotate)  类似，这里的欧拉角操作的模型的坐标系\n\n- 设在**右手坐标系**，矩阵**列向量**存储，旋转角**逆时针**为正方向（改变的角度方向取反），则\n  最后的转换矩阵为 Heading/Pich/Roll 按需要的顺序相乘的结果（HPR 为相机避免万向死锁的最佳顺序）\n  $$\n  \\begin{align}\n  Heading &= \n  \\begin{bmatrix}\n  cosH & 0 & sinH\\\\\n  0 & 1 & 0\\\\\n  -sinH & 0 & cosH\n  \\end{bmatrix} \\\\\n  Pich &=\n  \\begin{bmatrix}\n  1 & 0 & 0\\\\\n  0 & cosP & -sinP\\\\\n  0 & sinP & cosP\n  \\end{bmatrix} \n  \\\\\n  Roll &=\n  \\begin{bmatrix}\n  cosR & -sinR & 0\\\\\n  sinR & cosR & 0\\\\\n  0 & 0 & 1\n  \\end{bmatrix} \\\\\n  M_{HPR} &= \n  \\begin{bmatrix}\n  cosHcosR+sinHsinPsinR & -cosHsinR+sinHsinPcosR & sinHcosP \\\\\n  sinRcosP & cosRcosP & -sinP\\\\\n  -sinHcosR+cosHsinPsinR & sinRsinH+cosHsinPcosR & cosHcosP\n  \\end{bmatrix}\n  \\end{align}\\\\\n  $$\n\n![](images/rollPichYaw.png)\n\n\n\n旋转矩阵 $ \\to$ [欧拉角](#4.1 欧拉角 (Euler angles))\n\n> 转换后的欧拉角是限制欧拉角，即 Heading 和 Roll 范围为 $\\pm$180 度，Pich 的范围为 90 度 \n\n当矩阵每列都是单位向量时，矩阵为正交矩阵，则\n$$\nM \\cdot M^{-1} = M \\cdot M^T = I\\\\\nM^{-1} = M^T\\\\\n$$\n\n1. 若 $Pich \\neq \\pm 90$，由欧拉角转矩阵的公式可得：\n\n$$\n\\begin{align}\n\\begin{bmatrix}\nm11 & m12 & m13\\\\\nm21 & m22 & m23\\\\\nm31 & m32 & m33\n\\end{bmatrix}\n&= \n\\begin{bmatrix}\ncosHcosR+sinHsinPsinR & -cosHsinR+sinHsinPcosR & sinHcosP \\\\\nsinRcosP & cosRcosP & -sinP\\\\\n-sinHcosR+cosHsinPsinR & sinRsinH+cosHsinPcosR & cosHcosP\n\\end{bmatrix}\\\\\n\\\\\nm23 &= -sinP\\\\\narcsin(-m23) &= P\\\\\n\\\\\nm13 &= sinHcosP\\\\\nm33 &= cosHcosP\\\\\n{m13 \\over m33} &= tanH\\\\\narctan({m13 \\over m33}) &= H\\\\\n\\\\\nm21 &= sinRcosP\\\\\nm22 &= cosRcosP\\\\\narctan({m21 \\over m22}) &= R\\\\\n\\end{align}\n$$\n\n2. 若 $Pich = \\pm 90$，是万向锁情况，此时 Heading 和 Roll 绕同样的轴竖直旋转，默认旋转的最后一步 Roll 不转，即 Roll = 0 ，由欧拉角转矩阵的公式可得：\n\n$$\n\\begin{align}\n\\begin{bmatrix}\nm11 & m12 & m13\\\\\nm21 & m22 & m23\\\\\nm31 & m32 & m33\n\\end{bmatrix}\n&= \n\\begin{bmatrix}\ncosH & sinHsinP & 0 \\\\\n0 & 0 & -sinP\\\\\n-sinH & cosHsinP & 0\n\\end{bmatrix}\\\\\n\\\\\nm11 &= cosH\\\\\nm13 &= -sinH\\\\\n-arctan({m13\\over m11}) &= H\n\\end{align}\n$$\n\n\n\n### 4.2 四元数和旋转矩阵\n\n[四元数](#4.3 四元数 (Quaternion)) $ \\to$ 旋转矩阵 \n\n设四元数为 $\\vec Q = (\\vec n, cos{ \\theta \\over 2 }) = (x,y,z,w)$，绕 n 旋转 $\\theta$ ，由 [2.4 Rotate](#2.4 Rotate) 沿任意方向旋转的矩阵 得旋转矩阵 M：\n$$\n\\begin{bmatrix}\n(1-cos\\theta)x^2 + cos\\theta & (1-cos\\theta)yx+sin\\theta z & (1-cos\\theta)zx - sin\\theta y\\\\\n(1-cos\\theta)xy - sin\\theta z & (1-cos\\theta)y^2 + cos\\theta & -(1-cos\\theta)zy + sin\\theta x\\\\\n(1-cos\\theta)xz + sin\\theta y & (1-cos\\theta)yz - sin\\theta x & (1-cos\\theta)z^2 + cos\\theta\n\\end{bmatrix}\n\\to M = \n\\begin{bmatrix}\n1-2y^2-2z^2 & 2xy+2wz & 2xz-2wy\\\\\n2xy-2wz & 1-2x^2-2z^2 & 2yz+2wx\\\\\n2xz+2wy & 2yz-2wx & 1-2x^2-2y^2\n\\end{bmatrix}\n$$\n\n\n\n旋转矩阵 $\\to $ [四元数](#4.3 四元数 (Quaternion))\n\n1. 由四元数转旋转矩阵可知：\n\n$$\n\\begin{align}\n\\begin{bmatrix}\nm11 & m12 & m13\\\\\nm21 & m22 & m23\\\\\nm31 & m32 & m33\n\\end{bmatrix}\n&= \n\\begin{bmatrix}\n1-2y^2-2z^2 & 2xy+2wz & 2xz-2wy\\\\\n2xy-2wz & 1-2x^2-2z^2 & 2yz+2wx\\\\\n2xz+2wy & 2yz-2wx & 1-2x^2-2y^2\n\\end{bmatrix}\\\\\\\\\nm11+m22+m33\n&= (1-2y^2-2z^2)+(1-2x^2-2z^2)+(1-2x^2-2y^2)\\\\\n&= 3-4(x^2+y^2+z^2)\\\\\n&= 3-4(1-w^2)\\\\\n&= 4w^2-1\\\\\n\\\\\nw&={\\sqrt{m11+m22+m33+1} \\over 2}\n\n\n\\end{align}\n$$\n\n2. 使 m11、m22、m33 其中两个为负，一个为正可得：\n\n$$\n\\begin{align}\nx &={\\sqrt{m11-m22-m33+1} \\over 2}\\\\\ny &={\\sqrt{-m11+m22-m33+1} \\over 2}\\\\\nz &={\\sqrt{-m11-m22+m33+1} \\over 2}\n\\end{align}\n$$\n\n以上方法得到的**四元数总是正的**，没有判断四元数符号的依据\n\n3. 在由：\n\n$$\n\\begin{align}\nm12 + m21 &= 4xy\\\\\nm12 - m21 &= 4wz\\\\\nm31 + m13 &= 4xz\\\\\nm31 - m13 &= 4wy\\\\\nm23 + m32 &= 4yz\\\\\nm23 - m32 &= 4wx\\\\\n\\end{align}\n$$\n\n4. 综上可得：\n$$\n\\begin{align}\nx &= {m23-m32 \\over 4w}\\\\\n情况一、w ={\\sqrt{m11+m22+m33+1} \\over 2} \\to  y &= {m31-m13 \\over 4w}\\\\\nz &= {m12-m21 \\over 4w}\\\\\n\\\\\nw &= {m23-m32 \\over 4x}\\\\\n情况二、x ={\\sqrt{m11-m22-m33+1} \\over 2} \\to  y &= {m12+m21 \\over 4x}\\\\\nz &= {m31+m13 \\over 4x}\\\\\n\\\\\nw &= {m31-m13 \\over 4y}\\\\\n情况三、y ={\\sqrt{-m11+m22-m33+1} \\over 2} \\to  x &= {m12+m21 \\over 4y}\\\\\nz &= {m23+m32 \\over 4y}\\\\\n\\\\\nw &= {m12-m21 \\over 4z}\\\\\n情况四、z ={\\sqrt{-m11-m22+m33+1} \\over 2} \\to  x &= {m31+m13 \\over 4z}\\\\\ny &= {m23+m32 \\over 4z}\n\\end{align}\n$$\n\n5. 取 1、2 中得到的 w、x、y、z 的最大值是哪个来判断，选择哪种情况\n\n\n\n### 4.3 欧拉角和四元数\n\n[四元数](#4.3 四元数 (Quaternion)) $ \\to $ [欧拉角](#4.1 欧拉角 (Euler angles))\n\n由 旋转矩阵 转 四元数 和 旋转矩阵 转 欧拉角的条件可得：\n$$\n\\begin{align}\nP &= asin(-2(yz+wx)) \\\\\nH &=\n\\begin{cases}\natan2(xz-wy, 0.5 - x^2 - y^2),& \\cos P \\neq 0 \\\\\natan2(-xz-wy, 0.5 - y^2 - z^2),& \\cos P = 0\n\\end{cases}\\\\\nR &=\n\\begin{cases}\natan2(xy-wz,0.5 - x^2 - z^2),& \\cos P \\neq 0\\\\\n0,& \\cos P = 0\n\\end{cases}\n\\end{align}\n$$\n\n[欧拉角](#4.1 欧拉角 (Euler angles)) $\\rightarrow$ [四元数](#4.3 四元数 (Quaternion))\n\n由四元数的公式得，在**右手坐标系**下的欧拉角列向量 H、P、R 为：\n$$\nHPR=\n\\begin{bmatrix}\ncos{H \\over 2}\\\\\\\\ 0 \\\\-sin{H \\over 2}\\\\ 0\n\\end{bmatrix}\n\\begin{bmatrix}\ncos{P \\over 2}\\\\\\\\ -sin{H \\over 2} \\\\0 \\\\0\n\\end{bmatrix}\n\\begin{bmatrix}\ncos{R \\over 2}\\\\\\\\ 0\\\\ 0\\\\-sin{H \\over 2}\n\\end{bmatrix}\n= \n\\begin{bmatrix}\ncos{H \\over 2}cos{P \\over 2}cos{R \\over 2}+sin{H \\over 2}sin{P \\over 2}sin{R \\over 2}\\\\\\\\ \n-cos{H \\over 2}sin{P \\over 2}cos{R \\over 2}-sin{H \\over 2}cos{P \\over 2}sin{R \\over 2} \\\\\ncos{H \\over 2}sin{P \\over 2}sin{R \\over 2}-sin{H \\over 2}cos{P \\over 2}cos{R \\over 2} \\\\\nsin{H \\over 2}sin{P \\over 2}cos{R \\over 2}-cos{H \\over 2}cos{P \\over 2}sin{R \\over 2} \n\\end{bmatrix}\n$$\n![](images/rollPichYaw.png)\n\n## 5. SQT 变换\n\n> 四元数只能表示旋转，但是 4 X 4 的矩阵却可以表示旋转、平移、缩放\n\nSQT 变换矩阵：四元数、平移向量、缩放向量/缩放统一系数 构成的一个 4 X 3 的矩阵\n\n使用 SQT 矩阵的目的：便于对 旋转、平移、缩放 的插值计算\n\n\n\n\n\n# 引用\n\n- [欧拉角，万向锁 视频解释](http://v.youku.com/v_show/id_XNzkyOTIyMTI)\n- [四元数的可视化解释](https://www.bilibili.com/video/av33385105/#reply1117064951)\n- [四元数的证明方式解释](https://krasjet.github.io/quaternion/)\n- [四元数在三维计算的几何意义](https://qiita.com/HMMNRST/items/0a4ab86ed053c770ff6a)\n\n"
  },
  {
    "path": "LinearAlgebra/Part3_Triangles.md",
    "content": "[TOC]\n\n# 一、三角形\n\n## 1. 内心\n\n三角形内心（三角形内切圆的圆心）：三角形内角平分线的交点\n\n已知三点坐标，求三角形内心坐标，[证明过程](https://www.zybang.com/question/272657890b84080ca669265cd181789c.html)\n\n![](images/incenter.png)\n\n设 $a = |BC|, \\space b = |AC|,\\space c = |AB|$ 则\n$$\n(x_I,y_I) = {a(x_A,y_A)+b(x_B,y_B)+c(x_C,y_C) \\over a+b+c}\n$$\n\n\n\n## 2. 垂心\n\n三角形从顶点到其对边的三条高的交点\n\n![](./images/linear_interpolate_triangle.png)\n\n应用：三角形着色时，已知三个点的颜色，混合计算整个三角形的颜色（高度比求插值）\n$$\n\\begin{align}\nf_i &= d_i / h_i \\\\\nColor &= f_i * Color_i + f_j * Color_j + f_k * Color_k\n\\end{align}\n$$\n\n\n\n## 3. 重心\n\n三角形从顶点到其对边中点的交点，具体推到见 [纹理，重心坐标](../OpenGL/Part4_Texture.md)\n\n应用：三角形着色时，已知三个点的颜色，混合计算整个三角形的颜色（面积比求插值）\n$$\n\\begin{align}\nf_i &= {area(x, x_j, x_k) \\over area(x_i, x_j, x_k)} \\\\\nColor &= f_i * Color_i + f_j * Color_j + f_k * Color_k\n\\end{align}\n$$\n\n\n\n## 4. 求面积\n\n已知三点坐标，求证三角形面积 $S_{\\Delta ABC} = {1 \\over 2}[(x_2-x_1)(y_3-y_1) - (y_2-y_1)(x_3-x_1)]$\n\n![](images/triangleSquare.png)\n$$\n\\begin{align}\nS_{\\Delta ABC} \n&= {1 \\over 2} |height||L_1| \\\\\n&= {1 \\over 2} |sin\\theta||L_2||L_1| \\\\\n&= {1 \\over 2} \\sqrt{1-(cos\\theta)^2}|L_2||L_1| \\\\\n&= {1 \\over 2} \\sqrt{1-({\\vec {BA} \\cdot \\vec {CA} \\over |L_2||L_1|})^2}\\cdot |L_2||L_1| \\\\\n&= {1 \\over 2} \\sqrt{(|L_1||L_2|)^2-(\\vec {BA} \\cdot \\vec {CA})^2} \\\\\n&= {1 \\over 2} \\sqrt{[(x_3 - x_1)^2 + (y_3-y_1)^2][(x_2-x_1)^2 + (y_2-y_1)^2]-[(x_2 - x_1)(x_3-x_1) + (y_2-y1)(y_3-y_1)]^2} \\\\\n\\end{align}\n$$\n\n设 $a=x_2-x_1, \\space b=y_2-y_1, \\space c=x_3-x_1, \\space d=y_3-y_1$  则 \n\n$$\n\\begin{align}\nS_{\\Delta ABC} \n&= {1 \\over 2} \\sqrt{(c^2 + d^2)(a^2 + b^2)-(ac + bd)^2} \\\\\n&= {1 \\over 2}|ad - bd| \\\\\n&= {1 \\over 2}|(x_2-x_1)(y_3-y_1) - (y_2-y_1)(x_3-x_1)|\n\\end{align}\n$$\n\n\n\n\n\n# 二、几何图元的数据描述\n\n一般出于性能的考虑，使用**重叠**在物体上的更简单的外形（通常有较简单明确的数学定义，例：圆形，矩形）来进行碰撞检测是常用碰撞检测的方法\n缺点：这些外形通常无法完全包裹物体，当检测到碰撞时，实际的物体可能并没有真正的碰撞\n\n\n\n## 1. 基本数据\n\n**常用的几何图元表示**\n\n```c++\nclass Edge {\n  vec3 PointStart;\n  vec3 PointEnd;\n};\n\nclass Line {\n  vec3 Pos;\n  vec3 Direction;\n};\n\nclass Sphere {\n  vec3 Center;\n  vec3 Radius;\n};\n\nclass Plan {      // 平面的普通表示 1\n  vec3 Position;\n  vec3 Direction; \n};\n\nclass Plan {      // 平面的化简表示 2\n  vec3 Direction;\n  vec3 Distance;  // 平面到原点的距离\n};\n\nclass Triangle {\n  vec3 Vector0;\n  vec3 Vector1;\n  vec3 Vector2;\n};\n\nclass PlanBoundVolume {\n  std::vector<Plane> Planes;\n};\n```\n\n\n\n## 2. AABB\n\nAABB 轴对齐碰撞箱（Axis-Aligned Bounding Box）\n\n- 碰撞箱为 矩形/立方体，它的边与所包围物体的**世界坐标系对齐**（为了便于计算碰撞箱）\n- 不会随所包装物体的旋转而旋转，只会**随所包装物体的旋转而缩放**\n- 表示方法有很多，通常用两个坐标系各轴都是模型中的点 最大/最小 的点来表示\n  描述 AABB 的几何结构为：碰撞箱的中心点 + 碰撞箱子各个坐标轴的半径（为了方便碰撞检测）\n\n```c++\nclass AABB {\n  vec3 Min; // 一般在左上角，也可以当做 Position 用\n  vec3 Max;\n};\n```\n\n\n\n\n\n# 三、距离检测\n\n## 1. 点、直线\n\n点到直线的**最近点**：其中 $L_{Dir}$ 是单位向量，$P_p,P_L,P_N$ 均为同一坐标系下点的坐标\n$$\nP_N = P_L + (L_{Dir} \\cdot (P_p - P_L))L_{Dir}\n$$\n\n![](./images/Line_to_Point_Distance.png)\n\n\n\n## 2. 点、线段\n\n点到线段的**最近点**：其中 $L_{Dir}$ 是单位向量，$P_p,P_L,P_N,P_A$ 均为同一坐标系下点的坐标（截取思想）\n$$\nP_N = P_L + Clamp(L_{Dir} \\cdot (P_p - P_L), 0, Length_{LA})L_{Dir}\n$$\n\n![](./images/Line_to_Point_Distance2.png)\n\n\n\n## 3. 点、AABB（点在 AABB 外）\n\n点与 AABB 的**最短距离**：计算 $P$ 点和 $P_{nearest}$ 的距离，就是最短距离\n$$\n   P_{nearest} = AABB.Clamp(P)\n$$\n\n   ```c++\n   vec2 AABBClamp(vec2 p, AABB b)\n   {\n     return new vec2(\n       clamp(p.x, b.Min.x, b.Max.x),\n       clamp(p.y, b.Min.y, b.Max.y)\n     );\n   }\n   ```\n\n\n\n## 4. 点、平面\n\n点到平面的**最近点**：在三维空间中，需要 $Dir_{plan}$ 平面的方向（一般为单位向量），和平面中的任意一点 $P_{plan}$\n$$\nP_{Near} = P - Dir_{plan} \\cdot (P - P_{plan})\n$$\n![](./images/Point_to_Plan_Distance.png)\n\n\n\n## 5. 点、三角形\n\n点到三角形的**最近点**\n步骤：求解点在三角形的哪个区域\n\n1. 顶点区域（最近点是顶点）\n2. 边区域（最近点是点到线短的距离）\n\n![](./images/Point_to_Triangle.png)\n\n\n\n\n\n## 6. 点、四面体\n\n点到四面体（模型）的**最近点**：遍历四面体的四个三角面，将点在三角面上做**投影**，求点到三角形的最近点\n\n![](./images/Point_to_Tetrahedron.png)\n\n\n\n## 7. 凸包与凸包\n\n> 几何体与几何体之间的距离\n>\n> 凸包的性质：连接凸包任意两点的直线，都在这个凸包的内部\n\n\n技巧：凸物体与球的最短距离，等价转换为凸膨胀体与点的距离\n\n凸包与凸包之间的距离通用算法：GJK（Gilbert-Johnson-Keerthi distance algorithm）\n\n![](./images/Convex_hull.png)\n\n\n\n**工程化的计算凸包和凸包的距离**：通过有限次数的迭代 Support 点来求得两个凸包的近似距离\n\n\n\n\n\n# 四、区域检测\n\n## 1. 点、面\n\n对于平面 plane 和其法线 $\\vec n$\n\n- $(Q-P) \\cdot \\vec n > 0$，Q 在平面 plane 外侧 out\n- $(Q-P) \\cdot \\vec n = 0$，Q 在平面 plane 上\n- $(Q-P) \\cdot \\vec n < 0$，Q 在平面 plane 内侧 in\n\n![](./images/point_plan.jpg)\n\n\n\n## 2. 线、面\n\n对于任意一条直线 $Q_1Q_2$，则\n\n- $(Q_1-P) \\cdot \\vec n > 0, \\space (Q_2-P) \\cdot \\vec n > 0$，直线 $Q_1Q_2$ 在平面 plane 外侧 out\n- $(Q_1-P) \\cdot \\vec n < 0, \\space (Q_2-P) \\cdot \\vec n < 0$，直线 $Q_1Q_2$ 在平面 plane 内侧 in\n- $((Q_1-P) \\cdot \\vec n )* ((Q_2-P) \\cdot \\vec n) < 0$，直线 $Q_1Q_2$ 与平面 plane 有交点\n\n![](./images/line_plan.jpg)\n\n\n\n## 3. AABB、圆\n\n检测物体中心距离：找到 AABB 上距离圆最近的一个点，如果圆到这一点的距离小于圆的半径，那么就产生了碰撞\n\n![](./images/collisions_aabb_circle.png)\n\n```c++\nbool isCollision(BallObject &circle, AABB &box)\n{\n    // 圆的中心坐标\n    vec2 circleCenter(circle.Position + circle.Radius);\n    vec2 boxSizeHalf((box.Max - box.Min) * 0.5f);\n    vec2 boxCenter(box.Min + boxSizeHalf);\n    // 获取两个中心的差矢量\n    vec2 delta = circleCenter - boxCenter;\n    vec2 clamped = clamp(delta, -boxSizeHalf, boxSizeHalf);\n    // 得到了碰撞箱上距离圆最近的点closest\n    vec2 p = boxCenter + clamped;\n    // 获得圆心 C 和最近点 P 的矢量\n    delta = p - circleCenter;\n  \t// 判断 p 是否在圆内\n    return length(delta) < circle.Radius;\n}\n```\n\n\n\n## 4. AABB 与 AABB\n\n检测物体中心距离：两个物体的 AABB 碰撞箱的中心距离分别与两个 AABB 碰撞箱的**长总和**与**宽总和**比较\n\n```c\nbool isOverlap(AABB &box1, AABB &box2)\n{\n    vec3 box1Size = box1.Max - box1.Min;\n    vec3 box2Size = box2.Max - box2.Min;\n    vec3 box1Center = box1Size * 0.5f + box1.Min;\n    vec3 box2Center = box2Size * 0.5f + box2.Min;\n    vec3 boxCenterDistance = abs(box2Center - box1Center);\n    vec3 boxSizeSumHalf = (box1Size +　box2Size) * 0.5f;\n\t\n    return (\n        boxCenterDistance.x <= boxSizeSumHalf.x && \n　　　　　boxCenterDistance.y <= boxSizeSumHalf.y &&\n　　　　　boxCenterDistance.z <= boxSizeSumHalf.z\n    );\n}\n```\n\n\n\n## 5. 线段与线段\n\n![](./images/edge.png)\n\n分层测检测：\n\n1. 将线段套上 AABB 包围盒，做快速排斥检测\n\n   ```c\n   bool isOverlapRect(Edge& e1, Edge& e2)\n   {\n       return (\n           // X 轴投影重叠\n           min(e1.start.x, e1.end.x) <= max(e2.start.x, e2.end.x) &&\n           max(e1.start.x, e1.end.x) >= min(e2.start.x, e2.end.x) &&\n           // Y 轴投影重叠\n           min(e1.start.y, e1.end.y) <= max(e2.start.y, e2.end.y) &&\n           max(e1.start.y, e1.end.y) >= min(e2.start.y, e2.end.y)\n       );\n   }\n   ```\n\n2. 跨立实验\n\n   根据两个向量 $P_1 Q_1、P_1Q_2$的与同一个向量 $P_1P_2$ 叉乘结果相反，来判断两个向量是否在那同一个向量的两侧\n\n   ![](./images/lineSegmentOverlap.png)\n\n   ```c\n   bool isLineSegmentOverlap(Edge& e1, Edge& e2)\n   {\n       // 1. 判断点 e2.start 和 e2.end 是否在 线 e1 两侧\n       vec2 e2Start = e2.start - e1.start;\n       vec2 e2End = e2.end - e1.start;\n       vec2 start2End = e1.end - e1.start;\n       long cross1 = cross(e2Start, start2End);\n       long cross2 = cross(e2End, start2End);\n       if ( !(0 == cross1 && 0 == cross2) && (cross1 & cross2 >= 0) ) return false;\n       \n       // 2. 判断点 e1.start 和 e1.end 是否在 线 e2 两侧\n       e2Start = e1.start - e2.start;\n       e2End = e1.end - e2.start;\n       start2End = e2.end - e2.start;\n       cross1 = cross(e2Start, start2End);\n       cross2 = cross(e2End, start2End);\n       if ( !(0 == cross1 && 0 == cross2) && (cross1 & cross2 >= 0) ) return false;\n       \n       return true;\n   }\n   ```\n\n3. 交点计算（额外操作）\n   前提：一定有交点\n   设：射线段 $P_1 P_2、Q_1Q_2$ 相交与点 $A$，则\n\n   ![](./images/lineSegmentOverlap2.png)\n   $$\n   \\begin{align}\n   |\\vec {Q_1Q_2} \\cross \\vec {Q_1P_1}| &= |\\vec {Q_1Q_2}|d_1 \\\\\n   d_1 &= {|\\vec {Q_1Q_2} \\cross \\vec {Q_1P_1}| \\over |\\vec {Q_1Q_2}|} \\\\\n   d_2 &= {|\\vec {Q_1Q_2} \\cross \\vec {P_2Q_2}| \\over |\\vec {Q_1Q_2}|} \\\\\n   {d_1 \\over d_2} &= {|\\vec {P_1A}| \\over |\\vec {P_1P_2}| - |\\vec {P_1A}|}  = {t \\over 1- t}\\\\\n   t &= {d_1 \\over d_1 + d_2} = {|\\vec {Q_1Q_2} \\cross \\vec {Q_1P_1}| \\over |\\vec {Q_1Q_2} \\cross \\vec {Q_1P_1}| + |\\vec {Q_1Q_2} \\cross \\vec {P_2Q_2}|}\\\\\n   \\vec {P_1A} &= t\\vec {P_1P_2}\n   \\end{align}\n   $$\n\n\n\n\n\n# 五、区域划分\n\n根据划分条件的不同，可以采用不同的划分结构\n\n![](./images/spatial_partition.png)\n\n## 1. 划分条件\n\n### 1.1 传统划分\n\n### 1.2 空间划分\n\n\n\n\n\n## 2. 划分结构\n\n### 2.1 BVH 划分\n\n划分物体\n![](./images/spatial_partition3.png)\n\n### 2.2 K-D 树划分\n\n### 2.3 Uniform Grids 划分\n\n![](./images/spatial_partition2.png)\n\n\n\n\n\n# 引用\n\n1. [Distance Between Point and Triangle in 3D](https://www.geometrictools.com/Documentation/DistancePoint3Triangle3.pdf)\n2. [Building a Collision Engine Part 1: 2D GJK Collision Detection](https://blog.hamaluik.ca/posts/building-a-collision-engine-part-1-2d-gjk-collision-detection/)\n3. [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)\n4. [CLIPPING by Kenneth I. Joy](https://link.zhihu.com/?target=https%3A//fabiensanglard.net/polygon_codec/clippingdocument/Clipping.pdf)\n5. [Clipping implementation](https://link.zhihu.com/?target=https%3A//fabiensanglard.net/polygon_codec/)\n6. [GJK 检测算法](https://www.cnblogs.com/alps/p/12822653.html)\n7. [【计算几何】线段相交](https://www.cnblogs.com/dwdxdy/p/3230485.html)\n8. [点到 AABB，点到 OBB 的最近点和距离](http://www.idivecat.com/archives/494)\n9. [从零开始手敲次世代游戏引擎（四十五）](https://zhuanlan.zhihu.com/p/34344829?from_voters_page=true)\n\n"
  },
  {
    "path": "LinearAlgebra/README.md",
    "content": "# [Linear Algebra](https://space.bilibili.com/88461692#/channel/detail?cid=9450) Notes\n\n1. This notes written in [Typora](https://www.typora.io/). It is also recommended to use it to read.\n2. The structure of the notes is slightly different from that of the video.\n3. See [*GMath*](https://github.com/CatOnly/GMath) for the specific code implementation of Linear algebra, Euler angle and Quaternion.\n4. See [DrawEasy3D](https://github.com/CatOnly/DrawEasy3D) for the specific code implementation of [*GMath*](https://github.com/CatOnly/GMath).\n4. Thanks for [3Blue1Brown](https://space.bilibili.com/88461692#/)'s video.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n⚓️ Keep Reading , Keep Writing , Keep Coding.\n"
  },
  {
    "path": "README.md",
    "content": "#  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## Theory\n\n1. [Linear Algebra](https://github.com/CatOnly/CrashNote/tree/master/LinearAlgebra)\n2. [ComputerGraphics (OpenGL)](https://github.com/CatOnly/CrashNote/tree/master/ComputerGraphics(OpenGL))\n3. [Digital Image Processing](https://github.com/CatOnly/CrashNote/tree/master/DigitalImageProcessing)\n\n\n\n## Practice\n\n1. [Unreal Engine 4 Note](https://github.com/CatOnly/CrashNote/tree/master/UnrealEngine4)\n\n\n\n## Project\n\n1. [GMath](https://github.com/CatOnly/GMath): Practice of Linear algebra, Euler angle and Quaternion\n2. [ErosViewer](https://github.com/CatOnly/ErosViewer): Practice of *GMath*、OpenGL and Digital image processing\n\n\n\n## Principle\n\n1. <u>Articles need to be **maintained** as code</u>\n2. <u>Keep the process of writing articles **Simple** and **Reliable**</u>\n1. <u>Figure out the primary workflow</u>\n   You do not have to fully mastered the technology when your work need it.\n   Making an outline first and then filling in details.\n2. <u>Work out a practical scheme</u>\n\n\n\n\n\n\n\n\n\n\n\n\n\n⚓️ Keep Reading , Keep Writing , Keep Coding.\n"
  },
  {
    "path": "UnrealEngine4/Part0_Base.md",
    "content": "# 一、UE4 的开发流程\n\n> 这里以 Win 平台的开发流程为例子，其他的平台在 **UE 4.21.0** 源码的 readme 和 [UE 开发者文档](https://docs.unrealengine.com/4.26/zh-CN/ProgrammingAndScripting/ProgrammingWithCPP/CPPTutorials/FirstPersonShooter/1/) 里都有说明\n\n## 1. 环境配置\n\n1. **软件配置**\n   安装 Visual Studio 2017（或者用 [Rider for Unreal Engine](https://www.jetbrains.com/lp/rider-unreal/)，虽然付费但有[破解方式](https://iworkh.gitee.io/blog/2020/08/15/jetbrains-crack/)），Git\n   由于 win 对于文件路径限制在 255 字符以内，建议将 UE 的源码直接放在**磁盘根目录下**\n   \n2. **平台依赖软件包准备**\n   执行 `Setup.bat` 脚本，通过 Git 下载当前平台依赖的额外资源，它们存放在 UE 源码的 `FeaturePacks` 文件夹\n   \n   ```sh\n   # Setup.bat 加速\n   # 运行 setup.bat -help 可以看到参数说明\n   # 在 set PROMPT_ARGUMENT=--prompt 这里使用多线程来加速\n   # --exclude=排除不需要下载的库，Win 平台下使用时不要排除 Win32、VS2012、S2013，不然后期会出问题\n   # --cache=E:\\UE4.27.1\\SetupCaches 防止重复下载\n   set PROMPT_ARGUMENT=--prompt \n   --threads=20\n   --exclude=Linux --exclude=HTML5 --exclude=IOS --exclude=Android\n   --exclude=osx32 --exclude=osx64\n   ```\n   \n3. **构建项目**\n   执行 `GenerateProjectFiles.bat` 脚本，它会调用 `Engine/Build/BatchFiles` 下的 bat 脚本文件\n   里面的脚本会自动调用 Unreal Build Tool 项目，生成 `Unreal Build Tool.exe` \n   通过另外的一个脚本调用 `Unreal Build Tool.exe` 传入一些参数创建解决方案  `UE4.sln`\n   \n4. **生成 UE 各种游戏开发软件**\n   打开 `UE4.sln` 选择一种编译模式（如： Development Editor），选中解决方案 UE4，**生成 Build一下**\n   在文件夹 `Engine/Binaries/Win64` 找到对应编译平台生成的 UE4Editor.exe 等软件\n   \n5. **UnrealVersionSelector**\n   大部分用 UE Edtior 创建的不含有 C++ 文件的项目是不需要通过 Visual Studio 打开的，一般都需要用 UnrealVersionSelector 这个程序\n   这个程序诞生于对 UE 解决方案的编译（Win 使用时需要先双击一下注册程序到 Win 的注册表）\n   主要用来控制 UE Edtior 生成的 `.uproject` 文件（Win 选中后右键）在打开时使用哪个 UEEdtior 的版本（不同 UE 版本兼容性不太好）\n\n\n\n## 2. 跨平台项目构建 Unreal Build Tool\n\nUnreal Build Tool 是 UE 自己的跨平台构建工具，它代替了传统的 makefile 或 MS build\n本质上是个命令行程序，通过运行脚本 `GenerateProjectFiles[.sh/.bat/.command]` 来执行：\n生成工程文件、**解析所有依赖模块**、执行 Unreal Header Tool、为各种不同的平台个构建风格调用编译器（Compiler）和连接器（Linker）等功能\n\n### 2.1 模块 Modules\n\n**模块的分类**\n\n- C# 模块\n  使用 `.csproj`（Visual Studio C#工程描述文件）作为它的工程文件\n- C++ 模块\n  使用 `模块名.build.cs` 文件来定义（实际上是一个 C# 文件），这个文件跟 `vcxproj`（Visual Studio C++工程描述文件）类似\n  模块项目里 Public 目录下是对外暴露的接口，Private 则是内部使用的接口\n\n\n\n**模块的创建流程**\n模块的创建多用于 GamePlay 项目 Runtime 阶段，而非引擎内部或编辑器中\n\n1. 创建 Private、Public 文件夹\n\n2. 创建 模块名.build.cs、 模块名.h、 模块名.cpp 文件\n\n   ```c++\n   // 目录结构\n   /**\n   /模块名\n    |_ 模块名.build.cs\n    |_ /Public\n    |       |_ 模块名.h\n    |       |_ [模块名PrivatePCH.h] // 可以将模块内通用的头文件发在这个头文件中来加快编译\n    |_ /Private\n            |_ 模块名.cpp\n   */\n   \n   // 模块名.build.cs: ModuleDev.build.cs\n   using UnrealBuildTool;\n   \n   // 类的名称与模块名称一致\n   public class ModuleDev : ModuleRules\n   {\n       public ModuleDev(ReadOnlyTargetRules Target) : base(Target)\n       {\n           PublicDependencyModuleNames.AddRange(new string[] { \"Core\", \"CoreUOject\" });\n           PrivateDependencyModuleNames.AddRange(new string[] { });\n       }\n   }\n   \n   // 模块名.h: ModuleDev.h\n   #pragma once\n   #include \"ModuleDevPrivatePCH.h\" // 如果有这个文件\n   \n   class FModuleDevModule : public IModuleInterface\n   {\n   public:\n   \t/** IModuleInterface implementation */\n   \tvirtual void StartupModule() override;\n   \tvirtual void ShutdownModule() override;\n   };\n   \n   // 模块名.cpp: ModuleDev.cpp\n   #include \"ModuleDev.h\"\n   IMPLEMENT_MODULE(FModuleDevModule, ModuleDev);\n   // or 至少有一个 primary 模块，其他模块可用 IMPLEMENT_GAME_MODULE 注册\n   // IMPLEMENT_PRIMARY_GAME_MODULE(YourModuleNameClass, YourModuleName);\n   ```\n\n3. 引入模块\n\n   Editor 类型模块：在 UEEditor 顶栏的 Edit/Plugins 中根据模块名称添加对应模块\n   Runtime 类型模块：将模块代码放入 UE 工程文件的 Source 文件夹里，找到 `工程名.Target.cs` 文件\n\n   ```c\n   using UnrealBuildTool;\n   using System.Collections.Generic;\n   \n   public class UE4GameTarget : TargetRules\n   {\n   \tpublic UE4GameTarget(TargetInfo Target) : base(Target)\n   \t{\n   \t\tType = TargetType.Editor;\n   \n   \t\tExtraModuleNames.Add(\"UE4Game\");\n           ExtraModuleNames.AddRange(new string[] { \"ModuleDev\" });\n   \t}\n   }\n   ```\n\n4. 打开 `项目名.uproject` 文件\n\n   ```c\n   // 具体配置参数见 Engine\\Source\\Runtime\\Projects\\Public\\ModuleDescriptor.h\n   // PostConfigInit：引擎初始化阶段，在配置系统初始化完成后 PreLoadingScreen：引擎初始化阶段，可以在这里挂入 LoadingScreen 的注册\n   // PreDefault：引擎初始化阶段，在 Default 阶段之前\n   // Default：引擎初始化阶段，此时所有的游戏模块加载已经完成\n   // PostDefault：引擎初始化阶段，在Default阶段之后\n   // PostEngineInit：引擎初始化完成后\n   // None：不会自动加载\n   \"Modules\": [\n       {\n           \"Name\": \"YourProject\",\n           \"Type\": \"Runtime\",\n           \"LoadingPhase\": \"Default\"\n       },\n       {\n           \"Name\": \"YourModule\",\n           \"Type\": \"Runtime\",\n           \"LoadingPhase\": \"Default\"\n       },\n       {\n           \"Name\": \"YourModuleEdit\",\n           \"Type\": \"Editor\",\n           \"LoadingPhase\": \"PreDefault\" // 注意 Editor 类型的这里\n       }\n   ]\n   ```\n   \n5. 重新生成项目\n   为了防止文件冲突， 删除掉 `Engine/Binaries` 和 `Engine/Intermediate` 文件夹之后\n   再点击 Uproject 右键 Generate\n\n\n\n### 2.2 插件 Plugins\n\n位置：`Engine/Plugins`，和 Source 在同一个文件夹\n\n目录结构：\n\n```c\n/**\n/插件名\n |_ /Resources\n |    |_ Icon128.png // 在 UEEditor 里的 Icon\n |_ /Source\n |    |_ 插件名\n |         |_ /Public\n |         |     |_ 插件名.h\n |         |     |_ [插件名PrivatePCH.h] // 可以将模块内通用的头文件发在这个头文件中来加快编译\n |         |_ /Private\n |               |_ 插件名.cpp\n |_ 插件名.uplugin\n*/\n```\n\n插件的创建多用于对 UEEditor 和引擎的扩展，创建流程前三步和创建模块一样（游戏项目的插件可以在 UEEditor 内部创建）\n\n4. 设置依赖模块\n   修改 `插件名.uplugin`\n\n   ```c\n   {\n       \"FileVersion\": 3,\n       \"Version\": 1,\n       \"VersionName\": \"1.0\",\n       \"FriendlyName\": \"插件名\",\n       \"Description\": \"插件描述\",\n       \"Category\": \"Other\",\n       \"CreatedBy\": \"\",\n       \"CreatedByURL\": \"\",\n       \"DocsURL\": \"\",\n       \"MarketplaceURL\": \"\",\n       \"SupportURL\": \"\",\n       \"Modules\": [ // 在这里引入插件模块\n           {\n               \"Name\": \"插件模块名称\",\n               \"Type\": \"Editor\",                 \n               \"LoadingPhase\" : \"PostEngineInit\"  // 插件模块加载时机\n           }\n       ],\n       \"EnabledByDefault\": true,\n       \"CanContainContent\": true,\n       \"IsBetaVersion\": false,\n       \"Installed\": false\n   }\n   ```\n\n5. 重新生成项目\n   同模块的重新生成项目方法一致\n\n\n\n模块和插件的加载流程：虽然插件和模块的加载时机可以在 uproject 或者 uplugin 文件配置在读取 .ini 文件前后加载，但总体的流程还是不变的\n\n1. 加载 Platform File Module\n2. 加载 CoreUObject\n3. 加载 Render ... 等\n4. 加载 Core\n5. 加载 Networking\n6. 加载 运行平台相关模块\n7. 根据 Plugin 的启用状态加载 Plugin 模块\n\n\n\n### 2.3 构建配置\n\n状态 Status：\n\n![](./images/status.png)\n\n目标 Targets：\n\n![](./images/targets.png)\n\n\n\n### 2.4 游戏项目目录结构\n\n不管是 **引擎工程** 还是 **游戏工程** 都含有以下目录结构\n\n- **Binaries：**\n  存放编译生成的结果二进制文件\n  \n- **Config：**\n  配置文件 `.ini`\n  \n- **Content：**\n  平常最常用到，所有的资源和蓝图等都放在该目录里\n  \n- **DerivedDataCache（DDC）：**\n  存储着引擎**针对平台特化后的资源**版本\n  比如同一个图片，针对不同的平台有不同的适合格式，这个时候就可以在不改变原始的 uasset 的基础上，比较轻易的再生成不同格式资源版本\n  \n- **Intermediate：**\n  中间文件，存放着一些临时生成的文件，有：\n  - Build 的中间文件，.obj 和 预编译头 等\n  - UHT 预处理生成的 .generated.h/.cpp 文件\n  - VS .vcxproj 项目文件，可通过 .uproject 文件生成编译生成的 Shader 文件\n  - AssetRegistryCache\n    Asset Registry 系统的缓存文件，Asset Registry 可以简单理解为一个索引了所有 uasset 资源头信息的注册表。CachedAssetRegistry.bin 文件也是如此\n  \n- **Saved：**\n  存储自动保存文件，其他配置文件，日志文件，引擎崩溃日志，硬件信息，烘培信息数据等\n  \n- **Source：**\n  源代码文件\n\n\n\n## 3. 普通用户的开发流程\n\n1. 配置项目\n   配置文件可用于为加载项目时将初始化的属性设置值\n\n   ```c++\n   // 1. 代码里设置 配置类的 配置属性\n   UCLASS(Config=Game)\n   class AExampleClass : public AActor\n   {\n       GENERATED_UCLASS_BODY()\n   \n       UPROPERTY(Config)\n       float ExampleVariable; // 配置属性，可以被子类继承后继续使用\n   };\n   \n   // 2. 在 Engine/Config/*.ini 文件设置配置类的初始值\n   [/Script/ModuleName.ExampleClass]\n   ExampleVariable=0.0f\n   \n   // 3. 系统将所有特定于项目和特定于平台的差异保存到\n   [ProjectDirectory]/Saved/Config/[Platform]/[Category].ini\n   ```\n\n2. **创建、开发项目，使用 UE4Editor**\n   如果 UE4Editor 是源码编译的项目，会打开 Visual Studio，创建的游戏项目在 Games 文件夹下\n   生成并运行 Visual Studio 的游戏项目，UE4Editor 会重新运行，此时 UE4Editor 正编辑着刚创建的 UE4 项目\n\n3. 编辑**项目中**的 C++ 代码，使用 Visual Studio\n\n4. **编译、运行项目，使用 UE4Editor**\n   会先调用 Unreal Header Tool 命令行程序，在调用相关的平台编译器\n\n5. 调试项目中的 C++ 代码，使用 Visual Studio 打断点查看\n   UE4 Editor console 命令行函数调试（游戏运行时按 `~` 出现的 console），方便调试游戏复现场景\n\n   ```c++\n   // 命令行里使用：DemoTest 调用 Trigger Function\n   static FAutoConsoleCommand CVarStaticCommand(\n       TEXT(\"DemoTest\"), \t// The name of this CMD\n       TEXT(\"Helper text for this CMD\"),\n       FConsoleCommandWithArgsDelegate::CreateStatic(\n           [](const TArray<FString>& Platforms)\n           {\n               UE_LOG(LogTemp, Warning, TEXT(\"Trigger Function\"));\n           }\n       )\n   );\n   \n   // 从简单变量衍生而来，只用于 int,string,bool,在 TextureStreamingHelpers.cpp\n   TAutoConsoleVariable<int32> CVarSetTextureStreaming(\n   \tTEXT(\"r.TextureStreaming\"),\n   \t1,\n   \tTEXT(\"Allows to define if texture streaming is enabled, can be changed at run time.\\n\")\n   \tTEXT(\"0: off\\n\")\n   \tTEXT(\"1: on (default)\"),\n   \tECVF_Default | ECVF_RenderThreadSafe\n   );\n   ```\n   \n6. 调试项目，使用 UE4Editor 查看项目中 log 信息\n   在 UE4Editor 的 输出日志窗口、消息日志窗口 查看 log 信息\n   在 UE4Editor 的 运行游戏画面窗口，查看 `UEngine::AddOnScreenDebugMessage` 的 log 信息\n\n7. **测试项目**\n   在模块文件夹下的 `Private/Tests` 文件夹中新建 `模块名Test.cpp` 文件\n\n   ```c\n   #include \"模块名PrivatePCH.h\"\n   #include \"Misc/AutomationTest.h\"\n   \n   DEFINE_LOG_CATEGORY_STATIC(TestLog, Log, All)\n   IMPLEMENT_SIMPLE_AUTOMATION_TEST(FMultiThreadTest,\n                                    \"TestGroup.TestSubgroup.MultiThreadTest\", \n                                    EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter\n   )\n   \n   bool FMultiThreadTest::RunTest(const FString& Parameters)\n   {\n       UE_LOG(TestLog, Log, TEXT(\"Hello\"));\n       \n       return true; // 测试通过\n   }\n   ```\n\n   重新生成 UProject，在重新编译 UEEditor，来执行[自动测试](https://docs.unrealengine.com/4.27/zh-CN/TestingAndOptimization/Automation/TechnicalGuide/)\n   在 `Window/Developer Tools/Session Frontend` 里的 `Automation` 标签页里找到 **MultiThreadTest** 勾选后点击 `Start Tests` \n   然后通过 `Session Frontend` 里的 `Console`  标签页来查看 Log 信息\n\n8. **性能分析**\n   使用 RenderDoc 抓取 GPU 绘制信息 [RenderDoc | 虚幻引擎文档 (unrealengine.com)](https://docs.unrealengine.com/4.27/zh-CN/TestingAndOptimization/PerformanceAndProfiling/RenderDoc/)，[Render Doc 使用说明](https://zhuanlan.zhihu.com/p/80704313)\n\n9. **转化 / 烘焙（Cook）项目**，使用 UE4Editor 的虚幻自动化工具（UAT，Unreal Automation Tool）\n   将引擎内部使用的特定格式存储内容资源（如用 PNG 存储纹理）转换成打包平台下更节省内存或者性能更好的格式\n   **调试 Cook** `.uproject -run=Cook  -TargetPlatform=WindowsNoEditor`，只能在 Cook 时调试，不能运行游戏\n   **查看 Cook 后的资源，并在 VS 里使用 `-game` 命令才是 UE 真正 Runtime 的运行逻辑**\n   \n10. **打包项目**，使用 UE4Editor \n      将项目打包成平台原生的分发格式\n\n11. 打补丁，使用 UE4Editor\n      在最初的发布之后对其进行更新\n      方法一：保留原始版本或之前版本中的文件，但添加一个指向新内容的指针\n      方法二：使用二进制补丁转换原始版本中的内容\n\n\n\n## 4. 扩展\n\n### 4.1 游戏项目开发流程\n\n**开发流程**\n\n1. <u>策划设计</u>（策划提需求）\n   策划：制作人、主策、数值、关卡、系统、剧情、文案\n2. <u>主程分析和分配需求</u>（客户端、TA 是基于引擎软件在上层开发）\n   程序：主程、引擎、服务器、客户端、工具、TA（工具和服务，**可外包**）\n   美术：主美、原画、2D UI、角色、动作、场景、特效（原画、模型、动作，**可外包**）\n3. <u>开发联调，方案审核</u>\n4. <u>策划配置游戏数据和程序联调</u>\n5. <u>策划自测游戏</u>\n   逻辑测试\n6. <u>策划通知程序合并分支</u>\n   音频：作曲、音效、录音（**可外包**全部）\n7. <u>QA 测试</u>\n   测试开发\n8. <u>版本发布</u>\n   运营：市场、渠道、客服\n\n\n\n**项目管理工具**\n\n- 项目流程管理：JIRA\n- 项目知识库：Confluence（防止不同的人在同一个问题上重复踩坑）\n- 自动打包发布：Jenkins\n\n\n\n### 4.2 配置文件介绍\n\n\n\n\n\n\n\n# 二、UE4 的编译\n\n> UE 相应的构建配置现在都通过名为 Engine/UE4 的 `vcxproj` 项目来编译\n\n\n\n## 1. C++ 的反射\n\n### 1.1 功能\n\n反射在 Java 和 C# 等语言中比较常见，概括的说，反射数据描述了类在运行时的内容\n\n1. 基本功能\n   可以在**运行时**通过类名称（字符串）创建对象，通过字符串名称来获取类中申明的成员变量和方法并调用\n2. 延申功能\n   由于可以在运行时获得一个类或者对象的状态，反射功能方便了代码在运行时的调试和测试\n   方便了脚本语言（动态语言）对 C++ （静态语言）的运行时调用和修改，方便[热更新](https://baike.baidu.com/item/%E7%83%AD%E6%9B%B4%E6%96%B0/20842716)的实现\n   例：UE4 的蓝图功能通过 UI 操作配合 C++ 的反射，在运行时动态直观的增加游戏逻辑\n3. 可选功能\n   方便类能直观的在磁盘上存（序列化）取（反序列化）内容，进一步方便调试\n   例：UE4 依靠对象序列化为字符串，方便进行网络变量复制功能（这样执行效率不是最高，序列化可以序列化为二进制，这是综合考虑开发效率和程序执行效率的较好结果）\n\n\n\n### 1.2 实现方式\n\n可以通过不同的编译器关键字来实现反射，在不同平台不同编译器都有不一样的实现，这样代价最小却失去了跨平台的一致性\n编写跨平台一致性的反射功能还需要不依赖编译器特性，只通过编码实现反射功能，这就需要有：\n\n1. **动态类**：所有具有反射功能的类\n   存储 类属性名 和 类属性值 的 Map 集合，例：函数名 和 函数调用地址\n2. **虚表**：使动态类对象具有多态功能\n   存储 子类对象的虚函数调用表，方便实现多态\n3. **类工厂**：全局的动态类注册机，生存类对象的\n   存储 动态类名称 和 动态类构造函数 的 Map 集合\n   每个动态类在编辑完成后都要到类工厂注册一下，这样才能通过类工厂找到所有支持反射的动态类\n\n\n\n### 1.3 反射类的自动注册\n\n手动注册方式\n\n- 手动集中添加类，容易出错\n- 集中添加类，独立性差，改动类时，需要额外改动集中 include 的类\n- 注册顺序在代码里明确规定，确定可控\n\n```c++\n#include \"ClassReflectA.h\"\n#include \"ClassReflectB.h\"\nint main()\n{\n    ClassFactory::Get().Register<ClassReflectA>();\n    ClassFactory::Get().Register<ClassReflectB>();\n    [...]\n}\n```\n\n\n\n自动注册方式\n\n- 依靠共享的类来注册不同的类\n- 独立性强，只需要在单独的类里修改\n- 注册顺序取决于编译器，不同的编译器注册顺序不同\n- 最好在动态链接的时候使用\n  静态链接如果没有其他类引用，会绕过 static 的初始化，导致后续有人用的时候找不到该类\n  若想在静态链接使用需要相应的项目编译配置\n\n\n```c++\n//StaticAutoRegister.h\ntemplate<typename TClass>\nstruct StaticAutoRegister\n{\n    StaticAutoRegister()\n    {\n    \tRegister(TClass::StaticClass());\n    }\n};\n\n//MyClass.h\nclass MyClass\n{\n\t//[...]\n};\n//MyClass.cpp\n#include \"StaticAutoRegister.h\"\nconst static StaticAutoRegister<MyClass> AutoRegister;\n```\n\n\n\n## 2. 项目配置\n\n每次项目配置文件都会存储**前一次运行时**各个对象的参数数据，便于我们查看和调试\n\n```c++\n// 一、目录结构：配置文件 .ini 存放的位置\nEngine/Config\t\t\t// 引擎项目目录\nEngine/Saved/Config\t\t// 引擎运行后生成 Saved\n[ProjectName]/Config\t\t// 游戏项目目录\n[ProjectName]/Saved/Config\t// 游戏项目运行后生成 Saved\n\n// 二、类似于 BaseEngine.ini 的文件格式\n[配置标题 Section1]\nkey1 = value1\nkey2 = value2\n\n[配置标题 URL]\nkey1 = value1\n\n// 三、存储对象属性的类声明格式\nUCLASS(Config=SectionName)\nclass FiDemoClass \n{\n    GENERATED_BODY()\nprivate:\n    // 在要保存的属性声明前添加\n    UPROPERTY(Config)\n\tfloat iValue; // 要保存的属性\n    \n    \n}\n\n// 四、配置标题 Section 的种类\n// 1. 系统定义类型\nCompat\t\t\t\t（兼容性）\nDeviceProfiles\t\t（设备概述文件）\nEditor\t\t\t\t（编辑器）\nEditorGameAgnostic\t（编辑器游戏未知的配置信息）\nEditorKeyBindings\t（编辑器按键绑定）\nEditorUserSettings\t（编辑器用户设置）\nEngine\t（引擎）\nGame\t（游戏）\nInput\t（输入）\nLightmass\t（灯光构建相关）\nScalability\t（可扩展性）\nEditorLayout（编辑器布局）\nSourceControlSettings\t（源码控制设置，只存在于引擎和工程的Save目录）\nTemplateDefs\t\t\t（模板定义，只存在于引擎和工程的Save目录）\n    \n// 2. 自定义类型\n// 已知：类名称 FiDemoClass, 类文件所在 Source 中的工程名 ProjModuleName\n// 则，它的 Section 如下\n[/Script/ProjModuleName.iDemoClass]\niValue = 1.0f\n```\n\n\n\n## 3. 预编译 UBT，UHT\n\nUnreal Build Tool（UBT，C#）读取每个模块的 Target.cs、Build.cs，处理依赖关系，编译每个模块\nUnreal Header Tool（UHT，C++）一个分析源码标记并生成代码的工具 ，在 UE Editor 里的编译是调用 UHT\n为了更好的服务于 C++ 的反射功能，具备\n\n1. 只在类代码里添加一些宏标记，不破坏原来的类声明结构\n\n2. 在预编译阶段自己解析宏定义\n\n3. 预编译阶段得到与编译器类似的信息，便于调试\n   \n   \n\nUE 具有反射功能的类定义示例，具体使用方式是见 [Unreal 官方文档 | 游戏性架构 / 属性](https://docs.unrealengine.com/4.27/zh-CN/ProgrammingAndScripting/GameplayArchitecture/Properties/)\n```c++\nUCLASS()\nclass HELLO_API UMyClass : public UObject\n{\n\tGENERATED_BODY()\npublic:\n    // 蓝图属性\n\tUPROPERTY(BlueprintReadWrite, Category = \"Test\")\n\tfloat Score;\n\n    // 蓝图方法\n\tUFUNCTION(BlueprintCallable, Category = \"Test\")\n\tvoid CallableFuncTest();\n};\n```\n\n\n\n## 4. UE4 总体编译流程\n\n1. Unreal Build Tool 根据编译脚本为了编译各个已经预编译好的模块\n   调用 Unreal Header Tool 执行 UE 的预编译，生成反射所需要的文件 *.generated.h \n2. 调用平台特定的编译工具(VisualStudio, LLVM) 来编译各个模块\n3. Unreal Build Tool 根据编译脚本链接各个模块，最终生成可执行文件\n4. 引擎代码读取 `Engine/Config/*.ini` 下的配置类的值准备给配置类做初始化\n\n\n\n\n\n# 三、UE4 的内存管理\n\n## 1. 基础类继承树\n\n**类命名前缀**\n虚幻编辑器有一些命名规则，当类名不符合命名规则时，将触发警告或错误\n\n- 继承 Actor 的类，使用 A 作为前缀，如，AController\n- 继承 Object 的类，使用 U 作为前缀，如，UComponent\n- 枚举类型 Enums，使用 E 作为前缀，如，EFortificationType\n- 接口类 Interface，使用 I 作为前缀，如，IAbilitySystemInterface\n- 模板类 Template，使用 T 作为前缀，如，TArray\n- 继承 SWidget 的类(Slate UI)，使用前缀 S，如，SButton\n- 除此之外的纯 C++ 命名都用 F 前缀，如，FVector\n  （以前F代表的意思是Float，当时引擎的计算都是浮点数，但后来数学计算扩展到整数，而且引擎的传播很迅速，所以来不及改成更好的前缀字母了）\n\n![](./images/class_struct.png)\n\n\n\n## 2. 内存分配\n\n![](./images/memory.png)\n\n通过在 <u>ModuleBoilerplate.h</u> 文件里全局重载运算符 `new/delete` UE4 将我们每次 `new/delete` 对象时调用的内存分配函数重载为自己的内存分配器\nUE4 支持多种内存分配器，具体包括：\n\n- Ansi 内存分配器（标准 C）\n  直接调用 malloc、free、realloc 函数\n\n- TBB（Thread Building Blocks）内存分配器\n  Intel 提供的第三方库的一个可伸缩内存分配器（Scalable Memory Allocator）\n\n- Jemalloc 内存分配器（Linux / FreeBSD）\n  适合多线程下的内存分配管理　http://www.canonware.com/jemalloc/\n\n- Stomp\n  用于查非法内存操作（如：内存越界，野指针）的管理方式，目前只支持 windows、mac、unix 等 pc 平台\n  带命令行参数 -stompmalloc 来启用该分配器\n\n- Mimalloc：https://github.com/microsoft/mimalloc\n\n- UE4 内置的内存分配器\n  1. Binned  （第一代箱式内存分配器）\n  2. Binned2（第二代箱式内存分配器）\n     FMallocBinned2 比 FMallocBinned 的分配方式会简单一些，会根据小块内存、对齐大小和是否开启线程缓存（默认开启）选择对应分配器和策略\n  3. Binned3（第三代箱式内存分配器，仅支持 64 bits）\n     实现方式 FMallocBinned2 类似，支持线程缓存\n\n\n\n## 3. 经典 GC 算法\n\nGC，GarbageCollection 垃圾回收，自动内存管理的一种形式\n\n### 3.1 引用计数 Reference Counter\n\n方法\n\n1. 每个对象都有一个引用计数值，记录被其他对象引用的次数\n2. 每次拷贝改对象时需要累加引用计数值后在将引用计数值也拷贝过去\n3. 引用计数为 0 时，该对象被回收\n\n优点\n\n- 渐进式的，它能及时释放无用的对象，将内存管理的的开销实时的分布在用户程序运行过程中\n\n缺点\n\n- 调整引用计数需要同事调整新对象和旧对象的引用计数值，由于<u>考虑到多线程修改引用计数的问题</u>\n  这增加了指针的复制成本，整体开销可能比标记-清楚方法大\n- 实际应用中很多对象的生命周期很短，频繁的分配和释放导致内存碎片化严重\n  **内存碎片**意味着可用内存在总数量上足够但由于不连续因而实际上不可用，同时增加内存分配的时间\n- 环形引用问题（可以通过弱指针来解决）\n  假如 A 引用 B，B 引用 C，C 引用 A，这样这个三个对象构成环形引用都无法被释放\n\n\n\n### 3.2 GC 复制 GC Copy\n\n方法\n\n1. 初始化\n   分两个堆管理内存对象，**源堆**存储被引用和新创建的对象，**目标堆**存储被释放的对象\n2. 垃圾回收\n   **源堆**有引用的对象都复制到**目标堆**\n   源堆和目标堆**互换身份**\n\n优点\n\n- 不会产生内存碎片\n- 时间复杂度比标记-清除的方法低，效率高\n\n缺点\n\n- 内存使用率低，需要两块相同大小的堆内存互相 copy 使用\n- 为了便于 copy，对象内存块的大小需要有相关性\n- 对于内存占用大，存活周期长的对象效率较低\n- 是**非实时**的，它要求在垃圾收集器运行时暂停用户程序运行，这对于实时和交互式系统的影响非常大\n\n\n\n### 3.3 标记 - 清除 Mark-Sweep\n\n方法\n\n1. **标记阶段**\n   遍历所有已分配的对象，设置默认为未标记状态\n   从根节点开始**深度遍历**其引用的子节点为标记状态\n   <u>标记前要检测当前节点是否被标记，如果被标记，不需要深度遍历该节点及其子节点（对象的引用结构是图结构，不是树结构，防止循环引用）</u>\n2. **清除阶段**\n   遍历所有已分配的对象，回收那些没有被标记的对象\n\n算法改进：**标记 - 压缩 Mark-Compress**\n\n1. **标记阶段**\n   同 标记-清除 方法的标记阶段一样\n2. **清除阶段**\n   改用 CG 复制方法，但不使用两个堆\n   而是在<u>同一个堆内做内存移动</u>，具有引用的对象都移动到堆的一端（避免内存碎片）\n\n优点\n\n- 操作指针没有额外的开销\n- 不需要考虑环形引用问题\n- 操作与用户程序完全分离\n\n缺点\n\n- 是**非实时**的，它要求在垃圾收集器运行时暂停用户程序运行，这对于实时和交互式系统的影响非常大\n- 通常在回收内存时会同时合并相邻空闲内存块，然而在系统运行一段时间后仍然难免会生成大量**内存碎片**\n\n\n\n### 3.4 分代 GC Generational GC\n\n> 分代 GC 被 JVM(JDK 11之前)，.Net Framework，V8 ，Lua(5.4）等主流语言和框架所使用\n\n方法\n\n1. 堆中的对象不仅有引用标记，还有**年龄属性**，记录存活时间（GC 次数）\n2. 根据不同的年龄，对象具有不同的 CG 频率和 CG 算法\n\n优点\n\n- **单次年轻代的 GC 下大多数情况 GC 时间比 标记-清除 时间短**\n- 操作指针没有额外的开销\n- 不需要考虑环形引用问题\n- 操作与用户程序完全分离\n\n缺点\n\n- 在强制 GC 或者同时发生新旧代 GC 时，分代 GC 效率较 标记 - 清除 低\n- 是**非实时**的，它要求在垃圾收集器运行时暂停用户程序运行，这对于实时和交互式系统的影响非常大\n\n\n\n### 3.5 增量 GC Incremental GC\n\n方法\n**一次 GC 时间太长，将这一次 GC 分散到多次执行中去**\n\n1. 把对象按照 GC 的步骤分为三种颜色\n   白色：还未搜索过的对象（需要被回收的对象）\n   灰色：搜索过程中的对象（标记当前 GC 执行的位置，当 GC 被打断时可以继续上次的位置）\n   黑色：已经完成搜索的对象（被引用的对象）\n2. GC 所有对象刚开始**默认白色**\n3. 从根节点开始**深度遍历**，并将子节点**标记为灰色**送入栈中\n4. 从栈中取出节点**深度遍历**，并将子节点标记为灰色\n   当灰色节点的所有子节点都为灰色后，它自己被涂成黑色（标记已经完成它的搜索）\n\n优点\n\n- **近似于实时的方法，每次执行时间可控**\n- 操作指针没有额外的开销\n- 不需要考虑环形引用问题\n- 操作与用户程序完全分离\n\n缺点\n\n- 一次 GC 虽然分为多次进行，但**总体耗时会变长**\n- 会有**内存碎片**产生\n\n\n\n## 4. UE4 GC 流程\n\nUE4 的 GC 通过追踪 UObject 极其子类的标记状态来实现，其 GC 方式是全量标记，增量清除的方法\n\n### 4.1 触发 GC 的时机\n\n每次 GC 都从 **Gameplay 线程（主线程）加锁**开始，让 Gameplay 以外的线程停止，等待 GC 的完成\n\n\n\n**每一帧最多调用一次 GC**\n所有 GC 调用前都会通过 `GFrameCounter` 来<u>防止同一帧里重复调用</u> GC\n\n**手动 GC**\n调用 `UEngine::ForceGarbageCollection(true)` 时\n\n**自动 GC**\n在 Game play 线程中 `UWorld::Tick(ELevelTick tickType, float delSeconds)`  的最后都会调用 `UEngine::ConditionalCollectionGarbage()` \n依次判断：\n\n1. 是压力测试并且在异步加载时会**尝试全量 GC**（最多 10 次）\n2. 配置文件中配置了每帧强制开启 GC 时会进行**全量 GC**\n3. 只要有一个 `UWorld` 对象已经 `Actor::BeginPlay()` 并且上次增量清除已经完成时（此时，如果遇到异步加载时，需要等待异步加载完毕后进行 GC）\n   如果是在服务器上并且有玩家连入，至少间隔 10 分钟进行**尝试全量 GC**（最多 10 次）\n   或者如果是本地游戏，至少间隔 1 分钟进行**尝试全量 GC**（最多 10 次）\n   注：间隔时间、尝试次数都可以在 UE4Editor 修改\n4. 如果以上条件都不满足，会执行**增量清除**\n\n\n\n### 4.2 全量标记 GC\n\n**标记类型**\n\n- 不会被回收的类型\n  `EInternalObjectFlags::GarbageCollectionKeepFlags ` UClass、非 GamePlay 线程的对象、正在异步加载的对象\n  `EInternalObjectFlags::RootSet`\n  有除了 `EObjectFlags::RF_NoFlags` 标志其他以外的任何标志\n- 垃圾回收的类型\n  `EInternalObjectFlags::Unreachable`\n  `EInternalObjectFlags::PendingKill `\n- 可能会被回收的类型\n  `EInternalObjectFlags::ClusterRoot`  簇\n  簇的所有子节点是否被回收，**取决于簇本身**是否有对象引用它\n\n\n\n**簇 Cluster**\n\n- 作用：减少标记遍历时间，加速 Cook 后对象的回收\n\n- 方法：一次标记一棵子引用关系图，Cluster 将一棵高度为 N 的树统一转换高度为 1（一个根节点指向这棵树的其他所有节点）\n\n- 类 **FUObjectCluster** 的结构\n\n  ```c++\n  // 创建：与 UObject 类似，所有的 FUObjectCluster 创建后都会存储在 GUObjectClusters 里\n  // 删除：与 UObject 类似，采用标记删除法，并没有真的删除，只是记录下其索引，共下次创建使用\n  // 结构：所有创建的簇都在一个类似双向链的结构里\n  int32 RootIndex; // 根节点的索引\n  TArray<int32> ReferencedClusters; \t// 双亲 Cluster 索引数组\n  TArray<int32> ReferencedByClusters; // 子 Cluster 索引数组\n  TArray<int32> Objects; \t\t  // Cluster 包含所有的 UObject 索引数组\n  TArray<int32> MutableObjects; // 那些正在变化过程中的 UObject 索引数组(异步加载）\n  bool bNeedsDissolving; \t\t  // 是否需要释放此 Cluster (如某个引用对象被 PendingKill 的情况)\n  ```\n\n- **可以作为 Cluster root 的类**\n  根据默认返回 false 的 `bool UObjectBaseUtility::CanBeClusterRoot()` 方法可以找到以下几个类能有一个 Cluster：\n  `UMaterial`、`UParticleSystem`、`UBlueprintGeneratedClass`(需要配置)、`ULevelActorContainer`(代替 `ULevel`)\n\n- FUObjectCluster 的**创建时机**\n  ULevel 的 Cluster 是要在 Actor 的 Components 都加载完成之后才会触发创建\n  其他类的 Cluster 在 自己被加载完后 `void EndLoad()` 里被创建\n\n\n\n**FGCReferenceTokenStream**\n\n- 作用：解析当前**类 UClass** 中对所需对象的引用\n- 结构：存储了 `TArray<uint32> Tokens`，父类的 Tokens 会排在子类之前（考虑到 C++ 对象内存分配顺序 虚表、父类成员变量、子类成员变量）\n  其中 `uint32` 其实是一个同尺寸的 `FGCReferenceInfo` 对象，它包含\n  1. 引用对象类型标志 \n  2. 引用对象在当前对象内存分布的 Offset\n     方便通过 Offset 找到对象地址 = 对象指针地址 + 父类 Offset + 当前引用对象的 Offset\n  3. 从当前对象到引用对象的嵌套层数\n     `A->B->C` ，C 是 A 的引用对象，其嵌套层数为 2\n- 生成方法：从顶层父类开始逐个遍历 `UPROPERTY` 标记的成员变量，然后到子类继续遍历\n\n\n\n**标记流程**\n\n1. 标记所有<u>可回收</u>对象为**不可达**\n   使用 UE4 的 `ParallelFor` 来多线程标记**所有对象**，收集必然可达的对象\n   其中如果簇没有被对象引用，簇所有 `UObject` 对象引用都会被回收\n2. **遍历对象引用网络**来标记对象是否可达\n   通过步骤 1 获取的必然可达对象，开始向后遍历对象引用的其他可达对象\n   获取对象 `UClass` 类当前的 `ReferenceTokenStream` 类的值作为标记来判断是否可达\n3. 删除没有被引用的簇（标记删除法）\n4. 删除 `FGCArrayPool` 中不可达的弱指针对象引用\n\n\n\n### 4.3 增量清除 GC\n\n增量清除默认限制时间在 0.002 秒内清除标记为不可达的对象\n增量清除全在 Gameplay 主线程里，流程如下\n\n1. 将要清除对象调用 `UObject::ConditionalBeginDestroy()` \n2. 给要清除的对象标记 `EInternalObjectFlags::HadReferenceKilled`\n3. 通知所有监听 GC 的代理对象\n4. 检测将要清除的对象是否已经准备好被销毁\n   如果没有准备好，加入到 `GGCObjectsPendingDestruction` 列表里，下次增量清楚再次询问\n   如果准备好，进入下一步\n5. 依次调用 `UObject::ConditionalFinishDestroy`、`UObject::~UObject`\n6. 如果清楚对象不在永久对象池 `PermanentPool` 中，删除对象在操作系统中的内存\n7. 调用 `FMemory::Trim()`\n\n\n\n## 5. UE4 对于 GC 的使用\n\n**UObject 及其子类**\n\n```c++\n// UObject 的创建和销毁\nUObject* o = NewObject<UObject>(this); // 会被自动回收\n\n// UObject 防止被回收\nURPOPERTY()\nUObject* obj;\n\nUObject* instance = NewObject<UObject>();\ninstance->AddToRoot();\ninstance->RemoveFromRoot();\ninstance = nullptr;\n\n// AActor 创建和销毁\nAActor* a = NewObject<AActor>(this); // 会被自动回收\na->Destroy();\t\t\t\t\t\t // 提前标记要回收\n\nUActorComponent* c = NewObject<AActor>(this); // 会被自动回收\nc->DestroyComponent();\t\t\t\t\t\t  // 提前标记要回收\n\n// AActor 防止被回收\na->SetOwner(this);\n\n// TArray, TMap\n// 防止容器内对象被回收\nUPROPERTY(EditAnywhere, BlueprintReadWrite, Category = \"UCoolDownComponent\")\nTArray<UCoolDown*> array;\n\n// 回收容器内对象\narray->clear();\n```\n\n\n\n**自定义的类**\n加入到 UE 的 GC 里需要继承 FGCObject，并重写 `AddReferencedObjects` 方法（毕竟 UObject 太大了，尽量避免继承它）\n\n```c++\nclass FMyNormalClass : public FGCObject\n{\npublic:\n    UObject* SafeObject;\n    \n    FMyNormalClass(UObject* Object) : SafeObject(Object){ }\n    \n    void AddReferencedObjects(FReferenceCollector& Collector) override\n    {\n        // 手动添加一个硬引用，使其不能被垃圾回收\n    \tCollector.AddReferencedObject(SafeObject);\n    }\n}；\n```\n\n\n\n## 6. UObject 的序列化和反序列化\n\n由于序列化（正向写入）和反序列化（反向读取）需要按照同样的方式进行，并且在反序列化时需要先有实例化对象在填充反序列化数据，UE 的序列化和反序列化都在函数 `void UObject::Serialize( FArchive& Ar )` 里\n\n序列化面临的问题：指针互相引用问题（序列化包 A，但是 A 里面有指针指向了 不需要序列化的包 B 内的对象）\n解决方法：UPackage 的序列化方法\n\n- 建立 Imports table\n  给 Package 内部的对象标记索引来代替对象地址，方便序列化时包内部对象指针地址替换成索引\n- 建立 Exports table\n  给 Package 外部的对象标记索引来代替对象地址，方便序列化时包外部对象指针地址替换成索引\n\n\n\n\n\n# Reference\n\n- [用 Launcher 引擎调试 UE4 源码的方法 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/133172832)\n- [Unreal Property System (Reflection) (unrealengine.com)](https://www.unrealengine.com/zh-CN/blog/unreal-property-system-reflection)\n- [Unreal 官方文档 | 游戏性架构 / 属性](https://docs.unrealengine.com/4.27/zh-CN/ProgrammingAndScripting/GameplayArchitecture/Properties/)\n- [Unreal 官方文档 | 游戏性架构 / 游戏模块](https://docs.unrealengine.com/4.27/zh-CN/ProgrammingAndScripting/GameplayArchitecture/Gameplay/)\n- [Unreal 官方文档 | Content Cooking](https://docs.unrealengine.com/4.27/en-US/SharingAndReleasing/Deployment/Cooking/)\n- [UE4 Config 配置文件详解（2017.4.1更新）_Jerish 的博客-CSDN博客](https://blog.csdn.net/u012999985/article/details/52801264)\n- [UE4 中的配置文件](https://zhuanlan.zhihu.com/p/150373398)\n- [深入研究虚幻 4 反射系统实现原理（一） - 风恋残雪 - 博客园 (cnblogs.com)](https://www.cnblogs.com/ghl_carmack/p/5701862.html)\n- [C++ 反射机制的实现_ freshman94 的博客-CSDN 博客_C++ 反射](https://blog.csdn.net/qq_22660775/article/details/89713867)\n- [UE4 GC机制解析（一）：GC信息收集](https://zhuanlan.zhihu.com/p/402403281)\n- [《Exploring in UE4》配置文件详解 [原理分析]](https://zhuanlan.zhihu.com/p/34397162)\n- [《InsideUE4》UObject（一）开篇 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/24319968)\n- [UE4 UObject 反射系列(一) Class 相关 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/75533335)\n- [Memory Management | UE4 Community Wiki](https://www.ue4community.wiki/memory-management-6rlf3v4i#garbage-collection)\n- [编译配置参考 | 虚幻引擎文档 (unrealengine.com)](https://docs.unrealengine.com/4.26/zh-CN/ProductionPipelines/DevelopmentSetup/BuildConfigurations/)\n- [深入研究虚拟机之垃圾收集（GC）算法实现 - 牧涛 - 博客园 (cnblogs.com)](https://www.cnblogs.com/superjt/p/5946059.html)\n- [垃圾回收的算法与实践 - 中村成洋](https://book.douban.com/subject/26821357/)\n- [UE4 垃圾回收 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/341137213)\n- [UE4 性能分析和优化](https://zhuanlan.zhihu.com/p/150110172)\n- [UE4 使用自定义的 Console Command](https://blog.csdn.net/maxiaosheng521/article/details/107788415)\n- [[UE4]console命令行常用命令(command)](https://dawnarc.com/2016/05/ue4console%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4command/)\n- [UE4: How To Write a Commandlet](https://www.oneoddsock.com/blog/2020/07/08/ue4-how-to-write-a-commandlet/)\n- [UE4: Guide Book -- Exec Functions](https://unreal.gg-labs.com/wiki-archives/common-pitfalls/exec-functions)\n- [【UE4】Rider For Unreal 体验报告](https://zhuanlan.zhihu.com/p/379911259)\n\n"
  },
  {
    "path": "UnrealEngine4/README.md",
    "content": "# [Unreal Engine 4](https://www.unrealengine.com/) Notes\n\nThis note is only used to record the **application of UE4** according to the graphics theory.\n\nIf you want to understand the specific implementation principle, please see my [ComputerGraphics Notes](../ComputerGraphics(OpenGL)/README.md)\n\n- This notes written in [Typora](https://www.typora.io/). It is also recommended to use it to read.\n- Incomplete note files will be temporarily igrone on Git.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n⚓️ Keep Reading , Keep Writing , Keep Coding.\n"
  }
]