Add display_animations.py tool for viewing entity animation frames (#1746)

This extends upon my work in display_texture.py. I have used this for
identifying a few entities already. display_texture is suited for
viewing prims, while this one is better for entities that are rendered
by RenderEntities.

I think there's enough documentation at the top of the function for
others to use it. Hopefully it can be helpful, especially as it evolves
over time.

At some point this could be converted to an editor in addition to a
viewer, but for now the viewing capability is the core focus.

Also rewrote some of DRA's sprite data (this should probably be in
sotn-assets though) to fit the standard format so that this tool could
parse the data.
This commit is contained in:
bismurphy 2024-10-06 08:09:33 -04:00 committed by GitHub
parent 50b8135531
commit 02e4c62f6a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 349 additions and 175 deletions

View File

@ -737,7 +737,7 @@ s16 D_800D0EB4[];
s16 D_800D0EE4[];
s16 D_800D0F14[];
s16 D_800D0F30[];
u16 D_800D0F4C[];
s16 D_800D0F4C[];
u16* D_800CFF10[] = {
NULL, D_800D0020, D_800D003C, D_800D0058, D_800D0074, D_800D0090,
@ -1027,9 +1027,8 @@ s16 D_800D0F30[] = {
1, 0, -14, 15, 32, 16, 368, 104, 32, 48, 64, 64, 0, 0,
};
u16 D_800D0F4C[] = {
0x0001, 0x0024, 0xFFC1, 0xFFC1, 0x0080, 0x0080, 0x01A0,
0x0068, 0x0080, 0x0000, 0x0100, 0x0080, 0x0000,
s16 D_800D0F4C[] = {
1, 36, -63, -63, 128, 128, 416, 104, 128, 0, 256, 128, 0,
#ifdef VERSION_US // dirty data
0x5F65,
#endif
@ -2240,177 +2239,110 @@ u16 D_800D2CDC[] = {
#endif
};
u32 D_800D2D5C[];
u16 D_800D2D78[];
u32 D_800D2D94[];
u32 D_800D2DB0[];
u32 D_800D2DCC[];
u32 D_800D2DE8[];
u32 D_800D2E04[];
u32 D_800D2E20[];
u32 D_800D2E3C[];
u32 D_800D2E58[];
u32 D_800D2E74[];
u32 D_800D2E90[];
u32 D_800D2EAC[];
u32 D_800D2EC8[];
u32 D_800D2EE4[];
u32 D_800D2F00[];
u32 D_800D2F1C[];
u32 D_800D2F38[];
u32 D_800D2F54[];
u32 D_800D2F70[];
u32 D_800D2F8C[];
u32 D_800D2FA8[];
u32 D_800D2FC4[];
u16 D_800D2FE0[];
s16 D_800D2D5C[];
s16 D_800D2D78[];
s16 D_800D2D94[];
s16 D_800D2DB0[];
s16 D_800D2DCC[];
s16 D_800D2DE8[];
s16 D_800D2E04[];
s16 D_800D2E20[];
s16 D_800D2E3C[];
s16 D_800D2E58[];
s16 D_800D2E74[];
s16 D_800D2E90[];
s16 D_800D2EAC[];
s16 D_800D2EC8[];
s16 D_800D2EE4[];
s16 D_800D2F00[];
s16 D_800D2F1C[];
s16 D_800D2F38[];
s16 D_800D2F54[];
s16 D_800D2F70[];
s16 D_800D2F8C[];
s16 D_800D2FA8[];
s16 D_800D2FC4[];
s16 D_800D2FE0[];
s16* D_800D2CF8[] = {
NULL,
(s16*)D_800D2D5C,
(s16*)D_800D2D78,
(s16*)D_800D2D94,
(s16*)D_800D2DB0,
(s16*)D_800D2DCC,
(s16*)D_800D2DE8,
(s16*)D_800D2E04,
(s16*)D_800D2E20,
(s16*)D_800D2E3C,
(s16*)D_800D2E58,
(s16*)D_800D2E74,
(s16*)D_800D2E90,
(s16*)D_800D2EAC,
(s16*)D_800D2EC8,
(s16*)D_800D2EE4,
(s16*)D_800D2F00,
(s16*)D_800D2F1C,
(s16*)D_800D2F38,
(s16*)D_800D2F54,
(s16*)D_800D2F70,
(s16*)D_800D2F8C,
(s16*)D_800D2FA8,
(s16*)D_800D2FC4,
(s16*)D_800D2FE0,
NULL, D_800D2D5C, D_800D2D78, D_800D2D94, D_800D2DB0,
D_800D2DCC, D_800D2DE8, D_800D2E04, D_800D2E20, D_800D2E3C,
D_800D2E58, D_800D2E74, D_800D2E90, D_800D2EAC, D_800D2EC8,
D_800D2EE4, D_800D2F00, D_800D2F1C, D_800D2F38, D_800D2F54,
D_800D2F70, D_800D2F8C, D_800D2FA8, D_800D2FC4, D_800D2FE0,
};
u32 D_800D2D5C[] = {
0x00200001, 0xFFF3FFF9, 0x00100010, 0x00680195,
0x00000080, 0x00100090, 0x00000000,
s16 D_800D2D5C[] = {
1, 32, -7, -13, 16, 16, 405, 104, 128, 0, 144, 16, 0, 0,
};
u16 D_800D2D78[] = {
0x0001, 0x0020, 0xFFF8, 0xFFF4, 0x0010, 0x0010, 0x0195,
0x0068, 0x0090, 0x0000, 0x00A0, 0x0010, 0x0000, 0x0000,
s16 D_800D2D78[] = {
1, 32, -8, -12, 16, 16, 405, 104, 144, 0, 160, 16, 0, 0,
};
u32 D_800D2D94[] = {
0x00200001, 0xFFF4FFF8, 0x00100010, 0x00680195,
0x000000A0, 0x001000B0, 0x00000000,
s16 D_800D2D94[] = {
1, 32, -8, -12, 16, 16, 405, 104, 160, 0, 176, 16, 0, 0,
};
u32 D_800D2DB0[] = {
0x00200001, 0xFFF4FFF8, 0x00100010, 0x00680195,
0x000000B0, 0x001000C0, 0x00000000,
s16 D_800D2DB0[] = {
1, 32, -8, -12, 16, 16, 405, 104, 176, 0, 192, 16, 0, 0,
};
u32 D_800D2DCC[] = {
0x00000001, 0xFFF2FFF7, 0x00180018, 0x00680195,
0x00100080, 0x00280098, 0x00000000,
s16 D_800D2DCC[] = {
1, 0, -9, -14, 24, 24, 405, 104, 128, 16, 152, 40, 0, 0,
};
u32 D_800D2DE8[] = {
0x00000001, 0xFFF6FFF5, 0x00180018, 0x00680195,
0x00100098, 0x002800B0, 0x00000000,
s16 D_800D2DE8[] = {
1, 0, -11, -10, 24, 24, 405, 104, 152, 16, 176, 40, 0, 0,
};
u32 D_800D2E04[] = {
0x00000001, 0xFFF6FFF5, 0x00180018, 0x00680195,
0x001000B0, 0x002800C8, 0x00000000,
s16 D_800D2E04[] = {
1, 0, -11, -10, 24, 24, 405, 104, 176, 16, 200, 40, 0, 0,
};
u32 D_800D2E20[] = {
0x00000001, 0xFFF6FFF5, 0x00180018, 0x00680195,
0x001000C8, 0x002800E0, 0x00000000,
s16 D_800D2E20[] = {
1, 0, -11, -10, 24, 24, 405, 104, 200, 16, 224, 40, 0, 0,
};
u32 D_800D2E3C[] = {
0x00000001, 0xFFF5FFF5, 0x00180018, 0x00680195,
0x001000E0, 0x002800F8, 0x00000000,
s16 D_800D2E3C[] = {
1, 0, -11, -11, 24, 24, 405, 104, 224, 16, 248, 40, 0, 0,
};
u32 D_800D2E58[] = {
0x00000001, 0xFFF5FFF5, 0x00180018, 0x00680195,
0x00280080, 0x00400098, 0x00000000,
s16 D_800D2E58[] = {
1, 0, -11, -11, 24, 24, 405, 104, 128, 40, 152, 64, 0, 0,
};
u32 D_800D2E74[] = {
0x00000001, 0xFFF5FFF5, 0x00180018, 0x00680195,
0x00280098, 0x004000B0, 0x00000000,
s16 D_800D2E74[] = {
1, 0, -11, -11, 24, 24, 405, 104, 152, 40, 176, 64, 0, 0,
};
u32 D_800D2E90[] = {
0x00000001, 0xFFF5FFF5, 0x00180018, 0x00680195,
0x002800B0, 0x004000C8, 0x00000000,
s16 D_800D2E90[] = {
1, 0, -11, -11, 24, 24, 405, 104, 176, 40, 200, 64, 0, 0,
};
u32 D_800D2EAC[] = {
0x00000001, 0xFFF6FFF5, 0x00180018, 0x00680195,
0x002800C8, 0x004000E0, 0x00000000,
s16 D_800D2EAC[] = {
1, 0, -11, -10, 24, 24, 405, 104, 200, 40, 224, 64, 0, 0,
};
u32 D_800D2EC8[] = {
0x00000001, 0xFFF6FFF8, 0x00180010, 0x00680195,
0x002800E0, 0x004000F0, 0x00000000,
s16 D_800D2EC8[] = {
1, 0, -8, -10, 16, 24, 405, 104, 224, 40, 240, 64, 0, 0,
};
u32 D_800D2EE4[] = {
0x00000001, 0xFFFBFFF8, 0x00180010, 0x00680195,
0x00400080, 0x00580090, 0x00000000,
s16 D_800D2EE4[] = {
1, 0, -8, -5, 16, 24, 405, 104, 128, 64, 144, 88, 0, 0,
};
u32 D_800D2F00[] = {
0x00000001, 0xFFFAFFF9, 0x00180010, 0x00680195,
0x00400090, 0x005800A0, 0x00000000,
s16 D_800D2F00[] = {
1, 0, -7, -6, 16, 24, 405, 104, 144, 64, 160, 88, 0, 0,
};
u32 D_800D2F1C[] = {
0x00000001, 0xFFFCFFF7, 0x00180010, 0x00680195,
0x004000A0, 0x005800B0, 0x00000000,
s16 D_800D2F1C[] = {
1, 0, -9, -4, 16, 24, 405, 104, 160, 64, 176, 88, 0, 0,
};
u32 D_800D2F38[] = {
0x00000001, 0xFFFCFFF8, 0x00180010, 0x00680195,
0x004000B0, 0x005800C0, 0x00000000,
s16 D_800D2F38[] = {
1, 0, -8, -4, 16, 24, 405, 104, 176, 64, 192, 88, 0, 0,
};
u32 D_800D2F54[] = {
0x00000001, 0xFFFCFFF7, 0x00180010, 0x00680195,
0x004000C0, 0x005800D0, 0x00000000,
s16 D_800D2F54[] = {
1, 0, -9, -4, 16, 24, 405, 104, 192, 64, 208, 88, 0, 0,
};
u32 D_800D2F70[] = {
0x00000001, 0xFFFCFFF8, 0x00180010, 0x00680195,
0x004000D0, 0x005800E0, 0x00000000,
s16 D_800D2F70[] = {
1, 0, -8, -4, 16, 24, 405, 104, 208, 64, 224, 88, 0, 0,
};
u32 D_800D2F8C[] = {
0x00000001, 0x0001FFF8, 0x00100010, 0x00680195,
0x000000C0, 0x001000D0, 0x00000000,
s16 D_800D2F8C[] = {
1, 0, -8, 1, 16, 16, 405, 104, 192, 0, 208, 16, 0, 0,
};
u32 D_800D2FA8[] = {
0x00000001, 0x0001FFF8, 0x00100010, 0x00680195,
0x000000D0, 0x001000E0, 0x00000000,
s16 D_800D2FA8[] = {
1, 0, -8, 1, 16, 16, 405, 104, 208, 0, 224, 16, 0, 0,
};
u32 D_800D2FC4[] = {
0x00000001, 0x0002FFF9, 0x00080010, 0x00680195,
0x000000E0, 0x000800F0, 0x00000000,
s16 D_800D2FC4[] = {
1, 0, -7, 2, 16, 8, 405, 104, 224, 0, 240, 8, 0, 0,
};
u16 D_800D2FE0[] = {
0x0001, 0x0000, 0xFFFD, 0x0000, 0x0008, 0x0008, 0x0195,
0x0068, 0x00E8, 0x0008, 0x00F0, 0x0010, 0x0000,
s16 D_800D2FE0[] = {
1, 0, -3, 0, 8, 8, 405, 104, 232, 8, 240, 16, 0,
#ifdef VERSION_US // dirty data
0x6B61,
#endif

View File

@ -59,10 +59,10 @@ s32 unk14_yVel[] = {
};
u8 unk14_startFrame[] = {
/* 1038 */ 0x01,
/* 1039 */ 0x09,
/* 103A */ 0x15,
/* 103B */ 0x2B,
/* 1038 */ 1,
/* 1039 */ 9,
/* 103A */ 21,
/* 103B */ 43,
};
u16 unk14_lifetime[] = {

224
tools/display_animation.py Normal file
View File

@ -0,0 +1,224 @@
# Author: bismurphy. Please feel free to contact with any questions!
# This tool may be non-intuitive and/or buggy to use; I am happy to make
# fixes if anyone requests them.
# Displays animations for entities.
# Uses display_texture.py as dependency, so make sure that is available to it.
# Example use case: We wanted to figure out what entity EntityLightningCloud is
# in NO3/NP3. We can see in its initialization that its Animset is 0x8001
# and its palette is 0. Then, we can do:
# python3 tools/display_animation.py LIVE no3 0x8001 0 150 100
# To review all the frames in that animation.
# LIVE will pull a live vram dump from pcsx. no3 is the overlay to grab sprite
# data from. 0x8001 is that animset, 0 is the clut, and then 150x100 will be our
# viewport size as we click through the entities.
import display_texture as dt
import argparse
import re
import matplotlib.pyplot as plt
from matplotlib.widgets import Button
import numpy as np
from PIL import Image
# holds the list of animsets
DRA_ANIM_ARRAY_FILE = "src/dra/63ED4.c"
# some day this may have a symbol name
DRA_ANIM_ARRAY = "D_800A3B70"
# holds the individual animsets
DRA_ANIMSET_FILE = "src/dra/d_2F324.c"
def load_array_from_file(filelines, arrayname):
print(f"Trying to load {arrayname}")
arraydata = ""
inarray = False
for line in filelines:
if arrayname in line and "{" in line:
inarray = True
if inarray:
arraydata += line
if "}" in line:
inarray = False
# find the data between the curly braces
pattern = r"\{([^}]*)\}"
match = re.search(pattern, arraydata)
if match:
array_contents = match.group(1)
array_contents = array_contents.rstrip(",") # remove trailing comma if exists
# strip whitespace and turn into list of members
array_members = array_contents.replace(" ", "").split(",")
return array_members
print("Error loading animation array. Hmm.")
exit()
def show_animset(ovl_name, anim_num, arg_palette, view_w, view_h, unk5A):
# Now we have an array that tells us the name of all the frames.
# Start GUI code.
class AnimationShower:
def __init__(self):
self.anim_index = 1
self.textureDisplayer = dt.textureDisplayer(texture_data)
# Need to load the animation's frames now.
# Depends on if we're an ANIMSET_DRA or ANIMSET_OVL.
if anim_num & 0x8000:
print("Overlay animation")
assert ovl_name != "dra"
spritebank = anim_num & 0x7FFF
main_array_file = f"src/st/{ovl_name}/sprite_banks.h"
main_array = "spriteBanks"
animset_file = f"src/st/{ovl_name}/sprites.c"
else:
print("DRA animation")
assert ovl_name == "dra"
main_array_file = DRA_ANIM_ARRAY_FILE
main_array = DRA_ANIM_ARRAY
animset_file = DRA_ANIMSET_FILE
spritebank = anim_num
with open(main_array_file) as f:
animdata = f.read().splitlines()
animarray = load_array_from_file(animdata, main_array)
anim_set_name = animarray[spritebank]
print(f"Animation set {anim_num} is {anim_set_name}. Loading.")
with open(animset_file) as f:
self.framesdata = f.read().splitlines()
print("Loading framearray")
print(animset_file)
self.framearray = load_array_from_file(self.framesdata, anim_set_name)
print("Loaded framearray.")
def prev(self, event):
self.anim_index -= 1
if self.anim_index < 1:
self.anim_index = len(self.framearray) - 1
self.render_frame()
def next(self, event):
self.anim_index += 1
if self.anim_index >= len(self.framearray):
self.anim_index = 1
self.render_frame()
def render_frame(self):
print("RENDERING", self.anim_index)
frame_name = self.framearray[self.anim_index]
# prepend the s16 to make sure we get the actual array, not something that
# points to the array
frame_params = load_array_from_file(self.framesdata, "s16 " + frame_name)
data_size = 1 + int(frame_params[0]) * 11
frame_params = frame_params[:data_size]
frame_params = [int(x) for x in frame_params]
print(frame_params)
# Now we follow the logic of RenderEntities.
# r->spriteSheetIdx = *animFrame++;
spriteSheetIdx = frame_params[0]
frame_params = frame_params[1:]
# Not prepared to handle this case.
if spriteSheetIdx < 0 or spriteSheetIdx & 0x8000:
print("I don't know what this is yet, need new program logic")
exit()
# Now skip to line 984. We're going to make an image from the individual images.
overall_image = Image.new("RGBA", (view_w, view_h))
for i in range(spriteSheetIdx):
ax.clear()
print(frame_params)
frameFlags = frame_params[0] # line 989
tpage = frame_params[6] # line 990
tpage += unk5A # line 991
runk0 = tpage & 3 # 992
tpage >>= 2 # 993
xpivot = frame_params[1] # 994
ypivot = frame_params[2] # 995
width = frame_params[3] # 996
height = frame_params[4] # 997
# Skip all the logic with positioning and flipping.
# Pick up at line 1062
if arg_palette & 0x8000:
clut = arg_palette & 0x7FFF
else:
clut = frame_params[5] + arg_palette
u_0 = frame_params[7]
v_0 = frame_params[8]
u_1 = frame_params[9]
v_1 = frame_params[10]
assert u_1 - u_0 == width
assert v_1 - v_0 == height
image = self.textureDisplayer.get_image(
tpage, clut, u_0, v_0, width, height
)
if frameFlags & 2:
image = np.flip(image, 1)
frameFlags -= 2
if frameFlags != 0:
print("We have frame flags. Ignoring for now.", frameFlags)
pil_image = Image.fromarray(image)
# pass pil_image twice to get transparency
overall_image.paste(
pil_image,
(view_w // 2 + xpivot, view_h // 2 + ypivot),
pil_image,
)
frame_params = frame_params[11:]
ax.set_title(f"Frame #{self.anim_index} of {len(self.framearray)}")
ax.imshow(overall_image)
plt.draw()
shower = AnimationShower()
fig, ax = plt.subplots()
ax.set_xlim(-32, 32)
ax.set_ylim(-32, 32)
plt.subplots_adjust(bottom=0.15)
prev_button = Button(plt.axes([0.1, 0.025, 0.3, 0.1], facecolor="k"), "Prev Frame")
prev_button.on_clicked(shower.prev)
next_button = Button(plt.axes([0.6, 0.025, 0.3, 0.1], facecolor="k"), "Next Frame")
next_button.on_clicked(shower.next)
shower.render_frame()
plt.show()
parser = argparse.ArgumentParser(description="Renders in-game animations from ANIMSET")
parser.add_argument("dump_filename")
parser.add_argument("overlay", help="Overlay name. dra, no3, cen, etc")
parser.add_argument(
"animset_num", type=lambda x: int(x, 0), help="Animset number; 2 for ANIMSET_DRA(2)"
)
parser.add_argument(
"e_palette", type=lambda x: int(x, 0), help="Entity's Palette param"
)
parser.add_argument(
"view_width", type=lambda x: int(x, 0), help="Width of your view window"
)
parser.add_argument(
"view_height", type=lambda x: int(x, 0), help="Height of your view window"
)
parser.add_argument(
"--unk5A",
type=lambda x: int(x, 0),
default=0,
help="Entity's unk5A value (optional)",
)
args = parser.parse_args()
texture_data = dt.load_raw_dump(args.dump_filename)
show_animset(
args.overlay,
args.animset_num,
args.e_palette,
args.view_width,
args.view_height,
args.unk5A,
)

View File

@ -52,10 +52,11 @@ def convert_rgb555(in_array):
r = round(r / 31 * 255)
g = round(g / 31 * 255)
b = round(b / 31 * 255)
pixel = [r, g, b]
a = 255 if (r or g or b) else 0
pixel = [r, g, b, a]
out_row.append(pixel)
out_array.append(out_row)
return np.array(out_array)
return np.array(out_array, dtype="uint8")
# Once we have a tpage and a clut, apply that clut to color the tpage.
@ -112,9 +113,7 @@ def get_clut(colored_dump, clutnum):
# For a given tpage and clut, retrieve the tpage from the raw dump, and apply
# the clut to that tpage.
def retrieve_colored_tpage(raw_dump, tpage_number, clut_number):
# Load all the data as rgb555, the native format cluts are specified in.
colored = convert_rgb555(raw_dump)
def retrieve_colored_tpage(raw_dump, colored, tpage_number, clut_number):
tpage_rendering_clut = get_clut(colored, clut_number)
# Now we have our tpage rendering clut extracted, get the tpage, and apply that clut.
tpage = get_tpage_by_number(raw_dump, tpage_number)
@ -123,12 +122,21 @@ def retrieve_colored_tpage(raw_dump, tpage_number, clut_number):
def draw_tpage_selection(raw_dump, tpage_number, clut_number, left, top, width, height):
# Get the tpage with the proper clut applied
ctp = retrieve_colored_tpage(raw_dump, tpage_number, clut_number)
colored = convert_rgb555(raw_dump)
image = get_tpage_selection(
raw_dump, colored, tpage_number, clut_number, left, top, width, height
)
plt.imshow(image)
plt.show()
def get_tpage_selection(
raw_dump, colored, tpage_number, clut_number, left, top, width, height
):
ctp = retrieve_colored_tpage(raw_dump, colored, tpage_number, clut_number)
# Crop it to match the needed UV coords, and display it
segment = ctp[top : top + height, left : left + width]
plt.imshow(segment)
plt.show()
return segment
# For the chosen filename for the vram dump, we load the bytes, convert
@ -142,7 +150,8 @@ def load_raw_dump(filename):
dumpbytes = response.read()
print("VRAM fetched from PCSX.")
except urllib.error.URLError as e:
print("Error retrieving file:", e)
print("Error retrieving textures from PCSX. Is it running?", e)
exit()
else: # Load from a specified filename
with open(filename, "rb") as dumpfile:
dumpbytes = dumpfile.read()
@ -152,6 +161,15 @@ def load_raw_dump(filename):
return np.reshape(bytestring, (512, int(datasize / 512)))
class textureDisplayer:
def __init__(self, vram_dump):
self.rawvram = vram_dump
self.colored = convert_rgb555(vram_dump)
def get_image(self, tpage, clut, x, y, w, h):
return get_tpage_selection(self.rawvram, self.colored, tpage, clut, x, y, w, h)
parser = argparse.ArgumentParser(description="Renders textures from VRAM dumps")
parser.add_argument("dump_filename")
# Load the numerical values; the lambda auto-detects decimal or hexadecimal and processes either.
@ -165,17 +183,17 @@ parser.add_argument(
"--showclut", action="store_true", help="Just show the 16 colors in the CLUT"
)
if __name__ == "__main__":
args = parser.parse_args()
args = parser.parse_args()
array = load_raw_dump(args.dump_filename)
if args.showclut:
colored = convert_rgb555(array)
clut = get_clut(colored, args.clut_num)
clut = clut.reshape((1, 16, 3)) # reshape to turn the clut into a 1x16 image
plt.imshow(clut)
plt.show()
elif args.whole:
draw_tpage_selection(array, args.tpage_num, args.clut_num, 0, 0, 256, 256)
else:
draw_tpage_selection(array, args.tpage_num, args.clut_num, *args.UV_vals)
array = load_raw_dump(args.dump_filename)
if args.showclut:
colored = convert_rgb555(array)
clut = get_clut(colored, args.clut_num)
clut = clut.reshape((1, 16, 3)) # reshape to turn the clut into a 1x16 image
plt.imshow(clut)
plt.show()
elif args.whole:
draw_tpage_selection(array, args.tpage_num, args.clut_num, 0, 0, 256, 256)
else:
draw_tpage_selection(array, args.tpage_num, args.clut_num, *args.UV_vals)