مشروع: تطبيق Ray Tracing باستخدام C‎#‎ – الحلقة 3 – الظلال والانعكاسات « مغامرات برمجية

مشروع: تطبيق Ray Tracing باستخدام C‎#‎ – الحلقة 3 – الظلال والانعكاسات

في الدرس السابق وصلنا إلى الصورة بعاليه. وهي صورة فيها مقدار لا بأس به من الواقعية ولكن هناك حتماً مجال للتحسين. يمكننا أن نرى من التظليل أن مصدر الضوء قادم من الأعلى، مما يعني أنه من المفترض أن يكون هناك ظل أسفل الكرتين في السطح الأخضر. وسنضيف أيضاً أحد أشهر خواص الصور المنتجة بواسطة الـray tracing وهي الانعكاسات (كما يحدث في المرايا). وكما وعدتكم سنضيف مصدر ضوئي جديد هو الـpoint light. لدينا ما يكفي لدرس جديد إذن! جهزوا أدوية الصداع وهلموا معي.

 

بما أن الـray tracing هو محاكاة لعملية البصر الطبيعية، لننظر إلى كيفية حصول الظلال والانعكاسات فيزيائياً. كل ما علينا تذكره هو أن الضوء يسير في خطوط مستقيمة حتى يصطدم بشئٍ ما يغير من مساره. حاكينا هذا الموضوع لتمثيل التظليل في الدرس السابق، والآن لنرى تطبيقه في الظلال.

 

في الشكل بعاليه نرى إشعاعات الضوء تسقط من مصدر الضوء إلى السطح بالأسفل لتنيره. ولكن ليست كل الإشعاعات وصلت. هناك مجموعة منها اصطدمت بالكرة الحمراء، التي منعتها من الوصول إلى السطح. أي أن هناك منطقة في السطح لا يصل إليها الضوء لأن الكرة حجبته عنها. هذه المنطقة لا يتم إنارتها مما بنتج عنه منطقة مظلمة هي ما نسمبه نحن العامة الظل.

 

في الدرس السابق، عندما وجدت الإشعاعات الصادرة من الكاميرا جسماً، قمنا بإصدار إشعاعات إضافية إلى مصادر الضوء لتحديد اللون بعد عملية التظليل. ما سنفعله الآن هو أننا، بالإضاقة لذلك، سنرى إذا كان هذا الإشعاع الإضافي سيصطدم هو الآخر بجسم. إذا لم يحدث أي اصطدام فهذا يعني أنه لا يوجد هناك شي بين الجسم الذي نريد تصويره ومصدر الضوء. ولكن إذا اصطدمنا بجسم آخر فهذا يعني أن هذا الجسم الثاني يحجب الضوء من هذا المصدر، أي أننا في منطقة ظل لا يصل إليها الضوء من هذا المصدر لذا لا نقوم بتلوينه.

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//Iterate through the lights and calculate their contribution
foreach (ILight light in SceneToRender.SceneLights)
{
    var lightDirection = light.GetLightDirection(cameraCollision.HitPoint);
    var lightDistance = light.GetDistance(cameraCollision.HitPoint);

    //Construct ray from a point just outside the intersection point (to avoid hitting the same body) to light source. Used to find if object is in the shadows.
    var shadowRay = new Ray(cameraCollision.HitPoint + lightDirection * Globals.Epsilon, lightDirection);

    var shadowCollision = Trace(shadowRay);

    //When no collision occurs, it means there is nothing between the light source and the hit object.
    //When collision occurs, but the collision distance is larger than the distance to the light source, it means there is nothing between the light source and the hit object.
    //Calculate light contribution to color
    if (!shadowCollision.IsHit || shadowCollision.Distance > lightDistance)
    {
        //Calculate the light's contribution to the color of the pixel using the scene's shader
        pixelColor += SceneToRender.SceneShader.GetColor(cameraCollision.HitObject,
                                                            light,
                                                            ray.Direction,
                                                            lightDirection,
                                                            cameraCollision.Normal);
    }
}
 

هذه هي الـloop التي كنا نستخدمها في السابق للمرور على جميع مصادر الضوء لحساب نتيجة التظليل. في السابق كنا نكتفي بحساب اتجاه الضوء. لكننا الآن نقوم بإنشاء شعاع منفصل سميناه shadowCollision ومررناه هو الآخر في دالة Trace (أليست إعادة استخدام الكود شيئاً رائعاً؟). وكذلك حسبنا المسافة بين مصدر الضوء ونقطة الاصطدام وهذه خاصية جديدة لكلاسات مصادر الضوء سنتطرق إليها بعد قليل. إذا حدث اصطدام فهذا يعني وجود ظل، أي أننا لن نحسب لون الجسم لأن الضوء (من هذا المصدر) لا يصل إليه في هذه النقطة. عندما نعرف أن شعاع shadowCollision لم يصدم بشئ نقوم بعملية حساب اللون كما في السابق. لكن ماذا إذا كان هناك اصطدام بجسم آخر ولكنه كان خلف مصدر الضوء (تذكروا أننا نتتبع الشعاع من الجسم إلى اتجاه الضوء)؟ لن يحدث ظل هنا لذا يجب أن نحسب لون الجسم. لهذا وضعنا شرطاً إضافياً هو shadowCollision.Distance > lightDistance. عندما تكون المسافة إلى الجسم الآخر أكبر من المسافة إلى مصدر الضوء فهذا يعني أن الجسم الآخر خلف المصدر، أي أنه لا يوجد ظل.

 

نقطة إضافية، قد تكونوا قد لاحظتم أننا أنشأنا شعاع shadowCollision الذي ينطلق من نقطة الاصطدام إلى مصدر الضوء بهذه الطريقة:

 
1
var shadowRay = new Ray(cameraCollision.HitPoint + lightDirection * Globals.Epsilon, lightDirection);
 

لاحظوا أننا حددنا نقطة الإنطلاق بأنها cameraCollision.HitPoint + lightDirection * Globals.Epsilon وليس cameraCollision.HitPoint. السبب هو أننا إذا إستخدمنا نفس نقطة الاصطدام فإننا سنصدم بنفس الجسم عندما نقوم بعملية التتبع بسبب عدم دقة الأجزاء الكسرية في الحسابات. لهذا السبب ننطلق من نقطة أقرب بقليل إلى مصدر الضوء. هذه المسافة الصغيرة نحسبها بضرب متجه اتجاه الضوء بالقيمة الصغيرة Epsilon التي تحدثنا عنها في بداية هذه الدروس. النتيجة هي نقطة خارج الجسم بقليل.

 
وهذه ببساطة طريقة رسم الظلال! بقي الانعكاسات!
 
لكن دعونا لا نستعجل. قبل قليل قلت أننا أضفنا دالة جديدة لكلاسات مصادر الضوء لحساب المسافة بين مصدر الضوء ونقطة ما. لذا علينا تعديل ILight بالشكل التالي:
 
1
2
3
4
5
6
7
8
9
10
11
public interface ILight
{
    //Color of the light
    Color LightColor { get; set; }

    //Get direction vector from the target point to the light source
    Vector3D GetLightDirection(Vector3D targetPoint);

    //Get distance from targetPoint to location of light source
    double GetDistance(Vector3D targetPoint);
}
 

أضفنا الدالة الجديدة GetDistance. والآن لنطبقها على كلاس الـDirectional Light أو مصدر الضوء الاتجاهي.

 
1
2
3
4
public double GetDistance(Vector3D targetPoint)
{
    return Globals.Infinity;
}
 

بما أن هذا الضوء هو تمثيل للإضاءات الضخمة والبعيدة جداً كالشمس، ستكون المسافة بيننا وبينها ضخمة لدرجة أنها لن تؤثر على حساباتنا. أي أننا يمكننا اعتبارها بأنها لا نهاية.

 

والآن لنقدم شيئاً جديداً. نوع جديد من مصادر الضوء هو Point Light أو “النقطة المضيئة”. هذا المصدر هو عبارة عن نقطة ما في الفضاء (لها إحداثي) وتنطلق منها إشعاعات الضوء في جميع الاتجاهات. شئ كهذا:

 

 

كما تلاحظون أن إشعاعات الضوء هنا (بعكس الضوء الاتجاهي) ليست متوازية. وبما أن لدينا موقع محدد لمصدر الضوء فهذا يعني أن دالة المسافة ستعيد قيمة هنا. يمكننا تشبيه مصدر الضوء هذا بضوء المصباح. كفى كلاماُ، لنرى الكود:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class PointLight : ILight
{
    public Color LightColor { get; set; }

    //Location of the point light
    public Vector3D Location { get; set; }

    //Constructor
    public PointLight(Vector3D location, Color lightColor)
    {
        Location = location;
        LightColor = lightColor;
    }

    public Vector3D GetLightDirection(Vector3D targetPoint)
    {
        return (Location - targetPoint).Normalize();
    }

    public double GetDistance(Vector3D targetPoint)
    {
        return Vector3D.Distance(targetPoint, Location);
    }
}
 

هذا المصدر الإضافي سيساعدنا في تصوير مشاهد أكثر تعقيداً. والآن لنحول انتباهنا إلى أمر أخير، وهو الانعكاسات. مثل تكوين الظلال، الانعكاس في الـray tracing هو محاكاة لما يحدث في الواقع. هناك خامات تعكس الضوء أكثر من غيرها. سنسمي هذه الخاصية “معامل الانعكاس”، حيث عند القيمة صفر لا يحدث أي نوع من الانعكاس، وفي القيمة 1 يكون لدينا انعكاس كامل كما يحدث في المرآة. هذه الخاصية طبعاً سنحتاج أن نضيفها إلى كلاس Material كالتالي:

 
1
2
//The reflection coefficient of the material
public double ReflectionCoeff { get; set; }
 

