Building on top of other answers, and especially theStandard's idea, I came up with **Texture Atlas** approach. 1. For each texture type (albedo, normals etc.) combine all variants to a single file, e.g. if you have 3 base color textures and 3 normal textures, combine them to 1 base color texture and 1 normal texture. For example, you could combine albedo textures vertically like this: [![][1]][1] 2. Create a default material, select *Principled BSDF* node, press <kbd>CTRL</kbd> + <kbd>SHIFT</kbd> + <kbd>T</kbd> for default setup (you need to have *Node Wrangler* enabled in preferences->addons). [![2]][2] 3. UV map the mesh to the first texture in the file: [![3]][3] 4. Changing the *Y location* in mapping will now scroll through the texture atlas: [![4]][4] You can notice that `0.33 m` will point to the 2nd texture, and `0.66 m` will point to the last one. That's because there are 3 textures, and each takes `1/(number of textures) m` height. 5. Add a *Value* node, rename it to `Number of textures` for future reference and set the value to `3` (as in this example there's 3 textures in the atlas) [![5]][5] 6. As in Greg Zaal's answer, add *Object Info* node for the *"Random"* value. 7. Add *Math* node and multiply *Random* by number of textures, to convert our random range from `0 <= x < 1` to (in this example) `0 <= x < 3`: [![6]][6] 8. Add another *Math* node, to *Floor* the previous value, converting it from `0 <= x < 3` to `x ∈ {0, 1, 2}`. The following node setup simply converts a value in range `0..1` (excluding 1) to a non-negative integer lower than the integer we provided (marked red on the screenshot). [![7]][7] 9. As mentioned in p. 4. The offset for each texture is 0.33 or 1/3 m. We could just input it as another value, but it's better to calculate it automatically, so we don't have to manually update it, when we change the number of textures in the atlas (and so only the node added in p. 5. will have to be manually updated): [![8]][8] 10. Now let's multiply one by another to convert `x ∈ {0, 1, 2}` to `x ∈ {0, 0.33, 0.66}`, and then use *Combine XYZ* node, as we can't put our Y directly to *Mapping* node [I also rearranged the nodes visually a little]. [![9]][9] Final node setup: [![10]][10] And their effect: [![11]][11] **How to make the atlas two-dimensional** If you have a 3x3 grid of textures in your atlas, you have 9 possible textures, so you update `Number of Textures` to `9`. So now the multiply -> floor nodes will produce a number in {0, 1, 2, 3, 4, 5, 6, 7, 8}. If you write down a table on how they correspond to the atlas, you will get this: | y / x | x:0 | x:1 | x:2 | |-------|-----|-----|-----| |**y:0**| 0 | 1 | 2 | |**y:1**| 3 | 4 | 5 | |**y:2**| 6 | 7 | 8 | A standard way to convert index to row index is to make an integer division of the index by the number of columns. A standard way to convert index to column index is to calculate the integer division remainder (so-called *modulo operation*) - 0 // 3 = **0** (remainder: **0**) - 1 // 3 = **0** (remainder: **1**) - 2 // 3 = **0** (remainder: **2**) - 3 // 3 = **1** (remainder: **0**) - 4 // 3 = **1** (remainder: **1**) - 5 // 3 = **1** (remainder: **2**) - 6 // 3 = **2** (remainder: **0**) - 7 // 3 = **2** (remainder: **1**) - 8 // 3 = **2** (remainder: **2**) (first bold number above is **Y** coordinate, second is **X** coordinate) Now when we change the texture atlas and update the UV: [![][12]][12] We can use this node setup: [![][13]][13] It's quite ugly, we can simplify this part: [![][14]][14] Final result: [![][15]][15] Other than that, the setup would be much simpler if it just randomized X and Y separately - but that would assume the last row is full, as opposed to having e.g. 8 elements in a 3x3 grid where the bottom-right corner is empty. Update: To get a second random value don't listen to my comments under the clamping, as that can give uneven distribution. You can just pass your random value to a 1-dimensional noise, but that also can give you uneven distribution. So I'm using a setup like this: [![][16]][16] [1]: https://i.sstatic.net/BZQVH.png [2]: https://i.sstatic.net/z7C7C.png [3]: https://i.sstatic.net/HMqmh.png [4]: https://i.sstatic.net/nns39.gif [5]: https://i.sstatic.net/zDvsF.png [6]: https://i.sstatic.net/RITM9.png [7]: https://i.sstatic.net/2g1FT.png [8]: https://i.sstatic.net/4HasY.png [9]: https://i.sstatic.net/iLN22.png [10]: https://i.sstatic.net/fbXVF.png [11]: https://i.sstatic.net/DHSAv.png [12]: https://i.sstatic.net/SSFkr.png [13]: https://i.sstatic.net/ZhS4p.png [14]: https://i.sstatic.net/mPsRG.png [15]: https://i.sstatic.net/aq3I2.png [16]: https://i.sstatic.net/fnV0R.png