ข้อมูลพื้นฐานเกี่ยวกับ WebGL

ข้อมูลพื้นฐานเกี่ยวกับ WebGL

WebGL ช่วยให้สามารถแสดงกราฟิก 3 มิติแบบเรียลไทม์ที่น่าทึ่งในเบราว์เซอร์ของคุณได้ แต่สิ่งที่หลายๆ คนไม่ทราบก็คือ WebGL เป็น API แบบ 2 มิติ ไม่ใช่ API 3 มิติ ฉันขออธิบายเพิ่มเติม

WebGL สนใจแค่ 2 อย่างเท่านั้น พิกัด Clipspace ในระดับ 2 มิติและสี หน้าที่ของคุณในฐานะโปรแกรมเมอร์ที่ใช้ WebGL คือการจัดหา 2 สิ่งนี้ให้กับ WebGL โดยคุณต้องใส่ "เฉดสี" 2 สีเพื่อดำเนินการนี้ ตัวปรับเฉดสี Vertex ที่ระบุพิกัดคลิปสเปซและตัวปรับเฉดสีส่วนย่อยที่ให้สี พิกัด Clipspace จะเปลี่ยนจาก -1 ไปยัง +1 เสมอ ไม่ว่าแคนวาสจะเป็นขนาดใดก็ตาม ต่อไปนี้คือตัวอย่าง WebGL ง่ายๆ ที่แสดงให้เห็น WebGL ในรูปแบบที่เรียบง่ายที่สุด

// Get A WebGL context
var canvas = document.getElementById("canvas");
var gl = canvas.getContext("experimental-webgl");

// setup a GLSL program
var vertexShader = createShaderFromScriptElement(gl, "2d-vertex-shader");
var fragmentShader = createShaderFromScriptElement(gl, "2d-fragment-shader");
var program = createProgram(gl, [vertexShader, fragmentShader]);
gl.useProgram(program);

// look up where the vertex data needs to go.
var positionLocation = gl.getAttribLocation(program, "a_position");

// Create a buffer and put a single clipspace rectangle in
// it (2 triangles)
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(
    gl.ARRAY_BUFFER,
    new Float32Array([
        -1.0, -1.0,
         1.0, -1.0,
        -1.0,  1.0,
        -1.0,  1.0,
         1.0, -1.0,
         1.0,  1.0]),
    gl.STATIC_DRAW);
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);

// draw
gl.drawArrays(gl.TRIANGLES, 0, 6);

นี่คือเครื่องมือให้เฉดสี 2 ตัว

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;

void main() {
  gl_Position = vec4(a_position, 0, 1);
}
</script>

<script id="2d-fragment-shader" type="x-shader/x-fragment">
void main() {
  gl_FragColor = vec4(0,1,0,1);  // green
}
</script>

ขอย้ำอีกครั้งว่า พิกัดของคลิปสเปซจะเปลี่ยนจาก -1 ถึง +1 เสมอ ไม่ว่าผืนผ้าใบจะมีขนาดเท่าใดก็ตาม ในกรณีข้างต้น คุณจะเห็นว่าเรากำลังทำอะไรอยู่นอกจากส่งข้อมูลตำแหน่งของเราโดยตรง เนื่องจากข้อมูลตำแหน่งอยู่ในคลิปสเปซอยู่แล้ว คุณไม่จำเป็นต้องดำเนินการใดๆ ถ้าต้องการให้ 3D คุณต้องจัดหาตัวให้เฉดสีที่แปลงจาก 3 มิติเป็น 2 มิติ เพราะ WebGL เป็น 2D API! สำหรับภาพ 2 มิติ คุณอาจต้องทำงานเป็นพิกเซลมากกว่า Clipspace นั้น เรามาเปลี่ยนตัวปรับแสงเงาเพื่อที่เราจะได้สามารถใส่สี่เหลี่ยมเป็นพิกเซลและแปลงเป็นพื้นที่คลิปให้เราได้ นี่คือ Vertex Shades ใหม่

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;

uniform vec2 u_resolution;

void main() {
   // convert the rectangle from pixels to 0.0 to 1.0
   vec2 zeroToOne = a_position / u_resolution;

   // convert from 0->1 to 0->2
   vec2 zeroToTwo = zeroToOne * 2.0;

   // convert from 0->2 to -1->+1 (clipspace)
   vec2 clipSpace = zeroToTwo - 1.0;

   gl_Position = vec4(clipSpace, 0, 1);
}
</script>

ตอนนี้เราสามารถเปลี่ยนข้อมูลจากคลิปสเปซเป็นพิกเซลได้

