学习threejs,打造交互式泡泡、粒子特效与科幻氛围
👨⚕️ 主页: gis分享者
👨⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅!
👨⚕️ 收录于专栏:threejs gis工程师

文章目录
- 一、🍀前言
- 1.1 ☘️GLTFLoader glTF 2.0资源加载器
- 1.1.1 ☘️代码示例
- 1.1.2 ☘️构造函数
- 1.1.3 ☘️方法
- 2.1 ☘️RGBELoader HDR图像加载器
- 2.1.1 ☘️HDR 图片
- 2.1.2 ☘️用法
- 1. ☘️实现思路
- 2. ☘️代码样例
一、🍀前言
本文详细介绍如何基于threejs在三维场景打造交互式泡泡、粒子特效与科幻氛围,亲测可用。希望能帮助到您。一起学习,加油!加油!
1.1 ☘️GLTFLoader glTF 2.0资源加载器
glTF(gl传输格式)是一种开放格式的规范 (open format specification), 用于更高效地传输、加载3D内容。该类文件以JSON(.gltf)格式或二进制(.glb)格式提供, 外部文件存储贴图(.jpg、.png)和额外的二进制数据(.bin)。一个glTF组件可传输一个或多个场景, 包括网格、材质、贴图、蒙皮、骨架、变形目标、动画、灯光以及摄像机。
GLTFLoader 尽可能使用 ImageBitmapLoader。请注意,图像位图在不再被引用时不会自动被 GC 收集,并且在处置过程中需要特殊处理。有关如何处理对象指南中的更多信息。
1.1.1 ☘️代码示例
// 初始化GLTFLoader加载器
const loader = new GLTFLoader();// 可选:提供DRACOLoader实例来解码压缩的网格数据
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath( '/examples/jsm/libs/draco/' );
loader.setDRACOLoader( dracoLoader );// 加载glTF资源
loader.load(// 资源地址'models/gltf/duck/duck.gltf',// 回调函数function ( gltf ) {scene.add( gltf.scene );gltf.animations; // Array<THREE.AnimationClip>gltf.scene; // THREE.Groupgltf.scenes; // Array<THREE.Group>gltf.cameras; // Array<THREE.Camera>gltf.asset; // Object},// 加载进程function ( xhr ) {console.log( ( xhr.loaded / xhr.total * 100 ) + '% loaded' );},// 出错处理function ( error ) {console.log( 'An error happened' );}
);
1.1.2 ☘️构造函数
GLTFLoader( manager : LoadingManager )
属性:
.parameters
一个包含着构造函数中每个参数的对象。在对象实例化之后,对该属性的任何修改都不会改变这个几何体。
1.1.3 ☘️方法
.load ( url : String, onLoad : Function, onProgress : Function, onError : Function ) : undefined
url — 包含有.gltf/.glb文件路径/URL的字符串。
onLoad — 加载成功完成后将会被调用的函数。该函数接收parse所返回的已加载的JSON响应。
onProgress — (可选)加载正在进行过程中会被调用的函数。其参数将会是XMLHttpRequest实例,包含有总字节数.total与已加载的字节数.loaded。
onError — (可选)若在加载过程发生错误,将被调用的函数。该函数接收error来作为参数。
开始从url加载,并使用解析过的响应内容调用回调函数。
.setDRACOLoader ( dracoLoader : DRACOLoader ) : this
dracoLoader — THREE.DRACOLoader的实例,用于解码使用KHR_draco_mesh_compression扩展压缩过的文件。
.parse ( data : ArrayBuffer, path : String, onLoad : Function, onError : Function ) : undefined
data — 需要解析的glTF文件,值为一个ArrayBuffer或JSON字符串。
path — 用于找到后续glTF资源(如纹理和.bin数据文件)的基础路径。
onLoad — 解析成功完成后将会被调用的函数。
onError — (可选)若在解析过程发生错误,将被调用的函数。该函数接收error来作为参数。
解析基于glTF的ArrayBuffer或JSON字符串,并在完成后触发onLoad回调。onLoad的参数将是一个包含有已加载部分的Object:.scene、 .scenes、 .cameras、 .animations 和 .asset。
2.1 ☘️RGBELoader HDR图像加载器
THREE.RGBELoader是Three.js库中的一个加载器,用于加载HDR(High Dynamic Range)图像,特别是RGBE格式的文件。
2.1.1 ☘️HDR 图片
HDR,High-Dynamic Range的简称,意思是高动态范围图像,相比普通的图像,可以提供更多的动态范围和图像细节,根据不同的曝光时间的LDR(Low-Dynamic Range)图像,利用每个曝光时间相对应最佳细节的LDR图像来合成最终HDR图像 ,能够更好的反映出真实环境中的视觉效果。
2.1.2 ☘️用法
导入与初始化
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
// 创建实例
const loader = new RGBELoader();
加载 HDR 文件
// 同步加载
loader.load('path/to/file.hdr', function(texture) {// 成功回调
}, undefined, function(error) {// 错误处理
});
// 异步加载,推荐
loader.loadAsync('path/to/file.hdr').then(texture => {// 成功处理}).catch(error => {// 错误处理});
示例代码
import * as THREE from 'three';
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
import { PMREMGenerator } from 'three/addons/pmrem/PMREMGenerator.js';// 初始化场景
const scene = new THREE.Scene();
const renderer = new THREE.WebGLRenderer({ antialias: true });
document.body.appendChild(renderer.domElement);// 加载HDR环境贴图
const loader = new RGBELoader();
loader.loadAsync('assets/sky.hdr').then(texture => {texture.mapping = THREE.EquirectangularReflectionMapping;// PMREM优化const pmremGenerator = new PMREMGenerator(renderer);const envMap = pmremGenerator.fromEquirectangular(texture).texture;scene.environment = envMap;pmremGenerator.dispose();// 创建示例物体const sphere = new THREE.Mesh(new THREE.SphereGeometry(1, 32, 16),new THREE.MeshStandardMaterial({envMap: envMap,metalness: 0.9,roughness: 0.1}));scene.add(sphere);}).catch(err => console.error('HDR加载失败:', err));// 相机与渲染循环
const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);
camera.position.z = 5;function animate() {requestAnimationFrame(animate);renderer.render(scene, camera);
}
animate();
1. ☘️实现思路
这个样例是一个交互 + 三维可视化动画,用 Three.js 实现,核心内容是一个带环境光、粒子效果、泡泡(bubble)几何体 + 可以点击“pop”掉一些泡泡 + 基于光照/HDR 环境贴图来增强视觉感。整体风格 梦幻、稍带科幻/装饰感。
2. ☘️代码样例
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>交互式泡泡、粒子特效与科幻氛围</title><script src="https://unpkg.com/three@0.123.0/build/three.min.js"></script><script src="https://unpkg.com/three@0.123.0/examples/js/loaders/GLTFLoader.js"></script><script src="https://unpkg.com/three@0.123.0/examples/js/loaders/RGBELoader.js"></script><script src="https://unpkg.com/three@0.123.0/examples/js/controls/OrbitControls.js"></script><style>html,body {margin: 0;height: 100%;background: #22124a;overflow: hidden;perspective: 10rem;}.container {width: 100%;height: 100%;display: block;position: relative;}#canvas {position: absolute;width: 100%;height: 100%;overflow: hidden;}.sky {width: 100%;height: 100%;opacity: 0.5;background: url("https://stivs-assets.s3.us-east-2.amazonaws.com/mrp/background.png") repeat;background-size: cover;position: absolute;right: 0;top: 0;bottom: 0;}</style>
</head>
<body><div class="container"><div class="sky"></div><div id="canvas"></div></div>
</body>
<script type="x-shader/x-vertex" id="vertexshader">varying vec2 vUv;void main() {vUv = uv;gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );}</script><script type="x-shader/x-fragment" id="fragmentshader">uniform sampler2D baseTexture;uniform sampler2D bloomTexture;varying vec2 vUv;void main() {gl_FragColor = ( texture2D( baseTexture, vUv ) + vec4( 1.0 ) * texture2D( bloomTexture, vUv ) );}
</script>
<script type="importmap">{"imports": {"three": "https://unpkg.com/three@0.177.0/build/three.module.js","three/addons/": "https://unpkg.com/three@0.177.0/examples/jsm/"}}</script>
<script type="module">let scene,camera,controls,fieldOfView,aspectRatio,nearPlane,farPlane,renderer,container,hdrCubeRenderTarget,HEIGHT,WIDTH,hdrEquirect,tinky,particles,raycaster;const params = {color: 0x21024f,transmission: 0.9,envMapIntensity: 10,lightIntensity: 1,exposure: 0.5};const spheres = [];const meshes = {};const generateTexture = () => {const canvas = document.createElement("canvas");canvas.width = 2;canvas.height = 2;const context = canvas.getContext("2d");context.fillStyle = "white";context.fillRect(0, 1, 2, 1);return canvas;};const createScene = () => {HEIGHT = window.innerHeight;WIDTH = window.innerWidth;raycaster = new THREE.Raycaster();scene = new THREE.Scene();aspectRatio = WIDTH / HEIGHT;fieldOfView = 60;nearPlane = 1;farPlane = 10000;camera = new THREE.PerspectiveCamera(fieldOfView,aspectRatio,nearPlane,farPlane);camera.position.x = 0;camera.position.z = 500;camera.position.y = -10;renderer = new THREE.WebGLRenderer({alpha: true,antialias: true});renderer.setPixelRatio(window.devicePixelRatio);renderer.setSize(WIDTH, HEIGHT);renderer.shadowMap.enabled = true;renderer.shadowMap.type = THREE.PCFSoftShadowMap;renderer.toneMapping = THREE.ACESFilmicToneMapping;renderer.toneMappingExposure = 2;container = document.getElementById("canvas");container.appendChild(renderer.domElement);window.addEventListener("resize", handleWindowResize, false);scene.add(tinky);controls = new THREE.OrbitControls(camera, renderer.domElement);controls.maxDistance = 1000;controls.maxAzimuthAngle = 1;controls.minAzimuthAngle = -1;};const positionElements = () => {meshes.bigStar.position.y = -1.7;meshes.bigStar.position.x = -2.2;meshes.bigStar.position.z = 0.8;meshes.bigStar.rotation.z = -0.5;meshes.littleStar.position.y = -1.75;meshes.littleStar.position.x = 1.75;meshes.littleStar.position.z = 0.6;meshes.littleStar.rotation.z = 0.5;meshes.planet.position.y = 1.3;meshes.planet.position.x = 2.6;meshes.planet.position.z = 1;meshes.ClosedLeftEye.visible = false;meshes.ClosedRightEye.visible = false;};const handleWindowResize = () => {HEIGHT = window.innerHeight;WIDTH = window.innerWidth;renderer.setSize(WIDTH, HEIGHT);camera.aspect = WIDTH / HEIGHT;camera.updateProjectionMatrix();};const createLights = () => {const ambientLight = new THREE.AmbientLight(0xaa54f0, 1);const directionalLight1 = new THREE.DirectionalLight(0xffffff, 1);directionalLight1.position.set(-2, 2, 5);const directionalLight2 = new THREE.DirectionalLight(0xfff000, 1);directionalLight2.position.set(-2, 4, 4);directionalLight2.castShadow = true;scene.add(ambientLight, directionalLight1, directionalLight2);};const createBubbles = () => {const pmremGenerator = new THREE.PMREMGenerator(renderer);hdrCubeRenderTarget = pmremGenerator.fromEquirectangular(hdrEquirect);hdrEquirect.dispose();pmremGenerator.dispose();const bubbleTexture = new THREE.CanvasTexture(generateTexture());bubbleTexture.repeat.set(1);const bubbleMaterial = new THREE.MeshPhysicalMaterial({color: params.color,metalness: 0,roughness: 0,alphaMap: bubbleTexture,alphaTest: 0.5,envMap: hdrCubeRenderTarget.texture,envMapIntensity: params.envMapIntensity,depthWrite: false,transmission: params.transmission,opacity: 1,transparent: true});const bubbleMaterial1b = new THREE.MeshPhysicalMaterial().copy(bubbleMaterial);bubbleMaterial1b.side = THREE.BackSide;const bubbleGeometry1 = new THREE.SphereBufferGeometry(170, 64, 32);const bubbleGeometry2 = new THREE.SphereBufferGeometry(55, 64, 32);const bubbleGeometry3 = new THREE.SphereBufferGeometry(30, 64, 32);const bubbleGeometry4 = new THREE.SphereBufferGeometry(70, 64, 32);let bubble1 = new THREE.Mesh(bubbleGeometry1, bubbleMaterial1b);bubble1.position.z = 15;let bubble2 = new THREE.Mesh(bubbleGeometry2, bubbleMaterial1b);bubble2.position.y = -135;bubble2.position.x = -175;bubble2.position.z = 75;let bubble3 = new THREE.Mesh(bubbleGeometry3, bubbleMaterial1b);bubble3.position.y = -136;bubble3.position.x = 137;bubble3.position.z = 50;let bubble4 = new THREE.Mesh(bubbleGeometry4, bubbleMaterial1b);bubble4.position.y = 100;bubble4.position.x = 210;bubble4.position.z = 70;scene.add(bubble1, bubble2, bubble3, bubble4);};const createParticles = () => {const particlesGeometry = new THREE.BufferGeometry();const color = new THREE.Color();let components = [];const count = 400;const positions = new Float32Array(count * 3);const colors = new Float32Array(count * 3);for (let i = 0; i < count; i++) {if (i % 3 === 0) {color.setHSL(Math.random(), 1, 0.5);components = [color.r, color.g, color.b];}positions[i] = (Math.random() - 0.5) * 1000;colors[i] = components[i % 3];}particlesGeometry.setAttribute("position",new THREE.BufferAttribute(positions, 3));particlesGeometry.setAttribute("color",new THREE.BufferAttribute(colors, 3, true));const textureLoader = new THREE.TextureLoader();const particlesTexture = textureLoader.load("https://mrp.vercel.app/magic_05.png");const particlesMaterial = new THREE.PointsMaterial({size: 17,alphaMap: particlesTexture,transparent: true,depthWrite: false,blending: THREE.AdditiveBlending,vertexColors: true});particles = new THREE.Points(particlesGeometry, particlesMaterial);scene.add(particles);};window.addEventListener("click", (event) => {raycaster.setFromCamera(new THREE.Vector2((event.clientX / window.innerWidth) * 2 - 1,-(event.clientY / window.innerHeight) * 2 + 1),camera);const intersects = raycaster.intersectObjects(spheres);for (let i = 0; i < intersects.length; i++) {const sphere = intersects[i].object;scene.remove(sphere);spheres.splice(spheres.indexOf(sphere), 1);}});const time = new THREE.Clock();const loop = () => {const elapsedTime = time.getElapsedTime();const elapsedTimeInMs = Math.round(elapsedTime * 1000);controls.update();particles.rotation.y = elapsedTime * 0.02;for (const sphere of spheres) {const radius = 350;const speed = 0.02 + 0.01 * sphere.randomness;const heightAngle = elapsedTime * speed + sphere.randomness;const thetaAngle = elapsedTime * -speed + sphere.randomness * 0.5;sphere.position.x = radius * Math.cos(thetaAngle) * Math.sin(heightAngle);sphere.position.y = radius * Math.sin(thetaAngle) * Math.sin(heightAngle);sphere.position.z = radius * Math.cos(heightAngle);}if (meshes.planet) {meshes.planet.rotation.y += 0.002;meshes.planet.rotation.z += 0.002;}if (elapsedTimeInMs % 3000 > 2750) {meshes.RightEye.visible = false;meshes.LeftEye.visible = false;meshes.ClosedLeftEye.visible = true;meshes.ClosedRightEye.visible = true;} else {meshes.ClosedLeftEye.visible = false;meshes.ClosedRightEye.visible = false;meshes.RightEye.visible = true;meshes.LeftEye.visible = true;}renderer.render(scene, camera);requestAnimationFrame(loop);};const main = async () => {hdrEquirect = await new THREE.RGBELoader().setDataType(THREE.UnsignedByteType).load("https://stivs-assets.s3.us-east-2.amazonaws.com/mrp/env.hdr");await new Promise((resolve) => {new THREE.GLTFLoader().load("https://stivs-assets.s3.us-east-2.amazonaws.com/mrp/model.gltf", (gltf) => {tinky = gltf.scene;tinky.castShadow = true;tinky.receiveShadow = true;tinky.scale.set(80, 80, 80);tinky.children.forEach((el) => {el.receiveShadow = true;meshes[el.name] = el;});resolve();});});positionElements();createScene();createLights();createBubbles();createParticles();const bubbleGeometry5 = new THREE.SphereBufferGeometry(10, 64, 32);const pmremGenerator = new THREE.PMREMGenerator(renderer);hdrCubeRenderTarget = pmremGenerator.fromEquirectangular(hdrEquirect);hdrEquirect.dispose();pmremGenerator.dispose();const bubbleTexture = new THREE.CanvasTexture(generateTexture());bubbleTexture.repeat.set(1);const bubbleMaterial = new THREE.MeshPhysicalMaterial({color: params.color,metalness: 0,roughness: 0,alphaMap: bubbleTexture,alphaTest: 0.5,envMap: hdrCubeRenderTarget.texture,envMapIntensity: params.envMapIntensity,depthWrite: false,transmission: params.transmission,opacity: 1,transparent: true});const bubbleMaterial1b = new THREE.MeshPhysicalMaterial().copy(bubbleMaterial);bubbleMaterial1b.side = THREE.BackSide;setInterval(() => {if (spheres.length > 20) return;const mesh = new THREE.Mesh(bubbleGeometry5, bubbleMaterial1b);mesh.position.x = Math.random() * 1350 - 725;mesh.position.y = Math.random() * 1350 - 725;mesh.position.z = Math.random() * 1350 - 725;mesh.scale.x = mesh.scale.y = mesh.scale.z = Math.random() * 3 + 1;mesh.randomness = Math.random() * 50;spheres.push(mesh);scene.add(mesh);}, 2000);renderer.render(scene, camera);loop();};main();</script>
</html>
效果如下:

源码
