Optimize drawing 3d models by making one mesh
We are making a 3D game for school that runs on the Xbox 360. We have a huge level that consists of about 100 parts, each part consists of a lot of meshes which consists of a lot of vertices. We have a custom shader that will give off a ping like effect where ever you are in the level by shading the level around you. We also have a 3D mini map since the game is in space and you can be orientated in any direction. So when we draw the level we have to draw it 4 times per frame, one to draw in the main view port, one to draw the pings in the main view port, one to draw the level in the mini map, and one to draw the pings in the mini map. It runs a 60 frames per second on a fast PC but only at 20 on an xbox. We already turned off drawing the pieces behind us that you can not see and it helped some but we still need it to go faster.
Here is the main drawing for just the level with out the pings on the main view port...
//draw the main level in a regular way
foreach (LevelObject part in levelData.LevelParts)
{
partBounds.Center = part.position;
if (viewFrustum.Intersects(partBounds))
{
//Rotate X
Matrix worldMatrix = Matrix.CreateRotationX(MathHelper.ToRadians(part.Xrotate));
//Rotate Z
worldMatrix *= Matrix.CreateRotationZ(MathHelper.ToRadians(part.Zrotate));
//Rotate Y
worldMatrix *= Matrix.CreateRotationY(MathHelper.ToRadians(part.Yrotate));
worldMatrix *= Matrix.CreateWorld(part.position, Vector3.Forward, Vector3.Up);
Model Object = levelModels[part.modelName];
//set in the gamers viewport
foreach (ModelMesh mesh in Object.Meshes)
{
foreach (BasicEffect effect in mesh.Effects)
{
//effect.EnableDefaultLighting();
effect.LightingEnabled = true;
effect.AmbientLightColor = new Vector3(0.09f, 0.15f, 0.215f);
effect.DirectionalLight0.DiffuseColor = new Vector3(0.3f, 0.3f, 0.3f);
effect.DirectionalLight0.Direction = viewMatrix.Forward;
effect.DirectionalLight0.Enabled = true;
effect.DirectionalLight1.DiffuseColor = new Vector3(0.05f, 0.085f, 0.25f);
effect.DirectionalLight1.Direction = viewMatrix.Down;
effect.DirectionalLight1.Enabled = true;
effect.PreferPerPixelLighting = true;
effect.World = worldMatrix;
effect.View = viewMatrix;
effect.Projection = projectionMatrix;
mesh.Draw();
}
}
}
}
So if in blender I made the level part just one mesh it would have to do less loops, I'm not sure if that would make it draw faster. Any ideas I need to increase the drawing performance by a lot? This game is split screen and can have up to 4 players, which would increase the level draws by 4 times.
Here is the full draw function
public override void Draw(GameTime gameTime)
{
/* NORMAL VIEW */
//set viewport for everyone
for (int i = 0; i < SignedInGamer.SignedInGamers.Count; i++)
{
GraphicsDevice.Viewport = Camera.gameScreenViewPorts[SignedInGamer.SignedInGamers[i]];
Matrix viewMatrix = Camera.viewMatrix[SignedInGamer.SignedInGamers[i]];
Matrix projectionMatrix = Camera.projectionMatrix[SignedInGamer.SignedInGamers[i]];
GraphicsDevice.RasterizerState = RasterizerState.CullNone;
GraphicsDevice.BlendState = BlendState.Opaque;
//view frustrum object for culling
BoundingFrustum viewFrustum = new BoundingFrustum(viewMatrix * projectionMatrix);
BoundingSphere partBounds = new BoundingSphere();
partBounds.Radius = levelData.scale;
//draw the main level in a regular way
foreach (LevelObject part in levelData.LevelParts)
{
partBounds.Center = part.position;
if (viewFrustum.Intersects(partBounds))
{
//Rotate X
Matrix worldMatrix = Matrix.CreateRotationX(MathHelper.ToRadians(part.Xrotate));
//Rotate Z
worldMatrix *= Matrix.CreateRotationZ(MathHelper.ToRadians(part.Zrotate));
//Rotate Y
worldMatrix *= Matrix.CreateRotationY(MathHelper.ToRadians(part.Yrotate));
worldMatrix *= Matrix.CreateWorld(part.position, Vector3.Forward, Vector3.Up);
Model Object = levelModels[part.modelName];
//set in the gamers viewport
foreach (ModelMesh mesh in Object.Meshes)
{
foreach (BasicEffect effect in mesh.Effects)
{
//effect.EnableDefaultLighting();
effect.LightingEnabled = true;
effect.AmbientLightColor = new Vector3(0.09f, 0.15f, 0.215f);
effect.DirectionalLight0.DiffuseColor = new Vector3(0.3f, 0.3f, 0.3f);
effect.DirectionalLight0.Direction = viewMatrix.Forward;
effect.DirectionalLight0.Enabled = true;
effect.DirectionalLight1.DiffuseColor = new Vector3(0.05f, 0.085f, 0.25f);
effect.DirectionalLight1.Direction = viewMatrix.Down;
effect.DirectionalLight1.Enabled = true;
effect.PreferPerPixelLighting = true;
effect.World = worldMatrix;
effect.View = viewMatrix;
effect.Projection = projectionMatrix;
mesh.Draw();
}
}
}
}
/* PING VIEW */
List<Vector3> pingPos = new List<Vector3>();
List<Vector4> pingColor = new List<Vector4>();
List<float> pingRange = new List<float>();
for (int a = 0; a < Game.Components.Count; a++)
{
if (Game.Components[a] is Ship)
{
pingPos.Add(((Ship)Game.Components[a]).worldMatrix.Translation);
pingColor.Add(new Vector4(
((Ship)Game.Components[a]).playerColor.R,
((Ship)Game.Components[a]).playerColor.G,
((Ship)Game.Components[a]).playerColor.B,
1));
pingRange.Add(((Ship)Game.Components[a]).pingRange * (levelData.scale * 2.0f));
}
}
if (pingPos.Count() > 0)
{
//apply ping lighting stuffs here
pingTest.Parameters["PingPos"].SetValue(pingPos.ToArray());
pingTest.Parameters["PingPosCount"].SetValue(pingPos.Count());
pingTest.Parameters["PingColor"].SetValue(pingColor.ToArray());
pingTest.Parameters["PingColorCount"].SetValue(pingColor.Count());
pingTest.Parameters["PingRange"].SetValue(pingRange.ToArray());
pingTest.Parameters["PingRangeCount"].SetValue(pingRange.Count());
}
foreach (LevelObject part in levelData.LevelParts)
{
partBounds.Center = part.position;
if (viewFrustum.Intersects(partBounds))
{
//Rotate X
Matrix worldMatrix = Matrix.CreateRotationX(MathHelper.ToRadians(part.Xrotate));
//Rotate Z
worldMatrix *= Matrix.CreateRotationZ(MathHelper.ToRadians(part.Zrotate));
//Rotate Y
worldMatrix *= Matrix.CreateRotationY(MathHelper.ToRadians(part.Yrotate));
worldMatrix *= Matrix.CreateWorld(part.position, Vector3.Forward, Vector3.Up);
Model Object = levelModels[part.modelName];
foreach (ModelMesh mesh in Object.Meshes)
{
//ok, this is going to be kind of weird, and there's got to be a cleaner
//or better way to do this.
List<Effect> backup = new List<Effect>();
foreach (ModelMeshPart meshpart in mesh.MeshParts)
{
backup.Add(meshpart.Effect);
meshpart.Effect = pingTest;
meshpart.Effect.Parameters["World"].SetValue(worldMatrix * mesh.ParentBone.Transform);
meshpart.Effect.Parameters["View"].SetValue(viewMatrix);
meshpart.Effect.Parameters["Projection"].SetValue(projectionMatrix);
//Matrix worldInverseTransposeMatrix = Matrix.Transpose(Matrix.Invert(mesh.ParentBone.Transform * world));
//pingTest.Parameters["WorldInverseTranspose"].SetValue(worldInverseTransposeMatrix);
}
mesh.Draw();
//reset the basic effect crap
foreach (ModelMeshPart meshpart in mesh.MeshParts)
{
meshpart.Effect = backup.First();
backup.RemoveAt(0);
}
//
}
}
}
//Undo the weird things this shader does
GraphicsDevice.BlendState = BlendState.Opaque;
GraphicsDevice.DepthStencilState = DepthStencilState.Default;
/* MINIMAP VIEW */
GraphicsDevice.Viewport = Camera.mapViewports[SignedInGamer.SignedInGamers[i]];
viewMatrix = Camera.mapViewMatrix[SignedInGamer.SignedInGamers[i]];
projectionMatrix = Camera.mapProjectionMatrix[SignedInGamer.SignedInGamers[i]];
GraphicsDevice.RasterizerState = RasterizerState.CullNone;
GraphicsDevice.BlendState = BlendState.Opaque;
//view frustum for the map
BoundingFrustum mapFrustum = new BoundingFrustum(viewMatrix * projectionMatrix);
//draw the main level in a regular way
foreach (LevelObject part in levelData.LevelParts)
{
partBounds.Center = part.position;
if (mapFrustum.Intersects(partBounds))
{
//Rotate X
Matrix worldMatrix = Matrix.CreateRotationX(MathHelper.ToRadians(part.Xrotate));
//Rotate Z
worldMatrix *= Matrix.CreateRotationZ(MathHelper.ToRadians(part.Zrotate));
//Rotate Y
worldMatrix *= Matrix.CreateRotationY(MathHelper.ToRadians(part.Yrotate));
worldMatrix *= Matrix.CreateWorld(part.position, Vector3.Forward, Vector3.Up);
Model Object = levelModels[part.modelName];
//set in the gamers viewport
foreach (ModelMesh mesh in Object.Meshes)
{
foreach (BasicEffect effect in mesh.Effects)
{
//effect.EnableDefaultLighting();
effect.LightingEnabled = true;
if (match(part.position / 15.0f))
effect.AmbientLightColor = new Vector3(1.0f, 0.30f, 0.43f);
else
effect.AmbientLightColor = new Vector3(0.18f, 0.30f, 0.43f);
//effect.DirectionalLight0.DiffuseColor = new Vector3(0.6f, 0.6f, 0.6f);
effect.DirectionalLight0.DiffuseColor = new Vector3(0.6f, 0.6f, 0.6f);
effect.DirectionalLight0.Direction = viewMatrix.Forward;
effect.DirectionalLight0.Enabled = true;
//effect.DirectionalLight1.DiffuseColor = new Vector3(0.1f, 0.17f, 0.5f);
effect.DirectionalLight1.DiffuseColor = new Vector3(0.1f, 0.17f, 0.5f);
effect.DirectionalLight1.Direction = viewMatrix.Down;
effect.DirectionalLight1.Enabled = true;
effect.PreferPerPixelLighting = true;
effect.World = worldMatrix;
effect.View = viewMatrix;
effect.Projection = projectionMatrix;
mesh.Draw();
}
}
}
}
base.Draw(gameTime);
return;
if (pingPos.Count() > 0)
{
//apply ping lighting stuffs here
pingTest.Parameters["PingPos"].SetValue(pingPos.ToArray());
pingTest.Parameters["PingPosCount"].SetValue(pingPos.Count());
pingTest.Parameters["PingColor"].SetValue(pingColor.ToArray());
pingTest.Parameters["PingColorCount"].SetValue(pingColor.Count());
pingTest.Parameters["PingRange"].SetValue(pingRange.ToArray());
pingTest.Parameters["PingRangeCount"].SetValue(pingRange.Count());
}
foreach (LevelObject part in levelData.LevelParts)
{
partBounds.Center = part.position;
if (mapFrustum.Intersects(partBounds))
{
//Rotate X
Matrix worldMatrix = Matrix.CreateRotationX(MathHelper.ToRadians(part.Xrotate));
//Rotate Z
worldMatrix *= Matrix.Create开发者_开发技巧RotationZ(MathHelper.ToRadians(part.Zrotate));
//Rotate Y
worldMatrix *= Matrix.CreateRotationY(MathHelper.ToRadians(part.Yrotate));
worldMatrix *= Matrix.CreateWorld(part.position, Vector3.Forward, Vector3.Up);
Model Object = levelModels[part.modelName];
foreach (ModelMesh mesh in Object.Meshes)
{
//ok, this is going to be kind of weird, and there's got to be a cleaner
//or better way to do this.
List<Effect> backup = new List<Effect>();
foreach (ModelMeshPart meshpart in mesh.MeshParts)
{
backup.Add(meshpart.Effect);
meshpart.Effect = pingTest;
meshpart.Effect.Parameters["World"].SetValue(worldMatrix * mesh.ParentBone.Transform);
meshpart.Effect.Parameters["View"].SetValue(viewMatrix);
meshpart.Effect.Parameters["Projection"].SetValue(projectionMatrix);
//Matrix worldInverseTransposeMatrix = Matrix.Transpose(Matrix.Invert(mesh.ParentBone.Transform * world));
//pingTest.Parameters["WorldInverseTranspose"].SetValue(worldInverseTransposeMatrix);
}
mesh.Draw();
//reset the basic effect crap
foreach (ModelMeshPart meshpart in mesh.MeshParts)
{
meshpart.Effect = backup.First();
backup.RemoveAt(0);
}
//*/
}
}
}
//}
//Undo the weird things this shader does
GraphicsDevice.BlendState = BlendState.Opaque;
GraphicsDevice.DepthStencilState = DepthStencilState.Default;
base.Draw(gameTime);
}
}
Question about the garbage collector
So by setting vectors like effect.DirectionalLight0.DiffuseColor using this method...
effect.DirectionalLight0.DiffuseColor.X = 0.3f;
effect.DirectionalLight0.DiffuseColor.Y = 0.3f;
effect.DirectionalLight0.DiffuseColor.Z = 0.3f;
Instead of this method....
effect.DirectionalLight0.DiffuseColor = new Vector3(.3, .3, .3);
I allocates less memory for the garbage collector to pick up, but once the draw() function is done being called doesn't it add all the garbage to stack so the collector can pick it up almost instantly? I realize it adds a little extra work for the collector, but it shouldn't add that much right?
The issue of speed doesn't necessarily comes from what you think. The serious problem with C# in video games is that it's very easy to do wrong things because they look pretty.
For example :
effect.AmbientLightColor = new Vector3(0.09f, 0.15f, 0.215f);
That's incredibly pretty. You're allocating a Vector3 there. And 2 others.
But hey, you're in a double foreach loop!
Where did the previous effect.AmbientLightColor
go ? Well, it's gonna get garbage collected. That's a lot of allocations and garbage collection per frame.
Instead you should use something longer but much more efficient:
effect.AmbientLightColor.X = 0.09f;
effect.AmbientLightColor.Y = 0.15f;
effect.AmbientLightColor.Z = 0.215f;
If you do this everywhere where you don't actually need to allocate new objects, you will see a considerable performance increase.
A golden rule is always avoid allocations. I could try to help more if you provided more code, though, but I think this should help already.
I completely agree with @Heandel's answer. Here are a few more things besides the allocation:
What is actually variable inside the inner loop? It appears that only the assignment of 2 direction vectors and 3 matrixes call are changing. So 7 out of the 12 lines could be called only once when the effect is initially added to the mesh (probably at load time), instead of once per render frame.
Model Object = levelModels[part.modelName];
: Is this doing a hash lookup of a string for each part every frame? Why not do this only once on level load, and store a Model reference with the part, instead of just a name?
And you make 3 calls to MathHelper.ToRadians
for each part. Why not just store the rotations in radians to begin with?
One thing that eating up over 1/2 your frame rate is the mesh.Draw();
is in the wrong loop, you have
mesh.Draw();
}
}
it should be
}
mesh.Draw();
}
精彩评论