小程序WebGL奇妙的Bug之旅

3,751 阅读4分钟

小程序的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牛皮

并且来回切换页面会偶现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真奇妙