Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

基于WebGL的GPGPU指南 - 各阶段实现 #29

Open
llwanghong opened this issue Apr 28, 2023 · 0 comments
Open

基于WebGL的GPGPU指南 - 各阶段实现 #29

llwanghong opened this issue Apr 28, 2023 · 0 comments

Comments

@llwanghong
Copy link
Owner

llwanghong commented Apr 28, 2023

整理译文的目录章节如下:

各阶段实现

上一节中,我们将通用GPGPU实现拆分为六个小概念。现在我们将为其中五个概念编写代码,光栅化步骤则会由GPU自动完成。当然,我们会将代码组织成清晰、模块化且高度可复用的函数。就像乐高积木一样,我们可以通过组装这些函数来设计构建自己的实现。

我们将方法放在GPGPUtility中。构造函数提供了定义函数的上下文。具体来说,函数将使用这里定义的实例变量,例如高度 $height$、宽度 $width$ 和渲染上下文 $gl$

/**
 * Set of functions to facilitate the setup and execution of GPGPU tasks.
 *
 * @param {integer} width_  The width (x-dimension) of the problem domain.
 *                          Normalized to s in texture coordinates.
 * @param {integer} height_ The height (y-dimension) of the problem domain.
 *                          Normalized to t in texture coordinates.
 *
 * @param {WebGLContextAttributes} attributes_ A collection of boolean values to enable or disable various WebGL features.
 *                                             If unspecified, STANDARD_CONTEXT_ATTRIBUTES are used.
 *                                             @see STANDARD_CONTEXT_ATTRIBUTES
 *                                             @see{@link https://www.khronos.org/registry/webgl/specs/latest/1.0/#5.2}
 */
GPGPUtility = function (width_, height_, attributes_)
{
  var attributes;
  var canvas;
  /** @member {WebGLRenderingContext} gl The WebGL context associated with the canvas. */
  var gl;
  var canvasHeight, canvasWidth;
  var problemHeight, problemWidth;
  var standardVertexShader;
  var standardVertices;
  /** @member {Object} Non null if we enable OES_texture_float. */
  var textureFloat;
  /**
  ⋮ The code in the following paragraphs goes here.
  */
  canvasHeight = height_;
  problemHeight = canvasHeight;
  canvasWidth = width_;
  problemWidth = canvasWidth;
  attributes = typeof attributes_ === 'undefined' ? GPGPUtility.STANDARD_CONTEXT_ATTRIBUTES : attributes_;
  canvas = this.makeGPCanvas(canvasWidth, canvasHeight);
  gl = this.getGLContext();
  // Attempt to activate the extension, returns null if unavailable
  textureFloat  = gl.getExtension('OES_texture_float');
};

// Disable attributes unused in computations.
GPGPUtility.STANDARD_CONTEXT_ATTRIBUTES = { alpha: false, depth: false, antialias: false };

画布(Canvas)

即使第一步中我们已可以看到与常规 $WebGL$ 用法不同的地方。我们创建了画布,但是并没有将画布附加到 $DOM$ 上。只有当想将其渲染到屏幕上时,才需要将画布附加到 $DOM$ 上。许多纯计算问题可能永远不会渲染到屏幕上。

/**
 * Create a canvas for computational use. Computations don't
 * require attachment to the DOM.
 *
 * @param {integer} width The width (x-dimension) of the problem domain.
 * @param {integer} height The height (y-dimension) of the problem domain.
 *
 * @returns {HTMLCanvasElement} A canvas with the given height and width.
 */
this.makeGPCanvas = function (width, height)
{
  var canvas;

  canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;

  return canvas;
};

当获取WebGL上下文时,我们提供一组属性来禁用不会使用的功能。如果没有显式提供这些属性,会默认禁用一组标准功能。

// Disable attributes unused in computations.
GPGPUtility.STANDARD_CONTEXT_ATTRIBUTES = { alpha: false, depth: false, antialias: false };
      
/**
 * Get a 3d context, webgl or experimental-webgl. The context presents a
 * javascript API that is used to draw into it. The webgl context API is
 * very similar to OpenGL for Embedded Systems, or OpenGL ES.
 *
 * @returns {WebGLRenderingContext} A manifestation of OpenGL ES in JavaScript.
 */
this.getGLContext = function ()
{
  // Only fetch a gl context if we haven't already
  if(!gl)
  {
    gl = canvas.getContext("webgl", attributes)
      || canvas.getContext('experimental-webgl', attributes);
  }

  return gl;
};

几何形状

一个三角形带中的四个顶点覆盖了整个画布。x 和 y 是归一化的设备坐标。s 和 t 是纹理坐标。

覆盖画布最简单的几何形状是一个矩形,每个角都有一个顶点。幸运的是,在归一化设备坐标中,画布的各角坐标为 $(-1, -1)$$(1, -1)$$(1, 1)$$(-1, 1)$,顶点着色器会原样传递这些坐标,并不会进行任何投影或其它修改。

回想一下,问题网格与画布像素和纹理元素都是精确匹配的。这意味着我们也要将纹理附加到画布各角上。这可能有点令人困惑,因为纹理坐标是另一种坐标系。纹理坐标的范围从 $(0, 0)$,在 $(-1, -1)$ 顶点上,到 $(1, 1)$,在 $(1, 1)$顶点上。

/**
 * Return a standard geometry with texture coordinates for GPGPU calculations.
 * A simple triangle strip containing four vertices for two triangles that
 * completely cover the canvas. The included texture coordinates range from
 * (0, 0) in the lower left corner to (1, 1) in the upper right corner.
 *
 * @returns {Float32Array} A set of points and textures suitable for a two triangle
 *                         triangle fan that forms a rectangle covering the canvas
 *                         drawing surface.
 */
this.getStandardGeometry = function ()
{
  // Sets of x,y,z(=0),s,t coordinates.
  return new Float32Array([
    -1.0,  1.0, 0.0, 0.0, 1.0,  // upper left
    -1.0, -1.0, 0.0, 0.0, 0.0,  // lower left
    1.0,  1.0, 0.0, 1.0, 1.0,  // upper right
    1.0, -1.0, 0.0, 1.0, 0.0]);// lower right
};

创建纹理

普通纹理中的像素颜色数据,红 $red$、绿 $green$、蓝 $blue$ 以及透明 $alpha$ 每个参数通道有8位,所以每个像素的颜色会占用一个 $32$ 位字。浮点纹理则为 $RGB$$A$ 每个通道都分配了一个完整的32位浮点数。这样我们就能够轻松地使用一个纹理元素来存储浮点数。我们将利用这个机制来存储计算数据。正如将会看到的,我们也需要谨慎,因为浮点纹理是WebGL的一个可选部分。

OES_texture_float$OpenGL\ ES$ (因此也是 $WebGL$ )的扩展。这是 $OpenGL$ 的一个可选部分,可能不存在,并且需要使用 $getExtension$ 激活。如果扩展不可用, $getExtension$ 返回 $null$。后面我们将看到即使在浮点纹理不可用的情况下如何利用 $GPU$

// Non null if we enable OES_texture_float
var textureFloat;

// Attempt to activate the extension, returns null if unavailable
textureFloat = gl.getExtension('OES_texture_float');

/**
 * Check if floating point textures are available. This is an optional feature,
 * and even if present are usually not usable as a rendering target.
 */
this.isFloatingTexture = function()
{
  return textureFloat != null;
};

既然现在我们已启用了浮点纹理,那就来创建一个。我们使用 $createTexture$ 创建纹理,然后使用 $bindTexture$ 绑定使其成为当前活动纹理。

我们设置了一些选项可以使纹理更适合存储计算数据。将 $TEXTURE\_MIN\_FILTER$$TEXTURE\_MAG\_FILTER$ 都设置为 $NEAREST$,以避免纹理尺寸比几何形状更小或更大时产生问题。当纹理映射到一个比纹理更小或更大的几何形状时,这些设置就会发挥作用。但请记住,我们要保证画布、几何形状和纹理大小匹配同步,所以我们并不期望这些设置会真正发挥作用。任何情况下都不会期望在纹理值之间进行插值。

如果我们读取超过纹理边缘的值,则希望得到边缘的值。我们通过将 $TEXTURE\_WRAP\_S$$TEXTURE\_WRAP\_T$ 设置为 $CLAMP\_TO\_EDGE$ 来实现这一点。请记住, $s$$t$ 是归一化的纹理坐标。同样,我们实际上不期望 $GPU$ 计算中会真正使用这个功能。

$texImage2D$ 设置纹理的格式,并可选地设置纹理的数据。这次我们将设置一个 $RGBA$ 浮点纹理。这意味着 $R$$G$$B$$A$ 每个通道都包含一个浮点数。现在我们只会使用其中一个,但稍后将看到,通过利用所有四个通道可以实现一些显著的性能优化。

/**
 * Create a width x height texture of the given type for computation.
 * Width and height must be powers of two.
 *
 * @param {WebGLRenderingContext} The WebGL context for which we will create the texture.
 * @param {integer} width The width of the texture in pixels. Normalized to s in texture coordinates.
 * @param {integer} height The height of the texture in pixels. Normalized to t in texture coordinates.
 * @param {number} type A valid texture type. FLOAT, UNSIGNED_BYTE, etc.
 * @param {number[] | null} data Either texture data, or null to allocate the texture but leave the texels undefined.
 *
 * @returns {WebGLTexture} A reference to the created texture on the GPU.
 */
this.makeTexture = function (gl, width, height, type, data)
{
  var texture;

  // Create the texture
  texture = gl.createTexture();
  // Bind the texture so the following methods effect this texture.
  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  // Pixel format and data for the texture
  gl.texImage2D(
    gl.TEXTURE_2D, // Target, matches bind above.
    0,             // Level of detail.
    gl.RGBA,       // Internal format.
    width,         // Width - related to s on textures.
    height,        // Height - related to t on textures.
    0,             // Always 0 in OpenGL ES.
    gl.RGBA,       // Format for each pixel.
    type,          // Data type for each chanel.
    data);         // Image data in the described format, or null.
  // Unbind the texture.
  gl.bindTexture(gl.TEXTURE_2D, null);

  return texture;
};

输出纹理

通常来说,片段着色器的结果会绘制到屏幕上并且 $gl\_FragColor$ 的值就是像素的颜色。然而,我们将 $gl\_FragColor$ 捕获到纹理元素 $(texel)$ 中。因为画布、几何形状以及纹理的尺寸和位置都是匹配对齐的,所以我们甚至可以知道正在为哪个 $texel$ 赋值。

由于与画布尺寸匹配对齐,纹理的几何形状定义非常清晰。

纹理的 $x$ 坐标 $s$,从 $0$ 变化到 $1$,而画布从 $0$ 变化到 $canvas.width$。同样,纹理的 $y$ 坐标从 $0$ 变化到 $1$,而画布从 $0$ 变化到 $canvas.height$。我们因此可得到纹理的一些重要结论信息。

纹理元素之间的 $x$ 间距 $\delta_s$$y$ 间距 $\delta_t$,分别为:

$$\delta_s = \frac{1}{canvas.width}$$

$$\delta_t = \frac{1}{canvas.height}$$

在片段着色器中,我们可以访问到纹理坐标,从这个坐标可以准确地知道正在写入哪个纹理元素。具体来说,

$$i = floor\left( canvas.width \times s \right)$$

$$j = floor\left( canvas.height \times t \right)$$

最后,我们将纹理设置为渲染目标。 $OpenGL$ 始终渲染到帧缓冲区。我们创建自己的帧缓冲区对象 $(FBO)$,然后将其绑定为 $GPGPU$ 处理帧缓冲区操作的目标,比如渲染或附加一个纹理进行离屏渲染。

/**
 * Create and bind a framebuffer, then attach a texture.
 *
 * @param {WebGLRenderingContext} gl The WebGL context associated with the framebuffer and texture.
 * @param {WebGLTexture} texture The texture to be used as the buffer in this framebuffer object.
 *
 * @returns {WebGLFramebuffer} The framebuffer
 */
this.attachFrameBuffer = function (gl, texture)
{
  var frameBuffer;

  // Create a framebuffer
  frameBuffer = gl.createFramebuffer();
  // Make it the target for framebuffer operations - including rendering.
  gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer);
  // Our texture is the target of rendering ops now.
  gl.framebufferTexture2D(
    gl.FRAMEBUFFER,       // The target is always a FRAMEBUFFER.
    gl.COLOR_ATTACHMENT0, // We are providing the color buffer.
    gl.TEXTURE_2D,        // This is a 2D image texture.
    texture,              // The texture.
    0);                   // 0, we aren't using MIPMAPs

  return frameBuffer;
};

为了确保帧缓冲区对象可用,我们还必须调用 $checkFramebufferStatus$。如果返回的结果不是 $FRAMEBUFFER\_COMPLETE$,则代表帧缓冲区设置失败了。这一点尤其重要,因为我们假设可以将渲染结果写入浮点纹理。这是确定平台是否允许这样做的最早时刻。实际上这也是帧缓冲区不完整响应的最常见原因。

/**
 * Check the framebuffer status. Return false if the framebuffer is not complete,
 * That is if it is not fully and correctly configured as required by the current
 * hardware. True indicates that the framebuffer is ready to be rendered to.
 *
 * @returns {boolean} True if the framebuffer is ready to be rendered to. False if not.
 */
this.frameBufferIsComplete = function ()
{
  var message;
  var status;
  var value;

  status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);

  switch (status)
  {
    case gl.FRAMEBUFFER_COMPLETE:
      value = true;
      break;
    case gl.FRAMEBUFFER_UNSUPPORTED:
      message = "Framebuffer is unsupported";
      value = false;
      break;
    case gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
      message = "Framebuffer incomplete attachment";
      value = false;
      break;
    case gl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS:
      message = "Framebuffer incomplete (missmatched) dimensions";
      value = false;
      break;
    case gl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:
      message = "Framebuffer incomplete missing attachment";
      value = false;
      break;
    default:
      message = "Unexpected framebuffer status: " + status;
      value = false;
  }
  return {isComplete: value, message: message};
};
@llwanghong llwanghong changed the title 基于 WebGL 的 GPGPU 指南 - 各阶段实现 基于WebGL的GPGPU指南 - 各阶段实现 Apr 29, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant