学习threejs,虫洞特效

👨‍⚕️ 主页: gis分享者
👨‍⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅!
👨‍⚕️ 收录于专栏:threejs gis工程师



一、🍀前言

本文详细介绍如何基于threejs在三维场景中实现虫洞特效,亲测可用。希望能帮助到您。一起学习,加油!加油!

1.1 ☘️THREE.EffectComposer 后期处理

THREE.EffectComposer 用于在three.js中实现后期处理效果。该类管理了产生最终视觉效果的后期处理过程链。 后期处理过程根据它们添加/插入的顺序来执行,最后一个过程会被自动渲染到屏幕上。

1.1.1 ☘️代码示例

import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
// 初始化 composer
const composer = new EffectComposer(renderer);
// 创建 RenderPass 并添加到 composer
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
// 添加其他后期处理通道(如模糊)
// composer.addPass(blurPass);
// 在动画循环中渲染
function animate() {
  composer.render();
  requestAnimationFrame(animate);
}

1.1.2 ☘️构造函数

EffectComposer( renderer : WebGLRenderer, renderTarget : WebGLRenderTarget )
renderer – 用于渲染场景的渲染器。
renderTarget – (可选)一个预先配置的渲染目标,内部由 EffectComposer 使用。

1.1.3 ☘️属性

.passes : Array
一个用于表示后期处理过程链(包含顺序)的数组。

渲染通道:
BloomPass   该通道会使得明亮区域参入较暗的区域。模拟相机照到过多亮光的情形
DotScreenPass   将一层黑点贴到代表原始图片的屏幕上
FilmPass    通过扫描线和失真模拟电视屏幕
MaskPass    在当前图片上贴一层掩膜,后续通道只会影响被贴![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/b2d43891e8984bca92c2826093d2a144.gif)
的区域
RenderPass  该通道在指定的场景和相机的基础上渲染出一个新的场景
SavePass    执行该通道时,它会将当前渲染步骤的结果复制一份,方便后面使用。这个通道实际应用中作用不大;
ShaderPass  使用该通道你可以传入一个自定义的着色器,用来生成高级的、自定义的后期处理通道
TexturePass 该通道可以将效果组合器的当前状态保存为一个纹理,然后可以在其他EffectCoposer对象中将该纹理作为输入参数

.readBuffer : WebGLRenderTarget
内部读缓冲区的引用。过程一般从该缓冲区读取先前的渲染结果。

.renderer : WebGLRenderer
内部渲染器的引用。

.renderToScreen : Boolean
最终过程是否被渲染到屏幕(默认帧缓冲区)。

.writeBuffer : WebGLRenderTarget
内部写缓冲区的引用。过程常将它们的渲染结果写入该缓冲区。

1.1.4 ☘️方法

.addPass ( pass : Pass ) : undefined
pass – 将被添加到过程链的过程

将传入的过程添加到过程链。

.dispose () : undefined
释放此实例分配的 GPU 相关资源。每当您的应用程序不再使用此实例时调用此方法。

.insertPass ( pass : Pass, index : Integer ) : undefined
pass – 将被插入到过程链的过程。

index – 定义过程链中过程应插入的位置。

将传入的过程插入到过程链中所给定的索引处。

.isLastEnabledPass ( passIndex : Integer ) : Boolean
passIndex – 被用于检查的过程

如果给定索引的过程在过程链中是最后一个启用的过程,则返回true。 由EffectComposer所使用,来决定哪一个过程应当被渲染到屏幕上。

.removePass ( pass : Pass ) : undefined
pass – 要从传递链中删除的传递。

从传递链中删除给定的传递。

.render ( deltaTime : Float ) : undefined
deltaTime – 增量时间值。

执行所有启用的后期处理过程,来产生最终的帧,

.reset ( renderTarget : WebGLRenderTarget ) : undefined
renderTarget – (可选)一个预先配置的渲染目标,内部由 EffectComposer 使用。

重置所有EffectComposer的内部状态。

.setPixelRatio ( pixelRatio : Float ) : undefined
pixelRatio – 设备像素比

