Shapes are great for roughing out mechanics, but at some point you’ll want real art.
That’s where Textures come in.
Textures are GPU-friendly images that you can draw, flip, rotate, and scale. Think of them as a prepared image the GPU keeps on hand (VRAM). You upload it once, then tell the renderer where and how to place it each frame.
Creating Textures
Loading an image into GPU memory isn’t free. It’s best to load once and reuse rather than creating textures on the fly:
texture = kn.Texture("image.png")
while kn.window.is_open():
# Reuse texture herekn::Texture texture("image.png");
while (kn::window::isOpen()) {
// Reuse texture here
}Drawing Textures
The simplest draw call renders the texture at the origin (0, 0).
Use a Transform to control position, rotation, and scale:
# Draw at origin with default transform
kn.renderer.draw(texture)
# Draw with custom transform
transform = kn.Transform(
pos=kn.Vec2(150, 100),
angle=30 * kn.math.DEG2RAD
)
kn.renderer.draw(texture, transform)// Draw at origin with default transform
kn::renderer::draw(texture);
// Draw with custom transform
kn::Transform transform = {
.pos = {150, 100},
.angle = kn::math::toRadians(30)
};
kn::renderer::draw(texture, transform);Result:
Batching
Modern SDL3 backends (which we use) batch draw calls automatically if you draw the same texture consecutively. This keeps the GPU from doing expensive state changes.
# Best performance:
for i in range(5):
transform = kn.Transform(pos=kn.Vec2(i * 40, 100))
kn.renderer.draw(texture, transform)
# Do sparingly:
kn.renderer.draw(texA)
kn.renderer.draw(texB)
kn.renderer.draw(texA)// Group calls that use the same texture.
for (int i = 0; i < 5; ++i) {
kn::Transform transform = { .pos = {i * 40, 100} };
kn::renderer::draw(texture, transform);
}
// Changing textures may interrupt batching.
kn::renderer::draw(texA);
kn::renderer::draw(texB);
kn::renderer::draw(texA);So, wherever possible, group similar textures together when drawing.
Atlases
If your game uses lots of small sprites (UI icons, sprite states, etc.), pack them into a single large texture called an atlas. This means:
- Only one texture bind (fewer GPU state changes)
- Faster draws when sprites share the same atlas
The renderer accepts a source rectangle to select a portion of the texture to draw.
# Load the atlas texture
atlas_texture = kn.Texture("atlas.png")
heart_src = kn.Rect(0, 0, 32, 32)
heart_transform = kn.Transform(pos=kn.Vec2(16, 16))
power_src = kn.Rect(32, 0, 32, 32)
power_transform = kn.Transform(pos=kn.Vec2(32, 32))
while kn.window.is_open():
...
# Draw heart icon from atlas
kn.renderer.draw(atlas_texture, heart_transform, source=heart_src)
# Draw power icon from atlas
kn.renderer.draw(atlas_texture, power_transform, source=power_src)
...// Load the atlas texture
kn::Texture atlasTexture("atlas.png");
kn::Rect heartSrc(0, 0, 32, 32);
kn::Transform heartTransform = { .pos = {16, 16} };
kn::Rect powerSrc(32, 0, 32, 32);
kn::Transform powerTransform = { .pos = {32, 32} };
while (kn::window::isOpen()) {
...
// Draw heart icon from atlas
kn::renderer::draw(atlasTexture, heartTransform, { .source = heartSrc });
// Draw power icon from atlas
kn::renderer::draw(atlasTexture, powerTransform, { .source = powerSrc });
...
}Texture atlas:
Result:
