本项目将构建一个令人惊艳的交互式3D文字效果——当鼠标在立体文字上滑动时,文字表面会产生动态的"爆炸"变形,配合炫彩发光和物理回弹效果,营造出科幻感十足的视觉体验。
先看下效果:
模块导入
创建 main.js
文件,开始构建核心逻辑。首先导入必要的 Three.js 模块和工具。
import * as THREE from 'three/webgpu';
import { FontLoader, RoomEnvironment, TextGeometry } from 'three/examples/jsm/Addons.js';
import {
instancedBufferAttribute,
float,
color,
uv,
metalness,
hue,
length,
step,
mix,
vec3,
positionLocal,
roughness,
storage,
attribute,
Fn,
instanceIndex,
mx_noise_vec3,
time,
uniform,
rotate,
mx_noise_float,
transmission,
dispersion,
ior,
instancedArray,
thickness,
sheen,
iridescence,
smoothstep,
blur,
oneMinus,
sub,
pass,
mrt,
output,
normalView,
} from 'three/tsl';
import { bloom } from 'three/examples/jsm/tsl/display/BloomNode.js';
import { ao } from 'three/examples/jsm/tsl/display/GTAONode.js';
import { denoise } from 'three/examples/jsm/tsl/display/DenoiseNode.js';
import * as dat from 'dat.gui';
- 导入 WebGPU 版本的 Three.js 核心库
- 引入字体加载器、环境贴图和文字几何体生成器
- TSL (Three Shading Language) 模块提供 GPU 计算和着色器节点功能
- 后处理效果模块包括泛光、环境遮蔽和去噪
- dat.GUI 用于创建调试参数面板
设置资源管理和预加载系统:
const Resources = {
font: undefined,
};
function preload() {
const _font_loader = new FontLoader();
_font_loader.load('./assets/Times New Roman_Regular.json', (font) => {
Resources.font = font;
init();
});
}
window.onload = preload;
Resources
对象统一管理项目中使用的外部资源preload
函数负责异步加载字体文件- 字体加载完成后自动调用
init
函数开始初始化场景
场景基础设置
创建 Three.js 场景的基本组件。这包括场景对象、透视相机和 WebGPU 渲染器。
在 main.js
中添加 init
函数:
function init() {
document.body.classList.remove('loading');
const sizes = {
width: window.innerWidth,
height: window.innerHeight,
};
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, sizes.width / sizes.height, 0.1, 100);
const renderer = new THREE.WebGPURenderer({ antialias: true, canvas: document.getElementById('canvas') });
renderer.toneMapping = THREE.CineonToneMapping;
renderer.toneMappingExposure = 0.5;
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
renderer.setSize(sizes.width, sizes.height);
camera.position.z = 5.0;
scene.add(camera);
- 移除
loading
类,隐藏加载提示 sizes
对象存储当前窗口尺寸,用于响应式设计- 创建透视相机,视场角设为60度,适合3D文字展示
- WebGPU 渲染器启用抗锯齿,绑定到 HTML 中的 canvas 元素
- 色调映射使用 Cineon 模式,营造电影感视觉效果
- 限制像素比例,在高DPI设备上平衡性能和质量
环境光照配置
设置场景的环境光照系统,包括雾效、背景色、环境光照贴图和直接光源。
在 init
函数中继续添加:
// ... existing code ...
scene.add(camera);
scene.fog = new THREE.Fog(new THREE.Color('#41444c'), 0.0, 8.5);
scene.background = scene.fog.color;
const environment = new RoomEnvironment();
const pmremGenerator = new THREE.PMREMGenerator(renderer);
scene.environment = pmremGenerator.fromSceneAsync(environment).texture;
scene.environmentIntensity = 0.8;
const light = new THREE.DirectionalLight('#e7e2ca', 5);
light.position.x = 0.0;
(light.position.y = 1.2), (light.position.z = 3.86);
scene.add(light);
- 雾效从近处的完全透明渐变到远处的灰色,增强景深感
- 背景色与雾色保持一致,形成无缝的视觉过渡
RoomEnvironment
创建室内环境光照,提供真实的反射和环境光- PMREM 生成器将环境贴图转换为预滤波的辐射环境贴图
- 平行光源使用暖白色,强度较高,模拟主光源照明
3D文字几何体创建
使用 Three.js 的 TextGeometry
创建3D文字几何体,并进行居中处理。
继续在 init
函数中添加:
// ... existing code ...
scene.add(light);
const text_geo = new TextGeometry('NUEVOS', {
font: Resources.font,
size: 1.0,
depth: 0.2,
bevelEnabled: true,
bevelThickness: 0.1,
bevelSize: 0.01,
bevelOffset: 0,
bevelSegments: 1,
});
text_geo.computeBoundingBox();
const centerOffset = -0.5 * (text_geo.boundingBox.max.x - text_geo.boundingBox.min.x);
const centerOffsety = -0.5 * (text_geo.boundingBox.max.y - text_geo.boundingBox.min.y);
text_geo.translate(centerOffset, centerOffsety, 0);
const mesh = new THREE.Mesh(
text_geo,
new THREE.MeshStandardMaterial({
color: '#656565',
metalness: 0.4,
roughness: 0.3,
})
);
scene.add(mesh);
TextGeometry
将文字"NUEVOS"转换为3D几何体size
控制文字大小,depth
设置文字厚度- 启用斜面效果 (
bevelEnabled
) 让文字边缘更圆润 computeBoundingBox
计算几何体的边界框- 通过计算边界框的中心点,将文字几何体居中对齐
- 使用
MeshStandardMaterial
创建基于物理的材质,设置适中的金属度和粗糙度
鼠标交互系统
实现鼠标与3D文字的交互检测,使用射线投射技术获取鼠标在3D空间中与文字表面的交点。
在 scene.add(mesh);
后继续添加:
// ... existing code ...
scene.add(mesh);
const u_input_pos = uniform(new THREE.Vector3(0, 0, 0));
const u_input_pos_press = uniform(0.0);
const ray_cast = new THREE.Raycaster();
window.addEventListener(
'pointerup',
(event) => {
u_input_pos_press.value = 0.0;
},
{ passive: false }
);
window.addEventListener(
'pointermove',
(event) => {
const x = event.clientX / sizes.width - 0.5;
const y = event.clientY / sizes.height - 0.5;
const _p = new THREE.Vector2(x, -y).multiplyScalar(2.0);
ray_cast.setFromCamera(_p, camera);
const intersect = ray_cast.intersectObject(mesh, true);
if (intersect.length) {
u_input_pos_press.value = 1.0;
u_input_pos.value.copy(intersect[0].point);
}
},
{ passive: false }
);
u_input_pos
和u_input_pos_press
是 GPU 着色器的 uniform 变量Raycaster
用于检测鼠标位置与3D对象的交点pointerup
事件将交互状态重置为0,表示停止交互pointermove
事件将鼠标屏幕坐标转换为标准化设备坐标 (-1到1)- Y坐标取负值是因为屏幕坐标系与WebGL坐标系Y轴方向相反
intersectObject
检测射线与文字网格的交点- 如果检测到交点,将交互状态设为1,并记录3D空间中的交点位置
参数面板配置
使用 dat.GUI 创建实时参数调节面板,允许动态调整动画效果的各项参数。
在交互事件监听器后添加:
// ... existing code ...
);
const parameters = {
noise_amp: 1.6,
text_color: '#212121',
emissive_color: '#0000ff',
spring: 0.05,
friction: 0.9,
emissive_bust: 5.0,
explode_amp: 1.5,
};
const gui = new dat.GUI();
gui.addColor(parameters, 'text_color').onChange((value) => {
mesh.material.color = new THREE.Color(value);
});
gui.addColor(parameters, 'emissive_color').onChange((value) => {
emissive_color.value = new THREE.Color(value);
});
gui
.add(parameters, 'emissive_bust', 1.0, 10.0, 0.1)
.name('Emissive Boost')
.onChange((value) => {
emissive_bust.value = value;
});
gui
.add(parameters, 'noise_amp', 0.0, 3.0, 0.01)
.name('Noise Amplitude')
.onChange((value) => {
u_noise_amp.value = value;
});
gui
.add(parameters, 'explode_amp', 0.1, 2.5, 0.01)
.name('Explode Amplitude')
.onChange((value) => {
u_explode_amp.value = value;
});
gui
.add(parameters, 'spring', 0.0, 0.1, 0.01)
.name('Spring')
.onChange((value) => {
u_spring.value = value;
});
gui
.add(parameters, 'friction', 0.0, 0.99, 0.001)
.name('Friction')
.onChange((value) => {
u_friction.value = value;
});
parameters
对象包含所有可调节的动画参数及其默认值- 颜色选择器控制文字基础色和发光色
- 数值滑块控制物理动画参数:弹簧系数、摩擦力、爆炸强度等
onChange
回调函数将界面参数变化同步到GPU uniform变量- 参数范围经过精心设置,确保在有效范围内产生明显的视觉变化
GPU计算着色器初始化
设置 GPU 存储缓冲区和计算着色器,用于管理每个顶点的位置和速度数据。
在参数面板配置后添加:
// ... existing code ...
});
const u_noise_amp = uniform(parameters.noise_amp);
const u_spring = uniform(parameters.spring);
const u_friction = uniform(parameters.friction);
const u_explode_amp = uniform(parameters.explode_amp);
const count = text_geo.attributes.position.count;
const initial_position = storage(text_geo.attributes.position, 'vec3', count);
const normal_at = storage(text_geo.attributes.normal, 'vec3', count);
const position_storage_at = storage(new THREE.StorageBufferAttribute(count, 3), 'vec3', count);
const velocity_storage_at = storage(new THREE.StorageBufferAttribute(count, 3), 'vec3', count);
const compute_init = Fn(() => {
position_storage_at.element(instanceIndex).assign(initial_position.element(instanceIndex));
velocity_storage_at.element(instanceIndex).assign(vec3(0.0, 0.0, 0.0));
})().compute(count);
renderer.computeAsync(compute_init);
- 创建对应参数的 uniform 变量,连接 GUI 控制面板与 GPU 计算
count
获取文字几何体的顶点总数initial_position
和normal_at
存储几何体的原始位置和法向量数据position_storage_at
和velocity_storage_at
是动态更新的位置和速度缓冲区compute_init
是初始化计算着色器,将每个顶点设置到初始位置,速度为零renderer.computeAsync
在GPU上异步执行初始化计算
顶点动画计算着色器
实现核心的顶点动画逻辑,包括弹簧物理、噪声扰动、距离影响和旋转效果。
在初始化计算着色器后添加:
// ... existing code ...
renderer.computeAsync(compute_init);
const compute_update = Fn(() => {
const base_position = initial_position.element(instanceIndex);
const current_position = position_storage_at.element(instanceIndex);
const current_velocity = velocity_storage_at.element(instanceIndex);
const normal = normal_at.element(instanceIndex);
const noise = mx_noise_vec3(current_position.mul(0.5).add(vec3(0.0, time, 0.0)), 1.0).mul(u_noise_amp);
const distance = length(u_input_pos.sub(base_position));
const pointer_influence = step(distance, 0.5).mul(u_explode_amp);
const disorted_pos = base_position.add(noise.mul(normal.mul(pointer_influence)));
disorted_pos.assign(rotate(disorted_pos, vec3(normal.mul(distance)).mul(pointer_influence)));
disorted_pos.assign(mix(base_position, disorted_pos, u_input_pos_press));
current_velocity.addAssign(disorted_pos.sub(current_position).mul(u_spring));
current_position.addAssign(current_velocity);
current_velocity.assign(current_velocity.mul(u_friction));
})().compute(count);
mesh.material.positionNode = position_storage_at.toAttribute();
- 每个顶点获取其原始位置、当前位置和速度
mx_noise_vec3
生成基于时间的3D噪声,添加自然的随机扰动- 计算鼠标位置与顶点的距离,使用
step
函数创建0.5单位的影响半径 pointer_influence
控制变形强度,距离越近影响越大- 沿顶点法向量方向应用噪声扰动,形成"爆炸"效果
rotate
函数让顶点围绕法向量轴旋转,增加动态感mix
函数根据鼠标按压状态混合原始位置和变形位置- 弹簧物理系统:计算位置差产生恢复力,更新速度和位置
- 摩擦力逐渐减慢速度,让动画自然停止
动态发光效果
基于顶点速度实现动态颜色变化和发光效果,速度越快的顶点发光越强烈。
在位置节点设置后添加:
// ... existing code ...
mesh.material.positionNode = position_storage_at.toAttribute();
const emissive_color = color(parameters.emissive_color);
const emissive_bust = uniform(parameters.emissive_bust);
const vel_at = velocity_storage_at.toAttribute();
const hue_rotated = vel_at.mul(Math.PI * 10.0);
const emission_factor = length(vel_at).mul(10.0);
mesh.material.emissiveNode = hue(emissive_color, hue_rotated).mul(emission_factor).mul(emissive_bust);
emissive_color
和emissive_bust
控制发光的基础颜色和强度vel_at
将速度缓冲区转换为顶点属性,可在材质中访问hue_rotated
根据速度大小旋转色相,产生彩虹色效果emission_factor
将速度长度转换为发光强度因子- 最终的发光节点结合色相变化、速度强度和用户设置的增强值
- 快速运动的顶点会呈现更亮、更鲜艳的颜色
后处理效果管线
配置多重后处理效果,包括环境遮蔽、去噪、泛光和胶片噪点,提升最终画面质量。
在发光效果后添加:
// ... existing code ...
mesh.material.emissiveNode = hue(emissive_color, hue_rotated).mul(emission_factor).mul(emissive_bust);
const composer = new THREE.PostProcessing(renderer);
const scene_pass = pass(scene, camera);
scene_pass.setMRT(
mrt({
output: output,
normal: normalView,
})
);
const scene_color = scene_pass.getTextureNode('output');
const scene_depth = scene_pass.getTextureNode('depth');
const scene_normal = scene_pass.getTextureNode('normal');
const ao_pass = ao(scene_depth, scene_normal, camera);
ao_pass.resolutionScale = 1.0;
const ao_denoise = denoise(ao_pass.getTextureNode(), scene_depth, scene_normal, camera).mul(scene_color);
const bloom_pass = bloom(ao_denoise, 0.5, 0.2, 0.1);
const post_noise = mx_noise_float(vec3(uv(), time.mul(0.1)).mul(sizes.width), 0.03).mul(1.0);
composer.outputNode = ao_denoise.add(bloom_pass).add(post_noise);
PostProcessing
创建后处理管线,管理多个渲染通道scene_pass
渲染场景到多个渲染目标 (MRT)- MRT 同时输出颜色、深度和法向量信息
ao_pass
使用深度和法向量计算环境遮蔽,增强立体感denoise
对环境遮蔽进行降噪处理,减少噪点bloom_pass
对亮区进行泛光处理,参数控制阈值、强度和半径post_noise
添加基于时间的胶片噪点,增加质感- 最终输出合成环境遮蔽、泛光和噪点效果
渲染循环与响应式处理
实现主渲染循环和窗口大小变化的响应式处理,确保项目在不同设备上正常运行。
在后处理配置后添加:
// ... existing code ...
composer.outputNode = ao_denoise.add(bloom_pass).add(post_noise);
renderer.setAnimationLoop(animate);
function animate() {
renderer.computeAsync(compute_update);
composer.renderAsync();
}
window.addEventListener('resize', () => {
sizes.width = window.innerWidth;
sizes.height = window.innerHeight;
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix();
renderer.setSize(sizes.width, sizes.height);
});
}
setAnimationLoop
设置渲染循环,自动匹配显示器刷新率animate
函数每帧执行,先更新GPU计算再渲染后处理computeAsync
异步执行顶点动画计算着色器renderAsync
异步渲染整个后处理管线- 监听窗口大小变化事件,实时更新相机宽高比和渲染器尺寸
updateProjectionMatrix
重新计算投影矩阵,保证画面比例正确
源码
https://github.com/armdz/tsl_elastic_vertex_destruction (opens in a new tab)