|
|
--- |
|
|
datasets: |
|
|
- lamm-mit/Bioinspired3D |
|
|
language: |
|
|
- en |
|
|
base_model: |
|
|
- meta-llama/Llama-3.2-3B-Instruct |
|
|
--- |
|
|
|
|
|
# Bioinspired3D |
|
|
|
|
|
Fine-tuned version of meta-llama/Llama-3.2-3B-Instruct using LoRA adapters for Blender code generation for bioinspired 3D models. |
|
|
|
|
|
## Abstract |
|
|
|
|
|
Generative AI has made rapid progress in text, image, and video synthesis, yet text-to-3D modeling for |
|
|
scientific design remains particularly challenging due to limited controllability and high computational |
|
|
cost. Most existing 3D generative methods rely on meshes, voxels, or point clouds which can be costly |
|
|
to train and difficult to control. We introduce Bioinspired123D, a lightweight and modular code- |
|
|
as-geometry pipeline that generates fabricable 3D structures directly through parametric programs |
|
|
rather than dense visual representations. At the core of Bioinspired123D is Bioinspired3D, a compact |
|
|
language model finetuned to translate natural language design cues into Blender Python scripts |
|
|
encoding smooth, biologically inspired geometries. We curate a domain-specific dataset of over |
|
|
4,000 bioinspired and geometric design scripts spanning helical, cellular, and tubular motifs with |
|
|
parametric variability. The dataset is expanded and validated through an automated LLM-driven, |
|
|
Blender-based quality control pipeline. Bioinspired3D is then embedded in a graph-based agentic |
|
|
framework that integrates multimodal retrieval-augmented generation and a vision–language model |
|
|
critic to iteratively evaluate, critique, and repair generated scripts. We evaluate performance on a new |
|
|
benchmark for 3D geometry script generation and show that Bioinspired123D demonstrates a near |
|
|
fourfold improvement over its unfinetuned base model, while also outperforming substantially larger |
|
|
state-of-the-art language models despite using far fewer parameters and compute. By prioritizing |
|
|
code-as-geometry representations, Bioinspired123D enables compute-efficient, controllable, and |
|
|
interpretable text-to-3D generation, lowering barriers to AI driven scientific discovery in materials |
|
|
and structural design. |
|
|
|
|
|
## What’s in this repo (Hugging Face) |
|
|
|
|
|
This Hugging Face release contains **Bioinspired3D only**: a LoRA adapter that you load on top of the base model to generate **Blender Python scripts from natural-language prompts**. |
|
|
|
|
|
For the full **Bioinspired123D** agentic framework (retrieval + VLM critic + iterative repair), see the GitHub repo: |
|
|
https://github.com/lamm-mit/Bioinspired123D . For training and evaluation scripts, see also the project GitHub. |
|
|
|
|
|
|
|
|
## Usage |
|
|
|
|
|
### Install |
|
|
|
|
|
```bash |
|
|
pip install -U transformers accelerate peft torch |
|
|
``` |
|
|
|
|
|
### Load the base model + LoRA adapter |
|
|
```bash |
|
|
from transformers import AutoTokenizer, AutoModelForCausalLM |
|
|
from peft import PeftModel |
|
|
import torch |
|
|
|
|
|
BASE_MODEL = "meta-llama/Llama-3.2-3B-Instruct" |
|
|
LORA_ADAPTER = "rachelkluu/bioinspired3D" |
|
|
|
|
|
# Set this to your preferred device, e.g. "cuda:0" or "cpu" |
|
|
DEVICE_3D = "cuda:0" |
|
|
|
|
|
bio3d_tok = AutoTokenizer.from_pretrained(BASE_MODEL) |
|
|
|
|
|
base_model = AutoModelForCausalLM.from_pretrained( |
|
|
BASE_MODEL, |
|
|
torch_dtype=torch.float16, |
|
|
device_map={"": DEVICE_3D}, |
|
|
) |
|
|
|
|
|
bio3d_model = PeftModel.from_pretrained(base_model, LORA_ADAPTER) |
|
|
bio3d_model.eval() |
|
|
|
|
|
def format_input(prompt: str) -> str: |
|
|
return ( |
|
|
"<|begin_of_text|><|start_header_id|>system<|end_header_id|>\n\n" |
|
|
"You are a helpful assistant<|eot_id|>" |
|
|
"<|start_header_id|>user<|end_header_id|>\n\n" |
|
|
f"{prompt}<|eot_id|>" |
|
|
"<|start_header_id|>assistant<|end_header_id|>\n\n" |
|
|
) |
|
|
``` |
|
|
### Load utility functions |
|
|
```bash |
|
|
def extract_blender_code(model_out: str) -> str: |
|
|
matches = list(re.finditer(r"```python\s*(.*?)```", model_out, flags=re.DOTALL)) |
|
|
if matches: |
|
|
return matches[-1].group(1).strip() |
|
|
pos = model_out.rfind("import bpy") |
|
|
return model_out[pos:].strip() if pos != -1 else model_out.strip() |
|
|
|
|
|
|
|
|
def clean_blender_code(text: str) -> str: |
|
|
if not text: |
|
|
return "import bpy" |
|
|
code = text.strip() |
|
|
code = code.replace("```python", "").replace("```", "") |
|
|
code = re.sub(r"[\x00-\x08\x0b-\x1f]", "", code) |
|
|
if not code.lstrip().startswith("import bpy"): |
|
|
code = "import bpy\n" + code |
|
|
return code |
|
|
``` |
|
|
### Generate Blender code from a natural-language prompt |
|
|
```bash |
|
|
prompt = """Write Blender code to make a cellular structure.""" |
|
|
|
|
|
formatted = format_input(prompt) |
|
|
inputs = bio3d_tok(formatted, return_tensors="pt").to(bio3d_model.device) |
|
|
|
|
|
with torch.no_grad(): |
|
|
outputs = bio3d_model.generate( |
|
|
**inputs, |
|
|
max_new_tokens=2048, |
|
|
do_sample=True, |
|
|
temperature=0.1, |
|
|
top_p=0.9, |
|
|
) |
|
|
|
|
|
raw = bio3d_tok.decode(outputs[0], skip_special_tokens=True) |
|
|
raw_code = extract_blender_code(raw) |
|
|
blender_code = clean_blender_code(raw_code) |
|
|
|
|
|
print(blender_code) |
|
|
``` |
|
|
### Prompting tips |
|
|
|
|
|
Input: Natural language design intent (for example: “tubular structure with noisy placement”, “helical material with cylindrical fibers”, “smoothed cellular structure”). |
|
|
|
|
|
Output: A Blender Python script (intended to be executed in Blender) that constructs the requested geometry. |
|
|
|
|
|
To encourage explicit reasoning, append a variant of: “Think step by step.” to the end of your prompt. For example: "Write Blender code to make a tubular structure with z-aligned tubules. Think step by step." |
|
|
|
|
|
## Sample output #1 |
|
|
|
|
|
### Example prompt |
|
|
```text |
|
|
Write Blender code to make a cellular structure with smooth curves and layers on top and bottom |
|
|
``` |
|
|
### Example model output (Blender Python) |
|
|
```python |
|
|
import bpy |
|
|
import addon_utils |
|
|
|
|
|
# Clear the scene |
|
|
bpy.ops.object.select_all(action='SELECT') |
|
|
bpy.ops.object.delete(use_global=False) |
|
|
|
|
|
# Parameters for the sandwich structure |
|
|
strut_thickness = 0.2 # Thickness of the struts |
|
|
smoothness_level = 3 # Level of smoothness |
|
|
slab_thickness = 0.1 # Thickness of the top and bottom slabs |
|
|
|
|
|
def create_cellular_sandwich(): |
|
|
# Function to create a smooth cellular sandwich structure. |
|
|
# Steps: |
|
|
# 1. Create a cube. |
|
|
# 2. Fracture the cube into pieces. |
|
|
# 3. Apply wireframe modifier for cellular effect. |
|
|
# 4. Apply subsurface modifier for smoothing. |
|
|
# 5. Create top and bottom slabs. |
|
|
|
|
|
# Enable the Fracture Cell add-on |
|
|
addon_utils.enable('object_fracture_cell') |
|
|
|
|
|
# Create a cube |
|
|
bpy.ops.mesh.primitive_cube_add() |
|
|
cube = bpy.context.active_object |
|
|
|
|
|
# Scale the cube |
|
|
cube.scale = (1, 1, 0.75) |
|
|
bpy.ops.object.transform_apply(scale=True) |
|
|
|
|
|
# Fracture the cube |
|
|
bpy.ops.object.add_fracture_cell_objects( |
|
|
source={'PARTICLE_OWN'}, |
|
|
source_limit=100, |
|
|
source_noise=0.5, |
|
|
cell_scale=(1, 1, 1), |
|
|
recursion=0, |
|
|
recursion_source_limit=8, |
|
|
recursion_clamp=250, |
|
|
recursion_chance=0.25, |
|
|
recursion_chance_select='SIZE_MIN', |
|
|
use_smooth_faces=False, |
|
|
use_sharp_edges=True, |
|
|
use_sharp_edges_apply=True, |
|
|
use_data_match=True, |
|
|
use_island_split=True, |
|
|
margin=0.001, |
|
|
material_index=0, |
|
|
use_interior_vgroup=False, |
|
|
mass_mode='VOLUME', |
|
|
mass=1, |
|
|
use_recenter=True, |
|
|
use_remove_original=True, |
|
|
collection_name='', |
|
|
use_debug_points=False, |
|
|
use_debug_redraw=True, |
|
|
use_debug_bool=False |
|
|
) |
|
|
|
|
|
# Remove the original cube |
|
|
if cube and cube.name in bpy.data.objects: |
|
|
bpy.data.objects.remove(cube, do_unlink=True) |
|
|
|
|
|
# Join fractured pieces |
|
|
bpy.ops.object.select_all(action='DESELECT') |
|
|
for obj in bpy.context.scene.objects: |
|
|
if obj.type == 'MESH': |
|
|
obj.select_set(True) |
|
|
bpy.context.view_layer.objects.active = obj |
|
|
bpy.ops.object.join() |
|
|
|
|
|
# Clean up geometry |
|
|
active_mesh = bpy.context.view_layer.objects.active |
|
|
bpy.ops.object.mode_set(mode='EDIT') |
|
|
bpy.ops.mesh.select_all(action='SELECT') |
|
|
bpy.ops.mesh.remove_doubles(threshold=0.2) |
|
|
bpy.ops.mesh.dissolve_limited() |
|
|
bpy.ops.object.mode_set(mode='OBJECT') |
|
|
|
|
|
# Wireframe Modifier |
|
|
wireframe_mod = active_mesh.modifiers.new(name='Wireframe', type='WIREFRAME') |
|
|
wireframe_mod.thickness = strut_thickness |
|
|
wireframe_mod.offset = -1 |
|
|
wireframe_mod.use_even_offset = False |
|
|
wireframe_mod.use_relative_offset = True |
|
|
wireframe_mod.use_replace = True |
|
|
bpy.ops.object.convert(target='MESH') |
|
|
|
|
|
# Subsurface Modifier |
|
|
subsurf_mod = active_mesh.modifiers.new(name='Subdivision', type='SUBSURF') |
|
|
subsurf_mod.levels = smoothness_level |
|
|
subsurf_mod.render_levels = smoothness_level |
|
|
|
|
|
# Create slabs |
|
|
slab_height = 1.5 |
|
|
bottom_slab_z = -slab_height / 2 - slab_thickness / 2 |
|
|
top_slab_z = slab_height / 2 + slab_thickness / 2 |
|
|
|
|
|
# Bottom slab |
|
|
bpy.ops.mesh.primitive_cube_add(size=2, location=(0, 0, bottom_slab_z)) |
|
|
bottom_slab = bpy.context.active_object |
|
|
bottom_slab.scale = (1, 1, slab_thickness / 2) |
|
|
bpy.ops.object.transform_apply(scale=True) |
|
|
|
|
|
# Top slab |
|
|
bpy.ops.mesh.primitive_cube_add(size=2, location=(0, 0, top_slab_z)) |
|
|
top_slab = bpy.context.active_object |
|
|
top_slab.scale = (1, 1, slab_thickness / 2) |
|
|
bpy.ops.object.transform_apply(scale=True) |
|
|
|
|
|
# Create the sandwich structure |
|
|
create_cellular_sandwich() |
|
|
``` |
|
|
### Rendered example (image) |
|
|
 |
