Vertex Data Management

Ein gutes Layout und eine gute Komprimierung für Vertex-Daten ist für die Leistung jeder grafischen Anwendung unerlässlich. Dabei spielt es keine Rolle, ob eine App aus 2D-Benutzeroberflächen besteht oder es sich um ein großes 3D-Open-World-Spiel handelt. Interne Tests mit dem Frame Profiler des Android GPU Inspectors an Dutzenden von Top-Android-Spielen zeigen, dass die Vertex-Datenverwaltung viel verbessert werden könnte. Wir haben festgestellt, dass Vertexdaten üblicherweise mit voller Genauigkeit, 32-Bit-Gleitkommawerten für alle Vertex-Attribute und einem Vertex-Zwischenspeicherlayout verwenden, das ein Array von Strukturen verwendet, die mit vollständig verschränkten Attributen formatiert sind.

In diesem Artikel wird erläutert, wie Sie die Grafikleistung Ihrer Android-App mithilfe der folgenden Techniken optimieren können:

  • Vertex-Komprimierung
  • Vertex Stream Splitting

Durch die Implementierung dieser Techniken kann die Nutzung der Vertex-Arbeitsspeicherbandbreite um bis zu 50 % verbessert, Speicherbus-Konflikte mit der CPU reduziert, Störungen im Systemspeicher reduziert und die Akkulaufzeit verbessert werden. All das ist sowohl für Entwickler als auch für Endnutzer ein Gewinn!

Alle dargestellten Daten stammen aus einer beispielhaften statischen Szene mit ca. 19.000.000 Eckpunkten, die auf einem Pixel 4 laufen:

Beispielszene mit 6 Ringen und 19 m Eckpunkten

Abbildung 1:Beispielszene mit 6 Ringen und 19 m Eckpunkten

Vertex-Komprimierung

Vertex Compression ist ein Oberbegriff für verlustbehaftete Komprimierungstechniken, die ein effizientes Packen verwenden, um die Größe von Vertex-Daten sowohl während der Laufzeit als auch beim Speichern zu reduzieren. Die Reduzierung der Größe von Eckpunkten bietet mehrere Vorteile, darunter eine Reduzierung der Arbeitsspeicherbandbreite in der GPU (durch den Eintausch von Rechenleistung gegen die Bandbreite), eine verbesserte Cache-Auslastung und möglicherweise das Risiko von Spilling-Registern.

Gängige Ansätze für Vertex Compression:

  • Reduzierung der numerischen Genauigkeit von Vertex-Datenattributen (z. B. 32-Bit-Gleitkommazahl bis 16-Bit-Gleitkommazahl)
  • Attribute in unterschiedlichen Formaten darstellen

Wenn ein Eckpunkt beispielsweise vollständige 32-Bit-Gleitkommazahlen für Position (vec3), normale (vec3) und Texturkoordinaten (vec2) verwendet und all diese durch 16-Bit-Gleitkommazahlen ersetzt, verringert sich die Größe des Scheitelpunkts um 50% (16 Byte bei einem durchschnittlichen 32-Byte-Scheitelpunkt).

Vertex-Positionen

Vertex-Positionsdaten können von 32-Bit-Gleitkommawerten mit voller Genauigkeit zu 16-Bit-Gleitkommawerten mit halber Genauigkeit in den meisten Mesh-Netzwerken komprimiert werden. Halbe Gleitkommazahlen werden in der Hardware auf fast allen Mobilgeräten unterstützt. Eine Konvertierungsfunktion, die von Gleitkommazahl32 zu Gleitkommazahl16 wechselt, sieht wie folgt aus (aus diesem Leitfaden angepasst):

uint16_t f32_to_f16(float f) {
  uint32_t x = (uint32_t)f;
  uint32_t sign = (unsigned short)(x >> 31);
  uint32_t mantissa;
  uint32_t exp;
  uint16_t hf;

  mantissa = x & ((1 << 23) - 1);
  exp = x & (0xFF << 23);
  if (exp >= 0x47800000) {
    // check if the original number is a NaN
    if (mantissa && (exp == (0xFF << 23))) {
      // single precision NaN
      mantissa = (1 << 23) - 1;
    } else {
      // half-float will be Inf
      mantissa = 0;
    }
    hf = (((uint16_t)sign) << 15) | (uint16_t)((0x1F << 10)) |
         (uint16_t)(mantissa >> 13);
  }
  // check if exponent is <= -15
  else if (exp <= 0x38000000) {
    hf = 0;  // too small to be represented
  } else {
    hf = (((uint16_t)sign) << 15) | (uint16_t)((exp - 0x38000000) >> 13) |
         (uint16_t)(mantissa >> 13);
  }

  return hf;
}

