Hello Guys.
It really bugs me when my first post in any community I join has to be a request for help, so I have tried to hold off asking for assistance - but now this is holding up my development.
I have been having a headache over this for 2 weeks. There are plenty of examples and topics on the internet demonstrating this using primitives, and plenty that imply you want to use a content processor, but hardly any that ( none that I could find ) that shows how to implement this correctly when you want to copy vertices and indices from and existing model... and being fairly new to XNA ( specifically 3D programming ) , i have been losing sleep over only one and one particular issue: Geometry Instancing.
To begin - I started my implementation using the examples Here and Here, but with two notable differences:
- A: I did not need the "atlas texture" thing.
- B: I dont want to use generated geometry prrimitives, but instead copy the vertices from an existing model.
The MSDN Instanced Geometry Example doesnt help because the so called "instanced mesh" class mentioned in the article cannot be found.
In my particular project I need to check individual sub meshes ( + transforms ) to the camera's view frustum - so as a result - I want to maintain each individual mesh part in their own seperate vertexbuffers ( only examples I found that copy existing meshdata combines all meshparts into a single vertex buffer, making it impossible to manage individual mesh parts seperatly ).
In my project I have created the following classes:
- OSModel
- OSModelMesh
- OSModelMeshPart
I want OSModelMeshPart to contain my VertexBuffer / InstanceBuffer, as this would be initialized from an existing ModelMeshPart, and this is how I have written it.
At runtime, my entire draw routine passes through (during step through debugging) without raising any exceptions - however nothing displays. One of those issues where theres is nothing telling me what isnt working. VertexBuffers appear full ( and all copied data matches lengths and types of the original sources ). Its driving me nuts.
So I tried the following:
- Test: Forcing all instanced positions into the view frustum:
Result: FPS drops as if all positions in my instance buffer are being drawn, but they seem completly invisible. - Test: Alternate positioning by sending only a matrix to use as position transform
Result: Failed - Test: Tried using Generated Geometry ( manually typed in cube ).. as in the examples:
Result: It worked - which is why I believe Im copying existing model data incorrectly... - Test: A none instanced version of my shader:
Result: Passed - and renders correctly
In the attached images - all None-Instanced tests - all my positions are working - and my shaders are working, frustum checks are working, but I need this same result with Instancing, because I can maintain 20000+ ships in the game world, but I cannot put more than a few hundred of them inside my view frustum because I hit my batch limit ( too many draw calls ). I need to be able to put 2000 ships in my view frustum, in which some of these would be huge titans, I intend to LOD and Frustum Occlude each individual mesh part.
But when initializing these same models ( Skybox, Planets, and ships ) using the below instancing code, they do not render.
After banging my head against the wall a few times I figured the problem must be originating from the areas that differ from the examples:
- Either I am copying Initializing my VertexBuffer, and IndexBuffer, for each OSModelMeshPart when copying from a ModelMeshPart incorrectly... or....
- Maybe the extra elements ( currently only a vector 4, I removed others ) are not being passes to the shader correctly ( which I doubt since It appears the positions are working correctly ? )
My goal:
- I want to maintain seperate draw lists for every mesh part in the end, therefore seperate instancebuffers for each meshpart - each meshpart is tested against the cameras view frustum on update - and the final instance buffer would contain only those visible in the view frustum.
- DrawStates are controlled by OSModelMesh, and are ordered by OSModel (Opaque meshes are stored in a seperate lists from Alpha and additive meshes )
- For it to actually work.
My entire instancing takes place in OSModelMeshPart - which I wanted to attach instead of making my post too long, but it appears .VB files are not permitted....
This is the curent state of my OSModelMeshPart object in my framework: RenderHelper contains StateManagement and Frustum Checks, etc...
Imports OS.Graphics.RenderHelper
Namespace Graphics
Public Class OSModelMeshPart
Inherits OS.iGameObject
Friend m_VertexDeclaration As VertexDeclaration, _
_
m_GeometryBuffer As VertexBuffer,
m_IndexBuffer As IndexBuffer,
_
m_InstanceBuffer As VertexBuffer
Friend m_Bindings As VertexBufferBinding()
Friend m_VertexStreamElements As VertexElement()
Private m_Owner As OSModelMesh
Friend m_Vertices As VertexPositionTexture()
Friend m_VertexCount As Integer,
m_VertexOffset As Integer,
m_IndexCount As Integer,
m_PrimitiveCount As Integer,
_
m_Last_InstanceCount As Integer = 0
Private m_BoundingSphere As BoundingSphere,
m_BoundingBox_FromSphere As BoundingBox,
m_BoundingBox_FromVertices As BoundingBox
Private m_Cache_LastPositions As ModelPositionInfo()
' instance list ( we TRIM/ ADD per cycle - do not CLEAR - optimization )
Private m_Instances As New List(Of InstanceInfo)
Private m_ListIDCap As Integer = -1
' weather we have anything to draw:
Private m_CanDraw As Boolean = False
' bounds for each instance per cycle:
Friend m_Cache_Instances_BoundingSpheres As BoundingSphere() = {}
Friend m_Cache_Instances_BoundingBoxes As BoundingBox() = {}
''' <summary>
''' Returns Bounding Spheres for this mesh part for all previous instances passed.
''' Bounding spheres are returned for all instanced regardless of View Frustum visibility.
''' EG: Length of array will always metch the positioninfo list length.
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
ReadOnly Property Get_Last_Instance_BoundingSpheres As BoundingSphere()
Get
Return m_Cache_Instances_BoundingSpheres
End Get
End Property
''' <summary>
''' Returns all instance boundingboxes for this meshpart from the last instance positions list passed .
''' Bounding spheres are returned for all instanced regardless of View Frustum visibility.
''' EG: Length of array will always metch the positioninfo list length.
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
ReadOnly Property Get_Last_Instance_BoundingBoxes As BoundingBox()
Get
Return m_Cache_Instances_BoundingBoxes
End Get
End Property
ReadOnly Property Owner_Mesh As OSModelMesh
Get
Return m_Owner
End Get
End Property
ReadOnly Property Owner_Model As OSModel
Get
Return m_Owner.Owner_Model
End Get
End Property
'Private Structure InstanceInfo
' Public World As Vector4
' Public TeamColor_1 As Vector4 ' for meshes that support TeamColor on RED regionmap channel
' Public TeamColor_2 As Vector4 ' for meshes that support TeamColor on GREEN region map channel
' Public TeamColor_3 As Vector4 ' for meshes that support TeamColor on BLUE region map channel
'End Structure
Private Structure InstanceInfo
Public World As Vector4
End Structure
Friend Sub New(Owner As OSModelMesh, Part As ModelMeshPart)
MyBase.New(Owner.Game)
m_Owner = Owner
_Initialize_VertexDeclaration(Part)
_Initialize_Geometry(Part)
_Initialize_BoundingSphere()
_Initialize_BoundingBox_From_BoundingSphere()
_Initialize_BoundingBox_From_Vertices()
#If DEBUG Then
'TODO: log stats containing information about this meshpart:
#End If
End Sub
Private Sub _Initialize_VertexDeclaration(Part As ModelMeshPart)
'0 = POSITION1 : Model Transforms per instance
'1 = COLOR1 : Per Instance Team Color1
'2 = COLOR2 : Per Instance Team Color2
'3 = COLOR3 : Per Instance Team Color3
' m_VertexStreamElements =
' {
' New VertexElement(0, VertexElementFormat.Vector4, VertexElementUsage.Position, 1)
'_
' New VertexElement(SizeOfVector4 * 4, VertexElementFormat.Vector4, VertexElementUsage.Color, 1),
' New VertexElement(SizeOfVector4 * 5, VertexElementFormat.Vector4, VertexElementUsage.Color, 2),
' New VertexElement(SizeOfVector4 * 6, VertexElementFormat.Vector4, VertexElementUsage.Color, 3)
'_
' }
m_VertexStreamElements =
{
New VertexElement(0, VertexElementFormat.Vector4, VertexElementUsage.Position, 1)
}
m_VertexDeclaration = New VertexDeclaration(m_VertexStreamElements)
End Sub
Private Sub _Initialize_Geometry(Part As ModelMeshPart)
Dim Result_Vertex As VertexPositionTexture() = {}
With Part
' Initialize Vertices
m_VertexCount = .PrimitiveCount * 3
m_VertexOffset = .VertexOffset
m_PrimitiveCount = .PrimitiveCount
Array.Resize(Result_Vertex, m_VertexCount)
.VertexBuffer.GetData(Of VertexPositionTexture)(Result_Vertex)
' execute the original vertexbuffer
.VertexBuffer.Dispose()
' Bake Bone transforms into vertices:
' NOTE: if animated / dynamic bones are needed - then this should be removed - and bone transforms would need to be supplied somehow per instance
g_ApplyBoneTransforms(Result_Vertex, Owner_Mesh.m_ParentBone)
m_GeometryBuffer = New VertexBuffer(Owner_Model.GraphicsDevice, VertexPositionTexture.VertexDeclaration, Result_Vertex.Length, BufferUsage.None)
' apply:
m_GeometryBuffer.SetData(Of VertexPositionTexture)(Result_Vertex)
' Indices:
m_IndexCount = .IndexBuffer.IndexCount
' ----------------------------------------
Dim Result_Indices As Short() = {}
Array.Resize(Result_Indices, m_IndexCount)
.IndexBuffer.GetData(Of Short)(Result_Indices, 0, Result_Indices.Length)
m_IndexBuffer = New IndexBuffer(Owner_Model.GraphicsDevice, Part.IndexBuffer.IndexElementSize, .IndexBuffer.IndexCount, BufferUsage.None)
' apply
m_IndexBuffer.SetData(Of Short)(Result_Indices)
' ----------------------------------------
End With
' a copy of vertices remains in memory because we create bounds from them for thsi meshpart:
m_Vertices = Result_Vertex
End Sub
' we dont have dynamic bones in our model structure ( we dont need them in the current game ) - so we will instead bake the bone transforms into the vertices
Private Sub g_ApplyBoneTransforms(ByRef Vertices As VertexPositionTexture(), Bone As ModelBone)
If Bone Is Nothing Then Return
For I = 0 To Vertices.Length - 1
Vertices(I).Position = Vector3.Transform(Vertices(I).Position, GetAbsoluteBoneTransform(Bone))
Next
End Sub
' get absolute bone transforms: ( which will be hardbaked into our model )
Private Function GetAbsoluteBoneTransform(Bone As ModelBone) As Matrix
If Bone Is Nothing Then Return Matrix.Identity
Return Bone.Transform * GetAbsoluteBoneTransform(Bone.Parent)
End Function
Private Sub _Initialize_BoundingSphere()
Dim R As Single = 0
' use bone transform
' vertices already have bone transformations applied...
Dim BoundingSpherePosition As Vector3 = Vector3.Transform(Vector3.Zero, GetAbsoluteBoneTransform(Owner_Mesh.m_ParentBone))
For I = 0 To m_Vertices.Length - 1
Dim D = System.Math.Abs(Vector3.Distance(BoundingSpherePosition, m_Vertices(I).Position))
If D > R Then R = D
Next
m_BoundingSphere = New BoundingSphere(BoundingSpherePosition, R)
End Sub
Private Sub _Initialize_BoundingBox_From_BoundingSphere()
m_BoundingBox_FromSphere = BoundingBox.CreateFromSphere(m_BoundingSphere)
End Sub
' vertices at this point only have bone transforms applied: bounding box represents all verticies with bone transforms
' - so world transforms would have to be applied seperatly ( in a cached instance list )
Private Sub _Initialize_BoundingBox_From_Vertices()
Dim MIN As Vector3 = Vector3.Zero
Dim MAX As Vector3 = Vector3.Zero
For I = 0 To m_Vertices.Length - 1
MIN = Vector3.Min(MIN, m_Vertices(I).Position)
MAX = Vector3.Max(MAX, m_Vertices(I).Position)
Next
m_BoundingBox_FromVertices = New BoundingBox(MIN, MAX)
End Sub
' Update cache list of boundingboxes for all instances (sphere too )
Private Sub Update_Instance_BoundingBox(Position As ModelPositionInfo, Index As Integer)
m_Cache_Instances_BoundingBoxes(Index) = New BoundingBox(
Vector3.Transform(m_BoundingBox_FromVertices.Min, Position.GetTransform),
Vector3.Transform(m_BoundingBox_FromVertices.Max, Position.GetTransform)
)
m_Cache_Instances_BoundingSpheres(Index) = New BoundingSphere(
Vector3.Transform(m_BoundingSphere.Center, Position.GetTransform),
m_BoundingSphere.Radius * Position.Scale
)
End Sub
Friend Sub Update_Instance_Information(Positions As ModelPositionInfo())
If Not m_Cache_LastPositions Is Nothing And Not Positions Is Nothing Then
If m_Cache_LastPositions.GetHashCode = Positions.GetHashCode Then
'OPTIMIZATION: nothing has changed - use previous instance information:
If Not m_InstanceBuffer Is Nothing Then Return
End If
End If
m_CanDraw = False
m_Last_InstanceCount = 0
If Positions Is Nothing Then Return
If Positions.Count < 1 Then Return
' bounding boxes lengths:
If m_Cache_Instances_BoundingSpheres.Length <> Positions.Length Then Array.Resize(m_Cache_Instances_BoundingSpheres, Positions.Length)
If m_Cache_Instances_BoundingBoxes.Length <> Positions.Length Then Array.Resize(m_Cache_Instances_BoundingBoxes, Positions.Length)
' make sure we are not repeatedly growing the list in the loop per instance:
If m_Instances.Capacity < Positions.Length Then m_Instances.Capacity = Positions.Length
Dim Visible As Integer = 0
Dim Index As Integer = 0 ' so we may scale our list:
Dim TrueIndex As Integer = 0
For Each P As ModelPositionInfo In Positions
If P.Enabled Then
' we only check with wour bounding sphere because the method has extra checks in case of bad accuracy:
If g_IsInViewOfCamera(P.Position, m_BoundingSphere, Owner_Model.GameState.Camera) Then
' TODO: check LOD and determine weather to / or how to draw this meshpart Per Instance
Dim INSTANCE As New InstanceInfo With
{
.World = Vector4.Transform(Vector3.Zero, P.GetTransform)
}
If Owner_Mesh.IsBillboard Then
' TODO: Billboard Tranformation for this model instance ( must face camera )
End If
If m_ListIDCap < Index Then
' index does not exist in list yet:
m_Instances.Add(INSTANCE)
m_ListIDCap = Index
Else
' index exists : replace... ( avoid clearing list every cycle )
m_Instances(Index) = INSTANCE
End If
' done:
Index += 1
End If
End If
' finally - ensure we have a bounding box and bounding sphere for each instance:
' bounds are cached for ALL positions regardless of weather they are enabled, or in the view frustum
Update_Instance_BoundingBox(P, TrueIndex)
TrueIndex += 1
Next
' remove excess positions that are not drawn this time around
' ( extra instance information from a previous draw cycle ( eg: an instance is no longer in the view frustum) )
If m_Instances.Count >= Index Then
For I = Index To m_Instances.Count - 1
m_Instances.RemoveAt(I)
Next
End If
' check that we actually have vertices to send:
If m_Instances.Count < 1 Then
' nothing to draw:
m_InstanceBuffer = Nothing
m_CanDraw = False
m_Last_InstanceCount = 0
End If
' if not created:
If m_InstanceBuffer Is Nothing Then
m_InstanceBuffer = New VertexBuffer(Owner_Model.GraphicsDevice, m_VertexDeclaration, m_Instances.Count, BufferUsage.WriteOnly)
End If
' if count is extactly the same as previous cycle - then do not redeclare - simply replace the data:
'If m_InstanceBuffer.VertexCount <> m_Instances.Count Then
m_InstanceBuffer = New VertexBuffer(Owner_Model.GraphicsDevice, m_VertexDeclaration, m_Instances.Count, BufferUsage.WriteOnly)
' End If
' apply:
m_InstanceBuffer.SetData(m_Instances.ToArray)
' set draw information states:
m_CanDraw = True
m_Last_InstanceCount = m_Instances.Count
' hold a copy of previous positions list for hash comparison next cycle:
m_Cache_LastPositions = Positions
End Sub
' must be called from OSModelMesh.Draw() AFTER 'Update_Instance_Information()' is called
' returns total instances drawn:
Friend Function Draw() As Integer
' check is flag is set notifying that we have nothing to draw this time round:
If m_CanDraw = False Then Return 0
m_Bindings = Nothing
' prepare bindings:
m_Bindings = {
New VertexBufferBinding(m_GeometryBuffer),
New VertexBufferBinding(m_InstanceBuffer, 0, 1)
}
With Owner_Model.GraphicsDevice
.Indices = m_IndexBuffer
If Owner_Model.Effect.Shader.CurrentTechnique.Passes.Count = 1 Then
' single pass
Owner_Model.Effect.Shader.CurrentTechnique.Passes(0).Apply()
.SetVertexBuffers(m_Bindings)
.DrawInstancedPrimitives(PrimitiveType.TriangleList, 0, 0, m_VertexCount, 0, m_PrimitiveCount, m_Last_InstanceCount)
Else
' shader have multiple passes:
For Each P As EffectPass In Owner_Model.Effect.Shader.CurrentTechnique.Passes
P.Apply()
.SetVertexBuffers(m_Bindings)
.DrawInstancedPrimitives(PrimitiveType.TriangleList, 0, 0, m_VertexCount, 0, m_PrimitiveCount, m_Last_InstanceCount)
Next
End If
'.SetVertexBuffers(Nothing)
End With
Return m_Last_InstanceCount
End Function
End Class
End Namespace
I think my problem is here - were I initialize my geometry:
Private Sub _Initialize_Geometry(Part As ModelMeshPart)
Dim Result_Vertex As VertexPositionTexture() = {}
With Part
' Initialize Vertices
m_VertexCount = .PrimitiveCount * 3
m_VertexOffset = .VertexOffset
m_PrimitiveCount = .PrimitiveCount
Array.Resize(Result_Vertex, m_VertexCount)
.VertexBuffer.GetData(Of VertexPositionTexture)(Result_Vertex)
.VertexBuffer.Dispose()
' Bake Bone transforms into vertices:
' NOTE: if animated / dynamic bones are needed - then this should be removed - and bone transforms would need to be supplied somehow per instance
g_ApplyBoneTransforms(Result_Vertex, Owner_Mesh.m_ParentBone)
m_GeometryBuffer = New VertexBuffer(Owner_Model.GraphicsDevice, VertexPositionTexture.VertexDeclaration, Result_Vertex.Length, BufferUsage.None)
' apply:
m_GeometryBuffer.SetData(Of VertexPositionTexture)(Result_Vertex)
' Indices:
m_IndexCount = .IndexBuffer.IndexCount
' ----------------------------------------
Dim Result_Indices As Short() = {}
Array.Resize(Result_Indices, m_IndexCount)
.IndexBuffer.GetData(Of Short)(Result_Indices, 0, Result_Indices.Length)
m_IndexBuffer = New IndexBuffer(Owner_Model.GraphicsDevice, Part.IndexBuffer.IndexElementSize, .IndexBuffer.IndexCount, BufferUsage.None)
' apply
m_IndexBuffer.SetData(Of Short)(Result_Indices)
' ----------------------------------------
End With
' a copy of vertices remains in memory because we create bounds from them:
m_Vertices = Result_Vertex
End Sub
Notes:
- Update_Instance_Information() would be called at the end of every update cycle once - and is required before draw ( at the moment it is called below draw ):
- OSModel.Draw is called - wich contains each mesh sorted by Blendstate (opaque first, Additive second, Alpha Last ) , which calls OSModelMesh.Draw - and finally OSModelMeshPart.Draw().
- Shader effect parameters are set before calling OSModelMeshPart.Draw.
I am also supplying the shader I am trying to test my instancing with - which should should only be returning white for now:
// #include "functions.hlsl"
float4x4 View;
float4x4 Projection;
texture InputTexture;
sampler InputTextureSampler = sampler_state
{
texture = <InputTexture>;
mipfilter = LINEAR;
minfilter = LINEAR;
magfilter = LINEAR;
};
struct INSTANCED_VSI{
float4 Position : POSITION0;
};
struct INSTANCED_VSO{
float4 Position : POSITION0;
};
INSTANCED_VSO VS_MAIN(INSTANCED_VSI input, float4 InstanceTransform : POSITION1)
{
INSTANCED_VSO output;
// -->
float4x4 WVP = View * Projection;
float4 WorldPosition = input.Position + InstanceTransform;
output.Position = mul(WorldPosition, WVP);
return output;
}
float4 PS_MAIN(INSTANCED_VSO input) : COLOR0
{
float4 result = float4(1,1,1,1);
// we are ignoring the input texture for now....
return (result) ;
}
technique RENDER_WITH_PS
{
pass p0
{
//ZWriteEnable = false;
//AlphaTestEnable = false;
//AlphaBlendEnable = true;
//SrcBlend = SRCALPHA;
//DestBlend = ONE;
VertexShader = compile vs_3_0 VS_MAIN();
PixelShader = compile ps_3_0 PS_MAIN();
}
}
//technique RENDER_WITHOUT_PS
//{
// pass p0
// {
// //ZWriteEnable = false;
// //AlphaTestEnable = false;
// //AlphaBlendEnable = true;
// //SrcBlend = SRCALPHA;
// //DestBlend = ONE;
// VertexShader = compile vs_3_0 VertexShaderFunction();
// }
//}
Any help and explanation would be much appreciated. I can read both C# and VB , so any examples can be described in either language. I am writing in VB because of personal preference.