บทนำเกี่ยวกับเครื่องมือให้เฉดสี

บทนำ

เราได้แนะนำเกี่ยวกับ Three.js ให้คุณไปแล้วก่อนหน้านี้ หากคุณยังอ่านไม่ออก คุณอาจต้องการทำตามเนื่องจากนี่เป็นรากฐานที่เรา จะสร้างระหว่างบทความนี้

สิ่งที่เราจะทำคือพูดถึงเครื่องมือให้เฉดสี WebGL นั้นยอดเยี่ยมมาก และอย่างที่เราได้กล่าวไปก่อนหน้านี้ Three.js (และไลบรารีอื่นๆ) ทำงานได้อย่างยอดเยี่ยมในการทำให้สิ่งที่ยากๆ กลายเป็นเรื่องง่ายสำหรับคุณ แต่บางครั้งคุณอาจต้องการสร้างเอฟเฟกต์ที่เฉพาะเจาะจง หรือต้องการเจาะลึกเพิ่มเติมเกี่ยวกับวิธีที่สิ่งต่างๆ ที่น่าทึ่งปรากฏบนหน้าจอ และแทบจะไม่ต้องสงสัยเลยว่า Shader จะเป็นส่วนประกอบสำคัญในกระบวนการนี้ นอกจากนี้ หากคุณเป็นเหมือนผม คุณอาจต้องเปลี่ยนจาก ข้อมูลเบื้องต้นในบทแนะนำล่าสุดไปสู่สิ่งที่ยากขึ้นเล็กน้อย เราจะดำเนินการโดยสมมติว่าคุณใช้ Three.js เนื่องจากเครื่องมือนี้ช่วยทํางานให้เราได้มากมายในแง่ของการใช้ Shader เราขอแจ้งให้ทราบตั้งแต่ต้นว่าในช่วงเริ่มต้น เราจะอธิบายบริบทของเชดเดอร์ ส่วนช่วงหลังของบทแนะนำนี้จะเข้าสู่เนื้อหาขั้นสูงขึ้นเล็กน้อย กรณีนี้เกิดจากการที่แสงเงาไม่ปกติเมื่อดูครั้งแรก และใช้เวลาอธิบายสักเล็กน้อย

1. แรเงา 2 แบบของเรา

WebGL ไม่ได้เสนอการใช้ไปป์ไลน์แบบคงที่ ซึ่งเป็นวิธีที่สั้นๆ ในการบอกว่า WebGL ไม่ได้ให้วิธีการแสดงผลเนื้อหาของคุณ แต่สิ่งที่ มีให้คือไปป์ไลน์แบบโปรแกรมได้ ซึ่งมีประสิทธิภาพมากกว่า แต่ก็เข้าใจและใช้งานยากกว่า กล่าวโดยสรุปก็คือ Programmable Pipeline หมายความว่า ในฐานะโปรแกรมเมอร์ คุณจะต้องรับผิดชอบในการแสดงจุดยอดต่างๆ บนหน้าจอ Shaders เป็นส่วนหนึ่งของไปป์ไลน์นี้ และมี 2 ประเภท ได้แก่

  1. ตัวปรับแสงเงา Vertex
  2. ตัวปรับแสงเงาระดับเศษ

ทั้งสองข้อนี้ ฉันว่าคุณเห็นด้วย ไม่ได้แปลว่าอะไรไร้ปัญหาใดๆ เลย สิ่งที่ควรทราบเกี่ยวกับแอปเหล่านี้คือทั้ง 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. ระบบจะส่งยูนิฟอร์มไปยังทั้งตัวแปรภาพเวิร์กเชดและตัวแปรภาพแฟรกเมนต์ และมีค่าที่เหมือนกันตลอดทั้งเฟรมที่ผ่านการจัดการแสดงผล ตัวอย่างที่ดีของกรณีนี้คือตําแหน่งไฟ

  2. แอตทริบิวต์คือค่าที่ใช้กับจุดยอดแต่ละจุด แอตทริบิวต์ใช้ได้กับเวิร์กเชดเวอร์เทกซ์เท่านั้น ซึ่งอาจเป็นลักษณะอย่างเช่นแต่ละจุดยอดมีสีแตกต่างกัน แอตทริบิวต์มีความสัมพันธ์แบบ 1:1 กับจุดยอด

  3. ตัวแปรที่ผันแปรคือตัวแปรที่ประกาศในเวิร์กเทกซ์ 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 ถึง 1
  2. ลูปภาพเคลื่อนไหวใน 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. บทสรุป

เพียงเท่านี้ก็เรียบร้อยแล้ว ตอนนี้คุณจะเห็นภาพเคลื่อนไหวแบบเต้นระริกแปลกๆ (และดูหลอนเล็กน้อย)

เรายังมีเรื่องอื่นๆ อีกมากมายที่จะพูดถึงเกี่ยวกับหัวข้อนี้ แต่หวังว่าข้อมูลเบื้องต้นนี้จะช่วยคุณได้ ตอนนี้คุณน่าจะเข้าใจเกี่ยวกับเชดเดอร์เมื่อเห็นเชดเดอร์แล้ว รวมถึงมีความมั่นใจที่จะสร้างเชดเดอร์ที่ยอดเยี่ยมของคุณเอง