Skip to content

Commit a3f0250

Browse files
committed
feat: remodel lightmap-uv-channel cart under the asset-quality gate
The shipped cart read as placeholder: uniform tan (its DarkWood wheel material was silently never applied - a prefix lookup bug, replaced with exact per-part mapping), wheels as lumpy polygons with a black dot for a hub, no fittings. Rebuilt as an assembly of 21 named watertight parts: plank-grooved bed with side rails, corner brackets, and fasteners, real wheel assemblies (disc + hub + iron band + bolt ring) on a capped axle, and a ribbed canvas canopy. Atlas board enlarged for card-scale legibility. Render path now gates through gallery_asset_quality (exit 11): 5 materials, edge90 0.069, naming clean. Closed forms moved deliberately, contracts unchanged: parts 9 -> 21, islands 1114 -> 3680 (byte-identical on 5.1.2 and 4.5.11; the MeshUVLoopLayer re-fetch ordering that avoids the documented 4.5.11 segfault is preserved and verified). Re-falsified: clobber drift 2.068, overlap 15 SAT hits, margin collapse 2e-5. Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>
1 parent db3ecd2 commit a3f0250

2 files changed

Lines changed: 204 additions & 53 deletions

File tree

‎examples/lightmap-uv-channel/README.md‎

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
# Lightmap UV Channel
22

33
A runnable example building the second UV layer engines require for baked
4-
lighting, on an asset built to be reused: a market cart (bed, axle, two
5-
wheels, four posts, arched canvas canopy — ground-level pivot at `z=0`,
6-
identity transforms by construction, `Cart.*` datablocks, watertight parts
7-
throughout). UV0 is the texture channel (deterministic dominant-axis box
4+
lighting, on an asset built to be reused: a market cart assembled from
5+
21 named, watertight parts — a plank-grooved bed with side rails, corner
6+
brackets, and fasteners, real wheel assemblies (disc + hub + iron band +
7+
bolt ring) on a capped axle, four posts, and a ribbed canvas canopy.
8+
Ground-level pivot at `z=0`, identity transforms by construction, `Cart.*`
9+
datablocks. (An earlier revision shipped a soft uniform-tan cart with
10+
lumpy polygon wheels and was remodeled under the asset-quality gate;
11+
its wheel material was also silently never applied — a name-key lookup
12+
bug the exact per-part mapping now replaces.) UV0 is the texture channel (deterministic dominant-axis box
813
projection per face); UVLight is the baked-lighting atlas
914
(`smart_project` → `lightmap_pack`).
1015

@@ -60,15 +65,19 @@ layers by name and re-asserts flags):
6065
[`prop-origin-transform`](../prop-origin-transform/).
6166

6267
**Version witness:** check output is byte-identical on Blender 4.5.11 LTS
63-
and 5.1.2 (9 parts, 1114 islands, drift 0, overlap 0, min island distance
68+
and 5.1.2 (21 parts, 3680 islands, drift 0, overlap 0, min island distance
6469
0.00401). The UV *layout* is packer-version-dependent by design; the
6570
contracts are layout-independent invariants.
6671

67-
**Render as proof:** the cart beside its UV1 atlas board — the Bed's
68-
packed lightmap built from live UV data, so a change in the atlas moves
69-
the board geometry. The falsification variant (`--falsify`) translates the
70-
second-largest island onto the largest: a big emissive-red island visibly
71-
stacked over the atlas (15 SAT hits in the check probe).
72+
**Render as proof:** the cart beside its enlarged UV1 atlas board — the
73+
Bed's packed lightmap built from live UV data, so a change in the atlas
74+
moves the board geometry. The falsification variant (`--falsify`)
75+
translates the second-largest island onto the largest: a big emissive-red
76+
island visibly stacked over the atlas (15 SAT hits in the check probe).
77+
The render path also gates the asset itself through
78+
`examples/gallery_asset_quality.py` (naming, material variation, edge
79+
treatment — exit 11): 21 named parts, 5 materials, right-angle share
80+
0.069.
7281

7382
## Run
7483

‎examples/lightmap-uv-channel/lightmap_uv_channel.py‎

Lines changed: 185 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir))
4646
sys.dont_write_bytecode = True # keep examples/__pycache__ out of the repo tree
4747
import gallery_framing
48+
import gallery_asset_quality
4849

4950
TOL = 1e-6 # UV0 preservation tolerance
5051
BOUNDS_TOL = 1e-5 # UV1 unit-square slack
@@ -88,11 +89,11 @@ def _box(name, dims, center, bevel=0.0):
8889
return me
8990

9091

