Skip to content
Snippets Groups Projects
object_mesh_topology.py 77.9 KiB
Newer Older
# SPDX-License-Identifier: GPL-2.0-or-later
Maurice Raybaud's avatar
Maurice Raybaud committed

# <pep8 compliant>

"""Translate to POV the control point compounded geometries like polygon

meshes or curve based shapes."""

# --------
# -- Faster mesh export ...one day
# import numpy as np
# --------
Maurice Raybaud's avatar
Maurice Raybaud committed
import bpy
from . import texturing  # for how textures influence shaders
from .scenography import export_smoke

def matrix_as_pov_string(matrix):
    """Translate some transform matrix from Blender UI
Maurice Raybaud's avatar
Maurice Raybaud committed
    to POV syntax and return that string """
    return "matrix <" \
           "%.6f, %.6f, %.6f, " \
           "%.6f, %.6f, %.6f, " \
           "%.6f, %.6f, %.6f, " \
           "%.6f, %.6f, %.6f" \
           ">\n" % (
               matrix[0][0],
               matrix[1][0],
               matrix[2][0],
               matrix[0][1],
               matrix[1][1],
               matrix[2][1],
               matrix[0][2],
               matrix[1][2],
               matrix[2][2],
               matrix[0][3],
               matrix[1][3],
               matrix[2][3],
           )


def write_object_csg_inside_vector(ob, file):
    """Write inside vector for use by pov CSG, only once per object using boolean"""
    has_csg_inside_vector = False
    for modif in ob.modifiers:
        if (
                not has_csg_inside_vector
                and modif.type == 'BOOLEAN'
                and ob.pov.boolean_mod == "POV"
        ):
            file.write(
                "\tinside_vector <%.6g, %.6g, %.6g>\n"
                % (
                    ob.pov.inside_vector[0],
                    ob.pov.inside_vector[1],
                    ob.pov.inside_vector[2],
                )
            )
            has_csg_inside_vector = True
Maurice Raybaud's avatar
Maurice Raybaud committed


#    objectNames = {}
DEF_OBJ_NAME = "Default"


