Zum Inhalt

Modelle und Assets

Alles, was du im Spiel siehst, ist ein Modell (.mdl). Es besteht aus Meshes (.msh plus .msh.blob) mit Materialien (.mtl), und die Materialien verweisen auf Texturen (.dds, .tga, .hdr). Das Basisspiel bringt 4.473 Modelle und 6.806 Materialien mit. Modelle, Mesh-Indexdateien, Materialien und entpackte Animationen (.ani) sind Lua-Textdateien mit einer Funktion data(). Binär sind nur die Mesh-Daten in .msh.blob und die Texturen (Resource types). Unter den entpackten Spieldateien, aus denen diese Seiten zitieren, gibt es keine .msh-, .msh.blob- oder .ani-Dateien. Ihr Format stammt deshalb allein aus dem Wiki.

Modelle importierst und bearbeitest du mit dem Model Editor, der zum Spiel gehört. Wie man mit ihm arbeitet (Einrichtung, Namensregeln beim FBX-Import, Metadaten- und Material-Editor, Prüfung, Icons), steht auf einer eigenen Seite: Model Editor.

Dateinamen und Pfade

Datei- und Ordnernamen schreibst du klein und ohne Leerzeichen, Textdateien speicherst du als UTF-8 ohne Byte Order Mark (siehe Dateiverweise im Inhalt). Ein Modell verweist meist relativ zu sich selbst auf seine Meshes und Materialien (msh/... und mat/... in Ordnern neben der .mdl), ein Material relativ zu sich auf seine Texturen (tex/...). Die absoluten Formen (/, ::/ für Dateien des Basisspiels, <modId>::/) erklärt dieselbe Seite. Das Wartehäuschen der Bushaltestelle unten nutzt mat/... für die eigenen Teile und /assets/stations/street/... für Teile, die es mit anderen Haltestellen teilt. Ein Mod, der diese Basisteile wiederverwendet, schreibt ::/assets/stations/street/....

Eine Modelldatei

Die Funktion data() einer .mdl-Datei gibt eine Tabelle mit den Schlüsseln boundingInfo, collider, lods, metadata und version zurück. version = 2 kennzeichnet das TF3-Format und unterscheidet es vom alten Modellformat aus Transport Fever 2 (Model definition). Hier das Wartehäuschen base/content/stations/street/street/small_stops/small_new.mdl (gekürzt):

function data()
return { 
        boundingInfo = { 
            bbMax = { 3.67032, 1.30928, 2.8806, },
            bbMin = { -3.60838, -1.08571, -0.181345, },
        },
        collider = { 
            params = { 
                halfExtents = { 3.63935, 1.1975, 1.53097, },
            },
            transf = { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, },
            type = "BOX",
        },
        lods = {
            { 
                node = { 
                    children = {
                        { 
                            materials = { "mat/bus_era_c_glass.mtl", },
                            mesh = "msh/shelter_era_c_left_1_post_glass_lod0.msh",
                            name = "shelter_era_c_left_1_post_glass",
                            transf = { 1, 0, 0, 0, 0, -1.62921e-07, -1, 0, 0, 1, -1.62921e-07, 0, 2.09323, 0.397267, 1.36608, 1, },
                        },
                        { 
                            materials = { "/assets/stations/street/mat/streetstation_1_transparent.mtl", "/assets/stations/street/mat/streetstation_1.mtl", },
                            mesh = "/assets/stations/street/msh/bus_stop_sign_era_c_1_lod0.msh",
                            name = "bus_stop_sign_era_c_1",
                            transf = { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -3.34, 0.9, 0, 1, },
                        },
                        -- ...
                    },
                    name = "RootNode",
                    transf = { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, },
                },
                visibleFrom = 0,
                visibleTo = 50,
            },
            -- weitere LODs mit visibleFrom/visibleTo 50-250 und 250-850
        },
        metadata = {
            lightSourceList = { ... },
        },
        version = 2,
    }
end

Bounding Box und Collider

boundingInfo ist der Quader um das ganze Modell, angegeben als Abstände vom Modellursprung (bbMin, bbMax). Der Renderer entscheidet damit, ob das Modell im Bild ist. Laut Wiki soll der Quader deshalb auch den Bereich der Lichter des Modells einschließen, sonst tauchen sie am Bildrand plötzlich auf. Der Model Editor berechnet ihn per Knopfdruck neu.

collider regelt Kollisionen zwischen Modellen. Das Wiki nennt vier Typen:

type Form
"MESH" die Geometrie des Mesh selbst, params = { }
"BOX" ein Quader der doppelten Größe von params.halfExtents, verschoben um transf
"CYLINDER" ein Zylinder der doppelten Größe von params.halfExtents, verschoben um transf
"POINT_CLOUD" die Punkte in params.points (relativ zum Ursprung); transf wird ignoriert

Die Dateien des Basisspiels verwenden BOX (4.464 Collider), CYLINDER (96) und NONE (41). Auch das Collider-Format für Konstruktionen, ColliderUtilSerialized.Collider, akzeptiert NONE. Kein Basismodell nutzt MESH oder POINT_CLOUD, und das Wiki erwähnt NONE nicht.

Detailstufen

lods ist eine Liste von Detailstufen (LODs). Jeder Eintrag hat einen Baum aus Knoten (node) und einen Entfernungsbereich von visibleFrom bis visibleTo in Metern. Mindestens ein LOD ist Pflicht. Das Wartehäuschen hat drei, mit getrennten Meshes _lod0, _lod1 und _lod2. Pro LOD können zwei weitere Schlüssel vorkommen:

  • textureLodBase = true markiert den ersten LOD, ab dem texturierte Materialien an die Stelle der übrigen treten, zum Beispiel bei Gebäuden, die ab LOD 1 auf gebackene Texturen umschalten. 616 Basismodelle setzen ihn.
  • static steht im Beispiel des Wikis (static = false) und in 82 Basismodellen. Das Wiki erklärt ihn nicht.

