El motor Cocos2d-x ha evolucionado significativamente en su manejo de gráficos. Una de las transiciones más importantes ocurrió entre las versiones 2.x y 3.x, donde se rediseñó por completo el flujo de trabajo para interactuar con OpenGL ES. Comprender esta arquitectura es vital para cualquier desarrollador que necesite implementar sombreadores (shaders) personalizados o efectos visuales avanzados.
Evolución del modelo de renderizado: de 2.x a 3.x
En la versión 2.x, el proceso de renderizado estaba estrechamente ligado a la estructura del grafo de escena (Rendering Tree). Cada nodo ejecutaba de forma recursiva su función visit(), y dentro de esta se invocaba draw(), que contenía llamadas directas a la API de OpenGL. Este enfoque dificutlaba la optimización automática, como el batching de texturas.
A partir de la versión 3.0, Cocos2d-x introdujo un sistema basado en comandos. En lugar de ejecutar OpenGL inmediatamente, la función draw() ahora se encarga de empaquetar la información necesaria en un objeto RenderCommand y enviarlo a una cola administrada por el Renderer.
// Ejemplo conceptual del método draw en la versión 3.2 para un Sprite
void MiSprite::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{
// Verificamos si el objeto está dentro del área visible (Culling)
_isVisibleInView = (flags & FLAGS_TRANSFORM_DIRTY)
? renderer->checkVisibility(transform, _contentSize)
: _isVisibleInView;
if(_isVisibleInView)
{
// Inicializamos el comando de cuadrilátero (Quad) con la textura y el estado del shader
_comandoQuad.init(_globalZOrder, _texture->getName(), getGLProgramState(), _blendFunc, &_quadData, 1, transform);
renderer->addCommand(&_comandoQuad);
}
}
El Ciclo de Vida del Renderizado
El flujo de ejecución comienza en el bucle principle de la aplicación. El camino que sigue el motor para dibujar un cuadro es el siguiente:
- Application::run(): Punto de entrada que invoca el
mainLoopdel Director. - Director::drawScene(): Es el corazón del proceso. Realiza el cálculo del tiempo delta, limpia los buffers de color y profundidad (
glClear), y recorre la escena activa. - Node::visit(): Recorre recursivamente los hijos del nodo, respetando el orden Z. Cada nodo añade sus comandos al
Renderer. - Renderer::render(): Una vez que todos los comandos están en la cola, el
Rendererlos ordena (por Z global e ID de material) y finalmente ejecuta las llamadas a OpenGL.
Categorías de RenderCommand
El motor clasifica las tareas de dibujo en varios tipos de comandos para optimizar el rendimiento:
- QUAD_COMMAND: Utilizado principalmente por sprites y partículas. Agrupa datos de vértices (quands) para ser dibujados en conjunto si comparten la misma textura.
- CUSTOM_COMMAND: Permite insertar lógica personalizada de OpenGL mediante una función callback. Es ideal para efectos únicos.
- BATCH_COMMAND: Diseñado para manejar atlas de texturas (SpriteBatchNode), reduciendo la cantidad de llamadas de dibujo (draw calls).
- GROUP_COMMAND: Permite agrupar múltiples comandos para ser procesados como una unidad, útil para nodos de renderizado especial como
RenderTexture. - MESH_COMMAND: Específico para el renderizado de modelos 3D y mallas complejas.
Implementación de Renderizado Personalizado
Existen dos formas principales de extender las capacidades de dibujo en Cocos2d-x 3.2:
1. Uso de CustomCommand en una Capa
Si necesitamos dibujar geometrías personalizadas sobre una capa, podemos sorbescribir el método draw o visit para inyectar nuestro código OpenGL.
void MiCapaEspecial::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{
_miComandoPersonalizado.init(_globalZOrder);
_miComandoPersonalizado.func = CC_CALLBACK_0(MiCapaEspecial::ejecutarGL, this);
renderer->addCommand(&_miComandoPersonalizado);
}
void MiCapaEspecial::ejecutarGL()
{
auto programa = getGLProgram();
programa->use();
programa->setUniformsForBuiltins();
// Lógica directa de OpenGL ES
glEnableVertexAttribArray(GLProgram::VERTEX_ATTRIB_POSITION);
// ... configuración de buffers y llamadas a glDrawArrays o glDrawElements
CHECK_GL_ERROR_DEBUG();
}
2. Aplicación de Shaders a Sprites Individuales
Para aplicar un efecto (como un filtro de color o distorsión) a un sprite específico sin afectar al resto, manipulamos su GLProgramState.
bool MiEscena::init()
{
if (!Layer::init()) return false;
auto spriteOriginal = Sprite::create("personaje.png");
this->addChild(spriteOriginal);
// Carga de archivos de shader (Vertex y Fragment)
auto pGlProgram = GLProgram::createWithFilenames("mi_efecto.vsh", "mi_efecto.fsh");
auto pEstado = GLProgramState::getOrCreateWithGLProgram(pGlProgram);
spriteOriginal->setGLProgramState(pEstado);
// Pasar parámetros al shader
pEstado->setUniformVec2("u_resolucion", Vec2(480, 320));
pEstado->setUniformFloat("u_tiempo", 0.5f);
return true;
}
En este segundo método, el motor sigue utilizando su flujo estándar de QUAD_COMMAND, pero al procesar el sprite, activará el programa de sombreado vinculado y sus parámetros uniformes antes de enviar los vértices a la GPU. Esta es la forma más eficiente y recomendada para la mayoría de los efectos visuales en juegos 2D.