使用 Emscripten 将 OpenGL ES 转换反馈数据返回给主机

Getting OpenGL ES Transform Feedback data back to the host using Emscripten

下面的 C++ OpenGL ES 代码使用 GLFW 计算五个数字的平方根并将它们输出到命令行。该代码使用了转换反馈。当我在 Ubuntu 17.10 上编译时,使用以下命令,我得到了我期望的结果:

$ g++ tf.cpp -lGL -lglfw

但是,如果我使用 Emscripten,则会抛出异常,这表明 glMapBufferRange 仅在访问权限为 MAP_WRITE|INVALIDATE_BUFFER 时才受支持。我确实想阅读而不是写作,所以也许我不应该使用 glMapBufferRange,但我可以使用什么来代替?我在 Firefox 和 Chromium 上都试过了。我用Emscripten编译的命令是:

$ em++ -std=c++11 tf.cpp -s USE_GLFW=3 -s USE_WEBGL2=1 -s FULL_ES3=1 -o a.out.html

代码如下:

#include <iostream>

#define GLFW_INCLUDE_ES3
#include <GLFW/glfw3.h>

#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif

static const GLuint WIDTH  = 800;
static const GLuint HEIGHT = 600;
static const GLchar *vertex_shader_src =
"#version 300 es\n"
"precision mediump float;\n"
"in  float inValue;\n"
"out float outValue;\n"
"void main() {\n"
"  outValue = sqrt(inValue);\n"
"}\n";

// Emscripten complains if there's no fragment shader
static const GLchar *fragment_shader_src =
"#version 300 es\n"
"precision mediump float;\n"
"out vec4 colour;\n"
"void main() {\n"
"  colour = vec4(1.0, 1.0, 0.0, 1.0);\n"
"}\n";

static const GLfloat vertices[] = {
   0.0f,  0.5f, 0.0f,
   0.5f, -0.5f, 0.0f,
  -0.5f, -0.5f, 0.0f,
};

GLint get_shader_prog(const char *vert_src, const char *frag_src = "")
{
  enum Consts { INFOLOG_LEN = 512 };
  GLchar infoLog[INFOLOG_LEN];
  GLint fragment_shader, vertex_shader, shader_program, success;
  shader_program = glCreateProgram();

  auto mk_shader = [&](GLint &shader, const GLchar **str, GLenum shader_type) {
    shader = glCreateShader(shader_type);
    glShaderSource(shader, 1, str, NULL);
    glCompileShader(shader);
    glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
    if (!success) {
      glGetShaderInfoLog(shader, INFOLOG_LEN, NULL, infoLog);
      std::cout << "ERROR::SHADER::COMPILATION_FAILED\n" << infoLog << '\n';
    }
    glAttachShader(shader_program, shader);
  };

  mk_shader(vertex_shader,   &vert_src, GL_VERTEX_SHADER);
  mk_shader(fragment_shader, &frag_src, GL_FRAGMENT_SHADER);

  const GLchar* feedbackVaryings[] = { "outValue" };
  glTransformFeedbackVaryings(shader_program, 1, feedbackVaryings,
                              GL_INTERLEAVED_ATTRIBS);

  glLinkProgram(shader_program);
  glGetProgramiv(shader_program, GL_LINK_STATUS, &success);
  if (!success) {
    glGetProgramInfoLog(shader_program, INFOLOG_LEN, NULL, infoLog);
    std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << '\n';
  }
  glUseProgram(shader_program);

  glDeleteShader(vertex_shader);
  glDeleteShader(fragment_shader);
  return shader_program;
}

