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
相关产品推荐
相关产品推荐

