2.5D Renderer

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

Это дешевый в реализации, но заметный эффект. Сектора начинают читаться как разные зоны уровня, даже если используют один и тот же набор текстур.

Реализация шага на github