"""Export the district kit to the runtime GLB (positions + indices only). blender -b assets/blender/districts/nightdrive-districts.blend --python assets/blender/districts/export-districts.py [-- --preview out.png] Normals are derived per fragment at runtime (flat shading from screen-space derivatives), so the GLB carries no normals, UVs or materials and shared box corners stay welded. Writes provenance next to this script. """ import bpy, json, hashlib, sys, math from pathlib import Path here = Path(__file__).resolve().parent root = here.parents[2] scene = bpy.data.scenes['Roadside Districts'] output = root / 'src/assets/nightdrive-districts.glb' # Retired in round 6: the overhead gantry and the sign truss now own the road, # and the billboards' light strips read as garbled text. They stay authored in # the .blend but are no longer exported. RETIRED = {'gantry', 'billboard'} exported = [o for o in scene.objects if o.name.rsplit('_', 1)[0] not in RETIRED] for obj in scene.objects: obj.select_set(obj in exported) bpy.ops.export_scene.gltf( filepath=str(output), export_format='GLB', use_selection=True, use_active_scene=True, export_apply=True, export_normals=False, export_texcoords=False, export_materials='NONE', export_animations=False, export_cameras=False, export_lights=False, export_yup=True, ) triangles = 0 for obj in exported: obj.data.calc_loop_triangles() triangles += len(obj.data.loop_triangles) provenance = { 'asset': 'Nightdrive roadside districts', 'source': 'nightdrive-districts.blend', 'authorship': 'Original procedural modelling in Blender for this portfolio (author-districts.py); no external assets.', 'editable_scene': 'Roadside Districts', 'pieces': sorted({o.name.rsplit('_', 1)[0] for o in exported}), 'retired': sorted(RETIRED), 'triangles': triangles, 'attributes': 'POSITION + indices; flat normals derived at runtime', 'export': '../../../src/assets/nightdrive-districts.glb', 'sha256': hashlib.sha256(output.read_bytes()).hexdigest(), } (here / 'nightdrive-districts.asset.json').write_text(json.dumps(provenance, indent=2) + '\n') print(json.dumps(provenance)) if '--preview' in sys.argv: target = sys.argv[sys.argv.index('--preview') + 1] names = provenance['pieces'] for i, name in enumerate(names): for suffix in ('body', 'light'): obj = scene.objects[f'{name}_{suffix}'] obj.location = ((i % 4) * 34 - 50, (i // 4) * 40, 0) mat = bpy.data.materials.new(f'preview-{suffix}') mat.diffuse_color = (0.05, 0.05, 0.08, 1) if suffix == 'body' else (0.1, 0.9, 1.0, 1) obj.data.materials.append(mat) cam = bpy.data.objects.new('preview-cam', bpy.data.cameras.new('preview-cam')) scene.collection.objects.link(cam) cam.location = (95, -95, 70) cam.rotation_euler = (math.radians(58), 0, math.radians(45)) cam.data.lens = 30 scene.camera = cam try: scene.render.engine = 'BLENDER_WORKBENCH' except TypeError as e: print(e) scene.display.shading.color_type = 'MATERIAL' scene.display.shading.light = 'STUDIO' scene.display.shading.show_cavity = True scene.render.resolution_x, scene.render.resolution_y = 1600, 1000 scene.render.filepath = target bpy.ops.render.render(write_still=True)