-
Notifications
You must be signed in to change notification settings - Fork 1
/
core.py
331 lines (257 loc) · 12.2 KB
/
core.py
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
import bpy
import math
import json
import copy
import time
import os.path
import pathlib
from math import radians, degrees
from mathutils import Color, Matrix, Euler, Vector
from threading import Thread
from bpy.app.handlers import persistent
from . import utils
class SceneConstructor:
def __init__(self, name: str):
self.name = name
self.error_handler: ErrorHandler = ErrorHandler()
self.collection, self.layer_collection, self.scene_controller = self._create_scene_collection()
self.can_import_characters = utils.check_for_xps_importer()
self.active_armature = None
def _create_scene_collection(self):
collection = bpy.data.collections.new(self.name)
bpy.context.scene.collection.children.link(collection)
layer_collection = utils.find_layer_collection(collection.name)
scene_controller = utils.create_empty(link_collection=collection)
scene_controller.name = "Scene Controller"
return collection, layer_collection, scene_controller
def create_light(self, index, direction, intensity, color, shadow_depth):
# Set name
name = f"Light {index}"
# Create light
light_data = bpy.data.lights.new(name=name, type='SUN')
light = bpy.data.objects.new(name=name, object_data=light_data)
self.collection.objects.link(light)
# Rotate light direction from XPS to Blender
direction = utils.rotate(direction, (90, 0, 0))
sun = False
if sun:
# Set light properties
light.data.color = color
light.data.energy = 0.03
light.data.energy = 10 / 3 * intensity / 100
# Apply direction to light by pointing the light at the direction location
utils.look_at(light, direction)
# Move light above the scene
light.location[2] = 5
# Setup lights as point lights
else:
c = Color(color)
c.s += 0.1
light.data.type = "POINT"
light.location = -direction * 3
light.data.energy = intensity * 2.4
light.data.color = c
light.data.shadow_soft_size = 2
light.data.use_contact_shadow = True
light.data.contact_shadow_thickness = 0.005
# Create empty at the center so the light can be easily rotated
empty = utils.create_empty(link_collection=self.collection)
empty.location[2] += 1
empty.name = f"{light.name} Controller"
light.parent = empty
empty.parent = self.scene_controller
def create_camera(self, fov, target_pos, distance, rotation_horizontal, rotation_vertical):
# Create camera object
camera_data = bpy.data.cameras.new(name="Camera")
camera = bpy.data.objects.new(name="Camera", object_data=camera_data)
self.collection.objects.link(camera)
# Create camera controller
camera_controller = utils.create_empty(link_collection=self.collection)
camera_controller.name = "Camera Controller"
camera.parent = camera_controller
camera_controller.parent = self.scene_controller
# Move camera to the correct distance
camera.location[2] = distance
# Make camera look at the empty
utils.look_at(camera, camera_controller.location)
# Rotate target location from XPS space to Blender space
target_pos = utils.rotate(target_pos, (90, 0, 0))
# Move the controller to the target location
camera_controller.location = target_pos
# Rotate controller by the rotation values
camera_controller.rotation_euler = (radians(90) - rotation_vertical, 0, rotation_horizontal)
# Calculate the field of view angle
angle_unit = 0.0872664600610733 / 5
camera_angle = int(fov / angle_unit)
# Calculate focal length from angle
focal_length = camera.data.sensor_width / 2 / math.tan(radians(camera_angle) / 2)
# Apply the focal length
camera.data.lens = focal_length
# More camera settings
bpy.context.scene.render.resolution_percentage = 200
# Set this camera to active
bpy.context.scene.camera = camera
def add_character(self, file_directory, file_name, visibility):
self.active_armature = None
if not self.can_import_characters:
self.error_handler.add_error("XPS Importer not installed, skipping character import.")
return
# Search for the model directory in the install and asset folders
character_folder, mesh_file = utils.search_dirs_for_model(file_directory, file_name)
if not character_folder or not character_folder.exists():
folder_name = file_directory.split("\\")[-1]
self.error_handler.add_error(f"Model '{folder_name}' was not found in any selected folder. Full original path: '{file_directory}'")
return
if not mesh_file:
self.error_handler.add_error(f"Could not find any folder '{character_folder.name}' containing the file '{file_name}' (.xps, .mesh, .ascii).")
return
filepath_full = character_folder / mesh_file
print(f"\nImporting character {str(filepath_full)}...")
# Set the active collection to this XPS scene collection
bpy.context.view_layer.active_layer_collection = self.layer_collection
# Save all current collections to check witch one was added
collections_pre = [c for c in bpy.data.collections]
# Import the character from the given path
bpy.ops.xps_tools.import_model(
"EXEC_DEFAULT",
filepath=str(filepath_full),
)
print(f"Imported character {str(filepath_full)}...\n")
# Get the added collection
character_collection = None
for c in bpy.data.collections:
if c not in collections_pre:
character_collection = c
break
if not character_collection:
self.error_handler.add_error(f"Imported character '{filepath_full}' collection not found, skipping character import.")
return
# Hide all objects in the collection if they should be hidden
for obj in character_collection.objects:
# Hide and rename all accessories
if obj.parent and obj.parent.type == "ARMATURE":
name = obj.name.split("_")[1] # Separate name by underscores
name = " ".join([s.capitalize() for s in name.split(".")]) # Remove dots and capitalize
hide = False
if name.startswith("-"):
name = f"Item " + name[1:].capitalize()
hide = True
elif name.startswith("+"):
name = f"Item " + name[1:].capitalize()
if not visibility:
hide = True
# obj.name = name # TODO: judgearts thinks that renaming this to something more nice is not good. So it's disabled for now
utils.set_hide(obj, hide)
continue
# Hide the rest of the objects
utils.set_hide(obj, not visibility)
# Get the armature from the collection and set it as active
for obj in character_collection.objects:
if obj.type == "ARMATURE":
self.active_armature = obj
self.active_armature.parent = self.scene_controller
self.active_armature.name = character_folder.parts[-1]
utils.set_hide(self.active_armature, True)
break
if not self.active_armature:
self.error_handler.add_error(f"Character collection '{character_collection.name}' does not contain an armature, skipping character pose.")
return
# Move all objects from the character-collection to this xps scene collection
for obj in character_collection.objects:
self.collection.objects.link(obj)
character_collection.objects.unlink(obj)
# Delete the character collection
bpy.data.collections.remove(character_collection, do_unlink=True)
def pose_character(self, bone_name, rot, loc, scale):
if not self.active_armature:
return
# Get the bone
bone = self.active_armature.pose.bones.get(bone_name)
if not bone:
# print(f"Armature '{self.active_armature.name}' does not contain bone '{bone_name}', skipping bone pose.")
return
# Posing bone
if rot[0] or rot[1] or rot[2]:
utils.xps_bone_rotate(bone, Vector(rot))
if loc[0] or loc[1] or loc[2]:
utils.xps_bone_translate(bone, Vector(loc))
if scale[0] != 1 or scale[1] != 1 or scale[2] != 1:
# TODO: This is absolutely not like the XPS behavior, but it's somewhat close
utils.xps_bone_scale(bone, Vector(scale))
def transform_character(self, location, scale):
if not self.active_armature:
return
x, y, z = location
self.active_armature.location = (x, -z, y)
x, y, z = scale
self.active_armature.scale = (x, z, y)
def remove(self):
layer_collection = utils.find_layer_collection(self.collection.name)
utils.delete_hierarchy(layer_collection)
def set_camera_resolution(self, width, height):
if height > 10000 or width > 10000:
self.error_handler.add_error(f"Camera resolution is too big (>10000), likely due to incorrect reading of the scene file."
f"\n Please report this in our Github repository and attach this scene file.")
return
bpy.context.scene.render.resolution_x = width
bpy.context.scene.render.resolution_y = height
def create_ground(self, texture_path, visibility):
# Problem is that the scene always contains "data/ground.png" as the texture path, but it doesn't exist as a file
# XNALara probably defaults to the importing the floor mesh from data/Floor/Floor/Generic_Item.mesh
# Therefore we just import this by default
filepath = "data\\Floor\\Floor"
self.add_character(filepath, "generic_item", visibility)
plane_armature = self.active_armature
if not plane_armature:
self.error_handler.remove_error_containing_str(f"'{filepath}'")
self.error_handler.add_error(f"Could not find ground model, probably due to an unknown XPS installation folder.")
return
for obj in plane_armature.children:
if obj.type != "MESH":
continue
mat = obj.data.materials[0]
mat.shadow_method = 'NONE'
class ErrorHandler:
def __init__(self):
self.errors = []
def add_error(self, error):
if error not in self.errors:
self.errors.append(error)
print(error)
def get_error_message(self):
error_msg = "Errors while importing scene:"
for error in self.errors:
error_msg += f"\n- {error}"
print(error_msg)
return error_msg
def has_errors(self):
return len(self.errors) > 0
def remove_error_containing_str(self, containing_str):
[self.errors.remove(error) for error in self.errors if containing_str in error]
class SettingsHandler:
structure = {
"xps_importer_install_dir": "",
"xps_importer_asset_dir": "",
}
@staticmethod
def init():
# Add the load_settings function to the load_post handler in order to apply the settings as soon as a new file gets loaded
bpy.app.handlers.load_post.append(SettingsHandler.load_settings)
@staticmethod
def save_settings():
settings = copy.deepcopy(SettingsHandler.structure)
for key in settings.keys():
settings[key] = getattr(bpy.context.scene, key)
with open(utils.settings_file, "w+") as f:
json.dump(settings, f, indent=4)
@staticmethod
@persistent
def load_settings(arg1=None, arg2=None):
if not utils.settings_file.exists():
return
with open(utils.settings_file, "r") as f:
settings = json.load(f)
for key, value in settings.items():
if not value:
return
setattr(bpy.context.scene, key, value)