WebGL FBO(实时绘图)

WebGL FBO (real-time drawing)

我正在尝试翻译此示例 Three.js - https://codepen.io/tutsplus/pen/PZmpEM 到纯 WebGL。我尝试了很多代码,我认为纹理烘焙有错误,但我的尝试没有成功,如果不难,请!

WebGL 示例

let a_Position, u_Mouse, u_Sampler, u_Resolution;

const position = {
  screenRect: null,
  xyz: [0.0, 0.0, 0.0],
  mouseDown: false,
};

function main() {
  const canvas = document.getElementById('canvas');
  const gl = canvas.getContext('webgl');
  canvas.width = canvas.clientWidth;
  canvas.height = canvas.clientHeight;

  const program = webglUtils.createProgramFromScripts(gl, ["2d-vertex-shader", "2d-fragment-shader"]);
  gl.useProgram(program);

  const tick = function() {
    render(gl, canvas, fbo, plane);
    window.requestAnimationFrame(tick, canvas);
  };
  
  a_Position = gl.getAttribLocation(program, 'a_position');
  u_Mouse = gl.getUniformLocation(program, 'u_mouse');
  u_Resolution = gl.getUniformLocation(program, 'u_resolution');
  u_Sampler = gl.getUniformLocation(program, 'u_sampler');

  const fbo = [initFramebufferObject(gl), initFramebufferObject(gl)];
  const plane = initVertexBuffersForPlane(gl);
  tick();
}

let src = 0, dst = 1, t;
function render(gl, canvas, fbo, plane) {
  gl.bindFramebuffer(gl.FRAMEBUFFER, fbo[dst]);
  gl.viewport(0, 0, 1, 1);
  drawTexture(gl, gl.program, plane, fbo[src].texture);

  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
  gl.viewport(0, 0, canvas.width, canvas.height);
  drawTexture(gl, gl.program, plane, fbo[dst].texture);

  t = src;
  src = dst;
  dst = t;
}

function drawTexture(gl, program, o, texture) {
  gl.uniform3f(u_Mouse, ...position.xyz);
  gl.uniform2f(u_Resolution, canvas.width, canvas.height);

  initAttributeVariable(gl, a_Position, o.vertexBuffer);

  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, texture);

  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, o.indexBuffer);
  gl.drawElements(gl.TRIANGLES, o.numIndices, o.indexBuffer.type, 0);
}

function initAttributeVariable(gl, a_attribute, buffer) {
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.vertexAttribPointer(a_attribute, buffer.num, buffer.type, false, 0, 0);
  gl.enableVertexAttribArray(a_attribute);
}

function initFramebufferObject(gl) {
  const framebuffer = gl.createFramebuffer(), texture = gl.createTexture();

  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 0, 255]));
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  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);


  framebuffer.texture = texture;

  gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
  gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);

  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
  gl.bindTexture(gl.TEXTURE_2D, null);

  return framebuffer;
}

function initVertexBuffersForPlane(gl) {
  const vertices = new Float32Array([1.0, 1.0, 0.0,  -1.0, 1.0, 0.0,  -1.0,-1.0, 0.0,   1.0,-1.0, 0.0]);

  const texCoords = new Float32Array([1.0, 1.0,   0.0, 1.0,   0.0, 0.0,   1.0, 0.0]);

  const indices = new Uint8Array([0, 1, 2,   0, 2, 3]);

  const o = {};

  o.vertexBuffer = initArrayBufferForLaterUse(gl, vertices, 3, gl.FLOAT);
  o.indexBuffer = initElementArrayBufferForLaterUse(gl, indices, gl.UNSIGNED_BYTE);

  o.numIndices = indices.length;

  gl.bindBuffer(gl.ARRAY_BUFFER, null);
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);

  return o;
}

function initArrayBufferForLaterUse(gl, data, num, type) {
  const buffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
  buffer.num = num;
  buffer.type = type;
  return buffer;
}

function initElementArrayBufferForLaterUse(gl, data, type) {
  const buffer = gl.createBuffer();
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer);
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, data, gl.STATIC_DRAW);
  buffer.type = type;
  return buffer;
}
  
function mouseHandlers() {
  function getPosition(e) { 
   const x = e.clientX, y = window.innerHeight - e.clientY, z = 0.05;
   position.xyz = [x, y, z];
  }

  function getRect() {
     position.screnRect = canvas.getBoundingClientRect();
  }

  function mouseDown(e) {
     position.mouseDown = true;
     getPosition(e);
  }

  function move(e) {
     if (position.mouseDown) getPosition(e); 
     else return; 
  }

  function up() {
     position.mouseDown = false;
  }

  getRect();
  canvas.addEventListener('mousedown', mouseDown); 
  canvas.addEventListener('mousemove', move); 
  canvas.addEventListener('mouseup', up); 
 }
   
