Parte: 14 — Optimización, profiling y rendimiento · Fuente: Documentación de Godot 4 (GPU optimization / Using MultiMesh) ⏱️ Duración estimada: 80 min · Nivel: Avanzado
Atacar el lado GPU del rendimiento. Cuando el juego es GPU-bound, la CPU espera y el cuello está en cuántas veces le pedimos a la tarjeta que dibuje (draw calls), en cuántos píxeles se repintan de más (overdraw por transparencias), en el coste de rellenar la pantalla (fillrate) y en el tamaño de las texturas. Cada objeto dibujado por separado es una orden que la CPU envía a la GPU; miles de objetos pequeños generan miles de órdenes y saturan el envío antes que la propia potencia gráfica.
La palanca estrella de esta clase es el batching con MultiMeshInstance2D / MultiMeshInstance3D: dibujar miles de copias de la misma malla en una sola llamada. También veremos el overdraw que provocan las transparencias apiladas, el impacto del tamaño de textura, y el papel del MSAA. Medimos todo con el monitor RENDER_TOTAL_DRAW_CALLS_IN_FRAME y con el FPS, antes y después.
Al finalizar, el alumno podrá:
MultiMeshInstance y medir la caída de draw calls.| # | Tema | Por qué importa |
|---|---|---|
| 1 | Draw call | Cada una es una orden CPU→GPU con coste fijo. |
| 2 | Batching | Agrupa dibujos para reducir el número de órdenes. |
| 3 | MultiMesh | Dibuja miles de instancias en una sola llamada. |
| 4 | Overdraw | Píxeles repintados varias veces por transparencias. |
| 5 | Fillrate | Coste de escribir píxeles; escala con la resolución. |
| 6 | Tamaño de textura | Texturas grandes gastan memoria y ancho de banda. |
| 7 | Transparencias y orden | El alpha impide descartes y encarece el render. |
| 8 | MSAA | Suaviza bordes con un coste de fillrate. |
MultiMeshInstance2D/3D: nodo que renderiza un MultiMesh. Clave: sustituye a cientos de nodos individuales.Necesitas Godot 4.x. Mediremos con el monitor RENDER_TOTAL_DRAW_CALLS_IN_FRAME vía Performance.get_monitor() y con Monitors → Draw Calls en el Debugger, además del FPS. El MSAA está en Project Settings → Rendering → Anti Aliasing. Para el laboratorio 2D usaremos un QuadMesh como malla de las instancias.
Consulta la guía "Optimizing 3D performance" y la referencia de MultiMesh: https://docs.godotengine.org/en/stable/tutorials/performance/using_multimesh.html y https://docs.godotengine.org/en/stable/classes/class_multimesh.html. Para 2D y renderizado en general: https://docs.godotengine.org/en/stable/tutorials/performance/general_optimization.html.
Vamos a reducir draw calls sustituyendo cientos de nodos por un MultiMeshInstance2D y a medir la diferencia en el monitor de draw calls y en FPS.
Node2D raíz Escena con un Label y este script que instancia 2000 Sprite2D independientes. Cada uno genera su propia draw call:extends Node2D
@onready var _label: Label = $Label
func _ready() -> void:
var tex: Texture2D = preload("res://icon.svg")
for i in 2000:
var s := Sprite2D.new()
s.texture = tex
s.position = Vector2(randf() * 1000.0, randf() * 600.0)
s.scale = Vector2(0.1, 0.1)
add_child(s)
func _process(_delta: float) -> void:
var dc: int = Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME)
var fps: float = Performance.get_monitor(Performance.TIME_FPS)
_label.text = "Sprites sueltos | draw calls=%d | FPS=%.0f" % [dc, fps]
Ejecuta con F5 y anota draw calls y FPS. ANTES: verás un número de draw calls elevado (aunque Godot 2D hace algo de batching, la carga es notable) y el FPS afectado en hardware modesto.
Ahora la versión BARATA con MultiMeshInstance2D: una sola malla, 2000 instancias, una draw call. Crea un nuevo Node2D raíz y adjunta:
extends Node2D
@onready var _label: Label = $Label
func _ready() -> void:
var mm := MultiMesh.new()
mm.transform_format = MultiMesh.TRANSFORM_2D
var quad := QuadMesh.new()
quad.size = Vector2(32, 32)
mm.mesh = quad
mm.instance_count = 2000
for i in mm.instance_count:
var t := Transform2D()
t.origin = Vector2(randf() * 1000.0, randf() * 600.0)
mm.set_instance_transform_2d(i, t)
var mmi := MultiMeshInstance2D.new()
mmi.multimesh = mm
mmi.texture = preload("res://icon.svg")
add_child(mmi)
func _process(_delta: float) -> void:
var dc: int = Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME)
var fps: float = Performance.get_monitor(Performance.TIME_FPS)
_label.text = "MultiMesh | draw calls=%d | FPS=%.0f" % [dc, fps]
Ejecuta y compara. DESPUÉS: las 2000 instancias colapsan a esencialmente una draw call adicional, y el FPS sube. Abre Monitors → Draw Calls para ver el desplome en la gráfica al cambiar de escena.
Demuestra el overdraw. Añade sobre la escena varios ColorRect grandes semitransparentes (alpha ~0.3) solapados y observa cómo el FPS cae aunque las draw calls sean pocas: la GPU repinta los mismos píxeles muchas veces. Reduce el solape o el alpha y verás recuperarse el fillrate.
Ajusta el MSAA en Project Settings → Rendering → Anti Aliasing → MSAA 2D/3D: sube a 4x y observa en el monitor cómo el frame time crece por el mayor coste de fillrate; bájalo y compara calidad de bordes frente a coste.
Observable: la misma cantidad de objetos pasa de muchas draw calls a prácticamente una con MultiMesh, y el overdraw demuestra que "pocas draw calls" no basta si repintas la pantalla varias veces.
Toma una escena con al menos 1000 objetos visuales idénticos dibujados como nodos individuales. Reescríbela usando MultiMeshInstance2D o MultiMeshInstance3D. Entrega una comparativa medida del monitor de draw calls y del FPS antes y después, más una nota sobre si detectaste overdraw y cómo lo mitigaste.
Criterio de aceptación: la versión con MultiMesh reduce drásticamente el número de draw calls (de cientos/miles a un puñado) manteniendo el mismo resultado visual, y la comparativa lo demuestra con lecturas de RENDER_TOTAL_DRAW_CALLS_IN_FRAME.
| Síntoma | Causa y arreglo |
|---|---|
| Miles de nodos idénticos y FPS bajo | Cada nodo es una draw call. Agrúpalos en un MultiMeshInstance. |
| FPS bajo con pocas draw calls | Overdraw por transparencias apiladas. Reduce solape, alpha o número de capas. |
| MultiMesh no muestra nada | Falta asignar mesh, instance_count o las transformadas. Verifica que cada instancia tenga transform. |
| Caída de FPS al subir la resolución | Presión de fillrate. Baja MSAA o el tamaño de las texturas de pantalla completa. |
| Memoria de vídeo desbordada | Texturas enormes o sin mipmaps. Usa tamaños razonables (potencias de dos) y comprime. |
❓ ¿Godot 2D no hace batching solo? Hace cierto batching automático, pero objetos con distintos materiales o texturas rompen el lote. MultiMesh garantiza una sola llamada para instancias idénticas.
❓ ¿MultiMesh sirve para objetos que se mueven? Sí. Puedes actualizar cada transformada con set_instance_transform_2d/3d; el coste está en la actualización, no en dibujarlos.
❓ ¿El overdraw depende del número de draw calls? No directamente. Depende de cuántas veces se repinta cada píxel; las transparencias grandes lo disparan aunque haya una sola draw call.
❓ ¿MSAA siempre conviene? Mejora los bordes pero cuesta fillrate. En móviles o a resoluciones altas puede no compensar; evalúa 2x frente a 4x según tu presupuesto de frame.
Clase 243 - Optimización de CPU: lógica, scripts y llamadas