Stage 5: Текстурирование
Горизонтальное: пол и потолок
2.5D Renderer
2D Renderer
Управление камерой WASD и ZX
Со стенами все было относительно просто: мы уже знали экранную вертикальную полосу стены и могли растянуть по ней текстуру сверху вниз. Пол и потолок устроены иначе. Это горизонтальные плоскости, которые уходят в глубину, поэтому для них недостаточно взять одну вертикальную координату на экране и линейно растянуть bitmap.
Для пола и потолка сектору нужно хранить отдельные ID текстур. Высоты floorHeight и ceilHeightуже использовались при проекции стен, а теперь они еще говорят, на какой горизонтальной плоскости bitmap нужно искать точку пересечения луча.
const roomSector: Sector = {
// ..
floorHeight: 0,
floorTexture: 'floor',
ceilHeight: 400,
ceilTexture: 'ceil',
// ..
};Текстуры пола и потолка рисуются только там, где они действительно видны в текущем столбце. Если верх стены ниже текущего верхнего отсечения, между ними виден потолок. Если низ стены выше нижнего отсечения, между ними виден пол. Поэтому вызов текстурирования остается встроенным в уже существующую работу с upperClip иlowerClip.
if (drawTop > upperClip[x]) {
if (sector.ceilTexture) {
drawTexturedFloorCeil(buffer, camera, sector, x, upperClip[x], drawTop, false);
} else {
drawVerticalLine(buffer, x, upperClip[x], drawTop, ceilColor);
}
}
// ..
if (drawBottom < lowerClip[x]) {
if (sector.floorTexture) {
drawTexturedFloorCeil(buffer, camera, sector, x, drawBottom, lowerClip[x], true);
} else {
drawVerticalLine(buffer, x, drawBottom, lowerClip[x], floorColor);
}
}Вся отдельная логика собрана в drawTexturedFloorCeil. Один и тот же код подходит и для пола, и для потолка: флаг isFloor выбирает текстуру и сторону экрана. Для пола нужны пиксели ниже центра экрана, для потолка — выше центра.
function drawTexturedFloorCeil(
imageData: ImageData,
camera: Camera,
sector: Sector,
x: number,
yStart: number,
yEnd: number,
isFloor: boolean
): void {
const textureName = isFloor ? sector.floorTexture : sector.ceilTexture;
if (!textureName) return;
const texture = textures[textureName];
if (!texture) return;
// ..
}Основная идея: для каждого пикселя пола или потолка мы восстанавливаем, какой точке мира он соответствует.
const distToPlane = isFloor
? camera.z - sector.floorHeight
: sector.ceilHeight - camera.z;
const p = y - camera.screen.height / 2;
const rowDistance = distToPlane / Math.abs(p);
const planeLength = Math.tan(camera.fov.radians / 2);
const planeX = -camera.angle.sin * planeLength;
const planeY = camera.angle.cos * planeLength;
const t = x / camera.screen.width;
const rayDirX = camera.angle.cos + planeX * (2 * t - 1);
const rayDirY = camera.angle.sin + planeY * (2 * t - 1);
const worldX = camera.x + rayDirX * rowDistance;
const worldY = camera.y + rayDirY * rowDistance;
let texX = (worldX / texture.scale) % 1;
let texY = (worldY / texture.scale) % 1;Значение distToPlane — это расстояние от камеры до горизонтальной плоскости по вертикали. Для пола камера находится выше плоскости, поэтому берем camera.z - floorHeight. Для потолка камера находится ниже плоскости, поэтому берем ceilHeight - camera.z. Это не расстояние вперед по взгляду, а именно высотная разница: насколько нужно опуститься до пола или подняться до потолка.
Затем берем расстояние p от пикселя до центра экрана по вертикали. Чем ближе пиксель к центру, тем дальше находится точка на плоскости. Поэтому rowDistance считается через вертикальное расстояниеdistToPlane и наклон луча, который в этой упрощенной формуле выражен черезMath.abs(p).
Значение planeX и planeYнужны, чтобы получить не только центральный луч камеры, но и луч для любого столбца экрана. Направление камеры задается вектором camera.angle.cos/camera.angle.sin. А экранная плоскость идет поперек взгляда, поэтому берем перпендикуляр к этому направлению:(-sin, cos). Длина этого бокового вектора равна tan(FOV / 2): так левый и правый края плоскости совпадают с границами угла обзора.
Затем по экранной координате x строим направление луча: слева луч чуть повернут к левому краю FOV, справа — к правому. Умножаем это направление на rowDistance и получаем мировые координатыworldX/worldY. Эти координаты уже можно превратить в координаты текстуры.
Двойка в выражении 2 * t - 1появляется из-за перевода координаты столбца из диапазона 0..1в диапазон -1..1. При t = 0 получаем левый край экранной плоскости, при t = 0.5 — центр, а приt = 1 — правый край. Поэтому planeX/planeY добавляется с отрицательным, нулевым или положительным весом.
Значение texture.scale задает размер повторения текстуры в мире. После деления на scale берется дробная часть: так один и тот же bitmap повторяется по всей плоскости. Отрицательные координаты дополнительно приводятся к диапазону от 0 до 1.
В итоге получается, что стены и горизонтальные поверхности используют одни и те же текстуры, но семплируются по-разному.