From 46d652393378fc4ae3af51369e99a0d681306d28 Mon Sep 17 00:00:00 2001 From: Hotox Date: Sat, 23 Nov 2019 21:16:13 +0100 Subject: [PATCH 01/24] Fixed export warning being empty --- README.md | 5 ++++- __init__.py | 2 +- tools/importer.py | 19 ++++++++++++------- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a6d34017..af8bbce4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Cats Blender Plugin (0.16.1) +# Cats Blender Plugin (0.16.2) A tool designed to shorten steps needed to import and optimize models into VRChat. Compatible models are: MMD, XNALara, Mixamo, Source Engine, Unreal Engine, DAZ/Poser, Blender Rigify, Sims 2, Motion Builder, 3DS Max and potentially more @@ -298,6 +298,9 @@ It checks for a new version automatically once every day. ## Changelog +#### 0.16.2 +- Fixed export warning being empty + #### 0.16.1 - Fixed export warning bug diff --git a/__init__.py b/__init__.py index e5326342..aa1c6711 100644 --- a/__init__.py +++ b/__init__.py @@ -36,7 +36,7 @@ 'tracker_url': 'https://github.com/michaeldegroot/cats-blender-plugin/issues', 'warning': '', } -dev_branch = False +dev_branch = True import os import sys diff --git a/tools/importer.py b/tools/importer.py index 5a783f72..937e6068 100644 --- a/tools/importer.py +++ b/tools/importer.py @@ -754,6 +754,11 @@ def execute(self, context): _textures_found = False _eye_meshes_not_named_body = [] +max_mats = 4 +max_tris = 70000 +max_meshes_light = 2 +max_meshes_hard = 8 + @register_wrap class ExportModel(bpy.types.Operator): @@ -839,9 +844,9 @@ def execute(self, context): break # Check if a warning should be shown - if _meshes_count > 1 \ - or _tris_count > 70000 \ - or len(_mat_list) > 4 \ + if _meshes_count > max_meshes_light \ + or _tris_count > max_tris \ + or len(_mat_list) > max_mats \ or len(_broken_shapes) > 0 \ or not _textures_found and Settings.get_embed_textures()\ or len(_eye_meshes_not_named_body) > 0: @@ -929,7 +934,7 @@ def draw(self, context): layout = self.layout col = layout.column(align=True) - if self.tris_count > 70000: + if self.tris_count > max_tris: row = col.row(align=True) row.scale_y = 0.75 row.label(text="Too many polygons!", icon='ERROR') @@ -965,7 +970,7 @@ def draw(self, context): # col.separator() # col.separator() - if self.mat_count > 4: + if self.mat_count > max_mats: row = col.row(align=True) row.scale_y = 0.75 row.label(text="Model not optimized!", icon='INFO') @@ -985,7 +990,7 @@ def draw(self, context): col.separator() col.separator() - if self.meshes_count > 2: + if self.meshes_count > max_meshes_light: row = col.row(align=True) row.scale_y = 0.75 row.label(text="Meshes not joined!", icon='ERROR') @@ -997,7 +1002,7 @@ def draw(self, context): col.separator() row = col.row(align=True) row.scale_y = 0.75 - if self.meshes_count < 9: + if self.meshes_count <= max_meshes_hard: row.label(text="It is not very optimized and might cause lag for you and others.") else: row.label(text="It is extremely unoptimized and will cause lag for you and others.") From ca70724a09d584e6ca918807d835851bf8179658 Mon Sep 17 00:00:00 2001 From: Hotox Date: Mon, 6 Jan 2020 17:08:58 +0100 Subject: [PATCH 02/24] Made a model compatible --- resources/icons/supporters/TheKally.png | Bin 0 -> 2555 bytes resources/supporters.json | 11 ++++++-- tools/armature.py | 1 + tools/armature_bones.py | 36 ++++++++++++------------ 4 files changed, 27 insertions(+), 21 deletions(-) create mode 100644 resources/icons/supporters/TheKally.png diff --git a/resources/icons/supporters/TheKally.png b/resources/icons/supporters/TheKally.png new file mode 100644 index 0000000000000000000000000000000000000000..4b011a642407e14dafa26a62eaf1cb30915a409f GIT binary patch literal 2555 zcmVFc83h3iJi~96=r-fBFCg;vYd#q=oA?KvAbQI=QKb zNVX)3QYea-$8hd zj3VIudG~jpeC89qdil~TS1+=1ZGT)arKq}8MaT!q7G{B`$A*(dibhaCR4FQOHU>wE zgBZuyEuQ`Be|`GZ-qX}6KQm{}tZ&}>!^!sU!AbMWzyE06`MO=d{ii>8hw72T1`i~ro`61g%O91&U$7YA6Ie z{rw|YH}}4L^zm=^?mjux=x$xve*4b%M}xDkUBC0`hyVP}dp{a)4wC~?>hZzjNBj4) zYC6~$JFtK~OC^99LZ7H|0356mj0qwsDPg%bzxnKc|Lx;X_8uPxP|oxN6+gMZ_fP-x z)nC8=S8v>W^@|Mt{p)-CuB*vSisAm@>9P%1uV0)uEl9GYd)sA<6fqKb6%%?HLuF%z z6j!U|Pk;8azxmr=eKiYl^Mcr(6gwN$?iIa#V?Wjlcholr-+Sw=2dhwBx=IQtx|eT! z=j;8ojWMU5lZydGp}lAr1SQuiR4ycsB0|)d9zWhUHr>AUdfj){FJ+eZrfAn3d^IS_ z!=s1GX@2~9e0cwmchBE^>E_A9M?)bmoI4AcNTe(PBDvHX>WI*X5E3w9BwQ+PP|kPWur2)iT z`^Y8;C)+aKPQHBBPI-WsLBiO( zE|02+L#lly!Dgv;kT24XtjWy<{HUI22U-UlvU)CZwfp*&a_a?%Eao*QM(bW2+g7x ziA<7@wugqb(9W+3$%nReDe_dnwP^EucRzSx_dmcMyq%}yE?l~}d;a9p|L9)-;al0LeCNG) z2h-`o*%ou91{yVbvccKj#Y|z8DiDJYM7VY}syuW+5EM7rz=&#)x{LdVnVt+Ad8EoX zR#~O;$wU@YdEupR-MIZd9*$Q1X&Y)$P5OSIjLOhyu|Q|MpI~GW0(luQsN4)W2qF6@ z*50h2KX?AyH(r>G)Hi#NggWQU1~rrA7tODK{x84&;KM(A`zN>mm6wYg$-$^y4{aJ&H!+0`;MnJit(JCTQ6TW_TE=ZnFX6e@Ncl8?z#^5_|(7Q}=W z0!6?ZuiXj4*wD6pOfB90-QLsB@98t+PV}$7`9oQjHYRe%1F?WK=y9iuImLkBB7y@1 z(Q7>7L>{6bzz}Vq1jw8;dm~e4Y+F`YYuFn6=j8)szMi=Sk^#V5G8_;1P38(6lCL&Ffn*3P%{E@U|kH*R1C?W+`31v! Date: Tue, 21 Jan 2020 21:52:07 +0100 Subject: [PATCH 03/24] Fixed objects getting unhidden when doing any cats operation in 2.80+ --- README.md | 1 + tools/common.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index af8bbce4..71142f5c 100644 --- a/README.md +++ b/README.md @@ -300,6 +300,7 @@ It checks for a new version automatically once every day. #### 0.16.2 - Fixed export warning being empty +- Fixed objects getting unhidden when doing any cats operation in 2.80+ #### 0.16.1 - Fixed export warning bug diff --git a/tools/common.py b/tools/common.py index 821009a0..512773da 100644 --- a/tools/common.py +++ b/tools/common.py @@ -215,13 +215,13 @@ def hide(obj, val=True): if version_2_79_or_older(): obj.hide = val else: - obj.hide_viewport = val + obj.hide_set(val) def is_hidden(obj): if version_2_79_or_older(): return obj.hide - return obj.hide_viewport + return obj.hide_get() def set_unselectable(obj, val=True): From 114c706d077a0d1c66c012736b5988d541d8237f Mon Sep 17 00:00:00 2001 From: Hotox Date: Wed, 12 Feb 2020 02:29:58 +0100 Subject: [PATCH 04/24] Fixed importer error when a zip file contains another zip file --- resources/icons/supporters/CreedyFox.png | Bin 0 -> 5582 bytes resources/icons/supporters/NicoKuroKusagi.png | Bin 0 -> 3306 bytes resources/supporters.json | 6 ++++++ tools/importer.py | 7 +++++-- ui/armature.py | 2 ++ 5 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 resources/icons/supporters/CreedyFox.png create mode 100644 resources/icons/supporters/NicoKuroKusagi.png diff --git a/resources/icons/supporters/CreedyFox.png b/resources/icons/supporters/CreedyFox.png new file mode 100644 index 0000000000000000000000000000000000000000..e48238245feb5708d221bef59fd1da9e63c0b598 GIT binary patch literal 5582 zcmV;<6*20GP)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z000X4Nklx!29)9p)QmH2TPxtW~H{Zgq8Vd_~zWN8hk7?TIdVor?NU(Vs&0n||$#p%1 zETJkYs-jXgE11;^j^j`?im0l}Ixo!U_xvU;Ev;Opid8U}J3B~jW`=0A7QbI-~5T+Ct-xjD7`V7#jx zd+sWcKnR4uwoNXLT%cGi;MX)78EH>@gg@i!Y^(Ej#Cr>l+%cBG)i%_UAH$RI} zDIt6+j$h{NrD6QKpQ>q+s7;V77SLN7QDhGSb+t%7l}fpS&*x+B{{4La`%m-2zy6#= zZ7m1C{Uob9JCTJ%Bp$zE^{MtYn%h?*dife?a*L_JO2YmpJ? zi5RWnI9IM*X6tP`@H`J8#1aCY=P`QuGK!)QiA3=EeE<6!=JTvr(So9=$g%{khZ+u{ z*T#`so0<-qwuu&tV2n=_3WvG>zWZrSG;sH(ZDeyfMu&&FFfxJ)tX9vf`}pHWzRuv_AgvwkL}un#Zj_kKW>~)dR>ILJ zPkr}E@H`e4@+@1noN~F$Z-4gloajBy(RbcPQ5Bk+S8(8q524BmM^1i#91hVlbe7>a z-)6RIu&SejSzSF(ZuB+FznXI8fC-idl`>X(>NFn~W8AYNZbCX;61{1EGI-iRv%iYzY@FpL64!(d`!oVJziB$G=k z!t)rta)tJeRVa$GD8Z>FR8^(_^eF;?Aa!;1)Fu*0lCZO))ZZk)go>WJN}jWI~}3 zLI^6AGLj^-W$QN9ba!)hc!-kcqG*92#lixmVgb){Idb?g+qdtaF_pR@hmT9_$3wn0 zKdGiBJkO(4GB6ASvuYxQARKO{CK6$A;G;#dZyly2C6UTTkw7p+I1)us6iVd^hYmf* zLOy>zc{)AMnf}ul#p18DfCwQ721C@>CrLFm(U?k66NykP6bOZDh(x2j-*b%FYzBEv zSJ%P9LJr%u&;vmt(KzE1<4jGC(Y<~He!m}0(>dPr0lhsRvOJZ-G);2ZEY(T{)3T6d z2}zO=LNGlwi7d&eH|Uh@IH;<^F|B+2}b$rX!5G)-sKs?`h*oFg0#6NyCGd*?pV^YfhP z?`PlLcazWOh(uz$(Yt!%IL- z)8tJ5X@2(8Lktdl#J;;f#b15v3F7fu0Dkl@|475eE+ioYk`Q>Fhb+sKO9rZ{(AU?; z)Z`@H>(?_gGs81K_gOv}4zG7dKW}#xza*eht@g*zk#)(DeY@ckJZ( z=btAK4AR!tMq67u@pzosnHh$L2DozNGJ^x>7#bYlMDKAVN${mFe}$ITHaff3kjrIx zn&A9~xwKVv<#N?4&%OS#-l{xs2mFlu8DEO~Z9vWJP6SVx0Dl4s=~76bkcS zN8e-k{CS>w`fmYv@!x;Jsgoyg92?8BaU7eycihEiKlcC)%a-wzXP@E3@gClL?g$u_=F_OUM)vY35+2yLg=Ly}u7g0}^ZT)Ei){8P03972SZ0MnK1*wB>$O2G zVA~EppASir2nK`v`ClI7htD43_FcQ_J>ElOD#fbRomi%Yrt8$z*Hf)lKnOh7r7$^- zWZM?DZR2?^p_&NtHI4UNcX5LSnwpzQ&rNe>WSD3)N+y$LVPS!MK8tCZtnOUHj}QHf zhaUbi0B^qjDy?m8-1F&s={t1-*LAMVSD8YgfGo>6Rg>yM0mZUR%B3QHO-B+k2(T>+ zzh5U93^6}H$ICChNVQtUuW3Bg^7=A*=qEp7>$dG| z+PsDL-aX3L=qR!*10JDJ2*W5MB!OdDxTb{!f|6mN>4EE};n#FzS>nYP{*4DdcRzpg zz3(DP5{X2Da=A<{n`LC=0+J+g=f3@f!(obr0*`&;>wIG4O>Ek{g|lb+`Ljp9hT}M> zs*3A+Xu5vQ$OLYsis!m0#f2P^SR75$DHif*+Tx-4*=b5ffl{eNAP_*)G=x}mK}nJb zhr@WD$9JCiCXJ1ajE|3V=f3^y+P#;vXZrc0uYQF>p@4nuKInNKl}Z_(&rhXP!pL03 cb6x&>0A423Y_}ko*8l(j07*qoM6N<$f_WpidH?_b literal 0 HcmV?d00001 diff --git a/resources/icons/supporters/NicoKuroKusagi.png b/resources/icons/supporters/NicoKuroKusagi.png new file mode 100644 index 0000000000000000000000000000000000000000..8e3e02e68f19fa1330e172461565ef45a2dc5672 GIT binary patch literal 3306 zcmVEWZ_f;y-#hQ0SML4Z`@P?LuTvI(Xz zP*fWkS*0kFjK>n=-IpJu<&!f69nD;Q!=?0`Y{Q5IIX57M850Ec-%+16>>&*n=T7Y^s9Y26Y9sRH#)L+2@7xRK&H8SMM?NtS2a!NdFR=bn|z zIeIokPDUMzO0MP1*g^Vz?WnSZZUkv4tfn@;n5eQ1S(3=g zEu_Sw@Yb6z;I67A5r`A@DGarLOUM@`91LP;N%}?tI2;1TB9!J-v#ae5M*Jhp%dKI@ zCp-D{k#G6xhux@>!bqSUSqbv!y$^Hp#2{hQNic3>@8M^WN?r4X&1Od+W12~Fb91?M z^%A!J_IV_miX!VoN5k}=>|t7ulZ?$J5(?oPA4Ag&G~FP_IY!s%QAWe(aCtmb&0Wqw ze;0NIT8BQQF~5MV+jr4(-MO1{vyEiTaWnjV53Xz%R;!Jm&xhTnBJ>yy%kt?tc9Oxf-?6M_9Tr<5 zj>2vh-unQ3`}>LLf`eb2#$gRH>zDc9zzampWC}pNP-GR0BI9vDU2!IV{OBuY`-g~y z4Z7OCBd=yD3)dBM;Jp#7s>1m2FtfpFsv8@a@p_5HLRclp%`nL?P;usEQ`}U`4@X*& z)M5s|>BAqHq_m=%eFs~K2?vKyy+&bX4N`UXLSdRZZl{}Em%_(?{|B<7GBq&H^zby( z$x;@r&Bx}JaOcS!|7(b@mgCf4b}_!u5fb4D4v&X$C`>%y2VoKpd68v1(P)w!cOi+y zAajaKnVj}A8E_KO5>)7oyl`|Y(gnGTg`~*XR4X2b!khoIi~JHN9WCb>8?vU#6T91n zDUxJ+T#OENbL($b@WrcL1Oq|J>MvxlqZ?OF2I1)-p~x&+L?aTNB`##@OA|CS*I~)X z!8arLeD7(zvwkY2xxCu)bENu$CLt6BSqN;3#DD$b4`k#P(0eM1E!Tx9t7++36*H~p zMeC|4U!)N8ne=^e6bhO#gaM|6@BB0a9i8NsmDAnXfn9Q7iZJ&-cRglY=geovXg?dr z^t$j{e&GEhZy?nbGzlabtE`~wF&_BW?d0b*67m^XUG{Vb8|v$ztY$vvhx=K4*(&s8 zl4xLt`3*_xezFifnKB`9zDr~8pZ1YGr-c5VPV6=hOP1GBUK*vOu8z(x+cqOuAkk=jCYDx-}(y!KYY)u7GgH+ zMQSLxNT_KknaGM2Ns_tu>K!<8var}y&bA+*ap^iD1MiYqe+ODTNpP%}LdP(3)@>#* zX{1}XkqW^~GT1c$ib^yR$LY#JOA4l^yew@rIMQ;Q1MQt?W{j~wABJQiEy!CeQku3{ z9Hfqr;i2pQOgtU|Nu#^3g?vW2^^u!6aU_#aAjp#PK9aErfx@dvBuy+9D~YH^aMFum z2qe{ttlBZ+Cb5Ku&o{xUt)xzHwee4NNS0cV33;1RthCGtRxi_88aT{->-cG zQiY|;^bH*1M%NgxtWVjJvKz!AfAtm!>pD zhi>RZ!vW5;@8dh~NkR#Ks^ZemrHjkf2t|=et4&MdaaFKl!L=0UmE+0Fp)s$Gyygbp zdu<1iSeV1#c5wF%53q9aVvZj>hPCKsLNhb;bf4j_yQ}!*!w`8Dbxe#5;dbVs34=s5 zjz8w7we2I$1^b95f+VygCP}1A=Uy&SMWH35IPIBCg#z5V<}r%1%gD*irg^~#hFf8D zypx*C7t^xm6dSfZjNzT)li&XZCFe@c4W7n&{$qN*7D_7`a8;}#5f0(@<|e(N`@mRm4VelQPh-WG%dzVY?{j(?w}&Sf)$sTRAhOWv-%>05TrM&lyg~1FEH^y z1l46nwyNyf@gVV}h7{k-k)EA+63r}HxtXb{a|9zn+@3Ral&UF5ro{2U>aX%JW!IWgARr9V$nFG@x zrYCTy4k`+kQkFZ9ruj~;`Nh>Jshvrfsjq(Q6)A}d6@kEr#nXKNSs)~Z^WSvfDxb?M zPoF?iUChS(*rE`1%be}rk2f+*LXY5&1k>|JT3xeV2wewR#-n=DRCuGqtg5|}x{7lC z?Wq-{a-VXR{y_+g|KD=c88J-I^fa(kAi^|w<=3Z(nu1^`fTsJYw%5}?)yqJjhmNts zl;zgaKi-Zk%P9a?q@btg_*K=LNa_h*JN_#F(zKQ4C7ZbAv7&T)P9M{t^Ybq#TXGSe zoGc_uiWV7J*AXcoLNFVe<ZL47T#h+}BNME?rkKO;?`I-1&fUv@&o}4VdF%8m*sUJ29N9>#s@95lB2J-m z4p&@o6?=Ldgs|)==M_jhKA6! zEaHg?n(Ior<;5)=zwdKO>eq44%j*dx1zEPr^ncR)tofqIQ^xN${Wrh-Y#XXMZl{c(U2>{00mmSXh(8BRAcL__~OetS@Q2=rV#6ek7ZdXe2=_rg8XaD?{F4u3fhQ z!!R+TA;w3J^7wntliEE@6JqDP*|y~tq_x#+#B4Oghj)I(&`d88EyRw4zow|5gp#ac zipr~4wyc(4KKm4kGZ!f#2qZ#iMm){obKAD#E-WB49Y(Qc@c17dWbNW>XzOjIcHTnj z$`_G{M3EJpte{5Iwb|rWmSJ~VF?1d2?ae#IveLDj9y`MB?)TEGS&m`Ht0u|MEy9Ay zZCh_=?+2eS69(VRBtzppI2}$@prWLR4OiXB?$16Z7>W{)htY(IDKuo&LQ!rx8!!DS z0pB!Mr;XBRHs6mQBrNx_X3b`#7q5C)coVa1T(X57`+vjs^)K+m=TGCbTFK2TLKrb} zitQ-mF?QaEe>O-o7C_emxUq8UowxJtH-ik1kKhl7)APU|_L0=0m{^cgi;gUzTJ7An oZYzD=9n@zxFdZGDb?hMj2OOm* Date: Thu, 16 Apr 2020 02:53:04 +0200 Subject: [PATCH 05/24] Added 2.83 compatibility, fixed incorrect zip importer error --- README.md | 11 +++++-- __init__.py | 4 ++- resources/icons/supporters/Kally.png | Bin 0 -> 2555 bytes resources/supporters.json | 46 +++++++++++++++++---------- tools/importer.py | 40 ++++++++++++----------- 5 files changed, 62 insertions(+), 39 deletions(-) create mode 100644 resources/icons/supporters/Kally.png diff --git a/README.md b/README.md index 71142f5c..9d23bc0b 100644 --- a/README.md +++ b/README.md @@ -35,13 +35,15 @@ Join our Discord to report errors, suggestions and make comments! **Discord: https://discord.gg/f8yZGnv** ## Requirements - - Blender **2.79** or **2.80** or **2.81** (run as administrator is recommended) - - mmd_tools is **no longer required**! Cats comes pre-installed with it! + - Blender **2.79** or **2.80** or above (run as administrator is recommended) + - mmd_tools is **not required**! Cats comes pre-installed with it! - If you have custom Python installed which Blender might use, you need to have Numpy installed ## Installation - Download the plugin: [Cats Blender Plugin](https://github.com/michaeldegroot/cats-blender-plugin/archive/master.zip) - - Install the the addon in blender like so: + - **Important: Do NOT extract the downloaded zip!** + - Install the addon in blender like so: + - *This shows Blender 2.79. In Blender 2.80+ go to Edit > Preferences > Add-ons. Also you don't need to save the user settings there* ![](https://i.imgur.com/eZV1zrs.gif) @@ -299,7 +301,10 @@ It checks for a new version automatically once every day. ## Changelog #### 0.16.2 +- **Cats is now fully compatible with Blender 2.83!** + - It was compatible with 2.82 all long - Fixed export warning being empty +- Fixed importer error when a zip file contains another zip file - Fixed objects getting unhidden when doing any cats operation in 2.80+ #### 0.16.1 diff --git a/__init__.py b/__init__.py index aa1c6711..80c71a9e 100644 --- a/__init__.py +++ b/__init__.py @@ -318,8 +318,10 @@ def register(): # Set preferred Blender options if hasattr(tools.common.get_user_preferences(), 'system') and hasattr(tools.common.get_user_preferences().system, 'use_international_fonts'): tools.common.get_user_preferences().system.use_international_fonts = True - else: + elif hasattr(tools.common.get_user_preferences(), 'view') and hasattr(tools.common.get_user_preferences().view, 'use_international_fonts'): tools.common.get_user_preferences().view.use_international_fonts = True + else: + pass # From 2.83 on this is no longer needed tools.common.get_user_preferences().filepaths.use_file_compression = True bpy.context.window_manager.addon_support = {'OFFICIAL', 'COMMUNITY', 'TESTING'} diff --git a/resources/icons/supporters/Kally.png b/resources/icons/supporters/Kally.png new file mode 100644 index 0000000000000000000000000000000000000000..4b011a642407e14dafa26a62eaf1cb30915a409f GIT binary patch literal 2555 zcmVFc83h3iJi~96=r-fBFCg;vYd#q=oA?KvAbQI=QKb zNVX)3QYea-$8hd zj3VIudG~jpeC89qdil~TS1+=1ZGT)arKq}8MaT!q7G{B`$A*(dibhaCR4FQOHU>wE zgBZuyEuQ`Be|`GZ-qX}6KQm{}tZ&}>!^!sU!AbMWzyE06`MO=d{ii>8hw72T1`i~ro`61g%O91&U$7YA6Ie z{rw|YH}}4L^zm=^?mjux=x$xve*4b%M}xDkUBC0`hyVP}dp{a)4wC~?>hZzjNBj4) zYC6~$JFtK~OC^99LZ7H|0356mj0qwsDPg%bzxnKc|Lx;X_8uPxP|oxN6+gMZ_fP-x z)nC8=S8v>W^@|Mt{p)-CuB*vSisAm@>9P%1uV0)uEl9GYd)sA<6fqKb6%%?HLuF%z z6j!U|Pk;8azxmr=eKiYl^Mcr(6gwN$?iIa#V?Wjlcholr-+Sw=2dhwBx=IQtx|eT! z=j;8ojWMU5lZydGp}lAr1SQuiR4ycsB0|)d9zWhUHr>AUdfj){FJ+eZrfAn3d^IS_ z!=s1GX@2~9e0cwmchBE^>E_A9M?)bmoI4AcNTe(PBDvHX>WI*X5E3w9BwQ+PP|kPWur2)iT z`^Y8;C)+aKPQHBBPI-WsLBiO( zE|02+L#lly!Dgv;kT24XtjWy<{HUI22U-UlvU)CZwfp*&a_a?%Eao*QM(bW2+g7x ziA<7@wugqb(9W+3$%nReDe_dnwP^EucRzSx_dmcMyq%}yE?l~}d;a9p|L9)-;al0LeCNG) z2h-`o*%ou91{yVbvccKj#Y|z8DiDJYM7VY}syuW+5EM7rz=&#)x{LdVnVt+Ad8EoX zR#~O;$wU@YdEupR-MIZd9*$Q1X&Y)$P5OSIjLOhyu|Q|MpI~GW0(luQsN4)W2qF6@ z*50h2KX?AyH(r>G)Hi#NggWQU1~rrA7tODK{x84&;KM(A`zN>mm6wYg$-$^y4{aJ&H!+0`;MnJit(JCTQ6TW_TE=ZnFX6e@Ncl8?z#^5_|(7Q}=W z0!6?ZuiXj4*wD6pOfB90-QLsB@98t+PV}$7`9oQjHYRe%1F?WK=y9iuImLkBB7y@1 z(Q7>7L>{6bzz}Vq1jw8;dm~e4Y+F`YYuFn6=j8)szMi=Sk^#V5G8_;1P38(6lCL&Ffn*3P%{E@U|kH*R1C?W+`31v! Date: Thu, 16 Apr 2020 05:17:53 +0200 Subject: [PATCH 06/24] Fixed popup error in 2.83, fixed importer error when the fbx importer is not enabled --- README.md | 5 +++-- tools/armature_manual.py | 2 +- tools/atlas.py | 2 +- tools/importer.py | 26 +++++++++++++++++++------- ui/armature.py | 2 +- updater.py | 8 ++++---- 6 files changed, 29 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 9d23bc0b..a6c1f735 100644 --- a/README.md +++ b/README.md @@ -302,9 +302,10 @@ It checks for a new version automatically once every day. #### 0.16.2 - **Cats is now fully compatible with Blender 2.83!** - - It was compatible with 2.82 all long + - *It was compatible with 2.82 all long* - Fixed export warning being empty -- Fixed importer error when a zip file contains another zip file +- Fixed importer error when the FBX importer was not enabled +- Fixed importer error when a zip file contained another zip file - Fixed objects getting unhidden when doing any cats operation in 2.80+ #### 0.16.1 diff --git a/tools/armature_manual.py b/tools/armature_manual.py index 83847028..b0046087 100644 --- a/tools/armature_manual.py +++ b/tools/armature_manual.py @@ -220,7 +220,7 @@ def execute(self, context): def invoke(self, context, event): context.scene.pose_to_shapekey_name = 'Pose' dpi_value = Common.get_user_preferences().system.dpi - return context.window_manager.invoke_props_dialog(self, width=dpi_value * 4, height=-550) + return context.window_manager.invoke_props_dialog(self, width=dpi_value * 4) def check(self, context): # Important for changing options diff --git a/tools/atlas.py b/tools/atlas.py index 8e18344b..80124867 100644 --- a/tools/atlas.py +++ b/tools/atlas.py @@ -399,7 +399,7 @@ def execute(self, context): def invoke(self, context, event): dpi_value = Common.get_user_preferences().system.dpi - return context.window_manager.invoke_props_dialog(self, width=dpi_value * 5.3, height=-550) + return context.window_manager.invoke_props_dialog(self, width=dpi_value * 5.3) def check(self, context): # Important for changing options diff --git a/tools/importer.py b/tools/importer.py index cfe33d77..be8e993e 100644 --- a/tools/importer.py +++ b/tools/importer.py @@ -28,6 +28,7 @@ import copy import zipfile import webbrowser +import addon_utils import bpy_extras.io_utils from .. import globs @@ -192,6 +193,12 @@ def import_file(directory, file_name): # FBX elif file_ending == 'fbx': + + # Enable fbx if it isn't enabled yet + fbx_is_enabled = addon_utils.check('io_scene_fbx')[1] + if not fbx_is_enabled: + addon_utils.enable('io_scene_fbx') + try: bpy.ops.import_scene.fbx('EXEC_DEFAULT', filepath=file_path, @@ -309,7 +316,7 @@ def execute(self, context): def invoke(self, context, event): dpi_value = Common.get_user_preferences().system.dpi - return context.window_manager.invoke_props_dialog(self, width=dpi_value * 6, height=-550) + return context.window_manager.invoke_props_dialog(self, width=dpi_value * 6) def check(self, context): # Important for changing options @@ -376,7 +383,7 @@ def execute(self, context): def invoke(self, context, event): dpi_value = Common.get_user_preferences().system.dpi - return context.window_manager.invoke_props_dialog(self, width=dpi_value * 3, height=-550) + return context.window_manager.invoke_props_dialog(self, width=dpi_value * 3) def check(self, context): # Important for changing options @@ -491,6 +498,11 @@ def execute(self, context): if hasattr(context.scene, 'layers'): context.scene.layers[0] = True + # Enable fbx if it isn't enabled yet + fbx_is_enabled = addon_utils.check('io_scene_fbx')[1] + if not fbx_is_enabled: + addon_utils.enable('io_scene_fbx') + try: bpy.ops.import_scene.fbx('INVOKE_DEFAULT', automatic_bone_orientation=False, @@ -535,7 +547,7 @@ def execute(self, context): def invoke(self, context, event): dpi_value = Common.get_user_preferences().system.dpi - return context.window_manager.invoke_props_dialog(self, width=dpi_value * 4.5, height=-550) + return context.window_manager.invoke_props_dialog(self, width=dpi_value * 4.5) def check(self, context): # Important for changing options @@ -572,7 +584,7 @@ def execute(self, context): def invoke(self, context, event): dpi_value = Common.get_user_preferences().system.dpi - return context.window_manager.invoke_props_dialog(self, width=dpi_value * 4.5, height=-550) + return context.window_manager.invoke_props_dialog(self, width=dpi_value * 4.5) def check(self, context): # Important for changing options @@ -609,7 +621,7 @@ def execute(self, context): def invoke(self, context, event): dpi_value = Common.get_user_preferences().system.dpi - return context.window_manager.invoke_props_dialog(self, width=dpi_value * 4.5, height=-550) + return context.window_manager.invoke_props_dialog(self, width=dpi_value * 4.5) def check(self, context): # Important for changing options @@ -645,7 +657,7 @@ def execute(self, context): def invoke(self, context, event): dpi_value = Common.get_user_preferences().system.dpi - return context.window_manager.invoke_props_dialog(self, width=dpi_value * 4, height=-550) + return context.window_manager.invoke_props_dialog(self, width=dpi_value * 4) def check(self, context): # Important for changing options @@ -931,7 +943,7 @@ def invoke(self, context, event): self.eye_meshes_not_named_body = _eye_meshes_not_named_body dpi_value = Common.get_user_preferences().system.dpi - return context.window_manager.invoke_props_dialog(self, width=dpi_value * 6.1, height=-550) + return context.window_manager.invoke_props_dialog(self, width=dpi_value * 6.1) def check(self, context): # Important for changing options diff --git a/ui/armature.py b/ui/armature.py index be13922f..4b6c749b 100644 --- a/ui/armature.py +++ b/ui/armature.py @@ -191,7 +191,7 @@ def execute(self, context): def invoke(self, context, event): dpi_value = Common.get_user_preferences().system.dpi - return context.window_manager.invoke_props_dialog(self, width=dpi_value * 3.25, height=-550) + return context.window_manager.invoke_props_dialog(self, width=dpi_value * 3.25) def check(self, context): # Important for changing options diff --git a/updater.py b/updater.py index cfb6f707..bb07852f 100644 --- a/updater.py +++ b/updater.py @@ -165,7 +165,7 @@ def invoke(self, context, event): global used_updater_panel used_updater_panel = True dpi_value = get_user_preferences().system.dpi - return context.window_manager.invoke_props_dialog(self, width=dpi_value * 8.2, height=-550) + return context.window_manager.invoke_props_dialog(self, width=dpi_value * 8.2) def check(self, context): # Important for changing options @@ -214,7 +214,7 @@ def execute(self, context): def invoke(self, context, event): dpi_value = get_user_preferences().system.dpi - return context.window_manager.invoke_props_dialog(self, width=dpi_value * 4.1, height=-550) + return context.window_manager.invoke_props_dialog(self, width=dpi_value * 4.1) def check(self, context): # Important for changing options @@ -278,7 +278,7 @@ def execute(self, context): def invoke(self, context, event): dpi_value = get_user_preferences().system.dpi - return context.window_manager.invoke_props_dialog(self, width=dpi_value * 4.1, height=-550) + return context.window_manager.invoke_props_dialog(self, width=dpi_value * 4.1) def check(self, context): # Important for changing options @@ -327,7 +327,7 @@ def execute(self, context): def invoke(self, context, event): dpi_value = get_user_preferences().system.dpi - return context.window_manager.invoke_props_dialog(self, width=dpi_value * 4.6, height=-550) + return context.window_manager.invoke_props_dialog(self, width=dpi_value * 4.6) # def invoke(self, context, event): # return context.window_manager.invoke_props_dialog(self) From 19cfff8088ab7292b170b85cfd8fd11dbe82da84 Mon Sep 17 00:00:00 2001 From: Hotox Date: Thu, 16 Apr 2020 06:09:54 +0200 Subject: [PATCH 07/24] Added option to Fix Model to keep twist bones (experimental) --- README.md | 38 ++++++++++---------------------------- extentions.py | 7 +++++++ tools/armature.py | 9 +++++++++ tools/common.py | 5 +++++ ui/armature.py | 2 ++ 5 files changed, 33 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index a6c1f735..e87c092d 100644 --- a/README.md +++ b/README.md @@ -303,10 +303,16 @@ It checks for a new version automatically once every day. #### 0.16.2 - **Cats is now fully compatible with Blender 2.83!** - *It was compatible with 2.82 all long* -- Fixed export warning being empty -- Fixed importer error when the FBX importer was not enabled -- Fixed importer error when a zip file contained another zip file -- Fixed objects getting unhidden when doing any cats operation in 2.80+ +- **Fix Model:** + - Added "Keep Twist Bones" option to Fix Model (experimental) + - Added compatibility to more models +- **Importer:** + - Fixed export warning being empty + - Fixed importer error when the FBX importer was not enabled + - Fixed importer error when a zip file contained another zip file + - When importing a model, objects of a new scene now only get deleted if all three of them are present +- **General:** + - Fixed objects getting unhidden when doing any cats operation in 2.80+ #### 0.16.1 - Fixed export warning bug @@ -344,30 +350,6 @@ It checks for a new version automatically once every day. - Small bug fixes - Updated mmd_tools -#### 0.15.0 -- **Importer**: - - FBX no longer imports animations and poses by default -- **Fix Model**: - - Now always applies transforms of the model - - Added "Keep Upper Chest" option - - Warning: Currently having an Upper Chest breaks Eye Tracking, so don't use this if you want Eye Tracking - - Removed "Fix Full Body Tracking" option - - It is no longer needed for VRChat - - The button to add/remove the fix is still available in Model Options - - Improved Hips placement as recommended by VRChat - - Legs are now getting bend forward very slightly if they are completely straight - - Fixed a bug which could sometimes delete bones unintentionally -- **Model**: - - Fixed pose mode error in 2.80 -- **Model Options**: - - Added new "Delete Zero Weight Vertex Groups" button - - Improved layout of the "Full Body Tracking Fix" buttons - - Fixed visual "Merge Weights" bug in Blender 2.80 -- **Optimization**: - - Improved Material Combiner detection algorithm -- **General**: - - Updated mmd_tools - Read the full changelog [here](https://github.com/michaeldegroot/cats-blender-plugin/releases). diff --git a/extentions.py b/extentions.py index d5e3de6a..ea2d50d3 100644 --- a/extentions.py +++ b/extentions.py @@ -55,6 +55,13 @@ def register(): default=False ) + Scene.keep_twist_bones = BoolProperty( + name='Keep Twist Bones (Experimental)', + description="Saves twist bones from deletion. Currently only works on MMD models." + '\n\nVRChat can now make use of twist bones, so you can use this option to keep them', + default=False + ) + Scene.join_meshes = BoolProperty( name='Join Meshes', description='Joins all meshes of this model together.' diff --git a/tools/armature.py b/tools/armature.py index 828380a3..05150e21 100644 --- a/tools/armature.py +++ b/tools/armature.py @@ -943,6 +943,9 @@ def add_eye_children(eye_bone, parent_name): if not bone_child or not bone_parent: continue + if context.scene.keep_twist_bones and 'twist' in bone_child.name.lower(): + continue + # search for next parent that is not in the "reweight to parent" list parent_in_list = True while parent_in_list: @@ -1015,6 +1018,9 @@ def add_eye_children(eye_bone, parent_name): print('BUG: ' + bone[0] + ' tried to mix weights with itself!') continue + if context.scene.keep_twist_bones and 'twist' in bone[1].lower(): + continue + # print(bone[1] + " to1 " + bone[0]) # If important vertex group is not there create it @@ -1069,6 +1075,9 @@ def add_eye_children(eye_bone, parent_name): if not vg_to: continue + if context.scene.keep_twist_bones and 'twist' in vg_from.name.lower(): + continue + bone_tmp = armature.data.bones.get(vg_from.name) if bone_tmp: for child in bone_tmp.children: diff --git a/tools/common.py b/tools/common.py index 512773da..c42e2a47 100644 --- a/tools/common.py +++ b/tools/common.py @@ -1417,11 +1417,16 @@ def delete_zero_weight(armature_name=None, ignore=''): def remove_unused_objects(): + default_scene_objects = [] for obj in get_objects(): if (obj.type == 'CAMERA' and obj.name == 'Camera') \ or (obj.type == 'LAMP' and obj.name == 'Lamp') \ or (obj.type == 'LIGHT' and obj.name == 'Light') \ or (obj.type == 'MESH' and obj.name == 'Cube'): + default_scene_objects.append(obj) + + if len(default_scene_objects) == 3: + for obj in default_scene_objects: delete_hierarchy(obj) diff --git a/ui/armature.py b/ui/armature.py index 4b6c749b..c2ef8ead 100644 --- a/ui/armature.py +++ b/ui/armature.py @@ -207,6 +207,8 @@ def draw(self, context): row = col.row(align=True) row.prop(context.scene, 'keep_upper_chest') row = col.row(align=True) + row.prop(context.scene, 'keep_twist_bones') + row = col.row(align=True) row.prop(context.scene, 'join_meshes') row = col.row(align=True) row.prop(context.scene, 'connect_bones') From 05b93cf77770aa161d838d93f7c99581128abc93 Mon Sep 17 00:00:00 2001 From: Hotox Date: Thu, 16 Apr 2020 06:14:45 +0200 Subject: [PATCH 08/24] Updated mmd_tools --- README.md | 1 + extern_tools/mmd_tools_local/core/material.py | 156 +++++------- extern_tools/mmd_tools_local/core/model.py | 27 +-- extern_tools/mmd_tools_local/core/morph.py | 5 +- .../mmd_tools_local/core/pmd/__init__.py | 17 +- .../mmd_tools_local/core/pmd/importer.py | 4 +- .../mmd_tools_local/core/pmx/__init__.py | 26 +- .../mmd_tools_local/core/pmx/exporter.py | 51 ++-- .../mmd_tools_local/core/pmx/importer.py | 67 ++++-- extern_tools/mmd_tools_local/core/shader.py | 17 +- .../mmd_tools_local/core/vmd/__init__.py | 2 - .../mmd_tools_local/core/vmd/importer.py | 6 + .../mmd_tools_local/cycles_converter.py | 225 ++++++++++++------ .../mmd_tools_local/operators/material.py | 97 ++++---- .../mmd_tools_local/operators/misc.py | 36 ++- .../mmd_tools_local/panels/prop_bone.py | 30 ++- .../mmd_tools_local/panels/prop_material.py | 8 +- extern_tools/mmd_tools_local/panels/tool.py | 7 +- .../mmd_tools_local/panels/util_tools.py | 15 +- .../mmd_tools_local/panels/view_prop.py | 22 ++ .../mmd_tools_local/properties/bone.py | 41 ++-- .../mmd_tools_local/properties/material.py | 7 +- .../mmd_tools_local/properties/root.py | 62 ++++- 23 files changed, 576 insertions(+), 353 deletions(-) diff --git a/README.md b/README.md index e87c092d..f7f480a6 100644 --- a/README.md +++ b/README.md @@ -313,6 +313,7 @@ It checks for a new version automatically once every day. - When importing a model, objects of a new scene now only get deleted if all three of them are present - **General:** - Fixed objects getting unhidden when doing any cats operation in 2.80+ + - Updated mmd_tools #### 0.16.1 - Fixed export warning bug diff --git a/extern_tools/mmd_tools_local/core/material.py b/extern_tools/mmd_tools_local/core/material.py index 153cc98f..089e3bdb 100644 --- a/extern_tools/mmd_tools_local/core/material.py +++ b/extern_tools/mmd_tools_local/core/material.py @@ -6,6 +6,7 @@ import bpy from mmd_tools_local.bpyutils import addon_preferences, select_object from mmd_tools_local.core.exceptions import MaterialNotFoundError +from mmd_tools_local.core.shader import _NodeGroupUtils SPHERE_MODE_OFF = 0 SPHERE_MODE_MULT = 1 @@ -387,6 +388,7 @@ def convert_to_mmd_material(material): diffuse = m.diffuse_color*min(1.0, m.diffuse_intensity/0.8) if use_diffuse else (1.0, 1.0, 1.0) mmd_material.diffuse_color = diffuse + cast_shadows = getattr(m, 'cast_shadows', m.use_cast_buffer_shadows) map_alpha = next((s.blend_type for s in m.texture_slots if s and s.use_map_alpha), None) if m.use_transparency and map_alpha in {None, 'MULTIPLY'}: mmd_material.alpha = m.alpha @@ -394,7 +396,7 @@ def convert_to_mmd_material(material): mmd_material.specular_color = m.specular_color*min(1.0, m.specular_intensity/0.8) mmd_material.shininess = m.specular_hardness mmd_material.is_double_sided = m.game_settings.use_backface_culling - mmd_material.enabled_self_shadow_map = m.use_cast_buffer_shadows and m.alpha > 1e-3 + mmd_material.enabled_self_shadow_map = cast_shadows and m.alpha > 1e-3 mmd_material.enabled_self_shadow = m.use_shadows @@ -541,6 +543,7 @@ def update_alpha(self): elif len(mat.diffuse_color) > 3: mat.diffuse_color[3] = mmd_mat.alpha self.__update_shader_input('Alpha', mmd_mat.alpha) + self.update_self_shadow_map() def update_specular_color(self): mat = self.material @@ -588,6 +591,8 @@ def convert_to_mmd_material(material): if tex_node: tex_node.name = 'mmd_base_tex' + shadow_method = getattr(m, 'shadow_method', None) + mmd_material.diffuse_color = m.diffuse_color[:3] if hasattr(m, 'alpha'): mmd_material.alpha = m.alpha @@ -605,9 +610,9 @@ def convert_to_mmd_material(material): elif hasattr(m, 'use_backface_culling'): mmd_material.is_double_sided = not m.use_backface_culling - if hasattr(m, 'shadow_method'): - mmd_material.enabled_self_shadow_map = (m.shadow_method != 'NONE') and mmd_material.alpha > 1e-3 - mmd_material.enabled_self_shadow = (m.shadow_method != 'NONE') + if shadow_method: + mmd_material.enabled_self_shadow_map = (shadow_method != 'NONE') and mmd_material.alpha > 1e-3 + mmd_material.enabled_self_shadow = (shadow_method != 'NONE') def __update_shader_input(self, name, val): @@ -645,6 +650,7 @@ class _Dummy: default_value, is_linked = None, True node_shader.inputs.get('Alpha', _Dummy).default_value = mmd_mat.alpha node_shader.inputs.get('Double Sided', _Dummy).default_value = mmd_mat.is_double_sided node_shader.inputs.get('Self Shadow', _Dummy).default_value = mmd_mat.enabled_self_shadow + self.update_sphere_texture_type() node_uv = nodes.get('mmd_tex_uv', None) if node_uv is None: @@ -678,35 +684,26 @@ def __get_shader_uv(self): if len(shader.nodes): return shader - nodes, links = shader.nodes, shader.links - - def __new_node(idname, pos): - node = nodes.new(idname) - node.location = (pos[0]*210, pos[1]*220) - return node - - def __new_io(shader_io, io_sockets, io_name, socket): - links.new(io_sockets[-1], socket) - shader_io[-1].name = io_name + ng = _NodeGroupUtils(shader) ############################################################################ - node_output = __new_node('NodeGroupOutput', (6, 0)) + node_output = ng.new_node('NodeGroupOutput', (6, 0)) - tex_coord = __new_node('ShaderNodeTexCoord', (0, 0)) + tex_coord = ng.new_node('ShaderNodeTexCoord', (0, 0)) if hasattr(bpy.types, 'ShaderNodeUVMap'): - tex_coord1 = __new_node('ShaderNodeUVMap', (4, -2)) + tex_coord1 = ng.new_node('ShaderNodeUVMap', (4, -2)) tex_coord1.uv_map, socketUV1 = 'UV1', 'UV' else: - tex_coord1 = __new_node('ShaderNodeAttribute', (4, -2)) + tex_coord1 = ng.new_node('ShaderNodeAttribute', (4, -2)) tex_coord1.attribute_name, socketUV1 = 'UV1', 'Vector' - vec_trans = __new_node('ShaderNodeVectorTransform', (1, -1)) + vec_trans = ng.new_node('ShaderNodeVectorTransform', (1, -1)) vec_trans.vector_type = 'NORMAL' vec_trans.convert_from = 'OBJECT' vec_trans.convert_to = 'CAMERA' - node_vector = __new_node('ShaderNodeMapping', (2, -1)) + node_vector = ng.new_node('ShaderNodeMapping', (2, -1)) node_vector.vector_type = 'POINT' if bpy.app.version < (2, 81, 0): node_vector.translation = (0.5, 0.5, 0.0) @@ -715,13 +712,14 @@ def __new_io(shader_io, io_sockets, io_name, socket): node_vector.inputs['Location'].default_value = (0.5, 0.5, 0.0) node_vector.inputs['Scale'].default_value = (0.5, 0.5, 1.0) + links = ng.links links.new(tex_coord.outputs['Normal'], vec_trans.inputs['Vector']) links.new(vec_trans.outputs['Vector'], node_vector.inputs['Vector']) - __new_io(shader.outputs, node_output.inputs, 'Base UV', tex_coord.outputs['UV']) - __new_io(shader.outputs, node_output.inputs, 'Toon UV', node_vector.outputs['Vector']) - __new_io(shader.outputs, node_output.inputs, 'Sphere UV', node_vector.outputs['Vector']) - __new_io(shader.outputs, node_output.inputs, 'SubTex UV', tex_coord1.outputs[socketUV1]) + ng.new_output_socket('Base UV', tex_coord.outputs['UV']) + ng.new_output_socket('Toon UV', node_vector.outputs['Vector']) + ng.new_output_socket('Sphere UV', node_vector.outputs['Vector']) + ng.new_output_socket('SubTex UV', tex_coord1.outputs[socketUV1]) return shader @@ -731,64 +729,40 @@ def __get_shader(self): if len(shader.nodes): return shader - nodes, links = shader.nodes, shader.links - - def __new_node(idname, pos): - node = nodes.new(idname) - node.location = (pos[0]*210, pos[1]*220) - return node - - def __new_mix_node(blend_type, pos): - node = __new_node('ShaderNodeMixRGB', pos) - node.blend_type = blend_type - return node - - def __new_math_node(operation, pos): - node = __new_node('ShaderNodeMath', pos) - node.operation = operation - return node - - def __new_io(shader_io, io_sockets, io_name, socket, default_val=None, min_max=None): - links.new(io_sockets[-1], socket) - shader_io[-1].name = io_name - if default_val is not None: - shader_io[-1].default_value = default_val - if min_max is not None: - shader_io[-1].min_value, shader_io[-1].max_value = min_max + ng = _NodeGroupUtils(shader) ############################################################################ - node_input = __new_node('NodeGroupInput', (-5, -1)) - node_output = __new_node('NodeGroupOutput', (11, 1)) + node_input = ng.new_node('NodeGroupInput', (-5, -1)) + node_output = ng.new_node('NodeGroupOutput', (11, 1)) - node_diffuse = __new_mix_node('ADD', (-3, 4)) + node_diffuse = ng.new_mix_node('ADD', (-3, 4), fac=0.6) node_diffuse.use_clamp = True - node_diffuse.inputs['Fac'].default_value = 0.6 - - node_tex = __new_mix_node('MULTIPLY', (-2, 3.5)) - node_toon = __new_mix_node('MULTIPLY', (-1, 3)) - node_sph = __new_mix_node('MULTIPLY', (0, 2.5)) - node_spa = __new_mix_node('ADD', (0, 1.5)) - node_sphere = __new_mix_node('MIX', (1, 1)) - - node_geo = __new_node('ShaderNodeNewGeometry', (6, 3.5)) - node_invert = __new_math_node('LESS_THAN', (7, 3)) - node_cull = __new_math_node('MAXIMUM', (8, 2.5)) - node_alpha = __new_math_node('MINIMUM', (9, 2)) - node_alpha_tex = __new_math_node('MULTIPLY', (-1, -2)) - node_alpha_toon = __new_math_node('MULTIPLY', (0, -2.5)) - node_alpha_sph = __new_math_node('MULTIPLY', (1, -3)) - - node_reflect = __new_math_node('DIVIDE', (7, -1.5)) + + node_tex = ng.new_mix_node('MULTIPLY', (-2, 3.5)) + node_toon = ng.new_mix_node('MULTIPLY', (-1, 3)) + node_sph = ng.new_mix_node('MULTIPLY', (0, 2.5)) + node_spa = ng.new_mix_node('ADD', (0, 1.5)) + node_sphere = ng.new_mix_node('MIX', (1, 1)) + + node_geo = ng.new_node('ShaderNodeNewGeometry', (6, 3.5)) + node_invert = ng.new_math_node('LESS_THAN', (7, 3)) + node_cull = ng.new_math_node('MAXIMUM', (8, 2.5)) + node_alpha = ng.new_math_node('MINIMUM', (9, 2)) + node_alpha_tex = ng.new_math_node('MULTIPLY', (-1, -2)) + node_alpha_toon = ng.new_math_node('MULTIPLY', (0, -2.5)) + node_alpha_sph = ng.new_math_node('MULTIPLY', (1, -3)) + + node_reflect = ng.new_math_node('DIVIDE', (7, -1.5), value1=1) node_reflect.use_clamp = True - node_reflect.inputs[0].default_value = 1 - shader_diffuse = __new_node('ShaderNodeBsdfDiffuse', (8, 0)) - shader_glossy = __new_node('ShaderNodeBsdfGlossy', (8, -1)) - shader_base_mix = __new_node('ShaderNodeMixShader', (9, 0)) + shader_diffuse = ng.new_node('ShaderNodeBsdfDiffuse', (8, 0)) + shader_glossy = ng.new_node('ShaderNodeBsdfGlossy', (8, -1)) + shader_base_mix = ng.new_node('ShaderNodeMixShader', (9, 0)) shader_base_mix.inputs['Fac'].default_value = 0.02 - shader_trans = __new_node('ShaderNodeBsdfTransparent', (9, 1)) - shader_alpha_mix = __new_node('ShaderNodeMixShader', (10, 1)) + shader_trans = ng.new_node('ShaderNodeBsdfTransparent', (9, 1)) + shader_alpha_mix = ng.new_node('ShaderNodeMixShader', (10, 1)) + links = ng.links links.new(node_reflect.outputs['Value'], shader_glossy.inputs['Roughness']) links.new(shader_diffuse.outputs['BSDF'], shader_base_mix.inputs[1]) links.new(shader_glossy.outputs['BSDF'], shader_base_mix.inputs[2]) @@ -813,27 +787,27 @@ def __new_io(shader_io, io_sockets, io_name, socket, default_val=None, min_max=N links.new(shader_base_mix.outputs['Shader'], shader_alpha_mix.inputs[2]) ############################################################################ - __new_io(shader.inputs, node_input.outputs, 'Ambient Color', node_diffuse.inputs['Color1'], (0.4, 0.4, 0.4, 1)) - __new_io(shader.inputs, node_input.outputs, 'Diffuse Color', node_diffuse.inputs['Color2'], (0.8, 0.8, 0.8, 1)) - __new_io(shader.inputs, node_input.outputs, 'Specular Color', shader_glossy.inputs['Color'], (0.8, 0.8, 0.8, 1)) - __new_io(shader.inputs, node_input.outputs, 'Reflect', node_reflect.inputs[1], 50, min_max=(1, 512)) - __new_io(shader.inputs, node_input.outputs, 'Base Tex Fac', node_tex.inputs['Fac'], 1) - __new_io(shader.inputs, node_input.outputs, 'Base Tex', node_tex.inputs['Color2'], (1, 1, 1, 1)) - __new_io(shader.inputs, node_input.outputs, 'Toon Tex Fac', node_toon.inputs['Fac'], 1) - __new_io(shader.inputs, node_input.outputs, 'Toon Tex', node_toon.inputs['Color2'], (1, 1, 1, 1)) - __new_io(shader.inputs, node_input.outputs, 'Sphere Tex Fac', node_sph.inputs['Fac'], 1) - __new_io(shader.inputs, node_input.outputs, 'Sphere Tex', node_sph.inputs['Color2'], (1, 1, 1, 1)) - __new_io(shader.inputs, node_input.outputs, 'Sphere Mul/Add', node_sphere.inputs['Fac'], 0) - __new_io(shader.inputs, node_input.outputs, 'Double Sided', node_cull.inputs[1], 0, min_max=(0, 1)) - __new_io(shader.inputs, node_input.outputs, 'Alpha', node_alpha_tex.inputs[0], 1, min_max=(0, 1)) - __new_io(shader.inputs, node_input.outputs, 'Base Alpha', node_alpha_tex.inputs[1], 1, min_max=(0, 1)) - __new_io(shader.inputs, node_input.outputs, 'Toon Alpha', node_alpha_toon.inputs[1], 1, min_max=(0, 1)) - __new_io(shader.inputs, node_input.outputs, 'Sphere Alpha', node_alpha_sph.inputs[1], 1, min_max=(0, 1)) + ng.new_input_socket('Ambient Color', node_diffuse.inputs['Color1'], (0.4, 0.4, 0.4, 1)) + ng.new_input_socket('Diffuse Color', node_diffuse.inputs['Color2'], (0.8, 0.8, 0.8, 1)) + ng.new_input_socket('Specular Color', shader_glossy.inputs['Color'], (0.8, 0.8, 0.8, 1)) + ng.new_input_socket('Reflect', node_reflect.inputs[1], 50, min_max=(1, 512)) + ng.new_input_socket('Base Tex Fac', node_tex.inputs['Fac'], 1) + ng.new_input_socket('Base Tex', node_tex.inputs['Color2'], (1, 1, 1, 1)) + ng.new_input_socket('Toon Tex Fac', node_toon.inputs['Fac'], 1) + ng.new_input_socket('Toon Tex', node_toon.inputs['Color2'], (1, 1, 1, 1)) + ng.new_input_socket('Sphere Tex Fac', node_sph.inputs['Fac'], 1) + ng.new_input_socket('Sphere Tex', node_sph.inputs['Color2'], (1, 1, 1, 1)) + ng.new_input_socket('Sphere Mul/Add', node_sphere.inputs['Fac'], 0) + ng.new_input_socket('Double Sided', node_cull.inputs[1], 0, min_max=(0, 1)) + ng.new_input_socket('Alpha', node_alpha_tex.inputs[0], 1, min_max=(0, 1)) + ng.new_input_socket('Base Alpha', node_alpha_tex.inputs[1], 1, min_max=(0, 1)) + ng.new_input_socket('Toon Alpha', node_alpha_toon.inputs[1], 1, min_max=(0, 1)) + ng.new_input_socket('Sphere Alpha', node_alpha_sph.inputs[1], 1, min_max=(0, 1)) links.new(node_input.outputs['Sphere Tex Fac'], node_spa.inputs['Fac']) links.new(node_input.outputs['Sphere Tex'], node_spa.inputs['Color2']) - __new_io(shader.outputs, node_output.inputs, 'Shader', shader_alpha_mix.outputs['Shader']) + ng.new_output_socket('Shader', shader_alpha_mix.outputs['Shader']) return shader diff --git a/extern_tools/mmd_tools_local/core/model.py b/extern_tools/mmd_tools_local/core/model.py index 9520fea1..7434296b 100644 --- a/extern_tools/mmd_tools_local/core/model.py +++ b/extern_tools/mmd_tools_local/core/model.py @@ -552,9 +552,6 @@ def build(self): rigid_body.setRigidBodyWorldEnabled(rigidbody_world_enabled) def clean(self): - #FIXME rigid body cache is out of sync on Blender 2.8 - # [Build] at frame 1 -> [Play] -> [Stop] at frame N -> [Clean] at frame N -> [Play] -> crash - # [Build] at frame 1 -> [Play] -> [Stop] at frame N -> [Clean] at frame N -> go to frame 1 ->[Play] -> ok rigidbody_world_enabled = rigid_body.setRigidBodyWorldEnabled(False) logging.info('****************************************') logging.info(' Clean rig') @@ -573,18 +570,20 @@ def clean(self): if bpy.app.version < (2, 78, 0): self.__removeChildrenOfTemporaryGroupObject() # for speeding up only for i in self.temporaryObjects(): - if i.mmd_type in ['NON_COLLISION_CONSTRAINT', 'SPRING_GOAL', 'SPRING_CONSTRAINT']: - bpy.context.scene.objects.unlink(i) - bpy.data.objects.remove(i) - elif i.mmd_type == 'TRACK_TARGET': - bpy.context.scene.objects.unlink(i) - bpy.data.objects.remove(i) - else: + bpy.context.scene.objects.unlink(i) + bpy.data.objects.remove(i) + elif bpy.app.version < (2, 80, 0): for i in self.temporaryObjects(): - if i.mmd_type in ['NON_COLLISION_CONSTRAINT', 'SPRING_GOAL', 'SPRING_CONSTRAINT']: - bpy.data.objects.remove(i, do_unlink=True) - elif i.mmd_type == 'TRACK_TARGET': - bpy.data.objects.remove(i, do_unlink=True) + bpy.data.objects.remove(i, do_unlink=True) + else: + tmp_objs = tuple(self.temporaryObjects()) + for i in tmp_objs: + for c in i.users_collection: + c.objects.unlink(i) + bpy.ops.object.delete({'selected_objects':tmp_objs, 'active_object':self.rootObject()}) + for i in tmp_objs: + #assert(i.users == 0) + bpy.data.objects.remove(i) rigid_track_counts = 0 for i in self.rigidBodies(): diff --git a/extern_tools/mmd_tools_local/core/morph.py b/extern_tools/mmd_tools_local/core/morph.py index b54e9d2f..9b163046 100644 --- a/extern_tools/mmd_tools_local/core/morph.py +++ b/extern_tools/mmd_tools_local/core/morph.py @@ -308,7 +308,10 @@ def __cleanup(self, names_in_use=None): for n in m.node_tree.nodes: if n.name.startswith('mmd_bind'): m.node_tree.nodes.remove(n) - m.mmd_material.is_double_sided = m.mmd_material.is_double_sided # update mmd shader + if 'mmd_shader' in m.node_tree.nodes: + m.mmd_material.is_double_sided = m.mmd_material.is_double_sided # update mmd shader + else: + m.update_tag() attributes = set(TransformConstraintOp.min_max_attributes('LOCATION', 'to')) attributes |= set(TransformConstraintOp.min_max_attributes('ROTATION', 'to')) diff --git a/extern_tools/mmd_tools_local/core/pmd/__init__.py b/extern_tools/mmd_tools_local/core/pmd/__init__.py index 00cf3f25..8ed1c829 100644 --- a/extern_tools/mmd_tools_local/core/pmd/__init__.py +++ b/extern_tools/mmd_tools_local/core/pmd/__init__.py @@ -274,8 +274,8 @@ def load(self, fs): class Joint: def __init__(self): self.name = '' - self.src_rigid = 0 - self.dest_rigid = 0 + self.src_rigid = None + self.dest_rigid = None self.location = [] self.rotation = [] @@ -289,6 +289,19 @@ def __init__(self): self.spring_rotation_constant = [] def load(self, fs): + try: self._load(fs) + except struct.error: # possibly contains truncated data + if self.src_rigid is None or self.dest_rigid is None: raise + self.location = self.location or (0, 0, 0) + self.rotation = self.rotation or (0, 0, 0) + self.maximum_location = self.maximum_location or (0, 0, 0) + self.minimum_location = self.minimum_location or (0, 0, 0) + self.maximum_rotation = self.maximum_rotation or (0, 0, 0) + self.minimum_rotation = self.minimum_rotation or (0, 0, 0) + self.spring_constant = self.spring_constant or (0, 0, 0) + self.spring_rotation_constant = self.spring_rotation_constant or (0, 0, 0) + + def _load(self, fs): self.name = fs.readStr(20) self.src_rigid = fs.readUnsignedInt() diff --git a/extern_tools/mmd_tools_local/core/pmd/importer.py b/extern_tools/mmd_tools_local/core/pmd/importer.py index bf6ac60a..099ef674 100644 --- a/extern_tools/mmd_tools_local/core/pmd/importer.py +++ b/extern_tools/mmd_tools_local/core/pmd/importer.py @@ -87,6 +87,8 @@ def import_pmd_to_pmx(filepath): logging.info('------------------------------') logging.info(' Convert Bones') logging.info('------------------------------') + # ボーンの種類 + # 0:回転 1:回転/移動 2:IK 3:不明 4:IK影響下 5:回転影響下 6:IK接続先 7:非表示/ボーン接続先 8:捩り 9:回転運動 for i, bone in enumerate(pmd_model.bones): pmx_bone = pmx.Bone() pmx_bone.name = bone.name @@ -98,7 +100,7 @@ def import_pmd_to_pmx(filepath): else: pmx_bone.displayConnection = -1 if pmx_bone.displayConnection <= 0: - pmx_bone.displayConnection = [0.0, 0.0, 0.0] + pmx_bone.displayConnection = (0.0, 0.0, 0.0) pmx_bone.isIK = False if bone.type == 0: pmx_bone.isMovable = False diff --git a/extern_tools/mmd_tools_local/core/pmx/__init__.py b/extern_tools/mmd_tools_local/core/pmx/__init__.py index c69c3f0f..19260394 100644 --- a/extern_tools/mmd_tools_local/core/pmx/__init__.py +++ b/extern_tools/mmd_tools_local/core/pmx/__init__.py @@ -466,17 +466,10 @@ def load(self, fs): logging.debug(' Additional Transform: Bone:%d, influence: %f', *b.additionalTransform) logging.debug(' IK: %s', str(b.isIK)) if b.isIK: + logging.debug(' Unit Angle: %f', b.rotationConstraint) logging.debug(' Target: %d', b.target) for j, link in enumerate(b.ik_links): - if isinstance(link.minimumAngle, list) and len(link.minimumAngle) == 3: - min_str = '(%f, %f, %f)'%tuple(link.minimumAngle) - else: - min_str = '(None, None, None)' - if isinstance(link.maximumAngle, list) and len(link.maximumAngle) == 3: - max_str = '(%f, %f, %f)'%tuple(link.maximumAngle) - else: - max_str = '(None, None, None)' - logging.debug(' IK Link %d: %d, %s - %s', j, link.target, min_str, max_str) + logging.debug(' IK Link %d: %d, %s - %s', j, link.target, str(link.minimumAngle), str(link.maximumAngle)) logging.debug('') logging.info('----- Loaded %d bones.', len(self.bones)) @@ -1128,7 +1121,7 @@ def load(self, fs): def save(self, fs): fs.writeBoneIndex(self.target) - if isinstance(self.minimumAngle, list) and isinstance(self.maximumAngle, list): + if isinstance(self.minimumAngle, (tuple, list)) and isinstance(self.maximumAngle, (tuple, list)): fs.writeByte(1) fs.writeVector(self.minimumAngle) fs.writeVector(self.maximumAngle) @@ -1527,6 +1520,19 @@ def __init__(self): self.spring_rotation_constant = [] def load(self, fs): + try: self._load(fs) + except struct.error: # possibly contains truncated data + if self.src_rigid is None or self.dest_rigid is None: raise + self.location = self.location or (0, 0, 0) + self.rotation = self.rotation or (0, 0, 0) + self.maximum_location = self.maximum_location or (0, 0, 0) + self.minimum_location = self.minimum_location or (0, 0, 0) + self.maximum_rotation = self.maximum_rotation or (0, 0, 0) + self.minimum_rotation = self.minimum_rotation or (0, 0, 0) + self.spring_constant = self.spring_constant or (0, 0, 0) + self.spring_rotation_constant = self.spring_rotation_constant or (0, 0, 0) + + def _load(self, fs): self.name = fs.readStr() self.name_e = fs.readStr() diff --git a/extern_tools/mmd_tools_local/core/pmx/exporter.py b/extern_tools/mmd_tools_local/core/pmx/exporter.py index 7118db3b..530e2c6d 100644 --- a/extern_tools/mmd_tools_local/core/pmx/exporter.py +++ b/extern_tools/mmd_tools_local/core/pmx/exporter.py @@ -318,6 +318,9 @@ def __exportBones(self, meshes): A dictionary to map Blender bone names to bone indices of the pmx.model instance. """ arm = self.__armature + if hasattr(arm, 'evaluated_get'): + bpy.context.view_layer.update() + arm = arm.evaluated_get(bpy.context.evaluated_depsgraph_get()) boneMap = {} pmx_bones = [] pose_bones = arm.pose.bones @@ -359,11 +362,7 @@ def __to_pmx_axis(axis, pose_bone): pmx_bone.hasAdditionalRotate = mmd_bone.has_additional_rotation pmx_bone.hasAdditionalLocation = mmd_bone.has_additional_location - pmx_bone.additionalTransform = [None, mmd_bone.additional_transform_influence] - if mmd_bone.additional_transform_bone_id != -1: - fnBone = FnBone.from_bone_id(arm, mmd_bone.additional_transform_bone_id) - if fnBone: - pmx_bone.additionalTransform[0] = fnBone.pose_bone + pmx_bone.additionalTransform = [mmd_bone.additional_transform_bone, mmd_bone.additional_transform_influence] pmx_bone.location = __to_pmx_location(p_bone.head) pmx_bone.parent = bone.parent @@ -411,9 +410,7 @@ def __to_pmx_axis(axis, pose_bone): i.displayConnection = pmx_bones.index(i.displayConnection) elif isinstance(i.displayConnection, bpy.types.Bone): i.displayConnection = pmx_bones.index(boneMap[i.displayConnection]) - - pose_bone = i.additionalTransform[0] - i.additionalTransform[0] = r.get(pose_bone.name, -1) if pose_bone else -1 + i.additionalTransform[0] = r.get(i.additionalTransform[0], -1) if len(pmx_bones) == 0: # avoid crashing MMD @@ -429,7 +426,7 @@ def __to_pmx_axis(axis, pose_bone): self.__exportIK(r) return r - def __exportIKLinks(self, pose_bone, count, bone_map, ik_links): + def __exportIKLinks(self, pose_bone, count, bone_map, ik_links, custom_bone): if count <= 0 or pose_bone is None or pose_bone.name not in bone_map: return ik_links @@ -440,8 +437,17 @@ def __exportIKLinks(self, pose_bone, count, bone_map, ik_links): from math import pi minimum, maximum = [-pi]*3, [pi]*3 unused_counts = 0 + ik_limit_custom = next((c for c in custom_bone.constraints if c.type == 'LIMIT_ROTATION' and c.name == 'mmd_ik_limit_custom%d'%len(ik_links)), None) ik_limit_override = next((c for c in pose_bone.constraints if c.type == 'LIMIT_ROTATION' and not c.mute), None) for i, axis in enumerate('xyz'): + if ik_limit_custom: # custom ik limits for MMD only + if getattr(ik_limit_custom, 'use_limit_'+axis): + minimum[i] = getattr(ik_limit_custom, 'min_'+axis) + maximum[i] = getattr(ik_limit_custom, 'max_'+axis) + else: + unused_counts += 1 + continue + if getattr(pose_bone, 'lock_ik_'+axis): minimum[i] = maximum[i] = 0 elif ik_limit_override is not None and getattr(ik_limit_override, 'use_limit_'+axis): @@ -460,7 +466,7 @@ def __exportIKLinks(self, pose_bone, count, bone_map, ik_links): ik_link.minimumAngle = list(minimum) ik_link.maximumAngle = list(maximum) - return self.__exportIKLinks(pose_bone.parent, count - 1, bone_map, ik_links + [ik_link]) + return self.__exportIKLinks(pose_bone.parent, count - 1, bone_map, ik_links + [ik_link], custom_bone) def __exportIK(self, bone_map): @@ -471,12 +477,20 @@ def __exportIK(self, bone_map): arm = self.__armature ik_loop_factor = max(arm.get('mmd_ik_loop_factor', 1), 1) pose_bones = arm.pose.bones + + ik_target_custom_map = {getattr(b.constraints.get('mmd_ik_target_custom', None), 'subtarget', None):b for b in pose_bones if not b.is_mmd_shadow_bone} + def __ik_target_bone_get(ik_constraint_bone, ik_bone): + if ik_bone.name in ik_target_custom_map: + logging.debug(' (use "mmd_ik_target_custom")') + return ik_target_custom_map[ik_bone.name] # for supporting the ik target which is not a child of ik_constraint_bone + return self.__get_ik_target_bone(ik_constraint_bone) # this only search the children of ik_constraint_bone + for bone in pose_bones: if bone.is_mmd_shadow_bone: continue for c in bone.constraints: if c.type == 'IK' and not c.mute: - logging.debug(' Found IK constraint.') + logging.debug(' Found IK constraint on %s', bone.name) ik_pose_bone = self.__get_ik_control_bone(c) if ik_pose_bone is None: logging.warning(' * Invalid IK constraint "%s" on bone %s', c.name, bone.name) @@ -493,16 +507,19 @@ def __exportIK(self, bone_map): continue ik_chain0 = bone if c.use_tail else bone.parent - ik_target_bone = self.__get_ik_target_bone(bone) if c.use_tail else bone + ik_target_bone = __ik_target_bone_get(bone, ik_pose_bone) if c.use_tail else bone if ik_target_bone is None: logging.warning(' * IK bone: %s, IK Target not found !!!', ik_pose_bone.name) continue logging.debug(' - IK bone: %s, IK Target: %s', ik_pose_bone.name, ik_target_bone.name) pmx_ik_bone.isIK = True pmx_ik_bone.loopCount = max(int(c.iterations/ik_loop_factor), 1) - pmx_ik_bone.rotationConstraint = bone.mmd_bone.ik_rotation_constraint + if ik_pose_bone.name in ik_target_custom_map: + pmx_ik_bone.rotationConstraint = ik_pose_bone.mmd_bone.ik_rotation_constraint + else: + pmx_ik_bone.rotationConstraint = bone.mmd_bone.ik_rotation_constraint pmx_ik_bone.target = bone_map[ik_target_bone.name] - pmx_ik_bone.ik_links = self.__exportIKLinks(ik_chain0, c.chain_count, bone_map, []) + pmx_ik_bone.ik_links = self.__exportIKLinks(ik_chain0, c.chain_count, bone_map, [], ik_pose_bone) def __get_ik_control_bone(self, ik_constraint): arm = ik_constraint.target @@ -595,7 +612,7 @@ def __export_material_morphs(self, root): else: morph_data.index = -1 except ValueError: - logging.warning('Material Morph (%s): Material %s was not found.', morph.name, data.material) + logging.warning('Material Morph (%s): Material "%s" was not found.', morph.name, data.material) continue morph_data.offset_type = ['MULT', 'ADD'].index(data.offset_type) morph_data.diffuse_offset = data.diffuse_color @@ -670,7 +687,7 @@ def __export_bone_morphs(self, root): continue blender_bone = pose_bones.get(data.bone, None) if blender_bone is None: - logging.warning('Bone Morph (%s): Bone %s was not found.', morph.name, data.bone) + logging.warning('Bone Morph (%s): Bone "%s" was not found.', morph.name, data.bone) continue converter = bone_util_cls(blender_bone, self.__scale, invert=True) morph_data.location_offset = converter.convert_location(data.location) @@ -735,7 +752,7 @@ def __export_group_morphs(self, root): for data in morph.data: morph_index = morph_map.get((data.morph_type, data.name), -1) if morph_index < 0: - logging.warning('Group Morph (%s): Morph %s was not found.', morph.name, data.name) + logging.warning('Group Morph (%s): Morph "%s" was not found.', morph.name, data.name) continue morph_data = pmx.GroupMorphOffset() morph_data.morph = morph_index diff --git a/extern_tools/mmd_tools_local/core/pmx/importer.py b/extern_tools/mmd_tools_local/core/pmx/importer.py index 641e20b2..5198bb6f 100644 --- a/extern_tools/mmd_tools_local/core/pmx/importer.py +++ b/extern_tools/mmd_tools_local/core/pmx/importer.py @@ -57,6 +57,7 @@ def __init__(self): self.__imageTable = {} self.__sdefVertices = {} # pmx vertices + self.__blender_ik_links = set() self.__vertex_map = None self.__materialFaceCountTable = None @@ -327,31 +328,35 @@ def __applyIk(self, index, pmx_bone, pose_bones): # for tracking mmd ik target, simple explaination: # + Root # | + link1 - # | + link0 (ik_bone) <- ik constraint, chain_count=2 + # | + link0 (ik_constraint_bone) <- ik constraint, chain_count=2 # | + IK target (ik_target) <- constraint 'mmd_ik_target_override', subtarget=link0 - # + IK bone (target_bone) + # + IK bone (ik_bone) # # it is possible that the link0 is the IK target, # so ik constraint will be on link1, chain_count=1 # the IK target isn't affected by IK bone - target_bone = pose_bones[index] + ik_bone = pose_bones[index] ik_target = pose_bones[pmx_bone.target] - ik_bone = ik_target.parent + ik_constraint_bone = ik_target.parent is_valid_ik = False if len(pmx_bone.ik_links) > 0: - ik_bone_real = pose_bones[pmx_bone.ik_links[0].target] - if ik_bone_real == ik_target or ik_bone_real.parent == ik_target: - ik_bone_real = ik_target.parent + ik_constraint_bone_real = pose_bones[pmx_bone.ik_links[0].target] + if ik_constraint_bone_real == ik_target: + if len(pmx_bone.ik_links) > 1: + ik_constraint_bone_real = pose_bones[pmx_bone.ik_links[1].target] del pmx_bone.ik_links[0] - logging.warning(' * fix IK settings of IK bone (%s)', target_bone.name) - is_valid_ik = (ik_bone == ik_bone_real) + logging.warning(' * fix IK settings of IK bone (%s)', ik_bone.name) + is_valid_ik = (ik_constraint_bone == ik_constraint_bone_real) if not is_valid_ik: - ik_bone = ik_bone_real - logging.warning(' * IK bone (%s) error: IK target (%s) should be a child of IK link 0 (%s)', - target_bone.name, ik_target.name, ik_bone.name) - if ik_bone is None: - logging.warning(' * Invalid IK bone (%s)', target_bone.name) + ik_constraint_bone = ik_constraint_bone_real + logging.warning(' * IK bone (%s) warning: IK target (%s) is not a child of IK link 0 (%s)', + ik_bone.name, ik_target.name, ik_constraint_bone.name) + elif any(pose_bones[i.target].parent != pose_bones[j.target] for i, j in zip(pmx_bone.ik_links, pmx_bone.ik_links[1:])): + logging.warning(' * Invalid IK bone (%s): IK chain does not follow parent-child relationship', ik_bone.name) + return + if ik_constraint_bone is None or len(pmx_bone.ik_links) < 1: + logging.warning(' * Invalid IK bone (%s)', ik_bone.name) return c = ik_target.constraints.new(type='DAMPED_TRACK') @@ -359,14 +364,34 @@ def __applyIk(self, index, pmx_bone, pose_bones): c.mute = True c.influence = 0 c.target = self.__armObj - c.subtarget = ik_bone.name + c.subtarget = ik_constraint_bone.name + if not is_valid_ik or next((c for c in ik_constraint_bone.constraints if c.type == 'IK' and c.is_valid), None): + c.name = 'mmd_ik_target_custom' + c.subtarget = ik_bone.name # point to IK control bone + ik_bone.mmd_bone.ik_rotation_constraint = pmx_bone.rotationConstraint + use_custom_ik = True + else: + ik_constraint_bone.mmd_bone.ik_rotation_constraint = pmx_bone.rotationConstraint + use_custom_ik = False - ikConst = self.__rig.create_ik_constraint(ik_bone, target_bone) + ikConst = self.__rig.create_ik_constraint(ik_constraint_bone, ik_bone) ikConst.iterations = pmx_bone.loopCount ikConst.chain_count = len(pmx_bone.ik_links) - ikConst.mute = not is_valid_ik - ik_bone.mmd_bone.ik_rotation_constraint = pmx_bone.rotationConstraint - for i in pmx_bone.ik_links: + if not is_valid_ik: + ikConst.pole_target = self.__armObj # make it an incomplete/invalid setting + for idx, i in enumerate(pmx_bone.ik_links): + if use_custom_ik or i.target in self.__blender_ik_links: + c = ik_bone.constraints.new(type='LIMIT_ROTATION') + c.mute = True + c.influence = 0 + c.name = 'mmd_ik_limit_custom%d'%idx + use_limits = c.use_limit_x = c.use_limit_y = c.use_limit_z = (i.maximumAngle is not None) + if use_limits: + minimum, maximum = self.convertIKLimitAngles(i.minimumAngle, i.maximumAngle, pose_bones[i.target].bone.matrix_local) + c.max_x, c.max_y, c.max_z = maximum + c.min_x, c.min_y, c.min_z = minimum + continue + self.__blender_ik_links.add(i.target) if i.maximumAngle is not None: bone = pose_bones[i.target] minimum, maximum = self.convertIKLimitAngles(i.minimumAngle, i.maximumAngle, bone.bone.matrix_local) @@ -402,7 +427,7 @@ def __importBones(self): mmd_bone.transform_order = pmx_bone.transform_order mmd_bone.transform_after_dynamics = pmx_bone.transAfterPhis - if pmx_bone.displayConnection == -1 or pmx_bone.displayConnection == [0.0, 0.0, 0.0]: + if pmx_bone.displayConnection == -1 or pmx_bone.displayConnection == (0.0, 0.0, 0.0): mmd_bone.is_tip = True elif b_bone.name in specialTipBones: mmd_bone.is_tip = True @@ -416,7 +441,7 @@ def __importBones(self): b_bone.lock_location = [True, True, True] if pmx_bone.isIK: - if pmx_bone.target != -1: + if 0 <= pmx_bone.target < len(pose_bones): self.__applyIk(i, pmx_bone, pose_bones) if pmx_bone.hasAdditionalRotate or pmx_bone.hasAdditionalLocation: diff --git a/extern_tools/mmd_tools_local/core/shader.py b/extern_tools/mmd_tools_local/core/shader.py index 251422cb..a8233902 100644 --- a/extern_tools/mmd_tools_local/core/shader.py +++ b/extern_tools/mmd_tools_local/core/shader.py @@ -81,11 +81,16 @@ def new_output_socket(self, io_name, socket, default_val=None, min_max=None): self.__new_io(self.shader.outputs, self.node_output.inputs, io_name, socket, default_val, min_max) def __new_io(self, shader_io, io_sockets, io_name, socket, default_val=None, min_max=None): - if io_name in io_sockets: - self.links.new(io_sockets[io_name], socket) - else: - self.links.new(io_sockets[-1], socket) - shader_io[-1].name = io_name + if io_name not in io_sockets: + shader_io.new(type=socket.bl_idname, name=io_name) + if not min_max: + idname = socket.bl_idname + if idname.endswith('Factor') or io_name.endswith('Alpha'): + shader_io[io_name].min_value, shader_io[io_name].max_value = 0, 1 + elif idname.endswith('Float') or idname.endswith('Vector'): + shader_io[io_name].min_value, shader_io[io_name].max_value = -10, 10 + + self.links.new(io_sockets[io_name], socket) if default_val is not None: shader_io[io_name].default_value = default_val if min_max is not None: @@ -113,7 +118,7 @@ def setup_morph_nodes(cls, material, morphs): if n.node_tree.name != node.node_tree.name: n.location.x -= 100 if node.name.startswith('mmd_'): - n.location.y += 1000 + n.location.y += 1500 node = n return nodes diff --git a/extern_tools/mmd_tools_local/core/vmd/__init__.py b/extern_tools/mmd_tools_local/core/vmd/__init__.py index 619c198e..7034d53f 100644 --- a/extern_tools/mmd_tools_local/core/vmd/__init__.py +++ b/extern_tools/mmd_tools_local/core/vmd/__init__.py @@ -24,7 +24,6 @@ def load(self, fin): if self.signature[:len(self.VMD_SIGN)] != self.VMD_SIGN: raise InvalidFileError('File signature "%s" is invalid.'%self.signature) self.model_name = _toShiftJisString(struct.unpack('<20s', fin.read(20))[0]) - print(self) def save(self, fin): fin.write(struct.pack('<30s', self.VMD_SIGN)) @@ -189,7 +188,6 @@ def load(self, fin): ik_name = _toShiftJisString(struct.unpack('<20s', fin.read(20))[0]) state, = struct.unpack(' 3: - alpha_value = i.material.diffuse_color[3] + if hasattr(material, 'alpha'): + alpha_value = material.alpha + elif len(material.diffuse_color) > 3: + alpha_value = material.diffuse_color[3] if node_alpha or alpha_value < 1.0: - alpha_shader = i.material.node_tree.nodes.new('ShaderNodeGroup') + alpha_shader = material.node_tree.nodes.new('ShaderNodeGroup') alpha_shader.location.x = shader.location.x + 250 alpha_shader.location.y = shader.location.y - 150 alpha_shader.node_tree = mmd_alpha_shader_grp alpha_shader.inputs[1].default_value = alpha_value - i.material.node_tree.links.new(alpha_shader.inputs[0], outplug) + material.node_tree.links.new(alpha_shader.inputs[0], outplug) outplug = alpha_shader.outputs[0] if node_alpha: - i.material.node_tree.links.new(alpha_shader.inputs[1], node_alpha.outputs[-1]) + material.node_tree.links.new(alpha_shader.inputs[1], node_alpha.outputs[len(node_alpha.outputs)-1]) - material_output = __getMaterialOutput(i.material.node_tree.nodes) - i.material.node_tree.links.new(material_output.inputs['Surface'], outplug) + material_output = __getMaterialOutput(material.node_tree.nodes, 'ShaderNodeOutputMaterial') + material.node_tree.links.new(material_output.inputs['Surface'], outplug) material_output.location.x = shader.location.x + 500 material_output.location.y = shader.location.y - 150 if not hasattr(bpy.types, 'ShaderNodeMaterial'): return # Add necessary nodes to retain Blender Render functionality - mat_node = i.material.node_tree.nodes.new('ShaderNodeMaterial') - out_node = i.material.node_tree.nodes.new('ShaderNodeOutput') - mat_node.material = i.material + out_node = __getMaterialOutput(material.node_tree.nodes, 'ShaderNodeOutput') + mat_node = material.node_tree.nodes.new('ShaderNodeMaterial') + mat_node.material = material mat_node.location.x = shader.location.x - 250 mat_node.location.y = shader.location.y + 500 out_node.location.x = mat_node.location.x + 750 out_node.location.y = mat_node.location.y - i.material.node_tree.links.new(out_node.inputs['Color'], mat_node.outputs['Color']) - i.material.node_tree.links.new(out_node.inputs['Alpha'], mat_node.outputs['Alpha']) + material.node_tree.links.new(out_node.inputs['Color'], mat_node.outputs['Color']) + material.node_tree.links.new(out_node.inputs['Alpha'], mat_node.outputs['Alpha']) + +def __convertToPrincipledBsdf(material): + node_names = set() + for s in tuple(n for n in material.node_tree.nodes if isinstance(n, bpy.types.ShaderNodeGroup)): + if s.node_tree.name == 'MMDBasicShader': + for l in s.outputs[0].links: + to_node = l.to_node + # assuming there is no bpy.types.NodeReroute between MMDBasicShader and MMDAlphaShader + if isinstance(to_node, bpy.types.ShaderNodeGroup) and to_node.node_tree.name == 'MMDAlphaShader': + __switchToPrincipledBsdf(material.node_tree, s, to_node) + node_names.add(to_node.name) + else: + __switchToPrincipledBsdf(material.node_tree, s) + node_names.add(s.name) + elif s.node_tree.name == 'MMDShaderDev': + __switchToPrincipledBsdf(material.node_tree, s) + node_names.add(s.name) + # remove MMD shader nodes + nodes = material.node_tree.nodes + for name in node_names: + nodes.remove(nodes[name]) + +def __switchToPrincipledBsdf(node_tree, node_basic, node_alpha=None): + shader = node_tree.nodes.new('ShaderNodeBsdfPrincipled') + shader.parent = node_basic.parent + shader.location.x = node_basic.location.x + shader.location.y = node_basic.location.y + + alpha_socket_name = 'Alpha' + if node_basic.node_tree.name == 'MMDShaderDev': + node_alpha, alpha_socket_name = node_basic, 'Base Alpha' + if 'Base Tex' in node_basic.inputs and node_basic.inputs['Base Tex'].is_linked: + node_tree.links.new(node_basic.inputs['Base Tex'].links[0].from_socket, shader.inputs['Base Color']) + elif 'Diffuse Color' in node_basic.inputs: + shader.inputs['Base Color'].default_value[:3] = node_basic.inputs['Diffuse Color'].default_value[:3] + elif 'diffuse' in node_basic.inputs: + shader.inputs['Base Color'].default_value[:3] = node_basic.inputs['diffuse'].default_value[:3] + if node_basic.inputs['diffuse'].is_linked: + node_tree.links.new(node_basic.inputs['diffuse'].links[0].from_socket, shader.inputs['Base Color']) + + shader.inputs['IOR'].default_value = 1.0 + + output_links = node_basic.outputs[0].links + if node_alpha: + output_links = node_alpha.outputs[0].links + shader.parent = node_alpha.parent or shader.parent + shader.location.x = node_alpha.location.x + + if alpha_socket_name in node_alpha.inputs: + if 'Alpha' in shader.inputs: + shader.inputs['Alpha'].default_value = node_alpha.inputs[alpha_socket_name].default_value + if node_alpha.inputs[alpha_socket_name].is_linked: + node_tree.links.new(node_alpha.inputs[alpha_socket_name].links[0].from_socket, shader.inputs['Alpha']) + else: + shader.inputs['Transmission'].default_value = 1 - node_alpha.inputs[alpha_socket_name].default_value + if node_alpha.inputs[alpha_socket_name].is_linked: + node_invert = node_tree.nodes.new('ShaderNodeMath') + node_invert.parent = shader.parent + node_invert.location.x = node_alpha.location.x - 250 + node_invert.location.y = node_alpha.location.y - 300 + node_invert.operation = 'SUBTRACT' + node_invert.use_clamp = True + node_invert.inputs[0].default_value = 1 + node_tree.links.new(node_alpha.inputs[alpha_socket_name].links[0].from_socket, node_invert.inputs[1]) + node_tree.links.new(node_invert.outputs[0], shader.inputs['Transmission']) + + for l in output_links: + node_tree.links.new(shader.outputs[0], l.to_socket) diff --git a/extern_tools/mmd_tools_local/operators/material.py b/extern_tools/mmd_tools_local/operators/material.py index 9c3432b4..d37212af 100644 --- a/extern_tools/mmd_tools_local/operators/material.py +++ b/extern_tools/mmd_tools_local/operators/material.py @@ -5,9 +5,10 @@ from bpy.props import StringProperty, BoolProperty from mmd_tools_local import register_wrap +from mmd_tools_local import cycles_converter from mmd_tools_local.core.material import FnMaterial from mmd_tools_local.core.exceptions import MaterialNotFoundError -from mmd_tools_local import cycles_converter +from mmd_tools_local.core.shader import _NodeGroupUtils @register_wrap class ConvertMaterialsForCycles(Operator): @@ -16,14 +17,38 @@ class ConvertMaterialsForCycles(Operator): bl_description = 'Convert materials of selected objects for Cycles.' bl_options = {'REGISTER', 'UNDO'} + use_principled = bpy.props.BoolProperty( + name='Convert to Principled BSDF', + description='Convert MMD shader nodes to Principled BSDF as well if enabled', + default=False, + options={'SKIP_SAVE'}, + ) + + clean_nodes = bpy.props.BoolProperty( + name='Clean Nodes', + description='Remove redundant nodes as well if enabled. Disable it to keep node data.', + default=False, + options={'SKIP_SAVE'}, + ) + + @classmethod + def poll(cls, context): + return next((x for x in context.selected_objects if x.type == 'MESH'), None) + + def draw(self, context): + layout = self.layout + if cycles_converter.is_principled_bsdf_supported(): + layout.prop(self, 'use_principled') + layout.prop(self, 'clean_nodes') + def execute(self, context): try: context.scene.render.engine = 'CYCLES' except: self.report({'ERROR'}, ' * Failed to change to Cycles render engine.') return {'CANCELLED'} - for obj in [x for x in context.selected_objects if x.type == 'MESH']: - cycles_converter.convertToCyclesShader(obj) + for obj in (x for x in context.selected_objects if x.type == 'MESH'): + cycles_converter.convertToCyclesShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes) return {'FINISHED'} @register_wrap @@ -283,61 +308,45 @@ def __make_shader(self, m): node_shader.inputs['Alpha'].default_value = m.mmd_material.edge_color[3] def __get_edge_preview_shader(self): - shader = bpy.data.node_groups.get('MMDEdgePreview', None) - if shader: + group_name = 'MMDEdgePreview' + shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type='ShaderNodeTree') + if len(shader.nodes): return shader - shader = bpy.data.node_groups.new(name='MMDEdgePreview', type='ShaderNodeTree') - nodes, links = shader.nodes, shader.links - - def __new_node(idname, pos): - node = nodes.new(idname) - node.location = pos - return node - - def __new_io(shader_io, io_sockets, io_name, socket): - if io_name in io_sockets: - links.new(io_sockets[io_name], socket) - else: - links.new(io_sockets[-1], socket) - shader_io[-1].name = io_name + ng = _NodeGroupUtils(shader) - XPOS, YPOS = 210, 110 - node_input = __new_node('NodeGroupInput', (XPOS*-5, 0)) - node_output = __new_node('NodeGroupOutput', (XPOS*3, 0)) + node_input = ng.new_node('NodeGroupInput', (-5, 0)) + node_output = ng.new_node('NodeGroupOutput', (3, 0)) ############################################################################ - node_color = __new_node('ShaderNodeMixRGB', (XPOS*-1, YPOS*-3)) + node_color = ng.new_node('ShaderNodeMixRGB', (-1, -1.5)) node_color.mute = True - __new_io(shader.inputs, node_input.outputs, 'Color', node_color.inputs['Color1']) + ng.new_input_socket('Color', node_color.inputs['Color1']) if bpy.app.version < (2, 80, 0): - node_geo = __new_node('ShaderNodeGeometry', (XPOS*-2, YPOS*-5)) - node_cull = __new_node('ShaderNodeMath', (XPOS*-1, YPOS*-5)) - node_cull.operation = 'MULTIPLY' + node_geo = ng.new_node('ShaderNodeGeometry', (-2, -2.5)) + node_cull = ng.new_math_node('MULTIPLY', (-1, -2.5)) - links.new(node_geo.outputs['Front/Back'], node_cull.inputs[1]) + ng.links.new(node_geo.outputs['Front/Back'], node_cull.inputs[1]) - __new_io(shader.inputs, node_input.outputs, 'Alpha', node_cull.inputs[0]) - __new_io(shader.outputs, node_output.inputs, 'Color', node_color.outputs['Color']) - __new_io(shader.outputs, node_output.inputs, 'Alpha', node_cull.outputs['Value']) + ng.new_input_socket('Alpha', node_cull.inputs[0]) + ng.new_output_socket('Color', node_color.outputs['Color']) + ng.new_output_socket('Alpha', node_cull.outputs['Value']) ############################################################################ - node_ray = __new_node('ShaderNodeLightPath', (XPOS*-3, YPOS*3)) - node_geo = __new_node('ShaderNodeNewGeometry', (XPOS*-3, YPOS*0)) - node_max = __new_node('ShaderNodeMath', (XPOS*-2, YPOS*3)) - node_max.operation = 'MAXIMUM' + node_ray = ng.new_node('ShaderNodeLightPath', (-3, 1.5)) + node_geo = ng.new_node('ShaderNodeNewGeometry', (-3, 0)) + node_max = ng.new_math_node('MAXIMUM', (-2, 1.5)) node_max.mute = True - node_gt = __new_node('ShaderNodeMath', (XPOS*-1, YPOS*2)) - node_gt.operation = 'GREATER_THAN' - node_alpha = __new_node('ShaderNodeMath', (XPOS*0, YPOS*2)) - node_alpha.operation = 'MULTIPLY' - node_trans = __new_node('ShaderNodeBsdfTransparent', (XPOS*0, YPOS*0)) + node_gt = ng.new_math_node('GREATER_THAN', (-1, 1)) + node_alpha = ng.new_math_node('MULTIPLY', (0, 1)) + node_trans = ng.new_node('ShaderNodeBsdfTransparent', (0, 0)) EDGE_NODE_NAME = 'ShaderNodeEmission' if bpy.app.version < (2, 80, 0) else 'ShaderNodeBackground' - node_rgb = __new_node(EDGE_NODE_NAME, (XPOS*0, YPOS*-1)) # BsdfDiffuse/Background/Emission - node_mix = __new_node('ShaderNodeMixShader', (XPOS*1, YPOS*1)) + node_rgb = ng.new_node(EDGE_NODE_NAME, (0, -0.5)) # BsdfDiffuse/Background/Emission + node_mix = ng.new_node('ShaderNodeMixShader', (1, 0.5)) + links = ng.links links.new(node_ray.outputs['Is Camera Ray'], node_max.inputs[0]) links.new(node_ray.outputs['Is Glossy Ray'], node_max.inputs[1]) links.new(node_max.outputs['Value'], node_gt.inputs[0]) @@ -348,8 +357,8 @@ def __new_io(shader_io, io_sockets, io_name, socket): links.new(node_rgb.outputs[0], node_mix.inputs[2]) links.new(node_color.outputs['Color'], node_rgb.inputs['Color']) - __new_io(shader.inputs, node_input.outputs, 'Alpha', node_alpha.inputs[1]) - __new_io(shader.outputs, node_output.inputs, 'Shader', node_mix.outputs['Shader']) + ng.new_input_socket('Alpha', node_alpha.inputs[1]) + ng.new_output_socket('Shader', node_mix.outputs['Shader']) return shader diff --git a/extern_tools/mmd_tools_local/operators/misc.py b/extern_tools/mmd_tools_local/operators/misc.py index 6f6695f0..a5da178a 100644 --- a/extern_tools/mmd_tools_local/operators/misc.py +++ b/extern_tools/mmd_tools_local/operators/misc.py @@ -129,23 +129,24 @@ def poll(cls, context): obj = context.active_object return obj and obj.type == 'MESH' + def __separate_by_materials(self, obj): + utils.separateByMaterials(obj) + if self.clean_shape_keys: + bpy.ops.mmd_tools.clean_shape_keys() + def execute(self, context): obj = context.active_object root = mmd_model.Model.findRoot(obj) - if root: + if root is None: + self.__separate_by_materials(obj) + else: bpy.ops.mmd_tools.clear_temp_materials() bpy.ops.mmd_tools.clear_uv_morph_view() - if root: + # Store the current material names rig = mmd_model.Model(root) mat_names = [getattr(mat, 'name', None) for mat in rig.materials()] - utils.separateByMaterials(obj) - if self.clean_shape_keys: - bpy.ops.mmd_tools.clean_shape_keys() - if root: - rig = mmd_model.Model(root) - # The material morphs store the name of the mesh, not of the object. - # So they will not be out of sync + self.__separate_by_materials(obj) for mesh in rig.meshes(): FnMorph.clean_uv_morph_vertex_groups(mesh) if len(mesh.data.materials) > 0: @@ -153,10 +154,8 @@ def execute(self, context): idx = mat_names.index(getattr(mat, 'name', None)) MoveObject.set_index(mesh, idx) - if root and len(root.mmd_root.material_morphs) > 0: for morph in root.mmd_root.material_morphs: - mo = FnMorph(morph, mmd_model.Model(root)) - mo.update_mat_related_mesh() + FnMorph(morph, rig).update_mat_related_mesh() utils.clearUnusedMeshes() return {'FINISHED'} @@ -180,9 +179,8 @@ def execute(self, context): self.report({ 'ERROR' }, 'Select a MMD model') return { 'CANCELLED' } - if root: - bpy.ops.mmd_tools.clear_temp_materials() - bpy.ops.mmd_tools.clear_uv_morph_view() + bpy.ops.mmd_tools.clear_temp_materials() + bpy.ops.mmd_tools.clear_uv_morph_view() # Find all the meshes in mmd_root rig = mmd_model.Model(root) @@ -207,12 +205,8 @@ def execute(self, context): if self.sort_shape_keys: FnMorph.fixShapeKeyOrder(active_mesh, root.mmd_root.vertex_morphs.keys()) active_mesh.active_shape_key_index = 0 - - if len(root.mmd_root.material_morphs) > 0: - for morph in root.mmd_root.material_morphs: - mo = FnMorph(morph, rig) - mo.update_mat_related_mesh(active_mesh) - + for morph in root.mmd_root.material_morphs: + FnMorph(morph, rig).update_mat_related_mesh(active_mesh) utils.clearUnusedMeshes() return { 'FINISHED' } diff --git a/extern_tools/mmd_tools_local/panels/prop_bone.py b/extern_tools/mmd_tools_local/panels/prop_bone.py index 23b9bb33..241e8fef 100644 --- a/extern_tools/mmd_tools_local/panels/prop_bone.py +++ b/extern_tools/mmd_tools_local/panels/prop_bone.py @@ -16,6 +16,18 @@ class MMDBonePanel(Panel): def poll(cls, context): return context.active_bone + def __draw_ik_data(self, pose_bone): + bones = pose_bone.id_data.pose.bones + ik_bone_names = tuple(c.subtarget for c in pose_bone.constraints if c.type == 'IK' and c.subtarget in bones) + if ik_bone_names: + ik_custom_map = {getattr(b.constraints.get('mmd_ik_target_custom', None), 'subtarget', None) for b in bones if not b.is_mmd_shadow_bone} + row = self.layout.column(align=True) + for name in ik_bone_names: + if name in ik_custom_map: + row.prop(bones[name].mmd_bone, 'ik_rotation_constraint', text='IK Angle {%s}'%name) + else: + row.prop(pose_bone.mmd_bone, 'ik_rotation_constraint', text='IK Angle (%s)'%name) + def draw(self, context): pose_bone = context.active_pose_bone or \ context.active_object.pose.bones.get(context.active_bone.name, None) @@ -29,14 +41,17 @@ def draw(self, context): mmd_bone = pose_bone.mmd_bone - c = layout.column(align=True) + c = layout.column() + + row = c.row(align=True) + row.label(text='Information:') + if not mmd_bone.is_id_unique(): + row.label(icon='ERROR') + row.prop(mmd_bone, 'bone_id', text='ID') + c.prop(mmd_bone, 'name_j') c.prop(mmd_bone, 'name_e') - row = layout.row() - row.label(text='ID: %d'%(mmd_bone.bone_id)) - row.prop(pose_bone, 'mmd_ik_toggle') - c = layout.column(align=True) row = c.row() row.prop(mmd_bone, 'transform_order') @@ -45,10 +60,7 @@ def draw(self, context): row.prop(mmd_bone, 'is_controllable') row.prop(mmd_bone, 'is_tip') - c = layout.column(align=True) - row = c.row() - row.active = bool(next((i for i in pose_bone.constraints if i.type == 'IK'), None)) - row.prop(mmd_bone, 'ik_rotation_constraint') + self.__draw_ik_data(pose_bone) c = layout.column(align=True) row = c.row(align=True) diff --git a/extern_tools/mmd_tools_local/panels/prop_material.py b/extern_tools/mmd_tools_local/panels/prop_material.py index e8c7bdc2..36356f47 100644 --- a/extern_tools/mmd_tools_local/panels/prop_material.py +++ b/extern_tools/mmd_tools_local/panels/prop_material.py @@ -32,7 +32,13 @@ def draw(self, context): layout = self.layout col = layout.column() - col.label(text='Information:') + + row = col.row(align=True) + row.label(text='Information:') + if not mmd_material.is_id_unique(): + row.label(icon='ERROR') + row.prop(mmd_material, 'material_id', text='ID') + col.prop(mmd_material, 'name_j') col.prop(mmd_material, 'name_e') col.prop(mmd_material, 'comment') diff --git a/extern_tools/mmd_tools_local/panels/tool.py b/extern_tools/mmd_tools_local/panels/tool.py index c2015c59..2b0f38d5 100644 --- a/extern_tools/mmd_tools_local/panels/tool.py +++ b/extern_tools/mmd_tools_local/panels/tool.py @@ -313,8 +313,11 @@ def draw_item(self, context, layout, data, item, icon, active_data, active_propn class MMD_TOOLS_UL_MaterialMorphOffsets(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): if self.layout_type in {'DEFAULT'}: - material = item.material or 'All Materials' - layout.label(text=material, translate=False, icon='MATERIAL') + material = item.material + if not material and item.material_id >= 0: + layout.label(text='Material ID %d is missing'%item.material_id, translate=False, icon='ERROR') + else: + layout.label(text=material or 'All Materials', translate=False, icon='MATERIAL') elif self.layout_type in {'COMPACT'}: pass elif self.layout_type in {'GRID'}: diff --git a/extern_tools/mmd_tools_local/panels/util_tools.py b/extern_tools/mmd_tools_local/panels/util_tools.py index 949b5413..239d4679 100644 --- a/extern_tools/mmd_tools_local/panels/util_tools.py +++ b/extern_tools/mmd_tools_local/panels/util_tools.py @@ -72,18 +72,13 @@ def filter_items(self, context, data, propname): objects = getattr(data, propname) flt_flags = [~self.bitflag_filter_item] * len(objects) flt_neworder = list(range(len(objects))) - active_root = Model.findRoot(context.active_object) - #rig = Model(active_root) - #for i, obj in enumerate(objects): - # if (obj.type == 'MESH' and obj.mmd_type == 'NONE' - # and Model.findRoot(obj) == active_root): - # flt_flags[i] = self.bitflag_filter_item - # new_index = rig.getMeshIndex(obj.name) - # flt_neworder[i] = new_index + + armature = Model(Model.findRoot(context.active_object)).armature() + __is_child_of_armature = lambda x: x.parent and (x.parent == armature or __is_child_of_armature(x.parent)) + name_dict = {} for i, obj in enumerate(objects): - if (obj.type == 'MESH' and obj.mmd_type == 'NONE' - and Model.findRoot(obj) == active_root): + if obj.type == 'MESH' and obj.mmd_type == 'NONE' and __is_child_of_armature(obj): flt_flags[i] = self.bitflag_filter_item name_dict[obj.name] = i diff --git a/extern_tools/mmd_tools_local/panels/view_prop.py b/extern_tools/mmd_tools_local/panels/view_prop.py index 79961d15..7bc9a233 100644 --- a/extern_tools/mmd_tools_local/panels/view_prop.py +++ b/extern_tools/mmd_tools_local/panels/view_prop.py @@ -45,6 +45,28 @@ def draw(self, context): row = layout.row(align=True) row.prop(root.mmd_root, 'use_sdef', text='SDEF') + layout.prop(root.mmd_root, 'use_property_driver', text='Property Drivers', icon='DRIVER') + + self.__draw_IK_toggle(Model(root).armature() or root) + + def __draw_IK_toggle(self, armature): + bones = getattr(armature.pose, 'bones', ()) + ik_map = {bones[c.subtarget]:(b.bone, c.chain_count, not c.is_valid) for b in bones for c in b.constraints if c.type == 'IK' and c.subtarget in bones} + if ik_map: + base = sum(b.bone.length for b in ik_map.keys())/len(ik_map)*0.8 + groups = {} + for ik, (b, cnt, err) in ik_map.items(): + if any(all(x) for x in zip(ik.bone.layers, armature.data.layers)): + px, py, pz = -ik.bone.head_local/base + bx, by, bz = -b.head_local/base*0.15 + groups.setdefault((int(pz), int(bz), -cnt), set()).add(((px, -py, bx), ik)) # (px, pz, -py, bx, bz, -by) + layout = self.layout.box().column() + for _, group in sorted(groups.items()): + row = layout.row() + for _, ik in sorted(group, key=lambda x: x[0]): + ic = 'ERROR' if ik_map[ik][-1] else 'NONE' + row.prop(ik, 'mmd_ik_toggle', text=ik.name, toggle=True, icon=ic) + @register_wrap class MMDViewPanel(_PanelBase, Panel): bl_idname = 'OBJECT_PT_mmd_tools_view' diff --git a/extern_tools/mmd_tools_local/properties/bone.py b/extern_tools/mmd_tools_local/properties/bone.py index 2740f8d7..14aaab9a 100644 --- a/extern_tools/mmd_tools_local/properties/bone.py +++ b/extern_tools/mmd_tools_local/properties/bone.py @@ -55,7 +55,9 @@ class MMDBone(PropertyGroup): bone_id = IntProperty( name='Bone ID', + description='Unique ID for the reference of bone morph and rotate+/move+', default=-1, + min=-1, ) transform_order = IntProperty( @@ -176,35 +178,38 @@ class MMDBone(PropertyGroup): default=True ) + def is_id_unique(self): + return self.bone_id < 0 or not next((b for b in self.id_data.pose.bones if b.mmd_bone != self and b.mmd_bone.bone_id == self.bone_id), None) + def _mmd_ik_toggle_get(prop): return prop.get('mmd_ik_toggle', True) def _mmd_ik_toggle_set(prop, v): - #FIXME animation is not working well on Blender 2.8. Using driver is another way but it's troublesome. if v != prop.get('mmd_ik_toggle', None): prop['mmd_ik_toggle'] = v - #print('_mmd_ik_toggle_set', v, prop.name) - for b in prop.id_data.pose.bones: - for c in b.constraints: - if c.type == 'IK' and c.subtarget == prop.name: - #print(' ', b.name, c.name) - c.influence = v - __update_mmd_ik_chain(b if c.use_tail else b.parent, v, c.chain_count) - -def __update_mmd_ik_chain(bone, enable, chain_count): - for i in range(chain_count): - for c in bone.constraints: - if c.name.startswith('mmd_ik_limit'): - #print(' -', bone.name, c.name) - c.influence = enable - bone = bone.parent + _mmd_ik_toggle_update(prop, None) + +def _mmd_ik_toggle_update(prop, context): + v = prop.mmd_ik_toggle + #print('_mmd_ik_toggle_update', v, prop.name) + for b in prop.id_data.pose.bones: + for c in b.constraints: + if c.type == 'IK' and c.subtarget == prop.name: + #print(' ', b.name, c.name) + c.influence = v + b = b if c.use_tail else b.parent + for b in ([b]+b.parent_recursive)[:c.chain_count]: + c = next((c for c in b.constraints if c.type == 'LIMIT_ROTATION' and not c.mute), None) + if c: c.influence = v class _MMDPoseBoneProp: mmd_ik_toggle = BoolProperty( name='MMD IK Toggle', description='MMD IK toggle is used to import/export animation of IK on-off', - get=_mmd_ik_toggle_get, - set=_mmd_ik_toggle_set, + #get=_mmd_ik_toggle_get, + #set=_mmd_ik_toggle_set, + update=_mmd_ik_toggle_update, + default=True, ) diff --git a/extern_tools/mmd_tools_local/properties/material.py b/extern_tools/mmd_tools_local/properties/material.py index 13a4444c..1c26badf 100644 --- a/extern_tools/mmd_tools_local/properties/material.py +++ b/extern_tools/mmd_tools_local/properties/material.py @@ -92,7 +92,9 @@ class MMDMaterial(PropertyGroup): material_id = IntProperty( name='Material ID', - default=-1 + description='Unique ID for the reference of material morph', + default=-1, + min=-1, ) ambient_color = FloatVectorProperty( @@ -255,3 +257,6 @@ class MMDMaterial(PropertyGroup): description='Comment', ) + def is_id_unique(self): + return self.material_id < 0 or not next((m for m in bpy.data.materials if m.mmd_material != self and m.mmd_material.material_id == self.material_id), None) + diff --git a/extern_tools/mmd_tools_local/properties/root.py b/extern_tools/mmd_tools_local/properties/root.py index 0e9cabd7..5da71e85 100644 --- a/extern_tools/mmd_tools_local/properties/root.py +++ b/extern_tools/mmd_tools_local/properties/root.py @@ -17,6 +17,54 @@ from mmd_tools_local.properties.morph import GroupMorph import mmd_tools_local.core.model as mmd_model +def __driver_variables(id_data, path, index=-1): + d = id_data.driver_add(path, index) + variables = d.driver.variables + for x in variables: + variables.remove(x) + return d.driver, variables + +def __add_single_prop(variables, id_obj, data_path, prefix): + var = variables.new() + var.name = prefix + str(len(variables)) + var.type = 'SINGLE_PROP' + target = var.targets[0] + target.id_type = 'OBJECT' + target.id = id_obj + target.data_path = data_path + return var + +def _toggleUsePropertyDriver(self, context): + root = self.id_data + rig = mmd_model.Model(root) + bones = getattr((rig.armature() or root).pose, 'bones', ()) + ik_map = {bones[c.subtarget]:(b, c) for b in bones for c in b.constraints if c.type == 'IK' and c.is_valid and c.subtarget in bones} + prop_hide_viewport = 'hide_viewport' if hasattr(root, 'hide_viewport') else 'hide' + if self.use_property_driver: + for ik, (b, c) in ik_map.items(): + driver, variables = __driver_variables(c, 'influence') + driver.expression = '%s' % __add_single_prop(variables, ik.id_data, ik.path_from_id('mmd_ik_toggle'), 'use_ik').name + b = b if c.use_tail else b.parent + for b in ([b]+b.parent_recursive)[:c.chain_count]: + c = next((c for c in b.constraints if c.type == 'LIMIT_ROTATION' and not c.mute), None) + if c: + driver, variables = __driver_variables(c, 'influence') + driver.expression = '%s' % __add_single_prop(variables, ik.id_data, ik.path_from_id('mmd_ik_toggle'), 'use_ik').name + for i in rig.meshes(): + for prop_hide in (prop_hide_viewport, 'hide_render'): + driver, variables = __driver_variables(i, prop_hide) + driver.expression = 'not %s' % __add_single_prop(variables, root, 'mmd_root.show_meshes', 'show').name + else: + for ik, (b, c) in ik_map.items(): + c.driver_remove('influence') + b = b if c.use_tail else b.parent + for b in ([b]+b.parent_recursive)[:c.chain_count]: + c = next((c for c in b.constraints if c.type == 'LIMIT_ROTATION' and not c.mute), None) + if c: c.driver_remove('influence') + for i in rig.meshes(): + for prop_hide in (prop_hide_viewport, 'hide_render'): + i.driver_remove(prop_hide) + #=========================================== # Callback functions #=========================================== @@ -60,7 +108,6 @@ def _show_meshes_get(prop): return prop.get('show_meshes', True) def _show_meshes_set(prop, v): - #FIXME animation is not working well on Blender 2.8. Using driver is another way but it's troublesome. if v != prop.get('show_meshes', None): prop['show_meshes'] = v _toggleVisibilityOfMeshes(prop, bpy.context) @@ -269,9 +316,9 @@ class MMDRoot(PropertyGroup): show_meshes = BoolProperty( name='Show Meshes', description='Show all meshes of the MMD model', - get=_show_meshes_get, - set=_show_meshes_set, - #update=_toggleVisibilityOfMeshes, + #get=_show_meshes_get, + #set=_show_meshes_set, + update=_toggleVisibilityOfMeshes, ) show_rigid_bodies = BoolProperty( @@ -332,6 +379,13 @@ class MMDRoot(PropertyGroup): default=True, ) + use_property_driver = BoolProperty( + name='Use Property Driver', + description='Setup drivers for MMD property animation (Visibility and IK toggles)', + update=_toggleUsePropertyDriver, + default=False, + ) + is_built = BoolProperty( name='Is Built', ) From 92d583605787290395c61bcdda1077bddd4219ae Mon Sep 17 00:00:00 2001 From: Hotox Date: Wed, 22 Apr 2020 16:35:15 +0200 Subject: [PATCH 09/24] Added Separate by Copy Protection button (request, might not stay) --- README.md | 2 +- tools/armature_manual.py | 54 ++++++++++++++++++++++++++++++-- tools/common.py | 67 +++++++++++++++++++++++++++++++++++++--- ui/manual.py | 4 +++ 4 files changed, 120 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index f7f480a6..4490124f 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Join our Discord to report errors, suggestions and make comments! - If you have custom Python installed which Blender might use, you need to have Numpy installed ## Installation - - Download the plugin: [Cats Blender Plugin](https://github.com/michaeldegroot/cats-blender-plugin/archive/master.zip) + - Download the plugin: **[Cats Blender Plugin](https://github.com/michaeldegroot/cats-blender-plugin/archive/master.zip)** - **Important: Do NOT extract the downloaded zip!** - Install the addon in blender like so: - *This shows Blender 2.79. In Blender 2.80+ go to Edit > Preferences > Add-ons. Also you don't need to save the user settings there* diff --git a/tools/armature_manual.py b/tools/armature_manual.py index b0046087..0e5f1a0b 100644 --- a/tools/armature_manual.py +++ b/tools/armature_manual.py @@ -561,10 +561,60 @@ def execute(self, context): obj = meshes[0] obj_name = obj.name - Common.separate_by_shape_keys(context, obj) + done_message = 'Successfully separated by shape keys.' + if not Common.separate_by_shape_keys(context, obj): + done_message = 'No meshes had to be separated!' saved_data.load(ignore=[obj_name]) - self.report({'INFO'}, 'Successfully separated by shape keys.') + self.report({'INFO'}, done_message) + return {'FINISHED'} + + +@register_wrap +class SeparateByCopyProtection(bpy.types.Operator): + bl_idname = 'cats_manual.separate_by_copy_protection' + bl_label = 'Separate by Copy Protection' + bl_description = 'Separates selected mesh into two parts,' \ + '\ndepending on whether it is effected by the Cats Copy Protection or not.' \ + '\n' \ + '\nUseful if you have the Copy Protection enabled on multiple selected parts of your model' + bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + + @classmethod + def poll(cls, context): + obj = context.active_object + + if obj and obj.type == 'MESH': + return True + + meshes = Common.get_meshes_objects(check=False) + return meshes + + def execute(self, context): + saved_data = Common.SavedData() + obj = context.active_object + + if not obj or (obj and obj.type != 'MESH'): + Common.unselect_all() + meshes = Common.get_meshes_objects() + if len(meshes) == 0: + saved_data.load() + self.report({'ERROR'}, 'No meshes found!') + return {'FINISHED'} + if len(meshes) > 1: + saved_data.load() + self.report({'ERROR'}, 'Multiple meshes found!' + '\nPlease select the mesh you want to separate!') + return {'FINISHED'} + obj = meshes[0] + obj_name = obj.name + + done_message = 'Successfully separated by copy protection.' + if not Common.separate_by_cats_protection(context, obj): + done_message = 'No meshes had to be separated!' + + saved_data.load(ignore=[obj_name]) + self.report({'INFO'}, done_message) return {'FINISHED'} diff --git a/tools/common.py b/tools/common.py index c42e2a47..a8e7d78c 100644 --- a/tools/common.py +++ b/tools/common.py @@ -1000,10 +1000,68 @@ def separate_by_shape_keys(context, mesh): bpy.ops.mesh.select_all(action='DESELECT') switch('OBJECT') - for kb in mesh.data.shape_keys.key_blocks: - for i, (v0, v1) in enumerate(zip(kb.relative_key.data, kb.data)): - if v0.co != v1.co: - mesh.data.vertices[i].select = True + selected_count = 0 + max_count = 0 + if has_shapekeys(mesh): + for kb in mesh.data.shape_keys.key_blocks: + for i, (v0, v1) in enumerate(zip(kb.relative_key.data, kb.data)): + max_count += 1 + if v0.co != v1.co: + mesh.data.vertices[i].select = True + selected_count += 1 + + if not selected_count or selected_count == max_count: + return False + + switch('EDIT') + bpy.ops.mesh.select_all(action='INVERT') + + bpy.ops.mesh.separate(type='SELECTED') + + for ob in context.selected_objects: + if ob.type == 'MESH': + if ob != get_active(): + print('not active', ob.name) + active_tmp = get_active() + ob.name = ob.name.replace('.001', '') + '.no_shapes' + set_active(ob) + bpy.ops.object.shape_key_remove(all=True) + set_active(active_tmp) + select(ob, False) + else: + print('active', ob.name) + clean_shapekeys(ob) + switch('OBJECT') + + utils.clearUnusedMeshes() + + # Update the material list of the Material Combiner + update_material_list() + return True + + +def separate_by_cats_protection(context, mesh): + prepare_separation(mesh) + + switch('EDIT') + bpy.ops.mesh.select_mode(type="VERT") + bpy.ops.mesh.select_all(action='DESELECT') + + switch('OBJECT') + selected_count = 0 + max_count = 0 + if has_shapekeys(mesh): + for kb in mesh.data.shape_keys.key_blocks: + if kb.name == 'Basis Original': + for i, (v0, v1) in enumerate(zip(kb.relative_key.data, kb.data)): + max_count += 1 + if v0.co != v1.co: + mesh.data.vertices[i].select = True + selected_count += 1 + + if not selected_count or selected_count == max_count: + return False + switch('EDIT') bpy.ops.mesh.select_all(action='INVERT') @@ -1028,6 +1086,7 @@ def separate_by_shape_keys(context, mesh): # Update the material list of the Material Combiner update_material_list() + return True def prepare_separation(mesh): diff --git a/ui/manual.py b/ui/manual.py index 54f5e349..b940ad9d 100644 --- a/ui/manual.py +++ b/ui/manual.py @@ -122,3 +122,7 @@ def draw(self, context): row = col.row(align=True) row.scale_y = button_height row.operator(Armature_manual.FixVRMShapesButton.bl_idname, icon='SHAPEKEY_DATA') + + row = col.row(align=True) + row.scale_y = button_height + row.operator(Armature_manual.SeparateByCopyProtection.bl_idname, icon='SHAPEKEY_DATA') From de18ebe7ab8a9a71fcfe8a038d01a75916a2c744 Mon Sep 17 00:00:00 2001 From: Hotox Date: Mon, 27 Apr 2020 05:48:56 +0200 Subject: [PATCH 10/24] Added "Fix MMD Twist Bones" option to Fix Model --- README.md | 6 ++++- extentions.py | 16 ++++++++++-- resources/supporters.json | 2 +- tools/armature.py | 18 ++++++++++++++ tools/common.py | 52 ++++++++++++++++++++++++++++++++++++++- ui/armature.py | 2 ++ 6 files changed, 91 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4490124f..256efccf 100644 --- a/README.md +++ b/README.md @@ -304,7 +304,11 @@ It checks for a new version automatically once every day. - **Cats is now fully compatible with Blender 2.83!** - *It was compatible with 2.82 all long* - **Fix Model:** - - Added "Keep Twist Bones" option to Fix Model (experimental) + - Added "Keep Twist Bones" option to Fix Model + - This will keep any bone containing 'Twist' + - Added "Fix MMD Twist Bones" option to Fix Model + - This will apply a fix to make the MMD arm twist bones usable + - You do not need to enable "Keep Twist Bones" for this to work - Added compatibility to more models - **Importer:** - Fixed export warning being empty diff --git a/extentions.py b/extentions.py index ea2d50d3..361ea7d6 100644 --- a/extentions.py +++ b/extentions.py @@ -56,12 +56,24 @@ def register(): ) Scene.keep_twist_bones = BoolProperty( - name='Keep Twist Bones (Experimental)', - description="Saves twist bones from deletion. Currently only works on MMD models." + name='Keep Twist Bones', + description='This will keep any bone with "Twist" in the name.' + '\nSo if there are certain bones that you want to keep, you can add "Twist" to them and they won\'t get deleted.' '\n\nVRChat can now make use of twist bones, so you can use this option to keep them', default=False ) + Scene.fix_twist_bones = BoolProperty( + name='Fix MMD Twist Bones', + description='This will make MMD arm twist bones usable in VRChat.' + '\nWIll only work if the twist bones are properly named.' + '\nRequired names:' + '\n - ArmTwist[1-3]_[L/R]' + '\n - HandTwist[1-3]_[L/R]' + '\n\nYou don\'t need to enable "Keep Twist Bones" for this to work', + default=True + ) + Scene.join_meshes = BoolProperty( name='Join Meshes', description='Joins all meshes of this model together.' diff --git a/resources/supporters.json b/resources/supporters.json index f4fd9c72..4103b08c 100644 --- a/resources/supporters.json +++ b/resources/supporters.json @@ -283,7 +283,7 @@ "displayname": "Krisu.Miushy", "startdate": "2018-06-21", "description": "I'll support your creations, let's keep the hard work everyone!\n", - "website": "https://www.youtube.com/watch?v=8H5e3L_nXdY", + "website": "https://youtu.be/o7VLdHxHSII", "tier": 1 },{ "displayname": "Atirion", diff --git a/tools/armature.py b/tools/armature.py index 05150e21..0403052a 100644 --- a/tools/armature.py +++ b/tools/armature.py @@ -924,6 +924,12 @@ def add_eye_children(eye_bone, parent_name): if mod.type == 'ARMATURE': bpy.ops.object.modifier_remove(modifier=mod.name) + # Fix MMD twist bones + print('FIX TWIST BONES') + print(bones_to_delete) + Common.fix_twist_bones(mesh, bones_to_delete) + print(bones_to_delete) + # Add bones to parent reweight list for name in Bones.bone_reweigth_to_parent: if '\Left' in name or '\L' in name: @@ -945,6 +951,9 @@ def add_eye_children(eye_bone, parent_name): if context.scene.keep_twist_bones and 'twist' in bone_child.name.lower(): continue + if context.scene.fix_twist_bones and bone_child.name.lower() in ['handtwist_l', 'handtwist_r', 'armtwist_l', 'armtwist_r']: + print('TWIST FOUND!') + continue # search for next parent that is not in the "reweight to parent" list parent_in_list = True @@ -1020,6 +1029,9 @@ def add_eye_children(eye_bone, parent_name): if context.scene.keep_twist_bones and 'twist' in bone[1].lower(): continue + if context.scene.fix_twist_bones and bone[1].lower() in ['handtwist_l', 'handtwist_r', 'armtwist_l', 'armtwist_r']: + print('TWIST FOUND!') + continue # print(bone[1] + " to1 " + bone[0]) @@ -1077,6 +1089,9 @@ def add_eye_children(eye_bone, parent_name): if context.scene.keep_twist_bones and 'twist' in vg_from.name.lower(): continue + if context.scene.fix_twist_bones and vg_from.name.lower() in ['handtwist_l', 'handtwist_r', 'armtwist_l', 'armtwist_r']: + print('TWIST FOUND!') + continue bone_tmp = armature.data.bones.get(vg_from.name) if bone_tmp: @@ -1126,6 +1141,9 @@ def add_eye_children(eye_bone, parent_name): if key in armature.data.edit_bones and value in armature.data.edit_bones: armature.data.edit_bones.get(key).parent = armature.data.edit_bones.get(value) + # Fix MMD twist bone names + Common.fix_twist_bone_names(armature) + # Removes unused vertex groups Common.remove_unused_vertex_groups() diff --git a/tools/common.py b/tools/common.py index a8e7d78c..b0534890 100644 --- a/tools/common.py +++ b/tools/common.py @@ -1730,13 +1730,14 @@ def clean_material_names(mesh): mesh.active_material.name = mat.name[:-5] -def mix_weights(mesh, vg_from, vg_to, delete_old_vg=True): +def mix_weights(mesh, vg_from, vg_to, mix_strength=1.0, delete_old_vg=True): mesh.active_shape_key_index = 0 mod = mesh.modifiers.new("VertexWeightMix", 'VERTEX_WEIGHT_MIX') mod.vertex_group_a = vg_to mod.vertex_group_b = vg_from mod.mix_mode = 'ADD' mod.mix_set = 'B' + mod.mask_constant = mix_strength bpy.ops.object.modifier_apply(modifier=mod.name) if delete_old_vg: mesh.vertex_groups.remove(mesh.vertex_groups.get(vg_from)) @@ -2013,6 +2014,55 @@ def fix_vrm_shader(mesh): # continue +def fix_twist_bones(mesh, bones_to_delete): + # This will fix MMD twist bones + + for bone_type in ['Hand', 'Arm']: + for suffix in ['L', 'R']: + prefix = 'Left' if suffix == 'L' else 'Right' + bone_parent_name = prefix + ' ' + ('elbow' if bone_type == 'Hand' else 'arm') + + vg_twist = mesh.vertex_groups.get(bone_type + 'Twist_' + suffix) + vg_parent = mesh.vertex_groups.get(bone_parent_name) + + if not vg_twist: + print('1. no ' + bone_type + 'Twist_' + suffix) + continue + if not vg_parent: + print('2. no ' + bone_parent_name) + vg_parent = mesh.vertex_groups.new(name=bone_parent_name) + + vg_twist1 = mesh.vertex_groups.get(bone_type + 'Twist1_' + suffix) + vg_twist2 = mesh.vertex_groups.get(bone_type + 'Twist2_' + suffix) + vg_twist3 = mesh.vertex_groups.get(bone_type + 'Twist3_' + suffix) + + if vg_twist1: + mix_weights(mesh, vg_twist1.name, vg_twist.name, mix_strength=0.5, delete_old_vg=False) + mix_weights(mesh, vg_twist1.name, vg_parent.name, mix_strength=0.5) + bones_to_delete.append(vg_twist1.name) + + if vg_twist2: + mix_weights(mesh, vg_twist2.name, vg_twist.name, mix_strength=0.5, delete_old_vg=False) + mix_weights(mesh, vg_twist2.name, vg_parent.name, mix_strength=0.5) + bones_to_delete.append(vg_twist2.name) + + if vg_twist3: + mix_weights(mesh, vg_twist3.name, vg_twist.name, mix_strength=1.0) + bones_to_delete.append(vg_twist3.name) + + mix_weights(mesh, vg_twist.name, vg_parent.name, mix_strength=0.25, delete_old_vg=False) + + +def fix_twist_bone_names(armature): + # This will fix MMD twist bone names after the vertex groups have been fixed + + for bone_type in ['Hand', 'Arm']: + for suffix in ['L', 'R']: + bone_twist = armature.data.edit_bones.get(bone_type + 'Twist_' + suffix) + if bone_twist: + bone_twist.name = 'z' + bone_twist.name + + """ HTML <-> text conversions. diff --git a/ui/armature.py b/ui/armature.py index c2ef8ead..654e5e51 100644 --- a/ui/armature.py +++ b/ui/armature.py @@ -209,6 +209,8 @@ def draw(self, context): row = col.row(align=True) row.prop(context.scene, 'keep_twist_bones') row = col.row(align=True) + row.prop(context.scene, 'fix_twist_bones') + row = col.row(align=True) row.prop(context.scene, 'join_meshes') row = col.row(align=True) row.prop(context.scene, 'connect_bones') From 6586875624c64054e722f0aaf502ebc200c1ad57 Mon Sep 17 00:00:00 2001 From: Hotox Date: Tue, 28 Apr 2020 03:43:28 +0200 Subject: [PATCH 11/24] Fixed another model --- README.md | 2 +- tools/armature.py | 9 +++++++++ tools/armature_bones.py | 12 ++++++++---- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 256efccf..7d8e83f8 100644 --- a/README.md +++ b/README.md @@ -307,7 +307,7 @@ It checks for a new version automatically once every day. - Added "Keep Twist Bones" option to Fix Model - This will keep any bone containing 'Twist' - Added "Fix MMD Twist Bones" option to Fix Model - - This will apply a fix to make the MMD arm twist bones usable + - This will apply a fix to make the MMD arm twist bones usable **(Thanks Rokk!)** - You do not need to enable "Keep Twist Bones" for this to work - Added compatibility to more models - **Importer:** diff --git a/tools/armature.py b/tools/armature.py index 0403052a..2772ff3a 100644 --- a/tools/armature.py +++ b/tools/armature.py @@ -502,6 +502,10 @@ def execute(self, context): ('Joint_', ''), ('DEF_', ''), ] + # List of chars to replace if they are at the end of a bone name + ends_with = [ + ('_Bone', ''), + ] # Standardize names for bone in armature.data.edit_bones: @@ -530,6 +534,11 @@ def execute(self, context): if name.startswith(replacement[0]): name = replacement[1] + name[len(replacement[0]):] + # Replace if name ends with specified chars + for replacement in ends_with: + if name.endswith(replacement[0]): + name = name[:-len(replacement[0])] + replacement[1] + # Remove digits from the start name_split = name.split('_') if len(name_split) > 1 and name_split[0].isdigit(): diff --git a/tools/armature_bones.py b/tools/armature_bones.py index 8a644fed..34e9ee6b 100644 --- a/tools/armature_bones.py +++ b/tools/armature_bones.py @@ -319,8 +319,10 @@ # Replace '-' with '_' # Replace ' ' with '_' # Replace 'ValveBiped_' with '' -# Replace 'Bip01_' with 'Bip' -# Replace 'Bip001_' with 'Bip' +# Replace 'Bip1_' with 'Bip_' +# Replace 'Bip01_' with 'Bip_' +# Replace 'Bip001_' with 'Bip_' +# Replace '_Bone' with '' # # Replace New Bone Patterns: # Left/Right = \Left @@ -976,8 +978,10 @@ # Replace '-' with '_' # Replace ' ' with '_' # Replace 'ValveBiped_' with '' -# Replace 'Bip01_' with 'Bip' -# Replace 'Bip001_' with 'Bip' +# Replace 'Bip1_' with 'Bip_' +# Replace 'Bip01_' with 'Bip_' +# Replace 'Bip001_' with 'Bip_' +# Replace '_Bone' with '' # # Replace New Bone Patterns: # Left/Right = \Left From be14f8d20adb64edec1b34f66cc977b15d825982 Mon Sep 17 00:00:00 2001 From: Hotox Date: Tue, 28 Apr 2020 19:02:10 +0200 Subject: [PATCH 12/24] Fixed another model --- tools/armature.py | 22 ++++++++++++++-------- tools/armature_bones.py | 37 ++++++++++++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/tools/armature.py b/tools/armature.py index 2772ff3a..7198469f 100644 --- a/tools/armature.py +++ b/tools/armature.py @@ -505,6 +505,8 @@ def execute(self, context): # List of chars to replace if they are at the end of a bone name ends_with = [ ('_Bone', ''), + ('_Le', '_L'), + ('_Ri', '_R'), ] # Standardize names @@ -512,14 +514,7 @@ def execute(self, context): current_step += 1 wm.progress_update(current_step) - # Make all the underscores! - name = bone.name.replace(' ', '_')\ - .replace('-', '_')\ - .replace('.', '_')\ - .replace(':', '_')\ - .replace('____', '_')\ - .replace('___', '_')\ - .replace('__', '_')\ + name = bone.name # Always uppercase at the start and after an underscore upper_name = '' @@ -529,6 +524,17 @@ def execute(self, context): upper_name += s[:1].upper() + s[1:] name = upper_name + # Make all the underscores! And replace things! + name = name.replace(' ', '_')\ + .replace('-', '_')\ + .replace('.', '_')\ + .replace(':', '_')\ + .replace('____', '_')\ + .replace('___', '_')\ + .replace('__', '_')\ + .replace('_Le_', '_L_')\ + .replace('_Ri_', '_R_')\ + # Replace if name starts with specified chars for replacement in starts_with: if name.startswith(replacement[0]): diff --git a/tools/armature_bones.py b/tools/armature_bones.py index 34e9ee6b..b336c2d8 100644 --- a/tools/armature_bones.py +++ b/tools/armature_bones.py @@ -192,6 +192,8 @@ 'Knee7_2_\L', 'AbdomenLower', 'ChestUpper', + 'Back_Low', + 'Back_Mid', ] bone_list_conflicting_names = [ (['\L_Clavicle'], '\L_Shoulder', 'Arm_\L'), @@ -431,6 +433,9 @@ 'Spine_Upper_1', 'Spine_Upper_2', + 'J_SpineLower' + 'J_SpineUpper' + 'Abdomen', 'Spine0', @@ -472,6 +477,7 @@ 'J_Spine1', 'J_Spine2', 'J_Spine3', + 'J_Spine4', 'Spine_Jnt_01', 'Spine_Jnt_02', @@ -627,6 +633,7 @@ 'J_Sako_\L', '\L_ShoulderPad', 'Collarbone_\L', + 'J_Clavicle_\L', ] bone_rename['\Left arm'] = [ '\Left_Arm', @@ -673,7 +680,8 @@ '\L_Shldr', '\LShldrBend', 'Arm_Stretch_\L', - 'J_Ude_A_\L' + 'J_Ude_A_\L', + 'J_Shoulder_\L', ] bone_rename['Left arm'] = [ '+_Leisure_Elder_Supplement', @@ -724,6 +732,7 @@ '\LForearmBend', 'Forearm_Stretch_\L', 'J_Ude_B_\L', + 'J_Elbow_\L', ] bone_rename['\Left wrist'] = [ '\Left_Wrist', @@ -755,7 +764,8 @@ 'J_\L_Wrist', '\L_Wrist', '\LWrist', - 'J_Te_\L' + 'J_Te_\L', + 'J_Wrist_\L', ] bone_rename['Left wrist'] = [ 'Left_Hand_003', @@ -815,7 +825,8 @@ 'Upperleg01_\L', '\LThighBend', 'Thigh_Stretch_\L', - 'J_Asi_A_\L' + 'J_Asi_A_\L', + 'J_Hip_\L', ] bone_rename['\Left knee'] = [ '\Left_Knee', @@ -862,6 +873,7 @@ 'Lowerleg01_\L', 'Leg_Stretch_\L', 'J_Asi_B_\L', + 'J_Knee_\L', ] bone_rename['\Left ankle'] = [ '\Left_Ankle', @@ -896,6 +908,7 @@ 'J_\L_Foot', '\LAnkle', 'J_Asi_D_\L', + 'J_Ankle_\L', ] bone_rename['\Left toe'] = [ '\Left_Toe', @@ -939,6 +952,7 @@ 'Toe_Boot_\L', 'Toes_01_\L', 'J_Asi_E_\L', + 'J_Ball_\L', ] bone_rename['Eye_\L'] = [ '\Left_Eye', @@ -1368,6 +1382,8 @@ '\LCarpal3', '\LCarpal4', 'Arm_\Left_Fist', + 'J_Pinkybase_\L', + 'J_Ringbase_\L', ] bone_reweight['Left wrist'] = [ 'Left_Hand_002', @@ -1718,6 +1734,7 @@ 'Bip_FThumb01_\L', 'Arm_\Left_Finger_1a', 'J_Oya_A_\L', + 'J_Thumb_\L_1', ] bone_rename_fingers['Thumb1_\L'] = [ # 'Arm_\Left_Finger_1b', @@ -1750,6 +1767,7 @@ 'Bip_FThumb02_\L', 'Arm_\Left_Finger_1b', 'J_Oya_B_\L', + 'J_Thumb_\L_2', ] bone_rename_fingers['Thumb2_\L'] = [ # 'Arm_\Left_Finger_1c', @@ -1781,6 +1799,7 @@ 'Bip_FThumb03_\L', 'Arm_\Left_Finger_1c', 'J_Oya_C_\L', + 'J_Thumb_\L_3', ] bone_rename_fingers['IndexFinger1_\L'] = [ 'Fore1_\L', @@ -1815,6 +1834,7 @@ 'Bip_FIndex00_\L', 'Arm_\Left_Finger_2a', 'J_Hito_A_\L', + 'J_Index_\L_1', ] bone_rename_fingers['IndexFinger2_\L'] = [ 'Fore2_\L', @@ -1849,6 +1869,7 @@ 'Bip_FIndex01_\L', 'Arm_\Left_Finger_2b', 'J_Hito_B_\L', + 'J_Index_\L_2', ] bone_rename_fingers['IndexFinger3_\L'] = [ 'Fore3_\L', @@ -1884,6 +1905,7 @@ 'Bip_FIndex02_\L', 'Arm_\Left_Finger_2c', 'J_Hito_C_\L', + 'J_Index_\L_3', ] bone_rename_fingers['MiddleFinger1_\L'] = [ 'Middle1_\L', @@ -1919,6 +1941,7 @@ 'Bip_FMiddle00_\L', 'Arm_\Left_Finger_3a', 'J_Naka_A_\L', + 'J_Mid_\L_1', ] bone_rename_fingers['MiddleFinger2_\L'] = [ 'Middle2_\L', @@ -1953,6 +1976,7 @@ 'Bip_FMiddle01_\L', 'Arm_\Left_Finger_3b', 'J_Naka_B_\L', + 'J_Mid_\L_2', ] bone_rename_fingers['MiddleFinger3_\L'] = [ 'Middle3_\L', @@ -1987,6 +2011,7 @@ 'Bip_FMiddle02_\L', 'Arm_\Left_Finger_3c', 'J_Naka_C_\L', + 'J_Mid_\L_3', ] bone_rename_fingers['RingFinger1_\L'] = [ 'Third1_\L', @@ -2022,6 +2047,7 @@ 'Bip_FRing00_\L', 'Arm_\Left_Finger_4a', 'J_Kusu_A_\L', + 'J_Ring_\L_1', ] bone_rename_fingers['RingFinger2_\L'] = [ 'Third2_\L', @@ -2057,6 +2083,7 @@ 'Bip_FRing01_\L', 'Arm_\Left_Finger_4b', 'J_Kusu_B_\L', + 'J_Ring_\L_2', ] bone_rename_fingers['RingFinger3_\L'] = [ 'Third3_\L', @@ -2092,6 +2119,7 @@ 'Bip_FRing02_\L', 'Arm_\Left_Finger_4c', 'J_Kusu_C_\L', + 'J_Ring_\L_3', ] bone_rename_fingers['LittleFinger1_\L'] = [ 'Little1_\L', @@ -2128,6 +2156,7 @@ 'Bip_FPinky00_\L', 'Arm_\Left_Finger_5a', 'J_Ko_A_\L', + 'J_Pinky_\L_1', ] bone_rename_fingers['LittleFinger2_\L'] = [ 'Little2_\L', @@ -2164,6 +2193,7 @@ 'Bip_FPinky01_\L', 'Arm_\Left_Finger_5b', 'J_Ko_B_\L', + 'J_Pinky_\L_2', ] bone_rename_fingers['LittleFinger3_\L'] = [ 'Little3_\L', @@ -2200,4 +2230,5 @@ 'Bip_FPinky02_\L', 'Arm_\Left_Finger_5c', 'J_Ko_C_\L', + 'J_Pinky_\L_3', ] From d8608bd54a207ce9187d544b24a2131ecba2ffd4 Mon Sep 17 00:00:00 2001 From: Hotox Date: Wed, 29 Apr 2020 03:33:52 +0200 Subject: [PATCH 13/24] Changed twist bone mix values --- tools/common.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tools/common.py b/tools/common.py index b0534890..9ce90ff3 100644 --- a/tools/common.py +++ b/tools/common.py @@ -1730,12 +1730,12 @@ def clean_material_names(mesh): mesh.active_material.name = mat.name[:-5] -def mix_weights(mesh, vg_from, vg_to, mix_strength=1.0, delete_old_vg=True): +def mix_weights(mesh, vg_from, vg_to, mix_strength=1.0, mix_mode='ADD', delete_old_vg=True): mesh.active_shape_key_index = 0 mod = mesh.modifiers.new("VertexWeightMix", 'VERTEX_WEIGHT_MIX') mod.vertex_group_a = vg_to mod.vertex_group_b = vg_from - mod.mix_mode = 'ADD' + mod.mix_mode = mix_mode mod.mix_set = 'B' mod.mask_constant = mix_strength bpy.ops.object.modifier_apply(modifier=mod.name) @@ -2037,8 +2037,8 @@ def fix_twist_bones(mesh, bones_to_delete): vg_twist3 = mesh.vertex_groups.get(bone_type + 'Twist3_' + suffix) if vg_twist1: - mix_weights(mesh, vg_twist1.name, vg_twist.name, mix_strength=0.5, delete_old_vg=False) - mix_weights(mesh, vg_twist1.name, vg_parent.name, mix_strength=0.5) + mix_weights(mesh, vg_twist1.name, vg_twist.name, mix_strength=0.25, delete_old_vg=False) + mix_weights(mesh, vg_twist1.name, vg_parent.name, mix_strength=0.75) bones_to_delete.append(vg_twist1.name) if vg_twist2: @@ -2047,10 +2047,12 @@ def fix_twist_bones(mesh, bones_to_delete): bones_to_delete.append(vg_twist2.name) if vg_twist3: - mix_weights(mesh, vg_twist3.name, vg_twist.name, mix_strength=1.0) + mix_weights(mesh, vg_twist3.name, vg_twist.name, mix_strength=0.75, delete_old_vg=False) + mix_weights(mesh, vg_twist3.name, vg_parent.name, mix_strength=0.25) bones_to_delete.append(vg_twist3.name) - mix_weights(mesh, vg_twist.name, vg_parent.name, mix_strength=0.25, delete_old_vg=False) + # mix_weights(mesh, vg_twist.name, vg_parent.name, mix_strength=0.25, delete_old_vg=False) + # mix_weights(mesh, vg_twist.name, vg_twist.name, mix_strength=0.25, mix_mode='SUB', delete_old_vg=False) def fix_twist_bone_names(armature): From 4c4cde934d090be14d716095a41e8d13d9664210 Mon Sep 17 00:00:00 2001 From: Hotox Date: Wed, 29 Apr 2020 15:24:51 +0200 Subject: [PATCH 14/24] Adjusted twist bone mix values again --- tools/common.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/common.py b/tools/common.py index 9ce90ff3..3ea9e1de 100644 --- a/tools/common.py +++ b/tools/common.py @@ -2036,6 +2036,9 @@ def fix_twist_bones(mesh, bones_to_delete): vg_twist2 = mesh.vertex_groups.get(bone_type + 'Twist2_' + suffix) vg_twist3 = mesh.vertex_groups.get(bone_type + 'Twist3_' + suffix) + mix_weights(mesh, vg_twist.name, vg_parent.name, mix_strength=0.2, delete_old_vg=False) + mix_weights(mesh, vg_twist.name, vg_twist.name, mix_strength=0.2, mix_mode='SUB', delete_old_vg=False) + if vg_twist1: mix_weights(mesh, vg_twist1.name, vg_twist.name, mix_strength=0.25, delete_old_vg=False) mix_weights(mesh, vg_twist1.name, vg_parent.name, mix_strength=0.75) @@ -2051,9 +2054,6 @@ def fix_twist_bones(mesh, bones_to_delete): mix_weights(mesh, vg_twist3.name, vg_parent.name, mix_strength=0.25) bones_to_delete.append(vg_twist3.name) - # mix_weights(mesh, vg_twist.name, vg_parent.name, mix_strength=0.25, delete_old_vg=False) - # mix_weights(mesh, vg_twist.name, vg_twist.name, mix_strength=0.25, mix_mode='SUB', delete_old_vg=False) - def fix_twist_bone_names(armature): # This will fix MMD twist bone names after the vertex groups have been fixed From c434194933253dbe3f60a580413d6557c76aa705 Mon Sep 17 00:00:00 2001 From: Hotox Date: Thu, 30 Apr 2020 05:21:03 +0200 Subject: [PATCH 15/24] Imported meshes from VRM files now get automatically parented to their armature --- README.md | 1 + tools/armature.py | 4 +++- tools/importer.py | 16 ++++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7d8e83f8..8d0f4fec 100644 --- a/README.md +++ b/README.md @@ -311,6 +311,7 @@ It checks for a new version automatically once every day. - You do not need to enable "Keep Twist Bones" for this to work - Added compatibility to more models - **Importer:** + - Imported meshes from VRM files now get automatically parented to their armature - Fixed export warning being empty - Fixed importer error when the FBX importer was not enabled - Fixed importer error when a zip file contained another zip file diff --git a/tools/armature.py b/tools/armature.py index 7198469f..9d5b8d74 100644 --- a/tools/armature.py +++ b/tools/armature.py @@ -91,7 +91,9 @@ def execute(self, context): if mesh.name.endswith(('.baked', '.baked0')): is_vrm = True # TODO if not is_vrm: - self.report({'ERROR'}, 'No mesh inside the armature found!') + Common.show_error(3.8, ['No mesh inside the armature found!', + 'If there are meshes outside of the armature,', + 'set the armature as the parent of the meshes.']) return {'CANCELLED'} print('\nFixing Model:\n') diff --git a/tools/importer.py b/tools/importer.py index be8e993e..27f0d44b 100644 --- a/tools/importer.py +++ b/tools/importer.py @@ -215,13 +215,29 @@ def import_file(directory, file_name): # VRM elif file_ending == 'vrm': + pre_import_armatures = [obj for obj in bpy.data.objects if obj.type == 'ARMATURE'] + pre_import_meshes = [obj for obj in bpy.data.objects if obj.type == 'MESH'] + try: bpy.ops.import_scene.vrm('EXEC_DEFAULT', filepath=file_path) except (TypeError, ValueError): bpy.ops.import_scene.vrm('INVOKE_DEFAULT') + return except AttributeError: bpy.ops.cats_importer.install_vrm('INVOKE_DEFAULT') + return + + post_import_armatures = [obj for obj in bpy.data.objects if obj.type == 'ARMATURE' and obj not in pre_import_armatures] + post_import_meshes = [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj not in pre_import_meshes] + + if len(post_import_armatures) != 1: + return + + # Set imported vrm armature as the parent for the imported meshes + post_import_armature = post_import_armatures[0] + for mesh in post_import_meshes: + mesh.parent = post_import_armature # DAE elif file_ending == 'dae': From fe0ef05c6fe27ce83fd36c1fc8500521ba1b95c3 Mon Sep 17 00:00:00 2001 From: Hotox Date: Sat, 2 May 2020 02:04:19 +0200 Subject: [PATCH 16/24] Fixed another model --- tools/armature.py | 1 + tools/armature_bones.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/tools/armature.py b/tools/armature.py index 9d5b8d74..e0cab387 100644 --- a/tools/armature.py +++ b/tools/armature.py @@ -503,6 +503,7 @@ def execute(self, context): ('G_', ''), ('Joint_', ''), ('DEF_', ''), + ('Chr_', ''), ] # List of chars to replace if they are at the end of a bone name ends_with = [ diff --git a/tools/armature_bones.py b/tools/armature_bones.py index b336c2d8..171e6c54 100644 --- a/tools/armature_bones.py +++ b/tools/armature_bones.py @@ -827,6 +827,7 @@ 'Thigh_Stretch_\L', 'J_Asi_A_\L', 'J_Hip_\L', + '\LUpLeg', ] bone_rename['\Left knee'] = [ '\Left_Knee', @@ -909,6 +910,7 @@ '\LAnkle', 'J_Asi_D_\L', 'J_Ankle_\L', + '\LFoot', ] bone_rename['\Left toe'] = [ '\Left_Toe', @@ -953,6 +955,7 @@ 'Toes_01_\L', 'J_Asi_E_\L', 'J_Ball_\L', + '\LToeBase', ] bone_rename['Eye_\L'] = [ '\Left_Eye', @@ -1735,6 +1738,7 @@ 'Arm_\Left_Finger_1a', 'J_Oya_A_\L', 'J_Thumb_\L_1', + '\LHandThumb', ] bone_rename_fingers['Thumb1_\L'] = [ # 'Arm_\Left_Finger_1b', @@ -1768,6 +1772,7 @@ 'Arm_\Left_Finger_1b', 'J_Oya_B_\L', 'J_Thumb_\L_2', + '\LHandThumb1', ] bone_rename_fingers['Thumb2_\L'] = [ # 'Arm_\Left_Finger_1c', @@ -1800,6 +1805,7 @@ 'Arm_\Left_Finger_1c', 'J_Oya_C_\L', 'J_Thumb_\L_3', + '\LHandThumb2', ] bone_rename_fingers['IndexFinger1_\L'] = [ 'Fore1_\L', @@ -1835,6 +1841,7 @@ 'Arm_\Left_Finger_2a', 'J_Hito_A_\L', 'J_Index_\L_1', + '\LHandIndex', ] bone_rename_fingers['IndexFinger2_\L'] = [ 'Fore2_\L', @@ -1870,6 +1877,7 @@ 'Arm_\Left_Finger_2b', 'J_Hito_B_\L', 'J_Index_\L_2', + '\LHandIndex1', ] bone_rename_fingers['IndexFinger3_\L'] = [ 'Fore3_\L', @@ -1906,6 +1914,7 @@ 'Arm_\Left_Finger_2c', 'J_Hito_C_\L', 'J_Index_\L_3', + '\LHandIndex2', ] bone_rename_fingers['MiddleFinger1_\L'] = [ 'Middle1_\L', @@ -1942,6 +1951,7 @@ 'Arm_\Left_Finger_3a', 'J_Naka_A_\L', 'J_Mid_\L_1', + '\LHandMiddle', ] bone_rename_fingers['MiddleFinger2_\L'] = [ 'Middle2_\L', @@ -1977,6 +1987,7 @@ 'Arm_\Left_Finger_3b', 'J_Naka_B_\L', 'J_Mid_\L_2', + '\LHandMiddle1', ] bone_rename_fingers['MiddleFinger3_\L'] = [ 'Middle3_\L', @@ -2012,6 +2023,7 @@ 'Arm_\Left_Finger_3c', 'J_Naka_C_\L', 'J_Mid_\L_3', + '\LHandMiddle2', ] bone_rename_fingers['RingFinger1_\L'] = [ 'Third1_\L', @@ -2048,6 +2060,7 @@ 'Arm_\Left_Finger_4a', 'J_Kusu_A_\L', 'J_Ring_\L_1', + '\LHandRing', ] bone_rename_fingers['RingFinger2_\L'] = [ 'Third2_\L', @@ -2084,6 +2097,7 @@ 'Arm_\Left_Finger_4b', 'J_Kusu_B_\L', 'J_Ring_\L_2', + '\LHandRing1', ] bone_rename_fingers['RingFinger3_\L'] = [ 'Third3_\L', @@ -2120,6 +2134,7 @@ 'Arm_\Left_Finger_4c', 'J_Kusu_C_\L', 'J_Ring_\L_3', + '\LHandRing2', ] bone_rename_fingers['LittleFinger1_\L'] = [ 'Little1_\L', @@ -2157,6 +2172,7 @@ 'Arm_\Left_Finger_5a', 'J_Ko_A_\L', 'J_Pinky_\L_1', + '\LHandPinky', ] bone_rename_fingers['LittleFinger2_\L'] = [ 'Little2_\L', @@ -2194,6 +2210,7 @@ 'Arm_\Left_Finger_5b', 'J_Ko_B_\L', 'J_Pinky_\L_2', + '\LHandPinky1', ] bone_rename_fingers['LittleFinger3_\L'] = [ 'Little3_\L', @@ -2231,4 +2248,5 @@ 'Arm_\Left_Finger_5c', 'J_Ko_C_\L', 'J_Pinky_\L_3', + '\LHandPinky2', ] From 39f53da0f3de20888550f4eee65b12106e0fecb8 Mon Sep 17 00:00:00 2001 From: Hotox Date: Thu, 7 May 2020 13:32:48 +0200 Subject: [PATCH 17/24] Added "Remove Zero Weight Bones" option to Merge Armatures --- README.md | 3 +++ extentions.py | 11 +++++++++-- tools/armature.py | 8 ++++---- tools/armature_custom.py | 16 +++++++++------- ui/custom.py | 4 ++++ 5 files changed, 29 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 8d0f4fec..69014bdc 100644 --- a/README.md +++ b/README.md @@ -310,12 +310,15 @@ It checks for a new version automatically once every day. - This will apply a fix to make the MMD arm twist bones usable **(Thanks Rokk!)** - You do not need to enable "Keep Twist Bones" for this to work - Added compatibility to more models + - Disabling the option "Remove Zero Weight Bones" now also keeps unused vertex groups - **Importer:** - Imported meshes from VRM files now get automatically parented to their armature - Fixed export warning being empty - Fixed importer error when the FBX importer was not enabled - Fixed importer error when a zip file contained another zip file - When importing a model, objects of a new scene now only get deleted if all three of them are present +- **Custom Model Creation:** + - Added "Remove Zero Weight Bones" option to Merge Armatures - **General:** - Fixed objects getting unhidden when doing any cats operation in 2.80+ - Updated mmd_tools diff --git a/extentions.py b/extentions.py index 361ea7d6..a6d3ad06 100644 --- a/extentions.py +++ b/extentions.py @@ -41,8 +41,8 @@ def register(): Scene.remove_zero_weight = BoolProperty( name='Remove Zero Weight Bones', - description="Cleans up the bones hierarchy, deleting all bones that don't directly affect any vertices.\n" - 'Uncheck this if bones you want to keep got deleted', + description="Cleans up the bones hierarchy, deleting all bones that don't directly affect any vertices." + '\nUncheck this if bones or vertex groups that you want to keep got deleted', default=True ) @@ -175,6 +175,13 @@ def register(): default=True ) + Scene.merge_armatures_remove_zero_weight_bones = BoolProperty( + name='Remove Zero Weight Bones', + description="Cleans up the bones hierarchy, deleting all bones that don't directly affect any vertices." + '\nUncheck this if bones or vertex groups that you want to keep got deleted', + default=True + ) + # Decimation Scene.decimation_mode = EnumProperty( name="Decimation Mode", diff --git a/tools/armature.py b/tools/armature.py index e0cab387..2a27fb3c 100644 --- a/tools/armature.py +++ b/tools/armature.py @@ -1162,11 +1162,11 @@ def add_eye_children(eye_bone, parent_name): # Fix MMD twist bone names Common.fix_twist_bone_names(armature) - # Removes unused vertex groups - Common.remove_unused_vertex_groups() - - # Zero weight bones should be deleted if context.scene.remove_zero_weight: + # Removes unused vertex groups + Common.remove_unused_vertex_groups() + + # Zero weight bones should be deleted Common.delete_zero_weight() # Connect all bones with their children if they have exactly one diff --git a/tools/armature_custom.py b/tools/armature_custom.py index 95c8f63c..3fb9ca3e 100644 --- a/tools/armature_custom.py +++ b/tools/armature_custom.py @@ -385,9 +385,10 @@ def merge_armatures(base_armature_name, merge_armature_name, mesh_only, mesh_nam Common.set_default_stage() if not mesh_only: Common.delete_bone_constraints(armature_name=base_armature_name) - Common.remove_unused_vertex_groups(ignore_main_bones=True) - if Common.get_meshes_objects(armature_name=base_armature_name): - Common.delete_zero_weight(armature_name=base_armature_name, ignore=root_name) + if bpy.context.scene.merge_armatures_remove_zero_weight_bones: + Common.remove_unused_vertex_groups(ignore_main_bones=True) + if Common.get_meshes_objects(armature_name=base_armature_name): + Common.delete_zero_weight(armature_name=base_armature_name, ignore=root_name) Common.set_default_stage() # Merge bones into existing bones @@ -458,10 +459,11 @@ def merge_armatures(base_armature_name, merge_armature_name, mesh_only, mesh_nam # Remove all unused bones, constraints and vertex groups Common.set_default_stage() if not mesh_only: - Common.remove_unused_vertex_groups() - if Common.get_meshes_objects(armature_name=base_armature_name): - Common.delete_zero_weight(armature_name=base_armature_name, ignore=root_name) - Common.set_default_stage() + if bpy.context.scene.merge_armatures_remove_zero_weight_bones: + Common.remove_unused_vertex_groups() + if Common.get_meshes_objects(armature_name=base_armature_name): + Common.delete_zero_weight(armature_name=base_armature_name, ignore=root_name) + Common.set_default_stage() # Fix armature name Common.fix_armature_names(armature_name=base_armature_name) diff --git a/ui/custom.py b/ui/custom.py index f3190c7e..5b46e0cd 100644 --- a/ui/custom.py +++ b/ui/custom.py @@ -53,6 +53,10 @@ def draw(self, context): row.scale_y = 0.95 row.prop(context.scene, 'merge_armatures_join_meshes') + row = col.row(align=True) + row.scale_y = 0.95 + row.prop(context.scene, 'merge_armatures_remove_zero_weight_bones') + row = col.row(align=True) row.scale_y = 1.05 row.prop(context.scene, 'merge_armature_into', text='Base', icon=globs.ICON_MOD_ARMATURE) From 3b54d04d1e06ea43aeee5408b9adf6425e9d01dc Mon Sep 17 00:00:00 2001 From: Hotox Date: Thu, 7 May 2020 20:08:09 +0200 Subject: [PATCH 18/24] Fixed a Decimation bug and added option to Remove Doubles --- README.md | 2 ++ extentions.py | 6 ++++++ tools/common.py | 2 +- tools/decimation.py | 14 +++++++++++--- ui/decimation.py | 2 ++ 5 files changed, 22 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 69014bdc..6e81723a 100644 --- a/README.md +++ b/README.md @@ -319,6 +319,8 @@ It checks for a new version automatically once every day. - When importing a model, objects of a new scene now only get deleted if all three of them are present - **Custom Model Creation:** - Added "Remove Zero Weight Bones" option to Merge Armatures +- **Decimation:** + - Added "Remove Doubles" option - **General:** - Fixed objects getting unhidden when doing any cats operation in 2.80+ - Updated mmd_tools diff --git a/extentions.py b/extentions.py index a6d3ad06..74fd2136 100644 --- a/extentions.py +++ b/extentions.py @@ -262,6 +262,12 @@ def register(): "LittleFinger(1-3)_(L/R)" ) + Scene.decimation_remove_doubles = BoolProperty( + name="Remove Doubles", + description="Uncheck this if you got issues with with this checked", + default=True + ) + Scene.max_tris = IntProperty( name='Tris', description="The target amount of tris after decimation", diff --git a/tools/common.py b/tools/common.py index 3ea9e1de..b4f3d8c7 100644 --- a/tools/common.py +++ b/tools/common.py @@ -484,7 +484,7 @@ def get_meshes_decimation(self, context): for object in bpy.context.scene.objects: if object.type == 'MESH': - if object.parent is not None and object.parent.type == 'ARMATURE' and object.parent.name == bpy.context.scene.armature: + if object.parent and object.parent.type == 'ARMATURE' and object.parent.name == bpy.context.scene.armature: if object.name in Decimation.ignore_meshes: continue # 1. Will be returned by context.scene diff --git a/tools/decimation.py b/tools/decimation.py index 303d6321..7f8f5e24 100644 --- a/tools/decimation.py +++ b/tools/decimation.py @@ -74,12 +74,11 @@ class AddShapeButton(bpy.types.Operator): def poll(cls, context): if context.scene.add_shape_key == "": return False - return True def execute(self, context): shape = context.scene.add_shape_key - shapes = Common.get_shapekeys_decimation_list(self, context) + shapes = [x[0] for x in Common.get_shapekeys_decimation_list(self, context)] count = len(shapes) if count > 1 and shapes.index(shape) == count - 1: @@ -107,6 +106,14 @@ def poll(cls, context): def execute(self, context): ignore_meshes.append(context.scene.add_mesh) + for obj in bpy.context.scene.objects: + if obj.type == 'MESH': + if obj.parent and obj.parent.type == 'ARMATURE' and obj.parent.name == bpy.context.scene.armature: + if obj.name in ignore_meshes: + continue + context.scene.add_mesh = obj.name + break + return {'FINISHED'} @@ -189,7 +196,8 @@ def decimate(self, context): Common.switch('EDIT') bpy.ops.mesh.quads_convert_to_tris(quad_method='BEAUTY', ngon_method='BEAUTY') Common.switch('OBJECT') - Common.remove_doubles(mesh, 0.00001, save_shapes=True) + if context.scene.decimation_remove_doubles: + Common.remove_doubles(mesh, 0.00001, save_shapes=True) current_tris_count += len(mesh.data.polygons) if save_fingers: diff --git a/ui/decimation.py b/ui/decimation.py index 6e9531e3..882343f8 100644 --- a/ui/decimation.py +++ b/ui/decimation.py @@ -130,6 +130,8 @@ def draw(self, context): row = col.row(align=True) row.prop(context.scene, 'decimate_fingers') row = col.row(align=True) + row.prop(context.scene, 'decimation_remove_doubles') + row = col.row(align=True) row.prop(context.scene, 'max_tris') col.separator() row = col.row(align=True) From 7c8a1497e4e663b17ef0808c44e14d40e3c8e019 Mon Sep 17 00:00:00 2001 From: Hotox Date: Mon, 11 May 2020 20:19:38 +0200 Subject: [PATCH 19/24] Updated mmd_tools, ready for 0.17.0 --- README.md | 9 +-- __init__.py | 6 +- extentions.py | 2 +- extern_tools/mmd_tools_local/bpyutils.py | 19 ++++-- extern_tools/mmd_tools_local/core/bone.py | 18 +++--- extern_tools/mmd_tools_local/core/model.py | 21 ++----- extern_tools/mmd_tools_local/core/sdef.py | 68 ++++++++++++++-------- resources/supporters.json | 3 + ui/manual.py | 6 +- 9 files changed, 85 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index 6e81723a..f2e43f42 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Cats Blender Plugin (0.16.2) +# Cats Blender Plugin (0.17.0) A tool designed to shorten steps needed to import and optimize models into VRChat. Compatible models are: MMD, XNALara, Mixamo, Source Engine, Unreal Engine, DAZ/Poser, Blender Rigify, Sims 2, Motion Builder, 3DS Max and potentially more @@ -41,9 +41,9 @@ Join our Discord to report errors, suggestions and make comments! ## Installation - Download the plugin: **[Cats Blender Plugin](https://github.com/michaeldegroot/cats-blender-plugin/archive/master.zip)** - - **Important: Do NOT extract the downloaded zip!** + - **Important: Do NOT extract the downloaded zip! You will need the zip file during installation!** - Install the addon in blender like so: - - *This shows Blender 2.79. In Blender 2.80+ go to Edit > Preferences > Add-ons. Also you don't need to save the user settings there* + - *This shows Blender 2.79. In Blender 2.80+ go to Edit > Preferences > Add-ons. Also you don't need to save the user settings there.* ![](https://i.imgur.com/eZV1zrs.gif) @@ -300,7 +300,7 @@ It checks for a new version automatically once every day. ## Changelog -#### 0.16.2 +#### 0.17.0 - **Cats is now fully compatible with Blender 2.83!** - *It was compatible with 2.82 all long* - **Fix Model:** @@ -322,6 +322,7 @@ It checks for a new version automatically once every day. - **Decimation:** - Added "Remove Doubles" option - **General:** + - Fixed some bugs - Fixed objects getting unhidden when doing any cats operation in 2.80+ - Updated mmd_tools diff --git a/__init__.py b/__init__.py index 80c71a9e..b8ecdad3 100644 --- a/__init__.py +++ b/__init__.py @@ -30,13 +30,13 @@ 'author': 'GiveMeAllYourCats & Hotox', 'location': 'View 3D > Tool Shelf > CATS', 'description': 'A tool designed to shorten steps needed to import and optimize models into VRChat', - 'version': (0, 16, 1), # Has to be (x, x, x) not [x, x, x]!! # Only change this version and the dev branch var right before publishing the new update! + 'version': (0, 17, 0), # Has to be (x, x, x) not [x, x, x]!! # Only change this version and the dev branch var right before publishing the new update! 'blender': (2, 80, 0), 'wiki_url': 'https://github.com/michaeldegroot/cats-blender-plugin', 'tracker_url': 'https://github.com/michaeldegroot/cats-blender-plugin/issues', 'warning': '', } -dev_branch = True +dev_branch = False import os import sys @@ -76,7 +76,7 @@ importlib.reload(extentions) -# How to update mmd_tools: +# How to update mmd_tools: (outdated, no longer used) # Delete mmd_tools_local folder # Paste mmd_tools folder into project # Refactor folder name "mmd_tools" to "mmd_tools_local" diff --git a/extentions.py b/extentions.py index 74fd2136..55fe8424 100644 --- a/extentions.py +++ b/extentions.py @@ -164,7 +164,7 @@ def register(): Scene.apply_transforms = BoolProperty( name='Apply Transforms', description='Check this if both armatures and meshes are already at their correct positions.' - '\nThis will cause them to stay exactly like they are when merging', + '\nThis will cause them to stay exactly where they are when merging', default=False ) diff --git a/extern_tools/mmd_tools_local/bpyutils.py b/extern_tools/mmd_tools_local/bpyutils.py index b6bcd882..92918373 100644 --- a/extern_tools/mmd_tools_local/bpyutils.py +++ b/extern_tools/mmd_tools_local/bpyutils.py @@ -297,22 +297,31 @@ def __clean_drivers(self, key): if bpy.app.version < (2, 75, 0): def shape_key_remove(self, key): - assert(key.id_data == self.__obj.data.shape_keys) obj = self.__obj - key_blocks = obj.data.shape_keys.key_blocks # key.id_data.key_blocks + assert(key.id_data == obj.data.shape_keys) + key_blocks = key.id_data.key_blocks relative_key_map = {k.name:getattr(k.relative_key, 'name', '') for k in key_blocks} - obj.active_shape_key_index = key_blocks.find(key.name) + last_index, obj.active_shape_key_index = obj.active_shape_key_index, key_blocks.find(key.name) + if last_index >= obj.active_shape_key_index: + last_index = max(0, last_index-1) bpy.context.scene.objects.active, last = obj, bpy.context.scene.objects.active self.__clean_drivers(key) bpy.ops.object.shape_key_remove() bpy.context.scene.objects.active = last for k in key_blocks: k.relative_key = key_blocks.get(relative_key_map[k.name], key_blocks[0]) + obj.active_shape_key_index = min(last_index, len(key_blocks)-1) else: def shape_key_remove(self, key): - assert(key.id_data == self.__obj.data.shape_keys) + obj = self.__obj + assert(key.id_data == obj.data.shape_keys) + key_blocks = key.id_data.key_blocks + last_index = obj.active_shape_key_index + if last_index >= key_blocks.find(key.name): + last_index = max(0, last_index-1) self.__clean_drivers(key) - self.__obj.shape_key_remove(key) + obj.shape_key_remove(key) + obj.active_shape_key_index = min(last_index, len(key_blocks)-1) class TransformConstraintOp: diff --git a/extern_tools/mmd_tools_local/core/bone.py b/extern_tools/mmd_tools_local/core/bone.py index c22bc6a8..4856dfae 100644 --- a/extern_tools/mmd_tools_local/core/bone.py +++ b/extern_tools/mmd_tools_local/core/bone.py @@ -236,6 +236,14 @@ def patch_rna_idprop(pose_bones): @classmethod def clean_additional_transformation(cls, armature): + # clean constraints + for p_bone in armature.pose.bones: + p_bone.mmd_bone.is_additional_transform_dirty = True + constraints = p_bone.constraints + remove_constraint(constraints, 'mmd_additional_rotation') + remove_constraint(constraints, 'mmd_additional_location') + if remove_constraint(constraints, 'mmd_additional_parent'): + p_bone.bone.use_inherit_rotation = True # clean shadow bones shadow_bone_types = { 'DUMMY', @@ -246,19 +254,9 @@ def clean_additional_transformation(cls, armature): def __is_at_shadow_bone(b): return b.is_mmd_shadow_bone and b.mmd_shadow_bone_type in shadow_bone_types shadow_bone_names = [b.name for b in armature.pose.bones if __is_at_shadow_bone(b)] - if len(shadow_bone_names) > 0: with bpyutils.edit_object(armature) as data: remove_edit_bones(data.edit_bones, shadow_bone_names) - - # clean constraints - for p_bone in armature.pose.bones: - p_bone.mmd_bone.is_additional_transform_dirty = True - constraints = p_bone.constraints - remove_constraint(constraints, 'mmd_additional_rotation') - remove_constraint(constraints, 'mmd_additional_location') - if remove_constraint(constraints, 'mmd_additional_parent'): - p_bone.bone.use_inherit_rotation = True cls.patch_rna_idprop(armature.pose.bones) @classmethod diff --git a/extern_tools/mmd_tools_local/core/model.py b/extern_tools/mmd_tools_local/core/model.py index 7434296b..9de456f6 100644 --- a/extern_tools/mmd_tools_local/core/model.py +++ b/extern_tools/mmd_tools_local/core/model.py @@ -963,23 +963,12 @@ def buildJoints(self): i.rotation_euler = r.to_euler(i.rotation_mode) def cleanAdditionalTransformConstraints(self): - self.__detached_call(FnBone.clean_additional_transformation) + arm = self.armature() + if arm: + FnBone.clean_additional_transformation(arm) def applyAdditionalTransformConstraints(self): - self.__detached_call(FnBone.apply_additional_transformation) - - def __detached_call(self, exec_func): arm = self.armature() - # detach armature modifier for improving performance - detached = [] - for mesh in self.meshes(): - for m in mesh.modifiers: - if m.type == 'ARMATURE' and m.object == arm: - m.object = None - detached.append(m) - try: - exec_func(arm) - finally: - for m in detached: # store back - m.object = arm + if arm: + FnBone.apply_additional_transformation(arm) diff --git a/extern_tools/mmd_tools_local/core/sdef.py b/extern_tools/mmd_tools_local/core/sdef.py index 83e4fbbb..9ed0c235 100644 --- a/extern_tools/mmd_tools_local/core/sdef.py +++ b/extern_tools/mmd_tools_local/core/sdef.py @@ -10,6 +10,7 @@ class FnSDEF(): g_verts = {} # global cache g_shapekey_data = {} g_bone_check = {} + __g_armature_check = {} SHAPEKEY_NAME = 'mmd_sdef_skinning' MASK_NAME = 'mmd_sdef_mask' @@ -18,10 +19,14 @@ def __init__(self): @classmethod def __init_cache(cls, obj, shapekey): - if hash(obj) not in cls.g_verts: - key = hash(obj) + key = hash(obj) + obj = getattr(obj, 'original', obj) + mod = obj.modifiers.get('mmd_bone_order_override') + key_armature = hash(mod.object.pose) if mod and mod.type == 'ARMATURE' and mod.object else None + if key not in cls.g_verts or cls.__g_armature_check.get(key) != key_armature: cls.g_verts[key] = cls.__find_vertices(obj) cls.g_bone_check[key] = {} + cls.__g_armature_check[key] = key_armature shapekey_co = np.zeros(len(shapekey.data) * 3, dtype=np.float32) shapekey.data.foreach_get('co', shapekey_co) shapekey_co = shapekey_co.reshape(len(shapekey.data), 3) @@ -44,7 +49,9 @@ def mute_sdef_set(cls, obj, mute): if cls.SHAPEKEY_NAME in key_blocks: shapekey = key_blocks[cls.SHAPEKEY_NAME] shapekey.mute = mute - cls.__sdef_muted(obj, shapekey) + if cls.has_sdef_data(obj): + cls.__init_cache(obj, shapekey) + cls.__sdef_muted(obj, shapekey) @classmethod def __sdef_muted(cls, obj, shapekey): @@ -52,9 +59,8 @@ def __sdef_muted(cls, obj, shapekey): if mute != cls.g_bone_check[hash(obj)].get('sdef_mute'): mod = obj.modifiers.get('mmd_bone_order_override') if mod and mod.type == 'ARMATURE': - #FIXME not working well inside driver function on Blender 2.8 - if not mute and cls.MASK_NAME not in obj.vertex_groups: - mask = tuple(i[0] for v in cls.g_verts[hash(obj)].values() for i in v[2]) + if not mute and cls.MASK_NAME not in obj.vertex_groups and obj.mode != 'EDIT': + mask = tuple(i for v in cls.g_verts[hash(obj)].values() for i in v[3]) obj.vertex_groups.new(name=cls.MASK_NAME).add(mask, 1, 'REPLACE') mod.vertex_group = '' if mute else cls.MASK_NAME mod.invert_vertex_group = True @@ -99,8 +105,9 @@ def __find_vertices(cls, obj): r0 = c + r0 - rw r1 = c + r1 - rw - key = (hash(bone_map[bgs[0].group]), hash(bone_map[bgs[1].group])) + key = (bgs[0].group, bgs[1].group) if key not in vertices: + #TODO basically we can not cache any bone reference vertices[key] = (bone_map[bgs[0].group], bone_map[bgs[1].group], [], []) vertices[key][2].append((i, w0, w1, vd[i].co-c, (c+r0)/2, (c+r1)/2)) vertices[key][3].append(i) @@ -115,21 +122,28 @@ def driver_function_wrap(cls, obj_name, bulk_update, use_skip, use_scale): @classmethod def driver_function(cls, shapekey, obj_name, bulk_update, use_skip, use_scale): obj = bpy.data.objects[obj_name] + if getattr(shapekey.id_data, 'is_evaluated', False): + # For Blender 2.8x, we should use evaluated object, and the only reference is the "obj" variable of SDEF driver + #cls.driver_function(shapekey.id_data.original.key_blocks[shapekey.name], obj_name, bulk_update, use_skip, use_scale) # update original data + data_path = shapekey.path_from_id('value') + obj = next(i for i in shapekey.id_data.animation_data.drivers if i.data_path == data_path).driver.variables['obj'].targets[0].id cls.__init_cache(obj, shapekey) if cls.__sdef_muted(obj, shapekey): return 0.0 + pose_bones = obj.modifiers.get('mmd_bone_order_override').object.pose.bones if not bulk_update: shapekey_data = shapekey.data if use_scale: # with scale for bone0, bone1, sdef_data, vids in cls.g_verts[hash(obj)].values(): + bone0, bone1 = pose_bones[bone0.name], pose_bones[bone1.name] if use_skip and not cls.__check_bone_update(obj, bone0, bone1): continue mat0 = matmul(bone0.matrix, bone0.bone.matrix_local.inverted()) mat1 = matmul(bone1.matrix, bone1.bone.matrix_local.inverted()) - rot0 = mat0.to_quaternion() - rot1 = mat1.to_quaternion() + rot0 = mat0.to_euler('YXZ').to_quaternion() + rot1 = mat1.to_euler('YXZ').to_quaternion() if rot1.dot(rot0) < 0: rot1 = -rot1 s0, s1 = mat0.to_scale(), mat1.to_scale() @@ -140,12 +154,14 @@ def driver_function(cls, shapekey, obj_name, bulk_update, use_skip, use_scale): else: # default for bone0, bone1, sdef_data, vids in cls.g_verts[hash(obj)].values(): + bone0, bone1 = pose_bones[bone0.name], pose_bones[bone1.name] if use_skip and not cls.__check_bone_update(obj, bone0, bone1): continue mat0 = matmul(bone0.matrix, bone0.bone.matrix_local.inverted()) mat1 = matmul(bone1.matrix, bone1.bone.matrix_local.inverted()) - rot0 = mat0.to_quaternion() - rot1 = mat1.to_quaternion() + # workaround some weird result of matrix.to_quaternion() using to_euler(), but still minor issues + rot0 = mat0.to_euler('YXZ').to_quaternion() + rot1 = mat1.to_euler('YXZ').to_quaternion() if rot1.dot(rot0) < 0: rot1 = -rot1 for vid, w0, w1, pos_c, cr0, cr1 in sdef_data: @@ -156,12 +172,13 @@ def driver_function(cls, shapekey, obj_name, bulk_update, use_skip, use_scale): if use_scale: # scale & bulk update for bone0, bone1, sdef_data, vids in cls.g_verts[hash(obj)].values(): + bone0, bone1 = pose_bones[bone0.name], pose_bones[bone1.name] if use_skip and not cls.__check_bone_update(obj, bone0, bone1): continue mat0 = matmul(bone0.matrix, bone0.bone.matrix_local.inverted()) mat1 = matmul(bone1.matrix, bone1.bone.matrix_local.inverted()) - rot0 = mat0.to_quaternion() - rot1 = mat1.to_quaternion() + rot0 = mat0.to_euler('YXZ').to_quaternion() + rot1 = mat1.to_euler('YXZ').to_quaternion() if rot1.dot(rot0) < 0: rot1 = -rot1 s0, s1 = mat0.to_scale(), mat1.to_scale() @@ -172,12 +189,13 @@ def scale(mat_rot, w0, w1): else: # bulk update for bone0, bone1, sdef_data, vids in cls.g_verts[hash(obj)].values(): + bone0, bone1 = pose_bones[bone0.name], pose_bones[bone1.name] if use_skip and not cls.__check_bone_update(obj, bone0, bone1): continue mat0 = matmul(bone0.matrix, bone0.bone.matrix_local.inverted()) mat1 = matmul(bone1.matrix, bone1.bone.matrix_local.inverted()) - rot0 = mat0.to_quaternion() - rot1 = mat1.to_quaternion() + rot0 = mat0.to_euler('YXZ').to_quaternion() + rot1 = mat1.to_euler('YXZ').to_quaternion() if rot1.dot(rot0) < 0: rot1 = -rot1 shapekey_data[vids] = [matmul((rot0*w0 + rot1*w1).normalized().to_matrix(), pos_c) + matmul(mat0, cr0)*w0 + matmul(mat1, cr1)*w1 for vid, w0, w1, pos_c, cr0, cr1 in sdef_data] @@ -224,9 +242,6 @@ def bind(cls, obj, bulk_update=None, use_skip=True, use_scale=False): cls.register_driver_function() if bulk_update is None: bulk_update = cls.__get_benchmark_result(obj, shapekey, use_scale, use_skip) - # FIXME: force disable use_skip=True for bulk_update=False on 2.8 - if bpy.app.version >= (2, 80, 0) and (not bulk_update and use_skip): - use_skip = False # Add the driver to the shapekey f = obj.data.shape_keys.driver_add('key_blocks["'+cls.SHAPEKEY_NAME+'"].value', -1) if hasattr(f.driver, 'show_debug_info'): @@ -237,13 +252,16 @@ def bind(cls, obj, bulk_update=None, use_skip=True, use_scale=False): ov.type = 'SINGLE_PROP' ov.targets[0].id = obj ov.targets[0].data_path = 'name' - mod = obj.modifiers.get('mmd_bone_order_override') - if mod and mod.type == 'ARMATURE': - ov = f.driver.variables.new() - ov.name = 'arm' - ov.type = 'SINGLE_PROP' - ov.targets[0].id = mod.object - ov.targets[0].data_path = 'name' + if bpy.app.version >= (2, 80, 0): + if not bulk_update and use_skip: #FIXME: force disable use_skip=True for bulk_update=False on 2.8 + use_skip = False + mod = obj.modifiers.get('mmd_bone_order_override') + variables = f.driver.variables + for name in set(data[i].name for data in cls.g_verts[hash(obj)].values() for i in range(2)): # add required bones for dependency graph + var = variables.new() + var.type = 'TRANSFORMS' + var.targets[0].id = mod.object + var.targets[0].bone_target = name if hasattr(f.driver, 'use_self'): # Blender 2.78+ f.driver.use_self = True param = (bulk_update, use_skip, use_scale) diff --git a/resources/supporters.json b/resources/supporters.json index 4103b08c..5c4f6a5f 100644 --- a/resources/supporters.json +++ b/resources/supporters.json @@ -476,6 +476,9 @@ },{ "displayname": "BertBOT", "startdate": "2020-02-16" + },{ + "displayname": "DJ-G6PON3", + "startdate": "2020-05-11" } ] } diff --git a/ui/manual.py b/ui/manual.py index b940ad9d..bb9f64d3 100644 --- a/ui/manual.py +++ b/ui/manual.py @@ -123,6 +123,6 @@ def draw(self, context): row.scale_y = button_height row.operator(Armature_manual.FixVRMShapesButton.bl_idname, icon='SHAPEKEY_DATA') - row = col.row(align=True) - row.scale_y = button_height - row.operator(Armature_manual.SeparateByCopyProtection.bl_idname, icon='SHAPEKEY_DATA') + # row = col.row(align=True) + # row.scale_y = button_height + # row.operator(Armature_manual.SeparateByCopyProtection.bl_idname, icon='SHAPEKEY_DATA') From 7b2b1833ecf759c92189e25ba3e8865a116775a1 Mon Sep 17 00:00:00 2001 From: Hotox Date: Tue, 19 May 2020 17:16:07 +0200 Subject: [PATCH 20/24] Made another model compatible --- __init__.py | 4 ++-- tools/armature.py | 2 ++ tools/armature_bones.py | 14 ++++++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index b8ecdad3..72e20a61 100644 --- a/__init__.py +++ b/__init__.py @@ -30,13 +30,13 @@ 'author': 'GiveMeAllYourCats & Hotox', 'location': 'View 3D > Tool Shelf > CATS', 'description': 'A tool designed to shorten steps needed to import and optimize models into VRChat', - 'version': (0, 17, 0), # Has to be (x, x, x) not [x, x, x]!! # Only change this version and the dev branch var right before publishing the new update! + 'version': (0, 16, 1), # Has to be (x, x, x) not [x, x, x]!! # Only change this version and the dev branch var right before publishing the new update! 'blender': (2, 80, 0), 'wiki_url': 'https://github.com/michaeldegroot/cats-blender-plugin', 'tracker_url': 'https://github.com/michaeldegroot/cats-blender-plugin/issues', 'warning': '', } -dev_branch = False +dev_branch = True import os import sys diff --git a/tools/armature.py b/tools/armature.py index 2a27fb3c..31b3b023 100644 --- a/tools/armature.py +++ b/tools/armature.py @@ -502,6 +502,8 @@ def execute(self, context): ('Cf_J_', ''), ('G_', ''), ('Joint_', ''), + ('Def_C_', ''), + ('Def_', ''), ('DEF_', ''), ('Chr_', ''), ] diff --git a/tools/armature_bones.py b/tools/armature_bones.py index 171e6c54..444dc7cd 100644 --- a/tools/armature_bones.py +++ b/tools/armature_bones.py @@ -294,6 +294,8 @@ # Fix some model (['\LKnee'], '\LLeg', '\Left leg'), + + (['\L_Clav', '\L_ShoulderMid'], '\L_Shoulder', 'Arm_\L'), ] bone_finger_list = [ 'Thumb0_', @@ -470,6 +472,12 @@ 'Spine_D', 'Spine_E', + 'SpineA', + 'SpineB', + 'SpineC', + 'SpineD', + 'SpineE', + 'Spina00', 'Spina01', 'Spina02', @@ -634,6 +642,8 @@ '\L_ShoulderPad', 'Collarbone_\L', 'J_Clavicle_\L', + '\L_Clav', + 'Clav_\L', ] bone_rename['\Left arm'] = [ '\Left_Arm', @@ -1085,6 +1095,7 @@ 'Bip_Armpit_\L', 'Bip_UpperArmBase_\L', 'ShoulderHalf_\L', + 'ShoulderAux_\L', ] bone_reweight['\Left arm'] = [ 'Arm01_\L', @@ -1193,6 +1204,7 @@ 'N_Hkata_\L', 'Arm_\Left_Shoulder_Tw_A', 'Arm_\Left_Shoulder_Tw_B', + 'ArmAux_\L', ] bone_reweight['Left arm'] = [ # This has apparently no side in the name 'エプロンArm', @@ -1332,6 +1344,7 @@ 'N_Hhiji_\L', 'N_Hte_\L', 'Arm_\Left_Wrist_Tw_E', + 'ElbowAux_\L', ] bone_reweight['\Left wrist'] = [ # 'Sleeve3_\L', @@ -1501,6 +1514,7 @@ '\Left_Thigh_Twist', 'ThighLow_\L', 'ThighUp_\L', + 'LegAux_\L', ] bone_reweight['\Left knee'] = [ 'KneeD_\L', From 7cea6a982fab6e975a237251377973463b4e2684 Mon Sep 17 00:00:00 2001 From: Hotox Date: Wed, 20 May 2020 04:38:52 +0200 Subject: [PATCH 21/24] Added "Remove Rigidbodies and Joints" option to Fix Model --- README.md | 2 ++ extentions.py | 7 +++++++ tools/armature.py | 21 +++++++++++---------- tools/common.py | 9 +++++---- ui/armature.py | 2 ++ 5 files changed, 27 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index f2e43f42..c046eb1f 100644 --- a/README.md +++ b/README.md @@ -309,6 +309,8 @@ It checks for a new version automatically once every day. - Added "Fix MMD Twist Bones" option to Fix Model - This will apply a fix to make the MMD arm twist bones usable **(Thanks Rokk!)** - You do not need to enable "Keep Twist Bones" for this to work + - Added "Remove Rigidbodies and Joints" option to Fix Model + - This is solely intended for our non-VRChat users - Added compatibility to more models - Disabling the option "Remove Zero Weight Bones" now also keeps unused vertex groups - **Importer:** diff --git a/extentions.py b/extentions.py index 55fe8424..d976b744 100644 --- a/extentions.py +++ b/extentions.py @@ -100,6 +100,13 @@ def register(): default=True ) + Scene.remove_rigidbodies_joints = BoolProperty( + name='Remove Rigidbodies and Joints', + description="Rigidbodies and joints are used by MMD software to simulate physics." + "\nThey are completely useless for VRChat, so removing them is recommended for VRChat users!", + default=True + ) + Scene.use_google_only = BoolProperty( name='Use Old Translations (not recommended)', description="Ignores the internal dictionary and only uses the Google Translator for shape key translations." diff --git a/tools/armature.py b/tools/armature.py index 31b3b023..28fbcc85 100644 --- a/tools/armature.py +++ b/tools/armature.py @@ -256,17 +256,18 @@ def execute(self, context): set_material_shading() # Remove Rigidbodies and joints - to_delete = [] - for child in Common.get_top_parent(armature).children: - if 'rigidbodies' in child.name or 'joints' in child.name and child.name not in to_delete: - to_delete.append(child.name) - continue - for child2 in child.children: - if 'rigidbodies' in child2.name or 'joints' in child2.name and child2.name not in to_delete: - to_delete.append(child2.name) + if context.scene.remove_rigidbodies_joints: + to_delete = [] + for child in Common.get_top_parent(armature).children: + if 'rigidbodies' in child.name or 'joints' in child.name and child.name not in to_delete: + to_delete.append(child.name) continue - for obj_name in to_delete: - Common.delete_hierarchy(Common.get_objects()[obj_name]) + for child2 in child.children: + if 'rigidbodies' in child2.name or 'joints' in child2.name and child2.name not in to_delete: + to_delete.append(child2.name) + continue + for obj_name in to_delete: + Common.delete_hierarchy(Common.get_objects()[obj_name]) # Remove objects from different layers and things that are not meshes get_current_layers = [] diff --git a/tools/common.py b/tools/common.py index b4f3d8c7..287fe109 100644 --- a/tools/common.py +++ b/tools/common.py @@ -253,7 +253,7 @@ def set_default_stage(): """ # Remove rigidbody collections, as they cause issues if they are not in the view_layer - if not version_2_79_or_older(): + if not version_2_79_or_older() and bpy.context.scene.remove_rigidbodies_joints: print('Collections:') for collection in bpy.data.collections: print(' ' + collection.name, collection.name.lower()) @@ -1094,9 +1094,10 @@ def prepare_separation(mesh): unselect_all() # Remove Rigidbodies and joints - for obj in get_objects(): - if 'rigidbodies' in obj.name or 'joints' in obj.name: - delete_hierarchy(obj) + if bpy.context.scene.remove_rigidbodies_joints: + for obj in get_objects(): + if 'rigidbodies' in obj.name or 'joints' in obj.name: + delete_hierarchy(obj) save_shapekey_order(mesh.name) set_active(mesh) diff --git a/ui/armature.py b/ui/armature.py index 654e5e51..e7d500bc 100644 --- a/ui/armature.py +++ b/ui/armature.py @@ -221,6 +221,8 @@ def draw(self, context): row.prop(context.scene, 'combine_mats') row = col.row(align=True) row.prop(context.scene, 'remove_zero_weight') + row = col.row(align=True) + row.prop(context.scene, 'remove_rigidbodies_joints') col.separator() row = col.row(align=True) From 5add64c6849f850c3f4d487af65ac916b734dd9d Mon Sep 17 00:00:00 2001 From: Hotox Date: Sun, 24 May 2020 19:52:16 +0200 Subject: [PATCH 22/24] Updated mmd_tools --- extern_tools/mmd_tools_local/core/model.py | 39 +++++----- .../mmd_tools_local/core/rigid_body.py | 6 +- .../mmd_tools_local/operators/rigid_body.py | 72 +++++++++++++++++++ extern_tools/mmd_tools_local/panels/tool.py | 3 +- 4 files changed, 97 insertions(+), 23 deletions(-) diff --git a/extern_tools/mmd_tools_local/core/model.py b/extern_tools/mmd_tools_local/core/model.py index 9de456f6..b004e953 100644 --- a/extern_tools/mmd_tools_local/core/model.py +++ b/extern_tools/mmd_tools_local/core/model.py @@ -567,24 +567,6 @@ def clean(self): const = i.constraints['mmd_tools_rigid_track'] i.constraints.remove(const) - if bpy.app.version < (2, 78, 0): - self.__removeChildrenOfTemporaryGroupObject() # for speeding up only - for i in self.temporaryObjects(): - bpy.context.scene.objects.unlink(i) - bpy.data.objects.remove(i) - elif bpy.app.version < (2, 80, 0): - for i in self.temporaryObjects(): - bpy.data.objects.remove(i, do_unlink=True) - else: - tmp_objs = tuple(self.temporaryObjects()) - for i in tmp_objs: - for c in i.users_collection: - c.objects.unlink(i) - bpy.ops.object.delete({'selected_objects':tmp_objs, 'active_object':self.rootObject()}) - for i in tmp_objs: - #assert(i.users == 0) - bpy.data.objects.remove(i) - rigid_track_counts = 0 for i in self.rigidBodies(): rigid_type = int(i.mmd_rigid.type) @@ -609,6 +591,8 @@ def clean(self): for i in self.joints(): self.__restoreTransforms(i) + self.__removeTemporaryObjects() + arm = self.armature() if arm is not None: # update armature arm.update_tag() @@ -621,6 +605,25 @@ def clean(self): mmd_root.is_built = False rigid_body.setRigidBodyWorldEnabled(rigidbody_world_enabled) + def __removeTemporaryObjects(self): + if bpy.app.version < (2, 78, 0): + self.__removeChildrenOfTemporaryGroupObject() # for speeding up only + for i in self.temporaryObjects(): + bpy.context.scene.objects.unlink(i) + bpy.data.objects.remove(i) + elif bpy.app.version < (2, 80, 0): + for i in self.temporaryObjects(): + bpy.data.objects.remove(i, do_unlink=True) + else: + tmp_objs = tuple(self.temporaryObjects()) + for i in tmp_objs: + for c in i.users_collection: + c.objects.unlink(i) + bpy.ops.object.delete({'selected_objects':tmp_objs, 'active_object':self.rootObject()}) + for i in tmp_objs: + if i.users < 1: + bpy.data.objects.remove(i) + def __removeChildrenOfTemporaryGroupObject(self): tmp_grp_obj = self.temporaryGroupObject() tmp_cnt = len(tmp_grp_obj.children) diff --git a/extern_tools/mmd_tools_local/core/rigid_body.py b/extern_tools/mmd_tools_local/core/rigid_body.py index 975a6a19..144c643b 100644 --- a/extern_tools/mmd_tools_local/core/rigid_body.py +++ b/extern_tools/mmd_tools_local/core/rigid_body.py @@ -20,11 +20,9 @@ def collisionShape(shape_type): def setRigidBodyWorldEnabled(enable): + if bpy.ops.rigidbody.world_add.poll(): + bpy.ops.rigidbody.world_add() rigidbody_world = bpy.context.scene.rigidbody_world - if rigidbody_world is None: - if enable: - bpy.ops.rigidbody.world_add() - return False enabled = rigidbody_world.enabled rigidbody_world.enabled = enable return enabled diff --git a/extern_tools/mmd_tools_local/operators/rigid_body.py b/extern_tools/mmd_tools_local/operators/rigid_body.py index 83844370..9876319f 100644 --- a/extern_tools/mmd_tools_local/operators/rigid_body.py +++ b/extern_tools/mmd_tools_local/operators/rigid_body.py @@ -438,3 +438,75 @@ def execute(self, context): if root: utils.selectAObject(root) return { 'FINISHED' } + +@register_wrap +class UpdateRigidBodyWorld(Operator): + bl_idname = 'mmd_tools.rigid_body_world_update' + bl_label = 'Update Rigid Body World' + bl_description = 'Update rigid body world and references of rigid body constraint according to current scene objects (experimental)' + bl_options = {'REGISTER', 'UNDO'} + + @staticmethod + def __get_rigid_body_world_objects(): + rigid_body.setRigidBodyWorldEnabled(True) + rbw = bpy.context.scene.rigidbody_world + if bpy.app.version < (2, 80, 0): + if not rbw.group: + rbw.group = bpy.data.groups.new('RigidBodyWorld') + rbw.group.use_fake_user = True + if not rbw.constraints: + rbw.constraints = bpy.data.groups.new('RigidBodyConstraints') + rbw.constraints.use_fake_user = True + return rbw.group.objects, rbw.constraints.objects + + if not rbw.collection: + rbw.collection = bpy.data.collections.new('RigidBodyWorld') + rbw.collection.use_fake_user = True + if not rbw.constraints: + rbw.constraints = bpy.data.collections.new('RigidBodyConstraints') + rbw.constraints.use_fake_user = True + return rbw.collection.objects, rbw.constraints.objects + + def execute(self, context): + scene_objs = (bpy.context.scene.objects,) + scene_objs += tuple({x.dupli_group.objects for x in scene_objs[0] if x.dupli_type == 'GROUP' and x.dupli_group}) if bpy.app.version < (2, 80, 0)\ + else tuple({x.instance_collection.objects for x in scene_objs[0] if x.instance_type == 'COLLECTION' and x.instance_collection}) + + def _update_group(obj, group): + if any((obj in x.values()) for x in scene_objs): + if obj not in group.values(): + group.link(obj) + return True + elif obj in group.values(): + group.unlink(obj) + return False + + def _references(obj): + yield obj + if obj.proxy: + yield from _references(obj.proxy) + if getattr(obj, 'override_library', None): + yield from _references(obj.override_library.reference) + + _find_root = mmd_model.Model.findRoot + rb_objs, rbc_objs = self.__get_rigid_body_world_objects() + objects = bpy.data.objects + table = {} + + for i in (x for x in objects if x.rigid_body): + if _update_group(i, rb_objs): + rb_map = table.setdefault(_find_root(i), {}) + if i in rb_map: # means rb_map[i] will replace i + rb_objs.unlink(i) + continue + for r in _references(i): + rb_map[r] = i + + for i in (x for x in objects if x.rigid_body_constraint): + if _update_group(i, rbc_objs): + rbc, root = i.rigid_body_constraint, _find_root(i) + rb_map = table.get(root, {}) + rbc.object1 = rb_map.get(rbc.object1, rbc.object1) + rbc.object2 = rb_map.get(rbc.object2, rbc.object2) + + return { 'FINISHED' } diff --git a/extern_tools/mmd_tools_local/panels/tool.py b/extern_tools/mmd_tools_local/panels/tool.py index 2b0f38d5..73086fa5 100644 --- a/extern_tools/mmd_tools_local/panels/tool.py +++ b/extern_tools/mmd_tools_local/panels/tool.py @@ -50,6 +50,7 @@ def draw(self, context): row = col.row(align=True) row.operator('mmd_tools.create_mmd_model_root_object', text='Create Model', icon='OUTLINER_OB_ARMATURE') row.operator('mmd_tools.convert_to_mmd_model', text='Convert Model', icon='OUTLINER_OB_ARMATURE') + row.operator('mmd_tools.rigid_body_world_update', text='', icon='PHYSICS') col = layout.column(align=True) col.operator('mmd_tools.convert_materials_for_cycles', text='Convert Materials For Cycles') @@ -69,7 +70,7 @@ def draw(self, context): col.operator('mmd_tools.clean_additional_transform', text='Clean') col = row.column(align=True) - col.active = context.scene.rigidbody_world is not None and context.scene.rigidbody_world.enabled + col.active = getattr(context.scene.rigidbody_world, 'enabled', False) sub_row = col.row(align=True) sub_row.label(text='Physics:', icon='PHYSICS') if not root.mmd_root.is_built: From 8cae18faec0a0bb417948755409dcb21b47a212b Mon Sep 17 00:00:00 2001 From: Hotox Date: Thu, 28 May 2020 17:33:08 +0200 Subject: [PATCH 23/24] Imported armatures now always show their bones in front and in wire mode --- README.md | 1 + resources/icons/supporters/Lucifer MStar.png | Bin 0 -> 2018 bytes resources/supporters.json | 5 ++ tools/armature.py | 27 ++++--- tools/armature_bones.py | 1 + tools/importer.py | 70 +++++++++++-------- 6 files changed, 63 insertions(+), 41 deletions(-) create mode 100644 resources/icons/supporters/Lucifer MStar.png diff --git a/README.md b/README.md index c046eb1f..760ae722 100644 --- a/README.md +++ b/README.md @@ -315,6 +315,7 @@ It checks for a new version automatically once every day. - Disabling the option "Remove Zero Weight Bones" now also keeps unused vertex groups - **Importer:** - Imported meshes from VRM files now get automatically parented to their armature + - Imported armatures now always show their bones in front and in wire mode - Fixed export warning being empty - Fixed importer error when the FBX importer was not enabled - Fixed importer error when a zip file contained another zip file diff --git a/resources/icons/supporters/Lucifer MStar.png b/resources/icons/supporters/Lucifer MStar.png new file mode 100644 index 0000000000000000000000000000000000000000..2cbcd943618ce814867c7b5134ae0832138c4b9d GIT binary patch literal 2018 zcmV<82Oao{P)Px#1ZP1_K>z@;j|==^1pojCj7da6R9Hu)S9^?=#T7p@^F8ikAA%4Tu&@TCbtNhR z+Zv5!Nt(1o@DVlENE1zr8e;^bX#7Kh7HZoxMNR7?CN_;mV~oLwCK?S9h1fy}rTB;l z0tRJeSuT6`-ut~~roS`aw+lCDWw+<<$;>zN&H4S#nKNg;rT*;cBCz@1c~joJYyKkc`#O_Cw}#!s1Y|-4rmN%qonEqe)2%(T zbrdV>`O}K2am$1V{P@7p*ScGWzv!Usanp<_6E#~*jaw!}VADlE@2Pe4YzcL8mDGmf zv(DN2;)9n@DyGLJqG-=Z-g~V724nU8#)yU12r=cWJ=9QTs!uCi7T)pnyM~dbD zQ_g4$;-lV^zQ}IzvLOm=MxKtzw=s!8lCN+QNK+^bg_NRiRA5?bH?CZCPqF+o*?9l* z&R5o4A8_AAVC$pHmbCoRnm9f2LM4lz?_c{q{LAH|hZbHHNxSjux<_++h?Laul*m{K z1-cMq;fFy~3?>GK6@7IyrFoMxEu!IhS3b3Oe_@K=d-T%TQgy7iw(^^(kv_3>{aY(t z1iIt@T-Vt=d_|Y)cvefbdDHJtCR89AcmCtU5!$%Bg0K&hCr3b{30$bGBZk~>kb-)0 zn6~ea>5W|xeS8?l(~3I_GqiR6rI(bYza!M)P2Pz^BCP-06)#61q=9$3($6V8zK6nA z^YU_o-hTT}zpUMN`C`n@4 zJCNZ$v7GJy^RY`V@KW=R7_LtdL%T@D`$_1br+5xABzkg7_St>ENc(-!?6paTL<(k_ zM9&vd++FR6uKe^^7hOEdQs2ykw3ZYN#*`dOsdZqO>IVmrBp_DyN%@jGOBsDRSf%;X zQLwt`>=i$o{B6Yx|0WuZ9;Ls*mK~=wOSdfkYy2Q%kDY>j56~=G^`1|yS`tNNYM38U z;8jRf1gW6ryf#*l#@=VCq~^CZY7DkWR7+HuRwA$DBNrfeCrLa?laNe`wHTZf3}O8d zGBTxRY+oJG=Jo|EpPn>r_8t4iPQjc%&>42KLM2QV4 zL!MjDqtcA2H0i=#>OOZGsd6`tVHLzG0Iy(Y%2*{y!iE--YH>g7IY0|=406P{ zZ&FLiNONy38eH((7X}(`X(6Z8fn&m39r&tGBH2hc0Dxoz04g6@HfEq&A@>j+r~}|R zC==ozX3;k!tmXrXV=`oUD|!vhb?rO7?v^NyBH zqk(Zi6wh!vpW}{3q!0?r5zyQs&p9f{ha_$_jHvf6fA$+>ZZ2&RsE}NWZJ(+nj+Ov> zn?P~ISPhOIdxSa+ha8|2FaSs5OG%rlGklrKzg0q3NR zfN#^OQlOw9*Lm--!vKIGfPk0tgrnh%0gMSr*JrZ)9*nq_mBBdzOb+>kND!Pr(U{@D z3TY!CWOSBW?7RmMYQ6KcFwV~|&0L@7G0thv*Q4{C6P7^>@WKHeb9up$6q#4RIcXyx zWkU(R<=6^c39$SF>7>*2%G`+>Y{$u*F12{##lK{)U4K%CdiETJ$Gc!aMWgWnf z0@#7^Gc54)sPfq8T!uDD#DGry{>8sbKE=>!(v}0K%AA#SsG;ow1mI!ZCr5-2hoIcD z%0dNymln$3R|o(uFl3GvjaH)mo=AJy_kpu~Zv;-2w;rdkSIZl55nYQr=cgFL_b~@Q zKM%a$xCGhbCa&gxX`^1Mr=2e^T|4vx|FjbQ7x=O;Pm)w~z5oCK07*qoM6N<$f_fUf AZ~y=R literal 0 HcmV?d00001 diff --git a/resources/supporters.json b/resources/supporters.json index 5c4f6a5f..f88155ba 100644 --- a/resources/supporters.json +++ b/resources/supporters.json @@ -479,6 +479,11 @@ },{ "displayname": "DJ-G6PON3", "startdate": "2020-05-11" + },{ + "displayname": "Lucifer MStar", + "startdate": "2020-05-28", + "description": "Yes! It is me! That guy who makes all the worlds", + "website": "https://twitter.com/LuciferMStarVRC" } ] } diff --git a/tools/armature.py b/tools/armature.py index 28fbcc85..5b5efb29 100644 --- a/tools/armature.py +++ b/tools/armature.py @@ -514,6 +514,20 @@ def execute(self, context): ('_Le', '_L'), ('_Ri', '_R'), ] + # List of chars to replace + replaces = [ + (' ', '_'), + ('-', '_'), + ('.', '_'), + (':', '_'), + ('____', '_'), + ('___', '_'), + ('__', '_'), + ('_Le_', '_L_'), + ('_Ri_', '_R_'), + ('LEFT', 'Left'), + ('RIGHT', 'Right'), + ] # Standardize names for bone in armature.data.edit_bones: @@ -530,16 +544,9 @@ def execute(self, context): upper_name += s[:1].upper() + s[1:] name = upper_name - # Make all the underscores! And replace things! - name = name.replace(' ', '_')\ - .replace('-', '_')\ - .replace('.', '_')\ - .replace(':', '_')\ - .replace('____', '_')\ - .replace('___', '_')\ - .replace('__', '_')\ - .replace('_Le_', '_L_')\ - .replace('_Ri_', '_R_')\ + # Replace all the things! + for replacement in replaces: + name = name.replace(replacement[0], replacement[1]) # Replace if name starts with specified chars for replacement in starts_with: diff --git a/tools/armature_bones.py b/tools/armature_bones.py index 444dc7cd..a8d470a9 100644 --- a/tools/armature_bones.py +++ b/tools/armature_bones.py @@ -602,6 +602,7 @@ '\Left_Shoulder', '\LeftShoulder', 'Shoulder_\L', + 'Shoulder_\Left', '\LShoulder', '\LShoulderN', 'Shoulder\L', diff --git a/tools/importer.py b/tools/importer.py index 27f0d44b..35df5b5d 100644 --- a/tools/importer.py +++ b/tools/importer.py @@ -142,11 +142,7 @@ def execute(self, context): bpy.ops.cats_importer.zip_popup('INVOKE_DEFAULT') # Create list of armatures that got added during import, select them in cats and fix their bone orientations if necessary - arm_added_during_import = [obj for obj in bpy.data.objects if obj.type == 'ARMATURE' and obj not in pre_import_objects] - for armature in arm_added_during_import: - print('Added: ', armature.name) - bpy.context.scene.armature = armature.name - self.fix_bone_orientations(armature) + fix_armatures_post_import(pre_import_objects) return {'FINISHED'} @@ -281,31 +277,46 @@ def extract_file(): ImportAnyModel.import_file(model_dir, model_file_name) - @staticmethod - def fix_bone_orientations(armature): - Common.unselect_all() - Common.set_active(armature) - Common.switch('EDIT') - fix_bones = True +def fix_bone_orientations(armature): + Common.unselect_all() + Common.set_active(armature) + Common.switch('EDIT') + + fix_bones = True - # Check if all the bones are pointing in the same direction - for bone in armature.data.edit_bones: - equal_axis_count = 0 - if bone.head[0] == bone.tail[0]: - equal_axis_count += 1 - if bone.head[1] == bone.tail[1]: - equal_axis_count += 1 - if bone.head[2] == bone.tail[2]: - equal_axis_count += 1 + # Check if all the bones are pointing in the same direction + for bone in armature.data.edit_bones: + equal_axis_count = 0 + if bone.head[0] == bone.tail[0]: + equal_axis_count += 1 + if bone.head[1] == bone.tail[1]: + equal_axis_count += 1 + if bone.head[2] == bone.tail[2]: + equal_axis_count += 1 - # If the bone points to more than one direction, don't fix the armatures bones - if equal_axis_count < 2: - fix_bones = False + # If the bone points to more than one direction, don't fix the armatures bones + if equal_axis_count < 2: + fix_bones = False - if fix_bones: - Common.fix_bone_orientations(armature) - Common.switch('OBJECT') + if fix_bones: + Common.fix_bone_orientations(armature) + Common.switch('OBJECT') + + +def fix_armatures_post_import(pre_import_objects): + arm_added_during_import = [obj for obj in bpy.data.objects if obj.type == 'ARMATURE' and obj not in pre_import_objects] + for armature in arm_added_during_import: + print('Added: ', armature.name) + bpy.context.scene.armature = armature.name + fix_bone_orientations(armature) + + # Set better bone view + armature.draw_type = 'WIRE' + if version_2_79_or_older(): + armature.show_x_ray = True + else: + armature.show_in_front = True @register_wrap @@ -323,11 +334,8 @@ def execute(self, context): ImportAnyModel.extract_file() # Create list of armatures that got added during import, select them in cats and fix their bone orientations if necessary - arm_added_during_import = [obj for obj in bpy.data.objects if obj.type == 'ARMATURE' and obj not in pre_import_objects] - for armature in arm_added_during_import: - print('Added: ', armature.name) - bpy.context.scene.armature = armature.name - ImportAnyModel.fix_bone_orientations(armature) + fix_armatures_post_import(pre_import_objects) + return {'FINISHED'} def invoke(self, context, event): From 43b9402a72ab6fec3f189b66005678d0bf999111 Mon Sep 17 00:00:00 2001 From: Hotox Date: Wed, 3 Jun 2020 22:51:19 +0200 Subject: [PATCH 24/24] Ready for 0.17.0 --- __init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index 72e20a61..b8ecdad3 100644 --- a/__init__.py +++ b/__init__.py @@ -30,13 +30,13 @@ 'author': 'GiveMeAllYourCats & Hotox', 'location': 'View 3D > Tool Shelf > CATS', 'description': 'A tool designed to shorten steps needed to import and optimize models into VRChat', - 'version': (0, 16, 1), # Has to be (x, x, x) not [x, x, x]!! # Only change this version and the dev branch var right before publishing the new update! + 'version': (0, 17, 0), # Has to be (x, x, x) not [x, x, x]!! # Only change this version and the dev branch var right before publishing the new update! 'blender': (2, 80, 0), 'wiki_url': 'https://github.com/michaeldegroot/cats-blender-plugin', 'tracker_url': 'https://github.com/michaeldegroot/cats-blender-plugin/issues', 'warning': '', } -dev_branch = True +dev_branch = False import os import sys