用于深度排序2d对象的WebGL 3d用法

时间:2016-12-17 16:09:47

标签: html5 canvas 3d webgl depth-buffer

这个问题与我的另一个问题有很强的关系: Isometric rendering without tiles, is that goal reachable?

我想在等距世界(html5画布)中深度排序对象。 世界不是平铺的,因此世界上的每个项目都可以放置在每个x,y,z坐标上。由于它不是平铺世界,深度排序很难做到。 我甚至想要如果项目相交,那么可见部分就像是在完全三维世界中相交的部分一样被绘制出来。 正如人们在我的另一个问题中回答的那样,这可以通过将每个2d图像表示为3d模型来完成。 我想继续讨论以下关于该问题的评论:

  

使用webGL时,您不必使用3D。 WebGL绘制多边形,并且可以非常快速地将2D图像绘制成4个顶点,从而形成三角形的小扇形。您仍然可以使用zbuffer并将角(verts)设置为z距离。如果不存在webGL,大多数2D游戏库都使用webGL来渲染2D并回退到画布。在github上还有一个canvas API的webGL实现,您可以修改它以满足您的需求。   (comment link

因此,您可以将“逻辑”视为3d模型。 webGL的z缓冲区提供正确的渲染。渲染像素本身是2D图像的像素。但我不知道该怎么做。有人可以进一步解释如何完成这项工作吗?我阅读了很多信息,但这些都是关于真正的3D。

2 个答案:

答案 0 :(得分:2)

可以像你在其他问题中指出的那样使用深度精灵(ps,你真的应该将这些图像放在这个问题中)

要使用深度精灵,您需要启用EXT_frag_depth扩展名(如果存在)。然后,您可以在片段着色器中写入gl_fragDepthEXT。与制作3D模型相比,制作深度精灵听起来对我来说更有用。

在这种情况下,你只需要为每个精灵加载2个纹理,一个用于颜色,一个用于深度,然后执行类似

的操作
 #extension GL_EXT_frag_depth : require

 varying vec2 texcoord;

 uniform sampler2D colorTexture;
 uniform sampler2D depthTexture;
 uniform float depthScale;
 uniform float depthOffset;

 void main() {
   vec4 color = texture2D(colorTexture, texcoord);

   // don't draw if transparent
   if (color.a <= 0.01) {
     discard;
   }

   gl_FragColor = color;

   float depth = texture2D(depthTexture, texcoord).r;
   gl_FragDepthEXT = depthOffset - depth * depthScale;
 } 

您可以将depthOffsetdepthScale设置为

 var yTemp = yPosOfSpriteInPixelsFromTopOfScreen + tallestSpriteHeight;
 var depthOffset = 1. - yTemp / 65536;
 var depthScale = 1 / 256;

假设每个深度变化的深度纹理中的每个值都较少。

关于如何在WebGL中绘制2D see this article

这是一个似乎有用的例子。我生成了图像,因为我懒得在photoshop中绘制它。手动绘制深度值非常繁琐。假设深度值为1的图像中最远的像素,下一个最接近的像素的深度值为2,等等。

换句话说,如果您有一个小的3x3等距立方体,深度值将类似于

+---+---+---+---+---+---+---+---+---+---+
|   |   |   |   | 1 | 1 |   |   |   |   |
+---+---+---+---+---+---+---+---+---+---+
|   |   | 2 | 2 | 2 | 2 | 2 | 2 |   |   |
+---+---+---+---+---+---+---+---+---+---+
| 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
+---+---+---+---+---+---+---+---+---+---+
| 3 | 3 | 4 | 4 | 4 | 4 | 4 | 4 | 3 | 3 |
+---+---+---+---+---+---+---+---+---+---+
| 3 | 3 | 4 | 4 | 5 | 5 | 4 | 4 | 3 | 3 |
+---+---+---+---+---+---+---+---+---+---+
| 3 | 3 | 4 | 4 | 5 | 5 | 4 | 4 | 3 | 3 |
+---+---+---+---+---+---+---+---+---+---+
| 3 | 3 | 4 | 4 | 5 | 5 | 4 | 4 | 3 | 3 |
+---+---+---+---+---+---+---+---+---+---+
|   |   | 4 | 4 | 5 | 5 | 4 | 4 |   |   |
+---+---+---+---+---+---+---+---+---+---+
|   |   |   |   | 5 | 5 |   |   |   |   |
+---+---+---+---+---+---+---+---+---+---+

&#13;
&#13;
function makeDepthColor(depth) {
  return "rgb(" + depth + "," + depth + "," + depth + ")";
}

function makeSprite(ctx, depth) {
  // make an image (these would be made in photoshop ro
  // some other paint program but that's too much work for me
  ctx.canvas.width = 64;
  ctx.canvas.height = 64;
  for (y = 0; y <= 32; ++y) {
    var halfWidth = (y < 16 ? 1 + y : 33 - y) * 2;
    var width = halfWidth * 2;
    var cy = (16 - y);
    var cw = Math.max(0, 12 - Math.abs(cy) * 2) | 0;
    for (var x = 0; x < width; ++x) {
      var cx = x - halfWidth;
      var inCenter = Math.abs(cy) < 6 && Math.abs(cx) <= cw;
      var onEdge = x < 2 || x >= width - 2 || (inCenter && (Math.abs(cx / 2) | 0) === (cw / 2 | 0));
      var height = onEdge ? 12 : (inCenter ? 30 : 10);
      var color = inCenter ? (cx < 0 ? "#F44" : "#F66") : (cx < 0 ? "#44F" : "#66F");
      ctx.fillStyle = depth ? makeDepthColor(y + 1) : color;
      var xx = 32 - halfWidth + x;
      var yy = y;
      ctx.fillRect(xx, yy + 32 - height, 1, height);
      if (!depth) {
        ctx.fillStyle = onEdge ? "black" : "#CCF";
        ctx.fillRect(xx, yy + 32 - height, 1, 1);
      }
    }
  }
}

function main() {
  var m4 = twgl.m4;
  var gl = document.querySelector("canvas").getContext(
    "webgl", {preserveDrawingBuffer: true});
  var ext = gl.getExtension("EXT_frag_depth");
  if (!ext) {
    alert("need EXT_frag_depth");
    return;
  }

  var vs = `
    attribute vec4 position;
    attribute vec2 texcoord;

    varying vec2 v_texcoord;

    uniform mat4 u_matrix;
    uniform mat4 u_textureMatrix;

    void main() {
      v_texcoord = (u_textureMatrix * vec4(texcoord, 0, 1)).xy;
      gl_Position = u_matrix * position;
    }
  `;

  var fs = `
    #extension GL_EXT_frag_depth : require

    precision mediump float;

    varying vec2 v_texcoord;

    uniform sampler2D u_colorTexture;
    uniform sampler2D u_depthTexture;
    uniform float u_depthScale;
    uniform float u_depthOffset;

    void main() {
      vec4 color = texture2D(u_colorTexture, v_texcoord);
      if (color.a < 0.01) {
        discard;
      }

      float depth = texture2D(u_depthTexture, v_texcoord).r;
      gl_FragDepthEXT = u_depthOffset - depth * u_depthScale;
      gl_FragColor = color;
    }
  `;

  var programInfo = twgl.createProgramInfo(gl, [vs, fs]);
  var quadBufferInfo = twgl.createBufferInfoFromArrays(gl, {
    position: {
      numComponents: 2,
      data: [
        0, 0,
        0, 1,
        1, 0,
        1, 0,
        0, 1,
        1, 1,
      ],
    },
    texcoord: [
      0, 0,
      0, 1,
      1, 0,
      1, 0,
      0, 1,
      1, 1,
    ],
  });

  var ctx = document.createElement("canvas").getContext("2d");

  // make the color texture
  makeSprite(ctx, false);
  var colorTexture = twgl.createTexture(gl, {
    src: ctx.canvas,
    min: gl.NEAREST,
    mag: gl.NEAREST,
  });

  // make the depth texture
  makeSprite(ctx, true);
  var depthTexture = twgl.createTexture(gl, {
    src: ctx.canvas,
    format: gl.LUMINANCE,  // because depth is only 1 channel
    min: gl.NEAREST,
    mag: gl.NEAREST,
  });

  function drawDepthImage(
      colorTex, depthTex, texWidth, texHeight,
      x, y, z) {
    var dstY = y + z;
    var dstX = x;
    var dstWidth = texWidth;
    var dstHeight = texHeight;

    var srcX = 0;
    var srcY = 0;
    var srcWidth = texWidth;
    var srcHeight = texHeight;

    gl.useProgram(programInfo.program);

    twgl.setBuffersAndAttributes(gl, programInfo, quadBufferInfo);

    // this matirx will convert from pixels to clip space
    var matrix = m4.ortho(0, gl.canvas.width, gl.canvas.height, 0, -1, 1);

    // this matrix will translate our quad to dstX, dstY
    matrix = m4.translate(matrix, [dstX, dstY, 0]);

    // this matrix will scale our 1 unit quad
    // from 1 unit to texWidth, texHeight units
    matrix = m4.scale(matrix, [dstWidth, dstHeight, 1]);

    // just like a 2d projection matrix except in texture space (0 to 1)
    // instead of clip space. This matrix puts us in pixel space.
    var texMatrix = m4.scaling([1 / texWidth, 1 / texHeight, 1]);

    // because were in pixel space
    // the scale and translation are now in pixels
    var texMatrix = m4.translate(texMatrix, [srcX, srcY, 0]);
    var texMatrix = m4.scale(texMatrix, [srcWidth, srcHeight, 1]);

    twgl.setUniforms(programInfo, {
      u_colorTexture: colorTex,
      u_depthTexture: depthTex,
      u_matrix: matrix,
      u_textureMatrix: texMatrix,
      u_depthOffset: 1 - (dstY - z) / 65536,
      u_depthScale: 1 / 256,
    });

    twgl.drawBufferInfo(gl, quadBufferInfo);
  }

  // test render
  gl.enable(gl.DEPTH_TEST);

  var texWidth = 64;
  var texHeight = 64;

  // z is how much above/below ground
  function draw(x, y, z) {
    drawDepthImage(colorTexture, depthTexture, texWidth, texHeight , x, y, z);
  }

  draw(  0, 0, 0);  // draw on left

  draw(100, 0, 0);  // draw near center
  draw(113, 0, 0);  // draw overlapping

  draw(200, 0, 0);  // draw on right
  draw(200, 8, 0);  // draw on more forward

  draw(0, 60,  0);  // draw on left
  draw(0, 60, 10);  // draw on below

  draw(100, 60,  0);  // draw near center
  draw(100, 60, 20);  // draw below

  draw(200, 60, 20);  // draw on right
  draw(200, 60,  0);  // draw above
}

main();
&#13;
<script src="https://twgljs.org/dist/2.x/twgl-full.min.js"></script>
<canvas></canvas>
&#13;
&#13;
&#13;

左上角是图像的样子。顶部中间是并排绘制的2幅图像。右上角是在y中向下绘制的2个图像(x,y是等平面)。左下角是两个图像,一个在另一个下面绘制(在平面下方)。底部中间是分开更多的同一件事。右下角是相同的东西,除了以相反的顺序绘制(只是检查它是否有效)

为了节省内存,您可以将深度值放在颜色纹理的Alpha通道中。如果它丢弃了。

不幸的是,根据webglstats.com,只有75%的桌面和0%的手机支持EXT_frag_depth。虽然WebGL2需要支持gl_FragDepth和AFAIK,但大多数手机支持WebGL2所基于的OpenGL ES 3.0,因此在未来几个月内,大多数Android手机和大多数PC将获得WebGL2。另一方面,iOS与往常一样,Apple在将它们在iOS上发布WebGL2时是秘密。很明显,他们从未计划发布WebGL2,因为在2年多的时间里,WebGL没有为WebKit做过单一的提交。

对于不支持WebGL2上的WebGL2或EXT_frag_depth的系统,您可以使用顶点着色器模拟EXT_frag_depth。您将深度纹理传递到顶点着色器并使用gl.POINTS绘制,每个像素一个点。这样你就可以选择每个点的深度。

它可以工作,但最终可能会很慢。可能比在JavaScript中直接写入数组并使用Canvas2DRenderingContext.putImageData

更慢

这是一个例子

&#13;
&#13;
function makeDepthColor(depth) {
  return "rgb(" + depth + "," + depth + "," + depth + ")";
}

function makeSprite(ctx, depth) {
  // make an image (these would be made in photoshop ro
  // some other paint program but that's too much work for me
  ctx.canvas.width = 64;
  ctx.canvas.height = 64;
  for (y = 0; y <= 32; ++y) {
    var halfWidth = (y < 16 ? 1 + y : 33 - y) * 2;
    var width = halfWidth * 2;
    var cy = (16 - y);
    var cw = Math.max(0, 12 - Math.abs(cy) * 2) | 0;
    for (var x = 0; x < width; ++x) {
      var cx = x - halfWidth;
      var inCenter = Math.abs(cy) < 6 && Math.abs(cx) <= cw;
      var onEdge = x < 2 || x >= width - 2 || (inCenter && (Math.abs(cx / 2) | 0) === (cw / 2 | 0));
      var height = onEdge ? 12 : (inCenter ? 30 : 10);
      var color = inCenter ? (cx < 0 ? "#F44" : "#F66") : (cx < 0 ? "#44F" : "#66F");
      ctx.fillStyle = depth ? makeDepthColor(y + 1) : color;
      var xx = 32 - halfWidth + x;
      var yy = y;
      ctx.fillRect(xx, yy + 32 - height, 1, height);
      if (!depth) {
        ctx.fillStyle = onEdge ? "black" : "#CCF";
        ctx.fillRect(xx, yy + 32 - height, 1, 1);
      }
    }
  }
}

function main() {
  var m4 = twgl.m4;
  var gl = document.querySelector("canvas").getContext(
    "webgl", {preserveDrawingBuffer: true});
  var numVertexTextures = gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS);
  if (numVertexTextures < 2) {
    alert("GPU doesn't support textures in vertex shaders");
    return;
  }

  var vs = `
    attribute float count;

    uniform vec2 u_dstSize;
    uniform mat4 u_matrix;
    uniform mat4 u_textureMatrix;
    uniform sampler2D u_colorTexture;
    uniform sampler2D u_depthTexture;
    uniform float u_depthScale;
    uniform float u_depthOffset;

    varying vec4 v_color;

    void main() {
      float px = mod(count, u_dstSize.x);
      float py = floor(count / u_dstSize.x);

      vec4 position = vec4((vec2(px, py) + 0.5) / u_dstSize, 0, 1);
      vec2 texcoord = (u_textureMatrix * position).xy;

      float depth = texture2D(u_depthTexture, texcoord).r;

      gl_Position = u_matrix * position;
      gl_Position.z = u_depthOffset - depth * u_depthScale;
      v_color = texture2D(u_colorTexture, texcoord);
    }
  `;

  var fs = `
    precision mediump float;

    varying vec4 v_color;

    void main() {
      if (v_color.a < 0.01) {
        discard;
      }
      gl_FragColor = v_color;
    }
  `;
  
  // make a count
  var maxImageWidth = 256;
  var maxImageHeight = 256;
  var maxPixelsInImage = maxImageWidth * maxImageHeight
  var count = new Float32Array(maxPixelsInImage);
  for (var ii = 0; ii < count.length; ++ii) {
    count[ii] = ii;
  }

  var programInfo = twgl.createProgramInfo(gl, [vs, fs]);
  var quadBufferInfo = twgl.createBufferInfoFromArrays(gl, {
    count: { numComponents: 1, data: count, }
  });

  var ctx = document.createElement("canvas").getContext("2d");

  // make the color texture
  makeSprite(ctx, false);
  var colorTexture = twgl.createTexture(gl, {
    src: ctx.canvas,
    min: gl.NEAREST,
    mag: gl.NEAREST,
  });

  // make the depth texture
  makeSprite(ctx, true);
  var depthTexture = twgl.createTexture(gl, {
    src: ctx.canvas,
    format: gl.LUMINANCE,  // because depth is only 1 channel
    min: gl.NEAREST,
    mag: gl.NEAREST,
  });

  function drawDepthImage(
      colorTex, depthTex, texWidth, texHeight,
      x, y, z) {
    var dstY = y + z;
    var dstX = x;
    var dstWidth = texWidth;
    var dstHeight = texHeight;

    var srcX = 0;
    var srcY = 0;
    var srcWidth = texWidth;
    var srcHeight = texHeight;

    gl.useProgram(programInfo.program);

    twgl.setBuffersAndAttributes(gl, programInfo, quadBufferInfo);

    // this matirx will convert from pixels to clip space
    var matrix = m4.ortho(0, gl.canvas.width, gl.canvas.height, 0, -1, 1);

    // this matrix will translate our quad to dstX, dstY
    matrix = m4.translate(matrix, [dstX, dstY, 0]);

    // this matrix will scale our 1 unit quad
    // from 1 unit to texWidth, texHeight units
    matrix = m4.scale(matrix, [dstWidth, dstHeight, 1]);

    // just like a 2d projection matrix except in texture space (0 to 1)
    // instead of clip space. This matrix puts us in pixel space.
    var texMatrix = m4.scaling([1 / texWidth, 1 / texHeight, 1]);

    // because were in pixel space
    // the scale and translation are now in pixels
    var texMatrix = m4.translate(texMatrix, [srcX, srcY, 0]);
    var texMatrix = m4.scale(texMatrix, [srcWidth, srcHeight, 1]);

    twgl.setUniforms(programInfo, {
      u_colorTexture: colorTex,
      u_depthTexture: depthTex,
      u_matrix: matrix,
      u_textureMatrix: texMatrix,
      u_depthOffset: 1 - (dstY - z) / 65536,
      u_depthScale: 1 / 256,
      u_dstSize: [dstWidth, dstHeight],
    });

    var numDstPixels = dstWidth * dstHeight;
    twgl.drawBufferInfo(gl, quadBufferInfo, gl.POINTS, numDstPixels);
  }

  // test render
  gl.enable(gl.DEPTH_TEST);

  var texWidth = 64;
  var texHeight = 64;

  // z is how much above/below ground
  function draw(x, y, z) {
    drawDepthImage(colorTexture, depthTexture, texWidth, texHeight , x, y, z);
  }

  draw(  0, 0, 0);  // draw on left

  draw(100, 0, 0);  // draw near center
  draw(113, 0, 0);  // draw overlapping

  draw(200, 0, 0);  // draw on right
  draw(200, 8, 0);  // draw on more forward

  draw(0, 60,  0);  // draw on left
  draw(0, 60, 10);  // draw on below

  draw(100, 60,  0);  // draw near center
  draw(100, 60, 20);  // draw below

  draw(200, 60, 20);  // draw on right
  draw(200, 60,  0);  // draw above
}

main();
&#13;
<script src="https://twgljs.org/dist/2.x/twgl-full.min.js"></script>
<canvas></canvas>
&#13;
&#13;
&#13;

请注意,如果它太慢,我实际上并不认为在软件中使用JavaScript来保证它太慢。您可以使用asm.js来制作渲染器。您可以在JavaScript中设置和操作数据,然后调用asm.js例程来进行软件渲染。

作为示例this demo is entirely software rendered in asm.jsthis one

一样

如果结果太慢,则另一种方式需要某种3D数据用于2D图像。你可以使用立方体,如果2D图像总是立方体,但我已经可以从你的样本图片中看到这两个柜子需要一个3D模型,因为顶部比身体宽几个像素,而后面有一个支撑梁。

在任何情况下,假设您为对象制作3D模型,您将使用模板缓冲区+深度缓冲区。

  • 对于每个对象

    • 启用STENCIL_TESTDEPTH_TEST

      gl.enable(gl.STENCIL_TEST);
      gl.enable(gl.DEPTH_TEST);
      
    • 将模板func设置为ALWAYS,对迭代计数的引用和掩码为255

      var test = gl.ALWAYS;
      var ref = ndx;  // 1 for object 1, 2 for object 2, etc.
      var mask = 255;
      gl.stencilFunc(test, ref, mask);
      
    • 如果深度测试通过,则将模板操作设置为REPLACEKEEP否则

      var stencilTestFailOp = gl.KEEP;
      var depthTestFailOp = gl.KEEP;
      var bothPassOp = gl.REPLACE;
      gl.stencilOp(stencilTestFailOp, depthTestFailOp, bothPassOp);
      
    • 现在绘制您的立方体(或任何3D模型代表您的2D图像)

