AI 驱动 3D 动画
大家好,我是石小石!
前一段时间用threejs搭建了一些3D场景,最近突发奇想,想借助一些AI工具丰富一下我的3D场景。
AI与threejs能够结合的地方非常多,比如它们可以创造更加智能、动态的 3D 动画体验。在threejs中,实现一些动画效果非常常见:比如我可以实现一个粒子系统,然后通过设置粒子的运动轨迹实现下雨、下雪效果,或者模拟小虫子的运动轨迹。
在传统的3D动画实现中,我们需要通过手动编码或关键帧技术定义每一帧动画,十分麻烦!但是,借助AI算法,我们可以轻松实现下面的效果:
- 动作生成:通过神经网络生成复杂的人体动作(如走路、跳跃)。
- 路径优化:使用 AI 规划摄像机路径或物体运动轨迹。
- 实时反应:通过用户输入或传感器数据,动态生成响应动画。
为了感受AI在threejs中的魅力,本文将探讨如何使用 AI 算法
模拟物体运动轨迹,实现一个简单的案例:萤火虫飞舞效果。
前置知识及目标
核心目标
本文将借助Threejs生成一组粒子系统
,用于模拟场景中的萤火虫,并借助Threejs的效果合成器
模拟萤火虫的发光效果。最后,我们将通过 AI 算法
生成一组动态轨迹
,模拟萤火虫在 3D 空间中的运动,呈现自然且流畅的效果。
必备知识
本文核心是探讨AI算法
在Threejs中的应用,不会着重介绍Threejs的使用。如果你对Threejs不熟悉或者有遗忘,可以参考我的其他文章复习threejs的基础知识。
技术方案
项目及场景搭建
我们采用vite的vue脚手架搭建此项目,先安装threejs
的依赖,然后搭建最基本的Threejs场景
npm i three --save
// App.vue
<template>
<div ref="threeContainer" class="three-container"></div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import * as THREE from 'three';
// 用于渲染的 DOM 容器
const threeContainer = ref(null);
let scene, camera, renderer;
// 初始化 Three.js 场景
const initScene = () => {
// 创建场景
scene = new THREE.Scene();
// 创建摄像机
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 20, 1000);
camera.position.set(0, 150, 500); // 设置相机位置
camera.lookAt(0, 0, 0); // 看向原点
// 创建渲染器
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
threeContainer.value.appendChild(renderer.domElement);
};
// 生命周期钩子
onMounted(() => {
initScene();
});
</script>
<style>
.three-container {
position: fixed;
left: 0;
right: 0;
bottom: 0;
top: 0;
overflow: hidden;
}
</style>
现在,我们就得到了一个没有任何内容的黑色场景。
创建粒子系统(萤火虫)
我们可以将萤火虫理解为一个个位置不一样的自发光球体,那么萤火虫的实现就非常简单。
let scene, camera, renderer, particles;
const initScene = () => {
// ...
// 创建粒子系统(球体粒子-萤火虫)
particles = new THREE.Group();
const particlesCount = 200;
for (let i = 0; i < particlesCount; i++) {
const geometry = new THREE.SphereGeometry(5, 5, 5); // 创建球体
const material = new THREE.MeshStandardMaterial({
emissive: new THREE.Color("#CCFF00"), // 自发光颜色
emissiveIntensity: 1.5, // 自发光强度
});
const particle = new THREE.Mesh(geometry, material);
// 设置萤火虫的随机位置
particle.position.x = Math.random() * 1000 - 500; // X
particle.position.y = Math.random() * 1000 - 500; // Y
particle.position.z = Math.random() * 1000 - 500; // Z
particles.add(particle);
}
scene.add(particles);
};
// 动画循环
const animate = () => {
requestAnimationFrame(animate);
renderer.render(scene, camera);
};
// 生命周期钩子
onMounted(() => {
initScene();
animate();
});
上述代码中,particles用于储存粒子系统对象,方便后期更改参数。particlesCount代表的是萤火虫的数量,由于萤火虫的光一般是黄绿色,所以我们将其自发光颜色设置为"#CCFF00"。
实现萤火虫的发光效果
要实现萤火虫的发光效果,可以借助Threejs的EffectComposer效果合成器。EffectComposer的知识点可以参考我的其他文章:
threejs做特效:实现物体的发光效果-EffectComposer详解!
我们先引入效果合成器的一些threejs依赖项
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
然后创建一个处理发光效果的函数
let scene, camera, renderer, composer, particles;
// 初始化 Three.js 场景
const initScene = () => {
// ....
initPostProcessing()
};
// 初始化后期处理
const initPostProcessing = () => {
composer = new EffectComposer(renderer);
// 场景渲染 Pass
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
// 光晕效果 Pass
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
1.5, // 强度
0.3, // 半径
0.25 // 阈值
);
composer.addPass(bloomPass);
};
// 动画循环
const animate = () => {
animationFrameId = requestAnimationFrame(animate);
// 渲染场景(使用后期处理)
composer.render();
};
// 生命周期钩子
onMounted(() => {
initScene();
animate();
});
</script>
借助AI实现萤火虫的飞舞
要实现萤火虫的运动效果,我们可以通过自定义其运动轨迹来实现(更改它在threejs中的xyz坐标)。但是,我们自己手写的运动轨迹会非常生硬不自然,因此,我们可以借助AI算法
来实现其无规则或者规则的运动轨迹。
萤火虫算法
要通过AI算法实现萤火虫的运动轨迹,最合适的就是萤火虫算法
了。
萤火虫算法(Firefly Algorithm)是一种启发式算法,灵感来自于萤火虫闪烁的行为。萤火虫的闪光,其主要目的是作为一个信号系统,以吸引其他的萤火虫。
它的算法核心其实就是模拟下面的场景:
萤火虫不分性别,这样一个萤火虫将会吸引到所有其他的萤火虫;吸引力与它们的亮度成正比,对于任何两个萤火虫,不那么明亮的萤火虫被吸引,因此移动到更亮的一个,然而,亮度又随着其距离的增加而减少;如果没有比一个给定的萤火虫更亮的萤火虫,它会随机移动。
算法参考文章:zhuanlan.zhihu.com/p/430416502
考虑到篇幅问题,本文就不展开介绍萤火虫算法的具体内容了。同时,为了保证大家能更低成本的通过本文示例感受到AI在threejs中发挥的作用,我们通过类似的噪声算法来替代萤火虫算法。这样做的好处是大家可以在项目中快速引入算法,减少示例的代码量,降低上手难度。
Simplex Noise
simplex-noise
是一种改良的噪声算法,由 Perlin 噪声的发明者 Ken Perlin 提出。它适合用于生成平滑的伪随机数据,在许多场景中(如自然模拟、动画、程序化生成纹理等)都有应用。
萤火虫飞舞的效果应该满足以下特点:
- 运动是平滑的:不能有突兀的跳跃和不规则的抖动。
- 运动轨迹是自然的:看起来像随机飞舞,但又遵循某种自然规律。
Simplex Noise 的平滑、自然和伪随机特性,基本满足我们的需求。
在 simplex-noise
库中,可以用 createNoise2D
或 createNoise3D
创建 2D 或 3D 噪声生成器,用于生成平滑的、随时间变化的伪随机数据。可以看出,用Simplex Noise的AI算法数据模拟萤火虫的飞舞效果是非常合适的。
使用simplex-noise模拟运动轨迹
更改萤火虫的运动轨迹其实非常容易,我们只需要用simplex-noise的AI数据,改变每一个萤火虫在场景中的x,y,z轴位置即可。它的核心伪代码应该如下:
// 更新粒子位置
const updateParticles = () => {
particles.children.forEach((particle) => {
const { x, y, z } = particle.position;
// 使用噪声来更新粒子的位置
particle.position.x += AI位置; // X
particle.position.y += AI位置; // Y
particle.position.z += AI位置; // Z
});
};
完成的代码如下
npm i simplex-noise
<template>
<div ref="threeContainer" class="three-container"></div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import * as THREE from 'three';
import { createNoise2D } from 'simplex-noise';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
// 设置引用和变量
const threeContainer = ref(null); // 用于渲染的 DOM 容器
let scene, camera, renderer, composer, particles;
const noise = createNoise2D(); // 使用 SimplexNoise 创建噪声生成器
let time = 0; // 时间变量,用于驱动噪声的变化
// 初始化 Three.js 场景
const initScene = () => {
// 创建场景
scene = new THREE.Scene();
// 创建摄像机
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 20, 1000);
camera.position.set(0, 150, 500); // 设置相机位置
camera.lookAt(0, 0, 0); // 看向原点
// 创建渲染器
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
threeContainer.value.appendChild(renderer.domElement);
// 创建粒子系统(球体粒子)
particles = new THREE.Group();
const particlesCount = 200;
for (let i = 0; i < particlesCount; i++) {
const geometry = new THREE.SphereGeometry(5, 5, 5); // 创建球体
const material = new THREE.MeshStandardMaterial({
emissive: new THREE.Color("#CCFF00"), // 自发光颜色
emissiveIntensity: 1.5, // 自发光强度
});
const particle = new THREE.Mesh(geometry, material);
particle.position.x = Math.random() * 1000 - 500; // X
particle.position.y = Math.random() * 1000 - 500; // Y
particle.position.z = Math.random() * 1000 - 500; // Z
particles.add(particle);
}
scene.add(particles);
// 初始化后期处理
initPostProcessing();
};
// 初始化后期处理
const initPostProcessing = () => {
composer = new EffectComposer(renderer);
// 场景渲染 Pass
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
// 光晕效果 Pass
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
1.5, // 强度
0.3, // 半径
0.25 // 阈值
);
composer.addPass(bloomPass);
};
// 更新粒子位置
const updateParticles = () => {
particles.children.forEach((particle) => {
const { x, y, z } = particle.position;
// 使用噪声来更新粒子的位置
particle.position.x += noise(x, time) * 3; // X
particle.position.y += noise(y, time) * 3; // Y
particle.position.z += noise(z, time) * 3; // Z
});
time += 0.1; // 增加时间,控制噪声变化
};
// 动画循环
const animate = () => {
requestAnimationFrame(animate);
updateParticles(); // 更新粒子的位置
// 渲染场景(使用后期处理)
composer.render();
};
// 生命周期钩子
onMounted(() => {
initScene();
animate();
});
</script>
<style>
.three-container {
position: fixed;
left: 0;
right: 0;
bottom: 0;
top: 0;
overflow: hidden;
}
</style>
上述代码中,Simplex Noise 通过 createNoise2D
生成的一个二维噪声生成器,作为驱动萤火虫位置变化的核心工具。它为每个粒子生成平滑且自然的随机运动,使粒子看起来像真实的萤火虫飞舞。
代码中的粒子更新函数:
particle.position.x += noise(x, time) * 3; // X
particle.position.y += noise(y, time) * 3; // Y
particle.position.z += noise(z, time) * 3; // Z
-
noise(x, time)
:- 生成一个基于粒子当前位置
x
和时间time
的噪声值。 x
和time
作为输入,确保噪声在空间和时间上的连续性。
- 生成一个基于粒子当前位置
-
乘以一个因子
3
:- 噪声的值通常在
[-1, 1]
,通过乘以 3 将其映射到更大的运动幅度。
- 噪声的值通常在
-
时间
time
控制动态变化:- 时间的推进使得噪声生成的值不断变化,从而推动粒子的位置变化。
注:噪声算法没有完全模拟出萤火虫的相互吸引特性,只模拟了萤火虫的运动轨迹。
场景优化
为了让场景更加真实,我们可以给场景添加背景图片,模拟更好的夜晚效果
// 创建场景
scene = new THREE.Scene();
new THREE.CubeTextureLoader()
.setPath("/sky/")
.load(
[
"posx.jpg",
"negx.jpg",
"posy.jpg",
"negy.jpg",
"posz.jpg",
"negz.jpg",
],
(texture) => {
scene.background = texture;
}
);
注意,贴图需要自己准备
我们还可以添加轨道控制器,让场景支持旋转移动,以更丰富的视角观察萤火虫效果。
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// 初始化 Three.js 场景
const initScene = () => {
// ....
// 添加相机控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.enabled = true;
controls.update();
// 初始化后期处理
initPostProcessing();
};
此外,我们也可以添加场景的尺寸动态变化、注销等等其他优化逻辑。
// 初始化 Three.js 场景
const initScene = () => {
// ...
// 处理窗口大小变化
window.addEventListener('resize', onWindowResize);
};
// 窗口大小变化时更新渲染器和相机
const onWindowResize = () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
};
// 销毁场景
const destroyScene = () => {
cancelAnimationFrame(animationFrameId);
composer.dispose();
renderer.dispose();
window.removeEventListener('resize', onWindowResize);
};
onUnmounted(() => {
destroyScene();
});
可运行的完整代码
<template>
<div ref="threeContainer" class="three-container"></div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import * as THREE from 'three';
import { createNoise2D } from 'simplex-noise';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
// 设置引用和变量
const threeContainer = ref(null); // 用于渲染的 DOM 容器
let scene, camera, renderer, composer, particles, animationFrameId;
const noise = createNoise2D(); // 使用 SimplexNoise 创建噪声生成器
let time = 0; // 时间变量,用于驱动噪声的变化
// 初始化 Three.js 场景
const initScene = () => {
// 创建场景
scene = new THREE.Scene();
// 创建摄像机
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 20, 1000);
camera.position.set(0, 150, 500); // 设置相机位置
camera.lookAt(0, 0, 0); // 看向原点
// 创建渲染器
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
threeContainer.value.appendChild(renderer.domElement);
// 创建粒子系统(球体粒子)
particles = new THREE.Group();
const particlesCount = 200;
for (let i = 0; i < particlesCount; i++) {
const geometry = new THREE.SphereGeometry(5, 5, 5); // 创建球体
const material = new THREE.MeshStandardMaterial({
emissive: new THREE.Color("#CCFF00"), // 自发光颜色
emissiveIntensity: 1.5, // 自发光强度
// color: new THREE.Color(Math.random(), Math.random(), Math.random()), // 随机颜色
});
const particle = new THREE.Mesh(geometry, material);
particle.position.x = Math.random() * 1000 - 500; // X
particle.position.y = Math.random() * 1000 - 500; // Y
particle.position.z = Math.random() * 1000 - 500; // Z
particles.add(particle);
}
scene.add(particles);
// 添加光源
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const pointLight = new THREE.PointLight(0xffffff, 1.5, 1000);
pointLight.position.set(200, 300, 400);
scene.add(pointLight);
// 添加相机控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.enabled = true;
controls.update();
// 初始化后期处理
initPostProcessing();
// 处理窗口大小变化
window.addEventListener('resize', onWindowResize);
};
// 初始化后期处理
const initPostProcessing = () => {
composer = new EffectComposer(renderer);
// 场景渲染 Pass
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
// 光晕效果 Pass
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
1.5, // 强度
0.3, // 半径
0.25 // 阈值
);
composer.addPass(bloomPass);
};
// 窗口大小变化时更新渲染器和相机
const onWindowResize = () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
};
// 更新粒子位置
const updateParticles = () => {
particles.children.forEach((particle) => {
const { x, y, z } = particle.position;
// 使用噪声来更新粒子的位置
particle.position.x += noise(x, time) * 3; // X
particle.position.y += noise(y, time) * 3; // Y
particle.position.z += noise(z, time) * 3; // Z
});
time += 0.1; // 增加时间,控制噪声变化
};
// 动画循环
const animate = () => {
animationFrameId = requestAnimationFrame(animate);
updateParticles(); // 更新粒子的位置
// 渲染场景(使用后期处理)
composer.render();
};
// 销毁场景
const destroyScene = () => {
cancelAnimationFrame(animationFrameId);
composer.dispose();
renderer.dispose();
window.removeEventListener('resize', onWindowResize);
};
// 生命周期钩子
onMounted(() => {
initScene();
animate();
});
onUnmounted(() => {
destroyScene();
});
</script>
<style>
.three-container {
position: fixed;
left: 0;
right: 0;
bottom: 0;
top: 0;
overflow: hidden;
}
</style>
文中的demo非常简单,大家可以复制在自己项目中试试。
总结
上述的代码示例中,我们使用 Three.js 创建了一些粒子(萤火虫),并使用 AI 算法模拟了萤火虫的动态运动轨迹:
- 创建 3D 场景:通过 Three.js 搭建了基本的3D场景,使用
THREE.SphereGeometry
创建球体作为粒子,并设置自发光材质,模拟萤火虫的发光效果。 - 后期处理(发光效果) :利用 Three.js 的
EffectComposer
和UnrealBloomPass
实现了萤火虫的光晕发光效果。 - 使用 AI算法模拟萤火虫飞舞:通过
simplex-noise
库生成的噪声数据,控制每个粒子的运动轨迹,模拟萤火虫自然飞舞的效果。 - 场景优化:添加了背景图片,模拟夜空效果;并通过
OrbitControls
实现了场景的交互式旋转和缩放。
为了方便演示与阅读,我们使用了噪声算法而没有使用萤火虫算法,但通过效果示例,相信大家也能够深刻的感受到AI与threejs结合的魅力。
我相信,在AI的加持下,threejs能够发挥出更加灿烂的光芒!未来,AI也将持续发酵,与我们的生活息息相关~