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

HTML Canvas Ray Casting地板渲染效果异常问题排查

Wolfenstein风格射线投射地板渲染异常排查

问题背景

我正在使用Javascript/HTML-canvas开发一款Wolfenstein风格的小型游戏,开发过程参考Permadi教程。目前已经成功实现带纹理的墙体Ray Casting(射线投射)功能,接下来需要完成地板的射线投射渲染。
按照开发思路,绘制完单条墙体切片后,需要检查该墙体是否抵达画布底部:如果没有抵达,说明墙体下方区域需要渲染地板。此时需要遍历墙体底部到画布底部的每一个像素,计算这些像素对应的真实世界坐标,采样对应纹理后绘制到屏幕上。
开发过程参考两份原理示意图:
地板投射原理示意图1
地板投射原理示意图2
核心实现代码如下:

//we check if the wall reaches the bottom of the canvas
    // this.wallToBorder = (400 - wallHeight) / 2;
    if (this.wallToBorder > 0) {
      // we calculate how many pixels we have from bottom of wall to border of canvas
      var pixelsToBottom = Math.floor(this.wallToBorder);

      //we calculate the distance between the first pixel at the bottom of the wall and the player eyes (canvas.height / 2)
      var pixelRowHeight = 200 - pixelsToBottom;

      // then we loop through every pixels until we reach the border of the canvas
      for (let i = pixelRowHeight; i < 200; i++) {

        // we calculate the straight distance between the player and the pixel
        var directDistFloor = this.screenDist * (canvas.height/2) / Math.floor(i);
        
        // we calculate it's real world distance with the angle relative to the player
        var realDistance = directDistFloor / Math.cos(this.angleR);

        // we calculate it's real world coordinates with the player angle
        this.floorPointx = this.player.x + Math.cos(this.angle) * realDistance;
        this.floorPointy = this.player.y - Math.sin(this.angle) * realDistance;

        // we map the texture
        var textY = Math.floor(this.floorPointx % 64);
        var textX = Math.floor(this.floorPointy % 64);

        var pixWidthHeight = (1 / realDistance) * this.screenDist;
        if (pixWidthHeight < 1) pixWidthHeight = 1;
        // we draw it on the canvas
        this.ctx.drawImage(wallsSprite, textX, textY + 64, 1, 1, this.index, i + 200, pixWidthHeight, pixWidthHeight);
      }
    }

实际运行后渲染结果不符合预期,异常效果如下:
地板渲染异常截图
完整项目可在StackBlitz查看,入口文件为ray.js,需要排查实现逻辑中的错误。

错误点与修正方案

代码共有4处核心逻辑错误,逐一修正即可得到正确的渲染效果:

  • 距离计算逻辑错误
    计算地板点到玩家的垂直投影距离时,不应对行偏移值i提前做Math.floor()取整,否则会丢失精度导致近远景过渡错位,且当i值小于1时会触发除以0的异常;同时公式里的高度参数应该用玩家视高(和项目匹配的32单位,对应64尺寸墙高的一半),而非画布半高。修正为:
    var directDistFloor = this.screenDist * 32 / i;
    
  • 多余的鱼眼校正逻辑
    计算出的directDistFloor已经是垂直于投影平面的校正后距离,不需要再除以射线夹角余弦值做二次鱼眼校正,直接用这个距离沿当前射线方向计算世界坐标即可,多余的除法会导致远处地板被异常拉伸。
  • 纹理采样逻辑错误
    一是X、Y轴的纹理坐标映射写反,二是没有处理世界坐标为负数时取模得到负纹理坐标的问题,会采样到纹理范围外的错误像素;额外添加的pixWidthHeight像素缩放逻辑完全多余,逐列绘制地板时单像素尺寸固定为1*1即可,不需要动态缩放,否则会出现像素重叠、错位。修正后的纹理采样与绘制代码为:
    this.floorPointx = this.player.x + Math.cos(this.angle) * directDistFloor;
    this.floorPointy = this.player.y - Math.sin(this.angle) * directDistFloor;
    
    var textX = Math.floor(this.floorPointx % 64);
    var textY = Math.floor(this.floorPointy % 64);
    if (textX < 0) textX += 64;
    if (textY < 0) textY += 64;
    
    this.ctx.drawImage(wallsSprite, textX, textY + 64, 1, 1, this.index, i + 200, 1, 1);
    
  • 循环边界精度问题
    计算pixelsToBottom、pixelRowHeight时不需要提前做Math.floor()取整,浮点数计算可以保证墙体和地板衔接处没有缝隙,浏览器绘制时会自动处理像素对齐。

修正后墙体和地板可以自然衔接,纹理透视关系正确,不会出现拉伸、错位的异常效果。


内容的提问来源于stack exchange,提问作者Toto Briac

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 22:03:32