|
| 1 | +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. |
| 2 | +"""OCIO view-transform baking for non-tile renders. |
| 3 | +
|
| 4 | +Works around a Cinema 4D SDK bug: ``RenderDocument``'s internal save does not apply |
| 5 | +the OCIO View Transform, so ordinary (non-tile) renders to 8-bit display formats are |
| 6 | +written un-tone-mapped (dark/"Raw"). This bakes the view transform into the beauty |
| 7 | +image after the render -- the tile path (tile_rendering.finalize_tile_render) already |
| 8 | +does the equivalent for tiles. |
| 9 | +""" |
| 10 | + |
| 11 | +from __future__ import annotations |
| 12 | + |
| 13 | +import os |
| 14 | +from typing import Any |
| 15 | + |
| 16 | +try: |
| 17 | + import c4d # type: ignore |
| 18 | +except ImportError: # pragma: no cover |
| 19 | + raise OSError("Could not find the Cinema4D module. Are you running this inside of Cinema4D?") |
| 20 | + |
| 21 | +try: |
| 22 | + from cinema4d_adaptor.Cinema4DClient.tile_rendering import ( # type: ignore[import] |
| 23 | + C4D_VERSION_2025_2, |
| 24 | + get_format_info, |
| 25 | + ) |
| 26 | +except ImportError: |
| 27 | + from deadline.cinema4d_adaptor.Cinema4DClient.tile_rendering import ( # type: ignore[import] |
| 28 | + C4D_VERSION_2025_2, |
| 29 | + get_format_info, |
| 30 | + ) |
| 31 | + |
| 32 | + |
| 33 | +def _resolve_render_path(doc: Any, render_data: Any, render_bc: Any, frame: int, path: str) -> str: |
| 34 | + """Resolve a C4D render-path's tokens ($take, $frame, $res, ...) using C4D's own |
| 35 | + token system -- the same resolver C4D uses at save time. Returns ``path`` |
| 36 | + unchanged if the token system is unavailable or resolution fails. |
| 37 | +
|
| 38 | + Note: this expands tokens only; C4D still appends the frame number + extension to |
| 39 | + the result at save time (per RDATA_NAMEFORMAT), so the return value is the output |
| 40 | + BASE (a filename prefix), not the full final path. |
| 41 | + """ |
| 42 | + tokensystem = getattr(c4d.modules, "tokensystem", None) |
| 43 | + if tokensystem is None or not hasattr(tokensystem, "FilenameConvertTokens"): |
| 44 | + return path |
| 45 | + take_data = doc.GetTakeData() |
| 46 | + rp_data = { |
| 47 | + "_doc": doc, |
| 48 | + "_rData": render_data, |
| 49 | + "_rBc": render_bc, |
| 50 | + "_frame": frame, |
| 51 | + "_take": take_data.GetCurrentTake() if take_data else None, |
| 52 | + } |
| 53 | + try: |
| 54 | + return tokensystem.FilenameConvertTokens(path, rp_data) |
| 55 | + except Exception: |
| 56 | + return path |
| 57 | + |
| 58 | + |
| 59 | +def bake_full_frame_beauty( |
| 60 | + bm: Any, |
| 61 | + rd: Any, |
| 62 | + render_data: Any, |
| 63 | + doc: Any, |
| 64 | + frame: int, |
| 65 | + render_start_time: float, |
| 66 | +) -> None: |
| 67 | + """Bake the OCIO view transform into a single non-tiled beauty frame. |
| 68 | +
|
| 69 | + Works around a Cinema 4D SDK bug: ``RenderDocument``'s internal save does not |
| 70 | + apply the OCIO View Transform, so the beauty image is written un-tone-mapped |
| 71 | + (dark/"Raw"). Requires the render to have run with |
| 72 | + ``RDATA_BAKE_OCIO_VIEW_TRANSFORM_RENDER`` disabled so ``bm`` holds render-space |
| 73 | + data. No-op on pre-2025.2 Cinema 4D or non-8-bit output (float EXR etc. must stay |
| 74 | + scene-linear). |
| 75 | +
|
| 76 | + Call once per rendered frame (``bm`` holds one frame). The target file is found by |
| 77 | + resolving the render output path's tokens with C4D's own token system and matching |
| 78 | + only files under that exact base name -- so we never touch an unrelated file in the |
| 79 | + folder. C4D's ``A_`` alpha file is excluded for free (it does not start with the |
| 80 | + beauty base); multi-pass files are excluded by their own resolved base. Among |
| 81 | + matches, the file written by this render (newest mtime at/after |
| 82 | + ``render_start_time``) is baked, which is retry-safe. |
| 83 | +
|
| 84 | + Args: |
| 85 | + bm: The rendered MultipassBitmap for one frame (render-space). |
| 86 | + rd: The live render data instance (from GetDataInstance). |
| 87 | + render_data: The render data object (output path / format / tokens). |
| 88 | + doc: The active document (for token resolution). |
| 89 | + frame: The frame number this render produced (for $frame resolution). |
| 90 | + render_start_time: ``time.time()`` captured just before this frame's render. |
| 91 | + """ |
| 92 | + if not ( |
| 93 | + hasattr(c4d, "RDATA_BAKE_OCIO_VIEW_TRANSFORM_RENDER") |
| 94 | + and hasattr(c4d.documents, "BakeOcioViewToBitmap") |
| 95 | + ): |
| 96 | + return |
| 97 | + if rd[c4d.RDATA_FORMATDEPTH] != c4d.RDATA_FORMATDEPTH_8: |
| 98 | + return # only display-referred 8-bit output needs the view transform baked in |
| 99 | + |
| 100 | + beauty_path = render_data[c4d.RDATA_PATH] or "" |
| 101 | + if not beauty_path: |
| 102 | + return |
| 103 | + # Resolve tokens ($take, $frame, ...) with C4D's own resolver, then anchor on the |
| 104 | + # exact base name C4D derives -- C4D appends the frame number + extension to it. |
| 105 | + resolved_base = _resolve_render_path(doc, render_data, rd, frame, beauty_path) |
| 106 | + beauty_dir = os.path.dirname(resolved_base) or "." |
| 107 | + beauty_prefix = os.path.basename(resolved_base) |
| 108 | + if not beauty_prefix or not os.path.isdir(beauty_dir): |
| 109 | + return |
| 110 | + ext, save_filter = get_format_info(render_data[c4d.RDATA_FORMAT]) |
| 111 | + |
| 112 | + # Multi-pass files can share the beauty prefix (e.g. "beauty" vs "beauty_mp"), so |
| 113 | + # exclude them by their own resolved base -- but only when that base is a MORE |
| 114 | + # specific (longer) match than the beauty base (see the loop below). The "A_" alpha |
| 115 | + # file needs no explicit exclusion -- it does not start with the beauty base. |
| 116 | + mp_prefix = "" |
| 117 | + if render_data[c4d.RDATA_MULTIPASS_SAVEIMAGE] and render_data[c4d.RDATA_MULTIPASS_FILENAME]: |
| 118 | + mp_resolved = _resolve_render_path( |
| 119 | + doc, render_data, rd, frame, render_data[c4d.RDATA_MULTIPASS_FILENAME] |
| 120 | + ) |
| 121 | + mp_prefix = os.path.basename(mp_resolved) |
| 122 | + |
| 123 | + # The beauty file this render wrote: derived from C4D's resolved output base, |
| 124 | + # correct extension, not a multi-pass file, and modified at/after this render's |
| 125 | + # start (never re-bakes a stale file; a retry that overwrites in place is caught). |
| 126 | + target = None |
| 127 | + target_mtime = render_start_time |
| 128 | + for fn in os.listdir(beauty_dir): |
| 129 | + if not fn.startswith(beauty_prefix): |
| 130 | + continue |
| 131 | + if not fn.lower().endswith(ext.lower()): |
| 132 | + continue |
| 133 | + # A file is multi-pass only when the multi-pass base is a longer (more |
| 134 | + # specific) prefix than the beauty base. Guarding on length -- not just |
| 135 | + # inequality -- keeps a beauty file whose base merely starts with a shorter |
| 136 | + # multi-pass base (e.g. beauty "render_beauty", mp "render") from being |
| 137 | + # wrongly skipped, and still excludes real multi-pass files whose base |
| 138 | + # extends the beauty base (e.g. beauty "render", mp "render_mp"). |
| 139 | + if mp_prefix and len(mp_prefix) > len(beauty_prefix) and fn.startswith(mp_prefix): |
| 140 | + continue # multi-pass file -- leave as-is |
| 141 | + full = os.path.join(beauty_dir, fn) |
| 142 | + try: |
| 143 | + mtime = os.path.getmtime(full) |
| 144 | + except OSError: |
| 145 | + continue |
| 146 | + if mtime >= target_mtime: |
| 147 | + target = full |
| 148 | + target_mtime = mtime |
| 149 | + |
| 150 | + if target is None: |
| 151 | + print("OCIO view transform NOT baked: no beauty file found from this render") |
| 152 | + return |
| 153 | + |
| 154 | + baked = c4d.documents.BakeOcioViewToBitmap(bm, rd, c4d.SAVEBIT_NONE) |
| 155 | + bm = baked or bm |
| 156 | + if c4d.GetC4DVersion() >= C4D_VERSION_2025_2: |
| 157 | + bm.SetColorProfile(c4d.bitmaps.ColorProfile(), c4d.COLORPROFILE_INDEX_DISPLAYSPACE) |
| 158 | + bm.SetColorProfile(c4d.bitmaps.ColorProfile(), c4d.COLORPROFILE_INDEX_VIEW_TRANSFORM) |
| 159 | + bm.Save(target, save_filter) |
| 160 | + print(f"OCIO view transform baked into beauty output: {os.path.basename(target)}") |
0 commit comments