การเรนเดอร์ท้องฟ้า พระอาทิตย์ตก และดาวเคราะห์
(blog.maximeheckel.com)- เชดเดอร์บนเบราว์เซอร์ผสาน การกระเจิงแบบ Rayleigh, การกระเจิงแบบ Mie และการดูดกลืนของโอโซน เพื่อเรนเดอร์ท้องฟ้าสีฟ้าและแสงอาทิตย์ยามอาทิตย์ตก·อาทิตย์ขึ้นแบบเรียลไทม์
- ระบบจะสะสม optical depth ของรังสีกล้องและค่า transmissivity ตามกฎของ Beer's Law พร้อมคำนวณการกระจายของการกระเจิงตามทิศทางดวงอาทิตย์ด้วย phase function
- เอฟเฟกต์อาทิตย์ตกทำโดยรัน light-march แยกต่างหากไปตามทิศทางดวงอาทิตย์ในแต่ละ sample เพื่อสะท้อนปริมาณแสงอาทิตย์ที่สูญเสียไประหว่างผ่านชั้นบรรยากาศ
- เชดเดอร์ท้องฟ้าแบบระนาบจะกลายเป็น เอฟเฟกต์ post-processing ด้วย depth buffer และการกู้คืนพิกัดโลก จึงจัดการได้แม้กระทั่งหมอกบรรยากาศระหว่างวัตถุในฉาก
- ในระดับสเกลดาวเคราะห์ ระบบจะขยายต่อด้วย logarithmic depth buffer, ray-sphere intersection และ LUT สำหรับ Transmittance·Sky-view·Aerial Perspective
เป้าหมายของเชดเดอร์การกระเจิงในชั้นบรรยากาศและแหล่งอ้างอิง
- เป้าหมายคือการจำลองชั้นสีที่ต่อเนื่องจาก สีส้มเข้ม·สีน้ำเงิน·สีดำของฉากหลังอวกาศ ในชั้นบรรยากาศชั้นบนของโลกให้เหมือนภาพถ่ายพระอาทิตย์ตกวงโคจรต่ำของกระสวยอวกาศ Endeavour ด้วยเชดเดอร์บนเบราว์เซอร์
- ขอบเขตการพัฒนาเริ่มจาก sky dome ที่สมจริงซึ่งผสาน raymarching, การกระเจิงแบบ Rayleigh, การกระเจิงแบบ Mie และการดูดกลืนของโอโซน แล้วขยายต่อไปยังเปลือกบรรยากาศรอบดาวเคราะห์และการปรับแต่งประสิทธิภาพด้วย LUT
- แหล่งอ้างอิงหลักคือ Three Geospatial, A Scalable and Production Ready Sky and Atmosphere Rendering Technique ของ Sébastien Hillaire และ Atmospheric Scattering (and also just faking it)
โมเดลพื้นฐานของการเรนเดอร์ท้องฟ้า
-
เหตุใด gradient แบบง่ายจึงไม่เพียงพอ
- สีของท้องฟ้าไม่ควรถูกมองเป็นเพียงฉากหลังสีน้ำเงินธรรมดา แต่เป็นผลลัพธ์จากการที่แสงมีปฏิสัมพันธ์กับอากาศและองค์ประกอบต่าง ๆ ของมัน
- ต้องคำนึงถึงตัวแปรอย่างระดับความสูงของผู้สังเกต ปริมาณฝุ่น และช่วงเวลาในวัน โดยการคำนวณจะเกิดขึ้นภายใน ปริมาตร(volume)
-
การสุ่มตัวอย่างความหนาแน่นของบรรยากาศ
- ชั้นบรรยากาศจะถูกสุ่มตัวอย่างด้วย raymarching คล้าย volumetric clouds หรือ volumetric light
- ระบบจะยิงรังสีจากตำแหน่งกล้องและค่อย ๆ เคลื่อนไปตามตัวกลางโปร่งใส พร้อมคำนวณทั้ง transmittance ซึ่งคือแสงที่ยังคงเหลือหลังผ่านชั้นบรรยากาศ และ scattering ที่ถูกเปลี่ยนทิศกลับเข้าหากล้องในแต่ละ sample
- หากต้องการทบทวน raymarching สามารถดู Painting with Math: A Gentle Study of Raymarching
-
ความหนาแน่นแบบ Rayleigh และ optical depth
- หากต้องการหาค่า transmittance จำเป็นต้องสะสมความหนาแน่นของบรรยากาศที่รังสีเดินทางผ่านเพื่อคำนวณ optical depth
- ฟังก์ชันความหนาแน่นแบบ Rayleigh แสดงให้เห็นว่าในระดับความสูง
hมี “อากาศ” อยู่มากเพียงใด และสะท้อนผลที่ว่าชั้นบรรยากาศจะเบาบางลงเมื่อระดับความสูงเพิ่มขึ้น - ตัวอย่างการใช้งานใช้ค่า
RAYLEIGH_SCALE_HEIGHT = 8.0km,ATMOSPHERE_HEIGHT = 100.0km,VIEW_DISTANCE = 200.0km,PRIMARY_STEPS = 24 rayleighDensity(h)คือexp(-max(h, 0.0) / RAYLEIGH_SCALE_HEIGHT)และในลูปจะสะสมด้วยviewOpticalDepth += dR * stepSize
-
กฎของ Beer's Law และสีฟ้าของท้องฟ้ากลางวัน
- ระบบจะคำนวณค่า transmittance
Tของจุดหนึ่งจาก optical depth โดยT=1.0หมายถึงไม่มีการสูญเสียแสง และT=0.0หมายถึงแสงหายไปทั้งหมด - ค่า transmittance คำนวณด้วย Beer's Law โดยโค้ดตัวอย่างใช้
vec3 transmittance = exp(-rayleighBeta * viewOpticalDepth) rayleighBetaคือค่าสัมประสิทธิ์การกระเจิงแบบ Rayleigh และในเชดเดอร์เก็บเป็นvec3(0.0058, 0.0135, 0.0331)- มุมระหว่างทิศทางแสงอาทิตย์กับรังสีสายตาถูกจำลองด้วย Rayleigh phase function ในรูป
3.0 / (16.0 * PI) * (1.0 + mu * mu) - ด้วยค่าสัมประสิทธิ์การกระเจิงแบบ Rayleigh ทำให้สีแดงแทบไม่ถูกกระเจิง สีเขียวถูกกระเจิงมากขึ้นเล็กน้อย และสีน้ำเงินถูกกระเจิงมากที่สุด จึงทำให้ท้องฟ้ากลางวันดูเป็นสีฟ้า
- เมื่อขยายเป็นหนึ่งรังสีต่อหนึ่งพิกเซล บริเวณใกล้ขอบฟ้าจะดูเหมือนหมอกขาวสว่างเพราะแสงต้องผ่านบรรยากาศมากกว่า ส่วนเมื่อระดับความสูงมากขึ้นก็จะเปลี่ยนเป็นสีน้ำเงินที่เข้มและมืดขึ้น
- ระบบจะคำนวณค่า transmittance
การกระเจิงแบบ Mie และการดูดกลืนของโอโซน
-
เอฟเฟกต์ที่ Rayleigh เพียงอย่างเดียวยังไม่พอ
- แม้การกระเจิงแบบ Rayleigh เพียงอย่างเดียวจะให้ผลลัพธ์ที่ดีพอสมควร แต่ท้องฟ้าที่สมจริงยิ่งขึ้นยังต้องการเอฟเฟกต์บรรยากาศเพิ่มเติม
- การกระเจิงแบบ Mie แสดงปฏิสัมพันธ์ระหว่างแสงกับอนุภาคที่มีขนาดใหญ่กว่า เช่น ฝุ่นหรือแอโรซอล และมีทั้งฟังก์ชันความหนาแน่นกับ phase function ที่แสดงการกระจายซ้ำตามทิศทาง
- การดูดกลืนของโอโซน จะตัดความยาวคลื่นบางส่วนของแสงที่ผ่านชั้นบรรยากาศชั้นบนออกจากเส้นทาง โดยไม่ได้เกิดจากการกระเจิง
- การดูดกลืนของโอโซนช่วยเพิ่มความลึกของสีท้องฟ้าและเลื่อนโทนสี โดยเฉพาะบริเวณขอบฟ้า ช่วงอาทิตย์ตก อาทิตย์ขึ้น และแสงสนธยาก่อนหลังช่วงนั้น
-
การสะสมของ Mie และโอโซน
- อิมพลีเมนต์ที่ใช้ Rayleigh, Mie และโอโซนร่วมกันจะสะสม optical depth ของแต่ละชนิดไว้ใน
viewODR,viewODM,viewODO - ในแต่ละ sample จะคำนวณ
dR = rayleighDensity(h),dM = mieDensity(h),dO = ozoneDensity(h)และประกอบtauจากผลรวมของBETA_R * viewODR,BETA_M_EXT * viewODM,BETA_OZONE_ABS * viewODO - ค่า transmittance คำนวณด้วย
exp(-tau)และสะสมค่าความหนาแน่นกับ transmittance รวมถึงstepSizeลงในsumR,sumM,sumO - การกระเจิงสุดท้ายคำนวณในรูป
SUN_INTENSITY * (phaseR * BETA_R * sumR + phaseM * BETA_M_SCATTER * sumM + BETA_OZONE_SCATTER * sumO)
- อิมพลีเมนต์ที่ใช้ Rayleigh, Mie และโอโซนร่วมกันจะสะสม optical depth ของแต่ละชนิดไว้ใน
-
ค่าคงที่สำคัญและเอฟเฟกต์
MIE_SCALE_HEIGHTเป็นค่าที่สอดคล้องกับRAYLEIGH_SCALE_HEIGHTสำหรับแอโรซอล และตั้งให้เล็กกว่าที่1.2kmเพราะอนุภาคมักกระจุกตัวใกล้ขอบฟ้าMIE_BETA_SCATTERควบคุมว่าอนุภาคจะกระเจิงแสงเข้าหากล้องมากเพียงใด โดยส่วนใหญ่ไม่ขึ้นกับความยาวคลื่น จึงตั้งเป็นvec3(0.003)MIE_BETA_EXTคือค่าสัมประสิทธิ์การสูญดับแบบ Mie ที่บอกว่ามีแสงถูกตัดออกจากเส้นทางมากเพียงใด และทำให้บรรยากาศระยะไกลดูขุ่นมัวมากขึ้นMIE_Gควบคุม anisotropy โดย0.0หมายถึงการกระเจิงสม่ำเสมอ และ1.0หมายถึงมีแนวโน้มกระเจิงไปข้างหน้ามากขึ้นอย่างชัดเจนOZONE_BETA_ABSมีค่าเป็นvec3(0.00065, 0.00188, 0.00008)ซึ่งดูดกลืนสีเขียวและช่วงเหลือง-ส้มมากกว่า ทำให้สีท้องฟ้าเลื่อนไปทางน้ำเงิน·แดง·ม่วง- เมื่อรวม Mie และโอโซนเข้าด้วยกัน จะได้สี “sky blue” ที่เป็นธรรมชาติมากขึ้นและแสงฟุ้งมัวรอบดวงอาทิตย์ โดยเอฟเฟกต์การกระเจิงแบบ Mie จะเด่นชัดยิ่งขึ้นเมื่อดวงอาทิตย์อยู่ใกล้ขอบฟ้า
เส้นทางแสงและช่วงพระอาทิตย์ตก·พระอาทิตย์ขึ้น
-
ข้อจำกัดของการใช้งานเดิม
- sky fragment shader สามารถเรนเดอร์สีที่ดูเป็นธรรมชาติได้ในระดับความสูงที่หลากหลาย และรองรับโมเดลการส่งผ่านของ Mie, Rayleigh และโอโซน
- แต่เมื่อย้ายดวงอาทิตย์เข้าใกล้เส้นขอบฟ้า ก็จะเห็นเพียง กลุ่มแสงสีขาวฟุ้งมัว โดยไม่มีทั้งการลดทอนของแสงหรือเอฟเฟกต์พระอาทิตย์ตก·พระอาทิตย์ขึ้น
- เพราะในลูป raymarching เดิมมีการคำนวณการลดทอนของแสงเฉพาะบนลำแสงมองเห็นจากกล้องไปยังแต่ละจุดตัวอย่างเท่านั้น
- จึงต้องคำนวณด้วยว่า ก่อนแสงอาทิตย์จะมาถึงจุดตัวอย่างนั้น แสงสูญเสียไปมากแค่ไหนขณะผ่านชั้นบรรยากาศ
-
ลูปซ้อน light-march
- ที่แต่ละจุดตัวอย่าง จะรันลูปซ้อนแยกต่างหากไปตามทิศทางของแหล่งกำเนิดแสง เพื่อสุ่มตัวอย่าง การส่งผ่าน ของเส้นทางนั้น
- แนวทางนี้ยังถูกใช้ใน real-time cloudscapes และ volumetric lighting เช่นกัน
lightMarch(float start, float sunY)จะวนซ้ำตามจำนวนLIGHTMARCH_STEPSพร้อมสะสมodR,odM,odO- เพิ่ม optical depth ตามทิศทางดวงอาทิตย์
sunODเข้าไปใน optical depth ของการมองเห็นเดิมviewODR,viewODM,viewODO tauขั้นสุดท้ายประกอบจากผลรวมของBETA_R * (viewODR + sunOD.x),BETA_M_EXT * (viewODM + sunOD.y),BETA_OZONE_ABS * (viewODO + sunOD.z)- การใช้งานนี้ทำให้สามารถเรนเดอร์ท้องฟ้าในสภาพแสงของพระอาทิตย์ตก พระอาทิตย์ขึ้น ดวงอาทิตย์เหนือศีรษะ และช่วงระหว่างนั้นได้
- ใช้ uniform
sun angleเพื่อสร้างการเปลี่ยนแปลงของสีน้ำเงินบนท้องฟ้าตลอดทั้งวัน และการกระเจิงแบบ Mie จะช่วยผสมแสงเข้ากับเส้นขอบฟ้าอย่างเป็นธรรมชาติในช่วงพระอาทิตย์ตกและพระอาทิตย์ขึ้น - เมื่อดวงอาทิตย์อยู่ต่ำ โอโซนจะเพิ่ม โทนม่วง ให้กับท้องฟ้า
ขยายไปสู่บรรยากาศของดาวเคราะห์
-
จากฉากหลังแบบระนาบสู่เอฟเฟกต์หลังการประมวลผล
- เชดเดอร์ที่สร้างไว้ก่อนหน้านี้ให้ฉากหลังท้องฟ้าที่ดี แต่ยังใกล้เคียงกับฉากหลังแบบระนาบในฉาก React Three Fiber
- ขั้นต่อไปคือแปลงมันให้เป็น เอฟเฟกต์หลังการประมวลผล (post-processing effect) เพื่อเรนเดอร์เป็นปริมาตรที่คำนึงถึงความลึกของฉาก และเป็นเปลือกบรรยากาศที่ล้อมรอบ planetary mesh
- เพื่อทำเช่นนี้ จะต้องสร้างพิกัด world space ขึ้นใหม่จากพิกัด
screenUVและนำ depth buffer ของฉากมาใช้กับ raymarching
-
การสร้าง world space ใหม่และลำแสง 3D
- หากต้องการใช้ atmospheric scattering กับฉาก ไม่ใช่แค่วาดท้องฟ้า แต่ต้องเติมพื้นที่ระหว่างกล้องกับออบเจ็กต์ที่ถูกเรนเดอร์บนหน้าจอด้วย
- ข้อมูลที่ต้องใช้คือ depth buffer ของฉาก,
projectionMatrixInverse,matrixWorld,positionของกล้อง และส่งค่าเหล่านี้เป็น uniform ให้กับเอฟเฟกต์หลังการประมวลผล getWorldPosition(vec2 uv, float depth)จะสร้างclipZด้วยdepth * 2.0 - 1.0และสร้างพิกัด NDC ด้วยuv * 2.0 - 1.0จากนั้นจึงใช้projectionMatrixInverseและviewMatrixInverse- กระบวนการเดียวกันนี้ยังถูกใช้ในเอฟเฟกต์หลังการประมวลผลของ volumetric lighting ใน On Shaping Light ด้วย
- หลังจากได้
worldPositionของพิกเซลปัจจุบันแล้ว จะตั้งrayOriginเป็นตำแหน่งกล้อง และคำนวณrayDirด้วยnormalize(worldPosition - rayOrigin)เพื่อเดินหน้าไปตาม ลำแสง 3D ของแต่ละพิกเซลบนหน้าจอ
-
ปรับช่วง raymarch ด้วย depth buffer
- เพื่อคำนึงถึงเรขาคณิตของฉาก ต้องกำหนดช่วง raymarch ของลำแสงปัจจุบันด้วย depth buffer แทน
stepSizeแบบคงที่ - ใช้
sceneDepth = depthToRayDistance(uv, depth)เพื่อหาความลึกของฉากบนลำแสง - พิกเซลฉากหลังตรวจจับได้ด้วย
depth >= 1.0 - 1e-7และสำหรับ “sky pixels” จะใช้sceneDepth = atmosphereHeight * SKY_MARCH_DISTANCE_MULTIPLIER - หากลำแสงชี้ลงด้านล่าง จะคำนวณจุดตัดกับพื้นด้วย
tGround = observerAltitude / max(-rayDir.y, 1e-4)และจำกัดด้วยrayEnd = min(rayEnd, tGround) stepSizeขั้นสุดท้ายคำนวณเป็น(rayEnd - rayStart) / float(PRIMARY_STEPS)- ลำแสงที่ชนออบเจ็กต์ใกล้ตัวหรือพื้นจะถูกสุ่มตัวอย่างอย่างแม่นยำขึ้นด้วย
stepSizeที่เล็กกว่า ส่วนลำแสงที่ไปไกลจะกระจายจำนวนตัวอย่างเท่าเดิมออกไปบนระยะที่ยาวกว่า
- เพื่อคำนึงถึงเรขาคณิตของฉาก ต้องกำหนดช่วง raymarch ของลำแสงปัจจุบันด้วย depth buffer แทน
-
หมอกบรรยากาศภายในฉาก
- เชดเดอร์ที่ทำเป็นเอฟเฟกต์หลังการประมวลผลนี้จะใช้ atmospheric scattering กับทั้งปริมาตรของฉาก และยังสามารถใช้ sky shader เป็นฉากหลังโดยคำนึงถึงเรขาคณิตของฉากได้
- ออบเจ็กต์ที่อยู่ใกล้กล้องจะมองเห็นได้คมชัดกว่า ส่วนออบเจ็กต์ที่อยู่ไกลจะยิ่งพร่ามากขึ้น
- ตัวอย่างอินเทอร์แอ็กชันที่ใส่วัตถุท้องฟ้าซึ่งลากได้ด้วย
Raycasterดูได้ใน ทวีตของ MaximeHeckel
การเรนเดอร์ดาวเคราะห์
-
สองขั้นตอนที่จำเป็น
- หากต้องการเรนเดอร์บรรยากาศรอบดาวเคราะห์ให้สมจริง จำเป็นต้องมี logarithmic depth buffer สำหรับจัดการสเกลขนาดใหญ่ และเปลือกบรรยากาศทรงกลมที่ใช้กำหนดว่ารังสีเริ่มต้นและสิ้นสุดที่จุดใดในบรรยากาศ
-
logarithmic depth buffer
- ในระดับขนาดของดาวเคราะห์ เมื่อมองจากระยะไกล เชดเดอร์อาจแยกความแตกต่างของความลึกระหว่างบรรยากาศกับเปลือกดาวเคราะห์ได้ยาก ทำให้เกิด depth fighting
- เนื่องจากความสูงของบรรยากาศมีเพียงไม่กี่กิโลเมตร จึงต้องปรับทั้งการกำหนด depth buffer ของฉากและวิธีการอ่านมันในเอฟเฟ็กต์ post-processing
- ตั้งค่า
logarithmicDepthBuffer: trueใน propglที่ครอบCanvasของ React Three Fiber - ตัวอย่างการตั้งค่าอยู่ในรูปแบบ
<Canvas shadows gl={{ alpha: true, logarithmicDepthBuffer: true }}> - ในเชดเดอร์ มีการนิยามการคำนวณ
sceneDepthใหม่เพื่อแปลง logarithmic depth buffer กลับเป็นระยะบนแนวรังสี logDepthToViewZ(depth)ใช้pow(2.0, depth * log2(cameraFar + 1.0)) - 1.0และคืนค่า-d
-
หาช่วงบรรยากาศด้วย ray-sphere intersection
- ใช้ ray-sphere intersection test เพื่อหาจุดที่รังสีสายตาเข้าสู่และออกจาก ทรงกลมบรรยากาศ (atmospheric sphere)
- เมื่อได้จุดตัดสองจุดแล้ว ก็สามารถจำกัดลูป raymarching ให้ทำงานเฉพาะในช่วงนั้นได้ โดยไม่สิ้นเปลืองการสุ่มตัวอย่างนอกบรรยากาศ
- เนื่องจากดาวเคราะห์เป็นเมชทรงกลม และมีทรงกลมบรรยากาศที่ใหญ่กว่าเล็กน้อยห่อหุ้มอยู่ จึงใช้การทดสอบจุดตัดแบบเดียวกันกับตัวดาวเคราะห์ด้วย
- หากรังสีชนพื้นดินก่อนจะออกจากบรรยากาศ ก็ใช้จุดตัดกับพื้นเป็นจุดสิ้นสุดของช่วง raymarching
- การใช้งาน
raySphereIntersectที่ใช้ อ้างอิงจาก Ray-Surface intersection functions ของ Inigo Quilez
-
ออบเจ็กต์ในฉากและเงื่อนไขสิ้นสุดของบรรยากาศ
- บรรยากาศควรสิ้นสุดเมื่อสัมผัสพื้นผิวดาวเคราะห์ หรือเมื่อเจอออบเจ็กต์อื่นในฉากก่อนจะชนพื้น
- กรณีชนดาวเคราะห์ จะหยุดที่พื้นโดยพื้นฐานด้วย
atmosphereFar = min(atmosphereFar, planetHit.x) - หากมีเมชอื่นถูกเรนเดอร์อยู่ด้านหน้าพื้น จะตรวจด้วยเงื่อนไข
sceneDepth < planetHit.x - 2.0แล้วใช้atmosphereFar = min(atmosphereFar, sceneDepth) - หากไม่มีตรรกะนี้ จะเกิดปัญหาที่พื้นผิวดาวเคราะห์แสดงอยู่ด้านหน้าออบเจ็กต์
-
เดโม React Three Fiber และกลิตช์ที่ยังเหลืออยู่
- เมื่อนำการปรับสองส่วนนี้ไปใช้ในโค้ด ก็สามารถทำ atmospheric scattering เป็นเอฟเฟ็กต์ post-processing และเรนเดอร์บรรยากาศรอบดาวเคราะห์ได้
- ฉากเดโมเรนเดอร์ “Sun - Earth system” แบบง่ายใน React Three Fiber แล้วใช้เอฟเฟ็กต์แบบกำหนดเอง
- เมื่อปรับตำแหน่งดวงอาทิตย์และซูมออก ก็จะเห็นสีท้องฟ้าที่เชดเดอร์สร้างขึ้นจากหลายมุมมอง ตั้งแต่บนพื้นดินไปจนถึงวงโคจร
- เอฟเฟ็กต์เดียวกันนี้ถูกใช้กับภาพโปสเตอร์สำหรับโพสต์ตัวอย่างบทความช่วงต้นเดือนเมษายน และมีการแชร์ภาพเรนเดอร์ทาง ทวีต
- torus ในฉากอาจยังดูเหมือนอยู่ในสถานะ “lit-up” แม้หลังพระอาทิตย์ตกแล้ว
- สาเหตุมาจาก shadow-map หรือ shadow-camera ของ directional light หลักมีสเกลเล็กเกินไป จึงครอบคลุม torus ที่อยู่ไกลมากไม่ถึง
- วิธีเลี่ยงคืออาจนำแนวทาง shadow-mapping จาก บทความ volumetric lighting กลับมาใช้ แต่ยังไม่ได้ลองทำจริง
การจัดการสุริยุปราคา
- กรณีที่วัตถุท้องฟ้าขนาดใหญ่บังดวงอาทิตย์ สามารถเพิ่มได้โดยเรียกฟังก์ชัน
sunVisibilityหลังlightMarchแล้วนำค่าที่คืนกลับมา[0, 1]ไปคูณกับค่าการส่งผ่านแสง - แนวคิดพื้นฐานคือเปรียบเทียบดอตโปรดักต์ของ ทิศทางไปยังดวงจันทร์ และ ทิศทางไปยังดวงอาทิตย์ จากตำแหน่งตัวอย่างปัจจุบัน
- หากสองทิศทางเกือบตรงกันจนดอตโปรดักต์เข้าใกล้
1.0แสดงว่าดวงจันทร์กำลังบังดวงอาทิตย์ และหากตั้งฉากกันจนเข้าใกล้0.0แสดงว่าไม่มีการบัง - แต่ดอตโปรดักต์เพียงอย่างเดียวไม่สามารถสะท้อน ขนาดและสเกล ของวัตถุในฉากได้ ดังนั้นการใช้งานจริงจึงเปรียบเทียบระยะเชิงมุมระหว่างดวงอาทิตย์กับดวงจันทร์ รวมถึงรัศมีเชิงมุมของแต่ละดวง
sunVisibilityครอบคลุมกรณีที่ดวงจันทร์ไม่ได้บังดวงอาทิตย์, กรณีที่บังในสถานะที่เมื่อมองจากกล้องดวงจันทร์ดูใหญ่กว่าหรือมีขนาดใกล้เคียงดวงอาทิตย์, และกรณีที่บังในสถานะที่เมื่อมองจากกล้องดวงจันทร์เข้าไปอยู่ภายในรัศมีของดวงอาทิตย์- เดโมเพิ่ม
sunVisibilityและ เมชดวงจันทร์ ลงบนตัวอย่าง atmospheric scattering เดิม เพื่อให้ Atmospheric Scattering shader จัดการสถานการณ์ที่แสงไม่เพียงพอเมื่อจัดดวงจันทร์ให้อยู่แนวเดียวกับดวงอาทิตย์ - การจำลองสุริยุปราคาและโคโรนาที่ละเอียดกว่านี้มีอยู่ในงานวิจัย Physically Based Real-Time Rendering of Eclipses และยังไม่มีการพอร์ตอิมพลีเมนเทชันของงานนั้นไปเป็น WebGL
บรรยากาศของดาวเคราะห์ดวงอื่น
- แบบจำลองความหนาแน่นและการกระเจิงของบรรยากาศที่ใช้ ถูกกำหนดเป็นหลักด้วยค่าคงที่ไม่กี่ตัว เช่น รัศมีของดาวเคราะห์และบรรยากาศ,
RayleighScaleHeight,RayleighBeta,MieScaleHeight,MieBeta,mieBetaExt,mieG,OzoneHeight,OzoneWidth - เมื่อปรับค่าเหล่านี้ ก็สามารถสร้างผลลัพธ์ที่ใกล้เคียงกับ บรรยากาศของดาวอังคาร หรือบรรยากาศของดาวเคราะห์ดวงอื่นได้
- ค่าที่ใช้สำหรับดาวอังคารเป็นค่าประมาณ
planetRadius: 3390atmosphereRadius: 3500, ความหนาประมาณ110 kmrayleighScaleHeight: 11.1rayleighBeta: new THREE.Vector3(0.019, 0.013, 0.0057)mieScaleHeight: 1.5mieBeta: 0.04mieBetaExt: 0.044mieG: 0.65ozoneCenterHeight: 0.0ozoneWidth: 1.0ozoneBetaAbs: new THREE.Vector3(0.0, 0.0, 0.0)sunIntensity: 15.0planetSurfaceColor: '#8B4513'
- หากแทนที่ค่าคงที่เดิมด้วยค่าเหล่านี้ ก็จะได้บรรยากาศที่ มีฝุ่นมากขึ้นและออกโทนส้ม มากขึ้น รวมถึงได้ โทนสีน้ำเงินยามอาทิตย์ตก อันเป็นเอกลักษณ์ของดาวอังคารด้วย
- มีงานวิจัยที่เกี่ยวข้องคือ Physically Based Rendering of the Martian Atmosphere
การกระเจิงของบรรยากาศแบบอิง LUT
-
แนวทางและส่วนที่ย่อให้สั้นลง
- เชดเดอร์แบบเดิมสามารถเรนเดอร์บรรยากาศทั้งในสเกลเล็กและสเกลใหญ่ได้อย่างตรงไปตรงมา แต่มีต้นทุนการประมวลผลสูง เพราะมีลูป raymarching ที่
PRIMARY_STEPSจำนวนมาก, ลูปซ้อนlightmarchingและการคำนวณที่ความละเอียดเต็มหน้าจอ - A Scalable and Production Ready Sky and Atmosphere Rendering Technique ของ Sebastian Hillaire เสนอแนวทางแบบอิง Look Up Tables(LUTs) โดยเก็บการคำนวณการกระเจิงที่มีต้นทุนสูงไว้ในเท็กซ์เจอร์ แล้วค่อยสุ่มตัวอย่างและคอมโพสจากเท็กซ์เจอร์ที่คำนวณไว้ล่วงหน้าในขั้นตอนเรนเดอร์สุดท้าย
- LUT ที่กล่าวถึงมี Transmittance LUT สำหรับเก็บปริมาณแสงที่ยังคงเหลือหลังผ่านบรรยากาศ, Sky-view LUT สำหรับเก็บสีท้องฟ้าที่ตำแหน่งกล้องหนึ่ง ๆ มองเห็น, และ Aerial Perspective LUT สำหรับเก็บหมอกบรรยากาศและแสงกระเจิงระหว่างกล้องกับเรขาคณิตของฉากที่มองเห็น
- ไม่ได้นำการอิมพลีเมนต์จากทั้งงานวิจัยมาใช้ตรง ๆ ทั้งหมด และแม้ LUT จะเหมาะกับ compute shader ของ WebGPU แต่เพราะเวลาจำกัดและเพื่อให้บทความต่อเนื่อง จึงยังคงใช้ WebGL
- ในงานวิจัย Aerial Perspective LUT เป็น 3D texture แต่ในการอิมพลีเมนต์นี้ใช้ 2D render target
- วิธีนี้จำเป็นต้องสร้างเท็กซ์เจอร์ใหม่ทุกครั้งเมื่อกล้องเคลื่อนที่ เพื่อให้ได้ค่าพิกเซลที่ถูกต้อง จึงยากต่อการพรีคอมพิวต์ล่วงหน้า
- ละ Multi-Scattering ออกไปเพราะเวลาจำกัด
- เชดเดอร์แบบเดิมสามารถเรนเดอร์บรรยากาศทั้งในสเกลเล็กและสเกลใหญ่ได้อย่างตรงไปตรงมา แต่มีต้นทุนการประมวลผลสูง เพราะมีลูป raymarching ที่
-
Transmittance LUT
- ในเชดเดอร์แบบเดิม ทุกจุดตัวอย่างจะเรียก
lightmarchเพื่อคำนวณว่าแสงอาทิตย์เดินทางมาถึงได้มากแค่ไหน ซึ่งมีค่าใช้จ่ายสูง - Transmittance LUT จะเก็บข้อมูลนี้ไว้ล่วงหน้าที่ความละเอียดต่ำ เพื่อให้ LUT อื่น ๆ อ่านไปใช้ได้เมื่อจำเป็นต้องใช้ข้อมูลแสง
- การอิมพลีเมนต์กำหนด Frame Buffer Object เฉพาะที่ความละเอียด
250 x 64แล้วใช้ material เชดเดอร์แบบคัสตอมกับ full-screen quad ของซีนเฉพาะtransmittanceLUTSceneจากนั้นส่งเท็กซ์เจอร์ผลลัพธ์จากการเรนเดอร์เป็น uniform ให้กับ LUT ปลายน้ำ - ที่แต่ละพิกเซล จะทำ raymarching จาก
vec3(0.0, radius, 0.0)โดยradiusจะเพิ่มจากplanetRadiusไปถึงatmosphereRadiusตามพิกัดvUv.y - แกน x ของ LUT แทนมุมของแสง ส่วน แกน y แทนระดับความสูง โดยสีขาวล้วนหมายถึงอัตราการส่งผ่าน
100%ขณะที่บริเวณสีดำหรือมีสีแสดงถึงพื้นดินหรือส่วนที่อากาศหนาแน่นที่สุด - จากนั้น LUT อื่น ๆ ก็สามารถดึง “ปริมาณแสงที่ยังเหลือหลังผ่านบรรยากาศที่มุมและความสูงที่กำหนด” ได้ด้วยการ lookup เท็กซ์เจอร์เพียงอย่างเดียว
- ในเชดเดอร์แบบเดิม ทุกจุดตัวอย่างจะเรียก
-
Sky-view LUT
- Sky-view LUT คำนวณว่าสีของท้องฟ้าจะเป็นอย่างไรเมื่อมองขึ้นไปจากพื้นดินในทิศทางหนึ่ง ๆ
getSkyViewRayDirจะแมปvUv.xไปเป็น azimuth[-PI, PI]และvUv.yไปเป็น elevation[-PI/2, PI/2]เพื่อกำหนดทิศทาง raymarching- สำหรับ elevation ใช้ quadratic mapping คือ
(vUv.y * vUv.y - 0.5) * PIซึ่งเป็นวิธีแก้เฉพาะหน้าเพื่อหลีกเลี่ยงไม่ให้ Sky View กะพริบมากเกินไปในระยะไกล - ถ้าเรย์ไม่เข้าสู่บรรยากาศจะคืนค่าสีดำ และเรย์ที่ชนดาวเคราะห์จะทำ raymarching เฉพาะช่วงของบรรยากาศที่มองเห็นได้ พร้อมหยุดเร็วขึ้นเมื่อชนดาวเคราะห์
- ลูปการกระเจิงยังคงเหมือนเดิม แต่เดินหน้าไปตามทิศทาง Sky View และใช้ Transmittance LUT สำหรับแสงอาทิตย์
-
Aerial Perspective LUT
- ต่างจากงานวิจัยของ Hillaire ผลลัพธ์ของการอิมพลีเมนต์นี้เป็น 2D texture และแต่ละพิกเซลจะสอดคล้องกับพิกเซลหนึ่งจุดบนหน้าจอที่มองเห็น
- ใช้ depth buffer ของฉากเพื่อตัดสินว่าควรเดินตามเรย์ไปไกลแค่ไหนและสะสมการกระเจิงมากเพียงใด
- นำโค้ดการกระเจิงเดิมกลับมาใช้เกือบทั้งหมด แต่ให้แต่ละตัวอย่างดึงค่าการมองเห็นแสงอาทิตย์จาก Transmittance LUT
- เอาต์พุตจะเก็บค่าการกระเจิงบรรยากาศสะสมไว้ใน RGB และเก็บค่า packed view transmittance ที่ใช้ตอนคอมโพสไว้ในอัลฟา
- ลำดับการอิมพลีเมนต์คืออ่านค่าความลึกจาก
depthBufferแล้วกู้คืนตำแหน่งใน world space ของพิกเซลบนหน้าจอด้วยgetWorldPosition(vUv, depth)ก่อนคำนวณrayDirจากตำแหน่งกล้องไปยังตำแหน่งใน world space - จากนั้นใช้
logDepthToRayDistance(vUv, depth)เพื่อแปลงความลึกของฉากเป็นระยะตามเรย์ แล้วคำนวณจุดตัดกับบรรยากาศและดาวเคราะห์ ก่อนจะ march เฉพาะช่วงบรรยากาศที่มองเห็นได้
-
การคอมโพส
- หลังจากสร้าง Sky-view LUT และ Aerial Perspective LUT แล้ว จะรวมทั้งสองเข้าด้วยกันใน post-processing pass สุดท้าย
- งานหลักคือแปลง
rayDirปัจจุบันให้เป็น พิกัด UV ของ Sky View - สำหรับเรขาคณิตของฉาก จะใช้ Aerial Perspective LUT โดยใช้อัลฟาแชนเนลเป็น view transmittance และใช้แชนเนล RGB เป็นแสงกระเจิง เพื่อคำนวณ
color = color * aerialPerspective.a + aerialPerspective.rgb - สำหรับพิกเซลพื้นหลัง จะสุ่มตัวอย่างจาก Sky View LUT และหาก
depth >= 1.0 - 1e-7จะถือเป็นพื้นหลัง แล้วใช้color = inputColor.rgb + sampleSkyViewLUT(rayDir, planetCenter) - สุดท้ายจึงใช้
ACESFilm(color)และpow(color, vec3(1.0 / 2.2)) - ดูโค้ดการอิมพลีเมนต์บรรยากาศแบบอิง LUT ทั้งหมดได้ที่ Github link
สรุป
- ผลลัพธ์ของการกระเจิงบรรยากาศแบบอิง LUT อาจดูแทบไม่ต่างจากเวอร์ชัน raymarching เต็มรูปแบบก่อนหน้านี้ แต่กระบวนการภายในต่างกัน
- มันแบ่งงานออกเป็น LUT ขนาดเล็กหลายตัว แล้วคอมโพสในเอฟเฟ็กต์ขั้นสุดท้าย โดยไม่ต้อง raymarching ซ้ำไปทางดวงอาทิตย์ในทุกตัวอย่างเพื่อคำนวณแสงที่มาถึง
- เพราะดึงข้อมูลแสงมาโดยตรงจาก Transmittance LUT จึงแทนที่ลูปซ้อนที่มีต้นทุนสูงด้วยการ lookup เท็กซ์เจอร์แบบง่าย และได้ประสิทธิภาพที่ดีขึ้นอย่างมีนัยสำคัญในฉากสุดท้าย
- การอิมพลีเมนต์นี้ยังด้อยกว่าเมื่อเทียบกับ Sébastian Hillaire และงานอิมพลีเมนต์ในด้านอื่น ๆ โดยเฉพาะ Sky View ที่ยังมี banding และ flickering และยังไม่เหมาะที่สุดเพราะมีส่วนที่ย่อให้สั้นลง
- เป็นไปได้ว่าควรใช้ WebGPU ตั้งแต่แรก
- หากต้องการการอิมพลีเมนต์ระดับ production-grade จริง ๆ แนะนำ three-geospatial ของ Shoda Matsuda(@shotamatsuda)
- นอกจากนี้ยังได้ลองใส่ volumetric clouds เพิ่มเข้าไปด้วย แต่ผลลัพธ์ยังคละกันและยังไม่น่าพอใจพอจะนำมาแสดงในบทความ จึงยังต้องทำต่อ
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
มันมีความสนุกเป็นพิเศษในการพัฒนาเอฟเฟ็กต์ภาพและได้เห็นมันค่อย ๆ ถูกทำให้สมจริงขึ้นเรื่อย ๆ และสักวันก็อยากลองทำอะไรในสายนี้ด้วยตัวเอง
เมื่อก่อนยอดวิววิดีโออยู่ระดับหลายล้าน แต่ตอนนี้แค่ 5 แสนยังแทบจะไปไม่ถึง อาจเป็นผลจากช่วงโควิดที่ทุกคนอยู่บ้านและหันไปสนใจอะไรสุ่ม ๆ มากขึ้นก็ได้
ปกติผมมักเปิดฟังก่อนนอน แล้วก็เคยคิดเหมือนกันว่าอยากลองทำเอง เพราะอยากมีคอนเทนต์แบบนี้ที่สงบแต่ลงลึกกับหัวข้อเทคนิคมากกว่านี้
หลังพระอาทิตย์ตกไปสักพัก บรรยากาศเหนือศีรษะและบริเวณเหนือขอบฟ้ายังคงได้รับแสงอาทิตย์อยู่ และในบรรยากาศของโลกยังคงมีแสงสนธยาที่สังเกตได้จนกว่าดวงอาทิตย์จะต่ำกว่าขอบฟ้า 18 องศา แม้อาจไม่ค่อยเหมาะจะทำด้วย ray tracing แต่ก็มีอัลกอริทึมทั่วไปสำหรับโมเดลลักษณะนี้อยู่
https://www.threads.com/@mrsharpoblunto/post/DVS4wfYiG8f?xmt...
https://www.threads.com/@mrsharpoblunto/post/C6Vc-S1O9mX?xmt...
https://www.threads.com/@mrsharpoblunto/post/C6apksDRa8q?xmt...
ยังจำได้ว่าเคยทำ implementation ของ “Display of The Earth Taking into Account Atmospheric Scattering” ของ Nishita และคณะ ซึ่งเป็นงานปี 1993 และแทบจะเป็นงานต้นแบบของหัวข้อนี้ แถมอ่านง่ายมาก: https://www.researchgate.net/publication/2933032_Display_of_...
ตอนที่ทำให้มันใช้งานได้ มีจังหวะหนึ่งที่รู้สึกว่า “ปรากฏการณ์ซับซ้อนในโลกจริงแบบนี้ กลับจำลองได้ค่อนข้างดีด้วยการคำนวณที่ค่อนข้างเรียบง่ายไม่กี่อย่าง” จากเดิมที่มีแค่กล่องฟ้าสีน้ำเงินนิ่ง ๆ ก็กลายเป็นวัฏจักรกลางวันกลางคืนแบบสมบูรณ์ได้ในพริบตา
เมื่อก่อนเคยคิดว่า ถ้าลองเรนเดอร์ท้องฟ้าบนเว็บด้วยการซ้อน gradient หลายชั้นจะเป็นอย่างไร น่าจะพอสำเร็จระดับหนึ่งและได้ผลลัพธ์ที่ใช้ได้อยู่บ้าง แต่ก็เทียบกับสิ่งที่ทำไว้ที่นี่ไม่ได้เลย ผลลัพธ์น่าประทับใจและให้แรงบันดาลใจมาก
แค่นั้นก็ทำให้ได้วัฏจักรพระอาทิตย์ตก/พระอาทิตย์ขึ้นที่ดูน่าเชื่อถือพอสมควรแล้ว ซึ่งทำให้ผมประหลาดใจ และถ้าจำไม่ผิด ตัวดวงอาทิตย์เองก็ดูเหมือนจะโผล่ออกมาอย่างเป็นธรรมชาติจากตรงนั้นด้วย ตอนนั้นใช้ XNA ซึ่งเป็นแพลตฟอร์มพัฒนาเกม C# ของ Microsoft และทำตามชุดบทสอนอันยอดเยี่ยมของ Riemer โดยมีฉบับเก็บถาวรอยู่ที่นี่ https://github.com/SimonDarksideJ/XNAGameStudio/wiki/Riemers...
แต่ดูเหมือนจะไม่มีส่วนเรื่องการกระเจิงอยู่ตรงนั้น ดังนั้นส่วนนั้นผมอาจเอามาจากที่อื่น ยังจำได้ว่าเคยอ่านงานวิจัยที่มีสมการอยู่ด้วย
https://spaceengine.org/
คำตอบของ “SpaceEngine มีวัตถุกี่ชิ้น?” คือรวมทั้งแค็ตตาล็อกดาว Hipparcos ทั้งหมด ดาวเคราะห์นอกระบบที่รู้จักทั้งหมด กาแล็กซีมากกว่าหนึ่งหมื่นแห่ง และวัตถุส่วนใหญ่ในระบบสุริยะ รวมแล้ว 130,000 ชิ้น และยังมีการเพิ่มกาแล็กซีและระบบดาวจำนวนมากยิ่งกว่าที่มีอยู่จริงในเอกภพที่สังเกตได้ด้วย ส่วน “ทำไมดาวเคราะห์น้ำถึงร้อนได้?” ก็อธิบายว่าน้ำในชั้นบรรยากาศตอนบนเป็นไอน้ำร้อน แต่เมื่ออยู่ลึกลงไปจะค่อย ๆ เปลี่ยนเป็นของเหลวภายใต้ความดันสูง และลึกลงไปกว่านั้นจะกลายเป็นสถานะของแข็งที่เรียกว่า ice VII ส่วนคำตอบของ “เคลื่อนที่อย่างไร?” คือปุ่ม WASD
เป็นเกมที่ยอดเยี่ยม และถึงจะเก่าพอสมควรแล้วก็ยังไม่ค่อยเห็นอะไรที่ทำได้ดีระดับนี้
พออ่านโพสต์นี้แล้วผมก็นึกถึง SpaceEngine เหมือนกัน
หนึ่งในงานวิจัยที่ผมชอบ: http://www.graphics.stanford.edu/papers/bssrdf/bssrdf.pdf
คิดว่านี่น่าจะเป็นครั้งแรกที่ผมได้รู้ว่าการเรนเดอร์นมนั้นเป็นปัญหาที่ยุ่งยาก
ผมน่าจะเข้าใจแค่ราว 5% เท่านั้น แต่ก็ทึ่งมากจริง ๆ
แถมถ้าเป็น MIT License ด้วย ก็เท่ากับว่าปัญหา skybox ในเกมของผมได้รับการแก้แล้ว เพราะมุมมองจะถูกตรึงไว้ ดังนั้นขอแค่มีการเรนเดอร์ดวงอาทิตย์เคลื่อนข้ามท้องฟ้า แล้วก็ขยายต่อด้วยคาบแบบ sine wave เพื่อให้มุมดวงอาทิตย์เปลี่ยนไปตามฤดูกาลตลอดทั้งปีได้