shader 学习
前言
通过对GLSL 语言的学习,理解GPU 中shader的相关概念。
在学习 shader 之前需要去了解渲染管线。
渲染管线
所谓管线,其实就是一系列数据处理的步骤,每一步都会对自己这一步骤的输入数据进行某些处理,然后变成下一步骤的输入,最终变成帧缓冲里的颜色数据,再输出到显示器上就得到我们想要绘制的图像。
流程:
- 顶点数据的准备。顶点一般是3维的点坐标组成(x,y,z)。利用VAO 、VBO将数据传入到GPU中。
// 三个顶点的坐标
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
- 顶点着色器。对一些顶点属性(数据)的基本处理。其中包括坐标系的转换(三维变换),将三维空间中的坐标点(x,y,z),转化为手机屏幕空间的上的坐标点(物体坐标系→世界坐标系→观察坐标系→裁剪坐标系->屏幕坐标系)。
在坐标系的转换过程中会借助MVP矩阵完成转换。MVP矩阵分别是模型(Model)、观察(View)、投影(Projection)三个矩阵。
gl_Position = projection * view * model * vec4(x,y,z, 1.0);
//为什么反过来相乘?
知识点:
- 在空间中一个顶点的位置可以用一个向量来表示。
- 矩阵分为行优先矩阵【一行一行的读取矩阵】和列优先矩阵【一列一列读取矩阵】,这个是存储的方式。通过矩阵转置可以互相转换,OpenGL使用的是列优先矩阵。
- 数学角度的矩阵计算,是从左向右计算。OpenGL角度是从右向左。
参考: https://zhuanlan.zhihu.com/p/549145859
模型(Model):以物体的正中心为原点的坐标系 转换为 以世界为中心的世界坐标系。
观察(View):由世界坐标系转为以摄像机为原点的观察坐标系。
投影(Projection):由观察坐标系进行投影得到裁剪坐标系。
顶点着色器的输出的顶点都在裁剪空间内(NDC坐标)。随后在第5阶段,OpenGL将会自动进行透视除法和裁剪。将其变化为标准化坐标(NDC 坐标)。OpenGL通过视口变换 使用glViewport 函数所定义的坐标范围。将NDC 坐标映射到屏幕坐标。
NDC坐标:https://blog.csdn.net/hitzsf/article/details/128600861
- 裁剪坐标变换为屏幕坐标,我们将使用一个叫做视口变换的过程。视口变换将位于-1.0到1.0范围的坐标变换到由glViewport函数所定义的坐标范围内。最后变换出来的坐标将会送到光栅器,将其转化为片段。
例如:屏幕坐标:范围【0-ScreenWidth】【0-ScreenHeight】,OpenGL是以左下角为中心。
顶点着色器的代码流程会针对每个顶点执行一次。
- 图元装配。将所有的顶点数据,根据顺序 ,装配成指定图元的形状。例如,设置的图元为三角形,3个点,逆时针完成三角形构建。
- 几何着色器(Geometry Shader):把基本图元形式的顶点的几何作为输入,可以通过产生新顶点构造出新的基本图元来生成其它形状。
- 剪裁剔除操作,视口变换,OpenGL将会自动进行透视除法和裁剪。将其变化为标准化坐标(NDC 坐标)。在通过视口变换 使用glViewport 函数所定义的坐标范围。将NDC 坐标映射到屏幕坐标。视口外面,无法显示的图元都会舍弃。背面朝向我们的三角形剔除掉。加快后续步骤的渲染效率。
逆时针相连的多边形为正面,顺时针相连的为负面
- 光栅化(Rasterization):即像素化。将顶点构建的区域计算出来,用插值的方式映射为屏幕上网格的像素点,生成供片段着色器处理的片段(Fragment),
- 片元着色器(Fragment Shader):主要作用是计算出图元每一个像素点最终的颜色,通常片段着色器会包含3D场景的一些额外的数据,如光线、阴影等。
- 测试与混合:是对每个像素点进行深度测试,Alpha测试等测试并进行颜色混合的操作,这些测试与混合操作决定了屏幕视窗上每个像素点最终的颜色以及透明度。
在整个渲染管线中需要自定义处理的主要是顶点着色器和片段着色器。
一、GLSL 的基础概念
GLSL 语言用于 opengl 的着色器编写。着色器代码主要分成2个部分:Vertex(顶点着色器)和Fragment(片段着色器),有时还会有Geometry Shader(几何着色器)。
顶点着色器
从 GPU 存储上获取顶点信息 传入到顶点着色器。
执行的任务:
- 用模型矩阵、视图矩阵和投影矩阵变换顶点位置 。
- 法向量的变换和规一化 。
- 纹理坐标生成和变换 。
- 逐顶点的光照计算。
输出:
顶点着色器的输出数据(NDC坐标),包括顶点的位置和其它属性,被传递给图元装配,光栅化阶段。
整个顶点着色器执行的次数: 有多少个顶点就运行几次 。(3个顶点就是3次)
也就是说,会输出3个gl_Position 。
示例:
#version 330 core // 是指使用OpenGL 3.3对应的GLSL版本。https://www.cnblogs.com/OctoptusLian/p/9909170.html
layout (location = 0) in vec3 aPosition; //输入的顶点坐标
layout (location = 1) in vec3 aColor; //输入的顶点的颜色
out vec3 vColor; //输出的顶点颜色
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main() {
gl_Position = projection * view * model * vec4(aPosition, 1.0); //计算经过变换后的顶点位置
vColor = aColor; //将输入的顶点颜色直接传递给输出。
}
//
in :声明了两个输入属性,分别用于接收顶点的位置和颜色
out :声明了一个输出变量,用于将顶点的颜色传递给片段着色器。
uniform :声明了3个全局变量, 顶点着色器和片段着色器之间共享uniform变量
gl_Position: 是GLSL内部的关键字
片元着色器
片段着色器是通过对光栅化产生的片段数据的处理,产生一个颜色集合和一个深度信息。
输入:需要强调的是,片段着色器的输入,是光栅化后处理的后图元像素信息(距离,绘制三角形,那么片段着色器的输入为 由顶点插值形成的图元信息,也就是局部三角形。
这里的输入数据采用的是屏幕坐标系 ,原点为左下角。如果在android 中运行,则原点为左上角。
整个片元着色器执行的次数: 每个像素执行一次。注意,这里不是指整个屏幕的分辨率大小,而是指光栅化后形成的图形像素个数。
例如屏幕大小是600 *800 ,绘制一个200x200 左右大小的正方形,片元着色器执行的次数就是 200x200次。
#version 330 core
out vec4 fragColor;
in vec4 vColor ; //从顶点着色器中传过来的颜色
void main(){
//fragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
fragColor = vColor ;
}
二、内置变量
参考:https://zhuanlan.zhihu.com/p/349296191
顶点着色器内建特殊变量
- gl_VertexID : 输入变量,我们只能对它进行读取
整型变量gl_VertexID储存了正在绘制顶点的当前ID。当(使用glDrawElements)进行索引渲染的时候,这个变量会存储正在绘制顶点的当前索引。当(使用glDrawArrays)不使用索引进行绘制的时候,这个变量会储存从渲染调用开始的已处理顶点数量。 - gl_InstanceID: 是一个输入变量。
用于保存实例化绘图调用中图元的实例编号。对于常规的绘图调用,该值为0;gl_InstanceID是一个整数型变量,用highp精度限定修饰符声明。 - gl_Position: 是一个输出变量,负责向后续阶段输出顶点位置的处理结果
片元着色器内建特殊变量
- gl_FragCoord:片元着色器当前正在处理的像素坐标信息。
是一个 vec4 类型的变量 (x, y, z, 1/w),这里的坐标系是屏幕坐标系,以窗口左下角为原点。
有时候我们需要在 shader 内反算 观察坐标系 或 世界坐标系内的坐标。
n
d
c
.
x
y
z
w
=
(
g
l
F
r
a
g
C
o
o
r
d
.
x
y
/
v
i
e
w
p
o
r
t
.
w
h
∗
2.0
−
1.0
,
g
l
F
r
a
g
C
o
o
r
d
.
z
∗
2.0
−
1.0
,
1.0
)
ndc.xyzw = (gl_FragCoord.xy/viewport.wh * 2.0 - 1.0, gl_FragCoord.z * 2.0 - 1.0, 1.0 )
ndc.xyzw=(glFragCoord.xy/viewport.wh∗2.0−1.0,glFragCoord.z∗2.0−1.0,1.0)
这样我们只需向shader 中传入 矩阵信息 , 就可以获得该片元在指定空间内的坐标 ,例如
e
y
e
.
x
y
z
w
=
p
r
o
j
e
c
t
i
o
n
M
a
t
r
i
x
I
n
v
e
r
s
e
∗
n
d
c
.
x
y
z
w
eye.xyzw = projectionMatrixInverse * ndc.xyzw
eye.xyzw=projectionMatrixInverse∗ndc.xyzw
w
o
r
l
d
.
x
y
z
w
=
m
o
d
e
l
V
i
e
w
P
r
o
j
e
c
t
i
o
n
M
a
t
r
i
x
I
n
v
e
r
s
e
∗
n
d
c
.
x
y
z
w
world.xyzw = modelViewProjectionMatrixInverse * ndc.xyzw
world.xyzw=modelViewProjectionMatrixInverse∗ndc.xyzw
原文链接:https://blog.csdn.net/fatcat123/article/details/103861403/
坐标系
通常顶点着色器使用的是NDC 坐标系
片段着色器中 使用的是屏幕坐标系
如果想将顶点信息传到片段着色器中,需要在顶点着色器中进行坐标转换
#version 330 core
layout (location = 0) in vec2 inPosition;
out vec2 texCoord;
void main()
{
texCoord = inPosition * 0.5 + 0.5; // 将顶点坐标映射到 [0, 1] 范围
gl_Position = vec4(inPosition, 0.0, 1.0);
}