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

พื้นฐานเกี่ยวกับ WebGL

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

WebGL สนใจเพียง 2 สิ่งเท่านั้น พิกัดคลิปเพลตใน 2 มิติและสี งานของคุณในฐานะโปรแกรมเมอร์ที่ใช้ WebGL คือต้องจัดเตรียม 2 สิ่งดังกล่าวให้กับ WebGL คุณระบุ "Shader" 2 รายการเพื่อดำเนินการนี้ เวิร์กเท็กเจอร์ที่ให้พิกัดคลิปสเปซและฟร็กเมนเท็กเจอร์ที่ให้สี พิกัดพื้นที่คลิปจะอยู่ระหว่าง -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 เสมอ ไม่ว่าผ้าใบจะมีขนาดเท่าใดก็ตาม ในตัวอย่างนี้ คุณจะเห็นว่าเราไม่ได้ดำเนินการใดๆ เลยนอกจากส่งข้อมูลตำแหน่งของเราโดยตรง เนื่องจากข้อมูลตำแหน่งอยู่ในคลิปเพลตอยู่แล้ว คุณจึงไม่ต้องทำอะไร หากต้องการ 3 มิติ คุณจะต้องจัดหาชิเดอร์ที่แปลงจาก 3 มิติเป็น 2 มิติ เนื่องจาก WebGL เป็น API แบบ 2 มิติ สำหรับโมเดล 2 มิติ คุณอาจต้องการทำงานด้วยพิกเซลมากกว่าพื้นที่คลิป ดังนั้นเรามาเปลี่ยนโปรแกรมเปลี่ยนสีเพื่อให้เราระบุสี่เหลี่ยมผืนผ้าเป็นพิกเซลและแปลงเป็นพื้นที่คลิปให้เรา นี่คือเวิร์กเทกซ์ Shader ใหม่

<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-fragment" หมายถึงอะไร

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

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

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

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

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

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

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;
}

จากนั้นเราก็จัดให้มีกรอบสีแบบ Fragment เพื่อค้นหาสีจากพื้นผิว

<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 โดยใช้คณิตศาสตร์ง่ายๆ นี่คือ Shader ระดับเศษส่วนที่หาค่าเฉลี่ยพิกเซลซ้ายและขวาของพิกเซลแต่ละพิกเซลในพื้นผิว

<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);
...

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

<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 เราต้องระบุ Kernel การกรอง

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

เราหวังว่าข้อมูลนี้จะแสดงให้เห็นว่าการประมวลผลรูปภาพใน WebGL นั้นค่อนข้างง่าย ต่อไปเราจะพูดถึงวิธีใช้เอฟเฟกต์กับรูปภาพมากกว่า 1 รายการ

คำนำหน้า a, u และ v_ ที่อยู่หน้าตัวแปรใน GLSL มีไว้เพื่ออะไร

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

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

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

คุณอาจลองสร้างชิเดอร์ขณะที่เล่นได้ ระบุ 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 เป็นชื่อที่ไม่ค่อยดี 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 ว่าคุณต้องการแสดงผลบนผืนผ้าใบแทนที่จะเป็นเฟรมบัฟเฟอร์ใดภาพหนึ่งของคุณ WebGL ต้องแปลงจากพื้นที่คลิปกลับเป็นพิกเซล โดยอิงตามการตั้งค่าของ gl.viewport การตั้งค่า gl.viewport จะเริ่มต้นด้วยขนาดของผืนผ้าใบเมื่อเราเริ่มต้น WebGL เนื่องจากเฟรมบัฟเฟอร์ที่เราแสดงผลมีขนาดใหญ่กว่าผืนผ้าใบ เราจึงต้องตั้งค่าวิวพอร์ตให้เหมาะสม สุดท้ายในตัวอย่างพื้นฐานของ WebGL เราได้พลิกพิกัด Y เมื่อแสดงผล เนื่องจาก WebGL แสดงผืนผ้าใบโดยที่ 0,0 เป็นมุมล่างซ้ายแทนที่จะเป็นมุมซ้ายบนแบบดั้งเดิมสำหรับ 2 มิติ ซึ่งไม่จำเป็นเมื่อแสดงผลไปยังเฟรมบัฟเฟอร์ เนื่องจากระบบจะไม่แสดงเฟรมบัฟเฟอร์ จึงไม่สำคัญว่าส่วนใดจะอยู่ด้านบนหรือด้านล่าง สิ่งสำคัญก็คือพิกเซล 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 หลายรายการ โปรแกรมสำหรับปรับสี ระดับสี และความสว่าง อีกปุ่มหนึ่งสำหรับความสว่างและคอนทราสต์ 1 รายการสําหรับการกลับด้าน อีกรายการสําหรับการปรับระดับ เป็นต้น คุณจะต้องเปลี่ยนโค้ดเพื่อสลับโปรแกรม GLSL และอัปเดตพารามิเตอร์สําหรับโปรแกรมนั้นๆ เราเคยพิจารณาที่จะเขียนตัวอย่างนั้น แต่คิดว่าควรปล่อยให้ผู้อ่านลองทำเอง เนื่องจากโปรแกรม GLSL หลายรายการที่แต่ละรายการมีพารามิเตอร์ของตัวเองอาจต้องมีการรีแฟกทอริงครั้งใหญ่เพื่อไม่ให้ทุกอย่างกลายเป็นระเบียบวุ่นวาย เราหวังว่าตัวอย่างนี้และตัวอย่างก่อนหน้าจะช่วยให้ WebGL ดูเข้าถึงได้ง่ายขึ้น และหวังว่าการเริ่มต้นด้วย 2 มิติจะช่วยให้เข้าใจ WebGL ได้ง่ายขึ้น หากมีเวลา เราจะพยายามเขียนบทความเพิ่มเติมอีก 2-3 บทความเกี่ยวกับวิธีทำโมเดล 3 มิติ รวมถึงรายละเอียดเพิ่มเติมเกี่ยวกับสิ่งที่ WebGL ทําอยู่เบื้องหลัง

WebGL และอัลฟ่า

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

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

#1) บอก WebGL ว่าคุณต้องการใช้การคอมโพสิชันด้วยค่าอัลฟ่าแบบไม่คูณล่วงหน้า

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

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

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

หรือจะตั้งค่าเป็นสีดําเพื่อซ่อนปัญหาอัลฟ่าที่คุณมีก็ได้

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

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

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

#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);

แน่นอนว่าหากคุณแสดงผลไปยังเฟรมบัฟเฟอร์ของคุณเอง คุณอาจต้องเปิดการแสดงผลเป็นอัลฟ่าอีกครั้ง แล้วปิดอีกครั้งเมื่อเปลี่ยนไปแสดงผลไปยังผืนผ้าใบ

#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);

นั่นคือวิธีการที่เราทราบ หากมีข้อมูลเพิ่มเติม โปรดโพสต์ไว้ด้านล่าง