ولا أبسط! الآن لنرى ما يحدث في الانعكاس من ناحية فيزيائية. عندما يضرب الضوء سطحاً عاكساً سوف ينعكس مسار الضوء حسب عمود السطح (هل تتذكرونه من الدرس السابق). مسار الضوء الأول ومسار الضوء المنعكس سيكونان متناظران حول عمود السطح.

 

 

ما سيحدث برمجياً عندما نتتبع شعاعاً إلى جسم عاكس هو أننا سننشئ شعاعاً جديداً في اتجاه الانعكاس ونتتبعه هو الآخر ونتيجته تنطبق على نتيجة الشعاع الأول. كما ترون في الشكل بعاليه يمكن أن يحدث هذا الشئ أكثر من مرة، لذا سيكون من المفيد أن تكون الدالة التي ستحسب هذا الأمر تنادي نفسها Recursive. هكذا يمكننا أن ننتبع أكثر من انعكاس. هذه الدالة سنسميها TraceReflection، وسنناديها من كود دالة RayTrace الأساسية فقط عندما يكون معامل الانعكاس أكبر من صفر:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
private Color RayTrace(Ray ray, int Level)
{
    //Exit function if we have exceeded maximum recursion depth
    if (Level > Globals.MaxRecursionDepth)
    {
        return new Color(); //Stop tracing
    }

    //Trace the ray
    var cameraCollision = Trace(ray);

    if (cameraCollision.IsHit)  //The ray has hit an object
    {
        var pixelColor = new Color();   //Reset light pixel's color to black

        //Iterate through the lights and calculate their contribution
        foreach (ILight light in SceneToRender.SceneLights)
        {
            var lightDirection = light.GetLightDirection(cameraCollision.HitPoint);
            var lightDistance = light.GetDistance(cameraCollision.HitPoint);

            //Construct ray from a point just outside the intersection point (to avoid hitting the same body) to light source. Used to find if object is in the shadows.
            var shadowRay = new Ray(cameraCollision.HitPoint + lightDirection * Globals.Epsilon, lightDirection);

            var shadowCollision = Trace(shadowRay);

            //When no collision occurs, it means there is nothing between the light source and the hit object.
            //When collision occurs, but the collision distance is larger than the distance to the light source, it means there is nothing between the light source and the hit object.
            //Calculate light contribution to color
            if (!shadowCollision.IsHit || shadowCollision.Distance > lightDistance)
            {
                //Calculate the light's contribution to the color of the pixel using the scene's shader
                pixelColor += SceneToRender.SceneShader.GetColor(cameraCollision.HitObject,
                                                                    light,
                                                                    ray.Direction,
                                                                    lightDirection,
                                                                    cameraCollision.Normal);
            }
        }
       
        //If reflection coeffecient is larger than 0, trace for reflection
        if (cameraCollision.HitObject.PrimitiveMaterial.ReflectionCoeff > 0 )
        {
            //Add results of the reflection tracing
            pixelColor += TraceReflection(ray, cameraCollision.Normal, cameraCollision.HitPoint, cameraCollision.HitObject, Level);
        }

        return pixelColor;
    }
    else //The ray did not hit anything
    {
        //return the background color
        return SceneToRender.BackgroundColor;
    }
}
 

والدالة نفسها:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Reflection rendering function
private Color TraceReflection(Ray ray, Vector3D normal, Vector3D hitPoint, IPrimitive hitObject, int Level)
{
    //Calculate reflection direction
    var reflectionDir = (ray.Direction - (2 * (ray.Direction * normal) * normal)).Normalize();

    //Create reflection ray from just outside the intersection point, and trace it
    var reflectionRay = new Ray(hitPoint + reflectionDir * Globals.Epsilon, reflectionDir);

    //Get the color from the reflection
    var reflectionColor = RayTrace(reflectionRay, Level + 1);

    //Calculate final color
    var resultColor = reflectionColor * hitObject.PrimitiveMaterial.ReflectionCoeff;

    return resultColor;
}
 

هل لاحظتم وجود متغير جديد يتم تبادله بين دوال RayTrace وTraceReflection اسمه Level؟ هذا المتغير سنحتفظ فيه بعمق الـrecursion. بمعني أنه كلما قمنا بعملية recursion سنزيد قيمة Level بواحد. السبب باحتفاظنا بهذا الرقم هو أننا لا نريد أن نكون في وضع تنادي فيه الدوال نفسها بشكل لا نهائي. لذا سنضع سقفاً افتراضياً لأكبر “عمق” نصل إليه. ومثله مثل قيم Infinity و Epsilon سنحتفظ به كقيمة ثابتة في ملف Globals، لنستطيع اختبار العمق هكذا:

 
1
2
3
4
5
//Exit function if we have exceeded maximum recursion depth
if (Level > Globals.MaxRecursionDepth)
{
    return new Color(); //Stop tracing
}

وربما لاحظتم أيضاً أن الدالة الآن تعيد قيمة Color بدلاً من التعامل مع كائن Pixel مباشرة كما في السابق. هذا البرنامج يتطور بشكل طبيعي، أي أنني من وقت لآخر سأغير رأيي وأعدل في هيكلة البرنامج بالشكل الذي أراه مناسباً. وقد قمت بهذا التعديل لأن هذا سيجعل الـrecursion أسهل، كما أنه سيجعل دوال تتبع الإشعاع “عديمة التأثير الجانبي” No Side Effect Function مما سيجعل أمر تحويل هذا الكود إلى كود متوازي البرمجة Parallel Programming (يقسم العمل إلى عدة أجزاء تعمل في نفس الوقت لتحسين الأداء) أمراً أسهل. وهو أمر تحدثت عنه سابقاً في سلسلة البرمجة الوظيفية: هنا وهنا وهناك.

 

الآن صار لدينا أن نقوم بتطوير المشهد السابق. سنضيف مصدر جديد للضوء من نوع PointLight وسنعطي الكرة الزرقاء معامل انعكاس 0.8 لتحتفظ كرتنا ببعض لونها الأزرق.

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
static void Main(string[] args)
{
    //Timer to measure execution time
    var time = DateTime.Now;

    //Instantiate camera
    var camera = new VerySimpleCamera();
   
    //Instantiate shader
    var shader = new DiffuseShader();
               
    //Create red sphere
    var redSphere = new Sphere(new Vector3D(0, 0, 2), 2);
    redSphere.PrimitiveMaterial = new Material(new Color(.8, .2, .2), 1.0);

    //Create reflective blue sphere behind it
    var blueSphere = new Sphere(new Vector3D(5, 2.5, 7), 4);
    blueSphere.PrimitiveMaterial = new Material(new Color(.1, .1, .7), .2, .8);

    //Create dark green plane under the spheres
    var greenPlane = new Plane(new Vector3D(0, 1, 0), 7);
    greenPlane.PrimitiveMaterial = new Material(new Color(0, .5, 0), .3);
               
    //Add 3D objects to a list
    var objects = new List<IPrimitive>();
    objects.Add(redSphere);
    objects.Add(blueSphere);
    objects.Add(greenPlane);
   
    //Create directional light
    var dirLight = new DirectionalLight(new Vector3D(1, -1, 1), new Color(.7, .7, .7));

    //Create point light
    var pointLight = new PointLight(new Vector3D(0, 10, 0), new Color(1, 1, 1));
   
    //Add lights to a list
    var lights = new List<ILight>();
    lights.Add(dirLight);
    lights.Add(pointLight);

    //Instantiate scene, using a very dark gray as the background color
    var scene = new Scene(camera, objects, new Color(.1, .1, .1), lights, shader);

    //Instantiate ray tracing engine to produce a 1000 x 1000 pixel image. Pixel size of 0.01 means the image will 10.0 x 10.0 in real world units.
    var engine = new RayTracer(scene, 1000, 1000, 0.01);

    //Render the scene
    var view = engine.RenderScene();

    //Create a GDI bitmap
    var bmp = view.ExportImage();

    //Save the image
    bmp.Save("output.bmp");

    Console.WriteLine("Ray tracing done. Execution time: {0:d} ms", (DateTime.Now - time).Milliseconds);
    Console.ReadKey();
}
 

والنتيجة:

 

 

لاحظوا وجود ظلين لكل كرة لأنه لدينا مصدرين للضوء الآن. وأصبح للسطح الأخضر تظليل بسبب أن إشعاعات نقطة الضوء ليست متوازية مما ينتج عنه انعكاسات مختلفة الدرجة على مستوى السطح. ولاحظوا أن الانعكاس على الكرة يتناسب مع شكل الكرة وليس مستوياً كالمرآة. كلها أمور نتجت عن محاكاة قوانين الفيزياء الطبيعبة.

 

يمكنكم الحصول على كود هذا الدرس من هذا الرابط.

أو يمكنكم الحصول على آخر إصدارة من كود هذه السلسلة من الدروس من موقع Google Code هذا.


المقالات في هذه السلسلة:

Post to Twitter

لا تعليقات - أضف تعليق

أضف تعليق

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *

*

يمكنك استخدام أكواد HTML والخصائص التالية: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>


مرحباً , تاريخ اليوم هو الثلاثاء, 2017/02/21