Die Leistungstipps des Wikis für LODs: Teile die Zahl der Dreiecke pro LOD etwa durch vier, ziele im letzten LOD auf ein paar hundert Dreiecke und halte die Zahl der Meshes, Materialien und sortierten (transparenten) Materialien pro LOD niedrig. Typische Fahrzeuge des Basisspiels haben 20.000 bis 50.000 Dreiecke, die größten bis zu 100.000 (guidelines). Weitergehende Regeln zu Draw Calls stehen unter Best practice.

Knoten

Ein Knoten hat einen name, eine 4×4-Matrix transf aus 16 Zahlen (relativ zum Elternknoten) und optional mesh, materials, animations und children. Die Regeln aus dem Wiki:

  • materials nennt pro Submesh des Mesh ein .mtl. Das Schild oben braucht zwei. Die Anzahl muss passen. Der Model Editor meldet eine Abweichung und repariert die Gruppe.
  • Über name sprechen die Metadaten den Knoten an (childId, group, meshId, loadIndicator). Der Name muss innerhalb eines LOD eindeutig sein, und der entsprechende Knoten in jedem anderen LOD sollte gleich heißen.
  • Meshes mit Skinning (Figuren, Tiere, Faltenbälge, manche Brücken- und Tunnelteile) verwenden skin und skinMaterials statt mesh und materials. Ihre children sind Knochen mit name und transf. 303 Basismodelle sind so aufgebaut.

Ein Fahrzeugmodell verschachtelt Knoten für bewegliche Teile. Der Dampflastwagen in base/content/vehicle/truck/steam_lorry_univ/steam_lorry_univ/steam_lorry_univ.mdl hat einen Knoten body neben einem Gruppenknoten, dessen Kind g1 wiederum w1 enthält.

Animationen

animations an einem Knoten ordnet Ereignisnamen Animationen zu. Transformator-Skripte (siehe transformatorConfig weiter unten) lösen die Ereignisse über ihren Namen aus. Es gibt drei Formen:

animations = {
    attack = { params = { id = "ani/attack/root.ani", }, type = "FILE_REF", },
    open = {
        forward = true,  -- false spielt die Keyframes rückwärts ab
        params = {
            keyframes = {
                { rot = { 0, 0, 0, }, time = 0, transl = { 0, 0, 0, }, },
                { rot = { 0, 0, 0, }, time = 1200, transl = { 0.75, 0.055, 0, }, },
            },
            origin = { 0, 0, 0, },
        },
        type = "KEYFRAME",
    },
    -- "KEYFRAME_MATRIX": Keyframes mit time und einem transf aus 16 Zahlen
},

Zeiten zählen in Millisekunden ab dem Start. Das Wiki empfiehlt FILE_REF, damit Modelldateien klein bleiben, und das Basisspiel hält sich daran: 539 Basismodelle nutzen FILE_REF, 6 nutzen KEYFRAME, keines KEYFRAME_MATRIX. Eine .ani-Datei gibt times und eine Liste transfs mit einer Matrix pro Zeitpunkt zurück. Beide Listen müssen gleich lang sein, und die erste Zeit muss 0 sein.

Im Beispiel des Wikis steht als Kommentar zu id „relative to res/models/animation/“. Das ist die Ordnerstruktur von Transport Fever 2. Die Dateien des Basisspiels verweisen relativ zum Modell auf Animationen, etwa id = "ani/die/cougar_root.ani" in base/content/animal/animal/cougar/cougar.mdl.

Meshes

Ein Mesh besteht aus zwei Dateien mit gleichem Namen: cube_lod_0.msh ist ein Textindex, cube_lod_0.msh.blob enthält die binären Vertex- und Indexdaten (Mesh definition). Die Indexdatei gibt zwei Listen zurück:

function data()
return {
    subMeshes = { ... },   -- ein Eintrag pro Submesh, jeder bekommt in der .mdl ein Material
    vertexAttr = { ... },  -- wo jedes Vertex-Attribut im Blob liegt
}
end

Der Blob hat zwei Abschnitte. Im ersten liegen die Vertex-Attribute als 4-Byte-Floats. Für jedes Attribut nennt die Indexdatei den offset, die Zahl der Floats pro Element (numComp) und die Zahl der Elemente (count). Der zweite Abschnitt enthält pro Submesh eine Liste von Indizes auf diese Elemente, ebenfalls über Offset und Anzahl angegeben. Die Attribute heißen position, normal, tangent, uv0, uv1, jointWeights, offset, positionAmbient und attrOffsetLOD. Ein Mesh hat nur die, die es braucht. Manche Materialtypen setzen bestimmte Attribute voraus: _UV1_-Materialien brauchen uv1, Skinning-Materialien brauchen Joint Weights. Fehlt ein Attribut, meldet der Model Editor „requires missing mesh attribute“.

Meshes schreibt niemand von Hand. Der Model Editor erzeugt sie beim Import einer .fbx-Datei.

Modell-Metadaten

metadata legt fest, was ein Modell im Spiel ist: ein Fahrzeug, ein Baum, eine Person, ein Teil eines Bahnhofs. Jeder Block ist optional. Über alle .mdl-Dateien des Basisspiels gezählt, kommen diese Blöcke am häufigsten vor:

Block Dateien Block Dateien
lightSourceList 703 transportVehicle 311
transformatorConfig 540 particleSystem 291
soundConfig 514 landVehicle 288
availability 471 cargoModel 242
description 469 versioning 201
emissions 449 railVehicle 168
transportNetworkProvider 402 loadIndicator 142
extent 376 roadVehicle 120
categoryList 357 tree 90
cost 351 colorConfig 85
seatProvider 343 airVehicle, waterVehicle 42, 28
maintenance 322 person, rock, animal 38, 23, 15

Die Teal-Definition ModelMetadata in api/tealdef/api/type.d.tl („Contains all metadata of a model. A specific metadata might be nil.“) typisiert nur einen Teil davon: Animal, Description, Category, CategoryList, MenuCategory, Availability, SoundConfig, Maintenance, Emission/Emissions, VehicleEngine, TransportVehicle, Cost, LandVehicle, WaterVehicle, AirVehicle, Person und Tree. Blöcke wie lightSourceList, particleSystem oder loadIndicator stehen in den Dateien, aber nicht in diesem Record. Die eigene Liste der Metadatenfelder des Model Editors mit Anzeigenamen und Werttypen liegt in base/content/model_editor/model_editor/meta_metadata.lua.

Das Wiki beschreibt die Blöcke, die die meisten Modelltypen gemeinsam haben, auf der Modellseite und die fahrzeugspezifischen unter Vehicles. Die Metadaten eines Fahrzeugs am Beispiel des Dampflastwagens (gekürzt):

metadata = { 
    availability = { yearFrom = 1905, yearTo = 1945, },
    cost = { price = -1, },
    description = { 
        description = _("VEHICLE_TRUCK_STEAM_LORRY_UNIV_DESCRIPTION"),
        name = _("VEHICLE_TRUCK_STEAM_LORRY_UNIV_NAME"),
    },
    emissions = { noise = { score = 15, }, pollution = { score = 18, }, },
    landVehicle = { 
        engines = { { power = 14, tractiveEffort = 5, type = "STEAM", }, },
        topSpeed = 6.944,
        weightEmpty = 1000,
        weightMaxPayload = 12000,
    },
    maintenance = { lifespan = 14610, runningCosts = -1, },
    -- in einem Mod: "::/vehicle/truck/shared/sound/truck_old.snd"
    soundConfig = { soundSet = { name = "/vehicle/truck/shared/sound/truck_old.snd", }, },
    transformatorConfig = { 
        skipFromLod = 2,
        -- in einem Mod: "::/vehicle/shared/default_road.trf"
        transformator = { name = "/vehicle/shared/default_road.trf", },
    },
    transportVehicle = { 
        carrier = "ROAD",
        compartments = {
            { 
                loadConfigs = {
                    { 
                        cargoEntry = { 
                            capacity = 40,
                            cargoTypeSet = { cargoClassesIncluded = { "UNIVERSAL", }, },
                            loadIndicator = "bay_0",
                            seats = { },
                        },
                        toHide = { },
                    },
                },
            },
        },
        engineTransportModes = { "TRUCK", },
        filterTags = { "default", },
        transportModes = { "TRUCK", },
    },
},

Die Verschachtelung compartments → loadConfigs → cargoEntry entspricht ModelMetadata.TransportVehicle. cargoEntry.loadIndicator = "bay_0" verweist auf den Eintrag metadata.loadIndicator.configs.bay_0.

Häufige Blöcke

Block Was er festlegt
description name und description (beide in _() für die Übersetzung) für den Fahrzeugladen und die Infofenster. In Konstruktionen werden sie nicht verwendet.
availability yearFrom, yearTo; fehlt ein Wert oder ist er 0, gibt es kein Anfangs- oder Endjahr
cost price; -1 schaltet die automatische Preisberechnung ein, ein fehlender price gilt als 0. priceScale multipliziert den berechneten Preis.
maintenance runningCosts (-1 für automatisch) mit runningCostScale; lifespan in Vierteltagen bei normaler Geschwindigkeit (365 × 4 pro Jahr, die 14610 des Lastwagens sind also zehn Jahre); maintenanceScale für den Verschleiß (bei 1,0 verliert das Fahrzeug seine ganze Wartung in einem Zehntel seiner Lebensdauer)
extent bbMin/bbMax wie bei der Bounding Box, von der Simulation genutzt, etwa für die Fahrzeuglänge beim Kuppeln
cameraConfig positions, jeweils mit group (Knotenname), transf und fov, für die Cockpitkamera, die sonst am ersten Platz der Besatzung sitzt
loadIndicator wie Gütermodelle angezeigt werden, siehe unten
labelList dynamische Textbeschriftungen, siehe unten
lightSourceList Lichtquellen, siehe unten
particleSystem Emitter für Rauch, Dampf, Staub und Flammen, siehe unten
transformatorConfig das Transformator-Skript, das Animationen und Partikel steuert, siehe unten

Die automatischen Kosten kommen aus den Basisskripten. ModelMetadataUtil in base/content/base/base/model_metadata_util.lua ersetzt einen negativen price durch round(maintenanceCost * 6 * (priceScale or 1)) und negative runningCosts durch round(maintenanceCost * (runningCostScale or 1)). In einem Satz des Wikis heißt das Feld runningCost. Die Dateien und das Codebeispiel des Wikis selbst schreiben runningCosts. maintenanceScale kommt in keinem Basismodell vor.

Ladeanzeigen

loadIndicator.configs ist eine Tabelle benannter Anzeigen (bay_0, slots_0, ...), auf die ein cargoEntry über den Namen verweist. Jede gehört zu einer dieser drei Arten:

  • cargoBay: ein Quader (bbMin, bbMax, relativ zum Knoten childId), der sich mit Gütermodellen füllt. type = "DISCRETE" legt cargoFormats wie "SMALL" oder "BIG" in einem Raster gridSize aus (x, y und optional Schichten in z) und skaliert sie nach sizePolicy ("STRETCH", "STRETCH_HEIGHT_SCALEY", "BEST_FIT" oder ohne Skalierung). type = "LEVEL" zeigt eine Schüttgutoberfläche, deren Höhe von der Ladung abhängt. Dazu gehört sizePolicy = "STRETCH".
  • cargoSlots: eine capacity und eine Liste configurations. Eine Konfiguration ist eine Liste von Füllstufen von leer bis fast voll, und jede Stufe nennt Slot-Indizes aus loadIndicator.slots. Bei mehreren Konfigurationen wird eine zufällig gewählt. Jeder Slot hat eine group (Knoten), eine Liste models, aus der zufällig gewählt wird, eine transf und eine randomId. Slots mit gleicher randomId wählen denselben Modellindex. Slot-Modelle sind echte .mdl-Dateien oder ein Verweis auf ein Güterformat wie "#RECT_11x2_5".
  • cargoLanes: für statische Modelle. nodes aus Position, Tangente und Breite bilden Bahnen mit Gütern in SMALL/BIG, dazu optional renderDistance.

