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指南 - 初始化矩阵 #34

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

基于WebGL的GPGPU指南 - 初始化矩阵 #34

llwanghong opened this issue Apr 29, 2023 · 0 comments

Comments

@llwanghong
Copy link
Owner

llwanghong commented Apr 29, 2023

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

初始化矩阵

现在已经有了一个精美封装的库,我们在一个示例问题上应用一下。矩阵初始化是一个很好的例子,因为它能直接将一个矩阵映射到一个网格,并且片段着色器可以直接根据行列索引计算矩阵元素而不使用输入纹理。这使得初始化比后续的计算示例会稍微简单一些。这种初始化条件的设置在我们将要研究的许多问题中也很常见。

问题大部分代码在另一个文件MatrixInitializer.js中,我们将在下面几段内容中进行介绍。这种模块化使得页面上的代码简短且易于理解。

这段代码汇集了我们一直在讨论的许多概念,并将它们组合成一个完整的应用程序。

"use strict";

var bufferStatus;
var framebuffer;
var gpgpUtility;
var initializer;
var matrixColumns;
var matrixRows;
var texture;

matrixColumns = 128;
matrixRows = 128;
gpgpUtility = new vizit.utility.GPGPUtility(matrixColumns, matrixRows, { premultipliedAlpha: false });

if (GPGPUtility.isFloatingTexture())
{
  // Height and width are set in the constructor.
  texture = gpgpUtility.makeTexture(WebGLRenderingContext.FLOAT, null);
  framebuffer = gpgpUtility.attachFrameBuffer(texture);

  bufferStatus = gpgpUtility.frameBufferIsComplete();

  if (bufferStatus.isComplete)
  {
    initializer = new MatrixInitializer(gpgpUtility);
    initializer.initialize(matrixColumns, matrixRows);

    // Delete resources no longer in use.
    initializer.done();

    // Tests, terminate on first failure.
    initializer.test(  0,   0)
    && initializer.test( 10,  12)
    && initializer.test(100, 100);
  }
  else
  {
    alert(bufferStatus.message);
  }
}
else
{
  alert("Floating point textures are not supported.");
}

画布尺寸是根据问题规模来指定的,这个例子中是矩阵的列数 $matrixColumns$ 和行数 $matrixRows$。这比使用 $canvasWidth$$canvasHeight$ 变量更清晰。

创建画布和 $WebGL$ 上下文是如此标准,以至于我们将它们完全封装在 $GPGPUtility$ 类中。 $GPGPUtility$ 构造函数执行 $makeGPCanvas$ 函数,调用 $getGLContext$ 获取并存储 $WebGL$ 上下文,最后构造函数执行 $gl.getExtension$ 启用浮点纹理扩展。

$GPGPUtility$ 构造函数返回后,我们立即检查是否启用了浮点纹理。如果启用成功,下一步使用 $gpgpUtility.makeTexture$ 创建浮点纹理。空参数表示不为纹理提供任何数据1

后面章节我们将展示当浮点纹理不可用时如何将浮点值打包到四个无符号字节 $(RGBA)$ 中。

我们需要纹理作为 $GPU$ 的输出目标。我们通过调用 $gpgpUtility.attachFrameBuffer$ 使纹理成为渲染目标,该函数创建一个帧缓冲区并将纹理附加到它上面。当我们的代码运行时,结果将被放入纹理中,而不是绘制到屏幕上。

到目前为止,我们还没有涉及到任何问题特定的代码,但已经可以确保我们的设置可以在此系统上工作。 $gpgpUtility.frameBufferIsComplete$ 方法返回帧缓冲区状态和描述性文本。

$bufferStatus.isComplete$ 为真时,我们可以断定两件事:

  1. 我们可以创建浮点纹理
  2. 我们可以写入或渲染浮点纹理

$bufferStatus.isComplete$ 检查失败时,帧缓冲区无法使用。在这种情况下,我们会显示 $bufferStatus.message$ 中的描述并终止程序。我们后面在设置失败时将使用这个结果来做更多事情,而不仅仅是显示一条消息。我们将使用一种不同的方法来解决问题,尽管效率较低,但不依赖于浮点纹理。

矩阵初始化阶段在MatrixInitializer.js中,定义了一个 $MatrixInitializer$ 类。

$MatrixInitializer$ 继续使用模块化模式,以小的容易理解的代码块构建形成一个完整的解决方案,解决一开始看起来很复杂的问题。构造函数只包含三行可执行代码。

function MatrixInitializer(gpgpUtility_)
{
  /** WebGLRenderingContext */
  var gl;
  var gpgpUtility;
  var heightHandle;
  var positionHandle;
  var program;
  var textureCoordHandle;
  var widthHandle;
  .
  .
  .
  gpgpUtility = gpgpUtility_;
  gl = gpgpUtility.getGLContext();
  program = this.createProgram(gl);
}

我们将要解决的所有问题的几何形状和顶点着色器都是相同的。这使得程序可以得到相当大的简化。我们使用一个标准的顶点着色器,并提供片段着色器,这是所有问题具体代码所在的地方。我们通过将第一个参数置为 $null$ 的方式调用 $gpgpUtility.createProgram$ 来设置着色器,这样 $gpgpUtility$ 就会使用标准的顶点着色器。

/**
 * Compile shaders and link them into a program, then retrieve references to the
 * attributes and uniforms. The standard vertex shader, which simply passes on the
 * physical and texture coordinates, is used.
 *
 * @returns {WebGLProgram} The created program object.
 * @see {https://www.khronos.org/registry/webgl/specs/1.0/#5.6|WebGLProgram}
 */
this.createProgram = function (gl)
{
  var fragmentShaderSource;
  var program;

  // Note that the preprocessor requires the newlines.
  fragmentShaderSource = "#ifdef GL_FRAGMENT_PRECISION_HIGH\n"
    + "precision highp float;\n"
    + "#else\n"
    + "precision mediump float;\n"
    + "#endif\n"
    + ""
    + "uniform float height;"
    + "uniform float width;"
    + ""
    + "varying vec2 vTextureCoord;"
    + ""
    + "vec4 computeElement(float s, float t)"
    + "{"
    + "  float i = floor(width*s);"
    + "  float j = floor(height*t);"
    + "  return vec4(i*1000.0 + j, 0.0, 0.0, 0.0);"
    + "}"
    + ""
    + "void main()"
    + "{"
    + "  gl_FragColor = computeElement(vTextureCoord.s, vTextureCoord.t);"
    + "}";

  // Null first argument to createProgram => use the standard vertex shader
  program = gpgpUtility.createProgram(null, fragmentShaderSource);

  // position and textureCoord are attributes from the standard vertex shader
  positionHandle = gpgpUtility.getAttribLocation(program,  "position");
  gl.enableVertexAttribArray(positionHandle);
  textureCoordHandle = gpgpUtility.getAttribLocation(program,  "textureCoord");
  gl.enableVertexAttribArray(textureCoordHandle);
  // Height and width are the problem specific variables
  heightHandle = gpgpUtility.getUniformLocation(program, "height");
  widthHandle = gpgpUtility.getUniformLocation(program, "width");

  return program;
}

$createProgram$ 方法在 $MatrixInitializer$ 构造函数中被调用。当构造函数结束时,程序已成功编译,所有 $attributes$$uniforms$ 的句柄都已获知。我们可以将整个 $MatrixInitializer$ 放到一个静态方法中,但上述程序结构(即在构造函数中构建程序,并将功能特性拆分到在不同方法中)更具灵活性,我们将反复使用。

片段着色器将纹素设置为 $vec4(i \times 1000.0 + j, 0.0, 0.0, 0.0)$。这是一个 $R$ 分量值为 $i \times 1000.0 + j$,其它分量值为 $0.0$。是的, $GB$$A$ 纹理通道未被使用2

$initializer.initialize$ 方法运行程序,将值加载到纹理中。

/**
 * Runs the program to do the actual work. On exit the framebuffer &
 * texture are populated with the values computed in the fragment shader.
 * Use gl.readPixels to retrieve texture values.
 */
this.initialize = function(width, height)
{
  gl.useProgram(program);

  gpgpUtility.getStandardVertices();

  gl.vertexAttribPointer(
    positionHandle, // The attribute
    3, // The three (x,y,z) elements in each value
    gl.FLOAT, // The data type, so each position is three floating point numbers
    gl.FALSE, // Are values normalized - unused for float
    20, // Stride, the spacing, in bytes, between beginnings of successive values
    0); // Offset 0, data starts at the beginning of the array
  gl.vertexAttribPointer(
    textureCoordHandle, // The attribute
    2, // The two (s,t) elements in each value
    gl.FLOAT, // The data type, so each position is two floating point numbers
    gl.FALSE, // Are values normalized - unused for float
    20, // Stride, the spacing, in bytes, between beginnings of successive values
    12); // Offset 12 bytes, data starts after the positional data

  gl.uniform1f(widthHandle,  width);
  gl.uniform1f(heightHandle, height);

  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
};

在完成了初始化工作需要的所有计算之后,我们调用 $initializer.done()$。这将删除不再需要的东西,对于初始化来说,只有着色器程序不再需要了。其它的东西,如标准几何形状、帧缓冲区和纹理都将在后续计算中使用。

/**
 * Invoke to clean up resources specific to this program. We leave the texture
 * and frame buffer intact as they are used in followon calculations.
 */
this.done = function ()
{
  gl.deleteProgram(program);
};

当然,与任何合理的代码一样,我们以一组测试用例作为结束。这些测试也作为从 $GPU$ 读取数据到 $CPU$ 的示例。测试用例从纹理中读取 $(i, j)$ 元素,并通过在 $JavaScript$ 中计算与片段着色器中相同的表达式来计算预期值,如果对应的值匹配,则测试通过。

/**
 * Read back the i, j pixel and compare it with the expected value. The expected value
 * computation matches that in the fragment shader.
 * 
 * @param i {integer} the i index of the matrix.
 * @param j {integer} the j index of the matrix.
 */
this.test = function(i, j)
{
  var buffer;
  var expected;
  var passed;

  // One each for RGBA component of a pixel
  buffer = new Float32Array(4);
  // Read a 1x1 block of pixels, a single pixel
  gl.readPixels(
    i, // x-coord of lower left corner
    j, // y-coord of lower left corner
    1, // width of the block
    1, // height of the block
    gl.RGBA, // Format of pixel data.
    gl.FLOAT,// Data type of the pixel data, must match makeTexture
    buffer); // Load pixel data into buffer

  expected = i*1000.0 + j;

  passed   = (buffer[0] === expected);

  if (!passed)
  {
    alert("Read " + buffer[0] + " at (" + i + ", " + j + "), expected " + expected + ".");
  }

  return passed;
};
  1. 这个未初始化的纹理内存是WebGL早期安全问题的来源。现在要求清除这个内存。
  2. 从长远来看,这些其它纹理组件可以用于将整数打包到一组单字节通道中,或者优化浮点纹理方法。
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