Saltar al contingut principal
Tornar enrere

Godot #4: Game Juice amb Shaders

#godot #shaders #gpu #game-juice

Introducció al món dels Shaders. Arquitectura CPU vs GPU, GLSL bàsic i creació d'un efecte de Hit Flash pas a pas.

El Feedback Visual

En el capítol anterior vam aconseguir que els enemics morissin i caiguessin. Funciona, però no hi ha feedback quan reben dany. Perquè un joc se senti professional, necessitem Feedback Visual immediat. Això és el que a la indústria s’anomena “Game Juice”.

L’eina més potent per aconseguir aquest efecte visual són els Shaders.

1. Què és un Shader?

Un Shader és un petit programa que no s’executa a la CPU, sinó a la GPU (la targeta gràfica).

  • CPU: Executa el teu codi GDScript. Processa la lògica del joc (moviment, col·lisions, puntuació).
  • GPU: Dibuixa els píxels a la pantalla. És un processador especialitzat en operacions gràfiques massives.

Quan escrius un Shader, estàs donant instruccions directes a la GPU sobre com pintar cada píxel.

2. Arquitectura: Nuclis i Paral·lelisme

Per què usar la GPU en lloc de la CPU per a efectes visuals?

CPU: Pocs Nuclis, Molt Potents

Una CPU moderna com l’Intel Core i7-14700K té:

  • 20 nuclis físics (8 d’alt rendiment + 12 d’eficiència).
  • 28 fils (threads) gràcies a Hyper-Threading.

Cada nucli pot executar instruccions complexes i diferents entre si. Però fins i tot amb 28 fils, si necessites processar 10.000 píxels, la CPU hauria de repartir-los entre aquests fils i executar-los en lots seqüencials.

GPU: Milers de Nuclis, Molt Simples

Una GPU moderna com la NVIDIA RTX 4080 té:

  • 9.728 nuclis CUDA.
  • Cada nucli és molt més simple que un de CPU.
  • Però poden executar la mateixa instrucció en milers de dades diferents alhora.

Aquesta arquitectura s’anomena SIMD (Single Instruction, Multiple Data): una sola instrucció aplicada a moltes dades en paral·lel.

L’Exemple Concret

Si el teu sprite de l’enemic mesura 100x100 píxels, són 10.000 píxels en total.

ProcessadorFils Paral·lelsOperacions per Lot
CPU (i7-14700K)~2810.000 ÷ 28 = ~357 lots
GPU (RTX 4080)~9.728+10.000 ÷ 9.728 = ~1 lot

La GPU pot processar tots els píxels pràcticament d’una sola passada. Per això els Shaders són tan ràpids per a efectes visuals.

3. GLSL: El Llenguatge de la GPU

Els Shaders a Godot s’escriuen en un llenguatge anomenat GLSL (OpenGL Shading Language), similar a C. No t’espantis. Anem a explicar cada concepte abans d’usar-lo.

shader_type

Tot shader de Godot comença amb una declaració de tipus:

glsl
shader_type canvas_item;

Què significa?

  • shader_type canvas_item: Aquest shader és per a elements 2D (sprites, nodes de canvas).
  • Existeixen altres tipus com spatial (3D) o particles, però en aquest curs usarem canvas_item.

fragment()

És la funció principal del shader. S’executa un cop per cada píxel del sprite.

glsl
void fragment() {
  // Aquest codi s'executa per CADA píxel
  // La teva feina aquí: calcular el color final del píxel
}

Important: Si el teu sprite té 10.000 píxels, aquesta funció s’executa 10.000 vegades en paral·lel.

COLOR

És la variable de sortida obligatòria. És on guardes el color final que tindrà el píxel.

glsl
void fragment() {
  COLOR = vec4(1.0, 0.0, 0.0, 1.0); // Pinta TOT el sprite de vermell sòlid
}

vec4

Un color en GLSL es representa amb 4 números del 0.0 a l’1.0:

ComponentSignificatRang
R (Red)Quantitat de vermell0.0 - 1.0
G (Green)Quantitat de verd0.0 - 1.0
B (Blue)Quantitat de blau0.0 - 1.0
A (Alpha)Transparència0.0 (invisible) - 1.0 (sòlid)

Exemples:

  • vec4(1.0, 1.0, 1.0, 1.0) = Blanc opac
  • vec4(0.0, 0.0, 0.0, 1.0) = Negre opac
  • vec4(1.0, 0.0, 0.0, 0.5) = Vermell semitransparent

TEXTURE

És una variable especial que referencia la imatge del sprite (la textura que vas carregar a Sprite2D). No pots usar-la directament com a color. Necessites “llegir-la” amb la funció texture().

UV

Són les coordenades normalitzades de cada píxel dins del sprite.

  • Van de (0.0, 0.0) (cantó superior esquerre) a (1.0, 1.0) (cantó inferior dret).
  • Cada fil d’execució (cada píxel) té el seu propi valor d’UV.
  • El fil del píxel central tindria UV = (0.5, 0.5).

Nota tècnica: UV és als shaders el que self.position és als scripts. Cada fil sap la seva pròpia coordenada.

texture()

És una funció que llegeix el color d’un píxel d’una imatge donada una coordenada.

glsl
vec4 color = texture(TEXTURE, UV);

Desglossament:

  • TEXTURE: La imatge del sprite.
  • UV: La coordenada d’aquest píxel concret.
  • Resultat: Retorna el vec4 (color RGBA) del píxel en aquella posició de la imatge.

Amb això, cada un dels 10.000 fils llegeix el seu propi color de la imatge original, reconstruint l’sprite complet.


4. La Missió: Hit Flash

Volem un efecte on, en rebre dany, l’enemic parpellegi en blanc pur durant una fracció de segon.

Preparant el Material

  1. Selecciona el Sprite2D de l’enemic.
  2. A l’Inspector, busca CanvasItem > Material.
  3. Fes clic al desplegable i selecciona New ShaderMaterial.
  4. Fes clic en el nou material i a Shader, selecciona New Shader.
  5. Crea la carpeta shaders al teu projecte i guarda l’arxiu com shaders/hit_flash.gdshader.

Fase 1: Estructura Buida

Comencem amb l’estructura mínima d’un shader 2D:

glsl
shader_type canvas_item;

void fragment() {
  // Per ara, no fem res
  // L'sprite es veurà normal
}

Si proves ara, l’sprite es veurà invisible perquè no hem assignat res a COLOR.

Fase 2: Recuperar el Color Original

Perquè l’sprite es vegi normal, hem de llegir la seva textura i assignar-la a COLOR:

glsl
shader_type canvas_item;

void fragment() {
  // Llegim el color original de l'sprite en aquesta coordenada (UV)
  vec4 color_original = texture(TEXTURE, UV);
  
  // Assignem aquest color com a sortida
  COLOR = color_original;
}

Ara l’sprite es veu exactament igual que abans del shader. Hem “reconstruït” la imatge original.

Fase 3: Afegir la Variable de Control (Uniform)

Necessitem una forma d’activar/desactivar el flash des del codi GDScript. Per això usem una variable uniform:

glsl
shader_type canvas_item;

// Variable GLOBAL: la mateixa per a tots els píxels
// La controlarem des de GDScript
uniform bool flash_active = false;

void fragment() {
  vec4 color_original = texture(TEXTURE, UV);
  COLOR = color_original;
}

Per què uniform?

  • És una variable el valor de la qual ve de fora del shader (des de GDScript/CPU).
  • Tots els 10.000 fils reben el mateix valor. Per això s’anomena “uniforme”.
  • A diferència d’UV (que és diferent per a cada fil), flash_active serà true o false per a TOTS alhora.

Fase 4: La Lògica del Flash

Ara afegim la lògica condicional. Si flash_active és true, pintem blanc. Si no, pintem normal:

glsl
shader_type canvas_item;

uniform bool flash_active = false;

void fragment() {
  vec4 color_original = texture(TEXTURE, UV);
  
  if (flash_active) {
      // Si està actiu, pintem BLANC
      // Usem la transparència original (color_original.a) per no pintar píxels invisibles
      COLOR = vec4(1.0, 1.0, 1.0, color_original.a);
  } else {
      // Si no, pintem el color normal
      COLOR = color_original;
  }
}

Per què color_original.a? Si usàvem vec4(1.0, 1.0, 1.0, 1.0), pintaríem un quadrat blanc sòlid (incloent les àrees transparents del sprite). En preservar la transparència original (.a), només es tornen blancs els píxels que ja eren visibles.


5. Connectant CPU i GPU

El shader està llest, però no fa res perquè flash_active sempre és false. Ara tornem a GDScript (enemy_plane.gd) per activar-lo quan rebem dany.

Fase 1: Accedir al Material

Primer necessitem crear la funció hit_flash(), creem una variable amb la referència de Sprite2D i una altra amb la referència del material.

gdscript
func hit_flash():
  # 1. Obtenim el node Sprite2D
  var sprite = $Sprite2D
  
  # 2. Accedim al seu material i el tractem com ShaderMaterial
  var shader_material = sprite.material

sprite.material retorna un tipus genèric Material. Per accedir a set_shader_parameter(), necessitem que GDScript sàpiga que és un ShaderMaterial.

La solució és usar if shader_material is ShaderMaterial. Això mata dos ocells d’un tret, després ho explicaré:

Fase 2: Enviar la Dada a la GPU

Usem set_shader_parameter() per canviar el valor del uniform:

gdscript
func hit_flash():
  var sprite = $Sprite2D
  var shader_material = sprite.material

  if shader_material is ShaderMaterial:
      # Activem el flash (enviem 'true' a la GPU)
      shader_material.set_shader_parameter("flash_active", true)

Com funciona if shader_material is ShaderMaterial? Això s’anomena type narrowing (estrenyiment de tipus):

  1. Verifica en temps d’execució si shader_material és realment un ShaderMaterial.
  2. Dins del bloc if, GDScript sap que shader_material és un ShaderMaterial, habilitant l’autocompletat de set_shader_parameter().

Es pot assignar el tipus d’una altra manera usant as ShaderMaterial, però si el tipus no coincideix, retornarà null i podries tenir errors en intentar usar el resultat. Amb is, evitem aquest risc.

Què fa set_shader_parameter?

  • "flash_active" és el nom exacte de la variable uniform al shader.
  • true és el valor que volem assignar.
  • En cridar això, els 10.000 fils de la GPU reben true i pinten blanc.

Fase 3: Apagar el Flash

Si deixem el flash actiu per sempre, l’enemic es queda blanc. Necessitem apagar-lo després d’un instant:

gdscript
func hit_flash():
  var sprite = $Sprite2D
  var shader_material = sprite.material
  
  # Verifiquem que el material sigui un ShaderMaterial
  if shader_material is ShaderMaterial:
      # Activar flash
      shader_material.set_shader_parameter("flash_active", true)
      
      # Esperar 0.1 segons (100 mil·lisegons)
      await get_tree().create_timer(0.1).timeout
      
      # Desactivar flash
      shader_material.set_shader_parameter("flash_active", false)

Què fa await?

await és una paraula clau que pausa l’execució d’aquesta funció específica fins que passi alguna cosa (en aquest cas, que passi el temps del timer).

Important: await NO congela el joc. La resta del motor segueix funcionant:

  • L’enemic segueix movent-se.
  • Altres bales segueixen volant.
  • El jugador pot disparar.

Només AQUESTA funció (hit_flash) està “adormida” esperant. Quan el timer acaba, la funció “es desperta” i continua des d’on es va quedar (desactivar el flash).

És com posar un recordatori al teu mòbil: “En 0.1 segons, apaga el flash”. Mentrestant, segueixes amb la teva vida.

Fase 4: Cridar al Flash al Rebre Dany

Finalment, integrem la funció a _on_area_entered:

gdscript
func _on_area_entered(area):
  if is_dying:
      return
      
  hp -= 1
  area.queue_free()
  hit_flash() # Nou: activar el feedback visual
  
  if hp <= 0:
      die()

6. Script Complet del Shader

glsl
shader_type canvas_item;

uniform bool flash_active = false;

void fragment() {
  vec4 color_original = texture(TEXTURE, UV);
  
  if (flash_active) {
      COLOR = vec4(1.0, 1.0, 1.0, color_original.a);
  } else {
      COLOR = color_original;
  }
}

Repassem el que hem après

  1. Shader: Programa que s’executa a la GPU per modificar píxels.
  2. CPU vs GPU: Serial (una tasca alhora) vs Paral·lel (milers simultànies).
  3. shader_type canvas_item: Declaració per a shaders 2D.
  4. fragment(): Funció que s’executa un cop per cada píxel.
  5. COLOR: Variable de sortida que defineix el color final del píxel.
  6. vec4: Estructura de 4 floats per a colors (RGBA).
  7. TEXTURE: Referència a la imatge del sprite.
  8. UV: Coordenades normalitzades (0-1) de cada píxel.
  9. texture(): Funció per llegir un color d’una imatge.
  10. uniform: Variable global controlada des de la CPU.
  11. set_shader_parameter(): Funció de GDScript per enviar dades a la GPU.

En el proper capítol, aprendrem a donar vida als nostres enemics usant Trigonometria per crear patrons de moviment ondulants i circulars.