Editor’s Observe: With the Three.js Convention simply sooner or later away, we couldn’t think about a greater last characteristic earlier than the festivities start. At the moment, we’re delighted to highlight Ming Jyun Hung and Nonetheless, the most recent chapter in his ongoing astronaut story. Impressed by Akira and conventional Japanese artwork, Ming Jyun brings collectively toon shading, ink wash, silk textures, procedural flowers, and generative tendrils to create a world that feels each rigorously crafted and splendidly alive! We actually hope you take pleasure in this superb breakdown!
🇫🇷 Tomorrow’s the day! The primary ever Three.js Convention is nearly able to bloom in Paris. 🌸 If you happen to’re already round and nonetheless desirous about it, that is your final likelihood to be there! Use code CODROPS for 15% off and seize your ticket →
Nonetheless is chapter three of an interactive, real-time astronaut story I’ve been making on the internet (earlier Codrops article). Every chapter strikes the story ahead whereas giving me a brand new visible and technical query to discover.
After being misplaced in Drift, he ran throughout False Earth day after day, at all times transferring, by no means arriving. Ultimately, after what felt like perpetually, exhaustion caught up with him, and he lay down, suspended between waking and sleep. In that half-dreaming state, time loses its edges. The bottom, the flowers, and the go well with start to merge into one steady panorama, whereas his ideas drift between reminiscence and the current.
Constructing a Japanese Print Model
The visible exploration for this chapter began with Akira. My connection to the movie was emotional earlier than it grew to become technical: its visible model, ambiance, sound design, and music stayed with me lengthy after I watched it, leaving me eager to create one thing with a equally lasting feeling.
I then seemed towards conventional Japanese work and folding screens. Inserting Akira beside these work, I observed a shared visible logic: flat areas of coloration, clear edges, deliberate composition, woven surfaces, and irregular marks that create ambiance by form, floor, and spacing moderately than realism. In each, stillness and rhythm matter as a lot as element.
I translated this shared logic right into a real-time 3D scene. Toon shading retains the planes broad, the floor shadow turns into an uneven ink wash, the silk weave provides materials grain, and procedural progress provides the flowers and tendrils an natural rhythm. The objective was to not reproduce both reference actually, however to hold their sense of composition and materials presence right into a residing scene.


Woodblock Toon
I needed the astronaut and flowers to share the identical visible model, however every wanted its personal shading and description strategies. For shading, the go well with makes use of a toon materials on textured albedo, whereas petals and stems use a vertex-color materials constructed for VAT instancing. Making use of the identical quantized lighting to each is what makes them learn as one picture. For the outlines, the character makes use of an inverted hull, whereas the petals use a mask-based shader.
I begin with N·L, which measures how a lot a floor faces the sunshine: N is the conventional and L is the sunshine route. I remap it between thresholdLow and thresholdHigh, then use flooring to quantize it into coloration ranges. I preserve the end result to 2 ranges: one shadow band and one lit band, as a result of extra steps cease studying like print. Shadow and spotlight are tints on the bottom coloration, and when the bands look too clear I add a bit of world-space noise to the brink to interrupt the exhausting 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 = flooring(preShade.mul(colorLevels.sub(1.0)).add(0.5))
.div(colorLevels.sub(1.0));
const litColor = combine(
albedo.mul(shadowTint),
albedo.mul(highlightTint),
quantized,
);
The define follows the identical model, however the implementation relies on every asset’s geometry. On the character, I take advantage of an inverted hull: a second mesh with again faces pushed out alongside the conventional. Petals skip that path as a result of every VAT head is instanced tons of of occasions, so including one other mesh for each head can be costly and would nonetheless hint the incorrect silhouette: the deforming mesh shouldn’t be the petal cutout. The form is already in a masks texture, so I discard the skin and draw the rim within the shader on that very same masks, which lets one texture deal with each form and edge.
Ink-Wash Floor Shadow
Whereas I used to be taking a look at Akira for this chapter, I discovered this poster. Kaneda and the bike sit on flat white, however the shadow beneath is the half I preserve noticing: mushy on the edge, uneven inside, and solely loosely following the silhouette, like paint thinned on paper. Since the remainder of the scene is constructed to look drawn, I needed the shadow to comply with the identical logic.

