交互式文字爆炸效果:Three.js、WebGPU和TSL实战
项目概述
本教程将指导您创建一个交互式3D文字效果,其中字母会爆炸成动态形状。我们将使用Three.js、WebGPU和Three Shader Language(TSL)来实现这一效果。
项目结构
我们的脚本结构简单:一个函数用于预加载资源,另一个用于构建场景。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
const Resources = {
font: null
};
function preload() {
const _font_loader = new FontLoader();
_font_loader.load("../static/font/Times New Roman_Regular.json", (font) => {
Resources.font = font;
init();
});
}
function init() {
// 初始化场景
}
window.onload = preload;
|
场景设置与环境
经典的Three.js场景设置,需要注意的是我们使用TSL,因此渲染器需要是WebGPURenderer。
1
2
3
4
5
6
7
8
|
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGPURenderer({ antialias: true });
document.body.appendChild(renderer.domElement);
renderer.setSize(window.innerWidth, window.innerHeight);
camera.position.z = 5;
scene.add(camera);
|
环境光照设置
使用Three.js自带的RoomEnvironment和DirectionalLight:
1
2
3
4
5
6
7
8
|
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.set(0.0, 1.2, 3.86);
scene.add(light);
|
文字几何体
使用TextGeometry创建3D文字:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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
});
const mesh = new THREE.Mesh(
text_geo,
new THREE.MeshStandardMaterial({
color: "#656565",
metalness: 0.4,
roughness: 0.3
})
);
scene.add(mesh);
|
几何体居中处理
1
2
3
4
|
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);
|
Three Shader Language实现
初始化存储缓冲区
1
2
3
4
5
6
7
8
9
10
11
|
const initial_position = storage(text_geo.attributes.position, "vec3", count);
const normal_at = storage(text_geo.attributes.normal, "vec3", count);
const count = text_geo.attributes.position.count;
const position_storage_at = storage(new THREE.StorageBufferAttribute(count, 3), "vec3", count);
const compute_init = Fn(() => {
position_storage_at.element(instanceIndex).assign(initial_position.element(instanceIndex));
})().compute(count);
renderer.computeAsync(compute_init);
|
更新计算函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
const u_input_pos = uniform(new THREE.Vector3(0, 0, 0));
const u_input_pos_press = uniform(0.0);
const compute_update = Fn(() => {
const base_position = initial_position.element(instanceIndex);
const normal = normal_at.element(instanceIndex);
const current_position = position_storage_at.element(instanceIndex);
const distance = length(u_input_pos.sub(base_position));
const pointer_influence = step(distance, 0.5).mul(1.0);
const disorted_pos = base_position.add(normal.mul(pointer_influence));
current_position.assign(disorted_pos);
})().compute(count);
|
弹簧物理实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
const velocity_storage_at = storage(new THREE.StorageBufferAttribute(count, 3), "vec3", count);
const u_spring = uniform(0.05);
const u_friction = uniform(0.9);
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 distance = length(u_input_pos.sub(base_position));
const pointer_influence = step(distance, 0.5).mul(1.5);
const disorted_pos = base_position.add(normal.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_velocity.assign(current_velocity.mul(u_friction));
current_position.addAssign(current_velocity);
})().compute(count);
|
添加噪声和旋转效果
1
2
3
4
5
6
7
8
9
|
const compute_update = Fn(() => {
// ...之前的代码
const noise = mx_noise_vec3(current_position.mul(0.5).add(vec3(0.0, time, 0.0)), 1.0).mul(u_noise_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)));
// ...后续代码
})().compute(count);
|
材质和发光效果
1
2
3
4
5
6
|
const emissive_color = color(new THREE.Color("0000ff"));
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(5.0);
|
场景雾化和背景
1
2
|
scene.fog = new THREE.Fog(new THREE.Color("#41444c"), 0.0, 8.5);
scene.background = scene.fog.color;
|
后期处理效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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.3, 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);
|
动画循环
1
2
3
4
5
6
7
|
function animate() {
renderer.computeAsync(compute_update);
renderer.renderAsync(scene, camera);
requestAnimationFrame(animate);
}
animate();
|
通过本教程,您已经学会了如何使用Three.js、WebGPU和TSL创建复杂的交互式3D文字效果,包括物理模拟、着色器编程和后期处理等高级技术。