diff --git a/i_scene_cp77_gltf/animtools/__init__.py b/i_scene_cp77_gltf/animtools/__init__.py index 3ab7c28..60d1988 100644 --- a/i_scene_cp77_gltf/animtools/__init__.py +++ b/i_scene_cp77_gltf/animtools/__init__.py @@ -9,7 +9,7 @@ from ..main.common import get_classes from ..importers.import_with_materials import CP77GLBimport from .animtools import * - +from .generate_rigs import create_rigify_rig def CP77AnimsList(self, context): for action in bpy.data.actions: @@ -51,15 +51,13 @@ def draw(self, context): row.operator('cp77.rig_loader', text="Load Bundled Rig") obj = context.active_object if obj and obj.type == 'ARMATURE': - row = box.row(align=True) + col = box.column() if 'deformBonesHidden' in obj: - row.operator('bone_unhider.cp77',text='Unhide Deform Bones') + col.operator('bone_unhider.cp77',text='Unhide Deform Bones') else: - row.operator('bone_hider.cp77',text='Hide Deform Bones') - row = box.row() - row.operator('reset_armature.cp77') - row = box.row() - row.operator('cp77.anim_namer') + col.operator('bone_hider.cp77',text='Hide Deform Bones') + col.operator('reset_armature.cp77') + col.operator('cp77.anim_namer') available_anims = list(CP77AnimsList(context,obj)) active_action = obj.animation_data.action if obj.animation_data else None if not available_anims: @@ -298,6 +296,7 @@ class CP77RigLoader(Operator): appearances: StringProperty(name="Appearances", default="") directory: StringProperty(name="Directory", default="") filepath: StringProperty(name="Filepath", default="") + rigify_it: BoolProperty(name='Apply Rigify Rig', default=False) def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) @@ -315,7 +314,10 @@ def execute(self, context): filepath=selected_rig, hide_armatures=False, import_garmentsupport=False, files=[], directory='', appearances="ALL", remap_depot=False) if props.fbx_rot: rotate_quat_180(self,context) + if self.rigify_it: + create_rigify_rig(self,context) return {'FINISHED'} + def draw(self,context): props = context.scene.cp77_panel_props layout = self.layout @@ -323,8 +325,9 @@ def draw(self,context): row = box.row(align=True) row.label(text="Select rig to load: ") row.prop(props, 'body_list', text="",) - row = box.row(align=True) - row.prop(props, 'fbx_rot', text="Load Rig in FBX Orientation") + col = box.column() + col.prop(self, 'rigify_it', text="Generate Rigify Control Rig") + col.prop(props, 'fbx_rot', text="Load Rig in FBX Orientation") class CP77AnimNamer(Operator): diff --git a/i_scene_cp77_gltf/cyber_props.py b/i_scene_cp77_gltf/cyber_props.py index 6c2dc53..26022e2 100644 --- a/i_scene_cp77_gltf/cyber_props.py +++ b/i_scene_cp77_gltf/cyber_props.py @@ -16,33 +16,38 @@ def CP77RefitList(context): - target_addon_paths = [None] - target_addon_names = ['None'] - - SoloArmsAddon = os.path.join(refit_dir, "SoloArmsAddon.zip") - Adonis = os.path.join(refit_dir, "Adonis.zip") - VanillaFemToMasc = os.path.join(refit_dir, "f2m.zip") - VanillaFem_BigBoobs = os.path.join(refit_dir, "f_normal_to_big_boobs.zip") - VanillaFem_SmallBoobs = os.path.join(refit_dir, "f_normal_to_small_boobs.zip") - VanillaMascToFem = os.path.join(refit_dir, "m2f.zip") - Lush = os.path.join(refit_dir, "lush.zip") - Hyst_RB = os.path.join(refit_dir, "hyst_rb.zip") - Hyst_EBB = os.path.join(refit_dir, "hyst_ebb.zip") - Hyst_EBB_RB = os.path.join(refit_dir, "hyst_ebb_rb.zip") - Flat_Chest = os.path.join(refit_dir, "flat_chest.zip") - Solo_Ultimate = os.path.join(refit_dir, "solo_ultimate.zip") - Gymfiend = os.path.join(refit_dir, "gymfiend.zip") - Freyja = os.path.join(refit_dir, "freyja.zip") - # Hyst_EBBP_Addon = os.path.join(refit_dir, "hyst_ebbp_addon.zip") + Adonis = os.path.join(refit_dir, "Adonis.refitter.zip") + VanillaFemToMasc = os.path.join(refit_dir, "f2m.refitter.zip") + VanillaFem_BigBoobs = os.path.join(refit_dir, "f_normal_to_big_boobs.refitter.zip") + VanillaFem_SmallBoobs = os.path.join(refit_dir, "f_normal_to_small_boobs.refitter.zip") + VanillaMascToFem = os.path.join(refit_dir, "m2f.refitter.zip") + Lush = os.path.join(refit_dir, "lush.refitter.zip") + Hyst_RB = os.path.join(refit_dir, "hyst_rb.refitter.zip") + Hyst_EBB = os.path.join(refit_dir, "hyst_ebb.refitter.zip") + Hyst_EBB_RB = os.path.join(refit_dir, "hyst_ebb_rb.refitter.zip") + Flat_Chest = os.path.join(refit_dir, "flat_chest.refitter.zip") + Solo_Ultimate = os.path.join(refit_dir, "solo_ultimate.refitter.zip") + Gymfiend = os.path.join(refit_dir, "gymfiend.refitter.zip") + Freyja = os.path.join(refit_dir, "freyja.refitter.zip") # Return the list of variable names - target_body_paths = [ Gymfiend, Freyja, SoloArmsAddon, Solo_Ultimate, Adonis, Flat_Chest, Hyst_EBB_RB, Hyst_EBB, Hyst_RB, Lush, VanillaFemToMasc, VanillaMascToFem, VanillaFem_BigBoobs, VanillaFem_SmallBoobs ] - target_body_names = [ 'Gymfiend', 'Freyja', 'SoloArmsAddon', 'Solo_Ultimate', 'Adonis', 'Flat_Chest', 'Hyst_EBB_RB', 'Hyst_EBB', 'Hyst_RB', 'Lush', 'VanillaFemToMasc', 'VanillaMascToFem', 'VanillaFem_BigBoobs', 'VanillaFem_SmallBoobs' ] + target_body_paths = [ Gymfiend, Freyja, Solo_Ultimate, Adonis, Flat_Chest, Hyst_EBB_RB, Hyst_EBB, Hyst_RB, Lush, VanillaFemToMasc, VanillaMascToFem, VanillaFem_BigBoobs, VanillaFem_SmallBoobs ] + target_body_names = [ 'Gymfiend', 'Freyja', 'Solo_Ultimate', 'Adonis', 'Flat_Chest', 'Hyst_EBB_RB', 'Hyst_EBB', 'Hyst_RB', 'Lush', 'VanillaFemToMasc', 'VanillaMascToFem', 'VanillaFem_BigBoobs', 'VanillaFem_SmallBoobs' ] # Return the list of tuples return target_body_paths, target_body_names -#def VertColourPresetList +def CP77RefitAddonList(context): + + SoloArmsAddon = os.path.join(refit_dir, "SoloArmsAddon.refitter.zip") + Hyst_EBBP_Addon = os.path.join(refit_dir, "hyst_ebbp_addon.refitter.zip") + + # Return the list of variable names + addon_target_body_paths = [SoloArmsAddon, Hyst_EBBP_Addon] + addon_target_body_names = ['SoloArmsAddon','Hyst_EBBP_Addon'] + + # Return the list of tuples + return addon_target_body_paths, addon_target_body_names def SetCyclesRenderer(use_cycles=True, set_gi_params=False): @@ -84,6 +89,7 @@ def cp77riglist(self, context): man_big = os.path.join(rig_dir, "man_big_full.glb") man_fat = os.path.join(rig_dir, "man_fat_full.glb") Judy = os.path.join(rig_dir, "judy_full.glb") + Songbird = os.path.join(rig_dir, "songbird_full.glb") Panam = os.path.join(rig_dir, "panam_full.glb") Jackie = os.path.join(rig_dir, "jackie_full.glb") Rhino = os.path.join(rig_dir, "rhino_full.glb") @@ -91,8 +97,8 @@ def cp77riglist(self, context): Smasher = os.path.join(rig_dir, "smasher_full.glb") # Store the variable names in a list - cp77rigs = [man_base, woman_base, man_big, man_fat, Judy, Panam, Jackie, Rhino, Dex, Smasher] - cp77rig_names = ['man_base', 'woman_base', 'man_big', 'man_fat', 'Judy', 'Panam', 'Jackie', 'Rhino', 'Dex', 'Adam Smasher'] + cp77rigs = [man_base, woman_base, man_big, man_fat, Judy, Songbird, Panam, Jackie, Rhino, Dex, Smasher] + cp77rig_names = ['man_base', 'woman_base', 'man_big', 'man_fat', 'Judy', 'Songbird', 'Panam', 'Jackie', 'Rhino', 'Dex', 'Adam Smasher'] # Return the list of variable names return cp77rigs, cp77rig_names @@ -208,6 +214,11 @@ class CP77_PT_PanelProps(PropertyGroup): name="Body Shape" ) + refit_addon_json: EnumProperty( + items=[(addon_target_body_names, addon_target_body_names, '') for addon_target_body_names in CP77RefitAddonList(None)[1]], + name="Refitter Addon" + ) + selected_armature: EnumProperty( name="Armatures", items=CP77ArmatureList diff --git a/i_scene_cp77_gltf/exporters/__init__.py b/i_scene_cp77_gltf/exporters/__init__.py index 5a71e89..00760cd 100644 --- a/i_scene_cp77_gltf/exporters/__init__.py +++ b/i_scene_cp77_gltf/exporters/__init__.py @@ -21,10 +21,10 @@ class CP77StreamingSectorExport(Operator,ExportHelper): bl_idname = "export_scene.cp77_sector" bl_label = "Export Sector Updates for Cyberpunk" bl_options = {'REGISTER','UNDO'} - bl_description = "Export changes to Sectors back to project" + bl_description = "Export changes to Sectors back to project" filename_ext = ".cpmodproj" filter_glob: StringProperty(default="*.cpmodproj", options={'HIDDEN'}) - + def draw(self, context): props = context.scene.cp77_panel_props layout = self.layout @@ -44,7 +44,7 @@ class CP77GLBExport(Operator,ExportHelper): bl_description = "Export to GLB with optimized settings for use with Wolvenkit for Cyberpunk 2077" filename_ext = ".glb" ### adds a checkbox for anim export settings - + filter_glob: StringProperty(default="*.glb", options={'HIDDEN'}) filepath: StringProperty(subtype="FILE_PATH") @@ -54,7 +54,7 @@ class CP77GLBExport(Operator,ExportHelper): default=True, description="Only Export the Selected Meshes. This is probably the setting you want to use" ) - + static_prop: BoolProperty( name="Export as Static Prop", default=False, @@ -94,15 +94,16 @@ def draw(self, context): row.prop(self, "static_prop") row = layout.row(align=True) row.prop(self, "apply_transform") - + + def execute(self, context): export_cyberpunk_glb( - context=context, - filepath=self.filepath, - export_poses=self.export_poses, - export_visible=self.export_visible, - limit_selected=self.limit_selected, - static_prop=self.static_prop, + context=context, + filepath=self.filepath, + export_poses=self.export_poses, + export_visible=self.export_visible, + limit_selected=self.limit_selected, + static_prop=self.static_prop, apply_transform=self.apply_transform, ) return {'FINISHED'} @@ -128,7 +129,7 @@ class CP77MlSetupExport(Operator): bl_description = "EXPERIMENTAL: Export material changes to mlsetup files" filepath: StringProperty(subtype="FILE_PATH") - + def execute(self, context): cp77_mlsetup_export(self, context) return {"FINISHED"} diff --git a/i_scene_cp77_gltf/exporters/glb_export.py b/i_scene_cp77_gltf/exporters/glb_export.py index c814278..2cdd35b 100644 --- a/i_scene_cp77_gltf/exporters/glb_export.py +++ b/i_scene_cp77_gltf/exporters/glb_export.py @@ -28,10 +28,12 @@ def default_cp77_options(): "export_shared_accessors": True, "export_try_omit_sparse_sk": False, }) + return options #make sure meshes are exported with tangents, morphs and vertex colors def cp77_mesh_options(): + vers=bpy.app.version options = { 'export_animations': False, 'export_tangents': True, @@ -39,19 +41,31 @@ def cp77_mesh_options(): 'export_morph_tangent': True, 'export_morph_normal': True, 'export_morph': True, - 'export_colors': True, 'export_attributes': True, } + + if vers[0] < 4: + options.update({ + 'export_colors': True, + + }) + + if vers[1] >= 2: + options.update({ + "export_all_vertex_colors ": True, + "export_active_vertex_color_when_no_material": True, + }) return options #the options for anims def pose_export_options(): + vers=bpy.app.version options = { 'export_animations': True, 'export_anim_slide_to_zero': True, 'export_animation_mode': 'ACTIONS', 'export_anim_single_armature': True, - "export_bake_animation": True + "export_bake_animation": False } return options @@ -101,14 +115,14 @@ def export_cyberpunk_glb(context, filepath, export_poses=False, export_visible=F objects = context.selected_objects options = default_cp77_options() - + #if for photomode, make sure there's an armature selected, if not use the message box to show an error if export_poses: armatures = [obj for obj in objects if obj.type == 'ARMATURE'] if not armatures: show_message("No armature objects are selected, please select an armature") return {'CANCELLED'} - + export_anims(context, filepath, options, armatures) return{'FINISHED'} @@ -148,7 +162,7 @@ def export_anims(context, filepath, options, armatures): action["fallbackFrameIndices"] = [] if "optimizationHints" not in action: action["optimizationHints"] = { "preferSIMD": False, "maxRotationCompression": 1} - + options.update(pose_export_options()) for armature in armatures: reset_armature(armature, context) @@ -190,7 +204,7 @@ def export_meshes(context, filepath, export_visible, limit_selected, static_prop bpy.ops.mesh.select_face_by_sides(number=3, type='NOTEQUAL', extend=False) show_message("All faces must be triangulated before exporting. Untriangulated faces have been selected for you. See https://tinyurl.com/triangulate-faces") return {'CANCELLED'} - + if red_garment_col: add_garment_cap(mesh) @@ -217,7 +231,7 @@ def export_meshes(context, filepath, export_visible, limit_selected, static_prop if not armature_modifier: show_message((f"Armature missing from: {mesh.name} Armatures are required for movement. If this is intentional, try 'export as static prop'. See https://tinyurl.com/armature-missing")) return {'CANCELLED'} - + armature = armature_modifier.object # Make necessary to armature visibility and selection state for export @@ -237,7 +251,7 @@ def export_meshes(context, filepath, export_visible, limit_selected, static_prop group_has_bone[group.index] = True # print(vertex_group.name) - # Add groups with no weights to the set + # Add groups with no weights to the set for group_index, has_bone in group_has_bone.items(): if not has_bone: groupless_bones.add(mesh.vertex_groups[group_index].name) @@ -276,7 +290,7 @@ def export_meshes(context, filepath, export_visible, limit_selected, static_prop armature.hide_set(True) except Exception as e: print(e) - + # def ExportAll(self, context): # #Iterate through all objects in the scene def ExportAll(self, context): diff --git a/i_scene_cp77_gltf/importers/__init__.py b/i_scene_cp77_gltf/importers/__init__.py index cea7750..00f807f 100644 --- a/i_scene_cp77_gltf/importers/__init__.py +++ b/i_scene_cp77_gltf/importers/__init__.py @@ -3,7 +3,7 @@ from bpy_extras.io_utils import ImportHelper from bpy.props import (StringProperty, EnumProperty, BoolProperty, CollectionProperty) -from bpy.types import (Operator, Panel, OperatorFileListElement, TOPBAR_MT_file_import ) +from bpy.types import (Operator, OperatorFileListElement, TOPBAR_MT_file_import ) from .import_with_materials import * from .entity_import import * from .sector_import import * @@ -131,28 +131,28 @@ class CP77StreamingSectorImport(Operator,ImportHelper): want_collisions: BoolProperty(name="Import Collisions",default=False,description="Import Box and Capsule Collision objects (mesh not yet supported)") am_modding: BoolProperty(name="Generate New Collectors",default=False,description="Generate _new collectors for sectors to allow modifications to be saved back to game") - + with_lights: BoolProperty(name="Import Lights",default=True,description="Import and setup Lights based on worldLightNodes") def draw(self, context): cp77_addon_prefs = bpy.context.preferences.addons['i_scene_cp77_gltf'].preferences props = context.scene.cp77_panel_props layout = self.layout box = layout.box() - row = box.row(align=True) - row.prop(self, "want_collisions",) - row = layout.row(align=True) - row.prop(self, "am_modding") - row = layout.row(align=True) - row.prop(props, "with_materials") + col = box.column() + col.prop(self, "want_collisions",) + col.prop(self, "with_lights") + col.prop(self, "am_modding") + col.prop(props, "with_materials") if cp77_addon_prefs.experimental_features: - row = layout.row(align=True) - row.prop(props,"remap_depot") + box = layout.box() + col = box.column() + col.prop(props,"remap_depot") def execute(self, context): bob=self.filepath props = context.scene.cp77_panel_props print('Importing Sectors from project - ',bob) - importSectors( bob, props.with_materials, props.remap_depot, self.want_collisions, self.am_modding) + importSectors( bob, props.with_materials, props.remap_depot, self.want_collisions, self.am_modding, self.with_lights) return {'FINISHED'} @@ -188,7 +188,7 @@ class CP77Import(Operator, ImportHelper): appearances: StringProperty(name= "Appearances", description="Appearances to extract with models", - default="ALL" + default="Default" ) # switch back to operator draw function to align with other UI features @@ -196,18 +196,34 @@ def draw(self, context): cp77_addon_prefs = bpy.context.preferences.addons['i_scene_cp77_gltf'].preferences props = context.scene.cp77_panel_props layout = self.layout - col = layout.column() - col.prop(props, 'with_materials') + if not props.with_materials: + box = layout.box() + col = box.column() + col.prop(props, 'with_materials') + col.prop(self, 'hide_armatures') + col.prop(self, 'import_garmentsupport') + if cp77_addon_prefs.experimental_features: + col.prop(props,"remap_depot") if props.with_materials: - col.prop(self, 'image_format') + box = layout.box() + col = box.column() + col.prop(props, 'with_materials') col.prop(self, 'exclude_unused_mats') col.prop(props, 'use_cycles') if props.use_cycles: col.prop(props, 'update_gi') - col.prop(self, 'hide_armatures') - col.prop(self, 'import_garmentsupport') - if cp77_addon_prefs.experimental_features: - col.prop(props,"remap_depot") + box = layout.box() + box.label(text='Texture Format:') + box.prop(self, 'image_format', text='') + box = layout.box() + box.label(text='Appearances to Import:') + box.prop(self, 'appearances', text='') + box = layout.box() + col = box.column() + col.prop(self, 'hide_armatures') + col.prop(self, 'import_garmentsupport') + if cp77_addon_prefs.experimental_features: + col.prop(props,"remap_depot") def execute(self, context): diff --git a/i_scene_cp77_gltf/importers/entity_import.py b/i_scene_cp77_gltf/importers/entity_import.py index 52398a4..77a9a54 100644 --- a/i_scene_cp77_gltf/importers/entity_import.py +++ b/i_scene_cp77_gltf/importers/entity_import.py @@ -51,15 +51,12 @@ def importEnt(with_materials, filepath='', appearances=[], exclude_meshes=[], in if not cp77_addon_prefs.non_verbose: print(f"Importing appearance: {', '.join(appearances)} from entity: {ent_name}") if filepath is not None: - j=jsonload(filepath) + ent_apps, ent_components, ent_component_data, res, ent_default =jsonload(filepath) - ent_apps= j['Data']['RootChunk']['appearances'] ent_applist=[] for app in ent_apps: ent_applist.append(app['appearanceName']['$value']) #print(ent_applist) - ent_components= j['Data']['RootChunk']['components'] - ent_component_data= j['Data']['RootChunk']['compiledData']['Data']['Chunks'] #presto_stash.append(ent_components) ent_complist=[] ent_rigs=[] @@ -83,7 +80,7 @@ def importEnt(with_materials, filepath='', appearances=[], exclude_meshes=[], in # presto_stash.append(ent_rigs) resolved=[] - for res_p in j['Data']['RootChunk']['resolvedDependencies']: + for res_p in res: resolved.append(os.path.join(path,res_p['DepotPath']['$value'])) # if no apps requested populate the list with all available. @@ -95,7 +92,7 @@ def importEnt(with_materials, filepath='', appearances=[], exclude_meshes=[], in appearances.append('BASE_COMPONENTS_ONLY') VS=[] - for x in j['Data']['RootChunk']['components']: + for x in ent_components: if 'name' in x.keys() and x['name']['$value']=='vehicle_slots': VS.append(x) if len(VS)>0: @@ -224,7 +221,7 @@ def importEnt(with_materials, filepath='', appearances=[], exclude_meshes=[], in app_name=a['appearanceName']['$value'] if ent_app_idx<0 and app_name =='default': - ent_default=j['Data']['RootChunk']['defaultAppearance']['$value'] + #ent_default=j['Data']['RootChunk']['defaultAppearance']['$value'] for i,a in enumerate(ent_apps): if a['name']['$value']==ent_default: print('appearance matched, id = ',i) @@ -263,12 +260,12 @@ def importEnt(with_materials, filepath='', appearances=[], exclude_meshes=[], in else: print('app file not found -', filepath) else: - if j['Data']['RootChunk']['compiledData']['Data']['Chunks']: - chunks= j['Data']['RootChunk']['compiledData']['Data']['Chunks'] + if ent_component_data: + chunks= ent_component_data if len(comps)==0: print('falling back to rootchunk comps') - comps= j['Data']['RootChunk']['components'] + comps= ent_components for c in comps: if c['$type']=='gameTransformAnimatorComponent': diff --git a/i_scene_cp77_gltf/importers/import_with_materials.py b/i_scene_cp77_gltf/importers/import_with_materials.py index 331bbda..85ee341 100644 --- a/i_scene_cp77_gltf/importers/import_with_materials.py +++ b/i_scene_cp77_gltf/importers/import_with_materials.py @@ -40,10 +40,10 @@ def objs_in_col(top_coll, objtype): appearances = None collection = None -def CP77GLBimport(self, with_materials, remap_depot, exclude_unused_mats=True, image_format='png', filepath='', hide_armatures=True, import_garmentsupport=False, files=[], directory='', appearances=[]): +def CP77GLBimport(self, with_materials, remap_depot, exclude_unused_mats=True, image_format='png', filepath='', hide_armatures=True, import_garmentsupport=False, files=[], directory='', appearances=[]): cp77_addon_prefs = bpy.context.preferences.addons['i_scene_cp77_gltf'].preferences context=bpy.context - obj = None + # obj = None start_time = time.time() loadfiles=self.files appearances=self.appearances.split(",") @@ -135,9 +135,6 @@ def CP77GLBimport(self, with_materials, remap_depot, exclude_unused_mats=True, i if name not in existingMaterials: bpy.data.materials.remove(bpy.data.materials[name], do_unlink=True, do_id_user=True, do_ui_user=True) - if import_garmentsupport: - manage_garment_support(existingMeshes, gltf_importer) - #Kwek: Gate this--do the block iff corresponding Material.json exist #Kwek: was tempted to do a try-catch, but that is just La-Z #Kwek: Added another gate for materials @@ -147,17 +144,17 @@ def CP77GLBimport(self, with_materials, remap_depot, exclude_unused_mats=True, i else: if with_materials==True and has_material_json: matjsonpath = current_file_base_path + ".Material.json" - obj = jsonload(matjsonpath) - if obj == None: - break - DepotPath = str(obj["MaterialRepo"]) + "\\" + DepotPath, json_apps, mats = jsonload(matjsonpath) + if DepotPath == None: + break + #DepotPath = str(obj["MaterialRepo"]) + "\\" context=bpy.context if remap_depot and os.path.exists(context.preferences.addons[__name__.split('.')[0]].preferences.depotfolder_path): DepotPath = context.preferences.addons[__name__.split('.')[0]].preferences.depotfolder_path if not cp77_addon_prefs.non_verbose: print(f"Using depot path: {DepotPath}") DepotPath= DepotPath.replace('\\', os.sep) - json_apps=obj['Appearances'] + #json_apps=obj['Appearances'] # fix the app names as for some reason they have their index added on the end. appkeys=[k for k in json_apps.keys()] for i,k in enumerate(appkeys): @@ -166,29 +163,37 @@ def CP77GLBimport(self, with_materials, remap_depot, exclude_unused_mats=True, i #appearances = ({'name':'short_hair'},{'name':'02_ca_limestone'},{'name':'ml_plastic_doll'},{'name':'03_ca_senna'}) #if appearances defined populate valid mats with the mats for them, otherwise populate with everything used. if len(appearances)>0 and 'ALL' not in appearances: - for key in json_apps.keys(): - if key in appearances: - for m in json_apps[key]: - validmats[m]=True + if 'Default' in appearances: + first_key = next(iter(json_apps)) + for m in json_apps[first_key]: + validmats[m] = True + else: + for key in json_apps.keys(): + if key in appearances: + for m in json_apps[key]: + validmats[m]=True # there isnt always a default, so if none were listed, or ALL was used, or an invalid one add everything. if len(validmats)==0: for key in json_apps.keys(): for m in json_apps[key]: validmats[m]=True if with_materials: - import_mats(current_file_base_path, DepotPath, exclude_unused_mats, existingMeshes, gltf_importer, image_format, obj, validmats) + import_mats(current_file_base_path, DepotPath, exclude_unused_mats, existingMeshes, gltf_importer, image_format, mats, validmats) + + if import_garmentsupport: + manage_garment_support(existingMeshes, gltf_importer) if not cp77_addon_prefs.non_verbose: print(f"GLB Import Time: {(time.time() - start_time)} Seconds") print('') print('-------------------- Finished importing Cyberpunk 2077 Model --------------------') -def import_mats(BasePath, DepotPath, exclude_unused_mats, existingMeshes, gltf_importer, image_format, obj, validmats): +def import_mats(BasePath, DepotPath, exclude_unused_mats, existingMeshes, gltf_importer, image_format, mats, validmats): failedon = [] cp77_addon_prefs = bpy.context.preferences.addons['i_scene_cp77_gltf'].preferences start_time = time.time() for mat in validmats.keys(): - for m in obj['Materials']: + for m in mats: #obj['Materials']: if m['Name'] != mat: continue if 'BaseMaterial' in m.keys(): @@ -212,7 +217,7 @@ def import_mats(BasePath, DepotPath, exclude_unused_mats, existingMeshes, gltf_i print(m.keys()) MatImportList = [k for k in validmats.keys()] - Builder = MaterialBuilder(obj, DepotPath, str(image_format), BasePath) + Builder = MaterialBuilder(mats, DepotPath, str(image_format), BasePath) counter = 0 bpy_mats = bpy.data.materials for name in bpy.data.meshes.keys(): @@ -248,10 +253,10 @@ def import_mats(BasePath, DepotPath, exclude_unused_mats, existingMeshes, gltf_i else: if matname in validmats.keys(): index = 0 - for rawmat in obj["Materials"]: + for rawmat in mats: if rawmat["Name"] == matname: try: - bpymat = Builder.create(index) + bpymat = Builder.create(mats, index) if bpymat: bpymat['BaseMaterial'] = validmats[matname]['BaseMaterial'] bpymat['GlobalNormal'] = validmats[matname]['GlobalNormal'] @@ -279,10 +284,10 @@ def import_mats(BasePath, DepotPath, exclude_unused_mats, existingMeshes, gltf_i return index = 0 - for rawmat in obj["Materials"]: + for rawmat in mats:#obj["Materials"]: if rawmat["Name"] not in bpy.data.materials.keys() and ( (rawmat["Name"] in MatImportList) or len(MatImportList) < 1): - Builder.create(index) + Builder.create(mats,index) index = index + 1 def blender_4_scale_armature_bones(): @@ -310,6 +315,7 @@ def import_meshes_and_anims(collection, gltf_importer, hide_armatures, o): meshes = gltf_importer.data.meshes if animations: get_anim_info(animations) + bpy.context.scene.render.fps = 30 if meshes and "Icosphere" not in mesh.name: if 'Armature' in o.name: o.hide_set(hide_armatures) diff --git a/i_scene_cp77_gltf/importers/sector_import.py b/i_scene_cp77_gltf/importers/sector_import.py index a9c9711..cb25c6e 100644 --- a/i_scene_cp77_gltf/importers/sector_import.py +++ b/i_scene_cp77_gltf/importers/sector_import.py @@ -244,7 +244,7 @@ def get_tan_pos(inst): pos[1][2] = inst['Elements'][1]['Z'] return pos -def importSectors( filepath, with_mats, remap_depot, want_collisions=False, am_modding=False,with_lights=True ): +def importSectors( filepath, with_mats, remap_depot, want_collisions, am_modding, with_lights): cp77_addon_prefs = bpy.context.preferences.addons['i_scene_cp77_gltf'].preferences if not cp77_addon_prefs.non_verbose: print('') @@ -267,7 +267,6 @@ def importSectors( filepath, with_mats, remap_depot, want_collisions=False, am_m s.clip_end = 50000 jsonpath = glob.glob(os.path.join(path, "**", "*.streamingsector.json"), recursive = True) - print(jsonpath) meshes=[] C = bpy.context # Use object wireframe colors not theme - doesnt work need to find hte viewport as the context doesnt return that for this call @@ -277,11 +276,8 @@ def importSectors( filepath, with_mats, remap_depot, want_collisions=False, am_m continue if VERBOSE: print(os.path.join(path,os.path.basename(project)+'.streamingsector.json')) - print(filepath) - j = jsonload(filepath) + t, nodes = jsonload(filepath) sectorName=os.path.basename(filepath)[:-5] - t=j['Data']['RootChunk']['nodeData']['Data'] - nodes = j["Data"]["RootChunk"]["nodes"] #print(len(nodes)) #nodes=[] for i,e in enumerate(nodes): @@ -399,10 +395,8 @@ def importSectors( filepath, with_mats, remap_depot, want_collisions=False, am_m if VERBOSE: print(projectjson) print(filepath) - j=jsonload(filepath) - - t=j['Data']['RootChunk']['nodeData']['Data'] # add nodeDataIndex props to all the nodes in t + t, nodes = jsonload(filepath) for index, obj in enumerate(t): obj['nodeDataIndex']=index @@ -424,7 +418,6 @@ def importSectors( filepath, with_mats, remap_depot, want_collisions=False, am_m Sector_additions_coll=bpy.data.collections.new(sectorName+'_new') coll_scene.children.link(Sector_additions_coll) - nodes = j["Data"]["RootChunk"]["nodes"] print(fpn, ' Processing ',len(nodes),' nodes for sector', sectorName) group='' for i,e in enumerate(nodes): @@ -803,7 +796,7 @@ def importSectors( filepath, with_mats, remap_depot, want_collisions=False, am_m bpymat = mis[mipath] else: builder = MaterialBuilder(obj,path,'png',path) - bpymat = builder.create(index) + bpymat = builder.createdecal(index) mis[mipath] = bpymat if bpymat: o.data.materials.append(bpymat) diff --git a/i_scene_cp77_gltf/jsontool.py b/i_scene_cp77_gltf/jsontool.py index 6ee0eeb..d5c9362 100644 --- a/i_scene_cp77_gltf/jsontool.py +++ b/i_scene_cp77_gltf/jsontool.py @@ -1,7 +1,7 @@ import bpy import json import os -from .main.common import show_message +from .main.common import show_message, load_zip def normalize_paths(data): if isinstance(data, dict): @@ -37,7 +37,8 @@ def load_json(file_path): def jsonload(filepath): if not filepath.endswith('.json'): - raise ValueError("This is not a json, what are you doing?") + if not filepath.endswith('.zip'): + raise ValueError(f"{filepath} is not a json, what are you doing?") cp77_addon_prefs = bpy.context.preferences.addons['i_scene_cp77_gltf'].preferences # Extract the base name of the file @@ -54,7 +55,7 @@ def jsonload(filepath): print(f"invalid anims.json found at: {filepath} this plugin requires jsons generated using the latest version of Wolvenkit") show_message(f"invalid anims.json found at: {base_name} this plugin requires jsons generated using the latest version of Wolvenkit") # Do something for .anims.json - + return data case _ if base_name.endswith('.app.json'): if not cp77_addon_prefs.non_verbose: print(f"Processing: {base_name}") @@ -64,7 +65,7 @@ def jsonload(filepath): print(f"invalid app.json found at: {filepath} this plugin requires jsons generated using the latest version of Wolvenkit") show_message(f"invalid app.json: {base_name} this plugin requires jsons generated using the latest version of Wolvenkit") # Do something for .app.json - + return data case _ if base_name.endswith('.ent.json'): if not cp77_addon_prefs.non_verbose: print(f"Processing: {base_name}") @@ -74,8 +75,13 @@ def jsonload(filepath): print(f"attempted import of invalid ent.json from: {filepath} this plugin requires jsons generated using the latest version of Wolvenkit") show_message(f"attempted import of invalid ent.json: {base_name} this plugin requires jsons generated using the latest version of Wolvenkit") return 'CANCELLED' + ent_apps= data['Data']['RootChunk']['appearances'] + ent_components= data['Data']['RootChunk']['components'] + ent_component_data= data['Data']['RootChunk']['compiledData']['Data']['Chunks'] + res = data['Data']['RootChunk']['resolvedDependencies'] + ent_default = data['Data']['RootChunk']['defaultAppearance']['$value'] # Do something for .ent.json - + return ent_apps, ent_components, ent_component_data, res, ent_default case _ if base_name.endswith('.mesh.json'): if not cp77_addon_prefs.non_verbose: print(f"Processing: {base_name}") @@ -85,7 +91,7 @@ def jsonload(filepath): print(f"invalid mesh.json found at: {filepath} this plugin requires jsons generated using the latest version of Wolvenkit") show_message(f"found invalid mesh.json: {base_name} this plugin requires jsons generated using the latest version of Wolvenkit") # Do something for .mesh.json - # + return data case _ if base_name.endswith('.Material.json'): if not cp77_addon_prefs.non_verbose: print(f"Processing: {base_name}") @@ -97,8 +103,12 @@ def jsonload(filepath): else: if not cp77_addon_prefs.non_verbose: print('Building shaders') + depotpath = data["MaterialRepo"] + "\\" + json_apps = data['Appearances'] + mats = data['Materials'] + return depotpath, json_apps, mats # Do something for .material.json - + return data case _ if base_name.endswith('.gradient.json'): if not cp77_addon_prefs.non_verbose: print(f"Processing: {base_name}") @@ -107,7 +117,7 @@ def jsonload(filepath): if not cp77_addon_prefs.non_verbose: print(f"invalid gradient.json found at: {filepath} this plugin requires jsons generated using the latest version of Wolvenkit") show_message(f"found invalid gradient.json: {base_name} this plugin requires jsons generated using the latest version of Wolvenkit") - + return data case _ if base_name.endswith('.mlsetup.json'): if not cp77_addon_prefs.non_verbose: print(f"Processing: {base_name}") @@ -117,7 +127,7 @@ def jsonload(filepath): print(f"invalid mlsetup.json found at: {filepath} import will continue but shaders may be incorrectly set up for this mesh") show_message(f"invalid mlsetup.json: {base_name} import will continue but shaders may be incorrectly setup for this mesh") # Do something for .mlsetup.json - + return data case _ if base_name.endswith('.mltemplate.json'): if not cp77_addon_prefs.non_verbose: print(f"Processing: {base_name}") @@ -127,7 +137,7 @@ def jsonload(filepath): print(f"invalid mltemplate.json found at: {filepath} import will continue but shaders may be incorrectly set up for this mesh") show_message(f"invalid mltemplate.json: {base_name} import will continue but shaders may be incorrectly setup for this mesh") # Do something for .mlsetup.json - + return data case _ if base_name.endswith('.mt.json'): if not cp77_addon_prefs.non_verbose: print(f"Processing: {base_name}") @@ -136,7 +146,7 @@ def jsonload(filepath): if not cp77_addon_prefs.non_verbose: print(f"invalid mt.json found at: {filepath} import will continue but shaders may be incorrectly set up for this mesh") show_message(f"invalid mt.json: {base_name} import will continue but shaders may be incorrectly setup for this mesh") - + return data case _ if base_name.endswith('.mi.json'): if not cp77_addon_prefs.non_verbose: print(f"Processing: {base_name}") @@ -145,7 +155,7 @@ def jsonload(filepath): if not cp77_addon_prefs.non_verbose: print(f"invalid mi.json found at: {filepath} import will continue but shaders may be incorrectly set up for this mesh") show_message(f"invalid mi.json: {base_name} import will continue but shaders may be incorrectly setup for this mesh") - + return data case _ if base_name.endswith('.phys.json'): if not cp77_addon_prefs.non_verbose: print(f"Processing: {base_name}") @@ -155,7 +165,7 @@ def jsonload(filepath): print(f"invalid phys.json found at: {filepath} import may continue but .phys colliders will not be imported") show_message(f"invalid phys.json: {base_name} import may continue but .phys colliders will not be imported") # Do something for .phys.json - + return data case _ if base_name.endswith('.streamingsector.json'): if not cp77_addon_prefs.non_verbose: print(f"Processing: {base_name}") @@ -164,14 +174,17 @@ def jsonload(filepath): if not cp77_addon_prefs.non_verbose: print(f"invalid streamingsector.json found at: {filepath} this plugin requires jsons generated with the latest version of Wolvenkit") show_message(f"invalid streamingsector.json: {base_name} this plugin requires jsons generated with the latest version of Wolvenkit") - # Do something for .streamingsector.json + else: + t = data['Data']['RootChunk']['nodeData']['Data'] + nodes = data["Data"]["RootChunk"]["nodes"] + return t, nodes case _ if base_name.endswith('.streamingblock.json'): if not cp77_addon_prefs.non_verbose: print(f"Processing: {base_name}") data=load_json(filepath) # Do something for .streamingblock.json - + return data case _ if base_name.endswith('.rig.json'): if not cp77_addon_prefs.non_verbose: print(f"Processing: {base_name}") @@ -181,7 +194,7 @@ def jsonload(filepath): print(f"invalid rig.json found at: {filepath} this plugin requires jsons generated with the latest version of Wolvenkit") show_message(f"invalid rig.json: {base_name} this plugin requires jsons generated with the latest version of Wolvenkit") # Do something for .rig.json - + return data case _ if base_name.endswith('.hp.json'): if not cp77_addon_prefs.non_verbose: print(f"Processing: {base_name}") @@ -190,7 +203,7 @@ def jsonload(filepath): if not cp77_addon_prefs.non_verbose: print(f"invalid hp.json found at: {filepath} this plugin requires jsons generated with the latest version of Wolvenkit") show_message(f"invalid Hair Profile: {base_name} this plugin requires jsons generated with the latest version of Wolvenkit") - + return data case _ if base_name.endswith('.cfoliage.json'): if not cp77_addon_prefs.non_verbose: print(f"Processing: {base_name}") @@ -199,16 +212,28 @@ def jsonload(filepath): if not cp77_addon_prefs.non_verbose: print(f"invalid cfoliage.json found at: {filepath} this plugin requires jsons generated with the latest version of Wolvenkit") show_message(f"invalid cfoliage.json : {base_name} this plugin requires jsons generated with the latest version of Wolvenkit") - + return data + case _ if base_name.endswith('.refitter.zip'): + if not cp77_addon_prefs.non_verbose: + print(f"Processing: {base_name}") + data=load_zip(filepath) + data=jsonloads(data) + lattice_object_name = data["lattice_object_name"] + control_points = data["deformed_control_points"] + lattice_points = data["lattice_points"] + lattice_object_location = data["lattice_object_location"] + lattice_object_rotation = data["lattice_object_rotation"] + lattice_object_scale = data["lattice_object_scale"] + lattice_interpolation_u = data["lattice_interpolation_u"] + lattice_interpolation_v = data["lattice_interpolation_v"] + lattice_interpolation_w = data["lattice_interpolation_w"] + return lattice_object_name, control_points, lattice_points, lattice_object_location, lattice_object_rotation, lattice_object_scale, lattice_interpolation_u, lattice_interpolation_v, lattice_interpolation_w case _: if not cp77_addon_prefs.non_verbose: print(f"Incompatible Json: {base_name}") print("json files must be generated with a latest version of Wolvenkit") show_message(f"Incompatible Json: {base_name} json files must be generated with a latest version of Wolvenkit") # Do something for other json files - - # Return or process the data as needed - return data def jsonloads(jsonstrings): diff --git a/i_scene_cp77_gltf/main/common.py b/i_scene_cp77_gltf/main/common.py index 9494a9b..54f8996 100644 --- a/i_scene_cp77_gltf/main/common.py +++ b/i_scene_cp77_gltf/main/common.py @@ -2,6 +2,7 @@ import bpy import os import math +import zipfile from bpy.types import EnumProperty from mathutils import Color import pkg_resources @@ -10,6 +11,14 @@ from mathutils import Vector import json +def load_zip(path): + with zipfile.ZipFile(path, "r") as z: + filename=z.namelist()[0] + print(filename) + with z.open(filename) as f: + data = f.read() + return data + # Function to dynamically gather classes defined in the same file def get_classes(module): operators = set() diff --git a/i_scene_cp77_gltf/main/setup.py b/i_scene_cp77_gltf/main/setup.py index f136085..9440f93 100644 --- a/i_scene_cp77_gltf/main/setup.py +++ b/i_scene_cp77_gltf/main/setup.py @@ -33,15 +33,202 @@ class MaterialBuilder: - def __init__(self,Obj,BasePath,image_format,MeshPath): + def __init__(self, obj, BasePath,image_format,MeshPath): self.BasePath = BasePath self.image_format = image_format - self.obj = Obj + self.obj = obj # = Obj self.MeshPath= MeshPath before,mid,after=MeshPath.partition('source\\raw\\'.replace('\\',os.sep)) self.ProjPath=before+mid - def create(self,materialIndex): + def create(self, mats, materialIndex): + if mats: + rawMat = mats[materialIndex] + + verbose=True + + bpyMat = bpy.data.materials.new(rawMat["Name"]) + bpyMat['DepotPath'] = self.BasePath + bpyMat['ProjPath']= self.ProjPath + bpyMat['MaterialTemplate'] = rawMat["MaterialTemplate"] + bpyMat.use_nodes = True + no_shadows=False + match rawMat["MaterialTemplate"]: + case "engine\\materials\\multilayered.mt" | "base\\materials\\vehicle_destr_blendshape.mt" | "base\\materials\\multilayered_clear_coat.mt" | "base\\materials\\multilayered_terrain.mt": + multilayered = Multilayered(self.BasePath,self.image_format,self.ProjPath) + multilayered.create(rawMat["Data"],bpyMat) + + + #case "base\\materials\\multilayered_terrain.mt": + # multilayeredTerrain = Multilayered(self.BasePath,self.image_format, self.ProjPath) + # multilayeredTerrain.create(rawMat["Data"],bpyMat) + + # This material should be handled within the main multilayered.py file. Commenting this out for now in case I broke something - jato + #case "base\\materials\\multilayered_clear_coat.mt": + #multilayered = MultilayeredClearCoat(self.BasePath,self.image_format) + #multilayered.create(rawMat["Data"],bpyMat) + + # This material should be handled within the main multilayered.py file. Commenting this out for now in case I broke something - jato + #case "base\\materials\\vehicle_destr_blendshape.mt": + #vehicleDestrBlendshape = VehicleDestrBlendshape(self.BasePath, self.image_format) + #vehicleDestrBlendshape.create(rawMat["Data"],bpyMat) + + case "base\\materials\\mesh_decal.mt" | "base\\materials\\mesh_decal_wet_character.mt": + if 'EnableMask' in rawMat.keys(): + enableMask=rawMat['EnableMask'] + else: + enableMask=False + no_shadows=True + meshDecal = MeshDecal(self.BasePath, self.image_format, self.ProjPath,enableMask) + meshDecal.create(rawMat["Data"],bpyMat) + + case "base\\materials\\mesh_decal_double_diffuse.mt": + no_shadows=True + meshDecalDoubleDiffuse = MeshDecalDoubleDiffuse(self.BasePath, self.image_format) + meshDecalDoubleDiffuse.create(rawMat["Data"],bpyMat) + + case "base\\materials\\vehicle_mesh_decal.mt" : + no_shadows=True + if 'EnableMask' in rawMat.keys(): + enableMask=rawMat['EnableMask'] + else: + enableMask=False + vehicleMeshDecal = VehicleMeshDecal(self.BasePath, self.image_format, self.ProjPath, enableMask) + vehicleMeshDecal.create(rawMat["Data"],bpyMat) + + case "base\\materials\\vehicle_lights.mt": + vehicleLights = VehicleLights(self.BasePath, self.image_format, self.ProjPath) + vehicleLights.create(rawMat["Data"],bpyMat) + + case "base\\materials\\skin.mt": + skin = Skin(self.BasePath, self.image_format, self.ProjPath) + skin.create(rawMat["Data"],bpyMat) + + case "engine\\materials\\metal_base.remt" | "engine\\materials\\metal_base_proxy.mt" |\ + 'base\\materials\\metal_base_parallax.mt' | 'base\materials\metal_base_gradientmap_recolor.mt': + if 'EnableMask' in rawMat.keys(): + enableMask=rawMat['EnableMask'] + else: + enableMask=False + metalBase = MetalBase(self.BasePath,self.image_format, self.ProjPath, enableMask) + metalBase.create(rawMat["Data"],bpyMat) + + case "base\\materials\\metal_base_det.mt": + metalBaseDet = MetalBaseDet(self.BasePath,self.image_format, self.ProjPath) + metalBaseDet.create(rawMat["Data"],bpyMat) + + case "base\\materials\\hair.mt": + hair = Hair(self.BasePath,self.image_format, self.ProjPath) + hair.create(rawMat["Data"],bpyMat) + + case "base\\materials\\mesh_decal_gradientmap_recolor.mt": + no_shadows=True + meshDecalGradientMapReColor = MeshDecalGradientMapReColor(self.BasePath,self.image_format, self.ProjPath) + meshDecalGradientMapReColor.create(rawMat["Data"],bpyMat) + + case "base\\materials\\eye.mt": + eye = Eye(self.BasePath,self.image_format, self.ProjPath) + eye.create(rawMat["Data"],bpyMat) + + case "base\\materials\\eye_gradient.mt": + eyeGradient = EyeGradient(self.BasePath,self.image_format, self.ProjPath) + eyeGradient.create(rawMat["Data"],bpyMat) + + case "base\\materials\\eye_shadow.mt": + eyeShadow = EyeShadow(self.BasePath,self.image_format, self.ProjPath) + eyeShadow.create(rawMat["Data"],bpyMat) + + case "base\\materials\\mesh_decal_emissive.mt" : + no_shadows=True + meshDecalEmissive = MeshDecalEmissive(self.BasePath,self.image_format, self.ProjPath) + meshDecalEmissive.create(rawMat["Data"],bpyMat) + + case "base\\materials\\glass.mt" | "base\\materials\\vehicle_glass.mt": + glass = Glass(self.BasePath,self.image_format, self.ProjPath) + glass.create(rawMat["Data"],bpyMat) + + case "base\\materials\\glass_deferred.mt": + glassdef = GlassDeferred(self.BasePath,self.image_format, self.ProjPath) + glassdef.create(rawMat["Data"],bpyMat) + + case "base\\fx\\shaders\\signages.mt" : + signages= Signages(self.BasePath,self.image_format, self.ProjPath) + signages.create(rawMat["Data"],bpyMat) + + case "base\\materials\\glass_onesided.mt" | "base\\materials\\vehicle_glass_onesided.mt": + glass = Glass(self.BasePath,self.image_format, self.ProjPath) + glass.create(rawMat["Data"],bpyMat) + + case "base\\materials\\mesh_decal_parallax.mt" | "base\\materials\\vehicle_mesh_decal_parallax.mt": + no_shadows=True + meshDecalParallax = MeshDecalParallax(self.BasePath,self.image_format, self.ProjPath) + meshDecalParallax.create(rawMat["Data"],bpyMat) + + case "base\\fx\\shaders\\parallaxscreen.mt" : + parallaxScreen = ParallaxScreen(self.BasePath,self.image_format,self.ProjPath) + parallaxScreen.create(rawMat["Data"],bpyMat) + + case "base\\fx\\shaders\\parallaxscreen_transparent.mt" : + parallaxScreenTransparent = ParallaxScreenTransparent(self.BasePath,self.image_format,self.ProjPath) + parallaxScreenTransparent.create(rawMat["Data"],bpyMat) + + case "base\\materials\\speedtree_3d_v8_twosided.mt" | "base\\materials\\speedtree_3d_v8_onesided.mt" | "base\\materials\\speedtree_3d_v8_seams.mt": + speedtree = SpeedTree(self.BasePath,self.image_format, self.ProjPath) + speedtree.create(rawMat["Data"],bpyMat) + + case "base\\fx\\shaders\\television_ad.mt" : + televisionAd = TelevisionAd(self.BasePath,self.image_format,self.ProjPath) + televisionAd.create(rawMat["Data"],bpyMat) + + case "base\\materials\\window_parallax_interior_proxy.mt" | "base\\materials\\window_parallax_interior.mt": + window = windowParallaxIntProx(self.BasePath,self.image_format,self.ProjPath) + window.create(rawMat["Data"],bpyMat) + + case "base\\fx\\shaders\\hologram.mt" : + hologram = Hologram(self.BasePath,self.image_format,self.ProjPath) + hologram.create(rawMat["Data"],bpyMat) + + case _: + print('Unhandled mt - ', rawMat["MaterialTemplate"]) + context=bpy.context + if context.preferences.addons[__name__.split('.')[0]].preferences.experimental_features: + unkown = unknownMaterial(self.BasePath,self.image_format,self.ProjPath) + unkown.create(rawMat["Data"],bpyMat) + + #set the viewport blend mode to hashed - no more black tattoos and cybergear + bpyMat.blend_method='HASHED' + bpyMat['no_shadows']=no_shadows + return bpyMat + + else: + self.obj["Data"]["RootChunk"].get("baseMaterial") + if self.obj["Header"].get("ArchiveFileName"): + name = self.obj["Header"]["ArchiveFileName"] + name = os.path.basename(name) + else: + name = 'decal_material' + + bpyMat = bpy.data.materials.new(name) + bpyMat.use_nodes = True + + match self.obj["Data"]["RootChunk"]["baseMaterial"]["DepotPath"]['$value']: + case "base\\materials\\decal.remt" | "base\\materials\\decal_roughness.mt" | "base\\materials\\decal_puddle.mt":# | "base\materials\decal_normal_roughness_metalness.mt": + print('decal.remt') + decal = Decal(self.BasePath,self.image_format) + decal.create(self.obj["Data"]["RootChunk"],bpyMat) + + case "base\\materials\\decal_gradientmap_recolor.mt": + print('decal_gradientmap_recolor.mt') + decalGradientMapRecolor = DecalGradientmapRecolor(self.BasePath,self.image_format, self.ProjPath) + decalGradientMapRecolor.create(self.obj["Data"]["RootChunk"],bpyMat) + + + case _: + print(self.obj["Data"]["RootChunk"]["baseMaterial"]["DepotPath"]['$value']," | unimplemented yet") + + bpyMat.blend_method='HASHED' + return bpyMat + def createdecal(self,materialIndex): if self.obj.get("Materials"): rawMat = self.obj["Materials"][materialIndex] diff --git a/i_scene_cp77_gltf/meshtools/__init__.py b/i_scene_cp77_gltf/meshtools/__init__.py index 2e31e91..592a14d 100644 --- a/i_scene_cp77_gltf/meshtools/__init__.py +++ b/i_scene_cp77_gltf/meshtools/__init__.py @@ -51,7 +51,7 @@ def draw(self, context): col = box.column() col.operator("cp77.submesh_prep") col.operator("cp77.group_verts", text="Group Ungrouped Verts") - col.operator('object.delete_unused_vgroups', text="Delete Unused Vert Groups") + col.operator("cp77.del_empty_vgroup", text="Delete Unused Vert Groups") box = layout.box() box.label(text="AKL Autofitter", icon_value=get_icon("REFIT")) @@ -160,7 +160,7 @@ def invoke(self, context, event): def execute(self, context): # Call the trans_weights function with the provided arguments - result = trans_weights(self, context) + result = trans_weights(self, context, vertInterop, bySubmesh) return {"FINISHED"} def draw(self,context): @@ -263,6 +263,17 @@ class CP77GroupVerts(Operator): def execute(self, context): CP77GroupUngroupedVerts(self, context) return {'FINISHED'} + +class CP77DeleteVertGroups(Operator): + bl_idname = "cp77.del_empty_vgroup" + bl_parent_id = "CP77_PT_MeshTools" + bl_label = "Delete Unused Vertex Groups" + bl_options = {'REGISTER', 'UNDO'} + bl_description = "Assign ungrouped vertices to their nearest group" + + def execute(self, context): + del_empty_vgroup(self, context) + return {'FINISHED'} class CP77Autofitter(Operator): bl_idname = "cp77.auto_fitter" @@ -282,12 +293,20 @@ def invoke(self, context, event): def execute(self, context): props = context.scene.cp77_panel_props target_body_name = props.refit_json + addon_target_body_name = props.refit_addon_json target_body_paths, target_body_names = CP77RefitList(context) - refitter = CP77RefitChecker(self, context) + addon_target_body_paths, addon_target_body_names = CP77RefitAddonList(context) + refitter, addon = CP77RefitChecker(self, context) if target_body_name in target_body_names: target_body_path = target_body_paths[target_body_names.index(target_body_name)] - CP77Refit(context, refitter, target_body_path, target_body_name, props.fbx_rot) + if self.useAddon: + if addon_target_body_name in addon_target_body_names: + addon_target_body_path = addon_target_body_paths[addon_target_body_path.index(addon_target_body_name)] + else: + addon_target_body_path = None + addon_target_body_name = None + CP77Refit(context, refitter, addon, target_body_path, target_body_name, addon_target_body_path, addon_target_body_name,self.useAddon, props.fbx_rot) return {'FINISHED'} @@ -302,7 +321,7 @@ def draw(self,context): row = layout.row(align=True) split = row.split(factor=0.2,align=True) split.label(text="Addon:") - split.prop(props, 'refit_json', text="") + split.prop(props, 'refit_addon_json', text="") col = layout.column() col.prop(props, 'fbx_rot', text="Refit a mesh in FBX orientation") col.prop(self, 'useAddon', text="Use a Refitter Addon") diff --git a/i_scene_cp77_gltf/meshtools/meshtools.py b/i_scene_cp77_gltf/meshtools/meshtools.py index 0bb888a..66a57a7 100644 --- a/i_scene_cp77_gltf/meshtools/meshtools.py +++ b/i_scene_cp77_gltf/meshtools/meshtools.py @@ -1,4 +1,3 @@ -import zipfile import bpy import json import os @@ -6,7 +5,7 @@ from ..cyber_props import * from ..main.common import show_message from ..main.bartmoss_functions import setActiveShapeKey, getShapeKeyNames, getModNames - +from ..jsontool import * def CP77SubPrep(self, context, smooth_factor, merge_distance): scn = context.scene obj = context.object @@ -188,15 +187,17 @@ def CP77RefitChecker(self, context): scene = context.scene objects = scene.objects refitter = [] - + addon = [] for obj in objects: if obj.type =='LATTICE': if "refitter_type" in obj: refitter.append(obj) print('refitters found in scene:', refitter) - - print('refitter result:', refitter) - return refitter + if "refitter_addon" in obj: + addon.append(obj) + print('refitter addons found in scene:', addon) + print(f'refitter result: {refitter} refitter addon: {addon}') + return refitter, addon def applyModifierAsShapeKey(obj): names = getModNames(obj) @@ -239,21 +240,26 @@ def applyRefitter(obj): newgs = setActiveShapeKey(obj, name) newgs.name = 'GarmentSupport' -def CP77Refit(context, refitter, target_body_path, target_body_name, fbx_rot): - selected_meshes = [obj for obj in context.selected_objects if obj.type == 'MESH'] - scene = context.scene +def CP77Refit(context, refitter, addon, target_body_path, target_body_name, useAddon, addon_target_body_path, addon_target_body_name, fbx_rot): obj = context.object - current_mode = context.mode if not obj: show_message("No active object. Please Select a Mesh and try again") return {'CANCELLED'} if obj.type != 'MESH': show_message("The active object is not a mesh.") return {'CANCELLED'} + else: + autofitter(context, refitter, addon, target_body_path, useAddon, addon_target_body_path, addon_target_body_name, target_body_name, fbx_rot) + +def autofitter(context, refitter, addon, target_body_path, target_body_name, useAddon, addon_target_body_path, addon_target_body_name, fbx_rot): + selected_meshes = [obj for obj in context.selected_objects if obj.type == 'MESH'] + scene = context.scene + current_mode = context.mode refitter_obj = None + addon_obj = None r_c = None print(fbx_rot) - print(refitter) + print(refitter, addon) if len(refitter) != 0: for obj in refitter: @@ -261,24 +267,36 @@ def CP77Refit(context, refitter, target_body_path, target_body_name, fbx_rot): if obj['refitter_type'] == target_body_name: print(obj['refitter_type'], 'refitter found') refitter_obj = obj - if refitter_obj: print('theres a refitter:', refitter_obj.name, 'type:', refitter_obj['refitter_type']) for mesh in selected_meshes: - print('checking for refits:', mesh.name) + print('Checking mesh for refits:', mesh.name) refit = False for modifier in mesh.modifiers: if modifier.type == 'LATTICE' and modifier.object == refitter_obj: refit = True print(mesh.name, 'is already refit for', target_body_name) + applyRefitter(mesh) + if len(addon) != 0: + for obj in addon: + print('addon object:', obj.name) + if obj['refitter_addon_type'] == addon_target_body_name: + print(obj['refitter_addon_type'], 'refitter addon found') + addon_obj = obj + if addon_obj: + print('theres a refitter addon:', addon_obj.name, 'type:', addon_obj['refitter_addon_type']) + for mesh in selected_meshes: + print('Checking mesh for refit addons:', mesh.name) + refit = False + for modifier in mesh.modifiers: + if modifier.type == 'LATTICE' and modifier.object == addon_obj: + refit = True + print(mesh.name, 'is already refit for', addon_target_body_name) if not refit: print('refitting:', mesh.name, 'to:', target_body_name) - lattice_modifier = mesh.modifiers.new(refitter_obj.name, 'LATTICE') - lattice_modifier.object = refitter_obj - applyRefitter(mesh) - return{'FINISHED'} - + lattice_modifier = mesh.modifiers.new(addon_obj.name, 'LATTICE') + lattice_modifier.object = addon_obj for collection in scene.collection.children: if collection.name == 'Refitters': @@ -289,58 +307,57 @@ def CP77Refit(context, refitter, target_body_path, target_body_name, fbx_rot): scene.collection.children.link(r_c) # Get the JSON file path for the selected target_body - with zipfile.ZipFile(target_body_path, "r") as z: - filename=z.namelist()[0] - print(filename) - with z.open(filename) as f: - data = f.read() - data = json.loads(data) + lattice_object_name, control_points, lattice_points, lattice_object_location, lattice_object_rotation, lattice_object_scale, lattice_interpolation_u, lattice_interpolation_v, lattice_interpolation_w = jsonload(target_body_path) + new_lattice = setup_lattice(r_c, fbx_rot, lattice_object_name, target_body_name, control_points, lattice_points, lattice_object_location, lattice_object_rotation, lattice_object_scale,lattice_interpolation_u, lattice_interpolation_v, lattice_interpolation_w) - if data: - control_points = data.get("deformed_control_points", []) + for mesh in selected_meshes: + lattice_modifier = mesh.modifiers.new(new_lattice.name,'LATTICE') + print(f'refitting: {mesh.name} to: {new_lattice["refitter_type"]}') + if useAddon: + lattice_object_name, control_points, lattice_points, lattice_object_location, lattice_object_rotation, lattice_object_scale, lattice_interpolation_u, lattice_interpolation_v, lattice_interpolation_w = jsonload(addon_target_body_path) + new_lattice = setup_lattice(r_c, fbx_rot, lattice_object_name, addon_target_body_name, control_points, lattice_points, lattice_object_location, lattice_object_rotation, lattice_object_scale,lattice_interpolation_u, lattice_interpolation_v, lattice_interpolation_w) + lattice_modifier.object = new_lattice + + applyRefitter(mesh) # Create a new lattice object - bpy.ops.object.add(type='LATTICE', enter_editmode=False, location=(0, 0, 0)) - new_lattice = bpy.context.object - new_lattice.name = data.get("lattice_object_name", "NewLattice") - new_lattice["refitter_type"] = target_body_name - lattice = new_lattice.data - r_c.objects.link(new_lattice) - bpy.context.collection.objects.unlink(new_lattice) - - # Set the dimensions of the lattice - lattice.points_u = data["lattice_points"][0] - lattice.points_v = data["lattice_points"][1] - lattice.points_w = data["lattice_points"][2] - new_lattice.location[0] = data["lattice_object_location"][0] - new_lattice.location[1] = data["lattice_object_location"][1] - new_lattice.location[2] = data["lattice_object_location"][2] - if fbx_rot: - # Rotate the Z-axis by 180 degrees (pi radians) - new_lattice.rotation_euler = (data["lattice_object_rotation"][0], data["lattice_object_rotation"][1], data["lattice_object_rotation"][2] + 3.14159) - else: - new_lattice.rotation_euler = (data["lattice_object_rotation"][0], data["lattice_object_rotation"][1], data["lattice_object_rotation"][2]) - new_lattice.scale[0] = data["lattice_object_scale"][0] - new_lattice.scale[1] = data["lattice_object_scale"][1] - new_lattice.scale[2] = data["lattice_object_scale"][2] - # Set interpolation types - lattice.interpolation_type_u = data.get("lattice_interpolation_u", 'KEY_BSPLINE') - lattice.interpolation_type_v = data.get("lattice_interpolation_v", 'KEY_BSPLINE') - lattice.interpolation_type_w = data.get("lattice_interpolation_w", 'KEY_BSPLINE') - - # Create a flat list of lattice points - lattice_points = lattice.points - flat_lattice_points = [lattice_points[w + v * lattice.points_u + u * lattice.points_u * lattice.points_v] for u in range(lattice.points_u) for v in range(lattice.points_v) for w in range(lattice.points_w)] - - for control_point, lattice_point in zip(control_points, flat_lattice_points): - lattice_point.co_deform = control_point - - if new_lattice: - bpy.context.object.hide_viewport = True +def setup_lattice(r_c, fbx_rot, lattice_object_name, target_body_name, control_points, lattice_points, lattice_object_location, lattice_object_rotation, lattice_object_scale,lattice_interpolation_u, lattice_interpolation_v, lattice_interpolation_w): + bpy.ops.object.add(type='LATTICE', enter_editmode=False, location=(0, 0, 0)) + new_lattice = bpy.context.object + new_lattice.name = lattice_object_name + new_lattice["refitter_type"] = target_body_name + lattice = new_lattice.data + r_c.objects.link(new_lattice) + bpy.context.collection.objects.unlink(new_lattice) + + # Set the dimensions of the lattice + lattice.points_u = lattice_points[0] + lattice.points_v = lattice_points[1] + lattice.points_w = lattice_points[2] + new_lattice.location[0] = lattice_object_location[0] + new_lattice.location[1] = lattice_object_location[1] + new_lattice.location[2] = lattice_object_location[2] + if fbx_rot: + # Rotate the Z-axis by 180 degrees (pi radians) + new_lattice.rotation_euler = (lattice_object_rotation[0], lattice_object_rotation[1],lattice_object_rotation[2] + 3.14159) + else: + new_lattice.rotation_euler = (lattice_object_rotation[0], lattice_object_rotation[1],lattice_object_rotation[2]) + new_lattice.scale[0] = lattice_object_scale[0] + new_lattice.scale[1] = lattice_object_scale[1] + new_lattice.scale[2] = lattice_object_scale[2] - for mesh in selected_meshes: - lattice_modifier = mesh.modifiers.new(new_lattice.name,'LATTICE') - print('refitting:', mesh.name, 'to:', new_lattice["refitter_type"]) - lattice_modifier.object = new_lattice - applyRefitter(mesh) + # Set interpolation types + lattice.interpolation_type_u = lattice_interpolation_u + lattice.interpolation_type_v = lattice_interpolation_v + lattice.interpolation_type_w = lattice_interpolation_w + + # Create a flat list of lattice points + flat_lattice_points = [lattice_points[w + v * lattice.points_u + u * lattice.points_u * lattice.points_v] for u in range(lattice.points_u) for v in range(lattice.points_v) for w in range(lattice.points_w)] + + for control_point, lattice_point in control_points, flat_lattice_points: + lattice_point.co_deform = control_point + + if new_lattice: + bpy.context.object.hide_viewport = True + return new_lattice \ No newline at end of file diff --git a/i_scene_cp77_gltf/meshtools/verttools.py b/i_scene_cp77_gltf/meshtools/verttools.py index 7f874f2..b23159e 100644 --- a/i_scene_cp77_gltf/meshtools/verttools.py +++ b/i_scene_cp77_gltf/meshtools/verttools.py @@ -86,7 +86,7 @@ def CP77GroupUngroupedVerts(self, context): print(e) return {'FINISHED'} -def trans_weights(self, context): +def trans_weights(self, context, vertInterop): current_mode = context.mode props = context.scene.cp77_panel_props source_mesh = bpy.data.collections.get(props.mesh_source) diff --git a/i_scene_cp77_gltf/resources/refitters/SoloArmsAddon.zip b/i_scene_cp77_gltf/resources/refitters/SoloArmsAddon.refitter.zip similarity index 100% rename from i_scene_cp77_gltf/resources/refitters/SoloArmsAddon.zip rename to i_scene_cp77_gltf/resources/refitters/SoloArmsAddon.refitter.zip diff --git a/i_scene_cp77_gltf/resources/refitters/adonis.zip b/i_scene_cp77_gltf/resources/refitters/adonis.refitter.zip similarity index 100% rename from i_scene_cp77_gltf/resources/refitters/adonis.zip rename to i_scene_cp77_gltf/resources/refitters/adonis.refitter.zip diff --git a/i_scene_cp77_gltf/resources/refitters/f2m.zip b/i_scene_cp77_gltf/resources/refitters/f2m.refitter.zip similarity index 100% rename from i_scene_cp77_gltf/resources/refitters/f2m.zip rename to i_scene_cp77_gltf/resources/refitters/f2m.refitter.zip diff --git a/i_scene_cp77_gltf/resources/refitters/f_normal_to_big_boobs.zip b/i_scene_cp77_gltf/resources/refitters/f_normal_to_big_boobs.refitter.zip similarity index 100% rename from i_scene_cp77_gltf/resources/refitters/f_normal_to_big_boobs.zip rename to i_scene_cp77_gltf/resources/refitters/f_normal_to_big_boobs.refitter.zip diff --git a/i_scene_cp77_gltf/resources/refitters/f_normal_to_small_boobs.zip b/i_scene_cp77_gltf/resources/refitters/f_normal_to_small_boobs.refitter.zip similarity index 100% rename from i_scene_cp77_gltf/resources/refitters/f_normal_to_small_boobs.zip rename to i_scene_cp77_gltf/resources/refitters/f_normal_to_small_boobs.refitter.zip diff --git a/i_scene_cp77_gltf/resources/refitters/flat_chest.zip b/i_scene_cp77_gltf/resources/refitters/flat_chest.refitter.zip similarity index 100% rename from i_scene_cp77_gltf/resources/refitters/flat_chest.zip rename to i_scene_cp77_gltf/resources/refitters/flat_chest.refitter.zip diff --git a/i_scene_cp77_gltf/resources/refitters/freyja.zip b/i_scene_cp77_gltf/resources/refitters/freyja.refitter.zip similarity index 100% rename from i_scene_cp77_gltf/resources/refitters/freyja.zip rename to i_scene_cp77_gltf/resources/refitters/freyja.refitter.zip diff --git a/i_scene_cp77_gltf/resources/refitters/gymfiend.zip b/i_scene_cp77_gltf/resources/refitters/gymfiend.refitter.zip similarity index 100% rename from i_scene_cp77_gltf/resources/refitters/gymfiend.zip rename to i_scene_cp77_gltf/resources/refitters/gymfiend.refitter.zip diff --git a/i_scene_cp77_gltf/resources/refitters/hyst_ebb.zip b/i_scene_cp77_gltf/resources/refitters/hyst_ebb.refitter.zip similarity index 100% rename from i_scene_cp77_gltf/resources/refitters/hyst_ebb.zip rename to i_scene_cp77_gltf/resources/refitters/hyst_ebb.refitter.zip diff --git a/i_scene_cp77_gltf/resources/refitters/hyst_ebb_rb.zip b/i_scene_cp77_gltf/resources/refitters/hyst_ebb_rb.refitter.zip similarity index 100% rename from i_scene_cp77_gltf/resources/refitters/hyst_ebb_rb.zip rename to i_scene_cp77_gltf/resources/refitters/hyst_ebb_rb.refitter.zip diff --git a/i_scene_cp77_gltf/resources/refitters/hyst_ebbp_addon.zip b/i_scene_cp77_gltf/resources/refitters/hyst_ebbp_addon.refitter.zip similarity index 100% rename from i_scene_cp77_gltf/resources/refitters/hyst_ebbp_addon.zip rename to i_scene_cp77_gltf/resources/refitters/hyst_ebbp_addon.refitter.zip diff --git a/i_scene_cp77_gltf/resources/refitters/hyst_rb.zip b/i_scene_cp77_gltf/resources/refitters/hyst_rb.refitter.zip similarity index 100% rename from i_scene_cp77_gltf/resources/refitters/hyst_rb.zip rename to i_scene_cp77_gltf/resources/refitters/hyst_rb.refitter.zip diff --git a/i_scene_cp77_gltf/resources/refitters/lush.zip b/i_scene_cp77_gltf/resources/refitters/lush.refitter.zip similarity index 100% rename from i_scene_cp77_gltf/resources/refitters/lush.zip rename to i_scene_cp77_gltf/resources/refitters/lush.refitter.zip diff --git a/i_scene_cp77_gltf/resources/refitters/m2f.zip b/i_scene_cp77_gltf/resources/refitters/m2f.refitter.zip similarity index 100% rename from i_scene_cp77_gltf/resources/refitters/m2f.zip rename to i_scene_cp77_gltf/resources/refitters/m2f.refitter.zip diff --git a/i_scene_cp77_gltf/resources/refitters/solo_ultimate.refitter.zip b/i_scene_cp77_gltf/resources/refitters/solo_ultimate.refitter.zip new file mode 100644 index 0000000..09cc37b Binary files /dev/null and b/i_scene_cp77_gltf/resources/refitters/solo_ultimate.refitter.zip differ diff --git a/i_scene_cp77_gltf/resources/refitters/solo_ultimate.zip b/i_scene_cp77_gltf/resources/refitters/solo_ultimate.zip deleted file mode 100644 index cda3212..0000000 Binary files a/i_scene_cp77_gltf/resources/refitters/solo_ultimate.zip and /dev/null differ diff --git a/i_scene_cp77_gltf/resources/rigs/songbird_full.glb b/i_scene_cp77_gltf/resources/rigs/songbird_full.glb new file mode 100644 index 0000000..e44118d Binary files /dev/null and b/i_scene_cp77_gltf/resources/rigs/songbird_full.glb differ diff --git a/i_scene_cp77_gltf/resources/scripts/add_trackKeys.py b/i_scene_cp77_gltf/resources/scripts/add_trackKeys.py new file mode 100644 index 0000000..60ce564 --- /dev/null +++ b/i_scene_cp77_gltf/resources/scripts/add_trackKeys.py @@ -0,0 +1,58 @@ +import bpy +import idprop.types + +def convert_idproperty(prop): + """Convert Blender IDProperty to a standard Python data structure.""" + if isinstance(prop, idprop.types.IDPropertyArray): + return list(prop) + elif isinstance(prop, (list, tuple)): + return [convert_idproperty(item) for item in prop] + elif isinstance(prop, dict): + return {key: convert_idproperty(value) for key, value in prop.items()} + else: + return prop + +def extract_track_keys(track_keys): + """Extract and print the actual data from the trackKeys property.""" + if isinstance(track_keys, (list, tuple)): + return [extract_track_keys(item) for item in track_keys] + elif hasattr(track_keys, "to_dict"): + return track_keys.to_dict() + else: + return track_keys + +def add_to_track_keys(track_keys, new_data): + """Add new data to the trackKeys property.""" + # Convert IDPropertyArray to a regular Python list + if isinstance(track_keys, idprop.types.IDPropertyArray): + track_keys = list(track_keys) + elif isinstance(track_keys, list): + track_keys = track_keys + + # Add the new data as a dictionary directly, not as a list + track_keys.append(new_data) + + return track_keys + +def modify_track_keys(): + for action in bpy.data.actions: + if "constTrackKeys" in action.keys(): ## works for constTrackKeys or trackKeys + track_keys = action["constTrackKeys"] + + # Convert to standard Python structure for manipulation + converted_track_keys = convert_idproperty(track_keys) + print(f"Original trackKeys: {converted_track_keys}") + + # Add new data (as a dictionary, not a list) + new_data = {"trackIndex": 7, "time": 0.050059766, "value": 0.00} ##populate this with the data + updated_track_keys = add_to_track_keys(converted_track_keys, new_data) + + # Reassign the modified trackKeys to the action + action["constTrackKeys"] = updated_track_keys + + # Print the updated trackKeys + updated_track_keys = convert_idproperty(action["constTrackKeys"]) + print(f"Updated trackKeys: {updated_track_keys}") + +# Call the function to modify the trackKeys property +modify_track_keys()