← Volver al listado de tecnologías

Shaders Personalizados y Pipeline de Render

Por: Artiko
defoldshadersglslrenderingefectos-visuales

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

Herramientas de Desarrollo

🎯 Ejercicios Propuestos

  1. Shader de Agua Avanzado: Crea un shader de agua con reflexiones, refracciones y foam.

  2. Sistema de Post-Processing: Implementa una cadena completa con bloom, color grading y anti-aliasing.

  3. Shader de Tela: Simula el comportamiento de diferentes tipos de tela con subsurface scattering.

  4. Efectos de Clima: Crea shaders para lluvia, nieve y niebla volumétrica.

  5. 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.