2
$\begingroup$

does somebody know the algorithm or has an idea for this problem: I want to draw line (2d or 3d space) into the viewport and want to project points onto a mesh that are snapped around the covered object (line and edge loop) - here an image for better understanding:

enter image description here

The contours addon does this, but I cant really extract the algorithm from the code.

Thx!

EDIT: With the operator from "X Y" we get this result when objects get more complex, but ok, looks like the points are all on the object's surface. enter image description here

When I just drae the points it looks alright, but the distance of the points depends on the resolution of the mesh - it should be a constant distance:

enter image description here

I guess simple solution is to just add the point if the distance to a previous point is greater than a certain value. Algorithm could get quite slow for really high poly meshes :-)

$\endgroup$
7
  • $\begingroup$ object.ray_cast() can do that. Here is the api link: docs.blender.org/api/current/… $\endgroup$
    – X Y
    Commented Mar 13, 2022 at 18:07
  • $\begingroup$ Yes, but I was asking about an algorith to wrap points around a mesh. Problems that are to solve here: How to find the next points on the mesh, the direction, the distance and so on. $\endgroup$
    – Jayanam
    Commented Mar 14, 2022 at 10:22
  • $\begingroup$ The display line through the mesh because the order of the faces in the mesh is not stable. There has a easy way to fix it: make the batch draw separated, every batch draw one line only. $\endgroup$
    – X Y
    Commented Mar 16, 2022 at 11:13
  • $\begingroup$ If you want to draw a curve with the gpu module in viewport. It will become more hard to do. You need find the correct orders or the points and may be do a Bézier curve algorithm to show in the viewport. $\endgroup$
    – X Y
    Commented Mar 16, 2022 at 11:23
  • $\begingroup$ My idea is to start with the plane (X1, X2, Viewport), then raycast from X1 onto the mesh, and then again from the hit point in the same direction. Then find the center of the two points and regard this as center. Then from the center raycast in a circle (n times) and collect the hit points. Voila. $\endgroup$
    – Jayanam
    Commented Mar 17, 2022 at 4:44

1 Answer 1

1
$\begingroup$

To make sure the operator is work, after run the script, you need select your object in object mode. Make sure [Apply Object Transform]. Then press [Ctrl F10] on your keyboard to call the operator.

Suppose you want to get the intersection points after a cut.
Here is the result after finish the operator in the script.

enter image description here

#blender ver: 2.93.3

import bpy, bgl, blf, gpu, bmesh
import numpy as np
from mathutils import Vector
from gpu_extras.batch import batch_for_shader
from bpy_extras.view3d_utils import region_2d_to_location_3d as vec_2d_to_3d

def view3d_find( return_area = False ):
    # returns first 3d view, normally we get from context
    for area in bpy.context.window.screen.areas:
        if area.type == 'VIEW_3D':
            v3d = area.spaces[0]
            rv3d = v3d.region_3d
            for region in area.regions:
                if region.type == 'WINDOW':
                    if return_area: return region, rv3d, v3d, area
                    return region, rv3d, v3d
    return None, None

def draw_callback_px():
    font_id = 0  # XXX, need to find out how best to get this.

    # draw some text
    blf.position(font_id, 15, 30, 0)
    blf.size(font_id, 20, 72)
    blf.color(font_id, 1,1,1,1)
    blf.draw(font_id, CUT_OP.blf_info)

coords = []
shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')

def draw_line():
    shader.bind()
    shader.uniform_float("color", (1, 1, 0, 1))
    batch = batch_for_shader(shader, 'LINES', {"pos": coords})
    batch.draw(shader)


