Unity durchsuchen

Light-Baked-Prefabs auf Mobilgeräten einsetzen

und weitere einfache Tricks, wie Sie 60 fps auf Low-End-Geräten erreichen

Last updated: January 2019

What you will get from this page: Michelle Martin, software engineer at MetalPop Games explains how they optimized their new mobile strategy game, Galactic Colonies, for a range of mobile devices, so they could reach as many potential players as possible.

MetalPop Games faced the challenge of making a game in which players could build huge cities on their low-end devices, without their framerate dropping or their device overheating. See how they found a balance between good-looking visuals and solid performance.

Städtebau auf Low-End-Geräten

As powerful as mobile devices are today, it is still very difficult to run large and good looking game environments at a solid frame rate. Achieving solid 60fps in a large scale 3d environment on an older mobile device can be quite a challenge.

As developers we could just target high-end phones and assume that most players will have good enough hardware to run our game smoothly. But this will result in locking out a huge amount of potential players, as there are still many older devices in use. Those are all potential customers you don’t want to exclude if it can be avoided.

In our game Galactic Colonies, the players colonize alien planets and can build huge colonies made up from a large number of individual buildings. While smaller colonies might only have a dozen buildings, larger ones can easily have hundreds of them.

This is what our goal list looked like when we started building our pipeline:

  • We want huge maps with a high amount of buildings
  • We want to fast run on cheaper and/or older mobile devices
  • We want nice looking lights and shadows
  • We want an easy and maintainable production pipeline
Beleuchtung in mobilen Spielen: Die üblichen Herausforderungen

Eine gute Beleuchtung im Spiel ist der Schlüssel dafür, dass die 3D-Modelle fantastisch aussehen. In Unity ist das natürlich wirklich einfach: Erstellen Sie das Level, platzieren Sie die dynamische Beleuchtung und alles ist fertig. Und wenn Sie auf die Performance achten müssen, nutzen Sie Light-Baking und fügen einige SSAO und andere Hingucker über den Post-Processing-Stack hinzu. Und schon ist alles bereit!

Die Beleuchtung in mobilen Spielen erfordert oft zahlreiche Tricks und Umwege. Falls Sie nicht gerade auf High-End-Geräte abzielen, sollten Sie beispielsweise lieber keine Post-Processing-Effekte verwenden. Auch große Szenen mit dynamischer Beleuchtung verringern die Framerate dramatisch.

Echtzeit-Beleuchtung kann sehr aufwändig sein, selbst auf einem Desktop-PC. Auf Mobilgeräten sind die Grenzen sogar noch enger gesteckt und Sie werden nicht alle wunderbaren Funktionen verwirklichen können, die Sie gerne hätten.

Light-Baking als Rettung?

Auf Mobilgeräten sind die Ressourcen recht eingeschränkt und Sie möchten die Akkus Ihrer Nutzer nicht mehr als nötig belasten, indem Sie zu viel Beleuchtung in Ihrer Szene verwenden.

Wenn Sie ein Gerät immer wieder an die Grenzen seiner Hardware bringen, erhitzt es sich – und fährt herunter, um sich zu schützen. Deshalb würden Sie normalerweise überall dort Light-Baking durchführen, wo keine Echtzeit-Schatten notwendig sind.

Unity light baked prefabs for mobile MetalPopGames Galactic Colonies

(Ein typisches Level in "Galactic Colonies" mit vielen Gebäuden)

Für uns war das keine Option, da unsere Spielwelt in Echtzeit vom Spieler erbaut wird. Da ständig neue Regionen entdeckt, zusätzliche Gebäude gebaut oder bestehende Gebäude verbessert werden, konnten wir kein effizientes Light-Baking verwenden.

Durch Schein zum Sein

Der Light-Baking-Prozess ist die Vorausberechnung von Highlights und Schatten für eine (statische) Szene und die Speicherung dieser Informationen in einer Lightmap.

Dieser Prozess teilt dem Renderer mit, wo ein Modell heller oder dunkler sein soll, sodass die Illusion von Licht erzeugt wird.

