···11+# Fusion uses negative Z values for the depth buffer, while Blender denotes
22+# this with positive values. For "Depth Merge" and other nodes to work
33+# correctly, Blender's output must be flipped. I am unaware of how to do
44+# this in Blender itself, hence this simple post processor.
55+import OpenEXR
66+import Imath
77+import numpy as np
88+import argparse
99+import os
1010+1111+Z_FLIPPED_METADATA_KEY = "zBufferFlipped"
1212+1313+def invert_z_buffer(exr_input_path):
1414+ exr_file = OpenEXR.InputFile(exr_input_path)
1515+1616+ header = exr_file.header()
1717+ channels = header['channels'];
1818+ part_names = channels.keys()
1919+2020+ if Z_FLIPPED_METADATA_KEY in header:
2121+ print(f"Skipping {exr_input_path}")
2222+ exr_file.close()
2323+ return
2424+2525+ processed_parts = {}
2626+2727+ for part_name in part_names:
2828+ pixel_type = header['channels'][part_name].type
2929+ if pixel_type == Imath.PixelType(Imath.PixelType.HALF):
3030+ dtype = np.float16
3131+ elif pixel_type == Imath.PixelType(Imath.PixelType.FLOAT):
3232+ dtype = np.float32
3333+ else:
3434+ raise ValueError(f"Unsupported pixel type {pixel_type} for channel {part_name}.")
3535+3636+ channel_data = exr_file.channel(part_name, pixel_type)
3737+ channel_data_array = np.frombuffer(channel_data, dtype=dtype)
3838+3939+ if "Depth.Z" in part_name:
4040+ channel_data_array = -channel_data_array
4141+4242+ processed_parts[part_name] = channel_data_array.tobytes()
4343+4444+ header[Z_FLIPPED_METADATA_KEY] = 1
4545+4646+ exr_output = OpenEXR.OutputFile(exr_input_path, header)
4747+4848+ exr_output.writePixels(processed_parts)
4949+5050+ exr_file.close()
5151+ exr_output.close()
5252+5353+ print(f"Processed: {exr_input_path}")
5454+5555+if __name__ == "__main__":
5656+ parser = argparse.ArgumentParser(description="Invert the Z-buffer in multipart EXR files.")
5757+5858+ parser.add_argument('exr_files', nargs='+', help="List of EXR files to process.")
5959+6060+ args = parser.parse_args()
6161+6262+ for exr_file in args.exr_files:
6363+ if os.path.exists(exr_file):
6464+ invert_z_buffer(exr_file)
6565+ else:
6666+ print(f"File not found: {exr_file}")
+142
bin/import_quicktime_to_fusion.py
···11+#!/usr/bin/env python3
22+import subprocess
33+import os
44+import sys
55+import re
66+import glob
77+import pyperclip
88+import time
99+1010+def run_command(cmd, shell=False):
1111+ """Run a command and return its output, exit on failure"""
1212+ try:
1313+ if shell:
1414+ result = subprocess.run(cmd, shell=True, check=True, text=True, capture_output=True)
1515+ else:
1616+ result = subprocess.run(cmd, check=True, text=True, capture_output=True)
1717+ return result.stdout.strip()
1818+ except subprocess.CalledProcessError as e:
1919+ print(f"Error executing command: {cmd}")
2020+ print(f"Error message: {e.stderr}")
2121+ sys.exit(1)
2222+ except Exception as e:
2323+ print(f"Unexpected error running command: {e}")
2424+ sys.exit(1)
2525+2626+def find_file(base_name):
2727+ """Find a file with the given base name in the specified directory structure"""
2828+ search_path = "/Volumes/Project/*/Film/**/*"
2929+3030+ try:
3131+ # Expand the glob pattern to find all matching files
3232+ matching_files = []
3333+ for project_dir in glob.glob("/Volumes/Project/*/"):
3434+ for root, dirs, files in os.walk(os.path.join(project_dir, "Film")):
3535+ # Skip hidden directories
3636+ dirs[:] = [d for d in dirs if not d.startswith('.')]
3737+ for file in files:
3838+ if file == base_name and not file.startswith('.'):
3939+ matching_files.append(os.path.join(root, file))
4040+4141+ if not matching_files:
4242+ print(f"Error: Could not find file '{base_name}' in {search_path}")
4343+ sys.exit(1)
4444+ elif len(matching_files) > 1:
4545+ print(f"Warning: Found multiple matches for '{base_name}'. Using the first one.")
4646+4747+ return matching_files[0]
4848+ except Exception as e:
4949+ print(f"Error searching for file: {e}")
5050+ sys.exit(1)
5151+5252+def activate_app(app_name):
5353+ """Activate an application by name"""
5454+ try:
5555+ script = f'tell application "{app_name}" to activate'
5656+ subprocess.run(["osascript", "-e", script], check=True)
5757+ except Exception as e:
5858+ print(f"Error activating {app_name}: {e}")
5959+ sys.exit(1)
6060+6161+def main():
6262+ # Step 1: Run Apple Script to get frame and name from QuickTime Player
6363+ print("Step 1: Getting frame and name from QuickTime Player...")
6464+ applescript = '''
6565+ tell application "QuickTime Player" to tell document 1
6666+ set t to current time
6767+ step forward
6868+ set k to current time
6969+ set r to 1 / (k - t)
7070+ step backward
7171+ return "" & (round (r * t) rounding down) & ":" & name
7272+ end tell
7373+ '''
7474+7575+ try:
7676+ result = subprocess.run(["osascript", "-e", applescript],
7777+ check=True, text=True, capture_output=True)
7878+ frame_and_name = result.stdout.strip()
7979+8080+ if not frame_and_name or ":" not in frame_and_name:
8181+ print("Error: AppleScript did not return expected output")
8282+ sys.exit(1)
8383+8484+ target_frame, name = frame_and_name.split(":", 1)
8585+ target_frame = int(target_frame)
8686+8787+ print(f"Target frame: {target_frame}")
8888+ print(f"File name: {name}")
8989+ except Exception as e:
9090+ print(f"Error running AppleScript: {e}")
9191+ sys.exit(1)
9292+9393+ # Step 2: Find the file on disk
9494+ print("\nStep 2: Finding file on disk...")
9595+ file_path = find_file(name)
9696+ print(f"Found file at: {file_path}")
9797+9898+ # Step 3: Run Fusion script to get current frame
9999+ print("\nStep 3: Getting current frame from Fusion...")
100100+ fusion_script_cmd = "'/Applications/Blackmagic Fusion 19/Fusion.app/Contents/Libraries/fuscript' -x 'print(\"[[\"..Fusion().CurrentComp.CurrentTime..\"]]\")'"
101101+ fusion_output = run_command(fusion_script_cmd, shell=True)
102102+103103+ # Extract the frame number from the output
104104+ match = re.search(r'\[\[(\d+)\]\]', fusion_output)
105105+ if not match:
106106+ print(f"Error: Could not parse frame number from Fusion output: {fusion_output}")
107107+ sys.exit(1)
108108+109109+ current_frame = int(match.group(1))
110110+ print(f"Current frame: {current_frame}")
111111+112112+ # Step 4: Compute TRIM_IN and EXTEND_FIRST
113113+ print("\nStep 4: Computing TRIM_IN and EXTEND_FIRST...")
114114+ trim_in = 0
115115+ extend_first = 0
116116+117117+ if target_frame > current_frame:
118118+ trim_in = target_frame - current_frame
119119+ print(f"Target frame is AFTER current frame. Setting TRIM_IN to {trim_in}")
120120+ else:
121121+ extend_first = current_frame - target_frame
122122+ print(f"Target frame is BEFORE current frame. Setting EXTEND_FIRST to {extend_first}")
123123+124124+ # Step 5: Create the Fusion loader text and copy to clipboard
125125+ print("\nStep 5: Creating Fusion loader text and copying to clipboard...")
126126+ fusion_text = f'''{{Tools = ordered() {{Loader = Loader {{Clips = {{Clip {{ID = "Clip1",Filename = "{file_path}",FormatID = "QuickTimeMovies",Length = 9999999,Multiframe = true,TrimIn = {trim_in},TrimOut = 9999999,ExtendFirst = {extend_first},ExtendLast = 0,Loop = 1,AspectMode = 0,Depth = 0,TimeCode = 0,GlobalStart = 0,GlobalEnd = 9999999}}}},CtrlWZoom = false,Inputs = {{["Gamut.SLogVersion"] = Input {{ Value = FuID {{ "SLog2" }}, }}}},}}}},ActiveTool = "Loader"}}'''
127127+128128+ try:
129129+ pyperclip.copy(fusion_text)
130130+ print("Text copied to clipboard:")
131131+ print(fusion_text)
132132+ except Exception as e:
133133+ print(f"Error copying to clipboard: {e}")
134134+ sys.exit(1)
135135+136136+ # Finally, activate Fusion but don't paste
137137+ print("\nActivating Fusion...")
138138+ activate_app("Fusion")
139139+ print("Script completed successfully!")
140140+141141+if __name__ == "__main__":
142142+ main()
+11-7
readme.md
···11# Clover's Creative Control
2233-This is a set of tools to let additional hardware surfaces integrate with
44-creative applications on a Mac device. In addition to the primary keybinding
55-system, this project can be used as a library to use the control primitves
66-directly (either to build your own hardware integrations, or to control the
77-software).
33+This is a set of tools to let additional hardware devices integrate with
44+creative applications on a Mac device. Additionally, this repo contains a lot of
55+my own tools and scripts I use in the Music/Video creative processes.
8699-These tools only work on macOS. I don't have interest in maintaining other
1010-configurations.
77+In addition to the primary keybinding system and personal software
88+configurations, this project can be used as a library to use the control
99+primitives directly (either to build your own hardware integrations, or to
1010+control the software). This can be done by installing this repo as a `pnpm` git
1111+dependency in your project.
1212+1313+**NOTE**: These tools only work on macOS. I don't have interest in maintaining
1414+other configurations.
11151216## Hardware
1317