设置设备的像素比。该值通常被用于HiDPI设备,以阻止模糊的输出。 因此,该方法语义类似于WebGLRenderer.setPixelRatio()。

.setSize ( width : Integer, height : Integer ) : undefined
width – EffectComposer的宽度。
height – EffectComposer的高度。

考虑设备像素比,重新设置内部渲染缓冲和过程的大小为(width, height)。 因此,该方法语义类似于WebGLRenderer.setSize()。

.swapBuffers () : undefined
交换内部的读/写缓冲。

1.2 ☘️THREE.RenderPass

THREE.RenderPass用于将场景渲染到中间缓冲区,为后续的后期处理效果(如模糊、色调调整等)提供基础。

1.2.1 ☘️构造函数

RenderPass(scene, camera, overrideMaterial, clearColor, clearAlpha)

  • scene THREE.Scene 要渲染的 Three.js 场景对象。
  • camera THREE.Camera 场景对应的相机(如 PerspectiveCamera)。
  • overrideMaterial THREE.Material (可选) 覆盖场景中所有物体的材质(默认 null)。
  • clearColor THREE.Color (可选) 渲染前清除画布的颜色(默认不主动清除)。
  • clearAlpha number (可选) 清除画布的透明度(默认 0)。

1.2.2 ☘️属性

.enabled:boolean
是否启用此通道(默认 true)。设为 false 可跳过渲染。

.clear:boolean
渲染前是否清除画布(默认 true)。若需叠加多个 RenderPass,可设为 false。

.needsSwap:boolean
是否需要在渲染后交换缓冲区(通常保持默认 false)。

1.2.3 ☘️方法

.setSize(width, height)
调整通道的渲染尺寸(通常由 EffectComposer 自动调用)。
width: 画布宽度(像素)。
height: 画布高度(像素)。

1.3 ☘️THREE.UnrealBloomPass

THREE.UnrealBloomPass是 是 Three.js 中用于实现 虚幻引擎风格泛光效果(Bloom) 的后期处理通道。它通过模拟光线散射和光晕效果,增强场景中高光区域的视觉表现。

1.3.1 ☘️构造函数

THREE.UnrealBloomPass(
new THREE.Vector2(width, height), // 渲染目标尺寸(通常与画布一致)
strength, // 泛光强度 (默认 1)
radius, // 泛光半径 (默认 0)
threshold // 泛光阈值 (默认 0)
)

new THREE.Vector2(width, height)
渲染目标的分辨率,通常与画布尺寸一致(如 new THREE.Vector2(window.innerWidth, window.innerHeight))。
strength(强度)
控制泛光效果的强度(亮度)。值越大,泛光越明显。
范围:0(无效果)到 3(强烈)。
radius(半径)
控制泛光的扩散半径。值越大,光晕范围越广。
范围:0(无扩散)到 1(较大扩散)。
threshold(阈值)
仅对亮度高于此值的像素应用泛光。值越低,更多区域会被处理。
范围:0(所有像素)到 1(仅最亮像素)。

1.3.2 ☘️使用示例

// 初始化
const composer = new THREE.EffectComposer(renderer);
const renderPass = new THREE.RenderPass(scene, camera);
const bloomPass = new THREE.UnrealBloomPass(
  new THREE.Vector2(window.innerWidth, window.innerHeight),
  1.2, 0.2, 0.8
);

// 添加通道
composer.addPass(renderPass);
composer.addPass(bloomPass);

// 渲染
function animate() {
  requestAnimationFrame(animate);
  composer.render();
}

1.3.3 ☘️方法

.setSize(width, height)
调整通道的渲染尺寸(通常由 EffectComposer 自动调用)。
width: 画布宽度(像素)。
height: 画布高度(像素)。

.render(renderer, writeBuffer, readBuffer, deltaTime, maskActive)
内部方法,通常由 EffectComposer 自动调用,无需手动执行。

1.4 ☘️THREE.ShaderPass

THREE.ShaderPass是 Three.js 后期处理模块的核心组件之一,允许开发者通过自定义着色器(Shader)实现任意特效,为后期处理链提供高度灵活性。

