"""A small round teapot for the cottage desk, as mesh: my first. Run headless: "C:/Program Files/Blender Foundation/Blender 5.2/blender.exe" -b -P teapot.py -- [--render out.png] [--glb out.glb] Body and lid are lathed from profiles; spout and handle are bevelled curves converted to mesh. Units are metres (SL-sized: ~0.24 m across with the spout). Materials: glazed body (a deep sea-teal), cream lid knob. Writes a .glb (glTF) for upload and optionally a preview render. Claude Opus 5.5 (Tessel), 2026-10-10.""" import math import sys import bpy from mathutils import Vector args = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] opt = {args[i]: args[i + 1] for i in range(0, len(args) - 1, 2)} bpy.ops.wm.read_factory_settings(use_empty=True) scene = bpy.context.scene def material(name, rgb, rough=0.25): m = bpy.data.materials.new(name) m.use_nodes = True b = m.node_tree.nodes["Principled BSDF"] b.inputs["Base Color"].default_value = (*rgb, 1) b.inputs["Roughness"].default_value = rough return m GLAZE = material("glaze", (0.06, 0.32, 0.36), 0.18) CREAM = material("cream", (0.86, 0.80, 0.68), 0.35) def lathe(name, profile, segs=int(opt.get("--segs", 40)), mat=GLAZE): """profile: [(r, z)] bottom to top; r=0 points close the ends.""" verts, faces = [], [] n = len(profile) for s in range(segs): a = 2 * math.pi * s / segs for r, z in profile: verts.append((r * math.cos(a), r * math.sin(a), z)) for s in range(segs): t = (s + 1) % segs for i in range(n - 1): faces.append((s * n + i, t * n + i, t * n + i + 1, s * n + i + 1)) me = bpy.data.meshes.new(name) me.from_pydata(verts, [], faces) me.update() ob = bpy.data.objects.new(name, me) scene.collection.objects.link(ob) ob.data.materials.append(mat) bpy.context.view_layer.objects.active = ob ob.select_set(True) bpy.ops.object.mode_set(mode="EDIT") bpy.ops.mesh.remove_doubles(threshold=0.0005) # the axis points bpy.ops.mesh.normals_make_consistent(inside=False) bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.shade_smooth() ob.select_set(False) return ob def tube(name, pts, r0, r1, mat=GLAZE, res=int(opt.get("--res", 10))): """A bevelled poly-bezier through pts, radius tapering r0 -> r1, converted to mesh.""" cu = bpy.data.curves.new(name, "CURVE") cu.dimensions = "3D" cu.bevel_depth = 1.0 cu.bevel_resolution = int(opt.get("--bevel", 4)) cu.use_fill_caps = True sp = cu.splines.new("NURBS") sp.points.add(len(pts) - 1) for i, p in enumerate(pts): sp.points[i].co = (*p, 1) sp.points[i].radius = r0 + (r1 - r0) * i / (len(pts) - 1) sp.use_endpoint_u = True sp.order_u = 4 sp.resolution_u = res ob = bpy.data.objects.new(name, cu) scene.collection.objects.link(ob) ob.data.materials.append(mat) bpy.context.view_layer.objects.active = ob ob.select_set(True) bpy.ops.object.convert(target="MESH") bpy.ops.object.shade_smooth() ob.select_set(False) return ob if opt.get("--what") == "cup": # cup and saucer to match: a flared cup with a small loop handle on a shallow saucer saucer = [(0.0, 0.0), (0.03, 0.0), (0.032, 0.003), (0.058, 0.008), (0.072, 0.016), (0.074, 0.018), (0.07, 0.017), (0.056, 0.01), (0.032, 0.008), (0.0, 0.008)] lathe("saucer", saucer) cup = [(0.0, 0.009), (0.022, 0.009), (0.024, 0.012), (0.028, 0.016), (0.04, 0.03), (0.047, 0.05), (0.05, 0.066), (0.047, 0.066), (0.044, 0.05), (0.037, 0.031), (0.0, 0.018)] lathe("cup", cup) tea = [(0.0, 0.056), (0.0455, 0.056), (0.0, 0.0561)] lathe("tea", tea, mat=material("tea", (0.32, 0.14, 0.04), 0.05)) tube("handle", [(0.045, 0, 0.056), (0.064, 0, 0.056), (0.068, 0, 0.04), (0.056, 0, 0.028), (0.04, 0, 0.028)], 0.004, 0.0035) else: build_pot = True # body: a squat round pot with a foot ring and an open rim (wall thickness shown at the rim) body = [(0.0, 0.0), (0.045, 0.0), (0.05, 0.004), (0.048, 0.01), (0.062, 0.018), (0.078, 0.036), (0.086, 0.06), (0.084, 0.085), (0.074, 0.106), (0.058, 0.12), (0.046, 0.126), (0.046, 0.131), (0.041, 0.131), (0.041, 0.124), (0.0, 0.124)] lathe("body", body) lid = [(0.0, 0.132), (0.047, 0.132), (0.046, 0.136), (0.036, 0.146), (0.018, 0.152), (0.0, 0.153)] lathe("lid", lid) knob = [(0.0, 0.151), (0.008, 0.151), (0.012, 0.158), (0.011, 0.166), (0.005, 0.17), (0.0, 0.17)] lathe("knob", knob, segs=20, mat=CREAM) # spout leaves the body low on the +x side and rises in an S tube("spout", [(0.07, 0, 0.045), (0.098, 0, 0.06), (0.112, 0, 0.09), (0.125, 0, 0.115), (0.142, 0, 0.128)], 0.016, 0.0085) # handle: a loop on the -x side tube("handle", [(-0.072, 0, 0.1), (-0.105, 0, 0.112), (-0.128, 0, 0.085), (-0.118, 0, 0.045), (-0.08, 0, 0.035)], 0.0085, 0.0075) for ob in scene.objects: ob.select_set(True) bpy.context.view_layer.objects.active = bpy.data.objects["cup" if opt.get("--what") == "cup" else "body"] bpy.ops.object.join() pot = bpy.context.view_layer.objects.active pot.name = "Teacup" if opt.get("--what") == "cup" else "Teapot" print("tris:", sum(len(p.vertices) - 2 for p in pot.data.polygons)) if opt.get("--glb"): bpy.ops.export_scene.gltf(filepath=opt["--glb"], export_format="GLB", use_selection=True, export_apply=True) print("wrote", opt["--glb"]) if opt.get("--render"): cam_data = bpy.data.cameras.new("cam") cam = bpy.data.objects.new("cam", cam_data) scene.collection.objects.link(cam) cam.location = (0.42, -0.5, 0.32) direction = Vector((0, 0, 0.075)) - cam.location cam.rotation_euler = direction.to_track_quat("-Z", "Y").to_euler() cam_data.lens = 70 scene.camera = cam sun = bpy.data.objects.new("sun", bpy.data.lights.new("sun", "SUN")) sun.data.energy = 3.5 sun.rotation_euler = (math.radians(50), 0, math.radians(30)) scene.collection.objects.link(sun) world = bpy.data.worlds.new("w") world.use_nodes = True world.node_tree.nodes["Background"].inputs["Color"].default_value = (0.55, 0.5, 0.45, 1) world.node_tree.nodes["Background"].inputs["Strength"].default_value = 0.6 scene.world = world bpy.ops.mesh.primitive_plane_add(size=2) scene.render.engine = "BLENDER_EEVEE" scene.render.resolution_x = scene.render.resolution_y = 480 scene.render.filepath = opt["--render"] bpy.ops.render.render(write_still=True) print("rendered", opt["--render"])