Skip to content

Instantly share code, notes, and snippets.

@gwpl
Last active January 26, 2026 14:33
Show Gist options
  • Select an option

  • Save gwpl/5583fb5b836bf855af56ccbb2d34cf88 to your computer and use it in GitHub Desktop.

Select an option

Save gwpl/5583fb5b836bf855af56ccbb2d34cf88 to your computer and use it in GitHub Desktop.
render_stl.py - Render STL files to multiple PNG projections using f3d (Python/uv)

render_stl.py

Render STL files to multiple PNG projection images using f3d - a fast and minimalist 3D viewer.

See also: Comparison of programmatic STL rendering options for Linux - compares f3d, OpenSCAD, Blender, trimesh, and other tools.

Features

  • Generates 10 standard views: front, back, left, right, top, bottom, and 4 isometric angles
  • Zero setup with uv - dependencies auto-install on first run
  • Overwrite protection with --force flag
  • Configurable resolution and selective view rendering
  • Headless/offscreen rendering - no display required

Quick Start

# Make executable and run (uv auto-installs f3d)
chmod +x render_stl.py
./render_stl.py model.stl

Or explicitly with uv:

uv run render_stl.py model.stl

Installation

Option 1: uv (recommended)

No installation needed. Just run the script - uv handles dependencies automatically.

# Install uv if you don't have it
curl -LsSf https://astral.sh/uv/install.sh | sh

Option 2: pip

pip install f3d
python render_stl.py model.stl

Usage

usage: render_stl.py [-h] [-o OUTPUT_DIR] [-W WIDTH] [-H HEIGHT]
                     [--views VIEWS] [-f] [-v]
                     stl_file

Render STL file to multiple PNG projections.

positional arguments:
  stl_file              Input STL file

options:
  -h, --help            show this help message and exit
  -o, --output-dir      Output directory (default: same as input file)
  -W, --width           Image width (default: 1920)
  -H, --height          Image height (default: 1080)
  --views VIEWS         Comma-separated list of views to render (default: all)
  -f, --force           Force overwrite existing files
  -v, --verbose         Verbose output

Examples

# Generate all 10 views
./render_stl.py model.stl

# Verbose output
./render_stl.py model.stl -v

# Specific views only
./render_stl.py model.stl --views front,top,iso-front-right

# Custom output directory
./render_stl.py model.stl -o ./renders/

# 4K resolution
./render_stl.py model.stl -W 3840 -H 2160

# Force overwrite existing files
./render_stl.py model.stl -f

Available Views

View Azimuth Elevation
front
back 180°
right 90°
left -90°
top 90°
bottom -90°
iso-front-right 45° 30°
iso-back-right 135° 30°
iso-back-left -135° 30°
iso-front-left -45° 30°

Output

For input model.stl, generates:

model-front.png
model-back.png
model-left.png
model-right.png
model-top.png
model-bottom.png
model-iso-front-right.png
model-iso-back-right.png
model-iso-back-left.png
model-iso-front-left.png

Example

An ASCII STL cube (example-cube.stl) is included in this gist for testing:

./render_stl.py example-cube.stl -v

Requirements

  • Python 3.10+
  • f3d (auto-installed by uv)

License

MIT License

