第二章 着色器基础

2.1 着色器与OpenGL

  • 现代OpenGL渲染管线严重依赖着色器来处理传入的数据

2.2 OpenGL的可编程管线

  • 4.3版本的图形管线有4个处理阶段和1个通用计算阶段,每个阶段都由一个专门的着色器控制
    1. 顶点着色阶段(vertex shading stage)-- 接收你在顶点缓存对象中给出的顶点数据,独立处理每个顶点
    2. 细分着色器阶段(tessellation shading stage)-- 在OpenGL管线内部生成新的几何体;它会收到来自顶点着色阶段输出的数据,并对收到的顶点进行进一步的处理
    3. 几何着色阶段(geometry shading stage)-- 在OpenGL管线内部对所有的几何图元进行修改;这个阶段会作用于每个独立的几何图元;此时可以选择从输入图元生成更多的几何体,改变几何图元的类型,或者放弃所有的几何体;如果这个阶段被启用,那么几何着色阶段的输入可能来自顶点着色阶段完成的几何图元的顶点处理之后,也可能来自细分着色阶段生成的图元数据
    4. 片元着色阶段(fragment shading stage)-- 处理OpenGL光栅化之后生成的独立片元(如果启用了采样着色的模式,就是采样数据);在这个阶段,计算一个片元的颜色和深度值,然后传递到管线的片元测试和混合的模块
    5. 计算着色阶段(compute shading stage)-- 在程序中相对独立的一个阶段,它处理的数据是应用程序给定的范围的内容;它在应用程序中可以处理其他着色器程序所创建和使用的缓存数据,这其中也包括帧缓存的后处理效果
  • in和out变量–这两类变量的值会在OpenGL每次执行着色器的时候更新
  • uniform变量–直接从OpenGL应用程序中接收数据,不会随这顶点或者片元的变化而变化,它对于所有几何图元的值都是一样的,除非应用程序对它进行了更新

2.3 OpenGL着色语言概述

2.3.1 使用GLSL构建着色器

从这里出发

  • 声明GLSL版本
  • 没有返回值的main函数

变量的声明

  • 透明类型–float double int uint bool
  • 不透明类型–采样器(sampler),图像(image),原子计数器(atomic counter)它们声明的变量相当于一个不透明的句柄,可以用来读取纹理贴图、图像,以及原子计数器数据

变量的作用域

  • 全局作用域–在任何函数之外定义
  • 局部作用域

变量的初始化

  • 所有变量都必须在声明的同时进行初始化

构造函数

  • GLSL中可以隐式转换的类型
所需的类型 可以从这些类型隐式转换
uint int
float int uint
double int uint float
  • 除上述类型类,其他的数值转换都需要提供显式的转换构造函数

聚合类型

  • GLSL的向量类型
基本类型 2D向量 3D向量 4D向量
float vec2 vec3 vec4
double dvec2 dvec3 dvec4
int ivec2 ivec3 ivec4
uint ivec2 ivec3 ivec4
bool bvec2 bvec3 bvec4
  • GLSL的矩阵类型
矩阵类型(列x行)
mat2 mat3 mat4
mat2x2 mat2x3 mat2x4
mat3x2 mat3x3 mat3x4
mat4x2 mat4x3 mat4x4

访问向量和矩阵中的元素

  • 向量和矩阵中的元素可以单独访问和设置。支持两种类型的元素访问方式:分量的名称和数组访问形式
  • 向量分量的访问符
分量访问符 符号描述
( x, y, z, w) 与位置相关的分量
( r, g, b, a) 与颜色相关的分量
( s, t, p, q) 与纹理坐标相关的分量

结构体

  • 结构体可以简化多组数据传入函数的过程
  • 如果定义了一个结构体,OpenGL会自动创建一个新的类型,并且隐式定义一个构造函数,将各中类型的结构体元素做为输入参数

数组

  • GLSL的数据有一个隐式的方法可以返回元素的个数:length()
  • 向量和矩阵也可以使用length()方法;向量的长度就是它分量的个数;矩阵的长度就是它包含的列数
  • 对于向量和矩阵,以及大部分数组来说,length()都是一个编译时就已知的常量;对于着色器中包含的缓存对象(使用buffer来进行声明),length()的值直到渲染时才可能得到