91-
def _wheel(name, radius, width, center):
92+
def _wheel_disc(name, radius, width, center):
9293
me = bpy.data.meshes.new(name)
9394
bm = bmesh.new()
9495
try:
95-
bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=False, segments=20,
96+
bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=False, segments=24,
9697
radius1=radius, radius2=radius, depth=width)
9798
# cone axis is Z; the wheel rolls around Y -> rotate verts, applied in data
9899
for v in bm.verts:
@@ -106,54 +107,179 @@ def _wheel(name, radius, width, center):
106107
return me
107108

108109

110+
def _ring(name, r_out, r_in, width, center, segments=24):
111+
"""Flat annulus (iron tire band) spun from a 4-vert profile, watertight."""
112+
me = bpy.data.meshes.new(name)
113+
bm = bmesh.new()
114+
try:
115+
hw = width / 2
116+
prof = [(r_in, -hw), (r_out, -hw), (r_out, hw), (r_in, hw)]
117+
vs = [bm.verts.new((r, 0.0, z)) for r, z in prof]
118+
es = [bm.edges.new((vs[i], vs[(i + 1) % 4])) for i in range(4)]
119+
bmesh.ops.spin(bm, geom=vs + es, cent=(0.0, 0.0, 0.0),
120+
axis=(0.0, 0.0, 1.0), dvec=(0.0, 0.0, 0.0),
121+
angle=2 * math.pi, steps=segments, use_merge=True)
122+
for v in bm.verts:
123+
x, y, z = v.co.x, v.co.y, v.co.z
124+
v.co = (x + center[0], z + center[1], y + center[2])
125+
bm.to_mesh(me)
126+
finally:
127+
bm.free()
128+
return me
129+
130+
131+
def _bolt_ring(name, count, ring_r, bolt_r, center):
132+
"""count bolt heads on a ring around Y — one mesh, disconnected islands."""
133+
me = bpy.data.meshes.new(name)
134+
bm = bmesh.new()
135+
try:
136+
for i in range(count):
137+
a = 2 * math.pi * i / count
138+
cx, cz = center[0] + ring_r * math.cos(a), center[2] + ring_r * math.sin(a)
139+
before_v = set(bm.verts)
140+
bmesh.ops.create_uvsphere(bm, u_segments=8, v_segments=6, radius=bolt_r)
141+
for v in set(bm.verts) - before_v:
142+
v.co.x += cx
143+
v.co.y += center[1]
144+
v.co.z += cz
145+
bm.to_mesh(me)
146+
finally:
147+
bm.free()
148+
return me
149+
150+
151+
def _arc_band(bm, xc, width, radius, thickness, z0, segments):
152+
"""One arched sheet spanning x in [xc, xc+width], solidified with rims."""
153+
sweep = math.radians(120.0)
154+
a0 = math.radians(90.0) - sweep / 2
155+
outer, inner = [], []
156+
for i in range(segments + 1):
157+
a = a0 + sweep * i / segments
158+
ca, sa = math.cos(a), math.sin(a)
159+
outer.append(bm.verts.new((xc, radius * ca, z0 + radius * sa)))
160+
inner.append(bm.verts.new((xc, (radius - thickness) * ca,
161+
z0 + (radius - thickness) * sa)))
162+
def band(ring_o, ring_i):
163+
for i in range(segments):
164+
a, b = i, i + 1
165+
bm.faces.new((ring_o[a], ring_o[b], ring_i[b], ring_i[a]))
166+
outer_x2 = [bm.verts.new((v.co.x + width, v.co.y, v.co.z)) for v in outer]
167+
inner_x2 = [bm.verts.new((v.co.x + width, v.co.y, v.co.z)) for v in inner]
168+
band(outer, inner) # underside (thickness face)
169+
band(inner_x2, outer_x2) # top face
170+
for i in range(segments): # the two long rims
171+
a, b = i, i + 1
172+
bm.faces.new((outer[a], outer_x2[a], outer_x2[b], outer[b]))
173+
bm.faces.new((inner[a], inner[b], inner_x2[b], inner_x2[a]))
174+
bm.faces.new((outer[0], inner[0], inner_x2[0], outer_x2[0]))
175+
bm.faces.new((outer[-1], outer_x2[-1], inner_x2[-1], inner[-1]))
176+
177+
109178
def _canopy(name, width, radius, thickness, z0, segments=14):
110179
"""Arched canvas sheet: an arc band solidified with a rim, watertight."""
111180
me = bpy.data.meshes.new(name)
112181
bm = bmesh.new()
113182
try:
114-
sweep = math.radians(120.0)
115-
a0 = math.radians(90.0) - sweep / 2
116-
outer, inner = [], []
117-
for i in range(segments + 1):
118-
a = a0 + sweep * i / segments
119-
ca, sa = math.cos(a), math.sin(a)
120-
outer.append(bm.verts.new((-width / 2, radius * ca, z0 + radius * sa)))
121-
inner.append(bm.verts.new((-width / 2, (radius - thickness) * ca,
122-
z0 + (radius - thickness) * sa)))
123-
def band(off_x, ring_o, ring_i):
124-
for i in range(segments):
125-
a, b = i, i + 1
126-
bm.faces.new((ring_o[a], ring_o[b], ring_i[b], ring_i[a]))
127-
outer_x2 = [bm.verts.new((v.co.x + width, v.co.y, v.co.z)) for v in outer]
128-
inner_x2 = [bm.verts.new((v.co.x + width, v.co.y, v.co.z)) for v in inner]
129-
band(0, outer, inner) # underside (thickness face)
130-
band(width, inner_x2, outer_x2) # top face
131-
for i in range(segments): # the two long rims
132-
a, b = i, i + 1
133-
bm.faces.new((outer[a], outer_x2[a], outer_x2[b], outer[b]))
134-
bm.faces.new((inner[a], inner[b], inner_x2[b], inner_x2[a]))
135-
for ring in ((outer, outer_x2), (inner, inner_x2)): # end caps
136-
for i in (0, segments):
137-
pass
138-
bm.faces.new((outer[0], inner[0], inner_x2[0], outer_x2[0]))
139-
bm.faces.new((outer[-1], outer_x2[-1], inner_x2[-1], inner[-1]))
183+
_arc_band(bm, -width / 2, width, radius, thickness, z0, segments)
184+
bm.to_mesh(me)
185+
finally:
186+
bm.free()
187+
return me
188+
189+
190+
def _canopy_ribs(name, positions, width, radius, thickness, z0, segments=14):
191+
"""Thin wooden ribs following the canopy arc, one mesh, N islands."""
192+
me = bpy.data.meshes.new(name)
193+
bm = bmesh.new()
194+
try:
195+
for px in positions:
196+
_arc_band(bm, px - width / 2, width, radius, thickness, z0, segments)
197+
bm.to_mesh(me)
198+
finally:
199+
bm.free()
200+
return me
201+
202+
203+
def _bolts(name, positions, radius):
204+
"""Bolt head spheres at positions — one mesh, disconnected islands."""
205+
me = bpy.data.meshes.new(name)
206+
bm = bmesh.new()
207+
try:
208+
for px, py, pz in positions:
209+
before_v = set(bm.verts)
210+
bmesh.ops.create_uvsphere(bm, u_segments=8, v_segments=6, radius=radius)
211+
for v in set(bm.verts) - before_v:
212+
v.co.x += px
213+
v.co.y += py
214+
v.co.z += pz
215+
bm.to_mesh(me)
216+
finally:
217+
bm.free()
218+
return me
219+
220+
221+
def _multi_box(name, specs):
222+
"""Several beveled boxes as one mesh (disconnected islands, each
223+
watertight). specs: (dims, center, bevel)."""
224+
me = bpy.data.meshes.new(name)
225+
bm = bmesh.new()
226+
try:
227+
for dims, center, bevel in specs:
228+
before_e = set(bm.edges)
229+
before_v = set(bm.verts)
230+
bmesh.ops.create_cube(bm, size=1.0)
231+
new_edges = list(set(bm.edges) - before_e)
232+
for v in set(bm.verts) - before_v:
233+
v.co.x = v.co.x * dims[0] + center[0]
234+
v.co.y = v.co.y * dims[1] + center[1]
235+
v.co.z = v.co.z * dims[2] + center[2]
236+
if bevel > 0.0:
237+
bmesh.ops.bevel(bm, geom=new_edges, offset=bevel, segments=2,
238+
profile=0.5, affect="EDGES", clamp_overlap=True)
140239
bm.to_mesh(me)
141240
finally:
142241
bm.free()
143242
return me
144243

