初探 GLSL 着色器(一)

今天我们来谈一谈如何开始学习 GLSL , 这是一种适用于可编程渲染管线的着色器语言。 虽然大部分前端er们可能对其感觉有些陌生,不过没关系,舒马赫并不需要非常了解赛车的每一个零件,这不阻碍他成为一个伟大的车手。

如今 webgl 逐渐流行,前端er 们确实需要了解一些 3D 图形编程基础,但学习它目的不是为了写出多么高性能绚丽的 shader,而是可以帮助您更深层次的理解图形渲染的本质。这样您在今后使用 Threejs 等高级库时,就能做到知根知底,胸有成竹。

如何开始呢? 着色器是个什么概念呢? 什么叫可编程管线? 带着这些疑问,故事还的从固定管线时代开始说起:

可编程管线 VS 固定管线

Monochrome Display Adapter (IBM, 1981)

在早些时候,显示卡的每一项功能都是由固定硬件模块实现的。 比如实现光照、阴影、着色、等每一项工作都需要对应的集成电路来完成。 这样以来,可以实现的效果就非常有限,由于当时 PC 机并没有大规模流行, 真正需要显示卡的地方其实是图形工作站, 工作站的渲染工作类型相对固定,所以采用固定管线是可以胜任的,但随着个人PC的流行,采用固定管线的方案就逐渐被配抛弃了,当然这里面还有很多公司之间里的利益博弈。就省略不提了。

是用固定管线并不意味着你不需要写代码了,只不过你编写程序的创造力受到的很大限制。 OpenGL 4.0 前是支持固定管线的,在以往的旧代码中,OpenGL 的 API 是这个样子。

对于这些功能你只能选择用或不用、无法有其他修改。 编程时需要把它们一个个串联起来, 实现一个复杂效果的过程,简直可以戏称为 “花式调开关”。

固定管线被抛弃的另一个重要原因是,计算资源无法灵活配置,造成浪费。

打个比方说,你是一个老板。手下有 100 名工人。工厂有 3 种工作。“制作电饭煲” 、“制作微波炉” 、“制作台灯” ,这 3 种工作都有对应的厂区,因为无法预测未来工作每一种产品的订单量,所以你只能预先分配这些工人,一旦订单种类不均衡,便会出现有些厂区忙的要死,有些却被闲置的现象。

为了避免浪费,同时解放更多的功能,可编程管线诞生了。有了它厂区不再是固定的,而是当订单确定后,再组成一个个临时的厂区。

这样对于任何一种任务,都可以做到最大限度的提升效率,这种可以随机应变的 “厂区” 便是可编程管线。以前固定管线的所有功能,现在都可以通过编程来实现了,且编程带来的灵活性大大提高了渲染性能。

左侧为固定管线 右侧为可编程管线

GLSL 编程概念

需要一种语言来控制这些 “工人” 组成临时的生产线,这便是着色器存在的意义。本文中提及的 GLSL 是一种在 OpenGL 中使用的着色器语言,但并不是唯一的着色器语言。 除了 GLSL 还有微软的 HLSL 和英伟达的 CG ,这些我们只要了解即可。

顶点着色器 与 片元着色器

一个着色器程序分为两大部分,即 “顶点着色器” 与 “片元着色器” 简单来说,前者多用于模型构建,后者用于在光栅化时表现出更多细节,一个着色器程序必须同时包含这两部分,程序会先通过 “顶点着色器” 处理再交与 “片元着色器” 渲染细节。

举个例子:比如你想绘制一个蓝色四面体,那就需要用到 8 个顶点 和 “蓝” 这两个参数。 其中 8 个顶点数据先传入 “顶点着色器” 这时一个四面体的模型便建立了,而 “蓝色” 这一参数属于纹理细节,将在 “片元着色器” 中被处理。

我们来看一个超简单的顶点着色器实例

attribute vec3 aPosition;
attribute vec3 aColor;
varying vec4 vColor;
void main(void) {
    vColor = aColor;
    gl_Position = aPosition;
}

一个简单的片元着色器实例

varying vec4 vColor;
void main(void)
{
    gl_FragColor = vColor;
}

内置变量

上例着色器中,gl_Positiongl_FragColor 等这些以 gl_ 开头的变量都是内置变量,通过给这些特殊的变量赋值,可以完成与硬件的通讯。其中 gl_Position 用于放置顶点坐标信息,gl_FragColor 用于设置当前片段的颜色。

可以看出 GLSL 是一种面向过程的编程语言,有着与 C 语言类似的语法,但没有 C 语言复杂的指针概念。 常用基本的类型如下:

类型 说明
void 空类型,即不返回任何值
bool 布尔类型 true,false
int 带符号的整数 signed integer
float 带符号的浮点数 floating scalar
vec2, vec3, vec4 n维浮点数向量 n-component floating point vector
bvec2, bvec3, bvec4 n维布尔向量 Boolean vector
ivec2, ivec3, ivec4 n维整数向量 signed integer vector
mat2, mat3, mat4 2x2, 3x3, 4x4 浮点数矩阵 float matrix
sampler2D 2D纹理 a 2D texture
samplerCube 盒纹理 cube mapped texture

其中比较有趣的是 GLSL 中向量的访问是非常人性化的。比如说 vector.xyzw 这说明 vector 表示的是一个三维坐标,其中 xyzw 是可以自由组合的,比如 vector.xy 、 vector.xyz 甚至 vector.zxy 都是可以的。

vec4 vector=vec4(1.0,2.0,3.0,1.0);
vec3 xyz = vector.xyz; //vec3(1.0,2.0,3.0)
vec2 xy = vector.xy; //vec3(1.0,2.0)

不仅 vector.xyzw 支持这种特性 同理 vector.rgba (颜色)、vector.stpq(纹理坐标)都可以。

vec4 vector=vec4(1.0,2.0,3.0,1.0);
vec3 xyz = vector.xyz; //vec3(1.0,2.0,3.0)

vec4 rgba=vector.rgba;
vec3 rgb=rgba.rgb;

vec4 stpq=vector.stpq;

... ...

通过这一特性,我们可以轻松地将一个多维向量分解。

变量限定符:

下面来讲讲上例中的 attribute 与 varying 的意思。 其实这些都是 GLSL 的 变量限定符 一般用来声明与其宿主程序沟通的接口,什么意思呢。 假设在 WebGL 环境中, GLSL 的宿主程序就是 javascript, 所有数据均由 javascript 通过事先定义好的变量限定符传入 GLSL。

除了 attribute 和 varying 还有其他修饰符,具体见下表:

修饰符 说明
none (默认的可省略)本地变量,可读可写,函数的输入参数既是这种类型
const 声明变量或函数的参数为只读类型
attribute 只能存在于vertex shader中,一般用于保存顶点或法线数据,它可以在数据缓冲区中读取数据
uniform 在运行时shader无法改变uniform变量, 一般用来放置程序传递给shader的变换矩阵,材质,光照参数等等.
varying 主要负责在 vertex 和 fragment 之间传递变量

值得注意的是如果在 顶点着色器 与 片元着色器 中存在同名的 varying 变量,则其值可以由顶点着色器传递与片元着色器,如上例中都存在名为 vColor 的 varying 变量,执行 vColor=aColor 后在片元着色器中便可以取到 vColor 的值。

宿主程序(javascript)运行时, 会将四面体的顶点与颜色数据分别传给 aPosition 、 aColor。 一般来讲,给 gl_FragColor 赋值是整个着色器的最后一步工作,意为该点的最终颜色已确定。整体流程如下图所示:

函数参数限定符

GLSL 允许自定义函数,但参数默认是以值形式(in限定符)传入的,也就是说任何变量在传入时都会被拷贝一份,若想以引用方式传参,需要增加函数参数限定符。

限定符 说明
< none: default > 默认使用 in 限定符
in 复制到函数中在函数中可读写
out 返回时从函数中复制出来 (可写不可读)
inout 复制到函数中并在返回时复制出来

其中使用 inout 方式传递的参数便与其他 OOP 语言中的引用传递类似,参数可读写,函数内对参数的修改会影响到传入参数本身。

vec4 getPosition(out vec4 p){ 
    p = vec4(0.,0.,0.,1.);
    return v4;
}

void doubleSize(inout float size){
    size= size*2.0  ;
}

流控制

在语法上,GLSL 与 C 非常相似, 但多了一种特殊的控制语句 discard,它会立即跳出片元着色器,并不在向下任何语句。

for (l = 0; l < numLights; l++)
{
    if (!lightExists[l]);
        continue;
    color += light[l];
}
...

while (i < num)
{
    sum += color[i];
    i++;
}
...

do{
    color += light[lightNum];
    lightNum--;
}while (lightNum > 0)

...

if (true)
    discard;

参考文献:

shader 赏析库: https://www.shadertoy.com/

着色器语言 GLSL 入门大全 https://github.com/wshxbqq/GLSL-Card

留言:

称呼:*

邮件:

网站:

内容: