Skip to content
Snippets Groups Projects
import_x3d.py 86.8 KiB
Newer Older
  • Learn to ignore specific revisions
  •     # bpymesh = Mesh.Primitives.Cone(GLOBALS['CIRCLE_DETAIL'], diameter, height)
    
        bpy.ops.mesh.primitive_cone_add(vertices=GLOBALS['CIRCLE_DETAIL'],
                                        radius=diameter,
                                        depth=height,
                                        cap_end=True,
                                        view_align=False,
                                        enter_editmode=False,
                                        )
    
        bpymesh = bpy_ops_add_object_hack()
    
        bpymesh.transform(MATRIX_Z_TO_Y)
    
        # Warning - Rely in the order Blender adds verts
        # not nice design but wont change soon.
    
        bottom = geom.getFieldAsBool('bottom', True, ancestry)
        side = geom.getFieldAsBool('side', True, ancestry)
    
        if not bottom:  # last vert is on the bottom
            # bpymesh.vertices.delete([GLOBALS['CIRCLE_DETAIL'] + 1]) # XXX25
            pass
        if not side:  # second last vert is on the pointy bit of the cone
            # bpymesh.vertices.delete([GLOBALS['CIRCLE_DETAIL']]) # XXX25
            pass
    
        return bpymesh
    
    
    def importMesh_Box(geom, ancestry):
        # bpymesh = bpy.data.meshes.new()
    
        size = geom.getFieldAsFloatTuple('size', (2.0, 2.0, 2.0), ancestry)
    
        # bpymesh = Mesh.Primitives.Cube(1.0)
        bpy.ops.mesh.primitive_cube_add(view_align=False,
                                        enter_editmode=False,
                                        )
    
        bpymesh = bpy_ops_add_object_hack()
    
        # Scale the box to the size set
        scale_mat = Matrix(((size[0], 0, 0), (0, size[1], 0), (0, 0, size[2]))) * 0.5
    
        bpymesh.transform(scale_mat.to_4x4())
    
    
        return bpymesh
    
    
    def importShape(node, ancestry):
        vrmlname = node.getDefName()
        if not vrmlname:
            vrmlname = 'Shape'
    
        # works 100% in vrml, but not x3d
        #appr = node.getChildByName('appearance') # , 'Appearance'
        #geom = node.getChildByName('geometry') # , 'IndexedFaceSet'
    
        # Works in vrml and x3d
        appr = node.getChildBySpec('Appearance')
        geom = node.getChildBySpec(['IndexedFaceSet', 'IndexedLineSet', 'PointSet', 'Sphere', 'Box', 'Cylinder', 'Cone'])
    
        # For now only import IndexedFaceSet's
        if geom:
            bpymat = None
            bpyima = None
            texmtx = None
    
            depth = 0  # so we can set alpha face flag later
    
            if appr:
    
                #mat = appr.getChildByName('material') # 'Material'
                #ima = appr.getChildByName('texture') # , 'ImageTexture'
                #if ima and ima.getSpec() != 'ImageTexture':
                #   print('\tWarning: texture type "%s" is not supported' % ima.getSpec())
                #   ima = None
                # textx = appr.getChildByName('textureTransform')
    
                mat = appr.getChildBySpec('Material')
                ima = appr.getChildBySpec('ImageTexture')
    
                textx = appr.getChildBySpec('TextureTransform')
    
                if textx:
                    texmtx = translateTexTransform(textx, ancestry)
    
                # print(mat, ima)
                if mat or ima:
    
                    if not mat:
                        mat = ima  # This is a bit dumb, but just means we use default values for all
    
                    # all values between 0.0 and 1.0, defaults from VRML docs
                    bpymat = bpy.data.materials.new("XXX")
                    bpymat.ambient = mat.getFieldAsFloat('ambientIntensity', 0.2, ancestry)
                    bpymat.diffuse_color = mat.getFieldAsFloatTuple('diffuseColor', [0.8, 0.8, 0.8], ancestry)
    
                    # NOTE - blender dosnt support emmisive color
                    # Store in mirror color and approximate with emit.
                    emit = mat.getFieldAsFloatTuple('emissiveColor', [0.0, 0.0, 0.0], ancestry)
                    bpymat.mirror_color = emit
                    bpymat.emit = (emit[0] + emit[1] + emit[2]) / 3.0
    
                    bpymat.specular_hardness = int(1 + (510 * mat.getFieldAsFloat('shininess', 0.2, ancestry)))  # 0-1 -> 1-511
                    bpymat.specular_color = mat.getFieldAsFloatTuple('specularColor', [0.0, 0.0, 0.0], ancestry)
                    bpymat.alpha = 1.0 - mat.getFieldAsFloat('transparency', 0.0, ancestry)
                    if bpymat.alpha < 0.999:
                        bpymat.use_transparency = True
    
                if ima:
                    ima_url = ima.getFieldAsString('url', None, ancestry)
    
                    if ima_url == None:
                        try:
                            ima_url = ima.getFieldAsStringArray('url', ancestry)[0]  # in some cases we get a list of images.
                        except:
                            ima_url = None
    
                    if ima_url == None:
                        print("\twarning, image with no URL, this is odd")
                    else:
                        bpyima = image_utils.image_load(ima_url, dirName(node.getFilename()), place_holder=False, recursive=False, convert_callback=imageConvertCompat)
                        if bpyima:
                            texture = bpy.data.textures.new("XXX", 'IMAGE')
                            texture.image = bpyima
    
                            # Adds textures for materials (rendering)
                            try:
                                depth = bpyima.depth
                            except:
                                depth = -1
    
                            if depth == 32:
                                # Image has alpha
                                bpymat.setTexture(0, texture, Texture.TexCo.UV, Texture.MapTo.COL | Texture.MapTo.ALPHA)
                                texture.setImageFlags('MipMap', 'InterPol', 'UseAlpha')
                                bpymat.mode |= Material.Modes.ZTRANSP
                                bpymat.alpha = 0.0
                            else:
                                mtex = bpymat.texture_slots.add()
                                mtex.texture = texture
                                mtex.texture_coords = 'UV'
                                mtex.use_map_diffuse = True
    
                            ima_repS = ima.getFieldAsBool('repeatS', True, ancestry)
                            ima_repT = ima.getFieldAsBool('repeatT', True, ancestry)
    
                            # To make this work properly we'd need to scale the UV's too, better to ignore th
                            # texture.repeat =  max(1, ima_repS * 512), max(1, ima_repT * 512)
    
                            if not ima_repS:
                                bpyima.use_clamp_x = True
                            if not ima_repT:
                                bpyima.use_clamp_y = True
    
            bpydata = None
            geom_spec = geom.getSpec()
            ccw = True
            if geom_spec == 'IndexedFaceSet':
                bpydata, ccw = importMesh_IndexedFaceSet(geom, bpyima, ancestry)
            elif geom_spec == 'IndexedLineSet':
                bpydata = importMesh_IndexedLineSet(geom, ancestry)
            elif geom_spec == 'PointSet':
                bpydata = importMesh_PointSet(geom, ancestry)
            elif geom_spec == 'Sphere':
                bpydata = importMesh_Sphere(geom, ancestry)
            elif geom_spec == 'Box':
                bpydata = importMesh_Box(geom, ancestry)
            elif geom_spec == 'Cylinder':
                bpydata = importMesh_Cylinder(geom, ancestry)
            elif geom_spec == 'Cone':
                bpydata = importMesh_Cone(geom, ancestry)
            else:
                print('\tWarning: unsupported type "%s"' % geom_spec)
                return
    
            if bpydata:
                vrmlname = vrmlname + geom_spec
    
                bpydata.name = vrmlname
    
                bpyob = node.blendObject = bpy.data.objects.new(vrmlname, bpydata)
                bpy.context.scene.objects.link(bpyob)
    
                if type(bpydata) == bpy.types.Mesh:
                    is_solid = geom.getFieldAsBool('solid', True, ancestry)
                    creaseAngle = geom.getFieldAsFloat('creaseAngle', None, ancestry)
    
                    if creaseAngle != None:
                        bpydata.auto_smooth_angle = 1 + int(min(79, creaseAngle * RAD_TO_DEG))
                        bpydata.use_auto_smooth = True
    
                    # Only ever 1 material per shape
                    if bpymat:
                        bpydata.materials.append(bpymat)
    
                    if bpydata.uv_textures:
    
                        if depth == 32:  # set the faces alpha flag?
                            transp = Mesh.FaceTranspModes.ALPHA
                            for f in bpydata.uv_textures.active.data:
                                f.blend_type = 'ALPHA'
    
                        if texmtx:
                            # Apply texture transform?
                            uv_copy = Vector()
                            for f in bpydata.uv_textures.active.data:
                                fuv = f.uv
                                for i, uv in enumerate(fuv):
                                    uv_copy.x = uv[0]
                                    uv_copy.y = uv[1]
    
                                    fuv[i] = (uv_copy * texmtx)[0:2]
                    # Done transforming the texture
    
                    # Must be here and not in IndexedFaceSet because it needs an object for the flip func. Messy :/
                    if not ccw:
                        # bpydata.flipNormals()
                        # XXX25
                        pass
    
                # else could be a curve for example
    
                # Can transform data or object, better the object so we can instance the data
                #bpymesh.transform(getFinalMatrix(node))
                bpyob.matrix_world = getFinalMatrix(node, None, ancestry)
    
    
    def importLamp_PointLight(node, ancestry):
        vrmlname = node.getDefName()
        if not vrmlname:
            vrmlname = 'PointLight'
    
        # ambientIntensity = node.getFieldAsFloat('ambientIntensity', 0.0, ancestry) # TODO
        # attenuation = node.getFieldAsFloatTuple('attenuation', (1.0, 0.0, 0.0), ancestry) # TODO
        color = node.getFieldAsFloatTuple('color', (1.0, 1.0, 1.0), ancestry)
        intensity = node.getFieldAsFloat('intensity', 1.0, ancestry)  # max is documented to be 1.0 but some files have higher.
        location = node.getFieldAsFloatTuple('location', (0.0, 0.0, 0.0), ancestry)
        # is_on = node.getFieldAsBool('on', True, ancestry) # TODO
        radius = node.getFieldAsFloat('radius', 100.0, ancestry)
    
        bpylamp = bpy.data.lamps.new("ToDo", 'POINT')
        bpylamp.energy = intensity
        bpylamp.distance = radius
        bpylamp.color = color
    
        mtx = Matrix.Translation(Vector(location))
    
        return bpylamp, mtx
    
    
    def importLamp_DirectionalLight(node, ancestry):
        vrmlname = node.getDefName()
        if not vrmlname:
            vrmlname = 'DirectLight'
    
        # ambientIntensity = node.getFieldAsFloat('ambientIntensity', 0.0) # TODO
        color = node.getFieldAsFloatTuple('color', (1.0, 1.0, 1.0), ancestry)
        direction = node.getFieldAsFloatTuple('direction', (0.0, 0.0, -1.0), ancestry)
        intensity = node.getFieldAsFloat('intensity', 1.0, ancestry)  # max is documented to be 1.0 but some files have higher.
        # is_on = node.getFieldAsBool('on', True, ancestry) # TODO
    
        bpylamp = bpy.data.lamps.new(vrmlname, 'SUN')
        bpylamp.energy = intensity
        bpylamp.color = color
    
        # lamps have their direction as -z, yup
    
        mtx = Vector(direction).to_track_quat('-Z', 'Y').to_matrix().to_4x4()
    
    
        return bpylamp, mtx
    
    # looks like default values for beamWidth and cutOffAngle were swapped in VRML docs.
    
    
    def importLamp_SpotLight(node, ancestry):
        vrmlname = node.getDefName()
        if not vrmlname:
            vrmlname = 'SpotLight'
    
        # ambientIntensity = geom.getFieldAsFloat('ambientIntensity', 0.0, ancestry) # TODO
        # attenuation = geom.getFieldAsFloatTuple('attenuation', (1.0, 0.0, 0.0), ancestry) # TODO
        beamWidth = node.getFieldAsFloat('beamWidth', 1.570796, ancestry)  # max is documented to be 1.0 but some files have higher.
        color = node.getFieldAsFloatTuple('color', (1.0, 1.0, 1.0), ancestry)
        cutOffAngle = node.getFieldAsFloat('cutOffAngle', 0.785398, ancestry) * 2.0  # max is documented to be 1.0 but some files have higher.
        direction = node.getFieldAsFloatTuple('direction', (0.0, 0.0, -1.0), ancestry)
        intensity = node.getFieldAsFloat('intensity', 1.0, ancestry)  # max is documented to be 1.0 but some files have higher.
        location = node.getFieldAsFloatTuple('location', (0.0, 0.0, 0.0), ancestry)
        # is_on = node.getFieldAsBool('on', True, ancestry) # TODO
        radius = node.getFieldAsFloat('radius', 100.0, ancestry)
    
        bpylamp = bpy.data.lamps.new(vrmlname, 'SPOT')
        bpylamp.energy = intensity
        bpylamp.distance = radius
        bpylamp.color = color
        bpylamp.spot_size = cutOffAngle
        if beamWidth > cutOffAngle:
            bpylamp.spot_blend = 0.0
        else:
            if cutOffAngle == 0.0:  # this should never happen!
                bpylamp.spot_blend = 0.5
            else:
                bpylamp.spot_blend = beamWidth / cutOffAngle
    
        # Convert
    
        # lamps have their direction as -z, y==up
    
        mtx = Matrix.Translation(location) * Vector(direction).to_track_quat('-Z', 'Y').to_matrix().to_4x4()
    
    
        return bpylamp, mtx
    
    
    def importLamp(node, spec, ancestry):
        if spec == 'PointLight':
            bpylamp, mtx = importLamp_PointLight(node, ancestry)
        elif spec == 'DirectionalLight':
            bpylamp, mtx = importLamp_DirectionalLight(node, ancestry)
        elif spec == 'SpotLight':
            bpylamp, mtx = importLamp_SpotLight(node, ancestry)
        else:
            print("Error, not a lamp")
            raise ValueError
    
        bpyob = node.blendObject = bpy.data.objects.new("TODO", bpylamp)
        bpy.context.scene.objects.link(bpyob)
    
        bpyob.matrix_world = getFinalMatrix(node, mtx, ancestry)
    
    
    def importViewpoint(node, ancestry):
        name = node.getDefName()
        if not name:
            name = 'Viewpoint'
    
        fieldOfView = node.getFieldAsFloat('fieldOfView', 0.785398, ancestry)  # max is documented to be 1.0 but some files have higher.
        # jump = node.getFieldAsBool('jump', True, ancestry)
        orientation = node.getFieldAsFloatTuple('orientation', (0.0, 0.0, 1.0, 0.0), ancestry)
        position = node.getFieldAsFloatTuple('position', (0.0, 0.0, 0.0), ancestry)
        description = node.getFieldAsString('description', '', ancestry)
    
        bpycam = bpy.data.cameras.new(name)
    
        bpycam.angle = fieldOfView
    
        mtx = Matrix.Translation(Vector(position)) * translateRotation(orientation)
    
        bpyob = node.blendObject = bpy.data.objects.new("TODO", bpycam)
        bpy.context.scene.objects.link(bpyob)
        bpyob.matrix_world = getFinalMatrix(node, mtx, ancestry)
    
    
    def importTransform(node, ancestry):
        name = node.getDefName()
        if not name:
            name = 'Transform'
    
        bpyob = node.blendObject = bpy.data.objects.new(name, None)
        bpy.context.scene.objects.link(bpyob)
    
        bpyob.matrix_world = getFinalMatrix(node, None, ancestry)
    
        # so they are not too annoying
        bpyob.empty_draw_type = 'PLAIN_AXES'
        bpyob.empty_draw_size = 0.2
    
    
    #def importTimeSensor(node):
    def action_fcurve_ensure(action, data_path, array_index):
        for fcu in action.fcurves:
            if fcu.data_path == data_path and fcu.array_index == array_index:
                return fcu
    
        return action.fcurves.new(data_path=data_path, array_index=array_index)
    
    
    def translatePositionInterpolator(node, action, ancestry):
        key = node.getFieldAsArray('key', 0, ancestry)
        keyValue = node.getFieldAsArray('keyValue', 3, ancestry)
    
        loc_x = action_fcurve_ensure(action, "location", 0)
        loc_y = action_fcurve_ensure(action, "location", 1)
        loc_z = action_fcurve_ensure(action, "location", 2)
    
        for i, time in enumerate(key):
            try:
                x, y, z = keyValue[i]
            except:
                continue
    
    
            loc_x.keyframe_points.insert(time, x)
            loc_y.keyframe_points.insert(time, y)
            loc_z.keyframe_points.insert(time, z)
    
    
        for fcu in (loc_x, loc_y, loc_z):
            for kf in fcu.keyframe_points:
                kf.interpolation = 'LINEAR'
    
    
    def translateOrientationInterpolator(node, action, ancestry):
        key = node.getFieldAsArray('key', 0, ancestry)
        keyValue = node.getFieldAsArray('keyValue', 4, ancestry)
    
        rot_x = action_fcurve_ensure(action, "rotation_euler", 0)
        rot_y = action_fcurve_ensure(action, "rotation_euler", 1)
        rot_z = action_fcurve_ensure(action, "rotation_euler", 2)
    
        for i, time in enumerate(key):
            try:
                x, y, z, w = keyValue[i]
            except:
                continue
    
            mtx = translateRotation((x, y, z, w))
            eul = mtx.to_euler()
    
            rot_x.keyframe_points.insert(time, eul.x)
            rot_y.keyframe_points.insert(time, eul.y)
            rot_z.keyframe_points.insert(time, eul.z)
    
    
        for fcu in (rot_x, rot_y, rot_z):
            for kf in fcu.keyframe_points:
                kf.interpolation = 'LINEAR'
    
    
    # Untested!
    def translateScalarInterpolator(node, action, ancestry):
        key = node.getFieldAsArray('key', 0, ancestry)
        keyValue = node.getFieldAsArray('keyValue', 4, ancestry)
    
        sca_x = action_fcurve_ensure(action, "scale", 0)
        sca_y = action_fcurve_ensure(action, "scale", 1)
        sca_z = action_fcurve_ensure(action, "scale", 2)
    
        for i, time in enumerate(key):
            try:
                x, y, z = keyValue[i]
            except:
                continue
    
            sca_x.keyframe_points.new(time, x)
            sca_y.keyframe_points.new(time, y)
            sca_z.keyframe_points.new(time, z)
    
    
    def translateTimeSensor(node, action, ancestry):
        '''
        Apply a time sensor to an action, VRML has many combinations of loop/start/stop/cycle times
        to give different results, for now just do the basics
        '''
    
        # XXX25 TODO
        if 1:
            return
    
        time_cu = action.addCurve('Time')
        time_cu.interpolation = Blender.IpoCurve.InterpTypes.LINEAR
    
        cycleInterval = node.getFieldAsFloat('cycleInterval', None, ancestry)
    
        startTime = node.getFieldAsFloat('startTime', 0.0, ancestry)
        stopTime = node.getFieldAsFloat('stopTime', 250.0, ancestry)
    
        if cycleInterval != None:
            stopTime = startTime + cycleInterval
    
        loop = node.getFieldAsBool('loop', False, ancestry)
    
        time_cu.append((1 + startTime, 0.0))
        time_cu.append((1 + stopTime, 1.0 / 10.0))  # anoying, the UI uses /10
    
        if loop:
            time_cu.extend = Blender.IpoCurve.ExtendTypes.CYCLIC  # or - EXTRAP, CYCLIC_EXTRAP, CONST,
    
    
    def importRoute(node, ancestry):
        '''
        Animation route only at the moment
        '''
    
        if not hasattr(node, 'fields'):
            return
    
        routeIpoDict = node.getRouteIpoDict()
    
        def getIpo(id):
            try:
                action = routeIpoDict[id]
            except:
                action = routeIpoDict[id] = bpy.data.actions.new('web3d_ipo')
            return action
    
        # for getting definitions
        defDict = node.getDefDict()
        '''
        Handles routing nodes to eachother
    
    ROUTE vpPI.value_changed TO champFly001.set_position
    ROUTE vpOI.value_changed TO champFly001.set_orientation
    ROUTE vpTs.fraction_changed TO vpPI.set_fraction
    ROUTE vpTs.fraction_changed TO vpOI.set_fraction
    ROUTE champFly001.bindTime TO vpTs.set_startTime
        '''
    
        #from_id, from_type = node.id[1].split('.')
        #to_id, to_type = node.id[3].split('.')
    
        #value_changed
        set_position_node = None
        set_orientation_node = None
        time_node = None
    
        for field in node.fields:
            if field and field[0] == 'ROUTE':
                try:
                    from_id, from_type = field[1].split('.')
                    to_id, to_type = field[3].split('.')
                except:
                    print("Warning, invalid ROUTE", field)
                    continue
    
                if from_type == 'value_changed':
                    if to_type == 'set_position':
                        action = getIpo(to_id)
                        set_data_from_node = defDict[from_id]
                        translatePositionInterpolator(set_data_from_node, action, ancestry)
    
                    if to_type in ('set_orientation', 'rotation'):
                        action = getIpo(to_id)
                        set_data_from_node = defDict[from_id]
                        translateOrientationInterpolator(set_data_from_node, action, ancestry)
    
                    if to_type == 'set_scale':
                        action = getIpo(to_id)
                        set_data_from_node = defDict[from_id]
                        translateScalarInterpolator(set_data_from_node, action, ancestry)
    
                elif from_type == 'bindTime':
                    action = getIpo(from_id)
                    time_node = defDict[to_id]
                    translateTimeSensor(time_node, action, ancestry)
    
    
    def load_web3d(path, PREF_FLAT=False, PREF_CIRCLE_DIV=16, HELPER_FUNC=None):
    
        # Used when adding blender primitives
        GLOBALS['CIRCLE_DETAIL'] = PREF_CIRCLE_DIV
    
        #root_node = vrml_parse('/_Cylinder.wrl')
        if path.lower().endswith('.x3d'):
            root_node, msg = x3d_parse(path)
        else:
            root_node, msg = vrml_parse(path)
    
        if not root_node:
            print(msg)
            return
    
        # fill with tuples - (node, [parents-parent, parent])
        all_nodes = root_node.getSerialized([], [])
    
        for node, ancestry in all_nodes:
            #if 'castle.wrl' not in node.getFilename():
            #   continue
    
            spec = node.getSpec()
            '''
            prefix = node.getPrefix()
            if prefix=='PROTO':
                pass
            else
            '''
            if HELPER_FUNC and HELPER_FUNC(node, ancestry):
                # Note, include this function so the VRML/X3D importer can be extended
                # by an external script. - gets first pick
                pass
            if spec == 'Shape':
                importShape(node, ancestry)
            elif spec in ('PointLight', 'DirectionalLight', 'SpotLight'):
                importLamp(node, spec, ancestry)
            elif spec == 'Viewpoint':
                importViewpoint(node, ancestry)
            elif spec == 'Transform':
                # Only use transform nodes when we are not importing a flat object hierarchy
                if PREF_FLAT == False:
                    importTransform(node, ancestry)
                '''
            # These are delt with later within importRoute
            elif spec=='PositionInterpolator':
                action = bpy.data.ipos.new('web3d_ipo', 'Object')
                translatePositionInterpolator(node, action)
                '''
    
        # After we import all nodes, route events - anim paths
        for node, ancestry in all_nodes:
            importRoute(node, ancestry)
    
        for node, ancestry in all_nodes:
            if node.isRoot():
                # we know that all nodes referenced from will be in
                # routeIpoDict so no need to run node.getDefDict() for every node.
                routeIpoDict = node.getRouteIpoDict()
                defDict = node.getDefDict()
    
                for key, action in routeIpoDict.items():
    
                    # Assign anim curves
                    node = defDict[key]
                    if node.blendObject == None:  # Add an object if we need one for animation
                        node.blendObject = bpy.data.objects.new('AnimOb', None)  # , name)
                        bpy.context.scene.objects.link(node.blendObject)
    
                    if node.blendObject.animation_data is None:
                        node.blendObject.animation_data_create()
    
                    node.blendObject.animation_data.action = action
    
        # Add in hierarchy
        if PREF_FLAT == False:
            child_dict = {}
            for node, ancestry in all_nodes:
                if node.blendObject:
                    blendObject = None
    
                    # Get the last parent
                    i = len(ancestry)
                    while i:
                        i -= 1
                        blendObject = ancestry[i].blendObject
                        if blendObject:
                            break
    
                    if blendObject:
                        # Parent Slow, - 1 liner but works
                        # blendObject.makeParent([node.blendObject], 0, 1)
    
                        # Parent FAST
                        try:
                            child_dict[blendObject].append(node.blendObject)
                        except:
                            child_dict[blendObject] = [node.blendObject]
    
            # Parent
            for parent, children in child_dict.items():
                for c in children:
                    c.parent = parent
    
            # update deps
            bpy.context.scene.update()
            del child_dict
    
    
    def load(operator, context, filepath=""):
    
        load_web3d(filepath,
                   PREF_FLAT=True,
                   PREF_CIRCLE_DIV=16,
                   )
    
        return {'FINISHED'}