// set the resolution
var resolutionLocation = gl.getUniformLocation(program, "u_resolution");
gl.uniform2f(resolutionLocation, canvas.width, canvas.height);

// setup a rectangle from 10,20 to 80,30 in pixels
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
    10, 20,
    80, 20,
    10, 30,
    10, 30,
    80, 20,
    80, 30]), gl.STATIC_DRAW);

คุณอาจสังเกตเห็นว่าสี่เหลี่ยมผืนผ้าอยู่ใกล้กับส่วนล่างของพื้นที่นั้น WebGL ถือว่ามุมซ้ายล่างเป็น 0,0 เพื่อให้เป็นมุมซ้ายบนแบบเดิมที่ใช้สำหรับ API กราฟิก 2 มิติ เราแค่พลิกพิกัด y

gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);

เรามาลองทำให้โค้ดที่กำหนดรูปสี่เหลี่ยมผืนผ้าเป็นฟังก์ชันเพื่อให้เราสามารถเรียกใช้โค้ดนั้นสำหรับสี่เหลี่ยมผืนผ้าขนาดต่างๆ กัน ขณะที่เราดำเนินการ เราจะตั้งค่าสีให้ได้ ก่อนอื่นเราทำให้ตัวปรับแสงเงาบางส่วนป้อนอินพุตที่เป็นสีเดียวกัน

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

uniform vec4 u_color;

void main() {
   gl_FragColor = u_color;
}
</script>

และนี่คือโค้ดใหม่ที่วาดสี่เหลี่ยมผืนผ้า 50 รูปในที่ต่างๆ และสุ่มสี

...

  var colorLocation = gl.getUniformLocation(program, "u_color");
  ...
  // Create a buffer
  var buffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.enableVertexAttribArray(positionLocation);
  gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);

  // draw 50 random rectangles in random colors
  for (var ii = 0; ii < 50; ++ii) {
    // Setup a random rectangle
    setRectangle(
        gl, randomInt(300), randomInt(300), randomInt(300), randomInt(300));

    // Set a random color.
    gl.uniform4f(colorLocation, Math.random(), Math.random(), Math.random(), 1);

    // Draw the rectangle.
    gl.drawArrays(gl.TRIANGLES, 0, 6);
  }
}

// Returns a random integer from 0 to range - 1.
function randomInt(range) {
  return Math.floor(Math.random() * range);
}

// Fills the buffer with the values that define a rectangle.
function setRectangle(gl, x, y, width, height) {
  var x1 = x;
  var x2 = x + width;
  var y1 = y;
  var y2 = y + height;
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
     x1, y1,
     x2, y1,
     x1, y2,
     x1, y2,
     x2, y1,
     x2, y2]), gl.STATIC_DRAW);
}

เราหวังว่าคุณจะเห็นว่า จริงๆ แล้ว WebGL เป็น API ที่ไม่ซับซ้อน แม้ว่าการทำแบบ 3 มิติอาจมีความซับซ้อนมากขึ้น แต่คุณเพิ่มข้อมูลแทรกเข้ามาในรูปแบบของโปรแกรมให้เฉดสีที่ซับซ้อนยิ่งขึ้น WebGL API นั้นเป็นแบบ 2 มิติ และค่อนข้างเรียบง่าย

type="x-shader/x-vertex" และ type="x-shader/x-fulfillment" หมายถึงอะไร

แท็ก <script> โดยค่าเริ่มต้นจะมี JavaScript อยู่ จะไม่ป้อนประเภท หรือใส่ type="javascript" หรือ type="text/javascript" แล้วเบราว์เซอร์จะตีความเนื้อหาเป็น JavaScript หากคุณใส่อย่างอื่น เบราว์เซอร์จะไม่สนใจเนื้อหาของแท็กสคริปต์

เราสามารถใช้ฟีเจอร์นี้เพื่อเก็บตัวให้เฉดสีในแท็กสคริปต์ ยิ่งไปกว่านั้น เราสามารถสร้างประเภทของเราเองและใช้ใน JavaScript เพื่อตัดสินใจว่าจะคอมไพล์ตัวปรับแสงเงาเป็นจุดปรับเฉดสีแบบจุดรวมจุดหรือตัวปรับแสงเงาบางส่วน

ในกรณีนี้ ฟังก์ชัน createShaderFromScriptElement จะมองหาสคริปต์ที่มี id ที่ระบุ จากนั้นดูที่ type เพื่อตัดสินใจว่าจะสร้างตัวปรับแสงเงาประเภทใด

