forked from K0lb3/UnityPy
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSpriteHelper.py
More file actions
348 lines (286 loc) · 12.6 KB
/
SpriteHelper.py
File metadata and controls
348 lines (286 loc) · 12.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Dict, Iterable, Union, cast
from PIL import Image, ImageDraw
from PIL.Image import Transform, Transpose
from ..classes import SpriteAtlasData
from ..enums import (
ClassIDType,
SpriteMeshType,
SpritePackingMode,
SpritePackingRotation,
)
from ..helpers.MeshHelper import MeshHandler
from .Texture2DConverter import get_image_from_texture2d
if TYPE_CHECKING:
from typing import List, Optional, Tuple
from ..classes import PPtr, Sprite, Texture2D
try:
import numpy as np
except ImportError:
np = None
class SpriteSettings:
packed: bool
packingMode: SpritePackingMode
packingRotation: SpritePackingRotation
meshType: SpriteMeshType
def __init__(self, settings_raw):
self.settingsRaw = settings_raw
self.packed = bool(self.settingsRaw & 1) # 1
self.packingMode = SpritePackingMode((self.settingsRaw >> 1) & 1) # 1
self.packingRotation = SpritePackingRotation((self.settingsRaw >> 2) & 0xF) # 4
self.meshType = SpriteMeshType((self.settingsRaw >> 6) & 1) # 1
# rest of the bits are reserved
def get_image(sprite: Sprite, texture: PPtr[Texture2D], alpha_texture: Optional[PPtr[Texture2D]]) -> Image.Image:
assert sprite.assets_file, "Sprite assets file is not set!"
cache = cast(Dict[Any, Any], sprite.assets_file._cache) # TODO: edit in SerializibleFile
if alpha_texture:
cache_id = (texture.path_id, alpha_texture.path_id)
if cache_id not in cache:
original_image = get_image_from_texture2d(texture.read(), False)
alpha_image = get_image_from_texture2d(alpha_texture.read(), False)
original_image = Image.merge("RGBA", (*original_image.split()[:3], alpha_image.split()[0]))
cache[cache_id] = original_image
else:
cache_id = texture.path_id
if cache_id not in cache:
original_image = get_image_from_texture2d(texture.read(), False)
cache[cache_id] = original_image
return cache[cache_id]
def get_image_from_sprite(m_Sprite: Sprite) -> Image.Image:
atlas = None
if m_Sprite.m_SpriteAtlas:
atlas = m_Sprite.m_SpriteAtlas.read()
elif m_Sprite.m_AtlasTags:
# looks like the direct pointer is empty, let's try to find the Atlas via its name
assert m_Sprite.assets_file, "Sprite assets file is not set!"
for obj in m_Sprite.assets_file.objects.values():
if obj.type == ClassIDType.SpriteAtlas:
atlas = obj.read()
if atlas.m_Name == m_Sprite.m_AtlasTags[0]:
break
atlas = None
if atlas:
sprite_atlas_data = next(value for key, value in atlas.m_RenderDataMap if key == m_Sprite.m_RenderDataKey)
assert isinstance(sprite_atlas_data, SpriteAtlasData), "SpriteAtlasData not found!"
else:
sprite_atlas_data = m_Sprite.m_RD
m_Texture2D = sprite_atlas_data.texture
alpha_texture = sprite_atlas_data.alphaTexture
texture_rect = sprite_atlas_data.textureRect
settings_raw = sprite_atlas_data.settingsRaw
original_image = get_image(m_Sprite, m_Texture2D, alpha_texture)
sprite_image = original_image.crop(
(
texture_rect.x,
texture_rect.y,
texture_rect.x + texture_rect.width,
texture_rect.y + texture_rect.height,
)
)
settings_raw = SpriteSettings(settings_raw)
if settings_raw.packed == 1:
rotation = settings_raw.packingRotation
if rotation == SpritePackingRotation.kSPRFlipHorizontal:
sprite_image = sprite_image.transpose(Transpose.FLIP_LEFT_RIGHT)
# spriteImage = RotateFlip(RotateFlipType.RotateNoneFlipX)
elif rotation == SpritePackingRotation.kSPRFlipVertical:
sprite_image = sprite_image.transpose(Transpose.FLIP_TOP_BOTTOM)
# spriteImage.RotateFlip(RotateFlipType.RotateNoneFlipY)
elif rotation == SpritePackingRotation.kSPRRotate180:
sprite_image = sprite_image.transpose(Transpose.ROTATE_180)
# spriteImage.RotateFlip(RotateFlipType.Rotate180FlipNone)
elif rotation == SpritePackingRotation.kSPRRotate90:
sprite_image = sprite_image.transpose(Transpose.ROTATE_270)
# spriteImage.RotateFlip(RotateFlipType.Rotate270FlipNone)
if settings_raw.packingMode == SpritePackingMode.kSPMTight:
assert m_Sprite.object_reader, "Sprite object reader is not set!"
mesh = MeshHandler(m_Sprite.m_RD, m_Sprite.object_reader.version)
mesh.process()
if mesh.m_UV0 and any(u or v for u, v in mesh.m_UV0):
# copy triangles from mesh
sprite_image = render_sprite_mesh(m_Sprite, mesh, original_image)
else:
# create mask to keep only the polygon
sprite_image = mask_sprite(m_Sprite, mesh, sprite_image)
return sprite_image.transpose(Transpose.FLIP_TOP_BOTTOM)
def mask_sprite(m_Sprite: Sprite, mesh: MeshHandler, sprite_image: Image.Image) -> Image.Image:
mask_img = Image.new("1", sprite_image.size, color=0)
draw = ImageDraw.ImageDraw(mask_img)
# normalize the points
# shift the whole point matrix into the positive space
# multiply them with a factor to scale them to the image
positions = mesh.m_Vertices
assert positions, "No vertices found in sprite mesh!"
# find the axis that has only one value - can be removed
# usually the z axis
min_x = min(x for x, _y, _z in positions)
min_y = min(y for _x, y, _z in positions)
factor = m_Sprite.m_PixelsToUnits
positions_2d = [((x - min_x) * factor, (y - min_y) * factor) for x, y, _z in positions]
# generate triangles from the given points
triangles = [
(
positions_2d[a],
positions_2d[b],
positions_2d[c],
)
for submesh in mesh.get_triangles()
for a, b, c in submesh
]
for triangle in triangles:
draw.polygon(triangle, fill=1)
# apply the mask
if sprite_image.mode == "RGBA":
# the image already has an alpha channel,
# so we have to use composite to keep it
empty_img = Image.new(sprite_image.mode, sprite_image.size, color=0)
sprite_image = Image.composite(sprite_image, empty_img, mask_img)
else:
# add mask as alpha-channel to keep the polygon clean
sprite_image.putalpha(mask_img)
return sprite_image
def render_sprite_mesh(m_Sprite: Sprite, mesh: MeshHandler, texture: Image.Image) -> Image.Image:
for triangles in mesh.get_triangles():
positions = mesh.m_Vertices
if not positions:
continue
uv = mesh.m_UV0
if not uv:
raise ValueError("No UV coordinates found in sprite mesh!")
# 2. patch position data
# 2.1 make positions 2d
# find the axis that has only one value - can be removed
# usually the z axis
axis_values = [[pos[i] for pos in positions] for i in range(3)]
for i in range(2, -1, -1):
if len(set(axis_values[i])) == 1:
break
else:
raise ValueError("Can't process 3d sprites!")
axis_values = axis_values[:i] + axis_values[i + 1 :]
x_min = min(axis_values[0])
y_min = min(axis_values[1])
x_max = max(axis_values[0])
y_max = max(axis_values[1])
# 2.2 map positions from middle to top left
# 2.3 convert relative positions to absolute
pixels_to_units = m_Sprite.m_PixelsToUnits
positions_abs = [
(round((x - x_min) * pixels_to_units), round((y - y_min) * pixels_to_units)) for x, y in zip(*axis_values)
]
width, height = texture.size
uv_abs = [(round(u * width), round(v * height)) for u, v in uv]
# 2.4 generate final image size
size = (
round((x_max - x_min) * pixels_to_units),
round((y_max - y_min) * pixels_to_units),
)
sprite = Image.new(texture.mode, size)
for tri in triangles:
copy_triangle(
texture,
tuple(uv_abs[i] for i in tri), # type: ignore
sprite,
tuple(positions_abs[i] for i in tri), # type: ignore
)
return sprite
else:
raise ValueError("No triangles found in mesh!")
def copy_triangle(
src_img: Image.Image,
src_tri: Tuple[Tuple[int, int], Tuple[int, int], Tuple[int, int]],
dst_img: Image.Image,
dst_tri: Tuple[Tuple[int, int], Tuple[int, int], Tuple[int, int]],
) -> None:
src_off = (
(src_tri[1][0] - src_tri[0][0], src_tri[1][1] - src_tri[0][1]),
(src_tri[2][0] - src_tri[0][0], src_tri[2][1] - src_tri[0][1]),
)
dst_off = (
(dst_tri[1][0] - dst_tri[0][0], dst_tri[1][1] - dst_tri[0][1]),
(dst_tri[2][0] - dst_tri[0][0], dst_tri[2][1] - dst_tri[0][1]),
)
# check if transform is necessary by comparing the triangle sizes
if src_off[0] == dst_off[0] and src_off[1] == dst_off[1]:
# no transform necessary, just copy the triangle
# make rectangle that contains the triangle
# upper_left, _, lower_right = sorted(src_tri)
upper_left = (
min(src_tri[0][0], src_tri[1][0], src_tri[2][0]),
min(src_tri[0][1], src_tri[1][1], src_tri[2][1]),
)
lower_right = (
max(src_tri[0][0], src_tri[1][0], src_tri[2][0]),
max(src_tri[0][1], src_tri[1][1], src_tri[2][1]),
)
src_part = src_img.crop((*upper_left, *lower_right))
# create mask for triangle
mask_box = [(x - upper_left[0], y - upper_left[1]) for x, y in src_tri]
mask = Image.new("1", src_part.size)
maskdraw = ImageDraw.Draw(mask)
maskdraw.polygon(mask_box, fill=255)
# paste triangle into destination image
dst = (
int(min(dst_tri[0][0], dst_tri[1][0], dst_tri[2][0])),
int(min(dst_tri[0][1], dst_tri[1][1], dst_tri[2][1])),
)
dst_img.paste(src_part, dst, mask=mask)
else:
# transform is necessary, use affine transformation
# https://stackoverflow.com/a/6959111
((x11, x12), (x21, x22), (x31, x32)) = src_tri
((y11, y12), (y21, y22), (y31, y32)) = dst_tri
# Construct matrix M manually
M = [
[y11, y12, 1, 0, 0, 0],
[y21, y22, 1, 0, 0, 0],
[y31, y32, 1, 0, 0, 0],
[0, 0, 0, y11, y12, 1],
[0, 0, 0, y21, y22, 1],
[0, 0, 0, y31, y32, 1],
]
# Vector y corresponds to the x coordinates in the source triangle
y = [x11, x21, x31, x12, x22, x32]
if np:
A = np.linalg.solve(M, y)
else:
# np.lingal.solve - obviously way faster, but numpy will only come with 2.0
A = linalg_solve(M, y) # type: ignore
transformed = src_img.transform(dst_img.size, Transform.AFFINE, A)
mask = Image.new("1", dst_img.size)
maskdraw = ImageDraw.Draw(mask)
maskdraw.polygon(dst_tri, fill=255)
dst_img.paste(transformed, mask=mask)
def linalg_solve(M: List[List[Union[float, int]]], y: List[Union[float, int]]) -> List[float]:
# M^-1 * y
M_i = get_matrix_inverse(M)
return [sum(M_i[i][j] * y[j] for j in range(len(y))) for i in range(len(M_i))]
def transpose_matrix(m: List[List[float]]) -> Iterable[List[float]]:
# https://stackoverflow.com/a/39881366
return map(list, zip(*m))
def get_matrix_minor(m: List[List[float]], i: int, j: int) -> List[List[float]]:
# https://stackoverflow.com/a/39881366
return [row[:j] + row[j + 1 :] for row in (m[:i] + m[i + 1 :])]
def get_matrix_determinant(m: List[List[float]]) -> float:
# https://stackoverflow.com/a/39881366
# base case for 2x2 matrix
if len(m) == 2:
return m[0][0] * m[1][1] - m[0][1] * m[1][0]
return sum(((-1) ** c) * m[0][c] * get_matrix_determinant(get_matrix_minor(m, 0, c)) for c in range(len(m)))
def get_matrix_inverse(m: List[List[float]]) -> List[List[float]]:
# https://stackoverflow.com/a/39881366
determinant = get_matrix_determinant(m)
# special case for 2x2 matrix:
if len(m) == 2:
return [
[m[1][1] / determinant, -1 * m[0][1] / determinant],
[-1 * m[1][0] / determinant, m[0][0] / determinant],
]
# find matrix of cofactors
cofactors = [
[((-1) ** (r + c)) * get_matrix_determinant(get_matrix_minor(m, r, c)) for c in range(len(m))]
for r in range(len(m))
]
cofactors = list(transpose_matrix(cofactors))
return [[c / determinant for c in row] for row in cofactors]
__all__ = ["get_image_from_sprite"]