代码之家  ›  专栏  ›  技术社区  ›  Jeff Saremi

GLSL:未绑定任何制服时的强制错误

  •  0
  • Jeff Saremi  · 技术社区  · 6 年前

    我将GLSL 1.0与webgl1.0和2.0结合使用,我花了几个小时来解决一个在我看来应该在开始之前抛出错误的问题。

    我有 uniforms sampler2D 在我的碎片着色器中。我更改了一行代码,这一更改没有将任何输入纹理或数组绑定到着色器的位置 uniform 但是程序运行时没有问题,但是当 制服 s被读取。例如,调用 texture2D(MyTexture, vec2(x,y)) 不抛出任何错误,只返回0。

    在渲染之前或渲染过程中,是否仍要强制将其作为错误?

    1 回复  |  直到 6 年前
        1
  •  2
  •   gman    6 年前

    没有办法让WebGL自己检查您的错误。如果您想检查错误,可以编写自己的包装器。举个例子 webgl-debug context 调用的包装器 gl.getError 在每个WebGL命令之后。

    遵循类似的模式,您可以尝试检查是否没有设置制服,方法是包装与绘图、程序、制服、属性等相关的所有功能,或者只调用

    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。它可能没有那么有用,无论如何,是唯一的地方,它可以经常给一个错误是在绘制时间。换言之,在绘制之前,设置WebGL状态的30-100个命令在绘制之前不会出错,因为可以在绘制之前随时修复该状态。所以在draw时会出现错误,但这并不能告诉您之前的30-100个命令中的哪一个导致了这个问题。

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

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