Start here
Quick start
A portal in an ordinary three.js scene, with a custom flame next to it, click-to-cast bursts and a glossy floor that reflects the magic.
This is the complete examples/basic/ integration, with commentary. Every step is required except where it says optional.
#Set up the renderer
MagicSystem.render() does its own tone mapping (ACES) and sRGB output, so you don't set toneMapping on the renderer. Turn on shadow maps if you want the glow light to cast shadows.
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { MagicSystem, patchReflectiveFloor } from 'naturegl-magic';
const renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(Math.min(devicePixelRatio, 1.5));
renderer.setSize(innerWidth, innerHeight);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);#Build your scene as usual
NatureGL Magic works in metres. By default the simulation box is 4 × 6 × 4 m with its floor on y = 0, centred on the origin in x and z. Put the effects inside it.
const scene = new THREE.Scene();
scene.add(new THREE.HemisphereLight(0x2a2850, 0x0a080c, 0.6));
const floorMat = new THREE.MeshStandardMaterial({ color: 0x222228, roughness: 0.35 });
patchReflectiveFloor(floorMat); // optional: the magic reflects on the floor
const floor = new THREE.Mesh(new THREE.CircleGeometry(20, 64), floorMat);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
scene.add(floor);
const camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 0.1, 100);
camera.position.set(0, 2.5, 8);
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 2, 0);#Create the system
create() is async. It allocates the fluid fields, adds the sparks and the glow light to your scene, loads the preset and warms up the shaders with renderer.compileAsync.
const magic = await MagicSystem.create({ renderer, scene, camera, quality: 'high', preset: 'portal' });#Compose your own spell (optional)
The preset added a portal. Grab its handle, and add a generic emitter as an orange flame beside it. Colours can be hex strings.
const portal = magic.getEffect('portal');
const flame = magic.addEmitter({
position: [1.3, 0.3, 0.8], velocity: [0, 1.6, 0],
color: '#ff8a3d', radius: 0.2, rate: 12, glow: 6,
});
addEventListener('pointerdown', e => {
const p = new THREE.Vector3((e.clientX / innerWidth) * 3 - 1.5, 3.5 - (e.clientY / innerHeight) * 3, 0);
magic.impulse(p, [0, 1, 0], '#6af0ff'); // one-shot burst of cyan dye, glow and sparks
});#Update, then render
update(dt) does the offscreen work: effects, one fluid step, sparks and the glow-light readback. render() then draws your scene with the magic composited in. It replaces renderer.render(scene, camera).
addEventListener('resize', () => {
renderer.setSize(innerWidth, innerHeight);
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
magic.resize(); // after renderer.setSize
});
const timer = new THREE.Timer();
renderer.setAnimationLoop(t => {
timer.update(t);
const time = timer.getElapsed();
portal.move([Math.sin(time * 0.4) * 0.5, 2.25, 0]); // handles are live
flame.setIntensity(0.7 + 0.3 * Math.sin(time * 3));
controls.update();
magic.update(timer.getDelta());
magic.render();
});spellOrbs preset with an extra vortexRing added by addEffect(), the same setup as examples/cdn/.#Change the magic
magic.loadPreset('dragonBreath'); // params + effects, fields cleared
magic.setParams({ controls: { glow: 1.4, swirl: 0.8 } }); // deep-merge, effects untouched
magic.getEffect('dragonBreath').setColor('#a66bff'); // recolour one effect
magic.impulse([0, 2, 0], [0, 1, 0], '#ffc44a', { radius: 0.6, expansion: 0.8 });
magic.setQualityLevel('ultra'); // rebuilds the solver, clears the fields