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

บทนำ

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

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

1. Shader 2 รายการของเรา

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

  1. โปรแกรมเปลี่ยนรูปแบบเวิร์กเท็กซ์
  2. ตัวปรับแสงเงาระดับเศษ

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

2. เวิร์กเชดเดอร์ของจุดยอด

ใช้รูปทรงพื้นฐานมาตรฐาน เช่น ทรงกลม ประกอบด้วยจุดยอด ถูกต้องไหม เวิร์กเชดเวอร์เทกซ์จะได้รับเวิร์กเชดเวอร์เทกซ์เหล่านี้ทีละรายการและสามารถเล่นกับเวิร์กเชดเวอร์เทกซ์เหล่านั้นได้ ขึ้นอยู่กับว่า Vertex Shader จะทำอะไรกับแต่ละรายการ แต่มีหน้าที่อย่างหนึ่งคือต้องตั้งค่าสิ่งที่เรียกว่า gl_Position ซึ่งเป็นเวกเตอร์ 4 มิติแบบลอยตัว ซึ่งเป็นตำแหน่งสุดท้ายของจุดยอดบนหน้าจอ กระบวนการนี้น่าสนใจมาก เนื่องจากเรากำลังพูดถึงการนำตำแหน่ง 3 มิติ (จุดยอดที่มี x,y,z) มาวางหรือโปรเจ็กต์ลงบนหน้าจอ 2 มิติ แต่โชคดีที่หากเราใช้เครื่องมืออย่าง Three.js จะมีวิธีตั้งค่า gl_Position แบบย่อโดยไม่ต้องเขียนโค้ดให้ยุ่งยาก

3. โปรแกรมเปลี่ยนสีระดับเศษส่วน

เรามีวัตถุที่มีจุดยอด และเราฉายวัตถุเหล่านั้นไปยังหน้าจอ 2 มิติแล้ว แต่จะใช้สีอย่างไร แล้วพื้นผิวและการจัดแสงล่ะ นั่นคือเหตุผลที่ต้องมี Shader ระดับเศษ เช่นเดียวกับ Vertex Shader ตัวแปร Shader ของเศษส่วนมีหน้าที่เพียงอย่างเดียว ซึ่งก็คือต้องตั้งค่าหรือทิ้งตัวแปร gl_FragColor ซึ่งเป็นเวกเตอร์ 4 มิติแบบลอยตัวอีกตัวหนึ่ง ซึ่งเป็นสีสุดท้ายของเศษส่วน แต่ข้อมูลโค้ดคืออะไร ลองนึกถึงจุดยอด 3 จุดที่ประกอบกันเป็นสามเหลี่ยม พิกเซลแต่ละพิกเซลภายในสามเหลี่ยมนั้นต้องวาดออกมา เศษข้อมูลคือข้อมูลที่ได้จากจุดยอดทั้ง 3 จุดดังกล่าวเพื่อวัตถุประสงค์ในการวาดแต่ละพิกเซลในสามเหลี่ยมนั้น ด้วยเหตุนี้ เศษจะได้รับค่าที่อัตราส่วนจากจุดยอดที่ประกอบกัน หากจุดยอดหนึ่งเป็นสีแดง และจุดยอดข้างเคียงเป็นสีน้ำเงิน เราจะเห็นค่าสีที่หาค่าประมาณจากสีแดงผ่านสีม่วงไปจนถึงสีน้ำเงิน

4. ตัวแปร Shader

เมื่อพูดถึงตัวแปร คุณจะประกาศได้ 3 แบบ ได้แก่ Uniform, Attributes และ Varying เมื่อได้ยินชื่อ 3 รายการนี้ครั้งแรก ฉันรู้สึกสับสนมากเนื่องจากชื่อดังกล่าวไม่ตรงกับชื่ออื่นๆ ที่เคยใช้ แต่ลองพิจารณาข้อมูลต่อไปนี้

  1. ระบบจะส่งยูนิฟอร์มไปยังทั้งตัวแปรภาพเวิร์กเชดและตัวแปรภาพแฟรกเมนต์ และมีค่าที่เหมือนกันตลอดทั้งเฟรมที่แสดงผล ตัวอย่างที่ดีของกรณีนี้คือตําแหน่งไฟ

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

  3. ตัวแปรคือตัวแปรที่ประกาศในเวิร์กเทกซ์ Shader ที่ต้องการแชร์กับ ฟร็กเมนต์ Shader โดยเราตรวจสอบว่าได้ประกาศตัวแปรแบบต่างๆ ที่มีประเภทและชื่อเดียวกันทั้งในเวิร์กเท็กซ์เชดและแฟรกเมนต์เชดเดอร์ การใช้งานแบบคลาสสิกของข้อมูลนี้คือนอร์มัลของจุดยอด เนื่องจากนอร์มัลนี้นำไปใช้ในการคำนวณแสงได้

เราจะใช้ทั้ง 3 ประเภทในภายหลังเพื่อให้คุณเห็นภาพว่าการใช้งานจริงเป็นอย่างไร

ตอนนี้เราได้พูดถึง Vertex Shader และ Fragment Shader รวมถึงประเภทตัวแปรที่จัดการกันแล้ว ต่อไปเรามาลองดู Shader ที่ง่ายที่สุดที่เราสร้างได้กัน

5. Bonjourno World

ต่อไปนี้คือ "Hello World" ของเวิร์กเทกซ์ เชดเดอร์

/**
* 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

โอเค เราได้ตั้งค่า Shader แล้ว แต่จะใช้กับ 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

7. ขั้นตอนถัดไป

ตอนนี้เราจะใช้เวลาสักครู่เพื่อเพิ่มลูปภาพเคลื่อนไหว แอตทริบิวต์เวิร์กเท็กซ์ และยูนิฟอร์ม เราจะเพิ่มตัวแปรที่ผันแปรด้วยเพื่อให้เวิร์กเชดเวอร์เทกซ์ส่งข้อมูลบางอย่างไปยังเวิร์กเชดแฟรกเมนต์ได้ ผลลัพธ์ที่ได้คือทรงกลมที่เคยเป็นสีชมพูจะดูเหมือนมีแสงจากด้านบนและด้านข้าง และจะสั่น ข้อมูลนี้อาจดูซับซ้อน แต่หวังว่าจะช่วยให้คุณเข้าใจตัวแปรทั้ง 3 ประเภท รวมถึงความสัมพันธ์ระหว่างตัวแปรแต่ละประเภทกับเรขาคณิตพื้นฐาน

8. แสงจำลอง

มาอัปเดตการระบายสีกันเพื่อไม่ให้เป็นวัตถุสีแบน เราอาจลองดูว่า Three.js จัดการกับแสงอย่างไร แต่เราเข้าใจดีว่าคุณคงเข้าใจว่าวิธีนี้ซับซ้อนกว่าที่เราต้องการในตอนนี้ เราจึงจะจำลองแสงแทน คุณควรดูชิเดอร์ที่ยอดเยี่ยมซึ่งเป็นส่วนหนึ่งของ Three.js รวมถึงชิเดอร์จากโปรเจ็กต์ WebGL ที่ยอดเยี่ยมล่าสุดอย่าง Rome ของ Chris Milk และ Google กลับไปที่ชิเดอร์ เราจะอัปเดต 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 ครั้งจะแสดงให้เห็นว่ารูปภาพเป็นแบบสุ่ม

มาเริ่มกันด้วยการเพิ่มแอตทริบิวต์ลงในเวิร์กเท็กเจอร์ ดังนี้

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 ดังนั้น Shader จึงใช้ค่า 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()
});

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

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

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