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

مشروع: تطبيق Ray Tracing باستخدام C‎#‎ – الحلقة 2 – الإضاءة والتظليل


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

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

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

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


هناك عدة طرق لحساب كمية الإضاءة التي يتلقاها الجسم. ولكن من أسهلها ما يدعى بالـdiffuse shading أو التظليل الانتشاري. وهو ما سنطبقه اليوم. الفكرة هي أن الأجسام عادةً تمتص جزء من الضوء وتنشر الباقي. وكمية هذا الباقي التي يعكسها الجسم يعتمد على حساب الزاوية بين شعاع الضوء وعمود السطح. قد لا يكون مصطلح “عمود السطح” واضحاً للجميع لذا لنعرّفه أولاً. إذا نظرنا إلى النقطة التي يصطدم فيها شعاع الضوء بسطح الجسم، وأقمنا فيها خطاً يكون عمودياً على السطح في هذه النقطة بالذات نسمي هذا الخط بعمود السطح. يمكننا حساب كمية الضوء التي تصل إلى هذا الجزء من الجسم بحساب الزاوية بين شعاع الضوء وعمود السطح. عندما تكون هذه الزاوية صفراً سنعتبر أن الضوء وصل كاملاً إلى النقطة. وكلما زادت الزاوية قلت كمية الضوء. حتى نصل إلى الزاوية 90 (وأكثر) التي تعني أن الضوء لم ينر النقطة بتاتاً. سنستخدم هذه المعادلة المبنية على قانون لامبرت للانعكاس:

L = d \cdot n \times c

d هي المتجه من نقطة الاصطدام نحو مصدر الضوء. و n هو اتجاه عمود السطح. النقطة بينهما هي عملية ضرب قياسي أو dot product. و c هو معامل الانتشار أو diffuse coefficient، وهو عدد بين صفر و 1 يصف طبيعة الجسم فيما يتعلق بعكس الضوء. كلما كان المعامل كبيراً كلما زادت كمية الضوء المنعكس. في النهاية يصبح لدينا القيمة L. إذا كانت جميع المتجهات normalized فستكون قيمة L بين الصفر وواحد. نقوم بضرب هذه القيمة في قيمة لون الجسم وقيمة لون الضوء، والناتج هو اللون الذي سنراه.

والآن لننظر كيف سيغير هذا الكلام طريقة عمل الـray tracing.

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

لهذا علينا الآن تعديل الكلاس Collision ليعيد لنا مجموعة قيم جديدة. علينا معرفة إحداثي نقطة الاصطدام. وعلينا معرفة متجه عمود السطح (وهي أشياء سنحسبها داخل كلاس الجسم).

1
2
3
4
5
        //coordinates of the intersection point
        public Vector3D HitPoint { get; set; }

        //Normal of the hit object at the intersection point
        public Vector3D Normal { get; set; }

سنضع طبعاً حسابات نقطة الاصطدام وعمود السطح داخل كلاس الكرة Sphere. ولكن لِم لا نقوم أيضاً بإضافة مجسم جديد؟ التالي هو كود لتوصيف مستوى أو plane. وهو سطح ثنائي البعد غير نهائي. يمكننا وصفه بخاصيتين هي متجه المحور العمودي للمستوى، ومقدار إزاحة هذا المستوى من نقطة الأصل (0,0,0). لاحظوا طريقة استخدام Collision الجديدة، حيث نعيد أيضاً إحداثي نقطة الاصطدام وعمود السطح (الذي هو نفسه متجه المحور العمودي في حالة المستوى).

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
    public class Plane: IPrimitive
    {        
        public Material PrimitiveMaterial{get;set;}

        //Normal vector of the plane
        public Vector3D Normal { get; set; }
       
        //Offset from origin point
        public double Offset { get; set; }

        public Plane(Vector3D normal, double offset)
        {
            Normal = normal.Normalize();
            Offset = offset;
        }

        public Collision Intersect(Ray ray)
        {
            var dot = Normal * ray.Direction;     //When the dot product is zero the ray is parallel to the plane, and we can't see the plane.
            if (dot != 0)
            {
                var distance = -((Normal * ray.Origin) + Offset) / dot;
                if (distance > 0)   //when the distance is positive it means the plane is in front of us
                {
                    var hitPoint = ray.Origin + (distance * ray.Direction);
                    return new Collision(true, this, distance, Normal, hitPoint);
                }
            }
            return new Collision(false);
        }
    }

وهنا الكرة مع دالة لحساب عمود السطح:

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
    public class Sphere : IPrimitive
    {
        //Coordinates of the center of the sphere
        public Vector3D Center { get; set; }

        //The radius of the sphere
        public double Radius { get; set; }

        //Constructor
        public Sphere(Vector3D center, double radius)
        {
            Center = center;
            Radius = radius;
        }

        public Material PrimitiveMaterial { get; set; }

        //Intersection formula was taken from http://wiki.cgsociety.org/index.php/Ray_Sphere_Intersection
        public Collision Intersect(Ray ray)
        {
            var A = ray.Direction * ray.Direction;
            var B = 2 * (ray.Origin - Center) * ray.Direction;
            var C = (ray.Origin - Center) * (ray.Origin - Center) - Radius * Radius;

            var d = B * B - 4 * A * C;
            if (d < 0)
                return new Collision(false);    //No collision. Ray does not hit sphere.

            var sqrtD = Math.Sqrt(d);

            double q;
            if (B < 0)
                q = (-B - sqrtD) / 2;
            else
                q = (-B + sqrtD) / 2;

            var t0 = q / A;
            var t1 = C / q;

            if (t0 > t1)
            {
                var temp = t0;
                t1 = t0;
                t0 = temp;
            }

            if (t1 < 0)
                return new Collision(false);    //No collision. Sphere is behind origin point.

            if (t0 < 0)
            {
                //Origin inside sphere                
                var hitPoint1 = ray.Origin + ray.Direction * t1;
                return new Collision(true, this, t1, GetNormal(hitPoint1), hitPoint1);   //Collision. Origin point is inside sphere.
            }

            var hitPoint0 = ray.Origin + ray.Direction * t0;
            return new Collision(true, this, t0, GetNormal(hitPoint0), hitPoint0);       //Collision. Origin point is outside sphere.
        }

        //Get normal vector of the sphere's surface at hit point location
        private Vector3D GetNormal(Vector3D hitPoint)
        {
            var n = hitPoint - Center;
           
            var temp = 1 / Math.Sqrt(n * n);

            n = temp * n;

            return n;
        }
    }

لدينا أيضاً خاصية جديدة لخامة الجسم Material وهي معامل الانتشار.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    public class Material
    {
        //The diffuse color of the material
        public Color DiffuseColor { get; set; }

        //The diffuse coefficient of the material
        public double  DiffuseCoeff { get; set; }

        //Constructors
        public Material(Color diffuseColor)
        {
            DiffuseColor = diffuseColor;
            DiffuseCoeff = 1;
        }

        public Material(Color diffuseColor, double diffuseCoeff)
        {
            DiffuseColor = diffuseColor;
            DiffuseCoeff = diffuseCoeff;
        }
    }

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

1
2
3
4
5
6
7
8
    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);
    }

وبناءً على هذا سنبني كلاس الـdirectional light الذي له خاصية إضافية واحدة فقط هو اتجاه الضوء الموحد. لذا سيكون ناتج الدالة GetLightDirection هو عكس اتجاه الضوء. ولا أسهل!

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

        //Vector of the direction the light shines
        public Vector3D Direction { get; set; }

        //Constructor
        public DirectionalLight(Vector3D direction, Color lightColor)
        {
            Direction = direction.Normalize();
            LightColor = lightColor;
        }
       
        public Vector3D GetLightDirection(Vector3D targetPoint)
        {
            return -Direction;
        }

    }

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

1
2
3
4
5
6
7
    public interface IShader
    {
        //Gets the color we see, according the 3D object's properties, the light source properties, direction of the viewing ray,
        //the direction from the intersection point to the light source, and the normal vector on the surface of the 3D object at
        //the intersection point.
        Color GetColor(IPrimitive HitObject, ILight Light, Vector3D ViewDirection, Vector3D LightDirection, Vector3D Normal);
    }

وهنا كلاس الـDiffuse shader المبني عليه.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    public class DiffuseShader : IShader
    {
        public Color GetColor(IPrimitive HitObject, ILight Light, Vector3D ViewDirection, Vector3D LightDirection, Vector3D Normal)
        {
            //If diffuse coeffecient is zero, then no shading occurs
            if (HitObject.PrimitiveMaterial.DiffuseCoeff > 0)
            {
                var dot = LightDirection * Normal;  //if the dot product is zero or less that means the angle between the two vectors is 90 or more and no diffuse occurs.
                if (dot > 0)
                {
                    var diff = dot * HitObject.PrimitiveMaterial.DiffuseCoeff;                  //Calculate strength of the diffuse
                    return diff * HitObject.PrimitiveMaterial.DiffuseColor * Light.LightColor;  //Calculate result color
                }
            }

            return new Color();            
        }  
    }

المشهد لديه عناصر جديدة الآن. الأضواء والـshader بالتحديد. لذا علينها إضافتهم له.

1
2
3
4
5
        //List of all light sources in the scene
        public List<ILight> SceneLights { get; set; }

        //Shader to be used in the scene
        public IShader SceneShader { get; set; }

حسناً لدينا كل الكائنات التي سنحتاجها. بقي فقط تعديل كود الـray tracer نفسه. التغيير سيكون في الجزء الذي يحسب لون البيكسل في حالة الاصطدام. حيث سنمر على جميع مصادر الضوء، ونولد منها متجه الضوء الذي سنستخدمه في الـshader الخاص بالـscene لينتج لنا في النهاية لون البيكسل.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
            if (cameraCollision.IsHit)  //The ray has hit an object
            {
                var pixelColor = new Color();   //Reset 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);

                    //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);
                }

                //Set the pixel's color to be the calculated color
                TargetPixel.PixelColor = pixelColor;
            }

وخلاص! هكذا أصبح الـray tracer البدائي أكثر تطوراً بقليل. بقي أن نجربه. سنضيف مستوى أخضر للمشهد تحت الكرتين، بالإضافة إلى directional light بزاوية، وبالطبع الـshader ليصبح لدينا الآن:

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
        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), .9);

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

            //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));

            //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));

            //Add lights to a list
            var lights = new List<ILight>();
            lights.Add(dirLight);

            //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 400 x 400 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, 400, 400, 0.025);

            //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();
        }

وها هنا نتيجة درس اليوم:

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

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

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


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

Post to Twitter

5 تعليق - أضف تعليق
  1. احمد قدوري قال:

    بارك الله فيك اخي العزيز..والله انه موضوع مشوق ومفيد جدا ويتميز باللغه السلسه المفهومه..بالمناسبه انا طالب ماجستير وموضوع رسالتي هو تعقب الشعاع وكان موضوه اكثر من مفيد لي..

    • System Down قال:

      سعدت يكون موضوعي نال إعجابك. :)

      على ذكر موضوع الماجستير، أنا بدأت اهتم قي هذا الموضوع (Ray tracing) عن طريق طالبة ماجستير تدرس هذا الموضوع وهي زوجتي العزيزة. أنصحك بكتاب Ray Tracing from the Ground Up وهو الكناب الذي تستخدمه زوجتي. كتاب شامل جداً وأسلوبه سلس ويحوي العديد من الأكواد (بلغة ‎C‎+‎+‎)، واستقيت الكثير من معلوماتي منه.

أضف تعليق

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

*

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


مرحباً , تاريخ اليوم هو الخميس, 2017/03/23