Stage 5: Текстурирование
Уровень освещения секторов
2.5D Renderer
2D Renderer
Управление камерой WASD и ZX
На предыдущих шагах мы научились выбирать цвет из текстуры стены, пола и потолка. Добавим самый простой вариант освещения - коэффициент яркости на уровне сектора. Это не источник света и не расчет теней. Мы просто говорим: весь сектор рисуется с множителем яркости. Значение
1.0 оставляет цвет без изменений, 0.8 немного затемняет, 0.2 делает сектор
почти темным.
Добавим уровень освещенности в описание сектора:
interface Sector {
// ...
brightness?: number;
}
Теперь каждому сектору можно задать свое значение. В демо ступени становятся все темнее: геометрия та же, текстуры те же, но итоговый цвет пикселей постепенно уменьшается.
const stepSector1: Sector = {
// ..
brightness: 1.0,
};
const stepSector2: Sector = {
// ..
brightness: 0.8,
};
const stepSector3: Sector = {
// ..
brightness: 0.6,
};
Опишем очень простую функцию применения освещенности. Она получает уже найденный цвет texel и умножает каждый
RGB-канал на brightness. Если яркость не задана или равна 1, цвет остается как есть.
function applyBrightness(color: Color, brightness: number = 1): Color {
if (brightness >= 1.0) {
return color;
}
return {
r: Math.min(255, Math.floor(color.r * brightness)),
g: Math.min(255, Math.floor(color.g * brightness)),
b: Math.min(255, Math.floor(color.b * brightness))
};
}
Это удобно делать в самом конце выборки цвета. Рендер сначала работает как раньше: проецирует стену, находит
texX/texY, получает цвет из bitmap. И только перед записью в буфер пропускает цвет через
applyBrightness.
if (wallTexture) {
const texture = textures[wallTexture];
const texX = Math.floor(tx * texture.width);
for (let y = drawTop; y < drawBottom; y++) {
const v = (y - top) / (bottom - top);
const texY = Math.floor(v * texture.height) % texture.height;
const color = getTextureColor(texture, texX, texY);
drawPixel(buffer, x, y, applyBrightness(color, sector.brightness));
}
}
Для пола и потолка принцип такой же. Функция drawTexturedFloorCeil восстанавливает мировую точку,
выбирает texel и затемняет его яркостью текущего сектора. В порталах важно брать яркость той части, которую
рисуем: обычная поверхность использует sector.brightness, а верхняя или нижняя стенка соседнего
сектора - otherSector.brightness.
Поэтому достаточно обернуть рассчитанные значения цвета пикселей в applyBrightness в местах, где
текстура превращается в пиксель:
function drawTexturedFloorCeil(...) {
// ..
drawPixel(buffer, x, y, applyBrightness(color, sector.brightness));
// ..
}
function drawSolidSegment(...) {
// ..
drawPixel(buffer, x, y, applyBrightness(color, sector.brightness));
// ..
}
function drawPortalSegment(...) {
// ..
drawPixel(buffer, x, y, applyBrightness(color, otherSector.brightness));
// ..
}
Важно, что такой подход не меняет BSP, порталы, отсечения или перспективное текстурирование. Освещение становится последним шагом в пиксельном конвейере: сначала мы решаем, что именно видно и какой цвет у текстуры в этой точке, а потом сектор слегка подкрашивает результат своей яркостью.
Это дешевый в реализации, но заметный эффект. Сектора начинают читаться как разные зоны уровня, даже если используют один и тот же набор текстур.