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

three.js后处理原理及源码分析

Three.js中的后处理是相对比较难的部分。以我个人学习经历而言,之所以感觉难是因为前期缺乏对其宏观理解,不知道什么是后处理,甚至第一次听说后处理就直接去看后处理的API,看官网提供的案例,不解其原理。初看后处理的Demo感觉就很懵,这代码为什么可以?背后的运行逻辑是什么?出现问题不知道为什么,不知哪一步出的错。直到学了渲染到纹理,看了EffectComposer的源码,看了常用的后处理通道代码,才大概理解后处理的概念。还是那句话:源码之下无秘密。本文就先宏观讲一下我理解的后处理,再来讲Three.js的具体代码,如果觉得有所帮助还请点赞关注一下~~

我所理解的后处理

大部分情况下,我们都是将三维场景直接渲染场景到屏幕。借助WebGLRenderTarget,我们也可以将场景渲染的到一张纹理或者说是一块GPU buffer里面。渲染完成之后,我们可以获取这张纹理,可作为普通的贴图使用,或者将这张贴图进一步处理,增强其视觉特效或渲染质量。在这张图的基础上进一步加工,这种技术就叫后处理。也就是说后处理更像PS技术或者说是图像处理,输入的是图片,输出的也是图片,跟前期的三维场景关系已经不大了,甚至可以完全没有关系。将后处理说出图像处理技术是我自己琢磨的,不合适可讨论。

在Three.js中,后处理的每一次处理称为Pass,经常翻译成通道。那么什么是通道?通道就是处理输入图片的一道工序,输出是另一张图片。如一张图片可经过辉光、模糊、outline、抗锯齿、输出等多道工序。前一道工序的输出是后一道工序的输入。相对于通道,我更喜欢用工序这个词,好理解。

另外,不要认为工序的输入输出都是图形,那么工序的内部就不会涉及三维渲染。是不是涉及是工序内部自己决定。如某道工序内部可渲染另一个场景,并将结果与输入的图片进行整合。

渲染到纹理

渲染到纹理是后处理的基础,所以先看一下这块的API。three.js中渲染到纹理API还是比较简单的,其渲染结果会存储到WebGLRenderTarget目标对象中,通过其属性.texture可以获得渲染结果的RGBA像素数据,也就是一个Three.js的纹理对象THREE.Texture,可以作为材质对象颜色贴图属性map的属性值;下面是API演示代码:

// 1. 创建WebGLRenderTarget对象,可设置宽高、格式、颜色空间、是否有深度buffer、stencilBuffer等
const target = new THREE.WebGLRenderTarget(200, 200, { minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter, format: THREE.RGBFormat });// 2. 设置渲染目标,这样就不会输出到屏幕
renderer.setRenderTarget(target)
// 3. 执行渲染
renderer.render(scene, camera)// 4. 获取纹理
const map = target.texture// 5. 重新设置渲染到屏幕
renderer.setRenderTarget(null)

EffectComposer源码解读

EffectComposer 是 Three.js 中用于实现后期处理效果的核心类,它EffectComposer 是一个渲染通道的管理器,它按照你添加的顺序执行这些通道或工序,每个通道可以对前一通道的输出进行处理,最后一个通道输出到屏幕。我们在开发中一般先创建一个EffectComposer,之后增加各种渲染通道。形成类似以下代码:

// 创建一个EffectComposer
const composer = new EffectComposer( renderer );// 添加后处理通道
const renderPass = new RenderPass( scene, camera );
composer.addPass( renderPass );const glitchPass = new GlitchPass();
composer.addPass( glitchPass );const outputPass = new OutputPass();
composer.addPass( outputPass );// 进行渲染
composer.render();

所以很有必要看一下EffectComposer的源码,印证一下前面所讲的内容。打开three.js源码中examples/jsm/postprocessing/EffectComposer.js文件。先看其构造函数:

class EffectComposer {constructor( renderer, renderTarget ) {this.renderer = renderer;this._pixelRatio = renderer.getPixelRatio();// 默认创建一个WebGLRenderTargetif ( renderTarget === undefined ) {const size = renderer.getSize( new Vector2() );this._width = size.width;this._height = size.height;renderTarget = new WebGLRenderTarget( this._width * this._pixelRatio, this._height * this._pixelRatio, { type: HalfFloatType } );renderTarget.texture.name = 'EffectComposer.rt1';} else {this._width = renderTarget.width;this._height = renderTarget.height;}this.renderTarget1 = renderTarget;this.renderTarget2 = renderTarget.clone();this.renderTarget2.texture.name = 'EffectComposer.rt2';// 创建两个WebGLRenderTargetthis.writeBuffer = this.renderTarget1;this.readBuffer = this.renderTarget2;this.renderToScreen = true;// 存储的渲染通道数组this.passes = [];this.copyPass = new ShaderPass( CopyShader );this.copyPass.material.blending = NoBlending;this.clock = new Clock();}

其构造函数主要是对其字段的初始化,比较重要的是writeBuffer、readBuffer两个RenderTarget对象和passes数组。writeBuffer、readBuffer是渲染通道的输入与输出,有些通道的输入输出都是readBuffer,有些输出是writeBuffer,这需要与Pass中的needsSwap结合使用。passes是存储通道的数组,渲染时会按顺序执行。

再来看一下其render方法,这是其渲染核心方法,会依次调用各pass的render方法进行渲染。

render( deltaTime ) {// deltaTime value is in secondsif ( deltaTime === undefined ) {deltaTime = this.clock.getDelta();}// 保存渲染器当前的render targetconst currentRenderTarget = this.renderer.getRenderTarget();let maskActive = false;// 依次调用各个Pass进行渲染for ( let i = 0, il = this.passes.length; i < il; i ++ ) {const pass = this.passes[ i ];if ( pass.enabled === false ) continue;pass.renderToScreen = ( this.renderToScreen && this.isLastEnabledPass( i ) );// 调用pass的render方法进行渲染pass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime, maskActive );// 是否需要交换writeBuffer、readBuffer// 一些pass的输出是writeBuffer,需要交换// 输出是readBuffer的则不需要交换if ( pass.needsSwap ) {// 特殊逻辑,模版测试相关if ( maskActive ) {const context = this.renderer.getContext();const stencil = this.renderer.state.buffers.stencil;//context.stencilFunc( context.NOTEQUAL, 1, 0xffffffff );stencil.setFunc( context.NOTEQUAL, 1, 0xffffffff );this.copyPass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime );//context.stencilFunc( context.EQUAL, 1, 0xffffffff );stencil.setFunc( context.EQUAL, 1, 0xffffffff );}// 交换writeBuffer、readBufferthis.swapBuffers();}if ( MaskPass !== undefined ) {if ( pass instanceof MaskPass ) {maskActive = true;} else if ( pass instanceof ClearMaskPass ) {maskActive = false;}}}// 恢复render的默认render targetthis.renderer.setRenderTarget( currentRenderTarget );}

看完EffectComposer的源码是不是觉得后处理就是依次调用各pass,每个pass输入输出都是WebGLRenderTarget。这就是后处理的全局流程图景。

常用的通道

学完后处理的整体流程之后就可以关注具体的Pass了。Three.js本身提供了不少Pass,这些Pass代码在postprocessing文件夹下。

常用的Pass有:

  • RenderPass:用于将场景渲染到后处理链中,是大多数后处理流程的起点;
  • ShaderPass: 可以传入自定义着色器,如高斯模糊、色调调节等;
  • BloomPass/UnrealBloomPass:泛光效果;
  • SMAAPass / SSAAPass / FXAAPass:抗锯齿类Pass;
  • SSAOPass:环境光遮蔽,增强阴影与立体感;
  • OutlinePass:物体边缘高亮;
  • OutputPass:输出后处理结果到屏幕,可作为后处理链条的最后一个Pass;

还有很多的Pass,不一一介绍了。使用一个Pass之前最好看一下其源码,理解其作用。这里已OutputPass为例,看看这个Pass如何进行渲染的。

class OutputPass extends Pass {constructor() {super();//const shader = OutputShader;this.uniforms = UniformsUtils.clone( shader.uniforms );// 定义材质this.material = new RawShaderMaterial( {name: shader.name,uniforms: this.uniforms,vertexShader: shader.vertexShader,fragmentShader: shader.fragmentShader} );// 定义了一个可覆盖整个视口的大三角形this.fsQuad = new FullScreenQuad( this.material );// internal cachethis._outputColorSpace = null;this._toneMapping = null;}render( renderer, writeBuffer, readBuffer/*, deltaTime, maskActive */ ) {// 获取传过来纹理,设置到材料上this.uniforms[ 'tDiffuse' ].value = readBuffer.texture;// 设置色调调节的曝光度this.uniforms[ 'toneMappingExposure' ].value = renderer.toneMappingExposure;// rebuild defines if required// 设置颜色空间与色调调节,这些参数最终会传给Shaderif ( this._outputColorSpace !== renderer.outputColorSpace || this._toneMapping !== renderer.toneMapping ) {this._outputColorSpace = renderer.outputColorSpace;this._toneMapping = renderer.toneMapping;this.material.defines = {};if ( ColorManagement.getTransfer( this._outputColorSpace ) === SRGBTransfer ) this.material.defines.SRGB_TRANSFER = '';if ( this._toneMapping === LinearToneMapping ) this.material.defines.LINEAR_TONE_MAPPING = '';else if ( this._toneMapping === ReinhardToneMapping ) this.material.defines.REINHARD_TONE_MAPPING = '';else if ( this._toneMapping === CineonToneMapping ) this.material.defines.CINEON_TONE_MAPPING = '';else if ( this._toneMapping === ACESFilmicToneMapping ) this.material.defines.ACES_FILMIC_TONE_MAPPING = '';else if ( this._toneMapping === AgXToneMapping ) this.material.defines.AGX_TONE_MAPPING = '';else if ( this._toneMapping === NeutralToneMapping ) this.material.defines.NEUTRAL_TONE_MAPPING = '';this.material.needsUpdate = true;}// 一般情况下这里为Trueif ( this.renderToScreen === true ) {renderer.setRenderTarget( null );this.fsQuad.render( renderer );} else {renderer.setRenderTarget( writeBuffer );if ( this.clear ) renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );this.fsQuad.render( renderer );}}
}

从代码可看出,这个Pass工作原理是获取输入的纹理,设置颜色空间与色调调节,将贴图贴到一个大的三角形上渲染输出。这个Pass的shader也很值得研究,包含了颜色空间变换、色调调节的巨量知识。

总结

  1. 后处理是建立在渲染到纹理基础之上,依次对WebGLRenderTarget对象进行再次加工
  2. 每个Pass的作用算法各不相同,使用前最好研究其内部实现才能得心应手

相关文章:

  • VUE3:封装一个评论回复组件
  • Vue基础(7)_计算属性
  • 【mysql】python+agent调用
  • Adobe Lightroom Classic v14.3.0.8 一款专业的数字摄影后期处理软件
  • 【C++QT】Item Views 项目视图控件详解
  • 第二阶段:基础加强阶段总体介绍
  • 全面解析DeepSeek算法细节(2) —— 多令牌预测(Multi Token Prediction)
  • 如何在idea中编写spark程序
  • FDA会议类型总结
  • 排序算法详解笔记(一)
  • 生物化学笔记:神经生物学概论03 脑的高保真数字信号 突触可塑性
  • jquery解决谷歌浏览器自动保存加密密码是乱码
  • 每日一题(12)TSP问题的贪心法求解
  • 深度学习篇---抽样
  • 数据库- JDBC
  • LeetCode 热题 100_最小路径和(92_64_中等_C++)(多维动态规划)
  • React:封装一个评论回复组件
  • 使用JDK的数据校验和Spring的自定义注解校验前端传递参数的两种方法
  • 2025吃鸡变声器软件推荐
  • COMEM光纤温度传感器Optocon:可靠稳定的温度监测方案
  • 以“最美通缉犯”为噱头直播?光明网:违法犯罪不应成网红跳板
  • 韩国下届大选执政党初选4进2结果揭晓,金文洙、韩东勋胜出
  • 葛兰西:“生活就是抵抗”
  • 经济日报金观平:统筹国内经济工作和国际经贸斗争
  • 强政神鸟——故宫里的乌鸦
  • 大家聊中国式现代化|邓智团:践行人民城市理念,开创人民城市建设新局面