Still marks the third installment of an interactive, real-time astronaut narrative that Ming Jyun Hung has been developing for the web. Each new chapter advances the story while introducing fresh visual and technical challenges to explore.

The protagonist, having wandered through Drift and crossed False Earth repeatedly without reaching his destination, finally succumbs to exhaustion. Suspended between sleep and wakefulness, he experiences a state where time becomes fluid, and the landscape—ground, flowers, suit—merges into a continuous whole as his thoughts drift between memory and the present moment.

Building a Japanese Print Style

The visual direction emerged from Akira, a film whose atmosphere, sound design, and musical composition left a lasting emotional impression. Examining the film alongside traditional Japanese paintings and folding screens revealed a shared visual language: broad areas of flat color, sharp edges, deliberate composition, woven surfaces, and irregular marks that convey mood through shape, material, and spacing rather than photorealism. In both mediums, stillness and rhythm carry equal weight to detail.

Hung translated this shared visual logic into a real-time 3D environment. Toon shading maintains broad planes, the ground shadow becomes an uneven ink wash, silk weave supplies material texture, and procedural growth gives flowers and tendrils organic movement. Rather than reproducing either reference literally, the goal was to capture their compositional sense and material presence within a living, animated scene.

Woodblock Toon

The astronaut and flowers needed to share a unified visual style while using distinct shading and outline approaches. The suit employs a toon material applied to textured albedo, whereas petals and stems use a vertex-color material designed for VAT instancing. Applying identical quantized lighting to both elements creates visual cohesion. The character uses an inverted hull for outlines, while petals rely on a mask-based shader.

The shading begins with N·L, measuring how directly a surface faces the light source, where N represents the normal and L the light direction. This value is remapped between thresholdLow and thresholdHigh, then quantized into color levels using floor. The result stays limited to two levels—one shadow band and one lit band—because additional steps lose the print-like quality. Shadow and highlight become tints applied to the base color, and when the bands appear too sharp, world-space noise is added to the threshold to soften the hard edge.

const ndl = max(dot(N, L), 0.0); const thresholdNoise = fbm3(positionWorld.mul(thresholdNoiseScale)) .sub(0.5) .mul(thresholdNoiseStrength); const preShade = clamp( ndl.sub(thresholdLow.add(thresholdNoise)) .div(thresholdHigh.sub(thresholdLow)), 0.0, 1.0, ); const quantized = floor(preShade.mul(colorLevels.sub(1.0)).add(0.5)) .div(colorLevels.sub(1.0)); const litColor = mix( albedo.mul(shadowTint), albedo.mul(highlightTint), quantized, );

Outline rendering follows the same aesthetic but varies by geometry type. The character uses an inverted hull—a second mesh with back faces pushed outward along the normal. Petals cannot use this approach because each VAT head is instanced hundreds of times; adding another mesh per head would be computationally expensive and would still trace the wrong silhouette since the deforming mesh does not match the petal cutout. Instead, the shape already exists in a mask texture, so pixels outside the shape are discarded while the rim is drawn in the shader using that same mask, allowing one texture to handle both shape and edge.

Ink-Wash Ground Shadow

While studying Akira, Hung noticed a particular poster featuring Kaneda and his bike on flat white, with the shadow beneath them drawing the eye: soft at the edges, uneven inside, and only loosely following the silhouette, like paint thinned on paper. Since the rest of the scene was designed to look drawn, the shadow needed to follow the same visual logic.

The directional light already provides a shadow map. This map is read on the ground, inverted, and processed through smoothstep to create a broad wash with a soft edge. A noise field loosens the edge and breaks up the fill, so the edge and interior vary together.

The darker contour uses the same shadow value but sits just outside the wash rather than tracing the exact silhouette, remaining tied to the shadow while reading as a separate drawn stroke. A finer noise breaks the line so it does not run as one continuous edge, while fwidth maintains its screen-space width. The wash and line combine using max, preventing overlapping masks from stacking and darkening the result excessively.

