Материалы и свет
Что уже должно быть понятно:
- нормали, направленный свет, ambient
- instancing, camera, depth buffer
- текстуры, UV-координаты, сэмплеры
Что появится в этой главе:
- несколько источников света: массив в uniform-буфере
- diffuse texture вместо плоского цвета
- объединение light uniform, текстуры и сэмплера в один bind group
- аддитивное смешивание: вклад каждого источника складывается
Итог: сетка 3×3×3 кубов, освещённая тремя источниками разного цвета, с текстурой вместо плоского серого
В прошлой главе мы добавили один направленный свет. Но в реальных сценах несколько источников — солнце, небо, отражения. В этой главе мы добавим три источника с разными цветами и заменим плоский цвет на diffuse texture.
Несколько источников света
Источники хранятся в массиве фиксированной длины внутри uniform-буфера:
#[derive(ShaderType, Clone, Copy)]
struct Light {
direction: Vec3,
color: Vec3,
}
#[derive(ShaderType)]
struct LightUniforms {
lights: [Light; 3],
ambient: f32,
}Vec3 из glam реализует encase::ShaderType, поэтому Light тоже можно поместить в uniform. Массив [Light; 3] — фиксированный размер. WGSL не поддерживает динамические массивы в uniform, поэтому количество источников зашито на этапе компиляции.
Структура в WGSL
struct Light {
direction: vec3<f32>,
color: vec3<f32>,
};
struct LightUniforms {
lights: array<Light, 3>,
ambient: f32,
};Выравнивание массива структур
WGSL требует, чтобы каждый элемент массива был выровнен по размеру структуры, округлённому до ближайшего кратного 16 байт. Light содержит два vec3<f32>:
struct Light { размер смещение
direction: vec3<f32> 12 байт 0
// паддинг 4 байта 12
color: vec3<f32> 12 байт 16
// паддинг 4 байта 28
}; 32 байта totalvec3<f32> занимает 12 байт, но выравнивание vec3 — 16 байт (как у vec4). Поэтому после direction идёт 4 байта паддинга, и color начинается со смещения 16. Итоговый размер Light — 32 байта вместо ожидаемых 24.
LightUniforms целиком:
lights[0] 32 байта смещение 0
lights[1] 32 байта смещение 32
lights[2] 32 байта смещение 64
ambient 4 байта смещение 96
// паддинг 12 байт смещение 100
всего 112 байтencase автоматически вычисляет размер и паддинг, поэтому Rust-структура с #[derive(ShaderType)] совпадает с WGSL-представлением. Без encase пришлось бы вручную добавлять _padding: f32 поля.
WARNING
Если бы мы описали Light как struct Light { dir: vec4<f32>, col: vec4<f32> }, размер тоже был бы 32 байта — но без паддинга, с явным хранением w-компоненты. Оба подхода валидны, но vec3 + паддинг чище семантически.
Суммирование вклада источников
Фрагментный шейдер проходит по всем источникам в цикле:
var total = vec3<f32>(0.0);
for (var i = 0u; i < 3u; i++) {
let light_dir = normalize(-light.lights[i].direction);
let diffuse = max(dot(normal, light_dir), 0.0);
total += light.lights[i].color * diffuse * tex_color.rgb;
}
let ambient = light.ambient * tex_color.rgb;
return vec4<f32>(ambient + total, 1.0);Каждый источник даёт свой вклад. Это аддитивное смешивание — яркости складываются. Если три источника с цветами (1, 0, 0), (0, 1, 0), (0, 0, 1) осветят одну грань, результат будет близок к белому.
Если сумма каналов превышает 1.0 — значение обрезается (clamp) до 1.0 при записи в framebuffer. Это означает потерю информации: текстура под тремя яркими источниками будет выглядеть белой, независимо от исходного цвета. В главе про HDR мы научимся работать с яркостями > 1.0 через tone mapping.
Ambient добавляется один раз — он моделирует рассеянный свет, который не принадлежит конкретному источнику.
Diffuse texture
Вместо плоского vec3<f32>(0.85, 0.85, 0.85) используем текстуру:
let tex_color = textureSample(diffuse_tex, diffuse_sampler, input.uv);Цвет из текстуры умножается на вклад каждого источника. Это называется diffuse map — текстура, определяющая базовый цвет поверхности. Каждая точка поверхности отражает свет пропорционально своему цвету: красная стена под белым светом выглядит красной, под синим — почти чёрной.
Для этого вершины теперь хранят UV-координаты:
struct Vertex {
position: [f32; 3],
normal: [f32; 3],
uv: [f32; 2],
}Кубу по-прежнему нужны 24 вершины — у каждой грани своя нормаль и свой набор UV.
Шахматная текстура генерируется процедурно — массив пикселей в памяти. Фильтрация Nearest подходит для чётких клеток. Для фотографий или плавных градиентов Linear дал бы более гладкий результат.
Объединение в один bind group
В прошлой главе light uniform был в отдельном bind group. Теперь добавились текстура и сэмплер — все три ресурса используются только фрагментным шейдером, поэтому логично поместить их в один bind group:
let light_bgl = ctx.device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[
// binding 0: Light uniform
// binding 1: Diffuse texture
// binding 2: Diffuse sampler
],
});Все три ресурса используются только во фрагментном шейдере — visibility: ShaderStages::FRAGMENT.
Bind group 0 (camera) остаётся отдельным — он используется в вершинном шейдере и обновляется каждый кадр. Разделение по частоте обновления: camera меняется при движении, light и текстура — константы.
Три источника с разными цветами
lights: [
Light { direction: Vec3::new(-0.5, -1.0, -0.3), color: Vec3::new(1.0, 0.95, 0.85) },
Light { direction: Vec3::new(0.7, -0.3, 0.5), color: Vec3::new(0.3, 0.5, 1.0) },
Light { direction: Vec3::new(-0.2, -0.5, 0.8), color: Vec3::new(0.9, 0.3, 0.3) },
],Первый — тёплый белый (солнечный), второй — голубой, третий — красный. Разные направления создают цветовые переходы на гранях куба: грань, повёрнутая к голубому источнику, получит голубой оттенок, к красному — красноватый.
Что получилось
Типичные ошибки
vec3<f32>в WGSL struct занимает 16 байт (с паддингом), не 12 — если не учесть, буфер не совпадёт- Сумма цветов > 1.0 обрезается до 1.0 — потеря информации при ярких источниках
- Хотя разные binding могут иметь разную
visibility, обычно ресурсы, используемые одним шейдером, объединяют в один bind group с одинаковойvisibility— это устоявшаяся практика
27 кубов (3×3×3) с шахматной текстурой. Три источника света создают разноцветные блики и тени на гранях. Камера перемещается как обычно.
Попробуем
- Изменить цвета источников — посмотреть, как смешиваются
- Увеличить массив до
[Light; 5](и в WGSLarray<Light, 5>) — добавить больше источников - Убрать один источник (color =
Vec3::ZERO) — увидеть разницу - Заменить текстуру на однотонную — сравнить с результатом прошлой главы