OverviewFilesCommitsIssuesDocsReleasesPeers

hardops / operators / modals / mesh_fade.py

401 lines · python ref:
import bpy, bmesh, mathutils, math, gpu, time, numpy
from gpu_extras.batch import batch_for_shader
from mathutils import Vector, Matrix
from bpy.props import EnumProperty, StringProperty, BoolProperty
from ... utility import method_handler
from ... utility import addon

##############################################################################
# Draw Data
##############################################################################

HANDLE_3D = None
EXTRA_CHECK = 0

class Data:
    # States
    mesh_modal_running = False
    restart_modal = False
    # Draw Data
    lines = []
    indices = []
    # Shader
    shader = None
    batch = None

    @staticmethod
    def reset_data():
        Data.lines = []
        Data.indices = []
        Data.shader = None
        Data.batch = None

    @staticmethod
    def reset_states():
        Data.mesh_modal_running = False

    @staticmethod
    def set_batch():
        if not len(Data.lines): return
        built_in_shader = 'UNIFORM_COLOR' if bpy.app.version[0] >=4 else '3D_UNIFORM_COLOR'
        Data.shader = gpu.shader.from_builtin(built_in_shader)
        Data.batch = batch_for_shader(Data.shader, 'LINES', {"pos": Data.lines}, indices = Data.indices if len(Data.indices) else None)

##############################################################################
# Launch Operator
##############################################################################

class HOPS_OT_Draw_Wire_Mesh_Launcher(bpy.types.Operator):
    bl_idname = "hops.draw_wire_mesh_launcher"
    bl_label = "UV Draw Launcher"
    bl_options = {"INTERNAL"}

    target: EnumProperty(
        name="Mesh Target",
        description="What meshes to target",
        items=[
            ('ACTIVE'  , "Active"  , "Use the active object"),
            ('SELECTED', "Selected", "Use all the selected objects"),
            ('NONE'    , "None"    , "Void the options")],
        default='NONE')

    object_name: StringProperty(
        name='Object Name',
        description='The object to use as the fade target',
        default="")

    omit_subd: BoolProperty(
        name="Omit Subd Wire",
        default=False)

    def execute(self, context):

        # Extra Check : If draw modal has not saved a time than just reset every thing manual
        if time.time() - EXTRA_CHECK > addon.preference().ui.Hops_extra_draw_time:
            Data.reset_data()
            Data.reset_states()
            Data.restart_modal = False

        if addon.preference().ui.Hops_extra_draw == False:
            return {'FINISHED'}

        if self.target == 'ACTIVE':
            capture_active_mesh_data(context, self.omit_subd)

        elif self.target == 'SELECTED':
            capture_selected_mesh_data(context, self.omit_subd)

        elif self.target == 'NONE':
            capture_obj_mesh_data(context, self.object_name, self.omit_subd)

        # Trigger modal to reset its timers and start fade over again
        if Data.mesh_modal_running == True:
            Data.restart_modal = True
        # Call a fresh operator if none is currently running
        else:
            bpy.ops.hops.draw_wire_mesh('INVOKE_DEFAULT')

        return {'FINISHED'}


def capture_active_mesh_data(context, omit_subd):
    Data.reset_data()

    obj = context.active_object
    
    subd_mod_map = {}
    if omit_subd:
        for mod in obj.modifiers:
            if mod.type == 'SUBSURF':
                subd_mod_map[mod] = mod.show_viewport
                mod.show_viewport = False

    depsgraph = context.evaluated_depsgraph_get()
    if type(obj) == bpy.types.Object:
        if obj.type == 'MESH':
            object_eval = obj.evaluated_get(depsgraph)
            mesh_eval = object_eval.data
            Data.lines, Data.indices = get_draw_data(mesh_eval, obj.matrix_world)

    if omit_subd:
        for mod, show in subd_mod_map.items():
            mod.show_viewport = show

    Data.set_batch()


def capture_selected_mesh_data(context, omit_subd):
    Data.reset_data()

    set_to_edit = False
    if context.mode == 'EDIT_MESH':
        set_to_edit = True
        bpy.ops.object.mode_set(mode='OBJECT')

    objs = [o for o in context.selected_objects if o.type == 'MESH']
    depsgraph = context.evaluated_depsgraph_get()

    if objs:
        coords = []
        index_list = []
        for obj in objs:

            subd_mod_map = {}
            if omit_subd:
                for mod in obj.modifiers:
                    if mod.type == 'SUBSURF':
                        subd_mod_map[mod] = mod.show_viewport
                        mod.show_viewport = False

            object_eval = obj.evaluated_get(depsgraph)
            mesh_eval = object_eval.data
            lines, indices = get_draw_data(mesh_eval, obj.matrix_world)
            coords.append(lines)
            index_list.append(indices)

            if omit_subd:
                for mod, show in subd_mod_map.items():
                    mod.show_viewport = show

        Data.lines = numpy.concatenate(coords, axis=0).tolist()

        #remap indices
        add_index = 0
        for i in range(1, len(index_list)):
            index_array = index_list[i]
            add_index += len(coords[i-1])
            index_array += add_index

        Data.indices = numpy.concatenate(index_list, axis=0).tolist()

    Data.set_batch()

    if set_to_edit:
        bpy.ops.object.mode_set(mode='OBJECT')


def capture_obj_mesh_data(context, obj_name, omit_subd):
    Data.reset_data()

    obj = None
    if obj_name in context.scene.objects.keys():
        temp = context.scene.objects[obj_name]
        if type(temp) == bpy.types.Object:
            if temp.type == 'MESH':
                obj = temp

    if not obj: return

    if context.mode == 'EDIT_MESH':
        obj.update_from_editmode()

    subd_mod_map = {}
    if omit_subd:
        for mod in obj.modifiers:
            if mod.type == 'SUBSURF':
                subd_mod_map[mod] = mod.show_viewport
                mod.show_viewport = False

    depsgraph = context.evaluated_depsgraph_get()
    object_eval = obj.evaluated_get(depsgraph)
    mesh_eval = object_eval.data
    Data.lines, Data.indices = get_draw_data(mesh_eval, obj.matrix_world)

    if omit_subd:
        for mod, show in subd_mod_map.items():
            mod.show_viewport = show

    Data.set_batch()


#numpy is too wordy for copying this all over
def get_draw_data(mesh, matrix):
    #vert coords
    length = len(mesh.vertices)
    coords = numpy.empty([length, 3], dtype = 'f')
    mesh.vertices.foreach_get('co', numpy.reshape(coords, length * 3))

    #add normal offset
    normals = numpy.empty([length, 3], dtype = 'f')
    mesh.vertices.foreach_get('normal', numpy.reshape(normals, length * 3))
    coords += normals * 0.002

    coords = numpy.append(coords, numpy.ones([length, 1], dtype='f'), axis=1)
    coords = coords @ numpy.array(matrix.transposed(), dtype='f')
    coords = numpy.delete(coords, [3], 1)
    coords = numpy.array(coords, dtype ='f', copy=False, order='C') # restore 'C' order so vbuffer is happy

    #indices
    length = len(mesh.edges)
    indices = numpy.empty([len(mesh.edges), 2], dtype='i', order='C' )
    mesh.edges.foreach_get('vertices', numpy.reshape(indices, length * 2))

    return coords, indices

##############################################################################
# Drawing Modal
##############################################################################

class HOPS_OT_Draw_Wire_Mesh(bpy.types.Operator):
    bl_idname = "hops.draw_wire_mesh"
    bl_label = "Drawing for mesh wire fade"
    bl_options = {"INTERNAL"}

    def invoke(self, context, event):
        # Internal check
        self.last_called = time.time()

        # Registers
        self.shader = Shader(context)
        self.timer = context.window_manager.event_timer_add(0.075, window=context.window)
        context.window_manager.modal_handler_add(self)
        return {'RUNNING_MODAL'}


    def modal(self, context, event):

        Data.mesh_modal_running = True
        global EXTRA_CHECK
        EXTRA_CHECK = time.time()

        # Resest every thing if new drawing is initialized
        if Data.restart_modal:
            Data.restart_modal = False
            self.last_called = time.time()
            self.shader.reset(context)

        time_out = False
        if time.time() - self.last_called > addon.preference().ui.Hops_extra_draw_time:
            time_out = True

        # Reset everything
        if self.shader.fade_complete or time_out:
            self.__finished(context)
            return {'FINISHED'}

        # Redraw the viewport
        if context.area != None:
            context.area.tag_redraw()

        return {'PASS_THROUGH'}


    def __finished(self, context):
        '''Remove the timer, shader, and reset Data'''

        # Global
        Data.reset_data()
        Data.reset_states()

        # Unregister
        if self.timer != None:
            context.window_manager.event_timer_remove(self.timer)
        if self.shader != None:
            self.shader.destroy()
        if context.area != None:
            context.area.tag_redraw()


class Shader():

    def __init__(self, context):
        self.duration = addon.preference().ui.Hops_extra_draw_time
        self.reset(context)
        self.__setup_handle(context)


    def reset(self, context):
        '''Restart the shader.'''

        self.fade_complete = False
        self.start_time = time.time()
        color = addon.preference().color.Hops_wire_mesh
        self.alpha = color[3]
        self.og_alpha = color[3]
        self.color = (color[0], color[1], color[2], self.alpha)

        self.__setup_handle(context)


    def destroy(self):
        '''Remove the shader.'''

        global HANDLE_3D
        if HANDLE_3D != None:
            bpy.types.SpaceView3D.draw_handler_remove(HANDLE_3D, "WINDOW")
            HANDLE_3D = None

        Data.reset_data()
        Data.reset_states()


    def __setup_handle(self, context):
        '''Setup the draw handle for the UI'''

        global HANDLE_3D
        if HANDLE_3D == None:
            HANDLE_3D = bpy.types.SpaceView3D.draw_handler_add(self.__safe_draw_3d, (context, ), "WINDOW", "POST_VIEW")


    def __safe_draw_3d(self, context):
        method_handler(self.__draw_3d,
            arguments = (context,),
            identifier = 'Dice Fade Draw Shader 3D',
            exit_method = self.destroy)


    def __draw_3d(self, context):
        '''Draw the UVs.'''

        # Validate
        if not len(Data.lines) or not Data.shader or not Data.batch:
            self.fade_complete = True
            return

        # Lower alpha during fade
        self.__handle_fade()

        #Enable(GL_LINE_SMOOTH)
        gpu.state.blend_set('ALPHA')
        gpu.state.depth_test_set('LESS')
        #glDepthFunc(GL_LESS)
        gpu.state.line_width_set(3)

        # Draw wires
        Data.shader.bind()
        Data.shader.uniform_float("color", (self.color[0], self.color[1], self.color[2], self.alpha))
        Data.batch.draw(Data.shader)

        gpu.state.blend_set('NONE')
        gpu.state.depth_test_set('NONE')
        gpu.state.face_culling_set('NONE')
        gpu.state.depth_test_set('NONE')

        # Trigger exit
        if self.alpha <= 0:
            self.fade_complete = True


    def __handle_fade(self):
        '''Fade the alpha based on current time and Global DURATION variable'''

        diff = time.time() - self.start_time
        ratio = diff / self.duration
        self.alpha = self.og_alpha - (self.og_alpha * ratio)


##############################################################################
# Remove On App Reload
##############################################################################

from bpy.app.handlers import persistent
@persistent
def remove_mesh_draw_shader(dummy):
    global HANDLE_3D
    if HANDLE_3D != None:
        bpy.types.SpaceView3D.draw_handler_remove(HANDLE_3D, "WINDOW")
        HANDLE_3D = None

    Data.reset_data()
    Data.reset_states()