├── .gitignore ├── examples ├── normals │ ├── normals.blend │ └── normals_custom.blend ├── animated_mesh │ ├── door.blend │ └── multiple_animations.blend ├── collisions │ └── collisions.blend └── vegetation │ ├── touch_bending.blend │ ├── touch_bending2.blend │ └── multi_touch_bending.blend ├── io_bcry_exporter ├── icons │ └── CryEngine.png ├── outpipe.py ├── exceptions.py ├── configuration.py ├── udp.py ├── desc.py ├── export_materials.py ├── rc.py ├── export_animations.py ├── material_utils.py ├── export.py └── utils.py └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | */.mypy_cache/* 2 | */__pycache__/* 3 | */.idea/* 4 | */.vscode/ 5 | *.7z 6 | *.code-workspace 7 | *.blend1 8 | -------------------------------------------------------------------------------- /examples/normals/normals.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeonidasWhite/BCRYExporter/HEAD/examples/normals/normals.blend -------------------------------------------------------------------------------- /examples/animated_mesh/door.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeonidasWhite/BCRYExporter/HEAD/examples/animated_mesh/door.blend -------------------------------------------------------------------------------- /examples/collisions/collisions.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeonidasWhite/BCRYExporter/HEAD/examples/collisions/collisions.blend -------------------------------------------------------------------------------- /examples/normals/normals_custom.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeonidasWhite/BCRYExporter/HEAD/examples/normals/normals_custom.blend -------------------------------------------------------------------------------- /io_bcry_exporter/icons/CryEngine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeonidasWhite/BCRYExporter/HEAD/io_bcry_exporter/icons/CryEngine.png -------------------------------------------------------------------------------- /examples/vegetation/touch_bending.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeonidasWhite/BCRYExporter/HEAD/examples/vegetation/touch_bending.blend -------------------------------------------------------------------------------- /examples/vegetation/touch_bending2.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeonidasWhite/BCRYExporter/HEAD/examples/vegetation/touch_bending2.blend -------------------------------------------------------------------------------- /examples/vegetation/multi_touch_bending.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeonidasWhite/BCRYExporter/HEAD/examples/vegetation/multi_touch_bending.blend -------------------------------------------------------------------------------- /examples/animated_mesh/multiple_animations.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeonidasWhite/BCRYExporter/HEAD/examples/animated_mesh/multiple_animations.blend -------------------------------------------------------------------------------- /io_bcry_exporter/outpipe.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # Name: outpipe.py 3 | # Purpose: Pipeline for console output 4 | # 5 | # Author: Angelo J. Miner, 6 | # Mikołaj Milej, Özkan Afacan, Daniel White, 7 | # Oscar Martin Garcia, Duo Oratar, David Marcelis 8 | # 9 | # Created: N/A 10 | # Copyright: (c) N/A 11 | # Licence: GPLv2+ 12 | # ------------------------------------------------------------------------------ 13 | 14 | # 15 | 16 | 17 | from . import exceptions 18 | from logging import basicConfig, info, debug, warning, DEBUG 19 | 20 | 21 | class OutPipe(): 22 | 23 | def __init__(self): 24 | pass 25 | 26 | def pump(self, message, message_type='info', newline=False): 27 | if newline: 28 | print() 29 | 30 | if message_type == 'info': 31 | print("[Info] BCry: {!r}".format(message)) 32 | 33 | elif message_type == 'debug': 34 | print("[Debug] BCry: {!r}".format(message)) 35 | 36 | elif message_type == 'warning': 37 | print("[Warning] BCry: {!r}".format(message)) 38 | 39 | elif message_type == 'error': 40 | print("[Error] BCry: {!r}".format(message)) 41 | 42 | else: 43 | raise exceptions.BCryException("No such message type {!r}". 44 | format(message_type)) 45 | 46 | 47 | op = OutPipe() 48 | 49 | 50 | def bcPrint(msg, message_type='info', newline=False): 51 | op.pump(msg, message_type, newline) 52 | -------------------------------------------------------------------------------- /io_bcry_exporter/exceptions.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # Name: exceptions.py 3 | # Purpose: Holds custom exception classes 4 | # 5 | # Author: Mikołaj Milej, 6 | # Özkan Afacan, Daniel White 7 | # 8 | # Created: 23/06/2013 9 | # Copyright: (c) Mikołaj Milej 2013 10 | # Copyright: (c) Özkan Afacan 2016 11 | # License: GPLv2+ 12 | # ------------------------------------------------------------------------------ 13 | 14 | # 15 | 16 | 17 | class BCryException(RuntimeError): 18 | 19 | def __init__(self, message): 20 | self._message = message 21 | 22 | def __str__(self): 23 | return self.what() 24 | 25 | def what(self): 26 | return self._message 27 | 28 | 29 | class BlendNotSavedException(BCryException): 30 | 31 | def __init__(self): 32 | message = "Blend file has to be saved before exporting." 33 | 34 | BCryException.__init__(self, message) 35 | 36 | 37 | class TextureAndBlendDiskMismatchException(BCryException): 38 | 39 | def __init__(self, blend_path, texture_path): 40 | message = """ 41 | Blend file and all textures have to be placed on the same disk. 42 | It's impossible to create relative paths if they are not. 43 | Blend file: {!r} 44 | Texture file: {!r}""".format(blend_path, texture_path) 45 | 46 | BCryException.__init__(self, message) 47 | 48 | 49 | class NoRcSelectedException(BCryException): 50 | 51 | def __init__(self): 52 | message = """ 53 | Please find Resource Compiler first. 54 | Usually located in 'CryEngine\\Bin32\\rc\\rc.exe' 55 | """ 56 | 57 | BCryException.__init__(self, message) 58 | 59 | 60 | class NoGameDirectorySelected(BCryException): 61 | 62 | def __init__(self): 63 | message = "Please select a Game Directory!" 64 | 65 | BCryException.__init__(self, message) 66 | 67 | 68 | class MarkersNotFound(BCryException): 69 | 70 | def __init__(self): 71 | message = "Start or end marker is less!" 72 | 73 | BCryException.__init__(self, message) 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BCRYExporter 2 | This is ported version of BCRYExporter for Cryengine 5 from https://github.com/AFCStudio/BCRYExporter for Blender 2.8* 3 | 4 | Installation: 5 | Copy `io_bcry_exporter` folder to blender_path\Scripts\Addons directory. 6 | 7 | Example: C:\Users\ `Your_User_Name` \AppData\Roaming\Blender Foundation\Blender\2.80\scripts\addons 8 | 9 | Documentation: maybe soon 10 | 11 | Tested: 12 | 1) Export static geometry (support box, sphere, capsule, cylinder and mesh collision types). (see example) 13 | 2) Export usual and custom normal shading for objects. (see example) 14 | 3) Export skeleton(chr), skeleton radgoll settings, skin. 15 | 4) Export Animated Mesh (CGA) with single or multiple animations. (see example) 16 | 5) Export branch for touch vegetation. (see example) 17 | 6) Export animation through Limit with Values. 18 | 7) All other utilities (Export, configuration, bone utilities, mesh utilities, material utilities, UDP). 19 | 20 | Not tested but must work: 21 | 1) Export breakable joint 22 | 23 | What's new: 24 | 1) Now you can add collision object to selected objects (not only for active) 25 | 2) Add "Mesh collision" (Recommended use less than 256 vertices and convex hull shape, but concave support too) 26 | 3) Blender 2.80 has new type of grouping objects instead layer and group - Collection. Now when you create export node, BCRY exporter create cry_exporter_nodes collection. All export nodes inside this collection are exported when you call export. 27 | 28 | Menu: Blender 2.80 has Quick Favorite menu which called by "Q" key. But there is not way to add to quick favorite from python code. I added this menu in Configuration section. You can do right mouse click on them and set "Add to Quick favorite". 29 | 30 | About Mesh collision: Now when you click to create Mesh button (in Cry Utilities): 31 | 32 | In Object Mode: 33 | 1) Will be create the same mesh with wire display type. You can use separate checkbox in advanced options menu to separate new mesh into loose parts. (Multiple selected objects support) 34 | 35 | In Edit mode: 36 | 1) Blender now supports multiedit by core. You can select many objects and change mode to EDIT. Then you can select through link (key 'L') a part of this mesh and click Mesh button to separate selected part as a mesh collision. Multiple selected meshes supports too. The current mode state will be save. The objects without selected vertices will be ignored. 37 | 38 | Known issues/limitation: 39 | 1) Custom split normal doesn't support Sharp edges in skin node.
Solution: Add Edge split modifier before Armature modifier, set up and apply. The skin mesh will be rip in Sharp edges places. 40 | 2) There is no way to set active any collection, so be careful that current active collection be unhide and not disable. Otherwise you can expect Error that Object is Null. I put in some operators code which set Master collection(Scene Collection) active. 41 | 3) In some cases when you modify the armature as add bones through edit_bones.new() method or other way BCRYExporter will be export armature with broken bones hierarchy. I think that is Blender bug because I nothing changed in export code except fix errors with Bmesh convertation.
Solution: I added a button in Bone utilities: Rebuild-armature. It rebuild selected armature from zero and copy all bones parameters: name, matrix, roll, parent, some properties and custom properties then delete old bones. It don't copy bone constraints at the moment.
IMPORTANT: vertex groups renames too, but only if the object is a children of this armature. Do skin object a child of armature before use this button. 42 | 43 | Enjoy. 44 | -------------------------------------------------------------------------------- /io_bcry_exporter/configuration.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # Name: configuration.py 3 | # Purpose: Stores CryBlend configuration settings 4 | # 5 | # Author: Mikołaj Milej, 6 | # Angelo J. Miner, Daniel White, Özkan Afacan, 7 | # Oscar Martin Garcia, Duo Oratar, David Marcelis 8 | # 9 | # Created: 02/10/2013 10 | # Copyright: (c) Mikołaj Milej 2013 11 | # Licence: GPLv2+ 12 | # ------------------------------------------------------------------------------ 13 | 14 | # 15 | 16 | 17 | import os 18 | import pickle 19 | 20 | import bpy 21 | 22 | from .outpipe import bcPrint 23 | from .utils import get_filename 24 | 25 | 26 | class __Configuration: 27 | __CONFIG_PATH = bpy.utils.user_resource('CONFIG', 28 | path='scripts', 29 | create=True) 30 | __CONFIG_FILENAME = 'bcry.cfg' 31 | __CONFIG_FILEPATH = os.path.join(__CONFIG_PATH, __CONFIG_FILENAME) 32 | __DEFAULT_CONFIGURATION = {'RC_PATH': r'', 33 | 'TEXTURE_RC_PATH': r'', 34 | 'GAME_DIR': r''} 35 | 36 | def __init__(self): 37 | self.__CONFIG = self.__load({}) 38 | 39 | @property 40 | def rc_path(self): 41 | return self.__CONFIG['RC_PATH'] 42 | 43 | @rc_path.setter 44 | def rc_path(self, value): 45 | self.__CONFIG['RC_PATH'] = value 46 | 47 | @property 48 | def texture_rc_path(self): 49 | if (not self.__CONFIG['TEXTURE_RC_PATH']): 50 | return self.rc_path 51 | 52 | return self.__CONFIG['TEXTURE_RC_PATH'] 53 | 54 | @texture_rc_path.setter 55 | def texture_rc_path(self, value): 56 | self.__CONFIG['TEXTURE_RC_PATH'] = value 57 | 58 | @property 59 | def game_dir(self): 60 | return self.__CONFIG['GAME_DIR'] 61 | 62 | @game_dir.setter 63 | def game_dir(self, value): 64 | self.__CONFIG['GAME_DIR'] = value 65 | 66 | def configured(self): 67 | path = self.__CONFIG['RC_PATH'] 68 | if len(path) > 0 and get_filename(path) == "rc": 69 | return True 70 | 71 | return False 72 | 73 | def save(self): 74 | bcPrint("Saving configuration file.", 'debug') 75 | 76 | if os.path.isdir(self.__CONFIG_PATH): 77 | try: 78 | with open(self.__CONFIG_FILEPATH, 'wb') as f: 79 | pickle.dump(self.__CONFIG, f, -1) 80 | bcPrint("Configuration file saved.") 81 | 82 | bcPrint('Saved {}'.format(self.__CONFIG_FILEPATH)) 83 | 84 | except: 85 | bcPrint( 86 | "[IO] can not write: {}".format( 87 | self.__CONFIG_FILEPATH), 'error') 88 | 89 | else: 90 | bcPrint("Configuration file path is missing {}".format( 91 | self.__CONFIG_PATH), 92 | 'error') 93 | 94 | def __load(self, current_configuration): 95 | new_configuration = {} 96 | new_configuration.update(self.__DEFAULT_CONFIGURATION) 97 | new_configuration.update(current_configuration) 98 | 99 | if os.path.isfile(self.__CONFIG_FILEPATH): 100 | try: 101 | with open(self.__CONFIG_FILEPATH, 'rb') as f: 102 | new_configuration.update(pickle.load(f)) 103 | bcPrint('Configuration file loaded.') 104 | except: 105 | bcPrint("[IO] can not read: {}".format(self.__CONFIG_FILEPATH), 106 | 'error') 107 | 108 | return new_configuration 109 | 110 | 111 | Configuration = __Configuration() 112 | -------------------------------------------------------------------------------- /io_bcry_exporter/udp.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # Name: udp.py 3 | # Purpose: Holds UDP and IK property functions. 4 | # 5 | # Author: Özkan Afacan 6 | # 7 | # Created: 17/09/2016 8 | # Copyright: (c) Özkan Afacan 2016 9 | # License: GPLv2+ 10 | #------------------------------------------------------------------------------ 11 | 12 | # 13 | 14 | 15 | import math 16 | import re 17 | 18 | import bpy 19 | import bpy.ops 20 | import bpy_extras 21 | from bpy.props import * 22 | from bpy_extras.io_utils import ExportHelper 23 | 24 | #------------------------------------------------------------------------------ 25 | # User Defined Properties: 26 | #------------------------------------------------------------------------------ 27 | 28 | def get_udp(object_, udp_name, udp_value, is_checked=None): 29 | '''Get User Defined Property -- Overloaded function that have two variation''' 30 | 31 | if is_checked is None: 32 | try: 33 | temp_value = object_[udp_name] 34 | udp_value = True 35 | except: 36 | udp_value = False 37 | 38 | return udp_value 39 | 40 | else: 41 | try: 42 | udp_value = object_[udp_name] 43 | is_checked = True 44 | except: 45 | is_checked = False 46 | 47 | return udp_value, is_checked 48 | 49 | 50 | def edit_udp(object_, udp_name, udp_value, is_checked=True): 51 | '''Edit User Defined Property''' 52 | 53 | if is_checked: 54 | object_[udp_name] = udp_value 55 | else: 56 | try: 57 | del object_[udp_name] 58 | except: 59 | pass 60 | 61 | 62 | def is_user_defined_property(property_name): 63 | prop_list = [ 64 | "phys_proxy", 65 | "colltype_player", 66 | "no_explosion_occlusion", 67 | "entity", 68 | "mass", 69 | "density", 70 | "pieces", 71 | "dynamic", 72 | "no_hit_refinement", 73 | "limit", 74 | "bend", 75 | "twist", 76 | "pull", 77 | "push", 78 | "shift", 79 | "player_can_break", 80 | "gameplay_critical", 81 | "constraint_limit", 82 | "constraint_minang", 83 | "consrtaint_maxang", 84 | "constraint_damping", 85 | "constraint_collides", 86 | "stiffness", 87 | "hardness", 88 | "max_stretch", 89 | "max_impulse", 90 | "skin_dist", 91 | "thickness", 92 | "explosion_scale", 93 | "notaprim", 94 | "hull", 95 | "wheel"] 96 | 97 | return property_name in prop_list 98 | 99 | 100 | #------------------------------------------------------------------------------ 101 | # Bone Inverse Kinematics: 102 | #------------------------------------------------------------------------------ 103 | 104 | def get_bone_ik_max_min(pose_bone): 105 | xIK = yIK = zIK = "" 106 | 107 | if pose_bone.lock_ik_x: 108 | xIK = '_xmax={!s}'.format(0.0) + '_xmin={!s}'.format(0.0) 109 | else: 110 | xIK = '_xmax={!s}'.format(math.degrees(-pose_bone.ik_min_y)) \ 111 | + '_xmin={!s}'.format(math.degrees(-pose_bone.ik_max_y)) 112 | 113 | if pose_bone.lock_ik_y: 114 | yIK = '_ymax={!s}'.format(0.0) + '_ymin={!s}'.format(0.0) 115 | else: 116 | yIK = '_ymax={!s}'.format(math.degrees(-pose_bone.ik_min_x)) \ 117 | + '_ymin={!s}'.format(math.degrees(-pose_bone.ik_max_x)) 118 | 119 | if pose_bone.lock_ik_z: 120 | zIK = '_zmax={!s}'.format(0.0) + '_zmin={!s}'.format(0.0) 121 | else: 122 | zIK = '_zmax={!s}'.format(math.degrees(pose_bone.ik_max_z)) \ 123 | + '_zmin={!s}'.format(math.degrees(pose_bone.ik_min_z)) 124 | 125 | return xIK, yIK, zIK 126 | 127 | 128 | def get_bone_ik_properties(pose_bone): 129 | damping = [1.0, 1.0, 1.0] 130 | spring = [0.0, 0.0, 0.0] 131 | spring_tension = [1.0, 1.0, 1.0] 132 | 133 | try: 134 | damping = pose_bone['Damping'] 135 | except: 136 | pass 137 | 138 | try: 139 | spring = pose_bone['Spring'] 140 | except: 141 | pass 142 | 143 | try: 144 | spring_tension = pose_bone['Spring Tension'] 145 | except: 146 | pass 147 | 148 | return damping, spring, spring_tension 149 | -------------------------------------------------------------------------------- /io_bcry_exporter/desc.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # Name: desc.py 3 | # Purpose: Holds descriptions of UDP and IK properties. 4 | # 5 | # Author: Özkan Afacan 6 | # 7 | # Created: 07/02/2015 8 | # Copyright: (c) Özkan Afacan 2015 9 | # License: GPLv2+ 10 | # ------------------------------------------------------------------------------ 11 | 12 | # 13 | 14 | list = {} 15 | 16 | # ------------------------------------------------------------------------------ 17 | # Material Physics: 18 | # ------------------------------------------------------------------------------ 19 | 20 | list['physDefault'] = """The render geometry is used as physics proxy. This\ 21 | is expensive for complex objects, so use this only for simple objects\ 22 | like cubes or if you really need to fully physicalize an object.""" 23 | 24 | list['physProxyNoDraw'] = """Mesh is used exclusively for collision\ 25 | detection and is not rendered.""" 26 | 27 | list['physNoCollide'] = """Special purpose proxy which is used by the engine\ 28 | to detect player interaction (e.g. for vegetation touch bending).""" 29 | 30 | list['physObstruct'] = """Used for Soft Cover to block AI view\ 31 | (i.e. on dense foliage).""" 32 | 33 | list['physNone'] = """The render geometry have no physic just render it.""" 34 | 35 | # ------------------------------------------------------------------------------ 36 | # Inverse Kinematics: 37 | # ------------------------------------------------------------------------------ 38 | 39 | list['spring'] = """Stiffness of an angular spring at a joint can be adjusted\ 40 | via the 'Spring Tension' parameter. A value of 1 means acceleration\ 41 | of 1 radian/second2 (1 radian = 57°).""" 42 | 43 | list['damping'] = """The 'dampening' value in the IK Limit options will\ 44 | effect how loose the joint will be in the rag doll simulation of\ 45 | the dead body. Most times you will want the dampening value set at 1,0.""" 46 | 47 | # ------------------------------------------------------------------------------ 48 | # Physics Proxy: 49 | # ------------------------------------------------------------------------------ 50 | 51 | list['notaprim'] = """Force the engine NOT to convert this proxy to a\ 52 | primitive (for example if the proxy is already naturally box-shaped).""" 53 | 54 | list['no_exp_occlusion'] = """Will allow the force/damage of an explosion to\ 55 | penetrate through the phys proxy.""" 56 | 57 | list['colltpye_player'] = """If a phys proxy node has this string, then:\ 58 | 1 - This node will only receive player collisions, but no hit\ 59 | impacts. 2- If this object contains other phys proxy nodes,\ 60 | then those other nodes will not receive player collisions.""" 61 | 62 | # ------------------------------------------------------------------------------ 63 | # Render Mesh: 64 | # ------------------------------------------------------------------------------ 65 | 66 | list['is_entity'] = """If the render geometry properties include 'entity', the\ 67 | object will not fade out after being disconnected from the main object.""" 68 | 69 | list['mass'] = """Mass defines the weight of an object based on real world\ 70 | physics in kg. mass=0 sets the object to 'unmovable'.""" 71 | 72 | list['density'] = """The engine automatically calculates the mass for an\ 73 | object based on the density and the bounding box of an object.\ 74 | Can be used alternatively to mass.""" 75 | 76 | list['pieces'] = """Instead of disconnecting the piece when the joint is\ 77 | broken, it will instantly disappear spawning a particle effect\ 78 | depending on the surfacetype of the proxy.""" 79 | 80 | list['is_dynamic'] = """This is a special-case string for dynamically\ 81 | breakable meshes (i.e. glass) – this string flags the object as\ 82 | 'dynamically breakable'. However this string is not required\ 83 | on Glass, Trees, or Cloth, as these are already flagged\ 84 | automatically by the engine (through surface-type system).""" 85 | 86 | list['no_hit_refinement'] = """If the render geometry properties include\ 87 | 'entity', the object will not fade out after being disconnected\ 88 | from the main object.""" 89 | 90 | list['other_rendermesh'] = """(Mostly obsolete now) - This would be required\ 91 | if the phys proxy is a sibling of the rendermesh. Proxies should\ 92 | always be children of the rendermesh however, in which case\ 93 | other_rendermesh is not required.""" 94 | 95 | # ------------------------------------------------------------------------------ 96 | # Joint Node: 97 | # ------------------------------------------------------------------------------ 98 | 99 | list['limit'] = """Limit is a general value for several different kind of\ 100 | forces applied to the joint. It contains a combination of\ 101 | the values below.""" 102 | 103 | list['bend'] = """Maximum torque around an axis perpendicular to the normal.""" 104 | 105 | list['twist'] = """Maximum torque around the normal.""" 106 | 107 | list['pull'] = """Maximum force applied to the joint's 1st object against\ 108 | the joint normal (the parts are 'pulled together' as a reaction\ 109 | to external forces pulling them apart).""" 110 | 111 | list['push'] = """Maximum force applied to the joint's 1st object along\ 112 | the joint normal; joint normal is the joint's z axis, so for this\ 113 | value to actually be 'push apart', this axis must be directed\ 114 | inside the 1st object.""" 115 | 116 | list['shift'] = """Maximum force in the direction perpendicular to normal.""" 117 | 118 | list['player_can_break'] = """Joints in the entire breakable entity can be\ 119 | broken by the player bumping into them.""" 120 | 121 | list['gameplay_critical'] = """Joints in the entire entity will break, even\ 122 | if jointed breaking is disabled overall.""" 123 | 124 | # ------------------------------------------------------------------------------ 125 | # Deformable: 126 | # ------------------------------------------------------------------------------ 127 | 128 | list['stiffness'] = """Resilience to bending and shearing (default 10).""" 129 | 130 | list['hardness'] = """Resilience to stretching (default 10).""" 131 | 132 | list['max_stretch'] = """If any edge is stretched more than that, it's length\ 133 | is re-enforced. max_stretch = 0.3 means stretched to 130% of\ 134 | its original length.""" 135 | 136 | list['max_impulse'] = """Upper limit on all applied impulses. Default\ 137 | skeleton's mass*100.""" 138 | 139 | list['skin_dist'] = """Sphere radius in skinning assignment. Default is\ 140 | the minimum of the main mesh's bounding box's dimensions.""" 141 | 142 | list['thickness'] = """Sets the collision thickness for the skeleton.\ 143 | Setting thickness to 0 disables all collisions.""" 144 | 145 | list['explosion_scale'] = """Used to scale down the effect of explosions on\ 146 | the deformable. This lets you have visible deformations from bullet\ 147 | impacts, but without vastly distorting the object too far with explosions.""" 148 | 149 | list['notaprim'] = """A general physics proxy parameter, it keeps the physics\ 150 | mesh from being turned into a primitive (box, cylinder). This is\ 151 | especially important for deformable objects - the skeleton being\ 152 | a primitive will cause a crash!""" 153 | 154 | # ------------------------------------------------------------------------------ 155 | # Animation Range Types: 156 | # ------------------------------------------------------------------------------ 157 | 158 | list['range_timeline'] = """Animation range is set from Timeline Editor.\ 159 | You may directly change start and end frame from Timeline Editor. This is\ 160 | best choice for single animation per file.""" 161 | 162 | list['range_values'] = """Animation range is stored in custom properties\ 163 | values. You must enter animation range values. You can change start or end 164 | frame from object custom properties. This is ideal for multiple animations\ 165 | in a blender project.""" 166 | 167 | list['range_markers'] = """Animation range is on stored animation markers.\ 168 | Markers can directly be set on Timeline Editor to change frame range.""" 169 | 170 | # ------------------------------------------------------------------------------ 171 | # Locator Locomotion: 172 | # ------------------------------------------------------------------------------ 173 | 174 | list['locator_length'] = """The Locator Locomotion bone length to represented\ 175 | in 3D view.""" 176 | 177 | list['locator_root'] = """Skeleton Root Bone: The Locator Locomotion bone\ 178 | is going to be linked/parented to that bone.""" 179 | 180 | list['locator_move'] = """Movement Reference Bone: The Locator Locomotion\ 181 | use that bone to copy movements from selected axis.""" 182 | 183 | # ------------------------------------------------------------------------------ 184 | # Export: 185 | # ------------------------------------------------------------------------------ 186 | 187 | list['merge_all_nodes'] = """Compiles all the geometry from the different\ 188 | nodes into a single node which improves the efficiency. It's supported only\ 189 | for non-skinned geometry. For more information on Merge All Nodes,\ 190 | please refer http://docs.cryengine.com/display/CEMANUAL/Merge+All+Nodes""" 191 | -------------------------------------------------------------------------------- /io_bcry_exporter/export_materials.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # Name: export_materials.py 3 | # Purpose: Material exporter to CryEngine. 4 | # 5 | # Author: Özkan Afacan, 6 | # Angelo J. Miner, Mikołaj Milej, Daniel White, 7 | # Oscar Martin Garcia, Duo Oratar, David Marcelis 8 | # 9 | # Created: 30/09/2016 10 | # Copyright: (c) Özkan Afacan 2016 11 | # License: GPLv2+ 12 | # ------------------------------------------------------------------------------ 13 | 14 | if "bpy" in locals(): 15 | import importlib 16 | importlib.reload(utils) 17 | importlib.reload(material_utils) 18 | else: 19 | import bpy 20 | from . import utils, material_utils 21 | 22 | import copy 23 | import os 24 | import subprocess 25 | import threading 26 | import xml.dom.minidom 27 | from collections import OrderedDict 28 | from xml.dom.minidom import Document, Element, parse, parseString 29 | 30 | from bpy_extras.io_utils import ExportHelper 31 | 32 | from .outpipe import bcPrint 33 | 34 | 35 | class CrytekMaterialExporter: 36 | 37 | def __init__(self, config): 38 | self._config = config 39 | self._doc = Document() 40 | self._materials = material_utils.get_materials( 41 | config.export_selected_nodes) 42 | 43 | def generate_materials(self): 44 | material_utils.generate_mtl_files(self._config, self._materials) 45 | 46 | def get_materials_for_object(self, object_): 47 | materials = OrderedDict() 48 | for material_name, material in self._materials.items(): 49 | for object_material in object_.data.materials: 50 | if material.name == object_material.name: 51 | materials[material] = material_name 52 | 53 | return materials 54 | 55 | 56 | # ------------------------------------------------------------------------------ 57 | # Library Images: 58 | # ------------------------------------------------------------------------------ 59 | 60 | def export_library_images(self, library_images): 61 | images = [] 62 | for node in utils.get_export_nodes(): 63 | for material in self._materials: 64 | for image in material_utils.get_textures(material): 65 | if image: 66 | images.append(image) 67 | 68 | self._write_texture_nodes(list(set(images)), library_images) 69 | 70 | def _write_texture_nodes(self, images, library_images): 71 | for image in images: 72 | image_node = self._doc.createElement('image') 73 | image_node.setAttribute("id", image.name) 74 | image_node.setAttribute("name", image.name) 75 | init_form = self._doc.createElement('init_from') 76 | path = material_utils.get_image_path_for_game( 77 | image, self._config.game_dir) 78 | path_node = self._doc.createTextNode(path) 79 | init_form.appendChild(path_node) 80 | image_node.appendChild(init_form) 81 | library_images.appendChild(image_node) 82 | 83 | if self._config.convert_textures: 84 | material_utils.convert_image_to_dds(images, self._config) 85 | 86 | # ------------------------------------------------------------------------------ 87 | # Library Effects: 88 | # ------------------------------------------------------------------------------ 89 | 90 | def export_library_effects(self, library_effects): 91 | for material_name, material in self._materials.items(): 92 | self._export_library_effects_material( 93 | material, material_name, library_effects) 94 | 95 | def _export_library_effects_material( 96 | self, material, material_name, library_effects): 97 | 98 | images = material_utils.get_textures(material) 99 | 100 | effect_node = self._doc.createElement("effect") 101 | effect_node.setAttribute("id", "{}_fx".format(material_name)) 102 | profile_node = self._doc.createElement("profile_COMMON") 103 | self._write_surface_and_sampler(images, profile_node) 104 | 105 | technique_common = self._doc.createElement("technique") 106 | technique_common.setAttribute("sid", "common") 107 | 108 | self._write_phong_node(material, images, technique_common) 109 | profile_node.appendChild(technique_common) 110 | 111 | extra = self._create_double_sided_extra("GOOGLEEARTH") 112 | profile_node.appendChild(extra) 113 | effect_node.appendChild(profile_node) 114 | 115 | extra = self._create_double_sided_extra("MAX3D") 116 | effect_node.appendChild(extra) 117 | library_effects.appendChild(effect_node) 118 | 119 | def _write_surface_and_sampler(self, images, profile_node): 120 | for image in images: 121 | if image is None: 122 | continue 123 | 124 | surface = self._doc.createElement("newparam") 125 | surface.setAttribute("sid", "{}-surface".format(image.name)) 126 | surface_node = self._doc.createElement("surface") 127 | surface_node.setAttribute("type", "2D") 128 | init_from_node = self._doc.createElement("init_from") 129 | temp_node = self._doc.createTextNode(image.name) 130 | init_from_node.appendChild(temp_node) 131 | surface_node.appendChild(init_from_node) 132 | surface.appendChild(surface_node) 133 | sampler = self._doc.createElement("newparam") 134 | sampler.setAttribute("sid", "{}-sampler".format(image.name)) 135 | sampler_node = self._doc.createElement("sampler2D") 136 | source_node = self._doc.createElement("source") 137 | temp_node = self._doc.createTextNode( 138 | "{}-surface".format(image.name)) 139 | source_node.appendChild(temp_node) 140 | sampler_node.appendChild(source_node) 141 | sampler.appendChild(sampler_node) 142 | 143 | profile_node.appendChild(surface) 144 | profile_node.appendChild(sampler) 145 | 146 | def _write_phong_node(self, material, images, parent_node): 147 | phong = self._doc.createElement("phong") 148 | 149 | emission = self._create_color_node(material, "emission") 150 | ambient = self._create_color_node(material, "ambient") 151 | 152 | if images[0]: 153 | diffuse = self._create_texture_node(images[0].name, "diffuse") 154 | else: 155 | diffuse = self._create_color_node(material, "diffuse") 156 | 157 | if images[1]: 158 | specular = self._create_texture_node(images[1].name, "specular") 159 | else: 160 | specular = self._create_color_node(material, "specular") 161 | 162 | shininess = self._create_attribute_node(material, "shininess") 163 | index_refraction = self._create_attribute_node( 164 | material, "index_refraction") 165 | 166 | phong.appendChild(emission) 167 | phong.appendChild(ambient) 168 | phong.appendChild(diffuse) 169 | phong.appendChild(specular) 170 | phong.appendChild(shininess) 171 | phong.appendChild(index_refraction) 172 | 173 | if images[2]: 174 | normal = self._create_texture_node(images[2].name, "normal") 175 | phong.appendChild(normal) 176 | 177 | parent_node.appendChild(phong) 178 | 179 | def _create_color_node(self, material, type_): 180 | node = self._doc.createElement(type_) 181 | color = self._doc.createElement("color") 182 | color.setAttribute("sid", type_) 183 | col = material_utils.get_material_color(material, type_) 184 | color_text = self._doc.createTextNode(str(col)) 185 | color.appendChild(color_text) 186 | node.appendChild(color) 187 | 188 | return node 189 | 190 | def _create_texture_node(self, image_name, type_): 191 | node = self._doc.createElement(type_) 192 | texture = self._doc.createElement("texture") 193 | texture.setAttribute("texture", "{}-sampler".format(image_name)) 194 | node.appendChild(texture) 195 | 196 | return node 197 | 198 | def _create_attribute_node(self, material, type_): 199 | node = self._doc.createElement(type_) 200 | float = self._doc.createElement("float") 201 | float.setAttribute("sid", type_) 202 | val = material_utils.get_material_attribute(material, type_) 203 | value = self._doc.createTextNode(val) 204 | float.appendChild(value) 205 | node.appendChild(float) 206 | 207 | return node 208 | 209 | def _create_double_sided_extra(self, profile): 210 | extra = self._doc.createElement("extra") 211 | technique = self._doc.createElement("technique") 212 | technique.setAttribute("profile", profile) 213 | double_sided = self._doc.createElement("double_sided") 214 | double_sided_value = self._doc.createTextNode("1") 215 | double_sided.appendChild(double_sided_value) 216 | technique.appendChild(double_sided) 217 | extra.appendChild(technique) 218 | 219 | return extra 220 | 221 | # ------------------------------------------------------------------------------ 222 | # Library Materials: 223 | # ------------------------------------------------------------------------------ 224 | 225 | def export_library_materials(self, library_materials): 226 | for material_name, material in self._materials.items(): 227 | material_element = self._doc.createElement('material') 228 | material_element.setAttribute('id', material_name) 229 | instance_effect = self._doc.createElement('instance_effect') 230 | instance_effect.setAttribute('url', '#{}_fx'.format(material_name)) 231 | material_element.appendChild(instance_effect) 232 | library_materials.appendChild(material_element) 233 | -------------------------------------------------------------------------------- /io_bcry_exporter/rc.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # Name: rc.py 3 | # Purpose: Resource compiler transactions 4 | # 5 | # Author: Daniel White, 6 | # Angelo J. Miner, Mikołaj Milej, Özkan Afacan, 7 | # Oscar Martin Garcia, Duo Oratar, David Marcelis 8 | # 9 | # Created: 2/12/2016 10 | # Copyright: (c) Daniel White 2016 11 | # Licence: GPLv2+ 12 | # ------------------------------------------------------------------------------ 13 | 14 | # 15 | 16 | 17 | if "bpy" in locals(): 18 | import importlib 19 | importlib.reload(utils) 20 | else: 21 | import bpy 22 | from . import utils 23 | 24 | import fnmatch 25 | import os 26 | import shutil 27 | import subprocess 28 | import tempfile 29 | import threading 30 | 31 | from .outpipe import bcPrint 32 | 33 | 34 | class RCInstance: 35 | 36 | def __init__(self, config): 37 | self.__config = config 38 | 39 | def convert_tif(self, source): 40 | converter = _TIFConverter(self.__config, source) 41 | conversion_thread = threading.Thread(target=converter) 42 | conversion_thread.start() 43 | 44 | def convert_dae(self, source): 45 | converter = _DAEConverter(self.__config, source) 46 | conversion_thread = threading.Thread(target=converter) 47 | conversion_thread.start() 48 | 49 | 50 | class _DAEConverter: 51 | 52 | def __init__(self, config, source): 53 | self.__config = config 54 | self.__doc = source 55 | 56 | def __call__(self): 57 | filepath = bpy.path.ensure_ext(self.__config.filepath, ".dae") 58 | utils.generate_xml(filepath, self.__doc, overwrite=True) 59 | 60 | dae_path = utils.get_absolute_path_for_rc(filepath) 61 | 62 | if not self.__config.disable_rc: 63 | rc_params = ["/verbose", "/threads=processors", "/refresh"] 64 | if self.__config.vcloth_pre_process: 65 | rc_params.append("/wait=0 /forceVCloth") 66 | 67 | rc_process = run_rc(self.__config.rc_path, dae_path, rc_params) 68 | 69 | if rc_process is not None: 70 | rc_process.wait() 71 | 72 | if not self.__config.is_animation_process: 73 | self.__recompile(dae_path) 74 | else: 75 | self.__rename_anm_files(dae_path) 76 | 77 | if self.__config.make_layer: 78 | lyr_contents = self.__make_layer() 79 | lyr_path = os.path.splitext(filepath)[0] + ".lyr" 80 | utils.generate_file(lyr_path, lyr_contents) 81 | 82 | if not self.__config.save_dae: 83 | rcdone_path = "{}.rcdone".format(dae_path) 84 | utils.remove_file(dae_path) 85 | utils.remove_file(rcdone_path) 86 | 87 | def __recompile(self, dae_path): 88 | name = os.path.basename(dae_path) 89 | output_path = os.path.dirname(dae_path) 90 | ALLOWED_NODE_TYPES = ("chr", "skin") 91 | for group in utils.get_export_nodes(): 92 | node_type = utils.get_node_type(group) 93 | if node_type in ALLOWED_NODE_TYPES: 94 | out_file = os.path.join(output_path, group.name) 95 | args = [ 96 | self.__config.rc_path, 97 | "/refresh", 98 | "/vertexindexformat=u16", 99 | out_file] 100 | rc_second_pass = subprocess.Popen(args) 101 | elif node_type == 'i_caf': 102 | try: 103 | os.remove(os.path.join(output_path, ".animsettings")) 104 | os.remove(os.path.join(output_path, ".caf")) 105 | os.remove(os.path.join(output_path, ".$animsettings")) 106 | except: 107 | pass 108 | 109 | def __rename_anm_files(self, dae_path): 110 | output_path = os.path.dirname(dae_path) 111 | 112 | for collection in utils.get_export_nodes(): 113 | if utils.get_node_type(collection) == 'anm': 114 | node_name = utils.get_node_name(collection) 115 | src_name = "{}_{}".format(node_name, collection.name) 116 | src_name = os.path.join(output_path, src_name) 117 | src_cryasset_name = "{}_{}".format(node_name, collection.name + ".cryasset") 118 | src_cryasset_name = os.path.join(output_path, src_cryasset_name) 119 | 120 | if os.path.exists(src_name): 121 | dest_name = utils.get_geometry_animation_file_name(collection) 122 | dest_name = os.path.join(output_path, dest_name) 123 | dest_cryasset_name = utils.get_cryasset_animation_file_name(collection) 124 | dest_cryasset_name = os.path.join(output_path, dest_cryasset_name) 125 | 126 | if os.path.exists(dest_name): 127 | os.remove(dest_name) 128 | os.remove(dest_cryasset_name) 129 | 130 | os.rename(src_name, dest_name) 131 | os.rename(src_cryasset_name, dest_cryasset_name) 132 | 133 | def __get_mtl_files_in_directory(self, directory): 134 | MTL_MATCH_STRING = "*.{!s}".format("mtl") 135 | 136 | mtl_files = [] 137 | for file in os.listdir(directory): 138 | if fnmatch.fnmatch(file, MTL_MATCH_STRING): 139 | filepath = "{!s}/{!s}".format(directory, file) 140 | mtl_files.append(filepath) 141 | 142 | return mtl_files 143 | 144 | def __make_layer(self): 145 | layer_doc = Document() 146 | object_layer = layer_doc.createElement("ObjectLayer") 147 | layer_name = "ExportedLayer" 148 | layer = createAttributes( 149 | 'Layer', 150 | {'name': layer_name, 151 | 'GUID': utils.get_guid(), 152 | 'FullName': layer_name, 153 | 'External': '0', 154 | 'Exportable': '1', 155 | 'ExportLayerPak': '1', 156 | 'DefaultLoaded': '0', 157 | 'HavePhysics': '1', 158 | 'Expanded': '0', 159 | 'IsDefaultColor': '1' 160 | } 161 | ) 162 | 163 | layer_objects = layer_doc.createElement("LayerObjects") 164 | for group in utils.get_export_nodes(): 165 | if len(group.objects) > 1: 166 | origin = 0, 0, 0 167 | rotation = 1, 0, 0, 0 168 | else: 169 | origin = group.objects[0].location 170 | rotation = group.objects[0].delta_rotation_quaternion 171 | 172 | object = createAttributes( 173 | 'Object', 174 | {'name': group.name[14:], 175 | 'Type': 'Entity', 176 | 'Id': utils.get_guid(), 177 | 'LayerGUID': layer.getAttribute('GUID'), 178 | 'Layer': layer_name, 179 | 'Pos': "{}, {}, {}".format(origin[:]), 180 | 'Rotate': "{}, {}, {}, {}".format(rotation[:]), 181 | 'EntityClass': 'BasicEntity', 182 | 'FloorNumber': '-1', 183 | 'RenderNearest': '0', 184 | 'NoStaticDecals': '0', 185 | 'CreatedThroughPool': '0', 186 | 'MatLayersMask': '0', 187 | 'OutdoorOnly': '0', 188 | 'CastShadow': '1', 189 | 'MotionBlurMultiplier': '1', 190 | 'LodRatio': '100', 191 | 'ViewDistRatio': '100', 192 | 'HiddenInGame': '0', 193 | } 194 | ) 195 | properties = createAttributes( 196 | 'Properties', 197 | {'object_Model': '/Objects/{}.cgf'.format(group.name[14:]), 198 | 'bCanTriggerAreas': '0', 199 | 'bExcludeCover': '0', 200 | 'DmgFactorWhenCollidingAI': '1', 201 | 'esFaction': '', 202 | 'bHeavyObject': '0', 203 | 'bInteractLargeObject': '0', 204 | 'bMissionCritical': '0', 205 | 'bPickable': '0', 206 | 'soclasses_SmartObjectClass': '', 207 | 'bUsable': '0', 208 | 'UseMessage': '0', 209 | } 210 | ) 211 | health = createAttributes( 212 | 'Health', 213 | {'bInvulnerable': '1', 214 | 'MaxHealth': '500', 215 | 'bOnlyEnemyFire': '1', 216 | } 217 | ) 218 | interest = createAttributes( 219 | 'Interest', 220 | {'soaction_Action': '', 221 | 'bInteresting': '0', 222 | 'InterestLevel': '1', 223 | 'Pause': '15', 224 | 'Radius': '20', 225 | 'bShared': '0', 226 | } 227 | ) 228 | vOffset = createAttributes( 229 | 'vOffset', 230 | {'x': '0', 231 | 'y': '0', 232 | 'z': '0', 233 | } 234 | ) 235 | 236 | interest.appendChild(vOffset) 237 | properties.appendChild(health) 238 | properties.appendChild(interest) 239 | object.appendChild(properties) 240 | layer_objects.appendChild(object) 241 | 242 | layer.appendChild(layer_objects) 243 | object_layer.appendChild(layer) 244 | layer_doc.appendChild(object_layer) 245 | 246 | return layer_doc.toprettyxml(indent=" ") 247 | 248 | def __createAttributes(self, node_name, attributes): 249 | doc = Document() 250 | node = doc.createElement(node_name) 251 | for name, value in attributes.items(): 252 | node.setAttribute(name, value) 253 | 254 | return node 255 | 256 | 257 | class _TIFConverter: 258 | 259 | def __init__(self, config, source): 260 | self.__config = config 261 | self.__images_to_convert = source 262 | self.__tmp_images = {} 263 | self.__tmp_dir = tempfile.mkdtemp("CryBlend") 264 | 265 | def __call__(self): 266 | for image in self.__images_to_convert: 267 | rc_params = self.__get_rc_params(image.filepath) 268 | tiff_image_path = self.__get_temp_tiff_image_path(image) 269 | 270 | tiff_image_for_rc = utils.get_absolute_path_for_rc(tiff_image_path) 271 | bcPrint(tiff_image_for_rc) 272 | 273 | try: 274 | self.__create_normal_texture(image) 275 | except: 276 | bcPrint("Failed to invert green channel") 277 | 278 | rc_process = run_rc(self.__config.texture_rc_path, 279 | tiff_image_for_rc, 280 | rc_params) 281 | 282 | # re-save the original image after running the RC to 283 | # prevent the original one from getting lost 284 | try: 285 | if ("_ddn" in image.name): 286 | image.save() 287 | except: 288 | bcPrint("Failed to invert green channel") 289 | 290 | rc_process.wait() 291 | 292 | if self.__config.texture_rc_path: 293 | self.__save_tiffs() 294 | 295 | self.__remove_tmp_files() 296 | 297 | def __create_normal_texture(self, image): 298 | if ("_ddn" in image.name): 299 | # make a copy to prevent editing the original image 300 | temp_normal_image = image.copy() 301 | self.__invert_green_channel(temp_normal_image) 302 | # save to file and delete the temporary image 303 | new_normal_image_path = "{}_cb_normal.{}".format(os.path.splitext( 304 | temp_normal_image.filepath_raw)[0], 305 | os.path.splitext( 306 | temp_normal_image.filepath_raw)[1]) 307 | temp_normal_image.save_render(filepath=new_normal_image_path) 308 | bpy.data.images.remove(temp_normal_image) 309 | 310 | def __get_rc_params(self, destination_path): 311 | rc_params = ["/verbose", "/threads=cores", "/userdialog=1", "/refresh"] 312 | 313 | image_directory = os.path.dirname(utils.get_absolute_path_for_rc( 314 | destination_path)) 315 | 316 | rc_params.append("/targetroot={!s}".format(image_directory)) 317 | 318 | return rc_params 319 | 320 | def __invert_green_channel(self, image): 321 | override = {'edit_image': bpy.data.images[image.name]} 322 | bpy.ops.image.invert(override, invert_g=True) 323 | image.update() 324 | 325 | def __get_temp_tiff_image_path(self, image): 326 | # check if the image already is a .tif 327 | image_extension = utils.get_extension_from_path(image.filepath) 328 | bcPrint(image_extension) 329 | 330 | if ".tif" == image_extension: 331 | bcPrint( 332 | "Image {!r} is already a tif, not converting".format( 333 | image.name), 'debug') 334 | return image.filepath 335 | 336 | tiff_image_path = utils.get_path_with_new_extension(image.filepath, 337 | "tif") 338 | tiff_image_absolute_path = utils.get_absolute_path(tiff_image_path) 339 | tiff_file_name = os.path.basename(tiff_image_path) 340 | 341 | tmp_file_path = os.path.join(self.__tmp_dir, tiff_file_name) 342 | 343 | if tiff_image_path != image.filepath: 344 | self.__save_as_tiff(image, tmp_file_path) 345 | self.__tmp_images[tmp_file_path] = (tiff_image_absolute_path) 346 | 347 | return tmp_file_path 348 | 349 | def __save_as_tiff(self, image, tiff_file_path): 350 | originalPath = image.filepath 351 | 352 | try: 353 | image.filepath_raw = tiff_file_path 354 | image.file_format = 'TIFF' 355 | image.save() 356 | 357 | finally: 358 | image.filepath = originalPath 359 | 360 | def __save_tiffs(self): 361 | for tmp_image, dest_image in self.__tmp_images.items(): 362 | bcPrint("Moving tmp image: {!r} to {!r}".format(tmp_image, 363 | dest_image), 364 | 'debug') 365 | shutil.move(tmp_image, dest_image) 366 | 367 | def __remove_tmp_files(self): 368 | for tmp_image in self.__tmp_images: 369 | try: 370 | bcPrint("Removing tmp image: {!r}".format(tmp_image), 'debug') 371 | os.remove(tmp_image) 372 | except FileNotFoundError: 373 | pass 374 | 375 | os.removedirs(self.__tmp_dir) 376 | self.__tmp_images.clear() 377 | 378 | 379 | def run_rc(rc_path, files_to_process, params=None): 380 | bcPrint("RC Path: {}".format(os.path.abspath(rc_path)), newline=True) 381 | process_params = [rc_path] 382 | 383 | if isinstance(files_to_process, list): 384 | process_params.extend(files_to_process) 385 | else: 386 | process_params.append(files_to_process) 387 | 388 | process_params.extend(params) 389 | 390 | bcPrint("RC Parameters: {}".format(params)) 391 | bcPrint("Processing File: {}".format(files_to_process)) 392 | 393 | try: 394 | run_object = subprocess.Popen(process_params) 395 | except: 396 | raise exceptions.NoRcSelectedException 397 | 398 | print() 399 | return run_object 400 | -------------------------------------------------------------------------------- /io_bcry_exporter/export_animations.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # Name: export_animations.py 3 | # Purpose: Animation exporter to CryEngine 4 | # 5 | # Author: Özkan Afacan, 6 | # Angelo J. Miner, Mikołaj Milej, Daniel White, 7 | # Oscar Martin Garcia, Duo Oratar, David Marcelis 8 | # 9 | # Created: 13/06/2016 10 | # Copyright: (c) Özkan Afacan 2016 11 | # License: GPLv2+ 12 | # ------------------------------------------------------------------------------ 13 | 14 | 15 | if "bpy" in locals(): 16 | import importlib 17 | importlib.reload(utils) 18 | importlib.reload(exceptions) 19 | else: 20 | import bpy 21 | from . import export, utils, exceptions 22 | 23 | import os 24 | import xml.dom.minidom 25 | from xml.dom.minidom import Document, Element, parse, parseString 26 | 27 | from .outpipe import bcPrint 28 | from .rc import RCInstance 29 | 30 | AXES = { 31 | 'X': 0, 32 | 'Y': 1, 33 | 'Z': 2, 34 | } 35 | 36 | 37 | class CrytekDaeAnimationExporter(export.CrytekDaeExporter): 38 | 39 | def __init__(self, config): 40 | self._config = config 41 | self._doc = Document() 42 | 43 | def export(self): 44 | self._prepare_for_export() 45 | 46 | root_element = self._doc.createElement('collada') 47 | root_element.setAttribute( 48 | "xmlns", "http://www.collada.org/2005/11/COLLADASchema") 49 | root_element.setAttribute("version", "1.4.1") 50 | self._doc.appendChild(root_element) 51 | self._create_file_header(root_element) 52 | 53 | libanmcl = self._doc.createElement("library_animation_clips") 54 | libanm = self._doc.createElement("library_animations") 55 | root_element.appendChild(libanmcl) 56 | root_element.appendChild(libanm) 57 | 58 | lib_visual_scene = self._doc.createElement("library_visual_scenes") 59 | visual_scene = self._doc.createElement("visual_scene") 60 | visual_scene.setAttribute("id", "scene") 61 | visual_scene.setAttribute("name", "scene") 62 | lib_visual_scene.appendChild(visual_scene) 63 | root_element.appendChild(lib_visual_scene) 64 | 65 | initial_frame_active = bpy.context.scene.frame_current 66 | initial_frame_start = bpy.context.scene.frame_start 67 | initial_frame_end = bpy.context.scene.frame_end 68 | 69 | ALLOWED_NODE_TYPES = ("i_caf", "anm") 70 | for group in utils.get_animation_export_nodes(): 71 | 72 | node_type = utils.get_node_type(group) 73 | node_name = utils.get_node_name(group) 74 | 75 | if node_type in ALLOWED_NODE_TYPES: 76 | object_ = None 77 | layers = None 78 | 79 | if node_type == 'i_caf': 80 | object_ = utils.get_armature_from_node(group) 81 | layers = utils.activate_all_bone_layers(object_) 82 | elif node_type == 'anm': 83 | object_ = group.objects[0] 84 | 85 | frame_start, frame_end = utils.get_animation_node_range( 86 | object_, node_name, initial_frame_start, initial_frame_end) 87 | bpy.context.scene.frame_start = frame_start 88 | bpy.context.scene.frame_end = frame_end 89 | 90 | print('') 91 | bcPrint(group.name) 92 | bcPrint("Animation is being preparing to process.") 93 | bcPrint("Animation frame range are [{} - {}]".format( 94 | frame_start, frame_end)) 95 | 96 | if node_type == 'i_caf': 97 | utils.add_fakebones(group) 98 | try: 99 | self._export_library_animation_clips_and_animations( 100 | libanmcl, libanm, group) 101 | self._export_library_visual_scenes(visual_scene, group) 102 | except RuntimeError: 103 | pass 104 | finally: 105 | if node_type == 'i_caf': 106 | utils.remove_fakebones() 107 | utils.recover_bone_layers(object_, layers) 108 | 109 | bcPrint("Animation has been processed.") 110 | 111 | bpy.context.scene.frame_current = initial_frame_active 112 | bpy.context.scene.frame_start = initial_frame_start 113 | bpy.context.scene.frame_end = initial_frame_end 114 | print('') 115 | 116 | self._export_scene(root_element) 117 | 118 | converter = RCInstance(self._config) 119 | converter.convert_dae(self._doc) 120 | 121 | def _prepare_for_export(self): 122 | utils.clean_file() 123 | 124 | # ----------------------------------------------------------------------------- 125 | # Library Animations and Clips: --> Animations, F-Curves 126 | # ----------------------------------------------------------------------------- 127 | 128 | def _export_library_animation_clips_and_animations( 129 | self, libanmcl, libanm, group): 130 | 131 | scene = bpy.context.scene 132 | anim_id = utils.get_animation_id(group) 133 | 134 | animation_clip = self._doc.createElement("animation_clip") 135 | animation_clip.setAttribute("id", anim_id) 136 | animation_clip.setAttribute("start", "{:f}".format( 137 | utils.frame_to_time(scene.frame_start))) 138 | animation_clip.setAttribute("end", "{:f}".format( 139 | utils.frame_to_time(scene.frame_end))) 140 | is_animation = False 141 | 142 | for object_ in group.objects: 143 | if (object_.type != 'ARMATURE' and object_.animation_data and 144 | object_.animation_data.action): 145 | 146 | is_animation = True 147 | 148 | props_name = self._create_properties_name(object_, group) 149 | bone_name = "{!s}{!s}".format(object_.name, props_name) 150 | 151 | for axis in iter(AXES): 152 | animation = self._get_animation_location( 153 | object_, bone_name, axis, anim_id) 154 | if animation is not None: 155 | libanm.appendChild(animation) 156 | 157 | for axis in iter(AXES): 158 | animation = self._get_animation_rotation( 159 | object_, bone_name, axis, anim_id) 160 | if animation is not None: 161 | libanm.appendChild(animation) 162 | 163 | self._export_instance_animation_parameters( 164 | object_, animation_clip, anim_id) 165 | 166 | if is_animation: 167 | libanmcl.appendChild(animation_clip) 168 | 169 | def _export_instance_animation_parameters( 170 | self, object_, animation_clip, anim_id): 171 | location_exists = rotation_exists = False 172 | for curve in object_.animation_data.action.fcurves: 173 | for axis in iter(AXES): 174 | if curve.array_index == AXES[axis]: 175 | if curve.data_path == "location": 176 | location_exists = True 177 | if curve.data_path == "rotation_euler": 178 | rotation_exists = True 179 | if location_exists and rotation_exists: 180 | break 181 | 182 | if location_exists: 183 | self._export_instance_parameter( 184 | object_, animation_clip, "location", anim_id) 185 | if rotation_exists: 186 | self._export_instance_parameter( 187 | object_, animation_clip, "rotation_euler", anim_id) 188 | 189 | def _export_instance_parameter( 190 | self, 191 | object_, 192 | animation_clip, 193 | parameter, 194 | anim_id): 195 | for axis in iter(AXES): 196 | inst = self._doc.createElement("instance_animation") 197 | inst.setAttribute( 198 | "url", "#{!s}-{!s}_{!s}_{!s}".format( 199 | anim_id, object_.name, parameter, axis)) 200 | animation_clip.appendChild(inst) 201 | 202 | def _get_animation_location(self, object_, bone_name, axis, anim_id): 203 | attribute_type = "location" 204 | multiplier = 1 205 | target = "{!s}{!s}{!s}".format(bone_name, "/translation.", axis) 206 | 207 | animation_element = self._get_animation_attribute(object_, 208 | axis, 209 | attribute_type, 210 | multiplier, 211 | target, 212 | anim_id) 213 | return animation_element 214 | 215 | def _get_animation_rotation(self, object_, bone_name, axis, anim_id): 216 | attribute_type = "rotation_euler" 217 | multiplier = utils.to_degrees 218 | target = "{!s}{!s}{!s}{!s}".format(bone_name, 219 | "/rotation_", 220 | axis, 221 | ".ANGLE") 222 | 223 | animation_element = self._get_animation_attribute(object_, 224 | axis, 225 | attribute_type, 226 | multiplier, 227 | target, 228 | anim_id) 229 | return animation_element 230 | 231 | def _get_animation_attribute(self, 232 | object_, 233 | axis, 234 | attribute_type, 235 | multiplier, 236 | target, 237 | anim_id): 238 | id_prefix = "{!s}-{!s}_{!s}_{!s}".format(anim_id, object_.name, 239 | attribute_type, axis) 240 | source_prefix = "#{!s}".format(id_prefix) 241 | 242 | for curve in object_.animation_data.action.fcurves: 243 | if (curve.data_path == 244 | attribute_type and curve.array_index == AXES[axis]): 245 | keyframe_points = curve.keyframe_points 246 | sources = { 247 | "input": [], 248 | "output": [], 249 | "interpolation": [], 250 | "intangent": [], 251 | "outangent": [] 252 | } 253 | for keyframe_point in keyframe_points: 254 | khlx = keyframe_point.handle_left[0] 255 | khly = keyframe_point.handle_left[1] 256 | khrx = keyframe_point.handle_right[0] 257 | khry = keyframe_point.handle_right[1] 258 | frame, value = keyframe_point.co 259 | 260 | sources["input"].append(utils.frame_to_time(frame)) 261 | sources["output"].append(value * multiplier) 262 | sources["interpolation"].append( 263 | keyframe_point.interpolation) 264 | sources["intangent"].extend( 265 | [utils.frame_to_time(khlx), khly]) 266 | sources["outangent"].extend( 267 | [utils.frame_to_time(khrx), khry]) 268 | 269 | animation_element = self._doc.createElement("animation") 270 | animation_element.setAttribute("id", id_prefix) 271 | 272 | for type_, data in sources.items(): 273 | anim_node = self._create_animation_node( 274 | type_, data, id_prefix) 275 | animation_element.appendChild(anim_node) 276 | 277 | sampler = self._create_sampler(id_prefix, source_prefix) 278 | channel = self._doc.createElement("channel") 279 | channel.setAttribute( 280 | "source", "{!s}-sampler".format(source_prefix)) 281 | channel.setAttribute("target", target) 282 | 283 | animation_element.appendChild(sampler) 284 | animation_element.appendChild(channel) 285 | 286 | return animation_element 287 | 288 | def _create_animation_node(self, type_, data, id_prefix): 289 | id_ = "{!s}-{!s}".format(id_prefix, type_) 290 | type_map = { 291 | "input": ["float", ["TIME"]], 292 | "output": ["float", ["VALUE"]], 293 | "intangent": ["float", "XY"], 294 | "outangent": ["float", "XY"], 295 | "interpolation": ["name", ["INTERPOLATION"]] 296 | } 297 | 298 | source = utils.write_source( 299 | id_, type_map[type_][0], data, type_map[type_][1]) 300 | 301 | return source 302 | 303 | def _create_sampler(self, id_prefix, source_prefix): 304 | sampler = self._doc.createElement("sampler") 305 | sampler.setAttribute("id", "{!s}-sampler".format(id_prefix)) 306 | 307 | input = self._doc.createElement("input") 308 | input.setAttribute("semantic", "INPUT") 309 | input.setAttribute("source", "{!s}-input".format(source_prefix)) 310 | output = self._doc.createElement("input") 311 | output.setAttribute("semantic", "OUTPUT") 312 | output.setAttribute("source", "{!s}-output".format(source_prefix)) 313 | interpolation = self._doc.createElement("input") 314 | interpolation.setAttribute("semantic", "INTERPOLATION") 315 | interpolation.setAttribute( 316 | "source", "{!s}-interpolation".format(source_prefix)) 317 | intangent = self._doc.createElement("input") 318 | intangent.setAttribute("semantic", "IN_TANGENT") 319 | intangent.setAttribute( 320 | "source", "{!s}-intangent".format(source_prefix)) 321 | outangent = self._doc.createElement("input") 322 | outangent.setAttribute("semantic", "OUT_TANGENT") 323 | outangent.setAttribute( 324 | "source", "{!s}-outangent".format(source_prefix)) 325 | 326 | sampler.appendChild(input) 327 | sampler.appendChild(output) 328 | sampler.appendChild(interpolation) 329 | sampler.appendChild(intangent) 330 | sampler.appendChild(outangent) 331 | 332 | return sampler 333 | 334 | # --------------------------------------------------------------------- 335 | # Library Visual Scene: --> Skeleton and _Phys bones, Bone 336 | # Transformations, and Instance URL (_boneGeometry) and extras. 337 | # --------------------------------------------------------------------- 338 | 339 | def _export_library_visual_scenes(self, visual_scene, group): 340 | if utils.get_animation_export_nodes(): 341 | if utils.are_duplicate_nodes(): 342 | message = "Duplicate Node Names" 343 | bpy.ops.bcry.display_error('INVOKE_DEFAULT', message=message) 344 | 345 | self._write_export_node(group, visual_scene) 346 | else: 347 | pass # TODO: Handle No Export Nodes Error 348 | 349 | def _write_export_node(self, group, visual_scene): 350 | if not self._config.export_for_lumberyard: 351 | node_name = "CryExportNode_{}".format(utils.get_node_name(group)) 352 | node = self._doc.createElement("node") 353 | node.setAttribute("id", node_name) 354 | node.setIdAttribute("id") 355 | else: 356 | node_name = "{}".format(utils.get_node_name(group)) 357 | node = self._doc.createElement("node") 358 | node.setAttribute("id", node_name) 359 | node.setAttribute("LumberyardExportNode", "1") 360 | node.setIdAttribute("id") 361 | 362 | bpy.ops.mesh.primitive_cube_add(location=(0, 0, 0)) 363 | self._write_transforms(bpy.context.active_object, node) 364 | bpy.ops.object.delete(use_global=False) 365 | 366 | node = self._write_visual_scene_node(group.objects, node, group) 367 | 368 | extra = self._create_cryengine_extra(group) 369 | node.appendChild(extra) 370 | visual_scene.appendChild(node) 371 | 372 | def _write_visual_scene_node(self, objects, parent_node, group): 373 | node_type = utils.get_node_type(group) 374 | for object_ in objects: 375 | if node_type == 'i_caf' and object_.type == 'ARMATURE': 376 | self._write_bone_list([utils.get_root_bone( 377 | object_)], object_, parent_node, group) 378 | 379 | elif node_type == 'anm' and object_.type == 'MESH': 380 | prop_name = "{}{}".format( 381 | object_.name, self._create_properties_name( 382 | object_, group)) 383 | node = self._doc.createElement("node") 384 | node.setAttribute("id", prop_name) 385 | node.setAttribute("name", prop_name) 386 | node.setIdAttribute("id") 387 | 388 | self._write_transforms(object_, node) 389 | 390 | udp_extra = self._create_user_defined_property(object_) 391 | if udp_extra is not None: 392 | node.appendChild(udp_extra) 393 | 394 | parent_node.appendChild(node) 395 | 396 | return parent_node 397 | 398 | def _create_cryengine_extra(self, node): 399 | extra = self._doc.createElement("extra") 400 | technique = self._doc.createElement("technique") 401 | technique.setAttribute("profile", "CryEngine") 402 | properties = self._doc.createElement("properties") 403 | 404 | node_type = utils.get_node_type(node) 405 | prop = self._doc.createTextNode("fileType={}".format(node_type)) 406 | properties.appendChild(prop) 407 | 408 | prop = self._doc.createTextNode("CustomExportPath=") 409 | properties.appendChild(prop) 410 | 411 | technique.appendChild(properties) 412 | 413 | if (node.name[:6] == "_joint"): 414 | helper = self._create_helper_joint(node) 415 | technique.appendChild(helper) 416 | 417 | extra.appendChild(technique) 418 | extra.appendChild(self._create_xsi_profile(node)) 419 | 420 | return extra 421 | 422 | 423 | # ------------------------------------------------------------------- 424 | 425 | 426 | def save(config): 427 | # prevent wasting time for exporting if RC was not found 428 | if not config.disable_rc and not os.path.isfile(config.rc_path): 429 | raise exceptions.NoRcSelectedException 430 | 431 | exporter = CrytekDaeAnimationExporter(config) 432 | exporter.export() 433 | 434 | 435 | def register(): 436 | bpy.utils.register_class(CrytekDaeAnimationExporter) 437 | 438 | # bpy.utils.register_class(TriangulateMeError) 439 | # bpy.utils.register_class(Error) 440 | 441 | 442 | def unregister(): 443 | bpy.utils.unregister_class(CrytekDaeAnimationExporter) 444 | # bpy.utils.unregister_class(TriangulateMeError) 445 | # bpy.utils.unregister_class(Error) 446 | 447 | 448 | if __name__ == "__main__": 449 | register() 450 | 451 | # test call 452 | bpy.ops.export_mesh.crytekdae('INVOKE_DEFAULT') 453 | -------------------------------------------------------------------------------- /io_bcry_exporter/material_utils.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # Name: material_utils.py 3 | # Purpose: Holds material and texture functions. 4 | # 5 | # Author: Özkan Afacan 6 | # Angelo J. Miner, Mikołaj Milej, Daniel White, 7 | # Oscar Martin Garcia, Duo Oratar, David Marcelis 8 | # 9 | # Created: 17/09/2016 10 | # Copyright: (c) Özkan Afacan 2016 11 | # License: GPLv2+ 12 | # ------------------------------------------------------------------------------ 13 | 14 | 15 | if "bpy" in locals(): 16 | import importlib 17 | importlib.reload(utils) 18 | importlib.reload(exceptions) 19 | else: 20 | import bpy 21 | from . import utils, exceptions 22 | 23 | import math 24 | import os 25 | import re 26 | import xml.dom.minidom 27 | from collections import OrderedDict 28 | from xml.dom.minidom import Document, Element, parse, parseString 29 | 30 | import bpy 31 | from mathutils import Color 32 | 33 | from .outpipe import bcPrint 34 | from .rc import RCInstance 35 | 36 | # ------------------------------------------------------------------------------ 37 | # Generate Materials: 38 | # ------------------------------------------------------------------------------ 39 | 40 | 41 | def generate_mtl_files(_config, materials=None): 42 | if materials is None: 43 | materials = get_materials(_config.export_selected_nodes) 44 | 45 | for node in get_material_groups(materials): 46 | _doc = Document() 47 | parent_material = _doc.createElement('Material') 48 | parent_material.setAttribute("MtlFlags", "524544") 49 | parent_material.setAttribute("vertModifType", "0") 50 | sub_material = _doc.createElement('SubMaterials') 51 | parent_material.appendChild(sub_material) 52 | set_public_params(_doc, None, parent_material) 53 | 54 | print() 55 | bcPrint("'{}' material is being processed...".format(node)) 56 | 57 | for material_name, material in materials.items(): 58 | if material_name.split('__')[0] != node: 59 | continue 60 | 61 | print() 62 | write_material_information(material_name) 63 | 64 | material_node = _doc.createElement('Material') 65 | 66 | set_material_attributes(material, material_name, material_node) 67 | add_textures(_doc, material, material_node, _config) 68 | set_public_params(_doc, material, material_node) 69 | 70 | sub_material.appendChild(material_node) 71 | 72 | _doc.appendChild(parent_material) 73 | 74 | filename = "{!s}.mtl".format(node) 75 | filepath = os.path.join(os.path.dirname(_config.filepath), filename) 76 | utils.generate_xml(filepath, _doc, True, 1) 77 | utils.clear_xml_header(filepath) 78 | 79 | print() 80 | bcPrint("'{}' material file has been generated.".format(filename)) 81 | 82 | 83 | def write_material_information(material_name): 84 | parts = material_name.split('__') 85 | bcPrint("Subname: '{}' - Index: '{}' - Physic Type: '{}'".format( 86 | parts[2], parts[1], parts[3])) 87 | 88 | 89 | def get_material_groups(materials): 90 | material_groups = [] 91 | 92 | for material_name, material in materials.items(): 93 | group_name = material_name.split('__')[0] 94 | 95 | if not (group_name in material_groups): 96 | material_groups.append(group_name) 97 | 98 | return material_groups 99 | 100 | 101 | def sort_materials_by_names(unordered_materials): 102 | materials = OrderedDict() 103 | for material_name in sorted(unordered_materials): 104 | materials[material_name] = unordered_materials[material_name] 105 | 106 | return materials 107 | 108 | 109 | def get_materials(just_selected=False): 110 | materials = OrderedDict() 111 | material_counter = {} 112 | 113 | for group in utils.get_mesh_export_nodes(just_selected): 114 | material_counter[group.name] = 0 115 | for object in group.objects: 116 | for i in range(0, len(object.material_slots)): 117 | slot = object.material_slots[i] 118 | material = slot.material 119 | if material is None: 120 | continue 121 | 122 | if material not in materials.values(): 123 | node_name = utils.get_node_name(group) 124 | 125 | material.name = utils.replace_invalid_rc_characters( 126 | material.name) 127 | for image in get_textures(material): 128 | try: 129 | image.name = utils.replace_invalid_rc_characters( 130 | image.name) 131 | except AttributeError: 132 | pass 133 | 134 | node, index, name, physics = get_material_parts( 135 | node_name, slot.material.name) 136 | 137 | # check if material has no position defined 138 | if index == 0: 139 | material_counter[group.name] += 1 140 | index = material_counter[group.name] 141 | 142 | material_name = "{}__{:02d}__{}__{}".format( 143 | node, index, name, physics) 144 | materials[material_name] = material 145 | 146 | return sort_materials_by_names(materials) 147 | 148 | 149 | def set_material_attributes(material, material_name, material_node): 150 | material_node.setAttribute("Name", get_material_name(material_name)) 151 | material_node.setAttribute("MtlFlags", "524416") 152 | 153 | shader = "Illum" 154 | if "physProxyNoDraw" == get_material_physic(material_name): 155 | shader = "Nodraw" 156 | material_node.setAttribute("Shader", shader) 157 | material_node.setAttribute("GenMask", "60400000") 158 | material_node.setAttribute("StringGenMask", "%NORMAL_MAP%SPECULAR_MAP%SUBSURFACE_SCATTERING") 159 | material_node.setAttribute("SurfaceType", "") 160 | material_node.setAttribute("MatTemplate", "") 161 | 162 | use_default = True 163 | if material.use_nodes: 164 | bsdf = material.node_tree.nodes.get('Principled BSDF') 165 | if bsdf is not None: 166 | diffuse = Color( 167 | (bsdf.inputs['Base Color'].default_value[0], 168 | bsdf.inputs['Base Color'].default_value[1], 169 | bsdf.inputs['Base Color'].default_value[2]) 170 | ) 171 | specular = 1.0 # not support at the moment 172 | opacity = bsdf.inputs['Alpha'].default_value 173 | shininess = (1 - bsdf.inputs['Roughness'].default_value) * 255 174 | use_default = False 175 | 176 | if use_default: 177 | diffuse = Color( 178 | (material.diffuse_color[0], 179 | material.diffuse_color[1], 180 | material.diffuse_color[2]) 181 | ) 182 | specular = 1.0 # not support at the moment 183 | opacity = material.diffuse_color[3] 184 | shininess = (1 - material.roughness) * 255 185 | 186 | material_node.setAttribute("Diffuse", color_to_xml_string(diffuse)) 187 | material_node.setAttribute("Specular", color_to_xml_string(specular)) 188 | material_node.setAttribute("Opacity", str(opacity)) 189 | material_node.setAttribute("Shininess", str(shininess)) 190 | 191 | material_node.setAttribute("vertModifType", "0") 192 | material_node.setAttribute("LayerAct", "1") 193 | 194 | # if material.roughness: 195 | # emit_color = "1,1,1,{}".format(str(int(material.roughness * 100))) 196 | # material_node.setAttribute("Emittance", emit_color) 197 | 198 | 199 | def set_public_params(_doc, material, material_node): 200 | public_params = _doc.createElement('PublicParams') 201 | public_params.setAttribute("EmittanceMapGamma", "1") 202 | public_params.setAttribute("SSSIndex", "0") 203 | public_params.setAttribute("IndirectColor", "0.25, 0.25, 0.25") 204 | 205 | material_node.appendChild(public_params) 206 | 207 | 208 | def add_textures(_doc, material, material_node, _config): 209 | textures_node = _doc.createElement("Textures") 210 | 211 | diffuse = get_diffuse_texture(material) 212 | specular = get_specular_texture(material) 213 | normal = get_normal_texture(material) 214 | 215 | if diffuse: 216 | texture_node = _doc.createElement('Texture') 217 | texture_node.setAttribute("Map", "Diffuse") 218 | path = get_image_path_for_game(diffuse, _config.game_dir) 219 | texture_node.setAttribute("File", path) 220 | textures_node.appendChild(texture_node) 221 | bcPrint("Diffuse Path: {}.".format(path)) 222 | else: 223 | if "physProxyNoDraw" != get_material_physic(material.name): 224 | texture_node = _doc.createElement('Texture') 225 | texture_node.setAttribute("Map", "Diffuse") 226 | path = "%engine%/engineassets/textures/white.dds" 227 | texture_node.setAttribute("File", path) 228 | textures_node.appendChild(texture_node) 229 | bcPrint("Diffuse Path: {}.".format(path)) 230 | if specular: 231 | texture_node = _doc.createElement('Texture') 232 | texture_node.setAttribute("Map", "Specular") 233 | path = get_image_path_for_game(specular, _config.game_dir) 234 | texture_node.setAttribute("File", path) 235 | textures_node.appendChild(texture_node) 236 | bcPrint("Specular Path: {}.".format(path)) 237 | if normal: 238 | texture_node = _doc.createElement('Texture') 239 | texture_node.setAttribute("Map", "Normal") 240 | path = get_image_path_for_game(normal, _config.game_dir) 241 | texture_node.setAttribute("File", path) 242 | textures_node.appendChild(texture_node) 243 | bcPrint("Normal Path: {}.".format(path)) 244 | 245 | if _config.convert_textures: 246 | convert_image_to_dds([diffuse, specular, normal], _config) 247 | 248 | material_node.appendChild(textures_node) 249 | 250 | 251 | # ------------------------------------------------------------------------------ 252 | # Convert DDS: 253 | # ------------------------------------------------------------------------------ 254 | 255 | def convert_image_to_dds(images, _config): 256 | converter = RCInstance(_config) 257 | converter.convert_tif(images) 258 | 259 | 260 | # ------------------------------------------------------------------------------ 261 | # Collections: 262 | # ------------------------------------------------------------------------------ 263 | 264 | def get_textures(material): 265 | images = [] 266 | 267 | images.append(get_diffuse_texture(material)) 268 | images.append(get_specular_texture(material)) 269 | images.append(get_normal_texture(material)) 270 | 271 | return images 272 | 273 | 274 | def get_diffuse_texture(material): 275 | image = None 276 | try: 277 | if bpy.context.scene.render.engine == 'CYCLES': 278 | for node in material.node_tree.nodes: 279 | if node.type == 'TEX_IMAGE': 280 | if node.name == 'Image Texture' or \ 281 | node.name.lower().find('diffuse') != -1: 282 | image = node.image 283 | if is_valid_image(image): 284 | return image 285 | else: 286 | for slot in material.texture_slots: 287 | if slot.texture.type == 'IMAGE': 288 | if slot.use_map_color_diffuse: 289 | image = slot.texture.image 290 | if is_valid_image(image): 291 | return image 292 | except: 293 | pass 294 | 295 | return None 296 | 297 | 298 | def get_specular_texture(material): 299 | image = None 300 | try: 301 | if bpy.context.scene.render.engine == 'CYCLES': 302 | for node in material.node_tree.nodes: 303 | if node.type == 'TEX_IMAGE': 304 | if node.name.lower().find('specular') != -1: 305 | image = node.image 306 | if is_valid_image(image): 307 | return image 308 | else: 309 | for slot in material.texture_slots: 310 | if slot.texture.type == 'IMAGE': 311 | if slot.use_map_color_spec or slot.use_map_specular: 312 | image = slot.texture.image 313 | if is_valid_image(image): 314 | return image 315 | except: 316 | pass 317 | 318 | return None 319 | 320 | 321 | def get_normal_texture(material): 322 | image = None 323 | try: 324 | if bpy.context.scene.render.engine == 'CYCLES': 325 | for node in material.node_tree.nodes: 326 | if node.type == 'TEX_IMAGE': 327 | if node.name.lower().find('normal') != -1: 328 | image = node.image 329 | if is_valid_image(image): 330 | return image 331 | else: 332 | for slot in material.texture_slots: 333 | if slot.texture.type == 'IMAGE': 334 | if slot.use_map_color_normal: 335 | image = slot.texture.image 336 | if is_valid_image(image): 337 | return image 338 | except: 339 | pass 340 | 341 | return None 342 | 343 | 344 | # ------------------------------------------------------------------------------ 345 | # Conversions: 346 | # ------------------------------------------------------------------------------ 347 | 348 | def color_to_string(color, a): 349 | if type(color) in (float, int): 350 | return "{:f} {:f} {:f} {:f}".format(color, color, color, a) 351 | elif type(color).__name__ == "Color": 352 | return "{:f} {:f} {:f} {:f}".format(color.r, color.g, color.b, a) 353 | 354 | 355 | def color_to_xml_string(color): 356 | if type(color) in (float, int): 357 | return "{:f},{:f},{:f}".format(color, color, color) 358 | elif type(color).__name__ == "Color": 359 | return "{:f},{:f},{:f}".format(color[0], color[1], color[2]) 360 | 361 | 362 | # ------------------------------------------------------------------------------ 363 | # Materials: 364 | # ------------------------------------------------------------------------------ 365 | 366 | def get_material_counter(): 367 | """Returns a dictionary with all CryExportNodes.""" 368 | materialCounter = {} 369 | for collection in bpy.data.collections: 370 | if utils.is_export_node(collection): 371 | materialCounter[collection.name] = 0 372 | return materialCounter 373 | 374 | 375 | def get_material_physics(): 376 | """Returns a dictionary with the physics of all material names.""" 377 | physicsProperties = {} 378 | for material in bpy.data.materials: 379 | properties = extract_bcry_properties(material.name) 380 | if properties: 381 | physicsProperties[properties["Name"]] = properties["Physics"] 382 | return physicsProperties 383 | 384 | 385 | def get_materials_per_group(collection): 386 | materials = [] 387 | for _objtmp in bpy.data.collections[collection].objects: 388 | for material in _objtmp.data.materials: 389 | if material is not None: 390 | if material.name not in materials: 391 | materials.append(material.name) 392 | return materials 393 | 394 | 395 | def get_material_color(material, type_): 396 | color = 0.0 397 | alpha = 1.0 398 | 399 | if type_ == "emission": 400 | color = 0.0 401 | elif type_ == "ambient": 402 | color = 0.0 403 | elif type_ == "diffuse": 404 | col = material.diffuse_color 405 | color = Color((col[0], col[1], col[2])) 406 | alpha = col[3] 407 | elif type_ == "specular": 408 | # specular = Color((material.metallic, material.metallic, material.metallic)) 409 | color = 1.0 410 | 411 | col = color_to_string(color, alpha) 412 | return col 413 | 414 | 415 | def get_material_attribute(material, type_): 416 | if type_ == "shininess": 417 | float = (1 - material.roughness) * 255 418 | elif type_ == "index_refraction": 419 | float = material.diffuse_color[3] 420 | 421 | return str(float) 422 | 423 | 424 | def get_material_parts(node, material): 425 | VALID_PHYSICS = ("physDefault", "physProxyNoDraw", "physNoCollide", 426 | "physObstruct", "physNone") 427 | 428 | parts = material.split("__") 429 | count = len(parts) 430 | 431 | group = node 432 | index = 0 433 | name = material 434 | physics = "physNone" 435 | 436 | if count == 1: 437 | # name 438 | index = 0 439 | elif count == 2: 440 | # XXX__name or name__phys 441 | if parts[1] not in VALID_PHYSICS: 442 | # XXX__name 443 | index = int(parts[0]) 444 | name = parts[1] 445 | else: 446 | # name__phys 447 | name = parts[0] 448 | physics = parts[1] 449 | elif count == 3: 450 | # XXX__name__phys 451 | index = int(parts[0]) 452 | name = parts[1] 453 | physics = parts[2] 454 | elif count == 4: 455 | # group__XXX__name__phys 456 | group = parts[0] 457 | index = int(parts[1]) 458 | name = parts[2] 459 | physics = parts[3] 460 | 461 | name = utils.replace_invalid_rc_characters(name) 462 | if physics not in VALID_PHYSICS: 463 | physics = "physNone" 464 | 465 | return group, index, name, physics 466 | 467 | 468 | def extract_bcry_properties(material_name): 469 | """Returns the BCry properties of a material_name as dict or 470 | None if name is invalid. 471 | """ 472 | if is_bcry_material(material_name): 473 | groups = re.findall( 474 | "(.+)__([0-9]+)__(.*)__(phys[A-Za-z0-9]+)", 475 | material_name) 476 | properties = {} 477 | properties["ExportNode"] = groups[0][0] 478 | properties["Number"] = int(groups[0][1]) 479 | properties["Name"] = groups[0][2] 480 | properties["Physics"] = groups[0][3] 481 | return properties 482 | return None 483 | 484 | 485 | def remove_bcry_properties(): 486 | """Removes BCry Exporter properties from all material names.""" 487 | for material in bpy.data.materials: 488 | properties = extract_bcry_properties(material.name) 489 | if properties: 490 | material.name = properties["Name"] 491 | 492 | 493 | def is_bcry_material(material_name): 494 | if re.search(".+__[0-9]+__.*__phys[A-Za-z0-9]+", material_name): 495 | return True 496 | else: 497 | return False 498 | 499 | 500 | def is_bcry_material_with_numbers(material_name): 501 | if re.search("[0-9]+__.*", material_name): 502 | return True 503 | else: 504 | return False 505 | 506 | 507 | def get_material_name(material_name): 508 | try: 509 | return material_name.split('__')[2] 510 | except: 511 | raise exceptions.BCryException( 512 | "Material name is not convenient for BCry!") 513 | 514 | 515 | def get_material_physic(material_name): 516 | index = material_name.find("__phys") 517 | if index != -1: 518 | return material_name[index + 2:] 519 | 520 | return "physNone" 521 | 522 | 523 | def set_material_physic(self, context, phys_name): 524 | if not phys_name.startswith("__"): 525 | phys_name = "__" + phys_name 526 | 527 | me = bpy.context.active_object 528 | if me.active_material: 529 | me.active_material.name = replace_phys_material( 530 | me.active_material.name, phys_name) 531 | 532 | return {'FINISHED'} 533 | 534 | 535 | def replace_phys_material(material_name, phys): 536 | if "__phys" in material_name: 537 | return re.sub(r"__phys.*", phys, material_name) 538 | else: 539 | return "{}{}".format(material_name, phys) 540 | 541 | 542 | # ------------------------------------------------------------------------------ 543 | # Textures: 544 | # ------------------------------------------------------------------------------ 545 | 546 | def is_valid_image(image): 547 | try: 548 | return image.has_data and image.filepath 549 | except: 550 | return False 551 | 552 | 553 | def get_image_path_for_game(image, game_dir): 554 | if not game_dir or not os.path.isdir(game_dir): 555 | raise exceptions.NoGameDirectorySelected 556 | 557 | image_path = os.path.normpath(bpy.path.abspath(image.filepath)) 558 | image_path = "{}.dds".format(os.path.splitext(image_path)[0]) 559 | image_path = os.path.relpath(image_path, game_dir) 560 | 561 | return image_path 562 | -------------------------------------------------------------------------------- /io_bcry_exporter/export.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # Name: export.py 3 | # Purpose: Main exporter to CryEngine 4 | # 5 | # Author: Özkan Afacan, 6 | # Angelo J. Miner, Mikołaj Milej, Daniel White, 7 | # Oscar Martin Garcia, Duo Oratar, David Marcelis 8 | # Some code borrowed from fbx exporter Campbell Barton 9 | # 10 | # Created: 23/01/2012 11 | # Copyright: (c) Angelo J. Miner 2012 12 | # Copyright: (c) Özkan Afacan 2016 13 | # License: GPLv2+ 14 | # ------------------------------------------------------------------------------ 15 | 16 | 17 | if "bpy" in locals(): 18 | import importlib 19 | importlib.reload(utils) 20 | importlib.reload(export_materials) 21 | importlib.reload(udp) 22 | importlib.reload(exceptions) 23 | else: 24 | import bpy 25 | from . import utils, export_materials, udp, exceptions 26 | 27 | import copy 28 | import os 29 | import subprocess 30 | import threading 31 | import time 32 | import xml.dom.minidom 33 | from collections import OrderedDict 34 | from datetime import datetime 35 | from time import clock 36 | from xml.dom.minidom import Document, Element, parse, parseString 37 | 38 | import bmesh 39 | from bpy_extras.io_utils import ExportHelper 40 | from mathutils import Matrix, Vector 41 | 42 | from .outpipe import bcPrint 43 | from .rc import RCInstance 44 | from .utils import join 45 | 46 | 47 | class CrytekDaeExporter: 48 | 49 | def __init__(self, config): 50 | self._config = config 51 | self._doc = Document() 52 | self._m_exporter = export_materials.CrytekMaterialExporter(config) 53 | print("CrytekDaeExporter_INIT") 54 | 55 | def export(self): 56 | self._prepare_for_export() 57 | 58 | root_element = self._doc.createElement('collada') 59 | root_element.setAttribute( 60 | "xmlns", "http://www.collada.org/2005/11/COLLADASchema") 61 | root_element.setAttribute("version", "1.4.1") 62 | self._doc.appendChild(root_element) 63 | self._create_file_header(root_element) 64 | 65 | if self._config.generate_materials: 66 | self._m_exporter.generate_materials() 67 | 68 | # Just here for future use: 69 | self._export_library_cameras(root_element) 70 | self._export_library_lights(root_element) 71 | ### 72 | 73 | self._export_library_images(root_element) 74 | self._export_library_effects(root_element) 75 | self._export_library_materials(root_element) 76 | self._export_library_geometries(root_element) 77 | 78 | utils.add_fakebones() 79 | try: 80 | self._export_library_controllers(root_element) 81 | self._export_library_animation_clips_and_animations(root_element) 82 | self._export_library_visual_scenes(root_element) 83 | except RuntimeError: 84 | pass 85 | finally: 86 | utils.remove_fakebones() 87 | 88 | self._export_scene(root_element) 89 | 90 | converter = RCInstance(self._config) 91 | converter.convert_dae(self._doc) 92 | 93 | write_scripts(self=self, config=self._config) 94 | 95 | def _prepare_for_export(self): 96 | utils.clean_file(self._config.export_selected_nodes) 97 | 98 | if self._config.fix_weights: 99 | utils.fix_weights() 100 | 101 | def _create_file_header(self, parent_element): 102 | asset = self._doc.createElement('asset') 103 | parent_element.appendChild(asset) 104 | contributor = self._doc.createElement('contributor') 105 | asset.appendChild(contributor) 106 | author = self._doc.createElement('author') 107 | contributor.appendChild(author) 108 | author_name = self._doc.createTextNode('Blender User') 109 | author.appendChild(author_name) 110 | author_tool = self._doc.createElement('authoring_tool') 111 | author_name_text = self._doc.createTextNode( 112 | 'BCry v{}'.format(self._config.bcry_version)) 113 | author_tool.appendChild(author_name_text) 114 | contributor.appendChild(author_tool) 115 | created = self._doc.createElement('created') 116 | created_value = self._doc.createTextNode( 117 | datetime.now().isoformat(' ')) 118 | created.appendChild(created_value) 119 | asset.appendChild(created) 120 | modified = self._doc.createElement('modified') 121 | asset.appendChild(modified) 122 | unit = self._doc.createElement('unit') 123 | unit.setAttribute('name', 'meter') 124 | unit.setAttribute('meter', '1') 125 | asset.appendChild(unit) 126 | up_axis = self._doc.createElement('up_axis') 127 | z_up = self._doc.createTextNode('Z_UP') 128 | up_axis.appendChild(z_up) 129 | asset.appendChild(up_axis) 130 | 131 | # ------------------------------------------------------------------ 132 | # Library Cameras: 133 | # ------------------------------------------------------------------ 134 | 135 | def _export_library_cameras(self, root_element): 136 | library_cameras = self._doc.createElement('library_cameras') 137 | root_element.appendChild(library_cameras) 138 | 139 | # ------------------------------------------------------------------ 140 | # Library Lights: 141 | # ------------------------------------------------------------------ 142 | 143 | def _export_library_lights(self, root_element): 144 | library_lights = self._doc.createElement('library_lights') 145 | root_element.appendChild(library_lights) 146 | 147 | # ------------------------------------------------------------------ 148 | # Library Images: 149 | # ------------------------------------------------------------------ 150 | 151 | def _export_library_images(self, parent_element): 152 | library_images = self._doc.createElement('library_images') 153 | self._m_exporter.export_library_images(library_images) 154 | parent_element.appendChild(library_images) 155 | 156 | # -------------------------------------------------------------- 157 | # Library Effects: 158 | # -------------------------------------------------------------- 159 | 160 | def _export_library_effects(self, parent_element): 161 | library_effects = self._doc.createElement('library_effects') 162 | self._m_exporter.export_library_effects(library_effects) 163 | parent_element.appendChild(library_effects) 164 | 165 | # ------------------------------------------------------------------ 166 | # Library Materials: 167 | # ------------------------------------------------------------------ 168 | 169 | def _export_library_materials(self, parent_element): 170 | library_materials = self._doc.createElement('library_materials') 171 | self._m_exporter.export_library_materials(library_materials) 172 | parent_element.appendChild(library_materials) 173 | 174 | # ------------------------------------------------------------------ 175 | # Library Geometries: 176 | # ------------------------------------------------------------------ 177 | 178 | def _export_library_geometries(self, parent_element): 179 | libgeo = self._doc.createElement("library_geometries") 180 | parent_element.appendChild(libgeo) 181 | for collection in utils.get_mesh_export_nodes( 182 | self._config.export_selected_nodes): 183 | for object_ in collection.objects: 184 | if object_.type != 'MESH': 185 | continue 186 | 187 | apply_modifiers = self._config.apply_modifiers 188 | if utils.get_node_type(collection) in ('chr', 'skin'): 189 | apply_modifiers = False 190 | 191 | bmesh_ = utils.get_bmesh(object_, apply_modifiers) 192 | geometry_node = self._doc.createElement("geometry") 193 | geometry_name = utils.get_geometry_name(collection, object_) 194 | geometry_node.setAttribute("id", geometry_name) 195 | mesh_node = self._doc.createElement("mesh") 196 | 197 | print() 198 | bcPrint('"{}" object is being processed...'.format(object_.name)) 199 | 200 | start_time = clock() 201 | self._write_positions(bmesh_, mesh_node, geometry_name) 202 | bcPrint('Positions have been writed {:.4f} seconds.'.format(clock() - start_time)) 203 | 204 | start_time = clock() 205 | self._write_normals(object_, bmesh_, mesh_node, geometry_name) 206 | bcPrint('Normals have been writed {:.4f} seconds.'.format(clock() - start_time)) 207 | 208 | start_time = clock() 209 | self._write_uvs(object_, bmesh_, mesh_node, geometry_name) 210 | bcPrint('UVs have been writed {:.4f} seconds.'.format(clock() - start_time)) 211 | 212 | start_time = clock() 213 | self._write_vertex_colors(object_, bmesh_, mesh_node, geometry_name) 214 | bcPrint('Vertex colors have been writed {:.4f} seconds.'.format(clock() - start_time)) 215 | 216 | start_time = clock() 217 | self._write_vertices(mesh_node, geometry_name) 218 | bcPrint('Vertices have been writed {:.4f} seconds.'.format(clock() - start_time)) 219 | 220 | start_time = clock() 221 | self._write_triangle_list( 222 | object_, bmesh_, mesh_node, geometry_name) 223 | bcPrint('Triangle list have been writed {:.4f} seconds.'.format(clock() - start_time)) 224 | 225 | extra = self._create_double_sided_extra("MAYA") 226 | mesh_node.appendChild(extra) 227 | geometry_node.appendChild(mesh_node) 228 | libgeo.appendChild(geometry_node) 229 | 230 | utils.clear_bmesh(object_, bmesh_) 231 | bcPrint('"{}" object has been processed for "{}" node.'.format(object_.name, collection.name)) 232 | 233 | def _write_positions(self, bmesh_, mesh_node, geometry_name): 234 | float_positions = [] 235 | for vertex in bmesh_.verts: 236 | float_positions.extend(vertex.co) 237 | 238 | id_ = "{!s}-pos".format(geometry_name) 239 | source = utils.write_source(id_, "float", float_positions, "XYZ") 240 | mesh_node.appendChild(source) 241 | 242 | def _write_normals(self, object_, bmesh_, mesh_node, geometry_name): 243 | split_angle = 0 244 | use_edge_angle = False 245 | use_edge_sharp = False 246 | 247 | if object_.data.use_auto_smooth: 248 | use_edge_angle = True 249 | use_edge_sharp = True 250 | split_angle = object_.data.auto_smooth_angle 251 | else: 252 | for modifier in object_.modifiers: 253 | if modifier.type == 'EDGE_SPLIT' and modifier.show_viewport: 254 | use_edge_angle = modifier.use_edge_angle 255 | use_edge_sharp = modifier.use_edge_sharp 256 | split_angle = modifier.split_angle 257 | 258 | float_normals = None 259 | if self._config.custom_normals: 260 | float_normals = utils.get_custom_normals(bmesh_, use_edge_angle, 261 | split_angle) 262 | else: 263 | float_normals = utils.get_normal_array(bmesh_, use_edge_angle, 264 | use_edge_sharp, split_angle) 265 | 266 | id_ = "{!s}-normal".format(geometry_name) 267 | source = utils.write_source(id_, "float", float_normals, "XYZ") 268 | mesh_node.appendChild(source) 269 | 270 | def _write_uvs(self, object_, bmesh_, mesh_node, geometry_name): 271 | uv_layer = bmesh_.loops.layers.uv.active 272 | if object_.data.uv_layers.active is None: 273 | bcPrint( 274 | "{} object has no a UV map, creating a default UV...".format( 275 | object_.name)) 276 | uv_layer = bmesh_.loops.layers.uv.new() 277 | 278 | float_uvs = [] 279 | 280 | for face in bmesh_.faces: 281 | for loop in face.loops: 282 | float_uvs.extend(loop[uv_layer].uv) 283 | 284 | id_ = "{!s}-uvs".format(geometry_name) 285 | source = utils.write_source(id_, "float", float_uvs, "ST") 286 | mesh_node.appendChild(source) 287 | 288 | def _write_vertex_colors(self, object_, bmesh_, mesh_node, geometry_name): 289 | float_colors = [] 290 | alpha_found = False 291 | 292 | active_layer = bmesh_.loops.layers.color.active 293 | if object_.data.vertex_colors: 294 | if active_layer.name.lower() == 'alpha': 295 | alpha_found = True 296 | for vert in bmesh_.verts: 297 | loop = vert.link_loops[0] 298 | color = loop[active_layer] 299 | alpha_color = (color[0] + color[1] + color[2]) / 3.0 300 | float_colors.extend([1.0, 1.0, 1.0, alpha_color]) 301 | else: 302 | for vert in bmesh_.verts: 303 | loop = vert.link_loops[0] 304 | color = loop[active_layer] 305 | float_colors.extend([color[0], color[1], color[2]]) 306 | 307 | if float_colors: 308 | id_ = "{!s}-vcol".format(geometry_name) 309 | params = ("RGBA" if alpha_found else "RGB") 310 | source = utils.write_source(id_, "float", float_colors, params) 311 | mesh_node.appendChild(source) 312 | 313 | def _write_vertices(self, mesh_node, geometry_name): 314 | vertices = self._doc.createElement("vertices") 315 | vertices.setAttribute("id", "{}-vtx".format(geometry_name)) 316 | input = utils.write_input(geometry_name, None, "pos", "POSITION") 317 | vertices.appendChild(input) 318 | mesh_node.appendChild(vertices) 319 | 320 | def _write_triangle_list(self, object_, bmesh_, mesh_node, geometry_name): 321 | tessfaces = utils.get_tessfaces(bmesh_) 322 | current_material_index = 0 323 | for material, materialname in self._m_exporter.get_materials_for_object( 324 | object_).items(): 325 | triangles = '' 326 | triangle_count = 0 327 | normal_uv_index = 0 328 | for face in bmesh_.faces: 329 | norm_uv_indices = {} 330 | 331 | for index in range(0, len(face.verts)): 332 | norm_uv_indices[ 333 | str(face.verts[index].index)] = normal_uv_index + index 334 | 335 | if face.material_index == current_material_index: 336 | for tessface in tessfaces[face.index]: 337 | triangle_count += 1 338 | for vert in tessface: 339 | normal_uv = norm_uv_indices[str(vert)] 340 | dae_vertex = self._write_vertex_data( 341 | vert, normal_uv, normal_uv, object_.data.vertex_colors) 342 | triangles = join(triangles, dae_vertex) 343 | 344 | normal_uv_index += len(face.verts) 345 | 346 | current_material_index += 1 347 | 348 | if triangle_count == 0: 349 | continue 350 | 351 | triangle_list = self._doc.createElement('triangles') 352 | triangle_list.setAttribute('material', materialname) 353 | triangle_list.setAttribute('count', str(triangle_count)) 354 | 355 | inputs = [] 356 | inputs.append( 357 | utils.write_input( 358 | geometry_name, 359 | 0, 360 | 'vtx', 361 | 'VERTEX')) 362 | inputs.append( 363 | utils.write_input( 364 | geometry_name, 365 | 1, 366 | 'normal', 367 | 'NORMAL')) 368 | inputs.append( 369 | utils.write_input( 370 | geometry_name, 371 | 2, 372 | 'uvs', 373 | 'TEXCOORD')) 374 | if object_.data.vertex_colors: 375 | inputs.append( 376 | utils.write_input( 377 | geometry_name, 378 | 3, 379 | 'vcol', 380 | 'COLOR')) 381 | 382 | for input in inputs: 383 | triangle_list.appendChild(input) 384 | 385 | p = self._doc.createElement('p') 386 | p_text = self._doc.createTextNode(triangles) 387 | p.appendChild(p_text) 388 | 389 | triangle_list.appendChild(p) 390 | mesh_node.appendChild(triangle_list) 391 | 392 | def _write_vertex_data(self, vert, normal, uv, vertex_colors): 393 | if vertex_colors: 394 | return "{:d} {:d} {:d} {:d} ".format( 395 | vert, normal, uv, vert) 396 | else: 397 | return "{:d} {:d} {:d} ".format(vert, normal, uv) 398 | 399 | def _create_double_sided_extra(self, profile): 400 | extra = self._doc.createElement("extra") 401 | technique = self._doc.createElement("technique") 402 | technique.setAttribute("profile", profile) 403 | double_sided = self._doc.createElement("double_sided") 404 | double_sided_value = self._doc.createTextNode("1") 405 | double_sided.appendChild(double_sided_value) 406 | technique.appendChild(double_sided) 407 | extra.appendChild(technique) 408 | 409 | return extra 410 | 411 | # ------------------------------------------------------------------------- 412 | # Library Controllers: --> Skeleton Armature and List of Bone Names 413 | # --> Skin Geometry, Weights, Transform Matrices 414 | # ------------------------------------------------------------------------- 415 | 416 | def _export_library_controllers(self, parent_element): 417 | library_node = self._doc.createElement("library_controllers") 418 | 419 | ALLOWED_NODE_TYPES = ('chr', 'skin') 420 | for collection in utils.get_mesh_export_nodes( 421 | self._config.export_selected_nodes): 422 | node_type = utils.get_node_type(collection) 423 | if node_type in ALLOWED_NODE_TYPES: 424 | for object_ in collection.objects: 425 | if not utils.is_bone_geometry(object_): 426 | armature = utils.get_armature_for_object(object_) 427 | if armature is not None: 428 | self._process_bones(library_node, 429 | collection, 430 | object_, 431 | armature) 432 | parent_element.appendChild(library_node) 433 | 434 | def _process_bones(self, parent_node, group, object_, armature): 435 | id_ = "{!s}_{!s}".format(armature.name, object_.name) 436 | 437 | controller_node = self._doc.createElement("controller") 438 | parent_node.appendChild(controller_node) 439 | controller_node.setAttribute("id", id_) 440 | 441 | skin_node = self._doc.createElement("skin") 442 | skin_node.setAttribute( 443 | "source", "#{!s}".format( 444 | utils.get_geometry_name( 445 | group, object_))) 446 | controller_node.appendChild(skin_node) 447 | 448 | bind_shape_matrix = self._doc.createElement("bind_shape_matrix") 449 | utils.write_matrix(Matrix(), bind_shape_matrix) 450 | skin_node.appendChild(bind_shape_matrix) 451 | 452 | self._process_bone_joints(object_, armature, skin_node, group) 453 | self._process_bone_matrices(object_, armature, skin_node) 454 | self._process_bone_weights(object_, armature, skin_node) 455 | 456 | joints = self._doc.createElement("joints") 457 | input = utils.write_input(id_, None, "joints", "JOINT") 458 | joints.appendChild(input) 459 | input = utils.write_input(id_, None, "matrices", "INV_BIND_MATRIX") 460 | joints.appendChild(input) 461 | skin_node.appendChild(joints) 462 | 463 | def _process_bone_joints(self, object_, armature, skin_node, group): 464 | bones = utils.get_bones(armature) 465 | id_ = "{!s}_{!s}-joints".format(armature.name, object_.name) 466 | bone_names = [] 467 | for bone in bones: 468 | props_name = self._create_properties_name(bone, group) 469 | bone_name = "{!s}{!s}".format(bone.name, props_name) 470 | bone_names.append(bone_name) 471 | source = utils.write_source(id_, "IDREF", bone_names, []) 472 | skin_node.appendChild(source) 473 | 474 | def _process_bone_matrices(self, object_, armature, skin_node): 475 | 476 | bones = utils.get_bones(armature) 477 | bone_matrices = [] 478 | for bone in armature.pose.bones: 479 | 480 | bone_matrix = utils.transform_bone_matrix(bone) 481 | bone_matrices.extend(utils.matrix_to_array(bone_matrix)) 482 | 483 | id_ = "{!s}_{!s}-matrices".format(armature.name, object_.name) 484 | source = utils.write_source(id_, "float4x4", bone_matrices, []) 485 | skin_node.appendChild(source) 486 | 487 | def _process_bone_weights(self, object_, armature, skin_node): 488 | 489 | bones = utils.get_bones(armature) 490 | group_weights = [] 491 | vw = "" 492 | vertex_groups_lengths = "" 493 | vertex_count = 0 494 | bone_list = {} 495 | 496 | for bone_id, bone in enumerate(bones): 497 | bone_list[bone.name] = bone_id 498 | 499 | for vertex in object_.data.vertices: 500 | vertex_group_count = 0 501 | for group in vertex.groups: 502 | group_name = object_.vertex_groups[group.group].name 503 | if (group.weight == 0 or 504 | group_name not in bone_list): 505 | continue 506 | if vertex_group_count == 8: 507 | bcPrint("Too many bone references in {}:{} vertex group" 508 | .format(object_.name, group_name)) 509 | continue 510 | group_weights.append(group.weight) 511 | vw = "{}{} {} ".format(vw, bone_list[group_name], vertex_count) 512 | vertex_count += 1 513 | vertex_group_count += 1 514 | 515 | vertex_groups_lengths = "{}{} ".format(vertex_groups_lengths, 516 | vertex_group_count) 517 | 518 | id_ = "{!s}_{!s}-weights".format(armature.name, object_.name) 519 | source = utils.write_source(id_, "float", group_weights, []) 520 | skin_node.appendChild(source) 521 | 522 | vertex_weights = self._doc.createElement("vertex_weights") 523 | vertex_weights.setAttribute("count", str(len(object_.data.vertices))) 524 | 525 | id_ = "{!s}_{!s}".format(armature.name, object_.name) 526 | input = utils.write_input(id_, 0, "joints", "JOINT") 527 | vertex_weights.appendChild(input) 528 | input = utils.write_input(id_, 1, "weights", "WEIGHT") 529 | vertex_weights.appendChild(input) 530 | 531 | vcount = self._doc.createElement("vcount") 532 | vcount_text = self._doc.createTextNode(vertex_groups_lengths) 533 | vcount.appendChild(vcount_text) 534 | vertex_weights.appendChild(vcount) 535 | 536 | v = self._doc.createElement("v") 537 | v_text = self._doc.createTextNode(vw) 538 | v.appendChild(v_text) 539 | vertex_weights.appendChild(v) 540 | 541 | skin_node.appendChild(vertex_weights) 542 | 543 | # ----------------------------------------------------------------------------- 544 | # Library Animation and Clips: --> Animations, F-Curves 545 | # ----------------------------------------------------------------------------- 546 | 547 | def _export_library_animation_clips_and_animations(self, parent_element): 548 | libanmcl = self._doc.createElement("library_animation_clips") 549 | libanm = self._doc.createElement("library_animations") 550 | parent_element.appendChild(libanmcl) 551 | parent_element.appendChild(libanm) 552 | 553 | # --------------------------------------------------------------------- 554 | # Library Visual Scene: --> Skeleton and _Phys bones, Bone 555 | # Transformations, and Instance URL (_boneGeometry) and extras. 556 | # --------------------------------------------------------------------- 557 | 558 | def _export_library_visual_scenes(self, parent_element): 559 | current_element = self._doc.createElement("library_visual_scenes") 560 | visual_scene = self._doc.createElement("visual_scene") 561 | visual_scene.setAttribute("id", "scene") 562 | visual_scene.setAttribute("name", "scene") 563 | current_element.appendChild(visual_scene) 564 | parent_element.appendChild(current_element) 565 | 566 | if utils.get_mesh_export_nodes(self._config.export_selected_nodes): 567 | if utils.are_duplicate_nodes(): 568 | message = "Duplicate Node Names" 569 | bpy.ops.bcry.display_error('INVOKE_DEFAULT', message=message) 570 | 571 | for group in utils.get_mesh_export_nodes( 572 | self._config.export_selected_nodes): 573 | self._write_export_node(group, visual_scene) 574 | else: 575 | pass # TODO: Handle No Export Nodes Error 576 | 577 | def _write_export_node(self, group, visual_scene): 578 | if not self._config.export_for_lumberyard: 579 | node_name = "CryExportNode_{}".format(utils.get_node_name(group)) 580 | node = self._doc.createElement("node") 581 | node.setAttribute("id", node_name) 582 | node.setIdAttribute("id") 583 | else: 584 | node_name = "{}".format(utils.get_node_name(group)) 585 | node = self._doc.createElement("node") 586 | node.setAttribute("id", node_name) 587 | node.setAttribute("LumberyardExportNode", "1") 588 | node.setIdAttribute("id") 589 | 590 | root_objects = [] 591 | for object_ in group.objects: 592 | if utils.is_visual_scene_node_writed(object_, group): 593 | root_objects.append(object_) 594 | 595 | node = self._write_visual_scene_node(root_objects, node, group) 596 | 597 | extra = self._create_cryengine_extra(group) 598 | node.appendChild(extra) 599 | visual_scene.appendChild(node) 600 | 601 | def _write_visual_scene_node(self, objects, parent_node, group): 602 | for object_ in objects: 603 | if (object_.type == "MESH" or object_.type == 'EMPTY') \ 604 | and not utils.is_fakebone(object_) \ 605 | and not utils.is_lod_geometry(object_) \ 606 | and not utils.is_there_a_parent_releation(object_, group): 607 | prop_name = object_.name 608 | node_type = utils.get_node_type(group) 609 | if node_type in ('chr', 'skin'): 610 | prop_name = join( 611 | object_.name, self._create_properties_name( 612 | object_, group)) 613 | node = self._doc.createElement("node") 614 | node.setAttribute("id", prop_name) 615 | node.setAttribute("name", prop_name) 616 | node.setIdAttribute("id") 617 | 618 | self._write_transforms(object_, node) 619 | 620 | if not utils.is_dummy(object_): 621 | ALLOWED_NODE_TYPES = ('cgf', 'cga', 'chr', 'skin') 622 | if node_type in ALLOWED_NODE_TYPES: 623 | instance = self._create_instance(group, object_) 624 | if instance is not None: 625 | node.appendChild(instance) 626 | 627 | udp_extra = self._create_user_defined_property(object_) 628 | if udp_extra is not None: 629 | node.appendChild(udp_extra) 630 | 631 | parent_node.appendChild(node) 632 | 633 | if utils.is_has_lod(object_): 634 | sub_node = node 635 | for lod in utils.get_lod_geometries(object_): 636 | sub_node = self._write_lods(lod, sub_node, group) 637 | 638 | if node_type in ('chr', 'skin') and object_.parent \ 639 | and object_.parent.type == "ARMATURE": 640 | armature = object_.parent 641 | self._write_bone_list([utils.get_root_bone( 642 | armature)], object_, parent_node, group) 643 | 644 | armature_physic = utils.get_armature_physic(armature) 645 | if armature_physic: 646 | self._write_bone_list([utils.get_root_bone( 647 | armature_physic)], armature_physic, parent_node, group) 648 | else: 649 | self._write_child_objects(object_, node, group) 650 | 651 | return parent_node 652 | 653 | def _write_child_objects(self, parent_object, parent_node, group): 654 | for child_object in parent_object.children: 655 | if utils.is_lod_geometry(child_object): 656 | continue 657 | if not utils.is_object_in_group(child_object, group): 658 | continue 659 | 660 | prop_name = child_object.name 661 | node_type = utils.get_node_type(group) 662 | node = self._doc.createElement("node") 663 | node.setAttribute("id", prop_name) 664 | node.setAttribute("name", prop_name) 665 | node.setIdAttribute("id") 666 | 667 | self._write_transforms(child_object, node) 668 | 669 | ALLOWED_NODE_TYPES = ('cgf', 'cga', 'chr', 'skin') 670 | if utils.get_node_type(group) in ALLOWED_NODE_TYPES: 671 | instance = self._create_instance(group, child_object) 672 | if instance is not None: 673 | node.appendChild(instance) 674 | 675 | udp_extra = self._create_user_defined_property(child_object) 676 | if udp_extra is not None: 677 | node.appendChild(udp_extra) 678 | 679 | self._write_child_objects(child_object, node, group) 680 | 681 | parent_node.appendChild(node) 682 | 683 | return parent_node 684 | 685 | def _write_lods(self, object_, parent_node, group): 686 | # prop_name = object_.name 687 | prop_name = utils.changed_lod_name(object_.name) 688 | node_type = utils.get_node_type(group) 689 | if node_type in ('chr', 'skin'): 690 | prop_name = join(object_.name, 691 | self._create_properties_name(object_, group)) 692 | node = self._doc.createElement("node") 693 | node.setAttribute("id", prop_name) 694 | node.setAttribute("name", prop_name) 695 | node.setIdAttribute("id") 696 | 697 | self._write_transforms(object_, node) 698 | 699 | ALLOWED_NODE_TYPES = ('cgf', 'cga', 'chr', 'skin') 700 | if utils.get_node_type(group) in ALLOWED_NODE_TYPES: 701 | instance = self._create_instance(group, object_) 702 | if instance is not None: 703 | node.appendChild(instance) 704 | 705 | udp_extra = self._create_user_defined_property(object_) 706 | if udp_extra is not None: 707 | node.appendChild(udp_extra) 708 | 709 | parent_node.appendChild(node) 710 | 711 | return node 712 | 713 | def _write_bone_list(self, bones, object_, parent_node, group): 714 | scene = bpy.context.scene 715 | bone_names = [] 716 | 717 | for bone in bones: 718 | props_name = self._create_properties_name(bone, group) 719 | props_ik = self._create_ik_properties(bone, object_) 720 | bone_name = join(bone.name, props_name, props_ik) 721 | bone_names.append(bone_name) 722 | 723 | node = self._doc.createElement("node") 724 | node.setAttribute("id", bone_name) 725 | node.setAttribute("name", bone_name) 726 | node.setIdAttribute("id") 727 | 728 | fakebone = utils.get_fakebone(bone.name) 729 | if fakebone is not None: 730 | self._write_transforms(fakebone, node) 731 | 732 | bone_geometry = utils.get_bone_geometry(bone) 733 | if bone_geometry is not None: 734 | geo_name = utils.get_geometry_name(group, bone_geometry) 735 | instance = self._create_bone_instance( 736 | bone_geometry, geo_name) 737 | node.appendChild(instance) 738 | 739 | extra = self._create_physic_proxy_for_bone( 740 | object_.parent, bone) 741 | if extra is not None: 742 | node.appendChild(extra) 743 | 744 | elif utils.is_physic_bone(bone): 745 | bone_geometry = utils.get_bone_geometry(bone) 746 | if fakebone is not None: 747 | self._write_transforms(fakebone, node) 748 | 749 | parent_node.appendChild(node) 750 | 751 | if bone.children: 752 | self._write_bone_list(bone.children, object_, node, group) 753 | 754 | def _create_bone_instance(self, bone_geometry, geometry_name): 755 | instance = None 756 | 757 | instance = self._doc.createElement("instance_geometry") 758 | instance.setAttribute("url", "#{}".format(geometry_name)) 759 | bm = self._doc.createElement("bind_material") 760 | tc = self._doc.createElement("technique_common") 761 | 762 | for mat in bone_geometry.material_slots: 763 | im = self._doc.createElement("instance_material") 764 | im.setAttribute("symbol", mat.name) 765 | im.setAttribute("target", "#{}".format(mat.name)) 766 | bvi = self._doc.createElement("bind_vertex_input") 767 | bvi.setAttribute("semantic", "UVMap") 768 | bvi.setAttribute("input_semantic", "TEXCOORD") 769 | bvi.setAttribute("input_set", "0") 770 | im.appendChild(bvi) 771 | tc.appendChild(im) 772 | 773 | bm.appendChild(tc) 774 | instance.appendChild(bm) 775 | 776 | return instance 777 | 778 | def _create_physic_proxy_for_bone(self, object_, bone): 779 | extra = None 780 | try: 781 | bonePhys = object_.pose.bones[bone.name]['phys_proxy'] 782 | bcPrint(bone.name + " physic proxy is " + bonePhys) 783 | 784 | extra = self._doc.createElement("extra") 785 | techcry = self._doc.createElement("technique") 786 | techcry.setAttribute("profile", "CryEngine") 787 | prop2 = self._doc.createElement("properties") 788 | 789 | cryprops = self._doc.createTextNode(bonePhys) 790 | prop2.appendChild(cryprops) 791 | techcry.appendChild(prop2) 792 | extra.appendChild(techcry) 793 | except: 794 | pass 795 | 796 | return extra 797 | 798 | def _write_transforms(self, object_, node): 799 | trans = self._create_translation_node(object_) 800 | rotx, roty, rotz = self._create_rotation_node(object_) 801 | scale = self._create_scale_node(object_) 802 | 803 | node.appendChild(trans) 804 | node.appendChild(rotx) 805 | node.appendChild(roty) 806 | node.appendChild(rotz) 807 | node.appendChild(scale) 808 | 809 | def _create_translation_node(self, object_): 810 | trans = self._doc.createElement("translate") 811 | trans.setAttribute("sid", "translation") 812 | trans_text = self._doc.createTextNode("{:f} {:f} {:f}".format( 813 | * object_.location)) 814 | trans.appendChild(trans_text) 815 | 816 | return trans 817 | 818 | def _create_rotation_node(self, object_): 819 | rotz = self._write_rotation( 820 | "z", "0 0 1 {:f}", object_.rotation_euler[2]) 821 | roty = self._write_rotation( 822 | "y", "0 1 0 {:f}", object_.rotation_euler[1]) 823 | rotx = self._write_rotation( 824 | "x", "1 0 0 {:f}", object_.rotation_euler[0]) 825 | 826 | return rotz, roty, rotx 827 | 828 | def _write_rotation(self, axis, textFormat, rotation): 829 | rot = self._doc.createElement("rotate") 830 | rot.setAttribute("sid", "rotation_{}".format(axis)) 831 | rot_text = self._doc.createTextNode(textFormat.format( 832 | rotation * utils.to_degrees)) 833 | rot.appendChild(rot_text) 834 | 835 | return rot 836 | 837 | def _create_scale_node(self, object_): 838 | scale = self._doc.createElement("scale") 839 | scale.setAttribute("sid", "scale") 840 | scale_text = self._doc.createTextNode( 841 | utils.floats_to_string(object_.scale, " ", "%s")) 842 | scale.appendChild(scale_text) 843 | 844 | return scale 845 | 846 | def _create_instance(self, group, object_): 847 | armature = utils.get_armature_for_object(object_) 848 | node_type = utils.get_node_type(group) 849 | instance = None 850 | if armature and node_type in ('chr', 'skin'): 851 | instance = self._doc.createElement("instance_controller") 852 | # This binds the mesh object to the armature in control of it 853 | instance.setAttribute("url", "#{!s}_{!s}".format( 854 | armature.name, 855 | object_.name)) 856 | elif object_.name[:6] != "_joint" and object_.type == "MESH": 857 | instance = self._doc.createElement("instance_geometry") 858 | instance.setAttribute( 859 | "url", "#{!s}".format( 860 | utils.get_geometry_name( 861 | group, object_))) 862 | 863 | if instance is not None: 864 | bind_material = self._create_bind_material(object_) 865 | instance.appendChild(bind_material) 866 | return instance 867 | 868 | def _create_bind_material(self, object_): 869 | bind_material = self._doc.createElement('bind_material') 870 | technique_common = self._doc.createElement('technique_common') 871 | 872 | for material, materialname in self._m_exporter.get_materials_for_object( 873 | object_).items(): 874 | instance_material = self._doc.createElement( 875 | 'instance_material') 876 | instance_material.setAttribute('symbol', materialname) 877 | instance_material.setAttribute('target', '#{!s}'.format( 878 | materialname)) 879 | 880 | technique_common.appendChild(instance_material) 881 | 882 | bind_material.appendChild(technique_common) 883 | 884 | return bind_material 885 | 886 | def _create_cryengine_extra(self, node): 887 | extra = self._doc.createElement("extra") 888 | technique = self._doc.createElement("technique") 889 | technique.setAttribute("profile", "CryEngine") 890 | properties = self._doc.createElement("properties") 891 | 892 | ALLOWED_NODE_TYPES = ("cgf", "cga", "chr", "skin") 893 | 894 | if utils.is_export_node(node): 895 | node_type = utils.get_node_type(node) 896 | if node_type in ALLOWED_NODE_TYPES: 897 | prop = self._doc.createTextNode( 898 | "fileType={}".format(node_type)) 899 | properties.appendChild(prop) 900 | if not self._config.merge_all_nodes: 901 | prop = self._doc.createTextNode("DoNotMerge") 902 | properties.appendChild(prop) 903 | 904 | prop = self._doc.createTextNode("UseCustomNormals") 905 | properties.appendChild(prop) 906 | 907 | if self._config.vcloth_pre_process and node_type == 'skin': 908 | prop = self._doc.createTextNode("VClothPreProcess") 909 | properties.appendChild(prop) 910 | 911 | prop = self._doc.createTextNode("CustomExportPath=") 912 | properties.appendChild(prop) 913 | else: 914 | if not node.rna_type.id_data.items(): 915 | return 916 | 917 | technique.appendChild(properties) 918 | 919 | extra.appendChild(technique) 920 | extra.appendChild(self._create_xsi_profile(node)) 921 | 922 | return extra 923 | 924 | def _create_xsi_profile(self, node): 925 | technique_xsi = self._doc.createElement("technique") 926 | technique_xsi.setAttribute("profile", "XSI") 927 | 928 | xsi_custom_p_set = self._doc.createElement("XSI_CustomPSet") 929 | xsi_custom_p_set.setAttribute("name", "ExportProperties") 930 | 931 | propagation = self._doc.createElement("propagation") 932 | propagation.appendChild(self._doc.createTextNode("NODE")) 933 | xsi_custom_p_set.appendChild(propagation) 934 | 935 | type_node = self._doc.createElement("type") 936 | type_node.appendChild( 937 | self._doc.createTextNode("CryExportNodeProperties")) 938 | xsi_custom_p_set.appendChild(type_node) 939 | 940 | xsi_parameter = self._doc.createElement("XSI_Parameter") 941 | xsi_parameter.setAttribute("id", "FileType") 942 | xsi_parameter.setAttribute("type", "Integer") 943 | xsi_parameter.setAttribute("value", utils.get_xsi_filetype_value(node)) 944 | xsi_custom_p_set.appendChild(xsi_parameter) 945 | 946 | xsi_parameter = self._doc.createElement("XSI_Parameter") 947 | xsi_parameter.setAttribute("id", "Filename") 948 | xsi_parameter.setAttribute("type", "Text") 949 | xsi_parameter.setAttribute("value", utils.get_node_name(node)) 950 | xsi_custom_p_set.appendChild(xsi_parameter) 951 | 952 | xsi_parameter = self._doc.createElement("XSI_Parameter") 953 | xsi_parameter.setAttribute("id", "Exportable") 954 | xsi_parameter.setAttribute("type", "Boolean") 955 | xsi_parameter.setAttribute("value", "1") 956 | xsi_custom_p_set.appendChild(xsi_parameter) 957 | 958 | xsi_parameter = self._doc.createElement("XSI_Parameter") 959 | xsi_parameter.setAttribute("id", "MergeObjects") 960 | xsi_parameter.setAttribute("type", "Boolean") 961 | xsi_parameter.setAttribute("value", 962 | str(int(self._config.merge_all_nodes))) 963 | xsi_custom_p_set.appendChild(xsi_parameter) 964 | 965 | technique_xsi.appendChild(xsi_custom_p_set) 966 | 967 | return technique_xsi 968 | 969 | def _create_user_defined_property(self, object_): 970 | udp_buffer = "" 971 | for prop in object_.rna_type.id_data.items(): 972 | if prop: 973 | prop_name = prop[0] 974 | if udp.is_user_defined_property(prop_name): 975 | if isinstance(prop[1], str): 976 | udp_buffer += "{!s}\n".format(prop[1]) 977 | else: 978 | udp_buffer += "{!s}={!s}\n".format(prop[0], prop[1]) 979 | 980 | if udp_buffer or utils.is_dummy(object_): 981 | extra = self._doc.createElement("extra") 982 | technique = self._doc.createElement("technique") 983 | technique.setAttribute("profile", "CryEngine") 984 | properties = self._doc.createElement("properties") 985 | buffer = self._doc.createTextNode(udp_buffer) 986 | properties.appendChild(buffer) 987 | technique.appendChild(properties) 988 | if utils.is_dummy(object_): 989 | helper = self._create_helper_for_dummy(object_) 990 | technique.appendChild(helper) 991 | extra.appendChild(technique) 992 | 993 | return extra 994 | else: 995 | return None 996 | 997 | def _create_helper_for_dummy(self, object_): 998 | x1, y1, z1, x2, y2, z2 = utils.get_bounding_box(object_) 999 | 1000 | min = self._doc.createElement("bound_box_min") 1001 | min_text = self._doc.createTextNode( 1002 | "{:f} {:f} {:f}".format(x1, y1, z1)) 1003 | min.appendChild(min_text) 1004 | 1005 | max = self._doc.createElement("bound_box_max") 1006 | max_text = self._doc.createTextNode( 1007 | "{:f} {:f} {:f}".format(x2, y2, z2)) 1008 | max.appendChild(max_text) 1009 | 1010 | helper = self._doc.createElement("helper") 1011 | helper.setAttribute("type", "dummy") 1012 | helper.appendChild(min) 1013 | helper.appendChild(max) 1014 | 1015 | return helper 1016 | 1017 | def _create_properties_name(self, bone, group): 1018 | bone_name = bone.name.replace("__", "*") 1019 | node_name = utils.get_node_name(group) 1020 | props_name = '%{!s}%--PRprops_name={!s}'.format(node_name, bone_name) 1021 | 1022 | return props_name 1023 | 1024 | def _create_ik_properties(self, bone, object_): 1025 | props = "" 1026 | if utils.is_physic_bone(bone): 1027 | 1028 | armature_object = bpy.data.objects[object_.name[:-5]] 1029 | pose_bone = armature_object.pose.bones[bone.name[:-5]] 1030 | 1031 | xIK, yIK, zIK = udp.get_bone_ik_max_min(pose_bone) 1032 | 1033 | damping, spring, spring_tension = udp.get_bone_ik_properties( 1034 | pose_bone) 1035 | 1036 | props = join( 1037 | xIK, 1038 | '_xdamping={}'.format(damping[1]), 1039 | '_xspringangle={}'.format(spring[1]), 1040 | '_xspringtension={}'.format(spring_tension[1]), 1041 | 1042 | yIK, 1043 | '_ydamping={}'.format(damping[0]), 1044 | '_yspringangle={}'.format(spring[0]), 1045 | '_yspringtension={}'.format(spring_tension[0]), 1046 | 1047 | zIK, 1048 | '_zdamping={}'.format(damping[2]), 1049 | '_zspringangle={}'.format(spring[2]), 1050 | '_zspringtension={}'.format(spring_tension[2]) 1051 | ) 1052 | 1053 | return props 1054 | 1055 | def _export_scene(self, parent_element): 1056 | scene = self._doc.createElement("scene") 1057 | instance_visual_scene = self._doc.createElement("instance_visual_scene") 1058 | instance_visual_scene.setAttribute("url", "#scene") 1059 | scene.appendChild(instance_visual_scene) 1060 | parent_element.appendChild(scene) 1061 | 1062 | 1063 | def write_scripts(self, config): 1064 | filepath = bpy.path.ensure_ext(config.filepath, ".dae") 1065 | if not config.make_chrparams and not config.make_cdf: 1066 | return 1067 | 1068 | dae_path = utils.get_absolute_path_for_rc(filepath) 1069 | output_path = os.path.dirname(dae_path) 1070 | 1071 | for chr_name in utils.get_chr_names(self._config.export_selected_nodes): 1072 | if config.make_chrparams: 1073 | filepath = "{}/{}.chrparams".format(output_path, chr_name) 1074 | contents = utils.generate_file_contents("chrparams") 1075 | utils.generate_xml(filepath, contents) 1076 | if config.make_cdf: 1077 | filepath = "{}/{}.cdf".format(output_path, chr_name) 1078 | contents = utils.generate_file_contents("cdf") 1079 | utils.generate_xml(filepath, contents) 1080 | 1081 | 1082 | def save(config): 1083 | # prevent wasting time for exporting if RC was not found 1084 | if not config.disable_rc and not os.path.isfile(config.rc_path): 1085 | raise exceptions.NoRcSelectedException 1086 | 1087 | exporter = CrytekDaeExporter(config) 1088 | exporter.export() 1089 | 1090 | 1091 | def register(): 1092 | bpy.utils.register_class(CrytekDaeExporter) 1093 | # bpy.utils.register_class(TriangulateMeError) 1094 | # bpy.utils.register_class(Error) 1095 | 1096 | 1097 | def unregister(): 1098 | bpy.utils.unregister_class(CrytekDaeExporter) 1099 | # bpy.utils.unregister_class(TriangulateMeError) 1100 | # bpy.utils.unregister_class(Error) 1101 | 1102 | 1103 | if __name__ == "__main__": 1104 | register() 1105 | 1106 | # test call 1107 | bpy.ops.export_mesh.crytekdae('INVOKE_DEFAULT') 1108 | -------------------------------------------------------------------------------- /io_bcry_exporter/utils.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # Name: utils.py 3 | # Purpose: Utility functions for use throughout the add-on 4 | # 5 | # Author: Özkan Afacan, 6 | # Angelo J. Miner, Mikołaj Milej, Daniel White, 7 | # Oscar Martin Garcia, Duo Oratar, David Marcelis 8 | # 9 | # Created: 23/02/2012 10 | # Copyright: (c) Angelo J. Miner 2012 11 | # Copyright: (c) Özkan Afacan 2016 12 | # Licence: GPLv2+ 13 | # ------------------------------------------------------------------------------ 14 | 15 | # 16 | 17 | 18 | if "bpy" in locals(): 19 | import importlib 20 | importlib.reload(material_utils) 21 | importlib.reload(exceptions) 22 | else: 23 | import bpy 24 | from . import material_utils, exceptions 25 | 26 | 27 | import fnmatch 28 | import math 29 | import os 30 | import random 31 | import re 32 | import subprocess 33 | import sys 34 | import time 35 | import xml.dom.minidom 36 | from xml.dom.minidom import Document, parseString 37 | 38 | import bmesh 39 | import bpy 40 | import math 41 | from mathutils import Matrix, Vector 42 | 43 | from .outpipe import bcPrint 44 | 45 | # Globals: 46 | to_degrees = 180.0 / math.pi 47 | 48 | 49 | # ------------------------------------------------------------------------------ 50 | # Conversions: 51 | # ------------------------------------------------------------------------------ 52 | 53 | def transform_bone_matrix(bone): 54 | if not bone.parent: 55 | return Matrix() 56 | 57 | i1 = Vector((1.0, 0.0, 0.0)) 58 | i2 = Vector((0.0, 1.0, 0.0)) 59 | i3 = Vector((0.0, 0.0, 1.0)) 60 | 61 | x_axis = bone.y_axis 62 | y_axis = bone.x_axis 63 | z_axis = -bone.z_axis 64 | 65 | row_x = Vector((x_axis @ i1, x_axis @ i2, x_axis @ i3)) 66 | row_y = Vector((y_axis @ i1, y_axis @ i2, y_axis @ i3)) 67 | row_z = Vector((z_axis @ i1, z_axis @ i2, z_axis @ i3)) 68 | 69 | trans_matrix = Matrix((row_x, row_y, row_z)) 70 | 71 | location = trans_matrix @ bone.matrix.translation 72 | bone_matrix = trans_matrix.to_4x4() 73 | bone_matrix.translation = -location 74 | 75 | return bone_matrix 76 | 77 | 78 | def transform_animation_matrix(matrix): 79 | eu = matrix.to_euler() 80 | eu.rotate_axis('Z', math.pi / 2.0) 81 | eu.rotate_axis('X', math.pi) 82 | 83 | new_matrix = eu.to_matrix() 84 | new_matrix = new_matrix.to_4x4() 85 | new_matrix.translation = matrix.translation 86 | 87 | return new_matrix 88 | 89 | 90 | def frame_to_time(frame): 91 | fps_base = bpy.context.scene.render.fps_base 92 | fps = bpy.context.scene.render.fps 93 | return fps_base * frame / fps 94 | 95 | 96 | def matrix_to_string(matrix): 97 | return str(matrix_to_array(matrix)) 98 | 99 | 100 | def floats_to_string(floats, separator=" ", precision="%.6f"): 101 | return separator.join(precision % x for x in floats) 102 | 103 | 104 | def strings_to_string(strings, separator=" "): 105 | return separator.join(string for string in strings) 106 | 107 | 108 | def matrix_to_array(matrix): 109 | array = [] 110 | for row in matrix: 111 | array.extend(row) 112 | 113 | return array 114 | 115 | 116 | def write_matrix(matrix, node): 117 | doc = Document() 118 | for row in matrix: 119 | row_string = floats_to_string(row) 120 | node.appendChild(doc.createTextNode(row_string)) 121 | 122 | 123 | def join(*items): 124 | strings = [] 125 | for item in items: 126 | strings.append(str(item)) 127 | return "".join(strings) 128 | 129 | 130 | # ------------------------------------------------------------------------------ 131 | # XSI Functions: 132 | # ------------------------------------------------------------------------------ 133 | 134 | def get_xsi_filetype_value(node): 135 | node_type = get_node_type(node) 136 | if node_type == 'cgf': 137 | return "1" 138 | elif node_type == 'chr': 139 | return "4" 140 | elif node_type == 'cga': 141 | return "18" 142 | elif node_type == 'skin': 143 | return "32" 144 | elif node_type == 'i_caf' or node_type == 'anm': 145 | return "64" 146 | else: 147 | return "1" 148 | 149 | 150 | # ------------------------------------------------------------------------------ 151 | # Geometry Functions: 152 | # ------------------------------------------------------------------------------ 153 | 154 | def get_geometry_name(group, object_): 155 | node_name = get_node_name(group) 156 | if is_bone_geometry(object_): 157 | return "{}_{}".format(node_name, object_.name) 158 | elif is_lod_geometry(object_): 159 | return "{}_{}".format(node_name, changed_lod_name(object_.name)) 160 | else: 161 | return "{}_{}_geometry".format(node_name, object_.name) 162 | 163 | 164 | def get_bmesh(object_, apply_modifiers=False): 165 | set_active(object_) 166 | 167 | # bmesh may be gotten only in edit mode for active object. 168 | # Unfortunately Blender goes in edit mode just objects 169 | # in which first layer with bpy.ops.object.mode_set(mode='EDIT') 170 | # So we must temporarily activate first layer for objects which it is not 171 | # already in first layer. Also scene first layer must be active. 172 | # That lacking related with Blender, if it will fix in future that 173 | # code will be clean. 174 | 175 | bcry_split_modifier(object_) 176 | 177 | depsgraph = bpy.context.evaluated_depsgraph_get() 178 | bpy.ops.object.mode_set(mode='EDIT') 179 | 180 | if apply_modifiers: 181 | object_eval = object_.evaluated_get(depsgraph) 182 | mesh = object_eval.to_mesh() 183 | bmesh_ = bmesh.new() 184 | bmesh_.from_mesh(mesh) 185 | 186 | else: 187 | mesh = object_.to_mesh() 188 | bmesh_ = bmesh.new() 189 | bmesh_.from_mesh(mesh) 190 | 191 | return bmesh_ 192 | 193 | 194 | def clear_bmesh(object_, bmesh_): 195 | bpy.ops.object.mode_set(mode='OBJECT') 196 | remove_bcry_split_modifier(object_) 197 | object_.to_mesh_clear() 198 | 199 | 200 | def bcry_split_modifier(object_): 201 | if object_.data.use_auto_smooth: 202 | modifier_unique_name = 'BCRY_EDGE_SPLIT' 203 | 204 | object_.modifiers.new(modifier_unique_name, 'EDGE_SPLIT') 205 | edge_split_modifier = object_.modifiers.get(modifier_unique_name) 206 | edge_split_modifier.use_edge_angle = True 207 | edge_split_modifier.use_edge_sharp = True 208 | edge_split_modifier.split_angle = object_.data.auto_smooth_angle 209 | 210 | object_.data.use_auto_smooth = False 211 | 212 | 213 | def remove_bcry_split_modifier(object_): 214 | modifier_unique_name = 'BCRY_EDGE_SPLIT' 215 | 216 | edge_split_modifier = object_.modifiers.get(modifier_unique_name) 217 | if edge_split_modifier: 218 | object_.data.use_auto_smooth = True 219 | object_.modifiers.remove(edge_split_modifier) 220 | 221 | 222 | def get_tessfaces(bmesh_): 223 | tessfaces = [] 224 | tfs = bmesh_.calc_loop_triangles() 225 | 226 | for face in bmesh_.faces: 227 | # initialize tessfaces array 228 | tessfaces.append([]) 229 | 230 | for tf in tfs: 231 | vert_list = [] 232 | for loop in tf: 233 | # tessfaces[loop.face.index].append(loop.vert.index) 234 | vert_list.append(loop.vert.index) 235 | 236 | tessfaces[tf[0].face.index].append(vert_list) 237 | 238 | return tessfaces 239 | 240 | 241 | def get_custom_normals(bmesh_, use_edge_angle, split_angle): 242 | float_normals = [] 243 | 244 | for face in bmesh_.faces: 245 | if not face.smooth: 246 | for vertex in face.verts: 247 | float_normals.extend(face.normal.normalized()) 248 | else: 249 | for vertex in face.verts: 250 | v_normals = [[face.normal.normalized(), face.calc_area()]] 251 | for link_face in vertex.link_faces: 252 | if face.index == link_face.index: 253 | continue 254 | if link_face.smooth: 255 | if not use_edge_angle: 256 | v_normals.append([link_face.normal.normalized(), link_face.calc_area()]) 257 | 258 | elif use_edge_angle: 259 | face_angle = face.normal.normalized().dot(link_face.normal.normalized()) 260 | face_angle = min(1.0, max(face_angle, -1.0)) 261 | face_angle = math.acos(face_angle) 262 | if face_angle < split_angle: 263 | v_normals.append([link_face.normal.normalized(), link_face.calc_area()]) 264 | 265 | smooth_normal = Vector() 266 | area_sum = 0 267 | for vertex_normal in v_normals: 268 | area_sum += vertex_normal[1] 269 | for vertex_normal in v_normals: 270 | if area_sum: 271 | smooth_normal += vertex_normal[0] * \ 272 | (vertex_normal[1] / area_sum) 273 | float_normals.extend(smooth_normal.normalized()) 274 | 275 | return float_normals 276 | 277 | 278 | def get_normal_array(bmesh_, use_edge_angle, use_edge_sharp, split_angle): 279 | float_normals = [] 280 | 281 | for face in bmesh_.faces: 282 | if not face.smooth: 283 | for vertex in face.verts: 284 | float_normals.extend(face.normal.normalized()) 285 | else: 286 | for vertex in face.verts: 287 | v_normals = [[face.normal.normalized(), face.calc_area()]] 288 | for link_face in vertex.link_faces: 289 | if face.index == link_face.index: 290 | continue 291 | if link_face.smooth: 292 | if not use_edge_angle and not use_edge_sharp: 293 | v_normals.append([link_face.normal.normalized(), link_face.calc_area()]) 294 | 295 | elif use_edge_angle and not use_edge_sharp: 296 | face_angle = face.normal.normalized().dot(link_face.normal.normalized()) 297 | face_angle = min(1.0, max(face_angle, -1.0)) 298 | face_angle = math.acos(face_angle) 299 | if face_angle < split_angle: 300 | v_normals.append([link_face.normal.normalized(), link_face.calc_area()]) 301 | 302 | elif use_edge_sharp and not use_edge_angle: 303 | is_neighbor_face = False 304 | for edge in vertex.link_edges: 305 | if (edge in face.edges) and (edge in link_face.edges): 306 | is_neighbor_face = True 307 | if edge.smooth: 308 | v_normals.append([link_face.normal.normalized(), link_face.calc_area()]) 309 | 310 | if not is_neighbor_face: 311 | if check_sharp_edges(vertex, face, None, link_face): 312 | v_normals.append([link_face.normal.normalized(), link_face.calc_area()]) 313 | 314 | elif use_edge_angle and use_edge_sharp: 315 | face_angle = face.normal.normalized().dot(link_face.normal.normalized()) 316 | face_angle = min(1.0, max(face_angle, -1.0)) 317 | face_angle = math.acos(face_angle) 318 | if face_angle < split_angle: 319 | is_neighbor_face = False 320 | for edge in vertex.link_edges: 321 | if (edge in face.edges) and (edge in link_face.edges): 322 | is_neighbor_face = True 323 | if edge.smooth: 324 | v_normals.append([link_face.normal.normalized(), link_face.calc_area()]) 325 | # print(link_face.normal.normalized(), link_face.calc_area()) 326 | 327 | if not is_neighbor_face: 328 | if check_sharp_edges(vertex, face, None, link_face): 329 | v_normals.append([link_face.normal.normalized(), link_face.calc_area()]) 330 | 331 | smooth_normal = Vector() 332 | area_sum = 0 333 | for vertex_normal in v_normals: 334 | area_sum += vertex_normal[1] 335 | for vertex_normal in v_normals: 336 | if area_sum: 337 | smooth_normal += vertex_normal[0] * (vertex_normal[1] / area_sum) 338 | float_normals.extend(smooth_normal.normalized()) 339 | 340 | return float_normals 341 | 342 | 343 | def check_sharp_edges(vertex, current_face, previous_face, target_face): 344 | for trans_edge in current_face.edges: 345 | if trans_edge in vertex.link_edges: 346 | for neighbor_face in trans_edge.link_faces: 347 | if neighbor_face == current_face or neighbor_face == previous_face: 348 | continue 349 | if trans_edge.smooth: 350 | if neighbor_face == target_face: 351 | return True 352 | else: 353 | new_previous_face = current_face 354 | # print(neighbor_face, target_face, current_face, previous_face) 355 | return check_sharp_edges(vertex, neighbor_face, new_previous_face, target_face) 356 | 357 | return False 358 | 359 | 360 | def get_joint_name(object_, index=1): 361 | joint_name = "$joint{:02d}".format(index) 362 | 363 | for child in object_.children: 364 | if child.name == joint_name: 365 | return get_joint_name(object_, index + 1) 366 | 367 | return joint_name 368 | 369 | 370 | def rebuild_armature(armature): 371 | """ Try to re-create new armature bone to bone 372 | for fix wrong export bone positions and hierarchy""" 373 | old_bones = [] 374 | new_bones = [] 375 | obj_childrens = [] 376 | temp_suffix = "+oldbcry" 377 | 378 | # collect a list of childrens in armature for fix vgroup names 379 | for obj in armature.children: 380 | obj_childrens.append(obj) 381 | 382 | bpy.ops.object.mode_set(mode="EDIT") 383 | 384 | edit_bones = armature.data.edit_bones 385 | bones_count = len(edit_bones) 386 | 387 | # collect a list of old bones 388 | for bone in edit_bones: 389 | old_bones.append(bone) 390 | 391 | # create a list of new bones (empty) 392 | for i in range(bones_count): 393 | new_bone = edit_bones.new(str(i)) 394 | new_bones.append(new_bone) 395 | 396 | for i in range(bones_count): 397 | # copy bones attributes 398 | new_bones[i].head = old_bones[i].head 399 | new_bones[i].tail = old_bones[i].tail 400 | new_bones[i].roll = old_bones[i].roll 401 | new_bones[i].use_connect = old_bones[i].use_connect 402 | new_bones[i].use_deform = old_bones[i].use_deform 403 | old_name = old_bones[i].name 404 | old_bones[i].name = old_bones[i].name + temp_suffix 405 | new_bones[i].name = old_name 406 | 407 | # copy custom properties 408 | if len(old_bones[i].items()) > 0: 409 | for p in old_bones[i].items(): 410 | new_bones[i][p[0]] = p[1] 411 | 412 | for bone in edit_bones: 413 | if bone.name.endswith(temp_suffix): 414 | if bone.parent is not None: 415 | split_name = bone.name.split("+") 416 | temp_name = split_name[0] 417 | split_name = bone.parent.name.split("+") 418 | parent_name = split_name[0] 419 | edit_bones[temp_name].parent = edit_bones[parent_name] 420 | 421 | for i in range(bones_count): 422 | edit_bones.remove(old_bones[i]) 423 | 424 | bpy.ops.object.mode_set(mode="OBJECT") 425 | 426 | if len(obj_childrens) > 0: 427 | for obj in armature.children: 428 | if obj.type == 'MESH': 429 | for v in obj.vertex_groups.values(): 430 | if v.name.endswith(temp_suffix): 431 | split_name = v.name.split("+") 432 | v.name = split_name[0] 433 | 434 | # Rename data_path in assigned Actions through NLA strips 435 | 436 | if len(bpy.data.actions) > 0: 437 | 438 | for action in bpy.data.actions: 439 | for fc in action.fcurves: 440 | if fc.data_path: 441 | if temp_suffix in fc.data_path: 442 | fc.data_path = fc.data_path.replace(temp_suffix, "") 443 | 444 | if fc.group: 445 | if temp_suffix in fc.group.name: 446 | fc.group.name = fc.group.name.replace(temp_suffix, "") 447 | 448 | # ------------------------------------------------------------------------------ 449 | # Path Manipulations: 450 | # ------------------------------------------------------------------------------ 451 | 452 | def get_absolute_path(file_path): 453 | [is_relative, file_path] = strip_blender_path_prefix(file_path) 454 | 455 | if is_relative: 456 | blend_file_path = os.path.dirname(bpy.data.filepath) 457 | file_path = '{}/{}'.format(blend_file_path, file_path) 458 | 459 | return os.path.abspath(file_path) 460 | 461 | 462 | def get_absolute_path_for_rc(file_path): 463 | # 'z:' is for wine (linux, mac) path 464 | # there should be better way to determine it 465 | WINE_DEFAULT_DRIVE_LETTER = 'z:' 466 | 467 | file_path = get_absolute_path(file_path) 468 | 469 | if sys.platform != 'win32': 470 | file_path = '{}{}'.format(WINE_DEFAULT_DRIVE_LETTER, file_path) 471 | 472 | return file_path 473 | 474 | 475 | def get_relative_path(filepath, start=None): 476 | blend_file_directory = os.path.dirname(bpy.data.filepath) 477 | [is_relative_to_blend_file, filepath] = strip_blender_path_prefix(filepath) 478 | 479 | if not start: 480 | if is_relative_to_blend_file: 481 | return filepath 482 | 483 | # path is not relative, so create path relative to blend file. 484 | start = blend_file_directory 485 | 486 | if not start: 487 | raise exceptions.BlendNotSavedException 488 | 489 | else: 490 | # make absolute path to be able make relative to 'start' 491 | if is_relative_to_blend_file: 492 | filepath = os.path.normpath(os.path.join(blend_file_directory, 493 | filepath)) 494 | 495 | return make_relative_path(filepath, start) 496 | 497 | 498 | def strip_blender_path_prefix(path): 499 | is_relative = False 500 | BLENDER_RELATIVE_PATH_PREFIX = '//' 501 | prefix_length = len(BLENDER_RELATIVE_PATH_PREFIX) 502 | 503 | if path.startswith(BLENDER_RELATIVE_PATH_PREFIX): 504 | path = path[prefix_length:] 505 | is_relative = True 506 | 507 | return (is_relative, path) 508 | 509 | 510 | def make_relative_path(filepath, start): 511 | try: 512 | relative_path = os.path.relpath(filepath, start) 513 | return relative_path 514 | 515 | except ValueError: 516 | raise exceptions.TextureAndBlendDiskMismatchException(start, filepath) 517 | 518 | 519 | def get_path_with_new_extension(path, extension): 520 | return '{}.{}'.format(os.path.splitext(path)[0], extension) 521 | 522 | 523 | def strip_extension_from_path(path): 524 | return os.path.splitext(path)[0] 525 | 526 | 527 | def get_extension_from_path(path): 528 | return os.path.splitext(path)[1] 529 | 530 | 531 | def normalize_path(path): 532 | path = path.replace("\\", "/") 533 | 534 | multiple_paths = re.compile("/{2,}") 535 | path = multiple_paths.sub("/", path) 536 | 537 | if path[0] == "/": 538 | path = path[1:] 539 | 540 | if path[-1] == "/": 541 | path = path[:-1] 542 | 543 | return path 544 | 545 | 546 | def build_path(*components): 547 | path = "/".join(components) 548 | path = path.replace("/.", ".") # accounts for extension 549 | return normalize_path(path) 550 | 551 | 552 | def get_filename(path): 553 | path_normalized = normalize_path(path) 554 | components = path_normalized.split("/") 555 | name = os.path.splitext(components[-1])[0] 556 | return name 557 | 558 | 559 | def trim_path_to(path, trim_to): 560 | path_normalized = normalize_path(path) 561 | components = path_normalized.split("/") 562 | for index, component in enumerate(components): 563 | if component == trim_to: 564 | bcPrint("FOUND AN INSTANCE") 565 | break 566 | bcPrint(index) 567 | components_trimmed = components[index:] 568 | bcPrint(components_trimmed) 569 | path_trimmed = build_path(*components_trimmed) 570 | bcPrint(path_trimmed) 571 | return path_trimmed 572 | 573 | 574 | # ------------------------------------------------------------------------------ 575 | # File Clean-Up: 576 | # ------------------------------------------------------------------------------ 577 | 578 | def clean_file(just_selected=False): 579 | for node in get_export_nodes(just_selected): 580 | node_name = get_node_name(node) 581 | nodetype = get_node_type(node) 582 | node_name = replace_invalid_rc_characters(node_name) 583 | node.name = "{}.{}".format(node_name, nodetype) 584 | 585 | for object_ in node.objects: 586 | object_.name = replace_invalid_rc_characters(object_.name) 587 | try: 588 | object_.data.name = replace_invalid_rc_characters( 589 | object_.data.name) 590 | except AttributeError: 591 | pass 592 | if object_.type == "ARMATURE": 593 | for bone in object_.data.bones: 594 | bone.name = replace_invalid_rc_characters(bone.name) 595 | 596 | 597 | def replace_invalid_rc_characters(string): 598 | # Remove leading and trailing spaces. 599 | string.strip() 600 | 601 | # Replace remaining white spaces with double underscores. 602 | string = "__".join(string.split()) 603 | 604 | character_map = { 605 | "a": "àáâå", 606 | "c": "ç", 607 | "e": "èéêë", 608 | "i": "ìíîïı", 609 | "l": "ł", 610 | "n": "ñ", 611 | "o": "òóô", 612 | "u": "ùúû", 613 | "y": "ÿ", 614 | "ss": "ß", 615 | "ae": "äæ", 616 | "oe": "ö", 617 | "ue": "ü" 618 | } # Expand with more individual replacement rules. 619 | 620 | # Individual replacement. 621 | for good, bad in character_map.items(): 622 | for char in bad: 623 | string = string.replace(char, good) 624 | string = string.replace(char.upper(), good.upper()) 625 | 626 | # Remove all remaining non alphanumeric characters except underscores, 627 | # dots, and dollar signs. 628 | string = re.sub("[^.^_^$0-9A-Za-z]", "", string) 629 | 630 | return string 631 | 632 | 633 | def fix_weights(): 634 | for object_ in get_type("skins"): 635 | override = get_3d_context(object_) 636 | try: 637 | bpy.ops.object.vertex_group_normalize_all( 638 | override, lock_active=False) 639 | except: 640 | raise exceptions.BCryException( 641 | "Please fix weightless vertices first.") 642 | bcPrint("Weights Corrected.") 643 | 644 | 645 | # ------------------------------------------------------------------------------ 646 | # Collections: 647 | # ------------------------------------------------------------------------------ 648 | 649 | def get_export_nodes(just_selected=False): 650 | export_nodes = [] 651 | 652 | if just_selected: 653 | return __get_selected_nodes() 654 | 655 | export_nodes_collection = bpy.data.collections.get("cry_export_nodes") 656 | if export_nodes_collection is not None: 657 | 658 | for collection in bpy.data.collections: 659 | if is_export_node(collection) and len(collection.objects) > 0: 660 | export_nodes.append(collection) 661 | 662 | return export_nodes 663 | 664 | 665 | def get_mesh_export_nodes(just_selected=False): 666 | export_nodes = [] 667 | 668 | ALLOWED_NODE_TYPES = ('cgf', 'cga', 'chr', 'skin') 669 | for node in get_export_nodes(just_selected): 670 | if get_node_type(node) in ALLOWED_NODE_TYPES: 671 | export_nodes.append(node) 672 | 673 | return export_nodes 674 | 675 | 676 | def get_chr_node_from_skeleton(armature): 677 | for child in armature.children: 678 | for collection in child.users_collection: 679 | if collection.name.endswith('.chr'): 680 | return collection 681 | 682 | return None 683 | 684 | 685 | def get_chr_object_from_skeleton(armature): 686 | for child in armature.children: 687 | for group in child.users_collection: 688 | if group.name.endswith('.chr'): 689 | return child 690 | 691 | return None 692 | 693 | 694 | def get_chr_names(just_selected=False): 695 | chr_names = [] 696 | 697 | for node in get_export_nodes(just_selected): 698 | if get_node_type(node) == 'chr': 699 | chr_names.append(get_node_name(node)) 700 | 701 | return chr_names 702 | 703 | 704 | def get_animation_export_nodes(just_selected=False): 705 | export_nodes = [] 706 | 707 | if just_selected: 708 | return __get_selected_nodes() 709 | 710 | export_nodes_collection = bpy.data.collections.get("cry_export_nodes") 711 | if export_nodes_collection is not None: 712 | ALLOWED_NODE_TYPES = ('anm', 'i_caf') 713 | for collection in bpy.data.collections: 714 | if is_export_node(collection) and len(collection.objects) > 0: 715 | if get_node_type(collection) in ALLOWED_NODE_TYPES: 716 | export_nodes.append(collection) 717 | 718 | return export_nodes 719 | 720 | 721 | def __get_selected_nodes(): 722 | export_nodes = [] 723 | 724 | for obj in bpy.context.selected_objects: 725 | for group in obj.users_collection: 726 | if is_export_node(group) and group not in export_nodes: 727 | export_nodes.append(group) 728 | 729 | return export_nodes 730 | 731 | 732 | def get_type(type_): 733 | dispatch = { 734 | "objects": __get_objects, 735 | "geometry": __get_geometry, 736 | "controllers": __get_controllers, 737 | "skins": __get_skins, 738 | "fakebones": __get_fakebones, 739 | "bone_geometry": __get_bone_geometry, 740 | } 741 | return list(set(dispatch[type_]())) 742 | 743 | 744 | def __get_objects(): 745 | items = [] 746 | for group in get_export_nodes(): 747 | items.extend(group.objects) 748 | 749 | return items 750 | 751 | 752 | def __get_geometry(): 753 | items = [] 754 | for object_ in get_type("objects"): 755 | if object_.type == "MESH" and not is_fakebone(object_): 756 | items.append(object_) 757 | 758 | return items 759 | 760 | 761 | def __get_controllers(): 762 | items = [] 763 | for object_ in get_type("objects"): 764 | if not (is_bone_geometry(object_) or is_fakebone(object_)): 765 | if object_.parent is not None: 766 | if object_.parent.type == "ARMATURE": 767 | items.append(object_.parent) 768 | 769 | return items 770 | 771 | 772 | def __get_skins(): 773 | items = [] 774 | for object_ in get_type("objects"): 775 | if object_.type == "MESH": 776 | if not (is_bone_geometry(object_) or is_fakebone(object_)): 777 | if object_.parent is not None: 778 | if object_.parent.type == "ARMATURE": 779 | items.append(object_) 780 | 781 | return items 782 | 783 | 784 | def __get_fakebones(): 785 | items = [] 786 | for object_ in bpy.data.objects: 787 | if is_fakebone(object_): 788 | items.append(object_) 789 | 790 | return items 791 | 792 | 793 | def __get_bone_geometry(): 794 | items = [] 795 | for object_ in get_type("objects"): 796 | if is_bone_geometry(object_): 797 | items.append(object_) 798 | 799 | return items 800 | 801 | 802 | # ------------------------------------------------------------------------------ 803 | # Export Nodes: 804 | # ------------------------------------------------------------------------------ 805 | 806 | def is_export_node(node): 807 | extensions = [".cgf", ".cga", ".chr", ".skin", ".anm", ".i_caf"] 808 | for extension in extensions: 809 | if node.name.endswith(extension): 810 | if node.name in bpy.data.collections['cry_export_nodes'].children: 811 | return True 812 | 813 | return False 814 | 815 | 816 | def are_duplicate_nodes(): 817 | node_names = [] 818 | for group in get_export_nodes(): 819 | node_names.append(get_node_name(group)) 820 | unique_node_names = set(node_names) 821 | if len(unique_node_names) < len(node_names): 822 | return True 823 | 824 | 825 | def get_node_name(node): 826 | node_type = get_node_type(node) 827 | return node.name[:-(len(node_type) + 1)] 828 | 829 | 830 | def get_node_type(node): 831 | node_components = node.name.split(".") 832 | return node_components[-1] 833 | 834 | 835 | def is_visual_scene_node_writed(object_, group): 836 | if is_bone_geometry(object_): 837 | return False 838 | if object_.parent is not None and object_.type not in ('MESH', 'EMPTY'): 839 | return False 840 | 841 | return True 842 | 843 | 844 | def is_there_a_parent_releation(object_, group): 845 | while object_.parent: 846 | if is_object_in_group( 847 | object_.parent, 848 | group) and object_.parent.type in ( 849 | 'MESH', 850 | 'EMPTY'): 851 | return True 852 | else: 853 | return is_there_a_parent_releation(object_.parent, group) 854 | 855 | return False 856 | 857 | 858 | def is_object_in_group(object_, group): 859 | for obj in group.objects: 860 | if object_.name == obj.name: 861 | return True 862 | 863 | return False 864 | 865 | 866 | def is_dummy(object_): 867 | return object_.type == 'EMPTY' 868 | 869 | 870 | # ------------------------------------------------------------------------------ 871 | # Fakebones: 872 | # ------------------------------------------------------------------------------ 873 | 874 | 875 | def get_fakebone(bone_name): 876 | return next((fakebone for fakebone in get_type("fakebones") 877 | if fakebone.name == bone_name), None) 878 | 879 | 880 | def is_fakebone(object_): 881 | if object_.get("fakebone") is not None: 882 | return True 883 | else: 884 | return False 885 | 886 | 887 | def add_fakebones(group=None): 888 | '''Add helpers to track bone transforms.''' 889 | scene = bpy.context.scene 890 | 891 | """Set the Master collection(Scene) active, prevent errors when 892 | fakebones doesn't create if current active scene is hide""" 893 | x = bpy.context.view_layer.layer_collection 894 | bpy.context.view_layer.active_layer_collection = x 895 | remove_unused_meshes() 896 | 897 | if group: 898 | for object_ in group.objects: 899 | if object_.type == 'ARMATURE': 900 | armature = object_ 901 | else: 902 | armature = get_armature() 903 | 904 | if armature is None: 905 | return 906 | 907 | skeleton = armature.data 908 | 909 | skeleton.pose_position = 'REST' 910 | time.sleep(0.5) 911 | 912 | scene.frame_set(scene.frame_start) 913 | for pose_bone in armature.pose.bones: 914 | bone_matrix = transform_bone_matrix(pose_bone) 915 | # loc, rot, scl = bone_matrix.decompose() 916 | 917 | bpy.ops.mesh.primitive_cube_add(size=0.01) 918 | fakebone = bpy.context.active_object 919 | fakebone.matrix_world = bone_matrix 920 | fakebone.scale = (1, 1, 1) 921 | fakebone.name = pose_bone.name 922 | fakebone["fakebone"] = "fakebone" 923 | 924 | # set parent 925 | fakebone.parent = armature 926 | fakebone.parent_type = "BONE" 927 | fakebone.parent_bone = pose_bone.name 928 | 929 | fakebone.users_collection[0].objects.unlink(fakebone) 930 | 931 | if group: 932 | group.objects.link(fakebone) 933 | else: 934 | armature.users_collection[0].objects.link(fakebone) 935 | 936 | if group: 937 | if get_node_type(group) == 'i_caf': 938 | process_animation(armature, skeleton) 939 | 940 | 941 | def remove_fakebones(): 942 | '''Select to remove all fakebones from the scene.''' 943 | if len(get_type("fakebones")) == 0: 944 | return 945 | old_mode = bpy.context.mode 946 | if old_mode != 'OBJECT': 947 | bpy.ops.object.mode_set(mode='OBJECT') 948 | deselect_all() 949 | for fakebone in get_type("fakebones"): 950 | fakebone.select_set(True) 951 | bpy.ops.object.delete(use_global=False) 952 | if old_mode != 'OBJECT': 953 | bpy.ops.object.mode_set(mode=old_mode) 954 | 955 | 956 | # ------------------------------------------------------------------------------ 957 | # Animation and Keyframing: 958 | # ------------------------------------------------------------------------------ 959 | 960 | def process_animation(armature, skeleton): 961 | '''Process animation to export.''' 962 | skeleton.pose_position = 'POSE' 963 | time.sleep(0.5) 964 | 965 | location_list, rotation_list = get_keyframes(armature) 966 | set_keyframes(armature, location_list, rotation_list) 967 | 968 | 969 | def get_keyframes(armature): 970 | '''Get each bone location and rotation for each frame.''' 971 | location_list = [] 972 | rotation_list = [] 973 | 974 | for frame in range( 975 | bpy.context.scene.frame_start, 976 | bpy.context.scene.frame_end + 1): 977 | bpy.context.scene.frame_set(frame) 978 | 979 | locations = {} 980 | rotations = {} 981 | 982 | for bone in armature.pose.bones: 983 | bone_matrix = transform_animation_matrix(bone.matrix) 984 | if bone.parent and bone.parent.parent: 985 | parent_matrix = transform_animation_matrix(bone.parent.matrix) 986 | bone_matrix = parent_matrix.inverted() @ bone_matrix 987 | elif bone.name == 'Locator_Locomotion': 988 | bone_matrix = bone.matrix 989 | elif not bone.parent: 990 | bone_matrix = Matrix() 991 | 992 | loc, rot, scl = bone_matrix.decompose() 993 | 994 | locations[bone.name] = loc 995 | rotations[bone.name] = rot.to_euler() 996 | 997 | location_list.append(locations.copy()) 998 | rotation_list.append(rotations.copy()) 999 | 1000 | del locations 1001 | del rotations 1002 | 1003 | bcPrint("Keyframes have been appended to lists.") 1004 | 1005 | return location_list, rotation_list 1006 | 1007 | 1008 | def set_keyframes(armature, location_list, rotation_list): 1009 | '''Insert each keyframe from lists.''' 1010 | 1011 | bpy.context.scene.frame_set(bpy.context.scene.frame_start) 1012 | 1013 | for frame in range( 1014 | bpy.context.scene.frame_start, 1015 | bpy.context.scene.frame_end + 1): 1016 | set_keyframe(armature, frame, location_list, rotation_list) 1017 | 1018 | bpy.context.scene.frame_set(bpy.context.scene.frame_start) 1019 | bcPrint("Keyframes have been inserted to armature fakebones.") 1020 | 1021 | 1022 | def set_keyframe(armature, frame, location_list, rotation_list): 1023 | '''Inset keyframe for current frame from lists.''' 1024 | bpy.context.scene.frame_set(frame) 1025 | 1026 | for bone in armature.pose.bones: 1027 | index = frame - bpy.context.scene.frame_start 1028 | 1029 | fakeBone = bpy.data.objects[bone.name] 1030 | 1031 | fakeBone.location = location_list[index][bone.name] 1032 | fakeBone.rotation_euler = rotation_list[index][bone.name] 1033 | 1034 | fakeBone.keyframe_insert(data_path="location") 1035 | fakeBone.keyframe_insert(data_path="rotation_euler") 1036 | 1037 | 1038 | def apply_animation_scale(armature): 1039 | '''Apply Animation Scale.''' 1040 | scene = bpy.context.scene 1041 | x = bpy.context.view_layer.layer_collection 1042 | bpy.context.view_layer.active_layer_collection = x 1043 | remove_unused_meshes() 1044 | 1045 | if armature is None or armature.type != "ARMATURE": 1046 | return 1047 | 1048 | original_action = armature.animation_data.action 1049 | skeleton = armature.data 1050 | empties = [] 1051 | 1052 | deselect_all() 1053 | scene.frame_set(scene.frame_start) 1054 | for pose_bone in armature.pose.bones: 1055 | bmatrix = pose_bone.bone.head_local 1056 | bpy.ops.object.empty_add(type='PLAIN_AXES', radius=0.1, location=(0,0,0)) 1057 | empty = bpy.context.active_object 1058 | empty.name = pose_bone.name 1059 | 1060 | bpy.ops.object.constraint_add(type='CHILD_OF') 1061 | bpy.data.objects[empty.name].constraints['Child Of'].use_scale_x = False 1062 | bpy.data.objects[empty.name].constraints['Child Of'].use_scale_y = False 1063 | bpy.data.objects[empty.name].constraints['Child Of'].use_scale_z = False 1064 | 1065 | bpy.data.objects[empty.name].constraints['Child Of'].target = armature 1066 | bpy.data.objects[empty.name].constraints['Child Of'].subtarget = pose_bone.name 1067 | 1068 | bcPrint("Baking animation on " + empty.name + "...") 1069 | bpy.ops.nla.bake( 1070 | frame_start=scene.frame_start, 1071 | frame_end=scene.frame_end, 1072 | step=1, 1073 | only_selected=True, 1074 | visual_keying=True, 1075 | clear_constraints=True, 1076 | clear_parents=False, 1077 | use_current_action=False, 1078 | bake_types={'OBJECT'}) 1079 | 1080 | empty.animation_data.action.name += "+bcry" 1081 | empties.append(empty) 1082 | 1083 | bcPrint("Baked Animation successfully on empties.") 1084 | deselect_all() 1085 | 1086 | set_active(armature) 1087 | armature.select_set(True) 1088 | 1089 | bpy.ops.object.transform_apply(rotation=True, scale=True) 1090 | 1091 | bpy.ops.object.mode_set(mode='POSE') 1092 | bpy.ops.pose.user_transforms_clear() 1093 | 1094 | for pose_bone in armature.pose.bones: 1095 | pose_bone.constraints.new(type='COPY_LOCATION') 1096 | pose_bone.constraints.new(type='COPY_ROTATION') 1097 | 1098 | for empty in empties: 1099 | if empty.name == pose_bone.name: 1100 | pose_bone.constraints['Copy Location'].target = empty 1101 | pose_bone.constraints['Copy Rotation'].target = empty 1102 | break 1103 | 1104 | pose_bone.bone.select = True 1105 | 1106 | bcPrint("Baking Animation on skeleton...") 1107 | bpy.ops.nla.bake( 1108 | frame_start=scene.frame_start, 1109 | frame_end=scene.frame_end, 1110 | step=1, 1111 | only_selected=True, 1112 | visual_keying=True, 1113 | clear_constraints=True, 1114 | clear_parents=False, 1115 | use_current_action=False, 1116 | bake_types={'POSE'}) 1117 | 1118 | bpy.ops.object.mode_set(mode='OBJECT') 1119 | 1120 | armature.animation_data.action.name = original_action.name + "_scaled" 1121 | armature.animation_data.action.use_fake_user = True 1122 | 1123 | deselect_all() 1124 | 1125 | bcPrint("Clearing empty data...") 1126 | for empty in empties: 1127 | empty.select_set(True) 1128 | 1129 | bpy.ops.object.delete() 1130 | 1131 | # Remove temp acitons (created by empties) 1132 | remove_unused_actions() 1133 | 1134 | bcPrint("Apply Animation was completed.") 1135 | 1136 | 1137 | def get_animation_id(group): 1138 | node_type = get_node_type(group) 1139 | node_name = get_node_name(group) 1140 | 1141 | return "{!s}-{!s}".format(node_name, node_name) 1142 | 1143 | # Now anm files produces with name as node_name_node_name.anm 1144 | # after the process is done anm files are renmaed by rc.py to 1145 | # cga_name_node_name.anm 1146 | # In the future we may export directly correct name 1147 | # with using below codes. But there is a prerequisite for that: 1148 | # Dae have to be one main visual_node, others have to be in that main node 1149 | # To achieve that we must change a bit visual_exporting process for anm. 1150 | # Deficiency at that way process export nodes show as one at console. 1151 | if node_type == 'i_caf': 1152 | return "{!s}-{!s}".format(node_name, node_name) 1153 | else: 1154 | cga_node = find_cga_node_from_anm_node(group) 1155 | if cga_node: 1156 | cga_name = get_node_name(cga_node) 1157 | return "{!s}-{!s}".format(node_name, cga_name) 1158 | else: 1159 | cga_name = group.objects[0].name 1160 | return "{!s}-{!s}".format(node_name, cga_name) 1161 | 1162 | 1163 | def get_geometry_animation_file_name(collection): 1164 | node_type = get_node_type(collection) 1165 | node_name = get_node_name(collection) 1166 | 1167 | cga_node = find_cga_node_from_anm_node(collection) 1168 | if cga_node: 1169 | cga_name = get_node_name(cga_node) 1170 | return "{!s}_{!s}.anm".format(cga_name, node_name) 1171 | else: 1172 | cga_name = collection.objects[0].name 1173 | return "{!s}_{!s}.anm".format(cga_name, node_name) 1174 | 1175 | 1176 | def get_cryasset_animation_file_name(collection): 1177 | node_type = get_node_type(collection) 1178 | node_name = get_node_name(collection) 1179 | 1180 | cga_node = find_cga_node_from_anm_node(collection) 1181 | if cga_node: 1182 | cga_name = get_node_name(cga_node) 1183 | return "{!s}_{!s}.anm.cryasset".format(cga_name, node_name) 1184 | else: 1185 | cga_name = collection.objects[0].name 1186 | return "{!s}_{!s}.anm.cryasset".format(cga_name, node_name) 1187 | 1188 | 1189 | def find_cga_node_from_anm_node(anm_group): 1190 | for object_ in anm_group.objects: 1191 | for group in object_.users_collection: 1192 | if get_node_type(group) == 'cga': 1193 | return group 1194 | return None 1195 | 1196 | 1197 | # ------------------------------------------------------------------------------ 1198 | # LOD Functions: 1199 | # ------------------------------------------------------------------------------ 1200 | 1201 | def is_lod_geometry(object_): 1202 | return object_.name[:-1].endswith('_LOD') 1203 | 1204 | 1205 | def is_has_lod(object_): 1206 | return ("{}_LOD1".format(object_.name) in bpy.data.objects) 1207 | 1208 | 1209 | def changed_lod_name(lod_name): 1210 | index = lod_name[len(lod_name) - 1] 1211 | return "_lod{}_{}".format(index, lod_name[:-5]) 1212 | 1213 | 1214 | def get_lod_geometries(object_): 1215 | lods = [] 1216 | lod_base_name = "{}_LOD".format(object_.name) 1217 | for obj in bpy.data.objects: 1218 | if obj.name.startswith(lod_base_name): 1219 | lods.append(obj) 1220 | 1221 | return lods 1222 | 1223 | 1224 | # ------------------------------------------------------------------------------ 1225 | # Bone Physics: 1226 | # ------------------------------------------------------------------------------ 1227 | 1228 | def get_bone_geometry(bone): 1229 | bone_name = bone.name 1230 | if bone_name.endswith("_Phys"): 1231 | bone_name = bone_name[:-5] 1232 | 1233 | return bpy.data.objects.get("{}_boneGeometry".format(bone_name), None) 1234 | 1235 | 1236 | def is_bone_geometry(object_): 1237 | if object_.type == "MESH" and object_.name.endswith("_boneGeometry"): 1238 | return True 1239 | else: 1240 | return False 1241 | 1242 | 1243 | def is_physic_bone(bone): 1244 | if bone.name.endswith("_Phys"): 1245 | return True 1246 | else: 1247 | return False 1248 | 1249 | 1250 | def make_physic_bone(bone): 1251 | if bone.name.endswith('.001'): 1252 | bone.name = bone.name.replace('.001', '_Phys') 1253 | else: 1254 | bone.name = "{}_Phys".format(bone.name) 1255 | 1256 | 1257 | def get_armature_physic(armature): 1258 | physic_name = "{}_Phys".format(armature.name) 1259 | if physic_name in bpy.data.objects: 1260 | return bpy.data.objects[physic_name] 1261 | else: 1262 | return None 1263 | 1264 | 1265 | def get_bone_material_type(bone, bone_type): 1266 | if bone_type == 'leg' or bone_type == 'arm' or bone_type == 'foot': 1267 | left_list = ['left', '.l'] 1268 | if is_in_list(bone.name, left_list): 1269 | return "l{}".format(bone_type) 1270 | else: 1271 | return "r{}".format(bone_type) 1272 | 1273 | elif bone_type == 'other': 1274 | return 'primitive' 1275 | 1276 | return bone_type 1277 | 1278 | 1279 | def get_bone_type(bone): 1280 | if is_leg_bone(bone): 1281 | return 'leg' 1282 | elif is_arm_bone(bone): 1283 | return 'arm' 1284 | elif is_torso_bone(bone): 1285 | return 'torso' 1286 | elif is_head_bone(bone): 1287 | return 'head' 1288 | elif is_foot_bone(bone): 1289 | return 'foot' 1290 | else: 1291 | return 'other' 1292 | 1293 | 1294 | def is_leg_bone(bone): 1295 | leg = ['leg', 'shin', 'thigh', 'calf'] 1296 | return is_in_list(bone.name, leg) 1297 | 1298 | 1299 | def is_arm_bone(bone): 1300 | arm = ['arm', 'hand'] 1301 | return is_in_list(bone.name, arm) 1302 | 1303 | 1304 | def is_torso_bone(bone): 1305 | torso = ['hips', 'pelvis', 'spine', 'chest', 'torso'] 1306 | return is_in_list(bone.name, torso) 1307 | 1308 | 1309 | def is_head_bone(bone): 1310 | head = ['head', 'neck'] 1311 | return is_in_list(bone.name, head) 1312 | 1313 | 1314 | def is_foot_bone(bone): 1315 | foot = ['foot', 'toe'] 1316 | return is_in_list(bone.name, foot) 1317 | 1318 | 1319 | def is_in_list(str, list_): 1320 | for sub in list_: 1321 | if str.lower().find(sub) != -1: 1322 | return True 1323 | return False 1324 | 1325 | 1326 | # ------------------------------------------------------------------------------ 1327 | # Skeleton: 1328 | # ------------------------------------------------------------------------------ 1329 | 1330 | def get_root_bone(armature): 1331 | for bone in get_bones(armature): 1332 | if bone.parent is None: 1333 | return bone 1334 | 1335 | 1336 | def count_root_bones(armature): 1337 | count = 0 1338 | for bone in get_bones(armature): 1339 | if bone.parent is None: 1340 | count += 1 1341 | 1342 | return count 1343 | 1344 | 1345 | def get_armature_for_object(object_): 1346 | if object_.parent is not None: 1347 | if object_.parent.type == "ARMATURE": 1348 | return object_.parent 1349 | 1350 | 1351 | def get_armature(): 1352 | for object_ in get_type("controllers"): 1353 | return object_ 1354 | 1355 | 1356 | def get_bones(armature): 1357 | return [bone for bone in armature.data.bones] 1358 | 1359 | 1360 | def get_animation_node_range(object_, node_name, initial_start, initial_end): 1361 | try: 1362 | start_frame = object_["{}_Start".format(node_name)] 1363 | end_frame = object_["{}_End".format(node_name)] 1364 | 1365 | if isinstance(start_frame, str) and isinstance(end_frame, str): 1366 | tm = bpy.context.scene.timeline_markers 1367 | if tm.find(start_frame) != -1 and tm.find(end_frame) != -1: 1368 | return tm[start_frame].frame, tm[end_frame].frame 1369 | else: 1370 | raise exceptions.MarkerNotFound 1371 | else: 1372 | return start_frame, end_frame 1373 | except: 1374 | return initial_start, initial_end 1375 | 1376 | 1377 | def get_armature_from_node(group): 1378 | armature_count = 0 1379 | armature = None 1380 | for object_ in group.objects: 1381 | if object_.type == "ARMATURE": 1382 | armature_count += 1 1383 | armature = object_ 1384 | 1385 | if armature_count == 1: 1386 | return armature 1387 | 1388 | if armature_count == 0: 1389 | raise exceptions.BCryException("i_caf node has no armature!") 1390 | elif armature_count > 1: 1391 | raise exceptions.BCryException( 1392 | "{} i_caf node have more than one armature!".format(object_.name)) 1393 | 1394 | return None 1395 | 1396 | 1397 | def activate_all_bone_layers(armature): 1398 | layers = [] 1399 | for index in range(0, 32): 1400 | layers.append(armature.data.layers[index]) 1401 | armature.data.layers[index] = True 1402 | 1403 | return layers 1404 | 1405 | 1406 | def recover_bone_layers(armature, layers): 1407 | for index in range(0, 32): 1408 | armature.data.layers[index] = layers[index] 1409 | 1410 | 1411 | # ------------------------------------------------------------------------------ 1412 | # General: 1413 | # ------------------------------------------------------------------------------ 1414 | 1415 | def select_all(): 1416 | for object_ in bpy.data.objects: 1417 | object_.select_set(True) 1418 | 1419 | 1420 | def deselect_all(): 1421 | for object_ in bpy.data.objects: 1422 | object_.select_set(False) 1423 | 1424 | 1425 | def set_active(object_): 1426 | bpy.context.view_layer.objects.active = object_ 1427 | 1428 | 1429 | def get_object_children(parent): 1430 | return [child for child in parent.children 1431 | if child.type in {'ARMATURE', 'EMPTY', 'MESH'}] 1432 | 1433 | 1434 | def parent(children, parent): 1435 | for object_ in children: 1436 | object_.parent = parent 1437 | 1438 | return 1439 | 1440 | 1441 | def remove_unused_meshes(): 1442 | for mesh in bpy.data.meshes: 1443 | if mesh.users == 0: 1444 | bpy.data.meshes.remove(mesh) 1445 | 1446 | 1447 | def remove_unused_actions(): 1448 | for action in bpy.data.actions: 1449 | if "+bcry" in action.name: 1450 | bpy.data.actions.remove(action) 1451 | 1452 | 1453 | def get_bounding_box(object_): 1454 | vmin = Vector() 1455 | vmax = Vector() 1456 | if object_.type == 'EMPTY': 1457 | k = object_.empty_display_size 1458 | vmax = Vector((k, k, k)) 1459 | vmin = Vector((-k, -k, -k)) 1460 | elif object_.type == 'MESH': 1461 | box = object_.bound_box 1462 | vmin = Vector([box[0][0], box[0][1], box[0][2]]) 1463 | vmax = Vector([box[6][0], box[6][1], box[6][2]]) 1464 | 1465 | return vmin[0], vmin[1], vmin[2], vmax[0], vmax[1], vmax[2] 1466 | 1467 | 1468 | # ------------------------------------------------------------------------------ 1469 | # Overriding Context: 1470 | # ------------------------------------------------------------------------------ 1471 | 1472 | def get_3d_context(object_): 1473 | window = bpy.context.window 1474 | screen = window.screen 1475 | for area in screen.areas: 1476 | if area.type == "VIEW_3D": 1477 | area3d = area 1478 | break 1479 | for region in area3d.regions: 1480 | if region.type == "WINDOW": 1481 | region3d = region 1482 | break 1483 | override = { 1484 | "window": window, 1485 | "screen": screen, 1486 | "area": area3d, 1487 | "region": region3d, 1488 | "object": object_ 1489 | } 1490 | 1491 | return override 1492 | 1493 | 1494 | def override(obj, active=True, selected=True): 1495 | ctx = bpy.context.copy() 1496 | for area in bpy.context.screen.areas: 1497 | if area.type == 'VIEW_3D': 1498 | ctx['area'] = area 1499 | ctx['region'] = area.regions[-1] 1500 | break 1501 | 1502 | if active: 1503 | ctx['active_object'] = obj 1504 | ctx['active_base'] = obj 1505 | ctx['object'] = obj 1506 | 1507 | if selected: 1508 | ctx['selected_objects'] = [obj] 1509 | ctx['selected_bases'] = [obj] 1510 | ctx['selected_editable_objects'] = [obj] 1511 | ctx['selected_editable_bases'] = [obj] 1512 | 1513 | return ctx 1514 | 1515 | 1516 | # ------------------------------------------------------------------------------ 1517 | # Layer File: 1518 | # ------------------------------------------------------------------------------ 1519 | 1520 | def get_guid(): 1521 | GUID = "{{}-{}-{}-{}-{}}".format(random_hex_sector(8), 1522 | random_hex_sector(4), 1523 | random_hex_sector(4), 1524 | random_hex_sector(4), 1525 | random_hex_sector(12)) 1526 | return GUID 1527 | 1528 | 1529 | def random_hex_sector(length): 1530 | fixed_length_hex_format = "%0{}x".format(length) 1531 | return fixed_length_hex_format % random.randrange(16 ** length) 1532 | 1533 | 1534 | # ------------------------------------------------------------------------------ 1535 | # Scripting: 1536 | # ------------------------------------------------------------------------------ 1537 | 1538 | def generate_file_contents(type_): 1539 | if type_ == "chrparams": 1540 | return """\ 1541 | \ 1542 | \ 1543 | \ 1544 | """ 1545 | 1546 | elif type_ == "cdf": 1547 | return """\ 1548 | \ 1549 | \ 1550 | \ 1551 | \ 1552 | \ 1553 | \ 1554 | """ 1555 | 1556 | 1557 | def generate_file(filepath, contents, overwrite=False): 1558 | if not os.path.exists(filepath) or overwrite: 1559 | file = open(filepath, 'w') 1560 | file.write(contents) 1561 | file.close() 1562 | 1563 | 1564 | def generate_xml(filepath, contents, overwrite=False, ind=4): 1565 | if not os.path.exists(filepath) or overwrite: 1566 | if isinstance(contents, str): 1567 | script = parseString(contents) 1568 | else: 1569 | script = contents 1570 | contents = script.toprettyxml(indent=' ' * ind) 1571 | generate_file(filepath, contents, overwrite) 1572 | 1573 | 1574 | def clear_xml_header(filepath): 1575 | lines = open(filepath, 'r').readlines() 1576 | if lines[0].find("