BsRenderable.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663
  1. //********************************** Banshee Engine (www.banshee3d.com) **************************************************//
  2. //**************** Copyright (c) 2016 Marko Pintera ([email protected]). All rights reserved. **********************//
  3. #include "Renderer/BsRenderable.h"
  4. #include "Private/RTTI/BsRenderableRTTI.h"
  5. #include "Scene/BsSceneObject.h"
  6. #include "Mesh/BsMesh.h"
  7. #include "Material/BsMaterial.h"
  8. #include "Math/BsBounds.h"
  9. #include "Renderer/BsRenderer.h"
  10. #include "Animation/BsAnimation.h"
  11. #include "Animation/BsMorphShapes.h"
  12. #include "RenderAPI/BsGpuBuffer.h"
  13. #include "Animation/BsAnimationManager.h"
  14. #include "Scene/BsSceneManager.h"
  15. namespace bs
  16. {
  17. template<class T>
  18. bool isMeshValid(const T& mesh) { return false; }
  19. template<>
  20. bool isMeshValid(const HMesh& mesh) { return mesh.isLoaded(); }
  21. template<>
  22. bool isMeshValid(const SPtr<ct::Mesh>& mesh) { return mesh != nullptr; }
  23. template<bool Core>
  24. TRenderable<Core>::TRenderable()
  25. : mLayer(1), mUseOverrideBounds(false), mTfrmMatrix(BsIdentity), mTfrmMatrixNoScale(BsIdentity)
  26. , mAnimType(RenderableAnimType::None)
  27. {
  28. mMaterials.resize(1);
  29. }
  30. template<bool Core>
  31. TRenderable<Core>::~TRenderable()
  32. {
  33. }
  34. template <bool Core>
  35. void TRenderable<Core>::setTransform(const Transform& transform)
  36. {
  37. if (mMobility != ObjectMobility::Movable)
  38. return;
  39. mTransform = transform;
  40. mTfrmMatrix = transform.getMatrix();
  41. mTfrmMatrixNoScale = Matrix4::TRS(transform.getPosition(), transform.getRotation(), Vector3::ONE);
  42. _markCoreDirty(ActorDirtyFlag::Transform);
  43. }
  44. template<bool Core>
  45. void TRenderable<Core>::setMesh(const MeshType& mesh)
  46. {
  47. mMesh = mesh;
  48. int numSubMeshes = 0;
  49. if (isMeshValid(mesh))
  50. numSubMeshes = mesh->getProperties().getNumSubMeshes();
  51. mMaterials.resize(numSubMeshes);
  52. onMeshChanged();
  53. _markDependenciesDirty();
  54. _markResourcesDirty();
  55. _markCoreDirty();
  56. }
  57. template<bool Core>
  58. void TRenderable<Core>::setMaterial(UINT32 idx, const MaterialType& material)
  59. {
  60. if (idx >= (UINT32)mMaterials.size())
  61. return;
  62. mMaterials[idx] = material;
  63. _markDependenciesDirty();
  64. _markResourcesDirty();
  65. _markCoreDirty();
  66. }
  67. template<bool Core>
  68. void TRenderable<Core>::setMaterials(const Vector<MaterialType>& materials)
  69. {
  70. UINT32 numMaterials = (UINT32)mMaterials.size();
  71. UINT32 min = std::min(numMaterials, (UINT32)materials.size());
  72. for (UINT32 i = 0; i < min; i++)
  73. mMaterials[i] = materials[i];
  74. for (UINT32 i = min; i < numMaterials; i++)
  75. mMaterials[i] = nullptr;
  76. _markDependenciesDirty();
  77. _markResourcesDirty();
  78. _markCoreDirty();
  79. }
  80. template<bool Core>
  81. void TRenderable<Core>::setMaterial(const MaterialType& material)
  82. {
  83. setMaterial(0, material);
  84. }
  85. template<bool Core>
  86. void TRenderable<Core>::setLayer(UINT64 layer)
  87. {
  88. bool isPow2 = layer && !((layer - 1) & layer);
  89. if (!isPow2)
  90. {
  91. LOGWRN("Invalid layer provided. Only one layer bit may be set. Ignoring.");
  92. return;
  93. }
  94. mLayer = layer;
  95. _markCoreDirty();
  96. }
  97. template<bool Core>
  98. void TRenderable<Core>::setOverrideBounds(const AABox& bounds)
  99. {
  100. mOverrideBounds = bounds;
  101. if(mUseOverrideBounds)
  102. _markCoreDirty();
  103. }
  104. template<bool Core>
  105. void TRenderable<Core>::setUseOverrideBounds(bool enable)
  106. {
  107. if (mUseOverrideBounds == enable)
  108. return;
  109. mUseOverrideBounds = enable;
  110. _markCoreDirty();
  111. }
  112. template class TRenderable < false >;
  113. template class TRenderable < true >;
  114. void Renderable::initialize()
  115. {
  116. CoreObject::initialize();
  117. // Since we don't pass any information along to the core thread object on its construction, make sure the data
  118. // sync executes
  119. _markCoreDirty();
  120. }
  121. void Renderable::setAnimation(const SPtr<Animation>& animation)
  122. {
  123. mAnimation = animation;
  124. refreshAnimation();
  125. _markCoreDirty();
  126. }
  127. Bounds Renderable::getBounds() const
  128. {
  129. if(mUseOverrideBounds)
  130. {
  131. Sphere sphere(mOverrideBounds.getCenter(), mOverrideBounds.getRadius());
  132. Bounds bounds(mOverrideBounds, sphere);
  133. bounds.transformAffine(mTfrmMatrix);
  134. return bounds;
  135. }
  136. HMesh mesh = getMesh();
  137. if (!mesh.isLoaded())
  138. {
  139. const Transform& tfrm = getTransform();
  140. AABox box(tfrm.getPosition(), tfrm.getPosition());
  141. Sphere sphere(tfrm.getPosition(), 0.0f);
  142. return Bounds(box, sphere);
  143. }
  144. else
  145. {
  146. Bounds bounds = mesh->getProperties().getBounds();
  147. bounds.transformAffine(mTfrmMatrix);
  148. return bounds;
  149. }
  150. }
  151. SPtr<ct::Renderable> Renderable::getCore() const
  152. {
  153. return std::static_pointer_cast<ct::Renderable>(mCoreSpecific);
  154. }
  155. SPtr<ct::CoreObject> Renderable::createCore() const
  156. {
  157. ct::Renderable* handler = new (bs_alloc<ct::Renderable>()) ct::Renderable();
  158. SPtr<ct::Renderable> handlerPtr = bs_shared_ptr<ct::Renderable>(handler);
  159. handlerPtr->_setThisPtr(handlerPtr);
  160. return handlerPtr;
  161. }
  162. void Renderable::onMeshChanged()
  163. {
  164. refreshAnimation();
  165. }
  166. void Renderable::refreshAnimation()
  167. {
  168. if (mAnimation == nullptr)
  169. {
  170. mAnimType = RenderableAnimType::None;
  171. return;
  172. }
  173. if (mMesh.isLoaded(false))
  174. {
  175. SPtr<Skeleton> skeleton = mMesh->getSkeleton();
  176. SPtr<MorphShapes> morphShapes = mMesh->getMorphShapes();
  177. if (skeleton != nullptr && morphShapes != nullptr)
  178. mAnimType = RenderableAnimType::SkinnedMorph;
  179. else if (skeleton != nullptr)
  180. mAnimType = RenderableAnimType::Skinned;
  181. else if (morphShapes != nullptr)
  182. mAnimType = RenderableAnimType::Morph;
  183. else
  184. mAnimType = RenderableAnimType::None;
  185. mAnimation->setSkeleton(mMesh->getSkeleton());
  186. mAnimation->setMorphShapes(mMesh->getMorphShapes());
  187. }
  188. else
  189. {
  190. mAnimType = RenderableAnimType::None;
  191. mAnimation->setSkeleton(nullptr);
  192. mAnimation->setMorphShapes(nullptr);
  193. }
  194. }
  195. void Renderable::_updateState(const SceneObject& so, bool force)
  196. {
  197. UINT32 curHash = so.getTransformHash();
  198. if (curHash != mHash || force)
  199. {
  200. // If skinned animation, don't include own transform since that will be handled by root bone animation
  201. bool ignoreOwnTransform;
  202. if (mAnimType == RenderableAnimType::Skinned || mAnimType == RenderableAnimType::SkinnedMorph)
  203. ignoreOwnTransform = mAnimation->_getAnimatesRoot();
  204. else
  205. ignoreOwnTransform = false;
  206. if (ignoreOwnTransform)
  207. {
  208. // Note: Technically we're checking child's hash but using parent's transform. Ideally we check the parent's
  209. // hash to reduce the number of required updates.
  210. HSceneObject parentSO = so.getParent();
  211. if (parentSO != nullptr)
  212. setTransform(parentSO->getTransform());
  213. else
  214. setTransform(Transform());
  215. }
  216. else
  217. setTransform(so.getTransform());
  218. mHash = curHash;
  219. }
  220. // Hash now matches so transform won't be applied twice, so we can just call base class version
  221. SceneActor::_updateState(so, force);
  222. }
  223. void Renderable::_markCoreDirty(ActorDirtyFlag flag)
  224. {
  225. markCoreDirty((UINT32)flag);
  226. }
  227. void Renderable::_markDependenciesDirty()
  228. {
  229. markDependenciesDirty();
  230. }
  231. void Renderable::_markResourcesDirty()
  232. {
  233. markListenerResourcesDirty();
  234. }
  235. CoreSyncData Renderable::syncToCore(FrameAlloc* allocator)
  236. {
  237. UINT32 numMaterials = (UINT32)mMaterials.size();
  238. UINT64 animationId;
  239. if (mAnimation != nullptr)
  240. animationId = mAnimation->_getId();
  241. else
  242. animationId = (UINT64)-1;
  243. UINT32 size =
  244. getActorSyncDataSize() +
  245. rttiGetElemSize(mLayer) +
  246. rttiGetElemSize(mOverrideBounds) +
  247. rttiGetElemSize(mUseOverrideBounds) +
  248. rttiGetElemSize(numMaterials) +
  249. rttiGetElemSize(mTfrmMatrix) +
  250. rttiGetElemSize(mTfrmMatrixNoScale) +
  251. rttiGetElemSize(animationId) +
  252. rttiGetElemSize(mAnimType) +
  253. rttiGetElemSize(getCoreDirtyFlags()) +
  254. sizeof(SPtr<ct::Mesh>) +
  255. numMaterials * sizeof(SPtr<ct::Material>);
  256. UINT8* data = allocator->alloc(size);
  257. char* dataPtr = (char*)data;
  258. dataPtr = syncActorTo(dataPtr);
  259. dataPtr = rttiWriteElem(mLayer, dataPtr);
  260. dataPtr = rttiWriteElem(mOverrideBounds, dataPtr);
  261. dataPtr = rttiWriteElem(mUseOverrideBounds, dataPtr);
  262. dataPtr = rttiWriteElem(numMaterials, dataPtr);
  263. dataPtr = rttiWriteElem(mTfrmMatrix, dataPtr);
  264. dataPtr = rttiWriteElem(mTfrmMatrixNoScale, dataPtr);
  265. dataPtr = rttiWriteElem(animationId, dataPtr);
  266. dataPtr = rttiWriteElem(mAnimType, dataPtr);
  267. dataPtr = rttiWriteElem(getCoreDirtyFlags(), dataPtr);
  268. SPtr<ct::Mesh>* mesh = new (dataPtr) SPtr<ct::Mesh>();
  269. if (mMesh.isLoaded())
  270. *mesh = mMesh->getCore();
  271. dataPtr += sizeof(SPtr<ct::Mesh>);
  272. for (UINT32 i = 0; i < numMaterials; i++)
  273. {
  274. SPtr<ct::Material>* material = new (dataPtr)SPtr<ct::Material>();
  275. if (mMaterials[i].isLoaded())
  276. *material = mMaterials[i]->getCore();
  277. dataPtr += sizeof(SPtr<ct::Material>);
  278. }
  279. return CoreSyncData(data, size);
  280. }
  281. void Renderable::getCoreDependencies(Vector<CoreObject*>& dependencies)
  282. {
  283. if (mMesh.isLoaded())
  284. dependencies.push_back(mMesh.get());
  285. for (auto& material : mMaterials)
  286. {
  287. if (material.isLoaded())
  288. dependencies.push_back(material.get());
  289. }
  290. }
  291. void Renderable::getListenerResources(Vector<HResource>& resources)
  292. {
  293. if (mMesh != nullptr)
  294. resources.push_back(mMesh);
  295. for (auto& material : mMaterials)
  296. {
  297. if (material != nullptr)
  298. resources.push_back(material);
  299. }
  300. }
  301. void Renderable::notifyResourceLoaded(const HResource& resource)
  302. {
  303. if (resource == mMesh)
  304. onMeshChanged();
  305. markDependenciesDirty();
  306. markCoreDirty();
  307. }
  308. void Renderable::notifyResourceChanged(const HResource& resource)
  309. {
  310. if(resource == mMesh)
  311. onMeshChanged();
  312. markDependenciesDirty();
  313. markCoreDirty();
  314. }
  315. SPtr<Renderable> Renderable::create()
  316. {
  317. SPtr<Renderable> handlerPtr = createEmpty();
  318. handlerPtr->initialize();
  319. return handlerPtr;
  320. }
  321. SPtr<Renderable> Renderable::createEmpty()
  322. {
  323. Renderable* handler = new (bs_alloc<Renderable>()) Renderable();
  324. SPtr<Renderable> handlerPtr = bs_core_ptr<Renderable>(handler);
  325. handlerPtr->_setThisPtr(handlerPtr);
  326. return handlerPtr;
  327. }
  328. RTTITypeBase* Renderable::getRTTIStatic()
  329. {
  330. return RenderableRTTI::instance();
  331. }
  332. RTTITypeBase* Renderable::getRTTI() const
  333. {
  334. return Renderable::getRTTIStatic();
  335. }
  336. namespace ct
  337. {
  338. Renderable::Renderable()
  339. :mRendererId(0), mAnimationId((UINT64)-1), mMorphShapeVersion(0)
  340. {
  341. }
  342. Renderable::~Renderable()
  343. {
  344. if (mActive)
  345. gRenderer()->notifyRenderableRemoved(this);
  346. }
  347. void Renderable::initialize()
  348. {
  349. gRenderer()->notifyRenderableAdded(this);
  350. CoreObject::initialize();
  351. }
  352. Bounds Renderable::getBounds() const
  353. {
  354. if (mUseOverrideBounds)
  355. {
  356. Sphere sphere(mOverrideBounds.getCenter(), mOverrideBounds.getRadius());
  357. Bounds bounds(mOverrideBounds, sphere);
  358. bounds.transformAffine(mTfrmMatrix);
  359. return bounds;
  360. }
  361. SPtr<Mesh> mesh = getMesh();
  362. if (mesh == nullptr)
  363. {
  364. const Transform& tfrm = getTransform();
  365. AABox box(tfrm.getPosition(), tfrm.getPosition());
  366. Sphere sphere(tfrm.getPosition(), 0.0f);
  367. return Bounds(box, sphere);
  368. }
  369. else
  370. {
  371. Bounds bounds = mesh->getProperties().getBounds();
  372. bounds.transformAffine(mTfrmMatrix);
  373. return bounds;
  374. }
  375. }
  376. void Renderable::createAnimationBuffers()
  377. {
  378. if (mAnimType == RenderableAnimType::Skinned || mAnimType == RenderableAnimType::SkinnedMorph)
  379. {
  380. SPtr<Skeleton> skeleton = mMesh->getSkeleton();
  381. UINT32 numBones = skeleton != nullptr ? skeleton->getNumBones() : 0;
  382. if (numBones > 0)
  383. {
  384. GPU_BUFFER_DESC desc;
  385. desc.elementCount = numBones * 3;
  386. desc.elementSize = 0;
  387. desc.type = GBT_STANDARD;
  388. desc.format = BF_32X4F;
  389. desc.usage = GBU_DYNAMIC;
  390. SPtr<GpuBuffer> buffer = GpuBuffer::create(desc);
  391. UINT8* dest = (UINT8*)buffer->lock(0, numBones * 3 * sizeof(Vector4), GBL_WRITE_ONLY_DISCARD);
  392. // Initialize bone transforms to identity, so the object renders properly even if no animation is animating it
  393. for (UINT32 i = 0; i < numBones; i++)
  394. {
  395. memcpy(dest, &Matrix4::IDENTITY, 12 * sizeof(float)); // Assuming row-major format
  396. dest += 12 * sizeof(float);
  397. }
  398. buffer->unlock();
  399. mBoneMatrixBuffer = buffer;
  400. }
  401. else
  402. mBoneMatrixBuffer = nullptr;
  403. }
  404. else
  405. mBoneMatrixBuffer = nullptr;
  406. if (mAnimType == RenderableAnimType::Morph || mAnimType == RenderableAnimType::SkinnedMorph)
  407. {
  408. SPtr<MorphShapes> morphShapes = mMesh->getMorphShapes();
  409. UINT32 vertexSize = sizeof(Vector3) + sizeof(UINT32);
  410. UINT32 numVertices = morphShapes->getNumVertices();
  411. VERTEX_BUFFER_DESC desc;
  412. desc.vertexSize = vertexSize;
  413. desc.numVerts = numVertices;
  414. desc.usage = GBU_DYNAMIC;
  415. SPtr<VertexBuffer> vertexBuffer = VertexBuffer::create(desc);
  416. UINT32 totalSize = vertexSize * numVertices;
  417. UINT8* dest = (UINT8*)vertexBuffer->lock(0, totalSize, GBL_WRITE_ONLY_DISCARD);
  418. memset(dest, 0, totalSize);
  419. vertexBuffer->unlock();
  420. mMorphShapeBuffer = vertexBuffer;
  421. }
  422. else
  423. mMorphShapeBuffer = nullptr;
  424. mMorphShapeVersion = 0;
  425. }
  426. void Renderable::updateAnimationBuffers(const EvaluatedAnimationData& animData)
  427. {
  428. if (mAnimationId == (UINT64)-1)
  429. return;
  430. const EvaluatedAnimationData::AnimInfo* animInfo = nullptr;
  431. auto iterFind = animData.infos.find(mAnimationId);
  432. if (iterFind != animData.infos.end())
  433. animInfo = &iterFind->second;
  434. if (animInfo == nullptr)
  435. return;
  436. if (mAnimType == RenderableAnimType::Skinned || mAnimType == RenderableAnimType::SkinnedMorph)
  437. {
  438. const EvaluatedAnimationData::PoseInfo& poseInfo = animInfo->poseInfo;
  439. // Note: If multiple elements are using the same animation (not possible atm), this buffer should be shared by
  440. // all such elements
  441. UINT8* dest = (UINT8*)mBoneMatrixBuffer->lock(0, poseInfo.numBones * 3 * sizeof(Vector4), GBL_WRITE_ONLY_DISCARD);
  442. for (UINT32 j = 0; j < poseInfo.numBones; j++)
  443. {
  444. const Matrix4& transform = animData.transforms[poseInfo.startIdx + j];
  445. memcpy(dest, &transform, 12 * sizeof(float)); // Assuming row-major format
  446. dest += 12 * sizeof(float);
  447. }
  448. mBoneMatrixBuffer->unlock();
  449. }
  450. if (mAnimType == RenderableAnimType::Morph || mAnimType == RenderableAnimType::SkinnedMorph)
  451. {
  452. if (mMorphShapeVersion != animInfo->morphShapeInfo.version)
  453. {
  454. SPtr<MeshData> meshData = animInfo->morphShapeInfo.meshData;
  455. UINT32 bufferSize = meshData->getSize();
  456. UINT8* data = meshData->getData();
  457. mMorphShapeBuffer->writeData(0, bufferSize, data, BWT_DISCARD);
  458. mMorphShapeVersion = animInfo->morphShapeInfo.version;
  459. }
  460. }
  461. }
  462. void Renderable::syncToCore(const CoreSyncData& data)
  463. {
  464. char* dataPtr = (char*)data.getBuffer();
  465. mMaterials.clear();
  466. UINT32 numMaterials = 0;
  467. UINT32 dirtyFlags = 0;
  468. bool oldIsActive = mActive;
  469. dataPtr = syncActorFrom(dataPtr);
  470. dataPtr = rttiReadElem(mLayer, dataPtr);
  471. dataPtr = rttiReadElem(mOverrideBounds, dataPtr);
  472. dataPtr = rttiReadElem(mUseOverrideBounds, dataPtr);
  473. dataPtr = rttiReadElem(numMaterials, dataPtr);
  474. dataPtr = rttiReadElem(mTfrmMatrix, dataPtr);
  475. dataPtr = rttiReadElem(mTfrmMatrixNoScale, dataPtr);
  476. dataPtr = rttiReadElem(mAnimationId, dataPtr);
  477. dataPtr = rttiReadElem(mAnimType, dataPtr);
  478. dataPtr = rttiReadElem(dirtyFlags, dataPtr);
  479. SPtr<Mesh>* mesh = (SPtr<Mesh>*)dataPtr;
  480. mMesh = *mesh;
  481. mesh->~SPtr<Mesh>();
  482. dataPtr += sizeof(SPtr<Mesh>);
  483. for (UINT32 i = 0; i < numMaterials; i++)
  484. {
  485. SPtr<Material>* material = (SPtr<Material>*)dataPtr;
  486. mMaterials.push_back(*material);
  487. material->~SPtr<Material>();
  488. dataPtr += sizeof(SPtr<Material>);
  489. }
  490. UINT32 updateEverythingFlag = (UINT32)ActorDirtyFlag::Everything
  491. | (UINT32)ActorDirtyFlag::Active
  492. | (UINT32)ActorDirtyFlag::Dependency;
  493. if((dirtyFlags & updateEverythingFlag) != 0)
  494. {
  495. createAnimationBuffers();
  496. // Create special vertex declaration if using morph shapes
  497. if (mAnimType == RenderableAnimType::Morph || mAnimType == RenderableAnimType::SkinnedMorph)
  498. {
  499. SPtr<VertexDataDesc> vertexDesc = VertexDataDesc::create();
  500. *vertexDesc = * mMesh->getVertexDesc();
  501. vertexDesc->addVertElem(VET_FLOAT3, VES_POSITION, 1, 1);
  502. vertexDesc->addVertElem(VET_UBYTE4_NORM, VES_NORMAL, 1, 1);
  503. mMorphVertexDeclaration = VertexDeclaration::create(vertexDesc);
  504. }
  505. else
  506. mMorphVertexDeclaration = nullptr;
  507. if (oldIsActive != mActive)
  508. {
  509. if (mActive)
  510. gRenderer()->notifyRenderableAdded(this);
  511. else
  512. gRenderer()->notifyRenderableRemoved(this);
  513. }
  514. else
  515. {
  516. gRenderer()->notifyRenderableRemoved(this);
  517. gRenderer()->notifyRenderableAdded(this);
  518. }
  519. }
  520. else if((dirtyFlags & (UINT32)ActorDirtyFlag::Mobility) != 0)
  521. {
  522. gRenderer()->notifyRenderableRemoved(this);
  523. gRenderer()->notifyRenderableAdded(this);
  524. }
  525. else if ((dirtyFlags & (UINT32)ActorDirtyFlag::Transform) != 0)
  526. {
  527. if (mActive)
  528. gRenderer()->notifyRenderableUpdated(this);
  529. }
  530. }
  531. }
  532. }