今天开始学着色器编程,计划在2017年7月到9月,初步掌握GLSL。
学习教材:
- 书名:图形着色器——理论与实践(第2版)
- 原名:Graphics Shaders: Theory and Practice
- 作者:Mike Bailey, Steve Cunnungham
- 翻译:刘鹏
- 出版社:清华大学出版社
- ISBN:978-7-302-31599-5
调试工具:
- glman (http://web.engr.oregonstate.edu/~mjb/glman/)
- notepad++
- Windows 7.0
学习笔记
这本教材我买到手已经很久了,但是此前一直有别的事情要做,没有功夫仔细学习着色器编程。好在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
这个头文件,所以也就没有使用 aVertex
、aNormal
等变量名,而是直接使用低版本 GLSL 中的内建 attribute 变量 gl_Vertex
、gl_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;
}
运行的结果如下:
第二个着色器程序
第二个程序依然是根据第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.);
}
运行结果: