Map image across particles in Cycles

Map image across particles in Cycles

Introduction

I needed a way to easily texture a bunch of duplicated objects (in this case keys on a keyboard, or even multiple keyboards), preferably in an easily repeatable and (some what) procedural way, for a project I’m working on.

I did some research but couldn’t really find anything on it and based on that I’ve done something similar before I decided to try to do it myself. The final setup isn’t very complicated, but it took some trial and error to get it to work as wanted (mainly because the nodes isn’t the easiest way to do math, there’s some math functions missing [that you usually have when programming] and there is no easy way to debug).

Using the mapping node

Enough talking! You just want to get the creative part, right‽
If you don’t want to setup the nodes yourself (see below), you can just download the .blend (it’s free!).

The .blend contains the node setup as a node group called UVGridMap that you can just import into your own project, the file also contains an example with object and particle system (seen in image above).

1. The object and Particle system

First off you need an object that is UV unwrapped, exactly what (a cube is fine) and how is up to you. Just note that it might be beneficial to have a small margin around the UV map, depending on the texture you use.

If you want to have the simple grid setup you first need an emitter, a simple plane will do fine (in my case, I used two array modifiers to create a plane that you can easily change the size of, you don’t actually need that, you can just use a single faced plane). Then you need to add a particle system to that object (once again, you can import the Particle system from the .blend provided, or set it up yourself).

We start with setting the Start and End frames for the particle system to 1 (making sure that all particles appears at once). It might also be useful to increase the lifetime, so you’re sure that it doesn’t disappear.

You can use Jittered distribution and set Particles/Face to 1 (then you need to use an array modifier or turn the emitter plane into a grid), but that is a bit unpredictable. Instead we set it to Grid. The Resolution is the number of particles on the longest edge of the grid. So if you want a 10×10 grid, simply set it to 10. If you want a 10×15 (or 15×10) grid, set it to 15 and make sure your emitter plane has a 3:2 ratio (to keep the spacing even). The particles/objects will be distributed evenly across the length and width of the emitter.

The Grid distribution also have an option for Hexagonal Grid, which can be interesting if you want to do something like a keyboard.

Next, make sure to turn off any physics, velocity etc.

The last thing we’ll do with the particle system is to set the output/render to Object and select the object we want to use. I set the Size to 1 and to use the object’s Rotation (there could be a point to also enable the Scale) to make the objects be in the right orientation.

If you want to you can disable the render of the Emitter (and turn on rendering for Unborn and Died particles)

2. Setup the material

Now you should have a nice grid, but a quite boring one. It’s time to add a material to the object(s) and it’s here that the magic happens!

Make sure you have your object (and not the object with the particle system), add a material if there isn’t one already. Then bring out the node editor.

Start with adding the node group for the mapping (UVGridMap) (import it first, if you haven’t done so). Then we need to a Particle Info node and a Texture Coordinate node (both can be found under Input in the Add menu). We connect the Index output from Particle Info to the Index input on the mapping node and UV output form Texture Coordinate to the Vector input on the mapping node.

Then we add the texture (It might be useful to start with a UV grid) we want to use, connect it to a shader (in the example, I’m using the PBR Dielectric setup from BlenderGuru, slightly modified). We connect the Vector output from the mapping node to the Vector input of the Image Texture node.

The last step is to adjust the number of Rows and Columns, these values should match the number of rows and columns in the texture, and not how you have the particle system set up, for best effect (otherwise it well get misaligned). The values should also be integers/whole numbers, to avoid any problems.
If your particle system has more rows/columns than the texture, the texture should just wrap and go on

When your done, the node setup should look something like this:

3. Caveats etc

This setup is still relatively untested, so if you manage to break it, please tell me and I’ll see what I can do to fix it!

The first particle is the one in the bottom left corner, this is fine since this is also where origin (0,0) of the texture coordinates is. But could be good to keep in mind if you do something else than a simple grid.

4. Going further

This node group have potential beyond a grid system.

You could, as an example, use it on a single object and animate (just make sure that you don’t hit fractions) the index value and then use a texture as a sprite sheet.

Play with it and create new awesome things!

Setting up the nodes yourself

So you’re a DIY:er, ehh, like to do it by yourself‽
I get it, I’m the same!

1. The Logic / What we actually are doing

You can find the final node setup at the bottom, but first, let’s talk a bit about what it actually does. The whole process could be described in pseudo code like this:

def map( int index, int rows, int cols, Vector vector )
    float xOffset = 0, yOffset = 0

    xOffset = floor( index / rows ) / cols
    yOffset = modulo( index, rows ) / rows

    vector.x = vector.x / rows + xOffset
    vector.y = vector.y / cols + yOffset

    return vector
end

If the index is 12 and rows and cols both are 10, the values should be 0.1 for xOffset and 0.2 for yOffset.

Or in text:

Scale the vector/UV down by the amount of rows and columns we have (“slicing” the texture in say 10×10 pieces).
Then move the vector/UV with a (percentual) offset in each axis based on the current cell (column and row, calculated from the index).

2. Implementing it as nodes

The implementation as nodes is pretty straight forward, but there is some things that we need to keep in mind (NB: I will take this slowly, if you feel comfortable, you can just jump a bit and just look at the screenshots as guides).

The first thing is how we access (and “put back”) the X and Y coordinates, we do this by using the Separate XYZ and Combine XYZ, which takes a Vector and gives us the individual components (XY, Z), and vice versa combines them back to a Vector. Just add those two nodes (both can be find under Converter in the Add menu), connect the Z output directly to the Z input.

Before we go further, we’ll create a node group of the nodes: Select both nodes and hit Ctrl+G.

Connect the Vector input of the Separate XYZ to the first input of the Group inputs and vice versa the Vector output of the Combine XYZ to the Group output.

The rest is all done with several Math nodes, so add one from Converter in the Add menu (after that we can just duplicate the node with Shift+D). Set the operator of the Math node to Divide and duplicate it, putting it under the first one.

Connect X from Separate XYZ to the top input to one of the Divide nodes and Y to the top input of the other Divide node.
We then connect the bottom inputs to the Group input node.
Now is a good time to name the inputs, go to the top of the right panel (hit N if you don’t see it) and select the values and call them (you can either double click on it or go to the Name field) Rows (the one connected to “Y divide”) and Columns (the one connected to “X divide”).

Now we select both Divide nodes, duplicate, move them to the right and change the operator to Add. Connect the outputs from the Divide nodes to the top inputs of our new Add nodes. Before we move on we connect the output of the Add nodes to the Combine XYZ node.
If you want, you can select all the nodes between the group input/output nodes and hit H to minimize them.


3. Calculate the offset

The next thing we need to do is calculate the offsets, this is pretty straight forward, but with one small bump: In the pseudo code above we used a function called floor (which you often have access to in programming, it rounds a number, like 2.7, down, to 2), but it there is no function in the Cycles math node. There is round, but it would round any fraction with more than .5 upwards (so 2.7 would become 3, not 2), which would cause troubles in this case. We solve this by using the Modulo function (which in Cycles includes the fraction part) and then Subtract.

To action: Add a Math node (or duplicate one of the ones we already created) and set it to Divide. Connect the top input to a new value on the Group input node (call this new input Index) and connect the second input on the Divide node to Rows on the Group Input.

Create a new Math node, set it to Modulo. Connect the output from the Divide node to the top input of the Modulo node and set the second input to 1 (this will return the fraction part of the number: 2.7 -> 0.7). Add another Math node and set it Subtract, connect the output of the Divide node to the top input and the output of the Modulo node to the second input. We have now imitated the function of floor!

To finish this chain, add another Math node, set it to Divide and connect the output of the Subtract node to the top input and Columns from the Group Input to the second input (dividing the calculated value by the number of columns).
We can then connect the output of the last Divide, to the second input of the Add for the X value we created in the first step.

Now we just need to calculate the row/Y value, this is a bit simpler as there’s just two nodes in this chain.

Add another Math node and set it to Modulo (or just duplicate the previous one). Connect the Index value to the top input and Rows to the second input. Add a Division node and connect the output from the Modulo to the top input and Rows to the second input.
Finish it off by connecting the output of the Division node to the previously created Add node for the Y value.

You are now done! Your finished node group should look something like this (I’ve added some reroutes and frames to make it clearer/more descriptive).

4. The end

You can now use your group node in your projects, just connect it up as previously described (or however you want, you’re the artist!).

If you have any questions or suggestions, feel free to contact me!

Leave a Reply

Your email address will not be published. Required fields are marked *