โลกของกราฟิก 3 มิติในตอนแรกน่ากลัวมาก ไม่ว่าคุณจะต้องการสร้างโลโก้ 3 มิติแบบโต้ตอบหรือออกแบบเกมที่สมบูรณ์หากคุณไม่รู้หลักการของการเรนเดอร์ 3 มิติคุณอาจจมปลักอยู่ในห้องสมุดที่รวบรวมสิ่งต่างๆมากมาย
การใช้ห้องสมุดอาจเป็นเครื่องมือที่เหมาะสมและ JavaScript มีโอเพ่นซอร์สที่น่าทึ่งในรูปแบบของ three.js . มีข้อเสียบางประการในการใช้โซลูชันสำเร็จรูปแม้ว่า:
แม้ว่าคุณจะตัดสินใจใช้ไลบรารีกราฟิกระดับสูง แต่การมีความเข้าใจพื้นฐานเกี่ยวกับสิ่งที่ซ่อนอยู่จะช่วยให้คุณใช้งานได้อย่างมีประสิทธิภาพมากขึ้น ไลบรารียังสามารถมีฟังก์ชันขั้นสูงเช่น ShaderMaterial
ใน three.js
. การรู้หลักการแสดงกราฟิกช่วยให้คุณสามารถใช้คุณสมบัติดังกล่าวได้
กฎเกสตัลต์ขององค์กรการรับรู้
เป้าหมายของเราคือการแนะนำสั้น ๆ เกี่ยวกับแนวคิดหลักทั้งหมดที่อยู่เบื้องหลังการแสดงผลกราฟิก 3 มิติและการใช้ WebGL สำหรับแอปพลิเคชันของคุณ คุณจะเห็นว่าสิ่งที่ทำบ่อยที่สุดคือการแสดงและย้ายวัตถุ 3 มิติในพื้นที่ว่าง
รหัสสุดท้าย มีให้คุณทดลองเล่นด้วย
สิ่งแรกที่คุณต้องเข้าใจคือวิธีการแสดงผลโมเดล 3 มิติ แบบจำลองทำจากตาข่ายสามเหลี่ยม สามเหลี่ยมแต่ละอันจะแสดงด้วยจุดยอดสามจุดสำหรับแต่ละมุมของสามเหลี่ยม มีคุณสมบัติสามประการที่แนบมากับจุดยอดที่พบมากที่สุด
ตำแหน่งเป็นคุณสมบัติที่เข้าใจง่ายที่สุดของจุดยอด เป็นตำแหน่งในอวกาศ 3 มิติซึ่งแสดงด้วยเวกเตอร์พิกัด 3 มิติ หากคุณทราบพิกัดที่แน่นอนของจุดสามจุดในอวกาศคุณจะมีข้อมูลทั้งหมดที่จำเป็นในการวาดสามเหลี่ยมง่ายๆระหว่างจุดเหล่านั้น เพื่อให้แบบจำลองดูดีจริงๆเมื่อแสดงผลมีอีกสองสามอย่างที่ต้องเตรียมให้กับตัวแสดงผล
พิจารณาสองรุ่นก่อนหน้านี้ ประกอบด้วยตำแหน่งจุดยอดเดียวกันแม้ว่าจะดูแตกต่างกันโดยสิ้นเชิงเมื่อเป็นตัวแทน เป็นไปได้อย่างไร?
นอกจากการบอก เรนเดอร์ เราต้องการให้จุดยอดอยู่ตรงไหนเรายังสามารถให้คำใบ้ได้ด้วยว่าพื้นผิวเอียงไปยังตำแหน่งที่แน่นอนนั้นอย่างไร เบาะแสอยู่ในรูปของเวกเตอร์พื้นผิวปกติ ณ จุดนั้น ๆ ในแบบจำลองซึ่งแสดงด้วยเวกเตอร์ 3 มิติ ภาพต่อไปนี้ควรให้คำอธิบายที่ชัดเจนยิ่งขึ้นเกี่ยวกับวิธีการจัดการ
พื้นผิวด้านซ้ายและด้านขวาสอดคล้องกับลูกบอลด้านซ้ายและด้านขวาในภาพก่อนหน้าตามลำดับ ลูกศรสีแดงแสดงถึงเวกเตอร์ปกติที่ระบุไว้สำหรับจุดยอดในขณะที่ลูกศรสีน้ำเงินแสดงถึงการคำนวณของโปรเซสเซอร์ว่าเวกเตอร์ปกติควรหาจุดทั้งหมดระหว่างจุดยอดอย่างไร ภาพแสดงการสาธิตสำหรับพื้นที่ 2 มิติ แต่หลักการเดียวกันนี้ใช้กับ 3D
เวกเตอร์ปกติเป็นตัวบ่งชี้ว่าไฟจะส่องพื้นผิวอย่างไร ยิ่งทิศทางของรังสีแสงใกล้กับเวกเตอร์ปกติมากเท่าไหร่จุดก็ยิ่งสว่างมากขึ้นเท่านั้น การเปลี่ยนแปลงอย่างค่อยเป็นค่อยไปในทิศทางของเวกเตอร์ปกติทำให้เกิดการไล่ระดับสีเล็กน้อยในขณะที่การเปลี่ยนแปลงอย่างกะทันหันโดยไม่มีการเปลี่ยนแปลงระหว่างพื้นผิวส่งผลให้พื้นผิวมีแสงสว่างคงที่ตลอดจนการส่องสว่างที่เปลี่ยนไปอย่างกะทันหัน
คุณสมบัติสุดท้ายที่สำคัญคือพิกัดพื้นผิวซึ่งโดยทั่วไปเรียกว่าการทำแผนที่ UV คุณมีรูปแบบและพื้นผิวที่คุณต้องการใช้กับมัน พื้นผิวมีหลายพื้นที่และสิ่งเหล่านี้แสดงถึงภาพที่เราต้องการนำไปใช้กับส่วนต่างๆของโมเดล ต้องมีวิธีในการทำเครื่องหมายว่าสามเหลี่ยมใดควรแสดงโดยส่วนใดของพื้นผิว นั่นคือที่มาของการทำแผนที่พื้นผิว
สำหรับจุดยอดแต่ละจุดเราจะพล็อตพิกัดสองจุดคือ U และ V พิกัดเหล่านี้แสดงตำแหน่งบนพื้นผิวโดย U แทนแกนนอนและ V เป็นแกนแนวตั้ง ค่าไม่ได้เป็นพิกเซล แต่อยู่ในตำแหน่งเปอร์เซ็นต์ภายในรูปภาพ มุมล่างซ้ายของภาพแสดงด้วยเลขศูนย์สองตัวในขณะที่มุมขวาบนแสดงด้วยสองตัว
รูปสามเหลี่ยมถูกวาดโดยใช้พิกัด UV ของจุดยอดแต่ละจุดในรูปสามเหลี่ยมและใช้ภาพที่จับระหว่างพิกัดเหล่านั้นบนพื้นผิว
คุณสามารถดูการสาธิตการทำแผนที่ UV ในภาพด้านบน แบบจำลองทรงกลมถูกถ่ายและตัดเป็นชิ้นส่วนที่เล็กพอที่จะแบนบนพื้นผิว 2 มิติ ตะเข็บที่ตัดถูกทำเครื่องหมายด้วยเส้นที่หนาขึ้น หนึ่งในแพตช์ได้รับการไฮไลต์เพื่อให้คุณสามารถเห็นได้อย่างชัดเจนว่าสิ่งต่างๆเข้ากันอย่างไร นอกจากนี้คุณยังสามารถดูว่ารอยต่อตรงกลางรอยยิ้มวางส่วนต่างๆของปากไว้ในรอยยิ้มสองจุดได้อย่างไร
โครงร่างโครงร่างไม่ได้เป็นส่วนหนึ่งของพื้นผิว แต่ถูกวางซ้อนไว้ที่ด้านบนของภาพเพื่อให้คุณเห็นว่าสิ่งต่าง ๆ มีความสัมพันธ์กันอย่างไร
เชื่อหรือไม่ว่านี่คือสิ่งที่คุณต้องรู้เพื่อสร้างโมเดลโหลดเดอร์ง่ายๆของคุณเอง รูปแบบไฟล์ OBJ ง่ายพอที่จะใช้ตัวแยกวิเคราะห์ในโค้ดไม่กี่บรรทัด
ไฟล์แสดงตำแหน่งจุดยอดในรูปแบบ v
โดยมีหนึ่งในสี่ ลอย เป็นทางเลือกซึ่งเราจะเพิกเฉยเพื่อให้สิ่งต่างๆเรียบง่าย จุดยอดจะแสดงในทำนองเดียวกันกับ vn
ในที่สุดพิกัดพื้นผิวจะแสดงด้วย vt
ด้วยหนึ่งในสาม ลอย เป็นทางเลือกที่เราจะเพิกเฉยต่อไป ในทั้งสามกรณี ลอย แสดงพิกัดตามลำดับ คุณสมบัติทั้งสามนี้สะสมในสามเมทริกซ์
ใบหน้าแสดงด้วยกลุ่มของจุดยอด แต่ละจุดยอดจะแสดงด้วยดัชนีของคุณสมบัติแต่ละอย่างดังนั้นดัชนีจึงเริ่มต้นที่ 1 มีหลายวิธีในการแทนค่านี้ แต่เราจะยึดติดกับ 'v1 / vt1 / vn1 v2 / vt2 / vn2 v3 / vt3 / vn3` กำหนดให้มีคุณสมบัติทั้งสามและ จำกัด จำนวนจุดยอดสำหรับแต่ละใบหน้าไว้ที่สาม ข้อ จำกัด ทั้งหมดเหล่านี้ถูกสร้างขึ้นเพื่อให้ตัวโหลดง่ายที่สุดเท่าที่จะเป็นไปได้เนื่องจากตัวเลือกอื่น ๆ ทั้งหมดต้องการการประมวลผลเล็กน้อยก่อนที่จะสามารถอยู่ในรูปแบบที่ WebGL ชอบได้
เราได้วางข้อกำหนดไว้มากมายสำหรับโปรแกรมโหลดไฟล์ของเรา นั่นอาจฟังดู จำกัด แต่แอปพลิเคชันการสร้างแบบจำลอง 3 มิติมักให้ความสามารถในการกำหนดข้อ จำกัด เหล่านั้นเมื่อส่งออกโมเดลเป็นไฟล์ OBJ
โค้ดต่อไปนี้จะแยกวิเคราะห์สตริงที่เป็นตัวแทนของไฟล์ OBJ และสร้างโมเดลในรูปแบบของใบหน้า
function Geometry (faces) this.faces = faces // Parses an OBJ file, passed as a string Geometry.parseOBJ = function (src) { var POSITION = /^vs+([d.+-eE]+)s+([d.+-eE]+)s+([d.+-eE]+)/ var NORMAL = /^vns+([d.+-eE]+)s+([d.+-eE]+)s+([d.+-eE]+)/ var UV = /^vts+([d.+-eE]+)s+([d.+-eE]+)/ var FACE = /^fs+(-?d+)/(-?d+)/(-?d+)s+(-?d+)/(-?d+)/(-?d+)s+(-?d+)/(-?d+)/(-?d+)(?:s+(-?d+)/(-?d+)/(-?d+))?/ lines = src.split('
') var positions = [] var uvs = [] var normals = [] var faces = [] lines.forEach(function (line) { // Match each line of the file against various RegEx-es var result if ((result = POSITION.exec(line)) != null) { // Add new vertex position positions.push(new Vector3(parseFloat(result[1]), parseFloat(result[2]), parseFloat(result[3]))) } else if ((result = NORMAL.exec(line)) != null) { // Add new vertex normal normals.push(new Vector3(parseFloat(result[1]), parseFloat(result[2]), parseFloat(result[3]))) } else if ((result = UV.exec(line)) != null) { // Add new texture mapping point uvs.push(new Vector2(parseFloat(result[1]), 1 - parseFloat(result[2]))) } else if ((result = FACE.exec(line)) != null) { // Add new face var vertices = [] // Create three vertices from the passed one-indexed indices for (var i = 1; i <10; i += 3) { var part = result.slice(i, i + 3) var position = positions[parseInt(part[0]) - 1] var uv = uvs[parseInt(part[1]) - 1] var normal = normals[parseInt(part[2]) - 1] vertices.push(new Vertex(position, normal, uv)) } faces.push(new Face(vertices)) } }) return new Geometry(faces) } // Loads an OBJ file from the given URL, and returns it as a promise Geometry.loadOBJ = function (url) { return new Promise(function (resolve) { var xhr = new XMLHttpRequest() xhr.onreadystatechange = function () { if (xhr.readyState == XMLHttpRequest.DONE) { resolve(Geometry.parseOBJ(xhr.responseText)) } } xhr.open('GET', url, true) xhr.send(null) }) } function Face (vertices) [] function Vertex (position, normal, uv) function Vector3 (x, y, z) 0 this.y = Number(y) function Vector2 (x, y)
โครงสร้าง Geometry
มีข้อมูลที่แน่นอนที่จำเป็นในการส่งโมเดลไปยังการ์ดแสดงผลเพื่อประมวลผล ก่อนที่จะทำเช่นนั้นคุณอาจต้องการมีความสามารถในการเคลื่อนย้ายโมเดลบนหน้าจอ
จุดทั้งหมดในโมเดลที่เราโหลดนั้นสัมพันธ์กับระบบพิกัดของมัน หากเราต้องการแปลหมุนและปรับขนาดโมเดลสิ่งที่เราต้องทำคือดำเนินการนี้บนระบบพิกัด ระบบพิกัด A สัมพันธ์กับระบบพิกัด B ถูกกำหนดโดยตำแหน่งของศูนย์กลางเป็นเวกเตอร์ p_ab
และเวกเตอร์สำหรับแต่ละแกน x_ab
, y_ab
และ z_ab
แทนทิศทางของแกนนั้น ดังนั้นหากจุดถูกย้ายไป 10 บน x
จากระบบพิกัด A จากนั้น - ในระบบพิกัด B - มันจะเคลื่อนที่ไปในทิศทางของ x_ab
คูณด้วย 10
ข้อมูลทั้งหมดนี้ถูกจัดเก็บในรูปแบบเมทริกซ์ต่อไปนี้:
x_ab.x y_ab.x z_ab.x p_ab.x x_ab.y y_ab.y z_ab.y p_ab.y x_ab.z y_ab.z z_ab.z p_ab.z 0 0 0 1
หากเราต้องการแปลงเวกเตอร์ 3 มิติ q
เราจะต้องคูณเมทริกซ์การแปลงด้วยเวกเตอร์เท่านั้น:
q.x q.y q.z 1
สิ่งนี้ทำให้จุดเคลื่อนที่โดย q.x
ตามแกนใหม่ x
, โดย q.y
ตามแกนใหม่ y
และโดย q.z
ตามแกนใหม่ z
. ในที่สุดมันก็ทำให้จุดเคลื่อนที่ไปตามเวกเตอร์ p
ซึ่งเป็นสาเหตุที่เราใช้จุดหนึ่งเป็นองค์ประกอบสุดท้ายของการคูณ
ข้อได้เปรียบอย่างมากของการใช้เมทริกซ์เหล่านี้คือความจริงที่ว่าถ้าเรามีการแปลงหลายรูปแบบเพื่อดำเนินการกับจุดยอดเราสามารถรวมมันเข้าเป็นการแปลงเดียวคูณเมทริกซ์ของพวกเขาก่อนที่จะแปลงจุดยอดนั้นเอง
มีการเปลี่ยนแปลงหลายอย่างที่สามารถทำได้และเราจะมาดูการเปลี่ยนแปลงหลัก ๆ
หากไม่มีการเปลี่ยนแปลงเกิดขึ้นเวกเตอร์ p
เป็นเวกเตอร์ศูนย์เวกเตอร์ x
คือ [1, 0, 0]
, y
คือ [0, 1, 0]
และ z
คือ [0, 0, 1]
. จากนี้ไปเราจะอ้างถึงค่าเหล่านี้เป็นค่าเริ่มต้นสำหรับเวกเตอร์เหล่านี้ การประยุกต์ใช้ค่าเหล่านี้ทำให้เรามีเมทริกซ์เอกลักษณ์:
1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1
นี่เป็นจุดเริ่มต้นที่ดีสำหรับการเปลี่ยนโซ่
ในขณะที่ทำการแปลเวกเตอร์ทั้งหมดยกเว้นเวกเตอร์ p
จะมีค่าเริ่มต้น ผลลัพธ์ในเมทริกซ์ต่อไปนี้:
1 0 0 p.x 0 1 0 p.y 0 0 1 p.z 0 0 0 1
การติดตั้งโมเดลหมายถึงการลดจำนวนที่แต่ละพิกัดก่อให้เกิดตำแหน่งของจุด ไม่มีการกระจัดสม่ำเสมอที่เกิดจากความพอดีดังนั้นเวกเตอร์ p
คงค่าเริ่มต้นไว้ เวกเตอร์แกนเริ่มต้นต้องคูณด้วยปัจจัยมาตราส่วนตามลำดับซึ่งส่งผลให้เมทริกซ์ต่อไปนี้:
s_x 0 0 0 0 s_y 0 0 0 0 s_z 0 0 0 0 1
ที่นี่ s_x
, s_y
และ s_z
แสดงถึงการปรับใช้กับแต่ละแกน
ภาพด้านบนแสดงสิ่งที่เกิดขึ้นเมื่อเราหมุนกรอบพิกัดรอบแกน Z
การหมุนไม่ส่งผลให้เกิดการกระจัดสม่ำเสมอดังนั้นเวกเตอร์ p
คงค่าเริ่มต้นไว้ ตอนนี้สิ่งต่าง ๆ ซับซ้อนขึ้นเล็กน้อย การหมุนทำให้การเคลื่อนที่ตามแกนที่แน่นอนในระบบพิกัดเดิมเคลื่อนที่ไปในทิศทางอื่น ดังนั้นถ้าเราหมุนระบบพิกัด 45 องศารอบแกน Z โดยเคลื่อนที่ไปตามแกน x ของระบบพิกัดเดิมการเคลื่อนที่จะเกิดขึ้นในแนวทแยงระหว่างแกน x และแกน y ในระบบพิกัดใหม่
เพื่อให้สิ่งต่างๆง่ายขึ้นเราจะแสดงให้คุณเห็นว่าเมทริกซ์การแปลงมีลักษณะอย่างไรสำหรับการหมุนรอบแกนหลัก
Around X: 1 0 0 0 0 cos(phi) sin(phi) 0 0 -sin(phi) cos(phi) 0 0 0 0 1 Around Y: cos(phi) 0 sin(phi) 0 0 1 0 0 -sin(phi) 0 cos(phi) 0 0 0 0 1 Around Z: cos(phi) -sin(phi) 0 0 sin(phi) cos(phi) 0 0 0 0 1 0 0 0 0 1
ทั้งหมดนี้สามารถใช้เป็นคลาสที่เก็บตัวเลข 16 ตัวจัดเก็บอาร์เรย์ในไฟล์ ลำดับคอลัมน์หลัก .
function Transformation () { // Create an identity transformation this.fields = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] } // Multiply matrices, to chain transformations Transformation.prototype.mult = function (t) { var output = new Transformation() for (var row = 0; row <4; ++row) { for (var col = 0; col < 4; ++col) { var sum = 0 for (var k = 0; k < 4; ++k) { sum += this.fields[k * 4 + row] * t.fields[col * 4 + k] } output.fields[col * 4 + row] = sum } } return output } // Multiply by translation matrix Transformation.prototype.translate = function (x, y, z) // Multiply by scaling matrix Transformation.prototype.scale = function (x, y, z) // Multiply by rotation matrix around X axis Transformation.prototype.rotateX = function (angle) // Multiply by rotation matrix around Y axis Transformation.prototype.rotateY = function (angle) // Multiply by rotation matrix around Z axis Transformation.prototype.rotateZ = function (angle)
ส่วนสำคัญของการนำเสนอวัตถุบนหน้าจอมีดังนี้: กล้องถ่ายรูป ส่วนประกอบหลักของกล้องมีสองส่วน ตำแหน่งของมันและวิธีที่มันฉายวัตถุที่สังเกตเห็นบนหน้าจอ
ตำแหน่งของกล้องถูกจัดการด้วยเคล็ดลับง่ายๆ ไม่มีความแตกต่างระหว่างการเคลื่อนกล้องไปข้างหน้าหนึ่งเมตรและเคลื่อนโลกทั้งใบไปข้างหลังหนึ่งเมตร ตามธรรมชาติแล้วเราทำอย่างหลังโดยใช้อินเวอร์สของเมทริกซ์เป็นการแปลง
องค์ประกอบหลักที่สองคือวิธีฉายวัตถุที่สังเกตเห็นลงบนเลนส์ ใน WebGL ทุกสิ่งที่มองเห็นบนหน้าจอจะอยู่ในกล่อง ช่องมีช่วงระหว่าง -1 ถึง 1 ในแต่ละแกน ทุกสิ่งที่มองเห็นอยู่ในกล่องนั้น เราสามารถใช้วิธีเมทริกซ์การแปลงเดียวกันเพื่อสร้างเมทริกซ์การฉาย
การฉายภาพที่ง่ายที่สุดคือ [orthographic projection] (https://www.scratchapixel.com/lessons/3d-basic-rendering/perspective-and-orthographical-projection-matrix/orthographic-projection-matrix) กล่องถูกนำมาในช่องว่างที่ระบุความกว้างความสูงและความลึกโดยสมมติว่าจุดศูนย์กลางอยู่ที่ตำแหน่งศูนย์ จากนั้นการฉายภาพจะปรับขนาดกล่องให้พอดีกับกล่องที่อธิบายไว้ก่อนหน้านี้ซึ่ง WebGL สังเกตวัตถุ เนื่องจากเราต้องการปรับขนาดแต่ละมิติเป็นสองเราจึงตั้งค่าแต่ละแกนเป็น 2/size
โดยที่ size
คือขนาดของแกนตามลำดับ ข้อแม้เล็ก ๆ อย่างหนึ่งคือการที่เราคูณแกน Z ด้วยค่าลบ สิ่งนี้ทำเพราะเราต้องการพลิกทิศทางของมิตินั้น เมทริกซ์สุดท้ายมีรูปแบบนี้:
2/width 0 0 0 0 2/height 0 0 0 0 -2/depth 0 0 0 0 1
เราจะไม่ลงรายละเอียดเกี่ยวกับวิธีการออกแบบการฉายภาพนี้ แต่ใช้ไฟล์ สูตรสุดท้าย ซึ่งเป็นมาตรฐานที่ดีสำหรับตอนนี้ เราสามารถทำให้มันง่ายขึ้นโดยการวางเส้นโครงไว้ที่ตำแหน่งศูนย์บนแกน x และ y ทำให้ขีด จำกัด ด้านขวา / ซ้ายและบน / ล่างเท่ากับ width/2
และ height/2
ตามลำดับ พารามิเตอร์ n
และ f
เป็นตัวแทนของเครื่องบินตัด near
และ far
ซึ่งเป็นระยะทางที่เล็กที่สุดและใหญ่ที่สุดที่กล้องจะจับจุดได้ พวกมันแสดงด้วยด้านคู่ขนานของ น่าผิดหวัง ในภาพด้านบน
การฉายภาพมุมมองมักจะแสดงด้วยไฟล์ มุมมอง (เราจะใช้แนวตั้ง) อัตราส่วนภาพ และระยะทางใกล้และไกลของเครื่องบิน ข้อมูลนั้นสามารถใช้ในการคำนวณ width
และ height
จากนั้นอาร์เรย์สามารถสร้างได้จากเทมเพลตต่อไปนี้:
2*n/width 0 0 0 0 2*n/height 0 0 0 0 (f+n)/(n-f) 2*f*n/(n-f) 0 0 -1 0
ในการคำนวณความกว้างและความสูงสามารถใช้สูตรต่อไปนี้:
height = 2 * near * Math.tan(fov * Math.PI / 360) width = aspectRatio * height
FOV (มุมมองภาพ) หมายถึงมุมแนวตั้งที่กล้องจับด้วยเลนส์ อัตราส่วนภาพแสดงถึงอัตราส่วนของความกว้างต่อความสูงของภาพและขึ้นอยู่กับขนาดของหน้าจอที่เรากำลังแสดง
ตอนนี้เราสามารถแสดงกล้องเป็นคลาสซึ่งเก็บตำแหน่งกล้องและเมทริกซ์การฉายภาพ เราต้องรู้วิธีคำนวณการแปลงผกผันด้วย การแก้การผกผันของเมทริกซ์ทั่วไปอาจเป็นปัญหาได้ แต่มี แนวทางที่เรียบง่าย สำหรับกรณีของเราโดยเฉพาะ
function Camera () { this.position = new Transformation() this.projection = new Transformation() } Camera.prototype.setOrthographic = function (width, height, depth) { this.projection = new Transformation() this.projection.fields[0] = 2 / width this.projection.fields[5] = 2 / height this.projection.fields[10] = -2 / depth } Camera.prototype.setPerspective = function (verticalFov, aspectRatio, near, far) { var height_div_2n = Math.tan(verticalFov * Math.PI / 360) var width_div_2n = aspectRatio * height_div_2n this.projection = new Transformation() this.projection.fields[0] = 1 / height_div_2n this.projection.fields[5] = 1 / width_div_2n this.projection.fields[10] = (far + near) / (near - far) this.projection.fields[10] = -1 this.projection.fields[14] = 2 * far * near / (near - far) this.projection.fields[15] = 0 } Camera.prototype.getInversePosition = function () { var orig = this.position.fields var dest = new Transformation() var x = orig[12] var y = orig[13] var z = orig[14] // Transpose the rotation matrix for (var i = 0; i <3; ++i) { for (var j = 0; j < 3; ++j) { dest.fields[i * 4 + j] = orig[i + j * 4] } } // Translation by -p will apply R^T, which is equal to R^-1 return dest.translate(-x, -y, -z) }
นี่เป็นชิ้นสุดท้ายที่เราต้องการก่อนที่จะเริ่มวาดภาพบนหน้าจอ
พื้นผิวที่เรียบง่ายที่สุดที่คุณวาดได้คือสามเหลี่ยม ในความเป็นจริงสิ่งที่คุณวาดในอวกาศ 3 มิติส่วนใหญ่ประกอบด้วยสามเหลี่ยมจำนวนมาก
สิ่งแรกที่ต้องทำความเข้าใจคือการแสดงผลหน้าจอใน WebGL อย่างไร มันคือช่องว่าง 3 มิติซึ่งครอบคลุมระหว่าง -1 ถึง 1 บนแกน x , ย ย ด้วย . ตามค่าเริ่มต้นแกนนี้ ด้วย ไม่ได้ใช้ แต่คุณสนใจกราฟิก 3 มิติดังนั้นคุณควรเปิดใช้งานทันที
ด้วยเหตุนี้สิ่งต่อไปนี้จึงจำเป็นต้องมีสามขั้นตอนในการวาดสามเหลี่ยมบนพื้นผิวนี้
หลักการเกสตัลต์ของความใกล้ชิด
คุณสามารถกำหนดจุดยอดสามจุดที่จะแทนสามเหลี่ยมที่คุณต้องการวาด ข้อมูลจะถูกทำให้เป็นอนุกรมและส่งไปยัง GPU (หน่วยประมวลผลกราฟิก) ด้วยโมเดลทั้งหมดที่มีคุณสามารถทำได้สำหรับสามเหลี่ยมทั้งหมดในโมเดล ตำแหน่งจุดยอดที่คุณให้อยู่ในพื้นที่พิกัดโลคัลของโมเดลที่คุณโหลด พูดง่ายๆก็คือตำแหน่งที่คุณระบุคือตำแหน่งที่แน่นอนจากไฟล์ไม่ใช่ตำแหน่งที่คุณได้รับหลังจากทำการแปลงเมทริกซ์
ตอนนี้คุณได้กำหนดจุดยอดให้กับ GPU แล้วคุณจะบอก GPU ว่าจะใช้ตรรกะอะไรเมื่อวางจุดยอดบนหน้าจอ ขั้นตอนนี้จะใช้เพื่อใช้การแปลงเมทริกซ์ของเรา GPU นั้นดีมากในการคูณเมทริกซ์ 4x4 จำนวนมากดังนั้นเราจะนำคุณสมบัตินั้นไปใช้งานได้ดี
ในขั้นตอนสุดท้าย GPU จะแรสเตอร์สามเหลี่ยมนั้น Rasterization เป็นกระบวนการในการถ่ายภาพกราฟิกแบบเวกเตอร์และกำหนดว่าจะต้องทาสีพิกเซลใดบนหน้าจอเพื่อให้วัตถุกราฟิกเวกเตอร์แสดง ในกรณีของเรา GPU กำลังพยายามกำหนดพิกเซลที่อยู่ภายในแต่ละสามเหลี่ยม สำหรับแต่ละพิกเซล GPU จะถามคุณว่าคุณต้องการทาสีเป็นสีอะไร
นี่คือสี่องค์ประกอบที่จำเป็นในการวาดสิ่งที่คุณต้องการและเป็นตัวอย่างที่ง่ายที่สุดของ a ช่องกราฟิก . สิ่งต่อไปนี้คือการดูแต่ละรายการและการนำไปใช้งานง่ายๆ
องค์ประกอบที่สำคัญที่สุดสำหรับแอปพลิเคชัน WebGL คือบริบท WebGL คุณสามารถเข้าถึงได้ด้วย gl = canvas.getContext ('webgl')
หรือใช้ ’experimental-webgl'
หรือในกรณีที่เบราว์เซอร์ที่ใช้ในปัจจุบันไม่รองรับฟังก์ชันทั้งหมดของ WebGL 'ผืนผ้าใบ' ที่เราอ้างถึงคือองค์ประกอบ DOM ของผืนผ้าใบที่เราต้องการวาด บริบทประกอบด้วยหลายสิ่งซึ่ง ได้แก่ ไฟล์ เฟรมบัฟเฟอร์ กำหนดไว้ล่วงหน้า
สามารถอธิบาย เฟรมบัฟเฟอร์ เช่นเดียวกับบัฟเฟอร์ (วัตถุ) ใด ๆ ที่คุณสามารถวาดได้ โดยค่าเริ่มต้นไฟล์ เฟรมบัฟเฟอร์ ค่าเริ่มต้นจะจัดเก็บสีของแต่ละพิกเซลบนผืนผ้าใบที่เชื่อมโยงบริบท WebGL ตามที่อธิบายไว้ในส่วนก่อนหน้าเมื่อเราวาดไฟล์ เฟรมบัฟเฟอร์ แต่ละพิกเซลจะอยู่ระหว่าง -1 ถึง 1 บนแกน x ย ย . สิ่งที่เรากล่าวถึงก็คือความจริงที่ว่าโดยค่าเริ่มต้น WebGL ไม่ได้ใช้แกน ด้วย . สามารถเปิดใช้งานฟังก์ชันดังกล่าวได้โดยเรียกใช้ gl.enable (gl.DEPTH_TEST)
เยี่ยมมาก แต่การทดสอบเชิงลึกคืออะไร?
การเปิดใช้งานการทดสอบความลึกจะทำให้พิกเซลสามารถเก็บทั้งสีและความลึกได้ ความลึกคือพิกัด ด้วย ของพิกเซลนั้น หลังจากวาดพิกเซลที่ระดับความลึกที่กำหนด ด้วย ในการอัปเดตสีของพิกเซลนั้นคุณต้องวาดในตำแหน่ง ด้วย ใกล้กล้องมากขึ้น มิฉะนั้นความพยายามในการวาดจะถูกละเว้น สิ่งนี้ช่วยให้เกิดภาพลวงตาของ 3 มิติเนื่องจากการวาดวัตถุที่อยู่ด้านหลังวัตถุอื่น ๆ จะทำให้วัตถุอื่น ๆ ที่อยู่ข้างหน้าปิดทับวัตถุนั้น
ภาพวาดที่คุณทำจะยังคงอยู่บนหน้าจอจนกว่าคุณจะบอกให้ล้าง ในการดำเนินการนี้คุณต้องโทร gl.clear (gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
ซึ่งจะลบทั้งสีและบัฟเฟอร์ความลึก ในการเลือกสีที่จะตั้งค่าพิกเซลที่ถูกลบให้ใช้ gl.clearColor (rojo, verde, azul, alfa)
มาสร้างเรนเดอร์ที่ใช้แคนวาสและล้างตามความต้องการ:
function Renderer (canvas) Renderer.prototype.setClearColor = function (red, green, blue) { gl.clearColor(red / 255, green / 255, blue / 255, 1) } Renderer.prototype.getContext = function () { return this.gl } Renderer.prototype.render = function () gl.DEPTH_BUFFER_BIT) var renderer = new Renderer(document.getElementById('webgl-canvas')) renderer.setClearColor(100, 149, 237) loop() function loop () { renderer.render() requestAnimationFrame(loop) }
การแนบสคริปต์นี้เข้ากับ HTML ต่อไปนี้จะทำให้เกิดสี่เหลี่ยมผืนผ้าสีฟ้าสดใสบนหน้าจอ
requestAnimationFrame
ที่เรียกว่า N
ทำให้โหนดถูกเรียกอีกครั้งทันทีที่การเรนเดอร์ก่อนหน้าเสร็จสิ้นและการจัดการเหตุการณ์ทั้งหมดเสร็จสิ้น
สิ่งแรกที่คุณควรทำคือกำหนดจุดยอดที่คุณต้องการวาด คุณสามารถทำได้โดยอธิบายผ่านเวกเตอร์ในอวกาศ 3 มิติ หลังจากนั้นคุณต้องย้ายข้อมูลนั้นไปยัง GPU RAM สร้าง Buffer ใหม่ เวอร์เท็กซ์บัฟเฟอร์ (ก.พ. ).
ก วัตถุบัฟเฟอร์ โดยทั่วไปเป็นวัตถุที่เก็บอาร์เรย์ของหน่วยความจำบน GPU เป็นก ก.พ. สิ่งนี้บ่งบอกถึงสิ่งที่ GPU สามารถใช้หน่วยความจำได้เท่านั้น โดยส่วนใหญ่แล้ววัตถุบัฟเฟอร์ที่คุณสร้างจะเป็น VBO .
คุณสามารถกรอก ก.พ. รับจุดยอดทั้งหมด 3N
ที่เรามีและสร้างเมทริกซ์ของ ลอย ด้วยองค์ประกอบ 2N
สำหรับตำแหน่งจุดยอด y VBO บรรทัดฐานจุดยอดและ Geometry
สำหรับพิกัดพื้นผิว ก.พ. . กลุ่มละสามคน ลอย หรือสอง ลอย สำหรับพิกัด UV แสดงถึงพิกัดแต่ละจุดของจุดยอด จากนั้นเราส่งอาร์เรย์เหล่านี้ไปยัง GPU และจุดยอดของเราก็พร้อมสำหรับส่วนที่เหลือของช่อง
เนื่องจากตอนนี้ข้อมูลอยู่ใน RAM ของ GPU แล้วคุณจึงสามารถลบข้อมูลนั้นออกจาก RAM ที่ใช้งานทั่วไปได้ นั่นคือเว้นแต่คุณต้องการแก้ไขในภายหลังและโหลดซ้ำ การแก้ไขแต่ละครั้งจะต้องตามด้วยการอัปโหลดเนื่องจากการแก้ไขอาร์เรย์ JS ของเราใช้ไม่ได้กับ VBO ใน RAM จริงของ GPU
ด้านล่างนี้เป็นตัวอย่างโค้ดซึ่งมีฟังก์ชันทั้งหมดที่อธิบายไว้ ข้อสังเกตที่สำคัญคือตัวแปรที่เก็บไว้ใน GPU ไม่ใช่ขยะที่เก็บรวบรวม นั่นหมายความว่าเราต้องลบด้วยตนเองเมื่อเราไม่ต้องการใช้อีกต่อไป เราจะยกตัวอย่างให้คุณเห็นเท่านั้นและเราจะไม่ให้ความสำคัญกับแนวคิดนั้นมากขึ้น การปราบปรามตัวแปร GPU จำเป็นต่อเมื่อคุณวางแผนที่จะหยุดใช้รูปทรงเรขาคณิตบางอย่างตลอดทั้งโปรแกรมของคุณ
นอกจากนี้เรายังเพิ่มการทำให้เป็นอนุกรมในชั้นเรียนของเรา Geometry.prototype.vertexCount = function () { return this.faces.length * 3 } Geometry.prototype.positions = function () { var answer = [] this.faces.forEach(function (face) { face.vertices.forEach(function (vertex) { var v = vertex.position answer.push(v.x, v.y, v.z) }) }) return answer } Geometry.prototype.normals = function () { var answer = [] this.faces.forEach(function (face) { face.vertices.forEach(function (vertex) { var v = vertex.normal answer.push(v.x, v.y, v.z) }) }) return answer } Geometry.prototype.uvs = function () { var answer = [] this.faces.forEach(function (face) { face.vertices.forEach(function (vertex) { var v = vertex.uv answer.push(v.x, v.y) }) }) return answer } //////////////////////////////// function VBO (gl, data, count) { // Creates buffer object in GPU RAM where we can store anything var bufferObject = gl.createBuffer() // Tell which buffer object we want to operate on as a VBO gl.bindBuffer(gl.ARRAY_BUFFER, bufferObject) // Write the data, and set the flag to optimize // for rare changes to the data we're writing gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW) this.gl = gl this.size = data.length / count this.count = count this.data = bufferObject } VBO.prototype.destroy = function () { // Free memory that is occupied by our buffer object this.gl.deleteBuffer(this.data) }
และองค์ประกอบภายใน
VBO
ชนิดข้อมูล gl
สร้างไฟล์ ก.พ. ในบริบท WebGL ที่ส่งผ่านโดยยึดตามอาร์เรย์ที่ส่งผ่านเป็นพารามิเตอร์ที่สอง
คุณสามารถดูการเรียกบริบทได้สามครั้ง createBuffer ()
โทร bindBuffer ()
สร้างไฟล์ กันชน . โทร ARRAY_BUFFER
สั่งให้เครื่องสถานะ WebGL ใช้หน่วยความจำเฉพาะนี้เป็นหน่วยความจำปัจจุบัน ก.พ. (bufferData ()
) สำหรับการดำเนินการทั้งหมดในอนาคตจนกว่าจะระบุไว้เป็นอย่างอื่น หลังจากนั้นเราตั้งค่าของ ก.พ. ปัจจุบันกับข้อมูลที่ให้มาพร้อมกับ deleteBuffer()
.
นอกจากนี้เรายังมีวิธีการทำลายที่ลบวัตถุบัฟเฟอร์ GPU RAM ของเราโดยใช้ function Mesh (gl, geometry) { var vertexCount = geometry.vertexCount() this.positions = new VBO(gl, geometry.positions(), vertexCount) this.normals = new VBO(gl, geometry.normals(), vertexCount) this.uvs = new VBO(gl, geometry.uvs(), vertexCount) this.vertexCount = vertexCount this.position = new Transformation() this.gl = gl } Mesh.prototype.destroy = function () { this.positions.destroy() this.normals.destroy() this.uvs.destroy() }
.
คุณสามารถใช้สาม VBO และการเปลี่ยนแปลงเพื่ออธิบายคุณสมบัติทั้งหมดของตาข่ายพร้อมกับตำแหน่ง
Geometry.loadOBJ('/assets/model.obj').then(function (geometry) { var mesh = new Mesh(gl, geometry) console.log(mesh) mesh.destroy() })
ตัวอย่างเช่นนี่คือวิธีที่เราสามารถโหลดโมเดลเก็บคุณสมบัติไว้ในตาข่าย ( ตาข่าย ) แล้วทำลายมัน:
atributo
ต่อไปนี้เป็นกระบวนการสองขั้นตอนที่อธิบายไว้ก่อนหน้านี้ซึ่งเกี่ยวข้องกับการย้ายจุดไปยังตำแหน่งที่ต้องการและวาดภาพพิกเซลทั้งหมด ในการทำเช่นนี้เราเขียนโปรแกรมที่ทำงานบนกราฟิกการ์ดหลาย ๆ ครั้ง โดยปกติโปรแกรมนี้ประกอบด้วยอย่างน้อยสองส่วน ส่วนแรกคือไฟล์ Vertex Shader ซึ่งดำเนินการสำหรับแต่ละจุดยอดและเอาต์พุตที่เราควรวางจุดยอดบนหน้าจอเหนือสิ่งอื่นใด ส่วนที่สองคือ ชิ้นส่วน Shader ซึ่งทำงานสำหรับแต่ละพิกเซลที่สามเหลี่ยมครอบคลุมบนหน้าจอและสร้างสีที่ควรทาสีพิกเซล
สมมติว่าคุณต้องการมีโมเดลที่เลื่อนไปทางซ้ายและขวาบนหน้าจอ ด้วยวิธีที่ไร้เดียงสาคุณสามารถอัปเดตตำแหน่งของจุดยอดแต่ละจุดและส่งกลับไปที่ GPU กระบวนการนั้นมีราคาแพงและใช้เวลานาน หรือคุณจะให้โปรแกรมให้ GPU ทำงานสำหรับแต่ละจุดยอดและดำเนินการทั้งหมดควบคู่ไปกับโปรเซสเซอร์ที่สร้างขึ้นเพื่อทำงานนั้นอย่างแท้จริง นั่นคือบทบาทของก จุดยอด shader .
ก เงา จุดยอดคือส่วนหนึ่งของไปป์ไลน์การแสดงผลที่ประมวลผลจุดยอดแต่ละจุด โทร เงา ของจุดยอดได้รับจุดยอดเดียวและสร้างจุดยอดเดียวหลังจากใช้การแปลงที่เป็นไปได้ทั้งหมดกับจุดยอด
เฉดสี พวกเขาเขียนด้วย GLSL มีองค์ประกอบที่ไม่ซ้ำกันมากมายในภาษานี้ แต่ไวยากรณ์ส่วนใหญ่คล้ายกับ C มากดังนั้นคนส่วนใหญ่จึงควรเข้าใจ
มีตัวแปรสามประเภทที่เข้าและออกจากไฟล์ เงา ของจุดยอดและทั้งหมดนี้รองรับการใช้งานเฉพาะ:
วิธีเขียนโค้ด html ที่สะอาด
uniforme
- เป็นอินพุตที่มีคุณสมบัติเฉพาะของจุดยอด ก่อนหน้านี้เราได้อธิบายตำแหน่งของจุดยอดเป็นแอตทริบิวต์ในรูปแบบของเวกเตอร์สามองค์ประกอบ คุณสามารถดูแอตทริบิวต์เป็นค่าที่อธิบายจุดยอดuniforme
- เป็นอินพุตที่เหมือนกันสำหรับแต่ละจุดยอดภายในการเรียกใช้การแสดงผลเดียวกัน สมมติว่าเราต้องการย้ายโมเดลของเรากำหนดเมทริกซ์การเปลี่ยนแปลง คุณสามารถใช้ตัวแปร variaciones
เพื่ออธิบายว่า คุณยังสามารถใช้ทรัพยากร GPU เช่นพื้นผิว คุณสามารถดูเครื่องแบบเหล่านี้เป็นค่าที่อธิบายโมเดลหรือส่วนหนึ่งของโมเดลattribute vec3 position; attribute vec3 normal; attribute vec2 uv; uniform mat4 model; uniform mat4 view; uniform mat4 projection; varying vec3 vNormal; varying vec2 vUv; void main() { vUv = uv; vNormal = (model * vec4(normal, 0.)).xyz; gl_Position = projection * view * model * vec4(position, 1.); }
- นี่คือผลลัพธ์ที่เราส่งผ่านไปยังส่วนของ เงา . เนื่องจากสามเหลี่ยมของจุดยอดอาจมีหลายพันพิกเซลแต่ละพิกเซลจะได้รับค่าที่สอดแทรกสำหรับตัวแปรนี้ขึ้นอยู่กับตำแหน่ง ดังนั้นหากจุดยอดหนึ่งส่ง 500 เป็นเอาต์พุตและอีก 100 พิกเซลที่อยู่ตรงกลางของสิ่งเหล่านี้จะได้รับ 300 เป็นอินพุตสำหรับตัวแปรนั้น คุณสามารถดูรูปแบบต่างๆเป็นค่าที่อธิบายพื้นผิวระหว่างจุดยอดสมมติว่าคุณต้องการสร้างไฟล์ เงา ของจุดยอดที่ได้รับตำแหน่งปกติและพิกัด uv สำหรับแต่ละจุดยอดและตำแหน่งมุมมอง (ตำแหน่งกล้องถอยหลัง) และเมทริกซ์การฉายสำหรับแต่ละวัตถุที่แสดง สมมติว่าคุณต้องการวาดแต่ละพิกเซลตามพิกัด uv และค่ามาตรฐาน คุณจะถามตัวเองว่า 'รหัสนั้นจะเป็นอย่างไร'
main
รายการเหล่านี้ส่วนใหญ่ควรอธิบายได้ด้วยตนเอง สิ่งที่สำคัญที่สุดที่ควรทราบคือไม่มีค่าส่งคืนในฟังก์ชัน variantes
ค่าทั้งหมดที่เราต้องการส่งคืนจะถูกกำหนดให้กับตัวแปร gl_Position
หรือตัวแปรพิเศษ ในที่นี้เรากำหนดให้ vec4
ซึ่งเป็นเวกเตอร์สี่มิติดังนั้นมิติสุดท้ายจะต้องอยู่ในรูปเดียวเสมอ สิ่งแปลก ๆ อีกอย่างที่คุณอาจสังเกตเห็นคือวิธีที่เราสร้าง vec4
เวกเตอร์ไม่อยู่ในตำแหน่ง คุณสามารถสร้าง float
ใช้สี่ vec2
s, สอง variables
s หรือชุดค่าผสมอื่น ๆ ที่ให้ผลลัพธ์เป็นสี่องค์ประกอบ มีการหล่อแบบแปลก ๆ มากมายที่ดูสมเหตุสมผลเมื่อคุณคุ้นเคยกับเมทริกซ์การเปลี่ยนแปลง
คุณยังสามารถดูว่าที่นี่เราสามารถทำการแปลงเมทริกซ์ได้อย่างง่ายดาย GLSL ถูกออกแบบมาสำหรับงานประเภทนี้โดยเฉพาะ ตำแหน่งทางออกคำนวณโดยการคูณการฉายแบบจำลองมุมมองและเมทริกซ์และนำไปใช้กับตำแหน่ง เต้าเสียบธรรมดาเพิ่งถูกเปลี่ยนเป็นพื้นที่โลก ต่อมาเราจะอธิบายว่าทำไมเราถึงหยุดอยู่ที่นั่นด้วยการแปลงร่างปกติ
สำหรับตอนนี้เราจะทำให้มันง่ายและไปที่การวาดภาพแต่ละพิกเซล
ก ส่วน shader เป็นขั้นตอนหลังจากแรสเตอร์ในช่องกราฟิก สร้างสีความลึกและข้อมูลอื่น ๆ สำหรับแต่ละพิกเซลของวัตถุที่กำลังทาสี
หลักการที่อยู่เบื้องหลังการใช้แฟรกเมนต์เชเดอร์นั้นคล้ายกับหลักการมาก เฉดสี ของจุดยอด อย่างไรก็ตามมีความแตกต่างใหญ่สามประการ:
atributos
และอินพุต gl_FragColor
ถูกแทนที่ด้วยรายการ 'ผสม' เราเพิ่งไปที่ช่องของเราและสิ่งที่ส่งออกในไฟล์ เงา จุดยอดเป็นรายการในส่วนของ เงา .vec4
ซึ่งก็คือ #ifdef GL_ES precision highp float; #endif varying vec3 vNormal; varying vec2 vUv; void main() { vec2 clampedUv = clamp(vUv, 0., 1.); gl_FragColor = vec4(clampedUv, 1., 1.); }
องค์ประกอบแสดงถึงสีแดงเขียวน้ำเงินและอัลฟา (RGBA) ตามลำดับโดยมีตัวแปรอยู่ในช่วง 0 ถึง 1 คุณควรให้อัลฟ่าอยู่ที่ 1 เว้นแต่คุณจะใช้ความโปร่งใส อย่างไรก็ตามความโปร่งใสเป็นแนวคิดที่ก้าวหน้าพอสมควรดังนั้นเราจึงยึดติดกับวัตถุทึบแสงด้วยเหตุนี้คุณจึงสามารถเขียนไฟล์ เงา ซึ่งวาดช่องสีแดงตามตำแหน่ง U ช่องสีเขียวตามตำแหน่ง V และตั้งค่าช่องสีน้ำเงินเป็นค่าสูงสุด
clamp
ฟังก์ชั่น function ShaderProgram (gl, vertSrc, fragSrc) { var vert = gl.createShader(gl.VERTEX_SHADER) gl.shaderSource(vert, vertSrc) gl.compileShader(vert) if (!gl.getShaderParameter(vert, gl.COMPILE_STATUS)) { console.error(gl.getShaderInfoLog(vert)) throw new Error('Failed to compile shader') } var frag = gl.createShader(gl.FRAGMENT_SHADER) gl.shaderSource(frag, fragSrc) gl.compileShader(frag) if (!gl.getShaderParameter(frag, gl.COMPILE_STATUS)) { console.error(gl.getShaderInfoLog(frag)) throw new Error('Failed to compile shader') } var program = gl.createProgram() gl.attachShader(program, vert) gl.attachShader(program, frag) gl.linkProgram(program) if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { console.error(gl.getProgramInfoLog(program)) throw new Error('Failed to link program') } this.gl = gl this.position = gl.getAttribLocation(program, 'position') this.normal = gl.getAttribLocation(program, 'normal') this.uv = gl.getAttribLocation(program, 'uv') this.model = gl.getUniformLocation(program, 'model') this.view = gl.getUniformLocation(program, 'view') this.projection = gl.getUniformLocation(program, 'projection') this.vert = vert this.frag = frag this.program = program } // Loads shader files from the given URLs, and returns a program as a promise ShaderProgram.load = function (gl, vertUrl, fragUrl) { return Promise.all([loadFile(vertUrl), loadFile(fragUrl)]).then(function (files) { return new ShaderProgram(gl, files[0], files[1]) }) function loadFile (url) { return new Promise(function (resolve) { var xhr = new XMLHttpRequest() xhr.onreadystatechange = function () { if (xhr.readyState == XMLHttpRequest.DONE) { resolve(xhr.responseText) } } xhr.open('GET', url, true) xhr.send(null) }) } }
เพียง จำกัด ทั้งหมด ลอย บนวัตถุให้อยู่ในขอบเขตที่กำหนด ส่วนที่เหลือของโค้ดควรตรงไปตรงมา
เมื่อคำนึงถึงสิ่งนี้สิ่งที่ต้องทำคือการนำสิ่งนี้ไปใช้ใน WebGL
ขั้นตอนต่อไปคือการรวมเฉดสีลงในโปรแกรม:
ShaderProgram.prototype.use = function () { this.gl.useProgram(this.program) }
ไม่มีอะไรจะพูดมากเกี่ยวกับสิ่งที่เกิดขึ้นที่นี่ ให้กับแต่ละคน เงา มีการกำหนดสตริงเป็นแหล่งที่มาและคอมไพล์หลังจากนั้นเราจะตรวจสอบข้อผิดพลาดในการคอมไพล์ ดังนั้นเราจึงสร้างโปรแกรมที่เชื่อมโยงสองสิ่งนี้ เฉดสี . สุดท้ายเราจัดเก็บคำแนะนำสำหรับคุณลักษณะและเครื่องแบบที่เกี่ยวข้องทั้งหมดสำหรับลูกหลาน
สุดท้าย แต่ไม่ท้ายสุดวาดแบบจำลอง
ก่อนอื่นให้เลือกโปรแกรม เงา คุณต้องการใช้อะไร
Transformation.prototype.sendToGpu = function (gl, uniform, transpose) gl.uniformMatrix4fv(uniform, transpose Camera.prototype.use = function (shaderProgram) { this.projection.sendToGpu(shaderProgram.gl, shaderProgram.projection) this.getInversePosition().sendToGpu(shaderProgram.gl, shaderProgram.view) }
จากนั้นจะส่งเครื่องแบบที่เกี่ยวข้องกับกล้องทั้งหมดไปยัง GPU เครื่องแบบเหล่านี้จะเปลี่ยนเพียงครั้งเดียวสำหรับการเปลี่ยนกล้องหรือการเคลื่อนไหวแต่ละครั้ง
VBO.prototype.bindToAttribute = function (attribute) { var gl = this.gl // Tell which buffer object we want to operate on as a VBO gl.bindBuffer(gl.ARRAY_BUFFER, this.data) // Enable this attribute in the shader gl.enableVertexAttribArray(attribute) // Define format of the attribute array. Must match parameters in shader gl.vertexAttribPointer(attribute, this.size, gl.FLOAT, false, 0, 0) }
ในที่สุดการเปลี่ยนแปลงจะถูกนำมาใช้และ VBO และถูกกำหนดให้เป็นเครื่องแบบและคุณลักษณะตามลำดับ เนื่องจากสิ่งนี้จะต้องทำทุกๆ ก.พ. คุณสามารถสร้างการผูกข้อมูลของคุณเป็นวิธีการ
drawArrays ()
จากนั้นจึงกำหนดเมทริกซ์ของสาม ลอย สำหรับเครื่องแบบ เครื่องแบบแต่ละประเภทมีลายเซ็นที่แตกต่างกันดังนั้น เอกสารประกอบ และอื่น ๆ เอกสารประกอบ พวกเขาเป็นเพื่อนของคุณที่นี่ สุดท้ายวาดเมทริกซ์สามเหลี่ยมบนหน้าจอ มันบอกการเรียกดึง [TRIÁNGULOS
] (https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/drawArrays) จากจุดยอดที่สามารถเริ่มต้นได้และจำนวนจุดยอดที่จะวาด . พารามิเตอร์แรกที่ส่งผ่านจะบอก WebGL ว่าจะตีความอาร์เรย์จุดยอดอย่างไร ใช้ PUNTOS
ใช้จุดยอดสามเท่าสามจุดแล้ววาดสามเหลี่ยมสำหรับแต่ละสามเท่า ใช้ Mesh.prototype.draw = function (shaderProgram) { this.positions.bindToAttribute(shaderProgram.position) this.normals.bindToAttribute(shaderProgram.normal) this.uvs.bindToAttribute(shaderProgram.uv) this.position.sendToGpu(this.gl, shaderProgram.model) this.gl.drawArrays(this.gl.TRIANGLES, 0, this.vertexCount) }
มันจะดึงจุดสำหรับจุดยอดแต่ละจุดที่ส่งผ่านเท่านั้น มีตัวเลือกอื่น ๆ อีกมากมาย แต่ไม่จำเป็นต้องค้นพบทุกสิ่งในเวลาเดียวกัน ด้านล่างนี้คือรหัสสำหรับวาดวัตถุ:
Renderer.prototype.setShader = function (shader) { this.shader = shader } Renderer.prototype.render = function (camera, objects) { this.gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) var shader = this.shader if (!shader) { return } shader.use() camera.use(shader) objects.forEach(function (mesh) { mesh.draw(shader) }) }
ตัวแสดงภาพจำเป็นต้องขยายออกไปเล็กน้อยเพื่อรองรับองค์ประกอบเพิ่มเติมทั้งหมดที่ต้องจัดการ ต้องสามารถแนบโปรแกรมของ เงา และแสดงอาร์เรย์ของวัตถุตามตำแหน่งกล้องปัจจุบัน
var renderer = new Renderer(document.getElementById('webgl-canvas')) renderer.setClearColor(100, 149, 237) var gl = renderer.getContext() var objects = [] Geometry.loadOBJ('/assets/sphere.obj').then(function (data) { objects.push(new Mesh(gl, data)) }) ShaderProgram.load(gl, '/shaders/basic.vert', '/shaders/basic.frag') .then(function (shader) { renderer.setShader(shader) }) var camera = new Camera() camera.setOrthographic(16, 10, 10) loop() function loop () { renderer.render(camera, objects) requestAnimationFrame(loop) }
เราสามารถรวมองค์ประกอบทั้งหมดที่เรามีเพื่อวาดบางสิ่งบนหน้าจอได้ในที่สุด:
#ifdef GL_ES precision highp float; #endif varying vec3 vNormal; varying vec2 vUv; void main() { vec3 brown = vec3(.54, .27, .07); gl_FragColor = vec4(brown, 1.); }
ดูเหมือนจะสุ่มเล็กน้อย แต่คุณสามารถเห็นแพตช์ต่างๆบนทรงกลมขึ้นอยู่กับตำแหน่งที่อยู่บนแผนที่ UV คุณสามารถเปลี่ยนไฟล์ เงา เพื่อทาสีวัตถุเป็นสีน้ำตาล เพียงแค่ตั้งค่าสีสำหรับแต่ละพิกเซลให้เป็น RGBA สำหรับสีน้ำตาล:
#ifdef GL_ES precision highp float; #endif varying vec3 vNormal; varying vec2 vUv; void main() { vec3 brown = vec3(.54, .27, .07); vec3 sunlightDirection = vec3(-1., -1., -1.); float lightness = -clamp(dot(normalize(vNormal), normalize(sunlightDirection)), -1., 0.); gl_FragColor = vec4(brown * lightness, 1.); }
ดูไม่ค่อยน่าเชื่อเท่าไหร่ ดูเหมือนว่าฉากนั้นต้องการเอฟเฟกต์การแรเงาบางอย่าง
แสงและเงาเป็นเครื่องมือที่ช่วยให้เรารับรู้รูปร่างของวัตถุ ไฟมีหลายรูปทรงและขนาด: หลอดไฟที่ส่องเป็นรูปกรวยหลอดไฟที่กระจายแสงไปทุกทิศทางและที่น่าสนใจที่สุดคือดวงอาทิตย์ซึ่งอยู่ห่างไกลออกไปมากจนแสงทั้งหมดที่ส่องมาที่ตัวเราจะแผ่ออกไปสำหรับทุกความพยายามและ ไปในทิศทางเดียวกัน
แสงแดดดูเหมือนจะเป็นวิธีที่ง่ายที่สุดในการใช้งานเนื่องจากสิ่งที่คุณต้องมีคือทิศทางที่รังสีทั้งหมดกำลังแพร่กระจาย สำหรับแต่ละพิกเซลที่คุณวาดบนหน้าจอให้ตรวจสอบมุมที่แสงกระทบวัตถุ นี่คือที่มาของบรรทัดฐานพื้นผิว
คุณสามารถเห็นรังสีแสงทั้งหมดที่ไหลไปในทิศทางเดียวกันและตกกระทบพื้นผิวในมุมที่ต่างกันซึ่งขึ้นอยู่กับมุมระหว่างรังสีของแสงและพื้นผิวปกติ ยิ่งจับคู่กันแสงก็ยิ่งแรง
หากคุณสร้างผลิตภัณฑ์จุดระหว่างเวกเตอร์ที่เป็นมาตรฐานสำหรับรังสีของแสงและพื้นผิวปกติคุณจะได้รับ -1 หากรังสีตกกระทบพื้นผิวในแนวตั้งฉากอย่างสมบูรณ์ 0 หากรังสีขนานกับพื้นผิวและ 1 หากมีการส่องสว่างจาก ด้านตรงข้าม ดังนั้นสิ่งที่อยู่ระหว่าง 0 ถึง 1 ไม่ควรเพิ่มแสงในขณะที่ตัวเลขระหว่าง 0 ถึง -1 ควรค่อยๆเพิ่มปริมาณแสงที่ตกกระทบวัตถุ คุณสามารถทดสอบสิ่งนี้ได้โดยการเพิ่มแสงทึบในโค้ด เงา .
#ifdef GL_ES precision highp float; #endif varying vec3 vNormal; varying vec2 vUv; void main() { vec3 brown = vec3(.54, .27, .07); vec3 sunlightDirection = vec3(-1., -1., -1.); float lightness = -clamp(dot(normalize(vNormal), normalize(sunlightDirection)), -1., 0.); float ambientLight = 0.3; lightness = ambientLight + (1. - ambientLight) * lightness; gl_FragColor = vec4(brown * lightness, 1.); }
เราตั้งให้ดวงอาทิตย์ส่องไปในทิศทางไปข้างหน้า - ซ้าย - ลง คุณเห็นไหมว่าไฟล์ เงา แม้ว่ารูปแบบจะผิดปกติมาก คุณยังสามารถสังเกตเห็นความมืดที่ด้านล่างซ้าย เราสามารถเพิ่มระดับของแสงโดยรอบซึ่งจะทำให้พื้นที่ในเงาสว่างขึ้น
#ifdef GL_ES precision highp float; #endif uniform vec3 lightDirection; uniform float ambientLight; varying vec3 vNormal; varying vec2 vUv; void main() { vec3 brown = vec3(.54, .27, .07); float lightness = -clamp(dot(normalize(vNormal), normalize(lightDirection)), -1., 0.); lightness = ambientLight + (1. - ambientLight) * lightness; gl_FragColor = vec4(brown * lightness, 1.); }
คุณสามารถบรรลุเอฟเฟกต์เดียวกันนี้ได้โดยการเข้าสู่ระดับแสงที่เก็บทิศทางของแสงและความเข้มของแสงโดยรอบ จากนั้นคุณสามารถเปลี่ยนตัวอย่างข้อมูลจาก เงา เพื่อรองรับการเพิ่มนั้น
ตอนนี้เขา เงา กลายเป็น:
function Light () { this.lightDirection = new Vector3(-1, -1, -1) this.ambientLight = 0.3 } Light.prototype.use = function (shaderProgram) { var dir = this.lightDirection var gl = shaderProgram.gl gl.uniform3f(shaderProgram.lightDirection, dir.x, dir.y, dir.z) gl.uniform1f(shaderProgram.ambientLight, this.ambientLight) }
ตอนนี้คุณสามารถกำหนดแสง:
this.ambientLight = gl.getUniformLocation(program, 'ambientLight') this.lightDirection = gl.getUniformLocation(program, 'lightDirection')
ในคลาสโปรแกรม เงา เพิ่มเครื่องแบบที่จำเป็น:
Renderer.prototype.render = function (camera, light, objects) { this.gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) var shader = this.shader if (!shader) { return } shader.use() light.use(shader) camera.use(shader) objects.forEach(function (mesh) { mesh.draw(shader) }) }
ในโปรแกรมให้เพิ่มการโทรไปยังไฟใหม่ในตัวแสดงภาพ:
var light = new Light() loop() function loop () { renderer.render(camera, light, objects) requestAnimationFrame(loop) }
ปมจะเปลี่ยนไปเล็กน้อย:
sampler2D
หากคุณทำทุกอย่างถูกต้องภาพที่แสดงควรจะเหมือนกับภาพสุดท้าย
ขั้นตอนสุดท้ายที่ควรพิจารณาคือการเพิ่มพื้นผิวจริงให้กับแบบจำลองของเรา มาทำตอนนี้
HTML5 รองรับการโหลดรูปภาพได้อย่างดีเยี่ยมจึงไม่จำเป็นต้องทำการวิเคราะห์รูปภาพที่เข้มข้นมากนัก ภาพจะถูกส่งไปยัง GLSL เป็น sampler2D
บอก เงา พื้นผิวที่เชื่อมโยงกับตัวอย่างใด มีพื้นผิวจำนวน จำกัด ที่สามารถเชื่อมโยงได้และขีด จำกัด จะขึ้นอยู่กับฮาร์ดแวร์ที่ใช้ ก #ifdef GL_ES precision highp float; #endif uniform vec3 lightDirection; uniform float ambientLight; uniform sampler2D diffuse; varying vec3 vNormal; varying vec2 vUv; void main() { float lightness = -clamp(dot(normalize(vNormal), normalize(lightDirection)), -1., 0.); lightness = ambientLight + (1. - ambientLight) * lightness; gl_FragColor = vec4(texture2D(diffuse, vUv).rgb * lightness, 1.); }
สามารถสอบถามสีได้ในบางตำแหน่ง นี่คือจุดที่พิกัด UV เข้ามา นี่คือตัวอย่างที่เราแทนที่สีน้ำตาลด้วยสีตัวอย่าง
this.diffuse = gl.getUniformLocation(program, 'diffuse')
จะต้องเพิ่มเครื่องแบบใหม่ในรายการในโปรแกรมของ เงา :
function Texture (gl, image) { var texture = gl.createTexture() // Set the newly created texture context as active texture gl.bindTexture(gl.TEXTURE_2D, texture) // Set texture parameters, and pass the image that the texture is based on gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image) // Set filtering methods // Very often shaders will query the texture value between pixels, // and this is instructing how that value shall be calculated gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) this.data = texture this.gl = gl } Texture.prototype.use = function (uniform, binding) Texture.load = function (gl, url) { return new Promise(function (resolve) { var image = new Image() image.onload = function () { resolve(new Texture(gl, image)) } image.src = url }) }
สุดท้ายเราจะใช้การโหลดพื้นผิว ตามที่ระบุไว้ก่อนหน้านี้ HTML5 มีสิ่งอำนวยความสะดวกสำหรับการโหลดรูปภาพ สิ่งที่เราต้องทำคือส่งภาพไปยัง GPU:
sampler2D
กระบวนการไม่แตกต่างจากกระบวนการที่ใช้ในการโหลดและผูกมัด VBO . ข้อแตกต่างที่สำคัญคือเราไม่ได้ผูกมัดกับแอตทริบิวต์อีกต่อไป แต่เราผูกดัชนีพื้นผิวกับชุดจำนวนเต็มแทน ประเภท Mesh
มันไม่มีอะไรมากไปกว่าตัวชี้ที่ย้ายไปที่พื้นผิว
ตอนนี้สิ่งที่คุณต้องทำคือขยายชั้นเรียน function Mesh (gl, geometry, texture) { // added texture var vertexCount = geometry.vertexCount() this.positions = new VBO(gl, geometry.positions(), vertexCount) this.normals = new VBO(gl, geometry.normals(), vertexCount) this.uvs = new VBO(gl, geometry.uvs(), vertexCount) this.texture = texture // new this.vertexCount = vertexCount this.position = new Transformation() this.gl = gl } Mesh.prototype.destroy = function () { this.positions.destroy() this.normals.destroy() this.uvs.destroy() } Mesh.prototype.draw = function (shaderProgram) { this.positions.bindToAttribute(shaderProgram.position) this.normals.bindToAttribute(shaderProgram.normal) this.uvs.bindToAttribute(shaderProgram.uv) this.position.sendToGpu(this.gl, shaderProgram.model) this.texture.use(shaderProgram.diffuse, 0) // new this.gl.drawArrays(this.gl.TRIANGLES, 0, this.vertexCount) } Mesh.load = function (gl, modelUrl, textureUrl) { // new var geometry = Geometry.loadOBJ(modelUrl) var texture = Texture.load(gl, textureUrl) return Promise.all([geometry, texture]).then(function (params) { return new Mesh(gl, params[0], params[1]) }) }
ในการจัดการพื้นผิวด้วย:
var renderer = new Renderer(document.getElementById('webgl-canvas')) renderer.setClearColor(100, 149, 237) var gl = renderer.getContext() var objects = [] Mesh.load(gl, '/assets/sphere.obj', '/assets/diffuse.png') .then(function (mesh) { objects.push(mesh) }) ShaderProgram.load(gl, '/shaders/basic.vert', '/shaders/basic.frag') .then(function (shader) { renderer.setShader(shader) }) var camera = new Camera() camera.setOrthographic(16, 10, 10) var light = new Light() loop() function loop () { renderer.render(camera, light, objects) requestAnimationFrame(loop) }
และสคริปต์สุดท้ายหลักจะมีลักษณะดังนี้:
function loop () { renderer.render(camera, light, objects) camera.position = camera.position.rotateY(Math.PI / 120) requestAnimationFrame(loop) }
แม้แต่การเชียร์ก็สามารถทำได้ง่ายในจุดนี้ หากคุณต้องการให้กล้องหมุนรอบวัตถุของเราคุณสามารถทำได้โดยเพิ่มโค้ดเพียงบรรทัดเดียว:
ขนาดคิวรีสื่อสำหรับการออกแบบที่ตอบสนอง
void main() { float lightness = -clamp(dot(normalize(vNormal), normalize(lightDirection)), -1., 0.); lightness = lightness > 0.1 ? 1. : 0.; // new lightness = ambientLight + (1. - ambientLight) * lightness; gl_FragColor = vec4(texture2D(diffuse, vUv).rgb * lightness, 1.); }
คุณมีอิสระที่จะเล่นด้วย เฉดสี . การเพิ่มบรรทัดของโค้ดจะทำให้แสงที่สมจริงนี้กลายเป็นการ์ตูน
|_+_|
ทำได้ง่ายเพียงแค่บอกให้แสงไปที่ระดับสุดขั้วโดยขึ้นอยู่กับว่ามันเกินเกณฑ์ที่ตั้งไว้หรือไม่
มีแหล่งข้อมูลมากมายสำหรับการเรียนรู้กลเม็ดและความซับซ้อนทั้งหมดของ WebGL และส่วนที่ดีที่สุดคือหากคุณไม่พบคำตอบที่เกี่ยวข้องกับ WebGL คุณสามารถค้นหาได้ใน OpenGL เนื่องจาก WebGL อิงตามส่วนย่อยของ OpenGL ที่มีการเปลี่ยนชื่อบางชื่อ
ไม่เรียงตามลำดับใด ๆ ต่อไปนี้เป็นแหล่งข้อมูลที่ดีเยี่ยมสำหรับข้อมูลโดยละเอียดสำหรับทั้ง WebGL และ OpenGL