less copy protection, more size visualization
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

426 lines
16 KiB

  1. import bpy
  2. from mathutils import Vector, Euler, Color
  3. import json
  4. import pathlib
  5. import os
  6. from math import pi
  7. import random
  8. import bmesh
  9. VIEW_DATA = {
  10. "FRONT": [0, 1, 2, "Front"],
  11. "ANGLED": [0.25, 1, 2, "Angled"],
  12. "CORNER": [0.5, 1, 2, "Corner"],
  13. "SIDE": [1, 1, 2, "Side"],
  14. "BACK_CORNER": [1.5, 1, 2, "Back Corner"],
  15. "BACK": [2, 1, 2, "Back"],
  16. "TOP": [0, 0, 1, "Top"],
  17. "BOTTOM": [0, 2, 1, "Bottom"],
  18. "BOTTOM_FLIPPED": [2, 2, 1, "Bottom Flipped"],
  19. }
  20. def read_sci(property):
  21. return property.base * pow(10, property.power)
  22. def get_bounds(objects):
  23. xl = []
  24. yl = []
  25. zl = []
  26. for obj in objects:
  27. if not obj.hide_render:
  28. for bounds in obj.bound_box:
  29. v = obj.matrix_world @ Vector(bounds)
  30. xl += [v[0] for c in obj.bound_box]
  31. yl += [v[1] for c in obj.bound_box]
  32. zl += [v[2] for c in obj.bound_box]
  33. return (
  34. Vector([min(xl), min(yl), min(zl)]),
  35. Vector([min(xl), min(yl), max(zl)]),
  36. Vector([min(xl), max(yl), min(zl)]),
  37. Vector([min(xl), max(yl), max(zl)]),
  38. Vector([max(xl), min(yl), min(zl)]),
  39. Vector([max(xl), min(yl), max(zl)]),
  40. Vector([max(xl), max(yl), min(zl)]),
  41. Vector([max(xl), max(yl), max(zl)]),
  42. )
  43. class MVConfigCollection(bpy.types.Operator):
  44. bl_idname = "mv.config_collection"
  45. bl_label = "Configure root collection"
  46. @classmethod
  47. def poll(cls, context: bpy.types.Context):
  48. return True
  49. def execute(self, context: bpy.types.Context):
  50. coll = context.scene.collection.children[0]
  51. coll.name = "Macrovision"
  52. mats = [
  53. ("light", 0, 0, 1),
  54. ("medium", 0, 1, 0),
  55. ("dark", 1, 0, 0)
  56. ]
  57. for name, red, green, blue in mats:
  58. if name not in bpy.data.materials:
  59. bpy.data.materials.new(name)
  60. mat = bpy.data.materials[name]
  61. mat.use_nodes = True
  62. mat.use_fake_user = True
  63. mat.node_tree.nodes.clear()
  64. rgb = mat.node_tree.nodes.new("ShaderNodeRGB")
  65. out = mat.node_tree.nodes.new("ShaderNodeOutputMaterial")
  66. input = out.inputs['Surface']
  67. output = rgb.outputs['Color']
  68. mat.node_tree.links.new(input=input, output=output)
  69. output.default_value = (red, green, blue, 1.0)
  70. if "cam" not in bpy.data.objects:
  71. new_cam = bpy.data.cameras.new("cam")
  72. new_obj = bpy.data.objects.new("cam", new_cam)
  73. context.scene.collection.objects.link(new_obj)
  74. if "cam2" not in bpy.data.objects:
  75. new_cam = bpy.data.cameras.new("cam2")
  76. new_obj = bpy.data.objects.new("cam2", new_cam)
  77. context.scene.collection.objects.link(new_obj)
  78. if "Lines" not in bpy.data.materials:
  79. mat = bpy.data.materials.new("Lines")
  80. bpy.data.materials.create_gpencil_data(mat)
  81. if "lineart" not in bpy.data.objects:
  82. new_lineart = bpy.data.grease_pencils.new("lineart")
  83. new_obj = bpy.data.objects.new("lineart", new_lineart)
  84. new_obj.show_in_front = True
  85. context.scene.collection.objects.link(new_obj)
  86. modifier = new_obj.grease_pencil_modifiers.new(name='Lineart', type='GP_LINEART')
  87. material = bpy.data.materials["Lines"]
  88. new_lineart.materials.append(material)
  89. modifier.target_layer = "Lines"
  90. modifier.target_material = material
  91. new_lineart.layers.new("Lines")
  92. # we have to clear the bake, for some reason
  93. bpy.ops.object.lineart_clear_all()
  94. new_obj.grease_pencil_modifiers[0].use_custom_camera = True
  95. new_obj.grease_pencil_modifiers[0].source_camera = bpy.data.objects["cam2"]
  96. return {'FINISHED'}
  97. class MVAssignMaterials(bpy.types.Operator):
  98. bl_idname = "mv.assign_materials"
  99. bl_label = "Assign Materials"
  100. def execute(self, context: bpy.types.Context):
  101. mv = bpy.data.collections["Macrovision"]
  102. collections = mv.children
  103. for coll in collections:
  104. object: bpy.types.Object
  105. for object in coll.objects:
  106. if object.type != "MESH":
  107. continue
  108. if context.scene.mv_material_mode == 'RANDOM':
  109. for index in range(len(object.material_slots)):
  110. choices = [
  111. bpy.data.materials['light'],
  112. bpy.data.materials['medium'],
  113. bpy.data.materials['dark']
  114. ]
  115. object.material_slots[index].material = random.choice(choices)
  116. elif context.scene.mv_material_mode == 'NAMES':
  117. for index in range(len(object.material_slots)):
  118. if object.material_slots[index].material.name in ('light', 'medium', 'dark'):
  119. continue
  120. material = object.material_slots[index].material
  121. light_choices = context.scene.mv_material_names_light.split(",")
  122. medium_choices = context.scene.mv_material_names_medium.split(",")
  123. chosen = None
  124. for choice in light_choices:
  125. if choice in material.name or choice in object.name:
  126. chosen = bpy.data.materials['light']
  127. break
  128. for choice in medium_choices:
  129. if choice in material.name or choice in object.name:
  130. chosen = bpy.data.materials['medium']
  131. break
  132. if chosen is None:
  133. chosen = bpy.data.materials['dark']
  134. object.material_slots[index].material = chosen
  135. elif context.scene.mv_material_mode == 'FACE_MAPS':
  136. while len(object.material_slots) > 0:
  137. bpy.ops.object.material_slot_remove({'object': object})
  138. light_choices = context.scene.mv_material_names_light.split(",")
  139. medium_choices = context.scene.mv_material_names_medium.split(",")
  140. for map in object.face_maps:
  141. chosen = None
  142. if map.name in light_choices:
  143. chosen = bpy.data.materials['light']
  144. if map.name in medium_choices:
  145. chosen = bpy.data.materials['medium']
  146. if chosen is None:
  147. chosen = bpy.data.materials['dark']
  148. bpy.ops.object.material_slot_add({'object': object})
  149. index = len(object.material_slots) - 1
  150. object.material_slots[index].material = chosen
  151. bpy.ops.object.material_slot_add({'object': object})
  152. missing_idx = len(object.material_slots) - 1
  153. object.material_slots[missing_idx].material = bpy.data.materials['dark']
  154. bm: bpy.types.BMesh
  155. bpy.ops.object.select_all(action='DESELECT')
  156. object.select_set(True)
  157. bpy.ops.object.mode_set(mode = 'EDIT')
  158. bm = bmesh.from_edit_mesh(object.data)
  159. fm = bm.faces.layers.face_map.verify()
  160. for face in bm.faces:
  161. idx = face[fm]
  162. print(idx)
  163. if idx >= 0:
  164. face.material_index = idx
  165. else:
  166. face.material_index = missing_idx
  167. object.data.update()
  168. bm.free()
  169. print("OK")
  170. bpy.ops.object.mode_set(mode = 'OBJECT')
  171. elif context.scene.mv_material_mode == 'OBJECT_NAMES':
  172. while len(object.material_slots) > 0:
  173. bpy.ops.object.material_slot_remove({'object': object})
  174. light_choices = context.scene.mv_material_names_light.split(",")
  175. medium_choices = context.scene.mv_material_names_medium.split(",")
  176. bpy.ops.object.material_slot_add({'object': object})
  177. missing_idx = len(object.material_slots) - 1
  178. chosen = 'dark'
  179. if any([choice in object.name for choice in medium_choices]):
  180. chosen = 'medium'
  181. if any([choice in object.name for choice in light_choices]):
  182. chosen = 'light'
  183. object.material_slots[0].material = bpy.data.materials[chosen]
  184. return {'FINISHED'}
  185. class MVExport(bpy.types.Operator):
  186. bl_idname = "mv.export"
  187. bl_label = "Export objects"
  188. def execute(self, context: bpy.context):
  189. path_info = pathlib.Path(bpy.data.filepath).parent.joinpath("macrovision-directory.txt")
  190. config_path = pathlib.Path(open(path_info).read().strip())
  191. json_path = config_path.joinpath("config.json")
  192. config = json.load(open(json_path.resolve(), encoding="utf-8"))
  193. parent_workdir = config["work-directory"]
  194. c = bpy.data.objects["cam"]
  195. c2 = bpy.data.objects["cam2"]
  196. context.scene.camera = c
  197. lineart = bpy.data.objects["lineart"]
  198. lineart.grease_pencil_modifiers['Lineart'].source_type = 'COLLECTION'
  199. #lineart.data.stroke_thickness_space = 'SCREENSPACE'
  200. c.data.type = "ORTHO"
  201. c2.data.type = "ORTHO"
  202. bpy.data.scenes["Scene"].render.resolution_x = 2000
  203. bpy.data.scenes["Scene"].render.resolution_y = 2000
  204. bpy.data.scenes["Scene"].render.film_transparent = True
  205. bpy.data.scenes["Scene"].view_settings.view_transform = "Raw"
  206. bpy.data.worlds["World"].node_tree.nodes["Background"].inputs[1].default_value = 0
  207. mv = bpy.data.collections["Macrovision"]
  208. collections = mv.children
  209. all_data = {}
  210. all_data["name"] = context.scene.mv_name
  211. all_data["kind"] = context.scene.mv_kind
  212. all_data["trace_alpha"] = context.scene.mv_trace_mode
  213. all_data["forms"] = []
  214. default_views = []
  215. for view in context.scene.mv_views:
  216. key = view.view
  217. default_views.append([
  218. VIEW_DATA[key][0],
  219. VIEW_DATA[key][1],
  220. VIEW_DATA[key][2],
  221. view.name if view.name != "" else VIEW_DATA[key][3]
  222. ])
  223. print(default_views)
  224. workdir = pathlib.Path(parent_workdir).joinpath(all_data["name"])
  225. os.makedirs(workdir, exist_ok=True)
  226. coll: bpy.types.Collection
  227. for coll in collections:
  228. coll.hide_render = True
  229. for coll in collections:
  230. if bpy.context.scene.view_layers['ViewLayer'].layer_collection.children[0].children[coll.name].exclude:
  231. continue
  232. coll.hide_render = False
  233. bpy.ops.object.select_all(action='DESELECT')
  234. for obj in coll.objects:
  235. obj.select_set(True)
  236. data = {}
  237. data["name"] = coll.name
  238. data["views"] = []
  239. bounds = get_bounds(coll.objects)
  240. bounds_seq = []
  241. for bound in bounds:
  242. bounds_seq += [bound[0], bound[1], bound[2]]
  243. bound_min = bounds[0]
  244. bound_max = bounds[-1]
  245. dimensions = bound_max - bound_min
  246. size = max(dimensions)
  247. global_bbox_center = 0.5 * (bound_min + bound_max)
  248. view_list = []
  249. lineart.grease_pencil_modifiers['Lineart'].source_collection = coll
  250. if "Views" in coll:
  251. for view in coll["Views"].split(","):
  252. view_list.append(VIEW_DATA[view])
  253. else:
  254. view_list = default_views
  255. for angles in view_list:
  256. c.location = global_bbox_center
  257. c.rotation_euler = Euler([angles[1] * pi / 2, 0, angles[0] * pi / 2])
  258. print(list(bound_min) + list(bound_max))
  259. _, c.data.ortho_scale = c.camera_fit_coords(bpy.context.evaluated_depsgraph_get(), bounds_seq)
  260. c.location = Vector([c.location[0], c.location[1], c.location[2]])
  261. c.data.ortho_scale *= 1.1
  262. rot = c.rotation_euler.to_matrix()
  263. rot.invert()
  264. c.location += Vector([0, 0, size * 2]) @ rot
  265. c.data.clip_start = size / 4
  266. c.data.clip_end = size * 8
  267. c2.rotation_euler = Euler([angles[1] * pi / 2 + 0.001, 0.001, angles[0] * pi / 2 + 0.001])
  268. c2.location = c.location
  269. c2.data.ortho_scale = c.data.ortho_scale
  270. c2.data.clip_start = c.data.clip_start
  271. c2.data.clip_end = c2.data.clip_end
  272. scale_factor = (10 ** context.scene.mv_scale_factor)
  273. height = dimensions[angles[2]] * scale_factor
  274. view_data = {
  275. "name": angles[3],
  276. "height": height
  277. }
  278. volume_mode = context.scene.mv_entity_volume_mode
  279. mass_mode = context.scene.mv_entity_mass_mode
  280. volume = None
  281. if volume_mode == "MANUAL":
  282. volume = read_sci(coll.mv_entity_volume)
  283. else:
  284. # this over-estimates, since overlapping objects will count the same space
  285. volume = 0
  286. for object in coll.objects:
  287. if object.type == "MESH":
  288. bm = bmesh.new()
  289. bm.from_object(object, context.evaluated_depsgraph_get())
  290. volume += bm.calc_volume()
  291. bm.free()
  292. mass = None
  293. if mass_mode == "MANUAL":
  294. mass = read_sci(coll.mv_entity_mass)
  295. elif mass_mode == "DENSITY":
  296. mass = volume * read_sci(coll.mv_entity_density) * 1000
  297. if volume_mode != "OFF":
  298. view_data["volume"] = volume * (scale_factor ** 3)
  299. if mass is not None:
  300. view_data["mass"] = mass * (scale_factor ** 3)
  301. data["views"].append(view_data)
  302. lineart.hide_render = False
  303. filename = f"{coll.name}-{angles[3]}.png"
  304. bpy.context.scene.render.filepath = workdir.joinpath(filename).resolve().__str__()
  305. bpy.ops.render.render(write_still = True)
  306. lineart.hide_render = True
  307. filename = f"{coll.name}-{angles[3]}-noline.png"
  308. bpy.context.scene.render.filepath = workdir.joinpath(filename).resolve().__str__()
  309. bpy.ops.render.render(write_still = True)
  310. all_data["forms"].append(data)
  311. coll.hide_render = True
  312. with open(workdir.joinpath("data.json"), "w") as file:
  313. json.dump(all_data, file)
  314. return {'FINISHED'}
  315. clses = [
  316. MVExport,
  317. MVAssignMaterials,
  318. MVConfigCollection
  319. ]