How to Modify MCP 3D Relief to Support Different STL Output Formats
You can modify the MCP 3D Relief codebase to support both ASCII and binary STL output formats by refactoring the hard-coded ASCII writer in relief.py into separate format-specific functions and exposing a new format parameter through the CLI.
The bigchx/mcp_3d_relief repository currently generates 3D relief models exclusively as ASCII STL files. If you need to modify the code to support different STL output formats—such as the more compact binary variant—you must restructure the file-writing logic in relief.py. This guide walks through the exact source code changes required to implement multi-format support while preserving the existing depth-map processing pipeline.
Understanding the Current STL Implementation in relief.py
The current implementation hard-codes ASCII STL generation inside the generate_stl coroutine. Located in relief.py, this function opens the output file in text mode ("w") and iterates through computed facets, writing human-readable "solid", "facet normal", and "vertex" records.
This design limits the tool to ASCII output only. The write_facet helper function (also in relief.py) specifically formats strings for the ASCII specification, making it unsuitable for binary serialization without modification.
Refactoring the Code to Support Multiple STL Formats
To modify the code to support different STL output formats, you need to extract the writing logic into dedicated functions, update the function signatures to accept a format specifier, and propagate that parameter through the CLI.
Creating Format-Specific Writer Functions
First, create separate writer functions for ASCII and binary formats. Place these in relief.py near the existing write_facet definition.
The ASCII writer remains straightforward string formatting:
def _write_ascii_stl(file, facets):
"""Write facets to an ASCII STL file."""
file.write("solid mcp_3d_relief\n")
for normal, v1, v2, v3 in facets:
file.write(f" facet normal {normal[0]:.6f} {normal[1]:.6f} {normal[2]:.6f}\n")
file.write(" outer loop\n")
file.write(f" vertex {v1[0]:.6f} {v1[1]:.6f} {v1[2]:.6f}\n")
file.write(f" vertex {v2[0]:.6f} {v2[1]:.6f} {v2[2]:.6f}\n")
file.write(f" vertex {v3[0]:.6f} {v3[1]:.6f} {v3[2]:.6f}\n")
file.write(" endloop\n")
file.write(" endfacet\n")
file.write("endsolid mcp_3d_relief\n")
The binary writer requires Python's struct module to pack data according to the STL binary specification: an 80-byte header, a 4-byte unsigned integer for the triangle count, and for each triangle, 12 floats (normal + 3 vertices) followed by a 2-byte attribute count (typically zero).
import struct
def _write_binary_stl(file, facets):
"""Write facets to a binary STL file."""
# 80-byte header (can contain description)
header = b'Binary STL generated by MCP 3D Relief'
file.write(header + b' ' * (80 - len(header)))
# Number of triangles (uint32, little-endian)
file.write(struct.pack("<I", len(facets)))
# Each triangle: normal (3 floats), vertices (9 floats), attribute byte count (uint16)
for normal, v1, v2, v3 in facets:
data = struct.pack("<12fH",
normal[0], normal[1], normal[2],
v1[0], v1[1], v1[2],
v2[0], v2[1], v2[2],
v3[0], v3[1], v3[2],
0 # attribute byte count
)
file.write(data)
Updating the generate_stl Function Signature
Next, modify the generate_stl coroutine in relief.py to accept a format parameter and dispatch to the appropriate writer. Change the function signature to include fmt: str = "ascii" and replace the hard-coded file-writing block with conditional logic.
async def generate_stl(
depth_map,
output_path,
model_width=50,
model_thickness=5,
base_thickness=2.0,
fmt: str = "ascii", # New parameter for STL output format
):
"""
Generate an STL file from a depth map.
Args:
depth_map: 2D array of depth values
output_path: Path to write the STL file
model_width: Width of the model in mm
model_thickness: Maximum thickness of the relief
base_thickness: Thickness of the base layer
fmt: Output format - "ascii" or "binary"
"""
# ... existing vertex/facet computation logic ...
facets = [] # Computed list of (normal, v1, v2, v3) tuples
if fmt == "binary":
with open(output_path, "wb") as f:
_write_binary_stl(f, facets)
else: # default to ASCII
with open(output_path, "w") as f:
_write_ascii_stl(f, facets)
return output_path
Propagating the Format Parameter Through the API
You must thread the format parameter through the call stack. Update the high-level relief coroutine (also in relief.py) to accept stl_format: str = "ascii" and pass it to generate_stl.
async def relief(
input_image_path,
detail_level=0.5,
model_width=50,
model_thickness=5,
base_thickness=2.0,
output_dir=None,
skip_depth=False,
invert_depth=False,
stl_format: str = "ascii", # New parameter
):
"""
Process an image into a 3D relief model.
Args:
...
stl_format: STL output format - "ascii" or "binary"
"""
# ... existing processing logic ...
stl_path = await generate_stl(
depth_map,
stl_path,
model_width,
model_thickness,
base_thickness,
fmt=stl_format, # Pass format to generator
)
return stl_path
Finally, expose the option via the CLI. Locate the argparse configuration in the if __name__ == "__main__" block of relief.py and add the --stl_format argument.
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Generate 3D relief models from images")
parser.add_argument("input_image_path", help="Path to input image")
parser.add_argument("--detail_level", type=float, default=0.5)
parser.add_argument("--model_width", type=float, default=50)
parser.add_argument("--model_thickness", type=float, default=5)
parser.add_argument("--base_thickness", type=float, default=2.0)
parser.add_argument("--output_dir", default=None)
parser.add_argument("--skip_depth", action="store_true")
parser.add_argument("--invert_depth", action="store_true")
# New CLI argument for STL output format
parser.add_argument(
"--stl_format",
choices=["ascii", "binary"],
default="ascii",
help="Select STL output format (ascii or binary)",
)
args = parser.parse_args()
asyncio.run(
relief(
input_image_path=args.input_image_path,
detail_level=args.detail_level,
model_width=args.model_width,
model_thickness=args.model_thickness,
base_thickness=args.base_thickness,
output_dir=args.output_dir,
skip_depth=args.skip_depth,
invert_depth=args.invert_depth,
stl_format=args.stl_format, # Pass CLI value to function
)
)
Alternative Approaches for Binary STL Generation
While the struct-based approach gives you full control over the binary layout, you can simplify maintenance by using the numpy-stl library (pip install numpy-stl). This library handles the binary packing internally and reduces the risk of endianness or alignment errors.
To integrate numpy-stl, replace the custom _write_binary_stl function with:
import numpy as np
from stl import mesh
def _write_binary_stl_numpy(file_path, facets):
"""Write binary STL using numpy-stl library."""
# Create empty mesh
stl_mesh = mesh.Mesh(np.zeros(len(facets), dtype=mesh.Mesh.dtype))
# Fill mesh with facet data
for i, (normal, v1, v2, v3) in enumerate(facets):
stl_mesh.normals[i] = normal
stl_mesh.vectors[i] = np.array([v1, v2, v3])
# Save as binary
stl_mesh.save(file_path, mode=mesh.Mode.BINARY)
Then update the generate_stl function to call this alternative when fmt == "binary" and the numpy-stl backend is preferred.
Summary
To modify the MCP 3D Relief code to support different STL output formats, implement these architectural changes:
- Extract writer functions: Move the ASCII formatting logic from
generate_stlinto a dedicated_write_ascii_stlhelper, and create a parallel_write_binary_stlfunction using Python'sstructmodule to pack binary data according to the STL specification. - Extend the API signature: Add a
fmt: str = "ascii"parameter togenerate_stlandstl_format: str = "ascii"to the high-levelreliefcoroutine, enabling format selection at every layer of the application. - Update the CLI interface: Add a
--stl_formatargument withchoices=["ascii", "binary"]to theargparseconfiguration in the__main__block, ensuring users can specify their preferred format when running the tool from the command line. - Consider external libraries: For production environments, evaluate replacing the manual binary writer with the
numpy-stllibrary to reduce maintenance overhead and ensure spec-compliant binary output.
Frequently Asked Questions
What is the difference between ASCII and binary STL formats?
ASCII STL files contain human-readable text with keywords like solid, facet normal, and vertex, making them easy to debug but significantly larger in file size. Binary STL files store the same geometric data in a compact 80-byte header followed by 32-bit floating-point numbers and unsigned integers, resulting in files that are roughly 70% smaller and faster to parse by most 3D printing slicers.
How do I add support for additional mesh formats like OBJ or PLY?
To extend MCP 3D Relief beyond STL, create new writer functions (e.g., _write_obj or _write_ply) that translate the internal facets list—containing tuples of (normal, v1, v2, v3)—into the target format's syntax. Update the fmt parameter to accept new enum values (e.g., "obj", "ply"), and modify the conditional logic in generate_stl (or rename it to export_mesh) to dispatch to the appropriate writer based on the file extension or explicit format argument.
Will modifying the STL output format affect the 3D relief generation quality?
No, changing the output format between ASCII and binary does not affect the geometric quality or resolution of the generated 3D relief model. Both formats encode the exact same vertex coordinates and normal vectors; only the storage representation differs. The depth-map processing, mesh triangulation, and dimensional calculations in generate_stl remain identical regardless of whether you choose ASCII or binary output.
Where should I place the new writer functions in the codebase?
Place the new _write_ascii_stl and _write_binary_stl helper functions in relief.py, either immediately before the generate_stl coroutine or in a new stl_writer.py module if you prefer to separate I/O logic from the mesh generation algorithm. If you create a separate module, import the writer functions in relief.py to maintain the existing architecture while keeping the code organized and testable.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →