"""Evaluated v020 geometry, semantic PBR groups and UVs; never saves the source.""" import bpy, json, hashlib from collections import defaultdict from pathlib import Path from mathutils import Vector, Matrix root=Path.cwd();source=Path(bpy.data.filepath) # Linear colors. Painted black is a dielectric coating, not bare silver. specs={ 'body':('Корпус и подвеска',(.025,.028,.031),.12,.53), 'rubber':('Резина гусениц и катков',(.014,.017,.019),0,.72), 'track-steel':('Нержавеющие проставки',(.55,.58,.61),1,.17), 'box':('Бортовой ящик',(.034,.038,.042),.08,.40), 'molle':('MOLLE панель',(.085,.094,.105),.30,.44), 'motor':('Алюминий моторов',(.30,.32,.33),.68,.38), 'hardware':('Чёрный крепёж',(.018,.021,.024),.45,.38), 'polymer':('Пластик и кожухи',(.018,.022,.026),0,.44), 'drive':('Ведущие звёзды',(.22,.13,.038),0,.46), } materials={} for key,(label,color,metal,rough) in specs.items(): m=bpy.data.materials.new('rover/'+key);m.use_nodes=True;bs=m.node_tree.nodes.get('Principled BSDF') bs.inputs['Base Color'].default_value=(*color,1);bs.inputs['Metallic'].default_value=metal;bs.inputs['Roughness'].default_value=rough;materials[key]=m def classify(name,obj): n=name.lower();o=obj.lower() if 'stamped clips' in n or 'inter-window clips' in o:return 'track-steel' if 'molle' in n:return 'molle' if 'motor | cast' in n:return 'motor' if 'polyurethane' in n:return 'drive' if any(k in n for k in ['rubber','gasket','elastomer','jacket','coated roller']):return 'rubber' if any(k in n for k in ['case | textured','case | dark raised']):return 'box' if any(k in n+' '+o for k in ['bolt','screw','washer','nut','lock steel','zinc','stainless','connector brass','dark steel','black oxide']):return 'hardware' if any(k in n for k in ['polymer','asa','recess','cavities']):return 'polymer' return 'body' bpy.context.scene.frame_set(1);dg=bpy.context.evaluated_depsgraph_get();instances=[];assignment=defaultdict(set) for inst in dg.object_instances: o=inst.object;src=o.original;cols=[c.name for c in src.users_collection] if o.type not in {'MESH','CURVE'} or src.hide_render:continue if any(c.hide_render for c in src.users_collection) or any(any(t in c for t in ['STUDIO','BOOLEAN','DATUM']) for c in cols):continue if not inst.is_instance and not src.visible_get():continue mesh=bpy.data.meshes.new_from_object(o,preserve_all_data_layers=True,depsgraph=dg) for idx in range(len(mesh.materials)): name=mesh.materials[idx].name if mesh.materials[idx] else '';key=classify(name,src.name) mesh.materials[idx]=materials[key];assignment[key].add(src.name) if not mesh.materials:mesh.materials.append(materials['body']);assignment['body'].add(src.name) instances.append((src.name,mesh,inst.matrix_world.copy())) scene=bpy.data.scenes.new('Mission Core rover export');bpy.context.window.scene=scene;objects=[] for name,mesh,matrix in instances: o=bpy.data.objects.new(name,mesh);scene.collection.objects.link(o);o.matrix_world=matrix;objects.append(o) bpy.context.view_layer.update() lo=Vector((min((o.matrix_world@Vector(v))[i] for o in objects for v in o.bound_box) for i in range(3))) hi=Vector((max((o.matrix_world@Vector(v))[i] for o in objects for v in o.bound_box) for i in range(3))) shift=Vector((-(lo.x+hi.x)/2,-(lo.y+hi.y)/2,-lo.z)) for o in objects:o.matrix_world=Matrix.Translation(shift)@o.matrix_world count=len(objects) bpy.ops.object.select_all(action='SELECT');bpy.context.view_layer.objects.active=objects[0];bpy.ops.object.join() # Separate by material keeps picking exact while avoiding 1,426 draw calls. bpy.ops.object.mode_set(mode='EDIT');bpy.ops.mesh.select_all(action='SELECT');bpy.ops.mesh.separate(type='MATERIAL');bpy.ops.object.mode_set(mode='OBJECT') for o in list(scene.objects): bpy.ops.object.select_all(action='DESELECT');o.select_set(True);bpy.context.view_layer.objects.active=o key=o.data.materials[0].name.removeprefix('rover/');o.name=specs[key][0] for uv in list(o.data.uv_layers):o.data.uv_layers.remove(uv) bpy.ops.object.mode_set(mode='EDIT');bpy.ops.mesh.select_all(action='SELECT');bpy.ops.uv.cube_project(cube_size=.2);bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.select_all(action='SELECT') out=root/'apps/control-station/public/rover-scene/dcd006-v020.glb' bpy.ops.export_scene.gltf(filepath=str(out),export_format='GLB',use_selection=True,use_active_scene=True,export_animations=False,export_cameras=False,export_lights=False,export_extras=False,export_yup=True,export_apply=False,export_texcoords=True,export_attributes=False,export_vertex_color='NONE') manifest={'source':source.name,'source_sha256':hashlib.sha256(source.read_bytes()).hexdigest(),'model':'DCD-006 v020 complete rover','revision':'materials-v2','source_object_count':count,'render_mesh_count':len(scene.objects),'original_bounds':[list(lo),list(hi)],'dimensions_m':list(hi-lo),'translation_m':list(shift),'glb_bytes':out.stat().st_size,'glb_sha256':hashlib.sha256(out.read_bytes()).hexdigest(),'source_saved':False,'uv':'cube projection, 0.2 m','materials':{k:{'name':v[0],'baseColorLinear':v[1],'metalness':v[2],'roughness':v[3],'sourceObjects':sorted(assignment[k])} for k,v in specs.items()}} (out.parent/'dcd006-v020.json').write_text(json.dumps(manifest,ensure_ascii=False,indent=2)+'\n') print('ROVER EXPORT',out.stat().st_size,'bytes;',len(scene.objects),'material groups')