Automated randomly generated number decals. Script + procedural material.

0 votes
Date Updated: 

This post details how I set up an automated decal numbering system. A random number is assigned to a dummy object that matches a name convention and any decal objects attached to that dummy assume the same ID. The ID can be used for material assignment purposes, in this case it is to drive a sprite sheet that renders that number to a material.

At a minimum the script should be a good jump point for automating parts of procedural artwork.

As used in this image:

The vehicles were all instanced from an original with no other steps needed on the decals.

The script


  • Execute the script
  • Make a dummy object
  • Preface the name of the dummy object with Signage_ID, it now has a random ID between 1 and 999 (Object Properties > G-Buffer - Object ID)
  • Make some other objects, ie: decal planes
  • Link objects to the signage_id dummy, they automatically assume the same ID
  • Clone everything, new dummy object gets a new random ID


callbacks.removeScripts id:#autosignage
callbacks.addScript #nodeNameSet "onNodeNameSet()" id:#autosignage
callbacks.addScript #nodeCreated "onNodeCreated()" id:#autosignage
callbacks.addScript #nodeLinked "onNodeLinked()" id:#autosignage
function testName val =
	if matchPattern val pattern:"Signage_ID*" then
		return true
	return false
function assignID n id =
	n.gbufferChannel = id
	print ("node: " + + ", assigned id: " + id as string)
function onNodeNameSet = 
	n = callbacks.notificationParam();
	_node = n[3]
	_newName = n[2]
	if (classOf _node == Dummy AND testName _newName) then
		if (_node.gbufferChannel == 0) then
			assignID _node (random 1 999)
		for i in _node.children do
			assignID i _node.gbufferChannel
function onNodeCreated = 
	n = callbacks.notificationParam();
	if (classOf n == Dummy AND testName then
		assignID n (random 1 999)
function onNodeLinked = 
	n = callbacks.notificationParam();
	if (classOf n.parent == Dummy AND testName then
		assignID n n.parent.gbufferChannel

I'm using the gbufferChannel for ID because I am already using 'wire_color' as selection masks on this procedural material setup and later on I might use it as a group selector for randomising vehicle paint colours too.


Tut on how the decal plane material was set up:

I decided to draw the ID number straight onto the vehicles. To do that I built a simple sprite map system:

The number images are provided by one bitmap:

3DSMax has an OSL node called "Object ID" used to collect the ID numbers we have assigned to decal planes.

Next I needed to convert the ID into three separate digits so I made a new OSL script to do that:

shader IntSplit
[[ string help = "Splits input integer into 3 digits",
   string label= "IntSplit",
   string category = "Values" ]]
	int Input = 0 [[ string label = "Input" ]],
	output int I0 = 0,
	output int I1 = 0,
	output int I2 = 0,
	int i = Input;
	int d[3] = {0,0,0};
	int a = 0;
	while (i > 0) 
		int val = i % 10;
		d[a] = val;
		i = i / 10;
	I0 = d[0];
	I1 = d[1];
	I2 = d[2];

We are outputting whole numbers, but UVs operate in a 0-1 range, since our bitmap has ten columns I divide each input by ten.

The bitmap is piped through three UV transforms which each:
Set up the scaling: my decal planes are 3:1 ratio so I adjusted here to match
Set up the offsets for where each digit places

The UV translate number created earlier is then used to slide the bitmap horizontally to arrive at the given number.

Each transform then plugs into a layer map. I used checkerbox maps as a quick masking tool.


Hopefully this info dump helps someone in need.

Gallery of future images that will use this:


Application Version: 
3DSMax 2021. Maxscript should work in 2017
autosignage.ms1.13 KB
splitint.osl_.txt448 bytes