欢迎关注公众号:sumsmile /专注图形学
花了几天时间,整理了以前写的learnopengl笔记,做了删减、补充。
learnopengl地址:learnopengl-英文-pdf、learnopengl-中文、learnopengl-在线英文
入门
图形管线
- 顶点数据
是用顶点属性(Vertex Attribute)表示的,它可以包含任何我们想用的数据,如坐标、颜色、法线等
图元(Primitive),告诉GL如何解释顶点
- GL_POINTS 点
- GL_TRIANGLES 三角形
- GL_LINE_STRIP 线条
在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。
- 顶点缓冲对象(Vertex Buffer Objects, VBO),存放顶点数据
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
- GPU管理数据三种方式:
- GL_STATIC_DRAW :数据不会或几乎不会改变。
- GL_DYNAMIC_DRAW:数据会被改变很多。
- GL_STREAM_DRAW :数据每次绘制时都会改变。
- 顶点数据使用
// 0. 复制顶点数组到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 1. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 2. 当我们渲染一个物体时要使用着色器程序
glUseProgram(shaderProgram);
// 3. 绘制物体
someOpenGLFunctionThatDrawsOurTriangle();
- 顶点数组对象
顶点数组对象(Vertex Array Object, VAO):可以同时绑定一组缓冲
glEnableVertexAttribArray和glDisableVertexAttribArray的调用。
- 绘制三角形的三种方式
- GL_TRIANGLES
- GL_TRIANGLE_STRIP,每次增加一个点
- GL_TRIANGLE_FAN 扇形
- draw有两种方式
- drawArray
- drawElement
glUseProgram(shaderProgram);
// drawArrays
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
// drawElements
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
着色器
- 能声明的顶点属性是有上限的
OpenGL确保至少有16个包含4分量的顶点属性可用。
但是有些硬件或许允许更多的顶点属性,你可以查询GL_MAX_VERTEX_ATTRIBS来获取具体的上限
int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;
- 顶点数据的管理 有两种方式
- 写死一个位子,layout (location = 0),传输数据时自己按写死的位置来传
- 通过在OpenGL代码中使用glGetAttribLocation查询属性位置值(Location)
- 变量类型
- in/out(gl3.0+) varying(2.0)
- uniform:全局统一变量
纹理
- 纹理坐标左下为起点,右上为终点
- 纹理环绕方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
GL_TEXTURE_BORDER_COLOR需要额外设置参数
GL_TEXTURE_float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);BORDER_COLOR
- 纹理过滤
- GL_NEAREST 也叫邻近过滤,Nearest Neighbor Filtering
- GL_LINEAR(也叫线性过滤,(Bi)linear Filtering)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
- 多级渐远纹理
优点
- 消除由于纹理采样不足引起的混叠效果
- 提升性能,读取、缓存更小的图片
glGenerateMipmaps 生成的mipmap缓存在内存中
解决在多个level纹理之间的采样,配置和普通的纹理过滤相同
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
注意:mipmap额外生成了更小的图,解决的是在物体在远处变小的情况下的采样问题,所以放大无效,放大可能会报错或无效
- 加载纹理
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);
// 加载并生成纹理
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
if (data)
{
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data);
- 纹理应用
float vertices[] = {
// ---- 位置 ---- ---- 颜色 ---- - 纹理坐标 -
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // 右上
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 左下
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // 左上
};
- 一个纹理的默认纹理单元是0,它是默认的激活纹理单元
- OpenGL至少有16个纹理单元,从GL_TEXTURE0到GL_TEXTRUE15。也可以通过GL_TEXTURE0 + 8的方式获得GL_TEXTURE8
加上纹理的使用
- 绑定纹理
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
- 设置纹理到shader
ourShader.use(); // 不要忘记在设置uniform变量之前激活着色器程序!
glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0); // 手动设置
ourShader.setInt("texture2", 1); // 或者使用着色器类设置
坐标系统
参考之前写过的一篇你文章深入理解opengl坐标系统
- 记住几张流程图
整个渲染过程涉及5个坐标系,坐标范围如下,参考下图
为什么叫裁剪坐标,是指透视投影之后裁剪掉视口之外的数据
- 几个细节
- opengl管线会根据clip坐标进行剪裁,在camera视锥范围外的顶点会被丢弃,判断计算:
- 裁剪坐标-->NDC空间坐标是由管线自动完成的
摄像机
- 定义摄像机
- 位置
- 方向
- 右边
- 上边
相机LookAt矩阵
- 欧拉角
用pitch(俯仰角)和yaw(偏航角)定义相机的旋转。Roll(滚转角)一般用在游戏中的一些特殊视角。
- 欧拉角的问题
- 对两个朝向进行插值比较困难。简单地对X、Y、Z角度进行插值得到的结果不太理想
- 万向节死锁
- 不同的角度可产生同样的旋转(-180 180)
- 四元数-解决欧拉角的问题
四元数的讲解:
一篇四元数的文档:
光照
有两个个概念容易弄混:光照模型、着色模型
网上的资料有些把着色模型和光照模型混到一起讲,这里我还是按照两个概念来总结。
光照模型:描述光线打到物体表面的一个点上,如何产生颜色。描述的是如何计算一个点的光照效果。
着色模型:在确定光照模型,即确定单点颜色方法的前提下,如何生成整个面的颜色
- 光照模型:Lambert、Phong、Blinn-Phong、PBR
- Lambert模型(漫反射)
color = light * albedo * dot(normal, L)
// light:入射光强度
// albedo:反照率(有多少比例的光反射,剩下的会被吸收,不考虑次表面散射)
// normal:表面法线
Lambert没有高光效果,像塑料
- Phong模型(镜面反射)
-
Blinn-Phong光照模型(修正镜面光)
当材质比较粗糙,反射向量与视线的夹角可能会超过了90度,cos值小于0,Phone模型会忽略掉这部分亮度,整体偏暗。
Blinn-Phong在次基础上提出半程向量,计算入射光和视角的中间向量、法向量夹角余弦,避免cos值为负的情况
-
Rendering Equation(全局光照模型)
比较复杂,后面会单独讲
- 着色模型 Comparison flat, Gouraud, Phong shading
按出场时间顺序为Flat→Gouraud→Phong Shading,计算复杂度也如此,当然效果也越来越好
原理:
法线变换矩阵
法线只有方向,按照mv变换会改变法向方向,所以要单独处理。
或者:
推导参考:OpenGL Normal Vector Transformation
矩阵求逆对于着色器开销很大,必须在场景中的每一个顶点上进行。应该尽可能地避免在着色器中进行求逆运算。最好先在CPU上计算出法线矩阵,再通过uniform把它传递给着色器(就像模型矩阵一样)。
基础光照
颜色
颜色的原理:光的颜色经过物体表面吸收后,剩下的不被吸收的部分反射到人眼中,即我们看到的颜色。
如下:这个玩具吸收了光线中一半的绿色值,但仍然也反射了一半的绿色值。玩具现在看上去是深绿色(Dark-greenish)的
glm::vec3 lightColor(0.0f, 1.0f, 0.0f);
glm::vec3 toyColor(1.0f, 0.5f, 0.31f);
glm::vec3 result = lightColor * toyColor; // = (0.0f, 0.5f, 0.0f);
phone光照实现:
#version 330 core
out vec4 FragColor;
in vec3 Normal;
in vec3 FragPos;
uniform vec3 lightPos;
uniform vec3 viewPos;
uniform vec3 lightColor;
uniform vec3 objectColor;
void main() {
// ambient
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;
// diffuse
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
float diff = max(dot(lightDir, norm), 0.0);
vec3 diffuse = diff * lightColor;
// specular
float specularStrength = 0.5;
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
vec3 specular = specularStrength * max(0, pow(dot(reflectDir, viewDir), 32));
vec3 result = (ambient + diffuse + specular) * objectColor;
FraColor = vec4(result, 1.0);
}
注意reflect的方向定义:
glsl中*的作用: GLSL中向量间的*运算符指的是分量之间相乘(和向量间加法类似),不是点乘也不是矩阵乘法。只有与矩阵相关的一些*运算才定义为矩阵乘法。如果要点乘的话可以使用dot(v1,v2)。
在顶点着色器中实现的冯氏光照模型叫做Gouraud着色(Gouraud Shading),而不是冯氏着色(Phong Shading)。记住,由于插值,这种光照看起来有点逊色。冯氏着色能产生更平滑的光照效果。
在顶点着色器中做光照的优势是,相比片段来说,顶点要少得多,因此会更高效,所以(开销大的)光照计算频率会更低
材质
投光物(光照类型) 平行光、点光源、聚光
- 平行光
- 点光源,点光源需要额外乘上衰减系数
根据能量守恒,传播的越远,光的半径越大,平均到每个点上的能量越小
衰减函数曲线:
衰减系数是个经验值
加上衰减,代码实现:
float distance = length(light.position - FragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance +
light.quadratic * (distance * distance));
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;
- 聚光
聚光灯比点光源多两个参数:1)聚光灯方向;2)切光角(Cutoff Angle)
- LightDir:从片段指向光源的向量。
- SpotDir:聚光所指向的方向。
- Phiϕ:指定了聚光半径的切光角。落在这个角度之外的物体都不会被这个聚光所照亮。
- Thetaθ:LightDir向量和SpotDir向量之间的夹角。在聚光内部的话θ值应该比ϕ值小。
- 聚光灯 平滑/软化边缘
普通的实现在光照边缘处比较生硬,可增加平滑过渡。
需要模拟聚光有一个内圆锥(Inner Cone)和一个外圆锥(Outer Cone),内锥即上面聚光灯的最大轮廓,外追是额外增加的一圈过渡的区域,均用cos值表示
\begin{equation} I = \frac{\theta - \gamma}{\epsilon} \end{equation}
这里ϵ(Epsilon)是内(ϕ)和外圆锥(γ)之间的余弦值差(ϵ=ϕ−γ)。最终的I值就是在当前片段聚光的强度
关键代码:
lightingShader.setVec3("light.position", camera.Position);
lightingShader.setVec3("light.direction", camera.Front);
lightingShader.setFloat("light.cutOff", glm::cos(glm::radians(12.5f)));
lightingShader.setFloat("light.outerCutOff", glm::cos(glm::radians(17.5f)));
fragmentShader中
void main()
{
// ambient
vec3 ambient = light.ambient * texture(material.diffuse, TexCoords).rgb;
// diffuse
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(light.position - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = light.diffuse * diff * texture(material.diffuse, TexCoords).rgb;
// specular
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
vec3 specular = light.specular * spec * texture(material.specular, TexCoords).rgb;
// spotlight (soft edges)
float theta = dot(lightDir, normalize(-light.direction));
float epsilon = (light.cutOff - light.outerCutOff);
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);
diffuse *= intensity;
specular *= intensity;
// attenuation
float distance = length(light.position - FragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;
vec3 result = ambient + diffuse + specular;
FragColor = vec4(result, 1.0);
}
在正常的计算之后,额外增加了外锥的衰减系数,同样,聚光灯本质也是点光源,所以也会有距离衰减。
多光源
将所有类型的光源分别计算,再加和
// function prototypes
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir);
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir);
vec3 CalcSpotLight(SpotLight light, vec3 normal, vec3 fragPos, vec3 viewDir);
void main()
{
// properties
vec3 norm = normalize(Normal);
vec3 viewDir = normalize(viewPos - FragPos);
// phase 1: directional lighting
vec3 result = CalcDirLight(dirLight, norm, viewDir);
// phase 2: point lights
for(int i = 0; i < NR_POINT_LIGHTS; i++)
result += CalcPointLight(pointLights[i], norm, FragPos, viewDir);
// phase 3: spot light
result += CalcSpotLight(spotLight, norm, FragPos, viewDir);
FragColor = vec4(result, 1.0);
}
注意,访问shader中数组,需要挨个处理
fragment shader
#define NR_POINT_LIGHTS 4
uniform PointLight pointLights[NR_POINT_LIGHTS];
主程序中
// point light 1
lightingShader.setVec3("pointLights[0].position", pointLightPositions[0]);
lightingShader.setVec3("pointLights[0].ambient", 0.05f, 0.05f, 0.05f);
lightingShader.setVec3("pointLights[0].diffuse", 0.8f, 0.8f, 0.8f);
lightingShader.setVec3("pointLights[0].specular", 1.0f, 1.0f, 1.0f);
lightingShader.setFloat("pointLights[0].constant", 1.0f);
lightingShader.setFloat("pointLights[0].linear", 0.09f);
lightingShader.setFloat("pointLights[0].quadratic", 0.032f);
// point light 2
lightingShader.setVec3("pointLights[1].position", pointLightPositions[1]);
lightingShader.setVec3("pointLights[1].ambient", 0.05f, 0.05f, 0.05f);
lightingShader.setVec3("pointLights[1].diffuse", 0.8f, 0.8f, 0.8f);
lightingShader.setVec3("pointLights[1].specular", 1.0f, 1.0f, 1.0f);
lightingShader.setFloat("pointLights[1].constant", 1.0f);
lightingShader.setFloat("pointLights[1].linear", 0.09f);
lightingShader.setFloat("pointLights[1].quadratic", 0.032f);
高级OpenGL
深度测试
- Early Depth Testing(提前深度测试)
片段着色器通常开销都是很大的,所以我们应该尽可能避免运行它们。
提前深度测试允许深度测试在片段着色器之前运行。当使用提前深度测试时,不能写入片段的深度值。
glEnable(GL_DEPTH_TEST);
glDepthMask(GL_FALSE); // 只读模式
layout(early_fragment_tests) in;
- 深度测试,需要每次迭代之前执行清除操作
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
- 深度测试函数
最常用的是GL_LESS
glDepthFunc(GL_LESS);
- GLSL内建变量gl_FragCoord
- 片段着色器中直接访问
- gl_FragCoord的x和y分量代表了片段的屏幕空间坐标(其中(0, 0)位于左下角)
- gl_FragCoord中也包含了一个z分量,它包含了片段真正的深度值。z值就是需要与深度缓冲内容所对比的那个值
注意,片段着色器在光栅化之后,光栅化已经经过了视口变换viewport变换,变成了1920 * 720(举例)。TexCoords取值范围是(0~1),gl_FragCoord.xy已经是真实的值,z坐标其实是个相对值,用于深度测试。
- 深度冲突,远处会反复闪烁 两个原因:
- 本质原因,深度缓冲没有足够的精度
- 投影矩阵的实现决定近平面有更好的精度,所以远平面的精度较差,更容易发生z-fight
处理办法:
- 手动设置偏移,glPolygonOffset也可以实现
rdener1()//绘制物体glEnable(GL_POLYGON_OFFSET_FILL);
glPolygonOffset(0.0f,-1.0f)
rdener2()//绘制物体
- 尽可能将近平面设置远一些
- 使用更高精度的深度缓冲。大部分深度缓冲的精度都是24位的,但现在大部分的显卡都支持32位的深度缓冲
模板测试
很形象的图片:
模板测试例子:
stencil test在graphics pipeline里面的位置,模板测试在深度测试之前:
- 模板测试原理 模板测试和深度测试相似,过程更复杂一点
- 模板值(Stencil Value)是8位的,即每个像素/片段一共能有256种不同的模板值,深度缓冲是24位
- glfw默认生成了模板缓冲,但不是每个窗口库都会这么做
- 不用模板缓冲时,开启模板缓冲不会有任何影响,只是往一个不用的缓存中写入了数据
- 启用:glEnable(GL_STENCIL_TEST);
- 每次迭代需要清除
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
- 描边的例子
看个例子就明白了:
- 初始化模板测试配置,打开模板测试
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
glEnable(GL_STENCIL_TEST);
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
// 三个参数:
//1-模板测试失败
//2-模板测试通过、深度测试失败
//3-模板和深度测试都通过
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
- 绘制地板,禁用模板测试的写入,防止地板的影响
glStencilMask(0x00);
// 和glDepthMask(GL_FALSE)效果一样
glBindVertexArray(planeVAO);
glBindTexture(GL_TEXTURE_2D, floorTexture);
shader.setMat4("model", glm::mat4(1.0f));
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);
- 第一遍绘制箱子,额外增加模板缓冲写入
glStencilFunc(GL_ALWAYS, 1, 0xFF);
glStencilMask(0xFF); //打开模板写入
DrawTwoContainers();
- 第二遍绘制箱子,箱子放大到1.1倍,禁止模板写入,仅读取。大于原箱子的部分都是0,对大于的这部分描边。
// 放大比例为 1.1倍,用于模型变换
float scale = 1.1f;
// 设置模板测试函数为不等式通过,即原箱体外边的值
// 测试不通过是指,经过着色器之后的值,不更新或者覆盖像素缓冲里的值了
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
// 禁止写入,感觉这时候是否写入都无所谓了,这是最后一道工序,后面不用了,再循环时会clear掉
glStencilMask(0x00);
//禁用深度缓冲,因为描边的值会到地板下面去
glDisable(GL_DEPTH_TEST);
shaderSingleColor.use();
- 其他的例子
- 绘制镜子效果,参考DirectX11 平面镜像的实现
提出镜子外面的绘制,即用镜子作为模板缓冲
- 避免阴影重复绘制的优化
如果有重复绘制的阴影,可以用模板测试过滤掉
另外,支持Stencil Early的显卡上,可以直接跳过像素着色器的计算,参考OpenGL中的模板测试有哪些实际应用
混合
- discard使用
注意,绘制透明物体时,边缘可能有线,设置环绕方式为edge,不要设置成repeat,
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
- blend
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
- 透明物体绘制,需要按距离从远到近绘制(反之,后面的过不了深度测试)
使用std::map存储,std::map底层是红黑树,默认是搜索二叉树,从小至大排列
std::map<float, glm::vec3> sorted;
for (unsigned int i = 0; i < windows.size(); i++)
{
float distance = glm::length(camera.Position - windows[i]);
sorted[distance] = windows[i];
}
遍历则从后往前遍历
for(std::map<float,glm::vec3>::reverse_iterator it = sorted.rbegin(); it != sorted.rend(); ++it)
{
model = glm::mat4();
model = glm::translate(model, it->second);
shader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 6);
}
**注意:**源代码中,地板的纹理设置超过了2,实现repeat的地板
地板的顶点属性
float planeVertices[] = {
// positions // texture Coords
5.0f, -0.5f, 5.0f, 2.0f, 0.0f,
-5.0f, -0.5f, 5.0f, 0.0f, 0.0f,
-5.0f, -0.5f, -5.0f, 0.0f, 2.0f,
5.0f, -0.5f, 5.0f, 2.0f, 0.0f,
-5.0f, -0.5f, -5.0f, 0.0f, 2.0f,
5.0f, -0.5f, -5.0f, 2.0f, 2.0f
};
地板的环绕方式设置为repeat
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, format == GL_RGBA ? GL_CLAMP_TO_EDGE : GL_REPEAT); // for this tutorial: use GL_CLAMP_TO_EDGE to prevent semi-transparent borders. Due to interpolation it takes texels from next repeat
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, format == GL_RGBA ? GL_CLAMP_TO_EDGE : GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
源代码参考:learningOpenGl-blending_discard
更高级的技术还有次序无关透明度(Order Independent Transparency, OIT),参考OIT,后面有时间研究下
面剔除
默认会剔除顺时针方向的面
- 启用面剔除
glEnable(GL_CULL_FACE);
- 设置默认的正面
- GL_BACK:只剔除背向面。
- GL_FRONT:只剔除正向面。
- GL_FRONT_AND_BACK:剔除正向面和背向面
glCullFace(GL_FRONT);
glCullFace(GL_FRONT_AND_BACK)后,所有的多边形都将被剔除,所以看见的就只有点和直线
例子,实现一个前面剔除,两种实现
- 剔除前面
glEnable(GL_CULL_FACE);
glCullFace(GL_FRONT);
- 剔除背面,但是定义clock-wise(顺时针为前面)
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
glFrontFace(GL_CW);
帧缓冲
- 一个完整的帧缓冲需要满足以下的条件:
- 附加至少一个缓冲(颜色、深度或模板缓冲)。
- 至少有一个颜色附件(Attachment)。
- 所有的附件都必须是完整的(保留了内存)。
- 每个缓冲都应该有相同的样本数。
检查帧缓冲是否完整
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)
// 执行胜利的舞蹈
- Renderbuffer Object(渲染缓冲对象)
一种更适合GPU读写的缓冲格式,相比纹理,效率更高。缺点是readpix效率比较低
如果不需要作为纹理采样处理,可以使用Renderbuffer Object,比如深度、模板缓冲
参考 stackoverflow上的解释:What's the concept of and differences between Framebuffer and Renderbuffer in OpenGL?
- 看一个framebuffer的例子,理解framebuffer的流程
// 1. 创建帧缓冲,绑定
unsigned int framebuffer;
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
// 2. 创建纹理,设置参数,后面需要以它采样,用纹理的格式
// 生成纹理
unsigned int texColorBuffer;
glGenTextures(1, &texColorBuffer);
glBindTexture(GL_TEXTURE_2D, texColorBuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);
// 将它附加到当前绑定的帧缓冲对象
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texColorBuffer, 0);
// 3. 创建一个深度和模板渲染缓冲对象
unsigned int rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
// 4. 渲染缓冲对象附加到帧缓冲的深度和模板附件上
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
// 5. 检查帧缓冲
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
std::cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" << std::endl;
glBindFramebuffer(GL_FRAMEBUFFER, 0);
立方体贴图(天空盒)
- 原理及使用
和普通的纹理采样流程类似,区别在于
- 需要6张图片纹理
- 环绕方式多一个z方向,指两个面交接的地方如何拼接
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
- 片元着色器中 纹理坐标和2D纹理采样不同,改成3维度的向量,以一个向量方向来采样:
void main()
{
FragColor = texture(cubemap, textureDir);
}
- 纹理的绑定类型不同 GL_TEXTURE_CUBE_MAP
unsigned int textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);
- 天空盒的shader,片段着色器以顶点坐标作为方向采样
盒子中间是原点坐标,顶点的方向即天空盒上的采样方向
// 顶点着色器
void main()
{
TexCoords = aPos;
gl_Position = projection * view * vec4(aPos, 1.0);
}
// 片段着色器
#version 330 core
out vec4 FragColor;
in vec3 TexCoords;
uniform samplerCube skybox;
void main()
{
FragColor = texture(skybox, TexCoords);
}
- 将天空盒加入到场景中,有两种方法
- 先绘制天空盒,再绘制其他物体,这样天空盒成为背景。
注意绘制天空盒时,要禁用深度缓冲写入,否则天空盒前面会挡住其他物体
注意,游戏中天空盒一般不能随场景移动,禁止天空盒发生移动变换
// camera.GetViewMatrix():获取观察矩阵
// glm::mat3(camera.GetViewMatrix()) 去掉位移分量
// glm::mat4(... 外围填充上1和0
glm::mat4 view = glm::mat4(glm::mat3(camera.GetViewMatrix()));
-
优化,天空盒布满整个场景,效率较低,所有方向都会运行一次片段着色器。
改成先绘制其他物体,最后绘制天空盒。但需要改变天空盒的坐标计算,防止天空盒覆盖场景中的其他物体,改变z坐标为齐次坐标w,经过透视投影后,z = w/w = 1.0,天空盒永远在最远处
void main() { TexCoords = aPos; vec4 pos = projection * view * vec4(aPos, 1.0); gl_Position = pos.xyww; }
天空盒绘制,需要修改深度测试函数,默认是less,改成LEQUAL
// draw skybox as last glDepthFunc(GL_LEQUAL); // change depth function so depth test passes when values are equal to depth buffer's content skyboxShader.use(); view = glm::mat4(glm::mat3(camera.GetViewMatrix())); // remove translation from the view matrix skyboxShader.setMat4("view", view); skyboxShader.setMat4("projection", projection); // skybox cube glBindVertexArray(skyboxVAO); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture); glDrawArrays(GL_TRIANGLES, 0, 36); glBindVertexArray(0); glDepthFunc(GL_LESS); // set depth function back to default
- 反射与折射
实现反射、折射,需要在片段着色器中使用世界坐标系下的法线、顶点坐标
// 从顶点着色器中计算法线、顶点坐标
void main()
{
Normal = mat3(transpose(inverse(model))) * aNormal;
Position = vec3(model * vec4(aPos, 1.0));
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
// 片段着色器中使用
void main()
{
// 计算观察向量
vec3 I = normalize(Position - cameraPos);
// 根据观察向量和 法向量 计算反射向量
vec3 R = reflect(I, normalize(Normal));
// 根据反射向量,从天空盒采样器中取出颜色
FragColor = vec4(texture(skybox, R).rgb, 1.0);
}
折射效果修改一行代码,注意折射率
float ratio = 1.00 / 1.52;
vec3 I = normalize(Position - cameraPos);
vec3 R = refract(I, normalize(Normal), ratio);
- 反射
- 折射
高级数据,其他的顶点数据存取的方式
- glBufferSubData 普通的顶点数组缓冲
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
普通的顶点数据定义,是position texture Coords(纹理)/normal(法线)是交错定义的,通过步幅、起点来交叉读取顶点属性,复制给顶点属性赋值
使用分批的方式会更简单,与交错布局123123123123不同,我们将采用分批(Batched)的方式111122223333。
- glBufferSubData,分批填充顶点缓冲对象
// 将data填充到buffer中,从第24个字节开始填充
glBufferSubData(GL_ARRAY_BUFFER, 24, sizeof(data), &data)
不用glBufferSubData,也可以手动获取buffer的指针
float data[] = {
0.5f, 1.0f, -0.35f
...
};
glBindBuffer(GL_ARRAY_BUFFER, buffer);
// 获取指针
void *ptr = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
// 复制数据到内存
memcpy(ptr, data, sizeof(data));
// 记得告诉OpenGL我们不再需要这个指针了
glUnmapBuffer(GL_ARRAY_BUFFER);
注意:这样生成的buffer,使用起来和之前的没啥区别,只是数据存储的顺序不同,即步幅和起始点和之前的值不同
- glBufferSubData分批设置定点属性
分批或者整体设置定点属性,本质是一样的,看个人习惯
float positions[] = { ... };
float normals[] = { ... };
float tex[] = { ... };
// 填充缓冲
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(positions), &positions);
glBufferSubData(GL_ARRAY_BUFFER, sizeof(positions), sizeof(normals), &normals);
glBufferSubData(GL_ARRAY_BUFFER, sizeof(positions) + sizeof(normals), sizeof(tex), &tex);
// 设置定点属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), 0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)(sizeof(positions)));
glVertexAttribPointer(
2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)(sizeof(positions) + sizeof(normals)));
- 复制缓冲
使用分批的好处,是可以复用定点缓冲数据
每种类型的缓冲,GL一次只能绑定一个,作为当前状态。
即可以绑定不同类型的缓冲,同时作为当前状态
void glCopyBufferSubData(GLenum readtarget, GLenum writetarget, GLintptr readoffset,
GLintptr writeoffset, GLsizeiptr size);
复制缓冲数据
float vertexData[] = { ... };
glBindBuffer(GL_COPY_READ_BUFFER, vbo1);
glBindBuffer(GL_COPY_WRITE_BUFFER, vbo2);
glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, sizeof(vertexData));
或者
float vertexData[] = { ... };
glBindBuffer(GL_ARRAY_BUFFER, vbo1);
glBindBuffer(GL_COPY_WRITE_BUFFER, vbo2);
glCopyBufferSubData(GL_ARRAY_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, sizeof(vertexData));
高级GLSL
介绍一些特殊的GLSL语法使用
- GLSL内建变量
- 顶点着色器变量
- gl_PointSize,绘制点的时候有用
- gl_VertexID,当前点的索引(获取可以根据这个索引做特殊的渲染操作)
- 片段着色器变量
- gl_FragCoord(x、y屏幕空间坐标,z(0~1)表示深度)
void main()
{
if(gl_FragCoord.x < 400)
FragColor = vec4(1.0, 0.0, 0.0, 1.0);
else
FragColor = vec4(0.0, 1.0, 0.0, 1.0);
}
- gl_FrongFacing,判断正向还是背向
// 根据前脸还是后脸输出不同的纹理
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D frontTexture;
uniform sampler2D backTexture;
void main()
{
if(gl_FrontFacing)
FragColor = texture(frontTexture, TexCoords);
else
FragColor = texture(backTexture, TexCoords);
}
- gl_FragDepth
这个稍微复杂点,可以用来修改片段的Z坐标,即深度值
gl_FragDepth有个缺陷,会导致提前深度测试冲突禁用,原理很好理解,因为只要到真正片段着色器运行才知道实际的深度值,提前深度测试没有意义。从4.2版本开始有个折中的方案,定义gl_FragDepth变量的规则,提供给提前深度测试参考,做个粗略的评估
- 接口块,类似c语言中的结构体,减少变量声明
// vertex sheder
out VS_OUT
{
vec2 TexCoords;
} vs_out;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
vs_out.TexCoords = aTexCoords;
}
// fragment shader
in VS_OUT
{
vec2 TexCoords;
} fs_in;
uniform sampler2D texture;
void main()
{
FragColor = texture(texture, fs_in.TexCoords);
}
- Uniform缓冲对象、Uniform块布局
对相同的变量,设置为全局缓冲的uniform变量,比如view矩阵,每个shader都一样
有点复杂,暂时不展开了
几何着色器
作用,简单的顶点输入,生成复杂形状:
- 生成法向量、简单的头发等规则化的形状
- 爆炸效果
1.几何着色器的原理
(顶点着色器)输出顶点-->经过几何着色器-->输出新的顶点列表。即根据一个顶点属性,分裂出多组顶点
#version 330 core
layout (points) in;
// max_vertices表示总的输出点数
layout (line_strip, max_vertices = 2) out;
void main() {
gl_Position = gl_in[0].gl_Position + vec4(-0.1, 0.0, 0.0, 0.0);
EmitVertex();
gl_Position = gl_in[0].gl_Position + vec4( 0.1, 0.0, 0.0, 0.0);
EmitVertex();
EndPrimitive();
}
输入有多重图元类型,比如
- points:则接收的是一个点的数组
- lines:接收的是两个点的数组
- trinagles:接收的事三个点的数组
具体类型值为:
- 案例
实现代码
// 1. 声明4个顶点
float points[] = {
-0.5f, 0.5f, 1.0f, 0.0f, 0.0f, // 左上
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, // 右上
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, // 右下
-0.5f, -0.5f, 1.0f, 1.0f, 0.0f // 左下
};
// 2. 顶点着色器,没有特殊的,略..
// 3. 几何着色器,生成目标形状
fColor = gs_in[0].color; // gs_in[0] 因为只有一个输入顶点
gl_Position = position + vec4(-0.2, -0.2, 0.0, 0.0); // 1:左下
EmitVertex();
gl_Position = position + vec4( 0.2, -0.2, 0.0, 0.0); // 2:右下
EmitVertex();
gl_Position = position + vec4(-0.2, 0.2, 0.0, 0.0); // 3:左上
EmitVertex();
gl_Position = position + vec4( 0.2, 0.2, 0.0, 0.0); // 4:右上
EmitVertex();
gl_Position = position + vec4( 0.0, 0.4, 0.0, 0.0); // 5:顶部
EmitVertex();
// 可以同时修改每个点的颜色,不仅仅是坐标
// gl_Position = position + vec4( 0.0, 0.4, 0.0, 0.0); // 5:顶部
// fColor = vec3(1.0, 1.0, 1.0);
// EmitVertex();
EndPrimitive();
带颜色修改的效果:
- 爆破效果(爆炸成小块)、法向量显示
原理简述:
- 将每个三角形沿着法向偏移一个坐标,输入输出都是6个顶点
- 法向的计算要注意,从顶点获取的是view空间的法向(经过法向量矩阵),最后显示要在投影空间,
原理简述;
- 输入是每个三角形3个顶点,输出是线条-6个顶点
实例化
用于绘制大量相同物体,比如粒子、植被等
-
实例化原理 实例化可以减少drawcall次数,确切的说,是减少每次drallcall数据通信的时间。
与绘制顶点本身相比,使用glDrawArrays或glDrawElements函数告诉GPU会消耗更多的性能,
OpenGL在绘制顶点数据之前需要做很多准备工作(比如告诉GPU该从哪个缓冲读取数据,从哪寻找顶点属性,而且这些都是在相对缓慢的CPU到GPU总线(CPU to GPU Bus)上进行的)
实例化API基本差不多:
glBindVertexArray(quadVAO);
// 最后多了一个100,表示要实例化的数量
glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100);
vertex shader里,根据实例化id获取对应的参数 gl_InstanceID是内建变量
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;
out vec3 fColor;
uniform vec2 offsets[100];
void main()
{
vec2 offset = offsets[gl_InstanceID];
gl_Position = vec4(aPos + offset, 0.0, 1.0);
fColor = aColor;
}
- 实例化数组 glVertexAttribDivisor
解决uniform数量上限的问题,不使用gl_InstanceID,改用普通的顶点属性接收差异化的数据,比如偏移、旋转。
glEnableVertexAttribArray(2);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glVertexAttribDivisor(2, 1);
glVertexAttribDivisor(2, 1)意图:每调用一次vetex shader取两个值,每次更新一次顶点属性
同理:glVertexAttribDivisor(2, 10)表示每调用10次更新一次顶点属性
实例化数组可以避免使用gl_InstanceID,而且更灵活的控制实例的变化频率
其他案例,行星带
抗锯齿(Anti-Aliasing)
抗锯齿算法是很大一块内容,这里只是简单整理概念。
- 常见抗锯齿 所有的抗锯齿,本质上都是通过更多的采样做混合。常见的抗锯齿MSAA, SSAA, FXAA, TXAA。
-
SSAA(SuperSampling Anti-Aliasing,简称SSAA),用的不多,通过增加输出图像的分辨率实现,最后阶段再降采样压缩,内存、计算量都成倍增加
-
MSAA(MultiSampling Anti-Aliasing,简称MSAA),比较常用,内存占用和SSAA一样额外增加,颜色、模板、深度缓冲附件、顶点计算个数都成倍增加,但是在片段着色器环节只计算一次,通过对三角面片内子采样点赋值的方式,生成明暗自然过渡的边界。
MSAA对延迟渲染支持不太好,因为先过一遍光栅化,生成GBuffer,不直接做shading,所以没有子样本信息了,硬要做比较麻烦。
- FXAA(Fast Approximately -Aliasing,简称FXAA) FXAA,可以理解为后处理的方式,shader里提取边界,然后做模糊。
- 时间性抗锯齿(TXAA) 比较先进的抗锯齿方式,将采样均摊到每一帧上,然后结合之前的采样和当前的采样做混合。
弊端:
- 场景发生大的变化时无效,找不到对应点
- 要记录每帧的矩阵变换,以便找到相同点
- MSAA流程
- 开启msaa,一般默认会开,使用GLFW窗口框架已经封装好整个过程。
glfwWindowHint(GLFW_SAMPLES, 4);
- 离屏MSAA(自己处理整个流程) 代码参考:离屏MSAA"
核心代码:
// 1. 生成多重采样纹理附件,和普通的纹理类似
unsigned int framebuffer;
glGenFramebuffers(1, &framebuffer);
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, tex);
glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, GL_RGB, width, height, GL_TRUE);
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);
// 2. 作为附件,绑定到framebuffer
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, tex, 0);
// 3. 绑定多重采样渲染缓冲对象,用于模板、深度测试
glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_DEPTH24_STENCIL8, width, height);
// 4. 绘制
// 5. 将结果copy 到默认屏幕设备上
glBindFramebuffer(GL_READ_FRAMEBUFFER, multisampledFBO);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
注意glBlitFramebuffer的特殊用途
一个多重采样的图像包含比普通图像更多的信息,我们所要做的是缩小或者还原(Resolve)图像。多重采样帧缓冲的还原通常是通过glBlitFramebuffer来完成,它能够将一个帧缓冲中的某个区域复制到另一个帧缓冲中,并且将多重采样缓冲还原
- 自定义抗锯齿算法 不适用splitFramebuffer,而是在shader里接收多采样纹理,自己处理
uniform sampler2DMS screenTextureMS;
vec4 colorSample = texelFetch(screenTextureMS, TexCoords, 3); // 第4个子样本
注意,通过光栅化、片段着色器流程之后,color缓冲是4采样的。