学习threejs,把“数据流”做成未来城市光轨

👨‍⚕️ 主页: gis分享者

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

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

效果如下:

源码

相关推荐
军军君011 个月前
Three.js基础功能学习十五:智能黑板实现实例二
开发语言·前端·javascript·vue.js·3d·threejs·三维
军军君011 个月前
Three.js基础功能学习十八:智能黑板实现实例五
前端·javascript·vue.js·3d·typescript·前端框架·threejs
军军君011 个月前
Three.js基础功能学习十六:智能黑板实现实例三
前端·javascript·css·vue.js·3d·前端框架·threejs
军军君011 个月前
Three.js基础功能学习十四:智能黑板实现实例一
前端·javascript·css·typescript·前端框架·threejs·智能黑板
汀、人工智能1 个月前
[特殊字符] 第97课:前K个高频元素
数据结构·算法·数据库架构··数据流·前k个高频元素
汀、人工智能1 个月前
[特殊字符] 第98课:数据流中位数
数据结构·算法·数据库架构··数据流·数据流中位数
陶甜也1 个月前
3D智慧城市:blender建模、骨骼、动画、VUE、threeJs引入渲染,飞行视角,涟漪、人物行走
前端·3d·vue·blender·threejs·模型
汀、人工智能1 个月前
[特殊字符] 第58课:两个正序数组的中位数
数据结构·算法·数据库架构··数据流·两个正序数组的中位数
We་ct1 个月前
LeetCode 295. 数据流的中位数:双堆解法实战解析
开发语言·前端·数据结构·算法·leetcode·typescript·数据流