import bpy
import bmesh
import math
import random
from mathutils import Vector
# ==============================================================================
# 0. CLEANUP & SCENE SETUP
# ==============================================================================
def clean_scene():
if bpy.context.active_object and bpy.context.active_object.mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)
for block in list(bpy.data.meshes): bpy.data.meshes.remove(block)
for block in list(bpy.data.materials): bpy.data.materials.remove(block)
for block in list(bpy.data.lights): bpy.data.lights.remove(block)
for block in list(bpy.data.cameras): bpy.data.cameras.remove(block)
clean_scene()
scene_col = bpy.data.collections.new("DonutAndCoffeeScene")
bpy.context.scene.collection.children.link(scene_col)
def link_to_col(obj):
if obj.name in bpy.context.scene.collection.objects:
bpy.context.scene.collection.objects.unlink(obj)
if obj.name not in scene_col.objects:
scene_col.objects.link(obj)
# ==============================================================================
# 1. MATERIAL PALETTE
# ==============================================================================
def create_material(name, base_color, roughness=0.5, specular=0.5, transmission=0.0):
mat = bpy.data.materials.new(name=name)
mat.use_nodes = True
bsdf = mat.node_tree.nodes.get("Principled BSDF")
bsdf.inputs['Base Color'].default_value = base_color
bsdf.inputs['Roughness'].default_value = roughness
if 'Specular IOR Level' in bsdf.inputs:
bsdf.inputs['Specular IOR Level'].default_value = specular
elif 'Specular' in bsdf.inputs:
bsdf.inputs['Specular'].default_value = specular
if 'Transmission Weight' in bsdf.inputs:
bsdf.inputs['Transmission Weight'].default_value = transmission
elif 'Transmission' in bsdf.inputs:
bsdf.inputs['Transmission'].default_value = transmission
return mat
# Donut Materials
mat_dough = create_material("Mat_Dough", (0.75, 0.48, 0.22, 1.0), roughness=0.8, specular=0.2)
mat_frosting = create_material("Mat_Frosting", (0.92, 0.22, 0.52, 1.0), roughness=0.18, specular=0.8)
# Sprinkle Colors
sprinkle_colors = [
create_material("Sprinkle_Cyan", (0.05, 0.75, 0.95, 1.0), roughness=0.3),
create_material("Sprinkle_Yellow", (0.98, 0.85, 0.08, 1.0), roughness=0.3),
create_material("Sprinkle_White", (0.95, 0.95, 0.95, 1.0), roughness=0.3),
create_material("Sprinkle_Lime", (0.45, 0.9, 0.15, 1.0), roughness=0.3),
create_material("Sprinkle_Purple", (0.6, 0.15, 0.85, 1.0), roughness=0.3),
create_material("Sprinkle_Orange", (0.95, 0.45, 0.05, 1.0), roughness=0.3),
]
# Ceramic Set (Cup, Saucer, Base Plate)
mat_ceramic = create_material("Mat_CeramicTeal", (0.12, 0.52, 0.58, 1.0), roughness=0.15, specular=0.9)
mat_ceramic_rim = create_material("Mat_CeramicWhite", (0.95, 0.95, 0.96, 1.0), roughness=0.12, specular=0.9)
mat_table = create_material("Mat_TableWood", (0.82, 0.78, 0.72, 1.0), roughness=0.45)
# Coffee Liquid & Crema
mat_coffee = create_material("Mat_CoffeeDark", (0.08, 0.04, 0.02, 1.0), roughness=0.08, specular=0.9)
mat_crema = create_material("Mat_CoffeeCrema", (0.48, 0.32, 0.18, 1.0), roughness=0.35, specular=0.4)
# ==============================================================================
# 2. PROCEDURAL DONUT WITH DRIPPING FROSTING & SPRINKLES
# ==============================================================================
donut_pos = Vector((-0.75, -0.3, 0.0))
major_radius = 0.85
minor_radius = 0.45
major_segments = 48
minor_segments = 24
# Base Dough Torus
bpy.ops.mesh.primitive_torus_add(
major_radius=major_radius,
minor_radius=minor_radius,
major_segments=major_segments,
minor_segments=minor_segments,
location=donut_pos
)
dough_obj = bpy.context.active_object
dough_obj.name = "Donut_Dough"
dough_obj.data.materials.append(mat_dough)
bpy.ops.object.shade_smooth()
link_to_col(dough_obj)
# Organic surface displacement on dough
bm = bmesh.new()
bm.from_mesh(dough_obj.data)
for v in bm.verts:
noise_offset = Vector((
math.sin(v.co.x * 5.0) * 0.015,
math.cos(v.co.y * 5.0) * 0.015,
math.sin(v.co.z * 6.0) * 0.012
))
v.co += noise_offset
bm.to_mesh(dough_obj.data)
bm.free()
# Frosting Layer
bpy.ops.mesh.primitive_torus_add(
major_radius=major_radius,
minor_radius=minor_radius + 0.018,
major_segments=major_segments,
minor_segments=minor_segments,
location=donut_pos
)
frosting_obj = bpy.context.active_object
frosting_obj.name = "Donut_Frosting"
frosting_obj.data.materials.append(mat_frosting)
bpy.ops.object.shade_smooth()
link_to_col(frosting_obj)
# Drips carving
bm_f = bmesh.new()
bm_f.from_mesh(frosting_obj.data)
verts_to_delete = []
for v in bm_f.verts:
angle = math.atan2(v.co.y, v.co.x)
drip_wave = math.sin(angle * 6.0) * 0.11 + math.cos(angle * 11.0) * 0.07
cutoff_z = -0.05 + drip_wave
if v.co.z < cutoff_z:
verts_to_delete.append(v)
else:
v.co += v.normal * 0.018
bmesh.ops.delete(bm_f, geom=verts_to_delete, context='VERTS')
bm_f.to_mesh(frosting_obj.data)
bm_f.free()
# Modifiers for Donut
solid_mod = frosting_obj.modifiers.new(name="Solidify", type='SOLIDIFY')
solid_mod.thickness = 0.03
solid_mod.offset = 1.0
subsurf_f = frosting_obj.modifiers.new(name="Subsurf", type='SUBSURF')
subsurf_f.levels = 1
subsurf_f.render_levels = 2
subsurf_d = dough_obj.modifiers.new(name="Subsurf", type='SUBSURF')
subsurf_d.levels = 1
subsurf_d.render_levels = 2
# Sprinkles
random.seed(101)
num_sprinkles = 120
sprinkle_len = 0.12
sprinkle_rad = 0.024
for i in range(num_sprinkles):
theta = random.uniform(0, math.pi * 2)
ring_radius = major_radius + random.uniform(-minor_radius * 0.52, minor_radius * 0.52)
sx = donut_pos.x + math.cos(theta) * ring_radius
sy = donut_pos.y + math.sin(theta) * ring_radius
dist_from_center_ring = ring_radius - major_radius
rad_clamped = max(0.0, (minor_radius ** 2) - (dist_from_center_ring ** 2))
sz = donut_pos.z + math.sqrt(rad_clamped) + 0.04 + random.uniform(-0.004, 0.006)
bpy.ops.mesh.primitive_cylinder_add(
radius=sprinkle_rad,
depth=sprinkle_len,
vertices=10,
location=(sx, sy, sz)
)
sprinkle = bpy.context.active_object
sprinkle.name = f"Sprinkle_{i:03d}"
rot_x = math.radians(random.uniform(-18, 18))
rot_y = math.radians(random.uniform(-18, 18))
rot_z = random.uniform(0, math.pi * 2)
sprinkle.rotation_euler = (rot_x, rot_y, rot_z)
bpy.ops.object.shade_smooth()
bevel = sprinkle.modifiers.new(name="Bevel", type='BEVEL')
bevel.width = 0.01
bevel.segments = 2
sprinkle.data.materials.append(random.choice(sprinkle_colors))
link_to_col(sprinkle)
# ==============================================================================
# 3. PROCEDURAL CERAMIC COFFEE CUP, SAUCER & COFFEE LIQUID
# ==============================================================================
cup_center = Vector((1.2, 0.4, -minor_radius))
cup_height = 1.35
cup_top_radius = 0.72
cup_bottom_radius = 0.52
# 1. Coffee Cup Body
bpy.ops.mesh.primitive_cone_add(
vertices=36,
radius1=cup_top_radius,
radius2=cup_bottom_radius,
depth=cup_height,
location=(cup_center.x, cup_center.y, cup_center.z + cup_height / 2 + 0.04)
)
cup_obj = bpy.context.active_object
cup_obj.name = "Coffee_Cup"
cup_obj.rotation_euler[0] = math.pi # Flip cone so wider rim is at the top
bpy.ops.object.transform_apply(rotation=True)
bpy.ops.object.shade_smooth()
# Hollow out cup using Solidify
cup_solid = cup_obj.modifiers.new(name="CupSolidify", type='SOLIDIFY')
cup_solid.thickness = 0.075
cup_solid.offset = -1.0
cup_subsurf = cup_obj.modifiers.new(name="CupSubsurf", type='SUBSURF')
cup_subsurf.levels = 1
cup_subsurf.render_levels = 2
cup_obj.data.materials.append(mat_ceramic)
link_to_col(cup_obj)
# 2. Cup Handle (Curved Torus segment)
handle_radius = 0.38
bpy.ops.mesh.primitive_torus_add(
major_radius=handle_radius,
minor_radius=0.075,
major_segments=32,
minor_segments=16,
location=(cup_center.x + cup_top_radius * 0.85, cup_center.y, cup_center.z + cup_height * 0.55)
)
handle = bpy.context.active_object
handle.name = "Cup_Handle"
handle.rotation_euler = (math.radians(90), 0, math.radians(12))
bpy.ops.object.shade_smooth()
# Trim inner half of torus so it embeds cleanly into cup wall
bm_h = bmesh.new()
bm_h.from_mesh(handle.data)
verts_h_del = [v for v in bm_h.verts if v.co.x < -0.05]
bmesh.ops.delete(bm_h, geom=verts_h_del, context='VERTS')
bm_h.to_mesh(handle.data)
bm_h.free()
handle.data.materials.append(mat_ceramic)
link_to_col(handle)
# 3. Coffee Liquid Surface
coffee_z = cup_center.z + cup_height - 0.16
coffee_radius = cup_top_radius * 0.88
bpy.ops.mesh.primitive_cylinder_add(
radius=coffee_radius,
depth=0.04,
vertices=36,
location=(cup_center.x, cup_center.y, coffee_z)
)
coffee_obj = bpy.context.active_object
coffee_obj.name = "Coffee_Liquid"
coffee_obj.data.materials.append(mat_coffee)
bpy.ops.object.shade_smooth()
link_to_col(coffee_obj)
# 4. Crema Swirl / Latte Art Center Ring
bpy.ops.mesh.primitive_cylinder_add(
radius=coffee_radius * 0.55,
depth=0.042,
vertices=32,
location=(cup_center.x, cup_center.y, coffee_z + 0.001)
)
crema_obj = bpy.context.active_object
crema_obj.name = "Coffee_Crema"
crema_obj.data.materials.append(mat_crema)
bpy.ops.object.shade_smooth()
link_to_col(crema_obj)
# 5. Ceramic Saucer Under the Cup
saucer_radius = 1.35
saucer_height = 0.14
bpy.ops.mesh.primitive_cylinder_add(
radius=saucer_radius,
depth=saucer_height,
vertices=48,
location=(cup_center.x, cup_center.y, cup_center.z + 0.05)
)
saucer = bpy.context.active_object
saucer.name = "Coffee_Saucer"
saucer.data.materials.append(mat_ceramic)
bpy.ops.object.shade_smooth()
# Saucer Rim Curvature
saucer_bevel = saucer.modifiers.new(name="SaucerBevel", type='BEVEL')
saucer_bevel.width = 0.04
saucer_bevel.segments = 3
link_to_col(saucer)
# ==============================================================================
# 4. TABLETOP, LIGHTING & CAMERA
# ==============================================================================
# Table Surface
bpy.ops.mesh.primitive_cylinder_add(
radius=4.5,
depth=0.1,
vertices=64,
location=(0, 0, -minor_radius - 0.05)
)
table = bpy.context.active_object
table.name = "Breakfast_Table"
table.data.materials.append(mat_table)
bpy.ops.object.shade_smooth()
link_to_col(table)
# Three-Point Lighting Setup
# Key Light (Warm, primary directional)
key_light_data = bpy.data.lights.new(name="Key_Light", type='AREA')
key_light_data.energy = 95.0
key_light_data.size = 3.0
key_light_data.color = (1.0, 0.95, 0.9)
key_light = bpy.data.objects.new(name="Key_Light", object_data=key_light_data)
key_light.location = (3.2, -3.5, 4.0)
key_light.rotation_euler = (math.radians(48), math.radians(12), math.radians(42))
link_to_col(key_light)
# Fill Light (Cool ambient soft shadow filler)
fill_light_data = bpy.data.lights.new(name="Fill_Light", type='AREA')
fill_light_data.energy = 38.0
fill_light_data.size = 4.0
fill_light_data.color = (0.88, 0.94, 1.0)
fill_light = bpy.data.objects.new(name="Fill_Light", object_data=fill_light_data)
fill_light.location = (-3.5, 2.5, 3.2)
fill_light.rotation_euler = (math.radians(-42), math.radians(-18), math.radians(-125))
link_to_col(fill_light)
# Rim Light (Specular pop on ceramic rim and glossy glaze)
rim_light_data = bpy.data.lights.new(name="Rim_Light", type='POINT')
rim_light_data.energy = 55.0
rim_light_data.color = (1.0, 0.92, 0.96)
rim_light = bpy.data.objects.new(name="Rim_Light", object_data=rim_light_data)
rim_light.location = (0.2, 3.2, 2.6)
link_to_col(rim_light)
# Main Scene Camera (Cinematic composition framing both Donut & Coffee Cup)
cam_data = bpy.data.cameras.new(name="Breakfast_Camera")
cam_data.lens = 60
cam_data.clip_end = 100
cam_obj = bpy.data.objects.new(name="Breakfast_Camera", object_data=cam_data)
cam_obj.location = (2.2, -3.6, 2.8)
cam_obj.rotation_euler = (math.radians(55), 0, math.radians(34))
link_to_col(cam_obj)
bpy.context.scene.camera = cam_obj
total_objects = len(bpy.data.collections["DonutAndCoffeeScene"].objects)
print(f"Breakfast scene generated successfully with {total_objects} scene elements!")