Search code examples
javascriptthree.jsglsl

Recursively use a WebGLRenderTarget using threejs and GLSL


I am new to both ThreeJS and GLSL. I am trying to reuse the output of one fragment shader in another, and also use that same output in the same shader in the next frame. In other words, the output of the shader depends on the output of the same shader in the previous frame. I am trying to use WebGLRenderTarget to achieve that.

Here is my code:

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')
var fire = new THREE.TextureLoader().load('./images/fire_01.png')

const fireLayer = new THREE.WebGLRenderTarget(width, height)
const uniforms = {
    iTime: { value: 0 },
    iResolution: { value: new THREE.Vector3(width, height, 1) },
    iTexture: { value: texture},
    iFire: { value: fire }
}
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,
        fragmentShader: mainShader
    })
    material.needsUpdate = true

    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,
        fragmentShader: fireShader
    })
    material.needsUpdate = true
    const plane = new THREE.Mesh(geometry, material)

    scene_2.add(plane)   
}

function render() {
    uniforms.iTime.value = (new Date() - now) * .001
    renderer.setRenderTarget(fireLayer)
    renderer.render(scene_2, camera_2)
    if (scene.children[0])
        scene.children[0].material.uniforms.iFire.value = fireLayer.texture
    renderer.setRenderTarget(null)
    renderer.render(scene, camera)
}

function animate() {    
    render()
    requestAnimationFrame(animate)
}

init()
init_2()
animate()

Updating the texture as shown above ie scene.children[0].material.uniforms.iFire.value = fireLayer.texture results in an illegal feedback error. When I try updating the texture using {...fireLayer.texture} nothing gets displayed. I'm suspecting that it's because copying like that does not copy the image.

How do I correctly copy the output of a fragment shader and use it in the same shader that produced it?


Solution

  • If you do try to read and write to the same image, you get undefined results and an illegal feedback error. You have to use a FramebufferTexture. Copy the output of the shader into the framebuffer texture and use this texture as input for the next frame:

    const fireLayerFbCopy = new THREE.FramebufferTexture(width, height)
    const fireLayer = new THREE.WebGLRenderTarget(width, height)
    
    function render() {
        uniforms.iTime.value = (new Date() - now) * .001
        renderer.setRenderTarget(fireLayer)
        renderer.render(scene_2, camera_2)
        if (scene.children[0])
            scene.children[0].material.uniforms.iFire.value = fireLayerFbCopy
        renderer.copyFramebufferToTexture(new THREE.Vector2(), fireLayerFbCopy);
        renderer.setRenderTarget(null)
        renderer.render(scene, camera)
    }