Maxon Developers Maxon Developers
    • Documentation
      • Cinema 4D Python API
      • Cinema 4D C++ API
      • Cineware API
      • ZBrush Python API
      • ZBrush GoZ API
      • Code Examples on Github
    • Forum
    • Downloads
    • Support
      • Support Procedures
      • Registered Developer Program
      • Plugin IDs
      • Contact Us
    • Categories
      • Overview
      • News & Information
      • Cinema 4D SDK Support
      • Cineware SDK Support
      • ZBrush 4D SDK Support
      • Bugs
      • General Talk
    • Recent
    • Tags
    • Users
    • Register
    • Login

    Tile rendering with Cinema 4D

    Scheduled Pinned Locked Moved Cinema 4D SDK
    python2026
    5 Posts 2 Posters 80 Views 1 Watching
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • K Offline
      karthikbp
      last edited by

      Dear community,

      Is there a recommended way to do tile rendering—dividing a large image into smaller pieces that can be rendered quickly and then reassembled into one final image?

      Current Approach:

      We currently use the "Render Tiles" camera to divide an image into smaller pieces, then assemble them using software like FFmpeg or OpenImageIO.
      Here's our detailed step-by-step procedure: https://github.com/aws-deadline/deadline-cloud-for-cinema-4d/blob/mainline/docs/tile_rendering/tile_rendering.md based on this old article.

      Alternative approach:

      But while going over some of the render settings, I stumbled on "RDATA_RENDERREGION_LEFT" (similarly for left, top and bottom).
      Could we use this approach instead—rendering specific regions of the image separately and then assembling them into the final image?

      Has anyone implemented a similar solution or can provide guidance on whether this approach is feasible?

      Thank you for your assistance.

      K 1 Reply Last reply Reply Quote 0
      • K Offline
        karthikbp @karthikbp
        last edited by

        I was able to get tile rendering done by running a script like this:

        import c4d
        import os
        
        doc: c4d.documents.BaseDocument
        op: c4d.BaseObject | None
        
        
        def main() -> None:
            # --- Configure these ---
            tiles_x = 2  # columns
            tiles_y = 2  # rows
            output_dir = os.path.join(os.path.expanduser("~"), "Desktop", "tiles")
            # ------------------------
        
            os.makedirs(output_dir, exist_ok=True)
        
            base_rd = doc.GetActiveRenderData()
            full_w = int(base_rd[c4d.RDATA_XRES])
            full_h = int(base_rd[c4d.RDATA_YRES])
            tile_w = full_w // tiles_x
            tile_h = full_h // tiles_y
        
            for ty in range(tiles_y):
                for tx in range(tiles_x):
                    rd = base_rd.GetClone()
        
                    left = tx * tile_w
                    top = ty * tile_h
                    right = left + tile_w
                    bottom = top + tile_h
        
                    # Region at full resolution
                    rd[c4d.RDATA_RENDERREGION] = True
                    rd[c4d.RDATA_RENDERREGION_LEFT] = left
                    rd[c4d.RDATA_RENDERREGION_TOP] = top
                    rd[c4d.RDATA_RENDERREGION_RIGHT] = right
                    rd[c4d.RDATA_RENDERREGION_BOTTOM] = bottom
        
                    # Full-res bitmap — C4D renders region into this
                    bmp = c4d.bitmaps.MultipassBitmap(full_w, full_h, c4d.COLORMODE_RGB)
                    bmp.AddChannel(True, True)
        
                    result = c4d.documents.RenderDocument(
                        doc,
                        rd.GetData(),
                        bmp,
                        c4d.RENDERFLAGS_EXTERNAL | c4d.RENDERFLAGS_SHOWERRORS,
                    )
        
                    if result != c4d.RENDERRESULT_OK:
                        print(f"Tile ({tx}, {ty}) failed with code: {result}")
                        continue
        
                    # Crop the tile region out of the full bitmap
                    tile_bmp = c4d.bitmaps.BaseBitmap()
                    tile_bmp.Init(tile_w, tile_h)
                    for y in range(tile_h):
                        for x in range(tile_w):
                            r, g, b = bmp.GetPixel(left + x, top + y)
                            tile_bmp.SetPixel(x, y, r, g, b)
        
                    path = os.path.join(output_dir, f"tile_{tx}_{ty}.png")
                    tile_bmp.Save(path, c4d.FILTER_PNG)
                    print(f"Saved {path}")
        
            c4d.gui.MessageDialog(
                f"Done! {tiles_x * tiles_y} tiles saved to:\n{output_dir}"
            )
        
        
        if __name__ == "__main__":
            main()
        
        
        K 1 Reply Last reply Reply Quote 0
        • K Offline
          karthikbp @karthikbp
          last edited by

          Hmm, I was able to reassemble the tiles within Cinema 4D as well. Here's the script with reassembly.

          Feel free to chime in if we should not be doing this (performance impact, does not work with different renderers etc)

          import c4d
          import os
          
          doc: c4d.documents.BaseDocument
          op: c4d.BaseObject | None
          
          
          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)
          
              base_rd = doc.GetActiveRenderData()
              full_w = int(base_rd[c4d.RDATA_XRES])
              full_h = int(base_rd[c4d.RDATA_YRES])
              tile_w = full_w // tiles_x
              tile_h = full_h // tiles_y
          
              tile_bmps = {}
          
              for ty in range(tiles_y):
                  for tx in range(tiles_x):
                      rd = base_rd.GetClone()
          
                      left = tx * tile_w
                      top_ = ty * tile_h
                      right = left + tile_w
                      bottom = top_ + tile_h
          
                      rd[c4d.RDATA_RENDERREGION] = True
                      rd[c4d.RDATA_RENDERREGION_LEFT] = left
                      rd[c4d.RDATA_RENDERREGION_TOP] = top_
                      rd[c4d.RDATA_RENDERREGION_RIGHT] = right
                      rd[c4d.RDATA_RENDERREGION_BOTTOM] = bottom
          
                      bmp = c4d.bitmaps.MultipassBitmap(full_w, full_h, c4d.COLORMODE_RGB)
                      bmp.AddChannel(True, True)
          
                      print(f"Rendering tile ({tx}, {ty}) region=({left},{top_})-({right},{bottom})")
                      result = c4d.documents.RenderDocument(
                          doc,
                          rd.GetData(),
                          bmp,
                          c4d.RENDERFLAGS_EXTERNAL | c4d.RENDERFLAGS_SHOWERRORS,
                      )
          
                      if result != c4d.RENDERRESULT_OK:
                          print(f"Tile ({tx}, {ty}) failed with code: {result}")
                          return
          
                      # Crop tile — use same coords as region
                      tile_bmp = c4d.bitmaps.BaseBitmap()
                      tile_bmp.Init(tile_w, tile_h)
                      for y in range(tile_h):
                          for x in range(tile_w):
                              r, g, b = bmp.GetPixel(left + x, top_ + y)
                              tile_bmp.SetPixel(x, y, r, g, b)
          
                      tile_path = os.path.join(output_dir, f"tile_{tx}_{ty}.png")
                      tile_bmp.Save(tile_path, c4d.FILTER_PNG)
                      print(f"Saved {tile_path}")
          
                      tile_bmps[(tx, ty)] = tile_bmp
          
              # --- Reassemble: place each tile at its matching position, no flip ---
              print("Assembling final image...")
              final_bmp = c4d.bitmaps.BaseBitmap()
              final_bmp.Init(full_w, full_h)
          
              for ty in range(tiles_y):
                  for tx in range(tiles_x):
                      tile_bmp = tile_bmps[(tx, ty)]
                      dst_x = tx * tile_w
                      dst_y = ty * tile_h
                      for y in range(tile_h):
                          for x in range(tile_w):
                              r, g, b = tile_bmp.GetPixel(x, y)
                              final_bmp.SetPixel(dst_x + x, dst_y + y, r, g, b)
          
              final_bmp.Save(final_path, c4d.FILTER_PNG)
              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()
          
          
          ferdinandF 1 Reply Last reply Reply Quote 0
          • ferdinandF Offline
            ferdinand @karthikbp
            last edited by ferdinand

            Hey @karthikbp,

            Thank you for reaching out to us. And sorry, I somehow overlooked your topic. Yes, that is how I would have done it too, via RDATA_RENDERREGION_LEFT, etc. The 2026.2 Python SDK will contain new rendering examples, that could be something I could add, as tile rendering is something that comes up from time to time.

            As a fair warning: Your code will not respect OCIO, i.e., the images will have the wrong colors. Since you marked this as 2026, where OCIO is the color management standard, this would apply to all documents. See open_color_io_2025_2.py for an example how to do it correctly. The next release will streamline things there quite a bit.

            Cheers,
            Ferdinand

            MAXON SDK Specialist
            developers.maxon.net

            K 1 Reply Last reply Reply Quote 0
            • K Offline
              karthikbp @ferdinand
              last edited by karthikbp

              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()
              
              
              1 Reply Last reply Reply Quote 0
              • First post
                Last post