class CUT_OP(bpy.types.Operator):
    bl_idname = "view3d.test_operator"
    bl_label = "Simple Modal View3D Operator"

    draw_handler = None
    draw_handler_3d = None
    blf_info = ""

    def reg_handler(self):
        CUT_OP.draw_handler = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px, (), 'WINDOW', 'POST_PIXEL')
        CUT_OP.draw_handler_3d = bpy.types.SpaceView3D.draw_handler_add(draw_line, (), 'WINDOW', 'POST_VIEW')
    def kill_handler(self):
        bpy.types.SpaceView3D.draw_handler_remove(CUT_OP.draw_handler, 'WINDOW')
        bpy.types.SpaceView3D.draw_handler_remove(CUT_OP.draw_handler_3d, 'WINDOW')
        CUT_OP.draw_handler = None
        CUT_OP.draw_handler_3D = None

    def modal(self, context, event):
        context.area.tag_redraw()

        if event.type in {'ESC', 'RIGHTMOUSE'}: return {'CANCELLED'}
        if event.type == 'SPACE' and event.value == 'PRESS':
            if CUT_OP.draw_handler is None:     self.reg_handler()
            else:                               self.kill_handler()

        if self.def_modal(event):   return {'CANCELLED'}

        return {'RUNNING_MODAL'}

    def invoke(self, context, event):
        if not context.object:
            self.report({'WARNING'}, "context object not found, select a object first.")
            return {'CANCELLED'}
        if context.area.type == 'VIEW_3D':
            # Add the region OpenGL drawing callback
            # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
            if CUT_OP.draw_handler is None:     self.reg_handler()

            CUT_OP.blf_info = "Left Click to get first point, SPACE to remove/add draw_handler."
            coords.clear()
            self.def_modal = self.modal_get_first_click

            r3d, rv3d, v3d = view3d_find()
            self.rv3d = rv3d

            context.window_manager.modal_handler_add(self)
            context.area.tag_redraw()
            return {'RUNNING_MODAL'}
        else:
            self.report({'WARNING'}, "View3D not found, cannot run operator")
            return {'CANCELLED'}

    def upd_coord(self, event):
        xy = (event.mouse_region_x, event.mouse_region_y)
        p = vec_2d_to_3d(bpy.context.region, self.rv3d, xy, (0, 0, 0))
        if len(coords) == 2:   del coords[-1]
        coords.append(p)

    def modal_get_first_click(self, event):
        if event.type == 'LEFTMOUSE' and event.value == 'PRESS':
            xy = (event.mouse_region_x, event.mouse_region_y)
            print("xy: ", xy)
            p = vec_2d_to_3d(bpy.context.region, self.rv3d, xy, (0, 0, 0))
            print("p: ",p)
            coords.append(p)
            CUT_OP.blf_info = "Left Click to get second point"
            self.def_modal = self.modal_get_second_click
    def modal_get_second_click(self, event):
        if event.type == 'MOUSEMOVE':   self.upd_coord(event)
        elif event.type == 'LEFTMOUSE' and event.value == 'PRESS':
            self.upd_coord(event)
            calc_intersection_point(self.rv3d)
            CUT_OP.blf_info = "call the operatorion again and press Space to remove the text and line."
            return True

