def build(root, models, links_simple, links_revert, shapes_ids): """ Put shapes in the scene and apply the correct transforms. """ if config.verbose: print("models_builder_fbx launched") comment = etree.Comment("Models.") root.append(comment) # Create a dictionnary of models to pass through every parent models_id = {} for model in models: id, type, obj = model.get("value").replace("::", "").split(",") models_id[id] = tools.getProperties(model) for model in models: id, type, obj = model.get("value").replace("::", "").split(",") if id in links_simple: for link in links_simple[id]: if link in shapes_ids: # Only shapes (for now ?) curr_shape = etree.Element("shape") curr_shape.set("type", "instance") curr_ref = etree.SubElement(curr_shape, "ref") curr_ref.set("id", link) recursive_build_hierarchy(root, curr_shape, links_simple, links_revert, models_id, id) elif config.verbose: print("id " + id + " not in links_simple")
def build(root, textures, links_param_revert): """ Creates textures with in the scene. Return a list of texture ids. """ verbose = config.verbose if verbose: print("textures_builder_fbx launched") comment = etree.Comment("Textures with ids") root.append(comment) textures_id = {} if textures != [] and not os.path.exists(config.filepath + "export\\textures"): os.makedirs(config.filepath + "export\\textures") copyfile(config.curr_place + "\\missing_texture.png", config.filepath + "export\\textures\\missing_texture.png") # Go through all textures in the scene for texture in textures: id, type, obj = texture.get("value").replace("::", "").split(",") rel_reference = texture.find("RelativeFilename").text.strip() abs_reference = texture.find("FileName").text.strip() properties = tools.getProperties(texture) uoff, voff = properties["Translation"][ -3:-1] if "Translation" in properties else ["0", "0"] uscaling, vscaling = properties["Scaling"][ -3:-1] if "Scaling" in properties else ["1", "1"] if rel_reference == "" and abs_reference == "": if verbose: print("Empty reference for id " + id + ", replacing with error texture") reference = "textures\\missing_texture.png" # Try relative path then absolute if it did'nt work elif not Path(config.filepath + rel_reference).is_file(): if not Path(abs_reference).is_file(): if verbose: print("Missing texture : " + rel_reference) reference = "textures\\missing_texture.png" else: new_reference = "textures\\" + id + "." + abs_reference.split( ".")[-1] copyfile(abs_reference, config.filepath + "export\\" + new_reference) reference = new_reference else: new_reference = "textures\\" + id + "." + abs_reference.split( ".")[-1] copyfile(config.filepath + rel_reference, config.filepath + "export\\" + new_reference) reference = new_reference if id not in links_param_revert: if verbose: print("Texture " + reference + " never used. Not writing it to file.") elif any(reference.lower().endswith(s) for s in [".bmp", ".jpg", ".png", ".tga", ".exr"]): textures_id[id] = properties textures_id[id]["reference"] = reference curr_texture = tools.create_obj(root, "texture", "bitmap", id) tools.set_value(curr_texture, "string", "filename", reference) # Avoid cluttering for the final file, removing 0 offsets and 1 scaling if uoff != "0": tools.set_value(curr_texture, "float", "uoffset", uoff) if voff != "0": tools.set_value(curr_texture, "float", "voffset", voff) if uscaling != "1": tools.set_value(curr_texture, "float", "uscale", uscaling) if vscaling != "-1": tools.set_value(curr_texture, "float", "vscale", str(-float(vscaling))) # Textures in 3dsmax have their v values inverted compared to mitsuba. else: print("Unknown texture file type : " + reference.split(".")[-1]) print("for file : " + reference) return textures_id
def build(fbxtree): """ This function builds the scene from the fbx tree. """ verbose = config.verbose if verbose: print("builder_fromfbx launched") # Get information about the vertical axis used globalsettings = tools.getProperties(fbxtree.find("GlobalSettings")) config.upvector = "0 1 0" if globalsettings["UpAxis"][ -1] == "1" else "0 0 1" objects = fbxtree.find("Objects") # Separate objects into different categories models = objects.findall("Model") geometries = objects.findall("Geometry") materials = objects.findall("Material") nodes = objects.findall( "NodeAttribute") # Contains some cameras and lights properties # videos = objects.findall("Video") # Not sure about what it contains, it seems to be a duplicate of “textures”. Maybe for compatibility reasons ? textures = objects.findall("Texture") nulls = objects.findall("Null") # Useful for cameras and spot targets """ Useless for now # Animation. Will probably not be usable anim_c_n = objects.findall("AnimationCurveNode") anim_c = objects.findall("AnimationCurve") # Maybe useful. Not sure what to do with this. implem = objects.find("Implementation") bind_table= objects.find("BindingTable") bind_oper = objects.find("BindingOperator") """ # Useful to link objects, textures to materials, materials to models, and geometry to models. connections_list = fbxtree.find("Connections") comments = connections_list.findall("comment") links = connections_list.findall("C") # Stocking links between objects. # Links simple and links revert are simple links, for example for hierarchies of objects. # Links param and links param revert are for parameters, for example, a texture referenced by a material. links_simple, links_revert, links_param, links_param_revert = tools.extract_links( links) root = etree.Element("scene") root.set("version", "0.5.0") # Set up the integrator. I chose pathtracing by default TODO add as a parameter tools.create_obj(root, "integrator", "path") if not os.path.exists(config.filepath + "export"): os.makedirs(config.filepath + "export") # All the functions will directly add their elements to the root element light_cam_builder.build(root, nodes, models, nulls, links_simple, links_param) textures_id = textures_builder.build(root, textures, links_param_revert) materials_ids = materials_builder.build(root, materials, textures_id, links_param, links_param_revert) shapes_ids = shapes_builder.build(root, geometries, materials_ids, links_simple, links_revert) models_builder.build(root, models, links_simple, links_revert, shapes_ids) if verbose: print("Writing to file…") with open(config.filepath + "export\\" + config.filename + ".xml", "w", encoding="utf8") as outputfile: if verbose: print("prettifying… (Can take a while for big files)") stringout = tools.prettifyXml(etree.tostring(root).decode()) if verbose: print("writing…") outputfile.write(stringout)
def build(root, geometries, materials_ids, links_simple, links_revert): """ Exports all the geometry into a folder for later use in the scene. Return a list of ids for the geometry """ if config.verbose: print("shapes_builder_fbx launched") savepath = config.filepath + "export\\meshes\\" if not os.path.exists(savepath): os.makedirs(savepath) comment = etree.Comment("Shapes.") root.append(comment) geometries_id = [] # Only keep geometries with polygonvertex geometries = [ geometry for geometry in geometries if geometry.find("PolygonVertexIndex") != None ] for geometry in geometries: id, type, obj = geometry.get("value").replace("::", "").split(", ") if obj != "Mesh": print("Unknown Geometry obj : " + obj + " (id=" + id + ")") geometries_id.append(id) if id not in links_revert: if verbose: print("Model " + id + " never used. Not writing it to file.") else: properties = tools.getProperties(geometry) linked_materials = links_revert[id] if id in links_revert else [ ] # Get the model(s) containing this geometry (Only the models reference materials) if len(linked_materials) == 1: linked_materials = links_simple[linked_materials[0]] else: print("Multiple models containing geometry " + id + " ???") linked_materials = [] linked_materials = [ link for link in linked_materials if link in materials_ids ] #Only keep ids of materials # I think that the index for materials is based on the order they appear in the file # Maybe not, but i see no other info in the fbx to know that, so that's what i use and it seems to work vertices_data = geometry.find("Vertices") nb_vertices = int(vertices_data.get("value").replace("*", "")) polygons_data = geometry.find("PolygonVertexIndex") nb_poly_ind = int(polygons_data.get("value").replace("*", "")) edges_data = geometry.find("Edges") nb_edges = int(edges_data.get("value").replace("*", "")) normal_layer = geometry.find("LayerElementNormal") if normal_layer != None: normals_data = normal_layer.find("Normals") normalsW_data = normal_layer.find("NormalsW") nb_normals = int(normals_data.get("value").replace("*", "")) nb_normalsW = int(normalsW_data.get("value").replace("*", "")) uv_layer = geometry.find("LayerElementUV") if uv_layer != None: uv_data = uv_layer.find("UV") uv_index_data = uv_layer.find("UVIndex") nb_uv_data = int(uv_data.get("value").replace("*", "")) nb_uv_index = int(uv_index_data.get("value").replace("*", "")) material_layer = geometry.find("LayerElementMaterial") if material_layer != None: material_data = list( map( int, material_layer.find("Materials").find("a").text.split( ","))) if material_layer.find( "MappingInformationType").text == "AllSame": material_data = nb_poly_ind * [material_data[0]] vertices_in = [ tools.str2float2str(num) for num in vertices_data.find("a").text.split(",") ] polygons_in = list( map(int, polygons_data.find("a").text.split(","))) edges_in = [ tools.str2float2str(num) for num in edges_data.find("a").text.split(",") ] normals_in = [ tools.str2float2str(num) for num in normals_data.find("a").text.split(",") ] normalsW_in = [ tools.str2float2str(num) for num in normalsW_data.find("a").text.split(",") ] uv_in = [ tools.str2float2str(num) for num in uv_data.find("a").text.split(",") ] uv_index_in = list( map(int, uv_index_data.find("a").text.split(","))) vertices, polygon_vertex_index, normals, uv = [], [], [], [] if nb_vertices % 3 != 0: print("Points values not a multiple of 3 !") for i in range(int(nb_vertices / 3)): vertices.append(vertices_in[3 * i:3 * i + 3]) curr_vertex = [] for index in polygons_in: if index < 0: curr_vertex.append(-index - 1) polygon_vertex_index.append(curr_vertex) curr_vertex = [] else: curr_vertex.append(index) nb_polygons = len(polygon_vertex_index) if normal_layer != None: normal_type = normal_layer.find( "ReferenceInformationType").text if normal_type == "Direct": for i in range(int(nb_normals / 3)): normals.append(normals_in[3 * i:3 * i + 3]) elif normal_type == "IndexToDirect": # TODO print("NORMAL INDEXTODIRECT DOESN'T WORK RIGHT NOW") else: print( "Unknown ReferenceInformationType for normal in obj " + id) else: print("NO NORMALS for object with id " + id) if uv_layer != None: uv_type = uv_layer.find("ReferenceInformationType").text if uv_type == "Direct": for i in range(int(nb_uv_data / 2)): uv.append(uv_in[2 * i:2 * i + 2]) elif uv_type == "IndexToDirect": for i in range(int(nb_uv_index)): index = uv_index_in[i] uv.append(uv_in[2 * index:2 * index + 2]) else: print("Unknown ReferenceInformationType for uv in obj " + id) uv = ["0 0"] * nb_poly_ind else: if config.verbose: print("No uv for object with id " + id + ". Using default of 0, 0") uv = ["0 0"] * nb_poly_ind materials = geometry.find("LayerElementMaterial") if materials != None: materials = list( map(int, materials.find("Materials").find("a").text.split(","))) if materials == None or len(materials) <= 1: materials = [0 for i in range(nb_polygons)] max_material = max(materials) # The shapegroup will contain all meshes with different materials, and allow instanciation shapegroup = tools.create_obj(root, "shape", "shapegroup", id) # Initialize for i in range(max_material + 1): vertex_text = [] poly_index = [] total_index = 0 curr_polygon_vertex_num = 0 for j in range(len(polygon_vertex_index)): vertex_indexes = polygon_vertex_index[j] curr_poly_index = [] for k in range(len(vertex_indexes)): # Only keep polygons with the current material if material_layer == None or material_data[j] == i: curr_poly_index.append(str(total_index)) total_index += 1 vertex_index = vertex_indexes[k] curr_vertex_text = " ".join( vertices[vertex_index]) + " " curr_vertex_text += " ".join( normals[curr_polygon_vertex_num]) + " " curr_vertex_text += " ".join( uv[curr_polygon_vertex_num]) vertex_text.append(curr_vertex_text) curr_polygon_vertex_num += 1 # Generate multiple triangle to replace polygons (not supported by Mitsuba Renderer) if len(curr_poly_index) > 3: for k in range(len(curr_poly_index) - 2): curr_poly = [curr_poly_index[0] ] + curr_poly_index[k + 1:k + 3] poly_index.append(curr_poly) elif len(curr_poly_index) > 0: poly_index.append(curr_poly_index) if vertex_text != []: # Export only non-empty objects output = open(savepath + id + "_" + str(i) + ".ply", "w") output.write("ply\n") output.write("format ascii 1.0\n") output.write("element vertex " + str(len(vertex_text)) + "\n") output.write("""property float32 x property float32 y property float32 z property float32 nx property float32 ny property float32 nz property float32 u property float32 v """) output.write("element face " + str(len(poly_index)) + "\n") output.write("property list uint8 int32 vertex_indices\n") output.write("end_header\n") for vert in vertex_text: output.write(vert + "\n") for poly in poly_index: # Only triangles output.write("3 " + " ".join(poly) + "\n") shape = tools.create_obj(shapegroup, "shape", "ply") tools.set_value(shape, "string", "filename", "meshes/" + id + "_" + str(i) + ".ply") try: tools.set_value(shape, "ref", "bsdf", linked_materials[i]) except IndexError: if config.verbose: print("No material found for object " + id + ", index " + str(i)) return geometries_id
def build(root, nodes, models, nulls, links_simple, links_param): """ Creates lights and cameras in the scene """ verbose = config.verbose debug = config.debug if verbose: print("lightsandcameras_builder_fbx launched") comment = etree.Comment("Lights and cameras") root.append(comment) # Prepare nodes properties referenced by id for easy access camera_nodes, light_nodes = {}, {} for node in nodes: id, type, obj = node.get("value").replace("::", "").split(", ") properties = tools.getProperties(node) # named by id to find easily when needed node.tag = id if obj == "Camera": camera_nodes[id] = properties elif obj == "Light": light_nodes[id] = properties # Prepare models properties referenced by id for easy access camera_models, light_models, null_models = {}, {}, {} for model in models: id, type, obj = model.get("value").replace("::", "").split(", ") properties = tools.getProperties(model) # named by id to find easily when needed node.tag = id if obj == "Camera": camera_models[id] = properties elif obj == "Light": light_models[id] = properties elif obj == "Null": null_models[id] = properties # Lights # Every light placed in the scene is referenced by a model. # This model should be linked to a node containing all the light's properties. # Once we retrieved it, we are able to create the light correctly for id, light_model in light_models.items(): if len(links_simple[id]) != 1: print("light_cam_builder_fbx : " + len(links_simple[id]) + " node(s) for light model " + id) light_node = light_nodes[links_simple[id][0]] light_is_a_spot = "LookAtProperty" in links_param[ id] # LightType is present when the light has a target if "3dsMax|FSphereExtParameters|light_radius" in light_node: # This parameter means that the light is a sphere light_is_a_sphere = True sphere_radius = float( light_node["3dsMax|FSphereExtParameters|light_radius"][-1]) else: light_is_a_sphere = False # This is a mess. TODO clean all this, it's not legible if "3dsMax|FAreaParameters" in light_node: #TODO area lights print("Area lights not supported yet, light with id " + id + "will be considered as a point light for now") colors = light_node["Color"][-3:] if "Color" in light_node else [ "1", "1", "1" ] elif light_node["3dsMax|" + ( ("T" if light_is_a_spot else "F") + ("Sphere" if light_is_a_sphere else "Point") + "Parameters") + "|useKelvin"][-1] == "1": colors = tools.kelvin2rgb( float(light_node["3dsMax|" + ("FSphereParameters" if light_is_a_sphere else "FPointParameters") + "|kelvin"][-1])) # TODO color filter else: colors = light_node["Color"][-3:] if "Color" in light_node else [ "1" ] # Divide intensity by apparent surface of the sphere (disc) if it's not a point intensity = float(light_node["Intensity"][-1]) / ( 2. * math.pi * sphere_radius**2. if light_is_a_sphere else 1) rvb = [] for color in colors: rvb.append(str(intensity * float(color))) if light_is_a_sphere: light_shape = tools.create_obj(root, "shape", "sphere") tools.set_value(light_shape, "float", "radius", str(sphere_radius)) light = tools.create_obj( parent=light_shape if light_is_a_sphere else root, object="emitter", type="spot" if light_is_a_spot else "area" if light_is_a_sphere else "point") tools.set_value( light, "spectrum", "radiance" if light_is_a_sphere and not light_is_a_spot else "intensity", " ".join(rvb)) if light_is_a_spot: # angles are divided by 2 because 3dsMax expresses the total angle, and mitsuba the angle from the center of the ray tools.set_value(light, "float", "cutoffAngle", str(.5 * float(light_node["OuterAngle"][-1]))) tools.set_value(light, "float", "beamWidth", str(.5 * float(light_node["InnerAngle"][-1]))) tools.transform_lookat( light, " ".join(light_model["Lcl Translation"][-3:]), " ".join(null_models[links_param[id]["LookAtProperty"]] ["Lcl Translation"][-3:])) else: tools.transform_object(light_shape if light_is_a_sphere else light, light_model) # Handle cameras for id, camera_model in camera_models.items(): if len(links_simple[id]) != 1: print("light_cam_builder_fbx : " + str(len(links_simple[id])) + " node(s) for camera model " + id) camera_node = camera_nodes[links_simple[id][0]] curr_camera = tools.create_obj(root, "sensor", "perspective") curr_film = tools.create_obj(curr_camera, "film", "hdrfilm") tools.set_value(curr_film, "integer", "width", camera_node["AspectWidth"][-1]) tools.set_value(curr_film, "integer", "height", camera_node["AspectHeight"][-1]) rfilter = tools.create_obj(curr_film, "rfilter", "box") # Set up with nice values. TODO parameters to control this curr_sampler = tools.create_obj(curr_camera, "sampler", "ldsampler") tools.set_value(curr_sampler, "integer", "sampleCount", "64") tools.transform_lookat_from_properties(curr_camera, camera_node) tools.set_value(curr_camera, "float", "fov", camera_node["FieldOfView"][-1]) tools.set_value(curr_camera, "string", "fovAxis", "x") # Max expresses the horizontal fov
def build(root, materials, textures_id, links_param, links_param_revert): """ Creates materials with ids in the scene. Returns a list of materials id. """ if config.verbose : print("materials_builder_fbx launched") comment = etree.Comment("Materials with ids") root.append(comment) material_ids = [] for material in materials : id, type, obj = material.get("value").replace("::","").split(",") material_ids.append(id) # Get linked parameters (only textures ?) linked = links_param[id] if id in links_param else [] properties = tools.getProperties(material) diffuse_color =(" ".join(properties["Diffuse"][-3:]) if "Diffuse" in properties else(" ".join(properties["DiffuseColor"][-3:]) if "DiffuseColor" in properties else "1 0 0")) #Use red if there is no diffuse, for debug specular_color = " ".join(properties["Specular"][-3:]) if "Specular" in properties else "1 1 1" shininess = properties["ShininessExponent"][-1] if "ShininessExponent" in properties else "" # If bumpmap is on, use it if ("3dsMax|Parameters|bump_map" in linked) :# Use bump map scale = properties["3dsMax|Parameters|bump_map_amt"][-1] curr_bumpmod = tools.create_obj(root, "bsdf", "bumpmap", id) set_texture(curr_bumpmod, linked, "3dsMax|Parameters|bump_map", scale) curr_material = etree.SubElement(curr_bumpmod, "bsdf") else : curr_material = etree.SubElement(root, "bsdf") curr_material.set("id", id) # Roughness if "3dsMax|Parameters|roughness" in properties : if "3dsMax|Parameters|roughness_map" in linked : # Use a texture inverted_roughness = properties["3dsMax|Parameters|roughness_inv"][-1] == "1" texture_properties = textures_id[linked["3dsMax|Parameters|roughness_map"]] filename = texture_properties["reference"].replace("\\","/").split("/")[-1] roughness_reference = tools.roughness_convert(filename, inverted_roughness) uoff, voff = texture_properties["Translation"][-3:-1] if "Translation" in texture_properties else ["0", "0"] uscaling, vscaling = texture_properties["Scaling"] [-3:-1] if "Scaling" in texture_properties else ["1", "1"] # Create a new texture curr_roughness = tools.new_obj("texture", "bitmap") curr_roughness.set("name", "alpha") # Avoid cluttering for the final file, removing 0 offsets and 1 scaling if uoff != "0" : tools.set_value(curr_roughness, "float", "uoffset", uoff) if voff != "0" : tools.set_value(curr_roughness, "float", "voffset", voff) if uscaling != "1" : tools.set_value(curr_roughness, "float", "uscale", uscaling) if vscaling !="-1" : tools.set_value(curr_roughness, "float", "vscale", str(-float(vscaling))) tools.set_value(curr_roughness, "string", "filename", roughness_reference) else : # Use a value roughness = .5*float(properties["3dsMax|Parameters|roughness"][-1]) if roughness == 0 : curr_roughness = None else : curr_roughness = etree.Element("float") curr_roughness.set("name", "alpha") curr_roughness.set("value", str(roughness)) else : curr_roughness = None # Based on this blog post : https://simonstechblog.blogspot.com/2011/12/microfacet-brdf.html # √(2/(α+2)) # But divided by 2, because of mitsuba's roughness scale roughness = .5 * (2./(float(shininess)+2.)) ** (.5) curr_roughness = etree.Element("float") curr_roughness.set("name", "alpha") curr_roughness.set("value", str(roughness)) # Specular transmittance if "3dsMax|Parameters|transparency" in properties and float(properties["3dsMax|Parameters|transparency"][-1]) > 0 : transp = 1 - float(properties["3dsMax|Parameters|transparency"][-1]) transp_color = " ".join(properties["3dsMax|Parameters|trans_color"][-4:-1]) curr_material.set("type", "blendbsdf") tools.set_value(curr_material, "float", "weight", str(transp)) # Transparent material for the blend curr_transp = tools.create_obj(curr_material, "bsdf", "roughdielectric") tools.set_value(curr_transp, "string", "distribution", config.distrib) tools.set_value(curr_transp, "spectrum", "specularTransmittance", transp_color) tools.set_value(curr_transp, "float", "alpha", properties["3dsMax|Parameters|trans_roughness"][-1]) if "3dsMax|Parameters|trans_ior" in properties : tools.set_value(curr_transp, "float", "intIOR", properties["3dsMax|Parameters|trans_ior"][-1]) # Non-transparent part of the material curr_material = etree.SubElement(curr_material, "bsdf") # Metalness metalness = float(properties["3dsMax|Parameters|metalness"][-1]) if "3dsMax|Parameters|metalness" in properties else 0 if metalness > 0 : if metalness < 1 : curr_material.set("type", "blendbsdf") tools.set_value(curr_material, "float", "weight", str(1 - float(properties["3dsMax|Parameters|metalness"][-1])))# 1 - Metalness curr_metal = tools.create_obj(curr_material, "bsdf", "roughconductor" if curr_roughness != None else "conductor") else : curr_material.set("type", "roughconductor" if curr_roughness != None else "conductor") curr_metal = curr_material # Metal part of the material tools.set_value(curr_metal, "string" , "distribution", config.distrib) if curr_roughness != None : curr_metal.append(curr_roughness) tools.set_value(curr_metal, "string" , "material" , "none") # Perfect mirror, the color is set via specularReflectance. # Copy-paste from diffuse, this is what 3dsmax uses if "DiffuseColor" in linked :# Use a texture tools.set_value(curr_metal, "ref", "specularReflectance", linked["DiffuseColor"]) elif "3dsMax|Parameters|base_color_map" in linked : tools.set_value(curr_metal, "ref", "specularReflectance", linked["3dsMax|Parameters|base_color_map"]) elif diffuse_color != "1 1 1": tools.set_value(curr_metal, "spectrum", "specularReflectance", diffuse_color) # else it's the default of 1 1 1 # Non-metal part of the material if metalness < 1 : curr_material = etree.SubElement(curr_material, "bsdf") # Non transmitting and non-metal part of the material. Only if metalness < 1 if metalness < 1 : curr_material.set("type", "phong" if config.closest else ("roughplastic" if curr_roughness != None else "plastic")) tools.set_value(curr_material, "string", "distribution", config.distrib) # keep the original diffuse independantly of ior. if config.realist : tools.set_value(curr_material, "boolean", "nonlinear", "true") if "DiffuseColor" in linked :# Use a texture tools.set_value(curr_material, "ref", "diffuseReflectance", linked["DiffuseColor"]) elif "3dsMax|Parameters|base_color_map" in linked : tools.set_value(curr_material, "ref", "diffuseReflectance", linked["3dsMax|Parameters|base_color_map"]) else : tools.set_value(curr_material, "spectrum", "diffuseReflectance", diffuse_color) if specular_color != "1 1 1" : # Set up only if there is such a color curr_spec_color = etree.SubElement(curr_material, "spectrum") curr_spec_color.set("name", "specularReflectance") curr_spec_color.set("value", specular_color) if "3dsMax|Parameters|coat_ior" in properties : ior = properties["3dsMax|Parameters|coat_ior"][-1] tools.set_value(curr_material, "float", "intIOR", ior) if config.closest and shininess != "" : tools.set_value(curr_material, "float", "exponent", shininess) elif curr_roughness != None : curr_material.append(curr_roughness) return material_ids