145244

146245
def build_cart_meshes():
246+
"""The cart as an assembly of named, watertight parts: real wheel
247+
assemblies (disc + hub + iron band + bolt ring), plank-grooved bed with
248+
side rails, corner brackets, and fasteners, and a ribbed canvas canopy."""
147249
parts = {}
148250
parts["Bed"] = _box("Cart.Bed", (2.2, 1.2, 0.22), (0.0, 0.0, 0.85), 0.03)
149-
parts["Axle"] = _wheel("Cart.Axle", 0.05, 1.5, (0.55, 0.0, 0.5))
150-
parts["Wheel.L"] = _wheel("Cart.Wheel.L", 0.45, 0.12, (0.55, 0.68, 0.45))
151-
parts["Wheel.R"] = _wheel("Cart.Wheel.R", 0.45, 0.12, (0.55, -0.68, 0.45))
251+
parts["Bed.Rails"] = _multi_box("Cart.Bed.Rails", [
252+
((2.2, 0.06, 0.18), (0.0, 0.57, 1.02), 0.015),
253+
((2.2, 0.06, 0.18), (0.0, -0.57, 1.02), 0.015)])
254+
parts["Bed.Grooves"] = _multi_box("Cart.Bed.Grooves", [
255+
((0.035, 1.14, 0.015), (x, 0.0, 0.9575), 0.0)
256+
for x in (-0.66, -0.22, 0.22, 0.66)])
257+
parts["Bed.Brackets"] = _multi_box("Cart.Bed.Brackets", [
258+
((0.03, 0.13, 0.26), (sx * 1.105, sy * 0.535, 0.85), 0.008)
259+
for sx in (-1, 1) for sy in (-1, 1)])
260+
parts["Bed.Bolts"] = _bolts("Cart.Bed.Bolts", [
261+
(sx * 1.125, sy * 0.535, z) for sx in (-1, 1) for sy in (-1, 1)
262+
for z in (0.78, 0.92)], 0.018)
263+
parts["Axle"] = _wheel_disc("Cart.Axle", 0.05, 1.56, (0.55, 0.0, 0.5))
264+
parts["Axle.Caps"] = _multi_box("Cart.Axle.Caps", [
265+
((0.14, 0.05, 0.14), (0.55, sy * 0.80, 0.5), 0.02) for sy in (-1, 1)])
266+
for tag, sy in (("L", 1.0), ("R", -1.0)):
267+
y = sy * 0.68
268+
parts[f"Wheel.{tag}.Disc"] = _wheel_disc(
269+
f"Cart.Wheel.{tag}.Disc", 0.45, 0.10, (0.55, y, 0.45))
270+
parts[f"Wheel.{tag}.Hub"] = _wheel_disc(
271+
f"Cart.Wheel.{tag}.Hub", 0.14, 0.16, (0.55, y, 0.45))
272+
parts[f"Wheel.{tag}.Band"] = _ring(
273+
f"Cart.Wheel.{tag}.Band", 0.47, 0.435, 0.10, (0.55, y, 0.45))
274+
parts[f"Wheel.{tag}.Bolts"] = _bolt_ring(
275+
f"Cart.Wheel.{tag}.Bolts", 6, 0.09, 0.02, (0.55, y + sy * 0.085, 0.45))
152276
for tag, (px, py) in (("FL", (0.95, 0.48)), ("FR", (0.95, -0.48)),
153277
("RL", (-0.95, 0.48)), ("RR", (-0.95, -0.48))):
154278
parts[f"Post.{tag}"] = _box(f"Cart.Post.{tag}", (0.09, 0.09, 1.35),
155279
(px, py, 1.55), 0.015)
156280
parts["Canopy"] = _canopy("Cart.Canopy", 2.3, 0.85, 0.04, 1.9)
281+
parts["Canopy.Ribs"] = _canopy_ribs("Cart.Canopy.Ribs", (-0.75, 0.0, 0.75),
282+
0.07, 0.875, 0.035, 1.9)
157283
return parts
158284

