尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Three.js 封装电流效果:从 Shader 到可复用组件的完整实践

Three.js 封装电流效果:从 Shader 到可复用组件的完整实践 1. Three.js 电流效果到底是什么适合谁用Three.js 电流效果本质上是沿着一条预设路径样条线生成会抖动、会分叉、会闪烁的发光线条视觉上像闪电或高压电弧在导线里流动。它和普通Line最大的区别在于普通线是静态的、边缘平滑的电流效果是动态的、带随机扰动的而且每一帧都在重新计算顶点位置所以看起来活。能做什么在可视化大屏里它常被用来表示数据流向、能量传输、告警链路在游戏特效里用来做技能释放、机关通电、Boss 蓄力。适合谁适合已经会写基础 Three.js 场景会加相机、灯光、渲染循环但被怎么把闪电封装成能复用的组件卡住的开发者。如果你还在纠结Scene怎么创建建议先把官方 examples 跑一遍再回来。我试过直接抄官方LightningStrike的 demo能跑但一旦想加第二条、第三条电流代码就开始互相污染全局变量满天飞、outlineMeshArray越 push 越多、切换路径时旧电流不消失。这篇就把这套东西拆开做成一个ElectricLine类路径、颜色、粗细、速度全部走配置最后给你一份能直接复制进项目的组件代码。核心检索词先明确Three.js 电流效果封装、LightningStrike 可复用组件、CatmullRomCurve3 电流路径。下面所有代码都围绕这三个词展开。先说清楚原理不然后面调参你会懵。LightningStrike继承自BufferGeometry它内部维护了一组射线参数rayParameters每次调用update(time)时会根据timeScale、roughness、straightness这些参数用噪声算法重新生成顶点。也就是说电流的抖动不是贴图动画是几何体每帧重建。这也是为什么它必须配合MeshBasicMaterial而不是LineBasicMaterial——它本质是个 Mesh。理解了这点封装思路就清晰了一个电流 一条路径 一组射线参数 一个材质 一个更新函数。把这四样打包进类外部只传路径点和配置项内部自己管生命周期。下面进入实操。2. TaoToken 前置把模型对话和 Coding Plan 接进来辅助调参写 Shader 和调电流参数这件事最烦的不是写代码是这个roughness到底调多少才像闪电。我的做法是把当前参数贴给模型让它根据视觉描述给建议值再自己微调。这里用 TaoToken 的模型对话能力来辅助比纯靠猜快很多。TaoToken 是一个聚合多家大模型 API 的平台你可以用同一个 Key 调用不同模型适合这种调参问一句、写代码问一句的碎片化场景。官网入口在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册后在控制台创建 API Key 即可。具体怎么用打开模型对话页面 https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 把这段提示词丢进去我在用 Three.js 的 LightningStrike 做电流效果当前 rayParams 是 roughness: 1.55, straightness: 1.20, ramification: 10, recursionProbability: 0.6, timeScale: 10.7 现在电流看起来太直了分支也不够多想要更狂野的闪电感 请给出这几个参数的调整方向和推荐值并说明每个参数对视觉的影响。模型会告诉你straightness越小越扭曲、ramification越大分支越多。这种问答式调参比翻源码快得多。如果你是要长期写 Three.js 项目、经常需要模型帮你补全代码那更适合用 Coding Plan入口在 https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。它按订阅走适合高频调用。API Key 在控制台 https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 里管理接入文档在 https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。这里要强调一点TaoToken 只是帮你调参和写代码的辅助工具它不参与你的 Three.js 运行时渲染。电流效果最终跑在浏览器里靠的是LightningStrike和你的渲染循环。别把两者搞混。拿到 Key 之后如果你用 Cline 或 Claude Code 这类工具配置方式是把 Base URL 指向https://taotoken.net/apiKey 填你创建的Model ID 按文档里列出的填。三件套缺一不可Base URL、Key、Model ID。少一个就会报 401 或 model not found。3. 可复制配置ElectricLine 组件封装与参数文件这一节是核心直接给可复制的代码。先看目录结构建议src/ effects/ ElectricLine.js // 电流组件类 electricConfig.js // 参数配置 main.js // 场景入口先写配置文件electricConfig.js把路径和射线参数抽出来方便不同场景复用// src/effects/electricConfig.js import * as THREE from three; // 电流射线参数控制抖动、分支、速度 export const DEFAULT_RAY_PARAMS { sourceOffset: new THREE.Vector3(), destOffset: new THREE.Vector3(), radius0: 6, radius1: 6, minRadius: 2.5, maxIterations: 7, isEternal: true, timeScale: 10.7, propagationTimeFactor: 0.05, vanishingTimeFactor: 0.15, subrayPeriod: 3.5, subrayDutyCycle: 0.8, maxSubrayRecursion: 1.2, ramification: 10, recursionProbability: 0.6, roughness: 1.55, straightness: 1.20 }; // 颜色配置 export const DEFAULT_COLORS { lightning: 0xB0FFFF, outline: 0x00FFFF }; // 路径配置每条路径一组控制点 export const PATH_PRESETS { main: [ new THREE.Vector3(-10, 1300, 0), new THREE.Vector3(-300, 700, 0), new THREE.Vector3(300, 700, 0), new THREE.Vector3(-200, 500, 0), new THREE.Vector3(200, 400, 0), new THREE.Vector3(-600, 200, 0), new THREE.Vector3(400, 200, 0) ], branch: [ new THREE.Vector3(500, 500, 0), new THREE.Vector3(900, 600, 0), new THREE.Vector3(450, 200, 0), new THREE.Vector3(1100, 500, 0), new THREE.Vector3(1150, 450, 0) ] };然后是组件类ElectricLine.js。注意这里把LightningStrike的导入路径按你项目实际位置调整官方 examples 里在jsm/geometries/LightningStrike.js// src/effects/ElectricLine.js import * as THREE from three; import { LightningStrike } from three/examples/jsm/geometries/LightningStrike.js; import { DEFAULT_RAY_PARAMS, DEFAULT_COLORS } from ./electricConfig.js; export class ElectricLine { constructor(scene, options {}) { this.scene scene; this.rayParams { ...DEFAULT_RAY_PARAMS, ...(options.rayParams || {}) }; this.lightningColor new THREE.Color( options.lightningColor ?? DEFAULT_COLORS.lightning ); this.outlineColor new THREE.Color( options.outlineColor ?? DEFAULT_COLORS.outline ); this.timeRate options.timeRate ?? 4; this.currentTime 0; this.material new THREE.MeshBasicMaterial({ color: this.lightningColor }); this.strike new LightningStrike(this.rayParams); this.mesh new THREE.Mesh(this.strike, this.material); this.mesh.renderOrder 2; scene.add(this.mesh); this.beginPosition new THREE.Vector3(); this.endPosition new THREE.Vector3(); } // 设置电流两端位置 setEndpoints(begin, end) { this.beginPosition.copy(begin); this.endPosition.copy(end); } // 每帧调用delta 为 clock.getDelta() update(delta) { this.currentTime this.timeRate * delta; if (this.currentTime 0) this.currentTime 0; this.strike.rayParameters.sourceOffset.copy(this.beginPosition); this.strike.rayParameters.destOffset.copy(this.endPosition); this.strike.update(this.currentTime); } // 销毁避免内存泄漏 dispose() { this.scene.remove(this.mesh); this.strike.dispose?.(); this.material.dispose(); } }关键点说明renderOrder 2是为了配合后期处理里的OutlinePass让电流的辉光盖在场景物体之上。setEndpoints把两端位置独立出来这样同一条电流可以动态改变起点终点比如从 A 设备流向 B 设备。再写一个路径管理器把CatmullRomCurve3采样成点数组逐段生成电流// src/effects/PathElectricManager.js import * as THREE from three; import { ElectricLine } from ./ElectricLine.js; export class PathElectricManager { constructor(scene, options {}) { this.scene scene; this.options options; this.lines []; this.pointList []; this.curIndex 0; this.segmentLength options.segmentLength ?? 50; } // 传入控制点数组生成采样点 setPath(controlPoints) { const curve new THREE.CatmullRomCurve3(controlPoints); this.pointList curve.getPoints(this.segmentLength); this.curIndex 0; this.clear(); } clear() { this.lines.forEach((l) l.dispose()); this.lines []; } update(delta) { if (this.pointList.length 2) return; if (this.curIndex this.pointList.length - 1) { this.curIndex 0; this.clear(); return; } const begin this.pointList[this.curIndex]; const end this.pointList[this.curIndex 1]; const line new ElectricLine(this.scene, this.options); line.setEndpoints(begin, end); this.lines.push(line); this.curIndex; // 更新所有已生成电流 this.lines.forEach((l) l.update(delta)); } }这套封装的好处路径、参数、颜色全部走配置新增一条电流只要setPath一次。clear()负责销毁避免旧电流残留。4. 验证请求在浏览器里跑通电流流动与消散代码写完了得验证。新建main.js把场景、相机、渲染循环搭起来重点看电流是否沿路径流动、是否闪烁、走完是否消散。// src/main.js import * as THREE from three; import { OrbitControls } from three/examples/jsm/controls/OrbitControls.js; import { EffectComposer } from three/examples/jsm/postprocessing/EffectComposer.js; import { RenderPass } from three/examples/jsm/postprocessing/RenderPass.js; import { OutlinePass } from three/examples/jsm/postprocessing/OutlinePass.js; import { PathElectricManager } from ./effects/PathElectricManager.js; import { PATH_PRESETS } from ./effects/electricConfig.js; const container document.getElementById(div3D); const scene new THREE.Scene(); scene.background new THREE.Color(0x050510); const camera new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 1, 5000000 ); camera.position.set(557, 1221, 2863); const renderer new THREE.WebGLRenderer({ antialias: true, alpha: true }); renderer.setSize(window.innerWidth, window.innerHeight); container.appendChild(renderer.domElement); const controls new OrbitControls(camera, renderer.domElement); controls.enableDamping true; controls.target.set(-200, 0, -200); // 后期处理给电流加辉光 const composer new EffectComposer(renderer); composer.addPass(new RenderPass(scene, camera)); const outlinePass new OutlinePass( new THREE.Vector2(window.innerWidth, window.innerHeight), scene, camera, [] ); outlinePass.edgeStrength 1.9; outlinePass.edgeGlow 0.7; outlinePass.edgeThickness 2.3; outlinePass.visibleEdgeColor.set(0x00FFFF); outlinePass.hiddenEdgeColor.set(0); composer.addPass(outlinePass); // 创建电流管理器 const manager new PathElectricManager(scene, { lightningColor: 0xB0FFFF, outlineColor: 0x00FFFF, timeRate: 4, rayParams: { roughness: 1.55, straightness: 1.20, ramification: 10 } }); manager.setPath(PATH_PRESETS.main); // 把电流 mesh 加入 outline 检测 manager.lines.forEach((l) outlinePass.selectedObjects.push(l.mesh)); const clock new THREE.Clock(); function animate() { requestAnimationFrame(animate); const delta clock.getDelta(); manager.update(delta); // 动态同步 outline 目标 outlinePass.selectedObjects manager.lines.map((l) l.mesh); controls.update(); composer.render(); } animate();跑起来后你应该看到一条青白色电流从路径起点开始逐段向终点推进每段电流自身在抖动、分叉走完一段后旧段被清除整体像电在导线里跑。如果电流不动检查timeRate是否为 0如果电流不消散检查manager.update里curIndex重置逻辑。验证成功的标志有三个一是电流沿CatmullRomCurve3的弯曲路径走不是直线二是每段电流有随机分支不是一根光棍三是走完整条路径后场景里没有残留 mesh。你可以打开控制台console.log(manager.lines.length)正常应该在 0 到路径点数之间循环。想调视觉改electricConfig.js里的roughness越大越粗糙、straightness小于 1 更扭曲、ramification分支数量。改完刷新页面即可不用动组件代码。5. 本篇常见错排查401、local proxy failed、reading choices调这套东西时报错基本集中在两类一类是模型辅助工具TaoToken/Cline的接入报错一类是 Three.js 运行时报错。分开说。401 Unauthorized出现在你用 Cline 或 Claude Code 调 TaoToken 时。原因通常是 Key 没填、Key 填错、或者 Base URL 写成了带 UTM 的地址。注意 API 调用地址是https://taotoken.net/api不要带?utm_source...那串参数那是给网页访问用的。三件套检查Base URL https://taotoken.net/apiKey 控制台创建的那串Model ID 文档里列出的模型名。缺一个就 401。local proxy failed这个报错一般出现在你本地开了某个代理工具但代理没启动或端口不对。解决方式是关掉代理设置或者确认代理端口和工具里配置的一致。注意这里说的是本地开发环境的网络配置问题不涉及任何网络访问方式的选择纯粹是端口和进程是否匹配。reading choices这是解析模型返回时字段对不上。常见于你把非 OpenAI 格式的返回当 OpenAI 格式解析。检查你的请求体里model字段是否是 TaoToken 支持的模型 ID以及返回结构里choices[0].message.content是否存在。如果返回的是流式要按 SSE 逐块解析。Three.js 侧报错LightningStrike is not a constructor说明导入路径错了确认three/examples/jsm/geometries/LightningStrike.js存在Cannot read properties of undefined (reading update)说明strike没创建成功检查rayParams是否传了非法值电流不显示检查MeshBasicMaterial颜色是否和背景同色以及renderOrder是否被其他物体遮挡。OAuth 相关报错如果你用 Claude Code 接入报 OAuth 失败通常是认证方式选错了。Claude Code 走的是 API Key 模式不是 OAuth 登录模式在配置里选 API Key 而不是账号登录。配置项写全 Base URL、Key、Model ID 三件套。排查顺序建议先确认模型工具能正常返回用 curl 测一下再确认 Three.js 场景能渲染加个立方体试试最后才调电流参数。分层排查别一上来就怀疑 Shader。6. 语义一致 CTA把电流组件接进你的项目到这里ElectricLine和PathElectricManager两个类已经能独立工作了。接进你现有项目就三步把effects目录拷过去在场景初始化后new PathElectricManager(scene, options)在渲染循环里调manager.update(delta)。路径换成你自己的控制点数组即可。如果你在调参阶段想让模型帮你快速试不同风格模型对话入口在 https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 长期写 Three.js 项目、需要模型持续补全代码的用 Coding Plan https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite Key 在控制台 https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 管理接入细节看文档 https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。最后给个实用技巧电流的消散效果靠的是vanishingTimeFactor和路径走完后clear()的配合。如果你想要电流走到终点后停留一会儿再消失把isEternal设为 false并在update里加一个setTimeout延迟clear()。这个我踩过坑直接clear()会显得很突兀加 200ms 延迟过渡更自然。
返回列表