Instancing in Katana
Katana, as usual, doesn't offer a "ready to go" solution for instancing. This initial complexity can be overcome by the fact that we can create an instancing solution that exactly suits our needs. And that is what we are going to address in this post. Additionally, I will explain how I tried to create a flexible solution for instancing called KUI so you don't have to !
Intro
As Katana's motto states : Itβs all just a bunch of Attributes. And it applies to instances too. They are just a bunch of locations with a defined list of attributes understood by your render-engine. You can as such create an instance with a simple LocationCreate + AttributeSet setup (if you have time to lose). But we will be using OpScripts to do so.
Here is a quick diagram that could resume how an instance is built :

The basic principle is that an instance links at least to one instance source (a scene-graph location). The instance will create a "copy" of this instance source. You can then set transformations override that will allow the instance to have a different position, rotation, etc, than the source. Additional attributes can also be set and used for shading to make the instance even more different than the source.
Instancing Methods
Instancing comes in different flavors, that, similarly to all things, have specific ups and downs. Your render-engine may also supply alternative ways to produce instances so be sure to check its documentation on the topic.
Here is what the Katana documentation say about this:
Leaf-level
(Never used this one)
The Katana documentation is pretty explicit.
Would love to know in what case this one can be more pertinent than the other methods.
Hierarchical
Each instance = one scene-graph location.
Array
One single scene-graph location where each instance correspond to an index on each attribute.
And there is probably some additional pro/cons inherent to your render-engine so again, check the documentation, and test stuff. (For example, when I started to explore instancing, Redshift was not supporting locations with children when using the array method (not the case anymore).)
Instancing in Practice
To start, there is a nice small example on the official Katana documentation . It explains how to create instances using mostly Katana nodes and one small OpScript to avoid stacking numerous AttributeSet nodes. This approach is pretty basic: we manually set how many instances we want to create and we need to manually move them. The setup also takes time to build and is not very scalable.
A more widely used solution depends on point-clouds: a type of location composed of visual abstract "points" in the 3d space that can hold an arbitrary number of attributes based on the point index.
You use each individual point's attribute to create an instance. For example, each point can specify what kind of instance source it is representing, ... Furthermore, its "abstract" aspect makes it very convenient for transferring data between DCCs.
A convenient way to create scene graph locations based on a source object like a point-cloud, is to use the OpScript feature. It is an entry door to use scripting while staying in the Katana nodegraph system. Usage of OpScript require to learn the lua language . But don't worry, if you don't want to get your hands dirty you will be able to use a premade script/node shared in the Katana Uber Instancing section.
To create scene graph locations we need to know how they must be structured. For this what's better than having a look at the documentation : AttributesConventions/Instancing. You notice that we find the 3 instancing methods described again.
Let's now start building the scene.
Scene-Preparation
For you to follow the tutorial, I will be providing you a few assets. Actually only a point-cloud, as to keep it simple, instances sources will be primitives.
You can also download the pointcloud used in KUI for testing.
This point-cloud has been generated from Mash (see mash2pointcloud) and contains the most commonly used attributes.
Here is what it looked like in Maya :


And here is the instances-sources mapping list :
0: cube 1: cone 2: sphere
Here it is imported in Katana :

I also used a small OpScript that allow me to set the viewer size of the points. You can grab the OpScript here.
In the Attributes tab we can see what are the attributes stored on the point-cloud. This one has :
arbitrary
scale : XYZ per-point scale attribute.
rotation: XYZ per-point rotation attribute
objectIndex: per-point index to use for instance-source
colorRandom: per-point random color
point
P : XYZ per-point transform
v : per-point velocity
width : added via the OpScript for viewer size.
All the attributes in the arbitrary section don't have a naming convention. You must know which name corresponds to which type of data for when you are creating the OpScript that produce the instances.
For the instance-sources we will be using simple primitives as detailed above. You can use PrimitiveCreate node to create them. My final "initial" nodegraph is looking like this :

Now it's time to have a look at OpScripting.
OpScript-Preparation
We are going to manipulate a lot of inputs and data and at some point, we will need to see what X variable is equal to, what is the result of X operation, etc to just be able to know where we need to go scripting-wise. Usually, this is done by using the print() function. But this is very basic and can lead to various limitations.
To have a more robust way of debugging OpScripts I made myself a small logging module in lua. Kind of similar to what Python logging module does. It adds a bunch of line to your script but will allow more flexibility in the way data will be displayed to you.
Have a look at this repository to install the llloger module :
All instructions are specified in the documentation so I have not much to explain here.
We will then be able to use the logger methods to output messages to the console. (This just wrap the print() function which in Katana, output the result in the console that should be opened alongside your Katana)
logger:debug("any object") logger:info("any object") logger:warning("any object") logger:error("any object")
All these steps are not mandatory for this tutorial. They just help for faster debugging. (And pertinent if you want to write lua code by yourself.) Though, the llloger module is required for KUI to work, so if you plan to use it, you will need to install it anyways.
And by the way, if this is your first time with OpScript, the documentation can be a bit confusing at first. It is split into multiple "modules" with different language bindings. The one we use the most often is the CookInterface :
Basic Instancing : Hierarchical
For a first try, we will be using the OpScript provided on the Foundry's documentation. It's the most basic you can do which will be perfect for an introduction. It's the one for the hierarchical method.
Create an OpScript node and paste the bottom script inside the script.lua parameter
--[[ source: https://support.foundry.com/hc/en-us/articles/360006999279 ]] -- Read op arguments local instanceSourceLocation = Interface.GetOpArg("user.instanceSourceLocation"):getValue() local pointCloudLocation = Interface.GetOpArg("user.pointCloudLocation"):getValue() if Interface.AtRoot() then -- Read the point cloud local points = Interface.GetAttr("geometry.point.P", pointCloudLocation) -- ignore the other samples so no motion blur ! points = points:getNearestSample(0.0) -- Loop over points local x, y, z local gb = GroupBuilder() for i=0, #points/3 - 1 do x = points[3*i+1] y = points[3*i+2] z = points[3*i+3] -- Build op arguments for the child location gb:update(Interface.GetOpArg()) gb:set("childAttrs", Interface.GetAttr("", instanceSourceLocation)) gb:set("childAttrs.type", StringAttribute("instance")) gb:set("childAttrs.geometry.instanceSource", StringAttribute(instanceSourceLocation)) -- note: we shouldn't use `xform.interactive` as originaly specified. gb:set("childAttrs.xform.group0.translate", DoubleAttribute({x, y, z})) -- Create the child Interface.CreateChild( string.format("child%04d", i), Interface.GetOpType(), gb:build() ) end else local childAttrs = Interface.GetOpArg("childAttrs") for i=0, childAttrs:getNumberOfChildren()-1 do Interface.SetAttr(childAttrs:getChildName(i), childAttrs:getChildByIndex(i)) end end
If you look at the first lines you can see that we are getting some OpArg values. On OpScript nodes this corresponds to user parameters. This means we will need to create two of them.

You should have noticed the first script's limitation, we can only give one instance-source for now. But let's keep that for later. Set the 2 created user parameters values with their corresponding locations. (! the pointcloud is the location of type pointcloud , not its parent "group".)
Now let's view the OpScript node, and expand the target location in the SceneGraph to see our instances.

Yay, that was quick to have something working. But check the Attributes on one of the instance.

If you have a look at the xform.interactive attributes, we can see that only the translate attribute has non-default values. This is because our current OpScript only read the P attribute on the point-cloud which correspond to the instance translations.
25 gb:set("childAttrs", Interface.GetAttr("", instanceSourceLocation))
This would allow having the bounds attribute on the instance, so we have at least some primitive representation in the viewer. But the geometry attributes are not needed because they are copied from the instance-source at render-time. To fix this, the instance-source location would need to be a group with the mesh inside.
Now, what we should not forget, is cleaning the scene for rendering. This means :
Hide the pointcloud (cause you render-engine will probably render the points as spheres). You can use a VisibilityAssign node for this.
Hide the instances-sources. This can be graciously done by setting the type of the instance-source location to instance source. You can use an AttributeSet node for this.

Annnnd, we can try to fire up a render to see our instancing result. Nothing very exciting, using primitives doesn't look very impressive. You can have a try with any asset, just instance it's top-most location. Here is the result with a "heavy" asset :

100 x 3.2 mi vertices house asset, 1920x1080, 3Delight
And if you need it, here is the Katana file :
Basic Instancing : Array
Before trying to go further with hierarchical we are going to have a look at the array method. Keep the same scene, we will only need to change the OpScript.
And here it is. It's a slightly modified version from the one on Foundry's website. (better readability + bugs fixed)
--[[ source: https://support.foundry.com/hc/en-us/articles/360006999239 ]] -- Read the op arguments local instanceSourceLocations = Interface.GetOpArg("user.instanceSourceLocations") local pointCloudLocation = Interface.GetOpArg("user.pointCloudLocation"):getValue() -- Read the point cloud's points local pointAttr = Interface.GetAttr("geometry.point.P", pointCloudLocation) local points = pointAttr:getNearestSample(Interface.GetCurrentTime()) -- declare variable used to build the final instance -- The indexArray attribute determines which instance source each instance location represents local indexArray = {} local matrixArrayMap = {} --[[--------------------------------------------------------------------------- PROCESS instance source attribute ]] -- for each instance create an instance index for i=0,#points/3-1 do -- For this example, the instances are arbitrarily assigned to an -- instance source -- a more stable apporach would be to use an arbitrary attribute -- on the point cloud to assign an instance source indexArray[#indexArray+1] = i%instanceSourceLocations:getNumberOfTuples() end --[[--------------------------------------------------------------------------- PROCESS MATRIX ATTRIBUTE ]] -- Get the transforms from the points local numTimeSamples = pointAttr:getNumberOfTimeSamples() local matrixArray local workMatrix local sampleTime local pointSample local x, y, z -- to get motion blur on the instances, create an instanceMatrix at each -- time sample available from the point cloud points attribute for idx=0,numTimeSamples-1 do sampleTime = pointAttr:getSampleTime(idx) pointSample = pointAttr:getNearestSample(sampleTime) -- each instance in array has its own matrix matrixArray = {} workMatrix = Imath.M44d():toTable() -- for each instance build a matrix with a mocked up transformation for i=0,#pointSample/3-1 do -- grab the points that represent this instance x = pointSample[3*i+1] y = pointSample[3*i+2] z = pointSample[3*i+3] -- set the translate of the matrix to the points in the point cloud workMatrix[13] = x workMatrix[14] = y workMatrix[15] = z for j = 1,16 do matrixArray[#matrixArray+1]=workMatrix[j] end end matrixArrayMap[sampleTime] = matrixArray end --[[--------------------------------------------------------------------------- Build the array instance ]] -- Create a single location which will generate an array of instances -- Set type for this location to 'instance array' Interface.SetAttr('type', StringAttribute('instance array')) -- This instance array location must point to the instance source locations -- through the attribute 'geometry.instanceSource' Interface.SetAttr('geometry.instanceSource', instanceSourceLocations) -- Set index for instance array element Interface.SetAttr('geometry.instanceIndex', IntAttribute(indexArray, 1)) Interface.SetAttr('geometry.instanceMatrix', DoubleAttribute(matrixArrayMap, 16))
You still need to create 2 user parameters on the OpScript node, but this time user.instancesSourceLocations must be a string array of scene graph-locations.

And of course the same user.pointCloudLocation one. The location parameter still define where the instance is created but this time it's not the group holding the instances, but directly the full location of the instance (array instance is only one scene-graph location).
Make sure the OpScript is running and then check the attribute on the instance array location created.

This time we can use our different instance-sources thanks to the InstanceIndex attribute that specify which instance-source to use per point. But if we look more closely at the OpScript lua script, we notice the index are generated mathematically instead of using our point-cloud's objectIndex attribute. This will need to be addressed later of course.
We can also notice that we are not using the traditional "translate" attribute, but a matrix one. Matrices have the advantages of replacing 5 attributes with 1 (translations, rotations(X, Y, Z), scale) but are harder to modify "on-the-fly". In the end choose what suits you best for your workflow.
To know what kind of attributes are supposed to be supported by each instancing method, we can have a look at the documentation:
Only the Array method require specific attributes as all instances are represented by one scene-graph location.
Full Instancing
Aight' that was a quick first look at instancing, but as mentioned, we were not using all the exported attributes on our point-cloud. Supporting them requires extending the basics OpScripts we used but this will be too long for this blog-post. Instead, I'm just going to give the code logic you could be using if you want to go down that road. Else you will find a fully working solution in the Katana Uber Instancing section π.
Full Instancing : Hierarchical
Hierarchical is using single location per-instance, they can use the commonly used attributes for locations like xform. This transformation attributes are described in the docs : dev-guide/AttributeConventions/Transformations. So pretty easy to implement, in pseudo-code :
-- this is "pseudo-code", not usable as it is. local points = ... local translate_attr = ... local rotate_attr = ... local scale_attr = ... local out_translate local out_rotateX local out_rotateY local out_rotateZ local out_scale local instance -- points is divided by 3 cause it has `num point * XYZ` -- in lua we start counting at 1 but we need the `i` to start at 0 to correctly -- gather each point index. As we start at 0 we remove 1 to compensate. for i=0, #points/3 -1 do instance = GroupBuilder() out_translate = {points[3*i+1], points[3*i+2], points[3*i+3]} -- as stated in the doc, rotations need to define axis orientation. out_rotateX = {rotate_attr[3*i+1], 1.0, 0.0, 0.0} out_rotateY = {rotate_attr[3*i+2], 0.0, 1.0, 0.0} --[...] --[...] instance:set("childAttrs.xform.group0.translate", out_translate) instance:set("childAttrs.xform.group0.rotateZ", out_rotateX) instance:set("childAttrs.xform.group0.rotateY", out_rotateY) instance:set("childAttrs.xform.group0.rotateX", out_rotateZ) instance:set("childAttrs.xform.group0.scale", out_scale) Interface.CreateChild( instance_name, Interface.GetOpType(), instance:build() ) end --[...]
In the code I wrote in the past, my target was xform.interactive but this is wrong as the xform is not interactive like with a Transform3D ! You should use xform.groupN convention instead.
If you are now wondering who to determine which instanceSource to use, the logic is pretty simple :
-- this is "pseudo-code", not usable as it is. local user_instance_sources = ... local points = ... local instance_index_attr = ... local instance local out_instance_source local current_instance_index for i=0, #points/3 -1 do instance = GroupBuilder() -- find which index the currently visited point corresponds to current_instance_index = instance_index_attr[i+1] -- The user_instance_sources had of course to be submitted in the proper -- order to work. -- (in lua, we start counting from 1, so if the above index returned start -- at 0, we need to add 1.) out_instance_source = user_instance_sources[current_instance_index + 1] instance:set( "childAttrs.geometry.instanceSource", StringAttribute(out_instance_source) ) --[...] Interface.CreateChild( instance_name, Interface.GetOpType(), instance:build() ) end --[...]
And you could then do the same for arbitrary attributes like colorRandom. The only difference could be the target destination on the instance. You must check your render-engine documentation for that, but usually, it's :
-- this is "pseudo-code", not usable as it is. local random_color_attr = ... local points = ... local instance local cr, cg, cb for i=0, #points/3 -1 do instance = GroupBuilder() --[...] cr = random_color_attr[3*i+1] cg = random_color_attr[3*i+2] cb = random_color_attr[3*i+3] -- not gonna lie I don't really know what the scope does instance:set( "childAttrs.geometry.arbitrary.randomColor.scope", StringAttribute("primitive") ) -- inputType is important ! Depends on what node you use in the shading -- network to get back the data. In Arnold this would be an `user_data_rgb` instance:set( "childAttrs.geometry.arbitrary.randomColor.inputType", StringAttribute("color3") ) instance:set( "childAttrs.geometry.arbitrary.randomColor.value", FloatAttribute({cr, cg, cb}, 3) ) Interface.CreateChild( instance_name, Interface.GetOpType(), instance:build() ) end --[...]
And finally just for ""educational"" purposes, here is the code I used on a Redshift production. It's not that documented and code have a lot of mistakes so use it at your own risk. Again I recommend instead having a look at KUI.
Full Instancing : Array
Array is in a way more simple, you can just brainless copy the attributes from the point-cloud to the instance (if they are properly formatted). Make sure to check the documentation about what kind of attribute is expected. Pseudo code is looking like this :
-- this is "pseudo-code", not usable as it is (actually for this one it is). local pointCloudLocation = Interface.GetOpArg("user.pointCloudLocation"):getValue() local p_attr = Interface.GetAttr( "geometry.point.P", pointCloudLocation ) -- this already return a FloatAttribute instance. Interface.SetAttr('type', StringAttribute('instance array')) Interface.SetAttr('geometry.instanceTranslate', p_attr)
Yup, it is that easy if you only need translations.
To add rotations, you will need to split the incoming point-cloud attribute into X,Y and Z and add the axis direction. Works the same as for hierarchical. And imagine you are using a matrix instead. Even less code to write.
Anyway here was the solution I used in prod, same blah blah as for hierarchical...
Advanced workflows
Time samples and Motion-blur
An important topic that I actually only manage to understand very few time before publishing this article. To have motion-blur working on your instances (if there is movement), they need to store on attributes multiples samples that correspond to the shutterOpen/Close values specified in the RenderSettings. A sample could be considered as a "sub-frame", so with shutterOpen=-0.25, shutterClose=0.25 and the maxTimeSamples set to 3 you would find 3 time samples at -0.25, 0.0, 0.25 per attribute.
Example with an xform matrix attribute :
<DoubleAttribute: values=16, samples=3, tupleSize=4> { [-0.25] = {{1.0, 0.0, 0.0, 0.0}, {0.0, 1.0, 0.0, 0.0}, {0.0, 0.0, ... }}, [0.0] = {{1.0, 0.0, 0.0, 0.0}, {0.0, 1.0, 0.0, 0.0}, {0.0, 0.0, ... }}, [0.25] = {{1.0, 0.0, 0.0, 0.0}, {0.0, 1.0, 0.0, 0.0}, {0.0, 0.0, ... }}, }
All the code you saw in the Instancing In Practice section (except the Basic Array one) doesn't take account of multiple time samples and just gets the nearest sample at 0.0. And you better know if you need to support motion-blur before writing anything (I have to rewrite a good chunk of KUI because of not knowing about it).
The Katana Attributes documentation define all the methods you can use to manipulate time samples but I found it confusing with not a lot of examples to show how the overall picture is working. So here is a code snippet showcasing the 2 different options to manipulate values per time-sample :
-- pseudo code to showcase code logic, not usable as it is local data = Interface.GetAttr("geometry.point.P") local samples local sample local values local new_value = {} --[[ -------------------------------------------------------------------------- USING TABLES table are a bit faster but can only go up to 2^27 (134 million) values per attribute ]] samples = data:getNumberOfTimeSamples() for smplindex=0, samples - 1 do -- convert the smplindex to sampletime (shutterOpen/Close values) sample = data:getSampleTime(smplindex) values = data:getNearestSample(sample) -- table -- // do something with the values table new_value[sample] = values end --[[ -------------------------------------------------------------------------- USING ARRAYS arrays are a bit slower but have no limit, array manipulation is less convenient than tables. ]] samples = data:getSamples() for smplindex=0, #samples - 1 do sample = samples:get(smplindex) -- get() starts at 0 values = sample:toArray() -- Array -- // do something with the values Array new_value[sample:getSampleTime()] = values end -- new value is a table of time samples. Exemple : -- new_value {-0.25={...}, 0.0={...}, 0.25={...}} -- new_value {-0.25=Array, 0.0=Array, 0.25=Array}
Of course, this adds an additional small loop to process values which increase code complexity and could also damage performances if not optimized code is being used.
You can have a look at the lua files in My Foundry_Katana GitHub repository like attrTypeSwap.lua to see more context use of time-samples.
Instances preview in the Viewer
Since Katana 4.5, it is now possible to view instance array in the Viewer :
You need to set instance-source location type to instance source.
Make sure the instance-sources and the instance are set to be viewed in the Viewer (location expanded or "eye" checked for all the instance source hierarchy).
β Be careful though, as if your instance-sources are heavy meshes, you might end up with an un-responsive Viewer.
More details in this video. Or in the Katana 4.5 patch-note.
The above also apply to hierarchical even if not specified in the notes. For Katana < 4.5, there is no real solution for arrays, but there is one for hierarchical that make use of the proxies.viewer attribute :
Proxies and Good Data for Users
This solution require to have a pre-generated proxy alembic for your instance-source.
The attributes have to be set on the instances itself. But it would be less work to set these attributes on the instance-source and then make sure your OpScript copies the local attributes from the instance-source to the instance ( with Interface.GetAttr("", instanceSourceLocation)).
Nothing prevent you to also set it on an array instance, but this requires to have already pre-generated an exactly similar-looking alembic.
Modifying point-clouds | Transforms
You might stumble upon the case where you can't re-generate the point-cloud and you have to move it in Katana. But we can't use our good old Transform3D friend here because, well, the transformations data is stored in geometry attributes, and the Transform3D only modify the xform attribute !
But no need to worry I got u a solution on my GitHub :

PointcloudXform2P
Allow merging xform transformations on a pointcloud location to the geometry.point.P attribute.
As mentioned, the OpScript only modify the P attribute, meaning only the translation and rotation from the Transform3D are applied.
Modifying point-clouds | Culling
Another need would be to prune points to reduce instances. Even if instancing improve performances , more instances still costs at render-time so you wanna make sure you are not rendering non-contributing instances.
For this you could try to see Efthymis's OpScript :
Frustum Culling OpScript for Katana
This OpScript creates Attributes based on if the geometry/point is inside the Camera's Frustum Culling.
Hide geometry that is outside of the Frustum (from the viewport)
Set Visibility Attribute (for render)
Create Attributes based on distance from the camera.
Create instanceSkipIndex attribute for PointClouds.
The concept is to use the instanceSkipIndex attribute, at least for the array method, to specify point index that must not be rendered.
For hierarchical you would need to read this attribute and whenever the current point index you are visiting is in instanceSkipIndex you just don't build the instance and skip to the next point.
I'm planning to ship a solution with KUI to easily cull points using boxes, but if you are reading this means the feature has not been implemented yet. So make sure to follow the issue on the github repo to get notified when this is implemented.
Katana Uber Instancing
As we just saw, instancing can require in some cases quite some work before having a result. That's why I tried to produce a solution that would be very flexible with a very straightforward setup.
The goal here was to create an 'uber' instancing node (just a group node actually) where, using the same parameters, you could conveniently switch between different instancing methods and have a lot of flexibility on inputs. (Leaf-level has been excluded as I'm not familiar with it.)
A lot of work has been put into this project, learned a lot about lua and I'm really happy to share it with you. (Furthermore open-sourced)
The project is available on GitHub here :
I let you check the README.md that is listing all the instructions necessary to use this tool. There is pretty extensive documentation that should cover everything you need to know.
Render-Engines
Even if you are sure your instancing setup is correct, it might actually not be what your render-engine expect it to be. So golden rule, first read your renderer documentation carefully to see what is required, then if it's still not working, you will have to test stuff until it works π¬.
For Redshift, check the section right under, for other renderers, here is what I found :
3Delight
ANDRenderman
: arbitrary attribute need to be Float and not Double.3Delight
ANDRenderman
: instance array seems to only support instance matrix attribute and not the other TRS attributes.Arnold
: Using the TRS instances attributes with array method led to visually incorrect rotations. You have to use an instanceMatrix attribute to see the correct result.Arnold
: for array instancing, arbitrary attributes need to have the scope set to point to work.Renderman
: arbitrary attribute for hierarchical need to be located at prmanStatements.attributes.user.<myAttr>.value. See Renderman doc.Renderman
: arbitrary attribute for array are at the usual geometry.arbitrary path but scope need to be primitive. This mean the attribute can be used as a primvar in shading.Renderman
: for array instancing, the material must be assigned on the instance array location and not on the instances sources.
You can check the test scene I used for KUI that should have a working setup for Arnold, 3Delight and Renderman.
https://github.com/MrLixm/KUI/blob/master/main/scenes/kui.tests.katana
Redshift
The production where I had to look for instancing was using Redshift, and unfortunately, it seems that, at that time, the instancing features where "minimally" implemented and some stuff was missing/broken. Fortunately, Redshift developer Juanjo was very responsive and very quickly, fixed all the issues I found. Discussion can be found in this thread (maxon account required).
Didn't tested the latest version but I think you should now get the same features other render-engine have. I'm just not sure if arbitrary attributes still need to be in instance.arbitrary or is the commonly used geometry.arbitrary supported ?
Outro
And that's a wrap. Very happy to have finally published this tutorial that was hanging around for 4 months already haha. A topic that I could have explored in this post is USD, which is an additional solution for instancing. But having no experience at all with the format I will let you do the research.
I really hope this was useful for you because this was the kind of information I wish I had when starting looking for instancing ! As always feedback is welcome. If you notice anything let me know on the PYCO discord (link in the page's footer) or just e-mail me.
Discord - Foundry Katana (Community)
A community Discord server for Katana with official Foundry staffs.