""" Leenkx Scene Exporter https://leenkx.com/ Based on Open Game Engine Exchange https://opengex.org/ Export plugin for Blender by Eric Lengyel Copyright 2015, Terathon Software LLC This software is licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported License: https://creativecommons.org/licenses/by-sa/3.0/deed.en_US """ from enum import Enum, unique import math import os import time from typing import Any, Dict, List, Tuple, Union, Optional, TYPE_CHECKING import bpy if bpy.app.version >= (3, 0, 0): VertexColorType = bpy.types.Attribute else: VertexColorType = bpy.types.MeshLoopColorLayer import numpy as np import bpy from mathutils import Matrix, Vector import bmesh import lnx.utils import lnx.profiler from lnx import assets, exporter_opt, log, make_renderpath from lnx.material import cycles, make as make_material, mat_batch if lnx.is_reload(__name__): assets = lnx.reload_module(assets) exporter_opt = lnx.reload_module(exporter_opt) log = lnx.reload_module(log) make_renderpath = lnx.reload_module(make_renderpath) cycles = lnx.reload_module(cycles) make_material = lnx.reload_module(make_material) mat_batch = lnx.reload_module(mat_batch) lnx.utils = lnx.reload_module(lnx.utils) lnx.profiler = lnx.reload_module(lnx.profiler) else: lnx.enable_reload(__name__) @unique class NodeType(Enum): """Represents the type of an object.""" EMPTY = 0 BONE = 1 MESH = 2 LIGHT = 3 CAMERA = 4 SPEAKER = 5 DECAL = 6 PROBE = 7 @classmethod def get_bobject_type(cls, bobject: bpy.types.Object) -> "NodeType": """Returns the NodeType enum member belonging to the type of the given blender object.""" if bobject.type == "MESH": if bobject.data.polygons: return cls.MESH elif bobject.type in ('FONT', 'META'): return cls.MESH elif bobject.type == "LIGHT": return cls.LIGHT elif bobject.type == "CAMERA": return cls.CAMERA elif bobject.type == "SPEAKER": return cls.SPEAKER elif bobject.type == "LIGHT_PROBE": return cls.PROBE return cls.EMPTY STRUCT_IDENTIFIER = ("object", "bone_object", "mesh_object", "light_object", "camera_object", "speaker_object", "decal_object", "probe_object") # Internal target names for single FCurve data paths FCURVE_TARGET_NAMES = { "location": ("xloc", "yloc", "zloc"), "rotation_euler": ("xrot", "yrot", "zrot"), "rotation_quaternion": ("qwrot", "qxrot", "qyrot", "qzrot"), "scale": ("xscl", "yscl", "zscl"), "delta_location": ("dxloc", "dyloc", "dzloc"), "delta_rotation_euler": ("dxrot", "dyrot", "dzrot"), "delta_rotation_quaternion": ("dqwrot", "dqxrot", "dqyrot", "dqzrot"), "delta_scale": ("dxscl", "dyscl", "dzscl"), } current_output = None class LeenkxExporter: """Export to Leenkx format. Some common naming patterns: - out_[]: Variables starting with "out_" represent data that is exported to Iron - bobject: A Blender object (bpy.types.Object). Used because `object` is a reserved Python keyword """ compress_enabled = False export_all_flag = True # Indicates whether rigid body is exported export_physics = False optimize_enabled = False option_mesh_only = False # Class names of referenced traits import_traits: List[str] = [] def __init__(self, context: bpy.types.Context, filepath: str, scene: bpy.types.Scene = None, depsgraph: bpy.types.Depsgraph = None): global current_output self.filepath = filepath self.scene = context.scene if scene is None else scene self.depsgraph = context.evaluated_depsgraph_get() if depsgraph is None else depsgraph # The output dict contains all data that is later exported to Iron format self.output: Dict[str, Any] = {'frame_time': 1.0 / (self.scene.render.fps / self.scene.render.fps_base)} current_output = self.output # Stores the object type ("objectType") and the asset name # ("structName") in a dict for each object self.bobject_array: Dict[bpy.types.Object, Dict[str, Union[NodeType, str]]] = {} self.bobject_bone_array = {} self.mesh_array = {} self.light_array = {} self.probe_array = {} self.camera_array = {} self.speaker_array = {} self.material_array = [] self.world_array = [] self.particle_system_array = {} self.referenced_collections: List[bpy.types.Collection] = [] """Collections referenced by collection instances""" self.has_spawning_camera = False """Whether there is at least one camera in the scene that spawns by default""" self.material_to_object_dict = {} # If no material is assigned, provide default to mimic cycles self.default_material_objects = [] self.default_skin_material_objects = [] self.default_part_material_objects = [] self.material_to_lnx_object_dict = {} # Stores the link between a blender object and its # corresponding export data (lnx object) self.object_to_lnx_object_dict: Dict[bpy.types.Object, Dict] = {} self.bone_tracks = [] LeenkxExporter.preprocess() @classmethod def export_scene(cls, context: bpy.types.Context, filepath: str, scene: bpy.types.Scene = None, depsgraph: bpy.types.Depsgraph = None) -> None: """Exports the given scene to the given file path. This is the function that is called in make.py and the entry point of the exporter.""" with lnx.profiler.Profile('profile_exporter.prof', lnx.utils.get_pref_or_default('profile_exporter', False)): cls(context, filepath, scene, depsgraph).execute() @classmethod def preprocess(cls): wrd = bpy.data.worlds['Lnx'] if wrd.lnx_physics == 'Enabled': cls.export_physics = True cls.export_navigation = False if wrd.lnx_navigation == 'Enabled': cls.export_navigation = True cls.export_ui = False cls.export_network = False if wrd.lnx_network == 'Enabled': cls.export_network = True @staticmethod def write_matrix(matrix): return [matrix[0][0], matrix[0][1], matrix[0][2], matrix[0][3], matrix[1][0], matrix[1][1], matrix[1][2], matrix[1][3], matrix[2][0], matrix[2][1], matrix[2][2], matrix[2][3], matrix[3][0], matrix[3][1], matrix[3][2], matrix[3][3]] def get_meshes_file_path(self, object_id: str, compressed=False) -> str: index = self.filepath.rfind('/') mesh_fp = self.filepath[:(index + 1)] + 'meshes/' if not os.path.exists(mesh_fp): os.makedirs(mesh_fp) ext = '.lz4' if compressed else '.lnx' return mesh_fp + object_id + ext @staticmethod def get_shape_keys(mesh): rpdat = lnx.utils.get_rp() if rpdat.lnx_morph_target != 'On': return False # Metaball if not hasattr(mesh, 'shape_keys'): return False shape_keys = mesh.shape_keys if not shape_keys: return False if len(shape_keys.key_blocks) < 2: return False for shape_key in shape_keys.key_blocks[1:]: if not shape_key.mute: return True return False @staticmethod def get_morph_uv_index(mesh): i = 0 for uv_layer in mesh.uv_layers: if uv_layer.name == 'UVMap_shape_key': return i i +=1 def find_bone(self, name: str) -> Optional[Tuple[bpy.types.Bone, Dict]]: """Finds the bone reference (a tuple containing the bone object and its data) by the given name and returns it.""" for bone_ref in self.bobject_bone_array.items(): if bone_ref[0].name == name: return bone_ref return None @staticmethod def collect_bone_animation(armature: bpy.types.Object, name: str) -> List[bpy.types.FCurve]: path = f"pose.bones[\"{name}\"]." if armature.animation_data: action = armature.animation_data.action if action: return [fcurve for fcurve in action.fcurves if fcurve.data_path.startswith(path)] return [] def export_bone(self, armature, bone: bpy.types.Bone, o, action: bpy.types.Action): rpdat = lnx.utils.get_rp() bobject_ref = self.bobject_bone_array.get(bone) if rpdat.lnx_use_armature_deform_only: if not bone.use_deform: return if bobject_ref: o['type'] = STRUCT_IDENTIFIER[bobject_ref["objectType"].value] o['name'] = bobject_ref["structName"] self.export_bone_transform(armature, bone, o, action) self.export_bone_layers(armature, bone, o) o['bone_length'] = bone.length o['children'] = [] for sub_bobject in bone.children: so = {} self.export_bone(armature, sub_bobject, so, action) o['children'].append(so) @staticmethod def export_pose_markers(oanim, action): if action.pose_markers is None or len(action.pose_markers) == 0: return oanim['marker_frames'] = [] oanim['marker_names'] = [] for pos_marker in action.pose_markers: oanim['marker_frames'].append(int(pos_marker.frame)) oanim['marker_names'].append(pos_marker.name) @staticmethod def export_root_motion(oanim, action): oanim['root_motion_pos'] = action.lnx_root_motion_pos oanim['root_motion_rot'] = action.lnx_root_motion_rot @staticmethod def calculate_anim_frame_range(action: bpy.types.Action) -> Tuple[int, int]: """Calculates the required frame range of the given action by also taking fcurve modifiers into account. Modifiers that are not range-restricted are ignored in this calculation. """ start = action.frame_range[0] end = action.frame_range[1] # Take FCurve modifiers into account if they have a restricted # frame range for fcurve in action.fcurves: for modifier in fcurve.modifiers: if not modifier.use_restricted_range: continue if modifier.frame_start < start: start = modifier.frame_start if modifier.frame_end > end: end = modifier.frame_end return int(start), int(end) @staticmethod def export_animation_track(fcurve: bpy.types.FCurve, frame_range: Tuple[int, int], target: str) -> Dict: """This function exports a single animation track.""" out_track = {'target': target, 'frames': [], 'values': []} start = frame_range[0] end = frame_range[1] for frame in range(start, end + 1): out_track['frames'].append(frame) out_track['values'].append(fcurve.evaluate(frame)) return out_track def export_object_transform(self, bobject: bpy.types.Object, o): wrd = bpy.data.worlds['Lnx'] # HACK: In Blender 4.2.x, each camera must be selected to ensure its matrix is correctly assigned if bpy.app.version >= (4, 2, 0) and bobject.type == 'CAMERA' and bobject.users_scene: current_scene = bpy.context.window.scene bpy.context.window.scene = bobject.users_scene[0] bpy.context.view_layer.update() bobject.select_set(True) bpy.context.view_layer.update() bobject.select_set(False) bpy.context.window.scene = current_scene bpy.context.view_layer.update() # Static transform o['transform'] = {'values': LeenkxExporter.write_matrix(bobject.matrix_local)} # Animated transform if bobject.animation_data is not None and bobject.type != "ARMATURE": action = bobject.animation_data.action if action is not None: action_name = lnx.utils.safestr(lnx.utils.asset_name(action)) fp = self.get_meshes_file_path('action_' + action_name, compressed=LeenkxExporter.compress_enabled) assets.add(fp) ext = '.lz4' if LeenkxExporter.compress_enabled else '' if ext == '' and not wrd.lnx_minimize: ext = '.json' if 'object_actions' not in o: o['object_actions'] = [] o['object_actions'].append('action_' + action_name + ext) frame_range = self.calculate_anim_frame_range(action) out_anim = { 'begin': frame_range[0], 'end': frame_range[1], 'tracks': [] } self.export_pose_markers(out_anim, action) unresolved_data_paths = set() for fcurve in action.fcurves: data_path = fcurve.data_path try: out_track = self.export_animation_track(fcurve, frame_range, FCURVE_TARGET_NAMES[data_path][fcurve.array_index]) except KeyError: if data_path not in FCURVE_TARGET_NAMES: # This can happen if the target is simply not # supported or the action shares both bone # and object transform data (FCURVE_TARGET_NAMES # only contains object transform targets) unresolved_data_paths.add(data_path) continue # Missing target entry for array_index or something else raise if data_path.startswith('delta_'): out_anim['has_delta'] = True out_anim['tracks'].append(out_track) if len(unresolved_data_paths) > 0: warning = ( f'The action "{action_name}" has fcurve channels with data paths that could not be resolved.' ' This can be caused by the following things:\n' ' - The data paths are not supported.\n' ' - The action exists on both armature and non-armature objects or has both bone and object transform data.' ) if wrd.lnx_verbose_output: warning += f'\n Unresolved data paths: {unresolved_data_paths}' else: warning += '\n To see the list of unresolved data paths please recompile with Leenkx Project > Verbose Output enabled.' log.warn(warning) if True: # not action.lnx_cached or not os.path.exists(fp): if wrd.lnx_verbose_output: print('Exporting object action ' + action_name) out_object_action = { 'name': action_name, 'anim': out_anim, 'type': 'object', 'data_ref': '', 'transform': None } action_file = {'objects': [out_object_action]} lnx.utils.write_lnx(fp, action_file) def process_bone(self, bone: bpy.types.Bone) -> None: if LeenkxExporter.export_all_flag or bone.select: self.bobject_bone_array[bone] = { "objectType": NodeType.BONE, "structName": bone.name } for subbobject in bone.children: self.process_bone(subbobject) def process_bobject(self, bobject: bpy.types.Object) -> None: """Stores some basic information about the given object (its name and type). If the given object is an armature, its bones are also processed. """ if LeenkxExporter.export_all_flag or bobject.select_get(): btype: NodeType = NodeType.get_bobject_type(bobject) if btype is not NodeType.MESH and LeenkxExporter.option_mesh_only: return self.bobject_array[bobject] = { "objectType": btype, "structName": lnx.utils.asset_name(bobject) } if bobject.type == "ARMATURE": armature: bpy.types.Armature = bobject.data if armature: for bone in armature.bones: if not bone.parent: self.process_bone(bone) if bobject.lnx_instanced == 'Off': for subbobject in bobject.children: self.process_bobject(subbobject) def process_skinned_meshes(self): """Iterates through all objects that are exported and ensures that bones are actually stored as bones.""" for bobject_ref in self.bobject_array.items(): if bobject_ref[1]["objectType"] is NodeType.MESH: armature = bobject_ref[0].find_armature() if armature is not None: for bone in armature.data.bones: bone_ref = self.find_bone(bone.name) if bone_ref is not None: # If an object is used as a bone, then we # force its type to be a bone bone_ref[1]["objectType"] = NodeType.BONE def export_bone_transform(self, armature: bpy.types.Object, bone: bpy.types.Bone, o, action: bpy.types.Action): pose_bone = armature.pose.bones.get(bone.name) # if pose_bone is not None: # transform = pose_bone.matrix.copy() # if pose_bone.parent is not None: # transform = pose_bone.parent.matrix.inverted_safe() * transform # else: transform = bone.matrix_local.copy() if bone.parent is not None: transform = (bone.parent.matrix_local.inverted_safe() @ transform) o['transform'] = {'values': LeenkxExporter.write_matrix(transform)} fcurve_list = self.collect_bone_animation(armature, bone.name) if fcurve_list and pose_bone: begin_frame, end_frame = int(action.frame_range[0]), int(action.frame_range[1]) out_track = {'target': "transform", 'frames': [], 'values': []} o['anim'] = {'tracks': [out_track]} for i in range(begin_frame, end_frame + 1): out_track['frames'].append(i - begin_frame) self.bone_tracks.append((out_track['values'], pose_bone)) def export_bone_layers(self, armature: bpy.types.Object, bone: bpy.types.Bone, o): layers = [] if bpy.app.version < (4, 0, 0): for layer in bone.layers: layers.append(layer) else: for bonecollection in armature.data.collections: layers.append(bonecollection.is_visible) o['bone_layers'] = layers def use_default_material(self, bobject: bpy.types.Object, o): if lnx.utils.export_bone_data(bobject): o['material_refs'].append('lnxdefaultskin') self.default_skin_material_objects.append(bobject) else: o['material_refs'].append('lnxdefault') self.default_material_objects.append(bobject) def use_default_material_part(self): """Select the particle material variant for all particle system instance objects that use the lnxdefault material. """ for ps in bpy.data.particles: if ps.render_type != 'OBJECT' or ps.instance_object is None or not ps.instance_object.lnx_export: continue po = ps.instance_object if po not in self.object_to_lnx_object_dict: self.process_bobject(po) self.export_object(po) o = self.object_to_lnx_object_dict[po] # Check if the instance object uses the lnxdefault material if len(o['material_refs']) > 0 and o['material_refs'][0] == 'lnxdefault' and po not in self.default_part_material_objects: self.default_part_material_objects.append(po) o['material_refs'] = ['lnxdefaultpart'] # Replace lnxdefault def export_material_ref(self, bobject: bpy.types.Object, material, index, o): if material is None: # Use default for empty mat slots self.use_default_material(bobject, o) return if material not in self.material_array: self.material_array.append(material) o['material_refs'].append(lnx.utils.asset_name(material)) def export_particle_system_ref(self, psys: bpy.types.ParticleSystem, out_object): if psys.settings.instance_object is None or psys.settings.render_type != 'OBJECT' or not psys.settings.instance_object.lnx_export: return for obj in bpy.data.objects: if obj.name == out_object['name']: for mod in obj.modifiers: if mod.type == 'PARTICLE_SYSTEM': if mod.particle_system.name == psys.name: if not mod.show_render: return self.particle_system_array[psys.settings] = {"structName": psys.settings.name} pref = { 'name': psys.name, 'seed': psys.seed, 'particle': psys.settings.name } out_object['particle_refs'].append(pref) @staticmethod def get_view3d_area() -> Optional[bpy.types.Area]: screen = bpy.context.window.screen for area in screen.areas: if area.type == 'VIEW_3D': return area return None @staticmethod def get_viewport_view_matrix() -> Optional[Matrix]: play_area = LeenkxExporter.get_view3d_area() if play_area is None: return None for space in play_area.spaces: if space.type == 'VIEW_3D': return space.region_3d.view_matrix return None @staticmethod def get_viewport_projection_matrix() -> Tuple[Optional[Matrix], bool]: play_area = LeenkxExporter.get_view3d_area() if play_area is None: return None, False for space in play_area.spaces: if space.type == 'VIEW_3D': # return space.region_3d.perspective_matrix # pesp = window * view return space.region_3d.window_matrix, space.region_3d.is_perspective return None, False def write_bone_matrices(self, scene, action): # profile_time = time.time() begin_frame, end_frame = int(action.frame_range[0]), int(action.frame_range[1]) if len(self.bone_tracks) > 0: for i in range(begin_frame, end_frame + 1): scene.frame_set(i) for track in self.bone_tracks: values, pose_bone = track[0], track[1] parent = pose_bone.parent if parent: values += LeenkxExporter.write_matrix((parent.matrix.inverted_safe() @ pose_bone.matrix)) else: values += LeenkxExporter.write_matrix(pose_bone.matrix) # print('Bone matrices exported in ' + str(time.time() - profile_time)) @staticmethod def has_baked_material(bobject, materials): for mat in materials: if mat is None: continue baked_mat = mat.name + '_' + bobject.name + '_baked' if baked_mat in bpy.data.materials: return True return False @staticmethod def create_material_variants(scene: bpy.types.Scene) -> Tuple[List[bpy.types.Material], List[bpy.types.MaterialSlot]]: """Creates unique material variants for skinning, tilesheets and particles.""" matvars: List[bpy.types.Material] = [] matslots: List[bpy.types.MaterialSlot] = [] bobject: bpy.types.Object for bobject in scene.collection.all_objects.values(): variant_suffix = '' # Skinning if lnx.utils.export_bone_data(bobject): variant_suffix = '_lnxskin' # Tilesheets elif bobject.lnx_tilesheet != '': if not bobject.lnx_use_custom_tilesheet_node: variant_suffix = '_lnxtile' elif lnx.utils.export_morph_targets(bobject): variant_suffix = '_lnxskey' if variant_suffix == '': continue for slot in bobject.material_slots: if slot.material is None: continue if slot.material.library is not None: slot.material.lnx_particle_flag = True continue if slot.material.name.endswith(variant_suffix): continue matslots.append(slot) mat_name = slot.material.name + variant_suffix mat = bpy.data.materials.get(mat_name) # Create material variant if mat is None: mat = slot.material.copy() mat.name = mat_name if variant_suffix == '_lnxtile': mat.lnx_tilesheet_flag = True matvars.append(mat) slot.material = mat # Particle and non-particle objects can not share material particle_sys: bpy.types.ParticleSettings for particle_sys in bpy.data.particles: bobject = particle_sys.instance_object if bobject is None or particle_sys.render_type != 'OBJECT' or not bobject.lnx_export: continue for slot in bobject.material_slots: if slot.material is None or slot.material.library is not None: continue if slot.material.name.endswith('_lnxpart'): continue matslots.append(slot) mat_name = slot.material.name + '_lnxpart' mat = bpy.data.materials.get(mat_name) if mat is None: mat = slot.material.copy() mat.name = mat_name mat.lnx_particle_flag = True matvars.append(mat) slot.material = mat return matvars, matslots @staticmethod def slot_to_material(bobject: bpy.types.Object, slot: bpy.types.MaterialSlot): mat = slot.material # Pick up backed material if present if mat is not None: baked_mat = mat.name + '_' + bobject.name + '_baked' if baked_mat in bpy.data.materials: mat = bpy.data.materials[baked_mat] return mat # def ExportMorphWeights(self, node, shapeKeys, scene): # action = None # curveArray = [] # indexArray = [] # if (shapeKeys.animation_data): # action = shapeKeys.animation_data.action # if (action): # for fcurve in action.fcurves: # if ((fcurve.data_path.startswith("key_blocks[")) and (fcurve.data_path.endswith("].value"))): # keyName = fcurve.data_path.strip("abcdehklopstuvy[]_.") # if ((keyName[0] == "\"") or (keyName[0] == "'")): # index = shapeKeys.key_blocks.find(keyName.strip("\"'")) # if (index >= 0): # curveArray.append(fcurve) # indexArray.append(index) # else: # curveArray.append(fcurve) # indexArray.append(int(keyName)) # if ((not action) and (node.animation_data)): # action = node.animation_data.action # if (action): # for fcurve in action.fcurves: # if ((fcurve.data_path.startswith("data.shape_keys.key_blocks[")) and (fcurve.data_path.endswith("].value"))): # keyName = fcurve.data_path.strip("abcdehklopstuvy[]_.") # if ((keyName[0] == "\"") or (keyName[0] == "'")): # index = shapeKeys.key_blocks.find(keyName.strip("\"'")) # if (index >= 0): # curveArray.append(fcurve) # indexArray.append(index) # else: # curveArray.append(fcurve) # indexArray.append(int(keyName)) # animated = (len(curveArray) != 0) # referenceName = shapeKeys.reference_key.name if (shapeKeys.use_relative) else "" # for k in range(len(shapeKeys.key_blocks)): # self.IndentWrite(B"MorphWeight", 0, (k == 0)) # if (animated): # self.Write(B" %mw") # self.WriteInt(k) # self.Write(B" (index = ") # self.WriteInt(k) # self.Write(B") {float {") # block = shapeKeys.key_blocks[k] # self.WriteFloat(block.value if (block.name != referenceName) else 1.0) # self.Write(B"}}\n") # if (animated): # self.IndentWrite(B"Animation (begin = ", 0, True) # self.WriteFloat((action.frame_range[0]) * self.frameTime) # self.Write(B", end = ") # self.WriteFloat((action.frame_range[1]) * self.frameTime) # self.Write(B")\n") # self.IndentWrite(B"{\n") # self.indentLevel += 1 # structFlag = False # for a in range(len(curveArray)): # k = indexArray[a] # target = bytes("mw" + str(k), "UTF-8") # fcurve = curveArray[a] # kind = OpenGexExporter.ClassifyAnimationCurve(fcurve) # if ((kind != kAnimationSampled) and (not self.sampleAnimationFlag)): # self.ExportAnimationTrack(fcurve, kind, target, structFlag) # else: # self.ExportMorphWeightSampledAnimationTrack(shapeKeys.key_blocks[k], target, scene, structFlag) # structFlag = True # self.indentLevel -= 1 # self.IndentWrite(B"}\n") def export_object(self, bobject: bpy.types.Object, out_parent: Dict = None) -> None: """This function exports a single object in the scene and includes its name, object reference, material references (for meshes), and transform. Subobjects are then exported recursively. """ if not bobject.lnx_export: return bobject_ref = self.bobject_array.get(bobject) if bobject_ref is not None: object_type = bobject_ref["objectType"] # Linked object, not present in scene if bobject not in self.object_to_lnx_object_dict: out_object = { 'traits': [], 'spawn': False } self.object_to_lnx_object_dict[bobject] = out_object out_object = self.object_to_lnx_object_dict[bobject] out_object['type'] = STRUCT_IDENTIFIER[object_type.value] out_object['name'] = bobject_ref["structName"] if bobject.parent_type == "BONE": out_object['parent_bone'] = bobject.parent_bone if bobject.hide_render or not bobject.lnx_visible: out_object['visible'] = False if bpy.app.version < (3, 0, 0): if not bobject.cycles_visibility: out_object['visible_mesh'] = False out_object['visible_shadow'] = False else: if not bobject.visible_camera: out_object['visible_mesh'] = False if not bobject.lnx_visible_shadow: out_object['visible_shadow'] = False if not bobject.lnx_spawn: out_object['spawn'] = False out_object['mobile'] = bobject.lnx_mobile if bobject.instance_type == 'COLLECTION' and bobject.instance_collection is not None: out_object['group_ref'] = bobject.instance_collection.name self.referenced_collections.append(bobject.instance_collection) if bobject.lnx_tilesheet != '': out_object['tilesheet_ref'] = bobject.lnx_tilesheet out_object['tilesheet_action_ref'] = bobject.lnx_tilesheet_action if len(bobject.vertex_groups) > 0: out_object['vertex_groups'] = [] for group in bobject.vertex_groups: verts = [] for v in bobject.data.vertices: for g in v.groups: if g.group == group.index: verts.append(str(v.co.x)) verts.append(str(v.co.y)) verts.append(str(v.co.z)) out_vertex_groups = { 'name': group.name, 'value': verts } out_object['vertex_groups'].append(out_vertex_groups) if len(bobject.lnx_camera_list) > 0: out_camera_list = [] for camera in bobject.lnx_camera_list: if camera.lnx_camera_object_ptr != None: out_camera_list.append(camera.lnx_camera_object_ptr.name) if len(out_camera_list) > 0: out_object['camera_list'] = out_camera_list if len(bobject.lnx_propertylist) > 0: out_object['properties'] = [] for proplist_item in bobject.lnx_propertylist: # Check if the property is a collection (array type). if proplist_item.type_prop == 'array': # Convert the collection to a list. array_type = proplist_item.array_item_type collection_value = getattr(proplist_item, 'array_prop') property_name = array_type + '_prop' value = [str(getattr(item, property_name)) for item in collection_value] else: # Handle other types of properties. value = getattr(proplist_item, proplist_item.type_prop + '_prop') out_property = { 'name': proplist_item.name_prop, 'value': value } out_object['properties'].append(out_property) # Export the object reference and material references objref = bobject.data if objref is not None: objname = lnx.utils.asset_name(objref) # LOD if bobject.type == 'MESH' and hasattr(objref, 'lnx_lodlist') and len(objref.lnx_lodlist) > 0: out_object['lods'] = [] for lodlist_item in objref.lnx_lodlist: if not lodlist_item.enabled_prop: continue out_lod = { 'object_ref': lodlist_item.name, 'screen_size': lodlist_item.screen_size_prop } out_object['lods'].append(out_lod) if objref.lnx_lod_material: out_object['lod_material'] = True if object_type is NodeType.MESH: if objref not in self.mesh_array: self.mesh_array[objref] = {"structName": objname, "objectTable": [bobject]} else: self.mesh_array[objref]["objectTable"].append(bobject) oid = lnx.utils.safestr(self.mesh_array[objref]["structName"]) wrd = bpy.data.worlds['Lnx'] if wrd.lnx_single_data_file: out_object['data_ref'] = oid else: ext = '' if not LeenkxExporter.compress_enabled else '.lz4' if ext == '' and not bpy.data.worlds['Lnx'].lnx_minimize: ext = '.json' out_object['data_ref'] = 'mesh_' + oid + ext + '/' + oid out_object['material_refs'] = [] for i in range(len(bobject.material_slots)): mat = self.slot_to_material(bobject, bobject.material_slots[i]) # Export ref self.export_material_ref(bobject, mat, i, out_object) # Decal flag if mat is not None and mat.lnx_decal: out_object['type'] = 'decal_object' # No material, mimic cycles and assign default if len(out_object['material_refs']) == 0: self.use_default_material(bobject, out_object) num_psys = len(bobject.particle_systems) if num_psys > 0: out_object['particle_refs'] = [] out_object['render_emitter'] = bobject.show_instancer_for_render for i in range(num_psys): for obj in bpy.data.objects: for mod in obj.modifiers: if mod.type == 'PARTICLE_SYSTEM': if mod.particle_system.name == bobject.particle_systems[i].name: if mod.show_render: self.export_particle_system_ref(bobject.particle_systems[i], out_object) aabb = bobject.data.lnx_aabb if aabb[0] == 0 and aabb[1] == 0 and aabb[2] == 0: self.calc_aabb(bobject) out_object['dimensions'] = [aabb[0], aabb[1], aabb[2]] # shapeKeys = LeenkxExporter.get_shape_keys(objref) # if shapeKeys: # self.ExportMorphWeights(bobject, shapeKeys, scene, out_object) elif object_type is NodeType.LIGHT: if objref not in self.light_array: self.light_array[objref] = {"structName" : objname, "objectTable" : [bobject]} else: self.light_array[objref]["objectTable"].append(bobject) out_object['data_ref'] = self.light_array[objref]["structName"] elif object_type is NodeType.PROBE: if objref not in self.probe_array: self.probe_array[objref] = {"structName" : objname, "objectTable" : [bobject]} else: self.probe_array[objref]["objectTable"].append(bobject) dist = bobject.data.influence_distance if objref.type == "PLANAR": out_object['dimensions'] = [1.0, 1.0, dist] # GRID, CUBEMAP else: out_object['dimensions'] = [dist, dist, dist] out_object['data_ref'] = self.probe_array[objref]["structName"] elif object_type is NodeType.CAMERA: if out_object.get('spawn', True): # Also spawn object if 'spawn' attr doesn't exist self.has_spawning_camera = True if objref not in self.camera_array: self.camera_array[objref] = {"structName" : objname, "objectTable" : [bobject]} else: self.camera_array[objref]["objectTable"].append(bobject) out_object['data_ref'] = self.camera_array[objref]["structName"] elif object_type is NodeType.SPEAKER: if objref not in self.speaker_array: self.speaker_array[objref] = {"structName" : objname, "objectTable" : [bobject]} else: self.speaker_array[objref]["objectTable"].append(bobject) out_object['data_ref'] = self.speaker_array[objref]["structName"] # Export the transform. If object is animated, then animation tracks are exported here if bobject.type != 'ARMATURE' and bobject.animation_data is not None: action = bobject.animation_data.action export_actions = [action] for track in bobject.animation_data.nla_tracks: if track.strips is None: continue for strip in track.strips: if strip.action is None or strip.action in export_actions: continue export_actions.append(strip.action) orig_action = action for a in export_actions: bobject.animation_data.action = a self.export_object_transform(bobject, out_object) if len(export_actions) >= 2 and export_actions[0] is None: # No action assigned out_object['object_actions'].insert(0, 'null') bobject.animation_data.action = orig_action else: self.export_object_transform(bobject, out_object) # If the object is parented to a bone and is not relative, then undo the bone's transform if bobject.parent_type == "BONE": armature = bobject.parent.data bone = armature.bones[bobject.parent_bone] # if not bone.use_relative_parent: out_object['parent_bone_connected'] = bone.use_connect if bone.use_connect: bone_translation = Vector((0, bone.length, 0)) + bone.head out_object['parent_bone_tail'] = [bone_translation[0], bone_translation[1], bone_translation[2]] else: bone_translation = bone.tail - bone.head out_object['parent_bone_tail'] = [bone_translation[0], bone_translation[1], bone_translation[2]] pose_bone = bobject.parent.pose.bones[bobject.parent_bone] bone_translation_pose = pose_bone.tail - pose_bone.head out_object['parent_bone_tail_pose'] = [bone_translation_pose[0], bone_translation_pose[1], bone_translation_pose[2]] if bobject.type == 'ARMATURE' and bobject.data is not None: # Armature data bdata = bobject.data # Reference start action action = None adata = bobject.animation_data # Active action if adata is not None: action = adata.action if action is None: log.warn('Object ' + bobject.name + ' - No action assigned, setting to pose') bobject.animation_data_create() actions = bpy.data.actions action = actions.get('leenkxpose') if action is None: action = actions.new(name='leenkxpose') adata.action = action # Export actions export_actions = [action] # hasattr - armature modifier may reference non-parent # armature object to deform with if hasattr(adata, 'nla_tracks') and adata.nla_tracks is not None: for track in adata.nla_tracks: if track.strips is None: continue for strip in track.strips: if strip.action is None: continue if strip.action.name == action.name: continue export_actions.append(strip.action) armatureid = lnx.utils.safestr(lnx.utils.asset_name(bdata)) ext = '.lz4' if LeenkxExporter.compress_enabled else '' if ext == '' and not bpy.data.worlds['Lnx'].lnx_minimize: ext = '.json' out_object['bone_actions'] = [] for action in export_actions: aname = lnx.utils.safestr(lnx.utils.asset_name(action)) out_object['bone_actions'].append('action_' + armatureid + '_' + aname + ext) clear_op = set() skelobj = bobject baked_actions = [] orig_action = bobject.animation_data.action if bdata.lnx_autobake and bobject.name not in bpy.context.collection.all_objects: clear_op.add('unlink') # Clone bobject and put it in the current scene so # the bake operator can run if bobject.library is not None: skelobj = bobject.copy() clear_op.add('rem') bpy.context.collection.objects.link(skelobj) for action in export_actions: aname = lnx.utils.safestr(lnx.utils.asset_name(action)) skelobj.animation_data.action = action fp = self.get_meshes_file_path('action_' + armatureid + '_' + aname, compressed=LeenkxExporter.compress_enabled) assets.add(fp) if not bdata.lnx_cached or not os.path.exists(fp): # Store action to use it after autobake was handled original_action = action # Handle autobake if bdata.lnx_autobake: sel = bpy.context.selected_objects[:] for _o in sel: _o.select_set(False) skelobj.select_set(True) bake_result = bpy.ops.nla.bake( frame_start=int(action.frame_range[0]), frame_end=int(action.frame_range[1]), step=1, only_selected=False, visual_keying=True ) action = skelobj.animation_data.action skelobj.select_set(False) for _o in sel: _o.select_set(True) # Baking creates a new action, but only if it # was successful if 'FINISHED' in bake_result: baked_actions.append(action) wrd = bpy.data.worlds['Lnx'] if wrd.lnx_verbose_output: print('Exporting armature action ' + aname) bones = [] self.bone_tracks = [] for bone in bdata.bones: if not bone.parent: boneo = {} self.export_bone(skelobj, bone, boneo, action) bones.append(boneo) self.write_bone_matrices(bpy.context.scene, action) if len(bones) > 0 and 'anim' in bones[0]: self.export_pose_markers(bones[0]['anim'], original_action) self.export_root_motion(bones[0]['anim'], original_action) # Save action separately action_obj = {'name': aname, 'objects': bones} lnx.utils.write_lnx(fp, action_obj) # Use relative bone constraints out_object['relative_bone_constraints'] = bdata.lnx_relative_bone_constraints # Restore settings skelobj.animation_data.action = orig_action for a in baked_actions: bpy.data.actions.remove(a, do_unlink=True) if 'unlink' in clear_op: bpy.context.collection.objects.unlink(skelobj) if 'rem' in clear_op: bpy.data.objects.remove(skelobj, do_unlink=True) # TODO: cache per action bdata.lnx_cached = True if out_parent is None: self.output['objects'].append(out_object) else: out_parent['children'].append(out_object) self.post_export_object(bobject, out_object, object_type) if not hasattr(out_object, 'children') and len(bobject.children) > 0: out_object['children'] = [] if bobject.lnx_instanced == 'Off': for subbobject in bobject.children: self.export_object(subbobject, out_object) def export_skin(self, bobject: bpy.types.Object, armature, export_mesh: bpy.types.Mesh, out_mesh): """This function exports all skinning data, which includes the skeleton and per-vertex bone influence data""" oskin = {} out_mesh['skin'] = oskin # Write the skin bind pose transform otrans = {'values': LeenkxExporter.write_matrix(bobject.matrix_world)} oskin['transform'] = otrans bone_array = armature.data.bones bone_count = len(bone_array) rpdat = lnx.utils.get_rp() max_bones = rpdat.lnx_skin_max_bones bone_count = min(bone_count, max_bones) # Write the bone object reference array oskin['bone_ref_array'] = np.empty(bone_count, dtype=object) oskin['bone_len_array'] = np.empty(bone_count, dtype='= 0: #and bone_weight != 0.0: bone_values.append((bone_weight, bone_index)) total_weight += bone_weight bone_count += 1 if bone_count > 4: bone_count = 4 bone_values.sort(reverse=True) bone_values = bone_values[:4] bone_count_array[index] = bone_count for bv in bone_values: bone_weight_array[count] = bv[0] bone_index_array[count] = bv[1] count += 1 if total_weight not in (0.0, 1.0): normalizer = 1.0 / total_weight for i in range(bone_count): bone_weight_array[count - i - 1] *= normalizer bone_index_array = bone_index_array[:count] bone_weight_array = bone_weight_array[:count] bone_weight_array *= 32767 bone_weight_array = np.array(bone_weight_array, dtype=' 0: if 'constraints' not in oskin: oskin['constraints'] = [] self.add_constraints(bone, oskin, bone=True) def export_shape_keys(self, bobject: bpy.types.Object, export_mesh: bpy.types.Mesh, out_mesh): # Max shape keys supported max_shape_keys = 32 # Path to store shape key textures output_dir = bpy.path.abspath('//') + "MorphTargets" name = bobject.data.name vert_pos = [] vert_nor = [] names = [] default_values = [0] * max_shape_keys # Shape key base mesh shape_key_base = bobject.data.shape_keys.key_blocks[0] count = 0 # Loop through all shape keys for shape_key in bobject.data.shape_keys.key_blocks[1:]: if count > max_shape_keys - 1: break # get vertex data from shape key if shape_key.mute: continue vert_data = self.get_vertex_data_from_shape_key(shape_key_base, shape_key) vert_pos.append(vert_data['pos']) vert_nor.append(vert_data['nor']) names.append(shape_key.name) default_values[count] = shape_key.value count += 1 # No shape keys present or all shape keys are muted if count < 1: return # Convert to array for easy manipulation pos_array = np.array(vert_pos) nor_array = np.array(vert_nor) # Min and Max values of shape key displacements max = np.amax(pos_array) min = np.amin(pos_array) array_size = len(pos_array[0]), len(pos_array) # Get best 2^n image size to fit shape key data (min = 2 X 2, max = 4096 X 4096) img_size, extra_zeros, block_size = self.get_best_image_size(array_size) # Image size required is too large. Skip export if img_size < 1: log.error(f"""object {bobject.name} contains too many vertices or shape keys to support shape keys export""") self.remove_morph_uv_set(bobject) return # Write data to image self.bake_to_image(pos_array, nor_array, max, min, extra_zeros, img_size, name, output_dir) # Create a new UV set for shape keys self.create_morph_uv_set(bobject, img_size) # Export Shape Key names, defaults, etc.. morph_target = {} morph_target['morph_target_data_file'] = name morph_target['morph_target_ref'] = names morph_target['morph_target_defaults'] = default_values morph_target['num_morph_targets'] = count morph_target['morph_scale'] = max - min morph_target['morph_offset'] = min morph_target['morph_img_size'] = img_size morph_target['morph_block_size'] = block_size out_mesh['morph_target'] = morph_target return def get_vertex_data_from_shape_key(self, shape_key_base, shape_key_data): base_vert_pos = shape_key_base.data.values() base_vert_nor = shape_key_base.normals_split_get() vert_pos = shape_key_data.data.values() vert_nor = shape_key_data.normals_split_get() num_verts = len(vert_pos) pos = [] nor = [] # Loop through all vertices for i in range(num_verts): # Vertex position relative to base vertex pos.append(list(vert_pos[i].co - base_vert_pos[i].co)) temp = [] for j in range(3): # Vertex normal relative to base vertex temp.append(vert_nor[j + i * 3] - base_vert_nor[j + i * 3]) nor.append(temp) return {'pos': pos, 'nor': nor} def bake_to_image(self, pos_array, nor_array, pos_max, pos_min, extra_x, img_size, name, output_dir): # Scale position data between [0, 1] to bake to image pos_array_scaled = np.interp(pos_array, (pos_min, pos_max), (0, 1)) # Write positions to image self.write_output_image(pos_array_scaled, extra_x, img_size, name + '_morph_pos', output_dir) # Scale normal data between [0, 1] to bake to image nor_array_scaled = np.interp(nor_array, (-1, 1), (0, 1)) # Write normals to image self.write_output_image(nor_array_scaled, extra_x, img_size, name + '_morph_nor', output_dir) def write_output_image(self, data, extra_x, img_size, name, output_dir): # Pad data with zeros to make up for required number of pixels of 2^n format data = np.pad(data, ((0, 0), (0, extra_x), (0, 0)), 'minimum') pixel_list = [] for y in range(len(data)): for x in range(len(data[0])): # assign RGBA pixel_list.append(data[y, x, 0]) pixel_list.append(data[y, x, 1]) pixel_list.append(data[y, x, 2]) pixel_list.append(1.0) pixel_list = (pixel_list + [0] * (img_size * img_size * 4 - len(pixel_list))) image = bpy.data.images.new(name, width = img_size, height = img_size, is_data = True) image.pixels = pixel_list output_path = os.path.join(output_dir, name + ".png") image.save_render(output_path, scene= bpy.context.scene) bpy.data.images.remove(image) def get_best_image_size(self, size): for i in range(1, 12): block_len = pow(2, i) block_height = np.ceil(size[0]/block_len) if block_height * size[1] <= block_len: extra_zeros_x = block_height * block_len - size[0] return pow(2,i), round(extra_zeros_x), block_height return 0, 0, 0 def remove_morph_uv_set(self, obj): layer = obj.data.uv_layers.get('UVMap_shape_key') if layer is not None: obj.data.uv_layers.remove(layer) def create_morph_uv_set(self, obj, img_size): # Get/ create morph UV set if obj.data.uv_layers.get('UVMap_shape_key') is None: obj.data.uv_layers.new(name = 'UVMap_shape_key') bm = bmesh.new() bm.from_mesh(obj.data) uv_layer = bm.loops.layers.uv.get('UVMap_shape_key') pixel_size = 1.0 / img_size i = 0 j = 0 # Arrange UVs to match exported image pixels for v in bm.verts: for l in v.link_loops: uv_data = l[uv_layer] uv_data.uv = Vector(((i + 0.5) * pixel_size, (j + 0.5) * pixel_size)) i += 1 if i > img_size - 1: j += 1 i = 0 bm.to_mesh(obj.data) bm.free() def write_mesh(self, bobject: bpy.types.Object, fp, out_mesh): if bpy.data.worlds['Lnx'].lnx_single_data_file: self.output['mesh_datas'].append(out_mesh) # One mesh data per file else: mesh_obj = {'mesh_datas': [out_mesh]} lnx.utils.write_lnx(fp, mesh_obj) bobject.data.lnx_cached = True @staticmethod def calc_aabb(bobject): aabb_center = 0.125 * sum((Vector(b) for b in bobject.bound_box), Vector()) bobject.data.lnx_aabb = [ abs((bobject.bound_box[6][0] - bobject.bound_box[0][0]) / 2 + abs(aabb_center[0])) * 2, abs((bobject.bound_box[6][1] - bobject.bound_box[0][1]) / 2 + abs(aabb_center[1])) * 2, abs((bobject.bound_box[6][2] - bobject.bound_box[0][2]) / 2 + abs(aabb_center[2])) * 2 ] @staticmethod def get_num_vertex_colors(mesh: bpy.types.Mesh) -> int: """Return the amount of vertex color attributes of the given mesh.""" if bpy.app.version >= (3, 0, 0): num = 0 for attr in mesh.attributes: if attr.data_type in ('BYTE_COLOR', 'FLOAT_COLOR'): if attr.domain == 'CORNER': num += 1 else: log.warn(f'Only vertex colors with domain "Face Corner" are supported for now, ignoring "{attr.name}"') return num else: return len(mesh.vertex_colors) @staticmethod def get_nth_vertex_colors(mesh: bpy.types.Mesh, n: int) -> Optional[VertexColorType]: """Return the n-th vertex color attribute from the given mesh, ignoring all other attribute types and unsupported domains. """ if bpy.app.version >= (3, 0, 0): i = 0 for attr in mesh.attributes: if attr.data_type in ('BYTE_COLOR', 'FLOAT_COLOR'): if attr.domain != 'CORNER': log.warn(f'Only vertex colors with domain "Face Corner" are supported for now, ignoring "{attr.name}"') continue if i == n: return attr i += 1 return None else: if 0 <= n < len(mesh.vertex_colors): return mesh.vertex_colors[n] return None @staticmethod def check_uv_precision(mesh: bpy.types.Mesh, uv_max_dim: float, max_dim_uvmap: bpy.types.MeshUVLoopLayer, invscale_tex: float): """Check whether the pixel size (assuming max_texture_size below) can be represented inside the usual [0, 1] UV range with the given `invscale_tex` that is used to normalize the UV coords. If it is not possible, display a warning. """ max_texture_size = 16384 pixel_size = 1 / max_texture_size # There are fewer distinct floating point values around 1 than # around 0, so we use 1 for checking here. We do not check whether # UV coords with an absolute value > 1 can be reliably represented. if np.float32(1.0) == np.float32(1.0) - np.float32(pixel_size * invscale_tex): log.warn( f'Mesh "{mesh.name}": The UV map "{max_dim_uvmap.name}"' ' contains very large coordinates (max. distance from' f' origin: {uv_max_dim}). The UV precision may suffer.' ) def export_mesh_data(self, export_mesh: bpy.types.Mesh, bobject: bpy.types.Object, o, has_armature=False): if bpy.app.version < (4, 1, 0): export_mesh.calc_normals_split() else: updated_normals = export_mesh.corner_normals export_mesh.calc_loop_triangles() loops = export_mesh.loops num_verts = len(loops) num_uv_layers = len(export_mesh.uv_layers) is_baked = self.has_baked_material(bobject, export_mesh.materials) num_colors = self.get_num_vertex_colors(export_mesh) has_col = self.get_export_vcols(bobject.data) and num_colors > 0 # Check if shape keys were exported has_morph_target = self.get_shape_keys(bobject.data) if has_morph_target: # Shape keys UV are exported separately, so reduce UV count by 1 num_uv_layers -= 1 morph_uv_index = self.get_morph_uv_index(bobject.data) has_tex = (self.get_export_uvs(bobject.data) and num_uv_layers > 0) or is_baked has_tex1 = has_tex and num_uv_layers > 1 has_tang = self.has_tangents(bobject.data) pdata = np.empty(num_verts * 4, dtype=' maxdim: maxdim = abs(v.uv[0]) if abs(v.uv[1]) > maxdim: maxdim = abs(v.uv[1]) if has_tex1: lay1 = uv_layers[t1map] for v in lay1.data: if abs(v.uv[0]) > maxdim: maxdim = abs(v.uv[0]) maxdim_uvlayer = lay1 if abs(v.uv[1]) > maxdim: maxdim = abs(v.uv[1]) maxdim_uvlayer = lay1 if has_morph_target: morph_data = np.empty(num_verts * 2, dtype=' maxdim: maxdim = abs(v.uv[0]) maxdim_uvlayer = lay2 if abs(v.uv[1]) > maxdim: maxdim = abs(v.uv[1]) maxdim_uvlayer = lay2 if maxdim > 1: o['scale_tex'] = maxdim invscale_tex = (1 / o['scale_tex']) * 32767 else: invscale_tex = 1 * 32767 self.check_uv_precision(export_mesh, maxdim, maxdim_uvlayer, invscale_tex) if has_tang: try: export_mesh.calc_tangents(uvmap=lay0.name) except Exception as e: if hasattr(e, 'message'): log.error(e.message) else: # Assume it was caused because of encountering n-gons log.error(f"""object {bobject.name} contains n-gons in its mesh, so it's impossible to compute tanget space for normal mapping. Make sure the mesh only has tris/quads.""") tangdata = np.empty(num_verts * 3, dtype=' 2: o['scale_pos'] = maxdim / 2 else: o['scale_pos'] = 1.0 if has_armature: # Allow up to 2x bigger bounds for skinned mesh o['scale_pos'] *= 2.0 scale_pos = o['scale_pos'] invscale_pos = (1 / scale_pos) * 32767 verts = export_mesh.vertices if has_tex: lay0 = export_mesh.uv_layers[t0map] if has_tex1: lay1 = export_mesh.uv_layers[t1map] if has_morph_target: lay2 = export_mesh.uv_layers[morph_uv_index] if has_col: vcol0 = self.get_nth_vertex_colors(export_mesh, 0).data loop: bpy.types.MeshLoop for i, loop in enumerate(loops): v = verts[loop.vertex_index] co = v.co normal = loop.normal tang = loop.tangent i4 = i * 4 i2 = i * 2 pdata[i4 ] = co[0] pdata[i4 + 1] = co[1] pdata[i4 + 2] = co[2] pdata[i4 + 3] = normal[2] * scale_pos # Cancel scale ndata[i2 ] = normal[0] ndata[i2 + 1] = normal[1] if has_tex: uv = lay0.data[loop.index].uv t0data[i2 ] = uv[0] t0data[i2 + 1] = 1.0 - uv[1] # Reverse Y if has_tex1: uv = lay1.data[loop.index].uv t1data[i2 ] = uv[0] t1data[i2 + 1] = 1.0 - uv[1] if has_tang: i3 = i * 3 tangdata[i3 ] = tang[0] tangdata[i3 + 1] = tang[1] tangdata[i3 + 2] = tang[2] if has_morph_target: uv = lay2.data[loop.index].uv morph_data[i2 ] = uv[0] morph_data[i2 + 1] = 1.0 - uv[1] if has_col: col = vcol0[loop.index].color i3 = i * 3 cdata[i3 ] = col[0] cdata[i3 + 1] = col[1] cdata[i3 + 2] = col[2] mats = export_mesh.materials poly_map = [] for i in range(max(len(mats), 1)): poly_map.append([]) for poly in export_mesh.polygons: poly_map[poly.material_index].append(poly) o['index_arrays'] = [] # map polygon indices to triangle loops tri_loops = {} for loop in export_mesh.loop_triangles: if loop.polygon_index not in tri_loops: tri_loops[loop.polygon_index] = [] tri_loops[loop.polygon_index].append(loop) for index, polys in enumerate(poly_map): tris = 0 for poly in polys: tris += poly.loop_total - 2 if tris == 0: # No face assigned continue prim = np.empty(tris * 3, dtype=' 1: for i in range(len(mats)): # Multi-mat mesh if mats[i] == mats[index]: # Default material for empty slots ia['material'] = i break o['index_arrays'].append(ia) # Pack pdata *= invscale_pos ndata *= 32767 pdata = np.array(pdata, dtype=' 0 def export_mesh(self, object_ref): """Exports a single mesh object.""" # profile_time = time.time() table = object_ref[1]["objectTable"] bobject = table[0] oid = lnx.utils.safestr(object_ref[1]["structName"]) wrd = bpy.data.worlds['Lnx'] if wrd.lnx_single_data_file: fp = None else: fp = self.get_meshes_file_path('mesh_' + oid, compressed=LeenkxExporter.compress_enabled) assets.add(fp) # No export necessary if bobject.data.lnx_cached and os.path.exists(fp): return # Mesh users have different modifier stack for i in range(1, len(table)): if not self.mod_equal_stack(bobject, table[i]): log.warn('{0} users {1} and {2} differ in modifier stack - use Make Single User - Object & Data for now'.format(oid, bobject.name, table[i].name)) break if wrd.lnx_verbose_output: print('Exporting mesh ' + lnx.utils.asset_name(bobject.data)) out_mesh = {'name': oid} mesh = object_ref[0] struct_flag = False # Save the morph state if necessary active_shape_key_index = 0 show_only_shape_key = False current_morph_value = 0 shape_keys = LeenkxExporter.get_shape_keys(mesh) if shape_keys: # Save the morph state active_shape_key_index = bobject.active_shape_key_index show_only_shape_key = bobject.show_only_shape_key current_morph_value = bobject.active_shape_key.value # Reset morph state to base for mesh export bobject.active_shape_key_index = 0 bobject.show_only_shape_key = True self.depsgraph.update() armature = bobject.find_armature() apply_modifiers = not armature bobject_eval = bobject.evaluated_get(self.depsgraph) if apply_modifiers else bobject export_mesh = bobject_eval.to_mesh() # Export shape keys here if shape_keys: self.export_shape_keys(bobject, export_mesh, out_mesh) # Update dependancy after new UV layer was added self.depsgraph.update() bobject_eval = bobject.evaluated_get(self.depsgraph) if apply_modifiers else bobject export_mesh = bobject_eval.to_mesh() if export_mesh is None: log.warn(oid + ' was not exported') return if len(export_mesh.uv_layers) > 2: log.warn(oid + ' exceeds maximum of 2 UV Maps supported') # Update aabb self.calc_aabb(bobject) # Process meshes if LeenkxExporter.optimize_enabled: vert_list = exporter_opt.export_mesh_data(self, export_mesh, bobject, out_mesh, has_armature=armature is not None) if armature: exporter_opt.export_skin(self, bobject, armature, vert_list, out_mesh) else: self.export_mesh_data(export_mesh, bobject, out_mesh, has_armature=armature is not None) if armature: self.export_skin(bobject, armature, export_mesh, out_mesh) # Restore the morph state after mesh export if shape_keys: bobject.active_shape_key_index = active_shape_key_index bobject.show_only_shape_key = show_only_shape_key bobject.active_shape_key.value = current_morph_value self.depsgraph.update() mesh.update() # Check if mesh is using instanced rendering instanced_type, instanced_data = self.object_process_instancing(table, out_mesh['scale_pos']) # Save offset data for instanced rendering if instanced_type > 0: out_mesh['instanced_data'] = instanced_data out_mesh['instanced_type'] = instanced_type # Export usage if bobject.data.lnx_dynamic_usage: out_mesh['dynamic_usage'] = bobject.data.lnx_dynamic_usage self.write_mesh(bobject, fp, out_mesh) # print('Mesh exported in ' + str(time.time() - profile_time)) if hasattr(bobject, 'evaluated_get'): bobject_eval.to_mesh_clear() def export_light(self, object_ref): """Exports a single light object.""" rpdat = lnx.utils.get_rp() light_ref = object_ref[0] objtype = light_ref.type out_light = { 'name': object_ref[1]["structName"], 'type': objtype.lower(), 'cast_shadow': light_ref.use_shadow, 'near_plane': light_ref.lnx_clip_start, 'far_plane': light_ref.lnx_clip_end, 'fov': light_ref.lnx_fov, 'color': [light_ref.color[0], light_ref.color[1], light_ref.color[2]], 'strength': light_ref.energy, 'shadows_bias': light_ref.lnx_shadows_bias * 0.0001 } if rpdat.rp_shadows: if objtype == 'POINT': out_light['shadowmap_size'] = int(rpdat.rp_shadowmap_cube) else: out_light['shadowmap_size'] = lnx.utils.get_cascade_size(rpdat) else: out_light['shadowmap_size'] = 0 if objtype == 'SUN': out_light['strength'] *= 0.325 # Scale bias for ortho light matrix out_light['shadows_bias'] *= 20.0 if out_light['shadowmap_size'] > 1024: # Less bias for bigger maps out_light['shadows_bias'] *= 1 / (out_light['shadowmap_size'] / 1024) elif objtype == 'POINT': out_light['strength'] *= 0.01 out_light['fov'] = 1.5708 # pi/2 out_light['shadowmap_cube'] = True if light_ref.shadow_soft_size > 0.1: out_light['light_size'] = light_ref.shadow_soft_size * 10 elif objtype == 'SPOT': out_light['strength'] *= 0.01 out_light['spot_size'] = math.cos(light_ref.spot_size / 2) # Cycles defaults to 0.15 out_light['spot_blend'] = light_ref.spot_blend / 10 elif objtype == 'AREA': out_light['strength'] *= 0.01 out_light['size'] = light_ref.size out_light['size_y'] = light_ref.size_y self.output['light_datas'].append(out_light) def export_probe(self, objectRef): o = {'name': objectRef[1]["structName"]} bo = objectRef[0] if bo.type == 'GRID': o['type'] = 'grid' elif bo.type == 'PLANAR': o['type'] = 'planar' else: o['type'] = 'cubemap' self.output['probe_datas'].append(o) def export_collection(self, collection: bpy.types.Collection): """Exports a single collection.""" scene_objects = self.scene.collection.all_objects out_collection = { 'name': collection.name, 'instance_offset': list(collection.instance_offset), 'object_refs': [] } for bobject in collection.objects: if not bobject.lnx_export: continue # Only add unparented objects or objects with their parent # outside the collection, then instantiate the full object # child tree if the collection gets spawned as a whole if bobject.parent is None or bobject.parent.name not in collection.objects: asset_name = lnx.utils.asset_name(bobject) if collection.library and not collection.name in self.scene.collection.children: # Add external linked objects # Iron differentiates objects based on their names, # so errors will happen if two objects with the # same name exists. This check is only required # when the object in question is in a library, # otherwise Blender will not allow duplicate names if asset_name in scene_objects: log.warn("skipping export of the object" f" {bobject.name} (collection" f" {collection.name}) because it has the same" " export name as another object in the scene:" f" {asset_name}") continue self.process_bobject(bobject) self.export_object(bobject) out_collection['object_refs'].append(asset_name) self.output['groups'].append(out_collection) def get_camera_clear_color(self): if self.scene.world is None: return [0.051, 0.051, 0.051, 1.0] if self.scene.world.node_tree is None: c = self.scene.world.color return [c[0], c[1], c[2], 1.0] if 'Background' in self.scene.world.node_tree.nodes: background_node = self.scene.world.node_tree.nodes['Background'] col = background_node.inputs[0].default_value strength = background_node.inputs[1].default_value ar = [col[0] * strength, col[1] * strength, col[2] * strength, col[3]] ar[0] = max(min(ar[0], 1.0), 0.0) ar[1] = max(min(ar[1], 1.0), 0.0) ar[2] = max(min(ar[2], 1.0), 0.0) ar[3] = max(min(ar[3], 1.0), 0.0) return ar return [0.051, 0.051, 0.051, 1.0] @staticmethod def extract_projection(o, proj, with_planes=True): a = proj[0][0] b = proj[1][1] c = proj[2][2] d = proj[2][3] k = (c - 1.0) / (c + 1.0) o['fov'] = 2.0 * math.atan(1.0 / b) if with_planes: o['near_plane'] = (d * (1.0 - k)) / (2.0 * k) o['far_plane'] = k * o['near_plane'] @staticmethod def extract_ortho(o, proj): # left, right, bottom, top o['ortho'] = [-(1 + proj[3][0]) / proj[0][0], \ (1 - proj[3][0]) / proj[0][0], \ -(1 + proj[3][1]) / proj[1][1], \ (1 - proj[3][1]) / proj[1][1]] o['near_plane'] = (1 + proj[3][2]) / proj[2][2] o['far_plane'] = -(1 - proj[3][2]) / proj[2][2] o['near_plane'] *= 2 o['far_plane'] *= 2 def export_camera(self, objectRef): o = {} o['name'] = objectRef[1]["structName"] objref = objectRef[0] camera = objectRef[1]["objectTable"][0] render = self.scene.render proj = camera.calc_matrix_camera( self.depsgraph, x=render.resolution_x, y=render.resolution_y, scale_x=render.pixel_aspect_x, scale_y=render.pixel_aspect_y) if objref.type == 'PERSP': self.extract_projection(o, proj) else: self.extract_ortho(o, proj) o['frustum_culling'] = objref.lnx_frustum_culling o['clear_color'] = self.get_camera_clear_color() self.output['camera_datas'].append(o) def export_speaker(self, objectRef): # This function exports a single speaker object o = {} o['name'] = objectRef[1]["structName"] objref = objectRef[0] if objref.sound: # Packed if objref.sound.packed_file is not None: unpack_path = lnx.utils.get_fp_build() + '/compiled/Assets/unpacked' if not os.path.exists(unpack_path): os.makedirs(unpack_path) unpack_filepath = unpack_path + '/' + objref.sound.name if not os.path.isfile(unpack_filepath) or os.path.getsize(unpack_filepath) != objref.sound.packed_file.size: with open(unpack_filepath, 'wb') as f: f.write(objref.sound.packed_file.data) assets.add(unpack_filepath) # External else: assets.add(lnx.utils.asset_path(objref.sound.filepath)) # Link sound to assets o['sound'] = lnx.utils.extract_filename(objref.sound.filepath) else: o['sound'] = '' o['muted'] = objref.muted o['volume'] = objref.volume o['pitch'] = objref.pitch o['volume_min'] = objref.volume_min o['volume_max'] = objref.volume_max o['attenuation'] = objref.attenuation o['distance_max'] = objref.distance_max o['distance_reference'] = objref.distance_reference o['play_on_start'] = objref.lnx_play_on_start o['loop'] = objref.lnx_loop o['stream'] = objref.lnx_stream self.output['speaker_datas'].append(o) def make_default_mat(self, mat_name, mat_objs, is_particle=False): if mat_name in bpy.data.materials: return mat = bpy.data.materials.new(name=mat_name) # if default_exists: # mat.lnx_cached = True if is_particle: mat.lnx_particle_flag = True # Empty material roughness mat.use_nodes = True for node in mat.node_tree.nodes: if node.type == 'BSDF_PRINCIPLED': node.inputs[7].default_value = 0.25 o = {} o['name'] = mat.name o['contexts'] = [] mat_users = { mat: mat_objs } mat_lnxusers = { mat: [o] } make_material.parse(mat, o, mat_users, mat_lnxusers) self.output['material_datas'].append(o) bpy.data.materials.remove(mat) rpdat = lnx.utils.get_rp() if not rpdat.lnx_culling: o['override_context'] = {} o['override_context']['cull_mode'] = 'none' def signature_traverse(self, node, sign): sign += node.type + '-' if node.type == 'TEX_IMAGE' and node.image is not None: sign += node.image.filepath + '-' for inp in node.inputs: if inp.is_linked: sign = self.signature_traverse(inp.links[0].from_node, sign) else: # Unconnected socket if not hasattr(inp, 'default_value'): sign += 'o' elif inp.type in ('RGB', 'RGBA', 'VECTOR'): sign += str(inp.default_value[0]) sign += str(inp.default_value[1]) sign += str(inp.default_value[2]) else: sign += str(inp.default_value) return sign def get_signature(self, mat): nodes = mat.node_tree.nodes output_node = cycles.node_by_type(nodes, 'OUTPUT_MATERIAL') if output_node is not None: sign = self.signature_traverse(output_node, '') return sign return None def export_materials(self): wrd = bpy.data.worlds['Lnx'] # Keep materials with fake user for material in bpy.data.materials: if material.use_fake_user and material not in self.material_array: self.material_array.append(material) # Ensure the same order for merging materials self.material_array.sort(key=lambda x: x.name) if wrd.lnx_batch_materials: mat_users = self.material_to_object_dict mat_lnxusers = self.material_to_lnx_object_dict mat_batch.build(self.material_array, mat_users, mat_lnxusers) transluc_used = False overlays_used = False blending_used = False depthtex_used = False decals_used = False sss_used = False for material in self.material_array: # If the material is unlinked, material becomes None if material is None: continue if not material.use_nodes: material.use_nodes = True # Recache material signature = self.get_signature(material) if signature != material.signature: material.lnx_cached = False if signature is not None: material.signature = signature o = {} o['name'] = lnx.utils.asset_name(material) if material.lnx_skip_context != '': o['skip_context'] = material.lnx_skip_context rpdat = lnx.utils.get_rp() if material.lnx_two_sided or not rpdat.lnx_culling: o['override_context'] = {} o['override_context']['cull_mode'] = 'none' elif material.lnx_cull_mode != 'clockwise': o['override_context'] = {} o['override_context']['cull_mode'] = material.lnx_cull_mode if material.lnx_compare_mode != 'less': o['override_context'] = {} o['override_context']['compare_mode'] = material.lnx_compare_mode o['contexts'] = [] mat_users = self.material_to_object_dict mat_lnxusers = self.material_to_lnx_object_dict sd, rpasses, needs_sss = make_material.parse(material, o, mat_users, mat_lnxusers) sss_used |= needs_sss # Attach MovieTexture for con in o['contexts']: for tex in con['bind_textures']: if 'source' in tex and tex['source'] == 'movie': trait = {} trait['type'] = 'Script' trait['class_name'] = 'leenkx.trait.internal.MovieTexture' LeenkxExporter.import_traits.append(trait['class_name']) trait['parameters'] = ['"' + tex['file'] + '"'] for user in mat_lnxusers[material]: user['traits'].append(trait) if 'translucent' in rpasses: transluc_used = True if 'overlay' in rpasses: overlays_used = True if 'mesh' in rpasses: if material.lnx_blending: blending_used = True if material.lnx_depth_read: depthtex_used = True if 'decal' in rpasses: decals_used = True uv_export = False tang_export = False vcol_export = False vs_str = '' for con in sd['contexts']: for elem in con['vertex_elements']: if len(vs_str) > 0: vs_str += ',' vs_str += elem['name'] if elem['name'] == 'tang': tang_export = True elif elem['name'] == 'tex': uv_export = True elif elem['name'] == 'col': vcol_export = True for con in o['contexts']: # TODO: blend context if con['name'] == 'mesh' and material.lnx_blending: con['name'] = 'blend' if (material.export_tangents != tang_export) or \ (material.export_uvs != uv_export) or \ (material.export_vcols != vcol_export): material.export_uvs = uv_export material.export_vcols = vcol_export material.export_tangents = tang_export if material in self.material_to_object_dict: mat_users = self.material_to_object_dict[material] for ob in mat_users: ob.data.lnx_cached = False self.output['material_datas'].append(o) material.lnx_cached = True # Auto-enable render-path features rebuild_rp = False rpdat = lnx.utils.get_rp() if rpdat.rp_translucency_state == 'Auto' and rpdat.rp_translucency != transluc_used: rpdat.rp_translucency = transluc_used rebuild_rp = True if rpdat.rp_overlays_state == 'Auto' and rpdat.rp_overlays != overlays_used: rpdat.rp_overlays = overlays_used rebuild_rp = True if rpdat.rp_blending_state == 'Auto' and rpdat.rp_blending != blending_used: rpdat.rp_blending = blending_used rebuild_rp = True if rpdat.rp_depth_texture_state == 'Auto' and rpdat.rp_depth_texture != depthtex_used: rpdat.rp_depth_texture = depthtex_used rebuild_rp = True if rpdat.rp_decals_state == 'Auto' and rpdat.rp_decals != decals_used: rpdat.rp_decals = decals_used rebuild_rp = True if rpdat.rp_sss_state == 'Auto' and rpdat.rp_sss != sss_used: rpdat.rp_sss = sss_used rebuild_rp = True if rebuild_rp: make_renderpath.build() def export_particle_systems(self): if len(self.particle_system_array) > 0: self.output['particle_datas'] = [] for particleRef in self.particle_system_array.items(): padd = False; for obj in bpy.data.objects: for mod in obj.modifiers: if mod.type == 'PARTICLE_SYSTEM': if mod.particle_system.settings.name == particleRef[1]["structName"]: if mod.show_render: padd = True if not padd: continue; psettings = particleRef[0] if psettings is None: continue if psettings.instance_object is None or psettings.render_type != 'OBJECT': continue emit_from = 0 # VERT if psettings.emit_from == 'FACE': emit_from = 1 elif psettings.emit_from == 'VOLUME': emit_from = 2 out_particlesys = { 'name': particleRef[1]["structName"], 'type': 0 if psettings.type == 'EMITTER' else 1, # HAIR 'auto_start': psettings.lnx_auto_start, 'dynamic_emitter': psettings.lnx_dynamic_emitter, 'is_unique': psettings.lnx_is_unique, 'loop': psettings.lnx_loop, # Emission 'count': int(psettings.count * psettings.lnx_count_mult), 'frame_start': int(psettings.frame_start), 'frame_end': int(psettings.frame_end), 'lifetime': psettings.lifetime, 'lifetime_random': psettings.lifetime_random, 'emit_from': emit_from, # Velocity # 'normal_factor': psettings.normal_factor, # 'tangent_factor': psettings.tangent_factor, # 'tangent_phase': psettings.tangent_phase, 'object_align_factor': ( psettings.object_align_factor[0], psettings.object_align_factor[1], psettings.object_align_factor[2] ), # 'object_factor': psettings.object_factor, 'factor_random': psettings.factor_random, # Physics 'physics_type': 1 if psettings.physics_type == 'NEWTON' else 0, 'particle_size': psettings.particle_size, 'size_random': psettings.size_random, 'mass': psettings.mass, # Render 'instance_object': lnx.utils.asset_name(psettings.instance_object), # Field weights 'weight_gravity': psettings.effector_weights.gravity } if psettings.instance_object not in self.object_to_lnx_object_dict: # The instance object is not yet exported, e.g. because it is # in a different scene outside of every (non-scene) collection self.process_bobject(psettings.instance_object) self.export_object(psettings.instance_object) self.object_to_lnx_object_dict[psettings.instance_object]['is_particle'] = True self.output['particle_datas'].append(out_particlesys) def export_tilesheets(self): wrd = bpy.data.worlds['Lnx'] if len(wrd.lnx_tilesheetlist) > 0: self.output['tilesheet_datas'] = [] for ts in wrd.lnx_tilesheetlist: o = {} o['name'] = ts.name o['tilesx'] = ts.tilesx_prop o['tilesy'] = ts.tilesy_prop o['framerate'] = ts.framerate_prop o['actions'] = [] for tsa in ts.lnx_tilesheetactionlist: ao = {} ao['name'] = tsa.name ao['start'] = tsa.start_prop ao['end'] = tsa.end_prop ao['loop'] = tsa.loop_prop o['actions'].append(ao) self.output['tilesheet_datas'].append(o) def export_world(self): """Exports the world of the current scene.""" world = self.scene.world if world is not None: world_name = lnx.utils.safestr(lnx.utils.asset_name(world) if world.library else world.name) if world_name not in self.world_array: self.world_array.append(world_name) out_world = {'name': world_name} self.post_export_world(world, out_world) self.output['world_datas'].append(out_world) elif lnx.utils.get_rp().rp_background == 'World': log.warn(f'Scene "{self.scene.name}" is missing a world, some render targets will not be cleared') def export_objects(self, scene): """Exports all supported blender objects. References to objects are dictionaries storing the type and name of that object. Currently supported: - Mesh - Light - Camera - Speaker - Light Probe """ if not LeenkxExporter.option_mesh_only: self.output['light_datas'] = [] self.output['camera_datas'] = [] self.output['speaker_datas'] = [] for light_ref in self.light_array.items(): self.export_light(light_ref) for camera_ref in self.camera_array.items(): self.export_camera(camera_ref) # Keep sounds with fake user for sound in bpy.data.sounds: if sound.use_fake_user: assets.add(lnx.utils.asset_path(sound.filepath)) for speaker_ref in self.speaker_array.items(): self.export_speaker(speaker_ref) if bpy.data.lightprobes: self.output['probe_datas'] = [] for lightprobe_object in self.probe_array.items(): self.export_probe(lightprobe_object) self.output['mesh_datas'] = [] for mesh_ref in self.mesh_array.items(): self.export_mesh(mesh_ref) def execute(self): """Exports the scene.""" profile_time = time.time() wrd = bpy.data.worlds['Lnx'] if wrd.lnx_verbose_output: print('Exporting ' + lnx.utils.asset_name(self.scene)) if self.compress_enabled: print('Scene data will be compressed which might take a while.') current_frame, current_subframe = self.scene.frame_current, self.scene.frame_subframe scene_objects: List[bpy.types.Object] = self.scene.collection.all_objects.values() # bobject => blender object for bobject in scene_objects: # Initialize object export data (map objects to game objects) out_object: Dict[str, Any] = {'traits': []} self.object_to_lnx_object_dict[bobject] = out_object # Process # Skip objects that have a parent because children are # processed recursively if not bobject.parent: self.process_bobject(bobject) # Softbody needs connected triangles, use optimized # geometry export for mod in bobject.modifiers: if mod.type in ('CLOTH', 'SOFT_BODY'): LeenkxExporter.optimize_enabled = True self.process_skinned_meshes() self.output['name'] = lnx.utils.safestr(self.scene.name) if self.filepath.endswith('.lz4'): self.output['name'] += '.lz4' elif not bpy.data.worlds['Lnx'].lnx_minimize: self.output['name'] += '.json' # Create unique material variants for skinning, tilesheets and particles matvars, matslots = self.create_material_variants(self.scene) # Auto-bones rpdat = lnx.utils.get_rp() if rpdat.lnx_skin_max_bones_auto: max_bones = 8 for armature in bpy.data.armatures: if max_bones < len(armature.bones): max_bones = len(armature.bones) rpdat.lnx_skin_max_bones = max_bones # Terrain if self.scene.lnx_terrain_object is not None: assets.add_khafile_def('lnx_terrain') # Append trait out_trait = { 'type': 'Script', 'class_name': 'leenkx.trait.internal.TerrainPhysics' } if 'traits' not in self.output: self.output['traits']: List[Dict[str, str]] = [] self.output['traits'].append(out_trait) LeenkxExporter.import_traits.append(out_trait['class_name']) LeenkxExporter.export_physics = True # Export material mat = self.scene.lnx_terrain_object.children[0].data.materials[0] self.material_array.append(mat) # Terrain data out_terrain = { 'name': 'Terrain', 'sectors_x': self.scene.lnx_terrain_sectors[0], 'sectors_y': self.scene.lnx_terrain_sectors[1], 'sector_size': self.scene.lnx_terrain_sector_size, 'height_scale': self.scene.lnx_terrain_height_scale, 'material_ref': mat.name } self.output['terrain_datas'] = [out_terrain] self.output['terrain_ref'] = 'Terrain' # Export objects self.output['objects'] = [] for bobject in scene_objects: # Skip objects that have a parent because children are # exported recursively if not bobject.parent: self.export_object(bobject) # Export collections if bpy.data.collections: self.output['groups'] = [] for collection in bpy.data.collections: if collection.name.startswith(('RigidBodyWorld', 'Trait|')): continue if self.scene.user_of_id(collection) or collection in self.referenced_collections: self.export_collection(collection) if not LeenkxExporter.option_mesh_only: if self.scene.camera is not None: self.output['camera_ref'] = lnx.utils.asset_name(self.scene.camera) if self.scene.library else self.scene.camera.name else: if self.scene.name == lnx.utils.get_project_scene_name(): log.warn(f'Scene "{self.scene.name}" is missing a camera') self.output['material_datas'] = [] # Object with no material assigned in the scene if len(self.default_material_objects) > 0: self.make_default_mat('lnxdefault', self.default_material_objects) if len(self.default_skin_material_objects) > 0: self.make_default_mat('lnxdefaultskin', self.default_skin_material_objects) if len(bpy.data.particles) > 0: self.use_default_material_part() if len(self.default_part_material_objects) > 0: self.make_default_mat('lnxdefaultpart', self.default_part_material_objects, is_particle=True) self.export_materials() self.export_particle_systems() self.output['world_datas'] = [] self.export_world() self.export_tilesheets() if self.scene.world is not None: self.output['world_ref'] = lnx.utils.safestr(lnx.utils.asset_name(self.scene.world) if self.scene.world.library else self.scene.world.name) if self.scene.use_gravity: self.output['gravity'] = [self.scene.gravity[0], self.scene.gravity[1], self.scene.gravity[2]] rbw = self.scene.rigidbody_world if rbw is not None: weights = rbw.effector_weights self.output['gravity'][0] *= weights.all * weights.gravity self.output['gravity'][1] *= weights.all * weights.gravity self.output['gravity'][2] *= weights.all * weights.gravity else: self.output['gravity'] = [0.0, 0.0, 0.0] self.export_objects(self.scene) # Create Viewport camera if bpy.data.worlds['Lnx'].lnx_play_camera != 'Scene': self.create_default_camera(is_viewport_camera=True) elif self.scene.camera is not None and self.scene.camera.type != 'CAMERA': # Blender doesn't directly allow to set arbitrary objects as cameras, # but there is a `Set Active Object as Camera` operator which might # cause cases like this to happen log.warn(f'Camera "{self.scene.camera.name}" in scene "{self.scene.name}" is not a camera object, using default camera') self.create_default_camera() # No camera found, create default one if not self.has_spawning_camera: log.warn(f'Scene "{self.scene.name}" is missing a camera, using default camera') self.create_default_camera() self.export_scene_traits() self.export_canvas_themes() # Write embedded data references if len(assets.embedded_data) > 0: self.output['embedded_datas'] = [] for file in assets.embedded_data: self.output['embedded_datas'].append(file) # Write scene file lnx.utils.write_lnx(self.filepath, self.output) # Remove created material variants for slot in matslots: # Set back to original material orig_mat = bpy.data.materials[slot.material.name[:-8]] # _lnxskin, _lnxpart, _lnxtile orig_mat.export_uvs = slot.material.export_uvs orig_mat.export_vcols = slot.material.export_vcols orig_mat.export_tangents = slot.material.export_tangents orig_mat.lnx_cached = slot.material.lnx_cached slot.material = orig_mat for mat in matvars: bpy.data.materials.remove(mat, do_unlink=True) # Restore frame if self.scene.frame_current != current_frame: self.scene.frame_set(current_frame, subframe=current_subframe) if wrd.lnx_verbose_output: print('Scene exported in {:0.3f}s'.format(time.time() - profile_time)) def create_default_camera(self, is_viewport_camera=False): """Creates the default camera and adds a WalkNavigation trait to it.""" out_camera = { 'name': 'DefaultCamera', 'near_plane': 0.1, 'far_plane': 100.0, 'fov': 0.85, 'frustum_culling': True, 'clear_color': self.get_camera_clear_color() } # Set viewport camera projection if is_viewport_camera: proj, is_persp = self.get_viewport_projection_matrix() if proj is not None: if is_persp: self.extract_projection(out_camera, proj, with_planes=False) else: self.extract_ortho(out_camera, proj) self.output['camera_datas'].append(out_camera) out_object = { 'name': 'DefaultCamera', 'type': 'camera_object', 'data_ref': 'DefaultCamera', 'material_refs': [], 'transform': {} } viewport_matrix = self.get_viewport_view_matrix() if viewport_matrix is not None: out_object['transform']['values'] = LeenkxExporter.write_matrix(viewport_matrix.inverted_safe()) out_object['local_only'] = True else: out_object['transform']['values'] = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0] # Add WalkNavigation trait trait = { 'type': 'Script', 'class_name': 'leenkx.trait.WalkNavigation' } out_object['traits'] = [trait] LeenkxExporter.import_traits.append(trait['class_name']) self.output['objects'].append(out_object) self.output['camera_ref'] = 'DefaultCamera' self.has_spawning_camera = True @staticmethod def get_export_tangents(mesh): for material in mesh.materials: if material is not None and material.export_tangents: return True return False @staticmethod def get_export_vcols(mesh): for material in mesh.materials: if material is not None and material.export_vcols: return True return False @staticmethod def get_export_uvs(mesh): for material in mesh.materials: if material is not None and material.export_uvs: return True return False @staticmethod def object_process_instancing(refs, scale_pos): instanced_type = 0 instanced_data = None for bobject in refs: inst = bobject.lnx_instanced if inst != 'Off': if inst == 'Loc': instanced_type = 1 instanced_data = [0.0, 0.0, 0.0] # Include parent elif inst == 'Loc + Rot': instanced_type = 2 instanced_data = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] elif inst == 'Loc + Scale': instanced_type = 3 instanced_data = [0.0, 0.0, 0.0, 1.0, 1.0, 1.0] elif inst == 'Loc + Rot + Scale': instanced_type = 4 instanced_data = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0] for child in bobject.children: if not child.lnx_export or child.hide_render: continue if 'Loc' in inst: loc = child.matrix_local.to_translation() # Without parent matrix instanced_data.append(loc.x / scale_pos) instanced_data.append(loc.y / scale_pos) instanced_data.append(loc.z / scale_pos) if 'Rot' in inst: rot = child.matrix_local.to_euler() instanced_data.append(rot.x) instanced_data.append(rot.y) instanced_data.append(rot.z) if 'Scale' in inst: scale = child.matrix_local.to_scale() instanced_data.append(scale.x) instanced_data.append(scale.y) instanced_data.append(scale.z) break # Instance render collections with same children? # elif bobject.instance_type == 'GROUP' and bobject.instance_collection is not None: # instanced_type = 1 # instanced_data = [] # for child in bpy.data.collections[bobject.instance_collection].objects: # loc = child.matrix_local.to_translation() # instanced_data.append(loc.x) # instanced_data.append(loc.y) # instanced_data.append(loc.z) # break return instanced_type, instanced_data @staticmethod def rigid_body_static(rb): return (not rb.enabled and not rb.kinematic) or (rb.type == 'PASSIVE' and not rb.kinematic) def post_export_object(self, bobject: bpy.types.Object, o, type): # Export traits self.export_traits(bobject, o) wrd = bpy.data.worlds['Lnx'] phys_enabled = wrd.lnx_physics != 'Disabled' phys_pkg = 'bullet' if wrd.lnx_physics_engine == 'Bullet' else 'oimo' # Rigid body trait if bobject.rigid_body is not None and phys_enabled: LeenkxExporter.export_physics = True rb = bobject.rigid_body shape = 0 # BOX if rb.collision_shape == 'SPHERE': shape = 1 elif rb.collision_shape == 'CONVEX_HULL': shape = 2 elif rb.collision_shape == 'MESH': shape = 3 elif rb.collision_shape == 'CONE': shape = 4 elif rb.collision_shape == 'CYLINDER': shape = 5 elif rb.collision_shape == 'CAPSULE': shape = 6 body_mass = rb.mass is_static = self.rigid_body_static(rb) if is_static: body_mass = 0 x = {} x['type'] = 'Script' x['class_name'] = 'leenkx.trait.physics.' + phys_pkg + '.RigidBody' col_group = '' for b in rb.collision_collections: col_group = ('1' if b else '0') + col_group col_mask = '' for b in bobject.lnx_rb_collision_filter_mask: col_mask = ('1' if b else '0') + col_mask x['parameters'] = [str(shape), str(body_mass), str(rb.friction), str(rb.restitution), str(int(col_group, 2)), str(int(col_mask, 2)) ] lx = bobject.lnx_rb_linear_factor[0] ly = bobject.lnx_rb_linear_factor[1] lz = bobject.lnx_rb_linear_factor[2] ax = bobject.lnx_rb_angular_factor[0] ay = bobject.lnx_rb_angular_factor[1] az = bobject.lnx_rb_angular_factor[2] if bobject.lock_location[0]: lx = 0 if bobject.lock_location[1]: ly = 0 if bobject.lock_location[2]: lz = 0 if bobject.lock_rotation[0]: ax = 0 if bobject.lock_rotation[1]: ay = 0 if bobject.lock_rotation[2]: az = 0 col_margin = rb.collision_margin if rb.use_margin else 0.0 if rb.use_deactivation: deact_lv = rb.deactivate_linear_velocity deact_av = rb.deactivate_angular_velocity deact_time = bobject.lnx_rb_deactivation_time else: deact_lv = 0.0 deact_av = 0.0 deact_time = 0.0 body_params = {} body_params['linearDamping'] = rb.linear_damping body_params['angularDamping'] = rb.angular_damping body_params['linearFactorsX'] = lx body_params['linearFactorsY'] = ly body_params['linearFactorsZ'] = lz body_params['angularFactorsX'] = ax body_params['angularFactorsY'] = ay body_params['angularFactorsZ'] = az body_params['angularFriction'] = bobject.lnx_rb_angular_friction body_params['collisionMargin'] = col_margin body_params['linearDeactivationThreshold'] = deact_lv body_params['angularDeactivationThrshold'] = deact_av body_params['deactivationTime'] = deact_time # New velocity limit properties body_params['linearVelocityMin'] = bobject.lnx_rb_linear_velocity_min body_params['linearVelocityMax'] = bobject.lnx_rb_linear_velocity_max body_params['angularVelocityMin'] = bobject.lnx_rb_angular_velocity_min body_params['angularVelocityMax'] = bobject.lnx_rb_angular_velocity_max # New lock properties body_params['lockTranslationX'] = bobject.lnx_rb_lock_translation_x body_params['lockTranslationY'] = bobject.lnx_rb_lock_translation_y body_params['lockTranslationZ'] = bobject.lnx_rb_lock_translation_z body_params['lockRotationX'] = bobject.lnx_rb_lock_rotation_x body_params['lockRotationY'] = bobject.lnx_rb_lock_rotation_y body_params['lockRotationZ'] = bobject.lnx_rb_lock_rotation_z body_flags = {} body_flags['animated'] = rb.kinematic body_flags['trigger'] = bobject.lnx_rb_trigger body_flags['ccd'] = bobject.lnx_rb_ccd body_flags['interpolate'] = bobject.lnx_rb_interpolate body_flags['staticObj'] = is_static body_flags['useDeactivation'] = rb.use_deactivation x['parameters'].append(lnx.utils.get_haxe_json_string(body_params)) x['parameters'].append(lnx.utils.get_haxe_json_string(body_flags)) o['traits'].append(x) # Phys traits if phys_enabled: for modifier in bobject.modifiers: if modifier.type in ('CLOTH', 'SOFT_BODY'): self.add_softbody_mod(o, bobject, modifier) elif modifier.type == 'HOOK': self.add_hook_mod(o, bobject, modifier.object.name, modifier.vertex_group) # Rigid body constraint rbc = bobject.rigid_body_constraint if rbc is not None and rbc.enabled: self.add_rigidbody_constraint(o, bobject, rbc) # Camera traits if type is NodeType.CAMERA: # Viewport camera enabled, attach navigation to active camera if self.scene.camera is not None and bobject.name == self.scene.camera.name and bpy.data.worlds['Lnx'].lnx_play_camera != 'Scene': navigation_trait = {} navigation_trait['type'] = 'Script' navigation_trait['class_name'] = 'leenkx.trait.WalkNavigation' o['traits'].append(navigation_trait) # Map objects to materials, can be used in later stages for i in range(len(bobject.material_slots)): mat = self.slot_to_material(bobject, bobject.material_slots[i]) if mat in self.material_to_object_dict: self.material_to_object_dict[mat].append(bobject) self.material_to_lnx_object_dict[mat].append(o) else: self.material_to_object_dict[mat] = [bobject] self.material_to_lnx_object_dict[mat] = [o] # Add UniformsManager trait if type is NodeType.MESH: uniformManager = {} uniformManager['type'] = 'Script' uniformManager['class_name'] = 'leenkx.trait.internal.UniformsManager' o['traits'].append(uniformManager) # Export constraints if len(bobject.constraints) > 0: o['constraints'] = [] self.add_constraints(bobject, o) for x in o['traits']: LeenkxExporter.import_traits.append(x['class_name']) @staticmethod def add_constraints(bobject, o, bone=False): for constraint in bobject.constraints: if constraint.mute: continue out_constraint = {'name': constraint.name, 'type': constraint.type} if bone: out_constraint['bone'] = bobject.name if hasattr(constraint, 'target') and constraint.target is not None: if constraint.type == 'COPY_LOCATION': out_constraint['target'] = constraint.target.name out_constraint['use_x'] = constraint.use_x out_constraint['use_y'] = constraint.use_y out_constraint['use_z'] = constraint.use_z out_constraint['invert_x'] = constraint.invert_x out_constraint['invert_y'] = constraint.invert_y out_constraint['invert_z'] = constraint.invert_z out_constraint['use_offset'] = constraint.use_offset out_constraint['influence'] = constraint.influence elif constraint.type == 'CHILD_OF': out_constraint['target'] = constraint.target.name out_constraint['influence'] = constraint.influence o['constraints'].append(out_constraint) def export_traits(self, bobject: Union[bpy.types.Scene, bpy.types.Object], o): if not hasattr(bobject, 'lnx_traitlist'): return for traitlistItem in bobject.lnx_traitlist: # Do not export disabled traits but still export those # with fake user enabled so that nodes like `TraitNode` # still work if not traitlistItem.enabled_prop and not traitlistItem.fake_user: continue out_trait = {} if traitlistItem.type_prop == 'Logic Nodes' and traitlistItem.node_tree_prop is not None and traitlistItem.node_tree_prop.name != '': group_name = lnx.utils.safesrc(traitlistItem.node_tree_prop.name[0].upper() + traitlistItem.node_tree_prop.name[1:]) out_trait['type'] = 'Script' out_trait['class_name'] = lnx.utils.safestr(bpy.data.worlds['Lnx'].lnx_project_package) + '.node.' + group_name elif traitlistItem.type_prop == 'WebAssembly': wpath = os.path.join(lnx.utils.get_fp(), 'Bundled', traitlistItem.webassembly_prop + '.wasm') if not os.path.exists(wpath): log.warn(f'Wasm "{traitlistItem.webassembly_prop}" not found, skipping') continue out_trait['type'] = 'Script' out_trait['class_name'] = 'leenkx.trait.internal.WasmScript' out_trait['parameters'] = ["'" + traitlistItem.webassembly_prop + "'"] elif traitlistItem.type_prop == 'UI Canvas': cpath = os.path.join(lnx.utils.get_fp(), 'Bundled', 'canvas', traitlistItem.canvas_name_prop + '.json') if not os.path.exists(cpath): log.warn(f'Scene "{self.scene.name}" - Object "{bobject.name}" - Referenced canvas "{traitlistItem.canvas_name_prop}" not found, skipping') continue LeenkxExporter.export_ui = True out_trait['type'] = 'Script' out_trait['class_name'] = 'leenkx.trait.internal.CanvasScript' out_trait['parameters'] = ["'" + traitlistItem.canvas_name_prop + "'"] # Read file list and add canvas assets assetpath = os.path.join(lnx.utils.get_fp(), 'Bundled', 'canvas', traitlistItem.canvas_name_prop + '.files') if os.path.exists(assetpath): with open(assetpath,encoding="utf-8") as f: file_list = f.read().splitlines() for asset in file_list: # Relative to the root/Bundled/canvas path asset = asset[6:] # Strip ../../ to start in project root assets.add(asset) # Haxe/Bundled Script else: # Empty class name, skip if traitlistItem.class_name_prop == '': continue out_trait['type'] = 'Script' if traitlistItem.type_prop == 'Bundled Script': trait_prefix = 'leenkx.trait.' # TODO: temporary, export single mesh navmesh as obj if traitlistItem.class_name_prop == 'NavMesh' and bobject.type == 'MESH' and bpy.data.worlds['Lnx'].lnx_navigation != 'Disabled': LeenkxExporter.export_navigation = True nav_path = os.path.join(lnx.utils.get_fp_build(), 'compiled', 'Assets', 'navigation') if not os.path.exists(nav_path): os.makedirs(nav_path) nav_filepath = os.path.join(nav_path, 'nav_' + bobject.data.name + '.lnx') assets.add(nav_filepath) # TODO: Implement cache # if not os.path.isfile(nav_filepath): # override = {'selected_objects': [bobject]} # bobject.scale.y *= -1 # mesh = obj.data # for face in mesh.faces: # face.v.reverse() # if bpy.app.version[0] >= 4: # bpy.ops.wm.obj_export(override, use_selection=True, filepath=nav_filepath, check_existing=False, use_normals=False, use_uvs=False, use_materials=False) # else: # bpy.ops.export_scene.obj(override, use_selection=True, filepath=nav_filepath, check_existing=False, use_normals=False, use_uvs=False, use_materials=False) # bobject.scale.y *= -1 armature = bobject.find_armature() apply_modifiers = not armature bobject_eval = bobject.evaluated_get(self.depsgraph) if apply_modifiers else bobject export_mesh = bobject_eval.to_mesh() with open(nav_filepath, 'w') as f: for v in export_mesh.vertices: f.write("v %.4f " % (v.co[0] * bobject_eval.scale.x)) f.write("%.4f " % (v.co[2] * bobject_eval.scale.z)) f.write("%.4f\n" % (v.co[1] * bobject_eval.scale.y)) # Flipped for p in export_mesh.polygons: f.write("f") # Flipped normals for i in reversed(p.vertices): f.write(" %d" % (i + 1)) f.write("\n") # Haxe else: trait_prefix = lnx.utils.safestr(bpy.data.worlds['Lnx'].lnx_project_package) + '.' hxfile = os.path.join('Sources', (trait_prefix + traitlistItem.class_name_prop).replace('.', '/') + '.hx') if not os.path.exists(os.path.join(lnx.utils.get_fp(), hxfile)): # TODO: Halt build here once this check is tested print(f'Leenkx Error: Scene "{self.scene.name}" - Object "{bobject.name}": Referenced trait file "{hxfile}" not found') out_trait['class_name'] = trait_prefix + traitlistItem.class_name_prop # Export trait properties if traitlistItem.lnx_traitpropslist: out_trait['props'] = [] for trait_prop in traitlistItem.lnx_traitpropslist: out_trait['props'].append(trait_prop.name) out_trait['props'].append(trait_prop.type) if trait_prop.type.endswith("Object"): value = lnx.utils.asset_name(trait_prop.value_object) elif trait_prop.type == "TSceneFormat": value = lnx.utils.asset_name(trait_prop.value_scene) else: value = trait_prop.get_value() out_trait['props'].append(value) if not traitlistItem.enabled_prop: # If we're here, fake_user is enabled, otherwise we # would have skipped this trait already LeenkxExporter.import_traits.append(out_trait['class_name']) else: o['traits'].append(out_trait) def export_scene_traits(self) -> None: """Exports the traits of the scene and adds some internal traits to the scene depending on the exporter settings. """ wrd = bpy.data.worlds['Lnx'] if wrd.lnx_physics != 'Disabled' and LeenkxExporter.export_physics: if 'traits' not in self.output: self.output['traits'] = [] phys_pkg = 'bullet' if wrd.lnx_physics_engine == 'Bullet' else 'oimo' out_trait = { 'type': 'Script', 'class_name': 'leenkx.trait.physics.' + phys_pkg + '.PhysicsWorld' } rbw = self.scene.rigidbody_world if rbw is not None and rbw.enabled: if hasattr(rbw, 'substeps_per_frame'): substeps = str(rbw.substeps_per_frame) elif hasattr(rbw, 'steps_per_second'): scene_fps = bpy.context.scene.render.fps substeps_per_frame = rbw.steps_per_second / scene_fps substeps = str(int(round(substeps_per_frame))) else: print("WARNING: Physics rigid body world cannot determine steps/substeps. Please report this for further investigation.") print("Setting steps to 10 [ low ]") substeps = '10' out_trait['parameters'] = [str(rbw.time_scale), substeps, str(rbw.solver_iterations), str(wrd.lnx_physics_fixed_step)] if phys_pkg == 'bullet' or phys_pkg == 'oimo': debug_draw_mode = 1 if wrd.lnx_physics_dbg_draw_wireframe else 0 debug_draw_mode |= 2 if wrd.lnx_physics_dbg_draw_aabb else 0 debug_draw_mode |= 8 if wrd.lnx_physics_dbg_draw_contact_points else 0 debug_draw_mode |= 2048 if wrd.lnx_physics_dbg_draw_constraints else 0 debug_draw_mode |= 4096 if wrd.lnx_physics_dbg_draw_constraint_limits else 0 debug_draw_mode |= 16384 if wrd.lnx_physics_dbg_draw_normals else 0 debug_draw_mode |= 32768 if wrd.lnx_physics_dbg_draw_axis_gizmo else 0 debug_draw_mode |= 65536 if wrd.lnx_physics_dbg_draw_raycast else 0 out_trait['parameters'].append(str(debug_draw_mode)) self.output['traits'].append(out_trait) if wrd.lnx_navigation != 'Disabled' and LeenkxExporter.export_navigation: if 'traits' not in self.output: self.output['traits'] = [] out_trait = {'type': 'Script', 'class_name': 'leenkx.trait.navigation.Navigation'} self.output['traits'].append(out_trait) if wrd.lnx_debug_console: if 'traits' not in self.output: self.output['traits'] = [] LeenkxExporter.export_ui = True # Position debug_console_pos_type = 2 if wrd.lnx_debug_console_position == 'Left': debug_console_pos_type = 0 elif wrd.lnx_debug_console_position == 'Center': debug_console_pos_type = 1 else: debug_console_pos_type = 2 # Parameters out_trait = { 'type': 'Script', 'class_name': 'leenkx.trait.internal.DebugConsole', 'parameters': [ str(lnx.utils.get_ui_scale()), str(wrd.lnx_debug_console_scale), str(debug_console_pos_type), str(int(wrd.lnx_debug_console_visible)), str(int(wrd.lnx_debug_console_trace_pos)), str(int(lnx.utils.get_debug_console_visible_sc())), str(int(lnx.utils.get_debug_console_scale_in_sc())), str(int(lnx.utils.get_debug_console_scale_out_sc())) ] } self.output['traits'].append(out_trait) if lnx.utils.is_livepatch_enabled(): if 'traits' not in self.output: self.output['traits'] = [] out_trait = {'type': 'Script', 'class_name': 'leenkx.trait.internal.LivePatch'} self.output['traits'].append(out_trait) if len(self.scene.lnx_traitlist) > 0: if 'traits' not in self.output: self.output['traits'] = [] self.export_traits(self.scene, self.output) if 'traits' in self.output: for out_trait in self.output['traits']: LeenkxExporter.import_traits.append(out_trait['class_name']) @staticmethod def export_canvas_themes(): path_themes = os.path.join(lnx.utils.get_fp(), 'Bundled', 'canvas') file_theme = os.path.join(path_themes, "_themes.json") # If there is a canvas but no _themes.json, create it so that # CanvasScript.hx works if os.path.exists(path_themes) and not os.path.exists(file_theme): with open(file_theme, "w+", encoding='utf-8'): pass assets.add(file_theme) @staticmethod def add_softbody_mod(o, bobject: bpy.types.Object, modifier: Union[bpy.types.ClothModifier, bpy.types.SoftBodyModifier]): """Adds a softbody trait to the given object based on the given softbody/cloth modifier. """ LeenkxExporter.export_physics = True assets.add_khafile_def('lnx_physics_soft') phys_pkg = 'bullet' if bpy.data.worlds['Lnx'].lnx_physics_engine == 'Bullet' else 'oimo' out_trait = {'type': 'Script', 'class_name': 'leenkx.trait.physics.' + phys_pkg + '.SoftBody'} # ClothModifier if modifier.type == 'CLOTH': bend = modifier.settings.bending_stiffness soft_type = 0 # SoftBodyModifier elif modifier.type == 'SOFT_BODY': bend = (modifier.settings.bend + 1.0) * 10 soft_type = 1 else: # Wrong modifier type return out_trait['parameters'] = [str(soft_type), str(bend), str(modifier.settings.mass), str(bobject.lnx_soft_body_margin)] o['traits'].append(out_trait) if soft_type == 0: LeenkxExporter.add_hook_mod(o, bobject, '', modifier.settings.vertex_group_mass) @staticmethod def add_hook_mod(o, bobject: bpy.types.Object, target_name, group_name): LeenkxExporter.export_physics = True phys_pkg = 'bullet' if bpy.data.worlds['Lnx'].lnx_physics_engine == 'Bullet' else 'oimo' out_trait = {'type': 'Script', 'class_name': 'leenkx.trait.physics.' + phys_pkg + '.PhysicsHook'} verts = [] if group_name != '': group = bobject.vertex_groups[group_name].index for v in bobject.data.vertices: for g in v.groups: if g.group == group: verts.append(v.co.x) verts.append(v.co.y) verts.append(v.co.z) out_trait['parameters'] = [f"'{target_name}'", str(verts)] o['traits'].append(out_trait) @staticmethod def add_rigidbody_constraint(o, bobject, rbc): rb1 = rbc.object1 rb2 = rbc.object2 if rb1 is None or rb2 is None: return if rbc.type == "MOTOR": return LeenkxExporter.export_physics = True phys_pkg = 'bullet' if bpy.data.worlds['Lnx'].lnx_physics_engine == 'Bullet' else 'oimo' breaking_threshold = rbc.breaking_threshold if rbc.use_breaking else 0 trait = { 'type': 'Script', 'class_name': 'leenkx.trait.physics.' + phys_pkg + '.PhysicsConstraintExportHelper', 'parameters': [ "'" + rb1.name + "'", "'" + rb2.name + "'", str(rbc.disable_collisions).lower(), str(breaking_threshold), str(bobject.lnx_relative_physics_constraint).lower() ] } if rbc.type == "FIXED": trait['parameters'].insert(2,str(0)) if rbc.type == "POINT": trait['parameters'].insert(2,str(1)) if rbc.type == "GENERIC": limits = [ 1 if rbc.use_limit_lin_x else 0, rbc.limit_lin_x_lower, rbc.limit_lin_x_upper, 1 if rbc.use_limit_lin_y else 0, rbc.limit_lin_y_lower, rbc.limit_lin_y_upper, 1 if rbc.use_limit_lin_z else 0, rbc.limit_lin_z_lower, rbc.limit_lin_z_upper, 1 if rbc.use_limit_ang_x else 0, rbc.limit_ang_x_lower, rbc.limit_ang_x_upper, 1 if rbc.use_limit_ang_y else 0, rbc.limit_ang_y_lower, rbc.limit_ang_y_upper, 1 if rbc.use_limit_ang_z else 0, rbc.limit_ang_z_lower, rbc.limit_ang_z_upper ] trait['parameters'].insert(2,str(5)) trait['parameters'].append(str(limits)) if rbc.type == "GENERIC_SPRING": limits = [ 1 if rbc.use_limit_lin_x else 0, rbc.limit_lin_x_lower, rbc.limit_lin_x_upper, 1 if rbc.use_limit_lin_y else 0, rbc.limit_lin_y_lower, rbc.limit_lin_y_upper, 1 if rbc.use_limit_lin_z else 0, rbc.limit_lin_z_lower, rbc.limit_lin_z_upper, 1 if rbc.use_limit_ang_x else 0, rbc.limit_ang_x_lower, rbc.limit_ang_x_upper, 1 if rbc.use_limit_ang_y else 0, rbc.limit_ang_y_lower, rbc.limit_ang_y_upper, 1 if rbc.use_limit_ang_z else 0, rbc.limit_ang_z_lower, rbc.limit_ang_z_upper, 1 if rbc.use_spring_x else 0, rbc.spring_stiffness_x, rbc.spring_damping_x, 1 if rbc.use_spring_y else 0, rbc.spring_stiffness_y, rbc.spring_damping_y, 1 if rbc.use_spring_z else 0, rbc.spring_stiffness_z, rbc.spring_damping_z, 1 if rbc.use_spring_ang_x else 0, rbc.spring_stiffness_ang_x, rbc.spring_damping_ang_x, 1 if rbc.use_spring_ang_y else 0, rbc.spring_stiffness_ang_y, rbc.spring_damping_ang_y, 1 if rbc.use_spring_ang_z else 0, rbc.spring_stiffness_ang_z, rbc.spring_damping_ang_z ] trait['parameters'].insert(2,str(6)) trait['parameters'].append(str(limits)) if rbc.type == "HINGE": limits = [ 1 if rbc.use_limit_ang_z else 0, rbc.limit_ang_z_lower, rbc.limit_ang_z_upper ] trait['parameters'].insert(2,str(2)) trait['parameters'].append(str(limits)) if rbc.type == "SLIDER": limits = [ 1 if rbc.use_limit_lin_x else 0, rbc.limit_lin_x_lower, rbc.limit_lin_x_upper ] trait['parameters'].insert(2,str(3)) trait['parameters'].append(str(limits)) if rbc.type == "PISTON": limits = [ 1 if rbc.use_limit_lin_x else 0, rbc.limit_lin_x_lower, rbc.limit_lin_x_upper, 1 if rbc.use_limit_ang_x else 0, rbc.limit_ang_x_lower, rbc.limit_ang_x_upper ] trait['parameters'].insert(2,str(4)) trait['parameters'].append(str(limits)) o['traits'].append(trait) @staticmethod def post_export_world(world: bpy.types.World, out_world: Dict): wrd = bpy.data.worlds['Lnx'] bgcol = world.lnx_envtex_color # No compositor used if '_LDR' in world.world_defs: for i in range(0, 3): bgcol[i] = pow(bgcol[i], 1.0 / 2.2) out_world['background_color'] = lnx.utils.color_to_int(bgcol) if '_EnvSky' in world.world_defs: # Sky data for probe out_world['sun_direction'] = list(world.lnx_envtex_sun_direction) out_world['turbidity'] = world.lnx_envtex_turbidity out_world['ground_albedo'] = world.lnx_envtex_ground_albedo out_world['nishita_density'] = list(world.lnx_nishita_density) disable_hdr = world.lnx_envtex_name.endswith('.jpg') if '_EnvTex' in world.world_defs or '_EnvImg' in world.world_defs: out_world['envmap'] = world.lnx_envtex_name.rsplit('.', 1)[0] if disable_hdr: out_world['envmap'] += '.jpg' else: out_world['envmap'] += '.hdr' # Main probe rpdat = lnx.utils.get_rp() solid_mat = rpdat.lnx_material_model == 'Solid' lnx_irradiance = rpdat.lnx_irradiance and not solid_mat lnx_radiance = rpdat.lnx_radiance radtex = world.lnx_envtex_name.rsplit('.', 1)[0] # Remove file extension irrsharmonics = world.lnx_envtex_irr_name num_mips = world.lnx_envtex_num_mips strength = world.lnx_envtex_strength mobile_mat = rpdat.lnx_material_model in ('Mobile', 'Solid') if mobile_mat: lnx_radiance = False out_probe = {'name': lnx.utils.asset_name(world) if world.library else world.name} if lnx_irradiance: ext = '' if wrd.lnx_minimize else '.json' out_probe['irradiance'] = irrsharmonics + '_irradiance' + ext if lnx_radiance: out_probe['radiance'] = radtex + '_radiance' out_probe['radiance'] += '.jpg' if disable_hdr else '.hdr' out_probe['radiance_mipmaps'] = num_mips out_probe['strength'] = strength out_world['probe'] = out_probe @staticmethod def mod_equal(mod1: bpy.types.Modifier, mod2: bpy.types.Modifier) -> bool: """Compares whether the given modifiers are equal.""" # https://blender.stackexchange.com/questions/70629 return all([getattr(mod1, prop, True) == getattr(mod2, prop, False) for prop in mod1.bl_rna.properties.keys()]) @staticmethod def mod_equal_stack(obj1: bpy.types.Object, obj2: bpy.types.Object) -> bool: """Returns `True` if the given objects have the same modifiers.""" if len(obj1.modifiers) == 0 and len(obj2.modifiers) == 0: return True if len(obj1.modifiers) == 0 or len(obj2.modifiers) == 0: return False if len(obj1.modifiers) != len(obj2.modifiers): return False return all([LeenkxExporter.mod_equal(m, obj2.modifiers[i]) for i, m in enumerate(obj1.modifiers)])