บทนำ
ก่อนหน้านี้เราได้ให้ข้อมูลเบื้องต้นเกี่ยวกับ Three.js แก่คุณแล้ว หากคุณยังไม่ได้อ่านบทความดังกล่าว เราขอแนะนําให้อ่าน เนื่องจากบทความนี้เป็นรากฐานสําหรับบทความนี้
ฉันต้องการพูดคุยเกี่ยวกับโปรแกรมเปลี่ยนสี WebGL นั้นยอดเยี่ยมมาก และอย่างที่เราได้กล่าวไปก่อนหน้านี้ Three.js (และไลบรารีอื่นๆ) ทำงานได้อย่างยอดเยี่ยมในการทำให้สิ่งที่ซับซ้อนเข้าใจง่ายขึ้น แต่บางครั้งคุณอาจต้องการสร้างเอฟเฟกต์ที่เฉพาะเจาะจง หรือต้องการเจาะลึกเพิ่มเติมเกี่ยวกับวิธีที่สิ่งต่างๆ ที่น่าทึ่งปรากฏบนหน้าจอ และแทบจะไม่ต้องสงสัยเลยว่า Shader จะเป็นหนึ่งในองค์ประกอบนั้น นอกจากนี้ หากคุณเป็นเหมือนเรา คุณอาจต้องการดูเนื้อหาขั้นพื้นฐานในบทแนะนำที่แล้วต่อด้วยเนื้อหาที่ยากขึ้นเล็กน้อย เราจะดำเนินการโดยสมมติว่าคุณใช้ Three.js เนื่องจากเครื่องมือนี้ช่วยทํางานให้เราได้มากมายในแง่ของการใช้ Shader เราขอแจ้งให้ทราบตั้งแต่ต้นว่าในช่วงเริ่มต้น เราจะอธิบายบริบทของเชดเดอร์ ส่วนช่วงหลังของบทแนะนำนี้จะเข้าสู่เนื้อหาขั้นสูงขึ้นเล็กน้อย สาเหตุคือ Shader นั้นดูแปลกๆ เมื่อเห็นครั้งแรกและต้องมีการอธิบายเพิ่มเติม
1. Shader 2 รายการของเรา
WebGL ไม่ได้เสนอการใช้ไปป์ไลน์แบบคงที่ ซึ่งเป็นวิธีที่สั้นๆ ในการบอกว่า WebGL ไม่ได้ให้วิธีการแสดงผลเนื้อหาของคุณ แต่สิ่งที่ มีให้คือไปป์ไลน์แบบโปรแกรมได้ ซึ่งมีประสิทธิภาพมากกว่า แต่ก็เข้าใจและใช้งานยากกว่า กล่าวโดยย่อคือ ไปป์ไลน์แบบโปรแกรมได้หมายความว่าในฐานะโปรแกรมเมอร์ คุณมีหน้าที่รับผิดชอบในการทำให้จุดยอดและอื่นๆ แสดงผลบนหน้าจอ Shaders เป็นส่วนหนึ่งของไปป์ไลน์นี้ และมี 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:1 กับจุดยอด
ตัวแปรคือตัวแปรที่ประกาศในเวิร์กเทกซ์ 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
- ลูปภาพเคลื่อนไหวใน 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. บทสรุป
เพียงเท่านี้ก็เรียบร้อยแล้ว ตอนนี้คุณจะเห็นภาพเคลื่อนไหวดังกล่าวเต้นเป็นจังหวะแปลกๆ (และดูหลอนเล็กน้อย)
เรายังมีเรื่องอื่นๆ อีกมากมายที่จะพูดถึงเกี่ยวกับหัวข้อนี้ แต่หวังว่าข้อมูลเบื้องต้นนี้จะช่วยคุณได้ ตอนนี้คุณน่าจะเข้าใจเกี่ยวกับเชดเดอร์เมื่อเห็นเชดเดอร์แล้ว รวมถึงมีความมั่นใจที่จะสร้างเชดเดอร์ที่ยอดเยี่ยมของคุณเอง