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, порталы, отсечения или перспективное текстурирование. Освещение становится последним шагом в пиксельном конвейере: сначала мы решаем, что именно видно и какой цвет у текстуры в этой точке, а потом сектор слегка подкрашивает результат своей яркостью.
Это дешевый в реализации, но заметный эффект. Сектора начинают читаться как разные зоны уровня, даже если используют один и тот же набор текстур.