学习threejs,打造交互式泡泡、粒子特效与科幻氛围

👨‍⚕️ 主页: gis分享者

👨‍⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅!

👨‍⚕️ 收录于专栏:threejs 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>

效果如下:

源码

相关推荐
陶甜也4 天前
ThreeJS曲线动画:打造炫酷3D路径运动
前端·vue·threejs
gis分享者11 天前
学习threejs,打造交互式花卉生成器
交互·threejs·生成·shadermaterial·花卉·planegeometry
患得患失94915 天前
【ThreeJs】【性能优化】从渲染底层到业务逻辑的系统性提速方案
优化·threejs
接着奏乐接着舞。17 天前
3D地球可视化教程 - 第3篇:地球动画与相机控制
前端·vue.js·3d·threejs
gis分享者17 天前
学习threejs,实现粒子化交互文字
threejs·文字·shadermaterial·粒子化·icosahedron
患得患失9491 个月前
【Threejs】【工具类】Raycaster实现 3D 交互(如鼠标拾取、碰撞检测)的核心工具
3d·交互·threejs·raycaster
gis分享者1 个月前
学习threejs,使用自定义GLSL 着色器,实现水面、粒子特效
threejs·着色器·glsl·粒子·shadermaterial·unrealbloompass·水面
陶甜也2 个月前
threeJS 实现开花的效果
前端·vue·blender·threejs
二川bro2 个月前
第25节:VR基础与WebXR API入门
前端·3d·vr·threejs