WebGL ช่วยให้แสดงกราฟิก 3 มิติแบบเรียลไทม์ที่น่าทึ่งในเบราว์เซอร์ได้ แต่สิ่งที่หลายคนไม่ทราบก็คือ WebGL เป็น API แบบ 2 มิติ ไม่ใช่ API แบบ 3 มิติ เราขออธิบายให้คุณฟัง
WebGL สนใจแค่ 2 สิ่งเท่านั้น พิกัด Clipspace ในรูปแบบ 2 มิติและสี งานของคุณในฐานะโปรแกรมเมอร์ที่ใช้ WebGL คือการจัดเตรียม 2 สิ่งนี้ให้กับ WebGL คุณต้องระบุ "เฉดสี" 2 รายการเพื่อดำเนินการนี้ Vertex Shader ซึ่งให้พิกัด Clipspace และ Fragment Shader ที่ให้สี พิกัด Clipspace จะมีค่าตั้งแต่ -1 ถึง +1 เสมอ ไม่ว่า Canvas จะมีขนาดเท่าใดก็ตาม ต่อไปนี้เป็นตัวอย่าง 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 เสมอ ไม่ว่าขนาดของ Canvas จะเป็นเท่าใดก็ตาม ในกรณีข้างต้น คุณจะเห็นว่าเราไม่ได้ทำอะไรเลยนอกจากส่งต่อข้อมูลตำแหน่งของเราโดยตรง เนื่องจากข้อมูลตำแหน่งอยู่ใน Clipspace อยู่แล้ว จึงไม่ต้องดำเนินการใดๆ หากต้องการ 3 มิติ คุณจะต้องจัดหาเฉดสีที่แปลงจาก 3 มิติเป็น 2 มิติ เนื่องจาก WebGL เป็น API แบบ 2 มิติ สำหรับสิ่งที่เป็น 2 มิติ คุณอาจต้องการทำงานในพิกเซลมากกว่า Clipspace ดังนั้นเรามาเปลี่ยน Shader เพื่อให้สามารถระบุสี่เหลี่ยมผืนผ้าเป็นพิกเซลและแปลงเป็น Clipspace ให้เราได้ นี่คือเวอร์เท็กซ์เชเดอร์ใหม่
<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>
ตอนนี้เราสามารถเปลี่ยนข้อมูลจาก Clipspace เป็นพิกเซลได้แล้ว
// 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);
มาทำให้โค้ดที่กำหนดสี่เหลี่ยมผืนผ้าเป็นฟังก์ชันกัน เพื่อให้เราเรียกใช้โค้ดนี้สำหรับสี่เหลี่ยมผืนผ้าขนาดต่างๆ ได้ ในขณะเดียวกัน เราจะทำให้ตั้งค่าสีได้ด้วย ก่อนอื่น เราจะทำให้ Fragment Shader รับอินพุต Uniform ของสี
<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 คาดหวังพิกัด Clipspace เมื่อแสดงผลแทนพิกเซล WebGL คาดหวังพิกัดพื้นผิวเมื่ออ่านพื้นผิว พิกัดพื้นผิวจะเริ่มตั้งแต่ 0.0 ถึง 1.0 ไม่ว่าพื้นผิวจะมีขนาดเท่าใดก็ตาม เนื่องจากเราวาดสี่เหลี่ยมผืนผ้าเพียงรูปเดียว (หรือสามเหลี่ยม 2 รูป) เราจึงต้องบอก WebGL ว่าจุดแต่ละจุดในสี่เหลี่ยมผืนผ้าสอดคล้องกับตำแหน่งใดในพื้นผิว เราจะส่งข้อมูลนี้จาก Vertex Shader ไปยัง Fragment Shader โดยใช้ตัวแปรชนิดพิเศษที่เรียกว่า "Varying" เราเรียกสิ่งนี้ว่าตัวแปรเพราะมันแปรผันได้ WebGL จะประมาณค่าที่เราให้ไว้ใน Vertex Shader ขณะที่วาดแต่ละพิกเซลโดยใช้ Fragment Shader เมื่อใช้ Vertex Shader จากตอนท้ายของส่วนก่อนหน้า เราจำเป็นต้องเพิ่มแอตทริบิวต์เพื่อส่งผ่านพิกัดพื้นผิว แล้วส่งต่อไปยัง Fragment 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;
}
จากนั้นเราจะจัดหา Fragment Shader เพื่อค้นหาสีจากพื้นผิว
<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 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);
...
ตอนนี้เราทราบวิธีอ้างอิงพิกเซลอื่นๆ แล้ว มาใช้เคอร์เนลคอนโวลูชันเพื่อประมวลผลรูปภาพทั่วไปกัน ในกรณีนี้ เราจะใช้เคอร์เนลขนาด 3x3 เคอร์เนล Convolution เป็นเพียงเมทริกซ์ขนาด 3x3 โดยที่แต่ละรายการในเมทริกซ์แสดงถึงจำนวนที่จะคูณพิกเซล 8 พิกเซลรอบพิกเซลที่เรากำลังแสดงผล จากนั้นเราจะหารผลลัพธ์ด้วยน้ำหนักของเคอร์เนล (ผลรวมของค่าทั้งหมดในเคอร์เนล) หรือ 1.0 แล้วแต่ว่าค่าใดมากกว่า บทความนี้มีข้อมูลที่ค่อนข้างดีเกี่ยวกับเรื่องนี้ และนี่คือบทความอีกบทความหนึ่งที่แสดงโค้ดจริงหากคุณเขียนโค้ดนี้ด้วยมือใน C++ ในกรณีของเรา เราจะทำงานนั้นในเฉดสี ดังนั้นนี่คือ Fragment Shader ใหม่
<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 เราต้องระบุเคอร์เนลคอนโวลูชัน
...
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_ สำหรับ Uniform ซึ่งเป็นอินพุตของเฉดสี v_ สำหรับ Varying ซึ่งเป็นค่าที่ส่งจาก Vertex Shader ไปยัง Fragment Shader และมีการประมาณค่าในช่วง (หรือแปรผัน) ระหว่างจุดยอดสำหรับแต่ละพิกเซลที่วาด
การใช้เอฟเฟกต์หลายรายการ
คำถามที่ชัดเจนที่สุดถัดไปสำหรับการประมวลผลรูปภาพคือจะใช้เอฟเฟกต์หลายรายการได้อย่างไร
คุณอาจลองสร้างเฉดสีแบบเรียลไทม์ก็ได้ จัดเตรียม 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
ซึ่งเราต้องสร้างเฟรมบัฟเฟอร์เพื่อดำเนินการนี้ ใน 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 ต้องแปลงจาก Clipspace กลับเป็นพิกเซล โดยจะดำเนินการตามการตั้งค่าของ gl.viewport การตั้งค่าของ gl.viewport จะมีค่าเริ่มต้นเป็นขนาดของ Canvas เมื่อเราเริ่มต้น WebGL เนื่องจากเฟรมบัฟเฟอร์ที่เรากำลังแสดงผลมีขนาดแตกต่างจาก Canvas เราจึงต้องตั้งค่าวิวพอร์ตให้เหมาะสม
สุดท้าย ในตัวอย่างพื้นฐานของ WebGL เราได้พลิกพิกัด Y เมื่อแสดงผลเนื่องจาก WebGL แสดง Canvas โดยมี 0,0 เป็นมุมซ้ายล่างแทนที่จะเป็นมุมซ้ายบนแบบ 2D แบบดั้งเดิม ซึ่งไม่จำเป็นเมื่อแสดงผลไปยังเฟรมบัฟเฟอร์ เนื่องจากระบบจะไม่แสดงเฟรมบัฟเฟอร์ จึงไม่สำคัญว่าส่วนใดอยู่ด้านบนหรือด้านล่าง สิ่งสำคัญคือพิกเซล 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 ดูเข้าถึงได้ง่ายขึ้น และหวังว่าการเริ่มต้นด้วย 2D จะช่วยให้เข้าใจ WebGL ได้ง่ายขึ้น หากมีเวลา ฉันจะพยายามเขียนบทความเพิ่มเติมอีก 2-3 บทความเกี่ยวกับวิธีสร้าง 3 มิติ รวมถึงรายละเอียดเพิ่มเติมเกี่ยวกับสิ่งที่ WebGL ทำจริงๆ เบื้องหลัง
WebGL และอัลฟ่า
ฉันสังเกตเห็นว่านักพัฒนา OpenGL บางรายมีปัญหาเกี่ยวกับวิธีที่ WebGL จัดการกับอัลฟ่าในบัฟเฟอร์ด้านหลัง (เช่น Canvas) จึงคิดว่าน่าจะเป็นการดีที่จะพูดถึงความแตกต่างบางประการระหว่าง 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 ว่าคุณไม่ต้องการอัลฟ่าใน Backbuffer
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);
แน่นอนว่าหากคุณกำลังแสดงผลไปยังเฟรมบัฟเฟอร์ของคุณเอง คุณอาจต้องเปิดการแสดงผลเป็นอัลฟากลับมาอีกครั้ง แล้วปิดอีกครั้งเมื่อเปลี่ยนไปแสดงผลไปยัง 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);
ทั้งหมดนี้คือวิธีที่ฉันทราบ หากคุณทราบข้อมูลเพิ่มเติม โปรดโพสต์ไว้ด้านล่าง