"لقد فعل ذلك مرة أخرى!" - هذا هو ما حدث لي أول شيء عندما نظرت إلى الجزء الخلفي من نشرة Pixar
[1] ، المليئة بالكامل بالكود. تم توقيع مجموعة من الإنشاءات والعبارات في الزاوية اليمنى السفلى من قبل أندرو كينسلر. بالنسبة لأولئك الذين لا يعرفونه ، سأقول: أندرو هو مبرمج اخترع
جهاز تتبع بحجم 1337 بايت
بحجم بطاقة العمل في عام 2009.
هذه المرة ، توصل أندرو إلى شيء أكثر ضخامة ، ولكن بنتيجة مرئية أكثر إثارة للاهتمام. منذ أن أنهيت كتابتي Game Engine Black Books عن
Wolf3D و
DOOM ، كان لدي وقت لتعلم الدواوين الشفرة المشفرة. وعلى الفور تقريبًا ، كنت مفتونًا حقًا بالتقنيات المكتشفة فيه. كانت مختلفة تمامًا عن عمل أندرو السابق ، بناءً على جهاز تتبع شعاع "قياسي". لقد كنت مهتمًا بالتعرف على مسيرة الشعاع ، وخصائص الهندسة الحجرية البنّاءة ، وتقديم / مسار مونت كارلو ، بالإضافة إلى العديد من الحيل الأخرى التي استخدمها للضغط على الكود في قطعة صغيرة من الورق.
شفرة المصدر
الجزء الأمامي من المنشور هو إعلان لقسم التوظيف في بيكسار. على الجانب العكسي ، تتم طباعة 2037 بايت من رمز C ++ ، معتمًا بحيث تشغل أصغر سطح ممكن.
#include <stdlib.h> // card > pixar.ppm #include <stdio.h> #include <math.h> #define R return #define O operator typedef float F;typedef int I;struct V{F x,y,z;V(F v=0){x=y=z=v;}V(F a,F b,F c=0){x=a;y=b;z=c;}V O+(V r){RV(x+rx,y+ry,z+rz);}VO*(V r){RV(x*rx,y*r. y,z*rz);}FO%(V r){R x*r.x+y*r.y+z*rz;}VO!(){R*this*(1/sqrtf(*this%*this) );}};FL(F l,F r){R l<r?l:r;}FU(){R(F)rand()/RAND_MAX;}FB(V p,V l,V h){l=p +l*-1;h=h+p*-1;RL(L(L(lx,hx),L(ly,hy)),L(lz,hz));}FS(V p,I&m){F d=1\ e9;V f=p;fz=0;char l[]="5O5_5W9W5_9_COC_AOEOA_E_IOQ_I_QOUOY_Y_]OWW[WaOa_aW\ eWa_e_cWiO";for(I i=0;i<60;i+=4){V b=V(l[i]-79,l[i+1]-79)*.5,e=V(l[i+2]-79,l [i+3]-79)*.5+b*-1,o=f+(b+e*L(-L((b+f*-1)%e/(e%e),0),1))*-1;d=L(d,o%o);}d=sq\ rtf(d);V a[]={V(-11,6),V(11,6)};for(I i=2;i--;){V o=f+a[i]*-1;d=L(d,ox>0?f\ absf(sqrtf(o%o)-2):(o.y+=oy>0?-2:2,sqrtf(o%o)));}d=powf(powf(d,8)+powf(pz, 8),.125)-.5;m=1;F r=L(-L(B(p,V(-30,-.5,-30),V(30,18,30)),B(p,V(-25,17,-25),V (25,20,25))),B(V(fmodf(fabsf(px),8),py,pz),V(1.5,18.5,-25),V(6.5,20,25))) ;if(r<d)d=r,m=2;F s=19.9-py;if(s<d)d=s,m=3;R d;}IM(V o,V d,V&h,V&n){I m,s= 0;F t=0,c;for(;t<100;t+=c)if((c=S(h=o+d*t,m))<.01||++s>99)R n=!V(S(h+V(.01,0 ),s)-c,S(h+V(0,.01),s)-c,S(h+V(0,0,.01),s)-c),m;R 0;}VT(V o,V d){V h,n,r,t= 1,l(!V(.6,.6,1));for(I b=3;b--;){I m=M(o,d,h,n);if(!m)break;if(m==1){d=d+n*( n%d*-2);o=h+d*.1;t=t*.2;}if(m==2){F i=n%l,p=6.283185*U(),c=U(),s=sqrtf(1-c), g=nz<0?-1:1,u=-1/(g+nz),v=nx*ny*u;d=V(v,g+ny*ny*u,-ny)*(cosf(p)*s)+V( 1+g*nx*nx*u,g*v,-g*nx)*(sinf(p)*s)+n*sqrtf(c);o=h+d*.1;t=t*.2;if(i>0&&M(h +n*.1,l,h,n)==3)r=r+t*V(500,400,100)*i;}if(m==3){r=r+t*V(50,80,100);break;}} R r;}I main(){I w=960,h=540,s=16;V e(-22,5,25),g=!(V(-3,4,0)+e*-1),l=!V(gz, 0,-gx)*(1./w),u(gy*lz-gz*ly,gz*lx-gx*lz,gx*ly-gy*lx);printf("P\ 6 %d %d 255 ",w,h);for(I y=h;y--;)for(I x=w;x--;){V c;for(I p=s;p--;)c=c+T(e ,!(g+l*(xw/2+U())+u*(yh/2+U())));c=c*(1./s)+14./241;V o=c+1;c=V(cx/ox,c. y/oy,cz/oz)*255;printf("%c%c%c",(I)cx,(I)cy,(I)cz);}}// Andrew Kensler
هل هو حتى العمل؟
مع رمز هناك تعليمات لإطلاقها. الفكرة هي إعادة توجيه الإخراج القياسي إلى ملف. بالامتداد ، يمكننا أن نفترض أن تنسيق الإخراج هو تنسيق صورة نصية يسمى NetPBM
[2] .
$ clang -o card2 -O3 raytracer.cpp
$ time ./card> pixar.ppm
2m58.524s الحقيقي
المستخدم 2m57.567s
تميز الكلية 0m0.415s
بعد دقيقتين وثمانية وخمسين ثانية
[3] ، يتم إنشاء الصورة التالية. إنه لأمر مدهش كم هو مطلوب رمز لذلك.
يمكنك استخراج الكثير من الصورة أعلاه. حصى هو علامة واضحة على "مسار التتبع". هذا النوع من العارض يختلف عن التصوير الشعاعي لأنه لا يتم تتبع الأشعة مرة أخرى إلى مصادر الضوء. في هذه الطريقة ، تنبعث آلاف الأشعة لكل بكسل من المصادر ويقوم البرنامج بمراقبتها ، على أمل أن يعثروا على مصدر الضوء. هذه تقنية مثيرة للاهتمام ، أفضل بكثير من تتبع الشعاع ، والتي يمكنها التعامل مع التجسيد المحيط ، والظلال الناعمة ، والمواد الكاوية ، والإشعاع.
سنقوم بتقسيم الكود إلى أجزاء
يؤدي تمرير الإدخال إلى CLion إلى تهيئة الرمز (انظر الإخراج
هنا ) ويقسمه إلى أجزاء / مهام أصغر.
#include <stdlib.h> // card> pixar.ppm
# تضمين <stdio.h>
# تتضمن <math.h>
# حدد عودة R
# تعريف عامل التشغيل O
typedef تعويم F ؛ typedef int I؛
البنية V {F x، y، z؛ V (F v = 0) {x = y = z = v؛} V (F a، F b، F
c = 0) {x = a؛ y = b؛ z = c؛} V O + (V r) {RV (x + rx، y + ry، z + rz)؛} VO * (V r) {RV ( س * ص ، ص * ص.
y، z * rz)؛} FO٪ (V r) {R x * r.x + y * r.y + z * rz؛} VO! () {R * this * (1 / sqrtf (* this٪ * هذا)
) ؛}} ؛
FL (F l، F r) {R l <r؟ L: r؛} FU () {R (F) rand () / RAND_MAX؛} FB (V p، V l، V h) {l = p
+ l * -1 ؛ h = h + p * -1 ؛ RL (L (L (L (L (lx، hx) و L (ly، hy))، L (lz، hz))؛}
FS (V p ، I & m) {F d = 1 \
e9 ؛ V f = p ؛ fz = 0؛ char l [] = "5O5_5W9W5_9_COC_AOEOA_E_IOQ_I_QOUOY_Y_] OWW [WaOa_aW \
eWa_e_cWiO "؛ لـ (I i = 0؛ i <60؛ i + = 4) {V b = V (l [i] -79، l [i + 1] -79) *. 5، e = V (l [ i + 2] -79 ، ل
[i + 3] -79) *. 5 + b * -1 ، o = f + (b + e * L (-L ((b + f * -1)٪ e / (e٪ e) ، 0) ، 1)) * - 1 ؛ d = L (d ، o٪ o) ؛} d = sq \
rtf (d) ؛ V a [] = {V (-11.6) ، V (11.6)} ؛ من أجل (I i = 2 ؛ i - ؛) {V o = f + a [i] * -1 ؛ د = L (د ، ثور> 0؟ F \
absf (sqrtf (o٪ o) -2) :( o.y + = oy> 0؟ -2: 2، sqrtf (o٪ o)))؛} d = powf (powf (d، 8) + powf (pz ،
8) ، 125) - .5 ؛ م = 1 ؛ F r = L (-L (B (p ، V (-30 ، -. 5 ، -30) ، V (30،18،30)) ، B (ص ، الخامس (-25.17 ، -25) ، الخامس
(25،20،25))) ، B (V (fmodf (fabsf (px) ، 8) ، py ، pz) ، V (1.5،18.5، -25)، V (6.5،20،25)))
؛ إذا (r <d) d = r ، m = 2 ؛ F s = 19.9-py ؛ إذا (s <d) d = s، m = 3؛ R d؛}
IM (V o و V d و V & h و V & n) {I m، s =
0 ؛ F t = 0 ، c ؛ لـ (؛ t <100 ؛ t + = c) if ((c = S (h = o + d * t، m)) <. 01 || ++ s> 99) R n =! V (S (h + V (.01،0
)، s) -c، S (h + V (0، .01)، s) -c، S (h + V (0،0، .01)، s) -c)، m؛ R 0؛}
VT (V o، V d) {V h، n، r، t =
1 ، l (! V (.6 ، .6 ، 1)) ؛ من أجل (I b = 3 ؛ b - ؛) {I m = M (o ، d ، h ، n) ؛ إذا كسر (! M) ؛ إذا (م == 1) {د = د + ن * (
n٪ d * -2) ؛ o = h + d * .1 ؛ t = t * .2؛} إذا (m == 2) {F i = n٪ l، p = 6.283185 * U ()، c = U () ، s = sqrtf (1-c) ،
g = nz <0؟ -1: 1، u = -1 / (g + nz)، v = nx * ny * u؛ d = V (v، g + ny * ny * u، -ny) * (cosf (ع) * s) + V (
1 + g * nx * nx * u، g * v، -g * nx) * (sinf (p) * s) + n * sqrtf (c)؛ o = h + d * .1؛ t = t *. 2 ؛ إذا (i> 0 && M (h
+ n * .1 ، l ، h ، n) == 3) r = r + t * V (500،400،100) * i؛} إذا (m == 3) {r = r + t * V (50،80،100) ؛ break ؛}}
ص ص ؛}
I main () {I w = 960، h = 540، s = 16؛ V e (-22،5،25)، g =! (V (-3،4،0) + e * -1)، l =! V (gz ،
0، -xx) * (1./w) ، u (gy * lz-gz * ly ، gz * lx-gx * lz ، gx * ly-gy * lx) ؛ printf ("P \
6٪ d٪ d 255 "، w ، h) ؛ من أجل (I y = h ؛ y - ؛) لـ (I x = w ؛ x - ؛) {V c ؛ من أجل (I p = s؛ p- - ؛) ج = ج + T (ه
،! (g + l * (xw / 2 + U ()) + u * (yh / 2 + U ()))) ؛ c = c * (1./s) + 14. / 241 ؛ V o = c + 1 ؛ c = V (cx / ox ، c.
y / o، cz / oz) * 255؛ printf ("٪ c٪ c٪ c"، (I) cx، (I) cy، (I) cz)؛}}
// أندرو كينسلر
يوصف كل قسم بالتفصيل في بقية المقال:
t - الحيل العادية ،
■ - فئة المتجهات ، code - التعليمات البرمجية المساعدة ، database - قاعدة البيانات ،
mar - مسيرة راي ،
■ - أخذ العينات ، main - الكود الرئيسي.
الحيل المشتركة مع #define و typedef
تستخدم الحيل الشائعة #define و typedef لتقليل مقدار التعليمات البرمجية بشكل ملحوظ. هنا نشير إلى F = float و I = int و R = return و O = عامل التشغيل. الهندسة العكسية تافهة.
الدرجة الخامسة
بعد ذلك تأتي الفئة V ، التي قمت بإعادة تسميتها إلى Vec (على الرغم من ذلك ، كما سنرى أدناه ، يتم استخدامه أيضًا لتخزين قنوات RGB بتنسيق عائم).
struct Vec { float x, y, z; Vec(float v = 0) { x = y = z = v; } Vec(float a, float b, float c = 0) { x = a; y = b; z = c;} Vec operator+(Vec r) { return Vec(x + rx, y + ry, z + rz); } Vec operator*(Vec r) { return Vec(x * rx, y * ry, z * rz); }
لاحظ أنه لا يوجد عامل طرح (-) ، لذا بدلاً من كتابة "X = A - B" ، يتم استخدام "X = A + B * -1". يأتي الجذر التربيعي العكسي مفيدًا في وقت لاحق لتطبيع المتجهات.
الوظيفة الرئيسية
main () هو الحرف الوحيد الذي لا يمكن حجبه لأنه يتم استدعاؤه بواسطة الدالة _start لمكتبة libc. عادةً ما يستحق البدء به ، لأنه سيكون من الأسهل العمل بهذه الطريقة. استغرق الأمر مني بعض الوقت لمعرفة معاني الحروف الأولى ، ولكن لا يزال بإمكاني إنشاء شيء يمكن قراءته.
int main() { int w = 960, h = 540, samplesCount = 16; Vec position(-22, 5, 25); Vec goal = !(Vec(-3, 4, 0) + position * -1); Vec left = !Vec(goal.z, 0, -goal.x) * (1. / w);
لاحظ أن القيم الحرفية العائمة لا تحتوي على الحرف "f" ، ويتم تجاهل الجزء الكسري لتوفير المساحة. يتم استخدام نفس الخدعة أدناه ، حيث يتم إسقاط الجزء الصحيح (تعويم x = 0.5). من غير المألوف أيضًا إنشاء "for" مع تعبير التكرار الذي يتم إدراجه داخل شرط الاستراحة.
هذه هي الوظيفة الرئيسية القياسية جدا لتتبع الأشعة / المسار. يتم تعيين متجهات الكاميرا هنا وتنبعث الأشعة لكل بكسل. الفرق بين جهاز تتبع الشعاع وجهاز تتبع المسار هو أن العديد من الأشعة تنبعث لكل بكسل في TP ، والتي تحولت بشكل عشوائي قليلاً. ثم يتم تجميع اللون الناتج لكل شعاع في البيكسل في ثلاث قنوات عائمة R و B و G. في النهاية ، يتم إجراء تصحيح الدرجة اللونية لنتائج طريقة راينهارت.
الجزء الأكثر أهمية هو sampleCount ، والذي يمكن من الناحية النظرية تعيينه على 1 لتسريع التقديم والتكرار. فيما يلي نماذج للنتائج مع قيم عينة من 1 إلى 2048.
المفسد العنوان
1

2

4

8

16

32

64

128

256

512

1024

2048
كود المساعد
آخر قطعة بسيطة من التعليمات البرمجية هي وظائف المساعد. في هذه الحالة ، لدينا دالة تافهة min () ، ومولد قيمة عشوائية في الفاصل الزمني [0،1] و boxTest () الأكثر إثارة للاهتمام ، وهو جزء من نظام Constructive Solid Geometry (CSG) المستخدم لقطع العالم. تمت مناقشة CSG في القسم التالي.
float min(float l, float r) { return l < r ? l : r; } float randomVal() { return (float) rand() / RAND_MAX; }
وظائف الهندسة الحجرية البناءة
لا توجد رؤوس في الكود. كل شيء يتم باستخدام وظائف CSG. إذا كنت غير معتادًا عليها ، فقل ببساطة أن هذه وظائف تصف ما إذا كان الإحداثي داخل الكائن أم خارجه. إذا أرجعت الدالة مسافة موجبة ، فستكون النقطة داخل الكائن. تشير المسافة السالبة إلى أن النقطة تقع خارج الكائن. هناك العديد من الوظائف لوصف الكائنات المختلفة ، ولكن من أجل التبسيط ، لنأخذ على سبيل المثال كرة ونقطتين ، A و B.
ترجع الدالة testSphere () -1 للنقطة A (أي ، هي خارج) و 1 لـ B (أي ، هي داخل). تعتبر العلامات على المسافات مجرد خدعة ، حيث تتيح لك الحصول على قسمين من المعلومات بدلاً من واحدة في حالة قيمة واحدة. يمكن أيضًا كتابة نوع مماثل من الوظائف لوصف متوازي الاضلاع (هذا هو بالضبط ما يتم تنفيذه في الدالة BoxTest).
الآن دعونا نرى ما يحدث إذا قلبت علامة القيمة المرجعة.
الآن نحن لا نصف كائنًا صلبًا ، ولكننا أعلنا العالم كله صلبًا ونقطع مساحة فارغة فيه. يمكن استخدام الوظائف كطوب بناء ، والتي يمكن عند الجمع بينها وصف أشكال أكثر تعقيدًا. باستخدام عامل الإضافة المنطقية (دالة دقيقة) ، يمكننا قطع زوج من المستطيلات واحدًا فوق الآخر وستبدو النتيجة هكذا.
إذا كنت تفكر في الأمر ، فإنه يبدو وكأنه الغرفة التي ندرسها ، لأنه يتم التعبير عن الغرفة السفلية بهذه الطريقة بالضبط - بمساعدة اثنين من متوازي الأضلاع.
الآن ، بعد إتقان المعرفة القوية لـ CSG ، يمكننا العودة إلى الكود والنظر في وظيفة قاعدة البيانات ، والتي هي الأصعب في التعامل معها.
#define HIT_NONE 0 #define HIT_LETTER 1 #define HIT_WALL 2 #define HIT_SUN 3
يمكنك أن ترى هنا وظيفة "الاستغناء" عن متوازي الاضلاع ، حيث يتم استخدام مستطيلين فقط لبناء الغرفة بأكملها (يعمل عقولنا بالباقي ، ويمثل الجدران). السلم الأفقي عبارة عن وظيفة CSG أكثر تعقيدًا باستخدام تقسيم الباقي. وأخيرًا ، تتكون أحرف كلمة PIXAR من 15 سطرًا مع زوج "أصل / دلتا" وحالتين خاصتين لمنحنيات في الحرفين P و R.
راي يسير
امتلاك قاعدة بيانات لوظائف CSG التي تصف العالم ، يكفي أن نتخطى جميع الأشعة المنبعثة في الوظيفة الرئيسية (). راي مسيرة يستخدم وظيفة المسافة. هذا يعني أن موضع أخذ العينات ينتقل للأمام مسافة إلى أقرب عقبة.
إن فكرة مسيرة الشعاع على أساس المسافة هي المضي قدمًا إلى أقرب كائن. في النهاية ، ستقترب الحزمة من السطح لدرجة أنه يمكن اعتبارها نقطة وقوع.
لاحظ أن مسيرة الشعاع لا تُرجع تقاطعًا حقيقيًا مع السطح ، ولكن تقريبًا. هذا هو السبب في توقف المسيرة في الكود عند d <0.01f.
وضع كل ذلك معا: أخذ العينات
التحقيق في مسار التتبع قد اكتمل تقريبًا. نحن نفتقد الجسر الذي يربط الوظيفة الرئيسية () بمسيرة الشعاع. هذا الجزء الأخير ، الذي قمت بتسميته "تتبع" ، هو "الدماغ" الذي ترتد فيه الأشعة أو تتوقف ، حسب ما تواجهه.
Vec Trace(Vec origin, Vec direction) { Vec sampledPosition, normal, color, attenuation = 1; Vec lightDirection(!Vec(.6, .6, 1));
لقد جربت قليلاً مع هذه الوظيفة لتغيير الحد الأقصى لعدد انعكاسات الحزمة المسموح بها. القيمة "2" تعطي الحروف لون Vantablack المطلي بشكل جميل بشكل مدهش
[4] .
1234تنظيفها تماما شفرة المصدر
لوضع كل شيء معًا ، قمت بإنشاء
شفرة مصدر نظيفة تمامًا.
المراجع
[1] المصدر:
منشور Twitter lexfrench في 8 أكتوبر 2018.[2] المصدر:
تنسيق صورة NetPBM[3] المصدر:
أداء التصور على أقوى جهاز MacBook Pro ، 2017[4] المصدر:
ويكيبيديا: فانتابلاك