การประมวลผลรูปภาพ WebGL

การประมวลผลรูปภาพนั้นทำได้ง่ายใน WebGL ง่ายแค่ไหน โปรดอ่านด้านล่าง

ในการวาดภาพใน WebGL เราต้องใช้พื้นผิว คล้ายกับที่ WebGL คาดหวังพิกัดคลิปสเปซเมื่อแสดงภาพแทนพิกเซล WebGL คาดหวังพิกัดพื้นผิวเมื่ออ่านพื้นผิว พิกัดพื้นผิวจะเริ่มจาก 0.0 ถึง 1.0 ไม่ว่าพื้นผิวจะมีขนาดใดก็ตาม เนื่องจากเราจะวาดรูปสี่เหลี่ยมผืนผ้าเพียงรูปเดียว (หรือ 2 รูปสามเหลี่ยม) เราจึงต้องบอก WebGL ว่าจุดไหนในพื้นผิวของแต่ละจุดในรูปสี่เหลี่ยม เราจะส่งข้อมูลนี้จาก Vertex Shades ไปยังตัวปรับแสงเงาส่วนโดยใช้ตัวแปรพิเศษที่เรียกว่า 'varying' ซึ่งเรียกว่า "ที่แตกต่างกัน" เนื่องจากจะแตกต่างกันไป WebGL จะประมาณค่าที่เราให้ใน Vertex Shades ขณะดึงแต่ละพิกเซลโดยใช้ Fragment Shades การใช้ตัวปรับแสงเงาของจุดยอดจากส่วนท้ายของส่วนก่อนหน้า เราจำเป็นต้องเพิ่มแอตทริบิวต์เพื่อส่งผ่านพิกัดพื้นผิวแล้วส่งต่อไปยังตัวปรับแสงเงาส่วนย่อย

attribute vec2 a_texCoord;
...
varying vec2 v_texCoord;

void main() {
   ...
   // pass the texCoord to the fragment shader
   // The GPU will interpolate this value between points
   v_texCoord = a_texCoord;
}

จากนั้นเราจะสร้างตัวปรับแสงเงาบางส่วนเพื่อค้นหาสีจากพื้นผิว

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

// our texture
uniform sampler2D u_image;

// the texCoords passed in from the vertex shader.
varying vec2 v_texCoord;

void main() {
   // Look up a color from the texture.
   gl_FragColor = texture2D(u_image, v_texCoord);
}
</script>

ขั้นตอนสุดท้าย เราต้องโหลดรูปภาพ สร้างพื้นผิว และคัดลอกรูปภาพลงในพื้นผิว เนื่องจากเราโหลดภาพเบราว์เซอร์ไม่พร้อมกัน เราจึงต้องจัดเรียงโค้ดใหม่เล็กน้อยเพื่อรอให้พื้นผิวโหลด เมื่อโหลดเสร็จแล้ว เราจะวาดรูปให้

function main() {
  var image = new Image();
  image.src = "http://someimage/on/our/server";  // MUST BE SAME DOMAIN!!!
  image.onload = function() {
    render(image);
  }
}

function render(image) {
  ...
  // all the code we had before.
  ...
  // look up where the texture coordinates need to go.
  var texCoordLocation = gl.getAttribLocation(program, "a_texCoord");

  // provide texture coordinates for the rectangle.
  var texCoordBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
      0.0,  0.0,
      1.0,  0.0,
      0.0,  1.0,
      0.0,  1.0,
      1.0,  0.0,
      1.0,  1.0]), gl.STATIC_DRAW);
  gl.enableVertexAttribArray(texCoordLocation);
  gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);

  // Create a texture.
  var texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);

  // Set the parameters so we can render any size image.
  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.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

  // Upload the image into the texture.
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
  ...
}

ไม่น่าตื่นเต้นเกินไป มาปรับรูปกันเถอะ ลองสลับสีแดงกับน้ำเงินดูไหม

...
gl_FragColor = texture2D(u_image, v_texCoord).bgra;
...

ถ้าเราต้องการประมวลผลรูปภาพที่ดูพิกเซลอื่นๆ จริงๆ ล่ะ เนื่องจาก WebGL อ้างอิงพื้นผิวในพิกัดของพื้นผิวซึ่งเริ่มจาก 0.0 ถึง 1.0 เราจึงคำนวณได้ว่า 1 พิกเซลจะเคลื่อนไหวเท่าใดด้วยสูตรคำนวณง่ายๆ onePixel = 1.0 / textureSize นี่คือ Fragment Shaper ที่จะหาค่าเฉลี่ยพิกเซลด้านซ้ายและขวาของแต่ละพิกเซลในพื้นผิว

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

