MaterialXOCIO 0.1.39.0.1
Utilities for using OCIO to generate MaterialX definitions and graphs
Loading...
Searching...
No Matches
core.py
1#!/usr/bin/env python
2'''
3Utilities to generate MaterialX color transform definitions using OCIO.
4
5The minimum requirement is OCIO version 2.2 which is packaged with
6ACES Cg Config` and `ACES Studio Config` configurations.
7'''
8
9import PyOpenColorIO as OCIO
10import MaterialX as mx
11import re
12
14 '''
15 A class to generate MaterialX color transform definitions using OCIO.
16 '''
17
19 '''
20 Get the OCIO built in configurations.
21 Returnes a dictionary of color spaces and the default `ACES Cg Config`.
22 '''
23
24 # As of version 2.2, `ACES Cg Config` and `ACES Studio Config` are packaged with `OCIO`, meaning that
25 # they are available to use without having to download them separately. The `getBuiltinConfigs()`
26 # API is explained [here](https://opencolorio.readthedocs.io/en/latest/releases/ocio_2_2.html)
27
28 # Get the OCIO built in configs
29 registry = OCIO.BuiltinConfigRegistry().getBuiltinConfigs()
30
31 # Create a dictionary of configs
32 configs = {}
33 for item in registry:
34 # The short_name is the URI-style name.
35 # The ui_name is the name to use in a user interface.
36 short_name, ui_name, isRecommended, isDefault = item
37
38 # Don't present built-in configs to users if they are no longer recommended.
39 if isRecommended:
40 # Create a config using the Cg config
41 config = OCIO.Config.CreateFromBuiltinConfig(short_name)
42 colorSpaces = None
43 if config:
44 colorSpaces = config.getColorSpaces()
45
46 if colorSpaces:
47 configs[short_name] = [config, colorSpaces]
48
49 acesCgConfigPath = 'ocio://cg-config-v1.0.0_aces-v1.3_ocio-v2.1'
50 builtinCfgC = OCIO.Config.CreateFromFile(acesCgConfigPath)
51 print('Built-in config:', builtinCfgC.getName())
52 csnames = builtinCfgC.getColorSpaceNames()
53 print('- Number of color spaces: %d' % len(csnames))
54
55 return configs, builtinCfgC
56
57 def printConfigs(self, configs):
58 title = '| Configuration | Color Space | Aliases |\n'
59 title = title + '| --- | --- | --- |\n'
60
61 rows = ''
62 for c in configs:
63 config = configs[c][0]
64 colorSpaces = configs[c][1]
65 for colorSpace in colorSpaces:
66 aliases = colorSpace.getAliases()
67 rows = rows + '| ' + c + ' | ' + colorSpace.getName() + ' | ' + ', '.join(aliases) + ' |\n'
68
69 return title + rows
70
71 def createTransformName(self, sourceSpace, targetSpace, typeName, prefix = 'mx_'):
72 '''
73 Create a transform name from a source and target color space and a type name.
74 @param sourceSpace: The source color space.
75 @param targetSpace: The target color space.
76 @param typeName: The type name.
77 @param prefix: The prefix for the transform name. Default is 'mx_'.
78 '''
79 transformFunctionName = prefix + mx.createValidName(sourceSpace) + "_to_" + mx.createValidName(targetSpace) + "_" + typeName
80 return transformFunctionName
81
82 def setShaderDescriptionParameters(self, shaderDesc, sourceSpace, targetSpace, typeName):
83 '''
84 '''
85 transformFunctionName = self.createTransformName(sourceSpace, targetSpace, typeName)
86 shaderDesc.setFunctionName(transformFunctionName)
87 shaderDesc.setResourcePrefix(transformFunctionName)
88
89 def generateShaderCode(self, config, sourceColorSpace, destColorSpace, language):
90 '''
91 Generate shader for a transform from a source color space to a destination color space
92 for a given config and shader language.
93
94 Returns the shader code and the number of texture resources required.
95 '''
96 shaderCode = ''
97 textureCount = 0
98 if not config:
99 return shaderCode, textureCount
100
101 # Create a processor for a pair of colorspaces
102 processor = None
103 try:
104 processor = config.getProcessor(sourceColorSpace, destColorSpace)
105 except:
106 print('Failed to generated code for transform: %s -> %s' % (sourceColorSpace, destColorSpace))
107 return shaderCode, textureCount
108
109 if processor:
110 gpuProcessor = processor.getDefaultGPUProcessor()
111 if gpuProcessor:
112 shaderDesc = OCIO.GpuShaderDesc.CreateShaderDesc()
113 if shaderDesc:
114 try:
115 shaderDesc.setLanguage(language)
116 if shaderDesc.getLanguage() == language:
117 self.setShaderDescriptionParameters(shaderDesc, sourceColorSpace, destColorSpace, "color4")
118 gpuProcessor.extractGpuShaderInfo(shaderDesc)
119 shaderCode = shaderDesc.getShaderText()
120
121 for t in shaderDesc.getTextures():
122 textureCount += 1
123
124 if shaderCode:
125 shaderCode = shaderCode.replace(
126 "// Declaration of the OCIO shader function\n",
127 "// " + sourceColorSpace + " to " + destColorSpace + " function. Texture count: %d\n" % textureCount)
128
129 except OCIO.Exception as err:
130 print(err)
131
132 return shaderCode, textureCount
133
134 def generateTransformGraph(self, config, sourceColorSpace, destColorSpace):
135 '''
136 Generate the group of transforms required to go from a source color space to a destination color space.
137 The group of transforms is output from an optimized OCIO processor.
138 @param config: The OCIO configuration.
139 @param sourceColorSpace: The source color space.
140 @param destColorSpace: The destination color space.
141 @return: The group of transforms.
142 '''
143 if not config:
144 return None
145
146 # Create a processor for a pair of colorspaces (namely to go to linear)
147 processor = None
148 groupTransform = None
149 try:
150 processor = config.getProcessor(sourceColorSpace, destColorSpace)
151 except:
152 #print('Failed to get processor for: %s -> %s' % (sourceColorSpace, destColorSpace))
153 return groupTransform
154
155 if processor:
156 processor = processor.getOptimizedProcessor(OCIO.OPTIMIZATION_ALL)
157 groupTransform = processor.createGroupTransform()
158
159 return groupTransform
160
161 def hasTextureResources(self, configs, targetColorSpace, language):
162 '''
163 Scan through all the color spaces on the configs to check for texture resource usage.
164 Returns a set of color spaces that require texture resources.
165 '''
166 testedSources = set()
167 textureSources = set()
168 for c in configs:
169 config = OCIO.Config.CreateFromBuiltinConfig(c)
170 colorSpaces = config.getColorSpaces()
171 for colorSpace in colorSpaces:
172 colorSpaceName = colorSpace.getName()
173 # Skip if the colorspace is already tested
174 if colorSpaceName in testedSources:
175 continue
176 testedSources.add(colorSpaceName)
177
178 # Test for texture resource usage
179 code, textureCount = self.generateShaderCode(config, colorSpace.getName(), targetColorSpace, language)
180 if textureCount:
181 print('- Transform "%s" to "%s" requires %d texture resources' % (colorSpace.getName(), targetColorSpace, textureCount))
182 textureSources.add(colorSpaceName)
183
184 return textureSources
185
186 def MSL(self, config, sourceColorSpace, targetColorSpace):
187 language = OCIO.GpuLanguage.GPU_LANGUAGE_MSL_2_0
188 code, textureCount = self.generateShaderCode(config, sourceColorSpace, targetColorSpace, language)
189 if code:
190 code = code.replace("// Declaration of the OCIO shader function\n", "// " + sourceColorSpace + " to " + targetColorSpace + " function\n")
191 code = '```c++\n' + code + '\n```\n'
192
193 def OSL(self, config, sourceColorSpace, targetColorSpace):
194 if OCIO.GpuLanguage.LANGUAGE_OSL_1:
195 language = OCIO.GpuLanguage.LANGUAGE_OSL_1
196 code, textureCount = self.generateShaderCode(config, sourceColorSpace, targetColorSpace, language)
197 if code:
198 # Bit of ugly patching to make the main function name consistent.
199 transformName = self.createTransformName(sourceColorSpace, targetColorSpace, 'color4')
200 code = code.replace('OSL_' + transformName, '__temp_name__')
201 code = code.replace(transformName, transformName + '_impl')
202 code = code.replace('__temp_name__', transformName)
203 code = code.replace("// Declaration of the OCIO shader function\n", "// " + sourceColorSpace + " to " + targetColorSpace + " function\n")
204 code = '```c++\n' + code + '\n```\n'
205
206 def generateMaterialXDefinition(self, doc, sourceColorSpace, targetColorSpace, inputName, type):
207 '''
208 Create a new definition in a document for a given color space transform.
209 Returns the definition.
210 '''
211 # Create a definition
212 transformName = self.createTransformName(sourceColorSpace, targetColorSpace, type)
213 nodeName = transformName.replace('mx_', 'ND_')
214
215 comment = doc.addChildOfCategory('comment')
216 docString = ' Color space %s to %s transform. Generated via OCIO. ' % (sourceColorSpace, targetColorSpace)
217 comment.setDocString(docString)
218
219 definition = doc.addNodeDef(nodeName, 'color4')
220 category = sourceColorSpace + '_to_' + targetColorSpace
221 definition.setNodeString(category)
222 definition.setNodeGroup('colortransform')
223 definition.setDocString(docString)
224 definition.setVersionString('1.0')
225
226 defaultValueString = '0.0 0.0 0.0 1.0'
227 defaultValue = mx.createValueFromStrings(defaultValueString, 'color4')
228 input = definition.addInput(inputName, type)
229 input.setValue(defaultValue)
230 output = definition.getOutput('out')
231 output.setAttribute('default', 'in')
232
233 return definition
234
235 def writeShaderCode(self, outputPath, code, transformName, extension, target):
236 '''
237 Write the shader code to a file.
238 '''
239 # Write source code file
240 filename = outputPath / mx.FilePath(transformName + '.' + extension)
241 print('Write target[%s] source file %s' % (target,filename.asString()))
242 f = open(filename.asString(), 'w')
243 f.write(code)
244 f.close()
245
246 def createMaterialXImplementation(self, sourceColorSpace, targetColorSpace, doc, definition, transformName, extension, target):
247 '''
248 Create a new implementation in a document for a given definition.
249 '''
250 implName = transformName + '_' + target
251 filename = transformName + '.' + extension
252 implName = implName.replace('mx_', 'IM_')
253
254 # Check if implementation already exists
255 impl = doc.getImplementation(implName)
256 if impl:
257 print('Implementation already exists: %s' % implName)
258 return impl
259
260 comment = doc.addChildOfCategory('comment')
261 comment.setDocString(' Color space %s to %s transform. Generated via OCIO for target: %s'
262 % (sourceColorSpace, targetColorSpace, target))
263 impl = doc.addImplementation(implName)
264 impl.setFile(filename)
265 impl.setFunction(transformName)
266 impl.setTarget(target)
267 impl.setNodeDef(definition)
268
269 return impl
270
271 def generateOCIO(self, config, definitionDoc, implDoc, sourceColorSpace = 'acescg', targetColorSpace = 'lin_rec709',
272 type='color4', IN_PIXEL_STRING = 'in'):
273 '''
274 Generate a MaterialX definition and implementation for a given color space transform.
275 Returns the definition, implementation, source code, extension and target.
276 '''
277
278 # List of MaterialX target language, source code extensions, and OCIO GPU languages
279 generationList = [
280 ['genglsl', 'glsl', OCIO.GpuLanguage.GPU_LANGUAGE_GLSL_4_0]
281 #, ['genmsl', 'metal', OCIO.GpuLanguage.GPU_LANGUAGE_MSL_2_0]
282 ]
283
284 definition = None
285 transformName = self.createTransformName(sourceColorSpace, targetColorSpace, type)
286 for gen in generationList:
287 target = gen[0]
288 extension = gen[1]
289 language = gen[2]
290
291 code, textureCount = self.generateShaderCode(config, sourceColorSpace, targetColorSpace, language)
292
293 # Skip if there are texture resources
294 if textureCount:
295 print('- Skip generation for transform: "%s" to "%s" which requires %d texture resources' % (sourceColorSpace, targetColorSpace, textureCount))
296 continue
297
298 if code:
299 # Create the definition once
300 if not definition:
301 # Create color4 variant
302 definition = self.generateMaterialXDefinition(definitionDoc, sourceColorSpace, targetColorSpace,
303 IN_PIXEL_STRING, type)
304 # Create color3 variant (nodegraph)
305 self.createColor3Variant(definition, definitionDoc, IN_PIXEL_STRING)
306
307 # Create the implementation
308 self.createMaterialXImplementation(sourceColorSpace, targetColorSpace, implDoc, definition, transformName, extension, target)
309
310 return definition, transformName, code, extension, target
311
312 def generateOCIOGraph(self, config, sourceColorSpace = 'acescg', targetColorSpace = 'lin_rec709',
313 type='color3'):
314 '''
315 Generate a MaterialX nodegraph for a given color space transform.
316 @param config: The OCIO configuration.
317 @param sourceColorSpace: The source color space.
318 @param targetColorSpace: The destination color space.
319 @param type: The type of the transform.
320 Returns a MaterialX document containing a functional nodegraph and nodedef pair.
321 '''
322 groupTransform = self.generateTransformGraph(config, sourceColorSpace, targetColorSpace)
323
324 # To add. Proper testing of unsupported transforms...
325 invalidTransforms = [ OCIO.TransformType.TRANSFORM_TYPE_LUT3D, OCIO.TransformType.TRANSFORM_TYPE_LUT1D,
326 OCIO.TransformType.TRANSFORM_TYPE_GRADING_PRIMARY ]
327
328 # Create a document, a nodedef and a functional graph.
329 graphDoc = mx.createDocument()
330 outputType = 'color3'
331 xformName = sourceColorSpace + '_to_' + targetColorSpace + '_' + outputType
332
333 nd = graphDoc.addNodeDef('ND_' + xformName )
334 nd.setAttribute('node', xformName)
335 ndInput = nd.addInput('in', 'color3')
336 ndInput.setValue([0.0, 0.0, 0.0], 'color3')
337 docString = f'Generated color space {sourceColorSpace} to {targetColorSpace} transform.'
338 result = f'{groupTransform}'
339 # Replace '<' and '>' with '()' and ')'
340 result = result.replace('<', '(')
341 result = result.replace('>', ')')
342 result = re.sub(r'[\r\n]+', '', result)
343
344 print(result)
345 docString = docString + '. OCIO Transforms: ' + result
346 nd.setDocString(docString)
347
348 ng = graphDoc.addNodeGraph('NG_' + xformName)
349 ng.setAttribute('nodedef', nd.getName())
350 convertNode = ng.addNode('convert', 'asVec', 'vector3')
351 converInput = convertNode.addInput('in', 'color3')
352 converInput.setInterfaceName('in')
353
354 #print(f'Transform from: {sourceColorSpace} to {targetColorSpace}')
355 if not groupTransform:
356 #print(f'No group transform found for the color space transform: {sourceColorSpace} to {targetColorSpace}')
357 return None
358 #print(f'Number of transforms: {groupTransform.__len__()}')
359 previousNode = None
360
361 # Iterate and create appropriate nodes and connections
362 for i in range(groupTransform.__len__()):
363 transform = groupTransform.__getitem__(i)
364 # Get type of transform
365 transformType = transform.getTransformType()
366 if transformType in invalidTransforms:
367 print(f'- Transform[{i}]: {transformType} contains an unsupported transform type')
368 continue
369
370 #print(f'- Transform[{i}]: {transformType}')
371 if transformType == OCIO.TransformType.TRANSFORM_TYPE_MATRIX:
372 matrixNode = ng.addNode('transform', ng.createValidChildName(f'matrixTransform'), 'vector3')
373
374 # Route output from previous node as input of current node
375 inInput = matrixNode.addInput('in', 'vector3')
376 if previousNode:
377 inInput.setAttribute('nodename', previousNode)
378 else:
379 #if i==0:
380 inInput.setAttribute('nodename', 'asVec')
381 #else:
382 # inInput.setValue([0.0, 0.0, 0.0], 'vector3')
383
384 # Set matrix value
385 matInput = matrixNode.addInput('mat', 'matrix33')
386 matrixValue = transform.getMatrix()
387 # Extract 3x3 matrix from 4x4 matrix
388 matrixValue = matrixValue[0:3] + matrixValue[4:7] + matrixValue[8:11]
389 matrixValue = ', '.join([str(x) for x in matrixValue])
390 #print(' - Matrix:', matrixValue)
391 matInput.setAttribute('value', matrixValue)
392
393 # Add offset value - TODO
394 offsetValue = transform.getOffset()
395 offsetValue = ', '.join([str(x) for x in offsetValue])
396 #print(' - Offset:', offsetValue)
397 # Add a add vector3 to graph
398
399 previousNode = matrixNode.getName()
400
401 # TODO: Handle other transform types
402 elif transformType == OCIO.TransformType.TRANSFORM_TYPE_EXPONENT or transformType == OCIO.TransformType.TRANSFORM_TYPE_EXPONENT_WITH_LINEAR:
403
404 hasOffset = (transformType == OCIO.TransformType.TRANSFORM_TYPE_EXPONENT_WITH_LINEAR)
405
406 #print(f'- Transform[{i}]: {transformType} support has not been implemented yet')
407 exponentNode = ng.addNode('power', ng.createValidChildName(f'exponent'), 'vector3')
408 exponentInput = exponentNode.addInput('in1', 'vector3')
409 if previousNode:
410 exponentInput.setAttribute('nodename', previousNode)
411 else:
412 if i==0:
413 exponentInput.setAttribute('nodename', 'asVec')
414 else:
415 exponentInput.setValue([0.0, 0.0, 0.0], 'vector3')
416
417 exponentInput2 = exponentNode.addInput('in2', 'vector3')
418 exponentInput2Value = None
419 if not hasOffset:
420 exponentInput2Value = transform.getValue()
421 else:
422 exponentInput2Value = transform.getGamma()
423 # Only want the first 3 values in the array
424 exponentInput2Value = exponentInput2Value[0:3]
425 exponentInput2.setValue(exponentInput2Value, 'float')
426
427 previousNode = exponentNode.getName()
428
429 if hasOffset:
430 # Add offset
431 offsetNode = ng.addNode('add', ng.createValidChildName(f'offset'), 'vector3')
432 offsetInput2 = offsetNode.addInput('in2', 'vector3')
433 offsetInput2.setNodeName(exponentNode.getName())
434 offsetInput = offsetNode.addInput('in1', 'vector3')
435 offsetValue = transform.getOffset()
436 # Only want the first 3 values in the array
437 offsetValue = offsetValue[0:3]
438 offsetInput.setValue(offsetValue, 'vector3')
439
440 previousNode = offsetNode.getName()
441
442 else:
443 print(f'- Transform[{i}]: {transformType} support has not been implemented yet')
444 continue
445
446
447 # Create an output for the last node if any
448 convertNode2 = ng.addNode('convert', 'asColor', 'color3')
449 converInput2 = convertNode2.addInput('in', 'vector3')
450 if previousNode:
451 converInput2.setAttribute('nodename', previousNode)
452 else:
453 # Pass-through
454 #print('No transforms applied. Transform is a pass-through.')
455 converInput2.setAttribute('nodename', 'asVec')
456
457 out = ng.addOutput(ng.createValidChildName('out'), 'color3')
458 out.setAttribute('nodename', 'asColor')
459
460 return graphDoc
461
462 def createColor3Variant(self, definition, definitionDoc, IN_PIXEL_STRING = 'in'):
463 '''
464 Create a color3 variant of a color4 definition.
465 '''
466 color4Name = definition.getName()
467 color3Name = color4Name.replace('color4', 'color3')
468 color3Def = definitionDoc.addNodeDef(color3Name)
469 color3Def.copyContentFrom(definition)
470 c3input = color3Def.getInput(IN_PIXEL_STRING)
471 c3input.setType('color3')
472 c3input.setValue([0.0, 0.0, 0.0], 'color3')
473
474 ngName = color3Def.getName().replace('ND_', 'NG_')
475 ng = definitionDoc.addNodeGraph(ngName)
476 c4instance = ng.addNodeInstance(definition)
477 c4instance.addInputsFromNodeDef()
478 c4instanceIn = c4instance.getInput(IN_PIXEL_STRING)
479 c3to4 = ng.addNode('convert', 'c3to4', 'color4')
480 c3to4Input = c3to4.addInput('in', 'color3')
481 c4to3 = ng.addNode('convert', 'c4to3', 'color3')
482 c4to3Input = c4to3.addInput('in', 'color4')
483 ngout = ng.addOutput('out', 'color3')
484 #ngin = ng.addInput('in', 'color3')
485 ng.setNodeDef(color3Def)
486
487 c4instanceIn.setNodeName(c3to4.getName())
488 c4to3Input.setNodeName(c4instance.getName())
489 ngout.setNodeName(c4to3.getName())
490 c3to4Input.setInterfaceName(IN_PIXEL_STRING)
A class to generate MaterialX color transform definitions using OCIO.
Definition core.py:13
createMaterialXImplementation(self, sourceColorSpace, targetColorSpace, doc, definition, transformName, extension, target)
Create a new implementation in a document for a given definition.
Definition core.py:246
generateOCIO(self, config, definitionDoc, implDoc, sourceColorSpace='acescg', targetColorSpace='lin_rec709', type='color4', IN_PIXEL_STRING='in')
Generate a MaterialX definition and implementation for a given color space transform.
Definition core.py:272
generateOCIOGraph(self, config, sourceColorSpace='acescg', targetColorSpace='lin_rec709', type='color3')
Generate a MaterialX nodegraph for a given color space transform.
Definition core.py:313
generateTransformGraph(self, config, sourceColorSpace, destColorSpace)
Generate the group of transforms required to go from a source color space to a destination color spac...
Definition core.py:134
generateShaderCode(self, config, sourceColorSpace, destColorSpace, language)
Generate shader for a transform from a source color space to a destination color space for a given co...
Definition core.py:89
getBuiltinConfigs(self)
Get the OCIO built in configurations.
Definition core.py:18
setShaderDescriptionParameters(self, shaderDesc, sourceSpace, targetSpace, typeName)
Definition core.py:82
createColor3Variant(self, definition, definitionDoc, IN_PIXEL_STRING='in')
Create a color3 variant of a color4 definition.
Definition core.py:462
generateMaterialXDefinition(self, doc, sourceColorSpace, targetColorSpace, inputName, type)
Create a new definition in a document for a given color space transform.
Definition core.py:206
createTransformName(self, sourceSpace, targetSpace, typeName, prefix='mx_')
Create a transform name from a source and target color space and a type name.
Definition core.py:71
writeShaderCode(self, outputPath, code, transformName, extension, target)
Write the shader code to a file.
Definition core.py:235
hasTextureResources(self, configs, targetColorSpace, language)
Scan through all the color spaces on the configs to check for texture resource usage.
Definition core.py:161