import json import logging import os import multiprocessing.dummy as mp import glob import bisect from clarisse_survival_kit.settings import * from clarisse_survival_kit.utility import * from clarisse_survival_kit.surface import Surface def inspect_asset(asset_directory): json_data = get_json_data_from_directory(asset_directory) if json_data: json_data['displacement_multiplier'] = 0.2 return json_data else: return None def import_asset(asset_directory, report=None, **kwargs): ix = get_ix(kwargs.get('ix')) asset_directory = os.path.join(os.path.normpath(asset_directory), '') if not report: report = inspect_asset(asset_directory) if report: if not kwargs.get('color_spaces'): kwargs['color_spaces'] = get_color_spaces(MEGASCANS_COLOR_SPACES, ix=ix) asset_type = report.get('type') if asset_type: if asset_type == 'surface': import_surface(asset_directory, **kwargs) elif asset_type == '3d': import_3d(asset_directory, **kwargs) elif asset_type == '3dplant': import_3dplant(asset_directory, **kwargs) elif asset_type == 'atlas': import_atlas(asset_directory, **kwargs) def import_surface(asset_directory, target_ctx=None, ior=DEFAULT_IOR, projection_type='triplanar', object_space=0, clip_opacity=True, color_spaces=None, triplanar_blend=0.5, resolution=None, lod=None, **kwargs): """Imports a Megascans surface.""" logging.debug("++++++++++++++++++++++.") logging.debug("Import Megascans surface called.") ix = get_ix(kwargs.get('ix')) if not target_ctx: target_ctx = ix.application.get_working_context() if not check_context(target_ctx, ix=ix): return None if not os.path.isdir(asset_directory): return ix.log_warning('Invalid directory specified: ' + asset_directory) if not color_spaces: color_spaces = get_color_spaces(MEGASCANS_COLOR_SPACES, ix=ix) logging.debug('Asset directory: ' + asset_directory) # Initial data json_data = get_json_data_from_directory(asset_directory) logging.debug('JSON data:') logging.debug(str(json_data)) if not json_data: ix.log_warning('Could not find a Megascans JSON file. Defaulting to standard settings.') surface_height = json_data.get('surface_height', DEFAULT_DISPLACEMENT_HEIGHT) displacement_offset = DEFAULT_DISPLACEMENT_OFFSET logging.debug('Surface height JSON test: ' + str(surface_height)) scan_area = json_data.get('scan_area', DEFAULT_UV_SCALE) logging.debug('Scan area JSON test: ' + str(scan_area)) tileable = json_data.get('tileable', True) asset_name = os.path.basename(os.path.normpath(asset_directory)) logging.debug('Asset name: ' + asset_name) if scan_area[0] >= 2 and scan_area[1] >= 2: height = 0.2 else: height = 0.02 # All assets except 3dplant have the material in the root directory of the asset. logging.debug('Searching for textures: ') lod_match_template = r'(?:_LOD(?P<lod>[0-9]*)|_NormalBump$)' textures = get_textures_from_directory(asset_directory, resolution=resolution, lod=lod, lod_match_template=lod_match_template) if not textures: ix.log_warning('No textures found in directory. Directory is empty or resolution is invalid.') return None logging.debug('Found textures: ') logging.debug(str(textures)) streamed_maps = get_stream_map_files(textures) if streamed_maps: logging.debug('Streamed maps: ') logging.debug(str(streamed_maps)) surface = Surface(ix, projection=projection_type, uv_scale=scan_area, height=height, tile=tileable, object_space=object_space, triplanar_blend=triplanar_blend, ior=ior, specular_strength=1, displacement_offset=displacement_offset) mtl = surface.create_mtl(asset_name, target_ctx) surface.create_textures(textures, color_spaces=color_spaces, streamed_maps=streamed_maps, clip_opacity=clip_opacity) logging.debug("Import Megascans surface done.") logging.debug("++++++++++++++++++++++.") return surface def import_3d(asset_directory, target_ctx=None, lod=None, resolution=None, clip_opacity=True, **kwargs): """Imports a Megascans 3D object.""" ix = get_ix(kwargs.get('ix')) logging.debug("*******************************") logging.debug("Importing Megascans 3d asset...") # Force projection to uv kwargs['projection_type'] = 'uv' surface = import_surface(asset_directory=asset_directory, target_ctx=target_ctx, clip_opacity=clip_opacity, resolution=resolution, lod=lod, **kwargs) if not surface: logging.debug('Material creation failed. Specified resolution is probably not valid.') return None asset_name = surface.name mtl = surface.mtl ctx = surface.ctx files = [f for f in os.listdir(asset_directory) if os.path.isfile(os.path.join(asset_directory, f))] lod_files = {} for f in files: filename, extension = os.path.splitext(f) if extension.lower() == ".obj": if filename.lower().endswith('_high'): if not lod_files.get(-1): lod_files[-1] = [] lod_files[-1].append(os.path.normpath(os.path.join(asset_directory, f))) else: lod_level_match = re.search(r'.*?LOD(?P<lod>[0-9]*)$', filename, re.IGNORECASE) if lod_level_match: lod_level = int(lod_level_match.group('lod')) if not lod_files.get(lod_level): lod_files[lod_level] = [] lod_files[lod_level].append(os.path.normpath(os.path.join(asset_directory, f))) logging.debug("Found obj: " + f) elif extension.lower() == ".abc": abc_reference = ix.cmds.CreateFileReference(str(ctx), [os.path.normpath(os.path.join(asset_directory, f))]) for item in get_items(abc_reference, kind=('GeometryAbcMesh', 'AbcXform'), ix=ix): if not item.attrs.parent[0]: item.attrs.scale_offset[0] = .01 item.attrs.scale_offset[1] = .01 item.attrs.scale_offset[2] = .01 if lod_files: logging.debug('Lod files:') logging.debug(str(lod_files)) keys = lod_files.keys() keys.sort() search_key = lod if lod is not None else -1 search_key_index = bisect.bisect(keys, search_key) logging.debug(str(search_key_index)) files = lod_files.get(keys[search_key_index - 1]) logging.debug('Files:') logging.debug(str(files)) if files: for f in files: filename, extension = os.path.splitext(os.path.basename(f)) polyfile = ix.cmds.CreateObject(filename, "GeometryPolyfile", "Global", str(ctx)) ix.cmds.SetValue(str(polyfile) + ".filename", [f]) # Megascans .obj files are saved in cm, Clarisse imports them as meters. polyfile.attrs.scale_offset[0] = .01 polyfile.attrs.scale_offset[1] = .01 polyfile.attrs.scale_offset[2] = .01 geo = polyfile.get_module() for i in range(geo.get_shading_group_count()): logging.debug('Applying material to geometry') geo.assign_material(mtl.get_module(), i) ix.application.check_for_events() if clip_opacity and surface.get('opacity'): logging.debug('Applying clip map') geo.assign_clip_map(surface.get('opacity').get_module(), i) if not filename.endswith("_High") and surface.get('displacement'): logging.debug('Applying displacement map') geo.assign_displacement(surface.get('displacement_map').get_module(), i) logging.debug("Creating shading layers..") shading_layer = ix.cmds.CreateObject(asset_name + SHADING_LAYER_SUFFIX, "ShadingLayer", "Global", str(ctx)) ix.cmds.AddShadingLayerRule(str(shading_layer), 0, ["filter", "", "is_visible", "1"]) ix.cmds.SetShadingLayerRulesProperty(str(shading_layer), [0], "filter", ["./*"]) if lod in MESH_LOD_DISPLACEMENT_LEVELS and surface.get('displacement_map'): ix.cmds.SetShadingLayerRulesProperty(str(shading_layer), [0], "displacement", [str(surface.get('displacement_map'))]) ix.cmds.SetShadingLayerRulesProperty(str(shading_layer), [0], "material", [str(mtl)]) if surface.get('opacity') and clip_opacity: ix.cmds.SetShadingLayerRulesProperty(str(shading_layer), [0], "clip_map", [str(surface.get('opacity'))]) logging.debug("...done creating shading layers and importing 3d object.") logging.debug("********************************************************") def import_atlas(asset_directory, target_ctx=None, lod=None, clip_opacity=True, resolution=None, use_displacement=True, **kwargs): """Imports a Megascans 3D object.""" ix = get_ix(kwargs.get('ix')) logging.debug("*******************") logging.debug("Setting up atlas...") # Force projection to UV kwargs['projection_type'] = 'uv' surface = import_surface(asset_directory=asset_directory, target_ctx=target_ctx, clip_opacity=clip_opacity, double_sided=True, resolution=resolution, **kwargs) if not surface: logging.debug('Material creation failed. Specified resolution is probably not valid.') return None asset_name = surface.name mtl = surface.mtl ctx = surface.ctx files = [f for f in os.listdir(asset_directory) if os.path.isfile(os.path.join(asset_directory, f))] polyfiles = [] for key, f in enumerate(files): filename, extension = os.path.splitext(f) if extension.lower() == ".obj": logging.debug("Found obj: " + f) polyfile = ix.cmds.CreateObject(filename, "GeometryPolyfile", "Global", str(ctx)) polyfile.attrs.filename = os.path.normpath(os.path.join(asset_directory, f)) geo = polyfile.get_module() for i in range(geo.get_shading_group_count()): geo.assign_material(mtl.get_module(), i) if surface.get('opacity') and clip_opacity: geo.assign_clip_map(surface.get('opacity').get_module(), i) if surface.get('displacement') and use_displacement: geo.assign_displacement(surface.get('displacement').get_module(), i) polyfiles.append(polyfile) elif extension.lower() == ".abc": logging.debug("Found abc: " + f) abc_reference = ix.cmds.CreateFileReference(str(ctx), [os.path.normpath(os.path.join(asset_directory, f))]) logging.debug("Setting up shading layer: ") if files: shading_layer = ix.cmds.CreateObject("shading_layer", "ShadingLayer", "Global", str(ctx)) ix.cmds.AddShadingLayerRule(str(shading_layer), 0, ["filter", "", "is_visible", "1"]) ix.cmds.SetShadingLayerRulesProperty(str(shading_layer), [0], "filter", ["./*"]) ix.cmds.SetShadingLayerRulesProperty(str(shading_layer), [0], "material", [str(mtl)]) if surface.get('opacity'): ix.cmds.SetShadingLayerRulesProperty(str(shading_layer), [0], "clip_map", [str(surface.get('opacity'))]) if surface.get('displacement'): ix.cmds.SetShadingLayerRulesProperty(str(shading_layer), [0], "displacement", [str(surface.get('displacement_map'))]) logging.debug("...done setting up shading layer") logging.debug("Setting up group: ") group = ix.cmds.CreateObject(asset_name + GROUP_SUFFIX, "Group", "Global", str(ctx)) group.attrs.inclusion_rule = "./*" ix.cmds.AddValues([group.get_full_name() + ".filter"], ["GeometryAbcMesh"]) ix.cmds.AddValues([group.get_full_name() + ".filter"], ["GeometryPolyfile"]) ix.cmds.RemoveValue([group.get_full_name() + ".filter"], [2, 0, 1]) logging.debug("...done setting up group and atlas") logging.debug("**********************************") def import_3dplant(asset_directory, target_ctx=None, ior=DEFAULT_IOR, object_space=0, clip_opacity=True, use_displacement=True, color_spaces=MEGASCANS_COLOR_SPACES, triplanar_blend=0.5, resolution=None, lod=None, **kwargs): """Imports a Megascans 3D object.""" ix = get_ix(kwargs.get('ix')) logging.debug("*******************") logging.debug("Setting up 3d plant...") # Megascans 3dplant importer. The 3dplant importer requires 2 materials to be created. # Let's first find the textures of the Atlas and create the material. if not target_ctx: target_ctx = ix.application.get_working_context() if not check_context(target_ctx, ix=ix): return None asset_directory = os.path.normpath(asset_directory) if not os.path.isdir(asset_directory): return ix.log_warning("Invalid directory specified: " + asset_directory) logging.debug("Asset directory: " + asset_directory) # Initial data json_data = get_json_data_from_directory(asset_directory) logging.debug("JSON data:") logging.debug(str(json_data)) if not json_data: ix.log_warning("Could not find a Megascans JSON file. Defaulting to standard settings.") asset_type = json_data.get('type', 'surface') logging.debug("Asset type from JSON test: " + asset_type) surface_height = json_data.get('surface_height', DEFAULT_DISPLACEMENT_HEIGHT) logging.debug("Surface height JSON test: " + str(surface_height)) scan_area = json_data.get('scan_area', DEFAULT_UV_SCALE) logging.debug("Scan area JSON test: " + str(scan_area)) tileable = json_data.get('tileable', True) asset_name = os.path.basename(os.path.normpath(asset_directory)) logging.debug("Asset name: " + asset_name) logging.debug(os.path.join(asset_directory, 'Textures/Atlas/')) atlas_textures = get_textures_from_directory(os.path.join(asset_directory, 'Textures/Atlas/'), resolution=resolution) if not atlas_textures: ix.log_warning("No atlas textures found in directory. Files might have been exported flattened from Bridge.\n" "Testing import as Atlas.") import_atlas(asset_directory, target_ctx=target_ctx, use_displacement=use_displacement, clip_opacity=clip_opacity, resolution=resolution, **kwargs) return None logging.debug("Atlas textures: ") logging.debug(str(atlas_textures)) streamed_maps = get_stream_map_files(atlas_textures) logging.debug("Atlas streamed maps: ") logging.debug(str(streamed_maps)) atlas_surface = Surface(ix, projection='uv', uv_scale=scan_area, height=scan_area[0], tile=tileable, object_space=object_space, triplanar_blend=triplanar_blend, ior=ior, double_sided=True, specular_strength=1, displacement_multiplier=0.1) plant_root_ctx = ix.cmds.CreateContext(asset_name, "Global", str(target_ctx)) atlas_mtl = atlas_surface.create_mtl(ATLAS_CTX, plant_root_ctx) atlas_surface.create_textures(atlas_textures, color_spaces=color_spaces, streamed_maps=streamed_maps, clip_opacity=clip_opacity) atlas_ctx = atlas_surface.ctx # Find the textures of the Billboard and create the material. billboard_textures = get_textures_from_directory(os.path.join(asset_directory, 'Textures/Billboard/'), resolution=resolution) if not billboard_textures: ix.log_warning("No Billboard textures found in directory.") else: logging.debug("Billboard textures: ") logging.debug(str(billboard_textures)) streamed_maps = get_stream_map_files(billboard_textures) logging.debug("Billboard streamed maps: ") logging.debug(str(streamed_maps)) billboard_surface = Surface(ix, projection='uv', uv_scale=scan_area, height=scan_area[0], tile=tileable, object_space=object_space, triplanar_blend=triplanar_blend, ior=ior, double_sided=True, specular_strength=1, displacement_multiplier=0.1) billboard_mtl = billboard_surface.create_mtl(BILLBOARD_CTX, plant_root_ctx) billboard_surface.create_textures(billboard_textures, color_spaces=color_spaces, streamed_maps=streamed_maps, clip_opacity=clip_opacity) billboard_ctx = billboard_surface.ctx for dir_name in os.listdir(asset_directory): variation_dir = os.path.join(asset_directory, dir_name) if os.path.isdir(variation_dir) and dir_name.startswith('Var'): logging.debug("Variation dir found: " + variation_dir) files = [f for f in os.listdir(variation_dir) if os.path.isfile(os.path.join(variation_dir, f))] # Search for models files and apply material for f in files: filename, extension = os.path.splitext(f) if extension.lower() == ".obj": logging.debug("Found obj: " + f) filename, extension = os.path.splitext(f) polyfile = ix.cmds.CreateObject(filename, "GeometryPolyfile", "Global", str(plant_root_ctx)) ix.cmds.SetValue(str(polyfile) + ".filename", [os.path.normpath(os.path.join(variation_dir, f))]) # Megascans .obj files are saved in cm, Clarisse imports them as meters. polyfile.attrs.scale_offset[0] = .01 polyfile.attrs.scale_offset[1] = .01 polyfile.attrs.scale_offset[2] = .01 geo = polyfile.get_module() for i in range(geo.get_shading_group_count()): if filename.endswith('3'): geo.assign_material(billboard_mtl.get_module(), i) if clip_opacity and billboard_surface.get('opacity'): geo.assign_clip_map((billboard_surface.get('opacity')).get_module(), i) else: geo.assign_material(atlas_mtl.get_module(), i) if clip_opacity and atlas_surface.get('opacity'): geo.assign_clip_map(atlas_surface.get('opacity').get_module(), i) lod_level_match = re.sub('.*?([0-9]*)$', r'\1', filename) if int(lod_level_match) in ATLAS_LOD_DISPLACEMENT_LEVELS and use_displacement: geo.assign_displacement(atlas_surface.get('displacement_map').get_module(), i) elif extension.lower() == ".abc": logging.debug("Found abc: " + f) abc_reference = ix.cmds.CreateFileReference(str(plant_root_ctx), [os.path.normpath(os.path.join(variation_dir, f))]) for item in get_items(abc_reference, kind=('GeometryAbcMesh', 'AbcXform'), ix=ix): if not item.attrs.parent[0]: item.attrs.scale_offset[0] = .01 item.attrs.scale_offset[1] = .01 item.attrs.scale_offset[2] = .01 shading_layer = ix.cmds.CreateObject(asset_name + SHADING_LAYER_SUFFIX, "ShadingLayer", "Global", str(plant_root_ctx)) logging.debug("Creating shading layers and groups...") for i in range(0, 4): ix.cmds.AddShadingLayerRule(str(shading_layer), i, ["filter", "", "is_visible", "1"]) ix.cmds.SetShadingLayerRulesProperty(str(shading_layer), [i], "filter", ["./*LOD" + str(i) + "*"]) ix.cmds.SetShadingLayerRulesProperty(str(shading_layer), [i], "material", [str(atlas_mtl)]) if atlas_surface.get('opacity') and clip_opacity: ix.cmds.SetShadingLayerRulesProperty(str(shading_layer), [i], "clip_map", [str(atlas_surface.get('opacity'))]) if atlas_surface.get('displacement') and i in ATLAS_LOD_DISPLACEMENT_LEVELS and use_displacement: ix.cmds.SetShadingLayerRulesProperty(str(shading_layer), [i], "displacement", [str(atlas_surface.get('displacement_map'))]) group = ix.cmds.CreateObject(asset_name + "_LOD" + str(i) + GROUP_SUFFIX, "Group", "Global", str(plant_root_ctx)) group.attrs.inclusion_rule = "./*LOD" + str(i) + "*" ix.cmds.AddValues([group.get_full_name() + ".filter"], ["GeometryAbcMesh"]) ix.cmds.AddValues([group.get_full_name() + ".filter"], ["GeometryPolyfile"]) ix.cmds.RemoveValue([group.get_full_name() + ".filter"], [2, 0, 1]) logging.debug("...done setting up shading rules, groups and 3d plant") logging.debug("*****************************************************") def get_json_data_from_directory(directory): """Get the JSON data contents required for material setup.""" logging.debug("Searching for JSON...") files = [f for f in os.listdir(directory) if os.path.isfile(os.path.join(directory, f))] # Search for any JSON file. Custom Mixer scans don't have a suffix like the ones from the library. data = {} for f in files: filename, extension = os.path.splitext(f) if extension == ".json": logging.debug("...JSON found!!!") json_file = os.path.join(directory, filename + ".json") with open(json_file) as json_file: json_data = json.load(json_file) if not json_data: return None meta_data = json_data.get('meta') logging.debug("Meta JSON Data: " + str(meta_data)) if not meta_data: return None categories = json_data.get('categories') logging.debug("Categories JSON Data: " + str(categories)) if not categories: return None maps = json_data.get('maps') logging.debug("JSON follows Megascans structure.") if categories: if 'surface' in categories: data['type'] = 'surface' if '3d' in categories: data['type'] = '3d' if 'atlas' in categories: data['type'] = 'atlas' if '3dplant' in categories: data['type'] = '3dplant' if meta_data: for md in meta_data: if md['key'] == "height": data['surface_height'] = float((md['value']).replace("m", "").replace(" ", "")) elif md['key'] == "scanArea": data['scan_area'] = [float(val) for val in (md['value']).replace("m", "").replace(" ", "").split("x")] elif md['key'] == "tileable": data['tileable'] = md['value'] if maps: for mp in maps: if mp['type'] == 'displacement' and 'maxIntensity' in mp and 'minIntensity' in mp: # getting average intensity, using 260 as max RGB since that's what Megascans is doing data['displacement_offset'] = ((mp['maxIntensity'] + mp['minIntensity']) * 0.5) / 260.0 break return data def import_ms_library(library_dir, target_ctx=None, lod=None, custom_assets=True, resolution=None, skip_categories=(), **kwargs): """Imports the whole Megascans Library. Point it to the Downloaded folder inside your library folder. """ logging.debug("Importing Megascans library...") ix = get_ix(kwargs.get("ix")) if not target_ctx: target_ctx = ix.application.get_working_context() if not check_context(target_ctx, ix=ix): return None if not os.path.isdir(library_dir): return None if os.path.isdir(os.path.join(library_dir, "Downloaded")): library_dir = os.path.join(library_dir, "Downloaded") logging.debug("Directory set to: " + library_dir) print "Scanning folders in " + library_dir for category_dir_name in os.listdir(library_dir): category_dir_path = os.path.join(library_dir, category_dir_name) logging.debug("Checking if directory contains matches keywords: " + category_dir_name) if category_dir_name in ["3d", "3dplant", "surface", "surfaces", "atlas", "atlases"]: if category_dir_name not in skip_categories and os.path.isdir(category_dir_path): context_name = category_dir_name if os.path.basename(library_dir) == "My Assets" and category_dir_name == "surfaces": context_name = LIBRARY_MIXER_CTX ctx = ix.item_exists(str(target_ctx) + "/" + MEGASCANS_LIBRARY_CATEGORY_PREFIX + context_name) if not ctx: ctx = ix.cmds.CreateContext(MEGASCANS_LIBRARY_CATEGORY_PREFIX + context_name, "Global", str(target_ctx)) print "Importing library folder: " + category_dir_name for asset_directory_name in os.listdir(category_dir_path): asset_directory_path = os.path.join(category_dir_path, asset_directory_name) if os.path.isdir(asset_directory_path): if not ix.item_exists(str(ctx) + "/" + asset_directory_name): print "Importing asset: " + asset_directory_path import_asset(asset_directory_path, resolution=resolution, lod=lod, target_ctx=ctx, ix=ix) if custom_assets and os.path.isdir(os.path.join(library_dir, "My Assets")): logging.debug("My Assets exists...") import_ms_library(os.path.join(library_dir, "My Assets"), target_ctx=target_ctx, skip_categories=skip_categories, lod=lod, resolution=resolution, custom_assets=False, ix=ix)