Files
2025-07-03 17:03:00 +08:00

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()