GLSL / WebGL Minimal

Reveal Mask: WebGL / GLSL Minimal walkthrough

Two photos: drag to reveal the renovated room under the run-down one. This walkthrough explains what the demo is doing, which library outputs it uses, and which parameters matter.
Demo walkthrough

This version keeps the integration small so the moving parts are visible. It uses the WebGL / GLSL path from `three-fluid-fx`, where the effect is built with EffectComposer passes, ShaderMaterial uniforms, and WebGL render targets.

Live demo

Minimal WebGL Examples: Reveal Mask Open example

What this demo teaches

The fluid is not the picture; it is the stencil. The solver writes a screen-space texture every frame — density in one channel, the velocity vector in two more. Where the pointer paints density, the sealed layer gives way to the hidden one underneath; the velocity bends the boundary so the cut looks liquid instead of a hard circle. In this variant, the relevant fluid output is Texture objects: velocityTexture, densityTexture, and dyeTexture when dye is enabled.

  • Read the fluid output as data: density (.b) is a mask, velocity (.rg) is a 2D offset — not something you draw.
  • Composite two cover-fitted layers with mix(base, reveal, mask).
  • Confine the velocity distortion to a thin edge band so the boundary ripples while the revealed interior stays crisp.
  • Get a glowing "wet rim" for free from the same band, and let the reveal heal back via density dissipation.

Implementation path

  1. Render the fluid, then composite fullscreen

    The solver renders into a screen-space density/velocity texture. One fullscreen pass — a FullscreenPass + ShaderMaterial in WebGL, an output node in TSL — reads that texture together with the two image layers and writes the final pixel. No 3D scene is needed.

  2. Cover-fit both layers

    Each image is mapped with a background-size: cover UV so it fills the canvas without stretching; base and reveal can even be different sizes (the full demo lets you upload your own). Pass the image and canvas sizes as uniforms and crop the overflow.

  3. Density → reveal mask

    mask = smoothstep(lo, hi, density.b) turns the painted fluid into a 0..1 reveal amount; mix(base, reveal, mask) blends the sealed and hidden layers. Raising the lower threshold makes the reveal appear later, only under a denser stroke.

  4. Velocity → liquid edge + wet rim

    An edge band — mask·(1−mask), peaking at the boundary — is the trick. Offset the reveal UV by velocity·edge so only the boundary refracts (the interior stays sharp), then add reveal·edge·rim for a bright meniscus that traces the stroke.

  5. Heal-back and resize

    densityDissipation fades the painted density over time, so the reveal slowly closes on its own. On resize, keep the solver size, the view-size uniform and the renderer in sync.

  6. Minimal scope

    It avoids GUI state and preset switching. Read it first when you need the smallest reliable version of the technique.

import { FluidSimulation, FullscreenPass, FULLSCREEN_VERTEX } from 'three-fluid-fx'

const fluid = new FluidSimulation(renderer, { splatRadius: 0.0175, densityDissipation: 0.985 })
attachPointerSplats(renderer.domElement, fluid)

// density.b is the reveal mask, velocity.rg ripples the edge
const composite = new ShaderMaterial({
  vertexShader: FULLSCREEN_VERTEX,
  fragmentShader: /* glsl */ `
    vec4 fl = texture2D(tFluid, vUv);
    float mask = smoothstep(0.06, 0.32, fl.b);
    float edge = mask * (1.0 - mask) * 4.0;          // thin boundary band
    vec3 base = texture2D(tBase, imgUv).rgb;
    vec3 reveal = texture2D(tReveal, imgUv - fl.rg * uEdge * edge).rgb;
    gl_FragColor = vec4(mix(base, reveal, mask) + reveal * edge * uRim, 1.0);
  `,
  uniforms: { tBase, tReveal, tFluid: { value: fluid.densityTexture }, uEdge, uRim },
})
const pass = new FullscreenPass(composite)

Parameters to tune

Parameter What it controls How to tune it
splatRadius Width of the reveal stroke (how much density each pointer move injects). Larger uncovers a bolder swath; smaller paints a fine line.
densityDissipation How long the reveal lingers before it heals back. Lower heals fast (a quick scrub); near 0.99 it stays open much longer.
edge Velocity refraction applied at the reveal boundary. Gentle for a clean cut; higher for a watery, liquid boundary.
rim Brightness of the wet line where liquid meets the sealed area. Raise for a glowing meniscus; set to zero to remove it.
velocityDissipation How long the flow keeps moving after a stroke. Higher keeps it flowing like water; lower settles the reveal into the brush shape.

Source landmarks

Start from examples/glsl/minimal/reveal-mask/main.ts. These are the parts worth reading first:

  • Loading and cover-fitting the two layers (TextureLoader for images, VideoTexture for clips).
  • The smoothstep that turns density.b into the 0..1 mask.
  • The edge band that confines the velocity offset to the boundary and drives the rim.
  • The full demo: the image-pair switcher and Upload controls. In TSL: the shared fluidReveal node and the LinearSRGB output that matches the WebGL look.

Production notes

  • Keep the velocity offset weighted to the edge band, or the whole revealed area smears and becomes unreadable.
  • Frame the two layers identically (same camera and size) so the reveal lines up — for video, render the pair from one animation/turntable.
  • Tune densityDissipation for the feel: low for a wipe-and-restore scrub, high for a reveal that lingers.