加入 SpriteRegion 与基础 Tilemap 绘制

DrawContext 现在支持从 atlas 子区域绘制 SpriteRegion,并在 draw_sprite_ex 中加入屏幕裁剪,避免边缘 sprite 逐像素依赖 FrameBuffer 越界保护。新增基础 Tilemap 数据结构和 draw_tilemap,按视口可见范围遍历 tile,并在视口边缘进行像素级裁剪,支持 camera 像素级滚动时显示半个 tile。

Demo 增加小型滚动 tilemap 测试,复用现有测试 sprite 作为单 tile atlas。文档同步记录 SpriteRegion、Tilemap、viewport 裁剪语义和后续优化方向。

Constraint: IMX6U 热路径应避免全地图扫描和运行时资源解码
Constraint: 当前 tilemap 先保持单 atlas、固定 tile 尺寸的简单模型
Rejected: 直接每帧遍历整张 tilemap | 大地图上会浪费 CPU
Rejected: 立即引入复杂地图资源系统 | 当前阶段只需要验证绘制链路
Confidence: high
Scope-risk: moderate
Directive: 扩展 Tilemap 时保持 tile 范围遍历 + 像素级边缘裁剪的语义
Tested: cmake --build build-win --config Release
Not-tested: 尚未在 IMX6U 真机上验证 framebuffer/SDL 后端性能
This commit is contained in:
SepComet
2026-06-07 09:23:26 +08:00
parent 213fa7e961
commit 20d2422650
8 changed files with 286 additions and 32 deletions
+13 -4
View File
@@ -126,6 +126,16 @@ int main(int argc, char *argv[])
font.columns = font_columns;
font.first_char = font_first_char;
RenderData::Image sprite_img(test_sprite_pixels, test_sprite_width, test_sprite_height, 0x00000000);
RenderData::SpriteRegion sprite_region(&sprite_img, 0, 0, sprite_img.width, sprite_img.height);
const std::array<uint16_t, 8 * 4> tileIds = {
0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile,
RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0,
0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile,
RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0};
RenderData::Tilemap testTilemap(tileIds.data(), 8, 4, &sprite_img, sprite_img.width, sprite_img.height, 1);
Scene::Camera camera;
camera.transform.position = Math::Vector3(0.0f, 0.0f, 3.0f);
camera.transform.rotation = Math::Vector3(0.0f, 3.1415926535f, 0.0f);
@@ -253,11 +263,10 @@ int main(int argc, char *argv[])
}
// sprite 测试
RenderData::Image sprite_img(test_sprite_pixels, test_sprite_width, test_sprite_height, 0x00000000);
ctx.draw_sprite(10, 10, sprite_img);
ctx.draw_sprite_ex(30, 10, sprite_img, 0, 0, sprite_img.width, sprite_img.height, 2, false, false);
ctx.draw_sprite_ex(10, 30, sprite_img, 0, 0, sprite_img.width, sprite_img.height, 3, true, false);
ctx.draw_sprite_region_ex(30, 10, sprite_region, 2, false, false);
ctx.draw_sprite_region_ex(10, 30, sprite_region, 3, true, false);
ctx.draw_tilemap(testTilemap, 650, 500, 96, 48, static_cast<int32_t>(display->get_time_ms() / 20) % 32, 0);
// FPS 计数
++frame_count;
+164 -19
View File
@@ -60,52 +60,197 @@ namespace Gfx
draw_sprite_region(dst_x, dst_y, img, 0, 0, img.width, img.height);
}
void DrawContext::draw_sprite(int32_t dst_x, int32_t dst_y, const RenderData::SpriteRegion& region)
{
draw_sprite_region(dst_x, dst_y, region);
}
void DrawContext::draw_sprite_region(int32_t dst_x, int32_t dst_y, const RenderData::Image& img,
int32_t src_x, int32_t src_y, int32_t src_w, int32_t src_h)
{
draw_sprite_ex(dst_x, dst_y, img, src_x, src_y, src_w, src_h, 1, false, false);
}
void DrawContext::draw_sprite_region(int32_t dst_x, int32_t dst_y, const RenderData::SpriteRegion& region)
{
draw_sprite_region_ex(dst_x, dst_y, region, 1, false, false);
}
void DrawContext::draw_sprite_region_ex(int32_t dst_x, int32_t dst_y, const RenderData::SpriteRegion& region,
int32_t scale, bool flip_h, bool flip_v)
{
if (!region.atlas) return;
draw_sprite_ex(
dst_x,
dst_y,
*region.atlas,
region.x,
region.y,
region.width,
region.height,
scale,
flip_h,
flip_v);
}
void DrawContext::draw_sprite_ex(int32_t dst_x, int32_t dst_y, const RenderData::Image& img,
int32_t src_x, int32_t src_y, int32_t src_w, int32_t src_h,
int32_t scale, bool flip_h, bool flip_v)
{
if (scale < 1 || !img.pixels) return;
if (scale < 1 || !img.pixels || src_w <= 0 || src_h <= 0) return;
if (src_x < 0 || src_y < 0 || src_x + src_w > img.width || src_y + src_h > img.height) return;
const int32_t img_w = img.width;
const int32_t screen_w = frameBuffer->get_width();
const int32_t screen_h = frameBuffer->get_height();
const int32_t draw_w = src_w * scale;
const int32_t draw_h = src_h * scale;
if (dst_x >= screen_w || dst_y >= screen_h || dst_x + draw_w <= 0 || dst_y + draw_h <= 0) return;
int32_t start_dx = 0;
int32_t start_dy = 0;
int32_t end_dx = draw_w;
int32_t end_dy = draw_h;
if (dst_x < 0) start_dx = -dst_x;
if (dst_y < 0) start_dy = -dst_y;
if (dst_x + end_dx > screen_w) end_dx = screen_w - dst_x;
if (dst_y + end_dy > screen_h) end_dy = screen_h - dst_y;
const uint32_t* src = img.pixels;
const bool has_key = img.has_color_key;
const uint32_t key = img.color_key;
for (int32_t sy = 0; sy < src_h; ++sy)
if (scale == 1)
{
for (int32_t sy = start_dy; sy < end_dy; ++sy)
{
const int32_t read_y = flip_v ? (src_y + src_h - 1 - sy) : (src_y + sy);
const uint32_t* row = src + read_y * img_w;
const int32_t dst_y_abs = dst_y + sy;
for (int32_t sx = start_dx; sx < end_dx; ++sx)
{
const int32_t read_x = flip_h ? (src_x + src_w - 1 - sx) : (src_x + sx);
const uint32_t pixel = row[read_x];
if (has_key && pixel == key) continue;
frameBuffer->set_pixel(dst_x + sx, dst_y_abs, pixel);
}
}
return;
}
for (int32_t dy_abs = start_dy; dy_abs < end_dy; ++dy_abs)
{
const int32_t sy = dy_abs / scale;
const int32_t read_y = flip_v ? (src_y + src_h - 1 - sy) : (src_y + sy);
const uint32_t* row = src + read_y * img_w;
const int32_t dst_y_abs = dst_y + dy_abs;
for (int32_t sx = 0; sx < src_w; ++sx)
for (int32_t dx_abs = start_dx; dx_abs < end_dx; ++dx_abs)
{
const int32_t sx = dx_abs / scale;
const int32_t read_x = flip_h ? (src_x + src_w - 1 - sx) : (src_x + sx);
const uint32_t pixel = row[read_x];
if (has_key && pixel == key) continue;
const int32_t base_x = dst_x + sx * scale;
const int32_t base_y = dst_y + sy * scale;
frameBuffer->set_pixel(dst_x + dx_abs, dst_y_abs, pixel);
}
}
}
if (scale == 1)
{
frameBuffer->set_pixel(base_x, base_y, pixel);
}
else
{
for (int32_t dy = 0; dy < scale; ++dy)
{
for (int32_t dx = 0; dx < scale; ++dx)
{
frameBuffer->set_pixel(base_x + dx, base_y + dy, pixel);
}
}
}
void DrawContext::draw_tilemap(const RenderData::Tilemap& tilemap,
int32_t screen_x, int32_t screen_y,
int32_t camera_x, int32_t camera_y)
{
draw_tilemap(tilemap,
screen_x,
screen_y,
frameBuffer->get_width() - screen_x,
frameBuffer->get_height() - screen_y,
camera_x,
camera_y);
}
void DrawContext::draw_tilemap(const RenderData::Tilemap& tilemap,
int32_t screen_x, int32_t screen_y,
int32_t viewport_w, int32_t viewport_h,
int32_t camera_x, int32_t camera_y)
{
if (!tilemap.tiles || !tilemap.atlas || !tilemap.atlas->pixels) return;
if (tilemap.width <= 0 || tilemap.height <= 0) return;
if (tilemap.tile_w <= 0 || tilemap.tile_h <= 0 || tilemap.atlas_columns <= 0) return;
if (viewport_w <= 0 || viewport_h <= 0) return;
const int32_t viewport_left = screen_x;
const int32_t viewport_top = screen_y;
const int32_t viewport_right = screen_x + viewport_w;
const int32_t viewport_bottom = screen_y + viewport_h;
int32_t start_tile_x = camera_x / tilemap.tile_w;
int32_t start_tile_y = camera_y / tilemap.tile_h;
int32_t offset_x = -(camera_x % tilemap.tile_w);
int32_t offset_y = -(camera_y % tilemap.tile_h);
if (camera_x < 0 && camera_x % tilemap.tile_w != 0)
{
--start_tile_x;
offset_x = -camera_x - (-start_tile_x * tilemap.tile_w);
}
if (camera_y < 0 && camera_y % tilemap.tile_h != 0)
{
--start_tile_y;
offset_y = -camera_y - (-start_tile_y * tilemap.tile_h);
}
const int32_t visible_cols = viewport_w / tilemap.tile_w + 2;
const int32_t visible_rows = viewport_h / tilemap.tile_h + 2;
for (int32_t row = 0; row < visible_rows; ++row)
{
const int32_t map_y = start_tile_y + row;
if (map_y < 0 || map_y >= tilemap.height) continue;
const int32_t dst_y = screen_y + offset_y + row * tilemap.tile_h;
for (int32_t col = 0; col < visible_cols; ++col)
{
const int32_t map_x = start_tile_x + col;
if (map_x < 0 || map_x >= tilemap.width) continue;
const uint16_t tile_id = tilemap.get_tile(map_x, map_y);
if (tile_id == RenderData::Tilemap::EmptyTile) continue;
const int32_t src_x = (tile_id % tilemap.atlas_columns) * tilemap.tile_w;
const int32_t src_y = (tile_id / tilemap.atlas_columns) * tilemap.tile_h;
const int32_t dst_x = screen_x + offset_x + col * tilemap.tile_w;
const int32_t tile_right = dst_x + tilemap.tile_w;
const int32_t tile_bottom = dst_y + tilemap.tile_h;
int32_t clipped_left = dst_x;
int32_t clipped_top = dst_y;
int32_t clipped_right = tile_right;
int32_t clipped_bottom = tile_bottom;
if (clipped_left < viewport_left) clipped_left = viewport_left;
if (clipped_top < viewport_top) clipped_top = viewport_top;
if (clipped_right > viewport_right) clipped_right = viewport_right;
if (clipped_bottom > viewport_bottom) clipped_bottom = viewport_bottom;
if (clipped_left >= clipped_right || clipped_top >= clipped_bottom) continue;
const int32_t clipped_src_x = src_x + (clipped_left - dst_x);
const int32_t clipped_src_y = src_y + (clipped_top - dst_y);
const int32_t clipped_w = clipped_right - clipped_left;
const int32_t clipped_h = clipped_bottom - clipped_top;
draw_sprite_ex(clipped_left, clipped_top, *tilemap.atlas,
clipped_src_x, clipped_src_y, clipped_w, clipped_h,
1, false, false);
}
}
}
+13
View File
@@ -3,6 +3,8 @@
#include "Vector2.h"
#include "Triangle.h"
#include "Image.h"
#include "SpriteRegion.h"
#include "Tilemap.h"
#include "BitmapFont.h"
#include <cstdint>
@@ -50,11 +52,22 @@ namespace Gfx
void draw_triangle(const RenderData::Triangle& triangle, const RenderData::Color& color);
void draw_sprite(int32_t dst_x, int32_t dst_y, const RenderData::Image& img);
void draw_sprite(int32_t dst_x, int32_t dst_y, const RenderData::SpriteRegion& region);
void draw_sprite_region(int32_t dst_x, int32_t dst_y, const RenderData::Image& img,
int32_t src_x, int32_t src_y, int32_t src_w, int32_t src_h);
void draw_sprite_region(int32_t dst_x, int32_t dst_y, const RenderData::SpriteRegion& region);
void draw_sprite_region_ex(int32_t dst_x, int32_t dst_y, const RenderData::SpriteRegion& region,
int32_t scale, bool flip_h, bool flip_v);
void draw_sprite_ex(int32_t dst_x, int32_t dst_y, const RenderData::Image& img,
int32_t src_x, int32_t src_y, int32_t src_w, int32_t src_h,
int32_t scale, bool flip_h, bool flip_v);
void draw_tilemap(const RenderData::Tilemap& tilemap,
int32_t screen_x, int32_t screen_y,
int32_t camera_x, int32_t camera_y);
void draw_tilemap(const RenderData::Tilemap& tilemap,
int32_t screen_x, int32_t screen_y,
int32_t viewport_w, int32_t viewport_h,
int32_t camera_x, int32_t camera_y);
void fill_rect(int32_t x, int32_t y, int32_t w, int32_t h, const RenderData::Color& color);
void draw_text(const RenderData::BitmapFont& font, int32_t x, int32_t y,
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include <cstdint>
#include "Image.h"
namespace RenderData
{
struct SpriteRegion
{
const Image* atlas;
int32_t x;
int32_t y;
int32_t width;
int32_t height;
SpriteRegion()
: atlas(nullptr), x(0), y(0), width(0), height(0) {}
SpriteRegion(const Image* atlas, int32_t x, int32_t y, int32_t width, int32_t height)
: atlas(atlas), x(x), y(y), width(width), height(height) {}
};
}
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include <cstdint>
#include "Image.h"
namespace RenderData
{
struct Tilemap
{
static const uint16_t EmptyTile = 0xFFFF;
const uint16_t* tiles;
int32_t width;
int32_t height;
const Image* atlas;
int32_t tile_w;
int32_t tile_h;
int32_t atlas_columns;
Tilemap()
: tiles(nullptr),
width(0),
height(0),
atlas(nullptr),
tile_w(0),
tile_h(0),
atlas_columns(0)
{
}
Tilemap(const uint16_t* tiles, int32_t width, int32_t height,
const Image* atlas, int32_t tile_w, int32_t tile_h, int32_t atlas_columns)
: tiles(tiles),
width(width),
height(height),
atlas(atlas),
tile_w(tile_w),
tile_h(tile_h),
atlas_columns(atlas_columns)
{
}
uint16_t get_tile(int32_t x, int32_t y) const
{
return tiles[y * width + x];
}
};
}