Thanks @ferdinand .
I was about to add to this forum post that the rendered images are bit darker than actual.
Thanks for the code link. Here's my updated script with changes for OCIO and I was able to get it rendering correctly at least locally.
Anything to avoid or missed here?
import c4d
import os
doc: c4d.documents.BaseDocument
op: c4d.BaseObject | None
def EnsureIsOcioDocument(doc: c4d.documents.BaseDocument) -> None:
if doc[c4d.DOCUMENT_COLOR_MANAGEMENT] is not c4d.DOCUMENT_COLOR_MANAGEMENT_OCIO:
doc[c4d.DOCUMENT_COLOR_MANAGEMENT] = c4d.DOCUMENT_COLOR_MANAGEMENT_OCIO
doc.UpdateOcioColorSpaces()
if c4d.threading.GeIsMainThreadAndNoDrawThread():
c4d.EventAdd()
def main() -> None:
"""Renders the scene as a grid of tiles, then reassembles into the final image."""
# --- Configure these ---
tiles_x = 2
tiles_y = 2
output_dir = os.path.join(os.path.expanduser("~"), "Desktop", "tiles")
final_path = os.path.join(output_dir, "final_assembled.png")
# ------------------------
os.makedirs(output_dir, exist_ok=True)
EnsureIsOcioDocument(doc)
if not doc.GetDocumentPath():
c4d.gui.MessageDialog("Please save the document first.")
return
# Work directly on the document's render data, matching the reference pattern.
renderData: c4d.documents.RenderData = doc.GetActiveRenderData()
data: c4d.BaseContainer = renderData.GetDataInstance()
requiresBaking: bool = data[c4d.RDATA_FORMATDEPTH] is c4d.RDATA_FORMATDEPTH_8
xRes: int = int(data[c4d.RDATA_XRES_VIRTUAL] or data[c4d.RDATA_XRES])
yRes: int = int(data[c4d.RDATA_YRES_VIRTUAL] or data[c4d.RDATA_YRES])
tile_w = xRes // tiles_x
tile_h = yRes // tiles_y
# Determine save bit flags based on format depth
if data[c4d.RDATA_FORMATDEPTH] is c4d.RDATA_FORMATDEPTH_16:
save_bits = c4d.SAVEBIT_16BITCHANNELS
elif data[c4d.RDATA_FORMATDEPTH] is c4d.RDATA_FORMATDEPTH_32:
save_bits = c4d.SAVEBIT_32BITCHANNELS
else:
save_bits = c4d.SAVEBIT_NONE
# Save original render region state to restore later
orig_region = data[c4d.RDATA_RENDERREGION]
orig_region_left = data[c4d.RDATA_RENDERREGION_LEFT]
orig_region_top = data[c4d.RDATA_RENDERREGION_TOP]
orig_region_right = data[c4d.RDATA_RENDERREGION_RIGHT]
orig_region_bottom = data[c4d.RDATA_RENDERREGION_BOTTOM]
orig_bake_flag = data.GetBool(c4d.RDATA_BAKE_OCIO_VIEW_TRANSFORM_RENDER)
if requiresBaking:
data[c4d.RDATA_BAKE_OCIO_VIEW_TRANSFORM_RENDER] = False
tile_bmps = {}
for ty in range(tiles_y):
for tx in range(tiles_x):
left = tx * tile_w
top_ = ty * tile_h
right = left + tile_w
bottom = top_ + tile_h
data[c4d.RDATA_RENDERREGION] = True
data[c4d.RDATA_RENDERREGION_LEFT] = left
data[c4d.RDATA_RENDERREGION_TOP] = top_
data[c4d.RDATA_RENDERREGION_RIGHT] = right
data[c4d.RDATA_RENDERREGION_BOTTOM] = bottom
# Always render as 32-bit float
bmp = c4d.bitmaps.MultipassBitmap(xRes, yRes, c4d.COLORMODE_RGBf)
bmp.AddChannel(True, True)
print(f"Rendering tile ({tx}, {ty}) region=({left},{top_})-({right},{bottom})")
result = c4d.documents.RenderDocument(
doc,
data,
bmp,
c4d.RENDERFLAGS_EXTERNAL,
)
if result != c4d.RENDERRESULT_OK:
print(f"Tile ({tx}, {ty}) failed with code: {result}")
# Restore original settings before returning
data[c4d.RDATA_RENDERREGION] = orig_region
data[c4d.RDATA_RENDERREGION_LEFT] = orig_region_left
data[c4d.RDATA_RENDERREGION_TOP] = orig_region_top
data[c4d.RDATA_RENDERREGION_RIGHT] = orig_region_right
data[c4d.RDATA_RENDERREGION_BOTTOM] = orig_region_bottom
data[c4d.RDATA_BAKE_OCIO_VIEW_TRANSFORM_RENDER] = orig_bake_flag
return
# Bake OCIO and null profiles ā exactly as reference
if requiresBaking:
bmp = c4d.documents.BakeOcioViewToBitmap(bmp, data, c4d.SAVEBIT_NONE) or bmp
bmp.SetColorProfile(c4d.bitmaps.ColorProfile(), c4d.COLORPROFILE_INDEX_DISPLAYSPACE)
bmp.SetColorProfile(c4d.bitmaps.ColorProfile(), c4d.COLORPROFILE_INDEX_VIEW_TRANSFORM)
# Crop tile preserving bit depth
tile_bmp = bmp.GetClonePart(left, top_, tile_w, tile_h)
if tile_bmp is None:
print(f"Tile ({tx}, {ty}) crop failed")
continue
tile_path = os.path.join(output_dir, f"tile_{tx}_{ty}.png")
tile_bmp.Save(tile_path, c4d.FILTER_PNG, c4d.BaseContainer(), save_bits)
print(f"Saved {tile_path}")
tile_bmps[(tx, ty)] = tile_bmp
# Restore original render data settings
data[c4d.RDATA_RENDERREGION] = orig_region
data[c4d.RDATA_RENDERREGION_LEFT] = orig_region_left
data[c4d.RDATA_RENDERREGION_TOP] = orig_region_top
data[c4d.RDATA_RENDERREGION_RIGHT] = orig_region_right
data[c4d.RDATA_RENDERREGION_BOTTOM] = orig_region_bottom
data[c4d.RDATA_BAKE_OCIO_VIEW_TRANSFORM_RENDER] = orig_bake_flag
# --- Reassemble using GetPixelCnt/SetPixelCnt to preserve full bit depth ---
print("Assembling final image...")
# Get bit depth from the first tile
first_tile = tile_bmps.get((0, 0))
if first_tile is None:
print("No tiles to assemble")
return
tile_bpp = first_tile.GetBt()
bpc = tile_bpp // 3
if bpc == 32:
color_mode = c4d.COLORMODE_RGBf
inc = 12
elif bpc == 16:
color_mode = c4d.COLORMODE_RGBw
inc = 6
else:
color_mode = c4d.COLORMODE_RGB
inc = 3
final_bmp = c4d.bitmaps.BaseBitmap()
final_bmp.Init(xRes, yRes, depth=tile_bpp)
row_buffer = bytearray(tile_w * inc)
row_view = memoryview(row_buffer)
for ty in range(tiles_y):
for tx in range(tiles_x):
if (tx, ty) not in tile_bmps:
continue
tile_bmp = tile_bmps[(tx, ty)]
dst_x = tx * tile_w
dst_y = ty * tile_h
for py in range(tile_h):
tile_bmp.GetPixelCnt(
0, py, tile_w, row_view, inc, color_mode, c4d.PIXELCNT_0
)
final_bmp.SetPixelCnt(
dst_x, dst_y + py, tile_w, row_view, inc, color_mode, c4d.PIXELCNT_0
)
final_bmp.Save(final_path, c4d.FILTER_PNG, c4d.BaseContainer(), save_bits)
print(f"Saved assembled image to {final_path}")
c4d.bitmaps.ShowBitmap(final_bmp)
c4d.gui.MessageDialog(f"Done! {tiles_x * tiles_y} tiles rendered and assembled.\n{final_path}")
if __name__ == "__main__":
main()