2.3.2 存储限制符

  • GLSL的类型修饰符
类型修饰符 描述
const 将一个变量定义为只读形式
in 设置这个变量为着色器阶段的输入变量
out 设置这个变量为着色器阶段的输出变量
uniform 设置这个变量为用户应用程序传递给着色器的数据,它对于给定的图元而言是个常量
buffer 设置应用程序共享的一块可读写的内存。这块内存也做为着色器中的存储缓存(storage buffer)使用
shared 设置变量是本地工作组(local work group)共享的。他只能用于计算着色器中

uniform存储限制符

  • 在着色器运行之前,uniform修饰符可以指定一个在应用程序中设置好的变量,它不会在图元处理的过程中发生变化

  • uniform变量在所有可用的着色阶段之间都是共享的,它必须定义为全局变量

  • 着色器无法写入到uniform变量,也不能修改它的值

  • GLSL编译器会在链接着色器程序时创建一个uniform变量列表。如果要在应用程序中给uniform变量赋值,需要先获取uniform变量列表中该变量的索引,通过调用glGetUniformLocation(…)得到

  • GLint glGetUniformLocation(GLuint program, const char* name) 返回着色器中uniform变量name对应的索引值

  • 当得到uniform变量的索引值后,就可以通过glUniform*()或者glUniformMatrix*()系列函数来设置uniform变量的值

  • glUniform{1234}{fdi ui}(GLint location, TYPE value)
    glUniform{1234}{fdi ui}v(GLint location, GLsizei count, const TYPE* values)

    1. 对于向量形式的函数会载入count个数据的集合,并写入location位置的uniform变量中
    2. 如果location是数组的起始索引值,那么数组之后的连续count个元素也会被载入

    glUniformMatrix{234}{fd}v(GLint location, GLsizei count, GLboolean transpose, const GLfloat* values)
    glUniformMatrix{2x3,2x4,3x2,3x4,4x2,4x3}{fd}v(GLint location, GLsizei count, GLboolean transpose, const GLfloat* values)

    1. 如果transpose的值为GL_TRUE,那么values中的数据以行主序的的顺序读入
    2. 如果transpose的值为GL_FALSE,那么values中的数据以列主序的顺序读入

buffer存储限制符

  • 在应用程序中共享一大块缓存给着色器
  • 它与uniform变量类似,但是可以用着色器对他的值进行修改
  • buffer修饰符指定“块”做为着色器与应用程序共享的一块内存缓存。这块缓存对于着色器来说是可读可写的。缓存的大小可以在着色器编译和程序链接完成后设置

shared存储限制符

  • 只能用于计算着色器中,它可以建立本地工作组内共享的内存

2.3.3 语句

  • 着色器的真正工作是计算数值,以及完成一些决策工作

算术操作符

  • GLSL操作符和优先级
    《OpenGL编程指南》 笔记二 着色器基础

操作符重载

  • 两个向量相乘(*)得到的是一个逐分量相乘的新向量
  • 两个矩阵相乘(*)得到的通常是矩阵相乘的结果
  • 在OSG中向量的乘法:
    1. vec2 * vec2 相当于数学中的点乘,返回对应坐标乘积的和
    2. 二维向量没有定义叉乘操作符,但有专门的函数实现逐分量相乘,得到一个新的二维向量
    3. vec3 * vec3 点乘 返回对应坐标乘积的和
    4. vec3 ^ vec3 叉乘 返回叉乘后的新三维向量
    5. matrix * matrix 矩阵乘法公式计算
    6. vec3 * matrix 调用 matrix.preMult(vec3) 矩阵乘法公式计算
    7. matrix * vec3 调用 matrix.postMult(vec3) 矩阵乘法公式计算
    8. 矩阵乘法公式如下:
      《OpenGL编程指南》 笔记二 着色器基础
    9. 矩阵分左乘右乘,而不分点乘叉乘
    10. 叉乘的结果是垂直与另两个向量的向量,好像在二维下没有几何意义,所以没有定义二维的叉乘运算符(猜测)

控制流

  • if…else
  • switch

循环语句

  • for
  • while…do

流控制语句

  • break
  • continue
  • return
  • discard 丢弃当前的片元,终止着色器的执行。只能用于片元着色器

