2
$\begingroup$

One thing that I've noticed when drawing quads is that, if a single surface cannot be established using the vertex arrangement (for example, if you add the default plane and raise a single vertex to 1 on the Z-axis), Blender will treat it a quad (subsurf works fine with it and will smooth it out into a hyperbolic surface) but render it as two conjoined tris (prior to the subsurf).

Now, I'm fairly certain that I should avoid this in most cases and would want to fix it if it does occur. In my example above, I could obviously just shift the vertex back to 0 on the Z-axis, because the surface is completely flat. However, if I were to rotate this quad, I would be in a situation where moving the vertex to establish a flat surface would require very precise changes to the X, Y, and Z positions.

I was wondering if there is some feature of Blender or a technique to adjust vertices exactly that the quad is absolutely flat, even if the quad is rotated.

P.S. I apologize for the very technical and possibly pedantic style of writing. Because I want to ensure the question is communicated clearly, I felt that it was necessary to be scrupulous.

$\endgroup$
3
$\begingroup$

Extracted from my answer here, here is another scripted option. In contrary to Blender Dadaist's answer, this method creates a planar quad, by only moving the active vertex. ⇧ ShiftRMB RMB select the three vertices of the quad, which are not supposed to change, then ⇧ ShiftRMB RMB select the vertice of the quad, which you wish to change, marking it as the active vertex.

flatten vertex

The script registers an operator and places it in the menu: Mesh > Vertices > Flatten Vertex with the hotkey ⎈ Ctrl⇧ ShiftM.

bl_info = {
    "name": "Flatten Vertex Onto Face",
    "category": "Object",
}

import bpy
import bmesh


class ObjectFlattenVertex(bpy.types.Operator):
    """Flatten Vertex Onto Face"""
    bl_idname = "edit_mesh.flatten_vertex"
    bl_label = "Flatten Vertex"
    bl_options = {'REGISTER', 'UNDO'}

    influence = bpy.props.FloatProperty(name='Influence', subtype='PERCENTAGE', default=1, min=0, max=1)

    def execute(self, context):
        if (context.object == None):
            self.report({'ERROR_INVALID_CONTEXT'}, "This is only possible with vertices of an object.")
            return {'CANCELLED'}
        me = context.object.data

        if me.is_editmode:
            bm = bmesh.from_edit_mesh(me)
        else:
            bm = bmesh.new()
            bm.from_mesh(me)

        for elem in bm.select_history:
            if (isinstance(elem, bmesh.types.BMVert) == False):
                self.report({'ERROR_INVALID_INPUT'}, "Select vertices only.")
                return {'CANCELLED'}
        if len(bm.select_history) != 4:
            self.report({'ERROR_INVALID_INPUT'}, "Select exactly four vertices.")
            return {'CANCELLED'}
        if (bm.select_history.active == None):
            self.report({'ERROR_INVALID_INPUT'}, "Require an active vertex.")
            return {'CANCELLED'}
        active = bm.select_history.active
        # get only selected vertices
        p1, p2, origin = [v.co for v in bm.select_history if v != active]
        a = active.co - origin
        # normal
        n =  (p1 - origin).cross(p2 - origin)
        # projected
        prj =  n.cross(a.cross(n) / n.length) / n.length
        active.co = self.influence * prj + (1 - self.influence) * a + origin

        if bm.is_wrapped:
            bmesh.update_edit_mesh(me, False, False)
        else:
            bm.to_mesh(me)
            me.update()
        return {'FINISHED'}


def menu_func(self, context):
    self.layout.operator(ObjectFlattenVertex.bl_idname)

addon_keymaps = []

def register():
    bpy.utils.register_class(ObjectFlattenVertex)
    bpy.types.VIEW3D_MT_edit_mesh_vertices.append(menu_func)

    wm = bpy.context.window_manager # keymap
    kc = wm.keyconfigs.addon # background mode check
    if kc:
        km = wm.keyconfigs.addon.keymaps.new(name = "Window",space_type='EMPTY', region_type='WINDOW')
        kmi = km.keymap_items.new(ObjectFlattenVertex.bl_idname, type = "M", shift=True, ctrl=True, value = "PRESS")
        addon_keymaps.append(km)