def calc_intersection_point(rv3d):
    '''
    The idea is:
    First, get the projection plane from view_matrix and coords,
    and then for every faces in the object, find the interection point from all line,
    it will find 0 or 2 point(s) for every faces in most case, sometime it find 3 or 4 if on the corner.
    '''
    print("-- calc part --")
    def get_view_matrix(rv3d): #Error if rv3d not find
        rot = rv3d.view_rotation.to_euler()
        view_matrix = rv3d.view_matrix
        x, y, z = view_matrix.to_3x3()
        print("view_matrix 3x3: ", x, y, z)
        return x, y, z

    def is_inside(p, p0, p1): # return True if p is inside the line
        if not (p0[0] <= p[0] <= p1[0] or p1[0] <= p[0] <= p0[0]): return False
        if not (p0[1] <= p[1] <= p1[1] or p1[1] <= p[1] <= p0[1]): return False
        if not (p0[2] <= p[2] <= p1[2] or p1[2] <= p[2] <= p0[2]): return False
        return True

    def isect_line_plane_v3(p0, p1, p_co, p_no, epsilon=1e-6):
        """
        p0, p1: Define the line.
        p_co, p_no: define the plane:
            p_co Is a point on the plane (plane coordinate).
            p_no Is a normal vector defining the plane direction;
                (does not need to be normalized).

        Return a Vector or None (when the intersection can't be found).
        """
        
        def add_v3v3(v0, v1):
            return (
                v0[0] + v1[0],
                v0[1] + v1[1],
                v0[2] + v1[2],
            )
        def sub_v3v3(v0, v1):
            return (
                v0[0] - v1[0],
                v0[1] - v1[1],
                v0[2] - v1[2],
        )
        def dot_v3v3(v0, v1):
            return (
                (v0[0] * v1[0]) +
                (v0[1] * v1[1]) +
                (v0[2] * v1[2])
        )
        def len_squared_v3(v0):
            return dot_v3v3(v0, v0)

        def mul_v3_fl(v0, f):
            return (
                v0[0] * f,
                v0[1] * f,
                v0[2] * f,
        )

        u = sub_v3v3(p1, p0)
        dot = dot_v3v3(p_no, u)

        if abs(dot) > epsilon:
            # The factor of the point between p0 -> p1 (0 - 1)
            # if 'fac' is between (0 - 1) the point intersects with the segment.
            # Otherwise:
            #  < 0.0: behind p0.
            #  > 1.0: infront of p1.
            w = sub_v3v3(p0, p_co)
            fac = -dot_v3v3(p_no, w) / dot
            u = mul_v3_fl(u, fac)
            return add_v3v3(p0, u)

        # The segment is parallel to plane.
        return None

    vx, vy, vz = get_view_matrix(rv3d)

    #get normal of projection plane from 2 direction
    co0, co1 = coords[0], coords[1]
    v1, v2 = co1 - co0, vz
    print("v1, v2: ", v1, v2)
    nor = np.cross(v1, v2) #cross product of v1, v2
    print("plane normal: ", nor)

    #get context object bmesh
    bpy.ops.object.mode_set(mode='EDIT')
    bm = bmesh.from_edit_mesh(bpy.context.object.data)
    points = []
    coords.clear()

    def fn_append2_draw_coords(p):
        coords.append(p)
        coords.append(p)
    def fn_append_draw_coords(p):
        nonlocal sw_fn_append_draw_coords
        coords.append(p)
        sw_fn_append_draw_coords = fn_append2_draw_coords

    sw_fn_append_draw_coords = fn_append_draw_coords #change the fn when used
    for face in bm.faces:
        for edge in face.edges:
            # find interection point from edge and projection plane
            p1, p2 = edge.verts[0].co, edge.verts[1].co
            p = isect_line_plane_v3(p1, p2, co1, nor)
            if p is None:   continue # no intersection
            if is_inside(p, p1, p2): # if p on the line
                points.append(p)
                sw_fn_append_draw_coords(p)

    print("The result: ", points)
    bpy.ops.object.mode_set(mode='OBJECT')


addon_keymaps = []
def register():
    bpy.utils.register_class(CUT_OP)

    # Add the hotkey
    wm = bpy.context.window_manager
    kc = wm.keyconfigs.addon
    if kc:
        km = wm.keyconfigs.addon.keymaps.new(name='3D View', space_type='VIEW_3D')
        kmi = km.keymap_items.new("view3d.test_operator", type='F10', value='PRESS', ctrl=True)
        addon_keymaps.append((km, kmi))


def unregister():
    bpy.utils.unregister_class(CUT_OP)

    # Remove the hotkey
    for km, kmi in addon_keymaps:
        km.keymap_items.remove(kmi)
    addon_keymaps.clear()


if __name__ == "__main__":
    register()
$\endgroup$
2
  • $\begingroup$ Thx m8, kind of works, but please see my EDIT (picture) when objects get more complex. But I guess the points are all on the surface, it is just a matter peraps of ordering to use them to create a curve or circle? $\endgroup$
    – Jayanam
    Commented Mar 16, 2022 at 5:10
  • $\begingroup$ Another thing is that the density of the mesh seems to define the density of the points projectes onto it, but it shouldnt. The distance of the oints on the mesh should be a constant value. I want to use the points after the projection to create a curve. $\endgroup$
    – Jayanam
    Commented Mar 16, 2022 at 5:22

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .