Skip to main content

I had 216 assets to bake textures for, I’m not doing that by hand…

My normal baking workflow is in substance, which works beautifully with a nice shiny UI. It doesn’t however work on commandline without their Automation Toolkit. They’ve since given me a free licence for it (cheers!) so I’ll be having a look at that soon, but anyway, old favourites to the rescue!

I first looked at using the gamedev toolkit in houdini for this, but the mix of ACEScg gamuts, EXR files, and the beta nature of the tools just didn’t quite mix. I was getting colour shifts and unreliable outputs, so I put that one to the side to work out another day.

xNormal to the rescue

Old favourites die hard, like habits I guess. xNormal had been my trusty baker, with it’s weird and wonderful UI throughout my undergraduate studies, so why not have a look at it again? It’s not like the technology has changed much, and I knew I was feeding it “ideal” data as far as a baker was concerned – pretty much perfectly matching topology, no smoothing groups, just reduced polycounts.

The low poly generation in Houdini

xNormal can run on the commandline by passing an exported XML file of settings. You could create the settings you wanted, and then do a find and replace for each iteration’s high and low poly model, or generate an entire xml with the correct settings from scratch. In the interest of not re-inventing the wheel, I tentatively searched “xnormal python”, and well wouldn’t ya know it, three top results – a pypi page, a blog and a github! Christmas has come early!

So cheers Daniel Holden, of, you’ve saved me a bunch of hassle!

The library by default runs the program with the parameters you set, but I wanted to run this on the renderfarm, so I made use of the configurtion generation without processing, and just passed that onto xnormal as a commandline arguement.

For those curious, for photogrammetry I bake diffuse textures from a mid-res poly object (faster for meshroom to unwrap and project), but I bake normal maps from a high poly. The code to generate the xml is below:

import os
import sys
sys.path.insert(0, 'C:/_PLUGINS/PYTHONLIBS/Python-xNormal/build/lib')
import xNormal

project_root      = os.path.dirname(os.path.realpath(__file__))
dir_sfm           = os.path.join(project_root, '03_SFM')
dir_low           = os.path.join(project_root, '04_LOWPOLY')
dir_bake          = os.path.join(project_root, '05_BAKE')

image_set         = 'ETA-001.A'
xnorm_res         = '4096'

dir_sfm_path = os.path.join(dir_sfm, image_set)
dir_low_path = os.path.join(dir_low, image_set)
dir_bake_path = os.path.join(dir_bake, image_set)
if not os.path.exists(dir_bake):
if not os.path.exists(dir_bake_path):

#Config Files
xnorm_diffuse_xml       = os.path.abspath(os.path.join(dir_bake_path, '05_xNormal_Diffuse.xml'))
xnorm_normals_xml       = os.path.abspath(os.path.join(dir_bake_path, '05_xNormal_Normals.xml'))
#Import Paths
xnorm_midpoly_obj       = os.path.abspath(os.path.join(dir_sfm_path, 'midTextured.obj'))
xnorm_midpoly_diffuse   = os.path.abspath(os.path.join(dir_sfm_path, 'midTextured_u1_v1.png'))
xnorm_highpoly_obj      = os.path.abspath(os.path.join(dir_sfm_path, 'high.obj'))
#Export Paths
xnorm_lowpoly_obj       = os.path.abspath(os.path.join(dir_low_path, 'low.obj'))
xnorm_lowpoly_diffuse   = os.path.abspath(os.path.join(dir_bake_path, 'low.png'))
xnorm_lowpoly_normals   = os.path.abspath(os.path.join(dir_bake_path, 'low.png'))

#Generate Transfer Texture settings
high_config            = xNormal.high_mesh_options(xnorm_midpoly_obj, base_texture = xnorm_midpoly_diffuse)
low_config             = xNormal.low_mesh_options(xnorm_lowpoly_obj)
generation_config      = xNormal.generation_options(xnorm_lowpoly_diffuse,
                                                        width = xnorm_res,
                                                        height = xnorm_res, 
                                                        normals_high_texture = True, 
                                                        closest_if_fails = True, 
config = xNormal.config([high_config], [low_config], generation_config)
f = open(xnorm_diffuse_xml, 'w')
Rich Harper

I’m a Creative Technologist. I build, create, hack, reverse-engineer, design and develop pretty much anything related to visual media.

Leave a Reply