Before you read
React Three Fiber (R3F) is a very convenient and powerful threejs react wrapper. It can be used to quickly build rendering scenes using the react framework. For more details about the framework, please refer to the official documentation.
R3F provides EffectComposer and common post-processing effects, such as Bloom, Dither, etc.
However, this cannot meet the needs of highly customized scenes. At this time, you need to use the native interface of three.js.
In the previous use cases, I found that there are relatively few documents and cases in this area, so I decided to write an article to share my experience in using R3F to achieve multi-pass rendering.
Requirements
This article assumes that you have set up the basic R3F environment. If you haven't set it up yet, please refer to the official documentation.
In addition to the basic R3F environment, the postprocessing library from pmndrs will also be used. You can install it with the following command:
# yarn yarn add postprocessing # npm npm install postprocessing
For more information about the postprocessing library, please refer to the official documentation.
Disable R3F RenderLoop
The default rendering loop of R3F does not support custom pipelines very well, so we need to disable it first and establish our own rendering loop to control the rendering process.
<Canvas frameLoop="never"> {/* Scene */} </Canvas>
Note If you have set some effects through <EffectComposer />, doing this may cause the original effects to fail.
It is recommended to re-establish the relevant effects in the subsequent custom rendering loop.
Create EffectComposer
First, we need to create a new EffectComposer instance to manage our rendering process. In my use case,
the Canvas component will be reused in multiple places, so I put the EffectComposer in the scene
component.
// make sure to import the necessary modules from right package import { EffectComposer, RenderPass, ShaderPass } from 'postprocessing' function Scene() { const composerRef = useRef<EffectComposer>() const { gl, scene, camera } = useThree() function initComposer() { // 1. Create a new EffectComposer instance const composer = new EffectComposer(gl) // 2. Usually we want to leverage the existing scene setup inside r3f, // then apply post processing effects on top of that. composer.addPass(new RenderPass(scene, camera)) // 3. here you can add more passes, // e.g. composer.addPass(new ShaderPass(…)) // 4. Save the composer instance to a ref composerRef.current = composer; }; function disposeComposer() { if (composerRef.current) { composerRef.current.dispose() const composer = composerRef.current; for (var i = composer.passes.length - 1; i >= 0; i--) { composer.passes[i].dispose(); } composerRef.current = undefined; } }; useEffect(() => { // 1. initialize and setup the composer on scene && camera change initComposer(); // 2. dispose the composer when the component unmounts return disposeComposer }, [gl, scene, camera]) }
So far, we have created a basic EffectComposer instance, which contains a RenderPass for rendering the scene.
However, the <Canvas /> is still blank because we have not yet rendered the output of the EffectComposer to the screen.
Custom Rendering Loop
This article will introduce two ways to create a custom rendering loop, corresponding to the two frameLoop modes of R3F: demand and auto.
demand - Use react rendering mechanism
function Scene() { const composerRef = useRef<EffectComposer>(); composerRef.current?.render() {...} }
This way, rendering will be triggered every time the dom is updated by react, so that the Canvas can dynamically respond to state updates from react.
auto - Use requestAnimationLoop
function Scene() { const composerRef = useRef<EffectComposer>(); const renderLoopIdRef = useRef<number>(); useEffect(() = { // 1. cancel exisiting render loop if (renderLoopIdRef.current) { cancelAnimationFrame(renderLoopIdRef.current) } function renderLoop() { composerRef.current?.render(); renderLoopIdRef.current = requestAnimationLoop(renderLoop) } // 2. start a new render loop renderLoopIdRef.current = requestAnimationLoop(renderLoop) return () => { cancelAnimationFrame(renderLoopIdRef.current) } }, []) }
Now we have created a basic rendering loop, which will redraw the screen according to the refresh rate recommended by the browser.
References
- pmndrs/postprocessing https://pmndrs.github.io/postprocessing/public/docs/