Tutorial 08: Loading and Drawing Textures

CNA Tutorial Series  ·  Beginner

ℹ

What you’ll learn

  • Loading a Texture2D through ContentManager.
  • Why CNA needs no separate texture descriptor file.
  • The SpriteBatch::Draw overloads, and flipping with SpriteEffects.

Before you start — Tutorial 06: Drawing Your First 2D Shape for the SpriteBatch workflow, and Tutorial 07: Working with Colors for the tint parameter.

Texture2D Overview

Texture2D represents a 2D image stored on the GPU. In CNA you load image files (PNG, JPEG, BMP) and use SpriteBatch::Draw() to render them. Key properties:

#include "Microsoft/Xna/Framework/Graphics/Texture2D.hpp"

// After loading:
int w = texture.getWidthProperty();   // width in pixels
int h = texture.getHeightProperty();  // height in pixels

Texture2D is a value type: Load<Texture2D> returns it by value, and you keep it in a plain Texture2D member of your game class. Copies share the same GPU texture, and the GPU memory is released when the last copy goes away (or when you call Dispose()). Pass it to SpriteBatch::Draw by reference.

Loading Textures

CNA supports two loading paths:

Option A: Direct file load (simplest)

Pass a file path directly to the Texture2D constructor (a CNA extension; XNA has no such constructor):

void MyGame::LoadContent() {
    spriteBatch_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());

    // Load a PNG from the assets/ directory.
    // A relative path is resolved against the process's working directory.
    player_ = Texture2D("assets/player.png", getGraphicsDeviceProperty());

    background_ = Texture2D("assets/background.png", getGraphicsDeviceProperty());
}

Option B: ContentManager (XNA-style)

The game’s ContentManager, returned by getContentProperty(), provides the XNA-style asset pipeline. It caches assets and resolves paths relative to a root directory (Content by default):

MyGame::MyGame() : graphics_(this) {
    // Set the root directory for content. Do it in the constructor (or before
    // the Game::Initialize() call): the base Initialize() is what calls LoadContent().
    getContentProperty().setRootDirectoryProperty("assets");
}

void MyGame::LoadContent() {
    spriteBatch_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());

    // Loads assets/player.png — the extension is resolved automatically
    playerTex_ = getContentProperty().Load<Texture2D>("player");

    // Loads assets/ui/button.png
    buttonTex_ = getContentProperty().Load<Texture2D>("ui/button");
}

Declare the members as plain values: Texture2D playerTex_, buttonTex_;. The ContentManager works down a load ladder: for the name player it first looks for a compiled player.xnb, then a player.cnb, and only then a loose image file, resolving the name against the image formats CNA can decode. A missing or undecodable asset throws ContentLoadException.

⚠

A loose PNG is straight alpha, not premultiplied. Both Load<Texture2D> on a .png/.jpg and the direct Texture2D("file.png", device) constructor decode the pixels exactly as stored. Only content built by the XNA pipeline (.xnb) or by CNA’s cna-content tool (.cnb, premultiplied by default) is premultiplied. The default SpriteBatch::Begin() blend, AlphaBlend, expects premultiplied pixels, so a transparent PNG drawn that way shows bright or dark fringes around its edges. Draw loose PNGs with BlendState::NonPremultiplied — see Tutorial 22.

There Is No Texture Descriptor

Textures have no sidecar or descriptor file. Load<Texture2D> resolves your asset name against .png, .jpg, .jpeg, .bmp, .gif, .tga, .tif, .tiff and .qoi, and decodes the first one it finds. There is nothing else to configure at load time.

Filtering and wrapping are not properties of the asset. As in XNA, they are sampler state on the graphics device, applied when you draw — which means the same texture can be sampled differently in different batches:

#include "Microsoft/Xna/Framework/Graphics/SamplerState.hpp"

auto& gd = getGraphicsDeviceProperty();

// Point sampling for a crisp pixel-art look:
gd.getSamplerStatesProperty()[0] = SamplerState::PointClamp;

// Or tile a texture across a larger quad:
gd.getSamplerStatesProperty()[0] = SamplerState::LinearWrap;

For 2D drawing, though, SpriteBatch::Begin() applies its own sampler state (LinearClamp unless you pass another), so the usual way to choose point sampling for sprites is to pass it to Begin():

// Sort mode, blend state, sampler state (const SamplerState*), depth-stencil, rasterizer
spriteBatch_->Begin(SpriteSortMode::Deferred, BlendState::AlphaBlend,
                    &SamplerState::PointClamp, nullptr, nullptr);

Draw Overloads

SpriteBatch::Draw() has several overloads with increasing control. Here they are from simplest to most complete:

1. Position only

// Draw at (x, y), no tint (White), full texture
spriteBatch_->Draw(tex_, Vector2(100.0f, 200.0f), Color::White);

2. Position with tint

// Draw tinted red at position
spriteBatch_->Draw(tex_, Vector2(100.0f, 200.0f), Color::Red);

3. Destination rectangle

// Stretch texture to fill a 200x150 rectangle at (50,50)
spriteBatch_->Draw(tex_, Rectangle(50, 50, 200, 150), Color::White);

4. Source rectangle (draw sub-region)

// Draw only the top-left 32x32 pixels of the texture
Rectangle src(0, 0, 32, 32);
spriteBatch_->Draw(tex_, Vector2(100.0f, 100.0f), src, Color::White);

5. Full control overload

// Full signature:
// Draw(texture, position, sourceRect, color, rotation, origin, scale, effects, depth)
Vector2   position(400.0f, 300.0f);   // screen position
Rectangle source(0, 0, 64, 64);       // source region (std::nullopt for whole texture)
Color     color   = Color::White;
float     rotation = 0.5f;            // radians
Vector2   origin(32.0f, 32.0f);       // rotation pivot (centre of 64x64 sprite)
float     scale   = 2.0f;             // 2x size
SpriteEffects fx  = SpriteEffects::None;
float     depth   = 0.0f;             // z-depth for sorting (0=front, 1=back)

spriteBatch_->Draw(tex_, position, source, color,
                   rotation, origin, scale, fx, depth);

Parameters explained:

ParameterTypeDescription
positionVector2Top-left (or origin point) on screen
sourceRectangleRectangle (or std::nullopt)Region of the texture to draw; std::nullopt for the whole texture
colorColorTint color; White = no tint
rotationfloatRotation in radians, clockwise
originVector2Pivot point in texture space (pixels); (0,0) = top-left
scalefloat or Vector2Scale multiplier (1.0 = original size)
effectsSpriteEffectsFlip flags
layerDepthfloat0..1, used with BackToFront / FrontToBack sorting

Practical examples

// Draw a sprite centred at screen centre, rotated, 2x scale
auto& vp = getGraphicsDeviceProperty().getViewportProperty();
Vector2 screenCentre(vp.getWidthProperty() / 2.0f, vp.getHeightProperty() / 2.0f);
Vector2 texCentre(tex_.getWidthProperty() / 2.0f, tex_.getHeightProperty() / 2.0f);

spriteBatch_->Draw(tex_, screenCentre, std::nullopt, Color::White,
                   rotation_, texCentre, 2.0f, SpriteEffects::None, 0.0f);

// Update rotation in Update():
rotation_ += MathHelper::Pi * dt;  // rotate 180 degrees per second
// Non-uniform scale: stretch width only
Vector2 scaleXY(3.0f, 1.0f);  // triple width, normal height
spriteBatch_->Draw(tex_, Vector2(50.0f, 50.0f), std::nullopt,
                   Color::White, 0.0f, Vector2::Zero, scaleXY,
                   SpriteEffects::None, 0.0f);

SpriteEffects: Flipping

SpriteEffects is an enum used to flip sprites horizontally or vertically — essential for characters that face both left and right:

#include "Microsoft/Xna/Framework/Graphics/SpriteEffects.hpp"

// No flip (default)
SpriteEffects::None

// Mirror horizontally (flip left-right) — for a character facing left
SpriteEffects::FlipHorizontally

// Mirror vertically (flip up-down) — less common
SpriteEffects::FlipVertically

// Flip both axes: there is no FlipBoth enumerator; combine the two flags
SpriteEffects::FlipHorizontally | SpriteEffects::FlipVertically

Typical usage — a character that faces the direction of movement:

// In Update():
if (velocity_.X < 0.0f) {
    facingLeft_ = true;
} else if (velocity_.X > 0.0f) {
    facingLeft_ = false;
}

// In Draw():
SpriteEffects flip = facingLeft_
    ? SpriteEffects::FlipHorizontally
    : SpriteEffects::None;

spriteBatch_->Draw(playerTex_, position_, std::nullopt, Color::White,
                   0.0f, Vector2::Zero, 1.0f, flip, 0.0f);

You now know how to load image files and draw them with full control. In Tutorial 09 you will add text to your game using SpriteFont.