如何解决 webgl 上下文中 alpha 混合模式的跨浏览器问题?

How to fix crossbrowser issue with alpha blendmode in webgl context?

我尝试使用 gl.blendFuncSeparate 使片段着色器的背景透明。 这在 Windows (Chrome/FF/Edge) 上工作正常,但在 MacOS 上它在 Firefox 中仅 运行s。 Chrome Mac 和 Safari 绘制整个视口透明。

class Render {
  constructor() {
    this.pos = [];
    this.program = [];
    this.buffer = [];
    this.ut = [];
    this.resolution = [];
    
    this.frame = 0;
    this.start = Date.now();
    this.options = {
      alpha: true,
      premultipliedAlpha: true,
      preserveDrawingBuffer: false
     };
    
    this.canvas = document.querySelector('canvas');
    this.gl =  this.canvas.getContext('webgl', this.options);

    this.width = this.canvas.width;
    this.height = this.canvas.height;
    this.gl.viewport(0, 0, this.width, this.height);

    this.gl.enable(this.gl.BLEND);
    this.gl.blendFuncSeparate(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA, this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA);
          
    this.clearCanvas();
  
    window.addEventListener('resize', this.resetCanvas, true);
    this.init();
  }
  
   init = () => {
    let vertexSource = document.querySelector('#vertexShader').textContent;
    let fragmentSource = document.querySelector('#fragmentShader').textContent;

    this.createGraphics(vertexSource, fragmentSource, 0);
     
    this.canvas.addEventListener('mousemove', (e) => {
      this.mouseX = e.pageX / this.canvas.width;
      this.mouseY = e.pageY / this.canvas.height;
    }, false);

    this.renderLoop();
  };

  resetCanvas = () => {
    this.width = 300; //this.shaderCanvas.width;
    this.height = 300; // this.shaderCanvas.height;
    this.gl.viewport(0, 0, this.width, this.height);
    this.clearCanvas();
  };

  createShader = (type, source) => {
    let shader = this.gl.createShader(type);
    this.gl.shaderSource(shader, source);
    this.gl.compileShader(shader);
    let success = this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS);
    if (!success) {
      console.log(this.gl.getShaderInfoLog(shader));
      this.gl.deleteShader(shader);
      return false;
    }
    return shader;
  };

  createProgram = (vertexSource, fragmentSource) => {
    // Setup Vertext/Fragment Shader functions //
    this.vertexShader = this.createShader(this.gl.VERTEX_SHADER, vertexSource);
    this.fragmentShader = this.createShader(this.gl.FRAGMENT_SHADER, fragmentSource);
    
    // Setup Program and Attach Shader functions //
    let program = this.gl.createProgram();
    this.gl.attachShader(program, this.vertexShader);
    this.gl.attachShader(program, this.fragmentShader);
    this.gl.linkProgram(program);
    this.gl.useProgram(program);
    
    return program;
  };

  createGraphics = (vertexSource, fragmentSource, i) => {
    
    // Create the Program //
    this.program[i] = this.createProgram(vertexSource, fragmentSource);
    // Create and Bind buffer //
    this.buffer[i] = this.gl.createBuffer();
    this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer[i]);

    this.gl.bufferData(
      this.gl.ARRAY_BUFFER,
      new Float32Array([-1, 1, -1, -1, 1, -1, 1, 1]),
      this.gl.STATIC_DRAW
    );

    this.pos[i] = this.gl.getAttribLocation(this.program[i], 'pos');
    
    this.gl.vertexAttribPointer(
      this.pos[i],
      2,              // size: 2 components per iteration
      this.gl.FLOAT,  // type: the data is 32bit floats
      false,          // normalize: don't normalize the data
      0,              // stride: 0 = move forward size * sizeof(type) each iteration to get the next position
      0               // start at the beginning of the buffer
    );
    
    
    this.gl.enableVertexAttribArray(this.pos[i]);
    
    this.importProgram(i);
    
  };

  clearCanvas = () => {
    this.gl.clearColor(1,1,1,1);
    this.gl.clear(this.gl.COLOR_BUFFER_BIT);

    // Turn off rendering to alpha
    this.gl.colorMask(true, true, true, false);
  };

  updateUniforms = (i) => {
    this.importUniforms(i);
    
    this.gl.drawArrays(
      this.gl.TRIANGLE_FAN, // primitiveType
      0,                    // Offset
      4                     // Count
    );
  };

  importProgram = (i) => {
    this.ut[i] = this.gl.getUniformLocation(this.program[i], 'time');

    this.resolution[i] = new Float32Array([300, 300]);
    this.gl.uniform2fv(
      this.gl.getUniformLocation(this.program[i],'resolution'),
      this.resolution[i]
    );
  };

  importUniforms = (i) => {
    this.gl.uniform1f(this.ut[i], (Date.now() - this.start) / 1000);
  };

  renderLoop = () => {   
    this.frame++;
    this.updateUniforms(0);
    this.animation = window.requestAnimationFrame(this.renderLoop);
  };
}