159285

@@ -512,18 +638,30 @@ def render_still(path, engine, falsify=False):
512638
bpy.ops.wm.read_factory_settings(use_empty=True)
513639
sc = bpy.context.scene
514640

515-
wood = make_material("Wood", (0.42, 0.26, 0.13), rough=0.6)
641+
wood = make_material("Wood", (0.38, 0.22, 0.10), rough=0.55)
516642
darkwood = make_material("DarkWood", (0.16, 0.10, 0.05), rough=0.7)
517-
iron = make_material("Iron", (0.18, 0.18, 0.20), rough=0.35, metallic=0.9)
643+
iron = make_material("Iron", (0.26, 0.26, 0.30), rough=0.3, metallic=0.9)
518644
canvas = make_material("Canvas", (0.66, 0.56, 0.40), rough=0.9)
645+
groovemat = make_material("Groove", (0.05, 0.04, 0.03), rough=0.9)
646+
647+
mat_by_part = {
648+
"Bed": wood, "Bed.Rails": wood, "Bed.Grooves": groovemat,
649+
"Bed.Brackets": iron, "Bed.Bolts": iron,
650+
"Axle": iron, "Axle.Caps": iron,
651+
"Canopy": canvas, "Canopy.Ribs": darkwood,
652+
}
653+
for tag in ("L", "R"):
654+
mat_by_part[f"Wheel.{tag}.Disc"] = darkwood
655+
mat_by_part[f"Wheel.{tag}.Hub"] = darkwood
656+
mat_by_part[f"Wheel.{tag}.Band"] = iron
657+
mat_by_part[f"Wheel.{tag}.Bolts"] = iron
658+
for tag in ("FL", "FR", "RL", "RR"):
659+
mat_by_part[f"Post.{tag}"] = darkwood
519660

