Skip to content

Commit 5319c74

Browse files
Houdini model and lighting integration (#56)
* 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 <lindadaism@users.noreply.github.com> Co-authored-by: Thomas Shaw <printer.83mph@gmail.com>
1 parent 6ac3ee8 commit 5319c74

File tree

11 files changed

+312
-7
lines changed

11 files changed

+312
-7
lines changed

dcc/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
houdini/backup

dcc/houdini/CreateNew.hipnc

473 KB
Binary file not shown.

dcc/houdini/KarmaRendering.hipnc

869 KB
Binary file not shown.

dcc/houdini/Update.hipnc

872 KB
Binary file not shown.

dcc/houdini/launchTemplate.py

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# hou is the Houdini module, which will only be available after Houdini launches
2+
import hou
3+
import argparse
4+
import os
5+
6+
def is_class_asset_structure(source_folder, asset_name):
7+
is_root_existing = os.path.exists(os.path.join(source_folder, f'{asset_name}.usda'))
8+
# only check if one LOD exists
9+
is_LOD_existing = os.path.exists(os.path.join(source_folder, f'{asset_name}LOD0.usda'))
10+
is_geometry_existing = os.path.exists(os.path.join(source_folder, f'{asset_name}_model.usda'))
11+
is_material_existing = os.path.exists(os.path.join(source_folder, f'{asset_name}_material.usda'))
12+
return is_root_existing and is_LOD_existing and is_geometry_existing and is_material_existing
13+
14+
def is_houdini_asset_structure(source_folder):
15+
is_root_existing = os.path.exists(os.path.join(source_folder, 'root.usda'))
16+
is_geometry_dir = os.path.isdir(os.path.join(source_folder, 'Geometry'))
17+
is_material_dir = os.path.isdir(os.path.join(source_folder, 'Material'))
18+
return is_root_existing and is_material_dir and is_geometry_dir
19+
20+
if __name__ == "__main__":
21+
# command line flags
22+
parser = argparse.ArgumentParser()
23+
parser.add_argument("-a", "--assetname", required=True, help="Enter the asset name")
24+
parser.add_argument("-o", "--original", required=True, help="Enter the original asset directory to read your old asset USDs from")
25+
parser.add_argument("-n", "--new", help="Enter the new asset directory you want your asset USDs to output to")
26+
args = parser.parse_args()
27+
28+
assetname = args.assetname
29+
source_folder = args.original
30+
31+
if os.path.isdir(source_folder):
32+
# launching Update.hipnc template
33+
# assets in old hw10 structure
34+
if is_class_asset_structure(source_folder, assetname):
35+
print('Old asset structure')
36+
37+
# get and set houdini node parameters
38+
class_structure_node = hou.node("stage/load_class_asset_update")
39+
asset_name = class_structure_node.parm("asset_name")
40+
original_asset_directory = class_structure_node.parm("original_asset_directory")
41+
new_asset_directory = class_structure_node.parm("new_asset_directory")
42+
43+
asset_name.set(assetname)
44+
original_asset_directory.set(source_folder)
45+
new_asset_directory.set(args.new)
46+
47+
# set this node as the current selected and display output in viewport
48+
class_structure_node.setCurrent(True, True)
49+
class_structure_node.setDisplayFlag(True)
50+
51+
# assets in new houdini structure
52+
elif is_houdini_asset_structure(source_folder):
53+
print('New houdini asset structure')
54+
55+
new_structure_node = hou.node("stage/load_new_asset_update")
56+
asset_name = new_structure_node.parm("asset_name")
57+
asset_root_directory = new_structure_node.parm("asset_root_directory")
58+
asset_name.set(assetname)
59+
asset_root_directory.set(source_folder)
60+
61+
new_structure_node.setCurrent(True, True)
62+
new_structure_node.setDisplayFlag(True)
63+
64+
# launching CreateNew.hipnc template
65+
else:
66+
print('Creating new asset')
67+
68+
create_asset_node = hou.node("stage/create_new_asset")
69+
asset_name = create_asset_node.parm("asset_name")
70+
asset_root_directory = create_asset_node.parm("temp_asset_directory")
71+
# todo: specify LOD paths
72+
asset_name.set(assetname)
73+
asset_root_directory.set(source_folder)
74+
75+
create_asset_node.setCurrent(True, True)
76+
create_asset_node.setDisplayFlag(True)

dcc/houdini/quietRender.py

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# hou is the Houdini module, which will only be available after Houdini launches
2+
import hou
3+
import argparse
4+
import os
5+
6+
if __name__ == "__main__":
7+
# command line flags
8+
parser = argparse.ArgumentParser()
9+
parser.add_argument("-a", "--assetpath", required=True, help="Enter the asset USD full path")
10+
parser.add_argument("-o", "--outputpath", required=True, help="Enter the output render full path")
11+
args = parser.parse_args()
12+
13+
# NOTE: Must have the lighting .hip in the same directory as this script
14+
currentDir = os.path.dirname(os.path.realpath(__file__))
15+
hou.hipFile.load(os.path.join(currentDir, "KarmaRendering.hipnc"))
16+
17+
# retrieve node parameters/objects
18+
render_node = hou.node("stage/render_USD_geom")
19+
asset_path = render_node.parm("asset_path")
20+
output_path = render_node.parm("render_output_path")
21+
save_render_button = render_node.parm("save_render_button")
22+
23+
# set input paths
24+
asset_path.set(args.assetpath)
25+
output_path.set(args.outputpath)
26+
27+
# execute ROP render to disk
28+
save_render_button.pressButton()
29+
30+
print(f"Render image saved to {args.outputpath}")

frontend/src/main/lib/local-assets.ts

+128-2
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import archiver from 'archiver';
22
import { app, shell } from 'electron';
33
import extract from 'extract-zip';
44
import { createWriteStream } from 'fs';
5-
import { existsSync } from 'node:fs';
5+
import { existsSync, copyFile } from 'node:fs';
66
import fsPromises from 'node:fs/promises';
77
import path from 'node:path';
8-
import { hashElement } from 'folder-hash'
8+
import { hashElement } from 'folder-hash';
9+
import process from 'process';
10+
import fs from 'fs';
911

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

278280
store.set('downloadedAssetVersions', newVersions);
279281
}
282+
283+
function getCommandLine() {
284+
switch (process.platform) {
285+
case 'darwin' : return 'open ';
286+
case 'win32' : return 'start ';
287+
default : return 'xdg-open';
288+
}
289+
}
290+
291+
/**
292+
* Locates the downloaded asset folder and launches the respective Houdini template
293+
*/
294+
const houdini_src = '../dcc/houdini/';
295+
296+
export async function openHoudini(asset_id: string) {
297+
const stored = getDownloadedVersionByID(asset_id);
298+
if (!stored) return;
299+
300+
const downloadsFullpath = path.join(getDownloadFolder(), stored.folderName);
301+
const assetName = stored.folderName.split('_')[0];
302+
303+
// NOTE: Must have user set the $HFS system environment variable to their houdini installation path prior to using this feature
304+
if (!process.env.HFS) return;
305+
const houdiniCmd = path.join(process.env.HFS, '/bin/houdini');
306+
307+
const { spawn, exec } = require("child_process");
308+
309+
// If there's an existing Houdini file, open it.
310+
const destination = path.join(downloadsFullpath, `${assetName}.hipnc`);
311+
if (existsSync(destination)) {
312+
exec(getCommandLine() + destination);
313+
console.log(`Launching the existing Houdini file for ${asset_id}...`);
314+
}
315+
// Otherwise, load asset in a new template.
316+
else {
317+
const existsUsdOld = existsSync(path.join(downloadsFullpath, 'root.usda'));
318+
const existsUsdNew = existsSync(path.join(downloadsFullpath, `${assetName}.usda`));
319+
const houdiniTemplate = (!existsUsdOld && !existsUsdNew) ? 'CreateNew.hipnc' : 'Update.hipnc';
320+
const templateFullpath = path.join(process.cwd(), `${houdini_src}${houdiniTemplate}`);
321+
322+
// Copy template to asset's folder so we don't always edit on the same file
323+
copyFile(templateFullpath, destination, (err) => {
324+
if (err) throw err;
325+
console.log(`${houdiniTemplate} was copied to ${destination}`);
326+
});
327+
328+
const pythonScript = path.join(process.cwd(), `${houdini_src}/launchTemplate.py`);
329+
330+
// Launch houdini with a python session attached
331+
const bat = spawn(houdiniCmd, [
332+
destination, // Argument for cmd to carry out the specified file
333+
pythonScript, // Path to your script
334+
"-a", // First argument
335+
assetName, // n-th argument
336+
"-o",
337+
downloadsFullpath,
338+
"-n",
339+
downloadsFullpath
340+
], {
341+
shell: true,
342+
});
343+
344+
bat.stdout.on("data", (data) => {
345+
console.log(data.toString());
346+
});
347+
348+
bat.stderr.on("data", (err) => {
349+
console.log(err.toString());
350+
});
351+
}
352+
console.log(`Launching Houdini template for ${asset_id}...`);
353+
}
354+
355+
/**
356+
* Locates the downloaded asset folder and launches the Houdini lighting template headlessly to output a render image
357+
*/
358+
export async function quietRenderHoudini(asset_id: string) {
359+
const stored = getDownloadedVersionByID(asset_id);
360+
if (!stored) return;
361+
362+
const downloadsFullpath = path.join(getDownloadFolder(), stored.folderName);
363+
const assetName = stored.folderName.split('_')[0];
364+
365+
// NOTE: Must have user set the $HFS system environment variable to their houdini installation path prior to using this feature
366+
if (!process.env.HFS) return;
367+
const houdiniHython = path.join(process.env.HFS, '/bin/hython');
368+
369+
const { spawn } = require("child_process");
370+
371+
const pythonScript = path.join(process.cwd(), `${houdini_src}/quietRender.py`);
372+
373+
// locate the asset USD file based on different asset structures
374+
const usdNewFullpath = path.join(downloadsFullpath, 'root.usda');
375+
const usdOldFullpath = path.join(downloadsFullpath, `${assetName}.usda`);
376+
let assetFullpath;
377+
if (existsSync(usdOldFullpath)) {
378+
assetFullpath = usdOldFullpath;
379+
}
380+
else if (existsSync(usdNewFullpath)) {
381+
assetFullpath = usdNewFullpath;
382+
}
383+
else {
384+
console.log("No correct asset USD to render!");
385+
return;
386+
}
387+
388+
const bat = spawn(houdiniHython, [
389+
pythonScript, // Argument for cmd to carry out the specified file
390+
"-a", // First argument
391+
assetFullpath, // n-th argument
392+
"-o",
393+
downloadsFullpath
394+
], {
395+
shell: true,
396+
});
397+
398+
bat.stdout.on("data", (data) => {
399+
console.log(data.toString());
400+
});
401+
402+
bat.stderr.on("data", (err) => {
403+
console.log(err.toString());
404+
});
405+
}

