Skip to content

feat: implement custom STL preview renderer from scratch#1429

Draft
ludvig-sandh wants to merge 1 commit into
TagStudioDev:mainfrom
ludvig-sandh:custom-stl-renderer
Draft

feat: implement custom STL preview renderer from scratch#1429
ludvig-sandh wants to merge 1 commit 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 :)

if len(header) < _BINARY_STL_HEADER_SIZE:
raise StlRenderError("STL file is too small")

triangle_count = struct.unpack_from("<I", header, 80)[0]

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.

move the 80 to a constant to document what it represents

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

Comment on lines +110 to +111
if triangle_count > max_triangles:
raise StlRenderError("STL file contains too many triangles")

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 is duplicated and seems like it should be right after the triangle count is retrieved


data = filepath.read_bytes()
trailing = data[expected_size:] if expected_size <= file_size else b""
if expected_size < file_size and not trailing.strip(b"\x00\r\n\t "):

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 way of detecting that it's an ascii variant seems scuffed

@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)

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