Dabei gibt es eine Einschränkung: Die Präzision wird beeinträchtigt, wenn der Scheitelpunkt weiter vom Ursprung entfernt wird. Daher ist sie für Mesh-Netzwerke, die räumlich sehr groß sind (Eckpunkte mit Elementen, die über 1.024 Zeichen hinausgehen) weniger geeignet. Sie können dies umgehen, indem Sie ein Mesh-Netzwerk in kleinere Blöcke aufteilen, jeden Block auf den Modellursprung zentrieren und die Anzahl so skalieren, dass alle Eckpunkte für jeden Block in den Bereich [-1, 1] fallen, der die höchste Genauigkeit für Gleitkommawerte bietet. Der Pseudocode für die Komprimierung sieht so aus:

for each position p in Mesh:
   p -= center_of_bounding_box // Moves Mesh back to the center of model space
   p /= half_size_bounding_box // Fits the mesh into a [-1, 1] cube
   vec3<float16> result = vec3(f32_to_f16(p.x), f32_to_f16(p.y), f32_to_f16(p.z));

Sie verankern den Skalierungsfaktor und die Übersetzung in der Modellmatrix, um die Scheitelpunktdaten beim Rendern zu dekomprimieren. Denken Sie daran, dass Sie nicht dieselbe Modellmatrix zum Transformieren von Normalen verwenden möchten, da auf diese nicht dieselbe Komprimierung angewendet wurde. Sie benötigen eine Matrix ohne diese Dekomprimierungstransformationen für Normalen oder Sie können die Basismodellmatrix (die Sie für Normalen verwenden können) und dann die zusätzlichen Dekomprimierungstransformationen auf die Modellmatrix innerhalb des Shaders anwenden. Ein Beispiel:

vec3 in in_pos;

void main() {
   ...
   // bounding box data packed into uniform buffer
   vec3 decompress_pos = in_pos * half_size_bounding_box + center_of_bounding_box;
   gl_Position = proj * view * model * decompress_pos;
}

Ein anderer Ansatz umfasst die Verwendung von vorzeichenbehafteten normalisierten Ganzzahlen (SNORM). SNORM-Datentypen verwenden Ganzzahlen anstelle von Gleitkommazahlen, um Werte zwischen [-1, 1] darzustellen. Durch die Verwendung eines 16-Bit-SNORM-SNORMs für Positionen erhalten Sie die gleichen Speichereinsparungen wie bei einer FLOAT16 ohne die Nachteile einer ungleichmäßigen Verteilung. Eine empfohlene Implementierung für SNORM sieht so aus:

const int BITS = 16

for each position p in Mesh:
   p -= center_of_bounding_box // Moves Mesh back to the center of model space
   p /= half_size_bounding_box // Fits the mesh into a [-1, 1] cube
   // float to integer value conversion
   p = clamp(p * (2^(BITS - 1) - 1), -2^(BITS - 1), 2^(BITS - 1) - 1) 
Formatieren Größe
Vorher VEC4<float32> 16 Byte
Nachher VEC3<float16/SNORM16> 6 Byte

Scheitelpunktnormale und Tangente

Vertex Normals werden für die Beleuchtung benötigt und der Tangenteraum wird für kompliziertere Verfahren wie die normale Zuordnung benötigt.

Tangensraum

Der Tangensraum ist ein Koordinatensystem, in dem jeder Scheitelpunkt aus dem normalen Vektor, dem Tangens und dem Bitangensvektor besteht. Da diese drei Vektoren in der Regel orthogonal zueinander sind, müssen wir nur zwei von ihnen speichern und können den dritten Vektor berechnen, indem wir ein Kreuzprodukt der anderen beiden im Vertex-Shader berechnen.

Diese Vektoren können in der Regel mit 16-Bit-Gleitkommazahlen dargestellt werden, ohne dass es beim Wahrnehmungsverlust in der visuellen Qualität zu einem Verlust der visuellen Qualität kommt. Das ist also ein guter Anfang!

Eine weitere Komprimierung ist mit einer Methode namens QTangents möglich, bei der der gesamte Tangensraum in einem einzigen Quaternion gespeichert wird. Da Quaternionen zur Darstellung von Rotationen verwendet werden können, können wir uns die Tangentenraumvektoren als Spaltenvektoren einer 3x3-Matrix vorstellen, die eine Rotation darstellt (in diesem Fall vom Modellraum in den Tangensraum), sodass wir zwischen den beiden umrechnen können. Ein Quaternion kann datenbezogen als vec4-Wert behandelt werden und eine Konvertierung von Tangentenraumvektoren in eine QTangente basierend auf dem oben verlinkten Papier und an die Implementierung hier angepasst sieht so aus:

const int BITS = 16

quaternion tangent_space_to_quat(vec3 normal, vec3 tangent, vec3 bitangent) {
   mat3 tbn = {normal, tangent, bitangent};
   quaternion qTangent(tbn);
   qTangent.normalize();

   //Make sure QTangent is always positive
   if (qTangent.w < 0)
       qTangent = -qTangent;

   const float bias = 1.0 / (2^(BITS - 1) - 1);

   //Because '-0' sign information is lost when using integers,
   //we need to apply a "bias"; while making sure the Quaternion
   //stays normalized.
   // ** Also our shaders assume qTangent.w is never 0. **
   if (qTangent.w < bias) {
       Real normFactor = Math::Sqrt( 1 - bias * bias );
       qTangent.w = bias;
       qTangent.x *= normFactor;
       qTangent.y *= normFactor;
       qTangent.z *= normFactor;
   }

   //If it's reflected, then make sure .w is negative.
   vec3 naturalBinormal = cross_product(tangent, normal);
   if (dot_product(naturalBinormal, binormal) <= 0)
       qTangent = -qTangent;
   return qTangent;
}

Das Quaternion wird normalisiert und kann mit SNORM-Sensoren komprimiert werden. 16-Bit-SNORMs sorgen für hohe Präzision und sparen Speicherplatz. 8-Bit-SNORM-Systeme können noch mehr Einsparungen erzielen, können aber auf stark spekulären Materialien zu Artefakten führen. Sie können beide ausprobieren, um zu sehen, was für Ihre Assets am besten funktioniert. Die Codierung der Quaternion sieht so aus:

for each vertex v in mesh:
   quaternion res = tangent_space_to_quat(v.normal, v.tangent, v.bitangent);
   // Once we have the quaternion we can compress it
   res = clamp(res * (2^(BITS - 1) - 1), -2^(BITS - 1), 2^(BITS - 1) - 1);

So decodieren Sie das Quaternion im Vertex-Shader (von hier angepasst):

vec3 xAxis( vec4 qQuat )
{
  float fTy  = 2.0 * qQuat.y;
  float fTz  = 2.0 * qQuat.z;
  float fTwy = fTy * qQuat.w;
  float fTwz = fTz * qQuat.w;
  float fTxy = fTy * qQuat.x;
  float fTxz = fTz * qQuat.x;
  float fTyy = fTy * qQuat.y;
  float fTzz = fTz * qQuat.z;

  return vec3( 1.0-(fTyy+fTzz), fTxy+fTwz, fTxz-fTwy );
}

vec3 yAxis( vec4 qQuat )
{
  float fTx  = 2.0 * qQuat.x;
  float fTy  = 2.0 * qQuat.y;
  float fTz  = 2.0 * qQuat.z;
  float fTwx = fTx * qQuat.w;
  float fTwz = fTz * qQuat.w;
  float fTxx = fTx * qQuat.x;
  float fTxy = fTy * qQuat.x;
  float fTyz = fTz * qQuat.y;
  float fTzz = fTz * qQuat.z;

  return vec3( fTxy-fTwz, 1.0-(fTxx+fTzz), fTyz+fTwx );
}

void main() {
  vec4 qtangent = normalize(in_qtangent); //Needed because 16-bit quantization
  vec3 normal = xAxis(qtangent);
  vec3 tangent = yAxis(qtangent);
  float biNormalReflection = sign(in_qtangent.w); //ensured qtangent.w != 0
  vec3 binormal = cross(normal, tangent) * biNormalReflection;
  ...
}
Formatieren Größe
Vorher vec3<float32> + vec3<float32> + vec3<float32> 36 Byte
Nachher VEC4<SNORM16> 8 Byte

Nur normale Werte