frontend/src/main/message-handlers.ts

+10
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
ifFilesChanged,
99
openFolder,
1010
unsyncAsset,
11+
openHoudini,
12+
quietRenderHoudini,
1113
} from './lib/local-assets';
1214

1315
// Types for these can be found in `src/types/ipc.d.ts`
@@ -66,6 +68,14 @@ const messageHandlers: MessageHandlers = {
6668
return { ok: false };
6769
}
6870
},
71+
'assets:open-houdini': async (_, { asset_id }) => {
72+
await openHoudini(asset_id);
73+
return { ok: true };
74+
},
75+
'assets:quiet-render-houdini': async (_, { asset_id }) => {
76+
await quietRenderHoudini(asset_id);
77+
return { ok: true };
78+
},
6979
};
7080

7181
export default messageHandlers;

frontend/src/renderer/src/components/metadata.tsx

+57-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
import { useMemo, useState } from 'react';
22
import { Controller, useForm } from 'react-hook-form';
33
import { CiEdit } from 'react-icons/ci';
4-
import { MdArchive, MdFolderOpen, MdLogin, MdSync, MdSyncDisabled } from 'react-icons/md';
4+
import {
5+
MdArchive,
6+
MdCamera,
7+
MdFolderOpen,
8+
MdLaunch,
9+
MdLogin,
10+
MdSync,
11+
MdSyncDisabled,
12+
} from 'react-icons/md';
13+
import { SiHoudini } from 'react-icons/si';
514
import { Link, useNavigate } from 'react-router-dom';
615

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

154+
const onOpenHoudiniClick = async () => {
155+
if (!asset) return;
156+
157+
const downloaded = downloadedVersions?.find(({ asset_id }) => asset_id === asset.id);
158+
if (!downloaded) return;
159+
160+
await window.api.ipc('assets:open-houdini', {
161+
asset_id: asset.id,
162+
});
163+
};
164+
165+
const onRenderHoudiniClick = async () => {
166+
if (!asset) return;
167+
168+
const downloaded = downloadedVersions?.find(({ asset_id }) => asset_id === asset.id);
169+
if (!downloaded) return;
170+
171+
await window.api.ipc('assets:quiet-render-houdini', {
172+
asset_id: asset.id,
173+
});
174+
};
175+
145176
if (!asset) {
146177
return (
147178
<div className="flex h-full flex-col px-6 py-4">
@@ -327,12 +358,35 @@ export default function Metadata() {
327358
{/* Update Asset Button */}
328359
{isDownloaded && (
329360
<>
361+
<label className="select-none text-xs text-base-content/70">DCC Integrations</label>
362+
<div className="mt-1 select-none overflow-scroll rounded-box px-3 py-2 ring-1 ring-base-content/20">
363+
<div className="flex items-center">
364+
<div className="mr-auto flex items-center gap-2 text-xs font-medium">
365+
<SiHoudini />
366+
Houdini
367+
</div>
368+
<button
369+
className="btn btn-ghost btn-xs flex flex-row flex-nowrap items-center justify-start gap-2 font-normal"
370+
onClick={onOpenHoudiniClick}
371+
>
372+
<MdLaunch />
373+
Open
374+
</button>
375+
<button
376+
className="btn btn-ghost btn-xs flex flex-row flex-nowrap items-center justify-start gap-2 font-normal"
377+
onClick={onRenderHoudiniClick}
378+
>
379+
<MdCamera />
380+
Render
381+
</button>
382+
</div>
383+
</div>
330384
<button
331-
className="btn btn-ghost btn-sm flex w-full flex-row flex-nowrap items-center justify-start gap-2 text-sm font-normal"
385+
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"
332386
onClick={onOpenFolderClick}
333387
>
334388
<MdFolderOpen />
335-
Open
389+
Open Folder
336390
</button>
337391
<Link
338392
className="btn btn-outline btn-primary mt-2 w-full justify-start"

frontend/src/renderer/src/routes/home-view.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1+
import { AnimatePresence } from 'framer-motion';
12
import { Outlet } from 'react-router-dom';
23

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

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

0 commit comments

Comments
 (0)