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"
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 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
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 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
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
;
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
81 m_pModelDef
= modeldef
;
83 size_t numBones
= modeldef
->GetNumBones();
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;
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();
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
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
)
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
;
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
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;
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();
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
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
;
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
);
264 CSkeletonAnim
* anim
= new CSkeletonAnim();
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
;
274 anim
->m_ActionPos
= actionpos
* anim
->m_AnimDef
->GetDuration();
276 if (actionpos2
== -1.f
)
277 anim
->m_ActionPos2
= -1.f
;
279 anim
->m_ActionPos2
= actionpos2
* anim
->m_AnimDef
->GetDuration();
281 if (soundpos
== -1.f
)
282 anim
->m_SoundPos
= -1.f
;
284 anim
->m_SoundPos
= soundpos
* anim
->m_AnimDef
->GetDuration();
286 anim
->m_ObjectBounds
.SetEmpty();
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
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()
325 ENSURE(!m_Parent
|| m_Parent
->m_PositionValid
);
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
);
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
);
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
);
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();
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
457 m_Flags
&= ~MODELFLAG_NOLOOPANIMATION
;
460 m_Flags
|= MODELFLAG_NOLOOPANIMATION
;
462 if (!m_BoneMatrices
&& anim
->m_AnimDef
)
464 // not boned, can't animate
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)
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());
483 // reset the cached bounds when the animation is changed
484 m_ObjectBounds
.SetEmpty();
487 // start anim from beginning
496 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
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();
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;
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
)
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
)
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()
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();
583 CModelAbstract
* model
= propModel
->FindFirstAmmoProp();
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
);
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
);
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
)
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
);