函数

  • 可以在单个着色器中定义,然后在多个着色器中复用
  • 如果函数的定义和使用不在同一个着色器中,那么必须声明一个函数原型
  • 返回值为数组时,必须显示指定其大小

参数限制符

  • GLSL函数参数的访问修饰符
访问修饰符 描述
in 将数据拷贝到函数中(默认修饰符)
const in 将只读数据拷贝到函数中
out 从函数中获取数值(因此输入函数的值是未定义的)
inout 将数据拷贝到函数中,并且函数中修改的数据

2.3.4 计算的不变性

  • 关键字“invariant”和“precise”来确保着色器之间的计算不变性他们只能影响到图形硬件设备的计算结果

invariant限制符

  • 可以设置任何着色器的输出变量
  • 在调试过程中,如果需要将着色器中的所有变量都设置为invariant,可以通过顶点着色器的预编译命令pragma来完成“#pragma STDGL invariant(all)”

precise限制符

  • 可以设置任何计算中的变量或者函数返回值
  • 如果必须保证某个表达式产生的结果是一致的,即使表达式中的数据发生了变化(但是在数学上并不影响结果)也是如此,那么此时我们应该使用precise而非invariant

2.3.5 着色器的预处理器

  • 编译一个GLSL着色器的第一步是解析预处理器
  • GLSL的预处理命令
    《OpenGL编程指南》 笔记二 着色器基础

宏定义

  • 不支持字符串替换以及预编译连接符
  • GLSL预处理器中的预定义宏
    《OpenGL编程指南》 笔记二 着色器基础

预处理器中的条件分支

  • #ifdef … #endif
  • #if … #elif … #endif

2.3.6 编译器的控制

  • #pragma命令可以向编译器传递附加信息,并在着色器代码编译时设置一些额外属性

编译器优化选项

  • #pragma optimize(on)
  • #pragma optimize(off)
  • 着色器优化的启用或者禁用,默认为启用

编译器调试选项

  • #pragma debug(on)
  • #pragma debug(off)
  • 启用或者禁用着色器的额外诊断信息输出,默认禁用

2.3.7 全局着色器编译选项

  • #pragma命令的选项为STDGL时,表示启用所有输出变量值的不变性检查
  • #extension extension_name : 用于提示着色器的编译器在编译时如何处理可用的扩展内容
    1. extension_name 与调用glGetString(GL_EXTENSIONS)时获取的扩展功能名称是一致的
    2. 也可以使用 #extension all :
    3. 可用的选项
      《OpenGL编程指南》 笔记二 着色器基础

2.4 数据块接口

  • 着色器与应用程序之间,或者着色器各阶段之间共享的变量可以组织为变量块的形式,并且有时候必须采用这种方式

2.4.1 uniform块

  • 由于uniform变量的位置在着色器链接的时候生成,当在多个着色器程序中用到同一个uniform变量时,它在应用程序中获得的索引可能会有变化
  • uniform缓存对象(Uniform buffer object)是一种优化uniform变量访问,以及在不同着色器程序之间共享uniform数据的方法

2.4.2 指定着色器中的uniform块

  • 一个uniform块中只可以包含透明类型的变量,且必须在全局作用域内声明

uniform块的布局控制

  • uniform的布局限制符
    《OpenGL编程指南》 笔记二 着色器基础
  • 声明方式 layout (shared, row_major) uniform { … };
  • 在两个不同名的uniform块中声明同名变量会在编译时报错