def export_meshes(
        preview_dir,
        file,
        scene,
        sel,
        csg,
        string_strip_hyphen,
        safety,
        write_object_modifiers,
        material_names_dictionary,
        write_object_material_interior,
        exported_lights_count,
        unpacked_images,
        image_format,
        img_map,
        img_map_transforms,
        path_image,
        smoke_path,
        global_matrix,
        write_matrix,
        using_uberpov,
        comments,
        linebreaksinlists,
        tab,
        tab_level,
        tab_write,
        info_callback,
Maurice Raybaud's avatar
Maurice Raybaud committed
):
    """write all meshes as POV mesh2{} syntax to exported file """
    # # some numpy functions to speed up mesh export NOT IN USE YET
    # # Current 2.93 beta numpy linking has troubles so definitions commented off for now

    # # TODO: also write a numpy function to read matrices at object level?
    # # feed below with mesh object.data, but only after doing data.calc_loop_triangles()
    # def read_verts_co(self, mesh):
    # #'float64' would be a slower 64-bit floating-point number numpy datatype
    # # using 'float32' vert coordinates for now until any issue is reported
    # mverts_co = np.zeros((len(mesh.vertices) * 3), dtype=np.float32)
    # mesh.vertices.foreach_get("co", mverts_co)
    # return np.reshape(mverts_co, (len(mesh.vertices), 3))

    # def read_verts_idx(self, mesh):
    # mverts_idx = np.zeros((len(mesh.vertices)), dtype=np.int64)
    # mesh.vertices.foreach_get("index", mverts_idx)
    # return np.reshape(mverts_idx, (len(mesh.vertices), 1))

    # def read_verts_norms(self, mesh):
    # #'float64' would be a slower 64-bit floating-point number numpy datatype
    # # using less accurate 'float16' normals for now until any issue is reported
    # mverts_no = np.zeros((len(mesh.vertices) * 3), dtype=np.float16)
    # mesh.vertices.foreach_get("normal", mverts_no)
    # return np.reshape(mverts_no, (len(mesh.vertices), 3))

    # def read_faces_idx(self, mesh):
    # mfaces_idx = np.zeros((len(mesh.loop_triangles)), dtype=np.int64)
    # mesh.loop_triangles.foreach_get("index", mfaces_idx)
    # return np.reshape(mfaces_idx, (len(mesh.loop_triangles), 1))

    # def read_faces_verts_indices(self, mesh):
    # mfaces_verts_idx = np.zeros((len(mesh.loop_triangles) * 3), dtype=np.int64)
    # mesh.loop_triangles.foreach_get("vertices", mfaces_verts_idx)
    # return np.reshape(mfaces_verts_idx, (len(mesh.loop_triangles), 3))

    # # Why is below different from vertex indices?
    # def read_faces_verts_loops(self, mesh):
    # mfaces_verts_loops = np.zeros((len(mesh.loop_triangles) * 3), dtype=np.int64)
    # mesh.loop_triangles.foreach_get("loops", mfaces_verts_loops)
    # return np.reshape(mfaces_verts_loops, (len(mesh.loop_triangles), 3))

    # def read_faces_norms(self, mesh):
    # #'float64' would be a slower 64-bit floating-point number numpy datatype
    # # using less accurate 'float16' normals for now until any issue is reported
    # mfaces_no = np.zeros((len(mesh.loop_triangles) * 3), dtype=np.float16)
    # mesh.loop_triangles.foreach_get("normal", mfaces_no)
    # return np.reshape(mfaces_no, (len(mesh.loop_triangles), 3))

    # def read_faces_smooth(self, mesh):
    # mfaces_smth = np.zeros((len(mesh.loop_triangles) * 1), dtype=np.bool)
    # mesh.loop_triangles.foreach_get("use_smooth", mfaces_smth)
    # return np.reshape(mfaces_smth, (len(mesh.loop_triangles), 1))

    # def read_faces_material_indices(self, mesh):
    # mfaces_mats_idx = np.zeros((len(mesh.loop_triangles)), dtype=np.int16)
    # mesh.loop_triangles.foreach_get("material_index", mfaces_mats_idx)
    # return np.reshape(mfaces_mats_idx, (len(mesh.loop_triangles), 1))
Maurice Raybaud's avatar
Maurice Raybaud committed

    #        obmatslist = []
    #        def hasUniqueMaterial():
    #            # Grab materials attached to object instances ...
    #            if hasattr(obj, 'material_slots'):
    #                for ms in obj.material_slots:
Maurice Raybaud's avatar
Maurice Raybaud committed
    #                    if ms.material is not None and ms.link == 'OBJECT':
    #                        if ms.material in obmatslist:
    #                            return False
    #                        else:
    #                            obmatslist.append(ms.material)
    #                            return True
Maurice Raybaud's avatar
Maurice Raybaud committed
    #            # Grab materials attached to object instances ...
    #            if hasattr(obj, 'material_slots'):
    #                for ms in obj.material_slots:
Maurice Raybaud's avatar
Maurice Raybaud committed
    #                    if ms.material is not None and ms.link == 'OBJECT':
    #                        # If there is at least one material slot linked to the object
    #                        # and not the data (mesh), always create a new, "private" data instance.
    #                        return True
    #            return False
    # For objects using local material(s) only!
    # This is a mapping between a tuple (dataname, material_names_dictionary, ...),
    # and the POV dataname.
Maurice Raybaud's avatar
Maurice Raybaud committed
    # As only objects using:
    #     * The same data.
    #     * EXACTLY the same materials, in EXACTLY the same sockets.
    # ... can share a same instance in POV export.
    obmats2data = {}

    def check_object_materials(obj, obj_name, dataname):
Maurice Raybaud's avatar
Maurice Raybaud committed
        """Compare other objects exported material slots to avoid rewriting duplicates"""
Maurice Raybaud's avatar
Maurice Raybaud committed
            has_local_mats = False
            key = [dataname]