Das Rendering verläuft auf diese Art sehr schnell, da alle arbeitsintensiven und langsamen Lichtberechnungen bereits offline erledigt wurden. Zur Laufzeit muss der Renderer (Shader) das Ergebnis nur in einer Textur ablesen.

Allerdings sind dadurch auch zusätzliche Lightmap-Texturen vorhanden, die die Build-Größe erhöhen und zur Laufzeit zusätzlichen Textur-Speicherplatz erfordern.

Sie verlieren weiterhin Speicherplatz, da Ihre Meshes Lightmap-UVs benötigen und dadurch etwas größer werden.

Aber im Großen und Ganzen bedeutet dieser Prozess eine enorme Beschleunigung.

Unity light baked prefabs for mobile MetalPopGames

(Ein Gebäude mit und ohne Lightmap)

Doch einfach auf die Bake-Schaltfläche zu klicken, wird nicht funktionieren, wenn Sie eine dynamische Welt haben, die vom Spieler ständig verändert werden kann.

Wir stehen vor einer Reihe von Problemen, die beim Light-Baking für hochmodulare Szenen entstehen.

Light-Baking ist bei Prefabs nicht so einfach

Zunächst einmal werden die Light-Baking-Daten in Unity gespeichert und direkt den Szenendaten zugewiesen. Das ist kein Problem, wenn Sie individuelle Level und vorerstellte Szenen und nur eine Handvoll dynamischer Objekte haben. Sie können ein Beleuchtungs-Prebake durchführen und alles ist erledigt.

Das funktioniert jedoch selbstverständlich nicht, wenn Sie Ihre Level dynamisch erstellen. In einem Stadtbau-Spiel ist die Welt nicht im Voraus erstellt, sondern wird größtenteils dynamisch und spontan durch den Spieler erschaffen, der entscheidet, was er baut und wohin er es baut. Dies wird üblicherweise durch die Instanziierung von Prefabs verwirklicht, wo immer ein Spieler etwas bauen möchte.

Die einzige Lösung für dieses Problem ist, alle relevanten Light-Baking-Daten im Prefab statt in der Szene zu speichern.

Leider gibt es keinen einfachen Weg, die Daten, die die zu verwendende Lightmap und ihre Koordinaten bestimmen, zu kopieren und in ein Prefab zu skalieren.

Eine Pipeline für Light-Baked-Prefabs erstellen

Die beste Herangehensweise, um eine solide Pipeline für Light-Baked-Prefabs zu erreichen, ist, die Prefabs in einer anderen, separaten Szene (tatsächlich mehreren Szenen) zu erstellen und sie ins Spiel zu laden, wenn sie benötigt werden.

Jeder modulare Bereich durchläuft den Light-Bake-Prozess und wird dann ins Spiel geladen, wenn er gebraucht wird.

Wenn Sie sich das Light-Baking in Unity genauer ansehen, werden Sie erkennen, dass das Rendern eines Light-Baked-Meshes eigentlich nur die Anwendung einer anderen Textur darauf ist und das Mesh etwas heller oder dunkler (manchmal auch farbiger) wird. Sie benötigen lediglich eine Lightmap-Textur und die UV-Koordinaten – beides wird von Unity während des Light-Baking-Prozesses erstellt.

Während des Light-Baking-Vorgangs erstellt Unity einen neuen Satz UV-Koordinaten (die auf die Lightmap-Textur hinweisen) und einen Offset und die Skalierung für das individuelle Mesh. Das Re-Baking der Beleuchtung verändert diese Koordinaten jedes Mal.

Wie man UV-Kanäle einsetzt

Um eine Lösung für dieses Problem zu finden, ist es hilfreich zu verstehen, wie UV-Kanäle funktionieren und wie man sie am besten nutzt.

Jedes Mesh kann über mehrere Sätze von UV-Koordinaten verfügen (in Unity "UV-Kanäle" genannt). In den meisten Fällen ist ein UV-Satz ausreichend, da die verschiedenen Texturen (Diffuse, Spec, Bump usw.) die Informationen alle am gleichen Ort des Bildes speichern.

Aber wenn Objekte sich eine Textur wie eine Lightmap teilen und die Informationen eines ganz genauen Ortes in einer großen Textur abfragen müssen, führt oft kein Weg daran vorbei, einen weiteren Satz UVs hinzuzufügen, der für diese geteilte Textur genutzt wird.

Der Nachteil mehrerer UV-Koordinaten ist, dass sie zusätzlichen Speicherplatz benötigen. Wenn Sie zwei UV-Sätze statt nur einen benutzen, verfügt in der Folge JEDER einzelne der Vertices des Meshes über die doppelte Anzahl von UV-Koordinaten. Für jeden Vertex speichern Sie zwei zusätzliche Floating-Point-Zahlen und laden diese beim Rendern zur GPU hoch.

Erstellung der Prefabs

Mit Unity werden die Koordinaten und die Lightmap über die reguläre Light-Baking-Funktionalität erzeugt. Die Engine schreibt die UV-Koordinaten für die Lightmap in den zweiten UV-Kanal des Modells. Es ist wichtig zu wissen, dass der primäre Satz von UV-Koordinaten dafür nicht benutzt werden kann, da das Modell nicht gewrappt sein darf.

Stellen Sie sich eine Kiste vor, die dieselbe Textur für jede ihrer Seiten verwendet: Die einzelnen Seiten der Kiste haben alle die gleichen UV-Koordinaten, da sie alle dieselbe Textur wiederverwenden. Das funktioniert jedoch nicht mit einem Lightmap-Objekt, da jede Seite der Kiste individuell von Licht und Schatten getroffen wird. Jede Seite benötigt einen eigenen Platz mit individuellen Beleuchtungsdaten auf der Lightmap. Dementsprechend brauchen wir einen neuen Satz UVs.

Um also ein neues Light-Baked-Prefab zu erstellen, müssen wir lediglich die Textur und ihre Koordinaten speichern, sodass sie nicht verloren gehen, und diese in das Prefab kopieren.

Nach dem Light-Baking lassen wir ein Script ablaufen, das alle Meshes in der Szene durchläuft und die UV-Koordinaten in den tatsächlichen UV2-Kanal des Meshes schreibt, wobei die Werte für Offset und Scaling angewendet werden.

Der Code zur Änderung der Meshes ist relativ leicht zu durchschauen:

Mesh meshToModify = GetComponent().sharedMesh;
Vector4 lightmapOffsetAndScale = GetComponent().lightmapScaleOffset;

Vector2[] modifiedUV2s = meshToModify.uv2;
for (int i = 0; i < meshToModify.uv2.Length; i++)
{
    modifiedUV2s[i] = new Vector2(meshToModify.uv2[i].x * lightmapOffsetAndScale.x +
    lightmapOffsetAndScale.z, meshToModify.uv2[i].y * lightmapOffsetAndScale.y +
    lightmapOffsetAndScale.w);
}
meshToModify.uv2 = modifiedUV2s;

Um es noch einmal deutlicher zu sagen: Dies passiert mit einer Kopie der Meshes und nicht mit dem Original, da wir während des Baking-Prozesses weitere Optimierungen an unseren Meshes durchführen.

Die Kopien werden automatisch erstellt, in ein Prefab gespeichert und ihnen wird ein neues Material mit einem Custom-Shader und der neu erstellten Lightmap zugewiesen. Unsere Original-Meshes bleiben dabei unberührt und die Light-Baked-Prefabs sind sofort einsatzbereit.

Ein angepasster Lightmap-Shader

Ein angepasster Lightmap-Shader vereinfacht den Workflow erheblich. Um den Stil und das Aussehen der Grafik zu aktualisieren, öffnen Sie einfach die entsprechende Szene, nehmen alle gewünschten Änderungen vor und klicken auf "Start", um den automatisierten "Bake and Copy"-Prozess zu starten. Wenn dieser Vorgang beendet ist, ist alles erledigt und das Spiel verwendet ab diesem Zeitpunkt die aktualisierten Prefabs und Meshes mit der aktualisierten Beleuchtung.

Die eigentliche Lightmap-Textur wird über einen Custom-Shader hinzugefügt, der während des Renderns die Lightmap als zweite Textur auf das Modell anwendet.

