#! /usr/bin/python3 # =================================================================== # calculate a normal vector to a surface # ------------------------------------------------------------------- # What is a normal vector? # # A normal vector is a vector perpendicular to a surface (3D) or # a line (2D). If the surface or line is curved, it is perpendicular # at a specific point. If the surface is flat or the line is straight, # the normal vector is perpendicular everywhere. # # So what is a normal vector good for? # # In computer graphics, an object can be made of many small surfaces # (triangles, rectangles, ...). When drawing a solid object, it would # speed things up to not draw (rendered) hidden surfaces. A normal # vector to a flat surface can be used to determine if that surface # is pointed towards the viewer or away from the viewer. For solid # objects, a surface pointing away from the viewer can not be seen, # and does not need to be drawn (rendered). # # For example, if the viewer is at infinity on the +Z axis, a normal # vector's +z value means the surface is pointer toward the viewer # and should be drawn (rendered). A -z value means the surface is # pointed away from the viewer and will not be drawn (rendered). # if z = 0, the surface is sideways to the viewer and may or may # not be seen by the viewer. It depends on the thickness of the # object the surface represents. In many cases it is not seen (drawn) # because it is infinitely thin. # # How do I order the three points to make them clockwise # for this function? # # Pretend you are outside of a solid object looking down on one of # its surfaces. Select 3 points in clockwise order and list them # in order. For example: # # Given the following points on the plane of the object's surface ... # # +-------------------------------+ # | | # | p0 p2 p5 | # | | # | p4 p3 | # | | # | p6 | # | | # +-------------------------------+ # # Create a clockwise list of 3 points ... # # p0,p3,p6 or p2,p5,p3 or p4,p2,p5 or p3,p6,p4 or ... # # Notes: # # 1. Normal vectors are created/calculated so they point away from # an object and not into an object. Pick the "right" three points # on a surface in clockwise order and use the right hand rule to # see which direction the normal vector points. # # 2. The easiest way to pick three points is to pick points that are # used to define an object's surface. They can be used to # calculate a normal vector to the surface. # # ------------------------------------------------------------------- # A flat plane (surface) can defined by three points in 3D space # that are not in a straight line # ------------------------------------------------------------------- # ---- pt0 a point on the surface # ---- pt1 a point on the surface # ---- pt2 a point on the surface # ---- xyzmax maximum x,y,x values allowed # ---- (we only need the direction; not the magnitude) # ------------------------------------------------------------------- # ---- In this code: # ---- a. Point parameters (pt0,pt1,pt2) are entered in a # ---- clockwise order # ---- b. To calculate the normal vector, vectors to pt0 and pt2 are # ---- created/calculated from pt1 # ---- c. Using your left hand to show the direction of the normal # ---- vector. Curl your fingers (clockwise) over the points # ---- used. Your thumb points in the direction of the normal # ---- vector (towards or away from the viewer). # =================================================================== import numpy as np # ------------------------------------------------------------------- # ---- function: create a normal vector to a surface # ------------------------------------------------------------------- def create_normal_vector(pt0,pt1,pt2,xyzmax): # ---- to do the math, convert everything from tuples to # ---- numpy arrays # ---- pt0 - pt1 vx = pt0[0] - pt1[0] # vector x vy = pt0[1] - pt1[1] # vector y vz = pt0[2] - pt1[2] # vector z v1 = np.array([vx,vy,vz]) # create vector # ---- pt2 - pt1 vx = pt2[0] - pt1[0] # vector x vy = pt2[1] - pt1[1] # vector y vz = pt2[2] - pt1[2] # vector z v2 = np.array([vx,vy,vz]) # create vector # ---- create normal vector (cross product) xp = np.cross(v1,v2) # ---- scale the normal vector coords # ---- (direction is all we need; not magnitude) max = np.abs(xp[0]) if np.abs(xp[1]) > max: max = np.abs(xp[1]) elif np.abs(xp[2]) > max: max = np.abs(xp[2]) scale_factor = np.abs(xyzmax) / max x = round(xp[0] * scale_factor) y = round(xp[1] * scale_factor) z = round(xp[2] * scale_factor) scaled_xp = np.array([x,y,z]) ##print(f'scaled xp = {scaled_xp}') ##print('--------------------------------------------------') return np.array(scaled_xp) # ------------------------------------------------------------------- # test three points to see if they are collinear # (within a given tolerance) # return True if 'yes', False if 'no' # # Note: the tolerance parameters is there to avoid floating # point problems. # ---------- a very mathematical solution --------------------------- # three points are collinear if the vectors # v1 = p1 -> p0 and v2 = p2 -> p0 are parallel. # ---------- this is the solution we will use ----------------------- # three points lie on the straight line if the area formed by the # triangle of these three points is zero. So we will check if the # area formed by the triangle is zero or not. # ------------------------------------------------------------------- # www.quora.com/How-can-I-find-the-area-of-a-triangle-in-3D-coordinate-geometry # # This method doesn’t use the 3D cross product so it’s more general. # Let’s do it in N dimensions. # # coordinates of vertices are (p1,p2,...,pn), (q1,q2,...,qn), and (r1,r2,...rn) # # A =( q1 − p1)**2 + (q2 − p2)**2 + . . . + (qn − pn)**2 # # B = (r1 − q1)**2 + (r2 − q2)**2 + . . . + (rn − qn)**2 # # C = (p1 − r1)**2 + (p2 − r2)**2 + . . . + (pn − rn)**2 # # solve for the area S # # 16*(S**2) = (4*A*B) − (C−A−B)**2 # ------------------------------------------------------------------- def points_collinear(p,q,r,tolerance = 0.1): # ----- Calculation the area of triangle. A = (q[0]-p[0])**2 + (q[1]-p[1])**2 + (q[2]-p[2])**2 B = (r[0]-q[0])**2 + (r[1]-q[1])**2 + (r[2]-q[2])**2 C = (p[0]-r[0])**2 + (p[1]-r[1])**2 + (p[2]-r[2])**2 area = np.sqrt(((4*A*B) - (C-A-B)**2)/16) # ---- test the area of the triangle ##print(f'p = ({p[0]},{p[1]},{p[2]})') ##print(f'q = ({q[0]},{q[1]},{q[2]})') ##print(f'r = ({r[0]},{r[1]},{r[2]})') ##print(f'collinear area is {area}') if area < tolerance: return True return False # ------------------------------------------------------------------- # ---- main # ------------------------------------------------------------------- if __name__ == '__main__': import user_interface as ui def display_normal_vector(p0,p1,p2,xp,verbose=True): print() if verbose: print(f'p0 : {pts[0]}') print(f'p1 : {pts[1]}') print(f'p2 : {pts[2]}') print(f'v1 : {pts[0]},{pts[1]}') print(f'v2 : {pts[2]},{pts[1]}') print(f'nornal vector: {xp} (numpy.cross(v1,v2))') print() print('Test Creating Normal Vectors - Viewer at +Z infinity') # --------------------------------------------------------------- pts = [ (50,50,0), (50,-50,0), (-50,-50,0), (-50,50,0) ] p0 = pts[0] p1 = pts[1] p2 = pts[2] print() print('test 1, surface is X,Y plane, Z = 0') xp = create_normal_vector(p0,p1,p2,50) display_normal_vector(p0,p1,p2,xp) print() print('test 2, surface is X,Y plane, Z = 0') xp = create_normal_vector(p2,p1,p0,50) display_normal_vector(p2,p1,p0,xp) # --------------------------------------------------------------- pts = [ (0,50,50), (0,50,-50), (0,-50,-50), (0,-50,50) ] p0 = pts[0] p1 = pts[1] p2 = pts[2] print() print('test 3, surface is Y,Z plane, X = 0') xp = create_normal_vector(p0,p1,p2,50) display_normal_vector(p0,p1,p2,xp) print() print('test 4, surface is Y,Z plane, X = 0') xp = create_normal_vector(p2,p1,p0,50) display_normal_vector(p2,p1,p0,xp) # --------------------------------------------------------------- pts = [ (50,0,50), (-50,0,50), (-50,0,-50), (50,0,50) ] p0 = pts[0] p1 = pts[1] p2 = pts[2] print() print('test 5, surface is X,Z plane, Y = 0') xp = create_normal_vector(p0,p1,p2,50) display_normal_vector(p0,p1,p2,xp) print() print('test 6, surface is X,Z plane, Y = 0') xp = create_normal_vector(p2,p1,p0,50) display_normal_vector(p2,p1,p0,xp) # --------------------------------------------------------------- pts = [ (50,0,50), (-50,0,50), (-50,0,-50) ] p0 = pts[0] p1 = pts[1] p2 = pts[2] print() print('test 7, points collinear?') if points_collinear(p0,p1,p2): print('points ARE collinear') else: print('points ARE NOT collinear') # --------------------------------------------------------------- pts = [ (-50,0,0), (0,0,0), (50,0,0) ] p0 = pts[0] p1 = pts[1] p2 = pts[2] print() print('test 8, points collinear?') if points_collinear(p0,p1,p2): print('points ARE collinear') else: print('points ARE NOT collinear') # --------------------------------------------------------------- pts = [ (50,50,50), (0,0,0), (-50,-50,-50) ] p0 = pts[0] p1 = pts[1] p2 = pts[2] print() print('test 9, points collinear?') if points_collinear(p0,p1,p2): print('points ARE collinear') else: print('points ARE NOT collinear') # --------------------------------------------------------------- pts = [ (0,0,0), (0,0,0), (0,0,0) ] p0 = pts[0] p1 = pts[1] p2 = pts[2] print() print('test 10, points collinear?') if points_collinear(p0,p1,p2): print('points ARE collinear') else: print('points ARE NOT collinear') # --------------------------------------------------------------- pts = [ (50,50,50), (50,50,50), (50,50,50) ] p0 = pts[0] p1 = pts[1] p2 = pts[2] print() print('test 11, points collinear?') if points_collinear(p0,p1,p2): print('points ARE collinear') else: print('points ARE NOT collinear') ui.pause()