Three.js案例
text1

本项目将构建一个令人惊艳的交互式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_posu_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_positionnormal_at 存储几何体的原始位置和法向量数据
  • position_storage_atvelocity_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_coloremissive_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)