👨⚕️ 主页: gis分享者
👨⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅!
文章目录
- 一、🍀前言
-
- [1.1 ☘️GLTFLoader glTF 2.0资源加载器](#1.1 ☘️GLTFLoader glTF 2.0资源加载器)
-
- [1.1.1 ☘️代码示例](#1.1.1 ☘️代码示例)
- [1.1.2 ☘️构造函数](#1.1.2 ☘️构造函数)
- [1.1.3 ☘️方法](#1.1.3 ☘️方法)
- [2.1 ☘️RGBELoader HDR图像加载器](#2.1 ☘️RGBELoader HDR图像加载器)
-
- [2.1.1 ☘️HDR 图片](#2.1.1 ☘️HDR 图片)
- [2.1.2 ☘️用法](#2.1.2 ☘️用法)
- [1. ☘️实现思路](#1. ☘️实现思路)
- [2. ☘️代码样例](#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 ☘️代码示例
javascript
// 初始化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.Group
gltf.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 ☘️用法
导入与初始化
javascript
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
// 创建实例
const loader = new RGBELoader();
加载 HDR 文件
javascript
// 同步加载
loader.load('path/to/file.hdr', function(texture) {
// 成功回调
}, undefined, function(error) {
// 错误处理
});
// 异步加载,推荐
loader.loadAsync('path/to/file.hdr')
.then(texture => {
// 成功处理
})
.catch(error => {
// 错误处理
});
示例代码
javascript
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. ☘️代码样例
html
<!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>
效果如下:

源码