const shade = shadow(light).oneMinus(); const noise = fbm2(positionWorld.xz.mul(washScale)); const fill = smoothstep(washAt, washAt.add(washSoft), shade.add(noise.mul(washBleed))); const wash = fill.mul(float(1.0).sub(noise.mul(washMottle).max(0.0))); const wobble = mx_noise_float(positionWorld.xz.mul(contourWobbleScale)).mul(contourWobble); const penWidth = fwidth(shade).mul(contourWidth).max(0.0001); const line = float(1.0).sub(smoothstep(0.0, penWidth, shade.sub(contourShade.add(wobble)).abs())); const shColor = mix(washColor, contourColor, line); return mix(bg, shColor, max(wash.mul(washStr), line.mul(contourStr)));

Beyond the ground wash, flowers cast shadows onto the character without casting them onto themselves. A second shadow map contains only the flowers: their casters use a separate layer, and the plant-shadow light's shadow camera is restricted to that layer. The light shares the main light's position but has zero intensity, writing depth without adding visible illumination and keeping the character out of the map.

The low-polygon VAT mesh also serves as a shadow-only proxy on a separate render layer, so shadow passes do not require the full petal geometry.

Silk Weave

Two Japanese folding screens depicting hollyhocks—one by Sakai Hōitsu and another by Ogata Kenzan—provided reference material. The ground in both paintings drew particular attention: a faint weave, a grid resembling threads, and marks that appear like stains, uneven enough to suggest hand-painting. Hung wanted to carry this quality into the scene.

The weave was applied across the entire frame because, in those screens, the grid and stains belong to the painting's ground rather than to individual objects. Screen UV coordinates are scaled into thread cells, the grid is jittered to avoid a machine-made appearance, and warp and weft directions combine into a weave pattern. Thread tones shift and slower noise multiplies in for stains, then both layers multiply with the scene color, darkening the entire frame.

const coord = screenUV.mul(vec2(aspect, 1.0)).mul(threadCount); const x = coord.x.add(hash(floor(coord.y)).sub(0.5).mul(irregularity)); const y = coord.y.add(hash(floor(coord.x)).sub(0.5).mul(irregularity)); const warp = pow(abs(sin(x.mul(PI))), sharpness); const weft = pow(abs(sin(y.mul(PI))), sharpness); const checker = mod(floor(x).add(floor(y)), 2.0); const weave = mix(warp, weft, checker); const fabric = clamp(float(1.0).sub(strength.mul(float(1.0).sub(weave))).add(threadTone), 0.0, 1.0); const blotch = float(1.0).sub(blotchStrength.mul(smoothstep(0.45, 0.95, stain))); const overlaid = sceneColor.mul(tint).mul(fabric).mul(blotch);

One Plant

With the frame established, focus shifted to the smallest repeated unit: a single plant. Before distributing plants around the astronaut, Hung worked out the flower, stem, leaves, and lifecycle of one instance.

Flower

The Blooming Flowers Blender pack provided the flower animation, with Geometry Nodes already producing the detailed bloom motion needed. That animation was preserved and converted to VAT using the same workflow from False Earth, employing an addon Hung created to automate the entire process in Blender.

On top of the baked VAT, petals shed a few at a time, then lift and fan outward. The shedding concept drew inspiration from Flowers and People by teamLab, where a flower reaches its fullest moment just before falling. The mesh arrives as separate islands, so vertices group by connectivity with a petal id and pivot vertex packed into vertex color. A hash of that id staggers timing and varies each petal's lift, while the eased value drives both upward motion and outward spread. Combining baked VAT with procedural passes preserves detailed authored motion while adding variation and controllable behavior at runtime, making the animation feel alive and less repetitive without rebaking.

const petalId = color.g; // island id, 0..1 const pivot = sampleVAT(color.b, frame); // same vertex, current bloom frame const startJitter = fract(sin(petalId * 127.1) * 43758.5453); const heightJitter = fract(sin(petalId * 127.1 + 7.13) * 43758.5453); const t = clamp((shed - startJitter * stagger) / (1.0 - stagger), 0.0, 1.0); const ease = t * t * (3.0 - 2.0 * t); const shrunk = pivot.add(basePos.sub(pivot).mul(1.0 - ease)); const height = 1.0 + (heightJitter - 0.5) * 2.0 * riseVariance; const lift = rise * max(height, 0.0) * ease; const outward = normalize(vec3(pivot.x, 0.0, pivot.z)); const fan = rotate(outward, flowerRotation) * spread * ease; const position = rotate(shrunk, flowerRotation) + flowerPosition + vec3(0.0, lift, 0.0) * stemLength + fan * stemLength;

Stem

The Blooming Flowers pack also builds stems in Geometry Nodes, but those stems are tied to specific flowers. Hung needed a stem that could be reshaped in Three.js, so the same basic concept was rebuilt: a curve swept into a tube. A seeded Catmull-Rom curve starts slightly below ground, leans toward the flower head, and includes a sideways bend. The curve is sampled into rings, the base flares, and the shaft tapers toward the tip.

const from = new THREE.Vector3(0, -BASE_BURY, 0); const to = /* lean azimuth × stemLength */; const bend = /* seeded sideways offset */; const curve = new THREE.CatmullRomCurve3( [ from, from.clone().lerp(to, 0.25).add(bend), from.clone().lerp(to, 0.75).add(bend), to, ], false, 'centripetal', ); const scale = (1 - (1 - radiusAttenuation) * t) + baseFlare * (1 - t) ** 3;

After building the tube once, it grows with one value from 0 to 1. The fragment shader hides everything beyond the growth front, while the vertex shader scales each visible ring outward from the centerline and moves the front continuously through its active segment. The same value positions the flower head along the curve, keeping the bloom attached to the stem's end as it grows.

If(uv().x.greaterThan(growth), () => Discard()); const rScale = startScale + growth * (1.0 - startScale); grown = center.add(positionLocal.sub(center).mul(rScale));

Leaf

With the stem in place, leaves were added as a separate modeled mesh. In the shader, each blade bends, starting with a tight curl and easing open as the leaf grows.

A few leaves attach at seeded positions along the middle section of the curve, sorted from base to tip and alternated around the shaft. Each leaf attaches to the tube's surface at its own parameter value, so its position remains stable while the stem grows. The same 0 to 1 growth value reveals each leaf after the growth front reaches its attachment point.

curve.getPointAt(t, P); pos.copy(P).addScaledVector(outward, stemRadius * radiusScale); const growFrac = smoothstep(attachT, attachT + GROW_WINDOW, stemGrow); placed = attach.add(leafPos.mul(growFrac));

Lifecycle

The animation operates on two time scales. At the plant level, each instance owns a complete lifecycle with its own seeded age and durations, allowing plants to be in different phases while following the same rules. Within that timeline, the stem, flower, and leaves each receive their own component progress. These are not separate clocks: the plant lifecycle determines when each part acts, and its local progress determines how that part appears.

The plant clock uses four stages borrowed from False Earth: Delay, Grow, Keep, and Die. During Delay, the plant rests. During Grow, the stem advances from 0 to 1, the flower follows its tip as the VAT opens, and each leaf begins unfolding when the growth front reaches its attachment point. During Keep, the stem and flower remain full and leaves stay open. During Die, petal shedding begins while the stem still stands, then the stem returns from 1 to 0 and leaves retract with it.

The Field

Once a single plant functioned correctly, it could be distributed around the astronaut. Random copies read as noise, so the field was built as uneven masses with gaps of ground between them. The two visual references used clustered marks and open gaps to build density rather than filling the surface evenly. This balance—dense hubs, soft fall-off, and no uniform carpet—guided the field design.

The field was constructed as a probability map before placing any plants. Four anchors positioned around the astronaut raise local probability, and a warp breaks the resulting shapes into irregular masses. Hearts mark centers within those masses and move slowly, while the density field remains fixed. Flowers hop from a heart and repeat that step when a plant dies.

The Density Field

Four body-contact regions serve as anchors: the hip, left hand, left boot, and backpack. Each adds density nearby with an elliptical falloff along its local axis. These falloffs alone would be too regular, so the map is distorted by warping each sample coordinate before evaluation.

Anchor contributions combine so overlapping regions merge into one mass, then samples that fall inside or too close to the posed body model are rejected using packed BVHs. The tendril system uses the same BVHs later for surface queries, so the host geometry is built once and reused. Finally, bare patches are left so the result does not become a uniform carpet.

Hearts and Hops

Density indicates where plants can grow, but a mechanism was needed to group them. Hearts are placed as local clump centers, roughly one for every seven plants. They are distributed across anchors by weight and kept only if they land inside the density field. The hearts then wander slowly while the underlying density map stays fixed.