|
|
|
|
|
|
|
|
## Sample output #2 |
|
|
### Example prompt |
|
|
```text |
|
|
Write Blender code to make a helical bioinspired structure with flat rectangular struts in each layer |
|
|
``` |
|
|
### Example model output (Blender Python) |
|
|
```python |
|
|
import bpy |
|
|
import math |
|
|
|
|
|
bpy.ops.object.select_all(action='SELECT') |
|
|
bpy.ops.object.delete(use_global=False) |
|
|
|
|
|
# Parameters for the helical fiber structure |
|
|
ply_count = 7 # Number of plies in the structure |
|
|
angle_increment = 10 # Rotation angle for each ply |
|
|
fiber_form ='rect' # Shape of the fibers |
|
|
|
|
|
# Dimensions of the structure |
|
|
structure_height = 2 # Total height of the structure |
|
|
plate_dimension = 2 # Width and length of each plate |
|
|
thickness_per_ply = structure_height / ply_count # Thickness of each ply |
|
|
fiber_gap = 0.01 # Gap between fibers |
|
|
|
|
|
def create_fiber(form, diameter, x, y, z, thickness): |
|
|
if form =='rect': |
|
|
bpy.ops.mesh.primitive_cube_add(size=1.0, location=(x, y, z), rotation=(0, math.radians(90), 0)) |
|
|
fiber = bpy.context.object |
|
|
fiber.dimensions = (thickness, diameter, plate_dimension) |
|
|
return fiber |
|
|
|
|
|
def generate_ply(index): |
|
|
# Generate a ply of fibers in a helical arrangement. |
|
|
z_position = index * thickness_per_ply |
|
|
rotation_angle = index * angle_increment |
|
|
|
|
|
bpy.ops.object.empty_add(type='PLAIN_AXES', location=(0, 0, z_position)) |
|
|
empty_object = bpy.context.object |
|
|
|
|
|
fiber_diameter = thickness_per_ply |
|
|
fiber_distance = fiber_diameter + fiber_gap |
|
|
fiber_count = max(1, int(plate_dimension / fiber_distance)) |
|
|
|
|
|
total_fiber_space = fiber_count * fiber_distance |
|
|
start_y_position = -plate_dimension / 2 + fiber_distance / 2 + (plate_dimension - total_fiber_space) / 2 |
|
|
|
|
|
for i in range(fiber_count): |
|
|
fiber_y_center = start_y_position + i * fiber_distance |
|
|
fiber_instance = create_fiber(fiber_form, fiber_diameter, 0, fiber_y_center, z_position, thickness_per_ply) |
|
|
fiber_instance.parent = empty_object |
|
|
fiber_instance.matrix_parent_inverse = empty_object.matrix_world.inverted() |
|
|
|
|
|
empty_object.rotation_euler[2] = math.radians(rotation_angle) |
|
|
return empty_object |
|
|
|
|
|
# Create the helical structure |
|
|
for i in range(ply_count): |
|
|
generate_ply(i) |
|
|
``` |
|
|
|
|
|
### Rendered example (image) |
|
|
 |
|
|
|
|
|
|
|
|
## Notes: |
|
|
This adapter is meant to be used with the specified base model. Generated scripts should be treated like code: run in a sandboxed environment and validate geometry as needed. |
|
|
|
|
|
## Citation |
|
|
|
|
|
If you use Bioinspired3D or the broader Bioinspired123D framework in your work, please cite: |
|
|
|
|
|
```bibtex |
|
|
@article{luu2026bioinspired123d, |
|
|
title={Bioinspired123D: Generative 3D Modeling System for Bioinspired Structures}, |
|
|
author={Luu, Rachel K. and Buehler, Markus J.}, |
|
|
year={2026} |
|
|
} |
|
|
|