项目概述
项目名称
Codrops BatchedMesh & Post Processing Demo
项目类型
3D 交互式 Web 演示项目



开发者
Christophe Choffel (ULuCode)
项目来源
CODROPS - 2024年技术文章演示
一、项目背景与目标
1.1 项目背景
本项目是一个展示 Three.js 最新技术(WebGPU 渲染器、BatchedMesh 对象和高级后处理管线)的交互式 3D 演示。项目旨在向开发者展示如何使用现代 WebGL/WebGPU 技术创建高性能、视觉震撼的 Web 3D 应用。
1.2 核心目标
- 展示
WebGPURenderer的强大功能和性能优势 - 演示
BatchedMesh批量网格渲染技术,解决大量实例化对象的性能问题 - 实现高级后处理效果(SSAO、景深、FXAA、暗角)
- 提供流畅的交互式用户体验
- 展示主题切换的平滑过渡动画
1.3 技术亮点
- 批量渲染优化:使用 BatchedMesh 将数千个方块合并为单个绘制调用
- 现代渲染管线:完整展示 WebGPU + TSL (Three.js Shading Language) 的强大功能
- 数学驱动动画:使用单纯形噪声和 Inigo Quilez 的数学函数实现自然波动效果
- 电影级视觉效果:组合多种后处理效果实现专业级画面
二、技术栈分析
2.1 前端框架与构建工具
| 技术栈 | 版本 | 用途 |
|---|---|---|
| React | 18.3.1 | UI 框架,管理应用状态和界面 |
| TypeScript | 5.5.3 | 类型系统,提供类型安全 |
| Vite | 5.4.1 | 现代化构建工具,快速开发服务器 |
| @vitejs/plugin-react | 4.3.1 | Vite 的 React 插件 |
| vite-plugin-top-level-await | 1.4.4 | 支持顶级 await 语法 |
| ESLint | 9.9.0 | 代码检查工具 |
2.2 3D 图形库
| 技术栈 | 版本 | 用途 |
|---|---|---|
| Three.js | 0.169.0 | 核心 3D 库 |
| WebGPURenderer | - | WebGPU 渲染器(最新特性) |
| BatchedMesh | - | 批量网格渲染对象 |
| PostProcessing | - | 后处理管线 |
| UltraHDRLoader | - | 超高清图像加载器 |
| OrbitControls | - | 轨道相机控制器 |
| FastSimplexNoise | 0.0.1-a2 | 快速单纯形噪声生成 |
2.3 开发工具
- ESLint 9.9.0 - 代码质量检查
- typescript-eslint 8.0.1 - TypeScript ESLint 插件
三、项目架构设计
3.1 整体架构图
┌─────────────────────────────────────────────────────────┐
│ React UI 层 │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────────┐ │
│ │ App.tsx │ │ App.css │ │ threeCanvas.tsx │ │
│ └──────────┘ └──────────┘ └──────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Three.js 3D 渲染层 │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Demo.ts (核心逻辑类 - 497行) │ │
│ │ - 初始化管理 │ │
│ │ - 后处理管线配置 │ │
│ │ - BatchedMesh 管理 │ │
│ │ - 动画循环和更新 │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌──────────────┐ ┌──────────────┐
│ 组件层 │ │ 工具库层 │ │ 资源层 │
│ │ │ │ │ │
│ threeCanvas.tsx │ │ ABlock.ts │ │ blocks.glb │
│ │ │ BlockGeometry│ │ 天空盒贴图 │
│ │ │ Pointer.ts │ │ │
└─────────────────┘ └──────────────┘ └──────────────┘
3.2 目录结构
codrops-batchedmesh-main/
├── src/ # 源代码目录
│ ├── main.tsx # React 应用入口
│ ├── App.tsx # 主应用组件(104行)
│ ├── App.css # 应用样式(356行)
│ ├── Demo.ts # 核心 3D 演示逻辑(497行)
│ ├── vite-env.d.ts # Vite 类型定义
│ ├── components/ # React 组件
│ │ └── threeCanvas.tsx # Three.js 画布组件(48行)
│ └── lib/ # 工具库
│ ├── ABlock.ts # 方块数据模型(45行)
│ ├── BlockGeometry.ts # 方块几何体管理(61行)
│ └── Pointer.ts # 指针/交互处理(75行)
├── public/ # 静态资源
│ ├── assets/
│ │ ├── models/
│ │ │ └── blocks.glb # 9种方块3D模型
│ │ └── ultrahdr/
│ │ └── rustig_koppie_puresky_2k.jpg # 天空盒贴图
│ └── favicon.ico
├── package.json # 项目配置和依赖
├── vite.config.ts # Vite 构建配置(24行)
├── index.html # HTML 入口(17行)
├── tsconfig.json # TypeScript 配置
├── eslint.config.js # ESLint 配置
└── README.md # 项目说明文档
3.3 文件职责划分
| 文件 | 行数 | 主要职责 |
|---|---|---|
src/Demo.ts |
497 | 核心 3D 逻辑,包含初始化、渲染循环、后处理、方块更新等 |
src/App.tsx |
104 | React 主组件,UI 状态管理、主题切换、加载状态 |
src/App.css |
356 | 完整样式系统,主题变量、加载动画、响应式布局 |
src/lib/ABlock.ts |
45 | 方块数据模型,定义方块属性和颜色 |
src/lib/BlockGeometry.ts |
61 | 方块几何体管理,从 GLB 加载9种几何体 |
src/lib/Pointer.ts |
75 | 指针交互处理,屏幕坐标转换和射线投射 |
src/components/threeCanvas.tsx |
48 | Canvas 组件包装器,初始化 Demo 实例 |
四、核心功能模块设计
4.1 WebGPU 渲染器初始化
位置 : Demo.ts:46-75 init() 方法
实现逻辑:
typescript
1. 检测 WebGPU 可用性
└── WebGPU.isAvailable() - 返回浏览器是否支持 WebGPU
2. 创建 WebGPURenderer
└── new WebGPURenderer({ canvas, antialias: true })
└── 配置像素比、尺寸、色调映射
3. 初始化相机和控制器
└── PerspectiveCamera (FOV: 20°, 范围: 0.1-500)
└── OrbitControls (启用阻尼,限制极角范围)
4. 初始化后处理管线
└── 创建 PostProcessing 实例
└── 配置 Scene Pass、AO Pass、DoF Pass、FXAA
5. 加载环境资源
└── UltraHDR 天空盒
└── 地面网格
6. 生成网格和创建 BatchedMesh
└── initGrid() - 随机生成方块布局
└── initBlocks() - 创建批量渲染网格
7. 启动渲染循环
└── renderer.setAnimationLoop(this.animate.bind(this))
关键参数:
- 相机位置: 初始角度 -120° (2π/3),距离 100 单位
- 相机极角限制: maxPolarAngle = 7π/16 (约78.75°),避免穿入地面
- 相机距离限制: 20-250 单位
- 像素比: 1.0 (性能优先)
- 色调映射: ACESFilmicToneMapping,曝光 0.9
4.2 BatchedMesh 批量渲染
位置 : Demo.ts:234-291 initBlocks() 方法
4.2.1 技术原理
BatchedMesh 是 Three.js 新增的批量渲染对象,允许将多个不同几何体的实例合并为单个绘制调用。其优势包括:
- 性能优化: 数千个实例合并为 1-2 个 draw call
- 灵活几何体: 支持混合多种不同几何体
- 实例级控制: 每个实例可独立设置矩阵和颜色
4.2.2 实现步骤
步骤 1: 计算顶点和索引总数
├── 遍历所有方块几何体
├── 统计每种几何体的顶点数和索引数
└── 累加所有方块的顶点/索引总数
步骤 2: 创建 BatchedMesh
├── 实例数量: 方块数 * 2 (顶部 + 底部)
├── 顶点数: 计算得到的总数
├── 索引数: 计算得到的总数
└── 材质: MeshPhysicalNodeMaterial
步骤 3: 添加几何体
├── 循环遍历 9 种方块几何体
└── 调用 blockMesh.addGeometry(geom)
步骤 4: 添加实例
├── 为每个方块添加底部实例
├── 为每个方块添加顶部实例
├── 设置底部颜色
└── 设置顶部颜色
4.2.3 关键代码
typescript
// 材质配置
const mat = new MeshPhysicalNodeMaterial({
roughness: 0.1,
metalness: 0.0
});
mat.envMapIntensity = 0.25;
// 创建 BatchedMesh
const maxBlocks = this.blocks.length * 2; // 顶部 + 底部
this.blockMesh = new BatchedMesh(maxBlocks, totalV, totalI, mat);
this.blockMesh.sortObjects = false; // 禁用排序以提升性能
// 添加几何体
const geomIds = [];
for (let i = 0; i < geoms.length; i++) {
geomIds.push(this.blockMesh.addGeometry(geoms[i]));
}
// 添加实例
for (let i = 0; i < this.blocks.length; i++) {
const block = this.blocks[i];
this.blockMesh.addInstance(geomIds[block.typeBottom]);
this.blockMesh.addInstance(geomIds[block.typeTop]);
this.blockMesh.setColorAt(i * 2, block.baseColor);
this.blockMesh.setColorAt(i * 2 + 1, block.topColor);
}
4.2.4 性能指标
- 方块数量: ~2000+ 个 (148x148 网格随机生成)
- Draw Call: 从 4000+ 降至 1-2 个
- 性能提升: 约 100x-1000x (取决于硬件)
4.3 网格生成算法
位置 : Demo.ts:161-230 initGrid() 方法
4.3.1 算法设计
在 148x148 的区域内随机生成方块,使用占用网格(Occupancy Grid)算法避免重叠。
4.3.2 实现流程
初始化阶段
├── 创建 148x148 的占用网格 (初始值 -1 表示空闲)
├── 设置最大方块尺寸 (1-5 单位)
└── 设置方块生成概率 (50% 为方形)
主循环 (按行生成)
├── while (py < 148)
│ ├── while (px < 148)
│ │ ├── 检查当前位置到下一个被占用位置的最大宽度
│ │ ├── 如果宽度为 0,跳过 (px++)
│ │ ├── 随机决定方块形状 (方形或矩形)
│ │ ├── 随机选择方块类型 (0-5)
│ │ ├── 随机设置方块尺寸 (在最大宽度范围内)
│ │ ├── 标记占用网格区域
│ │ └── 推进指针 (px += sx)
│ ├── 下一行 (py++, px = 0)
│ └── 随机更新最大方块尺寸 (20% 概率)
4.3.3 关键数据结构
typescript
// 占用网格: 二维数组,-1 表示空闲,其他值为方块ID
const occupied: number[][] = Array.from({ length: 148 }, () =>
Array(148).fill(-1)
);
// 方块数据结构 (ABlock 类)
interface ABlock {
id: number; // 唯一标识
typeBottom: number; // 底部几何体类型 (0-2)
typeTop: number; // 顶部几何体类型 (0-5)
box: Box2; // 2D 边界框
height: number; // 高度
rotation: number; // 旋转角度
topColorIndex: number;// 颜色索引
topColor: Color; // 顶部颜色
baseColor: Color; // 底部颜色
}
4.3.4 方块类型映射
| 顶部类型索引 | 顶部几何体名称 | 底部类型索引 | 底部几何体名称 |
|---|---|---|---|
| 0 | Square_Top | 6 | Square_Base |
| 1 | Quart_Top | 7 | Quart_Base |
| 2 | Hole_Top | 8 | Hole_Base |
| 3 | Peg_Top | 6 | Square_Base |
| 4 | Divot_Top | 6 | Square_Base |
| 5 | Cross_Top | 6 | Square_Base |
4.4 高级后处理管线
位置 : Demo.ts:96-136 initPostProcessing() 方法
4.4.1 后处理架构
Scene (场景)
│
▼
┌───────────────────────────────────────┐
│ Scene Pass (多渲染目标 MRT) │
│ ├─ output: 颜色缓冲 │
│ ├─ normal: 世界空间法线 │
│ └─ depth: 深度缓冲 │
└───────────────────────────────────────┘
│
├─► 输出颜色 ──┐
│ │
├─► 法线 ──────┤
│ │
└─► 深度 ──────┤
│
┌──────────────▼──────────────┐
│ AO Pass (屏幕空间环境光遮蔽) │
│ - 输入: 深度 + 法线 │
│ - 参数: 半径、距离衰减等 │
└──────────────┬──────────────┘
│
▼
┌──────────────────────────────┐
│ 混合: AO × SceneColor │
└──────────────┬──────────────┘
│
┌──────────────▼──────────────┐
│ DoF Pass (景深效果) │
│ - 输入: 混合颜色 + 视深度 │
│ - 动态焦点和光圈 │
└──────────────┬──────────────┘
│
┌──────────────▼──────────────┐
│ Vignette (暗角效果) │
│ - 径向渐变遮罩 │
└──────────────┬──────────────┘
│
┌──────────────▼──────────────┐
│ FXAA (快速近似抗锯齿) │
└──────────────┬──────────────┘
│
▼
最终渲染输出
4.4.2 各通道详细配置
1. Scene Pass (多渲染目标)
typescript
const scenePass = pass(this.scene, this.camera);
scenePass.setMRT(mrt({
output: output, // RGB 颜色输出
normal: transformedNormalView // 视图空间法线
}));
2. AO Pass (屏幕空间环境光遮蔽)
typescript
const aoPass = ao(scenePassDepth, scenePassNormal, this.camera);
aoPass.distanceExponent.value = 1; // 距离指数
aoPass.distanceFallOff.value = .1; // 距离衰减
aoPass.radius.value = 1.0; // 采样半径
aoPass.scale.value = 1.5; // 强度缩放
aoPass.thickness.value = 1; // 厚度
参数说明:
radius: AO 采样半径,值越大影响范围越大distanceFallOff: 距离衰减系数,控制 AO 随距离的衰减速度scale: 整体强度,值越大 AO 效果越强thickness: 物体厚度感知,影响遮挡计算
3. DoF Pass (景深效果)
typescript
const dofPass = dof(
blendPassAO, // 输入纹理
scenePassViewZ, // 深度
effectController.focus, // 焦距 (动态更新)
effectController.aperture.mul(0.00001), // 光圈 (动态更新)
effectController.maxblur // 最大模糊度
);
动态参数:
focus: 在updateCamera()中动态计算,模拟自动对焦aperture: 根据相机距离动态调整 (100 - distance * 0.5)maxblur: 固定为 0.02
4. Vignette (暗角效果)
typescript
const vignetteFactor = clamp(
viewportUV.sub(0.5).length().mul(1.2),
0.0,
1.0
).oneMinus().pow(0.5);
效果: 从中心到边缘逐渐变暗,增强画面中心聚焦感
5. FXAA (快速近似抗锯齿)
typescript
this.post.outputNode = fxaa(dofPass.mul(vignetteFactor));
4.5 动画系统
位置 : Demo.ts:339-432 updateBlocks() 方法
4.5.1 高度计算算法
方块的实时高度由三个因素共同决定:
typescript
targetHeight = noiseHeight + baseHeight + pointerFactor + rippleEffect
1. 噪声高度 (自然波动)
typescript
noise = heightNoise.scaled2D(block.box.min.x * 0.1, block.box.min.y + elapsed * 5);
// FastSimplexNoise: 频率 0.05, 八度 2, 范围 0-1, 持续度 0.5
特点:
- 使用单纯形噪声生成平滑的随机波动
- 时间参数
elapsed * 5使波浪随时间移动 - 空间参数
block.box.min.x * 0.1控制空间频率
2. 基础高度
typescript
baseHeight = 1 // 方块最小高度
3. 鼠标交互因素
typescript
dx = blockCenter.x - pointerHandler.scenePointer.x;
dz = blockCenter.y - pointerHandler.scenePointer.z;
cDist = Math.sqrt(dx * dx + dz * dz);
cFactor = MathUtils.clamp(1 - cDist * 0.1, 0, 1);
pointerHeight = cFactor * 5; // 鼠标附近方块升高
效果: 鼠标位置附近方块隆起,模拟水面波纹效果
4. 主题切换涟漪
typescript
from0 = clamp(
sqrt((blockCenter.x - camDir.x - gridZone.max.x * 0.5)² +
(blockCenter.y - camDir.z - gridZone.max.y * 0.5)²) /
(gridZone.max.x * 0.5),
0, 1
);
ripple = cubicPulse(gain(transitionTime, 1.1)^0.9, 0.05, from0);
echoRipple = cubicPulse(gain(echoTime, 1.3), 0.025, from0);
rippleHeight = ripple * 10 + echoRipple * 5;
效果:
- 从屏幕中心向外扩散的双涟漪
- 第一涟漪影响底部高度
- 第二涟漪(延迟 0.3 秒)影响顶部高度
4.5.2 平滑插值
typescript
if (targetHeight >= block.height) {
// 上升时较慢 (系数 0.1)
block.height = lerp(block.height, targetHeight, 0.1);
} else {
// 下降时较快 (系数 0.3)
block.height = lerp(block.height, targetHeight, 0.3);
}
原理: 物理模拟中,物体下降通常比上升快(重力效应)
4.5.3 矩阵和颜色更新
typescript
// 更新底部
dummy.rotation.y = block.rotation;
dummy.position.set(blockCenter.x, 0, blockCenter.y);
dummy.scale.set(blockSize.x, block.height, blockSize.y);
dummy.updateMatrix();
blockMesh.setMatrixAt(baseI, dummy.matrix);
blockMesh.getColorAt(baseI, tempCol);
tempCol.lerp(baseTargetColor, ripple);
blockMesh.setColorAt(baseI, tempCol);
// 更新顶部
dummy.position.y += block.height;
dummy.scale.set(blockSize.x, 1, blockSize.y);
dummy.updateMatrix();
blockMesh.setMatrixAt(topI, dummy.matrix);
blockMesh.getColorAt(topI, tempCol);
tempCol.lerp(topTargetColors[block.topColorIndex], echoRipple);
blockMesh.setColorAt(topI, tempCol);
4.6 主题切换系统
位置 : Demo.ts:464-478 setColorMode() 方法
4.6.1 主题设计
深色主题 (Dark Mode):
typescript
baseColor: 0x000000 (黑色)
topColors: [
0x101010, 0x181818, 0x202020, 0x282828, 0xbe185d (洋红色)
]
浅色主题 (Light Mode):
typescript
baseColor: 0x999999 (灰色)
topColors: [
0xffffff, 0xcccccc, 0xaaaaaa, 0x999999, 0x086ff0 (蓝色)
]
4.6.2 切换流程
typescript
setColorMode(mode: string) {
this.colorMode = mode;
this.themeTransitionStart = this.elapsed; // 记录切换时间
if (mode == 'dark') {
this.baseTargetColor.copy(ABlock.DARK_BASE_COLOR);
this.topTargetColors = ABlock.DARK_COLORS;
} else {
this.baseTargetColor.copy(ABlock.LIGHT_BASE_COLOR);
this.topTargetColors = ABlock.LIGHT_COLORS;
}
}
4.6.3 动画过渡
在 updateBlocks() 中:
typescript
transitionTime = clamp((elapsed - themeTransitionStart) / 5, 0, 1);
echoTime = clamp((elapsed - themeTransitionStart - 0.3) / 5, 0, 1);
// 使用三次脉冲函数计算涟漪强度
ripple = cubicPulse(gain(transitionTime, 1.1)^0.9, 0.05, from0);
echoRipple = cubicPulse(gain(echoTime, 1.3), 0.025, from0);
过渡特点:
- 持续时间: 5 秒
- 双涟漪效果: 第二个涟漪延迟 0.3 秒
- 颜色插值: 使用
lerp()平滑过渡 - 高度叠加: 涟漪期间额外升高方块 (10 + 5 单位)
4.7 相机自动对焦系统
位置 : Demo.ts:442-457 updateCamera() 方法
4.7.1 物理模型
模拟相机的自动对焦系统,根据相机到焦平面的距离动态调整焦距和光圈。
4.7.2 实现逻辑
typescript
updateCamera(dt: number) {
// 1. 获取相机朝向
this.camera.getWorldDirection(camDir);
// 2. 创建地面相交平面 (略低于相机)
this.groundRayPlane.constant = this.camera.position.y * 0.1;
// 3. 计算相机到地面的射线距离
this.raycaster.set(this.camera.position, camDir.normalize());
this.raycaster.ray.intersectPlane(this.groundRayPlane, camDir);
const dist = camDir.sub(this.camera.position).length();
// 4. 模拟弹簧阻尼系统
const targetDist = dist;
const distVel = (targetDist - this.camDist) / dt;
this.camdistVel = lerp(this.camdistVel, distVel, 0.05);
this.camDist += this.camdistVel * dt;
// 5. 更新景深参数
this.effectController.focus.value = lerp(
this.effectController.focus.value,
this.camDist * 0.85,
0.05
);
this.effectController.aperture.value = lerp(
this.effectController.aperture.value,
100 - this.camDist * 0.5,
0.025
);
}
4.7.3 参数说明
| 参数 | 含义 | 更新速率 | 效果 |
|---|---|---|---|
focus |
焦距 | 0.05 | 控制清晰平面距离 (目标距离的 85%) |
aperture |
光圈 | 0.025 | 控制模糊范围 (距离越远光圈越大) |
camdistVel |
速度阻尼 | 0.05 | 模拟相机移动的平滑过渡 |
camK |
弹簧常数 | 0.05 | 控制响应速度 |
物理原理:
- 距离越远 → 光圈越大 → 景深越浅 → 背景模糊越明显
- 距离越近 → 光圈越小 → 景深越深 → 整体更清晰
4.8 交互系统
位置 : src/lib/Pointer.ts
4.8.1 功能设计
处理鼠标/触摸事件,将屏幕坐标转换为场景坐标,用于方块波动效果。
4.8.2 实现机制
typescript
export class Pointer {
// 核心属性
camera: Camera; // 相机引用
renderer: WebGPURenderer | WebGLRenderer; // 渲染器引用
rayCaster: Raycaster; // 射线投射器
// 坐标系统
clientPointer: Vector2; // 客户端像素坐标
pointer: Vector2; // NDC 归一化设备坐标 (-1 到 1)
scenePointer: Vector3; // 场景世界坐标
// 状态
pointerDown: boolean; // 鼠标按下状态
uPointerDown = uniform(0); // Uniform 输出
uPointer = uniform(new Vector3()); // Uniform 输出
}
4.8.3 事件处理
typescript
onPointerDown(e: PointerEvent) {
if (e.pointerType === 'mouse' || e.button === 0) {
this.pointerDown = true;
this.uPointerDown.value = 1;
}
this.clientPointer.set(e.clientX, e.clientY);
this.updateScreenPointer(e);
}
onPointerMove(e: PointerEvent) {
this.clientPointer.set(e.clientX, e.clientY);
this.updateScreenPointer(e);
}
onPointerUp(e: PointerEvent) {
this.clientPointer.set(e.clientX, e.clientY);
this.updateScreenPointer(e);
this.pointerDown = false;
this.uPointerDown.value = 0;
}
4.8.4 坐标转换
typescript
updateScreenPointer(e?: PointerEvent) {
// 1. 获取客户端坐标 (像素)
const clientX = e?.clientX || this.clientPointer.x;
const clientY = e?.clientY || this.clientPointer.y;
// 2. 转换为 NDC 坐标 (-1 到 1)
this.pointer.set(
(clientX / window.innerWidth) * 2 - 1,
-(clientY / window.innerHeight) * 2 + 1
);
// 3. 使用射线投射转换为场景坐标
this.rayCaster.setFromCamera(this.pointer, this.camera);
this.rayCaster.ray.intersectPlane(this.iPlane, this.scenePointer);
// 4. 更新 Uniform 输出 (供 Shader 使用)
this.uPointer.value.set(
this.scenePointer.x,
this.scenePointer.y,
this.scenePointer.z
);
}
坐标转换链:
屏幕像素 → NDC (-1,1) → 射线投射 → 场景世界坐标
五、数学函数库
位置 : Demo.ts:481-496
项目使用了 Inigo Quilez 的数学函数库,这些函数用于创建平滑的动画曲线。
5.1 pcurve (多项式曲线)
typescript
pcurve(x: number, a: number, b: number): number {
const k = Math.pow(a + b, a + b) / (Math.pow(a, a) * Math.pow(b, b));
return k * Math.pow(x, a) * Math.pow(1.0 - x, b);
}
用途 : 创建平滑的幂函数曲线
参数:
x: 输入值 [0, 1]a, b: 曲线形状参数
特性:
- 当 a = b = 1 时为线性
- 当 a > b 时曲线向左偏
- 当 b > a 时曲线向右偏
5.2 gain (增益函数)
typescript
gain(x: number, k: number): number {
const a = 0.5 * Math.pow(2.0 * ((x < 0.5) ? x : 1.0 - x), k);
return (x < 0.5) ? a : 1.0 - a;
}
用途 : 调整曲线的对比度/增益
参数:
x: 输入值 [0, 1]k: 增益系数
特性:
- 当 k = 1 时为线性
- 当 k > 1 时增加对比度 (中间值偏向两端)
- 当 k < 1 时降低对比度 (中间值偏向 0.5)
应用 : 主题切换涟漪的时间曲线 gain(transitionTime, 1.1)^0.9
5.3 cubicPulse (三次脉冲函数)
typescript
cubicPulse(c: number, w: number, x: number): number {
let x2 = Math.abs(x - c);
if (x2 > w) return 0.0;
x2 /= w;
return 1.0 - x2 * x2 * (3.0 - 2.0 * x2);
}
用途 : 创建平滑的脉冲波形
参数:
c: 脉冲中心位置w: 脉冲宽度x: 输入值
特性:
- 当
|x - c| > w时返回 0 (完全超出范围) - 在中心点
x = c时返回 1 (最大值) - 使用 smoothstep 平滑过渡 (Hermite 插值)
应用: 主题切换涟漪的空间分布
可视化:
1.0 | /---\
| / \
| / \
0.5 |----/---------\----
| / \
| / \
0.0 +--|-------|-------|--
c-w c c+w
六、样式系统
位置 : src/App.css
6.1 主题变量定义
css
:root {
--font-main: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
--color-frame-bg: rgba(255, 255, 255, 0.05);
--color-frame-title: #ffffff;
--color-frame-link: #ffffff;
--color-frame-link-hover: #ffffff;
}
[data-theme="dark"] {
--color-frame-bg: rgba(0, 0, 0, 0.1);
--color-frame-title: #ffffff;
--color-frame-link: #ffffff;
--color-frame-link-hover: #ffffff;
}
6.2 加载动画
css
.loader-container {
display: flex;
justify-content: center;
align-items: center;
height: 100px;
}
.loader {
width: 50px;
height: 50px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: #fff;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
body.loading {
overflow: hidden;
}
6.3 切换开关样式
css
.theme-toggle {
margin-top: 20px;
}
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
}
input:checked + .slider {
background-color: #2196F3;
}
input:checked + .slider:before {
transform: translateX(26px);
}
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
七、性能优化策略
7.1 BatchedMesh 优化
| 优化点 | 实现方式 | 性能提升 |
|---|---|---|
| 批量渲染 | 单个 DrawCall 渲染数千实例 | 100x-1000x |
| 禁用排序 | sortObjects = false |
减少 CPU 开销 |
| 统一材质 | 所有实例共享同一材质 | 减少 Shader 切换 |
7.2 渲染优化
typescript
// 像素比设置为 1
this.renderer.setPixelRatio(1);
// 使用 WebGPU 渲染器
new WebGPURenderer({ antialias: true });
// 合理限制帧率
// (隐式通过 requestAnimationFrame)
7.3 几何体加载优化
typescript
// 预加载所有几何体
await BlockGeometry.init();
// 使用共享几何体
this.geoms.push(topSquare, topQuart, ...);
// 每个方块引用相同的几何体对象
7.4 纹理优化
typescript
// Ultra HDR 格式:单文件存储多曝光级别
const texture = await new UltraHDRLoader()
.setDataType(FloatType)
.loadAsync('rustig_koppie_puresky_2k.jpg');
优势:
- 单次加载获得多级曝光
- 适用于 Tone Mapping
7.5 交互优化
typescript
// 使用射线缓存
this.raycaster = new Raycaster();
// 复用临时对象
dummy: Object3D = new Object3D();
tempCol: Color = new Color();
blockSize: Vector2 = new Vector2(1, 1);
blockCenter: Vector2 = new Vector2();
避免: 每帧创建新对象(垃圾回收压力)
八、兼容性与检测
8.1 WebGPU 检测
位置 : Demo.ts:47-50
typescript
if (WebGPU.isAvailable() === false) {
return; // 优雅降级,显示不支持信息
}
位置 : src/App.tsx:12
typescript
const [isGPUAvailable, setIsGPUAvailable] =
useState(WebGPU.isAvailable());
UI 反馈:
typescript
{!isGPUAvailable && (
<div className='demo__infos'>
<h1>WebGPU not available</h1>
<p>WebGPU is not available on your device or browser.</p>
</div>
)}
8.2 浏览器支持要求
- Chrome: 113+ (需要 WebGPU 支持)
- Edge: 113+
- Firefox: Nightly (实验性支持)
- Safari: 技术预览版
8.3 降级策略
当前实现不支持 WebGPU 时直接返回,未来可扩展:
- 回退到 WebGLRenderer
- 禁用高级后处理效果
- 使用普通 Mesh 替代 BatchedMesh
九、部署与构建
9.1 开发环境
bash
npm install # 安装依赖
npm run dev # 启动开发服务器 (http://localhost:5173)
9.2 生产构建
bash
npm run build # 构建 (输出到 dist/)
npm run preview # 预览构建结果
9.3 Vite 配置
位置 : vite.config.ts
typescript
export default defineConfig({
root: '',
base: './', // 相对路径,便于部署
plugins: [
react(), // React 支持
topLevelAwait(), // 顶级 await 支持
fullReloadAlways // 热更新全刷新
],
})
9.4 TypeScript 配置
- tsconfig.json: 应用配置
- tsconfig.node.json: 构建脚本配置
- tsconfig.app.json: 应用源码配置
十、项目总结
10.1 核心技术亮点
- BatchedMesh 批量渲染: 展示了 Three.js 最新批处理技术的强大性能优势
- WebGPU 渲染器: 利用现代 GPU 并行计算能力,突破传统 WebGL 性能瓶颈
- 高级后处理管线: 完整的 MRT、SSAO、DoF、FXAA 组合,实现电影级视觉效果
- 数学驱动动画: 使用噪声和数学函数创建自然流畅的交互效果
- 主题切换涟漪: 双涟漪 + 颜色插值 + 高度叠加的复杂过渡动画
10.2 代码质量
| 指标 | 评价 |
|---|---|
| 架构清晰度 | ⭐⭐⭐⭐⭐ (React UI 与 Three.js 逻辑分离) |
| 代码可读性 | ⭐⭐⭐⭐⭐ (详细注释,命名清晰) |
| 性能优化 | ⭐⭐⭐⭐⭐ (BatchedMesh、WebGPU、复用对象) |
| 类型安全 | ⭐⭐⭐⭐⭐ (完整 TypeScript 类型定义) |
| 可维护性 | ⭐⭐⭐⭐⭐ (模块化设计,职责单一) |
10.3 学习价值
本项目适合学习:
- Three.js 最新技术(WebGPU、BatchedMesh、PostProcessing)
- 3D 渲染管线原理(MRT、SSAO、DoF)
- 性能优化技巧(批量渲染、对象复用)
- 数学函数在图形学中的应用
- React 与 Three.js 的集成
- WebGL/WebGPU 开发实践
10.4 扩展方向
- 更多几何体类型: 扩展 blocks.glb,增加更多形状
- 物理交互: 使用 Cannon.js 或 Ammo.js 添加物理效果
- 音频响应: 使用 Web Audio API 实现音乐可视化
- 粒子系统: 添加火花、烟雾等粒子效果
- VR/AR: 使用 WebXR 扩展到 VR/AR 平台
- 多用户交互: 使用 WebSocket 实现多人实时协作
- 自定义着色器: 编写 TSL Shader 实现更多创意效果
十一、关键文件索引
核心逻辑
- Demo.ts:46-75 -
init()初始化流程 - Demo.ts:96-136 -
initPostProcessing()后处理管线 - Demo.ts:161-230 -
initGrid()网格生成算法 - Demo.ts:234-291 -
initBlocks()BatchedMesh 创建 - Demo.ts:339-432 -
updateBlocks()方块更新动画 - Demo.ts:442-457 -
updateCamera()自动对焦 - Demo.ts:481-496 - 数学函数库
数据模型
- ABlock.ts:8-44 - 方块数据结构定义
- BlockGeometry.ts:12-56 - 几何体加载与管理
- Pointer.ts:9-74 - 交互系统实现
UI 组件
- App.tsx:8-101 - 主应用组件
- threeCanvas.tsx:4-48 - Canvas 包装器
配置文件
- vite.config.ts:15-23 - Vite 构建配置
- package.json:1-33 - 项目依赖和脚本
附录
A. 方块类型索引
| 类型索引 | 名称 | 描述 |
|---|---|---|
| 0 | Square | 正方形 |
| 1 | Quart | 四分之一圆形 |
| 2 | Hole | 圆孔 |
| 3 | Peg | 凸起 |
| 4 | Divot | 凹陷 |
| 5 | Cross | 十字 |
B. 颜色方案
浅色主题 (Light Mode)
Base: #999999 (灰色)
Colors:
0: #ffffff (白色)
1: #cccccc (浅灰)
2: #aaaaaa (中灰)
3: #999999 (深灰)
4: #086ff0 (蓝色)
深色主题 (Dark Mode)
Base: #000000 (黑色)
Colors:
0: #101010 (黑灰)
1: #181818 (深灰)
2: #202020 (中灰)
3: #282828 (浅灰)
4: #be185d (洋红)
C. 性能指标(参考)
| 指标 | 值 |
|---|---|
| 方块数量 | ~2000+ |
| 顶点总数 | ~50,000+ |
| Draw Calls | 1-2 |
| 目标帧率 | 60 FPS |
| 内存占用 | ~100-200 MB (GPU) |