Show error in the UI for tangent calculation error instead of just crashing to blender UI
2922 lines
119 KiB
Python
Executable file
2922 lines
119 KiB
Python
Executable file
"""
|
|
Armory Scene Exporter
|
|
http://armory3d.org/
|
|
|
|
Based on Open Game Engine Exchange
|
|
http://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:
|
|
http://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
|
|
|
|
import numpy as np
|
|
|
|
import bpy
|
|
from mathutils import *
|
|
|
|
import arm.assets as assets
|
|
import arm.exporter_opt as exporter_opt
|
|
import arm.log as log
|
|
import arm.make_renderpath as make_renderpath
|
|
import arm.material.cycles as cycles
|
|
import arm.material.make as make_material
|
|
import arm.material.mat_batch as mat_batch
|
|
import arm.utils
|
|
import arm.profiler
|
|
|
|
import arm.log as log
|
|
|
|
@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 == "FONT" or bobject.type == "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 ArmoryExporter:
|
|
"""Export to Armory 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 = {}
|
|
|
|
# `True` if there is at least one spawned camera in the scene
|
|
self.camera_spawned = False
|
|
|
|
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_arm_object_dict = {}
|
|
# Stores the link between a blender object and its
|
|
# corresponding export data (arm object)
|
|
self.object_to_arm_object_dict: Dict[bpy.types.Object, Dict] = {}
|
|
|
|
self.bone_tracks = []
|
|
|
|
ArmoryExporter.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 arm.profiler.Profile('profile_exporter.prof', arm.utils.get_pref_or_default('profile_exporter', False)):
|
|
cls(context, filepath, scene, depsgraph).execute()
|
|
|
|
@classmethod
|
|
def preprocess(cls):
|
|
wrd = bpy.data.worlds['Arm']
|
|
|
|
if wrd.arm_physics == 'Enabled':
|
|
cls.export_physics = True
|
|
cls.export_navigation = False
|
|
if wrd.arm_navigation == 'Enabled':
|
|
cls.export_navigation = True
|
|
cls.export_ui = False
|
|
|
|
@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 '.arm'
|
|
return mesh_fp + object_id + ext
|
|
|
|
@staticmethod
|
|
def get_shape_keys(mesh):
|
|
# Metaball
|
|
if not hasattr(mesh, 'shape_keys'):
|
|
return None
|
|
|
|
shape_keys = mesh.shape_keys
|
|
if shape_keys and len(shape_keys.key_blocks) > 1:
|
|
return shape_keys
|
|
return None
|
|
|
|
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):
|
|
bobject_ref = self.bobject_bone_array.get(bone)
|
|
|
|
if bobject_ref:
|
|
o['type'] = STRUCT_IDENTIFIER[bobject_ref["objectType"].value]
|
|
o['name'] = bobject_ref["structName"]
|
|
self.export_bone_transform(armature, bone, o, action)
|
|
|
|
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 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):
|
|
# Static transform
|
|
o['transform'] = {'values': ArmoryExporter.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 = arm.utils.safestr(arm.utils.asset_name(action))
|
|
|
|
fp = self.get_meshes_file_path('action_' + action_name, compressed=ArmoryExporter.compress_enabled)
|
|
assets.add(fp)
|
|
ext = '.lz4' if ArmoryExporter.compress_enabled else ''
|
|
if ext == '' and not bpy.data.worlds['Arm'].arm_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)
|
|
|
|
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:
|
|
log.warn(f"Action {action_name}: The data path '{data_path}' is not supported (yet)!")
|
|
continue
|
|
# Missing target entry for array_index or something else
|
|
else:
|
|
raise
|
|
|
|
out_anim['tracks'].append(out_track)
|
|
|
|
if True: # not action.arm_cached or not os.path.exists(fp):
|
|
wrd = bpy.data.worlds['Arm']
|
|
if wrd.arm_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]}
|
|
arm.utils.write_arm(fp, action_file)
|
|
|
|
def process_bone(self, bone: bpy.types.Bone) -> None:
|
|
if ArmoryExporter.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 ArmoryExporter.export_all_flag or bobject.select_get():
|
|
btype: NodeType = NodeType.get_bobject_type(bobject)
|
|
|
|
if btype is not NodeType.MESH and ArmoryExporter.option_mesh_only:
|
|
return
|
|
|
|
self.bobject_array[bobject] = {
|
|
"objectType": btype,
|
|
"structName": arm.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.arm_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': ArmoryExporter.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 use_default_material(self, bobject: bpy.types.Object, o):
|
|
if arm.utils.export_bone_data(bobject):
|
|
o['material_refs'].append('armdefaultskin')
|
|
self.default_skin_material_objects.append(bobject)
|
|
else:
|
|
o['material_refs'].append('armdefault')
|
|
self.default_material_objects.append(bobject)
|
|
|
|
def use_default_material_part(self):
|
|
# Particle object with no material assigned
|
|
for ps in bpy.data.particles:
|
|
if ps.render_type != 'OBJECT' or ps.instance_object is None:
|
|
continue
|
|
po = ps.instance_object
|
|
if po not in self.object_to_arm_object_dict:
|
|
continue
|
|
o = self.object_to_arm_object_dict[po]
|
|
if len(o['material_refs']) > 0 and o['material_refs'][0] == 'armdefault' and po not in self.default_part_material_objects:
|
|
self.default_part_material_objects.append(po)
|
|
o['material_refs'] = ['armdefaultpart'] # Replace armdefault
|
|
|
|
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(arm.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':
|
|
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 = ArmoryExporter.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 = ArmoryExporter.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 += ArmoryExporter.write_matrix((parent.matrix.inverted_safe() @ pose_bone.matrix))
|
|
else:
|
|
values += ArmoryExporter.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 arm.utils.export_bone_data(bobject):
|
|
variant_suffix = '_armskin'
|
|
# Tilesheets
|
|
elif bobject.arm_tilesheet != '':
|
|
variant_suffix = '_armtile'
|
|
|
|
if variant_suffix == '':
|
|
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(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 == '_armtile':
|
|
mat.arm_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':
|
|
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('_armpart'):
|
|
continue
|
|
|
|
matslots.append(slot)
|
|
mat_name = slot.material.name + '_armpart'
|
|
mat = bpy.data.materials.get(mat_name)
|
|
if mat is None:
|
|
mat = slot.material.copy()
|
|
mat.name = mat_name
|
|
mat.arm_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, scene: bpy.types.Scene, 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.arm_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_arm_object_dict:
|
|
out_object = {
|
|
'traits': [],
|
|
'spawn': False
|
|
}
|
|
self.object_to_arm_object_dict[bobject] = out_object
|
|
|
|
out_object = self.object_to_arm_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.arm_visible:
|
|
out_object['visible'] = False
|
|
|
|
if not bobject.cycles_visibility.camera:
|
|
out_object['visible_mesh'] = False
|
|
|
|
if not bobject.cycles_visibility.shadow:
|
|
out_object['visible_shadow'] = False
|
|
|
|
if not bobject.arm_spawn:
|
|
out_object['spawn'] = False
|
|
|
|
out_object['mobile'] = bobject.arm_mobile
|
|
|
|
if bobject.instance_type == 'COLLECTION' and bobject.instance_collection is not None:
|
|
out_object['group_ref'] = bobject.instance_collection.name
|
|
|
|
if bobject.arm_tilesheet != '':
|
|
out_object['tilesheet_ref'] = bobject.arm_tilesheet
|
|
out_object['tilesheet_action_ref'] = bobject.arm_tilesheet_action
|
|
|
|
if len(bobject.arm_propertylist) > 0:
|
|
out_object['properties'] = []
|
|
for proplist_item in bobject.arm_propertylist:
|
|
out_property = {
|
|
'name': proplist_item.name_prop,
|
|
'value': getattr(proplist_item, proplist_item.type_prop + '_prop')}
|
|
out_object['properties'].append(out_property)
|
|
|
|
# Export the object reference and material references
|
|
objref = bobject.data
|
|
if objref is not None:
|
|
objname = arm.utils.asset_name(objref)
|
|
|
|
# LOD
|
|
if bobject.type == 'MESH' and hasattr(objref, 'arm_lodlist') and len(objref.arm_lodlist) > 0:
|
|
out_object['lods'] = []
|
|
for lodlist_item in objref.arm_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.arm_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 = arm.utils.safestr(self.mesh_array[objref]["structName"])
|
|
|
|
wrd = bpy.data.worlds['Arm']
|
|
if wrd.arm_single_data_file:
|
|
out_object['data_ref'] = oid
|
|
else:
|
|
ext = '' if not ArmoryExporter.compress_enabled else '.lz4'
|
|
if ext == '' and not bpy.data.worlds['Arm'].arm_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.arm_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):
|
|
self.export_particle_system_ref(bobject.particle_systems[i], out_object)
|
|
|
|
aabb = bobject.data.arm_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 = ArmoryExporter.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 'spawn' in out_object and not out_object['spawn']:
|
|
self.camera_spawned |= False
|
|
else:
|
|
self.camera_spawned = 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('armorypose')
|
|
if action is None:
|
|
action = actions.new(name='armorypose')
|
|
|
|
# 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 = arm.utils.safestr(arm.utils.asset_name(bdata))
|
|
ext = '.lz4' if ArmoryExporter.compress_enabled else ''
|
|
if ext == '' and not bpy.data.worlds['Arm'].arm_minimize:
|
|
ext = '.json'
|
|
out_object['bone_actions'] = []
|
|
for action in export_actions:
|
|
aname = arm.utils.safestr(arm.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.arm_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 = arm.utils.safestr(arm.utils.asset_name(action))
|
|
skelobj.animation_data.action = action
|
|
fp = self.get_meshes_file_path('action_' + armatureid + '_' + aname, compressed=ArmoryExporter.compress_enabled)
|
|
assets.add(fp)
|
|
if not bdata.arm_cached or not os.path.exists(fp):
|
|
# Store action to use it after autobake was handled
|
|
original_action = action
|
|
|
|
# Handle autobake
|
|
if bdata.arm_autobake:
|
|
sel = bpy.context.selected_objects[:]
|
|
for _o in sel:
|
|
_o.select_set(False)
|
|
skelobj.select_set(True)
|
|
bpy.ops.nla.bake(frame_start=action.frame_range[0], frame_end=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)
|
|
baked_actions.append(action)
|
|
|
|
wrd = bpy.data.worlds['Arm']
|
|
if wrd.arm_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)
|
|
# Save action separately
|
|
action_obj = {'name': aname, 'objects': bones}
|
|
arm.utils.write_arm(fp, action_obj)
|
|
|
|
# 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.arm_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.arm_instanced == 'Off':
|
|
for subbobject in bobject.children:
|
|
self.export_object(subbobject, scene, 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': ArmoryExporter.write_matrix(bobject.matrix_world)}
|
|
oskin['transform'] = otrans
|
|
|
|
bone_array = armature.data.bones
|
|
bone_count = len(bone_array)
|
|
rpdat = arm.utils.get_rp()
|
|
max_bones = rpdat.arm_skin_max_bones
|
|
if bone_count > max_bones:
|
|
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='<f4')
|
|
|
|
for i in range(bone_count):
|
|
bone_ref = self.find_bone(bone_array[i].name)
|
|
if bone_ref:
|
|
oskin['bone_ref_array'][i] = bone_ref[1]["structName"]
|
|
oskin['bone_len_array'][i] = bone_array[i].length
|
|
else:
|
|
oskin['bone_ref_array'][i] = ""
|
|
oskin['bone_len_array'][i] = 0.0
|
|
|
|
# Write the bind pose transform array
|
|
oskin['transformsI'] = []
|
|
for i in range(bone_count):
|
|
skeleton_inv = (armature.matrix_world @ bone_array[i].matrix_local).inverted_safe()
|
|
skeleton_inv = (skeleton_inv @ bobject.matrix_world)
|
|
oskin['transformsI'].append(ArmoryExporter.write_matrix(skeleton_inv))
|
|
|
|
# Export the per-vertex bone influence data
|
|
group_remap = []
|
|
for group in bobject.vertex_groups:
|
|
for i in range(bone_count):
|
|
if bone_array[i].name == group.name:
|
|
group_remap.append(i)
|
|
break
|
|
else:
|
|
group_remap.append(-1)
|
|
|
|
bone_count_array = np.empty(len(export_mesh.loops), dtype='<i2')
|
|
bone_index_array = np.empty(len(export_mesh.loops) * 4, dtype='<i2')
|
|
bone_weight_array = np.empty(len(export_mesh.loops) * 4, dtype='<f4')
|
|
|
|
vertices = bobject.data.vertices
|
|
count = 0
|
|
for index, l in enumerate(export_mesh.loops):
|
|
bone_count = 0
|
|
total_weight = 0.0
|
|
bone_values = []
|
|
for g in vertices[l.vertex_index].groups:
|
|
bone_index = group_remap[g.group]
|
|
bone_weight = g.weight
|
|
if bone_index >= 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 != 0.0 and total_weight != 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='<i2')
|
|
|
|
oskin['bone_count_array'] = bone_count_array
|
|
oskin['bone_index_array'] = bone_index_array
|
|
oskin['bone_weight_array'] = bone_weight_array
|
|
|
|
# Bone constraints
|
|
if not armature.data.arm_autobake:
|
|
for bone in armature.pose.bones:
|
|
if len(bone.constraints) > 0:
|
|
if 'constraints' not in oskin:
|
|
oskin['constraints'] = []
|
|
self.add_constraints(bone, oskin, bone=True)
|
|
|
|
def write_mesh(self, bobject: bpy.types.Object, fp, out_mesh):
|
|
if bpy.data.worlds['Arm'].arm_single_data_file:
|
|
self.output['mesh_datas'].append(out_mesh)
|
|
|
|
# One mesh data per file
|
|
else:
|
|
mesh_obj = {'mesh_datas': [out_mesh]}
|
|
arm.utils.write_arm(fp, mesh_obj)
|
|
bobject.data.arm_cached = True
|
|
|
|
@staticmethod
|
|
def calc_aabb(bobject):
|
|
aabb_center = 0.125 * sum((Vector(b) for b in bobject.bound_box), Vector())
|
|
bobject.data.arm_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 \
|
|
]
|
|
|
|
def export_mesh_data(self, exportMesh, bobject: bpy.types.Object, o, has_armature=False):
|
|
exportMesh.calc_normals_split()
|
|
exportMesh.calc_loop_triangles()
|
|
|
|
loops = exportMesh.loops
|
|
num_verts = len(loops)
|
|
num_uv_layers = len(exportMesh.uv_layers)
|
|
is_baked = self.has_baked_material(bobject, exportMesh.materials)
|
|
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
|
|
num_colors = len(exportMesh.vertex_colors)
|
|
has_col = self.get_export_vcols(bobject.data) and num_colors > 0
|
|
has_tang = self.has_tangents(bobject.data)
|
|
|
|
pdata = np.empty(num_verts * 4, dtype='<f4') # p.xyz, n.z
|
|
ndata = np.empty(num_verts * 2, dtype='<f4') # n.xy
|
|
if has_tex:
|
|
t0map = 0 # Get active uvmap
|
|
t0data = np.empty(num_verts * 2, dtype='<f4')
|
|
uv_layers = exportMesh.uv_layers
|
|
if uv_layers is not None:
|
|
if 'UVMap_baked' in uv_layers:
|
|
for i in range(0, len(uv_layers)):
|
|
if uv_layers[i].name == 'UVMap_baked':
|
|
t0map = i
|
|
break
|
|
else:
|
|
for i in range(0, len(uv_layers)):
|
|
if uv_layers[i].active_render:
|
|
t0map = i
|
|
break
|
|
if has_tex1:
|
|
t1map = 1 if t0map == 0 else 0
|
|
t1data = np.empty(num_verts * 2, dtype='<f4')
|
|
# Scale for packed coords
|
|
maxdim = 1.0
|
|
lay0 = uv_layers[t0map]
|
|
for v in lay0.data:
|
|
if abs(v.uv[0]) > 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])
|
|
if abs(v.uv[1]) > maxdim:
|
|
maxdim = abs(v.uv[1])
|
|
if maxdim > 1:
|
|
o['scale_tex'] = maxdim
|
|
invscale_tex = (1 / o['scale_tex']) * 32767
|
|
else:
|
|
invscale_tex = 1 * 32767
|
|
if has_tang:
|
|
try:
|
|
exportMesh.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='<f4')
|
|
if has_col:
|
|
cdata = np.empty(num_verts * 3, dtype='<f4')
|
|
|
|
# Scale for packed coords
|
|
maxdim = max(bobject.data.arm_aabb[0], max(bobject.data.arm_aabb[1], bobject.data.arm_aabb[2]))
|
|
if maxdim > 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 = exportMesh.vertices
|
|
if has_tex:
|
|
lay0 = exportMesh.uv_layers[t0map]
|
|
if has_tex1:
|
|
lay1 = exportMesh.uv_layers[t1map]
|
|
if has_col:
|
|
vcol0 = exportMesh.vertex_colors[0].data
|
|
|
|
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_col:
|
|
col = vcol0[loop.index].color
|
|
i3 = i * 3
|
|
cdata[i3 ] = col[0]
|
|
cdata[i3 + 1] = col[1]
|
|
cdata[i3 + 2] = col[2]
|
|
|
|
mats = exportMesh.materials
|
|
poly_map = []
|
|
for i in range(max(len(mats), 1)):
|
|
poly_map.append([])
|
|
for poly in exportMesh.polygons:
|
|
poly_map[poly.material_index].append(poly)
|
|
|
|
o['index_arrays'] = []
|
|
|
|
# map polygon indices to triangle loops
|
|
tri_loops = {}
|
|
for loop in exportMesh.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='<i4')
|
|
|
|
i = 0
|
|
for poly in polys:
|
|
for loop in tri_loops[poly.index]:
|
|
prim[i ] = loops[loop.loops[0]].index
|
|
prim[i + 1] = loops[loop.loops[1]].index
|
|
prim[i + 2] = loops[loop.loops[2]].index
|
|
i += 3
|
|
|
|
ia = {}
|
|
ia['values'] = prim
|
|
ia['material'] = 0
|
|
if len(mats) > 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='<i2')
|
|
ndata = np.array(ndata, dtype='<i2')
|
|
if has_tex:
|
|
t0data *= invscale_tex
|
|
t0data = np.array(t0data, dtype='<i2')
|
|
if has_tex1:
|
|
t1data *= invscale_tex
|
|
t1data = np.array(t1data, dtype='<i2')
|
|
if has_col:
|
|
cdata *= 32767
|
|
cdata = np.array(cdata, dtype='<i2')
|
|
if has_tang:
|
|
tangdata *= 32767
|
|
tangdata = np.array(tangdata, dtype='<i2')
|
|
|
|
# Output
|
|
o['vertex_arrays'] = []
|
|
o['vertex_arrays'].append({ 'attrib': 'pos', 'values': pdata, 'data': 'short4norm' })
|
|
o['vertex_arrays'].append({ 'attrib': 'nor', 'values': ndata, 'data': 'short2norm' })
|
|
if has_tex:
|
|
o['vertex_arrays'].append({ 'attrib': 'tex', 'values': t0data, 'data': 'short2norm' })
|
|
if has_tex1:
|
|
o['vertex_arrays'].append({ 'attrib': 'tex1', 'values': t1data, 'data': 'short2norm' })
|
|
if has_col:
|
|
o['vertex_arrays'].append({ 'attrib': 'col', 'values': cdata, 'data': 'short4norm', 'padding': 1 })
|
|
if has_tang:
|
|
o['vertex_arrays'].append({ 'attrib': 'tang', 'values': tangdata, 'data': 'short4norm', 'padding': 1 })
|
|
|
|
# If there are multiple morph targets, export them here.
|
|
# if (shapeKeys):
|
|
# shapeKeys.key_blocks[0].value = 0.0
|
|
# for m in range(1, len(currentMorphValue)):
|
|
# shapeKeys.key_blocks[m].value = 1.0
|
|
# mesh.update()
|
|
|
|
# node.active_shape_key_index = m
|
|
# morphMesh = node.to_mesh(scene, applyModifiers, "RENDER", True, False)
|
|
|
|
# # Write the morph target position array.
|
|
|
|
# self.IndentWrite(B"VertexArray (attrib = \"position\", morph = ", 0, True)
|
|
# self.WriteInt(m)
|
|
# self.Write(B")\n")
|
|
# self.IndentWrite(B"{\n")
|
|
# self.indentLevel += 1
|
|
|
|
# self.IndentWrite(B"float[3]\t\t// ")
|
|
# self.WriteInt(vertexCount)
|
|
# self.IndentWrite(B"{\n", 0, True)
|
|
# self.WriteMorphPositionArray3D(unifiedVertexArray, morphMesh.vertices)
|
|
# self.IndentWrite(B"}\n")
|
|
|
|
# self.indentLevel -= 1
|
|
# self.IndentWrite(B"}\n\n")
|
|
|
|
# # Write the morph target normal array.
|
|
|
|
# self.IndentWrite(B"VertexArray (attrib = \"normal\", morph = ")
|
|
# self.WriteInt(m)
|
|
# self.Write(B")\n")
|
|
# self.IndentWrite(B"{\n")
|
|
# self.indentLevel += 1
|
|
|
|
# self.IndentWrite(B"float[3]\t\t// ")
|
|
# self.WriteInt(vertexCount)
|
|
# self.IndentWrite(B"{\n", 0, True)
|
|
# self.WriteMorphNormalArray3D(unifiedVertexArray, morphMesh.vertices, morphMesh.tessfaces)
|
|
# self.IndentWrite(B"}\n")
|
|
|
|
# self.indentLevel -= 1
|
|
# self.IndentWrite(B"}\n")
|
|
|
|
# bpy.data.meshes.remove(morphMesh)
|
|
|
|
def has_tangents(self, exportMesh):
|
|
return self.get_export_uvs(exportMesh) and self.get_export_tangents(exportMesh) and len(exportMesh.uv_layers) > 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 = arm.utils.safestr(object_ref[1]["structName"])
|
|
|
|
wrd = bpy.data.worlds['Arm']
|
|
if wrd.arm_single_data_file:
|
|
fp = None
|
|
else:
|
|
fp = self.get_meshes_file_path('mesh_' + oid, compressed=ArmoryExporter.compress_enabled)
|
|
assets.add(fp)
|
|
# No export necessary
|
|
if bobject.data.arm_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.arm_verbose_output:
|
|
print('Exporting mesh ' + arm.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 = bobject.active_shape_key_index
|
|
show_only_shape_key = bobject.show_only_shape_key
|
|
current_morph_value = []
|
|
|
|
shape_keys = ArmoryExporter.get_shape_keys(mesh)
|
|
if shape_keys:
|
|
bobject.active_shape_key_index = 0
|
|
bobject.show_only_shape_key = True
|
|
|
|
base_index = 0
|
|
relative = shape_keys.use_relative
|
|
if relative:
|
|
morph_count = 0
|
|
base_name = shape_keys.reference_key.name
|
|
for block in shape_keys.key_blocks:
|
|
if block.name == base_name:
|
|
base_index = morph_count
|
|
break
|
|
morph_count += 1
|
|
|
|
morph_count = 0
|
|
for block in shape_keys.key_blocks:
|
|
current_morph_value.append(block.value)
|
|
block.value = 0.0
|
|
|
|
if block.name != "":
|
|
# self.IndentWrite(B"Morph (index = ", 0, struct_flag)
|
|
# self.WriteInt(morph_count)
|
|
|
|
# if (relative) and (morph_count != base_index):
|
|
# self.Write(B", base = ")
|
|
# self.WriteInt(base_index)
|
|
|
|
# self.Write(B")\n")
|
|
# self.IndentWrite(B"{\n")
|
|
# self.IndentWrite(B"Name {string {\"", 1)
|
|
# self.Write(bytes(block.name, "UTF-8"))
|
|
# self.Write(B"\"}}\n")
|
|
# self.IndentWrite(B"}\n")
|
|
# TODO
|
|
struct_flag = True
|
|
|
|
morph_count += 1
|
|
|
|
shape_keys.key_blocks[0].value = 1.0
|
|
mesh.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()
|
|
|
|
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 ArmoryExporter.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
|
|
if shape_keys:
|
|
bobject.active_shape_key_index = active_shape_key_index
|
|
bobject.show_only_shape_key = show_only_shape_key
|
|
|
|
for m in range(len(current_morph_value)):
|
|
shape_keys.key_blocks[m].value = current_morph_value[m]
|
|
|
|
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.arm_dynamic_usage:
|
|
out_mesh['dynamic_usage'] = bobject.data.arm_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 = arm.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.arm_clip_start,
|
|
'far_plane': light_ref.arm_clip_end,
|
|
'fov': light_ref.arm_fov,
|
|
'color': [light_ref.color[0], light_ref.color[1], light_ref.color[2]],
|
|
'strength': light_ref.energy,
|
|
'shadows_bias': light_ref.arm_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'] = arm.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'] *= 2.6
|
|
if bpy.app.version >= (2, 80, 72):
|
|
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'] *= 2.6
|
|
if bpy.app.version >= (2, 80, 72):
|
|
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'] *= 80.0 / (light_ref.size * light_ref.size_y)
|
|
if bpy.app.version >= (2, 80, 72):
|
|
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):
|
|
"""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:
|
|
|
|
# Add unparented objects only, then instantiate full object
|
|
# child tree
|
|
if bobject.parent is None and bobject.arm_export:
|
|
|
|
# This object is controlled by proxy
|
|
has_proxy_user = False
|
|
for bo in bpy.data.objects:
|
|
if bo.proxy == bobject:
|
|
has_proxy_user = True
|
|
break
|
|
if has_proxy_user:
|
|
continue
|
|
|
|
asset_name = arm.utils.asset_name(bobject)
|
|
|
|
if collection.library is None:
|
|
# collection is in the same file, but (likely) on another scene
|
|
if asset_name not in scene_objects:
|
|
self.process_bobject(bobject)
|
|
self.export_object(bobject, self.scene)
|
|
else:
|
|
# 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, self.scene)
|
|
|
|
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
|
|
else:
|
|
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.arm_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 = arm.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(arm.utils.asset_path(objref.sound.filepath)) # Link sound to assets
|
|
o['sound'] = arm.utils.extract_filename(objref.sound.filepath)
|
|
else:
|
|
o['sound'] = ''
|
|
o['muted'] = objref.muted
|
|
o['loop'] = objref.arm_loop
|
|
o['stream'] = objref.arm_stream
|
|
o['volume'] = objref.volume
|
|
o['pitch'] = objref.pitch
|
|
o['attenuation'] = objref.attenuation
|
|
o['play_on_start'] = objref.arm_play_on_start
|
|
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.arm_cached = True
|
|
if is_particle:
|
|
mat.arm_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 = dict()
|
|
mat_users[mat] = mat_objs
|
|
mat_armusers = dict()
|
|
mat_armusers[mat] = [o]
|
|
make_material.parse(mat, o, mat_users, mat_armusers)
|
|
self.output['material_datas'].append(o)
|
|
bpy.data.materials.remove(mat)
|
|
rpdat = arm.utils.get_rp()
|
|
if not rpdat.arm_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 == 'RGB' or inp.type == 'RGBA' or inp.type == '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['Arm']
|
|
|
|
# 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.arm_batch_materials:
|
|
mat_users = self.material_to_object_dict
|
|
mat_armusers = self.material_to_arm_object_dict
|
|
mat_batch.build(self.material_array, mat_users, mat_armusers)
|
|
|
|
transluc_used = False
|
|
overlays_used = False
|
|
blending_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.arm_cached = False
|
|
if signature is not None:
|
|
material.signature = signature
|
|
|
|
o = {}
|
|
o['name'] = arm.utils.asset_name(material)
|
|
|
|
if material.arm_skip_context != '':
|
|
o['skip_context'] = material.arm_skip_context
|
|
|
|
rpdat = arm.utils.get_rp()
|
|
if material.arm_two_sided or not rpdat.arm_culling:
|
|
o['override_context'] = {}
|
|
o['override_context']['cull_mode'] = 'none'
|
|
elif material.arm_cull_mode != 'clockwise':
|
|
o['override_context'] = {}
|
|
o['override_context']['cull_mode'] = material.arm_cull_mode
|
|
|
|
o['contexts'] = []
|
|
|
|
mat_users = self.material_to_object_dict
|
|
mat_armusers = self.material_to_arm_object_dict
|
|
sd, rpasses = make_material.parse(material, o, mat_users, mat_armusers)
|
|
|
|
# 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'] = 'armory.trait.internal.MovieTexture'
|
|
ArmoryExporter.import_traits.append(trait['class_name'])
|
|
trait['parameters'] = ['"' + tex['file'] + '"']
|
|
for user in mat_armusers[material]:
|
|
user['traits'].append(trait)
|
|
|
|
if 'translucent' in rpasses:
|
|
transluc_used = True
|
|
if 'overlay' in rpasses:
|
|
overlays_used = True
|
|
if 'mesh' in rpasses and material.arm_blending:
|
|
blending_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.arm_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.arm_cached = False
|
|
|
|
self.output['material_datas'].append(o)
|
|
material.arm_cached = True
|
|
|
|
# Auto-enable render-path featues
|
|
rebuild_rp = False
|
|
rpdat = arm.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_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():
|
|
psettings = particleRef[0]
|
|
|
|
if psettings is None:
|
|
continue
|
|
|
|
if psettings.instance_object is None or psettings.render_type != 'OBJECT':
|
|
continue
|
|
|
|
out_particlesys = {
|
|
'name': particleRef[1]["structName"],
|
|
'type': 0 if psettings.type == 'EMITTER' else 1, # HAIR
|
|
'loop': psettings.arm_loop,
|
|
# Emission
|
|
'count': int(psettings.count * psettings.arm_count_mult),
|
|
'frame_start': int(psettings.frame_start),
|
|
'frame_end': int(psettings.frame_end),
|
|
'lifetime': psettings.lifetime,
|
|
'lifetime_random': psettings.lifetime_random,
|
|
'emit_from': 1 if psettings.emit_from == 'VOLUME' else 0, # VERT, FACE
|
|
# 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': arm.utils.asset_name(psettings.instance_object),
|
|
# Field weights
|
|
'weight_gravity': psettings.effector_weights.gravity
|
|
}
|
|
|
|
self.object_to_arm_object_dict[psettings.instance_object]['is_particle'] = True
|
|
self.output['particle_datas'].append(out_particlesys)
|
|
|
|
def export_tilesheets(self):
|
|
wrd = bpy.data.worlds['Arm']
|
|
if len(wrd.arm_tilesheetlist) > 0:
|
|
self.output['tilesheet_datas'] = []
|
|
for ts in wrd.arm_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.arm_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 = arm.utils.safestr(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 arm.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 ArmoryExporter.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(arm.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()
|
|
print('Exporting ' + arm.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_arm_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 == 'CLOTH' or mod.type == 'SOFT_BODY':
|
|
ArmoryExporter.optimize_enabled = True
|
|
|
|
self.process_skinned_meshes()
|
|
|
|
self.output['name'] = arm.utils.safestr(self.scene.name)
|
|
if self.filepath.endswith('.lz4'):
|
|
self.output['name'] += '.lz4'
|
|
elif not bpy.data.worlds['Arm'].arm_minimize:
|
|
self.output['name'] += '.json'
|
|
|
|
# Create unique material variants for skinning, tilesheets and particles
|
|
matvars, matslots = self.create_material_variants(self.scene)
|
|
|
|
# Auto-bones
|
|
wrd = bpy.data.worlds['Arm']
|
|
rpdat = arm.utils.get_rp()
|
|
if rpdat.arm_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.arm_skin_max_bones = max_bones
|
|
|
|
# Terrain
|
|
if self.scene.arm_terrain_object is not None:
|
|
assets.add_khafile_def('arm_terrain')
|
|
|
|
# Append trait
|
|
out_trait = {
|
|
'type': 'Script',
|
|
'class_name': 'armory.trait.internal.TerrainPhysics'
|
|
}
|
|
if 'traits' not in self.output:
|
|
self.output['traits']: List[Dict[str, str]] = []
|
|
|
|
self.output['traits'].append(out_trait)
|
|
|
|
ArmoryExporter.import_traits.append(out_trait['class_name'])
|
|
ArmoryExporter.export_physics = True
|
|
|
|
# Export material
|
|
mat = self.scene.arm_terrain_object.children[0].data.materials[0]
|
|
self.material_array.append(mat)
|
|
# Terrain data
|
|
out_terrain = {
|
|
'name': 'Terrain',
|
|
'sectors_x': self.scene.arm_terrain_sectors[0],
|
|
'sectors_y': self.scene.arm_terrain_sectors[1],
|
|
'sector_size': self.scene.arm_terrain_sector_size,
|
|
'height_scale': self.scene.arm_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, self.scene)
|
|
|
|
# Export collections
|
|
if bpy.data.collections:
|
|
self.output['groups'] = []
|
|
for collection in bpy.data.collections:
|
|
if collection.name.startswith(('RigidBodyWorld', 'Trait|')):
|
|
continue
|
|
|
|
self.export_collection(collection)
|
|
|
|
if not ArmoryExporter.option_mesh_only:
|
|
if self.scene.camera is not None:
|
|
self.output['camera_ref'] = self.scene.camera.name
|
|
else:
|
|
if self.scene.name == arm.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('armdefault', self.default_material_objects)
|
|
if len(self.default_skin_material_objects) > 0:
|
|
self.make_default_mat('armdefaultskin', 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('armdefaultpart', 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'] = 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['Arm'].arm_play_camera != 'Scene':
|
|
self.create_default_camera(is_viewport_camera=True)
|
|
self.camera_spawned = True
|
|
|
|
# No camera found
|
|
if not self.camera_spawned:
|
|
log.warn( f'Scene "{self.scene.name}" is missing a camera')
|
|
|
|
# No camera found, create a default one
|
|
if (len(self.output['camera_datas']) == 0 or len(bpy.data.cameras) == 0) or not self.camera_spawned:
|
|
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
|
|
arm.utils.write_arm(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]] # _armskin, _armpart, _armtile
|
|
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.arm_cached = slot.material.arm_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)
|
|
|
|
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'] = ArmoryExporter.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': 'armory.trait.WalkNavigation'
|
|
}
|
|
out_object['traits'] = [trait]
|
|
ArmoryExporter.import_traits.append(trait['class_name'])
|
|
|
|
self.output['objects'].append(out_object)
|
|
self.output['camera_ref'] = 'DefaultCamera'
|
|
|
|
@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.arm_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.arm_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['Arm']
|
|
phys_enabled = wrd.arm_physics != 'Disabled'
|
|
phys_pkg = 'bullet' if wrd.arm_physics_engine == 'Bullet' else 'oimo'
|
|
|
|
# Rigid body trait
|
|
if bobject.rigid_body is not None and phys_enabled:
|
|
ArmoryExporter.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'] = 'armory.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.arm_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.arm_rb_linear_factor[0]
|
|
ly = bobject.arm_rb_linear_factor[1]
|
|
lz = bobject.arm_rb_linear_factor[2]
|
|
ax = bobject.arm_rb_angular_factor[0]
|
|
ay = bobject.arm_rb_angular_factor[1]
|
|
az = bobject.arm_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 = str(rb.collision_margin) if rb.use_margin else '0.0'
|
|
if rb.use_deactivation:
|
|
deact_lv = str(rb.deactivate_linear_velocity)
|
|
deact_av = str(rb.deactivate_angular_velocity)
|
|
deact_time = str(bobject.arm_rb_deactivation_time)
|
|
else:
|
|
deact_lv = '0.0'
|
|
deact_av = '0.0'
|
|
deact_time = '0.0'
|
|
body_params = '[{0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}]'.format(
|
|
str(rb.linear_damping),
|
|
str(rb.angular_damping),
|
|
str(lx), str(ly), str(lz),
|
|
str(ax), str(ay), str(az),
|
|
col_margin,
|
|
deact_lv, deact_av, deact_time
|
|
)
|
|
body_flags = '[{0}, {1}, {2}, {3}, {4}]'.format(
|
|
str(rb.kinematic).lower(),
|
|
str(bobject.arm_rb_trigger).lower(),
|
|
str(bobject.arm_rb_ccd).lower(),
|
|
str(is_static).lower(),
|
|
str(rb.use_deactivation).lower()
|
|
)
|
|
x['parameters'].append(body_params)
|
|
x['parameters'].append(body_flags)
|
|
o['traits'].append(x)
|
|
|
|
# Phys traits
|
|
if phys_enabled:
|
|
for modifier in bobject.modifiers:
|
|
if modifier.type == 'CLOTH' or modifier.type == '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, 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['Arm'].arm_play_camera != 'Scene':
|
|
navigation_trait = {}
|
|
navigation_trait['type'] = 'Script'
|
|
navigation_trait['class_name'] = 'armory.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_arm_object_dict[mat].append(o)
|
|
else:
|
|
self.material_to_object_dict[mat] = [bobject]
|
|
self.material_to_arm_object_dict[mat] = [o]
|
|
|
|
# Export constraints
|
|
if len(bobject.constraints) > 0:
|
|
o['constraints'] = []
|
|
self.add_constraints(bobject, o)
|
|
|
|
for x in o['traits']:
|
|
ArmoryExporter.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: bpy.types.Object, o):
|
|
if not hasattr(bobject, 'arm_traitlist'):
|
|
return
|
|
|
|
for traitlistItem in bobject.arm_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 = arm.utils.safesrc(traitlistItem.node_tree_prop.name[0].upper() + traitlistItem.node_tree_prop.name[1:])
|
|
|
|
out_trait['type'] = 'Script'
|
|
out_trait['class_name'] = arm.utils.safestr(bpy.data.worlds['Arm'].arm_project_package) + '.node.' + group_name
|
|
|
|
elif traitlistItem.type_prop == 'WebAssembly':
|
|
wpath = os.path.join(arm.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'] = 'armory.trait.internal.WasmScript'
|
|
out_trait['parameters'] = ["'" + traitlistItem.webassembly_prop + "'"]
|
|
|
|
elif traitlistItem.type_prop == 'UI Canvas':
|
|
cpath = os.path.join(arm.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
|
|
|
|
ArmoryExporter.export_ui = True
|
|
out_trait['type'] = 'Script'
|
|
out_trait['class_name'] = 'armory.trait.internal.CanvasScript'
|
|
out_trait['parameters'] = ["'" + traitlistItem.canvas_name_prop + "'"]
|
|
|
|
# Read file list and add canvas assets
|
|
assetpath = os.path.join(arm.utils.get_fp(), 'Bundled', 'canvas', traitlistItem.canvas_name_prop + '.files')
|
|
if os.path.exists(assetpath):
|
|
with open(assetpath) 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 = 'armory.trait.'
|
|
|
|
# TODO: temporary, export single mesh navmesh as obj
|
|
if traitlistItem.class_name_prop == 'NavMesh' and bobject.type == 'MESH' and bpy.data.worlds['Arm'].arm_navigation != 'Disabled':
|
|
ArmoryExporter.export_navigation = True
|
|
|
|
nav_path = os.path.join(arm.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 + '.arm')
|
|
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()
|
|
# 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 = arm.utils.safestr(bpy.data.worlds['Arm'].arm_project_package) + '.'
|
|
hxfile = os.path.join('Sources', (trait_prefix + traitlistItem.class_name_prop).replace('.', '/') + '.hx')
|
|
if not os.path.exists(os.path.join(arm.utils.get_fp(), hxfile)):
|
|
# TODO: Halt build here once this check is tested
|
|
print(f'Armory 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.arm_traitpropslist:
|
|
out_trait['props'] = []
|
|
for trait_prop in traitlistItem.arm_traitpropslist:
|
|
out_trait['props'].append(trait_prop.name)
|
|
out_trait['props'].append(trait_prop.type)
|
|
|
|
if trait_prop.type.endswith("Object"):
|
|
value = arm.utils.asset_name(trait_prop.value_object)
|
|
else:
|
|
value = trait_prop.get_value()
|
|
|
|
out_trait['props'].append(value)
|
|
|
|
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['Arm']
|
|
|
|
if wrd.arm_physics != 'Disabled' and ArmoryExporter.export_physics:
|
|
if 'traits' not in self.output:
|
|
self.output['traits'] = []
|
|
phys_pkg = 'bullet' if wrd.arm_physics_engine == 'Bullet' else 'oimo'
|
|
|
|
out_trait = {
|
|
'type': 'Script',
|
|
'class_name': 'armory.trait.physics.' + phys_pkg + '.PhysicsWorld'
|
|
}
|
|
|
|
rbw = self.scene.rigidbody_world
|
|
if rbw is not None and rbw.enabled:
|
|
out_trait['parameters'] = [str(rbw.time_scale), str(1 / rbw.steps_per_second), str(rbw.solver_iterations)]
|
|
|
|
self.output['traits'].append(out_trait)
|
|
|
|
if wrd.arm_navigation != 'Disabled' and ArmoryExporter.export_navigation:
|
|
if 'traits' not in self.output:
|
|
self.output['traits'] = []
|
|
out_trait = {'type': 'Script', 'class_name': 'armory.trait.navigation.Navigation'}
|
|
self.output['traits'].append(out_trait)
|
|
|
|
if wrd.arm_debug_console:
|
|
if 'traits' not in self.output:
|
|
self.output['traits'] = []
|
|
ArmoryExporter.export_ui = True
|
|
# Position
|
|
debug_console_pos_type = 2
|
|
if (wrd.arm_debug_console_position == 'Left'):
|
|
debug_console_pos_type = 0
|
|
elif (wrd.arm_debug_console_position == 'Center'):
|
|
debug_console_pos_type = 1
|
|
else:
|
|
debug_console_pos_type = 2
|
|
# Parameters
|
|
out_trait = {
|
|
'type': 'Script',
|
|
'class_name': 'armory.trait.internal.DebugConsole',
|
|
'parameters': [str(arm.utils.get_ui_scale()),
|
|
str(wrd.arm_debug_console_scale),
|
|
str(debug_console_pos_type),
|
|
str(int(wrd.arm_debug_console_visible)),
|
|
str(int(arm.utils.get_debug_console_visible_sc())),
|
|
str(int(arm.utils.get_debug_console_scale_in_sc())),
|
|
str(int(arm.utils.get_debug_console_scale_out_sc()))]
|
|
}
|
|
self.output['traits'].append(out_trait)
|
|
|
|
if wrd.arm_live_patch:
|
|
if 'traits' not in self.output:
|
|
self.output['traits'] = []
|
|
out_trait = {'type': 'Script', 'class_name': 'armory.trait.internal.LivePatch'}
|
|
self.output['traits'].append(out_trait)
|
|
|
|
if len(self.scene.arm_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']:
|
|
ArmoryExporter.import_traits.append(out_trait['class_name'])
|
|
|
|
@staticmethod
|
|
def export_canvas_themes():
|
|
path_themes = os.path.join(arm.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+"):
|
|
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.
|
|
"""
|
|
ArmoryExporter.export_physics = True
|
|
assets.add_khafile_def('arm_physics_soft')
|
|
|
|
phys_pkg = 'bullet' if bpy.data.worlds['Arm'].arm_physics_engine == 'Bullet' else 'oimo'
|
|
out_trait = {'type': 'Script', 'class_name': 'armory.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.arm_soft_body_margin)]
|
|
o['traits'].append(out_trait)
|
|
|
|
if soft_type == 0:
|
|
ArmoryExporter.add_hook_mod(o, bobject, '', modifier.settings.vertex_group_mass)
|
|
|
|
@staticmethod
|
|
def add_hook_mod(o, bobject: bpy.types.Object, target_name, group_name):
|
|
ArmoryExporter.export_physics = True
|
|
|
|
phys_pkg = 'bullet' if bpy.data.worlds['Arm'].arm_physics_engine == 'Bullet' else 'oimo'
|
|
out_trait = {'type': 'Script', 'class_name': 'armory.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, rbc):
|
|
rb1 = rbc.object1
|
|
rb2 = rbc.object2
|
|
if rb1 is None or rb2 is None:
|
|
return
|
|
if rbc.type == "MOTOR":
|
|
return
|
|
|
|
ArmoryExporter.export_physics = True
|
|
phys_pkg = 'bullet' if bpy.data.worlds['Arm'].arm_physics_engine == 'Bullet' else 'oimo'
|
|
breaking_threshold = rbc.breaking_threshold if rbc.use_breaking else 0
|
|
|
|
trait = {
|
|
'type': 'Script',
|
|
'class_name': 'armory.trait.physics.' + phys_pkg + '.PhysicsConstraintExportHelper',
|
|
'parameters': [
|
|
"'" + rb1.name + "'",
|
|
"'" + rb2.name + "'",
|
|
str(rbc.disable_collisions).lower(),
|
|
str(breaking_threshold)
|
|
]
|
|
}
|
|
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['Arm']
|
|
|
|
bgcol = world.arm_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'] = arm.utils.color_to_int(bgcol)
|
|
|
|
if '_EnvSky' in world.world_defs:
|
|
# Sky data for probe
|
|
out_world['sun_direction'] = list(world.arm_envtex_sun_direction)
|
|
out_world['turbidity'] = world.arm_envtex_turbidity
|
|
out_world['ground_albedo'] = world.arm_envtex_ground_albedo
|
|
|
|
disable_hdr = world.arm_envtex_name.endswith('.jpg')
|
|
|
|
if '_EnvTex' in world.world_defs or '_EnvImg' in world.world_defs:
|
|
out_world['envmap'] = world.arm_envtex_name.rsplit('.', 1)[0]
|
|
if disable_hdr:
|
|
out_world['envmap'] += '.jpg'
|
|
else:
|
|
out_world['envmap'] += '.hdr'
|
|
|
|
# Main probe
|
|
rpdat = arm.utils.get_rp()
|
|
solid_mat = rpdat.arm_material_model == 'Solid'
|
|
arm_irradiance = rpdat.arm_irradiance and not solid_mat
|
|
arm_radiance = False
|
|
radtex = world.arm_envtex_name.rsplit('.', 1)[0]
|
|
irrsharmonics = world.arm_envtex_irr_name
|
|
|
|
# Radiance
|
|
if '_EnvTex' in world.world_defs:
|
|
arm_radiance = rpdat.arm_radiance
|
|
elif '_EnvSky' in world.world_defs:
|
|
arm_radiance = rpdat.arm_radiance
|
|
radtex = 'hosek'
|
|
num_mips = world.arm_envtex_num_mips
|
|
strength = world.arm_envtex_strength
|
|
|
|
mobile_mat = rpdat.arm_material_model == 'Mobile' or rpdat.arm_material_model == 'Solid'
|
|
if mobile_mat:
|
|
arm_radiance = False
|
|
|
|
out_probe = {'name': world.name}
|
|
if arm_irradiance:
|
|
ext = '' if wrd.arm_minimize else '.json'
|
|
out_probe['irradiance'] = irrsharmonics + '_irradiance' + ext
|
|
if arm_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([ArmoryExporter.mod_equal(m, obj2.modifiers[i]) for i, m in enumerate(obj1.modifiers)])
|