我将 GLSL 1.0 与 WebGL 1.0 和 2.0 一起使用,我只是花了几个小时来解决一个问题,在我看来,这个问题应该在事情开始之前就抛出错误。
我的片段着色器中有
uniforms
和 sampler2D
。我更改了一行代码,该更改导致没有输入纹理或数组绑定到着色器uniform
s 的位置。然而,该程序运行没有问题,但在读取这些 uniform
时会产生零。例如,调用 texture2D(MyTexture, vec2(x,y))
不会抛出任何错误,而只会返回 0.
我有没有办法在渲染之前或渲染期间将其强制为错误?
更新:有一个库可以做下面提到的一些检查:见webgl-lint
没有办法让 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 错误的程序用日志消息填充内存。
幸运的是,如果你真的想要它,你可以自己写支票。