- เริ่มจากแอนิเมชัน CSS
border-radius แบบเรียบง่ายบนหน้าเว็บ แล้วลงมือสร้างแอนิเมชัน blob ที่ลื่นไหลด้วย fragment shader เพียงไม่กี่บรรทัด
- Shader คือโปรแกรมขนาดเล็กที่เปลี่ยนพิกัดพิกเซลให้เป็นสีบน GPU และทำงานได้เร็วด้วย การประมวลผลแบบขนาน แต่มีข้อจำกัดมากเรื่องการแชร์สถานะและ abstraction ระดับสูง
- ตัวอย่างพื้นฐานของ GLSL คือการแมปพิกัด
vUv ไปเป็นค่า RGBA ของ gl_FragColor พร้อมเรียนรู้ไวยากรณ์อย่าง varying, uniform, ชนิดเวกเตอร์ และ swizzling ไปตามลำดับ
- วงกลมและ blob สร้างด้วย
distance(), step(), smoothstep() และ Signed Distance Function (SDF) และนำ SDF หลายตัวมารวมกันด้วย min() หรือ smooth minimum
- ใช้
u_time เพื่อสร้างการเคลื่อนไหวของลูกบอล และเมื่อเพิ่ม u_mouse เข้าไปในอาร์เรย์ของจุดศูนย์กลาง ก็ขยายเป็น interactive shader ที่ผู้ใช้ควบคุมบางส่วนด้วยเมาส์ได้
สิ่งที่ Shader ทำและข้อจำกัด
- Shader คือโปรแกรมขนาดเล็กที่ทำงานบน GPU และอย่างน้อยที่สุดจะรับพิกัดพิกเซลเป็นอินพุตแล้วส่งออก สี
- ในวิดีโอเกม ใช้สร้างเอฟเฟกต์ภาพ เช่น แสง เอฟเฟกต์พิเศษ และการเรนเดอร์สไตล์การ์ตูน เป็นรากฐานสำคัญของกราฟิกเกมสมัยใหม่
- หัวใจของความเร็วอยู่ที่ การทำงานแบบขนาน ซึ่งรันพร้อมกันบนหลายพิกเซล
- บทนำนี้โฟกัสที่ OpenGL Shading Language หรือ GLSL ซึ่งเป็นมิตรกับเบราว์เซอร์
- สิ่งที่ต้องแลกมากับประสิทธิภาพคือ Shader ต้องเขียนให้เล็กและอยู่ในระดับล่าง
- พึ่งพา abstraction ระดับสูงหรือการนำเข้าไลบรารีได้ยาก
- เนื่องจากลักษณะการทำงานแบบขนาน จึงเป็นแนวทางแบบ ไร้สถานะ·ไร้หน่วยความจำ ที่ไม่สามารถจัดเก็บหรือแชร์ข้อมูลระหว่างพิกเซลได้
ตัวอย่าง GLSL แรก: เปลี่ยนพิกัดให้เป็นสี
- Shader แปลงพิกัดที่ทำให้เป็นค่าปกติแล้วให้เป็นสี RGBA
- โดยทั่วไปพิกัดจะถูก normalize ให้อยู่ระหว่าง 0 ถึง 1
(0, 0) คือมุมซ้ายล่าง และ (1, 1) คือมุมขวาบน
- ตามธรรมเนียม พิกัดนี้เรียกว่า
st หรือ uv
- ตัวอย่างที่ง่ายที่สุดคือ gradient ที่ค่าสีแดงเพิ่มขึ้นเมื่อพิกัด x มากขึ้น และค่าสีเขียวเพิ่มขึ้นเมื่อพิกัด y มากขึ้น
varying vec2 vUv;
void main() {
vec2 st = vUv;
gl_FragColor = vec4(st.x, st.y, 0.0, 1.0);
}
- gradient สีน้ำเงินสร้างได้ด้วยการใส่พิกัด x เข้าไปในช่องสีน้ำเงิน เช่น
gl_FragColor = vec4(0.0, 0.0, st.x, 1.0);
- องค์ประกอบสำคัญของไวยากรณ์มีดังนี้
varying: อินพุตที่ค่าแตกต่างกันไปในแต่ละพิกเซล
uniform: อินพุตที่เหมือนกันในทุกพิกเซล
vec2, vec3, vec4, mat2, mat3: ชนิดเวกเตอร์และเมทริกซ์ที่ระบุชนิดแบบเดียวกับภาษา C
- swizzling: รูปแบบการเขียนเพื่อดึงบางส่วนของเวกเตอร์ออกมา เช่น
vec4(1, 2, 3, 4).xy
gl_FragColor: เอาต์พุตที่กำหนดสีของแต่ละพิกเซลเมื่อจบ main()
สร้างวงกลมด้วย distance(), step(), smoothstep()
- แม้จะวาดรูปทรงคมชัดอย่างวงกลม ก็ไม่ได้ใช้ฟังก์ชันอย่าง
drawCircle() แต่ใช้ ระยะทางเชิงคณิตศาสตร์
- ระยะระหว่างพิกเซลปัจจุบันกับจุดศูนย์กลางของวงกลมคำนวณได้ด้วย
distance(vec2 p1, vec2 p2)
- หากแมประยะทางเป็นสีโดยตรงจะได้ gradient แบบวงกลม ส่วนวงกลมทึบสร้างด้วย
step(float threshold, float value)
- หากระยะเกินค่า threshold จะได้ 1
- มิฉะนั้นจะได้ 0
step() มีการเปลี่ยนค่าที่ฉับพลัน จึงอาจเกิด aliasing ที่ขอบวงกลม
- ขอบที่นุ่มขึ้นสร้างได้ด้วย
smoothstep(float t_start, float t_end, float x)
แสดงรูปทรงด้วย Signed Distance Function
- Signed Distance Function (SDF) แสดงว่าจุดหนึ่งในพื้นที่อยู่ห่างจากรูปทรงเท่าใดในรูปของระยะทางที่มีเครื่องหมาย
- ภายในรูปทรงเป็นค่าลบ
- ภายนอกเป็นค่าบวก
- ขอบเขตเป็น 0
- SDF ของวงกลมสร้างได้โดยนำรัศมีไปลบออกจากระยะทางเมื่ออิงจากจุดศูนย์กลาง
float circleSDF(vec2 p, float r) {
return length(p) - r;
}
- หากย้ายจุดโดยอิงจากตำแหน่งพิกเซล
uv ก็จะคำนวณระยะทางไปยังวงกลมที่อยู่ในตำแหน่งใดก็ได้ในพื้นที่ UV
- หาก
d < 0.0 หมายความว่าพิกเซลอยู่ภายในวงกลม จึงสามารถระบายเป็นสีอื่นได้
- รูปทรง SDF แบบ 2D ที่หลากหลายกว่านี้ดูได้จาก comprehensive list ของ Inigo Quilez
รวม SDF หลายตัวเพื่อสร้าง blob
- SDF ทำให้สร้างรูปทรงใหม่ด้วย การดำเนินการแบบ Boolean ได้ง่าย
- ยูเนียนของ SDF สองตัวทำได้โดยนำระยะทางทั้งสองไปหา
min()
- หากอยู่ภายในรูปทรงใดรูปทรงหนึ่ง ระยะทางต่ำสุดจะเป็นค่าลบ
- หากอยู่นอกทั้งสองรูปทรง ระยะทางต่ำสุดจะเป็นค่าบวก
- เหตุผลที่ใช้
1. - smoothstep() คือ step() และ smoothstep() จะส่งออก 1 เมื่อระยะทางมากกว่า threshold หรือก็คืออยู่นอกรูปทรง
min() แบบธรรมดาจะสร้างความไม่ต่อเนื่องที่คมบริเวณจุดที่วงกลมสองวงมาบรรจบกัน
- หากต้องการผสมให้ลื่นไหลเหมือน blob ให้ใช้ smooth minimum
- อาร์กิวเมนต์เพิ่มเติม
k ควบคุมระดับความนุ่ม
- ตัวอย่างใช้ฟังก์ชัน polynomial smooth min
float smin(float a, float b, float k)
{
float h = max( k-abs(a-b), 0.0 )/k;
return min( a, b ) - h*h*k*(1.0/4.0);
}
สร้างแอนิเมชัน metaball ด้วยเวลาแบบ uniform
- สามารถส่งค่าตามใจ เช่น ค่าจาก slider เข้าไปใน Shader เป็น uniform ได้
- แอนิเมชันทำโดยนำ
u_time ที่สร้างจาก JavaScript ส่งเป็นอินพุตให้ Shader แล้วใช้ค่านี้คำนวณพิกัดจุดศูนย์กลางของวงกลม
- โดยพื้นฐานแล้ว Shader จะอัปเดตด้วยค่า
u_time ใหม่ 60 ครั้งต่อวินาที ทำให้เกิดการเคลื่อนไหวที่ลื่นไหล
- จุดศูนย์กลางของวงกลมสามารถทำให้แกว่งได้ด้วยฟังก์ชันคาบเวลา เช่น
sin, cos
- เมื่อต้องการรวมวงกลมหลายวงเป็น metaball ให้เก็บพิกัดจุดศูนย์กลางไว้ในอาร์เรย์ แล้วสะสม SDF ด้วยลูป
vec2 centers[4] = vec2[4](c1,c2,c3,c4);
float d = 99.;
for (int i = 0; i < 4; i++) {
vec2 c = centers[i];
float sdf = circleSDF(uv, c, .1*u_slider);
d = smin(d, sdf, K);
}
- blob ที่ได้ทำงานได้ตามหน้าที่ แต่ยังเป็นสีเดียว จึงเพิ่มสีและการโต้ตอบในขั้นถัดไป
อินเทอร์แอคชันขั้นสุดท้ายด้วยเมาส์
- ขั้นสุดท้ายคือรับพิกัดเมาส์เป็น uniform
u_mouse เพื่อให้ผู้ใช้ควบคุมลูกบอลหนึ่งลูกใน blob ได้โดยตรง
uniform vec2 u_mouse;
- เมื่อเพิ่มพิกัดเมาส์เข้าไปในอาร์เรย์จุดศูนย์กลาง ก็เพิ่ม การโต้ตอบ ได้ในบรรทัดเดียว
vec2 centers[5] = vec2[5](c1,c2,c3,c4,u_mouse);
- Shader สุดท้ายจะกลับแกน y ของเมาส์ให้ตรงกับพิกัดของ canvas แล้วรวมจุดศูนย์กลางที่เคลื่อนที่ 4 จุดกับจุดศูนย์กลางจากเมาส์เข้าด้วยกัน
- สีถูกผสมด้วยการใช้
mix(colorA, colorB, percent) หลายครั้ง
- หากใช้
percent ในลักษณะคล้าย boolean จะทำงานคล้าย if/else
- ค่า
metaball, ระยะห่างจากจุดศูนย์กลาง, shine และการคำนวณ membrane จะถูกสะท้อนในสีสุดท้าย
- เมื่อเข้าใจหลักการเดียวกันนี้ ก็จะเข้าใจการทำงานภายในของตัวแก้ไข Shader แบบ node-based เช่น Blender’s shader nodes หรือ Unity’s Shader Graph ได้ดียิ่งขึ้น
แหล่งข้อมูลเพิ่มเติมที่น่าดู
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ในที่สุดก็รวบรวมความกล้ามาเขียนอะไรลงอินเทอร์เน็ตและเปิดเผยตัวตนของตัวเองได้เสียที อยากเรียนรู้เรื่อง shader มานานแล้ว และคิดว่าถ้าได้บันทึกกระบวนการเรียนรู้แล้วแชร์ให้คนอื่นก็น่าจะดี
การเขียนบล็อกอย่างจริงใจนั้นกำลังค่อย ๆ ตายไปอย่างมองไม่เห็นภายใต้เงาของอัลกอริทึมผู้ทรงอำนาจ และรู้สึกเหมือนอินเทอร์เน็ตเดินทางมาถึงกลางเรื่องของ Star Wars Episode IV แล้ว ข้อเสนอแนะมีอย่างเดียว: ขอบคุณ และอยากให้อ่านเขียนอีกเยอะ ๆ
ถ้าอยากเห็นว่าระดับเซียนทำอะไรกับ shader ได้บ้าง ขอแนะนำ Inigo Quilez และงานศิลปะ shader ของเขา: https://www.youtube.com/watch?v=BFld4EBO2RE
เพิ่มเติม: เพิ่งเห็นว่าเป็นผู้เขียนเอง บทความทำออกมาได้ดีมาก และกำลังมองหาทutorialที่ทำให้ศิลปะการเขียน shader เข้าถึงง่ายและอินเทอร์แอ็กทีฟมากขึ้นอยู่พอดี
smoothstep(0.0f, 0.01f, dist);ก็ใช้smoothstep(fwidth(dist), -fwidth(dist), dist);ได้เลยบทความค่อนข้างดี แต่พูดผ่านปัญหาหลักของ shader เบาเกินไป
Shader เป็นตัวปวดหัวที่โปรแกรมและแอปส่วนใหญ่ไม่ได้ต้องการ สำหรับงาน 3D นั้นสามเหลี่ยมเหมาะมาก และ GPU ก็เข้ากับ abstraction แบบนั้นได้ดี ส่วน shader ก็มีประโยชน์ในการทำ interpolation บนสามเหลี่ยมเหล่านั้น
แต่สำหรับแทบทุกอย่างที่ไม่ใช่ 3D สามเหลี่ยมนั้นไม่ค่อยเวิร์ก การเรนเดอร์ 2D ต้องการ path, การเรนเดอร์ฟอนต์ต้องการ path หรือ pixmap, GUI ก็เข้ากับ path และ pixmap ได้ดีกว่ามาก, compositor ต้องการ pixmap, ส่วน video decoder ต้องการ pixmap และการเรนเดอร์แบบขนาน
สิ่งที่ฝั่งที่ไม่ใช่ 3D ต้องการคือ pixmap ทรงสี่เหลี่ยมผืนผ้าและการเข้าถึงเชิงคำนวณกับ pixmap นั้นโดยตรง ซึ่ง GPU ไม่ได้ชอบสิ่งนี้นัก และ shader ก็ไม่ได้เข้ากันได้ดีกับงานลักษณะนี้
เห็นด้วยว่ามันไม่ใช่รูปแบบที่ต้องการในระดับสูง แต่ก็ยังดีพอในฐานะฐานสำหรับสร้าง abstraction ระดับสูงขึ้น จะให้ฮาร์ดแวร์รองรับโดยตรงก็ได้ แต่ก็ไม่ได้เห็นข้อดีชัดเจนนัก ไม่มีใครบ่นว่า CPU ไม่รองรับ
forloop ในเชิงสถาปัตยกรรมโดยตรงพอกลับมาอ่านอีกที ก็ยังไม่ค่อยเข้าใจว่ากำลังชี้ถึงปัญหาอะไรของ GPU กันแน่ จะมองข้าม vertex processing pipeline ไปเลยแล้ววาด full-screen quad อันเดียว หรือใช้ compute shader ก็ได้ ซึ่ง GPU จัดการสิ่งนี้ได้ดีมาก และบทความที่ลิงก์มาก็พูดถึงแนวนี้อยู่แล้ว
อย่างน้อยในยุค 2000s ก็คิดกันแบบนั้น พอซอฟต์แวร์ซับซ้อนขึ้น GPU อาจกลายเป็น optimization ที่ต้องการได้ แต่ GPU pipeline นั้นเข้มงวดและปิดมาโดยตลอด และการเปลี่ยนผ่านจากการเขียนโปรแกรม single-core ไปสู่ multi-core ก็มักต้องใช้อัลกอริทึมคนละแบบ
การเขียนโปรแกรม GPU แบบทั่วไปช่วยให้ไม่ต้องผูกติดกับสามเหลี่ยม แต่ถ้าจะใช้ประโยชน์จากความขนานนั้น ก็ยังต้องใช้แนวทางที่ต่างออกไปอย่างสิ้นเชิงอยู่ดี
เจ๋งมาก! ช่วงนี้กำลังไถลลงหลุมกระต่ายของ SDF อยู่พอดี ดีใจที่มีลิงก์ไปเว็บของ iq เพราะของฝั่งนั้นดีมากจริง ๆ
อดไม่ได้ที่จะลิงก์ shader “happy bouncing” ของเขา ส่วนตัวมองว่าน่าทึ่งมาก: https://www.shadertoy.com/view/3lsSzf
ยังมีวิดีโอ YouTube ยาว 6 ชั่วโมงที่บันทึกกระบวนการสร้างไว้ด้วย โค้ดยาวราว 500 บรรทัดแต่แน่นมาก
พยายามสนใจหัวข้อนี้มาหลายครั้งแต่หา入口ที่เข้าถึงง่ายไม่เจอ จนมาเจอในบทความแนะนำชิ้นนี้ ชอบมากที่ทำออกมาอย่างสนุกและขี้เล่น และอยากอ่านบทความถัดไปเร็ว ๆ
จับผิดเล็กน้อย แต่ตอนพูดถึง cel shading เขียนเป็น
cellคำนี้มาจาก cel ที่ใช้ในแอนิเมชันวาดมือ และจากโทนการแรเงาแบบแบ่งระดับของมันดีมากจริง ๆ ในฐานะคนที่เคยเป็นศิลปินแล้วมาเป็นโปรแกรมเมอร์ บางทีก็มีแรงกระตุ้นให้อยากลองเจาะลึกกราฟิกส์โปรแกรมมิง
เคยเขียน shader พื้นฐานมาก ๆ มาบ้าง แต่พอเริ่มมีคณิตศาสตร์เข้ามา ซึ่งจริง ๆ ก็ตั้งแต่ค่อนข้างต้นแล้ว ก็ชนกำแพงทันที เรียนมาทางศิลปะไม่ใช่วิทยาการคอมพิวเตอร์ เลยแทบไม่มีพื้นฐานคณิตศาสตร์เลย
ยังไงก็เขียนได้ดีมากและชอบบทความนี้
เป็นบทความเริ่มต้นที่ยอดเยี่ยม และหวังว่าจะมีต่อเรื่อย ๆ บทความแบบนี้เริ่มต้นได้เท่มาก แต่ก็มักถูกปล่อยค้างบ่อยเกินไป
ไม่เคยทำงานกับเชดเดอร์มาก่อน เลยอาจเป็นคำถามพื้นฐานมาก ๆ ว่า ในหนึ่งเฟรมของเกมนั้น โดยแก่นแล้ว เชดเดอร์เป็นคนวาดทุกอย่าง ใช่ไหม?
หรือว่ามีรูปทรงพื้นฐานอย่างสามเหลี่ยม สี่เหลี่ยม วงกลมอยู่แล้ว แล้วเชดเดอร์ค่อยเอาสิ่งอย่างเงาหรือการเกลี่ยขอบไปวางทับด้านบน?
จากตัวอย่างดูเหมือนว่าสามารถสร้างเชดเดอร์ที่วาดวัตถุใด ๆ ในฉากได้ แล้วค่อยนำเชดเดอร์อื่นมาประกอบกันเพื่อให้ได้เงาและแสง แต่จากประสบการณ์ที่จำกัดมาก ๆ ที่ฉันเคยวาดมา ฉันวาดรูปทรง ไม่ได้วาดด้วยเชดเดอร์ เลยคิดมาตลอดว่าเชดเดอร์ไม่ได้เป็นตัววาดวัตถุเอง
GPU จะนำรูปทรงเวกเตอร์เชิงนามธรรมอย่างสามเหลี่ยม ซึ่งนิยามด้วยจุดยอดสามจุดและข้อมูลที่ผูกกับแต่ละจุดยอด เช่น normal มาแปลงเป็นสตรีมของ fragment โดยหนึ่ง fragment ต่อหนึ่งพิกเซลที่รูปทรงนั้นครอบคลุมใน output buffer หรือมากกว่านั้นถ้าเป็นการทำ multisampling ขั้นตอนนี้ทั้งหมดทำโดยฮาร์ดแวร์
fragment ประกอบด้วยพิกัดพิกเซลและข้อมูลที่ผู้ใช้ส่งมา ข้อมูลนั้นอาจเป็น uniform ที่มีค่าคงที่ หรือเป็น varying ซึ่งคือข้อมูลจุดยอดที่กล่าวไปก่อนหน้าและถูก interpolate ครอบคลุมทั้งหน้าของสามเหลี่ยม การ interpolate นี้ก็ทำโดยฮาร์ดแวร์และไม่สามารถเขียนโปรแกรมกำหนดเองได้
fragment shader รับ fragment เป็นอินพุต แล้วคำนวณสีตามข้อมูลนั้น ก่อนจะผ่านอีกไม่กี่ขั้นตอนและถูกเขียนออกเป็นสีของพิกเซลนั้นบนหน้าจอหรือใน offscreen buffer อาจเป็นสีทึบธรรมดาหรือการคำนวณแสงที่ซับซ้อนก็ได้
ในการเรนเดอร์ด้วย GPU ทุกอย่างนี้เกิดขึ้นแบบขนานในวงกว้าง ทำให้มี fragment จำนวนมากถูกประมวลผลพร้อมกัน เชดเดอร์เป็นฟังก์ชันบริสุทธิ์ที่ไม่มีสถานะ จึงเข้าถึงได้เฉพาะอินพุต และสิ่งที่ทำได้ก็มีเพียงคืนค่าบางอย่าง เช่น สีและค่า depth
สรุปคือ ฮาร์ดแวร์ของ GPU จะคำนวณว่าต้องเติมพิกเซลใดบ้างเพื่อวาดแต่ละสามเหลี่ยม ส่วน fragment shader จะเป็นตัวตัดสินค่าสีของพิกเซลแต่ละพิกเซล
ก่อนที่ fragment shader จะวาด “ทับบน” ฉาก ยังมีขั้นอื่น ๆ อย่าง vertex และ tessellation ที่ใช้วาดรูปทรงพื้นฐานอยู่แล้ว
แม้แต่เรื่อง fragment shader เองก็ยังมีรายละเอียดมากกว่าที่อธิบายไปมาก เช่น deferred rendering[2] ซึ่งแค่เริ่มอธิบายก็เป็นหัวข้อใหญ่แล้ว
1: https://vulkan-tutorial.com/Drawing_a_triangle/Graphics_pipe...
2: https://learnopengl.com/Advanced-Lighting/Deferred-Shading
มี graphics pipeline ที่ผสมทั้งขั้นฮาร์ดแวร์แบบ fixed-function และขั้นที่เขียนโปรแกรมได้ ในระดับสูงคือ 1) GPU รับชุดสามเหลี่ยม 3D จาก CPU 2) vertex shader แปลงจุดยอดของสามเหลี่ยม 3D ให้เป็นจุดยอดของสามเหลี่ยม 2D ที่มีพิกัดพิกเซล ทำให้มันแบนลง 3) GPU ทำ rasterize สามเหลี่ยม 2D เพื่อกำหนดอย่างแม่นยำว่าพิกเซลใดถูกสามเหลี่ยมครอบคลุม 4) สำหรับทุกพิกเซลที่ถูกครอบคลุม pixel shader จะรันเพื่อกำหนดสีพิกเซล และ 5) สีพิกเซลที่ได้จะถูกเก็บลงใน frame buffer ซึ่งอาจถูก blend กับสีเดิมด้วย
pipeline นี้จะถูกทำซ้ำหลายครั้งด้วย mesh สามเหลี่ยมและเชดเดอร์ที่ต่างกัน จนกว่าจะวาดครบทั้งเฟรม
งานอย่างสี เงา shading เอฟเฟกต์ภาพ และการประมวลผลภาพทั่วไป ล้วนเป็นการคำนวณแบบขนานที่ผสานอาร์เรย์ข้อมูลหลายชุดเข้าด้วยกัน ซึ่งรวมถึงจุดยอดและแอตทริบิวต์ของมัน texture ต้นฉบับ ฟังก์ชันที่คำนวณไว้ล่วงหน้า texture ปลายทาง buffer เป็นต้น
ตัวอย่างเช่น ถ้าต้องการให้ได้แสงและเงา เชดเดอร์ต้องเข้าถึงสิ่งอย่างตำแหน่งและทิศทางของ spotlight ได้ ซึ่งน่าจะอยู่ในตัวแปร global แสงแบบ composite มักได้มาจากการรวม shader pass หลายรอบ เช่นมีหนึ่ง pass พื้นฐานสำหรับ global illumination และอีกหนึ่ง pass ต่อแสงแต่ละดวง โดยแต่ละ pass จะทำการเพิ่มแสงเข้าไปตามตัวอักษร
ทีนี้เพื่อไม่เพิ่มแสงให้กับพิกเซลที่แหล่งกำเนิดแสงถูกบังอยู่ หรือก็คือเงา เทคนิคที่ใช้บ่อยที่สุดคือสิ่งที่เรียกว่า Z-buffer ซึ่งโดยพื้นฐานแล้วคือ texture แบบ floating-point เนื่องจากเราต้องการรู้ว่าแสงแต่ละดวงไปได้ไกลแค่ไหน ก่อนจะใช้แสงทั้งหมด เราจะตั้งค่า shader pass เดียวที่รวมเรขาคณิตแข็งทั้งหมดของฉาก ใช้ตำแหน่งและทิศทางของแสงเหมือนกับการแปลงกล้อง และใช้เชดเดอร์พิเศษที่ทำแค่เขียนระยะทางไปยังวัตถุลงใน Z-buffer
หลังจากนั้น เมื่อใดก็ตามที่ต้องการรู้ว่าแสงไปถึงจุดหนึ่งในอวกาศหรือไม่ ก็ทำการคำนวณเรขาคณิตเล็กน้อยแล้ว sample Z-buffer นี้ จากนั้นเปรียบเทียบค่าที่เก็บไว้ในทิศทางนั้นกับระยะของจุดนั้น กระบวนการนี้อาจมีบั๊กได้มากและความคลาดเคลื่อนด้านความแม่นยำก็พบได้บ่อย เอนจินที่ดีจะทำสิ่งนี้ให้คุณอยู่แล้ว แต่ก็เปิดให้ปรับแก้กระบวนการได้
ที่เหลือทั้งหมดก็เป็นรูปแบบดัดแปลงของหัวข้อนี้ deferred rendering คือการเรนเดอร์ข้อมูลลงใน texture กลางแทนที่จะเป็นสี แล้วค่อยประมวลผลภายหลังเพื่อให้ได้สี เอฟเฟกต์ blur คือการทำ 2D convolution กับ render texture เช่นด้วย Gaussian kernel ส่วน tessellation shader เกี่ยวข้องกับการสร้างเรขาคณิตใหม่จาก vertex shader แม้แต่การวาดข้อความก็ยังทำผ่าน font atlas และสี่เหลี่ยมผืนผ้าขนาดเล็ก
ดังนั้นศิลปินที่มีฝีมือจึงผลักขีดจำกัดภายใน fragment shader และต่อสู้กับข้อแลกเปลี่ยนด้านประสิทธิภาพ
fragment shader มักถูกใช้กับเอฟเฟกต์ฟิลเตอร์ทั้งหน้าจอบ่อยกว่า เช่นการปรับสี
เชดเดอร์ยังถูกใช้ในการสร้าง texture และ material ของวัตถุพื้นฐานด้วย ศิลปินสาย material มักสร้าง texture ด้วยคณิตศาสตร์ของเชดเดอร์
เอฟเฟกต์ภาพจำนวนมากถูกสร้างขึ้นจากการใช้เชดเดอร์อย่างสร้างสรรค์
เชดเดอร์ทำงานบน GPU ในลักษณะขนานคล้ายคลื่น โดยมีเธรดจำนวนมากมหาศาลทำงานกับข้อมูลเดียวกันภายในหนึ่ง wave
ในบางกรณี เชดเดอร์เร็วกว่าโค้ดแบบมีการแตกแขนงบน CPU มาก และเชดเดอร์ยังเข้าถึงข้อมูลเรนเดอร์บางอย่างได้ง่ายกว่าด้วย
ดังนั้นนี่จึงเป็นพื้นที่ที่เหมาะสำหรับการสร้างเอฟเฟ็กต์พิเศษเชิงสร้างสรรค์ ในเกม วัตถุที่มีรายละเอียดพื้นผิวสูงมักเป็นเป้าหมายทั่วไปในการย้ายรายละเอียดนั้นไปไว้ในเชดเดอร์ เช่น พื้นผิวทะเล เมชเทสเซลเลชัน เป็นต้น และเนื่องจาก GPU มีทั้งพลังและความยืดหยุ่นสูง จึงมีการใช้งานอื่น ๆ อีกมาก
ถ้ามีใครเห็นภาพดูเหมือนนอยส์กระพริบ ๆ ผมแก้ด้วยการคัดลอกภาพจากเบราว์เซอร์แล้วไปวางที่อื่น แบบนั้นจะเห็นภาพได้ถูกต้อง
นี่คือลิงก์ Imgur ภาพแรกเป็นสกรีนช็อตของสิ่งที่เห็นในเบราว์เซอร์ และภาพที่เหลือคือภาพจริงหลังจากนำไปวางใน Imgur
https://imgur.com/a/F4203rz
ความเข้าใจของผมเกี่ยวกับเชดเดอร์เป็นแบบนี้
เวลาวาดเส้นบน CPU มันคือฟังก์ชันที่ไล่ดูแต่ละพิกเซลระหว่างจุด A กับจุด B แล้ววาดทีละพิกเซลตามลำดับ
มันมีจำนวนขั้นตอนแน่นอนเท่ากับจำนวนพิกเซลบนเส้น และรันครั้งเดียว
เวลาวาดเส้นบน GPU มันคือฟังก์ชันที่ตรวจว่าพิกเซลนั้นอยู่บนเส้นหรือไม่ แล้วถ้าใช่ก็วาด
มันรันพร้อมกันบนทุกพิกเซลของหน้าจอ รวมถึงพิกเซลที่อยู่ห่างจากเส้นมาก ๆ ด้วย
แบบนี้ถูกไหม?
อย่างที่สอง GPU ก็ไม่จำเป็นต้องรัน pixel shader ทั้งหน้าจอ สามารถรัน shader เฉพาะกับรูปทรงตามต้องการที่สร้างจากสามเหลี่ยมได้ ดังนั้นวิธีวาดเส้นอย่างมีประสิทธิภาพคือส่งสามเหลี่ยม 2 อันที่ตรงกับรูปทรงเรขาคณิตของเส้นที่ต้องการไปให้ GPU แล้วให้รัน pixel shader เฉพาะกับพิกเซลที่ทับกับสามเหลี่ยมนั้น แบบนี้มีประสิทธิภาพกว่ามาก