Post-Processing Effects

After the stroke is laid down, flow fields, diffusion, and ripples take over — keeping the ink alive

Contents

  1. Seasoning after you finish drawing
  2. Flow field: invisible wind moving your drawing
  3. Flow’s eight blend types
  4. FBM distort: viewing your drawing through water
  5. Resonance Scatter: 16 stones in a pond
  6. Cellular / Voronoi: cells under a microscope
  7. Metallic: turn ink into liquid metal
  8. White Dot & Film Grain: paper imperfections
  9. Combining effects
1

Seasoning after you finish drawing

Imagine finishing a dish—ingredients and cooking are done. Then you add salt, pepper, lemon, soy sauce. Those "seasonings" don’t change the food itself but change the flavor. InkField’s post effects are the seasoning after you finish drawing.

All effects operate on the "already drawn" image; they don’t change strokes—only how they are seen. Main effects come from two shaders:

🌊

flow.frag

Simplex displacement, blend types, edge halftone

🌀

distort.frag

FBM distort, resonance scatter, cellular, white dots, film grain

metallic.frag

Reflection, Fresnel, diamond dispersion, black-gold texture

📖 Further reading: inkField's distortion effects originate from the earlier work Equinox—pulling and stretching buffers to blend two seasons into a new state was the core technique, later adapted and refined for inkField

2

Flow field: invisible wind moving your drawing

Imagine drawing with ink on paper, then an invisible wind blows from all directions—ink moves, twists, bleeds. Different places have different wind direction and speed. That’s the Flow field.

flow.frag works by sampling from a different position:

// Basic noise displacement crd2 += simpleN2(coord * 0.001) * px * blendVol; crd2 += simpleN2(coord * 0.005) * px * blendVol / 2.0; vec4 sample1 = texture2D(tex0, crd2); vec4 finalColor = min(sample0, sample1);

Why min()?

min(original, displaced) means: if the displaced position is darker (has ink), that ink is "pulled" in. Ink spreads outward but bright areas don’t overwrite dark—like real ink on paper.

What is Simplex Noise?

A function: input a coordinate, output a "natural-looking" random value. Neighboring points are similar; farther points differ—like terrain: nearby elevation is similar, over the hill it changes.

In flow.frag, simpleN2() returns a 2D vector (x, y offset)—the direction each pixel is "blown."

3

Flow’s eight blend types

Besides basic noise, flow.frag has eight blendType modes:

blendTypeNameEffect
0BasicNoise displacement + globalStyle strength zones
2ConcentricTwo random centers create radial ripples; uses mix() to override base noise—ripples dominate near centers, organic noise preserved at edges
3Verticalsin/cos/tan layers; vertical texture
4HorizontalSame, 90° rotated; horizontal texture
5Crack PatternDual-layer Voronoi/cellular noise; strong displacement at cell boundaries creates crack/fracture texture, base noise preserved inside cells
6MosaicCanvas split into random-sized tiles, each with independent random offset—like broken tiles shifting apart, sharp grid boundaries
7VortexTwo vortex centers with polar rotation from original coords, Gaussian falloff—vortex dominates near centers, transitions to noise farther out
8CellularVoronoi + Simplex perturbation

Edge halftone

All Flow modes use halftone dithering near edges: the closer to the edge, the more pixels are "skipped," giving a scattered, ink-splash look instead of a smooth fade.

White brush protection (TypeMapBuffer)

White brush identity is now managed by an independent typeMapBuffer (the old G=R×0.5 tag system has been removed). Flow runs a separate sync pass (isTypeMapMode=1) using nearest-neighbor sampling to avoid interpolation blurring the identity:

// typeMap pass: nearest-neighbor sampling, conservative propagation vec2 nearestCrd = (floor(crd * rect.zw) + 0.5) / rect.zw; vec2 nearestCrd2 = (floor(crd2 * rect.zw) + 0.5) / rect.zw; vec4 type0 = texture2D(tex0, nearestCrd); vec4 type1 = texture2D(tex0, nearestCrd2); // Match color flow's min() semantics: darker ink dominates gl_FragColor = (type1.r < type0.r) ? type1 : type0;

