Manual/ Gráficos

Shaders propios

Escribe en GLSL cómo se ve una superficie: brillos, hologramas, disolver, agua que se mueve.

Un shader de superficie (.crshader) decide cómo se ve un material: su color, su brillo, su relieve, si emite luz o si se mueve. Se escribe en GLSL (el lenguaje de shaders de Vulkan/OpenGL), pero solo la parte que importa: el motor se encarga de la luz, las sombras, los reflejos, la niebla y la lluvia, así que tu superficie queda integrada en la escena como cualquier otro material (igual que los surface shaders de Unity).

Crear y usar un shader

  • Panel Proyecto > clic derecho > Crear > Shader (GLSL). Se abre en el editor de scripts con una plantilla que ya funciona.
  • Elige un material (.crmat) y en su Inspector, Shader > tu archivo. O clic derecho sobre el .crshader > Crear material con este shader.
  • Arrastra el material a un objeto. Cada vez que guardes el shader (Ctrl+S) se recompila y se ve al momento, también en Play.
Si hay un error, la línea se marca en rojo en el editor, el mensaje sale en la Consola y en el material, y mientras tanto el objeto se ve con el shader estándar. El icono del shader en el Proyecto se pone rojo.

Cómo es un .crshader

Assets/Shaders/Pulso.crshader

// Propiedades: se editan en el material y se leen aqui por su nombre.
property color tinte = 1.0, 0.4, 0.1
property range brillo = 2.0 (0, 10)
property float velocidad = 3.0

// Obligatoria: recibe la superficie ya calculada (texturas y valores del
// material) y la cambia.
void surface(inout Surface s) {
    float pulso = 0.5 + 0.5 * sin(TIME * velocidad);
    s.albedo *= tinte;
    s.emission = tinte * brillo * pulso;
}

// Opcional: mover los vertices antes de dibujarlos.
void vertex(inout Vertex v) {
    v.position += v.normal * 0.02 * sin(TIME * velocidad);
}

Puedes escribir más funciones tuyas (fuera de surface y vertex) y usar todo GLSL: vec2, vec3, mix, smoothstep, sin, texture...

Propiedades

Una línea por propiedad, en cualquier parte del archivo. En el material aparecen con el control que les toca.

LíneaEn el códigoEn el material
property float fuerza = 1.5floatNúmero.
property range brillo = 2 (0, 10)floatDeslizador entre 0 y 10.
property color tinte = 1, 0.5, 0.2vec3Selector de color.
property vector viento = 1, 0, 0vec3Tres números.
property texture ruidosampler2DHueco para una imagen de Assets (blanca si está vacío).

Como mucho 8 propiedades de números (float, range, color, vector) y 4 texturas. Los valores se cambian en vivo desde el material sin recompilar nada. Clic derecho sobre una propiedad: Valor por defecto.

La superficie (Surface)

CampoDescripción
s.albedo (vec3)Color base, como se ve (el mismo espacio que las texturas).
s.alpha (float)Por debajo de 0.5 el píxel no se dibuja (recortes, agujeros, disolver).
s.normal (vec3)Normal en el mundo, ya con el normal map del material.
s.metallic, s.roughnessMetálico y rugosidad (0..1): 0 de rugosidad es un espejo.
s.occlusionSombra ambiental (1 = ninguna).
s.emission (vec3)Luz propia. 1 equivale a la emisión 1 de un material; valores altos dan bloom.
s.uvCoordenadas de textura (solo lectura).
s.worldPositionPosición del píxel en el mundo (solo lectura).
s.vertexNormalNormal de la malla, sin normal map (solo lectura).
s.viewDirectionDirección del píxel hacia la cámara (solo lectura).

surface() recibe ya calculado lo que pone el material (su color y su textura de color, su normal map, su metálico, su rugosidad...), así que puedes combinar las dos cosas: texturas en el material y el efecto en el shader.

Mover vértices (Vertex)

CampoDescripción
v.positionPosición en el mundo (ya animada si el modelo tiene esqueleto).
v.normalNormal en el mundo.
v.uvCoordenadas de textura.

Lo que tienes a mano

NombreDescripción
TIMESegundos desde que empezó (para animar).
CAMERA_POSITIONPosición de la cámara en el mundo.
noise(vec2), noise(vec3)Ruido suave de 0 a 1.
fbm(vec2)Ruido en varias capas (nubes, manchas, mármol).
hash(vec2), hash(vec3)Número pseudoaleatorio de 0 a 1 por posición.
fresnel(s, potencia)0 de frente y 1 en el contorno (bordes brillantes).
remap(v, a, b, c, d)Pasa v del rango a..b al c..d.

Ejemplo: holograma

Líneas que suben, borde brillante y parpadeo. Queda muy bien con un material oscuro.

Holograma.crshader

property color tinte = 0.2, 0.8, 1.0
property range lineas = 40 (5, 120)
property range velocidad = 2 (0, 10)

void surface(inout Surface s) {
    float linea = step(0.5, fract(s.worldPosition.y * lineas * 0.1 - TIME * velocidad));
    float borde = fresnel(s, 2.5);
    float parpadeo = 0.85 + 0.15 * sin(TIME * 30.0);
    s.albedo = vec3(0.02);
    s.roughness = 0.3;
    s.emission = tinte * (0.3 + linea * 0.7 + borde * 2.0) * parpadeo;
}

Ejemplo: disolver

El objeto se deshace con un borde incandescente. Anima cantidad desde un script para hacerlo desaparecer.

Disolver.crshader

property range cantidad = 0.3 (0, 1)
property color borde = 1.0, 0.45, 0.05
property float escala = 4
property texture ruido   // opcional: una textura de ruido; sin ella se usa fbm()

void surface(inout Surface s) {
    float n = fbm(s.uv * escala) * texture(ruido, s.uv).r;
    if (n < cantidad) s.alpha = 0.0;                 // hueco
    float filo = 1.0 - smoothstep(0.0, 0.06, n - cantidad);
    s.emission += borde * filo * 4.0;               // borde al rojo
}

Ejemplo: lava

Lava.crshader

property color frio = 0.08, 0.02, 0.01
property color caliente = 1.0, 0.35, 0.02
property range velocidad = 0.15 (0, 1)

void surface(inout Surface s) {
    vec2 p = s.worldPosition.xz * 0.35;
    float n = fbm(p + vec2(TIME * velocidad, TIME * velocidad * 0.7));
    float grietas = smoothstep(0.45, 0.75, n);
    s.albedo = mix(frio, caliente, grietas);
    s.roughness = mix(0.9, 0.4, grietas);
    s.emission = caliente * grietas * grietas * 3.0;
}

Ejemplo: agua que se mueve (vértices)

Para un plano con bastantes vértices (por ejemplo, una malla creada por código). Mueve los vértices y recalcula la normal.

Olas.crshader

property range altura = 0.25 (0, 2)
property range frecuencia = 1.2 (0.1, 5)
property color color = 0.05, 0.25, 0.35

float ola(vec2 p) {
    return sin(p.x * frecuencia + TIME * 1.7) * 0.6 + sin((p.x + p.y) * frecuencia * 0.7 + TIME * 1.1) * 0.4;
}

void vertex(inout Vertex v) {
    float h = ola(v.position.xz) * altura;
    // Pendiente por diferencias: la normal de la ola.
    float hx = ola(v.position.xz + vec2(0.1, 0.0)) * altura;
    float hz = ola(v.position.xz + vec2(0.0, 0.1)) * altura;
    v.position.y += h;
    v.normal = normalize(vec3(h - hx, 0.1, h - hz));
}

void surface(inout Surface s) {
    s.albedo = color;
    s.roughness = 0.05;
    s.metallic = 0.0;
}

Ejemplo: vegetación con viento

Mueve más lo que está más alto (la copa) que la base. Úsalo con el material de las hojas.

Viento.crshader

property vector direccion = 1, 0, 0.3
property range fuerza = 0.15 (0, 1)
property float alturaBase = 0

void vertex(inout Vertex v) {
    float alto = max(v.position.y - alturaBase, 0.0);
    float rafaga = sin(TIME * 2.0 + v.position.x * 0.5) + 0.5 * sin(TIME * 5.3 + v.position.z);
    v.position += normalize(direccion) * rafaga * fuerza * alto * 0.3;
}

void surface(inout Surface s) {
}

Límites

  • Las sombras y la selección con el ratón usan la forma original: lo que muevas en vertex() no cambia la sombra.
  • Materiales opacos (con recortes por s.alpha). El modo Transparente del material no usa el shader propio.
  • 8 propiedades de números y 4 texturas por shader.
  • El shader se compila al usarse la primera vez (unas décimas de segundo). El juego exportado lleva el compilador (shaderc_shared.dll) y los compila igual.
¿Qué es cada cosa en GLSL? vec3(1.0, 0.5, 0.0) es un color o una posición; mix(a, b, t) mezcla; smoothstep(a, b, x) pasa suave de 0 a 1; fract(x) es la parte decimal (repite); step(a, x) es 0 o 1. Los decimales llevan punto: 1.0, no 1, cuando se mezclan con float.

¿Algo no queda claro o falta algo? Pregunta en el Discord.