Draw prop models with a single atlas material
Each generated prop used one material per part (body, accent, metal...). The model pipeline now packs a prop's palette into one power-of-two colour atlas plus a matching metallic/roughness atlas and emits a single material: - atlas_for(): 8x8 cells per palette role, colours in sRGB, roughness in the G and metallic in the B channel of the second atlas; NEAREST sampling keeps cells flat. - build_glb(): every part merges into one primitive with constant per-vertex atlas UVs; the material uses baseColorTexture + metallicRoughnessTexture with factors 1.0. - audit_glb(): fails unless a prop has exactly one material, both atlas textures, two images, and TEXCOORD_0 on every primitive. - Removed the now-unused sRGB->linear factor helpers; colour travels in the texture. Verification: headless 181 -> 182 (new "one material per prop scene" test), Python tooling 53/53 (atlas determinism/size/UV/palette-override tests replace the old factor tests), model audit OK for all six props, screenshots regenerated and screen_designs.py compare still 6 good / 0 bad. Version 0.3.1.
This commit is contained in:
1 parent
7f92b8861d
commit
f2863abeac
32 files changed
+275
-97
No files matched your search
@@ -94,14 +94,17 @@ class GlbTests(unittest.TestCase):
|
||||
self.assertEqual(gltf["asset"]["version"], "2.0")
|
||||
self.assertEqual(len(gltf["scenes"]), 1)
|
||||
primitives = gltf["meshes"][0]["primitives"]
|
||||
self.assertEqual(len(primitives), len(prop("bench")["parts"]),
|
||||
"one primitive per part")
|
||||
self.assertEqual(len(gltf["materials"]), len(primitives))
|
||||
self.assertEqual(len(primitives), 1,
|
||||
"parts share one primitive (one material)")
|
||||
self.assertEqual(len(gltf["materials"]), 1,
|
||||
"the prop draws with a single material")
|
||||
for primitive in primitives:
|
||||
self.assertIn("POSITION", primitive["attributes"])
|
||||
self.assertIn("NORMAL", primitive["attributes"])
|
||||
self.assertIn("TEXCOORD_0", primitive["attributes"],
|
||||
"atlas UVs are required")
|
||||
self.assertIsNotNone(primitive["indices"])
|
||||
self.assertLess(primitive["material"], len(gltf["materials"]))
|
||||
self.assertEqual(primitive["material"], 0)
|
||||
for accessor in gltf["accessors"]:
|
||||
self.assertIn(accessor["bufferView"],
|
||||
range(len(gltf["bufferViews"])))
|
||||
@@ -114,31 +117,41 @@ class GlbTests(unittest.TestCase):
|
||||
second = model_pipeline.build_glb(prop("bollard"))
|
||||
self.assertEqual(first, second)
|
||||
|
||||
def test_palette_override_changes_material_colour(self):
|
||||
def test_palette_override_changes_the_atlas_colour(self):
|
||||
palette = {role: list(rgb)
|
||||
for role, rgb in model_pipeline.PALETTE.items()}
|
||||
palette["metal"] = [0.9, 0.1, 0.1]
|
||||
gltf = parse_glb(model_pipeline.build_glb(
|
||||
prop("bollard"), palette))["gltf"]
|
||||
factors = [m["pbrMetallicRoughness"]["baseColorFactor"]
|
||||
for m in gltf["materials"]]
|
||||
self.assertIn(model_pipeline._linear_rgba([0.9, 0.1, 0.1]), factors)
|
||||
atlas = model_pipeline.atlas_for(prop("bollard"), palette)
|
||||
self.assertIn((0.9, 0.1, 0.1), atlas["colours"].values())
|
||||
default = model_pipeline.build_glb(prop("bollard"))
|
||||
overridden = model_pipeline.build_glb(prop("bollard"), palette)
|
||||
self.assertNotEqual(default, overridden,
|
||||
"the atlas texture carries the palette")
|
||||
|
||||
def test_glb_colours_are_written_in_linear_space(self):
|
||||
# glTF baseColorFactor is linear; writing sRGB made props render pastel.
|
||||
def test_material_uses_both_atlas_textures(self):
|
||||
gltf = parse_glb(model_pipeline.build_glb(prop("bollard")))["gltf"]
|
||||
factor = gltf["materials"][0]["pbrMetallicRoughness"]["baseColorFactor"]
|
||||
srgb_metal = model_pipeline.PALETTE["metal"][0]
|
||||
self.assertLess(factor[0], srgb_metal)
|
||||
self.assertAlmostEqual(factor[0],
|
||||
model_pipeline.srgb_to_linear(srgb_metal),
|
||||
places=4)
|
||||
pbr = gltf["materials"][0]["pbrMetallicRoughness"]
|
||||
self.assertEqual(pbr["metallicFactor"], 1.0)
|
||||
self.assertEqual(pbr["roughnessFactor"], 1.0)
|
||||
self.assertIn("baseColorTexture", pbr)
|
||||
self.assertIn("metallicRoughnessTexture", pbr)
|
||||
self.assertEqual(len(gltf["images"]), 2)
|
||||
self.assertEqual(len(gltf["textures"]), 2)
|
||||
for image in gltf["images"]:
|
||||
self.assertEqual(image["mimeType"], "image/png")
|
||||
self.assertEqual(gltf["samplers"][0]["magFilter"], 9728,
|
||||
"nearest sampling keeps palette cells flat")
|
||||
|
||||
def test_srgb_to_linear_reference_values(self):
|
||||
self.assertAlmostEqual(model_pipeline.srgb_to_linear(0.0), 0.0)
|
||||
self.assertAlmostEqual(model_pipeline.srgb_to_linear(1.0), 1.0)
|
||||
self.assertAlmostEqual(model_pipeline.srgb_to_linear(0.5), 0.2140,
|
||||
places=4)
|
||||
def test_atlas_is_deterministic_and_power_of_two(self):
|
||||
first = model_pipeline.atlas_for(prop("planter_box"))
|
||||
second = model_pipeline.atlas_for(prop("planter_box"))
|
||||
self.assertEqual(first["base_png"], second["base_png"])
|
||||
size = first["size"]
|
||||
self.assertEqual(size & (size - 1), 0, "atlas size is a power of two")
|
||||
for u, v in first["role_uv"].values():
|
||||
self.assertTrue(0.0 < u < 1.0 and 0.0 < v < 1.0,
|
||||
"UVs stay on the texture")
|
||||
self.assertEqual(len(first["roles"]), len(set(first["roles"])))
|
||||
|
||||
def test_indices_stay_inside_their_primitive(self):
|
||||
payload = model_pipeline.build_glb(prop("planter_box"))
|
||||
|
||||
Reference in new issue
Block a user