Bases: HoudiniInstancePlugin
Collect all assets introduced by the look.
We are looking to collect e.g. all texture resources so we can transfer them with the publish and write then to the publish location.
If possible, we'll also try to identify the colorspace of the asset.
Source code in client/ayon_houdini/plugins/publish/collect_usd_look_assets.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209 | class CollectUsdLookAssets(plugin.HoudiniInstancePlugin):
"""Collect all assets introduced by the look.
We are looking to collect e.g. all texture resources so we can transfer
them with the publish and write then to the publish location.
If possible, we'll also try to identify the colorspace of the asset.
"""
# TODO: Implement $F frame support (per frame values)
# TODO: If input image is already a published texture or resource than
# preferably we'd keep the link in-tact and NOT update it. We can just
# start ignoring AYON URIs
label = "Collect USD Look Assets"
order = pyblish.api.CollectorOrder
hosts = ["houdini"]
families = ["look"]
exclude_suffixes = [".usd", ".usda", ".usdc", ".usdz", ".abc", ".vbd"]
def process(self, instance):
# Get Sdf.Layers from "Collect ROP Sdf Layers and USD Stage" plug-in
layers = instance.data.get("layers")
if layers is None:
self.log.warning(f"No USD layers found on instance: {instance}")
return
layers: List[Sdf.Layer]
instance_resources = self.get_layer_assets(layers)
# Define a relative asset remapping for the USD Extractor so that
# any textures are remapped to their 'relative' publish path.
# All textures will be in a relative `./resources/` folder
remap = {}
for resource in instance_resources:
source = resource.source
name = os.path.basename(source)
remap[os.path.normpath(source)] = f"./resources/{name}"
instance.data["assetRemap"] = remap
# Store resources on instance
resources = instance.data.setdefault("resources", [])
for resource in instance_resources:
resources.append(dataclasses.asdict(resource))
# Log all collected textures
# Note: It is fine for a single texture to be included more than once
# where even one of them does not have a color space set, but the other
# does. For example, there may be a USD UV Texture just for a GL
# preview material which does not specify an OCIO color
# space.
all_files = []
for resource in instance_resources:
all_files.append(f"{resource.attribute}:")
for filepath in resource.files:
if resource.color_space:
file_label = f"- {filepath} ({resource.color_space})"
else:
file_label = f"- {filepath}"
all_files.append(file_label)
self.log.info(
"Collected assets:\n{}".format(
"\n".join(all_files)
)
)
def get_layer_assets(self, layers: List[Sdf.Layer]) -> List[Resource]:
# TODO: Correctly resolve paths using Asset Resolver.
# Preferably this would use one cached
# resolver context to optimize the path resolving.
# TODO: Fix for timesamples - if timesamples, then `.default` might
# not be authored on the spec
resources: List[Resource] = list()
for layer in layers:
for path in get_layer_property_paths(layer):
spec = layer.GetAttributeAtPath(path)
if not spec:
continue
if spec.typeName != "asset":
continue
asset: Sdf.AssetPath = spec.default
base, ext = os.path.splitext(asset.path)
if ext in self.exclude_suffixes:
continue
filepath = asset.path.replace("\\", "/")
# Expand <UDIM> to all files of the available files on disk
# TODO: Add support for `<TILE>`
# TODO: Add support for `<ATTR:name INDEX:name DEFAULT:value>`
if "<UDIM>" in filepath.upper():
pattern = re.sub(
r"<UDIM>",
# UDIM is always four digits
"[0-9]" * 4,
filepath,
flags=re.IGNORECASE
)
files = glob.glob(pattern)
else:
# Single file
files = [filepath]
# Detect the colorspace of the input asset property
colorspace = self.get_colorspace(spec)
resource = Resource(
attribute=path.pathString,
source=asset.path,
files=files,
color_space=colorspace
)
resources.append(resource)
# Sort by filepath
resources.sort(key=lambda r: r.source)
return resources
def get_colorspace(self, spec: Sdf.AttributeSpec) -> Optional[str]:
"""Return colorspace for a Asset attribute spec.
There is currently no USD standard on how colorspaces should be
represented for shaders or asset properties - each renderer's material
implementations seem to currently use their own way of specifying the
colorspace on the shader. As such, this comes with some guesswork.
Args:
spec (Sdf.AttributeSpec): The asset type attribute to retrieve
the colorspace for.
Returns:
Optional[str]: The colorspace for the given attribute, if any.
"""
# TODO: Support Karma, V-Ray, Renderman texture colorspaces
# Materialx image defines colorspace as custom info on the attribute
if spec.HasInfo("colorSpace"):
return spec.GetInfo("colorSpace")
# Arnold materials define the colorspace as a separate primvar
# TODO: Fix for timesamples - if timesamples, then `.default` might
# not be authored on the spec
prim_path = spec.path.GetPrimPath()
layer = spec.layer
for name in COLORSPACE_ATTRS:
colorspace_property_path = prim_path.AppendProperty(name)
colorspace_spec = layer.GetAttributeAtPath(
colorspace_property_path
)
if colorspace_spec and colorspace_spec.default:
return colorspace_spec.default
|
get_colorspace(spec)
Return colorspace for a Asset attribute spec.
There is currently no USD standard on how colorspaces should be represented for shaders or asset properties - each renderer's material implementations seem to currently use their own way of specifying the colorspace on the shader. As such, this comes with some guesswork.
Parameters:
Name | Type | Description | Default |
spec | AttributeSpec | The asset type attribute to retrieve the colorspace for. | required |
Returns:
Type | Description |
Optional[str] | Optional[str]: The colorspace for the given attribute, if any. |
Source code in client/ayon_houdini/plugins/publish/collect_usd_look_assets.py
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209 | def get_colorspace(self, spec: Sdf.AttributeSpec) -> Optional[str]:
"""Return colorspace for a Asset attribute spec.
There is currently no USD standard on how colorspaces should be
represented for shaders or asset properties - each renderer's material
implementations seem to currently use their own way of specifying the
colorspace on the shader. As such, this comes with some guesswork.
Args:
spec (Sdf.AttributeSpec): The asset type attribute to retrieve
the colorspace for.
Returns:
Optional[str]: The colorspace for the given attribute, if any.
"""
# TODO: Support Karma, V-Ray, Renderman texture colorspaces
# Materialx image defines colorspace as custom info on the attribute
if spec.HasInfo("colorSpace"):
return spec.GetInfo("colorSpace")
# Arnold materials define the colorspace as a separate primvar
# TODO: Fix for timesamples - if timesamples, then `.default` might
# not be authored on the spec
prim_path = spec.path.GetPrimPath()
layer = spec.layer
for name in COLORSPACE_ATTRS:
colorspace_property_path = prim_path.AppendProperty(name)
colorspace_spec = layer.GetAttributeAtPath(
colorspace_property_path
)
if colorspace_spec and colorspace_spec.default:
return colorspace_spec.default
|