บทนำ
เราได้แนะนำเกี่ยวกับ Three.js ให้คุณไปแล้วก่อนหน้านี้ หากคุณยังอ่านไม่ออก คุณอาจต้องการทำตามเนื่องจากนี่เป็นรากฐานที่เรา จะสร้างระหว่างบทความนี้
สิ่งที่เราจะทำคือพูดถึงเครื่องมือให้เฉดสี WebGL นั้นยอดเยี่ยมมาก และอย่างที่เราได้กล่าวไปก่อนหน้านี้ Three.js (และไลบรารีอื่นๆ) ทำงานได้อย่างยอดเยี่ยมในการทำให้สิ่งที่ยากๆ กลายเป็นเรื่องง่ายสำหรับคุณ แต่บางครั้งคุณอาจต้องการสร้างเอฟเฟกต์ที่เฉพาะเจาะจง หรือต้องการเจาะลึกเพิ่มเติมเกี่ยวกับวิธีที่สิ่งต่างๆ ที่น่าทึ่งปรากฏบนหน้าจอ และแทบจะไม่ต้องสงสัยเลยว่า Shader จะเป็นส่วนประกอบสำคัญในกระบวนการนี้ นอกจากนี้ หากคุณเป็นเหมือนผม คุณอาจต้องเปลี่ยนจาก ข้อมูลเบื้องต้นในบทแนะนำล่าสุดไปสู่สิ่งที่ยากขึ้นเล็กน้อย เราจะดำเนินการโดยสมมติว่าคุณใช้ Three.js เนื่องจากเครื่องมือนี้ช่วยทํางานให้เราได้มากมายในแง่ของการใช้ Shader เราขอแจ้งให้ทราบตั้งแต่ต้นว่าในช่วงเริ่มต้น เราจะอธิบายบริบทของเชดเดอร์ ส่วนช่วงหลังของบทแนะนำนี้จะเข้าสู่เนื้อหาขั้นสูงขึ้นเล็กน้อย กรณีนี้เกิดจากการที่แสงเงาไม่ปกติเมื่อดูครั้งแรก และใช้เวลาอธิบายสักเล็กน้อย
1. แรเงา 2 แบบของเรา
WebGL ไม่ได้เสนอการใช้ไปป์ไลน์แบบคงที่ ซึ่งเป็นวิธีที่สั้นๆ ในการบอกว่า WebGL ไม่ได้ให้วิธีการแสดงผลเนื้อหาของคุณ แต่สิ่งที่ มีให้คือไปป์ไลน์แบบโปรแกรมได้ ซึ่งมีประสิทธิภาพมากกว่า แต่ก็เข้าใจและใช้งานยากกว่า กล่าวโดยสรุปก็คือ Programmable Pipeline หมายความว่า ในฐานะโปรแกรมเมอร์ คุณจะต้องรับผิดชอบในการแสดงจุดยอดต่างๆ บนหน้าจอ Shaders เป็นส่วนหนึ่งของไปป์ไลน์นี้ และมี 2 ประเภท ได้แก่
- ตัวปรับแสงเงา Vertex
- ตัวปรับแสงเงาระดับเศษ
ทั้งสองข้อนี้ ฉันว่าคุณเห็นด้วย ไม่ได้แปลว่าอะไรไร้ปัญหาใดๆ เลย สิ่งที่ควรทราบเกี่ยวกับแอปเหล่านี้คือทั้ง 2 แอปทำงานบน GPU ของการ์ดกราฟิกโดยสมบูรณ์ ซึ่งหมายความว่าเราต้องการส่งงานทั้งหมดที่ทำได้ไปยังอุปกรณ์ดังกล่าว เพื่อให้ CPU ทำงานอื่นๆ ได้ GPU สมัยใหม่ได้รับการเพิ่มประสิทธิภาพอย่างมากสำหรับฟังก์ชันที่ชิดเดอร์ต้องการ คุณจึงใช้ GPU ได้อย่างยอดเยี่ยม
2. เวิร์กเชดเดอร์ของจุดยอด
ใช้รูปทรงพื้นฐานมาตรฐาน เช่น ทรงกลม ประกอบด้วยจุดยอด ถูกต้องไหม เวิร์กเชดเวอร์เทกซ์จะได้รับเวิร์กเชดเวอร์เทกซ์เหล่านี้ทีละรายการและสามารถเล่นกับเวิร์กเชดเวอร์เทกซ์เหล่านั้นได้ ขึ้นอยู่กับว่า Vertex Shader จะทำอะไรกับแต่ละรายการ แต่มีหน้าที่อย่างหนึ่งคือต้องตั้งค่าสิ่งที่เรียกว่า gl_Position ซึ่งเป็นเวกเตอร์ 4 มิติแบบลอยตัว ซึ่งเป็นตำแหน่งสุดท้ายของจุดยอดบนหน้าจอ กระบวนการนี้น่าสนใจมาก เนื่องจากเรากำลังพูดถึงการนำตำแหน่ง 3 มิติ (จุดยอดที่มี x,y,z) มาวางหรือโปรเจ็กต์ลงบนหน้าจอ 2 มิติ แต่โชคดีที่หากเราใช้เครื่องมืออย่าง Three.js จะมีวิธีตั้งค่า gl_Position แบบย่อโดยไม่ต้องเขียนโค้ดให้ยุ่งยาก
3. ตัวปรับแสงเงา Fragment
เรามีวัตถุที่มีจุดยอด และเราฉายวัตถุเหล่านั้นไปยังหน้าจอ 2 มิติแล้ว แต่จะใช้สีอย่างไร แล้วเรื่องของพื้นผิวและการจัดแสงล่ะ นั่นคือตัวปรับแสงเงา แบบ Fragment นั้นมีไว้เพื่ออะไร เช่นเดียวกับ Vertex Shader ตัวแปร Shader ของเศษส่วนมีหน้าที่เพียงอย่างเดียว ซึ่งก็คือต้องตั้งค่าหรือทิ้งตัวแปร gl_FragColor ซึ่งเป็นเวกเตอร์ 4 มิติแบบลอยตัวอีกตัวหนึ่ง ซึ่งเป็นสีสุดท้ายของเศษส่วน แต่ส่วนย่อยคืออะไร ลองนึกถึงจุดยอด 3 จุดที่ประกอบกันเป็นสามเหลี่ยม พิกเซลแต่ละพิกเซลภายในสามเหลี่ยมนั้นต้องวาดออกมา เศษข้อมูลคือข้อมูลที่ได้จากจุดยอดทั้ง 3 จุดดังกล่าวเพื่อวัตถุประสงค์ในการวาดแต่ละพิกเซลในสามเหลี่ยมนั้น ด้วยเหตุนี้ส่วนย่อยจึงได้รับค่าที่ประมาณจากจุดยอดของส่วนประกอบ หากจุดยอดหนึ่งเป็นสีแดง และจุดยอดข้างเคียงเป็นสีน้ำเงิน เราจะเห็นค่าสีที่หาค่าประมาณจากสีแดงผ่านสีม่วงไปจนถึงสีน้ำเงิน
4. ตัวแปรเฉดสี
เมื่อพูดถึงตัวแปร คุณจะประกาศได้ 3 แบบ ได้แก่ Uniform, Attributes และ Varying เมื่อได้ยินชื่อ 3 รายการนี้ครั้งแรก ฉันรู้สึกสับสนมากเนื่องจากชื่อดังกล่าวไม่ตรงกับชื่ออื่นๆ ที่เคยใช้ แต่ลองมาดูว่า
ระบบจะส่งยูนิฟอร์มไปยังทั้งตัวแปรภาพเวิร์กเชดและตัวแปรภาพแฟรกเมนต์ และมีค่าที่เหมือนกันตลอดทั้งเฟรมที่ผ่านการจัดการแสดงผล ตัวอย่างที่ดีของกรณีนี้คือตําแหน่งไฟ
แอตทริบิวต์คือค่าที่ใช้กับจุดยอดแต่ละจุด แอตทริบิวต์ใช้ได้กับเวิร์กเชดเวอร์เทกซ์เท่านั้น ซึ่งอาจเป็นลักษณะอย่างเช่นแต่ละจุดยอดมีสีแตกต่างกัน แอตทริบิวต์มีความสัมพันธ์แบบ 1:1 กับจุดยอด
ตัวแปรที่ผันแปรคือตัวแปรที่ประกาศในเวิร์กเทกซ์ Shader ที่ต้องการแชร์กับ ฟร็กเมนต์ Shader โดยเราตรวจสอบว่าได้ประกาศตัวแปรแบบต่างๆ ที่มีประเภทและชื่อเดียวกันทั้งในเวิร์กเท็กซ์ Shader และแฟรกเมนต์ Shader การใช้งานแบบคลาสสิกขององค์ประกอบนี้คือนอร์มัลของจุดยอด เนื่องจากนอร์มัลนี้นำไปใช้ในการคำนวณแสงได้
เราจะใช้ทั้ง 3 ประเภทในภายหลังเพื่อให้คุณเห็นภาพว่าการใช้งานจริงเป็นอย่างไร
ตอนนี้เราได้พูดถึง Vertex Shader และ Fragment Shader รวมถึงประเภทตัวแปรที่จัดการกันแล้ว ต่อไปเรามาลองดู Shader ที่ง่ายที่สุดที่เราสร้างได้กัน
5. Bonjourno World
ต่อไปคือ Hello World ของ Vertex Shaders มีดังนี้
/**
* Multiply each vertex by the model-view matrix
* and the projection matrix (both provided by
* Three.js) to get a final vertex position
*/
void main() {
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(position,1.0);
}
และนี่คือโค้ดเดียวกันสำหรับ Shader ระดับเศษ
/**
* Set the colour to a lovely pink.
* Note that the color is a 4D Float
* Vector, R,G,B and A and each part
* runs from 0.0 to 1.0
*/
void main() {
gl_FragColor = vec4(1.0, 0.0, 1.0, 1.0);
}
แต่ไม่ซับซ้อนเกินไปใช่ไหม
ในเวิร์กเท็กเจอร์ Three.js ส่งยูนิฟอร์ม 2 รายการให้เรา ยูนิฟอร์ม 2 รายการนี้คือเมทริกซ์ 4 มิติที่เรียกว่าเมทริกซ์โมเดล-มุมมองและเมทริกซ์การฉายภาพ คุณไม่จำเป็นต้องทราบวิธีการทํางานของสิ่งเหล่านี้อย่างละเอียด แต่การทำความเข้าใจวิธีการทํางานของสิ่งต่างๆ นั้นเป็นสิ่งที่ควรทำเสมอหากทําได้ เวอร์ชันสั้นๆ เป็นเพราะตำแหน่ง 3 มิติของจุดยอดมุมจะแสดงในตำแหน่ง 2 มิติสุดท้ายบนหน้าจอ
เราไม่ได้ใส่บรรทัดดังกล่าวไว้ในข้อมูลโค้ดด้านบนเนื่องจาก Three.js จะเพิ่มบรรทัดดังกล่าวไว้ที่ด้านบนของโค้ด Shader เอง คุณจึงไม่ต้องกังวลเกี่ยวกับเรื่องนี้ อันที่จริงแล้ว ไฟล์นี้เพิ่มข้อมูลมากกว่านั้นมาก เช่น ข้อมูลแสง สีของจุดยอด และเวิร์กเนอร์ัลของจุดยอด หากทําโดยไม่ใช้ Three.js คุณจะต้องสร้างและตั้งค่ายูนิฟอร์มและแอตทริบิวต์ทั้งหมดด้วยตนเอง เรื่องราวจริง
6. การใช้ MeshShaderMaterial
โอเค เราได้ตั้งค่าชิเดอร์แล้ว แต่จะใช้กับ Three.js ได้อย่างไร แต่ปรากฏว่าทำได้ง่ายมาก การทำงานจะคล้ายกับตัวอย่างต่อไปนี้
/**
* Assume we have jQuery to hand and pull out
* from the DOM the two snippets of text for
* each of our shaders
*/
var shaderMaterial = new THREE.MeshShaderMaterial({
vertexShader: $('vertexshader').text(),
fragmentShader: $('fragmentshader').text()
});
จากนั้น Three.js จะคอมไพล์และเรียกใช้ตัวให้เฉดสีที่แนบอยู่กับตาข่ายที่คุณให้วัสดุนั้น การดำเนินการนี้ง่ายที่สุดแล้ว ก็อาจจะใช่ แต่เรากำลังพูดถึงโมเดล 3 มิติที่ทำงานในเบราว์เซอร์ของคุณ เราจึงคิดว่าคุณคงเข้าใจถึงความซับซ้อนของการทำงาน
เราเพิ่มพร็อพเพอร์ตี้อีก 2 รายการลงใน MeshShaderMaterial ได้ ได้แก่ ยูนิฟอร์มและแอตทริบิวต์ ตัวแปรเหล่านี้สามารถใช้ได้ทั้งเวกเตอร์ จำนวนเต็ม หรือจำนวนทศนิยม แต่อย่างที่ได้กล่าวไปก่อนหน้านี้ว่าค่าแบบคงที่เหมือนกันสำหรับทั้งเฟรม กล่าวคือสำหรับจุดยอดทั้งหมด ดังนั้นค่าเหล่านี้จึงมักจะเป็นค่าเดี่ยว แต่แอตทริบิวต์คือตัวแปรต่อเวิร์กเท็กซ์ จึงควรเป็นอาร์เรย์ ควรมีความสัมพันธ์แบบ 1 ต่อ 1 ระหว่างจำนวนค่าในอาร์เรย์แอตทริบิวต์และจำนวนจุดยอดใน Mesh
7. ขั้นตอนถัดไป
ตอนนี้เราจะใช้เวลาสักครู่ในการเพิ่มลูปภาพเคลื่อนไหว แอตทริบิวต์เวิร์กเท็กซ์ และยูนิฟอร์ม เราจะเพิ่มตัวแปรที่ผันแปรด้วยเพื่อให้เวิร์กเชดเวอร์เทกซ์ส่งข้อมูลบางอย่างไปยังเวิร์กเชดแฟรกเมนต์ได้ ผลลัพธ์ที่ได้คือทรงกลมที่เคยเป็นสีชมพูจะดูเหมือนมีแสงจากด้านบนและด้านข้าง และจะสั่น ค่อนข้างซับซ้อน แต่หวังว่าจะทำให้คุณเข้าใจตัวแปรทั้ง 3 ประเภท รวมถึงความเกี่ยวข้องกันและรูปเรขาคณิตที่อยู่เบื้องหลัง
8. แสงจำลอง
โปรดอัปเดตการลงสีให้ไม่ใช่วัตถุที่มีสีแบนราบกัน เราอาจดูวิธีที่ Three.js จัดการกับแสง แต่เราเข้าใจดีว่าคุณคงเข้าใจว่าวิธีนี้ซับซ้อนกว่าที่เราต้องการในตอนนี้ เราจึงจะจำลองแสงแทน คุณควรดูเครื่องมือให้เฉดสีที่ยอดเยี่ยมซึ่งเป็นส่วนหนึ่งของ Three.js และเครื่องมือจากโครงการ WebGL ที่น่าทึ่งล่าสุดโดย Chris Milk และ Google, Rome กลับไปที่ชิเดอร์ เราจะอัปเดต Vertex Shader เพื่อส่งแต่ละจุดยอดไปให้ Fragment Shader เราดำเนินการดังกล่าวด้วยสิ่งต่อไปนี้
// create a shared variable for the
// VS and FS containing the normal
varying vec3 vNormal;
void main() {
// set the vNormal value with
// the attribute value passed
// in by Three.js
vNormal = normal;
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(position,1.0);
}
และใน Fragment Shader เราจะตั้งชื่อตัวแปรเดียวกัน จากนั้นใช้ผลคูณของจุดของจุดยอดปกติที่มีเวกเตอร์ที่แสดงถึงแสงที่ส่องจากด้านบนและด้านขวาของทรงกลม ผลลัพธ์ที่ได้คือเอฟเฟกต์ที่คล้ายกับแสงทิศทางในแพ็กเกจ 3 มิติ
// same name and type as VS
varying vec3 vNormal;
void main() {
// calc the dot product and clamp
// 0 -> 1 rather than -1 -> 1
vec3 light = vec3(0.5,0.2,1.0);
// ensure it's normalized
light = normalize(light);
// calculate the dot product of
// the light to the vertex normal
float dProd = max(0.0, dot(vNormal, light));
// feed into our frag colour
gl_FragColor = vec4(dProd, dProd, dProd, 1.0);
}
ดังนั้นเหตุผลที่ผลคูณจุดทํางานได้คือเมื่อให้เวกเตอร์ 2 รายการ ผลคูณจุดจะให้ตัวเลขที่บอกความ "คล้ายกัน" ของเวกเตอร์ 2 รายการนั้น เมื่อใช้เวกเตอร์ที่ปรับให้เป็นมาตรฐานแล้ว หากเวกเตอร์ชี้ไปในทิศทางเดียวกันทุกประการ คุณจะได้รับค่า 1 หากนิ้วชี้ไปในทิศทางตรงกันข้าม คุณจะได้รับคะแนน -1 สิ่งที่เราทําคือนำตัวเลขนั้นไปใช้กับการจัดแสง ดังนั้นจุดยอดด้านขวาบนจะมีค่าใกล้เคียงหรือเท่ากับ 1 ซึ่งหมายความว่ามีแสงสว่างเต็มที่ ส่วนจุดยอดด้านข้างจะมีค่าใกล้เคียงกับ 0 และจุดยอดด้านหลังจะมีค่าเป็น -1 เราจะจำกัดค่าเป็น 0 สำหรับค่าลบทั้งหมด แต่เมื่อคุณป้อนตัวเลข คุณจะเห็นแสงพื้นฐานที่เราเห็น
ขั้นตอนถัดไปคือ เราขอแนะนำให้ลองปรับตำแหน่งจุดยอด
9. Attributes
สิ่งที่เราต้องการทําตอนนี้คือการแนบตัวเลขสุ่มไปยังจุดยอดแต่ละจุดผ่านแอตทริบิวต์ เราจะใช้ตัวเลขนี้ดันจุดยอดมุมออกไป ตามปกติ ผลลัพธ์สุทธิจะเป็นลูกบอลที่มีหนามแปลกๆ ซึ่งจะเปลี่ยนแปลงทุกครั้งที่คุณรีเฟรชหน้าเว็บ รูปภาพยังไม่เป็นภาพเคลื่อนไหว (ซึ่งจะเกิดขึ้นในลำดับต่อไป) แต่เมื่อรีเฟรชหน้าอีก 2-3 หน้า ก็จะเป็นการสุ่มเลือก
เริ่มต้นด้วยการเพิ่มแอตทริบิวต์ลงใน Vertex Shadr ดังนี้
attribute float displacement;
varying vec3 vNormal;
void main() {
vNormal = normal;
// push the displacement into the three
// slots of a 3D vector so it can be
// used in operations with other 3D
// vectors like positions and normals
vec3 newPosition = position +
normal *
vec3(displacement);
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(newPosition,1.0);
}
แล้วดูว่าการทำงานเป็นอย่างไร
ก็ไม่ต่างกันเท่าไหร่นะ ที่เป็นเช่นนี้เพราะยังไม่มีการตั้งค่าแอตทริบิวต์ใน MeshShaderMaterial ดังนั้นเฉดสีจึงใช้ค่า 0 แทน ข้อมูลนี้ยังถือเป็นตัวยึดตำแหน่งอยู่ ในอีกสักครู่ เราจะเพิ่มแอตทริบิวต์ลงใน MeshShaderMaterial ใน JavaScript และ Three.js จะเชื่อมโยงทั้งสองเข้าด้วยกันโดยอัตโนมัติ
สิ่งที่ควรทราบอีกอย่างคือฉันต้องกำหนดตำแหน่งที่อัปเดตให้กับตัวแปร vec3 ใหม่ เนื่องจากแอตทริบิวต์เดิมเป็นแบบอ่านอย่างเดียวเช่นเดียวกับแอตทริบิวต์ทั้งหมด
10. การอัปเดต MeshShaderMaterial
มาเริ่มอัปเดต MeshShaderMaterial กันเลยด้วยแอตทริบิวต์ที่จําเป็นสําหรับขับเคลื่อนการเปลี่ยนตำแหน่ง อย่าลืมว่าแอตทริบิวต์ต่างๆ เป็นค่าต่อจุดยอดมุม เราจึงต้องมี 1 ค่าต่อจุดยอดมุมในทรงกลม ดังนี้
var attributes = {
displacement: {
type: 'f', // a float
value: [] // an empty array
}
};
// create the material and now
// include the attributes property
var shaderMaterial = new THREE.MeshShaderMaterial({
attributes: attributes,
vertexShader: $('#vertexshader').text(),
fragmentShader: $('#fragmentshader').text()
});
// now populate the array of attributes
var vertices = sphere.geometry.vertices;
var values = attributes.displacement.value
for(var v = 0; v < vertices.length; v++) {
values.push(Math.random() * 30);
}
ตอนนี้เราเห็นทรงกลมบิดเบี้ยว แต่สิ่งที่เจ๋งคือการเปลี่ยนแปลงทั้งหมดเกิดขึ้นใน GPU
11. Animating That Sucker
เราควรทำให้ภาพเคลื่อนไหวนี้ เราจะทำอย่างไร มี 2 สิ่งที่เราต้องเตรียม ได้แก่
- เครื่องแบบเพื่อสร้างการเคลื่อนไหวของระยะการกระจัดในแต่ละเฟรม เราสามารถใช้ฟังก์ชันไซน์หรือโคไซน์ได้ เนื่องจากฟังก์ชันเหล่านี้ทำงานได้ตั้งแต่ -1 ถึง 1
- ลูปภาพเคลื่อนไหวใน JS
เราจะเพิ่มแบบเดียวกัน ทั้งใน MeshShaderMaterial และ Vertex Shader เริ่มจาก Vertex Shader
uniform float amplitude;
attribute float displacement;
varying vec3 vNormal;
void main() {
vNormal = normal;
// multiply our displacement by the
// amplitude. The amp will get animated
// so we'll have animated displacement
vec3 newPosition = position +
normal *
vec3(displacement *
amplitude);
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(newPosition,1.0);
}
ต่อไปเราจะอัปเดต MeshShaderMaterial โดยทำดังนี้
// add a uniform for the amplitude
var uniforms = {
amplitude: {
type: 'f', // a float
value: 0
}
};
// create the final material
var shaderMaterial = new THREE.MeshShaderMaterial({
uniforms: uniforms,
attributes: attributes,
vertexShader: $('#vertexshader').text(),
fragmentShader: $('#fragmentshader').text()
});
ตอนนี้เครื่องมือให้เฉดสีของเราเสร็จแล้ว แต่ดูเหมือนเราจะถอยหลังไป 1 ก้าว สาเหตุหลักๆ ก็คือค่าแอมพลิจูดอยู่ที่ 0 และเนื่องจากเราคูณค่านั้นกับการกระจัด เราจึงไม่เห็นการเปลี่ยนแปลงใดๆ และเรายังไม่ได้ตั้งค่าลูปภาพเคลื่อนไหว จึงไม่เห็นการเปลี่ยนแปลง 0 กับสิ่งอื่นเลย
ตอนนี้เราต้องรวมการเรียกการแสดงผลไว้ในฟังก์ชันหนึ่งใน JavaScript จากนั้นใช้ requestAnimationFrame เพื่อเรียกใช้ เราต้องอัปเดตค่าของชุดเครื่องแบบด้วย
var frame = 0;
function update() {
// update the amplitude based on
// the frame value
uniforms.amplitude.value = Math.sin(frame);
frame += 0.1;
renderer.render(scene, camera);
// set up the next call
requestAnimFrame(update);
}
requestAnimFrame(update);
12. บทสรุป
เพียงเท่านี้ก็เรียบร้อยแล้ว ตอนนี้คุณจะเห็นภาพเคลื่อนไหวแบบเต้นระริกแปลกๆ (และดูหลอนเล็กน้อย)
เรายังมีเรื่องอื่นๆ อีกมากมายที่จะพูดถึงเกี่ยวกับหัวข้อนี้ แต่หวังว่าข้อมูลเบื้องต้นนี้จะช่วยคุณได้ ตอนนี้คุณน่าจะเข้าใจเกี่ยวกับเชดเดอร์เมื่อเห็นเชดเดอร์แล้ว รวมถึงมีความมั่นใจที่จะสร้างเชดเดอร์ที่ยอดเยี่ยมของคุณเอง