qwen3.8-flash · round 3 of 3 · seed 3$0.03MP4GLBscene.py
scene.py · 475 lines
# Cafe table scene: espresso cup on saucer, spoon, sugar cubes, croissant.
# Built only from primitive meshes (cube, cylinder, cone, sphere, torus).
# Meters, real-world scale: table top surface at z = 0.

import math
from math import radians, sin, cos, pi

import bpy

D = bpy.data


# ---------------------------------------------------------------- utilities
def wipe_scene():
    for o in list(D.objects):
        D.objects.remove(o, do_unlink=True)
    for block in (D.meshes, D.materials, D.lights, D.cameras):
        for b in list(block):
            block.remove(b)


def link(obj):
    bpy.context.scene.collection.objects.link(obj)
    return obj


def smooth(obj, auto=True, angle=radians(35)):
    for p in obj.data.polygons:
        p.use_smooth = True
    if auto and hasattr(obj.data, "use_auto_smooth"):
        obj.data.use_auto_smooth = True
        obj.data.auto_smooth_angle = angle
    return obj


def setmat(obj, mat):
    if mat is not None:
        obj.data.materials.clear()
        obj.data.materials.append(mat)
    return obj


def new_material(name):
    m = D.materials.new(name)
    m.use_nodes = True
    return m


def bsdf_of(m):
    for n in m.node_tree.nodes:
        if n.type == 'BSDF_PRINCIPLED':
            return n
    return None


def set_in(node, name, value):
    if name in node.inputs:
        node.inputs[name].default_value = value
        return True
    return False


def simple_material(name, color, rough=0.5, metal=0.0, transmission=0.0,
                    ior=1.45, coat=0.0, subsurface=0.0):
    m = new_material(name)
    b = bsdf_of(m)
    set_in(b, "Base Color", (color[0], color[1], color[2], 1.0))
    set_in(b, "Roughness", rough)
    set_in(b, "Metallic", metal)
    set_in(b, "Transmission Weight", transmission)
    set_in(b, "IOR", ior)
    set_in(b, "Coat Weight", coat) or set_in(b, "Clearcoat", coat)
    set_in(b, "Subsurface Weight", subsurface) or set_in(b, "Subsurface", subsurface)
    if subsurface:
        set_in(b, "Subsurface Radius", (0.01, 0.01, 0.01))
    return m


def join(objs, name):
    bpy.ops.object.select_all(action='DESELECT')
    for o in objs:
        o.select_set(True)
    bpy.context.view_layer.objects.active = objs[0]
    bpy.ops.object.join()
    r = bpy.context.active_object
    r.name = name
    # bake everything into world space so later moves/rotations behave
    bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
    return r


def place(obj, x, y, rot_deg=0.0, z=0.0):
    """Move obj so its bounding-box centre lands on (x, y) after a Z rotation."""
    bb = [tuple(c) for c in obj.bound_box]
    cx = sum(v[0] for v in bb) / 8.0
    cy = sum(v[1] for v in bb) / 8.0
    cz = sum(v[2] for v in bb) / 8.0
    r = radians(rot_deg)
    px = cos(r) * cx - sin(r) * cy
    py = sin(r) * cx + cos(r) * cy
    obj.rotation_euler = (0, 0, r)
    obj.location = (x - px, y - py, z - cz)
    return obj


def aim_z(obj, direction, up=(0, 0, 1)):
    """Rotate obj so its local +Z points along `direction`."""
    import mathutils
    q = mathutils.Vector(direction).to_track_quat('Z', 'Y')
    obj.rotation_euler = q.to_euler()
    return obj


# ------------------------------------------------------------- primitives
def cube(name, size=1.0, loc=(0, 0, 0), rot=(0, 0, 0), scale=(1, 1, 1), mat=None, smooth_=False):
    bpy.ops.mesh.primitive_cube_add(size=size, location=loc, rotation=rot)
    o = bpy.context.active_object
    o.name = name
    o.scale = scale
    setmat(o, mat)
    if smooth_:
        smooth(o, auto=False)
    return o


