117 lines
5.2 KiB
Python
117 lines
5.2 KiB
Python
|
|
"""
|
||
|
|
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()
|
||
|
|
|