Los shaders son fundamentales para los gráficos modernos. Dominar sus técnicas puede mejorar tanto el rendimiento como la calidad visual. A continuación, se presentan tips detallados y ejemplos extensos para experimentar y aprender.
Optimizar cálculos dentro del shader es crítico para mantener un buen rendimiento, especialmente en fragment shaders que se ejecutan millones de veces por frame. Siempre busca precalcular lo que puedas y usa las funciones nativas del lenguaje.
// Precalcular constantes fuera de loops pesados
const vec3 lightDir = normalize(vec3(1.0, 0.8, 0.6));
const float ambientStrength = 0.1;
// Función de iluminación difusa con normalización
vec3 computeLighting(vec3 fragPos, vec3 normal) {
// Calcula la dirección de luz normalizada
vec3 norm = normalize(normal);
vec3 light = normalize(lightDir);
// Componente difusa
float diff = max(dot(norm, light), 0.0);
// Componente especular usando Blinn-Phong
vec3 viewDir = normalize(-fragPos); // cámara en origen
vec3 halfwayDir = normalize(light + viewDir);
float spec = pow(max(dot(norm, halfwayDir), 0.0), 32.0);
// Resultado final
vec3 ambient = ambientStrength * vec3(1.0);
vec3 diffuse = diff * vec3(1.0);
vec3 specular = spec * vec3(1.0);
return ambient + diffuse + specular;
}
Raymarching permite crear escenas complejas a partir de signed distance fields (SDF). La clave está en usar pasos adaptativos y mantener los campos de distancia limpios.
// SDF de esfera y caja combinados
float sceneSDF(vec3 p) {
float sphere = length(p) - 1.0;
float box = length(max(abs(p) - vec3(0.5), 0.0)) - 0.2;
return min(sphere, box);
}
// Función de raymarching con pasos adaptativos
float rayMarch(vec3 ro, vec3 rd) {
float totalDistance = 0.0;
const int MAX_STEPS = 128;
const float EPSILON = 0.001;
const float MAX_DIST = 100.0;
for(int i = 0; i < MAX_STEPS; i++) {
vec3 p = ro + rd * totalDistance;
float dist = sceneSDF(p);
if(dist < EPSILON) return totalDistance; // colisión
totalDistance += dist;
if(totalDistance > MAX_DIST) break; // fuera de rango
}
return -1.0; // no colisión
}
// Calcular normal usando gradiente de SDF
vec3 calcNormal(vec3 p) {
const float h = 0.0001;
vec2 k = vec2(1, -1);
return normalize(
k.xyy * sceneSDF(p + k.xyy * h) +
k.yyx * sceneSDF(p + k.yyx * h) +
k.yxy * sceneSDF(p + k.yxy * h) +
k.xxx * sceneSDF(p + k.xxx * h)
);
}
MAX_STEPS y EPSILON según precisión y rendimiento.min (union), max (intersección), - (diferencia).Trabajar con texturas requiere coordenadas precisas y un buen control de la interpolación para evitar stretching o artefactos. Además, repetir texturas con fract permite patrones continuos.
// Coordenadas UV con repetición
vec2 repeatUV(vec2 uv, float times) {
return fract(uv * times);
}
// Aplicar textura con mipmaps para mejorar calidad
vec4 sampleTexture(sampler2D tex, vec2 uv, float mipLevel) {
return textureLod(tex, uv, mipLevel);
}
// Ejemplo de mezcla de dos texturas con máscara
vec4 blendTextures(sampler2D tex1, sampler2D tex2, vec2 uv, float mask) {
vec4 c1 = texture(tex1, uv);
vec4 c2 = texture(tex2, uv);
return mix(c1, c2, mask);
}
// Normal mapping
vec3 applyNormalMap(vec3 normal, sampler2D normalMap, vec2 uv) {
vec3 n = texture(normalMap, uv).rgb;
n = normalize(n * 2.0 - 1.0);
return normalize(normal + n);
}
Controlar el color y los gradientes es clave para efectos visuales impactantes. Usar funciones como mix, pow y smoothstep permite transiciones suaves.
// Gradiente radial
vec3 radialGradient(vec2 uv, vec3 innerColor, vec3 outerColor) {
float d = length(uv - 0.5);
float t = smoothstep(0.0, 0.5, d);
return mix(innerColor, outerColor, t);
}
// Gradiente basado en ángulo
vec3 angularGradient(vec2 uv, vec3 color1, vec3 color2) {
float angle = atan(uv.y - 0.5, uv.x - 0.5) / 3.14159 + 0.5;
return mix(color1, color2, angle);
}
// Ajuste de curva de luminancia
vec3 adjustBrightness(vec3 color, float gamma) {
return pow(color, vec3(gamma));
}
Ver lo que ocurre dentro del shader ayuda a depurar problemas de iluminación, normales o texturas.
// Mostrar normales como color
vec4 debugNormals(vec3 normal) {
return vec4(normal * 0.5 + 0.5, 1.0);
}
// Mostrar distancia de raymarching
vec4 debugDistance(float dist, float maxDist) {
float intensity = dist / maxDist;
return vec4(vec3(intensity), 1.0);
}
// Mapas de calor de intensidad de luz
vec4 debugLightIntensity(float intensity) {
return vec4(vec3(intensity), 1.0);
}
El rendimiento es crucial en tiempo real. Pequeños ajustes pueden multiplicar la velocidad sin afectar la calidad.
// Reducir resolución para pruebas rápidas
vec2 lowResUV(vec2 uv, float scale) {
return floor(uv * scale) / scale;
}
// Operaciones vectoriales preferibles a escalares
vec3 computeLightingVector(vec3 normal, vec3 lightDir, vec3 color) {
float diff = max(dot(normal, lightDir), 0.0);
return diff * color;
}
// Limitar loops y pasos
for(int i = 0; i < 64; i++) {
vec3 p = rayOrigin + rayDir * step;
float d = sceneSDF(p);
if(d < 0.001) break;
step += d;
}
unroll cuando sea posible.