Der Shader ist sehr einfach und kurz, und neben der Anwendung der Farbe und der Lightmap berechnet er einen einfachen, "gefälschten" Spiegelungs- oder Glanzeffekt.

Shader "Custom/LightmappedPrefabWithSpec"
{
  Properties
  {
    _MainTex("Base (RGB)", 2D) = "white" {}
    _Lightmap("Lightmap", 2D) = "white" {}
    _Specmap("Specmap", 2D) = "white" {}
    _SpecularAtt("Glossiness", Range(0.1, 2)) = 0.5
    _SpecularAmt("Specular", Range(0, 1)) = 0.5
  }

  SubShader
  {
    Tags{ "Queue" = "Geometry+1" }
    Pass
    {
      CGPROGRAM

      // Defining the name of the vertex shader
      #pragma vertex vert

      // Defining the name of the fragment shader
      #pragma fragment frag


      // Include some common helper functions,
      // specifically UnityObjectToClipPos and DecodeLightmap.
      #include "UnityCG.cginc"

      // Color Diffuse Map
      sampler2D _MainTex;
      // Tiling/Offset for _MainTex, used by TRANSFORM_TEX in vertex shader
      float4 _MainTex_ST;


      // Lightmap (created via Unity Lightbaking)
      sampler2D _Lightmap;
      // Tiling/Offset for _Lightmap, used by TRANSFORM_TEX in vertex shader
      float4 _Lightmap_ST;

      // Grayscale Map indicating which parts of the models have specular
      // Note: _Specmap_ST is not needed, as this map is using the same
      // UVs as for the _MainTex.
      sampler2D _Specmap;

      // This is the vertex shader input: position, UV0, UV1, normal
      // UV1 (= second UV channel) needed for the lightmap texture coordinates
      struct appdata
      {
        float4 vertex   : POSITION;
        float2 texcoord : TEXCOORD0;
        float2 texcoord1: TEXCOORD1;
        float3 normal: NORMAL;
      };

      // This is the data passed from the vertex to fragment shader
      struct v2f
      {
        float4 pos  : SV_POSITION; // position of the pixel
        float2 txuv : TEXCOORD0; // for accessing the diffuse color map
        float2 lmuv : TEXCOORD1; // for accessing the light map
        float3 normalDir : TEXCOORD2; // for fake specular
      };

      // This is the vertext shader, doing nothing special at all.
      // Most notably it is calculating the surface normal, because that
      // is needed for the fake specular lighting in the fragment shader.
      v2f vert(appdata v)
      {
        v2f o;
        o.pos = UnityObjectToClipPos(v.vertex);
        o.txuv = TRANSFORM_TEX(v.texcoord.xy, _MainTex); // using _MainTex_ST
        o.lmuv = TRANSFORM_TEX(v.texcoord1.xy, _Lightmap); // using _Lightmap_ST

        // Calculating the normal of the vertex for the fragment shader
        float4x4 modelMatrixInverse = unity_WorldToObject;
        o.normalDir = normalize(mul(float4(v.normal, 0.0), modelMatrixInverse).xyz);

        return o;
      }

      uniform float _SpecularAtt;
      uniform float _SpecularAmt;

      // Fragment Shader
      half4 frag(v2f i) : COLOR
      {
        // Reading color directly from the diffuse texture, using first UV channel
        half4 col = tex2D(_MainTex, i.txuv.xy);
        // Reading specular (on/off) value from spec map texture
        half4 specVal = tex2D(_Specmap, i.txuv.xy);
        // Reading lightmap value from the lightmap texture
        half4 lm = tex2D(_Lightmap, i.lmuv.xy);

        // Fake specular light angle calculation with a hard-coded light direction
        half3 th = normalize(half3(0, 1, -0.25));
        float spec = max(0, dot(i.normalDir, th));

        // Adjusting by overall specular amount and glossyness (material parameters)
        spec = _SpecularAmt * pow(spec, 40.0 * _SpecularAtt);
        // We're just using red value of the specular texture, like a grayscale map,
        // although technically spec could be colored.
        // Example: float3 specCol = specVal * spec;
        spec = spec * specVal.r;

        // Calculating the final color of the pixel by bringing it all together
        col.rgb = min(half4(1,1,1,1), col.rgb * DecodeLightmap(lm) + col.rgb * spec);
        return col;
      }
      ENDCG
    }
  }
  Fallback "Diffuse"
}

