Automatische Generierung von Terrain-Layern in Blender mit Python

Ein Python-Modul zur Erstellung gelayerter Terrain-Materials mit automatisch erzeugten Masken über Geometry Nodes

#Python#Projekte

Bei der Entwicklung von 3D-Spielen in Unity ist mir aufgefallen, dass ich 3D-Welten lieber zuerst in Blender aufbaue. Die Werkzeuge dort sind deutlich leistungsfähiger und flexibler als die integrierten Funktionen von Unity, besonders wenn es um die Gestaltung von Gelände geht.

Deshalb wollte ich eine Möglichkeit, schnell mehrere Textur-Ebenen auf ein Terrain-Mesh in Blender anzuwenden. Jede dieser Ebenen sollte eine eigene Maske besitzen, die steuert, in welchen Bereichen die jeweilige Textur sichtbar ist. Das manuelle Malen dieser Masken wäre nicht nur zeitaufwendig, sondern würde auch weniger organisch wirken. Daher sollten die Masken automatisch anhand von Gelände-Eigenschaften wie Höhe oder Neigung erzeugt werden. Außerdem war mir wichtig, den Prozess möglichst modular zu gestalten, um Ebenen flexibel hinzufügen, entfernen oder anpassen zu können.

Die Erzeugung der Masken lässt sich gut über Geometry Nodes realisieren, und die berechneten Masken können anschließend für die Weiterverarbeitung in Unity exportiert werden. Dank der Shader-Nodes in Blender ist es außerdem möglich, das Ergebnis direkt in der Vorschau zu betrachten und die spätere Optik des Terrains zu simulieren.

Da das wiederholte Erstellen dieser Node-Systeme bei vielen Layern jedoch mühsam und repetitiv ist, habe ich ein Python-Modul entwickelt, das diesen Ablauf automatisiert. Über das Blender-Modul bpy lassen sich Objekte und Node-Strukturen programmgesteuert erzeugen und bearbeiten, sowohl für Geometry Nodes als auch für Shader Nodes.

Funktionen

Mit dem Modul lassen sich unter anderem:

  • Beliebig viele Terrain-Layer mit individuellen Parametern definieren
  • Die Art der Masken-Erstellung pro Ebene auswählen, zum Beispiel:
    • Masken basierend auf Höhe
    • Masken basierend auf Neigung
    • Manuell gemalte Masken über Texture-Painting
  • Masken mit Prioritäten und Stärken überlagern und mischen
  • Ränder der Masken mit Rausch-Mustern versehen, um weichere und natürlichere Übergänge zu erzeugen
    • Rausch-Nodes werden automatisch wiederverwendet, wenn mehrere Ebenen dieselben Einstellungen nutzen, was die Performance verbessert
  • Ein Shader erzeugen, der die Ebenen anhand der Masken miteinander kombiniert
    • Farbtextur, Oberflächen-Details, Normalen und Höheninformationen werden maskenbasiert zusammengeführt und in einem Principled-BSDF-Shader ausgegeben
    • Wiederholungseffekte werden reduziert, indem die UV-Koordinaten über Rausch-Muster verzerrt werden
    • Eine zusätzliche, gedrehte Instanz der Grundtextur wird überlagert, um sichtbare Wiederholungsmuster weiter zu minimieren

Konfiguration

Die Terrain-Layer können über ein einfaches Python-Dictionary definiert werden, unterstützt durch Dataclasses für bessere Struktur und Type Checking. Hier ein Beispiel für eine mögliche Konfiguration:

# Two dual noise configs: default and an alternate for "Rock" dual_default = DualNoiseConfig(scale=6.0, large_scale=1.5, large_mix=0.35, detail=1.0) dual_alt = DualNoiseConfig(scale=10.0, large_scale=2.2, large_mix=0.55, detail=0.8) config = TerrainConfig( geometry_modifier_name="Terrain_Layer_Masks", layers=[ # Underwater layer: default layer for everything not covered by other layers # Uses uv warping and anti-tiling to improve texture appearance Layer( name="Underwater", priority=0, strength=1.0, ground_material=GroundMaterial( "Muddy ground with underwater moss", uv_scale=2.0, uv_warp=UVWarpConfig(), uv_anti_tiling=UVAntiTilingConfig(), ), ), # Beach layer: sandy areas near the shore # Uses height mask with noise to create natural transitions Layer( name="Beach", priority=10, strength=1.0, mask=HeightMask( min_height=1.0, max_height=6.5, ramp_low=0.35, ramp_high=0.55, ), mask_noise=MaskNoiseConfig( dual=dual_default, amount=2.0, sharpness=1.6, bias=0.0, zone_width=0.35, zone_softness=1.0, ), ground_material=GroundMaterial("Sand"), ), # Grass layer: mid-elevation grassy areas Layer( name="Grass", priority=20, strength=1.0, mask=HeightMask( min_height=3.5, max_height=8.0, ramp_low=0.45, ramp_high=0.65, ), mask_noise=MaskNoiseConfig( dual=dual_default, amount=1.8, sharpness=1.8, bias=0.0, zone_width=0.5, zone_softness=1.0, ), ground_material=GroundMaterial("Grass"), ), # Snow layer: high-elevation snowy areas Layer( name="Snow", priority=25, strength=1.0, mask=HeightMask( min_height=11.0, max_height=15.0, ramp_low=0.45, ramp_high=0.65, ), mask_noise=MaskNoiseConfig( dual=dual_default, amount=1.2, sharpness=2.2, bias=0.0, zone_width=0.3, zone_softness=1.2, ), ground_material=GroundMaterial("Snow"), ), # Rock layer: steep slope areas Layer( name="Rock", priority=27, strength=1.0, mask=SlopeMask( min_angle=25.0, max_angle=60.0, ramp_low=0.4, ramp_high=0.6, ), mask_noise=MaskNoiseConfig( dual=dual_alt, amount=2.2, sharpness=2.0, bias=0.0, zone_width=0.4, zone_softness=1.0, ), ground_material=GroundMaterial("Rock"), ), # Volcanos layer: specific areas defined by a paint mask Layer( name="Volcanos", priority=30, strength=1.0, mask=PaintMask( image_name="IMG_Terrain_VolcanosMask", ), ground_material=GroundMaterial("04 Vulcanic Rock Surface D"), ), ], )

Ergebnisse

Die folgenden Screenshots zeigen die automatisch erzeugten Geometry- und Shader-Nodes für ein Terrain mit mehreren Textur-Ebenen:

Erzeugte Geometry NodesErzeugte Shader Nodes

Und hier einige Render-Beispiele mit den generierten Terrain-Layern. Die Konfiguration und Ausführung des Skripts dauerten nur wenige Minuten, im Vergleich zu vielen Stunden manueller Arbeit.

Beispiel der Terrain-LayerDetailaufnahme der verzerrten UV-Textur

Zukunftsideen

Obwohl das Modul bereits jetzt ziemlich hilfreich ist, gibt es noch Erweiterungsmöglichkeiten. Da die Masken vorab berechnet werden, eignen sie sich nicht nur für Texturen, sondern auch für weitere Anwendungsfälle, wie etwa die Platzierung von Vegetation oder gezielte Verformungen des Geländes.

Denkbar wäre außerdem ein neuer Maskentyp für Straßen, bei dem Geometry Nodes automatisch ein Straßen-Mesh entlang eines in Blender definierten Bezier-Pfads erzeugen. Damit könnte das Projekt langfristig zu einer vollständigen Terrain-Generierungs-Pipeline wachsen, statt nur ein Texturierungs-Werkzeug zu sein.

Das Projekt ist auch hier zu finden:

Kommentare

Noch Fragen?