Skip to content

Commit

Permalink
Merge pull request #2054 from mikedh/fix/smooth
Browse files Browse the repository at this point in the history
mesh.smooth cache logic
  • Loading branch information
mikedh authored Oct 25, 2023
2 parents e62c526 + 6b4cda1 commit 0ca4012
Show file tree
Hide file tree
Showing 22 changed files with 350 additions and 254 deletions.
1 change: 0 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ RUN pip install -e .[all]

# check formatting
RUN ruff trimesh
RUN black --check trimesh

# run pytest wrapped with xvfb for simple viewer tests
RUN xvfb-run pytest --cov=trimesh \
Expand Down
4 changes: 3 additions & 1 deletion docs/content/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,10 @@ if __name__ == '__main__':
When you remove the embed and see the profile result you can then tweak the lines that are slow before finishing the function.

### Automatic Formatting
The only check in that's required to pass in CI is `ruff`, which I usually run with:
Trimesh uses `ruff` and `black` configured in `pyproject.toml`, you can run with:
```
ruff . --fix
black .
```
It can fix a lot of formatting issues automatically. We also periodically run `black` to autoformat the codebase.

Expand All @@ -82,6 +83,7 @@ It can fix a lot of formatting issues automatically. We also periodically run `b

Trimesh uses the [Sphinx Numpy-style](https://www.sphinx-doc.org/en/master/usage/extensions/example_numpy.html#example-numpy) docstrings which get parsed into the API reference page.


## General Tips

Python can be fast but only when you use it as little as possible. In general, if you ever have a block which loops through faces and vertices it will be basically unusable with even moderately sized meshes. All operations on face or vertex arrays should be vectorized numpy operations unless absolutely unavoidable. Profiling helps figure out what is slow, but some general advice:
Expand Down
29 changes: 16 additions & 13 deletions docs/content/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ pip install trimesh[all]
```


Conda Packages
--------------
## Conda Packages

