Tutorial 40: Primitive Types
The PrimitiveType enum controls how the GPU assembles raw vertex data into geometry. Choosing the right primitive type affects both performance and the structure of your vertex or index buffers.
The PrimitiveType enum
CNA exposes the same five primitive types as XNA 4.0:
namespace Microsoft::Xna::Framework::Graphics {
enum class PrimitiveType {
TriangleList, // every 3 vertices form an independent triangle
TriangleStrip, // first triangle = verts 0,1,2; each extra vert adds one triangle
LineList, // every 2 vertices form an independent line segment
LineStrip, // connected polyline; first segment = verts 0,1
PointList, // each vertex is a rendered point (size controlled by rasterizer)
};
} // namespace
When to use each
- TriangleList — most common. Each triangle is self-contained, making it easy to combine meshes. Required by
DrawIndexedPrimitivesin most scenes. - TriangleStrip — useful for ribbons, terrain strips, or generated geometry where vertices are shared between adjacent triangles. Uses roughly half the vertex bandwidth of a list for the same geometry.
- LineList — debug overlays, grids, wire frames. Every pair of vertices is independent.
- LineStrip — paths, trajectories, spline previews. One vertex shared between adjacent segments.
- PointList — particle systems, star fields, point clouds.
Winding order and back-face culling
CNA follows the XNA / OpenGL convention: counter-clockwise winding is front-facing. The default RasterizerState culls back faces (CullMode::CullCounterClockwiseFace in XNA terms maps to GL GL_BACK culling with CCW front).
// Triangle with CCW winding (visible from +Z looking toward origin)
VertexPositionColor verts[] = {
{ Vector3(-0.5f, -0.5f, 0.0f), Color::Red }, // bottom-left (index 0)
{ Vector3( 0.5f, -0.5f, 0.0f), Color::Green }, // bottom-right (index 1)
{ Vector3( 0.0f, 0.5f, 0.0f), Color::Blue }, // top-center (index 2)
// 0 -> 1 -> 2 is counter-clockwise when viewed from front: visible
};
// Disable culling to see both sides (e.g. flat sprites)
gd.setRasterizerState(RasterizerState::CullNone);
DrawPrimitives vs DrawIndexedPrimitives
DrawPrimitives reads vertices sequentially; every vertex in the buffer is unique.
DrawIndexedPrimitives reads through an IndexBuffer of uint16_t or uint32_t indices, allowing vertices to be shared between triangles — critical for reducing GPU memory when a mesh has thousands of shared vertices.
// DrawPrimitives — sequential, no index buffer
gd.setVertexBuffer(vertexBuffer);
gd.DrawPrimitives(PrimitiveType::TriangleList,
/*startVertex=*/0,
/*primitiveCount=*/numTriangles);
// DrawIndexedPrimitives — indexed, requires an IndexBuffer bound
gd.setVertexBuffer(vertexBuffer);
gd.setIndexBuffer(indexBuffer);
gd.DrawIndexedPrimitives(PrimitiveType::TriangleList,
/*baseVertex=*/0,
/*startIndex=*/0,
/*primitiveCount=*/numTriangles);
Triangle count calculation
The primitiveCount parameter to DrawPrimitives is not the vertex count. Use these formulas:
| PrimitiveType | primitiveCount from N vertices |
|---|---|
| TriangleList | N / 3 |
| TriangleStrip | N - 2 |
| LineList | N / 2 |
| LineStrip | N - 1 |
| PointList | N |
Code example: debug grid with LineList and a triangle-strip ribbon
#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/Graphics/GraphicsDeviceManager.hpp"
#include "Microsoft/Xna/Framework/Graphics/BasicEffect.hpp"
#include "Microsoft/Xna/Framework/Graphics/VertexBuffer.hpp"
#include "Microsoft/Xna/Framework/Graphics/VertexPositionColor.hpp"
using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Graphics;
class PrimitiveDemo final : public Game {
public:
PrimitiveDemo() : graphics_(this) {
graphics_.setPreferredBackBufferWidth(800);
graphics_.setPreferredBackBufferHeight(600);
}
protected:
void LoadContent() override {
effect_ = std::make_unique<BasicEffect>(getGraphicsDeviceProperty());
effect_->setVertexColorEnabled(true);
// --- Grid: LineList ---
// 11 horizontal + 11 vertical lines = 22 lines = 44 vertices
std::vector<VertexPositionColor> gridVerts;
gridVerts.reserve(44);
for (int i = 0; i <= 10; ++i) {
float x = -1.0f + i * 0.2f;
gridVerts.push_back({ Vector3(x, -1.0f, 0.0f), Color(80, 80, 80) });
gridVerts.push_back({ Vector3(x, 1.0f, 0.0f), Color(80, 80, 80) });
}
for (int i = 0; i <= 10; ++i) {
float y = -1.0f + i * 0.2f;
gridVerts.push_back({ Vector3(-1.0f, y, 0.0f), Color(80, 80, 80) });
gridVerts.push_back({ Vector3( 1.0f, y, 0.0f), Color(80, 80, 80) });
}
gridLineCount_ = static_cast<int>(gridVerts.size()) / 2; // primitiveCount
gridVB_ = std::make_unique<VertexBuffer>(
getGraphicsDeviceProperty(),
VertexPositionColor::VertexDeclaration,
static_cast<int>(gridVerts.size()),
BufferUsage::None);
gridVB_->SetData(gridVerts.data(), static_cast<int>(gridVerts.size()));
// --- Ribbon: TriangleStrip ---
// A sine-wave ribbon with alternating top/bottom vertices
std::vector<VertexPositionColor> ribbonVerts;
const int steps = 40;
for (int i = 0; i <= steps; ++i) {
float t = static_cast<float>(i) / steps;
float x = -0.9f + t * 1.8f;
float cy = std::sin(t * MathHelper::TwoPi) * 0.3f;
ribbonVerts.push_back({ Vector3(x, cy + 0.05f, 0.0f), Color::Yellow });
ribbonVerts.push_back({ Vector3(x, cy - 0.05f, 0.0f), Color::Orange });
}
ribbonVertCount_ = static_cast<int>(ribbonVerts.size());
ribbonVB_ = std::make_unique<VertexBuffer>(
getGraphicsDeviceProperty(),
VertexPositionColor::VertexDeclaration,
ribbonVertCount_,
BufferUsage::None);
ribbonVB_->SetData(ribbonVerts.data(), ribbonVertCount_);
}
void Update(const GameTime&) override {}
void Draw(const GameTime&) override {
auto& gd = getGraphicsDeviceProperty();
gd.Clear(Color::Black);
effect_->setWorld(Matrix::Identity);
effect_->setView(Matrix::CreateLookAt(
Vector3(0, 0, 2), Vector3::Zero, Vector3::Up));
effect_->setProjection(Matrix::CreateOrthographic(2.0f, 1.5f, 0.1f, 10.0f));
// Draw grid (LineList)
gd.setVertexBuffer(*gridVB_);
for (auto& pass : effect_->getCurrentTechnique().Passes) {
pass.Apply();
gd.DrawPrimitives(PrimitiveType::LineList, 0, gridLineCount_);
}
// Draw ribbon (TriangleStrip)
// primitiveCount = vertexCount - 2 for a strip
gd.setVertexBuffer(*ribbonVB_);
for (auto& pass : effect_->getCurrentTechnique().Passes) {
pass.Apply();
gd.DrawPrimitives(PrimitiveType::TriangleStrip, 0, ribbonVertCount_ - 2);
}
gd.Present();
}
private:
GraphicsDeviceManager graphics_;
std::unique_ptr<BasicEffect> effect_;
std::unique_ptr<VertexBuffer> gridVB_;
std::unique_ptr<VertexBuffer> ribbonVB_;
int gridLineCount_ = 0;
int ribbonVertCount_ = 0;
};
int main() { PrimitiveDemo game; game.Run(); }
Key takeaways
- Pass primitive count, not vertex count, to
DrawPrimitives. - Counter-clockwise winding is front-facing in CNA (matching XNA and OpenGL).
- Use
DrawIndexedPrimitivesfor meshes with shared vertices to save GPU bandwidth. - Disable culling with
RasterizerState::CullNonefor 2D sprites or double-sided geometry.