def cylinder(name, r=1.0, d=1.0, loc=(0, 0, 0), rot=(0, 0, 0), scale=(1, 1, 1),
             mat=None, verts=64, fill='NGON'):
    bpy.ops.mesh.primitive_cylinder_add(vertices=verts, radius=r, depth=d,
                                        location=loc, rotation=rot, end_fill_type=fill)
    o = bpy.context.active_object
    o.name = name
    o.scale = scale
    setmat(o, mat)
    smooth(o)
    return o


def cone(name, r1=1.0, r2=0.0, d=1.0, loc=(0, 0, 0), rot=(0, 0, 0), scale=(1, 1, 1),
         mat=None, verts=64, fill='NGON'):
    bpy.ops.mesh.primitive_cone_add(vertices=verts, radius1=r1, radius2=r2, depth=d,
                                    location=loc, rotation=rot, end_fill_type=fill)
    o = bpy.context.active_object
    o.name = name
    o.scale = scale
    setmat(o, mat)
    smooth(o)
    return o


def sphere(name, r=1.0, loc=(0, 0, 0), scale=(1, 1, 1), mat=None, seg=32, ring=16):
    bpy.ops.mesh.primitive_uv_sphere_add(segments=seg, ring_count=ring, radius=r, location=loc)
    o = bpy.context.active_object
    o.name = name
    o.scale = scale
    setmat(o, mat)
    smooth(o)
    return o


def torus(name, R=1.0, r=0.1, loc=(0, 0, 0), rot=(0, 0, 0), scale=(1, 1, 1),
          mat=None, maj=64, mino=24):
    bpy.ops.mesh.primitive_torus_add(major_segments=maj, minor_segments=mino,
                                     major_radius=R, minor_radius=r, location=loc,
                                     rotation=rot)
    o = bpy.context.active_object
    o.name = name
    o.scale = scale
    setmat(o, mat)
    smooth(o, angle=radians(45))
    return o


# ---------------------------------------------------------------- materials
def wood_material():
    m = new_material("TableWood")
    nt = m.node_tree
    n, l = nt.nodes, nt.links
    b = bsdf_of(m)
    tex = n.new("ShaderNodeTexCoord")
    mapping = n.new("ShaderNodeMapping")
    mapping.inputs["Scale"].default_value = (1.0, 6.0, 6.0)
    wave = n.new("ShaderNodeTexWave")
    wave.wave_type = 'BANDS'
    wave.bands_direction = 'Z'
    wave.inputs["Scale"].default_value = 0.55
    wave.inputs["Distortion"].default_value = 5.5
    wave.inputs["Detail"].default_value = 2.0
    wave.inputs["Detail Scale"].default_value = 1.4
    noise = n.new("ShaderNodeTexNoise")
    noise.inputs["Scale"].default_value = 9.0
    noise.inputs["Detail"].default_value = 6.0
    ramp = n.new("ShaderNodeValToRGB")
    ramp.color_ramp.elements[0].position = 0.25
    ramp.color_ramp.elements[0].color = (0.155, 0.062, 0.026, 1)
    ramp.color_ramp.elements[1].position = 0.85
    ramp.color_ramp.elements[1].color = (0.335, 0.150, 0.062, 1)
    mix = n.new("ShaderNodeMixRGB")
    mix.blend_type = 'MULTIPLY'
    mix.inputs["Fac"].default_value = 0.35
    l.new(tex.outputs["Object"], mapping.inputs["Vector"])
    l.new(mapping.outputs["Vector"], wave.inputs["Vector"])
    l.new(mapping.outputs["Vector"], noise.inputs["Vector"])
    l.new(wave.outputs["Color"], ramp.inputs["Fac"])
    l.new(ramp.outputs["Color"], mix.inputs["Color1"])
    l.new(noise.outputs["Color"], mix.inputs["Color2"])
    l.new(mix.outputs["Color"], b.inputs["Base Color"])
    set_in(b, "Roughness", 0.28)
    bump = n.new("ShaderNodeBump")
    bump.inputs["Strength"].default_value = 0.06
    l.new(wave.outputs["Color"], bump.inputs["Height"])
    l.new(bump.outputs["Normal"], b.inputs["Normal"])
    return m


