# -*- coding: utf-8 -*- # ##### 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 ##### # Copyright 2019 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 metaverse_tools.utils.helpers.extra_math import * EXPORT_VERSION = 85 def center_all(blender_object): for child in blender_object.children: select(child) blender_object.select_set(state=True) bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='BOUNDS') blender_object.select_set(state=False) def select(blender_object): for child in blender_object.children: select(child) blender_object.select_set(state=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 set_relative_to_parent(blender_object, json_data): 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) return json_data def parse_object(blender_object, path, options, gltf): # 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) 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_set(state=True) uid = "" reference_name = blender_object.data.name # TODO: If Child of armature, skip logic # 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.collection.objects.link(clone) clone.select_set(state=True) original_object.select_set(state=False) uid = "-" + generate_unique_id_modifier(clone.modifiers) bpy.context.view_layer.objects.active = clone apply_all_modifiers(clone.modifiers) blender_object = clone clone.select_set(state=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 # if gltf: file_path = path + reference_name + uid print("Writing GLTF with path_mode=", file_path) #bpy.ops.metaverse_toolset.export_scene_fbx(filepath=file_path, embed_textures=True, path_mode='COPY', use_selection=True, axis_forward='-Z', axis_up='Y') # TODO: Add gltf option HERE. return False else: file_path = path + reference_name + uid + ".fbx" print("Writing FBX with path_mode=", file_path) bpy.ops.metaverse_toolset.export_scene_fbx(filepath=file_path, 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 = "" if gltf: print("GLTF PLACEHOLDER") else: model_url = "atp:/"+ last_folder + reference_name + uid + '.fbx' else: if gltf: print("GLTF PLACEHOLDER") 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 }, 'grab':{ 'grabbable': False, 'triggerable': False, 'cloneable': False }, "shapeType": "static-mesh", 'userData': '{"blender_export":"' + scene_id +'"}' } json_data = set_relative_to_parent(blender_object, json_data) if original_object: bpy.ops.object.delete() blender_object = original_object blender_object.select_set(state=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_set(state=True) # TODO: Spot Lights require rotation by 90 degrees to get pointing in the right direction elif bo_type == 'ARMATURE': # Same as Mesh actually. # Get all children export as a single file. print(name, 'is armature. Not Supported as of the moment') elif bo_type == 'EMPTY': print(name, 'Adding an Empty') json_data = { 'id': scene_id, 'visible': False, 'collisionless': True, 'ignoreForCollisions': True, 'position': { 'x': position.x, 'y': position.y, 'z': position.z }, 'dimensions':{ 'x': 1, 'y': 1, 'z': 1, }, 'name': 'EMPTY-' + name, "color": { "blue": 128, "green": 0, "red": 255 }, "shape": "Cube", "type": "Box", 'userData': '{"blender_export":"' + scene_id +'", "grabbableKey":{"grabbable":false,"ignoreIK":false}}', } json_data = set_relative_to_parent(blender_object, json_data) 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, gltf=False): 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 = "" ## TODO: Remove marketplace hardcode reference 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, gltf) 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()