- สำหรับดวงจันทร์และดาวแก๊สที่ใช้เป็นฉากหลังท้องฟ้า สามารถใช้เพียงดิสก์วงกลมและ pixel shader เพื่อจัดการพื้นผิว การหมุน และบรรยากาศได้ โดยไม่ต้องมี mesh ทรงกลมจริง
- UV sphere แบบเดิมทำให้แมปเท็กซ์เจอร์ 2×1 ได้ง่าย แต่ก็มาพร้อม ขอบนอกเป็นเหลี่ยม, การสูญเสียจากการสุ่มตัวอย่าง mipmap, แพตเทิร์นซ้ำ, seam, การบิดเบี้ยวที่ขั้ว และข้อจำกัดของ halo บรรยากาศ
- วิธีแบบดิสก์จะกู้คืนตำแหน่งพื้นผิวทรงกลมจาก UV ที่ศูนย์กลาง แล้วใช้ UV ทรงกลม อิง
asin, เมทริกซ์หมุน และ spin ด้วยการเลื่อนพิกัด u เพื่อสร้างพื้นผิวที่ดูเหมือนทรงกลมในระดับพิกเซล
- การทำ shading เริ่มจากแสงแบบ Lambertian ที่ใช้ตำแหน่งพื้นผิวเป็น normal และสำหรับพื้นผิวหินจะใช้ เมทริกซ์ TBN กับ normal map ส่วนบรรยากาศจะประมาณค่าความสว่างของ halo และการดูดกลืนแยกตามช่องสี
- สามารถจัดการเท็กซ์เจอร์สแตติกจาก Substance Designer และเท็กซ์เจอร์เรนเดอร์แบบไดนามิกของ Unreal ด้วยวิธีเดียวกัน จึงลดภาระด้านประสิทธิภาพได้ด้วยเท็กซ์เจอร์ไดนามิกที่เล็กลงและการปรับ tiling
เป้าหมายด้านภาพของดาวเคราะห์ในสกายบ็อกซ์
- สกายบ็อกซ์ของโปรเจกต์มุ่งสร้างองค์ประกอบแบบท้องฟ้าต่างดาวด้วย ดวงจันทร์แบบแอนิเมชัน และ ดาวแก๊ส
- ดาวเคราะห์ทั้งสองหมุน และดาวแก๊สยังมี กระแสบรรยากาศ ที่เคลื่อนไหวด้วย
- การเคลื่อนไหวจริงค่อนข้างละเอียดอ่อน แต่เร่งความเร็วขึ้นเพื่อเอฟเฟกต์ทางภาพ
- แนวทางศิลป์เป็นสไตล์ กึ่งสมจริง (semi-realistic) ที่ได้แรงบันดาลใจจากงานศิลป์ SF ยุค 1970–80
อินพุตเท็กซ์เจอร์พื้นผิวและข้อจำกัด
- พื้นผิวของดาวแก๊สถูก สร้างแบบเรียลไทม์ ด้วย pixel shader และวิธี render-to-texture
- เท็กซ์เจอร์ดวงจันทร์ทำใน Substance Designer และสามารถทดลองลุคหลากหลายได้โดยเปลี่ยนพาเล็ตสีและ material seed
- ดาวแก๊สแบบไดนามิกต้องเรนเดอร์เท็กซ์เจอร์ทุกเฟรม ทำให้มีต้นทุนด้านประสิทธิภาพสูง
- หากเพิ่มความละเอียดเท็กซ์เจอร์เป็น 2 เท่า ต้นทุนการเรนเดอร์จะเพิ่มเป็น 4 เท่า
- ต้องการโซลูชันที่ทำงานได้สม่ำเสมอทั้งกับเท็กซ์เจอร์สแตติกจาก Substance Designer และเท็กซ์เจอร์ที่เรนเดอร์แบบไดนามิกใน Unreal
- ตอนแรกพิจารณา cubemap แต่เมื่อเทียบกับเท็กซ์เจอร์สี่เหลี่ยมจัตุรัสแล้วอาจใช้ ทรัพยากรมากกว่า 6 เท่า และ Substance Designer ไม่รองรับการสร้าง cubemap จึงไม่เหมาะในทางปฏิบัติ
ข้อจำกัดที่พบใน UV sphere
- UV sphere ดูเหมือนเป็นตัวเลือกพื้นฐาน เพราะจับคู่กับเท็กซ์เจอร์สี่เหลี่ยมผืนผ้า 2×1 และพิกัดได้ง่าย
- เนื่องจากมองเห็นเพียงด้านหนึ่งของทรงกลมในแต่ละครั้ง จึงอาจใช้ เท็กซ์เจอร์สี่เหลี่ยมจัตุรัสแบบ tile แทนเท็กซ์เจอร์สี่เหลี่ยมผืนผ้าที่ไม่ซ้ำทั้งผืนได้
- เมื่อนำไปใช้จริง ปัญหาหลายอย่างซ้อนทับกัน
- ขอบนอกดูเป็นเหลี่ยมอย่างเห็นได้ชัด ต้องเพิ่ม subdivision หรือใช้ mask ใน pixel shader เพื่อบัง
- พื้นผิวส่วนใหญ่ของทรงกลมถูกมองแบบเฉียง จึงถูกสุ่มตัวอย่างจาก mipmap ระดับต่ำ และสำหรับเท็กซ์เจอร์ไดนามิก พิกเซลจำนวนมากที่สร้างทุกเฟรมถูกใช้กับบริเวณที่แทบมองไม่เห็น
- การ tiling ต้องเพิ่มเป็นจำนวนเต็มเท่าเพื่อให้แพตเทิร์นทั้งหมดต่อเนื่องกัน และเพียงเพิ่มเป็น 2 เท่าก็เห็นความซ้ำชัดเจน
- fractional tiling สร้าง seam ที่ลากจากขั้วเหนือถึงขั้วใต้
- ที่ขั้วจะเกิด texture pinching และการบิดเบี้ยวของการแมปสามเหลี่ยม ซึ่งดูคล้ายความบิดเบี้ยวแบบเท็กซ์เจอร์สั่นของ PSX
- วาดได้เฉพาะพื้นผิว จึงต้องมีโมเดลแยกสำหรับ atmospheric halo
- UV sphere ยังใช้ได้สำหรับการโมเดลดาวเคราะห์ แต่ในงานสกายบ็อกซ์ต้องมีการแก้ไขและ hack จำนวนมาก
แนวทางดิสก์วงกลมและ pixel shader
- ดาวเคราะห์ในสกายบ็อกซ์ถูกสังเกตจากตำแหน่งหนึ่งในระยะไกล จึงไม่จำเป็นต้องใช้ mesh ทรงกลม 3D ที่ซับซ้อน
- ใช้ ดิสก์โพลีกอนวงกลมที่เติมเต็ม แบบเรียบง่าย แล้วจัดการการแมปเท็กซ์เจอร์ใน pixel shader
- สามารถวาด atmospheric halo ได้ภายใน mesh เดียวกัน
- จุดกำเนิด UV ของดิสก์ต้องอยู่ตรงศูนย์กลางพอดี
- หากถือว่ารัศมีดาวเคราะห์เป็น 1 พิกัด UV ต้องขยายออกไปเกินค่านั้นเพื่อให้มีพื้นที่วาดบรรยากาศ
การกู้คืนพื้นผิวทรงกลม
- ระบบพิกัดใช้ระบบ left-handed, Y-up ที่สอดคล้องกับ DirectX
- Unreal Engine ก็เป็น left-handed แต่ทิศขึ้นเป็น Z จึงต้องตรวจสอบทิศทางและฟอร์แมต normal map
- สมการพื้นผิวแมปตำแหน่ง 2D บนระนาบดิสก์ไปเป็นตำแหน่ง 3D บนพื้นผิวทรงกลม
- ก่อนอื่นตรวจสอบว่าพิกเซลอยู่ภายในพื้นผิวทรงกลมหรือไม่ โดยดูว่าความยาวเวกเตอร์ UV น้อยกว่ารัศมีหรือไม่
float CircleMask( float2 uv, float radius)
{
return length(uv) < radius? 1.0: 0.0;
}
- วิธีนี้ทำงานถูกต้องก็ต่อเมื่อพิกัด UV จัดวางให้ตรงกับศูนย์กลางของ mesh
- local position ของพื้นผิวทรงกลมใช้ x, y ของ UV ตามเดิม แล้วกู้คืนเฉพาะ z
float3 ReconstructSurface(float2 uv)
{
float zSquared = 1.0 - dot(uv, uv);
float z = sqrt(zSquared);
return float3(uv, z);
}
การสร้าง UV ทรงกลม
- กระบวนการแมปเท็กซ์เจอร์สี่เหลี่ยมจัตุรัสลงบนพื้นผิวทรงกลมแบ่งเป็นสามขั้นตอน
- พันรอบทรงกระบอก
- ทำกระบวนการเดียวกันซ้ำบนแกน y
- แปลงผลลัพธ์ให้เป็นรูปวงกลม
- พิกัดเท็กซ์เจอร์ x แปรผันตามมุมที่พันรอบทรงกระบอก โดยคำนวณ
arcsine(x) แล้วแมป [-π, π] กลับเป็น [0, 1]
- ใช้ generatrix ซึ่งเป็นความกว้างเฉพาะที่ของทรงกลม เพื่อทำให้พิกัด pinching ที่ขั้ว
float2 GenerateSphericalUV(float3 position)
{
float width = sqrt(1.0 - position.y * position.y);
float generatrixX = position.x / width * sign(position.z);
float2 generatrix = float2(generatrixX, position.y);
float2 uv = asin(generatrix) / 3.14159 + float2(0.5, 0.5);
return float2(uv);
}
ความเอียงของแกนและการหมุน
- เพื่อจัดองค์ประกอบดาวเคราะห์ให้ดูเป็นธรรมชาติมากขึ้น ให้เอียง แกนดาวเคราะห์ ด้วย pitch และ roll
- ดาวเคราะห์ในงานศิลป์ SF มักเอียง ทำให้องค์ประกอบดูมีพลวัตและแสดง ice cap บริเวณขั้วได้
- yaw เทียบเท่าการเคลื่อนที่หมุน และจัดการในขั้นตอนแยกต่างหากเพื่อหลีกเลี่ยงปัญหา seam
- สำหรับทรงกลมแบบ mesh โดยทั่วไปจะแปลงดาวเคราะห์ด้วยเมทริกซ์ แต่ในวิธีแบบดิสก์ แต่ละพิกเซลเป็นตัวกำหนดตำแหน่งพื้นผิว จึงใช้เมทริกซ์ 3×3 แบบง่ายที่มีเฉพาะการหมุนได้
float3x3 CreateRotationMatrix(float pitch, float roll) {
float cosPitch = cos(pitch);
float sinPitch = sin(pitch);
float cosRoll = cos(roll);
float sinRoll = sin(roll);
return (float3x3)(
cosRoll, -sinRoll * cosPitch, sinRoll * sinPitch,
sinRoll, cosRoll * cosPitch, -cosRoll * sinPitch,
0.0, sinPitch, cosPitch
);
}
- material editor ของ Unreal ไม่รองรับเมทริกซ์เป็นชนิดข้อมูล แต่มีวิธีเลี่ยงได้
การจัดการ scale, seam และ spin
- เมื่อเปลี่ยน scale ของเท็กซ์เจอร์ UV seam ที่เคยซ่อนอยู่ตรงขอบเท็กซ์เจอร์เดิมจะปรากฏขึ้น
- seam ไม่สามารถลบได้อย่างง่ายดายทั้งหมด แต่ถ้าย้ายไปด้านหลังจะมองเห็นน้อยลง
- แบ่งทรงกลมเป็นสี่ quadrant ตามเครื่องหมายของ position แล้วใช้ offset ทิศ u กับ quadrant ด้านหลังซ้ายและด้านหลังขวา
float2 GenerateSphericalUV(float3 position, float spin, float scale)
{
float leftRightSign = sign(position.x);
float frontBackSign = sign(position.z);
float width = sqrt(1.0 - position.y * position.y);
float generatrixX = position.x / width * frontBackSign;
float2 generatrix = float2 (generatrixX, position.y);
float2 uv = asin(generatrix) / 3.14159 + float2 (spin, 0.5);
if(frontBackSign < 0.0)
{
uv = float2 (uv.x + 1.0 * leftRightSign, uv.y);
}
return float2 (uv / scale);
}
- seam ยังคงเชื่อมขั้วเหนือกับขั้วใต้ แต่ถูกย้ายไปด้านหลังจึงมองเห็นน้อยลงมาก
- การหมุนจัดการด้วยการเลื่อนเท็กซ์เจอร์พื้นผิวไปตามทิศทางพิกัด u แทนที่จะหมุนตัวทรงกลมเอง
float2 GenerateSphericalUV(float3 position, float scale, float spin)
{
float width = sqrt(1.0 - position.y * position.y);
float generatrixX = position.x / width * sign(position.z);
float2 generatrix = float2(generatrixX, position.y);
float2 uv = asin(generatrix) / 3.14159 * scale + float2(0.5 + spin, 0.5);
return float2(uv);
}
float2 sphericalVU = GenerateSphericalUV(position, scale, time*speed)
การทำ shading พื้นผิวและ normal map
- หากต้องการให้ดาวเคราะห์ดูโค้งมน shading เป็นสิ่งสำคัญ
- ต่างจาก shading ของเอนจินทั่วไป ดาวเคราะห์มีสองเลเยอร์คือพื้นผิวและบรรยากาศ จึงต้องกำหนดแสงและ blending ของทั้งสองเลเยอร์ด้วยตนเอง
- เมื่อรัศมีเป็น 1 ตำแหน่งพื้นผิวก่อนหมุนสามารถใช้เป็น surface normal ได้เลย
- แสงใช้โมเดล Lambertian แบบง่าย
- เป้าหมายไม่ใช่ความสมจริงเต็มรูปแบบ แต่เป็นสุนทรียะหน้าปก SF แบบมีสไตล์ที่ได้แรงบันดาลใจจากภาพถ่ายของ NASA
float LambertianLight(float3 normal, float3 lightDirection) {
float NdotL = max(dot(normal, lightDirection), 0.0);
return NdotL;
}
- พื้นผิวเรียบแบบ gas giant อาจใช้ normal อย่างง่ายก็เพียงพอ
- ดาวเคราะห์หินเหมาะกับการจำลองภูมิประเทศอย่างเทือกเขา สันเขา และหลุมอุกกาบาตด้วย normal map
- หากใช้ normal map ต้องมี เมทริกซ์ TBN ที่ประกอบด้วย Tangent, Bitangent, Normal
- สำหรับทรงกลม ต้องคำนวณสิ่งนี้ในระดับพิกเซล
- เมทริกซ์ TBN ใช้แปลง normal จาก normal map ไปเป็น World Space Coordinates
ความไม่ต่อเนื่องของ UV และ artifact จาก mipmap
- UV ที่สร้างใน pixel shader อาจเกิดปัญหา ความไม่ต่อเนื่อง (discontinuity)
- back seam ควรมีองค์ประกอบ UV แนวนอนที่ต่อกันสนิทจาก 0.0 ถึง 1.0 ที่ขอบเขต แต่ในความเป็นจริงเกิด blocky artifact ตามแนว seam
- เส้นนี้สอดคล้องกับ DDX ของ UV
- DDX และ DDY วัดอัตราการเปลี่ยนแปลงของ UV บนแกนของพื้นที่หน้าจอ
- texture sampler ใช้ค่านี้เพื่อกำหนด mipmap ที่จะใช้
- UV derivative ต่ำสอดคล้องกับ mipmap ความละเอียดสูง ส่วน derivative สูงสอดคล้องกับ mipmap ความละเอียดต่ำ
- หากค่าซ้ายขวาของ seam กระโดดอย่างรุนแรง DDX จะสูงขึ้น และอาจสุ่มตัวอย่างจาก mipmap ต่ำสุดจนดูเหมือนเส้นสีเทา
- สำหรับดาวเคราะห์ จะมี polar patch บังไว้จึงไม่ใช่ปัญหาใหญ่ แต่หากมองเห็นชัด ต้องปรับแก้ DDX และ DDY ของ seam ด้วยตนเองแล้วส่งให้ sampler
- ตัวอย่าง fix มีไว้เพื่ออธิบาย ส่วนค่าจริงต้องอนุมานสมการเองให้สอดคล้องกับ mapping
polar patch และการแก้ pinching
- ความบิดเบี้ยวที่ขั้วถูกปิดทับด้วย polar patch ตามวิธีที่พิสูจน์แล้ว
- ในดาวเคราะห์หรือดวงจันทร์ พื้นที่ขั้วมักถูกปกคลุมด้วยน้ำแข็งและแตกต่างทางภาพจากบริเวณอื่น จึงใช้แพตช์เท็กซ์เจอร์แยกเพื่อซ่อนปัญหาและเพิ่มความน่าสนใจทางภาพได้
- polar patch สามารถเพิ่มได้โดยแมปลงบนระนาบที่ตั้งฉากกับแกนหมุน แล้วหมุนพิกัดผลลัพธ์
float2 PolarPatchMapping(float3 position, float scale, float spin)
{
float cosSpin = cos(spin);
float sinSpin = sin(spin);
float scale = 0.4;
float2 uv = float2(position.x, position.z) * scale;
float2 spinningUV = float2(uv.x * cosSpin - uv.y * sinSpin,
uv.x * sinSpin + uv.y * cosSpin);
return spinningUV;
}
- เพราะไม่ได้เก็บ UV map ไว้ที่ vertex แต่สร้างขึ้นต่อพิกเซล ความยืดหยุ่นในการแก้ pinching ที่ขั้วจึงสูงขึ้น
- สามารถแก้ aspect ratio ของเท็กซ์เจอร์บริเวณขั้วได้ด้วยการหารแบบง่าย
uv = float2(uv.x, uv.y/pow(width, 1/3));
- ผลการแก้อาจดูเหมือน artifact อีกแบบหนึ่ง แต่ในฉากสมจริงจะต่อเนื่องกับ polar patch ได้เนียนขึ้นและลดความไม่สอดคล้องทางภาพ
การประมาณ atmospheric halo
- เนื่องจากพื้นผิวดาวเคราะห์ไม่ได้กินพื้นที่ทั้งหมดของดิสก์ จึงสามารถวาด atmospheric halo ในพื้นที่ที่เหลือได้
- หากวาด halo ภายใน shader เดียวกัน จะผสานกับบรรยากาศเหนือดาวเคราะห์ได้เป็นธรรมชาติ
- การเรนเดอร์บรรยากาศแบบอิงฟิสิกส์มักใช้ Rayleigh scattering และ raymarching แต่ที่นี่ไม่ได้ใช้
- ใช้การประมาณคล้าย trick แสงวัตถุไดนามิกของ Quake 1·2
- static lighting ของ Quake ถูกคำนวณล่วงหน้าเป็น lightmap
- มอนสเตอร์แบบไดนามิกจะสุ่มตัวอย่าง lightmap ด้านล่างเพื่อปรับสี และแม้จะไม่ถูกต้องทางฟิสิกส์ ก็ทำให้เข้ากับสภาพแวดล้อมได้
- ใช้วิธีเดียวกันโดยอาศัยพื้นผิวดาวเคราะห์เพื่อประมาณแสงของ halo บรรยากาศ
- ขยาย surface normal ก่อน bump mapping เพื่อคำนวณความสว่างของ halo
- หาก remap ความสว่างแยกตามช่องสีต่างกัน ก็สามารถจำลองการดูดกลืนของความยาวคลื่นแสงที่ต่างกันอย่างง่ายได้
- เพราะบรรยากาศจะเห็นชัดกว่าจากมุมเฉียง จึง remap องค์ประกอบ Z ของ surface normal
- คำนวณระยะจากพื้นผิวทรงกลมด้วยเพื่อเอฟเฟกต์ fade ของ halo
- สุดท้ายรวมพื้นผิวและบรรยากาศเข้าด้วย alpha blending
ผลลัพธ์สุดท้ายและการใช้งาน
- แม้ต้องใช้หลายขั้นตอนกว่าที่คาด แต่ผลลัพธ์สุดท้ายดูเหมือน ทรงกลมเต็มรูปแบบ และรองรับการปรับแต่งเท็กซ์เจอร์กับ shader ตามต้องการ
- สามารถสร้างเท็กซ์เจอร์พื้นผิวด้วย graph เดียวใน Substance Designer และไม่ต้องทำ post-processing เพิ่มกับผลลัพธ์
- เท็กซ์เจอร์แบบแอนิเมชันก็จัดการได้ด้วยวิธีเดียวกัน
- ใช้ขนาดเท็กซ์เจอร์เล็กลงเพื่อประหยัดประสิทธิภาพได้
- ปรับ tiling ได้ตามต้องการ
- ดูวิดีโอผลลัพธ์ได้ที่ Video 3
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ดูเหมือนผู้เขียนจะตัด cubemap ทิ้งเร็วเกินไป ตอนผมลองใช้เรนเดอร์ดาวเคราะห์ก๊าซแบบไดนามิกในโปรเจกต์ส่วนตัว มันเป็นวิธีที่ง่ายที่สุด
การใช้ cubemap ไม่ได้ทำให้หน่วยความจำเพิ่มขึ้น 6 เท่า แต่เป็นการแบ่งเท็กซ์เจอร์สี่เหลี่ยมผืนผ้าขนาดใหญ่หนึ่งภาพออกเป็นหน้าสี่เหลี่ยมผืนผ้าเล็ก 6 หน้า ดังนั้นรายละเอียดรวมของเท็กซ์เจอร์จึงเท่าเดิม
ข้อดีคือไม่ต้องกังวลเรื่องการบีบอัดที่ขั้ว และสามารถสร้างฟิลด์การไหลแบบไร้รอยต่อสำหรับทำแอนิเมชัน/บิดเบือนเท็กซ์เจอร์ได้ง่ายด้วย ฟังก์ชัน noise แบบ 3 มิติหรือ 4 มิติ
https://www.junkship.net/News/2016/06/09/jupiter-jazz
แต่ละซีกจะถูกฉายเป็นแผ่นจานกลม แต่สามารถเติมเท็กซ์เจอร์ให้เต็มถึงขอบสี่เหลี่ยมจัตุรัส และทำให้บางส่วนของแต่ละซีกซ้ำอยู่ที่มุมของเท็กซ์เจอร์อีกด้านได้
ระหว่างจุดศูนย์กลางกับขอบของจานมีสเกลต่างกัน 1:2 จึงอาจมองได้ว่ามีการเปลืองพิกเซลเมื่อคิดจากรายละเอียดขั้นต่ำ แต่เพราะเป็นการฉายแบบคงมุม จึงทำให้วิธีสุ่มตัวอย่างสีของพิกเซลเป้าหมายในมุมมองที่มี perspective แรง ๆ จุกจิกน้อยลงมาก และการคำนวณไป-กลับก็ใช้แค่การหาร 1 ครั้งกับการบวก·คูณอีกไม่กี่ครั้งต่อจุดฉาย ถูกกว่าการฉายจากจุดศูนย์กลางของ cubemap ด้วยซ้ำ
ถ้าต้องการวิธีคงมุมที่ลดการเปลี่ยนสเกลและการเปลืองพิกเซลตามมุมลงอีก แต่ยังไม่ยากเกินไปในเชิงแนวคิด ก็ใช้ Mercator projection 2 ชิ้นที่ตั้งฉากกันและซ้อนทับกันเล็กน้อย เพื่อคลุมทรงกลมเหมือนหนังเบสบอลสองชิ้นได้
แต่ละชิ้นเป็นเท็กซ์เจอร์สี่เหลี่ยมผืนผ้าได้ และมีบทความของ NOAA บางฉบับที่เสนอแนวทางนี้สำหรับกริดสมการเชิงอนุพันธ์ในการจำลองสภาพอากาศของโลก
การฉายที่ผมรู้จักว่าใช้พิกเซลคุ้มค่าที่สุดคือแบ่งทรงกลมเป็นทรงแปดหน้า แล้วคลุมแต่ละออกแทนต์ด้วยกริดพิกเซลหกเหลี่ยมบนพื้นฐานของ “พิกัดพื้นที่บนทรงกลม”
แต่ละออกแทนต์สามารถแทนในภาพพิกเซลสี่เหลี่ยมจัตุรัสปกติเป็นครึ่งหนึ่งของสี่เหลี่ยมจัตุรัส หรือสามเหลี่ยมมุมฉาก 45-45-90 ได้ ทำให้ได้ผลลัพธ์แบบ https://observablehq.com/@jrus/sac-quincuncial และยังใช้กริดหกเหลี่ยมแบบ https://observablehq.com/@jrus/sphere-resample ได้ด้วย
แต่รายละเอียดเวลาต้องสุ่มตัวอย่างข้ามขอบเขตจะยุ่งยากกว่าวิธี stereographic 2 ซีกมาก และอาจเกิดอาร์ติแฟกต์รอยต่อได้
ต้องใช้คณิตศาสตร์มากขึ้น แต่ถ้าไม่ได้วางแผนจะแบ่งพื้นผิวให้ละเอียดขึ้นด้วยเหตุผลอื่น โดยทั่วไปก็ไม่ค่อยคุ้ม
การบีบอัดเท็กซ์เจอร์ที่ขั้ว จริง ๆ แล้วเป็นรูปแบบสุดโต่งของความบิดเบือนที่มีอยู่ทั่วทั้งพื้นผิว
ปกติมันจะเด่นชัดเฉพาะที่ขั้ว แต่ถ้าแบ่งทรงกลมเป็นสามเหลี่ยมแบบปรับตัวได้ ความบิดเบือนก็จะเปลี่ยนตามเมื่อระดับการแบ่งย่อยเปลี่ยน ทำให้เห็นได้ในตำแหน่งอื่นด้วย
ปัญหาคือทรงกลมถูกแบ่งเป็นสี่เหลี่ยม และแต่ละสี่เหลี่ยมถูกแทนด้วยสามเหลี่ยม 2 รูปที่มีพื้นที่เท่ากันในพื้นที่ UV แต่ในพื้นที่ 3D สามเหลี่ยมรูปหนึ่ง โดยเฉพาะรูปที่ขอบแนวนอนอยู่ใกล้ขั้วกว่า จะเล็กกว่า
ถึงอย่างนั้น UV ก็ยังถูกอินเทอร์โพเลตแบบเชิงเส้นภายในสามเหลี่ยม ทำให้ครึ่งหนึ่งของเท็กซ์เจอร์ถูกย่อ และอีกครึ่งถูกขยาย
ที่ขั้ว พื้นที่ 3D ของสามเหลี่ยมหนึ่งรูปกลายเป็น 0 จริง ๆ จึงเรนเดอร์เท็กซ์เจอร์ได้เพียงครึ่งเดียว และรอยต่อระหว่างสามเหลี่ยมก็ชัดเจนขึ้น
วิธีแก้ที่ถูกต้องคือ คำนวณพิกัด UV ต่อพิกเซลใน pixel shader แทนการอินเทอร์โพเลตเชิงเส้นต่อเวอร์เท็กซ์ และถ้าทำอย่างถูกต้อง ขั้วก็จะถูกจัดการได้แบบไร้รอยต่อ
เช่น ตอนเรนเดอร์ทรงกลมที่มีขั้ว “เหนือจริง” ถ้ามองจากด้านข้างก็ใช้ขั้วเรนเดอร์ฝั่งเหนือจริง และถ้ามองจากฝั่งใกล้เหนือจริงก็ใช้การเรนเดอร์แนวเส้นศูนย์สูตรที่ 0',0’ อะไรแบบนั้น
เริ่มอยากกลับไปดู displacement mapping อีกครั้ง
มันคงไม่ใช่ตัวเลือกแทนปัญหาที่ผู้เขียนพยายามแก้ แต่เรียบง่ายกว่าและค่อนข้างสนุก
ราว 25 ปีก่อน ผมเคยทำ visualizer เพลงชื่อ “Eclipse” สำหรับ SoundJam โดยอินพุตเป็นอาร์เรย์ระดับสัญญาณในย่านความถี่ที่ได้ยินและช่องซ้าย/ขวา
เป้าหมายคือภาพคล้ายดวงอาทิตย์ถูกบังคราสพร้อมการพ่นโคโรนา และข้อมูลเพลงก็กลายเป็น “สิ่งที่พ่นออกมา” นั้น
ความถี่ของข้อมูลเป็นตัวกำหนดว่ามันจะปรากฏตรงไหนรอบขอบจานดวงอาทิตย์
เมื่อเวลาผ่านไป สิ่งที่พ่นออกมาจะเคลื่อนออกห่างจากดวงอาทิตย์และ “เย็นลง” จนหายเป็นสีดำ สัญญาณแรงจะเริ่มจากสีขาว แล้วเมื่ออ่อนลงจะกลายเป็นเหลือง ส้ม แดง และน้ำตาล
ต้องเก็บอาร์เรย์ค่าข้อมูลเสียงไว้ใน circular buffer ที่ใหญ่พอจะครอบคลุมเวลาจนกว่าสิ่งที่พ่นออกมาจะหายเป็นสีดำ
ทั้ง “Eclipse” และการแสดงสิ่งที่พ่นออกมาท้ายที่สุดแล้วเป็น displacement map และผมคำนวณบิตแมปล่วงหน้าไว้ โดยให้ค่าของแต่ละพิกเซลเป็นออฟเซ็ตเข้าไปในบัฟเฟอร์ระดับเสียง
พิกเซลใกล้ผิวดวงอาทิตย์มีออฟเซ็ตของข้อมูลที่เพิ่งเข้ามา ส่วนพิกเซลด้านนอกมีออฟเซ็ตไปทางท้ายบัฟเฟอร์ที่กำลังจะหมดอายุ
การแมปการพ่นแบบวงกลม·รัศมีไปยังพิกัดคาร์ทีเซียนทำให้ต้องใช้คณิตศาสตร์เล็กน้อยในการสร้างค่าการเลื่อนตำแหน่ง
ลูปหลักคือรับค่าเสียงใหม่มาเขียนทับค่าที่เก่าที่สุด จากนั้นวนผ่าน displacement map ตามแถว·คอลัมน์ ดึงข้อมูลเสียงที่เกี่ยวข้องมาแมปเป็นสีจากพาเลตต์คงที่ แล้วใส่ลงในบัฟเฟอร์แสดงผล
มันไม่ได้ฉูดฉาดเท่า visualizer อื่น ๆ แต่มีความงามแบบนิ่งสงบ และสะท้อนข้อมูลเพลงได้ค่อนข้างดี
ต่างจาก visualizer รุ่นหลัง ๆ ที่ให้ความรู้สึกว่าแม้ใส่ความเงียบเข้าไปก็ยังอยู่นิ่งไม่ได้
บทความเจ๋งดี แต่ยิ่งเลื่อนลงไปด้านล่างก็ยิ่งโหลด shader จำนวนมาก จนถ้าไม่ใช่คอมพิวเตอร์ที่แรงมาก เบราว์เซอร์อาจแทบค้างได้
เป็นเรื่องน่าสนใจที่ผู้คนตีความคำว่า “ความสมจริง” ต่างกันมากแค่ไหน
ในยุคที่ทรัพยากรถูกจำกัดกว่านี้มาก ความแตกต่างนี้เห็นได้ชัดเป็นพิเศษในเกม และบางเกมก็ให้ความรู้สึกดื่มด่ำได้มากกว่าเกมงบมหาศาลในปัจจุบันเสียอีก ทั้งที่มีแค่กราฟิกพาเลตต์สีต่ำ ๆ แบบเหลี่ยม ๆ และเป็นพิกเซล
เลยทำให้รู้สึกมีส่วนร่วมอย่างกระตือรือร้นมากขึ้น
นึกถึงบทความนี้ที่พูดถึงแนวทางคล้ายกัน: https://bgolus.medium.com/rendering-a-sphere-on-a-quad-13c92...
ใน GPU หรือไลบรารี 3D ไม่มีฟังก์ชันที่ไล่สแกนแต่ละแถวแนวนอนของวงกลม แล้วแมป X,Y ของเท็กซ์เจอร์ไปยังตำแหน่ง “3D” บนทรงกลมหรือ?
ทรงกลมก็คือวงกลม และอัลกอริทึมวาดวงกลมก็เรียบง่าย แต่การหมุนและโปรเจกต์จุดยอดเป็นล้าน ๆ จุดสำหรับสามเหลี่ยมดูเหมือนเป็นการสิ้นเปลืองทรัพยากรมหาศาล
แค่สแกนแนวนอน แล้วเลื่อนลงมาหนึ่งบรรทัดและวาดต่อ ก็วาดได้ครบทุกบรรทัด
จำได้ว่าเคยทำอะไรแบบนี้ช่วงปลายยุค 80 เพื่อพรีคอมพิวต์ของอย่างเลนส์ขยายใน Second Reality
ทั้งเรนเดอริงไปป์ไลน์ถูกสร้างมาให้จัดการ linear interpolation อยู่แล้ว
แค่ shader program ตัวเดียวกับสามเหลี่ยม 1 รูปก็วาดทรงกลมที่มี anti-aliasing สมบูรณ์ได้แล้ว
ถ้าจะเรนเดอร์บน GPU แนวทางในบทความก็ดูค่อนข้างดี แม้ความซับซ้อนที่เห็นภายนอกจะเป็นอีกแบบ
มีชุดบทความยอดเยี่ยมเกี่ยวกับโครงสร้างการเรนเดอร์บน GPU: https://fgiesen.wordpress.com/2011/07/09/a-trip-through-the-...
ตอนที่ 6 พูดถึง rasterization
ดาวแบบ imposter ด้วย 2D pixel shader บนฉากหลังช่วยได้มากจริง ๆ ในงานที่มีดาวเคราะห์จำนวนมากอย่างจักรวาลแบบ procedural
ด้วยเหตุผลเรื่องหน่วยความจำและแบนด์วิดท์ GPU จึงจำกัดดาวเคราะห์แบบ sphere cube ไว้ที่ 1 อินสแตนซ์ แล้ววาดวัตถุท้องฟ้าที่อยู่ใกล้ ๆ ด้วยเทคนิคฉากหลัง
ในอวกาศจะมีระยะหนึ่งราว ๆ กึ่งกลางระหว่างวัตถุท้องฟ้าสองดวง ที่ทั้งคู่ถูกเรนเดอร์เป็น imposter และหลังจากนั้นจึงจัดการวัตถุท้องฟ้าที่ใกล้ที่สุดด้วยวิธี spherical quadtree
มันไม่สมบูรณ์แบบ แต่ภาพลวงตาแทบไม่มีจุดให้ติ
อีกอย่าง เพราะมันอยู่บนระนาบ จึงใส่เอฟเฟกต์ฟิสิกส์เบา ๆ ของเลนส์กล้องให้กับกาแล็กซีหรือวัตถุท้องฟ้าที่อยู่ไกลได้ง่ายขึ้น
เช่น เอฟเฟกต์ที่ดวงจันทร์ดูใหญ่กว่าความเป็นจริงตอนกำลังขึ้น หรือกาแล็กซีไกล ๆ ดูบิดงอจากแรงโน้มถ่วง
ชอบหน้าที่เกี่ยวกับดาวแก๊สนี้: https://emildziewanowski.com/flowfields/
icosphere สามารถคลี่ออกได้เรียบเนียนกว่ามาก และตำแหน่งจุดยอดกับขนาดองค์ประกอบก็เป็นระเบียบด้วย
การคลี่ออกไม่ใช่เรื่องง่าย ๆ แต่ก็ทำได้