Skip to content

Changelog#

This is a selection of features added, changes made and bugs fixed with each version. For a full list of changes please see the commits history on the Github repository.

dev#

Date: ongoing

Breaking#

  • Octarine now requires pygfx>=0.17 - 0.17 renamed two symbols in the shader API that octarine's custom shaders build on (lighting_phong() no longer takes is_front, and physical_albeido is now spelled physical_albedo)
  • the octarine.video_helpers module (i.e. make_rotation_video) has been removed in favour of octarine.Animation, which does the same job with better output (no stray alpha channel, no rescaling of odd-sized frames) and without the undeclared scipy/tqdm imports. make_rotation_video(v, "rotation.mp4", n_frames=100, fps=30, axis="z") becomes oc.Animation(v, fps=30).orbit(axis="view", recenter=False, duration=100 / 30) .render("rotation.mp4") - though for most scenes the new default, oc.Animation(v).orbit().render("rotation.mp4"), is what you actually want: a turntable rotation around the scene rather than a roll about the camera's own axis (see Animations)

Improvements#

  • new octarine.Animation class: a timeline of camera moves that can be played in the viewer or rendered to a video. Segments are appended one after the other - orbit (around the whole scene or given objects, about any axis, optionally re-framing the target first), move_to (a named view or a camera state grabbed from Viewer.get_view, swinging around what you are looking at rather than through it), zoom and hold - each with its own duration and easing. render writes .mp4/.gif (via imageio), a folder of numbered PNGs (no extra dependencies) or plain numpy arrays, frame by frame off the timeline rather than the wall clock, and works on an offscreen viewer (see Animations)
  • new Viewer.get_bounds method: like the Viewer.bounds property (which now delegates to it) but for individual objects - v.get_bounds("neuron_1")
  • Viewer.get_view gained a view parameter: v.get_view("XZ") returns the camera state that set_view("XZ") would produce, without moving the camera (or nudging any linked viewers)
  • control panel: new "Animation" tab - set up an orbit or capture the views you want to fly through as keyframes, preview it in the viewer and record it to file. Recording renders one frame per event-loop tick, so the window stays responsive and long renders can be cancelled (see Animations)
  • new Viewer.set_ambient_occlusion method (also available as add_effect('ao')): screen-space ambient occlusion, i.e. the shadowing that ambient light would produce in creases, cavities and where objects touch - pygfx has no ambient occlusion of its own beyond baked ao_map textures. Occlusion is estimated from the depth buffer, so no preprocessing and no extra geometry pass is needed, and the whole thing costs well under a millisecond per frame at the default 16 samples. radius (defaulting to 4% of the scene's diagonal) is the parameter that has to match the scene; intensity, bias, samples, power and blur tune the look, and debug=True renders the raw occlusion (see Ambient occlusion). With an automatic radius the effect now also tracks the scene, i.e. the radius is re-derived as objects are added or removed
  • control panel: the "Effects" tab now has one collapsible section per effect (the checkbox switches the effect on, the arrow expands its parameters) and gained controls for Viewer.shadows and Viewer.set_ambient_occlusion (see GUI Controls)
  • new Viewer.link / Viewer.unlink methods (plus a Viewer.linked property): keep the camera synchronised between two or more viewers, so that panning, rotating or zooming in one of them does the same in all the others - handy for comparing things side by side. Links are symmetrical and transitive, and sync/exclude let you share only part of the camera state (e.g. rotate together but zoom separately). Viewers need not show the same data or use the same controls (see Linking viewers)
  • new Viewer.set_subsurface method (plus a subsurface parameter for Viewer.add_mesh): render meshes as translucent, so that backlit regions glow and shading eases past the terminator instead of dropping off abruptly - the look of skin, wax, marble or thin neurites. Tunable via scatter_color, thickness, distortion, falloff, wrap and glow; composes with set_silhouette (see Subsurface scattering)
  • new Viewer.set_bg_gradient method: use a radial ("studio") gradient as background - a soft pool of light behind the object fading into near-black towards the edges of the frame. Comes with six presets (graphite, cinematic, warm, olive, burgundy, halo), each of which can be tweaked via colors, center, radius, falloff and vignette (see Background). The presets are also available from a new Background dropdown in the GUI controls (both the Qt panel and the Jupyter toolbar)
  • Viewer.set_bgcolor now also takes two colors (for a bottom-to-top gradient) or four colors (one per corner)
  • new Viewer.add_tubes method: render skeletons with a per-node radial profile as tubes, straight from the coefficients with no mesh in between. The surface is generated in the vertex shader, so angular level of detail via n_theta, k and k_normal is a re-draw rather than a re-upload; axial_lod thins the skeleton without touching its topology. Frames are realigned onto one rotation-minimizing chain over the whole tree, so the quads spanning a branch point are no longer twisted. Note that a swept surface self-intersects wherever the radius outruns the centreline's curvature, which rasterised skeletons routinely trigger; dropping harmonics via k is currently the only effective lever against it (see Tubes)
  • new Viewer.headlight property (plus a headlight parameter for octarine.Viewer and a checkbox in the GUI controls): link the light to the camera such that objects are always lit from the front; accepts a float or tuple to also set the light's offset from the camera's axis (see Lighting)
  • new Viewer.lights property: list all light sources illuminating the scene
  • Viewer.add_sparse_volume: new mode="surface" which renders the volume as a shaded isosurface (normals from the volume's gradient) instead of a flat blob; the level is set via the new threshold parameter (see Sparse Volumes)
  • Viewer.add_sparse_volume: new smoothing parameter for mode="surface" which widens the filter the surface normal is taken from, removing the voxel-scale stipple from the shading. The isosurface itself is left alone, so the silhouette is unchanged and no thin structures are lost (see Smoothing)
  • Viewer.add_sparse_volume: mode="density" now accumulates properly instead of saturating on the first voxel; the extinction per voxel is set via the new density parameter (previously it was tied to opacity, whose default of 1 made the volume opaque at the first sample)

  • Viewer.add_sparse_volume now accepts run-length encoded voxels as an (N, 4) array of (x, y, z, x_run_length) - the layout DVID's sparsevol endpoint returns (plus the octarine.VoxelRuns container). Runs are rendered from a bit-per-voxel bitmask with a sparse two-level index, which uses ~23x less GPU memory than the byte-per-voxel atlas (14 MB vs 328 MB for a 31M-voxel neuron) and never needs the coordinates materialized. Binary occupancy only - passing values alongside runs raises. Binary (N, 3) coordinates can opt in with method="bitmask" (see Sparse Volumes)

  • Viewer.screenshot gained a supersample parameter: the frame is rendered at N times the requested resolution and filtered back down, which - unlike the post-processing anti-aliasing that runs during interaction - actually resolves sub-pixel detail instead of smoothing over it. Thin lines, dense point clouds and curved silhouettes benefit the most. It defaults to 2 (4 is as good as it realistically gets) and the image dimensions are unaffected. Effects that are sized in pixels (outlines, depth of field, EDL, the occlusion blur) are scaled along, so the picture keeps its look. Note that Animation.render defaults to supersample=1, since there the cost is paid per frame

Fixes#

  • control panel: the "Browse..." buttons no longer flash up a file dialog that closes again immediately. The static QFileDialog helpers are modal, i.e. they run an event loop of their own - which does not survive every way of hosting the Qt loop (IPython's Qt input hook quits it as soon as the prompt wants control back, taking the dialog with it). The dialogs are now opened non-modally and report back through a signal
  • adding many objects one by one is no longer quadratic in the number of objects: the bounding box visual, the camera centering, the fit of the shadow-casting lights, the ambient occlusion radius and the environment all have to follow the scene, and each of them walks every visual on the canvas. They are now re-fitted once, immediately before the next frame, rather than on every single add. Filling a viewer with 800 objects went from ~11 s to ~0.3 s; the effect is most pronounced with shadows and ambient occlusion (now on by default), but the centering alone accounted for most of it. Note that this means those properties only catch up on the next draw - Viewer.bounds is unaffected and always reports the scene as it currently stands, as is an explicit Viewer.center_camera, which centers there and then. A camera you set up yourself after an add (e.g. via camera.show_object) also still wins, i.e. the deferred centering does not overrule it
  • add_volume/add_sparse_volume: a hex color string (e.g. "#ff9955") raised Colormap '#ff9955' not found instead of being used as a single color
  • fixed colormaps wrapping around: values at the very top of clim sampled the colormap at texcoord 1.0, where pygfx's default wrap="repeat" blended the last color with the (transparent, if hide_zero=True) first one - halving both color and alpha. Affects volumes and sparse volumes
  • Viewer.screenshot raised AttributeError on an on-screen canvas: it forced the frame through a private rendercanvas method that has since been renamed. It now uses the public canvas.force_draw()

Breaking#

  • shadows, the camera-linked headlight and ambient occlusion are now on by default, i.e. scenes look different out of the box: objects cast shadows onto each other, are always lit from the front and get contact shadows in their creases and cavities. Each can be switched off individually via the new shadows / ambient_occlusion parameters and the existing headlight parameter (or the matching properties/methods), e.g. oc.Viewer(shadows=False, headlight=False, ambient_occlusion=False) gets you the previous look. Note that fitting the shadows and the occlusion radius to the scene costs an extra pass over the objects, which is made once per frame in which the scene changed
  • opacity no longer scales the extinction per voxel in add_sparse_volume(mode="density"); it is now a plain global opacity in all modes and density takes its place

To install the current dev version of octarine:

pip uninstall octarine3d -y
pip install git+https://github.com/schlegelp/octarine.git

Version 0.7.0#

Date: 02/08/26

Improvements#

Fixes#

Version 0.6.0#

Date: 18/07/26

Breaking#

  • Octarine now requires pygfx>=0.16 (needed for the new custom shaders)

Improvements#

  • new Viewer.add_sparse_volume method (plus the octarine.VoxelCloud container): render tens of millions of voxels from an (N, 3) array of coordinates using a custom brick-based raycasting shader (see Sparse Volumes)
  • new Viewer.set_silhouette method (plus a silhouette parameter for add_mesh): Neuroglancer-style silhouette rendering for meshes (see Effects & Shading)
  • new Viewer.set_depth_of_field method: focal-blur post-processing with continuous autofocus (see Effects & Shading)
  • Viewer.add_effect: new effects "noise", "fog", "depth" (normalized depth / depth cueing), "normal" and "bloom"; new disable parameter to remove effects
  • Viewer.add_points: new marker options (marker, size_space, edge_size_space, min_size, max_size, edge_width, edge_color, edge_mode, min_edge_width) backed by a custom points shader (see Markers, sizes and edges)
  • control panel: new "Screenshot" (save to file or clipboard) and "Effects" tabs
  • IPython: the event loop is now started with %gui qt instead of %gui qt6

Fixes#

  • legend: color button and visibility checkbox are shown from the start; the color picker is synced to the active object

Full Changelog: v0.5.0...v0.6.0

Version 0.5.0#

Date: 12/06/26

Improvements#

  • Viewer.on_double_click now also accepts a custom callable (see Custom callbacks)
  • new Viewer.objects_grouped property
  • legend improvements: group contents are created lazily, new filter field, hovering over an entry highlights the object in the viewer, group labels show member counts

Full Changelog: v0.4.1...v0.5.0

Version 0.4.1#

Date: 03/06/26

Fixes#

  • don't import the controls module (and hence PySide6) at top level

Full Changelog: v0.4.0...v0.4.1

Version 0.4.0#

Date: 10/04/26

Improvements#

  • objects can now be assigned to a group (e.g. Viewer.add(x, group="my group")); groups show up as collapsible entries in the legend (see Grouping objects)
  • Viewer.add_animation: new on_error="log" option

Fixes#

  • legend: the color button now follows programmatic color changes; long labels are truncated
  • fixed the color picker when multiple viewers are open

Full Changelog: v0.3.5...v0.4.0

Version 0.3.5#

Date: 24/03/26

Improvements#

  • new octarine.video_helpers module with make_rotation_video (removed again in dev, see octarine.Animation)
  • the event-loop warning can now be suppressed

Fixes#

  • fixed Jupyter detection

Full Changelog: v0.3.4...v0.3.5

Version 0.3.4#

Date: 27/02/26

Improvements#

  • relaxed the pinned pygfx version requirement

Fixes#

  • Viewer.add: check for a converter before trying to iterate (e.g. don't treat gfx.Geometry as an iterable)

Full Changelog: v0.3.3...v0.3.4

Version 0.3.3#

Date: 22/10/25

Breaking#

  • dropped support for Python 3.9 - Octarine now requires Python >=3.10

Improvements#

Fixes#

  • fixed setting/getting Viewer.max_fps
  • fixed an issue in the screenshot function

Full Changelog: v0.3.2...v0.3.3

Version 0.3.2#

Date: 17/10/25

Breaking#

  • adapted to pygfx 0.14: the Viewer.blend_mode property is deprecated in favour of the new Viewer.set_alpha_mode method (alpha modes are also set automatically based on object opacity)

Fixes#

  • Viewer.screenshot: fixed transparency when alpha=True

Full Changelog: v0.3.1...v0.3.2

Version 0.3.1#

Date: 27/05/25

Fixes#

  • follow changes to Texture/Map in newer pygfx versions
  • Viewer.screenshot now resolves file paths (e.g. ~/screenshot.png)
  • don't try to start an event loop for offscreen canvases
  • handle ImportError gracefully when initializing the GUI event loop

Full Changelog: v0.3.0...v0.3.1

Version 0.3.0#

Date: 19/03/25

Breaking#

  • drop support for Python 3.8 (follows pygfx)

Improvements#

Full Changelog: v0.2.5...v0.3.0

Version 0.2.5#

Date: 31/09/24

Fixes#

  • fixed an segfault issue

Full Changelog: v0.2.4...v0.2.5

Version 0.2.4#

Date: 28/09/24

Fixes#

  • fix an issue when trimesh is installed without the optional scipy dependency

Full Changelog: v0.2.3...v0.2.4

Version 0.2.3#

Date: 27/09/24

Fixes#

  • fixes an issue with requirements

Full Changelog: v0.2.2...v0.2.3

Version 0.2.2#

Date: 27/09/24

Improvements#

  • existing viewers are tracked in octarine.viewers
  • allow using matplotlib-style line patterns (-, --, etc.)

Full Changelog: v0.2.1...v0.2.2

Version 0.2.1#

Date: 19/09/24

Fixes#

  • fixes an issue with importlib-metadata dependency

Full Changelog: v0.2.0...v0.2.1

Version 0.2.0#

Date: 19/09/24

Improvements#

  • added a basic picking system
  • color picker now shows alpha channel
  • general improvements to volume rendering
  • use Viewer.blend_mode to set blend mode
  • Viewer.set_view now also accepts a dictionary with camera state

Fixes#

  • fixes an issue with Viewer.screenshot

Full Changelog: v0.1.4...v0.2.0

Version <0.2.0#

For earlier versions, please see the commit history.