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

เกริ่นนำ

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

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

1. เครื่องมือเฉดสี 2 แบบของเรา

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

  1. ตัวปรับเฉดสี Vertex
  2. ตัวปรับเฉดสีส่วนย่อย

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

2. ตัวปรับเฉดสี Vertex

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

3. เฉดสีสำหรับ Fragment

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

4. ตัวแปรการให้แสงเงา

เมื่อพูดถึงตัวแปร คุณสามารถประกาศ 3 แบบ ได้แก่ เครื่องแบบ แอตทริบิวต์ และรูปแบบ ตอนที่ได้ยิน 3 รายการนี้เป็นครั้งแรก เราสับสนมากเพราะไม่ตรงกับ 3 อย่างที่ฉันเคยร่วมงานด้วย แต่คุณก็อาจคำนึงถึงสิ่งต่อไปนี้ด้วย

  1. เครื่องแบบจะส่งไปยังทั้งตัวสร้างเฉดสีเวอร์เท็กซ์และตัวปรับเฉดสีส่วนย่อย รวมถึงมีค่าที่เหมือนกันทั่วทั้งเฟรมที่แสดงผล ตัวอย่างที่ดีคือตำแหน่งของหลอดไฟ

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

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

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

เราได้พูดถึงตัวปรับเฉดสีเวอร์เท็กซ์และตัวให้เฉดสีส่วนย่อยและประเภทของตัวแปรที่มันจัดการแล้ว ตอนนี้ก็ขอแนะนำให้ดูตัวให้เฉดสีที่ง่ายที่สุดที่เราสามารถสร้างได้

5. สวนสนุก Bonjourno World

นี่คือ Hello World ของ Vertex Shades

/**
* 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);
}   

ซึ่งจะเหมือนกันใน Fragment Shades

/**
* 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);
}

ไม่ซับซ้อนเกินไปใช่ไหม

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

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

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

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. สร้างภาพเคลื่อนไหวที่ห่วยแตก

เราควรทำให้ภาพเคลื่อนไหวนี้สมบูรณ์แบบ เราทำได้อย่างไร เอาล่ะ มี 2 สิ่งที่เราต้องทำให้เสร็จ ได้แก่

  1. เครื่องแบบสำหรับแสดงภาพเคลื่อนไหวว่าควรใช้การกระจัดกระจายในแต่ละเฟรม เราสามารถใช้ sine หรือ cosine สำหรับเรื่องนั้นได้ เนื่องจากค่าเหล่านี้จะมีตั้งแต่ -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. บทสรุป

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

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