7O@1sqhx&yLsNK`~bt2;#|%57r+O+ZjkMnj`5g2m(mbI=71#|IH*%P{|C z9%tr@%bEw+{Jvi6g%~AH$h=+)w}XoX%`F$g`(F? zLhkp4ByUtijInH`4rp?bK6vmzJc9Z3ebG?Lj#7u^lj92qnaZ5ml%y;h%oqAr;xb4R zaeioNFxGO#j(O+~qGUFXwYKN!&bqv!dQXv77Iw}0L9czln}I$8&4uk5whK}^bKRrH zv250pB8*H-r_Y^BRLs(7ts9do%Lo$tA%pXSP1CKB137vDGY8`}zD(Nh{U}d3iM@FJ zS)hwJTSZ>JC!ycWe{i9YG)T)roZHvecdcJ1#A>}qG4#a?5hJ7P0T&SZyPGQ+1dA-H zVpb$3##v1iKhHmwP$1!vcVV=u+}b3AeAStkxE()Oaz+O|G7qTrjHB*1Wo>G1=1I%T z6MDO|vm+}lZ7E)BE-8t5 uOtA88 Bp)UZTH*=_`DTbvkZ*YN{v6|A`Osqjj+g54lIqJ=tCj4UMwBjUmSBpGJ%Mo$Ks} zE7u=N-g0){w$hCLdB$hUu3o*0_G)r^x|c|DEL-Q)v6C3Hj6PSdU1Ko(@M_{|Z!ihh z)eO34y>&qsiCB!~3fq!DPCr-FIHJ{?u8gLur^k{~iL2hW-o@v-`F4illIMpfa1;Cd z`TK>1g?K(>wzo-SZ7Lo2w`O=eM;2{4-&pw{5s{Rn GP`xU}?Rxs(s%o7>wt zCVh0|6cj6KYoSN6z3Gy4?3V^>KfJ!Pu*kGfGODF49NO5ecP3N6)e&Yf4%|rUk>f=a zPvW4By6-G0)b~Wt=XViv+20%=M~!y3T`8cm^IlGFuBv>pM9 !>t2#idmBR)_Do9S zSBp&ta#(9iO$Qk1%D;B%*L3p{5EDNN2?_ZcnEz#CgOZD@R9 _YFqL!3 zZy`G^9dCW?U_V@pu6Xi=T4-WoB71aOy!()4`l^zO%3aV{BocY 5_%aiAalzBOk%+OihJrPDk0ay@o>+ zV}YNKpPOW4N69r`GMXsVZ>MfyoihzBFyCFYD3dYp06i`ob9af}d-(9-O_e;qmrMqX z7NcdX$vyWX=)wc=X%ybNZmpiIAB-fpB`A1;)b+~|o9UJ+^FjwlqTT|ND$f(*k*w!( z2ZnD%4n6IKt!zKo8#`EOVQN25NT@{hnJUJyI(JcDwaau_V05}EVPn7GnTL(j8@F_O z;m2WNb@23(A|mME6IciWrV^H^Rb^pYG;NTG%3bUB_3mLiyRE74!kv;-l1%rpp^hor zIwFHF jAy+d>YsG7#>m3ZE|vc)F#{+d9%Q&>AG@a%;SKEMj`#nVl0 zQ0>iBZ!U9MBjj^;jf@>tiR@jSYE~|`z80=-uN;p{7hIP~{KOf&1cOQM%_Pa_`C?3z zQEub;rl#NW;z5tGnQX+89fx72xbd=u*cMxO_;$Uq_nf-JNG;#NmQ;b+P#yek7uqY7 zqRe*B>r*z#3Xd<&bl+VOrj&^2)-KXny1cQ!vy6*}XWMPm9Z1>Zsv1u*;kG@OT>Pc_ zESy<@5I1hza6H)i=I`%6-XTmn1$(EzwK_$cq|6mUB9r8Uv; L#0x-azJPIoA$nq zwS%zVq^GCLVh5SECiW;rz_+e(ueC7k0`ge(TMz3r6jRCXx=BZ;Ki8Sa{q@70#j(oD z9^{a*qL9$ZgYCXb$LW^_4ULUP-Bg9;Mm>Q=w3-!BaD-wtJ@)X?lDitIVqL4tzl}D- z^eWN%CTrbcuB-$GCQX&aW!-!VT_SAe!y;K4<$eJH6T>#`dOfM~pkyujYof(H1jQ2S zvFo*#Ci5SgXtGSnx`%AkIie-oRrBCTf3)S~T68vDNHQW0AfP4Z<0F;mDw>T8*aV35 zlEa1)mr7E%HG+kE_e)Kb`}Ulg$KHBO+5T!X;-=Eu`)BEj?z4@#=H4E?81M~ce4vv- zwfAXgj1@yKQ7fM-FDLf}key|(vH^?8DXlLbtDc32_le5(*?~o7n{JP7- )3_|fx7STpmSn`e+CX7CQj$NAW7rY~-NL%N<;*L# z^e?+CGGNqv1PeJH?J&xbY7%GA9;2(Te@{-%b~y9SfbK%Ds?N6y;mx&Y5oPbd56)pP zfIjGdo$a_h?mqQgakcs^0_BAhi-|Yi_K^-+n0l+r%e%U{5dp|T^Vk_0YiMfvgi>2r z*%J!k9LsV3%O1N{8%smNV2QAJzMd}}b*AyyThKg>fiYQ6>Twl;2(g-|TLE~;=(MVP z|0L49X#8Z1(=-vYgwAN0Q`y1(4(#k@*Dtp(yKjywR7Rw<;!1H>f#W7#%yODHTB7;Z zTk-}CWing=^Thai^;U#gn5e9`4^(>ap=O+@4>8zS9LR_$>L(~Q?$0yA9V#%>+5S35 zqOG_`)fgBUcyc`y5Gp`WMFZQ)MCOouF>%$V%6#~0!l@@=VM2z6Daod=$j0_|Dt7kG zgv&@i0{Coa;lQlfF5i96xngVj;-Wp*nCsd(IIdCFP1hxSOr%Op#FVd5QOU~7KTI-O z94ZWvPg7XwRkc{DJlG4V4Y;uO(F@DEnYNH#rw)S^)EVq+F|5|<+lWK W02Mep6i z0eF7{I@m`u=jP@XL&P&$nOo?7A#O124prIM+8CC_+9?Ow@Rv#^pvvOHvU8JpA-Y z8!$P~+!8z~{LOXUvVIyGT0HJ^kq{sR0A4^fmmy^1wCgUk(2Q~UT+F#OQtsm5^#y=2 zm@esi_xR1t&Exfu`6?Ilr+hA&e;jcbn}U;km4N|UqHDG*8LK%Q!8gBf?aGx0ZaW*V z7fQs9FL=ijBD2N_On3=Yx)3i~^-WnLXB=AH!!vYyAs#fjX#Yiw9HF#ORZ zy|w(FSu@RIN(%Mv?ruYSyTzKxRzX5f_pG+FGv|pDCo)z|ESA%p`1mS0***qql*eap z%g8^6=IzJ&v#tG&8d*|#rp8plUqtwr6%H+Mf00*EC~%44cfeVy*M9z@PN;v#Sv`oD zCAErkdA*%WBkg^+J!z`wL{g-A_0UjcQ4u>TaC~VH Z(Dg8pAz3ISE@m;h#KR20-cM?5rWg6ZMM& zxeE!!$eNS<{QNJNjc`gb<5P{JPivu62Zae>#y4vPsT&PSX5TJbdYl|?Zx4a+0~Z^+ zsx6u;vJ6w@qC424n6=0oXTzI&(rNRX`}K35L`Du>=@YZDNIHJoaLi4l&klg$J^j{* zT5#Y^uytQnSC48nKD~B-NFp<--6W)fiEX+`X6%ZF4BibIn(GL}3;@MAsaRgrfw^+W z=R=wz0?qwk&!IO%wH_>zsIoHA)YO!k>q@Pfb>p>`7YxVlgN(qy_{LGEqz)RsuxaD) zwW!<+d-@dTvfEb~FdC=siis6Z1alGFbz20D1BOEa ha z<>lR(J_n9F_RAN`Mf*~8fEZ1_quWXuy_{zWQ0K`m*kg0tSNtX8LojK}F>Y9VyYxIR z8>Z!2SqBjX74!=7&q3QVfrjARF+NxV13)1hJXt&wlZMMmP3^byxio|(s|-h8e!<)1 zrnr+62g+On)u3M&NQ9E%@#DvtO{7gUG$E+9>5^c}c3RWt-JYd5bLNbQxcDTBXqah> zcKuRyD(2b*XZ5wD={iO*rqH{_m(;xhFdTQiip}l3+G|4c@_^djf1T@E+1N0bH5eu+ z^}!&jj^eOEKX=YdOH8=vy|=F~lf!4VlbDy3W{4wTpDBa_4Q(Roo3ACYOpW!*YlEdF z Rx|bt1nI%jZ=qwQ& zFJ8X%Z=T|pc_y@`HWr3z`SN8WDo*8fEuvax!7WEcdN&EqdTe*aI?g4A13#p6li X zrUs*E71Ex=$&uoQ_{!v~vvG)0A@EKu_+sZJ393ewF5bg?cp8@yP!tN}Zmq>0U=*oW z^6hWx)P<0uEDvbUM7XB=_}4BeDn`Z}<<@~kbQhh4w`r;KMEDlQV<7QOKvT*^mRH=} z-PN4u62)K8KWIi&MyXnqKkw7@7@_Dlsb;~a n$l zUZop{-!W4R-mW}XL&nVf425e^mUna05$KfSjX@KL+r&eydO`~8GLt@Wu6N7wIOPqP z0-YK*=Drh@ty9mst?;UX7UE#;FYk^G12BAFS9kB;J%C?jAC+rH=rQ)cCiBTCDK(Hd z3|9g1)(D5H8O)=9Ru%#UVb`Wx?MywE^7~TK(zU$O`AXW_AP5v(2Q1x;55 z-~~{pzu0Dm(QW%H=LQ{saJZi>xjDGPrbrMpfOF|m3u ul#cxM^hfIuD{e zRbet)0bYypb%lM *xD>FS>L8Sn>0%!cQ|@O3iv#-*hAk#N~FZ>ll9)7)PT=k(~y zQ0;rQuj0Zp?7Wac8`BkI^2&X?)P5<><6u8}RUc5fZ)K&2m!2FKXJ%X+`DMqCM^Nav z{dh(%rr+e$6gF_Psmngyldm`KWI^-=aT1UP7b9DJ3JV?Lv1*l-m7j%$;o#!R*7GDy zk4{ZY+z}NOjsD_;d<&Qt6aJpX662NJkA1Rqulx6 zjNmssrv$s0+vmMTM@Q`j?SW;0WfDLHOT-$P(Q#xh-=q(Pih1FDwMP~!Y`rafxRJi% z%)C&2xxJC;sY2h~v}~&G;D#CSt}69~71zumGA3B->_>MI5owSa()8&3>>tO%^hq`D z?c29Kc7vmK{SOp+mO9iFsZwe49!5O(<0SWJt(}^gsfUvwY-D6~jA0muWhtJ;DV^wn zh3TY5pIy6_jE)t=i%z`53I|24`f0ctUCbz=!9~OB8z9B1q29VrMlA#$9v<(Oh#deq z6r|DV>Z+fY3=EdRtL;keIbaPKFrCh6rdu1npvqNko;hOGBbh?efzNA)FtRJpBR;#c z>~T=qx>7(XzNZ7+-$!u2olKVnqgmJIX4{^93w4v^H_LV=Z~7F(HcEE&YQmkaitYKd zMhG^-RLxghX(kP4`?54ko05gncQ!X0>?qOew|En%??%h456*z?acb@jD;StXcX4p( ze3B>GmKQwj%=={P61~>_uRg-c$-+$4?q#cVVhhoZ5yTvP#7B=Eg8+s5b>Gl~W2bak z2yf3G Qvv*))q#5{`l+WRS4#e3A^^QS&R %@`m8g z6bpLv1? )Uf1__ICMB7 &os_#j;9-h zH$Vqj_sA ?>n3g$VCTlx z)@ziMK3i)uF1(+*@Rw+4XeMf~_^yHvk%<>#&}(`|XVP5Zwu6e6Ph(>01H6gJe_0ww z`|{;W)Bd*)UcDld k8%Z5qH3q>Jc*rfHw7Z8H=T>yy#hu rfc8IAz7foP33J6fn);{rZ|8e_72wxl6xY9s3ALJK^r=!7V+dz2s zq0nNC&P2@0iUCpf $U+68DdfQ3Wk-bb6kC7G5g~ZqL0UK@8 zsTAn^Guk61c3&3 x5O5|=WxF}Gq9?52NE&^2=MxaR&shh+_j zYGwy=sr2;pViFOslZ}myfG$6QL4eq>zr>El2Wcy+iYCdkE(aJw-5|EGkj?89R-JbC z`HL6Dz=wyYWYw7-`6g^;EYxdq+(j|JRy!M@3IO$&2Dat7gQ%rFbBivPjL1b{5kN@( zka;Q?2#dG=V&2-|C^nOjoVd0shF~M6q@@iC*KmH*=AJQHbIYvb^H)E2#pJE4rDW*Q z;dyz?Aw?&^CF9}a+YR`J44Ib(EiRf0g y369;gpC08001kOLp4=-* zrL~!9n|dOAc^pm(%6cu}6m(YxN5s)|i@K>YOH2Tb^ATbMA(>~aL)LPh S zWZFI;YaGA4_Ko1KpR<1YG@{x@EM{$2Q?-RulBWe?p!r1yg|i1qLj5MCt&sO%2EDBx zw{+ZhV85ik;l%{;#kfCJ3>h6=8-=wQ>qW`rT(JQI#+Q)0n!@tfJoata1`9 E4B^{nFC4LtoCy+9%wKs#lligg6e+~wPvC>1 z=D~Y1GF1or-}oT=QwKJn7~-K0yK+#)e!FhG=yFNo+^H@pKFF4N1FV0Xp;`zGORO}c zn~YV1@4n?S35w_=l{okmfJjM6LD92usX3_~HfnSiBBlv?g*P~OB9n@ZNzz7VI=MYA z;(kG;YWPjy47{jE iHGM=$M#E5V `P>ueP&9Sa0>KV(e%G+fNW^B}AC z3=aPzh>%##2H&@`jbYpZjtnwXA5okM8(X1RKjh&BAuY4jSGoVNW?T#SFqF}O#DeF$ zcegM}INwXKjdUlPZ9(D`0x?o*8X6#-Gs`zrAlr?ashxM{CMdRk3n9Txr4MsvrY!{b z;eeeEsI7+JsAwUxlvB@beQ78h&IhxJn5pSa%25C0Sa)Ke08?b*zpSs<_VpFY_KL~& z$;fWlO)sQfS3P?4C^|Yi+PnICUx*9k Dadi!SjhJUJ*vGI*H zM2TOlV%9WjIoGfILx|{ce}| %FQmwkS6Hvh$bM&TmMu7cDB*!;&GzI9oC7I1z*fRz0C@pVw`hnWo=_`o;}% z9-i`Mx;gaEiRZ<{#7>_+ZKNeEDta1_RM()Q=Kb!;0aI2sN0#9Mvo}%@Yl2mcnHnl_ zF3)?Osi0X~TPr6ok4;RhQr}+0G|(C5e4U#5ohj?Y%uHY6LSn9_W_?@+ `Zyx_N@rl9K>q&kvQAw5N=Yz8lFdajs0C%VMcRK7cM)e){&v=sM`P{#MpUz`q zb!FuWm}(g=Fw-SbZeVVMwyo@0R2S`C; RQaYABD!)KOO`Y;U zX<4#Q!@>9t?La3I2&K&zit_TS +^ zti}F|=BeWtG2893QD9NH*JG2>5(ksZH=3A*cZVUjTSAkuf*?rHIR56so6yfyGCW~L zYIM(x$5tX%7MCjzVjwvYR*};^A-`W6uDROXC1nL6NN1XZ&twas+}DAY#S#wh2bf zyQ$6y9tE$Ekmzgev_}GsWi i(~I@`ATQCwb7*zhS;qe;l7cy=fmt1B%;BAT@)w GdPvoXXZ0?sX^Xx2_EVK_>){rtpwnCs}Ug7j) zX18>Co5>^9&eUq1fr#P4kKDe%+d}N*<~b{WBN|?Nu8)^_vO>zYm2^zru&L!`f4+R; z#Ig91t)F!O-faS<>e>(q;ZNbmh$goQ6>O7=S{G>Buwb{Xy-rl|SnwFgo1PGl5Q{G> zD--kJkOVMj$O9~hbh6(a)48av^WK@GzWJ(esjgmqoM& kHhoVd--0ZKdjVNF2XpA1)0qXS{4IX@}TX&(Lp=!3U_|5y(y9zt9z zsrRf^p8{A|xGf?@g-(Sn<+dzqA$8(HlreG;eT=duy <*@OlyRv|I_TF 4HYD`Z$O_F`%(Blu1fxV&-uytia?vPr-ZV*DlZYz;Mo zu^2nj4u_V-_`9;79 LXmR{i`(mF8w?pH(j^R!161)gdm=8w&XLCRH zZ^aoL!C!;?)Lnb~f@gtb=;|@$FnZQ-?VOe@Hr8sIG% z2$n>LwON}1+6Q1}$k&(NiS3KtHqRDZJg@qmAUl3*=cTf|yf|g(@En+{)x7TTH>rcE zYb8fV-YgTSzQ;t#kc+rd)9H9=qwZ5|d(g@g;2=>`p&rck6xTf8aUjE)2m%EE`Kj8q zvuz_QRG~Y}Be-a|NhW>qrR|NM Xm0lm{&scGBacj(iCA*59h9cl`X=UYsX-kIkllGjdWyh*Z zTi5&5O`mYixuz=@wvG7Qxl#I%2NJg*F7r%n8{d0-(e6ZP9Q8T bs@nV%J&G z5)~#NJ$p-rUSBM0;zlZGQ^xgGerc(+XP$m;j>viiv;wEdHg-R)4s$LfqK+@~b3-|M z7L e=4^WlDkYK700hgZ Dl2kw63C8^ACEKn%s?A1M(K1KRJf~T9KH0baJ1ChQ?mzXQFw1W z%r?rPvI;aYLBHe*1pegxToV$na-Oa(-n_B4Y8e)$y|P}*b@jN5<8exhW&N6ca5)fB z=&(j|9PF+|@)Q;oDV-^0((2x!fo Ekq${GPw!$OkJh>lg4t zW8CfsM!69Z3iz&*bCyx
5U&t1k~HQM zuk*FbH$NV^krTo#!Xfs^DhzMp2BMztO15%GX6g-~T)<=GIIkN6S1N+y8K&j{S( e0cyw|5ZKMruJI(UEIuz}>>`I4}rrG8e`u5hKBVYpd+iG1^yJV(YHOWKyXrz&ZE z-lV2Zei-MO3^Th{sLe&GN+h&C<0z|MX>A#cc)fAOd(an$wCfZCQJ6vq_zWE2kD>+- z;DU%4b Ai#n(7!V`c7sxf`7 i_=QOx|h%LlX|rDT+2=UMCanrBHeQ z_#$I| ?a5%Lr@^-X{Wm5mdSHm;7-J`?Z=;2R}1{i}0q zZiQJX=7ybUl4Jy-FJ*<#)ebe^6}3A3`gK+MP~LO5>%Jkyjw|c=vI=Sq4<^+eceW=M zX2xF6oUOk*JYS!HXlzE1Au3 SDqDp`S{s`0uGY+7NPCuZ70X&<@FjKUrARj#JsO+Ui=WULt&2}pGRe{KxIiG z#|7Czz2>lS^@aS(%!Of(gFUDs3Sw&K4h1>KLJ)>!VPU~)+W+npsp~tP5T29ou7#^r zL!pb~O6`RX<{;FdYi(R12? W#XcmOa&OEg!w7@{2Sm5eIz40i#lqiO^pxrCyL)l+4i;W+SUlaK*I z5DXx&8Zs;N*IL$!N=y{CD`SQFT&M@|f=Ii%x*F;)O!5u0)Uz0?F`b;YpuP{A@cjK$ zh0 p5+J#QC%JPC58ty~pIX&MlZN2??nn07YNR4>eeU}a}J%Pip`^6Jm zTbD#csj{U!;wUJfbc~#aD*S0@&iqaUzoQ}gmD{(Ug*|`rJk+;l?kf3B?{{b(g UizFi}^ZLko6_rcDqvy)LjiYZ7`wtbouYd>hrU8$?Nt*ZmLEZW|5rSjEz1cK&tHpBg@s%OlpmoOo>#vp@@hlh`g zP0%#DcPe|J%s<4xZroCv@Z^?L@w4a5uAz3~P$4|FUgCe*up@kAsQJ;rT*B~F^Zuv( z1jVmN57BEgWh?=&R_{{NR>>sEekAefNy`k$=$g}o`hYSZJ)wGo0}ZmHSjk nPm5K}}K?LZKo+s>;BxQ)k!*c%HIlk(cNhpVgBkaI)Duc&gS?4hNW zD2~v^P^t+iK1-oRJpyF*XWk^opbARVGo|iIN^bbALsXb!x1fN7g99~@VVI@yLn&E^ zwx^%;Lkb_lXHR2d+MuXU^;-WzfZbzk`@)8t4hqhk4juc*AU{8PW(kUJVty58{nF1{ zpKxqE^TUxv3%6tl`K)ks>VhZgN!`h4n7jsZ5%1gE!#h9Wl(ixkx#R>ByK0MMg>VRn z#qV{}F=8e^R+Vn(z8;E>fv$V7zlSj&*|6{RCP819$$FNon*ZoPz5!`tsRM)kA|b!m z9rhR2ep()y)=R8-^2ll<9dwK{>7fb_Yij#tYO-Bi`Z&hku|KtVO*6Z$uzn3f }mxpBkT8s2P4a zV@bbGAIWA$&c;WI$8>f^^Ie;I t2_#1s?3hXMop*xUPff%(XpcU4}e=0-{jzxI;^I*5qmLQ#8=xh^;@NZNyPLSZ=@ z)FxJqTjpKIBtj-PUc7nBcAY_@ONCo;b{GnOGWTlam1q?TOr<_S!Il1p$FGlX?&Mn4 zIHTAPBHTLJ$D#h3(&+9Lyf-!X>40>;Vs)B_h(r50WzTz5NhWYz4G%B0Rgd*ay4#;D z$y|Z-)f$ji^hQl`H Y0o; z=__`(O`!CF5d@=5Z$Y=>UoNuP6UjswNJYaH&ZqJ1lu(rQ4Go2}_<{pU83a%#_`>P4 zEe?*0%}Qc8FjuWXY-;r{tde!UZ6u|T#md%8uvd8Qr3k$!E2l5M(P4Wm(RoQmoyC$p zO3skD?XIQV?Zh4|(&ZA*J;?5wTidqN2kILLq@|*4J0uH^pE!|U9*2J-t>-Qj&7o*e zz^cKet)E9qBk^Nc#usKd=;_}E@7#h A`V1HfMlsFO>bsHt#V(Vi3K zBm`8Nxw
xo1aD--I9=K&|B=p87;|}YwZ{0 zZ@w}dCLEILCS O4VLxw)G`=rr1BF?C%pFW+O8FCHnICm#A8rQoc*qSfQ zP1Lu)yK$RBPzLhxo^jeq;xR1?7G<~1g>K|dYx=}yArx|(hzz1`hx9wTk3D(+IeLg8 zQamByQuXJFn!GiAWVX_52Zc|~o9oAYy&0$TPH&z#9{Qp`RI@j|elFsnSQm#yo8VVc zcE1{5+0&=b8hq_v@fH@qA-D{w+P%%w8+ERI7rBzJYuW=Lx|nIZig7GFNr&(TUC*ns z;fIB1+HRy+zj<~_2)D!MG(FHrWE2!$5|OM7@WMd*!+R4$cD4A5Py$>F{A9aHR=E#Q z$;SEj)YO2ELosOQKD9J8kux$rZD|pQw?*d0D)|6HV_af?40+xsfF(}c2M&GIbxi{5 z1E&dU`r2*u^(kM!er>0w7Li-q+#CRkLN{=|hx^5_{g7GC-Inaj<7*prd{?k@M@BR4 z`Y+a)vUcSfNI@b+^zL19W@fqLE*IYbBW$U{dHcZw>PP(cl5~LTa&oNORXKfYZo_|{ zKbJQvyRCqA-w$`kAAiv&c!)yi3JX)nX@e;LM(t~CY~fIWpaAbdIPmTJ#d@F-1mOE7 zA+ul1b;O^-ziu?^BZ;(Pz<&MU_{o#^UMl)k#v+0rF|M9H^TGC<;E~+gyMprg`nmpS z9v*6UiNHh3g$*>eH6|84$-@gR?pQllbBoQk*k1CPmW8jZxXCt-df)Y%H;ipGI~hF7 zZPta)+_lrdz`(O76-U)zP0r0|lJhWyrhL(gar`6-N*70chG%Dkf`-cXbaF5`H+g6I zrQ&IFDn!VB`KRoiZO=2v*SFI(3w?5OaOg1r5Tp(WSXrIUmO*ZAZXxrPfS-P!SlsVH z|IqL8D;-3C sGfskzsZ zQDWhAi8(n>A!TI+PJBW!Iu`0LqlPRhqX%*gT-4O*DmJz0ph%l@JRDL2kf$K_&WfIa zLYfyB&5Is@J#T7iDuP-<;K|iO3sCP9aeXfGI&qf35&|ka(GI$!dz=3Dl|`EhStn&L zLQUB0K5*MmH#t!1fIz_w$brRhT;Xqqp+dvMrG)fH*htq8vN#IO__OuB6?Y#?rE9U# zV-#Xph#Fl#)s-{%{PC^*>9K)g1JbS}iK)1N>G+5Y)4_sJuj{^|gQc 3X zqUkT=@y`pqNg2!W #=;f zv#V>M*hI=J4>K`IMp|k3<8ErxU5t^(CNl!3?c;6MuaR_Naw>Wx$VqY5T9%R!Jb#w$ z-laen!xR9~LhvmQQr_(^ZuH93St*lg%Zg7TVgx%smzQmiU3g kv)28;0>P7*Q#;c+t!t*H)C0!^Oo<7FE48vbk0Hw zhgbqtI2 ??r=z2!qst6-!Fcy=%p(NKNPrPPSFNa^;QW3E3P)dXJ266yHw=M#;UJxsZNwHg zT+Le4OEe2!iUF#s0%!_~G%RL*D2s#;gS>y}mb4byzI#PXeyGjbsulYs0YM>0p;|}~ z@$zmIWJRml^o9F-QQ;-mG}ddn);wzGOBOceRxWlj9nO2QUYU_3b#Oafy)?L7bO=&6 z!YnU|c#Sa#Po^nm=-2m3JPUhv{ybjwmkyoHf}$7Yr4G^x_s%@3PLZu&8EMko`kZFr zH;?W0dcjm;uo>PV>@Qw&04GcTvi0i5e9h}_+k)MSARey5UQw~WJfZdwzu-`l Yzf-Eagiu2@U*rT|J zw>z7ROJ&NtCGKaX_;zmkZd}tTaK^@sz2#&?RGG2;U^wa~thvJdh_`^i+H7dMlWAsZ z>ZkQ@ywko2J_;Hd+|Yr*R(*rX7d~TSGLR}p;^E=tHNO?4nWy7gTl(}wSYCReHv^Sg z2H|&K=MHZcbiaM%x^EW)N7LMs51}6)pKsB(i4SEDp@MYM4O9i%lP4rI-_{|AO(M~S zO5wt*E4K?@imS-Uy@$GKcsV4{wru2hKy7EbQkH~#Pqx!Y#ItAbp%m}vyOx(skvx6l zmX`2x0GraAH@Wdf#LCJXc9i}cmx52OsjIMv;9Nd?Yjyva^$`bqx1 sVqquB_bMqPe0AxDl&^3;z8Kkg66sYpa4E_qm6+Z-W$c7EWsTcdyV zxaE1`t~9X?;fQ9z=kaGsN0QrWe7mLuY>K}W444=xQ4wkxB~`Lwcr`sbQ)V_w{b(n~ zxo)-!tXo~Ku|~3-=anm0&S~WP`uKDfO2tZs_o0#yP%(|^##j!OVIpE;@lfw`PH3|9 zhJNl#sPmb#DkYLkmOV4huEqG0bMd5$E)mQ1KFN?c=P=q#bUq7?Tuph2l+-u1 zCHYEa#tQ)iUB;`f @q7Rbw7v5g%U-!sv*YC=;z|+4yc$^x@b1D-pChl$K_1T z#jl1GCRCn#gr^ok3iEQPV_&*51<5lqbdpCJZfqZHw>saD*=7`^Ioyd%;4`L;zWX*H z%^O%)hNSJ2*GVg}y)%1lgFYKR`zow1gJLYv>t{}0-#_O`CgO3`QynPCS?}2xtmh3h zhPP)`1`D`K-v9A|;q)|uLdEk#K5@oZ;qULGdWU*y(u~kxyJ`?qqj*ifl=(y}Ur`U+ zd-NF;CtRW`@7~RJjZ*^ZJx%(_CykyPsd=}~+&N!zen0r>yyojp(rCPH^(Hl*h!o1Y zNdL3aiQ*`p9{!9ITFFr?CJVe|zc$}+$_uiM2?CIQKth2svr8G&mdelAodD?jWo^yG zrs!;%M5U#)ZFpfKR}YF%j|wbMBV6d4+m3Hjp9z H_$f_ma z6;s8ympUdqO95d#AIx-w+N*zJsx>r5Y3DzTTWS?-k;eC*>z-f#;_6i$TufW#KBB{~ z^;VObP8k0=zTW*$qi?8%-5svj24IEZsv#;fl+WHWO|d96`nG@Ni7@`C?pktn&n5%@ zUQzx)+t74=FzzA^R xhDO8PcC(v&xu-2iH8IqabK)*x(9@SB zSn_q)Wr;I*1@cPO=y0N`HpC)Q^9Fo8N9d*T5<6H;3J0X+@nhe; z4K3K)s(t7H!^2K^2fxza6kfiMxZGQ@a>`d&Mqb17!2|sUGFjCV_|(+;XCKAAqCRu` z3UYErUokw(6KT7TWjSBxpXE}1(Y qnSB+sZP&o!3knolvY zu}4c@mgDQ`MKR=I0is4#cl!CE10G5Q-XAEL5$!z_s1LhbuCJ?LvEvw>+CA*`j_>NF z5$O(fXRji4x|i>IlQ=uRDuhKIeL;_hj*eOXf#povh%WtbD2~)*l$ZpQq~nN5#1*rG z3Z;5C_03ZcU&m`33R4EaW7K($j9bEc*JAei#47S92+|U3>FJUKQd4iVAHc(GZf=US z+TMZX3chu?lX~WC<5)#db-4?5d&|4aM1oqe700nIdn#(Z!h8IIbuh&QFLBUJ{m8*b zdc2;={d&Wzl0-{pn(BF}!+3Z-iyMLpa-RIvjm>TE(^F@uXNyKIOZr)|C4A zQq-h@^EW};Z*oT2p%~KSMQW62LRci; zHRHNNpLwVssnCA5yCi848mmr!-*eZsN&Vz05(R7Vj@j~)Xm4MqKlY2l!701DqHu+s zy&u;cc(%>0Ehm8ksM2i&IyWJyln@dJlXJ 8^xD7H?VIe^pzBV-z|C#f w2IV!yMY0b z2GwywqDDk?;VvH=Oj*a5rh<}+>Cs9H*|r`EQYEkhz2xb2w2I!6UH#d!=RoH=MWsy| z3WOd_CHjDlak`$K`O&J448{k$LW`XhmB?Xwm4e$cc%Dah-=tehYskpVD79}YwNT<3 zy&U+u5PeJ1{K%U*qI&Ue`#x5&Af+r!96aIyZzYSeR#rua*H>=i;fKQs pZa_l|pxL&tdE<=(hst@WS(`8>~Wa>kP;QOV$uFNEaKxJ-Nz8^|6)FHziO#-vf* z3MfT=nTp%F?M;DSc;%z0AMX(r6;-3v)ol;P9x%3LP9W73DO`aa-5&f)2+s+K5Q*rW zkAHLD^L9Kgv#WjDs3sGIkZ}B<33(JQUpR77P;PO`Epy1#8*8E=o9h; ZM;}d4ti^TYHq7zd)HW22io4=KJQ#FW7!S1pY0#Y&3->^K5;%1FDYT! z**)BUu)kj_6P?uFc;QIMqJ^zoVWf3+DfHxEZSsckxQ)8eBzT}`+ZQb~^IaP9-r3uu z-tk2#9SfRl-%@KU!>dp{R>3=_`h4yo$n<+uFK*+OES~4tF(o;9c<*@l&fZa*5i^VZ zxq{1|#rIv^fke+YFxBi9T@U<%P0Q*i+FFaYHM*FdQav;^rLWfD;=eejRg G5 z())zivl3ca355K#8lTja8jnz7jD9Bxi3z;U-LvYXh(hc+cfo}UOoE2#%Om!`-EN3+ zHAD0IEx3CS);+L#9(o~?28h5MY-f0mMzx(_>x;+ vU0n-H@7X@7D_w%I&_w| zl{vJ##^5Z$CLszbSjWdF2q%0fl+ 99&ngHKgnG)VH<_p`^jj@`jK!#X}q)Sh~&_K gX+3d%I7DK;s0V;C~{su-NZpcm`1uuG9XWzKpWGcqCW; zCLzzK)HIabRE0RLqeT;fC=*(BmzVNcHj=U)&8aC4(mAiCcyhh07FuMR-DZ+Cz}IWe zk+6khQ@6+%tM{qw(DsN_O!sH>kY4~=^M~RY?b2|?#5bA(eAQoN6eM4mYd)fEeJT5r z5S-8Of+UsXr}hpfl}IvHb(R-qtVqp?%ioXg99uGUg~dfl*q?^6BU5B#c=>v3GH1hL zTg^MXi;jjYiz#HtWkhaV-dVzIRlYUa9FrM5ml1W@eezm=*7)!-V?)xtB*!o4;eA@o zOZn$}Ls&gE2Q<$h0=3Izr 1o*?(-v)xC+Rj$ zFfCTp+jHq5h&wlrj&=KMWqJh4Q{UW&f =WL_YVxJcSAtz}$uMDI> zx;PTrIPm`&W?V!^9n`QML+yim_^?UG>*C4qdcIRl0nOIPB;gDmp~}UZI{RS_);TO? z5CVlG!*>QWSVbx7>-pgHFLKt$jeV;<`svX@Kqv(ZRg~8#X*`?5_0F4jinPz}(9qEO z2V)3Xa*lm>q&jMlym9ofp;AV)*m0XL&xV%Q=V-Yic-+rFL}Txy*l0TDrMx&P(`+LL zZCB^hyvOCD_&>rlYtI_ny}X2B!^d;VidoSUyn)kwaBE3p|48J0@4;G3M~?b0vo61s z2>g3LzUw~__C}TEV3km+Iod$Q*Lo~?y5<{sus&2FerdVC?;jkfKarir8Sz TOWoMx$( HY>7-s+* MbJp#dBN4&h8_Ma8`g-9<2NfP)|>M_OvOUJuqBaQ#au$cjlxF*P>2ClvHn ztGhj=#u!EMm@xiz@h0z`ad}mBH!byx=P^w4f^ztO4ubU8L!RtemaY!hVBJe}WP@|3 zt^32`U=<5zL~NT+a2)kB>L<)x7?>Op@m|C1gz8r>3My9wZMLS~ia6Xa?Mv*bNpkid zUTfXh|5|K(CN4e1vWi2B85_Rf=?gF$&90UqyOkfVcz5yM>(lE6qr*-up2d TY>hkqA3-JqhX`Be4zc*y_J%}VH*#7 zFPtV43@z{yad+ei2)RJl7-Bm@hS@dRN?=BSPz78860KAe6gYPm3GRau-lkWI1HK1{ zO~o+lVjzKMA0aP5un-Yt9~>Oya5`ax8hS3oGU8xsG7jAEpCO0wE8pa51^#=m#@6&{ zytoL|(7hN(4Sdey4>Hj#QmTl*=TK*1oLs+N^WKXSgWYYfizXo8Eg`29VRbIX!?=5p zMy8;DgabxpT3X%yko+FLE1u6b+gd-~vExOJhWlkfE7hskQStO-UZPT?&q>e&Wvtwo z8;fV>9;J9Jh?OET5eYsl^*0^EkIi?5_lY>{2ut<*A5$~Bq1?p6#%J_+{_$6d=4N3} zrH08Z@`C&Fx%ZIkZCupqFnT4;IlDM7%49ml-#+?Y=HXI@H|jb=c2Au($v-^P?C57{ z2cMJQkVt4w_|+xK4Q8!TMrP*0(}grFPUnf$lizS-9k=;&`@w_`DWBt&vGnogTtUsH zQ^eiu>gCd>T$hcS8U}DJq!H8P%#kt;b7>$S_vAf;EL^cd-8eoyA3Z^MXUpE=c9CAM z5`C2&az?9nL=>rgvVUl3^U`K-;rBeKLOfY0#mE@QgPAi06kaY!E!UOg;0jCa9NX3` zU02SRm6Ml}TSRKmVyju^K!Ols>KrajE)BPOT!wtyA(o)jgsBqO`K$o@Xo8cQz2$O= zi!$y#RXh67t9A4nn50WTY_5)HE%f?$Lc6+XmiiKfRRkHd=J7b=!}^3v`w^jJy|%n6 z6}#DIAT(4ht 1KNJyDpphsZRSZk{}}PEe Ab7hms5G-! zx7(r5pJ(ueNXUb+EQ11~x~wD-4GoQi_xk|%`S7iZkB<+5$wGh kiOP10MG;pnBUJu5Qp;B^=I@Bzc!y)h`kjIthC;9zSgilP3L zAnM~Igqc0Q6scxXTvC$Jr}XosvxE5%T?La)%gPOVK54$K^1K`x0e9qug+&|!+$P<9 zdYv}K@SXi{GO`M<#O3wUY~r} U_cUUIKgAx^Ap7 z>=Fk#;)Cu2u??fFN^D4T#=O-khblePW}Y_>bT1hAzj1#@2%an~`&lAw%RMp8P@ec` z67pQ0%v&t;zWG3!Xi~GurbzKMeEu%st56my?OV6Qtw+7@RGqE|W6n_zvn$IMouWQC z#dn}h$hw=h+i4D^=d79LaWRY5sD_JTDQRh|M`%)#{%YggXM}|TXgU;()E#)fYP{GP zG&G9{t@#U3$~-D6Zs!gMBo$(T1mfl8U2R%Ua0_%jMcupN@8 UK1kdS{S*agjfSAK|{MG3zRXB0(DYVMHfzM{D1kt8b zjxP-zAvH*{s8=(RjF ?^gXKvO>Q6<9P?RSb1d<=WDR% zx2RNA%~H$iFtI%=QqGsR*xv)ELt2@`H=Mv$u@`QxnTDk=4j-V}j;$pwYK9c_vWW`I zQLHL*PE@amz(?H2OGf-4$Yh0(u#z?SpvR~$hBeVIYemU;?83~5IcC5rxF`OB$ep$2 zt($IsMUrgB3KE8fOd{6GY|3;7K79oXDCZZZSVg8+F2Bx80=!4gvo-m(SM0uIzie;c zY99U3e7Z6(p (OiF}Thg>t7D7o9a+S1;R zRobegs=w7k*o$_Ap5{ASYJZFSknxDNQ8vbCMUPuf{GQyM mw;i?pk?k_ z26uO+^TR?{W9DEGUrAj~{z7WN*91%TJUDA~pt+M@ZoAMC27NU|K0Hr8cfy|gRaxMR zZLU<_59eN#?>%w6e%%fmp5PCn6kWx9S4f&6Gk(zPILv}E!YF3`8uT2NV#k=%lQgVV zemlE_u*tbO+&k%CS<=-dE-zMupd|f__2BXMBDC-ibA98cx2o{2 z+{nr|M>}J!*Q43DQ$Oski+?XFiq6~M0mG)k AUUIQ9-NyRZmEnqJ1?f}4qd%` zyu;NMI;fJI`vMB<7qCWDntyiU5siLlV5`CM_aD<)a?(GoGW9<5=FV&3G5V}jGq^Sru#e(DH5A(6CbvEVPuQaLsa4Gh%RF%v#F zImrqMG`<+R&8qrwxJ6dAG5 kD%Ulb84Kc>rMjED&51V-RamA`O zTMBT8jN {#8*7k$bOiJI)TC5sPSNX>1OU%u^WzZfNz}YHS$&a#rH*Y3F z5a%-8rd&vu(WUz-F|xOCoxSu*0|Q}alhi}b&R%`#>%FtxekovJ_e6Jb5kU>XBw^~> z_i1&ms~r7O<~t-&7Y&MgL`N2>o&EUz1;vp@lhXaofV N8ZQc9< zqjaoiMT&g&y>u7*dim%3KcmQUJ=8vn3TNP ?-$Mz1h!aqWKhW+#Vr)x0a?=P9!| zU|R AS$d=(XYkQo#=AgTL{ zw!&>@;3#`Q{{09JdyL0~QIDR?Vb=>v);irQn@O|v5H6dtPwzz|J&*ZLJz(z~tLFTt z*uLFF&3V)Mv(34 `>Gj*%!E&qlYR2~&qVEANnvON >h2C0)-v9En+}x(P(nM?El$0c2h&8c5FY1b0Z#}A z8km8}h>zQ5JEb kc_Y1>dXLx9)39ZoC_tojDpe-6o*r1o_!yU!;H5YzA-P=xnmeQa_E|#dI18 zGs8JtOs{>;gDTWH9%Dv^*rEonurC}a zOEl+i@LhfHk9OF4hOxgtT4GK@ 3nfc$YFK8?$42_Qv6D_ek(TE!dQFv^{VJj z*}<9B?O(8JXFo!hG{rmV@yPzl9nZ#?dU)%4D{56%fHd!5!Teybd}#fLSaB;cqqBnE z92$JNRwFjY8~2HGZ~3Od)jVO4fU>Yq9X0g;G;rhJb*)rI0Nq70W`w3Zha>znMDA z(taCRawLeWh*PoEOb!vybafYxZ6;RRaFGz+2>w#lsiSqCoz~ISx3tA=GT%i^04;#H zUzcz8ww~D9XVF~TxTDwdR9m?t5Cs(#>YKD2jNGVu`az^%*!jqlyJUTWR#OpWd)T^u zU_}|sCMew4z2-e>avWqZf{lwice)q9?9WH$*3!} ngGEf@kQ(17^t z0a~azlcFi(aYq0nhFU!`;m>ENBZ(d3JPIRT4zk4xwQDUIu{{d!^!oXPrI?iS-hGV< zS8jl7b*$8q+rREin-JmLXes^5u1Fa?EhN{Y_f7s`Zn=><`RPs6(lLxBpVe>LpGxNo z1MjoPG%z-7Qff2BNaCX;e3+dY{2VN*>DjtAtmRA{5D*oWe_Fy^?HY&5n7zgJaeuea zq;$>SxU_c-_m*C1hPZOknDbitM>h7!FRJvx+wU*BVu$&et$s$YE%Ln!;xoRMCJ{HV zR<6QN5;&auW51W=V{Hs6Z)%m^B`PU@?oyFbK0tv+h@=4B$`8iqsG2Ac>N}r|esq5R zWAjaRD)r3be4sqbSm9lDUL#pax# s9NuAl84-A=0EV7Xmipal03mhnt=6YKt z=lTon>XR8h<{ON?;%GTpRRK9GmFkeUL&V?aUycuu6xMMWmpkaFA|I@w)`a%y8J4!o z{`DF({g_C8m*ZG^_D8Ppn@gEiosC)5X@zo_bqQ_ji$-=vLR8PeCa1jNrpa`-uT}h& z3RGTb-?e|~$+gmrzN18CsChVi3!g;QF`SL|AFo2t?}02e^p?83l!EnPa%)l7X10jf z&>{or0po^Jace4i_KG+$GVfDPg&w_UdUye;?q8D)9S%!e{-9Shu7uDt4d;>$mnr8~ zki3JCI0pH@#p$}NQ?IJmaz(!{2Dr2$wmLSa$r-(l&`ZVRD%QSV3TT?J$D}&- aSi)P9 TxhQOMStGAl_{pEVkNEpc=?=ZN=(I#M)%)oy>`E}` PUXMjvsy3zew~OACDl@vik6L`oY4=O4rUdkS`H_V z|HnvyRCMBPV_)N0<9=J~?eap!^N+}Wx9PSCtmi-)XSk=_@})pm6d+aY=tsRusAI!g z;6q8tC8IsV5FG0GZR#;_%0bge4@bQU{H~>E>c0{|6D0R5%p k)hau9B;#` ykN64>`mAMPE!#{T>I<29^aglA`Z3{+}F zl|Mq8R4yX+ac`bYhNL)H5#_xBhi; zwv)y>G%T#B-{4xQO#aA4A60vFP^Kl}PiRmkHgdoE!pikb2HT)BQ&2Zrh@nw9@@Y zaPoT1`?^1CLl`*X<2^`T3_XYPgP^oZN?`kA1s?8$S2iZ3pay{Z2~$z<4Yt<@AXGN$ zh4Mpkog{4}V;aYc^1ff$ZGn{|50ZvOi#e;DtQ^PTSHe=S{8j~C8c+x103`T@T%>1E zp3wnbk#sYdciTV~-r3y^qkVxbjtz>W4j9uDuc*Z1J6inlxL kcfY%E-{M59^ zYZ2N?BJy!_jM+3SUlTj8ocD#|nw|y-f}*k-GbWAGUhVt}2d(Kf+34*7R+6Z |V=XA5eihcpm&d`uk4KuUpQ-x;-5!JQwG0TiU2z={^{`sT4B}->zB_G2Bxo zwX{;nSI1f0^SN!qBjes`xbfSV60frvYqlgKkJi?Kf@#Xs2YD1V*9&Xt<8lk9)Il7| z%gd`{m>iAd$?uPsHW6f|I3zE_%S1b>g#R35%WxPsJwXC`8^__#p;!G@p=G|;QRfC( z1Kz%Y%-tg}hfevMLnk9VEK){w>xQE(h31L|Tii+{wlA!;SB 7&{9X>oE}YeK<-!eK{!Lg{a6tl z1++{|5-~Yl3IX>^%(W3;8K2vFBU3scyLI&RHLhBfpS|XTlnq3(B7%Z2k;+Y{0aH9N z&sh(FJA`i(0WAjj!~;aWQDlCsND~DRYn=B~t#A-DS}Hy_gpvsqFN+_-_los;@AqFK z;3$B*Cun9+sr&@79mL-<1HOl=@CsER* Lvgm5q{R+ad-{mPUrd=)+igw=6 zM_0JXi1T|A$xzN1-!NrhppSMTST*#0`jlR$&tBiMEt`C&L@Am{_nJH3l(kBjteoy_ zt>X2Q)6Yq9y|pnzl9(uQ73}xa?dVZaPZxCcKb1MtLOT3x!Z3)`(II>m9uGf;<@7)X z@nfv}@ZqXUpZZ ~ZB=LO zaRCO3#_4I$nN2~r5>rumAd@M+2sFMKXwuGs*_HU}q@AB e`mxmY$_2n% z4nWU?+t{b&cmU!7Da!_d&HVv@Nd#pV`h4^VA3Vh40WQIQ0)~3%Pa4F^Q<;F3+2|y8 zpYQ725@L0C5!F9WLRO zH{_&sQE6Sja(4e{T8lb45W{2mn8_Sn#ePxlKs;zU5JCzq! ^a0zz*oz6F{4`a+gvuZ+OzSjTW$W(cFL4BUhhy?3cruv^akvRYEPQ>H-1;U zkMen<-$9S;!n-bT-Ep&+_MK%|>=&PckC?;*NlkNFgUrmYU-R~?XbM3?j)IjMTbOC! z@YiWr$o=O5;7jWHCZALVNYPWEI|B**&i- %3NxK=>aHa0OJO> zHIN%IGcqzPTG0im8W<1cu?Wya2dqheIP!c%r5g;rNQ8`bgi}*&tU`^`BYpFs=LxTe z^fV63Q`WgcXl`24S9wHxskO8nYIJ`+Omy^~)FY>$xO?UHLOh8BRsD+W4=98mP3g%S z`a{P}Ya$zxvH33l_$nO^TUAy}J3q`^zfn?ZS$Xg~4($#)InU#OjULQvo*o(3l*jqf zjiJ0F8=VYRy$PQdU^-Sh&^FkJA)j|S?bq0{MHLU8A~JYl15yE#2x(8iy#zxHDH`FH|5#Av(@kHKH$3}s8}E8b zDY^}cYW(Q mAi7QQSIp-)Io)~NGF7A4-S=A-m< za8StGpVKjyloGLjpst5Rw2!#XOKN2{)8zE@=)77dHKz;Xf+{2A8n!|T^M(P)uOS5v zd-R&LHy{ZMuv=wsm;Wr~Xo0s%K6-b;`J+AmFFWB~u3ET=E%2gDB zIUEdYkR^Rz2{JGdqXrTJv@;J5Ot5+)C{F?c$UtE%#B%{r7=fLJWH_)!J7DGuG*ii+ zJZY)>Qrp_(fqDzTuXB(!FeV#~yTt&gDq;2)Bf=47RTEYyK%;@r=apD|P2q)gGXd_r zR@Ls3uv;UG#AV=nHD?q{1N@wvy)M_Qw}mb!XgmAbKZYdmaELbjw MJ#B2y7|x9C z6)so=W=mN)VX2Uyr0w5zQDE7feK;eU?20s2We;o q85xH2;$LwMv*}Q`nBbcrd<0I|7D)TopqmS#f#>V z1sz?mv=l4kk6w7#op #p&75@C;CK`i&s{LLw;AY%3q`0@w= zjM%HU?{@)(RWRg&Z;;Q&egzotTQf7Wnx 7Q5(d=+ z63(hE$FpRw4inAL=2?RoTw1_gBus{Y`??fXKwR7jPjV8zrY+ HVaeg?L7;K8KY(k01w5JDlI*@pM_i&p8G2I0)6v1xfvuimk9 z@)2iFC%B|4dS|`v7?45VjD}0LXuTRPy6Yo!l|{p}slePCQT$MxhTs}4zD) zk9g((IfZ* z$%wdI_>C;o+i#5D*TP5vy?$$s2S^I%f;F|1jgt*G`hz<%Bv-Eq%ek$+ld6s<0rr8$ zZrJ);BTmZmay>ev;*`b7CQ?Y2+1po*{ zam{0x0Us}mN$;k;zQuiyU z>gexoM8Xtme6foAcy!T%!7_d&qHqO+B=m$a5b?EeWJ6Ew9pCq zPOwG&p>n`cehVyn5t>i4DFkwD1_DSF4yF0z7pk^31I(rbw+ZlTQpX`qJ|9OE|0l_?F8EeY+P^^YZW+7)-(cltEd8^cSMy-RJI(*>2Ou zm12FEN==C$9*~lC-KW~yQPYJ!Ag5AQHwf>^#JGkhytB-^2&N*SZ*Nkpzbzm>aHW Pb&?$(?2kJ7ird@zX@&2zI;fYb(f7r3o4#)u_+ZLn5tU$vTp7}NqM z6f&?%02G7!*<^hD1H|ABh|$fZ1!Mx<8v^<;I-rbnHhKq8|7?!u?l(zS2 CyD>civS!eHTIwrjKLfP1a4dAtC- A}>z>WQkbOB8ga5f7@y2sGXf1d2ib)6@)1=JtJdwbMu{?++BznH8Rl_M$*3D z1HWx}e7qR2>l7hH3`F7xg7IJln9lOwSt+BBJZcxu5`UzmzLCOrcq#0Ltb6w^7$NOL zghhSA6JsVVMwRT}b{mYxQOn73ny$q-9ra=v_c^{j<#th8pl&Jna83Bp6P~V3%J3u< z4C?AL0-5<`m)Zl__xKw&aqN%@2k)`8q8zGQt5#$GNXXD?YI<2-XVKJ@+Gt%}XQB0E z1!dh#@(U}~%-ULy)X&oP1BQ!2`D7k-&7;uq(~rFr`kwOiC#KVS7QWmm?t+|?4yPNz z#5pjw$EsJ^OIWa~&vb-amFaZVk_!C~`!&@aUU)LX=fjR5gaWGvZUvCP1`P5OM#fLT zDEk(D4CLn`olacXxrXLouC?Pq1RnuchV9;vQ6*v<2ZU#AaQ5}rKXf^94+y~EJDEW- zsb2kh$6k8}vg;r^4zBG3cKs&Ac~4=vK|WRv(SEvs*d``A?Y)M-=djyJ))y@CJK7R@ zr&^#=HAwzz)-JeceMO|*Ie2h L@c#GnJr;9+Ugg5~?k$ysU%u~R?B7!xrc9ZZz6?nsG@r%}$x)L !K|BLPIhQE56bF@}-Y=(Z_yz!R99ANHrBB{CNqY)&gEfkkHAwsX~X>*~+s+ zoc?939xlJxcjUvpi}j6}eNJa7+G&-In>)^~`w{+avnuxC3_Km0qx0>tSi?BTJz^I8 zVK@%8^FI@ exiWo?8oK8ZwO*GVs@3+^$NFt8I=)-2DNHZ z3<~Lh$7*k>?3?WQqcQI7P&C`3*rnO#ZOo04C|k7)hHAYc^KKh?=kM2qZU&Aux$)Kc z2M18{Ji&*QES}9Dh#H>EG(%0FwxF+%Lqa<1&&@%G?W)eth>xqSU2H!_$ehd{rm4AQ znRC`yS7W2Kq9Oh6U^Auze$Z}-o&6m80hY#S1M zU0YDjuv2=e~1reC2Bq4#=L6wjHUUv?G{5dQ%hzXOjab#Qk_S=aXS#I zd_d?ow5qZGHP~pf68T|V!oZ9MCP{6cn%8wyP5Iu1yZFT>7a}&7^{9cJf>tkICVWH{ zYV2>1a;b9|q+&wmf2eL1)wfmWSY`GLmFMuFbYW4a#D25TfIi{H71Ez0u_w=-w1j{E zv#UfWf8$`aH{CpQPFEWK-C5n;#|5HTH;y)iUK(Uk OSJ}Sb$k*j08-~O`r$h)mr&3yT)7hPeTM6ZVCvD?->HI0%{1)6%`d# z+WHpNFsv 7LwvwYQz3zxstxlE5SsHjkk1!j`5=Rd5xu)*K0yBg z@@uG505QUhh>%_LO8e#@lWH{MGbl?6UKRx0(+<2|iuF#MypF#}@&TLrP&_jnCQ+JH zEfOFG>L6|{h^YtweVWQDy3g;zgBV=^F>&Fc3G!ET-h~O@e|?UR-@sXi%=n?-5#Y-W zizn^U>wJJsR5X^aEn*p3*=qIk1~w^aU($BH@9nsXV(oQ;292YtAr9RQJ6XBKz0=Yn zN5>Ry+Gyu3#S=S4YPzQ)$bXJDyFI+$TQt_9#xrkWewE-vF)j6|h>BEX>3ht($=E9F z-4kQarXSCBjUh0GNT!c$pnW{yX8%e?Q a&zQX07d}1EZ${f3N=Qw z>@eL#WGqhio$^NV-@)`1RVw!(@e71u+?kKb1#&kpDK~U&o*^lPV{%#7oMI^&)-&cN{jrKBBv)do3=oYaR zSD%8HHwCHA;aK;N{W2Z_j_~hUSq~Ja7)Be`DR~z2uoOsuB HIki%1Et^7@T`7QF%}%+tuAwEyC-s z#-Gip7cPGLCLw(@%*tfBCaZ6fmAjZLVN19Yxn=iZPEu|nsnK%SRaJ*I+DU1@Ys=uI z`Qhb@7cY=tG{+MFMZlQ_2=9YC45bqj6E|oV5Ysgfv&SGghA0XUkZ>603U=DD%3p=* z63~8w^BM^;rW4VNY_v!8g@E~u=nFAgE{P%FxqzckX{QsFlS9ySzrKrWLwL<| 0N ztqgOUE4(55WVtgV081Altc(QYuylaP3~EVS3-QG|V_Fq^M|lbE8>TE*(?r)@U2)D{ zy20Bs7rKRef4Cn?N-DI-MX7fUgk@si8}c0HcL^NZ3hmUH18=*|%qMrF@vs*6=GEG( zu_!pF?zCns=ACHXExbCmFR0p;$DGvIOZSIOwi&cI2DzEs`@dgwhKKU;;!)E-oz3x% zVgjlNq@~Ov7iSs0VUK#c;Az|Vp Suva7kI|g#jusA@! z!eRY)-YN#+gvk7gPOUDtZNI_>@Njpy64 za@v&tWjy-oyBfOn+~NK{6&*Pq9v7@*==x$YKB=)|rH)wiqMVsYHZ)^~PZy%(2MXU` z` CDit29z|%-e cyl_v6QHeE;D%@9p; zXwObA?c{6z%iKy@5q }vznh0SB>LwqoweW3jdS9E zhp}Q7jT-^x4i_80r;>#d?>?!x!prNZ!i(TsL-g%t( 7UueSOQ-Z|pb^aNpRyd@>e8bo~aZtwF-ih{a+7R8)xf zboBQ%8QI*j7q)eckzIGQCJU{*-M&)wm22BuSQz9Bmi4Avid7$;!&fzH#ZffwJV9RT zP**|t+!Q~@73!E`{b}UAlG4hnM8^)gXL{2qtnuPEePv|)H*lI`WHPm-<<8GhPMp_m zE-kL=5c`Fd5RATPp1Y=yC#;AZY71%Qr}m?ZV=LAl5kES3C`mOHtQ=nCVE->a@2A-` z#67P_;37LK&nh%|o4>>Tr H`7%r4_Lu|&nUPqQC;S^iu F-!yz_1Li5Yu4gJ)PUtPH_O zg=GPN8~xDj8#jnWjBx_$WqMK3Ctz_y@=OTsa-QhfFcJ*9<;giTY;m_47#I-!aF|R{ z`nOv9S^4T8icXMO(DfSkDR;ce4|BkD6yr4|(98Aa9k0NiHf4PN5MQFdYi`3o6OV{U z7&^O5KcrTw-TT6dSeEH`TI?b8_xfG`ll6Z$h^zMVaxiRauyQ(I+9{xck oPRZ8=7GxUW-FeO+ts_6ecKZBDQSat;lB zBO~UYgSF1~t7BBpo3~pUUa(kMSpoM(O-&v^S Yp+R(JN9h^vU7 zyTar#>u3^?OriHaaC !@x5Vyp| zmqk{N8Z^9+lRbCZ^zyy2KmPlh)1S`v9v<5pk&$ 57~{JhObjl&fpgXCER1s!`BNoK=2+dV{;Eu zEyPLt%8qsg)8#CIguwzstwPVbV6spT%>16=)Q9gIF@6lD>LTW|0IpGTc@)8?hemhh z8qM*CTmKhq2Poo8cF+R8m*}we^v(m02C(o-Oaa&$&fiup`{UhR#Kfd!ko7cSR5N1e z;q5PJ@!-A*<=I1nnJ&bD3IE89g@D5$AFW3y|tQzOxOHZVoQ4ScJ5I*e}2gX2_@5 zZ4|fPk3R VmE_`2PCIJvvgW6w=R!r%y@=6!uyP_n ztGO5IpZQRBx@*ldV^`;a8HM%kPg!BE7YGMegEiAg*Um2#4i1K1zC7GXx1@fa5WORL zxnJ0h78nG1CYXuX&GB)KwLpc@cd*3b+}Rn?X_jS}CTKn+X3Ppncpg#)vkr`i?tdHP zpx5rAsTa_s_8EWImX|~Q=9|IDfepHCzmV_LH?PPhoQLPlhO16z^ZieZ$9wuzYdEy$ zVmN4NVU>u8h+ryneVBMY!jF!Iu{7h30)SkHjibXu?a_uyOp>1uL4vwOxjD9XMsR*j zDBA8%_W9Wl-z+nO4gLs^?hMTt(#U=W8mi r``*zRi!)7*&QI6X+dVr{ z#atOG6|Z$X+DehQazA(*G!I7Iw4aNY;vJm&@3b_o>PI*Do>1zA$L#xO(g&~Es6#XM z^>g#(&5E%1O9n$05mYwG0)Mu`iFCmsCjyGKQGYw%QcmiFiV}!Y)Zmi^xiD` zdd2M9k66%U##<5rei&e*Apl{6vD5=b4-k#5x8h&P?0I*|DRGFC1{x%I;jRbL9E`oV ze}VhK#DpFQz5wIB(GD#HU^xP`({e34a}Sc}|8RkB_n8{= 9^b_sBHZF{P zHK508ZEcP2T_Ttt{M+N#i*kVO^PrX5_=vQ6;-O;eg{Ujkkd)5Faj9F z4hk!z;|Bq-mwPY)Lr`lkr!qIjU6Y0Or4$t#zdx)ET^Z$|>F)Lq@t;4pk4j$WlHmQr zzID>wOdZhF&+j5Sctn?{_U$wAr5#?16jKZ3lfCVm+xuuXZZho>Iz!ddl?yrRz(swZ z MABIRAq5?hoon{&h~ xmZ<634ieB&rPgQUVxT z88&Y)_7R2_vziVAhw)$Qg)u=7N$^G%e}^H)e3k}6Xqwr)i&G~(W6pxpKfqu6IK)^s zBFgelj7-O?x3d*;{L=DD0b@pu{EVNN=KIL_*?m>HE6n>&OF1h|8ER~eGE!4LJ{6f! zCO%Jq^8Ua1ZP$h#%E^r-lm(C7qR-4Ka_e1pNh>Mw_>uWeqy5Y61;BDd_Z;~7Yp-O< z+z+lGWA_dI=-Osf%xu;2R7Wqp__AUcfEpT1%8;)Yi{T3|ixArQT@`$5Nfq=ck!q@m zEEx$Z4%WP(5h?MIvvD>QyvH@d)0<`-Au)mGwnYe(&R_BlTD$qZjTyB}=d&-t@zUoM zuaAm@V$2QWuGctTZ@y@i%pG?48U7|NO{A!TwX>8QX8efA4VxT4{{co%oDIQc?2T;~ z!M$Aqp9bqFhUC-|+?w9DK2^u6gE@N@tJCM#Q7g9M(16QJd{y)bRtS^tSC43Dx{AkQ zT?lQ#1nJ{G?6A1;`+T9t7i&F>LczRp~}0Ue^62*b;`dN)~mJae%eL!1mjqt-{d3 zhep!z{PB1grUzW14_b^}jF`vI(}}J7pt+FQjeY~NJ4#u1{&9CEa>NF_w*=gS3LKrl zQi)x v3yn49!O!*IwUZD403A0ZK zhj&BrC;8>p)j>P0PFfRn3zXWr&0e49arw`cetLdj);``fwyd0&qB47|env|ey@Q2; z0`5KwaQER)zW9O9stQSuHUEm)6NtG2^{jU^Tcg +$%(HoXucA~lQ`>!P2JcbwEGMSlYWV+Idf zOjGR+#Kz>9By)vpa+$7<#PvJ}CS(0n-v8yxh_jhfvws(VI?63mM>yQXOU8kTdYmpB z__{TnI@$_&SLTKebQ0cu`q1b9`-JKz)hTI#4 Y1ze-J0XH)G?C>rBbxW~#%eD4~5T&lyzXu JXh-E?g->ve9( 2SQDHwG9yy|<>q@p z9`eEK;uDv<(3%%H_y3Jrx{gHQKq=R=F Yziyl=V`-U6T_@W>LUJoYK|*S}^Ut4W$kEbw%$8Hrst^x* NEiC@BKv2Wu{{VFWkKzCT literal 0 HcmV?d00001 diff --git a/Documentation/images/example.png.meta b/Documentation/images/example.png.meta new file mode 100644 index 0000000..144ca3b --- /dev/null +++ b/Documentation/images/example.png.meta @@ -0,0 +1,124 @@ +fileFormatVersion: 2 +guid: 02addf231624d734aac11a99b93b93d4 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMasterTextureLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + spritePackingTag: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor.meta b/Editor.meta new file mode 100644 index 0000000..b9ca736 --- /dev/null +++ b/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b74bf422ea69c144d9559d5a8a9de006 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Scripts.meta b/Editor/Scripts.meta new file mode 100644 index 0000000..7da429c --- /dev/null +++ b/Editor/Scripts.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 15c9c665be3d06048bebd0e20ec2b173 +folderAsset: yes +timeCreated: 1566558344 +licenseType: Pro +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Scripts/OutlineBehaviourEditor.cs b/Editor/Scripts/OutlineBehaviourEditor.cs new file mode 100644 index 0000000..825853b --- /dev/null +++ b/Editor/Scripts/OutlineBehaviourEditor.cs @@ -0,0 +1,113 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +using System; +using UnityEditor; +using UnityEditorInternal; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.Rendering; + +namespace UnityFx.Outline +{ + [CustomEditor(typeof(OutlineBehaviour))] + public class OutlineBehaviourEditor : Editor + { + private OutlineBehaviour _effect; + private SerializedProperty _settingsProp; + private bool _debugOpened; + private bool _renderersOpened; + private bool _camerasOpened; + + private void OnEnable() + { + _effect = (OutlineBehaviour)target; + _settingsProp = serializedObject.FindProperty("_outlineSettings"); + } + + public override void OnInspectorGUI() + { + base.OnInspectorGUI(); + + // 1) Outline settings. + EditorGUI.BeginChangeCheck(); + + var mask = EditorGUILayout.MaskField("Ignore layers", _effect.IgnoreLayerMask, InternalEditorUtility.layers); + + if (_effect.IgnoreLayerMask != mask) + { + Undo.RecordObject(_effect, "Set Ignore Layers"); + _effect.IgnoreLayerMask = mask; + } + + var e = (CameraEvent)EditorGUILayout.EnumPopup("Render Event", _effect.RenderEvent); + + if (e != _effect.RenderEvent) + { + Undo.RecordObject(_effect, "Set Render Event"); + _effect.RenderEvent = e; + } + + var c = (Camera)EditorGUILayout.ObjectField("Target Camera", _effect.Camera, typeof(Camera), true); + + if (c != _effect.Camera) + { + Undo.RecordObject(_effect, "Set Target Camera"); + _effect.Camera = c; + } + + EditorGUILayout.PropertyField(_settingsProp); + serializedObject.ApplyModifiedProperties(); + + if (EditorGUI.EndChangeCheck()) + { + EditorUtility.SetDirty(_effect.gameObject); + + if (!EditorApplication.isPlayingOrWillChangePlaymode) + { + EditorSceneManager.MarkSceneDirty(_effect.gameObject.scene); + } + } + + // 2) Renderers (read-only). + _renderersOpened = EditorGUILayout.Foldout(_renderersOpened, "Renderers", true); + + if (_renderersOpened) + { + EditorGUI.BeginDisabledGroup(true); + EditorGUI.indentLevel += 1; + + var rendererNumber = 1; + + foreach (var renderer in _effect.OutlineRenderers) + { + EditorGUILayout.ObjectField("#" + rendererNumber.ToString(), renderer, typeof(Renderer), true); + rendererNumber++; + } + + EditorGUI.indentLevel -= 1; + EditorGUI.EndDisabledGroup(); + } + + // 3) Cameras (read-only). + _camerasOpened = EditorGUILayout.Foldout(_camerasOpened, "Cameras", true); + + if (_camerasOpened) + { + EditorGUI.BeginDisabledGroup(true); + EditorGUI.indentLevel += 1; + + var cameraNumber = 1; + + foreach (var camera in _effect.Cameras) + { + EditorGUILayout.ObjectField("#" + cameraNumber.ToString(), camera, typeof(Camera), true); + cameraNumber++; + } + + EditorGUI.indentLevel -= 1; + EditorGUI.EndDisabledGroup(); + } + } + } +} diff --git a/Editor/Scripts/OutlineBehaviourEditor.cs.meta b/Editor/Scripts/OutlineBehaviourEditor.cs.meta new file mode 100644 index 0000000..1c3d375 --- /dev/null +++ b/Editor/Scripts/OutlineBehaviourEditor.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: a6e77a9d499a86e4fbdc52a2977e774f +timeCreated: 1566572433 +licenseType: Pro +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Scripts/OutlineBuilderEditor.cs b/Editor/Scripts/OutlineBuilderEditor.cs new file mode 100644 index 0000000..ec5b4eb --- /dev/null +++ b/Editor/Scripts/OutlineBuilderEditor.cs @@ -0,0 +1,169 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEditor; +using UnityEditorInternal; +using UnityEditor.SceneManagement; +using UnityEngine; + +namespace UnityFx.Outline +{ + [CustomEditor(typeof(OutlineBuilder))] + public class OutlineBuilderEditor : Editor + { + private OutlineBuilder _builder; + private ReorderableList _content; + private List _lists; + + private void OnEnable() + { + _builder = (OutlineBuilder)target; + + if (EditorApplication.isPlaying) + { + if (_builder.OutlineLayers) + { + _lists = new List (_builder.OutlineLayers.Count); + + foreach (var layer in _builder.OutlineLayers) + { + var list0 = new ArrayList(layer.Count); + + foreach (var go in layer) + { + list0.Add(go); + } + + var editorList = new ReorderableList(list0, typeof(GameObject), false, true, true, true); + + editorList.onAddCallback += (list) => + { + list.list.Add(null); + }; + + editorList.onRemoveCallback += (list) => + { + var go = list.list[list.index]; + list.list.RemoveAt(list.index); + layer.Remove(go as GameObject); + }; + + editorList.drawElementCallback += (rect, index, isActive, isFocused) => + { + var prevGo = list0[index] as GameObject; + var go = (GameObject)EditorGUI.ObjectField(new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight), $"#{index}", prevGo, typeof(GameObject), true); + + if (prevGo != go) + { + list0[index] = go; + layer.Remove(prevGo); + layer.Add(go); + } + }; + + editorList.drawHeaderCallback += (rect) => + { + EditorGUI.LabelField(rect, layer.Name); + }; + + editorList.elementHeightCallback += (index) => + { + return EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing; + }; + + _lists.Add(editorList); + } + } + } + else + { + _content = new ReorderableList(_builder.Content, typeof(OutlineBuilder.ContentItem), true, true, true, true); + + _content.drawElementCallback += (rect, index, isActive, isFocused) => + { + if (_builder && _builder.Content != null && index < _builder.Content.Count) + { + var item = _builder.Content[index]; + + if (item != null) + { + item.Go = (GameObject)EditorGUI.ObjectField(new Rect(rect.x + rect.width * 0.3f + 1, rect.y, rect.width * 0.7f, EditorGUIUtility.singleLineHeight), item.Go, typeof(GameObject), true); + item.LayerIndex = EditorGUI.IntField(new Rect(rect.x, rect.y, rect.width * 0.3f - 1, EditorGUIUtility.singleLineHeight), item.LayerIndex); + } + } + }; + + _content.drawHeaderCallback += (rect) => + { + EditorGUI.LabelField(rect, "Content"); + }; + + _content.elementHeightCallback += (index) => + { + return EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing; + }; + } + } + + public override void OnInspectorGUI() + { + base.OnInspectorGUI(); + + EditorGUI.BeginChangeCheck(); + + if (_content != null) + { + EditorGUILayout.HelpBox("Game objectes listed below will be added to corresponding outline layers when application is started. Only scene references are allowed.", MessageType.Info); + _content.DoLayoutList(); + EditorGUILayout.Space(); + + if (GUILayout.Button("Clear")) + { + _builder.Content.Clear(); + } + + serializedObject.ApplyModifiedProperties(); + } + else if (_lists != null && _lists.Count > 0) + { + EditorGUILayout.HelpBox("Settings below are not serialized, they only exist in runtime.", MessageType.Info); + + for (var i = 0; i < _lists.Count; ++i) + { + _lists[i].DoLayoutList(); + EditorGUILayout.Space(); + } + + if (GUILayout.Button("Clear")) + { + foreach (var list in _lists) + { + list.list.Clear(); + } + + _builder.Clear(); + } + + serializedObject.ApplyModifiedProperties(); + } + else + { + // TODO + } + + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(_builder, "Builder"); + + if (!EditorApplication.isPlayingOrWillChangePlaymode) + { + EditorUtility.SetDirty(_builder.gameObject); + EditorSceneManager.MarkSceneDirty(_builder.gameObject.scene); + } + } + } + } +} diff --git a/Editor/Scripts/OutlineBuilderEditor.cs.meta b/Editor/Scripts/OutlineBuilderEditor.cs.meta new file mode 100644 index 0000000..20872eb --- /dev/null +++ b/Editor/Scripts/OutlineBuilderEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c2ba41e0b025a6e46abc7b69d31d1907 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Scripts/OutlineEditorUtility.cs b/Editor/Scripts/OutlineEditorUtility.cs new file mode 100644 index 0000000..df53c31 --- /dev/null +++ b/Editor/Scripts/OutlineEditorUtility.cs @@ -0,0 +1,100 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + +namespace UnityFx.Outline +{ + public static class OutlineEditorUtility + { + public static readonly GUIContent FilterSettingsContent = new GUIContent("Outline Filter Settings", ""); + public static readonly GUIContent LayerMaskContent = new GUIContent("Layer Mask", OutlineResources.OutlineLayerMaskTooltip); + public static readonly GUIContent RenderingLayerMaskContent = new GUIContent("Rendering Layer Mask", OutlineResources.OutlineRenderingLayerMaskTooltip); + public static readonly GUIContent ColorContent = new GUIContent("Color", "Outline color."); + public static readonly GUIContent WidthContent = new GUIContent("Width", "Outline width in pixels."); + public static readonly GUIContent RenderFlagsContent = new GUIContent("Render Flags", "Outline render flags. Multiple values can be selected at the same time."); + public static readonly GUIContent BlurIntensityContent = new GUIContent("Blur Intensity", "Outline intensity value. It is only usable for blurred outlines."); + public static readonly GUIContent AlphaCutoffContent = new GUIContent("Alpha Cutoff", "Outline alpha cutoff value. It is only usable when alpha testing is enabled and the material doesn't have _Cutoff property."); + + public static void RenderPreview(OutlineLayer layer, int layerIndex, bool showObjects) + { + if (layer != null) + { + var goIndex = 1; + + EditorGUILayout.BeginHorizontal(); + EditorGUI.indentLevel += 1; + EditorGUILayout.PrefixLabel("Layer #" + layerIndex.ToString()); + EditorGUI.indentLevel -= 1; + + if (layer.Enabled) + { + EditorGUILayout.LabelField(layer.OutlineRenderMode == OutlineRenderFlags.None ? layer.OutlineRenderMode.ToString() : string.Format("Blurred ({0})", layer.OutlineIntensity), GUILayout.MaxWidth(70)); + EditorGUILayout.IntField(layer.OutlineWidth, GUILayout.MaxWidth(100)); + EditorGUILayout.ColorField(layer.OutlineColor, GUILayout.MinWidth(100)); + } + else + { + EditorGUILayout.LabelField("Disabled."); + } + + EditorGUILayout.EndHorizontal(); + + if (showObjects) + { + if (layer.Count > 0) + { + foreach (var go in layer) + { + EditorGUI.indentLevel += 2; + EditorGUILayout.ObjectField("#" + goIndex.ToString(), go, typeof(GameObject), true); + EditorGUI.indentLevel -= 2; + + goIndex++; + } + } + else + { + EditorGUI.indentLevel += 2; + EditorGUILayout.LabelField("No objects."); + EditorGUI.indentLevel -= 2; + } + } + } + else + { + EditorGUILayout.BeginHorizontal(); + EditorGUI.indentLevel += 1; + EditorGUILayout.PrefixLabel("Layer #" + layerIndex.ToString()); + EditorGUI.indentLevel -= 1; + EditorGUILayout.LabelField("Null"); + EditorGUILayout.EndHorizontal(); + } + } + + public static void RenderPreview(IList layers, bool showObjects) + { + EditorGUI.BeginDisabledGroup(true); + + if (layers.Count > 0) + { + for (var i = 0; i < layers.Count; ++i) + { + RenderPreview(layers[i], i, showObjects); + } + } + else + { + EditorGUI.indentLevel += 1; + EditorGUILayout.LabelField("No layers."); + EditorGUI.indentLevel -= 1; + } + + EditorGUI.EndDisabledGroup(); + } + } +} diff --git a/Editor/Scripts/OutlineEditorUtility.cs.meta b/Editor/Scripts/OutlineEditorUtility.cs.meta new file mode 100644 index 0000000..28953ac --- /dev/null +++ b/Editor/Scripts/OutlineEditorUtility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ba087138029b59d4bbdf0783db0e2606 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Scripts/OutlineEffectEditor.cs b/Editor/Scripts/OutlineEffectEditor.cs new file mode 100644 index 0000000..75f3c3f --- /dev/null +++ b/Editor/Scripts/OutlineEffectEditor.cs @@ -0,0 +1,62 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine.Rendering; + +namespace UnityFx.Outline +{ + [CustomEditor(typeof(OutlineEffect))] + public class OutlineEffectEditor : Editor + { + private OutlineEffect _effect; + private bool _debugOpened; + private bool _previewOpened; + + private void OnEnable() + { + _effect = (OutlineEffect)target; + } + + public override void OnInspectorGUI() + { + base.OnInspectorGUI(); + + EditorGUI.BeginChangeCheck(); + var e = (CameraEvent)EditorGUILayout.EnumPopup("Render Event", _effect.RenderEvent); + + if (e != _effect.RenderEvent) + { + Undo.RecordObject(_effect, "Set Render Event"); + _effect.RenderEvent = e; + } + + if (EditorGUI.EndChangeCheck()) + { + EditorUtility.SetDirty(_effect.gameObject); + + if (!EditorApplication.isPlayingOrWillChangePlaymode) + { + EditorSceneManager.MarkSceneDirty(_effect.gameObject.scene); + } + } + + if (_effect.OutlineLayers) + { + if (_effect.OutlineLayers.Count > 0) + { + _previewOpened = EditorGUILayout.Foldout(_previewOpened, "Preview", true); + + if (_previewOpened) + { + OutlineEditorUtility.RenderPreview(_effect.OutlineLayers, true); + } + } + } + } + } +} diff --git a/Editor/Scripts/OutlineEffectEditor.cs.meta b/Editor/Scripts/OutlineEffectEditor.cs.meta new file mode 100644 index 0000000..4b3f6af --- /dev/null +++ b/Editor/Scripts/OutlineEffectEditor.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: 03c2e35bb52d2ba44882b92d7cde45bf +timeCreated: 1566558360 +licenseType: Pro +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Scripts/OutlineLayerCollectionEditor.cs b/Editor/Scripts/OutlineLayerCollectionEditor.cs new file mode 100644 index 0000000..94e799d --- /dev/null +++ b/Editor/Scripts/OutlineLayerCollectionEditor.cs @@ -0,0 +1,214 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEditor; +using UnityEditorInternal; +using UnityEngine; + +namespace UnityFx.Outline +{ + [CustomEditor(typeof(OutlineLayerCollection))] + public class OutlineLayerCollectionEditor : Editor + { + private OutlineLayerCollection _layers; + + private SerializedProperty _layersProp; + private ReorderableList _layersList; + + private void OnEnable() + { + _layers = (OutlineLayerCollection)target; + + _layersProp = serializedObject.FindProperty("_layers"); + _layersList = new ReorderableList(serializedObject, _layersProp, true, true, true, true); + _layersList.drawElementCallback += OnDrawLayer; + _layersList.drawHeaderCallback += OnDrawHeader; + _layersList.elementHeightCallback += OnGetElementHeight; + _layersList.onAddCallback += OnAddLayer; + _layersList.onRemoveCallback += OnRemoveLayer; + } + + public override void OnInspectorGUI() + { + base.OnInspectorGUI(); + + EditorGUI.BeginChangeCheck(); + + var mask = EditorGUILayout.MaskField("Ignore layers", _layers.IgnoreLayerMask, InternalEditorUtility.layers); + + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(_layers, "Change ignore mask"); + _layers.IgnoreLayerMask = mask; + } + + EditorGUILayout.Space(); + + _layersList.DoLayoutList(); + + if (_layers.NumberOfObjects > 0) + { + EditorGUILayout.Space(); + EditorGUILayout.HelpBox("Read-only lists below represent game objects assigned to specific outline layers. Only non-empty layers are displayed.", MessageType.Info); + + foreach (var layer in _layers) + { + if (layer.Count > 0) + { + EditorGUILayout.LabelField(layer.Name, EditorStyles.boldLabel); + EditorGUI.BeginDisabledGroup(true); + EditorGUI.indentLevel += 1; + + var index = 0; + + foreach (var go in layer) + { + EditorGUILayout.ObjectField($"#{index++}", go, typeof(GameObject), true); + } + + EditorGUI.indentLevel -= 1; + EditorGUI.EndDisabledGroup(); + } + } + } + + serializedObject.ApplyModifiedProperties(); + } + + private void OnDrawLayer(Rect rect, int index, bool isActive, bool isFocused) + { + var lineHeight = EditorGUIUtility.singleLineHeight; + var lineSpacing = EditorGUIUtility.standardVerticalSpacing; + var lineOffset = lineHeight + lineSpacing; + var y = rect.y + lineSpacing; + var layer = _layers[index]; + + var obj = layer.OutlineSettings; + var merge = layer.MergeLayerObjects; + var enabled = layer.Enabled; + var name = layer.NameTag; + var color = layer.OutlineColor; + var width = layer.OutlineWidth; + var renderMode = layer.OutlineRenderMode; + var blurIntensity = layer.OutlineIntensity; + var alphaCutoff = layer.OutlineAlphaCutoff; + + EditorGUI.BeginChangeCheck(); + + // Header + { + var rc = new Rect(rect.x, y, rect.width, lineHeight); + var bgRect = new Rect(rect.x - 2, y - 2, rect.width + 3, lineHeight + 3); + + // Header background + EditorGUI.DrawRect(rc, Color.gray); + EditorGUI.DrawRect(new Rect(bgRect.x, bgRect.y, bgRect.width, 1), Color.gray); + EditorGUI.DrawRect(new Rect(bgRect.x, bgRect.yMax, bgRect.width, 1), Color.gray); + EditorGUI.DrawRect(new Rect(bgRect.x, bgRect.y, 1, bgRect.height), Color.gray); + EditorGUI.DrawRect(new Rect(bgRect.xMax, bgRect.y, 1, bgRect.height), Color.gray); + + obj = (OutlineSettings)EditorGUI.ObjectField(rc, " ", obj, typeof(OutlineSettings), true); + enabled = EditorGUI.ToggleLeft(rc, "Layer #" + index.ToString(), enabled, EditorStyles.boldLabel); + y += lineOffset; + } + + // Layer properties + { + name = EditorGUI.TextField(new Rect(rect.x, y, rect.width, lineHeight), "Name", name); + y += lineOffset; + + merge = EditorGUI.Toggle(new Rect(rect.x, y, rect.width, lineHeight), "Merge Layer Objects", merge); + y += lineOffset; + } + + // Outline settings + { + EditorGUI.BeginDisabledGroup(obj != null); + + color = EditorGUI.ColorField(new Rect(rect.x, y, rect.width, lineHeight), OutlineEditorUtility.ColorContent, color, true, true, true); + y += lineOffset; + + width = EditorGUI.IntSlider(new Rect(rect.x, y, rect.width, lineHeight), OutlineEditorUtility.WidthContent, width, OutlineResources.MinWidth, OutlineResources.MaxWidth); + y += lineOffset; + + renderMode = (OutlineRenderFlags)EditorGUI.EnumFlagsField(new Rect(rect.x, y, rect.width, lineHeight), OutlineEditorUtility.RenderFlagsContent, renderMode); + y += lineOffset; + + if ((renderMode & OutlineRenderFlags.Blurred) != 0) + { + blurIntensity = EditorGUI.Slider(new Rect(rect.x, y, rect.width, lineHeight), OutlineEditorUtility.BlurIntensityContent, blurIntensity, OutlineResources.MinIntensity, OutlineResources.MaxIntensity); + y += lineOffset; + } + + if ((renderMode & OutlineRenderFlags.EnableAlphaTesting) != 0) + { + alphaCutoff = EditorGUI.Slider(new Rect(rect.x, y, rect.width, lineHeight), OutlineEditorUtility.AlphaCutoffContent, alphaCutoff, OutlineResources.MinAlphaCutoff, OutlineResources.MaxAlphaCutoff); + } + + EditorGUI.EndDisabledGroup(); + } + + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(_layers, "Layers changed"); + EditorUtility.SetDirty(_layers); + + layer.OutlineSettings = obj; + layer.Enabled = enabled; + layer.NameTag = name; + layer.MergeLayerObjects = merge; + layer.OutlineWidth = width; + layer.OutlineColor = color; + layer.OutlineRenderMode = renderMode; + layer.OutlineIntensity = blurIntensity; + layer.OutlineAlphaCutoff = alphaCutoff; + } + } + + private void OnDrawHeader(Rect rect) + { + EditorGUI.LabelField(rect, "Layer settings"); + } + + private float OnGetElementHeight(int index) + { + var numberOfLines = 6; + + if ((_layers[index].OutlineRenderMode & OutlineRenderFlags.Blurred) != 0) + { + ++numberOfLines; + } + + if ((_layers[index].OutlineRenderMode & OutlineRenderFlags.EnableAlphaTesting) != 0) + { + ++numberOfLines; + } + + return (EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing) * numberOfLines + EditorGUIUtility.standardVerticalSpacing; + } + + private void OnAddLayer(ReorderableList list) + { + var layer = new OutlineLayer(); + + Undo.RecordObject(_layers, "Add Layer"); + EditorUtility.SetDirty(_layers); + + _layers.Add(layer); + } + + private void OnRemoveLayer(ReorderableList list) + { + var index = list.index; + var layer = _layers[index]; + + Undo.RecordObject(_layers, "Remove Layer"); + EditorUtility.SetDirty(_layers); + + _layers.RemoveAt(index); + } + } +} diff --git a/Editor/Scripts/OutlineLayerCollectionEditor.cs.meta b/Editor/Scripts/OutlineLayerCollectionEditor.cs.meta new file mode 100644 index 0000000..528a02e --- /dev/null +++ b/Editor/Scripts/OutlineLayerCollectionEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5f4ec5de59e58794b8e34f2ca3c00199 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Scripts/OutlineSettingsEditor.cs b/Editor/Scripts/OutlineSettingsEditor.cs new file mode 100644 index 0000000..e017287 --- /dev/null +++ b/Editor/Scripts/OutlineSettingsEditor.cs @@ -0,0 +1,240 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + +namespace UnityFx.Outline +{ + [CustomEditor(typeof(OutlineSettings))] + public class OutlineSettingsEditor : Editor + { + private const string _filterModePropName = "_filterMode"; + private const string _layerMaskPropName = "_layerMask"; + private const string _renderingLayerMaskPropName = "_renderingLayerMask"; + private const string _settingsPropName = "_outlineSettings"; + private const string _colorPropName = "_outlineColor"; + private const string _widthPropName = "_outlineWidth"; + private const string _intensityPropName = "_outlineIntensity"; + private const string _cutoffPropName = "_outlineAlphaCutoff"; + private const string _renderModePropName = "_outlineMode"; + + private static readonly string[] _renderingLayerMaskNames = new string[] + { + "Layer1", + "Layer2", + "Layer3", + "Layer4", + "Layer5", + "Layer6", + "Layer7", + "Layer8", + "Layer9", + "Layer10", + "Layer11", + "Layer12", + "Layer13", + "Layer14", + "Layer15", + "Layer16", + "Layer17", + "Layer18", + "Layer19", + "Layer20", + "Layer21", + "Layer22", + "Layer23", + "Layer24", + "Layer25", + "Layer26", + "Layer27", + "Layer28", + "Layer29", + "Layer30", + "Layer31", + "Layer32", + }; + + public override void OnInspectorGUI() + { + base.OnInspectorGUI(); + + var colorProp = serializedObject.FindProperty(_colorPropName); + var widthProp = serializedObject.FindProperty(_widthPropName); + var intensityProp = serializedObject.FindProperty(_intensityPropName); + var cutoffProp = serializedObject.FindProperty(_cutoffPropName); + var renderModeProp = serializedObject.FindProperty(_renderModePropName); + var renderMode = (OutlineRenderFlags)renderModeProp.intValue; + + //EditorGUILayout.PropertyField(colorProp, _colorContent); + colorProp.colorValue = EditorGUILayout.ColorField(OutlineEditorUtility.ColorContent, colorProp.colorValue, true, true, true); + + EditorGUILayout.PropertyField(widthProp, OutlineEditorUtility.WidthContent); + + //EditorGUILayout.PropertyField(renderModeProp, _renderModeContent); + renderModeProp.intValue = (int)(OutlineRenderFlags)EditorGUILayout.EnumFlagsField(OutlineEditorUtility.RenderFlagsContent, renderMode); + + if ((renderMode & OutlineRenderFlags.Blurred) != 0) + { + EditorGUILayout.PropertyField(intensityProp, OutlineEditorUtility.BlurIntensityContent); + } + + if ((renderMode & OutlineRenderFlags.EnableAlphaTesting) != 0) + { + EditorGUILayout.PropertyField(cutoffProp, OutlineEditorUtility.AlphaCutoffContent); + } + + serializedObject.ApplyModifiedProperties(); + } + + internal static float GetSettingsInstanceHeight(SerializedProperty property) + { + var lineCy = EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing; + var renderModeProp = property.FindPropertyRelative(_renderModePropName); + var renderMode = (OutlineRenderFlags)renderModeProp.intValue; + var result = lineCy * 4; + + if ((renderMode & OutlineRenderFlags.Blurred) != 0) + { + result += lineCy; + } + + if ((renderMode & OutlineRenderFlags.EnableAlphaTesting) != 0) + { + result += lineCy; + } + + return result; + } + + internal static float GetSettingsWithMaskHeight(SerializedProperty property) + { + var lineCy = EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing; + var filterModeProp = property.FindPropertyRelative(_filterModePropName); + var renderOutlineSettings = false; + + if (filterModeProp.intValue == (int)OutlineFilterMode.UseLayerMask) + { + var layerMaskProp = property.FindPropertyRelative(_layerMaskPropName); + renderOutlineSettings = true; + } + else if (filterModeProp.intValue == (int)OutlineFilterMode.UseRenderingLayerMask) + { + var renderingLayerMaskProp = property.FindPropertyRelative(_renderingLayerMaskPropName); + renderOutlineSettings = true; + } + + if (renderOutlineSettings) + { + var renderModeProp = property.FindPropertyRelative(_renderModePropName); + var renderMode = (OutlineRenderFlags)renderModeProp.intValue; + var result = lineCy * 6; + + if ((renderMode & OutlineRenderFlags.Blurred) != 0) + { + result += lineCy; + } + + if ((renderMode & OutlineRenderFlags.EnableAlphaTesting) != 0) + { + result += lineCy; + } + + return result; + } + + return lineCy; + } + + internal static void DrawSettingsInstance(Rect rc, SerializedProperty property) + { + var settingsProp = property.FindPropertyRelative(_settingsPropName); + + EditorGUI.PropertyField(new Rect(rc.x, rc.y, rc.width, EditorGUIUtility.singleLineHeight), settingsProp); + EditorGUI.indentLevel += 1; + + if (settingsProp.objectReferenceValue) + { + var obj = new SerializedObject(settingsProp.objectReferenceValue); + var colorProp = obj.FindProperty(_colorPropName); + var widthProp = obj.FindProperty(_widthPropName); + var intensityProp = obj.FindProperty(_intensityPropName); + var cutoffProp = obj.FindProperty(_cutoffPropName); + var renderModeProp = obj.FindProperty(_renderModePropName); + + EditorGUI.BeginDisabledGroup(true); + DrawSettingsInternal(rc, colorProp, widthProp, intensityProp, cutoffProp, renderModeProp); + EditorGUI.EndDisabledGroup(); + } + else + { + var colorProp = property.FindPropertyRelative(_colorPropName); + var widthProp = property.FindPropertyRelative(_widthPropName); + var intensityProp = property.FindPropertyRelative(_intensityPropName); + var cutoffProp = property.FindPropertyRelative(_cutoffPropName); + var renderModeProp = property.FindPropertyRelative(_renderModePropName); + + DrawSettingsInternal(rc, colorProp, widthProp, intensityProp, cutoffProp, renderModeProp); + } + + EditorGUI.indentLevel -= 1; + } + + internal static void DrawSettingsWithMask(Rect rc, SerializedProperty property) + { + var lineCy = EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing; + var filterModeProp = property.FindPropertyRelative(_filterModePropName); + + EditorGUI.PropertyField(new Rect(rc.x, rc.y, rc.width, EditorGUIUtility.singleLineHeight), filterModeProp, OutlineEditorUtility.FilterSettingsContent); + + if (filterModeProp.intValue == (int)OutlineFilterMode.UseLayerMask) + { + var layerMaskProp = property.FindPropertyRelative(_layerMaskPropName); + + EditorGUI.indentLevel += 1; + EditorGUI.PropertyField(new Rect(rc.x, rc.y + lineCy, rc.width, EditorGUIUtility.singleLineHeight), layerMaskProp, OutlineEditorUtility.LayerMaskContent); + EditorGUI.indentLevel -= 1; + + DrawSettingsInstance(new Rect(rc.x, rc.y + lineCy * 2, rc.width, rc.height - lineCy), property); + } + else if (filterModeProp.intValue == (int)OutlineFilterMode.UseRenderingLayerMask) + { + var renderingLayerMaskProp = property.FindPropertyRelative(_renderingLayerMaskPropName); + + EditorGUI.indentLevel += 1; + renderingLayerMaskProp.intValue = EditorGUI.MaskField(new Rect(rc.x, rc.y + lineCy, rc.width, EditorGUIUtility.singleLineHeight), OutlineEditorUtility.RenderingLayerMaskContent, renderingLayerMaskProp.intValue, _renderingLayerMaskNames); + EditorGUI.indentLevel -= 1; + + DrawSettingsInstance(new Rect(rc.x, rc.y + lineCy * 2, rc.width, rc.height - lineCy), property); + } + } + + private static void DrawSettingsInternal(Rect rc, SerializedProperty colorProp, SerializedProperty widthProp, SerializedProperty intensityProp, SerializedProperty cutoffProp, SerializedProperty renderModeProp) + { + var renderMode = (OutlineRenderFlags)renderModeProp.intValue; + var lineCy = EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing; + var n = 4; + + //EditorGUI.PropertyField(new Rect(rc.x, rc.y + 1 * lineCy, rc.width, EditorGUIUtility.singleLineHeight), colorProp, _colorContent); + colorProp.colorValue = EditorGUI.ColorField(new Rect(rc.x, rc.y + 1 * lineCy, rc.width, EditorGUIUtility.singleLineHeight), OutlineEditorUtility.ColorContent, colorProp.colorValue, true, true, true); + + EditorGUI.PropertyField(new Rect(rc.x, rc.y + 2 * lineCy, rc.width, EditorGUIUtility.singleLineHeight), widthProp, OutlineEditorUtility.WidthContent); + + // NOTE: EditorGUI.PropertyField doesn't allow multi-selection, have to use EnumFlagsField explixitly. + renderModeProp.intValue = (int)(OutlineRenderFlags)EditorGUI.EnumFlagsField(new Rect(rc.x, rc.y + 3 * lineCy, rc.width, EditorGUIUtility.singleLineHeight), OutlineEditorUtility.RenderFlagsContent, renderMode); + + if ((renderMode & OutlineRenderFlags.Blurred) != 0) + { + EditorGUI.PropertyField(new Rect(rc.x, rc.y + n++ * lineCy, rc.width, EditorGUIUtility.singleLineHeight), intensityProp, OutlineEditorUtility.BlurIntensityContent); + } + + if ((renderMode & OutlineRenderFlags.EnableAlphaTesting) != 0) + { + EditorGUI.PropertyField(new Rect(rc.x, rc.y + n * lineCy, rc.width, EditorGUIUtility.singleLineHeight), cutoffProp, OutlineEditorUtility.AlphaCutoffContent); + } + } + } +} diff --git a/Editor/Scripts/OutlineSettingsEditor.cs.meta b/Editor/Scripts/OutlineSettingsEditor.cs.meta new file mode 100644 index 0000000..2569298 --- /dev/null +++ b/Editor/Scripts/OutlineSettingsEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 28f2a32d8600f8045a4b9a9916ff8801 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Scripts/OutlineSettingsInstanceDrawer.cs b/Editor/Scripts/OutlineSettingsInstanceDrawer.cs new file mode 100644 index 0000000..fa470ed --- /dev/null +++ b/Editor/Scripts/OutlineSettingsInstanceDrawer.cs @@ -0,0 +1,27 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + +namespace UnityFx.Outline +{ + [CustomPropertyDrawer(typeof(OutlineSettingsInstance))] + public class OutlineSettingsInstanceDrawer : PropertyDrawer + { + public override void OnGUI(Rect rc, SerializedProperty property, GUIContent label) + { + EditorGUI.BeginProperty(rc, label, property); + OutlineSettingsEditor.DrawSettingsInstance(rc, property); + EditorGUI.EndProperty(); + } + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + return OutlineSettingsEditor.GetSettingsInstanceHeight(property); + } + } +} diff --git a/Editor/Scripts/OutlineSettingsInstanceDrawer.cs.meta b/Editor/Scripts/OutlineSettingsInstanceDrawer.cs.meta new file mode 100644 index 0000000..99ae287 --- /dev/null +++ b/Editor/Scripts/OutlineSettingsInstanceDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9a7d070135166b04783f98841111c742 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Scripts/OutlineSettingsWithLayerMaskDrawer.cs b/Editor/Scripts/OutlineSettingsWithLayerMaskDrawer.cs new file mode 100644 index 0000000..76c1971 --- /dev/null +++ b/Editor/Scripts/OutlineSettingsWithLayerMaskDrawer.cs @@ -0,0 +1,27 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + +namespace UnityFx.Outline +{ + [CustomPropertyDrawer(typeof(OutlineSettingsWithLayerMask))] + public class OutlineSettingsWithLayerMaskDrawer : PropertyDrawer + { + public override void OnGUI(Rect rc, SerializedProperty property, GUIContent label) + { + EditorGUI.BeginProperty(rc, label, property); + OutlineSettingsEditor.DrawSettingsWithMask(rc, property); + EditorGUI.EndProperty(); + } + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + return OutlineSettingsEditor.GetSettingsWithMaskHeight(property); + } + } +} diff --git a/Editor/Scripts/OutlineSettingsWithLayerMaskDrawer.cs.meta b/Editor/Scripts/OutlineSettingsWithLayerMaskDrawer.cs.meta new file mode 100644 index 0000000..358c115 --- /dev/null +++ b/Editor/Scripts/OutlineSettingsWithLayerMaskDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5558cd049bcb613438115d59a4376272 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/UnityFx.Outline.Editor.asmdef b/Editor/UnityFx.Outline.Editor.asmdef new file mode 100644 index 0000000..04858cc --- /dev/null +++ b/Editor/UnityFx.Outline.Editor.asmdef @@ -0,0 +1,16 @@ +{ + "name": "UnityFx.Outline.Editor", + "references": [ + "UnityFx.Outline" + ], + "optionalUnityReferences": [], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [] +} \ No newline at end of file diff --git a/Editor/UnityFx.Outline.Editor.asmdef.meta b/Editor/UnityFx.Outline.Editor.asmdef.meta new file mode 100644 index 0000000..ec90eb8 --- /dev/null +++ b/Editor/UnityFx.Outline.Editor.asmdef.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 9ce6758f27194b64fadf06ac518b5196 +timeCreated: 1566558329 +licenseType: Pro +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/README.md b/README.md new file mode 100644 index 0000000..ab04468 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# UnityFx.Outline + +## SUMMARY +Screen-space outline effect for Unity. + +## USEFUL LINKS +* [Github project](https://github.com/Arvtesh/UnityFx.Outline) +* [npm package](https://www.npmjs.com/package/com.unityfx.outline) +* [Documentation](https://github.com/Arvtesh/UnityFx.Outline/blob/master/README.md) +* [License](https://github.com/Arvtesh/UnityFx.Outline/blob/master/LICENSE.md) +* [Support](mailto:arvtesh@gmail.com) diff --git a/README.md.meta b/README.md.meta new file mode 100644 index 0000000..eb46e78 --- /dev/null +++ b/README.md.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 4b1c4eed7166ed4429494dc10c2a3d6c +timeCreated: 1566148623 +licenseType: Free +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime.meta b/Runtime.meta new file mode 100644 index 0000000..a63e99c --- /dev/null +++ b/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c0a74cc33268cc443a59bdce8fcf3948 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Prefabs.meta b/Runtime/Prefabs.meta new file mode 100644 index 0000000..88c5e98 --- /dev/null +++ b/Runtime/Prefabs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 82099d138ba6a5c40a5915c4ca5211fe +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Prefabs/OutlineResources.asset b/Runtime/Prefabs/OutlineResources.asset new file mode 100644 index 0000000..b00ab91 --- /dev/null +++ b/Runtime/Prefabs/OutlineResources.asset @@ -0,0 +1,17 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: b503341e0a514e3489c4851727e68257, type: 3} + m_Name: OutlineResources + m_EditorClassIdentifier: + _renderShader: {fileID: 4800000, guid: ac20fbf75bafe454aba5ef3c098349df, type: 3} + _outlineShader: {fileID: 4800000, guid: 41c9acbf41c8245498ac9beab378de12, type: 3} + _enableInstancing: 0 diff --git a/Runtime/Prefabs/OutlineResources.asset.meta b/Runtime/Prefabs/OutlineResources.asset.meta new file mode 100644 index 0000000..d66452d --- /dev/null +++ b/Runtime/Prefabs/OutlineResources.asset.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: d28e70f030b1a634db9a6a6d5478ef19 +timeCreated: 1566149572 +licenseType: Free +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Runtime.meta b/Runtime/Runtime.meta new file mode 100644 index 0000000..d7752ff --- /dev/null +++ b/Runtime/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 437f4cd62fcd5614e807205e9adf0cb6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Runtime/HighLightManager.cs b/Runtime/Runtime/HighLightManager.cs new file mode 100644 index 0000000..a9c18d2 --- /dev/null +++ b/Runtime/Runtime/HighLightManager.cs @@ -0,0 +1,46 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityFx.Outline; + +public class HighLightManager : MonoBehaviour +{ + static OutlineLayerCollection layerCollection; + private static HighLightManager instance = null; + + public static HighLightManager Instance + { + get + { + if (instance == null) + { + GameObject go = new GameObject("HightlightMananger"); + DontDestroyOnLoad(go); + instance = go.AddComponent (); + layerCollection = Resources.Load ("OutlineLayerCollection"); + + } + return instance; + } + } + ///// + ///// 测试使用 + ///// + //[RuntimeInitializeOnLoadMethod] + //private static void Test() + //{ + // Instance.ControllerHighLight(GameObject.Find("Cube"), true); + //} + private void OnApplicationQuit() + { + layerCollection?.ClearLayerContent(); + } + public void ControllerHighLight(GameObject go, bool value) + { + if (value) + layerCollection.GetOrAddLayer(0).Add(go); + else + layerCollection.GetOrAddLayer(0).Remove(go); + } + +} diff --git a/Runtime/Runtime/HighLightManager.cs.meta b/Runtime/Runtime/HighLightManager.cs.meta new file mode 100644 index 0000000..3dc26ad --- /dev/null +++ b/Runtime/Runtime/HighLightManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ef887207b8bcd304c9e6ab4b272ccf0c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Runtime/Prefabs.meta b/Runtime/Runtime/Prefabs.meta new file mode 100644 index 0000000..5e39502 --- /dev/null +++ b/Runtime/Runtime/Prefabs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 66d913784d1955a4fbf8279b0a0f5d7b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Runtime/Prefabs/OutlineResources.URP.asset b/Runtime/Runtime/Prefabs/OutlineResources.URP.asset new file mode 100644 index 0000000..7a704f1 --- /dev/null +++ b/Runtime/Runtime/Prefabs/OutlineResources.URP.asset @@ -0,0 +1,17 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: b503341e0a514e3489c4851727e68257, type: 3} + m_Name: OutlineResources.URP + m_EditorClassIdentifier: + _renderShader: {fileID: 4800000, guid: 2140fc327e711b549bc9fe301e6f4621, type: 3} + _outlineShader: {fileID: 4800000, guid: da6518c999b52e743bff80732b460ff4, type: 3} + _enableInstancing: 0 diff --git a/Runtime/Runtime/Prefabs/OutlineResources.URP.asset.meta b/Runtime/Runtime/Prefabs/OutlineResources.URP.asset.meta new file mode 100644 index 0000000..265eebe --- /dev/null +++ b/Runtime/Runtime/Prefabs/OutlineResources.URP.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 231d88937a104094b8e4e0fdb8d2e77b +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Runtime/Scripts.meta b/Runtime/Runtime/Scripts.meta new file mode 100644 index 0000000..100ae09 --- /dev/null +++ b/Runtime/Runtime/Scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 85518f862b075044bbd76d57354f8f3e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Runtime/Scripts/OutlineFeature.cs b/Runtime/Runtime/Scripts/OutlineFeature.cs new file mode 100644 index 0000000..2847198 --- /dev/null +++ b/Runtime/Runtime/Scripts/OutlineFeature.cs @@ -0,0 +1,91 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +using System; +using UnityEngine; +using UnityEngine.Rendering; +using UnityEngine.Rendering.Universal; + +namespace UnityFx.Outline.URP +{ + ///+ /// Outline feature (URP). + /// + ///+ /// Add instance of this class to + public class OutlineFeature : ScriptableRendererFeature + { + #region data + +#pragma warning disable 0649 + + [SerializeField, Tooltip(OutlineResources.OutlineResourcesTooltip)] + private OutlineResources _outlineResources; + [SerializeField, Tooltip(OutlineResources.OutlineLayerCollectionTooltip)] + private OutlineLayerCollection _outlineLayers; + [SerializeField] + private OutlineSettingsWithLayerMask _outlineSettings; + [SerializeField] + private RenderPassEvent _renderPassEvent = RenderPassEvent.AfterRenderingSkybox; + [SerializeField] + public string[] _shaderPassNames; + +#pragma warning restore 0649 + + private OutlinePass _outlinePass; + private string _featureName; + + #endregion + + #region interface + + internal OutlineResources OutlineResources => _outlineResources; + + internal OutlineLayerCollection OutlineLayers => _outlineLayers; + + internal IOutlineSettings OutlineSettings => _outlineSettings; + + internal int OutlineLayerMask => _outlineSettings.OutlineLayerMask; + + internal uint OutlineRenderingLayerMask => _outlineSettings.OutlineRenderingLayerMask; + + internal string FeatureName => _featureName; + + #endregion + + #region ScriptableRendererFeature + + ///. Configure + /// and assign outline resources and layers collection. Make sure + /// is set if you use . + /// + public override void Create() + { + if (_outlineSettings != null) + { + _featureName = OutlineResources.EffectName + '-' + _outlineSettings.OutlineLayerMask; + } + else + { + _featureName = OutlineResources.EffectName; + } + + _outlinePass = new OutlinePass(this, _shaderPassNames) + { + renderPassEvent = _renderPassEvent + }; + } + + /// + public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) + { + if (_outlineResources && _outlineResources.IsValid) + { + _outlinePass.Setup(renderer); + renderer.EnqueuePass(_outlinePass); + } + } + + #endregion + } +} diff --git a/Runtime/Runtime/Scripts/OutlineFeature.cs.meta b/Runtime/Runtime/Scripts/OutlineFeature.cs.meta new file mode 100644 index 0000000..6c8367d --- /dev/null +++ b/Runtime/Runtime/Scripts/OutlineFeature.cs.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: dd37d03d18ee9584d881763c34816b35 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: + - _outlineResources: {fileID: 11400000, guid: 231d88937a104094b8e4e0fdb8d2e77b, + type: 2} + - _outlineLayers: {instanceID: 0} + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Runtime/Scripts/OutlinePass.cs b/Runtime/Runtime/Scripts/OutlinePass.cs new file mode 100644 index 0000000..5cf5821 --- /dev/null +++ b/Runtime/Runtime/Scripts/OutlinePass.cs @@ -0,0 +1,106 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Rendering; +using UnityEngine.Rendering.Universal; +using UnityEngine.XR; + +namespace UnityFx.Outline.URP +{ + internal class OutlinePass : ScriptableRenderPass + { + private const string _profilerTag = "OutlinePass"; + + private readonly OutlineFeature _feature; + private readonly List _renderObjects = new List (); + private readonly List _shaderTagIdList = new List (); + + private ScriptableRenderer _renderer; + + public OutlinePass(OutlineFeature feature, string[] shaderTags) + { + _feature = feature; + + if (shaderTags != null && shaderTags.Length > 0) + { + foreach (var passName in shaderTags) + { + _shaderTagIdList.Add(new ShaderTagId(passName)); + } + } + else + { + _shaderTagIdList.Add(new ShaderTagId("UniversalForward")); + _shaderTagIdList.Add(new ShaderTagId("LightweightForward")); + _shaderTagIdList.Add(new ShaderTagId("SRPDefaultUnlit")); + } + } + + public void Setup(ScriptableRenderer renderer) + { + _renderer = renderer; + } + + public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) + { + var outlineResources = _feature.OutlineResources; + var outlineSettings = _feature.OutlineSettings; + var camData = renderingData.cameraData; + var depthTexture = new RenderTargetIdentifier("_CameraDepthTexture"); + + if (_feature.OutlineLayerMask != 0) + { + var cmd = CommandBufferPool.Get(_feature.FeatureName); + var filteringSettings = new FilteringSettings(RenderQueueRange.all, _feature.OutlineLayerMask, _feature.OutlineRenderingLayerMask); + var renderStateBlock = new RenderStateBlock(RenderStateMask.Nothing); + var sortingCriteria = camData.defaultOpaqueSortFlags; + var drawingSettings = CreateDrawingSettings(_shaderTagIdList, ref renderingData, sortingCriteria); + + drawingSettings.enableDynamicBatching = true; + drawingSettings.overrideMaterial = outlineResources.RenderMaterial; + + if (outlineSettings.IsAlphaTestingEnabled()) + { + drawingSettings.overrideMaterialPassIndex = OutlineResources.RenderShaderAlphaTestPassId; + cmd.SetGlobalFloat(outlineResources.AlphaCutoffId, outlineSettings.OutlineAlphaCutoff); + } + else + { + drawingSettings.overrideMaterialPassIndex = OutlineResources.RenderShaderDefaultPassId; + } + + using (var renderer = new OutlineRenderer(cmd, outlineResources, _renderer.cameraColorTarget, depthTexture, camData.cameraTargetDescriptor)) + { + renderer.RenderObjectClear(outlineSettings.OutlineRenderMode); + context.ExecuteCommandBuffer(cmd); + + context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref filteringSettings, ref renderStateBlock); + + cmd.Clear(); + renderer.RenderOutline(outlineSettings); + } + + context.ExecuteCommandBuffer(cmd); + CommandBufferPool.Release(cmd); + } + + if (_feature.OutlineLayers) + { + var cmd = CommandBufferPool.Get(OutlineResources.EffectName); + + using (var renderer = new OutlineRenderer(cmd, outlineResources, _renderer.cameraColorTarget, depthTexture, camData.cameraTargetDescriptor)) + { + _renderObjects.Clear(); + _feature.OutlineLayers.GetRenderObjects(_renderObjects); + renderer.Render(_renderObjects); + } + + context.ExecuteCommandBuffer(cmd); + CommandBufferPool.Release(cmd); + } + } + } +} diff --git a/Runtime/Runtime/Scripts/OutlinePass.cs.meta b/Runtime/Runtime/Scripts/OutlinePass.cs.meta new file mode 100644 index 0000000..5c90ec9 --- /dev/null +++ b/Runtime/Runtime/Scripts/OutlinePass.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7fda4bd4356b87c4a93e27fbc5390d5f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Runtime/Shaders.meta b/Runtime/Runtime/Shaders.meta new file mode 100644 index 0000000..b6518ed --- /dev/null +++ b/Runtime/Runtime/Shaders.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5e29d2be8edefda438a2865a7030dad6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Runtime/Shaders/Outline.URP.shader b/Runtime/Runtime/Shaders/Outline.URP.shader new file mode 100644 index 0000000..bb5af30 --- /dev/null +++ b/Runtime/Runtime/Shaders/Outline.URP.shader @@ -0,0 +1,212 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +// Renders outline based on a texture produced with 'UnityF/OutlineColor'. +// Modified version of 'Custom/Post Outline' shader taken from https://willweissman.wordpress.com/tutorials/shaders/unity-shaderlab-object-outlines/. +Shader "Hidden/UnityFx/Outline.URP" +{ + HLSLINCLUDE + + #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" + + TEXTURE2D_X(_MaskTex); + SAMPLER(sampler_MaskTex); + + TEXTURE2D_X(_MainTex); + SAMPLER(sampler_MainTex); + float2 _MainTex_TexelSize; + + float4 _Color; + float _Intensity; + int _Width; + float _GaussSamples[32]; + + struct Varyings + { + float4 positionCS : SV_POSITION; + float2 uv : TEXCOORD0; + UNITY_VERTEX_OUTPUT_STEREO + }; + +#if SHADER_TARGET < 35 || _USE_DRAWMESH + + struct Attributes + { + float4 positionOS : POSITION; + float2 uv : TEXCOORD0; + UNITY_VERTEX_INPUT_INSTANCE_ID + }; + + Varyings VertexSimple(Attributes input) + { + Varyings output = (Varyings)0; + + UNITY_SETUP_INSTANCE_ID(input); + UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output); + + output.positionCS = float4(input.positionOS.xy, UNITY_NEAR_CLIP_VALUE, 1); + output.uv = ComputeScreenPos(output.positionCS).xy; + + return output; + } + +#else + + struct Attributes + { + uint vertexID : SV_VertexID; + UNITY_VERTEX_INPUT_INSTANCE_ID + }; + + Varyings VertexSimple(Attributes input) + { + Varyings output = (Varyings)0; + + UNITY_SETUP_INSTANCE_ID(input); + UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output); + + output.positionCS = GetFullScreenTriangleVertexPosition(input.vertexID); + output.uv = GetFullScreenTriangleTexCoord(input.vertexID); + + return output; + } + +#endif + + float CalcIntensityN0(float2 uv, float2 offset, int k) + { + return SAMPLE_TEXTURE2D_X(_MainTex, sampler_MainTex, uv + k * offset).r * _GaussSamples[k]; + } + + float CalcIntensityN1(float2 uv, float2 offset, int k) + { + return SAMPLE_TEXTURE2D_X(_MainTex, sampler_MainTex, uv - k * offset).r * _GaussSamples[k]; + } + + float CalcIntensity(float2 uv, float2 offset) + { + float intensity = 0; + + // Accumulates horizontal or vertical blur intensity for the specified texture position. + // Set offset = (tx, 0) for horizontal sampling and offset = (0, ty) for vertical. + // + // NOTE: Unroll directive is needed to make the method function on platforms like WebGL 1.0 where loops are not supported. + // If maximum outline width is changed here, it should be changed in OutlineResources.MaxWidth as well. + // + [unroll(32)] + for (int k = 1; k <= _Width; ++k) + { + intensity += CalcIntensityN0(uv, offset, k); + intensity += CalcIntensityN1(uv, offset, k); + } + + intensity += CalcIntensityN0(uv, offset, 0); + return intensity; + } + + float4 FragmentH(Varyings i) : SV_Target + { + UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i); + + float2 uv = UnityStereoTransformScreenSpaceTex(i.uv); + float intensity = CalcIntensity(uv, float2(_MainTex_TexelSize.x, 0)); + return float4(intensity, intensity, intensity, 1); + } + + float4 FragmentV(Varyings i) : SV_Target + { + UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i); + + float2 uv = UnityStereoTransformScreenSpaceTex(i.uv); + + if (SAMPLE_TEXTURE2D_X(_MaskTex, sampler_MaskTex, uv).r > 0) + { + // TODO: Avoid discard/clip to improve performance on mobiles. + discard; + } + + float intensity = CalcIntensity(uv, float2(0, _MainTex_TexelSize.y)); + intensity = _Intensity > 99 ? step(0.01, intensity) : intensity * _Intensity; + return float4(_Color.rgb, saturate(_Color.a * intensity)); + } + + ENDHLSL + + // SM3.5+ + SubShader + { + Tags{ "RenderPipeline" = "UniversalPipeline" } + + Cull Off + ZWrite Off + ZTest Always + Lighting Off + + Pass + { + Name "HPass" + + HLSLPROGRAM + + #pragma target 3.5 + #pragma multi_compile_instancing + #pragma shader_feature_local _USE_DRAWMESH + #pragma vertex VertexSimple + #pragma fragment FragmentH + + ENDHLSL + } + + Pass + { + Name "VPassBlend" + Blend SrcAlpha OneMinusSrcAlpha + + HLSLPROGRAM + + #pragma target 3.5 + #pragma multi_compile_instancing + #pragma shader_feature_local _USE_DRAWMESH + #pragma vertex VertexSimple + #pragma fragment FragmentV + + ENDHLSL + } + } + + // SM2.0 + SubShader + { + Tags { "RenderPipeline" = "UniversalPipeline" } + + Cull Off + ZWrite Off + ZTest Always + Lighting Off + + Pass + { + Name "HPass" + + HLSLPROGRAM + + #pragma vertex VertexSimple + #pragma fragment FragmentH + + ENDHLSL + } + + Pass + { + Name "VPassBlend" + Blend SrcAlpha OneMinusSrcAlpha + + HLSLPROGRAM + + #pragma vertex VertexSimple + #pragma fragment FragmentV + + ENDHLSL + } + } +} diff --git a/Runtime/Runtime/Shaders/Outline.URP.shader.meta b/Runtime/Runtime/Shaders/Outline.URP.shader.meta new file mode 100644 index 0000000..51c28b4 --- /dev/null +++ b/Runtime/Runtime/Shaders/Outline.URP.shader.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: da6518c999b52e743bff80732b460ff4 +ShaderImporter: + externalObjects: {} + defaultTextures: [] + nonModifiableTextures: [] + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Runtime/Shaders/OutlineColor.URP.shader b/Runtime/Runtime/Shaders/OutlineColor.URP.shader new file mode 100644 index 0000000..80f78ea --- /dev/null +++ b/Runtime/Runtime/Shaders/OutlineColor.URP.shader @@ -0,0 +1,67 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +// Renders everything with while color. +// Modified version of 'Custom/DrawSimple' shader taken from https://willweissman.wordpress.com/tutorials/shaders/unity-shaderlab-object-outlines/. +Shader "Hidden/UnityFx/OutlineColor.URP" +{ + HLSLINCLUDE + + #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" + #include "Packages/com.unity.render-pipelines.universal/Shaders/PostProcessing/Common.hlsl" + + TEXTURE2D(_MainTex); + SAMPLER(sampler_MainTex); + + half _Cutoff; + + half4 FragmentSimple(Varyings input) : SV_Target + { + return 1; + } + + half4 FragmentAlphaTest(Varyings input) : SV_Target + { + half4 c = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv); + clip(c.a - _Cutoff); + return 1; + } + + ENDHLSL + + SubShader + { + Tags { "RenderPipeline" = "UniversalPipeline" } + + Cull Off + ZWrite Off + ZTest LEqual + Lighting Off + + Pass + { + Name "Opaque" + + HLSLPROGRAM + + #pragma multi_compile_instancing + #pragma vertex Vert + #pragma fragment FragmentSimple + + ENDHLSL + } + + Pass + { + Name "Transparent" + + HLSLPROGRAM + + #pragma multi_compile_instancing + #pragma vertex Vert + #pragma fragment FragmentAlphaTest + + ENDHLSL + } + } +} diff --git a/Runtime/Runtime/Shaders/OutlineColor.URP.shader.meta b/Runtime/Runtime/Shaders/OutlineColor.URP.shader.meta new file mode 100644 index 0000000..8272b33 --- /dev/null +++ b/Runtime/Runtime/Shaders/OutlineColor.URP.shader.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 2140fc327e711b549bc9fe301e6f4621 +ShaderImporter: + externalObjects: {} + defaultTextures: [] + nonModifiableTextures: [] + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Runtime/UnityFx.Outline.URP.asmdef b/Runtime/Runtime/UnityFx.Outline.URP.asmdef new file mode 100644 index 0000000..3f9ff3f --- /dev/null +++ b/Runtime/Runtime/UnityFx.Outline.URP.asmdef @@ -0,0 +1,17 @@ +{ + "name": "UnityFx.Outline.URP", + "references": [ + "Unity.RenderPipelines.Core.Runtime", + "Unity.RenderPipelines.Universal.Runtime", + "UnityFx.Outline" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Runtime/Runtime/UnityFx.Outline.URP.asmdef.meta b/Runtime/Runtime/UnityFx.Outline.URP.asmdef.meta new file mode 100644 index 0000000..418ae3e --- /dev/null +++ b/Runtime/Runtime/UnityFx.Outline.URP.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 8130e23c3199afb43ae1c34b3e328d00 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts.meta b/Runtime/Scripts.meta new file mode 100644 index 0000000..fdf3731 --- /dev/null +++ b/Runtime/Scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5474ddc00e5e1574cba82c3dbad68ded +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/IOutlineSettings.cs b/Runtime/Scripts/IOutlineSettings.cs new file mode 100644 index 0000000..f44bbe7 --- /dev/null +++ b/Runtime/Scripts/IOutlineSettings.cs @@ -0,0 +1,51 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +using System; +using UnityEngine; + +namespace UnityFx.Outline +{ + /// + /// Generic outline settings. + /// + public interface IOutlineSettings : IEquatable+ { + /// + /// Gets or sets outline color. + /// + ///+ /// + Color OutlineColor { get; set; } + + /// + /// Gets or sets outline width in pixels. Allowed range is [ + ///, ]. + /// + /// + int OutlineWidth { get; set; } + + /// + /// Gets or sets outline intensity value. Allowed range is [ + ///, ]. + /// This is used for blurred oulines only (i.e. has flag). + /// + /// + /// + float OutlineIntensity { get; set; } + + /// + /// Gets or sets alpha cutoff value. Allowed range is [0, 1]. This is used only when + ///has flag. + /// + float OutlineAlphaCutoff { get; set; } + + /// + /// Gets or sets outline render mode. + /// + ///+ /// + /// + OutlineRenderFlags OutlineRenderMode { get; set; } + } +} diff --git a/Runtime/Scripts/IOutlineSettings.cs.meta b/Runtime/Scripts/IOutlineSettings.cs.meta new file mode 100644 index 0000000..688fd30 --- /dev/null +++ b/Runtime/Scripts/IOutlineSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: efc18f75d5206f14a80e9306650c858a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/OutlineBehaviour.cs b/Runtime/Scripts/OutlineBehaviour.cs new file mode 100644 index 0000000..12193c9 --- /dev/null +++ b/Runtime/Scripts/OutlineBehaviour.cs @@ -0,0 +1,443 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Rendering; + +namespace UnityFx.Outline +{ + /// + /// Attach this script to a + ///to add outline effect. It can be configured in edit-time or in runtime via scripts. + /// + [ExecuteInEditMode] + [DisallowMultipleComponent] + public sealed class OutlineBehaviour : MonoBehaviour, IOutlineSettings + { + #region data + +#pragma warning disable 0649 + + [SerializeField, Tooltip(OutlineResources.OutlineResourcesTooltip)] + private OutlineResources _outlineResources; + [SerializeField, HideInInspector] + private OutlineSettingsInstance _outlineSettings; + [SerializeField, HideInInspector] + private int _ignoreLayerMask; + [SerializeField, HideInInspector] + private CameraEvent _cameraEvent = OutlineRenderer.RenderEvent; + [SerializeField, HideInInspector] + private Camera _targetCamera; + [SerializeField, Tooltip("If set, list of object renderers is updated on each frame. Enable if the object has child renderers which are enabled/disabled frequently.")] + private bool _updateRenderers; + +#pragma warning restore 0649 + + private Dictionary _cameraMap = new Dictionary (); + private List _camerasToRemove = new List (); + private OutlineRendererCollection _renderers; + + #endregion + + #region interface + + /// + /// Gets or sets resources used by the effect implementation. + /// + ///Thrown if setter argument is + ///. + public OutlineResources OutlineResources + { + get + { + return _outlineResources; + } + set + { + if (value is null) + { + throw new ArgumentNullException(nameof(OutlineResources)); + } + + _outlineResources = value; + } + } + + /// + /// Gets or sets outline settings. Set this to non- + ///value to share settings with other components. + /// + public OutlineSettings OutlineSettings + { + get + { + if (_outlineSettings == null) + { + _outlineSettings = new OutlineSettingsInstance(); + } + + return _outlineSettings.OutlineSettings; + } + set + { + if (_outlineSettings == null) + { + _outlineSettings = new OutlineSettingsInstance(); + } + + _outlineSettings.OutlineSettings = value; + } + } + + /// + /// Gets or sets layer mask to use for ignored + public int IgnoreLayerMask + { + get + { + return _ignoreLayerMask; + } + set + { + if (_ignoreLayerMask != value) + { + _ignoreLayerMask = value; + _renderers?.Reset(false, value); + } + } + } + + ///components in this game object. + /// + /// Gets or sets + public CameraEvent RenderEvent + { + get + { + return _cameraEvent; + } + set + { + if (_cameraEvent != value) + { + foreach (var kvp in _cameraMap) + { + if (kvp.Key) + { + kvp.Key.RemoveCommandBuffer(_cameraEvent, kvp.Value); + kvp.Key.AddCommandBuffer(value, kvp.Value); + } + } + + _cameraEvent = value; + } + } + } + + ///used to render the outlines. + /// + /// Gets outline renderers. By default all child + ///components are used for outlining. + /// + public ICollection OutlineRenderers + { + get + { + CreateRenderersIfNeeded(); + return _renderers; + } + } + + /// + /// Gets or sets camera to render outlines to. If not set, outlines are rendered to all active cameras. + /// + ///+ public Camera Camera + { + get + { + return _targetCamera; + } + set + { + if (_targetCamera != value) + { + if (value) + { + _camerasToRemove.Clear(); + + foreach (var kvp in _cameraMap) + { + if (kvp.Key && kvp.Key != value) + { + kvp.Key.RemoveCommandBuffer(_cameraEvent, kvp.Value); + kvp.Value.Dispose(); + + _camerasToRemove.Add(kvp.Key); + } + } + + foreach (var camera in _camerasToRemove) + { + _cameraMap.Remove(camera); + } + } + + _targetCamera = value; + } + } + } + + /// + /// Gets all cameras outline data is rendered to. + /// + ///+ public ICollection Cameras => _cameraMap.Keys; + + /// + /// Updates renderer list. + /// + ///+ public void UpdateRenderers() + { + _renderers?.Reset(false, _ignoreLayerMask); + } + + #endregion + + #region MonoBehaviour + + private void Awake() + { + OutlineResources.LogSrpNotSupported(this); + OutlineResources.LogPpNotSupported(this); + + CreateRenderersIfNeeded(); + CreateSettingsIfNeeded(); + } + + private void OnEnable() + { + Camera.onPreRender += OnCameraPreRender; + } + + private void OnDisable() + { + Camera.onPreRender -= OnCameraPreRender; + + foreach (var kvp in _cameraMap) + { + if (kvp.Key) + { + kvp.Key.RemoveCommandBuffer(_cameraEvent, kvp.Value); + } + + kvp.Value.Dispose(); + } + + _cameraMap.Clear(); + } + + private void Update() + { + if (_outlineResources != null && _renderers != null) + { + _camerasToRemove.Clear(); + + if (_updateRenderers) + { + _renderers.Reset(false, _ignoreLayerMask); + } + + foreach (var kvp in _cameraMap) + { + var camera = kvp.Key; + var cmdBuffer = kvp.Value; + + if (camera) + { + cmdBuffer.Clear(); + FillCommandBuffer(camera, cmdBuffer); + } + else + { + cmdBuffer.Dispose(); + _camerasToRemove.Add(camera); + } + } + + foreach (var camera in _camerasToRemove) + { + _cameraMap.Remove(camera); + } + } + } + +#if UNITY_EDITOR + + private void OnValidate() + { + CreateRenderersIfNeeded(); + CreateSettingsIfNeeded(); + } + + private void Reset() + { + if (_renderers != null) + { + _renderers.Reset(false, _ignoreLayerMask); + } + } + +#endif + + #endregion + + #region IOutlineSettings + + /// + public Color OutlineColor + { + get + { + CreateSettingsIfNeeded(); + return _outlineSettings.OutlineColor; + } + set + { + CreateSettingsIfNeeded(); + _outlineSettings.OutlineColor = value; + } + } + + /// + public int OutlineWidth + { + get + { + CreateSettingsIfNeeded(); + return _outlineSettings.OutlineWidth; + } + set + { + CreateSettingsIfNeeded(); + _outlineSettings.OutlineWidth = value; + } + } + + /// + public float OutlineIntensity + { + get + { + CreateSettingsIfNeeded(); + return _outlineSettings.OutlineIntensity; + } + set + { + CreateSettingsIfNeeded(); + _outlineSettings.OutlineIntensity = value; + } + } + + /// + public float OutlineAlphaCutoff + { + get + { + CreateSettingsIfNeeded(); + return _outlineSettings.OutlineAlphaCutoff; + } + set + { + CreateSettingsIfNeeded(); + _outlineSettings.OutlineAlphaCutoff = value; + } + } + + /// + public OutlineRenderFlags OutlineRenderMode + { + get + { + CreateSettingsIfNeeded(); + return _outlineSettings.OutlineRenderMode; + } + set + { + CreateSettingsIfNeeded(); + _outlineSettings.OutlineRenderMode = value; + } + } + + #endregion + + #region IEquatable + + /// + public bool Equals(IOutlineSettings other) + { + return OutlineSettings.Equals(_outlineSettings, other); + } + + #endregion + + #region implementation + + private void OnCameraPreRender(Camera camera) + { + if (camera && (!_targetCamera || _targetCamera == camera)) + { + if (_outlineSettings.RequiresCameraDepth) + { + camera.depthTextureMode |= DepthTextureMode.Depth; + } + + if (!_cameraMap.ContainsKey(camera)) + { + var cmdBuf = new CommandBuffer(); + cmdBuf.name = string.Format("{0} - {1}", GetType().Name, name); + camera.AddCommandBuffer(_cameraEvent, cmdBuf); + + _cameraMap.Add(camera, cmdBuf); +#if UNITY_EDITOR + FillCommandBuffer(camera, cmdBuf); +#endif + } + } + } + + private void FillCommandBuffer(Camera camera, CommandBuffer cmdBuffer) + { + if (_renderers.Count > 0) + { + using (var renderer = new OutlineRenderer(cmdBuffer, _outlineResources, camera.actualRenderingPath)) + { + renderer.Render(_renderers.GetList(), _outlineSettings, name); + } + } + } + + private void CreateSettingsIfNeeded() + { + if (_outlineSettings == null) + { + _outlineSettings = new OutlineSettingsInstance(); + } + } + + private void CreateRenderersIfNeeded() + { + if (_renderers == null) + { + _renderers = new OutlineRendererCollection(gameObject); + _renderers.Reset(false, _ignoreLayerMask); + } + } + + #endregion + } +} diff --git a/Runtime/Scripts/OutlineBehaviour.cs.meta b/Runtime/Scripts/OutlineBehaviour.cs.meta new file mode 100644 index 0000000..a9c85ed --- /dev/null +++ b/Runtime/Scripts/OutlineBehaviour.cs.meta @@ -0,0 +1,15 @@ +fileFormatVersion: 2 +guid: 271c580db5fd384429cdac899152e9e0 +timeCreated: 1566149857 +licenseType: Free +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: + - _outlineResources: {fileID: 11400000, guid: d28e70f030b1a634db9a6a6d5478ef19, + type: 2} + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/OutlineBuilder.cs b/Runtime/Scripts/OutlineBuilder.cs new file mode 100644 index 0000000..f1b7433 --- /dev/null +++ b/Runtime/Scripts/OutlineBuilder.cs @@ -0,0 +1,93 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace UnityFx.Outline +{ + /// + /// A helper behaviour for managing content of + public sealed class OutlineBuilder : MonoBehaviour + { + #region data + + [Serializable] + internal class ContentItem + { + public GameObject Go; + public int LayerIndex; + } + +#pragma warning disable 0649 + + [SerializeField, Tooltip(OutlineResources.OutlineLayerCollectionTooltip)] + private OutlineLayerCollection _outlineLayers; + [SerializeField, HideInInspector] + private Listvia Unity Editor. + /// _content; + +#pragma warning restore 0649 + + #endregion + + #region interface + + internal List Content { get => _content; set => _content = value; } + + /// + /// Gets or sets a collection of layers to manage. + /// + public OutlineLayerCollection OutlineLayers { get => _outlineLayers; set => _outlineLayers = value; } + + ///+ /// Clears content of all layers. + /// + ///+ public void Clear() + { + _outlineLayers?.ClearLayerContent(); + } + + #endregion + + #region MonoBehaviour + + private void OnEnable() + { + if (_outlineLayers && _content != null) + { + foreach (var item in _content) + { + if (item.LayerIndex >= 0 && item.LayerIndex < _outlineLayers.Count && item.Go) + { + _outlineLayers.GetOrAddLayer(item.LayerIndex).Add(item.Go); + } + } + } + } + +#if UNITY_EDITOR + + private void Reset() + { + var effect = GetComponent (); + + if (effect) + { + _outlineLayers = effect.OutlineLayersInternal; + } + } + + private void OnDestroy() + { + _outlineLayers?.ClearLayerContent(); + } + +#endif + + #endregion + } +} diff --git a/Runtime/Scripts/OutlineBuilder.cs.meta b/Runtime/Scripts/OutlineBuilder.cs.meta new file mode 100644 index 0000000..fc6b25c --- /dev/null +++ b/Runtime/Scripts/OutlineBuilder.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e746e776b0ae00d4a9d458b9430b95d7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/OutlineEffect.cs b/Runtime/Scripts/OutlineEffect.cs new file mode 100644 index 0000000..3d5eda5 --- /dev/null +++ b/Runtime/Scripts/OutlineEffect.cs @@ -0,0 +1,267 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Rendering; + +namespace UnityFx.Outline +{ + /// + /// Renders outlines at specific camera. Should be attached to camera to function. + /// + ///+ /// + /// + /// + [ExecuteInEditMode] + [RequireComponent(typeof(Camera))] + public sealed partial class OutlineEffect : MonoBehaviour + { + #region data + + [SerializeField, Tooltip(OutlineResources.OutlineResourcesTooltip)] + private OutlineResources _outlineResources; + [SerializeField, Tooltip(OutlineResources.OutlineLayerCollectionTooltip)] + private OutlineLayerCollection _outlineLayers; + [SerializeField, HideInInspector] + private CameraEvent _cameraEvent = OutlineRenderer.RenderEvent; + + private Camera _camera; + private CommandBuffer _commandBuffer; + private List _renderObjects = new List (16); + + #endregion + + #region interface + + /// + /// Gets or sets resources used by the effect implementation. + /// + ///Thrown if setter argument is + public OutlineResources OutlineResources + { + get + { + return _outlineResources; + } + set + { + if (value is null) + { + throw new ArgumentNullException(nameof(OutlineResources)); + } + + _outlineResources = value; + } + } + + ///. + /// Gets collection of outline layers. + /// + public OutlineLayerCollection OutlineLayers + { + get + { + return _outlineLayers; + } + set + { + _outlineLayers = value; + } + } + + ///+ /// Gets outline layers (for internal use only). + /// + internal OutlineLayerCollection OutlineLayersInternal => _outlineLayers; + + ///+ /// Gets or sets + public CameraEvent RenderEvent + { + get + { + return _cameraEvent; + } + set + { + if (_cameraEvent != value) + { + if (_commandBuffer != null) + { + var camera = GetComponentused to render the outlines. + /// (); + + if (camera) + { + camera.RemoveCommandBuffer(_cameraEvent, _commandBuffer); + camera.AddCommandBuffer(value, _commandBuffer); + } + } + + _cameraEvent = value; + } + } + } + + /// + /// Adds the + /// Thepassed to the first outline layer. Creates the layer if needed. + /// to add and render outline for. + /// + public void AddGameObject(GameObject go) + { + AddGameObject(go, 0); + } + + /// + /// Adds the + /// Thepassed to the specified outline layer. Creates the layer if needed. + /// to add and render outline for. + /// + public void AddGameObject(GameObject go, int layerIndex) + { + if (layerIndex < 0) + { + throw new ArgumentOutOfRangeException("layerIndex"); + } + + CreateLayersIfNeeded(); + + while (_outlineLayers.Count <= layerIndex) + { + _outlineLayers.Add(new OutlineLayer()); + } + + _outlineLayers[layerIndex].Add(go); + } + + /// + /// Removes the specified + /// Afrom . + /// to remove. + public void RemoveGameObject(GameObject go) + { + if (_outlineLayers) + { + _outlineLayers.Remove(go); + } + } + + #endregion + + #region MonoBehaviour + + private void Awake() + { + OutlineResources.LogSrpNotSupported(this); + OutlineResources.LogPpNotSupported(this); + } + + private void OnEnable() + { + InitCameraAndCommandBuffer(); + } + + private void OnDisable() + { + ReleaseCameraAndCommandBuffer(); + } + + private void OnPreRender() + { + FillCommandBuffer(); + } + + private void OnDestroy() + { + // TODO: Find a way to do this once per OutlineLayerCollection instance. + if (_outlineLayers) + { + _outlineLayers.Reset(); + } + } + +#if UNITY_EDITOR + + //private void OnValidate() + //{ + // InitCameraAndCommandBuffer(); + // FillCommandBuffer(); + //} + + private void Reset() + { + _outlineLayers = null; + } + +#endif + + #endregion + + #region implementation + + private void InitCameraAndCommandBuffer() + { + _camera = GetComponent (); + + if (_camera && _commandBuffer is null) + { + _commandBuffer = new CommandBuffer + { + name = string.Format("{0} - {1}", GetType().Name, name) + }; + + _camera.depthTextureMode |= DepthTextureMode.Depth; + _camera.AddCommandBuffer(_cameraEvent, _commandBuffer); + } + } + + private void ReleaseCameraAndCommandBuffer() + { + if (_commandBuffer != null) + { + if (_camera) + { + _camera.RemoveCommandBuffer(_cameraEvent, _commandBuffer); + } + + _commandBuffer.Dispose(); + _commandBuffer = null; + } + + _camera = null; + } + + private void FillCommandBuffer() + { + if (_camera && _outlineLayers && _commandBuffer != null) + { + _commandBuffer.Clear(); + + if (_outlineResources && _outlineResources.IsValid) + { + using (var renderer = new OutlineRenderer(_commandBuffer, _outlineResources, _camera.actualRenderingPath)) + { + _renderObjects.Clear(); + _outlineLayers.GetRenderObjects(_renderObjects); + renderer.Render(_renderObjects); + } + } + } + } + + private void CreateLayersIfNeeded() + { + if (_outlineLayers is null) + { + _outlineLayers = ScriptableObject.CreateInstance (); + _outlineLayers.name = "OutlineLayers"; + } + } + + #endregion + } +} diff --git a/Runtime/Scripts/OutlineEffect.cs.meta b/Runtime/Scripts/OutlineEffect.cs.meta new file mode 100644 index 0000000..1044087 --- /dev/null +++ b/Runtime/Scripts/OutlineEffect.cs.meta @@ -0,0 +1,15 @@ +fileFormatVersion: 2 +guid: 270d3185d159bf54fb4cddbb42235437 +timeCreated: 1566149591 +licenseType: Free +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: + - _outlineResources: {fileID: 11400000, guid: d28e70f030b1a634db9a6a6d5478ef19, + type: 2} + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/OutlineFilterMode.cs b/Runtime/Scripts/OutlineFilterMode.cs new file mode 100644 index 0000000..f1edfdc --- /dev/null +++ b/Runtime/Scripts/OutlineFilterMode.cs @@ -0,0 +1,15 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +using System; +using UnityEngine; + +namespace UnityFx.Outline +{ + internal enum OutlineFilterMode + { + None, + UseLayerMask, + UseRenderingLayerMask, + } +} diff --git a/Runtime/Scripts/OutlineFilterMode.cs.meta b/Runtime/Scripts/OutlineFilterMode.cs.meta new file mode 100644 index 0000000..cd45e8d --- /dev/null +++ b/Runtime/Scripts/OutlineFilterMode.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 82c9d42cc303be24d852b8db7c4b650f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/OutlineLayer.cs b/Runtime/Scripts/OutlineLayer.cs new file mode 100644 index 0000000..b558afd --- /dev/null +++ b/Runtime/Scripts/OutlineLayer.cs @@ -0,0 +1,509 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using UnityEngine; + +namespace UnityFx.Outline +{ + /// + /// A collection of + ///instances that share outline settings. An + /// can only belong to one at time. + /// + /// + [Serializable] + public sealed class OutlineLayer : ICollection , IReadOnlyCollection , IOutlineSettings + { + #region data + + [SerializeField, HideInInspector] + private OutlineSettingsInstance _settings = new OutlineSettingsInstance(); + [SerializeField, HideInInspector] + private string _name; + [SerializeField, HideInInspector] + private bool _enabled = true; + [SerializeField, HideInInspector] + private bool _mergeLayerObjects; + + private OutlineLayerCollection _parentCollection; + private Dictionary _outlineObjects = new Dictionary (); + private List _mergedRenderers; + + #endregion + + #region interface + + /// + /// Gets the layer name. + /// + public string Name + { + get + { + if (string.IsNullOrEmpty(_name)) + { + return "OutlineLayer #" + Index.ToString(); + } + + return _name; + } + } + + ///+ /// Gets or sets a value indicating whether the layer is enabled. + /// + ///+ public bool Enabled + { + get + { + return _enabled; + } + set + { + _enabled = value; + } + } + + /// + /// Gets or sets a value indicating whether layer game objects should be trated as one. + /// + public bool MergeLayerObjects + { + get + { + return _mergeLayerObjects; + } + set + { + _mergeLayerObjects = value; + } + } + + ///+ /// Gets index of the layer in parent collection. + /// + public int Index + { + get + { + if (_parentCollection != null) + { + return _parentCollection.IndexOf(this); + } + + return -1; + } + } + + ///+ /// Gets or sets outline settings. Set this to non- + public OutlineSettings OutlineSettings + { + get + { + return _settings.OutlineSettings; + } + set + { + _settings.OutlineSettings = value; + } + } + + ///value to share settings with other components. + /// + /// Initializes a new instance of the + public OutlineLayer() + { + } + + ///class. + /// + /// Initializes a new instance of the + internal OutlineLayer(OutlineLayerCollection parentCollection) + { + _parentCollection = parentCollection; + } + + ///class. + /// + /// Initializes a new instance of the + public OutlineLayer(string name) + { + _name = name; + } + + ///class. + /// + /// Initializes a new instance of the + ///class. + /// Thrown if + public OutlineLayer(OutlineSettings settings) + { + if (settings is null) + { + throw new ArgumentNullException(nameof(settings)); + } + + _settings.OutlineSettings = settings; + } + + ///is . + /// Initializes a new instance of the + ///class. + /// Thrown if + public OutlineLayer(string name, OutlineSettings settings) + { + if (settings is null) + { + throw new ArgumentNullException(nameof(settings)); + } + + _name = name; + _settings.OutlineSettings = settings; + } + + ///is . + /// Attempts to get renderers assosiated with the specified + ///. + /// Thrown if + public bool TryGetRenderers(GameObject go, out ICollectionis . renderers) + { + if (go is null) + { + throw new ArgumentNullException(nameof(go)); + } + + if (_outlineObjects.TryGetValue(go, out var result)) + { + renderers = result; + return true; + } + + renderers = null; + return false; + } + + /// + /// Gets the objects for rendering. + /// + public void GetRenderObjects(IListrenderObjects) + { + if (_enabled) + { + if (_mergeLayerObjects) + { + renderObjects.Add(new OutlineRenderObject(GetRenderers(), this, Name)); + } + else + { + foreach (var kvp in _outlineObjects) + { + var go = kvp.Key; + + if (go && go.activeInHierarchy) + { + renderObjects.Add(new OutlineRenderObject(kvp.Value.GetList(), _settings, go.name)); + } + } + } + } + } + + /// + /// Gets all layer renderers. + /// + public IReadOnlyListGetRenderers() + { + if (_enabled) + { + if (_mergedRenderers != null) + { + _mergedRenderers.Clear(); + } + else + { + _mergedRenderers = new List (); + } + + foreach (var kvp in _outlineObjects) + { + var go = kvp.Key; + + if (go && go.activeInHierarchy) + { + var rl = kvp.Value.GetList(); + + for (var i = 0; i < rl.Count; i++) + { + _mergedRenderers.Add(rl[i]); + } + } + } + + return _mergedRenderers; + } + + return Array.Empty (); + } + + #endregion + + #region internals + + internal string NameTag + { + get + { + return _name; + } + set + { + _name = value; + } + } + + internal OutlineLayerCollection ParentCollection => _parentCollection; + + internal void UpdateRenderers(int ignoreLayers) + { + foreach (var renderers in _outlineObjects.Values) + { + renderers.Reset(false, ignoreLayers); + } + } + + internal void Reset() + { + _outlineObjects.Clear(); + } + + internal void SetCollection(OutlineLayerCollection collection) + { + if (_parentCollection == null || collection == null || _parentCollection == collection) + { + _parentCollection = collection; + } + else + { + throw new InvalidOperationException("OutlineLayer can only belong to a single OutlineLayerCollection."); + } + } + + #endregion + + #region IOutlineSettings + + /// + public Color OutlineColor + { + get + { + return _settings.OutlineColor; + } + set + { + _settings.OutlineColor = value; + } + } + + /// + public int OutlineWidth + { + get + { + return _settings.OutlineWidth; + } + set + { + _settings.OutlineWidth = value; + } + } + + /// + public float OutlineIntensity + { + get + { + return _settings.OutlineIntensity; + } + set + { + _settings.OutlineIntensity = value; + } + } + + /// + public float OutlineAlphaCutoff + { + get + { + return _settings.OutlineAlphaCutoff; + } + set + { + _settings.OutlineAlphaCutoff = value; + } + } + + /// + public OutlineRenderFlags OutlineRenderMode + { + get + { + return _settings.OutlineRenderMode; + } + set + { + _settings.OutlineRenderMode = value; + } + } + + #endregion + + #region ICollection + + /// + public int Count => _outlineObjects.Count; + + /// + public bool IsReadOnly => false; + + /// + public void Add(GameObject go) + { + if (go is null) + { + throw new ArgumentNullException(nameof(go)); + } + + if (!_outlineObjects.ContainsKey(go)) + { + var renderers = new OutlineRendererCollection(go); + renderers.Reset(false, _parentCollection.IgnoreLayerMask); + _outlineObjects.Add(go, renderers); + } + } + + /// + public bool Remove(GameObject go) + { + if (go is null) + { + return false; + } + + return _outlineObjects.Remove(go); + } + + /// + public bool Contains(GameObject go) + { + if (go is null) + { + return false; + } + + return _outlineObjects.ContainsKey(go); + } + + /// + public void Clear() + { + _outlineObjects.Clear(); + } + + /// + public void CopyTo(GameObject[] array, int arrayIndex) + { + _outlineObjects.Keys.CopyTo(array, arrayIndex); + } + + #endregion + + #region IEnumerable + + /// + public IEnumerator GetEnumerator() + { + return _outlineObjects.Keys.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _outlineObjects.Keys.GetEnumerator(); + } + + #endregion + + #region IEquatable + + /// + public bool Equals(IOutlineSettings other) + { + return OutlineSettings.Equals(this, other); + } + + #endregion + + #region Object + + /// + public override string ToString() + { + var text = new StringBuilder(); + + if (string.IsNullOrEmpty(_name)) + { + text.Append("OutlineLayer"); + } + else + { + text.Append(_name); + } + + if (_parentCollection != null) + { + text.Append(" #"); + text.Append(_parentCollection.IndexOf(this)); + } + + if (_outlineObjects.Count > 0) + { + text.Append(" ("); + + foreach (var go in _outlineObjects.Keys) + { + text.Append(go.name); + text.Append(", "); + } + + text.Remove(text.Length - 2, 2); + text.Append(")"); + } + + return string.Format("{0}", text); + } + + /// + public override bool Equals(object other) + { + return OutlineSettings.Equals(this, other as IOutlineSettings); + } + + /// + public override int GetHashCode() + { + return base.GetHashCode(); + } + + #endregion + + #region implementation + #endregion + } +} diff --git a/Runtime/Scripts/OutlineLayer.cs.meta b/Runtime/Scripts/OutlineLayer.cs.meta new file mode 100644 index 0000000..f199256 --- /dev/null +++ b/Runtime/Scripts/OutlineLayer.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: 1360e19784ddfac45a7dcb6ba39595ed +timeCreated: 1566130871 +licenseType: Free +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/OutlineLayerCollection.cs b/Runtime/Scripts/OutlineLayerCollection.cs new file mode 100644 index 0000000..5b12e22 --- /dev/null +++ b/Runtime/Scripts/OutlineLayerCollection.cs @@ -0,0 +1,320 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace UnityFx.Outline +{ + /// + /// A serializable collection of outline layers. + /// + ///+ /// + /// + [CreateAssetMenu(fileName = "OutlineLayerCollection", menuName = "UnityFx/Outline/Outline Layer Collection")] + public sealed class OutlineLayerCollection : ScriptableObject, IList , IReadOnlyList + { + #region data + + [SerializeField, HideInInspector] + private List _layers = new List (); + + [SerializeField, HideInInspector] + private int _ignoreLayerMask; + + #endregion + + #region interface + + /// + /// Gets or sets layer mask to use for ignored + public int IgnoreLayerMask + { + get + { + return _ignoreLayerMask; + } + set + { + if (_ignoreLayerMask != value) + { + _ignoreLayerMask = value; + + foreach (var layer in _layers) + { + layer.UpdateRenderers(value); + } + } + } + } + + ///components in layer game objects. + /// + /// Gets number of game objects in the layers. + /// + public int NumberOfObjects + { + get + { + var result = 0; + + foreach (var layer in _layers) + { + result += layer.Count; + } + + return result; + } + } + + ///+ /// Gets a layer with the specified index. If layer at the + public OutlineLayer GetOrAddLayer(int index) + { + if (index < 0) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + while (index >= _layers.Count) + { + _layers.Add(new OutlineLayer(this)); + } + + return _layers[index]; + } + + ///does not exist, creates one. + /// + /// Adds a new layer. + /// + public OutlineLayer AddLayer() + { + var layer = new OutlineLayer(this); + _layers.Add(layer); + return layer; + } + + ///+ /// Gets the objects for rendering. + /// + public void GetRenderObjects(IListrenderObjects) + { + foreach (var layer in _layers) + { + layer.GetRenderObjects(renderObjects); + } + } + + /// + /// Removes the specified + /// Afrom layers. + /// to remove. + public void Remove(GameObject go) + { + foreach (var layer in _layers) + { + if (layer.Remove(go)) + { + break; + } + } + } + + /// + /// Removes all game objects registered in layers. + /// + public void ClearLayerContent() + { + foreach (var layer in _layers) + { + layer.Clear(); + } + } + + #endregion + + #region internals + + internal void Reset() + { + foreach (var layer in _layers) + { + layer.Reset(); + } + } + + #endregion + + #region ScriptableObject + + private void OnEnable() + { + foreach (var layer in _layers) + { + layer.Clear(); + layer.SetCollection(this); + } + } + + #endregion + + #region IList + + ///+ public OutlineLayer this[int layerIndex] + { + get + { + return _layers[layerIndex]; + } + set + { + if (value is null) + { + throw new ArgumentNullException("layer"); + } + + if (layerIndex < 0 || layerIndex >= _layers.Count) + { + throw new ArgumentOutOfRangeException(nameof(layerIndex)); + } + + if (_layers[layerIndex] != value) + { + value.SetCollection(this); + + _layers[layerIndex].SetCollection(null); + _layers[layerIndex] = value; + } + } + } + + /// + public int IndexOf(OutlineLayer layer) + { + if (layer != null) + { + return _layers.IndexOf(layer); + } + + return -1; + } + + /// + public void Insert(int index, OutlineLayer layer) + { + if (layer is null) + { + throw new ArgumentNullException(nameof(layer)); + } + + if (layer.ParentCollection != this) + { + layer.SetCollection(this); + _layers.Insert(index, layer); + } + } + + /// + public void RemoveAt(int index) + { + if (index >= 0 && index < _layers.Count) + { + _layers[index].SetCollection(null); + _layers.RemoveAt(index); + } + } + + #endregion + + #region ICollection + + /// + public int Count => _layers.Count; + + /// + public bool IsReadOnly => false; + + /// + public void Add(OutlineLayer layer) + { + if (layer is null) + { + throw new ArgumentNullException(nameof(layer)); + } + + if (layer.ParentCollection != this) + { + layer.SetCollection(this); + _layers.Add(layer); + } + } + + /// + public bool Remove(OutlineLayer layer) + { + if (_layers.Remove(layer)) + { + layer.SetCollection(null); + return true; + } + + return false; + } + + /// + public void Clear() + { + if (_layers.Count > 0) + { + foreach (var layer in _layers) + { + layer.SetCollection(null); + } + + _layers.Clear(); + } + } + + /// + public bool Contains(OutlineLayer layer) + { + if (layer is null) + { + return false; + } + + return _layers.Contains(layer); + } + + /// + public void CopyTo(OutlineLayer[] array, int arrayIndex) + { + _layers.CopyTo(array, arrayIndex); + } + + #endregion + + #region IEnumerable + + /// + public IEnumerator GetEnumerator() + { + return _layers.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _layers.GetEnumerator(); + } + + #endregion + + #region implementation + #endregion + } +} diff --git a/Runtime/Scripts/OutlineLayerCollection.cs.meta b/Runtime/Scripts/OutlineLayerCollection.cs.meta new file mode 100644 index 0000000..3b46761 --- /dev/null +++ b/Runtime/Scripts/OutlineLayerCollection.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: 57d0c11168277cf4eb3b4b89706e6aa5 +timeCreated: 1566560091 +licenseType: Pro +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/OutlineResources.cs b/Runtime/Scripts/OutlineResources.cs new file mode 100644 index 0000000..d930930 --- /dev/null +++ b/Runtime/Scripts/OutlineResources.cs @@ -0,0 +1,532 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using UnityEditor; +using UnityEngine; +using UnityEngine.Rendering; + +namespace UnityFx.Outline +{ + /// + /// This asset is used to store references to shaders and other resources needed at runtime without having to use a Resources folder. + /// + ///+ [CreateAssetMenu(fileName = "OutlineResources", menuName = "UnityFx/Outline/Outline Resources")] + public sealed class OutlineResources : ScriptableObject + { + #region data + + [SerializeField] + private Shader _renderShader; + [SerializeField] + private Shader _outlineShader; + + private Material _renderMaterial; + private Material _outlineMaterial; + private MaterialPropertyBlock _props; + private Mesh _fullscreenTriangleMesh; + private float[][] _gaussSamples; + private bool _useDrawMesh; + + #endregion + + #region interface + + /// + /// Minimum value of outline width parameter. + /// + ///+ public const int MinWidth = 1; + + /// + /// Maximum value of outline width parameter. + /// + ///+ /// If the value is changed here, it should be adjusted in Outline.shader as well. + /// + ///+ public const int MaxWidth = 32; + + /// + /// Minimum value of outline intensity parameter. + /// + ///+ /// + public const int MinIntensity = 1; + + /// + /// Maximum value of outline intensity parameter. + /// + ///+ /// + public const int MaxIntensity = 64; + + /// + /// Value of outline intensity parameter that is treated as solid fill. + /// + ///+ /// + public const int SolidIntensity = 100; + + /// + /// Minimum value of outline alpha cutoff parameter. + /// + ///+ public const float MinAlphaCutoff = 0; + + /// + /// Maximum value of outline alpha cutoff parameter. + /// + ///+ public const float MaxAlphaCutoff = 1; + + /// + /// Name of _MainTex shader parameter. + /// + public const string MainTexName = "_MainTex"; + + ///+ /// Name of _MaskTex shader parameter. + /// + public const string MaskTexName = "_MaskTex"; + + ///+ /// Name of _TempTex shader parameter. + /// + public const string TempTexName = "_TempTex"; + + ///+ /// Name of _Color shader parameter. + /// + public const string ColorName = "_Color"; + + ///+ /// Name of _Width shader parameter. + /// + public const string WidthName = "_Width"; + + ///+ /// Name of _Intensity shader parameter. + /// + public const string IntensityName = "_Intensity"; + + ///+ /// Name of _Cutoff shader parameter. + /// + public const string AlphaCutoffName = "_Cutoff"; + + ///+ /// Name of _GaussSamples shader parameter. + /// + public const string GaussSamplesName = "_GaussSamples"; + + ///+ /// Name of the _USE_DRAWMESH shader feature. + /// + public const string UseDrawMeshFeatureName = "_USE_DRAWMESH"; + + ///+ /// Name of the outline effect. + /// + public const string EffectName = "Outline"; + + ///+ /// Tooltip text for + public const string OutlineResourcesTooltip = "Outline resources to use (shaders, materials etc). Do not change defaults unless you know what you're doing."; + + ///field. + /// + /// Tooltip text for + public const string OutlineLayerCollectionTooltip = "Collection of outline layers to use. This can be used to share outline settings between multiple cameras."; + + ///field. + /// + /// Tooltip text for outline + public const string OutlineLayerMaskTooltip = "Layer mask for outined objects."; + + ///field. + /// + /// Tooltip text for outline + public const string OutlineRenderingLayerMaskTooltip = "Rendering layer mask for outined objects."; + + ///field. + /// + /// Index of the default pass in + public const int RenderShaderDefaultPassId = 0; + + ///. + /// + /// Index of the alpha-test pass in + public const int RenderShaderAlphaTestPassId = 1; + + ///. + /// + /// Index of the HPass in + public const int OutlineShaderHPassId = 0; + + ///. + /// + /// Index of the VPass in + public const int OutlineShaderVPassId = 1; + + ///. + /// + /// SRP not supported message. + /// + internal const string SrpNotSupported = "{0} works with built-in render pipeline only. It does not support SRP (including URP and HDRP)."; + + ///+ /// Post-processing not supported message. + /// + internal const string PpNotSupported = "{0} does not support Unity Post-processing stack v2. It might not work as expected."; + + ///+ /// Hashed name of _MainTex shader parameter. + /// + public readonly int MainTexId = Shader.PropertyToID(MainTexName); + + ///+ /// Texture identifier for _MainTex shader parameter. + /// + public readonly RenderTargetIdentifier MainTex = new RenderTargetIdentifier(MainTexName); + + ///+ /// Hashed name of _MaskTex shader parameter. + /// + public readonly int MaskTexId = Shader.PropertyToID(MaskTexName); + + ///+ /// Texture identifier for _MaskTex shader parameter. + /// + public readonly RenderTargetIdentifier MaskTex = new RenderTargetIdentifier(MaskTexName); + + ///+ /// Hashed name of _TempTex shader parameter. + /// + public readonly int TempTexId = Shader.PropertyToID(TempTexName); + + ///+ /// Texture identifier for _TempTex shader parameter. + /// + public readonly RenderTargetIdentifier TempTex = new RenderTargetIdentifier(TempTexName); + + ///+ /// Hashed name of _Color shader parameter. + /// + public readonly int ColorId = Shader.PropertyToID(ColorName); + + ///+ /// Hashed name of _Width shader parameter. + /// + public readonly int WidthId = Shader.PropertyToID(WidthName); + + ///+ /// Hashed name of _Intensity shader parameter. + /// + public readonly int IntensityId = Shader.PropertyToID(IntensityName); + + ///+ /// Hashed name of _Cutoff shader parameter. + /// + public readonly int AlphaCutoffId = Shader.PropertyToID(AlphaCutoffName); + + ///+ /// Hashed name of _GaussSamples shader parameter. + /// + public readonly int GaussSamplesId = Shader.PropertyToID(GaussSamplesName); + + ///+ /// Temp materials list. Used by + internal readonly Listto avoid GC allocations. + /// TmpMaterials = new List (); + + /// + /// Gets a + public Shader RenderShader + { + get + { + return _renderShader; + } + } + + ///that renders objects outlined with a solid while color. + /// + /// Gets a + public Shader OutlineShader + { + get + { + return _outlineShader; + } + } + + ///that renders outline around the mask, that was generated with . + /// + /// Gets a + public Material RenderMaterial + { + get + { + if (_renderMaterial == null) + { + UnityEngine.Debug.Assert(_renderShader != null, "No RenderShader is set in outline resources.", this); + + _renderMaterial = new Material(_renderShader) + { + name = "Outline - RenderColor", + hideFlags = HideFlags.HideAndDontSave + }; + } + + return _renderMaterial; + } + } + + ///-based material. + /// + /// Gets a + public Material OutlineMaterial + { + get + { + if (_outlineMaterial == null) + { + UnityEngine.Debug.Assert(_outlineShader != null, "No OutlineShader is set in outline resources.", this); + + _outlineMaterial = new Material(_outlineShader) + { + name = "Outline - Main", + hideFlags = HideFlags.HideAndDontSave + }; + + if (_useDrawMesh) + { + _outlineMaterial.EnableKeyword(UseDrawMeshFeatureName); + } + } + + return _outlineMaterial; + } + } + + ///-based material. + /// + /// Gets a + public MaterialPropertyBlock Properties + { + get + { + if (_props is null) + { + _props = new MaterialPropertyBlock(); + } + + return _props; + } + } + + ///for . + /// + /// Gets or sets a fullscreen triangle mesh. The mesh is lazy-initialized on the first access. + /// + ///+ /// This is used by + ///to avoid Blit() calls and use DrawMesh() passing + /// this mesh as the first argument. When running on a device with Shader Model 3.5 support this + /// should not be used at all, as the vertices are generated in vertex shader with DrawProcedural() call. + /// + public Mesh FullscreenTriangleMesh + { + get + { + if (_fullscreenTriangleMesh == null) + { + _fullscreenTriangleMesh = new Mesh() + { + name = "Outline - FullscreenTriangle", + hideFlags = HideFlags.HideAndDontSave, + vertices = new Vector3[] { new Vector3(-1, -1, 0), new Vector3(3, -1, 0), new Vector3(-1, 3, 0) }, + triangles = new int[] { 0, 1, 2 } + }; + + _fullscreenTriangleMesh.UploadMeshData(true); + } + + return _fullscreenTriangleMesh; + } + set + { + _fullscreenTriangleMesh = value; + } + } + + /// + /// Gets or sets a value indicating whether + public bool UseFullscreenTriangleMesh + { + get + { + return _useDrawMesh; + } + set + { + if (_useDrawMesh != value) + { + _useDrawMesh = value; + + if (_outlineMaterial) + { + if (_useDrawMesh) + { + _outlineMaterial.EnableKeyword(UseDrawMeshFeatureName); + } + else + { + _outlineMaterial.DisableKeyword(UseDrawMeshFeatureName); + } + } + } + } + } + + ///is used for image effects rendering even when procedural rendering is available. + /// + /// Gets a value indicating whether the instance is in valid state. + /// + public bool IsValid => RenderShader && OutlineShader; + + ///+ /// Returns a + public MaterialPropertyBlock GetProperties(IOutlineSettings settings) + { + if (_props is null) + { + _props = new MaterialPropertyBlock(); + } + + _props.SetFloat(WidthId, settings.OutlineWidth); + _props.SetColor(ColorId, settings.OutlineColor); + + if ((settings.OutlineRenderMode & OutlineRenderFlags.Blurred) != 0) + { + _props.SetFloat(IntensityId, settings.OutlineIntensity); + } + else + { + _props.SetFloat(IntensityId, SolidIntensity); + } + + return _props; + } + + ///instance initialized with values from . + /// + /// Gets cached gauss samples for the specified outline + public float[] GetGaussSamples(int width) + { + var index = Mathf.Clamp(width, 1, MaxWidth) - 1; + + if (_gaussSamples is null) + { + _gaussSamples = new float[MaxWidth][]; + } + + if (_gaussSamples[index] is null) + { + _gaussSamples[index] = GetGaussSamples(width, null); + } + + return _gaussSamples[index]; + } + + ///. + /// + /// Resets the resources to defaults. + /// + public void ResetToDefaults() + { + _renderShader = Shader.Find("Hidden/UnityFx/OutlineColor"); + _outlineShader = Shader.Find("Hidden/UnityFx/Outline"); + } + + ///+ /// Calculates value of Gauss function for the specified + ///and values. + /// + /// + public static float Gauss(float x, float stdDev) + { + var stdDev2 = stdDev * stdDev * 2; + var a = 1 / Mathf.Sqrt(Mathf.PI * stdDev2); + var gauss = a * Mathf.Pow((float)Math.E, -x * x / stdDev2); + + return gauss; + } + + /// + /// Samples Gauss function for the specified + ///. + /// + public static float[] GetGaussSamples(int width, float[] samples) + { + // NOTE: According to '3 sigma' rule there is no reason to have StdDev less then width / 3. + // In practice blur looks best when StdDev is within range [width / 3, width / 2]. + var stdDev = width * 0.5f; + + if (samples is null) + { + samples = new float[MaxWidth]; + } + + for (var i = 0; i < width; i++) + { + samples[i] = Gauss(i, stdDev); + } + + return samples; + } + + /// + /// Writes a console warning if SRP is detected. + /// + public static void LogSrpNotSupported(UnityEngine.Object obj) + { + if (GraphicsSettings.renderPipelineAsset) + { + UnityEngine.Debug.LogWarningFormat(obj, SrpNotSupported, obj.GetType().Name); + } + } + + ///+ /// Writes a console warning if Post Processing Stack v2 is detected. + /// + [Conditional("UNITY_POST_PROCESSING_STACK_V2")] + public static void LogPpNotSupported(UnityEngine.Object obj) + { + UnityEngine.Debug.LogWarningFormat(obj, PpNotSupported, obj.GetType().Name); + } + + #endregion + + #region ScriptableObject + + private void OnValidate() + { + if (_renderMaterial) + { + _renderMaterial.shader = _renderShader; + } + + if (_outlineMaterial) + { + _outlineMaterial.shader = _outlineShader; + } + } + + #endregion + } +} diff --git a/Runtime/Scripts/OutlineResources.cs.meta b/Runtime/Scripts/OutlineResources.cs.meta new file mode 100644 index 0000000..ce5153a --- /dev/null +++ b/Runtime/Scripts/OutlineResources.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: b503341e0a514e3489c4851727e68257 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: + - _renderShader: {fileID: 4800000, guid: ac20fbf75bafe454aba5ef3c098349df, type: 3} + - _outlineShader: {fileID: 4800000, guid: 41c9acbf41c8245498ac9beab378de12, type: 3} + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/OutlineSettings.cs b/Runtime/Scripts/OutlineSettings.cs new file mode 100644 index 0000000..281c773 --- /dev/null +++ b/Runtime/Scripts/OutlineSettings.cs @@ -0,0 +1,144 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +using System; +using UnityEngine; + +namespace UnityFx.Outline +{ + ///+ /// Outline settings. + /// + [CreateAssetMenu(fileName = "OutlineSettings", menuName = "UnityFx/Outline/Outline Settings")] + public sealed class OutlineSettings : ScriptableObject, IOutlineSettings + { + #region data + + // NOTE: There is a custom editor for OutlineSettings, so no need to show these in default inspector. + [SerializeField, HideInInspector] + private Color _outlineColor = Color.red; + [SerializeField, HideInInspector, Range(OutlineResources.MinWidth, OutlineResources.MaxWidth)] + private int _outlineWidth = 4; + [SerializeField, HideInInspector, Range(OutlineResources.MinIntensity, OutlineResources.MaxIntensity)] + private float _outlineIntensity = 2; + [SerializeField, HideInInspector, Range(OutlineResources.MinAlphaCutoff, OutlineResources.MaxAlphaCutoff)] + private float _outlineAlphaCutoff = 0.9f; + [SerializeField, HideInInspector] + private OutlineRenderFlags _outlineMode; + + #endregion + + #region interface + + public static bool Equals(IOutlineSettings lhs, IOutlineSettings rhs) + { + if (lhs == null || rhs == null) + { + return false; + } + + return lhs.OutlineColor == rhs.OutlineColor && + lhs.OutlineWidth == rhs.OutlineWidth && + lhs.OutlineRenderMode == rhs.OutlineRenderMode && + Mathf.Approximately(lhs.OutlineIntensity, rhs.OutlineIntensity) && + Mathf.Approximately(lhs.OutlineAlphaCutoff, rhs.OutlineAlphaCutoff); + } + + #endregion + + #region IOutlineSettings + + ///+ public Color OutlineColor + { + get + { + return _outlineColor; + } + set + { + _outlineColor = value; + } + } + + /// + public int OutlineWidth + { + get + { + return _outlineWidth; + } + set + { + _outlineWidth = Mathf.Clamp(value, OutlineResources.MinWidth, OutlineResources.MaxWidth); + } + } + + /// + public float OutlineIntensity + { + get + { + return _outlineIntensity; + } + set + { + _outlineIntensity = Mathf.Clamp(value, OutlineResources.MinIntensity, OutlineResources.MaxIntensity); + } + } + + /// + public float OutlineAlphaCutoff + { + get + { + return _outlineAlphaCutoff; + } + set + { + _outlineAlphaCutoff = Mathf.Clamp(value, 0, 1); + } + } + + /// + public OutlineRenderFlags OutlineRenderMode + { + get + { + return _outlineMode; + } + set + { + _outlineMode = value; + } + } + + #endregion + + #region IEquatable + + /// + public bool Equals(IOutlineSettings other) + { + return Equals(this, other); + } + + #endregion + + #region Object + + /// + public override bool Equals(object other) + { + return Equals(this, other as IOutlineSettings); + } + + /// + public override int GetHashCode() + { + return base.GetHashCode(); + } + + #endregion + } +} diff --git a/Runtime/Scripts/OutlineSettings.cs.meta b/Runtime/Scripts/OutlineSettings.cs.meta new file mode 100644 index 0000000..c6de014 --- /dev/null +++ b/Runtime/Scripts/OutlineSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b579424fd3338724cba3155ee4d53475 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/OutlineSettingsExtensions.cs b/Runtime/Scripts/OutlineSettingsExtensions.cs new file mode 100644 index 0000000..584658f --- /dev/null +++ b/Runtime/Scripts/OutlineSettingsExtensions.cs @@ -0,0 +1,50 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +using System; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace UnityFx.Outline +{ + /// + /// Extension methods for + [EditorBrowsable(EditorBrowsableState.Never)] + public static class OutlineSettingsExtensions + { + ///. + /// + /// Gets a value indicating whether outline should use alpha testing. + /// + ///+ /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAlphaTestingEnabled(this IOutlineSettings settings) + { + return (settings.OutlineRenderMode & OutlineRenderFlags.EnableAlphaTesting) != 0; + } + + /// + /// Gets a value indicating whether outline should use depth testing. + /// + ///+ /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsDepthTestingEnabled(this IOutlineSettings settings) + { + return (settings.OutlineRenderMode & OutlineRenderFlags.EnableDepthTesting) != 0; + } + + /// + /// Gets a value indicating whether outline frame should be blurred. + /// + ///+ /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsBlurEnabled(this IOutlineSettings settings) + { + return (settings.OutlineRenderMode & OutlineRenderFlags.Blurred) != 0; + } + } +} diff --git a/Runtime/Scripts/OutlineSettingsExtensions.cs.meta b/Runtime/Scripts/OutlineSettingsExtensions.cs.meta new file mode 100644 index 0000000..961ea48 --- /dev/null +++ b/Runtime/Scripts/OutlineSettingsExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1faa9de2a3d5a374b84983eae45fc559 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/OutlineSettingsInstance.cs b/Runtime/Scripts/OutlineSettingsInstance.cs new file mode 100644 index 0000000..40168a7 --- /dev/null +++ b/Runtime/Scripts/OutlineSettingsInstance.cs @@ -0,0 +1,139 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +using System; +using UnityEngine; + +namespace UnityFx.Outline +{ + [Serializable] + internal class OutlineSettingsInstance : IOutlineSettings + { + #region data + +#pragma warning disable 0649 + + // NOTE: There are custom editors for public components, so no need to show these in default inspector. + [SerializeField, HideInInspector] + private OutlineSettings _outlineSettings; + [SerializeField, HideInInspector] + private Color _outlineColor = Color.red; + [SerializeField, HideInInspector, Range(OutlineResources.MinWidth, OutlineResources.MaxWidth)] + private int _outlineWidth = 4; + [SerializeField, HideInInspector, Range(OutlineResources.MinIntensity, OutlineResources.MaxIntensity)] + private float _outlineIntensity = 2; + [SerializeField, HideInInspector, Range(OutlineResources.MinAlphaCutoff, OutlineResources.MaxAlphaCutoff)] + private float _outlineAlphaCutoff = 0.9f; + [SerializeField, HideInInspector] + private OutlineRenderFlags _outlineMode; + +#pragma warning restore 0649 + + #endregion + + #region interface + + public bool RequiresCameraDepth + { + get + { + return (OutlineRenderMode & OutlineRenderFlags.EnableDepthTesting) != 0; + } + } + + public OutlineSettings OutlineSettings + { + get + { + return _outlineSettings; + } + set + { + _outlineSettings = value; + } + } + + #endregion + + #region IOutlineSettings + + /// + public Color OutlineColor + { + get + { + return _outlineSettings is null ? _outlineColor : _outlineSettings.OutlineColor; + } + set + { + _outlineColor = value; + } + } + + /// + public int OutlineWidth + { + get + { + return _outlineSettings is null ? _outlineWidth : _outlineSettings.OutlineWidth; + } + set + { + _outlineWidth = Mathf.Clamp(value, OutlineResources.MinWidth, OutlineResources.MaxWidth); + } + } + + /// + public float OutlineIntensity + { + get + { + return _outlineSettings is null ? _outlineIntensity : _outlineSettings.OutlineIntensity; + } + set + { + _outlineIntensity = Mathf.Clamp(value, OutlineResources.MinIntensity, OutlineResources.MaxIntensity); + } + } + + /// + public float OutlineAlphaCutoff + { + get + { + return _outlineSettings is null ? _outlineAlphaCutoff : _outlineSettings.OutlineAlphaCutoff; + } + set + { + _outlineAlphaCutoff = Mathf.Clamp(value, 0, 1); + } + } + + /// + public OutlineRenderFlags OutlineRenderMode + { + get + { + return _outlineSettings is null ? _outlineMode : _outlineSettings.OutlineRenderMode; + } + set + { + _outlineMode = value; + } + } + + #endregion + + #region IEquatable + + public bool Equals(IOutlineSettings other) + { + return OutlineSettings.Equals(this, other); + } + + #endregion + + #region implementation + #endregion + } +} diff --git a/Runtime/Scripts/OutlineSettingsInstance.cs.meta b/Runtime/Scripts/OutlineSettingsInstance.cs.meta new file mode 100644 index 0000000..177c3cb --- /dev/null +++ b/Runtime/Scripts/OutlineSettingsInstance.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8ea4c60e473b8ef4790934bb274993cc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/OutlineSettingsWithLayerMask.cs b/Runtime/Scripts/OutlineSettingsWithLayerMask.cs new file mode 100644 index 0000000..17ab348 --- /dev/null +++ b/Runtime/Scripts/OutlineSettingsWithLayerMask.cs @@ -0,0 +1,71 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +using System; +using UnityEngine; + +namespace UnityFx.Outline +{ + [Serializable] + internal class OutlineSettingsWithLayerMask : OutlineSettingsInstance + { + #region data + +#pragma warning disable 0649 + + // NOTE: There are custom editors for public components, so no need to show these in default inspector. + [SerializeField, HideInInspector] + private OutlineFilterMode _filterMode; + [SerializeField, HideInInspector] + private LayerMask _layerMask; + [SerializeField, HideInInspector] + private uint _renderingLayerMask = 1; + +#pragma warning restore 0649 + + #endregion + + #region interface + + public int OutlineLayerMask + { + get + { + if (_filterMode == OutlineFilterMode.UseLayerMask) + { + return _layerMask; + } + + if (_filterMode == OutlineFilterMode.UseRenderingLayerMask) + { + return -1; + } + + return 0; + } + } + + public uint OutlineRenderingLayerMask + { + get + { + if (_filterMode == OutlineFilterMode.UseLayerMask) + { + return uint.MaxValue; + } + + if (_filterMode == OutlineFilterMode.UseRenderingLayerMask) + { + return _renderingLayerMask; + } + + return 0; + } + } + + #endregion + + #region implementation + #endregion + } +} diff --git a/Runtime/Scripts/OutlineSettingsWithLayerMask.cs.meta b/Runtime/Scripts/OutlineSettingsWithLayerMask.cs.meta new file mode 100644 index 0000000..86fb5b9 --- /dev/null +++ b/Runtime/Scripts/OutlineSettingsWithLayerMask.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9f6645ede9c6d2346b6aee185f8261d8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Properties.meta b/Runtime/Scripts/Properties.meta new file mode 100644 index 0000000..708e1e7 --- /dev/null +++ b/Runtime/Scripts/Properties.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7bd10545b6de6654b864faecdec920cd +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Properties/AssemblyInfo.cs b/Runtime/Scripts/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..903d3da --- /dev/null +++ b/Runtime/Scripts/Properties/AssemblyInfo.cs @@ -0,0 +1,30 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("UnityFx.Outline")] +[assembly: AssemblyProduct("UnityFx.Outline")] +[assembly: AssemblyDescription("Screen-space outlines for Unity3d.")] +#if DEBUG +[assembly: AssemblyConfiguration("Debug")] +#else +[assembly: AssemblyConfiguration("Release")] +#endif +[assembly: AssemblyCompany("")] +[assembly: AssemblyCopyright("Copyright © Alexander Bogarsukov 2019-2020")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// Make internals visible to the editor assembly. +[assembly: InternalsVisibleTo("UnityFx.Outline.Editor")] +[assembly: InternalsVisibleTo("UnityFx.Outline.URP")] diff --git a/Runtime/Scripts/Properties/AssemblyInfo.cs.meta b/Runtime/Scripts/Properties/AssemblyInfo.cs.meta new file mode 100644 index 0000000..dd9f2bd --- /dev/null +++ b/Runtime/Scripts/Properties/AssemblyInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1613c034178676349be3282789167284 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Properties/Startup.cs b/Runtime/Scripts/Properties/Startup.cs new file mode 100644 index 0000000..d504f29 --- /dev/null +++ b/Runtime/Scripts/Properties/Startup.cs @@ -0,0 +1,10 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +#if !UNITY_2018_4_OR_NEWER +#error UnityFx.Outline requires Unity 2018.4 or newer. +#endif + +#if NET_LEGACY || NET_2_0 || NET_2_0_SUBSET +#error UnityFx.Outline does not support .NET 3.5. Please set Scripting Runtime Version to .NET 4.x Equivalent in Unity Player Settings. +#endif diff --git a/Runtime/Scripts/Properties/Startup.cs.meta b/Runtime/Scripts/Properties/Startup.cs.meta new file mode 100644 index 0000000..b4865cd --- /dev/null +++ b/Runtime/Scripts/Properties/Startup.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 955bb53eefa37054cb49969575341469 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Rendering.meta b/Runtime/Scripts/Rendering.meta new file mode 100644 index 0000000..aacee54 --- /dev/null +++ b/Runtime/Scripts/Rendering.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 263f9a02e31427d4d9d910267274bfa0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Rendering/OutlineRenderFlags.cs b/Runtime/Scripts/Rendering/OutlineRenderFlags.cs new file mode 100644 index 0000000..0537f8c --- /dev/null +++ b/Runtime/Scripts/Rendering/OutlineRenderFlags.cs @@ -0,0 +1,34 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +using System; + +namespace UnityFx.Outline +{ + /// + /// Enumerates outline render modes. + /// + [Flags] + public enum OutlineRenderFlags + { + ///+ /// Outline frame is a solid line. + /// + None = 0, + + ///+ /// Outline frame is blurred. + /// + Blurred = 1, + + ///+ /// Enables depth testing when rendering object outlines. Only visible parts of objects are outlined. + /// + EnableDepthTesting = 2, + + ///+ /// Enabled alpha testing when rendering outlines. + /// + EnableAlphaTesting = 4 + } +} diff --git a/Runtime/Scripts/Rendering/OutlineRenderFlags.cs.meta b/Runtime/Scripts/Rendering/OutlineRenderFlags.cs.meta new file mode 100644 index 0000000..8c8d06c --- /dev/null +++ b/Runtime/Scripts/Rendering/OutlineRenderFlags.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 836bd13bd33c59246b1cebab92f8e62a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Rendering/OutlineRenderObject.cs b/Runtime/Scripts/Rendering/OutlineRenderObject.cs new file mode 100644 index 0000000..02f4f8a --- /dev/null +++ b/Runtime/Scripts/Rendering/OutlineRenderObject.cs @@ -0,0 +1,64 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using UnityEngine; + +namespace UnityFx.Outline +{ + ///+ /// A single outline object + its outline settings. + /// + public readonly struct OutlineRenderObject : IEquatable+ { + #region data + + private readonly string _tag; + private readonly IReadOnlyList _renderers; + private readonly IOutlineSettings _outlineSettings; + + #endregion + + #region interface + + /// + /// Gets the object tag name. + /// + public string Tag => _tag; + + ///+ /// Gets renderers for the object. + /// + public IReadOnlyListRenderers => _renderers; + + /// + /// Gets outline settings for this object. + /// + public IOutlineSettings OutlineSettings => _outlineSettings; + + ///+ /// Initializes a new instance of the + public OutlineRenderObject(IReadOnlyListstruct. + /// renderers, IOutlineSettings outlineSettings, string tag = null) + { + _renderers = renderers; + _outlineSettings = outlineSettings; + _tag = tag; + } + + #endregion + + #region IEquatable + + /// + public bool Equals(OutlineRenderObject other) + { + return string.CompareOrdinal(_tag, other._tag) == 0 && _renderers == other._renderers && _outlineSettings == other._outlineSettings; + } + + #endregion + } +} diff --git a/Runtime/Scripts/Rendering/OutlineRenderObject.cs.meta b/Runtime/Scripts/Rendering/OutlineRenderObject.cs.meta new file mode 100644 index 0000000..024cad2 --- /dev/null +++ b/Runtime/Scripts/Rendering/OutlineRenderObject.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b9fa0d37014ee9049afd5e65be9f288b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Rendering/OutlineRenderer.cs b/Runtime/Scripts/Rendering/OutlineRenderer.cs new file mode 100644 index 0000000..3a351ed --- /dev/null +++ b/Runtime/Scripts/Rendering/OutlineRenderer.cs @@ -0,0 +1,476 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using UnityEngine; +using UnityEngine.Rendering; +using UnityEngine.XR; + +namespace UnityFx.Outline +{ + /// + /// Helper class for outline rendering with + ///. + /// + /// + ///The class can be used on its own or as part of a higher level systems. It is used + /// by higher level outline implementations ( + ///and + /// ). It is fully compatible with Unity post processing stack as well. The class implements + ///to be used inside + /// block as shown in the code samples. Disposing does not dispose + /// the corresponding . Command buffer is not cleared before rendering. It is user responsibility to do so if needed. + ///+ /// var commandBuffer = new CommandBuffer(); + /// + /// using (var renderer = new OutlineRenderer(commandBuffer, resources)) + /// { + /// renderer.Render(renderers, settings); + /// } + /// + /// camera.AddCommandBuffer(CameraEvent.BeforeImageEffects, commandBuffer); + /// + ///+ public readonly struct OutlineRenderer : IDisposable + { + #region data + + private readonly TextureDimension _rtDimention; + private readonly RenderTargetIdentifier _rt; + private readonly RenderTargetIdentifier _depth; + private readonly CommandBuffer _commandBuffer; + private readonly OutlineResources _resources; + + #endregion + + #region interface + + /// + /// A default + public const CameraEvent RenderEvent = CameraEvent.AfterSkybox; + + ///outline rendering should be assosiated with. + /// + /// A default render texture format for the outline effect. + /// + public const RenderTextureFormat RtFormat = RenderTextureFormat.R8; + + ///+ /// Initializes a new instance of the + /// Astruct. + /// to render the effect to. It should be cleared manually (if needed) before passing to this method. + /// Outline resources. + /// Thrown if + public OutlineRenderer(CommandBuffer cmd, OutlineResources resources) + : this(cmd, resources, BuiltinRenderTextureType.CameraTarget, BuiltinRenderTextureType.Depth, Vector2Int.zero) + { + } + + ///is . + /// Initializes a new instance of the + /// Astruct. + /// to render the effect to. It should be cleared manually (if needed) before passing to this method. + /// Outline resources. + /// The rendering path of target camera ( ). + /// Thrown if + public OutlineRenderer(CommandBuffer cmd, OutlineResources resources, RenderingPath renderingPath) + : this(cmd, resources, BuiltinRenderTextureType.CameraTarget, GetBuiltinDepth(renderingPath), Vector2Int.zero) + { + } + + ///is . + /// Initializes a new instance of the + /// Astruct. + /// to render the effect to. It should be cleared manually (if needed) before passing to this method. + /// Outline resources. + /// Render target. + /// Thrown if + public OutlineRenderer(CommandBuffer cmd, OutlineResources resources, RenderTargetIdentifier dst) + : this(cmd, resources, dst, BuiltinRenderTextureType.Depth, Vector2Int.zero) + { + } + + ///is . + /// Initializes a new instance of the + /// Astruct. + /// to render the effect to. It should be cleared manually (if needed) before passing to this method. + /// Render target. + /// The rendering path of target camera ( ). + /// Thrown if + public OutlineRenderer(CommandBuffer cmd, OutlineResources resources, RenderTargetIdentifier dst, RenderingPath renderingPath, Vector2Int rtSize) + : this(cmd, resources, dst, GetBuiltinDepth(renderingPath), rtSize) + { + } + + ///is . + /// Initializes a new instance of the + /// Astruct. + /// to render the effect to. It should be cleared manually (if needed) before passing to this method. + /// Outline resources. + /// Render target. + /// Depth dexture to use. + /// Thrown if + public OutlineRenderer(CommandBuffer cmd, OutlineResources resources, RenderTargetIdentifier dst, RenderTargetIdentifier depth, Vector2Int rtSize) + { + if (cmd is null) + { + throw new ArgumentNullException(nameof(cmd)); + } + + if (resources is null) + { + throw new ArgumentNullException(nameof(resources)); + } + + if (rtSize.x <= 0) + { + rtSize.x = -1; + } + + if (rtSize.y <= 0) + { + rtSize.y = -1; + } + + if (XRSettings.enabled) + { + var rtDesc = XRSettings.eyeTextureDesc; + + rtDesc.shadowSamplingMode = ShadowSamplingMode.None; + rtDesc.depthBufferBits = 0; + rtDesc.colorFormat = RtFormat; + + cmd.GetTemporaryRT(resources.MaskTexId, rtDesc, FilterMode.Bilinear); + cmd.GetTemporaryRT(resources.TempTexId, rtDesc, FilterMode.Bilinear); + + _rtDimention = rtDesc.dimension; + } + else + { + cmd.GetTemporaryRT(resources.MaskTexId, rtSize.x, rtSize.y, 0, FilterMode.Bilinear, RtFormat); + cmd.GetTemporaryRT(resources.TempTexId, rtSize.x, rtSize.y, 0, FilterMode.Bilinear, RtFormat); + + _rtDimention = TextureDimension.Tex2D; + } + + _rt = dst; + _depth = depth; + _commandBuffer = cmd; + _resources = resources; + } + + ///is . + /// Initializes a new instance of the + /// Astruct. + /// to render the effect to. It should be cleared manually (if needed) before passing to this method. + /// Outline resources. + /// Render target. + /// Depth dexture to use. + /// Render texture decsriptor. + /// Thrown if + public OutlineRenderer(CommandBuffer cmd, OutlineResources resources, RenderTargetIdentifier dst, RenderTargetIdentifier depth, RenderTextureDescriptor rtDesc) + { + if (cmd is null) + { + throw new ArgumentNullException(nameof(cmd)); + } + + if (resources is null) + { + throw new ArgumentNullException(nameof(resources)); + } + + if (rtDesc.width <= 0) + { + rtDesc.width = -1; + } + + if (rtDesc.height <= 0) + { + rtDesc.height = -1; + } + + if (rtDesc.dimension == TextureDimension.None || rtDesc.dimension == TextureDimension.Unknown) + { + rtDesc.dimension = TextureDimension.Tex2D; + } + + rtDesc.shadowSamplingMode = ShadowSamplingMode.None; + rtDesc.depthBufferBits = 0; + rtDesc.colorFormat = RtFormat; + rtDesc.msaaSamples = 1; + + cmd.GetTemporaryRT(resources.MaskTexId, rtDesc, FilterMode.Bilinear); + cmd.GetTemporaryRT(resources.TempTexId, rtDesc, FilterMode.Bilinear); + + _rtDimention = rtDesc.dimension; + _rt = dst; + _depth = depth; + _commandBuffer = cmd; + _resources = resources; + } + + ///is . + /// Renders outline around a single object. + /// + /// An object to be outlined. + ///+ public void Render(OutlineRenderObject obj) + { + Render(obj.Renderers, obj.OutlineSettings, obj.Tag); + } + + /// + /// Renders outline around multiple + /// An object to be outlined. + ///. + /// Thrown if + ///is . + public void Render(IReadOnlyList objects) + { + if (objects is null) + { + throw new ArgumentNullException(nameof(objects)); + } + + for (var i = 0; i < objects.Count; i++) + { + Render(objects[i]); + } + } + + /// + /// Renders outline around multiple + /// One or more renderers representing a single object to be outlined. + /// Outline settings. + /// Optional name of the sample (visible in profiler). + ///. + /// Thrown if any of the arguments is + ///. + public void Render(IReadOnlyList renderers, IOutlineSettings settings, string sampleName = null) + { + if (renderers is null) + { + throw new ArgumentNullException(nameof(renderers)); + } + + if (settings is null) + { + throw new ArgumentNullException(nameof(settings)); + } + + if (renderers.Count > 0) + { + // NOTE: Remove BeginSample/EndSample for now (https://github.com/Arvtesh/UnityFx.Outline/issues/44). + //if (string.IsNullOrEmpty(sampleName)) + //{ + // sampleName = renderers[0].name; + //} + + //_commandBuffer.BeginSample(sampleName); + { + RenderObjectClear(settings.OutlineRenderMode); + + for (var i = 0; i < renderers.Count; ++i) + { + DrawRenderer(renderers[i], settings); + } + + RenderOutline(settings); + } + //_commandBuffer.EndSample(sampleName); + } + } + + /// + /// Renders outline around a single + /// A. + /// representing an object to be outlined. + /// Outline settings. + /// Optional name of the sample (visible in profiler). + /// Thrown if any of the arguments is + ///. + public void Render(Renderer renderer, IOutlineSettings settings, string sampleName = null) + { + if (renderer is null) + { + throw new ArgumentNullException(nameof(renderer)); + } + + if (settings is null) + { + throw new ArgumentNullException(nameof(settings)); + } + + // NOTE: Remove BeginSample/EndSample for now (https://github.com/Arvtesh/UnityFx.Outline/issues/44). + //if (string.IsNullOrEmpty(sampleName)) + //{ + // sampleName = renderer.name; + //} + + // NOTE: Remove this for now (https://github.com/Arvtesh/UnityFx.Outline/issues/44). + //_commandBuffer.BeginSample(sampleName); + { + RenderObjectClear(settings.OutlineRenderMode); + DrawRenderer(renderer, settings); + RenderOutline(settings); + } + //_commandBuffer.EndSample(sampleName); + } + + /// + /// Specialized render target setup. Do not use if not sure. + /// + public void RenderObjectClear(OutlineRenderFlags flags) + { + // NOTE: Use the camera depth buffer when rendering the mask. Shader only reads from the depth buffer (ZWrite Off). + if ((flags & OutlineRenderFlags.EnableDepthTesting) != 0) + { + if (_rtDimention == TextureDimension.Tex2DArray) + { + // NOTE: Need to use this SetRenderTarget overload for XR, otherwise single pass instanced rendering does not function properly. + _commandBuffer.SetRenderTarget(_resources.MaskTex, _depth, 0, CubemapFace.Unknown, -1); + } + else + { + _commandBuffer.SetRenderTarget(_resources.MaskTex, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, _depth, RenderBufferLoadAction.Load, RenderBufferStoreAction.DontCare); + } + } + else + { + if (_rtDimention == TextureDimension.Tex2DArray) + { + _commandBuffer.SetRenderTarget(_resources.MaskTex, 0, CubemapFace.Unknown, -1); + } + else + { + _commandBuffer.SetRenderTarget(_resources.MaskTex, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store); + } + } + + _commandBuffer.ClearRenderTarget(false, true, Color.clear); + } + + ///+ /// Renders outline. Do not use if not sure. + /// + public void RenderOutline(IOutlineSettings settings) + { + var mat = _resources.OutlineMaterial; + var props = _resources.GetProperties(settings); + + _commandBuffer.SetGlobalFloatArray(_resources.GaussSamplesId, _resources.GetGaussSamples(settings.OutlineWidth)); + + if (_rtDimention == TextureDimension.Tex2DArray) + { + // HPass + _commandBuffer.SetRenderTarget(_resources.TempTex, 0, CubemapFace.Unknown, -1); + Blit(_resources.MaskTex, OutlineResources.OutlineShaderHPassId, mat, props); + + // VPassBlend + _commandBuffer.SetRenderTarget(_rt, 0, CubemapFace.Unknown, -1); + Blit(_resources.TempTex, OutlineResources.OutlineShaderVPassId, mat, props); + } + else + { + // HPass + _commandBuffer.SetRenderTarget(_resources.TempTex, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store); + Blit(_resources.MaskTex, OutlineResources.OutlineShaderHPassId, mat, props); + + // VPassBlend + _commandBuffer.SetRenderTarget(_rt, RenderBufferLoadAction.Load, RenderBufferStoreAction.Store); + Blit(_resources.TempTex, OutlineResources.OutlineShaderVPassId, mat, props); + } + } + + #endregion + + #region IDisposable + + ///+ /// Finalizes the effect rendering and releases temporary textures used. Should only be called once. + /// + public void Dispose() + { + _commandBuffer.ReleaseTemporaryRT(_resources.TempTexId); + _commandBuffer.ReleaseTemporaryRT(_resources.MaskTexId); + } + + #endregion + + #region implementation + + private void DrawRenderer(Renderer renderer, IOutlineSettings settings) + { + if (renderer && renderer.enabled && renderer.isVisible && renderer.gameObject.activeInHierarchy) + { + // NOTE: Accessing Renderer.sharedMaterials triggers GC.Alloc. That's why we use a temporary + // list of materials, cached with the outline resources. + renderer.GetSharedMaterials(_resources.TmpMaterials); + + if (_resources.TmpMaterials.Count > 0) + { + if (settings.IsAlphaTestingEnabled()) + { + for (var i = 0; i < _resources.TmpMaterials.Count; ++i) + { + var mat = _resources.TmpMaterials[i]; + + // Use material cutoff value if available. + if (mat.HasProperty(_resources.AlphaCutoffId)) + { + _commandBuffer.SetGlobalFloat(_resources.AlphaCutoffId, mat.GetFloat(_resources.AlphaCutoffId)); + } + else + { + _commandBuffer.SetGlobalFloat(_resources.AlphaCutoffId, settings.OutlineAlphaCutoff); + } + + _commandBuffer.SetGlobalTexture(_resources.MainTexId, _resources.TmpMaterials[i].mainTexture); + _commandBuffer.DrawRenderer(renderer, _resources.RenderMaterial, i, OutlineResources.RenderShaderAlphaTestPassId); + } + } + else + { + for (var i = 0; i < _resources.TmpMaterials.Count; ++i) + { + _commandBuffer.DrawRenderer(renderer, _resources.RenderMaterial, i, OutlineResources.RenderShaderDefaultPassId); + } + } + } + else + { + // NOTE: No materials set for renderer means we should still render outline for it. + _commandBuffer.DrawRenderer(renderer, _resources.RenderMaterial, 0, OutlineResources.RenderShaderDefaultPassId); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Blit(RenderTargetIdentifier src, int shaderPass, Material mat, MaterialPropertyBlock props) + { + // Set source texture as _MainTex to match Blit behavior. + _commandBuffer.SetGlobalTexture(_resources.MainTexId, src); + + // NOTE: SystemInfo.graphicsShaderLevel check is not enough sometimes (esp. on mobiles), so there is SystemInfo.supportsInstancing + // check and a flag for forcing DrawMesh. + if (SystemInfo.graphicsShaderLevel >= 35 && SystemInfo.supportsInstancing && !_resources.UseFullscreenTriangleMesh) + { + _commandBuffer.DrawProcedural(Matrix4x4.identity, mat, shaderPass, MeshTopology.Triangles, 3, 1, props); + } + else + { + _commandBuffer.DrawMesh(_resources.FullscreenTriangleMesh, Matrix4x4.identity, mat, 0, shaderPass, props); + } + } + + private static RenderTargetIdentifier GetBuiltinDepth(RenderingPath renderingPath) + { + return (renderingPath == RenderingPath.DeferredShading || renderingPath == RenderingPath.DeferredLighting) ? BuiltinRenderTextureType.ResolvedDepth : BuiltinRenderTextureType.Depth; + } + + #endregion + } +} diff --git a/Runtime/Scripts/Rendering/OutlineRenderer.cs.meta b/Runtime/Scripts/Rendering/OutlineRenderer.cs.meta new file mode 100644 index 0000000..6bfadc4 --- /dev/null +++ b/Runtime/Scripts/Rendering/OutlineRenderer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4271470bd9f5d5041a4a8881d8457a55 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Rendering/OutlineRendererCollection.cs b/Runtime/Scripts/Rendering/OutlineRendererCollection.cs new file mode 100644 index 0000000..6660443 --- /dev/null +++ b/Runtime/Scripts/Rendering/OutlineRendererCollection.cs @@ -0,0 +1,128 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace UnityFx.Outline +{ + internal class OutlineRendererCollection : ICollection+ { + #region data + + private readonly List _renderers = new List (); + private readonly GameObject _go; + + #endregion + + #region interface + + internal OutlineRendererCollection(GameObject go) + { + Debug.Assert(go); + _go = go; + } + + internal IReadOnlyList GetList() + { + return _renderers; + } + + internal void Reset(bool includeInactive) + { + _go.GetComponentsInChildren(includeInactive, _renderers); + } + + internal void Reset(bool includeInactive, int ignoreLayerMask) + { + _renderers.Clear(); + + if (ignoreLayerMask != 0) + { + var renderers = _go.GetComponentsInChildren (includeInactive); + + foreach (var renderer in renderers) + { + if (((1 << renderer.gameObject.layer) & ignoreLayerMask) == 0) + { + _renderers.Add(renderer); + } + } + } + else + { + _go.GetComponentsInChildren(includeInactive, _renderers); + } + } + + #endregion + + #region ICollection + + public int Count => _renderers.Count; + + public bool IsReadOnly => false; + + public void Add(Renderer renderer) + { + Validate(renderer); + + _renderers.Add(renderer); + } + + public bool Remove(Renderer renderer) + { + return _renderers.Remove(renderer); + } + + public void Clear() + { + _renderers.Clear(); + } + + public bool Contains(Renderer renderer) + { + return _renderers.Contains(renderer); + } + + public void CopyTo(Renderer[] array, int arrayIndex) + { + _renderers.CopyTo(array, arrayIndex); + } + + #endregion + + #region IEnumerable + + public IEnumerator GetEnumerator() + { + return _renderers.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _renderers.GetEnumerator(); + } + + #endregion + + #region implementation + + private void Validate(Renderer renderer) + { + if (renderer is null) + { + throw new ArgumentNullException(nameof(renderer)); + } + + if (!renderer.transform.IsChildOf(_go.transform)) + { + throw new ArgumentException(string.Format("Only children of the {0} are allowed.", _go.name), nameof(renderer)); + } + } + + #endregion + } +} diff --git a/Runtime/Scripts/Rendering/OutlineRendererCollection.cs.meta b/Runtime/Scripts/Rendering/OutlineRendererCollection.cs.meta new file mode 100644 index 0000000..f582b73 --- /dev/null +++ b/Runtime/Scripts/Rendering/OutlineRendererCollection.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 89621a3cc73c4e6498a00b2d180ed462 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Shaders.meta b/Runtime/Shaders.meta new file mode 100644 index 0000000..bf1ab62 --- /dev/null +++ b/Runtime/Shaders.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: e4ede0e617beaeb4a8781136599aa84e +folderAsset: yes +timeCreated: 1566126961 +licenseType: Free +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Shaders/Outline.shader b/Runtime/Shaders/Outline.shader new file mode 100644 index 0000000..41f6332 --- /dev/null +++ b/Runtime/Shaders/Outline.shader @@ -0,0 +1,196 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +// Renders outline based on a texture produced with 'UnityF/OutlineColor'. +// Modified version of 'Custom/Post Outline' shader taken from https://willweissman.wordpress.com/tutorials/shaders/unity-shaderlab-object-outlines/. +Shader "Hidden/UnityFx/Outline" +{ + HLSLINCLUDE + + #include "UnityCG.cginc" + + UNITY_DECLARE_SCREENSPACE_TEXTURE(_MaskTex); + UNITY_DECLARE_SCREENSPACE_TEXTURE(_MainTex); + float2 _MainTex_TexelSize; + + float4 _Color; + float _Intensity; + int _Width; + float _GaussSamples[32]; + +#if SHADER_TARGET < 35 || _USE_DRAWMESH + + v2f_img vert(appdata_img v) + { + v2f_img o; + UNITY_SETUP_INSTANCE_ID(v); + UNITY_INITIALIZE_OUTPUT(v2f_img, o); + UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); + + o.pos = float4(v.vertex.xy, UNITY_NEAR_CLIP_VALUE, 1); + o.uv = ComputeScreenPos(o.pos); + + return o; + } + +#else + + struct appdata_vid + { + uint vertexID : SV_VertexID; + UNITY_VERTEX_INPUT_INSTANCE_ID + }; + + float4 GetFullScreenTriangleVertexPosition(uint vertexID, float z = UNITY_NEAR_CLIP_VALUE) + { + // Generates a triangle in homogeneous clip space, s.t. + // v0 = (-1, -1, 1), v1 = (3, -1, 1), v2 = (-1, 3, 1). + float2 uv = float2((vertexID << 1) & 2, vertexID & 2); + return float4(uv * 2 - 1, z, 1); + } + + v2f_img vert(appdata_vid v) + { + v2f_img o; + UNITY_SETUP_INSTANCE_ID(v); + UNITY_INITIALIZE_OUTPUT(v2f_img, o); + UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); + + o.pos = GetFullScreenTriangleVertexPosition(v.vertexID); + o.uv = ComputeScreenPos(o.pos); + + return o; + } + +#endif + + float CalcIntensityN0(float2 uv, float2 offset, int k) + { + return UNITY_SAMPLE_SCREENSPACE_TEXTURE(_MainTex, uv + k * offset).r * _GaussSamples[k]; + } + + float CalcIntensityN1(float2 uv, float2 offset, int k) + { + return UNITY_SAMPLE_SCREENSPACE_TEXTURE(_MainTex, uv - k * offset).r * _GaussSamples[k]; + } + + float CalcIntensity(float2 uv, float2 offset) + { + float intensity = 0; + + // Accumulates horizontal or vertical blur intensity for the specified texture position. + // Set offset = (tx, 0) for horizontal sampling and offset = (0, ty) for vertical. + // + // NOTE: Unroll directive is needed to make the method function on platforms like WebGL 1.0 where loops are not supported. + // If maximum outline width is changed here, it should be changed in OutlineResources.MaxWidth as well. + // + [unroll(32)] + for (int k = 1; k <= _Width; ++k) + { + intensity += CalcIntensityN0(uv, offset, k); + intensity += CalcIntensityN1(uv, offset, k); + } + + intensity += CalcIntensityN0(uv, offset, 0); + return intensity; + } + + float4 frag_h(v2f_img i) : SV_Target + { + UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i); + + float intensity = CalcIntensity(i.uv, float2(_MainTex_TexelSize.x, 0)); + return float4(intensity, intensity, intensity, 1); + } + + float4 frag_v(v2f_img i) : SV_Target + { + UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i); + + if (UNITY_SAMPLE_SCREENSPACE_TEXTURE(_MaskTex, i.uv).r > 0) + { + // TODO: Avoid discard/clip to improve performance on mobiles. + discard; + } + + float intensity = CalcIntensity(i.uv, float2(0, _MainTex_TexelSize.y)); + intensity = _Intensity > 99 ? step(0.01, intensity) : intensity * _Intensity; + return float4(_Color.rgb, saturate(_Color.a * intensity)); + } + + ENDHLSL + + // SM3.5+ + SubShader + { + Cull Off + ZWrite Off + ZTest Always + Lighting Off + + Pass + { + Name "HPass" + + HLSLPROGRAM + + #pragma target 3.5 + #pragma multi_compile_instancing + #pragma shader_feature_local _USE_DRAWMESH + #pragma vertex vert + #pragma fragment frag_h + + ENDHLSL + } + + Pass + { + Name "VPassBlend" + Blend SrcAlpha OneMinusSrcAlpha + + HLSLPROGRAM + + #pragma target 3.5 + #pragma multi_compile_instancing + #pragma shader_feature_local _USE_DRAWMESH + #pragma vertex vert + #pragma fragment frag_v + + ENDHLSL + } + } + + // SM2.0 + SubShader + { + Cull Off + ZWrite Off + ZTest Always + Lighting Off + + Pass + { + Name "HPass" + + HLSLPROGRAM + + #pragma vertex vert + #pragma fragment frag_h + + ENDHLSL + } + + Pass + { + Name "VPassBlend" + Blend SrcAlpha OneMinusSrcAlpha + + HLSLPROGRAM + + #pragma vertex vert + #pragma fragment frag_v + + ENDHLSL + } + } +} diff --git a/Runtime/Shaders/Outline.shader.meta b/Runtime/Shaders/Outline.shader.meta new file mode 100644 index 0000000..bb55354 --- /dev/null +++ b/Runtime/Shaders/Outline.shader.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 41c9acbf41c8245498ac9beab378de12 +timeCreated: 1566126977 +licenseType: Free +ShaderImporter: + externalObjects: {} + defaultTextures: [] + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Shaders/OutlineColor.shader b/Runtime/Shaders/OutlineColor.shader new file mode 100644 index 0000000..9383693 --- /dev/null +++ b/Runtime/Shaders/OutlineColor.shader @@ -0,0 +1,77 @@ +// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. +// See the LICENSE.md file in the project root for more information. + +// Renders everything with while color. +// Modified version of 'Custom/DrawSimple' shader taken from https://willweissman.wordpress.com/tutorials/shaders/unity-shaderlab-object-outlines/. +Shader "Hidden/UnityFx/OutlineColor" +{ + HLSLINCLUDE + + #include "UnityCG.cginc" + + UNITY_DECLARE_TEX2D(_MainTex); + float _Cutoff; + + v2f_img vert(appdata_img v) + { + v2f_img o; + UNITY_SETUP_INSTANCE_ID(v); + UNITY_INITIALIZE_OUTPUT(v2f_img, o); + UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); + + o.pos = UnityObjectToClipPos(v.vertex); + o.uv = v.texcoord; + + return o; + } + + half4 frag() : SV_Target + { + return 1; + } + + half4 frag_clip(v2f_img i) : SV_Target + { + UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i); + + half4 c = UNITY_SAMPLE_TEX2D(_MainTex, i.uv); + clip(c.a - _Cutoff); + return 1; + } + + ENDHLSL + + SubShader + { + Cull Off + ZWrite Off + ZTest LEqual + Lighting Off + + Pass + { + Name "Opaque" + + HLSLPROGRAM + + #pragma multi_compile_instancing + #pragma vertex vert + #pragma fragment frag + + ENDHLSL + } + + Pass + { + Name "Transparent" + + HLSLPROGRAM + + #pragma multi_compile_instancing + #pragma vertex vert + #pragma fragment frag_clip + + ENDHLSL + } + } +} diff --git a/Runtime/Shaders/OutlineColor.shader.meta b/Runtime/Shaders/OutlineColor.shader.meta new file mode 100644 index 0000000..66aa3a2 --- /dev/null +++ b/Runtime/Shaders/OutlineColor.shader.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: ac20fbf75bafe454aba5ef3c098349df +timeCreated: 1566126977 +licenseType: Free +ShaderImporter: + externalObjects: {} + defaultTextures: [] + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/UnityFx.Outline.asmdef b/Runtime/UnityFx.Outline.asmdef new file mode 100644 index 0000000..af0702d --- /dev/null +++ b/Runtime/UnityFx.Outline.asmdef @@ -0,0 +1,3 @@ +{ + "name": "UnityFx.Outline" +} diff --git a/Runtime/UnityFx.Outline.asmdef.meta b/Runtime/UnityFx.Outline.asmdef.meta new file mode 100644 index 0000000..6a74c49 --- /dev/null +++ b/Runtime/UnityFx.Outline.asmdef.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: de7ed7f8e7092c144bd17cbabf282ba3 +timeCreated: 1566126961 +licenseType: Free +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/package.json b/package.json new file mode 100644 index 0000000..2778456 --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "com.unityfx.outline", + "version": "0.8.5", + "displayName": "Outline toolkit", + "description": "This package contains configurable per-object and per-camera outline effect implementation for built-in render pipeline. Both solid and blurred outline modes are supported (Gauss blur), as well as depth testing. Reusable and extensible API.", + "unity": "2018.4", + "keywords": [ + "UnityFx", + "UnityFx.Outline", + "outline", + "post-effect" + ], + "category": "UnityFx", + "author": { + "name": "Arvtesh", + "email": "arvtesh@gmail.com" + }, + "license": "MIT", + "homepage": "https://github.com/Arvtesh/UnityFx.Outline", + "repository": { + "type": "git", + "url": "https://github.com/Arvtesh/UnityFx.Outline.git" + }, + "bugs": { + "url": "https://github.com/Arvtesh/UnityFx.Outline/issues" + }, + "samples": [ + { + "displayName": "Example Setting", + "description": "", + "path": "~Samples/Example" + } + ] +} \ No newline at end of file diff --git a/package.json.meta b/package.json.meta new file mode 100644 index 0000000..198e6dc --- /dev/null +++ b/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f0fa2388b44716e42afe4670ecd796b3 +PackageManifestImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/~Samples.meta b/~Samples.meta new file mode 100644 index 0000000..dd1a919 --- /dev/null +++ b/~Samples.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7a2ba83db4065a54ea4edc22eb4efb69 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/~Samples/Example.meta b/~Samples/Example.meta new file mode 100644 index 0000000..3307acb --- /dev/null +++ b/~Samples/Example.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 122e93f02cdf07045a82cf5646b17038 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/~Samples/Example/.sample.json b/~Samples/Example/.sample.json new file mode 100644 index 0000000..3b43eaf --- /dev/null +++ b/~Samples/Example/.sample.json @@ -0,0 +1,5 @@ +{ + "displayName":"Example Sample", + "description": "Replace this string with your own description of the sample. Delete the Samples folder if not needed.", + "createSeparatePackage": false +} diff --git a/~Samples/Example/OutlineResources.asset b/~Samples/Example/OutlineResources.asset new file mode 100644 index 0000000..df54c32 --- /dev/null +++ b/~Samples/Example/OutlineResources.asset @@ -0,0 +1,16 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: b503341e0a514e3489c4851727e68257, type: 3} + m_Name: OutlineResources + m_EditorClassIdentifier: + _renderShader: {fileID: 4800000, guid: ac20fbf75bafe454aba5ef3c098349df, type: 3} + _outlineShader: {fileID: 4800000, guid: 41c9acbf41c8245498ac9beab378de12, type: 3} diff --git a/~Samples/Example/OutlineResources.asset.meta b/~Samples/Example/OutlineResources.asset.meta new file mode 100644 index 0000000..41bc6af --- /dev/null +++ b/~Samples/Example/OutlineResources.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9030338688e432a4a883ae181554f544 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/~Samples/Example/OutlineSettings.asset b/~Samples/Example/OutlineSettings.asset new file mode 100644 index 0000000..c75681a --- /dev/null +++ b/~Samples/Example/OutlineSettings.asset @@ -0,0 +1,19 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: b579424fd3338724cba3155ee4d53475, type: 3} + m_Name: OutlineSettings + m_EditorClassIdentifier: + _outlineColor: {r: 0, g: 1, b: 1, a: 1} + _outlineWidth: 32 + _outlineIntensity: 2.3 + _outlineAlphaCutoff: 0.9 + _outlineMode: 1 diff --git a/~Samples/Example/OutlineSettings.asset.meta b/~Samples/Example/OutlineSettings.asset.meta new file mode 100644 index 0000000..288ebb7 --- /dev/null +++ b/~Samples/Example/OutlineSettings.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8856b749fef9c674092af6d255f0a351 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/~Samples/Example/Resources.meta b/~Samples/Example/Resources.meta new file mode 100644 index 0000000..ef4241e --- /dev/null +++ b/~Samples/Example/Resources.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 73500f231d0ee644d9ffce9390f2ff91 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/~Samples/Example/Resources/OutlineLayerCollection.asset b/~Samples/Example/Resources/OutlineLayerCollection.asset new file mode 100644 index 0000000..d5a50d8 --- /dev/null +++ b/~Samples/Example/Resources/OutlineLayerCollection.asset @@ -0,0 +1,27 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 57d0c11168277cf4eb3b4b89706e6aa5, type: 3} + m_Name: OutlineLayerCollection + m_EditorClassIdentifier: + _layers: + - _settings: + _outlineSettings: {fileID: 11400000, guid: 8856b749fef9c674092af6d255f0a351, + type: 2} + _outlineColor: {r: 1, g: 0, b: 0.009791374, a: 1} + _outlineWidth: 32 + _outlineIntensity: 2.3 + _outlineAlphaCutoff: 0.9 + _outlineMode: 1 + _name: + _enabled: 1 + _mergeLayerObjects: 0 + _ignoreLayerMask: 0 diff --git a/~Samples/Example/Resources/OutlineLayerCollection.asset.meta b/~Samples/Example/Resources/OutlineLayerCollection.asset.meta new file mode 100644 index 0000000..e15903f --- /dev/null +++ b/~Samples/Example/Resources/OutlineLayerCollection.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c8518571f6021a848a8607839531e7fd +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 0 + userData: + assetBundleName: + assetBundleVariant: