Fix infinite loop detection when placing players randomly on the newer random map...
[0ad.git] / source / graphics / Model.cpp
blobee7303c891e1b2c61303db3e27022ca96db8b3e6
1 /* Copyright (C) 2016 Wildfire Games.
2 * This file is part of 0 A.D.
4 * 0 A.D. is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 2 of the License, or
7 * (at your option) any later version.
9 * 0 A.D. is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
19 * Mesh object with texture and skinning information
22 #include "precompiled.h"
24 #include "Model.h"
26 #include "Decal.h"
27 #include "ModelDef.h"
28 #include "maths/Quaternion.h"
29 #include "maths/BoundingBoxAligned.h"
30 #include "SkeletonAnim.h"
31 #include "SkeletonAnimDef.h"
32 #include "SkeletonAnimManager.h"
33 #include "MeshManager.h"
34 #include "ObjectEntry.h"
35 #include "lib/res/graphics/ogl_tex.h"
36 #include "lib/res/h_mgr.h"
37 #include "lib/sysdep/rtl.h"
38 #include "ps/Profile.h"
39 #include "ps/CLogger.h"
40 #include "renderer/Renderer.h"
41 #include "simulation2/Simulation2.h"
42 #include "simulation2/components/ICmpTerrain.h"
45 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
46 // Constructor
47 CModel::CModel(CSkeletonAnimManager& skeletonAnimManager, CSimulation2& simulation)
48 : m_Flags(0), m_Anim(NULL), m_AnimTime(0), m_Simulation(simulation),
49 m_BoneMatrices(NULL), m_AmmoPropPoint(NULL), m_AmmoLoadedProp(0),
50 m_SkeletonAnimManager(skeletonAnimManager)
54 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
55 // Destructor
56 CModel::~CModel()
58 ReleaseData();
61 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
62 // ReleaseData: delete anything allocated by the model
63 void CModel::ReleaseData()
65 rtl_FreeAligned(m_BoneMatrices);
67 for (size_t i = 0; i < m_Props.size(); ++i)
68 delete m_Props[i].m_Model;
69 m_Props.clear();
71 m_pModelDef = CModelDefPtr();
74 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
75 // InitModel: setup model from given geometry
76 bool CModel::InitModel(const CModelDefPtr& modeldef)
78 // clean up any existing data first
79 ReleaseData();
81 m_pModelDef = modeldef;
83 size_t numBones = modeldef->GetNumBones();
84 if (numBones != 0)
86 size_t numBlends = modeldef->GetNumBlends();
88 // allocate matrices for bone transformations
89 // (one extra matrix is used for the special case of bind-shape relative weighting)
90 m_BoneMatrices = (CMatrix3D*)rtl_AllocateAligned(sizeof(CMatrix3D) * (numBones + 1 + numBlends), 16);
91 for (size_t i = 0; i < numBones + 1 + numBlends; ++i)
93 m_BoneMatrices[i].SetIdentity();
97 m_PositionValid = true;
99 return true;
103 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
104 // CalcBound: calculate the world space bounds of this model
105 void CModel::CalcBounds()
107 // Need to calculate the object bounds first, if that hasn't already been done
108 if (! (m_Anim && m_Anim->m_AnimDef))
110 if (m_ObjectBounds.IsEmpty())
111 CalcStaticObjectBounds();
113 else
115 if (m_Anim->m_ObjectBounds.IsEmpty())
116 CalcAnimatedObjectBounds(m_Anim->m_AnimDef, m_Anim->m_ObjectBounds);
117 ENSURE(! m_Anim->m_ObjectBounds.IsEmpty()); // (if this happens, it'll be recalculating the bounds every time)
118 m_ObjectBounds = m_Anim->m_ObjectBounds;
121 // Ensure the transform is set correctly before we use it
122 ValidatePosition();
124 // Now transform the object-space bounds to world-space bounds
125 m_ObjectBounds.Transform(GetTransform(), m_WorldBounds);
128 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
129 // CalcObjectBounds: calculate object space bounds of this model, based solely on vertex positions
130 void CModel::CalcStaticObjectBounds()
132 m_ObjectBounds.SetEmpty();
134 size_t numverts=m_pModelDef->GetNumVertices();
135 SModelVertex* verts=m_pModelDef->GetVertices();
137 for (size_t i=0;i<numverts;i++) {
138 m_ObjectBounds+=verts[i].m_Coords;
142 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
143 // CalcAnimatedObjectBound: calculate bounds encompassing all vertex positions for given animation
144 void CModel::CalcAnimatedObjectBounds(CSkeletonAnimDef* anim, CBoundingBoxAligned& result)
146 result.SetEmpty();
148 // Set the current animation on which to perform calculations (if it's necessary)
149 if (anim != m_Anim->m_AnimDef)
151 CSkeletonAnim dummyanim;
152 dummyanim.m_AnimDef=anim;
153 if (!SetAnimation(&dummyanim)) return;
156 size_t numverts=m_pModelDef->GetNumVertices();
157 SModelVertex* verts=m_pModelDef->GetVertices();
159 // Remove any transformations, so that we calculate the bounding box
160 // at the origin. The box is later re-transformed onto the object, without
161 // having to recalculate the size of the box.
162 CMatrix3D transform, oldtransform = GetTransform();
163 CModelAbstract* oldparent = m_Parent;
165 m_Parent = 0;
166 transform.SetIdentity();
167 CRenderableObject::SetTransform(transform);
169 // Following seems to stomp over the current animation time - which, unsurprisingly,
170 // introduces artefacts in the currently playing animation. Save it here and restore it
171 // at the end.
172 float AnimTime = m_AnimTime;
174 // iterate through every frame of the animation
175 for (size_t j=0;j<anim->GetNumFrames();j++) {
176 m_PositionValid = false;
177 ValidatePosition();
179 // extend bounds by vertex positions at the frame
180 for (size_t i=0;i<numverts;i++)
182 result += CModelDef::SkinPoint(verts[i], GetAnimatedBoneMatrices());
184 // advance to next frame
185 m_AnimTime += anim->GetFrameTime();
188 m_PositionValid = false;
189 m_Parent = oldparent;
190 SetTransform(oldtransform);
191 m_AnimTime = AnimTime;
194 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
195 const CBoundingBoxAligned CModel::GetWorldBoundsRec()
197 CBoundingBoxAligned bounds = GetWorldBounds();
198 for (size_t i = 0; i < m_Props.size(); ++i)
199 bounds += m_Props[i].m_Model->GetWorldBoundsRec();
200 return bounds;
203 const CBoundingBoxAligned CModel::GetObjectSelectionBoundsRec()
205 CBoundingBoxAligned objBounds = GetObjectBounds(); // updates the (children-not-included) object-space bounds if necessary
207 // now extend these bounds to include the props' selection bounds (if any)
208 for (size_t i = 0; i < m_Props.size(); ++i)
210 const Prop& prop = m_Props[i];
211 if (prop.m_Hidden || !prop.m_Selectable)
212 continue; // prop is hidden from rendering, so it also shouldn't be used for selection
214 CBoundingBoxAligned propSelectionBounds = prop.m_Model->GetObjectSelectionBoundsRec();
215 if (propSelectionBounds.IsEmpty())
216 continue; // submodel does not wish to participate in selection box, exclude it
218 // We have the prop's bounds in its own object-space; now we need to transform them so they can be properly added
219 // to the bounds in our object-space. For that, we need the transform of the prop attachment point.
221 // We have the prop point information; however, it's not trivial to compute its exact location in our object-space
222 // since it may or may not be attached to a bone (see SPropPoint), which in turn may or may not be in the middle of
223 // an animation. The bone matrices might be of interest, but they're really only meant to be used for the animation
224 // system and are quite opaque to use from the outside (see @ref ValidatePosition).
226 // However, a nice side effect of ValidatePosition is that it also computes the absolute world-space transform of
227 // our props and sets it on their respective models. In particular, @ref ValidatePosition will compute the prop's
228 // world-space transform as either
230 // T' = T x B x O
231 // or
232 // T' = T x O
234 // where T' is the prop's world-space transform, T is our world-space transform, O is the prop's local
235 // offset/rotation matrix, and B is an optional transformation matrix of the bone the prop is attached to
236 // (taking into account animation and everything).
238 // From this, it is clear that either O or B x O is the object-space transformation matrix of the prop. So,
239 // all we need to do is apply our own inverse world-transform T^(-1) to T' to get our desired result. Luckily,
240 // this is precomputed upon setting the transform matrix (see @ref SetTransform), so it is free to fetch.
242 CMatrix3D propObjectTransform = prop.m_Model->GetTransform(); // T'
243 propObjectTransform.Concatenate(GetInvTransform()); // T^(-1) x T'
245 // Transform the prop's bounds into our object coordinate space
246 CBoundingBoxAligned transformedPropSelectionBounds;
247 propSelectionBounds.Transform(propObjectTransform, transformedPropSelectionBounds);
249 objBounds += transformedPropSelectionBounds;
252 return objBounds;
255 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
256 // BuildAnimation: load raw animation frame animation from given file, and build a
257 // animation specific to this model
258 CSkeletonAnim* CModel::BuildAnimation(const VfsPath& pathname, const CStr& name, const CStr& ID, int frequency, float speed, float actionpos, float actionpos2, float soundpos)
260 CSkeletonAnimDef* def = m_SkeletonAnimManager.GetAnimation(pathname);
261 if (!def)
262 return NULL;
264 CSkeletonAnim* anim = new CSkeletonAnim();
265 anim->m_Name = name;
266 anim->m_ID = ID;
267 anim->m_Frequency = frequency;
268 anim->m_AnimDef = def;
269 anim->m_Speed = speed;
271 if (actionpos == -1.f)
272 anim->m_ActionPos = -1.f;
273 else
274 anim->m_ActionPos = actionpos * anim->m_AnimDef->GetDuration();
276 if (actionpos2 == -1.f)
277 anim->m_ActionPos2 = -1.f;
278 else
279 anim->m_ActionPos2 = actionpos2 * anim->m_AnimDef->GetDuration();
281 if (soundpos == -1.f)
282 anim->m_SoundPos = -1.f;
283 else
284 anim->m_SoundPos = soundpos * anim->m_AnimDef->GetDuration();
286 anim->m_ObjectBounds.SetEmpty();
287 InvalidateBounds();
289 return anim;
292 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
293 // Update: update this model to the given time, in msec
294 void CModel::UpdateTo(float time)
296 // update animation time, but don't calculate bone matrices - do that (lazily) when
297 // something requests them; that saves some calculation work for offscreen models,
298 // and also assures the world space, inverted bone matrices (required for normal
299 // skinning) are up to date with respect to m_Transform
300 m_AnimTime = time;
302 // mark vertices as dirty
303 SetDirty(RENDERDATA_UPDATE_VERTICES);
305 // mark matrices as dirty
306 InvalidatePosition();
309 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
310 // InvalidatePosition
311 void CModel::InvalidatePosition()
313 m_PositionValid = false;
315 for (size_t i = 0; i < m_Props.size(); ++i)
316 m_Props[i].m_Model->InvalidatePosition();
319 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
320 // ValidatePosition: ensure that current transform and bone matrices are both uptodate
321 void CModel::ValidatePosition()
323 if (m_PositionValid)
325 ENSURE(!m_Parent || m_Parent->m_PositionValid);
326 return;
329 if (m_Parent && !m_Parent->m_PositionValid)
331 // Make sure we don't base our calculations on
332 // a parent animation state that is out of date.
333 m_Parent->ValidatePosition();
335 // Parent will recursively call our validation.
336 ENSURE(m_PositionValid);
337 return;
340 if (m_Anim && m_BoneMatrices)
342 // PROFILE( "generating bone matrices" );
344 ENSURE(m_pModelDef->GetNumBones() == m_Anim->m_AnimDef->GetNumKeys());
346 m_Anim->m_AnimDef->BuildBoneMatrices(m_AnimTime, m_BoneMatrices, !(m_Flags & MODELFLAG_NOLOOPANIMATION));
348 else if (m_BoneMatrices)
350 // Bones but no animation - probably a buggy actor forgot to set up the animation,
351 // so just render it in its bind pose
353 for (size_t i = 0; i < m_pModelDef->GetNumBones(); i++)
355 m_BoneMatrices[i].SetIdentity();
356 m_BoneMatrices[i].Rotate(m_pModelDef->GetBones()[i].m_Rotation);
357 m_BoneMatrices[i].Translate(m_pModelDef->GetBones()[i].m_Translation);
361 // For CPU skinning, we precompute as much as possible so that the only
362 // per-vertex work is a single matrix*vec multiplication.
363 // For GPU skinning, we try to minimise CPU work by doing most computation
364 // in the vertex shader instead.
365 // Using g_Renderer.m_Options to detect CPU vs GPU is a bit hacky,
366 // and this doesn't allow the setting to change at runtime, but there isn't
367 // an obvious cleaner way to determine what data needs to be computed,
368 // and GPU skinning is a rarely-used experimental feature anyway.
369 bool worldSpaceBoneMatrices = !g_Renderer.m_Options.m_GPUSkinning;
370 bool computeBlendMatrices = !g_Renderer.m_Options.m_GPUSkinning;
372 if (m_BoneMatrices && worldSpaceBoneMatrices)
374 // add world-space transformation to m_BoneMatrices
375 const CMatrix3D transform = GetTransform();
376 for (size_t i = 0; i < m_pModelDef->GetNumBones(); i++)
377 m_BoneMatrices[i].Concatenate(transform);
380 // our own position is now valid; now we can safely update our props' positions without fearing
381 // that doing so will cause a revalidation of this model (see recursion above).
382 m_PositionValid = true;
384 // re-position and validate all props
385 for (size_t j = 0; j < m_Props.size(); ++j)
387 const Prop& prop=m_Props[j];
389 CMatrix3D proptransform = prop.m_Point->m_Transform;
391 if (prop.m_Point->m_BoneIndex != 0xff)
393 CMatrix3D boneMatrix = m_BoneMatrices[prop.m_Point->m_BoneIndex];
394 if (!worldSpaceBoneMatrices)
395 boneMatrix.Concatenate(GetTransform());
396 proptransform.Concatenate(boneMatrix);
398 else
400 // not relative to any bone; just apply world-space transformation (i.e. relative to object-space origin)
401 proptransform.Concatenate(m_Transform);
404 // Adjust prop height to terrain level when needed
405 if (prop.m_MaxHeight != 0.f || prop.m_MinHeight != 0.f)
407 CVector3D propTranslation = proptransform.GetTranslation();
408 CVector3D objTranslation = m_Transform.GetTranslation();
410 CmpPtr<ICmpTerrain> cmpTerrain(m_Simulation, SYSTEM_ENTITY);
411 if (cmpTerrain)
413 float objTerrain = cmpTerrain->GetExactGroundLevel(objTranslation.X, objTranslation.Z);
414 float propTerrain = cmpTerrain->GetExactGroundLevel(propTranslation.X, propTranslation.Z);
415 float translateHeight = std::min(prop.m_MaxHeight,
416 std::max(prop.m_MinHeight, propTerrain - objTerrain));
417 CMatrix3D translate = CMatrix3D();
418 translate.SetTranslation(0.f, translateHeight, 0.f);
419 proptransform.Concatenate(translate);
423 prop.m_Model->SetTransform(proptransform);
424 prop.m_Model->ValidatePosition();
427 if (m_BoneMatrices)
429 for (size_t i = 0; i < m_pModelDef->GetNumBones(); i++)
431 m_BoneMatrices[i] = m_BoneMatrices[i] * m_pModelDef->GetInverseBindBoneMatrices()[i];
434 // Note: there is a special case of joint influence, in which the vertex
435 // is influenced by the bind-shape transform instead of a particular bone,
436 // which we indicate with the blending bone ID set to the total number
437 // of bones. But since we're skinning in world space, we use the model's
438 // world space transform and store that matrix in this special index.
439 // (see http://trac.wildfiregames.com/ticket/1012)
440 m_BoneMatrices[m_pModelDef->GetNumBones()] = m_Transform;
442 if (computeBlendMatrices)
443 m_pModelDef->BlendBoneMatrices(m_BoneMatrices);
448 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
449 // SetAnimation: set the given animation as the current animation on this model;
450 // return false on error, else true
451 bool CModel::SetAnimation(CSkeletonAnim* anim, bool once)
453 m_Anim = NULL; // in case something fails
455 if (anim)
457 m_Flags &= ~MODELFLAG_NOLOOPANIMATION;
459 if (once)
460 m_Flags |= MODELFLAG_NOLOOPANIMATION;
462 if (!m_BoneMatrices && anim->m_AnimDef)
464 // not boned, can't animate
465 return false;
468 if (m_BoneMatrices && !anim->m_AnimDef)
470 // boned, but animation isn't valid
471 // (e.g. the default (static) idle animation on an animated unit)
472 return false;
475 if (anim->m_AnimDef && anim->m_AnimDef->GetNumKeys() != m_pModelDef->GetNumBones())
477 // mismatch between model's skeleton and animation's skeleton
478 LOGERROR("Mismatch between model's skeleton and animation's skeleton (%lu model bones != %lu animation keys)",
479 (unsigned long)m_pModelDef->GetNumBones(), (unsigned long)anim->m_AnimDef->GetNumKeys());
480 return false;
483 // reset the cached bounds when the animation is changed
484 m_ObjectBounds.SetEmpty();
485 InvalidateBounds();
487 // start anim from beginning
488 m_AnimTime = 0;
491 m_Anim = anim;
493 return true;
496 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
497 // CopyAnimation
498 void CModel::CopyAnimationFrom(CModel* source)
500 m_Anim = source->m_Anim;
501 m_AnimTime = source->m_AnimTime;
503 m_Flags &= ~MODELFLAG_CASTSHADOWS;
504 if (source->m_Flags & MODELFLAG_CASTSHADOWS)
505 m_Flags |= MODELFLAG_CASTSHADOWS;
507 m_ObjectBounds.SetEmpty();
508 InvalidateBounds();
511 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
512 // AddProp: add a prop to the model on the given point
513 void CModel::AddProp(const SPropPoint* point, CModelAbstract* model, CObjectEntry* objectentry, float minHeight, float maxHeight, bool selectable)
515 // position model according to prop point position
517 // this next call will invalidate the bounds of "model", which will in turn also invalidate the selection box
518 model->SetTransform(point->m_Transform);
519 model->m_Parent = this;
521 Prop prop;
522 prop.m_Point = point;
523 prop.m_Model = model;
524 prop.m_ObjectEntry = objectentry;
525 prop.m_MinHeight = minHeight;
526 prop.m_MaxHeight = maxHeight;
527 prop.m_Selectable = selectable;
528 m_Props.push_back(prop);
531 void CModel::AddAmmoProp(const SPropPoint* point, CModelAbstract* model, CObjectEntry* objectentry)
533 AddProp(point, model, objectentry);
534 m_AmmoPropPoint = point;
535 m_AmmoLoadedProp = m_Props.size() - 1;
536 m_Props[m_AmmoLoadedProp].m_Hidden = true;
538 // we only need to invalidate the selection box here if it is based on props and their visibilities
539 if (!m_CustomSelectionShape)
540 m_SelectionBoxValid = false;
543 void CModel::ShowAmmoProp()
545 if (m_AmmoPropPoint == NULL)
546 return;
548 // Show the ammo prop, hide all others on the same prop point
549 for (size_t i = 0; i < m_Props.size(); ++i)
550 if (m_Props[i].m_Point == m_AmmoPropPoint)
551 m_Props[i].m_Hidden = (i != m_AmmoLoadedProp);
553 // we only need to invalidate the selection box here if it is based on props and their visibilities
554 if (!m_CustomSelectionShape)
555 m_SelectionBoxValid = false;
558 void CModel::HideAmmoProp()
560 if (m_AmmoPropPoint == NULL)
561 return;
563 // Hide the ammo prop, show all others on the same prop point
564 for (size_t i = 0; i < m_Props.size(); ++i)
565 if (m_Props[i].m_Point == m_AmmoPropPoint)
566 m_Props[i].m_Hidden = (i == m_AmmoLoadedProp);
568 // we only need to invalidate here if the selection box is based on props and their visibilities
569 if (!m_CustomSelectionShape)
570 m_SelectionBoxValid = false;
573 CModelAbstract* CModel::FindFirstAmmoProp()
575 if (m_AmmoPropPoint)
576 return m_Props[m_AmmoLoadedProp].m_Model;
578 for (size_t i = 0; i < m_Props.size(); ++i)
580 CModel* propModel = m_Props[i].m_Model->ToCModel();
581 if (propModel)
583 CModelAbstract* model = propModel->FindFirstAmmoProp();
584 if (model)
585 return model;
589 return NULL;
592 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
593 // Clone: return a clone of this model
594 CModelAbstract* CModel::Clone() const
596 CModel* clone = new CModel(m_SkeletonAnimManager, m_Simulation);
597 clone->m_ObjectBounds = m_ObjectBounds;
598 clone->InitModel(m_pModelDef);
599 clone->SetMaterial(m_Material);
600 clone->SetAnimation(m_Anim);
601 clone->SetFlags(m_Flags);
603 for (size_t i = 0; i < m_Props.size(); i++)
605 // eek! TODO, RC - need to investigate shallow clone here
606 if (m_AmmoPropPoint && i == m_AmmoLoadedProp)
607 clone->AddAmmoProp(m_Props[i].m_Point, m_Props[i].m_Model->Clone(), m_Props[i].m_ObjectEntry);
608 else
609 clone->AddProp(m_Props[i].m_Point, m_Props[i].m_Model->Clone(), m_Props[i].m_ObjectEntry, m_Props[i].m_MinHeight, m_Props[i].m_MaxHeight, m_Props[i].m_Selectable);
612 return clone;
616 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
617 // SetTransform: set the transform on this object, and reorientate props accordingly
618 void CModel::SetTransform(const CMatrix3D& transform)
620 // call base class to set transform on this object
621 CRenderableObject::SetTransform(transform);
622 InvalidatePosition();
625 //////////////////////////////////////////////////////////////////////////
627 void CModel::AddFlagsRec(int flags)
629 m_Flags |= flags;
631 if (flags & MODELFLAG_IGNORE_LOS)
633 m_Material.AddShaderDefine(str_IGNORE_LOS, str_1);
634 m_Material.RecomputeCombinedShaderDefines();
637 for (size_t i = 0; i < m_Props.size(); ++i)
638 if (m_Props[i].m_Model->ToCModel())
639 m_Props[i].m_Model->ToCModel()->AddFlagsRec(flags);
642 void CModel::RemoveShadowsRec()
644 m_Flags &= ~MODELFLAG_CASTSHADOWS;
646 m_Material.AddShaderDefine(str_DISABLE_RECEIVE_SHADOWS, str_1);
647 m_Material.RecomputeCombinedShaderDefines();
649 for (size_t i = 0; i < m_Props.size(); ++i)
651 if (m_Props[i].m_Model->ToCModel())
652 m_Props[i].m_Model->ToCModel()->RemoveShadowsRec();
653 else if (m_Props[i].m_Model->ToCModelDecal())
654 m_Props[i].m_Model->ToCModelDecal()->RemoveShadows();
658 void CModel::SetMaterial(const CMaterial &material)
660 m_Material = material;
663 void CModel::SetPlayerID(player_id_t id)
665 CModelAbstract::SetPlayerID(id);
667 for (std::vector<Prop>::iterator it = m_Props.begin(); it != m_Props.end(); ++it)
668 it->m_Model->SetPlayerID(id);
671 void CModel::SetShadingColor(const CColor& color)
673 CModelAbstract::SetShadingColor(color);
675 for (std::vector<Prop>::iterator it = m_Props.begin(); it != m_Props.end(); ++it)
676 it->m_Model->SetShadingColor(color);