今天开始学着色器编程

今天开始学着色器编程,计划在2017年7月到9月,初步掌握GLSL。

学习教材:

  • 书名:图形着色器——理论与实践(第2版)
  • 原名:Graphics Shaders: Theory and Practice
  • 作者:Mike Bailey, Steve Cunnungham
  • 翻译:刘鹏
  • 出版社:清华大学出版社
  • ISBN:978-7-302-31599-5

调试工具:

学习笔记

这本教材我买到手已经很久了,但是此前一直有别的事情要做,没有功夫仔细学习着色器编程。好在6月底我终于大致写完了jME3的初级教程,下面可以一边校对文章内容,一边学习GLSL了。

教材的前3章主要介绍了OpenGL着色器的发展史,以及5种着色器的概念和作用。书中有些高度凝炼的概念,如果之前没有见过着色器程序,我认为是很难理解的。

幸运的是,我已经学习jME3有两年了。jME3的材质系统基于GLSL,在开发jME3程序的过程中,我曾或多或少地试图修改过一些着色器代码,对GLSL的工作方式不算太陌生。所以前3章的学习对我来说并不是很困难。

从第4章开始,书中介绍了glman这个工具。我也曾试图去找一个好用一点的IDE,或者在Eclipse中集成支持glsl语法高亮的插件,但我放弃了。最后我还是倾向于使用已有的工具,先快速掌握这门语言,等我需要开发足够复杂的着色器程序时,再去考虑IDE的问题。

glman 使用GLIB来描述一个着色器程序。GLIB 文件与jME3的 j3md 文件很相似,都可以用来定义uniform变量。不过 GLIB
有很多额外的命令功能,诸如:设置窗口大小、改变摄像机参数、定义几何体、加载obj模型等。

第一个着色器程序

根据书本第3章和第4章的介绍,我制作了第一个着色器程序。这个程序与教材第3章介绍的 Sphere 着色器有些许不同。

首先,我没有使用 Gstap.h 这个头文件,所以也就没有使用 aVertexaNormal 等变量名,而是直接使用低版本 GLSL 中的内建 attribute 变量 gl_Vertexgl_Normal

我知道这样会有兼容性问题,但刚开始学习GLSL,我想让代码更加简单一些。只要能快速显示结果就行了,我不在乎在其它电脑上运行不了这个Shader。

其次,为了试验 uniform 变量的用法,我把原 Sphere.vert 中定义的常量 LIGHTPOS 改成了 uniform 变量 uLightPos,并在 GLIB 文件中设置了参数。同时,我额外增加了一个环境光颜色参数 uAmbient,并在 Sphere.vert 中使用了它。

第三,原 Sphere.vert 中的光源位置是固定在模型空间中的。我使用 gl_ModelViewMatrix * uLightPos 把它转到了眼睛空间中。

第四,顶点法线(transNormal)与光线方向(ECLightPosition-ECposition)向量的点乘(dot)结果,被我用clamp夹逼到了[0, 1]域内,这样背光的面就不会反光了,我认为这样做可以让光源看起来更加自然。

源代码如下:

Sphere.GLIB 文件:

WindowSize 600 480
LookAt 0 0 4  0 0 0  0 1 0
Perspective 90 // 透视相机
Background 0.6 0.8 0.9 1.0 // 背景色

Vertex Sphere.vert
Fragment Sphere.frag
Program Sphere \
    uLightPos {3. 5. 10.} \
    uAmbient {1. 0.56 0.31 1.}

Color 1. 0. 1.
Sphere 1. 100 100

顶点着色器 Sphere.vert

uniform vec4 uLightPos;

out vec4 vColor;
out vec3 vMCposition;
out float vLightIntensity;

void main() {
    vec3 transNormal = normalize( gl_NormalMatrix * gl_Normal.xyz);
    vec3 ECposition = vec3( gl_ModelViewMatrix * gl_Vertex );
    vec3 ECLightPosition = vec3 ( gl_ModelViewMatrix * uLightPos );
    vLightIntensity = dot( normalize( ECLightPosition - ECposition ), transNormal );
    vLightIntensity = clamp( vLightIntensity, 0., 1. );
    vColor = gl_Color;
    vMCposition = vec3( gl_Vertex );
    gl_Position = vec4( gl_ModelViewProjectionMatrix * gl_Vertex );
}

片元着色器 Sphere.frag

uniform vec4 uAmbient;

in vec4 vColor;
in float vLightIntensity;

void main() {
    // 环境光
    vec4 ambient = vec4( uAmbient.rgb, 1. );
    // 漫反射
    vec4 diffuse = vec4( vColor.rgb * vLightIntensity, 1.);
    // 合成光
    gl_FragColor = ambient * 0.3 + diffuse * 0.7;
}

运行的结果如下:

my first shader

第二个着色器程序

第二个程序依然是根据第3章改写的。我把方块的扭曲程度和高度分别做成了2个uniform变量,这样就可以通过GUI来更改参数了。

QuadXY.GLIB 文件

WindowSize 600 480
LookAt 0 0 4  0 0 0  0 1 0
Perspective 90 // 透视相机

Vertex QuadXY.vert
Fragment QuadXY.frag
Program QuadXY \
    uHeight <0.01 0.15 0.3> \
    uScale <1 5 8>

Color 1.0 0.8 0.0 
QuadXY -2. 2. 200 200

顶点着色器 QuadXY.vert

uniform float uScale;
uniform float uHeight;

out float vLightIntensity;
out vec4 vColor;

const vec3 LIGHTPOS = vec3(0., 10., 0.);

void main() {
    vec4 thisPos = gl_Vertex;
    vColor = gl_Color;

    // create a new height for this vertex
    float lengthSquared = dot(thisPos.xy, thisPos.xy) * uScale;

    // the Surface is z= uHeight * sin(x^2 + y^2)
    thisPos.z = uHeight * sin( lengthSquared );

    vec3 xtangent = vec3( 1.0, 0.0, 0.0);
    xtangent.z = 2.0 * uHeight * thisPos.x * cos( lengthSquared );

    vec3 ytangent = vec3( 0.0, 1.0, 0.0);
    ytangent.z = 2.0 * uHeight * thisPos.y * cos( lengthSquared );

    vec3 thisNormal = normalize( cross( xtangent, ytangent ) );
    vec3 ECposition = vec3( gl_ModelViewMatrix * gl_Vertex );

    vLightIntensity = dot( normalize( LIGHTPOS - ECposition ), thisNormal );
    vLightIntensity = 0.3 + abs( vLightIntensity );// 0.3 ambient
    vLightIntensity = clamp( vLightIntensity, 0., 1. );

    gl_Position = gl_ModelViewProjectionMatrix * thisPos;
}

片元着色器 QuadXY.frag

in vec4 vColor;
in float vLightIntensity;

void main() {
    gl_FragColor = vec4( vColor.rgb * vLightIntensity, 1.);
}

运行结果: