GLSL:如果未绑定任何制服,则发生强制错误

时间:2018-08-03 14:55:24

标签: glsl webgl

我正在将GLSL 1.0与WebGL 1.0和2.0结合使用,我只是花了几个小时来解决一个问题,我认为应该在开始之前就抛出错误。

我的片段着色器中有stage('Setup') { steps { dir ('bin') { deleteDir() } } } uniforms。我已经更改了一行代码,并且所做的更改未将任何输入纹理或数组绑定到Shader sampler2D的位置。但是,程序运行没有问题,但是在读取这些uniform时产生零。例如,对uniform的调用不会引发任何错误,而只会返回0。

无论如何,在渲染之前或渲染期间我是否都将其强制为错误?

1 个答案:

答案 0 :(得分:2)

无法让WebGL本身检查错误。如果要检查错误,可以编写自己的包装器。作为一个示例,有webgl-debug context包装器在每个WebGL命令之后调用gl.getError

按照类似的模式,您可以尝试通过包装与图形,程序,制服,属性等有关的所有功能,或者仅制作要调用的功能来检查是否未设置制服

function myUseProgram(..args..) {
  checkUseProgramStuff();
  gl.useProgram(..);
}

function myDrawArrays(..args..) {
  checkDrawArraysStuff();
  gl.drawArrays(..args..);
}

对于制服,您需要跟踪程序何时成功链接,然后遍历其所有制服(您可以查询)。跟踪当前程序是什么。跟踪对gl.uniform的呼叫,以跟踪是否设置了制服。

这是一个例子

(function() {
  const gl = null;  // just to make sure we don't see global gl
  const progDB = new Map();
  let currentUniformMap;
  const limits = {};
  const origGetUniformLocationFn = WebGLRenderingContext.prototype.getUniformLocation;

  function init(gl) {
    [gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS].forEach((pname) => {
      limits[pname] = gl.getParameter(pname);
    });
  }

  function isBuiltIn(info) {
    const name = info.name;
    return name.startsWith("gl_") || name.startsWith("webgl_");
  }

  function addProgramToDB(gl, prg) {
    const uniformMap = new Map();
    const numUniforms = gl.getProgramParameter(prg, gl.ACTIVE_UNIFORMS);
    for (let ii = 0; ii < numUniforms; ++ii) {
      const uniformInfo = gl.getActiveUniform(prg, ii);
      if (isBuiltIn(uniformInfo)) {
        continue;
      }
      const location = origGetUniformLocationFn.call(gl, prg, uniformInfo.name);
      uniformMap.set(location, {set: false, name: uniformInfo.name, type: uniformInfo.type, size: uniformInfo.size});
    }
    progDB.set(prg, uniformMap);
  }

  HTMLCanvasElement.prototype.getContext = function(origFn) {
    return function(type, ...args) {
      const ctx = origFn.call(this, type, ...args);
      if (ctx && type === 'webgl') {
        init(ctx);
      }
      return ctx;
    }
  }(HTMLCanvasElement.prototype.getContext);

  // getUniformLocation does not return the same location object
  // for the same location so mapping a location to uniform data
  // would be a PITA. So, let's make it return the same location objects.
  WebGLRenderingContext.prototype.getUniformLocation = function(origFn) {
    return function(prg, name) {
      const uniformMap = progDB.get(prg);
      for (const [location, uniformInfo] of uniformMap.entries()) {
        // note: not handling names like foo[0] vs foo
        if (uniformInfo.name === name) {
          return location;
        }
      }
      return null;
    };
  }(WebGLRenderingContext.prototype.getUniformLocation);

  WebGLRenderingContext.prototype.linkProgram = function(origFn) {
    return function(prg) {
      origFn.call(this, prg);
      const success = this.getProgramParameter(prg, this.LINK_STATUS);
      if (success) {
        addProgramToDB(this, prg);
      }
    };
  }(WebGLRenderingContext.prototype.linkProgram);

  WebGLRenderingContext.prototype.useProgram = function(origFn) {
    return function(prg) {
      origFn.call(this, prg);
      currentUniformMap = progDB.get(prg);
    };
  }(WebGLRenderingContext.prototype.useProgram);

  WebGLRenderingContext.prototype.uniform1i = function(origFn) {
    return function(location, v) {
      const uniformInfo = currentUniformMap.get(location);
      if (v === undefined) {
        throw new Error(`bad value for uniform: ${uniformInfo.name}`);  // do you care? undefined will get converted to 0
      }
      const val = parseFloat(v);
      if (isNaN(val) || !isFinite(val)) {
        throw new Error(`bad value NaN or Infinity for uniform: ${uniformInfo.name}`);  // do you care?
      }
      switch (uniformInfo.type) {
        case this.SAMPLER_2D:
        case this.SAMPLER_CUBE:
          if (val < 0 || val > limits[this.MAX_COMBINED_TEXTURE_IMAGE_UNITS]) {
            throw new Error(`texture unit out of range for uniform: ${uniformInfo.name}`);
          }
          break;
        default:
          break;
      }
      uniformInfo.set = true;
      origFn.call(this, location, v);
    };
  }(WebGLRenderingContext.prototype.uniform1i);

  WebGLRenderingContext.prototype.drawArrays = function(origFn) {
    return function(...args) {
      const unsetUniforms = [...currentUniformMap.values()].filter(u => !u.set);
      if (unsetUniforms.length) {
        throw new Error(`unset uniforms: ${unsetUniforms.map(u => u.name).join(', ')}`);
      }
      origFn.call(this, ...args);
    };
  }(WebGLRenderingContext.prototype.drawArrays);
}());

// ------------------- above is wrapper ------------------------
// ------------------- below is test ---------------------------

const gl = document.createElement('canvas').getContext('webgl');

const vs = `
uniform float foo;
uniform float bar;
void main() {
  gl_PointSize = 1.;
  gl_Position = vec4(foo, bar, 0, 1);
}
`;

const fs = `
precision mediump float;
uniform sampler2D tex;
void main() {
  gl_FragColor = texture2D(tex, vec2(0));
}
`;

const prg = twgl.createProgram(gl, [vs, fs]);
const fooLoc = gl.getUniformLocation(prg, 'foo');
const barLoc = gl.getUniformLocation(prg, 'bar');
const texLoc = gl.getUniformLocation(prg, 'tex');

gl.useProgram(prg);

test('fails with undefined', () => {
  gl.uniform1i(fooLoc, undefined);
});
test('fails with non number string', () => {
  gl.uniform1i(barLoc, 'abc');
});
test('fails with NaN', () => {
  gl.uniform1i(barLoc, 1/0);
});
test('fails with too large texture unit', () => {
  gl.uniform1i(texLoc, 1000);
})
test('fails with not all uniforms set', () => {
  gl.drawArrays(gl.POINTS, 0, 1);
});
test('fails with not all uniforms set',() => {
  gl.uniform1i(fooLoc, 0);
  gl.uniform1i(barLoc, 0);
  gl.drawArrays(gl.POINTS, 0, 1);
});
test('passes with all uniforms set', () => {
  gl.uniform1i(fooLoc, 0);
  gl.uniform1i(barLoc, 0);
  gl.uniform1i(texLoc, 0);
  gl.drawArrays(gl.POINTS, 0, 1);   // note there is no texture so will actually generate warning
});

function test(msg, fn) {
  const expectFail = msg.startsWith('fails');
  let result = 'success';
  let fail = false;
  try {
    fn();
  } catch (e) {
    result = e;
    fail = true;
  }
  log('black', msg);
  log(expectFail === fail ? 'green' : 'red', '  ', result);
}

function log(color, ...args) {
  const elem = document.createElement('pre');
  elem.textContent = [...args].join(' ');
  elem.style.color = color;
  document.body.appendChild(elem);
}
pre { margin: 0; }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>

上面的代码仅包装gl.uniform1i。它不处理制服数组,也不处理单个数组元素的位置。它确实显示了一种跟踪制服以及是否设置制服的方法。

按照类似的模式,您可以检查每个纹理单元是否分配了纹理等,是否已打开每个属性,等等...

当然,您也可以编写自己的WebGL框架来跟踪所有内容,而不用修改WebGL上下文本身。换句话说,例如three.js可以跟踪其所有制服的级别都高于WebGL级别,并且您自己的代码可以执行类似的操作。

关于为何WebGL不发出错误的原因很多。首先,不设置制服不是错误。制服具有默认值,可以使用默认值。

浏览器确实捕获了一些问题,但是由于WebGL是管道传输的,因此在发出命令时不会给您错误,而不会显着降低性能(上面提到的调试上下文将为您完成此工作)。因此,浏览器有时可以在控制台中发出警告,但无法在发出命令时停止JavaScript。无论如何,可能并不是唯一有帮助的地方就是在绘制时经常会出错的地方。换句话说,在绘制之前,发出30-100个命令来设置WebGL状态并不是错误,因为您可以在绘制之前随时修复该状态。因此,您会在绘制时遇到错误,但这并不能告诉您以前的30-100个命令中的哪个导致了此问题。

最后,存在一个哲学问题,即试图通过emscripten或WebAssembly从OpenGL / OpenGL ES支持本机端口。许多本机应用程序忽略 许多GL错误,但仍然可以运行。这是WebGL不抛出与OpenGL ES兼容的原因之一(以及上述原因)。这也是为什么大多数WebGL实现仅显示一些错误,然后打印“不再显示webgl错误”的原因,因为浏览器不希望忽略其WebGL错误的程序将日志消息填充到内存中。

幸运的是,如果您真的想要它,您可以编写自己的支票。