当前位置: 首页 > news >正文

Three.js 道路流光教程

道路流光 ·Road Shader· ▶ 在线运行案例

  • 案例合集:三维可视化功能案例(threehub.cn)
  • 开源仓库github地址:https://github.com/z2586300277/three-cesium-examples
  • 400个案例代码:网盘链接

你将学到什么

  • ShaderMaterial 自定义着色器实现核心视觉效果
  • EffectComposer 多 Pass 后期处理管线
  • OrbitControls 相机轨道交互
  • requestAnimationFrame渲染循环与resize自适应

效果说明

本案例演示道路流光效果:原场景渲染后经 EffectComposer 叠加 Bloom/模糊等全屏后期;核心用到 ShaderMaterial、EffectComposer、OrbitControls。建议先打开文首在线案例查看动态画面,再对照下方源码逐步理解。

核心概念

  • Scene / Camera / WebGLRenderer构成最小渲染闭环;大场景可开logarithmicDepthBuffer缓解 Z-fighting。
  • ShaderMaterial通过uniforms+ 自定义 GLSL 控制逐像素/逐点效果;透明粒子常配合depthTest: false
  • EffectComposer以多 Pass 链式渲染:RenderPass → 特效 Pass → 输出屏幕,替代直接renderer.render
  • OrbitControls提供轨道旋转/缩放;开启enableDamping后需在 animate 中controls.update()

实现步骤

  • 搭建 Scene、PerspectiveCamera、WebGLRenderer,挂载 canvas 并处理resize
  • 定义 uniforms / onBeforeCompile 或 ShaderMaterial,编写 GLSL 与材质参数
  • 组装 EffectComposer Pass 链,在 animate 中调用composer.render()
  • 创建 OrbitControls(及 Raycaster 等交互控件,若源码包含)
  • requestAnimationFrame循环中更新状态并 render(Cesium 为viewer.render或自动渲染)
  • 代码要点

    import * as THREE from 'three';

    import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; import { BloomPass } from 'three/addons/postprocessing/BloomPass.js'; import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';

    class Base { initThree(el) { this.container = el; this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); this.renderer.setSize(this.container.offsetWidth, this.container.offsetHeight); this.container.appendChild(this.renderer.domElement); this.scene = new THREE.Scene(); this.camera = new THREE.PerspectiveCamera( 45, this.container.offsetWidth / this.container.offsetHeight, 1, 2000 ); this.camera.position.set(0, 10, 50); new OrbitControls(this.camera, this.renderer.domElement); this.animate(); window.addEventListener('resize', this.onResize.bind(this)); } animate() { this.renderer.render(this.scene, this.camera); requestAnimationFrame(this.animate.bind(this)); } onResize() { if (this.container) { this.camera.aspect = this.container.offsetWidth / this.container.offsetHeight; this.camera.updateProjectionMatrix(); this.renderer.setSize(this.container.offsetWidth, this.container.offsetHeight); } } }

    class Road extends Base { constructor() { super(); this.speed = 0.005; } animate() { if (this.materials) { this.materials.forEach((m) => { m.uniforms.uTime.value += this.speed; if (m.uniforms.uTime.value > 1) { m.uniforms.uTime.value = 0; } }) } if (this.composer) { this.renderer.autoClear = false; this.renderer.clear(); this.normalObj.visible = false; this.composer.render(); this.renderer.clearDepth(); this.normalObj.visible = true; } this.renderer.render(this.scene, this.camera); this.threeAnim = requestAnimationFrame(this.animate.bind(this)); } initBloom() { const params = { threshold: 0, strength: 0.5, radius: 0.5, exposure: 0.5 }; const renderScene = new RenderPass(this.scene, this.camera); const bloomPass = new BloomPass(5, 20, 100); bloomPass.threshold = params.threshold; bloomPass.strength = params.strength; bloomPass.radius = params.radius; const composer = new EffectComposer(this.renderer); composer.addPass(renderScene); composer.addPass(bloomPass); composer.addPass(new OutputPass()); this.composer = composer; } createChart(that) { this.initBloom(); const commonUniforms = { uFade: { value: new THREE.Vector2(0, 0.6) }, uOffset: { value: new THREE.Vector2(40, 20) } }; const vertexMoveHeight =float getMove(float u, float offset) { float a = uPI2.0; return sin(a + PI0.25)u * offset; } float getHeight(float u, float offset) { float a = uPI3.0; return cos(a)uoffset; }; const spline = new THREE.LineCurve3( new THREE.Vector3(0, 0, that.height * 0.25), new THREE.Vector3(0, 0, -that.height * 0.75) ); const geometry = new THREE.TubeGeometry(spline, that.height, that.lineWidth, 8, false);

    const vertexShader =float PI = acos(-1.0); uniform vec2 uOffset; varying vec2 vUv; ${vertexMoveHeight} void main(void) { vUv = uv; float m = getMove(uv.x, uOffset.x); float h = getHeight(uv.x, uOffset.y); vec3 newPosition = position; newPosition.x += m; newPosition.y += h; gl_Position = projectionMatrixmodelViewMatrixvec4(newPosition, 1.0); };

    const fragmentShader =varying vec2 vUv; uniform float uSpeed; uniform float uTime; uniform vec2 uFade; uniform vec3 uColor; uniform float uDirection; void main() { vec3 color = uColor; float s = -uTime * uSpeed; float v = (uDirection == 1.0) ? vUv.x : -vUv.x; float d = mod(v + s, 1.0); if (d > uFade.y) discard; else { float alpha = smoothstep(uFade.x, uFade.y, d); if (alpha < 0.0001) discard; gl_FragColor = vec4(color, alpha); } };

    const materials = []; const amount = that.amount; const step = (that.width - that.gap) / amount;

    for (let i = 0; i < amount; i++) { const color = new THREE.Color(); const v = i / amount; color.setHSL( THREE.MathUtils.lerp(that.hueStart, that.hueEnd, v), 1, THREE.MathUtils.lerp(that.lightStart, that.lightEnd, v) );

    const material = new THREE.ShaderMaterial({ side: THREE.DoubleSide, transparent: true, uniforms: { uColor: { value: color }, uTime: { value: THREE.MathUtils.lerp(-1, 1, Math.random()) }, uDirection: { value: i < amount * 0.5 ? 1 : 0 }, uSpeed: { value: THREE.MathUtils.lerp(1, 1.5, Math.random()) }, ...commonUniforms }, vertexShader, fragmentShader });

    materials.push(material);

    const mesh = new THREE.Mesh(geometry, material); mesh.position.x = istep + (i > amount0.5 - 1 ? that.gap : 0); mesh.position.y = Math.random() * 5; this.scene.add(mesh); }

    this.materials = materials;

    const planeGeometry = new THREE.PlaneGeometry( that.width, that.height, that.width * 0.25, that.height * 0.25 );

    const planeMaterial = new THREE.ShaderMaterial({ side: THREE.DoubleSide, transparent: true, uniforms: { uColor: { value: new THREE.Color('blue') }, ...commonUniforms }, vertexShader:float PI = acos(-1.0); uniform vec2 uOffset; ${vertexMoveHeight} void main(void) { float m = getMove(uv.y, uOffset.x); float h = getHeight(uv.y, uOffset.y); vec3 newPosition = position; newPosition.x += m; newPosition.z += h; gl_Position = projectionMatrixmodelViewMatrixvec4(newPosition, 1.0); }, fragmentShader:uniform vec3 uColor; void main() { gl_FragColor = vec4(uColor, 0.6); }});

    this.planeMat = planeMaterial;

    const plane = new THREE.Mesh(planeGeometry, planeMaterial); plane.rotateX(-Math.PI * 0.5); plane.position.set(that.width0.5, -1, -that.height0.25); this.normalObj = plane; this.scene.add(plane); } }

    var road = new Road(); road.initThree(document.getElementById('box')); road.createChart({ lineWidth: 0.5, width: 48, height: 400, gap: 8, amount: 20, hueStart: 0.9, hueEnd: 0.1, lightStart: 0.5, lightEnd: 1.0 });

    完整源码:GitHub

    小结

    • 本文提供道路流光完整 Three.js 源码与在线 Demo,建议先运行案例再改 uniform/参数做二次实验
    • 更多 Three.js 实战案例见 three-cesium-examples 合集 与 GitHub 开源仓库
http://www.jsqmd.com/news/1096972/

相关文章:

  • 3步拯救损坏视频:Untrunc视频修复工具终极指南
  • 新手搭建 OpenClaw 智能代理,系统拦截与路径问题处理办法(含安装包)
  • Destiny 2 Solo Enabler:终极单人游戏解决方案,重新掌控你的游戏体验
  • ArkTS传参
  • ZonyLrcToolsX跨平台歌词下载实战指南:从基础到高级应用
  • Ai token 是什么
  • 如何彻底告别网盘限速:LinkSwift下载助手终极使用指南
  • 别再纠结了!ALAC、FLAC、APE到底选哪个?从兼容性、音质到手机播放,一次讲清楚
  • 2026年6月28日科技热点新闻
  • 酒店行业 Photo ZIP 定向钓鱼攻击与 Node.js 持久植入威胁深度研究
  • 追问“你确定吗”,多数大模型竟“滑跪”,新评测维度呼之欲出!
  • 杰理之麦克风音效流程加入LLNS节点后辅听异常-【篇】
  • 无人机强化学习仿真终极指南:5分钟搭建专业训练环境
  • Fan Control终极指南:Windows免费风扇控制软件完全掌握
  • 大模型思维链(CoT)理论梳理
  • 电路设计实战:电源防反接、光耦与磁耦隔离的选型与应用解析
  • PCB布线禁忌再思考:直角与锐角走线的真实影响与设计权衡
  • 性价比高的免费降英文AI工具效果如何
  • 迈向工业异常检测的全面召回:PatchCore核心思想与实践解析
  • 校易淘实训|Vue3+SpringBoot+MySQL 前后端分离项目从零搭建完整流程 + 全套踩坑解决方案
  • Three.js 简单碰撞检测教程
  • 告别安卓模拟器:3分钟学会在Windows上直接安装APK应用
  • 3分钟掌握Resemble Enhance:终极AI语音降噪增强神器
  • Spring Boot 与 Solon 比较,相互迁移实战指南
  • Cadence Allegro PCB Designer实战:从零到一绘制标准PCB封装
  • qrcode.vue:Vue生态中的专业二维码生成解决方案
  • Parsedown终极指南:3步打造高效Markdown解析工作流
  • 杨洋亮相青岛啤酒“白啤更懂夏的嗨”派对 共赴夏日之约
  • Kazumi番剧播放器:如何通过插件扩展实现全网动漫自由观看
  • 【全网最详细】Sucrose Wallpaper Engine下载免费版 动态桌面壁纸软件安装图解(2026最新)