Display the source blob
Display the rendered blob
Raw
solid cube
facet normal 0 0 -1
outer loop
vertex 0 0 0
vertex 1 0 0
vertex 1 1 0
endloop
endfacet
facet normal 0 0 -1
outer loop
vertex 0 0 0
vertex 1 1 0
vertex 0 1 0
endloop
endfacet
facet normal 0 0 1
outer loop
vertex 0 0 1
vertex 1 1 1
vertex 1 0 1
endloop
endfacet
facet normal 0 0 1
outer loop
vertex 0 0 1
vertex 0 1 1
vertex 1 1 1
endloop
endfacet
facet normal 0 -1 0
outer loop
vertex 0 0 0
vertex 1 0 1
vertex 1 0 0
endloop
endfacet
facet normal 0 -1 0
outer loop
vertex 0 0 0
vertex 0 0 1
vertex 1 0 1
endloop
endfacet
facet normal 0 1 0
outer loop
vertex 0 1 0
vertex 1 1 0
vertex 1 1 1
endloop
endfacet
facet normal 0 1 0
outer loop
vertex 0 1 0
vertex 1 1 1
vertex 0 1 1
endloop
endfacet
facet normal -1 0 0
outer loop
vertex 0 0 0
vertex 0 1 0
vertex 0 1 1
endloop
endfacet
facet normal -1 0 0
outer loop
vertex 0 0 0
vertex 0 1 1
vertex 0 0 1
endloop
endfacet
facet normal 1 0 0
outer loop
vertex 1 0 0
vertex 1 1 1
vertex 1 1 0
endloop
endfacet
facet normal 1 0 0
outer loop
vertex 1 0 0
vertex 1 0 1
vertex 1 1 1
endloop
endfacet
endsolid cube
#!/usr/bin/env -S uv run
# /// script
# requires-python = ">=3.10"
# dependencies = ["f3d"]
# ///
"""
render_stl.py - Render STL file to multiple PNG projections.
Generates standard views: front, back, left, right, top, bottom, and isometric views.
Output files are named: {input_basename}-{view_name}.png
INSTALLATION & USAGE
====================
Prerequisites:
- Python 3.10+
- uv (https://docs.astral.sh/uv/) - recommended
- OR: pip install f3d
Method 1: Direct execution with uv (recommended, zero setup)
------------------------------------------------------------
# uv auto-installs dependencies on first run
chmod +x render_stl.py
./render_stl.py model.stl
# Or explicitly:
uv run render_stl.py model.stl
Method 2: Using pip
-------------------
pip install f3d
python render_stl.py model.stl
EXAMPLES
========
# Generate all 10 views
./render_stl.py model.stl
# Generate specific views only
./render_stl.py model.stl --views front,top,iso-front-right
# Output to specific directory
./render_stl.py model.stl -o ./renders/
# Force overwrite + verbose
./render_stl.py model.stl -f -v
# Custom resolution
./render_stl.py model.stl -W 3840 -H 2160
AVAILABLE VIEWS
===============
front, back, left, right, top, bottom,
iso-front-right, iso-back-right, iso-back-left, iso-front-left
LICENSE
=======
MIT License - https://opensource.org/licenses/MIT
"""
import argparse
import sys
from pathlib import Path
import f3d
# Standard projection angles: (azimuth, elevation, name)
PROJECTIONS = [
(0, 0, "front"),
(180, 0, "back"),
(90, 0, "right"),
(-90, 0, "left"),
(0, 90, "top"),
(0, -90, "bottom"),
(45, 30, "iso-front-right"),
(135, 30, "iso-back-right"),
(-135, 30, "iso-back-left"),
(-45, 30, "iso-front-left"),
]
def render_view(engine, camera, output_path: Path, azimuth: float, elevation: float,
verbose: bool = False):
"""Render a single view with given camera angles."""
camera.reset_to_bounds()
if azimuth != 0:
camera.azimuth(azimuth)
if elevation != 0:
camera.elevation(elevation)
img = engine.window.render_to_image()
img.save(str(output_path))
if verbose:
print(f" Saved: {output_path}")
def main():
parser = argparse.ArgumentParser(
description="Render STL file to multiple PNG projections.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s model.stl
Generate model-front.png, model-iso-front-right.png, etc.
%(prog)s model.stl -o ./renders/
Output to specific directory
%(prog)s model.stl --views front,top,iso-front-right
Generate only specific views
%(prog)s model.stl -f -v
Force overwrite, verbose output
Available views:
front, back, left, right, top, bottom,
iso-front-right, iso-back-right, iso-back-left, iso-front-left
""",
)
parser.add_argument("stl_file", type=Path, help="Input STL file")
parser.add_argument(
"-o", "--output-dir", type=Path, default=None,
help="Output directory (default: same as input file)"
)
parser.add_argument(
"-W", "--width", type=int, default=1920, help="Image width (default: 1920)"
)
parser.add_argument(
"-H", "--height", type=int, default=1080, help="Image height (default: 1080)"
)
parser.add_argument(
"--views", type=str, default=None,
help="Comma-separated list of views to render (default: all)"
)
parser.add_argument(
"-f", "--force", action="store_true",
help="Force overwrite existing files"
)
parser.add_argument(
"-v", "--verbose", action="store_true",
help="Verbose output"
)
args = parser.parse_args()
# Validate input file
if not args.stl_file.exists():
print(f"Error: Input file not found: {args.stl_file}", file=sys.stderr)
sys.exit(1)
if not args.stl_file.suffix.lower() == ".stl":
print(f"Warning: Input file may not be an STL: {args.stl_file}", file=sys.stderr)
# Determine output directory
output_dir = args.output_dir or args.stl_file.parent
output_dir.mkdir(parents=True, exist_ok=True)
# Base name for output files
basename = args.stl_file.stem
# Filter projections if --views specified
if args.views:
requested_views = {v.strip().lower() for v in args.views.split(",")}
projections = [(az, el, name) for az, el, name in PROJECTIONS if name in requested_views]
unknown = requested_views - {name for _, _, name in PROJECTIONS}
if unknown:
print(f"Warning: Unknown views ignored: {', '.join(unknown)}", file=sys.stderr)
if not projections:
print("Error: No valid views specified", file=sys.stderr)
sys.exit(1)
else:
projections = PROJECTIONS
# Check for existing files (overwrite protection)
output_files = []
for _, _, view_name in projections:
output_path = output_dir / f"{basename}-{view_name}.png"
output_files.append(output_path)
if output_path.exists() and not args.force:
print(f"Error: Output file exists: {output_path}", file=sys.stderr)
print("Use -f/--force to overwrite", file=sys.stderr)
sys.exit(1)
if args.verbose:
print(f"Input: {args.stl_file}")
print(f"Output directory: {output_dir}")
print(f"Resolution: {args.width}x{args.height}")
print(f"Views to render: {len(projections)}")
# Create offscreen engine
if args.verbose:
print("Initializing f3d engine...")
engine = f3d.Engine.create(offscreen=True)
engine.window.size = args.width, args.height
# Load the STL file
if args.verbose:
print(f"Loading: {args.stl_file}")
engine.scene.add(str(args.stl_file))
camera = engine.window.camera
# Render all views
if args.verbose:
print("Rendering views:")
for (azimuth, elevation, view_name), output_path in zip(projections, output_files):
if args.verbose:
print(f" {view_name} (az={azimuth}, el={elevation})...")
render_view(engine, camera, output_path, azimuth, elevation, verbose=False)
# Summary
if args.verbose:
print(f"Done. Generated {len(projections)} images.")
else:
for output_path in output_files:
print(output_path)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment