深入了解Shader

本文将带大家深入了解Shader,下面是代码演示。

前言

上篇文章中我们已经和Shader有了一面之缘,本文将带大家深入Shader的世界,介绍Shader的语言特性,数据类型,内置方法等等。Shader语言和C语言很相似,如果你学过C语言应该可以很快适应Shader的编程风格。本文提供了一个具备Shader基本编程元素的例子,通过Shader控制三角形旋转,并把位置转变成了颜色,读者可以通过修改这个例子更快的熟悉Shader。

代码框架

无论是Vertex Shader还是Fragment Shader,都有基本的代码框架。下面是本文使用的Vertex Shader。

attribute vec4 position;
varying vec4 fragColor;
uniform float elapsedTime;
void main() {
    fragColor = position * 0.5 + 0.5;
    float rotateAngle = elapsedTime * 0.001;
    float x = position.x * cos(rotateAngle) - position.y * sin(rotateAngle);
    float y = position.x * sin(rotateAngle) + position.y * cos(rotateAngle);
    gl_Position = vec4(x, y, 0.0, 1.0);
}

只有Vertex Shader可以声明attribute变量,它用来接受CPU传递过来的顶点数据。除了attribute变量之外,还可以声明uniform变量和varying变量。具体含义还以下面会作详解。main方法是Shader代码执行的入口,这和C语言一模一样。在Vertex Shader中你必须给gl_Position赋值,否则这个Vertex Shader没有任何意义,没有任何顶点会传递给GPU。

下面是本文使用的Fragment Shader。

varying mediump vec4 fragColor;
void main() {
    gl_FragColor = fragColor;
}

Fragment Shader除了attribute变量其他变量都可以拥有,varying变量必须和Vertex Shader中的varying变量类型保持一致,varying变量会从Vertex Shader传递到Fragment Shader中。那么问题来了,Vertex Shader是每个顶点调用一次,而Fragment Shader是每个像素调用一次,那么顶点之间像素的varying变量值是如何计算的呢?答案是GPU会根据要绘制的形状自动插值计算。大家可以观察示例,三角形顶点之间的颜色正是通过各个顶点的fragColor插值计算出来的。

变量类型

Shader有下面几种变量类型:

  • void 和C语言的void一样,无类型
  • bool 布尔
  • int 有符号的int
  • float 浮点数
  • vec2, vec3, vec4 2,3,4维向量,如果你不知道什么是向量,可以理解为2,3,4长度的数组。
  • bvec2, bvec3, bvec4 2,3,4维布尔值的向量。
  • ivec2, ivec3, ivec4 2,3,4维int值的向量。
  • mat2, mat3, mat4 2x2, 3x3, 4x4 浮点数的矩阵,如果你不了解矩阵,后面会有一篇文章单独介绍矩阵。
  • sampler2D 纹理,后面会详细介绍。
  • samplerCube Cube纹理,后面会详细介绍。

变量精度

细心的读者可能会发现同样是varying变量,在Fragment Shader中多了一个mediump修饰符。mediump表示的是变量类型的精度。因为Fragment Shader是逐像素执行,所以会尽量控制计算的复杂度。对于不需要过高精度的变量,可以手动指定精度从而提高性能。精度主要分为下面3种。

  • highp, 16bit,浮点数范围(-2^62, 2^62),整数范围(-2^16, 2^16)
  • mediump, 10bit,浮点数范围(-2^14, 2^14),整数范围(-2^10, 2^10)
  • lowp, 8bit,浮点数范围(-2, 2),整数范围(-2^8, 2^8) 如果你想所有的float都是高精度的,可以在Shader顶部声明precision highp float;,这样你就不需要为每一个变量声明精度了。

运算符

Shader可以使用所有C语言的运算符。不过要注意的是二元运算比如加法,乘法等,只能用在两个类型相同的变量上,比如float只能和float相加。因为Shader不会为你进行隐式的类型转换,这样会增加GPU的负担。我们表示float变量时,需要自行增加小数点,比如浮点数5要写成5.0,否则会被认定为整数。下面是能够使用的运算符,读者可以当做参考。

uniform变量

uniform变量会被所有Shader共享,比如有3个顶点,Vertex Shader会被执行3次,每次访问的uniform变量都是同一个由js代码设定好的值。下面是本文使用的设定uniform elapsedTime的js代码。

elapsedTimeUniformLoc = gl.getUniformLocation(program, 'elapsedTime');
gl.uniform1f(elapsedTimeUniformLoc, elapesdTime);

首先获取uniform elapsedTime在Shader中的位置,然后设置它的值。uniform1funiformXXX函数簇里面用来设置一个float类型uniform的方法。通过uniformXXX里的XXX很容易看出来这个方法是设置什么类型的uniform的,下面是常见的几种格式。

  • uniform{n}{type} n表示数目1~4,type表示类型,floatfintiunsigned intui。所以设置一个整数就是 uniform1i
  • uniform{n}{type}v 相比于上面的多了一个v,表示向量,所以传递的参数就是类型为type,维度为n的向量。
  • uniformMatrix{n}{type}v 这个用来设置类型为type nxn的矩阵。 上面这些方法会在后面的文章中用到,这里大致了解即可。

varying变量

varying变量是Vertex Shader和Fragment Shader的桥梁,Fragment Shader中的varying变量由Vertex Shader中的varying变量自动插值计算出来。因为Fragment Shader是逐像素执行,某些使用varying变量的效果在Fragment Shader中实现会更加细腻,比如光照效果。

向量的访问

当我们拥有一个vec4变量,我们可以有很多种方法访问它内部的值。

  • vec4.x,vec4.y,vec4.z,vec4.w 通过x,y,z,w可以分别取出4个值。
  • vec4.xy,vec4.xx,vec4.xz,vec4.xyz 任意组合可以取出多种维度的向量。
  • vec4.r,vec4.g,vec4.b,vec4.a 还可以通过r,g,b,a分别取出4个值,同上可以任意组合。
  • vec4.s,vec4.t,vec4.p,vec4.q 还可以通过s,t,p,q分别取出4个值,同上可以任意组合。 vec3vec2也是同样,无非就是少了几个变量,比如vec3只有x,y,z。vec2只有x,y。

内置方法

有很多内置方法可以使用,如果可以选择内置方法实现算法,避免自己写代码再实现一遍,因为内置的方法能够得到更好的硬件支持。下面是可用的方法表格。 可能很多方法你都不知道有什么用,没关系,后面使用到时我会再做解释。

上面的介绍覆盖了Shader的大部分基础知识,当然还有很多使用细节和不常用的知识没有介绍,这些知识会在后面使用到时再详细介绍,这样可以避免大家刚开始就对Shader产生畏惧感。

Updated: