-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #73 from beersandrew/add-cards-generator-script
feat: adding cards generation script
- Loading branch information
Showing
12 changed files
with
463 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
# ignore temp files when using usdfixbrokenpixarschemas | ||
*.bck.* | ||
.DS_Store | ||
.DS_Store | ||
*.pyc |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
# USD Cards Generator | ||
|
||
## Purpose | ||
Given a USD file, take pictures of each of it's axes and assign them as the cards' images | ||
|
||
## Usage | ||
|
||
`python generate_cards.py --usd-file <usd_file>` | ||
|
||
optional arguments: | ||
- `usd-file` : The USD file you want to generate card images from | ||
- `-h`, `--help` : Show help | ||
- `--create-usdz-result` : Returns the resulting files as a new USDZ file called `<subject_usd_file>_Cards.usdz` | ||
- `--output-extension` : The file extension of the output image you want (exr, png..). If using exr, make sure your usd install includes OpenEXR | ||
- `--verbose` : Prints out the steps as they happen | ||
- `--dome-light` : The path to the dome light HDR image to use, if any | ||
- `--apply-cards` : Saves the images as the cards for the given USD file. If USDZ is input, a new USD file will be created to wrap the existing one called `<subject_usd_file>_Cards.usd` | ||
- `--render-purposes` : A comma separated list of render purposes to include in the thumbnail. Valid values are: default, render, proxy, guide | ||
- `--image-width`: The width of the image to generate. Default is 2048 | ||
- `--directory` : A directory to generate thumbnails for all .usd, .usda, .usdc, and .usdz files. When a directory is supplied, usd-file is ignored | ||
- `--recursive` : Will recursively search all directories underneath a given directory, requires a directory to be set | ||
|
||
Note: You must have usd installed and available in your path. [Install Steps Here](https://github.com/PixarAnimationStudios/OpenUSD#getting-and-building-the-code) | ||
|
||
## How it works when using --apply-cards | ||
Given a USD file to use as the subject of the cards do the following | ||
|
||
1. Apply DrawMode defaults (cards attribute, box geometry) | ||
2. Create, position, and orient cameras for each axis (XPos, XNeg, YPos, YNeg, ZPos, ZNeg) | ||
3. Sublayer the subject in the camera | ||
4. Run `usdrecord` to take a snapshot and store it in `/renders/<axis>.0.png` | ||
- macOS | ||
- `usdrecord --frames 0:0 --camera ZCamera --imageWidth 2048 --renderer Metal camera.usda <axis>.#.png` | ||
- windows | ||
- `usdrecord --frames 0:0 --camera ZCamera --imageWidth 2048 --renderer GL camera.usda <axis>.#.png` | ||
- note: this will run with `shell=True` for the subprocess call | ||
- linux | ||
- `usdrecord --frames 0:0 --camera ZCamera --imageWidth 2048 --renderer Metal camera.usda <axis>.#.png` | ||
- ZCamera & camera.usda are generated in step 2 | ||
4. If the file is not a USDZ file, assign each image as the usd's axis card image | ||
5. If the file is a USDZ file, create a new `<subject_usd_file>_Cards.usda`, assign each image as the usd's axis card image, and sublayer `<subject_usd_file>.usdz` | ||
6. If `--create-usdz-result` is passed in, combine all of the files into a USDZ, in the case of a USD file it would be the input file and each axis image. In the case of a USDZ file it would be the new USDA, the axis images, and the input USDZ |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
#!/usr/bin/env python3 | ||
|
||
from pxr import UsdGeom, Gf | ||
from camera.utils.distanceCalculator import get_distance_to_frame_subject | ||
from camera.globals import SENSOR_SIZE, FOCAL_LENGTH, FOCUS_DISTANCE | ||
|
||
def create_camera_for_card(card, camera_stage, center_of_card_face, bounding_box, camera_view_axis_distance): | ||
camera_prim = create_camera_with_defaults(camera_stage, card.name) | ||
|
||
distance = get_distance_to_frame_subject(bounding_box, SENSOR_SIZE, FOCAL_LENGTH) | ||
|
||
nearClip = distance / 10.0 | ||
farClip = (( distance + camera_view_axis_distance ) / 10.0) * 2 | ||
clippingPlanes = Gf.Vec2f(nearClip, farClip) | ||
camera_prim.GetClippingRangeAttr().Set(clippingPlanes) | ||
|
||
position_camera(camera_prim, card, center_of_card_face, distance) | ||
|
||
calculate_apertures(camera_prim, bounding_box, distance, card) | ||
|
||
rotate_camera(card, camera_prim) | ||
|
||
def create_camera_with_defaults(camera_stage, name): | ||
camera_prim = UsdGeom.Camera.Define(camera_stage, '/CardGenerator/' + name) | ||
camera_prim.CreateFocalLengthAttr(FOCAL_LENGTH) | ||
camera_prim.CreateFocusDistanceAttr(FOCUS_DISTANCE) | ||
camera_prim.CreateFStopAttr(0) | ||
camera_prim.CreateHorizontalApertureOffsetAttr(0) | ||
camera_prim.CreateProjectionAttr("perspective") | ||
camera_prim.CreateVerticalApertureOffsetAttr(0) | ||
|
||
return camera_prim | ||
|
||
def position_camera(camera_prim, card, center_of_card_face, distance): | ||
camera_translation = create_camera_translation(card, center_of_card_face, distance) | ||
apply_camera_translation(camera_prim, camera_translation) | ||
|
||
def create_camera_translation(card, center_of_card_face, distance): | ||
return center_of_card_face + create_translate_vector(distance * card.sign, card.translationIndex) | ||
|
||
def calculate_apertures(camera_prim, bounding_box, distance, card): | ||
flip_aperatures = False | ||
for rotation in card.rotations: | ||
if rotation.amount != 180: | ||
flip_aperatures = True | ||
break | ||
|
||
actual_horizontal_aperture = FOCAL_LENGTH * bounding_box.width / distance | ||
actual_vertical_aperture = FOCAL_LENGTH * bounding_box.height / distance | ||
|
||
if flip_aperatures: | ||
camera_prim.CreateHorizontalApertureAttr(actual_vertical_aperture) | ||
camera_prim.CreateVerticalApertureAttr(actual_horizontal_aperture) | ||
else: | ||
camera_prim.CreateHorizontalApertureAttr(actual_horizontal_aperture) | ||
camera_prim.CreateVerticalApertureAttr(actual_vertical_aperture) | ||
|
||
def rotate_camera(card, camera_prim): | ||
for rotation in card.rotations: | ||
apply_camera_rotation(camera_prim, rotation.index, rotation.amount) | ||
|
||
def create_translate_vector(distance, translationIndex): | ||
vector = Gf.Vec3d(0, 0, 0) | ||
vector[translationIndex] = distance / 10.0 # convert units from mm to cm | ||
return vector | ||
|
||
def apply_camera_translation(camera_prim, camera_translation): | ||
xformRoot = UsdGeom.Xformable(camera_prim.GetPrim()) | ||
translateOp = xformRoot.AddTranslateOp(UsdGeom.XformOp.PrecisionDouble) | ||
translateOp.Set(camera_translation) | ||
|
||
def apply_camera_rotation(camera_prim, rotationDirection, rotationAmount): | ||
xformRoot = UsdGeom.Xformable(camera_prim.GetPrim()) | ||
if rotationDirection == 0: | ||
rotateOp = xformRoot.AddRotateXOp() | ||
rotateOp.Set(rotationAmount) | ||
elif rotationDirection == 1: | ||
rotateOp = xformRoot.AddRotateYOp() | ||
rotateOp.Set(rotationAmount) | ||
else: | ||
rotateOp = xformRoot.AddRotateZOp() | ||
rotateOp.Set(rotationAmount) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
#!/usr/bin/env python3 | ||
|
||
from pxr import Usd, UsdGeom, UsdLux, Gf | ||
from collections import namedtuple | ||
from camera.cameraGenerator import create_camera_for_card | ||
|
||
BoundingBox = namedtuple('BoundingBox', ['width', 'height']) | ||
RENDER_PURPOSE_MAP = { | ||
"default": UsdGeom.Tokens.default_, | ||
"render": UsdGeom.Tokens.render, | ||
"proxy": UsdGeom.Tokens.proxy, | ||
"guide": UsdGeom.Tokens.guide | ||
} | ||
|
||
def setup_cameras(subject_stage, usd_file, cards, dome_light, render_purposes): | ||
camera_stage = create_camera_stage() | ||
create_cameras(camera_stage, subject_stage, cards, render_purposes) | ||
sublayer_subject(camera_stage, usd_file) | ||
|
||
if dome_light: | ||
add_domelight(camera_stage, dome_light) | ||
|
||
camera_stage.Save() | ||
|
||
def create_camera_stage(): | ||
stage = Usd.Stage.CreateNew('cameras.usda') | ||
stage.SetMetadata('metersPerUnit', 0.01) | ||
|
||
return stage | ||
|
||
def create_cameras(camera_stage, subject_stage, cards, render_purposes): | ||
render_purpose_tokens = convert_render_purposes_to_tokens(render_purposes) | ||
stage_bounding_box = get_bounding_box(subject_stage, render_purpose_tokens) | ||
min_bound = stage_bounding_box.GetMin() | ||
max_bound = stage_bounding_box.GetMax() | ||
subject_center = (min_bound + max_bound) / 2.0 | ||
|
||
for card in cards: | ||
card_width = (max_bound[card.horizontalIndex] - min_bound[card.horizontalIndex]) * 10 | ||
card_height = (max_bound[card.verticalIndex] - min_bound[card.verticalIndex]) * 10 | ||
card_bounding_box = BoundingBox(card_width, card_height) | ||
|
||
faceTranslationValue = max_bound[card.translationIndex] if card.sign > 0 else min_bound[card.translationIndex] | ||
center_of_card_face = Gf.Vec3d(subject_center[0], subject_center[1], subject_center[2]) | ||
center_of_card_face[card.translationIndex] = faceTranslationValue | ||
|
||
camera_view_axis_distance = card_height = (max_bound[card.translationIndex] - min_bound[card.translationIndex]) | ||
|
||
create_camera_for_card(card, camera_stage, center_of_card_face, card_bounding_box, camera_view_axis_distance) | ||
|
||
def convert_render_purposes_to_tokens(render_purposes): | ||
return [RENDER_PURPOSE_MAP[key] for key in render_purposes.split(',')] | ||
|
||
def get_bounding_box(subject_stage, render_purpose_tokens): | ||
bboxCache = UsdGeom.BBoxCache(Usd.TimeCode(0.0), render_purpose_tokens) | ||
root = subject_stage.GetPseudoRoot() | ||
return bboxCache.ComputeWorldBound(root).GetBox() | ||
|
||
|
||
def sublayer_subject(camera_stage, input_file): | ||
camera_stage.GetRootLayer().subLayerPaths = [input_file] | ||
|
||
def add_domelight(camera_stage, dome_light): | ||
UsdLux.DomeLight.Define(camera_stage, '/CardGenerator/DomeLight') | ||
domeLight = UsdLux.DomeLight(camera_stage.GetPrimAtPath('/CardGenerator/DomeLight')) | ||
domeLight.CreateTextureFileAttr().Set(dome_light) | ||
domeLight.CreateTextureFormatAttr().Set("latlong") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
#!/usr/bin/env python3 | ||
|
||
FOCAL_LENGTH = 50 | ||
SENSOR_SIZE = 24 | ||
FOCUS_DISTANCE = 168.60936 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
#!/usr/bin/env python3 | ||
|
||
import subprocess | ||
import os | ||
import sys | ||
from pathlib import Path | ||
from concurrent.futures import ThreadPoolExecutor | ||
|
||
CARDS_FOLDER_NAME = "cards" | ||
|
||
|
||
def take_snapshots(cards, output_extension, image_width): | ||
images = [] | ||
renderer = get_renderer() | ||
IMAGE_WIDTH = image_width | ||
|
||
def task(card): | ||
cardName = card.usdFileName + "_" + card.name | ||
image_name = create_image_filename(cardName, output_extension, card.parentPath) | ||
cmd = ['usdrecord','--camera', card.name, '--imageWidth', str(IMAGE_WIDTH), '--renderer', renderer, 'cameras.usda', image_name] | ||
run_os_specific_command(cmd) | ||
return image_name | ||
|
||
with ThreadPoolExecutor() as executor: | ||
images = list(executor.map(task, cards)) | ||
|
||
os.remove("cameras.usda") | ||
return images | ||
|
||
def create_image_filename(card_name, extension, parent_path): | ||
if CARDS_FOLDER_NAME: | ||
cards_folder_dir = Path().joinpath(parent_path, CARDS_FOLDER_NAME) | ||
else: | ||
cards_folder_dir = parent_path | ||
cards_folder_dir.mkdir(parents=True, exist_ok=True) | ||
return str(Path().joinpath(cards_folder_dir, card_name).with_suffix("." + extension)).replace("\\", "/") | ||
|
||
def get_renderer(): | ||
if os.name == 'nt': | ||
print("windows default renderer GL being used...") | ||
return "GL" | ||
else: | ||
if sys.platform == 'darwin': | ||
print("macOS default renderer Metal being used...") | ||
return 'Metal' | ||
else: | ||
print("linux default renderer GL being used...") | ||
return 'GL' | ||
|
||
def run_os_specific_command(cmd): | ||
if os.name == 'nt': | ||
subprocess.run(cmd, check=True, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) | ||
else: | ||
if sys.platform == 'darwin': | ||
subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) | ||
else: | ||
subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) |
Empty file.
20 changes: 20 additions & 0 deletions
20
scripts/cards-generator/src/camera/utils/distanceCalculator.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import math | ||
|
||
def get_distance_to_frame_subject(bounding_box, sensor_size, focal_length): | ||
distance_to_capture_horizontal = calculate_field_of_view_distance(sensor_size, bounding_box.width, focal_length) | ||
distance_to_capture_vertical = calculate_field_of_view_distance(sensor_size, bounding_box.height, focal_length) | ||
|
||
return max(distance_to_capture_horizontal, distance_to_capture_vertical) | ||
|
||
def calculate_field_of_view_distance(sensor_size, object_size, focal_length): | ||
return calculate_camera_distance(object_size, calculate_field_of_view(sensor_size, focal_length)) | ||
|
||
def calculate_field_of_view(sensor_size, focal_length): | ||
# Focal length and sensor size should be in the same units (e.g., mm) | ||
field_of_view = 2 * math.atan(sensor_size / (2 * focal_length)) | ||
return field_of_view | ||
|
||
def calculate_camera_distance(subject_size, field_of_view): | ||
# Subject size and field of view should be in the same units (e.g., mm and degrees) | ||
distance = (subject_size / 2) / math.tan(field_of_view / 2) | ||
return distance |
Oops, something went wrong.