1.4.1 ☘️构造函数

ShaderPass(shader, textureID)

  • shader Object 包含着色器代码和 uniforms 的配置对象。
  • textureID string (可选) 输入纹理的 uniform 名称(默认 tDiffuse)。

1.4.2 ☘️属性

.enabled:boolean
是否启用此通道(默认 true)。设为 false 可临时禁用效果。

.uniforms:object
着色器 uniforms 的引用,支持动态修改参数:

shaderPass.uniforms.uStrength.value = 0.8; // 修改自定义参数

.renderToScreen:boolean
是否直接渲染到屏幕(默认 false)。若为最后通道,需设为 true。

1.4.3 ☘️方法

.setSize(width, height)
调整通道的渲染尺寸(通常由 EffectComposer 自动调用)。
width: 画布宽度(像素)。
height: 画布高度(像素)。

二、🍀虫洞特效

1. ☘️实现思路

通过EffectComposer后期处理组合器,RenderPass、ShaderPass、UnrealBloomPass(泛光)后期处理通道,以及自定义shader着色器实现虫洞特效。具体代码参考代码样例。可以直接运行。

2. ☘️代码样例

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>虫洞</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/100/three.min.js"></script>
    <script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/68819/EffectComposer.js"></script>
    <script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/68819/RenderPass.js"></script>
    <script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/68819/ShaderPass.js"></script>
    <script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/68819/CopyShader.js"></script>
    <script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/68819/LuminosityHighPassShader.js"></script>
    <script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/68819/UnrealBloomPass.js"></script>
    <script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/gsap-latest-beta.min.js"></script>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
</head>
<style>
    body {
        margin: 0;
    }

    .experience {
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100vh;
        z-index: 2;
    }

    .scrollTarget {
        position: absolute;
        height: 1000vh;
        width: 100px;
        top: 0;
        z-index: 0;
    }

    .vignette-radial {
        position: fixed;
        z-index: 1;
        top: 0;
        left: 0;
        height: 100vh;
        width: 100%;
        pointer-events: none;
    }

    .vignette-radial:after {
        pointer-events: none;
        content: " ";
        position: absolute;
        top: 0;
        left: 0;
        bottom: 0;
        right: 0;
    }

