Three.js着色器进阶:光照、顶点位移与动画实现
本文是Three.js着色器教程的下篇,深入讲解如何在WebGL中实现动态效果。内容涵盖通过varying变量传递法向量以模拟光照,利用attribute变量对顶点进行随机位移变形,以及使用uniform变量结合requestAnimationFrame实现平滑的脉动动画。
Three.js与WebGL基础
Three.js是一款优秀的开源WebGL库,它允许JavaScript直接操作GPU,从而在浏览器中实现真正的3D渲染。然而,该技术目前仍处于发展阶段,相关学习资料相对稀缺,开发者通常需要依赖Demo源码以及Three.js自身的源代码来进行学习。
.简介
本文为WebGL着色器教程的后续部分。若读者未阅读前篇内容,可能会感到困惑,建议先回顾之前的教程以建立基础认知。
在上一篇教程的结尾,我们在屏幕中央绘制了一个美观的粉红色球体。接下来,我们将构建更具视觉冲击力的效果。
着色器变量与动画目标
本教程将首先引入动画循环,并介绍顶点属性变量(attribute)和统一变量(uniform)。此外,我们还将添加变化变量(varying),以便顶点着色器能将数据传输给片元着色器。最终效果是:粉红色球体将从顶部开始向两侧“燃烧”,并进行有规律的动态运动。虽然视觉效果略显迷幻,但这有助于深入理解着色器中三种变量的相互作用及其整体协作机制。所有操作均在Three.js框架内完成。
1.模拟光照
首先更新颜色,使球体不再呈现为扁平晦暗的圆形。若直接探究Three.js的光照处理机制,会发现其复杂度远超实际需求,因此我们先采用模拟光照的方式。建议读者浏览Three.js中丰富的着色器示例,以及Chris Milk与Google Rome合作的WebGL项目,以获取更多灵感。
模拟光照:法向量传递
回到着色器代码,我们需要更新顶点着色器,使其将顶点的法向量传递给片元着色器。这可以通过定义一个varying变量来实现:
具体代码如下:
// 声明一个varying变量vNormal,该变量在顶点着色器和片元着色器中均需存在
varying vec3 vNormal;
void main() {
// 将vNormal赋值为normal,normal是Three.js创建并传递给着色器的attribute变量
vNormal = normal;
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(position, 1.0);
} 在片元着色器中,我们将声明同名变量,并将法线向量与一个表示来自右上方光源方向的向量进行点积运算,最终结果用于计算颜色。这种处理方式模拟了平行光的效果。
具体代码如下:
// 声明与顶点着色器中相同的变量vNormal
varying vec3 vNormal;
void main() {
// 定义光线方向向量
vec3 light = vec3(0.5,0.2,1.0);
// 对光线向量进行归一化处理
light = normalize(light);
// 计算光线向量与法线向量的点积。若点积小于0(表示光线无法照射到该面),则设为0
float dProd = max(0.0, dot(vNormal, light));
// 填充片元颜色
gl_FragColor = vec4(dProd, // R
dProd, // G
dProd, // B
1.0); // A
} 使用点积的原因是:两个向量的点积反映了它们方向的相似程度。若两个向量均已归一化且方向完全一致,点积值为1;若方向完全相反,点积值为-1。我们将点积结果应用于颜色计算,因此球体右上方区域的点积值为1,表示完全照亮;而背对光源的区域点积值接近0甚至为-1。我们将所有负值截断为0,从而呈现出基本的光照效果。
接下来,我们将顶点坐标引入计算。
2.Attribut变量
随后,我将通过Attribute变量为每个顶点传递一个随机数,该随机数用于沿法线方向将顶点推离一段距离。新效果类似于一个怪异的不规则物体,每次刷新页面时形状都会随机变化。目前物体尚未运动(后续将添加动画),但多次刷新即可观察到其形状的随机性。
顶点位移:Attribute变量应用
现在开始为顶点着色器添加attribute变量:
具体代码如下:
attribute float displacement;
varying vec3 vNormal;
void main() {
vNormal = normal;
// 将随机数displacement转换为三维向量,以便与法线相乘
vec3 newPosition = position +
normal * vec3(displacement);
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(newPosition, 1.0);
} 此时画面看似无变化,因为attribute变量displacement尚未赋值,着色器默认使用0。虽然displacement尚未生效,但稍后我们将在着色器材质中配置该变量,Three.js会自动将其绑定并运行。
同时请注意,我们将更新后的位置赋值给了一个新的三维向量变量。这是因为原始的位置变量position作为attribute变量,是只读的。
3.更新着色器材质
接下来更新着色器材质,向attribute对象displacement传入数据。注意,attribute变量与顶点一一对应,因此我们需要为球体的每个顶点提供一个值,如下所示:
具体代码如下:
var attributes = {
displacement: {
type: 'f', // 浮点数类型
value: [] // 空数组
}
};
var vShader = $('#vertexshader');
var fShader = $('#fragmentshader');
// 创建包含attribute属性的着色器材质
var shaderMaterial =
new THREE.MeshShaderMaterial({
attributes: attributes,
vertexShader: vShader.text(),
fragmentShader: fShader.text()
});
// 向displacement数组填充随机数
var verts = sphere.geometry.vertices;
var values = attributes.displacement.value;
for(var v = 0; v < verts.length; v++) {
values.push(Math.random() * 30);
} 这样即可看到变形的球体。最酷之处在于:所有变形计算均在GPU中完成。
4.动起来
如何使该物体动起来?我们需要执行以下两步操作。
首先,定义一个uniform变量amplitude,用于控制每一帧中displacement实际产生的位移量。我们可以利用正弦或余弦函数在每一帧生成该值,因为它们的取值范围在-1到1之间。
其次,构建帧循环。
我们需要将此uniform变量加入着色器材质及顶点着色器。先查看顶点着色器的修改:
动态效果:Uniform变量与帧循环
具体代码如下:
uniform float amplitude;
attribute float displacement;
varying vec3 vNormal;
void main() {
vNormal = normal;
// 将displacement乘以amplitude,当我们在每一帧中平滑改变amplitude时,画面即产生动态效果
vec3 newPosition =
position +
normal *
vec3(displacement *
amplitude);
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(newPosition, 1.0);
} 随后更新着色器材质配置:
具体代码如下:
var uniforms = {
amplitude: {
type: 'f', // 浮点数类型
value: 0
}
};
var vShader = $('#vertexshader');
var fShader = $('#fragmentshader');
// 创建最终的着色器材质
var shaderMaterial =
new THREE.MeshShaderMaterial({
uniforms: uniforms,
attributes: attributes,
vertexShader: vShader.text(),
fragmentShader: fShader.text()
}); 此时着色器已就绪,但屏幕中似乎又变回了光滑球体。无需担心,这是因为amplitude初始值为0,导致displacement被乘以0,因此看不到任何变化。由于尚未设置循环,amplitude始终为0。
在JavaScript中,我们需要将渲染过程封装为一个函数,并通过requestAnimationFrame调用该函数。在此函数中,我们将更新uniform(即amplitude)的值。
具体代码如下:
var frame = 0;
function update() {
// amplitude的值由frame的正弦值决定
uniforms.amplitude.value =
Math.sin(frame);
// 更新全局变量frame
frame += 0.1;
renderer.render(scene, camera);
// 指定在下次屏幕刷新时调用update函数
requestAnimFrame(update);
}
requestAnimFrame(update); 5.小结
至此完成!你将看到球体正在奇怪地脉动。关于着色器的内容尚多,但希望本教程对你有所助益。现在,当你看到其他着色器示例时,希望你能理解其原理,并有信心创建自己的着色器!
与往常一样,我将本课的源码打包提供。
vtk.js 渲染窗口黑屏或无内容显示的常见原因及排查
2026-11-05 17:30:30
js-cookie 安装与基础用法:set、get、remove 详解
2026-11-05 16:35:06
适合JavaScript初学者的实战项目:制作简易待办事项列表
2026-11-05 16:17:07
JS打开新标签页时怎么通过URL传递参数
2026-11-05 12:21:08
sigma.js 自定义节点样式:设置颜色、大小及标签显示的技巧
2026-11-05 09:55:00
