diff --git a/README.md b/README.md index be9faf2..41111b3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Blender-ALAMAO-Plugin +# Blender-ALAMO-Plugin A plugin that allow reading and writing of ALAMO-Engine model(.alo) and animation(.ala) files. Specifically designed to work with Empire at War: Forces of Corruption. @@ -50,26 +50,36 @@ However it also means that an exported shadow mesh should not be able to cause a ## Sidebar The sidebar(default hotkey: 'N') offers an ALAMO properties option. -This lists the file format specific properties of the active object. -The avaible properties change depending on the mode and object type. +This lists the file format specific properties of the active object. -Scene propierties(always avaible): - - ActiveSkeleton: Files can only contain a single skeleton. Choose the skeleton that is used when exporting - - AnimationEndFrame: per action, length of the animation +Validate: + - Check for any problems that would cause an export to fail. -Object properties(Mesh in object-mode): +Object tools (Mesh in object-mode): - HasCollision: treated as collider ingame - Hidden: invisible ingame + +Armature Settings (always avaible): + - ActiveSkeleton: Files can only contain a single skeleton. Choose the skeleton that is used when exporting -Bone properties (Bone in edit-mode): +Bone Tools (Bone in edit-mode): + - billboardMode: Sets the billboard mode. Can only be set on individually-selected bones. - Visible: Visibility of attached object - EnableProxy: bone is a proxy to spawn effects ingame, enables additional options: - proxyIsHidden: flag that determines if the proxy is initially hidden - altDecreaseStayHidden: prevents proxies to become visible when the alt level decreases - - ProxyName: name of the effect to be attached to the proxy + - ProxyName: name of the effect to be attached to the proxy. Can only be set on individually-selected bones. Bone properties (Bone in pose-mode): - - proxyIsHiddenAnimation: animated visibility of the proxy, when hovering over it with mouse: press 'I' to set keyframe + - proxyIsHiddenAnimation: animated visibility of the proxy + - Action End Frames: per action, length of the animation + + Debug: + - Unmodified original UI + + ### Gotchas + - Any clicks on the sidebar, but not on an active control, will be treated as a click in the 3d view. This can cause you to lose your selection. + - On validation, errors only pop up briefly, and warnings don't pop up at all. Recommend opening an Info panel when validating. ## Alamo material properties diff --git a/io_alamo_tools/UI.py b/io_alamo_tools/UI.py new file mode 100644 index 0000000..dd193c2 --- /dev/null +++ b/io_alamo_tools/UI.py @@ -0,0 +1,622 @@ +from bpy.props import ( + StringProperty, + BoolProperty, + IntProperty, + EnumProperty, + PointerProperty, +) +from . import validation +from . import utils +import mathutils +import bpy + + +# UI Utilities #################################################################################### +def CheckObjectType(objects, target_type): + for obj in objects: + if obj.type != target_type: + return False + return True + + +def ShouldEnable(objects): + if objects is None or len(objects) <= 0: + return False + if bpy.context.mode == "OBJECT": + objects_same = CheckObjectType(objects, "MESH") + if not objects_same: + return False + return True + + +def CheckPropAllSame(objects, prop): + # True: All same, have value of True + # False: All same, have value of False + # None: Not all same + if objects is None or len(objects) <= 0: + return None + first_value = None + for obj in objects: + if first_value is None: + first_value = getattr(obj, prop) + elif getattr(obj, prop) != first_value: + return None + return first_value + + +def check_anim_prop_all_same(bones, prop): + """Like check_prop_all_same(), but for animation""" + + all_same = [] + + if bones is not None and len(bones) > 0: + all_same = list(set(getattr(bone, prop) for bone in bones)) + + if len(all_same) == 1: + return all_same[0] + + return None + + +def threebox(layout, all_same, operator, label): + icon = "ERROR" + if all_same is None: + icon = "LAYER_ACTIVE" + if all_same is True: + icon = "BLANK1" + if all_same is False: + icon = "CHECKMARK" + + row = layout.row() + row.operator(operator, text="", icon=icon) + row.label(text=label) + +def setProp(all_same, objects, prop): + set_to = False + if all_same in (None, False): + set_to = True + + for obj in objects: + setattr(obj, prop, set_to) + + +def proxy_name_update(self, context): + if self.ProxyName != self.ProxyName.upper(): # prevents endless recursion + self.ProxyName = self.ProxyName.upper() + + +def skeletonEnumCallback(scene, context): + armatures = [("None", "None", "", "", 0)] + counter = 1 + for arm in bpy.data.objects: # test if armature exists + if arm.type == "ARMATURE": + armatures.append((arm.name, arm.name, "", "", counter)) + counter += 1 + + return armatures + + +# Operators ####################################################################################### +class keyframeProxySet(bpy.types.Operator): + bl_idname = "alamo.set_keyframe_proxy" + bl_label = "" + bl_description = "Create a keyframe and set proxyIsHiddenAnimation for all selected bones" + + @classmethod + def poll(cls, context): + bones = bpy.context.selected_pose_bones + return bones is not None and len(bones) > 0 + + def execute(self, context): + bones = bpy.context.selected_pose_bones + + all_same = check_anim_prop_all_same(bones, "proxyIsHiddenAnimation") + operation = "SHOW" if all_same is True else "HIDE" + + for bone in list(bones): + if operation == "SHOW": + bone.proxyIsHiddenAnimation = False + if operation == "HIDE": + bone.proxyIsHiddenAnimation = True + + bone.keyframe_insert(data_path="proxyIsHiddenAnimation", group=bone.name) + + for area in bpy.context.screen.areas: + if area.type == "DOPESHEET_EDITOR": + area.tag_redraw() + + return {"FINISHED"} + + +class keyframeProxyDelete(bpy.types.Operator): + bl_idname = "alamo.delete_keyframe_proxy" + bl_label = "" + bl_description = "Delete proxyIsHiddenAnimation keyframes for all selected bones" + + @classmethod + def poll(cls, context): + bones = bpy.context.selected_pose_bones + action = utils.getCurrentAction() + frame = bpy.context.scene.frame_current + + if bones is None or len(bones) <= 0 or action is None: + return False + + for bone in list(bones): + keyframes = action.fcurves.find( + bone.path_from_id() + ".proxyIsHiddenAnimation" + ) + if keyframes is not None: + for keyframe in keyframes.keyframe_points: + if int(keyframe.co[0]) == frame: + return True + + return False + + def execute(self, context): + bones = list(bpy.context.selected_pose_bones) + for bone in bones: + bone.keyframe_delete(data_path="proxyIsHiddenAnimation") + + for area in bpy.context.screen.areas: + if area.type in ("DOPESHEET_EDITOR", "VIEW_3D"): + area.tag_redraw() + + return {"FINISHED"} + + +class ValidateFileButton(bpy.types.Operator): + bl_idname = "alamo.validate_file" + bl_label = "Validate" + + def execute(self, context): + mesh_list = validation.create_export_list( + bpy.context.scene.collection, True, "DATA" + ) + + # check if export objects satisfy requirements (has material, UVs, ...) + messages = validation.validate(mesh_list) + + if messages is not None and len(messages) > 0: + for message in messages: + self.report(*message) + else: + self.report({"INFO"}, "ALAMO - Validation complete. No errors detected!") + return {"FINISHED"} + + +# Legacy version, included for the debug panel +class createConstraintBoneButton(bpy.types.Operator): + bl_idname = "create.constraint_bone" + bl_label = "Create constraint bone" + + def execute(self, context): + obj = bpy.context.view_layer.objects.active + armature = utils.findArmature() + + bpy.context.view_layer.objects.active = armature + utils.setModeToEdit() + + bone = armature.data.edit_bones.new(obj.name) + bone.tail = bone.head + mathutils.Vector((0.0, 0.0, 1.0)) + bone.matrix = obj.matrix_world + obj.location = mathutils.Vector((0.0, 0.0, 0.0)) + obj.rotation_euler = mathutils.Euler((0.0, 0.0, 0.0), "XYZ") + constraint = obj.constraints.new("CHILD_OF") + constraint.target = armature + constraint.subtarget = bone.name + + utils.setModeToObject() + bpy.context.view_layer.objects.active = obj + return {"FINISHED"} + + +class CreateConstraintBone(bpy.types.Operator): + bl_idname = "alamo.create_constraint_bone" + bl_label = "Create Constraint Bone" + bl_description = "Adds a bone and parents the active object to it" + + @classmethod + def poll(cls, context): + obj = bpy.context.object + if ( + obj is not None + and obj.type == "MESH" + and bpy.context.mode == "OBJECT" + ): + armature = utils.findArmature() + if armature is not None: + has_child_constraint = False + for constraint in obj.constraints: + if constraint.type == "CHILD_OF": + has_child_constraint = True + if not has_child_constraint: + return True + return False + + def execute(self, context): + obj = bpy.context.view_layer.objects.active + armature = utils.findArmature() + + bpy.context.view_layer.objects.active = armature + utils.setModeToEdit() + + bone = armature.data.edit_bones.new(obj.name) + bone.tail = bone.head + mathutils.Vector((0.0, 0.0, 1.0)) + bone.matrix = obj.matrix_world + obj.location = mathutils.Vector((0.0, 0.0, 0.0)) + obj.rotation_euler = mathutils.Euler((0.0, 0.0, 0.0), "XYZ") + constraint = obj.constraints.new("CHILD_OF") + constraint.target = armature + constraint.subtarget = bone.name + + utils.setModeToObject() + bpy.context.view_layer.objects.active = obj + return {"FINISHED"} + + +class CopyProxyNameToSelected(bpy.types.Operator): + bl_idname = "alamo.copy_proxy_name" + bl_label = "" + bl_description = "Copy Proxy Name to Selected Bones" + + @classmethod + def poll(cls, context): + return ( + bpy.context.selected_bones is not None + and len(bpy.context.selected_bones) > 1 + ) + + def execute(self, context): + bones = list(bpy.context.selected_bones) + name = bones[0].ProxyName + for bone in bones: + bone.ProxyName = name + return {"FINISHED"} + + +class skeletonEnumClass(bpy.types.PropertyGroup): + skeletonEnum: EnumProperty( + name="Active Skeleton", + description="skeleton that is exported", + items=skeletonEnumCallback, + ) + + +# Panels ########################################################################################## +class ALAMO_PT_SettingsPanel(bpy.types.Panel): + + bl_label = "Settings" + bl_idname = "ALAMO_PT_SettingsPanel" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "ALAMO" + + def draw(self, context): + layout = self.layout + + row = layout.row() + row.operator("alamo.validate_file") + row.scale_y = 3.0 + + layout.separator() + + col = layout.column(align=True) + col.label(text="Active Skeleton") + row = col.row() + row.scale_y = 1.25 + row.prop(context.scene.ActiveSkeleton, "skeletonEnum", text="") + + +class ALAMO_PT_InfoPanel(bpy.types.Panel): + + bl_label = "Info" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "ALAMO" + + def draw(self, context): + col = self.layout.column(align=True) + col.label(text="Hold ALT when clicking a checkbox") + col.label(text="to apply it to all selected objects/bones.") + + col = self.layout.column(align=True) + col.label(text="Buttons in Animation are NOT checkboxes.") + + +class ALAMO_PT_ObjectPanel(bpy.types.Panel): + + bl_label = "Object" + bl_context = "objectmode" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "ALAMO" + + def draw(self, context): + layout = self.layout + + obj = context.object + if obj is not None and obj.type == 'MESH': + col = layout.column() + col.prop(obj, "HasCollision", text="Collision") + col.prop(obj, "Hidden") + col.scale_y = 1.25 + + layout.separator() + + col = layout.column() + col.scale_y = 1.25 + col.operator("alamo.create_constraint_bone") + + +class ALAMO_PT_EditBonePanel(bpy.types.Panel): + + bl_label = "Bone" + bl_context = "armature_edit" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "ALAMO" + + def draw(self, context): + bones = bpy.context.selected_bones + layout = self.layout + + layout.active = False + if bpy.context.mode == "EDIT_ARMATURE": + layout.active = True + + col = layout.column(align=True) + col.label(text="Billboard Mode") + row = col.row() + row.scale_y = 1.25 + row.enabled = False + if bones is not None: + if len(bones) == 1: + row.prop(list(bones)[0].billboardMode, "billboardMode", text="") + row.enabled = True + else: + row.label(text="Select a single bone") + else: + row.label(text="-") + + layout.separator() + + col = layout.column() + col.scale_y = 1.25 + col.active = False + if bpy.context.active_bone is not None: + col.active = True + col.prop(bpy.context.active_bone, "Visible") + col.prop(bpy.context.active_bone, "EnableProxy", text="Enable Proxy") + + +class ALAMO_PT_EditBoneSubPanel(bpy.types.Panel): + + bl_label = "" + bl_parent_id = "ALAMO_PT_EditBonePanel" + bl_context = "armature_edit" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "ALAMO" + bl_options = {"HIDE_HEADER"} + + def draw(self, context): + bone = bpy.context.active_bone + bones = bpy.context.selected_bones + layout = self.layout + + all_same = CheckPropAllSame(bones, "EnableProxy") + + layout.active = False + if all_same is not None: + layout.active = all_same + + if all_same is None and len(bones) > 0: + layout.label(icon="ERROR", text="Inconsistent EnableProxy states") + + col = layout.column() + col.scale_y = 1.25 + col.active = False + if bone is not None and bone.EnableProxy: + col.active = True + col.prop(bone, "proxyIsHidden", text="Proxy Visible", invert_checkbox=True) + col.prop(bone, "altDecreaseStayHidden") + + col = layout.column(align=True) + col.label(text="Proxy Name") + row = col.row(align=True) + row.scale_y = 1.25 + row.enabled = False + if bones is not None and len(bones) > 0 and all_same: + row.prop(list(bones)[0], "ProxyName", text="") + row.operator("alamo.copy_proxy_name", text="", icon="DUPLICATE") + row.enabled = True + else: + row.label(text="ProxyName") + + +class ALAMO_PT_AnimationPanel(bpy.types.Panel): + + bl_label = "Animation" + bl_context = "posemode" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "ALAMO" + + def draw(self, context): + layout = self.layout + + threebox( + layout, + check_anim_prop_all_same( + bpy.context.selected_pose_bones, "proxyIsHiddenAnimation" + ), + "alamo.set_keyframe_proxy", + "Keyframe Proxy Visible", + ) + + row = layout.row() + row.operator("alamo.delete_keyframe_proxy", text="", icon="X") + row.label(text="Delete Keyframes") + + # col = layout.column() + # col.scale_y = 1.25 + # col.prop(bpy.context.active_pose_bone, "proxyIsHiddenAnimation", text="Proxy Visible", invert_checkbox=True) + + +class ALAMO_PT_AnimationActionSubPanel(bpy.types.Panel): + + bl_label = "Action End Frames" + bl_parent_id = "ALAMO_PT_AnimationPanel" + bl_context = "posemode" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "ALAMO" + bl_options = {"HIDE_HEADER"} + + def draw(self, context): + layout = self.layout.column(align=True) + layout.label(text="Action End Frames") + for action in bpy.data.actions: + layout.prop(action, "AnimationEndFrame", text=action.name) + + +class ALAMO_PT_DebugPanel(bpy.types.Panel): + + bl_label = "Debug" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "ALAMO" + bl_options = {"DEFAULT_CLOSED"} + + def draw(self, context): + object = context.object + layout = self.layout + scene = context.scene + c = layout.column() + + c.prop(scene.ActiveSkeleton, "skeletonEnum") + + if type(object) != type(None): + if object.type == "MESH": + if bpy.context.mode == "OBJECT": + c.prop(object, "HasCollision") + c.prop(object, "Hidden") + + armature = utils.findArmature() + if armature != None: + has_child_constraint = False + for constraint in object.constraints: + if constraint.type == "CHILD_OF": + has_child_constraint = True + if not has_child_constraint: + self.layout.operator( + "create.constraint_bone", text="Create Constraint Bone" + ) + + action = utils.getCurrentAction() + if action != None: + c.prop(action, "AnimationEndFrame") + + bone = bpy.context.active_bone + if type(bone) != type(None): + if type(bpy.context.active_bone) is bpy.types.EditBone: + c.prop(bone.billboardMode, "billboardMode") + c.prop(bone, "Visible") + c.prop(bone, "EnableProxy") + if bone.EnableProxy: + c.prop(bone, "proxyIsHidden") + c.prop(bone, "altDecreaseStayHidden") + c.prop(bone, "ProxyName") + + elif ( + type(bpy.context.active_bone) is bpy.types.Bone + and bpy.context.mode == "POSE" + ): + poseBone = object.pose.bones[bone.name] + c.prop(poseBone, "proxyIsHiddenAnimation") + + +class billboardListProperties(bpy.types.PropertyGroup): + mode_options = [ + ("Disable", "Disable", "Description WIP", "", 0), + ("Parallel", "Parallel", "Description WIP", "", 1), + ("Face", "Face", "Description WIP", "", 2), + ("ZAxis View", "ZAxis View", "Description WIP", "", 3), + ("ZAxis Light", "ZAxis Light", "Description WIP", "", 4), + ("ZAxis Wind", "ZAxis Wind", "Description WIP", "", 5), + ("Sunlight Glow", "Sunlight Glow", "Description WIP", "", 6), + ("Sun", "Sun", "Description WIP", "", 7), + ] + + billboardMode: bpy.props.EnumProperty( + items=mode_options, + description="billboardMode", + default="Disable", + ) + + +# Registration #################################################################################### +classes = ( + skeletonEnumClass, + billboardListProperties, + ValidateFileButton, + CreateConstraintBone, + createConstraintBoneButton, + CopyProxyNameToSelected, + keyframeProxySet, + keyframeProxyDelete, + ALAMO_PT_SettingsPanel, + ALAMO_PT_InfoPanel, + ALAMO_PT_ObjectPanel, + ALAMO_PT_EditBonePanel, + ALAMO_PT_EditBoneSubPanel, + ALAMO_PT_AnimationPanel, + ALAMO_PT_AnimationActionSubPanel, + ALAMO_PT_DebugPanel, +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bpy.types.Scene.ActiveSkeleton = PointerProperty(type=skeletonEnumClass) + bpy.types.Scene.modelFileName = StringProperty(name="") + + bpy.types.Action.AnimationEndFrame = IntProperty(default=24) + + bpy.types.EditBone.Visible = BoolProperty(default=True) + bpy.types.EditBone.EnableProxy = BoolProperty() + bpy.types.EditBone.proxyIsHidden = BoolProperty() + bpy.types.PoseBone.proxyIsHiddenAnimation = BoolProperty() + bpy.types.EditBone.altDecreaseStayHidden = BoolProperty() + bpy.types.EditBone.ProxyName = StringProperty(update=proxy_name_update) + bpy.types.EditBone.billboardMode = bpy.props.PointerProperty(type=billboardListProperties) + + bpy.types.Object.HasCollision = BoolProperty() + bpy.types.Object.Hidden = BoolProperty() + + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + bpy.types.Scene.ActiveSkeleton + + bpy.types.Action.AnimationEndFrame + + bpy.types.EditBone.Visible + bpy.types.EditBone.EnableProxy + bpy.types.EditBone.proxyIsHidden + bpy.types.PoseBone.proxyIsHiddenAnimation + bpy.types.EditBone.altDecreaseStayHidden + bpy.types.EditBone.ProxyName + bpy.types.EditBone.billboardMode + + bpy.types.Object.HasCollision + bpy.types.Object.Hidden + + +if __name__ == "__main__": + register() diff --git a/io_alamo_tools/UI_material.py b/io_alamo_tools/UI_material.py new file mode 100644 index 0000000..7142566 --- /dev/null +++ b/io_alamo_tools/UI_material.py @@ -0,0 +1,252 @@ +import bpy +from . import settings + + +class ALAMO_PT_materialPropertyPanel(bpy.types.Panel): + bl_label = "Alamo Shader Properties" + bl_id = "ALAMO_PT_materialPropertyPanel" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "material" + + def draw(self, context): + object = context.object + layout = self.layout + col = layout.column() + + # if type(object) != type(None) and object.type == "MESH": + if type(object) != type(None) and object.type == "MESH": + material = bpy.context.active_object.active_material + if material is not None: + # a None image is needed to represent not using a texture + if "None" not in bpy.data.images: + bpy.data.images.new(name="None", width=1, height=1) + col.prop(material.shaderList, "shaderList") + if material.shaderList.shaderList != "alDefault.fx": + shader_props = settings.material_parameter_dict[ + material.shaderList.shaderList + ] + for shader_prop in shader_props: + # because contains() doesn't exist, apparently + if shader_prop.find("Texture") > -1: + layout.prop_search( + material, shader_prop, bpy.data, "images" + ) + # layout.template_ID(material, shader_prop, new="image.new", open="image.open") + + +class ALAMO_PT_materialPropertySubPanel(bpy.types.Panel): + bl_label = "Additional Properties" + bl_parent_id = "ALAMO_PT_materialPropertyPanel" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "material" + bl_options = {"DEFAULT_CLOSED"} + + def draw(self, context): + obj = context.object + layout = self.layout + col = layout.column() + + if obj is not None and obj.type == "MESH": + material = bpy.context.active_object.active_material + if ( + material is not None + and material.shaderList.shaderList != "alDefault.fx" + ): + shader_props = settings.material_parameter_dict[ + material.shaderList.shaderList + ] + for shader_prop in shader_props: + # because contains() doesn't exist, apparently + if shader_prop.find("Texture") == -1: + col.prop(material, shader_prop) + + +class shaderListProperties(bpy.types.PropertyGroup): + mode_options = [ + (shader_name, shader_name, "", "", index) + for index, shader_name in enumerate(settings.material_parameter_dict) + ] + + shaderList: bpy.props.EnumProperty( + items=mode_options, + description="Choose ingame Shader", + default="alDefault.fx", + ) + + +# Registration #################################################################################### +classes = ( + shaderListProperties, + ALAMO_PT_materialPropertyPanel, + ALAMO_PT_materialPropertySubPanel, +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bpy.types.Material.BaseTexture = bpy.props.StringProperty(default="None") + bpy.types.Material.DetailTexture = bpy.props.StringProperty(default="None") + bpy.types.Material.NormalDetailTexture = bpy.props.StringProperty(default="None") + bpy.types.Material.NormalTexture = bpy.props.StringProperty(default="None") + bpy.types.Material.GlossTexture = bpy.props.StringProperty(default="None") + bpy.types.Material.WaveTexture = bpy.props.StringProperty(default="None") + bpy.types.Material.DistortionTexture = bpy.props.StringProperty(default="None") + bpy.types.Material.CloudTexture = bpy.props.StringProperty(default="None") + bpy.types.Material.CloudNormalTexture = bpy.props.StringProperty(default="None") + + bpy.types.Material.shaderList = bpy.props.PointerProperty(type=shaderListProperties) + bpy.types.Material.Emissive = bpy.props.FloatVectorProperty( + min=0.0, max=1.0, size=4, default=(0.0, 0.0, 0.0, 0.0) + ) + bpy.types.Material.Diffuse = bpy.props.FloatVectorProperty( + min=0.0, max=1.0, size=4, default=(1.0, 1.0, 1.0, 0.0) + ) + bpy.types.Material.Specular = bpy.props.FloatVectorProperty( + min=0.0, max=1.0, size=4, default=(1.0, 1.0, 1.0, 0.0) + ) + bpy.types.Material.Shininess = bpy.props.FloatProperty( + min=0.0, max=255.0, default=32.0 + ) + bpy.types.Material.Colorization = bpy.props.FloatVectorProperty( + min=0.0, max=1.0, size=4, default=(1.0, 1.0, 1.0, 0.0) + ) + bpy.types.Material.DebugColor = bpy.props.FloatVectorProperty( + min=0.0, max=1.0, size=4, default=(0.0, 1.0, 0.0, 0.0) + ) + bpy.types.Material.UVOffset = bpy.props.FloatVectorProperty( + min=0.0, max=1.0, size=4, default=(0.0, 0.0, 0.0, 0.0) + ) + bpy.types.Material.Color = bpy.props.FloatVectorProperty( + min=0.0, max=1.0, size=4, default=(1.0, 1.0, 1.0, 1.0) + ) + bpy.types.Material.UVScrollRate = bpy.props.FloatVectorProperty( + min=0.0, max=1.0, size=4, default=(0.0, 0.0, 0.0, 0.0) + ) + bpy.types.Material.DiffuseColor = bpy.props.FloatVectorProperty( + min=0.0, max=1.0, size=3, default=(0.5, 0.5, 0.5) + ) + # shield shader properties + bpy.types.Material.EdgeBrightness = bpy.props.FloatProperty( + min=0.0, max=255.0, default=0.5 + ) + bpy.types.Material.BaseUVScale = bpy.props.FloatProperty( + min=-255.0, max=255.0, default=1.0 + ) + bpy.types.Material.WaveUVScale = bpy.props.FloatProperty( + min=-255.0, max=255.0, default=1.0 + ) + bpy.types.Material.DistortUVScale = bpy.props.FloatProperty( + min=-255.0, max=255.0, default=1.0 + ) + bpy.types.Material.BaseUVScrollRate = bpy.props.FloatProperty( + min=-255.0, max=255.0, default=-0.15 + ) + bpy.types.Material.WaveUVScrollRate = bpy.props.FloatProperty( + min=-255.0, max=255.0, default=-0.15 + ) + bpy.types.Material.DistortUVScrollRate = bpy.props.FloatProperty( + min=-255.0, max=255.0, default=-0.25 + ) + # tree properties + bpy.types.Material.BendScale = bpy.props.FloatProperty( + min=-255.0, max=255.0, default=0.4 + ) + # grass properties + bpy.types.Material.Diffuse1 = bpy.props.FloatVectorProperty( + min=0.0, max=1.0, size=4, default=(1.0, 1.0, 1.0, 1.0) + ) + # skydome.fx properties + bpy.types.Material.CloudScrollRate = bpy.props.FloatProperty( + min=-255.0, max=255.0, default=0.001 + ) + bpy.types.Material.CloudScale = bpy.props.FloatProperty( + min=-255.0, max=255.0, default=1.0 + ) + # nebula.fx properties + bpy.types.Material.SFreq = bpy.props.FloatProperty( + min=-255.0, max=255.0, default=0.002 + ) + bpy.types.Material.TFreq = bpy.props.FloatProperty( + min=-255.0, max=255.0, default=0.005 + ) + bpy.types.Material.DistortionScale = bpy.props.FloatProperty( + min=-255.0, max=255.0, default=1.0 + ) + # planet.fx properties + bpy.types.Material.Atmosphere = bpy.props.FloatVectorProperty( + min=0.0, max=1.0, size=4, default=(0.5, 0.5, 0.5, 0.5) + ) + bpy.types.Material.CityColor = bpy.props.FloatVectorProperty( + min=0.0, max=1.0, size=4, default=(0.5, 0.5, 0.5, 0.5) + ) + bpy.types.Material.AtmospherePower = bpy.props.FloatProperty( + min=-255.0, max=255.0, default=1.0 + ) + # tryplanar mapping properties + bpy.types.Material.MappingScale = bpy.props.FloatProperty( + min=0.0, max=255.0, default=0.1 + ) + bpy.types.Material.BlendSharpness = bpy.props.FloatProperty( + min=0.0, max=255.0, default=0.1 + ) + + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + bpy.types.Material.BaseTexture + bpy.types.Material.DetailTexture + bpy.types.Material.NormalTexture + bpy.types.Material.NormalDetailTexture + bpy.types.Material.GlossTexture + bpy.types.Material.WaveTexture + bpy.types.Material.DistortionTexture + bpy.types.Material.CloudTexture + bpy.types.Material.CloudNormalTexture + + bpy.types.Material.shaderList + bpy.types.Material.Emissive + bpy.types.Material.Diffuse + bpy.types.Material.Specular + bpy.types.Material.Shininess + bpy.types.Material.Colorization + bpy.types.Material.DebugColor + bpy.types.Material.UVOffset + bpy.types.Material.Color + bpy.types.Material.UVScrollRate + bpy.types.Material.DiffuseColor + # shield shader properties + bpy.types.Material.EdgeBrightness + bpy.types.Material.BaseUVScale + bpy.types.Material.WaveUVScale + bpy.types.Material.DistortUVScale + bpy.types.Material.BaseUVScrollRate + bpy.types.Material.WaveUVScrollRate + bpy.types.Material.DistortUVScrollRate + # tree properties + bpy.types.Material.BendScale + # grass properties + bpy.types.Material.Diffuse1 + # skydome.fx properties + bpy.types.Material.CloudScrollRate + bpy.types.Material.CloudScale + # nebula.fx properties + bpy.types.Material.SFreq + bpy.types.Material.TFreq + bpy.types.Material.DistortionScale + # planet.fx properties + bpy.types.Material.Atmosphere + bpy.types.Material.CityColor + bpy.types.Material.AtmospherePower + # tryplanar mapping properties + bpy.types.Material.MappingScale + bpy.types.Material.BlendSharpness + + +if __name__ == "__main__": + register() diff --git a/io_alamo_tools/__init__.py b/io_alamo_tools/__init__.py index 0fd34f1..52c0fa6 100644 --- a/io_alamo_tools/__init__.py +++ b/io_alamo_tools/__init__.py @@ -1,30 +1,7 @@ -bl_info = { - "name": "ALAMO Tools", - "author": "Gaukler", - "version": (0, 0, 1, 0), - "blender": (2, 82, 0), - "category": "Import-Export" -} - -if "bpy" in locals(): - import importlib - importlib.reload(import_alo) - importlib.reload(import_ala) - importlib.reload(export_alo) - importlib.reload(export_ala) - importlib.reload(settings) - importlib.reload(utils) -else: - from . import import_alo - from . import import_ala - from . import export_alo - from . import export_ala - from . import settings - from . import utils - -import bpy -import mathutils -from bpy.props import * +from bpy.types import (Panel, + Operator, + PropertyGroup, + ) from bpy.props import (StringProperty, BoolProperty, IntProperty, @@ -32,354 +9,96 @@ EnumProperty, PointerProperty, ) -from bpy.types import (Panel, - Operator, - PropertyGroup, - ) - -class createConstraintBoneButton(bpy.types.Operator): - bl_idname = "create.constraint_bone" - bl_label = "Create constraint bone" - - def execute(self, context): - object = bpy.context.view_layer.objects.active - armature = utils.findArmature() - - bpy.context.view_layer.objects.active = armature - utils.setModeToEdit() - - bone = armature.data.edit_bones.new(object.name) - bone.tail = bone.head + mathutils.Vector((0, 0, 1)) - bone.matrix = object.matrix_world - object.location = mathutils.Vector((0.0, 0.0, 0.0)) - object.rotation_euler = mathutils.Euler((0.0, 0.0, 0.0), 'XYZ') - constraint = object.constraints.new('CHILD_OF') - constraint.target = armature - constraint.subtarget = bone.name - - utils.setModeToObject() - bpy.context.view_layer.objects.active = object - return {'FINISHED'} - -def skeletonEnumCallback(scene, context): - armatures = [('None', 'None', '', '', 0)] - counter = 1 - for arm in bpy.data.objects: # test if armature exists - if arm.type == 'ARMATURE': - armatures.append((arm.name, arm.name, '', '', counter)) - counter += 1 - - return armatures - -class skeletonEnumClass(PropertyGroup): - skeletonEnum : EnumProperty( - name='Active Skeleton', - description = "skeleton that is exported", - items = skeletonEnumCallback - ) - -class ALAMO_PT_ToolsPanel(bpy.types.Panel): - - bl_label = "ALAMO properties" - bl_space_type = 'VIEW_3D' - bl_region_type = 'UI' - bl_category = "ALAMO" - - def draw(self, context): - object = context.object - layout = self.layout - scene = context.scene - c = layout.column() - - c.prop(scene.ActiveSkeleton, 'skeletonEnum') - - if type(object) != type(None): - if(object.type == 'MESH'): - if bpy.context.mode == 'OBJECT': - c.prop(object, "HasCollision") - c.prop(object, "Hidden") - - armature = utils.findArmature() - if armature != None: - hasChildConstraint = False - for constraint in object.constraints: - if constraint.type == 'CHILD_OF': - hasChildConstraint = True - if not hasChildConstraint: - self.layout.operator("create.constraint_bone", text = 'Create Constraint Bone') - - action = utils.getCurrentAction() - if(action != None): - c.prop(action, "AnimationEndFrame") - - - bone = bpy.context.active_bone - if type(bone) != type(None): - if(type(bpy.context.active_bone ) is bpy.types.EditBone): - c.prop(bone.billboardMode, "billboardMode") - c.prop(bone, "Visible") - c.prop(bone, "EnableProxy") - if bone.EnableProxy: - c.prop(bone, "proxyIsHidden") - c.prop(bone, "altDecreaseStayHidden") - c.prop(bone, "ProxyName") - - elif (type(bpy.context.active_bone) is bpy.types.Bone and bpy.context.mode == 'POSE'): - poseBone = object.pose.bones[bone.name] - c.prop(poseBone, "proxyIsHiddenAnimation") - -class ALAMO_PT_materialPropertyPanel(bpy.types.Panel): - bl_label = "Alamo material properties" - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "material" - - def draw(self, context): - object = context.object - layout = self.layout - c = layout.column() - - if type(object) != type(None): - if(object.type == 'MESH'): - material = bpy.context.active_object.active_material - if (material != None): - #a None image is needed to represent not using a texture - if 'None' not in bpy.data.images: - bpy.data.images.new(name='None', width=1, height=1) - c.prop(material.shaderList, "shaderList") - shaderProps = settings.material_parameter_dict[material.shaderList.shaderList] - if material.shaderList.shaderList != 'alDefault.fx': - for property in shaderProps: - if property == 'BaseTexture': - layout.prop_search(material, "BaseTexture", bpy.data, "images") - elif property == 'CloudTexture': - layout.prop_search(material, "CloudTexture", bpy.data, "images") - elif property == 'DetailTexture': - layout.prop_search(material, "DetailTexture", bpy.data, "images") - elif property == 'CloudNormalTexture': - layout.prop_search(material, "CloudNormalTexture", bpy.data, "images") - elif property == 'NormalTexture': - layout.prop_search(material, "NormalTexture", bpy.data, "images") - elif property == 'NormalDetailTexture': - layout.prop_search(material, "NormalDetailTexture", bpy.data, "images") - elif property == 'GlossTexture': - layout.prop_search(material, "GlossTexture", bpy.data, "images") - elif property == 'WaveTexture': - layout.prop_search(material, "WaveTexture", bpy.data, "images") - elif property == 'DistortionTexture': - layout.prop_search(material, "DistortionTexture", bpy.data, "images") - else: - c.prop(material, property) - -def createShaderModeOptions(): - mode_options = [] - - for index, shader_name in enumerate(settings.material_parameter_dict): - mode_options.append((shader_name, shader_name, '', '', index)) - - return mode_options - -class shaderListProperties(bpy.types.PropertyGroup): - mode_options = createShaderModeOptions() +from bpy.props import * +import mathutils +import bpy +import importlib - shaderList : bpy.props.EnumProperty( - items=mode_options, - description="Choose ingame Shader", - default="alDefault.fx", - ) +bl_info = { + "name": "ALAMO Tools", + "author": "Gaukler, evilbobthebob, inertial", + "version": (0, 0, 3, 4), + "blender": (2, 93, 0), + "category": "Import-Export" +} -class billboardListProperties(bpy.types.PropertyGroup): - mode_options = [ - ("Disable", "Disable", 'Description WIP', '', 0), - ("Parallel", "Parallel", 'Description WIP', '', 1), - ("Face", "Face", 'Description WIP', '', 2), - ("ZAxis View", "ZAxis View", 'Description WIP', '', 3), - ("ZAxis Light", "ZAxis Light", 'Description WIP', '', 4), - ("ZAxis Wind", "ZAxis Wind", 'Description WIP', '', 5), - ("Sunlight Glow", "Sunlight Glow", 'Description WIP', '', 6), - ("Sun", "Sun", 'Description WIP', '', 7), - ] +ADDON_FOLDER = 'io_alamo_tools' + +modules = ( + '.validation', + '.UI', + '.UI_material', + '.import_alo', + '.import_ala', + '.export_alo', + '.export_ala', + '.settings', + '.utils', +) - billboardMode : bpy.props.EnumProperty( - items = mode_options, - description = "billboardMode", - default="Disable", - ) +def import_modules(): + for mod in modules: + #print('importing with importlib.import_module =' + str(mod) + "=") + importlib.import_module(mod, ADDON_FOLDER) + +def reimport_modules(): + ''' + Reimports the modules. Extremely useful while developing the addon + ''' + for mod in modules: + # Reimporting modules during addon development + want_reload_module = importlib.import_module(mod, ADDON_FOLDER) + importlib.reload(want_reload_module) + +from . import validation +from . import UI +from . import UI_material +from . import import_alo +from . import import_ala +from . import export_alo +from . import export_ala +from . import settings +from . import utils -def proxy_name_update(self, context): - if self.ProxyName != self.ProxyName.upper(): #prevents endless recursion - self.ProxyName = self.ProxyName.upper() +classes = ( + import_alo.ALO_Importer, + import_ala.ALA_Importer, + export_alo.ALO_Exporter, + export_ala.ALA_Exporter, +) -#blender registration def menu_func_import(self, context): self.layout.operator(import_alo.ALO_Importer.bl_idname, text=".ALO Importer") self.layout.operator(import_ala.ALA_Importer.bl_idname, text=".ALA Importer") + def menu_func_export(self, context): self.layout.operator(export_alo.ALO_Exporter.bl_idname, text=".ALO Exporter") self.layout.operator(export_ala.ALA_Exporter.bl_idname, text=".ALA Exporter") -from . import_alo import ALO_Importer -from . import_ala import ALA_Importer -from . export_alo import ALO_Exporter -from . export_ala import ALA_Exporter - -classes = ( - skeletonEnumClass, - billboardListProperties, - shaderListProperties, - ALO_Importer, - ALA_Importer, - ALO_Exporter, - ALA_Exporter, - ALAMO_PT_materialPropertyPanel, - createConstraintBoneButton, - ALAMO_PT_ToolsPanel -) def register(): + import_modules() + UI.register() + UI_material.register() - from bpy.utils import register_class for cls in classes: - register_class(cls) + bpy.utils.register_class(cls) bpy.types.TOPBAR_MT_file_import.append(menu_func_import) bpy.types.TOPBAR_MT_file_export.append(menu_func_export) - bpy.types.Scene.ActiveSkeleton = PointerProperty(type=skeletonEnumClass) - bpy.types.Scene.modelFileName = StringProperty(name="") - - bpy.types.Action.AnimationEndFrame = IntProperty(default = 24) - - bpy.types.EditBone.Visible = BoolProperty(default=True) - bpy.types.EditBone.EnableProxy = BoolProperty() - bpy.types.EditBone.proxyIsHidden = BoolProperty() - bpy.types.PoseBone.proxyIsHiddenAnimation = BoolProperty() - bpy.types.EditBone.altDecreaseStayHidden = BoolProperty() - bpy.types.EditBone.ProxyName = StringProperty(update=proxy_name_update) - bpy.types.EditBone.billboardMode = bpy.props.PointerProperty(type=billboardListProperties) - - bpy.types.Object.HasCollision = BoolProperty() - bpy.types.Object.Hidden = BoolProperty() - - bpy.types.Material.BaseTexture = bpy.props.StringProperty(default='None') - bpy.types.Material.DetailTexture = bpy.props.StringProperty(default='None') - bpy.types.Material.NormalDetailTexture = bpy.props.StringProperty(default='None') - bpy.types.Material.NormalTexture = bpy.props.StringProperty(default='None') - bpy.types.Material.GlossTexture = bpy.props.StringProperty(default='None') - bpy.types.Material.WaveTexture = bpy.props.StringProperty(default='None') - bpy.types.Material.DistortionTexture = bpy.props.StringProperty(default='None') - bpy.types.Material.CloudTexture = bpy.props.StringProperty(default='None') - bpy.types.Material.CloudNormalTexture = bpy.props.StringProperty(default='None') - - bpy.types.Material.shaderList = bpy.props.PointerProperty(type=shaderListProperties) - bpy.types.Material.Emissive = bpy.props.FloatVectorProperty(min = 0, max = 1, size = 4, default=(0,0,0,0)) - bpy.types.Material.Diffuse = bpy.props.FloatVectorProperty(min=0, max=1, size=4, default=(1,1,1,0)) - bpy.types.Material.Specular = bpy.props.FloatVectorProperty(min=0, max=1, size=4, default=(1,1,1,0)) - bpy.types.Material.Shininess = bpy.props.FloatProperty(min=0, max=255, default = 32) - bpy.types.Material.Colorization = bpy.props.FloatVectorProperty(min=0, max=1, size=4, default=(1,1,1,0)) - bpy.types.Material.DebugColor = bpy.props.FloatVectorProperty(min=0, max=1, size=4, default=(0,1,0,0)) - bpy.types.Material.UVOffset = bpy.props.FloatVectorProperty(min=0, max=1, size=4, default=(0,0,0,0)) - bpy.types.Material.Color = bpy.props.FloatVectorProperty(min=0, max=1, size=4, default=(1,1,1,1)) - bpy.types.Material.UVScrollRate = bpy.props.FloatVectorProperty(min=0, max=1, size=4, default=(0,0,0,0)) - bpy.types.Material.DiffuseColor = bpy.props.FloatVectorProperty(min=0, max=1, size=3, default=(0.5,0.5,0.5)) - #shield shader properties - bpy.types.Material.EdgeBrightness = bpy.props.FloatProperty(min=0, max=255, default=0.5) - bpy.types.Material.BaseUVScale = bpy.props.FloatProperty(min=-255, max=255, default=1) - bpy.types.Material.WaveUVScale = bpy.props.FloatProperty(min=-255, max=255, default=1) - bpy.types.Material.DistortUVScale = bpy.props.FloatProperty(min=-255, max=255, default=1) - bpy.types.Material.BaseUVScrollRate = bpy.props.FloatProperty(min=-255, max=255, default=-0.15) - bpy.types.Material.WaveUVScrollRate = bpy.props.FloatProperty(min=-255, max=255, default=-0.15) - bpy.types.Material.DistortUVScrollRate = bpy.props.FloatProperty(min=-255, max=255, default=-0.25) - #tree properties - bpy.types.Material.BendScale = bpy.props.FloatProperty(min=-255, max=255, default=0.4) - #grass properties - bpy.types.Material.Diffuse1 = bpy.props.FloatVectorProperty(min=0, max=1, size=4, default=(1,1,1,1)) - #skydome.fx properties - bpy.types.Material.CloudScrollRate = bpy.props.FloatProperty(min=-255, max=255, default=0.001) - bpy.types.Material.CloudScale = bpy.props.FloatProperty(min=-255, max=255, default=1) - #nebula.fx properties - bpy.types.Material.SFreq = bpy.props.FloatProperty(min=-255, max=255, default=0.002) - bpy.types.Material.TFreq = bpy.props.FloatProperty(min=-255, max=255, default=0.005) - bpy.types.Material.DistortionScale = bpy.props.FloatProperty(min=-255, max=255, default=1) - #planet.fx properties - bpy.types.Material.Atmosphere = bpy.props.FloatVectorProperty(min=0, max=1, size=4, default=(0.5, 0.5, 0.5, 0.5)) - bpy.types.Material.CityColor = bpy.props.FloatVectorProperty(min=0, max=1, size=4, default=(0.5, 0.5, 0.5, 0.5)) - bpy.types.Material.AtmospherePower = bpy.props.FloatProperty(min=-255, max=255, default=1) - #tryplanar mapping properties - bpy.types.Material.MappingScale = bpy.props.FloatProperty(min=0, max=255, default=0.1) - bpy.types.Material.BlendSharpness = bpy.props.FloatProperty(min=0, max=255, default=0.1) def unregister(): - from bpy.utils import unregister_class + UI.unregister() + UI_material.unregister() + for cls in reversed(classes): - unregister_class(cls) + bpy.utils.unregister_class(cls) bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) - bpy.types.Scene.ActiveSkeleton - - bpy.types.Action.AnimationEndFrame - - bpy.types.EditBone.Visible - bpy.types.EditBone.EnableProxy - bpy.types.EditBone.proxyIsHidden - bpy.types.PoseBone.proxyIsHiddenAnimation - bpy.types.EditBone.altDecreaseStayHidden - bpy.types.EditBone.ProxyName - bpy.types.EditBone.billboardMode - - bpy.types.Object.HasCollision - bpy.types.Object.Hidden - - bpy.types.Material.BaseTexture - bpy.types.Material.DetailTexture - bpy.types.Material.NormalTexture - bpy.types.Material.NormalDetailTexture - bpy.types.Material.GlossTexture - bpy.types.Material.WaveTexture - bpy.types.Material.DistortionTexture - bpy.types.Material.CloudTexture - bpy.types.Material.CloudNormalTexture - - bpy.types.Material.shaderList - bpy.types.Material.Emissive - bpy.types.Material.Diffuse - bpy.types.Material.Specular - bpy.types.Material.Shininess - bpy.types.Material.Colorization - bpy.types.Material.DebugColor - bpy.types.Material.UVOffset - bpy.types.Material.Color - bpy.types.Material.UVScrollRate - bpy.types.Material.DiffuseColor - # shield shader properties - bpy.types.Material.EdgeBrightness - bpy.types.Material.BaseUVScale - bpy.types.Material.WaveUVScale - bpy.types.Material.DistortUVScale - bpy.types.Material.BaseUVScrollRate - bpy.types.Material.WaveUVScrollRate - bpy.types.Material.DistortUVScrollRate - # tree properties - bpy.types.Material.BendScale - # grass properties - bpy.types.Material.Diffuse1 - # skydome.fx properties - bpy.types.Material.CloudScrollRate - bpy.types.Material.CloudScale - # nebula.fx properties - bpy.types.Material.SFreq - bpy.types.Material.TFreq - bpy.types.Material.DistortionScale - # planet.fx properties - bpy.types.Material.Atmosphere - bpy.types.Material.CityColor - bpy.types.Material.AtmospherePower - #tryplanar mapping properties - bpy.types.Material.MappingScale - bpy.types.Material.BlendSharpness if __name__ == "__main__": register() diff --git a/io_alamo_tools/changelog.md b/io_alamo_tools/changelog.md new file mode 100644 index 0000000..9697a75 --- /dev/null +++ b/io_alamo_tools/changelog.md @@ -0,0 +1 @@ +# ` ¯\_(ツ)_/¯ ` \ No newline at end of file diff --git a/io_alamo_tools/export_ala.py b/io_alamo_tools/export_ala.py index ca5f791..d837a1a 100644 --- a/io_alamo_tools/export_ala.py +++ b/io_alamo_tools/export_ala.py @@ -22,6 +22,18 @@ import sys import os import bmesh +from contextlib import contextmanager + +@contextmanager +def disable_exception_traceback(): + """ + All traceback information is suppressed and only the exception type and value are printed + Used to make user friendly errors + """ + default_value = getattr(sys, "tracebacklimit", 1000) # `1000` is a Python's default value + sys.tracebacklimit = 0 + yield + sys.tracebacklimit = default_value # revert changes def chunk_size(size): #high bit is used to determine if a chunk holds chunks or data @@ -182,7 +194,7 @@ def create_animation(): # get armature name armature = utils.findArmature() if armature == None: - print("Warning: No armature found!") + self.report({"WARNING"}, "No armature found!") return b'' chunk += create_anim_info_chunk(armature) @@ -425,20 +437,24 @@ def create_visibility_chunk(armature, bone): class AnimationExporter(): def exportAnimation(self, path): - file = open(path, 'wb') # open file in read binary mode - - global translationOffsetDict - translationOffsetDict = {} - global translationScaleDict - translationScaleDict = {} - file.write(create_animation()) - file.close() - file = None + if os.access(path, os.W_OK) or not os.access(path, os.F_OK): + file = open(path, 'wb') # open file in read binary mode + + global translationOffsetDict + translationOffsetDict = {} + global translationScaleDict + translationScaleDict = {} + file.write(create_animation()) + file.close() + file = None + else: + with disable_exception_traceback(): + raise Exception(f"ALAMO - EXPORT FAILED; can't write {os.path.split(path)[1]}") class ALA_Exporter(bpy.types.Operator): """ALA Exporter""" # blender will use this as a tooltip for menu items and buttons. - bl_idname = "export.ala" # unique identifier for buttons and menu items to reference. + bl_idname = "export_anim.ala" # unique identifier for buttons and menu items to reference. bl_label = "Export ALA File" # display name in the interface. bl_options = {'REGISTER', 'UNDO'} # enable undo for the operator. bl_info = { diff --git a/io_alamo_tools/export_alo.py b/io_alamo_tools/export_alo.py index 71635bb..0d163ad 100644 --- a/io_alamo_tools/export_alo.py +++ b/io_alamo_tools/export_alo.py @@ -1,5 +1,5 @@ import bpy -from . import settings, utils, export_ala +from . import settings, utils, export_ala, validation from bpy.props import (StringProperty, BoolProperty, @@ -23,11 +23,34 @@ import os import bmesh import copy +from contextlib import contextmanager -class ALO_Exporter(bpy.types.Operator): +def skeletonEnumCallback(scene, context): + armatures = [('None', 'None', '', '', 0)] + counter = 1 + for arm in bpy.data.objects: # test if armature exists + if arm.type == 'ARMATURE': + armatures.append((arm.name, arm.name, '', '', counter)) + counter += 1 + + return armatures + + +@contextmanager +def disable_exception_traceback(): + """ + All traceback information is suppressed and only the exception type and value are printed + Used to make user friendly errors + """ + default_value = getattr(sys, "tracebacklimit", 1000) # `1000` is a Python's default value + sys.tracebacklimit = 0 + yield + sys.tracebacklimit = default_value # revert changes + +class ALO_Exporter(bpy.types.Operator, ExportHelper): """ALO Exporter""" # blender will use this as a tooltip for menu items and buttons. - bl_idname = "export.alo" # unique identifier for buttons and menu items to reference. + bl_idname = "export_mesh.alo" # unique identifier for buttons and menu items to reference. bl_label = "Export ALO File" # display name in the interface. bl_options = {'REGISTER', 'UNDO'} # enable undo for the operator. bl_info = { @@ -52,11 +75,38 @@ class ALO_Exporter(bpy.types.Operator): default=True, ) + useNamesFrom: EnumProperty( + name = "Use Names From", + description = "Whether the exporter should use object or mesh names.", + items=( + ('MESH', "Mesh", ""), + ('OBJECT', "Object", ""), + ), + default = 'MESH', + ) + + skeletonEnum : EnumProperty( + name='Active Skeleton', + description = "skeleton that is exported", + items = skeletonEnumCallback, + ) + + def draw(self, context): layout = self.layout + layout.use_property_split = True + + row = layout.row() + row.prop(self, "exportAnimations") + row = layout.row() + row.prop(self, "exportHiddenObjects") - layout.prop(self, "exportAnimations") - layout.prop(self, "exportHiddenObjects") + row = layout.row(heading="Names From") + row.use_property_split = False + row.prop(self, "useNamesFrom", expand = True) + + row = layout.row() + row.prop(bpy.context.scene.ActiveSkeleton, "skeletonEnum") def execute(self, context): # execute() is called by blender when running the operator. @@ -408,7 +458,7 @@ def create_animation_mapping(mesh, object, material, bone_name_per_alo_index): for index in group_index_list: if index == None: cleanUpModifiers(object) - raise RuntimeError('Missing vertex group on object: ' + object.name) + self.report({"ERROR"}, f'ALAMO - Missing vertex group on object: {object.name}') group_index_list.sort() group_to_alo_index = {} @@ -1289,97 +1339,9 @@ def chunk_size(size): #add 2147483648 instead of binary operation return size+2147483648 - def selectNonManifoldVertices(object): - if(bpy.context.mode != 'OBJECT'): - bpy.ops.object.mode_set(mode='OBJECT') - object.hide_set(False) - bpy.context.view_layer.objects.active = object - bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.mesh.select_all(action='DESELECT') - bpy.ops.mesh.select_non_manifold() - - def checkShadowMesh(mesh_list): #checks if shadow meshes are correct and checks if material is missing - for object in mesh_list: - if len(object.data.materials) == 0: - raise RuntimeError('Missing material on object: ' + object.name) - shader = object.data.materials[0].shaderList.shaderList - if shader == 'MeshShadowVolume.fx' or shader == 'RSkinShadowVolume.fx': - bm = bmesh.new() # create an empty BMesh - bm.from_mesh(object.data) # fill it in from a Mesh - bm.verts.ensure_lookup_table() - - for vertex in bm.verts: - if not vertex.is_manifold: - bm.free() - selectNonManifoldVertices(object) - raise RuntimeError('Non manifold geometry shadow mesh: ' + object.name) - - for edge in bm.edges: - if len(edge.link_faces) < 2 : - bm.free() - selectNonManifoldVertices(object) - raise RuntimeError('Non manifold geometry shadow mesh: ' + object.name) - - bm.free() - - - def checkUV(mesh_list): #throws error if object lacks UVs - for object in mesh_list: - for material in object.data.materials: - if material.shaderList.shaderList == 'MeshShadowVolume.fx' or material.shaderList.shaderList == 'RSkinShadowVolume.fx': - if len(object.data.materials) > 1: - raise RuntimeError('Multiple materials on shadow volume: ' + object.name + ' , remove additional materials') - else: - return - if object.HasCollision: - if len(object.data.materials) > 1: - raise RuntimeError('Multiple submeshes/materials on collision mesh: ' + object.name + ' , remove additional materials') - if object.data.uv_layers: #or material.shaderList.shaderList in settings.no_UV_Shaders: #currently UVs are needed for everything but shadows - continue - else: - raise RuntimeError('Missing UV: ' + object.name) - - def checkInvalidArmatureModifier(mesh_list): #throws error if armature modifier lacks rig, this would crash the exporter later and checks if skeleton in modifier doesn't match active skeleton - activeSkeleton = bpy.context.scene.ActiveSkeleton.skeletonEnum - for object in mesh_list: - for modifier in object.modifiers: - if modifier.type == "ARMATURE": - if modifier.object == None: - raise RuntimeError('Armature modifier without selected skeleton on: ' + object.name) - return True - elif modifier.object.type != 'NoneType': - if modifier.object.name != activeSkeleton: - raise RuntimeError('Armature modifier skeleton doesnt match active skeleton on: ' + object.name) - return True - for constraint in object.constraints: - if constraint.type == 'CHILD_OF': - if constraint.target is not None: - #print(type(constraint.target)) - if constraint.target.name != activeSkeleton: - raise RuntimeError('Constraint doesnt match active skeleton on: ' + object.name) - return True - - def checkFaceNumber(mesh_list): #checks if the number of faces exceeds max ushort, which is used to save the indices - for object in mesh_list: - if len(object.data.polygons) > 65535: - raise RuntimeError('Face number exceeds uShort max on object: ' + object.name + ' split mesh into multiple objects') - return True - - def checkAutosmooth(mesh_list): #prints a warning if Autosmooth is used - for object in mesh_list: - if object.data.use_auto_smooth: - print('Warning: ' + object.name + ' uses autosmooth, ingame shading might not match blender, use edgesplit instead') - - def checkTranslation(mesh_list): #prints warning when translation is not default - for object in mesh_list: - if object.location != mathutils.Vector((0.0, 0.0, 0.0)) or object.rotation_euler != mathutils.Euler((0.0, 0.0, 0.0), 'XYZ') or object.scale != mathutils.Vector((1.0, 1.0, 1.0)): - print('Warning: ' + object.name + ' is not aligned with the world origin, apply translation or bind to bone') - - def checkTranslationArmature(): #prints warning when translation is not default - armature = utils.findArmature() - if armature != None: - if armature.location != mathutils.Vector((0.0, 0.0, 0.0)) or armature.rotation_euler != mathutils.Euler((0.0, 0.0, 0.0), 'XYZ') or armature.scale != mathutils.Vector((1.0, 1.0, 1.0)): - print('Warning: active Armature is not aligned with the world origin') + def exportFailed(): + with disable_exception_traceback(): + raise Exception('ALAMO - EXPORT FAILED') def unhide(): hiddenList = [] @@ -1394,21 +1356,6 @@ def hide(hiddenList): object.hide_render = hiddenList[counter] counter += 1 - def create_export_list(collection): - export_list = [] - - if(collection.hide_viewport): - return export_list - - for object in collection.objects: - if(object.type == 'MESH' and (object.hide_viewport == False or self.exportHiddenObjects)): - export_list.append(object) - - for child in collection.children: - export_list.extend(create_export_list(child)) - - return export_list - #hidden objects and collections can't be accessed, avoid problems def unhide_collections(collection_parent): collection_is_hidden_list = [] @@ -1453,16 +1400,15 @@ def exportAnimations(filePath): exporter.exportAnimation(filePath + "_" + action.name + ".ALA") - mesh_list = create_export_list(bpy.context.scene.collection) + mesh_list = validation.create_export_list(bpy.context.scene.collection, self.exportHiddenObjects, self.useNamesFrom) #check if export objects satisfy requirements (has material, UVs, ...) - checkShadowMesh(mesh_list) - checkUV(mesh_list) - checkFaceNumber(mesh_list) - checkAutosmooth(mesh_list) - checkTranslation(mesh_list) - checkTranslationArmature() - checkInvalidArmatureModifier(mesh_list) + messages = validation.validate(mesh_list) + + if messages is not None and len(messages) > 0: + for message in messages: + self.report(*message) + exportFailed() hiddenList = unhide() collection_is_hidden_list = unhide_collections(bpy.context.scene.collection) @@ -1470,20 +1416,24 @@ def exportAnimations(filePath): path = self.properties.filepath global file - file = open(path, 'wb') # open file in read binary mode - - bone_name_per_alo_index = create_skeleton() - create_mesh(mesh_list, bone_name_per_alo_index) - create_connections(mesh_list) - - file.close() - file = None - #removeShadowDoubles() - hide(hiddenList) - hide_collections(bpy.context.scene.collection, collection_is_hidden_list, 0) - - if(self.exportAnimations): - exportAnimations(path) + if os.access(path, os.W_OK) or not os.access(path, os.F_OK): + file = open(path, 'wb') # open file in read binary mode + + bone_name_per_alo_index = create_skeleton() + create_mesh(mesh_list, bone_name_per_alo_index) + create_connections(mesh_list) + + file.close() + file = None + #removeShadowDoubles() + hide(hiddenList) + hide_collections(bpy.context.scene.collection, collection_is_hidden_list, 0) + + if(self.exportAnimations): + exportAnimations(path) + else: + self.report({"ERROR"}, f'ALAMO - Could not write to {os.path.split(path)[1]}') + exportFailed() return {'FINISHED'} # this lets blender know the operator finished successfully. diff --git a/io_alamo_tools/import_ala.py b/io_alamo_tools/import_ala.py index 1a8f040..3f14c3e 100644 --- a/io_alamo_tools/import_ala.py +++ b/io_alamo_tools/import_ala.py @@ -353,8 +353,7 @@ def validate(data): break if(fitting): return True - print("animation bones not matching active armature") - return False + raise Exception("animation bones not matching active armature") class AnimationImporter(): def loadAnimation(self, filePath): @@ -386,7 +385,7 @@ def loadAnimation(self, filePath): class ALA_Importer(bpy.types.Operator): """ALA Importer""" # blender will use this as a tooltip for menu items and buttons. - bl_idname = "import.ala" # unique identifier for buttons and menu items to reference. + bl_idname = "import_anim.ala" # unique identifier for buttons and menu items to reference. bl_label = "Import ALA File" # display name in the interface. bl_options = {'REGISTER', 'UNDO'} # enable undo for the operator. filename_ext = ".ala" diff --git a/io_alamo_tools/import_alo.py b/io_alamo_tools/import_alo.py index aed447b..4605035 100644 --- a/io_alamo_tools/import_alo.py +++ b/io_alamo_tools/import_alo.py @@ -24,6 +24,7 @@ from os import listdir import bmesh + def boneEnumCallback(scene, context): bones = [('None', 'None', '', '', 0)] counter = 1 @@ -32,61 +33,78 @@ def boneEnumCallback(scene, context): for bone in armature.data.bones: bones.append((bone.name, bone.name, '', '', counter)) counter += 1 - bones.sort(key=lambda tup : tup[0]) + bones.sort(key=lambda tup: tup[0]) return bones + class ALO_Importer(bpy.types.Operator): """ALO Importer""" # blender will use this as a tooltip for menu items and buttons. - bl_idname = "import.alo" # unique identifier for buttons and menu items to reference. + bl_idname = "import_mesh.alo" # unique identifier for buttons and menu items to reference. bl_label = "Import ALO File" # display name in the interface. bl_options = {'REGISTER', 'UNDO'} # enable undo for the operator. filename_ext = ".alo" - filter_glob : StringProperty(default="*.alo", options={'HIDDEN'}) + filter_glob: StringProperty(default="*.alo", options={'HIDDEN'}) bl_info = { "name": "ALO Importer", "category": "Import", } - parentName : EnumProperty( + parentName: EnumProperty( name='Attachment Bone', - description = "Bone that imported models are attached to", - items = boneEnumCallback + description="Bone that imported models are attached to", + items=boneEnumCallback + ) + + importAnimations: BoolProperty( + name="Import Animations", + description="Import the model's animations from the same path", + default=True, ) - importAnimations : BoolProperty( - name="Import Animations", - description="Import the model's animations from the same path", - default=True, - ) + textureOverride: EnumProperty( + name = "Submod Texture Override", + description = "Try to import textures from a different submod", + items=( + ("NONE", "None", ""), + ('CoreSaga', "Core Saga", ""), + ('FotR', "Fall of the Republic", ""), + ('GCW', "Imperial Reign", ""), + ('Rev', "Revan's Revenge", ""), + ('TR', "Thrawn's Revenge", ""), + ), + default="NONE", + ) def draw(self, context): layout = self.layout layout.prop(self, "importAnimations") layout.prop(self, "parentName") + layout.prop(self, "textureOverride") - filepath : StringProperty(name="File Path", description="Filepath used for importing the ALO file", maxlen=1024, default="") - - def execute(self, context): # execute() is called by blender when running the operator. + filepath: StringProperty( + name="File Path", description="Filepath used for importing the ALO file", maxlen=1024, default="") - #main structure + # execute() is called by blender when running the operator. + def execute(self, context): + # main structure def process_active_junk(): meshNameList = [] - #loop over file until end is reached + # loop over file until end is reached while(file.tell() < os.path.getsize(self.properties.filepath)): active_chunk = file.read(4) - #print(active_chunk) + # print(active_chunk) if active_chunk == b"\x00\x02\x00\00": armatureData = createArmature() elif active_chunk == b"\x00\x04\x00\00": - file.seek(4, 1) #skip size + file.seek(4, 1) # skip size meshName = processMeshChunk() meshNameList.append(meshName) - elif active_chunk == b"\x00\x13\x00\00": #light chunk is irrelevant - print('WARNING: file contains light objects, these are not supported and might cause minor issues') + elif active_chunk == b"\x00\x13\x00\00": # light chunk is irrelevant + self.report({"WARNING"}, "ALAMO - File contains light objects, these are not supported and might cause minor issues") size = read_chunk_length() - file.seek(size, 1) #skip to next chunk + file.seek(size, 1) # skip to next chunk elif active_chunk == b"\x00\x06\x00\00": file.seek(8, 1) # skip size and next header n_objects_proxies = get_n_objects_n_proxies() @@ -117,7 +135,8 @@ def __init__(self): def removeShadowDoubles(): for object in bpy.data.objects: if(object.type == 'MESH'): - if(len(object.material_slots) <= 0): continue; #already existing objects might not have a material + if(len(object.material_slots) <= 0): + continue # already existing objects might not have a material shader = object.material_slots[0].material.shaderList.shaderList if (shader == 'MeshCollision.fx' or shader == 'RSkinShadowVolume.fx' or shader == 'MeshShadowVolume.fx'): bpy.ops.object.select_all(action='DESELECT') @@ -133,18 +152,19 @@ def createArmature(): global fileName - #create armature + # create armature armatureBlender = bpy.data.armatures.new(fileName + "Armature") - #create object - armatureObj = bpy.data.objects.new(fileName + "Rig", object_data=armatureBlender) + # create object + armatureObj = bpy.data.objects.new( + fileName + "Rig", object_data=armatureBlender) # Link object to collection importCollection.objects.link(armatureObj) bpy.context.view_layer.objects.active = armatureObj bpy.context.view_layer.update() - #adjust settings and enter edit-mode + # adjust settings and enter edit-mode armatureObj = bpy.context.object armatureObj.show_in_front = True utils.setModeToEdit() @@ -163,25 +183,25 @@ def createArmature(): for bone in armatureData.bones: createBone(bone, armatureBlender, armatureData) - bpy.ops.object.mode_set(mode = 'OBJECT') + bpy.ops.object.mode_set(mode='OBJECT') bpy.context.scene.ActiveSkeleton.skeletonEnum = armatureObj.name return armatureData def get_bone_count(armatureData): - file.seek(8,1) #skip header and size + file.seek(8, 1) # skip header and size bone_count = struct.unpack(" 1: currentSubMeshMaxFaceIndex = currentMesh.subMeshList[0].nFaces subMeshCounter = 0 @@ -314,7 +337,7 @@ def readMeshInfo(currentMesh): create_object(currentMesh) def get_mesh_name(): - file.seek(4, 1) #skip header + file.seek(4, 1) # skip header length = read_chunk_length() counter = 0 mesh_name = "" @@ -333,7 +356,7 @@ def get_n_vertices_n_primitives(currentSubMesh): file.seek(120, 1) def processMeshChunk(): - #name chunk + # name chunk currentMesh = meshClass() meshList.append(currentMesh) currentMesh.name = get_mesh_name() @@ -357,7 +380,7 @@ def processMeshChunk(): return name def read_mesh_data(currentSubMesh): - file.seek(4,1) #skip header + file.seek(4, 1) # skip header meshDataChunkSize = read_chunk_length() currentPosition = file.tell() while (file.tell() < currentPosition + meshDataChunkSize): @@ -375,7 +398,8 @@ def read_mesh_data(currentSubMesh): read_animation_mapping(currentSubMesh) elif active_chunk == b"\x07\x00\x01\00": file.seek(4, 1) # skip size - vertex_data = process_vertex_buffer_2(False, currentSubMesh) + vertex_data = process_vertex_buffer_2( + False, currentSubMesh) elif active_chunk == b"\x05\x00\x01\00": file.seek(4, 1) # skip size # old version of the chunk @@ -385,13 +409,13 @@ def read_mesh_data(currentSubMesh): file.seek(size, 1) # skip to next chunk def read_material_info_chunk(currentSubMesh): - file.seek(4,1) #skip header + file.seek(4, 1) # skip header materialChunkSize = read_chunk_length() currentPosition = file.tell() while (file.tell() < currentPosition + materialChunkSize): active_chunk = file.read(4) if active_chunk == b"\x01\x01\x01\00": - create_material(currentSubMesh) + set_alamo_shader(currentSubMesh) elif active_chunk == b"\x02\x01\x01\00": read_int(currentSubMesh.material) elif active_chunk == b"\x03\x01\x01\00": @@ -402,6 +426,7 @@ def read_material_info_chunk(currentSubMesh): process_texture_chunk(currentSubMesh.material) elif active_chunk == b"\x06\x01\x01\00": read_float4(currentSubMesh.material) + create_material(currentSubMesh) set_up_textures(currentSubMesh.material) def read_animation_mapping(currentSubMesh): @@ -410,10 +435,152 @@ def read_animation_mapping(currentSubMesh): counter = 0 animation_mapping = [] while counter < read_counter: - currentSubMesh.animationMapping.append(struct.unpack("I", file.read(4))[0]) + currentSubMesh.animationMapping.append( + struct.unpack("I", file.read(4))[0]) counter += 1 return animation_mapping + def material_group_additive(context, operator, group_name, material, is_emissive): + node_group = bpy.data.node_groups.new(group_name, 'ShaderNodeTree') + + node = node_group.nodes.new + link = node_group.links.new + + group_out = node('NodeGroupOutput') + group_out.location.x += 200.0 + node_group.outputs.new('NodeSocketShader', 'Surface') + + mix_shader = node("ShaderNodeMixShader") + + transparent = node("ShaderNodeBsdfTransparent") + transparent.location.x -= 200 + transparent.location.y -= 50 + + base_image_node = node("ShaderNodeTexImage") + base_image_node.location.x -= 500 + + if is_emissive: + group_in = node('NodeGroupInput') + group_in.location.x -= 700 + emissive = node_group.inputs.new( + 'NodeSocketFloat', 'Emissive Strength') + emissive.default_value = 1.0 + color = node("ShaderNodeEmission") + link(group_in.outputs[0], color.inputs[1]) + eevee_alpha_fix = node("ShaderNodeInvert") + eevee_alpha_fix.location.x -= 500 + eevee_alpha_fix.location.y += 300 + # Fix for obnoxious transparency bug in Eevee + link(base_image_node.outputs[1], eevee_alpha_fix.inputs[1]) + link(base_image_node.outputs['Color'], + mix_shader.inputs['Fac']) + + else: + color = node("ShaderNodeBsdfDiffuse") + link(base_image_node.outputs['Alpha'], + mix_shader.inputs['Fac']) + + color.location.x -= 200 + color.location.y -= 150 + + link(base_image_node.outputs['Color'], color.inputs[0]) + link(transparent.outputs[0], mix_shader.inputs[1]) + link(color.outputs[0], mix_shader.inputs[2]) + + if material.BaseTexture != 'None' and material.BaseTexture in bpy.data.images: + diffuse_texture = bpy.data.images[material.BaseTexture] + diffuse_texture.alpha_mode = 'CHANNEL_PACKED' + base_image_node.image = diffuse_texture + + link(mix_shader.outputs[0], group_out.inputs[0]) + + return node_group + + def material_group_basic(context, operator, group_name, material): + node_group = bpy.data.node_groups.new(group_name, 'ShaderNodeTree') + + node = node_group.nodes.new + link = node_group.links.new + + group_in = node('NodeGroupInput') + group_in.location.x -= 700 + node_group.inputs.new('NodeSocketColor', 'Team Color') + spec = node_group.inputs.new( + 'NodeSocketFloat', 'Specular Intensity') + spec.default_value = 0.1 + + group_out = node('NodeGroupOutput') + node_group.outputs.new('NodeSocketColor', 'Base Color') + node_group.outputs.new('NodeSocketFloat', 'Specular') + node_group.outputs.new('NodeSocketVector', 'Normal') + + base_image_node = node("ShaderNodeTexImage") + base_image_node.location.x -= 500 + + mix_node = node("ShaderNodeMixRGB") + mix_node.blend_type = 'COLOR' + mix_node.location.x -= 200 + + link(base_image_node.outputs['Color'], mix_node.inputs['Color1']) + link(base_image_node.outputs['Alpha'], mix_node.inputs['Fac']) + link(mix_node.outputs['Color'], group_out.inputs['Base Color']) + + normal_image_node = node("ShaderNodeTexImage") + normal_image_node.location.x -= 1100.0 + normal_image_node.location.y -= 300.0 + + normal_split = node("ShaderNodeSeparateRGB") + normal_split.location.x -= 800 + normal_split.location.y -= 300 + normal_invert = node("ShaderNodeMath") + normal_invert.operation = 'SUBTRACT' + normal_invert.inputs[0].default_value = 1 + normal_invert.location.x -= 600 + normal_invert.location.y -= 300 + normal_combine = node("ShaderNodeCombineRGB") + normal_combine.location.x -= 400 + normal_combine.location.y -= 300 + + normal_map_node = node("ShaderNodeNormalMap") + normal_map_node.space = 'TANGENT' + normal_map_node.location.x -= 200.0 + normal_map_node.location.y -= 300.0 + + specular_multiply = node("ShaderNodeMath") + specular_multiply.operation = 'MULTIPLY' + specular_multiply.location.x -= 800 + specular_multiply.location.y -= 100 + + link(normal_image_node.outputs['Color'], + normal_split.inputs['Image']) + link(normal_split.outputs['R'], normal_combine.inputs['R']) + link(normal_split.outputs['G'], normal_invert.inputs[1]) + link(normal_invert.outputs[0], normal_combine.inputs['G']) + link(normal_split.outputs['B'], normal_combine.inputs['B']) + link(normal_combine.outputs[0], normal_map_node.inputs[1]) + link(normal_map_node.outputs[0], group_out.inputs[2]) + + link(normal_image_node.outputs['Alpha'], + specular_multiply.inputs[0]) + + link(group_in.outputs['Team Color'], mix_node.inputs['Color2']) + link(group_in.outputs['Specular Intensity'], + specular_multiply.inputs[1]) + link(specular_multiply.outputs[0], group_out.inputs[1]) + + if material.BaseTexture != 'None' and material.BaseTexture in bpy.data.images: + diffuse_texture = bpy.data.images[material.BaseTexture] + diffuse_texture.alpha_mode = 'CHANNEL_PACKED' + base_image_node.image = diffuse_texture + + if material.NormalTexture != 'None' and material.NormalTexture in bpy.data.images: + normal_texture = bpy.data.images[material.NormalTexture] + normal_texture.alpha_mode = 'CHANNEL_PACKED' + normal_image_node.image = normal_texture + normal_image_node.image.colorspace_settings.name = 'Raw' + + return node_group + def set_up_textures(material): material.use_nodes = True @@ -421,55 +588,78 @@ def set_up_textures(material): nodes = nt.nodes links = nt.links - #clean up - while(nodes): nodes.remove(nodes[0]) - - output = nodes.new("ShaderNodeOutputMaterial") - bsdf = nodes.new("ShaderNodeBsdfPrincipled") - - base_image_node = nodes.new("ShaderNodeTexImage") - normal_image_node = nodes.new("ShaderNodeTexImage") - - normal_map_node = nodes.new("ShaderNodeNormalMap") - normal_map_node.space = 'TANGENT' - normal_map_node.uv_map = 'MainUV' - - uvmap = nodes.new("ShaderNodeUVMap") - uvmap.uv_map = "MainUV" - - if material.BaseTexture != 'None': + # clean up + while(nodes): + nodes.remove(nodes[0]) + + output = nodes.new("ShaderNodeOutputMaterial") + custom_node_name = material.name + "Group" + my_group = 'null' + + if ("Additive" in material.shaderList.shaderList): + material.blend_method = "BLEND" + my_group = material_group_additive( + self, context, custom_node_name, material, True) + mat_group = nt.nodes.new("ShaderNodeGroup") + mat_group.node_tree = bpy.data.node_groups[my_group.name] + mat_group.location.x -= 200.0 + links.new(mat_group.outputs[0], output.inputs['Surface']) + elif ("Alpha" in material.shaderList.shaderList): + material.blend_method = "BLEND" + my_group = material_group_additive( + self, context, custom_node_name, material, False) + mat_group = nt.nodes.new("ShaderNodeGroup") + mat_group.node_tree = bpy.data.node_groups[my_group.name] + mat_group.location.x -= 200.0 + links.new(mat_group.outputs[0], output.inputs['Surface']) + else: + bsdf = nodes.new("ShaderNodeBsdfPrincipled") + bsdf.inputs[4].default_value = 0.1 # Set metallic to 0.1 + bsdf.inputs[7].default_value = 0.2 # Set roughness to 0.2 + bsdf.location.x -= 300.0 + links.new(bsdf.outputs['BSDF'], output.inputs['Surface']) + my_group = material_group_basic( + self, context, custom_node_name, material) + mat_group = nt.nodes.new("ShaderNodeGroup") + mat_group.node_tree = bpy.data.node_groups[my_group.name] + mat_group.location.x -= 500.0 + links.new(mat_group.outputs[0], bsdf.inputs['Base Color']) + links.new(mat_group.outputs[1], bsdf.inputs[5]) + links.new(mat_group.outputs[2], bsdf.inputs['Normal']) + + def create_material(currentSubMesh): + if currentSubMesh.material.name != "DUMMYMATERIAL": + return - links.new(output.inputs['Surface'], bsdf.outputs['BSDF']) - links.new(bsdf.inputs['Base Color'], base_image_node.outputs['Color']) - links.new(bsdf.inputs['Alpha'], base_image_node.outputs['Alpha']) - links.new(base_image_node.inputs['Vector'], uvmap.outputs['UV']) + oldMat = currentSubMesh.material - if material.BaseTexture in bpy.data.images: - diffuse_texture = bpy.data.images[material.BaseTexture] - base_image_node.image = diffuse_texture + texName = currentSubMesh.material.BaseTexture + texName = texName[0:len(texName) - 4] + " Material" + if texName in bpy.data.materials and oldMat.shaderList.shaderList != bpy.data.materials.get(texName).shaderList.shaderList: + texName += "1" + mat = assign_material(texName) - if material.NormalTexture != 'None': - links.new(normal_image_node.outputs['Color'], normal_map_node.inputs['Color']) - links.new(normal_map_node.outputs['Normal'], bsdf.inputs['Normal']) - links.new(normal_image_node.inputs['Vector'], uvmap.outputs['UV']) + mat.shaderList.shaderList = oldMat.shaderList.shaderList - if material.NormalTexture in bpy.data.images: - normal_texture = bpy.data.images[material.NormalTexture] - normal_image_node.image = normal_texture - normal_image_node.image.colorspace_settings.name = 'Raw' + # TODO: Extract set_alamo_shader's shader finder to new function, use that here. + material_props = ["BaseTexture", "NormalTexture", "GlossTexture", "WaveTexture", "DistortionTexture", "CloudTexture", "CloudNormalTexture", "Emissive", "Diffuse", "Specular", "Shininess", "Colorization", "DebugColor", "UVOffset", "Color", "UVScrollRate", "DiffuseColor", + "EdgeBrightness", "BaseUVScale", "WaveUVScale", "DistortUVScale", "BaseUVScrollRate", "WaveUVScrollRate", "DistortUVScrollRate", "BendScale", "Diffuse1", "CloudScrollRate", "CloudScale", "SFreq", "TFreq", "DistortionScale", "Atmosphere", "CityColor", "AtmospherePower"] - # distribute nodes along the x axis - for index, node in enumerate((uvmap, base_image_node, bsdf, output)): - node.location.x = 200.0 * index + for texture in material_props: + if texture in oldMat: + mat[texture] = oldMat[texture] - normal_map_node.location = bsdf.location - normal_map_node.location.y += 300.0 + obj = bpy.context.object - output.location.x += 200.0 - bsdf.location.x += 200.0 + obj.data.materials.clear() + obj.data.materials.append(mat) + currentSubMesh.material = mat - normal_image_node.location = base_image_node.location - normal_image_node.location.y += 300.0 + def assign_material(name): + if name in bpy.data.materials: + return bpy.data.materials.get(name) + else: + return bpy.data.materials.new(name) def create_object(currentMesh): global mesh @@ -492,7 +682,7 @@ def create_object(currentMesh): if (currentMesh.collision == 1): object.HasCollision = True - #create vertex groups + # create vertex groups armature = utils.findArmature() for bone in armature.data.bones: vertgroup = object.vertex_groups.new(name=bone.name) @@ -505,7 +695,8 @@ def process_vertex_buffer_2(legacy, currentSubMesh): coX = f.unpack(file.read(4))[0] coY = f.unpack(file.read(4))[0] coZ = f.unpack(file.read(4))[0] - currentSubMesh.vertices.append(mathutils.Vector((coX, coY, coZ))) + currentSubMesh.vertices.append( + mathutils.Vector((coX, coY, coZ))) file.seek(12, 1) UV = [] UV.append(f.unpack(file.read(4))[0]) @@ -523,60 +714,74 @@ def process_index_buffer(currentSubMesh): counter = 0 while counter < currentSubMesh.nFaces: face = [] - face.append(h.unpack(file.read(2))[0] + currentSubMesh.faceOffset) - face.append(h.unpack(file.read(2))[0] + currentSubMesh.faceOffset) - face.append(h.unpack(file.read(2))[0] + currentSubMesh.faceOffset) + face.append(h.unpack(file.read(2))[ + 0] + currentSubMesh.faceOffset) + face.append(h.unpack(file.read(2))[ + 0] + currentSubMesh.faceOffset) + face.append(h.unpack(file.read(2))[ + 0] + currentSubMesh.faceOffset) currentSubMesh.faces.append(face) counter += 1 def process_texture_chunk(material): - file.seek(5, 1) # skip chunk size and child header - length = struct.unpack("H", file.read(1) + b'\x00') # get string length - global texture_function_name - texture_function_name = "" - counter = 0 - while counter < length[0] - 1: - letter = str(file.read(1)) - letter = letter[2:len(letter) - 1] - texture_function_name = texture_function_name + letter - counter += 1 - file.seek(1, 1) # skip string end byte - file.seek(1, 1) # skip child header - length = struct.unpack("H", file.read(1) + b'\x00') # get string length - texture_name = "" - counter = 0 - while counter < length[0] - 1: - letter = str(file.read(1)) - letter = letter[2:len(letter) - 1] - texture_name = texture_name + letter - counter += 1 - # replace texture format with .dds - if texture_name != "None": - texture_name = texture_name[0:len(texture_name) - 4] + ".dds" - file.seek(1, 1) # skip string end byte - - load_image(texture_name) - exec('material.' + texture_function_name + '= texture_name') + file.seek(5, 1) # skip chunk size and child header + length = struct.unpack("H", file.read( + 1) + b'\x00') # get string length + global texture_function_name + texture_function_name = "" + counter = 0 + while counter < length[0] - 1: + letter = str(file.read(1)) + letter = letter[2:len(letter) - 1] + texture_function_name = texture_function_name + letter + counter += 1 + file.seek(1, 1) # skip string end byte + file.seek(1, 1) # skip child header + length = struct.unpack("H", file.read(1) + b'\x00') # get string length + texture_name = "" + counter = 0 + while counter < length[0] - 1: + letter = str(file.read(1)) + letter = letter[2:len(letter) - 1] + texture_name = texture_name + letter + counter += 1 + # replace texture format with .dds + if texture_name != "None": + texture_name = texture_name[0:len(texture_name) - 4] + ".dds" + file.seek(1, 1) # skip string end byte + + load_image(texture_name) + exec('material.' + texture_function_name + '= texture_name') def createUVLayer(layerName, uv_coordinates): vert_uvs = uv_coordinates - mesh.uv_layers.new(name = layerName) - mesh.uv_layers[-1].data.foreach_set("uv", [uv for pair in [vert_uvs[l.vertex_index] for l in mesh.loops] for uv in pair]) + mesh.uv_layers.new(name=layerName) + mesh.uv_layers[-1].data.foreach_set( + "uv", [uv for pair in [vert_uvs[l.vertex_index] for l in mesh.loops] for uv in pair]) - def create_material(currentSubMesh): # create material and assign + def set_alamo_shader(currentSubMesh): # create material and assign shaderName = read_string() obj = bpy.context.object - mat = bpy.data.materials.new(obj.name + "Material") - #find shader, ignoring case + if shaderName == 'MeshCollision.fx': + mat = assign_material("COLLISION") + elif shaderName in ['RSkinShadowVolume.fx', 'MeshShadowVolume.fx']: + mat = assign_material("SHADOW") + else: + mat = assign_material("DUMMYMATERIAL") + # DUMMYMATERIAL is a temporary material to allow Alamo shader properties to be assigned. + # Can't assign final material because material names are now based on BaseTexture, and textures aren't known yet. Probably a better way to do this. + + # find shader, ignoring case currentKey = None for key in settings.material_parameter_dict: if(key.lower() == shaderName.lower()): currentKey = key break - if currentKey == None: - print("Warning: unknown shader: " + shaderName + " setting shader to alDefault.fx") + if currentKey is None: + self.report({"WARNING"}, "ALAMO - Unknown shader: " + shaderName + + " setting shader to alDefault.fx") currentKey = "alDefault.fx" mat.shaderList.shaderList = currentKey @@ -603,10 +808,11 @@ def assign_vertex_groups(animation_mapping, currentMesh): mod.use_vertex_groups = True while counter < n_vertices: - object.vertex_groups[animation_mapping[bone_indices[counter]]].add([counter], 1, 'ADD') + object.vertex_groups[animation_mapping[bone_indices[counter]]].add([ + counter], 1, 'ADD') counter += 1 - #proxy and connection functions + # proxy and connection functions def get_n_objects_n_proxies(): size = read_chunk_length() @@ -614,10 +820,11 @@ def get_n_objects_n_proxies(): n_objects = struct.unpack("l", file.read(4)) file.seek(2, 1) n_proxies = struct.unpack("l", file.read(4)) - n_objects_proxies = {"n_objects": n_objects[0], "n_proxies": n_proxies[0]} + n_objects_proxies = { + "n_objects": n_objects[0], "n_proxies": n_proxies[0]} - #some .alo formats have an additional unspecified value at this position - #to read the rest correctly this code checks if this is the case here and skips appropriately + # some .alo formats have an additional unspecified value at this position + # to read the rest correctly this code checks if this is the case here and skips appropriately size -= 12 file.seek(size, 1) @@ -630,9 +837,9 @@ def read_conncetion(armatureData, meshNameList): bone_index = struct.unpack("I", file.read(4))[0] armatureBlender = utils.findArmature() - #set connection of object to bone and move object to bone + # set connection of object to bone and move object to bone obj = None - if mesh_index < len(meshNameList): #light objects can mess this up + if mesh_index < len(meshNameList): # light objects can mess this up obj = bpy.data.objects[meshNameList[mesh_index]] bone = armatureBlender.data.bones[bone_index] if obj != None: @@ -680,24 +887,24 @@ def read_proxy(): bone.altDecreaseStayHidden = altDecreaseStayHidden bpy.ops.object.mode_set(mode='OBJECT') # go to Edit mode - #Utility functions + # Utility functions def read_chunk_length(): - #the hight bit is used to tell if chunk holds data or chunks, so if it is set it has to be ignored when calculating length + # the hight bit is used to tell if chunk holds data or chunks, so if it is set it has to be ignored when calculating length length = struct.unpack("= 2147483648: length -= 2147483648 return length def cut_string(string): - #bones have a 63 character limit, this function cuts longer strings with space for .xyz end used by blender to distinguish double name - if(len(string)> 63): + # bones have a 63 character limit, this function cuts longer strings with space for .xyz end used by blender to distinguish double name + if(len(string) > 63): return string[0:59] else: return string def read_string(): - #reads string out of chunk containing only a string + # reads string out of chunk containing only a string length = struct.unpack("I", file.read(4)) # get string length string = "" counter = 0 @@ -710,7 +917,7 @@ def read_string(): return string def read_string_mini_chunk(): - file.seek(1,1)#skip chunk header + file.seek(1, 1) # skip chunk header size = length = struct.unpack(" 0: + shader = object.data.materials[0].shaderList.shaderList + if shader in ['MeshShadowVolume.fx', 'RSkinShadowVolume.fx']: + bm = bmesh.new() # create an empty BMesh + bm.from_mesh(object.data) # fill it in from a Mesh + bm.verts.ensure_lookup_table() + + for vertex in bm.verts: + if not vertex.is_manifold: + # bm.free() + selectNonManifoldVertices(object) + error += [({'ERROR'}, f'ALAMO - Non manifold geometry shadow mesh: {object.name}')] + break + + for edge in bm.edges: + if len(edge.link_faces) < 2: + # bm.free() + selectNonManifoldVertices(object) + error += [({'ERROR'}, f'ALAMO - Non manifold geometry shadow mesh: {object.name}')] + break + + bm.free() + else: + error += [({'ERROR'}, f'ALAMO - Missing material on object: {object.name}')] + + return error + +def checkUV(object): # throws error if object lacks UVs + error = [] + for material in object.data.materials: + if material.shaderList.shaderList == 'MeshShadowVolume.fx' or material.shaderList.shaderList == 'RSkinShadowVolume.fx': + if len(object.data.materials) > 1: + error += [({'ERROR'}, f'ALAMO - Multiple materials on shadow volume: {object.name}; remove additional materials')] + if object.HasCollision: + if len(object.data.materials) > 1: + error += [({'ERROR'}, f'ALAMO - Multiple submeshes/materials on collision mesh: {object.name}; remove additional materials')] + if not object.data.uv_layers: # or material.shaderList.shaderList in settings.no_UV_Shaders: #currently UVs are needed for everything but shadows + error += [({'ERROR'}, f'ALAMO - Missing UV: {object.name}')] + + return error + +# throws error if armature modifier lacks rig, this would crash the exporter later and checks if skeleton in modifier doesn't match active skeleton +def checkInvalidArmatureModifier(object): + activeSkeleton = bpy.context.scene.ActiveSkeleton.skeletonEnum + error = [] + for modifier in object.modifiers: + if modifier.type == "ARMATURE": + if modifier.object is None: + error += [({'ERROR'}, f'ALAMO - Armature modifier without selected skeleton on: {object.name}')] + break + elif modifier.object.type != 'NoneType': + if modifier.object.name != activeSkeleton: + error += [({'ERROR'}, f"ALAMO - Armature modifier skeleton doesn't match active skeleton on: {object.name}")] + break + for constraint in object.constraints: + if ( + constraint.type == 'CHILD_OF' + and constraint.target is not None + and constraint.target.name != activeSkeleton + ): + error += [({'ERROR'}, f"ALAMO - Constraint doesn't match active skeleton on: {object.name}")] + break + + return error + +# checks if the number of faces exceeds max ushort, which is used to save the indices +def checkFaceNumber(object): + if len(object.data.polygons) > 65535: + return [({'ERROR'}, f'ALAMO - {object.name} exceeds maximum face limit; split mesh into multiple objects')] + return [] + +def checkAutosmooth(object): # prints a warning if Autosmooth is used + if object.data.use_auto_smooth: + return [({'ERROR'}, f'ALAMO - {object.name} uses autosmooth, ingame shading might not match blender; use edgesplit instead')] + return [] + +def checkTranslation(object): # prints warning when translation is not default + if object.location != mathutils.Vector((0.0, 0.0, 0.0)) or object.rotation_euler != mathutils.Euler((0.0, 0.0, 0.0), 'XYZ'): + return [({'ERROR'}, f'ALAMO - {object.name} is not aligned with the world origin; apply translation or bind to bone')] + return [] + +def checkScale(object): # prints warning when scale is not default + if object.scale != mathutils.Vector((1.0, 1.0, 1.0)): + return [({'ERROR'}, f'ALAMO - {object.name} has non-identity scale. Apply scale.')] + return [] + +# checks if vertices have 0 or > 1 groups +def checkVertexGroups(object): + if object.vertex_groups is None or len(object.vertex_groups) == 0: + return [] + for vertex in object.data.vertices: + total = 0 + for group in vertex.groups: + if group.weight not in [0, 1]: + return [({'ERROR'}, f'ALAMO - Object {object.name} has improper vertex groups')] + total += group.weight + if total not in [0, 1]: + return [({'ERROR'}, f'ALAMO - Object {object.name} has improper vertex groups')] + + return [] + +def checkNumBones(object): + if type(object) != type(None) and object.type == 'MESH': + material = object.active_material + if material is not None and material.shaderList.shaderList.find("RSkin") > -1: + used_groups = [] + for vertex in object.data.vertices: + for group in vertex.groups: + if group.weight == 1: + used_groups.append(group.group) + + if len(set(used_groups)) > 23: + return [({'ERROR'}, f'ALAMO - Object {object.name} has more than 23 bones.')] + return [] + +def checkTranslationArmature(): # prints warning when translation is not default + armature = utils.findArmature() + if armature is not None and ( + armature.location != mathutils.Vector((0.0, 0.0, 0.0)) + or armature.rotation_euler != mathutils.Euler((0.0, 0.0, 0.0), 'XYZ') + or armature.scale != mathutils.Vector((1.0, 1.0, 1.0)) + ): + return [({'ERROR'}, f'ALAMO - Armature {armature} is not aligned with the world origin; apply translation')] + return [] + +def checkProxyKeyframes(): + local_errors = [] + actions = bpy.data.actions + current_frame = bpy.context.scene.frame_current + armature = findArmature() + if armature is not None: + for action in actions: + print(action.name) + for fcurve in action.fcurves: + print(fcurve.group.name) + if fcurve.data_path.find("proxyIsHiddenAnimation") > -1: + previous_keyframe = None + for keyframe in fcurve.keyframe_points: + bpy.context.scene.frame_set(int(keyframe.co[0])) + # TODO Keyframes don't store what action they're from. Maybe check each keyframe against the current action? + this_keyframe = armature.path_resolve(fcurve.data_path) + print(previous_keyframe, this_keyframe) + if this_keyframe == previous_keyframe: + local_errors += [({'WARNING'}, f'ALAMO - {fcurve.group.name} has duplicate keyframe on frame {bpy.context.scene.frame_current}')] + previous_keyframe = this_keyframe + bpy.context.scene.frame_set(current_frame) + return local_errors + +def validate(mesh_list): + errors = [] + checklist = [ + checkShadowMesh, + checkUV, + checkFaceNumber, + checkAutosmooth, + checkTranslation, + checkInvalidArmatureModifier, + checkScale, + checkVertexGroups, + checkNumBones, + ] + checklist_no_object = [ + checkTranslationArmature, + # checkProxyKeyframes, # Disabled until it can be fixed + ] + + for check in checklist: + for object in mesh_list: + errors += check(object) + for check in checklist_no_object: + errors += check() + + return errors