着色器是运行在GPU上的程序,用于计算三维场景中每个像素的位置、颜色等等各种属性。通常分为两种:
- 顶点着色器:主要用于对每个顶点进行操作,将其变换到最终渲染的位置,并将一些属性传递到片元着色器中。
- 片元着色器:主要用于对每个像素进行操作,计算像素的颜色值,并返回给渲染引擎。
着色器由专门的一门语言GLSL(Graphics Library Shader Language)编写,我们可以用着色器来实现许多高级渲染效果,如阴影、光照、纹理、模糊、反射、折射等等,从而创建更加逼真、酷炫的效果。
在web开发中,我们可以使用three.js等WebGL库来编写着色器,接着我们来看看在three.js中通过着色器为白模添加的一些光效。
一、给模型添加渐变色
首先我们给模型添加颜色,自下而上渐变,并且让模型有透明感。为此我们需要设置两个颜色,并进行插值计算,而插值的比例就是 顶点y坐标 / 模型高度 ,即对两个颜色进行线性插值,从而实现模型底部到顶部的颜色渐变。
// 给模型设置材质,给它一个基本颜色,并且设置支持透明度
mesh.material = new THREE.MeshBasicMaterial({
color: new THREE.Color('#4CBFFD'),
transparent: true
})
// 计算模型包围盒,从而得到模型的高度
mesh.geometry.computeBoundingBox()
const { min, max } = mesh.geometry.boundingBox
const uHeight = max.y - min.y
// 将模型高度和顶部颜色传给着色器
shader.uniforms.uHeight = { value: uHeight }
shader.uniforms.uTopColor = { value: new THREE.Color('#c5c8ff') }
着色器变量有三种修饰类型:
- uniform:全局变量,从外部传给着色器,在顶点着色器和片元着色器中都能使用。
- attribute:传给顶点着色器的变量,在片元着色器中不能使用。
- varying:由顶点着色器传给片元着色器的变量。
上面的代码,我们传了两个uniform变量,它们可以直接在片元着色器中使用。此外,我们还需要顶点的y坐标,这个值在顶点着色器中可以获取,要想在片元着色器中使用,就需要通过varying进行传递:
// 顶点着色器
varying vec3 vPosition;
void main() {
vec4 viewPosition = viewMatrix * modelMatrix * vec4(position, 1.0);
gl_Position = projectionMatrix * viewPosition;
// ...
vPosition = position; // 将顶点位置传给片元着色器
}
在片元着色器中接收,然后进行颜色的插值,并设置0.5的透明度:
// 片元着色器
uniform float uHeight;
uniform vec3 uTopColor;
varying vec3 vPosition;
void main() {
vec4 distGradColor = gl_FragColor;
float gradMix = vPosition.y / uHeight; // 混合百分比
vec3 gradMixColor = mix(distGradColor.xyz, uTopColor, gradMix);
gl_FragColor = vec4(gradMixColor, 0.5);
}
其中gl_FragColor是着色器的内置变量,即最终渲染的颜色。
二、给模型添加线框
模型线框可以结合Three.js的EdgesGeometry和LineSegments来实现,EdgesGeometry可以得到几何体的边缘,然后再由LineSegments绘制线段:
const edgesGeometry = new THREE.EdgesGeometry(mesh.geometry)
this.edgesMaterial = new THREE.LineBasicMaterial({
color: color
})
const edges = new THREE.LineSegments(
edgesGeometry,
this.edgesMaterial
)
// 更新变换矩阵
mesh.updateWorldMatrix(true, true)
edges.matrix.copy(mesh.matrixWorld)
edges.matrix.decompose(edges.position, edges.quaternion, edges.scale)
scene.add(mesh)
三、从下到上的扫描线效果
扫描线中间不透明,逐渐到两侧透明,并且要让它从下到上动起来。
先考虑扫描线的效果,这里需要用到一点数学知识。扫描线中心透明度为1,向两侧降为0,也就是这样的效果:
我们需要y轴上方的这部分,也就是说当这个值大于0的时候,我们进行颜色混合,使其呈现出扫描线的效果。而这个图形正是抛物线倒过来再加上一个值,即 -x * x + b ,这里x就是顶点的y坐标,b就需要我们设置了,这个值越大,y轴上方的部分就越大,也就是扫描线越宽。此外,我们还想让它动起来,因此还需要再增加一个变量,用来控制移动,最终得到的公式就是 -(x - t) * (x - t) + b 。代码实现如下:
// 设置两个全局变量,分别对应上述公式中的t和b
shader.uniforms.uToTopTime = { value: 0 }
shader.uniforms.uToTopWidth = { value: 40 }
片元着色器中接收并进行颜色混合:
// 片元着色器
uniform float uToTopTime;
uniform float uToTopWidth;
void main() {
// ...
float colorToTopMix = -(vPosition.y - uToTopTime) * (vPosition.y - uToTopTime) + uToTopWidth;
if (colorToTopMix > 0.0) { // 大于0时进行颜色混合
gl_FragColor = mix(gl_FragColor, vec4(0.8, 0.8, 1.0, 1.0), colorToTopMix / uToTopWidth);
}
}
接着我们要修改uToTopTime变量,使它能够动起来,这里我用了gsap动画库:
gsap.to(shader.uniforms.uToTopTime, {
value: 300,
duration: 3,
ease: 'none',
repeat: -1
})
四、光墙效果
光墙的效果跟之前渐变色的效果类似,只是渐变的是透明度。墙可以使用Three.js内置的几何体来创建。这里我用的是CylinderGeometry,创建了一个两端开口的正方体:
const geometry = new THREE.CylinderGeometry(16, 16, 3, 4, 1, true)
// 着色器材质
const material = new THREE.ShaderMaterial({
transparent: true,
vertexShader: vertex, // 指定顶点着色器
fragmentShader: fragment, // 指定片元着色器
side: THREE.DoubleSide,
depthWrite: false
})
const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh)
同样,这里需要将几何体的高度传给片元着色器,顶点的y坐标值越大越透明:
uniform float uHeight;
uniform vec3 lightWallColor;
varying vec3 vPosition;
void main() {
float alphaMix = (vPosition.y + uHeight / 2.0) / uHeight;
gl_FragColor = vec4(lightWallColor, 1.0 - alphaMix);
}
五、总结
通过以上案例可以对着色器有个初步的认识,通过着色器,我们可以实现非常酷炫的效果,另外,由于着色器直接运行在GPU上,所以它的性能也是非常高的。