Houdini VEX - Art Direct Your Instances Procedurally!
Introduction:
Picking up from an old post, I decided to revisit an old setup I had written about a while ago. My focus will be on developing a toolset that enables artists to Art Direct instances while providing them with production-friendly tools and parameters. The primary goal is to explain the steps and process of creating the tool rather than the VEX code itself. Additionally, we will explore some of the challenges that need to be addressed and any future developments and features that could enhance the tool's functionality. Finally, I will provide a link to download the tool and hip file.
To gain a better understanding of the tool's fundamental concept, you can refer to the previous post.
Displayed below is a brief demonstration of the toolset:
The 'Populate ToolSet' comprises of the following OTLs:
InstanceSrc: This OTL defines the initial attributes and settings necessary to instance the 'Bundles' inputs.
Override Instance: This OTL provides an optional layer to modify specific attributes or quickly address minor notes.
Populate: This OTL employs two methods to populate the 'Bundles' inputs. You can either populate using a custom tool or utilize the native Houdini Instance Tool. The tool also enables users to specify settings such as rendering as packed geometry or displaying only proxy geo.
PopulateGizmo: This small gizmo tool assists in visualizing the source point from where the cache will be triggered.
InstanceSrc (Instance Tool)
The InstanceSrc component accepts input points and enables users to employ two methods for generating attribute values required for instancing. The OTL generates the following attributes:
cache
proxy
offsetFrame
startFrame
id
rot
tag
pscale
To begin, we will check whether an 'id' attribute is present. If it does not exist, we will create one. We can use the haspointattrib function to verify the presence of the 'id' attribute, which returns a value between 0 and 1. The following code can be used to achieve this:
//set id
int id;
int chk = haspointattrib(geoself(), "id");
if(chk != 1)
{
id = i@ptnum;
}
else
{
id = i@id;
}
After obtaining the 'id' attribute, we can proceed with creating several arrays that will enable us to store user input values. Specifically, we need to generate a list of rendering caches and proxy caches that users input into the tool through the UI. The tool then combines these caches into a 'Bundle' and adds essential attributes such as tags and indexes that can be utilized later.
For this instance, we will employ an explosion cache to construct the Bundles.
Use the following code to accomplish this:
//import caches
//variables
int enableproxy, numinputs, i;
string cachelist[], proxylist[], taglist[], objidlist[];
string cache, proxy, tag, sep, itmp;
int objid;
//body
sep = "_";
numinputs = chi("../inputs");
enableproxy = chi("../enableProxy");
for(i = 0; i <= numinputs; i++)
{
itmp = sep + itoa(i);
if(ch("../enableInstance") == 0)
{
cache = chs("../input_cache" + itmp);
proxy = chs("../input_proxy" + itmp);
}
else
{
cache = chs("../input_cachefile" + itmp);
proxy = chs("../input_proxyfile" + itmp);
}
tag = chs("../input_tag" + itmp);
objid = chi("../objid" + itmp);
append(cachelist[i], cache);
append(taglist[i], tag);
append(objidlist[i], itoa(objid));
if(enableproxy == 1)
{
append(proxylist[i], proxy);
}
}
The concept here is to iterate through all parameters and generate lists for each component of the bundle, which can be used later to randomize or query different sliders and values.
Once all the necessary data is accumulated, we can create two methods for Populating the instances. Once the user selects a method, we can utilize an if-else statement to execute them.
Method 1: The Randomize Attribute Method.
In this method, we provide users with sliders to control the following:
Bundle Variations: A random distribution of the bundles on each point. The user can use a bundle variation value to change the seed.
Seed: This is a global seed value that randomizes all other attributes except the bundle distribution.
Min Pscale: The min value pscale to randomize.
Max Pscale: The max value pscale to randomize.
Min Rotation Y: The min value degree rotation y to randomize in radians.
Max Rotation Y: The max value degree rotation y to randomize in radians.
Min Startframe: The min startframe or trigger for a bundle to start.
Max Startframe: The max startframe or trigger for a bundle to start.
The @startFrame is used to calculate an offsetFrame, which helps offset the caches to trigger on the generated random frame.
Before executing the method, we need to create a simple function that fits a random value between 0 and 1 to be within the user-defined minimum and maximum range. The fit01 vex snippet does the opposite. To create a custom function, we can use the following snippet at the beginning of our code, before the main.
//--------function----------
//remap fit from 0 to 1 to new val
function float remap01(float in, min, max, out)
{
out = fit(in, 0, 1, min, max);
return(out);
}
Next, we can utilize the custom function remap01 to reorganize the random values, ensuring that they fall between the user-defined input values.
//methods:
//variables
float method = chi("method");
float rand, randcache, pscale, startFrame, cachevar;
vector rot;
string idstr, tmp, cacheout, proxyout, tagout, objidout;
int enable;
//body
if(method == 0)
{
//random attribs method
rand = rand((id + 1) * (ch("seed")));
randcache = rand((id + 1) * (ch("variation")));
//set variable values
remap01(rand, ch("min_pscale"), ch("max_pscale"), pscale);
remap01(rand, chi("min_startFrame"), chi("max_startFrame"), startFrame);
remap01(rand, ch("min_rot_y"), ch("max_rot_y"), rot.y);
//cache/proxy list
remap01(randcache, 0, numinputs, cachevar);
cacheout = cachelist[int(cachevar)];
proxyout = proxylist[int(cachevar)];
tagout = taglist[int(cachevar)];
objidout = objidlist[int(cachevar)];
}
Method 2: The Defined Attributes Method
This method enables users to provide precise values for each instance. To achieve this, we need to retrieve the previously created parameters. Additionally, I have included a toggle that allows users to choose between using a custom value or a "Global" value, making it simpler for users to experiment with the instances.
The following VEX snippet is used in the implementation:
else if(method == 1)
{
//defined attribs method
idstr = itoa(id);
tmp = sep + idstr;
//set variable values
enable = chi("../enable" + tmp);
if(enable == 1)
{
pscale = ch("../pscale" + tmp);
startFrame = ch("../startFrame" + tmp);
rot.y = ch("../rot_y" + tmp);
cachevar = ch("../bundle" + tmp);
cacheout = cachelist[int(cachevar)];
proxyout = proxylist[int(cachevar)];
tagout = taglist[int(cachevar)];
objidout = objidlist[int(cachevar)];
}
else
{
pscale = ch("../global_pscale");
startFrame = ch("../global_startFrame");
rot.y = ch("../rot_y");
cachevar = ch("../global_bundle");
cacheout = cachelist[int(cachevar)];
proxyout = proxylist[int(cachevar)];
tagout = taglist[int(cachevar)];
objidout = objidlist[int(cachevar)];
}
}
After defining the required values, we can proceed to create the final attributes that will be utilized for instancing.
//set attribs
@id = id;
@pscale = pscale;
i@startFrame = int(startFrame);
i@offsetedFrame = @Frame - (i@startFrame - (ch("fstart")));
@frameoffset = -(i@startFrame - (ch("fstart")));
p@rot = quaternion(radians(rot.y), {0, 1, 0});
s@tag = tagout;
i@objid = atoi(objidout);
i@index = i@objid;
To make the OTL more versatile, we can add features allowing users to input "Object" geometry or "File" geometry for the Bundles. This will make it possible to use the custom Populate OTL and the Native Houdini Instance SOP. To achieve this, we can create additional inputs for the OTL to accept geometry as input. These inputs can be labelled as "Objects Input" for the Bundles or "File Geometry" to allow the user to choose the preferred input method. By providing these options, users can work with the tool more efficiently and in a way that best suits their workflow.
if(ch("../enableInstance") == 0)
{
if(ch("../enableOperator") == 1)
{
s@cache = "op:" + cacheout;
s@proxy = "op:" + proxyout;
}
else
{
s@cache = cacheout;
s@proxy = proxyout;
}
}
else
{
int globalPad = chi("../globalPadding");
string curFrame = sprintf("%0" + itoa(globalPad) + "d", @Frame);
string newFrame = sprintf("%0" + itoa(globalPad) + "d", i@offsetedFrame);
i@padding = globalPad;
s@curframe = curFrame;
s@inputcache = cacheout;
s@inputproxy = proxyout;
s@cache = re_replace(curFrame, newFrame, cacheout);
s@proxy = re_replace(curFrame, newFrame, proxyout);
}
int instance, enablefilepath;
if(chi("../enableInstance") == 1 || ch("../enableOperator") == 1)
{
instance = 1;
enablefilepath = chi("../enableInstance");
}
setdetailattrib(geoself(), "instance", instance, "set");
setdetailattrib(geoself(), "enablefilepath", enablefilepath, "set");
After completing all the necessary steps, we can now put everything together into the final code, which looks like this:
VEX Snippet:
//--------function----------
//remap fit from 0 to 1 to new val
function float remap01(float in, min, max, out)
{
out = fit(in, 0, 1, min, max);
return(out);
}
//---------main--------------
//set id
int id;
int chk = haspointattrib(geoself(), "id");
if(chk != 1)
{
id = i@ptnum;
}
else
{
id = i@id;
}
//import caches
//variables
int enableproxy, numinputs, i;
string cachelist[], proxylist[], taglist[], objidlist[];
string cache, proxy, tag, sep, itmp;
int objid;
//body
sep = "_";
numinputs = chi("../inputs");
enableproxy = chi("../enableProxy");
for(i = 0; i <= numinputs; i++)
{
itmp = sep + itoa(i);
if(ch("../enableInstance") == 0)
{
cache = chs("../input_cache" + itmp);
proxy = chs("../input_proxy" + itmp);
}
else
{
cache = chs("../input_cachefile" + itmp);
proxy = chs("../input_proxyfile" + itmp);
}
tag = chs("../input_tag" + itmp);
objid = chi("../objid" + itmp);
append(cachelist[i], cache);
append(taglist[i], tag);
append(objidlist[i], itoa(objid));
if(enableproxy == 1)
{
append(proxylist[i], proxy);
}
}
//methods:
//variables
float method = chi("method");
float rand, randcache, pscale, startFrame, cachevar;
vector rot;
string idstr, tmp, cacheout, proxyout, tagout, objidout;
int enable;
//body
if(method == 0)
{
//random attribs method
rand = rand((id + 1) * (ch("seed")));
randcache = rand((id + 1) * (ch("variation")));
//set variable values
remap01(rand, ch("min_pscale"), ch("max_pscale"), pscale);
remap01(rand, chi("min_startFrame"), chi("max_startFrame"), startFrame);
remap01(rand, ch("min_rot_y"), ch("max_rot_y"), rot.y);
//cache/proxy list
remap01(randcache, 0, numinputs, cachevar);
cacheout = cachelist[int(cachevar)];
proxyout = proxylist[int(cachevar)];
tagout = taglist[int(cachevar)];
objidout = objidlist[int(cachevar)];
}
else if(method == 1)
{
//defined attribs method
idstr = itoa(id);
tmp = sep + idstr;
//set variable values
enable = chi("../enable" + tmp);
if(enable == 1)
{
pscale = ch("../pscale" + tmp);
startFrame = ch("../startFrame" + tmp);
rot.y = ch("../rot_y" + tmp);
cachevar = ch("../bundle" + tmp);
cacheout = cachelist[int(cachevar)];
proxyout = proxylist[int(cachevar)];
tagout = taglist[int(cachevar)];
objidout = objidlist[int(cachevar)];
}
else
{
pscale = ch("../global_pscale");
startFrame = ch("../global_startFrame");
rot.y = ch("../rot_y");
cachevar = ch("../global_bundle");
cacheout = cachelist[int(cachevar)];
proxyout = proxylist[int(cachevar)];
tagout = taglist[int(cachevar)];
objidout = objidlist[int(cachevar)];
}
}
else if(method == 2)
{
}
//set attribs
@id = id;
@pscale = pscale;
i@startFrame = int(startFrame);
i@offsetedFrame = @Frame - (i@startFrame - (ch("fstart")));
@frameoffset = -(i@startFrame - (ch("fstart")));
p@rot = quaternion(radians(rot.y), {0, 1, 0});
s@tag = tagout;
i@objid = atoi(objidout);
i@index = i@objid;
if(ch("../enableInstance") == 0)
{
if(ch("../enableOperator") == 1)
{
s@cache = "op:" + cacheout;
s@proxy = "op:" + proxyout;
}
else
{
s@cache = cacheout;
s@proxy = proxyout;
}
}
else
{
int globalPad = chi("../globalPadding");
string curFrame = sprintf("%0" + itoa(globalPad) + "d", @Frame);
string newFrame = sprintf("%0" + itoa(globalPad) + "d", i@offsetedFrame);
i@padding = globalPad;
s@curframe = curFrame;
s@inputcache = cacheout;
s@inputproxy = proxyout;
s@cache = re_replace(curFrame, newFrame, cacheout);
s@proxy = re_replace(curFrame, newFrame, proxyout);
}
int instance, enablefilepath;
if(chi("../enableInstance") == 1 || ch("../enableOperator") == 1)
{
instance = 1;
enablefilepath = chi("../enableInstance");
}
setdetailattrib(geoself(), "instance", instance, "set");
setdetailattrib(geoself(), "enablefilepath", enablefilepath, "set");
Override Instance (Instance Tool)
The Override Instance OTL is a useful tool for addressing small notes and quick fixes during production. It can be applied to groups or specific points in a scene. One of its unique features is the "Sequential by Ptnum" option, which allows users to create a series of trigger frames based on the order of the point numbers or IDs. There are two methods available: fixed frequency and random frequency.
In this OTL, some attributes that were previously generated may be overwritten with a new value, or existing attributes may be used as inputs for certain settings.
Within the Override OTL, the following network is designed to enable users to specify groups or ptnum to overwrite values on.
In this VEX snippet, I have implemented several useful features, such as overriding pscale, rotation, startFrame, and bundles. Additionally, users can trigger the startFrame based on ptnum or id using either fixed frequency or random frequency.
VEX Snippet:
//enable
//attrib
int enablepscale = chi("enable_pscale");
int enablestartFrame = chi("enable_startframe");
int enablerot = chi("enable_rot");
//cache
int enablecachebundle = chi("enable_cache");
//override
//attribs
//override pscale
if(enablepscale == 1)
{
@pscale = ch("override_pscale");
}
//override start frame
if(ch("../sequencial_by_ptnum") == 0)
{
if(enablestartFrame == 1)
{
i@startFrame = chi("override_startFrame");
i@offsetedFrame = int(@Frame - (i@startFrame - (ch("fstart"))));
}
}
else
{
float method = ch("../method");
if(method == 0)
{
//trigger based on ptnum or id by fixed frequency
i@startFrame = chi("fstart") + i@id * chi("nth_frame");
}
else if(method == 1)
{
//trigger based on ptnum or id by random frequency
int list[], i;
int minID = detail(0, "minID");
int maxID = detail(0, "maxID");
for(i = minID; i<= maxID; i++)
{
int rand_range = int(fit01(rand(i * (ch("seed") + 1)), ch("min_rand_range"), ch("max_rand_range")));
append(list, rand_range + list[i - 1]);
}
int range = list[i@id];
i@startFrame = chi("fstart") + range;
}
i@offsetedFrame = int(@Frame - (i@startFrame - (ch("fstart"))));
int enablefilepath = detail(0, "enablefilepath");
if(enablefilepath == 1)
{
//override bundle using filepath
int globalPad = i@padding;
string inputcache = s@inputcache;
string inputproxy = s@inputproxy;
string curFrame = s@curframe;
string newFrame = sprintf("%0" + itoa(globalPad) + "d", i@offsetedFrame);
s@cache = re_replace(curFrame, newFrame, inputcache);
s@proxy = re_replace(curFrame, newFrame, inputproxy);
}
}
//override rotation
if(enablerot == 1)
{
float rotY = ch("override_rotation");
float radiansRotY = radians(rotY);
vector dir = {0, 1, 0};
p@rot = quaternion(radiansRotY, dir);
}
if(enablecachebundle == 1)
{
s@cache = chs("override_cache");
s@proxy = chs("override_proxy");
}
@frameoffset = -(i@startFrame - (ch("fstart")));
Populate (Instance Tool)
The Populate OTL provides users with the ability to instance Bundles onto points using Copy Stamp. To keep the cache size manageable, the Bundles are instanced as packed geometry and can be displayed as "Packed Primitives" for efficient IFDs and large-scale instancing.
Moreover, the Populate tool offers the flexibility to switch between Proxy Display cache and the final Render Cache. To simplify the network, two Populate OTLs can be utilized, one with Proxy Cache enabled for display and another with the Render Cache for rendering with the Render Flag activated.
If users choose to use the filepath input for caches, then this system will function with the Native Houdini Instance. The Populate OTL generates an "instancepath" attribute that includes the path of the render cache or the proxy cache, which will be visible in Houdini's Native Instancer.
To trigger the Bundles at the correct frame, the Populate tool stamps a timeoffset using the "offsetFrame" attribute generated by the InstanceSrc to initiate caches at the appropriate frame. In the case of file paths, the $F or any padding specified is replaced by the offsetFrame in the InstanceSrc, which will be read by the File SOP in Houdini's Native Instancer.
Conclusion - Art Direct Instances Procedural:
The Populate Toolset is a valuable resource for Artists in Production who need to manage multiple shots that require variations of simulated caches to be instanced across a sequence of shots. This toolset provides Artists with an efficient method to quickly display proxy geometry for flipbooks and layouts. It allows them to use override settings to address minor issues identified during dailies. Furthermore, it optimizes scenes using packed geometry, among other useful features.
Despite its usefulness, opportunities exist to enhance the Populate Toolset even further. Firstly, there is ongoing development to incorporate options for using LOPs to create bundles and instances, leading to faster and more seamless iterations for Artists. Secondly, there is an opportunity to improve scenes further by offering object-level instancing to optimize them for even better performance.
Get the Toolset + Hip File on Gumroad: https://chakshuvfx.gumroad.com/l/art-direct-instances
Comments