0. 写在前面
本文仅为个人学习时记录的笔记。 其中大部分图形学知识仅是基础内容,后续对这部分内容有更深入的研究后会持续更新。
1. API基础
1.VAO:Vertex Array Object
顶点数组对象
unsigned int VAO;
glGenVertexArrays(1, &VAO);
封装绘制每个物体都需要调用的顶点设置流程: a. 绑定VBO,设置顶点数据 b. 设置顶点属性指针,指定数据解析方式 c. 绑定EBO:当目标是GL_ELEMENT_ARRAY_BUFFER时,VOA会储存glBindBuffer的调用,绑定、解绑EBO
2.VBO:Vertext Buffer Object
顶点缓冲对象 负责批量将顶点从CPU发送到GPU。 把vertices数据绑定到GL_ARRAY_BUFFER类型的VBO上
unsigned int VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
3.着色器
- layout (location = 0) in vec3 aPos,设置输入变量的位置值
glEnableVertexAttribArray(0)用于指定该位置值,将读出来的数据段赋值给指定的变量 in/out:in定义输入参数,out定义输出参数 - 编译着色器
需要运行时动态编译Shader代码,OpenGL才能使用
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
glUseProgram(shaderProgram)
glDeleteShader(xxx)
- 顶点着色器:手动指定顶点属性
负责告诉OpenGL怎么解析顶点数据。在解析前需要先把顶点数据初始化到缓冲
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
4.绘制
- 指定绘制的图形类型,顶点数组起始索引,和绘制的顶点个数
glDrawArrays(GL_TRIANGLES, 0, 3); - 索引缓冲对象 Element Buffer Object,EBO
定义顶点下标,避免顶点数据重复 // 绑定流程同 VBO,=》 GL_ELEMENT_ARRAY_BUFFER
unsigned int EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
5.Uniform
定义一个全局的变量
6.纹理API
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
stbi_image_free(data);
-
应用纹理 glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); glEnableVertexAttribArray(2); // vertext shader layout (location = 2) in vec2 aTexCoord; -
Sampler纹理采样器:用于将纹理数据传递给fragmentShader texture(ourTexture, TexCoord):接收纹理资源和顶点坐标,采样纹理颜色 -
纹理单元 GL_TEXTURE0~15
- Vertex Shader使用
OpenGL有16个纹理单元可以赋值 使用一个纹理单元前需要先激活,激活后的绑定则把纹理资源和纹理单元绑定在一起 glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texture1); - Fragment Shader使用
当fragment shader使用纹理时,需要定义: uniform sampler2D texture1; 然后把纹理单元编号指定给该变量 glUniform1i(glGetUniformLocation(ourShader.ID, “texture1”), 0);
7.构建相机
view = glm::lookAt(glm::vec3(0.0f, 0.0f, 3.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f)); 参数:相机位置、目标位置和世界坐标下的up向量
相机方向Direction:CameraPos - TargetPos 右轴方向Right:Dirction和世界Up叉乘 上轴方向(局部Up:Direciton和Right叉乘
2. 纹理
2.1 立方体贴图 CubeMap
1.生成纹理,并绑定到GL_TEXTURE_CUBE_MAP
unsigned int textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);
为6个面加载纹理
OpenGL定义的6个面的纹理目标,因为枚举(线性的Int递增),可以从GL_TEXTURE_CUBE_MAP_POSITIVE_X开始遍历
data = stbi_load(…)
glTexImage2D(
GL_TEXTURE_CUBE_MAP_POSITIVE_X + i,
0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data
);
2.天空盒
- 因为天空盒需要被绘制在所有物体的后面,所以需要第一个渲染天空盒,并且禁用深度写入
- 在玩家移动时天空盒需要保持不动,所以将渲染天空盒的观察矩阵转为33矩阵(移除位移),再转回44矩阵
glm::mat4 view = glm::mat4(glm::mat3(camera.GetViewMatrix())); - 优化:
每一个像素先跑一遍片元着色器渲染天空盒,但是其中大部分的像素又需要再渲染物体,导致性能浪费 所以优化为最后渲染天空盒,在顶点着色器中将天空盒的Z值设置为1.0,在顶点着色器后执行透视除法时,w/z还是1.0 即在NDC坐标下,天空盒的深度等于最大深度值1.0 将深度测试函数改为GL_LEQUAL,小于或等于都可以通过测试 这样在最后渲染天空盒时,就可以实现只渲染没有物体的像素了
2.2 使用CubeMap实现环境映射
计算相机在看向物体后的反射或折射的向量,在片段着色器中,用这个向量到CubeMap中采样
1.反射
vec3 I = normalize(Position - cameraPos);
vec3 R = reflect(I, normalize(Normal));
FragColor = vec4(texture(skybox, R).rgb, 1.0);
Normal = mat3(transpose(inverse(model))) * aNormal;
Position = vec3(model * vec4(aPos, 1.0));
2.折射
float ratio = 1.00 / 1.52;
vec3 I = normalize(Position - cameraPos);
vec3 R = refract(I, normalize(Normal), ratio);
FragColor = vec4(texture(skybox, R).rgb, 1.0);
2.3 法线贴图
因面法线一致导致光照计算没有细节 使用法线贴图,可以为每一个片元传递法线,提升光照细节
- 存储法线贴图
用纹理的rgb通道存储法线向量的xyz值 因为归一化的法线向量坐标范围在[-1, 1],纹理uv坐标是[0,1],需要转换 vec3 rgb_normal = normal * 0.5 + 0.5; // 从 [-1,1] 转换至 [0,1] 由于大部分法线都是指向z轴(0,0,1),转换后为(0.5, 0.5, 1),呈蓝色 - 切线空间:
因为实际的法线贴图的应用场景,面法线并不会指向z轴,所以纹理的法线向量不能是世界空间的,否则无法使用 将法线向量定义在切线空间,在切线空间,法线永远指向正z(可以理解为局部向量
3. Model
3.1Mesh
1.Struct Vertex和Struct Texture:两个结构体,分别存储Mesh的顶点和贴图信息
- 结构体特性:内存布局连续
vertex.Position = glm::vec3(0.2f, 0.4f, 0.6f); vertex.Normal = glm::vec3(0.0f, 1.0f, 0.0f); vertex.TexCoords = glm::vec2(1.0f, 0.0f); // = [0.2f, 0.4f, 0.6f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f]; 可以直接传入Vertex结构体 glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW); - 结构体特性:
offsetof(s, m):s结构体、m变量名,返回变量距结构体头部的字节偏移量 glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));
2.SetVAO、VBO、EBO
3.SetTexture
把贴图存到GL_TEXTURE0X并绑定到GL_TEXTURE_2D
4.DrawMesh
glDrawElements
3.2 Model
1.Mesh Vector
2.LoadModel Assimp::Importer(库) import Import.ReadFile:第三方库加载模型文件,支持指定后处理 aiProcess_Triangulate:加载时把图元都变换为三角形 aiProcess_FlipUVs:加载时翻转y轴的纹理坐标 aiProcess_GenNormals:如果模型数据不包含法向量,会为每个顶点创建法线 aiProcess_SplitLargeMeshes:将比较大的网格分割成更小的子网格 aiProcess_OptimizeMeshes:将多个小网格拼接为一个大的网格 返回一个aiScene对象,按第三方库的格式存储模型数据
3. ProcessNode 由LoadModel递归调用 处理子节点的所有网格 aiMesh *mesh = scene->mMeshes[node->mMeshes[i]]; meshes.push_back(processMesh(mesh, scene));
4. ProcessMesh 由ProcessNode调用 处理单个Mesh的数据 从aiMesh获取顶点数据,网格索引和材质数据,用于构建自有的Mesh对象
5. LoadMaterialTexture 由ProcessMesh调用 调用stb_image.TextureFromFile加载纹理数据, 缓存当前已加载的纹理路径,如果纹理已加载则跳过,直接用
4. 光照
4.1 基础光照
1. Phong和Blinn Phong模型
2. 法线和法线矩阵 当模型矩阵应用不等比缩放时,会导致原来的法线不再垂直平面 法线矩阵用于修复这个问题:模型矩阵左上角3×3的逆矩阵的转置矩阵 3×3:法线是单位向量,不需要应用位移数据
3. 封装材质
struct Material {
vec3 ambient;
sampler2D diffuse;
sampler2D specular;
float shininess;
};
① sampler2D diffuse:漫反射贴图,物体的漫反射颜色从该贴图采样 ② sampler2D specular:镜面反射贴图,物体的镜面反射颜色从该贴图采样 specular黑->白,颜色越亮 通过Uniform传递贴图数据 用顶点属性的行为传递纹理坐标用于顶点着色器采样
struct Light {
vec3 position;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
Light: 设置不同的光照强度
4. 投光物
-
平行光:固定的光照方向LightDir -
点光源:LightDir = LightPos - FragPos =》需要计算衰减 衰减公式:近距离非常亮,随着距离变远迅速变暗,更远时再平缓 Kc、Kl、Kq:1.0,0.09,0.032 -
聚光灯 只有在聚光方向的特定半径内的物体才会被照亮,其它的物体都会保持黑暗 =》 路灯、手电 ?:切光角,表示聚光半径 SpotDir:聚光指向的方向 LightDir:从Fragment指向光源,当LightDir和SpotDir的夹角小于?时,Fragment才会被照亮 根据Cos余弦值比较角度:LightDir和SpotDir的点积结果是余弦值,如果反余弦算夹角,在Shader中消耗大 软化边缘: 定义内圆锥和外圆锥,并且定义内圆锥半径和外圆锥半径 LightDir在内圆锥内,强度为1 在内圆锥到外圆锥之间,强度为0~1的插值 超过外圆锥强度为0 公式:cos角度越小值越大,so用θ-γ
4.2 HDR
高动态范围色彩,对比LDR:Lower Dynamic Range 显示器色彩被限制为只能显示0.0~1.0,光照方程没有这个限制 如果不做处理,颜色会被截断到1.0,导致颜色都接近白色,缺少细节 HDR可以暂时保留光照方程计算后超过1.0的部分,之后通过色调映射,将所有色彩通过某种算法映射回【0,1】
-
OpenGL应用 [ 浮点帧缓冲 ] 如果使用GL_RGB定义颜色缓冲,OpenGL会自动约束到【0,1】 使用GL_RGB16F、GL_RGBA16F、GL_RGB32F、GL_RGBA32F等浮点类型时可以超过 0,1
glBindTexture(GL_TEXTURE_2D, colorBuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL);
glBindFramebuffer(GL_FRAMEBUFFER, hdrFBO);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
hdrShader.Use();
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, hdrColorBufferTexture);
RenderQuad();
-
色调映射(shader)
- 平均映射
const float gamma = 2.2;
vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb;
vec3 mapped = hdrColor / (hdrColor + vec3(1.0));
mapped = pow(mapped, vec3(1.0 / gamma));
color = vec4(mapped, 1.0);
- 简单的曝光映射
const float gamma = 2.2;
vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb;
vec3 mapped = vec3(1.0) - exp(-hdrColor * exposure);
mapped = pow(mapped, vec3(1.0 / gamma));
color = vec4(mapped, 1.0);
不同的曝光数值,有不同的效果,如高爆光更亮,适合夜晚场景,低曝光更暗,适合白天场景
4.3 Bloom:基于HDR
思路:
- 先按HDR渲染一遍场景
- 在场景的HDR颜色缓冲中提取出明亮区域的部分
- 把带亮度的图片模糊
- 把结果再加回HDR场景图片
- 提亮
渲染HDR图和亮度图需要渲染场景两次 用MRT(Multiple Render Target)多渲染目标,支持一次渲染输出多个颜色缓冲
- Location
layout (location = 0) out vec4 FragColor; layout (location = 1) out vec4 BrightColor; 片元着色器定义多个location方便控制输出到不同的颜色缓冲 - 一个帧缓冲对象,附加两个颜色缓冲,并用GL_COLOR_ATTACHMENT0、GL_COLOR_ATTACHMENT1区分开
- 显示告知glDrawBuffers需要渲染多个颜色缓冲
GLuint attachments[2] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 }; glDrawBuffers(2, attachments); - 着色器内部,location修饰的变量,会自动写入对应的颜色缓冲
FragColor = vec4(lighting, 1.0f);
float brightness = dot(FragColor.rgb, vec3(0.2126, 0.7152, 0.0722));
if(brightness > 1.0)
BrightColor = vec4(FragColor.rgb, 1.0);
基于HDR处理泛光的好处是,泛光的程度更容易控制,否则会出现光晕效果过重的情况(因为色彩被截断为1 - 高斯模糊
高斯的特性:不需要采样整个二维四方形区域,只需要将二维高斯拆分成两个一维方程,在水平和垂直上各自采样,得出的结果是一样的。复杂度由N*N下降到N+N 用两个帧缓冲来回倒,A渲染水平模糊,再倒到B渲染垂直模糊。再来回AB,即可进行多次高斯模糊。 模糊的片元着色器代码: - 叠加纹理
在色调映射前叠加
4.4 Deferred Shading:延迟渲染
- 定义
- 区别于正向渲染:渲染复杂度和光源相关
- 延迟渲染:先渲染场景,把场景数据存储在G-Buffer,然后统一渲染一次光源:渲染复杂度和光源无关
缺点:更大的内存开销(存储G-Buffer),无法进行MSAA和混合 混合:因为第一遍刷场景几何存储G-Buffer数据时只会存储深度最前的片段数据 - G-Buffer:(所有场景数据的集合),通过MTR一次渲染多个缓冲
1) 存储所有片段位置向量 2) 存储所有片段发现向量 3) 存储所有片段的颜色向量(Albedo) 4) 存储所有片段的镜面强度值(Specular) 光源的位置和颜色、玩家的位置向量通过Uniform设置,这两个是和片段无关的参数 - 整体渲染过程
- G-buffer渲染
- 延迟光照处理
- 结合正向渲染
- 先处理延迟渲染
- 将延迟渲染中的深度缓冲从GBuffer中复制出来写入默认帧缓冲,再进行正向渲染,使得正向渲染是在有深度信息的前提下渲染
- 对更多光源的优化:Light Volumes
- 通过衰减方程,计算接收光源光照的最远距离
但其实实际应用中不能这样求解,因为GPU的高度并行计算,需要运行完全一致的着色器代码 而在着色器之中使用d来判断,形成分支的情况是不被允许的,GPU仍然会跑完所有if分支,相当于还是跑了所有的代码 - 渲染一个和光源光照体积大小一致的球,来决定片段是否参与这个光照的计算
- 其他高效的优化:TODO
1) 延迟光照(Deferred Lighting) 2) 切片式延迟着色(Tile-based Deferred Shading)
4.5 SSAO:屏幕空间环境光遮蔽
只有屏幕信息,没有场景信息,基于屏幕信息进行环境光的遮蔽计算
-
遮蔽因子 -
采样规律 i. 采样数量低会导致图像有波纹效果 ii. 采样数量高影响性能 iii. 随机旋转采样核心会导致明显噪声(引入随机性的副作用) iv. 通过模糊可修复:低采样->随机旋转核心->模糊,得到高质量效果 -
样本缓冲 计算遮蔽因子所需数据: 片段位置向量,片段法线向量,片段反射颜色?,采样核心?(半球),旋转采样核心的随机旋转向量 -
法向半球 在切线空间生成采样半球,Z轴和法向量重合 在切线空间内,在[-1, 1]范围内随机变换x/y,在[0, 1]范围内随机变换z来进行多采样点采样
5. 阴影
5.1 ShadowMapping,PCF
-
ShadowMapping ① 渲染深度贴图 ② 正常渲染场景,使用深度贴图判断fragment是否在阴影中(将片段转换到光源坐标,比较Z轴
-
渲染深度贴图:帧缓冲 准备资源
GLuint depthMapFBO;
glGenFramebuffers(1, &depthMapFBO);
const GLuint SHADOW_WIDTH = 1024, SHADOW_HEIGHT = 1024;
GLuint depthMap;
glGenTextures(1, &depthMap);
glBindTexture(GL_TEXTURE_2D, depthMap);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, SHADOW_WIDTH, SHADOW_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthMap, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
渲染循环
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glClear(GL_DEPTH_BUFFER_BIT);
lightProjection = glm::ortho(-10.0f, 10.0f, -10.0f, 10.0f, near_plane, far_plane);
lightView = glm::lookAt(lightPos, glm::vec3(0.0f), glm::vec3(0.0, 1.0, 0.0));
lightSpaceMatrix = lightProjection * lightView;
RenderScene();
glBindFramebuffer(GL_FRAMEBUFFER, 0);
-
渲染阴影: 1) 顶点着色器:通过lightSpaceMatrix矩阵,将顶点转换到光源空间,储存到FragPosLightSpace变量,传递给片元着色器 2) 片元着色器:接收FragPosLightSpace变量,和深度贴图 输出:
lighting = (ambient + (1.0 - shadow) * (diffuse + specular)) * color;
shadow计算:
vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
projCoords = projCoords * 0.5 + 0.5;
float closestDepth = texture(shadowMap, projCoords.xy).r;
float shadow = currentDepth > closestDepth ? 1.0 : 0.0;
-
阴影失真: 由于深度贴图受限于分辨率,当片段距离光源比较远时,可能多个片段会采样到同一个深度贴图的值。 当出现下图这种斜坡表面时,会计算出一段在阴影,一段不在,导致失真。 通过增加一个偏移量解决该问题。(相当于抬高物体的深度 float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0; // 偏移量的计算和表面坡度有关,当光照方向和表面法线平行时,偏移量取最小,垂直时取最大 float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005); PCF:percentage-closer filtering 对深度贴图中目标像素的周围多次采样,然后取平均
5.2 点光源阴影
使用cubeMap生成六面的深度贴图 OpenGL支持使用几何着色器实现一次渲染六个面的深度图
-
生成深度cubeMap贴图 -
配置vp矩阵数据 用一个列表存储每个面的观察矩阵 std::vector<glm::mat4> shadowTransforms;
shadowTransforms.push_back(shadowProj * glm::lookAt(lightPos, lightPos + glm::vec3(1.0,0.0,0.0), glm::vec3(0.0,-1.0,0.0));
shadowTransforms.push_back(shadowProj * glm::lookAt(lightPos, lightPos + glm::vec3(-1.0,0.0,0.0), glm::vec3(0.0,-1.0,0.0));
shadowTransforms.push_back(shadowProj * glm::lookAt(lightPos, lightPos + glm::vec3(0.0,1.0,0.0), glm::vec3(0.0,0.0,1.0));
shadowTransforms.push_back(shadowProj * glm::lookAt(lightPos, lightPos + glm::vec3(0.0,-1.0,0.0), glm::vec3(0.0,0.0,-1.0));
shadowTransforms.push_back(shadowProj * glm::lookAt(lightPos, lightPos + glm::vec3(0.0,0.0,1.0), glm::vec3(0.0,-1.0,0.0));
shadowTransforms.push_back(shadowProj * glm::lookAt(lightPos, lightPos + glm::vec3(0.0,0.0,-1.0), glm::vec3(0.0,-1.0,0.0));
因为投影矩阵和方向无关,用一个即可 glm::mat4 shadowProj = glm::perspective(glm::radians(90.0f), aspect, near, far); 90度确保视野可以填充cubeMap的每一个面,每个面和其他面在边缘对齐 -
顶点着色器不做处理,直接输出m矩阵变换后的顶点坐标 几何着色器:内建变量gl_Layer可以指定顶点图元被输出到立方体贴图的哪个面,对输入到几何着色器的每个三角形的三个顶点,都在六个面进行一次变换,并且输出到指定面 for(int face = 0; face < 6; ++face)
{
gl_Layer = face;
for(int i = 0; i < 3; ++i)
{
FragPos = gl_in[i].gl_Position;
gl_Position = shadowMatrices[face] * FragPos;
EmitVertex();
}
EndPrimitive();
}
片元着色器:手动计算深度 float lightDistance = length(FragPos.xyz - lightPos);
lightDistance = lightDistance / far_plane;
gl_FragDepth = lightDistance;
将距离差 / far_plane,线性的映射到0~1范围(深度贴图范围) -
渲染点光阴影 ShadowCalculation: vec3 fragToLight = fragPos - lightPos;
float closestDepth = texture(depthMap, fragToLight).r;
closestDepth *= far_plane;
float currentDepth = length(fragToLight);
float bias = 0.05;
float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0;
-
点光阴影中的PCF 均匀采样,但采样量过多(64次) 指定部分采样方向,剔除接近的方向,只保留偏移大的采样方向(20次)
6. 其他
6.1 深度测试
- 片段着色器之后,模板测试之后
- glEnable(GL_DEPTH_TEST) 开启深度测试
glClear(GL_DEPTH_BUFFER_BIT) 每个渲染循环前清除深度缓冲 glDepthMask(GL_FALSE) 深度测试掩码,设置深度缓冲为只读,禁止写入 glDepthFunc(GL_LESS) 设置深度测试的比较运算符 - 深度计算公式:
- 深度冲突:由于精度不足引起
6.2 模板测试
根据模板缓冲的值决定片元是否被丢弃
6.4 帧缓冲
用于后期处理
- 默认帧缓冲
- 自定义帧缓冲
- 创建帧缓冲对象
unsigned int fbo; glGenFramebuffers(1, &fbo); // 绑定为激活的帧缓冲 glBindFramebuffer(GL_FRAMEBUFFER, fbo); - 定义一个完整的帧缓冲:
附件至少一个缓冲(颜色、深度或模板缓冲) 至少有一个颜色附件(Attachment) 所有的附件都必须是完整的(保留了内存) 每个缓冲都应该有相同的样本数 - 查询帧缓冲是否完整:
glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE - 绑定为默认帧缓冲(如果需要
glBindFramebuffer(GL_FRAMEBUFFER, 0); - 操作结束后删除对象:
glDeleteFramebuffers(1, &fbo); - 添加附件:一个内存位置,帧缓冲中的缓冲,可以是一个纹理也可以是一个渲染缓冲对象
- 纹理附件
glTexImage2D:创建一个空纹理 glFrameBufferTexture2D:绑定到帧缓冲上 - 缓冲对象附件
unsigned int rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
渲染缓冲对象是只写的,如果需要深度和模板值测试,但不需要采样,优先选择这个如果需要采样,选择纹理附件 - 渲染到纹理
① 颜色渲染到纹理 ② 深度和模板渲染到渲染缓冲对象
6.5 切线空间
- 法线贴图中的法线向量需定义在切线空间,因为在切线空间中,法线永远指向正z(相当于局部坐标空间
- 切线空间由TBN矩阵描述
Tangent 切线 、Bitangent 副切线、Normal 法线 - 求TB向量
- 正交化
- OpenGL应用
- 在片元着色器,将法线坐标左乘TBN矩阵,转到世界坐标,和光照变量一致
- 在顶点着色器中,将光照向量lightDir和viewDir都乘TBN的逆矩阵,将光照向量从世界空间转为切线空间
7. PBR和IBL
参考资料
https://learnopengl-cn.github.io/
|