JS 中的简单 z-buffer 实现示例? [初学者]

simple z-buffer implementation example in JS? [beginner]

我正在寻找 Z 缓冲区的非常基本的实现,最好是在 JS 中。我正在尝试查看一个非常简单的代码,例如两个重叠的多边形,一个隐藏另一个。 我找不到这样的基本示例,但我可以找到几个“远高于我当前水平和理解力”的示例。 是否有您可以推荐的入门资源?

感谢您的帮助和建议!

Z 缓冲区(也称为深度缓冲区)只不过是一个 2D 像素阵列(想想图像)。它不是 RGB,它只在每个像素中存储一个值,即距当前视点的距离:

// one value for each pixel in our screen
const depthBuffer = new Array(screenWidth * screenHeight);

它增加了包含您呈现给用户的实际图像的颜色缓冲区:

// create buffer for color output
const numChannels = 3; // R G B
const colorBuffer = new Array(screenWidth * screenHeight * numChannels);

对于您绘制的形状的每个像素,您检查 Z-Buffer 以查看是否有任何靠近相机的东西遮挡了当前像素,如果有则不绘制它。这样你就可以按任何顺序绘制东西,并且它们仍然在每个像素级别上被正确遮挡。

Z-Buffering 不仅可以用于 3D,还可以用于 2D 以实现绘制顺序无关性。比方说我们想画几个盒子,这就是我们的盒子 class:

class Box {
  /** @member {Object} position of the box storing x,y,z coordinates */
  position;
  /** @member {Object} size of the box storing width and height */
  size;
  /** @member {Object} color of the box given in RGB */
  color;

  constructor (props) {
    this.position = props.position;
    this.size = props.size;
    this.color = props.color;
  }

  /**
   * Check if given point is in box
   * @param {Number} px coordinate of the point
   * @param {Number} py coordinate of the point
   * @return {Boolean} point in box
   */
  pointInBox (px,py) {
    return this.position.x < px && this.position.x + this.size.width > px
        && this.position.y < py && this.position.y + this.size.height > py;
  }
}

有了这个 class 我们现在可以创建几个盒子并绘制它们:

const boxes = [
  new Box({
    position: { x: 50, y: 50, z: 10 },
    size: { width: 50, height: 20 },
    color: { r: 255, g: 0, b:0 }
  }),
  // green box
  new Box({
    position: { x: 80, y: 30, z: 5 },
    size: { width: 10, height: 50 },
    color: { r: 0, g: 255, b:0 }
  }),
  // blue
  new Box({
    position: { x: 60, y: 55, z: 8 },
    size: { width: 50, height: 10 },
    color: { r: 0, g: 0, b: 255 }
  })
];

指定形状后,我们现在可以绘制它们了:

for(const box of boxes) {
  for(let x = 0; x < screenWidth; x++) {
    for(let y = 0; y < screenHeight; y++) {
      // check if our pixel is within the box
      if (box.pointInBox(x,y)) {
        // check if this pixel of our box is covered by something else
        // compare depth value in depthbuffer against box position
        // this is commonly referred to as "depth-test"
        if (depthBuffer[x + y * screenWidth] < box.position.z) {
          // something is already closer to the viewpoint than our current primitive, don't draw this pixel:
          continue;
        }
        // we passed the depth test, put our current depth value in the z-buffer
        depthBuffer[x + y * screenWidth] = box.position.z;
        // put the color in the color buffer, channel by channel
        colorBuffer[(x + y * screenWidth)*numChannels + 0] = box.color.r;
        colorBuffer[(x + y * screenWidth)*numChannels + 1] = box.color.g;
        colorBuffer[(x + y * screenWidth)*numChannels + 2] = box.color.b;
      }
    }
  }
}

请注意,此代码是示例性代码,因此就概念布局而言过于冗长且效率低下。

const ctx = document.getElementById("output").getContext('2d');
const screenWidth = 200;
const screenHeight = 200;

// one value for each pixel in our screen
const depthBuffer = new Array(screenWidth * screenHeight);

// create buffer for color output
const numChannels = 3; // R G B
const colorBuffer = new Array(screenWidth * screenHeight * numChannels);

/**
 * Represents a 2D box
 * @class
 */
class Box {
  /** @member {Object} position of the box storing x,y,z coordinates */
  position;
  /** @member {Object} size of the box storing width and height */
  size;
  /** @member {Object} color of the box given in RGB */
  color;

  constructor (props) {
    this.position = props.position;
    this.size = props.size;
    this.color = props.color;
  }

  /**
   * Check if given point is in box
   * @param {Number} px coordinate of the point
   * @param {Number} py coordinate of the point
   * @return {Boolean} point in box
   */
  pointInBox (px,py) {
    return this.position.x < px && this.position.x + this.size.width > px
        && this.position.y < py && this.position.y + this.size.height > py;
  }
}

const boxes = [
  // red box
  new Box({
    position: { x: 50, y: 50, z: 10 },
    size: { width: 150, height: 50 },
    color: { r: 255, g: 0, b:0 }
  }),
  // green box
  new Box({
    position: { x: 80, y: 30, z: 5 },
    size: { width: 10, height: 150 },
    color: { r: 0, g: 255, b:0 }
  }),
  // blue
  new Box({
    position: { x: 70, y: 70, z: 8 },
    size: { width: 50, height: 40 },
    color: { r: 0, g: 0, b: 255 }
  })
];
const varyZ = document.getElementById('varyz');
varyZ.onchange = draw;
function draw () {
  // clear depth buffer of previous frame
  depthBuffer.fill(10);
  for(const box of boxes) {
    for(let x = 0; x < screenWidth; x++) {
      for(let y = 0; y < screenHeight; y++) {
        // check if our pixel is within the box
        if (box.pointInBox(x,y)) {
          // check if this pixel of our box is covered by something else
          // compare depth value in depthbuffer against box position
          if (depthBuffer[x + y * screenWidth] < box.position.z) {
            // something is already closer to the viewpoint that our current primitive, don't draw this pixel:
            if (!varyZ.checked) continue;
            if (depthBuffer[x + y * screenWidth] < box.position.z + Math.sin((x+y))*Math.cos(x)*5) continue;
          }
          // we passed the depth test, put our current depth value in the z-buffer
          depthBuffer[x + y * screenWidth] = box.position.z;
          // put the color in the color buffer, channel by channel
          colorBuffer[(x + y * screenWidth)*numChannels + 0] = box.color.r;
          colorBuffer[(x + y * screenWidth)*numChannels + 1] = box.color.g;
          colorBuffer[(x + y * screenWidth)*numChannels + 2] = box.color.b;
        }
      }
    }
  }

  // convert to rgba for presentation
  const oBuffer = new Uint8ClampedArray(screenWidth*screenHeight*4);
  for (let i=0,o=0; i < colorBuffer.length; i+=3,o+=4) {
  oBuffer[o]=colorBuffer[i];
  oBuffer[o+1]=colorBuffer[i+1];
  oBuffer[o+2]=colorBuffer[i+2];
  oBuffer[o+3]=255;
  }
  ctx.putImageData(new ImageData(oBuffer, screenWidth, screenHeight),0,0);
}

document.getElementById('redz').oninput = e=>{boxes[0].position.z=parseInt(e.target.value,10);draw()};
document.getElementById('greenz').oninput = e=>{boxes[1].position.z=parseInt(e.target.value,10);draw()};
document.getElementById('bluez').oninput = e=>{boxes[2].position.z=parseInt(e.target.value,10);draw()};

draw();
canvas {
border:1px solid black;
float:left;
margin-right: 2rem;
}
label {display:block;}
label span {
display:inline-block;
width: 100px;
}
<canvas width="200" height="200" id="output"></canvas>
<label><span>Red Z</span>
<input type="range" min="0" max="10" value="10" id="redz"/>
</label>
<label><span>Green Z</span>
<input type="range" min="0" max="10" value="5" id="greenz"/>
</label>
<label><span>Blue Z</span>
<input type="range" min="0" max="10" value="8" id="bluez"/>
</label>
<label><span>Vary Z Per Pixel</span>
<input type="checkbox" id="varyz"/>
</label>