# ##### BEGIN GPL LICENSE BLOCK ##### # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ##### END GPL LICENSE BLOCK ##### # Scene Logic for Exporting Blender Scenes # By Matti 'Menithal' Lahtinen import bpy import uuid import re import os import json from mathutils import Quaternion from math import sqrt from hashlib import md5, sha256 from copy import copy, deepcopy from hifi_tools.utils.helpers import * EXPORT_VERSION = 84 def center_all(blender_object): for child in blender_object.children: select(child) blender_object.select = True bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='BOUNDS') blender_object.select = False def select(blender_object): for child in blender_object.children: select(child) blender_object.select = True # Can't use name to define the unique id as this is not shared between instancing, instead going to go through # Each modifier in order and hope the order is the same # TODO: Separate to utility perhaps? def generate_unique_id_modifier(modifiers): unique_name = "" for index, modifier in enumerate(modifiers): print(str(index), "Iterating", modifier.name, modifier.type) # for use only old_unique = unique_name + "|name>" + modifier.name unique_name = unique_name + "|" + str(index) + "|m:" + modifier.type if modifier.type == 'EDGE_SPLIT': print("Edge split") unique_name = unique_name + "|sa:" + str(modifier.split_angle) if modifier.use_apply_on_spline: unique_name = unique_name + "|uaos" if modifier.use_edge_angle: unique_name = unique_name + "|ua" if modifier.use_edge_sharp: unique_name = unique_name + "|us" elif modifier.type == 'MIRROR': print("Mirror") if modifier.mirror_object: unique_name = unique_name + '|m:' + modifier.mirror_object.name if modifier.use_x: unique_name = unique_name + "|x" if modifier.use_y: unique_name = unique_name + "|y" if modifier.use_z: unique_name = unique_name + "|z" if modifier.use_mirror_u: unique_name = unique_name + "|u" if modifier.use_mirror_v: unique_name = unique_name + "|v" if modifier.use_clip: unique_name = unique_name + "|c" if modifier.use_mirror_vertex_groups: unique_name = unique_name + "|mvg" if modifier.use_mirror_merge: unique_name = unique_name + "|mm:" + str(modifier.merge_threshold) elif modifier.type == 'ARRAY': print("Array") if modifier.fit_type == 'FIXED_COUNT': unique_name = unique_name + '|c:' + str(modifier.count) if modifier.fit_type == 'FIT_LENGTH': unique_name = unique_name + '|fl:' + str(modifier.fit_length) if modifier.fit_type == 'FIT_CURVE' and modifier.curve: unique_name = unique_name + '|cr:' + str(modifier.curve.name) if modifier.use_merge_vertices: unique_name = unique_name + '|mt:' + str(modifier.merge_threshold) if modifier.use_constant_offset: unique_name = unique_name + '|cod:' + str(modifier.constant_offset_display.to_tuple()) # This one behaves differently than above in blender, so custom method if modifier.use_relative_offset: rod = (modifier.relative_offset_displace[0], modifier.relative_offset_displace[1], modifier.relative_offset_displace[2]) unique_name = unique_name + '|rod:' + str(rod) if modifier.start_cap: unique_name = unique_name + '|sc:' + modifier.start_cap.name if modifier.end_cap: unique_name = unique_name + '|ec:' + modifier.end_cap.name if modifier.use_object_offset and modifier.offset_object: unique_name = unique_name + '|oo:' + modifier.offset_object.name else: # TODO: Add Support to subsurface / solidify unique_name = old_unique print( 'Unsupported modifier ', modifier.name, modifier.type, ' Skipping') print(unique_name) return str(uuid.uuid5(uuid.NAMESPACE_DNS, unique_name)) def apply_all_modifiers(modifiers): for modifier in modifiers: #Apply all but Armature if modifier.type != 'ARMATURE': bpy.ops.object.modifier_apply(apply_as='DATA', modifier=modifier.name) def parse_object(blender_object, path, options): # Store existing rotation mode, just in case. json_data = None # Make sure context is quaternion for the models if options.remove_trailing: name = re.sub(r'\.\d{3}$', '', blender_object.name) else: name = blender_object.name # If you ahve an object thats the same mesh, but different object: All Objects will use this as reference allowing for instancing. uuid_gen = uuid.uuid5(uuid.NAMESPACE_DNS, blender_object.name) scene_id = str(uuid_gen) reference_name = blender_object.data.name bo_type = blender_object.type stored_rotation_mode = str(blender_object.rotation_mode) blender_object.rotation_mode = 'QUATERNION' orientation = quat_swap_nzy(blender_object.rotation_quaternion) position = swap_nzy(blender_object.location) if bo_type == 'MESH': original_object = None blender_object.select = True uid = "" # Here comes the fun part: Apply all modifiers prior to using them in the instance if len(blender_object.modifiers) > 0: # Lets do a LOW-LEVEL duplicate, too much automation in duplicate clone = blender_object.copy() original_object = blender_object clone.data = blender_object.data.copy() bpy.context.scene.objects.link(clone) clone.select = True original_object.select = False uid = "-" + generate_unique_id_modifier(clone.modifiers) print(uid) bpy.context.scene.objects.active = clone apply_all_modifiers(clone.modifiers) blender_object = clone clone.select = True #temp_dimensions = Vector(blender_object.dimensions) dimensions = swap_yz(blender_object.dimensions) bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='BOUNDS') print("Storing existing rotation") temp_rotation = Quaternion(blender_object.rotation_quaternion) # Temporary Rotate Model to a zero rotation so that the exported model rotation is normalized. blender_object.rotation_quaternion = Quaternion((1,0,0,0)) #blender_object.dimensions = Vector((1,1,1)) # TODO: Option to also export via gltf instead of fbx # TODO: Add Option to not embedtextures / copy paths file_path = path + reference_name + uid + ".fbx" atp_enabled = options.atp bpy.ops.export_scene.fbx(filepath=file_path, version='BIN7400', embed_textures=True, path_mode='COPY', use_selection=True, axis_forward='-Z', axis_up='Y') # Restore earlier rotation # blender_object.dimensions = temp_dimensions blender_object.rotation_quaternion = temp_rotation if options.atp: if options.use_folder: last_folder_re = re.search(r"(?:=\/|\\)?([a-zA-Z0-9_\-]+)(?:\/|\\)?$", path) start = last_folder_re.start(0)+1 end = last_folder_re.end(0) last_folder = path[start:end] else: last_folder = "" model_url = "atp:/"+ last_folder + reference_name + uid + '.fbx' else: model_url = options.url_override + reference_name + uid + '.fbx' json_data = { 'name': name, 'id': scene_id, 'type': 'Model', 'modelURL': model_url, 'position': { 'x': position.x, 'y': position.y, 'z': position.z }, 'rotation': { 'x': orientation.x, 'y': orientation.y, 'z': orientation.z, 'w': orientation.w }, 'dimensions':{ 'x': dimensions.x, 'y': dimensions.y, 'z': dimensions.z }, "shapeType": "static-mesh", 'userData': '{"blender_export":"' + scene_id +'"}, "grabbable_key":["grabbable":false]}' } if blender_object.parent: parent = blender_object.parent parent_uuid = uuid.uuid5(uuid.NAMESPACE_DNS, parent.name) parent_orientation = quat_swap_nzy(relative_rotation(blender_object)) parent_position = swap_nzy(relative_position(blender_object)) json_data["position"] = { 'x': parent_position.x, 'y': parent_position.y, 'z': parent_position.z } json_data["rotation"] = { 'x': parent_orientation.x, 'y': parent_orientation.y, 'z': parent_orientation.z, 'w': parent_orientation.w } json_data["parentID"] = str(parent_uuid) if original_object: print("removing duplicate") bpy.ops.object.delete() blender_object = original_object print("new set", blender_object) blender_object.select = True elif bo_type == 'LAMP': print(name, 'is Light') # Hifi 5, Blender 3.3 light = blender_object.data color = blender_object.color falloff = sqrt(light.distance) distance = light.distance json_data = { 'name': name, 'id': scene_id, 'type': 'Light', 'position': { 'x': position.x, 'y': position.y, 'z': position.z }, 'color':{ 'blue': int(color[2] * 255), 'green': int(color[1] * 255), 'red': int(color[0] * 255) }, 'dimensions':{ 'x': distance, 'y': distance, 'z': distance, }, 'falloffRadius': falloff, 'rotation': { 'x': orientation.x, 'y': orientation.y, 'z': orientation.z, 'w': orientation.w }, 'intensity': light.energy, 'userData': '{"blender_export":"' + scene_id +'", "grabbable_key":["grabbable":false]}' } if light.type is 'POINT': blender_object.select = True # TODO: Spot Lights require rotation by 90 degrees to get pointing in the right direction elif bo_type == 'ARMATURE': # Same as Mesh actually. print(name, 'is armature') else: print('Skipping unsupported feature', name, bo_type) # Restore object's rotation mode print(blender_object) if blender_object: blender_object.rotation_mode = stored_rotation_mode bpy.ops.object.select_all(action = 'DESELECT') return json_data # Rotation is based on the rotaiton of the parent and self. def relative_rotation(parent_object): if not parent_object.parent: return parent_object.rotation_quaternion else: rotation = relative_rotation( parent_object.parent) current = parent_object.rotation_quaternion current.invert() print('rotation test', current) return rotation * current def relative_position(parent_object): if parent_object.parent is not None: return relative_rotation(parent_object.parent) * parent_object.location - relative_position(parent_object.parent) else: return parent_object.location def write_file(context): current_scene = bpy.context.scene read_scene = current_scene # Creating a temp copy to do the changes in. if context.clone_scene: bpy.ops.scene.new(type='FULL_COPY') read_scene = bpy.context.scene # sets the new scene as the new scene read_scene.name = 'Hifi_Export_Scene' # Make sure we are in Object mode bpy.ops.object.mode_set(mode = 'OBJECT') # Deselect all objects bpy.ops.object.select_all(action = 'DESELECT') # Clone Scene. Then select scene. After done delete scene path = os.path.dirname(os.path.realpath(context.filepath)) + '/' ## Parse the marketplace url url = "" if not context.atp: url = context.url_override if "https://highfidelity.com/marketplace/items/" in url: marketplace_id = url.replace("https://highfidelity.com/marketplace/items/", "").replace("/edit","").replace("/","") url = "http://mpassets.highfidelity.com/" + marketplace_id + "-v1/" if not url.endswith('/'): url = url + "/" entities = [] # Duplicate list to break reference as we may do updates to the scene current_scene_objects = list(read_scene.objects) for blender_object in current_scene_objects: print(len(current_scene_objects)) parsed = parse_object(blender_object, path, context) if parsed: entities.append(parsed) # Delete Cloned scene # if context.clone_scene: bpy.ops.scene.delete() hifi_scene = { 'Version': EXPORT_VERSION, 'Entities': entities } data = json.dumps(hifi_scene, indent=4) file = open(context.filepath, "w") try: file.write(data) except e: print('Could not write to file.', e) finally: file.close()