def dough_material():
    m = new_material("CroissantDough")
    nt = m.node_tree
    n, l = nt.nodes, nt.links
    b = bsdf_of(m)
    tex = n.new("ShaderNodeTexCoord")
    noise = n.new("ShaderNodeTexNoise")
    noise.inputs["Scale"].default_value = 26.0
    noise.inputs["Detail"].default_value = 8.0
    noise.inputs["Roughness"].default_value = 0.62
    ramp = n.new("ShaderNodeValToRGB")
    cr = ramp.color_ramp
    cr.elements[0].position = 0.30
    cr.elements[0].color = (0.30, 0.115, 0.026, 1)
    cr.elements[1].position = 0.62
    cr.elements[1].color = (0.63, 0.30, 0.075, 1)
    e = cr.elements.new(0.85)
    e.color = (0.80, 0.47, 0.16, 1)
    l.new(tex.outputs["Object"], noise.inputs["Vector"])
    l.new(noise.outputs["Color"], ramp.inputs["Fac"])
    l.new(ramp.outputs["Color"], b.inputs["Base Color"])
    set_in(b, "Roughness", 0.42)
    set_in(b, "Specular IOR Level", 0.35) or set_in(b, "Specular", 0.35)
    set_in(b, "Sheen Weight", 0.25) or set_in(b, "Sheen", 0.25)
    bump = n.new("ShaderNodeBump")
    bump.inputs["Strength"].default_value = 0.35
    bump.inputs["Distance"].default_value = 0.0012
    l.new(noise.outputs["Color"], bump.inputs["Height"])
    l.new(bump.outputs["Normal"], b.inputs["Normal"])
    return m


def sugar_material():
    m = new_material("Sugar")
    b = bsdf_of(m)
    set_in(b, "Base Color", (0.86, 0.86, 0.84, 1))
    set_in(b, "Roughness", 0.42)
    set_in(b, "Transmission Weight", 0.12) or set_in(b, "Transmission", 0.12)
    set_in(b, "IOR", 1.55)
    return m


# -------------------------------------------------------------------- build
wipe_scene()
scene = bpy.context.scene
scene.unit_settings.system = 'METRIC'

porcelain = simple_material("Porcelain", (0.78, 0.775, 0.765), rough=0.06, coat=0.35, ior=1.5)
porcelain_warm = simple_material("PorcelainWarm", (0.76, 0.755, 0.74), rough=0.08, coat=0.3)
crema = simple_material("EspressoCrema", (0.28, 0.135, 0.045), rough=0.22, coat=0.15)
steel = simple_material("Steel", (0.72, 0.73, 0.75), rough=0.11, metal=1.0)
wood = wood_material()
dough = dough_material()
sugar = sugar_material()
crumb_mat = simple_material("Crumb", (0.55, 0.30, 0.10), rough=0.6)
dark = simple_material("Dark", (0.02, 0.02, 0.02), rough=0.6)

# ---- table (round cafe table top, its edge stays out of frame)
table_top = cylinder("TableTop", r=0.45, d=0.035, loc=(0, 0, -0.0175), mat=wood, verts=128)
torus("TableEdge", R=0.4495, r=0.004, loc=(0, 0, -0.004), mat=wood)
cylinder("TablePedestal", r=0.045, d=0.70, loc=(0, 0, -0.385), mat=dark, verts=48)
cylinder("TableFoot", r=0.17, d=0.02, loc=(0, 0, -0.735), mat=dark, verts=64)

# room beyond the table edge (visible at the top of the frame)
cube("Floor", size=1.0, loc=(0, 0, -0.755), scale=(4, 4, 0.05),
     mat=simple_material("Floor", (0.13, 0.12, 0.115), rough=0.7))
cube("Wall", size=1.0, loc=(0, 0.95, 0.55), scale=(4, 0.06, 1.9),
     mat=simple_material("Wall", (0.34, 0.285, 0.235), rough=0.85))