2.4.3 从应用程序中访问uniform块

  • 对uniform块初始化的步骤:
    1. 找到块在着色器程序中的位置,调用glGetUniformBlockIndex()
    2. 判断uniform块变量总共占用了多大空间,调用glGetActiveUniformBlockiv(),并设置GL_UNIFORM_BLOCK_DATA_SIZE,这样就可以返回编译器分配的大小
    3. 创建uniform块对应的缓存对象,调用glGenBuffers()
    4. 初始化缓存对象,调用glBindBuffer(),将缓存对象绑定到目标GL_UNIFORM_BUFFER上
    5. 将应用程序中的数据传递到缓存对象中
    6. 将uniform块变量与缓存对象关联,glBindBufferBase()或者glBindBufferRange()
    7. 之后就可以对块中的变量进行初始化和修改
  • 对初始化uniform块过程的总结:想要给着色器中的变量赋值,首先要将应用程序中的数据传到GPU,而GPU存储数据的地方叫缓存对象,所以先创建缓存对象,然后把数据传过来;然后建立缓存对象与变量的关系,而这又需要变量的索引和变量的大小等信息,上边的步骤之所以跟这里说的不一致,主要是按获取相关必要参数的顺序写的
  • GLint glUniformBlockBinding(GLuint program, GLuint uniformBlockIndex, GLuint uniformBlockBinding) 显示地将块uniformBlockIndex绑定到uniformBlockBinding
  • glGetUniformIndeces(GLuint program, GLsizei uniformCount, count char** uniformNames, GLuint* uniformIndices) 获取指定uniform块变量中变量的索引
  • glGetActiveUniformsiv(…) 获取指定索引位置的大小和偏移量

2.4.4 buffer块

  • GLSL中的buffer块,就是着色器的存储缓存对象(shader storage buffer object)
  • 着色器可以对buffer块中的成员执行读或写操作。写入操作对着色器存储缓存对象的修改对于其他着色器调用都是可见的

2.5 着色器的编译

  • 创建着色器的步骤

    1. 创建着色器对象:GLuint glCreateShader(GLenum type) type的取值
      1. GL_VERTEX_SHADER
      2. GL_FRAGMENT_SHADER
      3. GL_TESS_CONTROL_SHADER
      4. GL_TESS_EVALUATION_SHADER
      5. GL_GEOMETRY_SHADER
    2. 将着色器源码编译为对象:glShaderSource(GLuint shader, GLsizei count, const GLchar** string, const GLint* length);void glCompileShader(GLuint shader);
    3. 验证着色器的编译是否成功:glGetShderiv(GL_COMPILE_STATUS);glGetShaderInfoLog(…);
    4. 创建一个着色器程序:GLuint glCreateProgram()
    5. 将着色器对象关联到着色器程序:glAttachShader(GLuint program, GLuint shader);与之对应的glDetachShader(…)移除着色器对象与着色器程序的关联
    6. 链接着色器程序:glLinkProgram(GLuint program)
    7. 判断着色器的链接过程是否成功:glGetProgramiv(GL_LINK_STATUS); glGetProgramInfoLog(…)
    8. 使用着色器来处理顶点和片元:glUseProgram(GLuint program)
  • 当着色器对象的任务完成后,通过glDeleteShder(shader)将它删除

  • glDeleteProgram(GLuint program) 立即删除一个当前没有在任何环境中使用的着色器程序

  • glIsShader(shader) 判断某个着色器对象是否存在

  • glIsProgram(pragram) 判断某个着色器程序是否存在

2.6 着色器子程序

  • 着色器子程序在概念上类似C语言中的函数指针,它可以实现多态子程序选择

2.6.1 GLSL的子程序设置

  • 设置子程序池的步骤:
    1. 通过关键字subroutine来定义子程序的类型 subroutine returnType subroutineType(type param, …);
    2. 定义子程序集合的内容 subroutine (subroutineType) returnType functionName(…);
    3. 声明一个子程序的uniform变量 subroutine uniform subrouteType variableName;

2.6.2 选择着色器子程序

  • 子程序在连接后使用
  • 步骤:
    1. 获取子程序中uniform变量的位置: glGetSubroutineUniformLocation(…)
    2. 获取子程序在着色器中的索引号:glGetSubroutineIndex(…)
    3. 指定在着色器中执行哪一个子程序函数:glUniformSubroutinesuiv(…)

2.7 独立的着色器对象

  • 独立的着色器对象可以将不同程序的着色阶段合并到一个程序管线中
  • 步骤:
    1. 创建用于着色器管线的着色器程序:glProgramParameteri(GL_PROGRAM_SEPARABLE);
    2. 链接着色器程序
    3. 用这个新的着色器管线结构来合并多个程序中的着色阶段;着色器管线的创建可以调用glGenProgramPipelines();然后将它传入glBindProgramPipeline(),使得该程序可以自由编辑和使用
    4. 调用glUseProgramStages()将之前标记为独立的程序对象关联到管线上

相关文章: