使用 webgl 绘制高分辨率 canvas

high resolution draw canvas with webgl

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

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

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

有没有办法从 webgl canvas 获取高分辨率的快照?我可以使用其他解决方案吗?

使用 gl.getParameter(gl.MAX_RENDERBUFFER_SIZE) 检查您的最大渲染缓冲区大小。您的 canvas 不能大于此。试图打破限制会让你失去背景。

如果您想要更大的屏幕截图,可以将其渲染成多个 steps/tiles。调整投影变换来实现这一点应该相当容易。

4000x4000 像素是 4000x4000x4 或 64meg 内存。 8000x8000 是 256 兆内存。浏览器不喜欢分配那么大的内存块,并且经常在页面上设置限制。例如,您有一个 8000x8000 WebGL canvas,它需要 2 个缓冲区。页面上显示的绘图缓冲区和纹理。绘图缓冲区可能是抗锯齿的。如果它是 4x MSAA,那么它只需要为该缓冲区提供大量内存。然后你截屏,这样就需要另外 256meg 的内存。所以是的,浏览器出于某种原因可能会终止您的页面。

除此之外,WebGL 有其自身的大小限制。您可以查找有效的限制 MAX_TEXTURE_SIZE or MAX_VIEWPORT_DIMS. You can see from those about 40% of machines can't drawing larger than 4096 (although if you 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

在八个 100x100 部分中以 400x200 渲染的相同代码

"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) {
   ...

我们将 canvas 设置为零件的尺寸

  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.
  }
}

这可以让您渲染成任何您想要的尺寸,但您需要一些其他方法来 assemble 将图像变成一个更大的图像。您可能会也可能不会在浏览器中执行此操作。您可以尝试制作一个巨大的 2D canvas 并将每个部分绘制到其中(假设 2d canvas 没有与 WebGL 相同的限制)。为此,无需制作图像,只需将 webgl canvas 绘制到 2d canvas.

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

或者,如果您只想显示它们,浏览器处理 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 图像,那么还有一个解决方案是使用上面的部分渲染代码,并将每行(或多行)图像的数据写入手动生成 PNG 的 blob。 This blog post covers manually generating PNGs from data and .

更新:

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