import sys, os.path, logging import bpy from bpy_extras.io_utils import ImportHelper from bpy.props import BoolProperty, StringProperty import json from array import array class dsf_geom_load (object): def __init__ (self): pass @classmethod def intern_geometry (self, jdata): v = array ('f') f = array ('i') m = array ('i') g = array ('i') for vertex in jdata['vertices']['values']: v.extend (vertex) group_list = jdata['polygon_groups']['values'] mat_list = jdata['polygon_material_groups']['values'] for polygon in jdata['polylist']['values']: (gidx, midx, verts) = (polygon[0], polygon[1], polygon[2:]) # for blender: rotate the face, if it ends in a 0. if len (verts) == 4 and verts[3] == 0: verts[0], verts[1:] = verts[3], verts[:3] if len (verts) != 4: verts.append (0) f.extend (verts) m.append (midx) g.append (gidx) return { 'v': v, 'g': g, 'm': m, 'f': f, 'gm': group_list, 'mm': mat_list } @classmethod def load_geometry (self, filename, feats = ['vt', 'g', 'm']): """create a model from the json-data in jdata. g - include face-groups m - include materials """ jdata = json.load (open (filename, 'r')) geom = self.intern_geometry\ (jdata['geometry_library'][0]) geom['id_path'] =\ jdata['asset_info']['id'] + "#" + jdata['node_library'][0]['id'] return geom @classmethod def load_file (self, filename): geom_data = self.load_geometry (filename) return geom_data genesis = '/images/winshare/dsdata4/data/DAZ 3D/Genesis/Base/Genesis.dsf' import bpy import logging from array import array class dsf_geom_define (object): """utility class for inserting mesh data into blender. """ log = logging.getLogger ('dsf_geom_define') @classmethod def create_vertex_groups (self, geom): """convert the face-groups to a map of vertex-groups. """ gmap = dict () for fidx, g in enumerate (geom['g']): fvs = geom['f'][4*fidx:4*fidx+4] if g not in gmap: gmap[g] = array ('i') if fvs[0] != 0: gmap[g].extend (fvs) else: gmap[g].extend (fvs[0:3]) gnmap = dict () for (gidx, gname) in enumerate (geom['gm']): if gidx in gmap: gnmap[gname] = gmap[gidx] return gnmap @classmethod def define_geom (self, name, geom): """load the vertices and faces into blender. """ mesh_dat = bpy.data.meshes.new (name) v = geom['v'] # insert vertices. qmod.v is a list of triples (x,y,z), so there are a # third of them verts. n_verts = len (v) // 3 mesh_dat.vertices.add (n_verts) idx = 0 for bvert in mesh_dat.vertices: bvert.co = v[idx : idx+3] idx += 3 # each face has exactly 4 vertex indices. f = geom['f'] mesh_dat.faces.add (len (f) // 4) mesh_dat.faces.foreach_set ("vertices_raw", f) mesh_obj = bpy.data.objects.new (name, mesh_dat) bpy.context.scene.objects.link (mesh_obj) bpy.context.scene.update () if 'id_path' in geom: mesh_obj['id_path'] = geom['id_path'] return mesh_obj @classmethod def define_materials (self, mesh, geom, use = True, **kwarg): """assign material indices based on the objects materials. This works only, if the object has no materials assigned to it. - use: if set, an existing material of the same name is used, otherwise a new material is created. """ # material index is the index within the mesh, not the obj-file. # Two save material-indexes, assign materials only if there are # actual faces using them. m = geom['m'] material_index = 0 for (mat_id, mat_name) in enumerate (geom['mm']): # get a list of all faces with the given material id. fset = array ('i', filter (lambda idx: m[idx] == mat_id, range (len (m)))) # only create a material if there are actually faces using it. # This is just by taste and should probably be user-selectable. if len (fset) > 0: if use and mat_name in bpy.data.materials: # re-use the existing material blender_mat = bpy.data.materials[mat_name] else: blender_mat = bpy.data.materials.new (mat_name) # if the material already exists, force the name by explicitly assigning # it. Otherwise the new material would get a new name with a suffix. # this should probably be configurable, but this default-behavior is # slightly more predictable (old materials get renamed). blender_mat.name = mat_name mesh.data.materials.append (blender_mat) for fidx in fset: mesh.data.faces[fidx].material_index = material_index material_index += 1 # todo: find out if these updates are necessary. mesh.data.update () bpy.context.scene.update () @classmethod def define_weight_by_name (self, mesh, group, verts): """weight-paint a mesh. group is given by name and created if not existant. otherwise the same as define_weight(). Helper function for define_groups. """ if group in mesh.vertex_groups: bgroup = mesh.vertex_groups[group] else: bgroup = mesh.vertex_groups.new (group) bgroup.add (verts, 1, 'REPLACE') @classmethod def define_groups (self, mesh, geom): """assign vertex groups based on the object face-groups. """ # the model only contains a map containing group-sets and their ids. # So first split this into single groups. gnmap = self.create_vertex_groups (geom) for (gname, vidxs) in gnmap.items (): if len (vidxs) > 0: self.define_weight_by_name (mesh, gname, vidxs) @classmethod def define_model (self, geom, use_mat = True): """build a blender object from the model. kwarg use_mat: do not create material if already exists. """ # insert the vertices and basic faces into the model. mesh_obj = self.define_geom ('Mesh', geom) if 'g' in geom: self.log.info ("define groups") self.define_groups (mesh_obj, geom) if 'm' in geom: self.log.info ("define materials") self.define_materials (mesh_obj, geom, use = use_mat) mesh_obj.data.update () bpy.context.scene.update () return mesh_obj log = logging.getLogger ('mesh_import_dsf') bl_info = { 'name': 'import dsf-geom', 'description': 'import geometry from a dsf file.', 'author': 'millighost', 'version': (1, 0), 'blender': (2,5,8), 'category': 'Import-Export', 'warning': '', 'wiki_url': 'http://nonexistent', } def mesh_import (mod, use_mat = True, **kwarg): """mod is a mesh-model that is completely initialized. if use_mat is set, existing materials are re-used. """ qmod = mesh_convert.create_quad_model (mod) msh_dsf = mesh_define.define_model (qmod, use_mat = use_mat, **kwarg) return msh_dsf # create parser-flags from the dictionary props. def get_parser_flags (props): prop_map = { 'materials': 'm', 'groups': 'g', 'uvs': 'vt', } pflags = list () for prop in props: if prop in prop_map: pflags.append (prop_map[prop]) return pflags def import_dsf_file (filename, props): """load the dsf-file and create a mesh. Props is a list containing flags to be set. groups: define vertex groups. materials: assign material indexes. use_mat: use existing materials with same name. """ parser_flags = get_parser_flags (props) # parse the dsf-file. geom = dsf_geom_load.load_file (filename) # insert the quad-model into blender. dsf_geom_define.define_model (geom, use_mat = 'use_mat' in props) # the rest defines the gui and the blender operator class import_dsf (bpy.types.Operator): # the doc text is displayed in the tooltip of the menu entry. """Load a daz studio 4 dsf file.""" # the bl_label is displayed in the operator-menu (with space-KEY). bl_label = 'import dsf-geom' # the bl_idname member is used by blender to call this class. bl_idname = 'import.dsf' # the filepath seems to be hidden magic; the file-selector # menu places the chosen filename-string into it. # (changes sometimes; look for path/dirname/filepath) filepath = StringProperty\ (name = 'file path', description = 'file path for importing dsf-file.', maxlen = 1000, default = '') filter_glob = StringProperty (default = '*.dsf') # the following properties are given to the import_dsf_file function # as keyword-arguments. prop_materials = BoolProperty\ (name = 'materials', description = 'assign material names to faces', default = True) prop_use_material = BoolProperty\ (name = 'use material', description = 'use existing materials if available', default = True) prop_groups = BoolProperty\ (name = 'groups', description = 'assign vertex groups based on face groups', default = True) def execute (self, context): """display the gui and load a file. This function should be called after the menu entry for the file is selected.""" import_props = { 'materials': self.properties.prop_materials, 'use_material': self.properties.prop_use_material, 'groups': self.properties.prop_groups, 'uvs': True, } log.info ("execute (path = {0}, kwargs = {1})".format\ (self.properties.filepath, str (import_props))) # call the main import function. This function should work # independent of this context-manager/operator logic. import_dsf_file (self.properties.filepath, import_props) return { 'FINISHED' } def invoke (self, context, event): """The invoke function should be called when the menu-entry for this operator is selected. It displays a file-selector and waits for execute() to be called.""" context.window_manager.fileselect_add (self) return {'RUNNING_MODAL'} def menu_func (self, context): """display the menu entry for calling the importer.""" # the first parameter is the operator to call (by its bl_idname), # the text parameter is displayed in the menu. self.layout.operator (import_dsf.bl_idname, text = 'dsf-file (.dsf)') def register (): """add an operator for importing dsf-files and registers a menu function for it.""" bpy.utils.register_module (__name__) bpy.types.INFO_MT_file_import.append (menu_func) def unregister (): """remove the operator for importing dsf-files.""" bpy.utils.unregister_module (__name__) bpy.types.INFO_MT_file_import.remove (menu_func) logging.basicConfig (level = logging.INFO)