int main(int argc, char *argv[])
{
  glfwInit();
  glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API);
  glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
  GLFWwindow *window = glfwCreateWindow(WIDTH, HEIGHT, __FILE__, NULL, NULL);
  glfwMakeContextCurrent(window);

  GLuint shader_prog = get_shader_prog(vertex_shader_src, fragment_shader_src);
  GLint inputAttrib = glGetAttribLocation(shader_prog, "inValue");

  glViewport(0, 0, WIDTH, HEIGHT);

  GLuint vao;
  glGenVertexArrays(1, &vao);
  glBindVertexArray(vao);

  GLfloat data[] = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f };

  GLuint vbo;
  glGenBuffers(1, &vbo);
  glBindBuffer(GL_ARRAY_BUFFER, vbo);
  glBufferData(GL_ARRAY_BUFFER, sizeof(data), data, GL_STATIC_DRAW);

  glVertexAttribPointer(inputAttrib, 1, GL_FLOAT, GL_FALSE, 0, 0);
  glEnableVertexAttribArray(inputAttrib);

  GLuint tbo;
  glGenBuffers(1, &tbo);
  glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, tbo);
  glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, sizeof(data),
               nullptr, GL_STATIC_READ);
  glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, tbo);
  glEnable(GL_RASTERIZER_DISCARD);

  glBeginTransformFeedback(GL_POINTS);
  glDrawArrays(GL_POINTS, 0, 5);
  glEndTransformFeedback();

  glDisable(GL_RASTERIZER_DISCARD);
  glFlush();

  GLfloat feedback[5]{1,2,3,4,5};
  void *void_buf = glMapBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER,0,
                                    sizeof(feedback), GL_MAP_READ_BIT);
  GLfloat *buf = static_cast<GLfloat *>(void_buf);
  for (int i = 0; i < 5; i++)
    feedback[i] = buf[i];
  glUnmapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER);

  for (int i = 0; i < 5; i++)
    std::cout << feedback[i] << ' ';
  std::cout << std::endl;

  glDeleteBuffers(1, &vbo);
  glDeleteBuffers(1, &tbo);
  glDeleteVertexArrays(1, &vao);
  glfwTerminate();

  return 0;
}

WebGL2 不支持 MapBufferRange 因为这将是一场安全噩梦。相反,它支持 getBufferSubData。我不知道这是否会在 emscripten 中暴露给 WebAssembly。 Emscripten 正在使用 bufferData 为您提到的案例模拟 MapBufferRange。参见 here and here

如果不支持getBufferSubData,您可以添加它。请参阅 library_gl.js for how readPixels is implemented and use that as inspiration for how to expose getBufferSubData to WebAssembly. Either that or add some inline JavaScript with the _EM_ASM. I haven't done it but googling for "getBufferSubData emscripten" bought up this gist

中的代码

正如另一个答案所提到的,WebGL 2.0 接近于 OpenGL ES 3.0,但令人困惑的是没有定义 glMapBufferRange() 函数,因此 Emscripten 尝试仅模拟该函数的部分功能。

然而,Real WebGL 2.0 公开了 glGetBufferSubData() 模拟,它在 OpenGL ES 中不存在,但在桌面 OpenGL 中存在。该方法可以通过 EM_ASM_:

包装在代码中
void myGetBufferSubData (GLenum theTarget, GLintptr theOffset, GLsizeiptr theSize, void* theData)
{
#ifdef __EMSCRIPTEN__
  EM_ASM_(
  {
    Module.ctx.getBufferSubData([=10=], , HEAPU8.subarray(,  + ));
  }, theTarget, theOffset, theData, theSize);
#else
  glGetBufferSubData (theTarget, theOffset, theSize, theData);
#endif
}

在多平台代码中取回 VBO 数据很麻烦:

  1. OpenGL 1.5+
    • glGetBufferSubData(): 是
    • glMapBufferRange(): 是
  2. OpenGL ES 2.0
    • glGetBufferSubData(): 否
    • glMapBufferRange(): 否
  3. OpenGL ES 3.0+
    • glGetBufferSubData(): 否
    • glMapBufferRange(): 是
  4. WebGL 1.0
    • glGetBufferSubData(): 否
    • glMapBufferRange(): 否
  5. WebGL 2.0
    • glGetBufferSubData(): 是
    • glMapBufferRange(): 否

这样:

  • 桌面 OpenGL 提供了最大的灵活性。
  • OpenGL ES 2.0 和 WebGL 1.0 没有机会检索数据。
  • OpenGL ES 3.0+ 仅提供映射缓冲区。
  • WebGL 2.0 仅提供 getBufferSubData()。