// our texture
uniform sampler2D u_image;
uniform vec2 u_textureSize;

// the texCoords passed in from the vertex shader.
varying vec2 v_texCoord;

void main() {
   // compute 1 pixel in texture coordinates.
   vec2 onePixel = vec2(1.0, 1.0) / u_textureSize;

   // average the left, middle, and right pixels.
   gl_FragColor = (
       texture2D(u_image, v_texCoord) +
       texture2D(u_image, v_texCoord + vec2(onePixel.x, 0.0)) +
       texture2D(u_image, v_texCoord + vec2(-onePixel.x, 0.0))) / 3.0;
}
</script>

จากนั้นเราต้องส่งผ่านขนาดของพื้นผิวจาก JavaScript

...
var textureSizeLocation = gl.getUniformLocation(program, "u_textureSize");
...
// set the size of the image
gl.uniform2f(textureSizeLocation, image.width, image.height);
...

ตอนนี้เมื่อเรารู้วิธีอ้างอิงพิกเซลอื่นๆ แล้ว ลองใช้เคอร์เนลแบบ Convolution เพื่อประมวลผลรูปภาพทั่วไปหลายๆ แบบ ในกรณีนี้ เราจะใช้เคอร์เนล 3x3 เคอร์เนลคอนโวลูชันเป็นเพียงเมทริกซ์ 3x3 ที่แต่ละรายการในเมทริกซ์จะแสดงจำนวนที่ใช้คูณ 8 พิกเซลรอบๆ พิกเซลที่เรากําลังแสดงผล จากนั้นเราจะหารผลลัพธ์ด้วยน้ำหนักของเคอร์เนล (ผลรวมของค่าทั้งหมดในเคอร์เนล) หรือ 1.0 ซึ่งจะมากกว่านั้น บทความนี้เป็นบทความที่ดีทีเดียว และนี่คือบทความอื่นที่แสดงโค้ดจริงหากคุณเขียนด้วยมือใน C++ ในกรณีของเรา เราจะทำอย่างนั้นในตัวสร้างเงาภายใน และนี่คือ Fragment Shades ใหม่

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

// our texture
uniform sampler2D u_image;
uniform vec2 u_textureSize;
uniform float u_kernel[9];

// the texCoords passed in from the vertex shader.
varying vec2 v_texCoord;

void main() {
   vec2 onePixel = vec2(1.0, 1.0) / u_textureSize;
   vec4 colorSum =
     texture2D(u_image, v_texCoord + onePixel * vec2(-1, -1)) * u_kernel[0] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 0, -1)) * u_kernel[1] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 1, -1)) * u_kernel[2] +
     texture2D(u_image, v_texCoord + onePixel * vec2(-1,  0)) * u_kernel[3] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 0,  0)) * u_kernel[4] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 1,  0)) * u_kernel[5] +
     texture2D(u_image, v_texCoord + onePixel * vec2(-1,  1)) * u_kernel[6] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 0,  1)) * u_kernel[7] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 1,  1)) * u_kernel[8] ;
   float kernelWeight =
     u_kernel[0] +
     u_kernel[1] +
     u_kernel[2] +
     u_kernel[3] +
     u_kernel[4] +
     u_kernel[5] +
     u_kernel[6] +
     u_kernel[7] +
     u_kernel[8] ;

   if (kernelWeight <= 0.0) {
     kernelWeight = 1.0;
   }

   // Divide the sum by the weight but just use rgb
   // we'll set alpha to 1.0
   gl_FragColor = vec4((colorSum / kernelWeight).rgb, 1.0);
}
</script>

ใน JavaScript เราจำเป็นต้องจัดหาเคอร์เนล Convolution

...
var kernelLocation = gl.getUniformLocation(program, "u_kernel[0]");
...
var edgeDetectKernel = [
    -1, -1, -1,
    -1,  8, -1,
    -1, -1, -1
];
gl.uniform1fv(kernelLocation, edgeDetectKernel);
...

เราหวังว่าการประมวลผลภาพใน WebGL จะช่วยให้คุณทำได้ง่ายๆ ต่อไปฉันจะพูดถึงวิธีใช้เอฟเฟกต์มากกว่าหนึ่งรายการกับรูปภาพ

คำนำหน้า , u และ v_ ในตัวแปรใน GLSL คืออะไร

