feat(rover): add live scene and material editing with environment migration

This commit is contained in:
DCCONSTRUCTIONS
2026-09-25 19:13:52 +03:00
parent 9d3329f608
commit a993eda6b8
24 changed files with 3640 additions and 471 deletions
+53 -32
View File
@@ -1,49 +1,70 @@
"""Immutable-source evaluated geometry export, owner-selected complete v020."""
import bpy,json,hashlib,struct
"""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
from mathutils import Vector, Matrix
root=Path.cwd();source=Path(bpy.data.filepath)
donor=root.parent/'NODEDC_ENGINE_INFRA/nodedc-source/public/3dassetnode/model.glb'
with donor.open('rb') as f:
f.read(12);n,t=struct.unpack('<II',f.read(8));donor_json=json.loads(f.read(n))
# Same physical PBR parameters as the drone's untextured metal/rubber/steel.
material_rules={'metal':1,'rubber':2,'steel':6,'paint':3}
# 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 kind,index in material_rules.items():
spec=donor_json['materials'][index]['pbrMetallicRoughness'];m=bpy.data.materials.new('NodeDC '+kind);m.use_nodes=True
bs=m.node_tree.nodes.get('Principled BSDF');bs.inputs['Base Color'].default_value=spec.get('baseColorFactor',[1,1,1,1]);bs.inputs['Metallic'].default_value=spec.get('metallicFactor',1);bs.inputs['Roughness'].default_value=spec.get('roughnessFactor',1);materials[kind]=m
source_scene=bpy.context.scene;source_scene.frame_set(1);dg=bpy.context.evaluated_depsgraph_get()
instances=[]
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(('STUDIO' in c or 'BOOLEAN' in c or 'DATUM' in c) for c in cols):continue
# Exclude standalone instancing source collections; their placed instances survive.
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)
names=[slot.material.name.lower() if slot.material else '' for slot in o.material_slots]
for idx in range(len(mesh.materials)):
name=names[idx] if idx<len(names) else ''
kind='rubber' if any(k in name for k in ['rubber','gasket','polymer','elastomer','jacket','asa']) else 'steel' if any(k in name for k in ['zinc','steel','brass','spring']) else 'paint' if any(k in name for k in ['paint','coated','black','charcoal']) else 'metal'
mesh.materials[idx]=materials[kind]
if not mesh.materials:mesh.materials.append(materials['metal'])
instances.append((src.name,mesh,inst.matrix_world.copy(),inst.is_instance))
scene=bpy.data.scenes.new('Mission Core rover export');bpy.context.window.scene=scene
objects=[]
for name,mesh,matrix,_ in instances:
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
source_object_count=len(objects)
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')
bpy.context.view_layer.objects.active=objects[0]
bpy.ops.object.join()
joined=bpy.context.object
for uv in list(joined.data.uv_layers):joined.data.uv_layers.remove(uv)
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=False,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','source_object_count':source_object_count,'render_mesh_count':1,'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(),'material_source_sha256':hashlib.sha256(donor.read_bytes()).hexdigest(),'material_rules':material_rules,'source_saved':False}
(out.parent/'dcd006-v020.json').write_text(json.dumps(manifest,ensure_ascii=False,indent=2)+'\n');print(json.dumps(manifest))
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')