def unregister():
    for km, kmi in addon_keymaps:
        km.keymap_items.remove(kmi)
    addon_keymaps.clear()

    bpy.utils.unregister_class(ObjectFlattenVertex)
    bpy.types.VIEW3D_MT_edit_mesh_vertices.remove(menu_func)


if __name__ == "__main__":
    register()

Some math

At # get only selected vertices the selected three vertice's coordinates (not the active) are stored in p1, p2 and origin. The active vertex is stored in active.

The two directional vectors of the plane are p1 - origin and p2 - origin. The "base" of the plane is at origin.
Thus, the normal vector of the plane is n = (p1 - origin).cross(p2 - origin) (cross product).

The active vertex's position relativ to the origin is at a = active.co - origin.
Now, a has to be projected onto the plane with the normal vector n. This is explained here.
enter image description here

In case the link goes down, I will excerpt the math in a very condensed form, but it's just projecting the vector a onto the plane.

a1  ...        a projected onto the plane
|a| ...        magnitude of a
θ   ...        angle between a and n

d   ...        directional vector pointing to a (normalized)
d = (a / |a|) × ((b × a)/|(b × a)|)
d = norm(a) × norm(b × a)

l   ...        magnitude of the projected vector
l = |a| · sin(θ)

a1 = l · d
a1 = |a| · sin(θ) · norm(a) × norm(b × a)

Given: norm(a) × norm(b) = norm(a × b) · sin(θ)
a1 = norm(a) · norm(n) × norm(a) × norm(n)

a1 = n × (a × n / |n|) / |n|

The shift the projected vector a1 by the position of the origin.

$\endgroup$
  • $\begingroup$ Neat solution indeed! But I need some time to understand it. Would you mind explaining prj = n.cross(a.cross(n) / n.length) / n.length or maybe just give a pointer where the relevant information can be found? Thank you. $\endgroup$ – Blender Dadaist Oct 7 '18 at 5:04
  • 1
    $\begingroup$ @BlenderDadaist See edit. $\endgroup$ – Leander Oct 7 '18 at 9:29
5
$\begingroup$

If you only care about planar faces in general, then there's an automatic way for that by selecting Mesh > Clean Up > Make Planar Faces.

If you really want to only move a specific vertex of the quad, you can

  1. move the 3D Cursor to one of the three other vertices (Select the vertex, Shift+S > Cursor to Selected) and switch to Pivot Point > 3D Cursor (. key)
  2. Shift-select all three vertices that should define your plane and should not move
  3. Hit Ctrl+Alt+Space to define a custom Transform Orientation
  4. Select the vertex that you want to move and scale it locally along the Z-axis of the new transform orientation: SZZ 0 Enter
$\endgroup$
  • $\begingroup$ I knew it had to be somewhere ... of course, my meshes never need cleaning up... so I've never found that button ¦ l $\endgroup$ – Robin Betts Oct 6 '18 at 10:24
4
$\begingroup$

Rotate vertex

This should be possible via python. Basically you rotate the vertex in question around the origin by the difference between the normals of the two planar faces of the non-planar quad.

import bmesh
import bpy

bm = bmesh.from_edit_mesh(bpy.context.object.data)
vert = [v for v in bm.verts if v.select == True][0]
face = vert.link_faces[0] #Assuming single linked face
i = [i for i in range(0, len(face.verts)) if face.verts[i] == vert][0]
next  = face.verts[(i+1)%4]#quad
prev  = face.verts[(i-1)%4]
opp  = face.verts[(i+2)%4]
f1 = bm.faces.new([prev, vert, next])
f2 = bm.faces.new([next, opp, prev])
f1.normal_update()
f2.normal_update()
quat = f1.normal.rotation_difference(f2.normal)
bmesh.ops.rotate(bm, cent=(0,0,0), matrix=quat.to_matrix(), verts=[vert])
bm.faces.remove(f1)
bm.faces.remove(f2)
bpy.ops.object.mode_set(mode = 'OBJECT', toggle = False)
$\endgroup$
  • 1
    $\begingroup$ Interesting take, cool re using the modulus to get other verts from index. Is it worth noting that this method using rotation preserves edge length. Method here translates vert to closest point on plane defined by other 3 face verts. Could use i = face.verts[:].index(vert) $\endgroup$ – batFINGER Oct 6 '18 at 11:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.