7.2 后期处理链:成品前的最后一道抛光


文档摘要

7.2 后期处理链:成品前的最后一道抛光 本节摘要:后期处理把"渲染结果直接上屏"改成"先流经一串效果器再上屏":泛光、景深、色调映射逐层加工。本节搭一条最小可用的处理链,讲清泛光的暗部纪律与分辨率代价,并预留一键降级开关——抛光工段的每一步都按整屏收费。 从直接上屏到流水抛光 前六章的所有渲染调用,最终都是 renderer.render 把画面直接画上屏。后期处理改掉这个终点:画面先画进一张离屏缓冲,再逐个流过效果器——每个效果器读入上一级的画面、写出到下一级,最后一站才上屏。这条链的专业说法叫 EffectComposer,效果器叫 Pass。 它解决的是"整幅画面级别的效果":泛光要把整屏找亮部晕开、景深要按整屏的深度做模糊、色调映射要按整屏的明暗分布调曲线。

7.2 后期处理链:成品前的最后一道抛光

本节摘要:后期处理把"渲染结果直接上屏"改成"先流经一串效果器再上屏":泛光、景深、色调映射逐层加工。本节搭一条最小可用的处理链,讲清泛光的暗部纪律与分辨率代价,并预留一键降级开关——抛光工段的每一步都按整屏收费。

从直接上屏到流水抛光

前六章的所有渲染调用,最终都是 renderer.render 把画面直接画上屏。后期处理改掉这个终点:画面先画进一张离屏缓冲,再逐个流过效果器——每个效果器读入上一级的画面、写出到下一级,最后一站才上屏。这条链的专业说法叫 EffectComposer,效果器叫 Pass。

它解决的是"整幅画面级别的效果":泛光要把整屏找亮部晕开、景深要按整屏的深度做模糊、色调映射要按整屏的明暗分布调曲线。这些效果的共性是"单像素不知道答案"——泛光的强度取决于邻域有多亮,必须先有整幅画面才能算。代价同样直白:每多一个效果器,整幅画面多一次读写,1080P 下就是每帧多搬运几百 MB 数据。

图:后期处理链的流水线与降级开关

图:后期处理链的流水线与降级开关

动手:给沙盘加泛光并留退路

背景:货架的呼吸灯(6.2)与叉车顶灯在演示屏上不够"亮眼",客户点名要"灯自己发光晕开"的效果。要求高端机全开、低端机自动关。

操作:只让发光物件过泛光阈值,链上就三站;按帧率探针留降级开关。

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 { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; const composer = new EffectComposer(stage.renderer); composer.addPass(new RenderPass(stage.scene, stage.camera)); // 起点:照常渲染 const bloom = new UnrealBloomPass( new THREE.Vector2(innerWidth, innerHeight), 0.6, // strength:晕开的力度,0.4 到 0.8 是克制区 0.4, // radius:晕开的范围 0.85 // threshold:亮度门槛,0.85 意味着只有真亮的东西晕 ); composer.addPass(bloom); composer.addPass(new OutputPass()); // 终点:色调映射上屏 // 呼吸灯材质:emissiveIntensity 峰值推过阈值,泛光自然只认它 stage.loop((dt, elapsed) => { lampMat.emissiveIntensity = 1.0 + glow * 1.4; // 峰值 2.4,远超 0.85 门槛 composer.render(); // 用 composer 代替 renderer.render }); // 降级开关:帧率持续不足时弃链直渲 let lowFrames = 0; stage.renderer.domElement.addEventListener('bp', () => {}); // 探针事件占位 setInterval(() => { const fps = currentFps(); // 假设已有帧率统计(第 8 章给实现) lowFrames = fps < 30 ? lowFrames + 1 : 0; if (lowFrames > 3) useComposer = false; // 连续低帧,弃链 }, 1000); // 渲染分支:useComposer ? composer.render() : stage.renderer.render(scene, camera)

结果:呼吸灯峰值时灯罩周围晕开一圈柔光,普通漆面纹丝不晕——阈值把泛光的戏份锁死在发光件上。低配机上探针触发降级后,画面只失去那圈光晕,其余分毫不差。

解读:泛光是后期链的第一课也是纪律课。threshold 拉低到 0.5 以下,白墙、浅色地面全部过阈,画面变成一片浆糊——泛光只该给"设计上就该发光"的东西,把发光件的 emissiveIntensity 推高、把门槛抬到 0.85,让画面自己选出主角。链的顺序也是纪律:RenderPass 必须在最前(没有它链上无画面可加工),OutputPass 必须在最后(它负责把线性色彩正确转回屏幕色彩,少了它画面发灰——和 3.2 的色彩空间是同一族问题)。分辨率账再报一次:每个 Pass 读写整屏,泛光内部还有多级降采样,低端机的账要在加链前先算。

变式:需要景深与暗角的页面,Pass 直接追加在 bloom 之后即可,顺序即画质。更省的替代思路:泛光效果用"预烘光晕贴图"实现——给灯罩叠一张半透明光晕面片,视觉近似、零后处理开销,是移动端页面常用的"伪泛光"。降级不止一档:bloom 的 resolution 减半也能救回一截帧率,不必直接整链关闭。

上链的隐形代价:抗锯齿

走后期链还有一个不显眼的损失:WebGLRenderer 的抗锯齿参数只在直接上屏时生效,画面一进离屏缓冲,多重采样就失效了——上链之后边缘锯齿往往"莫名回来"。处置有两条路:链上追加一条快速近似抗锯齿的效果器(开销远低于多重采样),或把 composer 的渲染目标换成带多重采样的缓冲(新版本支持,代价是显存)。展示类页面几乎必然上链,所以这条"隐形税"要提前计入预算:链的起步成本不止效果器本身,还有抗锯齿的补课。国产场景里还有一个反直觉现象值得记录:泛光开得太强时,暗部会被整体抬亮,画面"发灰发雾"——这不是色彩空间问题,是泛光把阈值以下的亮度也晕了出去,回到 threshold 纪律即可治愈。

本节要点回顾

  • 后期链是整屏税:每站读写整幅画面,效果器数量乘分辨率即搬运量;
  • 三站最小链:RenderPass 起点、效果器居中、OutputPass 收尾,顺序即画质;
  • 泛光认阈值不认全屏:门槛抬高、只让发光件过阈,白墙绝不晕光;
  • 缺 OutputPass 画面发灰:线性到屏幕的色彩转换靠它,与色彩空间同族问题;
  • 降级开关必须预埋:帧率探针切回直渲,加效果前先想退路。

画面已抛光。下一节批量喷洒:一万颗灰尘、一场金粉雨,靠的是"一颗粒子一个顶点"的批量手艺,顺带解锁万级同款小件的通用答案。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U