2.5D Renderer

Stage 1: Отрисовка стен

Отсечение: полярные координаты и интерполяция

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


  function render25d(
    ctx: CanvasRenderingContext2D,
    settings: Settings,
  ) {
    const camera = settings.camera;

    for (const linedef of settings.level.linedefs) {
      const projection = projectLinedef(camera, linedef);

      if (!projection) {
        continue;
      }
      
      drawPolygon(ctx, projectionToPoints(camera, projection));
    }
  }

Теперь поставим задачу по-другому: необходимо отрисовывать только то, что попадает в область видимости.

Может быть несколько вариантов того, как отрезок связан с областью видимости.

Отрезок полностью в FOV

Отрезок вне FOV

Отрезок пересекает один край FOV

Отрезок пересекает оба края FOV

Обозначения на схемах:

Красная точка — камера.

Красные пунктирные линии — границы FOV.

Красная дуга — весь угловой диапазон видимости.

Чёрная линия — сам отрезок.

Оранжевая дуга — угловой диапазон отрезка относительно камеры.

Зелёная дуга — часть углового диапазона отрезка, которая попадает в FOV.

Серая дуга — вспомогательный ориентир для сравнения углов.

Теперь перейдём к полярным координатам и будем рассматривать область видимости и отрезок как диапазоны значений в полярных координатах, то есть как диапазоны углов. Это позволит нам учесть все вышеописанные ситуации. Здесь мы можем определить функцию, которая возвращает те самые диапазоны углов, если отрезок попадает в область видимости.

К сожалению, у меня получилась большая реализация, поэтому для изучения функции обратитесь к её исходному коду.

Если отрезок пересекает край области видимости, то используем диапазоны углов и рассчитываем, какую часть диапазона углов для отрезка занимает точка пересечения с областью видимости. Получаем коэффициент от 0 до 1. Этот коэффициент мы будем использовать, чтобы домножать высоту отрезка. Как именно — смотрите в коде ниже или читайте исходный код шага.

2.5D Renderer

2D Renderer

Управление камерой WASD

Немного кода

Добавим несколько вспомогательных функций, основная из которых позволяет получить значения в полярных координатах, а именно нормализованное значение углов для области видимости и для спроецированного отрезка:


  interface IntersectionAngles {
    linedefFrom: number;
    linedefTo: number;
    cameraFrom: number;
    cameraTo: number;
  }

  function calculateIntersectionAngles(linedef: Linedef, camera: Camera): null | IntersectionAngles {
    // ..
  }

Рассчитаем процент видимой части диапазона углов для проекции относительно всего диапазона и используем этот процент для вычисления высоты в точке пересечения:


  function projectLinedef(camera: Camera, linedef: Linedef) : LinedefProjection | null {
    const angles = calculateIntersectionAngles(linedef, camera);

    if (angles === null) {
      return null;
    }

    const relativeAngleStart = new Angle(angles.linedefFrom - camera.angle.degrees);

    const distanceStart = toDistance(linedef.start, camera) * Math.abs(relativeAngleStart.cos);
    const distanceEnd = toDistance(linedef.end, camera) * Math.abs(relativeAngleEnd.cos);

    const heightStart = WALL_HEIGHT / distanceStart;
    const heightEnd = WALL_HEIGHT / distanceEnd;

    const isLinedefStartHeigher = heightStart > heightEnd;
    const linedefMinHeight = Math.min(heightStart, heightEnd);
    const linedefDiffHeight = Math.abs(heightStart - heightEnd);
    const linedefAngleRange = angles.linedefTo - angles.linedefFrom;

    let start;

    if (angles.linedefFrom < angles.cameraFrom) {
      const percent = (angles.cameraFrom - angles.linedefFrom) / linedefAngleRange;
      const k = isLinedefStartHeigher ? (1 - percent) : percent;

      start = {
        screenX: 0,
        height: linedefMinHeight + linedefDiffHeight * k
      }
    } else {
      start = {
        screenX: toScreenX('linedefFrom', angles, camera),
        height: heightStart
      }
    }

    // ..

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