javascript - 使用 drawImage 从 Tile Sheet、Sprite Sheet 或 Texture Atlas 绘制多个图像时,如何防止纹理出血?

标签 javascript graphics html5-canvas textures scaling

我正在使用 HTML5 Canvas API 为像素艺术游戏绘制瓷砖 map 。渲染的瓦片 map 由许多较小的图像组成,这些图像是从称为瓦片表的单个源图像中剪切出来的。我正在使用 drawImage(src_img, sx, sy, sw, sh, dx, dy, dw, dh)从源图像中切出单个图块并将它们绘制到目标 Canvas 上。我正在使用 setTransform(sx, 0, 0, sy, tx, ty)将缩放和平移应用于最终渲染的图像。

我需要修复的颜色“出血”问题是由采样器引起的,它在缩放操作期间使用插值来混合颜色,以使事物看起来不会像素化。这对于缩放数码照片非常有用,但不适用于像素艺术。虽然这不会对图块的中心造成太大的视觉损害,但采样器会沿着源图像中相邻图块的边缘混合颜色,从而在渲染的图块 map 中产生意想不到的颜色。而不是仅使用传递给 drawImage 的源矩形内的颜色,采样器从其边界之外混合颜色,导致瓷砖之间出现间隙。

下面是我的瓷砖表的源图像。它的实际大小是 24x24 像素,但我在 GIMP 中将其放大到 96x96 像素,以便您可以看到它。我在 GIMP 的缩放工具上使用了“插值:无”设置。正如您所看到的,由于采样器没有对颜色进行插值,因此各个图块周围没有间隙或模糊的边界。即使在 imageSmoothingEnabled 时, Canvas API 的采样器显然也会插入颜色。设置为 false .

enter image description here

下面是带有 imageSmoothingEnabled 的渲染图块 map 的一部分设置为 true .左箭头指向灰色瓷砖底部的一些红色出血。这是因为红色瓷砖在瓷砖表中的灰色瓷砖正下方。采样器正在将红色混合到灰色瓷砖的底部边缘。

右侧的箭头指向绿色图块的右边缘。如您所见,没有颜色渗入其中。这是因为源图像中绿色图块的右侧没有任何东西,因此采样器没有任何东西可以混合。

Tile Map With Image Smoothing Enabled

下面是带有 imageSmoothingEnabled 的渲染图块 map 设置为 false .根据比例和翻译,仍然会发生纹理渗色。左箭头指向从源图像中的红色瓷砖渗出的红色。视觉伤害减少了,但仍然存在。

右箭头指向最右侧绿色磁贴的问题,它有一条细灰色线从源图像中的灰色磁贴渗入,位于绿色磁贴的左侧。

Tile Map With Image Smoothing Disabled

上面的两张图片是从 Edge 截屏的。 Chrome 和 Firefox 在隐藏出血方面做得更好。 Edge 似乎在四面八方流血,但 Chrome 和 Firefox 似乎只在源矩形的右侧和底部流血。

如果有人知道如何解决这个问题,请告诉我。人们在很多论坛上询问这个问题,并得到解决以下问题的答案:

  • 用边框颜色填充源图块,以便采样器沿边缘混合相同的颜色。
  • 将您的源切片放在单独的文件中,这样采样器就没有任何东西可以超过边界进行采样。
  • 将所有内容绘制到未缩放的缓冲区 Canvas ,然后缩放缓冲区,确保采样器混合来自作为最终图像一部分的相邻图块的颜色,从而减轻视觉损坏。
  • 将所有内容绘制到未缩放的 Canvas 上,然后使用 CSS 使用 image-rendering:pixelated 缩放它,这与之前的工作基本相同。

  • 我想避免变通,但是如果您知道另一个变通方法,请发布。我想知道是否有办法关闭采样或插值,或者是否有任何其他方法可以阻止纹理流血,这不是我列出的解决方法之一。

    这是一个展示问题的 fiddle :https://jsfiddle.net/0rv1upjf/

    你可以在我的 Github Pages 页面上看到同样的例子:https://frankpoth.info/pages/javascript-projects/content/texture-bleeding/texture-bleeding.html

    更新:

    由于在绘制像素时使用了浮点数,因此出现了问题。解决方案是避免浮点数,只使用整数。不幸的是,这意味着 setTransform 无法有效使用,因为缩放通常会导致浮点数,但我仍然设法在 tile 渲染循环中保留了一些数学知识。这是代码:
    function drawRounded(source_image, context, scale) {
    
      var offset_x = -OFFSET.x * scale + context.canvas.width  * 0.5;
      var offset_y = -OFFSET.y * scale + context.canvas.height * 0.5;
    
      var map_height = (MAP_HEIGHT * scale)|0; // Similar to Math.trunc(MAP_HEIGHT * scale);
      var map_width  = (MAP_WIDTH  * scale)|0;
      var tile_size  = TILE_SIZE * scale;
    
      var rendered_tile_size = (tile_size + 1)|0; // Similar to Math.ceil(tile_size);
    
      var map_index = 0; // Track the tile index in the map. This increases once per draw loop.
    
      /* Loop through all tile positions in actual coordinate space so no additional calculations based on grid index are needed. */
      for (var y = 0; y < map_height; y += tile_size) { // y first so we draw rows from top to bottom
    
        for (var x = 0; x < map_width; x += tile_size) {
    
          var frame = FRAMES[MAP[map_index]]; // The frame is the source location of the tile in the source_image.
    
          // We have to keep the dx, dy truncation inside the loop to ensure the highest level of accuracy possible. 
          context.drawImage(source_image, frame.x, frame.y, TILE_SIZE, TILE_SIZE, (offset_x + x)|0, (offset_y + y)|0, rendered_tile_size, rendered_tile_size);
    
          map_index ++;
    
        }
    
      }
    
    }
    

    我正在使用按位或或 |运算符来做我的四舍五入。按位或在每个位位置返回 1,其中一个或两个操作数的相应位为 1。按位运算会将浮点数转换为整数。使用 0 作为右操作数将匹配左操作数中的所有位并 chop 小数。这样做的缺点是它只支持 32 位,但我怀疑我的图块位置永远不会超过 32 位。

    例如:

    -10.5 | 0 == -10

    10.1 | 0 == 10

    10.5 | 0 == 10

    在二进制中:

    1010 | 0000 == 1010

    最佳答案

    这是一个四舍五入的问题。
    已经有 that question当上下文准确地转换为 Safari 浏览器时遇到的这个问题 n.5 , Edge 和 IE 更糟,总是以一种或另一种方式流血,MacOs 版 Chrome 在 n.5 上流血也是,但只有在绘制 时, 才可以。
    至少可以说,这是一个有问题的区域。
    我没有检查规范以确切知道他们应该做什么,但有一个简单的解决方法。
    自己计算坐标的变换,以便您可以准确控制它们将如何四舍五入。
    这样您甚至不需要关闭图像平滑算法,您将始终拥有清晰的像素,因为您将始终在像素边界上进行绘制:

    // First calculate the scaled translations
    const scaled_offset_left = -OFFSET.x * scale + context.canvas.width * 0.5;
    const scaled_offset_top  = -OFFSET.y * scale + context.canvas.height * 0.5;
    
    // when drawing each tile
    
    const dest_x = Math.floor( scaled_offset_left + (x * scale) );
    const dest_y = Math.floor( scaled_offset_top  + (y * scale) );
    const dest_size = Math.ceil( TILE_SIZE * scale );
    
    context.drawImage( source_image,
      frame.x, frame.y, TILE_SIZE, TILE_SIZE,
      dest_x, dest_y, dest_size, dest_size,
    );
    

    /* This is the tile map. Each value is a frame index in the FRAMES array. Each frame tells drawImage where to blit the source from */
    const MAP = [
      0, 0, 0, 1, 1, 1, 1, 2, 2, 2,
      0, 1, 0, 1, 2, 2, 1, 2, 3, 2,
      0, 0, 0, 1, 1, 1, 1, 2, 2, 2,
      3, 3, 3, 4, 4, 4, 4, 5, 5, 5,
      3, 4, 3, 4, 5, 5, 4, 5, 6, 5,
      3, 4, 3, 4, 5, 5, 4, 5, 6, 5,
      3, 3, 3, 4, 4, 4, 4, 5, 5, 5,
      6, 6, 6, 7, 7, 7, 7, 8, 8, 8,
      6, 7, 6, 7, 8, 8, 7, 8, 0, 8,
      6, 6, 6, 7, 7, 7, 7, 8, 8, 8
    ];
    
    const TILE_SIZE = 8; // Each tile is 8x8 pixels
    
    const MAP_HEIGHT = 80; // The map is 80 pixels tall by 80 pixels wide
    const MAP_WIDTH = 80;
    
    /* Each frame represents the source x, y coordinates of a tile in the source image. They are indexed according to the map values */
    const FRAMES = [
      { x:0,  y:0 }, // map value = 0
      { x:8,  y:0 }, // map value = 1
      { x:16, y:0 }, // map value = 2
      { x:0,  y:8 }, // etc.
      { x:8,  y:8 },
      { x:16, y:8},
      { x:0,  y:16},
      { x:8,  y:16},
      { x:16, y:16}
    ];
    
    /* These represent the state of the keyboard keys being used. false is up and true is down */
    const KEYS = {
      down: false,
      left: false,
      right: false,
      scale_down: false, // the D key
      scale_up: false, // the F key
      up: false
    }
    
    /* This is the scroll offset. You can also think of it as the position of the red dot in the map. */
    const OFFSET = {
      x: MAP_WIDTH * 0.5,
      y: MAP_HEIGHT * 0.5
    }; // It starts out centered in the map.
    
    const MAX_SCALE = 75; // Max scale is 75 times larger than the actual image size.
    const MIN_SCALE = 0; // Texture bleeding seems to only occur on upscale, but min scale is 0 in case you want to try it.
    
    var scale = 4.71; // some arbitrary number that will hopefully cause the issue in your browser
    
    /* Get the canvas drawing context. */
    var context = document.querySelector('canvas').getContext('2d', {
      alpha: false,
      desynchronized: true
    });
    
    /* The toggle button is the div */
    var toggle = document.querySelector('div');
    
    /* The source image is a 24x24 square with 9 tile images of various colors in it. */
    var base_64_image_source = 'data:image/png;base64,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';
    
    var source_image = new Image(); // This will be the source image
    
    /* The keyboard event handler */
    function keyDownUp(event) {
    
      var state = event.type == 'keydown' ? true : false;
    
      switch (event.keyCode) {
    
        case 37:
          KEYS.left = state;
          break;
        case 38:
          KEYS.up = state;
          break;
        case 39:
          KEYS.right = state;
          break;
        case 40:
          KEYS.down = state;
          break;
        case 68:
          KEYS.scale_down = state;
          break;
        case 70:
          KEYS.scale_up = state;
    
      }
    
    }
    
    /* This is the update and rendering loop. It handles input and draws the images. */
    function loop() {
    
      window.requestAnimationFrame(loop); // Perpetuate the loop
    
      /* Prepare to move and scale the image with the keyboard input */
      if (KEYS.left) OFFSET.x -= 0.5;
      if (KEYS.right) OFFSET.x += 0.5;
      if (KEYS.up) OFFSET.y -= 0.5;
      if (KEYS.down) OFFSET.y += 0.5;
      if (KEYS.scale_down) scale -= 0.5 * scale / MAX_SCALE;
      if (KEYS.scale_up) scale += 0.5 * scale / MAX_SCALE;
    
      /* Keep the scale size within a defined range */
      if (scale > MAX_SCALE) scale = MAX_SCALE;
      else if (scale < MIN_SCALE) scale = MIN_SCALE;
    
      /* Clear the canvas to gray. */
      context.setTransform(1, 0, 0, 1, 0, 0); // Set the transform back to the identity matrix
      context.fillStyle = "#202830"; // Set the fill color to gray
      context.fillRect(0, 0, context.canvas.width, context.canvas.height); // fill the entire canvas
    
    
      /* [EDIT] 
        Don't set the transform, we will calculate it ourselves
        // context.setTransform(scale, 0, 0, scale, -OFFSET.x * scale + context.canvas.width * 0.5, -OFFSET.y * scale + context.canvas.height * 0.5); 
      
        First step is calculating the scaled translation
      */
      
      const scaled_offset_left = -OFFSET.x * scale + context.canvas.width * 0.5;
      const scaled_offset_top  = -OFFSET.y * scale + context.canvas.height * 0.5;
    
      let map_index = 0; // Track the tile index in the map. This increases once per draw loop.
    
    
     
      /* Loop through all tile positions in actual coordinate space so no additional calculations based on grid index are needed. */
      for (let y = 0; y < MAP_HEIGHT; y += TILE_SIZE) { // y first so we draw rows from top to bottom
    
        for (let x = 0; x < MAP_WIDTH; x += TILE_SIZE) {
    
          const frame = FRAMES[MAP[map_index]]; // The frame is the source location of the tile in the source_image.
    
          /* [EDIT] 
            We transform the coordinates ourselves
            We can control a uniform rounding by using floor and ceil
          */
    
          const dest_x = Math.floor( scaled_offset_left + (x * scale) );
          const dest_y = Math.floor( scaled_offset_top  + (y * scale) );
          const dest_size = Math.ceil(TILE_SIZE * scale);
    
          context.drawImage( source_image,
            frame.x, frame.y, TILE_SIZE, TILE_SIZE,
            dest_x, dest_y, dest_size, dest_size
          );
    
          map_index++;
    
        }
    
      }
    
      /* Draw the red dot in the center of the screen. */
      context.fillStyle = "#ff0000";
    
      /* [EDIT]
        Do the same kind of calculations for the "dot" if you don't want antialiasing
    
      //  const dot_x = Math.floor( scaled_offset_left + ((OFFSET.x - 0.5) * scale) );
      //  const dot_y = Math.floor( scaled_offset_top + ((OFFSET.y - 0.5) * scale) );
      //  const dot_size = Math.ceil( scale );
      //  context.fillRect( dot_x, dot_y, dot_size, dot_size ); // center on the dot
    
      But if you do want antialiasing for the dot, then just set the transformation for this drawing
      */
       context.setTransform(scale, 0, 0, scale, scaled_offset_left, scaled_offset_top);
       context.fillRect( (OFFSET.x - 0.5), (OFFSET.y - 0.5), 1, 1 ); // center on the dot
    
      
    
      var smoothing = context.imageSmoothingEnabled; // Get the current smoothing value because we are going to ignore it briefly.
    
      /* Draw the source image in the top left corner for reference. */
      context.setTransform(4, 0, 0, 4, 0, 0); // Zoom in on it so it's visible. 
      context.imageSmoothingEnabled = false; // Set smoothing to false so we get a crisp source image representation (the real source image is not scaled at all).
      context.drawImage( source_image, 0, 0 );
      context.imageSmoothingEnabled = smoothing; // Set smoothing back the way it was according to the toggle choice.
    
    }
    
    /* Turn image smoothing on and off when you press the toggle. */
    function toggleSmoothing(event) {
    
      context.imageSmoothingEnabled = !context.imageSmoothingEnabled;
    
      if (context.imageSmoothingEnabled) toggle.innerText = 'Smoothing Enabled'; // Make sure the button has appropriate text in it.
      else toggle.innerText = 'Smoothing Disabled';
    
    }
    
    /* The main loop will start after the source image is loaded to ensure there is something to draw. */
    source_image.addEventListener('load', (event) => {
    
      window.requestAnimationFrame(loop); // Start the loop
    
    }, { once: true });
    
    /* Add the toggle smoothing click handler to the div. */
    toggle.addEventListener('click', toggleSmoothing);
    
    /* Add keyboard input */
    window.addEventListener('keydown', keyDownUp);
    window.addEventListener('keyup', keyDownUp);
    
    /* Resize the canvas. */
    context.canvas.width = 480;
    context.canvas.height = 480;
    
    toggleSmoothing(); // Set imageSmoothingEnabled
    
    /* Load the source image from the base64 string. */
    source_image.setAttribute('src', base_64_image_source);
    * {
      box-sizing: border-box;
      margin: 0;
      overflow: hidden;
      padding: 0;
      user-select: none;
    }
    
    body,
    html {
      background-color: #202830;
      color: #ffffff;
      height: 100%;
      width: 100%;
    }
    
    body {
      align-items: center;
      display: grid;
      justify-items: center;
    }
    
    p {
      max-width: 640px;
    }
    
    div {
      border: #ffffff 2px solid;
      bottom: 4px;
      cursor: pointer;
      padding: 8px;
      position: fixed;
      right: 4px
    }
    <div>Smoothing Disabled</div>
    <p>Use the arrow keys to scroll and the D and F keys to scale. The source image is represented on the top left. Notice the vertical and horizontal lines that appear between tiles as you scroll and scale. They are the color of the tile's neighbor in the source
      image. This may be due to color sampling that occurs during scaling. Click the toggle to set imageSmoothingEnabled on the drawing context.</p>
    
    <canvas></canvas>

    请注意,要绘制您的“播放器”点,您可以选择手动执行相同的计算以避免由抗锯齿引起的模糊,或者如果您确实想要这种模糊,那么您可以简单地仅为该点设置变换。在你的位置上,我什至可能会在经过一定比例的回合后制作一些模块化的东西,并在下面平滑,但我会让读者去做那个实现。

    关于javascript - 使用 drawImage 从 Tile Sheet、Sprite Sheet 或 Texture Atlas 绘制多个图像时,如何防止纹理出血?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60684359/

    相关文章:

    javascript - 在 HTML Canvas 上加载图像时处理 Image.onerror 中的 404

    javascript - 从 Sprite 图像生成动画?

    java - 为复选框分配一个值(true 或 false)

    javascript - 网页中的计时器与 phantomjs 中的时间同步

    javascript - lightSlider 在缩略图旁边显示所有按钮

    macos - OpenGL 纹理单元的范围是什么?

    delphi - 在 TPNGImage 上拉伸(stretch)绘制

    c++ - 变换所需的矩阵乘法图形矩阵?

    arrays - 在另一个 Canvas 上创建 Canvas

    用于检查输入值的 Javascript