~2 мин чтения

gdshader подробнее — vertex, fragment, light

Структура spatial-шейдера, встроенные функции, uniforms, варинги, шаги оптимизации.

В главе про рендеринг видели базовый ShaderMaterial. Здесь — подробнее: устройство 3D-шейдера, что такое vertex/fragment/light-функции и как писать своё освещение.

Полная структура spatial-шейдера

shader_type spatial;
render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_burley, specular_schlick_ggx;

// Uniforms — параметры материала, доступные из Inspector
uniform sampler2D albedo_tex : source_color, filter_linear_mipmap;
uniform vec4 tint : source_color = vec4(1.0);
uniform float roughness : hint_range(0.0, 1.0) = 0.5;

// Varyings — данные, передаваемые из vertex в fragment
varying vec3 world_pos;
varying float vertical_factor;

void vertex() {
    // Здесь модифицируем позиции вершин и передаём данные вниз
    world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
    vertical_factor = clamp(VERTEX.y * 0.1, 0.0, 1.0);

    // Простая волна: смещение по синусу от мирового времени
    VERTEX.y += sin(TIME + world_pos.x) * 0.1;
}

void fragment() {
    // Здесь считаем цвет каждого пикселя
    vec4 tex = texture(albedo_tex, UV);
    ALBEDO = tex.rgb * tint.rgb;
    ALPHA = tex.a * tint.a;
    ROUGHNESS = roughness;
    METALLIC = 0.0;
    EMISSION = vec3(0.0, 0.5 * vertical_factor, 0.0);  // зелёный градиент по высоте
}

void light() {
    // (опционально) кастомное освещение, заменяет встроенное
    // LIGHT — итоговый цвет, добавляется к ALBEDO
    DIFFUSE_LIGHT += LIGHT_COLOR * dot(NORMAL, LIGHT) * ATTENUATION;
}

Что внутри:

  • shader_type spatial; — это для 3D mesh’а. Для 2D — canvas_item, для skybox — sky, и т.д.
  • render_mode — флаги: blend_* (transparency), cull_* (отбраковка), depth_* (Z-буфер), diffuse_* (модель диффузного освещения), specular_* (зеркальная), unshaded (без света).
  • uniform — параметр, видимый в Inspector материала и доступный из кода через set_shader_parameter.
  • varying — переменная, передаваемая из vertex в fragment (интерполируется по треугольнику).
  • vertex() — модифицирует вершины перед растеризацией.
  • fragment() — считает свойства поверхности (ALBEDO, NORMAL, ROUGHNESS, METALLIC, EMISSION, ALPHA).
  • light() — кастомная функция освещения (если хотите свой не-PBR look).

Встроенные переменные

Главные in/out:

В vertex():

  • VERTEX — позиция вершины в object-space (in/out).
  • NORMAL — нормаль (in/out).
  • TANGENT, BINORMAL — касательные.
  • UV, UV2 — координаты текстуры.
  • COLOR — vertex color.
  • MODEL_MATRIX, VIEW_MATRIX, PROJECTION_MATRIX — стандартные матрицы.
  • TIME — игровое время в секундах.
  • INSTANCE_ID, VERTEX_ID — индексы (для MultiMesh).

В fragment():

  • ALBEDO — диффузный цвет (out, vec3).
  • ALPHA — прозрачность (out, float).
  • METALLIC, ROUGHNESS, SPECULAR — PBR-параметры (out).
  • EMISSION — самосветящееся (out, vec3).
  • NORMAL, NORMAL_MAP — нормали (out).
  • AO, AO_LIGHT_AFFECT — ambient occlusion (out).
  • RIM, RIM_TINT — rim light (out).
  • CLEARCOAT, CLEARCOAT_GLOSS.
  • UV, FRAGCOORD, VIEW, VERTEX — input.
  • DEPTH — out (можно писать в depth buffer).

В light():

  • LIGHT — направление к источнику (in, vec3).
  • LIGHT_COLOR — цвет с интенсивностью (in, vec3).
  • ATTENUATION — затухание (in, float).
  • DIFFUSE_LIGHT, SPECULAR_LIGHT — итоговые (out, vec3).
  • NORMAL, VIEW — для расчётов.

Hints для uniforms

// Источник: цветовая текстура (применит inverse gamma — sRGB → linear автоматически)
uniform sampler2D albedo : source_color, filter_linear_mipmap, repeat_enable;

// Источник: normal map (распакуется правильно)
uniform sampler2D normal_map : hint_normal;

// Range slider в Inspector
uniform float intensity : hint_range(0.0, 5.0, 0.01) = 1.0;

// Color picker
uniform vec4 emission_color : source_color = vec4(1.0, 0.5, 0.0, 1.0);

// Cubemap для skybox / reflection
uniform samplerCube environment : source_color, filter_linear_mipmap;

Главные hints:

  • source_color — для color-текстур (sRGB-преобразование).
  • hint_normal — для normal-map.
  • hint_roughness_r / _g / _b / _gray — channel pack для PBR.
  • hint_range(min, max, step) — slider.
  • filter_* — фильтрация: nearest / linear / linear_mipmap / linear_mipmap_anisotropic.
  • repeat_* — wrap: enable / disable.

Пример: dissolve (распад)

Классический эффект “объект растворяется в шумовой узор”:

shader_type spatial;

uniform sampler2D albedo : source_color, filter_linear_mipmap;
uniform sampler2D noise : filter_linear, repeat_enable;
uniform float threshold : hint_range(0.0, 1.0) = 0.0;
uniform vec3 edge_color : source_color = vec3(1.0, 0.5, 0.0);
uniform float edge_width : hint_range(0.0, 0.1) = 0.03;

void fragment() {
    vec4 tex = texture(albedo, UV);
    float n = texture(noise, UV).r;

    if (n < threshold) {
        discard;  // отбросить этот пиксель
    }

    ALBEDO = tex.rgb;

    if (n < threshold + edge_width) {
        EMISSION = edge_color * 3.0;
        ALBEDO = edge_color;
    }
}

Использование:

var mat = $Mesh.material_override as ShaderMaterial
var tween = create_tween()
tween.tween_property(mat, "shader_parameter/threshold", 1.0, 2.0)
tween.tween_callback(queue_free)

Sub-pass: Cull back и transparency

Прозрачные объекты часто требуют ручной настройки:

shader_type spatial;
render_mode blend_mix, depth_draw_always, cull_disabled;

uniform vec4 color : source_color = vec4(1.0, 0.5, 0.5, 0.4);

void fragment() {
    ALBEDO = color.rgb;
    ALPHA = color.a;
}
  • blend_mix — стандартный alpha-blending.
  • depth_draw_always — писать в Z-буфер всегда (для правильных пересечений).
  • cull_disabled — рисовать обе стороны (для стекла, листвы).

Свой light() — toon shading

shader_type spatial;
render_mode unshaded;  // полностью отключим built-in lighting

uniform sampler2D albedo : source_color, filter_linear_mipmap;
uniform float bands : hint_range(2, 8, 1) = 3;

varying vec3 world_normal;

void vertex() {
    world_normal = (MODEL_MATRIX * vec4(NORMAL, 0.0)).xyz;
}

void light() {
    float diffuse = dot(normalize(world_normal), normalize(LIGHT));
    float quantized = floor(diffuse * bands) / bands;
    DIFFUSE_LIGHT += LIGHT_COLOR * max(quantized, 0.0);
}

void fragment() {
    ALBEDO = texture(albedo, UV).rgb;
}

render_mode unshaded отключает встроенное PBR-освещение, мы пишем своё в light().

Performance: ветвления в шейдере

if (n < threshold) { discard; } — это разветвление в fragment-шейдере. Стоит дёшево, но массивные ветвления убивают производительность. Чаще лучше smoothstep или маска через step() — без if’ов.

Particle shader

Для частиц shader_type particles; со своей семантикой:

shader_type particles;
render_mode keep_data;

uniform vec3 attract_center;
uniform float attract_strength : hint_range(0.0, 10.0) = 1.0;

void process() {
    vec3 to_center = attract_center - TRANSFORM[3].xyz;
    float dist = length(to_center);
    VELOCITY += normalize(to_center) * attract_strength * DELTA;
    TRANSFORM[3].xyz += VELOCITY * DELTA;
}

Применяется как process_material на GPUParticles3D (заменяет ParticleProcessMaterial).

Что не делать в шейдерах

  1. Тяжёлые вычисления в fragment(). Каждый пиксель вызывает fragment. Один лишний texture-sampling на 1080p — это 2 миллиона sample’ов в кадр.
  2. Большие циклы. GPU плохо переносит расходящиеся ветвления. Цикл for (int i = 0; i < 100; i++) — допустимо, но for (int i = 0; i < N; i++) if (cond[i]) ... — нет.
  3. Pixel-perfect логика в fragment. Если можно сделать в vertex (раз в N меньше вызовов) — делайте там.
  4. dynamic texture sampling через переменный индекс — GPU не любит. Лучше один atlas, чем массив текстур.

Что почитать

  • godotshaders.com — community-сайт с готовыми шейдерами.
  • Документация — Shading reference в Godot Docs (docs.godotengine.org/en/stable/tutorials/shaders/).
  • Исходники встроенных шейдеров — в Godot-репозитории servers/rendering/renderer_rd/shaders/.

В следующей главе — глоссарий.