Each flower begins with a short random step from a heart. The new point is kept only if it passes the same density check, staying inside the surrounding mass. When a plant dies, the step repeats around a nearby heart, so the next plant remains in the same local mass as hearts move. Local density then controls head size and opening range. To prevent clusters from becoming visually noisy, the quieter rose fills most of the field while the brighter, busier dahlia is reserved for fewer plants.

Packed Stems

With the layout fixed, repeated stem tubes merge into one geometry and render in a single draw call, following the instanced grass approach from False Earth. The CPU maintains each plant's static layout and shape data, written once when the field is built. The GPU receives only changing values—growth, sway, world offset, and rotation angle—through a DataTexture updated each frame. A respawn can then change a plant's position without rebuilding the geometry.

Generative Tendrils

Tendrils were added to connect the garden to the astronaut, both visually and conceptually. They transform the plants from an independent field into a living system that reaches back to him: the field grows around him while tendrils cross the suit and lead back to the ground. Each tendril is a wrap across the suit joined to a ground route, then combined into one tree.

Tendril Trees

Field stems are single tubes running from ground to tip, while tendrils form branching tube structures. One ground entry can split into several branches wrapping around the suit. These segments pack into one tree, so the whole structure reveals as one continuous path. The root-to-tip reveal idea from THREE.Tree is used, but branching comes from surface routes and wrap targets. Routes taper toward the tips according to their downstream load, and a shared world-space noise field gives nearby wraps a related wobble. Wrap shape depends on its guide: radial guides form partial arcs around a capsule, while planar guides form surface strokes. Both shapes stay slightly away from the suit surface to avoid mesh intersections.

How They Grow

Each tendril combines two parts: a wrap curve across the suit and a ground route back to the floor. The wrap follows the host's surface while the ground route follows its connectivity. The MeshBVH handles the wrap's geometric queries while the surface graph manages the route back to the ground. The hitch marks their handoff: the wrap begins there and the ground route connects it back to the floor through the graph. The workflow follows that order: prepare the hosts, construct the wrap, then connect the hitch to the ground.

Wrap Construction

  1. Choose a wrap station: Sample the posed surface, assign each accepted point to an eligible guide, and store its normalized position u along that guide. Together, these values determine where the wrap begins.
  2. Generate wrap candidates: The guide turns that station into temporary positions for testing the host surface.
  3. Project the candidates onto the host: Project each candidate onto the host surface using the guide and host BVH. The accepted hits are ordered into the wrap curve, whose first point is the hitch.

Ground Route

  1. Choose the graph node: The hitch does not always lie on a graph vertex, so connect it to a nearby graph node before tracing the ground route.
  2. Route from the ground: Use vertices near the ground as possible sources. Dijkstra selects the source connected to that graph node by the shortest path through the surface graph. The result is the ground route.

Finally, the local connection joins the graph node to the hitch. Both parts belong to one tendril tree, so a single growth front reveals the tendril from the ground, through the handoff, and around the wrap.

Suit Integration

The body and backpack use the same host and routing pipeline. The body carries most of the routes while the backpack adds a lighter contact layer. Plumeria heads bind to active wraps, giving the suit its own flower type within the field.

Conclusion

This chapter began with visual observation: how Akira and traditional Japanese paintings use flat color, clear edges, tactile surfaces, and careful spacing to create atmosphere. Those observations became toon-shaded forms, ink-wash shadows, woven surfaces, procedural flowers, and tendrils that grow across the posed body.

The process demonstrated how to turn observation into a working system. Rather than copying a reference, the challenge was identifying what created its feeling and finding a practical way to reproduce that quality in real-time 3D. The field rules, flower lifecycles, packed data, tendril routes, culling, level of detail, and lightweight shadow proxies all had to support a clear visual or narrative goal. The most difficult part was balancing visual richness with performance: the scene needed to feel dense, tactile, and alive, but every additional vertex, instance, shadow, and layer of detail carried a cost. That balance remains open, with room to push visual richness further while keeping the experience responsive.

This process will continue in future work. Hung intends to keep observing the visual qualities of artworks, the forms and rhythms of living things, and the details of the world around him. A new story may lead toward a different visual direction, while a visual experiment may reveal an unexpected story. For him, that exchange between observation, technology, and storytelling is where the next work begins.

Source: Codrops