content/hifi-content/caitlyn/scratch/hifi_tools (5)/files/hifi_json/writer.py
2022-02-13 22:19:19 +01:00

415 lines
No EOL
15 KiB
Python

# ##### 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()