let demo = new Render(document.body);
body {
  background: #333;
  padding: 0;
  margin: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

canvas {
  position: absolute;
  top: 0;
  right: 0;
  left: 0;
  bottom: 0;
  width: 200px;
  height: 200px;
  background: transparent;
}
<canvas width="300" height="300"></canvas>

<script id="vertexShader" type="x-shader/x-vertex">
  attribute vec3 pos;

  void main() {
    gl_Position=vec4(pos, .5);
  }
</script>

<script id="fragmentShader" type="x-shader/x-fragment">
  precision mediump float;

 uniform float time;
 uniform vec2 resolution;

 mat2 rotate2d(float angle){
   return mat2(cos(angle),-sin(angle),
         sin(angle),cos(angle));
 }

 float variation(vec2 v1, vec2 v2, float strength, float speed) {
  return sin(
   dot(normalize(v1), normalize(v2)) * strength + time * speed
  ) / 100.0;
 }

 vec4 paintCircle (vec2 uv, vec2 center, float rad, float width) {
  vec2 diff = center-uv;
  float len = length(diff);

  len += variation(diff, vec2(0.0, 1.0), 3.0, 2.0);
  len -= variation(diff, vec2(1.0, 0.0), 3.0, 2.0);

  float circle = 1. -smoothstep(rad-width, rad, len);

  return vec4(circle);
 }

 void main() {
  vec2 uv = gl_FragCoord.xy / resolution.xy;
  vec4 color;
  float radius = 0.15;
  vec2 center = vec2(0.5);

  color = paintCircle(uv, center, radius, .2);
  vec2 v = rotate2d(time) * uv;

  color *= vec4(255,255, 0,255);

     gl_FragColor = color;
 }
</script>

上面的代码片段在 MacOS Chrome 上不起作用,但 运行 在 Windows Chrome 上成功。您应该会看到一个流动的黄色圆圈。目标是只看到 HTML 背景上的动画人物 (#333)。 canvas 是透明的。 我已经尝试过不同的混合功能,但没有任何组合可以跨浏览器工作。

this.options = {
  alpha: true,
  premultipliedAlpha: true,
  preserveDrawingBuffer: false
 };
this.gl.enable(this.gl.BLEND);
this.gl.blendFuncSeparate(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA, this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA);
clearCanvas = () => {
  this.gl.clearColor(1,1,1,1);
  this.gl.clear(this.gl.COLOR_BUFFER_BIT);

  // Turn off rendering to alpha
  this.gl.colorMask(true, true, true, false);
};

我注意到它在 Firefox 上运行良好,但在 Chrome 上却不行。改变一件事后,它的工作原理如我所见。

视频: https://jmp.sh/AkzOl7b

代码笔: https://codepen.io/anon/pen/ZNKZqo

我改自:

preserveDrawingBuffer: false

preserveDrawingBuffer: true

我不知道你期望发生什么,你显然已经编辑了你的代码笔,使你的问题完全无关紧要,因为我们无法再检查问题。下次请使用snippet

对于preserveDrawingBuffer: false,每帧都会清除canvas。在 clearCanvas 中,您将 alpha 清除为 1,然后关闭对 alpha 的渲染,但是因为 preserveDrawingBuffer 为 false(默认值),绘图缓冲区被清除,这意味着 alpha 现在回到零。之后,您将 0,0,0 或 1,1,0 渲染到其中。当 premultipliedAlpha 为真(默认值)时,1,1,0,0 是无效颜色。为什么?因为 premultiplied 意味着您在 canvas 中输入的颜色已乘以 alpha。 alpha 是 0。0 乘以任何东西都是零,所以当 alpha 为零时,红色、绿色和蓝色也必须为零。

这就是您在不同浏览器上看到不同颜色的原因。当您的颜色无效时,结果是不确定的。

preserveDrawingBuffer 设置为 true 并不能解决您的问题。这只是意味着您将 alpha 设置为 1,然后将其保留为 1,因为您关闭了对 alpha 的渲染,因此整个 canvas 是不透明的。

对于您似乎想要的内容,正确的解决方法是根本不清除(让 preserverDrawingBuffer: false,为您清除)并且不要使用 gl.colorMask 关闭对 alpha 的渲染然后在你的着色器中,在你想看到背景的地方写 0 到 alpha,在你不想看到背景的地方写 1

const vertexSource = `
attribute vec3 pos;

void main() {
 gl_Position=vec4(pos, .5);
}
`;

const fragmentSource = `
precision mediump float;

 uniform float time;
 uniform vec2 resolution;

  mat2 rotate2d(float angle){
   return mat2(cos(angle),-sin(angle),
         sin(angle),cos(angle));
 }

 float variation(vec2 v1, vec2 v2, float strength, float speed) {
  return sin(
   dot(normalize(v1), normalize(v2)) * strength + time * speed
  ) / 100.0;
 }

// vec3 paintCircle (vec2 uv, vec2 center, float rad, float width) {
 vec4 paintCircle (vec2 uv, vec2 center, float rad, float width) {
  vec2 diff = center-uv;
  float len = length(diff);

  len += variation(diff, vec2(0.0, 1.0), 3.0, 2.0);
  len -= variation(diff, vec2(1.0, 0.0), 3.0, 2.0);

  float circle = 1. -smoothstep(rad-width, rad, len);

//  return vec3(circle);
  return vec4(circle);
 }


 void main() {
  vec2 uv = gl_FragCoord.xy / resolution.xy;
//  vec3 color;
  vec4 color;
  float radius = 0.15;
  vec2 center = vec2(0.5);

  color = paintCircle(uv, center, radius, .2);
  vec2 v = rotate2d(time) * uv;
  //color *= vec3(v.x, v.y, 0.7-v.y*v.x);

//  color *= vec3(255,255, 0);
  color *= vec4(255,255, 0,255);
  //color += paintCircle(uv, center, radius, 0.01);

//  gl_FragColor = vec4(color, 1.0);
  gl_FragColor = color;
 }
`;

class Render {
  constructor() {
    this.pos = [];
    this.program = [];
    this.buffer = [];
    this.ut = [];
    this.resolution = [];
    
    this.frame = 0;
    this.start = Date.now();
this.options = {
// these are already the defaults
//  alpha: true,
//  premultipliedAlpha: true,
//  preserveDrawingBuffer: false
 };
    
    this.canvas = document.querySelector('canvas');
    this.gl =  this.canvas.getContext('webgl', this.options);

    this.width = this.canvas.width;
    this.height = this.canvas.height;
    this.gl.viewport(0, 0, this.width, this.height);

//    this.gl.enable(this.gl.BLEND);
//    this.gl.blendFuncSeparate(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA, this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA);
    //this.gl.blendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA);
    //this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
          
// this.clearCanvas();
  
    window.addEventListener('resize', this.resetCanvas, true);
    this.init();
  }
  
   init = () => {
    this.createGraphics(vertexSource, fragmentSource, 0);
     
    this.renderLoop();
  };

  resetCanvas = () => {
    this.width = 300; //this.shaderCanvas.width;
    this.height = 300; // this.shaderCanvas.height;
    this.gl.viewport(0, 0, this.width, this.height);
    this.clearCanvas();
  };

  createShader = (type, source) => {
    let shader = this.gl.createShader(type);
    this.gl.shaderSource(shader, source);
    this.gl.compileShader(shader);
    let success = this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS);
    if (!success) {
      console.log(this.gl.getShaderInfoLog(shader));
      this.gl.deleteShader(shader);
      return false;
    }
    return shader;
  };

  createProgram = (vertexSource, fragmentSource) => {
    // Setup Vertext/Fragment Shader functions //
    this.vertexShader = this.createShader(this.gl.VERTEX_SHADER, vertexSource);
    this.fragmentShader = this.createShader(this.gl.FRAGMENT_SHADER, fragmentSource);
    
    // Setup Program and Attach Shader functions //
    let program = this.gl.createProgram();
    this.gl.attachShader(program, this.vertexShader);
    this.gl.attachShader(program, this.fragmentShader);
    this.gl.linkProgram(program);
    this.gl.useProgram(program);
    
    return program;
  };

  createGraphics = (vertexSource, fragmentSource, i) => {
    
    // Create the Program //
    this.program[i] = this.createProgram(vertexSource, fragmentSource);
    // Create and Bind buffer //
    this.buffer[i] = this.gl.createBuffer();
    this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer[i]);

    this.gl.bufferData(
      this.gl.ARRAY_BUFFER,
      new Float32Array([-1, 1, -1, -1, 1, -1, 1, 1]),
      this.gl.STATIC_DRAW
    );

    this.pos[i] = this.gl.getAttribLocation(this.program[i], 'pos');
    
    this.gl.vertexAttribPointer(
      this.pos[i],
      2,              // size: 2 components per iteration
      this.gl.FLOAT,  // type: the data is 32bit floats
      false,          // normalize: don't normalize the data
      0,              // stride: 0 = move forward size * sizeof(type) each iteration to get the next position
      0               // start at the beginning of the buffer
    );
    
    
    this.gl.enableVertexAttribArray(this.pos[i]);
    
    this.importProgram(i);
    
  };

  updateUniforms = (i) => {
    this.importUniforms(i);
    
    this.gl.drawArrays(
      this.gl.TRIANGLE_FAN, // primitiveType
      0,                    // Offset
      4                     // Count
    );
  };

  importProgram = (i) => {
    this.ut[i] = this.gl.getUniformLocation(this.program[i], 'time');

    this.resolution[i] = new Float32Array([300, 300]);
    this.gl.uniform2fv(
      this.gl.getUniformLocation(this.program[i],'resolution'),
      this.resolution[i]
    );
  };

  importUniforms = (i) => {
    this.gl.uniform1f(this.ut[i], (Date.now() - this.start) / 1000);
  };

  renderLoop = () => {   
    this.frame++;
    this.updateUniforms(0);
    this.animation = window.requestAnimationFrame(this.renderLoop);
  };
}

let demo = new Render(document.body);
body {
  background-color: red;
  background-image: linear-gradient(45deg, blue 25%, transparent 25%, transparent 75%, blue 75%, blue),
linear-gradient(-45deg, blue 25%, transparent 25%, transparent 75%, blue 75%, blue);
background-size: 30px 30px;
  padding: 0;
  margin: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

canvas {
  position: absolute;
  top: 0;
  right: 0;
  left: 0;
  bottom: 0;
  width: 300px;
  height: 300px;
  background: transparent;
}
<canvas width="300" height="300"></canvas>

请注意,我将背景设置为一个图案,以便我们可以看到它正在运行。

不确定你是不是指这一行

    color *= vec4(255,255, 0,255);

使用 255。WebGL 中的颜色从 0 到 1,所以也许你真的是这个意思

    color *= vec4(1, 1, 0, 1);

让我补充一下代码也有一些小问题。其中很多都是意见,所以要么接受要么离开。

  1. CSS

    让 canvas 填满屏幕的最简单方法是这个

    body { margin: 0; }
    canvas { width: 100vw; height: 100vh; display: block; }
    

    这就是你所需要的

  2. 根据调整大小事件调整大小

    我会争论there are better ways

  3. 使用Date.now

    requestAnimationFrame 传入自页面加载到回调的时间,分辨率高于 Date.now()

  4. 代码结构

    当然我不知道你的计划,但期望每对着色器使用相同的输入似乎有点不寻常。当然这是你的代码,所以也许这就是你想要的。

  5. 该代码是为多个程序设置的,但调用了一次 gl.useProgram

    似乎 updateUniforms 应该调用 gl.useProgram 所以它影响了正确的程序?

  6. 在 class 方法上使用箭头函数?

    https://medium.com/@charpeni/arrow-functions-in-class-properties-might-not-be-as-great-as-we-think-3b3551c440b1

    此外,据我所知,只有 Firefox 或 Safari 尚不支持此格式 Chrome(尽管您可以使用 Babel 进行翻译)

  7. 不是每帧都设置视口

    这是非常个人的意见,但您可能会在某个时候添加 不同大小的帧缓冲区,此时您需要始终设置视口。 Micro-optimizing一旦一帧发生的事情几乎不值得。

  8. 作为 vec3

    传入 position

    属性默认为 0,0,0,1,因此如果您没有从缓冲区中获取所有 4 个值,您将得到您所需要的。

这是一个包含其中一些更改的版本

const vertexSource = `
attribute vec4 pos;

void main() {
 gl_Position = pos;
}
`;

const fragmentSource = `
precision mediump float;

 uniform float time;
 uniform vec2 resolution;

  mat2 rotate2d(float angle){
   return mat2(cos(angle),-sin(angle),
         sin(angle),cos(angle));
 }

 float variation(vec2 v1, vec2 v2, float strength, float speed) {
  return sin(
   dot(normalize(v1), normalize(v2)) * strength + time * speed
  ) / 100.0;
 }

 vec4 paintCircle (vec2 uv, vec2 center, float rad, float width) {
  vec2 diff = center-uv;
  float len = length(diff);

  len += variation(diff, vec2(0.0, 1.0), 3.0, 2.0);
  len -= variation(diff, vec2(1.0, 0.0), 3.0, 2.0);

  float circle = 1. -smoothstep(rad-width, rad, len);

  return vec4(circle);
 }


 void main() {
  vec2 uv = gl_FragCoord.xy / resolution.xy;
  vec4 color;
  float radius = 0.15;
  vec2 center = vec2(0.5);

  color = paintCircle(uv, center, radius, .2);
  vec2 v = rotate2d(time) * uv;
  color *= vec4(1,1, 0,1);

  gl_FragColor = color;
 }
`;

class Render {
  constructor() {
    this.pos = [];
    this.program = [];
    this.buffer = [];
    this.ut = [];
    this.ures = [];
    
    this.frame = 0;
    this.canvas = document.querySelector('canvas');
    this.gl =  this.canvas.getContext('webgl');

    this.renderLoop = this.renderLoop.bind(this);

    this.init();
  }
  
   init() {
    this.createGraphics(vertexSource, fragmentSource, 0);
     
    this.renderLoop(0);
  }

  createShader(type, source) {
    let shader = this.gl.createShader(type);
    this.gl.shaderSource(shader, source);
    this.gl.compileShader(shader);
    let success = this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS);
    if (!success) {
      console.log(this.gl.getShaderInfoLog(shader));
      this.gl.deleteShader(shader);
      return false;
    }
    return shader;
  }

  createProgram (vertexSource, fragmentSource) {
    // Setup Vertext/Fragment Shader functions //
    this.vertexShader = this.createShader(this.gl.VERTEX_SHADER, vertexSource);
    this.fragmentShader = this.createShader(this.gl.FRAGMENT_SHADER, fragmentSource);
    
    // Setup Program and Attach Shader functions //
    let program = this.gl.createProgram();
    this.gl.attachShader(program, this.vertexShader);
    this.gl.attachShader(program, this.fragmentShader);
    this.gl.linkProgram(program);
    
    return program;
  }

  createGraphics (vertexSource, fragmentSource, i) {
    
    // Create the Program //
    this.program[i] = this.createProgram(vertexSource, fragmentSource);
    // Create and Bind buffer //
    this.buffer[i] = this.gl.createBuffer();
    this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer[i]);

    this.gl.bufferData(
      this.gl.ARRAY_BUFFER,
      new Float32Array([-1, 1, -1, -1, 1, -1, 1, 1]),
      this.gl.STATIC_DRAW
    );

    this.pos[i] = this.gl.getAttribLocation(this.program[i], 'pos');
    
    this.gl.vertexAttribPointer(
      this.pos[i],
      2,              // size: 2 components per iteration
      this.gl.FLOAT,  // type: the data is 32bit floats
      false,          // normalize: don't normalize the data
      0,              // stride: 0 = move forward size * sizeof(type) each iteration to get the next position
      0               // start at the beginning of the buffer
    );
    
    
    this.gl.enableVertexAttribArray(this.pos[i]);
    
    this.importProgram(i);
    
  }

  updateUniforms(i, time) {
    this.gl.useProgram(this.program[i]);
    this.importUniforms(i, time);
    
    this.gl.drawArrays(
      this.gl.TRIANGLE_FAN, // primitiveType
      0,                    // Offset
      4                     // Count
    );
  };

  importProgram(i) {
    this.ut[i] = this.gl.getUniformLocation(this.program[i], 'time');
    this.ures[i] = this.gl.getUniformLocation(this.program[i],'resolution');
  };

  importUniforms(i, time) {
    this.gl.uniform1f(this.ut[i], time / 1000);
    this.gl.uniform2f(this.ures[i], this.gl.canvas.width, this.gl.canvas.height);
  }

  resizeCanvasToDisplaySize() {
    const canvas = this.gl.canvas;
    const width = canvas.clientWidth;
    const height = canvas.clientHeight;
    const needResize = canvas.width !== width ||
                       canvas.height !== height;
    if (needResize) {
      canvas.width = width;
      canvas.height = height;
    }
    return needResize;
  }


  renderLoop(time) {   
    this.resizeCanvasToDisplaySize();
    this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);
    this.frame++;
    this.updateUniforms(0, time);
    this.animation = window.requestAnimationFrame(this.renderLoop);
  }
}

let demo = new Render(document.body);
body {
  background-color: red;
  margin: 0;
}

canvas {
  width: 100vw;
  height: 100vh;
  display: block;
}
<canvas></canvas>