If you prefer a `conda` environment, `trimesh` is available on `conda-forge` ([trimesh-feedstock repo](https://github.com/conda-forge/trimesh-feedstock))

Expand All @@ -34,20 +33,11 @@ If you install [Miniconda](https://docs.conda.io/projects/miniconda/en/latest/)
```
conda install -c conda-forge trimesh
```
Ubuntu-Debian Notes
-------------------

Blender and openSCAD are soft dependencies used for boolean operations with subprocess, you can get them with `apt`:

```
sudo apt-get install blender
```

Dependency Overview
## Dependency Overview
--------------------

Trimesh has a lot of soft-required upstream packages. We try to make sure they're active and big-ish. Here's a quick summary of what they're used for.
Trimesh has a lot of soft-required upstream packages, and we try to make sure they're actively maintained. Here's a quick summary of what they're used for:


| Package | Description | Alternatives | Level |
Expand Down Expand Up @@ -81,3 +71,16 @@ Trimesh has a lot of soft-required upstream packages. We try to make sure they'r
|`pytest-cov`| A plugin to calculate test coverage. | | `test`|
|`pyinstrument`| A sampling based profiler for performance tweaking. | | `test`|
|`vhacdx`| A binding for VHACD which provides convex decompositions | | `recommend`|

## Adding A Dependency

If there's no way to implement something reasonably in vectorized Python or there is a mature minimal C++ or Rust implementation of something useful and complicated we may add a dependency. If it's a major, active project with few dependencies (i.e. `jinja2`) that's probably fine. Otherwise it's a lot more of a commitment than just implementing the function in Python however. An example of this is `embree`, Intel's ray check engine: it is a super complicated thing to do well and 50-100x faster than Python ray checks.

There are a few projects that we've forked into the [`trimesh`](https://github.com/trimesh/) GitHub organization which you can take a look at. The general idea of the requirements for a new compiled dependency are:

- is actively maintained and has an MIT/BSD compatible license.
- has all source code in the repository or as a submodule, i.e. no mysterious binary blobs.
- binding preferably uses [pybind11](https://pybind11.readthedocs.io/en/stable/index.html), [nanobind](https://github.com/wjakob/nanobind) or [maturin/py03](https://github.com/PyO3/maturin) for Rust projects. Cython is also OK but other options are preferable if possible.
- uses `cibuildwheel` to publish releases configured in `pyproject.toml`.
- has unit tests which run in CI
- has minimal dependencies: ideally only `numpy`.
6 changes: 3 additions & 3 deletions examples/offscreen_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@
# a 45 degree homogeneous rotation matrix around
# the Y axis at the scene centroid
rotate = trimesh.transformations.rotation_matrix(
angle=np.radians(10.0), direction=[0, 1, 0], point=scene.centroid
angle=np.radians(30.0), direction=[1, 0, 0], point=scene.centroid
)

for i in range(4):
trimesh.constants.log.info("Saving image %d", i)
for i in range(10):
trimesh.constants.log.info(f"Saving image {i}")

# rotate the camera view transform
camera_old, _geometry = scene.graph[scene.camera.name]
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ requires = ["setuptools >= 61.0", "wheel"]
[project]
name = "trimesh"
requires-python = ">=3.7"
version = "4.0.0"
version = "4.0.1"
authors = [{name = "Michael Dawson-Haggerty", email = "[email protected]"}]
license = {file = "LICENSE.md"}
description = "Import, export, process, analyze and view triangular meshes."
Expand Down
2 changes: 1 addition & 1 deletion tests/regression.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def typical_application():

faces = mesh.facets[mesh.facets_area.argmax()]
outline = mesh.outline(faces) # NOQA
smoothed = mesh.smoothed() # NOQA
smoothed = mesh.smooth_shaded # NOQA

assert mesh.volume > 0.0

Expand Down
18 changes: 18 additions & 0 deletions tests/test_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,24 @@ def test_validate(self):
m.process(validate=True)
assert m.triangles.shape == (1, 3, 3)

def test_smooth_shade(self, count=10):
# test to make sure the smooth shaded copy is cached correctly
mesh = g.trimesh.creation.cylinder(radius=1, height=10)
scene = g.trimesh.Scene({"mesh": mesh})

initial = scene.camera_transform.copy()

hashes = set()
for n in range(count):
angle = g.np.pi * n / count
matrix = g.trimesh.transformations.rotation_matrix(angle, [1, 0, 0])
scene.geometry["mesh"].apply_transform(matrix)
hashes.add(hash(scene.geometry["mesh"].smooth_shaded))
scene.camera_transform = initial

# the smooth shade should be unique for every transform
assert len(hashes) == count


if __name__ == "__main__":
g.trimesh.util.attach_to_log()
Expand Down
8 changes: 4 additions & 4 deletions tests/test_color.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,24 +123,24 @@ def test_smooth(self):
m = g.get_mesh("featuretype.STL")

# will put smoothed mesh into visuals cache
s = m.smoothed()
s = m.smooth_shaded
# every color should be default color
assert s.visual.face_colors.ptp(axis=0).max() == 0

# set one face to a different color
m.visual.face_colors[0] = [255, 0, 0, 255]

# cache should be dumped yo
s1 = m.smoothed()
s1 = m.smooth_shaded
assert s1.visual.face_colors.ptp(axis=0).max() != 0

# do the same check on vertex color
m = g.get_mesh("featuretype.STL")
s = m.smoothed()
s = m.smooth_shaded
# every color should be default color
assert s.visual.vertex_colors.ptp(axis=0).max() == 0
m.visual.vertex_colors[g.np.arange(10)] = [255, 0, 0, 255]
s1 = m.smoothed()
s1 = m.smooth_shaded
assert s1.visual.face_colors.ptp(axis=0).max() != 0

def test_vertex(self):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def test_smoothed(self):

for name in ["ADIS16480.STL", "featuretype.STL"]:
mesh = g.get_mesh(name)
assert len(mesh.faces) == len(mesh.smoothed().faces)
assert len(mesh.faces) == len(mesh.smooth_shaded.faces)

def test_engines(self):
edges = g.np.arange(10).reshape((-1, 2))
Expand Down
16 changes: 16 additions & 0 deletions tests/test_grouping.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,22 @@ def test_blocks(self):
assert set(result[0]) == {1}
assert all(a[i].all() for i in result)

# make sure wrapping works if all values are True
arr = g.np.ones(10, dtype=bool)
result = blocks(arr, min_len=1, wrap=True, only_nonzero=True)
assert len(result) == 1
assert set(result[0]) == set(range(10))

# and all false
arr = g.np.zeros(10, dtype=bool)
result = blocks(arr, min_len=1, wrap=True, only_nonzero=True)
assert len(result) == 0

arr = g.np.zeros(10, dtype=bool)
result = blocks(arr, min_len=1, wrap=True, only_nonzero=False)
assert len(result) == 1
assert set(result[0]) == set(range(10))

def test_block_wrap(self):
"""
Test blocks with wrapping
Expand Down
41 changes: 29 additions & 12 deletions tests/test_interval.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,42 @@ def test_intersection(self):
[[5, 15], [7, 10]],
[[5, 10], [10, 9]],
[[0, 1], [0.9, 10]],
]
[[1000, 1001], [2000, 2001]],
],
dtype=g.np.float64,
)
tru_hit = [False, False, False, True, True, True, True]
tru_int = g.np.array(
[[0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [10, 20], [7, 10], [9, 10], [0.9, 1.0]]

# true intersection ranges
truth = g.np.array(
[
[0.0, 0.0],
[0.0, 0.0],
[0.0, 0.0],
[10, 20],
[7, 10],
[9, 10],
[0.9, 1.0],
[0, 0],
],
dtype=g.np.float64,
)

func = g.trimesh.interval.intersection
intersection = g.trimesh.interval.intersection
union = g.trimesh.interval.union

# check the single- interval results
for ab, h, i in zip(pairs, tru_hit, tru_int):
r_h, r_i = func(*ab)
assert g.np.allclose(r_i, i)
assert r_h == h
for ab, tru in zip(pairs, truth):
result = intersection(*ab)
assert g.np.allclose(result, tru)

# check the vectorized multiple interval results
r_h, r_i = func(pairs[:, 0, :], pairs[:, 1, :])
assert g.np.allclose(r_h, tru_hit)
assert g.np.allclose(r_i, tru_int)
inter = intersection(pairs[:, 0, :], pairs[:, 1, :])

assert g.np.allclose(truth, inter)

# now just run a union on these for the fun of it
u = union(pairs.reshape((-1, 2)))
assert g.np.allclose(u, [[0.0, 21.0], [1000.0, 1001.0], [2000.0, 2001.0]])


if __name__ == "__main__":
Expand Down
2 changes: 1 addition & 1 deletion tests/test_mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def test_meshes(self):
# on a Path3D object
test = outline.paths # NOQA

smoothed = mesh.smoothed() # NOQA
smoothed = mesh.smooth_shaded # NOQA

assert abs(mesh.volume) > 0.0

Expand Down
25 changes: 25 additions & 0 deletions tests/test_segments.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,31 @@ def test_resample(self):
# make sure overall length hasn't changed
assert g.np.isclose(length(res), length(seg))

def test_clean(self):
from trimesh.path.segments import clean, resample

seg = g.np.array(
[[[0, 0], [1, 0]], [[1, 0], [1, 1]], [[1, 1], [0, 1]], [[0, 1], [0, 0]]],
dtype=g.np.float64,
)

c = clean(seg)
assert len(seg) == len(c)
# bounding box should be the same
assert g.np.allclose(
c.reshape((-1, 2)).min(axis=0), seg.reshape((-1, 2)).min(axis=0)
)
assert g.np.allclose(
c.reshape((-1, 2)).max(axis=0), seg.reshape((-1, 2)).max(axis=0)
)

# resample to shorten
r = resample(seg, maxlen=0.3)
assert r.shape == (16, 2, 2)
# after cleaning should be back to 4 segments
rc = clean(r)
assert rc.shape == (4, 2, 2)

def test_svg(self):
from trimesh.path.segments import to_svg

Expand Down
2 changes: 1 addition & 1 deletion tests/test_smooth.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
class SmoothTest(g.unittest.TestCase):
def test_smooth(self):
m = g.get_mesh("chair_model.obj", force="mesh")
s = m.smoothed()
s = m.smooth_shaded

ori = g.np.hstack((m.visual.uv, m.vertices))
check = g.np.hstack((s.visual.uv, s.vertices))
Expand Down
Loading

0 comments on commit 0ca4012

Please sign in to comment.