Skip to content

Commit e3fe84a

Browse files
authored
Merge pull request #799 from compas-dev/creased-subd
Add creasing to catmull-clark
2 parents 2444a62 + 7492e59 commit e3fe84a

File tree

2 files changed

+73
-61
lines changed

2 files changed

+73
-61
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010

1111
### Added
1212

13+
* Added crease handling to catmull-clark subdivision scheme.
1314
* Added Python 3.9 support
1415

1516
### Changed

src/compas/datastructures/mesh/subdivision.py

Lines changed: 72 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,32 @@ def mesh_subdivide_catmullclark(mesh, k=1, fixed=None):
303303
>>> subd.number_of_faces() == mesh.number_of_faces() * 4 ** k
304304
True
305305
306+
The algorithm supports "integer creasing" as described in
307+
Subdivision Surfaces in Character Animation [1]_.
308+
Creases are supported through the optional edge attribute ``'crease'``,
309+
which can be set to an integer value that defines how sharp the crease is wrt
310+
the number of subdivision steps.
311+
312+
To add an infinitely sharp crease to an edge, set the ``'crease'`` attribute of the edge
313+
to a number higher than the number of subdivision steps.
314+
315+
>>> from compas.geometry import Box, dot_vectors
316+
>>> from compas.datastructures import Mesh
317+
318+
>>> cage = Mesh.from_shape(Box.from_width_height_depth(1, 1, 1))
319+
>>> cage.update_default_edge_attributes({'crease': 0})
320+
>>> top = sorted(cage.faces(), key=lambda face: dot_vectors(cage.face_normal(face), [0, 0, 1]))[-1]
321+
>>> cage.edges_attribute('crease', 5, keys=list(cage.face_halfedges(top)))
322+
323+
>>> subd = cage.subdivide(k=4)
324+
325+
References
326+
----------
327+
.. [1] Tony DeRose, Michael Kass and Tien Truong.
328+
Subdivision Surfaces in Character Animation.
329+
Pixar Animation Studios.
330+
see https://graphics.pixar.com/library/Geri/paper.pdf
331+
306332
"""
307333
cls = type(mesh)
308334
if not fixed:
@@ -314,9 +340,6 @@ def mesh_subdivide_catmullclark(mesh, k=1, fixed=None):
314340

315341
# keep track of original connectivity and vertex locations
316342

317-
bkeys = set(subd.vertices_on_boundary())
318-
bkey_edgepoints = {key: [] for key in bkeys}
319-
320343
# apply quad meshivision scheme
321344
# keep track of the created edge points that are not on the boundary
322345
# keep track track of the new edge points on the boundary
@@ -330,17 +353,14 @@ def mesh_subdivide_catmullclark(mesh, k=1, fixed=None):
330353
for u, v in mesh.edges():
331354

332355
w = subd.split_edge(u, v, allow_boundary=True)
356+
crease = mesh.edge_attribute((u, v), 'crease') or 0
333357

334-
# document why this is necessary
335-
# everything else in this loop is just quad subdivision
336-
if u in bkeys and v in bkeys:
337-
338-
bkey_edgepoints[u].append(w)
339-
bkey_edgepoints[v].append(w)
340-
341-
continue
342-
343-
edgepoints.append(w)
358+
if crease:
359+
edgepoints.append([w, True])
360+
subd.edge_attribute((u, w), 'crease', crease - 1)
361+
subd.edge_attribute((w, v), 'crease', crease - 1)
362+
else:
363+
edgepoints.append([w, False])
344364

345365
fkey_xyz = {fkey: mesh.face_centroid(fkey) for fkey in mesh.faces()}
346366

@@ -370,12 +390,12 @@ def mesh_subdivide_catmullclark(mesh, k=1, fixed=None):
370390
# move each edge point to the average of the neighboring centroids and
371391
# the original end points
372392

373-
for w in edgepoints:
374-
x, y, z = centroid_points([key_xyz[nbr] for nbr in subd.halfedge[w]])
375-
376-
subd.vertex[w]['x'] = x
377-
subd.vertex[w]['y'] = y
378-
subd.vertex[w]['z'] = z
393+
for w, crease in edgepoints:
394+
if not crease:
395+
x, y, z = centroid_points([key_xyz[nbr] for nbr in subd.halfedge[w]])
396+
subd.vertex[w]['x'] = x
397+
subd.vertex[w]['y'] = y
398+
subd.vertex[w]['z'] = z
379399

380400
# move each vertex to the weighted average of itself, the neighboring
381401
# centroids and the neighboring mipoints
@@ -384,32 +404,43 @@ def mesh_subdivide_catmullclark(mesh, k=1, fixed=None):
384404
if key in fixed:
385405
continue
386406

387-
if key in bkeys:
388-
nbrs = set(bkey_edgepoints[key])
389-
nbrs = [key_xyz[nbr] for nbr in nbrs]
390-
e = 0.5
391-
v = 0.5
392-
E = [coord * e for coord in centroid_points(nbrs)]
393-
V = [coord * v for coord in key_xyz[key]]
394-
x, y, z = [E[_] + V[_] for _ in range(3)]
407+
nbrs = mesh.vertex_neighbors(key)
408+
creases = mesh.edges_attribute('crease', keys=[(key, nbr) for nbr in nbrs])
395409

396-
else:
410+
C = sum(1 if crease else 0 for crease in creases)
411+
412+
if C < 2:
397413
fnbrs = [mesh.face_centroid(fkey) for fkey in mesh.vertex_faces(key) if fkey is not None]
398-
nbrs = [key_xyz[nbr] for nbr in subd.halfedge[key]]
399-
n = float(len(nbrs))
400-
f = 1.0 / n
401-
e = 2.0 / n
402-
v = (n - 3.0) / n
414+
enbrs = [key_xyz[nbr] for nbr in subd.halfedge[key]] # this should be the location of the original neighbour
415+
n = len(enbrs)
416+
v = n - 3.0
403417
F = centroid_points(fnbrs)
404-
E = centroid_points(nbrs)
418+
E = centroid_points(enbrs)
405419
V = key_xyz[key]
406-
x = f * F[0] + e * E[0] + v * V[0]
407-
y = f * F[1] + e * E[1] + v * V[1]
408-
z = f * F[2] + e * E[2] + v * V[2]
409-
410-
subd.vertex[key]['x'] = x
411-
subd.vertex[key]['y'] = y
412-
subd.vertex[key]['z'] = z
420+
x = (F[0] + 2.0 * E[0] + v * V[0]) / n
421+
y = (F[1] + 2.0 * E[1] + v * V[1]) / n
422+
z = (F[2] + 2.0 * E[2] + v * V[2]) / n
423+
subd.vertex[key]['x'] = x
424+
subd.vertex[key]['y'] = y
425+
subd.vertex[key]['z'] = z
426+
427+
elif C == 2:
428+
V = key_xyz[key]
429+
E = [0, 0, 0]
430+
for nbr, crease in zip(nbrs, creases):
431+
if crease:
432+
x, y, z = key_xyz[nbr]
433+
E[0] += x
434+
E[1] += y
435+
E[2] += z
436+
x = (6 * V[0] + E[0]) / 8
437+
y = (6 * V[1] + E[1]) / 8
438+
z = (6 * V[2] + E[2]) / 8
439+
subd.vertex[key]['x'] = x
440+
subd.vertex[key]['y'] = y
441+
subd.vertex[key]['z'] = z
442+
else:
443+
pass
413444

414445
mesh = subd
415446

@@ -746,23 +777,3 @@ def trimesh_subdivide_loop(mesh, k=1, fixed=None):
746777
from compas.datastructures import Mesh # noqa: F401
747778
from compas.geometry import Box # noqa: F401
748779
doctest.testmod(globs=globals())
749-
750-
# from compas.datastructures import Mesh
751-
# from compas.geometry import Box
752-
# from compas.utilities import print_profile
753-
# from compas_viewers.multimeshviewer import MultiMeshViewer
754-
755-
# subdivide = print_profile(mesh_subdivide_quad)
756-
757-
# box = Box.from_width_height_depth(10.0, 10.0, 10.0)
758-
# mesh = Mesh.from_shape(box)
759-
# mesh.default_face_attributes.update(path=[])
760-
# subd = subdivide(mesh, k=3)
761-
762-
# print(subd.face_attribute(subd.get_any_face(), 'path'))
763-
764-
# # print(mesh.number_of_faces())
765-
766-
# viewer = MultiMeshViewer()
767-
# viewer.meshes = [subd]
768-
# viewer.show()

0 commit comments

Comments
 (0)