前言
在three.js中实现各种滤镜效果,需要用到自定义着色器材质ShaderMaterial。
关于着色器的基础知识,我们在 用three.js写一个3D地球 一文中进行了介绍。这里就直接使用自定义着色器啦。
滤镜基础知识
WEBGL中的颜色表示法是RGBA,每一个像素点的颜色值由R、G、B、A 四个值 决定,其中每个分量的值都是范围在【0,1】之间的浮点数,例如,黑色是(0.0, 0.0, 0.0, 1.0),白色是(1.0, 1.0, 1.0, 1.0)。
three.js封装了WEBGL的许多实现细节,让开发3D应用变得简单,同时又开放了自定义着色器材质ShaderMaterial,让开发者可以自由地操作像素,实现各种定制的效果。
例如,通过自定义着色器,我们可以手动修改每个像素颜色值的四个分量值。下面我们还是以地球贴图为例。
用ShaderMaterial创建物体
我们用ShaderMaterial创建一个地球,代码如下。
//顶点着色器代码片段(在下面创建着色器材质时,作为参数传入)
var VSHADER_SOURCE = `
varying vec2 v_Uv; //顶点纹理坐标
void main () {
v_Uv = uv;
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4( position, 1.0 );
}
`
//片元着色器代码片段(在下面创建着色器材质时,作为参数传入)
var FSHADER_SOURCE = `
uniform sampler2D e_Texture; //纹理图像
varying vec2 v_Uv; //片元纹理坐标
void main () {
gl_FragColor = texture2D( e_Texture, v_Uv ); //提取纹理图像的颜色值(纹素),并赋值给gl_FragColor
}
`
//加载贴图纹理图像
var earthTexture = loader.load('./images/earth.png')
//创建着色器材质
var material = new THREE.ShaderMaterial({
uniforms: {
e_Texture: {
value: earthTexture //纹理图像
}
},
// 指定顶点着色器
vertexShader: VSHADER_SOURCE,
// 指定片元着色器
fragmentShader: FSHADER_SOURCE,
})
//创建集合体
var geometry = new THREE.SphereGeometry(20,30,30)
//创建物体
var sphere = new THREE.Mesh(geometry, material)
//将物体加到场景中
scene.add(sphere)
在上面的片元着色器代码片段中,我们通过 texture2D函数 提取纹理图像某一点的颜色值(纹素),这个函数的返回值是一个四维向量,分别代表R、G、B、A 四个分量。
获取到纹素后,将其赋值给gl_FragColor,用于指定片元的颜色。
RGBA分量操作
其实,我们在获取纹素后,可以先对 R、G、B、A 这四个分量进行一些操作,再赋值给gl_FragColor。
例如,把每个纹素的绿色(G)分量置为0,修改片元着色器代码如下:
var FSHADER_SOURCE =
`uniform sampler2D e_Texture;
varying vec2 v_Uv;
void main () {
vec4 textureColor = texture2D( e_Texture, v_Uv );
float green = textureColor.g * 0.0; //修改颜色值的绿色(G)分量为0
gl_FragColor = vec4(textureColor.r, green, textureColor.b, 1.0);
}`
效果如下:
看,是不是已经有点滤镜的感觉了。
其实滤镜的本质,就是按照某种算法修改每个像素点的颜色值,更具体一点,就是修改 R、G、B、A 这四个分量值。
下面来实现一个常见的灰度滤镜吧。
灰度滤镜的原理和矩阵实现
基础原理和实现
灰度的原理,简单来说就是将一张彩色图片变成灰白色图片。
常见的灰度算法有三种:
-
最大值法
-
平均值法
-
加权平均值法
下面要实践的是,加权平均值法。
具体的实现思路是,我们先将该图片的每个像素点的R、G、B分量值进行加权平均,然后将这个值作为每个像素点新的R、G、B分量值,具体公式如下:
其中 R、G、B 是原图片中的 R、G、B 通道的色值,V 是加权平均色值,a、b、c 是加权系数,满足 (a + b + c) = 1。
基于上面的原理,我们来实现一下。修改片元着色器代码:
var FSHADER_SOURCE =
`uniform sampler2D e_Texture;
varying vec2 v_Uv;
void main () {
vec4 textureColor = texture2D( e_Texture, v_Uv );
//计算加权平均值
float w_a = textureColor.r * 0.3 + textureColor.g * 0.6 + textureColor.b * 0.1;
gl_FragColor = vec4(w_a, w_a, w_a, 1.0);
}`
这里的加权系数分别为 a = 0.3, b = 0.6, c = 0.1,是因为人的视觉对 R、G、B 三色的敏感度不一样,所以三种颜色值的权重不一样,一般来说绿色最高,红色其次,蓝色最低。你可以修改成其他值,看看效果。
矩阵实现
如果要调整加权系数或者实现其他滤镜,就要再一次修改片元着色器代码。这样太麻烦了。
下面我们将借助矩阵来优化代码。
由于GLSL ES中的矩阵最多是四维的,所以,这里就这对 R、G、B 三个量进行矩阵变换,透明度alpha单独设置。
js代码修改如下:
//着色器材质
var material = new THREE.ShaderMaterial({
uniforms: {
e_Texture: {
value: earthTexture
},
t_Matrix: {
value: grayMatrix //变换矩阵由js传入
}
},
// 顶点着色器
vertexShader: VSHADER_SOURCE,
// 片元着色器
fragmentShader: FSHADER_SOURCE
})
片元着色器的代码也做相应的修改:
var FSHADER_SOURCE =
`uniform sampler2D e_Texture;
uniform mat4 t_Matrix; //接收变换矩阵
varying vec2 v_Uv;
void main () {
vec4 textureColor = texture2D( e_Texture, v_Uv );
//变换矩阵乘以 vec4(R,G,B,1) --->vec4(R,G,B,1) 是齐次坐标,原本是n维的向量用一个n+1维向量来表示
//vec4(R,G,B,1)第四个分量不是透明度
vec4 transColor = t_Matrix * vec4(textureColor.r, textureColor.g, textureColor.b, 1.0);
//设置透明度
transColor.a = 1.0;
gl_FragColor = transColor;
}`
按照矩阵与向量的乘法,灰度滤镜的变换矩阵应该是下面这样:
但在js中传入的矩阵却是下面这样,OpenGL API接受的矩阵要求是列主序的,如果一个OpenGL的应用使用的是行主序的矩阵,那么在将矩阵传给OpenGL API前,需要先转换为列主序。我们这里就手动转置了。
参考资料:OpenGL中矩阵的行主序与列主序
使用矩阵,可以很好地抽象和复用。一个矩阵就代表一种变换,只要乘上这个矩阵,就意味着做了同样的变换,而当你要修正某种变换,就只用修改它对应的变换矩阵。
几种滤镜效果的实现
实现下面几种滤镜效果,其实就是使用了几种不同的矩阵。(值得注意的是,下面的变换矩阵也都是转置过后的列主序矩阵。要推导的话请将下面的矩阵先转置回行主序)
反色
实现反色,就是 R、G、B 每个分量对 1 取补。
变换矩阵:
var oppositeMatrix = new Float32Array([
-1, 0, 0, 0,
0, -1, 0, 0,
0, 0, -1, 0,
1, 1, 1, 1
])
亮度
R、G、B 每个分量都乘上一个值p; p < 1 调暗,p = 1 原色, p > 1 调亮。
变换矩阵:(传入参数p,动态生成变换矩阵)
function genBrightMatrix (p) {
return new Float32Array([
p, 0, 0, 0,
0, p, 0, 0,
0, 0, p, 0,
0, 0, 0, 1
])
}
对比度
R、G、B 每个分量都乘上一个值p,然后加上 0.5 * (1 - p); p = 1 原色, p < 1 减弱对比度,p > 1 增强对比度。
基于矩阵的实现:(传入参数p,动态生成变换矩阵)
function genContrastMatrix (p) {
var d = 0.5 * (1 - p)
return new Float32Array([
p, 0, 0, 0,
0, p, 0, 0,
0, 0, p, 0,
d, d, d, 1
])
}
饱和度
传入参数p,计算方法如下。p = 0 完全灰度化,p = 1 原色,p > 1 增强饱和度。
function genSaturateMatrix (p) {
var rWeight = 0.3 * (1 - p)
var gWeight = 0.6 * (1 - p)
var bWeight = 0.1 * (1 - p)
return new Float32Array([ rWeight + p, rWeight, rWeight, 0, gWeight, gWeight + p, gWeight, 0, bWeight, bWeight, bWeight + p, 0, 0, 0, 0, 1 ])
}
今天就到这里啦,这篇文章主要探讨了一些基础滤镜的原理和矩阵的初级用法。更多酷炫滤镜的算法在后面的文章再慢慢解锁,下次见!
参考
极客时间《跟月影学可视化》