Creating a Glowing Portal Effect in Three.js with Post‑Processing
This article demonstrates how to use Three.js post‑processing techniques, including EffectComposer, RenderPass, BloomPass, and custom shaders, to build an animated 3D scene featuring a glowing portal and Rick and Morty model, covering shader setup, layer management, animation, and UI controls for fine‑tuning.
This tutorial explains the fundamentals of post‑processing in Three.js and walks through building a creative 3D scene that combines a glowing portal effect with a Rick and Morty model.
Post‑Processing Basics
Post‑processing is performed after the main scene render by using an EffectComposer that chains multiple Pass objects. Each pass can apply effects such as bloom, film grain, or custom shader operations.
Core Setup
First, import the required modules:
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';
import { BloomPass } from 'three/examples/jsm/postprocessing/BloomPass.js';
import { FilmPass } from 'three/examples/jsm/postprocessing/FilmPass.js';Initialize the renderer with renderer.autoClear = false so that post‑processing can render on top of the scene.
Creating the Portal Geometry
A PlaneBufferGeometry is used as the base mesh, and a ShaderMaterial supplies the portal visual:
const portalGeometry = new THREE.PlaneBufferGeometry(8, 8, 1, 1);
const portalMaterial = new THREE.ShaderMaterial({
uniforms: { /* uniforms defined below */ },
fragmentShader: portalFragmentShader,
vertexShader: portalVertexShader
});
const portal = new THREE.Mesh(portalGeometry, portalMaterial);
scene.add(portal);
portal.layers.set(1); // portal on its own layerShader Uniforms
The portal shader receives several textures and color vectors to generate animated noise patterns. Important uniforms include time , perlinnoise , sparknoise , waterturbulence , noiseTex , and five color vectors ( color0 … color5 ), as well as resolution for screen size.
const portalMaterial = new THREE.ShaderMaterial({
uniforms: {
time: { type: 'f', value: 0.0 },
perlinnoise: { type: 't', value: textureLoader.load('/images/perlinnoise.png') },
sparknoise: { type: 't', value: textureLoader.load('/images/sparknoise.png') },
waterturbulence: { type: 't', value: textureLoader.load('/images/waterturbulence.png') },
noiseTex: { type: 't', value: textureLoader.load('/images/noise.png') },
color0: { value: new THREE.Vector3(...options.color0) },
/* color1 … color5 omitted for brevity */
resolution: { value: new THREE.Vector2(sizes.width, sizes.height) }
},
fragmentShader: portalFragmentShader,
vertexShader: portalVertexShader
});Animating the Portal
The updateShaderMaterial function updates the time uniform each frame, while the render loop scales the portal using a cosine function to create a pulsating effect.
function updateShaderMaterial(deltaTime) {
portalMaterial.uniforms.time.value = deltaTime / 5000;
// update color uniforms if needed
}
function tick(deltaTime) {
updateShaderMaterial(deltaTime);
renderer.clear();
camera.layers.set(1);
bloomComposer.render();
renderer.clearDepth();
camera.layers.set(0);
renderer.render(scene, camera);
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);Adding Bloom to the Portal
Only the portal should receive bloom, so a separate EffectComposer with a RenderPass and an UnrealBloomPass is created. The bloom composer’s renderToScreen flag is set to true .
const renderScene = new RenderPass(scene, camera);
const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85);
const bloomComposer = new EffectComposer(renderer);
bloomComposer.renderToScreen = true;
bloomComposer.addPass(renderScene);
bloomComposer.addPass(bloomPass);Model Loading and Layer Separation
The Rick and Morty GLB model is loaded with DRACOLoader and GLTFLoader . The model is placed on layer 0 so it does not receive the bloom effect.
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('/draco/');
const loader = new GLTFLoader();
loader.setDRACOLoader(dracoLoader);
loader.load('/models/rickAndMorty.glb', (gltf) => {
gltf.scene.scale.set(0.02, 0.02, 0.02);
gltf.scene.position.x = -0.5;
gltf.scene.rotation.y = Math.PI;
scene.add(gltf.scene);
gltf.scene.layers.set(0);
});Interactive UI with dat.GUI
A dat.GUI panel lets users tweak bloom strength, radius, and the six portal colors in real time.
const gui = new dat.GUI();
const bloomFolder = gui.addFolder('bloom');
bloomFolder.add(options, 'bloomStrength', 0.0, 5.0).listen();
bloomFolder.add(options, 'bloomRadius', 0.1, 2.0).listen();
const colorsFolder = gui.addFolder('Colors');
colorsFolder.addColor(options, 'color0');
colorsFolder.addColor(options, 'color1');
/* … color5 omitted */Final Touches
A star‑field background image and a simple logo are added via CSS to complete the visual theme. The article ends with an appendix that lists many other built‑in Three.js post‑processing passes (RenderPass, ShaderPass, GlitchPass, OutlinePass, etc.) and their typical parameters.
Conclusion
By combining basic post‑processing pipelines, custom shaders, layer‑based rendering, and UI controls, developers can create sophisticated 3D effects such as a glowing portal that integrates seamlessly with animated models, all within a standard Three.js front‑end project.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.