cube("WallTrim", size=1.0, loc=(0, 0.93, -0.10), scale=(4, 0.05, 0.16),
     mat=simple_material("Trim", (0.16, 0.12, 0.085), rough=0.6))

# ---- saucer
saucer_top = 0.0065           # z of the well floor the cup stands on
parts = []
parts.append(cylinder("SaucerFoot", r=0.0455, d=0.0045, loc=(0, 0, 0.00225),
                      mat=porcelain_warm))
parts.append(cylinder("SaucerWell", r=0.047, d=0.0025, loc=(0, 0, saucer_top - 0.00125),
                      mat=porcelain_warm))
parts.append(cone("SaucerRim", r1=0.045, r2=0.0755, d=0.0125,
                  loc=(0, 0, saucer_top + 0.0035), mat=porcelain_warm,
                  fill='NOTHING', verts=96))
parts.append(torus("SaucerLip", R=0.0755, r=0.0022, loc=(0, 0, saucer_top + 0.0097),
                   mat=porcelain_warm))
saucer = join(parts, "Saucer")
smooth(saucer)


def move(obj, dx, dy, dz=0.0):
    obj.location.x += dx
    obj.location.y += dy
    obj.location.z += dz
    return obj

# ---- espresso cup (hollow shell: outer wall, inner wall, floor, rim torus)
z0 = saucer_top + 0.0005
H = 0.052
Ro_b, Ro_t = 0.0235, 0.0325        # outer radius bottom / top
Ri_b, Ri_t = 0.0205, 0.0295        # inner radius bottom / top
cparts = []
cparts.append(cone("CupOuter", r1=Ro_b, r2=Ro_t, d=H, loc=(0, 0, z0 + H / 2),
                   mat=porcelain, fill='NOTHING', verts=96))
cparts.append(cone("CupInner", r1=Ri_b, r2=Ri_t, d=H - 0.005,
                   loc=(0, 0, z0 + 0.005 + (H - 0.005) / 2),
                   mat=porcelain, fill='NOTHING', verts=96))
cparts.append(cone("CupFloor", r1=Ri_b, r2=Ri_b - 0.001, d=0.005,
                   loc=(0, 0, z0 + 0.0025), mat=porcelain, verts=96))
cparts.append(cylinder("CupBase", r=Ro_b + 0.0015, d=0.004, loc=(0, 0, z0 + 0.002),
                       mat=porcelain, verts=96))
rim_mid = 0.5 * (Ro_t + Ri_t)
rim_minor = 0.5 * (Ro_t - Ri_t)
cparts.append(torus("CupRim", R=rim_mid, r=rim_minor, loc=(0, 0, z0 + H), mat=porcelain))
# handle: torus ring in the XZ plane, biting into the outer wall
hx = Ro_t - 0.005
cparts.append(torus("CupHandle", R=0.0165, r=0.0046, loc=(hx, 0, z0 + 0.027),
                    rot=(radians(90), 0, 0), scale=(1.0, 1.18, 1.0)))
cup = join(cparts, "EspressoCup")
smooth(cup)

# ---- group placement on the table
CUP_POS = (0.030, -0.028)
move(saucer, *CUP_POS)
move(cup, *CUP_POS)

# espresso surface, a little below the rim
z_liq = z0 + H - 0.010
slope = (Ri_t - Ri_b) / (H - 0.005)
r_liq = Ri_b + slope * (z_liq - (z0 + 0.005)) + 0.0008
liquid = cylinder("Espresso", r=r_liq, d=0.0025, loc=(0, 0, z_liq), mat=crema, verts=96)
move(liquid, *CUP_POS)

# ---- spoon, lying on the table to the right of the saucer
sp_parts = []
sp_parts.append(sphere("SpoonBowl", r=1.0, loc=(0.0, 0, 0.0046),
                       scale=(0.0210, 0.0142, 0.0040), mat=steel, seg=32, ring=16))
sp_parts.append(cube("SpoonNeck", size=1.0, loc=(0.030, 0, 0.0024),
                     scale=(0.022, 0.0055, 0.0011), mat=steel))
sp_parts.append(cube("SpoonHandle", size=1.0, loc=(0.085, 0, 0.0022),
                     scale=(0.055, 0.0068, 0.0011), mat=steel))
sp_parts.append(sphere("SpoonTip", r=1.0, loc=(0.140, 0, 0.0022),
                       scale=(0.0075, 0.0055, 0.0012), mat=steel))
spoon = join(sp_parts, "Spoon")
place(spoon, 0.135, -0.030, rot_deg=-52)
smooth(spoon)

# ---- sugar cubes, left-front of the saucer
cube_s = 0.0135
sugar_positions = [(-0.098, -0.104, 0, 14),
                   (-0.121, -0.088, 0, -22),
                   (-0.110, -0.099, 1, 34)]
for i, (sx, sy, lvl, rot) in enumerate(sugar_positions):
    z = cube_s / 2 + lvl * cube_s
    c = cube("SugarCube%d" % i, size=cube_s, loc=(sx, sy, z + (0.0008 if lvl else 0.0)),
             rot=(0, 0, radians(rot)), mat=sugar)
    smooth(c, auto=False)
# a few sugar grains
for i, (gx, gy) in enumerate([(-0.082, -0.118), (-0.134, -0.076), (-0.090, -0.075),
                              (-0.128, -0.112)]):
    sphere("SugarGrain%d" % i, r=0.0011, loc=(gx, gy, 0.0009), mat=sugar, seg=12, ring=8)

# ---- croissant: chain of squashed spheres along an arc + two cone tips
arc_R = 0.042
a0, a1 = 208, 332
prof = [0.30, 0.52, 0.72, 0.88, 1.00, 1.00, 0.88, 0.72, 0.52, 0.30]
r_max = 0.0185
cp = []
n = len(prof)
for i, f in enumerate(prof):
    t = a0 + (a1 - a0) * (i / (n - 1))
    th = radians(t)
    r = r_max * (0.42 + 0.58 * f)
    x = arc_R * cos(th)
    y = arc_R * sin(th)
    squash = 0.70 + 0.10 * f
    cp.append(sphere("CroissantSeg%d" % i, r=r, loc=(x, y, r * squash * 0.98),
                     scale=(1.0, 1.0, squash), mat=dough, seg=28, ring=16))
for side, i in ((1, 0), (-1, n - 1)):
    t = a0 if side == 1 else a1
    th = radians(t)
    tang = (-sin(th), cos(th))                 # direction of increasing angle
    out = (tang[0] * side, tang[1] * side)     # outward along the crescent
    base = (arc_R * cos(th), arc_R * sin(th))
    r_end = r_max * (0.42 + 0.58 * prof[i])
    L = 0.024
    c = cone("CroissantTip%d" % i, r1=r_end * 0.95, r2=0.0014, d=L,
             loc=(0, 0, 0), mat=dough, verts=32)
    aim_z(c, (out[0], out[1], -0.35))
    c.location = (base[0] + out[0] * (L * 0.42),
                  base[1] + out[1] * (L * 0.42), r_end * 0.72)
    cp.append(c)
croissant = join(cp, "Croissant")
place(croissant, -0.120, 0.062, rot_deg=196)
smooth(croissant)

# crumbs
for i, (cx, cy, cr) in enumerate([(-0.062, 0.014, 0.0022), (-0.042, 0.042, 0.0016),
                                  (-0.075, -0.006, 0.0019), (-0.028, 0.068, 0.0013),
                                  (-0.150, 0.014, 0.0017)]):
    sphere("Crumb%d" % i, r=cr, loc=(cx, cy, cr * 0.7), mat=crumb_mat, seg=14, ring=10)

# --------------------------------------------------------------- camera etc.
cam_data = D.cameras.new("Camera")
cam_data.lens = 50
cam_data.sensor_width = 36
cam_data.dof.use_dof = True
cam_data.dof.focus_distance = 0.50
cam_data.dof.aperture_fstop = 8.0
cam = bpy.data.objects.new("Camera", cam_data)
link(cam)
scene.camera = cam
import mathutils
target = mathutils.Vector((-0.005, 0.005, 0.026))
cam.location = (0.275, -0.360, 0.235)
cam.rotation_euler = (target - cam.location).to_track_quat('-Z', 'Y').to_euler()

