From 710890d894066332387a5261821ebf61873d7adb Mon Sep 17 00:00:00 2001 From: Wenzel Jakob Date: Mon, 15 Nov 2010 16:05:50 +0100 Subject: [PATCH] initial support for importing animation tracks --- include/mitsuba/render/track.h | 67 +++++++++ src/converter/collada.cpp | 136 ++++++++++++++++-- tools/blender/mitsuba/ui/lamps.py | 2 +- .../blender/mitsuba/ui/materials/__init__.pyc | Bin 3892 -> 0 bytes .../mitsuba/ui/materials/lambertian.pyc | Bin 964 -> 0 bytes tools/blender/mitsuba/ui/materials/main.pyc | Bin 1479 -> 0 bytes .../blender/mitsuba/ui/textures/__init__.pyc | Bin 3726 -> 0 bytes .../mitsuba/ui/textures/checkerboard.pyc | Bin 969 -> 0 bytes .../mitsuba/ui/textures/ldrtexture.pyc | Bin 947 -> 0 bytes tools/blender/mitsuba/ui/textures/main.pyc | Bin 1897 -> 0 bytes tools/blender/mitsuba/ui/textures/mapping.pyc | Bin 961 -> 0 bytes 11 files changed, 195 insertions(+), 10 deletions(-) create mode 100644 include/mitsuba/render/track.h delete mode 100644 tools/blender/mitsuba/ui/materials/__init__.pyc delete mode 100644 tools/blender/mitsuba/ui/materials/lambertian.pyc delete mode 100644 tools/blender/mitsuba/ui/materials/main.pyc delete mode 100644 tools/blender/mitsuba/ui/textures/__init__.pyc delete mode 100644 tools/blender/mitsuba/ui/textures/checkerboard.pyc delete mode 100644 tools/blender/mitsuba/ui/textures/ldrtexture.pyc delete mode 100644 tools/blender/mitsuba/ui/textures/main.pyc delete mode 100644 tools/blender/mitsuba/ui/textures/mapping.pyc diff --git a/include/mitsuba/render/track.h b/include/mitsuba/render/track.h new file mode 100644 index 00000000..e181a03b --- /dev/null +++ b/include/mitsuba/render/track.h @@ -0,0 +1,67 @@ +/* + This file is part of Mitsuba, a physically based rendering system. + + Copyright (c) 2007-2010 by Wenzel Jakob and others. + + Mitsuba is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License Version 3 + as published by the Free Software Foundation. + + Mitsuba is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#if !defined(__ANIMATION_TRACK_H) +#define __ANIMATION_TRACK_H + +#include + +MTS_NAMESPACE_BEGIN + +template class AnimationTrack { +public: + enum EType { + EInvalid = 0, + ELocationX, ELocationY, ELocationZ, ELocationXYZ, + EScaleX, EScaleY, EScaleZ, EScaleXYZ, + ERotationX, ERotationY, ERotationZ, ERotationXYZ + }; + + AnimationTrack(EType type, size_t nKeyframes) + : m_type(type), m_times(nKeyframes), m_values(nKeyframes) { } + + /// Return the type of this track + inline EType getType() const { return m_type; } + + /// Return the number of keyframes + inline size_t getSize() const { return m_times.size(); } + + /// Set the time value of a certain keyframe + inline void setTime(size_t idx, Float time) { m_times[idx] = time; } + + /// Return the time value of a certain keyframe + inline Float getTime(size_t idx) const { return m_times[idx]; } + + /// Set the value of a certain keyframe + inline void setValue(size_t idx, const T &value) { m_values[idx] = value; } + + /// Return the value of a certain keyframe + inline const T &getValue(size_t idx) const { return m_values[idx]; } + + /// Evaluate the animation track at an arbitrary time value + inline T lookup(Float time) const { + } +private: + EType m_type; + std::vector m_times; + std::vector m_values; +}; + +MTS_NAMESPACE_END + +#endif /* __ANIMATION_TRACK_H */ diff --git a/src/converter/collada.cpp b/src/converter/collada.cpp index e7ca60db..1b75836a 100644 --- a/src/converter/collada.cpp +++ b/src/converter/collada.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -35,14 +36,21 @@ #include #endif +#ifndef WIN32 +#define __stdcall +#endif + #include "converter.h" +typedef AnimationTrack FloatTrack; typedef std::map StringMap; typedef std::map RefCountMap; +typedef std::map AnimationMap; struct ColladaContext { GeometryConverter *cvt; RefCountMap refCountMap; + AnimationMap animations; std::set serializedGeometry; fs::path texturesDirectory; fs::path meshesDirectory; @@ -153,6 +161,7 @@ VertexData *fetchVertexData(Transform transform, for (size_t i=0; igetOffset(), offset = offsetInStream; + daeURI &sourceRef = inputs[i]->getSource(); sourceRef.resolveElement(); domSource *source = daeSafeCast(sourceRef.getElement()); @@ -309,7 +318,6 @@ struct triangle_key_order : public std::binary_function target = tokenize(channel->getTarget(), "./"); + SAssertEx(target.size() == 3, "Encountered an unknown animation channel identifier!"); + + daeURI &sourceRef = channel->getSource(); + sourceRef.resolveElement(); + domSampler *sampler = daeSafeCast(sourceRef.getElement()); + if (!sampler) + SLog(EError, "Referenced animation sampler not found!"); + const domInputLocal_Array &inputs = sampler->getInput_array(); + FloatTrack *track = NULL; + FloatTrack::EType trackType = FloatTrack::EInvalid; + boost::to_lower(target[1]); + boost::to_lower(target[2]); + if (target[1] == "location") { + if (target[2] == "x") { + trackType = FloatTrack::ELocationX; + } else if (target[2] == "y") { + trackType = FloatTrack::ELocationY; + } else if (target[3] == "z") { + trackType = FloatTrack::ELocationZ; + } + } else if (target[1] == "scale") { + if (target[2] == "x") { + trackType = FloatTrack::EScaleX; + } else if (target[2] == "y") { + trackType = FloatTrack::EScaleY; + } else if (target[3] == "z") { + trackType = FloatTrack::EScaleZ; + } + } else if (target[1] == "rotationx" && target[2] == "angle") { + trackType = FloatTrack::ERotationX; + } else if (target[1] == "rotationy" && target[2] == "angle") { + trackType = FloatTrack::ERotationY; + } else if (target[1] == "rotationz" && target[2] == "angle") { + trackType = FloatTrack::ERotationZ; + } + + if (trackType == FloatTrack::EInvalid) { + SLog(EWarn, "Skipping unsupported animation track of type %s.%s", + target[1].c_str(), target[2].c_str()); + continue; + } + + for (size_t j=0; jgetSource(); + sourceRef.resolveElement(); + domSource *source = daeSafeCast(sourceRef.getElement()); + if (!source) + SLog(EError, "Referenced animation source not found!"); + std::string semantic = inputs[j]->getSemantic(); + domSource::domTechnique_common *techniqueCommon = source->getTechnique_common(); + if (!techniqueCommon) + SLog(EError, "Data source does not have a tag!"); + domAccessor *accessor = techniqueCommon->getAccessor(); + if (!accessor) + SLog(EError, "Data source does not have a tag!"); + unsigned int nParams = (unsigned int) accessor->getParam_array().getCount(), + stride = (unsigned int) accessor->getStride(); + size_t size = (size_t) accessor->getCount(); + + if (stride != 1 || nParams != 1) { + /// Only single-valued tracks are supported for now + SLog(EError, "Encountered a multi-valued animation track."); + } + if (!track) + track = new FloatTrack(trackType, size); + else + SAssert(track->getSize() == size); + + if (semantic == "INPUT") { + domListOfFloats &floatArray = source->getFloat_array()->getValue(); + for (size_t i=0; isetTime(i, floatArray[i]); + } else if (semantic == "OUTPUT") { + domListOfFloats &floatArray = source->getFloat_array()->getValue(); + for (size_t i=0; isetValue(i, floatArray[i]); + } else if (semantic == "INTERPOLATION") { + /// Ignored for now + } else { + SLog(EWarn, "Encountered an unsupported semantic: \"%s\"", semantic.c_str()); + } + } + + if (track) + ctx.animations.insert(AnimationMap::value_type(target[0], track)); + } +} GLvoid __stdcall tessBegin(GLenum type) { SAssert(type == GL_TRIANGLES); @@ -1338,15 +1448,12 @@ GLvoid __stdcall tessCombine(GLdouble coords[3], void *vertex_data[4], *outData = result; } -GLvoid __stdcall tessEnd() { -} - +GLvoid __stdcall tessEnd() { } +GLvoid __stdcall tessEdgeFlag(GLboolean) { } GLvoid __stdcall tessError(GLenum error) { SLog(EError, "The GLU tesselator generated an error: %s!", gluErrorString(error)); } -GLvoid __stdcall tessEdgeFlag(GLboolean) { -} void GeometryConverter::convertCollada(const fs::path &inputFile, std::ostream &os, @@ -1404,12 +1511,23 @@ void GeometryConverter::convertCollada(const fs::path &inputFile, for (size_t j=0; jgetLibrary_animations_array(); + for (size_t i=0; igetAnimation_array(); + for (size_t j=0; jsecond; os << "" << endl; diff --git a/tools/blender/mitsuba/ui/lamps.py b/tools/blender/mitsuba/ui/lamps.py index 42487ffe..5fcdd4b1 100644 --- a/tools/blender/mitsuba/ui/lamps.py +++ b/tools/blender/mitsuba/ui/lamps.py @@ -61,7 +61,7 @@ class lamps(DataButtonsPanel, property_group_renderer, bpy.types.Panel): else: layout.prop(lamp.mitsuba_lamp, "envmap_file", text="HDRI file") layout.prop(lamp.mitsuba_lamp, "intensity", text="Intensity") - layout.prop(lamp.mitsuba_lamp, "sampling_weight", text = "Sampling weight") + layout.prop(lamp.mitsuba_lamp, "samplingWeight", text = "Sampling weight") # SPOT LAMP: Blender Properties if lamp.type == 'SPOT': diff --git a/tools/blender/mitsuba/ui/materials/__init__.pyc b/tools/blender/mitsuba/ui/materials/__init__.pyc deleted file mode 100644 index 5c8316dc2591e40dc736c4f4d3fc9b860efda591..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3892 zcmcgvPfr_16o0e+gH0e1k`QQ;)>RMjp}|tr^i-uK0R^OBN8l?7Z!Fr``(*3@Bh4M%+1gKnfvQao5r3J=HKB> zPBD1+R}>KKXLE-FMO}yX9a4X7_B-f*uvi_Lw!RIe(G_8x{EH#M>E1}|-zMq%>S4NZ_?9{}5nqn?S= zGkq9Gwx?qgdL}k;o0s#$zx=+$oA81@(WZfBinTmatdnf*0B2_~bp153Ew^6HF@)`+ zb?A1D)|SQ7x*K^m;LqFmeAU=_yWZ5B_1D|=&Am*nj4X)SZjfkQ1r^C)f55DE6m?DQ z#DqVZpw(2|4=NPUro5|4y44EfWpkIg% zU{OUs6J5ZlMTYEECym->06+Z8{P|7TI4Ra4AQbf#Gl-t>q~lO0N1Z(H-YL*=5iBJx zwyxZlCBA}Bzg)}|u83~L_rkUETT02N2rrJ@Mn$t8Bz6^xc?Z8cj(Qy-jF;wd_ z>@IOY#VUvU|BP@e9y95L%J!CM3zfPiJwk#Ex0{|3;>atuxCt|GMVa)YM!r8}vv4HL zsi`3k+8>B&#Y%$b=kuj?@{+6O_)6!wm7HaPH4=?cB?v5Nt?H zxwXBqskb)QH#T?2i_$26*b8jpS9p^(44f^JKggMLM?Q0ud@hqbMNg>@;MFXuk41jl zgvPS^Spk))2@ichOMs5jfJvfO$F$Sysvk=w0WuNyW9+05%5W3j9!BAp5G>&jDlDjx z5xGut3WyPZLp-v9XlOn~=ea=sWxBws8S3LW6FBN1 zu<$$F4KH8hm13ND+ItLYbzY)=i8?s;isS)aA`kOfls-od5Vr&|H-VTXZ(b0$#}Rqz zfP8_DolBVzRfD}U$-sY6*4)7^nLq$73Fz)P=$?S?3+Vld3Sk#02|Mfsmp zzf7HZQFVp9Rq{Te3kRG59hsy+@q1DJHuWLU!=comd;hB-7T9_!{DS&9iUG=a!Yq%# zjpTbVBavogulgxmR+sP_hNVt|2!}SGeQrDTKbXs-&qqISD%B{CP7%(Y$|&+cY5nhQ#RS$*lteek2=fZb2hGL)2X5)Cg5yk z(!Ff@c$(Cn84z>IZwx;;OK>itLZt4YIsD^ye*XT}E&x7ZZOQ%6oY*~K1t7Q8|}HNE8D`oV#?HXdjVP_9r#J6 zb~_4eHxv`c}P^a4MXLOf!(y&)vrZwg7^woG98{7OvE z6=u@D3Ki^lFn;ilYsp0t+E@<7sFjf;dg z5r?B3kVQ4;JaU%Qs#Goo8@Ykx}r zR{u*sXMt=h1e~xkUuHbxnepd2-Pjy&{{C}3qouV9{UgL50yMse98tc|V{#FdG37Bu zUv)yiu$_{L$q-#9be+=Al=4wHbe@fnz(VLGi-5cXC~fDtP_k5=X{~Y+)+~6#3jY}L z3F7wvw?to%IgdgksEM>!5#0#MLqC0XyU3a zjkqz@5937POr=fxzF_OLu$AwWbHdq*zwe9v$-#l}v(`497fS28^5TwD!B*UJ?Zv?3 zq?z~EW;@Ko;0=xY6~H)JiPw`SBC`<137|m6r7@eZojep?b`13}e;fw2n>WVy+L@}r eVtD<>`x+Q}MVi*o&f(&{KWszyDPSYsivI%j8Uzgh diff --git a/tools/blender/mitsuba/ui/materials/main.pyc b/tools/blender/mitsuba/ui/materials/main.pyc deleted file mode 100644 index aa590c06bee3d5de80e370611d0b8c11e11ce41e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1479 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 KE%{6So%X-KbCB!+ diff --git a/tools/blender/mitsuba/ui/textures/checkerboard.pyc b/tools/blender/mitsuba/ui/textures/checkerboard.pyc deleted file mode 100644 index e087f3a726357b7183b7fd861f3f4b3baf78e9f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 969 zcmcgqO;6k~5S@H13#+ZDL?t+I*b~ZywS>eCp_WzEOBayXQ!bVhTdb+`p*W+mh#Oq_ ziS)O0oGg%5V&R09d7gO2GtbXH8-5!6d3gMpLTB{J|4L?$2qb+T7=XC-eJ~zmKEytF zKV$&+v>bx+K>=I^a23K`2yxGCx`=vIK$F2~|wRqZFJjbu8vwJ?Smo#XWZI~X4xGMiSqtSu8#7P)0hNf~OkRNAtx zjN@|A@I(FlBD!Hm-L4S~yfuF#Sd|*NGz`pl4<#!)7*(GLyP8Z+ti+Uh?XP@i2^)hkZ& z9X{LpY`>|g&Gc#y*GI1sU*1Ov+Ms`me21!4k9=o{h0HZ#+XiA@Dzg69`!b7GX4C}h_;+d>kt$EgBA$c61Qcp#l0?m zX)7|T3#IGRjEq&n6t0oh3Tp~_7>UtvJQi+Vo2qd_DpeLvtfUiU#7bo+))Ee@C4FHI zGB0mn?et~4Erx!u74Alx0}~%a5ktoL4tQO$4#^CkSvNj7@-MSlR@$}JwJcb{jYX}+ W`F{%SaDivtO`rSjT+A>CKZQ@T{Q(dF diff --git a/tools/blender/mitsuba/ui/textures/main.pyc b/tools/blender/mitsuba/ui/textures/main.pyc deleted file mode 100644 index d5190c99d251b19e78f2e353fa777cdd6561f467..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1897 zcmcgtU5ndB6upu^cI9lEKpR5SQbcJ%1hRrao*GKGVF?Sh9kBPIei>wGvL0tF3C&C# zm(A0bUv zq={y&9g+@c71AuE;AzpJzcJk<8ImMgc4*n9i!RN2{Lv)oA%Vh6qa;AsOp zLGT_AC&@mtxbM&mqG`Kid|W77HSy_5{46V_9z^ao{KFPlyp1xBdDxv)HvW@PbN z*+py@GR~`}k-3excw)P$CBE|vUjfRLn_QOC^r|`K zy?U5FJbd&hHF=|IYtpPJs?wxe5>uzr?OrgKsq(PixGu8SmDWF^H$0$T6kMU1OTwrO zc=TL?P81x!h%P8-DF@93l+q#FrDczneOm58AVGj~EcaP3@IfR+pM`W0gaN+3#<$Nt z5s@7rH!W{)A)&JXN}MDE02n!DJJaf{boXElPo|U8-;cy_D(XfWY0XaMertZe9Jl4* zunpS92r%T}B_FI%1Yft=csx8A`5Lo1KAldJ1CHYQ>TFXv>x&y~LKIaF`wF#p;V6`;Q9+1>Nwynt)>N)G?go6Go0YT+WZIpJ ze`)r2N4u1+@=WhoyoTNX`ONag=+p-d^HJuld5#C2e%S9som=~!ypO^X7VrmwevQHI sTsU>OyBj|HmvKqatZjdhU%LvN?dA0)WX2_a+!p^=-jTn>4#L~vd*b%j@Bjb+ diff --git a/tools/blender/mitsuba/ui/textures/mapping.pyc b/tools/blender/mitsuba/ui/textures/mapping.pyc deleted file mode 100644 index 6bb380459f2ed43b87e2869a54752f63bf992e31..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 961 zcmcgq%}(4f5T5+-(`75Fg2a_$4rMJNaYI!pLcOqn1nnWJWI3_5Yw9GHoe?bRt$nDz z4G+LeFiv(UZMAAowc;=1neoi{n>io89Q=5=KP}5ISsk&iJ*!h zk0JUX6ZlT*6jTff;5vcp6uza9uY^mN*$M^FwBam^2yvUB(yhlr@K)Wn=oBx6S7oq< z75)kB_q6>Rg0BE46giJVA(WHCO2P!D3FHaL7chw+PeHCxx&d9l*97A}$L_~ZyJKOk zt|u65M!I=c9MZ9zs`86+MI)TV4N^>;mJ5eK!b098`M0uh>sKAG`aEY@Oa}bR?at|+ zJeTF^sKs^4#d$3%#d-Ha&Z|bE3CCb73^$@srn8N&F8T5K_~PgacgQBSw?@pkb&XZ7 zo$-ll(E7enfi2EWQwrlb&*%gl;AfK9w5b%kQT3fNtT3vU%Iy`Xjbys^tuTfeUE}RO z+aDbqFkd=tThD})P3_s7NQRot`6XRX-pJ0n_Mv{7j=3ci>{kf}(Q3SzEb+>M2Zs46 zDAKbIjZlvk$(9ZwhG}hQR{2i45H%4Ors>aeeiHJZSXrks4*S-=4e#