• Das Erstellen neuer Accounts wurde ausgesetzt. Bei berechtigtem Interesse bitte Kontaktaufnahme über die üblichen Wege. Beste Grüße der Admin

[FRAGE] webgl und ping pong framebuffer

m1au

New member
Hallo :)

Ich würde gerne für WebGL einen ping-pong-framebuffer in JavaScript programmieren.

Wenn ich das richtig verstanden haben, würde man das in OpenGL mit mehrere COLOR_ATTACHMENTs machen. In WebGL scheint es davon aber nur das COLOR_ATTACHMENT0 zu geben. Also schaut das so aus, als müsste man in WebGL etwas tricksen.

Nachdem ich etwas rumgesucht habe, bin ich auf eine Variante gestoßen, wo neben dem Default-canvas-framebuffer anscheinend noch 2 weitere framebuffer eingesetzt werden. Jeder dieser beiden framebuffer ist eine eigene Textur zugewiesen.

Das shader-programm bekommt einerseits die neu gerenderten Daten, andererseits die Daten von der Textur vom framebuffer 0. Die Daten aus diesen beiden Quellen werden im shader-programm zusammengeführt und dann in die Textur von framebuffer 1 geschrieben. Ganz zu Beginn ist die Textur vom framebuffer 0 noch leer, das ändert sich dann aber in den nächsten Durchläufen.

Sofern es noch weitere Durchläufe gibt, werden nun die Daten von framebuffer 0 und 1 geswappt. Die Textur-Daten vom ehemaligen framebuffer 1 stehen dann im framebuffer 0. In diesem neuen Durchlauf bekommt also das shader-programm einerseits wieder die neu gerenderten Daten, andererseits die Textur von framebuffer 0, das diesesmal die Daten vom letzten Durchlauf beinhaltet. Das Ergebnis landet wieder in der Textur von framebuffer 1.

Im letzten Durchlauf erhält das shader-programm einerseits wieder die neu gerenderten Daten, andererseits wieder die Textur von framebuffer 0 (so wie gewohnt). Das Ergebnis landet dann aber nicht in einer Textur, sondern im Default-canvas-framebuffer.

Stimmt das alles? Habe ich mir das alles richtig zusammengereimt?

Kennt ihr vielleicht so ein kleines Tutorial oder ein Mini-Programm, wo man sich das dann auch richtig ansehen kann? Ich habe bis jetzt leider nichts brauchbares für WebGL gefunden. WebGL scheint mir - im Gegensatz zu OpenGL - irgendwie stiefmütterlich behandelt zu werden.
 
Also ich habe das jetzt hinbekommen. Aber ich habe jetzt 2 Varianten. Weiß nicht genau, welche ich nehmen soll.

In Variante 1 habe ich mir eine eigene Framebuffer-Klasse programmiert.

In Variante 2 habe ich zusätzlich eine Klasse PingPongFramebuffer. Darin wird mit 2 Framebuffer-Instanzen gearbeitet. PingPongFramebuffer implementiert aktuell das Iterator-Pattern.

Jetzt weiß ich nicht, ob das mit diesem Iterator-Pattern wirklich gut ist. Weil eine Zählervariable (in meinem Fall die Variable i) bleibt mir trotzdem nicht erspart in meinem Programm. Jetzt habe ich einerseits in PingPongFramebuffer eine Zählervariable, und andererseits im Hauptprogramm.

PHP:
// Projekt mit Framebuffer.
// Zuerst wird eine Szene gerendert. Das Ergebnis landet in einer Textur.
// In einem zweiten Render-Durchlauf wird dann die Textur auf ein Quadrat aufgetragen.

"use strict";

//
// initWebGL
//
// Initialize WebGL, returning the GL context or null if
// WebGL isn't available or could not be initialized.
//
function initWebGL(canvas) {
  let gl = null;
  try {
    gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
  }
  catch(e) {
  }
  if (!gl)
    throw "Unable to initialize WebGL. Your browser may not support it.";
  return gl;
}

