Skip to content

feat: implement custom STL preview renderer from scratch#1429

Draft
ludvig-sandh wants to merge 4 commits into
TagStudioDev:mainfrom
ludvig-sandh:custom-stl-renderer
Draft

feat: implement custom STL preview renderer from scratch#1429
ludvig-sandh wants to merge 4 commits into
TagStudioDev:mainfrom
ludvig-sandh:custom-stl-renderer

Conversation

@ludvig-sandh

@ludvig-sandh ludvig-sandh commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR experiments with a custom renderer for STL file previews (#351).

STL files only contain triangle information. No shading, material or light, which makes them pretty easy to parse. STL files are pretty rigid and come in two main formats: binary and ASCII. I handle both, then parse the triangles, compute normals (for lighting), project onto 2d, then draw onto a PIL image.

Pros:

Cons:

  • Slow. The rasterization step is sort of a bottleneck since we need to loop through all triangles and draw a polygon() on the image with each of them.

For now it is still an experiment (hence draft), so I have included some benchmarking code for tracking rendering times. My investigation shows roughly:
small files (0-2000 triangles): <80ms
medium files (2000-100,000): <1000ms
large files (100,000+): several seconds

For now I set a tri count cap of 100,000 and don't render anything above. Eventually the cap should be configurable.
It is also possible to approximate the rendering by reducing the triangle counts (only use a subset of them), but this didn't look good as the objects had visible holes in them.

This is what it looks like (two of the bottom files are not rendered due to the cap):
image

From my very short and limited testing the renders looks good enough, and the rendering seems performant enough for users to benefit from the feature. Especially with the tri count cap, as well as concurrently rendering many files at a time. Obviously we may need some more rigorous testing to make sure it works well (eg. on low-end systems).

Although I haven't done any research, I can image most STL files being pretty small. Especially if you have many enough to use TagStudio to organize them. If that's true, a slower renderer that by default only renders smaller STL files might work well for now.

Tasks Completed

  • Platforms Tested:
    • Windows x86
    • Windows ARM
    • macOS x86
    • macOS ARM
    • Linux x86
    • Linux ARM
  • Tested For:
    • Basic functionality
    • PyInstaller executable

@CyanVoxel CyanVoxel added Type: Feature New feature or request Type: UI/UX User interface and/or user experience TagStudio: Thumbs/Previews File thumbnails or previews labels Jul 5, 2026

@Computerdores Computerdores left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

read over the loading code out of curiosity and wrote down some thoughts I had
Note that I didn't think about it particularly long, so take these for the not-thought-through thoughts that they are :)

Comment thread src/tagstudio/qt/previews/stl_renderer.py Outdated
Comment on lines +95 to +97
def _read_stl_header(filepath: Path) -> bytes:
with filepath.open("rb") as file:
return file.read(_BINARY_STL_HEADER_SIZE)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems unnecessary to me

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is necessary to keep loading times small by avoiding expensive full file reads. In the common case for a correctly formatted binary STL file, the full file is never read into a python byte object, which is how loading times are kept below 1ms.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mb on the phrasing, what I meant was having it as a separate function and passing the result around

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see. Just thought it was clean. It's small but it has its very clear purpose. In general I like small functions bc they're easy to test. We can change it though. Curious, is there any downside to it that I'm not seeing perhaps?

Comment thread src/tagstudio/qt/previews/stl_renderer.py Outdated
Comment thread src/tagstudio/qt/previews/stl_renderer.py Outdated
@ludvig-sandh

Copy link
Copy Markdown
Contributor Author

@Computerdores thanks for the review! Good points :)

Well, do we want to move forward with this approach? There's no point in polishing experimental code until we aim to use it.

@CyanVoxel

Copy link
Copy Markdown
Member

Approach-wise this is the furthest anything has come by far, and is actually the first one to get some useful renders (and no crashes so far!). I think this is definitely worth refining as an approach, and I'm shocked that the performance seems decent (it's at least snappy with my 5-50 KB files).
image
There's definitely some visual quirks to iron out as well as what Computerdores has already mentioned, but I think this has my vote to continue on with. I'm having some fun playing with different angles and lighting in the meantime :)

@Computerdores

Computerdores commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

yeah my take is pretty much the same as Cyan's, only thing I am unsure about is the performance
80ms seems fairly good at first glance (and better then I would have expected), but these are also fairly small models and realistically in practice many if not most are going to be larger than 2k tris1 and especially during initial generation of the thumbnails we want the render times to be as small as possible
So imo this either needs significant optimisations (I've got no clue about numpy and co so maybe that is feasible?) or it would need to be written in faster (i.e. compiled) language which would have to be rust, because in the future the plan is to at least partially move to rust

Footnotes

  1. libraries likely either have no/very few 3D models or they have a ton because the person is using TS to manage their models. And in the latter case they are also likely to be larger so the effect would be amplified even more. (this is speculation of course, but I am fairly confident in my guess)

@ludvig-sandh

ludvig-sandh commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

I did three things:

  1. Polish the code, with the review comments in mind. I think it's much clearer now.
  2. Optimizations: (i) PIL .polygon() calls were expensive. Rasterizing ourselves was 3x faster, even in regular interpreted python. (ii) Removed regex parsing for ascii format to a custom solution. 1.5x faster ascii loading.
  3. Experimented with STL rendering in c++ (never used Rust lol) to investigate an upper bound on the speedup achievable by using bindings with a statically typed language.

Results for ~100k triangle render (edit: that's a 5MB file if binary format):

Approach Render time
PIL (original) ~500ms
Custom rasterizer (current) ~150ms
C++ (same alg as current) ~5ms

Two conclusions:

  1. With the recent optimizations, I think what we have now is much better (I'm thinking of @Computerdores perf concerns).
  2. C++ yielded another 30x on top of the optimizations I already made. So, if we wanted to use bindings with rust, we could expect another 10-30x speedup.

Thoughts?

@ludvig-sandh ludvig-sandh marked this pull request as ready for review July 6, 2026 21:12
@ludvig-sandh ludvig-sandh marked this pull request as draft July 6, 2026 21:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

TagStudio: Thumbs/Previews File thumbnails or previews Type: Feature New feature or request Type: UI/UX User interface and/or user experience

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants