ManimCommunity / ManimCommunity/manim

Arrow3D: expose the cone's resolution (currently unconfigurable, 95% of the arrow's submobjects)

Open
#5,020 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

new feature
Dominant language
Python
Stars
40.9k
Forks
3.1k
Avg merge
3d 12h
Merged PRs (30d)
25

Description

## Description of proposed feature

`Arrow3D` builds its conical tip as a `Cone`, which inherits `Surface`'s default
`resolution=32`. That is **1024 quad submobjects** for a tip that is about 22 × 40 px on a
1080p frame — **95% of every submobject the arrow contains**. `Arrow3D` does accept a
`resolution` argument, but it is applied to the shaft only, so the tip's mesh density cannot
be changed through any public API.

I'd like to propose exposing the tip's resolution and giving it a default in proportion to the
object's size. On scenes containing arrows this is worth roughly a **2.8×–4.6× speedup**, with
no visible difference at 1080p.

`Arrow3D.__init__` declares `resolution` as a named parameter, so it never lands in
`**kwargs` — and `**kwargs` is the only thing forwarded to the `Cone`:

```python
# manim/mobject/three_d/three_dimensions.py:1243
def __init__(
self,
...
resolution: int | tuple[int, int] = 24, # <- consumed here
**kwargs: Any,
) -> None:
super().__init__(..., resolution=resolution, **kwargs) # <- goes to Line3D
...
self.cone = Cone(
direction=self.direction,
base_radius=base_radius,
height=height,
**kwargs, # <- 'resolution' is not in here
)
```

`Cone.__init__` (`:674`) does not declare `resolution` either, so it forwards to `Surface`
(`:118`), whose default is 32. So the tip always gets 32.

The docstring is accurate about this — it documents `resolution` as "The resolution of the
arrow line." I'm not claiming the behaviour is *wrong*; the issue is that the tip has no knob
at all, and its inherited default is out of proportion for the object's size (1024 quads for
0.042% of the frame).

## How can the new feature be used?

Add a separate parameter for the tip, rather than overloading `resolution` (a `(u, v)` tuple
meant for a line cylinder has no sensible meaning for a cone):

```diff
def __init__(
self,
start: Point3DLike = LEFT,
end: Point3DLike = RIGHT,
thickness: float = 0.02,
height: float = 0.3,
base_radius: float = 0.08,
color: ParsableManimColor = WHITE,
resolution: int | tuple[int, int] = 24,
+ cone_resolution: int | tuple[int, int] = (8, 16),
**kwargs: Any,
) -> None:
@@
self.cone = Cone(
direction=self.direction,
base_radius=base_radius,
height=height,
+ resolution=cone_resolution,
**kwargs,
)
```

With `cone_resolution` defaulting to `(8, 16)`, the tip drops from 1026 submobjects to 130,
and the whole arrow from 1079 to 183.

`Cone`'s `u_range` is `(u_min, slant_length)` and its `v_range` is the azimuthal angle, so the
first number controls subdivisions along the slant and the second around the circumference.
`(8, 16)` keeps 16 segments around the base — the direction where faceting shows on a
silhouette — while cutting the slant direction, where 8 is plenty.

## Additional comments

### Reproduction

```python
from manim import *
from manim.mobject.three_d.three_dimensions import Cone

def count(m):
return 1 + sum(count(s) for s in m.submobjects)

a = Arrow3D(start=ORIGIN, end=RIGHT)
print(count(a)) # 1079
print(count(a.cone)) # 1027
print(len(a.cone.submobjects)) # 1026 (1024 mesh quads + 2 VectorizedPoints)

# 'resolution' is accepted by Arrow3D but never reaches the cone:
for kw in [{}, {"resolution": 4}, {"resolution": (4, 4)}, {"resolution": 32}, {"resolution": (8, 16)}]:
print(kw, len(Arrow3D(start=ORIGIN, end=RIGHT, **kw).cone.submobjects))
```

Observed (manim v0.21.0, cairo renderer) — all five print 1026:

```
1079
1027
1026
{} 1026
{'resolution': 4} 1026
{'resolution': (4, 4)} 1026
{'resolution': 32} 1026
{'resolution': (8, 16)} 1026
```

`Arrow3D(resolution=(8, 16))` hands that tuple to the *shaft* (`Line3D`'s `(2, N)` reduction
only kicks in when an `int` is passed, `:1010`) and still leaves the tip at 32 × 32.

The total is `u_res * v_res` mesh quads for the underlying `Surface`, plus the cone's two
`VectorizedPoint`s:

```python
Cone(resolution=4) # 18 submobjects = 4*4 + 2
Cone(resolution=(4, 8)) # 34 submobjects = 4*8 + 2
Cone(resolution=(8, 16)) # 130 submobjects = 8*16 + 2
Cone(resolution=32) # 1026 submobjects = 32*32 + 2 <- what Arrow3D's tip gets
```

### Impact

Measured at 1920×1080 with a static camera, `wait(1)` = 15 frames, timing only the in-process
`Scene.render()` (manim's ~1.6 s startup excluded), 3 repetitions, median reported:

| scene contents | stock cone (32×32) | cone at (8, 16) | speedup |
| --- | --- | --- | --- |
| 1 × `Arrow3D` | 25.4 ms/frame | 9.0 ms/frame | 2.8× |
| 5 × `Arrow3D` | 109.4 ms/frame | 24.0 ms/frame | 4.6× |

Put differently: five arrows in a 10-second animation at 30 fps (300 frames) cost **~26 extra
seconds of render time** for cone detail that is not resolvable on screen.

The default tip is `height=0.3`, `base_radius=0.08`. At 1080p with `frame_height = 8` that is
a 22 × 40 px object: 0.042% of the frame, drawn as 1024 separate quad mobjects, each going
through depth sorting and rasterization on every frame.

The same effect in a real scene: a 1274-frame, 59-animation teaching sequence at 854×480
containing `ThreeDAxes`, a parametric surface, a cone, and four `Arrow3D` went from **186.7 s
to 54.7 s (3.41×)** when only the tip's mesh density changed. Over four such scenes the ratio
ranged from 2.35× to 3.41×.

### Precedents already in the codebase

This is the same argument the project has already accepted elsewhere:

- **`Line3D`** hard-codes its own reduction in `__init__` (`:1010`):

```python
self.resolution = (2, resolution) if isinstance(resolution, int) else resolution
```

i.e. a line cylinder is only ever subdivided twice along its length, because subdividing
along the height cannot add visual detail. The corresponding commit in
[#1026](https://github.com/ManimCommunity/manim/pull/1026) describes it as reducing the
submobject count "by 12x while sacrificing no quality". The arrow's tip is exactly the same
situation and simply did not get the same treatment.

- **`Dot3D`**, another small 3D mobject, explicitly defaults to `resolution=(8, 8)` (`:524`)
rather than the `Surface` default.

- **`Sphere`** (`:448`) and **`Torus`** (`:1321`) do not hard-code a number at all; they pick a
resolution based on `config.renderer` (`(24, 12)` on cairo, `(101, 51)` on OpenGL).

So the codebase already treats "small mobject → lower resolution" and "resolution is a
renderer-dependent budget" as normal. `Arrow3D`'s tip is the case that misses both.

### On the default value

I compared renders side by side at 854×480 (zoomed 4×, nearest neighbour) and at 1920×1080
(zoomed 2×): stock vs `(8, 16)` was not distinguishable by eye in either. Dropping to `(4, 8)`
*was* distinguishable — visible polygonal faceting on the cone's surface. Hence `(8, 16)` as
the recommendation, with `(4, 8)` as the practical floor. This is a visual judgement, not a
measurement, so it's worth a second pair of eyes.

### Backwards compatibility

- The new parameter is additive; existing code keeps working unchanged.
- The **default render output changes slightly** — this is the one thing to decide on. The
`Line3D` change in #1026 took the same trade (`(2, N)` is also a default change) and was
accepted on the grounds that the removed detail was not visible.
- Existing graphical-unit tests for `Arrow3D` may need reference images regenerated. I have not
checked whether the difference is large enough to trip the test suite.

If maintainers would rather not change any default, the conservative alternative is to ship
`cone_resolution` defaulting to `32` (output stays byte-identical, the knob is merely
*available*). I'd argue against it: 1026 quads is the inherited `Surface` default nobody
chose, and most users will never discover the parameter.

### Workaround today

Monkeypatch the module global — patching `manim.Cone` does **not** work, because
`Arrow3D.__init__` resolves `Cone` in `three_dimensions`' namespace:

```python
import manim.mobject.three_d.three_dimensions as td # effective
# import manim; manim.Cone = ... # silently does nothing

class _FastCone(td.Cone):
def __init__(self, *args, **kwargs):
kwargs.setdefault("resolution", (8, 16))
super().__init__(*args, **kwargs)

td.Cone = _FastCone
```

I've been shipping this patch in a lecture-video pipeline; it's what led me to look upstream.

### Questions

1. Is `cone_resolution` the right API shape, or would you prefer to fold this into the
renderer-dependent resolution convention that `Sphere`/`Torus` already use?
2. Is changing the default to `(8, 16)` acceptable, or should the default stay at 32?
3. Happy to open the PR (with docs and a changelog entry) once the shape is agreed.

I searched for an existing issue covering this and found only the `Line3D` half of the
argument in [#1026](https://github.com/ManimCommunity/manim/pull/1026) — apologies if I missed
a duplicate; please point me at it.

Measurement script

```python
import statistics, time
from manim import *

config.pixel_width, config.pixel_height, config.frame_rate = 1920, 1080, 15
config.disable_caching = True

class Bench(ThreeDScene):
def construct(self):
self.set_camera_orientation(phi=70 * DEGREES, theta=-45 * DEGREES)
for _ in range(5):
self.add(Arrow3D(start=ORIGIN, end=[1, 1, 1], color=RED))
self.wait(1) # 15 frames

reps = []
for _ in range(3):
t = time.perf_counter()
Bench().render()
reps.append(time.perf_counter() - t)

print(statistics.median(reps) / 15 * 1000, "ms/frame")
```

Time it in-process, not by timing the `manim` CLI: startup is ~1.6 s here, which dwarfs a
15-frame measurement window and produced results that were stable but wrong until I moved the
timer inside the process.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in manim/mobject/three_d/three_dimensions.py at Arrow3D.__init__, then inspect Cone, Surface, Line3D, Dot3D, Sphere, and Torus resolution handling. Decide the cone-resolution API and default, update the relevant documentation and changelog, and check whether Arrow3D graphical tests require regenerated reference images.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
computer-graphics, performance
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.