feat: Introduce Dungeon Object Emulator Preview and Object Drawing Enhancements
- Added DungeonObjectEmulatorPreview for rendering dungeon objects using the SNES emulator, allowing real-time visualization of object graphics. - Implemented ObjectDrawer class to handle drawing of various object types to background buffers, utilizing game-specific patterns. - Updated DungeonCanvasViewer to integrate object rendering and improve background layer management. - Enhanced DungeonEditorV2 to support the new emulator preview, providing a more interactive editing experience. - Improved error handling and logging for better debugging during object rendering operations.
This commit is contained in:
220
src/app/zelda3/dungeon/object_drawer.cc
Normal file
220
src/app/zelda3/dungeon/object_drawer.cc
Normal file
@@ -0,0 +1,220 @@
|
||||
#include "object_drawer.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "app/gfx/snes_tile.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace zelda3 {
|
||||
|
||||
ObjectDrawer::ObjectDrawer(Rom* rom) : rom_(rom) {}
|
||||
|
||||
absl::Status ObjectDrawer::DrawObject(const RoomObject& object,
|
||||
gfx::BackgroundBuffer& bg1,
|
||||
gfx::BackgroundBuffer& bg2) {
|
||||
if (!rom_ || !rom_->is_loaded()) {
|
||||
return absl::FailedPreconditionError("ROM not loaded");
|
||||
}
|
||||
|
||||
// Ensure object has tiles loaded
|
||||
auto mutable_obj = const_cast<RoomObject&>(object);
|
||||
mutable_obj.set_rom(rom_);
|
||||
mutable_obj.EnsureTilesLoaded();
|
||||
|
||||
// Get tiles - silently skip objects that can't load tiles
|
||||
if (object.tiles().empty()) {
|
||||
// Many objects may not have tiles loaded yet - this is normal
|
||||
// Just skip them rather than failing the whole draw operation
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
const auto& tile = object.tiles()[0]; // Base tile for object
|
||||
|
||||
// Select buffer based on layer
|
||||
auto& target_bg = (object.layer_ == RoomObject::LayerType::BG2) ? bg2 : bg1;
|
||||
|
||||
// Dispatch to pattern-specific drawing based on object ID
|
||||
// This is reverse-engineered from the game's drawing routines
|
||||
|
||||
if (object.id_ == 0x34) {
|
||||
// Object 0x34: 1x1 solid block (simplest)
|
||||
Draw1x1Solid(object, target_bg, tile);
|
||||
}
|
||||
else if (object.id_ >= 0x00 && object.id_ <= 0x08) {
|
||||
// Objects 0x00-0x08: Rightward 2x2 patterns
|
||||
DrawRightwards2x2(object, target_bg, tile);
|
||||
}
|
||||
else if (object.id_ >= 0x60 && object.id_ <= 0x68) {
|
||||
// Objects 0x60-0x68: Downward 2x2 patterns
|
||||
DrawDownwards2x2(object, target_bg, tile);
|
||||
}
|
||||
else if (object.id_ >= 0x09 && object.id_ <= 0x14) {
|
||||
// Objects 0x09-0x14: Diagonal acute patterns
|
||||
DrawDiagonalAcute(object, target_bg, tile);
|
||||
}
|
||||
else if (object.id_ >= 0x15 && object.id_ <= 0x20) {
|
||||
// Objects 0x15-0x20: Diagonal grave patterns
|
||||
DrawDiagonalGrave(object, target_bg, tile);
|
||||
}
|
||||
else if (object.id_ == 0x33 || (object.id_ >= 0x70 && object.id_ <= 0x71)) {
|
||||
// 4x4 block objects
|
||||
Draw4x4Block(object, target_bg, tile);
|
||||
}
|
||||
else {
|
||||
// Default: Draw as simple 1x1 at position
|
||||
Draw1x1Solid(object, target_bg, tile);
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status ObjectDrawer::DrawObjectList(
|
||||
const std::vector<RoomObject>& objects,
|
||||
gfx::BackgroundBuffer& bg1,
|
||||
gfx::BackgroundBuffer& bg2) {
|
||||
|
||||
int drawn_count = 0;
|
||||
int skipped_count = 0;
|
||||
|
||||
for (const auto& object : objects) {
|
||||
auto status = DrawObject(object, bg1, bg2);
|
||||
if (status.ok()) {
|
||||
drawn_count++;
|
||||
} else {
|
||||
skipped_count++;
|
||||
// Only print errors that aren't "no tiles" (which is common and expected)
|
||||
if (status.code() != absl::StatusCode::kOk) {
|
||||
// Skip silently - many objects don't have tiles loaded yet
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (drawn_count > 0 || skipped_count > 0) {
|
||||
printf("[ObjectDrawer] Drew %d objects, skipped %d\n", drawn_count, skipped_count);
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Pattern Drawing Implementations
|
||||
// ============================================================================
|
||||
|
||||
void ObjectDrawer::Draw1x1Solid(const RoomObject& obj,
|
||||
gfx::BackgroundBuffer& bg,
|
||||
const gfx::Tile16& tile) {
|
||||
// Simple 1x1 tile placement
|
||||
WriteTile16(bg, obj.x_, obj.y_, tile);
|
||||
}
|
||||
|
||||
void ObjectDrawer::DrawRightwards2x2(const RoomObject& obj,
|
||||
gfx::BackgroundBuffer& bg,
|
||||
const gfx::Tile16& tile) {
|
||||
// Pattern: Draws 2x2 tiles rightward
|
||||
// Size byte determines how many times to repeat
|
||||
int repeat_count = (obj.size_ & 0x0F) + 1; // Low nibble = width
|
||||
|
||||
for (int i = 0; i < repeat_count; i++) {
|
||||
// Each iteration draws a 2x2 tile16
|
||||
int tile_x = obj.x_ + (i * 2); // Each tile16 is 2x2 8x8 tiles
|
||||
int tile_y = obj.y_;
|
||||
|
||||
WriteTile16(bg, tile_x, tile_y, tile);
|
||||
}
|
||||
}
|
||||
|
||||
void ObjectDrawer::DrawDownwards2x2(const RoomObject& obj,
|
||||
gfx::BackgroundBuffer& bg,
|
||||
const gfx::Tile16& tile) {
|
||||
// Pattern: Draws 2x2 tiles downward
|
||||
// Size byte determines how many times to repeat
|
||||
int repeat_count = ((obj.size_ >> 4) & 0x0F) + 1; // High nibble = height
|
||||
|
||||
for (int i = 0; i < repeat_count; i++) {
|
||||
int tile_x = obj.x_;
|
||||
int tile_y = obj.y_ + (i * 2);
|
||||
|
||||
WriteTile16(bg, tile_x, tile_y, tile);
|
||||
}
|
||||
}
|
||||
|
||||
void ObjectDrawer::DrawDiagonalAcute(const RoomObject& obj,
|
||||
gfx::BackgroundBuffer& bg,
|
||||
const gfx::Tile16& tile) {
|
||||
// Pattern: Diagonal line going down-right (/)
|
||||
int length = (obj.size_ & 0x0F) + 1;
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
int tile_x = obj.x_ + i;
|
||||
int tile_y = obj.y_ + i;
|
||||
|
||||
WriteTile16(bg, tile_x, tile_y, tile);
|
||||
}
|
||||
}
|
||||
|
||||
void ObjectDrawer::DrawDiagonalGrave(const RoomObject& obj,
|
||||
gfx::BackgroundBuffer& bg,
|
||||
const gfx::Tile16& tile) {
|
||||
// Pattern: Diagonal line going down-left (\)
|
||||
int length = (obj.size_ & 0x0F) + 1;
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
int tile_x = obj.x_ - i;
|
||||
int tile_y = obj.y_ + i;
|
||||
|
||||
WriteTile16(bg, tile_x, tile_y, tile);
|
||||
}
|
||||
}
|
||||
|
||||
void ObjectDrawer::Draw4x4Block(const RoomObject& obj,
|
||||
gfx::BackgroundBuffer& bg,
|
||||
const gfx::Tile16& tile) {
|
||||
// Pattern: 4x4 tile16 block (8x8 8x8 tiles total)
|
||||
for (int yy = 0; yy < 4; yy++) {
|
||||
for (int xx = 0; xx < 4; xx++) {
|
||||
int tile_x = obj.x_ + (xx * 2);
|
||||
int tile_y = obj.y_ + (yy * 2);
|
||||
|
||||
WriteTile16(bg, tile_x, tile_y, tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Utility Methods
|
||||
// ============================================================================
|
||||
|
||||
void ObjectDrawer::WriteTile16(gfx::BackgroundBuffer& bg, int tile_x, int tile_y,
|
||||
const gfx::Tile16& tile) {
|
||||
// A Tile16 is 2x2 8x8 tiles, so we write 4 tile entries
|
||||
|
||||
// Top-left (tile0)
|
||||
if (IsValidTilePosition(tile_x, tile_y)) {
|
||||
bg.SetTileAt(tile_x, tile_y, gfx::TileInfoToWord(tile.tile0_));
|
||||
}
|
||||
|
||||
// Top-right (tile1)
|
||||
if (IsValidTilePosition(tile_x + 1, tile_y)) {
|
||||
bg.SetTileAt(tile_x + 1, tile_y, gfx::TileInfoToWord(tile.tile1_));
|
||||
}
|
||||
|
||||
// Bottom-left (tile2)
|
||||
if (IsValidTilePosition(tile_x, tile_y + 1)) {
|
||||
bg.SetTileAt(tile_x, tile_y + 1, gfx::TileInfoToWord(tile.tile2_));
|
||||
}
|
||||
|
||||
// Bottom-right (tile3)
|
||||
if (IsValidTilePosition(tile_x + 1, tile_y + 1)) {
|
||||
bg.SetTileAt(tile_x + 1, tile_y + 1, gfx::TileInfoToWord(tile.tile3_));
|
||||
}
|
||||
}
|
||||
|
||||
bool ObjectDrawer::IsValidTilePosition(int tile_x, int tile_y) const {
|
||||
return tile_x >= 0 && tile_x < kMaxTilesX &&
|
||||
tile_y >= 0 && tile_y < kMaxTilesY;
|
||||
}
|
||||
|
||||
} // namespace zelda3
|
||||
} // namespace yaze
|
||||
|
||||
84
src/app/zelda3/dungeon/object_drawer.h
Normal file
84
src/app/zelda3/dungeon/object_drawer.h
Normal file
@@ -0,0 +1,84 @@
|
||||
#ifndef YAZE_APP_ZELDA3_DUNGEON_OBJECT_DRAWER_H
|
||||
#define YAZE_APP_ZELDA3_DUNGEON_OBJECT_DRAWER_H
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "app/gfx/background_buffer.h"
|
||||
#include "app/gfx/snes_tile.h"
|
||||
#include "app/rom.h"
|
||||
#include "app/zelda3/dungeon/room_object.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace zelda3 {
|
||||
|
||||
/**
|
||||
* @brief Draws dungeon objects to background buffers using game patterns
|
||||
*
|
||||
* This class interprets object IDs and draws them to BG1/BG2 buffers
|
||||
* using the patterns extracted from the game's drawing routines.
|
||||
*
|
||||
* Architecture:
|
||||
* 1. Load tile data from ROM for the object
|
||||
* 2. Determine drawing pattern (rightward, downward, diagonal, special)
|
||||
* 3. Write tiles to BackgroundBuffer according to pattern
|
||||
* 4. Handle size bytes for repeating patterns
|
||||
*/
|
||||
class ObjectDrawer {
|
||||
public:
|
||||
explicit ObjectDrawer(Rom* rom);
|
||||
|
||||
/**
|
||||
* @brief Draw a room object to background buffers
|
||||
* @param object The object to draw
|
||||
* @param bg1 Background layer 1 buffer
|
||||
* @param bg2 Background layer 2 buffer
|
||||
* @return Status of the drawing operation
|
||||
*/
|
||||
absl::Status DrawObject(const RoomObject& object,
|
||||
gfx::BackgroundBuffer& bg1,
|
||||
gfx::BackgroundBuffer& bg2);
|
||||
|
||||
/**
|
||||
* @brief Draw all objects in a room
|
||||
* @param objects Vector of room objects
|
||||
* @param bg1 Background layer 1 buffer
|
||||
* @param bg2 Background layer 2 buffer
|
||||
* @return Status of the drawing operation
|
||||
*/
|
||||
absl::Status DrawObjectList(const std::vector<RoomObject>& objects,
|
||||
gfx::BackgroundBuffer& bg1,
|
||||
gfx::BackgroundBuffer& bg2);
|
||||
|
||||
private:
|
||||
// Pattern-specific drawing methods
|
||||
void DrawRightwards2x2(const RoomObject& obj, gfx::BackgroundBuffer& bg,
|
||||
const gfx::Tile16& tile);
|
||||
void DrawDownwards2x2(const RoomObject& obj, gfx::BackgroundBuffer& bg,
|
||||
const gfx::Tile16& tile);
|
||||
void DrawDiagonalAcute(const RoomObject& obj, gfx::BackgroundBuffer& bg,
|
||||
const gfx::Tile16& tile);
|
||||
void DrawDiagonalGrave(const RoomObject& obj, gfx::BackgroundBuffer& bg,
|
||||
const gfx::Tile16& tile);
|
||||
void Draw1x1Solid(const RoomObject& obj, gfx::BackgroundBuffer& bg,
|
||||
const gfx::Tile16& tile);
|
||||
void Draw4x4Block(const RoomObject& obj, gfx::BackgroundBuffer& bg,
|
||||
const gfx::Tile16& tile);
|
||||
|
||||
// Utility methods
|
||||
void WriteTile16(gfx::BackgroundBuffer& bg, int tile_x, int tile_y,
|
||||
const gfx::Tile16& tile);
|
||||
bool IsValidTilePosition(int tile_x, int tile_y) const;
|
||||
|
||||
Rom* rom_;
|
||||
|
||||
// Canvas dimensions in tiles (64x64 = 512x512 pixels)
|
||||
static constexpr int kMaxTilesX = 64;
|
||||
static constexpr int kMaxTilesY = 64;
|
||||
};
|
||||
|
||||
} // namespace zelda3
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_ZELDA3_DUNGEON_OBJECT_DRAWER_H
|
||||
|
||||
@@ -310,30 +310,38 @@ void Room::RenderRoomGraphics() {
|
||||
int num_palettes = dungeon_pal_group.size();
|
||||
int palette_id = palette;
|
||||
|
||||
std::printf("5a. Dungeon palette group has %d palettes total\n", num_palettes);
|
||||
|
||||
// Validate palette ID and fall back to palette 0 if invalid
|
||||
if (palette_id < 0 || palette_id >= num_palettes) {
|
||||
//palette_id = 0;
|
||||
std::printf("5a. WARNING: palette_id %d is out of bounds [0, %d), using palette 0\n",
|
||||
palette_id, num_palettes);
|
||||
palette_id = 0;
|
||||
}
|
||||
|
||||
// Load the 90-color dungeon palette directly
|
||||
// The palette contains colors for BG layers - sprite colors are handled separately
|
||||
auto bg1_palette = dungeon_pal_group.palette(palette_id);
|
||||
auto bg1_palette = dungeon_pal_group[palette_id]; // Use operator[] to get a proper reference
|
||||
|
||||
std::printf("5a. Palette loaded: room palette_id=%d (requested=%d), size=%zu colors\n",
|
||||
palette_id, palette, bg1_palette.size());
|
||||
|
||||
// CRITICAL: Apply palette to bitmaps BEFORE creating/updating textures
|
||||
bg1_bmp.SetPaletteWithTransparent(bg1_palette, 0);
|
||||
bg2_bmp.SetPaletteWithTransparent(bg1_palette, 0);
|
||||
std::printf("5b. Palette applied to bitmaps\n");
|
||||
// CRITICAL: Only apply palette if it's valid
|
||||
if (bg1_palette.size() > 0) {
|
||||
bg1_bmp.SetPaletteWithTransparent(bg1_palette, 0);
|
||||
bg2_bmp.SetPaletteWithTransparent(bg1_palette, 0);
|
||||
std::printf("5b. Palette applied to bitmaps\n");
|
||||
} else {
|
||||
std::printf("5b. WARNING: Palette is empty, skipping SetPalette\n");
|
||||
}
|
||||
|
||||
// ALWAYS recreate textures when palette changes (UpdateBitmap doesn't update palette!)
|
||||
std::printf("6. Recreating bitmap textures with new palette\n");
|
||||
core::Renderer::Get().CreateAndRenderBitmap(
|
||||
0x200, 0x200, 0x200, gfx::Arena::Get().bg1().bitmap().vector(),
|
||||
0x200, 0x200, 8, gfx::Arena::Get().bg1().bitmap().vector(),
|
||||
gfx::Arena::Get().bg1().bitmap(), bg1_palette);
|
||||
core::Renderer::Get().CreateAndRenderBitmap(
|
||||
0x200, 0x200, 0x200, gfx::Arena::Get().bg2().bitmap().vector(),
|
||||
0x200, 0x200, 8, gfx::Arena::Get().bg2().bitmap().vector(),
|
||||
gfx::Arena::Get().bg2().bitmap(), bg1_palette);
|
||||
|
||||
std::printf("7. BG1 has texture: %d\n", bg1_bmp.texture() != nullptr);
|
||||
|
||||
@@ -347,6 +347,7 @@ class Room {
|
||||
auto& mutable_blocks() { return blocks_; }
|
||||
auto rom() { return rom_; }
|
||||
auto mutable_rom() { return rom_; }
|
||||
const std::array<uint8_t, 0x4000>& get_gfx_buffer() const { return current_gfx16_; }
|
||||
|
||||
private:
|
||||
Rom* rom_;
|
||||
|
||||
@@ -14,6 +14,7 @@ set(
|
||||
app/zelda3/dungeon/room_object.cc
|
||||
app/zelda3/dungeon/object_parser.cc
|
||||
app/zelda3/dungeon/object_renderer.cc
|
||||
app/zelda3/dungeon/object_drawer.cc
|
||||
app/zelda3/dungeon/room_layout.cc
|
||||
app/zelda3/dungeon/room_diagnostic.cc
|
||||
app/zelda3/dungeon/room_visual_diagnostic.cc
|
||||
|
||||
Reference in New Issue
Block a user