520-
mats = {"Bed": wood, "Axle": iron, "Wheel.L": darkwood, "Wheel.R": darkwood,
521-
"Canopy": canvas}
522661
parts_obs = []
523662
meshes = build_cart_meshes()
524663
for suffix, me in meshes.items():
525-
me.materials.append(mats.get(suffix.split(".")[0], wood)
526-
if not suffix.startswith("Post") else wood)
664+
me.materials.append(mat_by_part[suffix])
527665
me.uv_layers.new(name=LAYER0)
528666
me.uv_layers.new(name=LAYER1)
529667
write_uv0_box_projection(me)
@@ -553,10 +691,10 @@ def render_still(path, engine, falsify=False):
553691
# atlas board: the Bed's live UV1 as flat island polygons mapped onto the
554692
# board face — a change in the packed atlas moves the board geometry
555693
board_mat = make_material("Board", (0.04, 0.04, 0.05), rough=0.5, metallic=0.4)
556-
board = _box("Atlas.Board", (0.07, 1.7, 1.7), (0.0, 0.0, 0.0), 0.02)
694+
board = _box("Atlas.Board", (0.08, 2.2, 2.0), (0.0, 0.0, 0.0), 0.02)
557695
board.materials.append(board_mat)
558696
board_ob = bpy.data.objects.new("Atlas.Board", board)
559-
board_ob.location = (1.75, 0.55, 1.0)
697+
board_ob.location = (2.05, 0.55, 1.15)
560698
sc.collection.objects.link(board_ob)
561699

562700
bed = meshes["Bed"]
@@ -571,7 +709,7 @@ def render_still(path, engine, falsify=False):
571709
bm = bmesh.new()
572710
try:
573711
# board face: +X side; UV [0,1]^2 -> y,z on the face (u -> -y, v -> z)
574-
vs = [bm.verts.new((0.0, (0.5 - uv[0]) * 1.5, (uv[1] - 0.5) * 1.5))
712+
vs = [bm.verts.new((0.0, (0.5 - uv[0]) * 1.9, (uv[1] - 0.5) * 1.9))
575713
for uv in pts]
576714
bm.faces.new(vs)
577715
bm.to_mesh(me)
@@ -582,7 +720,7 @@ def render_still(path, engine, falsify=False):
582720
me.materials.append(make_material(f"IslandMat{fi}", rgb, rough=0.5,
583721
emit=rgb, estr=1.6 if defect else 0.5))
584722
ob = bpy.data.objects.new(f"Atlas.Island.{fi}", me)
585-
ob.location = (1.75 + 0.037 + (0.004 if fi in dragged else 0.0), 0.55, 1.0)
723+
ob.location = (2.05 + 0.045 + (0.004 if fi in dragged else 0.0), 0.55, 1.15)
586724
sc.collection.objects.link(ob)
587725
iso_obs.append(ob)
588726

@@ -591,10 +729,10 @@ def render_still(path, engine, falsify=False):
591729
cam_data = bpy.data.cameras.new("Cam")
592730
cam_data.lens = 47.0
593731
cam = bpy.data.objects.new("Cam", cam_data)
594-
cam.location = (5.4, -6.6, 2.7)
732+
cam.location = (5.9, -7.2, 2.8)
595733
sc.collection.objects.link(cam)
596734
aim = bpy.data.objects.new("Aim", None)
597-
aim.location = (0.75, 0.1, 1.08)
735+
aim.location = (0.85, 0.1, 1.05)
598736
sc.collection.objects.link(aim)
599737
tr = cam.constraints.new("TRACK_TO")
600738
tr.target = aim
@@ -625,6 +763,10 @@ def render_still(path, engine, falsify=False):
625763
)
626764
if fcode:
627765
return fcode
766+
aqcode = gallery_asset_quality.check_asset_quality(
767+
sc, cam, hero=hero, stage=[floor, wall])
768+
if aqcode:
769+
return aqcode
628770
bpy.ops.render.render(write_still=True)
629771
if not (os.path.exists(path) and os.path.getsize(path) > 0):
630772
print("ERROR: render produced no file", file=sys.stderr)

0 commit comments

Comments
 (0)