👨⚕️ 主页: 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 在当前图片上贴一层掩膜,后续通道只会影响被贴
的区域
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>
效果如下
参考:源码