The directional mild already provides me a shadow map. I learn it on the bottom, invert it, and run it by smoothstep to show it right into a broad wash with a mushy edge. One noise discipline loosens the sting and breaks up the fill, so the sting and fill fluctuate collectively.
The darker contour makes use of the identical shadow worth and sits simply exterior the wash moderately than tracing the silhouette, so it stays tied to the shadow whereas studying as a separate drawn stroke. A finer noise breaks the road so it doesn’t run as one steady edge, whereas fwidth retains its screen-space width regular. I mix the wash and line with max, so overlapping masks don’t stack and make the end result too darkish.
const shade = shadow(mild).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 = combine(washColor, contourColor, line);
return combine(bg, shColor, max(wash.mul(washStr), line.mul(contourStr)));
Past the bottom wash, I additionally needed the flowers to solid shadows on the character with out casting them onto themselves. For that, I take advantage of a second shadow map containing solely the flowers: their casters use a separate layer, and the plant-shadow mild’s shadow digicam is restricted to it. The sunshine shares the primary mild’s place however has zero depth, so it writes depth with out including seen mild and retains the character out of the map.
The low-poly VAT mesh additionally serves as a shadow-only proxy on a separate render layer, so the shadow passes don’t want the total petal geometry.
Silk Weave


I used to be taking a look at two Japanese folding screens of hollyhocks: Sakai Hōitsu’s and Ogata Kenzan’s. The bottom is the half I preserve noticing: a faint weave, a grid like threads, and marks that sit like stains, uneven sufficient to really feel painted by hand. I needed to hold that high quality into the scene.
I put that weave over the entire body as a result of, on these screens, the grid and stains belong to the bottom of the portray moderately than to 1 object. I scale screenUV into thread cells, jitter the grid so it doesn’t look machine-made, and mix the warp and weft instructions right into a weave. I shift the thread tones and multiply in slower noise for stains, then multiply each layers with the scene coloration so the feel darkens the body as a complete.
const coord = screenUV.mul(vec2(facet, 1.0)).mul(threadCount);
const x = coord.x.add(hash(flooring(coord.y)).sub(0.5).mul(irregularity));
const y = coord.y.add(hash(flooring(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(flooring(x).add(flooring(y)), 2.0);
const weave = combine(warp, weft, checker);
const material = clamp(float(1.0).sub(power.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(material).mul(blotch);
One Plant
With the body established, I flip to the smallest repeated unit within the backyard: one plant. Earlier than constructing the sphere round him, I take advantage of a single plant to work out its flower, stem, leaves, and lifecycle.
Flower
For the flower animation I take advantage of the Blooming Flowers Blender pack. Its Geometry Nodes already produce the detailed bloom movement I want, so I preserve that animation and convert it to VAT with the identical workflow I utilized in False Earth, utilizing an addon I made for the entire course of in Blender.

On high of that baked VAT, petals go away just a few at a time, then raise and fan out. I took the shedding concept from Flowers and Individuals by teamLab, the place a flower reaches its fullest level simply earlier than it falls. The mesh already comes as separate islands, so I group vertices by connectivity and pack a petal id and a pivot vertex into vertex coloration. A hash of that id staggers the timing and varies every petal’s raise, whereas the eased worth drives each the upward movement and the outward unfold. Combining baked VAT with a procedural move lets me protect detailed authored movement whereas including variation and controllable habits at runtime, making the animation really feel extra alive and fewer repetitive with out rebaking the unique.
const petalId = coloration.g; // island id, 0..1
const pivot = sampleVAT(coloration.b, body); // identical vertex, present bloom body
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 top = 1.0 + (heightJitter - 0.5) * 2.0 * riseVariance;
const raise = rise * max(top, 0.0) * ease;
const outward = normalize(vec3(pivot.x, 0.0, pivot.z));
const fan = rotate(outward, flowerRotation) * unfold * ease;
const place = rotate(shrunk, flowerRotation) + flowerPosition
+ vec3(0.0, raise, 0.0) * stemLength
+ fan * stemLength;
Stem
That pack additionally builds stems in Geometry Nodes, however these stems are tied to particular flowers. I needed a stem I might reshape in Three.js, so I rebuilt the identical fundamental concept: a curve swept right into a tube. A seeded Catmull-Rom curve begins barely beneath the bottom, leans towards the flower head, and will get a sideways bend. I pattern that curve into rings, flare the bottom, and taper the shaft towards 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 constructing the tube as soon as, I develop it with one worth from 0 to 1. The fragment shader hides every little thing past the expansion entrance, whereas the vertex shader scales every seen ring out from the centerline and strikes the entrance repeatedly by its lively section. I take advantage of the identical worth to put the flower head alongside the curve, so the bloom stays connected to the tip of the stem because it grows.
If(uv().x.greaterThan(progress), () => Discard());
const rScale = startScale + progress * (1.0 - startScale);
grown = heart.add(positionLocal.sub(heart).mul(rScale));
Leaf
With the stem in place, I add leaves as a separate modeled mesh. Within the shader I bend every blade, beginning with a decent curl and easing it open because the leaf grows.
I place just a few leaves at seeded positions alongside the center part of the curve, kind them from base to tip, and alternate them across the shaft. Every leaf attaches to the tube’s floor at its personal t, so its place stays steady whereas the stem grows. The identical 0 to 1 progress worth then reveals every leaf after the entrance reaches its attachment level.
curve.getPointAt(t, P);
pos.copy(P).addScaledVector(outward, stemRadius * radiusScale);
const growFrac = smoothstep(attachT, attachT + GROW_WINDOW, stemGrow);
positioned = connect.add(leafPos.mul(growFrac));
Lifecycle
The animation has two ranges of time. On the plant stage, every occasion owns a whole lifecycle with its personal seeded age and durations, so vegetation will be in numerous phases whereas utilizing the identical guidelines. Inside that timeline, the stem, flower, and leaves every obtain their very own part progress. These are usually not separate clocks: the plant lifecycle decides when every half acts, and its native progress decides how that half seems.
The plant clock makes use of the identical 4 phases as False Earth: Delay, Develop, Hold, Die. Throughout Delay, the plant is at relaxation. Throughout Develop, the stem advances from 0 to 1, the flower follows its tip because the VAT opens, and every leaf begins unfolding when the expansion entrance reaches its attachment level. Throughout Hold, the stem and flower keep full and the leaves stay open. Throughout Die, petal shed begins whereas the stem continues to be standing, then the stem returns from 1 to 0 and the leaves retract with it.

The Area
As soon as one plant works, I can distribute it across the astronaut. Random copies learn as noise, so I construct the sphere as uneven plenty with gaps of floor between them. The 2 references beneath construct density by clustered marks and open gaps moderately than filling the floor evenly. That’s the steadiness I needed for the sphere: dense hubs, mushy fall-off, and no uniform carpet.


I construct the sphere as a chance map earlier than inserting any vegetation. 4 anchors across the astronaut increase the native chance, and a warp breaks the ensuing shapes into irregular plenty. Hearts mark facilities inside these plenty and transfer slowly, whereas the density discipline stays mounted. Flowers hop from a coronary heart and repeat that step when a plant dies.
The Density Area
I outline 4 body-contact areas as anchors: the hip, left hand, left boot, and backpack. Every provides density close by with an elliptical falloff alongside its native axis. These falloffs can be too common on their very own, so I distort the map by warping every pattern coordinate earlier than evaluating it.
I mix the anchor contributions so overlapping areas merge into one mass, then use the posed physique and pack BVHs to reject samples that fall inside or too near the mannequin. The tendril system makes use of the identical BVHs later for its floor queries, so I construct the host geometry as soon as and reuse it. Lastly, I go away naked patches so the end result doesn’t grow to be a carpet.
Hearts and Hops
Density tells me the place vegetation can develop, however I nonetheless want a strategy to group them. I place hearts as native clump facilities, roughly one for each seven vegetation. I distribute them throughout the anchors by weight and preserve every one provided that it lands contained in the density discipline. The hearts then wander slowly, whereas the underlying density map stays mounted.
Every flower begins with a brief random step from a coronary heart. I preserve the brand new level provided that it passes the identical density verify, so it stays inside the encircling mass. When a plant dies, I repeat the step round a close-by coronary heart, so the subsequent plant stays in the identical native mass because the hearts transfer. Native density then controls head dimension and opening vary. To maintain the clusters from turning into visually noisy, I let the quieter rose fill a lot of the discipline and reserve the brighter, busier dahlia for fewer vegetation.
Packed Stems
With the format mounted, I merge the repeated stem tubes into one geometry and render them in a single draw, following the instanced grass method from False Earth. The CPU retains every plant’s static format and form information, written as soon as when the sphere is constructed. The GPU receives solely the values that change, reminiscent of progress, sway, world offset, and rotation angle, by a DataTexture up to date every body. A respawn can then change a plant’s place with out rebuilding the geometry.

Generative Tendrils
I added tendrils to attach the backyard to the astronaut, each visually and conceptually. They flip the vegetation from an unbiased discipline right into a residing system that reaches again to him: the sphere grows round him, whereas tendrils cross the go well with and lead again to the bottom. Every tendril is a wrap throughout the go well with joined to a floor route, then mixed into one tree.
Tendril Bushes
Area stems are single tubes that run from floor to tip, whereas tendrils type branching tube buildings. One floor entry can cut up into a number of branches that wrap across the go well with. I pack these segments into one tree, so the entire construction can reveal as one steady path. I take advantage of the root-to-tip reveal concept from THREE.Tree, however the branching comes from the floor routes and wrap targets. Routes taper towards the guidelines in line with their downstream load, and a shared world-space noise discipline provides close by wraps a associated wobble. The wrap form relies on its information: radial guides type partial arcs round a capsule, whereas planar guides type floor strokes. I preserve each shapes barely away from the go well with floor to keep away from mesh intersections.
How They Develop
Every tendril combines two elements: a wrap curve throughout the go well with and a floor route again to the ground. The wrap follows the host’s floor, whereas the bottom route follows its connectivity. I take advantage of the MeshBVH for the wrap’s geometric queries and the floor graph for the route again to the bottom. The hitch marks their handoff: the wrap begins there, and the bottom route connects it again to the ground by the graph. The workflow follows that order: put together the hosts, assemble the wrap, then join the hitch to the bottom.
- Put together the hosts: I bake the posed physique and backpack into host-space geometries, construct one BVH for every, and switch their meshes into floor graphs.
Wrap Development
- Select a wrap station: I pattern the posed floor, assign every accepted level to an eligible information, and retailer its normalized place
ualongside that information. Collectively, these values decide the place the wrap begins. - Generate wrap candidates: The information turns that station into short-term positions for testing the host floor.
- Venture the candidates onto the host: I mission every candidate onto the host floor utilizing the information and host BVH. The accepted hits are ordered into the wrap curve, whose first level is the hitch.
Floor Route
- Select the graph node: The hitch doesn’t at all times lie on a graph vertex, so I join it to a close-by graph node earlier than tracing the bottom route.
- Route from the bottom: I take advantage of the vertices close to the bottom as potential sources. Dijkstra selects the supply related to that graph node by the shortest path by the floor graph. The result’s the floor route.
Lastly, the native connection joins the graph node to the hitch. Each elements belong to 1 tendril tree, so a single progress entrance reveals the tendril from the bottom, by the handoff, and across the wrap.
Go well with Integration
The physique and backpack use the identical host and routing pipeline. The physique carries a lot of the routes, whereas the backpack provides a lighter contact layer. Plumeria heads bind to lively wraps, giving the go well with its personal flower kind throughout the discipline.
Conclusion
This chapter began with visible commentary: how Akira and conventional Japanese work use flat coloration, clear edges, tactile surfaces, and cautious spacing to create ambiance. These observations grew to become toon-shaded varieties, ink-wash shadows, woven surfaces, procedural flowers, and tendrils that develop throughout the posed physique.
The method taught me learn how to flip commentary right into a working system. Fairly than copying a reference, the problem was to establish what created its feeling and discover a sensible strategy to reproduce that high quality in real-time 3D. The sphere guidelines, flower lifecycles, packed information, tendril routes, culling, stage of element, and light-weight shadow proxies all needed to assist a transparent visible or narrative objective. Essentially the most troublesome half was balancing visible richness with efficiency: the scene wanted to really feel dense, tactile, and alive, however each extra vertex, occasion, shadow, and layer of element had a value. That steadiness stays open, with room to push the visible richness additional whereas maintaining the expertise responsive.
That course of will proceed in future work. I need to preserve observing the visible qualities of artworks, the varieties and rhythms of residing issues, and the main points of the world round me. A brand new story could lead towards a unique visible route, whereas a visible experiment could reveal an surprising story. For me, that alternate between commentary, know-how, and storytelling is the place the subsequent work begins.









