The three things the greengrocer was still missing, plus the physics bug that
finding them uncovered.
HANGING SCALES. Real PinJoint3D pendulums, not animations: a static dial and
rod, and a scale-pan rigid body pinned at its own origin so it can only rotate
about the pivot. They hang at chest height down the aisles — dial at eye level,
dish below — where a real shop scale hangs and where you keep walking into it.
The dish swings 0.40 m off a light knock and the pin holds the pivot to 1.1 mm.
The pan is steel so it never breaks; what you get is a heavy brass weight loose
in a room full of stacked fruit.
THE BAG YOU CANNOT OPEN. Tearing one off the roll is the easy half — that's the
setup. Then you have to open it, and the bag has two ends, only one of which
opens, and nothing tells you which. Rubbing alternates arrow keys, because
mashing one key is not rubbing. Either the meter climbs or it doesn't, and the
only way to learn which end you're holding is to have already lost several
seconds to the other one. SPACE turns it over. That is the whole solution and
the game never says so.
CARRY AND THROW. E picks up anything under 6 kg that isn't a record; LMB throws
it at 11 m/s, six times any brittle threshold in the game. Thrown fruit bursts
on landing through the same brittle_speed path a collapse uses — no separate
thrown-object code at all.
Then the part that took the longest. Round produce got sphere colliders (a box
on an apple is why nothing ever rolled) and every display in the shop instantly
fell over. Three separate things were wrong:
- the stack radii were TYPED, not measured. The table said a cabbage was
84 mm; the model is 213 mm, so that pyramid was built with every row driven
a third of the way into the row below it. Main._glb_radius() reads the asset.
- the lattice was box geometry. For spheres the rise per row is
sqrt(4r^2 - step^2/2) — the height at which a fruit touches all four
beneath it. Anything else spawns every row above the first in mid-air.
- there was no tray. A pyramid of spheres on a bare flat table cannot stand;
nothing holds the bottom row in, so the weight above wedges it outward and
the display walks itself apart in a second. Main._stack_tray() frames each
pile in four low timber walls sized to its base row, which is what every
greengrocer on earth already does.
All 310 bodies asleep within 3 s.
While chasing that, the spawn guard fired on a cardboard box in the OFFICE. The
guard is a net, not a test: it only catches a pair Jolt happens to resolve
violently on the frames it's watching, and this one had been interpenetrating
in four levels for weeks. dev/probe_overlap.gd now finds them by measurement,
comparing every pair of dynamic colliders across all six sites. It found 15,
including a row of filing cabinets 0.70 m apart that are 0.80 m wide, and a
stapler inside a monitor. The box asset is 1.35 m across, so boxes are now
stacked into piles rather than dotted about, and the Backrooms scatter uses
rejection sampling with a 1.6 m minimum. All six sites read CLEAN.
Also: the rage veins were drawing every branch from its own start point
regardless of how far the trunk had grown, so at low rage you got disconnected
fragments floating mid-screen that read as biro scribble rather than blood. A
branch now can't appear before the trunk carrying it, and trunks are thick and
dark where capillaries are fine and pale.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
446 lines
17 KiB
Python
446 lines
17 KiB
Python
"""The greengrocer's stock.
|
|
|
|
/Applications/Blender.app/Contents/MacOS/Blender --background \
|
|
--python tools/gen_produce.py -- [--render]
|
|
|
|
ORIGIN RULE, and it differs from every other prop in this repo: round produce is
|
|
CENTRED, not floor-centred. These things are meant to roll, and a rolling body whose
|
|
origin sits on the floor plane wobbles like a loaded die. Everything else here (crates,
|
|
bottles, the banana bunch) keeps the usual floor-centre origin, and Main knows which is
|
|
which from the level spec.
|
|
|
|
Deliberately a bit under-detailed: these spawn in pyramids of thirty, so the budget goes
|
|
on silhouette and colour rather than on a nice stem.
|
|
"""
|
|
|
|
import bpy
|
|
import math
|
|
import os
|
|
import sys
|
|
from mathutils import Vector, Matrix
|
|
|
|
OUT_DIR = os.path.join(
|
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
|
"game", "assets", "store")
|
|
PREVIEW_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "previews")
|
|
RENDER = "--render" in sys.argv
|
|
|
|
|
|
def reset():
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
|
|
|
|
def mat(name, color, rough=0.55, metal=0.0, alpha=1.0):
|
|
m = bpy.data.materials.new(name)
|
|
m.use_nodes = True
|
|
b = m.node_tree.nodes["Principled BSDF"]
|
|
b.inputs["Base Color"].default_value = (color[0], color[1], color[2], alpha)
|
|
b.inputs["Roughness"].default_value = rough
|
|
b.inputs["Metallic"].default_value = metal
|
|
if alpha < 1.0:
|
|
b.inputs["Alpha"].default_value = alpha
|
|
m.blend_method = 'BLEND'
|
|
return m
|
|
|
|
|
|
def assign(o, m):
|
|
o.data.materials.clear()
|
|
o.data.materials.append(m)
|
|
return o
|
|
|
|
|
|
def box(size, loc=(0, 0, 0), rot=(0, 0, 0)):
|
|
bpy.ops.mesh.primitive_cube_add(size=1.0, location=loc, rotation=rot)
|
|
o = bpy.context.active_object
|
|
o.scale = (size[0], size[1], size[2])
|
|
bpy.ops.object.transform_apply(scale=True)
|
|
return o
|
|
|
|
|
|
def cyl(r, depth, loc=(0, 0, 0), rot=(0, 0, 0), verts=16, r2=None):
|
|
if r2 is None:
|
|
bpy.ops.mesh.primitive_cylinder_add(radius=r, depth=depth, vertices=verts,
|
|
location=loc, rotation=rot)
|
|
else:
|
|
bpy.ops.mesh.primitive_cone_add(radius1=r, radius2=r2, depth=depth,
|
|
vertices=verts, location=loc, rotation=rot)
|
|
return bpy.context.active_object
|
|
|
|
|
|
def ball(r, loc=(0, 0, 0), segs=20, rings=12):
|
|
bpy.ops.mesh.primitive_uv_sphere_add(radius=r, segments=segs, ring_count=rings,
|
|
location=loc)
|
|
return bpy.context.active_object
|
|
|
|
|
|
def bevel(o, width=0.005, segments=2):
|
|
m = o.modifiers.new("bevel", "BEVEL")
|
|
m.width = width
|
|
m.segments = segments
|
|
m.limit_method = 'ANGLE'
|
|
m.angle_limit = math.radians(40)
|
|
bpy.context.view_layer.objects.active = o
|
|
try:
|
|
bpy.ops.object.modifier_apply(modifier=m.name)
|
|
except Exception:
|
|
o.modifiers.remove(m)
|
|
return o
|
|
|
|
|
|
def smooth(o, angle=45.0):
|
|
bpy.context.view_layer.objects.active = o
|
|
o.select_set(True)
|
|
try:
|
|
bpy.ops.object.shade_auto_smooth(angle=math.radians(angle))
|
|
except Exception:
|
|
bpy.ops.object.shade_smooth()
|
|
o.select_set(False)
|
|
|
|
|
|
def span(r0, r1, p0, p1, verts=8):
|
|
p0, p1 = Vector(p0), Vector(p1)
|
|
d = p1 - p0
|
|
n = d.length
|
|
if n < 1e-6:
|
|
return None
|
|
o = cyl(r0, n, verts=verts, r2=r1)
|
|
q = Vector((0, 0, 1)).rotation_difference(d.normalized())
|
|
o.matrix_world = Matrix.Translation(p0 + d * 0.5) @ q.to_matrix().to_4x4()
|
|
bpy.ops.object.transform_apply(location=True, rotation=True)
|
|
return o
|
|
|
|
|
|
# ---------------------------------------------------------------- produce
|
|
# All the round ones are CENTRED on the origin so they roll properly.
|
|
|
|
def p_apple(M):
|
|
parts = []
|
|
b = ball(0.040, segs=18, rings=12)
|
|
b.scale = (1.0, 1.0, 0.92)
|
|
bpy.ops.object.transform_apply(scale=True)
|
|
parts.append(assign(b, M["apple"]))
|
|
# the dimple: a dark disc SUNK into the top, not a bobble sitting on it
|
|
d = ball(0.017, loc=(0, 0, 0.029), segs=12, rings=8)
|
|
d.scale = (1.0, 1.0, 0.34)
|
|
bpy.ops.object.transform_apply(scale=True)
|
|
parts.append(assign(d, M["apple_dark"]))
|
|
parts.append(assign(span(0.004, 0.003, Vector((0, 0, 0.032)),
|
|
Vector((0.006, 0.004, 0.062)), verts=6), M["stem"]))
|
|
return parts
|
|
|
|
|
|
def p_orange(M):
|
|
parts = []
|
|
b = ball(0.037, segs=18, rings=12)
|
|
b.scale = (1.0, 1.0, 0.95)
|
|
bpy.ops.object.transform_apply(scale=True)
|
|
parts.append(assign(b, M["orange"]))
|
|
parts.append(assign(cyl(0.006, 0.004, loc=(0, 0, 0.034), verts=8), M["stem"]))
|
|
return parts
|
|
|
|
|
|
def p_tomato(M):
|
|
parts = []
|
|
b = ball(0.033, segs=18, rings=12)
|
|
b.scale = (1.0, 1.0, 0.82)
|
|
bpy.ops.object.transform_apply(scale=True)
|
|
parts.append(assign(b, M["tomato"]))
|
|
# calyx: five little leaves
|
|
for i in range(5):
|
|
a = i * (math.tau / 5.0)
|
|
parts.append(assign(span(0.006, 0.002,
|
|
Vector((0, 0, 0.026)),
|
|
Vector((math.cos(a) * 0.020, math.sin(a) * 0.020, 0.030)),
|
|
verts=5), M["leaf"]))
|
|
return parts
|
|
|
|
|
|
def p_melon(M):
|
|
"""Watermelon. Big, heavy, and the best thing in the shop to drop on other produce."""
|
|
parts = []
|
|
b = ball(0.145, segs=26, rings=16)
|
|
b.scale = (1.0, 1.28, 1.0)
|
|
bpy.ops.object.transform_apply(scale=True)
|
|
parts.append(assign(b, M["melon"]))
|
|
# Stripes as thin lenses through the middle, each a hair proud of the skin, so they
|
|
# read as bands wrapping the fruit. Boxes poking out radially made it look like a
|
|
# sea mine.
|
|
for i in range(7):
|
|
a = i * (math.pi / 7.0)
|
|
st = ball(0.1465, segs=26, rings=16)
|
|
st.scale = (0.115, 1.283, 1.004)
|
|
bpy.ops.object.transform_apply(scale=True)
|
|
st.rotation_euler = (0.0, 0.0, a)
|
|
bpy.ops.object.transform_apply(rotation=True)
|
|
parts.append(assign(st, M["melon_dark"]))
|
|
return parts
|
|
|
|
|
|
def p_cabbage(M):
|
|
parts = []
|
|
b = ball(0.082, segs=20, rings=14)
|
|
b.scale = (1.0, 1.0, 0.88)
|
|
bpy.ops.object.transform_apply(scale=True)
|
|
parts.append(assign(b, M["cabbage"]))
|
|
# a few outer leaves, so it isn't just a green ball
|
|
for i in range(6):
|
|
a = i * (math.tau / 6.0)
|
|
lf = ball(0.052, loc=(math.cos(a) * 0.058, math.sin(a) * 0.058, -0.016),
|
|
segs=12, rings=8)
|
|
lf.scale = (1.25, 1.25, 0.42)
|
|
bpy.ops.object.transform_apply(scale=True)
|
|
parts.append(assign(lf, M["cabbage_out"]))
|
|
return parts
|
|
|
|
|
|
def p_banana(M):
|
|
"""A hand of bananas. Floor-centred — these don't roll, they flop."""
|
|
parts = []
|
|
for i in range(5):
|
|
off = (i - 2) * 0.018
|
|
pos = Vector((off, 0.0, 0.030))
|
|
prev = pos
|
|
for seg in range(4):
|
|
a = math.radians(-38 + seg * 26)
|
|
nxt = prev + Vector((0.0, math.cos(a) * 0.042, math.sin(a) * 0.042 * 0.5))
|
|
r = 0.016 - seg * 0.0022
|
|
parts.append(assign(span(r, r * 0.86, prev, nxt, verts=8), M["banana"]))
|
|
prev = nxt
|
|
crown = box((0.10, 0.030, 0.026), loc=(0, -0.012, 0.030))
|
|
parts.append(assign(bevel(crown, 0.008), M["stem"]))
|
|
return parts
|
|
|
|
|
|
# ---------------------------------------------------------------- fittings
|
|
|
|
def p_bottle(M):
|
|
"""Glass juice bottle. Floor-centred. Breaks like glass, not like fruit."""
|
|
parts = []
|
|
body = cyl(0.037, 0.185, loc=(0, 0, 0.093), verts=20)
|
|
parts.append(assign(body, M["glass"]))
|
|
juice = cyl(0.031, 0.140, loc=(0, 0, 0.076), verts=18)
|
|
parts.append(assign(juice, M["juice"]))
|
|
shoulder = cyl(0.037, 0.055, loc=(0, 0, 0.212), r2=0.017, verts=20)
|
|
parts.append(assign(shoulder, M["glass"]))
|
|
neck = cyl(0.017, 0.048, loc=(0, 0, 0.262), verts=14)
|
|
parts.append(assign(neck, M["glass"]))
|
|
cap = cyl(0.020, 0.020, loc=(0, 0, 0.294), verts=14)
|
|
parts.append(assign(cap, M["cap"]))
|
|
label = cyl(0.0385, 0.070, loc=(0, 0, 0.085), verts=20)
|
|
parts.append(assign(label, M["label"]))
|
|
return parts
|
|
|
|
|
|
def p_crate(M):
|
|
"""Angled wooden display crate. Produce piles into it."""
|
|
parts = []
|
|
W, D, H, T = 0.52, 0.38, 0.15, 0.014
|
|
parts.append(assign(box((W, T, H), loc=(0, -D / 2, H / 2)), M["crate"]))
|
|
parts.append(assign(box((W, T, H * 1.5), loc=(0, D / 2, H * 0.75)), M["crate"]))
|
|
for s in (-1, 1):
|
|
parts.append(assign(box((T, D, H * 1.2), loc=(s * W / 2, 0, H * 0.6)), M["crate"]))
|
|
for i in range(5):
|
|
parts.append(assign(box((W, D / 5 - 0.01, T),
|
|
loc=(0, -D / 2 + (i + 0.5) * D / 5, T / 2)), M["crate"]))
|
|
return parts
|
|
|
|
|
|
def p_scale_head(M):
|
|
"""The dial and hanger of a shop scale. STATIC — the pan hangs off it on a joint."""
|
|
parts = []
|
|
parts.append(assign(cyl(0.010, 0.36, loc=(0, 0, 0.18), verts=8), M["steel"]))
|
|
hook = cyl(0.013, 0.070, loc=(0, 0, 0.02), verts=8)
|
|
parts.append(assign(hook, M["steel"]))
|
|
# the dial: a fat disc facing -Y, which is the direction shoppers stand in
|
|
body = cyl(0.135, 0.075, loc=(0, 0, -0.085), rot=(math.radians(90), 0, 0), verts=26)
|
|
parts.append(assign(body, M["brass"]))
|
|
face = cyl(0.120, 0.012, loc=(0, -0.042, -0.085), rot=(math.radians(90), 0, 0),
|
|
verts=26)
|
|
parts.append(assign(face, M["dial"]))
|
|
for i in range(12):
|
|
a = i * (math.tau / 12.0)
|
|
tick = box((0.006, 0.006, 0.022),
|
|
loc=(math.cos(a) * 0.098, -0.049, -0.085 + math.sin(a) * 0.098),
|
|
rot=(0, a, 0))
|
|
parts.append(assign(tick, M["steel"]))
|
|
needle = box((0.005, 0.008, 0.090), loc=(0.012, -0.050, -0.050),
|
|
rot=(0, 0, math.radians(18)))
|
|
parts.append(assign(needle, M["needle"]))
|
|
parts.append(assign(cyl(0.014, 0.014, loc=(0, -0.050, -0.085),
|
|
rot=(math.radians(90), 0, 0), verts=12), M["steel"]))
|
|
# the eye the pan hangs from
|
|
parts.append(assign(cyl(0.011, 0.045, loc=(0, 0, -0.165), verts=8), M["steel"]))
|
|
return parts
|
|
|
|
|
|
def p_scale_pan(M):
|
|
"""The swinging half: three chains and a brass pan. Origin at the TOP, on the pivot,
|
|
so a PinJoint3D at the origin hangs it correctly."""
|
|
parts = []
|
|
ring = cyl(0.016, 0.010, loc=(0, 0, -0.006), verts=12)
|
|
parts.append(assign(ring, M["steel"]))
|
|
for i in range(3):
|
|
a = i * (math.tau / 3.0)
|
|
top = Vector((0, 0, -0.010))
|
|
bot = Vector((math.cos(a) * 0.115, math.sin(a) * 0.115, -0.230))
|
|
parts.append(assign(span(0.005, 0.005, top, bot, verts=6), M["steel"]))
|
|
pan = cyl(0.155, 0.030, loc=(0, 0, -0.246), r2=0.185, verts=28)
|
|
parts.append(assign(pan, M["brass"]))
|
|
lip = cyl(0.186, 0.016, loc=(0, 0, -0.262), verts=28)
|
|
parts.append(assign(lip, M["brass"]))
|
|
return parts
|
|
|
|
|
|
def p_bag_roll(M):
|
|
"""The roll of produce bags. The single most infuriating object in any shop."""
|
|
parts = []
|
|
# A floor stand, at the height a shop actually puts one: the roll wants to be under
|
|
# your hands, not your knees.
|
|
H = 1.14
|
|
parts.append(assign(cyl(0.140, 0.030, loc=(0, 0, 0.015), verts=18), M["steel"]))
|
|
parts.append(assign(box((0.034, 0.034, H), loc=(0, 0, H * 0.5)), M["steel"]))
|
|
parts.append(assign(box((0.30, 0.026, 0.026), loc=(0, 0, H)), M["steel"]))
|
|
for s in (-1, 1):
|
|
parts.append(assign(box((0.026, 0.026, 0.10), loc=(s * 0.14, 0, H - 0.045)),
|
|
M["steel"]))
|
|
# the roll itself, on the spindle
|
|
roll = cyl(0.075, 0.245, loc=(0, 0, H - 0.045), rot=(0, math.radians(90), 0), verts=22)
|
|
parts.append(assign(roll, M["bag"]))
|
|
core = cyl(0.020, 0.255, loc=(0, 0, H - 0.045), rot=(0, math.radians(90), 0), verts=12)
|
|
parts.append(assign(core, M["core"]))
|
|
# and one hanging off it, half torn, forever
|
|
hang = box((0.20, 0.008, 0.30), loc=(0, 0.055, H - 0.185),
|
|
rot=(math.radians(-8), 0, 0))
|
|
parts.append(assign(hang, M["bag"]))
|
|
tail = box((0.16, 0.006, 0.10), loc=(0.02, 0.060, H - 0.330),
|
|
rot=(math.radians(-16), 0, math.radians(7)))
|
|
parts.append(assign(tail, M["bag"]))
|
|
return parts
|
|
|
|
|
|
PROPS = {
|
|
"scale-head": p_scale_head,
|
|
"scale-pan": p_scale_pan,
|
|
"bag-roll": p_bag_roll,
|
|
"produce-apple": p_apple,
|
|
"produce-orange": p_orange,
|
|
"produce-tomato": p_tomato,
|
|
"produce-melon": p_melon,
|
|
"produce-cabbage": p_cabbage,
|
|
"produce-banana": p_banana,
|
|
"juice-bottle": p_bottle,
|
|
"produce-crate": p_crate,
|
|
}
|
|
|
|
|
|
def materials():
|
|
return {
|
|
"apple": mat("apple", (0.62, 0.06, 0.07), rough=0.32),
|
|
"apple_dark": mat("apple_dark", (0.30, 0.04, 0.05), rough=0.5),
|
|
"orange": mat("orange", (0.88, 0.42, 0.05), rough=0.62),
|
|
"tomato": mat("tomato", (0.72, 0.07, 0.05), rough=0.28),
|
|
"melon": mat("melon", (0.16, 0.36, 0.12), rough=0.45),
|
|
"melon_dark": mat("melon_dark", (0.08, 0.20, 0.07), rough=0.45),
|
|
"cabbage": mat("cabbage", (0.68, 0.78, 0.48), rough=0.55),
|
|
"cabbage_out": mat("cabbage_out", (0.38, 0.58, 0.26), rough=0.62),
|
|
"banana": mat("banana", (0.90, 0.78, 0.18), rough=0.5),
|
|
"leaf": mat("leaf", (0.24, 0.44, 0.16), rough=0.7),
|
|
"stem": mat("stem", (0.34, 0.26, 0.13), rough=0.85),
|
|
"glass": mat("glass", (0.74, 0.82, 0.78), rough=0.06, alpha=0.34),
|
|
"juice": mat("juice", (0.86, 0.44, 0.06), rough=0.22),
|
|
"cap": mat("cap", (0.72, 0.70, 0.66), rough=0.3, metal=0.8),
|
|
"label": mat("label", (0.94, 0.92, 0.86), rough=0.85),
|
|
"crate": mat("crate", (0.56, 0.40, 0.22), rough=0.85),
|
|
"steel": mat("steel", (0.66, 0.67, 0.70), rough=0.30, metal=1.0),
|
|
"brass": mat("brass", (0.72, 0.56, 0.22), rough=0.28, metal=1.0),
|
|
"dial": mat("dial", (0.94, 0.93, 0.89), rough=0.5),
|
|
"needle": mat("needle", (0.68, 0.08, 0.06), rough=0.4),
|
|
# tinted: a white bag on a white wall is invisible, and you have to be able to
|
|
# find the thing you are about to be defeated by
|
|
"bag": mat("bag", (0.62, 0.78, 0.80), rough=0.40, alpha=0.66),
|
|
"core": mat("core", (0.62, 0.50, 0.34), rough=0.9),
|
|
}
|
|
|
|
|
|
def export(objs, name):
|
|
objs = [o for o in objs if o is not None]
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|
for o in objs:
|
|
o.select_set(True)
|
|
bpy.context.view_layer.objects.active = objs[0]
|
|
# Blender's cone primitive winds its caps inward when radius2 > radius1, which is
|
|
# exactly how the scale pan is built — and a backface-culled renderer then shows you
|
|
# the unlit inside of the dish as a black hole. Recalculate outward for everything,
|
|
# once, here, so no generator has to remember.
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
bpy.ops.mesh.select_all(action='SELECT')
|
|
bpy.ops.mesh.normals_make_consistent(inside=False)
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
path = os.path.join(OUT_DIR, name + ".glb")
|
|
bpy.ops.export_scene.gltf(filepath=path, export_format='GLB',
|
|
use_selection=True, export_apply=True, export_yup=True)
|
|
print("[produce] %-18s -> %s" % (name, os.path.basename(path)))
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|
|
|
|
|
def render_preview(name):
|
|
scene = bpy.context.scene
|
|
engines = scene.render.bl_rna.properties['engine'].enum_items.keys()
|
|
for want in ('BLENDER_EEVEE_NEXT', 'BLENDER_EEVEE', 'BLENDER_WORKBENCH'):
|
|
if want in engines:
|
|
scene.render.engine = want
|
|
break
|
|
scene.render.resolution_x = 480
|
|
scene.render.resolution_y = 480
|
|
if scene.world is None:
|
|
scene.world = bpy.data.worlds.new("preview")
|
|
scene.world.use_nodes = True
|
|
bg = scene.world.node_tree.nodes.get("Background")
|
|
if bg:
|
|
bg.inputs[0].default_value = (0.10, 0.10, 0.11, 1.0)
|
|
lo = Vector((1e9,) * 3)
|
|
hi = Vector((-1e9,) * 3)
|
|
for o in scene.objects:
|
|
if o.type != 'MESH':
|
|
continue
|
|
for c in o.bound_box:
|
|
w = o.matrix_world @ Vector(c)
|
|
lo = Vector((min(lo.x, w.x), min(lo.y, w.y), min(lo.z, w.z)))
|
|
hi = Vector((max(hi.x, w.x), max(hi.y, w.y), max(hi.z, w.z)))
|
|
centre = (lo + hi) * 0.5
|
|
radius = max((hi - lo).length * 0.5, 0.02)
|
|
dist = radius * 3.0
|
|
d = Vector((0.62, -0.74, 0.34)).normalized()
|
|
bpy.ops.object.camera_add(location=tuple(centre + d * dist))
|
|
cam = bpy.context.active_object
|
|
cam.data.lens = 58
|
|
cam.rotation_mode = 'QUATERNION'
|
|
cam.rotation_quaternion = (-d).to_track_quat('-Z', 'Y')
|
|
scene.camera = cam
|
|
for off, e, s in ((Vector((1.0, -1.0, 1.1)), 60.0, 1.2),
|
|
(Vector((-1.1, -0.4, 0.3)), 18.0, 1.8)):
|
|
bpy.ops.object.light_add(type='AREA', location=tuple(centre + off * dist))
|
|
L = bpy.context.active_object
|
|
L.data.energy = e * (dist ** 2)
|
|
L.data.size = s * radius
|
|
scene.render.filepath = os.path.join(PREVIEW_DIR, name + ".png")
|
|
bpy.ops.render.render(write_still=True)
|
|
|
|
|
|
def main():
|
|
os.makedirs(OUT_DIR, exist_ok=True)
|
|
os.makedirs(PREVIEW_DIR, exist_ok=True)
|
|
for name, builder in PROPS.items():
|
|
reset()
|
|
M = materials()
|
|
parts = builder(M)
|
|
for p in parts:
|
|
smooth(p, 45.0)
|
|
export(parts, name)
|
|
if RENDER:
|
|
render_preview(name)
|
|
print("[produce] done -> %s" % OUT_DIR)
|
|
|
|
|
|
main()
|