小程序WebGL奇妙的Bug之旅

小程序的WebGL坑比较多,其中一个算是HDR会在多次进出页面偶现全黑、全半黑、一半亮一半黑的情况。

Bug复现录屏(无光照,仅仅用HDR照亮场景)

image

把对应的envMap的纹理绘制出来发现问题了。

全黑 半黑 正常
image
image
image
image

通过阅读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);

结果很不幸,确实出现了一样的问题。

image

所以猜测是硬件问题?但是还需要排除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牛皮
image

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

如果不设置压缩的话,亮度的还原应该会更好。比如保存到bin文件,但是体积就比较大了。不过HDR 1024*512 还是会比产出的768*768大一些

image

image

结束

虽然不算找到根本原因,但是bug算是解决了也变相压缩了HDR文件?当然只能在three的场景。

小程序WebGL真奇妙

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容