diff --git a/loco-graphics-helper/__init__.py b/loco-graphics-helper/__init__.py index 214fd61..67b71a0 100644 --- a/loco-graphics-helper/__init__.py +++ b/loco-graphics-helper/__init__.py @@ -16,6 +16,7 @@ from .properties.general_properties import register_general_properties, unregister_general_properties from .properties.track_properties import register_track_properties, unregister_track_properties from .properties.object_properties import register_object_properties, unregister_object_properties +from .properties.file_versioning import register_file_updater, unregister_file_updater from .rct_graphics_helper_panel import GraphicsHelperPanel from . import developer_utils import importlib @@ -25,7 +26,7 @@ "name": "Loco Graphics Helper", "description": "Render tool to replicate Locomotion graphics (based on RCT Graphics Helper)", "author": "Olivier Wervers & OpenLoco Team", - "version": (0, 1, 6), + "version": (0, 1, 10), "blender": (2, 79, 0), "location": "Render", "support": "COMMUNITY", @@ -54,6 +55,7 @@ def register(): register_walls_properties() register_track_properties() register_object_properties() + register_file_updater() print("Registered {} with {} modules".format( bl_info["name"], len(modules))) @@ -71,5 +73,6 @@ def unregister(): unregister_walls_properties() unregister_track_properties() unregister_object_properties() + unregister_file_updater() print("Unregistered {}".format(bl_info["name"])) diff --git a/loco-graphics-helper/builders/task_builder.py b/loco-graphics-helper/builders/task_builder.py index 32ffec7..ac9c0b9 100644 --- a/loco-graphics-helper/builders/task_builder.py +++ b/loco-graphics-helper/builders/task_builder.py @@ -7,12 +7,24 @@ RCT Graphics Helper is licensed under the GNU General Public License version 3. ''' +import bpy from operator import length_hint from ..frame import Frame from ..render_task import RenderTask +from ..vehicle import get_vehicle_y_offset # Builder for creating render tasks procedurally +def get_offset_y(): + properties = bpy.context.scene.loco_graphics_helper_general_properties + if properties.render_mode == "TILES": + return 0 + elif properties.render_mode == "VEHICLE": + return get_vehicle_y_offset() + elif properties.render_mode == "WALLS": + return 0 + elif properties.render_mode == "TRACK": + return 0 class TaskBuilder: @@ -82,9 +94,14 @@ def add_frame(self, frame_index, number_of_viewing_angles, angle_index, animatio frame.set_target_object(target_object) + frame.set_offset_y(get_offset_y()) + self.angles.append(frame) self.output_index = self.output_index + 1 + def add_null_frames(self, number): + self.output_index += number + # Adds render angles for the given number of viewing angles relative to the currently configured rotation def add_viewing_angles(self, number_of_viewing_angles, animation_frame_index=0, animation_frames=1, rotational_symmetry=False): @@ -121,6 +138,8 @@ def add_viewing_angles(self, number_of_viewing_angles, animation_frame_index=0, frame.set_occlusion_layers(self.occlusion_layers) + frame.set_offset_y(get_offset_y()) + if self.occlusion_layers > 0: output_indices = [] for k in range(self.occlusion_layers): diff --git a/loco-graphics-helper/frame.py b/loco-graphics-helper/frame.py index e8040e1..5c02aeb 100644 --- a/loco-graphics-helper/frame.py +++ b/loco-graphics-helper/frame.py @@ -14,6 +14,14 @@ # Representation of a frame that is to be rendered +def recursive_hide_children(object, hide, type = 'NONE'): + object.hide_render = hide + for child in object.children: + if child.loco_graphics_helper_object_properties.object_type != 'NONE': + if type != 'NONE': + continue + recursive_hide_children(child, hide) + class Frame: def __init__(self, frame_index, task, view_angle, bank_angle=0, vertical_angle=0, mid_angle=0): self.frame_index = frame_index @@ -91,15 +99,13 @@ def prepare_scene(self): continue if o.loco_graphics_helper_object_properties.object_type == 'NONE': continue - o.hide_render = True - for c in o.children: - c.hide_render = True + recursive_hide_children(o,True) - self.target_object.hide_render = False - for c in self.target_object.children: - c.hide_render = False + recursive_hide_children(self.target_object,False, self.target_object.loco_graphics_helper_object_properties.object_type) object.location = self.target_object.matrix_world.translation + if self.target_object.loco_graphics_helper_vehicle_properties.bounding_box_override: + object.location = self.target_object.loco_graphics_helper_vehicle_properties.bounding_box_override.matrix_world.translation # This is a little hacky... if self.layer == 'Top Down Shadow': @@ -126,6 +132,9 @@ def set_offset(self, offset_x, offset_y): self.offset_x = offset_x self.offset_y = offset_y + def set_offset_y(self, offset_y): + self.offset_y = offset_y + def set_multi_tile_size(self, width, length, invert_tile_positions): self.width = width self.length = length diff --git a/loco-graphics-helper/loco_object_helper_panel.py b/loco-graphics-helper/loco_object_helper_panel.py index 370d1c7..2f777db 100644 --- a/loco-graphics-helper/loco_object_helper_panel.py +++ b/loco-graphics-helper/loco_object_helper_panel.py @@ -60,6 +60,15 @@ def draw_bogie_panel(self, context, layout): vehicle_properties = context.object.loco_graphics_helper_vehicle_properties + row.prop(vehicle_properties, "null_component") + row = layout.row() + + if vehicle_properties.null_component: + return + + row.prop(vehicle_properties, "index") + row = layout.row() + row.prop(vehicle_properties, "is_clone") row = layout.row() @@ -67,14 +76,15 @@ def draw_bogie_panel(self, context, layout): row = layout.row() if vehicle_properties.is_clone: - row.prop(vehicle_properties, "index",text="Clone of bogie index:") - row = layout.row() return + row.prop(vehicle_properties, "render_sprite") + row = layout.row() + box = layout.box() row = box.row() - row.label("Track Properties:") + row.label("Sprite:") split = box.split(.50) columns = [split.column(), split.column()] @@ -92,15 +102,14 @@ def draw_bogie_panel(self, context, layout): row.label("Sloped Viewing Angles: 32") row = layout.row() - row.prop(vehicle_properties, "index") - row = layout.row() - row.prop(vehicle_properties, "number_of_animation_frames") row = layout.row() row.prop(vehicle_properties, "rotational_symmetry") row = layout.row() + row.prop(vehicle_properties, "bounding_box_override") + def draw_body_panel(self, context, layout): scene = context.scene general_properties = scene.loco_graphics_helper_general_properties @@ -112,6 +121,15 @@ def draw_body_panel(self, context, layout): vehicle_properties = context.object.loco_graphics_helper_vehicle_properties + row.prop(vehicle_properties, "null_component") + row = layout.row() + + if vehicle_properties.null_component: + return + + row.prop(vehicle_properties, "index") + row = layout.row() + row.prop(vehicle_properties, "is_clone") row = layout.row() @@ -119,14 +137,15 @@ def draw_body_panel(self, context, layout): row = layout.row() if vehicle_properties.is_clone: - row.prop(vehicle_properties, "index",text="Clone of body index:") - row = layout.row() return - + + row.prop(vehicle_properties, "render_sprite") + row = layout.row() + box = layout.box() row = box.row() - row.label("Track Properties:") + row.label("Sprites:") split = box.split(.50) columns = [split.column(), split.column()] @@ -148,17 +167,14 @@ def draw_body_panel(self, context, layout): row.prop(vehicle_properties, "sloped_viewing_angles", text="") row = layout.row() - row.prop(vehicle_properties, "roll_angle") - row = layout.row() - - row.prop(vehicle_properties, "index") + row.prop(vehicle_properties, "tilt_angle") row = layout.row() row.prop(vehicle_properties, "number_of_animation_frames") row = layout.row() - if vehicle_properties.number_of_animation_frames != 1 and vehicle_properties.roll_angle != 0: - row.label("WARNING CANNOT HAVE BOTH ANIMATION FRAMES AND ROLL ANGLE SET") + if vehicle_properties.number_of_animation_frames != 1 and vehicle_properties.tilt_angle != 0: + row.label("WARNING: cannot have tilt frames and animation frames") row = layout.row() row.prop(vehicle_properties, "rotational_symmetry") @@ -167,8 +183,10 @@ def draw_body_panel(self, context, layout): row.prop(vehicle_properties, "braking_lights") row = layout.row() if vehicle_properties.braking_lights and vehicle_properties.roll_angle != 0: - row.label("WARNING CANNOT HAVE BOTH BRAKING LIGHTS AND ROLL ANGLE SET") + row.label("WARNING: cannot have brake lights and tilt frames") row = layout.row() row.prop(vehicle_properties, "is_airplane") row = layout.row() + + row.prop(vehicle_properties, "bounding_box_override") diff --git a/loco-graphics-helper/operators/init_operator.py b/loco-graphics-helper/operators/init_operator.py index 949d2be..eb3a2fb 100644 --- a/loco-graphics-helper/operators/init_operator.py +++ b/loco-graphics-helper/operators/init_operator.py @@ -78,6 +78,13 @@ def execute(self, context): compositorBuilder = CompositorBuilder() compositorBuilder.build(context) + # Set project properties + properties = context.scene.loco_graphics_helper_general_properties + addon_prefs = context.user_preferences.addons["loco-graphics-helper"].preferences + + properties.RCTPluginVersion = addon_prefs.RCTPluginVersion + properties.RCTPluginName = addon_prefs.printable_idname + return {'FINISHED'} def delete_default_render_layer(self, context): diff --git a/loco-graphics-helper/operators/vehicle_render_operator.py b/loco-graphics-helper/operators/vehicle_render_operator.py index bd85698..aaf2e30 100644 --- a/loco-graphics-helper/operators/vehicle_render_operator.py +++ b/loco-graphics-helper/operators/vehicle_render_operator.py @@ -13,6 +13,7 @@ from ..operators.render_operator import RCTRender from ..angle_sections.track import track_angle_sections, track_angle_sections_names +from ..vehicle import get_number_of_sprites class RenderVehicle(RCTRender, bpy.types.Operator): @@ -36,18 +37,17 @@ def create_task(self, context): self.task_builder.set_palette(self.palette_manager.get_base_palette( general_props.palette, general_props.number_of_recolorables, "FULL")) - bodies = [x for x in context.scene.objects if x.loco_graphics_helper_object_properties.object_type == "BODY" and not x.loco_graphics_helper_vehicle_properties.is_clone] + bodies = [x for x in context.scene.objects if x.loco_graphics_helper_object_properties.object_type == "BODY" and not x.loco_graphics_helper_vehicle_properties.is_clone and not x.loco_graphics_helper_vehicle_properties.null_component] bodies = sorted(bodies, key=lambda x: x.loco_graphics_helper_vehicle_properties.index) for body_object in bodies: self.add_render_angles(body_object) if bodies[0].loco_graphics_helper_vehicle_properties.is_airplane: - self.task_builder.set_cast_shadows( - True) + self.task_builder.set_cast_shadows(True) self.task_builder.set_palette(self.palette_manager.get_shadow_palette()) self.add_airplane_shadow_render_angles(bodies[0]) else: - bogies = [x for x in context.scene.objects if x.loco_graphics_helper_object_properties.object_type == "BOGIE" and not x.loco_graphics_helper_vehicle_properties.is_clone] + bogies = [x for x in context.scene.objects if x.loco_graphics_helper_object_properties.object_type == "BOGIE" and not x.loco_graphics_helper_vehicle_properties.is_clone and not x.loco_graphics_helper_vehicle_properties.null_component] bogies = sorted(bogies, key=lambda x: x.loco_graphics_helper_vehicle_properties.index) for bogie_object in bogies: self.add_render_angles(bogie_object) @@ -74,10 +74,13 @@ def should_render_feature(self, key, props): def add_render_angles(self, object): props = object.loco_graphics_helper_vehicle_properties + if not props.render_sprite: + self.task_builder.add_null_frames(get_number_of_sprites(object)) + return is_bogie = object.loco_graphics_helper_object_properties.object_type == "BOGIE" target_object = object animation_frames = props.number_of_animation_frames - roll_frames = 1 if props.roll_angle == 0 else 3 + tilt_frames = 1 if props.tilt_angle == 0 else 3 for i in range(len(track_angle_sections_names)): key = track_angle_sections_names[i] @@ -118,12 +121,12 @@ def add_render_angles(self, object): target_object = object for i in range(num_viewing_angles): - if roll_frames != 1: - roll_angles = [0, props.roll_angle, -props.roll_angle] - for j, roll_angle in enumerate(roll_angles): - frame_index = start_output_index + i * roll_frames + j + if tilt_frames != 1: + tilt_angles = [0, props.tilt_angle, -props.tilt_angle] + for j, tilt_angle in enumerate(tilt_angles): + frame_index = start_output_index + i * tilt_frames + j self.task_builder.set_rotation( - base_view_angle, roll_angle, vertical_angle=track_section[2]) + base_view_angle, tilt_angle, vertical_angle=track_section[2]) self.task_builder.add_frame( frame_index, num_viewing_angles, i, j, rotation_range, target_object) else: diff --git a/loco-graphics-helper/properties/file_versioning.py b/loco-graphics-helper/properties/file_versioning.py new file mode 100644 index 0000000..6244bc6 --- /dev/null +++ b/loco-graphics-helper/properties/file_versioning.py @@ -0,0 +1,81 @@ +''' +Copyright (c) 2024 RCT Graphics Helper developers + +For a complete list of all authors, please refer to the addon's meta info. +Interested in contributing? Visit https://github.com/oli414/Blender-RCT-Graphics + +RCT Graphics Helper is licensed under the GNU General Public License version 3. +''' + +import bpy +from bpy.app.handlers import persistent + +# Updating a project file to a newer version + +# each function updates the file to the version in the name +# if only new features were added, copy the format of version0 + +current_file_version = 4 + +def getAllComponents(): + return [x for x in bpy.context.scene.objects if x.loco_graphics_helper_object_properties.object_type != 'NONE'] + +class FileVersionUpdater: + # plugin version 0.1.6 + def version0(): + pass + + # plugin version 0.1.7 + # move component index from 1-index to 0-index + def version1(): + for object in getAllComponents(): + object.loco_graphics_helper_vehicle_properties.index -= 1 + + # plugin version 0.1.8 + # rename roll_angle to tilt_angle + def version2(): + for object in getAllComponents(): + if "roll_angle" in object.loco_graphics_helper_vehicle_properties: + object.loco_graphics_helper_vehicle_properties.tilt_angle = object.loco_graphics_helper_vehicle_properties["roll_angle"] + + # plugin version 0.1.9 + # add null component boolean + def version3(): + for object in getAllComponents(): + if object.loco_graphics_helper_vehicle_properties.index >= 255: + object.loco_graphics_helper_vehicle_properties.null_component = True + + # plugin version 0.1.10 + # add specific Y-offsets per entity type + def version4(): + bpy.context.scene.loco_graphics_helper_general_properties.y_offset += 17 + +update_functions = [getattr(FileVersionUpdater, func) for func in dir(FileVersionUpdater) if callable(getattr(FileVersionUpdater, func)) and not func.startswith("__")] + +def apply_update(): + print("loco-graphics-helper: Applying updates to file") + general_properties = bpy.context.scene.loco_graphics_helper_general_properties + addon_prefs = bpy.context.user_preferences.addons["loco-graphics-helper"].preferences + assert len(update_functions) == bpy.context.user_preferences.addons["loco-graphics-helper"].preferences.RCTPluginVersion + 1 + for i in range(int(general_properties.RCTPluginVersion)+1, int(addon_prefs.RCTPluginVersion)+1): + update_functions[i]() + general_properties.RCTPluginVersion = i + general_properties.RCTPluginName = addon_prefs.printable_idname + +@persistent +def check_for_update(_=None): + print("loco-graphics-helper: Checking for update") + general_properties = bpy.context.scene.loco_graphics_helper_general_properties + addon_prefs = bpy.context.user_preferences.addons["loco-graphics-helper"].preferences + if general_properties.RCTPluginName != addon_prefs.printable_idname: + return + if int(general_properties.RCTPluginVersion) >= int(addon_prefs.RCTPluginVersion): + return + print("File version is {}, plugin version is {}".format(general_properties.RCTPluginVersion, addon_prefs.RCTPluginVersion)) + apply_update() + +def register_file_updater(): + bpy.app.handlers.load_post.append(check_for_update) + +def unregister_file_updater(): + bpy.app.handlers.load_post.remove(check_for_update) diff --git a/loco-graphics-helper/properties/general_properties.py b/loco-graphics-helper/properties/general_properties.py index 19d3986..e2862c8 100644 --- a/loco-graphics-helper/properties/general_properties.py +++ b/loco-graphics-helper/properties/general_properties.py @@ -25,6 +25,16 @@ class GeneralProperties(bpy.types.PropertyGroup): script_file = os.path.realpath(__file__) directory = os.path.dirname(script_file) + RCTPluginName = bpy.props.StringProperty( + name="RCT Tools Type", + description="Which fork of RCTTools is this project for.", + default="unk") + + RCTPluginVersion = bpy.props.IntProperty( + name="RCT Tools Version", + description="What version of the fork this project is for. Number updates when a backwards-incompatible change is introduced.", + default=-1) + number_of_animation_frames = bpy.props.IntProperty( name="Animation Frames", description="Number of animation frames. For example in use for swinging, rotating or animated ride vehicles, animated rides, and animated scenery", @@ -112,26 +122,41 @@ class GeneralProperties(bpy.types.PropertyGroup): description="Whether or not the RCT add-on is currently rendering.", default=False) + transport_mode = bpy.props.EnumProperty( + name="Transport Mode", + items=( + ("RAIL","Rail","Railway vehicle",0), + ("ROAD","Road","Road or tram vehicle", 1), + ("AIR","Air","Aircraft", 2), + ("WATER","Water","Watercraft", 3) + ) + ) + + # currently unused build_gx = bpy.props.BoolProperty( name="Generate GX (optimized sprite file)", description="Whether or not to create a .dat sprite file. Having GXC installed is required.", default=False) + # currently unused build_assetpack = bpy.props.BoolProperty( name="Generate the asset pack file", description="Whether or not to the ORCT2 asset pack file", default=False) + # currently unused copy_assetpack_to_orct2 = bpy.props.BoolProperty( name="Copy to OpenRCT2", description="Copy the generated .graphics file to the ORCT2 assetpack folder.", default=False) + # currently unused build_parkobj = bpy.props.BoolProperty( name="Generate .parkobj file", description="Automatically build the .parkobj file. An object.json file with the object description is required in the output folder.", default=False) + # currently unused copy_parkobj_to_orct2 = bpy.props.BoolProperty( name="Copy to OpenRCT2", description="Copy the generated .parkobj file to the ORCT2 objects folder. Linking your OpenRCT2 Documents folder is required in the add-on preferences.", diff --git a/loco-graphics-helper/properties/preferences.py b/loco-graphics-helper/properties/preferences.py index 5c045de..1cc94d2 100644 --- a/loco-graphics-helper/properties/preferences.py +++ b/loco-graphics-helper/properties/preferences.py @@ -9,11 +9,19 @@ import bpy from bpy.types import AddonPreferences - +from .file_versioning import current_file_version class RCTGraphicsHelperPreferences(AddonPreferences): - bl_idname = "loco-graphics-helper" + printable_idname = "loco-graphics-helper" + bl_idname = printable_idname + + # make sure to add an updater to file_updater.py + RCTPluginVersion = bpy.props.IntProperty( + name="RCT Tools Version", + description="What version of the fork this project is for. Number updates when a backwards-incompatible change is introduced.", + default=current_file_version) + # currently unused orct2_directory = bpy.props.StringProperty( name="OpenRCT2 Path", description="The path to OpenRCT2. This should point to the directory that contains the object folder.", @@ -21,6 +29,7 @@ class RCTGraphicsHelperPreferences(AddonPreferences): subtype='DIR_PATH', default="") + # currently unused opengraphics_directory = bpy.props.StringProperty( name="OpenGraphics Repository Path", description="Root directory for the OpenGraphics repository, if available.", @@ -30,5 +39,5 @@ class RCTGraphicsHelperPreferences(AddonPreferences): def draw(self, context): layout = self.layout - layout.prop(self, "orct2_directory") - layout.prop(self, "opengraphics_directory") + col = layout.column() + col.label("""RCT Graphics Helper, "{}" Fork, file version {}""".format(self.bl_idname, self.RCTPluginVersion)) diff --git a/loco-graphics-helper/properties/vehicle_properties.py b/loco-graphics-helper/properties/vehicle_properties.py index b8e6166..2d34496 100644 --- a/loco-graphics-helper/properties/vehicle_properties.py +++ b/loco-graphics-helper/properties/vehicle_properties.py @@ -80,54 +80,70 @@ class VehicleProperties(bpy.types.PropertyGroup): default="32" ) - roll_angle = bpy.props.IntProperty( - name="Roll/Tilt Angle", - description="If non-zero will render a +angle -angle roll image", - default=0, - min=0) + tilt_angle = bpy.props.FloatProperty( + name="Tilt Angle", + description="Renders a left and right tilting sprite at the specified angle if non-zero", + default=0) index = bpy.props.IntProperty( - name="Body/Bogie Index", - description="Controls the order of the bodies/bogies", - default=1, - min=1) + name="Component Index", + description="Car/sub-component's index", + default=0, + min=0, + max=179) number_of_animation_frames = bpy.props.IntProperty( name="Animation Frames", - description="Number of animation frames. For example in use for animated wheels or cargo sprites", + description="Number of keyframed animation frames. Used for animated wheels and cargo", default=1, min=1) rotational_symmetry = bpy.props.BoolProperty( name="Rotational Symmetry", - description="If model is symmetrical when rotated around z access this will half the number of sprites rendered", + description="Component has 180-degree rotational symmetry. Reduces sprite count by half", default=False ) braking_lights = bpy.props.BoolProperty( name="Has Braking Lights", - description="If model has braking lights (located in layer 1) will render them", + description="Renders brake lights (layer 1)", default=False ) is_airplane = bpy.props.BoolProperty( name="Is an airplane", - description="If airplane will render airplane shadows (bogie)", + description="Renders airplane shadows", default=False ) is_clone = bpy.props.BoolProperty( - name="Is a clone of another bogie/body", - description="Clones will not be rendered and here just for show/location/meta data", + name="Is a duplicate of another sub-component", + description="Prevents rendering duplicate sprites for sub-components that are identical", default=False ) is_inverted = bpy.props.BoolProperty( - name="Direction is inverted", - description="Useful for clones to mark an inverted clone", + name="Component is reversed", + description="The car draws this sub-component facing backwards", + default=False + ) + + render_sprite = bpy.props.BoolProperty( + name="Render component", + description="Include this sub-component when batch rendering", + default=True + ) + + null_component = bpy.props.BoolProperty( + name="Null component", + description="This sub-component is not rendered in the game", default=False ) + bounding_box_override = bpy.props.PointerProperty( + type=bpy.types.Object, + name="BBox override", + description="Object to use when determining center of rotation and body parameters") def register_vehicles_properties(): bpy.types.Object.loco_graphics_helper_vehicle_properties = bpy.props.PointerProperty( diff --git a/loco-graphics-helper/rct_graphics_helper_panel.py b/loco-graphics-helper/rct_graphics_helper_panel.py index a456663..d1ddeaf 100644 --- a/loco-graphics-helper/rct_graphics_helper_panel.py +++ b/loco-graphics-helper/rct_graphics_helper_panel.py @@ -22,9 +22,11 @@ from .operators.render_tiles_operator import RenderTiles +from .properties.file_versioning import apply_update + from .models.palette import palette_colors, palette_colors_details -from .vehicle import get_car_components, VehicleComponent, SubComponent +from .vehicle import get_car_components, VehicleComponent, SubComponent, get_number_of_sprites, get_half_width class RepairConfirmOperator(bpy.types.Operator): """This action will clear out the default camera and light. Changes made to the rig object, compositor nodes and recolorable materials will be lost.""" @@ -43,6 +45,23 @@ def execute(self, context): def invoke(self, context, event): return context.window_manager.invoke_confirm(self, event) +class UpdateConfirmOperator(bpy.types.Operator): + """This action will perform the necessary updates to upgrade the file from plugin version 0.1.6 to current.""" + bl_idname = "loco_graphics_helper.update_from_prehistoric" + bl_label = "Perform updates" + bl_options = {'REGISTER', 'INTERNAL'} + + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + apply_update() + return {'FINISHED'} + + def invoke(self, context, event): + return context.window_manager.invoke_confirm(self, event) + class GraphicsHelperPanel(bpy.types.Panel): bl_label = "Loco Graphics Helper" bl_idname = "VIEW3D_PT_loco_graphics_helper" @@ -64,6 +83,27 @@ def draw(self, context): # General properties properties = scene.loco_graphics_helper_general_properties + addon_prefs = context.user_preferences.addons["loco-graphics-helper"].preferences + + col = layout.column() + col.label("File made with {}".format(properties.RCTPluginName)) + col.label("File version {}".format(properties.RCTPluginVersion)) + if properties.RCTPluginName == addon_prefs.printable_idname and properties.RCTPluginVersion > addon_prefs.RCTPluginVersion: + box = layout.box() + col = box.column() + col.label("WARNING: file was made with a") + col.label("newer version of this plugin!".format(properties.RCTPluginVersion)) + col.label("This plugin version: {}".format(addon_prefs.RCTPluginVersion)) + if properties.RCTPluginVersion == -1 and properties.RCTPluginName == "unk": + box = layout.box() + col = box.row() + col.label("Update from {} version 0.1.6?".format(addon_prefs.printable_idname)) + col.operator("loco_graphics_helper.update_from_prehistoric", text="Update") + elif properties.RCTPluginName != addon_prefs.printable_idname: + box = layout.box() + col = box.column() + col.label("WARNING: file was made for {} plugin".format(properties.RCTPluginName)) + col.label("this is the {} plugin".format(addon_prefs.printable_idname)) row = layout.row() row.separator() @@ -137,24 +177,6 @@ def draw(self, context): elif properties.render_mode == "TRACK": self.draw_track_panel(scene, box) - row = layout.row() - row.prop(properties, "build_gx") - - if properties.build_gx: - box = layout.box() - box.prop(properties, "build_assetpack") - - if properties.build_assetpack: - box2 = box.box() - box2.prop(properties, "copy_assetpack_to_orct2") - - row = layout.row() - row.prop(properties, "build_parkobj") - - if properties.build_parkobj: - box = layout.box() - box.prop(properties, "copy_parkobj_to_orct2") - def draw_tiles_panel(self, scene, layout): properties = scene.loco_graphics_helper_static_properties general_properties = scene.loco_graphics_helper_general_properties @@ -219,143 +241,140 @@ def draw_track_panel(self, scene, layout): def blender_to_loco_dist(dist): return int(dist * 32 + 0.5) + @staticmethod + def calculatePrecision(x): + return [y for y in range(8) if (1 << y) == int(x)][0] - 2 + def draw_vehicle_panel(self, scene, layout): general_properties = scene.loco_graphics_helper_general_properties + row = layout.row() + row.prop(general_properties,"transport_mode") + cars = [x for x in scene.objects if x.loco_graphics_helper_object_properties.object_type == "CAR"] cars = sorted(cars, key=lambda x: x.loco_graphics_helper_vehicle_properties.index) total_number_of_sprites = 0 + renderable_sprites = 0 components = get_car_components(cars) if len(components) == 0: col = layout.column() col.label(text="No cars detected.") - col.label(text="Ensure at least one BODY is parented to a CAR") - return - row = layout.row() - row.label("Car(s) details:") - - for component in components: - front = component.get_object(SubComponent.FRONT) - back = component.get_object(SubComponent.BACK) - body = component.get_object(SubComponent.BODY) - idx = body.loco_graphics_helper_vehicle_properties.index - - front_position = 0 - back_position = 0 - body_idx = idx - 1 + 180 if body.loco_graphics_helper_vehicle_properties.is_inverted else idx - 1 - front_idx = 255 - back_idx = 255 - warning = None - anim_location = 0 - front_name = '' if front is None else front.name - back_name = '' if back is None else back.name - mid_point_x = component.get_preferred_body_midpoint() - if not math.isclose(body.matrix_world.translation[0], mid_point_x, rel_tol=1e-4): - warning = "BODY LOCATION IS NOT AT PREFERRED MID X POINT! {}".format(round(mid_point_x,1)) - - if not front is None: - front_position = component.get_bogie_position(SubComponent.FRONT) - back_position = component.get_bogie_position(SubComponent.BACK) - - if component.get_number_of_sprites(SubComponent.FRONT) != 0: - front_idx = front.loco_graphics_helper_vehicle_properties.index - 1 - front_idx = front_idx + 180 if front.loco_graphics_helper_vehicle_properties.is_inverted else front_idx - - if component.get_number_of_sprites(SubComponent.BACK) != 0: - back_idx = back.loco_graphics_helper_vehicle_properties.index - 1 - back_idx = back_idx + 180 if front.loco_graphics_helper_vehicle_properties.is_inverted else back_idx - - anim_location = component.get_animation_location() - if anim_location > 255 or anim_location < 0: - warning = "Animation is too far from bogies" - anim_location = 255 - elif body.loco_graphics_helper_vehicle_properties.is_airplane: - front_idx = 0 - - row = layout.row() - row.label("{}. {}, {}, {}, {}".format(component.car.loco_graphics_helper_vehicle_properties.index - 1, component.car.name, body.name, front_name, back_name)) - row = layout.row() - row.label(" Front Position: {}".format(self.blender_to_loco_dist(front_position))) - row = layout.row() - row.label(" Back Position: {}".format(self.blender_to_loco_dist(back_position))) - row = layout.row() - row.label(" Front Bogie Sprite Index: {}".format(front_idx)) - row = layout.row() - row.label(" Back Bogie Sprite Index: {}".format(back_idx)) - row = layout.row() - row.label(" Body Sprite Index: {}".format(body_idx)) - row = layout.row() - row.label(" Animation Position: {}".format(anim_location)) + col.label(text="Ensure at least one body is parented to a car") + else: + for component in components: + front = component.get_object(SubComponent.FRONT) + back = component.get_object(SubComponent.BACK) + body = component.get_object(SubComponent.BODY) + + front_position = -1.0/32 + back_position = -1.0/32 + body_idx = component.get_component_index(SubComponent.BODY) + front_idx = component.get_component_index(SubComponent.FRONT) + back_idx = component.get_component_index(SubComponent.BACK) + warning = None + anim_location = 0 + front_name = '' if front is None else front.name + back_name = '' if back is None else back.name + mid_point_x = component.get_preferred_body_midpoint() + if body.loco_graphics_helper_vehicle_properties.bounding_box_override is None and not math.isclose(body.matrix_world.translation[0], mid_point_x, rel_tol=1e-4): + warning = "Body location is not at midpoint, off by {}".format(mid_point_x) + + if not front is None: + front_position = component.get_bogie_position(SubComponent.FRONT) + if not back is None: + back_position = component.get_bogie_position(SubComponent.BACK) + + anim_location = component.get_emitter_x() + if not anim_location is None and (anim_location > 255 or anim_location < 0): + warning = "Emitter is too far from bogies" + anim_location = 255 + elif body.loco_graphics_helper_vehicle_properties.is_airplane: + front_idx = 0 + back_idx = 255 + front_position = 0 + back_position = 0 + + box = layout.box() + box.label("Car {}: {}".format(component.car.loco_graphics_helper_vehicle_properties.index, component.car.name)) + col = box.column() + col.label("{}, {}, {}".format(body.name, front_name, back_name)) + col.label(" Front Position: {}".format(self.blender_to_loco_dist(front_position))) + col.label(" Back Position: {}".format(self.blender_to_loco_dist(back_position))) + col.label(" Front Bogie Sprite Index: {}".format(front_idx)) + col.label(" Back Bogie Sprite Index: {}".format(back_idx)) + col.label(" Body Sprite Index: {}".format(body_idx)) + if not anim_location is None: + col.label(" Emitter Horizontal Position: {}".format(anim_location)) + + if not warning is None: + row = box.row() + row.label(" WARNING: {},".format(warning)) + + bodies = [x for x in scene.objects if x.loco_graphics_helper_object_properties.object_type == "BODY" and not x.loco_graphics_helper_vehicle_properties.is_clone and get_number_of_sprites(x) > 0] + bodies = sorted(bodies, key=lambda x: x.loco_graphics_helper_vehicle_properties.index) + + if len(bodies) > 0: + for body in bodies: + number_of_sprites = get_number_of_sprites(body) + total_number_of_sprites += number_of_sprites + + half_width = -1.0/32 + car = None + if body.loco_graphics_helper_vehicle_properties.bounding_box_override: + half_width = get_half_width(body.loco_graphics_helper_vehicle_properties.bounding_box_override) + for component in components: + if component.body == body: + car = component + half_width = component.get_half_width() + break + emitter_z = car.get_emitter_z() + + if number_of_sprites == 0: + continue + + if body.loco_graphics_helper_vehicle_properties.render_sprite: + renderable_sprites += number_of_sprites + + box = layout.box() + row = box.row() + row.label("Body {}: {}".format(body.loco_graphics_helper_vehicle_properties.index, body.name)) + row.prop(body.loco_graphics_helper_vehicle_properties, "render_sprite") + col = box.column() + col.label(" Tilt Frames: {}".format(3 if body.loco_graphics_helper_vehicle_properties.tilt_angle != 0 else 1)) + col.label(" Half-Length: {}{}".format(self.blender_to_loco_dist(half_width), "" if body.loco_graphics_helper_vehicle_properties.bounding_box_override else " using bounding box override")) + col.label(" Flat Yaw Accuracy: {}".format(self.calculatePrecision(body.loco_graphics_helper_vehicle_properties.flat_viewing_angles))) + col.label(" Sloped Yaw Accuracy: {}".format(self.calculatePrecision(body.loco_graphics_helper_vehicle_properties.sloped_viewing_angles))) + col.label(" Number of sprites: {}".format(number_of_sprites)) + if not emitter_z is None: + col.label(" Emitter Vertical Position: {}".format(emitter_z)) + + bogies = [x for x in scene.objects if x.loco_graphics_helper_object_properties.object_type == "BOGIE" and not x.loco_graphics_helper_vehicle_properties.is_clone and get_number_of_sprites(x) > 0] + bogies = sorted(bogies, key=lambda x: x.loco_graphics_helper_vehicle_properties.index) - if not warning is None: - row = layout.row() - row.label(" WARNING: {},".format(warning)) + if len(bogies) > 0: + for bogie in bogies: + number_of_sprites = get_number_of_sprites(bogie) + total_number_of_sprites += number_of_sprites - row = layout.row() - row.label("Body(s) details:") - components = sorted(components, key=lambda x: x.body.loco_graphics_helper_vehicle_properties.index) - for component in components: - body = component.body - if body is None: - continue - if body.loco_graphics_helper_vehicle_properties.is_clone: - continue - number_of_sprites = component.get_number_of_sprites(SubComponent.BODY) - total_number_of_sprites = total_number_of_sprites + number_of_sprites - - if number_of_sprites == 0: - continue - - half_width = component.get_half_width() - row = layout.row() - row.label("{}. {}".format(body.loco_graphics_helper_vehicle_properties.index, body.name)) - row = layout.row() - row.label(" Half-Width: {}".format(self.blender_to_loco_dist(half_width))) - row = layout.row() - row.label(" Number of sprites: {}".format(number_of_sprites)) + if bogie.loco_graphics_helper_vehicle_properties.render_sprite: + renderable_sprites += number_of_sprites + + box = layout.box() + row = box.row() + row.label("Bogie {}: {}".format(bogie.loco_graphics_helper_vehicle_properties.index, bogie.name)) + row.prop(bogie.loco_graphics_helper_vehicle_properties, "render_sprite") + col = box.column() + col.label(" Number of sprites: {}".format(number_of_sprites)) - bogies = [x for x in scene.objects if x.loco_graphics_helper_object_properties.object_type == "BOGIE" and not x.loco_graphics_helper_vehicle_properties.is_clone] - bogies = sorted(bogies, key=lambda x: x.loco_graphics_helper_vehicle_properties.index) - - row = layout.row() - row.label("Bogie(s) details:") - for bogie in bogies: - car = None - sub_component = None - for component in components: - if component.front == bogie: - car = component - sub_component = SubComponent.FRONT - break - if component.back == bogie: - car = component - sub_component = SubComponent.BACK - break - if car is None: - continue - - number_of_sprites = car.get_number_of_sprites(sub_component) - total_number_of_sprites = total_number_of_sprites + number_of_sprites - - if number_of_sprites == 0: - continue - - half_width = component.get_half_width() - row = layout.row() - row.label("{}. {}".format(bogie.loco_graphics_helper_vehicle_properties.index, bogie.name)) - row = layout.row() - row.label(" Number of sprites: {}".format(number_of_sprites)) - row = layout.row() row.label("Total number of sprites: {}".format(total_number_of_sprites)) - - if total_number_of_sprites == 0: - row = layout.row() - row.label("NO BODIES OR BOGIES SET!") - row = layout.row() - row.label("NOTHING WILL BE RENDERED!") + row = layout.row() + if renderable_sprites > 0: + row.label("Sprites to render: {}".format(renderable_sprites)) + else: + row.label("WARNING: 0 sprites to render") row = layout.row() text = "Render" diff --git a/loco-graphics-helper/res/palettes/base_loco_palette.png b/loco-graphics-helper/res/palettes/base_loco_palette.png new file mode 100644 index 0000000..21380ed Binary files /dev/null and b/loco-graphics-helper/res/palettes/base_loco_palette.png differ diff --git a/loco-graphics-helper/res/palettes/custom_palette.bmp b/loco-graphics-helper/res/palettes/custom_palette.bmp new file mode 100644 index 0000000..fab08c1 Binary files /dev/null and b/loco-graphics-helper/res/palettes/custom_palette.bmp differ diff --git a/loco-graphics-helper/res/palettes/recolor_1_loco_palette.bmp b/loco-graphics-helper/res/palettes/recolor_1_loco_palette.bmp new file mode 100644 index 0000000..df7c18f Binary files /dev/null and b/loco-graphics-helper/res/palettes/recolor_1_loco_palette.bmp differ diff --git a/loco-graphics-helper/res/palettes/recolour_2_loco_palette.png b/loco-graphics-helper/res/palettes/recolour_2_loco_palette.png new file mode 100644 index 0000000..9bb893c Binary files /dev/null and b/loco-graphics-helper/res/palettes/recolour_2_loco_palette.png differ diff --git a/loco-graphics-helper/vehicle.py b/loco-graphics-helper/vehicle.py index 6bf383f..5132b88 100644 --- a/loco-graphics-helper/vehicle.py +++ b/loco-graphics-helper/vehicle.py @@ -17,6 +17,76 @@ class SubComponent(Enum): BACK = 1 BODY = 2 +def get_vehicle_y_offset(): + additional_offsets = { + "RAIL":0, + "ROAD":0, + "AIR":0, + "WATER":0, + } + return -17 + additional_offsets[bpy.context.scene.loco_graphics_helper_general_properties.transport_mode] + +def get_number_of_sprites(object): + + is_bogie = object.loco_graphics_helper_object_properties.object_type == "BOGIE" + props = object.loco_graphics_helper_vehicle_properties + + if props.null_component: + return 0 + + multiplier = props.number_of_animation_frames + if props.tilt_angle != 0: + multiplier = 3 + elif props.braking_lights: + multiplier = multiplier + 1 + if props.rotational_symmetry: + multiplier = multiplier / 2 + + num_transition_sprites = 0 if is_bogie else 4 + 4 + num_sprites = 0 + if props.sprite_track_flags[0]: + num_sprites = int(props.flat_viewing_angles) * multiplier + if props.sprite_track_flags[1]: + num_sprites = num_sprites + (int(props.sloped_viewing_angles) * 2 + num_transition_sprites) * multiplier + if props.sprite_track_flags[2]: + num_sprites = num_sprites + (int(props.sloped_viewing_angles) * 2 + num_transition_sprites) * multiplier + + if props.is_airplane: + num_sprites = num_sprites + int(props.flat_viewing_angles) * multiplier / 2 + return int(num_sprites) + +def _get_min_max_axis_bound_box_corners(object, axis): + bbox_corners = [object.matrix_world * Vector(corner) for corner in object.bound_box] + min_x = min([x[axis] for x in bbox_corners]) + max_x = max([x[axis] for x in bbox_corners]) + return (min_x, max_x) + +def _get_min_max_axis_bound_box_corners_with_children(object, axis): + mins = [] + maxs = [] + min_x, max_x = _get_min_max_axis_bound_box_corners(object, axis) + # This can happen if there are no dimensions to this object (or if its 0 width) + if min_x != max_x: + mins.append(min_x) + maxs.append(max_x) + + for c in object.children: + min_x, max_x = _get_min_max_axis_bound_box_corners_with_children(c, axis) + if min_x != max_x: + mins.append(min_x) + maxs.append(max_x) + if len(mins) == 0 or len(maxs) == 0: + return (0, 0) + + return (min(mins), max(maxs)) + +def get_half_width(object): + body_min_x, body_max_x = _get_min_max_axis_bound_box_corners_with_children(object, 0) + min_x = object.matrix_world.translation[0] - body_min_x + max_x = body_max_x - object.matrix_world.translation[0] + + return max(min_x, max_x) + class VehicleComponent: def __init__(self, car, front, back, body, animations = None): self.car = car @@ -35,8 +105,16 @@ def get_object(self, sub_component: SubComponent): SubComponent.BODY.value:self.body, } return object_mapping[sub_component.value] - - + + def get_component_index(self, sub_component: SubComponent): + object = self.get_object(sub_component) + if object == None: + return 255 + props = object.loco_graphics_helper_vehicle_properties + if props.null_component: + return 255 + return props.index + 180 * props.is_inverted + def has_sprites(self, sub_component: SubComponent): object = self.get_object(sub_component) if object is None: @@ -44,7 +122,8 @@ def has_sprites(self, sub_component: SubComponent): props = object.loco_graphics_helper_vehicle_properties if props.is_clone: return False - + # render_sprites == False does NOT return false here + if all(v == 0 for v in props.sprite_track_flags): return False @@ -52,61 +131,13 @@ def has_sprites(self, sub_component: SubComponent): def get_number_of_sprites(self, sub_component: SubComponent): object = self.get_object(sub_component) - - is_bogie = object.loco_graphics_helper_object_properties.object_type == "BOGIE" - props = object.loco_graphics_helper_vehicle_properties - - multiplier = props.number_of_animation_frames - if props.roll_angle != 0: - multiplier = 3 - elif props.braking_lights: - multiplier = multiplier + 1 - if props.rotational_symmetry: - multiplier = multiplier / 2 - - num_transition_sprites = 0 if is_bogie else 4 + 4 - num_sprites = 0 - if props.sprite_track_flags[0]: - num_sprites = int(props.flat_viewing_angles) * multiplier - if props.sprite_track_flags[1]: - num_sprites = num_sprites + (int(props.sloped_viewing_angles) * 2 + num_transition_sprites) * multiplier - if props.sprite_track_flags[2]: - num_sprites = num_sprites + (int(props.sloped_viewing_angles) * 2 + num_transition_sprites) * multiplier - - if props.is_airplane: - num_sprites = num_sprites + int(props.flat_viewing_angles) * multiplier / 2 - return int(num_sprites) + return get_number_of_sprites(object) def _get_min_max_x_bound_box_corners_with_children(self, object): - return self._get_min_max_axis_bound_box_corners_with_children(object, 0) + return _get_min_max_axis_bound_box_corners_with_children(object, 0) def _get_min_max_z_bound_box_corners_with_children(self, object): - return self._get_min_max_axis_bound_box_corners_with_children(object, 2) - - def _get_min_max_axis_bound_box_corners_with_children(self, object, axis): - mins = [] - maxs = [] - min_x, max_x = self._get_min_max_axis_bound_box_corners(object, axis) - # This can happen if there are no dimensions to this object (or if its 0 width) - if min_x != max_x: - mins.append(min_x) - maxs.append(max_x) - - for c in object.children: - min_x, max_x = self._get_min_max_axis_bound_box_corners_with_children(c, axis) - if min_x != max_x: - mins.append(min_x) - maxs.append(max_x) - if len(mins) == 0 or len(maxs) == 0: - return (0, 0) - - return (min(mins), max(maxs)) - - def _get_min_max_axis_bound_box_corners(self, object, axis): - bbox_corners = [object.matrix_world * Vector(corner) for corner in object.bound_box] - min_x = min([x[axis] for x in bbox_corners]) - max_x = max([x[axis] for x in bbox_corners]) - return (min_x, max_x) + return _get_min_max_axis_bound_box_corners_with_children(object, 2) def get_half_width(self): mins = [] @@ -143,22 +174,38 @@ def get_preferred_body_midpoint(self): def get_bogie_position(self, sub_component: SubComponent): assert sub_component != SubComponent.BODY - body_x = self.body.location[0] + body_x = self.body.matrix_world.translation[0] + half_width = self.get_half_width() + bounding_box = self.body.loco_graphics_helper_vehicle_properties.bounding_box_override + if bounding_box: + body_x = bounding_box.matrix_world.translation[0] + half_width = get_half_width(bounding_box) bogie = self.get_object(sub_component) - bogie_x = bogie.location[0] + bogie_x = bogie.matrix_world.translation[0] position_from_centre = max(body_x, bogie_x) - min(body_x, bogie_x) - return self.get_half_width() - position_from_centre - - def get_animation_location(self): + return half_width - position_from_centre + + def get_emitter_x(self): if len(self.animations) == 0: - return 0 - x_diff = self.front.location[0] - self.back.location[0] - print("front_x {} back_x {} anim_x {}".format(self.front.location[0], self.back.location[0], self.animations[0].location[0])) + return None + x_diff = self.front.matrix_world.translation[0] - self.back.matrix_world.translation[0] + # print("front_x {} back_x {} anim_x {}".format(self.front.location[0], self.back.location[0], self.animations[0].location[0])) x_factor = (1 / x_diff) - anim_diff = self.front.location[0] - self.animations[0].location[0] + anim_diff = self.front.matrix_world.translation[0] - self.animations[0].matrix_world.translation[0] anim_factor = (anim_diff * x_factor) * 128 return int(anim_factor) + 64 + def get_emitter_z(self): + if len(self.animations) == 0: + return None + target_object = self.front if not self.front is None else self.body + body_z = target_object.matrix_world.translation[2] + bounding_box = target_object.loco_graphics_helper_vehicle_properties.bounding_box_override + if bounding_box: + body_z = bounding_box.matrix_world.translation[2] + emitter_z = self.animations[0].matrix_world.translation[2] + return int((emitter_z - body_z) * 8) + 8 # I don't know if this is steam specific + def get_car_components(cars) -> List[VehicleComponent]: components = []