news 2026/7/1 17:38:28

Three.js 延迟光照教程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 开源仓库
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/1 17:38:20

OpenCV端侧处理效率提升系列(二): 硬件加速工具(GPU,NPU)

OpenCV 硬件加速模块完整详解及使用1. OpenCL 模块 cv::ocl&#xff08;跨平台通用 GPU 加速&#xff09;核心关系OpenCV 内置可选子模块&#xff0c;编译需开启 WITH_OPENCLON&#xff1b;适配硬件&#xff1a;Intel 核显、AMD 独显、NVIDIA 全系列、RK / 瑞芯微 NPU、手机 GP…

作者头像 李华
网站建设 2026/7/1 17:38:18

低成本高精度6DOF运动追踪系统设计与实现

1. 项目背景与核心需求在工业自动化、无人机导航和VR/AR设备开发中&#xff0c;精确的6DOF&#xff08;六自由度&#xff09;运动追踪一直是核心技术痛点。传统方案要么成本高昂&#xff08;如光学动捕系统&#xff09;&#xff0c;要么精度不足&#xff08;如消费级IMU模块&am…

作者头像 李华
网站建设 2026/7/1 17:38:11

2026护栏厂家采购干货:锌钢、边坡、球场防护工程厂家甄选指南

基建防护、生态治理、体育场地建设等工程的稳步推进&#xff0c;让各类护栏产品的市场需求持续攀升。锌钢护栏、边坡防护网、球场护栏作为通用性极强的防护建材&#xff0c;广泛应用于市政交通、水利工程、校园场馆、乡村建设等多个场景&#xff0c;产品的材质工艺、适配性、耐…

作者头像 李华
网站建设 2026/7/1 17:34:43

文件改名一个个改太麻烦?五款批量重命名工具实操记录

日常整理图片、文档、素材资料时&#xff0c;各类批量改名软件的命名规则丰富度、批量处理稳定性、附加功能各有差别。下面客观整理五款常用批量重命名工具的功能特点与短板&#xff0c;仅为个人实操记录&#xff0c;无测评、引流推广相关导向。一、鲲穹批量重命名这款批量改名…

作者头像 李华
网站建设 2026/7/1 17:32:29

AI验布机选择指南:五个核心指标比价格更重要

在纺织行业数字化转型的浪潮中&#xff0c;越来越多的企业开始关注AI验布机。然而&#xff0c;面对市场上快速涌现的品牌和型号&#xff0c;如何避开营销话术、找到真正适合自己工厂的方案&#xff0c;成为许多管理者的困扰。本文梳理了选购AI验布机时应该重点关注的五个核心指…

作者头像 李华