Du betrachtest gerade Vienna ULF Tram – english
Vienna ULF Tram – Rigged & Animated

Vienna ULF Tram – english

Vienna ULF Tram – From Model to Animated Rig in Blender

The Vienna ULF (Ultra Low Floor) tram is as iconic to Vienna’s cityscape as St. Stephen’s Cathedral. As a 3D artist, it was only a matter of time before I took on this vehicle. What started as a modeling project quickly turned into a technical challenge – because modeling a tram is one thing, making it realistically follow a track is another.

The Model

The ULF is not an ordinary vehicle. It consists of several rigid segments connected by portals (articulation joints). The wheels are not located under the car bodies but sit inside the portals themselves – a unique design feature that enables the ultra-low floor entry.

For the 3D model, I based it on line 25 heading to Floridsdorf. The segments are built as instances, which saves memory and ensures that changes to one segment are automatically applied to all identical parts. The driver cabs at the front and rear are standalone objects with individual details: destination displays, headlights, car number 727, and Wiener Linien branding.

The Challenge: Animating Along a Path

A stationary model looks nice, but a tram needs to move. And not just in a straight line – it needs to behave realistically in curves. The rigid car bodies must not bend; they should only articulate at the joint positions. Just like the real ULF.

I went through several approaches before finding a stable solution.

Attempt 1: Spline IK

The most obvious approach in Blender. A bone chain with a Spline IK constraint along a curve – sounds simple, works great for organic things like tentacles or snakes. But for rigid tram segments, Spline IK is the wrong tool. It bends the entire chain uniformly instead of articulating only at the joints. And as soon as you add Follow Path to move the tram along the curve, the two systems override each other.

Attempt 2: Spline IK + Follow Path + Drivers

Gemini suggested reading the curve length dynamically via a driver and using it to calculate the spacing. Sounds elegant – but it fails due to a fundamental limitation of Blender: calc_length() is a Python function and cannot be called inside drivers. The driver stays red no matter what you do.

Attempt 3: Geometry Nodes

A modern approach: Resample Curve, Curve to Points, Instance on Points. Works in principle, but the ULF has segments of different lengths. Uniform resampling doesn’t fit, and the complexity required for variable spacing is disproportionate to the benefit.

The Solution: Empty Chain with Dynamic Spacing

After three failed attempts and help from a German Blender forum, ChatGPT, Gemini, and finally Claude AI, I found a clean approach that works reliably.

The Principle

Movement and articulation are completely separated:

Empties as guide points sit at every pivot point of the tram – where the wheels are located inside the portals. Each empty has a Follow Path constraint with Fixed Position enabled on the track curve.

Only one empty is animated. The frontmost empty (ULF_Pivot.Front) gets keyframes on the Offset Factor. This is the „throttle“ for the entire tram.

All other empties follow via drivers. Each subsequent empty reads the Offset Factor of the first and adds its own spacing. The formula:

ziel + (DISTANCE_IN_METERS / laenge)

Where ziel is the Offset Factor of the first empty and laenge is the total length of the curve.

Bones span from empty to empty. Each bone gets a Copy Location constraint pointing to the front empty and a Damped Track constraint pointing to the rear empty. The mesh segments are bone-parented and remain completely rigid.

The Python Script

The curve length is automatically written as a custom property on the curve via a small Python script. This ensures the spacing adapts when you extend, shorten, or reshape the curve.

import bpy

def update_curve_length(scene, depsgraph):
    pivot = bpy.data.objects.get("ULF_Pivot.Front")
    if not pivot:
        return
    for con in pivot.constraints:
        if con.type == 'FOLLOW_PATH' and con.target:
            length = con.target.data.splines[0].calc_length()
            old_curve = bpy.data.objects.get("ULF_Track")
            if old_curve:
                old_curve["curve_length"] = length
            con.target["curve_length"] = length
            break

bpy.app.handlers.depsgraph_update_post.append(update_curve_length)
update_curve_length(None, None)

The script automatically detects the curve via the Follow Path constraint – regardless of its name. You can drop the tram into any scene and reassign it to any curve.

The Structure in Detail

The ULF has 4 real articulation points. At the front and rear, three segments each (driver cab plus two adjacent sections) are rigidly connected. This results in:

  • 6 empties at the pivot points (front wheels, 4 articulation joints, rear wheels)
  • 5 bones spanning from pivot to pivot
  • Connector pieces at the joints, positioned via helper empties with Copy Location and Copy Rotation constraints

Using It in Your Own Scenes

The tram can be appended into any Blender scene:

  1. File → Append → the tram file → Collection → ULF_Tram
  2. Create a Bezier curve as a track or use an existing one
  3. Change the Follow Path target on all ULF_Pivot empties to your curve
  4. Run the script
  5. Animate the Offset Factor of ULF_Pivot.Front – the entire tram follows

Note on Blender Versions

In Blender 5.0.1, the driver failed to read the custom property correctly (value remained at 0). In Blender 5.1, it works without issues. I recommend Blender 5.1 or later.

Availability

The model is available on two marketplaces:

🎉 Launch Offer: Until the end of April, the tram is available on Superhive at 50% off the regular price.

Both versions come complete with the rig, drivers, and the Python script. No external addons required.


Vavrinec

Meine 3D-Erfahrung reicht bis zu 3D Studio 4 unter DOS zurück; in den 1990ern arbeitete ich mit 3ds Max und Softimage. Seit 2011 bildet Blender die Basis meiner täglichen Produktion; je nach Projekt ergänze ich Adobe Substance 3D Painter, Affinity Photo, Affinity Designer und DaVinci Resolve – unter Linux Mint oder Windows. Du kannst präzise Modellierung, saubere Material-/Look-Entwicklung und klare, verlässliche Abläufe erwarten – von der ersten Skizze bis zum finalen Rendering. Ob Architektur- oder Produktvisualisierung, Standbild oder Animation: Ich verbinde technische Genauigkeit mit gestalterischer Klarheit, damit deine Inhalte ohne Umwege überzeugen.