//
// getShader
//
// Loads a shader program by scouring the current document,
// looking for a script with the specified ID.
//
function getShader(gl, id) {
  var shaderScript = document.getElementById(id);
  // Didn't find an element with the specified ID; abort.
  if (!shaderScript) {
    return null;
  }
  // Walk through the source element's children, building the
  // shader source string.
  var theSource = "";
  var currentChild = shaderScript.firstChild;
  while(currentChild) {
    if (currentChild.nodeType == 3) {
      theSource += currentChild.textContent;
    }
    currentChild = currentChild.nextSibling;
  }
  // Now figure out what type of shader script we have,
  // based on its MIME type.
  var shader;
  if (shaderScript.type == "x-shader/x-fragment") {
    shader = gl.createShader(gl.FRAGMENT_SHADER);
  } else if (shaderScript.type == "x-shader/x-vertex") {
    shader = gl.createShader(gl.VERTEX_SHADER);
  } else {
    return null;  // Unknown shader type
  }
  // Send the source to the shader object
  gl.shaderSource(shader, theSource);
  // Compile the shader program
  gl.compileShader(shader);
  // See if it compiled successfully
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    alert("An error occurred compiling the shaders: " + gl.getShaderInfoLog(shader));
    return null;
  }
  return shader;
}

class Framebuffer {
   /**
    * @param WebGLRenderingContext _gl
    * @throws string
    */
   constructor(_gl) {
      if(!(_gl instanceof WebGLRenderingContext))
         throw "gl must be of type WebGLRenderingContext";
      
      // frame-buffer:
      var framebuffer = _gl.createFramebuffer();
      
      // auf selbst definierten frame-buffer switchen:
      _gl.bindFramebuffer(_gl.FRAMEBUFFER, framebuffer); // bind

      // 2 dynamische Texturen für ping-pong-buffer:
      var texture = _gl.createTexture();
      _gl.bindTexture(_gl.TEXTURE_2D, texture);
      _gl.texParameteri(_gl.TEXTURE_2D, _gl.TEXTURE_MAG_FILTER, _gl.LINEAR);
      _gl.texParameteri(_gl.TEXTURE_2D, _gl.TEXTURE_MIN_FILTER, _gl.LINEAR);
      _gl.texImage2D(_gl.TEXTURE_2D, 0, _gl.RGBA, 512, 512, 0, _gl.RGBA, _gl.UNSIGNED_BYTE, null); // null bezieht sich auf die Daten

      // framebuffer-attachement: Für die Farben Texturen dem frame-buffer zuordnen:
      _gl.framebufferTexture2D(_gl.FRAMEBUFFER, _gl.COLOR_ATTACHMENT0, _gl.TEXTURE_2D, texture, 0);
      
      // depth-buffer:
      var depthbuffer = _gl.createRenderbuffer();
      _gl.bindRenderbuffer(_gl.RENDERBUFFER, depthbuffer);
      _gl.renderbufferStorage(_gl.RENDERBUFFER, _gl.DEPTH_COMPONENT16, 512, 512);

      // framebuffer-attachement: Für die z-Werte depth-buffer dem frame-buffer zuordnen:
      _gl.framebufferRenderbuffer(_gl.FRAMEBUFFER, _gl.DEPTH_ATTACHMENT, _gl.RENDERBUFFER, depthbuffer);
      
      /**
       * @return WebGLFramebuffer
       */
      this.getFramebuffer = function() {
         return framebuffer;
      };
      /**
       * @return WebGLTexture
       */
      this.getTexture = function() {
         return texture;
      };
      /**
       * @return WebGLRenderbuffer
       */
      this.getDepthbuffer = function() {
         return depthbuffer;
      };
   }
   /**
    * @return WebGLFramebuffer
    */
   get framebuffer() {
      return this.getFramebuffer();
   }
   /**
    * @return WebGLTexture
    */
   get texture() {
      return this.getTexture();
   }
   /**
    * @return WebGLRenderbuffer
    */
   get depthbuffer() {
      return this.getDepthbuffer();
   }
}

/**
 * implementiert das Iterator-Pattern
 */
class PingPongFramebuffer {
   /**
    * @param WebGLRenderingContext _gl
    * @param int _count >= 0, (maximale) Anzahl an Durchläufen.
    * @throws string
    */
   constructor(_gl, _count) {
      if(!(_gl instanceof WebGLRenderingContext))
         throw "gl must be of type WebGLRenderingContext";
      var gl = _gl;
      if(isNaN(_count) || _count < 0)
         throw "count must be of type integer >= 0";
      var count = _count;
      
      var framebuffers = [
         new Framebuffer(gl),
         new Framebuffer(gl)
      ];
      var shaderOutputFramebuffer = 0;
      var i = 0; // Zähler
      
      /** 
       * @return boolean
       */
      this.hasNext = function() {
         if(i >= count) {
            return false;
         }
         
         // Textur als shader-input:
         if(i == 0) {
            // Beim ersten Durchlauf muss die Textur während des renderns deaktiviert sein.
            // Macht man das nicht, liefert Chrome den folgenden Fehler:
            //    [GroupMarkerNotSet(crbug.com/242999)!:5C915608]
            //    GL ERROR :GL_INVALID_OPERATION : glDrawElements: Source and destination textures of the draw are the same.
            // Beim Firefox kommt dieser Fehler nicht.
            gl.bindTexture(gl.TEXTURE_2D, null);
            // Ist shaderInputTexture null bedeutet das, dass in den Shader keine Texturdaten reinkommen.
         }
         else {
            // Textur für shader-input vorbereiten:
            let shaderInputTexture = (shaderOutputFramebuffer + 1) % 2;
            gl.bindTexture(gl.TEXTURE_2D, framebuffers[shaderInputTexture].texture);
            gl.generateMipmap(gl.TEXTURE_2D);
         }
         
         // framebuffer als shader-output:
         if(i < count - 1) {
            gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffers[shaderOutputFramebuffer].framebuffer);
         }
         else {
            gl.bindFramebuffer(gl.FRAMEBUFFER, null);
            // Wird als Parameter null übergeben, wird der Default-framebuffer als Shader-Ausgabe verwendet.
            // D. h. es erfolgt eine Ausgabe im canvas.
         }
         
         return true;
      };
      this.next = function() {
         shaderOutputFramebuffer = (shaderOutputFramebuffer + 1) % 2;
         ++i;
      };
   }
}

