Introduce procedural motion generator

Removed pose editor and motion timeline editor.
Introduced simple procedural motion generator (Added experiment walk cycle).
master
huxingyi 2020-11-09 20:16:06 +09:30
parent e29bcf5619
commit 7a52133846
98 changed files with 3785 additions and 8053 deletions

View File

@ -1364,3 +1364,14 @@ https://www.reddit.com/r/gamedev/comments/5iuf3h/i_am_writting_a_3d_monster_mode
exception to your version of the library, but you are not obliged to do so.
If you do not wish to do so, delete this exception statement from your version.
</pre>
<h1>Prashanth Udupa</h1>
<pre>
https://www.vcreatelogic.com/index.php/2020/02/28/leveraging-shadow-maps-in-qt-without-using-qopenglframebufferobject/
</pre>
<h1>Ahmad Abdul Karim, Alexandre Meyer, Thibaut Gaudin, Axel Buendia and Saida Bouakaz</h1>
<pre>
Generic Spine Model with Simple Physics for Life-Like Quadrupeds and Reptiles
https://www.ahmadabdulkarim.com/?page_id=16
</pre>

View File

@ -278,36 +278,9 @@ HEADERS += src/skinnedmeshcreator.h
SOURCES += src/jointnodetree.cpp
HEADERS += src/jointnodetree.h
SOURCES += src/poser.cpp
HEADERS += src/poser.h
SOURCES += src/posemeshcreator.cpp
HEADERS += src/posemeshcreator.h
SOURCES += src/posepreviewmanager.cpp
HEADERS += src/posepreviewmanager.h
SOURCES += src/poseeditwidget.cpp
HEADERS += src/poseeditwidget.h
SOURCES += src/poselistwidget.cpp
HEADERS += src/poselistwidget.h
SOURCES += src/posemanagewidget.cpp
HEADERS += src/posemanagewidget.h
SOURCES += src/posepreviewsgenerator.cpp
HEADERS += src/posepreviewsgenerator.h
SOURCES += src/posewidget.cpp
HEADERS += src/posewidget.h
SOURCES += src/preferenceswidget.cpp
HEADERS += src/preferenceswidget.h
SOURCES += src/motioneditwidget.cpp
HEADERS += src/motioneditwidget.h
SOURCES += src/motionmanagewidget.cpp
HEADERS += src/motionmanagewidget.h
@ -320,9 +293,6 @@ HEADERS += src/motionwidget.h
SOURCES += src/motionsgenerator.cpp
HEADERS += src/motionsgenerator.h
SOURCES += src/animationclipplayer.cpp
HEADERS += src/animationclipplayer.h
SOURCES += src/texturetype.cpp
HEADERS += src/texturetype.h
@ -350,15 +320,6 @@ HEADERS += src/material.h
SOURCES += src/fbxfile.cpp
HEADERS += src/fbxfile.h
SOURCES += src/motiontimelinewidget.cpp
HEADERS += src/motiontimelinewidget.h
SOURCES += src/interpolationtype.cpp
HEADERS += src/interpolationtype.h
SOURCES += src/motionclipwidget.cpp
HEADERS += src/motionclipwidget.h
SOURCES += src/tabwidget.cpp
HEADERS += src/tabwidget.h
@ -377,18 +338,9 @@ HEADERS += src/uvunwrap.h
SOURCES += src/triangletangentresolve.cpp
HEADERS += src/triangletangentresolve.h
SOURCES += src/animalposer.cpp
HEADERS += src/animalposer.h
SOURCES += src/poserconstruct.cpp
HEADERS += src/poserconstruct.h
SOURCES += src/skeletondocument.cpp
HEADERS += src/skeletondocument.h
SOURCES += src/posedocument.cpp
HEADERS += src/posedocument.h
SOURCES += src/combinemode.cpp
HEADERS += src/combinemode.h
@ -544,6 +496,42 @@ HEADERS += src/remeshhole.h
SOURCES += src/centripetalcatmullromspline.cpp
HEADERS += src/centripetalcatmullromspline.h
SOURCES += src/simpleshadermesh.cpp
HEADERS += src/simpleshadermesh.h
SOURCES += src/simpleshadermeshbinder.cpp
HEADERS += src/simpleshadermeshbinder.h
SOURCES += src/simpleshaderwidget.cpp
HEADERS += src/simpleshaderwidget.h
SOURCES += src/blockmesh.cpp
HEADERS += src/blockmesh.h
SOURCES += src/planemesh.cpp
HEADERS += src/planemesh.h
SOURCES += src/hermitecurveinterpolation.cpp
HEADERS += src/hermitecurveinterpolation.h
SOURCES += src/genericspineandpseudophysics.cpp
HEADERS += src/genericspineandpseudophysics.h
SOURCES += src/chainsimulator.cpp
HEADERS += src/chainsimulator.h
SOURCES += src/vertebratamotion.cpp
HEADERS += src/vertebratamotion.h
SOURCES += src/simplerendermeshgenerator.cpp
HEADERS += src/simplerendermeshgenerator.h
SOURCES += src/motioneditwidget.cpp
HEADERS += src/motioneditwidget.h
SOURCES += src/vertebratamotionparameterswidget.cpp
HEADERS += src/vertebratamotionparameterswidget.h
SOURCES += src/main.cpp
HEADERS += src/version.h

View File

@ -39,7 +39,7 @@ DUST3D_DLL int DUST3D_API dust3dGetMeshTriangleAndQuadCount(dust3d *ds3
DUST3D_DLL void DUST3D_API dust3dGetMeshTriangleAndQuadIndices(dust3d *ds3, int *indices);
DUST3D_DLL void DUST3D_API dust3dClose(dust3d *ds3);
DUST3D_DLL int DUST3D_API dust3dError(dust3d *ds3);
DUST3D_DLL const char * DUST3D_API dust3dVersion(void);
DUST3D_DLL const char * DUST3D_API dust3dVersion();
#ifdef __cplusplus
}

View File

@ -30,6 +30,10 @@
<file>shaders/default.frag</file>
<file>shaders/default.core.vert</file>
<file>shaders/default.core.frag</file>
<file>shaders/scene.vert</file>
<file>shaders/scene.frag</file>
<file>shaders/shadow.vert</file>
<file>shaders/shadow.frag</file>
<file>thirdparty/three.js/dust3d.three.js</file>
<file>languages/dust3d_zh_CN.qm</file>
<file>ACKNOWLEDGEMENTS.html</file>

File diff suppressed because it is too large Load Diff

View File

@ -7,281 +7,281 @@ DUST3D 1.0 xml 0000000194
<?xml version="1.0" encoding="UTF-8"?>
<canvas originX="0.473267" originY="0.463367" originZ="1.43861" rigType="Animal">
<nodes>
<node id="{023a80e0-43e7-40e9-adb7-4ef5f3fa75aa}" partId="{9d71e1d8-5520-4917-9c9f-f696577a1cd2}" radius="0.0118812" x="0.269307" y="0.20198" z="1.8604"/>
<node id="{056a9f72-5ead-4159-b6a5-c212d83a2677}" partId="{f01e24d7-29d8-49c1-b37c-11f3e82a2892}" radius="0.0158416" x="0.39913" y="0.295249" z="1.07103"/>
<node id="{066f6612-c3f3-43fd-b76b-ccf438c4071e}" partId="{3cf19fde-79ef-433e-9318-716cb610a893}" radius="0.0237624" x="0.59604" y="0.842085" z="1.36627"/>
<node boneMark="Joint" id="{08f8c71c-a231-4e6a-9e15-661bd73a15fc}" partId="{62fe2f2c-316a-4281-9642-710ce43ee9c5}" radius="0.130693" x="0.469307" y="0.40396" z="1.4495"/>
<node id="{0ae3ec9b-8846-4555-991b-fbafd9a4154f}" partId="{3cf19fde-79ef-433e-9318-716cb610a893}" radius="0.0277228" x="0.60396" y="0.779759" z="1.34228"/>
<node id="{102b730d-9aed-455f-99c0-e8be9cac874e}" partId="{62fe2f2c-316a-4281-9642-710ce43ee9c5}" radius="0.0831683" x="0.469307" y="0.320792" z="1.72673"/>
<node id="{11d48e26-1748-42bd-b47b-017dcd512092}" partId="{1d5afc0c-8ef3-4e4a-9a56-d2e17062d666}" radius="0.0118812" x="0.627721" y="0.90951" z="1.89045"/>
<node id="{133d5bfb-c7df-4dcc-9b21-25e543dac08f}" partId="{e694d1b9-7b59-4983-8b5a-92146b4f9240}" radius="0.0129208" x="0.50308" y="0.329485" z="0.854391"/>
<node id="{173321be-2056-48ec-85b1-08aaa859d343}" partId="{94e49ac0-6887-4f3f-aa18-aa600351e205}" radius="0.019802" x="0.659502" y="0.955812" z="1.33644"/>
<node id="{175e6a16-6b47-4778-ab4f-44663443f223}" partId="{a40908d4-a52a-41af-984e-6ae82bae00b3}" radius="0.019802" x="0.310892" y="0.974075" z="1.12007"/>
<node id="{19df1fa9-30bc-46fe-9718-6e4b2fdfcea0}" partId="{62fe2f2c-316a-4281-9642-710ce43ee9c5}" radius="0.146535" x="0.469307" y="0.461386" z="1.32277"/>
<node id="{1a42548c-9759-433c-88df-0773544fc93e}" partId="{41dd5aed-f2be-4812-bbc2-7c9192b2859d}" radius="0.019802" x="0.624428" y="0.959104" z="1.33726"/>
<node id="{1d1270f2-522f-45b7-a5cd-1de2087f8394}" partId="{f01e24d7-29d8-49c1-b37c-11f3e82a2892}" radius="0.0158416" x="0.489302" y="0.457853" z="1.04091"/>
<node id="{1e0c5040-58f2-43c9-8fb4-c18c4e9cb856}" partId="{41dd5aed-f2be-4812-bbc2-7c9192b2859d}" radius="0.0118812" x="0.659404" y="0.952184" z="1.37126"/>
<node id="{1ebb942b-7519-48e7-b821-301572103a9d}" partId="{b2b41478-6984-4196-a44c-eb2015044599}" radius="0.0158416" x="0.473267" y="0.423762" z="0.777228"/>
<node id="{1fd89589-6f11-4083-b13b-a901f3361839}" partId="{3d579313-129f-465c-a9bf-3c4842ed74dd}" radius="0.00792079" x="0.556436" y="0.457426" z="1.04455"/>
<node id="{25526437-305a-48e8-b187-584666755165}" partId="{62fe2f2c-316a-4281-9642-710ce43ee9c5}" radius="0.130693" x="0.469307" y="0.364356" z="1.58614"/>
<node id="{27bbad83-de13-47ad-a2a0-948cccdaadd2}" partId="{a721a808-a940-40e9-b789-1816a9b1fdb8}" radius="0.0118812" x="0.928713" y="0.655446" z="1.32574"/>
<node id="{2b6b012d-7aa5-4007-bcdf-b6526580f8c8}" partId="{e694d1b9-7b59-4983-8b5a-92146b4f9240}" radius="0.0158416" x="0.52053" y="0.329057" z="0.843339"/>
<node id="{2c3dc485-7343-47b6-8c03-954ed6ab5de8}" partId="{3224e51d-8591-424e-ad66-7172b5c41b95}" radius="0.0277228" x="0.519433" y="0.289602" z="0.923667"/>
<node id="{2c495afe-b535-46cd-9320-f191921982a3}" partId="{a40908d4-a52a-41af-984e-6ae82bae00b3}" radius="0.0237624" x="0.350496" y="0.878607" z="1.17633"/>
<node id="{2d9609bb-4d5a-46f4-9706-d0c347f5f575}" partId="{62fe2f2c-316a-4281-9642-710ce43ee9c5}" radius="0.0792079" x="0.469307" y="0.354455" z="1.01881"/>
<node id="{2e4de01d-3a82-45de-a21d-dd9ace951611}" partId="{ca4a8faf-2872-43f8-b126-eb2134631b2b}" radius="0.019802" x="0.267995" y="0.970767" z="1.08317"/>
<node boneMark="Limb" id="{36b74bf8-1471-4510-8fad-137161307c17}" partId="{3cf19fde-79ef-433e-9318-716cb610a893}" radius="0.0633663" x="0.564356" y="0.568317" z="1.20099"/>
<node id="{3bcbbc2f-d1c9-48f9-8f53-c6c15a65c7d8}" partId="{ce2bba62-2f15-4c68-9418-c06540dd1631}" radius="0.0118812" x="0.316832" y="0.927329" z="1.73799"/>
<node id="{3c81b646-fb11-4c7a-903f-e5bf7121b6ab}" partId="{a993e970-3ff6-487a-a4f6-142680bcba44}" radius="0.0831683" x="0.473267" y="0.348515" z="0.89901"/>
<node id="{3db449a2-938e-49a6-8d71-d3b6179db330}" partId="{fda51f9f-2214-4b87-9a23-1b877b18720e}" radius="0.0118812" x="0.663364" y="0.950263" z="1.37078"/>
<node boneMark="Neck" id="{3f931589-a3ba-4eba-b0c0-c074ecf440c4}" partId="{a993e970-3ff6-487a-a4f6-142680bcba44}" radius="0.0712234" x="0.473267" y="0.348515" z="1.01986"/>
<node id="{4625811e-114f-4af6-b47f-acbe1aee124f}" partId="{3224e51d-8591-424e-ad66-7172b5c41b95}" radius="0.019802" x="0.549149" y="0.238266" z="0.945545"/>
<node id="{47095026-e7d1-4100-bd48-afdaa8b3e033}" partId="{7d3bfd96-a801-4d2d-9cc7-695946484697}" radius="0.019802" x="0.590765" y="0.924582" z="1.85924"/>
<node boneMark="Joint" id="{4a5aef40-d2fa-4204-89e0-0d9f5e70c7c1}" partId="{a40908d4-a52a-41af-984e-6ae82bae00b3}" radius="0.019802" x="0.336635" y="0.926871" z="1.17104"/>
<node id="{54f09f23-ac6c-4bac-a91c-25e6969a1ce3}" partId="{3d579313-129f-465c-a9bf-3c4842ed74dd}" radius="0.00792079" x="0.532673" y="0.433663" z="1.05644"/>
<node id="{583bcd24-d6d9-4e30-8100-991863b692cb}" partId="{9d71e1d8-5520-4917-9c9f-f696577a1cd2}" radius="0.0118812" x="0.227723" y="0.150495" z="1.8604"/>
<node boneMark="Joint" id="{5e5488ac-949f-460b-b63b-65adbbc7d027}" partId="{0b421427-908d-4e93-b15c-e16849cf1ba1}" radius="0.0435644" x="0.3576" y="0.639429" z="1.6588"/>
<node id="{5f9dcc9c-032e-42a4-948d-6dc92ee4faf1}" partId="{a993e970-3ff6-487a-a4f6-142680bcba44}" radius="0.0475248" x="0.473267" y="0.411881" z="0.837624"/>
<node id="{613faf35-b88c-4bc4-9a36-d7d5d0436dbe}" partId="{0b421427-908d-4e93-b15c-e16849cf1ba1}" radius="0.019802" x="0.328338" y="0.896674" z="1.76248"/>
<node id="{621a74b3-7853-45d9-8b18-3df5e2f89506}" partId="{0b421427-908d-4e93-b15c-e16849cf1ba1}" radius="0.0831683" x="0.380151" y="0.527216" z="1.64365"/>
<node id="{633b195a-814b-4317-a014-9bd3529243c2}" partId="{9fb2aeb4-9523-4780-accf-bea5ca94732d}" radius="0.0831683" x="0.564318" y="0.562204" z="1.66683"/>
<node id="{6364d658-7500-4f23-be78-de07110f0cad}" partId="{3cf19fde-79ef-433e-9318-716cb610a893}" radius="0.0752475" x="0.526733" y="0.477228" z="1.15941"/>
<node id="{64b2a8e8-8be5-4041-a467-3ad9a14605db}" partId="{a40908d4-a52a-41af-984e-6ae82bae00b3}" radius="0.039604" x="0.358417" y="0.669016" z="1.20309"/>
<node id="{68430a1a-cd14-4244-8ca4-f4fcc3f6e082}" partId="{9fb2aeb4-9523-4780-accf-bea5ca94732d}" radius="0.019802" x="0.605258" y="0.874465" z="1.89947"/>
<node boneMark="Limb" id="{6a024a4b-a397-4176-b46c-994f922de02a}" partId="{a40908d4-a52a-41af-984e-6ae82bae00b3}" radius="0.0633663" x="0.38218" y="0.560498" z="1.20909"/>
<node id="{6eaaa75a-ae01-4930-b607-05c3a8b0c75c}" partId="{9fb2aeb4-9523-4780-accf-bea5ca94732d}" radius="0.019802" x="0.593289" y="0.838226" z="1.8953"/>
<node id="{73eed031-997c-449f-b3e1-641bf1b8bc66}" partId="{3cf19fde-79ef-433e-9318-716cb610a893}" radius="0.039604" x="0.588119" y="0.658598" z="1.2615"/>
<node id="{79039736-e239-4cc9-8048-f5209c678d67}" partId="{9fb2aeb4-9523-4780-accf-bea5ca94732d}" radius="0.0237624" x="0.581631" y="0.795734" z="1.87472"/>
<node id="{7a2eda66-6b4a-4b3f-9fd8-83a5a12dfa91}" partId="{9d71e1d8-5520-4917-9c9f-f696577a1cd2}" radius="0.0158416" x="0.217822" y="0.132673" z="1.8604"/>
<node id="{7b26b07f-49ad-4ab0-9a97-a82a749a78b3}" partId="{b2b41478-6984-4196-a44c-eb2015044599}" radius="0.0158416" x="0.473267" y="0.409901" z="0.80198"/>
<node boneMark="Joint" id="{7b62e0e8-9e92-4af1-b605-82ee389515b8}" partId="{9fb2aeb4-9523-4780-accf-bea5ca94732d}" radius="0.019802" x="0.615418" y="0.908889" z="1.89281"/>
<node boneMark="Joint" id="{7c330ed5-d190-48a4-954c-d4bbcfcaa37e}" partId="{3cf19fde-79ef-433e-9318-716cb610a893}" radius="0.0277228" x="0.59802" y="0.722365" z="1.29664"/>
<node id="{7e029738-dc7c-4b74-bb67-17d5b9a1daf1}" partId="{3224e51d-8591-424e-ad66-7172b5c41b95}" radius="0.00792079" x="0.574982" y="0.228982" z="0.967327"/>
<node id="{8090775c-1908-4b93-96d8-ff533e152982}" partId="{5b93898b-08ba-4c7b-ae30-072119dc85a9}" radius="0.019802" x="0.303069" y="0.967375" z="1.08317"/>
<node boneMark="Joint" id="{821d83ad-f4a0-4127-807c-86256f56c3d6}" partId="{0b421427-908d-4e93-b15c-e16849cf1ba1}" radius="0.019802" x="0.316983" y="0.926699" z="1.7451"/>
<node id="{88a8c64f-4aff-4dff-a766-5a2af365529e}" partId="{62fe2f2c-316a-4281-9642-710ce43ee9c5}" radius="0.0871287" x="0.469307" y="0.376238" z="1.10099"/>
<node id="{8ba0dc3d-345b-49f1-991c-f4a8ab562ca9}" partId="{a993e970-3ff6-487a-a4f6-142680bcba44}" radius="0.0356436" x="0.473267" y="0.435644" z="0.79703"/>
<node id="{8d16f910-b1f0-46ab-9611-d81642aabcce}" partId="{75a5ca65-abc5-492c-917c-70fe2460afaa}" radius="0.019802" x="0.277896" y="0.942404" z="1.70677"/>
<node id="{8d5b108a-83a0-4bbb-8eda-22b1fde7c4a6}" partId="{a993e970-3ff6-487a-a4f6-142680bcba44}" radius="0.0554455" x="0.473267" y="0.394059" z="0.867327"/>
<node id="{8e49161d-c926-45f9-b0b4-1a897d93e257}" partId="{f01e24d7-29d8-49c1-b37c-11f3e82a2892}" radius="0.0158416" x="0.449226" y="0.466156" z="1.04003"/>
<node id="{8ef4569f-5495-489f-9fba-5b00ff21c11f}" partId="{4c3c4a67-e536-4d3b-80ac-7f8e1c490c63}" radius="0.0118812" x="0.517174" y="0.332284" z="0.854456"/>
<node id="{96930462-7869-475a-b4cc-cf9156833908}" partId="{62fe2f2c-316a-4281-9642-710ce43ee9c5}" radius="0.130693" x="0.469307" y="0.453465" z="1.20594"/>
<node id="{96b3deac-17ca-4677-b872-42508caa6a49}" partId="{0e9aeab7-d6b6-4235-acb8-40ea97945c84}" radius="0.019802" x="0.658638" y="0.924582" z="1.85924"/>
<node id="{97547a66-19e3-4b7e-bab8-6ea341002a1d}" partId="{ce2bba62-2f15-4c68-9418-c06540dd1631}" radius="0.019802" x="0.345769" y="0.942404" z="1.70677"/>
<node id="{976e3bae-7782-4d56-9102-cf0a36f9010f}" partId="{a721a808-a940-40e9-b789-1816a9b1fdb8}" radius="0.0118812" x="0.805941" y="0.655446" z="1.32574"/>
<node id="{9892449b-dea5-4ea9-8505-6ec248e21602}" partId="{cf781dce-ed78-4c86-997d-eeeaea1a4825}" radius="0.0118812" x="0.314852" y="0.927332" z="1.73798"/>
<node boneMark="Joint" id="{98c15e13-f685-47c7-b540-e3853a0c05ec}" partId="{9d71e1d8-5520-4917-9c9f-f696577a1cd2}" radius="0.0158416" x="0.356435" y="0.239604" z="1.86634"/>
<node id="{9c4cf20c-9cee-4662-8649-d781ecd9411b}" partId="{62fe2f2c-316a-4281-9642-710ce43ee9c5}" radius="0.122772" x="0.469307" y="0.356436" z="1.66337"/>
<node id="{9ed89d37-8a50-4c99-af34-5ae3d0eba099}" partId="{94e49ac0-6887-4f3f-aa18-aa600351e205}" radius="0.0118812" x="0.661384" y="0.950264" z="1.37077"/>
<node id="{9ef98bda-9176-4056-be98-8adbe212f8e0}" partId="{5b93898b-08ba-4c7b-ae30-072119dc85a9}" radius="0.0118812" x="0.304951" y="0.970296" z="1.11782"/>
<node id="{a11f70a2-9eb4-44d1-8fc6-05de70d4f7a3}" partId="{75a5ca65-abc5-492c-917c-70fe2460afaa}" radius="0.0118812" x="0.312872" y="0.929123" z="1.73883"/>
<node id="{a4597bbc-4b50-480c-a9f4-556347cef1f6}" partId="{a40908d4-a52a-41af-984e-6ae82bae00b3}" radius="0.0752475" x="0.419803" y="0.462728" z="1.2307"/>
<node id="{a69cf011-bfc6-431f-bcea-88ab4629012e}" partId="{a721a808-a940-40e9-b789-1816a9b1fdb8}" radius="0.0158416" x="0.908911" y="0.655446" z="1.32574"/>
<node boneMark="Joint" id="{a8226f62-d288-47ac-bbcd-3cdff15337e6}" partId="{3cf19fde-79ef-433e-9318-716cb610a893}" radius="0.019802" x="0.609901" y="0.883815" z="1.39109"/>
<node id="{ab44fe1a-dc80-4fa8-8c33-869f35b23fbb}" partId="{3cf19fde-79ef-433e-9318-716cb610a893}" radius="0.019802" x="0.635644" y="0.95219" z="1.37879"/>
<node boneMark="Limb" id="{aca4bc0a-4fb2-4333-9a89-d59a9c4f4de7}" partId="{9fb2aeb4-9523-4780-accf-bea5ca94732d}" radius="0.106931" x="0.531514" y="0.407615" z="1.64629"/>
<node boneMark="Joint" id="{af42e085-2c7b-48e8-b5cd-1cb8cfb631bd}" partId="{0b421427-908d-4e93-b15c-e16849cf1ba1}" radius="0.0277228" x="0.345347" y="0.760815" z="1.76771"/>
<node id="{b4495b2b-be38-42c3-847b-73e2fd5b5870}" partId="{f01e24d7-29d8-49c1-b37c-11f3e82a2892}" radius="0.0158416" x="0.53124" y="0.432878" z="1.04574"/>
<node id="{b5c68cfd-650e-4805-a4f7-51426864fc14}" partId="{0b421427-908d-4e93-b15c-e16849cf1ba1}" radius="0.0277228" x="0.349352" y="0.70664" z="1.70495"/>
<node id="{b8ab0228-438a-40d6-89ac-a3d9863a78e0}" partId="{f01e24d7-29d8-49c1-b37c-11f3e82a2892}" radius="0.0158416" x="0.380762" y="0.435698" z="1.04262"/>
<node boneMark="Tail" id="{b8e9ac77-9b1a-4dc2-8344-917b27c89b87}" partId="{9d71e1d8-5520-4917-9c9f-f696577a1cd2}" radius="0.019802" x="0.447525" y="0.279208" z="1.80891"/>
<node id="{b9a9601e-cdfb-456d-bb3f-5f88dbf32500}" partId="{0b421427-908d-4e93-b15c-e16849cf1ba1}" radius="0.0237624" x="0.354698" y="0.814999" z="1.76442"/>
<node id="{bb11999a-5e23-466c-ac7d-9c032eda1d83}" partId="{a40908d4-a52a-41af-984e-6ae82bae00b3}" radius="0.0277228" x="0.342576" y="0.814394" z="1.19468"/>
<node id="{bb35013a-12e7-475e-be03-6717831a2c74}" partId="{8c8fb7ed-f22a-461e-8819-aebb998c0e84}" radius="0.019802" x="0.560396" y="0.504951" z="1.05248"/>
<node id="{bbce9a5f-6b44-405f-9483-8e7cfe2a3087}" partId="{fda51f9f-2214-4b87-9a23-1b877b18720e}" radius="0.019802" x="0.692301" y="0.959104" z="1.33726"/>
<node id="{bbf72811-7f32-4039-9921-7e4c6cdf367b}" partId="{9d71e1d8-5520-4917-9c9f-f696577a1cd2}" radius="0.0158416" x="0.384158" y="0.239604" z="1.8604"/>
<node id="{bd729586-94d1-4284-8582-8d2da360ac5a}" partId="{f01e24d7-29d8-49c1-b37c-11f3e82a2892}" radius="0.0158416" x="0.444216" y="0.273729" z="1.08017"/>
<node id="{be743b33-6308-4175-becd-9ee0bab5c033}" partId="{a721a808-a940-40e9-b789-1816a9b1fdb8}" radius="0.0158416" x="0.825743" y="0.655446" z="1.32574"/>
<node id="{bfb5b901-f1f7-4a01-ab6f-9b55a4f3e7a8}" partId="{ca4a8faf-2872-43f8-b126-eb2134631b2b}" radius="0.0118812" x="0.302971" y="0.972277" z="1.11783"/>
<node id="{c15ffc82-4c53-4374-87ca-086c16adff43}" partId="{9fb2aeb4-9523-4780-accf-bea5ca94732d}" radius="0.0277228" x="0.589874" y="0.712877" z="1.78321"/>
<node id="{c1d08fe7-a509-4466-956b-e384d9aa088d}" partId="{0e9aeab7-d6b6-4235-acb8-40ea97945c84}" radius="0.0118812" x="0.629701" y="0.909507" z="1.89046"/>
<node id="{c2b190c8-35c0-42b3-bab9-0754598ecacb}" partId="{1d5afc0c-8ef3-4e4a-9a56-d2e17062d666}" radius="0.019802" x="0.625839" y="0.921507" z="1.85781"/>
<node id="{c6487a43-16dc-4f97-8b19-40543692464e}" partId="{a721a808-a940-40e9-b789-1816a9b1fdb8}" radius="0.0118812" x="0.885149" y="0.655446" z="1.32574"/>
<node id="{c91524bd-f6ba-4ca2-8b32-1c2768840d47}" partId="{a721a808-a940-40e9-b789-1816a9b1fdb8}" radius="0.0118812" x="0.849505" y="0.655446" z="1.32574"/>
<node id="{ca53f4b4-7444-47cb-ad7b-76d9ee7e1457}" partId="{9d71e1d8-5520-4917-9c9f-f696577a1cd2}" radius="0.0118812" x="0.29901" y="0.221782" z="1.86634"/>
<node boneMark="Joint" id="{cafa9c74-f5bd-47de-bee8-7cd2c831224d}" partId="{9d71e1d8-5520-4917-9c9f-f696577a1cd2}" radius="0.0158416" x="0.178218" y="0.116832" z="1.8604"/>
<node id="{cb123999-11b6-4591-82c7-2e0716d4fd0d}" partId="{f01e24d7-29d8-49c1-b37c-11f3e82a2892}" radius="0.0158416" x="0.40581" y="0.45287" z="1.04143"/>
<node id="{cba57de1-f600-41bd-82f4-1f725cecca71}" partId="{8c8fb7ed-f22a-461e-8819-aebb998c0e84}" radius="0.019802" x="0.554456" y="0.463366" z="1.05248"/>
<node id="{cc288173-3e64-437d-a139-02b8ba21a88c}" partId="{a40908d4-a52a-41af-984e-6ae82bae00b3}" radius="0.019802" x="0.322774" y="0.954901" z="1.14877"/>
<node id="{cdccbbfe-13bb-495d-a5a8-fc817fb80656}" partId="{be83e76b-6e07-4719-a513-f612609b57e3}" radius="0.0118812" x="0.306931" y="0.970297" z="1.11783"/>
<node id="{cebb06d0-9e3d-4d00-b671-87faa90606ec}" partId="{e694d1b9-7b59-4983-8b5a-92146b4f9240}" radius="0.0129208" x="0.542726" y="0.331556" z="0.855736"/>
<node id="{d0c9355c-7532-428d-91af-b0aacda51b06}" partId="{7d3bfd96-a801-4d2d-9cc7-695946484697}" radius="0.0118812" x="0.625741" y="0.911301" z="1.8913"/>
<node id="{d14bfe92-9324-41a4-a113-766cfb10b5ca}" partId="{9d71e1d8-5520-4917-9c9f-f696577a1cd2}" radius="0.00792079" x="0.0990099" y="0.0316832" z="1.83069"/>
<node boneMark="Limb" id="{d222223b-e828-4ed9-a100-3ac87684a4ea}" partId="{0b421427-908d-4e93-b15c-e16849cf1ba1}" radius="0.106931" x="0.408035" y="0.406148" z="1.64483"/>
<node boneMark="Joint" id="{db503a99-a319-49f2-b214-b5b2a006dafe}" partId="{9fb2aeb4-9523-4780-accf-bea5ca94732d}" radius="0.0435644" x="0.583324" y="0.664096" z="1.71769"/>
<node id="{e1e7b8a2-752e-4c66-8ca5-a0d4e7850aa1}" partId="{f01e24d7-29d8-49c1-b37c-11f3e82a2892}" radius="0.0158416" x="0.518074" y="0.264687" z="1.0756"/>
<node id="{e6482f6f-ec3e-41e1-b425-1fafab2a5581}" partId="{3cf19fde-79ef-433e-9318-716cb610a893}" radius="0.019802" x="0.623762" y="0.919605" z="1.39017"/>
<node id="{e8de381e-f820-4434-b06e-7bc7ed200b80}" partId="{a993e970-3ff6-487a-a4f6-142680bcba44}" radius="0.0831683" x="0.473267" y="0.338614" z="0.946535"/>
<node id="{e8f04abe-6e33-44c1-b860-8e0dd4d505c3}" partId="{be83e76b-6e07-4719-a513-f612609b57e3}" radius="0.019802" x="0.335868" y="0.970767" z="1.08317"/>
<node id="{e92cabcb-988a-4ff6-ab07-89f1f01d7588}" partId="{0b421427-908d-4e93-b15c-e16849cf1ba1}" radius="0.019802" x="0.341564" y="0.861468" z="1.77019"/>
<node boneMark="Joint" id="{ebef38af-a58a-46bd-8fc3-01104fe86aad}" partId="{9fb2aeb4-9523-4780-accf-bea5ca94732d}" radius="0.0277228" x="0.592802" y="0.743789" z="1.86019"/>
<node id="{ecb0921f-75e0-43e8-95bc-3ab9de5c3fbd}" partId="{f01e24d7-29d8-49c1-b37c-11f3e82a2892}" radius="0.0158416" x="0.360724" y="0.350763" z="1.06067"/>
<node id="{ecd6799e-e553-4ec8-9c5d-c6d9720f839e}" partId="{cf781dce-ed78-4c86-997d-eeeaea1a4825}" radius="0.019802" x="0.31297" y="0.939329" z="1.70534"/>
<node id="{efb9f9ce-9463-4c47-a12f-c7047a87758a}" partId="{9d71e1d8-5520-4917-9c9f-f696577a1cd2}" radius="0.0158416" x="0.241584" y="0.184158" z="1.8604"/>
<node id="{f14a06cb-83f0-456f-9a4f-1f32883da263}" partId="{9d71e1d8-5520-4917-9c9f-f696577a1cd2}" radius="0.0118812" x="0.132673" y="0.0712871" z="1.83861"/>
<node id="{f224a6aa-1026-4e50-9bdf-de48cd7d5b29}" partId="{f01e24d7-29d8-49c1-b37c-11f3e82a2892}" radius="0.0158416" x="0.570031" y="0.354276" z="1.05824"/>
<node id="{f5c879ae-8afa-4630-9d0a-76162c364f7f}" partId="{b2b41478-6984-4196-a44c-eb2015044599}" radius="0.019802" x="0.473267" y="0.417822" z="0.784158"/>
<node id="{f5cc172a-f273-437a-a325-c3d94ae53018}" partId="{9d71e1d8-5520-4917-9c9f-f696577a1cd2}" radius="0.019802" x="0.415841" y="0.257426" z="1.85446"/>
<node boneMark="Joint" id="{f9f0d428-5bb8-43b3-a02a-34885cc95ad3}" partId="{a40908d4-a52a-41af-984e-6ae82bae00b3}" radius="0.0277228" x="0.348516" y="0.74109" z="1.19277"/>
<node id="{fcfb83be-2cba-42fd-9dcf-55f55caccf04}" partId="{9d71e1d8-5520-4917-9c9f-f696577a1cd2}" radius="0.0356436" x="0.475247" y="0.324752" z="1.71782"/>
<node id="{023a80e0-43e7-40e9-adb7-4ef5f3fa75aa}" partId="{c4df1b3d-8a8c-43bd-abea-3d0df226dca4}" radius="0.0118812" x="0.269307" y="0.20198" z="1.8604"/>
<node id="{056a9f72-5ead-4159-b6a5-c212d83a2677}" partId="{09d488db-863d-4868-8890-ed2f8cd941df}" radius="0.0158416" x="0.39913" y="0.295249" z="1.07103"/>
<node id="{066f6612-c3f3-43fd-b76b-ccf438c4071e}" partId="{80cfca5d-e04a-4e08-94d9-f4225387107b}" radius="0.0237624" x="0.59604" y="0.842085" z="1.36627"/>
<node boneMark="Joint" id="{08f8c71c-a231-4e6a-9e15-661bd73a15fc}" partId="{af64c2d3-a4ff-4c82-894b-f22e2f7ce2e7}" radius="0.130693" x="0.469307" y="0.40396" z="1.4495"/>
<node id="{0ae3ec9b-8846-4555-991b-fbafd9a4154f}" partId="{80cfca5d-e04a-4e08-94d9-f4225387107b}" radius="0.0277228" x="0.60396" y="0.779759" z="1.34228"/>
<node id="{102b730d-9aed-455f-99c0-e8be9cac874e}" partId="{af64c2d3-a4ff-4c82-894b-f22e2f7ce2e7}" radius="0.0831683" x="0.469307" y="0.320792" z="1.72673"/>
<node id="{11d48e26-1748-42bd-b47b-017dcd512092}" partId="{6b686ee6-2eeb-4d81-a207-d2dea8fe764b}" radius="0.0118812" x="0.627721" y="0.90951" z="1.89045"/>
<node id="{133d5bfb-c7df-4dcc-9b21-25e543dac08f}" partId="{b8a1ab8c-a094-416d-9d13-00aca25e3b3e}" radius="0.0129208" x="0.50308" y="0.329485" z="0.854391"/>
<node id="{173321be-2056-48ec-85b1-08aaa859d343}" partId="{99aa4033-e6b7-456f-abef-b021acf490f6}" radius="0.019802" x="0.659502" y="0.955812" z="1.33644"/>
<node boneMark="Joint" id="{175e6a16-6b47-4778-ab4f-44663443f223}" partId="{dc4f431d-fa39-4577-ae64-de12cd1a7a33}" radius="0.019802" x="0.310892" y="0.974075" z="1.12007"/>
<node id="{19df1fa9-30bc-46fe-9718-6e4b2fdfcea0}" partId="{af64c2d3-a4ff-4c82-894b-f22e2f7ce2e7}" radius="0.146535" x="0.469307" y="0.461386" z="1.32277"/>
<node id="{1a42548c-9759-433c-88df-0773544fc93e}" partId="{ab27a4f2-6c05-46f4-b81a-c2b0453a9bb3}" radius="0.019802" x="0.624428" y="0.959104" z="1.33726"/>
<node id="{1d1270f2-522f-45b7-a5cd-1de2087f8394}" partId="{09d488db-863d-4868-8890-ed2f8cd941df}" radius="0.0158416" x="0.489302" y="0.457853" z="1.04091"/>
<node id="{1e0c5040-58f2-43c9-8fb4-c18c4e9cb856}" partId="{ab27a4f2-6c05-46f4-b81a-c2b0453a9bb3}" radius="0.0118812" x="0.659404" y="0.952184" z="1.37126"/>
<node id="{1ebb942b-7519-48e7-b821-301572103a9d}" partId="{054515ed-3093-40f5-87d3-214e15044bba}" radius="0.0158416" x="0.473267" y="0.423762" z="0.777228"/>
<node id="{1fd89589-6f11-4083-b13b-a901f3361839}" partId="{5b83b16a-09e1-4d56-acbd-7b6a612d150a}" radius="0.00792079" x="0.556436" y="0.457426" z="1.04455"/>
<node id="{25526437-305a-48e8-b187-584666755165}" partId="{af64c2d3-a4ff-4c82-894b-f22e2f7ce2e7}" radius="0.130693" x="0.469307" y="0.364356" z="1.58614"/>
<node id="{27bbad83-de13-47ad-a2a0-948cccdaadd2}" partId="{a77512d7-fe5f-465a-aa97-33e0ca928b51}" radius="0.0118812" x="0.928713" y="0.655446" z="1.32574"/>
<node id="{2b6b012d-7aa5-4007-bcdf-b6526580f8c8}" partId="{b8a1ab8c-a094-416d-9d13-00aca25e3b3e}" radius="0.0158416" x="0.52053" y="0.329057" z="0.843339"/>
<node id="{2c3dc485-7343-47b6-8c03-954ed6ab5de8}" partId="{8676598d-d624-4d86-b498-c76f8a1aa810}" radius="0.0277228" x="0.519433" y="0.289602" z="0.923667"/>
<node id="{2c495afe-b535-46cd-9320-f191921982a3}" partId="{dc4f431d-fa39-4577-ae64-de12cd1a7a33}" radius="0.0237624" x="0.350496" y="0.878607" z="1.17633"/>
<node id="{2d9609bb-4d5a-46f4-9706-d0c347f5f575}" partId="{af64c2d3-a4ff-4c82-894b-f22e2f7ce2e7}" radius="0.0792079" x="0.469307" y="0.354455" z="1.01881"/>
<node id="{2e4de01d-3a82-45de-a21d-dd9ace951611}" partId="{1c66ae95-c9c5-474d-b3db-ae2bccfd2f13}" radius="0.019802" x="0.267995" y="0.970767" z="1.08317"/>
<node boneMark="Limb" id="{36b74bf8-1471-4510-8fad-137161307c17}" partId="{80cfca5d-e04a-4e08-94d9-f4225387107b}" radius="0.0633663" x="0.564356" y="0.568317" z="1.20099"/>
<node id="{3bcbbc2f-d1c9-48f9-8f53-c6c15a65c7d8}" partId="{d27d045a-44e9-44fd-9d76-13eddc43aae1}" radius="0.0118812" x="0.316832" y="0.927329" z="1.73799"/>
<node id="{3c81b646-fb11-4c7a-903f-e5bf7121b6ab}" partId="{f8df3586-c5cc-497c-b394-e84e7873dc30}" radius="0.0831683" x="0.473267" y="0.348515" z="0.89901"/>
<node id="{3db449a2-938e-49a6-8d71-d3b6179db330}" partId="{a09f07af-91ad-412f-ae6b-1f06ff7808d3}" radius="0.0118812" x="0.663364" y="0.950263" z="1.37078"/>
<node boneMark="Neck" id="{3f931589-a3ba-4eba-b0c0-c074ecf440c4}" partId="{f8df3586-c5cc-497c-b394-e84e7873dc30}" radius="0.0712234" x="0.473267" y="0.348515" z="1.01986"/>
<node id="{4625811e-114f-4af6-b47f-acbe1aee124f}" partId="{8676598d-d624-4d86-b498-c76f8a1aa810}" radius="0.019802" x="0.549149" y="0.238266" z="0.945545"/>
<node id="{47095026-e7d1-4100-bd48-afdaa8b3e033}" partId="{3ed6f8ae-f85b-4bd2-b393-523c3c3e2d0b}" radius="0.019802" x="0.590765" y="0.924582" z="1.85924"/>
<node id="{4a5aef40-d2fa-4204-89e0-0d9f5e70c7c1}" partId="{dc4f431d-fa39-4577-ae64-de12cd1a7a33}" radius="0.019802" x="0.336635" y="0.926871" z="1.17104"/>
<node id="{54f09f23-ac6c-4bac-a91c-25e6969a1ce3}" partId="{5b83b16a-09e1-4d56-acbd-7b6a612d150a}" radius="0.00792079" x="0.532673" y="0.433663" z="1.05644"/>
<node id="{583bcd24-d6d9-4e30-8100-991863b692cb}" partId="{c4df1b3d-8a8c-43bd-abea-3d0df226dca4}" radius="0.0118812" x="0.227723" y="0.150495" z="1.8604"/>
<node boneMark="Joint" id="{5e5488ac-949f-460b-b63b-65adbbc7d027}" partId="{cada7dd1-4087-4257-998f-7151930b31d4}" radius="0.0435644" x="0.3576" y="0.639429" z="1.6588"/>
<node id="{5f9dcc9c-032e-42a4-948d-6dc92ee4faf1}" partId="{f8df3586-c5cc-497c-b394-e84e7873dc30}" radius="0.0475248" x="0.473267" y="0.411881" z="0.837624"/>
<node id="{613faf35-b88c-4bc4-9a36-d7d5d0436dbe}" partId="{cada7dd1-4087-4257-998f-7151930b31d4}" radius="0.019802" x="0.328338" y="0.896674" z="1.76248"/>
<node id="{621a74b3-7853-45d9-8b18-3df5e2f89506}" partId="{cada7dd1-4087-4257-998f-7151930b31d4}" radius="0.0831683" x="0.380151" y="0.527216" z="1.64365"/>
<node id="{633b195a-814b-4317-a014-9bd3529243c2}" partId="{fd44561f-05bc-48a4-9a48-45b730ae92a3}" radius="0.0831683" x="0.564318" y="0.562204" z="1.66683"/>
<node id="{6364d658-7500-4f23-be78-de07110f0cad}" partId="{80cfca5d-e04a-4e08-94d9-f4225387107b}" radius="0.0752475" x="0.526733" y="0.477228" z="1.15941"/>
<node id="{64b2a8e8-8be5-4041-a467-3ad9a14605db}" partId="{dc4f431d-fa39-4577-ae64-de12cd1a7a33}" radius="0.039604" x="0.358417" y="0.669016" z="1.20309"/>
<node id="{68430a1a-cd14-4244-8ca4-f4fcc3f6e082}" partId="{fd44561f-05bc-48a4-9a48-45b730ae92a3}" radius="0.019802" x="0.605258" y="0.874465" z="1.89947"/>
<node boneMark="Limb" id="{6a024a4b-a397-4176-b46c-994f922de02a}" partId="{dc4f431d-fa39-4577-ae64-de12cd1a7a33}" radius="0.0633663" x="0.38218" y="0.560498" z="1.20909"/>
<node id="{6eaaa75a-ae01-4930-b607-05c3a8b0c75c}" partId="{fd44561f-05bc-48a4-9a48-45b730ae92a3}" radius="0.019802" x="0.593289" y="0.838226" z="1.8953"/>
<node id="{73eed031-997c-449f-b3e1-641bf1b8bc66}" partId="{80cfca5d-e04a-4e08-94d9-f4225387107b}" radius="0.039604" x="0.588119" y="0.658598" z="1.2615"/>
<node id="{79039736-e239-4cc9-8048-f5209c678d67}" partId="{fd44561f-05bc-48a4-9a48-45b730ae92a3}" radius="0.0237624" x="0.581631" y="0.795734" z="1.87472"/>
<node id="{7a2eda66-6b4a-4b3f-9fd8-83a5a12dfa91}" partId="{c4df1b3d-8a8c-43bd-abea-3d0df226dca4}" radius="0.0158416" x="0.217822" y="0.132673" z="1.8604"/>
<node id="{7b26b07f-49ad-4ab0-9a97-a82a749a78b3}" partId="{054515ed-3093-40f5-87d3-214e15044bba}" radius="0.0158416" x="0.473267" y="0.409901" z="0.80198"/>
<node boneMark="Joint" id="{7b62e0e8-9e92-4af1-b605-82ee389515b8}" partId="{fd44561f-05bc-48a4-9a48-45b730ae92a3}" radius="0.019802" x="0.615418" y="0.908889" z="1.89281"/>
<node boneMark="Joint" id="{7c330ed5-d190-48a4-954c-d4bbcfcaa37e}" partId="{80cfca5d-e04a-4e08-94d9-f4225387107b}" radius="0.0277228" x="0.59802" y="0.722365" z="1.29664"/>
<node id="{7e029738-dc7c-4b74-bb67-17d5b9a1daf1}" partId="{8676598d-d624-4d86-b498-c76f8a1aa810}" radius="0.00792079" x="0.574982" y="0.228982" z="0.967327"/>
<node id="{8090775c-1908-4b93-96d8-ff533e152982}" partId="{cb40704a-692b-4078-9eb8-74a281aea1c8}" radius="0.019802" x="0.303069" y="0.967375" z="1.08317"/>
<node boneMark="Joint" id="{821d83ad-f4a0-4127-807c-86256f56c3d6}" partId="{cada7dd1-4087-4257-998f-7151930b31d4}" radius="0.019802" x="0.316983" y="0.926699" z="1.7451"/>
<node id="{88a8c64f-4aff-4dff-a766-5a2af365529e}" partId="{af64c2d3-a4ff-4c82-894b-f22e2f7ce2e7}" radius="0.0871287" x="0.469307" y="0.376238" z="1.10099"/>
<node id="{8ba0dc3d-345b-49f1-991c-f4a8ab562ca9}" partId="{f8df3586-c5cc-497c-b394-e84e7873dc30}" radius="0.0356436" x="0.473267" y="0.435644" z="0.79703"/>
<node id="{8d16f910-b1f0-46ab-9611-d81642aabcce}" partId="{61baa97d-912a-46da-ab8d-b3940204a8cf}" radius="0.019802" x="0.277896" y="0.942404" z="1.70677"/>
<node id="{8d5b108a-83a0-4bbb-8eda-22b1fde7c4a6}" partId="{f8df3586-c5cc-497c-b394-e84e7873dc30}" radius="0.0554455" x="0.473267" y="0.394059" z="0.867327"/>
<node id="{8e49161d-c926-45f9-b0b4-1a897d93e257}" partId="{09d488db-863d-4868-8890-ed2f8cd941df}" radius="0.0158416" x="0.449226" y="0.466156" z="1.04003"/>
<node id="{8ef4569f-5495-489f-9fba-5b00ff21c11f}" partId="{dc9e5599-dc68-409b-ae2d-1e6fa79a0310}" radius="0.0118812" x="0.517174" y="0.332284" z="0.854456"/>
<node id="{96930462-7869-475a-b4cc-cf9156833908}" partId="{af64c2d3-a4ff-4c82-894b-f22e2f7ce2e7}" radius="0.130693" x="0.469307" y="0.453465" z="1.20594"/>
<node id="{96b3deac-17ca-4677-b872-42508caa6a49}" partId="{48d5870b-b94b-4b8f-9347-f397293a610e}" radius="0.019802" x="0.658638" y="0.924582" z="1.85924"/>
<node id="{97547a66-19e3-4b7e-bab8-6ea341002a1d}" partId="{d27d045a-44e9-44fd-9d76-13eddc43aae1}" radius="0.019802" x="0.345769" y="0.942404" z="1.70677"/>
<node id="{976e3bae-7782-4d56-9102-cf0a36f9010f}" partId="{a77512d7-fe5f-465a-aa97-33e0ca928b51}" radius="0.0118812" x="0.805941" y="0.655446" z="1.32574"/>
<node id="{9892449b-dea5-4ea9-8505-6ec248e21602}" partId="{b7773748-17ff-4bcf-93ae-2fe6ab5a0fc6}" radius="0.0118812" x="0.314852" y="0.927332" z="1.73798"/>
<node boneMark="Joint" id="{98c15e13-f685-47c7-b540-e3853a0c05ec}" partId="{c4df1b3d-8a8c-43bd-abea-3d0df226dca4}" radius="0.0158416" x="0.356435" y="0.239604" z="1.86634"/>
<node id="{9c4cf20c-9cee-4662-8649-d781ecd9411b}" partId="{af64c2d3-a4ff-4c82-894b-f22e2f7ce2e7}" radius="0.122772" x="0.469307" y="0.356436" z="1.66337"/>
<node id="{9ed89d37-8a50-4c99-af34-5ae3d0eba099}" partId="{99aa4033-e6b7-456f-abef-b021acf490f6}" radius="0.0118812" x="0.661384" y="0.950264" z="1.37077"/>
<node id="{9ef98bda-9176-4056-be98-8adbe212f8e0}" partId="{cb40704a-692b-4078-9eb8-74a281aea1c8}" radius="0.0118812" x="0.304951" y="0.970296" z="1.11782"/>
<node id="{a11f70a2-9eb4-44d1-8fc6-05de70d4f7a3}" partId="{61baa97d-912a-46da-ab8d-b3940204a8cf}" radius="0.0118812" x="0.312872" y="0.929123" z="1.73883"/>
<node id="{a4597bbc-4b50-480c-a9f4-556347cef1f6}" partId="{dc4f431d-fa39-4577-ae64-de12cd1a7a33}" radius="0.0752475" x="0.419803" y="0.462728" z="1.2307"/>
<node id="{a69cf011-bfc6-431f-bcea-88ab4629012e}" partId="{a77512d7-fe5f-465a-aa97-33e0ca928b51}" radius="0.0158416" x="0.908911" y="0.655446" z="1.32574"/>
<node id="{a8226f62-d288-47ac-bbcd-3cdff15337e6}" partId="{80cfca5d-e04a-4e08-94d9-f4225387107b}" radius="0.019802" x="0.609901" y="0.883815" z="1.39109"/>
<node boneMark="Joint" id="{ab44fe1a-dc80-4fa8-8c33-869f35b23fbb}" partId="{80cfca5d-e04a-4e08-94d9-f4225387107b}" radius="0.019802" x="0.635644" y="0.95219" z="1.37879"/>
<node boneMark="Limb" id="{aca4bc0a-4fb2-4333-9a89-d59a9c4f4de7}" partId="{fd44561f-05bc-48a4-9a48-45b730ae92a3}" radius="0.106931" x="0.531514" y="0.407615" z="1.64629"/>
<node boneMark="Joint" id="{af42e085-2c7b-48e8-b5cd-1cb8cfb631bd}" partId="{cada7dd1-4087-4257-998f-7151930b31d4}" radius="0.0277228" x="0.345347" y="0.760815" z="1.76771"/>
<node id="{b4495b2b-be38-42c3-847b-73e2fd5b5870}" partId="{09d488db-863d-4868-8890-ed2f8cd941df}" radius="0.0158416" x="0.53124" y="0.432878" z="1.04574"/>
<node id="{b5c68cfd-650e-4805-a4f7-51426864fc14}" partId="{cada7dd1-4087-4257-998f-7151930b31d4}" radius="0.0277228" x="0.349352" y="0.70664" z="1.70495"/>
<node id="{b8ab0228-438a-40d6-89ac-a3d9863a78e0}" partId="{09d488db-863d-4868-8890-ed2f8cd941df}" radius="0.0158416" x="0.380762" y="0.435698" z="1.04262"/>
<node boneMark="Tail" id="{b8e9ac77-9b1a-4dc2-8344-917b27c89b87}" partId="{c4df1b3d-8a8c-43bd-abea-3d0df226dca4}" radius="0.019802" x="0.447525" y="0.279208" z="1.80891"/>
<node id="{b9a9601e-cdfb-456d-bb3f-5f88dbf32500}" partId="{cada7dd1-4087-4257-998f-7151930b31d4}" radius="0.0237624" x="0.354698" y="0.814999" z="1.76442"/>
<node id="{bb11999a-5e23-466c-ac7d-9c032eda1d83}" partId="{dc4f431d-fa39-4577-ae64-de12cd1a7a33}" radius="0.0277228" x="0.342576" y="0.814394" z="1.19468"/>
<node id="{bb35013a-12e7-475e-be03-6717831a2c74}" partId="{0e885ccb-6cd3-410c-bfdb-1b0cad02c80e}" radius="0.019802" x="0.560396" y="0.504951" z="1.05248"/>
<node id="{bbce9a5f-6b44-405f-9483-8e7cfe2a3087}" partId="{a09f07af-91ad-412f-ae6b-1f06ff7808d3}" radius="0.019802" x="0.692301" y="0.959104" z="1.33726"/>
<node id="{bbf72811-7f32-4039-9921-7e4c6cdf367b}" partId="{c4df1b3d-8a8c-43bd-abea-3d0df226dca4}" radius="0.0158416" x="0.384158" y="0.239604" z="1.8604"/>
<node id="{bd729586-94d1-4284-8582-8d2da360ac5a}" partId="{09d488db-863d-4868-8890-ed2f8cd941df}" radius="0.0158416" x="0.444216" y="0.273729" z="1.08017"/>
<node id="{be743b33-6308-4175-becd-9ee0bab5c033}" partId="{a77512d7-fe5f-465a-aa97-33e0ca928b51}" radius="0.0158416" x="0.825743" y="0.655446" z="1.32574"/>
<node id="{bfb5b901-f1f7-4a01-ab6f-9b55a4f3e7a8}" partId="{1c66ae95-c9c5-474d-b3db-ae2bccfd2f13}" radius="0.0118812" x="0.302971" y="0.972277" z="1.11783"/>
<node id="{c15ffc82-4c53-4374-87ca-086c16adff43}" partId="{fd44561f-05bc-48a4-9a48-45b730ae92a3}" radius="0.0277228" x="0.589874" y="0.712877" z="1.78321"/>
<node id="{c1d08fe7-a509-4466-956b-e384d9aa088d}" partId="{48d5870b-b94b-4b8f-9347-f397293a610e}" radius="0.0118812" x="0.629701" y="0.909507" z="1.89046"/>
<node id="{c2b190c8-35c0-42b3-bab9-0754598ecacb}" partId="{6b686ee6-2eeb-4d81-a207-d2dea8fe764b}" radius="0.019802" x="0.625839" y="0.921507" z="1.85781"/>
<node id="{c6487a43-16dc-4f97-8b19-40543692464e}" partId="{a77512d7-fe5f-465a-aa97-33e0ca928b51}" radius="0.0118812" x="0.885149" y="0.655446" z="1.32574"/>
<node id="{c91524bd-f6ba-4ca2-8b32-1c2768840d47}" partId="{a77512d7-fe5f-465a-aa97-33e0ca928b51}" radius="0.0118812" x="0.849505" y="0.655446" z="1.32574"/>
<node id="{ca53f4b4-7444-47cb-ad7b-76d9ee7e1457}" partId="{c4df1b3d-8a8c-43bd-abea-3d0df226dca4}" radius="0.0118812" x="0.29901" y="0.221782" z="1.86634"/>
<node boneMark="Joint" id="{cafa9c74-f5bd-47de-bee8-7cd2c831224d}" partId="{c4df1b3d-8a8c-43bd-abea-3d0df226dca4}" radius="0.0158416" x="0.178218" y="0.116832" z="1.8604"/>
<node id="{cb123999-11b6-4591-82c7-2e0716d4fd0d}" partId="{09d488db-863d-4868-8890-ed2f8cd941df}" radius="0.0158416" x="0.40581" y="0.45287" z="1.04143"/>
<node id="{cba57de1-f600-41bd-82f4-1f725cecca71}" partId="{0e885ccb-6cd3-410c-bfdb-1b0cad02c80e}" radius="0.019802" x="0.554456" y="0.463366" z="1.05248"/>
<node id="{cc288173-3e64-437d-a139-02b8ba21a88c}" partId="{dc4f431d-fa39-4577-ae64-de12cd1a7a33}" radius="0.019802" x="0.322774" y="0.954901" z="1.14877"/>
<node id="{cdccbbfe-13bb-495d-a5a8-fc817fb80656}" partId="{accf6745-05a1-44e4-ab9d-9e4e5b61a0c6}" radius="0.0118812" x="0.306931" y="0.970297" z="1.11783"/>
<node id="{cebb06d0-9e3d-4d00-b671-87faa90606ec}" partId="{b8a1ab8c-a094-416d-9d13-00aca25e3b3e}" radius="0.0129208" x="0.542726" y="0.331556" z="0.855736"/>
<node id="{d0c9355c-7532-428d-91af-b0aacda51b06}" partId="{3ed6f8ae-f85b-4bd2-b393-523c3c3e2d0b}" radius="0.0118812" x="0.625741" y="0.911301" z="1.8913"/>
<node id="{d14bfe92-9324-41a4-a113-766cfb10b5ca}" partId="{c4df1b3d-8a8c-43bd-abea-3d0df226dca4}" radius="0.00792079" x="0.0990099" y="0.0316832" z="1.83069"/>
<node boneMark="Limb" id="{d222223b-e828-4ed9-a100-3ac87684a4ea}" partId="{cada7dd1-4087-4257-998f-7151930b31d4}" radius="0.106931" x="0.408035" y="0.406148" z="1.64483"/>
<node boneMark="Joint" id="{db503a99-a319-49f2-b214-b5b2a006dafe}" partId="{fd44561f-05bc-48a4-9a48-45b730ae92a3}" radius="0.0435644" x="0.583324" y="0.664096" z="1.71769"/>
<node id="{e1e7b8a2-752e-4c66-8ca5-a0d4e7850aa1}" partId="{09d488db-863d-4868-8890-ed2f8cd941df}" radius="0.0158416" x="0.518074" y="0.264687" z="1.0756"/>
<node id="{e6482f6f-ec3e-41e1-b425-1fafab2a5581}" partId="{80cfca5d-e04a-4e08-94d9-f4225387107b}" radius="0.019802" x="0.623762" y="0.919605" z="1.39017"/>
<node id="{e8de381e-f820-4434-b06e-7bc7ed200b80}" partId="{f8df3586-c5cc-497c-b394-e84e7873dc30}" radius="0.0831683" x="0.473267" y="0.338614" z="0.946535"/>
<node id="{e8f04abe-6e33-44c1-b860-8e0dd4d505c3}" partId="{accf6745-05a1-44e4-ab9d-9e4e5b61a0c6}" radius="0.019802" x="0.335868" y="0.970767" z="1.08317"/>
<node id="{e92cabcb-988a-4ff6-ab07-89f1f01d7588}" partId="{cada7dd1-4087-4257-998f-7151930b31d4}" radius="0.019802" x="0.341564" y="0.861468" z="1.77019"/>
<node boneMark="Joint" id="{ebef38af-a58a-46bd-8fc3-01104fe86aad}" partId="{fd44561f-05bc-48a4-9a48-45b730ae92a3}" radius="0.0277228" x="0.592802" y="0.743789" z="1.86019"/>
<node id="{ecb0921f-75e0-43e8-95bc-3ab9de5c3fbd}" partId="{09d488db-863d-4868-8890-ed2f8cd941df}" radius="0.0158416" x="0.360724" y="0.350763" z="1.06067"/>
<node id="{ecd6799e-e553-4ec8-9c5d-c6d9720f839e}" partId="{b7773748-17ff-4bcf-93ae-2fe6ab5a0fc6}" radius="0.019802" x="0.31297" y="0.939329" z="1.70534"/>
<node id="{efb9f9ce-9463-4c47-a12f-c7047a87758a}" partId="{c4df1b3d-8a8c-43bd-abea-3d0df226dca4}" radius="0.0158416" x="0.241584" y="0.184158" z="1.8604"/>
<node id="{f14a06cb-83f0-456f-9a4f-1f32883da263}" partId="{c4df1b3d-8a8c-43bd-abea-3d0df226dca4}" radius="0.0118812" x="0.132673" y="0.0712871" z="1.83861"/>
<node id="{f224a6aa-1026-4e50-9bdf-de48cd7d5b29}" partId="{09d488db-863d-4868-8890-ed2f8cd941df}" radius="0.0158416" x="0.570031" y="0.354276" z="1.05824"/>
<node id="{f5c879ae-8afa-4630-9d0a-76162c364f7f}" partId="{054515ed-3093-40f5-87d3-214e15044bba}" radius="0.019802" x="0.473267" y="0.417822" z="0.784158"/>
<node id="{f5cc172a-f273-437a-a325-c3d94ae53018}" partId="{c4df1b3d-8a8c-43bd-abea-3d0df226dca4}" radius="0.019802" x="0.415841" y="0.257426" z="1.85446"/>
<node boneMark="Joint" id="{f9f0d428-5bb8-43b3-a02a-34885cc95ad3}" partId="{dc4f431d-fa39-4577-ae64-de12cd1a7a33}" radius="0.0277228" x="0.348516" y="0.74109" z="1.19277"/>
<node id="{fcfb83be-2cba-42fd-9dcf-55f55caccf04}" partId="{c4df1b3d-8a8c-43bd-abea-3d0df226dca4}" radius="0.0356436" x="0.475247" y="0.324752" z="1.71782"/>
</nodes>
<edges>
<edge from="{2c3dc485-7343-47b6-8c03-954ed6ab5de8}" id="{03c28f36-b688-4baf-a7b1-5117cf679bbb}" partId="{3224e51d-8591-424e-ad66-7172b5c41b95}" to="{4625811e-114f-4af6-b47f-acbe1aee124f}"/>
<edge from="{bbf72811-7f32-4039-9921-7e4c6cdf367b}" id="{0c2cc283-75eb-456d-9937-1633bb89ba47}" partId="{9d71e1d8-5520-4917-9c9f-f696577a1cd2}" to="{98c15e13-f685-47c7-b540-e3853a0c05ec}"/>
<edge from="{e92cabcb-988a-4ff6-ab07-89f1f01d7588}" id="{0e3125c1-bc2e-4ed1-a4a0-34d57fbb28cb}" partId="{0b421427-908d-4e93-b15c-e16849cf1ba1}" to="{613faf35-b88c-4bc4-9a36-d7d5d0436dbe}"/>
<edge from="{db503a99-a319-49f2-b214-b5b2a006dafe}" id="{12044065-1f30-45cf-bc80-b227c525eb14}" partId="{9fb2aeb4-9523-4780-accf-bea5ca94732d}" to="{c15ffc82-4c53-4374-87ca-086c16adff43}"/>
<edge from="{88a8c64f-4aff-4dff-a766-5a2af365529e}" id="{139f04fc-00d3-44ea-9fc2-6b6c5b7053bc}" partId="{62fe2f2c-316a-4281-9642-710ce43ee9c5}" to="{96930462-7869-475a-b4cc-cf9156833908}"/>
<edge from="{be743b33-6308-4175-becd-9ee0bab5c033}" id="{14d85597-1b5a-4982-862e-3a89c4d80209}" partId="{a721a808-a940-40e9-b789-1816a9b1fdb8}" to="{c91524bd-f6ba-4ca2-8b32-1c2768840d47}"/>
<edge from="{d0c9355c-7532-428d-91af-b0aacda51b06}" id="{157c55db-457c-422b-aef7-ceaaf49ad2c2}" partId="{7d3bfd96-a801-4d2d-9cc7-695946484697}" to="{47095026-e7d1-4100-bd48-afdaa8b3e033}"/>
<edge from="{98c15e13-f685-47c7-b540-e3853a0c05ec}" id="{1888813a-8c1a-4055-9234-62980b1d19b5}" partId="{9d71e1d8-5520-4917-9c9f-f696577a1cd2}" to="{ca53f4b4-7444-47cb-ad7b-76d9ee7e1457}"/>
<edge from="{25526437-305a-48e8-b187-584666755165}" id="{1cc2cfbd-a7aa-4ab2-90db-b17df6ff95e7}" partId="{62fe2f2c-316a-4281-9642-710ce43ee9c5}" to="{9c4cf20c-9cee-4662-8649-d781ecd9411b}"/>
<edge from="{a8226f62-d288-47ac-bbcd-3cdff15337e6}" id="{2393f058-06a8-4845-8f4b-721aa0f2a426}" partId="{3cf19fde-79ef-433e-9318-716cb610a893}" to="{e6482f6f-ec3e-41e1-b425-1fafab2a5581}"/>
<edge from="{c6487a43-16dc-4f97-8b19-40543692464e}" id="{2483380d-a704-425e-8603-c4d314b38381}" partId="{a721a808-a940-40e9-b789-1816a9b1fdb8}" to="{a69cf011-bfc6-431f-bcea-88ab4629012e}"/>
<edge from="{f14a06cb-83f0-456f-9a4f-1f32883da263}" id="{258a1121-19ff-432e-98b1-d359c2dd53fd}" partId="{9d71e1d8-5520-4917-9c9f-f696577a1cd2}" to="{d14bfe92-9324-41a4-a113-766cfb10b5ca}"/>
<edge from="{a11f70a2-9eb4-44d1-8fc6-05de70d4f7a3}" id="{261764c2-6bac-499c-9039-bad06c2d195e}" partId="{75a5ca65-abc5-492c-917c-70fe2460afaa}" to="{8d16f910-b1f0-46ab-9611-d81642aabcce}"/>
<edge from="{96930462-7869-475a-b4cc-cf9156833908}" id="{2b414be6-af5d-45f6-bee7-c227d9b51f7a}" partId="{62fe2f2c-316a-4281-9642-710ce43ee9c5}" to="{19df1fa9-30bc-46fe-9718-6e4b2fdfcea0}"/>
<edge from="{b8e9ac77-9b1a-4dc2-8344-917b27c89b87}" id="{34324e9f-cb10-48d0-a355-4d56929af96d}" partId="{9d71e1d8-5520-4917-9c9f-f696577a1cd2}" to="{f5cc172a-f273-437a-a325-c3d94ae53018}"/>
<edge from="{19df1fa9-30bc-46fe-9718-6e4b2fdfcea0}" id="{355ef96c-3a7b-4301-ae5f-917aaa632ca9}" partId="{62fe2f2c-316a-4281-9642-710ce43ee9c5}" to="{08f8c71c-a231-4e6a-9e15-661bd73a15fc}"/>
<edge from="{0ae3ec9b-8846-4555-991b-fbafd9a4154f}" id="{3adecb1b-c4bc-48f4-8df4-20be03b896c9}" partId="{3cf19fde-79ef-433e-9318-716cb610a893}" to="{066f6612-c3f3-43fd-b76b-ccf438c4071e}"/>
<edge from="{2c495afe-b535-46cd-9320-f191921982a3}" id="{3da3e6b1-bd3a-40d7-b818-ed0d3498adc1}" partId="{a40908d4-a52a-41af-984e-6ae82bae00b3}" to="{4a5aef40-d2fa-4204-89e0-0d9f5e70c7c1}"/>
<edge from="{d222223b-e828-4ed9-a100-3ac87684a4ea}" id="{4209a6de-9b44-4292-b01f-5a16668de3da}" partId="{0b421427-908d-4e93-b15c-e16849cf1ba1}" to="{621a74b3-7853-45d9-8b18-3df5e2f89506}"/>
<edge from="{cafa9c74-f5bd-47de-bee8-7cd2c831224d}" id="{442c9e62-4793-4102-acbc-60c5415ac092}" partId="{9d71e1d8-5520-4917-9c9f-f696577a1cd2}" to="{f14a06cb-83f0-456f-9a4f-1f32883da263}"/>
<edge from="{4625811e-114f-4af6-b47f-acbe1aee124f}" id="{453d7c5e-c966-444f-ba52-5f8aafd865f0}" partId="{3224e51d-8591-424e-ad66-7172b5c41b95}" to="{7e029738-dc7c-4b74-bb67-17d5b9a1daf1}"/>
<edge from="{cdccbbfe-13bb-495d-a5a8-fc817fb80656}" id="{45c45716-63b7-4fa4-9380-4255f3f61d57}" partId="{be83e76b-6e07-4719-a513-f612609b57e3}" to="{e8f04abe-6e33-44c1-b860-8e0dd4d505c3}"/>
<edge from="{ecb0921f-75e0-43e8-95bc-3ab9de5c3fbd}" id="{4b575c3e-1c67-4d90-b865-86ac49a3f0cd}" partId="{f01e24d7-29d8-49c1-b37c-11f3e82a2892}" to="{b8ab0228-438a-40d6-89ac-a3d9863a78e0}"/>
<edge from="{79039736-e239-4cc9-8048-f5209c678d67}" id="{4c60e711-ddab-4d29-9e46-74912b48a56d}" partId="{9fb2aeb4-9523-4780-accf-bea5ca94732d}" to="{6eaaa75a-ae01-4930-b607-05c3a8b0c75c}"/>
<edge from="{2d9609bb-4d5a-46f4-9706-d0c347f5f575}" id="{542438fc-27cf-40a4-8663-4075ee370eb4}" partId="{62fe2f2c-316a-4281-9642-710ce43ee9c5}" to="{88a8c64f-4aff-4dff-a766-5a2af365529e}"/>
<edge from="{9c4cf20c-9cee-4662-8649-d781ecd9411b}" id="{581b137d-1e2e-48af-becf-6e9a61149981}" partId="{62fe2f2c-316a-4281-9642-710ce43ee9c5}" to="{102b730d-9aed-455f-99c0-e8be9cac874e}"/>
<edge from="{023a80e0-43e7-40e9-adb7-4ef5f3fa75aa}" id="{58f0f30e-7407-41c6-b290-83dec0642086}" partId="{9d71e1d8-5520-4917-9c9f-f696577a1cd2}" to="{efb9f9ce-9463-4c47-a12f-c7047a87758a}"/>
<edge from="{b9a9601e-cdfb-456d-bb3f-5f88dbf32500}" id="{5c0d3867-8a25-4116-966f-dc02fcb82520}" partId="{0b421427-908d-4e93-b15c-e16849cf1ba1}" to="{e92cabcb-988a-4ff6-ab07-89f1f01d7588}"/>
<edge from="{056a9f72-5ead-4159-b6a5-c212d83a2677}" id="{618f599d-0433-4243-9060-ddd9b2fce405}" partId="{f01e24d7-29d8-49c1-b37c-11f3e82a2892}" to="{ecb0921f-75e0-43e8-95bc-3ab9de5c3fbd}"/>
<edge from="{1fd89589-6f11-4083-b13b-a901f3361839}" id="{643e203c-adfa-4b15-a144-97f85916bb8b}" partId="{3d579313-129f-465c-a9bf-3c4842ed74dd}" to="{54f09f23-ac6c-4bac-a91c-25e6969a1ce3}"/>
<edge from="{36b74bf8-1471-4510-8fad-137161307c17}" id="{64ccc62e-dbf0-4a62-9837-ac5b365d64c1}" partId="{3cf19fde-79ef-433e-9318-716cb610a893}" to="{73eed031-997c-449f-b3e1-641bf1b8bc66}"/>
<edge from="{73eed031-997c-449f-b3e1-641bf1b8bc66}" id="{6518b186-bf13-4985-8b4e-8a982557477f}" partId="{3cf19fde-79ef-433e-9318-716cb610a893}" to="{7c330ed5-d190-48a4-954c-d4bbcfcaa37e}"/>
<edge from="{9ed89d37-8a50-4c99-af34-5ae3d0eba099}" id="{65382b51-3bb3-488a-9e37-c86039626329}" partId="{94e49ac0-6887-4f3f-aa18-aa600351e205}" to="{173321be-2056-48ec-85b1-08aaa859d343}"/>
<edge from="{7a2eda66-6b4a-4b3f-9fd8-83a5a12dfa91}" id="{6653edc6-de2c-4825-a37d-20f502c30ff6}" partId="{9d71e1d8-5520-4917-9c9f-f696577a1cd2}" to="{cafa9c74-f5bd-47de-bee8-7cd2c831224d}"/>
<edge from="{6eaaa75a-ae01-4930-b607-05c3a8b0c75c}" id="{666eeb9d-5845-4498-b584-d6d9ac1955a0}" partId="{9fb2aeb4-9523-4780-accf-bea5ca94732d}" to="{68430a1a-cd14-4244-8ca4-f4fcc3f6e082}"/>
<edge from="{b4495b2b-be38-42c3-847b-73e2fd5b5870}" id="{6a0a24fe-4a1b-4076-b18b-68a4d5ee2ad9}" partId="{f01e24d7-29d8-49c1-b37c-11f3e82a2892}" to="{f224a6aa-1026-4e50-9bdf-de48cd7d5b29}"/>
<edge from="{aca4bc0a-4fb2-4333-9a89-d59a9c4f4de7}" id="{6cb2a4c2-ba28-4011-ba70-4c843aae1e80}" partId="{9fb2aeb4-9523-4780-accf-bea5ca94732d}" to="{633b195a-814b-4317-a014-9bd3529243c2}"/>
<edge from="{af42e085-2c7b-48e8-b5cd-1cb8cfb631bd}" id="{6e164a7b-63fc-4194-882a-b6d75b72f1a1}" partId="{0b421427-908d-4e93-b15c-e16849cf1ba1}" to="{b9a9601e-cdfb-456d-bb3f-5f88dbf32500}"/>
<edge from="{4a5aef40-d2fa-4204-89e0-0d9f5e70c7c1}" id="{6f3e8258-b03e-46cf-87b4-73203f9f7675}" partId="{a40908d4-a52a-41af-984e-6ae82bae00b3}" to="{cc288173-3e64-437d-a139-02b8ba21a88c}"/>
<edge from="{bd729586-94d1-4284-8582-8d2da360ac5a}" id="{6fbe4df2-6e7f-41f0-b83a-6ed86f374e54}" partId="{f01e24d7-29d8-49c1-b37c-11f3e82a2892}" to="{056a9f72-5ead-4159-b6a5-c212d83a2677}"/>
<edge from="{08f8c71c-a231-4e6a-9e15-661bd73a15fc}" id="{70285289-b87c-4f9c-87ec-b2af148982be}" partId="{62fe2f2c-316a-4281-9642-710ce43ee9c5}" to="{25526437-305a-48e8-b187-584666755165}"/>
<edge from="{3c81b646-fb11-4c7a-903f-e5bf7121b6ab}" id="{72e81a0a-73bd-457d-b6d8-8079d89f4cf6}" partId="{a993e970-3ff6-487a-a4f6-142680bcba44}" to="{e8de381e-f820-4434-b06e-7bc7ed200b80}"/>
<edge from="{68430a1a-cd14-4244-8ca4-f4fcc3f6e082}" id="{73921f4c-fe4a-4256-80d8-e5e2f7dc5f54}" partId="{9fb2aeb4-9523-4780-accf-bea5ca94732d}" to="{7b62e0e8-9e92-4af1-b605-82ee389515b8}"/>
<edge from="{fcfb83be-2cba-42fd-9dcf-55f55caccf04}" id="{747a04f3-c547-4ed4-9d2b-6b2ec02013a8}" partId="{9d71e1d8-5520-4917-9c9f-f696577a1cd2}" to="{b8e9ac77-9b1a-4dc2-8344-917b27c89b87}"/>
<edge from="{133d5bfb-c7df-4dcc-9b21-25e543dac08f}" id="{74d908ed-a5de-41c4-82a0-e6b1eb1d7922}" partId="{e694d1b9-7b59-4983-8b5a-92146b4f9240}" to="{2b6b012d-7aa5-4007-bcdf-b6526580f8c8}"/>
<edge from="{613faf35-b88c-4bc4-9a36-d7d5d0436dbe}" id="{7bb86cce-0998-4f44-8817-d97bd1de06e5}" partId="{0b421427-908d-4e93-b15c-e16849cf1ba1}" to="{821d83ad-f4a0-4127-807c-86256f56c3d6}"/>
<edge from="{5f9dcc9c-032e-42a4-948d-6dc92ee4faf1}" id="{7dd9e3fa-79fd-4241-9cdb-b81052eee9e9}" partId="{a993e970-3ff6-487a-a4f6-142680bcba44}" to="{8d5b108a-83a0-4bbb-8eda-22b1fde7c4a6}"/>
<edge from="{8d5b108a-83a0-4bbb-8eda-22b1fde7c4a6}" id="{7e1be5ae-dc90-47f5-873e-0917245e1244}" partId="{a993e970-3ff6-487a-a4f6-142680bcba44}" to="{3c81b646-fb11-4c7a-903f-e5bf7121b6ab}"/>
<edge from="{11d48e26-1748-42bd-b47b-017dcd512092}" id="{869fc2e4-f6ce-42e8-9afa-28c6a8124031}" partId="{1d5afc0c-8ef3-4e4a-9a56-d2e17062d666}" to="{c2b190c8-35c0-42b3-bab9-0754598ecacb}"/>
<edge from="{e1e7b8a2-752e-4c66-8ca5-a0d4e7850aa1}" id="{896463b2-398e-48e9-bce8-4f983924016b}" partId="{f01e24d7-29d8-49c1-b37c-11f3e82a2892}" to="{bd729586-94d1-4284-8582-8d2da360ac5a}"/>
<edge from="{cb123999-11b6-4591-82c7-2e0716d4fd0d}" id="{8b35ee21-d0e7-4e18-a581-5012886e55f4}" partId="{f01e24d7-29d8-49c1-b37c-11f3e82a2892}" to="{8e49161d-c926-45f9-b0b4-1a897d93e257}"/>
<edge from="{e8de381e-f820-4434-b06e-7bc7ed200b80}" id="{8ff1f2c7-9b5a-4b9e-9ae2-e09d2685d72f}" partId="{a993e970-3ff6-487a-a4f6-142680bcba44}" to="{3f931589-a3ba-4eba-b0c0-c074ecf440c4}"/>
<edge from="{583bcd24-d6d9-4e30-8100-991863b692cb}" id="{9135098f-4485-479a-a8c8-129f5a53c980}" partId="{9d71e1d8-5520-4917-9c9f-f696577a1cd2}" to="{7a2eda66-6b4a-4b3f-9fd8-83a5a12dfa91}"/>
<edge from="{f224a6aa-1026-4e50-9bdf-de48cd7d5b29}" id="{97b54ea0-bce1-4389-a382-b7c5ec071af9}" partId="{f01e24d7-29d8-49c1-b37c-11f3e82a2892}" to="{e1e7b8a2-752e-4c66-8ca5-a0d4e7850aa1}"/>
<edge from="{8ba0dc3d-345b-49f1-991c-f4a8ab562ca9}" id="{9846672d-74ec-492b-8156-339cf4634d69}" partId="{a993e970-3ff6-487a-a4f6-142680bcba44}" to="{5f9dcc9c-032e-42a4-948d-6dc92ee4faf1}"/>
<edge from="{b8ab0228-438a-40d6-89ac-a3d9863a78e0}" id="{9c09ab97-b97d-43cc-8662-830a34b9ac7c}" partId="{f01e24d7-29d8-49c1-b37c-11f3e82a2892}" to="{cb123999-11b6-4591-82c7-2e0716d4fd0d}"/>
<edge from="{633b195a-814b-4317-a014-9bd3529243c2}" id="{9e37ff4c-377c-4461-962b-f1d377049155}" partId="{9fb2aeb4-9523-4780-accf-bea5ca94732d}" to="{db503a99-a319-49f2-b214-b5b2a006dafe}"/>
<edge from="{5e5488ac-949f-460b-b63b-65adbbc7d027}" id="{9e98031b-d221-4cb0-bd0b-2ac0d3dcbd0d}" partId="{0b421427-908d-4e93-b15c-e16849cf1ba1}" to="{b5c68cfd-650e-4805-a4f7-51426864fc14}"/>
<edge from="{bfb5b901-f1f7-4a01-ab6f-9b55a4f3e7a8}" id="{aa00adae-7305-4d07-ad88-c0c5d5f15bc6}" partId="{ca4a8faf-2872-43f8-b126-eb2134631b2b}" to="{2e4de01d-3a82-45de-a21d-dd9ace951611}"/>
<edge from="{1e0c5040-58f2-43c9-8fb4-c18c4e9cb856}" id="{ab03d5dc-f2d3-442f-b0a7-9485f240c242}" partId="{41dd5aed-f2be-4812-bbc2-7c9192b2859d}" to="{1a42548c-9759-433c-88df-0773544fc93e}"/>
<edge from="{3bcbbc2f-d1c9-48f9-8f53-c6c15a65c7d8}" id="{ab64866d-03f4-4612-a10f-4f9efe4fbe51}" partId="{ce2bba62-2f15-4c68-9418-c06540dd1631}" to="{97547a66-19e3-4b7e-bab8-6ea341002a1d}"/>
<edge from="{e6482f6f-ec3e-41e1-b425-1fafab2a5581}" id="{ad0c423f-6a8e-4127-926e-e48fc99aa77d}" partId="{3cf19fde-79ef-433e-9318-716cb610a893}" to="{ab44fe1a-dc80-4fa8-8c33-869f35b23fbb}"/>
<edge from="{1ebb942b-7519-48e7-b821-301572103a9d}" id="{af4fd0c3-d865-46f3-8fc1-e66a86f6645b}" partId="{b2b41478-6984-4196-a44c-eb2015044599}" to="{f5c879ae-8afa-4630-9d0a-76162c364f7f}"/>
<edge from="{a69cf011-bfc6-431f-bcea-88ab4629012e}" id="{b022022a-2240-4609-a88f-c50c219a6128}" partId="{a721a808-a940-40e9-b789-1816a9b1fdb8}" to="{27bbad83-de13-47ad-a2a0-948cccdaadd2}"/>
<edge from="{ebef38af-a58a-46bd-8fc3-01104fe86aad}" id="{b3315daf-1af7-4e48-9db7-0db8efa0be1f}" partId="{9fb2aeb4-9523-4780-accf-bea5ca94732d}" to="{79039736-e239-4cc9-8048-f5209c678d67}"/>
<edge from="{b5c68cfd-650e-4805-a4f7-51426864fc14}" id="{b33668b9-e1eb-4c86-b0a0-fe1e0b264c1b}" partId="{0b421427-908d-4e93-b15c-e16849cf1ba1}" to="{af42e085-2c7b-48e8-b5cd-1cb8cfb631bd}"/>
<edge from="{ca53f4b4-7444-47cb-ad7b-76d9ee7e1457}" id="{b6a6aaa3-a13d-4f4d-ba6a-1f3b2eaf25a3}" partId="{9d71e1d8-5520-4917-9c9f-f696577a1cd2}" to="{023a80e0-43e7-40e9-adb7-4ef5f3fa75aa}"/>
<edge from="{2b6b012d-7aa5-4007-bcdf-b6526580f8c8}" id="{b71b8555-cab1-472a-ba59-9d638c95bb2c}" partId="{e694d1b9-7b59-4983-8b5a-92146b4f9240}" to="{cebb06d0-9e3d-4d00-b671-87faa90606ec}"/>
<edge from="{621a74b3-7853-45d9-8b18-3df5e2f89506}" id="{b7c1a75d-c5ce-4656-ba0f-e90e44195f50}" partId="{0b421427-908d-4e93-b15c-e16849cf1ba1}" to="{5e5488ac-949f-460b-b63b-65adbbc7d027}"/>
<edge from="{9ef98bda-9176-4056-be98-8adbe212f8e0}" id="{ba509689-fece-4d1b-8c18-b68bbc89e5d2}" partId="{5b93898b-08ba-4c7b-ae30-072119dc85a9}" to="{8090775c-1908-4b93-96d8-ff533e152982}"/>
<edge from="{bb35013a-12e7-475e-be03-6717831a2c74}" id="{bb13a5d1-27c4-463a-8200-e856d66d6193}" partId="{8c8fb7ed-f22a-461e-8819-aebb998c0e84}" to="{cba57de1-f600-41bd-82f4-1f725cecca71}"/>
<edge from="{efb9f9ce-9463-4c47-a12f-c7047a87758a}" id="{bb1d95b3-f217-4f5a-891c-0f39759df5d3}" partId="{9d71e1d8-5520-4917-9c9f-f696577a1cd2}" to="{583bcd24-d6d9-4e30-8100-991863b692cb}"/>
<edge from="{c91524bd-f6ba-4ca2-8b32-1c2768840d47}" id="{c020dc6f-a1da-4731-a73b-321b4ddd3f5f}" partId="{a721a808-a940-40e9-b789-1816a9b1fdb8}" to="{c6487a43-16dc-4f97-8b19-40543692464e}"/>
<edge from="{f5c879ae-8afa-4630-9d0a-76162c364f7f}" id="{c49de403-8cfb-49b7-8092-bf1147f8b5eb}" partId="{b2b41478-6984-4196-a44c-eb2015044599}" to="{7b26b07f-49ad-4ab0-9a97-a82a749a78b3}"/>
<edge from="{64b2a8e8-8be5-4041-a467-3ad9a14605db}" id="{c5962140-74dd-445b-8dfa-45731e293873}" partId="{a40908d4-a52a-41af-984e-6ae82bae00b3}" to="{f9f0d428-5bb8-43b3-a02a-34885cc95ad3}"/>
<edge from="{be743b33-6308-4175-becd-9ee0bab5c033}" id="{c8115639-ce28-4502-83f8-ecf080d2f53c}" partId="{a721a808-a940-40e9-b789-1816a9b1fdb8}" to="{976e3bae-7782-4d56-9102-cf0a36f9010f}"/>
<edge from="{c15ffc82-4c53-4374-87ca-086c16adff43}" id="{c94334bf-66bb-46ec-b922-cb20b78b6216}" partId="{9fb2aeb4-9523-4780-accf-bea5ca94732d}" to="{ebef38af-a58a-46bd-8fc3-01104fe86aad}"/>
<edge from="{a4597bbc-4b50-480c-a9f4-556347cef1f6}" id="{ca19f8a4-d494-4b65-86cf-4abf976f8790}" partId="{a40908d4-a52a-41af-984e-6ae82bae00b3}" to="{6a024a4b-a397-4176-b46c-994f922de02a}"/>
<edge from="{066f6612-c3f3-43fd-b76b-ccf438c4071e}" id="{d097360f-dd7f-4fdb-aa63-ce3e9f6899a4}" partId="{3cf19fde-79ef-433e-9318-716cb610a893}" to="{a8226f62-d288-47ac-bbcd-3cdff15337e6}"/>
<edge from="{1d1270f2-522f-45b7-a5cd-1de2087f8394}" id="{d41b2a71-fea1-4a9f-919c-2ccb3d090f7e}" partId="{f01e24d7-29d8-49c1-b37c-11f3e82a2892}" to="{b4495b2b-be38-42c3-847b-73e2fd5b5870}"/>
<edge from="{f9f0d428-5bb8-43b3-a02a-34885cc95ad3}" id="{d4ddaf9a-50fc-471f-be2b-feb535755372}" partId="{a40908d4-a52a-41af-984e-6ae82bae00b3}" to="{bb11999a-5e23-466c-ac7d-9c032eda1d83}"/>
<edge from="{9892449b-dea5-4ea9-8505-6ec248e21602}" id="{d9bfc3ca-b2aa-45c8-a72e-27d94ab1c886}" partId="{cf781dce-ed78-4c86-997d-eeeaea1a4825}" to="{ecd6799e-e553-4ec8-9c5d-c6d9720f839e}"/>
<edge from="{bb11999a-5e23-466c-ac7d-9c032eda1d83}" id="{dd814577-510a-4b8b-a2a5-5e7ae3c1031b}" partId="{a40908d4-a52a-41af-984e-6ae82bae00b3}" to="{2c495afe-b535-46cd-9320-f191921982a3}"/>
<edge from="{3db449a2-938e-49a6-8d71-d3b6179db330}" id="{e14eb1ad-72e9-47da-9169-fff0899b379c}" partId="{fda51f9f-2214-4b87-9a23-1b877b18720e}" to="{bbce9a5f-6b44-405f-9483-8e7cfe2a3087}"/>
<edge from="{6364d658-7500-4f23-be78-de07110f0cad}" id="{e99e35df-dc5a-438e-9ade-7b32d599b69c}" partId="{3cf19fde-79ef-433e-9318-716cb610a893}" to="{36b74bf8-1471-4510-8fad-137161307c17}"/>
<edge from="{cc288173-3e64-437d-a139-02b8ba21a88c}" id="{ea2e43cb-fe87-4343-9241-d2cbc6e6a0c4}" partId="{a40908d4-a52a-41af-984e-6ae82bae00b3}" to="{175e6a16-6b47-4778-ab4f-44663443f223}"/>
<edge from="{8e49161d-c926-45f9-b0b4-1a897d93e257}" id="{f2179d52-1ca8-4b7a-9eb4-c739f9a7d6b7}" partId="{f01e24d7-29d8-49c1-b37c-11f3e82a2892}" to="{1d1270f2-522f-45b7-a5cd-1de2087f8394}"/>
<edge from="{f5cc172a-f273-437a-a325-c3d94ae53018}" id="{f96c7d61-2268-4e25-a010-86aaed57a6a1}" partId="{9d71e1d8-5520-4917-9c9f-f696577a1cd2}" to="{bbf72811-7f32-4039-9921-7e4c6cdf367b}"/>
<edge from="{7c330ed5-d190-48a4-954c-d4bbcfcaa37e}" id="{f9ad040b-f53c-4eea-b593-4b6183a92587}" partId="{3cf19fde-79ef-433e-9318-716cb610a893}" to="{0ae3ec9b-8846-4555-991b-fbafd9a4154f}"/>
<edge from="{c1d08fe7-a509-4466-956b-e384d9aa088d}" id="{fcca27a8-1206-46c4-8206-14d368beea35}" partId="{0e9aeab7-d6b6-4235-acb8-40ea97945c84}" to="{96b3deac-17ca-4677-b872-42508caa6a49}"/>
<edge from="{6a024a4b-a397-4176-b46c-994f922de02a}" id="{ff8b7a13-7299-4faa-8906-8092e6178b45}" partId="{a40908d4-a52a-41af-984e-6ae82bae00b3}" to="{64b2a8e8-8be5-4041-a467-3ad9a14605db}"/>
<edge from="{2c3dc485-7343-47b6-8c03-954ed6ab5de8}" id="{03c28f36-b688-4baf-a7b1-5117cf679bbb}" partId="{8676598d-d624-4d86-b498-c76f8a1aa810}" to="{4625811e-114f-4af6-b47f-acbe1aee124f}"/>
<edge from="{bbf72811-7f32-4039-9921-7e4c6cdf367b}" id="{0c2cc283-75eb-456d-9937-1633bb89ba47}" partId="{c4df1b3d-8a8c-43bd-abea-3d0df226dca4}" to="{98c15e13-f685-47c7-b540-e3853a0c05ec}"/>
<edge from="{e92cabcb-988a-4ff6-ab07-89f1f01d7588}" id="{0e3125c1-bc2e-4ed1-a4a0-34d57fbb28cb}" partId="{cada7dd1-4087-4257-998f-7151930b31d4}" to="{613faf35-b88c-4bc4-9a36-d7d5d0436dbe}"/>
<edge from="{db503a99-a319-49f2-b214-b5b2a006dafe}" id="{12044065-1f30-45cf-bc80-b227c525eb14}" partId="{fd44561f-05bc-48a4-9a48-45b730ae92a3}" to="{c15ffc82-4c53-4374-87ca-086c16adff43}"/>
<edge from="{88a8c64f-4aff-4dff-a766-5a2af365529e}" id="{139f04fc-00d3-44ea-9fc2-6b6c5b7053bc}" partId="{af64c2d3-a4ff-4c82-894b-f22e2f7ce2e7}" to="{96930462-7869-475a-b4cc-cf9156833908}"/>
<edge from="{be743b33-6308-4175-becd-9ee0bab5c033}" id="{14d85597-1b5a-4982-862e-3a89c4d80209}" partId="{a77512d7-fe5f-465a-aa97-33e0ca928b51}" to="{c91524bd-f6ba-4ca2-8b32-1c2768840d47}"/>
<edge from="{d0c9355c-7532-428d-91af-b0aacda51b06}" id="{157c55db-457c-422b-aef7-ceaaf49ad2c2}" partId="{3ed6f8ae-f85b-4bd2-b393-523c3c3e2d0b}" to="{47095026-e7d1-4100-bd48-afdaa8b3e033}"/>
<edge from="{98c15e13-f685-47c7-b540-e3853a0c05ec}" id="{1888813a-8c1a-4055-9234-62980b1d19b5}" partId="{c4df1b3d-8a8c-43bd-abea-3d0df226dca4}" to="{ca53f4b4-7444-47cb-ad7b-76d9ee7e1457}"/>
<edge from="{25526437-305a-48e8-b187-584666755165}" id="{1cc2cfbd-a7aa-4ab2-90db-b17df6ff95e7}" partId="{af64c2d3-a4ff-4c82-894b-f22e2f7ce2e7}" to="{9c4cf20c-9cee-4662-8649-d781ecd9411b}"/>
<edge from="{a8226f62-d288-47ac-bbcd-3cdff15337e6}" id="{2393f058-06a8-4845-8f4b-721aa0f2a426}" partId="{80cfca5d-e04a-4e08-94d9-f4225387107b}" to="{e6482f6f-ec3e-41e1-b425-1fafab2a5581}"/>
<edge from="{c6487a43-16dc-4f97-8b19-40543692464e}" id="{2483380d-a704-425e-8603-c4d314b38381}" partId="{a77512d7-fe5f-465a-aa97-33e0ca928b51}" to="{a69cf011-bfc6-431f-bcea-88ab4629012e}"/>
<edge from="{f14a06cb-83f0-456f-9a4f-1f32883da263}" id="{258a1121-19ff-432e-98b1-d359c2dd53fd}" partId="{c4df1b3d-8a8c-43bd-abea-3d0df226dca4}" to="{d14bfe92-9324-41a4-a113-766cfb10b5ca}"/>
<edge from="{a11f70a2-9eb4-44d1-8fc6-05de70d4f7a3}" id="{261764c2-6bac-499c-9039-bad06c2d195e}" partId="{61baa97d-912a-46da-ab8d-b3940204a8cf}" to="{8d16f910-b1f0-46ab-9611-d81642aabcce}"/>
<edge from="{96930462-7869-475a-b4cc-cf9156833908}" id="{2b414be6-af5d-45f6-bee7-c227d9b51f7a}" partId="{af64c2d3-a4ff-4c82-894b-f22e2f7ce2e7}" to="{19df1fa9-30bc-46fe-9718-6e4b2fdfcea0}"/>
<edge from="{b8e9ac77-9b1a-4dc2-8344-917b27c89b87}" id="{34324e9f-cb10-48d0-a355-4d56929af96d}" partId="{c4df1b3d-8a8c-43bd-abea-3d0df226dca4}" to="{f5cc172a-f273-437a-a325-c3d94ae53018}"/>
<edge from="{19df1fa9-30bc-46fe-9718-6e4b2fdfcea0}" id="{355ef96c-3a7b-4301-ae5f-917aaa632ca9}" partId="{af64c2d3-a4ff-4c82-894b-f22e2f7ce2e7}" to="{08f8c71c-a231-4e6a-9e15-661bd73a15fc}"/>
<edge from="{0ae3ec9b-8846-4555-991b-fbafd9a4154f}" id="{3adecb1b-c4bc-48f4-8df4-20be03b896c9}" partId="{80cfca5d-e04a-4e08-94d9-f4225387107b}" to="{066f6612-c3f3-43fd-b76b-ccf438c4071e}"/>
<edge from="{2c495afe-b535-46cd-9320-f191921982a3}" id="{3da3e6b1-bd3a-40d7-b818-ed0d3498adc1}" partId="{dc4f431d-fa39-4577-ae64-de12cd1a7a33}" to="{4a5aef40-d2fa-4204-89e0-0d9f5e70c7c1}"/>
<edge from="{d222223b-e828-4ed9-a100-3ac87684a4ea}" id="{4209a6de-9b44-4292-b01f-5a16668de3da}" partId="{cada7dd1-4087-4257-998f-7151930b31d4}" to="{621a74b3-7853-45d9-8b18-3df5e2f89506}"/>
<edge from="{cafa9c74-f5bd-47de-bee8-7cd2c831224d}" id="{442c9e62-4793-4102-acbc-60c5415ac092}" partId="{c4df1b3d-8a8c-43bd-abea-3d0df226dca4}" to="{f14a06cb-83f0-456f-9a4f-1f32883da263}"/>
<edge from="{4625811e-114f-4af6-b47f-acbe1aee124f}" id="{453d7c5e-c966-444f-ba52-5f8aafd865f0}" partId="{8676598d-d624-4d86-b498-c76f8a1aa810}" to="{7e029738-dc7c-4b74-bb67-17d5b9a1daf1}"/>
<edge from="{cdccbbfe-13bb-495d-a5a8-fc817fb80656}" id="{45c45716-63b7-4fa4-9380-4255f3f61d57}" partId="{accf6745-05a1-44e4-ab9d-9e4e5b61a0c6}" to="{e8f04abe-6e33-44c1-b860-8e0dd4d505c3}"/>
<edge from="{ecb0921f-75e0-43e8-95bc-3ab9de5c3fbd}" id="{4b575c3e-1c67-4d90-b865-86ac49a3f0cd}" partId="{09d488db-863d-4868-8890-ed2f8cd941df}" to="{b8ab0228-438a-40d6-89ac-a3d9863a78e0}"/>
<edge from="{79039736-e239-4cc9-8048-f5209c678d67}" id="{4c60e711-ddab-4d29-9e46-74912b48a56d}" partId="{fd44561f-05bc-48a4-9a48-45b730ae92a3}" to="{6eaaa75a-ae01-4930-b607-05c3a8b0c75c}"/>
<edge from="{2d9609bb-4d5a-46f4-9706-d0c347f5f575}" id="{542438fc-27cf-40a4-8663-4075ee370eb4}" partId="{af64c2d3-a4ff-4c82-894b-f22e2f7ce2e7}" to="{88a8c64f-4aff-4dff-a766-5a2af365529e}"/>
<edge from="{9c4cf20c-9cee-4662-8649-d781ecd9411b}" id="{581b137d-1e2e-48af-becf-6e9a61149981}" partId="{af64c2d3-a4ff-4c82-894b-f22e2f7ce2e7}" to="{102b730d-9aed-455f-99c0-e8be9cac874e}"/>
<edge from="{023a80e0-43e7-40e9-adb7-4ef5f3fa75aa}" id="{58f0f30e-7407-41c6-b290-83dec0642086}" partId="{c4df1b3d-8a8c-43bd-abea-3d0df226dca4}" to="{efb9f9ce-9463-4c47-a12f-c7047a87758a}"/>
<edge from="{b9a9601e-cdfb-456d-bb3f-5f88dbf32500}" id="{5c0d3867-8a25-4116-966f-dc02fcb82520}" partId="{cada7dd1-4087-4257-998f-7151930b31d4}" to="{e92cabcb-988a-4ff6-ab07-89f1f01d7588}"/>
<edge from="{056a9f72-5ead-4159-b6a5-c212d83a2677}" id="{618f599d-0433-4243-9060-ddd9b2fce405}" partId="{09d488db-863d-4868-8890-ed2f8cd941df}" to="{ecb0921f-75e0-43e8-95bc-3ab9de5c3fbd}"/>
<edge from="{1fd89589-6f11-4083-b13b-a901f3361839}" id="{643e203c-adfa-4b15-a144-97f85916bb8b}" partId="{5b83b16a-09e1-4d56-acbd-7b6a612d150a}" to="{54f09f23-ac6c-4bac-a91c-25e6969a1ce3}"/>
<edge from="{36b74bf8-1471-4510-8fad-137161307c17}" id="{64ccc62e-dbf0-4a62-9837-ac5b365d64c1}" partId="{80cfca5d-e04a-4e08-94d9-f4225387107b}" to="{73eed031-997c-449f-b3e1-641bf1b8bc66}"/>
<edge from="{73eed031-997c-449f-b3e1-641bf1b8bc66}" id="{6518b186-bf13-4985-8b4e-8a982557477f}" partId="{80cfca5d-e04a-4e08-94d9-f4225387107b}" to="{7c330ed5-d190-48a4-954c-d4bbcfcaa37e}"/>
<edge from="{9ed89d37-8a50-4c99-af34-5ae3d0eba099}" id="{65382b51-3bb3-488a-9e37-c86039626329}" partId="{99aa4033-e6b7-456f-abef-b021acf490f6}" to="{173321be-2056-48ec-85b1-08aaa859d343}"/>
<edge from="{7a2eda66-6b4a-4b3f-9fd8-83a5a12dfa91}" id="{6653edc6-de2c-4825-a37d-20f502c30ff6}" partId="{c4df1b3d-8a8c-43bd-abea-3d0df226dca4}" to="{cafa9c74-f5bd-47de-bee8-7cd2c831224d}"/>
<edge from="{6eaaa75a-ae01-4930-b607-05c3a8b0c75c}" id="{666eeb9d-5845-4498-b584-d6d9ac1955a0}" partId="{fd44561f-05bc-48a4-9a48-45b730ae92a3}" to="{68430a1a-cd14-4244-8ca4-f4fcc3f6e082}"/>
<edge from="{b4495b2b-be38-42c3-847b-73e2fd5b5870}" id="{6a0a24fe-4a1b-4076-b18b-68a4d5ee2ad9}" partId="{09d488db-863d-4868-8890-ed2f8cd941df}" to="{f224a6aa-1026-4e50-9bdf-de48cd7d5b29}"/>
<edge from="{aca4bc0a-4fb2-4333-9a89-d59a9c4f4de7}" id="{6cb2a4c2-ba28-4011-ba70-4c843aae1e80}" partId="{fd44561f-05bc-48a4-9a48-45b730ae92a3}" to="{633b195a-814b-4317-a014-9bd3529243c2}"/>
<edge from="{af42e085-2c7b-48e8-b5cd-1cb8cfb631bd}" id="{6e164a7b-63fc-4194-882a-b6d75b72f1a1}" partId="{cada7dd1-4087-4257-998f-7151930b31d4}" to="{b9a9601e-cdfb-456d-bb3f-5f88dbf32500}"/>
<edge from="{4a5aef40-d2fa-4204-89e0-0d9f5e70c7c1}" id="{6f3e8258-b03e-46cf-87b4-73203f9f7675}" partId="{dc4f431d-fa39-4577-ae64-de12cd1a7a33}" to="{cc288173-3e64-437d-a139-02b8ba21a88c}"/>
<edge from="{bd729586-94d1-4284-8582-8d2da360ac5a}" id="{6fbe4df2-6e7f-41f0-b83a-6ed86f374e54}" partId="{09d488db-863d-4868-8890-ed2f8cd941df}" to="{056a9f72-5ead-4159-b6a5-c212d83a2677}"/>
<edge from="{08f8c71c-a231-4e6a-9e15-661bd73a15fc}" id="{70285289-b87c-4f9c-87ec-b2af148982be}" partId="{af64c2d3-a4ff-4c82-894b-f22e2f7ce2e7}" to="{25526437-305a-48e8-b187-584666755165}"/>
<edge from="{3c81b646-fb11-4c7a-903f-e5bf7121b6ab}" id="{72e81a0a-73bd-457d-b6d8-8079d89f4cf6}" partId="{f8df3586-c5cc-497c-b394-e84e7873dc30}" to="{e8de381e-f820-4434-b06e-7bc7ed200b80}"/>
<edge from="{68430a1a-cd14-4244-8ca4-f4fcc3f6e082}" id="{73921f4c-fe4a-4256-80d8-e5e2f7dc5f54}" partId="{fd44561f-05bc-48a4-9a48-45b730ae92a3}" to="{7b62e0e8-9e92-4af1-b605-82ee389515b8}"/>
<edge from="{fcfb83be-2cba-42fd-9dcf-55f55caccf04}" id="{747a04f3-c547-4ed4-9d2b-6b2ec02013a8}" partId="{c4df1b3d-8a8c-43bd-abea-3d0df226dca4}" to="{b8e9ac77-9b1a-4dc2-8344-917b27c89b87}"/>
<edge from="{133d5bfb-c7df-4dcc-9b21-25e543dac08f}" id="{74d908ed-a5de-41c4-82a0-e6b1eb1d7922}" partId="{b8a1ab8c-a094-416d-9d13-00aca25e3b3e}" to="{2b6b012d-7aa5-4007-bcdf-b6526580f8c8}"/>
<edge from="{613faf35-b88c-4bc4-9a36-d7d5d0436dbe}" id="{7bb86cce-0998-4f44-8817-d97bd1de06e5}" partId="{cada7dd1-4087-4257-998f-7151930b31d4}" to="{821d83ad-f4a0-4127-807c-86256f56c3d6}"/>
<edge from="{5f9dcc9c-032e-42a4-948d-6dc92ee4faf1}" id="{7dd9e3fa-79fd-4241-9cdb-b81052eee9e9}" partId="{f8df3586-c5cc-497c-b394-e84e7873dc30}" to="{8d5b108a-83a0-4bbb-8eda-22b1fde7c4a6}"/>
<edge from="{8d5b108a-83a0-4bbb-8eda-22b1fde7c4a6}" id="{7e1be5ae-dc90-47f5-873e-0917245e1244}" partId="{f8df3586-c5cc-497c-b394-e84e7873dc30}" to="{3c81b646-fb11-4c7a-903f-e5bf7121b6ab}"/>
<edge from="{11d48e26-1748-42bd-b47b-017dcd512092}" id="{869fc2e4-f6ce-42e8-9afa-28c6a8124031}" partId="{6b686ee6-2eeb-4d81-a207-d2dea8fe764b}" to="{c2b190c8-35c0-42b3-bab9-0754598ecacb}"/>
<edge from="{e1e7b8a2-752e-4c66-8ca5-a0d4e7850aa1}" id="{896463b2-398e-48e9-bce8-4f983924016b}" partId="{09d488db-863d-4868-8890-ed2f8cd941df}" to="{bd729586-94d1-4284-8582-8d2da360ac5a}"/>
<edge from="{cb123999-11b6-4591-82c7-2e0716d4fd0d}" id="{8b35ee21-d0e7-4e18-a581-5012886e55f4}" partId="{09d488db-863d-4868-8890-ed2f8cd941df}" to="{8e49161d-c926-45f9-b0b4-1a897d93e257}"/>
<edge from="{e8de381e-f820-4434-b06e-7bc7ed200b80}" id="{8ff1f2c7-9b5a-4b9e-9ae2-e09d2685d72f}" partId="{f8df3586-c5cc-497c-b394-e84e7873dc30}" to="{3f931589-a3ba-4eba-b0c0-c074ecf440c4}"/>
<edge from="{583bcd24-d6d9-4e30-8100-991863b692cb}" id="{9135098f-4485-479a-a8c8-129f5a53c980}" partId="{c4df1b3d-8a8c-43bd-abea-3d0df226dca4}" to="{7a2eda66-6b4a-4b3f-9fd8-83a5a12dfa91}"/>
<edge from="{f224a6aa-1026-4e50-9bdf-de48cd7d5b29}" id="{97b54ea0-bce1-4389-a382-b7c5ec071af9}" partId="{09d488db-863d-4868-8890-ed2f8cd941df}" to="{e1e7b8a2-752e-4c66-8ca5-a0d4e7850aa1}"/>
<edge from="{8ba0dc3d-345b-49f1-991c-f4a8ab562ca9}" id="{9846672d-74ec-492b-8156-339cf4634d69}" partId="{f8df3586-c5cc-497c-b394-e84e7873dc30}" to="{5f9dcc9c-032e-42a4-948d-6dc92ee4faf1}"/>
<edge from="{b8ab0228-438a-40d6-89ac-a3d9863a78e0}" id="{9c09ab97-b97d-43cc-8662-830a34b9ac7c}" partId="{09d488db-863d-4868-8890-ed2f8cd941df}" to="{cb123999-11b6-4591-82c7-2e0716d4fd0d}"/>
<edge from="{633b195a-814b-4317-a014-9bd3529243c2}" id="{9e37ff4c-377c-4461-962b-f1d377049155}" partId="{fd44561f-05bc-48a4-9a48-45b730ae92a3}" to="{db503a99-a319-49f2-b214-b5b2a006dafe}"/>
<edge from="{5e5488ac-949f-460b-b63b-65adbbc7d027}" id="{9e98031b-d221-4cb0-bd0b-2ac0d3dcbd0d}" partId="{cada7dd1-4087-4257-998f-7151930b31d4}" to="{b5c68cfd-650e-4805-a4f7-51426864fc14}"/>
<edge from="{bfb5b901-f1f7-4a01-ab6f-9b55a4f3e7a8}" id="{aa00adae-7305-4d07-ad88-c0c5d5f15bc6}" partId="{1c66ae95-c9c5-474d-b3db-ae2bccfd2f13}" to="{2e4de01d-3a82-45de-a21d-dd9ace951611}"/>
<edge from="{1e0c5040-58f2-43c9-8fb4-c18c4e9cb856}" id="{ab03d5dc-f2d3-442f-b0a7-9485f240c242}" partId="{ab27a4f2-6c05-46f4-b81a-c2b0453a9bb3}" to="{1a42548c-9759-433c-88df-0773544fc93e}"/>
<edge from="{3bcbbc2f-d1c9-48f9-8f53-c6c15a65c7d8}" id="{ab64866d-03f4-4612-a10f-4f9efe4fbe51}" partId="{d27d045a-44e9-44fd-9d76-13eddc43aae1}" to="{97547a66-19e3-4b7e-bab8-6ea341002a1d}"/>
<edge from="{e6482f6f-ec3e-41e1-b425-1fafab2a5581}" id="{ad0c423f-6a8e-4127-926e-e48fc99aa77d}" partId="{80cfca5d-e04a-4e08-94d9-f4225387107b}" to="{ab44fe1a-dc80-4fa8-8c33-869f35b23fbb}"/>
<edge from="{1ebb942b-7519-48e7-b821-301572103a9d}" id="{af4fd0c3-d865-46f3-8fc1-e66a86f6645b}" partId="{054515ed-3093-40f5-87d3-214e15044bba}" to="{f5c879ae-8afa-4630-9d0a-76162c364f7f}"/>
<edge from="{a69cf011-bfc6-431f-bcea-88ab4629012e}" id="{b022022a-2240-4609-a88f-c50c219a6128}" partId="{a77512d7-fe5f-465a-aa97-33e0ca928b51}" to="{27bbad83-de13-47ad-a2a0-948cccdaadd2}"/>
<edge from="{ebef38af-a58a-46bd-8fc3-01104fe86aad}" id="{b3315daf-1af7-4e48-9db7-0db8efa0be1f}" partId="{fd44561f-05bc-48a4-9a48-45b730ae92a3}" to="{79039736-e239-4cc9-8048-f5209c678d67}"/>
<edge from="{b5c68cfd-650e-4805-a4f7-51426864fc14}" id="{b33668b9-e1eb-4c86-b0a0-fe1e0b264c1b}" partId="{cada7dd1-4087-4257-998f-7151930b31d4}" to="{af42e085-2c7b-48e8-b5cd-1cb8cfb631bd}"/>
<edge from="{ca53f4b4-7444-47cb-ad7b-76d9ee7e1457}" id="{b6a6aaa3-a13d-4f4d-ba6a-1f3b2eaf25a3}" partId="{c4df1b3d-8a8c-43bd-abea-3d0df226dca4}" to="{023a80e0-43e7-40e9-adb7-4ef5f3fa75aa}"/>
<edge from="{2b6b012d-7aa5-4007-bcdf-b6526580f8c8}" id="{b71b8555-cab1-472a-ba59-9d638c95bb2c}" partId="{b8a1ab8c-a094-416d-9d13-00aca25e3b3e}" to="{cebb06d0-9e3d-4d00-b671-87faa90606ec}"/>
<edge from="{621a74b3-7853-45d9-8b18-3df5e2f89506}" id="{b7c1a75d-c5ce-4656-ba0f-e90e44195f50}" partId="{cada7dd1-4087-4257-998f-7151930b31d4}" to="{5e5488ac-949f-460b-b63b-65adbbc7d027}"/>
<edge from="{9ef98bda-9176-4056-be98-8adbe212f8e0}" id="{ba509689-fece-4d1b-8c18-b68bbc89e5d2}" partId="{cb40704a-692b-4078-9eb8-74a281aea1c8}" to="{8090775c-1908-4b93-96d8-ff533e152982}"/>
<edge from="{bb35013a-12e7-475e-be03-6717831a2c74}" id="{bb13a5d1-27c4-463a-8200-e856d66d6193}" partId="{0e885ccb-6cd3-410c-bfdb-1b0cad02c80e}" to="{cba57de1-f600-41bd-82f4-1f725cecca71}"/>
<edge from="{efb9f9ce-9463-4c47-a12f-c7047a87758a}" id="{bb1d95b3-f217-4f5a-891c-0f39759df5d3}" partId="{c4df1b3d-8a8c-43bd-abea-3d0df226dca4}" to="{583bcd24-d6d9-4e30-8100-991863b692cb}"/>
<edge from="{c91524bd-f6ba-4ca2-8b32-1c2768840d47}" id="{c020dc6f-a1da-4731-a73b-321b4ddd3f5f}" partId="{a77512d7-fe5f-465a-aa97-33e0ca928b51}" to="{c6487a43-16dc-4f97-8b19-40543692464e}"/>
<edge from="{f5c879ae-8afa-4630-9d0a-76162c364f7f}" id="{c49de403-8cfb-49b7-8092-bf1147f8b5eb}" partId="{054515ed-3093-40f5-87d3-214e15044bba}" to="{7b26b07f-49ad-4ab0-9a97-a82a749a78b3}"/>
<edge from="{64b2a8e8-8be5-4041-a467-3ad9a14605db}" id="{c5962140-74dd-445b-8dfa-45731e293873}" partId="{dc4f431d-fa39-4577-ae64-de12cd1a7a33}" to="{f9f0d428-5bb8-43b3-a02a-34885cc95ad3}"/>
<edge from="{be743b33-6308-4175-becd-9ee0bab5c033}" id="{c8115639-ce28-4502-83f8-ecf080d2f53c}" partId="{a77512d7-fe5f-465a-aa97-33e0ca928b51}" to="{976e3bae-7782-4d56-9102-cf0a36f9010f}"/>
<edge from="{c15ffc82-4c53-4374-87ca-086c16adff43}" id="{c94334bf-66bb-46ec-b922-cb20b78b6216}" partId="{fd44561f-05bc-48a4-9a48-45b730ae92a3}" to="{ebef38af-a58a-46bd-8fc3-01104fe86aad}"/>
<edge from="{a4597bbc-4b50-480c-a9f4-556347cef1f6}" id="{ca19f8a4-d494-4b65-86cf-4abf976f8790}" partId="{dc4f431d-fa39-4577-ae64-de12cd1a7a33}" to="{6a024a4b-a397-4176-b46c-994f922de02a}"/>
<edge from="{066f6612-c3f3-43fd-b76b-ccf438c4071e}" id="{d097360f-dd7f-4fdb-aa63-ce3e9f6899a4}" partId="{80cfca5d-e04a-4e08-94d9-f4225387107b}" to="{a8226f62-d288-47ac-bbcd-3cdff15337e6}"/>
<edge from="{1d1270f2-522f-45b7-a5cd-1de2087f8394}" id="{d41b2a71-fea1-4a9f-919c-2ccb3d090f7e}" partId="{09d488db-863d-4868-8890-ed2f8cd941df}" to="{b4495b2b-be38-42c3-847b-73e2fd5b5870}"/>
<edge from="{f9f0d428-5bb8-43b3-a02a-34885cc95ad3}" id="{d4ddaf9a-50fc-471f-be2b-feb535755372}" partId="{dc4f431d-fa39-4577-ae64-de12cd1a7a33}" to="{bb11999a-5e23-466c-ac7d-9c032eda1d83}"/>
<edge from="{9892449b-dea5-4ea9-8505-6ec248e21602}" id="{d9bfc3ca-b2aa-45c8-a72e-27d94ab1c886}" partId="{b7773748-17ff-4bcf-93ae-2fe6ab5a0fc6}" to="{ecd6799e-e553-4ec8-9c5d-c6d9720f839e}"/>
<edge from="{bb11999a-5e23-466c-ac7d-9c032eda1d83}" id="{dd814577-510a-4b8b-a2a5-5e7ae3c1031b}" partId="{dc4f431d-fa39-4577-ae64-de12cd1a7a33}" to="{2c495afe-b535-46cd-9320-f191921982a3}"/>
<edge from="{3db449a2-938e-49a6-8d71-d3b6179db330}" id="{e14eb1ad-72e9-47da-9169-fff0899b379c}" partId="{a09f07af-91ad-412f-ae6b-1f06ff7808d3}" to="{bbce9a5f-6b44-405f-9483-8e7cfe2a3087}"/>
<edge from="{6364d658-7500-4f23-be78-de07110f0cad}" id="{e99e35df-dc5a-438e-9ade-7b32d599b69c}" partId="{80cfca5d-e04a-4e08-94d9-f4225387107b}" to="{36b74bf8-1471-4510-8fad-137161307c17}"/>
<edge from="{cc288173-3e64-437d-a139-02b8ba21a88c}" id="{ea2e43cb-fe87-4343-9241-d2cbc6e6a0c4}" partId="{dc4f431d-fa39-4577-ae64-de12cd1a7a33}" to="{175e6a16-6b47-4778-ab4f-44663443f223}"/>
<edge from="{8e49161d-c926-45f9-b0b4-1a897d93e257}" id="{f2179d52-1ca8-4b7a-9eb4-c739f9a7d6b7}" partId="{09d488db-863d-4868-8890-ed2f8cd941df}" to="{1d1270f2-522f-45b7-a5cd-1de2087f8394}"/>
<edge from="{f5cc172a-f273-437a-a325-c3d94ae53018}" id="{f96c7d61-2268-4e25-a010-86aaed57a6a1}" partId="{c4df1b3d-8a8c-43bd-abea-3d0df226dca4}" to="{bbf72811-7f32-4039-9921-7e4c6cdf367b}"/>
<edge from="{7c330ed5-d190-48a4-954c-d4bbcfcaa37e}" id="{f9ad040b-f53c-4eea-b593-4b6183a92587}" partId="{80cfca5d-e04a-4e08-94d9-f4225387107b}" to="{0ae3ec9b-8846-4555-991b-fbafd9a4154f}"/>
<edge from="{c1d08fe7-a509-4466-956b-e384d9aa088d}" id="{fcca27a8-1206-46c4-8206-14d368beea35}" partId="{48d5870b-b94b-4b8f-9347-f397293a610e}" to="{96b3deac-17ca-4677-b872-42508caa6a49}"/>
<edge from="{6a024a4b-a397-4176-b46c-994f922de02a}" id="{ff8b7a13-7299-4faa-8906-8092e6178b45}" partId="{dc4f431d-fa39-4577-ae64-de12cd1a7a33}" to="{64b2a8e8-8be5-4041-a467-3ad9a14605db}"/>
</edges>
<parts>
<part chamfered="false" color="#fffeca90" deformThickness="0.88" disabled="false" id="{0b421427-908d-4e93-b15c-e16849cf1ba1}" locked="false" rounded="true" subdived="true" visible="true" xMirrored="false"/>
<part chamfered="false" colorSolubility="0.08" cutFace="Pentagon" disabled="false" id="{0e9aeab7-d6b6-4235-acb8-40ea97945c84}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" colorSolubility="0.03" cutFace="Pentagon" disabled="false" id="{1d5afc0c-8ef3-4e4a-9a56-d2e17062d666}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" color="#fffeca90" deformThickness="0.59" disabled="false" id="{3224e51d-8591-424e-ad66-7172b5c41b95}" locked="false" rounded="false" subdived="false" visible="true" xMirrored="true"/>
<part chamfered="false" color="#fffeca90" deformThickness="0.88" disabled="false" id="{3cf19fde-79ef-433e-9318-716cb610a893}" locked="false" rounded="true" subdived="true" visible="true" xMirrored="false"/>
<part chamfered="false" color="#ff614831" disabled="false" id="{3d579313-129f-465c-a9bf-3c4842ed74dd}" locked="false" metalness="1" roughness="0" rounded="false" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" colorSolubility="0.06" cutFace="Pentagon" disabled="false" id="{41dd5aed-f2be-4812-bbc2-7c9192b2859d}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" color="#ff000000" disabled="false" id="{4c3c4a67-e536-4d3b-80ac-7f8e1c490c63}" locked="false" rounded="false" subdived="true" visible="true" xMirrored="true"/>
<part chamfered="false" colorSolubility="0.03" cutFace="Pentagon" disabled="false" id="{5b93898b-08ba-4c7b-ae30-072119dc85a9}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" color="#fffeca90" deformThickness="0.88" disabled="false" id="{62fe2f2c-316a-4281-9642-710ce43ee9c5}" locked="false" rounded="true" subdived="true" visible="true" xMirrored="false"/>
<part chamfered="false" colorSolubility="0.06" cutFace="Pentagon" disabled="false" id="{75a5ca65-abc5-492c-917c-70fe2460afaa}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" colorSolubility="0.06" cutFace="Pentagon" disabled="false" id="{7d3bfd96-a801-4d2d-9cc7-695946484697}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" color="#ffcc3d4f" cutFace="{a721a808-a940-40e9-b789-1816a9b1fdb8}" cutRotation="-0.45" disabled="false" id="{8c8fb7ed-f22a-461e-8819-aebb998c0e84}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" colorSolubility="0.03" cutFace="Pentagon" disabled="false" id="{94e49ac0-6887-4f3f-aa18-aa600351e205}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" color="#fffeca90" cutFace="Pentagon" cutRotation="-0.26" disabled="false" id="{9d71e1d8-5520-4917-9c9f-f696577a1cd2}" locked="false" rounded="false" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" color="#fffeca90" deformThickness="0.88" disabled="false" id="{9fb2aeb4-9523-4780-accf-bea5ca94732d}" locked="false" rounded="true" subdived="true" visible="true" xMirrored="false"/>
<part chamfered="false" color="#fffeca90" deformThickness="0.88" disabled="false" id="{a40908d4-a52a-41af-984e-6ae82bae00b3}" locked="false" rounded="true" subdived="true" visible="true" xMirrored="false"/>
<part chamfered="false" disabled="false" id="{a721a808-a940-40e9-b789-1816a9b1fdb8}" locked="false" rounded="false" subdived="false" target="CutFace" visible="true" xMirrored="false"/>
<part chamfered="false" color="#fffeca90" disabled="false" id="{a993e970-3ff6-487a-a4f6-142680bcba44}" locked="false" rounded="true" subdived="true" visible="true" xMirrored="false"/>
<part chamfered="false" color="#ff281a17" colorSolubility="0.04" cutFace="Pentagon" disabled="false" id="{b2b41478-6984-4196-a44c-eb2015044599}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" colorSolubility="0.08" cutFace="Pentagon" disabled="false" id="{be83e76b-6e07-4719-a513-f612609b57e3}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" colorSolubility="0.06" cutFace="Pentagon" disabled="false" id="{ca4a8faf-2872-43f8-b126-eb2134631b2b}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" colorSolubility="0.08" cutFace="Pentagon" disabled="false" id="{ce2bba62-2f15-4c68-9418-c06540dd1631}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" colorSolubility="0.03" cutFace="Pentagon" disabled="false" id="{cf781dce-ed78-4c86-997d-eeeaea1a4825}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" color="#fffeca90" disabled="false" id="{e694d1b9-7b59-4983-8b5a-92146b4f9240}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="true"/>
<part chamfered="false" color="#ff614831" deformWidth="0.21" disabled="false" id="{f01e24d7-29d8-49c1-b37c-11f3e82a2892}" locked="false" metalness="1" roughness="0" rounded="false" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" colorSolubility="0.08" cutFace="Pentagon" disabled="false" id="{fda51f9f-2214-4b87-9a23-1b877b18720e}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" color="#ff281a17" colorSolubility="0.04" cutFace="Pentagon" disabled="false" id="{054515ed-3093-40f5-87d3-214e15044bba}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" color="#ff614831" deformWidth="0.21" disabled="false" id="{09d488db-863d-4868-8890-ed2f8cd941df}" locked="false" metalness="1" roughness="0" rounded="false" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" color="#ffcc3d4f" cutFace="{a77512d7-fe5f-465a-aa97-33e0ca928b51}" cutRotation="-0.45" disabled="false" id="{0e885ccb-6cd3-410c-bfdb-1b0cad02c80e}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" colorSolubility="0.06" cutFace="Pentagon" disabled="false" id="{1c66ae95-c9c5-474d-b3db-ae2bccfd2f13}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" colorSolubility="0.06" cutFace="Pentagon" disabled="false" id="{3ed6f8ae-f85b-4bd2-b393-523c3c3e2d0b}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" colorSolubility="0.08" cutFace="Pentagon" disabled="false" id="{48d5870b-b94b-4b8f-9347-f397293a610e}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" color="#ff614831" disabled="false" id="{5b83b16a-09e1-4d56-acbd-7b6a612d150a}" locked="false" metalness="1" roughness="0" rounded="false" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" colorSolubility="0.06" cutFace="Pentagon" disabled="false" id="{61baa97d-912a-46da-ab8d-b3940204a8cf}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" colorSolubility="0.03" cutFace="Pentagon" disabled="false" id="{6b686ee6-2eeb-4d81-a207-d2dea8fe764b}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" color="#fffeca90" deformThickness="0.88" disabled="false" id="{80cfca5d-e04a-4e08-94d9-f4225387107b}" locked="false" rounded="true" subdived="true" visible="true" xMirrored="false"/>
<part chamfered="false" color="#fffeca90" deformThickness="0.59" disabled="false" id="{8676598d-d624-4d86-b498-c76f8a1aa810}" locked="false" rounded="false" subdived="false" visible="true" xMirrored="true"/>
<part chamfered="false" colorSolubility="0.03" cutFace="Pentagon" disabled="false" id="{99aa4033-e6b7-456f-abef-b021acf490f6}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" colorSolubility="0.08" cutFace="Pentagon" disabled="false" id="{a09f07af-91ad-412f-ae6b-1f06ff7808d3}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" disabled="false" id="{a77512d7-fe5f-465a-aa97-33e0ca928b51}" locked="false" rounded="false" subdived="false" target="CutFace" visible="true" xMirrored="false"/>
<part chamfered="false" colorSolubility="0.06" cutFace="Pentagon" disabled="false" id="{ab27a4f2-6c05-46f4-b81a-c2b0453a9bb3}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" colorSolubility="0.08" cutFace="Pentagon" disabled="false" id="{accf6745-05a1-44e4-ab9d-9e4e5b61a0c6}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" color="#fffeca90" deformThickness="0.88" disabled="false" id="{af64c2d3-a4ff-4c82-894b-f22e2f7ce2e7}" locked="false" rounded="true" subdived="true" visible="true" xMirrored="false"/>
<part chamfered="false" colorSolubility="0.03" cutFace="Pentagon" disabled="false" id="{b7773748-17ff-4bcf-93ae-2fe6ab5a0fc6}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" color="#fffeca90" disabled="false" id="{b8a1ab8c-a094-416d-9d13-00aca25e3b3e}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="true"/>
<part chamfered="false" color="#fffeca90" cutFace="Pentagon" cutRotation="-0.26" disabled="false" id="{c4df1b3d-8a8c-43bd-abea-3d0df226dca4}" locked="false" rounded="false" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" color="#fffeca90" deformThickness="0.88" disabled="false" id="{cada7dd1-4087-4257-998f-7151930b31d4}" locked="false" rounded="true" subdived="true" visible="true" xMirrored="false"/>
<part chamfered="false" colorSolubility="0.03" cutFace="Pentagon" disabled="false" id="{cb40704a-692b-4078-9eb8-74a281aea1c8}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" colorSolubility="0.08" cutFace="Pentagon" disabled="false" id="{d27d045a-44e9-44fd-9d76-13eddc43aae1}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="false"/>
<part chamfered="false" color="#fffeca90" deformThickness="0.88" disabled="false" id="{dc4f431d-fa39-4577-ae64-de12cd1a7a33}" locked="false" rounded="true" subdived="true" visible="true" xMirrored="false"/>
<part chamfered="false" color="#ff000000" disabled="false" id="{dc9e5599-dc68-409b-ae2d-1e6fa79a0310}" locked="false" rounded="false" subdived="true" visible="true" xMirrored="true"/>
<part chamfered="false" color="#fffeca90" disabled="false" id="{f8df3586-c5cc-497c-b394-e84e7873dc30}" locked="false" rounded="true" subdived="true" visible="true" xMirrored="false"/>
<part chamfered="false" color="#fffeca90" deformThickness="0.88" disabled="false" id="{fd44561f-05bc-48a4-9a48-45b730ae92a3}" locked="false" rounded="true" subdived="true" visible="true" xMirrored="false"/>
</parts>
<components>
<component combineMode="Normal" expanded="false" id="{4cb516a1-5ecf-4aec-a400-caf15ec91afc}" linkData="{a993e970-3ff6-487a-a4f6-142680bcba44}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{6abad0d3-c042-46ee-abae-a5d664607035}" linkData="{62fe2f2c-316a-4281-9642-710ce43ee9c5}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{935dea42-e3d8-4a4e-b1ec-0255c5584f7e}" linkData="{3cf19fde-79ef-433e-9318-716cb610a893}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{7c98c53e-7080-4546-a3ed-258d32455560}" linkData="{a40908d4-a52a-41af-984e-6ae82bae00b3}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{b5986cfe-2f23-4cdf-93ca-1176d7bfad52}" linkData="{9d71e1d8-5520-4917-9c9f-f696577a1cd2}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{f9c3d87b-6065-411b-834d-5db8fec864c2}" linkData="{9fb2aeb4-9523-4780-accf-bea5ca94732d}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{59deb49e-6f87-4766-adcd-5102a41ee27e}" linkData="{0b421427-908d-4e93-b15c-e16849cf1ba1}" linkDataType="partId"/>
<component combineMode="Uncombined" expanded="false" id="{a177224b-c3e7-442e-a16b-59ba2006f066}" linkData="{f01e24d7-29d8-49c1-b37c-11f3e82a2892}" linkDataType="partId"/>
<component combineMode="Uncombined" expanded="false" id="{bc024a94-0e3a-4df9-a239-f4487dffbe8b}" linkData="{3d579313-129f-465c-a9bf-3c4842ed74dd}" linkDataType="partId"/>
<component combineMode="Uncombined" expanded="false" id="{206e166c-2f16-4ef0-89e2-35f5e618e3e8}" linkData="{8c8fb7ed-f22a-461e-8819-aebb998c0e84}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{03818d56-6def-434c-9187-348f570c139a}" linkData="{a721a808-a940-40e9-b789-1816a9b1fdb8}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{2e974139-011f-4926-9f52-f0e32085f30a}" linkData="{3224e51d-8591-424e-ad66-7172b5c41b95}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{d2968dc5-f775-4f91-abc1-a6cb5a3c72a4}" linkData="{b2b41478-6984-4196-a44c-eb2015044599}" linkDataType="partId"/>
<component combineMode="Inversion" expanded="false" id="{59cce8ca-31de-42c8-9c75-0a22e432ca35}" linkData="{e694d1b9-7b59-4983-8b5a-92146b4f9240}" linkDataType="partId"/>
<component combineMode="Uncombined" expanded="false" id="{484db505-e7d7-4f41-b734-8b91798ad651}" linkData="{4c3c4a67-e536-4d3b-80ac-7f8e1c490c63}" linkDataType="partId"/>
<component combineMode="Normal" expanded="true" id="{bf6c7b13-3bb5-4c46-9438-c9c4ee6f4d5b}" name="RightFrontFoot">
<component combineMode="Normal" expanded="false" id="{46c5f6ff-aaf6-4254-8e68-c3d2982f052a}" linkData="{5b93898b-08ba-4c7b-ae30-072119dc85a9}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{db224f02-e9a9-4620-bdcc-dff7b4413245}" linkData="{ca4a8faf-2872-43f8-b126-eb2134631b2b}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{344930ce-ed88-40e2-958e-3b0e47680613}" linkData="{be83e76b-6e07-4719-a513-f612609b57e3}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{2dd077aa-e476-4c68-8e51-48dfecffb5bc}" linkData="{f8df3586-c5cc-497c-b394-e84e7873dc30}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{bc625a8a-5979-48a4-b513-940220ce0517}" linkData="{af64c2d3-a4ff-4c82-894b-f22e2f7ce2e7}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{aee1978b-f6c8-47a7-b0ec-a6b361be7570}" linkData="{80cfca5d-e04a-4e08-94d9-f4225387107b}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{c0af8cec-93be-416e-963b-f900e49f2c72}" linkData="{dc4f431d-fa39-4577-ae64-de12cd1a7a33}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{82ee4ed0-05c6-4d41-8965-28c2ab1d10ec}" linkData="{c4df1b3d-8a8c-43bd-abea-3d0df226dca4}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{1645b9f2-d7d1-45c1-aff3-f48079cc6ff9}" linkData="{fd44561f-05bc-48a4-9a48-45b730ae92a3}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{7e6b818c-b1b4-4856-95a9-e7c519d6b9e3}" linkData="{cada7dd1-4087-4257-998f-7151930b31d4}" linkDataType="partId"/>
<component combineMode="Uncombined" expanded="false" id="{24bdfa59-7dbd-4a73-88c4-a3d649281a10}" linkData="{09d488db-863d-4868-8890-ed2f8cd941df}" linkDataType="partId"/>
<component combineMode="Uncombined" expanded="false" id="{5ee956f3-d487-4060-9024-1252926c627b}" linkData="{5b83b16a-09e1-4d56-acbd-7b6a612d150a}" linkDataType="partId"/>
<component combineMode="Uncombined" expanded="false" id="{de10ec59-3d8f-429d-948c-9ff8daaf7658}" linkData="{0e885ccb-6cd3-410c-bfdb-1b0cad02c80e}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{e4d11ff8-4422-43a3-99d5-8face1363cdd}" linkData="{a77512d7-fe5f-465a-aa97-33e0ca928b51}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{2733ff69-c700-488a-b633-798fe2ac9eb3}" linkData="{8676598d-d624-4d86-b498-c76f8a1aa810}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{e1b16ca6-5fa2-4059-83b3-ec31a0c2e6dc}" linkData="{054515ed-3093-40f5-87d3-214e15044bba}" linkDataType="partId"/>
<component combineMode="Inversion" expanded="false" id="{5bdae388-00e1-47f9-af25-f1c0edadc092}" linkData="{b8a1ab8c-a094-416d-9d13-00aca25e3b3e}" linkDataType="partId"/>
<component combineMode="Uncombined" expanded="false" id="{3d04308b-959a-4e89-bfc8-ba1af50b594d}" linkData="{dc9e5599-dc68-409b-ae2d-1e6fa79a0310}" linkDataType="partId"/>
<component combineMode="Normal" expanded="true" id="{addbd958-8954-4618-8825-2b7f8ce8e1ad}" name="RightFrontFoot">
<component combineMode="Normal" expanded="false" id="{97922e3f-6298-444c-ad6e-40c0f06c6863}" linkData="{cb40704a-692b-4078-9eb8-74a281aea1c8}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{550ee353-14a8-4e0f-901a-2b8141112834}" linkData="{1c66ae95-c9c5-474d-b3db-ae2bccfd2f13}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{b2bedf29-3329-4957-989b-2f7dbeadab7b}" linkData="{accf6745-05a1-44e4-ab9d-9e4e5b61a0c6}" linkDataType="partId"/>
</component>
<component combineMode="Normal" expanded="true" id="{047098a5-71ec-4138-8257-53c3d9fe3c40}" name="LeftFrontFoot">
<component combineMode="Normal" expanded="false" id="{c41cc0ff-aee1-44f1-a6ed-250a0268a382}" linkData="{94e49ac0-6887-4f3f-aa18-aa600351e205}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{a9d4798e-c884-4965-912b-13b72044663b}" linkData="{41dd5aed-f2be-4812-bbc2-7c9192b2859d}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{96f4f5bf-21eb-45b5-8233-1658d85df248}" linkData="{fda51f9f-2214-4b87-9a23-1b877b18720e}" linkDataType="partId"/>
<component combineMode="Normal" expanded="true" id="{3ebaa85b-de7c-4e4b-bc28-cda30a8683a0}" name="LeftFrontFoot">
<component combineMode="Normal" expanded="false" id="{a374a8c3-b27a-48ee-95c2-b3820dddeebe}" linkData="{99aa4033-e6b7-456f-abef-b021acf490f6}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{e4e4cd72-1bef-40e9-9996-6b303f01a68e}" linkData="{ab27a4f2-6c05-46f4-b81a-c2b0453a9bb3}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{789320bd-48db-44c8-8ea4-8f701270bc3b}" linkData="{a09f07af-91ad-412f-ae6b-1f06ff7808d3}" linkDataType="partId"/>
</component>
<component combineMode="Normal" expanded="true" id="{9f7bd419-fec8-4cf9-ad2d-b4329f6e2d8c}" name="LeftBackFoot">
<component combineMode="Normal" expanded="false" id="{073bdd1e-a65a-48b8-bcf0-03e138b427b2}" linkData="{1d5afc0c-8ef3-4e4a-9a56-d2e17062d666}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{e9fb4d99-069b-488e-9a75-e9a35821f90e}" linkData="{7d3bfd96-a801-4d2d-9cc7-695946484697}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{536753e7-61d7-4298-9fca-6bdcbbdd770c}" linkData="{0e9aeab7-d6b6-4235-acb8-40ea97945c84}" linkDataType="partId"/>
<component combineMode="Normal" expanded="true" id="{4c6b9ce8-7126-409f-b5e0-2d0be5c44a35}" name="LeftBackFoot">
<component combineMode="Normal" expanded="false" id="{99fd8e40-9ea5-41f4-9167-ba1239cde400}" linkData="{6b686ee6-2eeb-4d81-a207-d2dea8fe764b}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{4bfc5e5f-b32d-404b-a584-b51d1397c396}" linkData="{3ed6f8ae-f85b-4bd2-b393-523c3c3e2d0b}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{6ad9c9ed-6416-418b-aca4-70a54de8f8e1}" linkData="{48d5870b-b94b-4b8f-9347-f397293a610e}" linkDataType="partId"/>
</component>
<component combineMode="Normal" expanded="true" id="{08462972-89b7-4f86-bf13-6073190f96d1}" name="RightBackFoot">
<component combineMode="Normal" expanded="false" id="{954fc532-4475-4cb0-9a90-e3402b1d9802}" linkData="{cf781dce-ed78-4c86-997d-eeeaea1a4825}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{3151dbba-f7ed-4097-966d-8c9d01ce77ab}" linkData="{75a5ca65-abc5-492c-917c-70fe2460afaa}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{945fa723-b000-4b6e-8e0f-407e1395710a}" linkData="{ce2bba62-2f15-4c68-9418-c06540dd1631}" linkDataType="partId"/>
<component combineMode="Normal" expanded="true" id="{f7dc2b74-dff0-4bfd-9b91-cc89ce8ca013}" name="RightBackFoot">
<component combineMode="Normal" expanded="false" id="{8fcbd194-aa4e-4636-a178-e48d8d5561ed}" linkData="{b7773748-17ff-4bcf-93ae-2fe6ab5a0fc6}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{18e02637-ea51-4e70-a4d6-8a03ae8072b0}" linkData="{61baa97d-912a-46da-ab8d-b3940204a8cf}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{bb622071-b576-4b4d-8a9c-5e8e026ed1b2}" linkData="{d27d045a-44e9-44fd-9d76-13eddc43aae1}" linkDataType="partId"/>
</component>
</components>
<materials/>

View File

@ -1,120 +1,120 @@
DUST3D 1.0 xml 0000000193
<?xml version="1.0" encoding="UTF-8"?>
<ds3>
<model name="model.xml" offset="0" size="18787"/>
<asset name="canvas.png" offset="18787" size="163519"/>
<model name="model.xml" offset="0" size="19140"/>
<asset name="canvas.png" offset="19140" size="163519"/>
</ds3>
<?xml version="1.0" encoding="UTF-8"?>
<canvas originX="0.832218" originY="0.430706" originZ="2.51087" rigType="None">
<canvas originX="0.832218" originY="0.430706" originZ="2.51087" rigType="Animal">
<nodes>
<node id="{00fde052-9d4e-448e-9682-f5ed93434e0c}" partId="{2c7dcbf9-1455-49f4-9a7c-7ddb17c2179d}" radius="0.0182927" x="0.872962" y="0.190653" z="2.37365"/>
<node id="{091f00c6-a346-44ce-b2f0-905256644b35}" partId="{a5a129b6-564f-46cb-8309-cf05419aa7ed}" radius="0.0108696" x="1.00543" y="0.407609" z="2.1875"/>
<node id="{1021605a-1c1a-4266-83bb-761abfcaebd4}" partId="{a5a129b6-564f-46cb-8309-cf05419aa7ed}" radius="0.0353261" x="0.88587" y="0.278897" z="2.30844"/>
<node id="{18e00014-6fd3-4f46-b474-25a98ad0a5a5}" partId="{5a4379c7-bcc8-4e50-9c3c-24390279a7b1}" radius="0.0271739" x="0.831522" y="0.524457" z="2.98098"/>
<node id="{1ef138ab-f1c0-416c-af05-b988dc62c9ec}" partId="{5a4379c7-bcc8-4e50-9c3c-24390279a7b1}" radius="0.0597826" x="0.831522" y="0.475543" z="2.91848"/>
<node id="{3ccfd176-17d3-4e5e-9a66-7973b67a9b7d}" partId="{5a4379c7-bcc8-4e50-9c3c-24390279a7b1}" radius="0.076087" x="0.831522" y="0.290761" z="2.4375"/>
<node id="{45fa4407-c043-4aba-907d-4c84d16ba5e5}" partId="{a5a129b6-564f-46cb-8309-cf05419aa7ed}" radius="0.0108696" x="1.00543" y="0.665761" z="2.13587"/>
<node id="{4fc24460-bb11-492b-95d5-3df726947c9a}" partId="{5a4379c7-bcc8-4e50-9c3c-24390279a7b1}" radius="0.0466936" x="0.831522" y="0.228261" z="2.2962"/>
<node id="{551efaf5-ae0d-4700-9e9f-acae50c9ba96}" partId="{0ec19fd0-d226-4ba3-8cda-0c444a398038}" radius="0.0308874" x="0.831522" y="0.273312" z="2.18007"/>
<node id="{59f24f88-0441-418f-8d8d-4d30d4b5015b}" partId="{3875e962-9c33-4214-9049-34a042e653ad}" radius="0.0108696" x="1.28533" y="0.741848" z="2.86141"/>
<node id="{5c63c4df-8f54-4fef-933a-f975973d0e0e}" partId="{57488f4d-0076-4e58-94c5-9ff29d94a77f}" radius="0.0127174" x="1.07609" y="0.75" z="2.66033"/>
<node id="{60de62dd-338e-425a-9b83-ed0776bb380a}" partId="{5a4379c7-bcc8-4e50-9c3c-24390279a7b1}" radius="0.0163044" x="0.831522" y="0.274457" z="2.11617"/>
<node id="{67ed5a94-e749-46d1-8ccf-2b33a3e878d6}" partId="{0ec19fd0-d226-4ba3-8cda-0c444a398038}" radius="0.0180256" x="0.831522" y="0.345109" z="2.18328"/>
<node id="{6a47f6ac-e4a8-4e5d-9000-b4c0dfb999a3}" partId="{5a4379c7-bcc8-4e50-9c3c-24390279a7b1}" radius="0.0625" x="0.831522" y="0.315217" z="2.50815"/>
<node id="{71b0f00a-6909-4121-bf1d-83b0a64eab41}" partId="{57488f4d-0076-4e58-94c5-9ff29d94a77f}" radius="0.0127174" x="1.21196" y="0.866848" z="2.78261"/>
<node id="{71ff2d8c-862c-4b06-aa8e-6d89afc335fd}" partId="{5a4379c7-bcc8-4e50-9c3c-24390279a7b1}" radius="0.0244565" x="0.831522" y="0.277174" z="2.2337"/>
<node id="{83aaf66e-e525-42d1-b8fe-3f1772fe57ef}" partId="{3875e962-9c33-4214-9049-34a042e653ad}" radius="0.0190217" x="0.869565" y="0.293478" z="2.43839"/>
<node id="{8aaa9417-0abe-4616-9ef3-01de9803cfe2}" partId="{3875e962-9c33-4214-9049-34a042e653ad}" radius="0.0190217" x="1.13859" y="0.38587" z="2.46624"/>
<node id="{8e680f16-9603-40de-9a5e-90a849ff05f6}" partId="{57488f4d-0076-4e58-94c5-9ff29d94a77f}" radius="0.0344565" x="0.872282" y="0.260869" z="2.37976"/>
<node id="{95b3fc4e-9bab-4b12-80c0-a875649ca4aa}" partId="{a5a129b6-564f-46cb-8309-cf05419aa7ed}" radius="0.0108696" x="1.16033" y="0.842391" z="2.01087"/>
<node id="{96f55ae2-c07f-4637-b8b6-4b7d4c2b209a}" partId="{5a4379c7-bcc8-4e50-9c3c-24390279a7b1}" radius="0.0438766" x="0.831522" y="0.271739" z="2.17935"/>
<node id="{98a8ed94-50ed-43d3-85cf-7968f5e260da}" partId="{3875e962-9c33-4214-9049-34a042e653ad}" radius="0.0108696" x="0.980978" y="0.453804" z="2.78533"/>
<node id="{9b835bb3-040c-4f5f-9993-4240128e0297}" partId="{57488f4d-0076-4e58-94c5-9ff29d94a77f}" radius="0.0222283" x="0.948369" y="0.349185" z="2.39368"/>
<node id="{a0e15099-f840-47fe-b7cc-76e2821f82c8}" partId="{2c7dcbf9-1455-49f4-9a7c-7ddb17c2179d}" radius="0.0689289" x="0.916609" y="0.226513" z="2.64565"/>
<node id="{a64dcc04-41eb-4d71-b95a-9fb7dacc3350}" partId="{2c7dcbf9-1455-49f4-9a7c-7ddb17c2179d}" radius="0.0271739" x="0.870884" y="0.191146" z="2.41539"/>
<node id="{abfebd7d-3a35-41c8-adee-06aced895298}" partId="{a5a129b6-564f-46cb-8309-cf05419aa7ed}" radius="0.0108696" x="1.1087" y="0.774457" z="2.06793"/>
<node id="{ac99b0c2-942c-446e-a0bf-7210382e66a5}" partId="{0ec19fd0-d226-4ba3-8cda-0c444a398038}" radius="0.005" x="0.831522" y="0.559285" z="2.20502"/>
<node id="{b0c0ad56-ba00-4999-8adb-466c6d7185ba}" partId="{5a4379c7-bcc8-4e50-9c3c-24390279a7b1}" radius="0.0597826" x="0.831522" y="0.32337" z="2.61141"/>
<node id="{bfb2b58d-773e-4636-a95f-f036139f6d9b}" partId="{5a4379c7-bcc8-4e50-9c3c-24390279a7b1}" radius="0.0108696" x="0.831522" y="0.244565" z="2.04348"/>
<node id="{cc6c7d03-156f-455f-8456-0b6438e1f6ef}" partId="{a5a129b6-564f-46cb-8309-cf05419aa7ed}" radius="0.0108696" x="1.1875" y="0.869565" z="1.94293"/>
<node id="{d45a860c-f9ec-41bb-868e-3bdc3cf04e8d}" partId="{0ec19fd0-d226-4ba3-8cda-0c444a398038}" radius="0.0125908" x="0.831522" y="0.448867" z="2.19144"/>
<node id="{d6d1a24b-f3b8-478d-9989-feda57a2ee32}" partId="{5a4379c7-bcc8-4e50-9c3c-24390279a7b1}" radius="0.0108696" x="0.831522" y="0.263587" z="2.0788"/>
<node id="{dacc2607-14f6-4c36-a20a-31d5b57d1488}" partId="{5a4379c7-bcc8-4e50-9c3c-24390279a7b1}" radius="0.0597826" x="0.831522" y="0.350543" z="2.73098"/>
<node id="{deb5ae03-03f6-4e0c-a275-fa681b60b379}" partId="{57488f4d-0076-4e58-94c5-9ff29d94a77f}" radius="0.0127174" x="1.23098" y="0.899456" z="2.85055"/>
<node id="{df279fce-55a5-4c68-bb96-feefc9a068c8}" partId="{57488f4d-0076-4e58-94c5-9ff29d94a77f}" radius="0.0127174" x="0.972826" y="0.538044" z="2.55163"/>
<node id="{e1fa1f4c-8426-4dc1-afc1-3f8dee8f1641}" partId="{2c7dcbf9-1455-49f4-9a7c-7ddb17c2179d}" radius="0.107635" x="0.940221" y="0.294171" z="2.87398"/>
<node id="{e9410d86-9d17-41a5-9b55-8587d1e93ea3}" partId="{3875e962-9c33-4214-9049-34a042e653ad}" radius="0.0108696" x="1.45652" y="0.913043" z="3.05163"/>
<node id="{e9d3ac47-6d17-4439-8176-9c51498f8cf0}" partId="{3875e962-9c33-4214-9049-34a042e653ad}" radius="0.0108696" x="1.01359" y="0.470109" z="2.50428"/>
<node id="{f1a99c28-4dd0-45f5-9dc4-769ff13d2fda}" partId="{a5a129b6-564f-46cb-8309-cf05419aa7ed}" radius="0.0190217" x="1.01359" y="0.413043" z="2.35598"/>
<node id="{f2677136-fe7c-4157-bf6e-01f0f51ef4ff}" partId="{5a4379c7-bcc8-4e50-9c3c-24390279a7b1}" radius="0.0597826" x="0.831522" y="0.407609" z="2.84239"/>
<node id="{f5447f26-5f45-4192-97a8-0d6cc635a19b}" partId="{3875e962-9c33-4214-9049-34a042e653ad}" radius="0.0108696" x="1.5" y="0.9375" z="3.11685"/>
<node id="{f6045c8a-b651-425b-86d6-4656ed9d9081}" partId="{2c7dcbf9-1455-49f4-9a7c-7ddb17c2179d}" radius="0.0597826" x="1.01505" y="0.360815" z="3.07335"/>
<node id="{f845abe2-f311-47be-8c61-8b0fa1fa98d2}" partId="{3875e962-9c33-4214-9049-34a042e653ad}" radius="0.0108696" x="1.40761" y="0.858696" z="2.96467"/>
<node id="{f9dad8ee-47b7-4fb4-aa7a-846995320792}" partId="{5a4379c7-bcc8-4e50-9c3c-24390279a7b1}" radius="0.0682336" x="0.831522" y="0.214674" z="2.37228"/>
<node id="{fa1a535e-4d99-42a8-930c-2143b1d124eb}" partId="{a5a129b6-564f-46cb-8309-cf05419aa7ed}" radius="0.0217391" x="1.02717" y="0.342391" z="2.32065"/>
<node id="{fb6417cf-c962-41fd-ac81-20e8b427ab2e}" partId="{57488f4d-0076-4e58-94c5-9ff29d94a77f}" radius="0.01" x="1.17391" y="0.826087" z="2.72283"/>
<node id="{fd9f1818-8101-4850-a420-55f72567c639}" partId="{57488f4d-0076-4e58-94c5-9ff29d94a77f}" radius="0.0127174" x="1.02174" y="0.470109" z="2.44022"/>
<node id="{fdfccd5b-e3a1-4ed3-8e26-2db335c6b683}" partId="{57488f4d-0076-4e58-94c5-9ff29d94a77f}" radius="0.0188359" x="1.1087" y="0.380435" z="2.40761"/>
<node id="{00fde052-9d4e-448e-9682-f5ed93434e0c}" partId="{52c57fba-89d2-4bb0-a9e8-17d96dda6f04}" radius="0.0182927" x="0.872962" y="0.190653" z="2.37365"/>
<node boneMark="Joint" id="{091f00c6-a346-44ce-b2f0-905256644b35}" partId="{d04ab937-945f-4b67-9667-a144a29ae0e9}" radius="0.0108696" x="1.00543" y="0.412463" z="2.1875"/>
<node id="{1021605a-1c1a-4266-83bb-761abfcaebd4}" partId="{d04ab937-945f-4b67-9667-a144a29ae0e9}" radius="0.020763" x="0.856744" y="0.230353" z="2.29388"/>
<node boneMark="Joint" id="{18e00014-6fd3-4f46-b474-25a98ad0a5a5}" partId="{8eb40b25-f5d1-42b6-9906-db2f81a37f4f}" radius="0.0271739" x="0.831522" y="0.524457" z="2.98098"/>
<node boneMark="Joint" id="{1ef138ab-f1c0-416c-af05-b988dc62c9ec}" partId="{8eb40b25-f5d1-42b6-9906-db2f81a37f4f}" radius="0.0597826" x="0.831522" y="0.475543" z="2.91848"/>
<node id="{3ccfd176-17d3-4e5e-9a66-7973b67a9b7d}" partId="{8eb40b25-f5d1-42b6-9906-db2f81a37f4f}" radius="0.076087" x="0.831522" y="0.290761" z="2.4375"/>
<node id="{45fa4407-c043-4aba-907d-4c84d16ba5e5}" partId="{d04ab937-945f-4b67-9667-a144a29ae0e9}" radius="0.0108696" x="1.00543" y="0.670615" z="2.13587"/>
<node id="{4fc24460-bb11-492b-95d5-3df726947c9a}" partId="{8eb40b25-f5d1-42b6-9906-db2f81a37f4f}" radius="0.0466936" x="0.831522" y="0.228261" z="2.2962"/>
<node id="{551efaf5-ae0d-4700-9e9f-acae50c9ba96}" partId="{4997addf-2435-4f34-9df4-ee1c7d24a5f6}" radius="0.0308874" x="0.831522" y="0.273312" z="2.18007"/>
<node boneMark="Joint" id="{59f24f88-0441-418f-8d8d-4d30d4b5015b}" partId="{a85ee6f4-0813-4e23-8430-eeedb08f3b7d}" radius="0.0108696" x="1.28533" y="0.741848" z="2.86141"/>
<node id="{5c63c4df-8f54-4fef-933a-f975973d0e0e}" partId="{8545ec00-3fec-4bb5-aeb8-79850f846ef2}" radius="0.0127174" x="1.07609" y="0.75" z="2.66033"/>
<node id="{60de62dd-338e-425a-9b83-ed0776bb380a}" partId="{8eb40b25-f5d1-42b6-9906-db2f81a37f4f}" radius="0.0163044" x="0.831522" y="0.274457" z="2.11617"/>
<node id="{67ed5a94-e749-46d1-8ccf-2b33a3e878d6}" partId="{4997addf-2435-4f34-9df4-ee1c7d24a5f6}" radius="0.0180256" x="0.831522" y="0.345109" z="2.18328"/>
<node boneMark="Tail" id="{6a47f6ac-e4a8-4e5d-9000-b4c0dfb999a3}" partId="{8eb40b25-f5d1-42b6-9906-db2f81a37f4f}" radius="0.0625" x="0.831522" y="0.315217" z="2.50815"/>
<node id="{71b0f00a-6909-4121-bf1d-83b0a64eab41}" partId="{8545ec00-3fec-4bb5-aeb8-79850f846ef2}" radius="0.0127174" x="1.21196" y="0.866848" z="2.78261"/>
<node boneMark="Neck" id="{71ff2d8c-862c-4b06-aa8e-6d89afc335fd}" partId="{8eb40b25-f5d1-42b6-9906-db2f81a37f4f}" radius="0.0244565" x="0.831522" y="0.277174" z="2.2337"/>
<node id="{83aaf66e-e525-42d1-b8fe-3f1772fe57ef}" partId="{a85ee6f4-0813-4e23-8430-eeedb08f3b7d}" radius="0.0190217" x="0.869565" y="0.269206" z="2.45295"/>
<node boneMark="Limb" id="{8aaa9417-0abe-4616-9ef3-01de9803cfe2}" partId="{a85ee6f4-0813-4e23-8430-eeedb08f3b7d}" radius="0.0190217" x="1.13859" y="0.38587" z="2.46624"/>
<node id="{8e680f16-9603-40de-9a5e-90a849ff05f6}" partId="{8545ec00-3fec-4bb5-aeb8-79850f846ef2}" radius="0.0247478" x="0.852865" y="0.21718" z="2.37005"/>
<node id="{95b3fc4e-9bab-4b12-80c0-a875649ca4aa}" partId="{d04ab937-945f-4b67-9667-a144a29ae0e9}" radius="0.0108696" x="1.16033" y="0.847245" z="2.01087"/>
<node id="{96f55ae2-c07f-4637-b8b6-4b7d4c2b209a}" partId="{8eb40b25-f5d1-42b6-9906-db2f81a37f4f}" radius="0.0438766" x="0.831522" y="0.271739" z="2.17935"/>
<node boneMark="Joint" id="{98a8ed94-50ed-43d3-85cf-7968f5e260da}" partId="{a85ee6f4-0813-4e23-8430-eeedb08f3b7d}" radius="0.0108696" x="0.980978" y="0.453804" z="2.78533"/>
<node boneMark="Limb" id="{9b835bb3-040c-4f5f-9993-4240128e0297}" partId="{8545ec00-3fec-4bb5-aeb8-79850f846ef2}" radius="0.0222283" x="0.948369" y="0.349185" z="2.39368"/>
<node id="{a0e15099-f840-47fe-b7cc-76e2821f82c8}" partId="{52c57fba-89d2-4bb0-a9e8-17d96dda6f04}" radius="0.0689289" x="0.916609" y="0.226513" z="2.64565"/>
<node id="{a64dcc04-41eb-4d71-b95a-9fb7dacc3350}" partId="{52c57fba-89d2-4bb0-a9e8-17d96dda6f04}" radius="0.0271739" x="0.870884" y="0.191146" z="2.41539"/>
<node id="{abfebd7d-3a35-41c8-adee-06aced895298}" partId="{d04ab937-945f-4b67-9667-a144a29ae0e9}" radius="0.0108696" x="1.1087" y="0.779311" z="2.06793"/>
<node id="{ac99b0c2-942c-446e-a0bf-7210382e66a5}" partId="{4997addf-2435-4f34-9df4-ee1c7d24a5f6}" radius="0.005" x="0.831522" y="0.559285" z="2.20502"/>
<node boneMark="Joint" id="{b0c0ad56-ba00-4999-8adb-466c6d7185ba}" partId="{8eb40b25-f5d1-42b6-9906-db2f81a37f4f}" radius="0.0597826" x="0.831522" y="0.32337" z="2.61141"/>
<node boneMark="Joint" id="{bfb2b58d-773e-4636-a95f-f036139f6d9b}" partId="{8eb40b25-f5d1-42b6-9906-db2f81a37f4f}" radius="0.0108696" x="0.831522" y="0.244565" z="2.04348"/>
<node boneMark="Joint" id="{cc6c7d03-156f-455f-8456-0b6438e1f6ef}" partId="{d04ab937-945f-4b67-9667-a144a29ae0e9}" radius="0.0108696" x="1.1875" y="0.874419" z="1.94293"/>
<node id="{d45a860c-f9ec-41bb-868e-3bdc3cf04e8d}" partId="{4997addf-2435-4f34-9df4-ee1c7d24a5f6}" radius="0.0125908" x="0.831522" y="0.448867" z="2.19144"/>
<node id="{d6d1a24b-f3b8-478d-9989-feda57a2ee32}" partId="{8eb40b25-f5d1-42b6-9906-db2f81a37f4f}" radius="0.0108696" x="0.831522" y="0.263587" z="2.0788"/>
<node boneMark="Joint" id="{dacc2607-14f6-4c36-a20a-31d5b57d1488}" partId="{8eb40b25-f5d1-42b6-9906-db2f81a37f4f}" radius="0.0597826" x="0.831522" y="0.350543" z="2.73098"/>
<node boneMark="Joint" id="{deb5ae03-03f6-4e0c-a275-fa681b60b379}" partId="{8545ec00-3fec-4bb5-aeb8-79850f846ef2}" radius="0.0127174" x="1.23098" y="0.899456" z="2.85055"/>
<node boneMark="Joint" id="{df279fce-55a5-4c68-bb96-feefc9a068c8}" partId="{8545ec00-3fec-4bb5-aeb8-79850f846ef2}" radius="0.0127174" x="0.972826" y="0.538044" z="2.55163"/>
<node id="{e1fa1f4c-8426-4dc1-afc1-3f8dee8f1641}" partId="{52c57fba-89d2-4bb0-a9e8-17d96dda6f04}" radius="0.107635" x="0.940221" y="0.294171" z="2.87398"/>
<node id="{e9410d86-9d17-41a5-9b55-8587d1e93ea3}" partId="{a85ee6f4-0813-4e23-8430-eeedb08f3b7d}" radius="0.0108696" x="1.45652" y="0.913043" z="3.05163"/>
<node boneMark="Joint" id="{e9d3ac47-6d17-4439-8176-9c51498f8cf0}" partId="{a85ee6f4-0813-4e23-8430-eeedb08f3b7d}" radius="0.0108696" x="1.01359" y="0.470109" z="2.50428"/>
<node boneMark="Joint" id="{f1a99c28-4dd0-45f5-9dc4-769ff13d2fda}" partId="{d04ab937-945f-4b67-9667-a144a29ae0e9}" radius="0.0190217" x="1.01359" y="0.417897" z="2.35598"/>
<node boneMark="Joint" id="{f2677136-fe7c-4157-bf6e-01f0f51ef4ff}" partId="{8eb40b25-f5d1-42b6-9906-db2f81a37f4f}" radius="0.0597826" x="0.831522" y="0.407609" z="2.84239"/>
<node boneMark="Joint" id="{f5447f26-5f45-4192-97a8-0d6cc635a19b}" partId="{a85ee6f4-0813-4e23-8430-eeedb08f3b7d}" radius="0.0108696" x="1.5" y="0.9375" z="3.11685"/>
<node id="{f6045c8a-b651-425b-86d6-4656ed9d9081}" partId="{52c57fba-89d2-4bb0-a9e8-17d96dda6f04}" radius="0.0597826" x="1.01505" y="0.360815" z="3.07335"/>
<node id="{f845abe2-f311-47be-8c61-8b0fa1fa98d2}" partId="{a85ee6f4-0813-4e23-8430-eeedb08f3b7d}" radius="0.0108696" x="1.40761" y="0.858696" z="2.96467"/>
<node id="{f9dad8ee-47b7-4fb4-aa7a-846995320792}" partId="{8eb40b25-f5d1-42b6-9906-db2f81a37f4f}" radius="0.0682336" x="0.831522" y="0.214674" z="2.37228"/>
<node boneMark="Limb" id="{fa1a535e-4d99-42a8-930c-2143b1d124eb}" partId="{d04ab937-945f-4b67-9667-a144a29ae0e9}" radius="0.0217391" x="1.02717" y="0.347245" z="2.32065"/>
<node id="{fb6417cf-c962-41fd-ac81-20e8b427ab2e}" partId="{8545ec00-3fec-4bb5-aeb8-79850f846ef2}" radius="0.01" x="1.17391" y="0.826087" z="2.72283"/>
<node boneMark="Joint" id="{fd9f1818-8101-4850-a420-55f72567c639}" partId="{8545ec00-3fec-4bb5-aeb8-79850f846ef2}" radius="0.0127174" x="1.02174" y="0.470109" z="2.44022"/>
<node id="{fdfccd5b-e3a1-4ed3-8e26-2db335c6b683}" partId="{8545ec00-3fec-4bb5-aeb8-79850f846ef2}" radius="0.0188359" x="1.1087" y="0.380435" z="2.40761"/>
</nodes>
<edges>
<edge from="{60de62dd-338e-425a-9b83-ed0776bb380a}" id="{004e3284-50b1-4c43-ae03-351057f72d34}" partId="{5a4379c7-bcc8-4e50-9c3c-24390279a7b1}" to="{d6d1a24b-f3b8-478d-9989-feda57a2ee32}"/>
<edge from="{1021605a-1c1a-4266-83bb-761abfcaebd4}" id="{0569f362-f47c-485c-a6a9-6e3481b21a90}" partId="{a5a129b6-564f-46cb-8309-cf05419aa7ed}" to="{fa1a535e-4d99-42a8-930c-2143b1d124eb}"/>
<edge from="{f1a99c28-4dd0-45f5-9dc4-769ff13d2fda}" id="{0a8be92c-5977-4471-b2d3-5009eda7df2e}" partId="{a5a129b6-564f-46cb-8309-cf05419aa7ed}" to="{091f00c6-a346-44ce-b2f0-905256644b35}"/>
<edge from="{5c63c4df-8f54-4fef-933a-f975973d0e0e}" id="{0b30676d-4c10-4c4b-aa73-04a7a42a1b7f}" partId="{57488f4d-0076-4e58-94c5-9ff29d94a77f}" to="{fb6417cf-c962-41fd-ac81-20e8b427ab2e}"/>
<edge from="{df279fce-55a5-4c68-bb96-feefc9a068c8}" id="{0b6366a7-a16e-4f6f-a0f7-13d1dfa5f591}" partId="{57488f4d-0076-4e58-94c5-9ff29d94a77f}" to="{5c63c4df-8f54-4fef-933a-f975973d0e0e}"/>
<edge from="{b0c0ad56-ba00-4999-8adb-466c6d7185ba}" id="{0d2b5542-fbb8-4aa9-a0cb-c2517dcf1aa9}" partId="{5a4379c7-bcc8-4e50-9c3c-24390279a7b1}" to="{dacc2607-14f6-4c36-a20a-31d5b57d1488}"/>
<edge from="{96f55ae2-c07f-4637-b8b6-4b7d4c2b209a}" id="{0e321006-a9ad-4bd1-9429-80e9b4781c3e}" partId="{5a4379c7-bcc8-4e50-9c3c-24390279a7b1}" to="{71ff2d8c-862c-4b06-aa8e-6d89afc335fd}"/>
<edge from="{67ed5a94-e749-46d1-8ccf-2b33a3e878d6}" id="{10582139-843f-4591-bc63-1e9ae19c6d42}" partId="{0ec19fd0-d226-4ba3-8cda-0c444a398038}" to="{d45a860c-f9ec-41bb-868e-3bdc3cf04e8d}"/>
<edge from="{45fa4407-c043-4aba-907d-4c84d16ba5e5}" id="{22253314-b363-4698-a072-c0a5ba0ac000}" partId="{a5a129b6-564f-46cb-8309-cf05419aa7ed}" to="{abfebd7d-3a35-41c8-adee-06aced895298}"/>
<edge from="{f9dad8ee-47b7-4fb4-aa7a-846995320792}" id="{23fcdae1-af2b-4420-a761-63f3d0d203b4}" partId="{5a4379c7-bcc8-4e50-9c3c-24390279a7b1}" to="{3ccfd176-17d3-4e5e-9a66-7973b67a9b7d}"/>
<edge from="{8aaa9417-0abe-4616-9ef3-01de9803cfe2}" id="{35fca7ae-1411-4125-a29d-c977e60c2026}" partId="{3875e962-9c33-4214-9049-34a042e653ad}" to="{e9d3ac47-6d17-4439-8176-9c51498f8cf0}"/>
<edge from="{fdfccd5b-e3a1-4ed3-8e26-2db335c6b683}" id="{3a69636f-46a5-4363-88a3-afa75c303fb7}" partId="{57488f4d-0076-4e58-94c5-9ff29d94a77f}" to="{fd9f1818-8101-4850-a420-55f72567c639}"/>
<edge from="{f2677136-fe7c-4157-bf6e-01f0f51ef4ff}" id="{3af2982e-d86f-492c-b9e0-ef4f5c0af53d}" partId="{5a4379c7-bcc8-4e50-9c3c-24390279a7b1}" to="{1ef138ab-f1c0-416c-af05-b988dc62c9ec}"/>
<edge from="{d45a860c-f9ec-41bb-868e-3bdc3cf04e8d}" id="{3d874434-2913-4166-8e6e-712a47e44db0}" partId="{0ec19fd0-d226-4ba3-8cda-0c444a398038}" to="{ac99b0c2-942c-446e-a0bf-7210382e66a5}"/>
<edge from="{f845abe2-f311-47be-8c61-8b0fa1fa98d2}" id="{4dfeb7b4-4426-418f-b85f-c14da4d296f2}" partId="{3875e962-9c33-4214-9049-34a042e653ad}" to="{e9410d86-9d17-41a5-9b55-8587d1e93ea3}"/>
<edge from="{6a47f6ac-e4a8-4e5d-9000-b4c0dfb999a3}" id="{566cd759-190b-4374-b57d-1ef8ed54cb45}" partId="{5a4379c7-bcc8-4e50-9c3c-24390279a7b1}" to="{b0c0ad56-ba00-4999-8adb-466c6d7185ba}"/>
<edge from="{dacc2607-14f6-4c36-a20a-31d5b57d1488}" id="{5b0805fc-6b71-4a8f-9318-4dd3df63f718}" partId="{5a4379c7-bcc8-4e50-9c3c-24390279a7b1}" to="{f2677136-fe7c-4157-bf6e-01f0f51ef4ff}"/>
<edge from="{a64dcc04-41eb-4d71-b95a-9fb7dacc3350}" id="{63dac02d-b2f8-4ebc-98c4-54fcea0d9798}" partId="{2c7dcbf9-1455-49f4-9a7c-7ddb17c2179d}" to="{00fde052-9d4e-448e-9682-f5ed93434e0c}"/>
<edge from="{60de62dd-338e-425a-9b83-ed0776bb380a}" id="{67bd7a5e-4002-418c-ad41-f8827056d936}" partId="{5a4379c7-bcc8-4e50-9c3c-24390279a7b1}" to="{96f55ae2-c07f-4637-b8b6-4b7d4c2b209a}"/>
<edge from="{95b3fc4e-9bab-4b12-80c0-a875649ca4aa}" id="{67d3a132-f6f4-4c39-93fd-53d66a5df4ab}" partId="{a5a129b6-564f-46cb-8309-cf05419aa7ed}" to="{cc6c7d03-156f-455f-8456-0b6438e1f6ef}"/>
<edge from="{a0e15099-f840-47fe-b7cc-76e2821f82c8}" id="{6cc8a10c-58e8-4833-98f3-d1bba4461f34}" partId="{2c7dcbf9-1455-49f4-9a7c-7ddb17c2179d}" to="{e1fa1f4c-8426-4dc1-afc1-3f8dee8f1641}"/>
<edge from="{a64dcc04-41eb-4d71-b95a-9fb7dacc3350}" id="{720d02e7-45a3-4659-a779-2b775f58d61f}" partId="{2c7dcbf9-1455-49f4-9a7c-7ddb17c2179d}" to="{a0e15099-f840-47fe-b7cc-76e2821f82c8}"/>
<edge from="{e9410d86-9d17-41a5-9b55-8587d1e93ea3}" id="{748ecd4b-ecaa-4707-9d1b-eba56429b77a}" partId="{3875e962-9c33-4214-9049-34a042e653ad}" to="{f5447f26-5f45-4192-97a8-0d6cc635a19b}"/>
<edge from="{e1fa1f4c-8426-4dc1-afc1-3f8dee8f1641}" id="{76dd8ca8-896a-4899-acff-c60f42725d45}" partId="{2c7dcbf9-1455-49f4-9a7c-7ddb17c2179d}" to="{f6045c8a-b651-425b-86d6-4656ed9d9081}"/>
<edge from="{091f00c6-a346-44ce-b2f0-905256644b35}" id="{7cd9de86-8db5-4672-9d73-e813f5cad9a9}" partId="{a5a129b6-564f-46cb-8309-cf05419aa7ed}" to="{45fa4407-c043-4aba-907d-4c84d16ba5e5}"/>
<edge from="{abfebd7d-3a35-41c8-adee-06aced895298}" id="{7de5e8e0-24ff-4b64-9047-4bd8f4855506}" partId="{a5a129b6-564f-46cb-8309-cf05419aa7ed}" to="{95b3fc4e-9bab-4b12-80c0-a875649ca4aa}"/>
<edge from="{8e680f16-9603-40de-9a5e-90a849ff05f6}" id="{8a744155-4a9f-446d-a39e-713fdd134426}" partId="{57488f4d-0076-4e58-94c5-9ff29d94a77f}" to="{9b835bb3-040c-4f5f-9993-4240128e0297}"/>
<edge from="{fa1a535e-4d99-42a8-930c-2143b1d124eb}" id="{a439f439-4dba-488b-b4ee-d38b19fc4fcf}" partId="{a5a129b6-564f-46cb-8309-cf05419aa7ed}" to="{f1a99c28-4dd0-45f5-9dc4-769ff13d2fda}"/>
<edge from="{fd9f1818-8101-4850-a420-55f72567c639}" id="{a6357a12-14ba-4dc4-846b-4be892746e6a}" partId="{57488f4d-0076-4e58-94c5-9ff29d94a77f}" to="{df279fce-55a5-4c68-bb96-feefc9a068c8}"/>
<edge from="{71ff2d8c-862c-4b06-aa8e-6d89afc335fd}" id="{ab708822-ad92-4977-b209-b1103a334e82}" partId="{5a4379c7-bcc8-4e50-9c3c-24390279a7b1}" to="{4fc24460-bb11-492b-95d5-3df726947c9a}"/>
<edge from="{1ef138ab-f1c0-416c-af05-b988dc62c9ec}" id="{b17d6b41-898c-46db-8d88-1117bc53c627}" partId="{5a4379c7-bcc8-4e50-9c3c-24390279a7b1}" to="{18e00014-6fd3-4f46-b474-25a98ad0a5a5}"/>
<edge from="{71b0f00a-6909-4121-bf1d-83b0a64eab41}" id="{c232a9eb-fda5-4938-82da-c807c021b102}" partId="{57488f4d-0076-4e58-94c5-9ff29d94a77f}" to="{deb5ae03-03f6-4e0c-a275-fa681b60b379}"/>
<edge from="{9b835bb3-040c-4f5f-9993-4240128e0297}" id="{caaaf017-8034-4deb-ac06-b5052603bd6e}" partId="{57488f4d-0076-4e58-94c5-9ff29d94a77f}" to="{fdfccd5b-e3a1-4ed3-8e26-2db335c6b683}"/>
<edge from="{d6d1a24b-f3b8-478d-9989-feda57a2ee32}" id="{cb1c1ed1-eb5b-4622-b491-2b454836600b}" partId="{5a4379c7-bcc8-4e50-9c3c-24390279a7b1}" to="{bfb2b58d-773e-4636-a95f-f036139f6d9b}"/>
<edge from="{83aaf66e-e525-42d1-b8fe-3f1772fe57ef}" id="{cb880891-b7e8-4592-ab80-fd165c70eccb}" partId="{3875e962-9c33-4214-9049-34a042e653ad}" to="{8aaa9417-0abe-4616-9ef3-01de9803cfe2}"/>
<edge from="{98a8ed94-50ed-43d3-85cf-7968f5e260da}" id="{d102fa2e-afb6-4f0c-910a-2952dea4781c}" partId="{3875e962-9c33-4214-9049-34a042e653ad}" to="{59f24f88-0441-418f-8d8d-4d30d4b5015b}"/>
<edge from="{e9d3ac47-6d17-4439-8176-9c51498f8cf0}" id="{d55b2dbc-4c3a-4c52-9c2c-16127e00a889}" partId="{3875e962-9c33-4214-9049-34a042e653ad}" to="{98a8ed94-50ed-43d3-85cf-7968f5e260da}"/>
<edge from="{3ccfd176-17d3-4e5e-9a66-7973b67a9b7d}" id="{d89420d6-ffca-4dd4-a5b5-2ff9b8618b01}" partId="{5a4379c7-bcc8-4e50-9c3c-24390279a7b1}" to="{6a47f6ac-e4a8-4e5d-9000-b4c0dfb999a3}"/>
<edge from="{67ed5a94-e749-46d1-8ccf-2b33a3e878d6}" id="{e99f5404-7dce-4770-aac9-22ad610b4420}" partId="{0ec19fd0-d226-4ba3-8cda-0c444a398038}" to="{551efaf5-ae0d-4700-9e9f-acae50c9ba96}"/>
<edge from="{59f24f88-0441-418f-8d8d-4d30d4b5015b}" id="{ee50292f-d21a-4ba6-8e31-d2f9fb577398}" partId="{3875e962-9c33-4214-9049-34a042e653ad}" to="{f845abe2-f311-47be-8c61-8b0fa1fa98d2}"/>
<edge from="{fb6417cf-c962-41fd-ac81-20e8b427ab2e}" id="{f1d56833-ff9a-44d7-a337-76eb68549678}" partId="{57488f4d-0076-4e58-94c5-9ff29d94a77f}" to="{71b0f00a-6909-4121-bf1d-83b0a64eab41}"/>
<edge from="{4fc24460-bb11-492b-95d5-3df726947c9a}" id="{fc919b1d-e12b-42d0-a3af-fc36c8c78ef4}" partId="{5a4379c7-bcc8-4e50-9c3c-24390279a7b1}" to="{f9dad8ee-47b7-4fb4-aa7a-846995320792}"/>
<edge from="{60de62dd-338e-425a-9b83-ed0776bb380a}" id="{004e3284-50b1-4c43-ae03-351057f72d34}" partId="{8eb40b25-f5d1-42b6-9906-db2f81a37f4f}" to="{d6d1a24b-f3b8-478d-9989-feda57a2ee32}"/>
<edge from="{1021605a-1c1a-4266-83bb-761abfcaebd4}" id="{0569f362-f47c-485c-a6a9-6e3481b21a90}" partId="{d04ab937-945f-4b67-9667-a144a29ae0e9}" to="{fa1a535e-4d99-42a8-930c-2143b1d124eb}"/>
<edge from="{f1a99c28-4dd0-45f5-9dc4-769ff13d2fda}" id="{0a8be92c-5977-4471-b2d3-5009eda7df2e}" partId="{d04ab937-945f-4b67-9667-a144a29ae0e9}" to="{091f00c6-a346-44ce-b2f0-905256644b35}"/>
<edge from="{5c63c4df-8f54-4fef-933a-f975973d0e0e}" id="{0b30676d-4c10-4c4b-aa73-04a7a42a1b7f}" partId="{8545ec00-3fec-4bb5-aeb8-79850f846ef2}" to="{fb6417cf-c962-41fd-ac81-20e8b427ab2e}"/>
<edge from="{df279fce-55a5-4c68-bb96-feefc9a068c8}" id="{0b6366a7-a16e-4f6f-a0f7-13d1dfa5f591}" partId="{8545ec00-3fec-4bb5-aeb8-79850f846ef2}" to="{5c63c4df-8f54-4fef-933a-f975973d0e0e}"/>
<edge from="{b0c0ad56-ba00-4999-8adb-466c6d7185ba}" id="{0d2b5542-fbb8-4aa9-a0cb-c2517dcf1aa9}" partId="{8eb40b25-f5d1-42b6-9906-db2f81a37f4f}" to="{dacc2607-14f6-4c36-a20a-31d5b57d1488}"/>
<edge from="{96f55ae2-c07f-4637-b8b6-4b7d4c2b209a}" id="{0e321006-a9ad-4bd1-9429-80e9b4781c3e}" partId="{8eb40b25-f5d1-42b6-9906-db2f81a37f4f}" to="{71ff2d8c-862c-4b06-aa8e-6d89afc335fd}"/>
<edge from="{67ed5a94-e749-46d1-8ccf-2b33a3e878d6}" id="{10582139-843f-4591-bc63-1e9ae19c6d42}" partId="{4997addf-2435-4f34-9df4-ee1c7d24a5f6}" to="{d45a860c-f9ec-41bb-868e-3bdc3cf04e8d}"/>
<edge from="{45fa4407-c043-4aba-907d-4c84d16ba5e5}" id="{22253314-b363-4698-a072-c0a5ba0ac000}" partId="{d04ab937-945f-4b67-9667-a144a29ae0e9}" to="{abfebd7d-3a35-41c8-adee-06aced895298}"/>
<edge from="{f9dad8ee-47b7-4fb4-aa7a-846995320792}" id="{23fcdae1-af2b-4420-a761-63f3d0d203b4}" partId="{8eb40b25-f5d1-42b6-9906-db2f81a37f4f}" to="{3ccfd176-17d3-4e5e-9a66-7973b67a9b7d}"/>
<edge from="{8aaa9417-0abe-4616-9ef3-01de9803cfe2}" id="{35fca7ae-1411-4125-a29d-c977e60c2026}" partId="{a85ee6f4-0813-4e23-8430-eeedb08f3b7d}" to="{e9d3ac47-6d17-4439-8176-9c51498f8cf0}"/>
<edge from="{fdfccd5b-e3a1-4ed3-8e26-2db335c6b683}" id="{3a69636f-46a5-4363-88a3-afa75c303fb7}" partId="{8545ec00-3fec-4bb5-aeb8-79850f846ef2}" to="{fd9f1818-8101-4850-a420-55f72567c639}"/>
<edge from="{f2677136-fe7c-4157-bf6e-01f0f51ef4ff}" id="{3af2982e-d86f-492c-b9e0-ef4f5c0af53d}" partId="{8eb40b25-f5d1-42b6-9906-db2f81a37f4f}" to="{1ef138ab-f1c0-416c-af05-b988dc62c9ec}"/>
<edge from="{d45a860c-f9ec-41bb-868e-3bdc3cf04e8d}" id="{3d874434-2913-4166-8e6e-712a47e44db0}" partId="{4997addf-2435-4f34-9df4-ee1c7d24a5f6}" to="{ac99b0c2-942c-446e-a0bf-7210382e66a5}"/>
<edge from="{f845abe2-f311-47be-8c61-8b0fa1fa98d2}" id="{4dfeb7b4-4426-418f-b85f-c14da4d296f2}" partId="{a85ee6f4-0813-4e23-8430-eeedb08f3b7d}" to="{e9410d86-9d17-41a5-9b55-8587d1e93ea3}"/>
<edge from="{6a47f6ac-e4a8-4e5d-9000-b4c0dfb999a3}" id="{566cd759-190b-4374-b57d-1ef8ed54cb45}" partId="{8eb40b25-f5d1-42b6-9906-db2f81a37f4f}" to="{b0c0ad56-ba00-4999-8adb-466c6d7185ba}"/>
<edge from="{dacc2607-14f6-4c36-a20a-31d5b57d1488}" id="{5b0805fc-6b71-4a8f-9318-4dd3df63f718}" partId="{8eb40b25-f5d1-42b6-9906-db2f81a37f4f}" to="{f2677136-fe7c-4157-bf6e-01f0f51ef4ff}"/>
<edge from="{a64dcc04-41eb-4d71-b95a-9fb7dacc3350}" id="{63dac02d-b2f8-4ebc-98c4-54fcea0d9798}" partId="{52c57fba-89d2-4bb0-a9e8-17d96dda6f04}" to="{00fde052-9d4e-448e-9682-f5ed93434e0c}"/>
<edge from="{60de62dd-338e-425a-9b83-ed0776bb380a}" id="{67bd7a5e-4002-418c-ad41-f8827056d936}" partId="{8eb40b25-f5d1-42b6-9906-db2f81a37f4f}" to="{96f55ae2-c07f-4637-b8b6-4b7d4c2b209a}"/>
<edge from="{95b3fc4e-9bab-4b12-80c0-a875649ca4aa}" id="{67d3a132-f6f4-4c39-93fd-53d66a5df4ab}" partId="{d04ab937-945f-4b67-9667-a144a29ae0e9}" to="{cc6c7d03-156f-455f-8456-0b6438e1f6ef}"/>
<edge from="{a0e15099-f840-47fe-b7cc-76e2821f82c8}" id="{6cc8a10c-58e8-4833-98f3-d1bba4461f34}" partId="{52c57fba-89d2-4bb0-a9e8-17d96dda6f04}" to="{e1fa1f4c-8426-4dc1-afc1-3f8dee8f1641}"/>
<edge from="{a64dcc04-41eb-4d71-b95a-9fb7dacc3350}" id="{720d02e7-45a3-4659-a779-2b775f58d61f}" partId="{52c57fba-89d2-4bb0-a9e8-17d96dda6f04}" to="{a0e15099-f840-47fe-b7cc-76e2821f82c8}"/>
<edge from="{e9410d86-9d17-41a5-9b55-8587d1e93ea3}" id="{748ecd4b-ecaa-4707-9d1b-eba56429b77a}" partId="{a85ee6f4-0813-4e23-8430-eeedb08f3b7d}" to="{f5447f26-5f45-4192-97a8-0d6cc635a19b}"/>
<edge from="{e1fa1f4c-8426-4dc1-afc1-3f8dee8f1641}" id="{76dd8ca8-896a-4899-acff-c60f42725d45}" partId="{52c57fba-89d2-4bb0-a9e8-17d96dda6f04}" to="{f6045c8a-b651-425b-86d6-4656ed9d9081}"/>
<edge from="{091f00c6-a346-44ce-b2f0-905256644b35}" id="{7cd9de86-8db5-4672-9d73-e813f5cad9a9}" partId="{d04ab937-945f-4b67-9667-a144a29ae0e9}" to="{45fa4407-c043-4aba-907d-4c84d16ba5e5}"/>
<edge from="{abfebd7d-3a35-41c8-adee-06aced895298}" id="{7de5e8e0-24ff-4b64-9047-4bd8f4855506}" partId="{d04ab937-945f-4b67-9667-a144a29ae0e9}" to="{95b3fc4e-9bab-4b12-80c0-a875649ca4aa}"/>
<edge from="{8e680f16-9603-40de-9a5e-90a849ff05f6}" id="{8a744155-4a9f-446d-a39e-713fdd134426}" partId="{8545ec00-3fec-4bb5-aeb8-79850f846ef2}" to="{9b835bb3-040c-4f5f-9993-4240128e0297}"/>
<edge from="{fa1a535e-4d99-42a8-930c-2143b1d124eb}" id="{a439f439-4dba-488b-b4ee-d38b19fc4fcf}" partId="{d04ab937-945f-4b67-9667-a144a29ae0e9}" to="{f1a99c28-4dd0-45f5-9dc4-769ff13d2fda}"/>
<edge from="{fd9f1818-8101-4850-a420-55f72567c639}" id="{a6357a12-14ba-4dc4-846b-4be892746e6a}" partId="{8545ec00-3fec-4bb5-aeb8-79850f846ef2}" to="{df279fce-55a5-4c68-bb96-feefc9a068c8}"/>
<edge from="{71ff2d8c-862c-4b06-aa8e-6d89afc335fd}" id="{ab708822-ad92-4977-b209-b1103a334e82}" partId="{8eb40b25-f5d1-42b6-9906-db2f81a37f4f}" to="{4fc24460-bb11-492b-95d5-3df726947c9a}"/>
<edge from="{1ef138ab-f1c0-416c-af05-b988dc62c9ec}" id="{b17d6b41-898c-46db-8d88-1117bc53c627}" partId="{8eb40b25-f5d1-42b6-9906-db2f81a37f4f}" to="{18e00014-6fd3-4f46-b474-25a98ad0a5a5}"/>
<edge from="{71b0f00a-6909-4121-bf1d-83b0a64eab41}" id="{c232a9eb-fda5-4938-82da-c807c021b102}" partId="{8545ec00-3fec-4bb5-aeb8-79850f846ef2}" to="{deb5ae03-03f6-4e0c-a275-fa681b60b379}"/>
<edge from="{9b835bb3-040c-4f5f-9993-4240128e0297}" id="{caaaf017-8034-4deb-ac06-b5052603bd6e}" partId="{8545ec00-3fec-4bb5-aeb8-79850f846ef2}" to="{fdfccd5b-e3a1-4ed3-8e26-2db335c6b683}"/>
<edge from="{d6d1a24b-f3b8-478d-9989-feda57a2ee32}" id="{cb1c1ed1-eb5b-4622-b491-2b454836600b}" partId="{8eb40b25-f5d1-42b6-9906-db2f81a37f4f}" to="{bfb2b58d-773e-4636-a95f-f036139f6d9b}"/>
<edge from="{83aaf66e-e525-42d1-b8fe-3f1772fe57ef}" id="{cb880891-b7e8-4592-ab80-fd165c70eccb}" partId="{a85ee6f4-0813-4e23-8430-eeedb08f3b7d}" to="{8aaa9417-0abe-4616-9ef3-01de9803cfe2}"/>
<edge from="{98a8ed94-50ed-43d3-85cf-7968f5e260da}" id="{d102fa2e-afb6-4f0c-910a-2952dea4781c}" partId="{a85ee6f4-0813-4e23-8430-eeedb08f3b7d}" to="{59f24f88-0441-418f-8d8d-4d30d4b5015b}"/>
<edge from="{e9d3ac47-6d17-4439-8176-9c51498f8cf0}" id="{d55b2dbc-4c3a-4c52-9c2c-16127e00a889}" partId="{a85ee6f4-0813-4e23-8430-eeedb08f3b7d}" to="{98a8ed94-50ed-43d3-85cf-7968f5e260da}"/>
<edge from="{3ccfd176-17d3-4e5e-9a66-7973b67a9b7d}" id="{d89420d6-ffca-4dd4-a5b5-2ff9b8618b01}" partId="{8eb40b25-f5d1-42b6-9906-db2f81a37f4f}" to="{6a47f6ac-e4a8-4e5d-9000-b4c0dfb999a3}"/>
<edge from="{67ed5a94-e749-46d1-8ccf-2b33a3e878d6}" id="{e99f5404-7dce-4770-aac9-22ad610b4420}" partId="{4997addf-2435-4f34-9df4-ee1c7d24a5f6}" to="{551efaf5-ae0d-4700-9e9f-acae50c9ba96}"/>
<edge from="{59f24f88-0441-418f-8d8d-4d30d4b5015b}" id="{ee50292f-d21a-4ba6-8e31-d2f9fb577398}" partId="{a85ee6f4-0813-4e23-8430-eeedb08f3b7d}" to="{f845abe2-f311-47be-8c61-8b0fa1fa98d2}"/>
<edge from="{fb6417cf-c962-41fd-ac81-20e8b427ab2e}" id="{f1d56833-ff9a-44d7-a337-76eb68549678}" partId="{8545ec00-3fec-4bb5-aeb8-79850f846ef2}" to="{71b0f00a-6909-4121-bf1d-83b0a64eab41}"/>
<edge from="{4fc24460-bb11-492b-95d5-3df726947c9a}" id="{fc919b1d-e12b-42d0-a3af-fc36c8c78ef4}" partId="{8eb40b25-f5d1-42b6-9906-db2f81a37f4f}" to="{f9dad8ee-47b7-4fb4-aa7a-846995320792}"/>
</edges>
<parts>
<part chamfered="false" color="#ffaeb0b0" countershaded="true" disabled="false" id="{0ec19fd0-d226-4ba3-8cda-0c444a398038}" locked="false" rounded="true" subdived="true" visible="true" xMirrored="false"/>
<part base="Average" chamfered="false" color="#c5aeb0b0" cutRotation="-0.44" deformUnified="true" deformWidth="0.11" disabled="false" id="{2c7dcbf9-1455-49f4-9a7c-7ddb17c2179d}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="true"/>
<part chamfered="false" color="#ffaeb0b0" disabled="false" id="{3875e962-9c33-4214-9049-34a042e653ad}" locked="false" rounded="false" subdived="false" visible="true" xMirrored="true"/>
<part chamfered="false" color="#ffaeb0b0" disabled="false" id="{57488f4d-0076-4e58-94c5-9ff29d94a77f}" locked="false" rounded="false" subdived="false" visible="true" xMirrored="true"/>
<part chamfered="false" color="#ffaeb0b0" countershaded="true" disabled="false" id="{5a4379c7-bcc8-4e50-9c3c-24390279a7b1}" locked="false" rounded="true" subdived="true" visible="true" xMirrored="false"/>
<part chamfered="false" color="#ffaeb0b0" disabled="false" id="{a5a129b6-564f-46cb-8309-cf05419aa7ed}" locked="false" rounded="false" subdived="false" visible="true" xMirrored="true"/>
<part chamfered="false" color="#ffaeb0b0" countershaded="true" disabled="false" id="{4997addf-2435-4f34-9df4-ee1c7d24a5f6}" locked="false" rounded="true" subdived="true" visible="true" xMirrored="false"/>
<part base="Average" chamfered="false" color="#c5aeb0b0" cutRotation="-0.44" deformUnified="true" deformWidth="0.11" disabled="false" id="{52c57fba-89d2-4bb0-a9e8-17d96dda6f04}" locked="false" rounded="true" subdived="false" visible="true" xMirrored="true"/>
<part chamfered="false" color="#ffaeb0b0" disabled="false" id="{8545ec00-3fec-4bb5-aeb8-79850f846ef2}" locked="false" rounded="false" subdived="false" visible="true" xMirrored="true"/>
<part chamfered="false" color="#ffaeb0b0" countershaded="true" disabled="false" id="{8eb40b25-f5d1-42b6-9906-db2f81a37f4f}" locked="false" rounded="true" subdived="true" visible="true" xMirrored="false"/>
<part chamfered="false" color="#ffaeb0b0" disabled="false" id="{a85ee6f4-0813-4e23-8430-eeedb08f3b7d}" locked="false" rounded="false" subdived="false" visible="true" xMirrored="true"/>
<part chamfered="false" color="#ffaeb0b0" disabled="false" id="{d04ab937-945f-4b67-9667-a144a29ae0e9}" locked="false" rounded="false" subdived="false" visible="true" xMirrored="true"/>
</parts>
<components>
<component combineMode="Normal" expanded="false" id="{5d2450d5-1eb2-4dfb-9fe0-4daba5f7a2cd}" linkData="{a5a129b6-564f-46cb-8309-cf05419aa7ed}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{fe2deda2-992a-4e97-af19-f9d59dbc4ded}" linkData="{57488f4d-0076-4e58-94c5-9ff29d94a77f}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{9b365770-82c8-443c-af3d-2052b5f5676a}" linkData="{3875e962-9c33-4214-9049-34a042e653ad}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{f63c9190-de38-4e5b-8d31-93e5740eb2f2}" linkData="{2c7dcbf9-1455-49f4-9a7c-7ddb17c2179d}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{221aa226-dae0-41a1-8bc4-0d68a513b8d4}" linkData="{5a4379c7-bcc8-4e50-9c3c-24390279a7b1}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{e49264f4-179b-42ec-9b3d-b022e1ab6ea2}" linkData="{0ec19fd0-d226-4ba3-8cda-0c444a398038}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{940d25d1-1fb9-40a3-a203-d7fd764d400c}" linkData="{d04ab937-945f-4b67-9667-a144a29ae0e9}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{41260d2b-c3e7-4156-9630-03595453f3f7}" linkData="{8545ec00-3fec-4bb5-aeb8-79850f846ef2}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{339ac492-a58e-4ddb-b34d-c44adc9d68be}" linkData="{a85ee6f4-0813-4e23-8430-eeedb08f3b7d}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{956d448e-721e-4a42-a687-4c85db687893}" linkData="{52c57fba-89d2-4bb0-a9e8-17d96dda6f04}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{e0ec89c9-ca82-4c27-998b-013189600f37}" linkData="{8eb40b25-f5d1-42b6-9906-db2f81a37f4f}" linkDataType="partId"/>
<component combineMode="Normal" expanded="false" id="{3e48354e-984f-431c-be46-d77146ac3d43}" linkData="{4997addf-2435-4f34-9df4-ee1c7d24a5f6}" linkDataType="partId"/>
</components>
<materials/>
<poses/>

125
shaders/scene.frag Normal file
View File

@ -0,0 +1,125 @@
#version 110
/*
* This file follow the Stackoverflow content license: CC BY-SA 4.0,
* since it's based on Prashanth N Udupa's work: https://stackoverflow.com/questions/35134270/how-to-use-qopenglframebufferobject-for-shadow-mapping
*/
struct directional_light
{
vec3 direction;
vec3 eye;
vec4 ambient;
vec4 diffuse;
vec4 specular;
};
struct material_properties
{
vec4 ambient;
vec4 diffuse;
vec4 specular;
float specularPower;
float opacity;
float brightness;
};
uniform directional_light qt_Light;
uniform material_properties qt_Material;
uniform sampler2D qt_ShadowMap;
uniform bool qt_ShadowEnabled;
varying vec4 v_Normal;
varying vec4 v_ShadowPosition;
const float c_zNear = 0.1;
const float c_ZFar = 1000.0;
const float c_zero = 0.0;
const float c_one = 1.0;
const float c_half = 0.5;
const float textureSize = 2048.0;
const vec2 texelSize = 1.0 / vec2(textureSize,textureSize);
vec4 evaluateLightMaterialColor(in vec4 normal)
{
// Start with black color
vec3 finalColor = vec3(c_zero, c_zero, c_zero);
// Upgrade black color to the base ambient color
finalColor += qt_Light.ambient.rgb * qt_Material.ambient.rgb;
// Add diffuse component to it
vec4 lightDir = vec4( normalize(qt_Light.direction), 0.0 );
float diffuseFactor = max( c_zero, dot(lightDir, normal) );
if(diffuseFactor > c_zero)
{
finalColor += qt_Light.diffuse.rgb *
qt_Material.diffuse.rgb *
diffuseFactor *
qt_Material.brightness;
}
// Add specular component to it
const vec3 blackColor = vec3(c_zero, c_zero, c_zero);
if( !(qt_Material.specular.rgb == blackColor || qt_Light.specular.rgb == blackColor || qt_Material.specularPower == c_zero) )
{
vec4 viewDir = vec4( normalize(qt_Light.eye), 0.0 );
vec4 reflectionVec = reflect(lightDir, normal);
float specularFactor = max( c_zero, dot(reflectionVec, -viewDir) );
if(specularFactor > c_zero)
{
specularFactor = pow( specularFactor, qt_Material.specularPower );
finalColor += qt_Light.specular.rgb *
qt_Material.specular.rgb *
specularFactor;
}
}
// All done!
return vec4( finalColor, qt_Material.opacity );
}
float linearizeDepth(float depth)
{
float z = depth * 2.0 - 1.0; // Back to NDC
return (2.0 * c_zNear * c_ZFar) / (c_ZFar + c_zNear - z * (c_ZFar - c_zNear));
}
float evaluateShadow(in vec4 shadowPos)
{
vec3 shadowCoords = shadowPos.xyz / shadowPos.w;
shadowCoords = shadowCoords * c_half + c_half;
if(shadowCoords.z > c_one)
return c_one;
float currentDepth = shadowPos.z;
float shadow = c_zero;
const int sampleRange = 2;
const float nrSamples = (2.0*float(sampleRange) + 1.0)*(2.0*float(sampleRange) + 1.0);
for(int x=-sampleRange; x<=sampleRange; x++)
{
for(int y=-sampleRange; y<=sampleRange; y++)
{
vec2 pcfCoords = shadowCoords.xy + vec2(x,y)*texelSize;
float pcfDepth = linearizeDepth( texture2D(qt_ShadowMap, pcfCoords).r );
shadow += (currentDepth < pcfDepth) ? c_one : c_half;
}
}
shadow /= nrSamples;
return shadow;
}
void main(void)
{
vec4 lmColor = evaluateLightMaterialColor(v_Normal);
if(qt_ShadowEnabled == true)
{
float shadow = evaluateShadow(v_ShadowPosition);
gl_FragColor = vec4(lmColor.xyz * shadow, qt_Material.opacity);
}
else
gl_FragColor = lmColor;
}

23
shaders/scene.vert Normal file
View File

@ -0,0 +1,23 @@
#version 110
/*
* This file follow the Stackoverflow content license: CC BY-SA 4.0,
* since it's based on Prashanth N Udupa's work: https://stackoverflow.com/questions/35134270/how-to-use-qopenglframebufferobject-for-shadow-mapping
*/
attribute vec4 qt_Vertex;
attribute vec4 qt_Normal;
uniform mat4 qt_NormalMatrix;
uniform mat4 qt_LightViewProjectionMatrix;
uniform mat4 qt_ModelViewProjectionMatrix;
varying vec4 v_Normal;
varying vec4 v_ShadowPosition;
void main(void)
{
v_Normal = normalize(qt_NormalMatrix * qt_Normal);
v_ShadowPosition = qt_LightViewProjectionMatrix * vec4(qt_Vertex.xyz, 1.0);
gl_Position = qt_ModelViewProjectionMatrix * qt_Vertex;
}

10
shaders/shadow.frag Normal file
View File

@ -0,0 +1,10 @@
#version 110
/*
* This file follow the Stackoverflow content license: CC BY-SA 4.0,
* since it's based on Prashanth N Udupa's work: https://stackoverflow.com/questions/35134270/how-to-use-qopenglframebufferobject-for-shadow-mapping
*/
void main(void)
{
gl_FragDepth = gl_FragCoord.z;
}

14
shaders/shadow.vert Normal file
View File

@ -0,0 +1,14 @@
#version 110
/*
* This file follow the Stackoverflow content license: CC BY-SA 4.0,
* since it's based on Prashanth N Udupa's work: https://stackoverflow.com/questions/35134270/how-to-use-qopenglframebufferobject-for-shadow-mapping
*/
attribute vec3 qt_Vertex;
uniform mat4 qt_LightViewProjectionMatrix;
const float c_one = 1.0;
void main(void)
{
gl_Position = qt_LightViewProjectionMatrix * vec4(qt_Vertex, c_one);
}

View File

@ -1,222 +0,0 @@
#include <cmath>
#include <QtMath>
#include "animalposer.h"
#include "util.h"
AnimalPoser::AnimalPoser(const std::vector<RiggerBone> &bones) :
Poser(bones)
{
}
void AnimalPoser::resolveTransform()
{
std::map<QString, std::vector<QString>> chains;
std::vector<QString> boneNames;
for (const auto &item: parameters()) {
boneNames.push_back(item.first);
}
Poser::fetchChains(boneNames, chains);
for (auto &chain: chains) {
resolveChainRotation(chain.second);
}
float mostBottomYBeforeTransform = std::numeric_limits<float>::max();
for (const auto &bone: bones()) {
if (bone.tailPosition.y() < mostBottomYBeforeTransform)
mostBottomYBeforeTransform = bone.tailPosition.y();
}
auto transformedJointNodeTree = m_jointNodeTree;
transformedJointNodeTree.recalculateTransformMatrices();
float mostBottomYAfterTransform = std::numeric_limits<float>::max();
for (int i = 0; i < (int)transformedJointNodeTree.nodes().size(); ++i) {
const auto &bone = bones()[i];
const auto &jointNode = transformedJointNodeTree.nodes()[i];
QVector3D newPosition = jointNode.transformMatrix * bone.tailPosition;
if (newPosition.y() < mostBottomYAfterTransform)
mostBottomYAfterTransform = newPosition.y();
}
float translateY = mostBottomYBeforeTransform - mostBottomYAfterTransform;
if (!qFuzzyIsNull(translateY)) {
int rootBoneIndex = findBoneIndex(Rigger::rootBoneName);
if (-1 == rootBoneIndex) {
qDebug() << "Find root bone failed:" << Rigger::rootBoneName;
return;
}
m_jointNodeTree.addTranslation(rootBoneIndex, QVector3D(0, translateY * m_yTranslationScale, 0));
}
}
std::pair<bool, QVector3D> AnimalPoser::findQVector3DFromMap(const std::map<QString, QString> &map, const QString &xName, const QString &yName, const QString &zName)
{
auto findXResult = map.find(xName);
auto findYResult = map.find(yName);
auto findZResult = map.find(zName);
if (findXResult == map.end() &&
findYResult == map.end() &&
findZResult == map.end()) {
return {false, QVector3D()};
}
return {true, {
valueOfKeyInMapOrEmpty(map, xName).toFloat(),
valueOfKeyInMapOrEmpty(map, yName).toFloat(),
valueOfKeyInMapOrEmpty(map, zName).toFloat()
}};
}
std::pair<bool, std::pair<QVector3D, QVector3D>> AnimalPoser::findBonePositionsFromParameters(const std::map<QString, QString> &map)
{
auto findBoneStartResult = findQVector3DFromMap(map, "fromX", "fromY", "fromZ");
auto findBoneStopResult = findQVector3DFromMap(map, "toX", "toY", "toZ");
if (!findBoneStartResult.first || !findBoneStopResult.first)
return {false, {QVector3D(), QVector3D()}};
return {true, {findBoneStartResult.second, findBoneStopResult.second}};
}
void AnimalPoser::resolveChainRotation(const std::vector<QString> &limbBoneNames)
{
std::vector<QQuaternion> rotationsForEndEffector;
size_t endEffectorStart = 0;
// We match the poses by the distance and rotation plane
if (limbBoneNames.size() >= 2) {
endEffectorStart = 2;
const auto &beginBoneName = limbBoneNames[0];
const auto &middleBoneName = limbBoneNames[1];
const auto &beginBoneParameters = parameters().find(beginBoneName);
if (beginBoneParameters == parameters().end()) {
qDebug() << beginBoneName << "'s parameters not found";
return;
}
auto matchBeginBonePositions = findBonePositionsFromParameters(beginBoneParameters->second);
if (!matchBeginBonePositions.first) {
qDebug() << beginBoneName << "'s positions not found";
return;
}
const auto &middleBoneParameters = parameters().find(middleBoneName);
if (middleBoneParameters == parameters().end()) {
qDebug() << middleBoneName << "'s parameters not found";
return;
}
auto matchMiddleBonePositions = findBonePositionsFromParameters(middleBoneParameters->second);
if (!matchMiddleBonePositions.first) {
qDebug() << middleBoneName << "'s positions not found";
return;
}
float matchBeginBoneLength = (matchBeginBonePositions.second.first - matchBeginBonePositions.second.second).length();
float matchMiddleBoneLength = (matchMiddleBonePositions.second.first - matchMiddleBonePositions.second.second).length();
float matchLimbLength = matchBeginBoneLength + matchMiddleBoneLength;
auto matchDistanceBetweenBeginAndEndBones = (matchBeginBonePositions.second.first - matchMiddleBonePositions.second.second).length();
auto matchRotatePlaneNormal = QVector3D::crossProduct((matchBeginBonePositions.second.second - matchBeginBonePositions.second.first).normalized(), (matchMiddleBonePositions.second.second - matchBeginBonePositions.second.second).normalized());
auto matchDirectionBetweenBeginAndEndPones = (matchMiddleBonePositions.second.second - matchBeginBonePositions.second.first).normalized();
int beginBoneIndex = findBoneIndex(beginBoneName);
if (-1 == beginBoneIndex) {
qDebug() << beginBoneName << "not found in rigged bones";
return;
}
const auto &beginBone = bones()[beginBoneIndex];
int middleBoneIndex = findBoneIndex(middleBoneName);
if (-1 == middleBoneIndex) {
qDebug() << middleBoneName << "not found in rigged bones";
return;
}
const auto &middleBone = bones()[middleBoneIndex];
float targetBeginBoneLength = (beginBone.headPosition - beginBone.tailPosition).length();
float targetMiddleBoneLength = (middleBone.headPosition - middleBone.tailPosition).length();
float targetLimbLength = targetBeginBoneLength + targetMiddleBoneLength;
float targetDistanceBetweenBeginAndEndBones = matchDistanceBetweenBeginAndEndBones * (targetLimbLength / matchLimbLength);
QVector3D targetEndBoneStartPosition = beginBone.headPosition + matchDirectionBetweenBeginAndEndPones * targetDistanceBetweenBeginAndEndBones;
float angleBetweenDistanceAndMiddleBones = 0;
{
const float &a = targetMiddleBoneLength;
const float &b = targetDistanceBetweenBeginAndEndBones;
const float &c = targetBeginBoneLength;
double cosC = (a*a + b*b - c*c) / (2.0*a*b);
angleBetweenDistanceAndMiddleBones = qRadiansToDegrees(acos(cosC));
if (std::isnan(angleBetweenDistanceAndMiddleBones) || std::isinf(angleBetweenDistanceAndMiddleBones))
angleBetweenDistanceAndMiddleBones = 0;
}
QVector3D targetMiddleBoneStartPosition;
{
//qDebug() << beginBoneName << "Angle:" << angleBetweenDistanceAndMiddleBones << "Distance:" << targetDistanceBetweenBeginAndEndBones;
auto rotation = QQuaternion::fromAxisAndAngle(matchRotatePlaneNormal, angleBetweenDistanceAndMiddleBones);
targetMiddleBoneStartPosition = targetEndBoneStartPosition + rotation.rotatedVector(-matchDirectionBetweenBeginAndEndPones).normalized() * targetMiddleBoneLength;
}
// Now the bones' positions have been resolved, we calculate the rotation
auto oldBeginBoneDirection = (beginBone.tailPosition - beginBone.headPosition).normalized();
auto newBeginBoneDirection = (targetMiddleBoneStartPosition - beginBone.headPosition).normalized();
//qDebug() << beginBoneName << "oldBeginBoneDirection:" << oldBeginBoneDirection << "newBeginBoneDirection:" << newBeginBoneDirection;
auto beginBoneRotation = QQuaternion::rotationTo(oldBeginBoneDirection, newBeginBoneDirection);
m_jointNodeTree.updateRotation(beginBoneIndex, beginBoneRotation);
auto oldMiddleBoneDirection = (middleBone.tailPosition - middleBone.headPosition).normalized();
auto newMiddleBoneDirection = (targetEndBoneStartPosition - targetMiddleBoneStartPosition).normalized();
//qDebug() << beginBoneName << "oldMiddleBoneDirection:" << oldMiddleBoneDirection << "newMiddleBoneDirection:" << newMiddleBoneDirection;
oldMiddleBoneDirection = beginBoneRotation.rotatedVector(oldMiddleBoneDirection);
//qDebug() << beginBoneName << "oldMiddleBoneDirection:" << oldMiddleBoneDirection << "after rotation";
auto middleBoneRotation = QQuaternion::rotationTo(oldMiddleBoneDirection, newMiddleBoneDirection);
m_jointNodeTree.updateRotation(middleBoneIndex, middleBoneRotation);
rotationsForEndEffector.push_back(beginBoneRotation);
rotationsForEndEffector.push_back(middleBoneRotation);
}
// Calculate the end effectors' rotation
if (limbBoneNames.size() > endEffectorStart) {
for (size_t i = endEffectorStart; i < limbBoneNames.size(); ++i) {
const auto &boneName = limbBoneNames[i];
int boneIndex = findBoneIndex(boneName);
if (-1 == boneIndex) {
qDebug() << "Find bone failed:" << boneName;
continue;
}
const auto &bone = bones()[boneIndex];
const auto &boneParameters = parameters().find(boneName);
if (boneParameters == parameters().end()) {
qDebug() << "Find bone parameters:" << boneName;
continue;
}
auto matchBonePositions = findBonePositionsFromParameters(boneParameters->second);
if (!matchBonePositions.first) {
qDebug() << "Find bone positions failed:" << boneName;
continue;
}
auto matchBoneDirection = (matchBonePositions.second.second - matchBonePositions.second.first).normalized();
auto oldBoneDirection = (bone.tailPosition - bone.headPosition).normalized();
auto newBoneDirection = matchBoneDirection;
for (const auto &rotation: rotationsForEndEffector) {
oldBoneDirection = rotation.rotatedVector(oldBoneDirection);
}
auto boneRotation = QQuaternion::rotationTo(oldBoneDirection, newBoneDirection);
m_jointNodeTree.updateRotation(boneIndex, boneRotation);
rotationsForEndEffector.push_back(boneRotation);
}
}
}
void AnimalPoser::commit()
{
resolveTransform();
Poser::commit();
}

View File

@ -1,20 +0,0 @@
#ifndef DUST3D_ANIMAL_POSER_H
#define DUST3D_ANIMAL_POSER_H
#include <vector>
#include "poser.h"
class AnimalPoser : public Poser
{
Q_OBJECT
public:
AnimalPoser(const std::vector<RiggerBone> &bones);
void commit() override;
private:
void resolveTransform();
void resolveChainRotation(const std::vector<QString> &limbBoneNames);
std::pair<bool, QVector3D> findQVector3DFromMap(const std::map<QString, QString> &map, const QString &xName, const QString &yName, const QString &zName);
std::pair<bool, std::pair<QVector3D, QVector3D>> findBonePositionsFromParameters(const std::map<QString, QString> &map);
};
#endif

View File

@ -1,75 +0,0 @@
#include "animationclipplayer.h"
AnimationClipPlayer::~AnimationClipPlayer()
{
clear();
}
void AnimationClipPlayer::setSpeedMode(SpeedMode speedMode)
{
m_speedMode = speedMode;
}
void AnimationClipPlayer::updateFrameMeshes(std::vector<std::pair<float, Model *>> &frameMeshes)
{
clear();
m_frameMeshes = frameMeshes;
frameMeshes.clear();
m_currentPlayIndex = 0;
m_countForFrame.restart();
if (!m_frameMeshes.empty())
m_timerForFrame.singleShot(0, this, &AnimationClipPlayer::frameReadyToShow);
}
void AnimationClipPlayer::clear()
{
freeFrames();
delete m_lastFrameMesh;
m_lastFrameMesh = nullptr;
}
void AnimationClipPlayer::freeFrames()
{
for (auto &it: m_frameMeshes) {
delete it.second;
}
m_frameMeshes.clear();
}
int AnimationClipPlayer::getFrameDurationMillis(int frame)
{
int millis = m_frameMeshes[frame].first * 1000;
if (SpeedMode::Slow == m_speedMode) {
millis *= 2;
} else if (SpeedMode::Fast == m_speedMode) {
millis /= 2;
}
return millis;
}
Model *AnimationClipPlayer::takeFrameMesh()
{
if (m_currentPlayIndex >= (int)m_frameMeshes.size()) {
if (nullptr != m_lastFrameMesh)
return new Model(*m_lastFrameMesh);
return nullptr;
}
int millis = getFrameDurationMillis(m_currentPlayIndex) - m_countForFrame.elapsed();
if (millis > 0) {
m_timerForFrame.singleShot(millis, this, &AnimationClipPlayer::frameReadyToShow);
if (nullptr != m_lastFrameMesh)
return new Model(*m_lastFrameMesh);
return nullptr;
}
m_currentPlayIndex = (m_currentPlayIndex + 1) % m_frameMeshes.size();
m_countForFrame.restart();
Model *mesh = new Model(*m_frameMeshes[m_currentPlayIndex].second);
m_timerForFrame.singleShot(getFrameDurationMillis(m_currentPlayIndex), this, &AnimationClipPlayer::frameReadyToShow);
delete m_lastFrameMesh;
m_lastFrameMesh = new Model(*mesh);
return mesh;
}

View File

@ -1,43 +0,0 @@
#ifndef DUST3D_ANIMATION_PLAYER_H
#define DUST3D_ANIMATION_PLAYER_H
#include <QObject>
#include <QTimer>
#include <QTime>
#include "model.h"
class AnimationClipPlayer : public QObject
{
Q_OBJECT
signals:
void frameReadyToShow();
public:
enum class SpeedMode
{
Slow,
Normal,
Fast
};
~AnimationClipPlayer();
Model *takeFrameMesh();
void updateFrameMeshes(std::vector<std::pair<float, Model *>> &frameMeshes);
void clear();
public slots:
void setSpeedMode(SpeedMode speedMode);
private:
void freeFrames();
int getFrameDurationMillis(int frame);
Model *m_lastFrameMesh = nullptr;
int m_currentPlayIndex = 0;
std::vector<std::pair<float, Model *>> m_frameMeshes;
QTime m_countForFrame;
QTimer m_timerForFrame;
SpeedMode m_speedMode = SpeedMode::Normal;
};
#endif

86
src/blockmesh.cpp Normal file
View File

@ -0,0 +1,86 @@
#include "blockmesh.h"
std::vector<size_t> BlockMesh::buildFace(const QVector3D &origin,
const QVector3D &faceNormal,
const QVector3D &startDirection,
double radius)
{
std::vector<size_t> face;
face.push_back(m_resultVertices->size() + 0);
face.push_back(m_resultVertices->size() + 1);
face.push_back(m_resultVertices->size() + 2);
face.push_back(m_resultVertices->size() + 3);
auto upDirection = QVector3D::crossProduct(startDirection, faceNormal);
m_resultVertices->push_back(origin + startDirection * radius);
m_resultVertices->push_back(origin - upDirection * radius);
m_resultVertices->push_back(origin - startDirection * radius);
m_resultVertices->push_back(origin + upDirection * radius);
return face;
}
QVector3D BlockMesh::calculateStartDirection(const QVector3D &direction)
{
const std::vector<QVector3D> axisList = {
QVector3D {1, 0, 0},
QVector3D {0, 1, 0},
QVector3D {0, 0, 1},
};
float maxDot = -1;
size_t nearAxisIndex = 0;
bool reversed = false;
for (size_t i = 0; i < axisList.size(); ++i) {
const auto axis = axisList[i];
auto dot = QVector3D::dotProduct(axis, direction);
auto positiveDot = std::abs(dot);
if (positiveDot >= maxDot) {
reversed = dot < 0;
maxDot = positiveDot;
nearAxisIndex = i;
}
}
const auto& choosenAxis = axisList[(nearAxisIndex + 1) % 3];
auto startDirection = QVector3D::crossProduct(direction, choosenAxis).normalized();
return reversed ? -startDirection : startDirection;
}
void BlockMesh::buildBlock(const Block &block)
{
QVector3D fromFaceNormal = (block.toPosition - block.fromPosition).normalized();
QVector3D startDirection = calculateStartDirection(-fromFaceNormal);
std::vector<size_t> fromFaces = buildFace(block.fromPosition,
-fromFaceNormal, startDirection, block.fromRadius);
std::vector<size_t> toFaces = buildFace(block.toPosition,
-fromFaceNormal, startDirection, block.toRadius);
m_resultQuads->push_back(fromFaces);
for (size_t i = 0; i < fromFaces.size(); ++i) {
size_t j = (i + 1) % fromFaces.size();
m_resultQuads->push_back({fromFaces[j], fromFaces[i], toFaces[i], toFaces[j]});
}
std::reverse(toFaces.begin(), toFaces.end());
m_resultQuads->push_back(toFaces);
}
void BlockMesh::build()
{
delete m_resultVertices;
m_resultVertices = new std::vector<QVector3D>;
delete m_resultQuads;
m_resultQuads = new std::vector<std::vector<size_t>>;
for (const auto &block: m_blocks)
buildBlock(block);
delete m_resultFaces;
m_resultFaces = new std::vector<std::vector<size_t>>;
m_resultFaces->reserve(m_resultQuads->size() * 2);
for (const auto &quad: *m_resultQuads) {
m_resultFaces->push_back({quad[0], quad[1], quad[2]});
m_resultFaces->push_back({quad[2], quad[3], quad[0]});
}
}

69
src/blockmesh.h Normal file
View File

@ -0,0 +1,69 @@
#ifndef DUST3D_BLOCK_MESH_H
#define DUST3D_BLOCK_MESH_H
#include <QVector3D>
#include <vector>
class BlockMesh
{
public:
struct Block
{
QVector3D fromPosition;
double fromRadius;
QVector3D toPosition;
double toRadius;
};
BlockMesh()
{
}
~BlockMesh()
{
delete m_resultVertices;
delete m_resultQuads;
delete m_resultFaces;
}
std::vector<QVector3D> *takeResultVertices()
{
std::vector<QVector3D> *resultVertices = m_resultVertices;
m_resultVertices = nullptr;
return resultVertices;
}
std::vector<std::vector<size_t>> *takeResultFaces()
{
std::vector<std::vector<size_t>> *resultFaces = m_resultFaces;
m_resultFaces = nullptr;
return resultFaces;
}
void addBlock(const QVector3D &fromPosition, double fromRadius, const QVector3D &toPosition, double toRadius)
{
Block block;
block.fromPosition = fromPosition;
block.fromRadius = fromRadius;
block.toPosition = toPosition;
block.toRadius = toRadius;
m_blocks.push_back(block);
}
void build();
private:
std::vector<QVector3D> *m_resultVertices = nullptr;
std::vector<std::vector<size_t>> *m_resultFaces = nullptr;
std::vector<std::vector<size_t>> *m_resultQuads = nullptr;
std::vector<Block> m_blocks;
void buildBlock(const Block &block);
std::vector<size_t> buildFace(const QVector3D &origin,
const QVector3D &faceNormal,
const QVector3D &startDirection,
double radius);
QVector3D calculateStartDirection(const QVector3D &direction);
};
#endif

View File

@ -2,36 +2,48 @@
#include <QMatrix4x4>
#include <QDebug>
#include <cmath>
#include <QtMath>
#include "ccdikresolver.h"
#include "util.h"
CCDIKSolver::CCDIKSolver() :
m_maxRound(4),
m_distanceThreshold2(0.001 * 0.001),
m_distanceCeaseThreshold2(0.001 * 0.001)
CcdIkSolver::CcdIkSolver()
{
}
void CCDIKSolver::setMaxRound(int maxRound)
void CcdIkSolver::setSolveFrom(int fromNodeIndex)
{
m_fromNodeIndex = fromNodeIndex;
}
void CcdIkSolver::setNodeHingeConstraint(int nodeIndex,
const QVector3D &axis, double minLimitDegrees, double maxLimitDegrees)
{
auto &node = m_nodes[nodeIndex];
node.axis = axis;
node.minLimitDegrees = minLimitDegrees;
node.maxLimitDegrees = maxLimitDegrees;
}
void CcdIkSolver::setMaxRound(int maxRound)
{
m_maxRound = maxRound;
}
void CCDIKSolver::setDistanceThreshod(float threshold)
void CcdIkSolver::setDistanceThreshod(float threshold)
{
m_distanceThreshold2 = threshold * threshold;
}
int CCDIKSolver::addNodeInOrder(const QVector3D &position)
int CcdIkSolver::addNodeInOrder(const QVector3D &position)
{
CCDIKNode node;
CcdIkNode node;
node.position = position;
int nodeCount = m_nodes.size();
m_nodes.push_back(node);
return nodeCount;
}
void CCDIKSolver::solveTo(const QVector3D &position)
void CcdIkSolver::solveTo(const QVector3D &position)
{
//qDebug() << "solveTo:" << position;
m_destination = position;
@ -49,29 +61,61 @@ void CCDIKSolver::solveTo(const QVector3D &position)
}
}
const QVector3D &CCDIKSolver::getNodeSolvedPosition(int index)
const QVector3D &CcdIkSolver::getNodeSolvedPosition(int index)
{
Q_ASSERT(index >= 0 && index < (int)m_nodes.size());
return m_nodes[index].position;
}
int CCDIKSolver::getNodeCount(void)
int CcdIkSolver::getNodeCount()
{
return m_nodes.size();
return (int)m_nodes.size();
}
void CCDIKSolver::iterate()
void CcdIkSolver::iterate()
{
for (int i = m_nodes.size() - 2; i >= 0; i--) {
auto rotateChildren = [&](const QQuaternion &quaternion, int i) {
const auto &origin = m_nodes[i];
const auto &endEffector = m_nodes[m_nodes.size() - 1];
QVector3D from = (endEffector.position - origin.position).normalized();
QVector3D to = (m_destination - origin.position).normalized();
auto quaternion = QQuaternion::rotationTo(from, to);
for (size_t j = i + 1; j <= m_nodes.size() - 1; j++) {
auto &next = m_nodes[j];
const auto offset = next.position - origin.position;
next.position = origin.position + quaternion.rotatedVector(offset);
}
};
for (int i = m_nodes.size() - 2; i >= m_fromNodeIndex; i--) {
const auto &origin = m_nodes[i];
const auto &endEffector = m_nodes[m_nodes.size() - 1];
QVector3D from = (endEffector.position - origin.position).normalized();
QVector3D to = (m_destination - origin.position).normalized();
auto quaternion = QQuaternion::rotationTo(from, to);
rotateChildren(quaternion, i);
if (origin.axis.isNull())
continue;
QVector3D oldAxis = origin.axis;
QVector3D newAxis = quaternion.rotatedVector(oldAxis);
auto hingQuaternion = QQuaternion::rotationTo(newAxis, oldAxis);
rotateChildren(hingQuaternion, i);
// TODO: Support angle limit for other axis
int parentIndex = i - 1;
if (parentIndex < 0)
continue;
int childIndex = i + 1;
if (childIndex >= m_nodes.size())
continue;
const auto &parent = m_nodes[parentIndex];
const auto &child = m_nodes[childIndex];
QVector3D angleFrom = (QVector3D(0.0, parent.position.y(), parent.position.z()) -
QVector3D(0.0, origin.position.y(), origin.position.z())).normalized();
QVector3D angleTo = (QVector3D(0.0, child.position.y(), child.position.z()) -
QVector3D(0.0, origin.position.y(), origin.position.z())).normalized();
float degrees = angleInRangle360BetweenTwoVectors(angleFrom, angleTo, QVector3D(1.0, 0.0, 0.0));
if (degrees < origin.minLimitDegrees) {
auto quaternion = QQuaternion::fromAxisAndAngle(QVector3D(1.0, 0.0, 0.0), origin.minLimitDegrees - degrees);
rotateChildren(quaternion, i);
} else if (degrees > origin.maxLimitDegrees) {
auto quaternion = QQuaternion::fromAxisAndAngle(QVector3D(-1.0, 0.0, 0.0), degrees - origin.maxLimitDegrees);
rotateChildren(quaternion, i);
}
}
}

View File

@ -4,29 +4,36 @@
#include <QVector3D>
#include <QQuaternion>
struct CCDIKNode
struct CcdIkNode
{
QVector3D position;
QVector3D axis;
double minLimitDegrees;
double maxLimitDegrees;
};
class CCDIKSolver
class CcdIkSolver
{
public:
CCDIKSolver();
CcdIkSolver();
void setMaxRound(int maxRound);
void setDistanceThreshod(float threshold);
int addNodeInOrder(const QVector3D &position);
void solveTo(const QVector3D &position);
const QVector3D &getNodeSolvedPosition(int index);
int getNodeCount(void);
int getNodeCount();
void setNodeHingeConstraint(int nodeIndex,
const QVector3D &axis, double minLimitDegrees, double maxLimitDegrees);
void setSolveFrom(int fromNodeIndex);
private:
void iterate();
private:
std::vector<CCDIKNode> m_nodes;
std::vector<CcdIkNode> m_nodes;
QVector3D m_destination;
int m_maxRound;
float m_distanceThreshold2;
float m_distanceCeaseThreshold2;
int m_maxRound = 4;
float m_distanceThreshold2 = 0.001 * 0.001;
float m_distanceCeaseThreshold2 = 0.001 * 0.001;
int m_fromNodeIndex = 0;
};
#endif

117
src/chainsimulator.cpp Normal file
View File

@ -0,0 +1,117 @@
#include <set>
#include <unordered_set>
#include <unordered_map>
#include <QDebug>
#include "chainsimulator.h"
void ChainSimulator::prepareChains()
{
for (size_t i = 1; i < m_vertices->size(); ++i) {
size_t h = i - 1;
m_chains.push_back({h, i,
((*m_vertices)[h] - (*m_vertices)[i]).length()});
}
}
void ChainSimulator::initializeVertexMotions()
{
for (size_t i = 0; i < m_vertices->size(); ++i)
m_vertexMotions[i].position = m_vertexMotions[i].lastPosition = (*m_vertices)[i];
}
void ChainSimulator::outputChainsForDebug(const char *filename, const std::vector<Chain> &springs)
{
FILE *fp = fopen(filename, "wb");
for (const auto &it: *m_vertices) {
fprintf(fp, "v %f %f %f\n", it[0], it[1], it[2]);
}
for (const auto &it: springs) {
fprintf(fp, "l %zu %zu\n", it.from + 1, it.to + 1);
}
fclose(fp);
}
void ChainSimulator::start()
{
initializeVertexMotions();
prepareChains();
outputChainsForDebug("debug-chains.obj", m_chains);
}
const ChainSimulator::VertexMotion &ChainSimulator::getVertexMotion(size_t vertexIndex)
{
return m_vertexMotions[vertexIndex];
}
void ChainSimulator::updateVertexForces()
{
for (auto &it: m_vertexMotions)
it.second.force = QVector3D();
QVector3D combinedForce = QVector3D(0.0, -9.80665, 0.0) + m_externalForce;
for (auto &it: m_vertexMotions) {
it.second.force += combinedForce;
}
}
void ChainSimulator::doVerletIntegration(double stepSize)
{
for (auto &it: m_vertexMotions) {
if (it.second.fixed)
continue;
QVector3D &x = it.second.position;
QVector3D temp = x;
QVector3D &oldX = it.second.lastPosition;
QVector3D a = it.second.force / m_parameters.particleMass;
x += x - oldX + a * stepSize * stepSize;
oldX = temp;
}
}
void ChainSimulator::applyBoundingConstraints(QVector3D *position)
{
if (position->y() < m_groundY)
position->setY(m_groundY);
}
void ChainSimulator::applyConstraints()
{
for (size_t iteration = 0; iteration < m_parameters.iterations; ++iteration) {
for (auto &it: m_chains) {
auto &from = m_vertexMotions[it.from];
auto &to = m_vertexMotions[it.to];
auto delta = from.position - to.position;
auto deltaLength = delta.length();
if (qFuzzyIsNull(deltaLength))
continue;
auto diff = (it.restLength - deltaLength) / deltaLength;
auto offset = delta * 0.5 * diff;
if (!from.fixed) {
from.position += offset;
applyBoundingConstraints(&from.position);
}
if (!to.fixed) {
to.position += -offset;
applyBoundingConstraints(&to.position);
}
}
}
}
void ChainSimulator::updateVertexPosition(size_t vertexIndex, const QVector3D &position)
{
m_vertexMotions[vertexIndex].position = position;
}
void ChainSimulator::fixVertexPosition(size_t vertexIndex)
{
m_vertexMotions[vertexIndex].fixed = true;
}
void ChainSimulator::simulate(double stepSize)
{
updateVertexForces();
doVerletIntegration(stepSize);
applyConstraints();
}

70
src/chainsimulator.h Normal file
View File

@ -0,0 +1,70 @@
#ifndef DUST3D_CHAIN_SIMULATOR_H
#define DUST3D_CHAIN_SIMULATOR_H
#include <vector>
#include <QVector3D>
#include <unordered_map>
class ChainSimulator
{
public:
struct Parameters
{
double particleMass = 1;
size_t iterations = 2;
};
struct Chain
{
size_t from;
size_t to;
double restLength;
};
struct VertexMotion
{
QVector3D position;
QVector3D lastPosition;
QVector3D force;
bool fixed = false;
};
ChainSimulator(const std::vector<QVector3D> *vertices) :
m_vertices(vertices)
{
}
void setExternalForce(const QVector3D &externalForce)
{
m_externalForce = externalForce;
}
void setGroundY(double groundY)
{
m_groundY = groundY;
}
void start();
void simulate(double stepSize);
const VertexMotion &getVertexMotion(size_t vertexIndex);
void updateVertexPosition(size_t vertexIndex, const QVector3D &position);
void fixVertexPosition(size_t vertexIndex);
private:
Parameters m_parameters;
std::vector<Chain> m_chains;
const std::vector<QVector3D> *m_vertices = nullptr;
std::unordered_map<size_t, VertexMotion> m_vertexMotions;
QVector3D m_externalForce;
double m_groundY = 0.0;
void initializeVertexMotions();
void prepareChains();
void updateVertexForces();
void applyConstraints();
void doVerletIntegration(double stepSize);
void applyBoundingConstraints(QVector3D *position);
void outputChainsForDebug(const char *filename, const std::vector<Chain> &springs);
};
#endif

View File

@ -67,7 +67,6 @@ Document::Document() :
m_resultRigWeights(nullptr),
m_isRigObsolete(false),
m_riggedOutcome(new Outcome),
m_posePreviewsGenerator(nullptr),
m_currentRigSucceed(false),
m_materialPreviewsGenerator(nullptr),
m_motionsGenerator(nullptr),
@ -470,38 +469,16 @@ QUuid Document::createNode(QUuid nodeId, float x, float y, float z, float radius
return node.id;
}
void Document::addPose(QUuid poseId, QString name, std::vector<std::pair<std::map<QString, QString>, std::map<QString, std::map<QString, QString>>>> frames, QUuid turnaroundImageId, float yTranslationScale)
{
QUuid newPoseId = poseId;
auto &pose = poseMap[newPoseId];
pose.id = newPoseId;
pose.name = name;
pose.frames = frames;
pose.turnaroundImageId = turnaroundImageId;
pose.yTranslationScale = yTranslationScale;
pose.dirty = true;
poseIdList.push_back(newPoseId);
emit posesChanged();
emit poseAdded(newPoseId);
emit poseListChanged();
emit optionsChanged();
}
void Document::addMotion(QUuid motionId, QString name, std::vector<MotionClip> clips)
void Document::addMotion(QUuid motionId, QString name, std::map<QString, QString> parameters)
{
QUuid newMotionId = motionId;
auto &motion = motionMap[newMotionId];
motion.id = newMotionId;
motion.name = name;
motion.clips = clips;
motion.parameters = parameters;
motion.dirty = true;
motionIdList.push_back(newMotionId);
emit motionsChanged();
emit motionAdded(newMotionId);
emit motionListChanged();
@ -515,7 +492,6 @@ void Document::removeMotion(QUuid motionId)
qDebug() << "Remove a none exist motion:" << motionId;
return;
}
motionIdList.erase(std::remove(motionIdList.begin(), motionIdList.end(), motionId), motionIdList.end());
motionMap.erase(findMotionResult);
emit motionsChanged();
emit motionListChanged();
@ -523,17 +499,17 @@ void Document::removeMotion(QUuid motionId)
emit optionsChanged();
}
void Document::setMotionClips(QUuid motionId, std::vector<MotionClip> clips)
void Document::setMotionParameters(QUuid motionId, std::map<QString, QString> parameters)
{
auto findMotionResult = motionMap.find(motionId);
if (findMotionResult == motionMap.end()) {
qDebug() << "Find motion failed:" << motionId;
return;
}
findMotionResult->second.clips = clips;
findMotionResult->second.parameters = parameters;
findMotionResult->second.dirty = true;
emit motionsChanged();
emit motionClipsChanged(motionId);
emit motionParametersChanged(motionId);
emit optionsChanged();
}
@ -553,92 +529,6 @@ void Document::renameMotion(QUuid motionId, QString name)
emit optionsChanged();
}
void Document::removePose(QUuid poseId)
{
auto findPoseResult = poseMap.find(poseId);
if (findPoseResult == poseMap.end()) {
qDebug() << "Remove a none exist pose:" << poseId;
return;
}
poseIdList.erase(std::remove(poseIdList.begin(), poseIdList.end(), poseId), poseIdList.end());
poseMap.erase(findPoseResult);
emit posesChanged();
emit poseListChanged();
emit poseRemoved(poseId);
emit optionsChanged();
}
void Document::setPoseFrames(QUuid poseId, std::vector<std::pair<std::map<QString, QString>, std::map<QString, std::map<QString, QString>>>> frames)
{
auto findPoseResult = poseMap.find(poseId);
if (findPoseResult == poseMap.end()) {
qDebug() << "Find pose failed:" << poseId;
return;
}
findPoseResult->second.frames = frames;
findPoseResult->second.dirty = true;
bool foundMotion = false;
for (auto &it: motionMap) {
for (const auto &clip: it.second.clips) {
if (poseId == clip.linkToId) {
it.second.dirty = true;
foundMotion = true;
break;
}
}
}
emit posesChanged();
emit poseFramesChanged(poseId);
emit optionsChanged();
if (foundMotion) {
emit motionsChanged();
}
}
void Document::setPoseTurnaroundImageId(QUuid poseId, QUuid imageId)
{
auto findPoseResult = poseMap.find(poseId);
if (findPoseResult == poseMap.end()) {
qDebug() << "Find pose failed:" << poseId;
return;
}
if (findPoseResult->second.turnaroundImageId == imageId)
return;
findPoseResult->second.turnaroundImageId = imageId;
findPoseResult->second.dirty = true;
emit poseTurnaroundImageIdChanged(poseId);
emit optionsChanged();
}
void Document::setPoseYtranslationScale(QUuid poseId, float scale)
{
auto findPoseResult = poseMap.find(poseId);
if (findPoseResult == poseMap.end()) {
qDebug() << "Find pose failed:" << poseId;
return;
}
findPoseResult->second.yTranslationScale = scale;
findPoseResult->second.dirty = true;
emit poseYtranslationScaleChanged(poseId);
emit optionsChanged();
}
void Document::renamePose(QUuid poseId, QString name)
{
auto findPoseResult = poseMap.find(poseId);
if (findPoseResult == poseMap.end()) {
qDebug() << "Find pose failed:" << poseId;
return;
}
if (findPoseResult->second.name == name)
return;
findPoseResult->second.name = name;
emit poseNameChanged(poseId);
emit poseListChanged();
emit optionsChanged();
}
bool Document::originSettled() const
{
return !qFuzzyIsNull(getOriginX()) && !qFuzzyIsNull(getOriginY()) && !qFuzzyIsNull(getOriginZ());
@ -786,14 +676,6 @@ const Component *Document::findComponent(QUuid componentId) const
return &it->second;
}
const Pose *Document::findPose(QUuid poseId) const
{
auto it = poseMap.find(poseId);
if (it == poseMap.end())
return nullptr;
return &it->second;
}
const Material *Document::findMaterial(QUuid materialId) const
{
auto it = materialMap.find(materialId);
@ -1143,7 +1025,6 @@ void Document::markAllDirty()
void Document::toSnapshot(Snapshot *snapshot, const std::set<QUuid> &limitNodeIds,
DocumentToSnapshotFor forWhat,
const std::set<QUuid> &limitPoseIds,
const std::set<QUuid> &limitMotionIds,
const std::set<QUuid> &limitMaterialIds) const
{
@ -1359,52 +1240,16 @@ void Document::toSnapshot(Snapshot *snapshot, const std::set<QUuid> &limitNodeId
snapshot->materials.push_back(std::make_pair(material, layers));
}
}
if (DocumentToSnapshotFor::Document == forWhat ||
DocumentToSnapshotFor::Poses == forWhat) {
for (const auto &poseId: poseIdList) {
if (!limitPoseIds.empty() && limitPoseIds.find(poseId) == limitPoseIds.end())
continue;
auto findPoseResult = poseMap.find(poseId);
if (findPoseResult == poseMap.end()) {
qDebug() << "Find pose failed:" << poseId;
continue;
}
auto &poseIt = *findPoseResult;
std::map<QString, QString> pose;
pose["id"] = poseIt.second.id.toString();
if (!poseIt.second.name.isEmpty())
pose["name"] = poseIt.second.name;
if (!poseIt.second.turnaroundImageId.isNull())
pose["canvasImageId"] = poseIt.second.turnaroundImageId.toString();
if (poseIt.second.yTranslationScaleAdjusted())
pose["yTranslationScale"] = QString::number(poseIt.second.yTranslationScale);
snapshot->poses.push_back(std::make_pair(pose, poseIt.second.frames));
}
}
if (DocumentToSnapshotFor::Document == forWhat ||
DocumentToSnapshotFor::Motions == forWhat) {
for (const auto &motionId: motionIdList) {
if (!limitMotionIds.empty() && limitMotionIds.find(motionId) == limitMotionIds.end())
for (const auto &motionIt: motionMap) {
if (!limitMotionIds.empty() && limitMotionIds.find(motionIt.first) == limitMotionIds.end())
continue;
auto findMotionResult = motionMap.find(motionId);
if (findMotionResult == motionMap.end()) {
qDebug() << "Find motion failed:" << motionId;
continue;
}
auto &motionIt = *findMotionResult;
std::map<QString, QString> motion;
std::map<QString, QString> motion = motionIt.second.parameters;
motion["id"] = motionIt.second.id.toString();
if (!motionIt.second.name.isEmpty())
motion["name"] = motionIt.second.name;
std::vector<std::map<QString, QString>> clips;
for (const auto &clip: motionIt.second.clips) {
std::map<QString, QString> attributes;
attributes["duration"] = QString::number(clip.duration);
attributes["linkDataType"] = clip.linkDataType();
attributes["linkData"] = clip.linkData();
clips.push_back(attributes);
}
snapshot->motions.push_back(std::make_pair(motion, clips));
snapshot->motions[motion["id"]] = motion;
}
}
if (DocumentToSnapshotFor::Document == forWhat) {
@ -1815,45 +1660,14 @@ void Document::addFromSnapshot(const Snapshot &snapshot, enum SnapshotSource sou
componentMap[childComponentId].parentId = componentId;
}
}
for (const auto &poseIt: snapshot.poses) {
QUuid newPoseId = QUuid::createUuid();
auto &newPose = poseMap[newPoseId];
newPose.id = newPoseId;
const auto &poseAttributes = poseIt.first;
newPose.name = valueOfKeyInMapOrEmpty(poseAttributes, "name");
auto findCanvasImageId = poseAttributes.find("canvasImageId");
if (findCanvasImageId != poseAttributes.end())
newPose.turnaroundImageId = QUuid(findCanvasImageId->second);
auto findYtranslationScale = poseAttributes.find("yTranslationScale");
if (findYtranslationScale != poseAttributes.end())
newPose.yTranslationScale = findYtranslationScale->second.toFloat();
newPose.frames = poseIt.second;
oldNewIdMap[QUuid(valueOfKeyInMapOrEmpty(poseAttributes, "id"))] = newPoseId;
poseIdList.push_back(newPoseId);
emit poseAdded(newPoseId);
}
for (const auto &motionIt: snapshot.motions) {
for (const auto &motionKv: snapshot.motions) {
QUuid oldMotionId = QUuid(motionKv.first);
QUuid newMotionId = QUuid::createUuid();
auto &newMotion = motionMap[newMotionId];
newMotion.id = newMotionId;
const auto &motionAttributes = motionIt.first;
newMotion.name = valueOfKeyInMapOrEmpty(motionAttributes, "name");
for (const auto &attributes: motionIt.second) {
auto linkData = valueOfKeyInMapOrEmpty(attributes, "linkData");
QUuid testId = QUuid(linkData);
if (!testId.isNull()) {
auto findInOldNewIdMapResult = oldNewIdMap.find(testId);
if (findInOldNewIdMapResult != oldNewIdMap.end()) {
linkData = findInOldNewIdMapResult->second.toString();
}
}
MotionClip clip(linkData,
valueOfKeyInMapOrEmpty(attributes, "linkDataType"));
clip.duration = valueOfKeyInMapOrEmpty(attributes, "duration").toFloat();
newMotion.clips.push_back(clip);
}
oldNewIdMap[QUuid(valueOfKeyInMapOrEmpty(motionAttributes, "id"))] = newMotionId;
motionIdList.push_back(newMotionId);
auto &motion = motionMap[newMotionId];
motion.id = newMotionId;
oldNewIdMap[oldMotionId] = motion.id;
motion.name = valueOfKeyInMapOrEmpty(motionKv.second, "name");
motion.parameters = motionKv.second;
emit motionAdded(newMotionId);
}
@ -1889,8 +1703,6 @@ void Document::addFromSnapshot(const Snapshot &snapshot, enum SnapshotSource sou
if (!snapshot.materials.empty())
emit materialListChanged();
if (!snapshot.poses.empty())
emit poseListChanged();
if (!snapshot.motions.empty())
emit motionListChanged();
}
@ -1907,10 +1719,7 @@ void Document::silentReset()
componentMap.clear();
materialMap.clear();
materialIdList.clear();
poseMap.clear();
poseIdList.clear();
motionMap.clear();
motionIdList.clear();
rootComponent = Component();
removeRigResults();
}
@ -3280,15 +3089,9 @@ void Document::saveSnapshot()
QElapsedTimer elapsedTimer;
elapsedTimer.start();
toSnapshot(&item.snapshot);
//item.hash = item.snapshot.hash();
//if (!m_undoItems.empty() && item.hash == m_undoItems[m_undoItems.size() - 1].hash) {
// qDebug() << "Snapshot has the same hash:" << item.hash << "skipped";
// return;
//}
if (m_undoItems.size() + 1 > m_maxSnapshot)
m_undoItems.pop_front();
m_undoItems.push_back(item);
qDebug() << "Snapshot saved with hash:" << item.hash << " Time consumed:" << elapsedTimer.elapsed() << "History count:" << m_undoItems.size();
}
void Document::undo()
@ -3354,17 +3157,6 @@ bool Document::hasPastableMaterialsInClipboard() const
return false;
}
bool Document::hasPastablePosesInClipboard() const
{
const QClipboard *clipboard = QApplication::clipboard();
const QMimeData *mimeData = clipboard->mimeData();
if (mimeData->hasText()) {
if (-1 != mimeData->text().indexOf("<pose "))
return true;
}
return false;
}
bool Document::hasPastableMotionsInClipboard() const
{
const QClipboard *clipboard = QApplication::clipboard();
@ -3748,18 +3540,15 @@ void Document::generateMotions()
return;
}
m_motionsGenerator = new MotionsGenerator(rigType, rigBones, rigWeights, currentRiggedOutcome());
m_motionsGenerator = new MotionsGenerator(rigType, *rigBones, *rigWeights, currentRiggedOutcome());
m_motionsGenerator->enableSnapshotMeshes();
bool hasDirtyMotion = false;
for (const auto &pose: poseMap) {
m_motionsGenerator->addPoseToLibrary(pose.first, pose.second.frames, pose.second.yTranslationScale);
}
for (auto &motion: motionMap) {
if (motion.second.dirty) {
hasDirtyMotion = true;
motion.second.dirty = false;
m_motionsGenerator->addRequirement(motion.first);
m_motionsGenerator->addMotion(motion.first, motion.second.parameters);
}
m_motionsGenerator->addMotionToLibrary(motion.first, motion.second.clips);
}
if (!hasDirtyMotion) {
delete m_motionsGenerator;
@ -3784,8 +3573,8 @@ void Document::motionsReady()
for (auto &motionId: m_motionsGenerator->generatedMotionIds()) {
auto motion = motionMap.find(motionId);
if (motion != motionMap.end()) {
auto resultPreviewMeshs = m_motionsGenerator->takeResultPreviewMeshs(motionId);
motion->second.updatePreviewMeshs(resultPreviewMeshs);
auto resultPreviewMesh = m_motionsGenerator->takeResultSnapshotMesh(motionId);
motion->second.updatePreviewMesh(resultPreviewMesh);
motion->second.jointNodeTrees = m_motionsGenerator->takeResultJointNodeTrees(motionId);
emit motionPreviewChanged(motionId);
emit motionResultChanged(motionId);
@ -3800,70 +3589,6 @@ void Document::motionsReady()
generateMotions();
}
void Document::generatePosePreviews()
{
if (nullptr != m_posePreviewsGenerator) {
return;
}
const std::vector<RiggerBone> *rigBones = resultRigBones();
const std::map<int, RiggerVertexWeights> *rigWeights = resultRigWeights();
if (nullptr == rigBones || nullptr == rigWeights) {
return;
}
m_posePreviewsGenerator = new PosePreviewsGenerator(rigType, rigBones,
rigWeights, *m_riggedOutcome);
bool hasDirtyPose = false;
for (auto &poseIt: poseMap) {
if (!poseIt.second.dirty)
continue;
if (poseIt.second.frames.empty())
continue;
int middle = poseIt.second.frames.size() / 2;
if (middle >= (int)poseIt.second.frames.size())
middle = 0;
m_posePreviewsGenerator->addPose({poseIt.first, middle}, poseIt.second.frames[middle].second);
poseIt.second.dirty = false;
hasDirtyPose = true;
}
if (!hasDirtyPose) {
delete m_posePreviewsGenerator;
m_posePreviewsGenerator = nullptr;
return;
}
qDebug() << "Pose previews generating..";
QThread *thread = new QThread;
m_posePreviewsGenerator->moveToThread(thread);
connect(thread, &QThread::started, m_posePreviewsGenerator, &PosePreviewsGenerator::process);
connect(m_posePreviewsGenerator, &PosePreviewsGenerator::finished, this, &Document::posePreviewsReady);
connect(m_posePreviewsGenerator, &PosePreviewsGenerator::finished, thread, &QThread::quit);
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
thread->start();
}
void Document::posePreviewsReady()
{
for (const auto &poseIdAndFrame: m_posePreviewsGenerator->generatedPreviewPoseIdAndFrames()) {
auto pose = poseMap.find(poseIdAndFrame.first);
if (pose != poseMap.end()) {
Model *resultPartPreviewMesh = m_posePreviewsGenerator->takePreview(poseIdAndFrame);
pose->second.updatePreviewMesh(resultPartPreviewMesh);
emit posePreviewChanged(poseIdAndFrame.first);
}
}
delete m_posePreviewsGenerator;
m_posePreviewsGenerator = nullptr;
qDebug() << "Pose previews generation done";
generatePosePreviews();
}
void Document::addMaterial(QUuid materialId, QString name, std::vector<MaterialLayer> layers)
{
auto findMaterialResult = materialMap.find(materialId);
@ -4198,11 +3923,11 @@ const QString &Document::scriptConsoleLog() const
return m_scriptConsoleLog;
}
void Document::startPaint(void)
void Document::startPaint()
{
}
void Document::stopPaint(void)
void Document::stopPaint()
{
if (m_vertexColorPainter || m_isMouseTargetResultObsolete) {
m_saveNextPaintSnapshot = true;

View File

@ -18,9 +18,7 @@
#include "bonemark.h"
#include "riggenerator.h"
#include "rigtype.h"
#include "posepreviewsgenerator.h"
#include "texturetype.h"
#include "interpolationtype.h"
#include "jointnodetree.h"
#include "skeletondocument.h"
#include "combinemode.h"
@ -42,7 +40,6 @@ class GeneratedCacheContext;
class HistoryItem
{
public:
uint32_t hash;
Snapshot snapshot;
};
@ -217,103 +214,6 @@ private:
std::set<QUuid> m_childrenIdSet;
};
class Pose
{
public:
Pose()
{
}
~Pose()
{
delete m_previewMesh;
}
QUuid id;
QString name;
bool dirty = true;
QUuid turnaroundImageId;
float yTranslationScale = 1.0;
std::vector<std::pair<std::map<QString, QString>, std::map<QString, std::map<QString, QString>>>> frames; // pair<attributes, parameters>
void updatePreviewMesh(Model *previewMesh)
{
delete m_previewMesh;
m_previewMesh = previewMesh;
}
Model *takePreviewMesh() const
{
if (nullptr == m_previewMesh)
return nullptr;
return new Model(*m_previewMesh);
}
bool yTranslationScaleAdjusted() const
{
return fabs(yTranslationScale - 1.0) >= 0.01;
}
private:
Q_DISABLE_COPY(Pose);
Model *m_previewMesh = nullptr;
};
enum class MotionClipType
{
Pose,
Interpolation,
Motion,
ProceduralAnimation
};
class MotionClip
{
public:
MotionClip()
{
}
MotionClip(const QString &linkData, const QString &linkDataType)
{
if ("poseId" == linkDataType) {
clipType = MotionClipType::Pose;
linkToId = QUuid(linkData);
} else if ("InterpolationType" == linkDataType) {
clipType = MotionClipType::Interpolation;
interpolationType = InterpolationTypeFromString(linkData.toUtf8().constData());
} else if ("ProceduralAnimation" == linkDataType) {
clipType = MotionClipType::ProceduralAnimation;
proceduralAnimation = ProceduralAnimationFromString(linkData.toUtf8().constData());
} else if ("motionId" == linkDataType) {
clipType = MotionClipType::Motion;
linkToId = QUuid(linkData);
}
}
QString linkDataType() const
{
if (MotionClipType::Pose == clipType)
return "poseId";
if (MotionClipType::Interpolation == clipType)
return "InterpolationType";
if (MotionClipType::ProceduralAnimation == clipType)
return "ProceduralAnimation";
if (MotionClipType::Motion == clipType)
return "motionId";
return "poseId";
}
QString linkData() const
{
if (MotionClipType::Pose == clipType)
return linkToId.toString();
if (MotionClipType::Interpolation == clipType)
return InterpolationTypeToString(interpolationType);
if (MotionClipType::ProceduralAnimation == clipType)
return ProceduralAnimationToString(proceduralAnimation);
if (MotionClipType::Motion == clipType)
return linkToId.toString();
return linkToId.toString();
}
float duration = 0.0;
MotionClipType clipType = MotionClipType::Pose;
QUuid linkToId;
InterpolationType interpolationType;
ProceduralAnimation proceduralAnimation;
};
class Motion
{
public:
@ -322,36 +222,28 @@ public:
}
~Motion()
{
releasePreviewMeshs();
delete m_previewMesh;
}
QUuid id;
QString name;
bool dirty = true;
std::vector<MotionClip> clips;
std::map<QString, QString> parameters;
std::vector<std::pair<float, JointNodeTree>> jointNodeTrees;
void updatePreviewMeshs(std::vector<std::pair<float, Model *>> &previewMeshs)
void updatePreviewMesh(Model *mesh)
{
releasePreviewMeshs();
m_previewMeshs = previewMeshs;
previewMeshs.clear();
delete m_previewMesh;
m_previewMesh = mesh;
}
Model *takePreviewMesh() const
{
if (m_previewMeshs.empty())
if (nullptr == m_previewMesh)
return nullptr;
int middle = std::max((int)m_previewMeshs.size() / 2 - 1, (int)0);
return new Model(*m_previewMeshs[middle].second);
return new Model(*m_previewMesh);
}
private:
Q_DISABLE_COPY(Motion);
void releasePreviewMeshs()
{
for (const auto &item: m_previewMeshs) {
delete item.second;
}
m_previewMeshs.clear();
}
std::vector<std::pair<float, Model *>> m_previewMeshs;
Model *m_previewMesh = nullptr;
};
class MaterialMap
@ -403,7 +295,6 @@ enum class DocumentToSnapshotFor
Document = 0,
Nodes,
Materials,
Poses,
Motions
};
@ -495,23 +386,14 @@ signals:
void checkEdge(QUuid edgeId);
void optionsChanged();
void rigTypeChanged();
void posesChanged();
void motionsChanged();
void poseAdded(QUuid poseId);
void poseRemoved(QUuid);
void poseListChanged();
void poseNameChanged(QUuid poseId);
void poseFramesChanged(QUuid poseId);
void poseTurnaroundImageIdChanged(QUuid poseId);
void poseYtranslationScaleChanged(QUuid poseId);
void posePreviewChanged(QUuid poseId);
void motionAdded(QUuid motionId);
void motionRemoved(QUuid motionId);
void motionListChanged();
void motionNameChanged(QUuid motionId);
void motionClipsChanged(QUuid motionId);
void motionParametersChanged(QUuid motionId);
void motionPreviewChanged(QUuid motionId);
void motionResultChanged(QUuid motionId);
void motionListChanged();
void materialAdded(QUuid materialId);
void materialRemoved(QUuid materialId);
void materialListChanged();
@ -553,10 +435,7 @@ public:
std::map<QUuid, Component> componentMap;
std::map<QUuid, Material> materialMap;
std::vector<QUuid> materialIdList;
std::map<QUuid, Pose> poseMap;
std::vector<QUuid> poseIdList;
std::map<QUuid, Motion> motionMap;
std::vector<QUuid> motionIdList;
Component rootComponent;
QImage preview;
bool undoable() const override;
@ -568,7 +447,6 @@ public:
void copyNodes(std::set<QUuid> nodeIdSet) const override;
void toSnapshot(Snapshot *snapshot, const std::set<QUuid> &limitNodeIds=std::set<QUuid>(),
DocumentToSnapshotFor forWhat=DocumentToSnapshotFor::Document,
const std::set<QUuid> &limitPoseIds=std::set<QUuid>(),
const std::set<QUuid> &limitMotionIds=std::set<QUuid>(),
const std::set<QUuid> &limitMaterialIds=std::set<QUuid>()) const;
void fromSnapshot(const Snapshot &snapshot);
@ -583,7 +461,6 @@ public:
const Component *findComponentParent(QUuid componentId) const;
QUuid findComponentParentId(QUuid componentId) const;
const Material *findMaterial(QUuid materialId) const;
const Pose *findPose(QUuid poseId) const;
const Motion *findMotion(QUuid motionId) const;
Model *takeResultMesh();
Model *takePaintedMesh();
@ -594,7 +471,6 @@ public:
const std::map<int, RiggerVertexWeights> *resultRigWeights() const;
void updateTurnaround(const QImage &image);
bool hasPastableMaterialsInClipboard() const;
bool hasPastablePosesInClipboard() const;
bool hasPastableMotionsInClipboard() const;
const Outcome &currentPostProcessedOutcome() const;
bool isExportReady() const;
@ -650,8 +526,6 @@ public slots:
void postProcessedMeshResultReady();
void generateRig();
void rigReady();
void generatePosePreviews();
void posePreviewsReady();
void generateMaterialPreviews();
void materialPreviewsReady();
void generateMotions();
@ -739,17 +613,9 @@ public slots:
void toggleSmoothNormal();
void enableWeld(bool enabled);
void setRigType(RigType toRigType);
void addPose(QUuid poseId, QString name, std::vector<std::pair<std::map<QString, QString>, std::map<QString, std::map<QString, QString>>>> frames,
QUuid turnaroundImageId,
float yTranslationScale);
void removePose(QUuid poseId);
void setPoseFrames(QUuid poseId, std::vector<std::pair<std::map<QString, QString>, std::map<QString, std::map<QString, QString>>>> frames);
void setPoseTurnaroundImageId(QUuid poseId, QUuid imageId);
void setPoseYtranslationScale(QUuid poseId, float scale);
void renamePose(QUuid poseId, QString name);
void addMotion(QUuid motionId, QString name, std::vector<MotionClip> clips);
void addMotion(QUuid motionId, QString name, std::map<QString, QString> parameters);
void removeMotion(QUuid motionId);
void setMotionClips(QUuid motionId, std::vector<MotionClip> clips);
void setMotionParameters(QUuid motionId, std::map<QString, QString> parameters);
void renameMotion(QUuid motionId, QString name);
void addMaterial(QUuid materialId, QString name, std::vector<MaterialLayer>);
void removeMaterial(QUuid materialId);
@ -764,8 +630,8 @@ public slots:
void scriptResultReady();
void updateVariable(const QString &name, const std::map<QString, QString> &value);
void updateVariableValue(const QString &name, const QString &value);
void startPaint(void);
void stopPaint(void);
void startPaint();
void stopPaint();
void setMousePickMaskNodeIds(const std::set<QUuid> &nodeIds);
private:
void splitPartByNode(std::vector<std::vector<QUuid>> *groups, QUuid nodeId);
@ -812,7 +678,6 @@ private: // need initialize
std::map<int, RiggerVertexWeights> *m_resultRigWeights;
bool m_isRigObsolete;
Outcome *m_riggedOutcome;
PosePreviewsGenerator *m_posePreviewsGenerator;
bool m_currentRigSucceed;
MaterialPreviewsGenerator *m_materialPreviewsGenerator;
MotionsGenerator *m_motionsGenerator;

View File

@ -79,14 +79,6 @@ void DocumentSaver::collectUsedResourceIds(const Snapshot *snapshot,
loadSkeletonFromXmlStream(&fileSnapshot, stream, SNAPSHOT_ITEM_CANVAS | SNAPSHOT_ITEM_COMPONENT);
collectUsedResourceIds(&fileSnapshot, imageIds, fileIds);
}
for (auto &pose: snapshot->poses) {
auto findCanvasImageId = pose.first.find("canvasImageId");
if (findCanvasImageId != pose.first.end()) {
QUuid imageId = QUuid(findCanvasImageId->second);
imageIds.insert(imageId);
}
}
}
bool DocumentSaver::save(const QString *filename,

View File

@ -495,19 +495,6 @@ DocumentWindow::DocumentWindow() :
updateRigWeightRenderWidget();
});
QDockWidget *poseDocker = new QDockWidget(tr("Poses"), this);
poseDocker->setAllowedAreas(Qt::RightDockWidgetArea);
PoseManageWidget *poseManageWidget = new PoseManageWidget(m_document, poseDocker);
poseDocker->setWidget(poseManageWidget);
connect(poseManageWidget, &PoseManageWidget::registerDialog, this, &DocumentWindow::registerDialog);
connect(poseManageWidget, &PoseManageWidget::unregisterDialog, this, &DocumentWindow::unregisterDialog);
addDockWidget(Qt::RightDockWidgetArea, poseDocker);
connect(poseDocker, &QDockWidget::topLevelChanged, [=](bool topLevel) {
Q_UNUSED(topLevel);
for (const auto &pose: m_document->poseMap)
emit m_document->posePreviewChanged(pose.first);
});
QDockWidget *motionDocker = new QDockWidget(tr("Motions"), this);
motionDocker->setAllowedAreas(Qt::RightDockWidgetArea);
MotionManageWidget *motionManageWidget = new MotionManageWidget(m_document, motionDocker);
@ -524,8 +511,7 @@ DocumentWindow::DocumentWindow() :
tabifyDockWidget(partsDocker, materialDocker);
tabifyDockWidget(materialDocker, rigDocker);
tabifyDockWidget(rigDocker, poseDocker);
tabifyDockWidget(poseDocker, motionDocker);
tabifyDockWidget(rigDocker, motionDocker);
tabifyDockWidget(motionDocker, scriptDocker);
partsDocker->raise();
@ -922,13 +908,6 @@ DocumentWindow::DocumentWindow() :
});
m_windowMenu->addAction(m_showRigAction);
m_showPosesAction = new QAction(tr("Poses"), this);
connect(m_showPosesAction, &QAction::triggered, [=]() {
poseDocker->show();
poseDocker->raise();
});
m_windowMenu->addAction(m_showPosesAction);
m_showMotionsAction = new QAction(tr("Motions"), this);
connect(m_showMotionsAction, &QAction::triggered, [=]() {
motionDocker->show();
@ -1281,7 +1260,6 @@ DocumentWindow::DocumentWindow() :
m_modelRenderWidget->updateMesh(resultMesh);
});
connect(m_document, &Document::posesChanged, m_document, &Document::generateMotions);
connect(m_document, &Document::motionsChanged, m_document, &Document::generateMotions);
connect(graphicsWidget, &SkeletonGraphicsWidget::cursorChanged, [=]() {
@ -1313,16 +1291,6 @@ DocumentWindow::DocumentWindow() :
//connect(m_document, &SkeletonDocument::resultRigChanged, tetrapodPoseEditWidget, &TetrapodPoseEditWidget::updatePreview);
connect(m_document, &Document::poseAdded, this, [=](QUuid poseId) {
Q_UNUSED(poseId);
m_document->generatePosePreviews();
});
connect(m_document, &Document::poseFramesChanged, this, [=](QUuid poseId) {
Q_UNUSED(poseId);
m_document->generatePosePreviews();
});
connect(m_document, &Document::resultRigChanged, m_document, &Document::generatePosePreviews);
connect(m_document, &Document::resultRigChanged, m_document, &Document::generateMotions);
connect(m_document, &Document::materialAdded, this, [=](QUuid materialId) {
@ -1943,11 +1911,8 @@ void DocumentWindow::exportFbxToFilename(const QString &filename)
QApplication::setOverrideCursor(Qt::WaitCursor);
Outcome skeletonResult = m_document->currentPostProcessedOutcome();
std::vector<std::pair<QString, std::vector<std::pair<float, JointNodeTree>>>> exportMotions;
for (const auto &motionId: m_document->motionIdList) {
const Motion *motion = m_document->findMotion(motionId);
if (nullptr == motion)
continue;
exportMotions.push_back({motion->name, motion->jointNodeTrees});
for (const auto &motionIt: m_document->motionMap) {
exportMotions.push_back({motionIt.second.name, motionIt.second.jointNodeTrees});
}
FbxFileWriter fbxFileWriter(skeletonResult, m_document->resultRigBones(), m_document->resultRigWeights(), filename,
m_document->textureImage,
@ -1980,11 +1945,8 @@ void DocumentWindow::exportGlbToFilename(const QString &filename)
QApplication::setOverrideCursor(Qt::WaitCursor);
Outcome skeletonResult = m_document->currentPostProcessedOutcome();
std::vector<std::pair<QString, std::vector<std::pair<float, JointNodeTree>>>> exportMotions;
for (const auto &motionId: m_document->motionIdList) {
const Motion *motion = m_document->findMotion(motionId);
if (nullptr == motion)
continue;
exportMotions.push_back({motion->name, motion->jointNodeTrees});
for (const auto &motionIt: m_document->motionMap) {
exportMotions.push_back({motionIt.second.name, motionIt.second.jointNodeTrees});
}
GlbFileWriter glbFileWriter(skeletonResult, m_document->resultRigBones(), m_document->resultRigWeights(), filename,
m_document->textureHasTransparencySettings,

View File

@ -15,7 +15,6 @@
#include "exportpreviewwidget.h"
#include "rigwidget.h"
#include "bonemark.h"
#include "posemanagewidget.h"
#include "preferenceswidget.h"
#include "graphicscontainerwidget.h"
#include "normalanddepthmapsgenerator.h"
@ -202,7 +201,6 @@ private:
QAction *m_showDebugDialogAction;
QAction *m_showMaterialsAction;
QAction *m_showRigAction;
QAction *m_showPosesAction;
QAction *m_showMotionsAction;
QAction *m_showScriptAction;

View File

@ -2602,8 +2602,8 @@ FbxFileWriter::FbxFileWriter(Outcome &outcome,
deformer.addChild(userData);
deformer.addPropertyNode("Indexes", bindPerBone[i].first);
deformer.addPropertyNode("Weights", bindPerBone[i].second);
deformer.addPropertyNode("Transform", matrixToVector(jointNode.transformMatrix.inverted()));
deformer.addPropertyNode("TransformLink", matrixToVector(jointNode.transformMatrix));
deformer.addPropertyNode("Transform", matrixToVector(jointNode.inverseBindMatrix));
deformer.addPropertyNode("TransformLink", matrixToVector(jointNode.bindMatrix));
deformer.addPropertyNode("TransformAssociateModel", m_identityMatrix);
deformer.addChild(FBXNode());
}
@ -2774,7 +2774,7 @@ FbxFileWriter::FbxFileWriter(Outcome &outcome,
const auto &boneNode = boneNodes[i];
FBXNode poseNode("PoseNode");
poseNode.addPropertyNode("Node", (int64_t)limbNodeIds[1 + i]);
poseNode.addPropertyNode("Matrix", matrixToVector(boneNode.transformMatrix));
poseNode.addPropertyNode("Matrix", matrixToVector(boneNode.bindMatrix));
poseNode.addChild(FBXNode());
pose.addChild(poseNode);
}

View File

@ -0,0 +1,87 @@
#include <QDebug>
#include "genericspineandpseudophysics.h"
const double GenericSpineAndPseudoPhysics::g = 9.80665;
void GenericSpineAndPseudoPhysics::calculateFootHeights(double preferredHeight, double stanceTime, double swingTime,
std::vector<double> *heights, std::vector<double> *moveOffsets)
{
double totalTime = stanceTime + swingTime;
double halfSwingTime = 0.5 * swingTime;
double pelvisVelocity = g * (halfSwingTime);
double receptionTime = 0.5 * stanceTime;
double propulsionTime = stanceTime - receptionTime;
double pelvisAcceleration = pelvisVelocity / receptionTime;
double frameTime = 1.0 / 60;
double deformHeight = 0;
for (double time = 0.0; time < totalTime; time += frameTime) {
double t, v0, a, s;
if (time < stanceTime) {
if (time < receptionTime) {
t = time;
a = -pelvisAcceleration;
v0 = pelvisVelocity;
s = v0 * t + 0.5 * a * std::pow(t, 2);
qDebug() << "Stance[Reception] s:" << s << "time:" << time << "t:" << t << "v0:" << v0 << "a:" << a;
deformHeight = s;
heights->push_back(preferredHeight - s);
if (nullptr != moveOffsets)
moveOffsets->push_back((receptionTime - time) / receptionTime);
} else {
t = time - receptionTime;
a = pelvisAcceleration;
v0 = 0;
s = v0 * t + 0.5 * a * std::pow(t, 2);
qDebug() << "Stance[Propulsion] s:" << s << "time:" << time << "t:" << t << "v0:" << v0 << "a:" << a;
heights->push_back(preferredHeight - (deformHeight - s));
if (nullptr != moveOffsets)
moveOffsets->push_back(-t / propulsionTime);
}
} else {
if (time - stanceTime < halfSwingTime) {
t = time - stanceTime;
a = -g;
v0 = pelvisVelocity;
s = v0 * t + 0.5 * a * std::pow(t, 2);
qDebug() << "Swing[1/2] s:" << s << "time:" << time << "t:" << t << "v0:" << v0 << "a:" << a;
deformHeight = s;
heights->push_back(preferredHeight + s);
if (nullptr != moveOffsets)
moveOffsets->push_back(-(halfSwingTime - t) / halfSwingTime);
} else {
t = time - stanceTime - halfSwingTime;
a = g;
v0 = 0;
s = v0 * t + 0.5 * a * std::pow(t, 2);
qDebug() << "Swing[2/2] s:" << s << "time:" << time << "t:" << t << "v0:" << v0 << "a:" << a;
heights->push_back(preferredHeight + (deformHeight - s));
if (nullptr != moveOffsets)
moveOffsets->push_back(t / halfSwingTime);
}
}
}
double time = 0.0;
for (size_t i = 0; i < heights->size(); ++i) {
QString phase;
if (time <= stanceTime) {
if (time <= receptionTime) {
phase = "Stance[Reception]";
} else {
phase = "Stance[Propulsion]";
}
} else {
if (time - stanceTime < halfSwingTime) {
phase = "Swing[1/2]";
} else {
phase = "Swing[2/2]";
}
}
qDebug() << phase << "frame[" << i << "]:" << (*heights)[i] << " move:" << (*moveOffsets)[i];
time += frameTime;
}
}

View File

@ -0,0 +1,19 @@
#ifndef DUST3D_GENERIC_SPINE_AND_PSEUDO_PHYSICS_H
#define DUST3D_GENERIC_SPINE_AND_PSEUDO_PHYSICS_H
#include <vector>
class GenericSpineAndPseudoPhysics
{
public:
GenericSpineAndPseudoPhysics()
{
}
static void calculateFootHeights(double preferredHeight, double stanceTime, double swingTime,
std::vector<double> *heights, std::vector<double> *moveOffsets=nullptr);
private:
static const double g;
};
#endif

View File

@ -92,20 +92,22 @@ GlbFileWriter::GlbFileWriter(Outcome &outcome,
m_json["nodes"][1]["skin"] = 0;
m_json["skins"][0]["joints"] = {};
const QQuaternion noneRotation;
for (size_t i = 0; i < boneNodes.size(); i++) {
const auto &bone = (*resultRigBones)[i];
m_json["skins"][0]["joints"] += skeletonNodeStartIndex + i;
m_json["nodes"][skeletonNodeStartIndex + i]["name"] = boneNodes[i].name.toUtf8().constData();
m_json["nodes"][skeletonNodeStartIndex + i]["translation"] = {
boneNodes[i].translation.x(),
boneNodes[i].translation.y(),
boneNodes[i].translation.z()
boneNodes[i].bindTranslation.x(),
boneNodes[i].bindTranslation.y(),
boneNodes[i].bindTranslation.z()
};
m_json["nodes"][skeletonNodeStartIndex + i]["rotation"] = {
boneNodes[i].rotation.x(),
boneNodes[i].rotation.y(),
boneNodes[i].rotation.z(),
boneNodes[i].rotation.scalar()
noneRotation.x(),
noneRotation.y(),
noneRotation.z(),
noneRotation.scalar()
};
if (!boneNodes[i].children.empty()) {

View File

@ -0,0 +1,54 @@
#include <QVector3D>
#include <QDebug>
#include "hermitecurveinterpolation.h"
void HermiteCurveInterpolation::update()
{
std::vector<std::pair<size_t, QVector2D>> keyNodes;
for (size_t nodeIndex = 0; nodeIndex < m_nodes.size(); ++nodeIndex) {
auto findPerpendicularDirections = m_perpendicularDirectionsPerNode.find(nodeIndex);
if (findPerpendicularDirections == m_perpendicularDirectionsPerNode.end())
continue;
QVector2D perpendicularDirection;
for (const auto &it: findPerpendicularDirections->second)
perpendicularDirection += it;
perpendicularDirection.normalize();
QVector3D direction3 = QVector3D(perpendicularDirection.x(), perpendicularDirection.y(), 0.0);
QVector3D tangent3 = QVector3D::crossProduct(direction3, QVector3D(0, 0, 1.0));
keyNodes.push_back({nodeIndex, QVector2D(tangent3.x(), tangent3.y())});
}
m_updatedPositions = m_nodes;
for (size_t keyIndex = 1; keyIndex < keyNodes.size(); ++keyIndex) {
const auto &firstKey = keyNodes[keyIndex - 1];
const auto &secondKey = keyNodes[keyIndex];
double distance = 0;
for (size_t nodeIndex = firstKey.first + 1; nodeIndex <= secondKey.first; ++nodeIndex) {
distance += (m_nodesForDistance[nodeIndex] - m_nodesForDistance[nodeIndex - 1]).length();
}
double t = 0;
const auto &p1 = m_nodes[firstKey.first];
const auto &t1 = firstKey.second;
const auto &p2 = m_nodes[secondKey.first];
const auto &t2 = secondKey.second;
//qDebug() << "======================" << keyIndex << "=========================";
//qDebug() << "P1:" << p1.x() << "," << p1.y();
//qDebug() << "T1:" << t1.x() << "," << t1.y();
//qDebug() << "P2:" << p2.x() << "," << p2.y();
//qDebug() << "T2:" << t2.x() << "," << t2.y();
for (size_t nodeIndex = firstKey.first + 1; nodeIndex < secondKey.first; ++nodeIndex) {
t += (m_nodesForDistance[nodeIndex] - m_nodesForDistance[nodeIndex - 1]).length();
double s = t / distance;
double s3 = std::pow(s, 3);
double s2 = std::pow(s, 2);
double h1 = 2 * s3 - 3 * s2 + 1;
double h2 = -2 * s3 + 3 * s2;
double h3 = s3 - 2 * s2 + s;
double h4 = s3 - s2;
//qDebug() << "s:" << s << "t:" << t << "distance:" << distance << "h1:" << h1 << "h2:" << h2 << "h3:" << h3 << "h4:" << h4;
//qDebug() << "HCI original s:" << s << " position:" << m_updatedPositions[nodeIndex].x() << "," << m_updatedPositions[nodeIndex].y();
m_updatedPositions[nodeIndex] = h1 * p1 + h2 * p2 + h3 * t1 + h4 * t2;
//qDebug() << "HCI Updated s:" << s << " position:" << m_updatedPositions[nodeIndex].x() << "," << m_updatedPositions[nodeIndex].y();
}
}
}

View File

@ -0,0 +1,41 @@
#ifndef DUST3D_HERMITE_CURVE_INTERPOLATION_H
#define DUST3D_HERMITE_CURVE_INTERPOLATION_H
#include <vector>
#include <unordered_map>
#include <QVector2D>
#include <QVector3D>
class HermiteCurveInterpolation
{
public:
HermiteCurveInterpolation()
{
}
size_t addNode(const QVector2D &node, const QVector3D &originalNode)
{
size_t nodeIndex = m_nodes.size();
m_nodes.push_back(node);
m_nodesForDistance.push_back(originalNode);
return nodeIndex;
}
void addPerpendicularDirection(size_t nodeIndex, const QVector2D &direction)
{
m_perpendicularDirectionsPerNode[nodeIndex].push_back(direction);
}
void update();
const QVector2D &getUpdatedPosition(size_t nodeIndex)
{
return m_updatedPositions[nodeIndex];
}
private:
std::vector<QVector2D> m_nodes;
std::vector<QVector3D> m_nodesForDistance;
std::unordered_map<size_t, std::vector<QVector2D>> m_perpendicularDirectionsPerNode;
std::vector<QVector2D> m_updatedPositions;
};
#endif

View File

@ -1,78 +0,0 @@
#include <map>
#include <QEasingCurve>
#include "interpolationtype.h"
IMPL_InterpolationTypeFromString
IMPL_InterpolationTypeToString
IMPL_InterpolationTypeToDispName
IMPL_InterpolationTypeToEasingCurveType
float calculateInterpolation(InterpolationType type, float knot)
{
QEasingCurve easing;
easing.setType(InterpolationTypeToEasingCurveType(type));
return easing.valueForProgress(knot);
}
bool InterpolationIsLinear(InterpolationType type)
{
return QEasingCurve::Linear == InterpolationTypeToEasingCurveType(type);
}
bool InterpolationHasAccelerating(InterpolationType type)
{
QString name = InterpolationTypeToString(type);
if (-1 != name.indexOf("In"))
return true;
return false;
}
bool InterpolationHasDecelerating(InterpolationType type)
{
QString name = InterpolationTypeToString(type);
if (-1 != name.indexOf("Out"))
return true;
return false;
}
bool InterpolationIsBouncingBegin(InterpolationType type)
{
QString name = InterpolationTypeToString(type);
if (-1 != name.indexOf("InBack") || -1 != name.indexOf("InOutBack"))
return true;
return false;
}
bool InterpolationIsBouncingEnd(InterpolationType type)
{
QString name = InterpolationTypeToString(type);
if (-1 != name.indexOf("OutBack") || -1 != name.indexOf("InOutBack"))
return true;
return false;
}
InterpolationType InterpolationMakeFromOptions(bool isLinear,
bool hasAccelerating, bool hasDecelerating,
bool boucingBegin, bool bouncingEnd)
{
if (isLinear)
return InterpolationType::Linear;
if (boucingBegin && bouncingEnd) {
return InterpolationType::EaseInOutBack;
} else if (boucingBegin) {
return InterpolationType::EaseInBack;
} else if (bouncingEnd) {
return InterpolationType::EaseOutBack;
} else {
if (hasAccelerating && hasDecelerating) {
return InterpolationType::EaseInOutCubic;
} else if (hasAccelerating) {
return InterpolationType::EaseInCubic;
} else if (hasDecelerating) {
return InterpolationType::EaseOutCubic;
} else {
return InterpolationType::EaseInOutCubic;
}
}
}

View File

@ -1,229 +0,0 @@
#ifndef DUST3D_INTERPOLATION_TYPE_H
#define DUST3D_INTERPOLATION_TYPE_H
#include <QString>
#include <QEasingCurve>
enum class InterpolationType
{
None = 0,
Linear,
EaseInQuad,
EaseOutQuad,
EaseInOutQuad,
EaseOutInQuad,
EaseInCubic,
EaseOutCubic,
EaseInOutCubic,
EaseOutInCubic,
EaseInQuart,
EaseOutQuart,
EaseInOutQuart,
EaseOutInQuart,
EaseInQuint,
EaseOutQuint,
EaseInOutQuint,
EaseOutInQuint,
EaseInSine,
EaseOutSine,
EaseInOutSine,
EaseOutInSine,
EaseInExpo,
EaseOutExpo,
EaseInOutExpo,
EaseOutInExpo,
EaseInCirc,
EaseOutCirc,
EaseInOutCirc,
EaseOutInCirc,
EaseInElastic,
EaseOutElastic,
EaseInOutElastic,
EaseOutInElastic,
EaseInBack,
EaseOutBack,
EaseInOutBack,
EaseOutInBack,
EaseInBounce,
EaseOutBounce,
EaseInOutBounce,
EaseOutInBounce,
Count
};
InterpolationType InterpolationTypeFromString(const char *typeString);
#define IMPL_InterpolationTypeFromString \
InterpolationType InterpolationTypeFromString(const char *typeString) \
{ \
QString type = typeString; \
static std::map<QString, InterpolationType> s_map = { \
{"None", InterpolationType::None}, \
{"Linear", InterpolationType::Linear}, \
{"EaseInQuad", InterpolationType::EaseInQuad}, \
{"EaseOutQuad", InterpolationType::EaseOutQuad}, \
{"EaseInOutQuad", InterpolationType::EaseInOutQuad}, \
{"EaseOutInQuad", InterpolationType::EaseOutInQuad}, \
{"EaseInCubic", InterpolationType::EaseInCubic}, \
{"EaseOutCubic", InterpolationType::EaseOutCubic}, \
{"EaseInOutCubic", InterpolationType::EaseInOutCubic}, \
{"EaseOutInCubic", InterpolationType::EaseOutInCubic}, \
{"EaseInQuart", InterpolationType::EaseInQuart}, \
{"EaseOutQuart", InterpolationType::EaseOutQuart}, \
{"EaseInOutQuart", InterpolationType::EaseInOutQuart}, \
{"EaseOutInQuart", InterpolationType::EaseOutInQuart}, \
{"EaseInQuint", InterpolationType::EaseInQuint}, \
{"EaseOutQuint", InterpolationType::EaseOutQuint}, \
{"EaseInOutQuint", InterpolationType::EaseInOutQuint}, \
{"EaseOutInQuint", InterpolationType::EaseOutInQuint}, \
{"EaseInSine", InterpolationType::EaseInSine}, \
{"EaseOutSine", InterpolationType::EaseOutSine}, \
{"EaseInOutSine", InterpolationType::EaseInOutSine}, \
{"EaseOutInSine", InterpolationType::EaseOutInSine}, \
{"EaseInExpo", InterpolationType::EaseInExpo}, \
{"EaseOutExpo", InterpolationType::EaseOutExpo}, \
{"EaseInOutExpo", InterpolationType::EaseInOutExpo}, \
{"EaseOutInExpo", InterpolationType::EaseOutInExpo}, \
{"EaseInCirc", InterpolationType::EaseInCirc}, \
{"EaseOutCirc", InterpolationType::EaseOutCirc}, \
{"EaseInOutCirc", InterpolationType::EaseInOutCirc}, \
{"EaseOutInCirc", InterpolationType::EaseOutInCirc}, \
{"EaseInElastic", InterpolationType::EaseInElastic}, \
{"EaseOutElastic", InterpolationType::EaseOutElastic}, \
{"EaseInOutElastic", InterpolationType::EaseInOutElastic}, \
{"EaseOutInElastic", InterpolationType::EaseOutInElastic}, \
{"EaseInBack", InterpolationType::EaseInBack}, \
{"EaseOutBack", InterpolationType::EaseOutBack}, \
{"EaseInOutBack", InterpolationType::EaseInOutBack}, \
{"EaseOutInBack", InterpolationType::EaseOutInBack}, \
{"EaseInBounce", InterpolationType::EaseInBounce}, \
{"EaseOutBounce", InterpolationType::EaseOutBounce}, \
{"EaseInOutBounce", InterpolationType::EaseInOutBounce}, \
{"EaseOutInBounce", InterpolationType::EaseOutInBounce} \
}; \
auto findResult = s_map.find(type); \
if (findResult != s_map.end()) \
return findResult->second; \
return InterpolationType::None; \
}
const char *InterpolationTypeToString(InterpolationType type);
#define IMPL_InterpolationTypeToString \
const char *InterpolationTypeToString(InterpolationType type) \
{ \
static const char *s_names[] = { \
"None", \
"Linear", \
"EaseInQuad", \
"EaseOutQuad", \
"EaseInOutQuad", \
"EaseOutInQuad", \
"EaseInCubic", \
"EaseOutCubic", \
"EaseInOutCubic", \
"EaseOutInCubic", \
"EaseInQuart", \
"EaseOutQuart", \
"EaseInOutQuart", \
"EaseOutInQuart", \
"EaseInQuint", \
"EaseOutQuint", \
"EaseInOutQuint", \
"EaseOutInQuint", \
"EaseInSine", \
"EaseOutSine", \
"EaseInOutSine", \
"EaseOutInSine", \
"EaseInExpo", \
"EaseOutExpo", \
"EaseInOutExpo", \
"EaseOutInExpo", \
"EaseInCirc", \
"EaseOutCirc", \
"EaseInOutCirc", \
"EaseOutInCirc", \
"EaseInElastic", \
"EaseOutElastic", \
"EaseInOutElastic", \
"EaseOutInElastic", \
"EaseInBack", \
"EaseOutBack", \
"EaseInOutBack", \
"EaseOutInBack", \
"EaseInBounce", \
"EaseOutBounce", \
"EaseInOutBounce", \
"EaseOutInBounce" \
}; \
size_t index = (size_t)type; \
if (index < sizeof(s_names) / sizeof(s_names[0])) \
return s_names[index]; \
return ""; \
}
QString InterpolationTypeToDispName(InterpolationType type);
#define IMPL_InterpolationTypeToDispName \
QString InterpolationTypeToDispName(InterpolationType type) \
{ \
return InterpolationTypeToString(type); \
}
QEasingCurve::Type InterpolationTypeToEasingCurveType(InterpolationType type);
#define IMPL_InterpolationTypeToEasingCurveType \
QEasingCurve::Type InterpolationTypeToEasingCurveType(InterpolationType type)\
{ \
static QEasingCurve::Type s_types[] = { \
QEasingCurve::Linear, \
QEasingCurve::Linear, \
QEasingCurve::InQuad, \
QEasingCurve::OutQuad, \
QEasingCurve::InOutQuad, \
QEasingCurve::OutInQuad, \
QEasingCurve::InCubic, \
QEasingCurve::OutCubic, \
QEasingCurve::InOutCubic, \
QEasingCurve::OutInCubic, \
QEasingCurve::InQuart, \
QEasingCurve::OutQuart, \
QEasingCurve::InOutQuart, \
QEasingCurve::OutInQuart, \
QEasingCurve::InQuint, \
QEasingCurve::OutQuint, \
QEasingCurve::InOutQuint, \
QEasingCurve::OutInQuint, \
QEasingCurve::InSine, \
QEasingCurve::OutSine, \
QEasingCurve::InOutSine, \
QEasingCurve::OutInSine, \
QEasingCurve::InExpo, \
QEasingCurve::OutExpo, \
QEasingCurve::InOutExpo, \
QEasingCurve::OutInExpo, \
QEasingCurve::InCirc, \
QEasingCurve::OutCirc, \
QEasingCurve::InOutCirc, \
QEasingCurve::OutInCirc, \
QEasingCurve::InElastic, \
QEasingCurve::OutElastic, \
QEasingCurve::InOutElastic, \
QEasingCurve::OutInElastic, \
QEasingCurve::InBack, \
QEasingCurve::OutBack, \
QEasingCurve::InOutBack, \
QEasingCurve::OutInBack, \
QEasingCurve::InBounce, \
QEasingCurve::OutBounce, \
QEasingCurve::InOutBounce, \
QEasingCurve::OutInBounce \
}; \
size_t index = (size_t)type; \
if (index < sizeof(s_types) / sizeof(s_types[0])) \
return s_types[index]; \
return QEasingCurve::Linear; \
}
bool InterpolationIsLinear(InterpolationType type);
bool InterpolationHasAccelerating(InterpolationType type);
bool InterpolationHasDecelerating(InterpolationType type);
bool InterpolationIsBouncingBegin(InterpolationType type);
bool InterpolationIsBouncingEnd(InterpolationType type);
InterpolationType InterpolationMakeFromOptions(bool isLinear,
bool hasAccelerating, bool hasDecelerating,
bool boucingBegin, bool bouncingEnd);
float calculateInterpolation(InterpolationType type, float knot);
#endif

View File

@ -1,3 +1,4 @@
#include <QMatrix3x3>
#include "jointnodetree.h"
#include "util.h"
@ -6,63 +7,33 @@ const std::vector<JointNode> &JointNodeTree::nodes() const
return m_boneNodes;
}
void JointNodeTree::updateRotation(int index, QQuaternion rotation)
void JointNodeTree::updateRotation(int index, const QQuaternion &rotation)
{
m_boneNodes[index].rotation = rotation;
}
void JointNodeTree::updateTranslation(int index, QVector3D translation)
void JointNodeTree::updateTranslation(int index, const QVector3D &translation)
{
m_boneNodes[index].translation = translation;
}
void JointNodeTree::addTranslation(int index, QVector3D translation)
void JointNodeTree::updateMatrix(int index, const QMatrix4x4 &matrix)
{
m_boneNodes[index].translation += translation;
}
const QMatrix4x4 &localMatrix = matrix;
void JointNodeTree::reset()
{
for (auto &node: m_boneNodes) {
node.rotation = QQuaternion();
node.translation = node.bindTranslation;
}
}
updateTranslation(index,
QVector3D(localMatrix(0, 3), localMatrix(1, 3), localMatrix(2, 3)));
void JointNodeTree::calculateBonePositions(std::vector<std::pair<QVector3D, QVector3D>> *bonePositions,
const JointNodeTree *jointNodeTree,
const std::vector<RiggerBone> *rigBones) const
{
if (nullptr == bonePositions || nullptr == jointNodeTree || nullptr == rigBones)
return;
(*bonePositions).resize(jointNodeTree->nodes().size());
for (int i = 0; i < (int)jointNodeTree->nodes().size(); i++) {
const auto &node = jointNodeTree->nodes()[i];
(*bonePositions)[i] = std::make_pair(node.transformMatrix * node.position,
node.transformMatrix * (node.position + ((*rigBones)[i].tailPosition - (*rigBones)[i].headPosition)));
}
}
void JointNodeTree::recalculateTransformMatrices()
{
for (decltype(m_boneNodes.size()) i = 0; i < m_boneNodes.size(); i++) {
QMatrix4x4 parentTransformMatrix;
auto &node = m_boneNodes[i];
if (node.parentIndex != -1) {
const auto &parent = m_boneNodes[node.parentIndex];
parentTransformMatrix = parent.transformMatrix;
}
QMatrix4x4 translateMatrix;
translateMatrix.translate(node.translation);
QMatrix4x4 rotationMatrix;
rotationMatrix.rotate(node.rotation);
node.transformMatrix = parentTransformMatrix * translateMatrix * rotationMatrix;
}
for (decltype(m_boneNodes.size()) i = 0; i < m_boneNodes.size(); i++) {
auto &node = m_boneNodes[i];
node.transformMatrix *= node.inverseBindMatrix;
}
float scalar = std::sqrt(std::max(0.0f, 1.0f + localMatrix(0, 0) + localMatrix(1, 1) + localMatrix(2, 2))) / 2.0f;
float x = std::sqrt(std::max(0.0f, 1.0f + localMatrix(0, 0) - localMatrix(1, 1) - localMatrix(2, 2))) / 2.0f;
float y = std::sqrt(std::max(0.0f, 1.0f - localMatrix(0, 0) + localMatrix(1, 1) - localMatrix(2, 2))) / 2.0f;
float z = std::sqrt(std::max(0.0f, 1.0f - localMatrix(0, 0) - localMatrix(1, 1) + localMatrix(2, 2))) / 2.0f;
x *= x * (localMatrix(2, 1) - localMatrix(1, 2)) > 0 ? 1 : -1;
y *= y * (localMatrix(0, 2) - localMatrix(2, 0)) > 0 ? 1 : -1;
z *= z * (localMatrix(1, 0) - localMatrix(0, 1)) > 0 ? 1 : -1;
float length = std::sqrt(scalar * scalar + x * x + y * y + z * z);
updateRotation(index,
QQuaternion(scalar / length, x / length, y / length, z / length));
}
JointNodeTree::JointNodeTree(const std::vector<RiggerBone> *resultRigBones)
@ -78,38 +49,20 @@ JointNodeTree::JointNodeTree(const std::vector<RiggerBone> *resultRigBones)
auto &node = m_boneNodes[i];
node.name = bone.name;
node.position = bone.headPosition;
QMatrix4x4 parentMatrix;
if (-1 == node.parentIndex) {
node.bindTranslation = node.position;
} else {
const auto &parentNode = m_boneNodes[node.parentIndex];
node.bindTranslation = node.position - parentNode.position;
parentMatrix = parentNode.bindMatrix;
}
QMatrix4x4 translationMatrix;
translationMatrix.translate(node.bindTranslation);
node.bindMatrix = parentMatrix * translationMatrix;
node.inverseBindMatrix = node.bindMatrix.inverted();
node.children = bone.children;
for (const auto &childIndex: bone.children)
m_boneNodes[childIndex].parentIndex = i;
}
for (decltype(resultRigBones->size()) i = 0; i < resultRigBones->size(); i++) {
QMatrix4x4 parentTransformMatrix;
auto &node = m_boneNodes[i];
if (node.parentIndex != -1) {
const auto &parent = m_boneNodes[node.parentIndex];
parentTransformMatrix = parent.transformMatrix;
node.translation = node.position - parent.position;
} else {
node.translation = node.position;
}
node.bindTranslation = node.translation;
QMatrix4x4 translateMatrix;
translateMatrix.translate(node.translation);
node.transformMatrix = parentTransformMatrix * translateMatrix;
node.inverseBindMatrix = node.transformMatrix.inverted();
}
}
JointNodeTree JointNodeTree::slerp(const JointNodeTree &first, const JointNodeTree &second, float t)
{
JointNodeTree slerpResult = first;
for (decltype(first.nodes().size()) i = 0; i < first.nodes().size() && i < second.nodes().size(); i++) {
slerpResult.updateRotation(i,
quaternionOvershootSlerp(first.nodes()[i].rotation, second.nodes()[i].rotation, t));
slerpResult.updateTranslation(i, (first.nodes()[i].translation * (1.0 - t) + second.nodes()[i].translation * t));
}
slerpResult.recalculateTransformMatrices();
return slerpResult;
}

View File

@ -13,7 +13,7 @@ struct JointNode
QVector3D bindTranslation;
QVector3D translation;
QQuaternion rotation;
QMatrix4x4 transformMatrix;
QMatrix4x4 bindMatrix;
QMatrix4x4 inverseBindMatrix;
std::vector<int> children;
};
@ -23,15 +23,9 @@ class JointNodeTree
public:
const std::vector<JointNode> &nodes() const;
JointNodeTree(const std::vector<RiggerBone> *resultRigBones);
void updateRotation(int index, QQuaternion rotation);
void updateTranslation(int index, QVector3D translation);
void addTranslation(int index, QVector3D translation);
void reset();
void recalculateTransformMatrices();
void calculateBonePositions(std::vector<std::pair<QVector3D, QVector3D>> *bonePositions,
const JointNodeTree *jointNodeTree,
const std::vector<RiggerBone> *rigBones) const;
static JointNodeTree slerp(const JointNodeTree &first, const JointNodeTree &second, float t);
void updateRotation(int index, const QQuaternion &rotation);
void updateTranslation(int index, const QVector3D &translation);
void updateMatrix(int index, const QMatrix4x4 &matrix);
private:
std::vector<JointNode> m_boneNodes;
};

View File

@ -116,7 +116,7 @@ DUST3D_DLL void * DUST3D_API dust3dGetUserData(dust3d *ds3)
return ds3->userData;
}
DUST3D_DLL const char * DUST3D_API dust3dVersion(void)
DUST3D_DLL const char * DUST3D_API dust3dVersion()
{
return APP_NAME " " APP_HUMAN_VER;
}

View File

@ -4,7 +4,7 @@
#include "logbrowser.h"
#include "logbrowserdialog.h"
bool LogBrowser::m_enableOutputToFile = false;
bool LogBrowser::m_enableOutputToFile = true;
LogBrowser::LogBrowser(QObject *parent) :
QObject(parent)

View File

@ -49,6 +49,7 @@ MaterialEditWidget::MaterialEditWidget(const Document *document, QWidget *parent
m_previewWidget->resize(512, 512);
m_previewWidget->move(-128, -128);
m_previewWidget->enableEnvironmentLight();
m_previewWidget->setNotGraphics(true);
QFont nameFont;
nameFont.setWeight(QFont::Light);

View File

@ -308,7 +308,7 @@ void MaterialListWidget::copy()
Snapshot snapshot;
m_document->toSnapshot(&snapshot, emptySet, DocumentToSnapshotFor::Materials,
emptySet, emptySet, limitMaterialIds);
emptySet, limitMaterialIds);
QString snapshotXml;
QXmlStreamWriter xmlStreamWriter(&snapshotXml);
saveSkeletonToXmlStream(&snapshot, &xmlStreamWriter);

View File

@ -18,7 +18,7 @@ float ModelWidget::m_maxZoomRatio = 80.0;
int ModelWidget::m_defaultXRotation = 30 * 16;
int ModelWidget::m_defaultYRotation = -45 * 16;
int ModelWidget::m_defaultZRotation = 0;
QVector3D ModelWidget::m_defaultEyePosition = QVector3D(0, 0, -4.0);
QVector3D ModelWidget::m_defaultEyePosition = QVector3D(0, 0, -2.5);
ModelWidget::ModelWidget(QWidget *parent) :
QOpenGLWidget(parent),
@ -329,11 +329,11 @@ void ModelWidget::toggleUvCheck()
update();
}
bool ModelWidget::inputMousePressEventFromOtherWidget(QMouseEvent *event)
bool ModelWidget::inputMousePressEventFromOtherWidget(QMouseEvent *event, bool notGraphics)
{
bool shouldStartMove = false;
if (event->button() == Qt::LeftButton) {
if (QGuiApplication::queryKeyboardModifiers().testFlag(Qt::AltModifier) &&
if ((notGraphics || QGuiApplication::queryKeyboardModifiers().testFlag(Qt::AltModifier)) &&
!QGuiApplication::queryKeyboardModifiers().testFlag(Qt::ControlModifier)) {
shouldStartMove = m_moveEnabled;
}
@ -547,7 +547,7 @@ void ModelWidget::setMoveAndZoomByWindow(bool byWindow)
void ModelWidget::mousePressEvent(QMouseEvent *event)
{
inputMousePressEventFromOtherWidget(event);
inputMousePressEventFromOtherWidget(event, m_notGraphics);
}
void ModelWidget::mouseMoveEvent(QMouseEvent *event)
@ -565,3 +565,7 @@ void ModelWidget::mouseReleaseEvent(QMouseEvent *event)
inputMouseReleaseEventFromOtherWidget(event);
}
void ModelWidget::setNotGraphics(bool notGraphics)
{
m_notGraphics = notGraphics;
}

View File

@ -55,7 +55,7 @@ public:
void setMoveAndZoomByWindow(bool byWindow);
void disableCullFace();
void setMoveToPosition(const QVector3D &moveToPosition);
bool inputMousePressEventFromOtherWidget(QMouseEvent *event);
bool inputMousePressEventFromOtherWidget(QMouseEvent *event, bool notGraphics=false);
bool inputMouseMoveEventFromOtherWidget(QMouseEvent *event);
bool inputWheelEventFromOtherWidget(QWheelEvent *event);
bool inputMouseReleaseEventFromOtherWidget(QMouseEvent *event);
@ -64,6 +64,7 @@ public:
void updateToonNormalAndDepthMaps(QImage *normalMap, QImage *depthMap);
int widthInPixels();
int heightInPixels();
void setNotGraphics(bool notGraphics);
public slots:
void setXRotation(int angle);
void setYRotation(int angle);
@ -119,6 +120,7 @@ private:
QVector3D m_moveToPosition;
bool m_moveAndZoomByWindow = true;
bool m_enableCullFace = true;
bool m_notGraphics = false;
std::pair<QVector3D, QVector3D> screenPositionToMouseRay(const QPoint &screenPosition);
void updateProjectionMatrix();
public:

View File

@ -1,132 +0,0 @@
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include "motionclipwidget.h"
#include "posewidget.h"
#include "motionwidget.h"
MotionClipWidget::MotionClipWidget(const Document *document, QWidget *parent) :
QFrame(parent),
m_document(document)
{
setObjectName("MotionClipFrame");
}
QSize MotionClipWidget::preferredSize()
{
int preferredWidth = 0;
switch (m_clip.clipType) {
case MotionClipType::Motion:
preferredWidth = Theme::motionPreviewImageSize;
break;
case MotionClipType::Pose:
preferredWidth = Theme::posePreviewImageSize;
break;
case MotionClipType::ProceduralAnimation:
{
QPushButton testButton(ProceduralAnimationToDispName(m_clip.proceduralAnimation));
preferredWidth = testButton.sizeHint().width();
}
break;
case MotionClipType::Interpolation:
preferredWidth = Theme::normalButtonSize;
break;
default:
break;
}
return QSize(preferredWidth, maxSize().height());
}
QSize MotionClipWidget::maxSize()
{
auto maxWidth = std::max(Theme::posePreviewImageSize, Theme::motionPreviewImageSize);
auto maxHeight = std::max(PoseWidget::preferredHeight(), MotionWidget::preferredHeight());
return QSize(maxWidth, maxHeight);
}
void MotionClipWidget::setClip(MotionClip clip)
{
m_clip = clip;
reload();
}
void MotionClipWidget::reload()
{
if (nullptr != m_reloadToWidget)
m_reloadToWidget->deleteLater();
m_reloadToWidget = new QWidget(this);
m_reloadToWidget->setContentsMargins(1, 0, 0, 0);
m_reloadToWidget->setFixedSize(preferredSize());
m_reloadToWidget->show();
QVBoxLayout *layout = new QVBoxLayout;
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
layout->setSizeConstraint(QLayout::SetMinimumSize);
layout->addStretch();
switch (m_clip.clipType) {
case MotionClipType::Motion:
{
MotionWidget *motionWidget = new MotionWidget(m_document, m_clip.linkToId);
motionWidget->reload();
layout->addWidget(motionWidget);
}
break;
case MotionClipType::Pose:
{
PoseWidget *poseWidget = new PoseWidget(m_document, m_clip.linkToId);
poseWidget->reload();
layout->addWidget(poseWidget);
}
break;
case MotionClipType::ProceduralAnimation:
{
QPushButton *proceduralAnimationButton = new QPushButton(ProceduralAnimationToDispName(m_clip.proceduralAnimation));
proceduralAnimationButton->setFocusPolicy(Qt::NoFocus);
layout->addWidget(proceduralAnimationButton);
}
break;
case MotionClipType::Interpolation:
{
QHBoxLayout *interpolationButtonLayout = new QHBoxLayout;
QPushButton *interpolationButton = new QPushButton(QChar(fa::arrowsh));
Theme::initAwesomeButton(interpolationButton);
interpolationButtonLayout->setContentsMargins(0, 0, 0, 0);
interpolationButtonLayout->setSpacing(0);
interpolationButtonLayout->addStretch();
interpolationButtonLayout->addWidget(interpolationButton);
interpolationButtonLayout->addStretch();
layout->addLayout(interpolationButtonLayout);
connect(interpolationButton, &QPushButton::clicked, this, &MotionClipWidget::modifyInterpolation);
QHBoxLayout *interpolationDurationLayout = new QHBoxLayout;
QLabel *durationLabel = new QLabel;
durationLabel->setText(QString::number(m_clip.duration) + "s");
interpolationDurationLayout->setContentsMargins(0, 0, 0, 0);
interpolationDurationLayout->setSpacing(0);
interpolationDurationLayout->addStretch();
interpolationDurationLayout->addWidget(durationLabel);
interpolationDurationLayout->addStretch();
layout->addLayout(interpolationDurationLayout);
}
break;
default:
break;
}
layout->addStretch();
m_reloadToWidget->setLayout(layout);
}
void MotionClipWidget::updateCheckedState(bool checked)
{
if (checked)
setStyleSheet("#MotionClipFrame {border: 1px solid " + Theme::red.name() + ";}");
else
setStyleSheet("#MotionClipFrame {border: 1px solid transparent;}");
}

View File

@ -1,28 +0,0 @@
#ifndef DUST3D_MOTION_CLIP_WIDGET_H
#define DUST3D_MOTION_CLIP_WIDGET_H
#include <QFrame>
#include "document.h"
class MotionClipWidget : public QFrame
{
Q_OBJECT
signals:
void modifyInterpolation();
public:
MotionClipWidget(const Document *document, QWidget *parent=nullptr);
QSize preferredSize();
static QSize maxSize();
public slots:
void setClip(MotionClip clip);
void reload();
void updateCheckedState(bool checked);
private:
const Document *m_document = nullptr;
MotionClip m_clip;
QWidget *m_reloadToWidget = nullptr;
};
#endif

View File

@ -1,141 +1,54 @@
#include <QSpinBox>
#include <QDoubleSpinBox>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QGridLayout>
#include <QSlider>
#include <QThread>
#include <QTimer>
#include <QFormLayout>
#include <QMessageBox>
#include <QDebug>
#include <QStackedWidget>
#include <QScrollArea>
#include "motioneditwidget.h"
#include "simpleshaderwidget.h"
#include "simplerendermeshgenerator.h"
#include "motionsgenerator.h"
#include "util.h"
#include "poselistwidget.h"
#include "motionlistwidget.h"
#include "version.h"
#include "tabwidget.h"
#include "flowlayout.h"
#include "proceduralanimation.h"
#include "vertebratamotionparameterswidget.h"
MotionEditWidget::MotionEditWidget(const Document *document, QWidget *parent) :
QDialog(parent),
m_document(document)
MotionEditWidget::~MotionEditWidget()
{
setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint);
for (auto &it: m_frames)
delete it;
while (!m_renderQueue.empty()) {
delete m_renderQueue.front();
m_renderQueue.pop();
}
delete m_bones;
delete m_rigWeights;
delete m_outcome;
}
m_clipPlayer = new AnimationClipPlayer;
MotionEditWidget::MotionEditWidget()
{
m_modelRenderWidget = new SimpleShaderWidget;
m_timelineWidget = new MotionTimelineWidget(document, this);
m_parametersArea = new QScrollArea;
m_parametersArea->setFrameShape(QFrame::NoFrame);
connect(m_timelineWidget, &MotionTimelineWidget::clipsChanged, this, &MotionEditWidget::setUnsavedState);
connect(m_timelineWidget, &MotionTimelineWidget::clipsChanged, this, &MotionEditWidget::generatePreviews);
m_previewWidget = new ModelWidget(this);
m_previewWidget->setFixedSize(384, 384);
m_previewWidget->enableMove(true);
m_previewWidget->enableZoom(false);
m_previewWidget->move(-64, 0);
m_previewWidget->toggleWireframe();
connect(m_clipPlayer, &AnimationClipPlayer::frameReadyToShow, this, [=]() {
m_previewWidget->updateMesh(m_clipPlayer->takeFrameMesh());
});
PoseListWidget *poseListWidget = new PoseListWidget(document);
poseListWidget->setCornerButtonVisible(true);
poseListWidget->setHasContextMenu(false);
QWidget *poseListContainerWidget = new QWidget;
QGridLayout *poseListLayoutForContainer = new QGridLayout;
poseListLayoutForContainer->addWidget(poseListWidget);
poseListContainerWidget->setLayout(poseListLayoutForContainer);
poseListContainerWidget->resize(512, Theme::posePreviewImageSize);
connect(poseListWidget, &PoseListWidget::cornerButtonClicked, this, [=](QUuid poseId) {
m_timelineWidget->addPose(poseId);
});
//FlowLayout *proceduralAnimationListLayout = new FlowLayout;
//for (size_t i = 0; i < (int)ProceduralAnimation::Count - 1; ++i) {
// auto proceduralAnimation = (ProceduralAnimation)(i + 1);
// QString dispName = ProceduralAnimationToDispName(proceduralAnimation);
// QPushButton *addButton = new QPushButton(Theme::awesome()->icon(fa::plus), dispName);
// connect(addButton, &QPushButton::clicked, this, [=]() {
// m_timelineWidget->addProceduralAnimation(proceduralAnimation);
// });
// proceduralAnimationListLayout->addWidget(addButton);
//}
//QWidget *proceduralAnimationListContainerWidget = new QWidget;
//proceduralAnimationListContainerWidget->setLayout(proceduralAnimationListLayout);
//proceduralAnimationListContainerWidget->resize(512, Theme::motionPreviewImageSize);
MotionListWidget *motionListWidget = new MotionListWidget(document);
motionListWidget->setCornerButtonVisible(true);
motionListWidget->setHasContextMenu(false);
QWidget *motionListContainerWidget = new QWidget;
QGridLayout *motionListLayoutForContainer = new QGridLayout;
motionListLayoutForContainer->addWidget(motionListWidget);
motionListContainerWidget->setLayout(motionListLayoutForContainer);
motionListContainerWidget->resize(512, Theme::motionPreviewImageSize);
QStackedWidget *stackedWidget = new QStackedWidget;
stackedWidget->addWidget(poseListContainerWidget);
//stackedWidget->addWidget(proceduralAnimationListContainerWidget);
stackedWidget->addWidget(motionListContainerWidget);
connect(motionListWidget, &MotionListWidget::cornerButtonClicked, this, [=](QUuid motionId) {
m_timelineWidget->addMotion(motionId);
});
std::vector<QString> tabs = {
tr("Poses"),
//tr("Procedural Animations"),
tr("Motions")
};
TabWidget *tabWidget = new TabWidget(tabs);
tabWidget->setCurrentIndex(0);
connect(tabWidget, &TabWidget::currentIndexChanged, stackedWidget, &QStackedWidget::setCurrentIndex);
QVBoxLayout *motionEditLayout = new QVBoxLayout;
motionEditLayout->addWidget(tabWidget);
motionEditLayout->addWidget(stackedWidget);
motionEditLayout->addStretch();
motionEditLayout->addWidget(Theme::createHorizontalLineWidget());
motionEditLayout->addWidget(m_timelineWidget);
QSlider *speedModeSlider = new QSlider(Qt::Horizontal);
speedModeSlider->setFixedWidth(100);
speedModeSlider->setMaximum(2);
speedModeSlider->setMinimum(0);
speedModeSlider->setValue(1);
connect(speedModeSlider, &QSlider::valueChanged, this, [=](int value) {
m_clipPlayer->setSpeedMode((AnimationClipPlayer::SpeedMode)value);
});
QHBoxLayout *sliderLayout = new QHBoxLayout;
sliderLayout->addStretch();
sliderLayout->addSpacing(50);
sliderLayout->addWidget(new QLabel(tr("Slow")));
sliderLayout->addWidget(speedModeSlider);
sliderLayout->addWidget(new QLabel(tr("Fast")));
sliderLayout->addSpacing(50);
sliderLayout->addStretch();
QVBoxLayout *previewLayout = new QVBoxLayout;
previewLayout->addStretch();
previewLayout->addLayout(sliderLayout);
previewLayout->addSpacing(20);
QHBoxLayout *topLayout = new QHBoxLayout;
topLayout->addLayout(previewLayout);
topLayout->addWidget(Theme::createVerticalLineWidget());
topLayout->addLayout(motionEditLayout);
topLayout->setStretch(2, 1);
QHBoxLayout *canvasLayout = new QHBoxLayout;
canvasLayout->setSpacing(0);
canvasLayout->setContentsMargins(0, 0, 0, 0);
canvasLayout->addWidget(m_modelRenderWidget);
canvasLayout->addWidget(m_parametersArea);
canvasLayout->addSpacing(3);
canvasLayout->setStretch(0, 1);
m_nameEdit = new QLineEdit;
m_nameEdit->setFixedWidth(200);
connect(m_nameEdit, &QLineEdit::textChanged, this, &MotionEditWidget::setUnsavedState);
connect(m_nameEdit, &QLineEdit::textChanged, this, [=]() {
m_name = m_nameEdit->text();
m_unsaved = true;
updateTitle();
});
QPushButton *saveButton = new QPushButton(tr("Save"));
connect(saveButton, &QPushButton::clicked, this, &MotionEditWidget::save);
saveButton->setDefault(true);
@ -147,32 +60,67 @@ MotionEditWidget::MotionEditWidget(const Document *document, QWidget *parent) :
baseInfoLayout->addWidget(saveButton);
QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addLayout(topLayout);
mainLayout->addLayout(canvasLayout);
mainLayout->addWidget(Theme::createHorizontalLineWidget());
mainLayout->addLayout(baseInfoLayout);
setLayout(mainLayout);
QWidget *centralWidget = new QWidget;
centralWidget->setLayout(mainLayout);
setCentralWidget(centralWidget);
connect(this, &MotionEditWidget::addMotion, m_document, &Document::addMotion);
connect(this, &MotionEditWidget::renameMotion, m_document, &Document::renameMotion);
connect(this, &MotionEditWidget::setMotionClips, m_document, &Document::setMotionClips);
QTimer *timer = new QTimer(this);
timer->setInterval(17);
connect(timer, &QTimer::timeout, [this] {
if (m_renderQueue.size() > 600) {
checkRenderQueue();
return;
}
if (this->m_frames.empty())
return;
if (this->m_frameIndex < this->m_frames.size()) {
m_renderQueue.push(new SimpleShaderMesh(*this->m_frames[this->m_frameIndex]));
checkRenderQueue();
}
this->m_frameIndex = (this->m_frameIndex + 1) % this->m_frames.size();
});
timer->start();
connect(this, &MotionEditWidget::parametersChanged, this, &MotionEditWidget::updateParameters);
updateParametersArea();
generatePreview();
updateTitle();
}
MotionEditWidget::~MotionEditWidget()
void MotionEditWidget::updateParametersArea()
{
delete m_clipPlayer;
VertebrataMotionParametersWidget *widget = new VertebrataMotionParametersWidget(m_parameters);
connect(widget, &VertebrataMotionParametersWidget::parametersChanged, this, [=]() {
this->m_parameters = widget->getParameters();
emit parametersChanged();
});
m_parametersArea->setWidget(widget);
}
QSize MotionEditWidget::sizeHint() const
{
return QSize(1024, 768);
return QSize(650, 460);
}
void MotionEditWidget::reject()
void MotionEditWidget::updateParameters()
{
close();
m_unsaved = true;
generatePreview();
updateTitle();
}
void MotionEditWidget::updateTitle()
{
if (m_motionId.isNull()) {
setWindowTitle(unifiedWindowTitle(tr("New") + (m_unsaved ? "*" : "")));
return;
}
setWindowTitle(unifiedWindowTitle(m_name + (m_unsaved ? "*" : "")));
}
void MotionEditWidget::closeEvent(QCloseEvent *event)
@ -190,23 +138,18 @@ void MotionEditWidget::closeEvent(QCloseEvent *event)
}
m_closed = true;
hide();
if (nullptr != m_previewsGenerator) {
if (nullptr != m_previewGenerator) {
event->ignore();
return;
}
event->accept();
}
void MotionEditWidget::save()
void MotionEditWidget::setEditMotionName(const QString &name)
{
if (m_motionId.isNull()) {
m_motionId = QUuid::createUuid();
emit addMotion(m_motionId, m_nameEdit->text(), m_timelineWidget->clips());
} else if (m_unsaved) {
emit renameMotion(m_motionId, m_nameEdit->text());
emit setMotionClips(m_motionId, m_timelineWidget->clips());
}
clearUnsaveState();
m_name = name;
m_nameEdit->setText(name);
updateTitle();
}
void MotionEditWidget::clearUnsaveState()
@ -215,13 +158,7 @@ void MotionEditWidget::clearUnsaveState()
updateTitle();
}
void MotionEditWidget::setUnsavedState()
{
m_unsaved = true;
updateTitle();
}
void MotionEditWidget::setEditMotionId(QUuid motionId)
void MotionEditWidget::setEditMotionId(const QUuid &motionId)
{
if (m_motionId == motionId)
return;
@ -230,77 +167,106 @@ void MotionEditWidget::setEditMotionId(QUuid motionId)
updateTitle();
}
void MotionEditWidget::setEditMotionName(QString name)
void MotionEditWidget::setEditMotionParameters(const std::map<QString, QString> &parameters)
{
m_nameEdit->setText(name);
updateTitle();
m_parameters = parameters;
updateParametersArea();
generatePreview();
}
void MotionEditWidget::updateTitle()
void MotionEditWidget::save()
{
if (m_motionId.isNull()) {
setWindowTitle(unifiedWindowTitle(tr("New") + (m_unsaved ? "*" : "")));
return;
m_motionId = QUuid::createUuid();
emit addMotion(m_motionId, m_name, m_parameters);
} else if (m_unsaved) {
emit renameMotion(m_motionId, m_name);
emit setMotionParameters(m_motionId, m_parameters);
}
const Motion *motion = m_document->findMotion(m_motionId);
if (nullptr == motion) {
qDebug() << "Find motion failed:" << m_motionId;
return;
}
setWindowTitle(unifiedWindowTitle(motion->name + (m_unsaved ? "*" : "")));
clearUnsaveState();
}
void MotionEditWidget::setEditMotionClips(std::vector<MotionClip> clips)
void MotionEditWidget::updateBones(RigType rigType,
const std::vector<RiggerBone> *rigBones,
const std::map<int, RiggerVertexWeights> *rigWeights,
const Outcome *outcome)
{
m_timelineWidget->setClips(clips);
m_rigType = rigType;
delete m_bones;
m_bones = nullptr;
delete m_rigWeights;
m_rigWeights = nullptr;
delete m_outcome;
m_outcome = nullptr;
if (nullptr != rigBones &&
nullptr != rigWeights &&
nullptr != outcome) {
m_bones = new std::vector<RiggerBone>(*rigBones);
m_rigWeights = new std::map<int, RiggerVertexWeights>(*rigWeights);
m_outcome = new Outcome(*outcome);
generatePreview();
}
}
void MotionEditWidget::generatePreviews()
void MotionEditWidget::generatePreview()
{
if (nullptr != m_previewsGenerator) {
m_isPreviewsObsolete = true;
if (nullptr != m_previewGenerator) {
m_isPreviewObsolete = true;
return;
}
m_isPreviewsObsolete = false;
m_isPreviewObsolete = false;
const std::vector<RiggerBone> *rigBones = m_document->resultRigBones();
const std::map<int, RiggerVertexWeights> *rigWeights = m_document->resultRigWeights();
if (nullptr == rigBones || nullptr == rigWeights) {
if (RigType::None == m_rigType || nullptr == m_bones || nullptr == m_rigWeights || nullptr == m_outcome)
return;
}
m_previewsGenerator = new MotionsGenerator(m_document->rigType, rigBones, rigWeights,
m_document->currentRiggedOutcome());
for (const auto &pose: m_document->poseMap)
m_previewsGenerator->addPoseToLibrary(pose.first, pose.second.frames, pose.second.yTranslationScale);
for (const auto &motion: m_document->motionMap)
m_previewsGenerator->addMotionToLibrary(motion.first, motion.second.clips);
m_previewsGenerator->addMotionToLibrary(QUuid(), m_timelineWidget->clips());
m_previewsGenerator->addRequirement(QUuid());
QThread *thread = new QThread;
m_previewsGenerator->moveToThread(thread);
connect(thread, &QThread::started, m_previewsGenerator, &MotionsGenerator::process);
connect(m_previewsGenerator, &MotionsGenerator::finished, this, &MotionEditWidget::previewsReady);
connect(m_previewsGenerator, &MotionsGenerator::finished, thread, &QThread::quit);
m_previewGenerator = new MotionsGenerator(m_rigType, *m_bones, *m_rigWeights, *m_outcome);
m_previewGenerator->enablePreviewMeshes();
m_previewGenerator->addMotion(QUuid(), m_parameters);
m_previewGenerator->moveToThread(thread);
connect(thread, &QThread::started, m_previewGenerator, &MotionsGenerator::process);
connect(m_previewGenerator, &MotionsGenerator::finished, this, &MotionEditWidget::previewReady);
connect(m_previewGenerator, &MotionsGenerator::finished, thread, &QThread::quit);
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
thread->start();
}
void MotionEditWidget::previewsReady()
void MotionEditWidget::previewReady()
{
auto resultPreviewMeshs = m_previewsGenerator->takeResultPreviewMeshs(QUuid());
m_clipPlayer->updateFrameMeshes(resultPreviewMeshs);
for (auto &it: m_frames)
delete it;
m_frames.clear();
delete m_previewsGenerator;
m_previewsGenerator = nullptr;
std::vector<std::pair<float, SimpleShaderMesh *>> frames = m_previewGenerator->takeResultPreviewMeshes(QUuid());
for (const auto &frame: frames) {
m_frames.push_back(frame.second);
}
delete m_previewGenerator;
m_previewGenerator = nullptr;
if (m_closed) {
close();
return;
}
if (m_isPreviewsObsolete)
generatePreviews();
if (m_isPreviewObsolete)
generatePreview();
}
void MotionEditWidget::checkRenderQueue()
{
if (m_renderQueue.empty())
return;
SimpleShaderMesh *mesh = m_renderQueue.front();
m_renderQueue.pop();
m_modelRenderWidget->updateMesh(mesh);
}

View File

@ -1,50 +1,68 @@
#ifndef DUST3D_MOTION_EDIT_WIDGET_H
#define DUST3D_MOTION_EDIT_WIDGET_H
#include <QDialog>
#include <QLineEdit>
#include <QMainWindow>
#include <QCloseEvent>
#include "document.h"
#include "motiontimelinewidget.h"
#include "modelwidget.h"
#include "motionsgenerator.h"
#include "animationclipplayer.h"
#include <queue>
#include <QLineEdit>
#include <QUuid>
#include "vertebratamotion.h"
#include "rigger.h"
#include "outcome.h"
class MotionEditWidget : public QDialog
class SimpleShaderWidget;
class MotionsGenerator;
class SimpleShaderMesh;
class QScrollArea;
class MotionEditWidget : public QMainWindow
{
Q_OBJECT
signals:
void addMotion(QUuid motionId, QString name, std::vector<MotionClip> clips);
void setMotionClips(QUuid motionId, std::vector<MotionClip> clips);
void renameMotion(QUuid motionId, QString name);
public:
MotionEditWidget(const Document *document, QWidget *parent=nullptr);
MotionEditWidget();
~MotionEditWidget();
protected:
void closeEvent(QCloseEvent *event) override;
void reject() override;
QSize sizeHint() const override;
signals:
void parametersChanged();
void addMotion(const QUuid &motionId, const QString &name, const std::map<QString, QString> &parameters);
void removeMotion(const QUuid &motionId);
void setMotionParameters(const QUuid &motionId, const std::map<QString, QString> &parameters);
void renameMotion(const QUuid &motionId, const QString &name);
public slots:
void checkRenderQueue();
void generatePreview();
void previewReady();
void updateBones(RigType rigType,
const std::vector<RiggerBone> *rigBones,
const std::map<int, RiggerVertexWeights> *rigWeights,
const Outcome *outcome);
void setEditMotionName(const QString &name);
void setEditMotionId(const QUuid &motionId);
void setEditMotionParameters(const std::map<QString, QString> &parameters);
void updateTitle();
void save();
void clearUnsaveState();
void setEditMotionId(QUuid poseId);
void setEditMotionName(QString name);
void setEditMotionClips(std::vector<MotionClip> clips);
void setUnsavedState();
void generatePreviews();
void previewsReady();
void updateParameters();
void updateParametersArea();
protected:
QSize sizeHint() const override;
void closeEvent(QCloseEvent *event) override;
private:
const Document *m_document = nullptr;
MotionTimelineWidget *m_timelineWidget = nullptr;
ModelWidget *m_previewWidget = nullptr;
QUuid m_motionId;
QString m_name;
std::map<QString, QString> m_parameters;
SimpleShaderWidget *m_modelRenderWidget = nullptr;
std::queue<SimpleShaderMesh *> m_renderQueue;
MotionsGenerator *m_previewGenerator = nullptr;
bool m_isPreviewObsolete = false;
std::vector<SimpleShaderMesh *> m_frames;
size_t m_frameIndex = 0;
RigType m_rigType = RigType::None;
std::vector<RiggerBone> *m_bones = nullptr;
std::map<int, RiggerVertexWeights> *m_rigWeights = nullptr;
Outcome *m_outcome = nullptr;
QLineEdit *m_nameEdit = nullptr;
std::vector<std::pair<float, QUuid>> m_keyframes;
bool m_unsaved = false;
bool m_closed = false;
bool m_isPreviewsObsolete = false;
MotionsGenerator *m_previewsGenerator = nullptr;
AnimationClipPlayer *m_clipPlayer = nullptr;
QUuid m_motionId;
QScrollArea *m_parametersArea = nullptr;
};
#endif

View File

@ -112,7 +112,8 @@ void MotionListWidget::mousePressEvent(QMouseEvent *event)
bool startAdd = false;
bool stopAdd = false;
std::vector<QUuid> waitQueue;
for (const auto &childId: m_document->motionIdList) {
for (const auto &motionIt: m_document->motionMap) {
const auto &childId = motionIt.first;
if (m_shiftStartMotionId == childId || motionId == childId) {
if (startAdd) {
stopAdd = true;
@ -169,9 +170,9 @@ void MotionListWidget::showContextMenu(const QPoint &pos)
unorderedMotionIds.insert(m_currentSelectedMotionId);
std::vector<QUuid> motionIds;
for (const auto &cand: m_document->motionIdList) {
if (unorderedMotionIds.find(cand) != unorderedMotionIds.end())
motionIds.push_back(cand);
for (const auto &cand: m_document->motionMap) {
if (unorderedMotionIds.find(cand.first) != unorderedMotionIds.end())
motionIds.push_back(cand.first);
}
QAction modifyAction(tr("Modify"), this);
@ -240,7 +241,9 @@ void MotionListWidget::reload()
for (int i = 0; i < columns; i++)
setColumnWidth(i, columnWidth);
std::vector<QUuid> orderedMotionIdList = m_document->motionIdList;
std::vector<QUuid> orderedMotionIdList;
for (const auto &motionIt: m_document->motionMap)
orderedMotionIdList.push_back(motionIt.first);
std::sort(orderedMotionIdList.begin(), orderedMotionIdList.end(), [&](const QUuid &firstMotionId, const QUuid &secondMotionId) {
const auto *firstMotion = m_document->findMotion(firstMotionId);
const auto *secondMotion = m_document->findMotion(secondMotionId);

View File

@ -2,9 +2,9 @@
#include <QVBoxLayout>
#include <QPushButton>
#include "motionmanagewidget.h"
#include "motioneditwidget.h"
#include "theme.h"
#include "infolabel.h"
#include "motioneditwidget.h"
MotionManageWidget::MotionManageWidget(const Document *document, QWidget *parent) :
QWidget(parent),
@ -65,21 +65,28 @@ void MotionManageWidget::showAddMotionDialog()
void MotionManageWidget::showMotionDialog(QUuid motionId)
{
MotionEditWidget *motionEditWidget = new MotionEditWidget(m_document);
MotionEditWidget *motionEditWidget = new MotionEditWidget;
motionEditWidget->setAttribute(Qt::WA_DeleteOnClose);
motionEditWidget->updateBones(m_document->rigType,
m_document->resultRigBones(),
m_document->resultRigWeights(),
&m_document->currentRiggedOutcome());
if (!motionId.isNull()) {
const Motion *motion = m_document->findMotion(motionId);
if (nullptr != motion) {
motionEditWidget->setEditMotionId(motionId);
motionEditWidget->setEditMotionName(motion->name);
motionEditWidget->setEditMotionClips(motion->clips);
motionEditWidget->setEditMotionParameters(motion->parameters);
motionEditWidget->clearUnsaveState();
motionEditWidget->generatePreviews();
motionEditWidget->generatePreview();
}
}
motionEditWidget->show();
connect(motionEditWidget, &QDialog::destroyed, [=]() {
connect(motionEditWidget, &QMainWindow::destroyed, [=]() {
emit unregisterDialog((QWidget *)motionEditWidget);
});
connect(motionEditWidget, &MotionEditWidget::addMotion, m_document, &Document::addMotion);
connect(motionEditWidget, &MotionEditWidget::renameMotion, m_document, &Document::renameMotion);
connect(motionEditWidget, &MotionEditWidget::setMotionParameters, m_document, &Document::setMotionParameters);
emit registerDialog((QWidget *)motionEditWidget);
}

View File

@ -1,59 +1,50 @@
#include <QGuiApplication>
#include <QElapsedTimer>
#include <cmath>
#include <QRegularExpression>
#include <QMatrix4x4>
#include "motionsgenerator.h"
#include "posemeshcreator.h"
#include "poserconstruct.h"
#include "posedocument.h"
#include "boundingboxmesh.h"
#include "vertebratamotion.h"
#include "blockmesh.h"
#include "vertebratamotionparameterswidget.h"
#include "util.h"
MotionsGenerator::MotionsGenerator(RigType rigType,
const std::vector<RiggerBone> *rigBones,
const std::map<int, RiggerVertexWeights> *rigWeights,
const std::vector<RiggerBone> &bones,
const std::map<int, RiggerVertexWeights> &rigWeights,
const Outcome &outcome) :
m_rigType(rigType),
m_rigBones(*rigBones),
m_rigWeights(*rigWeights),
m_bones(bones),
m_rigWeights(rigWeights),
m_outcome(outcome)
{
}
MotionsGenerator::~MotionsGenerator()
{
for (auto &item: m_resultPreviewMeshs) {
for (auto &it: m_resultSnapshotMeshes)
delete it.second;
for (auto &item: m_resultPreviewMeshes) {
for (auto &subItem: item.second) {
delete subItem.second;
}
}
#if ENABLE_PROCEDURAL_DEBUG
for (const auto &item: m_proceduralDebugPreviews) {
for (const auto &subItem: item.second) {
delete subItem;
}
}
#endif
delete m_poser;
}
void MotionsGenerator::addPoseToLibrary(const QUuid &poseId, const std::vector<std::pair<std::map<QString, QString>, std::map<QString, std::map<QString, QString>>>> &frames, float yTranslationScale)
void MotionsGenerator::enablePreviewMeshes()
{
m_poses[poseId] = frames;
m_posesYtranslationScales[poseId] = yTranslationScale;
m_previewMeshesEnabled = true;
}
void MotionsGenerator::addMotionToLibrary(const QUuid &motionId, const std::vector<MotionClip> &clips)
void MotionsGenerator::enableSnapshotMeshes()
{
m_motions[motionId] = clips;
m_snapshotMeshesEnabled = true;
}
void MotionsGenerator::addRequirement(const QUuid &motionId)
void MotionsGenerator::addMotion(const QUuid &motionId, const std::map<QString, QString> &parameters)
{
m_requiredMotionIds.insert(motionId);
}
const std::set<QUuid> &MotionsGenerator::requiredMotionIds()
{
return m_requiredMotionIds;
m_motions[motionId] = parameters;
}
const std::set<QUuid> &MotionsGenerator::generatedMotionIds()
@ -61,249 +52,23 @@ const std::set<QUuid> &MotionsGenerator::generatedMotionIds()
return m_generatedMotionIds;
}
std::vector<MotionClip> *MotionsGenerator::findMotionClips(const QUuid &motionId)
Model *MotionsGenerator::takeResultSnapshotMesh(const QUuid &motionId)
{
auto findMotionResult = m_motions.find(motionId);
if (findMotionResult == m_motions.end())
auto findResult = m_resultSnapshotMeshes.find(motionId);
if (findResult == m_resultSnapshotMeshes.end())
return nullptr;
std::vector<MotionClip> &clips = findMotionResult->second;
return &clips;
auto result = findResult->second;
m_resultSnapshotMeshes.erase(findResult);
return result;
}
std::vector<std::pair<std::map<QString, QString>, std::map<QString, std::map<QString, QString>>>> *MotionsGenerator::findPoseFrames(const QUuid &poseId)
std::vector<std::pair<float, SimpleShaderMesh *>> MotionsGenerator::takeResultPreviewMeshes(const QUuid &motionId)
{
auto findPoseResult = m_poses.find(poseId);
if (findPoseResult == m_poses.end())
return nullptr;
std::vector<std::pair<std::map<QString, QString>, std::map<QString, std::map<QString, QString>>>> &frames = findPoseResult->second;
return &frames;
}
void MotionsGenerator::generatePreviewsForOutcomes(const std::vector<std::pair<float, JointNodeTree>> &outcomes, std::vector<std::pair<float, Model *>> &previews)
{
for (const auto &item: outcomes) {
PoseMeshCreator *poseMeshCreator = new PoseMeshCreator(item.second.nodes(), m_outcome, m_rigWeights);
poseMeshCreator->createMesh();
previews.push_back({item.first, poseMeshCreator->takeResultMesh()});
delete poseMeshCreator;
}
}
float MotionsGenerator::calculatePoseDuration(const QUuid &poseId)
{
const std::vector<std::pair<std::map<QString, QString>, std::map<QString, std::map<QString, QString>>>> *pose = findPoseFrames(poseId);
if (nullptr == pose)
return 0;
float totalDuration = 0;
if (pose->size() > 1) {
// Pose with only one frame has zero duration
for (const auto &frame: *pose) {
totalDuration += valueOfKeyInMapOrEmpty(frame.first, "duration").toFloat();
}
}
return totalDuration;
}
float MotionsGenerator::calculateMotionDuration(const QUuid &motionId, std::set<QUuid> &visited)
{
const std::vector<MotionClip> *motionClips = findMotionClips(motionId);
if (!motionClips || motionClips->empty())
return 0;
if (visited.find(motionId) != visited.end()) {
qDebug() << "Found recursive motion link";
return 0;
}
float totalDuration = 0;
visited.insert(motionId);
for (int clipIndex = 0; clipIndex < (int)(*motionClips).size(); ++clipIndex) {
const auto &clip = (*motionClips)[clipIndex];
if (clip.clipType == MotionClipType::Interpolation)
totalDuration += clip.duration;
else if (clip.clipType == MotionClipType::Pose)
totalDuration += calculatePoseDuration(clip.linkToId);
else if (clip.clipType == MotionClipType::Motion)
totalDuration += calculateMotionDuration(clip.linkToId, visited);
}
return totalDuration;
}
void MotionsGenerator::generateMotion(const QUuid &motionId, std::set<QUuid> &visited, std::vector<std::pair<float, JointNodeTree>> &outcomes, std::vector<Model *> *previews)
{
if (visited.find(motionId) != visited.end()) {
qDebug() << "Found recursive motion link";
return;
}
visited.insert(motionId);
std::vector<MotionClip> *motionClips = findMotionClips(motionId);
if (!motionClips || motionClips->empty())
return;
std::vector<float> timePoints;
float totalDuration = 0;
for (int clipIndex = 0; clipIndex < (int)(*motionClips).size(); ++clipIndex) {
auto &clip = (*motionClips)[clipIndex];
if (clip.clipType == MotionClipType::Motion) {
std::set<QUuid> subVisited;
clip.duration = calculateMotionDuration(clip.linkToId, subVisited);
} else if (clip.clipType == MotionClipType::Pose) {
clip.duration = calculatePoseDuration(clip.linkToId);
}
timePoints.push_back(totalDuration);
totalDuration += clip.duration;
}
auto findClipIndexByProgress = [=](float progress) {
for (size_t i = 0; i < timePoints.size(); ++i) {
if (progress >= timePoints[i] && i + 1 < timePoints.size() && progress <= timePoints[i + 1])
return (int)i;
}
return (int)timePoints.size() - 1;
};
float interval = 1.0 / m_fps;
float lastProgress = 0;
if (totalDuration < interval)
totalDuration = interval;
for (float progress = 0; progress < totalDuration; ) {
int clipIndex = findClipIndexByProgress(progress);
if (-1 == clipIndex) {
qDebug() << "findClipIndexByProgress failed, progress:" << progress << "total duration:" << totalDuration << "interval:" << interval;
break;
}
float clipLocalProgress = progress - timePoints[clipIndex];
const MotionClip &progressClip = (*motionClips)[clipIndex];
if (MotionClipType::Interpolation == progressClip.clipType) {
if (clipIndex <= 0) {
qDebug() << "Clip type is interpolation, but clip sit at begin";
break;
}
if (clipIndex >= (int)motionClips->size() - 1) {
qDebug() << "Clip type is interpolation, but clip sit at end";
break;
}
const JointNodeTree *beginJointNodeTree = findClipEndJointNodeTree((*motionClips)[clipIndex - 1]);
if (nullptr == beginJointNodeTree) {
qDebug() << "findClipEndJointNodeTree failed";
break;
}
const JointNodeTree *endJointNodeTree = nullptr;
if (MotionClipType::ProceduralAnimation == (*motionClips)[clipIndex + 1].clipType) {
endJointNodeTree = beginJointNodeTree;
} else {
endJointNodeTree = findClipBeginJointNodeTree((*motionClips)[clipIndex + 1]);
if (nullptr == endJointNodeTree) {
qDebug() << "findClipBeginJointNodeTree failed";
break;
}
}
outcomes.push_back({progress - lastProgress,
generateInterpolation(progressClip.interpolationType, *beginJointNodeTree, *endJointNodeTree, clipLocalProgress / std::max((float)0.0001, progressClip.duration))});
lastProgress = progress;
progress += interval;
continue;
} else if (MotionClipType::Pose == progressClip.clipType) {
const auto &frames = findPoseFrames(progressClip.linkToId);
float clipDuration = std::max((float)0.0001, progressClip.duration);
int frame = clipLocalProgress * frames->size() / clipDuration;
if (frame >= (int)frames->size())
frame = frames->size() - 1;
int previousFrame = frame - 1;
if (previousFrame < 0)
previousFrame = frames->size() - 1;
int nextFrame = frame + 1;
if (nextFrame >= (int)frames->size())
nextFrame = 0;
if (frame >= 0 && frame < (int)frames->size()) {
const JointNodeTree previousJointNodeTree = poseJointNodeTree(progressClip.linkToId, previousFrame);
const JointNodeTree jointNodeTree = poseJointNodeTree(progressClip.linkToId, frame);
const JointNodeTree nextJointNodeTree = poseJointNodeTree(progressClip.linkToId, nextFrame);
const JointNodeTree middleJointNodeTree = generateInterpolation(InterpolationType::Linear, previousJointNodeTree, nextJointNodeTree, 0.5);
outcomes.push_back({progress - lastProgress,
generateInterpolation(InterpolationType::Linear, jointNodeTree, middleJointNodeTree, 0.75)});
lastProgress = progress;
}
progress += interval;
continue;
} else if (MotionClipType::Motion == progressClip.clipType) {
generateMotion(progressClip.linkToId, visited, outcomes);
progress += progressClip.duration;
continue;
}
progress += interval;
}
}
JointNodeTree MotionsGenerator::generateInterpolation(InterpolationType interpolationType, const JointNodeTree &first, const JointNodeTree &second, float progress)
{
return JointNodeTree::slerp(first, second, calculateInterpolation(interpolationType, progress));
}
const JointNodeTree &MotionsGenerator::poseJointNodeTree(const QUuid &poseId, int frame)
{
auto findResult = m_poseJointNodeTreeMap.find({poseId, frame});
if (findResult != m_poseJointNodeTreeMap.end())
return findResult->second;
const auto &frames = m_poses[poseId];
const auto &posesYtranslationScale = m_posesYtranslationScales[poseId];
m_poser->reset();
if (frame < (int)frames.size()) {
const auto &parameters = frames[frame].second;
PoseDocument postDocument;
postDocument.fromParameters(&m_rigBones, parameters);
std::map<QString, std::map<QString, QString>> translatedParameters;
postDocument.toParameters(translatedParameters);
m_poser->parameters() = translatedParameters;
m_poser->setYtranslationScale(posesYtranslationScale);
}
m_poser->commit();
auto insertResult = m_poseJointNodeTreeMap.insert({{poseId, frame}, m_poser->resultJointNodeTree()});
return insertResult.first->second;
}
const JointNodeTree *MotionsGenerator::findClipBeginJointNodeTree(const MotionClip &clip)
{
if (MotionClipType::Pose == clip.clipType) {
const JointNodeTree &jointNodeTree = poseJointNodeTree(clip.linkToId, 0);
return &jointNodeTree;
} else if (MotionClipType::Motion == clip.clipType) {
const std::vector<MotionClip> *motionClips = findMotionClips(clip.linkToId);
if (nullptr != motionClips && !motionClips->empty()) {
return findClipBeginJointNodeTree((*motionClips)[0]);
}
return nullptr;
}
return nullptr;
}
const JointNodeTree *MotionsGenerator::findClipEndJointNodeTree(const MotionClip &clip)
{
if (MotionClipType::Pose == clip.clipType) {
const std::vector<std::pair<std::map<QString, QString>, std::map<QString, std::map<QString, QString>>>> *poseFrames = findPoseFrames(clip.linkToId);
if (nullptr != poseFrames && !poseFrames->empty()) {
return &poseJointNodeTree(clip.linkToId, poseFrames->size() - 1);
}
return nullptr;
} else if (MotionClipType::Motion == clip.clipType) {
const std::vector<MotionClip> *motionClips = findMotionClips(clip.linkToId);
if (nullptr != motionClips && !motionClips->empty()) {
return findClipEndJointNodeTree((*motionClips)[motionClips->size() - 1]);
}
return nullptr;
}
return nullptr;
}
std::vector<std::pair<float, Model *>> MotionsGenerator::takeResultPreviewMeshs(const QUuid &motionId)
{
auto findResult = m_resultPreviewMeshs.find(motionId);
if (findResult == m_resultPreviewMeshs.end())
auto findResult = m_resultPreviewMeshes.find(motionId);
if (findResult == m_resultPreviewMeshes.end())
return {};
auto result = findResult->second;
m_resultPreviewMeshs.erase(findResult);
m_resultPreviewMeshes.erase(findResult);
return result;
}
@ -315,40 +80,341 @@ std::vector<std::pair<float, JointNodeTree>> MotionsGenerator::takeResultJointNo
return findResult->second;
}
void MotionsGenerator::generate()
void MotionsGenerator::generateMotion(const QUuid &motionId)
{
m_poser = newPoser(m_rigType, m_rigBones);
if (nullptr == m_poser)
if (m_bones.empty())
return;
for (const auto &motionId: m_requiredMotionIds) {
std::set<QUuid> visited;
#if ENABLE_PROCEDURAL_DEBUG
std::vector<Model *> previews;
generateMotion(motionId, visited, m_resultJointNodeTrees[motionId], &previews);
#else
generateMotion(motionId, visited, m_resultJointNodeTrees[motionId]);
#endif
generatePreviewsForOutcomes(m_resultJointNodeTrees[motionId], m_resultPreviewMeshs[motionId]);
#if ENABLE_PROCEDURAL_DEBUG
if (!previews.empty()) {
const auto &tree = m_resultJointNodeTrees[motionId];
auto &target = m_resultPreviewMeshs[motionId];
for (size_t i = 0; i < tree.size() && i < previews.size(); ++i) {
int edgeVertexCount = previews[i]->edgeVertexCount();
if (0 == edgeVertexCount)
std::map<QString, std::vector<int>> chains;
QRegularExpression reJoints("^([a-zA-Z]+\\d*)_Joint\\d+$");
QRegularExpression reSpine("^([a-zA-Z]+)\\d*$");
for (int index = 0; index < (int)m_bones.size(); ++index) {
const auto &bone = m_bones[index];
const auto &item = bone.name;
QRegularExpressionMatch match = reJoints.match(item);
if (match.hasMatch()) {
QString name = match.captured(1);
chains[name].push_back(index);
} else {
match = reSpine.match(item);
if (match.hasMatch()) {
QString name = match.captured(1);
if (item.startsWith(name + "0"))
chains[name + "0"].push_back(index);
else
chains[name].push_back(index);
} else if (item.startsWith("Virtual_")) {
//qDebug() << "Ignore connector:" << item;
} else {
qDebug() << "Unrecognized bone name:" << item;
}
}
}
std::vector<std::pair<int, bool>> spineBones;
auto findSpine = chains.find("Spine");
if (findSpine != chains.end()) {
for (const auto &it: findSpine->second) {
spineBones.push_back({it, true});
}
}
auto findNeck = chains.find("Neck");
if (findNeck != chains.end()) {
for (const auto &it: findNeck->second) {
spineBones.push_back({it, true});
}
}
std::reverse(spineBones.begin(), spineBones.end());
auto findSpine0 = chains.find("Spine0");
if (findSpine0 != chains.end()) {
for (const auto &it: findSpine0->second) {
spineBones.push_back({it, false});
}
}
auto findTail = chains.find("Tail");
if (findTail != chains.end()) {
for (const auto &it: findTail->second) {
spineBones.push_back({it, false});
}
}
double radiusScale = 0.5;
std::vector<VertebrataMotion::Node> spineNodes;
if (!spineBones.empty()) {
const auto &it = spineBones[0];
if (it.second) {
spineNodes.push_back({m_bones[it.first].tailPosition,
m_bones[it.first].tailRadius * radiusScale,
it.first,
true});
} else {
spineNodes.push_back({m_bones[it.first].headPosition,
m_bones[it.first].headRadius * radiusScale,
it.first,
false});
}
}
std::map<int, size_t> spineBoneToNodeMap;
for (size_t i = 0; i < spineBones.size(); ++i) {
const auto &it = spineBones[i];
if (it.second) {
spineBoneToNodeMap[it.first] = i;
if (m_bones[it.first].name == "Spine1")
spineBoneToNodeMap[0] = spineNodes.size();
spineNodes.push_back({m_bones[it.first].headPosition,
m_bones[it.first].headRadius * radiusScale,
it.first,
false});
} else {
spineNodes.push_back({m_bones[it.first].tailPosition,
m_bones[it.first].tailRadius * radiusScale,
it.first,
true});
}
}
VertebrataMotion *vertebrataMotion = new VertebrataMotion;
VertebrataMotion::Parameters parameters =
VertebrataMotionParametersWidget::toVertebrataMotionParameters(m_motions[motionId]);
if ("Vertical" == valueOfKeyInMapOrEmpty(m_bones[0].attributes, "spineDirection"))
parameters.biped = true;
vertebrataMotion->setParameters(parameters);
vertebrataMotion->setSpineNodes(spineNodes);
double groundY = std::numeric_limits<double>::max();
for (const auto &it: spineNodes) {
if (it.position.y() - it.radius < groundY)
groundY = it.position.y() - it.radius;
}
for (const auto &chain: chains) {
std::vector<VertebrataMotion::Node> legNodes;
VertebrataMotion::Side side;
if (chain.first.startsWith("LeftLimb")) {
side = VertebrataMotion::Side::Left;
} else if (chain.first.startsWith("RightLimb")) {
side = VertebrataMotion::Side::Right;
} else {
continue;
ShaderVertex *source = previews[i]->edgeVertices();
ShaderVertex *edgeVertices = new ShaderVertex[edgeVertexCount];
for (int j = 0; j < edgeVertexCount; ++j) {
edgeVertices[j] = source[j];
}
target[i].second->updateEdges(edgeVertices, edgeVertexCount);
//target[i].second->updateTriangleVertices(nullptr, 0);
int virtualBoneIndex = m_bones[chain.second[0]].parent;
if (-1 == virtualBoneIndex)
continue;
int spineBoneIndex = m_bones[virtualBoneIndex].parent;
auto findSpine = spineBoneToNodeMap.find(spineBoneIndex);
if (findSpine == spineBoneToNodeMap.end())
continue;
legNodes.push_back({m_bones[virtualBoneIndex].headPosition,
m_bones[virtualBoneIndex].headRadius * radiusScale,
virtualBoneIndex,
false});
legNodes.push_back({m_bones[virtualBoneIndex].tailPosition,
m_bones[virtualBoneIndex].tailRadius * radiusScale,
virtualBoneIndex,
true});
for (const auto &it: chain.second) {
legNodes.push_back({m_bones[it].tailPosition,
m_bones[it].tailRadius * radiusScale,
it,
true});
}
vertebrataMotion->setLegNodes(findSpine->second, side, legNodes);
for (const auto &it: legNodes) {
if (it.position.y() - it.radius < groundY)
groundY = it.position.y() - it.radius;
}
}
#endif
m_generatedMotionIds.insert(motionId);
vertebrataMotion->setGroundY(groundY);
vertebrataMotion->generate();
std::vector<std::pair<float, JointNodeTree>> jointNodeTrees;
std::vector<std::pair<float, SimpleShaderMesh *>> previewMeshes;
Model *snapshotMesh = nullptr;
std::vector<QMatrix4x4> bindTransforms(m_bones.size());
for (size_t i = 0; i < m_bones.size(); ++i) {
const auto &bone = m_bones[i];
QMatrix4x4 parentMatrix;
QMatrix4x4 translationMatrix;
if (-1 != bone.parent) {
const auto &parentBone = m_bones[bone.parent];
parentMatrix = bindTransforms[bone.parent];
translationMatrix.translate(bone.headPosition - parentBone.headPosition);
} else {
translationMatrix.translate(bone.headPosition);
}
bindTransforms[i] = parentMatrix * translationMatrix;
}
const auto &vertebrataMotionFrames = vertebrataMotion->frames();
for (size_t frameIndex = 0; frameIndex < vertebrataMotionFrames.size(); ++frameIndex) {
const auto &frame = vertebrataMotionFrames[frameIndex];
std::vector<RiggerBone> transformedBones = m_bones;
for (const auto &node: frame) {
if (-1 == node.boneIndex)
continue;
if (node.isTail) {
transformedBones[node.boneIndex].tailPosition = node.position;
for (const auto &childIndex: m_bones[node.boneIndex].children)
transformedBones[childIndex].headPosition = node.position;
} else {
transformedBones[node.boneIndex].headPosition = node.position;
auto parentIndex = m_bones[node.boneIndex].parent;
if (-1 != parentIndex) {
transformedBones[parentIndex].tailPosition = node.position;
for (const auto &childIndex: m_bones[parentIndex].children)
transformedBones[childIndex].headPosition = node.position;
}
}
}
std::vector<QMatrix4x4> poseTransforms(transformedBones.size());
std::vector<QMatrix4x4> poseRotations(transformedBones.size());
for (size_t i = 0; i < transformedBones.size(); ++i) {
const auto &oldBone = m_bones[i];
const auto &bone = transformedBones[i];
QMatrix4x4 parentMatrix;
QMatrix4x4 translationMatrix;
QMatrix4x4 rotationMatrix;
QMatrix4x4 parentRotation;
if (-1 != bone.parent) {
const auto &oldParentBone = m_bones[oldBone.parent];
parentMatrix = poseTransforms[bone.parent];
parentRotation = poseRotations[bone.parent];
translationMatrix.translate(oldBone.headPosition - oldParentBone.headPosition);
QQuaternion rotation = QQuaternion::rotationTo((oldBone.tailPosition - oldBone.headPosition).normalized(),
(bone.tailPosition - bone.headPosition).normalized());
rotationMatrix.rotate(rotation);
} else {
translationMatrix.translate(bone.headPosition + (bone.tailPosition - oldBone.tailPosition));
}
poseTransforms[i] = parentMatrix * translationMatrix * parentRotation.inverted() * rotationMatrix;
poseRotations[i] = rotationMatrix;
}
JointNodeTree jointNodeTree(&m_bones);
for (size_t i = 0; i < m_bones.size(); ++i) {
const auto &bone = transformedBones[i];
if (-1 != bone.parent) {
jointNodeTree.updateMatrix(i, poseTransforms[bone.parent].inverted() * poseTransforms[i]);
} else {
jointNodeTree.updateMatrix(i, poseTransforms[i]);
}
}
jointNodeTrees.push_back({0.017f, jointNodeTree});
const std::vector<JointNode> &jointNodes = jointNodeTree.nodes();
std::vector<QMatrix4x4> jointNodeMatrices(m_bones.size());
for (size_t i = 0; i < m_bones.size(); ++i) {
const auto &bone = transformedBones[i];
QMatrix4x4 translationMatrix;
translationMatrix.translate(jointNodes[i].translation);
QMatrix4x4 rotationMatrix;
rotationMatrix.rotate(jointNodes[i].rotation);
if (-1 != bone.parent) {
jointNodeMatrices[i] *= jointNodeMatrices[bone.parent];
}
jointNodeMatrices[i] *= translationMatrix * rotationMatrix;
}
for (size_t i = 0; i < m_bones.size(); ++i)
jointNodeMatrices[i] = jointNodeMatrices[i] * bindTransforms[i].inverted();
std::vector<QVector3D> transformedVertices(m_outcome.vertices.size());
for (size_t i = 0; i < m_outcome.vertices.size(); ++i) {
const auto &weight = m_rigWeights[i];
for (int x = 0; x < 4; x++) {
float factor = weight.boneWeights[x];
if (factor > 0) {
transformedVertices[i] += jointNodeMatrices[weight.boneIndices[x]] * m_outcome.vertices[i] * factor;
}
}
}
std::vector<QVector3D> frameVertices = transformedVertices;
std::vector<std::vector<size_t>> frameFaces = m_outcome.triangles;
std::vector<std::vector<QVector3D>> frameCornerNormals;
const std::vector<std::vector<QVector3D>> *triangleVertexNormals = m_outcome.triangleVertexNormals();
if (nullptr == triangleVertexNormals) {
frameCornerNormals.resize(frameFaces.size());
for (size_t i = 0; i < m_outcome.triangles.size(); ++i) {
const auto &triangle = m_outcome.triangles[i];
QVector3D triangleNormal = QVector3D::normal(
transformedVertices[triangle[0]],
transformedVertices[triangle[1]],
transformedVertices[triangle[2]]
);
frameCornerNormals[i] = {
triangleNormal, triangleNormal, triangleNormal
};
}
} else {
frameCornerNormals = *triangleVertexNormals;
}
if (m_snapshotMeshesEnabled) {
if (frameIndex == vertebrataMotionFrames.size() / 2) {
delete snapshotMesh;
snapshotMesh = new Model(frameVertices, frameFaces, frameCornerNormals);
}
}
if (m_previewMeshesEnabled) {
BlockMesh blockMesh;
blockMesh.addBlock(
QVector3D(0.0, groundY + parameters.groundOffset, 0.0), 100.0,
QVector3D(0.0, groundY + parameters.groundOffset - 0.02, 0.0), 100.0);
for (const auto &bone: transformedBones) {
if (0 == bone.index)
continue;
blockMesh.addBlock(bone.headPosition, bone.headRadius * 0.5,
bone.tailPosition, bone.tailRadius * 0.5);
}
blockMesh.build();
std::vector<QVector3D> *resultVertices = blockMesh.takeResultVertices();
std::vector<std::vector<size_t>> *resultFaces = blockMesh.takeResultFaces();
size_t oldVertexCount = frameVertices.size();
for (const auto &v: *resultVertices)
frameVertices.push_back(QVector3D(v.x() - 0.5, v.y(), v.z()));
for (const auto &f: *resultFaces) {
std::vector<size_t> newF = f;
for (auto &v: newF)
v += oldVertexCount;
frameFaces.push_back(newF);
QVector3D triangleNormal = QVector3D::normal(
(*resultVertices)[f[0]],
(*resultVertices)[f[1]],
(*resultVertices)[f[2]]
);
frameCornerNormals.push_back({
triangleNormal, triangleNormal, triangleNormal
});
}
delete resultFaces;
delete resultVertices;
previewMeshes.push_back({0.017f, new SimpleShaderMesh(
new std::vector<QVector3D>(frameVertices),
new std::vector<std::vector<size_t>>(frameFaces),
new std::vector<std::vector<QVector3D>>(frameCornerNormals))});
}
}
if (m_previewMeshesEnabled)
m_resultPreviewMeshes[motionId] = previewMeshes;
m_resultJointNodeTrees[motionId] = jointNodeTrees;
m_resultSnapshotMeshes[motionId] = snapshotMesh;
}
void MotionsGenerator::generate()
{
for (const auto &it: m_motions) {
generateMotion(it.first);
m_generatedMotionIds.insert(it.first);
}
}

View File

@ -5,29 +5,27 @@
#include <map>
#include <set>
#include "model.h"
#include "simpleshadermesh.h"
#include "rigger.h"
#include "jointnodetree.h"
#include "document.h"
#include "poser.h"
#define ENABLE_PROCEDURAL_DEBUG 1
class MotionsGenerator : public QObject
{
Q_OBJECT
public:
MotionsGenerator(RigType rigType,
const std::vector<RiggerBone> *rigBones,
const std::map<int, RiggerVertexWeights> *rigWeights,
const std::vector<RiggerBone> &bones,
const std::map<int, RiggerVertexWeights> &rigWeights,
const Outcome &outcome);
~MotionsGenerator();
void addPoseToLibrary(const QUuid &poseId, const std::vector<std::pair<std::map<QString, QString>, std::map<QString, std::map<QString, QString>>>> &frames, float yTranslationScale);
void addMotionToLibrary(const QUuid &motionId, const std::vector<MotionClip> &clips);
void addRequirement(const QUuid &motionId);
std::vector<std::pair<float, Model *>> takeResultPreviewMeshs(const QUuid &motionId);
void addMotion(const QUuid &motionId, const std::map<QString, QString> &parameters);
Model *takeResultSnapshotMesh(const QUuid &motionId);
std::vector<std::pair<float, SimpleShaderMesh *>> takeResultPreviewMeshes(const QUuid &motionId);
std::vector<std::pair<float, JointNodeTree>> takeResultJointNodeTrees(const QUuid &motionId);
const std::set<QUuid> &requiredMotionIds();
const std::set<QUuid> &generatedMotionIds();
void enablePreviewMeshes();
void enableSnapshotMeshes();
void generate();
signals:
void finished();
@ -36,36 +34,19 @@ public slots:
void process();
private:
void generateMotion(const QUuid &motionId, std::set<QUuid> &visited, std::vector<std::pair<float, JointNodeTree>> &outcomes,
std::vector<Model *> *previews=nullptr);
const JointNodeTree &poseJointNodeTree(const QUuid &poseId, int frame);
JointNodeTree generateInterpolation(InterpolationType interpolationType, const JointNodeTree &first, const JointNodeTree &second, float progress);
const JointNodeTree *findClipBeginJointNodeTree(const MotionClip &clip);
const JointNodeTree *findClipEndJointNodeTree(const MotionClip &clip);
std::vector<MotionClip> *findMotionClips(const QUuid &motionId);
std::vector<std::pair<std::map<QString, QString>, std::map<QString, std::map<QString, QString>>>> *findPoseFrames(const QUuid &poseId);
void generatePreviewsForOutcomes(const std::vector<std::pair<float, JointNodeTree>> &outcomes, std::vector<std::pair<float, Model *>> &previews);
float calculateMotionDuration(const QUuid &motionId, std::set<QUuid> &visited);
float calculatePoseDuration(const QUuid &poseId);
RigType m_rigType = RigType::None;
std::vector<RiggerBone> m_rigBones;
std::vector<RiggerBone> m_bones;
std::map<int, RiggerVertexWeights> m_rigWeights;
std::map<int, std::vector<std::pair<float, JointNodeTree>>> m_proceduralAnimations;
#if ENABLE_PROCEDURAL_DEBUG
std::map<int, std::vector<Model *>> m_proceduralDebugPreviews;
#endif
Outcome m_outcome;
std::map<QUuid, std::vector<std::pair<std::map<QString, QString>, std::map<QString, std::map<QString, QString>>>>> m_poses;
std::map<QUuid, float> m_posesYtranslationScales;
std::map<QUuid, std::vector<MotionClip>> m_motions;
std::set<QUuid> m_requiredMotionIds;
std::map<QUuid, std::map<QString, QString>> m_motions;
std::set<QUuid> m_generatedMotionIds;
std::map<QUuid, std::vector<std::pair<float, Model *>>> m_resultPreviewMeshs;
std::map<QUuid, Model *> m_resultSnapshotMeshes;
std::map<QUuid, std::vector<std::pair<float, SimpleShaderMesh *>>> m_resultPreviewMeshes;
std::map<QUuid, std::vector<std::pair<float, JointNodeTree>>> m_resultJointNodeTrees;
std::map<std::pair<QUuid, int>, JointNodeTree> m_poseJointNodeTreeMap;
Poser *m_poser = nullptr;
int m_fps = 30;
bool m_previewMeshesEnabled = false;
bool m_snapshotMeshesEnabled = false;
void generateMotion(const QUuid &motionId);
};
#endif

View File

@ -1,411 +0,0 @@
#include <QAbstractItemView>
#include <QPalette>
#include <QMenu>
#include <QWidgetAction>
#include <QCheckBox>
#include <QFormLayout>
#include <QDoubleSpinBox>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QStackedWidget>
#include "motiontimelinewidget.h"
#include "motionclipwidget.h"
#include "theme.h"
#include "posewidget.h"
#include "motionwidget.h"
#include "tabwidget.h"
MotionTimelineWidget::MotionTimelineWidget(const Document *document, QWidget *parent) :
QListWidget(parent),
m_document(document)
{
setSelectionMode(QAbstractItemView::NoSelection);
setFocusPolicy(Qt::NoFocus);
QPalette palette = this->palette();
palette.setColor(QPalette::Window, Qt::transparent);
palette.setColor(QPalette::Base, Qt::transparent);
setPalette(palette);
setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setSpacing(0);
setContentsMargins(0, 0, 0, 0);
setFlow(QListWidget::LeftToRight);
auto minHeight = MotionClipWidget::maxSize().height();
setMinimumHeight(minHeight + 4);
setMaximumHeight(minHeight + 4 + 20);
}
QSize MotionTimelineWidget::sizeHint() const
{
return QSize(0, MotionClipWidget::maxSize().height() + 4);
}
const std::vector<MotionClip> &MotionTimelineWidget::clips()
{
return m_clips;
}
void MotionTimelineWidget::setClips(std::vector<MotionClip> clips)
{
m_clips = clips;
if (m_currentSelectedIndex >= (int)m_clips.size())
m_currentSelectedIndex = -1;
reload();
}
void MotionTimelineWidget::addPose(QUuid poseId)
{
MotionClip clip;
clip.linkToId = poseId;
clip.clipType = MotionClipType::Pose;
clip.duration = 0;
addClipAfterCurrentIndex(clip);
emit clipsChanged();
reload();
}
void MotionTimelineWidget::addClipAfterCurrentIndex(const MotionClip &clip)
{
MotionClip interpolationClip;
bool needPrependInterpolationClip = false;
int afterIndex = m_currentSelectedIndex;
if (-1 == afterIndex)
afterIndex = m_clips.size() - 1;
if (-1 != afterIndex) {
if (m_clips[afterIndex].clipType == MotionClipType::Interpolation) {
--afterIndex;
}
}
if (clip.clipType == MotionClipType::Interpolation) {
if (m_clips.empty())
return;
if (m_clips[m_clips.size() - 1].clipType == MotionClipType::Interpolation)
return;
} else {
if (!m_clips.empty() && m_clips[m_clips.size() - 1].clipType != MotionClipType::Interpolation) {
interpolationClip.interpolationType = InterpolationType::EaseInOutCubic;
interpolationClip.clipType = MotionClipType::Interpolation;
interpolationClip.duration = 1.0;
needPrependInterpolationClip = true;
}
}
if (-1 == afterIndex) {
if (needPrependInterpolationClip)
m_clips.push_back(interpolationClip);
m_clips.push_back(clip);
} else {
if (needPrependInterpolationClip)
m_clips.insert(m_clips.begin() + afterIndex + 1, interpolationClip);
m_clips.insert(m_clips.begin() + afterIndex + 2, clip);
}
}
void MotionTimelineWidget::addMotion(QUuid motionId)
{
MotionClip clip;
clip.linkToId = motionId;
clip.clipType = MotionClipType::Motion;
clip.duration = 0;
addClipAfterCurrentIndex(clip);
emit clipsChanged();
reload();
}
void MotionTimelineWidget::addProceduralAnimation(ProceduralAnimation proceduralAnimation)
{
MotionClip clip;
clip.clipType = MotionClipType::ProceduralAnimation;
clip.duration = 0;
clip.proceduralAnimation = proceduralAnimation;
addClipAfterCurrentIndex(clip);
emit clipsChanged();
reload();
}
void MotionTimelineWidget::setClipInterpolationType(int index, InterpolationType type)
{
if (index >= (int)m_clips.size())
return;
if (m_clips[index].clipType != MotionClipType::Interpolation)
return;
if (m_clips[index].interpolationType == type)
return;
m_clips[index].interpolationType = type;
emit clipsChanged();
}
void MotionTimelineWidget::setClipDuration(int index, float duration)
{
if (index >= (int)m_clips.size())
return;
if (m_clips[index].clipType == MotionClipType::Motion)
return;
m_clips[index].duration = duration;
MotionClipWidget *widget = (MotionClipWidget *)itemWidget(item(index));
widget->setClip(m_clips[index]);
widget->reload();
emit clipsChanged();
}
void MotionTimelineWidget::reload()
{
clear();
for (int row = 0; row < (int)m_clips.size(); ++row) {
MotionClipWidget *widget = new MotionClipWidget(m_document);
widget->setClip(m_clips[row]);
connect(widget, &MotionClipWidget::modifyInterpolation, this, [=]() {
showInterpolationSettingPopup(row, mapFromGlobal(QCursor::pos()));
});
QListWidgetItem *item = new QListWidgetItem(this);
auto itemSize = widget->preferredSize();
itemSize.setWidth(itemSize.width() + 2);
itemSize.setHeight(itemSize.height() + 2);
item->setSizeHint(itemSize);
item->setData(Qt::UserRole, QVariant(row));
item->setBackground(Theme::black);
addItem(item);
setItemWidget(item, widget);
widget->reload();
if (m_currentSelectedIndex == row)
widget->updateCheckedState(true);
}
}
void MotionTimelineWidget::showInterpolationSettingPopup(int clipIndex, const QPoint &pos)
{
QMenu popupMenu;
QWidget *popup = new QWidget;
QWidget *linearWidget = new QWidget;
QWidget *cubicWidget = new QWidget;
QCheckBox *hasAcceleratingBox = new QCheckBox();
hasAcceleratingBox->setText(tr("Accelerating"));
Theme::initCheckbox(hasAcceleratingBox);
QCheckBox *hasDeceleratingBox = new QCheckBox();
hasDeceleratingBox->setText(tr("Decelerating"));
Theme::initCheckbox(hasDeceleratingBox);
QCheckBox *bouncingBeginBox = new QCheckBox();
bouncingBeginBox->setText(tr("Bouncing"));
Theme::initCheckbox(bouncingBeginBox);
QCheckBox *bouncingEndBox = new QCheckBox();
bouncingEndBox->setText(tr("Bouncing"));
Theme::initCheckbox(bouncingEndBox);
QStackedWidget *stackedWidget = new QStackedWidget;
stackedWidget->addWidget(linearWidget);
stackedWidget->addWidget(cubicWidget);
bool currentIsLinear = InterpolationIsLinear(clips()[clipIndex].interpolationType);
std::vector<QString> tabs = {
tr("Linear"),
tr("Cubic")
};
TabWidget *tabWidget = new TabWidget(tabs);
tabWidget->setCurrentIndex(currentIsLinear ? 0 : 1);
stackedWidget->setCurrentIndex(currentIsLinear ? 0 : 1);
auto updateBoxes = [=](InterpolationType type) {
hasAcceleratingBox->setChecked(InterpolationHasAccelerating(type));
hasDeceleratingBox->setChecked(InterpolationHasDecelerating(type));
bouncingBeginBox->setChecked(InterpolationIsBouncingBegin(type));
bouncingEndBox->setChecked(InterpolationIsBouncingEnd(type));
};
updateBoxes(clips()[clipIndex].interpolationType);
auto updateInterpolation = [=]() {
bool isLinear = 0 == tabWidget->currentIndex();
bool bouncingBegin = bouncingBeginBox->isChecked();
bool bouncingEnd = bouncingEndBox->isChecked();
bool hasAccelerating = bouncingBegin || hasAcceleratingBox->isChecked();
bool hasDecelerating = bouncingEnd || hasDeceleratingBox->isChecked();
InterpolationType newType = InterpolationMakeFromOptions(isLinear,
hasAccelerating, hasDecelerating,
bouncingBegin, bouncingEnd);
setClipInterpolationType(clipIndex, newType);
updateBoxes(newType);
};
connect(tabWidget, &TabWidget::currentIndexChanged, this, [=](int index) {
stackedWidget->setCurrentIndex(index);
updateInterpolation();
});
connect(hasAcceleratingBox, &QCheckBox::stateChanged, this, [=]() {
updateInterpolation();
});
connect(hasDeceleratingBox, &QCheckBox::stateChanged, this, [=]() {
updateInterpolation();
});
connect(bouncingBeginBox, &QCheckBox::stateChanged, this, [=]() {
updateInterpolation();
});
connect(bouncingEndBox, &QCheckBox::stateChanged, this, [=]() {
updateInterpolation();
});
updateInterpolation();
QDoubleSpinBox *durationEdit = new QDoubleSpinBox();
durationEdit->setDecimals(2);
durationEdit->setMaximum(60);
durationEdit->setMinimum(0);
durationEdit->setSingleStep(0.1);
durationEdit->setValue(clips()[clipIndex].duration);
connect(durationEdit, static_cast<void (QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged), this, [=](double value) {
setClipDuration(clipIndex, (float)value);
});
QVBoxLayout *mainLayout = new QVBoxLayout;
QVBoxLayout *cubicLayout = new QVBoxLayout;
QHBoxLayout *acceleratingLayout = new QHBoxLayout;
acceleratingLayout->addWidget(hasAcceleratingBox);
acceleratingLayout->addWidget(bouncingBeginBox);
cubicLayout->addLayout(acceleratingLayout);
QHBoxLayout *deceleratingLayout = new QHBoxLayout;
deceleratingLayout->addWidget(hasDeceleratingBox);
deceleratingLayout->addWidget(bouncingEndBox);
cubicLayout->addLayout(deceleratingLayout);
cubicWidget->setLayout(cubicLayout);
mainLayout->addWidget(tabWidget);
mainLayout->addWidget(stackedWidget);
{
QFormLayout *formLayout = new QFormLayout;
formLayout->addRow(tr("Duration:"), durationEdit);
mainLayout->addLayout(formLayout);
}
popup->setLayout(mainLayout);
QWidgetAction *action = new QWidgetAction(this);
action->setDefaultWidget(popup);
popupMenu.addAction(action);
popupMenu.exec(mapToGlobal(pos));
}
void MotionTimelineWidget::mousePressEvent(QMouseEvent *event)
{
QListWidget::mousePressEvent(event);
if (event->button() == Qt::RightButton) {
showContextMenu(mapFromGlobal(event->globalPos()));
return;
}
QModelIndex itemIndex = indexAt(event->pos());
if (!itemIndex.isValid())
return;
if (event->button() == Qt::LeftButton) {
int row = itemIndex.row();
setCurrentIndex(row);
}
}
void MotionTimelineWidget::setCurrentIndex(int index)
{
if (m_currentSelectedIndex == index || index >= (int)m_clips.size())
return;
if (-1 != m_currentSelectedIndex) {
MotionClipWidget *widget = (MotionClipWidget *)itemWidget(item(m_currentSelectedIndex));
widget->updateCheckedState(false);
}
m_currentSelectedIndex = index;
{
MotionClipWidget *widget = (MotionClipWidget *)itemWidget(item(m_currentSelectedIndex));
widget->updateCheckedState(true);
}
}
void MotionTimelineWidget::removeClip(int index)
{
if (index >= (int)m_clips.size())
return;
if (index == m_currentSelectedIndex) {
if (index - 2 >= 0) {
setCurrentIndex(index - 2);
} else if (index + 2 < (int)m_clips.size()) {
setCurrentIndex(index + 2);
// We need remove one clip and the interpolation, so here we -2
m_currentSelectedIndex -= 2;
} else {
m_currentSelectedIndex = -1;
}
}
m_clips.erase(m_clips.begin() + index);
if (index - 2 >= 0) {
// Remove the interpolation before this clip
m_clips.erase(m_clips.begin() + index - 1);
} else if (index < (int)m_clips.size()) {
// Remove the interpolation after this clip
m_clips.erase(m_clips.begin() + index);
}
emit clipsChanged();
reload();
}
void MotionTimelineWidget::showContextMenu(const QPoint &pos)
{
QMenu contextMenu(this);
QAction doubleDurationAction(tr("Double Duration"), this);
if (-1 != m_currentSelectedIndex) {
if (m_clips[m_currentSelectedIndex].clipType == MotionClipType::Interpolation) {
connect(&doubleDurationAction, &QAction::triggered, [=]() {
setClipDuration(m_currentSelectedIndex, m_clips[m_currentSelectedIndex].duration * 2);
});
contextMenu.addAction(&doubleDurationAction);
}
}
QAction halveDurationAction(tr("Halve Duration"), this);
if (-1 != m_currentSelectedIndex) {
if (m_clips[m_currentSelectedIndex].clipType == MotionClipType::Interpolation) {
connect(&halveDurationAction, &QAction::triggered, [=]() {
setClipDuration(m_currentSelectedIndex, m_clips[m_currentSelectedIndex].duration / 2);
});
contextMenu.addAction(&halveDurationAction);
}
}
QAction deleteAction(tr("Delete"), this);
if (-1 != m_currentSelectedIndex) {
if (m_clips[m_currentSelectedIndex].clipType != MotionClipType::Interpolation) {
connect(&deleteAction, &QAction::triggered, [=]() {
removeClip(m_currentSelectedIndex);
});
contextMenu.addAction(&deleteAction);
}
}
contextMenu.exec(mapToGlobal(pos));
}

View File

@ -1,44 +0,0 @@
#ifndef DUST3D_MOTION_TIMELINE_WIDGET_H
#define DUST3D_MOTION_TIMELINE_WIDGET_H
#include <QListWidget>
#include <QUuid>
#include <QMouseEvent>
#include "document.h"
#include "interpolationtype.h"
#include "proceduralanimation.h"
class MotionTimelineWidget : public QListWidget
{
Q_OBJECT
signals:
void clipsChanged();
public:
MotionTimelineWidget(const Document *document, QWidget *parent=nullptr);
const std::vector<MotionClip> &clips();
public slots:
void setClips(std::vector<MotionClip> clips);
void addPose(QUuid poseId);
void addMotion(QUuid motionId);
void addProceduralAnimation(ProceduralAnimation proceduralAnimation);
void reload();
void setClipInterpolationType(int index, InterpolationType type);
void setClipDuration(int index, float duration);
void showInterpolationSettingPopup(int clipIndex, const QPoint &pos);
void showContextMenu(const QPoint &pos);
void setCurrentIndex(int index);
void removeClip(int index);
protected:
void mousePressEvent(QMouseEvent *event) override;
QSize sizeHint() const override;
private:
void addClipAfterCurrentIndex(const MotionClip &clip);
std::vector<MotionClip> m_clips;
const Document *m_document = nullptr;
int m_currentSelectedIndex = -1;
};
#endif

View File

@ -56,8 +56,6 @@ void NormalAndDepthMapsGenerator::generate()
void NormalAndDepthMapsGenerator::process()
{
generate();
m_normalMapRender->setRenderThread(QGuiApplication::instance()->thread());
m_depthMapRender->setRenderThread(QGuiApplication::instance()->thread());
emit finished();
}

59
src/planemesh.cpp Normal file
View File

@ -0,0 +1,59 @@
#include <map>
#include <QDebug>
#include "planemesh.h"
void PlaneMesh::build()
{
delete m_resultVertices;
m_resultVertices = new std::vector<QVector3D>;
delete m_resultQuads;
m_resultQuads = new std::vector<std::vector<size_t>>;
std::map<std::pair<size_t, size_t>, size_t> *columnAndRowToIndexMap = new std::map<std::pair<size_t, size_t>, size_t>;
auto addVertex = [=](const std::pair<size_t, size_t> columnAndRow, const QVector3D &position) {
auto insertResult = columnAndRowToIndexMap->insert({columnAndRow, m_resultVertices->size()});
if (insertResult.second) {
m_resultVertices->push_back(position);
}
return insertResult.first->second;
};
QVector3D perpunicularAxis = QVector3D::crossProduct(m_axis, m_normal);
QVector3D stepWidth = m_axis * m_radius;
QVector3D stepHeight = perpunicularAxis * m_radius;
//qDebug() << "stepWidth:" << stepWidth.x() << "," << stepWidth.y() << "," << stepWidth.z();
//qDebug() << "stepHeight:" << stepHeight.x() << "," << stepHeight.y() << "," << stepHeight.z();
for (size_t row = 0; row < m_halfRows; ++row) {
for (size_t column = 0; column < m_halfColumns; ++column) {
QVector3D bottomLeft = m_origin + stepHeight * (double)row + stepWidth * (double)column;
std::vector<QVector3D> positions = {
bottomLeft,
bottomLeft + stepHeight,
bottomLeft + stepHeight + stepWidth,
bottomLeft + stepWidth
};
//qDebug() << "column:" << column << "row:" << row;
//for (size_t i = 0; i < 4; ++i) {
// qDebug() << "position[" << i << "]:" << positions[i].x() << "," << positions[i].y() << "," << positions[i].z();
//}
m_resultQuads->push_back({
addVertex({column + 0, row + 0}, positions[0]),
addVertex({column + 0, row + 1}, positions[1]),
addVertex({column + 1, row + 1}, positions[2]),
addVertex({column + 1, row + 0}, positions[3])
});
}
}
delete columnAndRowToIndexMap;
delete m_resultFaces;
m_resultFaces = new std::vector<std::vector<size_t>>;
m_resultFaces->reserve(m_resultQuads->size() * 2);
for (const auto &quad: *m_resultQuads) {
m_resultFaces->push_back({quad[0], quad[1], quad[2]});
m_resultFaces->push_back({quad[2], quad[3], quad[0]});
}
}

66
src/planemesh.h Normal file
View File

@ -0,0 +1,66 @@
#ifndef DUST3D_PLANE_MESH_H
#define DUST3D_PLANE_MESH_H
#include <QVector3D>
#include <vector>
class PlaneMesh
{
public:
PlaneMesh(const QVector3D &normal,
const QVector3D &axis,
const QVector3D &origin,
double radius,
size_t halfColumns,
size_t halfRows) :
m_normal(normal),
m_axis(axis),
m_origin(origin),
m_radius(radius),
m_halfColumns(halfColumns),
m_halfRows(halfRows)
{
}
~PlaneMesh()
{
delete m_resultVertices;
delete m_resultQuads;
delete m_resultFaces;
}
std::vector<QVector3D> *takeResultVertices()
{
std::vector<QVector3D> *resultVertices = m_resultVertices;
m_resultVertices = nullptr;
return resultVertices;
}
std::vector<std::vector<size_t>> *takeResultQuads()
{
std::vector<std::vector<size_t>> *resultQuads = m_resultQuads;
m_resultQuads = nullptr;
return resultQuads;
}
std::vector<std::vector<size_t>> *takeResultFaces()
{
std::vector<std::vector<size_t>> *resultFaces = m_resultFaces;
m_resultFaces = nullptr;
return resultFaces;
}
void build();
private:
std::vector<QVector3D> *m_resultVertices = nullptr;
std::vector<std::vector<size_t>> *m_resultQuads = nullptr;
std::vector<std::vector<size_t>> *m_resultFaces = nullptr;
QVector3D m_normal;
QVector3D m_axis;
QVector3D m_origin;
double m_radius = 0.0;
size_t m_halfColumns = 0;
size_t m_halfRows = 0;
};
#endif

View File

@ -1,734 +0,0 @@
#include <QDebug>
#include <QXmlStreamWriter>
#include <QClipboard>
#include <QApplication>
#include <QMimeData>
#include <QRegularExpression>
#include "posedocument.h"
#include "rigger.h"
#include "util.h"
#include "document.h"
#include "snapshot.h"
#include "snapshotxml.h"
const float PoseDocument::m_nodeRadius = 0.01;
const float PoseDocument::m_groundPlaneHalfThickness = 0.005 / 4;
const bool PoseDocument::m_hideRootAndVirtual = true;
const float PoseDocument::m_outcomeScaleFactor = 0.5;
void PoseDocument::setSideVisiableState(SkeletonSide side, bool visible)
{
bool isSideVisible = m_hiddenSides.find(side) == m_hiddenSides.end();
if (isSideVisible == visible)
return;
if (visible)
m_hiddenSides.erase(side);
else
m_hiddenSides.insert(side);
emit sideVisibleStateChanged(side);
const QUuid partId = m_partIdMap[side];
partMap[partId].visible = visible;
emit partVisibleStateChanged(partId);
}
bool PoseDocument::isSideVisible(SkeletonSide side)
{
return m_hiddenSides.find(side) == m_hiddenSides.end();
}
bool PoseDocument::hasPastableNodesInClipboard() const
{
const QClipboard *clipboard = QApplication::clipboard();
const QMimeData *mimeData = clipboard->mimeData();
if (mimeData->hasText()) {
if (-1 != mimeData->text().indexOf("<pose ") && -1 != mimeData->text().indexOf("<parameter "))
return true;
}
return false;
}
bool PoseDocument::originSettled() const
{
return false;
}
bool PoseDocument::isNodeEditable(QUuid nodeId) const
{
if (m_otherIds.find(nodeId) != m_otherIds.end())
return false;
return true;
}
bool PoseDocument::isEdgeEditable(QUuid edgeId) const
{
if (m_otherIds.find(edgeId) != m_otherIds.end())
return false;
return true;
}
bool PoseDocument::isNodeDeactivated(QUuid nodeId) const
{
if (m_otherIds.find(nodeId) != m_otherIds.end())
return true;
return false;
}
bool PoseDocument::isEdgeDeactivated(QUuid edgeId) const
{
if (m_otherIds.find(edgeId) != m_otherIds.end())
return true;
return false;
}
void PoseDocument::copyNodes(std::set<QUuid> nodeIdSet) const
{
std::map<QString, std::map<QString, QString>> parameters;
toParameters(parameters, nodeIdSet);
if (parameters.empty())
return;
Document document;
QUuid poseId = QUuid::createUuid();
auto &pose = document.poseMap[poseId];
pose.id = poseId;
pose.frames.push_back({std::map<QString, QString>(), parameters});
document.poseIdList.push_back(poseId);
Snapshot snapshot;
std::set<QUuid> limitPoseIds;
document.toSnapshot(&snapshot, limitPoseIds, DocumentToSnapshotFor::Poses);
QString snapshotXml;
QXmlStreamWriter xmlStreamWriter(&snapshotXml);
saveSkeletonToXmlStream(&snapshot, &xmlStreamWriter);
QClipboard *clipboard = QApplication::clipboard();
clipboard->setText(snapshotXml);
}
void PoseDocument::saveHistoryItem()
{
PoseHistoryItem item;
toParameters(item.parameters);
m_undoItems.push_back(item);
}
bool PoseDocument::undoable() const
{
return m_undoItems.size() >= 2;
}
bool PoseDocument::redoable() const
{
return !m_redoItems.empty();
}
void PoseDocument::undo()
{
if (!undoable())
return;
m_redoItems.push_back(m_undoItems.back());
m_undoItems.pop_back();
const auto &item = m_undoItems.back();
fromParameters(&m_riggerBones, item.parameters);
}
void PoseDocument::redo()
{
if (m_redoItems.empty())
return;
m_undoItems.push_back(m_redoItems.back());
const auto &item = m_redoItems.back();
fromParameters(&m_riggerBones, item.parameters);
m_redoItems.pop_back();
}
void PoseDocument::paste()
{
const QClipboard *clipboard = QApplication::clipboard();
const QMimeData *mimeData = clipboard->mimeData();
if (mimeData->hasText()) {
QXmlStreamReader xmlStreamReader(mimeData->text());
Snapshot snapshot;
loadSkeletonFromXmlStream(&snapshot, xmlStreamReader);
if (snapshot.poses.empty())
return;
const auto &firstPose = *snapshot.poses.begin();
if (firstPose.second.empty())
return;
const auto &firstFrame = *firstPose.second.begin();
fromParameters(&m_riggerBones, firstFrame.second);
saveHistoryItem();
}
}
void PoseDocument::updateTurnaround(const QImage &image)
{
turnaround = image;
emit turnaroundChanged();
}
void PoseDocument::updateOtherFramesParameters(const std::vector<std::map<QString, std::map<QString, QString>>> &otherFramesParameters)
{
m_otherFramesParameters = otherFramesParameters;
}
void PoseDocument::resetWithoutNotifingParametersChanged()
{
nodeMap.clear();
edgeMap.clear();
partMap.clear();
m_otherIds.clear();
m_boneNameToIdsMap.clear();
m_partIdMap.clear();
emit cleanup();
}
void PoseDocument::reset()
{
resetWithoutNotifingParametersChanged();
emit parametersChanged();
}
void PoseDocument::clearHistories()
{
m_undoItems.clear();
m_redoItems.clear();
}
void PoseDocument::updateBonesFromParameters(std::vector<RiggerBone> *bones,
const std::map<QString, std::map<QString, QString>> &parameters,
float firstSpineBoneLength,
const QVector3D &firstSpineBonePosition,
const QVector3D &neckJoint1BoneDirection)
{
float firstSpineBoneLengthFromParameters = 0.0;
QVector3D firstSpineBonePositionFromParameters;
firstSpinePositionAndLengthFromParameters(parameters,
&firstSpineBoneLengthFromParameters,
&firstSpineBonePositionFromParameters);
float boneScaleFactor = 1.0;
QVector3D firstSpineBonePositionOffset;
if (firstSpineBoneLengthFromParameters > 0 && firstSpineBoneLength > 0) {
boneScaleFactor = firstSpineBoneLengthFromParameters / firstSpineBoneLength;
firstSpineBonePositionOffset = firstSpineBonePositionFromParameters - firstSpineBonePosition;
}
//QVector3D neckJoint1DirectionInParameters;
//neckJoint1DirectionFromParameters(parameters, &neckJoint1DirectionInParameters);
//QQuaternion neckJoint1Rotation = QQuaternion::rotationTo(neckJoint1DirectionInParameters, neckJoint1BoneDirection);
for (auto &bone: *bones) {
const auto findParameterResult = parameters.find(bone.name);
if (findParameterResult == parameters.end()) {
bone.headPosition *= boneScaleFactor;
bone.tailPosition *= boneScaleFactor;
bone.headPosition += firstSpineBonePositionOffset;
bone.tailPosition += firstSpineBonePositionOffset;
continue;
}
const auto &map = findParameterResult->second;
{
auto findXResult = map.find("fromX");
auto findYResult = map.find("fromY");
auto findZResult = map.find("fromZ");
if (findXResult != map.end() ||
findYResult != map.end() ||
findZResult != map.end()) {
bone.headPosition = {
valueOfKeyInMapOrEmpty(map, "fromX").toFloat(),
valueOfKeyInMapOrEmpty(map, "fromY").toFloat(),
valueOfKeyInMapOrEmpty(map, "fromZ").toFloat()
};
}
}
{
auto findXResult = map.find("toX");
auto findYResult = map.find("toY");
auto findZResult = map.find("toZ");
if (findXResult != map.end() ||
findYResult != map.end() ||
findZResult != map.end()) {
QVector3D toPosition = {
valueOfKeyInMapOrEmpty(map, "toX").toFloat(),
valueOfKeyInMapOrEmpty(map, "toY").toFloat(),
valueOfKeyInMapOrEmpty(map, "toZ").toFloat()
};
bone.tailPosition = toPosition;
}
}
//if (bone.name.startsWith("Neck_")) {
//bone.tailPosition = bone.headPosition +
// neckJoint1Rotation.rotatedVector(bone.tailPosition - bone.headPosition);
//}
}
}
void PoseDocument::fromParameters(const std::vector<RiggerBone> *rigBones,
const std::map<QString, std::map<QString, QString>> &parameters)
{
if (nullptr == rigBones || rigBones->empty()) {
m_riggerBones.clear();
return;
}
if (&m_riggerBones != rigBones)
m_riggerBones = *rigBones;
float firstSpineBoneLength = 0.0;
QVector3D firstSpineBonePosition;
QVector3D neckJoint1BoneDirection = QVector3D(0.0, 1.0, 0.0);
for (const auto &bone: *rigBones) {
if ("Spine1" == bone.name) {
firstSpineBonePosition = bone.headPosition;
firstSpineBoneLength = bone.headPosition.distanceToPoint(bone.tailPosition);
} else if ("Neck_Joint1" == bone.name) {
neckJoint1BoneDirection = (bone.tailPosition - bone.headPosition).normalized();
}
}
std::vector<RiggerBone> bones = *rigBones;
updateBonesFromParameters(&bones,
parameters,
firstSpineBoneLength,
firstSpineBonePosition,
neckJoint1BoneDirection);
resetWithoutNotifingParametersChanged();
for (const auto &otherParameters: m_otherFramesParameters) {
std::vector<RiggerBone> otherBones = *rigBones;
updateBonesFromParameters(&otherBones,
otherParameters,
firstSpineBoneLength,
firstSpineBonePosition,
neckJoint1BoneDirection);
std::map<QString, std::pair<QUuid, QUuid>> boneNameToIdsMap;
parametersToNodes(&otherBones,
&boneNameToIdsMap,
&m_partIdMap,
true);
}
parametersToNodes(&bones,
&m_boneNameToIdsMap,
&m_partIdMap,
false);
emit parametersChanged();
}
void PoseDocument::parametersToNodes(const std::vector<RiggerBone> *rigBones,
std::map<QString, std::pair<QUuid, QUuid>> *boneNameToIdsMap,
std::map<SkeletonSide, QUuid> *m_partIdMap,
bool isOther)
{
if (nullptr == rigBones || rigBones->empty()) {
return;
}
std::set<QUuid> newAddedNodeIds;
std::set<QUuid> newAddedEdgeIds;
auto addPartIdOfSide = [=](SkeletonSide side) {
if (m_partIdMap->find(side) != m_partIdMap->end())
return;
QUuid partId = QUuid::createUuid();
auto &bonesPart = this->partMap[partId];
bonesPart.id = partId;
bonesPart.visible = this->m_hiddenSides.find(side) == this->m_hiddenSides.end();
(*m_partIdMap)[side] = partId;
};
addPartIdOfSide(SkeletonSide::Left);
addPartIdOfSide(SkeletonSide::None);
addPartIdOfSide(SkeletonSide::Right);
//qDebug() << "rigBones size:" << rigBones->size();
std::vector<std::pair<int, int>> edgePairs;
for (size_t i = m_hideRootAndVirtual ? 1 : 0; i < rigBones->size(); ++i) {
const auto &bone = (*rigBones)[i];
for (const auto &child: bone.children) {
//qDebug() << "Add pair:" << bone.name << "->" << (*rigBones)[child].name;
edgePairs.push_back({i, child});
}
}
std::map<int, QUuid> boneIndexToHeadNodeIdMap;
for (const auto &edgePair: edgePairs) {
QUuid firstNodeId, secondNodeId;
SkeletonSide firstNodeSide = SkeletonSideFromBoneName((*rigBones)[edgePair.first].name);
SkeletonSide secondNodeSide = SkeletonSideFromBoneName((*rigBones)[edgePair.second].name);
auto findFirst = boneIndexToHeadNodeIdMap.find(edgePair.first);
if (findFirst == boneIndexToHeadNodeIdMap.end()) {
const auto &bone = (*rigBones)[edgePair.first];
if (!bone.name.startsWith("Virtual_") || !m_hideRootAndVirtual) {
SkeletonNode node;
node.partId = (*m_partIdMap)[firstNodeSide];
node.id = QUuid::createUuid();
partMap[node.partId].nodeIds.push_back(node.id);
node.setRadius(m_nodeRadius);
node.setX(fromOutcomeX(bone.headPosition.x()));
node.setY(fromOutcomeY(bone.headPosition.y()));
node.setZ(fromOutcomeZ(bone.headPosition.z()));
node.name = bone.name + "Start";
nodeMap[node.id] = node;
//qDebug() << "Add first node:" << (*rigBones)[edgePair.first].name;
newAddedNodeIds.insert(node.id);
boneIndexToHeadNodeIdMap[edgePair.first] = node.id;
firstNodeId = node.id;
}
} else {
firstNodeId = findFirst->second;
}
auto findSecond = boneIndexToHeadNodeIdMap.find(edgePair.second);
if (findSecond == boneIndexToHeadNodeIdMap.end()) {
const auto &bone = (*rigBones)[edgePair.second];
if (!bone.name.startsWith("Virtual_") || !m_hideRootAndVirtual) {
SkeletonNode node;
node.partId = (*m_partIdMap)[secondNodeSide];
node.id = QUuid::createUuid();
partMap[node.partId].nodeIds.push_back(node.id);
node.setRadius(m_nodeRadius);
node.setX(fromOutcomeX(bone.headPosition.x()));
node.setY(fromOutcomeY(bone.headPosition.y()));
node.setZ(fromOutcomeZ(bone.headPosition.z()));
node.name = bone.name;
nodeMap[node.id] = node;
//qDebug() << "Add second node:" << (*rigBones)[edgePair.second].name;
newAddedNodeIds.insert(node.id);
boneIndexToHeadNodeIdMap[edgePair.second] = node.id;
secondNodeId = node.id;
}
} else {
secondNodeId = findSecond->second;
}
if (firstNodeId.isNull() || secondNodeId.isNull()) {
continue;
}
if (firstNodeSide != secondNodeSide) {
qDebug() << "First node side:" << SkeletonSideToDispName(firstNodeSide) << "is different with second node side:" << SkeletonSideToDispName(secondNodeSide);
continue;
}
SkeletonEdge edge;
edge.partId = (*m_partIdMap)[firstNodeSide];
edge.id = QUuid::createUuid();
edge.nodeIds.push_back(firstNodeId);
edge.nodeIds.push_back(secondNodeId);
edgeMap[edge.id] = edge;
newAddedEdgeIds.insert(edge.id);
nodeMap[firstNodeId].edgeIds.push_back(edge.id);
nodeMap[secondNodeId].edgeIds.push_back(edge.id);
}
for (size_t i = m_hideRootAndVirtual ? 1 : 0; i < rigBones->size(); ++i) {
if (boneIndexToHeadNodeIdMap.find(i) != boneIndexToHeadNodeIdMap.end())
continue;
const auto &bone = (*rigBones)[i];
if (!bone.children.empty())
continue;
if (!bone.name.startsWith("Virtual_") || !m_hideRootAndVirtual) {
SkeletonSide side = SkeletonSideFromBoneName(bone.name);
SkeletonNode node;
node.partId = (*m_partIdMap)[side];
node.id = QUuid::createUuid();
partMap[node.partId].nodeIds.push_back(node.id);
node.setRadius(m_nodeRadius);
node.setX(fromOutcomeX(bone.headPosition.x()));
node.setY(fromOutcomeY(bone.headPosition.y()));
node.setZ(fromOutcomeZ(bone.headPosition.z()));
nodeMap[node.id] = node;
newAddedNodeIds.insert(node.id);
boneIndexToHeadNodeIdMap[i] = node.id;
}
}
for (size_t i = m_hideRootAndVirtual ? 1 : 0; i < rigBones->size(); ++i) {
const auto &bone = (*rigBones)[i];
if (m_hideRootAndVirtual && bone.name.startsWith("Virtual_"))
continue;
if (bone.children.empty()) {
SkeletonSide side = SkeletonSideFromBoneName(bone.name);
const QUuid &firstNodeId = boneIndexToHeadNodeIdMap[i];
SkeletonNode node;
node.partId = (*m_partIdMap)[side];
node.id = QUuid::createUuid();
partMap[node.partId].nodeIds.push_back(node.id);
node.setRadius(m_nodeRadius / 2);
node.setX(fromOutcomeX(bone.tailPosition.x()));
node.setY(fromOutcomeY(bone.tailPosition.y()));
node.setZ(fromOutcomeZ(bone.tailPosition.z()));
nodeMap[node.id] = node;
newAddedNodeIds.insert(node.id);
(*boneNameToIdsMap)[bone.name] = {firstNodeId, node.id};
SkeletonEdge edge;
edge.partId = (*m_partIdMap)[side];
edge.id = QUuid::createUuid();
partMap[node.partId].nodeIds.push_back(node.id);
edge.nodeIds.push_back(firstNodeId);
edge.nodeIds.push_back(node.id);
edgeMap[edge.id] = edge;
newAddedEdgeIds.insert(edge.id);
nodeMap[firstNodeId].edgeIds.push_back(edge.id);
nodeMap[node.id].edgeIds.push_back(edge.id);
//qDebug() << "Add pair:" << bone.name << "->" << "~";
continue;
}
if (boneIndexToHeadNodeIdMap.find(i) == boneIndexToHeadNodeIdMap.end())
continue;
for (const auto &child: bone.children) {
if (boneIndexToHeadNodeIdMap.find(child) == boneIndexToHeadNodeIdMap.end())
continue;
(*boneNameToIdsMap)[bone.name] = {boneIndexToHeadNodeIdMap[i], boneIndexToHeadNodeIdMap[child]};
}
}
auto findRootNodeId = boneIndexToHeadNodeIdMap.find(0);
if (findRootNodeId != boneIndexToHeadNodeIdMap.end()) {
nodeMap[findRootNodeId->second].setRadius(m_nodeRadius * 2);
}
if (isOther) {
for (const auto &nodeIt: newAddedNodeIds)
m_otherIds.insert(nodeIt);
for (const auto &edgeIt: newAddedEdgeIds)
m_otherIds.insert(edgeIt);
}
for (const auto &nodeIt: newAddedNodeIds) {
emit nodeAdded(nodeIt);
}
for (const auto &edgeIt: newAddedEdgeIds) {
emit edgeAdded(edgeIt);
}
for (const auto &it: *m_partIdMap) {
emit partVisibleStateChanged(it.second);
}
}
void PoseDocument::moveNodeBy(QUuid nodeId, float x, float y, float z)
{
auto it = nodeMap.find(nodeId);
if (it == nodeMap.end()) {
qDebug() << "Find node failed:" << nodeId;
return;
}
it->second.addX(x);
it->second.addY(y);
it->second.addZ(z);
emit nodeOriginChanged(it->first);
emit parametersChanged();
}
void PoseDocument::setNodeOrigin(QUuid nodeId, float x, float y, float z)
{
auto it = nodeMap.find(nodeId);
if (it == nodeMap.end()) {
qDebug() << "Find node failed:" << nodeId;
return;
}
it->second.setX(x);
it->second.setY(y);
it->second.setZ(z);
auto part = partMap.find(it->second.partId);
if (part != partMap.end())
part->second.dirty = true;
emit nodeOriginChanged(nodeId);
emit parametersChanged();
}
float PoseDocument::findFootBottomY() const
{
auto maxY = std::numeric_limits<float>::lowest();
for (const auto &nodeIt: nodeMap) {
auto y = nodeIt.second.getY() + nodeIt.second.radius;
if (y > maxY)
maxY = y;
}
return maxY;
}
void PoseDocument::toParameters(std::map<QString, std::map<QString, QString>> &parameters, const std::set<QUuid> &limitNodeIds) const
{
for (const auto &item: m_boneNameToIdsMap) {
const auto &boneNodeIdPair = item.second;
auto findFirstNode = nodeMap.find(boneNodeIdPair.first);
if (findFirstNode == nodeMap.end()) {
continue;
}
auto findSecondNode = nodeMap.find(boneNodeIdPair.second);
if (findSecondNode == nodeMap.end()) {
continue;
}
if (limitNodeIds.empty() || limitNodeIds.find(boneNodeIdPair.first) != limitNodeIds.end() ||
limitNodeIds.find(boneNodeIdPair.second) != limitNodeIds.end()) {
auto &boneParameter = parameters[item.first];
boneParameter["fromX"] = QString::number(toOutcomeX(findFirstNode->second.getX()));
boneParameter["fromY"] = QString::number(toOutcomeY(findFirstNode->second.getY()));
boneParameter["fromZ"] = QString::number(toOutcomeZ(findFirstNode->second.getZ()));
boneParameter["toX"] = QString::number(toOutcomeX(findSecondNode->second.getX()));
boneParameter["toY"] = QString::number(toOutcomeY(findSecondNode->second.getY()));
boneParameter["toZ"] = QString::number(toOutcomeZ(findSecondNode->second.getZ()));
}
}
}
float PoseDocument::fromOutcomeX(float x)
{
return x * m_outcomeScaleFactor + 0.5;
}
float PoseDocument::toOutcomeX(float x)
{
return (x - 0.5) / m_outcomeScaleFactor;
}
float PoseDocument::fromOutcomeY(float y)
{
return -y * m_outcomeScaleFactor + 0.5;
}
float PoseDocument::toOutcomeY(float y)
{
return (0.5 - y) / m_outcomeScaleFactor;
}
float PoseDocument::fromOutcomeZ(float z)
{
return -z * m_outcomeScaleFactor + 1;
}
float PoseDocument::toOutcomeZ(float z)
{
return (1.0 - z) / m_outcomeScaleFactor;
}
QString PoseDocument::findBoneNameByNodeId(const QUuid &nodeId)
{
for (const auto &item: m_boneNameToIdsMap) {
if (nodeId == item.second.first || nodeId == item.second.second)
return item.first;
}
return QString();
}
void PoseDocument::switchChainSide(const std::set<QUuid> nodeIds)
{
QRegularExpression reJoints("^(Left|Right)([a-zA-Z]+\\d*)_(Joint\\d+)$");
std::set<QString> baseNames;
for (const auto &nodeId: nodeIds) {
QString boneName = findBoneNameByNodeId(nodeId);
if (boneName.isEmpty()) {
//qDebug() << "Find bone name for node failed:" << nodeId;
continue;
}
QRegularExpressionMatch match = reJoints.match(boneName);
if (!match.hasMatch()) {
//qDebug() << "Match bone name for side failed:" << boneName;
continue;
}
QString baseName = match.captured(2);
baseNames.insert(baseName);
}
auto switchYZ = [=](const QUuid &first, const QUuid &second) {
auto findFirstNode = nodeMap.find(first);
if (findFirstNode == nodeMap.end())
return;
auto findSecondNode = nodeMap.find(second);
if (findSecondNode == nodeMap.end())
return;
{
float firstNodeY = findFirstNode->second.getY();
float secondNodeY = findSecondNode->second.getY();
findFirstNode->second.setY(secondNodeY);
findSecondNode->second.setY(firstNodeY);
}
//std::swap(findFirstNode->second.y, findSecondNode->second.y);
{
float firstNodeZ = findFirstNode->second.getZ();
float secondNodeZ = findSecondNode->second.getZ();
findFirstNode->second.setZ(secondNodeZ);
findSecondNode->second.setZ(firstNodeZ);
}
//std::swap(findFirstNode->second.z, findSecondNode->second.z);
emit nodeOriginChanged(first);
emit nodeOriginChanged(second);
};
std::set<std::pair<QUuid, QUuid>> switchPairs;
for (const auto &baseName: baseNames) {
for (const auto &item: m_boneNameToIdsMap) {
QRegularExpressionMatch match = reJoints.match(item.first);
if (!match.hasMatch())
continue;
QString itemSide = match.captured(1);
QString itemBaseName = match.captured(2);
QString itemJointName = match.captured(3);
//qDebug() << "itemSide:" << itemSide << "itemBaseName:" << itemBaseName << "itemJointName:" << itemJointName;
if (itemBaseName == baseName && "Left" == itemSide) {
QString otherSide = "Right";
QString pairedName = otherSide + itemBaseName + "_" + itemJointName;
const auto findPaired = m_boneNameToIdsMap.find(pairedName);
if (findPaired == m_boneNameToIdsMap.end()) {
qDebug() << "Couldn't find paired name:" << pairedName;
continue;
}
//qDebug() << "Switched:" << pairedName;
switchPairs.insert({item.second.first, findPaired->second.first});
switchPairs.insert({item.second.second, findPaired->second.second});
}
}
}
for (const auto &pair: switchPairs) {
switchYZ(pair.first, pair.second);
}
//qDebug() << "switchedPairNum:" << switchPairs.size();
if (!switchPairs.empty())
emit parametersChanged();
}
void PoseDocument::firstSpinePositionAndLengthFromParameters(const std::map<QString, std::map<QString, QString>> &parameters,
float *length, QVector3D *position)
{
*length = 0.0;
*position = QVector3D(0.0, 0.0, 0.0);
const auto &findFirstSpine = parameters.find("Spine1");
if (findFirstSpine == parameters.end())
return;
QVector3D head = QVector3D(valueOfKeyInMapOrEmpty(findFirstSpine->second, "fromX").toFloat(),
valueOfKeyInMapOrEmpty(findFirstSpine->second, "fromY").toFloat(),
valueOfKeyInMapOrEmpty(findFirstSpine->second, "fromZ").toFloat());
QVector3D tail = QVector3D(valueOfKeyInMapOrEmpty(findFirstSpine->second, "toX").toFloat(),
valueOfKeyInMapOrEmpty(findFirstSpine->second, "toY").toFloat(),
valueOfKeyInMapOrEmpty(findFirstSpine->second, "toZ").toFloat());
*length = head.distanceToPoint(tail);
*position = head;
}
void PoseDocument::neckJoint1DirectionFromParameters(const std::map<QString, std::map<QString, QString>> &parameters,
QVector3D *direction)
{
*direction = QVector3D(0.0, 1.0, 0.0);
const auto &findNeckJoint1 = parameters.find("Neck_Joint1");
if (findNeckJoint1 == parameters.end())
return;
QVector3D head = QVector3D(valueOfKeyInMapOrEmpty(findNeckJoint1->second, "fromX").toFloat(),
valueOfKeyInMapOrEmpty(findNeckJoint1->second, "fromY").toFloat(),
valueOfKeyInMapOrEmpty(findNeckJoint1->second, "fromZ").toFloat());
QVector3D tail = QVector3D(valueOfKeyInMapOrEmpty(findNeckJoint1->second, "toX").toFloat(),
valueOfKeyInMapOrEmpty(findNeckJoint1->second, "toY").toFloat(),
valueOfKeyInMapOrEmpty(findNeckJoint1->second, "toZ").toFloat());
*direction = (tail - head).normalized();
}

View File

@ -1,101 +0,0 @@
#ifndef DUST3D_POSE_DOCUMENT_H
#define DUST3D_POSE_DOCUMENT_H
#include <map>
#include <QString>
#include <deque>
#include "skeletondocument.h"
#include "rigger.h"
struct PoseHistoryItem
{
std::map<QString, std::map<QString, QString>> parameters;
};
class PoseDocument : public SkeletonDocument
{
Q_OBJECT
signals:
void turnaroundChanged();
void cleanup();
void nodeAdded(QUuid nodeId);
void edgeAdded(QUuid edgeId);
void nodeOriginChanged(QUuid nodeId);
void parametersChanged();
void sideVisibleStateChanged(SkeletonSide side);
void partVisibleStateChanged(QUuid partId);
public:
bool undoable() const override;
bool redoable() const override;
bool hasPastableNodesInClipboard() const override;
bool originSettled() const override;
bool isNodeEditable(QUuid nodeId) const override;
bool isEdgeEditable(QUuid edgeId) const override;
bool isNodeDeactivated(QUuid nodeId) const override;
bool isEdgeDeactivated(QUuid edgeId) const override;
void copyNodes(std::set<QUuid> nodeIdSet) const override;
void updateTurnaround(const QImage &image);
void updateOtherFramesParameters(const std::vector<std::map<QString, std::map<QString, QString>>> &otherFramesParameters);
void resetWithoutNotifingParametersChanged();
void reset();
void toParameters(std::map<QString, std::map<QString, QString>> &parameters, const std::set<QUuid> &limitNodeIds=std::set<QUuid>()) const;
void fromParameters(const std::vector<RiggerBone> *rigBones,
const std::map<QString, std::map<QString, QString>> &parameters);
bool isSideVisible(SkeletonSide side);
public slots:
void saveHistoryItem();
void clearHistories();
void undo() override;
void redo() override;
void paste() override;
void moveNodeBy(QUuid nodeId, float x, float y, float z);
void setNodeOrigin(QUuid nodeId, float x, float y, float z);
void switchChainSide(const std::set<QUuid> nodeIds);
void setSideVisiableState(SkeletonSide side, bool visible);
public:
static const float m_nodeRadius;
static const float m_groundPlaneHalfThickness;
static const bool m_hideRootAndVirtual;
static const float m_outcomeScaleFactor;
private:
QString findBoneNameByNodeId(const QUuid &nodeId);
float findFootBottomY() const;
void parametersToNodes(const std::vector<RiggerBone> *rigBones,
std::map<QString, std::pair<QUuid, QUuid>> *boneNameToIdsMap,
std::map<SkeletonSide, QUuid> *m_partIdMap,
bool isOther=false);
void updateBonesFromParameters(std::vector<RiggerBone> *bones,
const std::map<QString, std::map<QString, QString>> &parameters,
float firstSpineBoneLength,
const QVector3D &firstSpineBonePosition,
const QVector3D &neckJoint1BoneDirection);
std::map<QString, std::pair<QUuid, QUuid>> m_boneNameToIdsMap;
std::map<SkeletonSide, QUuid> m_partIdMap;
std::deque<PoseHistoryItem> m_undoItems;
std::deque<PoseHistoryItem> m_redoItems;
std::vector<RiggerBone> m_riggerBones;
std::vector<std::map<QString, std::map<QString, QString>>> m_otherFramesParameters;
std::set<QUuid> m_otherIds;
std::set<SkeletonSide> m_hiddenSides;
static float fromOutcomeX(float x);
static float toOutcomeX(float x);
static float fromOutcomeY(float y);
static float toOutcomeY(float y);
static float fromOutcomeZ(float z);
static float toOutcomeZ(float z);
static void firstSpinePositionAndLengthFromParameters(const std::map<QString, std::map<QString, QString>> &parameters,
float *length, QVector3D *position);
static void neckJoint1DirectionFromParameters(const std::map<QString, std::map<QString, QString>> &parameters,
QVector3D *direction);
};
#endif

View File

@ -1,668 +0,0 @@
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFormLayout>
#include <QGridLayout>
#include <QMenu>
#include <QWidgetAction>
#include <QLineEdit>
#include <QMessageBox>
#include <QFileDialog>
#include <QSpinBox>
#include <QSlider>
#include "theme.h"
#include "poseeditwidget.h"
#include "floatnumberwidget.h"
#include "version.h"
#include "poserconstruct.h"
#include "graphicscontainerwidget.h"
#include "documentwindow.h"
#include "shortcuts.h"
#include "imageforever.h"
float PoseEditWidget::m_defaultBlur = 0.5;
PoseEditWidget::PoseEditWidget(const Document *document, QWidget *parent) :
QDialog(parent),
m_document(document),
m_poseDocument(new PoseDocument)
{
setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint);
m_posePreviewManager = new PosePreviewManager();
connect(m_posePreviewManager, &PosePreviewManager::renderDone, [=]() {
if (m_closed) {
close();
return;
}
if (m_isPreviewDirty)
updatePreview();
});
connect(m_posePreviewManager, &PosePreviewManager::resultPreviewMeshChanged, [=]() {
m_previewWidget->updateMesh(m_posePreviewManager->takeResultPreviewMesh());
});
SkeletonGraphicsWidget *graphicsWidget = new SkeletonGraphicsWidget(m_poseDocument);
graphicsWidget->setNodePositionModifyOnly(true);
graphicsWidget->setBackgroundBlur(m_defaultBlur);
m_poseGraphicsWidget = graphicsWidget;
initShortCuts(this, graphicsWidget);
GraphicsContainerWidget *containerWidget = new GraphicsContainerWidget;
containerWidget->setGraphicsWidget(graphicsWidget);
QGridLayout *containerLayout = new QGridLayout;
containerLayout->setSpacing(0);
containerLayout->setContentsMargins(1, 0, 0, 0);
containerLayout->addWidget(graphicsWidget);
containerWidget->setLayout(containerLayout);
containerWidget->setMinimumSize(400, 400);
m_previewWidget = new ModelWidget(this);
m_previewWidget->setFixedSize(384, 384);
m_previewWidget->enableMove(true);
m_previewWidget->enableZoom(false);
m_previewWidget->move(-64, 0);
m_poseGraphicsWidget->setModelWidget(m_previewWidget);
containerWidget->setModelWidget(m_previewWidget);
connect(containerWidget, &GraphicsContainerWidget::containerSizeChanged,
graphicsWidget, &SkeletonGraphicsWidget::canvasResized);
connect(graphicsWidget, &SkeletonGraphicsWidget::moveNodeBy, m_poseDocument, &PoseDocument::moveNodeBy);
connect(graphicsWidget, &SkeletonGraphicsWidget::setNodeOrigin, m_poseDocument, &PoseDocument::setNodeOrigin);
connect(graphicsWidget, &SkeletonGraphicsWidget::groupOperationAdded, m_poseDocument, &PoseDocument::saveHistoryItem);
connect(graphicsWidget, &SkeletonGraphicsWidget::undo, m_poseDocument, &PoseDocument::undo);
connect(graphicsWidget, &SkeletonGraphicsWidget::redo, m_poseDocument, &PoseDocument::redo);
connect(graphicsWidget, &SkeletonGraphicsWidget::paste, m_poseDocument, &PoseDocument::paste);
connect(graphicsWidget, &SkeletonGraphicsWidget::switchChainSide, m_poseDocument, &PoseDocument::switchChainSide);
connect(m_poseDocument, &PoseDocument::cleanup, graphicsWidget, &SkeletonGraphicsWidget::removeAllContent);
connect(m_poseDocument, &PoseDocument::nodeAdded, graphicsWidget, &SkeletonGraphicsWidget::nodeAdded);
connect(m_poseDocument, &PoseDocument::edgeAdded, graphicsWidget, &SkeletonGraphicsWidget::edgeAdded);
connect(m_poseDocument, &PoseDocument::nodeOriginChanged, graphicsWidget, &SkeletonGraphicsWidget::nodeOriginChanged);
connect(m_poseDocument, &PoseDocument::partVisibleStateChanged, graphicsWidget, &SkeletonGraphicsWidget::partVisibleStateChanged);
connect(m_poseDocument, &PoseDocument::parametersChanged, this, [&]() {
m_currentParameters.clear();
m_poseDocument->toParameters(m_currentParameters);
syncFrameFromCurrent();
emit parametersAdjusted();
});
QSlider *opacitySlider = new QSlider(Qt::Horizontal);
opacitySlider->setFixedWidth(100);
opacitySlider->setMaximum(10);
opacitySlider->setMinimum(0);
opacitySlider->setValue(m_defaultBlur * 10);
connect(opacitySlider, &QSlider::valueChanged, this, [=](int value) {
graphicsWidget->setBackgroundBlur((float)value / 10);
});
auto updateSideVisibleButtonState = [=](QPushButton *button, SkeletonSide side) {
this->updateSideButtonState(button, m_poseDocument->isSideVisible(side));
};
QPushButton *leftSideVisibleButton = new QPushButton(QChar('L'));
leftSideVisibleButton->setToolTip(tr("Toggle Left Side Visibility"));
initSideButton(leftSideVisibleButton);
updateSideVisibleButtonState(leftSideVisibleButton, SkeletonSide::Left);
connect(leftSideVisibleButton, &QPushButton::clicked, this, [=]() {
m_poseDocument->setSideVisiableState(SkeletonSide::Left, !m_poseDocument->isSideVisible(SkeletonSide::Left));
});
QPushButton *middleSideVisibleButton = new QPushButton(QChar('M'));
middleSideVisibleButton->setToolTip(tr("Toggle Middle Side Visibility"));
initSideButton(middleSideVisibleButton);
updateSideVisibleButtonState(middleSideVisibleButton, SkeletonSide::None);
connect(middleSideVisibleButton, &QPushButton::clicked, this, [=]() {
m_poseDocument->setSideVisiableState(SkeletonSide::None, !m_poseDocument->isSideVisible(SkeletonSide::None));
});
QPushButton *rightSideVisibleButton = new QPushButton(QChar('R'));
rightSideVisibleButton->setToolTip(tr("Toggle Right Side Visibility"));
initSideButton(rightSideVisibleButton);
updateSideVisibleButtonState(rightSideVisibleButton, SkeletonSide::Right);
connect(rightSideVisibleButton, &QPushButton::clicked, this, [=]() {
m_poseDocument->setSideVisiableState(SkeletonSide::Right, !m_poseDocument->isSideVisible(SkeletonSide::Right));
});
connect(m_poseDocument, &PoseDocument::sideVisibleStateChanged, this, [=](SkeletonSide side) {
switch (side) {
case SkeletonSide::Left:
updateSideVisibleButtonState(leftSideVisibleButton, side);
break;
case SkeletonSide::None:
updateSideVisibleButtonState(middleSideVisibleButton, side);
break;
case SkeletonSide::Right:
updateSideVisibleButtonState(rightSideVisibleButton, side);
break;
}
});
QHBoxLayout *sideButtonLayout = new QHBoxLayout;
sideButtonLayout->setSpacing(0);
sideButtonLayout->addStretch();
sideButtonLayout->addWidget(rightSideVisibleButton);
sideButtonLayout->addWidget(middleSideVisibleButton);
sideButtonLayout->addWidget(leftSideVisibleButton);
sideButtonLayout->addStretch();
QHBoxLayout *sliderLayout = new QHBoxLayout;
sliderLayout->addStretch();
sliderLayout->addSpacing(50);
sliderLayout->addWidget(new QLabel(tr("Dark")));
sliderLayout->addWidget(opacitySlider);
sliderLayout->addWidget(new QLabel(tr("Bright")));
sliderLayout->addSpacing(50);
sliderLayout->addStretch();
QVBoxLayout *previewLayout = new QVBoxLayout;
previewLayout->addStretch();
previewLayout->addLayout(sideButtonLayout);
previewLayout->addLayout(sliderLayout);
previewLayout->addSpacing(20);
QHBoxLayout *paramtersLayout = new QHBoxLayout;
paramtersLayout->addWidget(containerWidget);
QHBoxLayout *topLayout = new QHBoxLayout;
topLayout->addLayout(previewLayout);
topLayout->addWidget(Theme::createVerticalLineWidget());
topLayout->addLayout(paramtersLayout);
topLayout->setStretch(2, 1);
m_nameEdit = new QLineEdit;
m_nameEdit->setFixedWidth(200);
connect(m_nameEdit, &QLineEdit::textChanged, this, [=]() {
setUnsaveState();
});
m_durationEdit = new QDoubleSpinBox();
m_durationEdit->setDecimals(2);
m_durationEdit->setMaximum(60);
m_durationEdit->setMinimum(0);
m_durationEdit->setSingleStep(0.1);
m_durationEdit->setValue(m_duration);
connect(m_durationEdit, static_cast<void (QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged), this, [=](double value) {
setDuration((float)value);
});
QPushButton *saveButton = new QPushButton(tr("Save"));
connect(saveButton, &QPushButton::clicked, this, &PoseEditWidget::save);
saveButton->setDefault(true);
QPushButton *setPoseSettingButton = new QPushButton(Theme::awesome()->icon(fa::gear), tr(""));
connect(setPoseSettingButton, &QPushButton::clicked, this, [=]() {
showPoseSettingPopup(mapFromGlobal(QCursor::pos()));
});
QPushButton *changeReferenceSheet = new QPushButton(tr("Change Reference Sheet..."));
connect(changeReferenceSheet, &QPushButton::clicked, this, &PoseEditWidget::changeTurnaround);
connect(m_poseDocument, &PoseDocument::turnaroundChanged,
graphicsWidget, &SkeletonGraphicsWidget::turnaroundChanged);
m_framesSettingButton = new QPushButton();
connect(m_framesSettingButton, &QPushButton::clicked, this, [=]() {
showFramesSettingPopup(mapFromGlobal(QCursor::pos()));
});
m_currentFrameSlider = new QSlider(Qt::Horizontal);
m_currentFrameSlider->setRange(0, m_frames.size() - 1);
m_currentFrameSlider->setValue(m_currentFrame);
//m_currentFrameSlider->hide();
connect(m_currentFrameSlider, static_cast<void (QSlider::*)(int)>(&QSlider::valueChanged), this, [=](int value) {
setCurrentFrame(value);
});
connect(m_document, &Document::resultRigChanged, this, &PoseEditWidget::updatePoseDocument);
QPushButton *moveToFirstFrameButton = new QPushButton(Theme::awesome()->icon(fa::angledoubleleft), "");
connect(moveToFirstFrameButton, &QPushButton::clicked, this, [=]() {
setCurrentFrame(0);
});
QPushButton *moveToPreviousFrameButton = new QPushButton(Theme::awesome()->icon(fa::angleleft), "");
connect(moveToPreviousFrameButton, &QPushButton::clicked, this, [=]() {
if (m_currentFrame > 0)
setCurrentFrame(m_currentFrame - 1);
});
QPushButton *moveToNextFrameButton = new QPushButton(Theme::awesome()->icon(fa::angleright), "");
connect(moveToNextFrameButton, &QPushButton::clicked, this, [=]() {
if (m_currentFrame + 1 < (int)m_frames.size())
setCurrentFrame(m_currentFrame + 1);
});
QPushButton *moveToLastFrameButton = new QPushButton(Theme::awesome()->icon(fa::angledoubleright), "");
connect(moveToLastFrameButton, &QPushButton::clicked, this, [=]() {
if (!m_frames.empty())
setCurrentFrame(m_frames.size() - 1);
});
QPushButton *insertAfterFrameButton = new QPushButton(Theme::awesome()->icon(fa::plus), "");
connect(insertAfterFrameButton, &QPushButton::clicked, this, &PoseEditWidget::insertFrameAfterCurrentFrame);
QPushButton *deleteFrameButton = new QPushButton(Theme::awesome()->icon(fa::trash), "");
connect(deleteFrameButton, &QPushButton::clicked, this, &PoseEditWidget::removeCurrentFrame);
QHBoxLayout *timelineLayout = new QHBoxLayout;
timelineLayout->addWidget(insertAfterFrameButton);
timelineLayout->addWidget(moveToFirstFrameButton);
timelineLayout->addWidget(moveToPreviousFrameButton);
timelineLayout->addWidget(moveToNextFrameButton);
timelineLayout->addWidget(moveToLastFrameButton);
timelineLayout->addWidget(m_framesSettingButton);
timelineLayout->addWidget(m_currentFrameSlider);
timelineLayout->addWidget(deleteFrameButton);
timelineLayout->setStretch(6, 1);
QHBoxLayout *baseInfoLayout = new QHBoxLayout;
baseInfoLayout->addWidget(new QLabel(tr("Name")));
baseInfoLayout->addWidget(m_nameEdit);
baseInfoLayout->addSpacing(10);
baseInfoLayout->addWidget(new QLabel(tr("Duration")));
baseInfoLayout->addWidget(m_durationEdit);
baseInfoLayout->addStretch();
baseInfoLayout->addWidget(setPoseSettingButton);
baseInfoLayout->addWidget(changeReferenceSheet);
baseInfoLayout->addWidget(saveButton);
QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addLayout(topLayout);
mainLayout->addWidget(Theme::createHorizontalLineWidget());
mainLayout->addLayout(timelineLayout);
mainLayout->addLayout(baseInfoLayout);
setLayout(mainLayout);
connect(this, &PoseEditWidget::parametersAdjusted, this, &PoseEditWidget::updatePreview);
connect(this, &PoseEditWidget::parametersAdjusted, [=]() {
setUnsaveState();
});
connect(this, &PoseEditWidget::addPose, m_document, &Document::addPose);
connect(this, &PoseEditWidget::renamePose, m_document, &Document::renamePose);
connect(this, &PoseEditWidget::setPoseFrames, m_document, &Document::setPoseFrames);
connect(this, &PoseEditWidget::setPoseTurnaroundImageId, m_document, &Document::setPoseTurnaroundImageId);
connect(this, &PoseEditWidget::setPoseYtranslationScale, m_document, &Document::setPoseYtranslationScale);
updatePoseDocument();
updateTitle();
updateFramesSettingButton();
m_poseDocument->saveHistoryItem();
}
void PoseEditWidget::initSideButton(QPushButton *button)
{
QFont font;
font.setWeight(QFont::Light);
font.setPixelSize(Theme::toolIconFontSize);
font.setBold(false);
button->setFont(font);
button->setFixedSize(Theme::toolIconSize, Theme::toolIconSize);
button->setStyleSheet("QPushButton {color: " + Theme::white.name() + "}");
button->setFocusPolicy(Qt::NoFocus);
}
void PoseEditWidget::updateSideButtonState(QPushButton *button, bool visible)
{
if (visible)
button->setStyleSheet("QPushButton {color: " + Theme::white.name() + "}");
else
button->setStyleSheet("QPushButton {color: " + Theme::black.name() + "}");
}
void PoseEditWidget::showPoseSettingPopup(const QPoint &pos)
{
QMenu popupMenu;
QWidget *popup = new QWidget;
FloatNumberWidget *yTranslationScaleWidget = new FloatNumberWidget;
yTranslationScaleWidget->setItemName(tr("Height Adjustment Scale"));
yTranslationScaleWidget->setRange(0, 1);
yTranslationScaleWidget->setValue(m_yTranslationScale);
connect(yTranslationScaleWidget, &FloatNumberWidget::valueChanged, [&](float value) {
m_yTranslationScale = value;
setUnsaveState();
});
QPushButton *yTranslationScaleEraser = new QPushButton(QChar(fa::eraser));
Theme::initAwesomeToolButton(yTranslationScaleEraser);
connect(yTranslationScaleEraser, &QPushButton::clicked, [=]() {
yTranslationScaleWidget->setValue(1.0);
});
QHBoxLayout *yTranslationScaleLayout = new QHBoxLayout;
yTranslationScaleLayout->addWidget(yTranslationScaleEraser);
yTranslationScaleLayout->addWidget(yTranslationScaleWidget);
popup->setLayout(yTranslationScaleLayout);
QWidgetAction *action = new QWidgetAction(this);
action->setDefaultWidget(popup);
popupMenu.addAction(action);
popupMenu.exec(mapToGlobal(pos));
}
void PoseEditWidget::showFramesSettingPopup(const QPoint &pos)
{
QMenu popupMenu;
QWidget *popup = new QWidget;
QSpinBox *framesEdit = new QSpinBox();
framesEdit->setMaximum(m_frames.size());
framesEdit->setMinimum(1);
framesEdit->setSingleStep(1);
framesEdit->setValue(m_frames.size());
connect(framesEdit, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, [=](int value) {
setCurrentFrame(value);
});
QFormLayout *formLayout = new QFormLayout;
formLayout->addRow(tr("Frames:"), framesEdit);
popup->setLayout(formLayout);
QWidgetAction *action = new QWidgetAction(this);
action->setDefaultWidget(popup);
popupMenu.addAction(action);
popupMenu.exec(mapToGlobal(pos));
}
void PoseEditWidget::updateFramesSettingButton()
{
m_currentFrameSlider->setRange(0, m_frames.size() - 1);
if (m_currentFrame != m_currentFrameSlider->value())
m_currentFrameSlider->setValue(m_currentFrame);
m_framesSettingButton->setText(tr("Frame: %1 / %2").arg(QString::number(m_currentFrame + 1).rightJustified(2, ' ')).arg(QString::number(m_frames.size()).leftJustified(2, ' ')));
}
void PoseEditWidget::ensureEnoughFrames()
{
if (m_currentFrame >= (int)m_frames.size()) {
m_frames.resize(m_currentFrame + 1);
setUnsaveState();
updateFramesSettingButton();
}
}
void PoseEditWidget::syncFrameFromCurrent()
{
ensureEnoughFrames();
m_frames[m_currentFrame] = {m_currentAttributes, m_currentParameters};
updateFramesDurations();
}
void PoseEditWidget::updateFramesDurations()
{
if (m_frames.empty())
return;
float frameDuration = m_duration / m_frames.size();
for (auto &frame: m_frames)
frame.first["duration"] = QString::number(frameDuration);
}
void PoseEditWidget::setDuration(float duration)
{
if (qFuzzyCompare(duration, m_duration))
return;
m_duration = duration;
setUnsaveState();
updateFramesDurations();
}
void PoseEditWidget::setCurrentFrame(int frame)
{
if (m_currentFrame == frame)
return;
m_currentFrame = frame;
ensureEnoughFrames();
updateFramesSettingButton();
m_currentAttributes = m_frames[m_currentFrame].first;
m_currentParameters = m_frames[m_currentFrame].second;
updatePoseDocument();
}
void PoseEditWidget::insertFrameAfterCurrentFrame()
{
int currentFrame = m_currentFrame;
m_frames.resize(m_frames.size() + 1);
updateFramesDurations();
if (-1 != currentFrame) {
for (int index = m_frames.size() - 1; index > currentFrame; --index) {
m_frames[index] = m_frames[index - 1];
}
}
setUnsaveState();
setCurrentFrame(currentFrame + 1);
}
void PoseEditWidget::removeCurrentFrame()
{
if (m_frames.size() <= 1)
return;
int currentFrame = m_currentFrame;
if (-1 != currentFrame) {
for (int index = currentFrame + 1; index < (int)m_frames.size(); ++index) {
m_frames[index - 1] = m_frames[index];
}
m_frames.resize(m_frames.size() - 1);
}
updateFramesDurations();
setUnsaveState();
if (currentFrame - 1 >= 0)
setCurrentFrame(currentFrame - 1);
else if (currentFrame < (int)m_frames.size()) {
m_currentFrame = -1;
setCurrentFrame(currentFrame);
} else
setCurrentFrame(0);
}
void PoseEditWidget::changeTurnaround()
{
QString fileName = QFileDialog::getOpenFileName(this, QString(), QString(),
tr("Image Files (*.png *.jpg *.bmp)")).trimmed();
if (fileName.isEmpty())
return;
QImage image;
if (!image.load(fileName))
return;
auto newImageId = ImageForever::add(&image);
if (m_imageId == newImageId)
return;
setUnsaveState();
m_imageId = newImageId;
m_poseDocument->updateTurnaround(image);
}
void PoseEditWidget::updatePoseDocument()
{
m_otherFramesParameters.clear();
for (int i = 0; i < (int)m_frames.size(); ++i) {
if (m_currentFrame == i)
continue;
m_otherFramesParameters.push_back(m_frames[i].second);
}
m_poseDocument->updateOtherFramesParameters(m_otherFramesParameters);
m_poseDocument->fromParameters(m_document->resultRigBones(), m_currentParameters);
m_poseDocument->clearHistories();
m_poseDocument->saveHistoryItem();
updatePreview();
}
void PoseEditWidget::reject()
{
close();
}
void PoseEditWidget::closeEvent(QCloseEvent *event)
{
if (m_unsaved && !m_closed) {
QMessageBox::StandardButton answer = QMessageBox::question(this,
APP_NAME,
tr("Do you really want to close while there are unsaved changes?"),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::No);
if (answer != QMessageBox::Yes) {
event->ignore();
return;
}
}
m_closed = true;
hide();
if (m_posePreviewManager->isRendering()) {
event->ignore();
return;
}
event->accept();
}
QSize PoseEditWidget::sizeHint() const
{
return QSize(1024, 768);
}
PoseEditWidget::~PoseEditWidget()
{
delete m_posePreviewManager;
delete m_poseDocument;
}
void PoseEditWidget::updatePreview()
{
if (m_closed)
return;
if (m_posePreviewManager->isRendering()) {
m_isPreviewDirty = true;
return;
}
const std::vector<RiggerBone> *rigBones = m_document->resultRigBones();
const std::map<int, RiggerVertexWeights> *rigWeights = m_document->resultRigWeights();
m_isPreviewDirty = false;
if (nullptr == rigBones || nullptr == rigWeights) {
return;
}
Poser *poser = newPoser(m_document->rigType, *rigBones);
if (nullptr == poser)
return;
poser->parameters() = m_currentParameters;
poser->commit();
m_posePreviewManager->postUpdate(*poser, m_document->currentRiggedOutcome(), *rigWeights);
delete poser;
}
void PoseEditWidget::setEditPoseId(QUuid poseId)
{
if (m_poseId == poseId)
return;
m_poseId = poseId;
updateTitle();
}
void PoseEditWidget::updateTitle()
{
if (m_poseId.isNull()) {
setWindowTitle(unifiedWindowTitle(tr("New") + (m_unsaved ? "*" : "")));
return;
}
const Pose *pose = m_document->findPose(m_poseId);
if (nullptr == pose) {
qDebug() << "Find pose failed:" << m_poseId;
return;
}
setWindowTitle(unifiedWindowTitle(pose->name + (m_unsaved ? "*" : "")));
}
void PoseEditWidget::setEditPoseName(QString name)
{
m_nameEdit->setText(name);
updateTitle();
}
void PoseEditWidget::setEditPoseFrames(std::vector<std::pair<std::map<QString, QString>, std::map<QString, std::map<QString, QString>>>> frames)
{
m_frames = frames;
if (!m_frames.empty()) {
m_currentFrame = 0;
const auto &frame = m_frames[m_currentFrame];
m_currentAttributes = frame.first;
m_currentParameters = frame.second;
}
float totalDuration = 0;
for (const auto &frame: m_frames) {
float frameDuration = valueOfKeyInMapOrEmpty(frame.first, "duration").toFloat();
totalDuration += frameDuration;
}
if (qFuzzyIsNull(totalDuration))
totalDuration = 1.0;
m_durationEdit->setValue(totalDuration);
updatePoseDocument();
updatePreview();
updateFramesSettingButton();
m_poseDocument->saveHistoryItem();
}
void PoseEditWidget::setEditPoseTurnaroundImageId(QUuid imageId)
{
m_imageId = imageId;
const auto &image = ImageForever::get(m_imageId);
if (nullptr == image)
return;
m_poseDocument->updateTurnaround(*image);
}
void PoseEditWidget::setEditPoseYtranslationScale(float yTranslationScale)
{
m_yTranslationScale = yTranslationScale;
}
void PoseEditWidget::clearUnsaveState()
{
m_unsaved = false;
updateTitle();
}
void PoseEditWidget::setUnsaveState()
{
m_unsaved = true;
updateTitle();
}
void PoseEditWidget::save()
{
if (m_poseId.isNull()) {
m_poseId = QUuid::createUuid();
emit addPose(m_poseId, m_nameEdit->text(), m_frames, m_imageId, m_yTranslationScale);
} else if (m_unsaved) {
emit renamePose(m_poseId, m_nameEdit->text());
emit setPoseFrames(m_poseId, m_frames);
emit setPoseTurnaroundImageId(m_poseId, m_imageId);
emit setPoseYtranslationScale(m_poseId, m_yTranslationScale);
}
clearUnsaveState();
}

View File

@ -1,88 +0,0 @@
#ifndef DUST3D_POSE_EDIT_WIDGET_H
#define DUST3D_POSE_EDIT_WIDGET_H
#include <QDialog>
#include <map>
#include <QCloseEvent>
#include <QLineEdit>
#include <QSlider>
#include <QDoubleSpinBox>
#include "posepreviewmanager.h"
#include "document.h"
#include "modelwidget.h"
#include "rigger.h"
#include "skeletongraphicswidget.h"
#include "posedocument.h"
#include "floatnumberwidget.h"
#include "skeletonside.h"
class PoseEditWidget : public QDialog
{
Q_OBJECT
signals:
void addPose(QUuid poseId, QString name, std::vector<std::pair<std::map<QString, QString>, std::map<QString, std::map<QString, QString>>>> frames, QUuid turnaroundImageId, float yTranslationScale);
void removePose(QUuid poseId);
void setPoseFrames(QUuid poseId, std::vector<std::pair<std::map<QString, QString>, std::map<QString, std::map<QString, QString>>>> frames);
void setPoseTurnaroundImageId(QUuid poseId, QUuid imageId);
void setPoseYtranslationScale(QUuid poseId, float scale);
void renamePose(QUuid poseId, QString name);
void parametersAdjusted();
public:
PoseEditWidget(const Document *document, QWidget *parent=nullptr);
~PoseEditWidget();
float m_duration = 1.0;
public slots:
void updatePoseDocument();
void updatePreview();
void syncFrameFromCurrent();
void setEditPoseId(QUuid poseId);
void setEditPoseName(QString name);
void setEditPoseFrames(std::vector<std::pair<std::map<QString, QString>, std::map<QString, std::map<QString, QString>>>> frames);
void setEditPoseTurnaroundImageId(QUuid imageId);
void setEditPoseYtranslationScale(float yTranslationScale);
void setCurrentFrame(int frame);
void insertFrameAfterCurrentFrame();
void removeCurrentFrame();
void setDuration(float duration);
void updateTitle();
void save();
void clearUnsaveState();
void setUnsaveState();
void changeTurnaround();
private slots:
void updateFramesSettingButton();
void showFramesSettingPopup(const QPoint &pos);
void showPoseSettingPopup(const QPoint &pos);
void updateFramesDurations();
protected:
QSize sizeHint() const override;
void closeEvent(QCloseEvent *event) override;
void reject() override;
private:
void ensureEnoughFrames();
const Document *m_document = nullptr;
PosePreviewManager *m_posePreviewManager = nullptr;
ModelWidget *m_previewWidget = nullptr;
bool m_isPreviewDirty = false;
bool m_closed = false;
std::vector<std::pair<std::map<QString, QString>, std::map<QString, std::map<QString, QString>>>> m_frames;
std::map<QString, QString> m_currentAttributes;
std::map<QString, std::map<QString, QString>> m_currentParameters;
std::vector<std::map<QString, std::map<QString, QString>>> m_otherFramesParameters;
int m_currentFrame = 0;
QUuid m_poseId;
bool m_unsaved = false;
QUuid m_imageId;
float m_yTranslationScale = 1.0;
QLineEdit *m_nameEdit = nullptr;
QDoubleSpinBox *m_durationEdit = nullptr;
PoseDocument *m_poseDocument = nullptr;
SkeletonGraphicsWidget *m_poseGraphicsWidget = nullptr;
QPushButton *m_framesSettingButton = nullptr;
QSlider *m_currentFrameSlider = nullptr;
static float m_defaultBlur;
void initSideButton(QPushButton *button);
void updateSideButtonState(QPushButton *button, bool visible);
};
#endif

View File

@ -1,307 +0,0 @@
#include <QGuiApplication>
#include <QMenu>
#include <QXmlStreamWriter>
#include <QClipboard>
#include <QApplication>
#include "snapshotxml.h"
#include "poselistwidget.h"
PoseListWidget::PoseListWidget(const Document *document, QWidget *parent) :
QTreeWidget(parent),
m_document(document)
{
setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
setFocusPolicy(Qt::NoFocus);
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setAutoScroll(false);
setHeaderHidden(true);
QPalette palette = this->palette();
palette.setColor(QPalette::Window, Qt::transparent);
palette.setColor(QPalette::Base, Qt::transparent);
setPalette(palette);
setStyleSheet("QTreeView {qproperty-indentation: 0;}");
setContentsMargins(0, 0, 0, 0);
connect(document, &Document::poseListChanged, this, &PoseListWidget::reload);
connect(document, &Document::cleanup, this, &PoseListWidget::removeAllContent);
connect(this, &PoseListWidget::removePose, document, &Document::removePose);
setContextMenuPolicy(Qt::CustomContextMenu);
connect(this, &QTreeWidget::customContextMenuRequested, this, &PoseListWidget::showContextMenu);
reload();
}
void PoseListWidget::poseRemoved(QUuid poseId)
{
if (m_currentSelectedPoseId == poseId)
m_currentSelectedPoseId = QUuid();
m_selectedPoseIds.erase(poseId);
m_itemMap.erase(poseId);
}
void PoseListWidget::updatePoseSelectState(QUuid poseId, bool selected)
{
auto findItemResult = m_itemMap.find(poseId);
if (findItemResult == m_itemMap.end()) {
qDebug() << "Find pose item failed:" << poseId;
return;
}
PoseWidget *poseWidget = (PoseWidget *)itemWidget(findItemResult->second.first, findItemResult->second.second);
poseWidget->updateCheckedState(selected);
if (m_cornerButtonVisible) {
poseWidget->setCornerButtonVisible(selected);
}
}
void PoseListWidget::selectPose(QUuid poseId, bool multiple)
{
if (multiple) {
if (!m_currentSelectedPoseId.isNull()) {
m_selectedPoseIds.insert(m_currentSelectedPoseId);
m_currentSelectedPoseId = QUuid();
}
if (m_selectedPoseIds.find(poseId) != m_selectedPoseIds.end()) {
updatePoseSelectState(poseId, false);
m_selectedPoseIds.erase(poseId);
} else {
updatePoseSelectState(poseId, true);
m_selectedPoseIds.insert(poseId);
}
if (m_selectedPoseIds.size() > 1) {
return;
}
if (m_selectedPoseIds.size() == 1)
poseId = *m_selectedPoseIds.begin();
else
poseId = QUuid();
}
if (!m_selectedPoseIds.empty()) {
for (const auto &id: m_selectedPoseIds) {
updatePoseSelectState(id, false);
}
m_selectedPoseIds.clear();
}
if (m_currentSelectedPoseId != poseId) {
if (!m_currentSelectedPoseId.isNull()) {
updatePoseSelectState(m_currentSelectedPoseId, false);
}
m_currentSelectedPoseId = poseId;
if (!m_currentSelectedPoseId.isNull()) {
updatePoseSelectState(m_currentSelectedPoseId, true);
}
}
}
void PoseListWidget::mousePressEvent(QMouseEvent *event)
{
QModelIndex itemIndex = indexAt(event->pos());
QTreeView::mousePressEvent(event);
if (event->button() == Qt::LeftButton) {
bool multiple = QGuiApplication::queryKeyboardModifiers().testFlag(Qt::ControlModifier);
if (itemIndex.isValid()) {
QTreeWidgetItem *item = itemFromIndex(itemIndex);
auto poseId = QUuid(item->data(itemIndex.column(), Qt::UserRole).toString());
if (QGuiApplication::queryKeyboardModifiers().testFlag(Qt::ShiftModifier)) {
bool startAdd = false;
bool stopAdd = false;
std::vector<QUuid> waitQueue;
for (const auto &childId: m_document->poseIdList) {
if (m_shiftStartPoseId == childId || poseId == childId) {
if (startAdd) {
stopAdd = true;
} else {
startAdd = true;
}
}
if (startAdd)
waitQueue.push_back(childId);
if (stopAdd)
break;
}
if (stopAdd && !waitQueue.empty()) {
if (!m_selectedPoseIds.empty()) {
for (const auto &id: m_selectedPoseIds) {
updatePoseSelectState(id, false);
}
m_selectedPoseIds.clear();
}
if (!m_currentSelectedPoseId.isNull()) {
m_currentSelectedPoseId = QUuid();
}
for (const auto &waitId: waitQueue) {
selectPose(waitId, true);
}
}
return;
} else {
m_shiftStartPoseId = poseId;
}
selectPose(poseId, multiple);
return;
}
if (!multiple)
selectPose(QUuid());
}
}
bool PoseListWidget::isPoseSelected(QUuid poseId)
{
return (m_currentSelectedPoseId == poseId ||
m_selectedPoseIds.find(poseId) != m_selectedPoseIds.end());
}
void PoseListWidget::showContextMenu(const QPoint &pos)
{
if (!m_hasContextMenu)
return;
QMenu contextMenu(this);
std::set<QUuid> unorderedPoseIds = m_selectedPoseIds;
if (!m_currentSelectedPoseId.isNull())
unorderedPoseIds.insert(m_currentSelectedPoseId);
std::vector<QUuid> poseIds;
for (const auto &cand: m_document->poseIdList) {
if (unorderedPoseIds.find(cand) != unorderedPoseIds.end())
poseIds.push_back(cand);
}
QAction modifyAction(tr("Modify"), this);
if (poseIds.size() == 1) {
connect(&modifyAction, &QAction::triggered, this, [=]() {
emit modifyPose(*poseIds.begin());
});
contextMenu.addAction(&modifyAction);
}
QAction copyAction(tr("Copy"), this);
if (!poseIds.empty()) {
connect(&copyAction, &QAction::triggered, this, &PoseListWidget::copy);
contextMenu.addAction(&copyAction);
}
QAction pasteAction(tr("Paste"), this);
if (m_document->hasPastablePosesInClipboard()) {
connect(&pasteAction, &QAction::triggered, m_document, &Document::paste);
contextMenu.addAction(&pasteAction);
}
QAction deleteAction(tr("Delete"), this);
if (!poseIds.empty()) {
connect(&deleteAction, &QAction::triggered, [=]() {
for (const auto &poseId: poseIds)
emit removePose(poseId);
});
contextMenu.addAction(&deleteAction);
}
contextMenu.exec(mapToGlobal(pos));
}
void PoseListWidget::resizeEvent(QResizeEvent *event)
{
QTreeWidget::resizeEvent(event);
if (calculateColumnCount() != columnCount())
reload();
}
int PoseListWidget::calculateColumnCount()
{
if (nullptr == parentWidget())
return 0;
int columns = parentWidget()->width() / Theme::posePreviewImageSize;
if (0 == columns)
columns = 1;
return columns;
}
void PoseListWidget::reload()
{
removeAllContent();
int columns = calculateColumnCount();
if (0 == columns)
return;
int columnWidth = parentWidget()->width() / columns;
//qDebug() << "parentWidth:" << parentWidget()->width() << "columnWidth:" << columnWidth << "columns:" << columns;
setColumnCount(columns);
for (int i = 0; i < columns; i++)
setColumnWidth(i, columnWidth);
std::vector<QUuid> orderedPoseIdList = m_document->poseIdList;
std::sort(orderedPoseIdList.begin(), orderedPoseIdList.end(), [&](const QUuid &firstPoseId, const QUuid &secondPoseId) {
const auto *firstPose = m_document->findPose(firstPoseId);
const auto *secondPose = m_document->findPose(secondPoseId);
if (nullptr == firstPose || nullptr == secondPose)
return false;
return QString::compare(firstPose->name, secondPose->name, Qt::CaseInsensitive) < 0;
});
decltype(orderedPoseIdList.size()) poseIndex = 0;
while (poseIndex < orderedPoseIdList.size()) {
QTreeWidgetItem *item = new QTreeWidgetItem(this);
item->setFlags((item->flags() | Qt::ItemIsEnabled) & ~(Qt::ItemIsSelectable) & ~(Qt::ItemIsEditable));
for (int col = 0; col < columns && poseIndex < orderedPoseIdList.size(); col++, poseIndex++) {
const auto &poseId = orderedPoseIdList[poseIndex];
item->setSizeHint(col, QSize(columnWidth, PoseWidget::preferredHeight() + 2));
item->setData(col, Qt::UserRole, poseId.toString());
PoseWidget *widget = new PoseWidget(m_document, poseId);
connect(widget, &PoseWidget::modifyPose, this, &PoseListWidget::modifyPose);
connect(widget, &PoseWidget::cornerButtonClicked, this, &PoseListWidget::cornerButtonClicked);
setItemWidget(item, col, widget);
widget->reload();
widget->updateCheckedState(isPoseSelected(poseId));
m_itemMap[poseId] = std::make_pair(item, col);
}
invisibleRootItem()->addChild(item);
}
}
void PoseListWidget::setCornerButtonVisible(bool visible)
{
m_cornerButtonVisible = visible;
}
void PoseListWidget::setHasContextMenu(bool hasContextMenu)
{
m_hasContextMenu = hasContextMenu;
}
void PoseListWidget::removeAllContent()
{
m_itemMap.clear();
clear();
}
void PoseListWidget::copy()
{
if (m_selectedPoseIds.empty() && m_currentSelectedPoseId.isNull())
return;
std::set<QUuid> limitPoseIds = m_selectedPoseIds;
if (!m_currentSelectedPoseId.isNull())
limitPoseIds.insert(m_currentSelectedPoseId);
std::set<QUuid> emptySet;
Snapshot snapshot;
m_document->toSnapshot(&snapshot, emptySet, DocumentToSnapshotFor::Poses,
limitPoseIds);
QString snapshotXml;
QXmlStreamWriter xmlStreamWriter(&snapshotXml);
saveSkeletonToXmlStream(&snapshot, &xmlStreamWriter);
QClipboard *clipboard = QApplication::clipboard();
clipboard->setText(snapshotXml);
}

View File

@ -1,43 +0,0 @@
#ifndef DUST3D_POSE_LIST_WIDGET_H
#define DUST3D_POSE_LIST_WIDGET_H
#include <QTreeWidget>
#include <map>
#include <QMouseEvent>
#include "document.h"
#include "posewidget.h"
class PoseListWidget : public QTreeWidget
{
Q_OBJECT
signals:
void removePose(QUuid poseId);
void modifyPose(QUuid poseId);
void cornerButtonClicked(QUuid poseId);
public:
PoseListWidget(const Document *document, QWidget *parent=nullptr);
bool isPoseSelected(QUuid poseId);
public slots:
void reload();
void removeAllContent();
void poseRemoved(QUuid poseId);
void showContextMenu(const QPoint &pos);
void selectPose(QUuid poseId, bool multiple=false);
void copy();
void setCornerButtonVisible(bool visible);
void setHasContextMenu(bool hasContextMenu);
protected:
void resizeEvent(QResizeEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
private:
int calculateColumnCount();
void updatePoseSelectState(QUuid poseId, bool selected);
const Document *m_document = nullptr;
std::map<QUuid, std::pair<QTreeWidgetItem *, int>> m_itemMap;
std::set<QUuid> m_selectedPoseIds;
QUuid m_currentSelectedPoseId;
QUuid m_shiftStartPoseId;
bool m_cornerButtonVisible = false;
bool m_hasContextMenu = true;
};
#endif

View File

@ -1,90 +0,0 @@
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QPushButton>
#include "posemanagewidget.h"
#include "theme.h"
#include "poseeditwidget.h"
#include "infolabel.h"
PoseManageWidget::PoseManageWidget(const Document *document, QWidget *parent) :
QWidget(parent),
m_document(document)
{
QPushButton *addPoseButton = new QPushButton(Theme::awesome()->icon(fa::plus), tr("Add Pose..."));
addPoseButton->hide();
connect(addPoseButton, &QPushButton::clicked, this, &PoseManageWidget::showAddPoseDialog);
QHBoxLayout *toolsLayout = new QHBoxLayout;
toolsLayout->addWidget(addPoseButton);
m_poseListWidget = new PoseListWidget(document);
connect(m_poseListWidget, &PoseListWidget::modifyPose, this, &PoseManageWidget::showPoseDialog);
InfoLabel *infoLabel = new InfoLabel;
infoLabel->hide();
auto refreshInfoLabel = [=]() {
if (m_document->currentRigSucceed()) {
if (m_document->rigType == RigType::Animal) {
infoLabel->setText("");
infoLabel->hide();
addPoseButton->show();
} else {
infoLabel->setText(tr("Pose editor doesn't support this rig type yet: ") + RigTypeToDispName(m_document->rigType));
infoLabel->show();
addPoseButton->hide();
}
} else {
infoLabel->setText(tr("Missing Rig"));
infoLabel->show();
addPoseButton->hide();
}
};
connect(m_document, &Document::resultRigChanged, this, refreshInfoLabel);
connect(m_document, &Document::cleanup, this, refreshInfoLabel);
QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addWidget(infoLabel);
mainLayout->addLayout(toolsLayout);
mainLayout->addWidget(m_poseListWidget);
setLayout(mainLayout);
}
PoseListWidget *PoseManageWidget::poseListWidget()
{
return m_poseListWidget;
}
QSize PoseManageWidget::sizeHint() const
{
return QSize(Theme::sidebarPreferredWidth, 0);
}
void PoseManageWidget::showAddPoseDialog()
{
showPoseDialog(QUuid());
}
void PoseManageWidget::showPoseDialog(QUuid poseId)
{
PoseEditWidget *poseEditWidget = new PoseEditWidget(m_document);
poseEditWidget->setAttribute(Qt::WA_DeleteOnClose);
if (!poseId.isNull()) {
const Pose *pose = m_document->findPose(poseId);
if (nullptr != pose) {
poseEditWidget->setEditPoseId(poseId);
poseEditWidget->setEditPoseName(pose->name);
poseEditWidget->setEditPoseFrames(pose->frames);
poseEditWidget->setEditPoseTurnaroundImageId(pose->turnaroundImageId);
poseEditWidget->setEditPoseYtranslationScale(pose->yTranslationScale);
poseEditWidget->clearUnsaveState();
}
}
poseEditWidget->show();
connect(poseEditWidget, &QDialog::destroyed, [=]() {
emit unregisterDialog((QWidget *)poseEditWidget);
});
emit registerDialog((QWidget *)poseEditWidget);
}

View File

@ -1,26 +0,0 @@
#ifndef DUST3D_POSE_MANAGE_WIDGET_H
#define DUST3D_POSE_MANAGE_WIDGET_H
#include <QWidget>
#include "document.h"
#include "poselistwidget.h"
class PoseManageWidget : public QWidget
{
Q_OBJECT
signals:
void registerDialog(QWidget *widget);
void unregisterDialog(QWidget *widget);
public:
PoseManageWidget(const Document *document, QWidget *parent=nullptr);
PoseListWidget *poseListWidget();
protected:
virtual QSize sizeHint() const;
public slots:
void showAddPoseDialog();
void showPoseDialog(QUuid poseId);
private:
const Document *m_document = nullptr;
PoseListWidget *m_poseListWidget = nullptr;
};
#endif

View File

@ -1,46 +0,0 @@
#include <QGuiApplication>
#include "posemeshcreator.h"
#include "skinnedmeshcreator.h"
PoseMeshCreator::PoseMeshCreator(const std::vector<JointNode> &resultNodes,
const Outcome &outcome,
const std::map<int, RiggerVertexWeights> &resultWeights) :
m_resultNodes(resultNodes),
m_outcome(outcome),
m_resultWeights(resultWeights)
{
}
PoseMeshCreator::~PoseMeshCreator()
{
delete m_resultMesh;
}
Model *PoseMeshCreator::takeResultMesh()
{
Model *resultMesh = m_resultMesh;
m_resultMesh = nullptr;
return resultMesh;
}
void PoseMeshCreator::createMesh()
{
SkinnedMeshCreator skinnedMeshCreator(m_outcome, m_resultWeights);
std::vector<QMatrix4x4> matricies;
matricies.resize(m_resultNodes.size());
for (decltype(m_resultNodes.size()) i = 0; i < m_resultNodes.size(); i++) {
const auto &node = m_resultNodes[i];
matricies[i] = node.transformMatrix;
}
delete m_resultMesh;
m_resultMesh = skinnedMeshCreator.createMeshFromTransform(matricies);
}
void PoseMeshCreator::process()
{
createMesh();
emit finished();
}

View File

@ -1,29 +0,0 @@
#ifndef DUST3D_POSE_MESH_CREATOR_H
#define DUST3D_POSE_MESH_CREATOR_H
#include <QObject>
#include "model.h"
#include "jointnodetree.h"
#include "outcome.h"
class PoseMeshCreator : public QObject
{
Q_OBJECT
signals:
void finished();
public:
PoseMeshCreator(const std::vector<JointNode> &resultNodes,
const Outcome &outcome,
const std::map<int, RiggerVertexWeights> &resultWeights);
~PoseMeshCreator();
void createMesh();
Model *takeResultMesh();
public slots:
void process();
private:
std::vector<JointNode> m_resultNodes;
Outcome m_outcome;
std::map<int, RiggerVertexWeights> m_resultWeights;
Model *m_resultMesh = nullptr;
};
#endif

View File

@ -1,59 +0,0 @@
#include <QThread>
#include <QGridLayout>
#include "posepreviewmanager.h"
PosePreviewManager::PosePreviewManager()
{
}
PosePreviewManager::~PosePreviewManager()
{
delete m_previewMesh;
}
bool PosePreviewManager::isRendering()
{
return nullptr != m_poseMeshCreator;
}
bool PosePreviewManager::postUpdate(const Poser &poser,
const Outcome &outcome,
const std::map<int, RiggerVertexWeights> &resultWeights)
{
if (nullptr != m_poseMeshCreator)
return false;
qDebug() << "Pose mesh generating..";
QThread *thread = new QThread;
m_poseMeshCreator = new PoseMeshCreator(poser.resultNodes(), outcome, resultWeights);
m_poseMeshCreator->moveToThread(thread);
connect(thread, &QThread::started, m_poseMeshCreator, &PoseMeshCreator::process);
connect(m_poseMeshCreator, &PoseMeshCreator::finished, this, &PosePreviewManager::poseMeshReady);
connect(m_poseMeshCreator, &PoseMeshCreator::finished, thread, &QThread::quit);
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
thread->start();
return true;
}
Model *PosePreviewManager::takeResultPreviewMesh()
{
if (nullptr == m_previewMesh)
return nullptr;
return new Model(*m_previewMesh);
}
void PosePreviewManager::poseMeshReady()
{
delete m_previewMesh;
m_previewMesh = m_poseMeshCreator->takeResultMesh();
qDebug() << "Pose mesh generation done";
delete m_poseMeshCreator;
m_poseMeshCreator = nullptr;
emit resultPreviewMeshChanged();
emit renderDone();
}

View File

@ -1,30 +0,0 @@
#ifndef DUST3D_POSE_PREVIEW_MANAGER_H
#define DUST3D_POSE_PREVIEW_MANAGER_H
#include <QWidget>
#include "document.h"
#include "poser.h"
#include "posemeshcreator.h"
#include "model.h"
class PosePreviewManager : public QObject
{
Q_OBJECT
public:
PosePreviewManager();
~PosePreviewManager();
bool isRendering();
bool postUpdate(const Poser &poser,
const Outcome &outcome,
const std::map<int, RiggerVertexWeights> &resultWeights);
Model *takeResultPreviewMesh();
private slots:
void poseMeshReady();
signals:
void resultPreviewMeshChanged();
void renderDone();
private:
PoseMeshCreator *m_poseMeshCreator = nullptr;
Model *m_previewMesh = nullptr;
};
#endif

View File

@ -1,72 +0,0 @@
#include <QGuiApplication>
#include <QElapsedTimer>
#include "posepreviewsgenerator.h"
#include "posemeshcreator.h"
#include "poserconstruct.h"
#include "posedocument.h"
PosePreviewsGenerator::PosePreviewsGenerator(RigType rigType,
const std::vector<RiggerBone> *rigBones,
const std::map<int, RiggerVertexWeights> *rigWeights,
const Outcome &outcome) :
m_rigType(rigType),
m_rigBones(*rigBones),
m_rigWeights(*rigWeights),
m_outcome(new Outcome(outcome))
{
}
PosePreviewsGenerator::~PosePreviewsGenerator()
{
for (auto &item: m_previews) {
delete item.second;
}
delete m_outcome;
}
void PosePreviewsGenerator::addPose(std::pair<QUuid, int> idAndFrame, const std::map<QString, std::map<QString, QString>> &pose)
{
m_poses.push_back(std::make_pair(idAndFrame, pose));
}
const std::set<std::pair<QUuid, int>> &PosePreviewsGenerator::generatedPreviewPoseIdAndFrames()
{
return m_generatedPoseIdAndFrames;
}
Model *PosePreviewsGenerator::takePreview(std::pair<QUuid, int> idAndFrame)
{
Model *resultMesh = m_previews[idAndFrame];
m_previews[idAndFrame] = nullptr;
return resultMesh;
}
void PosePreviewsGenerator::process()
{
QElapsedTimer countTimeConsumed;
countTimeConsumed.start();
Poser *poser = newPoser(m_rigType, m_rigBones);
for (const auto &pose: m_poses) {
PoseDocument poseDocument;
poseDocument.fromParameters(&m_rigBones, pose.second);
std::map<QString, std::map<QString, QString>> translatedParameters;
poseDocument.toParameters(translatedParameters);
poser->parameters() = translatedParameters;
poser->commit();
PoseMeshCreator *poseMeshCreator = new PoseMeshCreator(poser->resultNodes(), *m_outcome, m_rigWeights);
poseMeshCreator->createMesh();
m_previews[pose.first] = poseMeshCreator->takeResultMesh();
delete poseMeshCreator;
poser->reset();
m_generatedPoseIdAndFrames.insert(pose.first);
}
delete poser;
qDebug() << "The pose previews generation took" << countTimeConsumed.elapsed() << "milliseconds";
emit finished();
}

View File

@ -1,38 +0,0 @@
#ifndef DUST3D_POSE_PREVIEWS_GENERATOR_H
#define DUST3D_POSE_PREVIEWS_GENERATOR_H
#include <QObject>
#include <map>
#include <QUuid>
#include <vector>
#include "model.h"
#include "rigger.h"
#include "outcome.h"
#include "rigtype.h"
class PosePreviewsGenerator : public QObject
{
Q_OBJECT
public:
PosePreviewsGenerator(RigType rigType,
const std::vector<RiggerBone> *rigBones,
const std::map<int, RiggerVertexWeights> *rigWeights,
const Outcome &outcome);
~PosePreviewsGenerator();
void addPose(std::pair<QUuid, int> idAndFrame, const std::map<QString, std::map<QString, QString>> &pose);
const std::set<std::pair<QUuid, int>> &generatedPreviewPoseIdAndFrames();
Model *takePreview(std::pair<QUuid, int> idAndFrame);
signals:
void finished();
public slots:
void process();
private:
RigType m_rigType = RigType::None;
std::vector<RiggerBone> m_rigBones;
std::map<int, RiggerVertexWeights> m_rigWeights;
Outcome *m_outcome = nullptr;
std::vector<std::pair<std::pair<QUuid, int>, std::map<QString, std::map<QString, QString>>>> m_poses;
std::map<std::pair<QUuid, int>, Model *> m_previews;
std::set<std::pair<QUuid, int>> m_generatedPoseIdAndFrames;
};
#endif

View File

@ -1,98 +0,0 @@
#include <QQuaternion>
#include <QRegularExpression>
#include "poser.h"
Poser::Poser(const std::vector<RiggerBone> &bones) :
m_bones(bones),
m_jointNodeTree(&bones)
{
for (decltype(m_bones.size()) i = 0; i < m_bones.size(); i++) {
m_boneNameToIndexMap[m_bones[i].name] = i;
}
}
Poser::~Poser()
{
}
void Poser::setYtranslationScale(float scale)
{
m_yTranslationScale = scale;
}
int Poser::findBoneIndex(const QString &name)
{
auto findResult = m_boneNameToIndexMap.find(name);
if (findResult == m_boneNameToIndexMap.end())
return -1;
return findResult->second;
}
const RiggerBone *Poser::findBone(const QString &name)
{
auto findResult = m_boneNameToIndexMap.find(name);
if (findResult == m_boneNameToIndexMap.end())
return nullptr;
return &m_bones[findResult->second];
}
const std::vector<RiggerBone> &Poser::bones() const
{
return m_bones;
}
const std::vector<JointNode> &Poser::resultNodes() const
{
return m_jointNodeTree.nodes();
}
const JointNodeTree &Poser::resultJointNodeTree() const
{
return m_jointNodeTree;
}
void Poser::commit()
{
m_jointNodeTree.recalculateTransformMatrices();
}
void Poser::reset()
{
m_jointNodeTree.reset();
}
std::map<QString, std::map<QString, QString>> &Poser::parameters()
{
return m_parameters;
}
void Poser::fetchChains(const std::vector<QString> &boneNames, std::map<QString, std::vector<QString>> &chains)
{
QRegularExpression reJoints("^([a-zA-Z]+\\d*)_Joint\\d+$");
QRegularExpression reSpine("^([a-zA-Z]+)\\d*$");
for (const auto &item: boneNames) {
QRegularExpressionMatch match = reJoints.match(item);
if (match.hasMatch()) {
QString name = match.captured(1);
chains[name].push_back(item);
} else {
match = reSpine.match(item);
if (match.hasMatch()) {
QString name = match.captured(1);
if (item.startsWith(name + "0"))
chains[name + "0"].push_back(item);
else
chains[name].push_back(item);
} else if (item.startsWith("Virtual_")) {
//qDebug() << "Ignore connector:" << item;
} else {
qDebug() << "Unrecognized bone name:" << item;
}
}
}
for (auto &chain: chains) {
std::sort(chain.second.begin(), chain.second.end(), [](const QString &first, const QString &second) {
return first < second;
});
}
}

View File

@ -1,32 +0,0 @@
#ifndef DUST3D_POSER_H
#define DUST3D_POSER_H
#include <QObject>
#include "rigger.h"
#include "jointnodetree.h"
#include "util.h"
class Poser : public QObject
{
Q_OBJECT
public:
Poser(const std::vector<RiggerBone> &bones);
~Poser();
const RiggerBone *findBone(const QString &name);
int findBoneIndex(const QString &name);
const std::vector<RiggerBone> &bones() const;
const std::vector<JointNode> &resultNodes() const;
const JointNodeTree &resultJointNodeTree() const;
std::map<QString, std::map<QString, QString>> &parameters();
void setYtranslationScale(float scale);
virtual void commit();
void reset();
static void fetchChains(const std::vector<QString> &boneNames, std::map<QString, std::vector<QString>> &chains);
protected:
std::vector<RiggerBone> m_bones;
std::map<QString, int> m_boneNameToIndexMap;
JointNodeTree m_jointNodeTree;
std::map<QString, std::map<QString, QString>> m_parameters;
float m_yTranslationScale = 1.0;
};
#endif

View File

@ -1,9 +0,0 @@
#include "poserconstruct.h"
#include "animalposer.h"
Poser *newPoser(RigType rigType, const std::vector<RiggerBone> &bones)
{
if (rigType == RigType::Animal)
return new AnimalPoser(bones);
return nullptr;
}

View File

@ -1,9 +0,0 @@
#ifndef DUST3D_POSER_CONSTRUCT_H
#define DUST3D_POSER_CONSTRUCT_H
#include "rigtype.h"
#include "poser.h"
#include "rigger.h"
Poser *newPoser(RigType rigType, const std::vector<RiggerBone> &bones);
#endif

View File

@ -1,116 +0,0 @@
#include <QVBoxLayout>
#include "posewidget.h"
PoseWidget::PoseWidget(const Document *document, QUuid poseId) :
m_poseId(poseId),
m_document(document)
{
setObjectName("PoseFrame");
m_previewWidget = new ModelWidget(this);
m_previewWidget->setAttribute(Qt::WA_TransparentForMouseEvents);
m_previewWidget->setFixedSize(Theme::posePreviewImageSize, Theme::posePreviewImageSize);
m_previewWidget->enableMove(false);
m_previewWidget->enableZoom(false);
m_nameLabel = new QLabel;
m_nameLabel->setAlignment(Qt::AlignCenter);
m_nameLabel->setStyleSheet("background: qlineargradient(x1:0.5 y1:-15.5, x2:0.5 y2:1, stop:0 " + Theme::white.name() + ", stop:1 #252525);");
QFont nameFont;
nameFont.setWeight(QFont::Light);
//nameFont.setPixelSize(9);
nameFont.setBold(false);
m_nameLabel->setFont(nameFont);
QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->setContentsMargins(0, 0, 0, 0);
mainLayout->addStretch();
mainLayout->addWidget(m_nameLabel);
setLayout(mainLayout);
setFixedSize(Theme::posePreviewImageSize, PoseWidget::preferredHeight());
connect(document, &Document::poseNameChanged, this, [=](QUuid poseId) {
if (poseId != m_poseId)
return;
updateName();
});
connect(document, &Document::posePreviewChanged, this, [=](QUuid poseId) {
if (poseId != m_poseId)
return;
updatePreview();
});
}
void PoseWidget::setCornerButtonVisible(bool visible)
{
if (nullptr == m_cornerButton) {
m_cornerButton = new QPushButton(this);
m_cornerButton->move(Theme::posePreviewImageSize - Theme::miniIconSize - 2, 2);
Theme::initAwesomeMiniButton(m_cornerButton);
m_cornerButton->setText(QChar(fa::plussquare));
connect(m_cornerButton, &QPushButton::clicked, this, [=]() {
emit cornerButtonClicked(m_poseId);
});
}
m_cornerButton->setVisible(visible);
}
void PoseWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
m_previewWidget->move((width() - Theme::posePreviewImageSize) / 2, 0);
}
int PoseWidget::preferredHeight()
{
return Theme::posePreviewImageSize;
}
void PoseWidget::reload()
{
updatePreview();
updateName();
}
void PoseWidget::updatePreview()
{
const Pose *pose = m_document->findPose(m_poseId);
if (!pose) {
qDebug() << "Pose not found:" << m_poseId;
return;
}
Model *previewMesh = pose->takePreviewMesh();
m_previewWidget->updateMesh(previewMesh);
}
void PoseWidget::updateName()
{
const Pose *pose = m_document->findPose(m_poseId);
if (!pose) {
qDebug() << "Pose not found:" << m_poseId;
return;
}
m_nameLabel->setText(pose->name);
}
void PoseWidget::updateCheckedState(bool checked)
{
if (checked)
setStyleSheet("#PoseFrame {border: 1px solid " + Theme::red.name() + ";}");
else
setStyleSheet("#PoseFrame {border: 1px solid transparent;}");
}
ModelWidget *PoseWidget::previewWidget()
{
return m_previewWidget;
}
void PoseWidget::mouseDoubleClickEvent(QMouseEvent *event)
{
QFrame::mouseDoubleClickEvent(event);
emit modifyPose(m_poseId);
}

View File

@ -1,36 +0,0 @@
#ifndef DUST3D_POSE_WIDGET_H
#define DUST3D_POSE_WIDGET_H
#include <QFrame>
#include <QLabel>
#include <QIcon>
#include "document.h"
#include "modelwidget.h"
class PoseWidget : public QFrame
{
Q_OBJECT
signals:
void modifyPose(QUuid poseId);
void cornerButtonClicked(QUuid poseId);
public:
PoseWidget(const Document *document, QUuid poseId);
static int preferredHeight();
ModelWidget *previewWidget();
protected:
void mouseDoubleClickEvent(QMouseEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
public slots:
void reload();
void updatePreview();
void updateName();
void updateCheckedState(bool checked);
void setCornerButtonVisible(bool visible);
private:
QUuid m_poseId;
const Document *m_document = nullptr;
ModelWidget *m_previewWidget = nullptr;
QLabel *m_nameLabel = nullptr;
QPushButton *m_cornerButton = nullptr;
};
#endif

View File

@ -273,21 +273,6 @@ void RigGenerator::buildBoneNodeChain()
const auto &chain = m_boneNodeChain[i];
const auto &node = m_outcome->bodyNodes[chain.fromNodeIndex];
const auto &isSpine = chain.isSpine;
//printf("Chain[%lu] %s %s", i, BoneMarkToString(node.boneMark), isSpine ? "SPINE " : "");
//printf("|");
//for (size_t j = 0; j < chain.nodeChain.size(); ++j) {
// printf("%lu%s ", chain.nodeChain[j], chain.nodeIsJointFlags[j] ? "(JOINT)" : "");
//}
//printf("\r\n");
//for (size_t j = 0; j < chain.nodeChain.size(); ++j) {
// const auto &node = m_outcome->bodyNodes[chain.nodeChain[j]];
// printf(" >>%lu part:%s node:%s (%f,%f,%f)%s\r\n",
// chain.nodeChain[j],
// node.partId.toString().toUtf8().constData(),
// node.nodeId.toString().toUtf8().constData(),
// node.origin.x(), node.origin.y(), node.origin.z(),
// chain.nodeIsJointFlags[j] ? "(JOINT)" : "");
//}
if (isSpine) {
m_spineChains.push_back(i);
continue;
@ -364,6 +349,14 @@ void RigGenerator::attachLimbsToSpine()
}
}
int RigGenerator::attachedBoneIndex(size_t spineJointIndex)
{
if (spineJointIndex == m_rootSpineJointIndex) {
return m_boneNameToIndexMap[QString("Body")];
}
return m_boneNameToIndexMap[QString("Spine") + QString::number(spineJointIndex - m_rootSpineJointIndex)];
}
void RigGenerator::buildSkeleton()
{
bool addMarkHelpInfo = false;
@ -388,6 +381,7 @@ void RigGenerator::buildSkeleton()
return;
calculateSpineDirection(&m_isSpineVertical);
qDebug() << "Spine:" << (m_isSpineVertical ? "Vertical" : "Horizontal");
auto sortLimbChains = [&](std::vector<size_t> &chains) {
std::sort(chains.begin(), chains.end(), [&](const size_t &first,
@ -407,14 +401,14 @@ void RigGenerator::buildSkeleton()
extractSpineJoints();
extractBranchJoints();
size_t rootSpineJointIndex = m_attachLimbsToSpineJointIndices[0];
size_t lastSpineJointIndex = m_spineJoints.size() - 1;
m_rootSpineJointIndex = m_attachLimbsToSpineJointIndices[0];
m_lastSpineJointIndex = m_spineJoints.size() - 1;
m_resultBones = new std::vector<RiggerBone>;
m_resultWeights = new std::map<int, RiggerVertexWeights>;
{
const auto &firstSpineNode = m_outcome->bodyNodes[m_spineJoints[rootSpineJointIndex]];
const auto &firstSpineNode = m_outcome->bodyNodes[m_spineJoints[m_rootSpineJointIndex]];
RiggerBone bone;
bone.headPosition = QVector3D(0.0, 0.0, 0.0);
bone.tailPosition = firstSpineNode.origin;
@ -422,20 +416,14 @@ void RigGenerator::buildSkeleton()
bone.tailRadius = firstSpineNode.radius;
bone.color = Theme::white;
bone.name = QString("Body");
bone.attributes["spineDirection"] = m_isSpineVertical ? "Vertical" : "Horizontal";
bone.index = m_resultBones->size();
bone.parent = -1;
m_boneNameToIndexMap.insert({bone.name, (int)bone.index});
m_resultBones->push_back(bone);
}
auto attachedBoneIndex = [&](size_t spineJointIndex) {
if (spineJointIndex == rootSpineJointIndex) {
return m_boneNameToIndexMap[QString("Body")];
}
return m_boneNameToIndexMap[QString("Spine") + QString::number(spineJointIndex - rootSpineJointIndex)];
};
for (size_t spineJointIndex = rootSpineJointIndex;
for (size_t spineJointIndex = m_rootSpineJointIndex;
spineJointIndex + 1 < m_spineJoints.size();
++spineJointIndex) {
const auto &currentNode = m_outcome->bodyNodes[m_spineJoints[spineJointIndex]];
@ -445,8 +433,8 @@ void RigGenerator::buildSkeleton()
bone.tailPosition = nextNode.origin;
bone.headRadius = currentNode.radius;
bone.tailRadius = nextNode.radius;
bone.color = 0 == (spineJointIndex - rootSpineJointIndex) % 2 ? Theme::white : BoneMarkToColor(BoneMark::Joint);
bone.name = QString("Spine") + QString::number(spineJointIndex + 1 - rootSpineJointIndex);
bone.color = 0 == (spineJointIndex - m_rootSpineJointIndex) % 2 ? Theme::red : Qt::blue; //BoneMarkToColor(BoneMark::Joint);
bone.name = QString("Spine") + QString::number(spineJointIndex + 1 - m_rootSpineJointIndex);
bone.index = m_resultBones->size();
bone.parent = attachedBoneIndex(spineJointIndex);
m_boneNameToIndexMap.insert({bone.name, (int)bone.index});
@ -467,7 +455,7 @@ void RigGenerator::buildSkeleton()
bone.tailPosition = limbFirstNode.origin;
bone.headRadius = spineNode.radius;
bone.tailRadius = limbFirstNode.radius;
bone.color = Theme::white;
bone.color = chainPrefix.startsWith("Left") ? BoneMarkToColor(BoneMark::Tail) : BoneMarkToColor(BoneMark::Neck);
bone.name = QString("Virtual_") + (*m_resultBones)[parentIndex].name + QString("_") + chainName;
bone.index = m_resultBones->size();
bone.parent = parentIndex;
@ -536,7 +524,7 @@ void RigGenerator::buildSkeleton()
auto parentName = QString("Neck_Joint") + QString::number(neckJointIndex);
bone.parent = m_boneNameToIndexMap[parentName];
} else {
auto parentName = QString("Spine") + QString::number(lastSpineJointIndex - rootSpineJointIndex);
auto parentName = QString("Spine") + QString::number(m_lastSpineJointIndex - m_rootSpineJointIndex);
bone.parent = m_boneNameToIndexMap[parentName];
}
m_boneNameToIndexMap.insert({bone.name, (int)bone.index});
@ -547,7 +535,7 @@ void RigGenerator::buildSkeleton()
if (!m_tailJoints.empty()) {
QString nearestSpine = "Body";
for (int spineJointIndex = rootSpineJointIndex;
for (int spineJointIndex = m_rootSpineJointIndex;
spineJointIndex >= 0;
--spineJointIndex) {
if (m_spineJoints[spineJointIndex] == m_tailJoints[0])
@ -561,14 +549,14 @@ void RigGenerator::buildSkeleton()
bone.tailPosition = nextNode.origin;
bone.headRadius = currentNode.radius;
bone.tailRadius = nextNode.radius;
bone.color = 0 == (rootSpineJointIndex - spineJointIndex) % 2 ? BoneMarkToColor(BoneMark::Joint) : Theme::white;
bone.name = QString("Spine0") + QString::number(rootSpineJointIndex - spineJointIndex + 1);
bone.color = 0 == (m_rootSpineJointIndex - spineJointIndex) % 2 ? BoneMarkToColor(BoneMark::Joint) : Theme::white;
bone.name = QString("Spine0") + QString::number(m_rootSpineJointIndex - spineJointIndex + 1);
bone.index = m_resultBones->size();
if ((int)rootSpineJointIndex == spineJointIndex) {
if ((int)m_rootSpineJointIndex == spineJointIndex) {
auto parentName = QString("Body");
bone.parent = m_boneNameToIndexMap[parentName];
} else {
auto parentName = QString("Spine0") + QString::number(rootSpineJointIndex - spineJointIndex);
auto parentName = QString("Spine0") + QString::number(m_rootSpineJointIndex - spineJointIndex);
bone.parent = m_boneNameToIndexMap[parentName];
}
m_boneNameToIndexMap.insert({bone.name, (int)bone.index});
@ -605,14 +593,12 @@ void RigGenerator::buildSkeleton()
m_isSuccessful = true;
//for (size_t i = 0; i < m_resultBones->size(); ++i) {
// const auto &bone = (*m_resultBones)[i];
// std::cout << "bone:" << bone.name.toUtf8().constData() << " " << " headRadius:" << bone.headRadius << " tailRadius:" << bone.tailRadius << std::endl;
// for (const auto &childIndex: bone.children) {
// const auto &child = (*m_resultBones)[childIndex];
// std::cout << " child:" << child.name.toUtf8().constData() << " " << std::endl;
// }
//}
if (nullptr != m_resultBones) {
for (size_t i = 0; i < m_resultBones->size(); ++i) {
const auto &bone = (*m_resultBones)[i];
qDebug() << "Bone[" << i << "]: name:" << bone.name << "parent:" << (-1 == bone.parent ? "(null)" : (*m_resultBones)[bone.parent].name);
}
}
}
void RigGenerator::computeSkinWeights()
@ -746,6 +732,8 @@ void RigGenerator::computeSkinWeights()
QString("Spine"), backSpineVertices);
}
fixVirtualBoneSkinWeights();
for (auto &it: *m_resultWeights)
it.second.finalizeWeights();
@ -753,23 +741,183 @@ void RigGenerator::computeSkinWeights()
// auto findWeights = m_resultWeights->find(i);
// if (findWeights == m_resultWeights->end()) {
// const auto &sourceNode = m_outcome->vertexSourceNodes[i];
// printf("NoWeight vertex index:%lu Source:%s %s\r\n",
// i,
// sourceNode.first.toString().toUtf8().constData(),
// sourceNode.second.toString().toUtf8().constData());
// qDebug() << "NoWeight vertex index:" << i << sourceNode.first << sourceNode.second;
// }
//}
}
void RigGenerator::fixVirtualBoneSkinWeights()
{
auto calculateSide = [](float x) {
if (x < 0)
return -1;
else if (x > 0)
return 1;
return 0;
};
struct VirtualBone
{
int index;
int side;
int parentIndex;
int parentNextIndex;
};
std::vector<VirtualBone> virtualBones;
for (size_t limbIndex = 0;
limbIndex < m_attachLimbsToSpineJointIndices.size();
++limbIndex) {
QString limbChainName = QString("Limb") + QString::number(limbIndex + 1);
const auto &spineJointIndex = m_attachLimbsToSpineJointIndices[limbIndex];
const auto &parentIndex = attachedBoneIndex(spineJointIndex);
if (0 == parentIndex)
continue;
int parentNextIndex = -1;
for (const auto &childIndex: (*m_resultBones)[parentIndex].children) {
const auto &child = (*m_resultBones)[childIndex];
if (child.name.startsWith("Spine")) {
parentNextIndex = childIndex;
break;
}
}
if (-1 == parentNextIndex)
continue;
QString prefixName = QString("Virtual_") + (*m_resultBones)[parentIndex].name;
QString leftBoneName = prefixName + QString("_Left") + limbChainName;
QString rightBoneName = prefixName + QString("_Right") + limbChainName;
auto findLeftIndex = m_boneNameToIndexMap.find(leftBoneName);
if (findLeftIndex != m_boneNameToIndexMap.end()) {
virtualBones.push_back({findLeftIndex->second,
calculateSide((*m_resultBones)[findLeftIndex->second].tailPosition.x()),
parentIndex,
parentNextIndex});
//const auto &leftBone = (*m_resultBones)[findLeftIndex->second];
//qDebug() << "leftBone:" << leftBone.name << "headRadius:" << leftBone.headRadius << "tailRadius:" << leftBone.tailRadius;
}
auto findRightIndex = m_boneNameToIndexMap.find(rightBoneName);
if (findRightIndex != m_boneNameToIndexMap.end()) {
virtualBones.push_back({findRightIndex->second,
calculateSide((*m_resultBones)[findRightIndex->second].tailPosition.x()),
parentIndex,
parentNextIndex});
//const auto &rightBone = (*m_resultBones)[findRightIndex->second];
//qDebug() << "rightBone:" << rightBone.name << "headRadius:" << rightBone.headRadius << "tailRadius:" << rightBone.tailRadius;
}
}
std::unordered_map<int, std::vector<size_t>> boneVerticesMap;
for (auto &it: *m_resultWeights) {
for (const auto &weight: it.second.boneRawWeights()) {
const auto &boneIndex = weight.first;
if (0 == boneIndex)
continue;
boneVerticesMap[boneIndex].push_back(it.first);
}
}
for (const auto &it: virtualBones) {
const auto &bone = (*m_resultBones)[it.index];
double boneLength = (bone.tailPosition - bone.headPosition).length() * 0.75;
QVector3D boundaryLineTailForParentOnZ;
QVector3D boundaryLineHeadForParentOnZ;
QVector3D boundaryLineTailForParentNextOnZ;
QVector3D boundaryLineHeadForParentNextOnZ;
QVector3D boundaryLineTailForParentOnX;
QVector3D boundaryLineHeadForParentOnX;
QVector3D boundaryLineTailForParentNextOnX;
QVector3D boundaryLineHeadForParentNextOnX;
if (m_isSpineVertical) {
boundaryLineTailForParentOnX = QVector3D(bone.tailPosition.x(),
bone.tailPosition.y() - bone.tailRadius,
bone.tailPosition.z());
boundaryLineHeadForParentOnX = QVector3D(bone.headPosition.x(),
bone.headPosition.y() - bone.headRadius,
bone.headPosition.z());
boundaryLineTailForParentNextOnX = QVector3D(bone.tailPosition.x(),
bone.tailPosition.y() + bone.tailRadius,
bone.tailPosition.z());
boundaryLineHeadForParentNextOnX = QVector3D(bone.headPosition.x(),
bone.headPosition.y() + bone.headRadius,
bone.headPosition.z());
} else {
boundaryLineTailForParentOnZ = QVector3D(bone.tailPosition.x(),
bone.tailPosition.y(),
bone.tailPosition.z() - bone.tailRadius);
boundaryLineHeadForParentOnZ = QVector3D(bone.headPosition.x(),
bone.headPosition.y(),
bone.headPosition.z() - bone.headRadius);
boundaryLineTailForParentNextOnZ = QVector3D(bone.tailPosition.x(),
bone.tailPosition.y(),
bone.tailPosition.z() + bone.tailRadius);
boundaryLineHeadForParentNextOnZ = QVector3D(bone.headPosition.x(),
bone.headPosition.y(),
bone.headPosition.z() + bone.headRadius);
}
float angleInRangle360BetweenTwoVectors(QVector3D a, QVector3D b, QVector3D planeNormal);
for (const auto &vertexIndex: boneVerticesMap[it.parentIndex]) {
if (it.side != calculateSide(m_outcome->vertices[vertexIndex].x()))
continue;
QVector3D projectedPosition = projectPointOnLine(m_outcome->vertices[vertexIndex], bone.tailPosition, bone.headPosition);
if ((projectedPosition - bone.tailPosition).length() > boneLength)
continue;
if (m_isSpineVertical) {
double angle = angleInRangle360BetweenTwoVectors((boundaryLineHeadForParentOnX - boundaryLineTailForParentOnX).normalized(),
(m_outcome->vertices[vertexIndex] - boundaryLineTailForParentOnX).normalized(),
QVector3D(0.0, 0.0, -it.side));
if (angle > 180)
continue;
} else {
double angle = angleInRangle360BetweenTwoVectors((boundaryLineHeadForParentOnZ - boundaryLineTailForParentOnZ).normalized(),
(m_outcome->vertices[vertexIndex] - boundaryLineTailForParentOnZ).normalized(),
QVector3D(1.0, 0.0, 0.0));
if (angle > 180)
continue;
}
(*m_resultWeights)[vertexIndex].addBone(it.index, 1.0);
}
for (const auto &vertexIndex: boneVerticesMap[it.parentNextIndex]) {
if (it.side != calculateSide(m_outcome->vertices[vertexIndex].x()))
continue;
QVector3D projectedPosition = projectPointOnLine(m_outcome->vertices[vertexIndex], bone.tailPosition, bone.headPosition);
if ((projectedPosition - bone.tailPosition).length() > boneLength)
continue;
if (m_isSpineVertical) {
double angle = angleInRangle360BetweenTwoVectors((m_outcome->vertices[vertexIndex] - boundaryLineTailForParentNextOnX).normalized(),
(boundaryLineHeadForParentNextOnX - boundaryLineTailForParentNextOnX).normalized(),
QVector3D(0.0, 0.0, -it.side));
if (angle > 180)
continue;
} else {
double angle = angleInRangle360BetweenTwoVectors((m_outcome->vertices[vertexIndex] - boundaryLineTailForParentNextOnZ).normalized(),
(boundaryLineHeadForParentNextOnZ - boundaryLineTailForParentNextOnZ).normalized(),
QVector3D(1.0, 0.0, 0.0));
if (angle > 180)
continue;
}
(*m_resultWeights)[vertexIndex].addBone(it.index, 1.0);
}
}
}
void RigGenerator::computeBranchSkinWeights(size_t fromBoneIndex,
const QString &boneNamePrefix,
const std::vector<size_t> &vertexIndices,
std::vector<size_t> *discardedVertexIndices)
{
//qDebug() << "computeBranchSkinWeights boneNamePrefix:" << boneNamePrefix;
std::vector<size_t> remainVertexIndices = vertexIndices;
size_t currentBoneIndex = fromBoneIndex;
while (true) {
const auto &currentBone = (*m_resultBones)[currentBoneIndex];
//qDebug() << " bone:" << currentBone.name;
std::vector<size_t> newRemainVertexIndices;
const auto &parentBone = (*m_resultBones)[currentBone.parent];
auto currentDirection = (currentBone.tailPosition - currentBone.headPosition).normalized();
@ -780,7 +928,7 @@ void RigGenerator::computeBranchSkinWeights(size_t fromBoneIndex,
auto beginGradientLength = parentBone.headRadius * 0.5f;
auto endGradientLength = parentBone.tailRadius * 0.5f;
auto parentLength = (parentBone.tailPosition - parentBone.headPosition).length();
auto previousBoneIndex = currentBone.name.startsWith("Virtual") ? parentBone.parent : currentBone.parent;
auto previousBoneIndex = /*currentBone.name.startsWith("Virtual") ? parentBone.parent : */currentBone.parent;
for (const auto &vertexIndex: remainVertexIndices) {
const auto &position = m_outcome->vertices[vertexIndex];
auto direction = (position - currentBone.headPosition).normalized();
@ -790,7 +938,7 @@ void RigGenerator::computeBranchSkinWeights(size_t fromBoneIndex,
if (projectedLength < 0)
projectedLength = 0;
if (projectedLength <= endGradientLength) {
auto factor = 0.5 * (1.0 - projectedLength / endGradientLength);
auto factor = 0.1 + 0.4 * (1.0 - projectedLength / endGradientLength);
(*m_resultWeights)[vertexIndex].addBone(previousBoneIndex, factor);
}
newRemainVertexIndices.push_back(vertexIndex);
@ -821,7 +969,7 @@ void RigGenerator::computeBranchSkinWeights(size_t fromBoneIndex,
(*m_resultWeights)[vertexIndex].addBone(previousBoneIndex, factor);
continue;
}
auto factor = 0.5 * (1.0 - (projectedLength - parentLength) / beginGradientLength);
auto factor = 0.1 + 0.4 * (1.0 - (projectedLength - parentLength) / beginGradientLength);
(*m_resultWeights)[vertexIndex].addBone(previousBoneIndex, factor);
continue;
}
@ -1105,7 +1253,7 @@ void RigGenerator::buildDemoMesh()
const auto &resultBones = *m_resultBones;
std::vector<std::tuple<QVector3D, QVector3D, float, float, QColor>> boxes;
for (const auto &bone: resultBones) {
if (bone.name.startsWith("Virtual") || bone.name.startsWith("Body"))
if (/*bone.name.startsWith("Virtual") || */bone.name.startsWith("Body"))
continue;
boxes.push_back(std::make_tuple(bone.headPosition, bone.tailPosition,
bone.headRadius, bone.tailRadius, bone.color));

View File

@ -62,6 +62,9 @@ private:
int m_debugEdgeVerticesNum = 0;
bool m_isSpineVertical = false;
bool m_isSuccessful = false;
size_t m_rootSpineJointIndex = 0;
size_t m_lastSpineJointIndex = 0;
void buildNeighborMap();
void buildBoneNodeChain();
void buildSkeleton();
@ -96,6 +99,8 @@ private:
std::unordered_set<size_t> *visited);
void removeBranchsFromNodes(const std::vector<std::vector<size_t>> *boneNodeIndices,
std::vector<size_t> *resultNodes);
void fixVirtualBoneSkinWeights();
int attachedBoneIndex(size_t spineJointIndex);
};
#endif

View File

@ -27,6 +27,7 @@ public:
float headRadius = 0.0;
float tailRadius = 0.0;
QColor color;
std::map<QString, QString> attributes;
std::vector<int> children;
};
@ -64,6 +65,10 @@ public:
}
}
}
const std::vector<std::pair<int, float>> &boneRawWeights() const
{
return m_boneRawWeights;
}
private:
std::vector<std::pair<int, float>> m_boneRawWeights;
};

View File

@ -38,6 +38,7 @@ RigWidget::RigWidget(const Document *document, QWidget *parent) :
m_rigWeightRenderWidget->setZRotation(0);
m_rigWeightRenderWidget->setEyePosition(QVector3D(0.0, 0.0, -2.0));
m_rigWeightRenderWidget->toggleWireframe();
m_rigWeightRenderWidget->setNotGraphics(true);
m_infoLabel = new InfoLabel;
m_infoLabel->hide();

View File

@ -0,0 +1,34 @@
#include <QDebug>
#include "simplerendermeshgenerator.h"
void SimpleRenderMeshGenerator::process()
{
generate();
emit finished();
}
void SimpleRenderMeshGenerator::generate()
{
if (nullptr == m_triangleCornerNormals ||
m_triangleCornerNormals->empty()) {
delete m_triangleCornerNormals;
m_triangleCornerNormals = new std::vector<std::vector<QVector3D>>(m_triangles->size());
for (size_t i = 0; i < m_triangles->size(); ++i) {
const auto &triangle = (*m_triangles)[i];
QVector3D triangleNormal = QVector3D::normal(
(*m_vertices)[triangle[0]],
(*m_vertices)[triangle[1]],
(*m_vertices)[triangle[2]]
);
(*m_triangleCornerNormals)[i] = {triangleNormal,
triangleNormal, triangleNormal
};
}
}
delete m_renderMesh;
m_renderMesh = new SimpleShaderMesh(m_vertices, m_triangles, m_triangleCornerNormals);
m_vertices = nullptr;
m_triangles = nullptr;
m_triangleCornerNormals = nullptr;
}

View File

@ -0,0 +1,51 @@
#ifndef DUST3D_RENDER_MESH_GENERATOR_H
#define DUST3D_RENDER_MESH_GENERATOR_H
#include <QObject>
#include <QVector3D>
#include "simpleshadermesh.h"
class SimpleRenderMeshGenerator: public QObject
{
Q_OBJECT
public:
SimpleRenderMeshGenerator(const std::vector<QVector3D> &vertices,
const std::vector<std::vector<size_t>> &triangles,
const std::vector<std::vector<QVector3D>> *triangleCornerNormals=nullptr) :
m_vertices(new std::vector<QVector3D>(vertices)),
m_triangles(new std::vector<std::vector<size_t>>(triangles)),
m_triangleCornerNormals(nullptr != triangleCornerNormals ?
new std::vector<std::vector<QVector3D>>(*triangleCornerNormals) :
nullptr)
{
}
~SimpleRenderMeshGenerator()
{
delete m_vertices;
delete m_triangles;
delete m_renderMesh;
delete m_triangleCornerNormals;
}
SimpleShaderMesh *takeRenderMesh()
{
SimpleShaderMesh *renderMesh = m_renderMesh;
m_renderMesh = nullptr;
return renderMesh;
}
void generate();
signals:
void finished();
public slots:
void process();
private:
std::vector<QVector3D> *m_vertices = nullptr;
std::vector<std::vector<size_t>> *m_triangles = nullptr;
SimpleShaderMesh *m_renderMesh = nullptr;
std::vector<std::vector<QVector3D>> *m_triangleCornerNormals = nullptr;
};
#endif

2
src/simpleshadermesh.cpp Normal file
View File

@ -0,0 +1,2 @@
#include "simpleshadermesh.h"

56
src/simpleshadermesh.h Normal file
View File

@ -0,0 +1,56 @@
#ifndef DUST3D_SIMPLE_SHADER_MESH_H
#define DUST3D_SIMPLE_SHADER_MESH_H
#include <QVector3D>
#include <vector>
class SimpleShaderMesh
{
public:
SimpleShaderMesh(std::vector<QVector3D> *vertices,
std::vector<std::vector<size_t>> *triangles,
std::vector<std::vector<QVector3D>> *triangleCornerNormals) :
m_vertices(vertices),
m_triangles(triangles),
m_triangleCornerNormals(triangleCornerNormals)
{
}
SimpleShaderMesh(const SimpleShaderMesh &mesh)
{
if (nullptr != mesh.m_vertices)
m_vertices = new std::vector<QVector3D>(*mesh.m_vertices);
if (nullptr != mesh.m_triangles)
m_triangles = new std::vector<std::vector<size_t>>(*mesh.m_triangles);
if (nullptr != mesh.m_triangleCornerNormals)
m_triangleCornerNormals = new std::vector<std::vector<QVector3D>>(*mesh.m_triangleCornerNormals);
}
~SimpleShaderMesh()
{
delete m_vertices;
delete m_triangles;
delete m_triangleCornerNormals;
}
const std::vector<QVector3D> *vertices()
{
return m_vertices;
}
const std::vector<std::vector<size_t>> *triangles()
{
return m_triangles;
}
const std::vector<std::vector<QVector3D>> *triangleCornerNormals()
{
return m_triangleCornerNormals;
}
private:
std::vector<QVector3D> *m_vertices = nullptr;
std::vector<std::vector<size_t>> *m_triangles = nullptr;
std::vector<std::vector<QVector3D>> *m_triangleCornerNormals = nullptr;
};
#endif

View File

@ -0,0 +1,220 @@
// This file follow the Stackoverflow content license: CC BY-SA 4.0,
// since it's based on Prashanth N Udupa's work: https://stackoverflow.com/questions/35134270/how-to-use-qopenglframebufferobject-for-shadow-mapping
#include <QColor>
#include <QMutexLocker>
#include <QDebug>
#include "simpleshadermeshbinder.h"
void SimpleShaderMeshBinder::updateMesh(SimpleShaderMesh *mesh)
{
QMutexLocker lock(&m_newMeshMutex);
if (mesh != m_mesh) {
delete m_newMesh;
m_newMesh = mesh;
m_newMeshComing = true;
}
}
void SimpleShaderMeshBinder::checkNewMesh()
{
SimpleShaderMesh *newMesh = nullptr;
if (m_newMeshComing) {
QMutexLocker lock(&m_newMeshMutex);
if (m_newMeshComing) {
newMesh = m_newMesh;
m_newMesh = nullptr;
m_newMeshComing = false;
}
}
if (nullptr != newMesh) {
QVector<QVector3D> vertices;
QVector<int> indexes;
const auto &meshVertices = *newMesh->vertices();
const auto &meshTriangles = *newMesh->triangles();
const auto &meshNormals = *newMesh->triangleCornerNormals();
int i = 0;
for (size_t triangleIndex = 0; triangleIndex < meshTriangles.size(); ++triangleIndex) {
const auto &triangle = meshTriangles[triangleIndex];
const auto &v0 = meshVertices[triangle[0]];
const auto &v1 = meshVertices[triangle[1]];
const auto &v2 = meshVertices[triangle[2]];
vertices.push_back(QVector3D(v0[0], v0[1], v0[2]));
vertices.push_back(QVector3D(v1[0], v1[1], v1[2]));
vertices.push_back(QVector3D(v2[0], v2[1], v2[2]));
indexes << i << i + 1 << i + 2;
i += 3;
}
m_vertexCount = vertices.size();
m_normalOffset = vertices.size() * int(sizeof(QVector3D));
for (size_t triangleIndex = 0; triangleIndex < meshTriangles.size(); ++triangleIndex) {
const auto &normals = meshNormals[triangleIndex];
const auto &n0 = normals[0];
const auto &n1 = normals[1];
const auto &n2 = normals[2];
vertices.push_back(QVector3D(n0[0], n0[1], n0[2]));
vertices.push_back(QVector3D(n1[0], n1[1], n1[2]));
vertices.push_back(QVector3D(n2[0], n2[1], n2[2]));
}
delete m_vertexBuffer;
m_vertexBuffer = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer);
m_vertexBuffer->create();
m_vertexBuffer->bind();
m_vertexBuffer->allocate(
static_cast<const void*>(vertices.constData()),
vertices.size() * int(sizeof(QVector3D))
);
m_vertexBuffer->release();
delete m_indexBuffer;
m_indexBuffer = new QOpenGLBuffer(QOpenGLBuffer::IndexBuffer);
m_indexBuffer->create();
m_indexBuffer->bind();
m_indexBuffer->allocate(
static_cast<const void*>(indexes.constData()),
indexes.size() * int(sizeof(int))
);
m_indexBuffer->release();
delete newMesh;
}
}
void SimpleShaderMeshBinder::renderShadow(const QMatrix4x4 &projectionMatrix, const QMatrix4x4 &viewMatrix)
{
if (nullptr == m_shadowProgram) {
if (!m_openglFunctionsInitialized) {
QOpenGLFunctions::initializeOpenGLFunctions();
m_openglFunctionsInitialized = true;
}
m_shadowProgram = new QOpenGLShaderProgram;
m_shadowProgram->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/shadow.vert");
m_shadowProgram->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/shadow.frag");
m_shadowProgram->link();
}
checkNewMesh();
if (nullptr == m_indexBuffer) {
return;
}
m_shadowProgram->bind();
m_vertexBuffer->bind();
m_indexBuffer->bind();
const QMatrix4x4 modelMatrix = m_sceneMatrix * m_matrix;
const QMatrix4x4 lightViewProjectionMatrix = projectionMatrix * viewMatrix * modelMatrix;
m_shadowProgram->enableAttributeArray("qt_Vertex");
m_shadowProgram->setAttributeBuffer("qt_Vertex", GL_FLOAT, 0, 3, 0);
m_shadowProgram->setUniformValue("qt_LightViewProjectionMatrix", lightViewProjectionMatrix);
glDrawElements(GL_TRIANGLES, m_vertexCount, GL_UNSIGNED_INT, nullptr);
m_indexBuffer->release();
m_vertexBuffer->release();
m_shadowProgram->release();
}
void SimpleShaderMeshBinder::renderScene(const QVector3D &eyePosition, const QVector3D &lightDirection,
const QMatrix4x4 &projectionMatrix, const QMatrix4x4 &viewMatrix,
const QMatrix4x4 &lightViewMatrix)
{
if (nullptr == m_sceneProgram) {
if (!m_openglFunctionsInitialized) {
QOpenGLFunctions::initializeOpenGLFunctions();
m_openglFunctionsInitialized = true;
}
m_sceneProgram = new QOpenGLShaderProgram;
m_sceneProgram->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/scene.vert");
m_sceneProgram->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/scene.frag");
m_sceneProgram->link();
}
checkNewMesh();
if (nullptr == m_indexBuffer || 0 == m_shadowMapTextureId) {
return;
}
m_sceneProgram->bind();
m_vertexBuffer->bind();
m_indexBuffer->bind();
const QMatrix4x4 modelMatrix = m_sceneMatrix * m_matrix;
const QMatrix4x4 modelViewMatrix = (viewMatrix * modelMatrix);
const QMatrix4x4 modelViewProjectionMatrix = projectionMatrix * modelViewMatrix;
const QMatrix4x4 normalMatrix = modelMatrix.inverted().transposed();
m_sceneProgram->enableAttributeArray("qt_Vertex");
m_sceneProgram->setAttributeBuffer("qt_Vertex", GL_FLOAT, 0, 3, 0);
m_sceneProgram->enableAttributeArray("qt_Normal");
m_sceneProgram->setAttributeBuffer("qt_Normal", GL_FLOAT, m_normalOffset, 3, 0);
m_sceneProgram->setUniformValue("qt_ViewMatrix", viewMatrix);
m_sceneProgram->setUniformValue("qt_NormalMatrix", normalMatrix);
m_sceneProgram->setUniformValue("qt_ModelMatrix", modelMatrix);
m_sceneProgram->setUniformValue("qt_ModelViewMatrix", modelViewMatrix);
m_sceneProgram->setUniformValue("qt_ProjectionMatrix", projectionMatrix);
m_sceneProgram->setUniformValue("qt_ModelViewProjectionMatrix", modelViewProjectionMatrix);
m_sceneProgram->setUniformValue("qt_LightMatrix", lightViewMatrix);
m_sceneProgram->setUniformValue("qt_LightViewMatrix", lightViewMatrix * modelMatrix);
m_sceneProgram->setUniformValue("qt_LightViewProjectionMatrix", projectionMatrix * lightViewMatrix * modelMatrix);
if (m_shadowMapTextureId > 0) {
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_shadowMapTextureId);
m_sceneProgram->setUniformValue("qt_ShadowMap", 0);
m_sceneProgram->setUniformValue("qt_ShadowEnabled", true);
} else {
m_sceneProgram->setUniformValue("qt_ShadowEnabled", false);
}
m_sceneProgram->setUniformValue("qt_Light.ambient", QColor(255, 255, 255));
m_sceneProgram->setUniformValue("qt_Light.diffuse", QColor(Qt::white));
m_sceneProgram->setUniformValue("qt_Light.specular", QColor(Qt::white));
m_sceneProgram->setUniformValue("qt_Light.direction", lightDirection);
m_sceneProgram->setUniformValue("qt_Light.eye", eyePosition);
QColor ambient = Qt::white;
ambient.setRgbF(
ambient.redF() * qreal(0.1f),
ambient.greenF() * qreal(0.1f),
ambient.blueF() * qreal(0.1f)
);
QColor diffuse = Qt::white;
diffuse.setRgbF(
diffuse.redF() * qreal(1.0f),
diffuse.greenF() * qreal(1.0f),
diffuse.blueF() * qreal(1.0f)
);
QColor specular = Qt::white;
m_sceneProgram->setUniformValue("qt_Material.ambient", ambient);
m_sceneProgram->setUniformValue("qt_Material.diffuse", diffuse);
m_sceneProgram->setUniformValue("qt_Material.specular", specular);
m_sceneProgram->setUniformValue("qt_Material.specularPower", 0.0f);
m_sceneProgram->setUniformValue("qt_Material.brightness", 1.0f);
m_sceneProgram->setUniformValue("qt_Material.opacity", 1.0f);
glDrawElements(GL_TRIANGLES, m_vertexCount, GL_UNSIGNED_INT, nullptr);
if (m_shadowMapTextureId > 0) {
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, 0);
}
m_indexBuffer->release();
m_vertexBuffer->release();
m_sceneProgram->release();
}

View File

@ -0,0 +1,62 @@
// This file follow the Stackoverflow content license: CC BY-SA 4.0,
// since it's based on Prashanth N Udupa's work: https://stackoverflow.com/questions/35134270/how-to-use-qopenglframebufferobject-for-shadow-mapping
#ifndef DUST3D_SIMPLE_SHADER_MESH_BINDER_H
#define DUST3D_SIMPLE_SHADER_MESH_BINDER_H
#include <QMatrix4x4>
#include <QVector3D>
#include <QOpenGLShaderProgram>
#include <QOpenGLFunctions>
#include <QOpenGLBuffer>
#include <QMutex>
#include "simpleshadermesh.h"
class SimpleShaderMeshBinder : public QOpenGLFunctions
{
public:
~SimpleShaderMeshBinder()
{
delete m_shadowProgram;
delete m_sceneProgram;
delete m_vertexBuffer;
delete m_indexBuffer;
delete m_mesh;
delete m_newMesh;
}
void updateMesh(SimpleShaderMesh *mesh);
void setShadowMapTextureId(const uint &val)
{
m_shadowMapTextureId = val;
}
void setSceneMatrix(const QMatrix4x4 &matrix)
{
m_sceneMatrix = matrix;
}
void renderShadow(const QMatrix4x4 &projectionMatrix, const QMatrix4x4 &viewMatrix);
void renderScene(const QVector3D &eyePosition, const QVector3D &lightDirection,
const QMatrix4x4 &projectionMatrix, const QMatrix4x4 &viewMatrix,
const QMatrix4x4 &lightViewMatrix);
private:
QMatrix4x4 m_sceneMatrix;
QMatrix4x4 m_matrix;
QOpenGLShaderProgram *m_shadowProgram = nullptr;
QOpenGLShaderProgram *m_sceneProgram = nullptr;
bool m_openglFunctionsInitialized = false;
QOpenGLBuffer *m_vertexBuffer = nullptr;
QOpenGLBuffer *m_indexBuffer = nullptr;
uint m_shadowMapTextureId = 0;
int m_vertexCount = 0;
int m_normalOffset = 0;
SimpleShaderMesh *m_mesh = nullptr;
bool m_newMeshComing = false;
SimpleShaderMesh *m_newMesh = nullptr;
QMutex m_newMeshMutex;
void checkNewMesh();
};
#endif

247
src/simpleshaderwidget.cpp Normal file
View File

@ -0,0 +1,247 @@
// This file follow the Stackoverflow content license: CC BY-SA 4.0,
// since it's based on Prashanth N Udupa's work: https://stackoverflow.com/questions/35134270/how-to-use-qopenglframebufferobject-for-shadow-mapping
#include <QOpenGLTexture>
#include <QOpenGLFramebufferObject>
#include <QGuiApplication>
#include "simpleshaderwidget.h"
static const int SHADOW_WIDTH = 2048;
static const int SHADOW_HEIGHT = 2048;
SimpleShaderWidget::SimpleShaderWidget(QWidget *parent) :
QOpenGLWidget(parent)
{
QSurfaceFormat format = QSurfaceFormat::defaultFormat();
format.setProfile(QSurfaceFormat::OpenGLContextProfile::CompatibilityProfile);
format.setVersion(1, 1);
setFormat(format);
}
SimpleShaderWidget::~SimpleShaderWidget()
{
}
void SimpleShaderWidget::updateMesh(SimpleShaderMesh *mesh)
{
if (nullptr == m_meshBinder) {
delete mesh;
return;
}
m_meshBinder->updateMesh(mesh);
update();
}
void SimpleShaderWidget::resizeEvent(QResizeEvent *event)
{
QOpenGLWidget::resizeEvent(event);
}
void SimpleShaderWidget::resizeGL(int w, int h)
{
m_cameraPositionMatrix.setToIdentity();
m_cameraPositionMatrix.rotate(20, 0, 1, 0);
m_cameraPositionMatrix.rotate(-25, 1, 0, 0);
m_lightPositionMatrix = m_cameraPositionMatrix;
m_lightPositionMatrix.rotate(45, 0, 1, 0);
m_lightPositionMatrix.translate(0, 0, 50.0f);
m_projectionMatrix.setToIdentity();
m_projectionMatrix.perspective(45.0, float(w)/float(h), 0.1f, 1000.0f);
}
void SimpleShaderWidget::cleanup()
{
makeCurrent();
if (m_shadowMapInitialized) {
glDeleteTextures(1, &m_shadowMapTextureId);
glDeleteFramebuffers(1, &m_shadowMapFrameBufferId);
}
delete m_meshBinder;
m_meshBinder = nullptr;
doneCurrent();
}
void SimpleShaderWidget::initializeGL()
{
connect(context(), &QOpenGLContext::aboutToBeDestroyed, this, &SimpleShaderWidget::cleanup);
QOpenGLFunctions::initializeOpenGLFunctions();
constexpr float color = (float)0x25 / 255;
glClearColor(color, color, color, 1.0f);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
glEnable(GL_POLYGON_OFFSET_LINE);
glPolygonOffset(-0.03125f, -0.03125f);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
}
void SimpleShaderWidget::paintGL()
{
if (nullptr == m_meshBinder)
m_meshBinder = new SimpleShaderMeshBinder;
renderToShadowMap();
renderToScreen();
}
void SimpleShaderWidget::renderToShadowMap()
{
// Refer http://learnopengl.com/#!Advanced-Lighting/Shadows/Shadow-Mapping
if (!m_shadowMapInitialized) {
// Create a texture for storing the depth map
glGenTextures(1, &m_shadowMapTextureId);
glBindTexture(GL_TEXTURE_2D, m_shadowMapTextureId);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT,
SHADOW_WIDTH, SHADOW_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
GLfloat borderColor[] = { 1.0, 1.0, 1.0, 1.0 };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
// Create a frame-buffer and associate the texture with it.
glGenFramebuffers(1, &m_shadowMapFrameBufferId);
glBindFramebuffer(GL_FRAMEBUFFER, m_shadowMapFrameBufferId);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, m_shadowMapTextureId, 0);
// Let OpenGL know that we are not interested in colors for this buffer
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
// Cleanup for now.
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glBindTexture(GL_TEXTURE_2D, 0);
m_shadowMapInitialized = true;
}
// Render into the depth framebuffer
glBindFramebuffer(GL_FRAMEBUFFER, m_shadowMapFrameBufferId);
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
glClear(GL_DEPTH_BUFFER_BIT);
glEnable(GL_CULL_FACE);
glCullFace(GL_FRONT);
m_lightViewMatrix.setToIdentity();
m_lightViewMatrix.lookAt(m_lightPositionMatrix.map(QVector3D(0, 0, 0)),
QVector3D(),
m_lightPositionMatrix.map(QVector3D(0, 1, 0)).normalized());
m_meshBinder->setShadowMapTextureId(0);
m_meshBinder->renderShadow(m_projectionMatrix, m_lightViewMatrix);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
void SimpleShaderWidget::renderToScreen()
{
const int pixelRatio = devicePixelRatio();
const int w = width() * pixelRatio;
const int h = height() * pixelRatio;
glViewport(0, 0, w, h);
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
const QVector3D center = QVector3D();
const QVector3D eye(0, 0, 1.0);
const QVector3D lightDirection = m_lightPositionMatrix.map(QVector3D(0, 0, -1)).normalized();
m_sceneMatrix.setToIdentity();
m_sceneMatrix.rotate(m_rotationX / 16.0f, 1, 0, 0);
m_sceneMatrix.rotate(m_rotationY / 16.0f, 0, 1, 0);
auto zoomedMatrix = m_cameraPositionMatrix;
zoomedMatrix.translate(0, 0, m_zoom);
m_viewMatrix.setToIdentity();
m_viewMatrix.lookAt(zoomedMatrix.map(QVector3D(0, 0, 0)),
QVector3D(), zoomedMatrix.map(QVector3D(0, 1, 0)).normalized());
m_meshBinder->setShadowMapTextureId(m_shadowMapTextureId);
m_meshBinder->setSceneMatrix(m_sceneMatrix);
m_meshBinder->renderScene(eye, lightDirection, m_projectionMatrix, m_viewMatrix, m_lightViewMatrix);
}
void SimpleShaderWidget::mousePressEvent(QMouseEvent *event)
{
if ((event->button() == Qt::LeftButton/* && QGuiApplication::queryKeyboardModifiers().testFlag(Qt::AltModifier)*/) ||
event->button() == Qt::MidButton) {
m_lastPos = mapFromGlobal(event->globalPos());
if (!m_moveStarted) {
m_moveStarted = true;
}
}
}
void SimpleShaderWidget::setRotationX(int angle)
{
normalizeAngle(angle);
if (angle != m_rotationX) {
m_rotationX = angle;
update();
}
}
void SimpleShaderWidget::setRotationY(int angle)
{
normalizeAngle(angle);
if (angle != m_rotationY) {
m_rotationY = angle;
update();
}
}
void SimpleShaderWidget::normalizeAngle(int &angle)
{
while (angle < 0)
angle += 360 * 16;
while (angle > 360 * 16)
angle -= 360 * 16;
}
void SimpleShaderWidget::mouseMoveEvent(QMouseEvent *event)
{
QPoint pos = mapFromGlobal(event->globalPos());
if (m_moveStarted) {
int dx = pos.x() - m_lastPos.x();
int dy = pos.y() - m_lastPos.y();
setRotationX(m_rotationX + 8 * dy);
setRotationY(m_rotationY + 8 * dx);
}
m_lastPos = pos;
}
void SimpleShaderWidget::wheelEvent(QWheelEvent *event)
{
qreal delta = geometry().height() * 0.1f;
if (event->delta() < 0)
delta = -delta;
zoom(delta);
}
void SimpleShaderWidget::mouseReleaseEvent(QMouseEvent *event)
{
if (m_moveStarted)
m_moveStarted = false;
}
void SimpleShaderWidget::zoom(float delta)
{
m_zoom += m_zoom * (delta > 0 ? -0.1 : 0.1);
update();
}

63
src/simpleshaderwidget.h Normal file
View File

@ -0,0 +1,63 @@
// This file follow the Stackoverflow content license: CC BY-SA 4.0,
// since it's based on Prashanth N Udupa's work: https://stackoverflow.com/questions/35134270/how-to-use-qopenglframebufferobject-for-shadow-mapping
#ifndef DUST3D_SIMPLE_SHADER_WIDGET_H
#define DUST3D_SIMPLE_SHADER_WIDGET_H
#include <QOpenGLWidget>
#include <QOpenGLFunctions>
#include <QResizeEvent>
#include <QMatrix4x4>
#include <QWheelEvent>
#include <QMouseEvent>
#include <QVector3D>
#include <QPoint>
#include "simpleshadermesh.h"
#include "simpleshadermeshbinder.h"
class SimpleShaderWidget: public QOpenGLWidget, public QOpenGLFunctions
{
public:
SimpleShaderWidget(QWidget *parent=nullptr);
~SimpleShaderWidget();
void updateMesh(SimpleShaderMesh *mesh);
void zoom(float delta);
protected:
void resizeEvent(QResizeEvent *event) override;
void resizeGL(int w, int h) override;
void initializeGL() override;
void paintGL() override;
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void wheelEvent(QWheelEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
public slots:
void cleanup();
private:
uint m_shadowMapFrameBufferId = 0;
uint m_shadowMapTextureId = 0;
bool m_shadowMapInitialized = false;
QMatrix4x4 m_sceneMatrix;
QMatrix4x4 m_projectionMatrix;
QMatrix4x4 m_viewMatrix;
QMatrix4x4 m_cameraPositionMatrix;
QMatrix4x4 m_lightPositionMatrix;
QMatrix4x4 m_lightViewMatrix;
SimpleShaderMeshBinder *m_meshBinder = nullptr;
QPoint m_lastPos;
bool m_moveStarted = false;
int m_rotationX = 0;
int m_rotationY = 0;
float m_zoom = 2.5f;
QVector3D m_eyePosition = QVector3D(0.0, 0.0, 1.0);
void setRotationX(int angle);
void setRotationY(int angle);
void renderToShadowMap();
void renderToScreen();
void normalizeAngle(int &angle);
};
#endif

View File

@ -32,8 +32,8 @@ public:
}
void setRadius(float toRadius)
{
if (toRadius < 0.005)
toRadius = 0.005;
if (toRadius < 0.005f)
toRadius = 0.005f;
else if (toRadius > 1)
toRadius = 1;
radius = toRadius;

View File

@ -835,7 +835,7 @@ void SkeletonGraphicsWidget::mouseMoveEvent(QMouseEvent *event)
mouseMove(event);
}
bool SkeletonGraphicsWidget::rotated(void)
bool SkeletonGraphicsWidget::rotated()
{
return m_rotated;
}

View File

@ -546,7 +546,7 @@ public:
void setNodePositionModifyOnly(bool nodePositionModifyOnly);
void setMainProfileOnly(bool mainProfileOnly);
bool inputWheelEventFromOtherWidget(QWheelEvent *event);
bool rotated(void);
bool rotated();
protected:
void mouseMoveEvent(QMouseEvent *event) override;
void wheelEvent(QWheelEvent *event) override;

View File

@ -48,7 +48,7 @@ void SkeletonIkMover::process()
void SkeletonIkMover::resolve()
{
CCDIKSolver solver;
CcdIkSolver solver;
for (auto i = 0u; i < m_ikNodes.size(); i++) {
solver.addNodeInOrder(m_ikNodes[i].position);
}

View File

@ -18,106 +18,9 @@ public:
std::map<QString, std::map<QString, QString>> parts;
std::map<QString, std::map<QString, QString>> components;
std::map<QString, QString> rootComponent;
std::vector<std::pair<std::map<QString, QString>, std::vector<std::pair<std::map<QString, QString>, std::map<QString, std::map<QString, QString>>>>>> poses; // std::pair<Pose attributes, frames> frame: std::pair<Frame attributes, Frame parameters>
std::vector<std::pair<std::map<QString, QString>, std::vector<std::map<QString, QString>>>> motions; // std::pair<Motion attributes, clips>
std::map<QString, std::map<QString, QString>> motions;
std::vector<std::pair<std::map<QString, QString>, std::vector<std::pair<std::map<QString, QString>, std::vector<std::map<QString, QString>>>>>> materials; // std::pair<Material attributes, layers> layer: std::pair<Layer attributes, maps>
uint64_t hash() const
{
std::vector<unsigned char> buffer;
auto addQStringToBuffer = [&buffer](const QString &str) {
auto byteArray = str.toUtf8();
for (const auto &byte: byteArray)
buffer.push_back(byte);
};
for (const auto &item: canvas) {
addQStringToBuffer(item.first);
addQStringToBuffer(item.second);
}
for (const auto &item: nodes) {
addQStringToBuffer(item.first);
for (const auto &subItem: item.second) {
addQStringToBuffer(subItem.first);
addQStringToBuffer(subItem.second);
}
}
for (const auto &item: edges) {
addQStringToBuffer(item.first);
for (const auto &subItem: item.second) {
addQStringToBuffer(subItem.first);
addQStringToBuffer(subItem.second);
}
}
for (const auto &item: parts) {
addQStringToBuffer(item.first);
for (const auto &subItem: item.second) {
addQStringToBuffer(subItem.first);
addQStringToBuffer(subItem.second);
}
}
for (const auto &item: components) {
addQStringToBuffer(item.first);
for (const auto &subItem: item.second) {
addQStringToBuffer(subItem.first);
addQStringToBuffer(subItem.second);
}
}
for (const auto &item: rootComponent) {
addQStringToBuffer(item.first);
addQStringToBuffer(item.second);
}
for (const auto &item: poses) {
for (const auto &subItem: item.first) {
addQStringToBuffer(subItem.first);
addQStringToBuffer(subItem.second);
}
for (const auto &subItem: item.second) {
for (const auto &subSubItem: subItem.first) {
addQStringToBuffer(subSubItem.first);
addQStringToBuffer(subSubItem.second);
}
for (const auto &subSubItem: subItem.second) {
addQStringToBuffer(subSubItem.first);
for (const auto &subSubSubItem: subSubItem.second) {
addQStringToBuffer(subSubSubItem.first);
addQStringToBuffer(subSubSubItem.second);
}
}
}
}
for (const auto &item: motions) {
for (const auto &subItem: item.first) {
addQStringToBuffer(subItem.first);
addQStringToBuffer(subItem.second);
}
for (const auto &subItem: item.second) {
for (const auto &subSubItem: subItem) {
addQStringToBuffer(subSubItem.first);
addQStringToBuffer(subSubItem.second);
}
}
}
for (const auto &item: materials) {
for (const auto &subItem: item.first) {
addQStringToBuffer(subItem.first);
addQStringToBuffer(subItem.second);
}
for (const auto &subItem: item.second) {
for (const auto &subSubItem: subItem.first) {
addQStringToBuffer(subSubItem.first);
addQStringToBuffer(subSubItem.second);
}
for (const auto &subSubItem: subItem.second) {
for (const auto &subSubSubItem: subSubItem) {
addQStringToBuffer(subSubSubItem.first);
addQStringToBuffer(subSubSubItem.second);
}
}
}
}
return crc64(0, buffer.data(), buffer.size());
}
void resolveBoundingBox(QRectF *mainProfile, QRectF *sideProfile, const QString &partId=QString()) const;
};

View File

@ -126,65 +126,14 @@ void saveSkeletonToXmlStream(Snapshot *snapshot, QXmlStreamWriter *writer)
}
writer->writeEndElement();
writer->writeStartElement("poses");
//std::vector<std::pair<std::map<QString, QString>, std::map<QString, std::map<QString, QString>>>>::iterator poseIterator;
std::vector<std::pair<std::map<QString, QString>, std::vector<std::pair<std::map<QString, QString>, std::map<QString, std::map<QString, QString>>>>>>::iterator poseIterator;
for (poseIterator = snapshot->poses.begin(); poseIterator != snapshot->poses.end(); poseIterator++) {
std::map<QString, QString>::iterator poseAttributeIterator;
writer->writeStartElement("pose");
for (poseAttributeIterator = poseIterator->first.begin(); poseAttributeIterator != poseIterator->first.end(); poseAttributeIterator++) {
writer->writeAttribute(poseAttributeIterator->first, poseAttributeIterator->second);
}
writer->writeStartElement("frames");
std::vector<std::pair<std::map<QString, QString>, std::map<QString, std::map<QString, QString>>>>::iterator frameIterator;
for (frameIterator = poseIterator->second.begin(); frameIterator != poseIterator->second.end(); frameIterator++) {
std::map<QString, QString>::iterator frameAttributeIterator;
writer->writeStartElement("frame");
for (frameAttributeIterator = frameIterator->first.begin(); frameAttributeIterator != frameIterator->first.end(); frameAttributeIterator++) {
writer->writeAttribute(frameAttributeIterator->first, frameAttributeIterator->second);
}
writer->writeStartElement("parameters");
std::map<QString, std::map<QString, QString>>::iterator itemsIterator;
for (itemsIterator = frameIterator->second.begin(); itemsIterator != frameIterator->second.end(); itemsIterator++) {
std::map<QString, QString>::iterator parametersIterator;
writer->writeStartElement("parameter");
writer->writeAttribute("for", itemsIterator->first);
for (parametersIterator = itemsIterator->second.begin(); parametersIterator != itemsIterator->second.end();
parametersIterator++) {
writer->writeAttribute(parametersIterator->first, parametersIterator->second);
}
writer->writeEndElement();
}
writer->writeEndElement();
writer->writeEndElement();
}
writer->writeEndElement();
writer->writeEndElement();
}
writer->writeEndElement();
writer->writeStartElement("motions");
std::vector<std::pair<std::map<QString, QString>, std::vector<std::map<QString, QString>>>>::iterator motionIterator;
std::map<QString, std::map<QString, QString>>::iterator motionIterator;
for (motionIterator = snapshot->motions.begin(); motionIterator != snapshot->motions.end(); motionIterator++) {
std::map<QString, QString>::iterator motionAttributeIterator;
writer->writeStartElement("motion");
for (motionAttributeIterator = std::get<0>(*motionIterator).begin(); motionAttributeIterator != std::get<0>(*motionIterator).end(); motionAttributeIterator++) {
for (motionAttributeIterator = motionIterator->second.begin(); motionAttributeIterator != motionIterator->second.end(); motionAttributeIterator++) {
writer->writeAttribute(motionAttributeIterator->first, motionAttributeIterator->second);
}
writer->writeStartElement("clips");
{
std::vector<std::map<QString, QString>>::iterator itemsIterator;
for (itemsIterator = std::get<1>(*motionIterator).begin(); itemsIterator != std::get<1>(*motionIterator).end(); itemsIterator++) {
std::map<QString, QString>::iterator attributesIterator;
writer->writeStartElement("clip");
for (attributesIterator = itemsIterator->begin(); attributesIterator != itemsIterator->end();
attributesIterator++) {
writer->writeAttribute(attributesIterator->first, attributesIterator->second);
}
writer->writeEndElement();
}
}
writer->writeEndElement();
writer->writeEndElement();
}
writer->writeEndElement();
@ -200,9 +149,6 @@ void loadSkeletonFromXmlStream(Snapshot *snapshot, QXmlStreamReader &reader, qui
std::vector<QString> elementNameStack;
std::pair<std::map<QString, QString>, std::vector<std::map<QString, QString>>> currentMaterialLayer;
std::pair<std::map<QString, QString>, std::vector<std::pair<std::map<QString, QString>, std::vector<std::map<QString, QString>>>>> currentMaterial;
std::pair<std::map<QString, QString>, std::vector<std::pair<std::map<QString, QString>, std::map<QString, std::map<QString, QString>>>>> currentPose;
std::pair<std::map<QString, QString>, std::map<QString, std::map<QString, QString>>> currentPoseFrame;
std::pair<std::map<QString, QString>, std::vector<std::map<QString, QString>>> currentMotion;
while (!reader.atEnd()) {
reader.readNext();
if (!reader.isStartElement() && !reader.isEndElement()) {
@ -310,41 +256,16 @@ void loadSkeletonFromXmlStream(Snapshot *snapshot, QXmlStreamReader &reader, qui
foreach(const QXmlStreamAttribute &attr, reader.attributes()) {
currentMaterial.first[attr.name().toString()] = attr.value().toString();
}
} else if (fullName == "canvas.poses.pose") {
QString poseId = reader.attributes().value("id").toString();
if (poseId.isEmpty())
continue;
currentPose = decltype(currentPose)();
foreach(const QXmlStreamAttribute &attr, reader.attributes()) {
currentPose.first[attr.name().toString()] = attr.value().toString();
}
} else if (fullName == "canvas.poses.pose.frames.frame") {
foreach(const QXmlStreamAttribute &attr, reader.attributes()) {
currentPoseFrame.first[attr.name().toString()] = attr.value().toString();
}
} else if (fullName == "canvas.poses.pose.frames.frame.parameters.parameter") {
QString forWhat = reader.attributes().value("for").toString();
if (forWhat.isEmpty())
continue;
foreach(const QXmlStreamAttribute &attr, reader.attributes()) {
if ("for" == attr.name().toString())
continue;
currentPoseFrame.second[forWhat][attr.name().toString()] = attr.value().toString();
}
} else if (fullName == "canvas.motions.motion") {
QString motionId = reader.attributes().value("id").toString();
if (motionId.isEmpty())
continue;
currentMotion = decltype(currentMotion)();
if (flags & SNAPSHOT_ITEM_MOTION) {
std::map<QString, QString> *motionMap = &snapshot->motions[motionId];
foreach(const QXmlStreamAttribute &attr, reader.attributes()) {
currentMotion.first[attr.name().toString()] = attr.value().toString();
(*motionMap)[attr.name().toString()] = attr.value().toString();
}
} else if (fullName == "canvas.motions.motion.clips.clip") {
std::map<QString, QString> attributes;
foreach(const QXmlStreamAttribute &attr, reader.attributes()) {
attributes[attr.name().toString()] = attr.value().toString();
}
currentMotion.second.push_back(attributes);
}
} else if (reader.isEndElement()) {
if (fullName.startsWith("canvas.components.component")) {
@ -355,16 +276,6 @@ void loadSkeletonFromXmlStream(Snapshot *snapshot, QXmlStreamReader &reader, qui
if (flags & SNAPSHOT_ITEM_MATERIAL) {
snapshot->materials.push_back(currentMaterial);
}
} else if (fullName == "canvas.poses.pose.frames.frame") {
currentPose.second.push_back(currentPoseFrame);
} else if (fullName == "canvas.poses.pose") {
if (flags & SNAPSHOT_ITEM_POSE) {
snapshot->poses.push_back(currentPose);
}
} else if (fullName == "canvas.motions.motion") {
if (flags & SNAPSHOT_ITEM_MOTION) {
snapshot->motions.push_back(currentMotion);
}
}
}
}

View File

@ -6,13 +6,11 @@
#define SNAPSHOT_ITEM_CANVAS 0x00000001
#define SNAPSHOT_ITEM_COMPONENT 0x00000002
#define SNAPSHOT_ITEM_MATERIAL 0x00000004
#define SNAPSHOT_ITEM_POSE 0x00000008
#define SNAPSHOT_ITEM_MOTION 0x00000010
#define SNAPSHOT_ITEM_MOTION 0x00000008
#define SNAPSHOT_ITEM_ALL ( \
SNAPSHOT_ITEM_CANVAS | \
SNAPSHOT_ITEM_COMPONENT | \
SNAPSHOT_ITEM_MATERIAL | \
SNAPSHOT_ITEM_POSE | \
SNAPSHOT_ITEM_MOTION \
)

296
src/vertebratamotion.cpp Normal file
View File

@ -0,0 +1,296 @@
#include "vertebratamotion.h"
#include "genericspineandpseudophysics.h"
#include "hermitecurveinterpolation.h"
#include "chainsimulator.h"
#include "ccdikresolver.h"
#include <QDebug>
void VertebrataMotion::prepareLegHeights()
{
GenericSpineAndPseudoPhysics physics;
physics.calculateFootHeights(m_parameters.hipHeight,
m_parameters.stanceTime, m_parameters.swingTime, &m_legHeights, &m_legMoveOffsets);
}
void VertebrataMotion::prepareLegs()
{
m_legs.clear();
for (size_t spineNodeIndex = 0;
spineNodeIndex < m_spineNodes.size(); ++spineNodeIndex) {
std::vector<std::vector<Node>> nodesBySide = {
std::vector<Node>(),
std::vector<Node>(),
std::vector<Node>()
};
bool foundLeg = false;
for (int side = 0; side < 3; ++side) {
auto findLegNodes = m_legNodes.find({spineNodeIndex, (Side)side});
if (findLegNodes == m_legNodes.end())
continue;
nodesBySide[side] = findLegNodes->second;
foundLeg = true;
}
if (!foundLeg)
continue;
Leg leg;
leg.nodes = leg.updatedNodes = nodesBySide;
leg.spineNodeIndex = spineNodeIndex;
m_legs.push_back(leg);
}
}
void VertebrataMotion::prepareLegHeightIndices()
{
int balancedStart = 0;
for (int side = 0; side < 3; ++side) {
bool foundSide = false;
int heightIndex = balancedStart;
for (size_t legIndex = 0; legIndex < m_legs.size(); ++legIndex) {
auto &leg = m_legs[legIndex];
if (leg.nodes[side].empty())
continue;
foundSide = true;
leg.heightIndices[side] = heightIndex;
heightIndex += m_parameters.legSideIntval * m_legHeights.size();
}
if (!foundSide)
continue;
balancedStart += m_parameters.legBalanceIntval * m_legHeights.size();
}
}
void VertebrataMotion::calculateSpineJoints()
{
HermiteCurveInterpolation spineInterpolation;
if (m_spineNodes.size() >= 2) {
spineInterpolation.addPerpendicularDirection(0, QVector2D(0.0, -1.0));
spineInterpolation.addPerpendicularDirection(m_spineNodes.size() - 1, QVector2D(0.0, -1.0));
}
for (size_t legIndex = 0; legIndex < m_legs.size(); ++legIndex) {
auto &leg = m_legs[legIndex];
for (int side = 0; side < 3; ++side) {
if (leg.nodes[side].empty())
continue;
auto &nodes = leg.updatedNodes[side];
QVector3D legDirection = nodes[nodes.size() - 1].position - nodes[0].position;
if (m_parameters.biped && legIndex != m_legs.size() - 1) {
legDirection = QVector3D(1.0, 0.0, 0.0);
} else {
legDirection = legDirection.normalized() * (1.0 - m_parameters.spineStability) +
QVector3D(0.0, -1.0, 0.0) * m_parameters.spineStability;
}
QVector2D legDirection2 = QVector2D(legDirection.z(), legDirection.y());
spineInterpolation.addPerpendicularDirection(leg.spineNodeIndex, legDirection2.normalized());
}
if (0 == legIndex && 0 != leg.spineNodeIndex) {
m_updatedSpineNodes[0].position.setY(m_updatedSpineNodes[0].position.y() +
leg.top - m_updatedSpineNodes[leg.spineNodeIndex].position.y());
}
if (m_legs.size() - 1 == legIndex && m_updatedSpineNodes.size() - 1 != leg.spineNodeIndex) {
m_updatedSpineNodes[m_updatedSpineNodes.size() - 1].position.setY(m_updatedSpineNodes[m_updatedSpineNodes.size() - 1].position.y() +
leg.top - m_updatedSpineNodes[leg.spineNodeIndex].position.y());
}
m_updatedSpineNodes[leg.spineNodeIndex].position.setY(leg.top);
}
for (size_t nodeIndex = 0; nodeIndex < m_spineNodes.size(); ++nodeIndex) {
const auto &node = m_updatedSpineNodes[nodeIndex];
const auto &originalNode = m_spineNodes[nodeIndex];
spineInterpolation.addNode(QVector2D(node.position.z(), node.position.y()), originalNode.position);
}
spineInterpolation.update();
for (size_t spineNodexIndex = 0; spineNodexIndex < m_spineNodes.size(); ++spineNodexIndex) {
auto &node = m_updatedSpineNodes[spineNodexIndex];
const auto &updatedPosition2 = spineInterpolation.getUpdatedPosition(spineNodexIndex);
node.position.setZ(updatedPosition2.x());
node.position.setY(updatedPosition2.y());
}
}
void VertebrataMotion::calculateLegMoves(size_t heightIndex)
{
auto calculateLegTargetTop = [&](size_t legIndex) {
double sumTop = 0.0;
size_t countForAverageTop = 0;
auto &leg = m_legs[legIndex];
for (int side = 0; side < 3; ++side) {
if (leg.nodes[side].empty())
continue;
sumTop += m_legHeights[(heightIndex + leg.heightIndices[side]) % m_legHeights.size()];
++countForAverageTop;
}
if (0 == countForAverageTop)
return 0.0;
return sumTop / countForAverageTop;
};
auto calculateLegCurrentTop = [&](size_t legIndex) {
double sumTop = 0.0;
size_t countForAverageTop = 0;
auto &leg = m_legs[legIndex];
for (int side = 0; side < 3; ++side) {
if (leg.nodes[side].empty())
continue;
sumTop += leg.nodes[side][0].position.y();
++countForAverageTop;
}
if (0 == countForAverageTop)
return 0.0;
return sumTop / countForAverageTop;
};
m_updatedSpineNodes = m_spineNodes;
double hipOffset = 0;
if (m_parameters.biped && m_legs.size() > 1) {
size_t legIndex = m_legs.size() - 1;
double targetTop = calculateLegTargetTop(legIndex);
double currentTop = calculateLegCurrentTop(legIndex);
hipOffset = targetTop - currentTop;
}
for (size_t legIndex = 0; legIndex < m_legs.size(); ++legIndex) {
auto &leg = m_legs[legIndex];
if (m_parameters.biped && legIndex != m_legs.size() - 1) {
leg.top = calculateLegCurrentTop(legIndex) + hipOffset;
} else {
leg.top = calculateLegTargetTop(legIndex);
}
for (int side = 0; side < 3; ++side) {
if (leg.nodes[side].empty())
continue;
double offset = leg.top - leg.nodes[side][0].position.y();
leg.updatedNodes[side] = leg.nodes[side];
auto &nodes = leg.updatedNodes[side];
for (auto &node: nodes)
node.position.setY(node.position.y() + offset);
if (nodes.size() < 2)
continue;
double bottom = 0.0;
if (m_parameters.biped && legIndex != m_legs.size() - 1) {
bottom = std::max(leg.top - m_parameters.armLength, m_groundY);
} else {
bottom = std::max(leg.top - m_parameters.hipHeight, m_groundY);
}
leg.move[side] = m_legMoveOffsets[(heightIndex + leg.heightIndices[side]) % m_legHeights.size()];
double legLength = (nodes[0].position - nodes[nodes.size() - 1].position).length();
auto moveOffset = leg.move[side] * 0.5 * legLength;
CcdIkSolver ccdIkSolver;
if (m_parameters.biped && legIndex != m_legs.size() - 1) {
nodes[1].position.setZ(nodes[1].position.z() + moveOffset * 0.15);
nodes[1].position.setY(nodes[1].position.y() + moveOffset * 0.05);
ccdIkSolver.setSolveFrom(1);
}
for (const auto &node: nodes) {
int nodeIndex = ccdIkSolver.addNodeInOrder(node.position);
if (0 == nodeIndex)
continue;
if (Side::Left != (Side)side && Side::Right != (Side)side)
continue;
if (m_parameters.biped && legIndex != m_legs.size() - 1) {
if (nodeIndex == nodes.size() - 2) {
ccdIkSolver.setNodeHingeConstraint(nodeIndex, QVector3D(1.0, 0.0, 0.0), 180 - 60, 180 - 30);
continue;
}
//if (nodeIndex == nodes.size() - 3) {
// ccdIkSolver.setNodeHingeConstraint(nodeIndex, QVector3D(1.0, 0.0, 0.0), 180 - 15, 180 + 15);
// continue;
//}
} else {
if (nodeIndex == nodes.size() - 2) {
ccdIkSolver.setNodeHingeConstraint(nodeIndex, QVector3D(1.0, 0.0, 0.0), 90 - 15, 90 + 15);
continue;
}
if (m_parameters.biped) {
if (nodeIndex == nodes.size() - 3) {
ccdIkSolver.setNodeHingeConstraint(nodeIndex, QVector3D(1.0, 0.0, 0.0), 240 - 15, 240 + 15);
continue;
}
} else {
if (nodeIndex == nodes.size() - 3) {
ccdIkSolver.setNodeHingeConstraint(nodeIndex, QVector3D(1.0, 0.0, 0.0), 270 - 15, 270 + 15);
continue;
}
}
}
ccdIkSolver.setNodeHingeConstraint(nodeIndex, QVector3D(1.0, 0.0, 0.0), 0, 360);
}
const auto &topNodePosition = nodes[0].position;
const auto &shoulderNodePosition = nodes[1].position;
const auto &bottomNodePosition = nodes[nodes.size() - 1].position;
if (m_parameters.biped && legIndex != m_legs.size() - 1) {
ccdIkSolver.solveTo(QVector3D(shoulderNodePosition.x(), bottom, topNodePosition.z() + moveOffset));
} else {
ccdIkSolver.solveTo(QVector3D(bottomNodePosition.x(), bottom, topNodePosition.z() + moveOffset));
}
for (size_t i = 0; i < nodes.size(); ++i) {
nodes[i].position = ccdIkSolver.getNodeSolvedPosition(i);
}
}
}
}
void VertebrataMotion::generate()
{
prepareLegHeights();
prepareLegs();
prepareLegHeightIndices();
ChainSimulator *tailSimulator = nullptr;
int tailNodeIndex = -1;
if (!m_legs.empty())
tailNodeIndex = m_legs[m_legs.size() - 1].spineNodeIndex;
QVector3D tailSpineOffset;
for (size_t cycle = 0; cycle < m_parameters.cycles; ++cycle) {
for (size_t heightIndex = 0; heightIndex < m_legHeights.size(); ++heightIndex) {
calculateLegMoves(heightIndex);
calculateSpineJoints();
if (nullptr == tailSimulator && -1 != tailNodeIndex) {
std::vector<QVector3D> ropeVertices(m_spineNodes.size() - tailNodeIndex);
for (size_t nodeIndex = tailNodeIndex; nodeIndex < m_spineNodes.size(); ++nodeIndex) {
ropeVertices[nodeIndex - tailNodeIndex] = m_spineNodes[nodeIndex].position;
}
tailSimulator = new ChainSimulator(&ropeVertices);
tailSimulator->setExternalForce(QVector3D(0.0, 9.80665 * m_parameters.tailLiftForce, -9.80665 * m_parameters.tailDragForce));
tailSimulator->fixVertexPosition(0);
tailSimulator->setGroundY(m_groundY);
tailSimulator->start();
tailSpineOffset = m_updatedSpineNodes[tailNodeIndex].position - m_spineNodes[tailNodeIndex].position;
}
if (nullptr != tailSimulator) {
tailSimulator->updateVertexPosition(0, m_updatedSpineNodes[tailNodeIndex].position - tailSpineOffset);
tailSimulator->simulate(1.0 / 60);
for (size_t nodeIndex = tailNodeIndex; nodeIndex < m_spineNodes.size(); ++nodeIndex) {
m_updatedSpineNodes[nodeIndex].position = tailSimulator->getVertexMotion(nodeIndex - tailNodeIndex).position + tailSpineOffset;
}
}
auto frame = m_updatedSpineNodes;
for (const auto &it: m_legs) {
for (const auto &nodes: it.updatedNodes)
for (const auto &node: nodes)
frame.push_back(node);
}
m_frames.push_back(frame);
}
}
delete tailSimulator;
}

100
src/vertebratamotion.h Normal file
View File

@ -0,0 +1,100 @@
#ifndef DUST3D_VERTEBRATA_MOTION_H
#define DUST3D_VERTEBRATA_MOTION_H
#include <QVector3D>
#include <vector>
#include <map>
#include <unordered_map>
class VertebrataMotion
{
public:
struct Parameters
{
double stanceTime = 0.35;
double swingTime = 0.23;
double hipHeight = 0.39;
double armLength = 0.29;
double legSideIntval = 0.5;
double legBalanceIntval = 0.5;
double spineStability = 0.5;
size_t cycles = 5;
double groundOffset = 0.4;
double tailLiftForce = 2.0;
double tailDragForce = 4.0;
bool biped = false;
};
struct Node
{
QVector3D position;
double radius;
int boneIndex = -1;
bool isTail = false;
};
enum class Side
{
Middle = 0,
Left,
Right,
};
struct Leg
{
std::vector<std::vector<Node>> nodes;
std::vector<std::vector<Node>> updatedNodes;
size_t heightIndices[3] = {0, 0, 0};
size_t spineNodeIndex = 0;
double top = 0.0;
double move[3] = {0, 0, 0};
};
VertebrataMotion()
{
}
void setSpineNodes(const std::vector<Node> &nodes)
{
m_spineNodes = nodes;
}
void setLegNodes(size_t spineNodeIndex, Side side, const std::vector<Node> &nodes)
{
m_legNodes[{spineNodeIndex, side}] = nodes;
}
const std::vector<std::vector<Node>> &frames()
{
return m_frames;
}
void setParameters(const Parameters &parameters)
{
m_parameters = parameters;
}
void setGroundY(double groundY)
{
m_groundY = groundY;
}
void generate();
private:
std::vector<std::vector<Node>> m_frames;
Parameters m_parameters;
std::vector<Node> m_spineNodes;
std::vector<Node> m_updatedSpineNodes;
std::map<std::pair<size_t, Side>, std::vector<Node>> m_legNodes;
std::vector<double> m_legHeights;
std::vector<double> m_legMoveOffsets;
std::vector<Leg> m_legs;
double m_groundY = 0.0;
void prepareLegHeights();
void prepareLegs();
void prepareLegHeightIndices();
void calculateLegMoves(size_t heightIndex);
void calculateSpineJoints();
};
#endif

View File

@ -0,0 +1,170 @@
#include <QFormLayout>
#include <QDoubleSpinBox>
#include <QSpinBox>
#include "vertebratamotionparameterswidget.h"
#include "util.h"
std::map<QString, QString> VertebrataMotionParametersWidget::fromVertebrataMotionParameters(const VertebrataMotion::Parameters &from)
{
std::map<QString, QString> parameters;
parameters["stanceTime"] = QString::number(from.stanceTime);
parameters["swingTime"] = QString::number(from.swingTime);
parameters["hipHeight"] = QString::number(from.hipHeight);
parameters["armLength"] = QString::number(from.armLength);
parameters["legSideIntval"] = QString::number(from.legSideIntval);
parameters["legBalanceIntval"] = QString::number(from.legBalanceIntval);
parameters["spineStability"] = QString::number(from.spineStability);
parameters["cycles"] = QString::number(from.cycles);
parameters["groundOffset"] = QString::number(from.groundOffset);
parameters["tailLiftForce"] = QString::number(from.tailLiftForce);
parameters["tailDragForce"] = QString::number(from.tailDragForce);
return parameters;
}
VertebrataMotion::Parameters VertebrataMotionParametersWidget::toVertebrataMotionParameters(const std::map<QString, QString> &parameters)
{
VertebrataMotion::Parameters vertebrataMotionParameters;
if (parameters.end() != parameters.find("stanceTime"))
vertebrataMotionParameters.stanceTime = valueOfKeyInMapOrEmpty(parameters, "stanceTime").toDouble();
if (parameters.end() != parameters.find("swingTime"))
vertebrataMotionParameters.swingTime = valueOfKeyInMapOrEmpty(parameters, "swingTime").toDouble();
if (parameters.end() != parameters.find("hipHeight"))
vertebrataMotionParameters.hipHeight = valueOfKeyInMapOrEmpty(parameters, "hipHeight").toDouble();
if (parameters.end() != parameters.find("armLength"))
vertebrataMotionParameters.armLength = valueOfKeyInMapOrEmpty(parameters, "armLength").toDouble();
if (parameters.end() != parameters.find("legSideIntval"))
vertebrataMotionParameters.legSideIntval = valueOfKeyInMapOrEmpty(parameters, "legSideIntval").toDouble();
if (parameters.end() != parameters.find("legBalanceIntval"))
vertebrataMotionParameters.legBalanceIntval = valueOfKeyInMapOrEmpty(parameters, "legBalanceIntval").toDouble();
if (parameters.end() != parameters.find("spineStability"))
vertebrataMotionParameters.spineStability = valueOfKeyInMapOrEmpty(parameters, "spineStability").toDouble();
if (parameters.end() != parameters.find("cycles"))
vertebrataMotionParameters.cycles = valueOfKeyInMapOrEmpty(parameters, "cycles").toInt();
if (parameters.end() != parameters.find("groundOffset"))
vertebrataMotionParameters.groundOffset = valueOfKeyInMapOrEmpty(parameters, "groundOffset").toDouble();
if (parameters.end() != parameters.find("tailLiftForce"))
vertebrataMotionParameters.tailLiftForce = valueOfKeyInMapOrEmpty(parameters, "tailLiftForce").toDouble();
if (parameters.end() != parameters.find("tailDragForce"))
vertebrataMotionParameters.tailDragForce = valueOfKeyInMapOrEmpty(parameters, "tailDragForce").toDouble();
return vertebrataMotionParameters;
}
VertebrataMotionParametersWidget::VertebrataMotionParametersWidget(const std::map<QString, QString> &parameters) :
m_parameters(parameters)
{
m_vertebrataMotionParameters = toVertebrataMotionParameters(m_parameters);
QFormLayout *parametersLayout = new QFormLayout;
parametersLayout->setSpacing(0);
QDoubleSpinBox *stanceTimeBox = new QDoubleSpinBox;
stanceTimeBox->setValue(m_vertebrataMotionParameters.stanceTime);
stanceTimeBox->setSuffix(" s");
parametersLayout->addRow(tr("Stance time: "), stanceTimeBox);
connect(stanceTimeBox, QOverload<double>::of(&QDoubleSpinBox::valueChanged), [&](double value){
m_vertebrataMotionParameters.stanceTime = value;
m_parameters = fromVertebrataMotionParameters(m_vertebrataMotionParameters);
emit parametersChanged();
});
QDoubleSpinBox *swingTimeBox = new QDoubleSpinBox;
swingTimeBox->setValue(m_vertebrataMotionParameters.swingTime);
swingTimeBox->setSuffix(" s");
parametersLayout->addRow(tr("Swing time: "), swingTimeBox);
connect(swingTimeBox, QOverload<double>::of(&QDoubleSpinBox::valueChanged), [&](double value){
m_vertebrataMotionParameters.swingTime = value;
m_parameters = fromVertebrataMotionParameters(m_vertebrataMotionParameters);
emit parametersChanged();
});
QDoubleSpinBox *hipHeightBox = new QDoubleSpinBox;
hipHeightBox->setValue(m_vertebrataMotionParameters.hipHeight);
parametersLayout->addRow(tr("Hip height: "), hipHeightBox);
connect(hipHeightBox, QOverload<double>::of(&QDoubleSpinBox::valueChanged), [&](double value){
m_vertebrataMotionParameters.hipHeight = value;
m_parameters = fromVertebrataMotionParameters(m_vertebrataMotionParameters);
emit parametersChanged();
});
QDoubleSpinBox *armLengthBox = new QDoubleSpinBox;
armLengthBox->setValue(m_vertebrataMotionParameters.armLength);
parametersLayout->addRow(tr("Arm length: "), armLengthBox);
connect(armLengthBox, QOverload<double>::of(&QDoubleSpinBox::valueChanged), [&](double value){
m_vertebrataMotionParameters.armLength = value;
m_parameters = fromVertebrataMotionParameters(m_vertebrataMotionParameters);
emit parametersChanged();
});
QDoubleSpinBox *legSideIntvalBox = new QDoubleSpinBox;
legSideIntvalBox->setValue(m_vertebrataMotionParameters.legSideIntval);
parametersLayout->addRow(tr("Leg side intval: "), legSideIntvalBox);
connect(legSideIntvalBox, QOverload<double>::of(&QDoubleSpinBox::valueChanged), [&](double value){
m_vertebrataMotionParameters.legSideIntval = value;
m_parameters = fromVertebrataMotionParameters(m_vertebrataMotionParameters);
emit parametersChanged();
});
QDoubleSpinBox *legBalanceIntvalBox = new QDoubleSpinBox;
legBalanceIntvalBox->setValue(m_vertebrataMotionParameters.legBalanceIntval);
parametersLayout->addRow(tr("Leg balance intval: "), legBalanceIntvalBox);
connect(legBalanceIntvalBox, QOverload<double>::of(&QDoubleSpinBox::valueChanged), [&](double value){
m_vertebrataMotionParameters.legBalanceIntval = value;
m_parameters = fromVertebrataMotionParameters(m_vertebrataMotionParameters);
emit parametersChanged();
});
QDoubleSpinBox *spineStabilityBox = new QDoubleSpinBox;
spineStabilityBox->setValue(m_vertebrataMotionParameters.spineStability);
parametersLayout->addRow(tr("Spine stability: "), spineStabilityBox);
connect(spineStabilityBox, QOverload<double>::of(&QDoubleSpinBox::valueChanged), [&](double value){
m_vertebrataMotionParameters.spineStability = value;
m_parameters = fromVertebrataMotionParameters(m_vertebrataMotionParameters);
emit parametersChanged();
});
QSpinBox *cyclesBox = new QSpinBox;
cyclesBox->setValue(m_vertebrataMotionParameters.cycles);
parametersLayout->addRow(tr("Cycles: "), cyclesBox);
connect(cyclesBox, QOverload<int>::of(&QSpinBox::valueChanged), [&](int value){
m_vertebrataMotionParameters.cycles = value;
m_parameters = fromVertebrataMotionParameters(m_vertebrataMotionParameters);
emit parametersChanged();
});
QDoubleSpinBox *groundOffsetBox = new QDoubleSpinBox;
groundOffsetBox->setRange(-2.0, 2.0);
groundOffsetBox->setValue(m_vertebrataMotionParameters.groundOffset);
parametersLayout->addRow(tr("Ground offset: "), groundOffsetBox);
connect(groundOffsetBox, QOverload<double>::of(&QDoubleSpinBox::valueChanged), [&](double value){
m_vertebrataMotionParameters.groundOffset = value;
m_parameters = fromVertebrataMotionParameters(m_vertebrataMotionParameters);
emit parametersChanged();
});
QDoubleSpinBox *tailLiftForceBox = new QDoubleSpinBox;
tailLiftForceBox->setRange(-10.0, 10.0);
tailLiftForceBox->setValue(m_vertebrataMotionParameters.tailLiftForce);
tailLiftForceBox->setSuffix(" g");
parametersLayout->addRow(tr("Tail lift force: "), tailLiftForceBox);
connect(tailLiftForceBox, QOverload<double>::of(&QDoubleSpinBox::valueChanged), [&](double value){
m_vertebrataMotionParameters.tailLiftForce = value;
m_parameters = fromVertebrataMotionParameters(m_vertebrataMotionParameters);
emit parametersChanged();
});
QDoubleSpinBox *tailDragForceBox = new QDoubleSpinBox;
tailDragForceBox->setValue(m_vertebrataMotionParameters.tailDragForce);
tailDragForceBox->setSuffix(" g");
parametersLayout->addRow(tr("Tail drag force: "), tailDragForceBox);
connect(tailDragForceBox, QOverload<double>::of(&QDoubleSpinBox::valueChanged), [&](double value){
m_vertebrataMotionParameters.tailDragForce = value;
m_parameters = fromVertebrataMotionParameters(m_vertebrataMotionParameters);
emit parametersChanged();
});
setLayout(parametersLayout);
}

View File

@ -0,0 +1,28 @@
#ifndef DUST3D_VERTEBRATA_MOTION_PARAMETERS_WIDGET_H
#define DUST3D_VERTEBRATA_MOTION_PARAMETERS_WIDGET_H
#include <QWidget>
#include <map>
#include <QString>
#include "vertebratamotion.h"
class VertebrataMotionParametersWidget : public QWidget
{
Q_OBJECT
signals:
void parametersChanged();
public:
VertebrataMotionParametersWidget(const std::map<QString, QString> &parameters=std::map<QString, QString>());
const std::map<QString, QString> &getParameters() const
{
return m_parameters;
}
static std::map<QString, QString> fromVertebrataMotionParameters(const VertebrataMotion::Parameters &from);
static VertebrataMotion::Parameters toVertebrataMotionParameters(const std::map<QString, QString> &parameters);
private:
std::map<QString, QString> m_parameters;
VertebrataMotion::Parameters m_vertebrataMotionParameters;
};
#endif