mouseHandlers();
main();
body {
  margin: 0;
}

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

<script  id="2d-vertex-shader" type="x-shader/x-vertex"> 
  attribute vec4 a_position;
  void main() {
    gl_Position = a_position;
  }
</script>

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

  uniform sampler2D u_sampler;
  uniform vec2 u_resolution;
  uniform vec3 u_mouse;

  void main() {
     vec2 uv = gl_FragCoord.xy / u_resolution;
     gl_FragColor = texture2D(u_sampler, uv);
     float dist = distance(u_mouse.xy, gl_FragCoord.xy);
     gl_FragColor.rgb += u_mouse.z * max(15.0-dist,0.0);
     //gl_FragColor.gb += 0.01; /* testing FBO */
  }
</script>

<script src="https://webglfundamentals.org/webgl/resources/webgl-utils.js"></script>

所以我在移动鼠标后得到了一个结果,但是出了点问题:

应该是:

为 frambuffer 创建纹理对象时有一个明显的错误。

如果不生成mipmaps(按gl.generateMipmap),那么重要的是要设置gl.TEXTURE_MIN_FILTER。由于默认过滤器是 gl.NEAREST_MIPMAP_LINEAR 如果您不将缩小函数更改为 gl.NEARESTgl.LINEAR:

,则纹理将不完整
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

进一步查看 OpenGL ES 2.0 Full Specification - 3.7.10 Texture Completeness


我建议检查帧缓冲区的完整性:

if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE) {
    // [...]
}

帧缓冲区纹理的大小必须是 2 的幂 (WebGL 1.0)。创建具有固定大小(例如 1024x1024)的帧缓冲区:

framebuffer.size = [1024, 1024];
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, ...framebuffer.size, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(tblack));
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);

framebuffer.texture = texture;
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);

if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE) {
    alert("incomplete frambuffer");
}

确保制服设置正确。根据帧缓冲区的大小设置分辨率 (u_resolution)。鼠标的位置 (u_mouse) 必须与帧缓冲区的大小相关:

function drawTexture(gl, program, o, texture, resolution) {
    const mx = position.xyz[0] * resolution[0] / canvas.width;
    const my = position.xyz[1] * resolution[1] / canvas.height;
    gl.uniform3f(u_Mouse, mx, my, position.xyz[2]);
    gl.uniform2f(u_Resolution, resolution[0], resolution[1]);

    initAttributeVariable(gl, a_Position, o.vertexBuffer);

    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, texture);

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, o.indexBuffer);
    gl.drawElements(gl.TRIANGLES, o.numIndices, o.indexBuffer.type, 0);
}

切换当前帧缓冲区时设置视口矩形

let src = 0, dst = 1, t;
function render(gl, canvas, fbo, plane) {
    gl.bindFramebuffer(gl.FRAMEBUFFER, fbo[dst]);
    gl.viewport(0, 0, ...fbo[dst].size);
    drawTexture(gl, gl.program, plane, fbo[src].texture, fbo[src].size);

    gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    gl.viewport(0, 0, canvas.width, canvas.height);
    drawTexture(gl, gl.program, plane, fbo[dst].texture, [canvas.width, canvas.height]);

    t = src;
    src = dst;
    dst = t;
}

查看示例,其中计算到鼠标的距离根据 canvas 分辨率和帧缓冲区的比例进行缩放:

let a_Position, u_Mouse, u_Sampler;

const position = {
  screenRect: null,
  xyz: [0.0, 0.0, 0.0],
  mouseDown: false,
};

function main() {
  const canvas = document.getElementById('canvas');
  const gl = canvas.getContext('webgl');
  canvas.width = canvas.clientWidth;
  canvas.height = canvas.clientHeight;

  const program = webglUtils.createProgramFromScripts(gl, ["2d-vertex-shader", "2d-fragment-shader"]);
  gl.useProgram(program);

  const tick = function() {
    render(gl, canvas, fbo, plane);
    window.requestAnimationFrame(tick, canvas);
  };
  
  a_Position = gl.getAttribLocation(program, 'a_position');
  u_Mouse = gl.getUniformLocation(program, 'u_mouse');
  u_Sampler = gl.getUniformLocation(program, 'u_sampler');
  u_Resolution = gl.getUniformLocation(program, 'u_resolution');
  u_CanvasSize = gl.getUniformLocation(program, 'u_canvasSize');

  const fbo = [initFramebufferObject(gl), initFramebufferObject(gl)];
  const plane = initVertexBuffersForPlane(gl);
  tick();
}

