
Lab notes
This experiment creates rising smoke above a coffee cup with a custom GLSL shader on a tall plane—not a particle system, not animated sprites. A Perlin noise texture drives both the shape (fragment alpha) and the motion (vertex twist and wind). The plume sits in a baked 3D scene so the vapor feels grounded in a real environment instead of floating in a void.
Good smoke reads as soft volume that twists as it rises: denser and more chaotic in the middle, faded at the plane edges, scrolling upward over time, and drifting harder toward the top like air currents. The vertex shader warps the plane into a corkscrew plume; the fragment shader paints translucent noise that looks like steam.
Baked scene, procedural plume
The cup and table come from a baked GLTF (`bakedModel.glb`) with anisotropy boosted so wood grain and mug detail stay sharp. The smoke is a separate `PlaneGeometry(1, 1, 16, 64)`—extra segments on Y so vertical twist looks smooth. Geometry is pivoted at the bottom, scaled tall, and parked just above the rim so the steam reads as escaping the mug.
`perlin.png` loads with `RepeatWrapping` so UVs can scroll forever. The material is a double-sided transparent `ShaderMaterial` with `depthWrite: false`, so soft vapor layers over the baked scene without fighting the depth buffer. Uniforms stay minimal: `uTime` and `uPerlinTexture`.
Vertex stage: twist and wind
Twist samples Perlin along a vertical line (uv.y, slowly scrolling with time). That channel becomes a rotation angle per height band; `rotate2D` spins each vertex’s xz around the plume axis. Different heights twist differently, which is what sells the corkscrew.
Wind uses two more Perlin samples (different X offsets) as a 2D offset centered around zero. Strength is multiplied by `pow(uv.y, 2.0)` so the base stays anchored in the cup while the top drifts—like real steam escaping into air. Then the usual `projectionMatrix * modelViewMatrix` projects the deformed plane.
Fragment stage: vapor, not a grey sheet
UVs are scaled down for wider, softer features, and y is shifted by `-uTime` so the pattern rises. A `smoothstep(0.4, 1.0, smoke)` remap kills weak noise, leaving sparse cloudy blobs instead of a full fog rectangle.
Four edge `smoothstep`s on UV x/y feather the plane borders so you never see a hard quad. Color is a warm brownish RGB `(0.6, 0.3, 0.2)` with alpha from smoke density—coffee steam, not white fog. Tone-mapping and colorspace includes keep Three’s color pipeline correct.
Why one noise texture is enough
Noise is a control field: the same map drives twist, wind, and opacity. A subdivided plane acts as a cheap volume billboard—believable plume without particle budgets. Height-dependent deformation keeps the base calm and the top wild. UV scroll plus remapping turns a still image into endless rising vapor; edge feathering keeps the silhouette soft.
Open the live build and watch the steam leave the mug. If the base holds and the top wanders, the experiment did its job: procedural coffee steam from a transparent plane, twisted and wind-blown in the vertex shader, filled with scrolling Perlin vapor in the fragment shader.