""" Maya script for removing faces from 3D garement model that are not visible from the outside cameras The goal is to imitate scanning artifacts that result in missing geometry * Maya 2022+ """ from maya import OpenMaya from maya import cmds import numpy as np from datetime import datetime # My modules from pygarment.mayaqltools import utils def _sample_on_sphere(rad): """Uniformly sample a point on a sphere with radious rad. Return as Maya-compatible floating-point vector""" # Using method of (Muller 1959, Marsaglia 1972) # see the last one here https://mathworld.wolfram.com/SpherePointPicking.html uni_array = np.random.normal(size=3) uni_array = uni_array / np.linalg.norm(uni_array) * rad return OpenMaya.MFloatVector(uni_array[0], uni_array[1], uni_array[2]) def _camera_surface(target, obstacles=[], vertical_scaling_factor=1.5, ground_scaling_factor=1.2): """Generate a (3D scanning) camera surface around provided scene""" # basically, draw a bounding box around the target bbox = np.array(cmds.exactWorldBoundingBox(obstacles + [target])) # [xmin, ymin, zmin, xmax, ymax, zmax] top = bbox[3:] bottom = bbox[:3] center = (top + bottom) / 2 dims = top - bottom dims = [max(dims[0], dims[2]) * ground_scaling_factor, dims[1] * vertical_scaling_factor] cube = cmds.polyCube(height=dims[1], depth=dims[0], width=dims[0], name='camera_surface') # align with center cmds.move(center[0], center[1], center[2], cube, absolute=True) # remove bottom face -- as if no cameras there # adding '.f[1]' would also remove the ceiling cmds.polyDelFacet( cube[0] + '.f[3]') # we know exact structure of default polyCube in Maya2018 & Maya2020 return cube[0], np.max(dims) def remove_invisible(target, obstacles=[], num_rays=30, visibile_rays=4): """Update target 3D mesh: remove faces that are not visible from camera_surface * due to self-occlusion or occlusion by an obstacle * Camera surface is generated aroung the target as a small "room" with empty floor and ceiling In my context, target is usually a garment mesh, and obstacle is a body surface Noise control: * num_rays -- number of random rays to emit from each face -- the less rays, the more noisy the output is * visibile_rays -- number of rays to hit camera surface without obstacles to consider the face to be visible BUT at least one ray is always required to consider face as visible! """ # Follows the idea of self_intersect_3D() checks used in simulation pipeline print('Performing scanning imitation on {} with obstacles {}'.format(target, obstacles)) # generate apropriate camera surface camera_surface_obj, ray_dist = _camera_surface(target, obstacles) start_time = datetime.now() # get mesh objects as OpenMaya object target_mesh, target_dag = utils.get_mesh_dag(target) camera_surface_mesh, _ = utils.get_mesh_dag(camera_surface_obj) obstacles_meshes = [utils.get_mesh_dag(name)[0] for name in obstacles] # search for intersections target_accelerator = target_mesh.autoUniformGridParams() cam_surface_accelerator = camera_surface_mesh.autoUniformGridParams() obstacles_accs = [mesh.autoUniformGridParams() for mesh in obstacles_meshes] to_delete = [] target_face_iterator = OpenMaya.MItMeshPolygon(target_dag) while not target_face_iterator.isDone(): # https://stackoverflow.com/questions/40422082/how-to-find-face-neighbours-in-maya # midpoint of the current face -- start of all the rays face_mean = OpenMaya.MFloatPoint(target_face_iterator.center(OpenMaya.MSpace.kWorld)) face_id = target_face_iterator.index() visible_count = 0 visible = False # Send rays in all directions from the currect vertex for _ in range(num_rays): rayDir = _sample_on_sphere(ray_dist) # Case when face is visible from camera surface if (utils.test_ray_intersect(camera_surface_mesh, face_mean, rayDir, cam_surface_accelerator) # intesection with camera surface and not any([utils.test_ray_intersect(mesh, face_mean, rayDir, acc,) for mesh, acc in zip(obstacles_meshes, obstacles_accs)]) # intesects any of the obstacles and not utils.test_ray_intersect(target_mesh, face_mean, rayDir, target_accelerator, hit_tol=1e-5)): # intersects itself visible_count += 1 if visible_count >= visibile_rays: # enough rays are visible -- no need to test more visible = True if not visible: to_delete.append(face_id) target_face_iterator.next() # iterate! cmds.delete(camera_surface_obj) # clean-up the scene # Remove invisible faces delete_strs = [target + '.f[{}]'.format(face_id) for face_id in to_delete] if len(delete_strs) > 0: cmds.polyDelFacet(tuple(delete_strs)) # as this is the last command to execute, it could be undone with Ctrl-Z once passed = datetime.now() - start_time print('{}::Removed {} faces after {}. Press Ctrl-Z to undo the changes'.format(target, len(to_delete), passed)) return len(to_delete), passed.total_seconds()