👨⚕️ 主页: gis分享者
👨⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅!
文章目录
- 一、🍀前言
-
- [1.1 ☘️THREE.ShaderMaterial](#1.1 ☘️THREE.ShaderMaterial)
-
- [1.1.1 ☘️注意事项](#1.1.1 ☘️注意事项)
- [1.1.2 ☘️构造函数](#1.1.2 ☘️构造函数)
- [1.1.3 ☘️属性](#1.1.3 ☘️属性)
- [1.1.4 ☘️方法](#1.1.4 ☘️方法)
- [1.2 ☘️THREE.EffectComposer 后期处理](#1.2 ☘️THREE.EffectComposer 后期处理)
-
- [1.2.1 ☘️代码示例](#1.2.1 ☘️代码示例)
- [1.2.2 ☘️构造函数](#1.2.2 ☘️构造函数)
- [1.2.3 ☘️属性](#1.2.3 ☘️属性)
- [1.2.4 ☘️方法](#1.2.4 ☘️方法)
- [1.3 ☘️THREE.RenderPass](#1.3 ☘️THREE.RenderPass)
-
- [1.3.1 ☘️构造函数](#1.3.1 ☘️构造函数)
- [1.3.2 ☘️属性](#1.3.2 ☘️属性)
- [1.3.3 ☘️方法](#1.3.3 ☘️方法)
- [1.4 ☘️THREE.UnrealBloomPass](#1.4 ☘️THREE.UnrealBloomPass)
-
- [1.4.1 ☘️构造函数](#1.4.1 ☘️构造函数)
- [1.4.2 ☘️方法](#1.4.2 ☘️方法)
- [1.5 ☘️THREE.OutputPass](#1.5 ☘️THREE.OutputPass)
-
- [1.5.1 ☘️构造函数](#1.5.1 ☘️构造函数)
- [1.5.2 ☘️属性](#1.5.2 ☘️属性)
- [1.5.3 ☘️方法](#1.5.3 ☘️方法)
- [1.6 ☘️THREE.ShaderPass](#1.6 ☘️THREE.ShaderPass)
-
- [1.6.1 ☘️构造函数](#1.6.1 ☘️构造函数)
- [1.6.2 ☘️属性](#1.6.2 ☘️属性)
- [1.6.3 ☘️方法](#1.6.3 ☘️方法)
- 二、🍀把"数据流"做成未来城市光轨
-
- [1. ☘️实现思路](#1. ☘️实现思路)
- [2. ☘️代码样例](#2. ☘️代码样例)
一、🍀前言
本文详细介绍如何基于threejs在三维场景中把"数据流"做成未来城市光轨,亲测可用。希望能帮助到您。一起学习,加油!加油!
1.1 ☘️THREE.ShaderMaterial
THREE.ShaderMaterial使用自定义shader渲染的材质。 shader是一个用GLSL编写的小程序 ,在GPU上运行。
1.1.1 ☘️注意事项
- ShaderMaterial 只有使用 WebGLRenderer 才可以绘制正常, 因为 vertexShader 和
fragmentShader 属性中GLSL代码必须使用WebGL来编译并运行在GPU中。 - 从 THREE r72开始,不再支持在ShaderMaterial中直接分配属性。 必须使用
BufferGeometry实例,使用BufferAttribute实例来定义自定义属性。 - 从 THREE r77开始,WebGLRenderTarget 或 WebGLCubeRenderTarget
实例不再被用作uniforms。 必须使用它们的texture 属性。 - 内置attributes和uniforms与代码一起传递到shaders。
如果您不希望WebGLProgram向shader代码添加任何内容,则可以使用RawShaderMaterial而不是此类。 - 您可以使用指令#pragma unroll_loop_start,#pragma unroll_loop_end
以便通过shader预处理器在GLSL中展开for循环。 该指令必须放在循环的正上方。循环格式必须与定义的标准相对应。 - 循环必须标准化normalized。
- 循环变量必须是i。
- 对于给定的迭代,值 UNROLLED_LOOP_INDEX 将替换为 i 的显式值,并且可以在预处理器语句中使用。
javascript
#pragma unroll_loop_start
for ( int i = 0; i < 10; i ++ ) {
// ...
}
#pragma unroll_loop_end
代码示例
javascript
const material = new THREE.ShaderMaterial( {
uniforms: {
time: { value: 1.0 },
resolution: { value: new THREE.Vector2() }
},
vertexShader: document.getElementById( 'vertexShader' ).textContent,
fragmentShader: document.getElementById( 'fragmentShader' ).textContent
} );
1.1.2 ☘️构造函数
ShaderMaterial( parameters : Object )
parameters - (可选)用于定义材质外观的对象,具有一个或多个属性。 材质的任何属性都可以从此处传入(包括从Material继承的任何属性)。
1.1.3 ☘️属性
共有属性请参见其基类Material。
.clipping : Boolean
定义此材质是否支持剪裁; 如果渲染器传递clippingPlanes uniform,则为true。默认值为false。
.defaultAttributeValues : Object
当渲染的几何体不包含这些属性但材质包含这些属性时,这些默认值将传递给shaders。这可以避免在缓冲区数据丢失时出错。
javascript
this.defaultAttributeValues = {
'color': [ 1, 1, 1 ],
'uv': [ 0, 0 ],
'uv2': [ 0, 0 ]
};
.defines : Object
使用 #define 指令在GLSL代码为顶点着色器和片段着色器定义自定义常量;每个键/值对产生一行定义语句:
javascript
defines: {
FOO: 15,
BAR: true
}
这将在GLSL代码中产生如下定义语句:
javascript
#define FOO 15
#define BAR true
.extensions : Object
一个有如下属性的对象:
javascript
this.extensions = {
derivatives: false, // set to use derivatives
fragDepth: false, // set to use fragment depth values
drawBuffers: false, // set to use draw buffers
shaderTextureLOD: false // set to use shader texture LOD
};
.fog : Boolean
定义材质颜色是否受全局雾设置的影响; 如果将fog uniforms传递给shader,则为true。默认值为false。
.fragmentShader : String
片元着色器的GLSL代码。这是shader程序的实际代码。在上面的例子中, vertexShader 和 fragmentShader 代码是从DOM(HTML文档)中获取的; 它也可以作为一个字符串直接传递或者通过AJAX加载。
.glslVersion : String
定义自定义着色器代码的 GLSL 版本。仅与 WebGL 2 相关,以便定义是否指定 GLSL 3.0。有效值为 THREE.GLSL1 或 THREE.GLSL3。默认为空。
.index0AttributeName : String
如果设置,则调用gl.bindAttribLocation 将通用顶点索引绑定到属性变量。默认值未定义。
.isShaderMaterial : Boolean
只读标志,用于检查给定对象是否属于 ShaderMaterial 类型。
.lights : Boolean
材质是否受到光照的影响。默认值为 false。如果传递与光照相关的uniform数据到这个材质,则为true。默认是false。
.linewidth : Float
控制线框宽度。默认值为1。
由于OpenGL Core Profile与大多数平台上WebGL渲染器的限制,无论如何设置该值,线宽始终为1。
.flatShading : Boolean
定义材质是否使用平面着色进行渲染。默认值为false。
.uniforms : Object
如下形式的对象:
javascript
{ "uniform1": { value: 1.0 }, "uniform2": { value: 2 } }
指定要传递给shader代码的uniforms;键为uniform的名称,值(value)是如下形式:
javascript
{ value: 1.0 }
这里 value 是uniform的值。名称必须匹配 uniform 的name,和GLSL代码中的定义一样。 注意,uniforms逐帧被刷新,所以更新uniform值将立即更新GLSL代码中的相应值。
.uniformsNeedUpdate : Boolean
可用于在 Object3D.onBeforeRender() 中更改制服时强制进行制服更新。默认为假。
.vertexColors : Boolean
定义是否使用顶点着色。默认为假。
.vertexShader : String
顶点着色器的GLSL代码。这是shader程序的实际代码。 在上面的例子中,vertexShader 和 fragmentShader 代码是从DOM(HTML文档)中获取的; 它也可以作为一个字符串直接传递或者通过AJAX加载。
.wireframe : Boolean
将几何体渲染为线框(通过GL_LINES而不是GL_TRIANGLES)。默认值为false(即渲染为平面多边形)。
.wireframeLinewidth : Float
控制线框宽度。默认值为1。
由于OpenGL Core Profile与大多数平台上WebGL渲染器的限制,无论如何设置该值,线宽始终为1。
1.1.4 ☘️方法
共有方法请参见其基类Material。
.clone () : ShaderMaterial this : ShaderMaterial
创建该材质的一个浅拷贝。需要注意的是,vertexShader和fragmentShader使用引用拷贝; attributes的定义也是如此; 这意味着,克隆的材质将共享相同的编译WebGLProgram; 但是,uniforms 是 值拷贝,这样对不同的材质我们可以有不同的uniforms变量。
1.2 ☘️THREE.EffectComposer 后期处理
THREE.EffectComposer 用于在three.js中实现后期处理效果。该类管理了产生最终视觉效果的后期处理过程链。 后期处理过程根据它们添加/插入的顺序来执行,最后一个过程会被自动渲染到屏幕上。
1.2.1 ☘️代码示例
javascript
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.2.2 ☘️构造函数
EffectComposer( renderer : WebGLRenderer, renderTarget : WebGLRenderTarget )
renderer -- 用于渲染场景的渲染器。
renderTarget -- (可选)一个预先配置的渲染目标,内部由 EffectComposer 使用。
1.2.3 ☘️属性
.passes : Array
一个用于表示后期处理过程链(包含顺序)的数组。
javascript
渲染通道:
BloomPass 该通道会使得明亮区域参入较暗的区域。模拟相机照到过多亮光的情形
DotScreenPass 将一层黑点贴到代表原始图片的屏幕上
FilmPass 通过扫描线和失真模拟电视屏幕
MaskPass 在当前图片上贴一层掩膜,后续通道只会影响被贴的区域
RenderPass 该通道在指定的场景和相机的基础上渲染出一个新的场景
SavePass 执行该通道时,它会将当前渲染步骤的结果复制一份,方便后面使用。这个通道实际应用中作用不大;
ShaderPass 使用该通道你可以传入一个自定义的着色器,用来生成高级的、自定义的后期处理通道
TexturePass 该通道可以将效果组合器的当前状态保存为一个纹理,然后可以在其他EffectCoposer对象中将该纹理作为输入参数
.readBuffer : WebGLRenderTarget
内部读缓冲区的引用。过程一般从该缓冲区读取先前的渲染结果。
.renderer : WebGLRenderer
内部渲染器的引用。
.renderToScreen : Boolean
最终过程是否被渲染到屏幕(默认帧缓冲区)。
.writeBuffer : WebGLRenderTarget
内部写缓冲区的引用。过程常将它们的渲染结果写入该缓冲区。
1.2.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.3 ☘️THREE.RenderPass
THREE.RenderPass用于将场景渲染到中间缓冲区,为后续的后期处理效果(如模糊、色调调整等)提供基础。
1.3.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.3.2 ☘️属性
.enabled:boolean
是否启用此通道(默认 true)。设为 false 可跳过渲染。
.clear:boolean
渲染前是否清除画布(默认 true)。若需叠加多个 RenderPass,可设为 false。
.needsSwap:boolean
是否需要在渲染后交换缓冲区(通常保持默认 false)。
1.3.3 ☘️方法
.setSize(width, height)
调整通道的渲染尺寸(通常由 EffectComposer 自动调用)。
width: 画布宽度(像素)。
height: 画布高度(像素)。
1.4 ☘️THREE.UnrealBloomPass
UnrealBloomPass 是 Three.js 中实现高质量泛光效果的后期处理通道,通过模拟类似 Unreal Engine 的泛光效果,为场景中的明亮区域添加柔和的光晕,提升视觉表现力。
1.4.1 ☘️构造函数
new UnrealBloomPass(resolution, strength, radius, threshold)
- resolution (Vector2): 泛光效果应用的场景分辨率,需与画布尺寸一致。
示例:new THREE.Vector2(window.innerWidth, window.innerHeight) - strength (Number): 泛光强度,默认值 1.0。值越大,光晕越明显。
- radius (Number): 模糊半径,默认值 0.4。值越大,光晕扩散范围越广。
- threshold (Number): 泛光阈值,默认值 0.85。仅对亮度高于此值的区域生效。
1.4.2 ☘️方法
- renderToScreen: 是否直接渲染到屏幕,默认为 false(需通过 EffectComposer 管理)。
- clearColor: 设置背景清除颜色,默认为透明。
1.5 ☘️THREE.OutputPass
OutputPass 是 Three.js 后期处理(Post-Processing)中的一个通道(Pass),用于控制最终渲染输出的颜色空间、色调映射(Tone Mapping)和抗锯齿等效果。它通常作为 EffectComposer 的最后一个通道,负责将处理后的图像输出到屏幕。
1.5.1 ☘️构造函数
new OutputPass(resolution)
resolution (Vector2): 可选参数,指定输出分辨率。默认值为 null,自动匹配画布尺寸。
1.5.2 ☘️属性
.output:WebGLRenderTarget
类型: WebGLRenderTarget
描述: 存储最终渲染结果的渲染目标对象。
.uniforms:Object
类型: Object
描述: 包含着色器统一变量(Uniforms)的对象,用于控制输出效果。常用属性:
toneMappingExposure (Number): 色调映射曝光值,默认 1.0。
toneMappingType (Number): 色调映射算法类型,默认 NoToneMapping。
.needsSwap:boolean
类型: Boolean
描述: 是否需要交换帧缓冲区,通常设为 true 以确保输出到屏幕。
1.5.3 ☘️方法
.setSize(width, height)
调整通道的渲染尺寸(通常由 EffectComposer 自动调用)。
width: 画布宽度(像素)。
height: 画布高度(像素)。
.render(renderer, writeBuffer, readBuffer)
功能: 执行渲染操作(通常由 EffectComposer 自动调用)。
1.6 ☘️THREE.ShaderPass
THREE.ShaderPass是 Three.js 后期处理模块的核心组件之一,允许开发者通过自定义着色器(Shader)实现任意特效,为后期处理链提供高度灵活性。
1.6.1 ☘️构造函数
ShaderPass(shader, textureID)
- shader Object 包含着色器代码和 uniforms 的配置对象。
- textureID string (可选) 输入纹理的 uniform 名称(默认 tDiffuse)。
1.6.2 ☘️属性
.enabled:boolean
是否启用此通道(默认 true)。设为 false 可临时禁用效果。
.uniforms:object
着色器 uniforms 的引用,支持动态修改参数:
javascript
shaderPass.uniforms.uStrength.value = 0.8; // 修改自定义参数
.renderToScreen:boolean
是否直接渲染到屏幕(默认 false)。若为最后通道,需设为 true。
1.6.3 ☘️方法
.setSize(width, height)
调整通道的渲染尺寸(通常由 EffectComposer 自动调用)。
width: 画布宽度(像素)。
height: 画布高度(像素)。
二、🍀把"数据流"做成未来城市光轨
1. ☘️实现思路
使用 Three.js 的 WebGL 小实验。在大多数 Three.js 项目里,我们看到的往往是模型展示、简单动画,或者基础的粒子效果。但这次这段源码,做的事情不一样------它用 纯程序 + Shader + 后处理,构建了一个类似"数据流动宇宙"的视觉系统。。具体代码参考下面代码样例。
2. ☘️代码样例
html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>把"数据流"做成了未来城市光轨</title>
<script async src="https://unpkg.com/es-module-shims@1.8.0/dist/es-module-shims.js"></script>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/",
"lil-gui": "https://unpkg.com/lil-gui@0.19.1/dist/lil-gui.esm.min.js"
}
}
</script>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #000000;
}
canvas {
display: block;
}
</style>
</head>
<body>
<script type="module">
import * as THREE from "three";
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js";
import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js";
import { OutputPass } from "three/addons/postprocessing/OutputPass.js";
import GUI from "lil-gui";
// --- Core Variables ---
let scene, camera, renderer, composer, bloomPass, blurPass, outputPass;
let clock = new THREE.Clock();
let globalTime = 0;
let floorMesh;
const trailObjects = [];
const trailMaterials = [];
// --- Configuration ---
const config = {
dpr: 1, // Default Device Pixel Ratio as requested
bloomStrength: 0.4,
bloomRadius: 0.8,
bloomThreshold: 0.0,
speedMultiplier: 0.2,
linesCount: 100,
dotDensity: 70,
dotSize: 0.12375,
dotSpeed: 0.5,
blurStrength: 2.3,
// Geometry settings
arcRadius: 38.07,
bendStartZ: -26.2,
floorLength: 85.0,
wallHeight: 171.0,
// Selectable colors
color0: "#004c94",
color1: "#2e89ff",
color2: "#003994",
color3: "#004bad",
color4: "#ff5900"
};
// Optimized Custom Curve
class CycCurve extends THREE.Curve {
constructor(x, zStart, zBend, radius, yEnd) {
super();
this.x = x;
this.zStart = zStart;
this.zBend = zBend;
this.radius = radius;
this.yEnd = yEnd;
this.L_flat = Math.abs(zStart - (zBend + radius));
this.L_arc = Math.PI * radius * 0.5;
this.L_up = Math.max(0.1, yEnd - radius);
this.totalLength = this.L_flat + this.L_arc + this.L_up;
}
getPoint(t, optionalTarget = new THREE.Vector3()) {
const d = t * this.totalLength;
let py = 0,
pz = 0;
if (d <= this.L_flat) {
pz = this.zStart - d;
} else if (d <= this.L_flat + this.L_arc) {
const norm = (d - this.L_flat) / this.L_arc;
const eased = norm * norm * (3.0 - 2.0 * norm);
const angle = (norm * 0.4 + eased * 0.6) * (Math.PI * 0.5);
py = this.radius * (1.0 - Math.cos(angle));
pz = this.zBend + this.radius - Math.sin(angle) * this.radius;
} else {
py = this.radius + (d - (this.L_flat + this.L_arc));
pz = this.zBend;
}
return optionalTarget.set(this.x, py, pz);
}
}
init();
animate();
function init() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);
camera = new THREE.PerspectiveCamera(
80,
window.innerWidth / window.innerHeight,
1,
2000
);
camera.position.set(0, 12, 100);
renderer = new THREE.WebGLRenderer({
antialias: false,
powerPreference: "high-performance"
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(config.dpr); // Set initial DPR
renderer.toneMapping = THREE.LinearToneMapping;
document.body.appendChild(renderer.domElement);
camera.lookAt(0, 20, -50);
const renderTarget = new THREE.WebGLRenderTarget(
window.innerWidth,
window.innerHeight,
{
type: THREE.HalfFloatType,
format: THREE.RGBAFormat,
samples: 8
}
);
const renderScene = new RenderPass(scene, camera);
bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
config.bloomStrength,
config.bloomRadius,
config.bloomThreshold
);
const foregroundBlurShader = {
uniforms: {
tDiffuse: { value: null },
resolution: {
value: new THREE.Vector2(window.innerWidth, window.innerHeight)
},
blurStrength: { value: config.blurStrength }
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
uniform vec2 resolution;
uniform float blurStrength;
varying vec2 vUv;
void main() {
float mask = 1.0 - smoothstep(0.0, 0.35, vUv.y);
float radius = mask * blurStrength;
if (radius < 0.1) {
gl_FragColor = texture2D(tDiffuse, vUv);
} else {
vec4 color = vec4(0.0);
float total = 0.0;
const float GA = 2.3999632;
for (int i = 0; i < 32; i++) {
float f = float(i);
float r = sqrt(f) * radius;
float theta = f * GA;
vec2 offset = vec2(cos(theta), sin(theta)) * (r / resolution);
color += texture2D(tDiffuse, vUv + offset);
total += 1.0;
}
gl_FragColor = color / total;
}
}
`
};
blurPass = new ShaderPass(foregroundBlurShader);
outputPass = new OutputPass();
composer = new EffectComposer(renderer, renderTarget);
composer.addPass(renderScene);
composer.addPass(bloomPass);
composer.addPass(blurPass);
composer.addPass(outputPass);
createFloor();
generateTrails();
updateGeometries();
setupGUI();
window.addEventListener("resize", onWindowResize);
}
function createFloor() {
const floorMat = new THREE.MeshBasicMaterial({
color: 0x000000,
transparent: true,
opacity: 0.85,
depthWrite: false,
side: THREE.DoubleSide
});
floorMesh = new THREE.Mesh(new THREE.BufferGeometry(), floorMat);
floorMesh.position.set(0, -0.5, -0.5);
floorMesh.renderOrder = 1;
scene.add(floorMesh);
}
function generateTrails() {
const group = new THREE.Group();
scene.add(group);
const vertexShader = `
varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vViewPosition;
void main() {
vUv = uv;
vNormal = normalMatrix * normal;
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
vViewPosition = -mvPosition.xyz;
gl_Position = projectionMatrix * mvPosition;
}
`;
const fragmentShader = `
varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vViewPosition;
uniform float uTime;
uniform vec3 uColor;
uniform float uSpeed;
uniform float uOffset;
uniform float uTailLength;
uniform float uIntensityMultiplier;
uniform float uBendUv;
uniform float uIsReflection;
uniform float uDotDensity;
uniform float uDotSize;
uniform float uDotSpeed;
float hash(vec2 p) {
return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
}
void main() {
float t = fract(uTime * uSpeed + uOffset);
float dist = fract(t - vUv.x + 1.0);
float baseAlpha = smoothstep(uTailLength, 0.0, dist);
baseAlpha = pow(max(0.0, baseAlpha), 1.2);
vec3 viewDir = normalize(vViewPosition);
float fresnel = max(0.0, dot(normalize(vNormal), viewDir));
baseAlpha *= smoothstep(0.0, 0.05, fresnel);
float core = pow(max(0.0, baseAlpha), 3.0) * 1.5;
float movingUV = vUv.x - (uTime * uSpeed * uDotSpeed) - uOffset;
float signalPos = movingUV * uDotDensity;
float dotId = floor(signalPos);
float dotLocal = fract(signalPos);
float distToCenter = length(vec2((dotLocal - 0.5) * 2.0, (fract(vUv.y + 0.5) - 0.5) * 6.0));
float dotShape = 1.0 - smoothstep(0.0, max(0.001, uDotSize), distToCenter);
float dotFinal = dotShape * step(0.6, hash(vec2(dotId, uOffset))) * (sin(uTime * 4.0 + hash(vec2(dotId)) * 6.28) * 0.3 + 0.7) * baseAlpha;
if (uIsReflection > 0.5) {
float refFade = smoothstep(uBendUv + 0.05, uBendUv - 0.05, vUv.x);
baseAlpha *= refFade;
core *= refFade;
dotFinal *= refFade * 0.1;
baseAlpha = pow(max(0.0, baseAlpha), 0.5) * (0.7 + hash(vUv * 300.0 + uTime * 0.05) * 0.3);
core *= 0.3;
}
vec3 trailColor = uColor * (baseAlpha + core * 1.5) * uIntensityMultiplier;
vec3 rgb = trailColor / max(1.0 - clamp(dotFinal * 1.8, 0.0, 0.95), 0.001) + uColor * dotFinal * 2.5 * uIntensityMultiplier;
gl_FragColor = vec4(rgb, (baseAlpha + dotFinal) * uIntensityMultiplier);
}
`;
for (let i = 0; i < config.linesCount; i++) {
const normIdx = (i / (config.linesCount - 1)) * 2 - 1;
const startX =
Math.sign(normIdx) * Math.pow(Math.abs(normIdx), 1.8) * 60 +
(Math.random() - 0.5) * 1.5;
const thickness = Math.random() * 0.15 + 0.08;
const colorIdx = Math.floor(Math.random() * 5);
const uniforms = {
uTime: { value: 0 },
uColor: { value: new THREE.Color(config[`color${colorIdx}`]) },
uColorIndex: { value: colorIdx },
uSpeed: { value: Math.random() * 0.5 + 0.2 },
uOffset: { value: Math.random() },
uTailLength: { value: Math.random() * 0.4 + 0.3 },
uIntensityMultiplier: { value: 1.0 },
uBendUv: { value: 0.0 },
uIsReflection: { value: 0.0 },
uDotDensity: { value: config.dotDensity },
uDotSize: { value: config.dotSize },
uDotSpeed: { value: config.dotSpeed }
};
const material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
uniforms,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending
});
const mesh = new THREE.Mesh(new THREE.BufferGeometry(), material);
const refMaterial = material.clone();
refMaterial.uniforms.uIntensityMultiplier.value = 0.4;
refMaterial.uniforms.uIsReflection.value = 1.0;
const refMesh = new THREE.Mesh(new THREE.BufferGeometry(), refMaterial);
refMesh.scale.y = -1;
refMesh.position.y = -1.0;
group.add(mesh, refMesh);
trailMaterials.push(material.uniforms, refMaterial.uniforms);
trailObjects.push({ mesh, refMesh, startX, thickness });
}
}
function updateGeometries() {
if (floorMesh.geometry) floorMesh.geometry.dispose();
const floorGeo = new THREE.PlaneGeometry(1000, 1000, 150, 150);
floorGeo.rotateX(-Math.PI * 0.5);
const pos = floorGeo.attributes.position.array;
for (let i = 0; i < pos.length; i += 3) {
if (pos[i + 2] < config.bendStartZ) {
let d = config.bendStartZ - pos[i + 2];
let maxA = config.arcRadius * Math.PI * 0.5;
if (d < maxA) {
let n = d / maxA;
let a = (n * 0.4 + n * n * (3.0 - 2.0 * n) * 0.6) * (Math.PI * 0.5);
pos[i + 1] = config.arcRadius * (1.0 - Math.cos(a));
pos[i + 2] = config.bendStartZ - Math.sin(a) * config.arcRadius;
} else {
pos[i + 1] = config.arcRadius + (d - maxA);
pos[i + 2] = config.bendStartZ - config.arcRadius;
}
}
}
floorGeo.computeVertexNormals();
floorMesh.geometry = floorGeo;
const bendUv =
Math.abs(config.floorLength - config.bendStartZ) /
(Math.abs(config.floorLength - config.bendStartZ) +
Math.PI * config.arcRadius * 0.5 +
Math.max(0.1, config.wallHeight - config.arcRadius));
trailObjects.forEach((obj) => {
const path = new CycCurve(
obj.startX,
config.floorLength,
config.bendStartZ - config.arcRadius,
config.arcRadius,
config.wallHeight
);
const geo = new THREE.TubeGeometry(path, 200, obj.thickness, 8, false);
if (obj.mesh.geometry) obj.mesh.geometry.dispose();
obj.mesh.geometry = obj.refMesh.geometry = geo;
obj.mesh.material.uniforms.uBendUv.value = obj.refMesh.material.uniforms.uBendUv.value = bendUv;
});
}
function setupGUI() {
const gui = new GUI({ title: "Scene Settings" });
// System Folder for DPR and basic rendering settings
const sys = gui.addFolder("System");
sys
.add(config, "dpr", 0.5, 2.0, 0.01)
.name("DPR (Pixel Ratio)")
.onChange((v) => {
renderer.setPixelRatio(v);
composer.setPixelRatio(v);
onWindowResize(); // Re-sync buffer sizes
});
const post = gui.addFolder("Post-Processing (Bloom)");
post
.add(config, "bloomStrength", 0.0, 5.0)
.name("Strength")
.onChange((v) => (bloomPass.strength = v));
post
.add(config, "bloomRadius", 0.0, 1.5)
.name("Radius")
.onChange((v) => (bloomPass.radius = v));
post
.add(config, "bloomThreshold", 0.0, 1.0)
.name("Threshold")
.onChange((v) => (bloomPass.threshold = v));
post
.add(config, "blurStrength", 0.0, 20.0)
.name("Foreground Blur")
.onChange((v) => (blurPass.uniforms.blurStrength.value = v));
const geom = gui.addFolder("Geometry Settings");
const upd = () => updateGeometries();
geom.add(config, "arcRadius", 10.0, 200.0).name("Arc Radius").onChange(upd);
geom
.add(config, "bendStartZ", -100.0, 100.0)
.name("Bend Start Z")
.onChange(upd);
geom
.add(config, "floorLength", 50.0, 500.0)
.name("Floor Length")
.onChange(upd);
geom
.add(config, "wallHeight", 50.0, 1000.0)
.name("Wall Height")
.onChange(upd);
const cols = gui.addFolder("Trail Colors");
for (let i = 0; i < 5; i++) {
cols
.addColor(config, `color${i}`)
.name(`Color ${i + 1}`)
.onChange((v) => {
const c = new THREE.Color(v);
trailMaterials.forEach((m) => {
if (m.uColorIndex.value === i) m.uColor.value.copy(c);
});
});
}
const anim = gui.addFolder("Animation");
anim.add(config, "speedMultiplier", 0.0, 1.0).name("Global Speed");
const dots = gui.addFolder("Signal Dots");
const setDots = (u, v) => trailMaterials.forEach((m) => (m[u].value = v));
dots
.add(config, "dotDensity", 10.0, 100.0)
.name("Density")
.onChange((v) => setDots("uDotDensity", v));
dots
.add(config, "dotSize", 0.05, 0.5)
.name("Dot Size")
.onChange((v) => setDots("uDotSize", v));
dots
.add(config, "dotSpeed", 0.5, 3.0)
.name("Speed Multiplier")
.onChange((v) => setDots("uDotSpeed", v));
gui.close(); // Hide panel by default
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
blurPass.uniforms.resolution.value.set(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
globalTime += clock.getDelta() * config.speedMultiplier;
trailMaterials.forEach((m) => (m.uTime.value = globalTime));
composer.render();
}
</script>
</body>
</html>
效果如下:

源码