【问题标题】:Raycaster displays phantom perpendicular wall facesRaycaster 显示幻象垂直壁面
【发布时间】:2015-05-16 17:45:12
【问题描述】:

输出如下:

您应该只看到一面平坦、连续的红墙,另一面是蓝墙,另一面是绿色,另一面是黄色(参见地图的定义,testMapTiles,它只是一张有四面墙的地图)。然而,有这些高度不同的幻像墙壁面,它们垂直于真实的墙壁。为什么?

请注意,白色的“间隙”实际上并不是间隙:它试图绘制高度为Infinity(距离为 0)的墙。如果你特别考虑到它(这个版本的代码没有)并且只是将它限制在屏幕高度,那么你只会看到那里有一堵非常高的墙。

源代码如下。它是普通的 Haskell,使用 Haste 编译为 JavaScript 并渲染到画布。它基于来自 this tutorial 的 C++ 代码,但请注意,我将 mapXmapY 替换为 tileXtileY,并且我没有 ray 前缀 pos 和 @ 987654332@ 在主循环中。与 C++ 代码的任何差异都可能是破坏一切的原因,但在多次仔细研究此代码后,我似乎找不到任何差异。

有什么帮助吗?

import Data.Array.IArray
import Control.Arrow (first, second)

import Control.Monad (forM_)

import Haste
import Haste.Graphics.Canvas

data MapTile = Empty | RedWall | BlueWall | GreenWall | YellowWall deriving (Eq)

type TilemapArray = Array (Int, Int) MapTile

emptyTilemapArray :: (Int, Int) -> TilemapArray
emptyTilemapArray dim@(w, h) = listArray ((1, 1), dim) $ replicate (w * h) Empty

testMapTiles :: TilemapArray
testMapTiles =
    let arr = emptyTilemapArray (16, 16)
        myBounds@((xB, yB), (w, h)) = bounds arr
    in  listArray myBounds $ flip map (indices arr) (\(x, y) ->
            if x == xB then RedWall
            else if y == yB then BlueWall
            else if x == w then GreenWall
            else if y == h then YellowWall
            else Empty)

type Vec2 a = (a, a)
type DblVec2 = Vec2 Double
type IntVec2 = Vec2 Int

add :: (Num a) => Vec2 a -> Vec2 a -> Vec2 a
add (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)

mul :: (Num a) => Vec2 a -> a -> Vec2 a
mul (x, y) factor = (x * factor, y * factor)

rot :: (Floating a) => Vec2 a -> a -> Vec2 a
rot (x, y) angle =
    (x * (cos angle) - y * (sin angle), x * (sin angle) + y * (cos angle))

dbl :: Int -> Double
dbl = fromIntegral

-- fractional part of a float
-- `truncate` matches behaviour of C++'s int()
frac :: Double -> Double
frac d = d - dbl (truncate d)

-- get whole and fractional parts of a float
split :: Double -> (Int, Double)
split d = (truncate d, frac d)

-- stops 'Warning: Defaulting the following constraint(s) to type ‘Integer’'
square :: Double -> Double
square = (^ (2 :: Int))

-- raycasting algorithm based on code here:
-- http://lodev.org/cgtutor/raycasting.html#Untextured_Raycaster_

data HitSide = NorthSouth | EastWest deriving (Show)

-- direction, tile, distance
type HitInfo = (HitSide, IntVec2, Double)

-- pos: start position
-- dir: initial direction
-- plane: camera "plane" (a line, really, perpendicular to the direction)
traceRays :: TilemapArray -> Int -> DblVec2 -> DblVec2 -> DblVec2 -> [HitInfo]
traceRays arr numRays pos dir plane = 
    flip map [0..numRays] $ \x -> 
        let cameraX = 2 * ((dbl x) / (dbl numRays)) - 1
        in  traceRay arr pos $ dir `add` (plane `mul` cameraX)

traceRay :: TilemapArray -> DblVec2 -> DblVec2 -> HitInfo
traceRay arr pos@(posX, posY) dir@(dirX, dirY) =
    -- map tile we're in (whole part of position)
    -- position within map tile (fractional part of position)
    let ((tileX, fracX), (tileY, fracY)) = (split posX, split posY)
        tile = (tileX, tileY)
    -- length of ray from one x or y-side to next x or y-side
        deltaDistX = sqrt $ 1 + (square dirY / square dirX)
        deltaDistY = sqrt $ 1 + (square dirX / square dirY)
        deltaDist  = (deltaDistX, deltaDistY)
    -- direction of step
        stepX = if dirX < 0 then -1 else 1
        stepY = if dirY < 0 then -1 else 1
        step  = (stepX, stepY)
    -- length of ray from current position to next x or y-side
        sideDistX = deltaDistX * if dirX < 0 then fracX else 1 - fracX
        sideDistY = deltaDistY * if dirY < 0 then fracY else 1 - fracY
        sideDist  = (sideDistX, sideDistY)
        (hitSide, wallTile) = traceRayInner arr step deltaDist tile sideDist
    in  (hitSide, wallTile, calculateDistance hitSide pos dir wallTile step)

traceRayInner :: TilemapArray -> IntVec2 -> DblVec2 -> IntVec2 -> DblVec2 -> (HitSide, IntVec2)
traceRayInner arr step@(stepX, stepY) deltaDist@(deltaDistX, deltaDistY) tile sideDist@(sideDistX, sideDistY)
    -- a wall has been hit, report hit direction and coördinates
    | arr ! tile /= Empty   = (hitSide, tile)
    -- advance until a wall is hit
    | otherwise             = case hitSide of
        EastWest ->
            let newSideDist = first (deltaDistX+) sideDist
                newTile     = first (stepX+) tile
            in
                traceRayInner arr step deltaDist newTile newSideDist
        NorthSouth ->
            let newSideDist = second (deltaDistY+) sideDist
                newTile     = second (stepY+) tile
            in
                traceRayInner arr step deltaDist newTile newSideDist
    where
        hitSide = if sideDistX < sideDistY then EastWest else NorthSouth

-- calculate distance projected on camera direction
-- (an oblique distance would give a fisheye effect)
calculateDistance :: HitSide -> DblVec2 -> DblVec2 -> IntVec2 -> IntVec2 -> Double
calculateDistance EastWest (startX, _) (dirX, _) (tileX, _) (stepX, _) =
    ((dbl tileX) - startX + (1 - dbl stepX) / 2) / dirX
calculateDistance NorthSouth (_, startY) (_, dirY) (_, tileY) (_, stepY) =
    ((dbl tileY) - startY + (1 - dbl stepY) / 2) / dirY

-- calculate the height of the vertical line on-screen based on the distance
calculateHeight :: Double -> Double -> Double
calculateHeight screenHeight 0 = screenHeight
calculateHeight screenHeight perpWallDist = screenHeight / perpWallDist

width   :: Double
height  :: Double
(width, height) = (640, 480)

main :: IO ()
main = do
    cvElem <- newElem "canvas" `with` [
            attr "width" =: show width,
            attr "height" =: show height
        ]
    addChild cvElem documentBody
    Just canvas <- getCanvas cvElem
    let pos     = (8, 8)
        dir     = (-1, 0)
        plane   = (0, 0.66)
    renderGame canvas pos dir plane

renderGame :: Canvas -> DblVec2 -> DblVec2 -> DblVec2 -> IO ()
renderGame canvas pos dir plane = do
    let rays    = traceRays testMapTiles (floor width) pos dir plane
    render canvas $ forM_ (zip [0..width - 1] rays) (\(x, (side, tile, dist)) ->
        let lineHeight  = calculateHeight height dist
            wallColor   = case testMapTiles ! tile of
                RedWall     -> RGB 255 0 0
                BlueWall    -> RGB 0 255 0
                GreenWall   -> RGB 0 0 255
                YellowWall  -> RGB 255 255 0
                _           -> RGB 255 255 255
            shadedWallColor = case side of
                EastWest    -> 
                    let (RGB r g b) = wallColor
                    in  RGB (r `div` 2) (g `div` 2) (b `div` 2)
                NorthSouth  -> wallColor
        in  color shadedWallColor $ do
                translate (x, height / 2) $ stroke $ do
                    line (0, -lineHeight / 2) (0, lineHeight / 2))
    -- 25fps
    let fps             = 25
        timeout         = (1000 `div` fps) :: Int
        rots_per_min    = 1
        rots_per_sec    = dbl rots_per_min / 60
        rots_per_frame  = rots_per_sec / dbl fps
        tau             = 2 * pi
        increment       = tau * rots_per_frame 

    setTimeout timeout $ do
       renderGame canvas pos (rot dir $ -increment) (rot plane $ -increment)

HTML 页面:

<!doctype html>
<meta charset=utf-8>
<title>Raycaster</title>

<noscript>If you're seeing this message, either your browser doesn't support JavaScript, or it is disabled for some reason. This game requires JavaScript to play, so you'll need to make sure you're using a browser which supports it, and enable it, to play.</noscript>
<script src=raycast.js></script>

【问题讨论】:

    标签: haskell html5-canvas raycasting haste


    【解决方案1】:

    出现“幻脸”是因为报告了不正确的HitSide:您是说脸部在水平移动时被击中 (EastWest),但实际上是在垂直移动时被击中 (NorthSouth ),反之亦然。

    那为什么它会报告不正确的值呢? if sideDistX &lt; sideDistY then EastWest else NorthSouth 看起来很简单,对吧?确实如此。

    问题不在于我们如何计算该值。 我们计算了这个值。距离计算函数需要知道我们移动到墙壁的方向。然而,我们实际上给出的是如果我们要继续前进(也就是说,如果那块瓷砖不是墙,或者我们出于某种原因忽略它)我们会前进的方向。

    查看 Haskell 代码:

    traceRayInner arr step@(stepX, stepY) deltaDist@(deltaDistX, deltaDistY) tile sideDist@(sideDistX, sideDistY)
        -- a wall has been hit, report hit direction and coördinates
        | arr ! tile /= Empty   = (hitSide, tile)
        -- advance until a wall is hit
        | otherwise             = case hitSide of
            EastWest ->
                let newSideDist = first (deltaDistX+) sideDist
                    newTile     = first (stepX+) tile
                in
                    traceRayInner arr step deltaDist newTile newSideDist
            NorthSouth ->
                let newSideDist = second (deltaDistY+) sideDist
                    newTile     = second (stepY+) tile
                in
                    traceRayInner arr step deltaDist newTile newSideDist
        where
            hitSide = if sideDistX < sideDistY then EastWest else NorthSouth
    

    注意我们是按这个顺序做事的:

    1. 计算hitSide
    2. 检查是否有墙壁被撞,如果是,报告hitSide
    3. 移动

    将此与原始 C++ 代码进行比较:

    //perform DDA
    while (hit == 0)
    {
      //jump to next map square, OR in x-direction, OR in y-direction
      if (sideDistX < sideDistY)
      {
        sideDistX += deltaDistX;
        mapX += stepX;
        side = 0;
      }
      else
      {
        sideDistY += deltaDistY;
        mapY += stepY;
        side = 1;
      }
      //Check if ray has hit a wall
      if (worldMap[mapX][mapY] > 0) hit = 1;
    }
    

    它以不同的顺序做事:

    1. 检查是否撞墙,如果撞墙,报告side(相当于hitSide
    2. 移动计算side

    C++ 代码仅在移动时计算side,然后在撞墙时报告该值。因此,它会报告它移动的方式以撞墙。

    Haskell 代码计算 side 是否移动:因此每次移动都是正确的,但是当它撞到墙上时,它会报告如果继续移动它会移动的方式。

    因此,可以通过重新排序 Haskell 代码来修复它,以便它检查移动后 的命中,如果是,则报告该移动的 hitSide 值。这不是漂亮的代码,但它可以工作:

    traceRayInner arr step@(stepX, stepY) deltaDist@(deltaDistX, deltaDistY) tile sideDist@(sideDistX, sideDistY) =
        let hitSide = if sideDistX < sideDistY then EastWest else NorthSouth
        in  case hitSide of
            EastWest ->
                let newSideDist = first (deltaDistX+) sideDist
                    newTile     = first (stepX+) tile
                in  case arr ! newTile of
                    -- advance until a wall is hit
                    Empty   ->  traceRayInner arr step deltaDist newTile newSideDist
                    -- a wall has been hit, report hit direction and coördinates
                    _       ->  (hitSide, newTile)
            NorthSouth ->
                let newSideDist = second (deltaDistY+) sideDist
                    newTile     = second (stepY+) tile
                in  case arr ! newTile of
                    -- advance until a wall is hit
                    Empty   ->  traceRayInner arr step deltaDist newTile newSideDist
                    -- a wall has been hit, report hit direction and coördinates
                    _       ->  (hitSide, newTile)
    

    问题解决了!


    旁注:我在纸上执行算法后发现出了什么问题。虽然在这种特殊情况下,最后两个 HitSide 值恰好匹配,但很明显它们可能并非在所有情况下都匹配。因此,非常感谢 Freenode 的 #algorithms 上的 Madsy 建议在纸上进行尝试。 :)

    【讨论】:

    • @K3N 我知道。你必须等待一段时间才能接受任何答案,即使是你自己的。
    • 嘿,我记得我在过去遇到了同样的问题,原因有两个,一个是玩家在单元格边界上,另一个和你一样......我最终得到了一个小翻译第一种情况下的射线,另一种通过 z 对每两个水平和垂直命中进行排序来解决...见Ray Casting with different height size
    猜你喜欢
    • 1970-01-01
    • 2019-08-05
    • 1970-01-01
    • 2012-02-25
    • 1970-01-01
    • 1970-01-01
    • 2019-01-17
    • 2013-03-15
    • 2019-07-21
    相关资源
    最近更新 更多