Maurice Raybaud's avatar
Maurice Raybaud committed
                if ms.material is not None:
                    key.append(ms.material.name)
                    if ms.link == 'OBJECT' and not has_local_mats:
                        has_local_mats = True
                else:
                    # Even if the slot is empty, it is important to grab it...
                    key.append("")
            if has_local_mats:
                # If this object uses local material(s), lets find if another object
                # using the same data and exactly the same list of materials
                # (in the same slots) has already been processed...
                # Note that here also, we use object name as new, unique dataname for Pov.
                key = tuple(key)  # Lists are not hashable...
                if key not in obmats2data:
Maurice Raybaud's avatar
Maurice Raybaud committed
                return obmats2data[key]
        return None

    data_ref = {}

    def store(scene, ob, name, dataname, matrix):
        # The Object needs to be written at least once but if its data is
        # already in data_ref this has already been done.
        # This func returns the "povray" name of the data, or None
        # if no writing is needed.
        if ob.is_modified(scene, 'RENDER'):
            # Data modified.
            # Create unique entry in data_ref by using object name
            # (always unique in Blender) as data name.
            data_ref[name] = [(name, matrix_as_pov_string(matrix))]
            return name
        # Here, we replace dataname by the value returned by check_object_materials, only if
        # it is not evaluated to False (i.e. only if the object uses some local material(s)).
        dataname = check_object_materials(ob, name, dataname) or dataname
        if dataname in data_ref:
            # Data already known, just add the object instance.
            data_ref[dataname].append((name, matrix_as_pov_string(matrix)))
            # No need to write data
            return None
        # Else (no return yet): Data not yet processed, create a new entry in data_ref.
        data_ref[dataname] = [(name, matrix_as_pov_string(matrix))]
        return dataname

    ob_num = 0
    depsgraph = bpy.context.evaluated_depsgraph_get()
Maurice Raybaud's avatar
Maurice Raybaud committed
    for ob in sel:
        # Using depsgraph
        ob = bpy.data.objects[ob.name].evaluated_get(depsgraph)

        # subtract original from the count of their instances as were not counted before 2.8
        if not (ob.is_instancer and ob.original != ob):
            ob_num += 1

            # XXX I moved all those checks here, as there is no need to compute names
            #     for object we won't export here!
            if ob.type in {
                'LIGHT',
                'CAMERA',  # 'EMPTY', #empties can bear dupligroups
Maurice Raybaud's avatar
Maurice Raybaud committed
                'META',
                'ARMATURE',
                'LATTICE',
            }:
                continue
Maurice Raybaud's avatar
Maurice Raybaud committed
            for mod in ob.modifiers:
                if mod and hasattr(mod, 'fluid_type'):
Maurice Raybaud's avatar
Maurice Raybaud committed
                    if mod.fluid_type == 'DOMAIN':
                        if mod.domain_settings.domain_type == 'GAS':
                            export_smoke(
                                file, ob.name, smoke_path, comments, global_matrix, write_matrix
                            )
                        break  # don't render domain mesh, skip to next object.
                    if mod.fluid_type == 'FLOW':  # The domain contains all the smoke. so that's it.
                        if mod.flow_settings.flow_type == 'SMOKE':  # Check how liquids behave
                            break  # don't render smoke flow emitter mesh either, skip to next object.
Maurice Raybaud's avatar
Maurice Raybaud committed
                if hasattr(ob, 'particle_systems'):
                    # Importing function Export Hair
                    # here rather than at the top recommended for addons startup footprint
                    from .object_particles import export_hair
Maurice Raybaud's avatar
Maurice Raybaud committed
                    for p_sys in ob.particle_systems:
Maurice Raybaud's avatar
Maurice Raybaud committed
                            m
                            for m in ob.modifiers
                            if (m is not None) and (m.type == 'PARTICLE_SYSTEM')
                        ]:
                            if (
                                    (p_sys.settings.render_type == 'PATH')
                                    and particle_mod.show_render
                                    and (p_sys.name == particle_mod.particle_system.name)
Maurice Raybaud's avatar
Maurice Raybaud committed
                            ):
                                export_hair(file, ob, particle_mod, p_sys, global_matrix, write_matrix)
                    if not ob.show_instancer_for_render:
                        continue  # don't render emitter mesh, skip to next object.
                # ------------------------------------------------
