__init__.py 33 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066
  1. # ----------------------------------------------------------
  2. # File __init__.py
  3. # ----------------------------------------------------------
  4. # Imports
  5. # Check if this add-on is being reloaded
  6. if "bpy" in locals():
  7. # reloading .py files
  8. import bpy
  9. import importlib
  10. # from . import zs_renderscene as zsrs
  11. # importlib.reload(zsrs)
  12. # or if this is the first load of this add-on
  13. else:
  14. import bpy
  15. import json
  16. from pathlib import Path
  17. from dataclasses import dataclass
  18. from mathutils import Euler, Vector, Quaternion, Matrix
  19. import math
  20. import os
  21. import base64
  22. import numpy as np
  23. # from . import zs_renderscene as zsrs # noqa
  24. # Addon info
  25. bl_info = {
  26. "name": "ZS Stable Diffusion Connection V0.0.1",
  27. "author": "Sergiu <sergiu@zixelise.com>",
  28. "Version": (0, 0, 1),
  29. "blender": (4, 00, 0),
  30. "category": "Scene",
  31. "description": "Stable Diffusion Connection",
  32. }
  33. # -------------------------------------------------------------------
  34. # Convert Data Functions
  35. # -------------------------------------------------------------------
  36. def convert_loc(x):
  37. return u * Vector([x[0], -x[2], x[1]])
  38. def convert_quat(q):
  39. return Quaternion([q[3], q[0], -q[2], q[1]])
  40. def convert_scale(s):
  41. return Vector([s[0], s[2], s[1]])
  42. def local_rotation(obj, rotation_before, rot):
  43. """Appends a local rotation to vnode's world transform:
  44. (new world transform) = (old world transform) @ (rot)
  45. without changing the world transform of vnode's children.
  46. For correctness, rot must be a signed permutation of the axes
  47. (eg. (X Y Z)->(X -Z Y)) OR vnode's scale must always be uniform.
  48. """
  49. rotation_before = Quaternion((1, 0, 0, 0))
  50. obj.rotation_before @= rot
  51. # # Append the inverse rotation after children's TRS to cancel it out.
  52. # rot_inv = rot.conjugated()
  53. # for child in gltf.vnodes[vnode_id].children:
  54. # gltf.vnodes[child].rotation_after = rot_inv @ gltf.vnodes[child].rotation_after
  55. # -------------------------------------------------------------------
  56. # Functions
  57. # -------------------------------------------------------------------
  58. @dataclass
  59. class AssetData:
  60. path: str
  61. collection_name: str
  62. type: str
  63. product_asset = AssetData(
  64. path="sample_scene/01_Products", collection_name="01_Products", type="product"
  65. )
  66. element_asset = AssetData(
  67. path="sample_scene/02_Elements", collection_name="02_Elements", type="asset"
  68. )
  69. basic_shapes_asset = AssetData(
  70. path="sample_scene/03_BasicShapes",
  71. collection_name="03_BasicShapes",
  72. type="shape",
  73. )
  74. def convert_base64_string_to_object(base64_string):
  75. bytes = base64.b64decode(base64_string)
  76. string = bytes.decode("ascii")
  77. return json.loads(string)
  78. return string
  79. def load_scene():
  80. print("Loading Scene")
  81. # load scene data
  82. scene_data = load_scene_data()
  83. print(scene_data)
  84. # create parent collections
  85. create_parent_collections(product_asset.collection_name)
  86. create_parent_collections(element_asset.collection_name)
  87. create_parent_collections(basic_shapes_asset.collection_name)
  88. create_parent_collections("05_Cameras")
  89. # append products
  90. products_data = load_objects_data(scene_data, product_asset.type)
  91. for index, product in enumerate(products_data):
  92. append_objects(product, index, product_asset)
  93. # append elements
  94. elements_data = load_objects_data(scene_data, element_asset.type)
  95. for index, product in enumerate(elements_data):
  96. append_objects(product, index, element_asset)
  97. # append shapes
  98. basic_shapes_data = load_objects_data(scene_data, basic_shapes_asset.type)
  99. for index, product in enumerate(basic_shapes_data):
  100. append_objects(product, index, basic_shapes_asset)
  101. # set lighting
  102. set_environment(scene_data)
  103. # set camera
  104. create_cameras(scene_data)
  105. # rename outputs
  106. set_output_paths(
  107. "D://Git//ap-canvas-creation-module//03_blender//sd_blender//sample_scene//Renders",
  108. scene_data["project_id"],
  109. )
  110. # setup compositing
  111. set_cryptomatte_objects("01_Products", "mask_product")
  112. def invert_id_name(json_data):
  113. for obj in json_data["scene"]["objects"]:
  114. obj["name"], obj["id"] = obj["id"], obj["name"]
  115. return json_data
  116. def load_scene_data():
  117. # print("Loading Scene Data")
  118. # # load scene data
  119. # # to be replaced with actual data
  120. # # open scene_info.json
  121. # script_path = Path(__file__).resolve()
  122. # scene_data_path = script_path.parent / "sample_scene" / "scene_info.json"
  123. # with scene_data_path.open() as file:
  124. # scene_data = json.load(file)
  125. # print(scene_data)
  126. # return scene_data
  127. # load scene data
  128. print("Loading Scene Data")
  129. if bpy.context.scene.load_local_DB:
  130. loaded_scene_data = bpy.context.scene.config_string
  131. # check if loaded_scene_data is base64 encoded
  132. if loaded_scene_data.startswith("ey"):
  133. scene_data = convert_base64_string_to_object(loaded_scene_data)
  134. else:
  135. scene_data = json.loads(loaded_scene_data)
  136. else:
  137. scene_data = json.loads(bpy.context.scene.shot_info_ai)
  138. # invert_scene_data = invert_id_name(scene_data)
  139. return scene_data
  140. def load_objects_data(scene_data, object_type: str):
  141. print("Loading Assets Data")
  142. # load assets data
  143. objects_data = []
  144. for object in scene_data["scene"]["objects"]:
  145. if object["group_type"] == object_type:
  146. # get additional object data by id and combine with object data
  147. object_data = get_object_data_by_id(object["id"])
  148. if object_data:
  149. # temporary fix
  150. # object_data = get_object_data_by_id(object["name"])
  151. object.update(object_data)
  152. objects_data.append(object)
  153. else:
  154. print("Object not found in database", object["id"], object["name"])
  155. return objects_data
  156. # to be replaced with actual data
  157. def get_object_data_by_id(object_id: str):
  158. print("Getting Object Data")
  159. # open assets_database.json
  160. script_path = Path(__file__).resolve()
  161. assets_data_path = script_path.parent / "sample_scene" / "assets_database.json"
  162. with assets_data_path.open() as file:
  163. assets_data = json.load(file)
  164. # get object data by id
  165. for object in assets_data:
  166. if object["id"] == object_id:
  167. return object
  168. def get_hdri_data_by_id(object_id: str):
  169. print("Getting HDRI Data")
  170. # open assets_database.json
  171. script_path = Path(__file__).resolve()
  172. assets_data_path = script_path.parent / "sample_scene" / "lighting_database.json"
  173. with assets_data_path.open() as file:
  174. assets_data = json.load(file)
  175. # get object data by id
  176. for object in assets_data:
  177. if object["id"] == object_id:
  178. return object
  179. def append_objects(asset_info, index, asset_data: AssetData):
  180. print("Appending Objects")
  181. blendFileName = asset_info["name"]
  182. # visibleLayersJSONName = productInfo["Version"]
  183. # activeSwitchMaterials = json.dumps(productInfo["ActiveMaterials"])
  184. collectionName = blendFileName + "_" + str(index)
  185. append_active_layers(collectionName, asset_info, asset_data)
  186. # replace_switch_materials(shotInfo, productInfo["ActiveMaterials"])
  187. link_collection_to_collection(
  188. asset_data.collection_name, bpy.data.collections[collectionName]
  189. )
  190. def append_active_layers(newCollectionName, product_info, asset_data: AssetData):
  191. utility_collections = [
  192. "MaterialLibrary",
  193. "Animation_Controller",
  194. "Animation_Rig",
  195. "Animation_Target",
  196. ]
  197. # visible_layers = utility_collections + product_info["VisibleLayers"]
  198. visible_layers = utility_collections
  199. script_path = Path(__file__).resolve().parent
  200. filePath = str(
  201. script_path
  202. / asset_data.path
  203. / product_info["name"]
  204. / "BLEND"
  205. / (product_info["name"] + ".blend")
  206. )
  207. print(filePath)
  208. # delete all objects and collections from product collection
  209. create_parent_collections(newCollectionName)
  210. # append active collections
  211. with bpy.data.libraries.load(filePath) as (data_from, data_to):
  212. data_to.collections = []
  213. for name in data_from.collections:
  214. if name in visible_layers:
  215. data_to.collections.append(name)
  216. if "NonConfigurable" in name:
  217. data_to.collections.append(name)
  218. # link appended colections to newCollection
  219. for collection in data_to.collections:
  220. # try:
  221. # bpy.context.scene.collection.children.link(collection)
  222. # except:
  223. # print(collection)
  224. link_collection_to_collection(newCollectionName, collection)
  225. # hide utility collections
  226. for utilityCollectionName in utility_collections:
  227. if utilityCollectionName in collection.name:
  228. # rename utility collections
  229. collection.name = newCollectionName + "_" + utilityCollectionName
  230. hide_collection(collection)
  231. # need to redo this in the future
  232. if "Animation_Target" in collection.name:
  233. # set the x location
  234. collection.objects[0].location.x = product_info["properties"][
  235. "transform"
  236. ]["position"][0]
  237. # set the y location
  238. collection.objects[0].location.y = -product_info["properties"][
  239. "transform"
  240. ]["position"][2]
  241. # set the z location
  242. collection.objects[0].location.z = product_info["properties"][
  243. "transform"
  244. ]["position"][1]
  245. # collection.objects[0].location = product_info["properties"][
  246. # "transform"
  247. # ]["position"]
  248. # collection.objects[0].rotation_euler = product_info["properties"][
  249. # "transform"
  250. # ]["rotation"]
  251. rotation_in_degrees = product_info["properties"]["transform"][
  252. "rotation"
  253. ]
  254. rotation_in_degrees[0] = rotation_in_degrees[0] + 90
  255. # set object rotation in euler from radians
  256. collection.objects[0].rotation_euler = (
  257. math.radians(rotation_in_degrees[0]),
  258. math.radians(rotation_in_degrees[2]),
  259. math.radians(rotation_in_degrees[1]),
  260. )
  261. collection.objects[0].scale = product_info["properties"]["transform"][
  262. "scale"
  263. ]
  264. # make all objects in collection local
  265. for obj in bpy.data.collections[newCollectionName].all_objects:
  266. if obj.type == "MESH":
  267. obj.make_local()
  268. obj.data.make_local()
  269. # remove duplicated material slots
  270. mats = bpy.data.materials
  271. for obj in bpy.data.collections[newCollectionName].all_objects:
  272. for slot in obj.material_slots:
  273. part = slot.name.rpartition(".")
  274. if part[2].isnumeric() and part[0] in mats:
  275. slot.material = mats.get(part[0])
  276. def create_parent_collections(group_name: str):
  277. if collection_exists(group_name):
  278. remove_collection_and_objects(group_name)
  279. else:
  280. create_collection(group_name)
  281. def set_environment(scene_data):
  282. # Find the group named NG_Canvas_Background in the Blender world
  283. world = bpy.context.scene.world
  284. if world is not None:
  285. # Get the node group named NG_Canvas_Background
  286. node_group = world.node_tree.nodes.get("NG_Canvas_Background")
  287. if node_group is not None:
  288. # Set the group's properties from scene_data
  289. node_group.inputs[0].default_value = float(
  290. scene_data["scene"]["environment"]["lighting"]["rotation"]
  291. )
  292. node_group.inputs[1].default_value = float(
  293. scene_data["scene"]["environment"]["lighting"]["intensity"]
  294. )
  295. if scene_data["scene"]["environment"]["lighting"]["visible"] == True:
  296. node_group.inputs[2].default_value = 1.0
  297. else:
  298. node_group.inputs[2].default_value = 0.0
  299. node_group.inputs[3].default_value = (
  300. scene_data["scene"]["environment"]["background"]["color"]["r"],
  301. scene_data["scene"]["environment"]["background"]["color"]["g"],
  302. scene_data["scene"]["environment"]["background"]["color"]["b"],
  303. 1.0,
  304. )
  305. hdri_data = get_hdri_data_by_id(
  306. scene_data["scene"]["environment"]["lighting"]["id"]
  307. )
  308. # go inside the node group, and find the image texture node, and set the image to the one from the scene data
  309. for node in node_group.node_tree.nodes:
  310. if node.name == "environment_map":
  311. node.image = bpy.data.images.load(
  312. str(
  313. Path(__file__).resolve().parent
  314. / "sample_scene"
  315. / "05_Lighting"
  316. / "HDRI"
  317. / (hdri_data["name"] + ".exr")
  318. )
  319. )
  320. else:
  321. print("Group 'NG_Canvas_Background' not found")
  322. def calculate_focal_length(fov, film_height):
  323. # Convert FOV from degrees to radians
  324. fov_rad = math.radians(fov)
  325. # Calculate the focal length
  326. focal_length = (film_height / 2) / math.tan(fov_rad / 2)
  327. return focal_length
  328. def quaternion_multiply(q1, q2):
  329. """
  330. Multiplies two quaternions.
  331. q1 and q2 are arrays or lists of length 4.
  332. Returns the resulting quaternion as a NumPy array.
  333. """
  334. w1, x1, y1, z1 = q1
  335. w2, x2, y2, z2 = q2
  336. w = w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2
  337. x = w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2
  338. y = w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2
  339. z = w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2
  340. return np.array([w, x, y, z])
  341. def create_cameras(scene_data):
  342. # # Get the 05_Cameras collection, or create it if it doesn't exist
  343. collection_name = "05_Cameras"
  344. if collection_name in bpy.data.collections:
  345. collection = bpy.data.collections[collection_name]
  346. else:
  347. return
  348. # Assuming `scene_data` and `collection` are already defined
  349. for camera_data in scene_data["scene"]["cameras"]:
  350. # Create a new camera object
  351. bpy.ops.object.camera_add()
  352. # Get the newly created camera
  353. camera = bpy.context.object
  354. # Set the camera's name
  355. camera.name = camera_data["name"]
  356. # Set the camera's position
  357. position = camera_data["properties"]["transform"]["position"]
  358. camera.location.x = position[0]
  359. camera.location.y = -position[2]
  360. camera.location.z = position[1]
  361. # Set the camera's rotation
  362. # # euler
  363. # rotation_euler = camera_data["properties"]["transform"]["rotation"]
  364. # rotation_euler = Euler(
  365. # (
  366. # math.radians(rotation_euler[0] + 90),
  367. # math.radians(-rotation_euler[2]),
  368. # math.radians(rotation_euler[1]),
  369. # ),
  370. # "XYZ",
  371. # )
  372. # quaternion
  373. rotation_quat_data = camera_data["properties"]["transform"]["rotation"]
  374. # rotation_quat = (
  375. # rotation_quat[3],
  376. # rotation_quat[0],
  377. # rotation_quat[1],
  378. # rotation_quat[2],
  379. # )
  380. # new_quat = Quaternion((2**0.5 / 2, 2**0.5 / 2, 0.0, 0.0))
  381. # current_quat = Quaternion(rotation_quat)
  382. # result_quat = current_quat @ new_quat
  383. # rotation_quat = (result_quat.w, result_quat.x, result_quat.y, result_quat.z)
  384. rotation_quat = [
  385. rotation_quat_data[3],
  386. rotation_quat_data[0],
  387. rotation_quat_data[1],
  388. rotation_quat_data[2],
  389. ] # Example quaternion
  390. # new_quat = [2**0.5 / 2, 2**0.5 / 2, 0.0, 0.0]
  391. # result_quat = quaternion_multiply(rotation_quat, new_quat)
  392. # Example quaternion from GLTF: [x, y, z, w]
  393. gltf_quat = [0.0, 0.707, 0.0, 0.707]
  394. # Convert the GLTF quaternion to Blender space (Y-up to Z-up)
  395. converted_quat = Quaternion(
  396. [
  397. rotation_quat_data[3],
  398. rotation_quat_data[0],
  399. rotation_quat_data[1],
  400. rotation_quat_data[2],
  401. ]
  402. )
  403. # Define the camera correction quaternion (from GLTF file)
  404. camera_correction = Quaternion((2**0.5 / 2, 2**0.5 / 2, 0.0, 0.0))
  405. # Apply the camera correction to the converted quaternion
  406. corrected_quat = camera_correction @ converted_quat
  407. # Apply the corrected quaternion to the camera's rotation
  408. camera.rotation_mode = "QUATERNION"
  409. camera.rotation_quaternion = corrected_quat
  410. # camera.rotation_mode = "QUATERNION"
  411. # camera.rotation_quaternion = rotation_quat
  412. # Apply the local rotation to the camera
  413. # Set the camera's lens properties
  414. lens = camera_data["properties"]["lens"]
  415. type_mapping = {
  416. "PERSPECTIVE": "PERSP",
  417. "ORTHOGRAPHIC": "ORTHO",
  418. "PANORAMIC": "PANO",
  419. }
  420. camera.data.lens = lens["focalLength"]
  421. camera.data.type = type_mapping.get(lens["type"].upper(), "PERSP")
  422. camera.data.clip_start = lens["near"]
  423. camera.data.clip_end = lens["far"]
  424. # Add the camera to the 05_Cameras collection
  425. collection.objects.link(camera)
  426. bpy.context.scene.collection.objects.unlink(camera)
  427. # Set the camera as the active camera if "active" is true
  428. if camera_data["properties"]["active"]:
  429. bpy.context.scene.camera = camera
  430. def set_output_paths(base_path, project_name):
  431. # check if folder exist, if not create it
  432. folder_path = base_path + "//" + project_name
  433. if not os.path.exists(folder_path):
  434. os.makedirs(folder_path)
  435. # Get the current scene
  436. scene = bpy.context.scene
  437. # Check if the scene has a compositor node tree
  438. if scene.node_tree:
  439. # Iterate over all nodes in the node tree
  440. for node in scene.node_tree.nodes:
  441. # Check if the node is an output node
  442. if node.type == "OUTPUT_FILE":
  443. # Set the base path of the output node
  444. node.base_path = folder_path
  445. # Iterate over all file slots of the output node
  446. # for file_slot in node.file_slots:
  447. # # Set the path of the file slot
  448. # file_slot.path = project_name
  449. def set_cryptomatte_objects(collection_name, node_name):
  450. # Get all objects in the specified collection
  451. objects = bpy.data.collections[collection_name].all_objects
  452. # Convert the objects to a list
  453. object_names = [obj.name for obj in objects]
  454. print(object_names)
  455. # Get the current scene
  456. scene = bpy.context.scene
  457. # Check if the scene has a compositor node tree
  458. if scene.node_tree:
  459. # Iterate over all nodes in the node tree
  460. for node in scene.node_tree.nodes:
  461. # Check if the node is a Cryptomatte node with the specified name
  462. if node.type == "CRYPTOMATTE_V2" and node.name == node_name:
  463. # Set the Matte ID objects of the node
  464. node.matte_id = ",".join(object_names)
  465. # -------------------------------------------------------------------
  466. # Utilities
  467. # -------------------------------------------------------------------
  468. def remove_collection_and_objects(collection_name):
  469. oldObjects = list(bpy.data.collections[collection_name].all_objects)
  470. for obj in oldObjects:
  471. bpy.data.objects.remove(obj, do_unlink=True)
  472. old_collection = bpy.data.collections[collection_name]
  473. if old_collection is not None:
  474. old_collection_names = get_subcollection_names(old_collection)
  475. else:
  476. print("Collection not found.")
  477. # print line break
  478. print("-----------------------------------------------------------------")
  479. print(old_collection_names)
  480. print("-----------------------------------------------------------------")
  481. for old_collection_name in old_collection_names:
  482. for collection in bpy.data.collections:
  483. if collection.name == old_collection_name:
  484. bpy.data.collections.remove(collection)
  485. bpy.ops.outliner.orphans_purge(
  486. do_local_ids=True, do_linked_ids=True, do_recursive=True
  487. )
  488. def get_subcollection_names(collection):
  489. subcollection_names = []
  490. for child in collection.children:
  491. subcollection_names.append(child.name)
  492. subcollection_names.extend(get_subcollection_names(child))
  493. return subcollection_names
  494. def select_objects_in_collection(collection):
  495. """Recursively select all objects in the given collection and its subcollections."""
  496. for obj in collection.objects:
  497. obj.select_set(True)
  498. for subcollection in collection.children:
  499. select_objects_in_collection(subcollection)
  500. def link_collection_to_collection(parentCollectionName, childCollection):
  501. if bpy.context.scene.collection.children:
  502. parentCollection = bpy.context.scene.collection.children.get(
  503. parentCollectionName
  504. )
  505. # Add it to the main collection
  506. # childCollection = bpy.context.scene.collection.children.get(childCollection)
  507. parentCollection.children.link(childCollection)
  508. # if child collection is in scene collection unlink it
  509. if bpy.context.scene.collection.children.get(childCollection.name):
  510. bpy.context.scene.collection.children.unlink(childCollection)
  511. # bpy.context.scene.collection.children.unlink(childCollection)
  512. # link collection to collection
  513. def link_collection_to_collection_old(parentCollectionName, childCollection):
  514. if bpy.context.scene.collection.children:
  515. parentCollection = bpy.context.scene.collection.children.get(
  516. parentCollectionName
  517. )
  518. # Add it to the main collection
  519. try:
  520. childCollection = bpy.data.collections[childCollection]
  521. except:
  522. print("Collection not found.")
  523. return
  524. parentCollection.children.link(childCollection)
  525. bpy.context.scene.collection.children.unlink(childCollection)
  526. # function that checks if a collection exists
  527. def collection_exists(collection_name):
  528. return collection_name in bpy.data.collections
  529. # function that creates a new collection and adds it to the scene
  530. def create_collection(collection_name):
  531. new_collection = bpy.data.collections.new(collection_name)
  532. bpy.context.scene.collection.children.link(new_collection)
  533. def hide_collection(collection):
  534. collection.hide_render = True
  535. collection.hide_viewport = True
  536. def check_if_selected_objects_have_parent(self):
  537. for obj in bpy.context.selected_objects:
  538. if obj.parent is None:
  539. message = f"Object {obj.name} has no parent"
  540. self.report({"ERROR"}, message)
  541. return False
  542. return True
  543. def add_rig_controller_to_selection(self):
  544. # add "Rig_Controller_Main" to the selection
  545. has_controller_object = False
  546. for obj in bpy.data.objects:
  547. if obj.name == "Rig_Controller_Main":
  548. if obj.hide_viewport:
  549. self.report({"ERROR"}, "Rig_Controller_Main is hidden")
  550. return has_controller_object
  551. obj.select_set(True)
  552. # if object is not visible, make it visible
  553. has_controller_object = True
  554. return has_controller_object
  555. if not has_controller_object:
  556. message = f"Rig_Controller_Main not found"
  557. self.report({"ERROR"}, message)
  558. print("Rig_Controller_Main not found")
  559. return has_controller_object
  560. def select_objects_in_collection(collection):
  561. """Recursively select all objects in the given collection and its subcollections."""
  562. for obj in collection.objects:
  563. obj.select_set(True)
  564. for subcollection in collection.children:
  565. select_objects_in_collection(subcollection)
  566. def check_if_object_has_principled_material(obj):
  567. for slot in obj.material_slots:
  568. # if more than one slot is principled, return
  569. for node in slot.material.node_tree.nodes:
  570. # print(node.type)
  571. if node.type == "BSDF_PRINCIPLED":
  572. return True
  573. else:
  574. print("Object has no principled material", obj.name)
  575. return False
  576. return True
  577. def unhide_all_objects():
  578. # show all objects using operator
  579. bpy.ops.object.hide_view_clear()
  580. # -------------------------------------------------------------------
  581. # Scene optimization
  582. # -------------------------------------------------------------------
  583. def export_non_configurable_to_fbx():
  584. # Get the current .blend file path
  585. blend_filepath = bpy.data.filepath
  586. if not blend_filepath:
  587. print("Save the .blend file first.")
  588. return
  589. # Get the parent directory of the .blend file
  590. blend_dir = os.path.dirname(blend_filepath)
  591. parent_dir = os.path.dirname(blend_dir)
  592. blend_filename = os.path.splitext(os.path.basename(blend_filepath))[0]
  593. # Create the FBX export path
  594. fbx_export_path = os.path.join(parent_dir, "FBX", f"{blend_filename}.fbx")
  595. # Ensure the FBX directory exists
  596. os.makedirs(os.path.dirname(fbx_export_path), exist_ok=True)
  597. # Deselect all objects
  598. bpy.ops.object.select_all(action="DESELECT")
  599. # Select all objects in the NonConfigurable collection
  600. collection_name = "NonConfigurable"
  601. if collection_name in bpy.data.collections:
  602. collection = bpy.data.collections[collection_name]
  603. for obj in collection.objects:
  604. obj.select_set(True)
  605. else:
  606. print(f"Collection '{collection_name}' not found.")
  607. return
  608. def export_scene_to_fbx(self):
  609. # Ensure the .blend file is saved
  610. if not bpy.data.is_saved:
  611. print("Save the .blend file first.")
  612. self.report({"ERROR"}, "Save the .blend file first.")
  613. return
  614. # Get the current .blend file path
  615. blend_filepath = bpy.data.filepath
  616. if not blend_filepath:
  617. print("Unable to get the .blend file path.")
  618. return
  619. # Get the parent directory of the .blend file
  620. blend_dir = os.path.dirname(blend_filepath)
  621. parent_dir = os.path.dirname(blend_dir)
  622. blend_filename = os.path.splitext(os.path.basename(blend_filepath))[0]
  623. # Create the FBX export path
  624. fbx_export_path = os.path.join(parent_dir, "FBX", f"{blend_filename}.fbx")
  625. # Ensure the FBX directory exists
  626. os.makedirs(os.path.dirname(fbx_export_path), exist_ok=True)
  627. # unhide all objects
  628. unhide_all_objects()
  629. # Deselect all objects
  630. bpy.ops.object.select_all(action="DESELECT")
  631. # Select all objects in the NonConfigurable collection and its subcollections
  632. collection_name = "NonConfigurable"
  633. if collection_name in bpy.data.collections:
  634. collection = bpy.data.collections[collection_name]
  635. select_objects_in_collection(collection)
  636. else:
  637. print(f"Collection '{collection_name}' not found.")
  638. return
  639. # check if all objects selected have a parent, if not return
  640. if not check_if_selected_objects_have_parent(self):
  641. return
  642. if not add_rig_controller_to_selection(self):
  643. return
  644. # Export selected objects to FBX
  645. bpy.ops.export_scene.fbx(
  646. filepath=fbx_export_path,
  647. use_selection=True,
  648. global_scale=1.0,
  649. apply_unit_scale=True,
  650. bake_space_transform=False,
  651. object_types={"MESH", "ARMATURE", "EMPTY"},
  652. use_mesh_modifiers=True,
  653. mesh_smooth_type="FACE",
  654. use_custom_props=True,
  655. # bake_anim=False,
  656. )
  657. print(f"Exported to {fbx_export_path}")
  658. def export_scene_to_glb(self):
  659. # Ensure the .blend file is saved
  660. if not bpy.data.is_saved:
  661. print("Save the .blend file first.")
  662. self.report({"ERROR"}, "Save the .blend file first.")
  663. return
  664. # Get the current .blend file path
  665. blend_filepath = bpy.data.filepath
  666. if not blend_filepath:
  667. print("Unable to get the .blend file path.")
  668. return
  669. # Get the parent directory of the .blend file
  670. blend_dir = os.path.dirname(blend_filepath)
  671. parent_dir = os.path.dirname(blend_dir)
  672. blend_filename = os.path.splitext(os.path.basename(blend_filepath))[0]
  673. # Create the GLB export path
  674. glb_export_path = os.path.join(parent_dir, "WEB", f"{blend_filename}.glb")
  675. # Ensure the GLB directory exists
  676. os.makedirs(os.path.dirname(glb_export_path), exist_ok=True)
  677. # unhide all objects
  678. unhide_all_objects()
  679. # Deselect all objects
  680. bpy.ops.object.select_all(action="DESELECT")
  681. # Select all objects in the NonConfigurable collection and its subcollections
  682. collection_name = "WebGL"
  683. if collection_name in bpy.data.collections:
  684. collection = bpy.data.collections[collection_name]
  685. select_objects_in_collection(collection)
  686. else:
  687. print(f"Collection '{collection_name}' not found.")
  688. return
  689. if not check_if_selected_objects_have_parent(self):
  690. return
  691. # check if all objects selected have a parent, if not return
  692. if not add_rig_controller_to_selection(self):
  693. return
  694. # # for each selected objects, check if the the material is principled, if not return
  695. # for obj in bpy.context.selected_objects:
  696. # if not check_if_object_has_principled_material(obj):
  697. # message = f"Object {obj.name} has no principled material"
  698. # self.report({"ERROR"}, message)
  699. # return
  700. # Export selected objects to GLB
  701. bpy.ops.export_scene.gltf(
  702. filepath=glb_export_path,
  703. export_format="GLB",
  704. use_selection=True,
  705. export_apply=True,
  706. export_animations=False,
  707. export_yup=True,
  708. export_cameras=False,
  709. export_lights=False,
  710. export_materials="EXPORT",
  711. export_normals=True,
  712. export_tangents=True,
  713. export_morph=False,
  714. export_skins=False,
  715. export_draco_mesh_compression_enable=False,
  716. export_draco_mesh_compression_level=6,
  717. export_draco_position_quantization=14,
  718. export_draco_normal_quantization=10,
  719. export_draco_texcoord_quantization=12,
  720. export_draco_color_quantization=10,
  721. export_draco_generic_quantization=12,
  722. export_keep_originals=False,
  723. export_texture_dir="",
  724. )
  725. print(f"Exported to {glb_export_path}")
  726. # -------------------------------------------------------------------
  727. # Operators
  728. # -------------------------------------------------------------------
  729. # load scene operator
  730. class ZSSD_OT_LoadScene(bpy.types.Operator):
  731. bl_idname = "zs_sd_loader.load_scene"
  732. bl_label = "Load Scene"
  733. bl_description = "Load Scene"
  734. def execute(self, context):
  735. load_scene()
  736. return {"FINISHED"}
  737. # canvas exporter operator
  738. class ZSSD_OT_ExportAssets(bpy.types.Operator):
  739. bl_idname = "zs_canvas.export_assets"
  740. bl_label = "Export Assets"
  741. bl_description = "Export Scene Assets to FBX and GLB"
  742. def execute(self, context):
  743. export_scene_to_fbx(self)
  744. export_scene_to_glb(self)
  745. return {"FINISHED"}
  746. # parent class for panels
  747. class ZSSDPanel:
  748. bl_space_type = "VIEW_3D"
  749. bl_region_type = "UI"
  750. bl_category = "ZS SD Loader"
  751. # -------------------------------------------------------------------
  752. # Draw
  753. # -------------------------------------------------------------------
  754. # Panels
  755. class ZSSD_PT_Main(ZSSDPanel, bpy.types.Panel):
  756. bl_label = "SD Loader"
  757. def draw(self, context):
  758. layout = self.layout
  759. scene = context.scene
  760. col = layout.column()
  761. self.is_connected = False
  762. col.label(text="Stable Diffusion Connection")
  763. col.prop(context.scene, "load_local_DB")
  764. col.prop(context.scene, "config_string")
  765. # load scene button
  766. col.operator("zs_sd_loader.load_scene", text="Load Scene")
  767. col.separator()
  768. # export assets button
  769. col.operator("zs_canvas.export_assets", text="Export Assets")
  770. # modify after making products
  771. blender_classes = [
  772. ZSSD_PT_Main,
  773. ZSSD_OT_LoadScene,
  774. ZSSD_OT_ExportAssets,
  775. ]
  776. def register():
  777. # register classes
  778. for blender_class in blender_classes:
  779. bpy.utils.register_class(blender_class)
  780. bpy.types.Scene.shot_info_ai = bpy.props.StringProperty(
  781. name="Shot Info",
  782. )
  783. bpy.types.Scene.config_string = bpy.props.StringProperty( # type: ignore
  784. name="Configuration String",
  785. )
  786. bpy.types.Scene.load_local_DB = bpy.props.BoolProperty( # type: ignore
  787. name="Load Local DB",
  788. )
  789. # Has to be afqter class registering to correctly register property
  790. # register global properties
  791. # register list
  792. # list data
  793. # bpy.types.Scene.zs_product_list = bpy.props.CollectionProperty(
  794. # type=ZS_Product_ListItem)
  795. # current item in list
  796. def unregister():
  797. # unregister classes
  798. for blender_class in blender_classes:
  799. bpy.utils.unregister_class(blender_class)
  800. # unregister global properties
  801. del bpy.types.Scene.shot_info_ai
  802. del bpy.types.Scene.config_string
  803. del bpy.types.Scene.load_local_DB
  804. # unregister list items
  805. # del bpy.types.Scene.my_list
  806. # del bpy.types.Scene.product_product_list_index