NatureGL Magicv1.0.0

Guides

Ink tanks and force fields

The solver has two kinds of solid boundary besides its floor. A container is a box the fluid can't leave, which the ink effect uses as a water tank. An obstacle is a moving sphere the fluid must flow around, which the force field uses to part smoke.

#Ink in water

ink: a drop every 0.95 s, cycling through five colours. The drops sink at 2.4 m/s and billow as they slow, lit by a bright white key light.
js
const [ink] = magic.loadPreset('ink');   // physics for water: weight, no buoyancy
ink.drop();                              // random spot under the surface, next colour
ink.drop([0.3, 2.2, 0], '#ff4f8e');      // your spot and colour
ink.move([0.3, 1.31, 0]);                // moves the whole tank (position = tank centre)

The ink effect does two things:

  • It sets the frame's container to container { min, max }. The fluid can't leave that box, and max.y is the water surface.
  • Every interval seconds it calls drop(). A drop is an impulse() pushed down at drop.sink with no glow, no heat and no sparks.

Automatic drops land at a random spot within spread [0.95, 0.4] (half-extents in x and z around the tank centre), depth 0.12 m below the surface. Every drop, yours included, is clamped to at least 0.3 m from the side walls.

OptionDefault
container{ min: [-1.5, 0.12, -0.95], max: [1.5, 2.5, 0.95] }The tank interior, world space
interval0.95Seconds between automatic drops (0 = manual only)
colorsrose, blue, gold, violet, greenCycled drop by drop
spread, depth[0.95, 0.4], 0.12Where automatic drops land
drop{ radius: 0.2, density: 60, glow: 0, sink: 2.4, duration: 0.22, expansion: 0.05 }Passed to impulse() for each drop

#Why it looks like ink

The effect on its own only drops dye. The ink preset supplies the rest:

SettingPresetDefaultWhy
controls.buoyancy01Dye doesn't rise
physics.weight1.150Dense dye sinks
physics.dissipation0.020.85Ink doesn't fade
render.colorAbsorption10.35Dye absorbs its complement, so red ink looks red against the light
render.densityPower1.351Sharper edges on the billows
lighting.keyColor[2.6, 2.55, 2.45][0.16, 0.18, 0.3]Bright white light on the dye instead of dim moonlight
post.bloom0.080.5Almost none: ink doesn't glow

#Force fields

forceField: three vents (rose, cyan, gold) whose plumes part around the energy sphere as it wanders.
js
const field = magic.addEffect('forceField', { position: [0, 2.4, 0], radius: 0.5 });
field.move([0.8, 2, 0]);       // pauses the wander path for wander.holdTime (2.5 s)
field.velocity;                // measured from its motion, clamped to 6 m/s
field.pickTarget;              // the shell mesh, for raycast dragging

A force field is the frame's obstacle: a solid sphere of radius the fluid must flow around. The field's own velocity is written into the cells it covers, so moving it drags the smoke along with it. A radial push (5 m/s²) acts in a shell out to 2.3 × the radius, so plumes part before they touch it.

When nobody moves it, the field follows a wander path: a Lissajous curve around wander.center with per-axis amplitude and speed. It eases toward the path at follow. Each move() pauses the wander for holdTime seconds, after which it drifts back.

The visual is a fresnel shell with a moving interference pattern and a bright core sphere, added to your scene as MagicForceField. It draws additively and leaves destination alpha alone. Set visual: null for no mesh; the obstacle still works.

OptionDefault
position, radius[0, 2.4, 0], 0.5
push5Radial push (m/s²), scaled by the swirl control and the intensity
wander{ enabled: true, center: [0, 2.4, 0], amplitude: [1.2, 0.5, 0.3], speed: [0.55, 0.8, 0.37], follow: 2, holdTime: 2.5 }Idle path
visual{ color: [0.6, 0.8, 1.6], core: [1.2, 1.6, 3.0] }Linear HDR colours; setColor() tints the shell

#Drag it with the mouse

The demo raycasts against pickTarget and calls move() while you drag. The wander pauses as long as you keep moving it.

js
const hit = raycaster.intersectObject(field.pickTarget, false)[0];
if (hit) dragging = true;
// on pointermove, with a plane through the hit point:
field.move(pointOnPlane);