From acbf36ee28836092d57e1f71116afbbe01ca646d Mon Sep 17 00:00:00 2001 From: gered Date: Mon, 7 Nov 2022 13:21:45 -0500 Subject: [PATCH] adjust rotozoom internal transformed destination rect coord calculation maybe marginally better/simpler, but also some minor cleanups and a semi-hackfix for minor destination blit corners being cutoff slightly for some rotation angles --- libretrogd/src/graphics/bitmap/blit.rs | 56 ++++++++---------- .../tests/ref/blended_rotozoom_blits.pcx | Bin 30752 -> 30848 bytes .../blended_rotozoom_transparent_blits.pcx | Bin 30686 -> 30781 bytes libretrogd/tests/ref/rotozoom_blits.pcx | Bin 6373 -> 6482 bytes .../tests/ref/rotozoom_offset_blits.pcx | Bin 6373 -> 6482 bytes .../tests/ref/rotozoom_transparent_blits.pcx | Bin 6437 -> 6542 bytes .../ref/rotozoom_transparent_offset_blits.pcx | Bin 6437 -> 6542 bytes 7 files changed, 26 insertions(+), 30 deletions(-) diff --git a/libretrogd/src/graphics/bitmap/blit.rs b/libretrogd/src/graphics/bitmap/blit.rs index 8e1d281..4ca7053 100644 --- a/libretrogd/src/graphics/bitmap/blit.rs +++ b/libretrogd/src/graphics/bitmap/blit.rs @@ -302,16 +302,6 @@ unsafe fn per_pixel_flipped_blit( } } -#[inline] -fn rotate_xy(x: f32, y: f32, angle: f32, origin_x: f32, origin_y: f32) -> (f32, f32) { - let sin = angle.sin(); - let cos = angle.cos(); - ( - origin_x + ((x - origin_x) * cos) - ((y - origin_y) * sin), - origin_y + ((x - origin_x) * sin) + ((y - origin_y) * cos) - ) -} - #[inline] unsafe fn per_pixel_rotozoom_blit( dest: &mut Bitmap, @@ -324,13 +314,13 @@ unsafe fn per_pixel_rotozoom_blit( scale_y: f32, pixel_fn: impl Fn(u8, &mut Bitmap, i32, i32), ) { - let dest_width = (src_region.width as f32 * scale_x) as i32; - let dest_height = (src_region.height as f32 * scale_y) as i32; + let dest_width = src_region.width as f32 * scale_x; + let dest_height = src_region.height as f32 * scale_y; - let half_src_width = (src_region.width / 2) as f32; - let half_src_height = (src_region.height / 2) as f32; - let half_dest_width = (dest_width / 2) as f32; - let half_dest_height = (dest_height / 2) as f32; + let half_src_width = src_region.width as f32 * 0.5; + let half_src_height = src_region.height as f32 * 0.5; + let half_dest_width = dest_width * 0.5; + let half_dest_height = dest_height * 0.5; // calculate the destination bitmap axis-aligned bounding box of the region we're drawing to // based on the source bitmap bounds when rotated and scaled. this is to prevent potentially @@ -339,20 +329,26 @@ unsafe fn per_pixel_rotozoom_blit( // extents for 90-degree angle rotations. this feels kinda ugly to me, but not sure what other // clever way to calculate this that there might be (if any). - let left = 0.0; - let top = 0.0; - let right = (dest_width - 1) as f32; - let bottom = (dest_height - 1) as f32; + let sin = angle.sin(); + let cos = angle.cos(); - let (top_left_x, top_left_y) = rotate_xy(left, top, angle, half_dest_width, half_dest_height); - let (top_right_x, top_right_y) = rotate_xy(right, top, angle, half_dest_width, half_dest_height); - let (bottom_left_x, bottom_left_y) = rotate_xy(left, bottom, angle, half_dest_width, half_dest_height); - let (bottom_right_x, bottom_right_y) = rotate_xy(right, bottom, angle, half_dest_width, half_dest_height); + let left = -half_dest_width * cos - half_dest_height * sin; + let top = -half_dest_width * sin + half_dest_height * cos; + let right = half_dest_width * cos - half_dest_height * sin; + let bottom = half_dest_width * sin + half_dest_height * cos; - let x1 = top_left_x.min(bottom_left_x).min(top_right_x).min(bottom_right_x) as i32; - let x2 = top_left_x.max(bottom_left_x).max(top_right_x).max(bottom_right_x) as i32; - let y1 = top_left_y.min(bottom_left_y).min(top_right_y).min(bottom_right_y) as i32; - let y2 = top_left_y.max(bottom_left_y).max(top_right_y).max(bottom_right_y) as i32; + let (top_left_x, top_left_y) = (left + half_dest_width, top + half_dest_height); + let (top_right_x, top_right_y) = (right + half_dest_width, bottom + half_dest_height); + let (bottom_left_x, bottom_left_y) = (-left + half_dest_width, -top + half_dest_height); + let (bottom_right_x, bottom_right_y) = (-right + half_dest_width, -bottom + half_dest_height); + + // HACK: -/+ 1's because this seems to fix some destination area accidental clipping for _some_ + // rotation angles ... ? i guess some other math is probably wrong somewhere or some + // floating point rounding fun perhaps? + let x1 = top_left_x.min(bottom_left_x).min(top_right_x).min(bottom_right_x) as i32 - 1; + let x2 = top_left_x.max(bottom_left_x).max(top_right_x).max(bottom_right_x) as i32 + 1; + let y1 = top_left_y.min(bottom_left_y).min(top_right_y).min(bottom_right_y) as i32 - 1; + let y2 = top_left_y.max(bottom_left_y).max(top_right_y).max(bottom_right_y) as i32 + 1; // now we're ready to draw. we'll be iterating through each pixel on the area we calculated // just above -- that is (x1,y1)-(x2,y2) -- on the DESTINATION bitmap and for each of these @@ -385,8 +381,8 @@ unsafe fn per_pixel_rotozoom_blit( if src_x >= 0.0 && (src_x as i32) < (src_region.width as i32) && src_y >= 0.0 && (src_y as i32) < (src_region.height as i32) { let pixel = src.get_pixel_unchecked(src_x as i32 + src_region.x, src_y as i32 + src_region.y); - let draw_x = x + dest_x as i32; - let draw_y = y + dest_y as i32; + let draw_x = x + dest_x; + let draw_y = y + dest_y; pixel_fn(pixel, dest, draw_x, draw_y); } } diff --git a/libretrogd/tests/ref/blended_rotozoom_blits.pcx b/libretrogd/tests/ref/blended_rotozoom_blits.pcx index c628b83467be7077765ea4b6f1dc35c8d07fd9c9..477c12ec506f660c139e8e4fd629cd731a6d6f8f 100644 GIT binary patch delta 1482 zcmbW1QA`v^7{{|WdvoBS9vlh?a)+FNVnj*>C0bC6z0(RO8feu}(H_+lqn5k0O%u^R zG@6|>?tZYFm^AXxq^9kuWY34jCM`3&$K8R755>Nilg2cCXso{Y+J1X!)0aLqne1f0 z`F3Z%|NOrH)N|?L3+d{WGNDo{W$Qc{;Ym8L8Y)$Zh&{nCDG$=4z zd4r@U6!U!54L5Ew4@;44?zACT8lpp|%)5ajlB9zgT?M{qGb}8Z;b`6aRFCnLG7Hnu zI(Sz1NwL91jE{3>L8)A=?JkGw>-KpJ5wr4P4$s#G$$f)CS$(HhWjv6u0wTf${LofG z(kg|s_4SSPzG^&BjX9NyWJDx|E@-+C!C*!ey0wT=ItRn*;Zk5&@)u+|66$bLAk^lwK^Zi}Bg_fR6I(nAR8qALIfG!p-ox+QvZKmCE%1ts80@bL2CpDiU zLZ1IZsMBSh+U<40^^I;4;o*W#PCC9*RAU^E)mnQNeDW%9cG5X|fIAXR*AAJ*d+*2+ zh2f)@9ahcW{kJ4Ji}k^hCy0ob!K(+}aOZRpQ$QN3g{cGEi*gQF0iDAy2cpG>NLhN4 zQyYS3YREhy@OE!+iOOd3M!RK*q#|HxXp^KszIO`|ij7xX4h?rqPj?~cm@NIuQkkh#0Fm%uHmVX~PO%bU`7=d@Kg8oBs7j1%8T0yed z!%K(mlI(;tZ!|N9?@00-_Agu*SWAS#;fsO2#KPWrI`EE+_LVbVAG}5+1~1yG{+E$J ze&kED;rP!ILHM*2JIOo}+;!rlXAavi6<{#4@3Oh&6ID%sz)wdz%}skf!IkRkc=hZ7 z_(q4YdDdm#7Zu`l74NsSv~8#cf9tBq(lM51Iulp+5f0{6k?Oha{q11W(?C`<{Lyll zC^~9_txx4NnHst7?6%aGme;Xm4`DhODOc|WyF3%we}EJIR;x zC|klWR$+XgVZWZVcG!KHajXH0jRAZ&5Fv7oyQT)d!9yNdmuS+OiPHNr{nuUrBnP`5 z_f#W0*yxf+rf8DQuqfs(HNiXd<{^b|uzVNm|0Oyyl`UCGHp?9+*cz}rbj`s^$uKMB z&@?>iR23#!VPysL!&jUIm5MAF-oT&3Gqvj)m4k|2EdTc;tZTS)?jwwg^$xHM=gA## zq!zvB7oCNLCxBUTAuHg^h#$KnUc%-7edJ{{x zf!}VPz@si#RZ*px1T9Ol&2L7e0-*o^ diff --git a/libretrogd/tests/ref/blended_rotozoom_transparent_blits.pcx b/libretrogd/tests/ref/blended_rotozoom_transparent_blits.pcx index 1f13edd847c6b81097702a1b73899eaaf7640847..13bc054ee3f8c749131fd41e01ef5ed8ec4a7587 100644 GIT binary patch delta 1290 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 delta 1231 zcmds%ziU)M5XX1lkA3guF3IKOq8K4jr2}v-CUXn}97`)^;`|jNj!M{MbV|Qo2GdnwYZW$N7l0Co=a6EYx2IB+KzYoq@S9%u0BYjTi(m`eId}NiTV#?-<)$EK-TONlK z$?Xsm9Uh`ECG9FWDz?_Gp)#3XzJp7m#aTh2OrOD!*wIixrgSteiueoRZ9~iTMHG_4 zOXhQJ(|4yFmQf0=vw4nXEGZUH-s;qX?NwYRe>GzD>@S7H4^u#59pCYcomb zL8sHaP0XX9(q@^ol%7k2LNT!cmz{K+49cJwWGKR8=a452#cWHlbr^R#ydQKA=38P7 zO!uI(ND2DZ3iP@iZh_{OSV6eqc3S~1AiQ*Y4H~B1(E|bgMkSOXW+^LE=kDu5=EDo- zY(_t2g_WIy7hWr$gK_UU1WnCwI@wQ@+QEsP+4qwQMnSeJiJSyG^@bqE24Fkm|l-z@;<-!je4l^Qurc#GD?`s zJK>`dNxo19rQP@BSN(1#!Wg9djJ~Ke$~X#LA~UD+ov4vdl1jf#0YY8sr125qTxzc# SZl}8R@G3P#!&rCoPV*06>uY}i diff --git a/libretrogd/tests/ref/rotozoom_offset_blits.pcx b/libretrogd/tests/ref/rotozoom_offset_blits.pcx index 382da4990f1a05b5b3bc25e26bd485036836c526..6f2808210c0d41245c9c175922b5b72245276dcd 100644 GIT binary patch delta 1330 zcmds1&2J4+6z_EA&BuMcGxKITuiCzdB4~U>8<7YhHdYokVkKcoY;0`0z=DNFgS+q# zSYTn(#6n`xd+wdrG`@lmf}Kiz_d`x;b)1>!^%4Jp%;MbNIlpsq@A>^QZwDR^UOK}+ zp|8r?)t>(V{ma9p1in;R_(U9SKh)jiW;jh>KC|&X8XaptW{B<9k)HRMs7MDYw6&R9UXw9}iMVC0 zFe#3XTb>cXatRTVx_DLE<;MZ+G51+J@yr2$s@(Vj~DW!GId*O*wZK(SWxmxb&ERLd*pQ& z9N=FNxQf(owl<*+N(Qu11FKug(^l%roSnKUu|MY1(YZlYHNm0nFQHp z|Hipoc@_Mr`fMWwt_9|@-jo!(%abM~QjE2q6E{2dty#>8(6Y-yfm=O7fi>;81gPjNR`8!kI#fuylK6f50TO~3r0l~u!yJogK& z5h7_4Vc6|Ab0JQJLm}z;mutjQxMKDAzwY>sNuV0On8M%((vFp*J9 z!Yqd^1qqH`jfRZ_?=bm^x-Nqd3{QzKB7xwTOF@vw0j@N0TBV z8c$U zf3p4zR~k>*H9f{RaKJwgH`BezvlosT=XxeE@}(PjrqoNuAMbdghNjR3GlagXGWf}` zH8|+6gOu4}BL-d=#3*2&S#TCmcwD8g;EdT~&0!$Ch{G+jn3+W`Ji$dKsNhe8mzkD} zQy7F?_#uxnN{e*aP}0m93>{&JCcCL@NESL1$jeMvf=?>D^phQ=E*eCWCK{;0DI1Y3 zjS^Gl5-X8;LYYfG59vJUwDN16apabyT9^`ioT@Hu#e*d{Yqh6I5H63DP=Wi_PCIl3 z_p^d8!jRQrf2Z%@Q8riro$auuFbtyX9PD8oY?jv9U=HCD>o#JXKzPdbG*bA)2DZoe z3xy+0=L@}+6IwDISv)3&VNST^HF}xp!m9q+F?eFH*T!JTehhIo4@dmdM5zs|)X}X! zh(+YCrZZnv{^l}|a6R<}`qLMxjo;FTTbIZt>P#ytt%Ix}oZv60!TV1*UUoNmt2i#9 zVK2Z#tL+cVWq9wl^A(oMR7Y@GYr?YvyS;XrQpK}^G?l9T8rw`%>EAx^I@!EZb!wVn z>tV_}xnJEZ`7gpqgxv(~EYi?B(h6(v7cu8XHR!W&H`|w>?t(}Du8kpvejOOyBFgK; zR6dCQZSBTanxUv!=4GKcf>E)$s9p1wN~usBUa@>YQLI&8Q=e=WdC^i3`NTlZceIBn wB%UFpaW9JIa!aHvb^9o})QK^IlRMP-7U6hqiwakBT`D}w-J&qqo!_AU25Q}S(*OVf diff --git a/libretrogd/tests/ref/rotozoom_transparent_blits.pcx b/libretrogd/tests/ref/rotozoom_transparent_blits.pcx index bfc33a212c6421e008b1cd3eb07f4716f551f0f5..fe941ae7b17cd133357cfd92c422b3a61727958a 100644 GIT binary patch delta 1278 zcmds1&r4KM6z0x*@6MaiX+~4Ql2V5(!IVrFlDi-xsFu2lk|0`yv}kAKqKGiWIJAjq zWi6tev}!f?y_qS|>=#8X+NG8$jXye;qp$D2ndglELR@(Fo^!r??)T1j?tR=jdUR|! zw#d>o(wr9ZHOqS=y@4YmD0XJNdd3(;FNxceauR-U9DbD;Z2fs-ph3_Jqh_rR?T zhaF2eTppU3Hsc~*O3OGROSxEY%Q`CnT^#31N5~bJ8R5!5ER|3aI0IotXJmuD%F;0u zjH|LS>N3K4X8iX9x%bj9;MsB+DK@lImksQ4NC!H?5?M~0js*EEs5x?(UCS+E)7y-5 z2qS>4rqbAq$N>9O=gwFag7BWw#!!0o%phDQf*fJb$ zk2)dn*5*1r4|i1l1T7bv70WeRM0^Pj#f~YLso8Fi`m{~3m(*`}hSyk{%UF-vJqB3O zEBk(8$rou7UneBLp(vLl`>ib$ah66Rjb@7F96gIX*`urL@s^L)RnHWKb(p?Z-Jsd% z**4VGHC#@W%ehkU?GzGDI#;n^I8ym-F;jUWnb$y+Cg_6=-!6#{o4R8!kYCcgb?m>> zyDy&Hp?98c#}Dn=)_W68@tacNWVz;>G#@_z1&f+*Ko%jHI_#!E`M;>v@kCopD~}55 zwg=#m-D+qJ#uGyct)<6h`X8>UPrlUpBjBL>6>b9%xJW5>O<$i$-4p{=cS64SBXq60 zs~L@=jFNnwHsK58ggr4;JtpKFOZ~>2uc_-QEQ>b`?@;lH`rrE3KYjn_08 edd}AL8&jB@Y7hGJsoEp{e7*Ljo+pgDX5$}zjm)wD delta 1149 zcmd^9O-~d-5KYfV&#VjCU2r#uCa$OfA}$fUnD_@g`EgKB9=w_8m4w9jk(dyTD5T=a z$W70hJ&8uMJ+rd}6%!5|JQ)o}*a!$KPvhWNy|WAW8@x<)RlTX#Rj<464L%+iyOCUi zZ6BrK6?!i6j*dmB&?j-Xv7V=4 z<6I(+M|fKl=4nAsTMp0}>taW>&gBaxWQHba5j10U7>g(_SSdk<*-1Hh_z)mX#UPIv zeCY|f1|u}tuk&(Ou_Qx61a$h zCzrVlJX^sR8cCT{_PUZ)$co_KHBO;yH|ms4j@g`$-x&bk<_;_k4n)9UUMK`MZzrbZ zom6bU4h}HI91LEBEb*KJDBcA-%HZmd8>qx={0j{^yIVQu86~FRWJnWE!BK_>+X`vU z=}&y)F5z2poic90S?0ccYmH901y}cI@sQ}IJ77hqf+pQ#2Ag&FK{CQ`X7LR91@thn z*Nm`0uM>H(K%Wy&sJ|^ou6d2Rk|%qA@XUOawhL6NhvN2|qSZi;l2w{)IZGc~hVmN> z;ZZfSp?0y&rQy~W3{L-F%1S=N@kyXzr{f=GH)uN5lZcg#peRgrZ)*F8`qKv++J>f?TAq0qpu)A({8W9O4QO=LUqk?G{`%gk+7_YTdr9o?^> z%q^Mt>abth5=sY(J?&${`l_$}tDU)mUg#I1{viVw^aOZ|qkTX$myrWJCdPvy9=u`XpfLtR zFdZ+(ctQ`x8}a06yL)DpfcT0=58mVrad}uED`WL6%i~`#bEvNBU)R*sS3S?#hE5C* zn2TWXxCfqYy#wpvU{@vJ>(&GYKDNa_1G5772B;)PTKkvz9<1XoQo{+x88PX~DcFcjx(;o1o;(;&g z72#|pOce%9lNs#Y8l8~sh|P9F0L(3rqS@8B4%7*N6Lm@4 zrsTYm)mFh_Ns7)5rOx1BQKtj|DtWBbUi}3Qg*3;?olOzsGFBH=iVwnZacS+cZuAyk6Jst|AMUlCV4ftm-$~Z zV;@F>+sro+i>w1_)#@wc|6sRHC7Vq_9y`czxKQv^Ymx+mkz{`oS?=e5ES1T<5&RJ~ zGd!RP4#<21+&o^ba0Kh5U;qFB delta 1149 zcmd^;&rcIU6vwmMr8~Q2+ug3^XA>)GfQS&mi;4e$Cx0B&lLv1mdL$%vR_ICV0Xm~y>?p%e2k`B9fP&T(l|YMHhg!nys+Lknjdf95{O}>b zwLLe4RS+JewFZ|>+Jm2rl;or`*FCcgt0*|#szl)+-PkA4*cVqN^lvbW@C~j& zMB;)du(%wF3u(U2&at}GigCX#E@0l^m@rGQ@(5V&C8Sm2gcR=%cN;}Wqk<@fM)hus zV=|TJm*QZJX7oYbOG%`i8ZGNX##gQqzT<9@BL?*~uHSby>4X>*T%+r_o1hzFSPM}G z&52_wn=Nra5#kr5DdXm6&;w&{EW|3kGIGi)eKH=?P`Zso>>BkXPWFH2lYQj>22`!P zq~JGbLaTxvCdxF|dX_%4j^?(b`|cvG)vw^3c= z`ivd2Av~(3dYf#2(~y0z!L~*h?A``je#U$DfEl7%|F;HcLG-IkqH15YqLvGqjK`f$ zWrJp%E*@Vvx0vp2T1p??@4-v&V&c}}b$*v>ut z3EH#oA+tLz{+gpfC-+8oy95hJWcI0S1~TW>x1fnkZ)Bckjznf9bCb>N<+h&KAD#}7 As{jB1