3D Object Gallery Hero (PBR, IBL, shadows)
A hero for projects that ask for real-time 3D on the web. Three specimens — polished metal, glazed ceramic and cast glass — can be swapped to show material work, yet the block ships no model files and no texture images: the shapes are generated in code with TorusKnot, Lathe (the vessel profile comes from a function) and Extrude (rounded shape with a bevel). Reflections come from a studio built in code — a white box with four emissive panels — folded into an environment map by PMREMGenerator.fromScene(), so PBR reflections read as real area lights without loading an HDRI, and the amber back panel tints the highlights on brand. Three lights are used: a shadow-casting directional key with PCF shadows, an amber point light for the rim, and a hemisphere fill. The only shadow catcher is a ShadowMaterial floor, which keeps the white page clean while grounding the object. Entrance, exit and float are driven by an AnimationMixer: keyframe tracks such as VectorKeyframeTrack('.scale') are assembled by hand, so a timeline is available even without an animated model. The specimen switcher lives in the hairline ledger below the canvas as real buttons with aria-pressed, so everything is reachable by keyboard. Adding three is the single exception to this gallery's zero-dependency rule, taken only because PBR materials, environment maps, shadows and animation are not worth hand-writing in raw WebGL; anything raw WebGL covers well (planes, shader backdrops, particles) stays dependency-free elsewhere. The measured cost of the exception (three 0.185, production build) is one extra chunk of 725,217 bytes (~708KB), 181,641 bytes (~177KB) gzipped, and it is never imported statically: a dynamic import inside useEffect splits it into its own chunk that loads only where 3D is actually shown (scaled-down previews are detected via rect.width / offsetWidth and never load it). Under prefers-reduced-motion neither the auto-rotation nor the timeline runs and a single frame is drawn; when WebGL is unavailable, the context is lost or the frame budget is missed, it falls back to a static panel.