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
}
}
// ..