ข้อมูลพื้นฐานเกี่ยวกับ 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 เมื่อใช้เวิร์กเชดเวอร์เทกซ์จากส่วนท้ายของส่วนก่อนหน้า เราต้องเพิ่มแอตทริบิวต์เพื่อส่งพิกัดพื้นผิว แล้วส่งต่อไปยังฟร็กเมนท์เชด

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 นี่คือ 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 การกรองเพื่อประมวลผลรูปภาพทั่วไปกัน ในกรณีนี้ เราจะใช้ Kernel ขนาด 3x3 กรวยเชิงซ้อนเป็นเพียงเมทริกซ์ 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_ สำหรับค่าผันแปรซึ่งเป็นค่าที่ส่งจากโปรแกรมเปลี่ยนรูปแบบเวิร์กเท็กซ์ไปยังโปรแกรมเปลี่ยนรูปแบบเศษและได้รับการประมาณ (หรือผันแปร) ระหว่างเวิร์กเท็กซ์สำหรับพิกเซลแต่ละพิกเซลที่วาด

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

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

คุณอาจลองสร้างโปรแกรมเปลี่ยนสีขณะทำงาน ระบุ 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 ว่าคุณต้องการแสดงผลไปยัง Canvas แทนการแสดงผลไปยัง Framebuffer รายการใดรายการหนึ่ง WebGL ต้องแปลงจากพื้นที่คลิปกลับเป็นพิกเซล โดยอิงตามการตั้งค่าของ 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 หลายรายการ โปรแกรมสำหรับปรับสี ความอิ่มตัว และระดับความสว่าง อีกอันสำหรับความสว่างและคอนทราสต์ 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});

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

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

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

#2) บอก WebGL ว่าไม่ต้องการใช้อัลฟ่าในแบ็กบัฟเฟอร์

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

ซึ่งจะทำให้ทำงานได้เหมือน OpenGL มากขึ้น เนื่องจากแบ็กบัฟเฟอร์จะมีเพียง 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);

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