You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JavaScript光线投射器(Raycaster)纹理显示异常求助

JavaScript光线投射器纹理显示错乱问题排查

我正在开发一个JavaScript光线投射器作为个人项目,参考相关教程实现后,遇到了纹理显示异常的问题,纹理呈现错乱扭曲状态(效果截图:纹理错乱效果)。以下是我的项目代码,请求帮忙排查问题:

<canvas id="myCanvas" width="600" height="256" style="border:1px solid #d3d3d3;">
Your browser does not support the HTML canvas tag.</canvas>
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
var angle_rad;
var t;
var height = 0;
let collide;
let downPressed;
let upPressed;
let rightPressed;
let leftPressed;
var TILE = {
  t: 0,
  x: 0,
  y: 0

}

ctx.lineWidth = 3.5;

var map = {

  cols: 8,
  rows: 8,
  tsize: 32,
  tiles: [
    0, 1, 0, 0, 0, 0, 0, 0,
    0, 1, 0, 0, 0, 1, 1, 0,
    1, 1, 1, 0, 1, 1, 1, 0,
    1, 0, 1, 0, 1, 0, 0, 0,
    1, 0, 1, 1, 1, 1, 0, 0,
    1, 0, 0, 1, 1, 1, 2, 0,
    1, 0, 0, 1, 0, 0, 0, 0,
    1, 1, 1, 1, 0, 0, 0, 0
  ],
  getTile: function(col, row) {
    return this.tiles[row * map.cols + col];
  }
}

var player = {
  speed: 1,
  angle: 0,
  x: 100,
  y: 100,
  dx: 0,
  dy: 0,
  fx: 0,
  fy: 0,
  drawPlayer: function() {

    ctx.beginPath();
    ctx.rect(this.x, this.y, 1, 1);
    ctx.stroke();
  }
}

//ray
var ray = {
  x: 0,
  y: 0,
  d: 0,
  collide: false
}

// delcare scale data or some thing
const texture = {
  width: 8,
  height: 8,
  bitmap: [
    [0, 0, 0, 1, 0, 0, 1, 0],
    [0, 0, 0, 1, 0, 0, 1, 0],
    [1, 1, 1, 1, 1, 1, 1, 1],
    [0, 1, 0, 0, 1, 0, 0, 0],
    [0, 1, 0, 0, 1, 0, 0, 0],
    [1, 1, 1, 1, 1, 1, 1, 1],
    [0, 0, 1, 0, 0, 1, 0, 0],
    [0, 0, 1, 0, 0, 1, 0, 0]
  ],
  colors: [
    "#ff0000",
    "#ffffff",
  ]

}

// function to draw map
function drawmap() {
  for (var column = 0; column < map.cols; column++) {
    for (var row = 0; row < map.rows; row++) {
      const tile = map.getTile(column, row);
      const x = column * map.tsize;
      const y = row * map.tsize;
      if (tile === 0) {
        ctx.beginPath();
        ctx.rect(x, y, 32, 32);
        ctx.stroke();
      }
      //detect collision and prevent it

      if (tile === 1 || tile === 2) {
        if (player.x > x && player.x < x + map.tsize && player.y > y && player.y < y + map.tsize) {

          collide = true;
        } else {
          collide = false;
        }


      }

    }
  }
}


// do stuff when key press and not pressed
function keyDownHandler(e) {
  if (e.key === "Right" || e.key === "ArrowRight") {
    rightPressed = true;
  } else if (e.key === "Left" || e.key === "ArrowLeft") {
    leftPressed = true;
  }
  if (e.key === "Up" || e.key === "ArrowUp") {
    upPressed = true;
  } else if (e.key === "Down" || e.key === "ArrowDown") {
    downPressed = true;
  }
}

function keyUpHandler(e) {
  if (e.key === "Right" || e.key === "ArrowRight") {
    rightPressed = false;
  } else if (e.key === "Left" || e.key === "ArrowLeft") {
    leftPressed = false;
  }
  if (e.key === "Up" || e.key === "ArrowUp") {
    upPressed = false;
  } else if (e.key === "Down" || e.key === "ArrowDown") {
    downPressed = false;
  }
}

// loop
setInterval(loop, 10);

//move player fucntion
function move() {

  angle_rad = player.angle * Math.PI / 180;

  if (upPressed) {
    player.x += player.speed * Math.sin(angle_rad);
    player.y -= player.speed * Math.cos(angle_rad);

  }
  if (leftPressed) {
    player.angle += 2;
  } else if (rightPressed) {
    player.angle -= 2;
  }
}

function raycast(a) {
  console.log(ray.x);
  ray.x = player.x;
  ray.y = player.y;

  for (let i = 0; i < 200; i++) {
    for (let c = 0; c < map.cols; c++) {
      for (let r = 0; r < map.rows; r++) {
        let type = map.getTile(c, r);
        let TX = c * map.tsize;
        let TY = r * map.tsize;

        if (type === 1) {
          if (ray.x > TX && ray.x < TX + map.tsize && ray.y > TY && ray.y < TY + map.tsize) {
            ray.x += Math.sin(a);
            ray.y -= Math.cos(a);
          } else {

          }
        }
      }
    }



  }
}




//loop function
function loop() {

  //log distance




  //clear prevoius mess
  ctx.clearRect(0, 0, 600, 256);
  drawmap();
  move();

  for (var i = 0; i < 90; i++) {
    //get distance
    ray.d = Math.sqrt(Math.pow(player.x - ray.x, 2) + Math.pow(player.y - ray.y, 2));
    height = Math.floor(1000 / ray.d);

    let tex_X = Math.floor((texture.width * (ray.x + ray.y)) % texture.width);

    raycast(angle_rad + ((i * Math.PI / 180)) - 45);
    ray.d = ray.d * Math.cos((angle_rad + ((i * Math.PI / 180)) - 45) - angle_rad);

    drawline(i * 3 + 300, 0, i * 3 + 300, 126 - height, "cyan");
    drawTexture(i * 3 + 300, height, tex_X, texture);
    drawline(i * 3 + 300, 126 + height, i * 3 + 300, 256, "green")


  }

  ctx.beginPath();
  ctx.arc(ray.x, ray.y, 5, 0, 2 * Math.PI);
  ctx.stroke();



  player.drawPlayer();

}

//draw line function
function drawline(x1, y1, x2, y2, color) {
  ctx.strokeStyle = color;
  ctx.beginPath();
  ctx.moveTo(x1, y1);
  ctx.lineTo(x2, y2);
  ctx.stroke();
}
//draw textured wall function
function drawTexture(x, wall_height, tex_posX, tex) {
  let yinc = (wall_height * 2) / tex.height;
  let y = 126 - wall_height;

  for (let i = 0; i < tex.height; i++) {
    ctx.strokeStyle = tex.colors[tex.bitmap[i][tex_posX]];
    ctx.beginPath();
    ctx.moveTo(x, y);
    ctx.lineTo(x, y + (yinc + 0.5));
    ctx.stroke();
    y += yinc;
  }
}

// listen for kep press and key downs
document.addEventListener("keydown", keyDownHandler, false);
document.addEventListener("keyup", keyUpHandler, false);

问题根源及修复方案

1. 纹理采样坐标计算错误

当前tex_X = Math.floor((texture.width * (ray.x + ray.y)) % texture.width)的计算逻辑完全错误,没有基于射线与墙面的碰撞位置采样纹理,导致纹理错乱。正确逻辑是区分射线碰撞的是垂直/水平墙面,根据碰撞点在瓷砖内的相对位置计算纹理X坐标。

2. Raycast函数逻辑错误

当前通过遍历所有瓷砖+固定步数推进射线的方式,既低效又无法准确获取碰撞点位置。应改用DDA(数字差分分析)算法精准追踪射线碰撞路径,获取碰撞墙面的类型和相对位置。

3. 距离计算顺序错误

loop循环中先计算ray.d再调用raycast,导致使用上一帧射线的位置数据,完全不符合逻辑。必须先执行raycast获取当前射线的碰撞结果,再计算距离并做鱼眼校正。


修改后的关键代码

修正Raycast函数(改用DDA算法)

function raycast(angle) {
  ray.x = player.x;
  ray.y = player.y;
  
  const rayDirX = Math.sin(angle);
  const rayDirY = -Math.cos(angle);
  
  // 当前所在瓷砖
  let mapX = Math.floor(ray.x / map.tsize);
  let mapY = Math.floor(ray.y / map.tsize);
  
  // 射线到下一个X/Y瓷砖边界的距离
  let sideDistX, sideDistY;
  // 每步X/Y方向的增量
  let deltaDistX = Math.abs(1 / rayDirX);
  let deltaDistY = Math.abs(1 / rayDirY);
  let stepX, stepY;
  let hit = 0; // 是否碰撞墙面
  let side = 0; // 0=垂直墙面,1=水平墙面

  // 初始化步进方向和初始距离
  if (rayDirX < 0) {
    stepX = -1;
    sideDistX = (ray.x - mapX * map.tsize) * deltaDistX;
  } else {
    stepX = 1;
    sideDistX = ((mapX + 1) * map.tsize - ray.x) * deltaDistX;
  }
  if (rayDirY < 0) {
    stepY = -1;
    sideDistY = (ray.y - mapY * map.tsize) * deltaDistY;
  } else {
    stepY = 1;
    sideDistY = ((mapY + 1) * map.tsize - ray.y) * deltaDistY;
  }

  // DDA循环直到碰撞墙面
  while (hit === 0) {
    // 推进射线到下一个瓷砖边界
    if (sideDistX < sideDistY) {
      sideDistX += deltaDistX;
      mapX += stepX;
      side = 0; // 垂直墙面
    } else {
      sideDistY += deltaDistY;
      mapY += stepY;
      side = 1; // 水平墙面
    }
    // 检查是否碰撞到非空瓷砖
    if (map.getTile(mapX, mapY) > 0) hit = 1;
  }

  // 计算射线到碰撞点的距离
  if (side === 0) {
    ray.d = (mapX * map.tsize - ray.x + (1 - stepX) / 2) / rayDirX;
    // 计算垂直墙面的纹理X坐标:碰撞点的Y值在瓷砖内的比例
    ray.texX = ray.y + ray.d * rayDirY;
  } else {
    ray.d = (mapY * map.tsize - ray.y + (1 - stepY) / 2) / rayDirY;
    // 计算水平墙面的纹理X坐标:碰撞点的X值在瓷砖内的比例
    ray.texX = ray.x + ray.d * rayDirX;
  }
  // 转换为纹理的相对位置(0到texture.width之间)
  ray.texX -= Math.floor(ray.texX / map.tsize) * map.tsize;
  ray.texX = (ray.texX / map.tsize) * texture.width;
  ray.side = side;
}

修正Loop循环中的顺序和纹理坐标计算

function loop() {
  ctx.clearRect(0, 0, 600, 256);
  drawmap();
  move();

  for (var i = 0; i < 90; i++) {
    const rayAngle = angle_rad + ((i * Math.PI / 180)) - 45;
    // 先执行raycast获取当前射线数据
    raycast(rayAngle);
    // 鱼眼校正
    const correctedDist = ray.d * Math.cos(rayAngle - angle_rad);
    height = Math.floor(1000 / correctedDist);

    // 使用raycast返回的正确纹理X坐标
    let tex_X = Math.floor(ray.texX);
    // 防止超出纹理范围
    tex_X = tex_X % texture.width;

    drawline(i * 3 + 300, 0, i * 3 + 300, 126 - height, "cyan");
    drawTexture(i * 3 + 300, height, tex_X, texture);
    drawline(i * 3 + 300, 126 + height, i * 3 + 300, 256, "green")
  }

  ctx.beginPath();
  ctx.arc(ray.x + Math.sin(angle_rad)*ray.d, ray.y - Math.cos(angle_rad)*ray.d, 5, 0, 2 * Math.PI);
  ctx.stroke();

  player.drawPlayer();
}

优化纹理绘制函数(可选)

让纹理缩放更平滑:

function drawTexture(x, wall_height, tex_posX, tex) {
  const yStart = 126 - wall_height;
  const yEnd = 126 + wall_height;
  const texStep = tex.height / (yEnd - yStart);
  let texY = 0;

  for (let y = yStart; y < yEnd; y++) {
    const texRow = Math.floor(texY);
    ctx.strokeStyle = tex.colors[tex.bitmap[texRow][tex_posX]];
    ctx.beginPath();
    ctx.moveTo(x, y);
    ctx.lineTo(x, y + 1);
    ctx.stroke();
    texY += texStep;
  }
}

内容的提问来源于stack exchange,提问作者Jon Ali

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 11:12:02