普通视图

发现新文章,点击刷新页面。
昨天 — 2026年1月28日首页

Three.js 变形动画-打造花瓣绽放

2026年1月28日 18:01

概述

本文将详细介绍如何使用 Three.js 实现变形动画效果。我们将学习如何利用 Morph Targets(形态目标)技术,让 3D 模型在不同形状之间平滑过渡,创造出花瓣绽放等生动的动画效果。

screenshot_2026-01-28_17-56-51.gif

准备工作

首先,我们需要引入必要的 Three.js 库和相关工具:

import * as THREE from "three";
// 导入轨道控制器
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
// 导入动画库
import gsap from "gsap";
// 导入dat.gui
import * as dat from "dat.gui";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader";

场景初始化

首先,我们需要创建一个基本的 Three.js 场景:

const gui = new dat.GUI();
// 1、创建场景
const scene = new THREE.Scene();

// 2、创建相机
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
camera.aspect = window.innerWidth / window.innerHeight;
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix();

// 设置相机位置
camera.position.set(0, 0, 20);
scene.add(camera);

环境设置

添加 HDR 环境纹理,提升场景的真实感:

// 添加hdr环境纹理
const loader = new RGBELoader();
loader.load("./textures/038.hdr", function (texture) {
  texture.mapping = THREE.EquirectangularReflectionMapping;
  scene.environment = texture;
});

DRACO 压缩模型加载

使用 DRACO 压缩技术加载 GLB 模型:

// 加载压缩的glb模型
const gltfLoader = new GLTFLoader();
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath("./draco/gltf/");
dracoLoader.setDecoderConfig({ type: "js" });
dracoLoader.preload();
gltfLoader.setDRACOLoader(dracoLoader);

变形动画核心实现

这是变形动画的关键部分,通过 Morph Targets 技术实现模型变形:

let params = {
  value: 0,
  value1: 0,
};

let mixer;
let stem, petal, stem1, petal1, stem2, petal2;

// 加载第一个模型(初始状态)
gltfLoader.load("./model/f4.glb", function (gltf1) {
  console.log(gltf1);
  stem = gltf1.scene.children[0];
  petal = gltf1.scene.children[1];
  gltf1.scene.rotation.x = Math.PI;

  // 遍历场景中的对象并处理材质
  gltf1.scene.traverse((item) => {
    if (item.material && item.material.name == "Water") {
      item.material = new THREE.MeshStandardMaterial({
        color: "skyblue",
        depthWrite: false,
        transparent: true,
        depthTest: false,
        opacity: 0.5,
      });
    }
    if (item.material && item.material.name == "Stem") {
      stem = item;
    }
    if (item.material && item.material.name == "Petal") {
      petal = item;
    }
  });

  // 加载第二个模型(中间状态)
  gltfLoader.load("./model/f2.glb", function (gltf2) {
    gltf2.scene.traverse((item) => {
      if (item.material && item.material.name == "Stem") {
        stem1 = item;
        // 将第二个模型的几何体作为第一个形态目标添加到基础模型
        stem.geometry.morphAttributes.position = [
          stem1.geometry.attributes.position,
        ];
        stem.updateMorphTargets();
      }
      if (item.material && item.material.name == "Petal") {
        petal1 = item;
        // 将第二个模型的几何体作为第一个形态目标添加到基础模型
        petal.geometry.morphAttributes.position = [
          petal1.geometry.attributes.position,
        ];
        petal.updateMorphTargets();
        console.log(petal.morphTargetInfluences);
      }

      // 加载第三个模型(最终状态)
      gltfLoader.load("./model/f1.glb", function (gltf2) {
        gltf2.scene.traverse((item) => {
          if (item.material && item.material.name == "Stem") {
            stem2 = item;
            // 将第三个模型的几何体作为第二个形态目标添加到基础模型
            stem.geometry.morphAttributes.position.push(
              stem2.geometry.attributes.position
            );
            stem.updateMorphTargets();
          }
          if (item.material && item.material.name == "Petal") {
            petal2 = item;
            // 将第三个模型的几何体作为第二个形态目标添加到基础模型
            petal.geometry.morphAttributes.position.push(
              petal2.geometry.attributes.position
            );
            petal.updateMorphTargets();
            console.log(petal.morphTargetInfluences);
          }
        });
      });
    });
  });

  // 使用 GSAP 创建变形动画
  gsap.to(params, {
    value: 1,
    duration: 4,
    onUpdate: function () {
      // 控制第一个形态目标的影响程度
      stem.morphTargetInfluences[0] = params.value;
      petal.morphTargetInfluences[0] = params.value;
    },
    onComplete: function () {
      // 在第一个动画完成后,开始第二个变形动画
      gsap.to(params, {
        value1: 1,
        duration: 4,
        onUpdate: function () {
          // 控制第二个形态目标的影响程度
          stem.morphTargetInfluences[1] = params.value1;
          petal.morphTargetInfluences[1] = params.value1;
        },
      });
    },
  });

  scene.add(gltf1.scene);
});

渲染器设置

配置 WebGL 渲染器:

// 初始化渲染器
const renderer = new THREE.WebGLRenderer({
  logarithmicDepthBuffer: true,
  antialias: true,
});
// 设置渲染的尺寸大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 开启场景中的阴影贴图
renderer.shadowMap.enabled = true;
renderer.physicallyCorrectLights = true;
renderer.setClearColor(0xcccccc, 1);
renderer.autoClear = false;
// 设置电影渲染模式
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.sortObjects = true;
renderer.logarithmicDepthBuffer = true;

// 将webgl渲染的canvas内容添加到body
document.body.appendChild(renderer.domElement);

控制器和动画循环

设置轨道控制器和渲染循环:

// 创建轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 设置控制器阻尼,让控制器更有真实效果,必须在动画循环里调用.update()。
controls.enableDamping = true;

// 添加坐标轴辅助器
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);

// 设置时钟
const clock = new THREE.Clock();
function render() {
  let time = clock.getDelta();
  if (mixer) {
    mixer.update(time);
  }
  controls.update();

  renderer.render(scene, camera);
  // 渲染下一帧的时候就会调用render函数
  requestAnimationFrame(render);
}

render();

变形动画原理详解

Morph Targets(形态目标)是一种在计算机图形学中用于实现网格变形的技术。其基本原理是:

  1. 基础几何体: 定义一个基础的网格几何体
  2. 目标几何体: 定义一个或多个"目标"几何体,它们与基础几何体有相同的拓扑结构(相同的顶点数量和连接关系),但顶点位置不同
  3. 权重控制: 通过权重值(0到1之间)来控制目标几何体对基础几何体的影响程度

在代码中,我们使用 morphTargetInfluences 数组来控制每个形态目标的影响程度:

  • morphTargetInfluences[0] = 0 时,模型呈现初始状态
  • morphTargetInfluences[0] = 1 时,模型完全变成第一个目标状态
  • morphTargetInfluences[0] = 0.5 时,模型是初始状态和目标状态的中间形态

应用场景

变形动画在 3D 应用中有广泛的应用:

  1. 角色面部表情: 实现人物的表情变化
  2. 物体形态变化: 如花朵绽放、物体变形等
  3. 动画过渡: 在不同模型状态之间平滑过渡
  4. 程序化生成: 根据参数动态改变模型形状

性能优化建议

  1. 合理使用: Morph Targets 会增加内存消耗,只在必要时使用
  2. 减少目标数量: 尽量减少形态目标的数量以提高性能
  3. 压缩模型: 使用 DRACO 等压缩技术减少模型文件大小
  4. 优化动画: 使用高效的动画库如 GSAP 来控制变形过程

总结

通过这个项目,我们学习了如何使用 Three.js 的 Morph Targets 技术:

  1. 如何加载多个具有相同拓扑结构的模型
  2. 如何将目标模型的几何体作为形态目标添加到基础模型
  3. 如何通过控制权重来实现平滑的变形动画
  4. 如何使用 GSAP 等动画库来管理复杂的动画序列

变形动画是一个强大而灵活的技术,能够为你的 3D 应用增添生动有趣的视觉效果,特别是在创建有机形态变化(如植物生长、花朵绽放等)方面特别有效。

Three.js 曲线应用详解

2026年1月28日 17:51

概述

本文将详细介绍如何使用 Three.js 中的曲线功能。我们将学习如何创建 CatmullRomCurve3 样条曲线,并利用曲线来控制物体的运动轨迹,以及如何将曲线可视化地显示在场景中。

screenshot_2026-01-28_17-49-20.gif

准备工作

首先,我们需要引入必要的 Three.js 库:

import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

场景初始化

首先,我们需要创建一个基本的 Three.js 场景:

let camera, scene, renderer;
const clock = new THREE.Clock();
const textureLoader = new THREE.TextureLoader();

let moon;
let earth;
let curve;
const raycaster = new THREE.Raycaster();

function init() {
  const EARTH_RADIUS = 1;
  const MOON_RADIUS = 0.27;

  // 创建透视相机
  camera = new THREE.PerspectiveCamera(
    45,
    window.innerWidth / window.innerHeight,
    1,
    200
  );
  camera.position.set(0, 5, -10);

  // 初始化场景
  scene = new THREE.Scene();
}

光照设置

添加适当的光照使场景更真实:

// 方向光
const dirLight = new THREE.DirectionalLight(0xffffff);
dirLight.position.set(0, 0, -1);
scene.add(dirLight);

// 环境光
const light = new THREE.AmbientLight(0xffffff, 0.5); // soft white light
scene.add(light);

创建地球和月球模型

使用纹理贴图创建地球和月球模型:

// 创建地球
const earthGeometry = new THREE.SphereGeometry(EARTH_RADIUS, 16, 16);
const earthMaterial = new THREE.MeshPhongMaterial({
  specular: 0x333333,
  shininess: 5,
  map: textureLoader.load("textures/planets/earth_atmos_2048.jpg"),
  specularMap: textureLoader.load("textures/planets/earth_specular_2048.jpg"),
  normalMap: textureLoader.load("textures/planets/earth_normal_2048.jpg"),
  normalScale: new THREE.Vector2(0.85, 0.85),
});

earth = new THREE.Mesh(earthGeometry, earthMaterial);
scene.add(earth);

// 创建月球
const moonGeometry = new THREE.SphereGeometry(MOON_RADIUS, 16, 16);
const moonMaterial = new THREE.MeshPhongMaterial({
  shininess: 5,
  map: textureLoader.load("textures/planets/moon_1024.jpg"),
});
moon = new THREE.Mesh(moonGeometry, moonMaterial);
scene.add(moon);

创建 CatmullRomCurve3 曲线

这是关键部分,我们使用一系列点来创建平滑的 CatmullRomCurve3 样条曲线:

// 根据这一系列的点创建闭合曲线
curve = new THREE.CatmullRomCurve3(
  [
    new THREE.Vector3(-10, 0, 10),  // 起始点
    new THREE.Vector3(-5, 5, 5),    // 控制点1
    new THREE.Vector3(0, 0, 5),     // 控制点2
    new THREE.Vector3(5, -5, 5),    // 控制点3
    new THREE.Vector3(10, 0, 10),   // 结束点
  ],
  true  // true 表示创建闭合曲线,false 表示开放曲线
);

CatmullRomCurve3 曲线是一种样条曲线,它会经过所有的控制点,形成平滑的路径。参数 true 表示创建闭合曲线,即曲线的终点会连接到起点。

可视化曲线

将曲线可视化地显示在场景中:

// 在曲线里,getPoints获取501个点(500个分段+1)
const points = curve.getPoints(500);
console.log(points);
const geometry = new THREE.BufferGeometry().setFromPoints(points);

const material = new THREE.LineBasicMaterial({ color: 0xff0000 });

// 创建线条对象并添加到场景
const curveObject = new THREE.Line(geometry, material);
scene.add(curveObject);

getPoints(divisions) 方法会沿着曲线均匀地采样指定数量的点。在这里,我们使用 500 个分段,因此会得到 501 个点(包括起始点和结束点)。这样可以确保曲线看起来平滑流畅。

渲染器和控制器设置

设置渲染器和控制器:

renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

const controls = new OrbitControls(camera, renderer.domElement);
controls.minDistance = 5;
controls.maxDistance = 100;

动画循环与曲线运动

在动画循环中,我们让月球沿着曲线运动,同时相机也跟随曲线:

function animate() {
  requestAnimationFrame(animate);

  const elapsed = clock.getElapsedTime();
  // 计算归一化的时间参数(0到1之间)
  const time = elapsed/10 % 1;
  
  // 根据时间参数获取曲线上对应的点
  const point = curve.getPoint(time);
  
  // 将月球位置设置为曲线上当前点的位置
  moon.position.copy(point);
  
  // 相机也跟随曲线运动
  camera.position.copy(point);
  
  // 相机始终看向地球位置
  camera.lookAt(earth.position)
  
  // 渲染场景
  renderer.render(scene, camera);
}

getPoint(t) 方法用于获取曲线上特定参数位置的点,其中 t 是一个介于 0 和 1 之间的值。当 t=0 时,返回曲线的起始点;当 t=1 时,返回曲线的结束点(在闭合曲线的情况下,这与起始点相同)。

曲线类型详解

Three.js 提供了多种曲线类型:

  1. CatmullRomCurve3: 经过所有控制点的三次样条曲线
  2. CubicBezierCurve3: 三次贝塞尔曲线
  3. QuadraticBezierCurve3: 二次贝塞尔曲线
  4. LineCurve3: 直线段

CatmullRomCurve3 曲线参数

CatmullRomCurve3 曲线构造函数接受以下参数:

new THREE.CatmullRomCurve3(points, closed, curveType, tension)
  • points: Vector3 类型的数组,定义曲线经过的点
  • closed: Boolean 类型,是否闭合曲线
  • curveType: String 类型,默认为 "centripetal",还有 "chordal" 和 "catmullrom"
  • tension: Number 类型,张力参数,默认为 0.5,值越大曲线越紧绷

应用场景

曲线在 Three.js 中有多种应用场景:

  1. 路径动画: 让物体沿着预设路径运动
  2. 摄像机动画: 让摄像机沿着路径移动,创建电影般的镜头效果
  3. 地形生成: 使用曲线生成平滑的地形轮廓
  4. 粒子系统: 控制粒子的运动轨迹
  5. UI 动画: 创建复杂的 UI 动画效果

总结

通过这个项目,我们学习了如何使用 Three.js 的曲线功能:

  1. 如何创建 CatmullRomCurve3 样条曲线
  2. 如何将曲线可视化显示在场景中
  3. 如何让物体沿着曲线运动
  4. 如何控制动画的时间参数
  5. 曲线在 3D 应用中的多种用途

曲线是 Three.js 中一个非常强大的功能,它可以让我们创建流畅的动画和复杂的路径效果,为我们的 3D 应用增添更多的动态美感。

Three.js CSS2D渲染器实现3D标签效果

2026年1月28日 12:20

概述

本文将详细介绍如何使用 Three.js 的 CSS2DRenderer 来在 3D 场景中添加 HTML 标签。CSS2DRenderer 是 Three.js 提供的一种特殊的渲染器,它允许我们在 3D 对象上叠加 HTML 元素,非常适合创建标签、标注和信息展示等效果。

screenshot_2026-01-28_12-18-09.gif

准备工作

首先,我们需要引入必要的 Three.js 库和 CSS2D 渲染器模块:

import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import {
  CSS2DRenderer,
  CSS2DObject,
} from "three/examples/jsm/renderers/CSS2DRenderer.js";

场景初始化

首先,我们需要创建一个基本的 Three.js 场景:

let camera, scene, renderer, labelRenderer;

const clock = new THREE.Clock();
const textureLoader = new THREE.TextureLoader();

let moon;
let chinaLabel;
const raycaster = new THREE.Raycaster();

function init() {
  const EARTH_RADIUS = 1;
  const MOON_RADIUS = 0.27;

  camera = new THREE.PerspectiveCamera(
    45,
    window.innerWidth / window.innerHeight,
    0.1,
    200
  );
  camera.position.set(0, 5, -10);

  scene = new THREE.Scene();
}

光照设置

添加适当的光照使场景更真实:

const dirLight = new THREE.DirectionalLight(0xffffff);
dirLight.position.set(0, 0, 1);
scene.add(dirLight);

const light = new THREE.AmbientLight(0xffffff, 0.5); // soft white light
scene.add(light);

创建地球模型

使用纹理贴图创建地球模型:

const earthGeometry = new THREE.SphereGeometry(EARTH_RADIUS, 16, 16);
const earthMaterial = new THREE.MeshPhongMaterial({
  specular: 0x333333,
  shininess: 5,
  map: textureLoader.load("textures/planets/earth_atmos_2048.jpg"),
  specularMap: textureLoader.load("textures/planets/earth_specular_2048.jpg"),
  normalMap: textureLoader.load("textures/planets/earth_normal_2048.jpg"),
  normalScale: new THREE.Vector2(0.85, 0.85),
});

const earth = new THREE.Mesh(earthGeometry, earthMaterial);
scene.add(earth);

创建月球模型

同样地,创建月球模型:

const moonGeometry = new THREE.SphereGeometry(MOON_RADIUS, 16, 16);
const moonMaterial = new THREE.MeshPhongMaterial({
  shininess: 5,
  map: textureLoader.load("textures/planets/moon_1024.jpg"),
});
moon = new THREE.Mesh(moonGeometry, moonMaterial);
scene.add(moon);

创建CSS2D标签

这是关键部分,我们使用 CSS2DObject 来创建可以附加到 3D 对象上的 HTML 标签:

// 创建地球标签
const earthDiv = document.createElement('div');
earthDiv.className = "label";
earthDiv.innerHTML = "地球";
const earthLabel = new CSS2DObject(earthDiv);
earthLabel.position.set(0, 1, 0);
earth.add(earthLabel);

// 创建中国标签
const chinaDiv = document.createElement('div');
chinaDiv.className = "label1";
chinaDiv.innerHTML = "中国";
chinaLabel = new CSS2DObject(chinaDiv);
chinaLabel.position.set(-0.3, 0.5, -0.9);
earth.add(chinaLabel);

// 创建月球标签
const moonDiv = document.createElement('div');
moonDiv.className = "label";
moonDiv.innerHTML = "月球";
const moonLabel = new CSS2DObject(moonDiv);
moonLabel.position.set(0, 0.3, 0);
moon.add(moonLabel);

CSS2D渲染器设置

实例化并配置 CSS2D 渲染器:

// 实例化css2d的渲染器
labelRenderer = new CSS2DRenderer();
labelRenderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(labelRenderer.domElement);
labelRenderer.domElement.style.position = 'fixed';
labelRenderer.domElement.style.top = '0px';
labelRenderer.domElement.style.left = '0px';
labelRenderer.domElement.style.zIndex = '10';

WebGL渲染器设置

设置标准的 WebGL 渲染器:

renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

控制器设置

配置轨道控制器以便用户可以交互:

const controls = new OrbitControls(camera, labelRenderer.domElement);
controls.minDistance = 5;
controls.maxDistance = 100;

窗口大小调整

处理窗口大小变化:

window.addEventListener("resize", onWindowResize);

function onWindowResize() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
  labelRenderer.setSize(window.innerWidth, window.innerHeight);
}

动画循环与标签隐藏检测

在动画循环中,我们不仅移动月球,还实现了标签的智能显示/隐藏逻辑:

function animate() {
  requestAnimationFrame(animate);

  const elapsed = clock.getElapsedTime();

  // 移动月球
  moon.position.set(Math.sin(elapsed) * 5, 0, Math.cos(elapsed) * 5);

  // 检测中国标签是否被遮挡
  const chinaPosition = chinaLabel.position.clone();
  // 计算出标签跟摄像机的距离
  const labelDistance = chinaPosition.distanceTo(camera.position);
  // 检测射线的碰撞
  // 向量(坐标)从世界空间投影到相机的标准化设备坐标 (NDC) 空间。
  chinaPosition.project(camera);
  raycaster.setFromCamera(chinaPosition, camera);

  const intersects = raycaster.intersectObjects(scene.children, true);

  // 如果没有碰撞到任何物体,那么让标签显示
  if(intersects.length == 0){
    chinaLabel.element.classList.add('visible');
  } else {
    const minDistance = intersects[0].distance;
    if(minDistance < labelDistance){
      chinaLabel.element.classList.remove('visible');
    } else {
      chinaLabel.element.classList.add('visible');
    }
  }

  // 标签渲染器渲染
  labelRenderer.render(scene, camera);

  // WebGL渲染器渲染
  renderer.render(scene, camera);
}

CSS样式

为了让标签正确显示,我们需要适当的 CSS 样式:

.label {
  color: #fff;
  font-size: 1rem;
}

.label1 {
  color: #fff;
  display: none;
  font-size: 1rem;
}

.label1.visible {
  display: block;
}

CSS2DRenderer 工作原理

CSS2DRenderer 的工作原理是:

  1. 它不会渲染 3D 几何体,而是将附加到 3D 对象上的 HTML 元素定位到相应的位置
  2. 它会根据相机视角自动调整 HTML 元素的位置和大小
  3. HTML 元素始终保持面向相机,提供良好的可读性

标签遮挡检测

在这个示例中,我们实现了智能的标签遮挡检测:

  1. 使用 Raycaster 计算从相机到标签的射线
  2. 检查射线上是否有其他物体会遮挡标签
  3. 如果有遮挡,则隐藏标签;如果没有遮挡,则显示标签

优势与应用场景

CSS2DRenderer 的优势包括:

  1. 可以使用完整的 HTML 和 CSS 功能
  2. 支持复杂的布局和交互
  3. 文字渲染质量高
  4. 易于集成现有的 UI 组件

典型应用场景包括:

  • 3D 场景中的标注和信息展示
  • 地图应用中的地点标签
  • 3D 模型的部件说明
  • 数据可视化中的标签显示

总结

通过这个项目,我们学习了如何使用 Three.js 的 CSS2DRenderer:

  1. 如何创建和配置 CSS2DRenderer
  2. 如何使用 CSS2DObject 将 HTML 元素附加到 3D 对象
  3. 如何处理两个渲染器的协调工作
  4. 如何实现智能的标签遮挡检测

CSS2DRenderer 是一个强大的工具,它可以让我们在 3D 场景中轻松添加富文本标签和其他 HTML 元素,极大地增强了 3D 应用的信息展示能力。

Three.js 后期处理效果合成详解

2026年1月28日 12:12

概述

本文将详细介绍如何使用 Three.js 的后期处理系统来创建各种视觉效果。后期处理是在场景渲染完成后,对最终图像进行额外处理的技术,可以用来实现发光、模糊、故障效果等多种视觉增强效果。

screenshot_2026-01-28_12-08-21.gif

screenshot_2026-01-28_12-09-32.gif

准备工作

首先,我们需要引入必要的 Three.js 库和后期处理模块:

import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import gsap from "gsap";
import * as dat from "dat.gui";
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";

// 导入后期效果合成器
import {EffectComposer} from 'three/examples/jsm/postprocessing/EffectComposer';

// three框架本身自带效果
import {RenderPass} from 'three/examples/jsm/postprocessing/RenderPass';
import {DotScreenPass} from 'three/examples/jsm/postprocessing/DotScreenPass';
import {SMAAPass} from 'three/examples/jsm/postprocessing/SMAAPass';
import {SSAARenderPass} from 'three/examples/jsm/postprocessing/SSAARenderPass';
import {GlitchPass} from 'three/examples/jsm/postprocessing/GlitchPass';
import {UnrealBloomPass} from 'three/examples/jsm/postprocessing/UnrealBloomPass';
import {ShaderPass} from 'three/examples/jsm/postprocessing/ShaderPass';

场景初始化

首先,我们需要创建一个基本的 Three.js 场景:

// 初始化场景
const scene = new THREE.Scene();

// 创建透视相机
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerHeight / window.innerHeight,
  1,
  50
);

// 设置相机位置
camera.position.set(0, 0, 3);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
scene.add(camera);

// 加入辅助轴,帮助我们查看3维坐标轴
// const axesHelper = new THREE.AxesHelper(5);
// scene.add(axesHelper);

环境设置

设置环境纹理和光照:

// 加载纹理
const textureLoader = new THREE.TextureLoader();

// 添加环境纹理
const cubeTextureLoader = new THREE.CubeTextureLoader();
const envMapTexture = cubeTextureLoader.load([
  "textures/environmentMaps/0/px.jpg",
  "textures/environmentMaps/0/nx.jpg",
  "textures/environmentMaps/0/py.jpg",
  "textures/environmentMaps/0/ny.jpg",
  "textures/environmentMaps/0/pz.jpg",
  "textures/environmentMaps/0/nz.jpg",
]);
scene.background = envMapTexture;
scene.environment = envMapTexture;

// 添加方向光
const directionLight = new THREE.DirectionalLight('#ffffff', 1);
directionLight.castShadow = true;
directionLight.position.set(0, 0, 200);
scene.add(directionLight);

模型加载

加载 3D 模型:

// 模型加载
const gltfLoader = new GLTFLoader();
gltfLoader.load('./models/DamagedHelmet/glTF/DamagedHelmet.gltf', (gltf) => {
  console.log(gltf);
  const mesh = gltf.scene.children[0];
  scene.add(mesh);
});

后期处理合成器设置

这是后期处理的核心部分,创建效果合成器并添加各种通道:

// 初始化渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;

// 合成效果
const effectComposer = new EffectComposer(renderer);
effectComposer.setSize(window.innerWidth, window.innerHeight);

// 添加渲染通道
const renderPass = new RenderPass(scene, camera);
effectComposer.addPass(renderPass);

// 点效果
const dotScreenPass = new DotScreenPass();
dotScreenPass.enabled = false;
effectComposer.addPass(dotScreenPass);

// 抗锯齿
const smaaPass = new SMAAPass();
effectComposer.addPass(smaaPass);

// 发光效果
const unrealBloomPass = new UnrealBloomPass();
effectComposer.addPass(unrealBloomPass);

// 屏幕闪动
// const glitchPass = new GlitchPass();
// effectComposer.addPass(glitchPass)

发光效果参数调节

设置发光效果的参数并添加 GUI 控制:

renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1;
unrealBloomPass.strength = 1;
unrealBloomPass.radius = 0;
unrealBloomPass.threshold = 1;

// 添加GUI控制
gui.add(renderer,'toneMappingExposure').min(0).max(2).step(0.01);
gui.add(unrealBloomPass,'strength').min(0).max(2).step(0.01);
gui.add(unrealBloomPass,'radius').min(0).max(2).step(0.01);
gui.add(unrealBloomPass,'threshold').min(0).max(2).step(0.01);

自定义着色器后期处理

创建自定义着色器效果:

// 着色器写渲染通道
const shaderPass = new ShaderPass(
  {
    uniforms:{
      tDiffuse:{
        value:null
      },
      uColor:{
        value:new THREE.Color(colorParams.r,colorParams.g,colorParams.b)
      }
    },
    vertexShader:`
      varying vec2 vUv;
      void main(){
        vUv = uv;
        gl_Position = projectionMatrix*modelViewMatrix*vec4(position,1.0);
      }
    `,
    fragmentShader:`
      varying vec2 vUv;
      uniform sampler2D tDiffuse;
      uniform vec3 uColor;
      void main(){
        vec4 color = texture2D(tDiffuse,vUv);
        // gl_FragColor = vec4(vUv,0.0,1.0);
        color.xyz+=uColor;
        gl_FragColor = color;
      }
    `
  }
);

effectComposer.addPass(shaderPass);

// 颜色参数控制
const colorParams = {
  r:0,
  g:0,
  b:0
}

gui.add(colorParams,'r').min(-1).max(1).step(0.01).onChange((value)=>{
  shaderPass.uniforms.uColor.value.r = value;
});
gui.add(colorParams,'g').min(-1).max(1).step(0.01).onChange((value)=>{
  shaderPass.uniforms.uColor.value.g = value;
});
gui.add(colorParams,'b').min(-1).max(1).step(0.01).onChange((value)=>{
  shaderPass.uniforms.uColor.value.b = value;
});

技术效果着色器

添加技术感的后期处理效果:

const normalTexture = textureLoader.load('./textures/interfaceNormalMap.png');

const techPass = new ShaderPass({
  uniforms:{
    tDiffuse:{
      value:null
    },
    uNormalMap:{
      value:null
    },
    uTime:{
      value:0
    }
  },
  vertexShader:`
    varying vec2 vUv;
    void main(){
      vUv = uv;
      gl_Position = projectionMatrix*modelViewMatrix*vec4(position,1.0);
    }
  `,
  fragmentShader:`
    varying vec2 vUv;
    uniform sampler2D tDiffuse;
    uniform sampler2D uNormalMap;
    uniform float uTime;
    void main(){

      vec2 newUv = vUv;
      newUv += sin(newUv.x*10.0+uTime*0.5)*0.03;

      vec4 color = texture2D(tDiffuse,newUv);
      // gl_FragColor = vec4(vUv,0.0,1.0);
      vec4 normalColor = texture2D(uNormalMap,vUv);
      // 设置光线的角度
      vec3 lightDirection = normalize(vec3(-5,5,2)) ;

      float lightness = clamp(dot(normalColor.xyz,lightDirection),0.0,1.0) ;
      color.xyz+=lightness;
      gl_FragColor = color;
    }
  `
})
techPass.material.uniforms.uNormalMap.value = normalTexture;
effectComposer.addPass(techPass);

渲染循环

设置渲染循环并应用后期处理:

const clock = new THREE.Clock();

function animate(t) {
  controls.update();
  const time = clock.getElapsedTime();
  requestAnimationFrame(animate);
  // 使用渲染器渲染相机看这个场景的内容渲染出来
  // renderer.render(scene, camera);
  techPass.material.uniforms.uTime.value = time;
  effectComposer.render();
}

animate();

窗口大小调整

处理窗口大小变化:

// 监听屏幕大小改变的变化,设置渲染的尺寸
window.addEventListener("resize", () => {
  // 更新摄像头
  camera.aspect = window.innerWidth / window.innerHeight;
  // 更新摄像机的投影矩阵
  camera.updateProjectionMatrix();

  // 更新渲染器
  renderer.setSize(window.innerWidth, window.innerHeight);
  // 设置渲染器的像素比例
  renderer.setPixelRatio(window.devicePixelRatio);

  effectComposer.setSize(window.innerWidth, window.innerHeight);
  effectComposer.setPixelRatio(window.devicePixelRatio);
});

各种后期处理效果介绍

  1. RenderPass: 基础渲染通道,负责渲染原始场景
  2. DotScreenPass: 点阵屏幕效果,产生类似扫描线的视觉效果
  3. SMAAPass: 智能抗锯齿处理,提高画面质量
  4. UnrealBloomPass: 虚幻引擎风格的发光效果,使明亮区域产生光晕
  5. GlitchPass: 故障效果,模拟信号干扰的视觉效果
  6. ShaderPass: 自定义着色器效果,可实现任意的后期处理效果

总结

通过这个项目,我们学习了如何使用 Three.js 的后期处理系统:

  1. 创建 EffectComposer 作为后期处理的核心
  2. 添加不同类型的 Pass 来实现各种效果
  3. 通过参数调节控制效果强度
  4. 实现自定义着色器后期处理
  5. 在渲染循环中使用 composer.render() 替代传统的 renderer.render()

后期处理技术是提升三维场景视觉效果的重要手段,能够显著增强画面的表现力和沉浸感。掌握这些技术可以让你的 Three.js 应用更具视觉冲击力。

❌
❌