นั่นเป็นเพียงรูปแบบการตั้งชื่อเท่านั้น a_ สำหรับแอตทริบิวต์ซึ่งเป็นข้อมูลจากบัฟเฟอร์ u_ สำหรับเครื่องแบบซึ่งเป็นอินพุตไปยังตัวให้เฉดสี v_ สำหรับค่าต่างๆ ซึ่งเป็นค่าที่ส่งจากตัวปรับแสงเงาจุดยอดไปยังตัวปรับเฉดสีส่วนย่อยและแทรก (หรือแตกต่างกัน) ระหว่างจุดยอดของแต่ละพิกเซลที่วาด

การใช้เอฟเฟกต์หลายรายการ

คำถามถัดไปที่เห็นได้ชัดที่สุดเกี่ยวกับการประมวลผลรูปภาพคือ จะใส่เอฟเฟกต์หลายอย่างได้อย่างไร

คุณอาจลองสร้างให้เฉดสีได้ทันที จัดทำ UI ที่ให้ผู้ใช้เลือกเอฟเฟกต์ที่ต้องการใช้ แล้วสร้างตัวปรับแสงเงาที่ใส่เอฟเฟกต์ได้ทั้งหมด แต่บางครั้งก็อาจทำไม่ได้ แต่เทคนิคนั้นมักจะใช้สร้างเอฟเฟกต์สำหรับกราฟิกแบบเรียลไทม์ วิธีที่ยืดหยุ่นมากขึ้นคือการใช้พื้นผิวเพิ่ม 2 พื้นผิวและเรนเดอร์ไปยังแต่ละพื้นผิวโดยสลับกันไปกลับมา แล้วนำเอฟเฟ็กต์ถัดไปไปใช้ทุกครั้ง

Original Image -> [Blur]        -> Texture 1
Texture 1      -> [Sharpen]     -> Texture 2
Texture 2      -> [Edge Detect] -> Texture 1
Texture 1      -> [Blur]        -> Texture 2
Texture 2      -> [Normal]      -> Canvas

ในการดำเนินการดังกล่าว เราต้องสร้าง Framebuffers ใน WebGL และ OpenGL จริงๆ แล้ว Framebuffer เป็นชื่อที่ไม่ดี เฟรมบัฟเฟอร์ของ WebGL/OpenGL เป็นเพียงคอลเล็กชันของสถานะ ไม่ใช่บัฟเฟอร์แบบใดแบบหนึ่ง แต่การแนบพื้นผิวกับเฟรมบัฟเฟอร์จะทำให้เราแสดงผลเป็นพื้นผิวนั้นได้ ก่อนอื่น เรามาเปลี่ยนโค้ดการสร้างพื้นผิวเดิมให้เป็นฟังก์ชันกัน

function createAndSetupTexture(gl) {
  var texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);

  // Set up texture so we can render any size image and so we are
  // working with pixels.
  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.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

  return texture;
}

// Create a texture and put the image in it.
var originalImageTexture = createAndSetupTexture(gl);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);

คราวนี้ลองใช้ฟังก์ชันนั้นเพื่อสร้างพื้นผิวอีก 2 แบบแล้วแนบเข้ากับ 2 เฟรมบัฟเฟอร์

// create 2 textures and attach them to framebuffers.
var textures = [];
var framebuffers = [];
for (var ii = 0; ii < 2; ++ii) {
  var texture = createAndSetupTexture(gl);
  textures.push(texture);

  // make the texture the same size as the image
  gl.texImage2D(
      gl.TEXTURE_2D, 0, gl.RGBA, image.width, image.height, 0,
      gl.RGBA, gl.UNSIGNED_BYTE, null);

  // Create a framebuffer
  var fbo = gl.createFramebuffer();
  framebuffers.push(fbo);
  gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);

  // Attach a texture to it.
  gl.framebufferTexture2D(
      gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
}

ตอนนี้เรามาสร้างชุดเคอร์เนลและลิสต์ของเคอร์เนลที่จะใช้กัน

// Define several convolution kernels
var kernels = {
  normal: [
    0, 0, 0,
    0, 1, 0,
    0, 0, 0
  ],
  gaussianBlur: [
    0.045, 0.122, 0.045,
    0.122, 0.332, 0.122,
    0.045, 0.122, 0.045
  ],
  unsharpen: [
    -1, -1, -1,
    -1,  9, -1,
    -1, -1, -1
  ],
  emboss: [
     -2, -1,  0,
     -1,  1,  1,
      0,  1,  2
  ]
};

