-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathMetaData.py
More file actions
418 lines (350 loc) · 20.1 KB
/
MetaData.py
File metadata and controls
418 lines (350 loc) · 20.1 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
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
# --------------------------
# MetaData: Including data structures for action data
# 11.02.2022
# --------------------------
# Robin Hohnsbeen (Ryou)
import bpy
import math
def has_anim_target() -> bool:
if bpy.context.scene.anim_target_enum == "1_Object":
return bpy.context.scene.anim_target is not None
else:
if bpy.context.scene.anim_target_collection is None:
return False
return len(bpy.context.scene.anim_target_collection.all_objects) > 0
def get_action_entry_tools(action_entry):
tools = []
collection = None
if action_entry.additional_object_enum == "1_Object" and action_entry.additional_object != None:
tools.append(action_entry.additional_object)
elif action_entry.additional_object_enum == "2_Collection" and action_entry.additional_collection != None:
collection = action_entry.additional_collection
for object in action_entry.additional_collection.all_objects:
tools.append(object)
return tools, collection
def get_anim_targets():
if bpy.context.scene.anim_target_enum == "1_Object":
return [bpy.context.scene.anim_target]
else:
return bpy.context.scene.anim_target_collection.all_objects
class ActionMetaData(bpy.types.PropertyGroup):
is_used: bpy.props.BoolProperty(
name='Enabled',
default=True,
description="Determines if this action entry is currently in use. Can be useful if you want to keep an entry for later"
)
action: bpy.props.PointerProperty(type=bpy.types.Action, name='Action',
description="The reference to the action that will be applied on the action target when rendering a spritesheet")
override_resolution: bpy.props.BoolProperty(
name='Override resolution',
default=False,
description="You can render sprites with resolutions other than in the render settings. Keep in mind thought that all resolutions are multiplied by the resolution percentage (100% => 1.0, 200% => 2.0, ...). Use [arrows keys] while in preview to change these values on the fly"
)
start_frame: bpy.props.IntProperty(
name='Start frame', default=1, soft_min=0, description="The number of the first frame of the action")
max_frames: bpy.props.IntProperty(name='Max frames', default=16, min=1, soft_max=256,
description="The amount of frames that are rendered for this action")
width: bpy.props.IntProperty(name='Pixel width', default=16, min=1, max=2048,
description="The pixel width of one sprite image. (Will be multiplied by the resolution percentage (100% => 1.0, 200% => 2.0, ...)")
height: bpy.props.IntProperty(name='Pixel height', default=20, min=1, max=2048,
description="The pixel height of one sprite image. (Will be multiplied by the resolution percentage (100% => 1.0, 200% => 2.0, ...)")
use_alternative_name: bpy.props.BoolProperty(
name='Override name',
default=False,
description="In case you want to have a separate clonk action that references the same blender action. e.g Sword Fight, Axe Fight. Each can use the same blender action but use different tools."
)
alternative_name: bpy.props.StringProperty(name='Name Override', default="", maxlen=25,
description="Fill in, if you want this action to have a different name. This is only usefull if two entries share the same action")
is_rendered: bpy.props.BoolProperty(
name='Is Rendered To Spritesheet',
default=True,
description="When this is false, it will reference another action and print it with a different name to the ActMap"
)
render_type_enum: bpy.props.EnumProperty(
items={
("Spriteanimation", "Spriteanimation",
"Render several frames and put them next to each other on the spritesheet.", 0),
("Picture", "Picture", "Render one frame and put it where it fits. This is useful for title images.", 1)},
default="Spriteanimation", options={"HIDDEN"}, name=''
)
image_for_picture_combined: bpy.props.PointerProperty(
type=bpy.types.Image, name='', description="Use an image for the title picture directly and omit rendering. This image will be used for the combined or the graphics sprite sheet")
image_for_picture_overlay: bpy.props.PointerProperty(
type=bpy.types.Image, name='', description="Use an image for the title picture directly and omit rendering. This image will be used for the overlay sprite sheet")
additional_object_enum: bpy.props.EnumProperty(
items={("1_Object", "Object", "Render one object", 1),
("2_Collection", "Collection", "Render whole collection", 2)},
default="1_Object", options={"HIDDEN"}, name=''
)
additional_object: bpy.props.PointerProperty(
type=bpy.types.Object, name='', description="An object that is only visible in this action. This can be used for tools that a clonk is holding for example")
additional_collection: bpy.props.PointerProperty(
type=bpy.types.Collection, name='', description="A collection that holds objects that are only visible in this action. This can be used for tools that a clonk is holding for example")
find_material_name: bpy.props.StringProperty(
name='Find material name', maxlen=32, description="Materials containing that name will be replaced by the replace material")
replace_material: bpy.props.PointerProperty(
type=bpy.types.Material, name='Replace material', description="The material that it will be replaced with")
region_cropping: bpy.props.FloatVectorProperty(
name='Region Cropping',
default=[0.0, 1.0, 0.0, 1.0],
description="This uses the render region to crop the rendered image to a smaller piece. This is useful for animated doors on buildings for example",
size=4)
invert_region_cropping: bpy.props.BoolProperty(
name='Cut out region instead of cropping',
default=False,
description="Instead of cropping the rendered image, the region itself will be transparent"
)
use_normal_action_placement: bpy.props.BoolProperty(
name='Use default action placement',
default=True,
description="Determines whether this action is placed on the spritesheet in order of its list index (Default) or placed at the end where it fits (Non default). Uncheck this for title images of objects or Clonks"
)
override_camera: bpy.props.PointerProperty(
type=bpy.types.Object, name='', description="The camera that will be used during this action instead of the default one. Can be left empty")
override_facet_offset: bpy.props.BoolProperty(
name='Override facet offset',
default=False,
description="Overrides the facet offset inside ActMap.txt. Has no effect on rendering the action, so you can simply hit export/update ActMap.txt. Sprite renders may be off center to use the sprite area more efficiently. This offset can move it back to the center in the game"
)
facet_offset_x: bpy.props.IntProperty(
name='Facet offset x', default=0, description="X direction offset in which the facet will be moved inside the game")
facet_offset_y: bpy.props.IntProperty(
name='Facet offset y', default=0, description="Y direction offset in which the facet will be moved inside the game")
override_camera_shift: bpy.props.BoolProperty(
name='Override camera shift', description="Change the camera shift for this action. This can be used to arrange the sprite in the camera bounds better. Use [shift + arrow keys] while in preview to change these values on the fly. You need to rerender the sprite sheet and update the ActMap to see an effect in the game")
camera_shift_x: bpy.props.IntProperty(
name='Camera shift x', default=0, description="X direction shift of the camera (in pixels)")
camera_shift_y: bpy.props.IntProperty(
name='Camera shift y', default=0, description="Y direction shift of the camera (in pixels)")
camera_shift_changes_facet_offset: bpy.props.BoolProperty(name='Add camera shift to facet offset', default=True,
description="The camera shift will be added to the facet offset to keep the sprite at its original position in the game")
class SpriteSheetMetaData(bpy.types.PropertyGroup):
overlay_rendering_enum: bpy.props.EnumProperty(
items={
("Separate", "Separate", "Graphics and Overlay rendered separately. Materials with \"Overlay\" in their name will be replaced with the overlay material", 0),
("Combined", "Combined", "Graphics and Overlay in one image. (Materials with \"Overlay\" in their name will be replaced with a blue fill material)", 1)},
default="Separate", options={"HIDDEN"}, name='Overlay Render Setting'
)
overlay_material: bpy.props.PointerProperty(type=bpy.types.Material, name='Overlay Material',
description="Materials with \"Overlay\" in its name will be replaced with this material upon render")
fill_material: bpy.props.PointerProperty(type=bpy.types.Material, name='Fill Material',
description="Materials with \"Overlay\" in its name will be replaced with this material upon render")
add_suffix_for_combined: bpy.props.BoolProperty(name='Add suffix \"_Combined\"', default=True,
description="This will add \"_Combined\" at the end of the sprite sheet file name. Useful for testing because it prevents to override the Graphics.png")
spritesheet_suffix: bpy.props.StringProperty(
name='Spritesheet name suffix', maxlen=32, description="A text that will be added at the end of the output file")
render_direction: bpy.props.EnumProperty(
items={
("Horizontal", "Horizontal",
"Sprites in one animation will be placed horizontally", 0),
("Vertical", "Vertical", "Sprites in one animation will be placed vertically", 1)},
default="Horizontal", options={"HIDDEN"}, name='Sprite packing'
)
custom_object_dimensions: bpy.props.BoolProperty(
name='Custom object size in DefCore',
default=False,
description="Acts as reference size to correctly calculate the facet offset if the render resolution differs from the object size (width and height) in DefCore. If false, the render resolution is used to set the width and height"
)
object_width: bpy.props.IntProperty(
name='Object width', default=16, min=1, description="Width in DefCore")
object_height: bpy.props.IntProperty(
name='Object height', default=20, min=1, description="Height in DefCore")
override_object_offset: bpy.props.BoolProperty(
name='Override object offset',
default=False,
description="Determines if the object center (offset) in DefCore will be overridden. Sometimes it is useful to handle this value manually. When set to false, half of the resolution (or custom size) will be calculated for the object center"
)
object_center_x: bpy.props.IntProperty(
name='Object center x', default=8, min=0, description="X distance to object center")
object_center_y: bpy.props.IntProperty(
name='Object center y', default=10, min=0, description="Y distance to object center")
output_compression: bpy.props.IntProperty(
name='Output compression', default=15, min=0, max=100, subtype="PERCENTAGE", description="Output compression intensity")
def MakeRectCutoutPixelPerfect(action_entry: ActionMetaData):
scene = bpy.context.scene
render_width = action_entry.width if action_entry.override_resolution else scene.render.resolution_x
render_height = action_entry.height if action_entry.override_resolution else scene.render.resolution_y
pixel_ratio_x = 1.0 / render_width
pixel_ratio_y = 1.0 / render_height
action_entry.region_cropping[0] = math.floor(
action_entry.region_cropping[0] / pixel_ratio_x) * pixel_ratio_x
action_entry.region_cropping[1] = math.floor(
action_entry.region_cropping[1] / pixel_ratio_x) * pixel_ratio_x
action_entry.region_cropping[2] = math.floor(
action_entry.region_cropping[2] / pixel_ratio_y) * pixel_ratio_y
action_entry.region_cropping[3] = math.floor(
action_entry.region_cropping[3] / pixel_ratio_y) * pixel_ratio_y
return action_entry
# Sprites with different resolution need to compensate for their position change
def get_automatic_facet_offset(scene, anim_entry, do_round=True):
custom_dimensions = scene.spritesheet_settings.custom_object_dimensions
x_res = scene.spritesheet_settings.object_width if custom_dimensions else scene.render.resolution_x
y_res = scene.spritesheet_settings.object_height if custom_dimensions else scene.render.resolution_y
anim_width = anim_entry.width if anim_entry.override_resolution else scene.render.resolution_x
anim_height = anim_entry.height if anim_entry.override_resolution else scene.render.resolution_y
x_offset = -(anim_width - x_res) / 2.0
y_offset = -(anim_height - y_res) / 2.0
if do_round:
return round(x_offset), round(y_offset)
else:
return round(x_offset, 2), round(y_offset, 2)
def get_res_multiplier():
return bpy.context.scene.render.resolution_percentage / 100.0
def GetPixelFromCutout(action_entry: ActionMetaData, scaled=False):
scene = bpy.context.scene
render_width = action_entry.width if action_entry.override_resolution else scene.render.resolution_x
render_height = action_entry.height if action_entry.override_resolution else scene.render.resolution_y
res_multiplier = get_res_multiplier() if scaled else 1.0
# Rounding the solution should mitigate the risk of losing a pixel
pixel_ratio_x = 1.0 / render_width
x_pixel_min = round(
action_entry.region_cropping[0] / pixel_ratio_x * res_multiplier)
x_pixel_max = round(
action_entry.region_cropping[1] / pixel_ratio_x * res_multiplier)
pixel_ratio_y = 1.0 / render_height
y_pixel_min = round(
action_entry.region_cropping[2] / pixel_ratio_y * res_multiplier)
y_pixel_max = round(
action_entry.region_cropping[3] / pixel_ratio_y * res_multiplier)
width = x_pixel_max - x_pixel_min
height = y_pixel_max - y_pixel_min
min_max_pixels = [x_pixel_min, x_pixel_max, y_pixel_min, y_pixel_max]
pixel_dimensions = [width, height]
return min_max_pixels, pixel_dimensions
def SetRenderBorder(action_entry):
scene = bpy.context.scene
scene.render.border_min_x = action_entry.region_cropping[0]
scene.render.border_max_x = action_entry.region_cropping[1]
scene.render.border_min_y = action_entry.region_cropping[2]
scene.render.border_max_y = action_entry.region_cropping[3]
def UnsetRenderBorder():
scene = bpy.context.scene
scene.render.border_min_x = 0.0
scene.render.border_max_x = 1.0
scene.render.border_min_y = 0.0
scene.render.border_max_y = 1.0
def is_using_cutout(action_entry):
return action_entry.region_cropping[0] != 0.0 or action_entry.region_cropping[1] != 1.0 or action_entry.region_cropping[2] != 0.0 or action_entry.region_cropping[3] != 1.0
def GetActionNameFromIndex(list_index):
if len(bpy.context.scene.animlist) == 0 or bpy.context.scene.animlist[list_index].action is None:
return ""
else:
return GetActionName(bpy.context.scene.animlist[list_index])
def GetActionName(action_entry: ActionMetaData):
name = action_entry.action.name
if action_entry.use_alternative_name and len(action_entry.alternative_name) > 0:
name = action_entry.alternative_name
return name
def CheckIfActionListIsValid(action_entries):
action_entry_names = set()
for entry in action_entries:
if GetActionName(entry) not in action_entry_names:
action_entry_names.add(GetActionName(entry))
else:
return "ERROR", "Can't have two actions with the same name: %s. Use \"override name\" in one of each action." % (GetActionName(entry))
return "INFO", "All entries are valid."
def GetValidActionEntries():
valid_action_entries = []
for action_index, action_entry in enumerate(bpy.context.scene.animlist):
if action_entry.action == None:
print(
f"Action {action_index} omitted, because no blender action was referenced.")
continue
if action_entry.is_used == False:
print(f"Action {action_index} omitted, because it was disabled.")
continue
valid_action_entries.append(action_entry)
return valid_action_entries
def MakeActionEntry(anim_data):
new_entry: ActionMetaData = bpy.context.scene.animlist.add()
new_entry.action = anim_data["Action"]
if bpy.context.scene.render.resolution_x != anim_data["Width"] or bpy.context.scene.render.resolution_y != anim_data["Height"]:
new_entry.override_resolution = True
new_entry.height = anim_data["Height"]
new_entry.width = anim_data["Width"]
new_entry.max_frames = anim_data["Length"]
return new_entry
def replace_duplicate_materials(in_objects):
unused_materials = set()
for new_object in in_objects:
if new_object.type == "MESH":
# Replace imported materials with existing materials.
for material_slot in new_object.material_slots:
if material_slot.material is not None:
for index in range(1, 5):
if f".00{index}" in material_slot.material.name:
existing_material = bpy.data.materials.find(material_slot.material.name.replace(f".00{index}", ""))
if existing_material:
unused_materials.add(material_slot.material)
material_slot.material = bpy.data.materials[existing_material]
break
for unused_material in unused_materials:
if unused_material is None:
continue
bpy.data.materials.remove(unused_material)
vgroup_map = {
"dagger": "Tool1",
"arrow": "Tool2",
"spear": "Tool1",
"minigun": "Tool1",
"sword": "Tool1",
"crossbow": "Tool1",
"staff": "Tool1",
"hammer": "Tool1",
"pistole": "Tool1",
"rocketlauncher": "Tool1",
"bottle": "Tool1",
"rocket": "Tool1",
"mg": "Tool1",
"bow": "Tool1",
"shield": "Tool2",
"axe": "Tool1",
"shovel": "Tool1",
"fightaxe": "Tool1",
"magic": "Tool2",
"pumpgun": "Tool1",
"grenadelauncher": "Tool1",
"kopf": "Head",
"hals1": "Neck",
"schulterl": "R Shoulder",
"schuterr": "L Shoulder",
"armobenl": "R Upper Arm",
"armobenr": "L Upper Arm",
"armuntenl": "R Forearm",
"armuntenr": "L Forearm",
"handl": "R Hand",
"handr": "L Hand",
"örper": "Body",
"beckenl": "R Pelvis",
"beckenr": "L Pelvis",
"beinobenl": "R Upper Leg",
"beinobenr": "L Upper Leg",
"beinuntenl": "R Foreleg",
"beinuntenr": "L Foreleg",
"fuß2": "R Foot",
"fußr": "L Foot",
"cloak": "Cloak",
"camera": "Camera",
"tube": "Tool1",
"feather": "Tool2",
"tool1": "Tool1",
"tool2": "Tool2"
}
def get_vgroup_mapping(name):
name = name.lower()
if vgroup_map.get(name) != None:
return vgroup_map[name]
return None
action_map = {
"aimbow": "BowAim",
"bowaimride": "RideBowAim",
"bowload": "LoadBow",
"bowloadride": "RideLoadBow",
"armorputon": "PutOnArmor",
"magicride": "RideMagic",
}
def get_action_name_mapping(name):
name = name.lower()
if action_map.get(name) != None:
return action_map[name]
return None