Source code for procfunc.ops.file

import logging
import tempfile
import time
from pathlib import Path

import bpy
import numpy as np

from procfunc import types as t
from procfunc.util.log import Suppress

logger = logging.getLogger(__name__)


_SUFFIX_TO_FORMAT = {
    ".png": "PNG",
    ".jpg": "JPEG",
    ".jpeg": "JPEG",
    ".bmp": "BMP",
    ".tga": "TARGA",
    ".tif": "TIFF",
    ".tiff": "TIFF",
    ".exr": "OPEN_EXR",
    ".hdr": "HDR",
    ".webp": "WEBP",
}

_DEVICE_PRIORITY = ["OPTIX", "CUDA", "METAL", "HIP", "CPU"]


[docs] def load_blend(input_path: Path | str): if isinstance(input_path, Path): input_path = str(input_path) bpy.ops.wm.open_mainfile(filepath=input_path)
[docs] def save_blend( output_path: Path | str, autopack: bool = False, ): logger.info(f"Saving blend to {output_path}") if isinstance(output_path, Path): output_path = str(output_path) with Suppress(): if autopack: bpy.ops.file.autopack_toggle() bpy.ops.wm.save_as_mainfile(filepath=output_path) if autopack: bpy.ops.file.autopack_toggle() return output_path
[docs] def import_mesh(path: Path, **kwargs): ext = path.suffix.lower() funcs = { ".obj": bpy.ops.wm.obj_import, ".fbx": bpy.ops.import_scene.fbx, ".stl": bpy.ops.import_mesh.stl, ".ply": bpy.ops.wm.ply_import, ".usdc": bpy.ops.wm.usd_import, } if ext not in funcs: raise ValueError( f"{import_mesh.__name__} does not yet support extension {ext}, please contact the developer" ) funcs[ext](filepath=str(path), **kwargs) if len(bpy.context.selected_objects) > 1 if ext != "usdc" else 2: logger.warning( f"Warning: {ext.upper()} Import produced {len(bpy.context.selected_objects)} objects, " f"but only the first is returned by import_obj" ) if ext != "usdc": return bpy.context.selected_objects[0] else: return next(o for o in bpy.context.selected_objects if o.type != "EMPTY")
[docs] def save_mesh( output_path: Path, objects: list[t.Object | t.Object] | None = None, **kwargs, ) -> Path: funcs = { ".obj": bpy.ops.wm.obj_export, ".fbx": bpy.ops.export_scene.fbx, ".stl": bpy.ops.export_mesh.stl, ".ply": bpy.ops.wm.ply_export, ".usdc": bpy.ops.wm.usd_export, } if output_path.suffix not in funcs: raise ValueError( f"{save_mesh.__name__} does not yet support extension {output_path.suffix}, " " please contact the developer" ) if objects is not None: for obj in bpy.data.objects: obj.select_set(False) for obj in objects: if isinstance(obj, t.Object): obj = obj.item() obj.select_set(True) use_selection = objects is not None match output_path.suffix: case ".obj": bpy.ops.wm.obj_export( filepath=str(output_path), export_selected_objects=use_selection, **kwargs, ) case ".fbx": bpy.ops.export_scene.fbx( filepath=str(output_path), use_selection=use_selection, **kwargs ) case ".stl": bpy.ops.export_mesh.stl( filepath=str(output_path), use_selection=use_selection, **kwargs ) case ".ply": bpy.ops.wm.ply_export( filepath=str(output_path), export_selected_objects=use_selection, **kwargs, ) case ".usdc": bpy.ops.wm.usd_export( filepath=str(output_path), selected_objects_only=use_selection, **kwargs ) case _: raise ValueError(f"Unknown extension {output_path.suffix}") if not output_path.exists(): raise FileNotFoundError(f"Failed to save mesh to {output_path}") return output_path
def _configure_render_device(engine: str, device: str) -> None: scene = bpy.context.scene for i in range(10): try: scene.render.engine = engine break except Exception as e: logger.warning(f"Error setting render engine {i}: {e}") time.sleep(1) else: raise RuntimeError(f"Failed to set render engine to {engine} after 10 attempts") if engine != "CYCLES": return cycles_prefs = bpy.context.preferences.addons["cycles"].preferences if device == "CPU": cycles_prefs.compute_device_type = "NONE" scene.cycles.device = "CPU" return candidates = _DEVICE_PRIORITY if device == "AUTO" else [device, "CPU"] for device_type in candidates: try: cycles_prefs.compute_device_type = device_type cycles_prefs.get_devices() break except Exception: continue else: raise RuntimeError( f"No supported Cycles compute device of types {candidates}, " f"available devices: {cycles_prefs.devices}" ) for d in cycles_prefs.devices: d.use = d.type == device_type scene.cycles.device = "CPU" if device_type == "CPU" else "GPU" def _load_png_rgb(path: Path) -> np.ndarray: img = bpy.data.images.load(str(path), check_existing=False) try: w, h = img.size channels = img.channels pixels = np.asarray(img.pixels[:], dtype=np.float32).reshape(h, w, channels) pixels = pixels[::-1] # bpy stores bottom-up rgb = pixels[..., :3] if channels >= 3 else pixels return np.clip(rgb * 255.0, 0, 255).astype(np.uint8) finally: bpy.data.images.remove(img)
[docs] def render( path: Path | str | None = None, *, camera: "str | bpy.types.Object | None" = None, engine: str = "CYCLES", device: str = "AUTO", samples: int = 128, resolution: int | tuple[int, int] = 512, color_mode: str = "RGB", file_format: str | None = None, view_transform: str = "Filmic", look: str = "None", exposure: float = 0.0, gamma: float = 1.0, display_device: str = "sRGB", ) -> Path | np.ndarray: """Render the active scene with platform-portable defaults. If ``path`` is None, render to a tempdir and return an ``(H, W, 3)`` uint8 ndarray. Otherwise write to ``path`` (file_format inferred from suffix if not given) and return the resolved Path. """ scene = bpy.context.scene _configure_render_device(engine, device) if camera is not None: cam_obj = bpy.data.objects[camera] if isinstance(camera, str) else camera scene.camera = cam_obj if scene.camera is None: raise RuntimeError("No camera set on scene and no `camera` argument given") if isinstance(resolution, int): res_x = res_y = resolution else: res_x, res_y = resolution scene.render.resolution_x = res_x scene.render.resolution_y = res_y if engine == "CYCLES": scene.cycles.samples = samples scene.render.image_settings.color_mode = color_mode scene.view_settings.view_transform = view_transform scene.view_settings.look = look scene.view_settings.exposure = exposure scene.view_settings.gamma = gamma scene.display_settings.display_device = display_device return_array = path is None if return_array: tmp = tempfile.TemporaryDirectory(prefix="pf-render-") out_path = Path(tmp.name) / "render.png" scene.render.image_settings.file_format = "PNG" else: out_path = Path(path) suffix = out_path.suffix.lower() if file_format is None: file_format = _SUFFIX_TO_FORMAT.get(suffix, "PNG") scene.render.image_settings.file_format = file_format out_path.parent.mkdir(parents=True, exist_ok=True) tmp = None scene.render.filepath = str(out_path) bpy.ops.render.render(write_still=True) if return_array: try: arr = _load_png_rgb(out_path) finally: tmp.cleanup() return arr if not out_path.exists(): raise FileNotFoundError(f"Render did not produce {out_path}") return out_path