Shaders Personalizados y Pipeline de Render
Shaders Personalizados y Pipeline de Render
Los shaders son programas que se ejecutan en la GPU para controlar cómo se renderizan los gráficos. En esta lección aprenderás a crear efectos visuales únicos usando GLSL y personalizar el pipeline de render de Defold.
🎨 Fundamentos de Shaders en Defold
Anatomía de un Shader
// sprite.vp (Vertex Shader)
attribute vec4 position;
attribute vec2 texcoord0;
attribute vec4 color;
varying vec2 var_texcoord0;
varying vec4 var_color;
uniform mat4 view_proj;
void main()
{
gl_Position = view_proj * vec4(position.xyz, 1.0);
var_texcoord0 = texcoord0;
var_color = color;
}
// sprite.fp (Fragment Shader)
precision mediump float;
varying vec2 var_texcoord0;
varying vec4 var_color;
uniform sampler2D texture_sampler;
uniform vec4 tint;
uniform float time;
void main()
{
vec4 tex_color = texture2D(texture_sampler, var_texcoord0);
gl_FragColor = tex_color * var_color * tint;
}
Material Personalizado
-- custom_material.material
name: "custom_sprite"
tags: ["sprite"]
vertex_program: "/shaders/custom_sprite.vp"
fragment_program: "/shaders/custom_sprite.fp"
vertex_constants:
- name: "view_proj"
type: "CONSTANT_TYPE_VIEWPROJ"
fragment_constants:
- name: "tint"
type: "CONSTANT_TYPE_USER"
value: [1.0, 1.0, 1.0, 1.0]
- name: "time"
type: "CONSTANT_TYPE_USER"
value: [0.0, 0.0, 0.0, 0.0]
samplers:
- name: "texture_sampler"
wrap_u: "WRAP_MODE_REPEAT"
wrap_v: "WRAP_MODE_REPEAT"
filter_min: "FILTER_MODE_MIN_LINEAR"
filter_mag: "FILTER_MODE_MAG_LINEAR"
Control desde Script
-- shader_controller.script
function init(self)
self.time = 0
self.material_url = "#sprite"
-- Configurar valores iniciales
sprite.set_constant(self.material_url, "tint", vmath.vector4(1, 1, 1, 1))
end
function update(self, dt)
self.time = self.time + dt
-- Actualizar tiempo en el shader
sprite.set_constant(self.material_url, "time", vmath.vector4(self.time, 0, 0, 0))
end
-- Funciones de utilidad para efectos
local function flash_white(self, duration)
local white = vmath.vector4(2, 2, 2, 1)
local normal = vmath.vector4(1, 1, 1, 1)
sprite.set_constant(self.material_url, "tint", white)
go.animate(".", "tint", go.PLAYBACK_ONCE_FORWARD, normal, go.EASING_OUTQUAD, duration)
end
local function fade_out(self, duration)
local transparent = vmath.vector4(1, 1, 1, 0)
go.animate(".", "tint.w", go.PLAYBACK_ONCE_FORWARD, 0, go.EASING_OUTSINE, duration)
end
✨ Efectos Visuales Avanzados
Dissolve Effect
// dissolve.fp
precision mediump float;
varying vec2 var_texcoord0;
varying vec4 var_color;
uniform sampler2D texture_sampler;
uniform sampler2D noise_texture;
uniform float dissolve_amount;
uniform vec3 edge_color;
uniform float edge_width;
void main()
{
vec4 tex_color = texture2D(texture_sampler, var_texcoord0);
float noise = texture2D(noise_texture, var_texcoord0).r;
// Calcular dissolve
float dissolve_edge = dissolve_amount + edge_width;
if (noise < dissolve_amount) {
discard; // Pixel completamente disuelto
} else if (noise < dissolve_edge) {
// Borde brillante
float edge_factor = (dissolve_edge - noise) / edge_width;
gl_FragColor = vec4(edge_color, 1.0) * edge_factor + tex_color * (1.0 - edge_factor);
} else {
gl_FragColor = tex_color * var_color;
}
}
Outline Effect
// outline.fp
precision mediump float;
varying vec2 var_texcoord0;
varying vec4 var_color;
uniform sampler2D texture_sampler;
uniform vec2 texture_size;
uniform vec4 outline_color;
uniform float outline_width;
void main()
{
vec2 texel_size = 1.0 / texture_size;
vec4 tex_color = texture2D(texture_sampler, var_texcoord0);
// Si el pixel actual no es transparente, renderizar normal
if (tex_color.a > 0.1) {
gl_FragColor = tex_color * var_color;
return;
}
// Buscar pixels no transparentes alrededor
float outline_alpha = 0.0;
for (float x = -outline_width; x <= outline_width; x++) {
for (float y = -outline_width; y <= outline_width; y++) {
vec2 offset = vec2(x, y) * texel_size;
float sample_alpha = texture2D(texture_sampler, var_texcoord0 + offset).a;
if (sample_alpha > 0.1) {
outline_alpha = 1.0;
break;
}
}
if (outline_alpha > 0.0) break;
}
gl_FragColor = outline_color * outline_alpha;
}
Distortion Effects
// distortion.fp
precision mediump float;
varying vec2 var_texcoord0;
varying vec4 var_color;
uniform sampler2D texture_sampler;
uniform float time;
uniform vec2 distortion_strength;
uniform float wave_frequency;
void main()
{
// Ondas sinusoidales para distorsión
vec2 distortion = vec2(
sin(var_texcoord0.y * wave_frequency + time * 3.0) * distortion_strength.x,
cos(var_texcoord0.x * wave_frequency + time * 2.0) * distortion_strength.y
);
vec2 distorted_uv = var_texcoord0 + distortion;
// Verificar límites de UV
if (distorted_uv.x < 0.0 || distorted_uv.x > 1.0 ||
distorted_uv.y < 0.0 || distorted_uv.y > 1.0) {
discard;
}
vec4 tex_color = texture2D(texture_sampler, distorted_uv);
gl_FragColor = tex_color * var_color;
}
Water Shader
// water.fp
precision mediump float;
varying vec2 var_texcoord0;
varying vec4 var_color;
uniform sampler2D texture_sampler;
uniform sampler2D normal_map;
uniform float time;
uniform vec3 light_direction;
uniform vec4 water_color;
uniform float reflection_strength;
vec3 calculate_normal(vec2 uv) {
vec2 offset = vec2(0.01, 0.0);
// Sample normal map con movimiento
vec2 moving_uv1 = uv + vec2(time * 0.02, time * 0.03);
vec2 moving_uv2 = uv + vec2(-time * 0.015, time * 0.025);
vec3 normal1 = texture2D(normal_map, moving_uv1).rgb * 2.0 - 1.0;
vec3 normal2 = texture2D(normal_map, moving_uv2).rgb * 2.0 - 1.0;
return normalize(normal1 + normal2);
}
void main()
{
vec3 normal = calculate_normal(var_texcoord0);
// Cálculo de iluminación
float NdotL = max(dot(normal, normalize(light_direction)), 0.0);
// Reflexión especular
vec3 view_dir = vec3(0.0, 0.0, 1.0);
vec3 reflect_dir = reflect(-light_direction, normal);
float spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0);
// Color base del agua
vec4 base_color = texture2D(texture_sampler, var_texcoord0);
vec4 final_color = mix(base_color, water_color, 0.7);
// Aplicar iluminación
final_color.rgb *= (0.3 + 0.7 * NdotL);
final_color.rgb += spec * reflection_strength;
gl_FragColor = final_color * var_color;
}
🌈 Post-Processing Pipeline
Custom Render Script
-- custom_render.render_script
function init(self)
self.tile_pred = render.predicate({"tile"})
self.gui_pred = render.predicate({"gui"})
self.text_pred = render.predicate({"text"})
self.particle_pred = render.predicate({"particle"})
-- Render targets para post-processing
local color_params = {
format = render.FORMAT_RGBA,
width = render.get_window_width(),
height = render.get_window_height(),
min_filter = render.FILTER_LINEAR,
mag_filter = render.FILTER_LINEAR,
u_wrap = render.WRAP_CLAMP_TO_EDGE,
v_wrap = render.WRAP_CLAMP_TO_EDGE
}
self.screen_rt = render.render_target(color_params)
self.blur_rt = render.render_target(color_params)
-- Quad para fullscreen effects
self.fullscreen_quad = self:create_fullscreen_quad()
-- Post-processing materials
self.blur_material = resource.load("/materials/blur.material")
self.bloom_material = resource.load("/materials/bloom.material")
self.tone_map_material = resource.load("/materials/tonemap.material")
end
local function create_fullscreen_quad(self)
local vertices = {
-1, -1, 0, 0, 0, -- Bottom-left
1, -1, 0, 1, 0, -- Bottom-right
1, 1, 0, 1, 1, -- Top-right
-1, 1, 0, 0, 1 -- Top-left
}
local indices = {0, 1, 2, 0, 2, 3}
return render.create_vertex_buffer(vertices, indices)
end
function update(self)
local width = render.get_window_width()
local height = render.get_window_height()
-- Clear screen
render.set_viewport(0, 0, width, height)
render.clear({[render.BUFFER_COLOR_BIT] = vmath.vector4(0, 0, 0, 1)})
-- === PASS 1: Render scene to texture ===
render.set_render_target(self.screen_rt)
render.set_viewport(0, 0, width, height)
render.clear({[render.BUFFER_COLOR_BIT] = vmath.vector4(0, 0, 0, 0)})
-- Render game objects
render.set_view(self.view)
render.set_projection(self.projection)
render.draw(self.tile_pred)
render.draw(self.particle_pred)
-- === PASS 2: Post-processing ===
render.set_render_target(render.RENDER_TARGET_DEFAULT)
render.set_view(vmath.matrix4())
render.set_projection(vmath.matrix4_orthographic(0, width, 0, height, -1, 1))
-- Apply post-processing effects
self:apply_bloom_effect()
self:apply_tone_mapping()
-- === PASS 3: UI ===
render.set_view(vmath.matrix4())
render.set_projection(vmath.matrix4_orthographic(0, width, 0, height, -1, 1))
render.enable_state(render.STATE_STENCIL_TEST)
render.draw(self.gui_pred)
render.draw(self.text_pred)
render.disable_state(render.STATE_STENCIL_TEST)
end
local function apply_bloom_effect(self)
-- Extraer píxeles brillantes
render.enable_material(self.bloom_material)
render.set_constant("threshold", vmath.vector4(0.8, 0, 0, 0))
-- Render to blur target
render.set_render_target(self.blur_rt)
render.draw(self.fullscreen_quad, {texture = self.screen_rt})
-- Aplicar blur
self:apply_gaussian_blur(self.blur_rt, 2)
render.disable_material()
end
local function apply_gaussian_blur(self, source_rt, iterations)
render.enable_material(self.blur_material)
for i = 1, iterations do
-- Horizontal blur
render.set_constant("blur_direction", vmath.vector4(1, 0, 0, 0))
-- ... render pass
-- Vertical blur
render.set_constant("blur_direction", vmath.vector4(0, 1, 0, 0))
-- ... render pass
end
render.disable_material()
end
Bloom Shader
// bloom_extract.fp
precision mediump float;
varying vec2 var_texcoord0;
uniform sampler2D texture_sampler;
uniform float threshold;
void main()
{
vec4 color = texture2D(texture_sampler, var_texcoord0);
// Extraer píxeles brillantes
float brightness = dot(color.rgb, vec3(0.299, 0.587, 0.114));
if (brightness > threshold) {
gl_FragColor = color;
} else {
gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
}
}
// gaussian_blur.fp
precision mediump float;
varying vec2 var_texcoord0;
uniform sampler2D texture_sampler;
uniform vec2 blur_direction;
uniform vec2 texture_size;
const float weights[5] = float[](0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216);
void main()
{
vec2 tex_offset = 1.0 / texture_size;
vec3 result = texture2D(texture_sampler, var_texcoord0).rgb * weights[0];
for (int i = 1; i < 5; ++i) {
vec2 offset = blur_direction * float(i) * tex_offset;
result += texture2D(texture_sampler, var_texcoord0 + offset).rgb * weights[i];
result += texture2D(texture_sampler, var_texcoord0 - offset).rgb * weights[i];
}
gl_FragColor = vec4(result, 1.0);
}
🎭 Efectos de Iluminación
Dynamic Lighting
// dynamic_lighting.fp
precision mediump float;
varying vec2 var_texcoord0;
varying vec4 var_color;
varying vec3 var_world_position;
uniform sampler2D texture_sampler;
uniform vec3 light_positions[8];
uniform vec3 light_colors[8];
uniform float light_radii[8];
uniform int light_count;
uniform vec3 ambient_color;
void main()
{
vec4 tex_color = texture2D(texture_sampler, var_texcoord0);
vec3 final_color = ambient_color;
// Calcular contribución de cada luz
for (int i = 0; i < 8; i++) {
if (i >= light_count) break;
vec3 light_dir = light_positions[i] - var_world_position;
float distance = length(light_dir);
if (distance < light_radii[i]) {
light_dir = normalize(light_dir);
// Atenuación
float attenuation = 1.0 - (distance / light_radii[i]);
attenuation = attenuation * attenuation;
// Añadir contribución de luz
final_color += light_colors[i] * attenuation;
}
}
gl_FragColor = tex_color * vec4(final_color, 1.0) * var_color;
}
Shadow Mapping
// shadow_map.vp
attribute vec4 position;
attribute vec2 texcoord0;
varying vec2 var_texcoord0;
varying vec4 var_shadow_coord;
uniform mat4 view_proj;
uniform mat4 light_view_proj;
void main()
{
gl_Position = view_proj * position;
var_texcoord0 = texcoord0;
// Transformar a espacio de luz
var_shadow_coord = light_view_proj * position;
}
// shadow_map.fp
precision mediump float;
varying vec2 var_texcoord0;
varying vec4 var_shadow_coord;
uniform sampler2D texture_sampler;
uniform sampler2D shadow_map;
float calculate_shadow(vec4 shadow_coord) {
vec3 proj_coords = shadow_coord.xyz / shadow_coord.w;
proj_coords = proj_coords * 0.5 + 0.5;
if (proj_coords.z > 1.0) return 0.0;
float closest_depth = texture2D(shadow_map, proj_coords.xy).r;
float current_depth = proj_coords.z;
float bias = 0.005;
return current_depth - bias > closest_depth ? 1.0 : 0.0;
}
void main()
{
vec4 color = texture2D(texture_sampler, var_texcoord0);
float shadow = calculate_shadow(var_shadow_coord);
vec3 lighting = (1.0 - shadow) * vec3(1.0);
gl_FragColor = vec4(color.rgb * lighting, color.a);
}
🔥 Efectos de Partículas con Shaders
GPU Particles
// particle_update.vp (Compute Shader simulado)
attribute vec3 position;
attribute vec3 velocity;
attribute float life;
varying vec3 out_position;
varying vec3 out_velocity;
varying float out_life;
uniform float delta_time;
uniform vec3 gravity;
uniform float damping;
void main()
{
out_life = life - delta_time;
if (out_life > 0.0) {
// Actualizar física
out_velocity = velocity + gravity * delta_time;
out_velocity *= damping;
out_position = position + out_velocity * delta_time;
} else {
// Partícula muerta
out_position = vec3(0.0);
out_velocity = vec3(0.0);
out_life = 0.0;
}
gl_Position = vec4(out_position, 1.0);
}
Fire Effect
// fire_particle.fp
precision mediump float;
varying vec2 var_texcoord0;
varying float var_life;
uniform sampler2D noise_texture;
uniform float time;
void main()
{
vec2 uv = var_texcoord0;
// Distorsionar UV con noise
vec2 noise_uv = uv + vec2(time * 0.1, time * 0.2);
float noise = texture2D(noise_texture, noise_uv).r;
uv.x += (noise - 0.5) * 0.1;
// Gradiente de fuego
float height_factor = 1.0 - uv.y;
float life_factor = var_life;
// Colores del fuego
vec3 fire_color;
if (height_factor > 0.7) {
fire_color = mix(vec3(1.0, 1.0, 0.8), vec3(1.0, 0.5, 0.0), (height_factor - 0.7) / 0.3);
} else if (height_factor > 0.3) {
fire_color = mix(vec3(1.0, 0.5, 0.0), vec3(1.0, 0.0, 0.0), (height_factor - 0.3) / 0.4);
} else {
fire_color = mix(vec3(1.0, 0.0, 0.0), vec3(0.0, 0.0, 0.0), height_factor / 0.3);
}
float alpha = height_factor * life_factor * (0.5 + 0.5 * noise);
gl_FragColor = vec4(fire_color, alpha);
}
🎮 Proyecto Práctico: Sistema de Efectos Completo
Vamos a crear un sistema completo de efectos visuales:
1. Effect Manager
-- effect_manager.script
local EFFECT_TYPES = {
DISSOLVE = "dissolve",
OUTLINE = "outline",
GLOW = "glow",
DISTORTION = "distortion",
COLOR_GRADE = "color_grade"
}
function init(self)
self.active_effects = {}
self.effect_materials = {}
self.global_time = 0
-- Cargar materiales de efectos
for effect_type, _ in pairs(EFFECT_TYPES) do
local material_path = "/materials/" .. effect_type .. ".material"
self.effect_materials[effect_type] = resource.load(material_path)
end
end
local function apply_effect(self, target, effect_type, parameters)
local effect_id = hash(tostring(target) .. effect_type)
local effect = {
target = target,
type = effect_type,
parameters = parameters or {},
start_time = self.global_time,
duration = parameters.duration or 1.0,
material = self.effect_materials[effect_type]
}
self.active_effects[effect_id] = effect
-- Cambiar material del objeto
if effect.material then
sprite.set_material(target, effect.material)
end
return effect_id
end
local function update_effect(self, effect_id, effect)
local elapsed = self.global_time - effect.start_time
local progress = math.min(elapsed / effect.duration, 1.0)
if effect.type == EFFECT_TYPES.DISSOLVE then
local dissolve_amount = progress
sprite.set_constant(effect.target, "dissolve_amount", vmath.vector4(dissolve_amount, 0, 0, 0))
elseif effect.type == EFFECT_TYPES.GLOW then
local glow_intensity = math.sin(self.global_time * 3.0) * 0.5 + 0.5
sprite.set_constant(effect.target, "glow_intensity", vmath.vector4(glow_intensity, 0, 0, 0))
elseif effect.type == EFFECT_TYPES.DISTORTION then
local distortion_strength = effect.parameters.strength or 0.1
sprite.set_constant(effect.target, "distortion_strength",
vmath.vector4(distortion_strength * progress, 0, 0, 0))
end
-- Remover efecto si terminó
if progress >= 1.0 and effect.duration > 0 then
self:remove_effect(effect_id)
end
end
function update(self, dt)
self.global_time = self.global_time + dt
-- Actualizar tiempo global en todos los shaders
for _, effect in pairs(self.active_effects) do
sprite.set_constant(effect.target, "time", vmath.vector4(self.global_time, 0, 0, 0))
end
-- Actualizar efectos activos
for effect_id, effect in pairs(self.active_effects) do
update_effect(self, effect_id, effect)
end
end
2. Shader Library
-- shader_library.script
local ShaderLib = {}
function ShaderLib.create_dissolve_effect(target, duration, edge_color)
return effect_manager.apply_effect(target, "dissolve", {
duration = duration,
edge_color = edge_color or vmath.vector3(1, 0.5, 0),
edge_width = 0.1
})
end
function ShaderLib.create_hit_flash(target)
return effect_manager.apply_effect(target, "flash", {
duration = 0.2,
flash_color = vmath.vector3(1, 1, 1),
intensity = 2.0
})
end
function ShaderLib.create_force_field(target)
return effect_manager.apply_effect(target, "force_field", {
duration = -1, -- Efecto permanente
wave_frequency = 5.0,
wave_amplitude = 0.1
})
end
function ShaderLib.create_hologram(target)
return effect_manager.apply_effect(target, "hologram", {
duration = -1,
scan_line_speed = 2.0,
flicker_intensity = 0.3,
tint_color = vmath.vector3(0, 1, 1)
})
end
return ShaderLib
📊 Optimización de Shaders
Performance Profiling
-- shader_profiler.script
function init(self)
self.shader_stats = {}
self.draw_call_count = 0
self.last_profile_time = 0
end
local function profile_shader_performance(self)
-- Medir draw calls por shader
local stats = render.get_render_stats()
for material_name, usage in pairs(stats.materials) do
if not self.shader_stats[material_name] then
self.shader_stats[material_name] = {
draw_calls = 0,
pixel_fills = 0,
avg_time = 0
}
end
self.shader_stats[material_name].draw_calls = usage.draw_calls
self.shader_stats[material_name].pixel_fills = usage.pixel_fills
end
-- Detectar shaders costosos
for material, stats in pairs(self.shader_stats) do
if stats.pixel_fills > 100000 then -- Muchos pixels
print("Warning: Shader costoso detectado:", material)
end
end
end
Shader Optimization Tips
// Optimizaciones en GLSL
// ❌ Evitar: divisiones costosas
float result = expensive_calculation() / 2.0;
// ✅ Mejor: usar multiplicación
float result = expensive_calculation() * 0.5;
// ❌ Evitar: funciones trigonométricas en loops
for (int i = 0; i < 10; i++) {
float angle = sin(float(i) * 0.1);
}
// ✅ Mejor: precalcular valores
float[10] precomputed_angles = float[10](...);
// ❌ Evitar: sampling excesivo de texturas
vec4 color1 = texture2D(tex, uv + offset1);
vec4 color2 = texture2D(tex, uv + offset2);
vec4 color3 = texture2D(tex, uv + offset3);
// ✅ Mejor: usar mip-mapping y LOD
vec4 color = texture2DLod(tex, uv, lod_level);
📚 Recursos y Referencias
GLSL Built-in Functions
mix()- Interpolación linealsmoothstep()- Interpolación suaveclamp()- Limitar valoresnormalize()- Normalizar vectoresdot()- Producto puntoreflect()- Reflexión vectorial
Herramientas de Desarrollo
- Shader Toy - Editor online para prototipos
- RenderDoc - Debug de render pipeline
- Mali Graphics Debugger - Profiling móvil
- Xcode GPU Debugger - Debug en iOS
Links Útiles
🎯 Ejercicios Propuestos
-
Shader de Agua Avanzado: Crea un shader de agua con reflexiones, refracciones y foam.
-
Sistema de Post-Processing: Implementa una cadena completa con bloom, color grading y anti-aliasing.
-
Shader de Tela: Simula el comportamiento de diferentes tipos de tela con subsurface scattering.
-
Efectos de Clima: Crea shaders para lluvia, nieve y niebla volumétrica.
-
Cel Shading: Implementa un pipeline completo de cel shading con outlines.
Los shaders son una herramienta poderosa que puede transformar completamente la apariencia de tu juego. Dominar GLSL te permitirá crear efectos visuales únicos que distingan tu juego de la competencia.