101 lines
5.5 KiB
Python
101 lines
5.5 KiB
Python
|
# Instant field-aligned meshes: minimal Python implementation in 100 LOC. 4-(R/P)oSy, uses randomization approach
|
||
|
import numpy as np
|
||
|
from itertools import product as all_combinations
|
||
|
|
||
|
def lattice_op(p, o, n, target, scale, op = np.floor):
|
||
|
""" 4-PoSy lattice floor/rounding operation -- see the paper appendix for details """
|
||
|
t, d = np.cross(n, o), target - p
|
||
|
return p + scale * (o * op(np.dot(o, d) / scale) + t * op(np.dot(t, d) / scale))
|
||
|
|
||
|
def intermediate_pos(p0, n0, p1, n1):
|
||
|
""" Find an intermediate position between two vertices -- see the paper appendix """
|
||
|
n0p0, n0p1, n1p0, n1p1, n0n1 = np.dot(n0, p0), np.dot(n0, p1), np.dot(n1, p0), np.dot(n1, p1), np.dot(n0, n1)
|
||
|
denom = 1.0 / (1.0 - n0n1*n0n1 + 1e-4)
|
||
|
lambda_0 = 2.0*(n0p1 - n0p0 - n0n1*(n1p0 - n1p1))*denom
|
||
|
lambda_1 = 2.0*(n1p0 - n1p1 - n0n1*(n0p1 - n0p0))*denom
|
||
|
return 0.5 * (p0 + p1) - 0.25 * (n0 * lambda_0 + n1 * lambda_1)
|
||
|
|
||
|
def compat_orientation_extrinsic(o0, n0, o1, n1):
|
||
|
""" Find compatible versions of two representative orientations (with specified normals) """
|
||
|
return max(all_combinations([o0, np.cross(n0, o0), -o0, -np.cross(n0, o0)], [o1, np.cross(n1, o1)]),
|
||
|
key = lambda x: np.dot(x[0], x[1]))
|
||
|
|
||
|
def compat_position_extrinsic(o0, p0, n0, v0, o1, p1, n1, v1, scale):
|
||
|
""" Find compatible versions of two representative positions (with specified normals and orientations) """
|
||
|
t0, t1, middle = np.cross(n0, o0), np.cross(n1, o1), intermediate_pos(v0, n0, v1, n1)
|
||
|
p0, p1 = lattice_op(p0, o0, n0, middle, scale), lattice_op(p1, o1, n1, middle, scale)
|
||
|
x = min(all_combinations([0, 1], [0, 1], [0, 1], [0, 1]),
|
||
|
key = lambda x : np.linalg.norm((p0 + scale * (o0 * x[0] + t0 * x[1])) - (p1 + scale * (o1 * x[2] + t1 * x[3]))))
|
||
|
result = (p0 + scale * (o0 * x[0] + t0 * x[1]), p1 + scale * (o1 * x[2] + t1 * x[3]))
|
||
|
return result
|
||
|
|
||
|
class Mesh:
|
||
|
def __init__(self, filename):
|
||
|
print('Loading \"%s\" ..' % filename)
|
||
|
with open(filename) as f:
|
||
|
if f.readline().strip() != 'OFF': raise Exception("Invalid format")
|
||
|
self.nverts, self.nfaces, _ = map(int, f.readline().split())
|
||
|
self.vertices, self.faces = np.zeros((self.nverts, 3)), np.zeros((self.nfaces, 3), np.uint32)
|
||
|
for i in range(self.nverts):
|
||
|
self.vertices[i, :] = np.fromstring(f.readline(), sep=' ')
|
||
|
for i in range(self.nfaces):
|
||
|
self.faces[i, :] = np.fromstring(f.readline(), sep=' ', dtype=np.uint32)[1:]
|
||
|
print('Computing face and vertex normals ..')
|
||
|
v = [self.vertices[self.faces[:, i], :] for i in range(3)]
|
||
|
face_normals = np.cross(v[2] - v[0], v[1] - v[0])
|
||
|
face_normals /= np.linalg.norm(face_normals, axis=1)[:, None]
|
||
|
self.normals = np.zeros((self.nverts, 3))
|
||
|
for i, j in np.ndindex(self.faces.shape):
|
||
|
self.normals[self.faces[i, j], :] += face_normals[i, :]
|
||
|
self.normals /= np.linalg.norm(self.normals, axis=1)[:, None]
|
||
|
print('Building adjacency matrix ..')
|
||
|
self.adjacency = [set() for _ in range(self.nfaces)]
|
||
|
for i, j in np.ndindex(self.faces.shape):
|
||
|
e0, e1 = self.faces[i, j], self.faces[i, (j+1)%3]
|
||
|
self.adjacency[e0].add(e1)
|
||
|
self.adjacency[e1].add(e0)
|
||
|
print('Randomly initializing fields ..')
|
||
|
self.o_field = np.zeros((self.nverts, 3))
|
||
|
self.p_field = np.zeros((self.nverts, 3))
|
||
|
min_pos, max_pos = self.vertices.min(axis=0), self.vertices.max(axis=0)
|
||
|
np.random.seed(0)
|
||
|
for i in range(self.nverts):
|
||
|
d, p = np.random.standard_normal(3), np.random.random(3)
|
||
|
d -= np.dot(d, self.normals[i]) * self.normals[i]
|
||
|
self.o_field[i] = d / np.linalg.norm(d)
|
||
|
self.p_field[i] = (1-p) * min_pos + p * max_pos
|
||
|
|
||
|
def smooth_orientations(self, iterations = 100):
|
||
|
for i in range(iterations):
|
||
|
print('Smoothing orientations (%i/%i) ..' % (i+1, iterations))
|
||
|
for i in np.random.permutation(np.arange(self.nverts)):
|
||
|
o_i, n_i, weight = self.o_field[i], self.normals[i], 0
|
||
|
for j in np.random.permutation(list(self.adjacency[i])):
|
||
|
o_compat = compat_orientation_extrinsic(o_i, n_i, self.o_field[j], self.normals[j])
|
||
|
o_i = weight*o_compat[0] + o_compat[1]
|
||
|
o_i -= n_i * np.dot(o_i, n_i)
|
||
|
o_i /= np.linalg.norm(o_i)
|
||
|
weight += 1
|
||
|
self.o_field[i] = o_i
|
||
|
|
||
|
def smooth_positions(self, scale, iterations = 100):
|
||
|
for i in range(iterations):
|
||
|
print('Smoothing positions (%i/%i) ..' % (i+1, iterations))
|
||
|
for i in np.random.permutation(np.arange(self.nverts)):
|
||
|
o_i, p_i, n_i, v_i, weight = self.o_field[i], self.p_field[i], self.normals[i], self.vertices[i], 0
|
||
|
for j in self.adjacency[i]:
|
||
|
p_compat = compat_position_extrinsic(o_i, p_i, n_i, v_i,
|
||
|
self.o_field[j], self.p_field[j], self.normals[j], self.vertices[j], scale)
|
||
|
p_i = (weight*p_compat[0] + p_compat[1]) / (weight + 1)
|
||
|
p_i -= n_i * np.dot(p_i - v_i, n_i)
|
||
|
weight += 1
|
||
|
self.p_field[i] = lattice_op(p_i, o_i, n_i, v_i, scale, op = np.round)
|
||
|
|
||
|
def save_position_field(self, filename):
|
||
|
np.savetxt(filename, self.p_field, header="OFF\n%i 0 0" % self.nverts, comments='')
|
||
|
|
||
|
mesh = Mesh("bunny.off")
|
||
|
mesh.smooth_orientations()
|
||
|
mesh.smooth_positions(scale = 0.01)
|
||
|
mesh.save_position_field("output.off")
|