如何在ThreeJS中用WebGLRenderTarget实现着色器输出递归复用?
ThreeJS着色器输出复用解决方案
问题描述
作为ThreeJS和GLSL新手,需要实现两个核心需求:
- 将一个片段着色器的输出传递给另一个着色器使用
- 同一着色器的下一帧能复用自身前一帧的输出(当前帧渲染依赖前一帧结果)
尝试用WebGLRenderTarget实现时遇到两个问题:
- 直接执行
scene.children[0].material.uniforms.iFire.value = fireLayer.texture触发非法反馈错误,因为WebGL禁止同时将一个纹理作为渲染目标和采样源 - 使用
{...fireLayer.texture}浅拷贝纹理后无内容显示,这种方式无法复制纹理的图像数据
解决方案
核心思路是使用两个WebGLRenderTarget交替读写,避免同一帧内对同一个纹理同时进行读写操作,具体步骤如下:
1. 创建双渲染目标
创建两个规格完全一致的WebGLRenderTarget,用于交替存储上一帧的渲染结果,一个作为当前帧的输入(读),另一个作为当前帧的输出(写)。
2. 交替渲染与赋值
在每一帧的渲染循环中:
- 先将着色器渲染到写目标
- 将读目标的纹理赋值给需要复用的uniform(包括当前着色器自身下一帧的输入)
- 交换两个目标的读写角色,为下一帧做准备
3. 修正uniform共享问题
避免两个着色器共享同一套uniform对象,防止互相干扰,应为每个着色器单独创建uniform(可以复用基础值)。
修改后的完整代码
import * as THREE from "three" var camera, camera_2, fireShader, mainShader, renderer, scene, scene_2 const width = 1200 const height = 720 const texture = new THREE.TextureLoader().load('./images/00_map.png') const initialFire = new THREE.TextureLoader().load('./images/fire_01.png') // 创建两个交替使用的渲染目标 const fireLayer1 = new THREE.WebGLRenderTarget(width, height) const fireLayer2 = new THREE.WebGLRenderTarget(width, height) // 初始化读写目标,初始读目标用初始纹理 let currentReadTarget = { texture: initialFire } let currentWriteTarget = fireLayer1 // 为两个着色器分别创建uniform,避免共享干扰 const mainUniforms = { iTime: { value: 0 }, iResolution: { value: new THREE.Vector3(width, height, 1) }, iTexture: { value: texture}, iFire: { value: initialFire } } const fireUniforms = { iTime: { value: 0 }, iResolution: { value: new THREE.Vector3(width, height, 1) }, iFire: { value: initialFire } // 这里传入上一帧的结果 } const now = new Date() async function init() { const stage = document.getElementById('stage') renderer = new THREE.WebGLRenderer({antialias: true}) scene = new THREE.Scene() camera = new THREE.OrthographicCamera(width / -2, width / 2, height / 2, height / -2, 0.1, 20) camera.position.z = 5 renderer.setSize(width, height) stage.appendChild(renderer.domElement) mainShader = await (await fetch('shaders/frag.glsl')).text() const geometry = new THREE.PlaneGeometry(width, height) const material = new THREE.ShaderMaterial({ uniforms: mainUniforms, fragmentShader: mainShader }) const plane = new THREE.Mesh(geometry, material) scene.add(plane) } async function init_2 () { scene_2 = new THREE.Scene() camera_2 = new THREE.OrthographicCamera(width / -2, width / 2, height / -2, height / -2, 0.1, 20) camera_2.position.z = 5 fireShader = await (await fetch('shaders/fire.glsl')).text() const geometry = new THREE.PlaneGeometry(width, height) const material = new THREE.ShaderMaterial({ uniforms: fireUniforms, fragmentShader: fireShader }) const plane = new THREE.Mesh(geometry, material) scene_2.add(plane) } function render() { const elapsedTime = (new Date() - now) * .001 mainUniforms.iTime.value = elapsedTime fireUniforms.iTime.value = elapsedTime // 1. 将fireShader渲染到当前写目标 renderer.setRenderTarget(currentWriteTarget) // 给fireShader传入上一帧的结果(读目标的纹理) fireUniforms.iFire.value = currentReadTarget.texture renderer.render(scene_2, camera_2) // 2. 将最新的结果传递给mainShader mainUniforms.iFire.value = currentWriteTarget.texture // 3. 交换读写目标,为下一帧准备 const temp = currentReadTarget currentReadTarget = currentWriteTarget currentWriteTarget = temp === fireLayer1 ? fireLayer2 : fireLayer1 // 4. 渲染主场景到屏幕 renderer.setRenderTarget(null) renderer.render(scene, camera) } function animate() { render() requestAnimationFrame(animate) } init() init_2() animate()
关键修改说明
- 双RenderTarget交替:用
fireLayer1和fireLayer2交替存储帧数据,避免同一帧内读写冲突 - 独立uniform对象:为mainShader和fireShader分别创建uniform,防止赋值互相覆盖
- 帧数据传递逻辑:每一帧先把上一帧的结果(读目标)传给fireShader,渲染后将新结果(写目标)传给mainShader,最后交换目标角色
内容的提问来源于stack exchange,提问作者murume
相关产品推荐
相关产品推荐