Beschriftungen

labelList.labels setzt dynamischen Text auf ein Modell. Eine Beschriftung hat einen Knoten childId, eine transf an der unteren linken Ecke und eine size (Breite, Höhe; sie bestimmt auch die Schriftgröße). type wählt den Text: "LINE_NAME", "NEXT_STOP", "NAME", "COMPANY_NAME", "STATION_NAME", "CUSTOM" (aus einem labelText in der Konstruktion) oder "NONE". Weitere Schlüssel:

Schlüssel Werte
color, alpha, alphaMode RGB 0 bis 1; 0 bis 1; "CUTOUT", "BLEND", "NONE"
renderMode "EMISSIVE" für LCD-artige Anzeigen, sonst "STD"
alignment, verticalAlignment "LEFT"/"CENTER"/"RIGHT"; "BOTTOM"/"CENTER"/"TOP"
fitting "NONE" (läuft über), "CUT", "SCALE"
nLines Zahl der Zeilen
filter, params "NONE", "NUMBER" oder "CUSTOM" mit params.expr (regulärer Ausdruck), params.replace (\\0, \\1, ...) und für NEXT_STOP offset und relative
font nur die eingebauten Schriften Lato und Noto; andere bringen das Spiel zum Absturz

Lichtquellen

lightSourceList.lights enthält Lichter, die am Knoten aus meshId hängen und über transf platziert werden. Wird der Elternknoten skaliert, skaliert das Licht mit. Eine Animation, die ein Mesh ausblendet, schaltet also auch sein Licht aus.

Schlüssel Bedeutung
type "SPOT" (radius, angleMin, angleMax), "POINT" (radius), "CYLINDRICAL" und "CHEAP_CYLINDRICAL" (length, radius), "PARALLEL" (length, width, height)
mode "LIGHT", "ABSORBER" (dunkelt ab), "DISCARD" (schneidet Meshes weg, an Tunnel- und Treppeneingängen)
color, strength RGB 0 bis 1, Intensität
mask, invertMask Bitmaske gegen light_receiver.lightMask des Materials (1 Gelände, 2 außen, 4 Wasser, 8 Partikel, 32 innen). Außenlichter nutzen mask = 0 mit invertMask = true, Innenlichter von Fahrzeugen 32 mit invertMask = false.
backFaceLit beleuchtet auch Flächen, die vom Licht abgewandt sind
fromTime, toTime Stunden 0 bis 24; beide auf -1 schaltet das Licht bei Dunkelheit und starkem Regen ein
visibleFrom, visibleTo, fadeOutStart Sichtweite in Metern ab der Kamera, mit Ausblenden von fadeOutStart bis visibleTo

Das Wartehäuschen setzt visibleFrom = -1 und visibleTo = -1. Diesen Wert erklärt das Wiki nicht. Es rät zu wenigen, gut platzierten Lichtern und schlägt für Gebäudefenster Lichtmaterialien vor (map_id, map_lgt).

Partikel-Emitter

particleSystem.emitters ist eine Liste von Emittern. Jeder braucht child (den Knoten), frequency (Partikel pro Sekunde) und lifeTime. Optional sind unter anderem particleId (für den Transformator), position, velocity, gravity, albedoTexture, normalMapTexture, Einstellungen für Sprite Sheets (numFrames, frame, frameSpeed), animateInvisible und maxDistance (Standard 100). Zahlenwerte sind fest ({ value = 30 }) oder zufällig ({ randomMinMax = { min = 10, max = 30 } }). Schlüssel mit der Endung OverLifeTime (Alpha, Farbe, Geschwindigkeit, Größe) sind Kurven aus Punkten { time, value, easing } mit time von 0 bis 1. easing nimmt die üblichen Easing-Namen von Linear bis EaseInOutBounce. Die vollständige Schlüsseltabelle mit Standardwerten steht auf der Modellseite im Wiki. Halte frequency, lifeTime und maxDistance niedrig, und setze animateInvisible nur bei großen, langlebigen Partikeln wie dem Abdampf einer Dampflok.

Transformator-Konfiguration

In TF3 sind Animations- und Partikelereignisse nicht fest eingebaut, ein Transformator-Skript löst sie aus. transformatorConfig.transformator.name verweist auf eine .trf-Datei (die Vorgaben des Basisspiels sind /vehicle/shared/default_road.trf und /vehicle/train/shared/default_train.trf), und params reicht Werte des Modells an sie weiter, etwa pantographMin/pantographMax. Die Modelle des Basisspiels schreiben diese Pfade mit /. In einem Mod schreibst du ::/vehicle/shared/default_road.trf, denn ein Pfad mit / am Anfang zeigt in deinen eigenen Mod. So macht es carnival_waggon_1.mdl aus der Kampagne (mods/release/urbangames_campaign_mission_01/content/vehicle/vehicle/bus/carnival_waggon_1/). skipFromLod ist der erste LOD, der den Transformator nicht mehr ausführt. Fehlt der Wert, gilt laut Wiki 1. 438 Basismodelle nutzen skipFromLod = 2. Siehe Transformator-Typen und die vehicle advanced topics im Wiki.

Metadaten werden beim Laden nachbearbeitet

Das Basisspiel schreibt die Modell-Metadaten beim Laden um. base/content/base/base/base_mod.lua registriert eine Kette von loadModel-Modifiern, darunter:

addModifier("loadModel", model_metadata_util.addTransformatorConfig)
addModifier("loadModel", metadataanimationutil.createAnimationEventsRoadVehicle)
-- ...
addModifier("loadModel", model_metadata_util.toCompartmentList)
addModifier("loadModel", model_metadata_util.turnRoadAndRailToLandVehicle)
addModifier("loadModel", model_metadata_util.addVehicleExtent)

Alle Umwandler stehen in ModelMetadataUtil (addEmissionMetadata, addCostMetadata, convertCargoTypes, makeLoadIndicators, sortLods, addTextureLods, ...). Namen wie turnRoadAndRailToLandVehicle oder convertCargoTypeSet lassen vermuten, dass ältere Metadatenformate ins aktuelle umgewandelt werden. Einige davon (addTransformatorConfig, sortLods und die Funktionen createAnimationEvents*) wandeln nur Modelle vor version = 2 um. Ein neues Modell muss diese Blöcke also in ihrer endgültigen Form enthalten. Andere Werte, etwa Standard-Icons und Filter-Tags, werden auch bei Modellen mit version = 2 ergänzt (siehe Fahrzeuge). Dieselbe Datei leitet außerdem seatProvider aus transportVehicle.seats ab und füllt comfortFactor/priceFactor je Verkehrsträger, wenn sie -1 sind. Der Model Editor führt aus base/content/model_editor/model_editor/editor_base_mod.lua eine eigene Kopie des größten Teils dieser Kette aus. Ein Modell sieht im Editor deshalb so aus wie im Spiel.

Ein Mod kann sich an derselben Stelle einklinken. mods/release/urbangames_vehicles_no_end_year/content/mod.script.lua setzt in einem loadModel-Modifier für jedes Fahrzeug data.metadata.availability.yearTo = 0 (gezeigt unter Ressourcen-Modifier). Nach dem Laden liegen die Modelle in api.res.modelRep. Kampagnenmission 01 lässt das Pferdefuhrwerk in ihrer postRunFn schneller beladen, indem sie model.metadata["transportVehicle"].loadSpeed ändert.

Materialien

Eine .mtl-Datei wählt einen Materialtyp (type) und füllt seine Parameter. order sortiert Materialien desselben Typs fürs Rendern. Der Standard ist 0, und ein Innenraummaterial mit order = -1 wird vor den Fenstern desselben Typs gerendert (Material definition). Aus base/content/stations/street/street/small_stops/mat/bus_era_c.mtl (gekürzt):

function data()
return { 
        __version = "",
        order = 0,
        params = { 
            map_albedo = { 
                fragmentSamplers = { 
                    albedoTex = { 
                        fileName = "tex/bus_stop_era_c_alb.dds",
                        type = "TWOD",
                        wrapS = "REPEAT",
                        wrapT = "REPEAT",
                    },
                },
            },
            map_metal_gloss_ao = { 
                fragmentSamplers = { 
                    metalGlossAoTex = { fileName = "tex/bus_stop_era_c_mga.dds", type = "TWOD", wrapS = "REPEAT", wrapT = "REPEAT", },
                },
            },
            map_normal = { 
                fragmentSamplers = { 
                    normalTex = { fileName = "tex/bus_stop_era_c_nrm.dds", redGreen = true, type = "TWOD", wrapS = "REPEAT", wrapT = "REPEAT", },
                },
            },
            window_light_color = { 
                fragmentProperties = {
                    { color1 = { 0.92, 0.81, 0.72, }, color2 = { 0.9, 0.8, 0.7, }, },
                },
            },
            -- map_id, map_lgt, map_op_1, map_op_2, operation_1, operation_2, texcoord_scale, light_receiver ...
        },
        type = "PHYSICAL_NRML_MAP_OP_LIGHT",
    }
end

Jeder Parameter packt seine Werte in fragmentSamplers (Texturen), fragmentProperties (die meisten Einstellungen) oder vertexProperties (einige wenige wie fade_out_range). Das Wiki empfiehlt, Materialien im Model Editor zu bearbeiten, weil er diese Verschachtelung übernimmt.

Wie Materialtypen definiert sind

type nennt einen Materialtyp, der in base/content/rendering/ definiert ist (die Liste files in base/_content.json führt sie als lose Dateien rendering/*.mat.lua). rendering/physical_nrml_map_op_light.mat.lua deklariert legacyName = "PHYSICAL_NRML_MAP_OP_LIGHT", das kurze prefix, die Liste der Eigenschaften, die der Typ annimmt, seine Render-Passes und seine Technique:

function data()
return {
    hasSmoothLod = false,
    legacyName = "PHYSICAL_NRML_MAP_OP_LIGHT",
    needsTangentVertexAttrib = true,
    prefix = "PNOL",
    properties = { 
        { name = "map_metal_gloss_ao", id = "properties/map_metal_gloss_ao.prop",  },
        { name = "map_albedo", id = "properties/map_albedo.prop",  },
        { name = "map_normal", id = "properties/map_normal.prop",  },
        -- ...
    },
    renderPasses = {
        COLOR = { needsAlphaBlending = true, needsBlend4 = false, programName = "programs/color_nrml_map.prog" },
        DEPTH = { needsAlphaBlending = false, needsBlend4 = false, programName = "programs/depth.prog" },
        NORMAL = { needsAlphaBlending = false, needsBlend4 = false, programName = "programs/phys_nrml_map_op_light.prog" },
    },
    technique = "techniques/standard_nrml_map_op.tec",
    transparent = false,
    order = 10,
}
end

Jeder Schlüssel unter params in einer .mtl entspricht hier einem name in den Eigenschaften, und eine Eigenschaftsdatei wie rendering/properties/map_albedo.prop.lua deklariert die Sampler, die sie bindet (albedoTex). Shader-Programme sind .prog.lua-Dateien in rendering/programs. Das prefix ist der Kurzcode, den der Model Editor anzeigt. Mit ihm wählt ein FBX-Materialname seinen Typ (mymaterial|PNOL, siehe Model Editor).

Unter einem Materialtyp liegen weitere Rendering-Dateien, alle in base/content/rendering/. technique nennt eine .tec.lua-Datei (32 im Basisspiel). Sie deklariert für jeden Render-Pass die Vertex-, Instanz- und Storage-Eingaben und die Topologie. Jeder programName nennt eine .prog.lua-Datei (223, davon 8 Konsolenvarianten unter programs/xbox/ und programs/ps5/). Ein Programm listet seine defines, seine Descriptor Sets (.desc.lua in programs/descriptors/, die Bindungen von Samplern und Uniform Buffern) und seine shaderStages, deren sourceFiles Shader-Quellen wie shaders/mat/fs/color/std.fs sind. Eine Eigenschaftsdatei (.prop.lua, 47 Stück) deklariert jeden Wert eines Parameters mit Typ und Standardwert. Aus rendering/programs/color_nrml_map.prog.lua (gekürzt):

descriptorSets = { "descriptors/phys_world.desc", "descriptors/phys_material_type.desc", "descriptors/set_36.desc", },
shaderStages = {
    {
        sourceFiles = { "shaders/mat/fs/color/std.fs", "shaders/util/lighting.fs", --[[ ... ]] },
        stage = "FRAGMENT",
    },
    {
        sourceFiles = { "shaders/mat/vs/std/color.vs", "shaders/util/fade_out.glsl", "shaders/util/inst_attr.vs", "shaders/util/fog.glsl", },
        stage = "VERTEX",
    },
},

Die Shader-Quellen (.fs, .vs, .glsl und andere) stehen in base/_content.json, gehören aber nicht zu den entpackten Dateien, aus denen diese Seiten zitieren. Für jeden dieser Dateitypen gibt es einen Ressourcen-Modifier (loadMaterialType, loadTechnique, loadProgram, loadDescriptorSet, loadProperty). Ein Materialtyp, der anders aussehen soll als die vorhandenen, braucht eigene Shader. Die upload guidelines verbieten eigene Shader-Dateien nur auf Konsolen und stellen für den PC keine solche Regel auf. Wie man einen Shader für das Spiel schreibt, erklären aber weder sie noch eine andere Wiki-Seite.

Materialtypen

Das Basisspiel definiert 51 Materialtypen. Die Tabellen im Wiki führen 43 auf und sprechen von „over 40“. Sie lassen sich in diese Gruppen einteilen (Präfix in Klammern):

Gruppe Typen Verwendung
Physikalisch, deckend PHYSICAL (P), PHYSICAL_OP (PO), PHYSICAL_NRML_MAP (PN), PHYSICAL_NRML_MAP_CBLEND (PNC), PHYSICAL_NRML_MAP_CBLEND_DIRT (PNCD), PHYSICAL_NRML_MAP_OP (PNO), PHYSICAL_NRML_MAP_CBLEND_OP (PNCO), PHYSICAL_NRML_MAP_UV1_AO (PNUA), PHYSICAL_NRML_MAP_OP_UV1_AO (PNOUA), ..._OP_UV1_AO_MASK (PNOUAM), ..._OP_DIRT_UV1_AO_MASK_DIRT_MASK (PNODUAM) die meisten Gebäude, Assets und Fahrzeugkarosserien
Physikalisch, transparent PHYS_TRANSPARENT (T), _OP (TO), _UV1_OP (TUO), _NRML_MAP (TN), _NRML_MAP_OP (TNO), _NRML_MAP_SMOOTH_LOD (TNS), _NRML_MAP_UV1_OP (TNUO), _NRML_MAP_UV1_OP_SMOOTH_LOD (TNUOS), _NRML_MAP_CBLEND (TNC), _NRML_MAP_CBLEND_DIRT (TNCD) Glas, Fahrzeugfenster, verzierte Zäune
Physikalisch mit Licht PHYSICAL_NRML_MAP_LIGHT (PNL), PHYSICAL_NRML_MAP_OP_LIGHT (PNOL), PHYSICAL_NRML_MAP_OP_UV1_AO_LIGHT (PNOUAL), PHYS_TRANSPARENT_NRML_MAP_LIGHT (TNL), PHYS_TRANSPARENT_NRML_MAP_OP_UV1_AO_MASK_LIGHT (TNOUAML) beleuchtete Fenster von Stadtgebäuden, Bahnhöfen und Industrien
Skinning SKINNING_TEXTURED (S), SKINNING_PHYS_CBLEND4 (SC), SKINNING_PHYS_NRML_MAP (SN), ..._NRML_MAP_OP (SNO), ..._NRML_MAP_CBLEND4 (SNC), ..._NRML_MAP_CBLEND_DIRT (SNCD), SKINNING_PHYS_TRANSPARENT_NRML_MAP (STN) mit _OP (STNO), _CBLEND4 (STNC), _CBLEND_DIRT (STNCD) Meshes mit Knochen: Menschen, Tiere
Sonstige BILLBOARD (B), BILLBOARD_MULTI (BM), EMISSIVE (E), emissiv mit Skinning (SE), LEAF_CARD (L), PHYSICAL_GLOSS_ONLY (PG), PHYSICAL_OP_GLOSS_ONLY (POG) ferne Baum-LODs, leuchtende Flächen, Baumlaub

Welche Maps ein Typ annimmt, steht in den Tabellen des Wikis. Verbindlich für einen Typ ist die Liste properties in seiner rendering/*.mat.lua. Meshes mit Skinning können bis zu 40 Knochen haben, und jeder Vertex folgt höchstens vier davon, mit Gewichten aus der Modellierungssoftware.

In diesen Punkten weichen die Spieldateien von den Tabellen im Wiki ab:

  • Das Wiki nennt den emissiven Skinning-Typ SKINNED_EMISSIVE. In rendering/skinning_emissive.mat.lua steht legacyName = "SKINNING_EMISSIVE", und so schreiben ihn auch die .mtl-Dateien.
  • Acht Typen gibt es in rendering/, aber nicht im Wiki: BILLBOARD2 (B2, 49 Basismaterialien), PHYSICAL_NRML_MAP_CBLEND4 (PNC4), PHYSICAL_NRML_MAP_CBLEND_DIRT_LOGO (ohne Präfix), PHYSICAL_NRML_MAP_OP_UV1_AO_MASK_LIGHT (PNOUAML), PHYSICAL_NRML_MAP_OP_UV1_LIGHT (PNOUL), PHYS_TRANSPARENT_DIFFUSE (TD, das Platzhaltermaterial des Importers), PHYS_TRANSPARENT_NRML_MAP_AO_SMOOTH_LOD und PHYS_TRANSPARENT_NRML_MAP_CBLEND_DIRT_UV1_OP (TNCDUO).
  • PHYS_TRANSPARENT_NRML_MAP_AO_SMOOTH_LOD nutzen 288 Basismaterialien. Der Typ teilt sich das Präfix TNS mit PHYS_TRANSPARENT_NRML_MAP_SMOOTH_LOD, das Präfix allein wählt also keinen der beiden eindeutig aus.

Textur-Sampler

Ein Map-Parameter enthält genau einen Sampler. Alle Sampler-Schlüssel:

map_albedo_opacity = { 
    fragmentSamplers = { 
        albedoOpacityTex = { 
            fileName = "tex/bus_d40_albo.dds",
            type = "TWOD",                       -- TWOD, CUBE_MAP, TWOD_ARRAY
            magFilter = "LINEAR",                -- NEAREST, LINEAR
            minFilter = "LINEAR_MIPMAP_LINEAR",  -- NEAREST, LINEAR, LINEAR_MIPMAP_LINEAR, LINEAR_MIPMAP_NEAREST, NEAREST_MIPMAP_NEAREST
            wrapS = "CLAMP_TO_EDGE",             -- REPEAT, CLAMP_TO_EDGE
            wrapT = "CLAMP_TO_EDGE",
            maxDegreeOfAnisotropy = -1,
            mipmapAlphaScale = 0,
            mipmapBaseLevel = 0,
            redGreen = false,
            scaleDownAllowed = true,
            textureLodOffset = 0,
        },
    },
},

wrapS/wrapT wählen zwischen Kacheln und Abschneiden am Rand. maxDegreeOfAnisotropy = -1 ist der empfohlene Standard. mipmapAlphaScale skaliert das Alpha in Mipmaps, die aus .tga-Dateien erzeugt werden, und macht Cutout-Kanten weicher. mipmapBaseLevel erzwingt eine kleinere Mipmap einer .dds. redGreen kennzeichnet eine Normal Map mit zwei Kanälen. scaleDownAllowed erlaubt Texture Streaming, und textureLodOffset verschiebt die gestreamte Mipmap. Die Liste der Materialfelder im Model Editor (base/content/model_editor/model_editor/meta_material.lua) kennt zusätzlich ein Flag compressionAllowed, das das Wiki nicht erwähnt.

Maps

Map Inhalt
map_albedo, map_color Farbe, kein Alpha
map_color_alpha Farbe mit Transparenz an/aus
map_albedo_opacity Farbe mit 0 bis 100 % Transparenz
map_albedo_gloss Farbe mit Glanz im Alpha (alter Train-Fever-Stil)
map_emissive Lichtfarbe, nur für EMISSIVE
map_metal_gloss_ao R Metall, G Glanz, B Ambient Occlusion (0 = volle Verschattung)
map_ao eigene Ambient Occlusion auf dem zweiten UV-Satz
map_normal Normal Map im Tangentenraum
map_cblend vier Masken für die Farbmischung in RGBA, mit colorConfig bei Figuren
map_cblend_dirt_rust R Farbmischung, G Schmutz, B Roststärke
map_dirt, map_rust, map_dirt_normal, map_rust_normal eigene Schmutz- und Rosttexturen; ohne sie gelten die gemeinsamen aus vehicle/shared/mat/tex/
map_op_1, map_op_2 Overlay-Texturen für die beiden Operationen
map_id 8-Bit-Graustufen mit IDs für Fensterlicht: Flächen mit gleichem Wert schalten gemeinsam ein. Erlaubt sind 0, 255 oder 4 + 8 × n. Nutze NEAREST-Filter, CLAMP_TO_EDGE und scaleDownAllowed = false.
map_lgt der Rotkanal maskiert die Stärke des Fensterlichts (Vorhänge, eine dunklere Oberkante)

Die Kanäle der Farbmischung wirken umgekehrt: Je dunkler der Kanal, desto stärker die Umfärbung. Setze color_blend.albedoScales auf die Helligkeit der ursprünglichen Texturfarbe in diesem Bereich, damit umgefärbte Teile zu anderen Modellen passen.

Operationen

operation_1 und operation_2 mischen map_op_1/map_op_2 in die Albedo, etwa um sich wiederholende Wandtexturen aufzulockern:

operation_1 = {
    fragmentProperties = {
        {
            op = "OVERLAY",        -- NO_OP, MULTIPLICATION, OVERLAY, LINEAR_BURN, ALPHA_BLEND
            mode = "NORMAL",       -- TEXCOORD, WORLD_XY, NORMAL, OFFSET_TEXCOORD
            scale = { 0.05, 0.5 },
            opacity = 0.5,
        },
    },
},

MULTIPLICATION multipliziert die Kanäle (dunkler), OVERLAY multipliziert unter 0,5 und hellt darüber auf (Screen), LINEAR_BURN addiert und zieht 1 ab, und ALPHA_BLEND legt das Overlay dort über die Albedo, wo es deckend ist (Moos am Fuß eines Baumstamms). mode wählt die Koordinaten des Overlays: modell-lokal (NORMAL), Welt (WORLD_XY), UV (TEXCOORD) oder UV mit zufälligem Versatz (OFFSET_TEXCOORD).

Eigenschaften

Parameter Schlüssel Hinweise
light_receiver lightMask, isLegacyMaterial, disableShadowMapWrite, lightMaxDistanceScale Bits von lightMask wie bei den Lichtern: 2 für die meisten Modelle, 32 für Fahrzeuginnenräume, 34 für Figuren. isLegacyMaterial ist nur für umgewandelte TF2-Texturen gedacht.
albedo_scale albedoScale (RGB) färbt eine Textur in mehrere Farbvarianten ein
alpha_scale alphaScale skaliert die Transparenz
emissive_scale emissiveScale (RGB), fromTime, toTime für EMISSIVE; Zeiten auf -1 folgen Tageslicht und Wetter
normal_scale normalScale Skalieren des Knotens ändert auch die Stärke der Normalen
alpha_test alphaThreshold, a2CThreshold, cutout, sorted, disableDepthAndNormalWrite Schwelle für Schatten, Schwelle für Sichtbarkeit, Transparenzmodus und Sortierung
color_blend albedoScales, color oder colors ein Wert für die rote Maske, vier für SKINNING_PHYS_NRML_MAP_CBLEND4
dirt_rust age, dirtColor, dirtOpacity, dirtScale, rustColor, rustOpacity, rustScale age 0 bis 1 für Modelle, die vom Spiel kein Alter bekommen
fade_out_range fadeInStartDist, fadeInEndDist, fadeOutStartDist, fadeOutEndDist in vertexProperties; blendet aus, statt schlagartig zu verschwinden
polygon_offset factor, units, forceColorWrite, forceDepthWrite verschiebt die Tiefe; setze factor und units auf denselben kleinen negativen Wert und forceDepthWrite beim Material mit dem niedrigsten Offset
two_sided twoSided, flipNormal rendert die Flächen doppelt; setze flipNormal bei Normal Maps
tex_animation uvSpeed, frameSize, frameSpeed laufende UVs oder ein Sprite-Raster
window_light_color color1, color2, fromTime, toTime Farben beleuchteter Fenster

Im Basisspiel setzen 1.162 .mtl-Dateien isLegacyMaterial = true und 5.585 setzen es auf false, obwohl das Wiki das Flag für alte Texturen aus Transport Fever 2 vorsieht. Die Standardvorlage des Editors für Schmutz setzt außerdem dirtFactor und rustFactor, die das Wiki nicht aufführt.

Transparenz

Deckende Typen werden vor transparenten gerendert, und alles hinter einer transparenten Fläche muss vorher gerendert werden, sonst verschwindet es. Das Wiki nennt vier Mittel in dieser Reihenfolge. Erstens order innerhalb eines Typs. Zweitens alpha_test.cutout = true: Es macht aus der Transparenz ein Dithermuster (sauber bei 0, 25, 50, 75 und 100 %, dazwischen körnig) und passt zu Geländern und Laub. Drittens alpha_test.sorted = true: Es sortiert Meshes nach dem Abstand ihres Ursprungs zur Kamera und kostet zusätzliche Draw Calls, gehört also nicht in niedrige LODs. Viertens polygon_offset, das die Fläche im Tiefenpuffer nach hinten schiebt, etwa bei Scheinwerfern hinter Glas.

Texturen

Das bevorzugte Texturformat ist .dds mit Kompression und Mipmaps (Resource types). Die unterstützten Kompressionen:

Format Verwendung
BC1/DXT1 kein Alpha oder Alpha an/aus; kleinste Dateien
BC2/DXT3 4-Bit-Alpha
BC3/DXT5, BC7 weiches Alpha
BC5/3Dc (ATI2A2XY) Normal Maps; laut Wiki für sie Pflicht
BC4/R8/L8 8-Bit-Graustufenmasken wie map_id

Für Grafikkarten von ATI und Intel müssen die Größen Zweierpotenzen von 16 bis 4096 sein. .tga ist für Grafiken der Oberfläche gedacht. Icons liegen für 4K-Bildschirme in doppelter Größe vor, mit der Endung @2x.tga. Für Modelltexturen nutzt du .tga nur während der Arbeit: Der Model Editor wandelt .tga-Texturen beim Speichern eines Modells in .dds um. .hdr steht ohne weitere Angaben in der Liste der Texturformate. Werkzeuge, die DDS exportieren, findest du unter External tools.

Materialien verweisen relativ zur Materialdatei (tex/...) oder absolut (/placeholders/mat/tex/default_map_id.dds) auf Texturen. Das Basisspiel benennt sie mit einer Endung nach der Map: _alb, _albo, _nrm, _mga, _cdr, _cblend, _id, _e (Lichtmaske) und so weiter. Der Model Editor verlässt sich beim Import einer .fbx auf diese Endungen. Die vollständige Tabelle steht auf der Seite Model Editor.

texture_metadata_cache.lua im Hauptordner jedes Mods und von base/ listet jede Textur mit channels, extent, numMipmaps und numStrippedMipmaps:

["animal/bird_crane/mat/tex/bird_crane_albedo_opacity.dds"] = { 
    channels = 4,
    extent = { 512, 512, },
    numMipmaps = 10,
    numStrippedMipmaps = 0,
},

Ob ein Mod diesen Cache mitliefern muss, ist offen. Mehr dazu unter Vom Spiel erzeugte Dateien.

Weitere Asset-Dateien

Endung Beispiel Inhalt
.gtex.lua base/content/stations/air/air/helipad/helipad.gtex.lua Bodentextur: eine Textur mit Materialindizes plus materialIndexMap vom Farbindex auf .tmat-Geländematerialien
.tmat.lua ::/terrain/materials/asphalt_02/asphalt_02.tmat Geländematerial, siehe Geländematerialien
.snd.lua ::/vehicle/truck/shared/sound/truck_old.snd Soundset, auf das soundConfig.soundSet.name verweist, siehe Soundsets
.trf.lua ::/vehicle/shared/default_road.trf Transformator, auf den transformatorConfig verweist (Transformator-Typen)
.ani ani/die/cougar_root.ani (neben ::/animal/cougar/cougar.mdl) Animation: times in Millisekunden und eine Matrix transfs pro Zeitpunkt
.wav, .ogg Soundeffekte, Musik

Offizielles Wiki: Resource types, Model definition (.mdl), Mesh definition (.msh), Material definition (.mtl), Model Editor, Best practice.