__init__.py 39 KB

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