Stage 3: Binary Space Partition
Порталы 2D: соединяем сектора
На прошлом шаге порталы работали только в простом случае: соседние секторы имели одинаковую высоту пола и потолка, поэтому общий сегмент можно было просто не рисовать. Теперь усложним задачу. Пусть портал может вести в сектор с отличающейся высотой и вокруг проема должны появиться видимые части пола, потолка или нижней/верхней стенки.
Одного диапазона по X для этого уже недостаточно. Диапазон говорит только “эта колонка закрыта полностью”, но портал закрывает колонку не целиком: часть колонки может быть занята потолком текущего сектора, часть — полом, а середина остается открытой для дальнего сектора. Поэтому добавим два массива размером с ширину экрана: upperClip и lowerClip.
Для каждой экранной колонки эти массивы хранят верхнюю и нижнюю границы еще видимой области. Непрозрачная стена заполняет весь доступный интервал и сдвигает обе границы навстречу друг другу. Портал работает мягче: он рисует только закрытые части вокруг проема и сужает окно, через которое следующая геометрия сможет быть видна.
2.5D Renderer
2D Renderer
Управление камерой WASD
В результате BSP по-прежнему отвечает за порядок от ближнего к дальнему, wallRanges отбрасывает полностью закрытые колонки, а upperClip и lowerClip ограничивают видимую высоту внутри каждой колонки. Эти три механизма вместе позволяют корректно рисовать цепочку секторов через порталы.
Немного кода
BSP-обход остается прежним, но обычные и портальные сегменты теперь обрабатываются разными функциями. Обычная стена закрывает доступный интервал колонки, а портал рисует только верхнюю и нижнюю закрытые части, оставляя середину для следующего сектора.
function render25d(
ctx: CanvasRenderingContext2D,
settings: Settings,
) {
const camera = settings.camera;
const allSegments = settings.level.linedefs;
const bspTree = buildBSPTree(allSegments);
const wallRanges = createSolidWallRanges(camera);
const upperClip = new Array(camera.screen.width).fill(-1);
const lowerClip = new Array(camera.screen.width).fill(camera.screen.height);
traverseBSPTree(bspTree, camera, (bspNode: BSPLeaf) => {
for (const seg of bspNode.segs) {
const sector = seg.frontSector!;
const projection = projectSeg(camera, sector, seg);
if (!projection) {
continue;
}
if (isPortal(seg)) {
drawPortalSegment(ctx, camera, seg, projection, wallRanges, upperClip, lowerClip);
} else {
drawSolidSegment(ctx, camera, seg, projection, wallRanges, upperClip, lowerClip);
}
}
});
}
Теперь посмотрим на функцию drawPortalSegment.
Она получает тот же набор ограничителей, что и обычная стена, но не закрывает колонку полностью. Портал должен
оставить “окно” для соседнего сектора, поэтому функция рисует только те части текущего сектора, которые находятся
выше и ниже проема.
function drawPortalSegment(
ctx: CanvasRenderingContext2D,
camera: Camera,
seg: Seg,
projection: SegProjection,
solidWallRanges: SolidSegmentRange[],
upperClip: number[],
lowerClip: number[],
) {
const frontSector = seg.frontSector!;
const backSector = seg.backSector!;
const cameraSide = getPointSide(seg, { x: camera.x, y: camera.y });
const currentSector = cameraSide >= 0 ? frontSector : backSector;
for (let x = xFrom; x <= xTo; x++) {
if (!isWallVisible(x, solidWallRanges)) {
continue;
}
const t = (x - xStart) / (xEnd - xStart);
const top = startTop + (endTop - startTop) * t;
const bottom = startBottom + (endBottom - startBottom) * t;
const drawTop = Math.max(upperClip[x], top);
const drawBottom = Math.min(lowerClip[x], bottom);
if (drawTop > upperClip[x]) {
drawVerticalLine(ctx, x, upperClip[x], drawTop, currentSector.ceilColor!);
upperClip[x] = drawTop;
}
if (drawBottom < lowerClip[x]) {
drawVerticalLine(ctx, x, drawBottom, lowerClip[x], currentSector.floorColor!);
lowerClip[x] = drawBottom;
}
}
}
Сначала функция определяет, с какой стороны портала находится камера. Это нужно, чтобы выбрать текущий сектор:
именно его потолок и пол должны закрывать верхнюю и нижнюю часть колонки вокруг проема. Затем для каждой экранной
колонки интерполируются top
и bottom — верхняя и нижняя
границы портального seg на экране.
Если между старым upperClip[x]
и новым верхом портала есть место, рисуем там потолок текущего сектора и сдвигаем верхнюю границу вниз. Если между
низом портала и lowerClip[x]
есть место, рисуем пол и сдвигаем нижнюю границу вверх.