GLSL基础(上)(OpenGL Shading Language)
——内容来自Jacobo Rodriguez《GLSL Essentials》,摘取自其中内容。关于GLSL语言部分主要是第二章内容,如果只想了解GLSL语言本身语法,可以跳到第二章GLSL语言基础部分。(注:这个glsl总结是个人开发过程中记录的,目前的主流shader应该去看Vulkan或Directx 12 (推荐FRANK D.LUNA的书,有中译版),还有Unity相关的《Unity shader入门精要》)第一章介绍渲染管线,以及整个编程阶段的概览。
第二章介绍GLSL基本的语言类型,对向量操作,流控制,预处理器,着色器输入和输出。
第三章介绍顶点着色器,顶点的可编程部分,一致变量和基本光源,以及顶点着色器的案例。
第四章介绍片段着色器,着重于可操作模型,输入输出,以及相关案例。
第五章介绍几何着色器,着重于几何体的着色器结构,接口块以及相关案例。
第六章介绍了计算着色器,包括可执行模型,GPGPU(General-Purpose computation on GPUs)基础,纹理渲染,基本加工计算。
(上篇主要是第一章和第二章的内容,下篇介绍三四五六章,三四五六建议学习具体OpenGL API,当下更流行Vulkan和DirectX)
第一章图形渲染管线:
开始写GLSL之前,我们要知道我们的Shader代码是运行在GPU之上的,传统的编程运行是使用CPU架构,有一个ALU,寄存器,IO设备,程序基本是串行运行的,一个指令接一个指令。而GPU架构与此不同,GPU是高度并行化的架构。
Shader程序运行在大规模的并行环境中,I/O处理完全不同,程序会生成百上千个实例,运行在成百个真实的硬件线程中。
图形硬件(GPU)的历史:
Graphics hardware(也叫做图形卡或者GPU),GPU的架构仍在不断发展。运行在其中的数据也需要按照某种方式和顺序进行处理执行,这一处理流程叫图形渲染管线。它就像是一个流水线。我们将数据送到一端——顶点,纹理,着色器——然后数据在执行非常精密的运算机器中经过处理最终输出到另一端:最终的渲染结果。早期的OpenGL中,图形渲染管线是完全固定的,输入的数据经历了相同的运算操作,这是早期的渲染器(2002年之前)。下图是一个早期的固定渲染管线的表示(摘自原pdf):
在2002年和2004年之间,GPU中实现了一部分的可编程性,这些代替了原来的固定部分。
第一代的着色器中,图形程序员可以编写伪汇编代码,并且这些着色器都是平台专用的。事实上,程序员不得不为每一种图形硬件供应商都至少编写一个不同的着色器,因为它们的汇编代码各不相同。但至少它们都够替代更低一级的旧版固定管线。不过这仅仅只是实时图形编程历史巨大变革的一个开始。
一些公司为程序员提供了更高级的编程语言解决方案,比如说NVidia的Cg和Microsoft的HLSL,但这些语言都不能够实现跨平台。Cg只对NVidia的GPU有效,HLSL只能支持Direct3D。
在2004年间,一些公司意识到了需要更高级的着色器语言,要能够在不同平台上通用,并且一定程度上作为Shader Programming的标准。因此GLSL(OpenGL Shading Language)诞生了,它让程序员能够使用一个独特的类似C语言的着色器语言替代它们五花八门的汇编代码,并且能支持所有硬件厂商。
在2004年,固定管线中只有两个部分能够被替代,顶点处理单元(主要处理几何变换和光照)和片段处理单元(主要是将颜色绘制到像素上)。这些新加的可编程处理单元被称为顶点着色器和片段着色器。之后又增加了两个新的部分:几何着色器和计算着色器,这两个新的部分分别是2008年和2012年加入到官方的OpenGL规范当中。
对比和之前所示结构的不同,新的管线结构如下(来自原书):
图形渲染管线
对应于上述可编程管线结构的内容,我们先来描述数据经过图形管线中各个部分经历了哪些操作。
Geometry stages(per-vertex operations)左边几何处理阶段(逐顶点操作)
这一部分关注于顶点数据从初始状态(模型坐标系下)变换到最终状态(视图坐标系下的顶点):
表中名称具体内容顶点数据(vertex data):是整个流程的输入数据。这里我们为管线送入我们的几何体的全部向量化数据,包括顶点,法线,索引,切线 ,副法线,纹理坐标等等。纹理(Textures):随着着色器的出现,为顶点阶段增加这一输入变得可行。为了让我们的渲染器色彩丰富,我们可以将纹理作为顶点和几何着色器的输入,比如说将顶点存储的值替换为纹理值(可以使用纹理映射技术实现)。顶点着色器(Vertex shader):这一系统负责顶点从本地保存坐标到裁剪空间下坐标的变换(transformation),主要是通过不同的变换矩阵实现(MVP变换model,view和projection)。裁剪(Clipping):当顶点被变换到裁剪空间后,我们很容易确定哪些点落在裁剪空间内(规定可视的场景空间),对于落在裁剪空间之外的点可以丢弃不考虑,这是简化计算的一个重要方式。透视除法(Perspective division):这一操作间我们的可视体(一个视锥金字塔,通常叫作frustum截锥体)转化到一个规则的单位化的立方体中。(对于正交投影不需要使用正交除法)视口变换(Viewport transform):裁切体(最后的归一化立方体)的近平面被平移放缩到对应的视口坐标系。这意味着将顶点坐标映射到视口坐标系下(通常指的是我们的屏幕或者我们的视窗)所有数据经过光栅化器(流程图中间的RASTERIZER):这一步是将我们左边输出的向量化数据(主要是顶点)变为离散化的表示(帧缓冲区framebuffer数据的表示形式)。离散表示用于之后几步的处理。Fragment stages(per-fragment operations)右边片段处理阶段(逐片段处理)
这里我们的输入已经从向量化数据转换为离散数据,等待进行光栅化。最里面的多层子块表明对片段数据会如何处理,输出以离散数据的形式表达:
图中名称具体内容片段着色器(Fragment shader)在这一步骤中会对纹理,颜色,光照进行计算实现和组合。最终叠加各种属性形成片段。片段后处理(Post fragment processing)在这一步实现了混合,深度测试,剪裁测试,alpha测试等等。之前产生的片段会在这一阶段进行组合,测试,丢弃并最终被写入帧缓冲区(framebuffer)External stages 额外的阶段
在图表中逐顶点和逐片段之外的部分还有计算着色器(compute shader)阶段。这一步中可以进行编程来影响其它部分的结果。
要知道可编程管线是在固定管线的基础上形成的。着色器只是取代了之前几个固定化的模块,所以管线的概念并没有变化太多。
在顶点着色器中,它们替代了变换和光照模块,现在我们要编程实现相关功能。我们只要注意到每个模块输出数据的格式要符合下一个模块的输入。你可以通过打印出顶点在裁剪坐标系下的坐标来确定自己实现的结果。
在片段着色器中,固定的管线用一种受限的方式结合纹理。如今片段着色器的输出结果是一个片段,每个片段最终都可能被画在像素上,所以它们最简单的表示是RGBA值。为了将片段着色器和后面的管线模块联系起来,你的输出结果应该表示颜色,但你可以按你自己的方式计算颜色值。
片段着色器中输出的是颜色值,对于framebuffer,其它需要的数据包括光栅化后的位置和深度,所以才需要进一步的深度测试和裁剪测试。在当前的光栅化位置对所有的片段都处理完毕后,最终保留的颜色值就是光栅化位置的像素值。
几何着色器和计算着色器是可选的:
几何着色器:放置在顶点着色器之后,裁剪操作之前。这一着色器的目标是基于输入的顶点产生新的基元(not vertices!比如说网格Mesh,voxel,point cloud等等其它都有可能)。
计算着色器:这是一个补充模块。和其它的几个着色器完全不同,因为它会对整个管线产生影响。它的主要目的是为通用GPGPU提供一个方法,和图形学关系不太大。它有点像OpenCL,但是对于图形学程序员来说更方便,因为它集成到了整个管线上。在具体的实例中,它们可以用于图片的变换,或者用于实现比OpenCL更高效的延迟渲染。
几种着色器类型
四种:之前已经提到的:顶点着色器,片段着色器,几何着色器,计算着色器。
对应于后续的三四五六章。
GPU:向量化,并行化的架构
如果你编写过CPU的软光栅处理程序,你会发现它的处理效果很差。即使使用目前最新的向量化指令集(比如SSE3的加速CPU向量运算)或者使用多线程方式,它们的表现都比GPU要差。CPU并不擅长处理大量的像素。比起CPU串行的执行指令,GPU有很多的微小的专用计算核心,同时GPU最基本的数据存储类型也是向量化的,所以可以高度并行化的处理特定的矢量数据,可以说就是专门为向量化运算而生的。
在CPU里的数据结构如下:
struct Vector3
{
float x,y,z;
}
// 计算两个向量的叉乘如下:
vec3 a;
vec3 b = {1,2,3};
vec3 c = {1,1,1};
// a = cross(b,c);
a.x = (b.y*c.z) - (b.z*c.y);
a.y = (b.z*c.x) - (b.x*c.z);
a.z = (b.x*c.y) - (b.y*c.x);CPU需要进行6次乘法指令和三次加法指令完成叉乘运算。而在GPU中,vec3类型就好像CPU中的浮点类型或者说int类型。对基本类型的操作也都是直接进行的:
// GPU中
vec3 b = vec3(1,2,3);
vec3 c = vec3(1,1,1);
vec3 a = cross(b,c);对于4*4的矩阵,CPU要完成相应的操作更加复杂,而对于GPU而言,对应的操作并没有变得更复杂,只是一次操作就可以完成。在GPU中还有很多内置的操作:加减,点乘,内乘,外乘,几何运算,三角函数或者指数函数。所有的这些内嵌的操作都可以直接映射到图形硬件上,因此与CPU上进行运算相比花销只是很小的一部分。为什么使用GPU可以比CPU更快的完成向量计算和图形运算:
第一:很多着色器中的的操作可以被同时执行
第二:在着色器内部,原先在CPU下的许多指令可以在一个块中完成。
着色器环境
以前的很多应用都是被编译生成运行在CPU之上的,这意味着程序的编译器是将高级语言编译成CPU能理解的语言。不管使用的编程语言是什么,最终都会被编译器编译成CPU能处理的语言。
而Shader有一点不同,因为它们是专用于图形学的,所有运行和如下两点密切相关:
首先,它们需要运行在显卡之上,只有显卡才有运行它们的处理器,这一特殊的处理单元被叫做GPU(Graphics Processing Unit图形处理单元)。
第二,需要构建一系列软件来获取GPU硬件资源:GPU驱动。
要编写Shader,首先要做的就是构建开发环境,包括下载显卡驱动,保持更新显卡驱动。现在假定你已经写好了第一个shader。你应该编译并把它送到GPU去执行。因为GLSL依赖于OpenGL,所以你必须使用OpenGL来编译执行你的Shader。OpenGL有专门用于Shader编译的API调用:link, execution and debug(链接,执行,调试)。现在你可以把OpenGL程序看作一个主程序,用来管理你的Shader和其它资源(比如纹理,顶点,法线,帧缓冲数据,渲染状态等)。
第二章GLSL基础:
主要介绍GLSL语言的细节,包括语法(grammar)和语义(syntax)的细节。
GLSL是基于ANSI C构建的。许多C编程语言的特性都被引入到GLSL,同时删除了很多不利于性能和语言简洁性的特性。
所有的GLSL 着色器类型使用相同的语言。这一章所学的语言基础在之后的章节中都会用到。这一章会介绍该语言的基础以及不同Shader类型之间公有的元素。主要介绍如下主题:
[*]语言基础。
[*]着色器的 输入/输出 变量。
因为GLSL非常类似于C语言,所以这里不会列举全部规则,概述介绍基本和规则,主要关注它和C语言不同的地方,剩余的部分基本是互通的。它们最大的不同,也是让很多读者开心的一点是,GLSL中没有指针(嘿嘿)。
GLSL语言:
在编写shader时,必须记住一个最重要的事情,你打算编写哪个版本的GLSL代码。这往往是为你的程序决定的最小要求。GLSL的版本往往受限于具体的OpenGL版本。所以为了未来需要,我们也要关注GLSL版本支持的最小OpenGL版本。
本书基于GLSL4.30.6,具体的细节可以通过OpenGL的官网了解https://www.opengl.org/registry/doc/GLSLangSpec.4.30.8.pdf
我们只关注GLSL的核心配置文件,前向兼容。核心配置文件前向兼容保证了弃置元素不再可用。不鼓励使用该语言中仍旧保留的OpenGL的显示列表和即时绘制机制。核心配置文件前向兼容的使用避免了出现编译运行错误。
在开始之前,希望你对C语言有基本的理解和运用。在其它语言比如Java,Python或者C#中都支持OpenGL。我们主要关注C/GLSL的概念。
这一小节介绍类C语言的GLSL语言基础,按照如下的目录进行介绍
基础指令基本数据类型(重点)变量的初始化(重点)向量和矩阵的操作(重点)类型转换和值转换:代码注释流程控制(if和switch)循环(for,while,do while)结构体数组函数预处理器(预编译头和预处理)(重点)基础指令:
所有的指令都以分号结尾,并且每一行可以放置多条指令。
c = cross(a,b);
vec4 g; g = vec4(1,1,1,1);通过使用花括号可以将指令放置在块中。在块中声明的变量生命周期也就是整个块。如果两个变量有相同的变量名(一个在块中,另一个在块内)——那么默认地,我们会引用块内的变量,这一点在其它语言中也都一样(变量的生命周期和优先级问题)。
float a = 1.0;
float b = 2.0;
{
float a = 4.0;
float c = a + 1.0; // c-> 4.0 + 1.0
}
b = b + c // 错误,因为不存在变量c制表符和空格不影响语言的语义,可以使用它们来对齐代码格式。
基本类型(重点)
GLSL有非常丰富的基本类型,除了标准的C类型,还增加了其它的类型——主要是向量的表示和基于GPU架构的拓展类型。
数据类型取值解释booltrue/false布尔类型,基本的C类型intint有符号整数有符号intuint无符号整数无符号uintsamplersampler1D表示纹理的数据类型sampler2Dsampler3Dfloatfloat/ double实数浮点数 / 双精度浮点数Vectors:bvec2,bvec3,bvec4元素是bool型的向量ivec2,ivec3,ivec4元素是int型的向量uvec2,uvec3,uvec4元素是uint型的向量vec2,vec3,vec4元素是float型的向量dvec2,dvec3,dvec4元素是double型的向量Matrices注意matrice的表示穷尽了4阶以下的矩阵的情况矩阵总是float浮点类型的,加d表示double型精度mat2,mat3,mat4对应的n阶方阵,floatdmat2.dmat3,dmat4对应的n阶方阵,doublemat2x3,mat2x4,mat3x2对应的2*3,2*4矩阵 ...mat3x4,mat4x2,mat4x3.... 用行数x列数表示dmat2x3,dmat4x2,dmat4x3....用行数x列数表示,double。备注:注意在C/C++中浮点数表示:1.5 , 1.5231, 1.87213f, 1.23Ffloat ,有小数点或加f, F1.52 lf,1.521341 lFdouble,需要加 lf , lF变量的初始化(重点):
如下给出了几种变量初始化的案例
//类似与c语言变量定义和初始化
float a = 1.0;
bool switch = false;
ivec3 a = ivec3(1,2,3);
uvec3 a = uvec2(-1,2); // error , 因为是无符号类型
vec3 a(1.0,2.9); // error,不能使用C++构造函数的方式,要用相应容器进行初始化
vec3 a = vec3(1.9, 2.9); // ok ,注意到参数数量不对应也是ok的我们可以使用很多容器进行初始化,同时支持类型转换,可以从vec3和float中构建vec4,或者使用两个vec2,或者使用float和vec3,多种方式都可行:
// 不同的vec4初始化方式
vec4 a = vec4(1.0,vec3(0.0,1.0,0.0));
vec4 a = vec4(vec3(0.0,1.0,0.0),0.9);
vec4 a = vec4(vec2(1.0,1.0),vec2(0.5,0.5));向量可以看作一种数据结构或者说看作数组,一个向量的结构体在语言本身中进行了预定义,数组的大小只需要通过变量名就可以知道(比如vec3就是3维数组)。
对于向量而言,下面几种结构体的名称都是有效的(这三组名称是同义的)(注意命名集合不能混合使用,下面有案例)
[*] {x,y,z,w}在获取表示位置的向量时有效(齐次坐标)
[*] {r,g,b,a}在获取表示颜色的向量时有效(RGBA四通道颜色)
[*] {s,t,p,q}在获取表示纹理坐标的向量时有效(二维纹理使用s,t用于表示平面,目前二维纹理平面为了避免s,t的歧义用u,v)
借助于此,我们也可以使用结构体下标法为向量赋值。
vec2 p;
p = 1.0;
p.x = 1.0;
p.y = 2.0;
p.z = 3.0; // 非法,因为p是二维向量,没有z分量GLSL可以对向量组成成分进行复制或重排,类似案例如下:
vec4 color1 = vec4(0.5,0.2,1.0,1.0)//定义一个RGBA的颜色值
// 将颜色转换为abgr(列位置倒序重排)
vec4 color2 = color1.abgr; // 等价于 color1.wzyx
//基于rgb中的red分量构建灰度图
vec4 redGray = color1.rrrr;
float red = color1.r;
// 下面随即地进行一系列有效排序
vec4 color3 = color1.gbgb;
vec4 color4 = vec4(color1.rr,color2.bb) // .rr , .bb 都是vec2
vec4 color5 = color1.tptp; // 类似于.gbgb ,使用了stpq的描述
vec4 color6 = color1.yzyz; // 类似于.gbgb , 使用了xyzw的描述
color6.xy = vec2(1.0,1.0);
color6 = 2.0;
vec2 p;
// 下面是一些无效的变换
p = color1.rgb; // .rgb是vec3,p是vec2
p.xyz = color.rgb; // p没有.xyz
p = 3.0; // 索引越界,p的索引下标为0,1
vec4 color7 = color1.xxqq // 非法表示,一次只能使用一种命名集,不能混合使用(注意这一点)向量和矩阵的操作(重点):
(记住一般的运算符都是按照线性代数的运算规则进行运算)
对于矩阵和向量的一些算数运算符已经进行了重载,以匹配相关的线性代数运算。几乎所有重载的操作都是对应分量进行操作,除了矩阵乘矩阵和矩阵乘向量根据线性代数中的运算规则进行了转换:
mat R,T,M;
vec3 v,b;
float f;
// 初始化 f, b, v, R 和 T ,具体代码略
//...
// 对应分量操作的案例
b = v + f;
/* 这一操作等价于
b.x = v.x + f;
b.y = v.y + f;
b.z = v.z + f;
在加减操作包括向量点乘操作中,都是这样的运算方式
*/
// 矩阵乘以向量,线性代数中的运算规则
b = T * v;
/* 操作等价于:其中T, T , T分别表示矩阵对应列的列向量
b.x = T.x * v.x + T.x * v.y + T.x * v.z;
b.y = T.y * v.x + T.y * v.y + T.y * v.z;
b.z = T.z * v.x + T.z * v.y + T.z * v.z;
*/
M = T * R // 同理也是矩阵乘法运算,不再列出正如图形学中所学的一样,要对向量施加对应的变换,只需要相应的矩阵后乘向量。
vec4 v,b; //齐次坐标下的向量v和b
vec4 vertex_position,vertex; // 齐次坐标下的顶点位置坐标
b = T * v; // 平移变换
b = R * T * v; // 先平移再旋转,仿射变换
vertex = projection * modelview * vertex_position; // 对顶点进行MVP变换类型转换和值转换:
类型转换和值转换要在有意义的条件下(精度不发生错误问题)进行,我们可以将隐式的将int转化为uint,将int转化为float,将float转化为double等等。和C语言中的转换类似,注意GLSL中没有字符型变量。
float threshold = 0.5;
int a = int(threshold); // 小数部分丢弃, 最终 a 等于 0
double value = 0.3341lf;
float value2 = float(value);
bool c = false ;
value2 = float(c) ; // value2等于0.0
c = true;
value2 = float(c); // value2等于1.0代码注释
前面已经使用了很多次代码注释了,和C/C++中的注释方式相同,使用 // 或者 / /
// 这是单行注释
/*
这是
多行
块注释
*/流程控制(if和switch)
实现方式和C语言中相同,有if表达式和switch表达式两种流控制方式。 具体语法如下。
在if - else if - else 语法结构中, 在() 内编写条件判别表达式 ,对于多行的执行语句,使用{}放在代码块中:
if( a > threshold ){
lightColor = vec4(1.0,1.0,1.0,1.0);
baseColor = vec4(1.0,0.0,0.0,1.0);
}
else if ( a == threshold){
lightColor = vec4(0.5,0.5,0.5,0.5);
baseColor = vec4(0.5,0.0,0.0,0.5);
}
else
lightColor = vec4(0.0,0.0,0.0,0.0);在switch-case结构中,举例如下,注意需要break跳出,否则会顺序依次执行每个case内容:
switch(myVariable){
case 1:// myVariable值为1时跳入
lightColor = vec4(0.0,1.0,0.0,1.0);
break;
case 2:// myVariable值为2时跳入
lightColor = vec4(1.0,0.0,0.0,1.0);
break;
case 3:// myVariable值为3时跳入
lightColor = vec4(0.0,0.0,0.0,1.0);
break;
default: // 其它情况跳入
lightColor = vec4(1.0,1.0,1.0,1.0);
}循环(for,while, do while)
和C/C++语言中一致,有三种表达式,for,while和do while。
for(定义初始值;循环判别条件;循环值变化){循环体}
while(循环判别条件){循环体}
do{循环体}while(循环判别条件)
不做详细介绍,给出代码示例:
// for
vec3 myVariable=vec3(1.0,1.0,1.0);
for(int i=0;i<10;i++){
myVariable = myVariable + float(i);
}
// 判别通过i = 0,1,2,3,4,5,6,7,8,9,i=10不通过
// while
vec3 myVariable=vec3(1.0,1.0,1.0);
int i = 0;
while(i<10){
myVariable = myVariable + float(i);
i++;
}
// 判别通过i = 0,1,2,3,4,5,6,7,8,9,i=10不通过
// do while,注意会先执行一次代码,在判断循环条件。
vec3 myVariable=vec3(1.0,1.0,1.0);
int i = 0;
do{
myVariable = myVariable + float(i);
i++;
}while(i<10)
// 判别通过i= 1,2,3,4,5,6,7,8,9 i=10不通过退出。由于i=1之前已经执行过一次,所以总的执行次数还是10次。结构体
类似于C语言中进行结构体定义的方式,按照如下方式就定义了一个结构体。
struct MySurface
{
vec3 baseTextureColor; // RGB color
float roughness;
vec3 tint; // RGB Color
}可以直接使用结构体的名称来创建结构体类型的变量
// 使用下标索引方法进行赋值
MySurface tableSurface;
tableSurface.roughness = 2.0;
// 使用容器初始化方式进行赋值
MySurface surface = MySurface(vec3(1.0,1.0,1.0),0.5,vec3(1.0,1.0,1.0));数组
类似于C语言中创建静态数组的方式,在GLSL中,我们也可以创建结构体类型或者vec类型的数组,并且数组和向量都具有.length()属性。具体的例子如下。
float coefficient;
MySurface surfaces;
for(int i=0;i<4;i++){
//注意索引,超出声明的限制就会发生运行错误
coefficient = i*2.0;
}
for(int i=0;i<3;i++){
surfaces.roughness = i + 1.2;
}
// 注意数组可以使用.lengh()方法获得长度,这是对数组和向量都适用的一个实行
for(int i=0;i<cofficient.length();++i){
//do whatever
coefficient = i*2.0;
}
// 数组初始化以及创建向量类型数组
float myArray[] = {1.0,2.0,3.0,4.0};
vec2 myArray2[] = {vec2(0.0,1.0),vec2(1.0,0.0),vec2(1.0,1.0)}函数
类似于其它语言,在GLSL中也有函数,和C语言中的函数基本完全一致,同时对于GLSL程序源码,往往也需要一个主函数即main()函数。 一段包含主函数的代码示例如下,之后我们介绍GLSL中的函数形式。
// 结构体声明,Light结构体
struct Light
{
vec3 position;
vec3 diffuseColor;
vec3 attenuation;
};
// 类似C语言的主函数,Shader的入口,注意返回值类型是void,
// 返回值和C语言返回int ,return 0有所不同
void main()
{
vec3 myPosition = vec3(1.0, 0.0, 0.0);
// Let&#39;s create and initialize some ligthts
Light light1 = Light(vec3(10.0, 0.0, 0.0), vec3(1.0, 0.0, 0.0),
vec3(1.0, 2.0, 3.0));
Light light2 = Light(vec3(0.0, 10.0, 0.0), vec3(1.0, 0.0, 0.0) ,
vec3(1.0, 2.0, 3.0));
Light light3 = Light(vec3(0.0, 0.0, 10.0), vec3(1.0, 0.0, 0.0) ,
vec3(1.0, 2.0, 3.0));
// Calculate simplified light contribution and add to final color
vec3 finalColor = vec3(0.0, 0.0, 0.0);
//distance is a GLSL built-in function
float distance1 = distance(myPosition, light1.position);
float attenuation1 = 1.0 / (light1.attenuation + light1.
attenuation * distance1 + light1.attenuation *
distance1 * distance1);
finalColor += light1.diffuseColor * light1.attenuation;
// Let&#39;s calculate the same, for light2
float distance2 = distance(myPosition, light2.position);
float attenuation2 = 1.0 / (light2.attenuation + light2.
attenuation * distance2 + light2.attenuation *
distance2 * distance2);
finalColor += light2.diffuseColor * light2.attenuation;
// Light 3
float distance3 = distance(myPosition, light3.position);
float attenuation3 = 1.0 / (light3.attenuation + light3.
attenuation * distance3 + light3.attenuation * distance3 *
distance3);
finalColor += light3.diffuseColor * light3.attenuation;
// Now finalColor stores our desired color
}这是一个一般的Shader的代码,同时可以看到在它的主函数中,有三段代码是重复的,我们可以使用函数的方式来处理重复代码。注意到我们的重复代码需要的输入和最终输出的值,将它们分别作为函数的输入参数和返回值。调整后的代码如下,注意函数的写法和C语言中非常相似,只是返回值多了新增的数据类型,并且参数也不能是指针了,转而使用in,out,inout关键词替代。
struct Light
{
vec3 position;
vec3 diffuseColor;
vec3 attenuation;
};
// 将重复代码写为函数形式,这里可以注意静态常量的写法,在前面加const,
// 这里的in是关键词,在下面介绍
vec3 CalculateContribution(const in Light light, const in vec3
position)
{
float distance = distance(position, light.position);
float attenuation = 1.0 / (light.attenuation + light.
attenuation * distance + light.attenuation * distance *
distance);
return light.diffuseColor * light.attenuation;
}
// 主函数(Shader)入口
void main()
{
vec3 myPosition = vec3(1.0, 0.0, 0.0);
Light light1 = Light(vec3(10.0, 0.0, 0.0), vec3(1.0, 0.0, 0.0) ,
vec3(1.0, 2.0, 3.0));
Light light2 = Light(vec3(0.0, 10.0, 0.0), vec3(1.0, 0.0, 0.0) ,
vec3(1.0, 2.0, 3.0));
Light light3 = Light(vec3(0.0, 0.0, 10.0), vec3(1.0, 0.0, 0.0) ,
vec3(1.0, 2.0, 3.0));
// Calculate light1
vec3 finalColor = CalculateContribution(light1, myPosition);
// Calculate light2
finalColor += CalculateContribution(light2, myPosition);
// Calculate light3
finalColor += CalculateContribution(light3, myPosition);
}由于在GLSL中不存在指针,所以我们不能像原来一样使用指针的方式进行引用传递,转而使用in/out/inout关键词来说明在函数调用的过程中是值传递还是引用传递。关键词in意味着在函数调用之后,变量将保持不变(值传递)。而关键词out说明变量只在输出的时候进行拷贝,输入的时候不会进行拷贝(在原变量上操作,引用传递)。还有个关键词inout表明变量在输入输出的时候都会进行拷贝。要知道const只能和in结合使用,const不能和out以及inout结合使用。如果不写in/out/inout关键字,那么默认情况是使用的in。
预处理器(预编译头和预处理)(重点)
GLSL就像C语言一样有预处理器,预处理器是对shader源码的一个预编译(在将GLSL程序编译为机器码的步骤之前)。主要操作是执行字符串的替换和其他一些事情比如读取编译指令。通常,预处理器会扫描代码并寻找其中可以被计算或替换为实值的标记
所有的预处理器标记都是以标记符号 # 开头,几个比较重要的标记如下:
error
error指令会在Shader的编译日志中显示该关键词后面的信息。当预处理器遇到error标记,就会认为代码结构出现了错误并且不再执行,比如说:
#ifndef DEG2RAD
#error missing conversion macro DEG2RAD
#endifversion
version指令是强制编译器转到一个特定的GLSL版本。它必须放在shader代码的第一行。该关键词非常有用,因为他保证代码更好的兼容性。如果你想用特定版本GLSL的特性(比如说,避免shader代码中的重复冗余), 这一方法至关重要,因为当你编写的代码使用了更旧的或更新的GLSL语言特性,它会向你抛出error。
在本教程的shader中,第一行可以写上 #version 430
pragma
pragma指令用于给GLSL编译器提供一些信息,比如使用pragma调整debug和release模式调试版本,发布版本(#pragma debug(on)或者#pragma debug (off))。默认情况下pragma参数是关闭debug模式的。
作者建议使用debug(on) pragma因为发生错误后编译器会打印更多的信息,同时,作者建议关闭优化(optimize(off) pragma),因为编译器在优化代码时时不时会报错,你可能用很长时间去debugging不是你造成的错误。在实际运行环境中,要打开优化,同时保证代码跟非优化状态下运行效果一致(但是更快)。
define
define指令可以定义一个预处理符号,可以选择为它分配一个值。假的参数也可以用于创建伪函数(宏),而#undef 可以从预处理器的宏定义链表中删除相关的符号。
if #ifdef #ifndef #else #elif #endif
标记#if和#endif指令可以控制代码工作流程,如果#if指令的条件判断为真,在#if和#endif之间的代码会被编译,如果为假,那么在#if和#endif之间的源码就会在编译中忽略掉,因此不被编译,剩余的几个指令就和正常的编程语言中一样,也是控制代码的编译流程的。
比较特殊的一个指令是#ifdef,因为它不是判断一个布尔表达式,而是检查#ifdef之后的预处理符号是否声明过(#ifdef检查symbol是否声明过)
如下是一段含有许多预处理头的代码:
// 确定glsl语言版本和debug,optimize模式
#version 430
#pragma debug(on)
#pragma optimize(off)
// 定义一个宏: PI值
#ifdef PI
#undef PI
#endif
#define PI 3.141592
# 定义一个弧度角度转换的宏
#define DEG2RAD(x) ((x) * PI / 180.0)
# 定义一个采样值宏
#define NUMBER_OF_TEXTURES 2
void main()
{
float degrees = 45.0;
/* 确定是否定义了弧度角度转换的宏,如果定义了就使用宏,没有定义就自己计算 */
#ifdef DEG2RAD
float cosine2 = cos(DEG2RAD(degrees));
#else
float cosine = cos(degrees * PI / 180.0);
#endif
// 这段代码只是为了展示如何使用预处理头,这样写代码不太好
vec4 color;
#if NUMBER_OF TEXTURES == 2
color = ReadTextureColor(2);
color += ReadTextureColor(1);
#elif NUMBER_OF_TEXTURES == 1
color = ReadTextureColor(1);
#elif NUMBER_OF_TEXTURES == 0
color = vec4(0.0, 0.0, 0.0, 1.0);
#else
// 定义一个错误检测,当没有找到TEXTURE值时,报错
#error Unsupported number of textures
#endif
}仔细阅读后可以知道,最终预处理后的代码其实可以写为:
#version 430
#pragma debug(on)
#pragma optimize(off)
void main()
{
float degrees = 45.0;
float cosine2 = cos(((degrees) * 3.141592 / 180.0));
vec4 color;
color = ReadTextureColor(2);
color += ReadTextureColor(1);
}着色器输入输出变量(重点)
我们之前的讨论主要是GLSL语言本身的内容,基本上是和C语言一致的内容,我们也要重点关注它们两者的不同。下面的讨论只是语言本身部分的不同,来了解一下GLSL功能部分的不同,一个很重要的内容就是:着色器的输入输出。
Uniform变量(一致变量,重点)
假定你要创建一个着色器,该着色器使用最简单的光照,显然,着色器本身并不知道灯光或者其它的高级的概念。着色器(Shader)只知道数学变量和编程程序,因此,如果我们的语言本身并不含有光照属性,我们如何在着色器中使用灯光的位置和颜色信息呢。
你需要把相关的这些变量先传入你的着色器(shader)中,然后在着色器(shader)中进行光照相关的计算。
从程序中传入着色器的变量叫作一致变量。这些变量往往是只读的(常量),它们是由可执行程序产生的,对于整个着色器来说是全局变量。其实一致变量是使用uniform关键字进行修饰的变量。
程序流程是这样的:第一步,设置一致变量,绘制一些三角形,改变这些变量(如果需要的话),绘制其它的三角形,重复这一过程直到完成全部的渲染工作。
下面的代码展示了如何在主程序中设置一致变量的值(这些是主程序的C代码和OpenGL代码,不是GLSL代码)。在shader编译链接生成结束之后,你需要找到变量值的索引。具体代码如下:
// C 语言代码初始化一致变量
// programID = OpenGL program id ,
// &#34;lightPosition&#34; = 在shader中定义的对应变量名称:具体程序具体定义,需要注意
int location = glGetUniformLocation(programID,&#34;lightPosition&#34;);
// 主程序中创建一个暗淡光,使用3维数组
float myLight1Position[] = {0, 10.0f, 0};
// 将值传入一致插槽中
glUniform3fv(location, myLight1Position);如果在使用shader前不进行传值,就会出现value is undefined。
#version 430
#pragma debug(on)
#pragma optimize(off)
uniform vec3 lightPosition;
void main()
{
// 使用lightPosition进行相关计算
}使用uniform variable(一致变量)可以传递任何想要传递的值:整数,矩阵,结构体甚至结构体数组。
其它输入变量
有许多其它类型的输入变量,可以用于其他相关的管线处理阶段。依赖于所处的管线位置,他们的使用各不相同,下面展示几个案例,具体的内容会在接下来的章节深入介绍,这几个输入变量都是全局变量,就像uniform变量一样,并且在shader中使用时,它们需要加关键词in进行声明。
#version 430
//先不要关心smooth关键词
in smooth vec3 vertexColor
out vec4 frameBufferColor;
void main(){
frameBufferColor = vec4(vertexColor, 1.0);
}在后一个shader中,vertexColor是一个输入变量,它的输入来自于顶点着色器的顶点插值颜色输出。
着色器输出变量
每一个着色器都可以执行一种工作,工作的结果是output输出的形式,以fragment shader为例,输出至少是片段的颜色。在一个顶点着色器中,输出至少要包括顶点在裁剪坐标系下的位置,通常其他的顶点属性会以此进行插值并传递到片段着色器中。
GLSL为包括以上所有类型的变量提供了一个修饰词:一个重要的关键词out。和输入变量一样,输出变量也需要全局声明。在片段着色器中,out的语义是(大多数情况下)所有将会被绘制到framebuffer当中的颜色值。
注:实际上片段着色器也可以输出颜色之外的量,比如设深度信息或者为多个framebuffers分别提供颜色值,但大多数情况下片段着色器的输出我们只考虑颜色。
页:
[1]