Wenn Sie nur normale Vektoren speichern müssen, gibt es einen anderen Ansatz, der zu weiteren Einsparungen führen kann. Verwenden Sie zur Komprimierung des normalen Vektors die Oktaeder-Zuordnung von Einheitsvektoren anstelle von kartesischen Koordinaten. Bei der oktaedralen Kartierung wird eine Einheitssphäre auf einen Oktaeder projiziert und anschließend auf eine 2D-Ebene projiziert. Das Ergebnis ist, dass Sie jeden normalen Vektor mit nur zwei Zahlen darstellen können. Diese beiden Zahlen können als Texturkoordinaten angesehen werden, mit denen wir die 2D-Ebene „abbilden“, auf die wir die Kugel projiziert haben, und so den ursprünglichen Vektor wiedererlangen. Diese beiden Nummern können dann in einem SNORM8-System gespeichert werden.

Projizieren einer Einheitssphäre auf einen Oktaeder und Projizieren des Oktaeders auf eine 2D-Ebene

Abbildung 2:Visualisierung der Octaedral-Zuordnung (Quelle)

const int BITS = 8

// Assumes the vector is unit length
// sign() function should return positive for 0
for each normal n in mesh:
  float invL1Norm = 1.0 / (abs(n.x) + abs(n.y) + abs(n.z));
  vec2 res;
  if (n.z < 0.0) {
    res.x = (1.0 - abs(n.y * invL1Norm)) * sign(n.x);
    res.y = (1.0 - abs(n.x * invL1Norm)) * sign(n.y);
  } else {
    res.x = n.x * invL1Norm;
    res.y = n.y * invL1Norm;
  }
  res = clamp(res * (2^(BITS - 1) - 1), -2^(BITS - 1), 2^(BITS - 1) - 1)

Die Dekomprimierung im Vertex-Shader (um wieder in kartesische Koordinaten umzuwandeln) ist kostengünstig. Bei den meisten modernen Mobilgeräten haben wir bei der Implementierung dieses Verfahrens keine nennenswerten Leistungseinbußen festgestellt. Die Dekomprimierung im Vertex-Shader:

//Additional Optimization: twitter.com/Stubbesaurus/status/937994790553227264
vec3 oct_to_vec(vec2 e):
  vec3 v = vec3(e.xy, 1.0 - abs(e.x) - abs(e.y));
  float t = max(-v.z, 0.0);
  v.xy += t * -sign(v.xy);
  return v;

Dieser Ansatz kann auch verwendet werden, um den gesamten Tangensraum zu speichern, wobei Sie damit den normalen und den Tangensvektor mit vec2<SNORM8> speichern müssen. Sie müssen jedoch eine Möglichkeit finden, die Richtung der Bitange zu speichern (erforderlich für das gängige Szenario, in dem Sie gespiegelte UV-Koordinaten in einem Modell haben). Eine Möglichkeit, dies zu implementieren, besteht darin, eine Komponente Ihrer Tangentenvektor-Codierung so abzubilden, dass sie immer positiv ist, und dann das Vorzeichen umdrehen, wenn Sie die Bitangenrichtung umdrehen und dies im Vertex-Shader überprüfen müssen:

const int BITS = 8
const float bias = 1.0 / (2^(BITS - 1) - 1)

// Compressing
for each normal n in mesh:
  //encode to octahedron, result in range [-1, 1]
  vec2 res = vec_to_oct(n);

  // map y to always be positive
  res.y = res.y * 0.5 + 0.5;

  // add a bias so that y is never 0 (sign in the vertex shader)
  if (res.y < bias)
    res.y = bias;

  // Apply the sign of the binormal to y, which was computed elsewhere
  if (binormal_sign < 0)
    res.y *= -1;

  res = clamp(res * (2^(BITS - 1) - 1), -2^(BITS - 1), 2^(BITS - 1) - 1)
// Vertex shader decompression
vec2 encode = vec2(tangent_encoded.x, abs(tangent_encoded.y) * 2.0 - 1.0));
vec3 tangent_real = oct_to_vec3(encode);
float binormal_sign = sign(tangent_encode.y);
Formatieren Größe
Vorher VEC3<float32> 12 Byte
Nachher vec2<SNORM8> 2 Byte

Vertex UV-Koordinaten

UV-Koordinaten, die unter anderem für die Texturzuordnung verwendet werden, werden in der Regel als 32-Bit-Gleitkommazahlen gespeichert. Die Komprimierung mit 16-Bit-Gleitkommazahlen führt zu Genauigkeitsproblemen bei Texturen, die größer als 1024 × 1024 sind.Eine Gleitkommagenauigkeit zwischen [0,5, 1,0] bedeutet, dass die Werte um mehr als 1 Pixel erhöht werden.

Der bessere Ansatz ist die Verwendung unvorzeichenbehafteter normalisierter Ganzzahlen (UNORM), insbesondere UNORM16. Dadurch wird eine gleichmäßige Verteilung über den gesamten Texturkoordinatenbereich ermöglicht und Texturen bis zu 65.536 × 65.536 Pixel unterstützt. Dabei wird davon ausgegangen, dass die Texturkoordinaten im Bereich [0,0, 1,0] pro Element liegen, was je nach Mesh-Netzwerk möglicherweise nicht der Fall ist (z.B.können Wände Texturkoordinaten verwenden, die über 1,0 hinausgehen). Dies sollten Sie beim Betrachten dieses Verfahrens beachten. Die Konvertierungsfunktion würde so aussehen:

const int BITS = 16

for each vertex_uv V in mesh:
  V *= clamp(2^BITS - 1, 0, 2^BITS - 1);  // float to integer value conversion
Formatieren Größe
Vorher vec2<float32> 8 Byte
Nachher vec2<UNORM16> 4 Byte

Vertex-Komprimierungsergebnisse

Diese Vertex-Komprimierungstechniken führten zu einer Reduzierung des Vertex-Arbeitsspeichers um 66% von 48 Byte auf 16 Byte. Dies kündigte sich so an:

  • Lesebandbreite in Vertex Memory:
    • Binning: 27 GB/s bis 9 GB/s
    • Rendering: 4,5 B/s bis 1,5 GB/s
  • Vertex Fetch-Stände:
    • Binning: 50% bis 0%
    • Rendering: 90% bis 90%
  • Durchschnittliche Byte/Scheitelpunkt:
    • Binning: 48 B bis 16 B
    • Rendering: 52 bis 18 B

Android GPU Inspector-Ansicht von unkomprimierten Eckpunkten

Abbildung 3:Ansicht des Android GPU Inspectors mit nicht komprimierten Eckpunkten

Android GPU Inspector-Ansicht komprimierter Eckpunkte

Abbildung 4:Ansicht des Android GPU Inspectors mit komprimierten Eckpunkten

Vertex Stream Splitting

Mit Vertex Stream Splitting wird die Organisation der Daten im Vertex-Zwischenspeicher optimiert. Dies ist eine Optimierung der Cache-Leistung, die einen Unterschied zu kachelbasierten GPUs ausmacht, die in der Regel auf Android-Geräten zu finden sind, insbesondere während des Binning-Schritts des Renderingprozesses.

Kachelbasierte GPUs erstellen einen Shader, der die normalisierten Gerätekoordinaten auf der Grundlage des bereitgestellten Vertex-Shaders für das Binning berechnet. Sie wird zuerst an jedem Scheitelpunkt in der Szene ausgeführt, unabhängig davon, ob sie sichtbar ist oder nicht. Daher ist es ein großes Plus, die Vertex-Positionsdaten im Speicher zusammenhängend zu lassen. In anderen Fällen ist dieses Vertex-Stream-Layout auch für Schattendurchgänge von Vorteil, da Sie in der Regel nur Positionsdaten für Schattenberechnungen sowie Tiefenvorläufe benötigen. Ein Verfahren, das normalerweise beim Konsolen-/Desktop-Rendering verwendet wird. Dieses Vertex-Stream-Layout kann für mehrere Klassen des Rendering-Moduls von Vorteil sein.

Bei der Stream-Aufteilung wird der Zwischenspeicher für Scheitelpunkte mit einem zusammenhängenden Abschnitt der Eckpunktpositionsdaten und einem anderen Abschnitt mit verschränkten Vertex-Attributen eingerichtet. Die meisten Anwendungen richten ihre Puffer normalerweise vollständig ein, wodurch alle Attribute vollständig verschachtelt sind. Hier sehen Sie den Unterschied:

Before:
|Position1/Normal1/Tangent1/UV1/Position2/Normal2/Tangent2/UV2......|

After:
|Position1/Position2...|Normal1/Tangent1/UV1/Normal2/Tangent2/UV2...|