</style>
<body>
<canvas class="experience"></canvas>
<div class="scrollTarget"></div>
<div class="vignette-radial"></div>
<script>
  const MathUtils = {
    normalize: (value, min, max) => (value - min) / (max - min),
    interpolate: (normValue, min, max) => min + (max - min) * normValue,
    map: (value, min1, max1, min2, max2) => {
      value = Math.min(Math.max(value, min1), max1);
      return MathUtils.interpolate(
        MathUtils.normalize(value, min1, max1),
        min2,
        max2
      );
    }
  };

  let w = window.innerWidth;
  let h = window.innerHeight;

  const renderer = new THREE.WebGLRenderer({
    canvas: document.querySelector("canvas"),
    antialias: true,
    shadowMapEnabled: true,
    shadowMapType: THREE.PCFSoftShadowMap
  });
  renderer.setSize(w, h);

  const scene = new THREE.Scene();
  scene.background = new THREE.Color(0x000000);

  const camera = new THREE.PerspectiveCamera(45, w / h, 0.001, 200);
  let cameraRotationProxyX = Math.PI;
  let cameraRotationProxyY = 0;
  camera.rotation.y = cameraRotationProxyX;
  camera.rotation.z = cameraRotationProxyY;

  const cameraGroup = new THREE.Group();
  cameraGroup.position.z = 400;
  cameraGroup.add(camera);
  scene.add(cameraGroup);

  const generatePathPoints = (count = 10, spacing = 25) => {
    const points = [];
    for (let i = 0; i < count; i++) {
      const x = i * spacing;
      const y = Math.sin(i * 0.5 + Math.random()) * 100 + 50;
      const z = Math.cos(i * 0.3 + Math.random()) * 100 + 50;
      points.push(new THREE.Vector3(x, z, y));
    }
    return points;
  };

  const points = generatePathPoints(10);
  const path = new THREE.CatmullRomCurve3(points);
  path.closed = true;
  path.tension = 1;

  const ringCount = 600;
  const ringRadius = 3;
  const ringSegments = 32;

  const geometry = new THREE.TubeGeometry(
    path,
    ringCount,
    ringRadius,
    ringSegments,
    true
  );
  const wireframe = new THREE.LineSegments(
    new THREE.EdgesGeometry(geometry),
    new THREE.LineBasicMaterial({ linewidth: 0.1, opacity: 0.1 })
  );
  scene.add(wireframe);

  const ringMaterial = new THREE.LineBasicMaterial({ color: 0xffffff });

  const ringMaterial1 = new THREE.LineBasicMaterial({
    color: 0xffffff,
    transparent: true,
    opacity: 0.8,
    depthWrite: false
  });

  const frenetFrames = path.computeFrenetFrames(ringCount, true);

  for (let i = 0; i <= ringCount; i++) {
    const t = i / ringCount;
    const pos = path.getPointAt(t);
    const normal = frenetFrames.normals[i];
    const binormal = frenetFrames.binormals[i];

    const ringPoints = [];
    for (let j = 0; j <= ringSegments; j++) {
      const theta = (j / ringSegments) * Math.PI * 2;
      const x = Math.cos(theta) * ringRadius;
      const y = Math.sin(theta) * ringRadius;

      const point = new THREE.Vector3().addVectors(
        pos,
        new THREE.Vector3()
          .addScaledVector(normal, x)
          .addScaledVector(binormal, y)
      );

      ringPoints.push(point);
    }

    const ringGeometry = new THREE.BufferGeometry().setFromPoints(ringPoints);
    const ringMesh = new THREE.LineLoop(ringGeometry, ringMaterial);
    scene.add(ringMesh);
  }

  const light = new THREE.PointLight(0xffffff, 0.1, 4, 0);
  light.castShadow = true;
  scene.add(light);

  const renderScene = new THREE.RenderPass(scene, camera);
  const bloomPass = new THREE.UnrealBloomPass(
    new THREE.Vector2(w, h),
    1.5,
    0.4,
    0.5
  );
  bloomPass.renderToScreen = true;

  const composer = new THREE.EffectComposer(renderer);
  composer.setSize(w, h);
  composer.addPass(renderScene);
  composer.addPass(bloomPass);

  let cameraTargetPercentage = 0;
  let currentCameraPercentage = 0;

  function updateCameraPercentage(percentage) {
    const p1 = path.getPointAt(percentage % 1);
    const p2 = path.getPointAt((percentage + 0.01) % 1);

    cameraGroup.position.set(p1.x, p1.y, p1.z);
    cameraGroup.lookAt(p2);
    light.position.set(p2.x, p2.y, p2.z);
  }

  const tubePerc = { percent: 0 };

  function render(time) {
    cameraTargetPercentage = (cameraTargetPercentage + 0.001) % 1;
    updateCameraPercentage(cameraTargetPercentage);
    composer.render();

    camera.rotation.y += (cameraRotationProxyX - camera.rotation.y) / 15;
    camera.rotation.x += (cameraRotationProxyY - camera.rotation.x) / 15;

    requestAnimationFrame(render);
  }

  function render() {
    cameraTargetPercentage = (cameraTargetPercentage + 0.001) % 1;

    camera.rotation.y += (cameraRotationProxyX - camera.rotation.y) / 15;
    camera.rotation.x += (cameraRotationProxyY - camera.rotation.x) / 15;
    updateCameraPercentage(cameraTargetPercentage);
    composer.render();
    requestAnimationFrame(render);
    console.log(cameraTargetPercentage);
  }

  requestAnimationFrame(render);

  window.addEventListener("resize", () => {
    w = window.innerWidth;
    h = window.innerHeight;
    camera.aspect = w / h;
    camera.updateProjectionMatrix();
    renderer.setSize(w, h);
    composer.setSize(w, h);
  });
</script>
</body>
</html>

效果如下
在这里插入图片描述
参考:源码

评论 40
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

gis分享者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值