__init__.py 32 KB

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