// List of effects to apply.
var effectsToApply = [
  "gaussianBlur",
  "emboss",
  "gaussianBlur",
  "unsharpen"
];

และสุดท้าย นำแต่ละแบบไปใช้ เช่น ปิงปอง ว่าพื้นผิวใดที่เราแสดงผลด้วย

// start with the original image
gl.bindTexture(gl.TEXTURE_2D, originalImageTexture);

// don't y flip images while drawing to the textures
gl.uniform1f(flipYLocation, 1);

// loop through each effect we want to apply.
for (var ii = 0; ii < effectsToApply.length; ++ii) {
  // Setup to draw into one of the framebuffers.
  setFramebuffer(framebuffers[ii % 2], image.width, image.height);

  drawWithKernel(effectsToApply[ii]);

  // for the next draw, use the texture we just rendered to.
  gl.bindTexture(gl.TEXTURE_2D, textures[ii % 2]);
}

// finally draw the result to the canvas.
gl.uniform1f(flipYLocation, -1);  // need to y flip for canvas
setFramebuffer(null, canvas.width, canvas.height);
drawWithKernel("normal");

function setFramebuffer(fbo, width, height) {
  // make this the framebuffer we are rendering to.
  gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);

  // Tell the shader the resolution of the framebuffer.
  gl.uniform2f(resolutionLocation, width, height);

  // Tell webgl the viewport setting needed for framebuffer.
  gl.viewport(0, 0, width, height);
}

function drawWithKernel(name) {
  // set the kernel
  gl.uniform1fv(kernelLocation, kernels[name]);

  // Draw the rectangle.
  gl.drawArrays(gl.TRIANGLES, 0, 6);
}

ฉันควรจะพูดเพิ่มอะไรบ้าง

การเรียกใช้ gl.bindFramebuffer ด้วย null จะบอก WebGL ว่าคุณต้องการแสดงผลใน Canvas แทนการบันทึกไว้ในเฟรมบัฟเฟอร์อันใดอันหนึ่งของคุณ WebGL ต้องแปลงจาก Clipspace กลับไปเป็นพิกเซล การดำเนินการนี้จะอิงจากการตั้งค่าของ gl.viewport การตั้งค่าของ gl.viewport จะมีค่าเริ่มต้นเป็นขนาดของผืนผ้าใบเมื่อเราเริ่มใช้ WebGL เนื่องจากเฟรมบัฟเฟอร์ที่เราแสดงภาพเป็นขนาดอื่น ผืนผ้าใบจึงต้องตั้งค่าวิวพอร์ตอย่างเหมาะสม สุดท้ายในตัวอย่างพื้นฐานของ WebGL เรากลับพิกัด Y ขณะแสดงผล เนื่องจาก WebGL แสดงภาพผืนผ้าใบโดยที่ 0.0 คือมุมซ้ายล่างของเนื้อผ้าแบบดั้งเดิมแทนที่จะเป็น 2 มิติด้านบนซ้าย ซึ่งเป็นสิ่งที่ไม่จำเป็นเมื่อแสดงผลไปยัง Framebuffer เนื่องจากเฟรมบัฟเฟอร์จะไม่แสดง ส่วนบนสุดและล่างสุดจะไม่เกี่ยวข้อง ที่สำคัญก็คือ พิกเซล 0.0 ในเฟรมบัฟเฟอร์ตรงกับ 0,0 ในการคำนวณของเรา ในการจัดการเรื่องนี้ ผมทำให้คุณสามารถตั้งค่าได้ว่า จะพลิกหรือไม่ โดยการเพิ่มอินพุตอีก 1 ตัวลงในตัวปรับแสงเงา

<script id="2d-vertex-shader" type="x-shader/x-vertex">
...
uniform float u_flipY;
...

void main() {
   ...
   gl_Position = vec4(clipSpace * vec2(1, u_flipY), 0, 1);
   ...
}
</script>

แล้วเราก็จะตั้งได้เมื่อเราแสดงผลด้วย

...
var flipYLocation = gl.getUniformLocation(program, "u_flipY");
...
// don't flip
gl.uniform1f(flipYLocation, 1);
...
// flip
gl.uniform1f(flipYLocation, -1);

ผมทำตามตัวอย่างนี้ได้ง่ายๆ โดยใช้โปรแกรม GLSL เดียว ซึ่งสามารถทำให้เกิดผลลัพธ์ได้หลายอย่าง หากต้องการใช้การประมวลผลรูปภาพอย่างเต็มรูปแบบ คุณอาจต้องใช้โปรแกรม GLSL หลายโปรแกรม โปรแกรมสำหรับการปรับโทนสี ความอิ่มตัว และความสว่าง อีกตัวเลือกหนึ่งสำหรับความสว่างและคอนทราสต์ หนึ่งสำหรับการสลับ อีกแบบสำหรับปรับระดับ เป็นต้น คุณจะต้องเปลี่ยนโค้ดเพื่อสลับโปรแกรม GLSL และอัปเดตพารามิเตอร์สำหรับโปรแกรมนั้นโดยเฉพาะ เราคิดว่าควรเขียนตัวอย่างนั้น แต่เป็นแบบฝึกหัดที่ดีที่สุดสำหรับผู้อ่าน เนื่องจากโปรแกรม GLSL หลายโปรแกรมต่างก็มีพารามิเตอร์ของตนเองอาจหมายถึงการปรับเปลี่ยนโครงสร้างที่สำคัญเพื่อทำให้ทุกโปรแกรมไม่กลายเป็นสปาเกตตีที่ยุ่งเหยิงมาก เราหวังว่านี่และตัวอย่างก่อนหน้านี้จะทำให้ WebGL สามารถเข้าถึงได้มากขึ้นเล็กน้อยและหวังว่าการเริ่มต้นด้วยแบบ 2 มิติจะช่วยให้ WebGL เข้าใจง่ายขึ้นเล็กน้อย หากพอมีเวลา ฉันจะลองเขียนบทความเพิ่มเติมสัก 2-3 บทความเกี่ยวกับวิธีสร้างภาพ 3 มิติและรายละเอียดเพิ่มเติมเกี่ยวกับการทำงานของ WebGL ในขั้นสูง

WebGL และอัลฟ่า

ผมสังเกตเห็นว่านักพัฒนาซอฟต์แวร์ OpenGL บางคนมีปัญหาเกี่ยวกับวิธีที่ WebGL จัดการอัลฟ่าใน Backbuffer (ได้แก่ Canvas) จึงคิดว่าน่าจะดีหากเราจะพูดถึงความแตกต่างบางอย่างระหว่าง WebGL และ OpenGL ที่เกี่ยวข้องกับอัลฟ่า

ความแตกต่างที่มากที่สุดระหว่าง OpenGL และ WebGL คือ OpenGL จะแสดงผลให้กับ Backbuffer ที่ไม่ได้ทำการ Composite ด้วยอะไรอย่างนั้น หรือไม่ได้ทำการ Composite กับสิ่งใดโดยตัวจัดการหน้าต่างของระบบปฏิบัติการอย่างมีประสิทธิภาพ ดังนั้นจึงไม่สำคัญว่าอัลฟ่าของคุณจะเป็นอะไร WebGL จะทำการ Composite โดยเบราว์เซอร์ที่มีหน้าเว็บ ค่าเริ่มต้นคือการใช้แท็กอัลฟ่าที่คูณไว้ล่วงหน้าเช่นเดียวกับแท็ก .png <img> ที่มีแท็กความโปร่งใสและแท็ก Canvas 2d WebGL มีหลายวิธีที่จะทำให้สิ่งนี้เหมือนกับ OpenGL มากขึ้น

#1) บอก WebGL ว่าคุณต้องการให้ทำการผสมกับอัลฟ่าที่ไม่ได้คำนวณล่วงหน้า

gl = canvas.getContext("experimental-webgl", {premultipliedAlpha: false});

ค่าเริ่มต้นคือ True แน่นอนว่าผลลัพธ์จะยังคงมีการนำมาประกอบในหน้าโดยมีสีพื้นหลังใดๆ ก็ตามอยู่ใต้ผืนผ้าใบ (สีพื้นหลังของผืนผ้าใบ สีพื้นหลังคอนเทนเนอร์ของ Canvas สีพื้นหลังของหน้าเว็บ สิ่งที่อยู่หลังผืนผ้าใบ หากผืนผ้าใบมีดัชนี z > 0 เป็นต้น...) กล่าวคือ สี CSS จะเป็นตัวกำหนดพื้นที่ของหน้าเว็บนั้น วิธีตรวจสอบที่ดีมากว่าคุณมีปัญหาเกี่ยวกับอัลฟ่าหรือไม่คือการตั้งค่าพื้นหลังของผืนผ้าใบให้เป็นสีสว่าง เช่น สีแดง คุณจะเห็นได้ทันทีว่าเกิดอะไรขึ้น

<canvas style="background: red;"></canvas>

คุณยังสามารถตั้งค่าเป็นสีดำ ซึ่งจะซ่อนปัญหาอัลฟ่าที่คุณพบ

#2) บอก WebGL ว่าคุณไม่ต้องการให้มีอัลฟ่าใน Backbuffer

gl = canvas.getContext("experimental-webgl", {alpha: false});

ซึ่งจะทำให้แอปทำงานเหมือนกับ OpenGL มากขึ้นเนื่องจาก Backbuffer จะมีเฉพาะ RGB เท่านั้น นี่อาจเป็นตัวเลือกที่ดีที่สุดเนื่องจากเบราว์เซอร์ที่ดีจะเห็นว่าคุณไม่มีเวอร์ชันอัลฟ่า และจริงๆ แล้วเพิ่มประสิทธิภาพวิธีการผสม WebGL และแน่นอนว่านั่นหมายความว่ามันจะไม่มีอัลฟ่าใน Backbuffer จริงๆ ดังนั้นหากคุณใช้อัลฟ่าใน Backbuffer เพื่อวัตถุประสงค์บางประการที่อาจไม่เหมาะกับคุณ มีบางแอปที่ฉันรู้ว่าใช้อัลฟ่าในบัฟเฟอร์ เราคิดว่าตัวเลือกนี้น่าจะเป็นค่าเริ่มต้น

3) ล้างอัลฟาในช่วงท้ายการแสดงภาพ

..
renderScene();
..
// Set the backbuffer's alpha to 1.0
gl.clearColor(1, 1, 1, 1);
gl.colorMask(false, false, false, true);
gl.clear(gl.COLOR_BUFFER_BIT);

โดยทั่วไปการล้างข้อมูลจะเร็วมากเนื่องจากมีกรณีพิเศษในฮาร์ดแวร์ส่วนใหญ่ ฉันทำเช่นนี้ในการสาธิตส่วนใหญ่ ถ้าฉันฉลาด ก็จะเปลี่ยนไปใช้วิธีที่ 2 ข้างต้น ไว้จะทำรูปหลังจากที่โพสต์คอนเทนต์นี้นะ ดูเหมือนว่าไลบรารี WebGL ส่วนใหญ่ควรจะใช้วิธีนี้เป็นค่าเริ่มต้น นักพัฒนาซอฟต์แวร์ส่วนน้อยที่ใช้อัลฟ่าสำหรับการจัดองค์ประกอบจะขอเอฟเฟกต์นี้ได้ ส่วนที่เหลือจะทำให้ได้ประสิทธิภาพที่ดีที่สุดและเซอร์ไพรส์น้อยที่สุด

#4) ล้างรุ่นอัลฟ่า 1 ครั้งและไม่ต้องแสดงผลอีกต่อไป

// At init time. Clear the back buffer.
gl.clearColor(1,1,1,1);
gl.clear(gl.COLOR_BUFFER_BIT);

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

แน่นอนว่าหากคุณกำลังแสดงภาพลงใน Framebuffer ของคุณเอง คุณอาจต้องเปิดการแสดงผลเป็นโหมดอัลฟ่าอีกครั้ง แล้วปิดโหมดนี้อีกครั้งเมื่อคุณเปลี่ยนไปใช้การแสดงภาพใน Canvas

5) การจัดการรูปภาพ

และถ้าคุณโหลดไฟล์ PNG ที่มีอัลฟาลงในพื้นผิว ค่าเริ่มต้นคือระบบจะคูณอัลฟ่าล่วงหน้า ซึ่งโดยทั่วไปไม่ใช่วิธีที่เกมส่วนใหญ่ทำ หากคุณต้องการป้องกันการทำงานดังกล่าว คุณต้องแจ้งให้ WebGL ทราบ

gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);

6) การใช้สมการการผสมที่ทำงานร่วมกับอัลฟ่าที่เติมล่วงหน้า

แอป OpenGL เกือบทั้งหมดที่ฉันเขียนหรือทำงานใช้งานอยู่

gl.blendFunc(gl.SRC_ALPHA, gl_ONE_MINUS_SRC_ALPHA);

ซึ่งใช้ได้กับพื้นผิวอัลฟ่าที่ยังไม่ได้คูณ ถ้าคุณต้องการทำงานกับพื้นผิวอัลฟ่าที่มีการคูณล่วงหน้า คุณอาจอยาก

gl.blendFunc(gl.ONE, gl_ONE_MINUS_SRC_ALPHA);

ฉันรู้จักวิธีเหล่านั้น หากคุณมีความรู้เพิ่มเติม โปรดโพสต์ที่ด้านล่าง