Foliage cards for Blender: drop a texture and get a shaded card, assemble a volumetric cluster, bend the profile, animate it in the wind, or paint real 3D plants onto branches.
Ceiba turns a leaf or branch texture into a game-ready foliage card in one drop, with the material decisions that usually take a shader graph and some trial and error already made: which alpha mode survives wind animation, how to light both sides of a single-layer card, how backlit leaves let light through.
| Stage | What it decides | Where it acts |
|---|---|---|
| Card | mesh sized to the texture, pivot at the branch attachment point | new object per dropped image |
| Material | alpha mode, two-sided lighting, translucency, AO, normal, emission | per card |
| Assembly | duplicate into a cross/star cluster around a shared base | selected card |
| Bend | quick angle or a hand-drawn curve profile | selected card(s) |
| Wind | procedural sway, gusts, turbulence, flutter, per-object phase | Geometry Nodes modifier |
| 3D paint | scatter real plant objects onto a mesh's branches | Weight Paint + Geometry Nodes |
Blender 5.0 or newer. Developed and verified against Blender 5.2 LTS.
Drag a texture straight into the 3D viewport. A card appears, sized to the
image, with a complete material already wired. Companion maps next to the
dropped file (_normal, _roughness, _ao, _emission, _translucency,
_alpha...) are picked up automatically by filename suffix.
If the branch does not attach at the bottom-center of the image, open it in the Image Editor, Ceiba tab, and click the attachment point. It becomes the card's origin and the wind's rigid anchor.
- Alpha: Clip (binary threshold, no temporal noise — the right default for anything the wind will animate), Hashed (dithered, only clean on a static card: EEVEE Next needs several accumulated frames to resolve the dither pattern, which a mesh displaced every frame by the wind modifier never lets converge), or Blend (classic, sorting artifacts between overlapping cards).
- Two-sided lighting without duplicated geometry: the normal is flipped on backfacing polygons in the shader graph itself.
- Translucency: a Translucent BSDF mixed in by facing ratio, so a backlit leaf lets light through.
- Base Color × AO, Normal Map, Roughness, Emission, all optional and auto-detected from companion textures.
Cross assembly: duplicate the active card around its shared base, fanned over 180° or spread over a full 360° star, with rotation/scale jitter, optionally joined into one object.
Bend: a signed angle slider for a quick convex/concave arc, or a curve profile drawn by hand in the panel — the same curve widget Blender uses for an image's exposure/tone curve — for a bend that is not a uniform arc.
A Geometry Nodes group (built by the add-on, not hand-authored) layers a
primary sway, slow gusts, low-frequency turbulence, high-frequency flutter,
and a side-to-side component perpendicular to the wind direction. A point
attribute (ceiba_sway, distance from the pivot) keeps the area near the
branch attachment rigid while the rest moves. Every card gets an independent
random phase, so identical cards never swing in sync. The panel's sliders
push live to every card that already carries the modifier.
For volume a flat card cannot give: point Ceiba at a Collection of 3D plant objects, start a paint layer on a branch mesh, and paint density with Blender's own Weight Paint brush — add with the brush, subtract with Ctrl, radius and falloff are the built-in tool, not a custom brush engine. A Geometry Nodes group scatters instances with Poisson-disk spacing, masked by the painted weight, with randomized scale/rotation/tilt and alignment to the surface normal.
Deleting one plant precisely: Realize converts a layer to real geometry,
then in Edit Mode, hover the plant, L to select the island, X to delete
it — no custom picking tool needed.
ceiba/
blender_manifest.toml Extensions Platform manifest
__init__.py bl_info, module registration
properties.py scene PropertyGroups (material, wind, assembly, scatter)
preferences.py add-on preferences
utils.py texture detection, card mesh + pivot, version-safe RNA access
material_builder.py the shader node graph, built per card
geonodes_wind.py builds the "Ceiba Wind" Geometry Nodes group
geonodes_bend.py builds the "Ceiba Curve Bend" Geometry Nodes group
geonodes_scatter.py builds the "Ceiba Scatter" Geometry Nodes group
operators.py every action: create, assemble, bend, wind, paint, realize
file_handler.py drag-and-drop import (bpy.types.FileHandler)
pivot_picker.py click-to-place pivot in the Image Editor
panels.py the Ceiba sidebar tab
Three Geometry Nodes groups are constructed by Python at first use rather
than shipped as a .blend — there is nothing to keep in sync between a
binary asset and the code that configures it, and every socket a modifier
reads is defined next to the node graph that produces it.
Blender 5.0 replaced the old modifier["Socket_id"] = value ID-property
storage for Geometry Nodes modifier inputs with typed RNA properties
(modifier.properties.inputs[...].value = ...). Every place that sets a
modifier input goes through _set_gn_input(), which tries the 5.x path
first and falls back to the old one, so the same code can in principle run
against pre-5.0 Blender — untested, since only 5.2 has been verified.
blender --background --factory-startup --python tests/test_headless.pyRegistration, the three Geometry Nodes groups (expected sockets, connected output), the material graph (alpha wiring, connected output), card mesh generation and pivot math, and the operators that do not need a window (cross assembly, wind, bend, curve bend). The pivot picker and the weight-paint workflow are interactive by nature and are exercised by hand. Exits non-zero on the first failure, so it doubles as a CI step. CI runs it against Blender 5.0 and 5.2.
GPL-3.0-or-later. See LICENSE.