此时,模板缓冲区将在绘制立方体的任何地方都有ref的2D蒙版。因此,现在使用模板绘制2D图像,仅绘制成功绘制立方体的位置

  • 绘制图像

    • 关闭DEPTH_TEST

      gl.disable(gl.DEPTH_TEST);
      
    • 设置模板功能,因此我们只绘制模板等于ref的位置

      var test = gl.EQUAL;
      var mask = 255;
      gl.stencilFunc(test, ref, mask);
      
    • 针对所有情况将模板操作设置为KEEP

      var stencilTestFailOp = gl.KEEP;
      var depthTestFailOp = gl.KEEP;
      var bothPassOp = gl.KEEP;
      gl.stencilOp(stencilTestFailOp, depthTestFailOp, bothPassOp);
      
    • 绘制2D图像

      这将最终仅绘制多维数据集绘制的位置。

对每个对象重复。

您可能希望在每个对象之后或每254个对象之后清除模板缓冲区,并确保ref始终在1到255之间,因为模板缓冲区只有8位,这意味着当您绘制对象256时它将使用与对象#1相同的值,因此如果模板缓冲区中存在任何这些值,则可能会意外地在那里绘制。

 objects.forEach(object, ndx) {
    if (ndx % 255 === 0) {
      gl.clear(gl.STENCIL_BUFFER_BIT);
    }

    var ref = ndx % 255 + 1;  // 1 to 255

    ... do as above ...

答案 1 :(得分:1)

您可以为每个对象使用单独的预渲染深度贴图执行此操作。使用其他法线贴图,您也可以模拟延迟光照。但是这种技术需要为每个对象创建3D,以便为正确的交叉对象渲染漫反射,法线和深度图。

请参阅https://www.youtube.com/watch?v=-Q6ISVaM5Ww

上的XNA演示

此演示中的每个对象都只是一个带有漫反射,法线和深度贴图的渲染精灵。

在视频结束时,您会看到它是如何工作的。作者有一个额外的解释&amp; shader code-examples在他的博客https://infictitious.blogspot.de/2012/09/25d-xna-rpg-engine-some-technical.html

我认为WebGL也是如此。