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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
#!/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