diff --git a/loco-graphics-helper/__init__.py b/loco-graphics-helper/__init__.py index 214fd61..14d7ce6 100644 --- a/loco-graphics-helper/__init__.py +++ b/loco-graphics-helper/__init__.py @@ -16,6 +16,8 @@ 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.track_piece_properties import register_track_piece_properties, unregister_track_piece_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 +27,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, 2, 1), "blender": (2, 79, 0), "location": "Render", "support": "COMMUNITY", @@ -54,6 +56,8 @@ def register(): register_walls_properties() register_track_properties() register_object_properties() + register_track_piece_properties() + register_file_updater() print("Registered {} with {} modules".format( bl_info["name"], len(modules))) @@ -71,5 +75,7 @@ def unregister(): unregister_walls_properties() unregister_track_properties() unregister_object_properties() + unregister_track_piece_properties() + unregister_file_updater() print("Unregistered {}".format(bl_info["name"])) diff --git a/loco-graphics-helper/angle_sections/track.py b/loco-graphics-helper/angle_sections/track.py index 726e9dd..b3763c4 100644 --- a/loco-graphics-helper/angle_sections/track.py +++ b/loco-graphics-helper/angle_sections/track.py @@ -1,12 +1,14 @@ ''' -Copyright (c) 2022 RCT Graphics Helper developers +Copyright (c) 2025 Loco 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 +Interested in contributing? Visit https://github.com/OpenLoco/Blender-Loco-Graphics -RCT Graphics Helper is licensed under the GNU General Public License version 3. +Loco Graphics Helper is licensed under the GNU General Public License version 3. ''' +from enum import Enum + track_angle_sections_names = [ "VEHICLE_SPRITE_FLAG_FLAT", "VEHICLE_SPRITE_FLAG_GENTLE_SLOPES", @@ -31,3 +33,291 @@ [False, 4, -22.2052] ], } + +# note to self: images are ordered by sequence FIRST then angle SECOND +""" +layer_names = [ + ["Model"], + ["Pickup Icon", "Placement Icon"], + ["Ballast", "Ties", "Rails"] +] + +layer_descriptions = [ + ["Icon Model"], + ["Icon, drawn as train pickup button", "Icon, drawn as train placement button"], + ["Ballast, drawn above terrain or bridge. Sloped tracks use only this layer, sloped tram track still use all three.", "Ties, drawn above ballast", "Rails, drawn above ties"], +] +""" + +max_layers = 3 + +track_layer_names = [ + ("Spinning Icon","Icon used for tab icon in track building and vehicle purchase windows"), + ("Pickup Icon", "Icon for pick up vehicle button"), + ("Placement Icon", "Icon for place vehicle button"), + ("Ballast","Ballast is drawn above surface and below all other layers"), + ("Ties","Ties is drawn above ballast and below rail and road layer"), + ("Rails", "Rails is drawn above ties and road layer"), + ("Combined", "") +] + +road_layer_names = [ + ("Spinning Icon","Icon used for tab icon in track building and vehicle purchase windows"), + ("Pickup Icon", "Icon for pick up vehicle button"), + ("Placement Icon", "Icon for place vehicle button"), + ("Road", "Road is drawn above ties and below rails layer"), + ("Road", "Road is drawn above ties and below rails layer"), + ("Road", "Road is drawn above ties and below rails layer"), + ("Road", "Road is drawn above ties and below rails layer") +] + +class TrackType(Enum): + RAIL = 0 + ROAD = 1 + TRAM = 2 + +# these are not the OpenLoco track piece IDs. These are the index of the piece in the manifest +# the enum names and number of enum items must be the same between the two +# Values of -1 do not show up in blender enum properties. This is used to filter invalid entries +class TrackPieceType(Enum): + NONE = 0 + PREVIEW = 1 + PICKUP = 2 + PLACE = 3 + STRAIGHT = 4 + ROAD_CURVE_VERY_SMALL = -1 + T_INTERSECTION = -1 + FOUR_WAY_INTERSECTION = -1 + CURVE_SMALL = 8 + CURVE_SMALL_SLOPE_SHALLOW_UP = 9 + CURVE_SMALL_SLOPE_SHALLOW_DOWN = 10 + CURVE_SMALL_SLOPE_STEEP_UP = 11 + CURVE_SMALL_SLOPE_STEEP_DOWN = 12 + CURVE = 13 + SLOPE_SHALLOW = 14 + SLOPE_STEEP = 15 + CURVE_DIAG = 16 + DIAG_STRAIGHT = 17 + S_BEND = 18 + TRACK_CURVE_VERY_SMALL = 19 + +class RoadPieceType(Enum): + NONE = 0 + PREVIEW = 1 + PICKUP = 2 + PLACE = 3 + STRAIGHT = 4 + ROAD_CURVE_VERY_SMALL = 5 + T_INTERSECTION = 6 + FOUR_WAY_INTERSECTION = 7 + CURVE_SMALL = 8 + CURVE_SMALL_SLOPE_SHALLOW_UP = -1 + CURVE_SMALL_SLOPE_SHALLOW_DOWN = -1 + CURVE_SMALL_SLOPE_STEEP_UP = -1 + CURVE_SMALL_SLOPE_STEEP_DOWN = -1 + CURVE = -1 + SLOPE_SHALLOW = 14 + SLOPE_STEEP = 15 + CURVE_DIAG = -1 + DIAG_STRAIGHT = -1 + S_BEND = -1 + TRACK_CURVE_VERY_SMALL = -1 + +default_manifest = { + "name": "default track type", + "angles": 4, + "symmetric": False, + "render_mirror": False, + "grid_size": [1,1], + "subposition_order": [0], + "subposition_y_offset": None, + "sprite_type": "NULL", + "canvas_size": None, + "camera_world_offset": [0.5, 0 , 0], # tile X, tile Y, smallZ + "base_layer_name": 3 +} + +class TrackPieceManifest: + track_piece = 0 + name= "null track type" + angles = 4 + symmetric = False + render_mirror = False + grid_size= [1,1] + subposition_order = [0] + subposition_y_offset = [0] + sprite_type = "NULL" + canvas_size = None + camera_world_offset = [0,0,0] + base_layer_name = 3 + + def __init__(self, input, piece): + self.track_piece = piece + for key in default_manifest.keys(): + if key in input: + setattr(self, key, input[key]) + if self.canvas_size == None: + self.canvas_size = [max(self.grid_size[0],self.grid_size[1])*64 for _ in range(2)] + if self.subposition_y_offset == None: + self.subposition_y_offset = [0 for _ in range(len(self.subposition_order))] + def get_sprites_per_layer(self): + sprites = self.angles / (2 if self.symmetric else 1) + sprites *= len(self.subposition_order) + sprites *= 2 if self.render_mirror else 1 + return int(sprites) + def get_output_order(self): + order = [] + for i in range(self.grid_size[0] * self.grid_size[1]): + if i in self.subposition_order: + order.append(int(self.subposition_order.index(i))) + else: + # a negative number so small no project file would ever hit it + order.append(-99999) + return order + +track_piece_manifest = [ + { + "angles": 0, + "name": "None" + }, + { # Tab icon when building track type + "name": "Spinning Preview", + "angles": 32, + "symmetric": True, + "grid_size": [1,1], + "subposition_order": [0], + "sprite_type": "UI", + "canvas_size": [29,22], + "camera_world_offset": [0, 0, 0], + "base_layer_name": 0 + }, + { # vehicle UI icons for picking up and placing vehicle + "name": "Pickup icon", + "angles": 1, + "grid_size": [1,1], + "subposition_order": [0], + "sprite_type": "UI", + "canvas_size": [20,20], + "camera_world_offset": [0, 0, 0], + "base_layer_name": 1 + }, + { # vehicle UI icons for picking up and placing vehicle + "name": "Placement icon", + "angles": 1, + "grid_size": [1,1], + "subposition_order": [0], + "sprite_type": "UI", + "canvas_size": [20,20], + "camera_world_offset": [0, 0, 0], + "base_layer_name": 2 + }, + { + "name": "Straight", + "sprite_type": "FLAT", + "symmetric": True + }, + { # Roads put this here in the order + "name": "Very Small Curve", + "sprite_type": "FLAT" + }, + { + "name": "T-Intersection", + "sprite_type": "FLAT" + }, + { + "name": "4-Way Intersection", + "sprite_type": "FLAT", + "angles": 1 + }, + { + "name": "Small Curve", + "sprite_type": "FLAT", + "grid_size": [2,2], + "subposition_order": [2,3,0,1], + "camera_world_offset": [1, -0.5, 0] + }, + { + "name": "Small Curve Gentle Slope Up", + "sprite_type": "SLOPE", + "grid_size": [2,2], + "subposition_order": [2,3,0,1], + "camera_world_offset": [1, -0.5, 0], + "base_layer_name": 6 + }, + { + "name": "Small Curve Gentle Slope Down", + "sprite_type": "SLOPE", + "grid_size": [2,2], + "subposition_order": [2,3,0,1], + "camera_world_offset": [1, -0.5, 0], + "base_layer_name": 6 + }, + { + "name": "Small Curve Steep Slope Up", + "sprite_type": "SLOPE", + "grid_size": [2,2], + "subposition_order": [2,3,0,1], + "camera_world_offset": [1, -0.5, 0], + "base_layer_name": 6 + }, + { + "name": "Small Curve Steep Slope Down", + "sprite_type": "SLOPE", + "grid_size": [2,2], + "subposition_order": [2,3,0,1], + "camera_world_offset": [1, -0.5, 0], + "base_layer_name": 6 + }, + { + "name": "Medium Curve", + "sprite_type": "FLAT", + "grid_size": [3,3], + "subposition_order": [6,3,4,1,2], + "camera_world_offset": [1.5, -1, 0] + }, + { + "name": "Gentle Slope", + "sprite_type": "SLOPE", + "grid_size": [1,2], + "subposition_order": [1,0], + "camera_world_offset": [1, 0, 0], + "base_layer_name": 6 + }, + { + "name": "Steep Slope", + "sprite_type": "SLOPE", + "base_layer_name": 6 + }, + { + "name": "Diagonal Curve", + "sprite_type": "FLAT", + "render_mirror": True, + "grid_size": [2,3], + "subposition_order": [4,2,3,0,1], + "camera_world_offset": [1.5, -0.5, 0] + }, + { + "name": "Diagonal Straight", + "symmetric": True, + "sprite_type": "FLAT", + "grid_size": [2,2], + "subposition_order": [2,0,3,1], + "camera_world_offset": [0.5, -.5, 0] + }, + { + "name": "S-Bend", + "sprite_type": "FLAT", + "symmetric": True, + "render_mirror": True, + "grid_size": [2,3], + "subposition_order": [5, 3, 2, 0], + "camera_world_offset": [1.5, 0.5, 0] + }, + { # Railways put it here in the order + "name": "Very Small Curve", + "sprite_type": "FLAT" + } +] + +for i in range(len(track_piece_manifest)): + track_piece_manifest[i] = TrackPieceManifest(track_piece_manifest[i], i) \ No newline at end of file diff --git a/loco-graphics-helper/builders/task_builder.py b/loco-graphics-helper/builders/task_builder.py index 32ffec7..c5e7c18 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: @@ -33,6 +45,9 @@ def __init__(self): self.use_anti_aliasing = True self.anti_alias_with_background = False self.maintain_aliased_silhouette = True + self.mirror_x = False + + self.target_object = None self.output_index = 0 @@ -46,7 +61,10 @@ def __init__(self): self.offset_x = 0 self.offset_y = 0 + self.output_flags = 0 + self.output_zoomOffset = 0 + self.output_prefix = "Sprite" self.occlusion_layers = 0 self.task = RenderTask(None) @@ -75,6 +93,11 @@ def add_frame(self, frame_index, number_of_viewing_angles, angle_index, animatio frame.set_base_palette(self.palette) + frame.output_prefix = self.output_prefix + frame.set_output_flags(self.output_flags) + frame.set_output_zoomOffset(self.output_zoomOffset) + frame.set_mirror_x(self.mirror_x) + frame.set_anti_aliasing_with_background( self.use_anti_aliasing, self.anti_alias_with_background, self.maintain_aliased_silhouette) @@ -82,11 +105,16 @@ 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): + def add_viewing_angles(self, number_of_viewing_angles, animation_frame_index=0, animation_frames=1, rotational_symmetry=False, oversize_order = "SCENERY"): start_output_index = self.output_index @@ -94,12 +122,13 @@ def add_viewing_angles(self, number_of_viewing_angles, animation_frame_index=0, number_of_viewing_angles = int(number_of_viewing_angles / 2) rotation_range = 180 if rotational_symmetry else 360 + frames = 0 + for viewing_angle_index in range(number_of_viewing_angles): + for animation_frame in range(animation_frames): + num_sprites = 1 + angle = rotation_range / number_of_viewing_angles * viewing_angle_index - for i in range(number_of_viewing_angles): - for j in range(animation_frames): - angle = rotation_range / number_of_viewing_angles * i - - frame_index = start_output_index + i * animation_frames + j + frame_index = start_output_index + viewing_angle_index * animation_frames + animation_frame frame = Frame(frame_index, self.task, angle + self.view_angle, self.bank_angle, self.vertical_angle, self.mid_angle) frame.set_multi_tile_size(self.width, self.length, self.invert_tile_positions) @@ -111,41 +140,48 @@ def add_viewing_angles(self, number_of_viewing_angles, animation_frame_index=0, frame.set_cast_shadows(self.cast_shadows) frame.set_layer(self.layer) + + frame.set_target_object(self.target_object) frame.set_base_palette(self.palette) + + frame.output_prefix = self.output_prefix + frame.set_output_flags(self.output_flags) + frame.set_output_zoomOffset(self.output_zoomOffset) + frame.set_mirror_x(self.mirror_x) frame.set_anti_aliasing_with_background( self.use_anti_aliasing, self.anti_alias_with_background, self.maintain_aliased_silhouette) - frame.animation_frame_index = animation_frame_index + j + frame.animation_frame_index = animation_frame_index + animation_frame 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): output_indices.append( - start_output_index + k * animation_frames * number_of_viewing_angles + j * number_of_viewing_angles + i) + start_output_index + k * animation_frames * number_of_viewing_angles + animation_frame * number_of_viewing_angles + viewing_angle_index) frame.set_output_indices(output_indices) + num_sprites = self.occlusion_layers if frame.oversized: - output_indices = [] - for k in range(frame.width * frame.length): - tile_index = k - if frame.invert_tile_positions: - tile_index = (frame.width * frame.length - k - 1) - output_indices.append( - start_output_index + tile_index * animation_frames * number_of_viewing_angles + j * number_of_viewing_angles + i) - - frame.set_output_indices(output_indices) - + if oversize_order == "SCENERY": + num_sprites = frame.oversize_order_scenery(start_output_index, number_of_viewing_angles, viewing_angle_index, animation_frames, animation_frame) + elif oversize_order == "TRACK": + num_sprites = frame.oversize_order_track(start_output_index, number_of_viewing_angles, viewing_angle_index, animation_frames, animation_frame) + else: + raise Exception("Invalid oversize order type") + frames += num_sprites self.angles.append(frame) - + """ frames = number_of_viewing_angles * \ animation_frames * self.width * self.length if self.occlusion_layers > 0: frames *= self.occlusion_layers - + """ self.output_index += frames # Sets the number of recolorable materials @@ -181,6 +217,9 @@ def set_size(self, width, length, invert_tile_positions): self.length = length self.invert_tile_positions = invert_tile_positions + def set_mirror_scale(self, scale): + self.scale = scale + # Sets the rotation applied to future render angles def set_rotation(self, view_angle, bank_angle=0, vertical_angle=0, mid_angle=0): self.view_angle = view_angle @@ -194,6 +233,7 @@ def reset_rotation(self): self.bank_angle = 0 self.vertical_angle = 0 self.mid_angle = 0 + self.mirror_x = False # Sets the number of occlusion layers def set_occlusion_layers(self, layers): @@ -212,9 +252,11 @@ def clear(self): self.width = 1 self.length = 1 + self.output_flags = 0 + self.output_zoomOffset = 0 self.set_offset(0, 0) - + self.target_object = None self.set_occlusion_layers(0) self.recolorables = 0 @@ -222,3 +264,5 @@ def clear(self): self.task = RenderTask(None) self.reset_rotation() + self.output_prefix = "Sprite" + diff --git a/loco-graphics-helper/frame.py b/loco-graphics-helper/frame.py index e8040e1..e31c8f9 100644 --- a/loco-graphics-helper/frame.py +++ b/loco-graphics-helper/frame.py @@ -14,11 +14,18 @@ # 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 self.output_indices = [frame_index] - self.task = task self.view_angle = view_angle self.bank_angle = bank_angle @@ -44,8 +51,14 @@ def __init__(self, frame_index, task, view_angle, bank_angle=0, vertical_angle=0 self.cast_shadows = True + self.output_prefix = "Sprite" self.offset_x = 0 self.offset_y = 0 + self.output_flags = 0 + self.output_zoomOffset = 0 + self.scale = (1, 1, 1) + self.mirror_x = False + self.view_angle_offset = -45 self.base_palette = None @@ -76,41 +89,61 @@ def get_final_output_paths(self): output_paths = [] for output_index in self.output_indices: output_paths.append(os.path.join( - self.task.get_output_folder(), "sprites", "sprite_{}.png".format(output_index))) + self.task.get_output_folder(), "sprites", "{}_{}.png".format(self.output_prefix, int(output_index)))) return output_paths else: - return [os.path.join(self.task.get_output_folder(), "sprites", "sprite_{}.png".format(self.frame_index))] + return [os.path.join(self.task.get_output_folder(), "sprites", "{}_{}.png".format(self.output_prefix,self.frame_index))] - def prepare_scene(self): - object = bpy.data.objects['Rig'] - if object is None: - return + def prepare_scene_vehicle(self): + location = None if not self.target_object is None: for o in bpy.data.scenes[0].objects: if o == self.target_object: 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 - - self.target_object.hide_render = False - for c in self.target_object.children: - c.hide_render = False - - object.location = self.target_object.matrix_world.translation + recursive_hide_children(o,True) + recursive_hide_children(self.target_object,False, self.target_object.loco_graphics_helper_object_properties.object_type) + location = self.target_object.matrix_world.translation + if self.target_object.loco_graphics_helper_vehicle_properties.bounding_box_override: + 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': bpy.data.objects['AirplaneShadowLight'].hide_render = False else: bpy.data.objects['AirplaneShadowLight'].hide_render = True + return location + + def prepare_scene_track(self): + for o in bpy.data.scenes[0].objects: + if o.loco_graphics_helper_object_properties.object_type == 'NONE': + continue + recursive_hide_children(o,True) + if self.target_object is not None: + for o in self.target_object.objects: + recursive_hide_children(o,False) + return self.target_object.location + return self.target_object.location + + def prepare_scene(self): + object = bpy.data.objects['Rig'] + general_properties = bpy.context.scene.loco_graphics_helper_general_properties + render_mode = general_properties.render_mode + if object is None: + return + + if render_mode == "VEHICLE": + object.location = self.prepare_scene_vehicle() + elif render_mode == "TRACK": + object.location = self.prepare_scene_track() + object.rotation_euler = (math.radians(self.bank_angle), math.radians(self.vertical_angle), math.radians(self.mid_angle)) vJoint = object.children[0] - vJoint.rotation_euler = (0, 0, math.radians(self.view_angle - 45)) + vJoint.scale = self.scale + vJoint.rotation_euler = (0, 0, math.radians(self.view_angle + self.view_angle_offset )) def set_anti_aliasing_with_background(self, use_anti_aliasing, anti_alias_with_background, maintain_aliased_silhouette): self.use_anti_aliasing = use_anti_aliasing @@ -126,6 +159,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 @@ -155,9 +191,57 @@ def set_output_indices(self, indices): if len(self.output_indices) != self.width * self.length * layers: raise Exception( "The number of output indices does not match the number of expected output sprites for this frame") - + + def oversize_order_scenery(self, start_output_index, number_of_viewing_angles, viewing_angle_index, animation_frames, animation_frame): + output_indices = [] + for k in range(self.width * self.length): + tile_index = k + if self.invert_tile_positions: + tile_index = (self.width * self.length - k - 1) + output_indices.append( + start_output_index + tile_index * animation_frames * number_of_viewing_angles + animation_frame * number_of_viewing_angles + viewing_angle_index) + self.set_output_indices(output_indices) + return self.width * self.length + + def oversize_order_track(self, start_output_index, number_of_viewing_angles, viewing_angle_index, animation_frames, animation_frame): + if self.target_object is None: + self.set_output_indices([start_output_index + viewing_angle_index * animation_frames * self.width * self.length + x for x in range(self.width * self.length)]) + return + offset_order = self.target_object.manifest.get_output_order() + num_sprites = len(self.target_object.manifest.subposition_order) + output_indices = [] + for offset in offset_order: + frame_number = offset + start_output_index + frame_number += viewing_angle_index * num_sprites # do I bother with animation frames? + output_indices.append(frame_number) + self.set_output_indices(output_indices) + return num_sprites + def set_target_object(self, object): self.target_object = object def set_cast_shadows(self, cast_shadows): self.cast_shadows = cast_shadows + + def set_output_flags(self, flags): + self.output_flags = flags + + def set_output_zoomOffset(self, offset): + if offset < 0: + raise Exception("Zoom Offset must be positive") + if offset > self.frame_index: + raise Exception("Zoom Offset may not be larger than the current sprite number") + self.output_zoomOffset = offset + + def set_mirror_x(self, mirror): + if mirror == self.mirror_x: + return + self.mirror_x = mirror + if mirror: + self.scale = (-1, 1, 1) + self.view_angle_offset = -90-45 + self.view_angle = 360-self.view_angle + else: + self.scale = (1, 1, 1) + self.view_angle_offset = -45 + self.view_angle = 360-self.view_angle diff --git a/loco-graphics-helper/loco_object_helper_panel.py b/loco-graphics-helper/loco_object_helper_panel.py index 370d1c7..e461b3c 100644 --- a/loco-graphics-helper/loco_object_helper_panel.py +++ b/loco-graphics-helper/loco_object_helper_panel.py @@ -8,6 +8,8 @@ ''' import bpy +from .track import is_road, is_rail, TrackPiece, get_layer_names, get_num_layers +from .angle_sections.track import TrackPieceType, RoadPieceType, track_piece_manifest, TrackType class LocoObjectHelperPanel(bpy.types.Panel): bl_label = "Loco Graphics" @@ -19,29 +21,124 @@ class LocoObjectHelperPanel(bpy.types.Panel): def draw(self, context): layout = self.layout object_properties = context.object.loco_graphics_helper_object_properties + object_type = object_properties.object_type + + # this connects a track layer object to its parent object + track_object = context.object + scene = context.scene + while track_object.parent is not None: + track_object = track_object.parent + if track_object.loco_graphics_helper_object_properties.object_type == "TRACK_PIECE": + break + if track_object == scene or track_object == context.object: + track_object = None row = layout.row() if not "Rig" in context.scene.objects: row.label("Tool is not intialised.") return - row.prop(object_properties, "object_type") - if object_properties.object_type == "BODY": + # if it's a track layer object, force the type to NONE and don't show the property + if track_object and track_object.loco_graphics_helper_object_properties.object_type == "TRACK_PIECE": + pass + else: + row.prop(object_properties, "object_type") + + if object_type == "BODY": self.draw_body_panel(context, layout) - if object_properties.object_type == "BOGIE": + if object_type == "BOGIE": self.draw_bogie_panel(context, layout) - if object_properties.object_type == "CAR": + if object_type == "CAR": self.draw_car_panel(context, layout) - def draw_car_panel(self, context, layout): + # if it's a track layer object, show the track properties of the track piece object + if track_object and track_object.loco_graphics_helper_object_properties.object_type == "TRACK_PIECE": + self.draw_piece_panel(context, track_object, layout) + elif object_type == "TRACK_PIECE": + self.draw_piece_panel(context, context.object, layout) + + @staticmethod + def wrong_render_mode(context, layout, mode): scene = context.scene general_properties = scene.loco_graphics_helper_general_properties + + if general_properties.render_mode != mode: + row = layout.row() + row.label("{} render mode required".format(mode)) + return True + return False + + def draw_piece_panel(self, context, track_object, layout): + + if self.wrong_render_mode(context, layout, "TRACK"): + return + + # track piece's properties + track_piece_properties = track_object.loco_graphics_helper_track_piece_properties + if is_road(): + row = layout.row() + row.prop(track_piece_properties,"road_piece") + row = layout.row() + row.prop(track_piece_properties,"reversed") + else: + row = layout.row() + row.prop(track_piece_properties,"track_piece") + + piece_name = track_piece_properties.road_piece if is_road() else track_piece_properties.track_piece + piece_type = RoadPieceType[piece_name] if is_road() else TrackPieceType[piece_name] + + if piece_type.value <= 0: + return + manifest = track_piece_manifest[piece_type.value] + track_piece = TrackPiece(manifest, track_object) + box = layout.box() + box.label("Layers:") + split = box.split(.50) + columns = [split.column(), split.column()] + for i in range(get_num_layers(manifest.sprite_type)): + names = get_layer_names(i + manifest.base_layer_name) + # track layer's properties + columns[i % 2].row().prop(context.object.loco_graphics_helper_track_piece_properties, "layers", + index=i, text=names[0]) + box = layout.box() + col = box.column() + for i in range(track_piece.num_layers): + col.label("{} layer:".format(track_piece.layer_names[i])) + if len(track_piece.layer_objects[i]) == 0: + col.label(" No model set. Sprites will be blank.") + else: + col.label(" "+", ".join([x.name for x in track_piece.layer_objects[i]])) + + def draw_layer_panel(self, context, layout): + row = layout.row() + + if self.wrong_render_mode(context, layout, "TRACK"): + return + + track_piece_properties = context.object.loco_graphics_helper_track_piece_properties + + parent = context.object.parent + parent_properties = parent.loco_graphics_helper_track_piece_properties + piece_name = parent_properties.road_piece if is_road() else parent_properties.track_piece + piece_type = RoadPieceType[piece_name] if is_road() else TrackPieceType[piece_name] + if piece_type.value <= 0: + return + manifest = track_piece_manifest[piece_type.value] + box = layout.box() + box.label("Layers:") + split = box.split(.50) + columns = [split.column(), split.column()] + for i in range(get_num_layers(manifest.sprite_type)): + names = get_layer_names(i + manifest.base_layer_name) + columns[i % 2].row().prop(track_piece_properties, "layers", + index=i, text=names[0]) + + def draw_car_panel(self, context, layout): row = layout.row() - if not general_properties.render_mode == "VEHICLE": - row.label("Vehicle Render Mode Required") + if self.wrong_render_mode(context, layout, "VEHICLE"): return vehicle_properties = context.object.loco_graphics_helper_vehicle_properties @@ -50,16 +147,22 @@ def draw_car_panel(self, context, layout): row = layout.row() def draw_bogie_panel(self, context, layout): - scene = context.scene - general_properties = scene.loco_graphics_helper_general_properties - row = layout.row() - if not general_properties.render_mode == "VEHICLE": - row.label("Vehicle Render Mode Required") + if self.wrong_render_mode(context, layout, "VEHICLE"): return vehicle_properties = context.object.loco_graphics_helper_vehicle_properties + row = layout.row() + 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 +170,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,26 +196,31 @@ 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 - row = layout.row() - if not general_properties.render_mode == "VEHICLE": - row.label("Vehicle Render Mode Required") + if self.wrong_render_mode(context, layout, "VEHICLE"): return vehicle_properties = context.object.loco_graphics_helper_vehicle_properties + row = layout.row() + row.prop(vehicle_properties, "null_component") + + if vehicle_properties.null_component: + return + + row = layout.row() + row.prop(vehicle_properties, "index") + row = layout.row() + row.prop(vehicle_properties, "is_clone") row = layout.row() @@ -119,14 +228,14 @@ 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:") + box.label("Sprites:") split = box.split(.50) columns = [split.column(), split.column()] @@ -148,17 +257,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 +273,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/track_render_operator.py b/loco-graphics-helper/operators/track_render_operator.py index 33e495c..af9391b 100644 --- a/loco-graphics-helper/operators/track_render_operator.py +++ b/loco-graphics-helper/operators/track_render_operator.py @@ -1,10 +1,10 @@ ''' -Copyright (c) 2022 RCT Graphics Helper developers +Copyright (c) 2025 Loco 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 +Interested in contributing? Visit https://github.com/OpenLoco/Blender-Loco-Graphics -RCT Graphics Helper is licensed under the GNU General Public License version 3. +Loco Graphics Helper is licensed under the GNU General Public License version 3. ''' import bpy @@ -12,8 +12,15 @@ import os from .render_operator import RCTRender +from ..track import get_valid_pieces, get_track_pieces +class TrackPieceLayer(): + def __init__(self, location, objects, manifest): + self.location = location + self.manifest = manifest + self.objects = objects + class RenderTrack(RCTRender, bpy.types.Operator): bl_idname = "render.loco_track" bl_label = "Render Loco Track" @@ -22,7 +29,56 @@ def create_task(self, context): scene = context.scene props = scene.loco_graphics_helper_track_properties general_props = scene.loco_graphics_helper_general_properties - - # Create the list of frames with our parameters + self.task_builder.clear() + + self.task_builder.set_anti_aliasing_with_background( + context.scene.render.use_antialiasing, general_props.anti_alias_with_background, general_props.maintain_aliased_silhouette) + + self.task_builder.set_output_index(general_props.out_start_index) + self.task_builder.set_recolorables(general_props.number_of_recolorables) + self.task_builder.set_cast_shadows(general_props.cast_shadows) + + self.task_builder.set_palette(self.palette_manager.get_base_palette( + general_props.palette, general_props.number_of_recolorables, "FULL")) + + valid_pieces = get_valid_pieces() + + track_pieces = [x for x in scene.objects if x.loco_graphics_helper_object_properties.object_type == "TRACK_PIECE" and not x.loco_graphics_helper_track_piece_properties.reversed] + + ndot_pieces = get_track_pieces(track_pieces) + for i in valid_pieces: + self.add_track_piece(ndot_pieces[i]) + + if props.one_way: + reverse_pieces = [x for x in scene.objects if x.loco_graphics_helper_object_properties.object_type == "TRACK_PIECE" and x.loco_graphics_helper_track_piece_properties.reversed] + rdot_pieces = get_track_pieces(reverse_pieces) + for i in valid_pieces: + self.add_track_piece(rdot_pieces[i]) + return self.task_builder.create_task(context) + + def add_track_piece(self, track_piece): + num_frames = track_piece.manifest.get_sprites_per_layer() * track_piece.num_layers + if track_piece.render_sprite: + for i in range(track_piece.num_layers): + self.add_layer(track_piece, i) + else: + self.task_builder.output_flags = 0 + self.task_builder.add_null_frames(num_frames) + + def add_layer(self, track_piece, layer): + """track_piece.location""" + manifest = track_piece.manifest + target_object = TrackPieceLayer((0,0,0),track_piece.layer_objects[layer], manifest) + self.task_builder.output_flags = 1 + # self.task_builder.output_prefix = "{}_{}".format(manifest.name, track_piece.layer_names[layer]) + self.task_builder.target_object = target_object + self.task_builder.mirror_x = False + self.task_builder.set_size(manifest.grid_size[0], manifest.grid_size[1], False) + self.task_builder.add_viewing_angles(manifest.angles, 0, 1, manifest.symmetric, "TRACK") + + if manifest.render_mirror: + self.task_builder.mirror_x = True + self.task_builder.add_viewing_angles(manifest.angles, 0, 1, manifest.symmetric, "TRACK") + \ No newline at end of file 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/processors/sub_processes/frame_processors/post_processor.py b/loco-graphics-helper/processors/sub_processes/frame_processors/post_processor.py index 6d33803..386a46e 100644 --- a/loco-graphics-helper/processors/sub_processes/frame_processors/post_processor.py +++ b/loco-graphics-helper/processors/sub_processes/frame_processors/post_processor.py @@ -21,6 +21,8 @@ def __init__(self): self.index = 0 self.offset_x = 0 self.offset_y = 0 + self.flags = 0 + self.zoomOffset = 0 # Frame processor for masking, dithering and cropping the final image @@ -111,6 +113,8 @@ def _process_default(self, magick_command, frame): output_info.offset_x += frame.offset_x output_info.offset_y += frame.offset_y + output_info.flags = frame.output_flags + output_info.zoomOffset = frame.output_zoomOffset frame.task.output_info.append(output_info) @@ -129,6 +133,8 @@ def _process_oversized(self, magick_command, frame): for j in range(frame.length): tile_index = j * frame.width + i final_output_index = frame.output_indices[tile_index] + if final_output_index < 0: + continue final_output_path = frame.get_final_output_paths()[tile_index] tile_magic_command = MagickCommand(quantized_output_path) @@ -148,20 +154,27 @@ def _process_oversized(self, magick_command, frame): result, final_output_index, final_output_path) # Modify the output offsets for the sub tile we're processing - x, y = (i - (frame.width - 1) / - 2), (j - (frame.length - 1) / 2) + x, y = (i - (frame.width - 1) / 2), (j - (frame.length - 1) / 2) rot = round(frame.view_angle / 90) % 4 - + + a = output_info.offset_x + b = output_info.offset_y + + if frame.mirror_x: + x = -x + rot = 4 - rot + if rot == 1: x, y = (-y, x) if rot == 2: x, y = (-x, -y) if rot == 3: x, y = (y, -x) - + dx = -int((x * 32) - (y * 32)) - dy = -int((y * 16) + (x * 16)) + dy = -int((x * 16) + (y * 16)) + output_info.offset_x += dx output_info.offset_y += dy @@ -171,6 +184,9 @@ def _process_oversized(self, magick_command, frame): output_info.offset_x += frame.offset_x output_info.offset_y += frame.offset_y + print("FRAME NUMBER {}: Mirrored: {}, rot = {}, x = {}, y = {}, dx = {}, dy = {}, offset_x = {}, offset_y = {}".format(final_output_index,frame.mirror_x, rot, x, y, dx, dy, a, b, output_info.offset_x, output_info.offset_y)) + output_info.flags = frame.output_flags + output_info.zoomOffset = frame.output_zoomOffset output_infos.append(output_info) diff --git a/loco-graphics-helper/processors/sub_processes/sprites_manifest_processor.py b/loco-graphics-helper/processors/sub_processes/sprites_manifest_processor.py index e29d4bf..7459835 100644 --- a/loco-graphics-helper/processors/sub_processes/sprites_manifest_processor.py +++ b/loco-graphics-helper/processors/sub_processes/sprites_manifest_processor.py @@ -30,24 +30,40 @@ def process(self, master_context, callback=None): file_path = os.path.join( task.get_output_folder(), "sprites.json") - output_info_list = task.output_info + def frame_filter(a): + return a.index >= 0 and a.offset_x >= -128 and a.offset_x <= 127 and a.offset_y >= -128 and a.offset_y < 127 + + output_info_list = [a for a in task.output_info if frame_filter(a)] def get_index(output_info): return output_info.index output_info_list.sort(key=get_index) + + images = [] + if os.path.exists(file_path): + try: + with open(file_path, "r") as images_file: + images = json.loads(images_file.read(), + object_pairs_hook=OrderedDict) + images_file.close() + except Exception as e: + print("Error when reading sprites.json",e) with open(file_path, "w") as images_file: - images = [] for output_info in output_info_list: while len(images) <= output_info.index: - images.append("") + images.append({}) image_dict = OrderedDict() image_dict["path"] = "sprites/" + \ os.path.basename(output_info.path) image_dict["x"] = output_info.offset_x image_dict["y"] = output_info.offset_y + if output_info.flags != 0: + image_dict["flags"] = output_info.flags + if output_info.zoomOffset != 0: + image_dict["zoomSprite"] = output_info.zoomSprite images[output_info.index] = image_dict diff --git a/loco-graphics-helper/properties/file_versioning.py b/loco-graphics-helper/properties/file_versioning.py new file mode 100644 index 0000000..06e4a32 --- /dev/null +++ b/loco-graphics-helper/properties/file_versioning.py @@ -0,0 +1,91 @@ +''' +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, use pass argument for an empty function + +current_file_version = 6 + +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 + + # plugin version 0.2.0 + # add track rendering + def version5(): + pass + + # plugin version 0.2.1 + # remove TRACK_LAYER object type + def version6(): + pass + +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/object_properties.py b/loco-graphics-helper/properties/object_properties.py index 1a05b70..ed75715 100644 --- a/loco-graphics-helper/properties/object_properties.py +++ b/loco-graphics-helper/properties/object_properties.py @@ -27,6 +27,12 @@ def object_type_update_func(self, context): props.flat_viewing_angles = "64" props.sloped_viewing_angles = "32" + # Reset to default for track pieces + if object.loco_graphics_helper_object_properties.object_type == "TRACK_PIECE": + props = object.loco_graphics_helper_track_piece_properties + props.flat_viewing_angles = "64" + props.sloped_viewing_angles = "32" + class ObjectProperties(bpy.types.PropertyGroup): object_type = bpy.props.EnumProperty( @@ -38,6 +44,7 @@ class ObjectProperties(bpy.types.PropertyGroup): ("CARGO", "Cargo", "", 3), ("CAR", "Car", "", 4), ("ANIMATION", "Animation position", "", 5), + ("TRACK_PIECE","Track piece","",6), ), default="NONE", update=object_type_update_func 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/track_piece_properties.py b/loco-graphics-helper/properties/track_piece_properties.py new file mode 100644 index 0000000..def30bf --- /dev/null +++ b/loco-graphics-helper/properties/track_piece_properties.py @@ -0,0 +1,49 @@ +''' +Copyright (c) 2025 Loco 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/OpenLoco/Blender-Loco-Graphics + +Loco Graphics Helper is licensed under the GNU General Public License version 3. +''' + +import bpy +from ..angle_sections.track import track_piece_manifest, TrackPieceType, RoadPieceType, max_layers + +class TrackPieceProperties(bpy.types.PropertyGroup): + track_piece = bpy.props.EnumProperty( + name="Track Piece", + items=tuple((enum_item.name,track_piece_manifest[enum_item.value].name,"",enum_item.value) for enum_item in TrackPieceType), + default="NONE" + ) + road_piece = bpy.props.EnumProperty( + name="Road Piece", + items=tuple((enum_item.name,track_piece_manifest[enum_item.value].name,"",enum_item.value) for enum_item in RoadPieceType), + default="NONE" + ) + + reversed = bpy.props.BoolProperty( + name="Reverse Road", + description="If set, the normal direction of travel is toward the image Southwest. If unset, normal direction of travel is toward the image Northeast, or is two-way", + default=False) + + layers = bpy.props.BoolVectorProperty( + name="Track Layers", + default=[False for _ in range(max_layers)], + description="Which layers this model is rendered as", + size=max_layers + ) + + render_sprite = bpy.props.BoolProperty( + name="Render Track piece", + description="Include this track piece when batch rendering", + default=True + ) + +def register_track_piece_properties(): + bpy.types.Object.loco_graphics_helper_track_piece_properties = bpy.props.PointerProperty( + type=TrackPieceProperties) + + +def unregister_track_piece_properties(): + del bpy.types.Object.loco_graphics_helper_track_piece_properties diff --git a/loco-graphics-helper/properties/track_properties.py b/loco-graphics-helper/properties/track_properties.py index 0aa208c..0012291 100644 --- a/loco-graphics-helper/properties/track_properties.py +++ b/loco-graphics-helper/properties/track_properties.py @@ -1,25 +1,33 @@ ''' -Copyright (c) 2022 RCT Graphics Helper developers +Copyright (c) 2025 Loco 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 +Interested in contributing? Visit https://github.com/OpenLoco/Blender-Loco-Graphics -RCT Graphics Helper is licensed under the GNU General Public License version 3. +Loco Graphics Helper is licensed under the GNU General Public License version 3. ''' import bpy import math import os - from ..builders.task_builder import TaskBuilder from ..operators.render_operator import RCTRender class TrackProperties(bpy.types.PropertyGroup): - placeholder = bpy.props.BoolProperty( - name="Placeholder", - description="Test.", - default=False) + track_type = bpy.props.EnumProperty( + name="Track Type", + items=( + ("RAIL", "Railway", "", 0), + ("ROAD", "Road", "", 1), + ("TRAM", "Tramway", "", 2), + ), + default="RAIL" + ) + one_way = bpy.props.BoolProperty( + name="one_way", + description="Models for both directions required", + default = False) def register_track_properties(): 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..cb300b1 100644 --- a/loco-graphics-helper/rct_graphics_helper_panel.py +++ b/loco-graphics-helper/rct_graphics_helper_panel.py @@ -22,9 +22,14 @@ 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 + +from .track import is_rail, is_road, get_track_pieces, TrackPiece, get_valid_pieces +from .angle_sections.track import track_piece_manifest 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 +48,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 +86,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 +180,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 @@ -195,167 +220,204 @@ def draw_walls_panel(self, scene, layout): text = "Failed" row.operator("render.loco_walls", text=text) + @staticmethod + def create_track_box(track_piece, layout): + box = layout.box() + row = box.row() + manifest = track_piece_manifest[track_piece.track_piece] + row.label(manifest.name) + if track_piece.root_object is not None: + row.prop(track_piece.root_object.loco_graphics_helper_track_piece_properties, "render_sprite") + col = box.column() + for i in range(track_piece.num_layers): + col.label("{} layer:".format(track_piece.layer_names[i])) + if len(track_piece.layer_objects[i]) == 0: + col.label(" No model set. Sprites will be blank.") + else: + col.label(" "+", ".join([x.name for x in track_piece.layer_objects[i]])) + def draw_track_panel(self, scene, layout): properties = scene.loco_graphics_helper_track_properties general_properties = scene.loco_graphics_helper_general_properties - - row = layout.row() - row.label("Work in progress") #row = layout.row() #row.operator("render.loco_track", text="Generate Splines") - # - #row = layout.row() - #row.prop(properties, "placeholder") -# - #if "Rig" in context.scene.objects: - # row = layout.row() - # text = "Render" - # if general_properties.rendering: - # text = "Failed" - # row.operator("render.loco_track", text=text) + + row = layout.row() + row.prop(properties, "track_type") + + if is_road(): + row = layout.row() + row.prop(properties,"one_way") + + track_pieces = [x for x in scene.objects if x.loco_graphics_helper_object_properties.object_type == "TRACK_PIECE" and not x.loco_graphics_helper_track_piece_properties.reversed] + reverse_pieces = [x for x in scene.objects if x.loco_graphics_helper_object_properties.object_type == "TRACK_PIECE" and x.loco_graphics_helper_track_piece_properties.reversed] + + col = layout.column() + valid_pieces = get_valid_pieces() + ndot_pieces = get_track_pieces(track_pieces) + if is_road(): + col.label("Road pieces:") + box = col.box() + for piece_number in valid_pieces: + self.create_track_box(ndot_pieces[piece_number],box) + if properties.one_way: + col.label("Reverse road pieces:") + rdot_pieces = get_track_pieces(reverse_pieces) + box = col.box() + for piece_number in valid_pieces: + self.create_track_box(rdot_pieces[piece_number],box) + else: + col.label("Track pieces:") + box = col.box() + for piece_number in valid_pieces: + self.create_track_box(ndot_pieces[piece_number],box) + + + row = layout.row() + text = "Render" + if general_properties.rendering: + text = "Failed" + row.operator("render.loco_track", text=text) @staticmethod 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 + for component in components: + if component.body == body: + car = component + half_width = component.get_half_width() + break + if not body.loco_graphics_helper_vehicle_properties.bounding_box_override is None: + half_width = get_half_width(body.loco_graphics_helper_vehicle_properties.bounding_box_override) + 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 is None 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/renderer.py b/loco-graphics-helper/renderer.py index 34b0197..00f8d1d 100644 --- a/loco-graphics-helper/renderer.py +++ b/loco-graphics-helper/renderer.py @@ -38,7 +38,7 @@ def __init__(self, context, palette_manager): self.context = context self.magick_path = "magick" - self.floyd_steinberg_diffusion = 5 + self.floyd_steinberg_diffusion = 35 self.palette_manager = palette_manager 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..2f01760 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..5ea8d16 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..844cf9c Binary files /dev/null and b/loco-graphics-helper/res/palettes/recolour_2_loco_palette.png differ diff --git a/loco-graphics-helper/track.py b/loco-graphics-helper/track.py new file mode 100644 index 0000000..2da097f --- /dev/null +++ b/loco-graphics-helper/track.py @@ -0,0 +1,90 @@ +''' +Copyright (c) 2025 Loco 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/OpenLoco/Blender-Loco-Graphics + +Loco Graphics Helper is licensed under the GNU General Public License version 3. +''' + +import bpy +from mathutils import Vector +from typing import List +from .angle_sections.track import * + +def track_properties(): + return bpy.context.scene.loco_graphics_helper_track_properties + +def get_num_layers(sprite_type): + if sprite_type == "UI" or sprite_type == "NULL": + return 1 + object_type = TrackType[track_properties().track_type] + map = { + TrackType.RAIL: {"FLAT": 3, "SLOPE": 1}, + TrackType.ROAD: {"FLAT": 1, "SLOPE": 1}, + TrackType.TRAM: {"FLAT": 3, "SLOPE": 3} + } + return map[object_type][sprite_type] + +# used to determine available pieces +def is_road(): + object_type = TrackType[track_properties().track_type] + if object_type == TrackType.TRAM: + return True + return object_type == TrackType.ROAD + +# used to determine layer names +def is_rail(): + object_type = TrackType[track_properties().track_type] + if object_type == TrackType.TRAM: + return True + return object_type == TrackType.RAIL + +def get_layer_names(layer): + if is_rail(): + return track_layer_names[layer] + else: + return road_layer_names[layer] + +class TrackPiece(): + track_piece = 0 + root_object = None + num_layers = 0 + layer_objects = [] + layer_names = [] + manifest = None + render_sprite = False + location = [0,0,0] + def __init__(self, manifest, root_object = None): + self.manifest = manifest + self.track_piece = manifest.track_piece + self.num_layers = get_num_layers(manifest.sprite_type) + self.layer_names = [get_layer_names(manifest.base_layer_name + i)[0] for i in range(self.num_layers)] + self.layer_objects = [[] for i in range(self.num_layers)] + if root_object is not None: + self.set_root_object(root_object) + + def set_root_object(self, object): + self.root_object = object + self.location = object.matrix_world.translation + self.render_sprite = object.loco_graphics_helper_track_piece_properties.render_sprite + objects = object.children + for i in range(self.num_layers): + self.layer_objects[i] = [x for x in objects if x.loco_graphics_helper_track_piece_properties.layers[i]] + if (object.loco_graphics_helper_track_piece_properties.layers[i]): + self.layer_objects[i].append(object) + +def get_valid_pieces(): + piece_list = RoadPieceType if is_road() else TrackPieceType # the enum with the list of pieces + return [a.value for a in piece_list if a.value > 0] + +def get_track_pieces(objects) -> List[TrackPiece]: + pieces = [TrackPiece(a) for a in track_piece_manifest] + piece_type_property = "road_piece" if is_road() else "track_piece" # the property in track_piece_properties + for object in objects: + if piece_type_property in object.loco_graphics_helper_track_piece_properties: + piece_number = object.loco_graphics_helper_track_piece_properties[piece_type_property] + if piece_number > 0: + pieces[piece_number].set_root_object(object) + + return pieces \ No newline at end of file diff --git a/loco-graphics-helper/vehicle.py b/loco-graphics-helper/vehicle.py index 6bf383f..9736f6f 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 + 128 * 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 = []