IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 游戏开发 -> 图形_LearnOpenGL学习笔记 -> 正文阅读

[游戏开发]图形_LearnOpenGL学习笔记

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才能使用
	// 创建着色器,用ID索引
	unsigned int vertexShader; 
	vertexShader = glCreateShader(GL_VERTEX_SHADER);
	// 把Shader源码附加到着色器,vertexShaderSource TO vertexShader
	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);
	// 绘制函数,从当前绑定的EBO中取顶点数据
	glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

5.Uniform

定义一个全局的变量

6.纹理API

  • 生成纹理
	// 使用ID引用纹理对象
	unsigned int texture; 
	glGenTextures(1, &texture);
	// 绑定到GL_TEXTURE_2D
	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); 
	// 自动生成Mipmap
	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);
	// 顶点着色器需提供世界坐标Position和法线坐标Normal   
	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);
    	// 使用一个不同的着色器将HDR颜色缓冲渲染至2D铺屏四边形上
    	hdrShader.Use();
    	glActiveTexture(GL_TEXTURE0);
    	glBindTexture(GL_TEXTURE_2D, hdrColorBufferTexture);
    	RenderQuad();
    
  • 色调映射(shader)

    • 平均映射
      	const float gamma = 2.2;
      	vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb;
      	// Reinhard色调映射
      	vec3 mapped = hdrColor / (hdrColor + vec3(1.0));
      	// Gamma校正
      	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);
      	 // Gamma校正 
      	 mapped = pow(mapped, vec3(1.0 / gamma));
      	 color = vec4(mapped, 1.0);
      
      不同的曝光数值,有不同的效果,如高爆光更亮,适合夜晚场景,低曝光更暗,适合白天场景
      在这里插入图片描述

4.3 Bloom:基于HDR

思路:

  1. 先按HDR渲染一遍场景
  2. 在场景的HDR颜色缓冲中提取出明亮区域的部分
  3. 把带亮度的图片模糊
  4. 把结果再加回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修饰的变量,会自动写入对应的颜色缓冲
      	// 正常的HDR颜色缓冲
      	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);
      	// 绑定纹理和缓冲,指定缓冲渲染深度信息到depthMap
      	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);
      	// 将lightSpace矩阵传入shader,使后续渲染的场景都从世界空间转换到光源的view/projection空间内计算
      	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:绘制场景,缓冲对象会自动根据设置,在Shader运行过后输出深度信息到贴图
      	RenderScene();
      	glBindFramebuffer(GL_FRAMEBUFFER, 0);
      
    • 渲染阴影:
      1) 顶点着色器:通过lightSpaceMatrix矩阵,将顶点转换到光源空间,储存到FragPosLightSpace变量,传递给片元着色器
      2) 片元着色器:接收FragPosLightSpace变量,和深度贴图
      输出:

      	// 表示片段有多大成分不在阴影中
      	lighting = (ambient + (1.0 - shadow) * (diffuse + specular)) * color; 
      	shadow计算:
      	// 执行透视除法,从顶点着色器输出顶点到gl_Position时,会自动进行透视除法,把裁剪空间的坐标转为NDC坐标,范围从-w~w转为-1~1
      	// 这里因为不是输出gl_Position,所以需要手动执行
      	vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
      	// 由于深度贴图的深度范围是0~1,把projCorrd也转为0~1
      	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; // built-in variable that specifies to which face we render.
    	    for(int i = 0; i < 3; ++i) // for each triangle's vertices
    	    {
    	        FragPos = gl_in[i].gl_Position;
    	        gl_Position = shadowMatrices[face] * FragPos;
    	        EmitVertex();
    	    }    
    	    EndPrimitive();
    	}
    

    片元着色器:手动计算深度

    	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 模板测试

根据模板缓冲的值决定片元是否被丢弃

  • 接口
    glEnable(GL_STENCIL_TEST); // 开启模板测试
    glClear(GL_STENCIL_BUFFER_BIT); // 每一次渲染迭代开始时清空模板缓冲
    // 模板测试掩码:掩码值与写入的模板值做与运算
    glStencilMask(0xFF); // 每一位写入模板缓冲时都保持原样
    glStencilMask(0x00); // 每一位在写入模板缓冲时都会变成0(禁用写入)

  • 模板函数:配置模板测试

    • glStencilFunc(GLenum func, GLint ref, GLuint mask):描述如何比较
      func:设置测试函数,可选GL_NEVER、GL_LESS、GL_LEQUAL、GL_GREATER、GL_GEQUAL、GL_EQUAL、
      GL_NOTEQUAL、GL_ALWAYS
      ref:参考值,和缓冲通过func比较
      mask:掩码

    • glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass):描述如何更新模板缓冲
      在这里插入图片描述

      默认为:glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP),不论是否通过都保留值

    • 应用:绘制物体轮廓
      ① 在绘制(需要添加轮廓的)物体之前,将模板函数设置为GL_ALWAYS,每当物体的片段被渲染时,将模板缓冲更新为1。
      ② 渲染物体。
      ③ 禁用模板写入以及深度测试。
      ④ 将每个物体缩放一点点。
      ⑤ 使用一个不同的片段着色器,输出一个单独的(边框)颜色。
      ⑥ 再次绘制物体,但只在它们片段的模板值不等于1时才绘制。
      ⑦ 再次启用模板写入和深度测试。

    6.3 透明和半透明

    • 透明材质
      • 加载带alpha通道的纹理,以及使用RGBA
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
      • 去掉完全透明的部分:discard
        vec4 texColor = texture(texture1, TexCoords);
        if(texColor.a < 0.1) discard;
      • 对于有透明度的纹理,使用GL_CLAMP_TO_EDGE的纹理环绕方式
    • 混合:处理半透明材质
      • 作用时机:片段着色器之后,所有测试通过之后
      • 开启混合:glEnable(GL_BLEND);
      • 设置混合因子:glBlendFunc(GLenum sfactor, GLenum dfactor)
      • 排序:
        ① 先绘制所有不透明的物体。
        ② 对所有透明的物体排序。
        ③ 按顺序绘制所有透明的物体。
    • 面剔除
      • 在进入着色器之前,检查所有面的面向,找到背向相机的面剔除,只渲染面向相机的面
      • 分析顶点数据的环绕顺序:
        在这里插入图片描述
        从观察者的方向看,逆时针渲染的三角形是正向的,顺时针渲染的三角形是逆向的
      • 开启:glEnable(GL_CULL_FACE);
        设置哪个面是正向的:glFrontFace(GL_CCW);
        设置剔除哪个面:glCullFace(GL_BACK);

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/

  游戏开发 最新文章
6、英飞凌-AURIX-TC3XX: PWM实验之使用 GT
泛型自动装箱
CubeMax添加Rtthread操作系统 组件STM32F10
python多线程编程:如何优雅地关闭线程
数据类型隐式转换导致的阻塞
WebAPi实现多文件上传,并附带参数
from origin ‘null‘ has been blocked by
UE4 蓝图调用C++函数(附带项目工程)
Unity学习笔记(一)结构体的简单理解与应用
【Memory As a Programming Concept in C a
上一篇文章      下一篇文章      查看所有文章
加:2022-02-16 13:27:57  更:2022-02-16 13:29:47 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/27 16:51:09-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码