目录
给shader分配代码 -> gl.shaderSource()
编译shader -> gl.compileShader()
判断是否编译成功 -> gl.getShaderParameter()
在对WebGL的渲染流程和GLSL着色器语言有了基本认知后,我们就直接开始学习Three.js的shader着色器部分。着色器一般分为顶点着色器Vertex Shader和片元着色器Fragment Shader。在Three.js的材质中会提到的:MeshPhongMaterial、MeshLambertMaterial等材质对象,本质上都是着色器代码,材质会调用绝对路径下的着色器代码,经过编译后在显卡里运行。
下面基于作业0的着色器内容分别对shader运行过程、glsl中顶点着色器和片元着色器的编写、shader的编译与连接几个部分进行介绍。(注意,代码段我选择的语言是C#,有一些关键字标注的地方很奇怪这里提个醒~不过不影响阅读!)
shader如何运行
sahder的编译和连接是把写好的着色器经过封装并通过程序对象参与进渲染流水线的过程,下面是参考OpenGL学习笔记(5)之Shader编译链接 总结的OpenGL的shader编译和连接的过程图,我觉得WeblGL和OpenGL的过程是几乎一样的,这里就直接放找到的OpenGL的过程图了。
下面先介绍顶点着色器和片元着色器的GLSL:
顶点着色器 Vertex Shader
与片元着色器相比,顶点着色器作用不是很大,主要是负责处理顶点位置、顶点颜色、顶点向量等顶点的数据;处理一些顶点的变换:例如在进行视图变换和投影变换时MVP矩阵会改变顶点的位置信息。
输入
顶点着色器输入部分主要是声明①储存着每个顶点属性的attribute变量;②所有顶点都会实现的(例如MVP矩阵、透视变换矩阵等)全局的uniform变量;③一些会传递给片元着色器的varying变量。
我们来看看作业0中 phongShader -> vertex.glsl 顶点着色器代码的输入部分:
1 声明顶点的位置、法线、uv坐标等变量
attribute vec3 aVertexPosition;
attribute vec3 aNormalPosition;
attribute vec2 aTextureCoord;
2 声明视图变换矩阵及透视变换矩阵
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
3 声明顶点着色器会传递给片元着色器的变量
varying highp vec2 vTextureCoord;
varying highp vec3 vFragPos;
varying highp vec3 vNormal;
输出
顶点着色器输出的一般是:①顶点坐标gl_Position;②给输入部分声明的varying变量赋值;
还是以作业0中的顶点着色器为例,看看它的输出部分:
1 对输出中声明的三个varying变量赋值
2 输出gl_Position
这里gl_Position的计算就用到了101学到的知识啦!把坐标变成齐次坐标后依次进行视图变换和投影变换,矩阵计算从右往左进行。
void main(void) {
vFragPos = aVertexPosition;
vNormal = aNormalPosition;
//计算点坐标
gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aVertexPosition, 1.0);
vTextureCoord = aTextureCoord;
}
片元着色器 Fragment Shader
片元着色器在整个渲染过程中起到了非常大的作用,颜色、贴图、光照阴影等计算都在片元着色器中进行。
输入
输入的数据首先有顶点着色器传入的varying变量,以及一些全局的uniform变量:例如颜色值、光源位置、相机位置等。
以作业0为例,输入的内容有:
1 预处理指令、精度限定
和c++的预处理指令很类似,预处理指令被用在着色器真正的编译之前对代码进行预处理,以#开始,#endif结尾。
//预处理指令
//检查是否定义了GL_ES宏,如果定义了就执行#ifdef和#endif中间的内容
#ifdef GL_ES
precision mediump float;//着色器不支持高精度,限定浮点类型为中精度
#endif
关于精度定义,精度限定符有三个:lowp、mediump和highhp,用以限定数据精度例如int和float
//除了上述出现的“着色器不支持高精度,限定浮点类型为中精度”
precision mediump float;
//还有表示支持高精度的:“着色器支持高精度,限定浮点类型为高精度”
precision highp float;
//以及仅支持低精度的:
prcision lowp float;
接下来开始进行着色器真正的编译,其中输入部分有:
2 声明全局的uniform变量
//声明一个纹理相关的变量,sampler2D 也是一种数据类型
//它是一种取样器类型的变量,该变量对应传入的纹理2D图片的像素数据
uniform sampler2D uSampler;
//Blinn-Phong需要的漫反射项kd、高光项ks、光源位置、相机位置、光强
uniform vec3 uKd;
uniform vec3 uKs;
uniform vec3 uLightPos;
uniform vec3 uCameraPos;
uniform float uLightIntensity;
// 这个TextureSample参数作用应该是:
// 以左上角为原点,通过UV截取图片,如果==1,则正常取值
uniform int uTextureSample;
3 继承varying变量 精度为高精度
//继承varying参数
varying highp vec2 vTextureCoord;
varying highp vec3 vFragPos;
varying highp vec3 vNormal;
输出
输出的结果一般是设置gl_FragColor.
以作业0为例,Blinn-Phong Fragment Shader输出的是:
gl_FragColor = vec4(pow((ambient + diffuse + specular), vec3(1.0/2.2)), 1.0);
对整个输出结果的Phong计算部分做简单的注释:
//Blinn-Phong
//强烈建议这里跟作业3的texture_fragment_shader一起对比着看!
void main(void) {
// 这里的color只有vec3->RGB,最后输出还要加上alpha透明度值
vec3 color;
if (uTextureSample == 1) {
// texture2D(uSampler,vTextureCoord)这个是从Sampler中读取coord信息,进行了伽马矫正
color = pow(texture2D(uSampler, vTextureCoord).rgb, vec3(2.2));
} else {
color = uKd;
}
// Blinn-Phong模型,还是做一遍注释加强理解!
//计算环境光la ->直接是个常量
vec3 ambient = 0.05 * color;
//计算漫反射Ld
vec3 lightDir = normalize(uLightPos - vFragPos);
vec3 normal = normalize(vNormal);
float diff = max(dot(lightDir, normal), 0.0);
//I/r²
float light_atten_coff = uLightIntensity / length(uLightPos - vFragPos);
//Ld
vec3 diffuse = diff * light_atten_coff * color;
//计算高光Ls
vec3 viewDir = normalize(uCameraPos - vFragPos);
float spec = 0.0;
vec3 reflectDir = reflect(-lightDir, normal);
//p取的35
spec = pow (max(dot(viewDir, reflectDir), 0.0), 35.0);
//计算Ls
vec3 specular = uKs * light_atten_coff * spec;
//gl_FragColor就是fragment shader的输出啦
//输出的是一个vec4值,-> RGBA(alpha表示透明度)
//其中为什么要有1/2.2的幂次?-> Gamma Correction伽马矫正
//这里手动进行了伽马矫正(通常矫正值是2.2),以实现像素值和屏幕亮度呈线性关系
gl_FragColor = vec4(pow((ambient + diffuse + specular), vec3(1.0/2.2)), 1.0);
}
Gamma Correction 伽马矫正
其中涉及到了一个图形学的概念:Gamma Correction 伽马矫正。在Unity3D里,已经有GUI选项直接引擎内部帮你解决这个问题,即转化成sRGB值,但作业0代码中还是需要手动进行矫正。对于伽马矫正, 如果不想深入了解只需要知道在颜色储存到buffer前会以采用以下矫正代码:
fragColor.rgb = pow(fragColor.rgb, 1.0/2.2);
想要深入了解伽马矫正的含义可以参考这篇文章,写得非常全面:
【图形学】我理解的伽马校正(Gamma Correction)
以上代码分析的都是作业0中PhongShader的内容,lightShader其实也是一样的,这里就不赘述。
WebGL中shader的编译和连接
着色器处理完毕后,就要进入——代码编译,生成着色器环节。
着色器对象的编译及生成
通过WebGL的内部函数就可以实现编译,一般会构建一个函数,用来实现编译着色器会进行的步骤:构造 -> 编译 -> 判断编译是否成功 -> 输出生成的shader,将实现这些步骤的函数都囊括进去,例如作业0中的compileShader()函数,代码如下:
constructor(gl, vsSrc, fsSrc, shaderLocations) {
this.gl = gl;
//vsSrc/fsSrc -> 着色器对应的代码
//gl.VERTEX_SHADER/gl.FRAGMENT_SAHDER -> 分别进行顶点和片元shader
const vs = this.compileShader(vsSrc, gl.VERTEX_SHADER);
const fs = this.compileShader(fsSrc, gl.FRAGMENT_SHADER);
...
}
compileShader(shaderSource, shaderType) {
const gl = this.gl;
//生成shader
//createShader()会根据顶点和片段shader传入不同的参数
var shader = gl.createShader(shaderType);
//输入shader字符串
//将标签shaderSource中的代码分配给着色器shader
gl.shaderSource(shader, shaderSource);
//编译shader
gl.compileShader(shader);
// 判断是否编译成功
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(shaderSource);
//如果失败,则提示:
console.error(('shader compiler error:\n' + gl.getShaderInfoLog(shader)));
}
return shader;
};
下面结合该函数的内容,我们来简单熟悉一下实现shader编译会用到的函数有哪些:
生成shader -> gl.createShader()
该函数有一个参数——createShader(shaderType),其中shaderType是着色器的类型,该函数通过判断着色器类型传入不同的参数。这个函数也是顶点着色器和片元着色器编译的最大不同之处,其余的函数其实都是相同的,shaderType有指定的类型,如下:
- gl.VERTEX_SHADER——生成顶点着色器
- gl.FRAGMENT_SHADER——生成片元着色器
var shader = gl.createShader(shaderType);
给shader分配代码 -> gl.shaderSource()
该函数有两个参数——shaderSource(shader, shaderSource);第一个参数是分配代码的着色器对象,第二个是代码。这个函数只是向着色器指定GLSL 源代码(js中源码以字符串的形式储存),并没有进行编译的操作。
gl.shaderSource(shader, shaderSource);
编译shader -> gl.compileShader()
该函数有一个参数——compileShader(shader),给他传递需要编译的着色器,函数以实现着色器的编译。GLSL语言接近C,使用前需要被编译成二进制这种可被WebGL执行的格式。
gl.compileShader(shader);
判断是否编译成功 -> gl.getShaderParameter()
该函数有两个参数——getShaderParameter(shader, COMPILE_STATUS);其中COMPILE_STATUS是WebGL中的一个常量参数。该函数返回false则代表编译失败。
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(shaderSource);
//如果失败,则提示:
console.error(('shader compiler error:\n' + gl.getShaderInfoLog(shader)));
}
程序对象的生成及连接
生成着色器后,还需要生成程序对象,那么什么是程序对象?程序对象是管理着色器对象的容器,程序对象可以实现shader与shader之间数据的传递,它是着色器之间数据沟通的连接。WebGL中一个程序对象必须包含一个顶点着色器和一个片元着色器。
与着色器对象相同,一般会构造专门的一个函数用来囊括程序对象从生成到连接的过程,过程包括:
- 程序对象的生成 -> gl.createProgram()
- 对应的还可以执行程序对象的删除 -> gl.deleteProgram()
- 给程序对象分配两个着色器 -> gl.attachShader()
- 将两个着色器连接起来 -> gl.linkProgram() ,为什么要连接?为了使两个着色器的变量同名同类型且一一对应。
作业0中用以实现程序对象的生成及连接的函数如下:
//与着色器对象相同,vs和fs分别对应着色器的glsl源码
linkShader(vs, fs) {
const gl = this.gl;
//程序对象的生成
var prog = gl.createProgram();
//像
gl.attachShader(prog, vs);
gl.attachShader(prog, fs);
gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
abort('shader linker error:\n' + gl.getProgramInfoLog(prog));
}
return prog;
};
参考
如何在threejs中使用shader - 知乎 (zhihu.com)
Three.js 着色器 shader_ShaderMaterial 介绍
WebGL-着色器的编译和连接 - 知乎 (zhihu.com)
【《WebGL编程指南》读书笔记——着色器和程序对象的准备】
以上是结合作业0中shader.js和Phongshader文件的代码总结的WebGL中shader的编译和连接过程。