let src = 0, dst = 1, t;
function render(gl, canvas, fbo, plane) {
    gl.bindFramebuffer(gl.FRAMEBUFFER, fbo[dst]);
    gl.viewport(0, 0, ...fbo[dst].size);
    drawTexture(gl, gl.program, plane, fbo[src].texture, fbo[src].size);

    gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    gl.viewport(0, 0, canvas.width, canvas.height);
    drawTexture(gl, gl.program, plane, fbo[dst].texture, [canvas.width, canvas.height]);

    t = src;
    src = dst;
    dst = t;
}

function drawTexture(gl, program, o, texture, resolution) {
    const mx = position.xyz[0] * resolution[0] / canvas.width;
    const my = position.xyz[1] * resolution[1] / canvas.height;
    gl.uniform3f(u_Mouse, mx, my, position.xyz[2]);
    gl.uniform2f(u_Resolution, resolution[0], resolution[1]);
    gl.uniform2f(u_CanvasSize, canvas.width, canvas.height);

    initAttributeVariable(gl, a_Position, o.vertexBuffer);

    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, texture);

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, o.indexBuffer);
    gl.drawElements(gl.TRIANGLES, o.numIndices, o.indexBuffer.type, 0);
}

function initAttributeVariable(gl, a_attribute, buffer) {
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.vertexAttribPointer(a_attribute, buffer.num, buffer.type, false, 0, 0);
  gl.enableVertexAttribArray(a_attribute);
}

function initFramebufferObject(gl) {
    let framebuffer = gl.createFramebuffer(), texture = gl.createTexture();

    framebuffer.size = [1024, 1024];
    let tblack = []
    for (let i= 0; i < framebuffer.size[0]*framebuffer.size[1]; i ++) tblack.push(0, 0, 0, 255);
    
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, ...framebuffer.size, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(tblack));
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);

    framebuffer.texture = texture;
    gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);

    if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE) {
      alert("incomplete frambuffer");
    }

    gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    gl.bindTexture(gl.TEXTURE_2D, null);

    return framebuffer;
}

function initVertexBuffersForPlane(gl) {
  const vertices = new Float32Array([1.0, 1.0, 0.0,  -1.0, 1.0, 0.0,  -1.0,-1.0, 0.0,   1.0,-1.0, 0.0]);

  const texCoords = new Float32Array([1.0, 1.0,   0.0, 1.0,   0.0, 0.0,   1.0, 0.0]);

  const indices = new Uint8Array([0, 1, 2,   0, 2, 3]);

  const o = {};

  o.vertexBuffer = initArrayBufferForLaterUse(gl, vertices, 3, gl.FLOAT);
  o.indexBuffer = initElementArrayBufferForLaterUse(gl, indices, gl.UNSIGNED_BYTE);

  o.numIndices = indices.length;

  gl.bindBuffer(gl.ARRAY_BUFFER, null);
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);

  return o;
}

function initArrayBufferForLaterUse(gl, data, num, type) {
  const buffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
  buffer.num = num;
  buffer.type = type;
  return buffer;
}

function initElementArrayBufferForLaterUse(gl, data, type) {
  const buffer = gl.createBuffer();
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer);
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, data, gl.STATIC_DRAW);
  buffer.type = type;
  return buffer;
}
  
function mouseHandlers() {
  function getPosition(e) { 
   const x = e.clientX, y = window.innerHeight - e.clientY, z = 0.05;
   position.xyz = [x, y, z];
  }

  function getRect() {
     position.screnRect = canvas.getBoundingClientRect();
  }

  function mouseDown(e) {
     position.mouseDown = true;
     getPosition(e);
  }

  function move(e) {
     if (position.mouseDown) getPosition(e); 
     else return; 
  }

  function up() {
     position.mouseDown = false;
  }

  getRect();
  canvas.addEventListener('mousedown', mouseDown); 
  canvas.addEventListener('mousemove', move); 
  canvas.addEventListener('mouseup', up); 
 }
   
mouseHandlers();
main();
<style>
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
</style>
<script  id="2d-vertex-shader" type="x-shader/x-vertex"> 
  attribute vec4 a_position;
  void main() {
    gl_Position = a_position;
  }
</script>

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

  uniform sampler2D u_sampler;
  uniform vec2 u_resolution;
  uniform vec2 u_canvasSize;
  uniform vec3 u_mouse;

  void main() {
     vec2 uv = gl_FragCoord.xy / u_resolution.xy;
     vec4 texColor = texture2D(u_sampler, uv);
     vec2 scale = u_canvasSize / u_resolution;
     float dist = distance(u_mouse.xy * scale, gl_FragCoord.xy * scale);
     float intensity = u_mouse.z * max(15.0-dist,0.0);
     gl_FragColor = texColor + vec4(vec3(intensity), 0.0);
  }
</script>

<canvas id="canvas"></canvas>

<script src="https://webglfundamentals.org/webgl/resources/webgl-utils.js"></script>