Wenn wir uns ansehen, wie die GPU Vertexdaten abruft, können wir die Vorteile der Streamaufteilung verstehen. Für das Argument angenommen:

  • 32 Byte Cache-Zeilen (eine ziemlich häufige Größe)
  • Vertex-Format, bestehend aus:
    • Position, vec3<float32> = 12 Byte
    • Normales vec3<float32> = 12 Byte
    • UV-Koordinaten vec2<float32> = 8 Byte
    • Gesamtgröße = 32 Byte

Wenn die GPU Daten zum Binning aus dem Speicher abruft, zieht sie eine 32-Byte-Cache-Zeile zur Verarbeitung. Ohne die Vertex-Stream-Aufteilung werden nur die ersten 12 Byte dieser Cache-Zeile für das Binning verwendet. Die anderen 20 Byte werden beim Abrufen des nächsten Eckpunkts verworfen. Bei der Vertex-Stream-Aufteilung sind die Scheitelpunktpositionen im Speicher zusammenhängend. Wenn dieser 32-Byte-Chunk in den Cache geladen wird, enthält er tatsächlich 2 ganze Vertex-Positionen, an denen gearbeitet werden kann, bevor er zurück zum Hauptspeicher zurückkehren muss, um mehr abzurufen, eine 2-fache Verbesserung!

Wenn wir nun die Vertex-Stream-Aufteilung mit der Vertex-Komprimierung kombinieren, reduzieren wir die Größe einer einzelnen Vertex-Position auf 6 Byte. Eine einzelne 32-Byte-Cache-Zeile, die aus dem Systemspeicher abgerufen wird, verfügt also über 5 ganze Vertex-Positionen, an denen sie arbeiten können. Eine Verbesserung um das 5-Fache!

Ergebnisse für die Aufteilung in Vertex Stream

  • Lesebandbreite in Vertex Memory:
    • Binning: 27 GB/s bis 6,5 GB/s
    • Rendering: 4,5 GB/s bis 4,5 GB/s
  • Vertex Fetch-Stände:
    • Binning: 40% bis 0%
    • Rendering: 90% bis 90%
  • Durchschnittliche Byte/Scheitelpunkt:
    • Binning: 48 bis 12 Mrd.
    • Rendering: 52 bis 52 B

Android GPU Inspector von nicht aufgeteilten Vertex-Streams

Abbildung 5:Ansicht des Android GPU Inspectors von nicht aufgeteilten Vertex-Streams

Android GPU Inspector-Ansicht von aufgeteilten Vertex-Streams

Abbildung 6:Ansicht des Android GPU Inspectors von aufgeteilten Vertex-Streams

Zusammengesetzte Ergebnisse

  • Lesebandbreite in Vertex Memory:
    • Binning: 25 GB/s bis 4,5 GB/s
    • Rendering: 4,5 GB/s bis 1,7 GB/s
  • Vertex Fetch-Stände:
    • Binning: 41% bis 0%
    • Rendering: 90% bis 90%
  • Durchschnittliche Byte/Scheitelpunkt:
    • Binning: 48 B bis 8 B
    • Rendering: 52 bis 19 B

Android GPU Inspector von nicht aufgeteilten Vertex-Streams

Abbildung 7:Ansicht des Android GPU Inspectors von nicht aufgeteilten, unkomprimierten Vertex-Streams

Android GPU Inspector von nicht aufgeteilten Vertex-Streams

Abbildung 8:Ansicht des Android GPU Inspectors von aufgeteilten, komprimierten Vertex-Streams

Weitere Überlegungen

Indexzwischenspeicherdaten von 16 vs. 32 Bit

  • Split-Mesh-Netzwerke/Chunk-Mesh-Netzwerke sollten immer so aufgeteilt werden, dass sie in einen 16-Bit-Indexzwischenspeicher passen (max. 65.536 eindeutige Eckpunkte). Dies hilft beim indexierten Rendering auf Mobilgeräten, da es kostengünstiger ist, Vertex-Daten abzurufen, und es verbraucht weniger Strom.

Nicht unterstützte Vertex-Pufferattributformate

  • SSCALED-Vertex-Formate werden auf Mobilgeräten nicht umfassend unterstützt und können bei ihrer Verwendung kostspielige Leistungseinbußen bei Treibern haben, die sie nachahmen, wenn sie keine Hardwareunterstützung haben. Mach immer einen Schnarchen und bezahle nur die unwesentlichen ALU-Kosten zum Entspannen.