-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathConverter.py
3828 lines (2982 loc) · 180 KB
/
Converter.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# █████ █████ █████████ ██████████ ██████ █████ █████████ ██████████
# ░░███ ░░███ ███░░░░░███░░███░░░░░█░░██████ ░░███ ███░░░░░███░░███░░░░░█
# ░███ ░███ ███ ░░░ ░███ █ ░ ░███░███ ░███ ░███ ░░░ ░███ █ ░
# ░███ ░███ ░███ ░██████ ░███░░███░███ ░░█████████ ░██████
# ░███ ░███ ░███ ░███░░█ ░███ ░░██████ ░░░░░░░░███ ░███░░█
# ░███ █ ░███ ░░███ ███ ░███ ░ █ ░███ ░░█████ ███ ░███ ░███ ░ █
# ███████████ █████ ░░█████████ ██████████ █████ ░░█████░░█████████ ██████████
# ░░░░░░░░░░░ ░░░░░ ░░░░░░░░░ ░░░░░░░░░░ ░░░░░ ░░░░░ ░░░░░░░░░ ░░░░░░░░░░
##### BEGIN GPL LICENSE BLOCK #####
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
# You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
##### END GPL LICENSE BLOCK #####
# █████ █████ ███████████ ███████████ █████████ ███████████ █████ ██████████ █████████
# ░░███ ░░███ ░░███░░░░░███░░███░░░░░███ ███░░░░░███ ░░███░░░░░███ ░░███ ░░███░░░░░█ ███░░░░░███
# ░███ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ░███ █ ░ ░███ ░░░
# ░███ ░███ ░██████████ ░██████████ ░███████████ ░██████████ ░███ ░██████ ░░█████████
# ░███ ░███ ░███░░░░░███ ░███░░░░░███ ░███░░░░░███ ░███░░░░░███ ░███ ░███░░█ ░░░░░░░░███
# ░███ █ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ░ █ ███ ░███
# ███████████ █████ ███████████ █████ █████ █████ █████ █████ █████ █████ ██████████░░█████████
# ░░░░░░░░░░░ ░░░░░ ░░░░░░░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░░░░░░ ░░░░░░░░░
import bpy
import os
import glob
import datetime
import sys
import shutil
from pathlib import Path
import json
import re
import logging
from bpy.app.handlers import persistent
import itertools
from itertools import chain
import time
import numpy as np
from mathutils import Vector
import csv
# ███████████ █████ █████ ██████ █████ █████████ ███████████ █████ ███████ ██████ █████ █████████
# ░░███░░░░░░█░░███ ░░███ ░░██████ ░░███ ███░░░░░███░█░░░███░░░█░░███ ███░░░░░███ ░░██████ ░░███ ███░░░░░███
# ░███ █ ░ ░███ ░███ ░███░███ ░███ ███ ░░░ ░ ░███ ░ ░███ ███ ░░███ ░███░███ ░███ ░███ ░░░
# ░███████ ░███ ░███ ░███░░███░███ ░███ ░███ ░███ ░███ ░███ ░███░░███░███ ░░█████████
# ░███░░░█ ░███ ░███ ░███ ░░██████ ░███ ░███ ░███ ░███ ░███ ░███ ░░██████ ░░░░░░░░███
# ░███ ░ ░███ ░███ ░███ ░░█████ ░░███ ███ ░███ ░███ ░░███ ███ ░███ ░░█████ ███ ░███
# █████ ░░████████ █████ ░░█████ ░░█████████ █████ █████ ░░░███████░ █████ ░░█████░░█████████
# ░░░░░ ░░░░░░░░ ░░░░░ ░░░░░ ░░░░░░░░░ ░░░░░ ░░░░░ ░░░░░░░ ░░░░░ ░░░░░ ░░░░░░░░░
# Read Settings.json file where the user variables are stored.
def read_json(json_file):
try:
with open(json_file, 'r') as openfile:
# Read from JSON file
json_object = json.load(openfile)
return json_object
print(f"Read {json_file.name}")
logging.info(f"Read {json_file.name}")
except Exception as Argument:
logging.exception(f"Could Not Read {json_file.name}")
# Set global variables from JSON dictionary.
def set_settings(json_dict):
try:
for key, value in json_dict.items():
# Preserve quotation marks during exec() if value is a string type object.
if isinstance(value, str):
value = repr(value)
# Don't preserve quotation marks during exec() if value is not a string type object.
else:
value = str(value)
# Concatenate command.
variable_assignment_command = f"globals()['{key}'] = {value}"
# Execute variable assignment.
exec(variable_assignment_command)
print("Set setting from JSON")
logging.info("Set settings from JSON")
except Exception as Argument:
logging.exception("Could not set settings from JSON")
# Read dictionary of settings from JSON file.
def get_settings(json_files):
try:
# Loop through JSON files list.
for json_file in json_files:
json_file = Path(__file__).parent.resolve() / json_file
# Assign variables from dictionary and make all variables global
json_dict = read_json(json_file)
# Set settings.
set_settings(json_dict)
print("Got setting from JSON")
logging.info("Got settings from JSON")
except Exception as Argument:
logging.exception("Could not get settings from JSON")
# Copy finished log file to other import directories.
def copy_log_file(log_file):
try:
for import_settings_dict in imports:
directory = import_settings_dict["directory"]
if Path(directory) == log_file.parent:
continue
copy_file(directory, log_file)
except Exception as Argument:
logging.exception("Could not copy log file to other import directories")
# Make a log file to log conversion process.
def make_log_file():
# Set path to log file with timestamp
timestamp = str(datetime.datetime.now().strftime("%Y%m%d_%H%M%S"))
log_directory = imports[0]["directory"]
log_file = Path(log_directory, f"Transmogrifier_Log_{timestamp}.txt")
# Create log file.
if logging_save_log:
logging.basicConfig(
level=logging.INFO,
filename=log_file,
filemode="w",
format="%(asctime)s %(levelname)s %(message)s",
force=True
)
return log_file
# Enable addons that the Converter depends upon, namely Node Wrangler and Materials Utilities.
def enable_addons():
try:
bpy.ops.preferences.addon_enable(module='node_wrangler')
bpy.ops.preferences.addon_enable(module='materials_utils')
print("Enabled addons")
logging.info("Enabled addons")
except Exception as Argument:
logging.exception("Could not enable addons")
# Copy file from source to destination.
def copy_file(directory, file_source):
try:
file_destination = Path(directory, Path(file_source).name) # Set destination path.
if Path(file_source).is_dir(): # Check if "file" is a directory.
if Path(file_destination).exists():
shutil.rmtree(file_destination) # Remove any existing destination directory.
shutil.copytree(file_source, file_destination) # Copy the directory.
elif Path(file_source).is_file(): # Check if "file" is a file.
if Path(file_destination).is_file():
Path.unlink(file_destination) # Remove any existing destination file.
shutil.copy(file_source, file_destination) # Copy the file.
else:
return # Return nothing if source file doesn't exist.
print(f"Copied {Path(file_source).name} to {directory}")
logging.info(f"Copied {Path(file_source).name} to {directory}")
except Exception as Argument:
logging.exception(f"Could not copy {Path(file_source).name} to {directory}")
# Move file from source to destination.
def move_file(directory, file_source):
try:
file_destination = Path(directory, Path(file_source).name) # Set destination path.
if Path(file_source).is_dir(): # Check if "file" is a directory.
if Path(file_destination).exists():
shutil.rmtree(file_destination) # Remove any existing destination directory.
shutil.move(file_source, file_destination) # Move the directory.
elif Path(file_source).is_file(): # Check if "file" is a file.
if Path(file_destination).is_file():
Path.unlink(file_destination) # Remove any existing destination file.
shutil.move(file_source, file_destination) # Move the file.
else:
return # Return nothing if source file doesn't exist.
print(f"Moved {Path(file_source).name} to {directory}")
logging.info(f"Moved {Path(file_source).name} to {directory}")
except Exception as Argument:
logging.exception(f"Could not move {Path(file_source).name} to {directory}")
# Override context perform certain context-dependent Blender operators.
def override_context(area_type, region_type):
try:
win = bpy.context.window
scr = win.screen
areas = [area for area in scr.areas if area.type == area_type]
regions = [region for region in areas[0].regions if region.type == region_type]
override = {
'window': win,
'screen': scr,
'area': areas[0],
'region': regions[0],
}
print("Overrode context")
logging.info("Overrode context")
return override
except Exception as Argument:
logging.exception("Could not override context")
# Preserve unused materials & textures by setting fake user(s).
def use_fake_user():
try:
for datablock in chain(bpy.data.materials, bpy.data.textures):
datablock.use_fake_user = True
print("Used fake user for textures & materials")
logging.info("Used fake user for textures & materials")
except Exception as Argument:
logging.exception("Could not use fake user for textures & materials")
# Sometimes purge orphans won't delete data blocks (e.g. images) even though they have no users. This will force the deletion of any data blocks within a specified bpy.data.[data type]
def clean_data_block(block):
try:
# iterate over every entry in the data block
for data in block:
block.remove(data)
print(f"Cleaned data block: {str(block).upper()}")
logging.info(f"Cleaned data block: {str(block).upper()}")
except Exception as Argument:
logging.exception(f"Could not clean data block: {str(block).upper()}")
# Add new collection with name of import_file.
def add_collection(item_name):
try:
# Add new collection.
collection_name = item_name
collection = bpy.data.collections.new(collection_name)
# Add collection to scene collection.
bpy.context.scene.collection.children.link(collection)
# Make collection active so imported file contents are put inside/linked to the collection.
layer_collection = bpy.context.view_layer.layer_collection.children[collection.name]
bpy.context.view_layer.active_layer_collection = layer_collection
print(f"Added new collection: {collection.name}")
logging.info(f"Added new collection: {collection.name}")
except Exception as Argument:
logging.exception(f"Could not add new collection: {collection.name}")
# Recursively delete orphaned data blocks.
def purge_orphans():
try:
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
print("Purged orphaned data blocks recursively")
logging.info("Purged orphaned data blocks recursively")
except Exception as Argument:
logging.exception("Could not purge orphaned data blocks recursively")
# Enable addon dependencies and clear the scene
def setup_scene(item_name):
try:
clean_data_block(bpy.data.objects)
clean_data_block(bpy.data.collections)
purge_orphans()
add_collection(item_name)
print("Set up scene")
logging.info("Set up scene")
except Exception as Argument:
logging.exception("Could not set up scene")
# Append a blend file's objects to Converter.blend.
def append_blend_objects(import_file_command, import_file_options, import_file):
try:
with bpy.data.libraries.load(str(import_file), link=False) as (data_from, data_to): # Append all objects.
data_to.objects = [object for object in data_from.objects]
for object in data_to.objects: # Link all objects to current scene.
if object is not None:
bpy.context.collection.objects.link(object)
print(f"Appended blend file objects: {import_file.name}")
logging.info(f"Appended blend file objects: {import_file.name}")
except Exception as Argument:
logging.exception(f"Could not append blend file objects: {import_file.name}")
# Import file of a format type supplied by the user.
def import_a_file(import_file, import_settings_dict):
try:
# Get import options as a dictionary.
options = eval(import_settings_dict["options"])
# Update options with filepath to the location of the model to be imported.
options["filepath"] = str(import_file)
# Get import operator.
operator = import_settings_dict["operator"]
# Select "Objects" Library to append from current .blend file if importing a blend file.
if operator == "bpy.ops.wm.append(**":
append_blend_objects(operator, options, import_file)
return
# Concatenate the import command with the import options dictionary
operator = f"{operator}{options})"
print(operator)
logging.info(operator)
# Run operator, which is stored as a string and won't run otherwise.
exec(operator)
print(f"Imported file: {import_file.name}")
logging.info(f"Imported file: {import_file.name}")
except Exception as Argument:
logging.exception(f"Could not import file: {import_file.name}")
# Move all objects and collections to item_name collection.
def move_objects_and_collections_to_item_collection(item_name):
try:
item_collection = bpy.data.collections[item_name]
collections_to_move = [collection for collection in bpy.context.scene.collection.children if collection != item_collection] # Get a list of additional collections in the scene.
objects_to_move = [object for object in bpy.context.scene.collection.objects] # Get a list of objects that exist directly in the default Scene Collection.
if collections_to_move:
for collection in collections_to_move:
bpy.context.scene.collection.children.unlink(collection)
item_collection.children.link(collection)
if objects_to_move:
for object in objects_to_move:
bpy.context.scene.collection.objects.unlink(object)
item_collection.objects.link(object)
print(f"Moved objects in to Collection: {item_collection.name}")
logging.info(f"Moved objects in to Collection: {item_collection.name}")
except Exception as Argument:
logging.exception(f"Could not move objects in to Collection: {item_collection.name}")
# Remove all animation data from imported objects. Sometimes 3DS Max exports objects with keyframes that cause scaling/transform issues in a GLB and USDZ.
def clear_animation_data():
try:
bpy.ops.object.select_all(action='SELECT')
objects = bpy.context.scene.objects
for object in objects:
object.animation_data_clear()
print(f"Deleted animations for {object.name}")
logging.info(f"Deleted animations for {object.name}")
print("Cleared animation data")
logging.info("Cleared animation data")
except Exception as Argument:
logging.exception("Could not clear animation data")
# Select all imported objects and apply transformations
def apply_transformations(apply_transforms_filter):
try:
# Set default filters to true
filter_location = True
filter_rotation = True
filter_scale = True
# Disable filters if User elected not to include them
if "Location" not in apply_transforms_filter:
filter_location = False
if "Rotation" not in apply_transforms_filter:
filter_rotation = False
if "Scale" not in apply_transforms_filter:
filter_scale = False
# Select all objects
bpy.ops.object.select_all(action='SELECT')
obj = bpy.context.window.scene.objects[0]
bpy.context.view_layer.objects.active = obj
# Apply transformations according to filter
bpy.ops.object.transform_apply(location=filter_location, rotation=filter_rotation, scale=filter_scale)
print(f"Applied transformations: {apply_transforms_filter}")
logging.info(f"Applied transformations: {apply_transforms_filter}")
except Exception as Argument:
logging.exception(f"Could not apply transformations: {apply_transforms_filter}")
# Clear all users of all materials.
def clear_materials_users():
try:
# Remove any imported materials.
bpy.ops.view3d.materialutilities_remove_all_material_slots(only_active=False)
# Delete any old materials that might have the same name as the imported object.
purge_orphans()
for material in bpy.data.materials:
material.user_clear()
print("Cleared all users of all materials")
logging.info("Cleared all users of all materials")
except Exception as Argument:
logging.exception("Could not clear all users of all materials")
# Select all objects again before exporting. The previously actively selected object should still be a MESH type object, although this should no longer matter.
def select_all():
try:
bpy.ops.object.select_all(action='SELECT')
print("Selected all objects of all types")
logging.info("Selected all objects of all types")
except Exception as Argument:
logging.exception("Could not select all objects of all types")
# Select all objects again before exporting. The previously actively selected object should still be a MESH type object, although this should no longer matter.
def deselect_all():
try:
bpy.ops.object.select_all(action='DESELECT')
print("Deselected all objects of all types")
logging.info("Deselected all objects of all types")
except Exception as Argument:
logging.exception("Could not deselect all objects of all types")
# Prevent an empty from being actively selected object. This prevents a later error from happening when imported materials are removed later.
def select_only_meshes():
try:
objects = bpy.context.scene.objects
for object in objects:
object.select_set(object.type == "MESH")
object = bpy.context.window.scene.objects[0]
bpy.context.view_layer.objects.active = object
print("Selected only mesh-type objects")
logging.info("Selected only mesh-type objects")
except Exception as Argument:
logging.exception("Could not select only mesh-type objects")
# Select all objects again before exporting. The previously actively selected object should still be a MESH type object, although this should no longer matter.
def select_by_material(material):
try:
bpy.ops.view3d.materialutilities_select_by_material_name(material_name = material.name)
print(f"Selected objects with material: {material.name}")
logging.info(f"Selected objects with material: {material.name}")
except Exception as Argument:
logging.exception(f"Could not select objects with material: {material.name}")
# Copy textures from custom directory to item_name directory.
def copy_textures_from_custom_source(textures_custom_dir, item_dir, textures_dir):
try:
if Path(textures_custom_dir).exists():
if Path(textures_dir).exists(): # Cannot create another textures folder if one already exists.
if copy_textures_custom_dir and overwrite_textures: # If User elected to replace an existing textures directory that might be inside the item_name folder, then delete it.
shutil.rmtree(textures_dir)
else: # If not, preserve existing textures folder by renaming adding an "_original" suffix.
textures_dir_name = [d.name for d in Path.iterdir(item_dir) if "textures" in d.name.lower()][0] # Need to get specific textures_dir folder characters in case any other files are pathed to it.
textures_dir_original_name = [d.name for d in Path.iterdir(item_dir) if "textures" in d.name.lower() and "_original" not in d.name.lower()][0] + "_original"
textures_dir_original = Path(item_dir, textures_dir_original_name)
if Path(textures_dir_original).exists(): # If a textures folder had already existed and had been preserved as "textures_original", assume that the item_dir has already been transmogrified and the current textures_dir is a copy from the custom source.
shutil.rmtree(textures_dir)
elif not Path(textures_dir_original).exists(): # If a textures folder already exists but had not yet been preserved as a "textures_orignal" folder, then rename it so.
Path(textures_dir).rename(textures_dir_original)
shutil.copytree(textures_custom_dir, textures_dir) # Temporarily copy textures from custom directory as the current textures_dir.
else:
print("Custom textures directory does not exist.")
logging.info("Custom textures directory does not exist.")
print("Copied textures from custom source")
logging.info("Copied textures from custom source")
except Exception as Argument:
logging.exception("Could not copy textures from custom source")
# If User elected not to copy the custom textures directory to each item_name folder, delete the temporary copy of it there.
def remove_copy_textures_custom_dir(item_dir, textures_dir):
try:
textures_dir_original_name = [d.name for d in Path.iterdir(item_dir) if "textures_original" in d.name.lower()]
if Path(textures_dir).exists() and textures_dir_original_name:
shutil.rmtree(textures_dir)
if textures_dir_original_name: # If there was a textures_dir there before transmogrification, return its name to its original form.
textures_dir_original_name = textures_dir_original_name[0] # If the list is not empty, get the first item_name in the list.
textures_dir_original = Path(item_dir, textures_dir_original_name)
if Path(textures_dir_original).exists():
Path(textures_dir_original).rename(Path(item_dir, textures_dir_original_name.replace("_original", ""))) # Return original textures_dir back to its former name before the conversion.
print("Removed copied textures from custom source")
logging.info("Removed copied textures from custom source")
except Exception as Argument:
logging.exception("Could not remove copied textures from custom source")
# Define list of supported image texture extensions.
def supported_image_ext():
try:
supported_image_ext = (
".bmp",
".sgi",
".rgb",
".bw",
".png",
".jpg",
".jpeg",
".jp2",
".j2c",
".tga",
".cin",
".dpx",
".exr",
".hdr",
".tif",
".tiff",
".webp"
)
return supported_image_ext
print("Got supported image extensions")
logging.info("Got supported image extensions")
except Exception as Argument:
logging.exception("Could not get supported image extensions")
# Remove textures_temp_dir
def delete_textures_temp(textures_temp_dir):
try:
# Delete "textures_temp" if folder already exists. It will already exist if the User elected to save a .blend file, and it may exist if Transmogrifier quit after an error.
if Path(textures_temp_dir).exists():
shutil.rmtree(textures_temp_dir)
print("Deleted temporary textures directory")
logging.info("Deleted temporary textures directory")
except Exception as Argument:
logging.exception("Could not delete temporary textures directory")
# Create a temporary textures folder where images can be resized and exported with specified models without affecting the quality of the original texture files.
# If multiple texture sets exist, assign each image texture file a prefix of the name of it's parent texture set directory folder.
# If image textures already have a prefix of the same name as the texture set, don't add the same prefix again.
def create_textures_temp(item_dir, textures_dir, textures_temp_dir):
try:
# Delete "textures_temp" if folder already exists. It will already exist if the User elected to save a .blend file, and it may exist if Transmogrifier quit after an error.
delete_textures_temp(textures_temp_dir)
# Check if a "textures" directory exists and is not empty. Copy it and call it textures_[item_name]_temp if it does, otherwise create an empty directory and fill it with image textures found in the item_dir.
if Path(textures_dir).exists():
# If a textures directory exists but is empty, make one and fill it with images.
if not any(Path.iterdir(textures_dir)):
print("Textures directory is empty. Looking for textures in parent directory...")
logging.info("Textures directory is empty. Looking for textures in parent directory...")
Path.mkdir(textures_temp_dir)
image_ext = supported_image_ext() # Get a list of image extensions that could be used as textures
image_list = [file.name for file in Path.iterdir(item_dir) if file.name.lower().endswith(image_ext) and not file.name.startswith("Preview_")] # Make a list of all potential texture candidates except for the preview images.
if not image_list: # i.e. if image_list is empty
print(f"No potential image textures found in {item_dir}")
logging.info(f"No potential image textures found in {item_dir}")
else:
print(f"The following images will be copied to textures_temp: {image_list}")
logging.info(f"The following images will be copied to textures_temp: {image_list}")
for image in image_list:
image_src = Path(item_dir, image)
image_dest = Path(textures_temp_dir, image)
shutil.copy(image_src, image_dest) # Copy each potential image texture to textures_temp
# If a textures directory exists and is not empty, assume it contains images or texture set subdirectories containing images.
else:
shutil.copytree(textures_dir, textures_temp_dir)
# If no textures directory exists, make one and fill it with images.
else:
Path.mkdir(textures_temp_dir)
image_ext = supported_image_ext() # Get a list of image extensions that could be used as textures
image_list = [file.name for file in Path.iterdir(item_dir) if file.name.lower().endswith(image_ext) and not file.name.startswith("Preview_")] # Make a list of all potential texture candidates except for the preview images.
if not image_list: # i.e. if image_list is empty
print(f"No potential image textures found in {item_dir}")
logging.info(f"No potential image textures found in {item_dir}")
else:
print(f"The following images will be copied to textures_temp: {image_list}")
logging.info(f"The following images will be copied to textures_temp: {image_list}")
for image in image_list:
image_src = Path(item_dir, image)
image_dest = Path(textures_temp_dir, image)
shutil.copy(image_src, image_dest) # Copy each potential image texture to textures_temp
print("Created temporary textures directory")
logging.info("Created temporary textures directory")
except Exception as Argument:
logging.exception("Could not create temporary textures directory")
# Split image texture name into components to be regexed in find_replace_pbr_tag function.
# The following code is adapted from Blender 3.5, Node Wrangler 3.43, __util.py__, Line 7
def split_into_components(string):
try:
"""
Split filename into components
'WallTexture_diff_2k.002.jpg' -> ['WallTexture', 'diff', '2k', '002', 'jpg']
"""
# Get original string name for printout.
string_original = string
# Remove file path.
string = Path(string).name
# Replace common separators with SPACE.
separators = ["_", ",", ".", "-", "__", "--", "#"]
for sep in separators:
string = string.replace(sep, " ")
components = string.split(" ")
print(f"Split into components: {Path(string_original).name} --> {components}")
logging.info(f"Split into components: {Path(string_original).name} --> {components}")
return components
except Exception as Argument:
logging.exception(f"Could not split into components: {Path(string_original).name} --> {components}")
# Regex, i.e. find and replace messy/misspelled PBR tag with clean PBR tag in a given image texture's name supplied by the regex_textures_external function.
def find_replace_pbr_tag(texture):
try:
# Set original texture name.
texture_original = texture
# Dictionary with regex keys will be used as the pattern by turning it into a list then to a string.
pbr_dict = {
'(?i)^basecolou?r$|^albedo$|^d?iffuse$|^d?iff$|^colou?r$|^col$': 'BaseColor',
'(?i)^subsurf.*|^sss$': 'Subsurface',
'(?i)^m?etall?ic$|^m?etalness?$|^metal$|^mtl$': 'Metallic',
'(?i)^specul.*|^spe?c$': 'Specular',
'(?i)^rou?gh$|^rou?ghn.*|^rgh$': 'Roughness',
'(?i)^gloss?y?$|^gloss?iness?$|^gls$': 'Gloss',
'(?i)^no?rma?l?$|^nor$': 'Normal',
'(?i)^bu?mp$|^bumpiness?$': 'Bump',
'(?i)^displacem?e?n?t?$|^di?sp$|^he?i?ght$|^hi?e?ght$': 'Height',
'(?i)^tra?nsmi?ss?i?o?n$': 'Transmission',
'(?i)^emiss.*|^emit$': 'Emission',
'(?i)^alpha$|^opac.*|^tra?ns?pa.*|^transpr.*': 'Opacity',
'(?i)^ambi?e?nt$|^occ?lus.*|^ambi?e?ntocc?lusion$|^ao$': 'Occlusion'
}
dictkeys_pattern = re.compile('|'.join(pbr_dict), re.IGNORECASE)
#If string is found in the pattern, match to key in the dictionary and return corresponding value.
texture_found = re.findall(dictkeys_pattern, texture)
if texture_found:
texture = []
for i in texture_found:
for k, v in pbr_dict.items():
if re.match(k, i, re.IGNORECASE):
texture.append(v)
return texture
else:
texture = None
return texture
print(f"Found and replaced pbr tag: {texture_original} --> {texture}")
logging.info(f"Found and replaced pbr tag: {texture_original} --> {texture}")
except Exception as Argument:
logging.exception(f"Could not find and replace pbr tag: {texture_original} --> {texture}")
# Regex, i.e. find and replace messy/misspelled transparency tag with clean PBR tag in a given object's name supplied by the regex_transparent_objects function.
def find_replace_transparency_tag(mesh_object):
try:
# Set original mesh_object name.
mesh_object_original = mesh_object
# Dictionary with regex keys will be used as the pattern by turning it into a list then to a string.
pbr_dict = {
'(?i)^alpha$|^opac.*|^tra?ns?pa.*|^transpr.*|^glass$': 'transparent',
'(?i)^cutout$|^cut$|^out$': 'cutout',
}
dictkeys_pattern = re.compile('|'.join(pbr_dict), re.IGNORECASE)
#If string is found in the pattern, match to key in the dictionary and return corresponding value.
mesh_object_found = re.findall(dictkeys_pattern, mesh_object)
if mesh_object_found:
mesh_object = []
for i in mesh_object_found:
for k, v in pbr_dict.items():
if re.match(k, i, re.IGNORECASE):
mesh_object.append(v)
return mesh_object
else:
mesh_object = None
return mesh_object
print(f"Found and replaced transparency tag: {mesh_object_original} --> {mesh_object}")
logging.info(f"Found and replaced transparency tag: {mesh_object_original} --> {mesh_object}")
except Exception as Argument:
logging.exception(f"Could not find and replaced transparency tag: {mesh_object_original} --> {mesh_object}")
# Find and rename image textures from a dictionary with regex keys.
def regex_textures_external(textures_temp_dir):
try:
for subdir, dirs, files in os.walk(textures_temp_dir):
for file in files:
file = Path(subdir, file)
texture = file
texture_path = Path(texture).parent.resolve()
components_original = split_into_components(texture)
components = split_into_components(texture)
for component in components:
pbr_tag_renamed = find_replace_pbr_tag(component)
if pbr_tag_renamed != None:
pbr_tag_renamed = pbr_tag_renamed[0]
print(f"Found a match for {pbr_tag_renamed}")
logging.info(f"Found a match for {pbr_tag_renamed}")
tag_index = components.index(component)
components[tag_index] = pbr_tag_renamed
if pbr_tag_renamed == "BaseColor" and components[tag_index-1].lower() == "base":
components.pop(tag_index-1) # Was getting "...base_BaseColor..." when original name was "base_color"
elif pbr_tag_renamed == "Occlusion" and components[tag_index-1].lower() == "occlusion":
components.pop(tag_index+1) # Was getting "...Ambient_Occlusion_Occlusion..." when original name was "Ambient_Occlusion"
break
if components_original != components:
texture = Path(texture_path, texture)
texture_renamed = '_'.join(components[:-1])
texture_renamed = Path(texture_path, texture_renamed + '.' + components[-1])
print(f"Renamed texture: {Path(texture).name} --> {Path(texture_renamed).name}")
logging.info(f"Renamed texture: {Path(texture).name} --> {Path(texture_renamed).name}")
Path(texture).rename(texture_renamed)
else:
print("No PBR match found for current texture.")
logging.info("No PBR match found for current texture.")
print("Regexed external textures")
logging.info("Regexed external textures")
except Exception as Argument:
logging.exception("Could not regex external textures")
# Find and rename image textures from a dictionary with regex keys.
def regex_textures_packed():
try:
for texture in bpy.data.images:
texture_name = texture.name
components_original = split_into_components(texture_name)
components = split_into_components(texture_name)
for component in components:
pbr_tag_renamed = find_replace_pbr_tag(component)
if pbr_tag_renamed != None:
pbr_tag_renamed = pbr_tag_renamed[0]
print(f"Found a match for {pbr_tag_renamed}")
logging.info(f"Found a match for {pbr_tag_renamed}")
tag_index = components.index(component)
if pbr_tag_renamed == "Opacity" and len(components) - tag_index != -2: # Don't rename any transparency string if it's not at the end of the texture name.
continue
components[tag_index] = pbr_tag_renamed
if pbr_tag_renamed == "BaseColor" and components[tag_index-1].lower() == "base":
components.pop(tag_index-1) # Was getting "...base_BaseColor..." when original name was "base_color"
elif pbr_tag_renamed == "Occlusion" and components[tag_index+1].lower() == "occlusion":
components.pop(tag_index+1) # Was getting "...Ambient_Occlusion_Occlusion..." when original name was "Ambient_Occlusion"
if components_original != components:
texture_renamed = '_'.join(components[:-1])
texture.name = texture_renamed
print(f"Renamed texture: {Path(texture).name} --> {Path(texture_renamed).name}")
logging.info(f"Renamed texture: {Path(texture).name} --> {Path(texture_renamed).name}")
else:
print("No PBR match found for current texture.")
logging.info("No PBR match found for current texture.")
print("Regexed packed textures")
logging.info("Regexed packed textures")
except Exception as Argument:
logging.exception("Could not regex packed textures")
# Find and rename transparent objects that have mispellings of transparency with regex keys.
def regex_transparent_objects():
try:
objects = bpy.context.selected_objects
for object in objects:
object_name = object.name
components_original = split_into_components(object_name)
components = split_into_components(object_name)
for component in components:
transparency_tag_renamed = find_replace_transparency_tag(component)
if transparency_tag_renamed != None:
transparency_tag_renamed = transparency_tag_renamed[0]
print(f"Found a match for {transparency_tag_renamed}")
logging.info(f"Found a match for {transparency_tag_renamed}")
tag_index = components.index(component)
components[tag_index] = transparency_tag_renamed
if components_original != components:
object_renamed = '_'.join(components)
object.name = object_renamed
print(f"Renamed object: {object_name} --> {object_renamed}")
logging.info(f"Renamed object: {object_name} --> {object_renamed}")
else:
print("No transparency tag match found for current object.")
logging.info("No transparency tag match found for current object.")
print("Regexed transparent objects")
logging.info("Regexed transparent objects")
except Exception as Argument:
logging.exception("Could not regex transparent objects")
# Determine if current item_name has multiple texture sets present in textures_temp_dir. If so, loop through each texture set and create a material from the image textures in that set.
def create_materials(item_name, textures_temp_dir):
try:
image_ext = supported_image_ext() # Get a list of image extensions that could be used as textures
# Check if textures are stored in subdirectories.
# Check if there are subdirectories.
texture_set_dir_list = next(os.walk(textures_temp_dir))[1]
# Check if there are images in subdirectories or if these folders are used for other purposes.
if texture_set_dir_list:
for texture_set_dir in texture_set_dir_list:
texture_set_dir = Path(textures_temp_dir, texture_set_dir)
textures_in_subdirs = [texture.name for texture in Path.iterdir(texture_set_dir) if texture.name.lower().endswith(image_ext)]
# If there are images stored in subdirectories, assume all texture sets are organized in subdirectories and use these to create materials.
if textures_in_subdirs:
for texture_set_dir in texture_set_dir_list:
texture_set_dir = Path(textures_temp_dir, texture_set_dir)
texture_set = Path(texture_set_dir).name # Get the subdirectory's name, which will be used as the material name.
# Add texture set prefix to images based on texture set directory name.
for texture in Path.iterdir(texture_set_dir):
texture_path = Path(texture_set_dir, texture)
texture_renamed = texture_set + "_" + texture.name
texture_renamed_path = Path(texture_set_dir, texture_renamed)
if not texture.name.startswith(texture_set) and texture_set != "textures_temp": # If all textures exist directly in textures_temp_dir, don't add that directory name as a prefix.
Path(texture_path).rename(texture_renamed_path)
else:
continue
textures = [texture.name for texture in Path.iterdir(texture_set_dir)]
create_a_material(item_name=texture_set, textures_temp_dir=texture_set_dir, textures=textures) # Parameters are temporarily reassigned in order that the create_a_material function can be reused.
# If there are no subdirectories containing images, then determine how many texture sets exists in textures_temp directory.
elif not textures_in_subdirs:
textures_list = [image.name for image in Path.iterdir(textures_temp_dir) if image.name.lower().endswith(image_ext)]
basecolor_count = 0
# Count how many times the regexed "BaseColor" string occurs in the list of images.
for texture in textures_list:
if "BaseColor" in texture:
basecolor_count += 1
# If there is more than one BaseColor image, assume that there are multiple texture sets.
if basecolor_count > 1:
texture_sets = list(set([image.split('_')[0] for image in textures_list]))
print(f"Detected {basecolor_count} texture sets: {texture_sets}")
logging.info(f"Detected {basecolor_count} texture sets: {texture_sets}")
for texture_set in texture_sets:
textures = [texture for texture in textures_list if texture.startswith(texture_set)]
create_a_material(item_name=texture_set, textures_temp_dir=textures_temp_dir, textures=textures)
# If there are less than or equal to 6 images in textures_temp, assume there is only one texture set.
elif basecolor_count <= 1:
print(f"Detected {basecolor_count} texture set")
logging.info(f"Detected {basecolor_count} texture set")
textures = textures_list
create_a_material(item_name, textures_temp_dir, textures)
# If there are no subdirectories containing images, then determine how many texture sets exists in textures_temp directory.
elif not texture_set_dir_list:
textures_list = [image.name for image in Path.iterdir(textures_temp_dir) if image.name.lower().endswith(image_ext)]
basecolor_count = 0
# Count how many times the regexed "BaseColor" string occurs in the list of images.
for texture in textures_list:
if "BaseColor" in texture:
basecolor_count += 1
# If there is more than one BaseColor image, assume that there are multiple texture sets.
if basecolor_count > 1:
texture_sets = list(set([image.split('_')[0] for image in textures_list]))
print(f"Detected {basecolor_count} texture sets: {texture_sets}")
logging.info(f"Detected {basecolor_count} texture sets: {texture_sets}")
for texture_set in texture_sets:
textures = [texture for texture in textures_list if texture.startswith(texture_set)]
create_a_material(item_name=texture_set, textures_temp_dir=textures_temp_dir, textures=textures)
# If there are less than or equal to 6 images in textures_temp, assume there is only one texture set.
elif basecolor_count <= 1:
print(f"Detected {basecolor_count} texture set")
logging.info(f"Detected {basecolor_count} texture set")
textures = textures_list
create_a_material(item_name, textures_temp_dir, textures)
# If there are no textures, print a message.
else:
print("No textures were found.")
logging.info("No textures were found.")
print("Created materials")
logging.info("Created materials")
except Exception as Argument:
logging.exception("Could not create materials")
# Add principled setup with Node Wrangler.
def add_principled_setup(material, textures_temp_dir, textures):
try:
# Select shader before importing textures.
material.use_nodes = True
tree = material.node_tree
nodes = tree.nodes
# Make sure the Principled BSDF shader is selected.
material.node_tree.nodes['Material Output'].select = False
material.node_tree.nodes['Principled BSDF'].select = True
material.node_tree.nodes.active = material.node_tree.nodes.get("Principled BSDF")
# Log the textures to be used for this material.
print("Textures for " + str(material.name) + ": " + str(textures))
logging.info("Textures for " + str(material.name) + ": " + str(textures))