Diese Abbildung zeigt ein Material-Setup mit dem oben dargestellten Shader:

Unity light baked prefabs for mobile Material setup MetalPopGames

(Material-Setup mit einem Custom-Shader)

Setup und Static Batching

In unserem Fall haben wir vier verschiedene Szenen mit bereits eingerichteten Prefabs. Unser Spiel beinhaltet verschiedene Biome wie die Tropen, Eis, Wüste usw. und wir teilen unsere Szenen dementsprechend auf. Für Ihr Spiel kann die Anzahl der Szenen je nach den individuellen Erfordernissen abweichen.

Alle Prefabs, die in einer bestimmten Szene benutzt werden, teilen sich eine einzige Lightmap. Das bedeutet, dass zusätzlich zu den Prefabs, die sich nur ein Material teilen, nur eine einzige zusätzliche Textur notwendig ist.

Als Ergebnis konnten wir alle Modelle als statisch rendern und für beinahe die gesamte Spielwelt ein Batch-Rendering mit nur einem Draw-Call durchführen.

Unity light baked prefabs for mobile bake level MetalPopGames

(Bake-Level im Unity-Editor)

Die Light-Baking-Szenen, in der alle unsere Tiles/Gebäude eingerichtet sind, verfügen über zusätzliche Lichtquellen, um festgelegte Highlights zu setzen. Sie können so viele Lichtquellen in den Setup-Szenen platzieren, wie Sie möchten, da diese beim Baking-Prozess ohnehin eingebunden werden.

Der Bake-Prozess wird über einen angepassten UI-Dialog gesteuert, der sich um alle notwendigen Schritte kümmert. Er stellt sicher, dass:

  • The correct material is assigned to all meshes
  • Hides everything that doesn’t need to be baked during the process
  • Combines/bakes the meshes
  • Copies the UVs and creates prefabs
  • Names everything correctly and checks out the necessary files from the version control system

Unity light baked prefabs for mobile custom inspector MetalPopGames

(Angepasster Inspector für einen einfachen Workflow)

Richtig benannte Prefabs werden aus diesen Meshes heraus erstellt, sodass der Gamecode sie direkt laden und verwenden kann. Auch die Meta-Dateien werden bei diesem Vorgang geändert, sodass Referenzen auf die Meshes von Prefabs nicht verloren gehen.

Dieser Workflow ermöglicht uns, unsere Gebäude so weit zu optimieren, wie wir möchten, sie zu beleuchten, wie wir möchten und dann dem Script die restliche Arbeit zu überlassen.

Wenn wir dann zu unserer Hauptszene zurückgehen und das Spiel ausführen, funktioniert es einfach – manuelle Änderungen oder weitere Updates sind nicht erforderlich.

Fake-Light und Dynamisches

Ein offensichtlicher Nachteil einer Szene, in der 100 % der Beleuchtung im Voraus den Baking-Prozess durchlaufen haben, ist, dass es schwierig ist, dynamische Objekte oder Bewegung darin zu verwirklichen. Alles, was einen Schatten wirft, würde eine Berechnung von Licht und Schatten in Echtzeit erfordern, was wir natürlich vermeiden wollen. Aber ohne bewegliche Objekte würde die 3D-Umgebung statisch und tot wirken.

Wir waren natürlich bereit, einige Einschränkungen hinzunehmen, da unsere wichtigsten Ziele gute Visuals und schnelles Rendering waren. Um den Eindruck einer lebendigen, beweglichen Raumkolonie oder Stadt zu erwecken, war es nicht notwendig, dass allzu viele Objekte tatsächlich beweglich waren. Und die meisten der beweglichen Objekte mussten nicht unbedingt Schatten werfen, oder zumindest fiel das Fehlen von Schatten nicht auf.

In unserem Fall haben wir damit begonnen, alle Stadtbau-Blöcke in zwei separate Prefabs aufzuteilen: In einen statischen Anteil, der die Mehrzahl der Vertices, also alle komplizierten Teile unserer Meshes enthielt – und einen dynamischen mit so wenigen Vertices wie möglich.

Die dynamischen Anteile eines Prefabs sind animierte Einheiten, die auf den statischen platziert sind und keinen Light-Bake-Prozess durchlaufen. Wir haben einen sehr schnellen, einfachen Fake-Lighting-Shader verwendet, um die Illusion zu erzeugen, dass das Objekt dynamisch beleuchtet wird.

Shader "Custom/FakeLighting"
{
  Properties
  {
    _Color ("Color", Color) = (1,1,1,1)
    _Brightness ("Brightness", Range(0,1)) = 0.4
    _MainTex ("Albedo (RGB)", 2D) = "white" {}
  }

  SubShader
  {
    Tags { "RenderType" = "Opaque" }
    LOD 200
    Pass
    {

      CGPROGRAM

      // Define name of vertex shader
      #pragma vertex vert
      // Define name of fragment shader
      #pragma fragment frag

      // Include some common helper functions, such as UnityObjectToClipPos
      #include "UnityCG.cginc"

      float4 _Color;
      float _Brightness;

      // Color Diffuse Map
      sampler2D _MainTex;
      // Tiling/Offset for _MainTex, used by TRANSFORM_TEX in vertex shader
      float4 _MainTex_ST;

      // This is the vertex shader input: position, UV0, UV1, normal
      struct appdata
      {
        float4 vertex   : POSITION;
        float2 texcoord : TEXCOORD0;
        float3 normal: NORMAL;
      };

      // This is the data passed from the vertex to fragment shader
      struct v2f
      {
        float4 pos  : SV_POSITION;
        float2 txuv : TEXCOORD0;
        float3 normalDir : TEXCOORD2;
      };

      // This is the vertex shader
      v2f vert(appdata v)
      {
        v2f o;
        o.pos = UnityObjectToClipPos(v.vertex);
        o.txuv = TRANSFORM_TEX(v.texcoord.xy,_MainTex);

        // Calculating normal so it can be used for fake lighting
        // in the fragment shader
        float4x4 modelMatrixInverse = unity_WorldToObject;
        o.normalDir = normalize(mul(float4(v.normal, 0.0), modelMatrixInverse).xyz);

        return o;
      }

      // This is the fragment shader
      half4 frag(v2f i) : COLOR
      {
        // Reading color from diffuse texture
        half4 col = tex2D(_MainTex, i.txuv.xy);

        // Using hard-coded light direction for fake lighting
        half3 th = normalize(half3(0.25, 1, -0.25));
        // Using hard-coded light direction for fake specular
        // This matches the value inside the LightmappedPrefabWithSpec shader
        half3 sth = normalize(half3(0, 1, -0.25));

        // Fake lighting
        float lightVal = max(0, dot (i.normalDir, th));
        float lightScale = 0.75;
        lightVal = lightVal * lightScale;

        // Fake spec
        float spec = max(0, dot(i.normalDir, sth));
        float specScale = 0.65;
        float specAtt = 0.65;
        spec = specScale * pow (spec, 40.0 * specAtt);

        // Add in a general brightness (similar to ambient/gamma) and then
        // calculate the final color of the pixel
        col.rgb = min(half4(1,1,1,1), col.rgb * _Brightness +
                      col.rgb * lightVal * _Color + col.rgb * spec);
        return col;
      }

      ENDCG
    }
  }
  FallBack "Diffuse"
}

Die Objekte haben also entweder keinen Schatten oder wir haben einen "gefälschten" Schatten als Teil der dynamischen Anteile erstellt. Die meisten unserer Oberflächen sind flach, also war das in unserem Fall kein großes Problem.

Unity light baked prefabs for mobile Galactic Colonies MetalPopGames

(Gebäude mit zusätzlichen dynamischen Objekten)

Die dynamischen Einheiten werfen keinen Schatten, was aber kaum auffällt, wenn man nicht direkt danach sucht. Die Beleuchtung der dynamischen Prefabs ist ebenfalls vollständig "gefälscht" – es ist keinerlei Echtzeit-Licht vorhanden.

Die erste einfache "Abkürzung", die wir genommen haben, war das Hardcoding unserer Lichtquelle (Sonne) in den Fake-Lighting-Shader. So gibt es eine Variable weniger, die der Shader suchen und dynamisch aus der Welt füllen muss.

Es geht immer schneller, mit einem konstanten statt mit einem dynamischen Wert zu arbeiten. Wir haben so die grundlegende Beleuchtung, also helle und dunkle Seiten der Meshes, erhalten.

Spiegelungen/Glanz

Um alles etwas aufzupolieren, haben wir den Shadern eine Fake-Spiegelung/Glanz-Berechnung (Specular/Gloss) hinzugefügt, sowohl für die dynamischen als auch für die statischen Objekte. Spiegelnde Reflexionen bewirken ein metallisches Aussehen, helfen aber auch dabei, die Krümmung einer Oberfläche wiederzugeben.

Da spiegelnde Highlights eine Form von Reflexion sind, muss der Winkel zwischen der Kamera und der Lichtquelle relativ zueinander richtig berechnet werden. Wenn sich die Kamera bewegt oder dreht, ändert sich die Spiegelung. Jeder rechnende Shader würde Zugriff auf die Kameraposition und jede Lichtquelle in der Szene benötigen.

In unserem Spiel haben wir allerdings nur eine Lichtquelle, die für Spiegelungen benutzt werden sollte: die Sonne. In unserem Fall ist die Sonne unbeweglich und kann als gerichtetes Licht angesehen werden. Wir können den Shader wesentlich vereinfachen, indem wir nur eine einzige Lichtquelle benutzen und dafür einfach eine feste Position und einen festen Winkel annehmen.

Und was noch besser ist: Unsere Kamera in "Galactic Colonies" zeigt die Szene in Draufsicht, also von oben, wie die meisten Städtebau-Spiele. Die Kamera kann ein bisschen geneigt werden und zoomen, aber nicht um die Aufwärts-Achse rotiert werden.

Im Allgemeinen blickt die Kamera immer von oben auf die Umgebung. Um eine einfache, "gefälschte" Spiegelwirkung zu erzielen, haben wir so getan, als ob die Kamera vollständig fixiert und der Winkel zwischen Kamera und Licht unveränderlich wäre.

So konnten wir wieder einen konstanten Wert in den Shader hartkodieren und auf diese Weise einen einfachen Spiegelung-/Glanzeffekt erhalten.

Unity light baked prefabs for mobile Galactic Colonies MetalPopGames

(Fake-Spiegelung-/Glanzeffekt)

Einen festgelegten Winkel für die Spiegelung zu verwenden, ist technisch gesehen natürlich nicht korrekt – allerdings ist es praktisch unmöglich, den Unterschied zu erkennen, solange der Kamerawinkel sich nicht allzu sehr verändert. Für den Spieler sieht die Szene nach wie vor "richtig" aus, was schließlich auch der Sinn von Echtzeit-Beleuchtung ist.

Die Beleuchtung einer Umgebung in einem Echtzeit-Videospiel ist und war schon immer etwas, das visuell richtig erscheint und nicht unbedingt etwas, das physikalisch korrekt simuliert ist.

Da sich beinahe alle unsere Meshes ein Material teilen, wobei viele der Details von der Lightmap und den Vertices stammen, haben wir eine "Specular Texture Map" hinzugefügt, die dem Shader sagt, wann und wo der Specular-Wert angewendet werden und wie stark der Effekt ausfallen soll. Die Textur wird über den primären UV-Kanal angesteuert, es ist also kein zusätzlicher Koordinatensatz notwendig. Und da nicht viele Details enthalten sind, ist die Auflösung niedrig und es wird kaum Speicherplatz gebraucht.

Für unsere kleineren dynamischen Einheiten mit weniger Vertices konnten wir sogar Unitys automatisches dynamisches Batching nutzen, was das Rendern weiter beschleunigt hat.

Light-Baked-Prefabs auf Light-Baked-Prefabs

Alle Schatten, die den Baking-Prozess durchlaufen haben, können unter Umständen zu neuen Problemen führen, besonders, wenn man mit relativ modularen Gebäuden arbeitet. In einem Fall hatten wir ein Lagerhaus, das Spieler erbauen konnten, in dem auch die gelagerten Waren dargestellt wurden.

Das führt zu Problemen, da wir dann ein Light-Baked-Objekt haben, das auf einem anderen Light-Baked-Objekt platziert ist. Lightbake, wohin man schaut!

Wir sind dieses Problem mit einem weiteren einfachen Trick angegangen:

  • The surface where the additional object was to be added had to be flat and use a specific gray color matching the base building
  • In return for that trade-off, we could bake the objects on a smaller flat surface and place it on top of the area with just a slight offset
  • Lights, highlights, colored glow and shadows were all baked into the tile

Unity light baked prefabs for mobile Galactic Colonies MetalPopGames

(Die Baked-Objekte haben einen blauen Schimmer und werfen einen Schatten)

Einfaches und effizientes Lightbaking – wenn Sie mit den Einschränkungen leben können

Unsere Prefabs auf diese Weise mit Light-Baking zu erstellen ermöglicht uns, über riesige Karten mit Hunderten von Gebäuden zu verfügen und dabei die Drawcall-Anzahl sehr gering zu halten. Unser ganzes Spiel wird mehr oder weniger mit nur einem Material gerendert, und wir stehen an einem Punkt, an dem die UI mehr Drawcalls benötigt als unsere Spielwelt. Je weniger verschiedene Materialien Unity rendern muss, desto besser ist es für die Performance Ihrer Spiele.

So haben wir genügend Freiraum, um unserer Welt weitere Dinge wie Partikel, Wettereffekte und andere "Hingucker" hinzuzufügen.

Und sogar Spieler mit älteren Geräten können große Städte mit Hunderten von Gebäuden erbauen und dabei eine stabile Framerate von 60 fps beibehalten.

Unity light baked prefabs for mobile Galactic Colonies MetalPopGames

(Eine von einem Spieler erbaute Kolonie bei Verwendung von Light-Baked-Prefabs)

Nehmen Sie, was für Ihr Spiel passt

Unsere Lösung für das Rendern großer Planeten mit lebhaften Städten ist für unser Spiel maßgeschneidert. Wir haben gemacht, was für uns funktioniert. Wir haben unsere eingeschränkten Kamerawinkel und die einzelne Lichtquelle in der Szene benutzt, um die Shader-Komplexität stark zu vereinfachen. Mit einem festgelegten Winkel konnten wir außerdem Prozesse abkürzen, die die Beleuchtung dynamischer Objekte und die Berechnung von Spiegelungen betreffen. Wir haben die Anzahl der dynamischen Objekte reduziert und den Rest statisch als Batch behandelt.

Und natürlich macht unser visueller Low-Poly-Stil viele Dinge einfacher, da er uns ermöglicht, Materialien für fast alle Objekte zu teilen, was wiederum sowohl statisches als auch dynamisches Batching ermöglicht.

Höchstwahrscheinlich werden Sie nicht alle beschriebenen Lösungen genau so auf Ihr Spiel übertragen können – aber so sind sie auch gar nicht gedacht. Es handelt sich vielmehr um Ausgangspunkte, auf denen Sie Ihre eigenen Wege aufbauen können, um Ihr Spiel zu optimieren. Überlegen Sie sich, mit welchen Einschränkungen Ihr Spiel leben kann, und nutzen Sie sie, um es schneller zu machen.

Weitere Ressourcen

Wir wollen es wissen! Haben Ihnen diese Inhalte gefallen?

Ja, weiter so. Na ja. Könnte besser sein.
Alles klar

Wir verwenden Cookies, damit wir Ihnen die beste Nutzererfahrung auf unserer Website bieten können. Weitere Informationen erhalten Sie in unserer Cookie-Richtlinie.