315 lines
12 KiB
Python
315 lines
12 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 Parsing and Navigating Content Tree
|
|
# By Matti 'Menithal' Lahtinen
|
|
|
|
|
|
import bpy
|
|
from hashlib import md5
|
|
from mathutils import Quaternion, Vector, Euler, Matrix
|
|
|
|
from hifi_tools.world import primitives as prims
|
|
from hifi_tools.utils.helpers import PIVOT_VECTOR, swap_nyz, swap_nzy, parse_dict_quaternion, parse_dict_vector, swap_yz, swap_pivot, quat_swap_nyz
|
|
|
|
|
|
class HifiScene:
|
|
def __init__(self, json,
|
|
uv_sphere=False,
|
|
join_children=True,
|
|
merge_distance=0.01,
|
|
delete_interior_faces=True,
|
|
use_boolean_operation="NONE"):
|
|
json_entities = json['Entities']
|
|
|
|
self.uv_sphere = uv_sphere
|
|
self.join_children = join_children
|
|
self.merge_distance = merge_distance
|
|
self.delete_interior_faces = delete_interior_faces
|
|
self.use_boolean_operation = use_boolean_operation
|
|
|
|
self.entities = []
|
|
self.materials = []
|
|
self.entity_ids = []
|
|
self.material_index = []
|
|
self.materials = []
|
|
self.parent_entities = []
|
|
|
|
self.root = []
|
|
|
|
# Build Indices for entity ids, and build the Objects
|
|
print(' building indices ')
|
|
for idx, entity in enumerate(json_entities):
|
|
self.entity_ids.append(entity['id'])
|
|
hifi_entity = HifiObject(entity, self)
|
|
self.entities.append(hifi_entity)
|
|
|
|
# Build Trees by checking if parents exist in parent tree
|
|
print(' building parents ')
|
|
for entity in self.entities:
|
|
self.append_parent(entity)
|
|
|
|
self.build_scene()
|
|
|
|
# links Parents and children together. to build a tree
|
|
def append_parent(self, entity):
|
|
# Check if parent id for entity exists
|
|
result = self.search_entity(entity.parent_id)
|
|
if result is not None:
|
|
# add entity as a child of parent
|
|
result.add_child(entity)
|
|
# set parent as a parent of entity
|
|
entity.set_parent(result)
|
|
|
|
def search_entity(self, id):
|
|
# TODO: May need to do some more advanced tricks here maybe later.
|
|
found_entity = None
|
|
try:
|
|
# Get index of instance of the id from existing ids
|
|
index = self.entity_ids.index(id)
|
|
# if found, return found entity
|
|
found_entity = self.entities[index]
|
|
except e:
|
|
pass
|
|
finally:
|
|
# Regardless of failure, always return None or the entity
|
|
return found_entity
|
|
|
|
def build_scene(self):
|
|
# Store context to set cursor
|
|
current_context = bpy.context.area.type
|
|
bpy.context.area.type = 'VIEW_3D'
|
|
# set context to 3D View and set Cursor
|
|
bpy.context.space_data.cursor_location[0] = 0.0
|
|
bpy.context.space_data.cursor_location[1] = 0.0
|
|
bpy.context.space_data.cursor_location[2] = 0.0
|
|
# return context back to earlier, and build scene.
|
|
bpy.context.area.type = current_context
|
|
print("Building Scene out of " + str(len(self.entities)) + ' Objects and '
|
|
+ str(len(self.material_index)) + ' materials')
|
|
|
|
for entity in self.entities:
|
|
if entity.is_root():
|
|
entity.build()
|
|
|
|
def append_material(self, color):
|
|
# Just Hash result
|
|
|
|
material_hash = md5(
|
|
str(color[0] + color[1] << 2 + color[2] << 4).encode('utf-8')).hexdigest()
|
|
|
|
if material_hash not in self.material_index:
|
|
index = len(self.material_index)
|
|
self.material_index.append(material_hash)
|
|
|
|
mat = bpy.data.materials.new(str(color))
|
|
# convert from rgb to float
|
|
mat.diffuse_color = tuple(c/255 for c in color)
|
|
|
|
# Make sure material at first is not metallic
|
|
mat.specular_color = (0, 0, 0)
|
|
|
|
self.materials.append(mat)
|
|
return mat
|
|
|
|
return self.materials[self.material_index.index(material_hash)]
|
|
|
|
|
|
class HifiObject:
|
|
|
|
def __init__(self, entity, scene):
|
|
|
|
self.id = entity['id']
|
|
self.children = []
|
|
self.blender_object = None
|
|
self.scene = scene
|
|
# Make sure the Blender Object has a name: And to make it unique, append id of the entity as well.
|
|
if 'name' in entity and len(entity['name'].strip()) > 0:
|
|
self.name = entity['name'] + '-' + self.id
|
|
elif 'shape' in entity:
|
|
self.name = entity['shape'] + '-' + self.id
|
|
else:
|
|
self.name = entity['type'] + '-' + self.id
|
|
|
|
self.position_original = Vector(parse_dict_vector(entity, 'position'))
|
|
self.position = swap_nyz(parse_dict_vector(entity, 'position'))
|
|
|
|
self.pivot = swap_pivot(parse_dict_vector(
|
|
entity, 'registrationPoint', PIVOT_VECTOR))
|
|
self.dimensions = swap_yz(parse_dict_vector(entity, 'dimensions'))
|
|
|
|
self.rotation_original = Quaternion(
|
|
parse_dict_quaternion(entity, 'rotation'))
|
|
self.rotation = quat_swap_nyz(
|
|
parse_dict_quaternion(entity, 'rotation'))
|
|
|
|
self.parent = None
|
|
self.children = []
|
|
|
|
if 'parentID' in entity:
|
|
self.parent_id = entity['parentID']
|
|
else:
|
|
self.parent_id = None
|
|
|
|
self.type = entity['type']
|
|
self.shape = None
|
|
if 'shape' in entity:
|
|
self.shape = entity['shape']
|
|
|
|
if self.type != 'Light' and self.type != 'Zone' and self.type != 'Particle':
|
|
if 'color' in entity:
|
|
color = entity['color']
|
|
self.material = scene.append_material(
|
|
(color['red'], color['green'], color['blue']))
|
|
else:
|
|
self.material = None
|
|
|
|
def is_root(self):
|
|
if self.parent is None:
|
|
return True
|
|
return False
|
|
|
|
def select(self):
|
|
self.blender_object.select = True
|
|
|
|
def build(self):
|
|
# First places down the children (recursive)
|
|
for child in self.children:
|
|
child.build()
|
|
|
|
# Place self by selecting what primitive to add, depending on type and shape.
|
|
# Boxes and Spheres are the only primitive to have a separate type vs others
|
|
if self.type == 'Shape':
|
|
if self.shape == 'Icosahedron':
|
|
prims.add_icosahedron(self)
|
|
elif self.shape == 'Dodecahedron':
|
|
prims.add_docadehedron(self)
|
|
elif self.shape == 'Cylinder':
|
|
prims.add_cylinder(self)
|
|
elif self.shape == 'Octagon':
|
|
prims.add_octagon(self)
|
|
elif self.shape == 'Hexagon':
|
|
prims.add_hexagon(self)
|
|
elif self.shape == 'Tetrahedron':
|
|
prims.add_tetrahedron(self)
|
|
elif self.shape == 'Octahedron':
|
|
prims.add_octahedron(self)
|
|
elif self.shape == 'Cone':
|
|
prims.add_cone(self)
|
|
elif self.shape == 'Triangle':
|
|
prims.add_triangle(self)
|
|
|
|
# The following will be deprecated, as they are no longer quads or circles when exported
|
|
elif self.shape == 'Quad':
|
|
prims.add_quad(self)
|
|
elif self.shape == 'Circle':
|
|
prims.add_circle(self)
|
|
else:
|
|
print(' Warning: ', self.shape, ' Not Defined ')
|
|
return
|
|
else:
|
|
if self.type == "Text":
|
|
prims.add_box(self)
|
|
elif self.type == "Light":
|
|
prims.add_light(self)
|
|
elif self.type == "Model":
|
|
prims.add_box(self)
|
|
elif self.type == 'Sphere':
|
|
if self.scene.uv_sphere:
|
|
prims.add_uv_sphere(self)
|
|
else:
|
|
prims.add_sphere(self)
|
|
elif self.type == 'Box':
|
|
prims.add_box(self)
|
|
else:
|
|
print(' Warning: ', self.type, self.shape, ' Not Supported ')
|
|
return
|
|
|
|
# Now if above is a mesh type to do join / boolean operations on
|
|
if self.blender_object.type == "MESH":
|
|
for child in self.children:
|
|
# Material Combinator to pre-combine materials prior to applying boolean operator or joining objects together
|
|
# This allows the materials to be maintained even if they are joined.
|
|
# for each material the child's blender objects have
|
|
for material in child.blender_object.data.materials.values():
|
|
# and if the material is set, and is not yet set for the parent, add an instance of the material to the parent
|
|
if material is not None and material not in bpy.context.object.data.materials.values():
|
|
bpy.context.object.data.materials.append(material)
|
|
# If the scene wants to use boolean operators, this overrides join children (as it is a method to join children)
|
|
if self.scene.use_boolean_operation != "NONE":
|
|
bpy.ops.object.modifier_add(type='BOOLEAN')
|
|
# Set name for modifier to keep track of it.
|
|
name = child.name + '-Boolean'
|
|
bpy.context.object.modifiers["Boolean"].name = name
|
|
bpy.context.object.modifiers[name].operation = 'UNION'
|
|
bpy.context.object.modifiers[name].solver = self.scene.use_boolean_operation
|
|
bpy.context.object.modifiers[name].object = child.blender_object
|
|
bpy.ops.object.modifier_apply(
|
|
apply_as='DATA', modifier=name)
|
|
# Clean up the child object from the blender scene.
|
|
bpy.data.objects.remove(child.blender_object)
|
|
# TODO: Set Child.blender_object as the blender object of the parent to maintain links
|
|
# If not boolean operator, but join_children is still True
|
|
elif self.scene.join_children:
|
|
# Select the child
|
|
child.select()
|
|
# Select self
|
|
self.select()
|
|
# Join
|
|
bpy.ops.object.join()
|
|
|
|
# Safety select
|
|
self.select()
|
|
|
|
bpy.ops.object.modifier_add(type='EDGE_SPLIT')
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
|
|
if len(self.children) > 0:
|
|
bpy.ops.mesh.remove_doubles(
|
|
threshold=self.scene.merge_distance)
|
|
# endif
|
|
|
|
# And at the end, make sure object is selected.
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
|
|
# Position and rotations of children are always relative to parent in Hifi Tree.
|
|
|
|
# Get the absolute position by getting the relative position of the parent, and adding my own to it.
|
|
# note that then position is relative to the parents rotation too, so make sure to eliminate that as well.
|
|
def relative_position(self):
|
|
if self.parent is not None:
|
|
return self.parent.relative_rotation() * self.position + self.parent.relative_position()
|
|
else:
|
|
return self.position
|
|
|
|
# Rotation is based on the rotaiton of the parent and self.
|
|
def relative_rotation(self):
|
|
if self.parent is not None:
|
|
rotation = self.parent.relative_rotation()
|
|
return rotation * self.rotation
|
|
else:
|
|
return self.rotation
|
|
|
|
def set_parent(self, parent):
|
|
if type(parent) is HifiObject:
|
|
self.parent = parent
|
|
else:
|
|
print('Warning: Type parent was not HifiObject')
|
|
|
|
def add_child(self, child):
|
|
self.children.append(child)
|