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

Three.js 延迟光照教程

延迟光照 ·Deferred Lighting· ▶ 在线运行案例

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

你将学到什么

  • ShaderMaterial 自定义着色器实现核心视觉效果
  • EffectComposer 多 Pass 后期处理管线
  • UnrealBloomPass 辉光 Bloom 效果
  • OrbitControls 相机轨道交互
  • FBXLoader 加载 FBX 城市/角色模型
  • requestAnimationFrame渲染循环与resize自适应

效果说明

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

核心概念

  • 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
  • 异步加载模型 / 3D Tiles / GeoJSON 等资源并加入 scene 或 entities
  • 定义 uniforms / onBeforeCompile 或 ShaderMaterial,编写 GLSL 与材质参数
  • 组装 EffectComposer Pass 链,在 animate 中调用composer.render()
  • 创建 OrbitControls(及 Raycaster 等交互控件,若源码包含)
  • requestAnimationFrame循环中更新状态并 render(Cesium 为viewer.render或自动渲染)
  • 代码要点

    import * as THREE from 'three';

    import Stats from 'three/examples/jsm/libs/stats.module.js'; import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js' 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 { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js'; import { GUI } from "three/addons/libs/lil-gui.module.min.js" const gui=new GUI() const bloomParams = { exposure: 1, bloomStrength: 0.01, bloomThreshold: 0, bloomRadius: 0.5 }; console.log('Three.js 版本:', THREE.REVISION); // 初始化场景、相机、渲染器 const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000); camera.position.set(400, 400, 400); scene.add(camera); const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, logarithmicDepthBuffer: true }); renderer.outputColorSpace = 'srgb' renderer.setSize(window.innerWidth, window.innerHeight); renderer.setClearColor(0x000000); document.body.appendChild(renderer.domElement);

    const ambientLight = new THREE.AmbientLight('#fff', 2); scene.add(ambientLight); // 添加性能监控 const stats = new Stats(); document.body.appendChild(stats.dom); // 初始化控制器 const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true;

    const lightGroup = new THREE.Group(); const geometry = new THREE.PlaneGeometry( 10000, 10000); const material = new THREE.MeshBasicMaterial( {color: 0xcccccc} ); const plane = new THREE.Mesh( geometry, material ); plane.rotation.x = -Math.PI/2; scene.add(plane); // 加载模型 fbx 未使用预览图模型 使用仓库已有的模型,最终效果与外部预览图不一致 new FBXLoader().load(HOST + '/files/model/city.FBX', (object3d) => { object3d.scale.multiplyScalar(0.1) object3d.position.set(0, -1, 0) scene.add(object3d) })

    //后处理管理对象 const postprocessing = {} const numLights = 1000; const width = numLights; // 每行存储 numLights 个光源信息 const height = 2; // 两行 // 创建一个 Float32Array 来存储数据 const data = new Float32Array(widthheight4); // 4 通道 (RGBA) let effectComposer,renderPass,bloomPass const lightTexture = new THREE.DataTexture(data, width, height, THREE.RGBAFormat, THREE.FloatType);

    function updateBloom() { bloomPass.strength = bloomParams.bloomStrength; bloomPass.radius = bloomParams.bloomRadius; bloomPass.threshold = bloomParams.bloomThreshold; }

    const WIDTH = window.innerWidth; const HEIGHT = window.innerHeight; initPostprocessing(WIDTH,HEIGHT) addLight() updateLights() // 动画渲染 function animate() { requestAnimationFrame(animate) updateLights() scene.overrideMaterial = null //写入原场景渲染图 renderer.setRenderTarget(postprocessing.texture1) renderer.render(scene, camera) //将定点数据 法相数据存入通道 scene.overrideMaterial = postprocessing.gBufferPass renderer.setRenderTarget(postprocessing.gBuffer) renderer.render(scene, camera) renderer.setRenderTarget(null) renderer.render(postprocessing.scene, postprocessing.camera); effectComposer.render() stats.update() controls.update(); }

    animate();

    function initPostprocessing(renderTargetWidth, renderTargetHeight) { postprocessing.scene = new THREE.Scene(); postprocessing.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); postprocessing.scene.add(postprocessing.camera); postprocessing.texture1 = new THREE.WebGLRenderTarget(renderTargetWidth, renderTargetHeight, { format: THREE.RGBAFormat, type: THREE.FloatType, colorSpace: THREE.SRGBColorSpace, depthBuffer: true, stencilBuffer: false }) postprocessing.gBuffer = new THREE.WebGLRenderTarget(renderTargetWidth, renderTargetHeight, { format: THREE.RGBAFormat, // 使用 RGBAFormat 确保有 alpha 通道 type: THREE.FloatType, // 使用 FloatType 以确保存储精度 depthBuffer: true, // 确保有深度缓冲 count: 2 })

    // G-BUFFER 管线 postprocessing.gBufferPass = new THREE.ShaderMaterial({ vertexShader:out vec3 vNormal; out vec3 vWorldPosition; void main() { vNormal = normal; // 计算顶点的世界坐标,模型矩阵将顶点从模型空间转换到世界空间 vec4 worldPosition = modelMatrix * vec4(position, 1.0); vWorldPosition = worldPosition.xyz; gl_Position = projectionMatrixviewMatrixworldPosition; }, fragmentShader:in vec3 vNormal; in vec3 vWorldPosition; layout(location = 0) out vec4 gPosition; layout(location = 1) out vec4 gNormal; void main() { gPosition = vec4(vWorldPosition, 1.0); gNormal = normalize(vec4(vNormal, 1.0)); }, glslVersion: '300 es', })

    postprocessing.lightMaterial = new THREE.ShaderMaterial({ defines: { EMISSIVE: 10, }, vertexShader:out vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrixviewMatrixmodelMatrix * vec4(position, 1.0); }, fragmentShader:precision highp float; precision highp int; // 从 G-buffer 中读取的位置、法线和颜色纹理 uniform sampler2D tPosition; uniform sampler2D tNormal; uniform sampler2D tDiffuse; uniform sampler2D tLightData; uniform vec2 resolution; uniform int offset; // 输入 UV 坐标 in vec2 vUv; // 输出最终颜色 out vec4 pc_FragColor; const int MAX_LIGHTS_PER_PASS = 50; float maxDistance=100.0; float smoothFactor=300.0; void main() { vec3 diffuse = texture(tDiffuse, vUv).rgb; vec3 normal = texture(tNormal, vUv).rbg; vec3 position = texture(tPosition, vUv).rgb; vec3 resultColor = vec3(0.0); int numLights = int(resolution.x); for (int i = 0; i < numLights; i++) { vec2 uvPosition = vec2(float(i) / resolution.x, 0.0); vec4 positionData = texture2D(tLightData, uvPosition); vec3 lightPosition = positionData.xyz; vec3 lightDir = normalize(lightPosition - position); // 计算法线与光照方向的点积 float NdotL = max(dot(normal, lightDir), 0.0); // 如果 NdotL <= 0.0,跳过该光源 if (NdotL <= 0.0) { continue; } vec2 uvColorIntensity = vec2(float(i) / resolution.x, 1.0 / resolution.y); vec4 colorIntensityData = texture2D(tLightData, uvColorIntensity); vec3 lightColor = colorIntensityData.rgb; float distance = length(lightPosition - position); // 使用平滑衰减函数,避免硬性阈值判断 float attenuation = smoothstep(maxDistance, maxDistance - smoothFactor, distance); // 通过衰减因子对光照强度进行调整 float intensity = colorIntensityData.a * attenuation; if (intensity > 0.0) { // 计算光照贡献 vec3 lightContribution = lightColordiffuseNdotL * intensity; resultColor += lightContribution; } } pc_FragColor = vec4(resultColor+diffuse , 1.0); }, glslVersion: '300 es', uniforms: { tPosition: {value: postprocessing.gBuffer.textures[0]}, tNormal: {value: postprocessing.gBuffer.textures[1]}, tDiffuse: {value: postprocessing.texture1.texture}, tLightData: {value: lightTexture}, resolution: {value: new THREE.Vector2(width, height)}, offset: {value: 0} }, }) postprocessing.quad = new THREE.Mesh( new THREE.PlaneGeometry(2.0, 2.0), postprocessing.lightMaterial ); postprocessing.scene.add(postprocessing.quad);

    effectComposer=new EffectComposer(renderer) renderPass=new RenderPass(postprocessing.scene,postprocessing.camera)

    effectComposer.addPass(renderPass) // 创建泛光效果 bloomPass = new UnrealBloomPass( new THREE.Vector2(renderTargetWidth, renderTargetHeight), bloomParams.bloomStrength, bloomParams.bloomRadius, bloomParams.bloomThreshold ); effectComposer.addPass(bloomPass);

    // 添加GUI控制 const bloomFolder = gui.addFolder('Bloom Effect'); bloomFolder.add(bloomParams, 'bloomStrength', 0, 1).name('强度').onChange(updateBloom); bloomFolder.add(bloomParams, 'bloomRadius', 0, 1).name('半径').onChange(updateBloom); bloomFolder.add(bloomParams, 'bloomThreshold', 0, 1).name('阈值').onChange(updateBloom); bloomFolder.open();

    }

    function addLight() { for (let i = 0; i < numLights; i++) { const randomColor = Math.floor(Math.random() * 16777215); const light = new THREE.PointLight(randomColor, 50); light.userData.initialPosition = { x: Math.random() * (1000 - -1000) + -1000, y: Math.random() * 200 + 10, z: Math.random() * (1000 - -1000) + -1000 }; light.userData.movement = { xSpeed: Math.random() * 2 - 1, ySpeed: Math.random() * 2 - 1, zSpeed: Math.random() * 2 - 1, xFrequency: Math.random() * 2 + 1, yFrequency: Math.random() * 2 + 1, zFrequency: Math.random() * 2 + 1 }; light.position.set(light.userData.initialPosition.x, light.userData.initialPosition.y, light.userData.initialPosition.z); lightGroup.add(light); } }

    function updateLights() { const time = Date.now() * 0.001; // 时间因子,控制速度 lightGroup.children.forEach((light, i) => { if (light instanceof THREE.PointLight) { const {initialPosition, movement} = light.userData; light.position.x = initialPosition.x + Math.sin(timemovement.xFrequency)movement.xSpeed * 50; light.position.y = initialPosition.y + Math.sin(timemovement.yFrequency)movement.ySpeed * 50; light.position.z = initialPosition.z + Math.sin(timemovement.zFrequency)movement.zSpeed * 50;

    // 填充第一行的位置信息 data[i * 4 + 0] = light.position.x // x data[i * 4 + 1] = light.position.y // y data[i * 4 + 2] = light.position.z // z data[i * 4 + 3] = 0.0; // 占位

    // 填充第二行的颜色和强度信息 data[(width4) + i4 + 0] = light.color.r; // r data[(width4) + i4 + 1] = light.color.g; // g data[(width4) + i4 + 2] = light.color.b // b data[(width4) + i4 + 3] = light.intensity; // intensity } }) lightTexture.needsUpdate = true; } // 窗口大小调整 window.addEventListener('resize', onWindowResize, false); function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); composer.setSize(window.innerWidth, window.innerHeight); }

    完整源码:GitHub

    小结

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

相关文章:

  • OpenCV端侧处理效率提升系列(二): 硬件加速工具(GPU,NPU)
  • 低成本高精度6DOF运动追踪系统设计与实现
  • 2026护栏厂家采购干货:锌钢、边坡、球场防护工程厂家甄选指南
  • 文件改名一个个改太麻烦?五款批量重命名工具实操记录
  • 终极指南:如何用Python命令行工具完美下载网易云音乐无损资源
  • AI验布机选择指南:五个核心指标比价格更重要
  • 微信聊天记录误删怎么办?官方完整恢复教程整理
  • 工业视觉检测中的漫射照明技术
  • 电站机组振动传感器DP-VB-400
  • 从成年到新生,覆盖犬皮肤研究的“年龄维度”:云克隆犬角质形成细胞(AEK NEK)全系列上线
  • Docker 学习笔记(三):Docker 网络、bridge、子网和容器互通
  • 3分钟掌握NCM解密:终极免费工具实现音乐格式自由转换
  • Go 语言 fmt 与 log 打印方式详解
  • 【观止·诗史汇 HarmonyOS 实战系列 07】兴替明鉴:四维总览与六类分析的朝代洞察模型
  • Java计算机毕设之基于 SpringBoot 的加密云端日记本管理系统的设计与实现 基于 SpringBoot 的在线日记归档查询系统(完整前后端代码+说明文档+LW,调试定制等)
  • Unity 外部资源加载器(图片+视频+音频+文本)
  • Hermes精装攻略|从毛坯到全能助手。工欲善其事,必先利其器
  • 浏览器资源嗅探终极指南:猫抓Cat-Catch完整教程与高效使用技巧
  • NS-USBLoader完整指南:一站式Switch文件管理解决方案
  • 【解决问题】关于firstVMware Workstation Profirstsf1虚拟机开机后不能打开的解决方案
  • AssetStudio深度实战:高效提取Unity游戏资源的开源解决方案
  • 大家都在用什么材料进行3D打印?210位用户给出答案
  • buildroot , overlay 配置
  • 国常会定调AI:智算集群与“人工智能+“对企业落地的实质影响
  • 如何一键自动化安装激活Office?LKY Office Tools终极指南
  • Ryujinx实战手册:在PC上解锁Switch游戏体验的五大核心技巧
  • MAA明日方舟助手:3大核心功能彻底解放你的游戏时间
  • 【爱马仕智能体】Hermes 客户端运行故障排查,部署加载失败、程序无响应处理办法(含安装包)
  • 如何在5分钟内完成Office全自动安装?LKY Office Tools终极指南
  • 终极指南:三分钟解决Windows DLL注入难题的完整方案