function start() {
   let canvas = document.getElementById("glcanvas");
   let gl = initWebGL(canvas);  // Initialize the GL context
   if (gl) {
      gl.clearDepth(1);         // Clear everything
      gl.enable(gl.DEPTH_TEST); // Enable depth testing
      gl.depthFunc(gl.LEQUAL);  // Near things obscure far things

      // shader:
      let fragmentShader = getShader(gl, "shader-fs");
      let vertexShader = getShader(gl, "shader-vs");
      let shaderProgram = gl.createProgram();
      gl.attachShader(shaderProgram, vertexShader);
      gl.attachShader(shaderProgram, fragmentShader);
      gl.linkProgram(shaderProgram);
      if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS))
         throw "Unable to initialize the shader program."; 
      gl.useProgram(shaderProgram);

      // Positionen Vertices:
      let vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
      gl.enableVertexAttribArray(vertexPositionAttribute);
      let vertices = [
          -1, -1,  0, // links unten
           1, -1,  0,
           1,  1,  0,
          -1,  1,  0
      ];
      let verticesBuffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, verticesBuffer);
      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
      gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);
      
      // Indizes:
      let vertexIndices = [
         0, 1, 2,   0, 2, 3  
      ];
      let verticesIndexBuffer = gl.createBuffer();
      gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, verticesIndexBuffer);
      gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(vertexIndices), gl.STATIC_DRAW);
         
      // Farben für beide Renderdurchläufe:
      let verticesColorBuffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, verticesColorBuffer);
      let vertexColorAttribute = gl.getAttribLocation(shaderProgram, "aVertexColor");
      gl.enableVertexAttribArray(vertexColorAttribute);
        
      // Uniforms für beide Renderdurchläufe:
      let useTexturesUniform = gl.getUniformLocation(shaderProgram, "uUseTextures");
      let samplerUniform = gl.getUniformLocation(shaderProgram, "uSampler");
      let pUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");
         
      // model-view-Matrix für beide Renderdurchläufe gleich gewählt:
      let mvUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix");
      let mvMatrix = Matrix.I(4).x(Matrix.Translation($V([0, 0, -6])).ensure4x4());
      gl.uniformMatrix4fv(mvUniform, false, new Float32Array(mvMatrix.flatten()));
           
      let vertexColors = [];
      if(true) { // ob mit oder ohne Textur rendern
         // Textur-Koordinaten:
         let textureCoordAttribute = gl.getAttribLocation(shaderProgram, "aTextureCoord");
         gl.enableVertexAttribArray(textureCoordAttribute);
         let textureCoordinates = [
            0, 0, // links unten ist u = v = 0
            1, 0,
            1, 1,
            0, 1
         ];
         let verticesTextureCoordBuffer = gl.createBuffer();
         gl.bindBuffer(gl.ARRAY_BUFFER, verticesTextureCoordBuffer);
         gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoordinates), gl.STATIC_DRAW);
         gl.vertexAttribPointer(textureCoordAttribute, 2, gl.FLOAT, false, 0, 0);
         
         // Hintergrundfarben für die Durchläufe:
         let clearColors = [
            [1, 0, 0], // rot
            [0, 1, 0], // grün
            [0, 0, 1]  // blau
         ];
         
         let count = 3; // Anzahl der Durchläufe
         let ppFramebuffer = new PingPongFramebuffer(gl, count); // framebuffer/Texturen
         let i = 0; // aktueller Durchlauf
         while(ppFramebuffer.hasNext()) {         
            // Ob im shader mit eingehenden Texturdaten gearbeitet werden soll:
            gl.uniform1i(useTexturesUniform, i > 0);
               
            // Farben:
            if(i == 0) {
               // Im ersten Durchlauf wird das Quadrat eingefärbt.
               vertexColors = [
                  1, 1, 1, // weiß
                  1, 0, 0, // rot
                  0, 1, 0, // grün
                  0, 0, 1  // blau
               ];
            }
            else {
               // In alles weiteren Durchläufen soll der Textur keine weitere Farbe hinzugefügt werden.
               vertexColors = [
                  0, 0, 0,
                  0, 0, 0,
                  0, 0, 0,
                  0, 0, 0
               ];
            };
            gl.bindBuffer(gl.ARRAY_BUFFER, verticesColorBuffer);
            gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexColors), gl.STATIC_DRAW);
            gl.vertexAttribPointer(vertexColorAttribute, 3, gl.FLOAT, false, 0, 0);
            
            // projection-Matrix:
            let perspectiveMatrix;
            if(i < count - 1) {
               let viewportSize = 512;
               perspectiveMatrix = makePerspective(45, viewportSize / viewportSize, 0.1, 100);
               gl.viewport(0, 0, viewportSize, viewportSize);
            }
            else {
               // im letzten Durchlauf wird im canvas ausgegeben:
               perspectiveMatrix = makePerspective(45, canvas.width / canvas.height, 0.1, 100);
               gl.viewport(0, 0, canvas.width, canvas.height);
            }
         
            // rendern:
            console.log(i);
            gl.clearColor(clearColors[i][0], clearColors[i][1], clearColors[i][2], 1);
            gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
            gl.uniformMatrix4fv(pUniform, false, new Float32Array(perspectiveMatrix.flatten()));
            gl.drawElements(gl.TRIANGLES, vertexIndices.length, gl.UNSIGNED_SHORT, 0);
            
            // framebuffer-swap:
            ppFramebuffer.next();
            
            // nächster Durchlauf:
            ++i;
         }        
      }
      else {
         // Farben:
         vertexColors = [
            1, 1, 1, // weiß
            1, 1, 1,   
            1, 1, 1,   
            1, 1, 1     
         ];
         gl.bindBuffer(gl.ARRAY_BUFFER, verticesColorBuffer);
         gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexColors), gl.STATIC_DRAW);
         gl.vertexAttribPointer(vertexColorAttribute, 3, gl.FLOAT, false, 0, 0);
         
         // ohne Textur:
         gl.uniform1i(useTexturesUniform, false); 

         // projection-Matrix:
         let perspectiveMatrix = makePerspective(45, canvas.width / canvas.height, 0.1, 100);
         gl.uniformMatrix4fv(pUniform, false, new Float32Array(perspectiveMatrix.flatten()));

         // rendern:
         gl.viewport(0, 0, canvas.width, canvas.height);
         gl.clearColor(0, 0, 0, 1);
         gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
         gl.drawElements(gl.TRIANGLES, vertexIndices.length, gl.UNSIGNED_SHORT, 0);
      }
   }
}

Interessante Zeilen:

290: PingPongFramebuffer-Instanz angelegt
291: Schleife (hasNext), Framebuffer und Textur bereitgestellt
340: nächsten Durchlauf vorbereiten (next), hier wird geswappt

Das mit den 2 Schleifenzählern ist irgendwie unelegant, oder?
 
Zuletzt bearbeitet:
Zurück
Oben