world = D.worlds.new("World") if not D.worlds else D.worlds[0]
world.use_nodes = True
bg = world.node_tree.nodes.get("Background")
bg.inputs[0].default_value = (0.28, 0.30, 0.34, 1)
bg.inputs[1].default_value = 0.12


def add_light(name, kind, loc, energy, color=(1, 1, 1), size=0.3, target_pt=(0, 0, 0),
              rotation=None):
    ld = D.lights.new(name, kind)
    ld.energy = energy
    ld.color = color
    if kind == 'AREA':
        ld.shape = 'RECTANGLE'
        ld.size = size
        ld.size_y = size * 0.7
    elif kind == 'SUN':
        ld.angle = radians(3)
    o = bpy.data.objects.new(name, ld)
    link(o)
    o.location = loc
    if rotation is not None:
        o.rotation_euler = rotation
    else:
        d = mathutils.Vector(target_pt) - mathutils.Vector(loc)
        o.rotation_euler = d.to_track_quat('-Z', 'Y').to_euler()
    return o


add_light("Key", 'AREA', (0.38, -0.34, 0.46), 7.5, (1.0, 0.96, 0.90), size=0.45,
          target_pt=(0.0, 0.0, 0.02))
add_light("Fill", 'AREA', (-0.42, -0.15, 0.30), 2.2, (0.90, 0.94, 1.0), size=0.5,
          target_pt=(0.0, 0.02, 0.02))
add_light("Rim", 'AREA', (-0.18, 0.45, 0.30), 3.5, (1.0, 0.97, 0.92), size=0.35,
          target_pt=(-0.05, 0.05, 0.03))
add_light("Room", 'AREA', (0.05, 0.55, 0.45), 4.0, (1.0, 0.93, 0.85), size=0.6,
          target_pt=(0.0, 0.6, 0.2))

scene.render.engine = 'CYCLES'
scene.cycles.device = 'CPU'
try:
    scene.view_settings.view_transform = 'Filmic'
    scene.view_settings.look = 'Medium High Contrast'
except Exception as ex:
    print("view transform:", ex)
scene.view_settings.exposure = 0.0
round 1 of 3
Café table from primitives, by Qwen3.8 Flash, the render after round 1
123What the model saw after each round. The picture above is the round that was scored, round 3.

The spoon is hidden behind the saucer, so it cannot be judged. The croissant is fine.

modelqwen/qwen3.8-flashtaskblender-cafe-table@v2date2026-09-21rounds3 of 8 allowedcost$0.0293thinkingmediumtokens330,816 in, 33,565 out, of which 20,770 reasoningtime12 min 0 s

What the harness did to this run. The 12-minute clock ended it before the model finished, so this is where it had got to when time ran out.

All five objectsdecided by looking · Pass if all five can be pointed at in the render as separate objects. One missing, or hidden behind another, fails it.pass
Primitives onlydecided by a number, from the filepass
Spoon reads as a spoondecided by looking · Pass if the spoon is one connected piece with a bowl at one end, wider than the handle, and the handle running from it in a line. Loose parts, or no handle, fail it.n/d
Croissant reads as a croissantdecided by looking · Pass if it is a crescent that is thickest in the middle and tapers to both tips, with segments or scoring across it. A smooth curved tube fails it, and so does a segmented one with no taper.pass
Consistent scaledecided by a number, from the filepass

There is no post about this one, since only some results need one, so the picture, the source and the scoring are here instead.

its other runs
the prompt it was given
Write a Blender Python script that builds a café table scene: an espresso cup on a saucer, a spoon, sugar cubes and a croissant. Use only primitive meshes (cube, cylinder, cone, sphere, torus).
Frozen at v2. This model got this and nothing else.
ground truth

The render, and the script that made it: the objects a person would name unprompted, at sizes a person would accept. A 5 cm spoon and an 11 cm croissant are checkable in the source. How it is scored, and this run's files on GitHub.