Stage 6: Прочее

Спрайт предмета

2.5D Renderer

2D Renderer

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

На предыдущем шаге мы уже научились находить прямоугольную область, в которой должен появиться предмет: экранная X-координата берется из угла до предмета, а нижняя граница привязана к высоте пола сектора. Теперь вместо одноцветной заливки растянем внутри этой области простую bitmap-текстуру.

Для аптечки используем textures.health_sprite. Это маленькая сетка 8 на 8. Нулевой цвет прозрачный, единичный цвет красный:

  health_sprite: {
    scale: 1,
    width: 8,
    height: 8,
    bitmap: [
      [0, 0, 0, 1, 1, 0, 0, 0],
      [0, 0, 0, 1, 1, 0, 0, 0],
      [0, 0, 0, 1, 1, 0, 0, 0],
      [1, 1, 1, 1, 1, 1, 1, 1],
      [1, 1, 1, 1, 1, 1, 1, 1],
      [0, 0, 0, 1, 1, 0, 0, 0],
      [0, 0, 0, 1, 1, 0, 0, 0],
      [0, 0, 0, 1, 1, 0, 0, 0],
    ],
    colors: [
      { r: 0, g: 0, b: 0, a: 0 },
      { r: 255, g: 80, b: 80, a: 255 },
    ],
  }

item.radius остается размером в мире, тем же радиусом, которым предмет отмечен на карте. Поэтому сначала переводим этот радиус в угловой размер черезMath.atan2(radius, distance), а затем в экранные пиксели. Если предмет занимает почти ширину коридора в мире, близко к камере он тоже будет занимать большую область на экране. Даже когда камера оказывается внутри радиуса предмета, дистанция для масштаба все равно считается до центра окружности: так спрайт не переключается в отдельный режим и плавно растет по мере приближения к центру. Высоту не берем из сектора: она должна сохранять пропорции самой текстуры. Для квадратной аптечки 8 на 8 высота будет равна ширине, а для вытянутого спрайта изменится по соотношениюtexture.height / texture.width.

  const texture = textures.health_sprite;
  const itemAngularRadius = Math.atan2(item.radius, distance) * 180 / Math.PI;
  const screenHalfWidth =
    (itemAngularRadius / camera.fov.degrees) * camera.screen.width;

  const ceilZ = sector.ceilHeight ?? floorZ;
  const screenBottomY = screenCenterY - (floorZ - camera.z) * projectionScale;
  const screenCeilY = screenCenterY - (ceilZ - camera.z) * projectionScale;

  const textureRect =
    scaleTextureToWidth(texture, screenX, screenBottomY, screenHalfWidth * 2);
  const itemBounds = {
    x1: textureRect.x1,
    x2: textureRect.x2,
    y1: Math.max(textureRect.y1, Math.ceil(screenCeilY)),
    y2: textureRect.y2,
  };

  const visibleBounds = fitTextureIntoBounds(itemBounds, screenBounds);

При рисовании каждого пикселя внутри полученного прямоугольника переводим его локальную позицию в координаты текстуры. Если texel прозрачный, ничего не пишем в буфер. Если texel видимый, затемняем его яркостью сектора и рисуем. Голубая рамка показывает bounding этой текстуры: bitmap прижимается к левому и правому краю внутренней области, а высота вычисляется из пропорции texture.height / texture.width. Поэтому спрайт не растягивается до верхней границы, если его собственные пропорции этого не требуют. Если raw-bounding спрайта оказывается шире или выше canvas, его размер не сжимается до экрана. Цикл рисования обрезается одновременно по экрану и по item bounding, но координаты текстуры продолжают считаться от исходных raw-границ, поэтому видимая часть не растягивается. Верхняя граница дополнительно обрезается проекцией потолка текущего сектора только в видимой области рисования, так что полная высота спроецированного item не уменьшается, но спрайт не может залезть вышеceilHeight.

  const textureInnerRect = {
    x1: textureRect.x1 + 1,
    x2: textureRect.x2 - 1,
    y1: textureRect.y1 + 1,
    y2: textureRect.y2 - 1,
  };
  const itemInnerBounds = {
    x1: itemBounds.x1 + 1,
    x2: itemBounds.x2 - 1,
    y1: itemBounds.y1 + 1,
    y2: itemBounds.y2 - 1,
  };

  const drawBounds = fitTextureIntoBounds(textureInnerRect, itemInnerBounds);
  if (!drawBounds) return;

  const visibleDrawBounds = fitTextureIntoBounds(drawBounds, screenBounds);
  if (!visibleDrawBounds) return;

  const textureWidth = textureInnerRect.x2 - textureInnerRect.x1;
  const textureHeight = textureInnerRect.y2 - textureInnerRect.y1;

  const tx =
    Math.floor(((x - textureInnerRect.x1) / textureWidth) * texture.width);
  const ty =
    Math.floor(((y - textureInnerRect.y1) / textureHeight) * texture.height);
  const color = getTextureColor(texture, tx, ty);

  if (color.a === 0) {
    continue;
  }

  drawPixel(buffer, x, y, applyBrightness(color, projected.sector.brightness));

Пока все предметы рисуются одной текстурой аптечки. Дальше можно будет выбирать текстуру поitem.type или хранить имя спрайта прямо вItem.

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