Introduce procedural motion generator
Removed pose editor and motion timeline editor. Introduced simple procedural motion generator (Added experiment walk cycle).master
parent
e29bcf5619
commit
7a52133846
|
@ -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>
|
||||
|
|
84
dust3d.pro
84
dust3d.pro
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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/>
|
||||
|
|
|
@ -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/>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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;
|
||||
}
|
|
@ -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
|
|
@ -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]});
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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
|
321
src/document.cpp
321
src/document.cpp
|
@ -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;
|
||||
|
|
165
src/document.h
165
src/document.h
|
@ -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 ¤tPostProcessedOutcome() 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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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()) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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;}");
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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> ¶meters)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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> ¶meters);
|
||||
void removeMotion(const QUuid &motionId);
|
||||
void setMotionParameters(const QUuid &motionId, const std::map<QString, QString> ¶meters);
|
||||
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> ¶meters);
|
||||
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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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> ¶meters)
|
||||
{
|
||||
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 ¶meters = 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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> ¶meters);
|
||||
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
|
||||
|
|
|
@ -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));
|
||||
}
|
|
@ -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
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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]});
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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>> ¶meters,
|
||||
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>> ¶meters)
|
||||
{
|
||||
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>> ¶meters, 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>> ¶meters,
|
||||
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>> ¶meters,
|
||||
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();
|
||||
}
|
|
@ -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>> ¶meters, const std::set<QUuid> &limitNodeIds=std::set<QUuid>()) const;
|
||||
void fromParameters(const std::vector<RiggerBone> *rigBones,
|
||||
const std::map<QString, std::map<QString, QString>> ¶meters);
|
||||
|
||||
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>> ¶meters,
|
||||
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>> ¶meters,
|
||||
float *length, QVector3D *position);
|
||||
static void neckJoint1DirectionFromParameters(const std::map<QString, std::map<QString, QString>> ¶meters,
|
||||
QVector3D *direction);
|
||||
};
|
||||
|
||||
#endif
|
|
@ -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();
|
||||
}
|
|
@ -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
|
|
@ -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(©Action, &QAction::triggered, this, &PoseListWidget::copy);
|
||||
contextMenu.addAction(©Action);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
|
@ -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
|
|
@ -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);
|
||||
}
|
|
@ -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
|
|
@ -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();
|
||||
}
|
|
@ -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
|
|
@ -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();
|
||||
}
|
|
@ -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
|
|
@ -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();
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
32
src/poser.h
32
src/poser.h
|
@ -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>> ¶meters();
|
||||
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
|
|
@ -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;
|
||||
}
|
|
@ -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
|
|
@ -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);
|
||||
}
|
|
@ -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
|
|
@ -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 ¤tNode = 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 ¤tBone = (*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));
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
|||
#include "simpleshadermesh.h"
|
||||
|
|
@ -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
|
|
@ -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();
|
||||
}
|
|
@ -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
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
@ -835,7 +835,7 @@ void SkeletonGraphicsWidget::mouseMoveEvent(QMouseEvent *event)
|
|||
mouseMove(event);
|
||||
}
|
||||
|
||||
bool SkeletonGraphicsWidget::rotated(void)
|
||||
bool SkeletonGraphicsWidget::rotated()
|
||||
{
|
||||
return m_rotated;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 \
|
||||
)
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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 ¶meters)
|
||||
{
|
||||
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
|
|
@ -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> ¶meters)
|
||||
{
|
||||
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> ¶meters) :
|
||||
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);
|
||||
}
|
|
@ -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> ¶meters=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> ¶meters);
|
||||
private:
|
||||
std::map<QString, QString> m_parameters;
|
||||
VertebrataMotion::Parameters m_vertebrataMotionParameters;
|
||||
};
|
||||
|
||||
#endif
|
Loading…
Reference in New Issue