الظلال الثنائية الأبعاد على حقول المسافات الموقعة

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


[GIF ولدت القطع الأثرية الإضافية أثناء إعادة الضغط.]

التكوين الأساسي


قمت بإنشاء تكوين بسيط مع غرفة ، ويستخدم التقنيات الموضحة في البرامج التعليمية السابقة. في وقت سابق ، لم أذكر أنني استخدمت وظيفة abs للمتجه 2 لتعكس الموضع بالنسبة لمحور x و y ، وأيضًا قمت بقلب مسافة الشكل من أجل تبديل الأجزاء الداخلية والخارجية.

سنقوم بنسخ الملف 2D_SDF.cginc من البرنامج التعليمي السابق إلى مجلد واحد باستخدام التظليل ، والذي سنكتبه في هذا البرنامج التعليمي.

 Shader "Tutorial/037_2D_SDF_Shadows"{ Properties{ } SubShader{ //the material is completely non-transparent and is rendered at the same time as the other opaque geometry Tags{ "RenderType"="Opaque" "Queue"="Geometry"} Pass{ CGPROGRAM #include "UnityCG.cginc" #include "2D_SDF.cginc" #pragma vertex vert #pragma fragment frag struct appdata{ float4 vertex : POSITION; }; struct v2f{ float4 position : SV_POSITION; float4 worldPos : TEXCOORD0; }; v2f vert(appdata v){ v2f o; //calculate the position in clip space to render the object o.position = UnityObjectToClipPos(v.vertex); //calculate world position of vertex o.worldPos = mul(unity_ObjectToWorld, v.vertex); return o; } float scene(float2 position) { float bounds = -rectangle(position, 2); float2 quarterPos = abs(position); float corner = rectangle(translate(quarterPos, 1), 0.5); corner = subtract(corner, rectangle(position, 1.2)); float diamond = rectangle(rotate(position, 0.125), .5); float world = merge(bounds, corner); world = merge(world, diamond); return world; } fixed4 frag(v2f i) : SV_TARGET{ float dist = scene(i.worldPos.xz); return dist; } ENDCG } } FallBack "Standard" //fallback adds a shadow pass so we get shadows on other objects } 

إذا كنا لا نزال نستخدم تقنية التصور من البرنامج التعليمي السابق ، فسيبدو الشكل كما يلي:


ظلال بسيطة


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

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

 float traceShadow(float2 position, float2 lightPosition){ float direction = normalise(lightPosition - position); float distance = length(lightPosition - position); } 

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

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

 // outside of function #define SAMPLES 32 // in shadow function float rayDistance = 0; for(int i=0 ;i<SAMPLES; i++){ float sceneDist = scene(pos + direction * rayDistance); //do other stuff and move the ray further } 

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

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

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

 #define SAMPLES 32 float traceShadows(float2 position, float2 lightPosition){ float2 direction = normalize(lightPosition - position); float lightDistance = length(lightPosition - position); float rayProgress = 0; for(int i=0 ;i<SAMPLES; i++){ float sceneDist = scene(position + direction * rayProgress); if(sceneDist <= 0){ return 0; } if(rayProgress > lightDistance){ return 1; } rayProgress = rayProgress + sceneDist; } return 0; } 

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

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

fixed4 frag(v2f i) : SV_TARGET{ float2 position = i.worldPos.xz;

 float2 lightPos; sincos(_Time.y, lightPos.x /*sine of time*/, lightPos.y /*cosine of time*/); float shadows = traceShadows(position, lightPos); float3 light = shadows * float3(.6, .6, 1); float sceneDistance = scene(position); float distanceChange = fwidth(sceneDistance) * 0.5; float binaryScene = smoothstep(distanceChange, -distanceChange, sceneDistance); float3 geometry = binaryScene * float3(0, 0.3, 0.1); float3 col = geometry + light; return float4(col, 1); } 


الظلال الناعمة


الانتقال من هذه الظلال القاسية إلى ليونة وأكثر واقعية أمر سهل بما فيه الكفاية. في الوقت نفسه ، لا يصبح التظليل أكثر تكلفة حسابيًا.

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

 float traceShadows(float2 position, float2 lightPosition){ float2 direction = normalize(lightPosition - position); float lightDistance = length(lightPosition - position); float rayProgress = 0; float nearest = 9999; for(int i=0 ;i<SAMPLES; i++){ float sceneDist = scene(position + direction * rayProgress); if(sceneDist <= 0){ return 0; } if(rayProgress > lightDistance){ return saturate(nearest); } nearest = min(nearest, sceneDist); rayProgress = rayProgress + sceneDist; } return 0; } 


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

 float traceShadows(float2 position, float2 lightPosition, float hardness){ float2 direction = normalize(lightPosition - position); float lightDistance = length(lightPosition - position); float rayProgress = 0; float nearest = 9999; for(int i=0 ;i<SAMPLES; i++){ float sceneDist = scene(position + direction * rayProgress); if(sceneDist <= 0){ return 0; } if(rayProgress > lightDistance){ return saturate(nearest); } nearest = min(nearest, hardness * sceneDist); rayProgress = rayProgress + sceneDist; } return 0; } 

 //in fragment function float shadows = traceShadows(position, lightPos, 20); 


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

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

سنجري أيضًا تغييرًا بسيطًا واحدًا: نظرًا لأننا نقسم على rayProgress ، يجب ألا تبدأ بالرقم 0 (القسمة على الصفر هي دائمًا فكرة سيئة). كبداية ، يمكنك اختيار أي عدد صغير جدًا.

 float traceShadows(float2 position, float2 lightPosition, float hardness){ float2 direction = normalize(lightPosition - position); float lightDistance = length(lightPosition - position); float rayProgress = 0.0001; float shadow = 9999; for(int i=0 ;i<SAMPLES; i++){ float sceneDist = scene(position + direction * rayProgress); if(sceneDist <= 0){ return 0; } if(rayProgress > lightDistance){ return saturate(shadow); } shadow = min(shadow, hardness * sceneDist / rayProgress); rayProgress = rayProgress + sceneDist; } return 0; } 


مصادر الإضاءة المتعددة


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

 fixed4 frag(v2f i) : SV_TARGET{ float2 position = i.worldPos.xz; float2 lightPos1 = float2(sin(_Time.y), -1); float shadows1 = traceShadows(position, lightPos1, 20); float3 light1 = shadows1 * float3(.6, .6, 1); float2 lightPos2 = float2(-sin(_Time.y) * 1.75, 1.75); float shadows2 = traceShadows(position, lightPos2, 10); float3 light2 = shadows2 * float3(1, .6, .6); float sceneDistance = scene(position); float distanceChange = fwidth(sceneDistance) * 0.5; float binaryScene = smoothstep(distanceChange, -distanceChange, sceneDistance); float3 geometry = binaryScene * float3(0, 0.3, 0.1); float3 col = geometry + light1 + light2; return float4(col, 1); } 


شفرة المصدر


مكتبة SDF ثنائية الأبعاد (لم تتغير ، ولكنها تستخدم هنا)



ظلال ناعمة ثنائية الأبعاد



 Shader "Tutorial/037_2D_SDF_Shadows"{ Properties{ } SubShader{ //the material is completely non-transparent and is rendered at the same time as the other opaque geometry Tags{ "RenderType"="Opaque" "Queue"="Geometry"} Pass{ CGPROGRAM #include "UnityCG.cginc" #include "2D_SDF.cginc" #pragma vertex vert #pragma fragment frag struct appdata{ float4 vertex : POSITION; }; struct v2f{ float4 position : SV_POSITION; float4 worldPos : TEXCOORD0; }; v2f vert(appdata v){ v2f o; //calculate the position in clip space to render the object o.position = UnityObjectToClipPos(v.vertex); //calculate world position of vertex o.worldPos = mul(unity_ObjectToWorld, v.vertex); return o; } float scene(float2 position) { float bounds = -rectangle(position, 2); float2 quarterPos = abs(position); float corner = rectangle(translate(quarterPos, 1), 0.5); corner = subtract(corner, rectangle(position, 1.2)); float diamond = rectangle(rotate(position, 0.125), .5); float world = merge(bounds, corner); world = merge(world, diamond); return world; } #define STARTDISTANCE 0.00001 #define MINSTEPDIST 0.02 #define SAMPLES 32 float traceShadows(float2 position, float2 lightPosition, float hardness){ float2 direction = normalize(lightPosition - position); float lightDistance = length(lightPosition - position); float lightSceneDistance = scene(lightPosition) * 0.8; float rayProgress = 0.0001; float shadow = 9999; for(int i=0 ;i<SAMPLES; i++){ float sceneDist = scene(position + direction * rayProgress); if(sceneDist <= 0){ return 0; } if(rayProgress > lightDistance){ return saturate(shadow); } shadow = min(shadow, hardness * sceneDist / rayProgress); rayProgress = rayProgress + max(sceneDist, 0.02); } return 0; } fixed4 frag(v2f i) : SV_TARGET{ float2 position = i.worldPos.xz; float2 lightPos1 = float2(sin(_Time.y), -1); float shadows1 = traceShadows(position, lightPos1, 20); float3 light1 = shadows1 * float3(.6, .6, 1); float2 lightPos2 = float2(-sin(_Time.y) * 1.75, 1.75); float shadows2 = traceShadows(position, lightPos2, 10); float3 light2 = shadows2 * float3(1, .6, .6); float sceneDistance = scene(position); float distanceChange = fwidth(sceneDistance) * 0.5; float binaryScene = smoothstep(distanceChange, -distanceChange, sceneDistance); float3 geometry = binaryScene * float3(0, 0.3, 0.1); float3 col = geometry + light1 + light2; return float4(col, 1); } ENDCG } } FallBack "Standard" } 

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

Source: https://habr.com/ru/post/ar438942/


All Articles