See Bug Stories for the full TypeMapBuffer architecture.

4

FBM distort: viewing your drawing through water

FBM distort — refraction through water
Imagine your drawing under water, viewed from above. Water waves refract the image—that’s FBM distort. FBM = Fractal Brownian Motion: multiple layers of noise added together.

distort.frag builds FBM in layers:

float fbm4(vec2 p) { float f = 0.0; f += 0.5000 * noise(p); // Layer 1: mountains p = m * p * 2.02; f += 0.2500 * noise(p); // Layer 2: hills p = m * p * 2.02; f += 0.1250 * noise(p); // Layer 3: slopes p = m * p * 2.02; f += 0.0625 * noise(p); // Layer 4: gravel detail return f; }
Like viewing a mountain—from far you see one outline (layer 1), closer reveals mid-size hills (layer 2), then slopes (layer 3), then gravel and grass (layer 4). Stacked together they produce nature's fractal "the closer you look, the more detail" quality.

func(): FBM of FBM

The core func() goes further—it uses one FBM's result to offset another FBM, then uses that to offset a third. Like using a mountain's shape to warp another mountain, then warping a third. The result is extremely natural and organic.

vec2 o = fbm4_2(0.9 * q); // First FBM → offset vector vec2 n = fbm6_2(3.0 * o); // Use offset to sample second FBM float f = fbm4(1.8 * q + 6.0 * n); // Use that to offset a third

How distort uses FBM

FBM value acts as a mask (fbmMask) controlling displacement strength:

  • High fbmMask → strong displacement → visible distortion
  • Low fbmMask → weak displacement → almost unchanged

Additionally, hue shifts slightly with field direction in dark areas, adding organic feel.

5

Resonance Scatter: 16 stones in a pond

Resonance scatter — overlapping ripples
A calm pond; you drop 16 stones at once. Each creates ripples; they overlap—peaks meet (constructive) or cancel (destructive). That’s Resonance Scatter.

16 wave sources

Sixteen points in a 4×4 grid (slightly offset by force map). Each pixel’s distance to these points drives a sine wave:

for(int i = 0; i < 16; i++) { vec2 pt = getResonancePoint(i); // Wave source position float weight = getPointWeight(i); // Weight (stronger force → more important) float L = length(pos - pt); // Distance to source C += sin(TAU * f * (t - L/v) / scale) * weight; // ↑ Classic wave equation: sin(freq × (time - distance/speed)) }

How ripples become displacement

Final displacement mixes two components:

  • Oscillatory: wave value directly as displacement (pixels sway with the wave)
  • Gradient: wave slope direction as displacement (pixels slide along wave surface)

rsGradientMix controls the ratio—oscillatory-dominant looks like "water ripples," gradient-dominant looks like "light refraction."

6

Cellular / Voronoi: cells under a microscope

Cellular Voronoi
Seeds on a field each claim the region closest to them. The boundaries form "cell walls"—that’s a Voronoi diagram.

cellular2x2()

cellular2x2() computes the two nearest seed distances F.x and F.y (nearest and second-nearest):

vec2 F = cellular2x2(cellUV); float facets = F.y - F.x; // second-nearest - nearest float dots = smoothstep(0.05, 0.3, F.x); float mask = step(0.4, facets) * dots; // facets large → far from boundary (cell interior) // facets small → on boundary (cell wall)

This mask controls displacement strength—strong at boundaries (darker cell walls), weak inside.

Time animation

The cellular effect drifts slowly (speed ×0.7), giving the texture a "flowing" feel. Edges use smoothstep for a 10% transition to avoid abrupt cutoff at the image boundary.

7

Metallic: turn ink into liquid metal

Gold metallic
Gold
Silver metallic
Silver
Your ink becomes liquid mercury—mirror reflection, brighter edges (Fresnel), and diamond-like dispersion. metallic.frag does this, only where there is ink (bugsMask).

Reflection and normals

The program computes "surface normals" from the mask gradient—where ink density changes sharply, the surface acts like a slope and light reflects sideways:

// Compute normals from mask gradient float dx = (maskRight - maskCenter) * resolution.x * 0.5; float dy = (maskTop - maskCenter) * resolution.y * 0.5; vec3 normal = normalize(vec3(dx, dy, sqrt(1.0 - dx*dx - dy*dy)));

Normals are like the "facing direction" at each point—flat surfaces face up, slopes face sideways. With normals, we can calculate where light bounces off the surface.

Fresnel effect: brighter edges

Standing by a lake, looking straight down at the water—you see the bottom. But looking far out, the water becomes a mirror reflecting the sky. Same water, different viewing angle, different reflectivity. That's the Fresnel effect.

The metallic shader uses Schlick's approximation for Fresnel:

float fresnel = 1.0 - pow(dot(normal, -viewDir), 1.0); // Normal facing you → large dot → low fresnel → not very bright // Normal facing sideways → small dot → high fresnel → brighter edges

Six metallic tones

ToneRGBSpecial handling
Gold(0.88, 0.72, 0.52)Warm reflection
Silver(0.75, 0.75, 0.75)Grayscale detection → neutral reflection
Copper(0.72, 0.50, 0.35)Reddish copper tone
Rose(0.88, 0.65, 0.70)Rose gold
Black Gold(0.15, 0.12, 0.08)Special black-gold texture + rust detail
Diamond(0.95, 0.95, 1.0)Dispersion refraction! Different IOR per RGB channel

Diamond dispersion

Diamond mode is the most spectacular—it simulates real diamond "dispersion." Diamonds refract different colors of light at different rates:

const float diamondIOR_r = 2.408; // Red light IOR const float diamondIOR_g = 2.424; // Green light IOR const float diamondIOR_b = 2.432; // Blue light IOR // Each color refracts separately, sampling from different positions // → Result: rainbow halo at edges

Because red bends least and blue bends most, the three colors separate at edges—just like a real diamond's "fire."

8

White Dot & Film Grain: paper imperfections

White dot
White Dot
Film grain
Film Grain
Real paper has pinholes, spots, scratches. Old film has grain. These "flaws" add warmth and a hand-made feel.

Three sizes of white dots

LayerSizeDescription
Layer 1~5pxSmall punctures—like pinholes poked in paper
Layer 210–30pxMedium spots—random-radius circular white patches
Layer 340–100pxLarge gouges—elliptical, directional scratches

Each layer uses hash noise, with a clustering effect—dots tend to appear in groups, not uniformly scattered. Density controlled by whiteDotDensity.

Film grain

Simulates real film grain with three characteristics:

  • More grain in dark areas: lower brightness → more visible grain (like real film)
  • More grain on strokes: areas with strong force field (active brushwork) get denser grain
  • Multi-frequency mix: fine 60% + medium clumps 30% + coarse patches 10%
float gFine = grain(coord); // Fine grain float gMedium = grain(floor(coord * 0.5) * 2.0); // Medium clumps float gCoarse = hash(floor(coord * 0.15)); // Coarse patches float g = gFine * 0.6 + gMedium * 0.3 + gCoarse * 0.1;
9

Combining effects

Effects can be stacked. Pipeline order:

flow.frag (per stroke) — Simplex + blendType + edge halftone + white brush fix
distort.frag (per frame) — FBM → Resonance → Cellular → White Dots → Film Grain
metallic.frag (per frame) — Reflection + Fresnel + dispersion + texture

Suggested combos

StyleCombo
Classic inkFlow (basic) + little Distort + some White Dot + Film Grain
AbstractFlow (vortex) + FBM + Resonance Scatter
OrganicFlow (cellular) + Cellular + Film Grain
Metal sculptureFlow (basic) + Metallic (Gold/Diamond)
Vintage filmFlow (basic) + strong Film Grain + lots of White Dot

Summary

All effects share one rule: they don’t change the original strokes, only how they’re seen. Like layers of filters—stack them, use one, adjust strength. The same stroke can look like ink, watercolor, metal, or tissue under a microscope.

← Previous: Ink Effects   |   Next: Blend & Flow →

InkField Tutorial Series — Understanding digital ink, explained simply