Threejs使用着色器给白模添加特效

2023-12-25 11:34 月霖 751

着色器是运行在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上,所以它的性能也是非常高的。