高分辨率使用webgl绘制画布

时间:2018-07-08 12:31:15

标签: canvas webgl snapshot high-resolution

我想获取我的webgl画布的快照,并且想要高分辨率捕获,所以我增加了画布的大小。这将自动更改gl.draingBufferWidthgl.draingBufferWidth。然后设置视口,然后渲染场景。

我的代码在低分辨率(4000 * 4000以下)下可以正常工作,但在高分辨率下存在很多问题。

如果分辨率稍高,则快照不会完全显示。查看附件。如果分辨率提高更多,则不会显示任何内容。最后,在某些解决方案下,我的webgl实例被破坏了,我必须重新启动浏览器才能再次运行webgl

有什么方法可以从webgl画布获取高分辨率的快照吗?我可以使用其他解决方案吗?

2 个答案:

答案 0 :(得分:1)

4000x4000像素是4000x4000x4或64兆像素的内存。 8000x8000是256兆的内存。浏览器不喜欢分配那么大的内存,并且经常在页面上设置限制。因此,例如,您有一个8000x8000 WebGL画布,它需要2个缓冲区。页面上显示的drawingbuffer和纹理。绘图缓冲区可能是抗锯齿。如果它是MSAA的4倍,那么仅该缓冲区就需要一个内存。然后拍摄一张屏幕截图,以便再增加256兆的内存。因此,是的,浏览器出于某种原因很可能会杀死您的页面。

除了WebGL之外,它还有自己的大小限制。您可以查找实际上是MAX_TEXTURE_SIZEMAX_VIEWPORT_DIMS的限制。从中可以看出,大约40%的机器不能绘制大于4096的图形(尽管如果filter to desktop only it's much better,则可以绘制)。该数字仅表示硬件可以执行的操作。仍然受内存限制。

一种可能解决此问题的方法是将图像分成几部分绘制。如何执行将取决于您的应用程序。如果对所有渲染都使用相当标准的透视矩阵,则可以使用略有不同的数学来渲染视图的任何部分。大多数3d数学库都具有perspective函数,并且大多数还具有相应的frustum函数,该函数稍微灵活一些。

这是一个相当标准的WebGL简单样式示例,它使用典型的perspective函数绘制一个立方体

"use strict";

const vs = `
uniform mat4 u_worldViewProjection;

attribute vec4 position;
attribute vec3 normal;

varying vec3 v_normal;

void main() {
  v_normal = normal;
  gl_Position = u_worldViewProjection * position;
}
`;
const fs = `
precision mediump float;

varying vec3 v_normal;

void main() {
  gl_FragColor = vec4(v_normal * .5 + .5, 1);
}
`;

const m4 = twgl.m4;
const gl = document.querySelector("canvas").getContext("webgl");
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);

const bufferInfo = twgl.primitives.createCubeBufferInfo(gl, 2);

twgl.resizeCanvasToDisplaySize(gl.canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

gl.enable(gl.DEPTH_TEST);
gl.enable(gl.CULL_FACE);
gl.clearColor(0.2, 0.2, 0.2, 1);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

const fov = 30 * Math.PI / 180;
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const zNear = 0.5;
const zFar = 10;
const projection = m4.perspective(fov, aspect, zNear, zFar);
const eye = [1, 4, -6];
const target = [0, 0, 0];
const up = [0, 1, 0];

const camera = m4.lookAt(eye, target, up);
const view = m4.inverse(camera);
const viewProjection = m4.multiply(projection, view);
const world = m4.rotationY(Math.PI * .33);

gl.useProgram(programInfo.program);
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
twgl.setUniforms(programInfo, {
  u_worldViewProjection: m4.multiply(viewProjection, world),
});
twgl.drawBufferInfo(gl, bufferInfo);
<canvas></canvas>
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>

这是使用典型的frustum函数而不是perspective

在400x200上以八个100x100部分进行相同的代码渲染的方法

"use strict";

const vs = `
uniform mat4 u_worldViewProjection;

attribute vec4 position;
attribute vec3 normal;

varying vec3 v_normal;

void main() {
  v_normal = normal;
  gl_Position = u_worldViewProjection * position;
}
`;
const fs = `
precision mediump float;

varying vec3 v_normal;

void main() {
  gl_FragColor = vec4(v_normal * .5 + .5, 1);
}
`;

const m4 = twgl.m4;
const gl = document.createElement("canvas").getContext("webgl");
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);

const bufferInfo = twgl.primitives.createCubeBufferInfo(gl, 2);

// size to render
const totalWidth = 400;
const totalHeight = 200;
const partWidth = 100;
const partHeight = 100;

// this fov is for the totalHeight
const fov = 30 * Math.PI / 180;
const aspect = totalWidth / totalHeight;
const zNear = 0.5;
const zFar = 10;

const eye = [1, 4, -6];
const target = [0, 0, 0];
const up = [0, 1, 0];

// since the camera doesn't change let's compute it just once
const camera = m4.lookAt(eye, target, up);
const view = m4.inverse(camera);
const world = m4.rotationY(Math.PI * .33);

const imgRows = []; // this is only to insert in order
for (let y = 0; y < totalHeight; y += partHeight) {
  const imgRow = [];
  imgRows.push(imgRow)
  for (let x = 0; x < totalWidth; x += partWidth) {
    renderPortion(totalWidth, totalHeight, x, y, partWidth, partHeight);
    const img = new Image();
    img.src = gl.canvas.toDataURL();
    imgRow.push(img);
  }
}

// because webgl goes positive up we're generating the rows
// bottom first
imgRows.reverse().forEach((imgRow) => {
  imgRow.forEach(document.body.appendChild.bind(document.body));
  document.body.appendChild(document.createElement("br"));
});

function renderPortion(totalWidth, totalHeight, partX, partY, partWidth, partHeight) {
  gl.canvas.width = partWidth;
  gl.canvas.height = partHeight;
  
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
  
  gl.enable(gl.DEPTH_TEST);
  gl.enable(gl.CULL_FACE);
  gl.clearColor(0.2, 0.2, 0.2, 1);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

  // corners at zNear for tital image
  const zNearTotalTop = Math.tan(fov) * 0.5 * zNear;
  const zNearTotalBottom = -zNearTotalTop;
  const zNearTotalLeft = zNearTotalBottom * aspect;
  const zNearTotalRight = zNearTotalTop * aspect;
  
  // width, height at zNear for total image
  const zNearTotalWidth = zNearTotalRight - zNearTotalLeft;
  const zNearTotalHeight = zNearTotalTop - zNearTotalBottom;
  
  const zNearPartLeft = zNearTotalLeft + partX * zNearTotalWidth / totalWidth;   const zNearPartRight = zNearTotalLeft + (partX + partWidth) * zNearTotalWidth / totalWidth;
  const zNearPartBottom = zNearTotalBottom + partY * zNearTotalHeight / totalHeight;
  const zNearPartTop = zNearTotalBottom + (partY + partHeight) * zNearTotalHeight / totalHeight;

  const projection = m4.frustum(zNearPartLeft, zNearPartRight, zNearPartBottom, zNearPartTop, zNear, zFar);
  const viewProjection = m4.multiply(projection, view);

  gl.useProgram(programInfo.program);
  twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
  twgl.setUniforms(programInfo, {
    u_worldViewProjection: m4.multiply(viewProjection, world),
  });
  twgl.drawBufferInfo(gl, bufferInfo);
}
img { border: 1px solid red; }
body { line-height: 0 }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>

如果运行上面的代码段,您会看到它正在生成8张图像

重要的部分是这个

首先,我们需要确定所需的总大小

const totalWidth = 400;
const totalHeight = 200;

然后我们将创建一个函数,该函数将呈现该大小的任何较小部分

function renderPortion(totalWidth, totalHeight, partX, partY, partWidth, partHeight) {
   ...

我们将画布设置为零件的大小

  gl.canvas.width = partWidth;
  gl.canvas.height = partHeight;

  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

然后计算我们需要传递给frustum函数的内容。首先,我们在zNear处计算一个矩形,该矩形将根据给定的视场,宽高比和zNear值生成透视矩阵。

  // corners at zNear for total image
  const zNearTotalTop = Math.tan(fov) * 0.5 * zNear;
  const zNearTotalBottom = -zNearTotalTop;
  const zNearTotalLeft = zNearTotalBottom * aspect;
  const zNearTotalRight = zNearTotalTop * aspect;

  // width, height at zNear for total image
  const zNearTotalWidth = zNearTotalRight - zNearTotalLeft;
  const zNearTotalHeight = zNearTotalTop - zNearTotalBottom;

然后,我们在zNear处为要渲染的部分计算相应的面积,并将其传递给frustum以生成投影矩阵。

  const zNearPartLeft = zNearTotalLeft + partX * zNearTotalWidth / totalWidth;   const zNearPartRight = zNearTotalLeft + (partX + partWidth) * zNearTotalWidth / totalWidth;
  const zNearPartBottom = zNearTotalBottom + partY * zNearTotalHeight / totalHeight;
  const zNearPartTop = zNearTotalBottom + (partY + partHeight) * zNearTotalHeight / totalHeight;

  const projection = m4.frustum(zNearPartLeft, zNearPartRight, zNearPartBottom, zNearPartTop, zNear, zFar);

然后我们就像正常渲染一样

最后在外面,我们有一个循环,可以使用刚生成的函数以所需的任意分辨率渲染任意数量的零件。

const totalWidth = 400;
const totalHeight = 200;
const partWidth = 100;
const partHeight = 100;

for (let y = 0; y < totalHeight; y += partHeight) {
  for (let x = 0; x < totalWidth; x += partWidth) {
    renderPortion(totalWidth, totalHeight, x, y, partWidth, partHeight);
    const img = new Image();
    img.src = gl.canvas.toDataURL();
    // do something with image.
  }
}

这将使您可以渲染为所需的任何大小,但需要其他方法将图像组合成一个较大的图像。您可能会或可能不会在浏览器中执行此操作。您可以尝试制作一个巨大的2D画布并在其中绘制每个零件(假设2D画布与WebGL的限制不同)。为此,无需制作图像,只需将webgl画布绘制到2d画布中即可。

否则,您可能必须将它们发送到您创建的服务器以组装图像,或者根据用例,让用户保存它们并将它们全部加载到图像编辑程序中。

或者,如果您只想显示它们,则浏览器对16x16 1024x1024图像的效果可能会比一张16kx16k图片更好。在这种情况下,您可能想调用canvas.toBlob而不是使用dataURL,然后为每个Blob调用URL.createObjectURL。这样一来,您就不会再出现这些巨大的dataURL字符串了。

示例:

"use strict";

const vs = `
uniform mat4 u_worldViewProjection;

attribute vec4 position;
attribute vec3 normal;

varying vec3 v_normal;

void main() {
  v_normal = normal;
  gl_Position = u_worldViewProjection * position;
}
`;
const fs = `
precision mediump float;

varying vec3 v_normal;

void main() {
  gl_FragColor = vec4(v_normal * .5 + .5, 1);
}
`;

const m4 = twgl.m4;
const gl = document.createElement("canvas").getContext("webgl");
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);

const bufferInfo = twgl.primitives.createCubeBufferInfo(gl, 2);

// size to render
const totalWidth = 16384;
const totalHeight = 16385;
const partWidth = 1024;
const partHeight = 1024;

// this fov is for the totalHeight
const fov = 30 * Math.PI / 180;
const aspect = totalWidth / totalHeight;
const zNear = 0.5;
const zFar = 10;

const eye = [1, 4, -6];
const target = [0, 0, 0];
const up = [0, 1, 0];

// since the camera doesn't change let's compute it just once
const camera = m4.lookAt(eye, target, up);
const view = m4.inverse(camera);
const world = m4.rotationY(Math.PI * .33);

const imgRows = []; // this is only to insert in order
for (let y = 0; y < totalHeight; y += partHeight) {
  const imgRow = [];
  imgRows.push(imgRow)
  for (let x = 0; x < totalWidth; x += partWidth) {
    renderPortion(totalWidth, totalHeight, x, y, partWidth, partHeight);
    const img = new Image();
    gl.canvas.toBlob((blob) => {
      img.src = URL.createObjectURL(blob);
    });
    imgRow.push(img);
  }
}

// because webgl goes positive up we're generating the rows
// bottom first
imgRows.reverse().forEach((imgRow) => {
  const div = document.createElement('div');
  imgRow.forEach(div.appendChild.bind(div));
  document.body.appendChild(div);
});

function renderPortion(totalWidth, totalHeight, partX, partY, partWidth, partHeight) {
  gl.canvas.width = partWidth;
  gl.canvas.height = partHeight;
  
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
  
  gl.enable(gl.DEPTH_TEST);
  gl.enable(gl.CULL_FACE);
  gl.clearColor(0.2, 0.2, 0.2, 1);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

  // corners at zNear for tital image
  const zNearTotalTop = Math.tan(fov) * 0.5 * zNear;
  const zNearTotalBottom = -zNearTotalTop;
  const zNearTotalLeft = zNearTotalBottom * aspect;
  const zNearTotalRight = zNearTotalTop * aspect;
  
  // width, height at zNear for total image
  const zNearTotalWidth = zNearTotalRight - zNearTotalLeft;
  const zNearTotalHeight = zNearTotalTop - zNearTotalBottom;
  
  const zNearPartLeft = zNearTotalLeft + partX * zNearTotalWidth / totalWidth;   const zNearPartRight = zNearTotalLeft + (partX + partWidth) * zNearTotalWidth / totalWidth;
  const zNearPartBottom = zNearTotalBottom + partY * zNearTotalHeight / totalHeight;
  const zNearPartTop = zNearTotalBottom + (partY + partHeight) * zNearTotalHeight / totalHeight;

  const projection = m4.frustum(zNearPartLeft, zNearPartRight, zNearPartBottom, zNearPartTop, zNear, zFar);
  const viewProjection = m4.multiply(projection, view);

  gl.useProgram(programInfo.program);
  twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
  twgl.setUniforms(programInfo, {
    u_worldViewProjection: m4.multiply(viewProjection, world),
  });
  twgl.drawBufferInfo(gl, bufferInfo);
}
img { border: 1px solid red; }
div { white-space: nowrap; }
body { line-height: 0 }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>

如果您希望用户能够下载16386x16386图像而不是256个1024x1024图像,那么另一种解决方案是使用上面的部分渲染代码,并对图像的每一行(或多行)将其数据写入blob手动生成PNG。 This blog post涵盖了根据数据和this answer suggests how to do it for very large data手动生成PNG。

更新:

我只是为了好玩而写了library to help generate giant pngs in the browser

答案 1 :(得分:-1)

使用gl.getParameter(gl.MAX_RENDERBUFFER_SIZE)检查最大渲染缓冲区大小。您的画布不能大于该值。尝试突破限制可能会使您失去上下文。

如果您想要更大的屏幕截图,可以分多个步骤/平铺显示。调整投影变换以实现此目标应该相当容易。