Чтобы узнать что-то новое, я пишу 3D-игру с лучевыми лучами в Javascript.
Я уже изучил превосходное руководство Javidx9 по YT; https://www.youtube.com/watch?v=xW8skO7MFYw&t=16s
А также;
Учебное пособие по компьютерной графике Лоде https://lodev.org/cgtutor/raycasting.html
Учебник по созданию лучей для разработки игр И другие цели Ф. Пермади https://permadi.com/1996/05/ray-casting-tutorial-table-of-contents/
Эти уроки имеют один аспект - все их карты основаны на сетке. Это облегчает текстурирование стен: я sh.
Чтобы сделать еще один шаг вперед go, мой raycaster вместо этого работает с полигональной картой секторов - поэтому мое тестирование лучей / стен выполняется с помощью линии алгоритм пересечения, который я украл из 'net.
. Это делает его достаточно отличным от приведенных выше руководств, так что теперь я не уверен, как go насчет добавления текстур, в частности; получая U из UV-координат, которые мне нужно определить для точки, которую я ударил по краю (стене).
Я использую Canvas drawImage, так как он работает быстрее, чем рисование объектов попиксельно и может сделайте необходимое масштабирование для меня. Это делает «верхнюю» V-координату очень легкой - она всегда равна 0. У меня теперь горизонтальные полосатые текстурированные стены: D
Что касается U-координаты; Я думаю, что я должен преобразовать свою точку пересечения со стеной из мирового пространства в «пространство стены», т.е. мне нужно знать, как далеко вдоль стены я ударил (середина была бы 0,5). Это даст мне U, да?
Я вполне уверен, что скоординированная нормализация к стене будет хорошей отправной точкой. Я, вероятно, в конечном итоге получу растянутые текстуры, но думаю, что справлюсь с некоторым разделением по ширине стены.
Вот класс и метод __RenderScreenColumn, который отображает один столбец экрана. Он вызывается для каждого столбца экрана. Там есть дополнительные вещи для обработки рендеринга соседних секторов карты, но я думаю, что это не имеет отношения к этой проблеме.
Мне кажется, что эта проблема может быть решена с помощью матричной проекции, как в «реальном» 3D движок. Однако они мне пока не нужны (я вращаюсь только в 2D; это 2D-движок), поэтому предпочел бы избегать их, если это возможно.
import Canvas from "../Engine/Canvas.js";
import V2 from "../Engine/V2.js";
import Polygon from "../Engine/Polygon.js";
export default class GameView3D {
OnNewGame(_game) {
this.__game = _game;
this.__ctx = Canvas.Create(this.__game.camera.screen);
// The 'unit' height of walls.
this.__wallScale = this.__game.camera.screen[1];
}
OnUpdate(_deltaTime, _fps) {
// Don't draw if the game is not ready or the player is out of map.
if(!this.__game || this.__game.localPlayer.sectorIndex < 0) return;
this.__ctx.fillStyle = "#000";
this.__ctx.fillRect(0, 0, this.__game.camera.screen[0], this.__game.camera.screen[1]);
// Draw map.
for(let index = 0; index < this.__game.camera.screen[0]; index += this.__game.config.castingResolution[0])
this.__RenderScreenColumn(index, this.__game.localPlayer.sectorIndex, 0);
}
/*
Draw a sector, screen-column-by-screen-column.
@param {int} _column The screen-column we're rendering.
@param {int} _sectorIndex The index into map sectors, identifying the sector we're casting from. Initially this will be the camera's sector.
@param {float} _rayLength The length of the ray from the camera at which to start casting. Initially 0, this will be incremented as we iterate through the ray.
@param {[_renderedSectorIndices]} This is passed recursively by the method in order to track which sectors do not need to be re-drawn at this column.
*/
__RenderScreenColumn(_column, _sectorIndex, _rayLength, _renderedSectorIndices = []) {
// Keep a frame-by-frame record of which sectors we've drawn this column; so we don't try to draw them more than once.
_renderedSectorIndices.push(_sectorIndex);
let sector = this.__game.map.sectors[_sectorIndex],
// We 're flipping the X (mirroring the output) otherwise it does not match the top down view.
x = this.__game.camera.screen[0] - _column,
// Create a ray from the camera pointing in to the scene.
rayAngle = (this.__game.camera.transform.rotation - this.__game.camera.fovHalf) + (_column / this.__game.camera.screen[0]) * this.__game.camera.fov,
rayDirection = [ Math.sin(rayAngle), Math.cos(rayAngle) ],
rayLength = _rayLength;
// Scan forward along the ray.
while(rayLength < this.__game.camera.depth) {
let lastTestPoint = V2.Add(this.__game.camera.transform.position, V2.Mul(rayDirection, rayLength));
rayLength += this.__game.config.castingResolution[1];
// The current test point of the ray.
let testPoint = V2.Add(this.__game.camera.transform.position, V2.Mul(rayDirection, rayLength)),
// TODO!
fisheyeCorrectedLength = rayLength;// * rayDirection[0];
// TODO; Have we hit a floor?
// TODO; Have we hit a sprite?
// At some point we should record the range of pixel rows we've already drawn this column.
// Then when drawing things behind things, we dont draw pixels in those already-drawn ranges.
// Have we hit a wall?
// As we test, we'll hit or more likely cross a wall.
// This will store the exact intersection between our casting ray and the wall. Or null if there wasn't an intersection.
let intersectionPoint = null,
// If an intersection was found, this is the index of the edge it was found on.
edgeIndex = null;
// Find the intersection point of where we're looking and one of the edges in our sector.
// If we find an intersection, that's the edge/wall we're looking at.
for(edgeIndex = 0; edgeIndex < sector.edges.length; edgeIndex++) {
// Test intersection point between wall and a line from camera to the test point.
intersectionPoint = Polygon.LineIntersect([ lastTestPoint, testPoint ], sector.edges[edgeIndex]);
if(intersectionPoint) break;
}
// No wall, continue to next depth.
if(!intersectionPoint) continue;
// We've hit a wall.
// However, if it is a portal, draw the sector of which it views instead of the wall. It's not really there :D
let neighbourIndex = sector.portalIndices[edgeIndex];
if(neighbourIndex > -1) {
// NB. At some point this is about where we might add in ceiling height?
// If we've already drawn this neighbour sector, break so we don't draw anything else this column.
if(_renderedSectorIndices.indexOf(neighbourIndex) > -1) break;
// Draw the neighbour sector's column through the portal, then break.
this.__RenderScreenColumn(_column, neighbourIndex, rayLength, _renderedSectorIndices);
// NB. At some point this is about where we might add in floor height?
break;
}
// The edge with which we've tested against.
let edge = sector.edges[edgeIndex],
// The height of the edge, adjusted for perspective due to distance.
wallHeight = this.__wallScale / fisheyeCorrectedLength,
wallHeightHalf = wallHeight / 2,
// The top and bottom positions of the wall.
wallTop = this.__game.camera.screenHalf[1] - wallHeightHalf,
wallBottom = wallTop + wallHeight,
// The top and bottom positions of the wall, clipped to the screen.
wallClipTop = wallTop < 0 ? 0 : wallTop,
wallClipBottom = wallBottom > this.__game.camera.screen[1] ? this.__game.camera.screen[1] : wallBottom,
wallClipHeight = wallClipBottom - wallClipTop;
// Draw the wall.
this.__ctx.fillStyle = "#ccc";
this.__ctx.fillRect(x, wallClipTop, 1, wallClipHeight);
// Texturing.
// TODO; Store texure index in map file somewhere so we can pick different ones. For now, one texture for every wall.
let texture = this.__game.textures[0],
edgeLength = V2.Distance(edge[0], edge[1]),
// These should be normalized.
uv = [
0, // U: Need to figure out where on the edge we hit, 0 - 1?
0 // V: We're always going to draw from the top of the texture; drawImage will handle scaling for us! And seemingly cheaply too.
];
this.__ctx.drawImage(
texture.image, // image
uv[0] * texture.size[0], // sourceX
uv[1] * texture.size[1], // sourceY
1, // sourceW
texture.size[1], // sourceH
x, // destX
wallClipTop, // destY
1, // destW
wallClipHeight // destH
);
// No need to draw anything else for this column.
// ... for now. Perhaps there will be ceiling/floor code here? Not sure yet.
break;
}
}
};
TLDR;
Как найти ультрафиолетовое излучение "плоскости", пораженной лучом в движке полигонального вещания?
Или
Учитывая две пересекающиеся линии с результирующей точкой пересечения, как мне "нормализовать" эту точку к одному из этих ребер, чтобы определить, как далеко вдоль этого ребра мы пересеклись?
Спасибо очень за ваше время и помощь.