Maurice Raybaud's avatar
Maurice Raybaud committed
                # Generating a name for object just like materials to be able to use it
                # (baking for now or anything else).
                # XXX I don't understand that if we are here, sel if a non-empty iterable,
                #     so this condition is always True, IMO -- mont29
                # EMPTY type objects treated  a little further below -- MR

                # modified elif to if below as non EMPTY objects can also be instancers
                if ob.is_instancer:
Maurice Raybaud's avatar
Maurice Raybaud committed
                    if ob.instance_type == 'COLLECTION':
                        name_orig = "OB" + ob.name
                        dataname_orig = "DATA" + ob.instance_collection.name
                    else:
                        # hoping only dupligroups have several source datablocks
                        # ob_dupli_list_create(scene) #deprecated in 2.8
                        for eachduplicate in depsgraph.object_instances:
                            # Real dupli instance filtered because
                            # original included in list since 2.8
                            if eachduplicate.is_instance:
                                dataname_orig = "DATA" + eachduplicate.object.name
                        # obj.dupli_list_clear() #just don't store any reference to instance since 2.8
                elif ob.data:  # not an EMPTY type object
                    name_orig = "OB" + ob.name
                    dataname_orig = "DATA" + ob.data.name
Maurice Raybaud's avatar
Maurice Raybaud committed
                elif ob.type == 'EMPTY':
                    name_orig = "OB" + ob.name
                    dataname_orig = "DATA" + ob.name
                else:
                    name_orig = DEF_OBJ_NAME
                    dataname_orig = DEF_OBJ_NAME
                name = string_strip_hyphen(bpy.path.clean_name(name_orig))
                dataname = string_strip_hyphen(bpy.path.clean_name(dataname_orig))
                #                if slot.material is not None and slot.link == 'OBJECT':
                #                    obmaterial = slot.material
                # ------------------------------------------------
Maurice Raybaud's avatar
Maurice Raybaud committed

                if info_callback:
                    info_callback("Object %2.d of %2.d (%s)" % (ob_num, len(sel), ob.name))

                me = ob.data
Maurice Raybaud's avatar
Maurice Raybaud committed

                matrix = global_matrix @ ob.matrix_world
                povdataname = store(scene, ob, name, dataname, matrix)
                if povdataname is None:
                    print("This is an instance of " + name)
                    continue

                print("Writing Down First Occurrence of " + name)

                # ------------ Mesh Primitives ------------ #
Maurice Raybaud's avatar
Maurice Raybaud committed
                # special export_curves() function takes care of writing
                # lathe, sphere_sweep, birail, and loft except with modifiers
                # converted to mesh
                if not ob.is_modified(scene, 'RENDER'):
                    if ob.type == 'CURVE' and (
                            ob.pov.curveshape in {'lathe', 'sphere_sweep', 'loft'}
Maurice Raybaud's avatar
Maurice Raybaud committed
                    ):
                        continue  # Don't render proxy mesh, skip to next object
                # pov_mat_name = "Default_texture" # Not used...remove?
                # Implicit else-if (as not skipped by previous "continue")
                # which itself has no "continue" (to combine custom pov code)?, so Keep this last.
                # For originals, but not their instances, attempt to export mesh:
                if not ob.is_instancer:
                    # except duplis which should be instances groups for now but all duplis later
                    if ob.type == 'EMPTY':
                        # XXX Should we only write this once and instantiate the same for every
                        # empty in the final matrix writing, or even no matrix and just a comment
                        # with empty object transforms ?
                        tab_write("\n//dummy sphere to represent Empty location\n")
Maurice Raybaud's avatar
Maurice Raybaud committed
                        tab_write(
                            "#declare %s =sphere {<0, 0, 0>,0 pigment{rgbt 1} no_image no_reflection no_radiosity photons{pass_through collect off} hollow}\n"
                            % povdataname
Maurice Raybaud's avatar
Maurice Raybaud committed
                        )
                        continue  # Don't render empty object but this is later addition, watch it.
                    ob_eval = ob  # not sure this is needed in case to_mesh_clear could damage obj ?
                    try:
                        me = ob_eval.to_mesh()
                    # Here identify the exception for mesh object with no data: Runtime-Error ?
                    # So we can write something for the dataname or maybe treated "if not me" below
                    except BaseException as e:
                        print(e.__doc__)
                        print('An exception occurred: {}'.format(e))
                        # also happens when curves cant be made into meshes because of no-data
                        continue
                    importance = ob.pov.importance_value
                    if me:
                        me.calc_loop_triangles()
                        me_materials = me.materials
                        me_faces = me.loop_triangles[:]
                        # --- numpytest
                        # me_looptris = me.loops
                        # Below otypes = ['int32'] is a 32-bit signed integer number numpy datatype
                        # get_v_index = np.vectorize(lambda l: l.vertex_index, otypes = ['int32'], cache = True)
                        # faces_verts_idx = get_v_index(me_looptris)
                    # if len(me_faces)==0:
                    # tab_write("\n//dummy sphere to represent empty mesh location\n")
                    # tab_write("#declare %s =sphere {<0, 0, 0>,0 pigment{rgbt 1} no_image no_reflection no_radiosity photons{pass_through collect off} hollow}\n" % povdataname)
                    if not me or not me_faces:
                        tab_write("\n//dummy sphere to represent empty mesh location\n")
                        tab_write(
                            "#declare %s =sphere {<0, 0, 0>,0 pigment{rgbt 1} no_image no_reflection no_radiosity photons{pass_through collect off} hollow}\n"
                            % povdataname
                        )
                        continue
                    uv_layers = me.uv_layers
                    if len(uv_layers) > 0:
                        if me.uv_layers.active and uv_layers.active.data:
                            uv_layer = uv_layers.active.data
                    else:
                        uv_layer = None
                    try:
                        # vcol_layer = me.vertex_colors.active.data
                        vcol_layer = me.vertex_colors.active.data
                    except AttributeError:
                        vcol_layer = None
Maurice Raybaud's avatar
Maurice Raybaud committed

                    faces_verts = [f.vertices[:] for f in me_faces]
                    faces_normals = [f.normal[:] for f in me_faces]
                    verts_normals = [v.normal[:] for v in me.vertices]

                    # Use named declaration to allow reference e.g. for baking. MR
                    file.write("\n")
                    tab_write("#declare %s =\n" % povdataname)
                    tab_write("mesh2 {\n")
                    tab_write("vertex_vectors {\n")
                    tab_write("%d" % len(me.vertices))  # vert count

                    tab_str = tab * tab_level
                    for v in me.vertices:
                        if linebreaksinlists:
                            file.write(",\n")
                            file.write(tab_str + "<%.6f, %.6f, %.6f>" % v.co[:])  # vert count
                        else:
                            file.write(", ")
                            file.write("<%.6f, %.6f, %.6f>" % v.co[:])  # vert count
                        # tab_write("<%.6f, %.6f, %.6f>" % v.co[:])  # vert count
                    file.write("\n")
                    tab_write("}\n")

                    # Build unique Normal list
                    uniqueNormals = {}
                    for fi, f in enumerate(me_faces):
                        fv = faces_verts[fi]
                        # [-1] is a dummy index, use a list so we can modify in place
                        if f.use_smooth:  # Use vertex normals
                            for v in fv:
                                key = verts_normals[v]
                                uniqueNormals[key] = [-1]
                        else:  # Use face normal
                            key = faces_normals[fi]
                            uniqueNormals[key] = [-1]

                    tab_write("normal_vectors {\n")
                    tab_write("%d" % len(uniqueNormals))  # vert count
                    idx = 0
                    tab_str = tab * tab_level
                    for no, index in uniqueNormals.items():
                        if linebreaksinlists:
                            file.write(",\n")
                            file.write(tab_str + "<%.6f, %.6f, %.6f>" % no)  # vert count
                        else:
                            file.write(", ")
                            file.write("<%.6f, %.6f, %.6f>" % no)  # vert count
                        index[0] = idx
                        idx += 1
                    file.write("\n")
                    tab_write("}\n")

                    # Vertex colors
Loading
Loading full blame...