Skip to content

Commit

Permalink
Houdini model and lighting integration (#56)
Browse files Browse the repository at this point in the history
* add houdini integration

- add UI button "Open in Houdini" in the metadata section
- launch the Update.hipnc template by default

* open the correct houdini template based on new/existing assets

* copy template to asset folder if it doesn't exist

* add lighting integration

* add houdini lighting integration

* Delete Pipfile

* Add "DCC Integrations" UI

---------

Co-authored-by: Linda Zhu <[email protected]>
Co-authored-by: Thomas Shaw <[email protected]>
  • Loading branch information
3 people authored Apr 30, 2024
1 parent 6ac3ee8 commit 5319c74
Show file tree
Hide file tree
Showing 11 changed files with 312 additions and 7 deletions.
1 change: 1 addition & 0 deletions dcc/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
houdini/backup
Binary file added dcc/houdini/CreateNew.hipnc
Binary file not shown.
Binary file added dcc/houdini/KarmaRendering.hipnc
Binary file not shown.
Binary file added dcc/houdini/Update.hipnc
Binary file not shown.
76 changes: 76 additions & 0 deletions dcc/houdini/launchTemplate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# hou is the Houdini module, which will only be available after Houdini launches
import hou
import argparse
import os

def is_class_asset_structure(source_folder, asset_name):
is_root_existing = os.path.exists(os.path.join(source_folder, f'{asset_name}.usda'))
# only check if one LOD exists
is_LOD_existing = os.path.exists(os.path.join(source_folder, f'{asset_name}LOD0.usda'))
is_geometry_existing = os.path.exists(os.path.join(source_folder, f'{asset_name}_model.usda'))
is_material_existing = os.path.exists(os.path.join(source_folder, f'{asset_name}_material.usda'))
return is_root_existing and is_LOD_existing and is_geometry_existing and is_material_existing

def is_houdini_asset_structure(source_folder):
is_root_existing = os.path.exists(os.path.join(source_folder, 'root.usda'))
is_geometry_dir = os.path.isdir(os.path.join(source_folder, 'Geometry'))
is_material_dir = os.path.isdir(os.path.join(source_folder, 'Material'))
return is_root_existing and is_material_dir and is_geometry_dir

if __name__ == "__main__":
# command line flags
parser = argparse.ArgumentParser()
parser.add_argument("-a", "--assetname", required=True, help="Enter the asset name")
parser.add_argument("-o", "--original", required=True, help="Enter the original asset directory to read your old asset USDs from")
parser.add_argument("-n", "--new", help="Enter the new asset directory you want your asset USDs to output to")
args = parser.parse_args()

assetname = args.assetname
source_folder = args.original

if os.path.isdir(source_folder):
# launching Update.hipnc template
# assets in old hw10 structure
if is_class_asset_structure(source_folder, assetname):
print('Old asset structure')

# get and set houdini node parameters
class_structure_node = hou.node("stage/load_class_asset_update")
asset_name = class_structure_node.parm("asset_name")
original_asset_directory = class_structure_node.parm("original_asset_directory")
new_asset_directory = class_structure_node.parm("new_asset_directory")

asset_name.set(assetname)
original_asset_directory.set(source_folder)
new_asset_directory.set(args.new)

# set this node as the current selected and display output in viewport
class_structure_node.setCurrent(True, True)
class_structure_node.setDisplayFlag(True)

# assets in new houdini structure
elif is_houdini_asset_structure(source_folder):
print('New houdini asset structure')

new_structure_node = hou.node("stage/load_new_asset_update")
asset_name = new_structure_node.parm("asset_name")
asset_root_directory = new_structure_node.parm("asset_root_directory")
asset_name.set(assetname)
asset_root_directory.set(source_folder)

new_structure_node.setCurrent(True, True)
new_structure_node.setDisplayFlag(True)

# launching CreateNew.hipnc template
else:
print('Creating new asset')

create_asset_node = hou.node("stage/create_new_asset")
asset_name = create_asset_node.parm("asset_name")
asset_root_directory = create_asset_node.parm("temp_asset_directory")
# todo: specify LOD paths
asset_name.set(assetname)
asset_root_directory.set(source_folder)

create_asset_node.setCurrent(True, True)
create_asset_node.setDisplayFlag(True)
30 changes: 30 additions & 0 deletions dcc/houdini/quietRender.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# hou is the Houdini module, which will only be available after Houdini launches
import hou
import argparse
import os

if __name__ == "__main__":
# command line flags
parser = argparse.ArgumentParser()
parser.add_argument("-a", "--assetpath", required=True, help="Enter the asset USD full path")
parser.add_argument("-o", "--outputpath", required=True, help="Enter the output render full path")
args = parser.parse_args()

# NOTE: Must have the lighting .hip in the same directory as this script
currentDir = os.path.dirname(os.path.realpath(__file__))
hou.hipFile.load(os.path.join(currentDir, "KarmaRendering.hipnc"))

# retrieve node parameters/objects
render_node = hou.node("stage/render_USD_geom")
asset_path = render_node.parm("asset_path")
output_path = render_node.parm("render_output_path")
save_render_button = render_node.parm("save_render_button")

# set input paths
asset_path.set(args.assetpath)
output_path.set(args.outputpath)

# execute ROP render to disk
save_render_button.pressButton()

print(f"Render image saved to {args.outputpath}")
130 changes: 128 additions & 2 deletions frontend/src/main/lib/local-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import archiver from 'archiver';
import { app, shell } from 'electron';
import extract from 'extract-zip';
import { createWriteStream } from 'fs';
import { existsSync } from 'node:fs';
import { existsSync, copyFile } from 'node:fs';
import fsPromises from 'node:fs/promises';
import path from 'node:path';
import { hashElement } from 'folder-hash'
import { hashElement } from 'folder-hash';
import process from 'process';
import fs from 'fs';

import { DownloadedEntry, Version } from '../../types/ipc';
import { getAuthToken } from './authentication';
Expand Down Expand Up @@ -277,3 +279,127 @@ export async function unsyncAsset(asset_id: string) {

store.set('downloadedAssetVersions', newVersions);
}

function getCommandLine() {
switch (process.platform) {
case 'darwin' : return 'open ';
case 'win32' : return 'start ';
default : return 'xdg-open';
}
}

/**
* Locates the downloaded asset folder and launches the respective Houdini template
*/
const houdini_src = '../dcc/houdini/';

export async function openHoudini(asset_id: string) {
const stored = getDownloadedVersionByID(asset_id);
if (!stored) return;

const downloadsFullpath = path.join(getDownloadFolder(), stored.folderName);
const assetName = stored.folderName.split('_')[0];

// NOTE: Must have user set the $HFS system environment variable to their houdini installation path prior to using this feature
if (!process.env.HFS) return;
const houdiniCmd = path.join(process.env.HFS, '/bin/houdini');

const { spawn, exec } = require("child_process");

// If there's an existing Houdini file, open it.
const destination = path.join(downloadsFullpath, `${assetName}.hipnc`);
if (existsSync(destination)) {
exec(getCommandLine() + destination);
console.log(`Launching the existing Houdini file for ${asset_id}...`);
}
// Otherwise, load asset in a new template.
else {
const existsUsdOld = existsSync(path.join(downloadsFullpath, 'root.usda'));
const existsUsdNew = existsSync(path.join(downloadsFullpath, `${assetName}.usda`));
const houdiniTemplate = (!existsUsdOld && !existsUsdNew) ? 'CreateNew.hipnc' : 'Update.hipnc';
const templateFullpath = path.join(process.cwd(), `${houdini_src}${houdiniTemplate}`);

// Copy template to asset's folder so we don't always edit on the same file
copyFile(templateFullpath, destination, (err) => {
if (err) throw err;
console.log(`${houdiniTemplate} was copied to ${destination}`);
});

const pythonScript = path.join(process.cwd(), `${houdini_src}/launchTemplate.py`);

// Launch houdini with a python session attached
const bat = spawn(houdiniCmd, [
destination, // Argument for cmd to carry out the specified file
pythonScript, // Path to your script
"-a", // First argument
assetName, // n-th argument
"-o",
downloadsFullpath,
"-n",
downloadsFullpath
], {
shell: true,
});

bat.stdout.on("data", (data) => {
console.log(data.toString());
});

bat.stderr.on("data", (err) => {
console.log(err.toString());
});
}
console.log(`Launching Houdini template for ${asset_id}...`);
}

/**
* Locates the downloaded asset folder and launches the Houdini lighting template headlessly to output a render image
*/
export async function quietRenderHoudini(asset_id: string) {
const stored = getDownloadedVersionByID(asset_id);
if (!stored) return;

const downloadsFullpath = path.join(getDownloadFolder(), stored.folderName);
const assetName = stored.folderName.split('_')[0];

// NOTE: Must have user set the $HFS system environment variable to their houdini installation path prior to using this feature
if (!process.env.HFS) return;
const houdiniHython = path.join(process.env.HFS, '/bin/hython');

const { spawn } = require("child_process");

const pythonScript = path.join(process.cwd(), `${houdini_src}/quietRender.py`);

// locate the asset USD file based on different asset structures
const usdNewFullpath = path.join(downloadsFullpath, 'root.usda');
const usdOldFullpath = path.join(downloadsFullpath, `${assetName}.usda`);
let assetFullpath;
if (existsSync(usdOldFullpath)) {
assetFullpath = usdOldFullpath;
}
else if (existsSync(usdNewFullpath)) {
assetFullpath = usdNewFullpath;
}
else {
console.log("No correct asset USD to render!");
return;
}

const bat = spawn(houdiniHython, [
pythonScript, // Argument for cmd to carry out the specified file
"-a", // First argument
assetFullpath, // n-th argument
"-o",
downloadsFullpath
], {
shell: true,
});

bat.stdout.on("data", (data) => {
console.log(data.toString());
});

bat.stderr.on("data", (err) => {
console.log(err.toString());
});
}
10 changes: 10 additions & 0 deletions frontend/src/main/message-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
ifFilesChanged,
openFolder,
unsyncAsset,
openHoudini,
quietRenderHoudini,
} from './lib/local-assets';

// Types for these can be found in `src/types/ipc.d.ts`
Expand Down Expand Up @@ -66,6 +68,14 @@ const messageHandlers: MessageHandlers = {
return { ok: false };
}
},
'assets:open-houdini': async (_, { asset_id }) => {
await openHoudini(asset_id);
return { ok: true };
},
'assets:quiet-render-houdini': async (_, { asset_id }) => {
await quietRenderHoudini(asset_id);
return { ok: true };
},
};

export default messageHandlers;
60 changes: 57 additions & 3 deletions frontend/src/renderer/src/components/metadata.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { useMemo, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { CiEdit } from 'react-icons/ci';
import { MdArchive, MdFolderOpen, MdLogin, MdSync, MdSyncDisabled } from 'react-icons/md';
import {
MdArchive,
MdCamera,
MdFolderOpen,
MdLaunch,
MdLogin,
MdSync,
MdSyncDisabled,
} from 'react-icons/md';
import { SiHoudini } from 'react-icons/si';
import { Link, useNavigate } from 'react-router-dom';

import { useSelectedAsset } from '@renderer/hooks/use-asset-select';
Expand Down Expand Up @@ -142,6 +151,28 @@ export default function Metadata() {
syncAsset({ uuid: asset.id, asset_name: asset.asset_name, semver });
};

const onOpenHoudiniClick = async () => {
if (!asset) return;

const downloaded = downloadedVersions?.find(({ asset_id }) => asset_id === asset.id);
if (!downloaded) return;

await window.api.ipc('assets:open-houdini', {
asset_id: asset.id,
});
};

const onRenderHoudiniClick = async () => {
if (!asset) return;

const downloaded = downloadedVersions?.find(({ asset_id }) => asset_id === asset.id);
if (!downloaded) return;

await window.api.ipc('assets:quiet-render-houdini', {
asset_id: asset.id,
});
};

if (!asset) {
return (
<div className="flex h-full flex-col px-6 py-4">
Expand Down Expand Up @@ -327,12 +358,35 @@ export default function Metadata() {
{/* Update Asset Button */}
{isDownloaded && (
<>
<label className="select-none text-xs text-base-content/70">DCC Integrations</label>
<div className="mt-1 select-none overflow-scroll rounded-box px-3 py-2 ring-1 ring-base-content/20">
<div className="flex items-center">
<div className="mr-auto flex items-center gap-2 text-xs font-medium">
<SiHoudini />
Houdini
</div>
<button
className="btn btn-ghost btn-xs flex flex-row flex-nowrap items-center justify-start gap-2 font-normal"
onClick={onOpenHoudiniClick}
>
<MdLaunch />
Open
</button>
<button
className="btn btn-ghost btn-xs flex flex-row flex-nowrap items-center justify-start gap-2 font-normal"
onClick={onRenderHoudiniClick}
>
<MdCamera />
Render
</button>
</div>
</div>
<button
className="btn btn-ghost btn-sm flex w-full flex-row flex-nowrap items-center justify-start gap-2 text-sm font-normal"
className="btn btn-ghost btn-sm mt-6 flex w-full flex-row flex-nowrap items-center justify-start gap-2 text-sm font-normal"
onClick={onOpenFolderClick}
>
<MdFolderOpen />
Open
Open Folder
</button>
<Link
className="btn btn-outline btn-primary mt-2 w-full justify-start"
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/renderer/src/routes/home-view.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { AnimatePresence } from 'framer-motion';
import { Outlet } from 'react-router-dom';

import AssetList from '@renderer/components/asset-list';
import { useAssetSelectStore } from '@renderer/hooks/use-asset-select';
import Navbar from '../components/layout/navbar';
import Metadata from '../components/metadata';
import { AnimatePresence } from 'framer-motion';

function HomeView(): JSX.Element {
const setSelectedAssetId = useAssetSelectStore((state) => state.setSelected);
Expand All @@ -14,7 +14,7 @@ function HomeView(): JSX.Element {
<div className="grid h-screen w-screen min-w-[400px] grid-rows-[min-content_1fr] overflow-clip">
<Navbar />
{/* with explorer panel: grid-cols-[minmax(160px,calc(min(25%,320px)))_minmax(0,1fr)_minmax(160px,calc(min(25%,320px)))] */}
<div className="grid grid-cols-[minmax(0,1fr)_minmax(240px,calc(min(30%,360px)))]">
<div className="grid grid-cols-[minmax(0,1fr)_minmax(300px,calc(min(30%,360px)))]">
{/* TODO: re-add this asset explorer panel if we have functionality */}
{/* <div className="relative border-r-[1px] border-base-content/20">
<div className="absolute inset-0 px-6 py-4">
Expand Down
Loading

0 comments on commit 5319c74

Please sign in to comment.