Python automation tools for shape editing (ShapeEdit, ShapeIO, and TrackshapeUtils)

Collapse
X
 
  • Time
  • Show
Clear All
new posts
  • pgroenbaek
    Member
    • Jan 2024
    • 98
    • Denmark
    • ORTS

    #1

    Python automation tools for shape editing (ShapeEdit, ShapeIO, and TrackshapeUtils)

    This post is somewhat lengthy and technical, but it is worth a read. Using these Python modules that I created, you can accomplish tasks that would otherwise be practically impossible to do by hand - the type of things that might make people go, “What in the ... kind of sorcery is this?”

    For example (the Python script that did this):

    V4hs1t_RKL_Curve.png
    As far as I’m aware, there are no other tools available that can do what these modules are capable of, other than performing manual edits using a text editor. The closest neighboring tools might be ShapeConverter and TSFV - and those aren’t really editing tools, but rather utilities that can, to some extent, convert shapes back into 3D modeling formats.

    With these modules, all editing takes place directly in the raw .s file, so using them does not cause the loss of things like texture coordinates or animations. There is no 3D modeling software involved; everything is done purely through programmatic access to the shape’s data structure.

    This thread is a continuation of my shape-editing adventures that started here:

    https://www.trainsim.com/forums/foru...es-with-python

    From those experiments, I have created this set of Python modules with far more advanced editing capabilities than just moving points around, as I was doing at the beginning of that thread.

    These are:
    - ShapeIO - for reading and writing uncompressed shape files.
    - ShapeEdit - a wrapper around the shape data structure from ShapeIO that provides advanced, easy-to-use editing capabilities.
    - TrackshapeUtils - additional utilities that make it easy to work with track shapes.
    - pyFFEDITC - a wrapper around ffeditc_unicode.exe so it can be used easily from within Python scripts (necessary since ShapeIO is currently unable to deal with compressed shapes).

    ShapeIO loads the entire shape data structure as Python objects, so in principle you can edit anything and then save the modified shape. However, because the shape data structure contains many invariants, it’s very easy to break things if you don’t know exactly what you’re doing.

    That’s where ShapeEdit comes in. It’s a DSL-like wrapper around the shape data structure that allows you to easily access all of the shape’s geometry. It also provides operations that won’t break the shape to the point where it can’t load in Open Rails. Things might end up looking really wonky depending on what you do, but the shape will still load in the sim.

    TrackshapeUtils includes additional functions that allow you to load track centers from a tsection.dat file and use them to calculate new vertex positions relative to the track center. It also provides operations such as calculating distances from the nearest track center and along the length of the track. This is quite useful for creating scripts that can handle both straight and curved track sections.

    These modules can be installed from PyPI using pip:

    Code:
    pip install --upgrade shapeio
    pip install --upgrade shapeedit
    pip install --upgrade trackshapeutils
    pip install --upgrade pyffeditc
    You can then use the modules within Python scripts. I don’t expect breaking changes in the external APIs of the modules at this point. However, keep in mind that the modules are currently at version 0.5.0-beta2. It’s still early days, and it is possible for functionality to break due to unhandled edge-cases, particularly when loading shapes, but I think the modules are now stable enough to share and allow others to use them.

    ShapeEdit currently provides the following operations:
    - Easy access to the entire geometry structure, including LOD controls, distance levels, sub-objects, primitives, vertices, and triangles.
    - Access to and modification of vertex data (points, UV coordinates, normals).
    - Access to and modification of triangle data (face normals).
    - Insertion of new vertices.
    - Insertion of new triangles.
    - Removal of specific triangles.
    - Removal of all triangles connected to a vertex.

    ShapeEdit does not yet support adding new textures, primitives, or sub-objects. However, this functionality is coming eventually, as I will need it to insert custom Danish switch stands on Norbert’s DBTracks switches.

    Once I implement that functionality, you could, in principle, model something in Blender, export it to a new shape using the MSTS/ORTS exporter, and then transfer the newly modeled geometry into an existing shape.

    Here are some examples of what has already been done using earlier versions of these tools:
    - NR_Emb shapes with ATracks railheads
    - Insertion of overhead wires onto Laci1959's animated DblSlip7_5d switches

    I am in the process of making some customizations to DBTracks shapes here. And in doing so, I am also creating shapes that don’t exist in the original DBTracks packages. Xover7_5d's with overhead wires being one example. And who knows, maybe even the missing switches at one point (but no promises there, as the level of difficulty in doing that is quite high even with these modules).

    Here are a couple more screenshots showing what’s possible through scripting with these Python modules. You can use them on any kind of shape - not just track sections, which is what I’ve mostly been working with so far.

    NohabMyMirrors.png
    This script made the above edit. Here the side mirrors are removed from the Nohab MY locomotive model by Pál Tamás.

    NR_Emb_AT.png
    This script made the above edit. This is probably the most advanced script I've made up until now. It shrinks the rail tops and then quite literally weaves new geometry into the rail sides to make the NR_Emb shapes fit Eric's ATracks. And it handles straight/curved track as well as parallel tracks correctly.


    To give you an idea of how little scripting it takes to perform simple edits, here are some quick examples:

    In this example, all vertex data within a sub-object is set to zero:

    Code:
    import shapeio
    from shapeedit import ShapeEditor
    
    my_shape = shapeio.load("./path/to/example.s")
    
    shape_editor = ShapeEditor(my_shape)
    
    sub_object = shape_editor.lod_control(0).distance_level(200).sub_object(0)
    
    # Set point/uv_point/normal data of all vertices within the subobject to zero.
    for vertex in sub_object.vertices():
        vertex.point.x = 0.0
        vertex.point.y = 0.0
        vertex.point.z = 0.0
        vertex.uv_point.u = 0.0
        vertex.uv_point.v = 0.0
        vertex.normal.x = 0.0
        vertex.normal.y = 0.0
        vertex.normal.z = 0.0
    
    shapeio.dump(my_shape, "./path/to/output.s")

    And here's how to add vertices/triangles:

    Code:
    import shapeio
    from shapeio.shape import Point, UVPoint, Vector
    from shapeedit import ShapeEditor
    
    my_shape = shapeio.load("./path/to/example.s")
    
    shape_editor = ShapeEditor(my_shape)
    
    # Add three new vertices to primitives associated with prim_state_idx 22.
    # Connect the three vertices added to each primitive with a triangle.
    for lod_control in shape_editor.lod_controls():
        for distance_level in lod_control.distance_levels():
            for sub_object in distance_level.sub_objects():
                for primitive in sub_object.primitives(prim_state_index=22):
                   new_point1 = Point(0.0, 0.0, 0.0)
                   new_point2 = Point(1.0, 0.0, 0.0)
                   new_point3 = Point(2.0, 0.0, 1.0)
                   new_uv_point = UVPoint(0.0, 0.0)
                   new_normal = Vector(0.0, 0.0, 0.0)
                  
                   new_vertex1 = primitive.add_vertex(new_point1, new_uv_point, new_normal)
                   new_vertex2 = primitive.add_vertex(new_point2, new_uv_point, new_normal)
                   new_vertex3 = primitive.add_vertex(new_point3, new_uv_point, new_normal)
                  
                   primitive.insert_triangle(new_vertex1, new_vertex2, new_vertex3)
    
    shapeio.dump(my_shape, "./path/to/output.s")

    And removal of triangles:

    Code:
    import shapeio
    from shapeedit import ShapeEditor
    
    my_shape = shapeio.load("./path/to/example.s")
    
    shape_editor = ShapeEditor(my_shape)
    
    # Remove all triangles from primitives associated with any prim_state named "Rails".
    for lod_control in shape_editor.lod_controls():
        for distance_level in lod_control.distance_levels():
            for sub_object in distance_level.sub_objects():
                for primitive in sub_object.primitives(prim_state_name="Rails"):
                    for vertex in primitive.vertices():
                        primitive.remove_triangles_connected_to(vertex)
    
    shapeio.dump(my_shape, "./path/to/output.s")

    If you use the modules and have feedback, suggestions, questions, or encounter issues or bugs, feel free to post here or open issues on GitHub. Documentation and example scripts are also available on GitHub, and code contributions are also welcome there.

    Have fun if you decide to use these modules for something! They are a powerful set of development tools, and you can do a lot of interesting things with them.


    Edit: Updated some of the links to GitHub since I split the examples out into separate repositories​
    Last edited by pgroenbaek; 12-08-2025, 06:12.
    - Peter

    Route Projects: DK24
    Utilities and Tools: ShapeIO, ShapeEdit, TrackshapeUtils
  • SD40-2
    1000 Post Veteran


    • May 2010
    • 1011
    • Metairie, Louisiana
    • English, C++, HTML
    • General

    #2
    This looks very interesting. Impressive work indeed. How you use this tool to replace chunkier wheels with finer and more detailed wheels, while maintaining the same texture mapping?

    Edit - Added clarification.
    Last edited by SD40-2; 12-01-2025, 23:46.
    Parker B. - A Misplaced Midwesterner.
    Also known as Mr. Two Bits and Mr. Squarewheels.

    Comment

    • pgroenbaek
      Member
      • Jan 2024
      • 98
      • Denmark
      • ORTS

      #3
      Originally posted by SD40-2
      This looks very interesting. Impressive work indeed. How you use this tool to replace chunkier wheels with finer and more detailed wheels, while maintaining the same mapping?
      I assume you mean texture mapping?

      Two ways come to mind:
      1. Modeling entirely new geometry in, e.g., Blender, mapping it to the same texture, and then transferring that geometry data into the existing shape’s primitives to replace the wheels.
      2. Manually inserting new vertices into the existing wheels and splitting the triangles - that is, removing the old triangles and reconnecting them with new ones.

      Option 2 is the harder one to pull off, simply because it requires calculating new vertex positions, texture coordinates, and vertex normals along the circular path of the wheels. The most difficult part is probably reconnecting everything with new triangles, since you need to define the corners of each individual triangle in the correct order to calculate the face normal properly. Otherwise you end up with triangles facing the wrong direction. And the correct order is not necessarily obvious - I had a lot of trouble with that when working on the modified NR_Emb shapes.

      When doing something like that manually, you quickly realize just how much 3D modeling tools actually take care of for you. The Python modules do provide the low-level access to a shape needed to do something like this; however, it’s still quite a difficult thing to do. And the wheels being circular definitely doesn’t make it any easier.

      With option 1, you let the 3D modeling tool handle all the low-level details, and then you simply copy the geometry and apply a position offset so that the new wheels are centered in the same place as the old wheels.

      - Peter

      Route Projects: DK24
      Utilities and Tools: ShapeIO, ShapeEdit, TrackshapeUtils

      Comment

      • SD40-2
        1000 Post Veteran


        • May 2010
        • 1011
        • Metairie, Louisiana
        • English, C++, HTML
        • General

        #4
        Thank you for explaining that, and yes, I did mean texture mapping.
        Parker B. - A Misplaced Midwesterner.
        Also known as Mr. Two Bits and Mr. Squarewheels.

        Comment

        • pgroenbaek
          Member
          • Jan 2024
          • 98
          • Denmark
          • ORTS

          #5
          A script to copy newly modeled geometry into an existing shape’s primitives would look similar to this. That one copies the overhead wire from one of Norbert’s DblSlip7_5d shapes into his Xover7_5d shapes with a Z-offset applied.

          Essentially the same process would be needed to copy something modeled in, e.g., Blender and exported into a separate .s file.

          You can compare that to the NR_Emb script, which is far more extensive. My guess is that making the wheels more detailed manually would be even more involved than that.

          Edit: Updated links to GitHub since I split the examples out into separate repositories
          Last edited by pgroenbaek; 12-08-2025, 06:10.
          - Peter

          Route Projects: DK24
          Utilities and Tools: ShapeIO, ShapeEdit, TrackshapeUtils

          Comment

          • pwillard
            3000 Post Veteran



            • Nov 1999
            • 3070
            • Cumming Ga
            • ORTS

            #6
            Something to keep in mind. Chatgpt would be helpful here for the NON-Programmers among us. You could technically feed Chatgpt with the existing tools... and then compose prompts for what you are after and it can take it from there. You might have good luck getting results... Not tried... but maybe this could expand the list of people who can make use of this. Any successful examples should be shared.
            http://www.railsimstuff.com
            Just Blender now, 3DCrafter only when I have to.
            Formerly The Keystone Works (All Permissions Granted)
            Things I work on: https://github.com/pwillard

            Comment

            • pgroenbaek
              Member
              • Jan 2024
              • 98
              • Denmark
              • ORTS

              #7
              Originally posted by pwillard
              Something to keep in mind. Chatgpt would be helpful here for the NON-Programmers among us. You could technically feed Chatgpt with the existing tools... and then compose prompts for what you are after and it can take it from there. You might have good luck getting results... Not tried... but maybe this could expand the list of people who can make use of this. Any successful examples should be shared.
              Yeah, that's a great idea Pete.

              I have tried giving it some raw shapefile to chew on a few times, and that didn't go well at all. In my experience, it does not know the raw MSTS formats and will just spew garbage most of the time, that has nothing to do with what you are trying to accomplish.

              But when everything is reduced down to Python code and the common 3D model concepts that ChatGPT should know, like vertex positions, texture coords, normals and triangles, like it is with the API here, then it might actually be able to do useful stuff with it.

              Certainly is something that should be tried.
              - Peter

              Route Projects: DK24
              Utilities and Tools: ShapeIO, ShapeEdit, TrackshapeUtils

              Comment

              • pwillard
                3000 Post Veteran



                • Nov 1999
                • 3070
                • Cumming Ga
                • ORTS

                #8
                For reference. I recently asked Chatgpt to help out with converting ACE files to PNG and it came through as being very helpful.


                A set of tools to convert legacy ACE texture files to PNG and then to DDS file for use with Open Rails. - Releases · pwillard/Ace2DDS
                http://www.railsimstuff.com
                Just Blender now, 3DCrafter only when I have to.
                Formerly The Keystone Works (All Permissions Granted)
                Things I work on: https://github.com/pwillard

                Comment

                • SD40-2
                  1000 Post Veteran


                  • May 2010
                  • 1011
                  • Metairie, Louisiana
                  • English, C++, HTML
                  • General

                  #9
                  Originally posted by pgroenbaek
                  A script to copy newly modeled geometry into an existing shape’s primitives would look similar to this. That one copies the overhead wire from one of Norbert’s DblSlip7_5d shapes into his Xover7_5d shapes with a Z-offset applied.

                  Essentially the same process would be needed to copy something modeled in, e.g., Blender and exported into a separate .s file.
                  Wheels aren't too hard, just rotate a profile around a curricular axis. Thanks for the example. I may need a bit of help trouble-shooting the code when I get around to doing that.
                  Parker B. - A Misplaced Midwesterner.
                  Also known as Mr. Two Bits and Mr. Squarewheels.

                  Comment

                  • pgroenbaek
                    Member
                    • Jan 2024
                    • 98
                    • Denmark
                    • ORTS

                    #10
                    Originally posted by SD40-2
                    I may need a bit of help trouble-shooting the code when I get around to doing that.
                    Yeah of course, feel free to post here if you need assistance.
                    - Peter

                    Route Projects: DK24
                    Utilities and Tools: ShapeIO, ShapeEdit, TrackshapeUtils

                    Comment

                    Working...