小程序的WebGL坑比较多,其中一个算是HDR会在多次进出页面偶现全黑、全半黑、一半亮一半黑的情况。
Bug复现录屏(无光照,仅仅用HDR照亮场景)
把对应的envMap的纹理绘制出来发现问题了。
全黑 | 半黑 | 正常 |
---|---|---|
通过阅读PMREMGenerator
的源码,了解到其生成方式
function _applyPMREM( cubeUVRenderTarget ) {
var autoClear = _renderer.autoClear;
_renderer.autoClear = false;
for ( var i = 1; i < TOTAL_LODS; i ++ ) {
var sigma = Math.sqrt(
_sigmas[ i ] * _sigmas[ i ] -
_sigmas[ i - 1 ] * _sigmas[ i - 1 ] );
var poleAxis =
_axisDirections[ ( i - 1 ) % _axisDirections.length ];
_blur( cubeUVRenderTarget, i - 1, i, sigma, poleAxis );
}
_renderer.autoClear = autoClear;
}
/**
* This is a two-pass Gaussian blur for a cubemap. Normally this is done
* vertically and horizontally, but this breaks down on a cube. Here we apply
* the blur latitudinally (around the poles), and then longitudinally (towards
* the poles) to approximate the orthogonally-separable blur. It is least
* accurate at the poles, but still does a decent job.
*/
function _blur( cubeUVRenderTarget, lodIn, lodOut, sigma, poleAxis ) {
_halfBlur(
cubeUVRenderTarget,
_pingPongRenderTarget,
lodIn,
lodOut,
sigma,
'latitudinal',
poleAxis );
_halfBlur(
_pingPongRenderTarget,
cubeUVRenderTarget,
lodOut,
lodOut,
sigma,
'longitudinal',
poleAxis );
}
// _halfBlur的代码就不贴了,_blur的注视大概描述了
其实是一个pingpong图像处理方式,可参考WebGLFundamental,简单理解就是
先把初始图像写入FBOPing
FBOPing纹理输入 -> 经过高斯模糊latitudinal, 缩小一倍 -> FBOPong
FBOPong纹理输入 -> 经过高斯模糊longitudinal, 写到相同位置 -> FBOPing
FBOPing纹理输入 -> 经过高斯模糊latitudinal, 缩小一倍 -> FBOPong
FBOPong纹理输入 -> 经过高斯模糊longitudinal, 写到相同位置 -> FBOPing
FBOPing纹理输入 -> 经过高斯模糊latitudinal, 缩小一倍 -> FBOPong
FBOPong纹理输入 -> 经过高斯模糊longitudinal, 写到相同位置 -> FBOPing
FBOPing纹理输入 -> 经过高斯模糊latitudinal, 缩小一倍 -> FBOPong
FBOPong纹理输入 -> 经过高斯模糊longitudinal, 写到相同位置 -> FBOPing
...
使用得到最后的FBOPing
所以猜测是硬件问题?但是需要排除_halfBlur/Shader问题,所以需要编写简单类似PingPong
处理方式观察是否有问题
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera();
const geometry = new THREE.PlaneGeometry(0.7, 0.7, 1, 1);
const geometry1 = new THREE.PlaneGeometry(0.5, 0.5, 1, 1);
const plane = new THREE.Mesh(
geometry,
new THREE.MeshBasicMaterial({
color: new THREE.Color(0x123456),
side: THREE.DoubleSide,
}),
);
const planeTmp = new THREE.Mesh(
geometry,
new THREE.MeshBasicMaterial({ side: THREE.DoubleSide }),
);
const srcTarget = new THREE.WebGLRenderTarget(
this.canvas.width,
this.canvas.height,
);
const destTarget = new THREE.WebGLRenderTarget(
this.canvas.width,
this.canvas.height,
);
plane.position.z = -0.1;
const autoClear = this.renderer.autoClear;
this.renderer.autoClear = false;
scene.add(plane);
const viewport = (target, x, y, w, h) => {
target.viewport.set(x, y, w, h);
target.scissor.set(x, y, w, h);
};
const wh = this.canvas.width / 2;
const hh = this.canvas.height / 2;
[
[0, 0],
[wh, 0],
[0, hh],
[wh, hh],
].forEach(([x, y]) => {
viewport(srcTarget, x, y, wh, hh);
this.renderer.setRenderTarget(srcTarget);
this.renderer.render(scene, camera); // 手机使用PerspectiveCamera,画不出来东西,除了行123,可以,奇怪了
scene.remove(plane);
scene.add(planeTmp);
// 结果写入到dest
planeTmp.material.map = srcTarget.texture;
viewport(destTarget, x, y, wh, hh);
this.renderer.setRenderTarget(destTarget);
this.renderer.render(scene, this.camera);
});
// show result
this.renderer.autoClear = autoClear;
this.renderer.setRenderTarget(null);
const planeSrc = new THREE.Mesh(
geometry1,
new THREE.MeshBasicMaterial({
map: srcTarget.texture,
side: THREE.DoubleSide,
}),
);
const planeDest = new THREE.Mesh(
geometry1,
new THREE.MeshBasicMaterial({
map: destTarget.texture,
side: THREE.DoubleSide,
}),
);
planeSrc.position.z = 0.2;
planeDest.position.z = -0.2;
this.scene.add(planeSrc, planeDest);
结果很不幸,确实出现了一样的问题。
所以猜测是硬件问题?但是还需要排除Three的问题,所以需要编写纯WebGL的demo看是否能复现。(WebGL的代码比较繁琐,shader也写得不利索,大佬轻喷,主要看render函数即可)
Page({
data: {},
onReady() {this.onClick()},
onClick() {
wx.createSelectorQuery().select('#gl').node().exec((res) => {
if (res[0]) {
this.test(res[0].node)
}
})
},
async test(canvas) {
const gl = canvas.getContext('webgl');
const maxVertexShaderTextureUnits = gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS);
const maxFragmentShaderTextureUnits = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS);
const { windowHeight, windowWidth, pixelRatio } = wx.getSystemInfoSync()
canvas.height = windowHeight * pixelRatio;
canvas.width = windowWidth * pixelRatio;
const fb0 = gl.createFramebuffer();
const fb1 = gl.createFramebuffer();
const tex0 = gl.createTexture();
const tex1 = gl.createTexture();
// prettier-ignore
const VERTXES = [
0.0, 1.0,
1.0, -1.0,
-1.0, -1.0,
];
// prettier-ignore
const TEXTURE_VERTXES = [
-1.0, 1.0,
-1.0, 0.0,
1.0, 1.0,
1.0, 0.0,
-1.0, 0.0,
1.0, 1.0,
]
// prettier-ignore
const COPY_TEXTURE_VERTXES = [
-1.0, 1.0,
-1.0, -1.0,
1.0, 1.0,
1.0, -1.0,
-1.0, -1.0,
1.0, 1.0,
]
const [triangleProgram, triangleVS, triangleFS] = createProgram(
glsl`
#pragma vscode_glsllint_stage : vert
attribute vec2 a_position;
varying vec4 v_color;
void main() {
gl_Position = vec4(a_position.x, a_position.y, 0, 1);
v_color = gl_Position * 0.5 + 0.5;
}
`,
glsl`
#pragma vscode_glsllint_stage : frag
precision mediump float;
varying vec4 v_color;
void main() {
gl_FragColor = v_color;
}
`,
);
const [copyTextureProgram, copyTexVS, copyTexFS] = createProgram(
glsl`
#pragma vscode_glsllint_stage : vert
attribute vec2 a_position;
varying vec2 v_texcoord;
void main() {
gl_Position = vec4(a_position.x, a_position.y, 0, 1);
v_texcoord = vec2((a_position.x + 1.0) * .5, (a_position.y + 1.0) * 0.5);
}
`,
glsl`
#pragma vscode_glsllint_stage : frag
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D u_texture;
void main() {
gl_FragColor = texture2D(u_texture, v_texcoord);
}
`,
);
const [textureProgram, texVS, texFS] = createProgram(
glsl`
#pragma vscode_glsllint_stage : vert
attribute vec2 a_position;
uniform bool u_up;
varying vec2 v_texcoord;
void main() {
if (u_up) {
gl_Position = vec4(a_position.x, a_position.y, 0, 1);
} else {
gl_Position = vec4(a_position.x, a_position.y - 1.0, 0, 1);
}
v_texcoord = vec2((a_position.x + 1.0) * 0.5, a_position.y);
}
`,
glsl`
#pragma vscode_glsllint_stage : frag
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D u_texture;
void main() {
gl_FragColor = texture2D(u_texture, v_texcoord);
}
`,
);
const buffer = gl.createBuffer();
initTexture(tex0);
initTexture(tex1);
bindFrameBufferToTexture(fb0, tex0);
bindFrameBufferToTexture(fb1, tex1);
gl.bindFramebuffer(gl.FRAMEBUFFER, fb0);
drawTriangle();
for (let index = 0; index < 2; index++) {
renader()
}
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.clearColor(1, 1, 0, 1)
gl.clear(gl.COLOR_BUFFER_BIT);
drawTexture(tex0, 1);
drawTexture(tex1, 0);
dispose()
// 工具函数
function renader() {
gl.bindFramebuffer(gl.FRAMEBUFFER, fb1);
copyTexture(tex0);
gl.bindFramebuffer(gl.FRAMEBUFFER, fb0);
drawTexture(tex0, 1); // 神奇的小程序WebGL
drawTexture(tex1, 0);
}
function copyTexture(tex) {
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.useProgram(copyTextureProgram);
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array(COPY_TEXTURE_VERTXES),
gl.STATIC_DRAW,
);
const aPosition = gl.getAttribLocation(copyTextureProgram, 'a_position');
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.enableVertexAttribArray(aPosition);
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);
const uTexture = gl.getUniformLocation(copyTextureProgram, 'u_texture');
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.uniform1i(uTexture, 0);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
function drawTexture(tex, up = 1) {
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.useProgram(textureProgram);
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array(TEXTURE_VERTXES),
gl.STATIC_DRAW,
);
const aPosition = gl.getAttribLocation(textureProgram, 'a_position');
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.enableVertexAttribArray(aPosition);
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);
const uTexture = gl.getUniformLocation(textureProgram, 'u_texture');
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.uniform1i(uTexture, 0);
const uUp = gl.getUniformLocation(textureProgram, 'u_up');
gl.uniform1i(uUp, up);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
function drawTriangle() {
gl.useProgram(triangleProgram);
checkError('after useProgram');
// 写入顶点
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(VERTXES), gl.STATIC_DRAW);
checkError('after bufferData');
// buffer写入aPosition
const aPosition = gl.getAttribLocation(triangleProgram, 'a_position');
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.enableVertexAttribArray(aPosition);
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);
checkError('after vertexAttribPointer');
// 绘制
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
checkError('after viewport');
gl.drawArrays(gl.TRIANGLES, 0, 3);
}
}
// 一些工具函数隐藏了
})
其实主要看render函数即可,错误的代码发现了更加神奇的Bug,小程序的WebGL居然允许自产自销?
bindFrameBufferToTexture(fb0, tex0);
bindFrameBufferToTexture(fb1, tex1);
function renader() {
gl.bindFramebuffer(gl.FRAMEBUFFER, fb1);
copyTexture(tex0);
gl.bindFramebuffer(gl.FRAMEBUFFER, fb0);
drawTexture(tex0, 1);
// 小程序WebGL允许读取绑定到fb0的tex0,写入到fb0绑定的tex0 ???
drawTexture(tex1, 0);
}
小程序 | PC |
---|---|
并且来回切换页面会偶现WebGL罢工,什么东西都无绘制的状态。。。同时也不报错。瞬间感觉不是硬件问题了。
曲线救国,Bug还是得解的
既然是定位到PMREMGenerator
过程的问题,那么貌似最快的解法就是避免在可能有bug的three,可能有bug的微信小程序WebGL,可能有bug的手机OpenGL,可能有bug的手。
所以解法就是直接使用正确生成出来的纹理。
// const hdr = await rgbeLoader.loadAsync('your.hdr');
// const envMap = pmremGenerator.fromEquirectangular(hdr).texture;
const envMap = await textureLoader.loadAsync('上面envMap保存之后的图片.png')
envMap.magFilter = THREE.NearestFilter
envMap.minFilter = THREE.NearestFilter
envMap.generateMipmaps = false
envMap.type = THREE.UnsignedByteType
envMap.format = THREE.RGBEFormat
envMap.encoding = THREE.RGBEEncoding
envMap.mapping = THREE.CubeUVReflectionMapping
envMap.name = 'PMREM.cubeUv';
envMap.needsUpdate = true;
scene.environment = envMap
如果不设置压缩的话,亮度的还原应该会更好。比如保存到bin文件,但是体积就比较大了。不过HDR 1024*512 还是会比产出的768*768大一些
结束
虽然不算找到根本原因,但是bug算是解决了也变相压缩了HDR文件?当然只能在three的场景。
小程序WebGL真奇妙