From 0f30e080e3ec1d64236b8e9c1956ebb583e35e13 Mon Sep 17 00:00:00 2001 From: indigo Date: Mon, 6 Jul 2026 08:39:16 +0800 Subject: [PATCH] Add preview shape picker to shader ball; drag controls for node properties Preview geometry dropdown: Sphere / Cube / Cylinder (native prims), Teapot and Hair (bundled resources/preview/*.usdc - teapot tessellated from the Newell patch data with the Blinn 1.3x height correction, hair as 220 procedural B-spline strands), and Cloud Volume (UsdVolVolume over the OpenVDB bunny_cloud sample, downloaded at CMake configure and gitignored like the HDRIs). Shapes live under one scope with the material bound on it; switching activates one shape and reframes the camera. Missing assets drop out of the dropdown. usdVol added to the USD link set. The camera starts in a Maya-style 3/4 view. Node property values switch from Input* fields back to draggable controls (Ctrl+click to type): live-apply while dragging, one undo command on release. Co-Authored-By: Claude Fable 5 --- .gitignore | 1 + CMakeLists.txt | 35 +++++++- cmake/modules/FindOpenUSD.cmake | 1 + resources/preview/hair.usdc | Bin 0 -> 22206 bytes resources/preview/teapot.usdc | Bin 0 -> 32170 bytes src/ui/MaterialEditorPanel.cpp | 43 +++++----- src/ui/MaterialPreviewRenderer.cpp | 123 +++++++++++++++++++++++++++-- src/ui/MaterialPreviewRenderer.h | 9 +++ 8 files changed, 181 insertions(+), 31 deletions(-) create mode 100644 resources/preview/hair.usdc create mode 100644 resources/preview/teapot.usdc diff --git a/.gitignore b/.gitignore index ac37312..b99f840 100644 --- a/.gitignore +++ b/.gitignore @@ -79,3 +79,4 @@ imgui.ini # OCIO config LUT files — downloaded automatically at CMake configure time resources/OpenColorIO-Configs/ resources/hdri/ +resources/vdb/ diff --git a/CMakeLists.txt b/CMakeLists.txt index ed94508..18227b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -606,6 +606,33 @@ foreach(_hdri ${_hdri_files}) endif() endforeach() +# --------------------------------------------------------------------------- +# VDB: cloud volume for the shader-ball preview (OpenVDB sample model) at +# configure time if not present +# --------------------------------------------------------------------------- +set(_vdb_dir "${CMAKE_SOURCE_DIR}/resources/vdb") +file(MAKE_DIRECTORY "${_vdb_dir}") +if(NOT EXISTS "${_vdb_dir}/bunny_cloud.vdb") + message(STATUS "Downloading OpenVDB sample bunny_cloud.vdb ...") + set(_vdb_zip "${CMAKE_BINARY_DIR}/bunny_cloud.zip") + file(DOWNLOAD + "https://artifacts.aswf.io/io/aswf/openvdb/models/bunny_cloud.vdb/1.0.0/bunny_cloud.vdb-1.0.0.zip" + "${_vdb_zip}" + STATUS _vdb_dl_status + ) + list(GET _vdb_dl_status 0 _vdb_dl_code) + if(_vdb_dl_code EQUAL 0) + execute_process( + COMMAND ${CMAKE_COMMAND} -E tar xf "${_vdb_zip}" + WORKING_DIRECTORY "${_vdb_dir}" + ) + file(REMOVE "${_vdb_zip}") + message(STATUS "bunny_cloud.vdb installed to ${_vdb_dir}") + else() + message(WARNING "OpenVDB sample download failed (${_vdb_dl_status}) — the Cloud Volume preview shape will be unavailable.") + endif() +endif() + # Copy resources to build directory file(COPY ${CMAKE_SOURCE_DIR}/resources DESTINATION ${CMAKE_BINARY_DIR} @@ -624,7 +651,13 @@ add_custom_command(TARGET UsdLayerManager POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_SOURCE_DIR}/resources/hdri" "$/resources/hdri" - COMMENT "Copying fonts, SVG icons and HDRI presets to build output" + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${CMAKE_SOURCE_DIR}/resources/preview" + "$/resources/preview" + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${CMAKE_SOURCE_DIR}/resources/vdb" + "$/resources/vdb" + COMMENT "Copying fonts, SVG icons, HDRI presets and preview shapes to build output" ) # Copy ACES OCIO config to build output — only on first build (skipped if already present). diff --git a/cmake/modules/FindOpenUSD.cmake b/cmake/modules/FindOpenUSD.cmake index 207092f..60d3e0e 100644 --- a/cmake/modules/FindOpenUSD.cmake +++ b/cmake/modules/FindOpenUSD.cmake @@ -59,6 +59,7 @@ set(OpenUSD_REQUIRED_LIBS usdGeom usdLux usdShade + usdVol usdImaging usdImagingGL hdx diff --git a/resources/preview/hair.usdc b/resources/preview/hair.usdc new file mode 100644 index 0000000000000000000000000000000000000000..0187432db67ba236edb589fdccfa84befeae1dc7 GIT binary patch literal 22206 zcmbrlhf@?!&_7JhIY~x>k_1r!3Af#XBH2I&F`;4vMNFuufMkgRB8Va)f(cMml-p^+ z95F}4>{k)Y0R{EF=lMPV!Mjy`GgWuByHzvS)BOokB7*EfgC~p^lN0;j>qqGS8~^`) zivQmYq(r04|IHOdqoOF3M4>DS6;Y^)qDvJ2b7}s+K}>AQ|4hWC#Z)E44E}d6CnoWK z-`#0GO12j-QtO|+^!Iu*CR`1`hP(Um+5bJnpyi~#q2g{+{a5oW5zK~3N(HuA(8E?`pw9Qb>keU&U>0U6KW-J4tN?>$HAw3EgV z?WNoVV}C5EUdIV{7{hpbAH0^MG42XM`hTo40;LeXql3cO{rJYZ4meT?-NwP?2>pcX%`uL#su^4`rZ~`>wjY21u z%ahRKk@~)%+Mlp zHU;cy;T6iNVef8N{Qfx&-X}BYIN*er2R1=@$vIeb(-@=1XF*xgIdC1;1OIK<2Yb%m zf>?ewoP6*Yq_6hFkB(N}^@}`q{-=w2@?V+x9V7g*#tBE>ji)P)!|=tBbu3za6i&>_ zW24gY*w^cJs3~=V1%9h%@~V2c*Y6cubpASX-SZlrG)mLhV?P)lwFIoaHECIzBJ~@@ z@V`bFl5~wW&1$^K6rNkq?K%T8Qw*gw(xWLmaV-eEd%!tKAB`@~0i!vupmMJ{)}G-Y z=A01J&bwgNgHfQ>6$_5l5qL5o9D;AA3qBOC#ltU(V13+X*5{Ff5w)#wca0wPd@ID# z&a3d`$|kB@x)tNJTG@-kZdmfImUCR(#d0nwYggc9es{iL)u<-y#YZ{Z1oHj>f;% z!JForSfj-d>banSQ##f$*ANqm{_q2Amt?ZwT1y)GW;e*S)UctP ztW383{7XJ2+8m!2+LFwocHT4l9q+q70CPeO1g^6z*_3xa*f=zUlho$ev9V+Di(5Un zEo=r`GhY#(2!lEC&Qdn&a}B(A$UT*pe4F*h91@%~(d2(MD3MNTGfU9FCR`C~O*a&M zX~mD1f{ss9VD*3ts=eOM9@Q*@m@p@FtJ%TW*9u6CH^Jl6cd#t&+Yp-h0o=L|vL`{( zXd00S-68i_-?|})`;vL*N3vuWYJ^)WpD>jqGtzG~#9xxL=x6+BTDEfxy>{3RU(Cfp z{csqK++GMJ`c`mMJAtCxJs@oF8qoNeOjfI^1+hkZ;nwR-wBYr8R(JIz)Yqlal?)|H z+H({x%WR?DwaY1H*-mKct)O`8y00Rz3(ANWKsBB z^)N*JEeCVyxp=>ygS}TW!6;@fj(Iwiw|!RtAJ#5FE!P8Vy~c64KWi-(*o~q69zEbP zDGyU-6;a|Wc^oleFCV8f5^Xd-!qDl{dFwP=jFXZ=c|j_F_=Yy#&QieVb9(sie;&gy zOD%j|>dwZwOo7{1EKyLmlohI-=9jPXLf-{R?0!ryQ*w*NUi}&D&&F8#l)4GW{JSgA z5c3ng_cyLeZy%Q><%3E&(a_Xq0B(9?(Oka-&Uj1*YZ!uVB?lq(WhykhxdDrQGw@rQ z1D{^lK<)W^;Lhj4rt`AQtaShq9m`?kkO6j7po1@OYjXchvPb>&FCdnX!zno0VC^Fb zJb8UF*U_VaL!1?GyZdZ`ndqKr7P_eHE5qL%V+)^p9PspSAKuegm$gorilw|zxJf~Q z+#W8&*DKx#`l>cj_>DB=vOTG1dkWM}2w=f_0rW695h7OvGo8jzvbi|~Wa^Sx(3^Qw zIew)uGh!EWKEHviRZ^H@5YNKw^67%98u?^jWgRCf$#U;<690Xfjq=({6>=@K%ejLc zdD+AkNsU3re+dwf-oYfE+vCg~IbdYj#SAwL!I&w9z)4C_{E3I4;Z*{oybLJdKX-WX zU^hIT=}53sj-5WsL+3YdO30F-j}Kme+o}+{X|;idUXsAJlfO94mmYX%ra5dg>EwEs zBhGc%2x`N7xbxO_sCSxR>U&4nEvtx9^?$*!HyZ|Zk3iHDEo6l)@T%h^XDV-uA=55_ z%I$^B=HpPD9DN>M{5GJ>K})Zy=zR&t`MWEGb#MleRl7M6)Mh4fa%SUoe@dT_~lZHC!?}bHHyP;}BCWai$VD_FxP}rS`UQ!w) zH&_YIFLQ8hY$EX^&cHv(Qk=_x!GUWVz~5_CG}3vj8g;iiKUH3x5n0q#4bUooJI_V!%X)qgPMn*&`i7S%?3WU>CG zH|g39r4k2iygG9g{Qf*1+3}rR`@^je-0Xq><+gM29s8m2$4Injmxs`}OQ77Lf(vg< zfV1M?pli4ghX39OF3y^m8}^YCY%c-za$7WeI+0CUSOq=50B7A%Bm2|4U~l3?C^7TF ztb}^u@W2hQLt!FLJ1WLM(5wWR%Z|8qeVx!ah2Z8aEo>^UcW+Vo2!mCZKvM3xpe{@U z?SphdjT7Ty@@-Mu&4e}Hi4`c;BRV~jqJBdkVbYtQa3M+x=ZAN*CDzgyWG;cTU(3=` z2RY1@dk%}d^r+;d65_y4cx7cuix%sl(z<>w`?opm@g0fx^d#BNz1DO{+!rs)K4Y7W zTCZl@d?OT0uW}6Mnt3S_29rVJ!&|q+qm1QFf?a?@WI;dU?V+ZF* zp@G>FI4fVxY6J6O(YgX~3GHNq^ECM3a~U)|?_oD(A2MOcPk8eFGrROThGM>HqQQl^ z{4GC!e4>>jyy-TA%^dBE>GS%9WiN_Y=%=xG?CA*M`|%y@*ybVlccT(#v+g-o9t!La+ve%IxK7wqkHS;!T~26m?~7F z=)xq}7b3-K@2kPJ2I?A9(`%r449xhB(e{Y6Y4rNYJn&jd)`$ z-}qjz{i7pQ&vL}@v7^k+3yrwOqTQA>p8OyYB-4r#N za_tNkFUm86iZ=9KEuZU=`2k~IyVFF=4V+8L9?iA8LoGykT+uV{66**}_GCU1jFqH{idp$m=8x&nQn z8eFbl0NJX4hEIV~Y&6Buot6E-oe{F96S65%>pze?XHWjB+em)FZFpjwO^Yn6s3hes zJe+ryy3bdU)N38V#2KFW;8hNDk=iBvHxe;D=p_3u(3f9#&<4Afb}_}foA{HrrSQS= zpRDHcOn$k4C7hI2Al^lu-#oci(0s*^$}SZM|154`6E&S^%CN)2<1dl+eVa^2Ga|uF zmcxbTcL~N>Zi6f7Zuq|CEvMV^6guUs&_mY|g6|ALuR>|u)$ITpTb*#aOC5x{_HnCR zJTYi)I`>RxI+uUk6DKF9F&{MLn&(cySWzy0oYTh5K93Y!<_(IwcECe@Z|eKv1lxS- zz~Edk{r2C_ZC<$xPFpRaf$C#?&D&CV+@D4s{Ri2ViQ7T)K{0Jsv7tzxCdm9#MSG8B z(J8Gv@Z7tW+5Q+`kfX{Nem{_h8rB-c3TWN2~yWmgSBRAld#B$PC38wLWUx3QL!eZ74!S}SV zb>w>pO?w8D8ph%TNg>qT`U%~cp?G$904Ozn1;vZ=F!rA#KVSVdpw&Wb&^g5-N4|n~ z|An~F%9B2~|AWaI%dsN3ip1me@a3>U{@vt>7&>o_@Kohyrn$ruPk$2dW3_Lv=ND}8 z`t|L?$|X`1JVydYInLoOR;ttO^;^LHmn=L$EjkmYz>B&BQ1aEKf$}?S{2O)XYqFws zM`nBu<~;j(y`4sah}tH7{^@YwmJ-|7bqu z-3FXpdz_|-ufvE}Ml3nN7r$NoCXkpsf<27)#vczfg(p>_89s4FiIQXdkFC}0@U|f+ zkzmiIwH}hEa`F)fV5lyWYE1B=;W@2GADSXbv}T z+6Do(SGh4eZ1B?57LYiX%^f@N%zgwPUa?$gAAyGsPq#_8Zc#i6J( z+)&{Ewg+YA#hu6sBI z52oR1QP1BksfZd@1soaLk@RgDG-^A<$uv)*;(~6NUt$3x_Rga4vsn<%Zw9SDb12d4 ztiZ~0AI!cphvGh!v4Y?Apm!vW29`L`e7iyzsk@ofXOz%=$3-w&QGw=d)5bS8v$#Wg znlx&j23qfVA()^tj5-etK=gQ_FhACVly5hJ^tMg>t>rPqZIXahw3**Eej^R&4P^%= z{wMs~lSSqV5+rGUNZ?VGM^6{U(RDL#?$oQr+#gZ>yy=-dxAiyAB|P`RVuMI7`^XW_ z$a*w>SaB3l1~ zic~JRZ;YhZ50&t5;8{MkS`BT_45g0iLs{$>Wh_^+BU8t*>}>aUNK6_{xY3yP**8L< zlsnm;ea7eRSA~_ECehWV{i0r7jA@UKqi3^j@~a=eWd)s?q|q@(%G}9sirAY=?BfVmPV?_R_^f`5 z>1;m6{c5u0Bxb*3M}KR>V*A}J*;9@RdOhL%SdM;OHK$1>b;9*t`q=hVlDbbl6P)~| zgJJeE#3%J|r;Q}=PqQ}VUmOQ!b520QN+%lY7X(M|sK6uN=~S|C7#yEIguRQ~NCDT{ zIrt_Wf-47oi_fP!IIyd2F>FMl!)qdbKfEG7Rx4msrBUk zY#-ckQl{B~+9-E_8Qg79r7~9yyekl4o`-cwUU3kLZ^y#l$s=i6Ofx(`D}V>1r;_Iv zIWRpv24b96kl$==W;8Db3QuIvPyIpmb?FZH9=(NnUd*ST@6SPwrZmQ1@_-wzX4Ec_ z#*&V)Fm|UYeO{oA#buAVACU&MCfpJCynH6KSZ_o-OT%!_w|Ojhq63-FkH?zspDb<7 z6q+!8C6-N}MBN1mG%;cs4$LX1YNs?ZZ0g`DfB0bK?cr>7@DtAZ=0t3~wSa{QWx;HL z6V|WGVGpHUA)`$T;XxHs^4tK2$}Yj}(pHvoqy~Iu=z!Car_48-2ZN2~>~g0piT%3* zEfq499Bo3)R~q>bE&e!l@CMghsLC6g`rvSlHL!LmaDSGJ#thkgqGsd)mshTU+FnQD zYUVv|yZ3GoQ*40|1D@Q5`$d9P`DfvHJTLSOJj#@ZeTV1wV))QOUsC>Sf&nt`V2t!b znEK5Ki?&JOn|C+iVz~jHIWC2}>gpiN#SlwlQ7)y4KH)EIpF+!|_TEla(-Y_3XVmx-VUgrbvR6s*B;0-lNwrBZ4 zIB{+`ULJhKpOUx*H=tcMGpJtCaUh8IPfTzgWc` z8J6!Bg74n0qPm$P%;C~_{OnQ%E`rCr$yHy`bIPHy{xyGoZwT(sTLg;z6@1LGMR;J# z6)r01zK{i` zjXKO!!V3)*U4YAWW8#sX_*^Cz`XU3_*SRC{ocvL^xOyi0F-!?PesqG$mrYFDr5OyT zi|P~mDz?$Jj*Au_gzKUsWnN4&oAp{5o991eQ_q;wYfE#yJnb~Q6g?aZj3=pvy3yEVSVE961CUkLknFOa4x8FI`1MzS=^ zV0xUC#@afw*-nW$bn2@C{rq%{O{lPCvETeL^L3Hnl!6b-$n(Z`|5b1X?r19O0WQQvH`muje{p5Y-D4F0HySDAZ8^4rF*uB4{IS}MIQ*=m9TeABdCs* zLE~+0kS=i)bO)7DM@IspY8ia2*Tl3)d-h80G3=Raj20rSM*g-0K0lQSX|t^{j5R~c z%Omig?JyLd+69j7FJNA=GFs>SfZ_vE_`~`f%o!(+R@tgpe_ai(YsjFMgcgpS=*+IC zsNw!>T|9V7gOZk;qhYo=KHQT|1@GK3CFefq6i)+-sfK87{27A%C&5lfM;v=c0ZXjD zbGB!F@UyKknv@R;ZN$Uz&Qd${*WSdm{AXdNpEK^bDo=-(EX1XE+;ET0JSq!Vji#pK zaE3+$`JGEgIelwfo&N@+PVE7yyFNJQ&@(7lTMV9#5qL@WB+MO_0Qu#SIA!-tII(&T zC@4nakZOHFNZJ6fH2%v^$Mau2~D zS800vL5}7pHo)}PYVN=D{X0!1jpYsnM7Kum&F&7{_|O)9GghehnJDmjsiHWZGp*RZ<%E27C1A*6#WnNFze&> z5aOYN+Han*qPiEr*4~Bl>2KI=Hw`THp9W`U%TS(Z-AbN^^A?ttWN>FBx-CA-lGpo^ zp|uU}4fdvA&a-HK%wa*;Twkne(t%dLPPfM+C*px}9|)}1=1*-Og-aVUz<6jW|82J- z%1t>A(dR|^pr{_Mf9ZoY(bfF?`!1aIEOm74p3A2iS26Vu!|-gVJ-<`gjXr@RKJMDd zJ@*d44&PpZrg95+uh$pH|H$NC>K);vUyj8KkCmWF?JgH@qk|dJD`7sFfN91hka^q; z<-Q4U;;0J5-+2OcIyKNc(~;%v`w2!K4EEXC+TittKP0SwWxpgbp2H_Q0cQ(+Qzq^l(0 z97Y{wBa{UspDyHhy9qS5IfR_2g}_POaoA&iL7+RQ5+dCIhu=QJX@qn`R-GN19{S7; z9jS&Br>o$)l1^?eS)w(68hSU(=8n%EgO?gqploiQVD}>zOdT|4da`wb=?=m$X)==DeLl$%FD3NpQP@?t1Ca z?ge}3?d?jk=*waGBS+w{xg|__!FDE{XpPcS7&AND!rCV2puA@vyC2oXX8FE`re_MY zAzPC2{ntXrTwQv4M~l3)nQ+(Cp`^;0(3dO6SaiK56*pUueBd}beRB*oNm(I(@+y2$ zr~yB9cWh|801j5QkgDN>DmQB2lI?yNQ67Yk`rIMuSsPftn~AYMBZX0a9>Ay>iP)2t z%e*3ffuwu}-q6scdlTgGEfr&Y)LIJ2S4C-+cW|jD3nrCVkd(<+IF*zFd-FJ&F8>{( z^hdyRzsaPNKL`_R{RMxWBk8~edAvGe8jCcaMgBVt@%S4FI`nlBg-jiV{pPc&>2We? z8hhaS_Ies@%cYvu|M)mvbKDkrA3PS1WR=>I#JRjH4nqrX<5sctlO zMVr9f?e1*D{`n{sssXvJdF*oXeEhm7nY$fX%YKerh+oW)@>BPhGb^nmEO6Pu_BAbM zh4;7M!6`%N-2^YzH&~5_#Fx{&hEVoDJpA4vjTHU7nEfug#D5AJgGyGKGls8QXiY^nlAOYmPr~2!99_?=VJBW02#!tkrR7or z%JY)v{tOBs7OA}#TLunm4h`Z;#*N)aImu+w&`o5kMDPQe7y>0IT+xY z&~mVvbQDe~48z~4aa`e~hmgE|EJ_EgWFws<(C*SyydS1Z(T_B+SSnDck)wb`QaUKu zenOxTB8&T4To4M{xc23*L2b-bRN@WcU*dMyIyV|4gY6-{aX+UqV-}8xQG%&g0-3_7 z1$gjiJ?CAmOwJS6V}FZ3w`g<{xp?Pelx!(4`8^F1OnqsY2rH<`Tn``G7gFctP3&Z{ zJ^Y!GM8iCGGQXG)f`&6|DeGxDJGjM(UC3KYk(*brt><5}Iqqo`_MaQ`Q}(6Fy+y>5 zU0B(tZU3ve*!lbsM5*3|E8omect95KDO`sj2}_(S)WlxJJ>a<50{5nWuPdqhZp0eX zvKCSP7f1x*bqO1apYKfVe`lcXgnU8?clubTN8)ewaj?jX-@0ii<%qcKZBf74_O_YQ zyUYJzv1GeYNnk@Mod;lQJL8`|^`O;rE^wo_IWi5kSdtnY&9>UiV{k5=R3-Ik<&AK* zL~ASU9LOX+8DmyiK89vqTLX_@Uu6-V6UaGA!~s`cWBj>5`eURCH0&O`QW#3NS4aL|Kh!0gob>j%3z57|vQ#uY;4;XO| zqF%w4ie_&0H!%obqmGKX>sgXo0OX6i;HW_hf`c>RkD)m=28yFVa}#U-?L;Mq2H^1A zSQfGt=;4E{15-Z=c8_0z zfye8(8TmW8d6P5I&0U{$4mNTtn#*wX;9Iu*N&v^USEKgN5NehxYMXAh7heqSp$j&~ z{CBmzZLx3t@l{m{|DkjxUu5Zn<7-S<&Pk(rfR7)J_;+EwOC*=`=G$u%a6~D5k_HV&7F8?X}>N|>5zeJF`L;$_{G>(3%a6*%z zZnz(}vq@LY1;IC*QEl@@w(C%UVBA|%EbRZwa&o1FS37^e!r6M1KE|BS-kAZfKHAYh zPzs;pcva|gPe6B5()jw&Gi>bvUkYe<=KZ%!qR)wubo*mJKW5wqShCv$<+sYR(ZAn_ z@C-oBq?dfBa~sH2M&e5A-Mm(BDEO>cf*ty4{F#&;!bdf$as03Ye68djc6Y}njEa(G z-ZLC%|L8275a-N}3k&I}dkNO=f54cX8QN#$3qCCQ%+8sb;MZO2`3H&GG(uVtx9F>| zx0?m@qVWpoc-XK9Ooa6<0QlZ3&4%@@BIi9k@BMBgZ(EQ|a+c4S*ZcXx`_GffGcA(1 zxmScAYEodPfgbF5JPCJyu7wQ}a!_G64PC5{Lwtxi_rY^EIvN~-3rnO}ncgCN?^g?D z&xuXFxE?qC+6kNeM^nnxE!dcS7#g*=(9ND5xaRW>_#aJNw$px$4Qq#A>n&h;pEH>i zUxe{%b)m>}D(Ur|hx5io+&$%aG(^O$PtGjkgPjv7=HEkz-&n*}y-T7V+h0&nuSynO zsiN9V1ru`TlUC*ya$RYNBOU8$chPo=y_O62FO0|fJv#iUv#szfejL6T^O2vk`8xP7 zw#6LIgPm}E0D8O>`i94_^Fi;R_+mL+I}pac8Oq@UXI+6&p9@=PI2>nH9APJY9hj_) zH$L1uk+P?Svb-Bg;O^{;?`|rv(&<{TgY&|m#{>M4C6hsQnmsmz7V`&f_d-d&l&Joe z;*}14flyKXuOQ>YuiCATInuv`GflyyKdZ;yrdH-BSyP^ZB=&BN$`xv2l_pVcldInfJQ6JZ7 zv`tHiD0hE6w8Rcf!)-YChFFY0*u^hVT*bYgmxAU} z|FQUjsoZVFJe;*Cnv9%}3*MMcU|P*C=pM+!<*-pq)t1AjK7+6a|rb;5ez z0`}4LKj>V28#+@jvbA3$A#?I4(6{-;7C*hp`($b1={#9_u>2RZxbBRzgQcnJ@p77V zKL|%Gsu1pd?2AY26GIR350{o;F{mJR%K-Y7ND-u5@N!wNV(q zgv0QE1*|c(g|(~C!Yh_(tY)+aEzeEHvvmb*b-WcGmpli8!&NNe#W3^?e+awj1Pk!e z#J;$(`Ms{eq0ApK3 z8oojg-dTYA-FS~QBF|9;c_*ul?8OT!Jq&$9T!ca@-EKmY2s@tKTx4dmXIB z^EkMim!o}E@7XGyK5lfj4uwCIrs{q-_Uwl-{gY6o9B~C&;9yBtNHmZ6L;T4*^;A3tW_M=ZCT5&J0;>lPH3a)zN`GJjh`TN{b*cM+{t%z zUk0g*K{#lb%WK}>4D#0TsL^mwcy?kUw{Yu5ynRbS_~iZ+Rvf<>XD|IF+&k?_Sb|sEgiyU^6EvUw%6<6!0;ZUx!`oNBtaF|UZkbmNzfCo1gryz2mYP7t zr-cw|JQmkWDMO8O1O(mp$1MYLFyQ}zGcJq8R|kedu=G5>Yo(~@C z9|IM(5)kX@gh$mQVbarYocb|!-1#9Jc5QjheO}uJAzde6x1|)^>blIim3{(ybO!T| zIQAk@6RS+t!l$~SGhC(vuYIeGzvDD9s-l#aJtL2gdq?Ax>Yu`aqp#pzfge7* z_spG-DgeWm;aDm(5ft4r;ARxhM5WLN!U=u(?5_PH48KsrOMb8=y9pamxx1H7zEec& z-sNGmQa5YY9fA|A(_vPG4DIyMLih7Ukht5J(w~aq;@SIQm*g0Ff9fRE`CkRAU=Qk$ zH-)VEAHcKBpF+3%$2XmlMBQCs^mF}d7VEBn<4(lV9E(U&ou!O<^@{A~PYY<>HyQ^f z1hM)sLwFh#i0!${*rTC`xZP39FxqSyTf2A^uemb|)#lhR_r*(C_?oTglOxYUZhT{X zTIHDjK$l(q>q9|?6?nlfoP{slD$<_p#7`%tfmO(Od=lu!Box-ewaeo$#XZ&KV z1L{~W;$9Bu4rumO(y$I+bJc7ABs zei&(xO7@TTGmqYb@To7KI$DR(boY}W-C03nb2d|q+zs&6sU?-uM7JLFh&a6wnCsC6 zDKj%T=Lw@Q^8Ib7|9Fo35dk>&XDz4{S;L8c;TUnn7((YIK$_uNT;A#383Xpkr1`93Xgf#(5ka;pgZR<>~pclN?#jJyWtz~{npqn-ohPq zHN{EeG;q$8ZQRNK0FN18huu250Z*M#q=YJZ`%KjeX8@V8?3=`#@ zku&+=?%8<5aWlnvlnT_|)xjqJarjSbxX>o%Hi&2_=BNmr>oZI_jBK(k_PrjxTb4(UQvWgG?j(BCTS>l&di1LM82OLdLqQ2TR9~x(WoBj&E@MG@ z$5qi4!oj)Hj#j<=1M!!a!p5JjWYj3qxaBT{z@LFMW#@S4%MFL_kqc?cOE3QJ*AS>T zOrz~zn^>#&atL%PpwWxRQ1rhN*nV6Gw-)>c$>3ZTeZ&@(rhXUgoo063&jp9ncEZAY z_ZaI(JeHLWs#YIZbcHXL4djY6Mc-LNOB6PK@n#%;W3%=oqIi=uW$*aKE+5Rqyo!Z1 zewi#K$KGUD3!g*A!k29P>W8d6=n3SuD^izdKdY)c1(p3)JDbY>~Udl9Q*S0 zJxdN5fva^lvN{WCdTXqYCEl5=b*&nmcKZa+Ys*-VjUip$wFQ1ZYGN%TN6_8tWrEb2 zo2)I}gYHe=&Wde5vB}AigwO2g%{B>IXR&}ie`bc)*ItLOz1b`|*BBoMDWmQ6jcj+Z z45p5E!0}oBY)9`gV7gv7t_cMr;@ z?>4)ebOXLzl0+G?Uu?Cl3s{aGhHuTqNb}+s{>*4^l)3SREqU^b>6y;JyiIQzcRY?3 z^v}cpX1MY5I`vUG=`&M4uPIbqu8SKNsnXE2L++Di%V4RW9bF38ES%!E{K;9xnT(#;xJJntSb|UOk^N<`R94mvsJEyt! z3^V!~S`4whb6McOG4v$43~op2Q^hup>g1F$B*PHx6gAoVK|}oNsE>(t)7YLT7HH-p zhvPdpvo;at(Z2B-gg?n(9bN9&x-1ONA6&`SREOf!k)!#j${1EXZY63A9B1~TGsayj z8}ky~sn?>ES(^#j-kG92-Q`Gf1~1vJ%u$fD#)mTVC25!IJ+8?+ggl#7XsXNwA?iod z+Nm03pAf}9ro@u;fCkxJ?q%n_*U(L`AyiuIMLqdh^wis&Vl;{=qo+;IlCW*$(G;7s0A zHhyvl>WwvkcV?NM^kosn3kXMForK ztY9>~yqSs{(hrl{1qT`#FT+%>dSS&qQC5~WW7-)W=+nIgCI{KG^=;PJHM0d+IL9&` z41#ju4Uvv^Caamf0cOp50~!^%tXQm!-)H>~Om{W&?Hn@$n*i_cwubx-BuT z-hsWabifxsc{b3nit&+lIPPB$yX?K2RsYb#nhuc;+kr97KkospRmeQ}D*N^#72HK$ zBFEu3SeE$`VPpI-vh%phj6098a$R@Iig?4ak0R;nhtk3b5!OB^z;F?OZ+Pzph;weZ zU0}+M{UC;2GL~4nc%~rqfCiq5ltkm!O~Q{}Cg`+yC){m$CamnU#rylLIq`@7!uJV?5BF$$ol8vWBN4u(QmX z-np2w!Dl%FVPF8BSUr+oeocajalTl+%7jlU%4P-{P8i_wLvYLQ1XI4IiY3EUpn39H z*7*2{sAO6OZQo9_D|-sKSBhJ}xBDV9fIRlBcoPhH`;iF)94Ol$2zrFNaF-j6HA#;k zwqZJStJ~w)y3b%;wgG%gb+BXUU--qXfsuvxK~+i~?bpYENrN*~tun;+n=_$IO_lvL zcSZg}DmbLcQOLLm9F<=OOMEv`=F|jS`C$v(ZXlTa-k2ud+Xrq}+FFp14TI(l7FKW;10a zMRa#bg=^7=Sc$hfZoL!0zx>2lqlqC7-|&dNe?zQvf&-3ookecng{-CWqtMsS0k*E0 zfNvI-@QImb(7VzX-;CB}b6YF8*$*O-yBW#8#_!~-)YhQJriJX{_U$ZaQ$9x8Phe7Y zLuhQ`PPAX3$S`sR)$sc;B&3V?OKhh9@u50xpTSC(b13(r0AyxPV_z!WQ7Q8Q?2oo* zQ6{!nUoMFX&dTiSpdv2LRltGi!&uP*A$UuO`qifOY}^o8P(3ZekXnwjHEzS$^cN<$ zMeiAtke8&!S03p3^DUcLIu1VE(I71+8TugU3iT!SbVp+-1wZQL)apIy&uUk)e*U`c zbDqCQ!{tW}H;~;<4kT5YOpn_hGS3ZD$>8*4Qp$FreO5E+(u`m_vsR=PU$~l*Z%@XH z1&(l&!kGDu>A1wn8Z4*fu?4T^;;78y+!niCY)MfZp3@2CI|eFQgzH9h9kZF~F3n@- zGV*YLrY!ZzrL)78RajOzoxUp+vd(1(u*s%|o=rN;nzu*N_H9$4b#^?rUpSjCPxpc5 zrWf4AHFIg#H8F?~d&xy!i6e2NJ#B|Stl?6&rqY#AUv|CQp>4{r5=#H}nlX^)H5S%U zgX4ITUnDL3A5U;dY7ve48OJHuwZg(QAuL>OO$P<7@bB<3_%y*�|BG^enmHuNFjO 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 literal 0 HcmV?d00001 diff --git a/src/ui/MaterialEditorPanel.cpp b/src/ui/MaterialEditorPanel.cpp index 8c223bd..dfdf83b 100644 --- a/src/ui/MaterialEditorPanel.cpp +++ b/src/ui/MaterialEditorPanel.cpp @@ -293,6 +293,7 @@ void MaterialEditorPanel::RenderPreviewPanel() { m_preview.RenderRendererDropdown(halfWidth); ImGui::SameLine(); m_preview.RenderLightingDropdown(halfWidth); + m_preview.RenderShapeDropdown(-1.0f); if (m_stage && !m_materialPath.IsEmpty()) { // Hypershade-style: with a node selected, the ball previews that @@ -454,12 +455,13 @@ void MaterialEditorPanel::RenderInputValueWidget(const ShaderGraphNode& node, bool authored = false; pxr::VtValue current = ReadInputValue(prim, node.shaderId, input, &authored); - // Widget vocabulary mirrors PropertyPanel's DrawVtValueWidget (Input* - // fields with %.4f, applied on IsItemDeactivatedAfterEdit; ColorEdit - // applied live) — plus this panel's undo convention: pre-edit state is - // stashed on activation and one undoable command is pushed per edit. - pxr::VtValue newValue; // applied live (colors, checkbox) as it changes - pxr::VtValue finalValue; // applied + committed on deactivate (Input* fields) + // PropertyPanel-style table around draggable controls: Drag* widgets + // (Ctrl+click to type exact values) apply live every frame so the shader + // ball and viewport track the drag, with this panel's undo convention — + // pre-edit state stashed on activation, one undoable command pushed when + // the edit ends (IsItemDeactivatedAfterEdit). + pxr::VtValue newValue; // applied live as it changes (drags, colors, checkbox) + pxr::VtValue finalValue; // applied + committed on deactivate (text fields) auto scalarAsFloat = [¤t]() -> float { if (current.IsHolding()) return current.UncheckedGet(); @@ -482,34 +484,29 @@ void MaterialEditorPanel::RenderInputValueWidget(const ShaderGraphNode& node, } else if (type == tn->Float3 || type == tn->Vector3f || type == tn->Normal3f || type == tn->Point3f) { pxr::GfVec3f v = current.IsHolding() ? current.UncheckedGet() : pxr::GfVec3f(0.0f); - ImGui::InputScalarN(widgetId.c_str(), ImGuiDataType_Float, v.data(), 3, nullptr, nullptr, "%.4f"); - if (ImGui::IsItemDeactivatedAfterEdit()) - finalValue = v; + if (ImGui::DragFloat3(widgetId.c_str(), v.data(), 0.01f, 0.f, 0.f, "%.4f")) + newValue = v; } else if (type == tn->Float2 || type == tn->TexCoord2f) { pxr::GfVec2f v = current.IsHolding() ? current.UncheckedGet() : pxr::GfVec2f(0.0f); - ImGui::InputScalarN(widgetId.c_str(), ImGuiDataType_Float, v.data(), 2, nullptr, nullptr, "%.4f"); - if (ImGui::IsItemDeactivatedAfterEdit()) - finalValue = v; + if (ImGui::DragFloat2(widgetId.c_str(), v.data(), 0.01f, 0.f, 0.f, "%.4f")) + newValue = v; } else if (type == tn->Float4) { pxr::GfVec4f v = current.IsHolding() ? current.UncheckedGet() : pxr::GfVec4f(0.0f); - ImGui::InputScalarN(widgetId.c_str(), ImGuiDataType_Float, v.data(), 4, nullptr, nullptr, "%.4f"); - if (ImGui::IsItemDeactivatedAfterEdit()) - finalValue = v; + if (ImGui::DragFloat4(widgetId.c_str(), v.data(), 0.01f, 0.f, 0.f, "%.4f")) + newValue = v; } else if (type == tn->Float || type == tn->Double || type == tn->Half) { float f = scalarAsFloat(); - ImGui::InputFloat(widgetId.c_str(), &f, 0.f, 0.f, "%.4f"); - if (ImGui::IsItemDeactivatedAfterEdit()) { - if (type == tn->Double) finalValue = static_cast(f); - else if (type == tn->Half) finalValue = pxr::GfHalf(f); - else finalValue = f; + if (ImGui::DragFloat(widgetId.c_str(), &f, 0.01f, 0.f, 0.f, "%.4f")) { + if (type == tn->Double) newValue = static_cast(f); + else if (type == tn->Half) newValue = pxr::GfHalf(f); + else newValue = f; } } else if (type == tn->Int) { int v = current.IsHolding() ? current.UncheckedGet() : 0; - ImGui::InputInt(widgetId.c_str(), &v); - if (ImGui::IsItemDeactivatedAfterEdit()) - finalValue = v; + if (ImGui::DragInt(widgetId.c_str(), &v)) + newValue = v; } else if (type == tn->Bool) { bool v = current.IsHolding() && current.UncheckedGet(); if (ImGui::Checkbox(widgetId.c_str(), &v)) diff --git a/src/ui/MaterialPreviewRenderer.cpp b/src/ui/MaterialPreviewRenderer.cpp index 8a7c416..57b6e98 100644 --- a/src/ui/MaterialPreviewRenderer.cpp +++ b/src/ui/MaterialPreviewRenderer.cpp @@ -1,9 +1,17 @@ #include "MaterialPreviewRenderer.h" #include "../utils/Logger.h" #include "../utils/PathUtils.h" +#include #include +#include +#include +#include #include #include +#include +#include +#include +#include #include #include #include @@ -23,10 +31,26 @@ namespace UsdLayerManager { namespace { -const pxr::SdfPath kSpherePath("/Preview/Sphere"); +const pxr::SdfPath kShapesRootPath("/Preview/Shapes"); const pxr::SdfPath kDistantLightPath("/Preview/Light"); const pxr::SdfPath kDomeLightPath("/Preview/DomeLight"); +/// Selectable preview geometry; prim name is a child of kShapesRootPath. +/// Shapes backed by an asset file are only created when the file exists. +struct ShapePreset { + const char* label; + const char* primName; +}; +const ShapePreset kShapePresets[] = { + {"Sphere", "Sphere"}, + {"Cube", "Cube"}, + {"Cylinder", "Cylinder"}, + {"Teapot", "Teapot"}, + {"Hair", "Hair"}, + {"Cloud Volume", "Cloud"}, +}; +constexpr int kShapePresetCount = static_cast(sizeof(kShapePresets) / sizeof(kShapePresets[0])); + /// Scratch UsdPreviewSurface authored inside the (referenced) material scope /// when previewing a non-terminal node output — child of the material so the /// connection stays encapsulated. @@ -56,9 +80,51 @@ void MaterialPreviewRenderer::EnsureInitialized() { // Pin the up axis rather than inheriting the site fallback: the light // rig, dome-pole compensation, and camera framing all assume Y-up. pxr::UsdGeomSetStageUpAxis(m_previewStage, pxr::UsdGeomTokens->y); - pxr::UsdGeomSphere sphere = pxr::UsdGeomSphere::Define(m_previewStage, kSpherePath); + + // All selectable preview shapes live under one scope; the material is + // bound on the scope so it inherits to whichever shape is active, and + // ApplyPreviewShape keeps exactly one child active. + pxr::UsdGeomScope::Define(m_previewStage, kShapesRootPath); + + pxr::UsdGeomSphere sphere = pxr::UsdGeomSphere::Define( + m_previewStage, kShapesRootPath.AppendChild(pxr::TfToken("Sphere"))); sphere.GetRadiusAttr().Set(1.0); + pxr::UsdGeomCube cube = pxr::UsdGeomCube::Define( + m_previewStage, kShapesRootPath.AppendChild(pxr::TfToken("Cube"))); + cube.GetSizeAttr().Set(1.2); + cube.CreateExtentAttr(pxr::VtValue(pxr::VtVec3fArray{ + pxr::GfVec3f(-0.6f), pxr::GfVec3f(0.6f)})); + + pxr::UsdGeomCylinder cylinder = pxr::UsdGeomCylinder::Define( + m_previewStage, kShapesRootPath.AppendChild(pxr::TfToken("Cylinder"))); + cylinder.GetRadiusAttr().Set(0.7); + cylinder.GetHeightAttr().Set(1.6); + cylinder.GetAxisAttr().Set(pxr::UsdGeomTokens->y); // default Z lies on its side in a Y-up stage + cylinder.CreateExtentAttr(pxr::VtValue(pxr::VtVec3fArray{ + pxr::GfVec3f(-0.7f, -0.8f, -0.7f), pxr::GfVec3f(0.7f, 0.8f, 0.7f)})); + + // Asset-backed shapes, created only when their file is bundled. + for (const char* name : {"Teapot", "Hair"}) { + const std::string file = ResourcePath( + std::string("resources/preview/") + pxr::TfStringToLower(name) + ".usdc"); + if (!std::filesystem::exists(file)) continue; + pxr::UsdPrim prim = m_previewStage->DefinePrim( + kShapesRootPath.AppendChild(pxr::TfToken(name))); + prim.GetReferences().AddReference(file); + } + + const std::string vdbFile = ResourcePath("resources/vdb/bunny_cloud.vdb"); + if (std::filesystem::exists(vdbFile)) { + pxr::UsdVolVolume cloud = pxr::UsdVolVolume::Define( + m_previewStage, kShapesRootPath.AppendChild(pxr::TfToken("Cloud"))); + pxr::UsdVolOpenVDBAsset density = pxr::UsdVolOpenVDBAsset::Define( + m_previewStage, cloud.GetPath().AppendChild(pxr::TfToken("density"))); + density.CreateFilePathAttr().Set(pxr::SdfAssetPath(vdbFile)); + density.CreateFieldNameAttr().Set(pxr::TfToken("density")); + cloud.CreateFieldRelationship(pxr::TfToken("density"), density.GetPath()); + } + // Storm renders fine with zero authored lights (it injects a GL headlight // as a fallback), but proper Hydra delegates like Embree/Arnold shade // through scene lights only — without one they render solid black. @@ -83,9 +149,12 @@ void MaterialPreviewRenderer::EnsureInitialized() { m_renderer.SetComplexity(1.3f); ApplyLightPreset(m_lightPreset); + ApplyPreviewShape(m_previewShape); // activates one shape + frames the camera - m_previewBBox = pxr::GfBBox3d(m_renderer.ComputeStageBounds()); - m_camera.FrameSelection(m_previewBBox, 1.6); // extra margin so the sphere doesn't touch the frame edge + // Maya-style default 3/4 view: orbit the framed camera ~45° around and + // ~25° down instead of the dead-on front view (subsequent shape switches + // keep whatever orientation the user has orbited to). + m_camera.Tumble(-45.0, 25.0); m_renderer.SetCameraStateFromGfCamera(m_camera.ComputeGfCamera(m_previewBBox)); m_initialized = true; @@ -130,14 +199,14 @@ void MaterialPreviewRenderer::SetMaterial(const pxr::UsdStageRefPtr& sourceStage sourceStage->GetRootLayer()->GetIdentifier(), materialPath); } - pxr::UsdPrim spherePrim = m_previewStage->GetPrimAtPath(kSpherePath); + pxr::UsdPrim shapesRoot = m_previewStage->GetPrimAtPath(kShapesRootPath); pxr::UsdShadeMaterial material(m_previewStage->GetPrimAtPath(materialPath)); if (!material) { LOG_WARNING("MaterialPreviewRenderer: no material composed at " + materialPath.GetString()); return; } - if (spherePrim) - pxr::UsdShadeMaterialBindingAPI::Apply(spherePrim).Bind(material); + if (shapesRoot) // bound on the scope: inherits to whichever shape is active + pxr::UsdShadeMaterialBindingAPI::Apply(shapesRoot).Bind(material); // ── Selected-node preview (Hypershade-style) ───────────────────────── // All node-preview overrides (the scratch wrapper shader and the local @@ -220,6 +289,46 @@ void MaterialPreviewRenderer::ApplyLightPreset(int index) { m_dirty = true; } +void MaterialPreviewRenderer::ApplyPreviewShape(int index) { + if (index < 0 || index >= kShapePresetCount) index = 0; + // Fall back to the sphere if the requested shape's asset wasn't bundled. + if (!m_previewStage->GetPrimAtPath( + kShapesRootPath.AppendChild(pxr::TfToken(kShapePresets[index].primName)))) + index = 0; + m_previewShape = index; + + for (int i = 0; i < kShapePresetCount; ++i) { + pxr::UsdPrim prim = m_previewStage->GetPrimAtPath( + kShapesRootPath.AppendChild(pxr::TfToken(kShapePresets[i].primName))); + if (prim) + prim.SetActive(i == index); + } + + // Shapes differ wildly in size (the cloud VDB spans hundreds of units) — + // reframe on the active shape's bounds; inactive prims don't contribute. + m_previewBBox = pxr::GfBBox3d(m_renderer.ComputeStageBounds()); + m_camera.FrameSelection(m_previewBBox, 1.6); // margin so the shape doesn't touch the frame edge + m_renderer.SetCameraStateFromGfCamera(m_camera.ComputeGfCamera(m_previewBBox)); + m_dirty = true; +} + +void MaterialPreviewRenderer::RenderShapeDropdown(float width) { + if (!m_initialized) return; + ImGui::SetNextItemWidth(width); + if (ImGui::BeginCombo("##PreviewShape", kShapePresets[m_previewShape].label)) { + for (int i = 0; i < kShapePresetCount; ++i) { + if (!m_previewStage->GetPrimAtPath( + kShapesRootPath.AppendChild(pxr::TfToken(kShapePresets[i].primName)))) + continue; // asset not bundled + if (ImGui::Selectable(kShapePresets[i].label, i == m_previewShape) && i != m_previewShape) + ApplyPreviewShape(i); + } + ImGui::EndCombo(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Shader-ball preview geometry"); +} + void MaterialPreviewRenderer::UpdateDomeOrientation() { if (!m_previewStage) return; pxr::UsdPrim dome = m_previewStage->GetPrimAtPath(kDomeLightPath); diff --git a/src/ui/MaterialPreviewRenderer.h b/src/ui/MaterialPreviewRenderer.h index 02ac5ee..6a1a288 100644 --- a/src/ui/MaterialPreviewRenderer.h +++ b/src/ui/MaterialPreviewRenderer.h @@ -59,9 +59,17 @@ public: /// width: ImGui item width (-1 = fill available). void RenderLightingDropdown(float width = -1.0f); + /// Preview-geometry picker (Sphere / Cube / Cylinder / Teapot / Hair / + /// Cloud Volume). Shapes whose asset is missing are omitted. + /// width: ImGui item width (-1 = fill available). + void RenderShapeDropdown(float width = -1.0f); + private: void EnsureInitialized(); void ApplyLightPreset(int index); + /// Activates the chosen preview shape (deactivating the others) and + /// reframes the camera to its bounds. + void ApplyPreviewShape(int index); /// Compensates the dome-light pole convention per render delegate: Storm /// samples with the pole along local +Y, spec-following delegates /// (hdArnold, hdCycles) along local +Z. Called on preset and renderer @@ -78,6 +86,7 @@ private: pxr::SdfPath m_previewNodePath; // node whose output the ball previews (empty = whole material) std::string m_previewNodeOutput; int m_lightPreset = 0; + int m_previewShape = 0; bool m_initialized = false; bool m_dirty = true; int m_lastWidth = 0;