Tabletop Simulator Guide

Asset Creation Part 3 for Tabletop Simulator

Asset Creation Part 3

Overview

Continuation of the Asset Creation guide series. This is part 3 (you can read part 1 here) that will cover more in depth modelling, unwrapping, texturing, Unity components and how to tie it all together as an asset using LUA scripting.

Introduction

This guide assumes you are familiar with everything outlined in Part 1 of the series.

It is expected of you to:

  • Be somewhat familiar with a 3D modelling tool of your choosing
    Examples are Blender, Maya, 3DS Max
  • Have some knowledge about creating textures, typography and using external tools and resources
    I will be using materials from sketchuptextureclub.com or textures.com. I will also be using Photoshop CC to create some graphics and typography.
  • Have a Unity project with the TTS modding project loaded and working

I will go over creating props using a 3D model, textures, Unity components and a bit of LUA scripting to tie everything together. We will be creating everything ourselves from scratch and publishing it on the workshop. If any of the mentioned tools are not applicable to you, substitute with your own.

Creating a Mesh

Our prop, whatever it may be, needs a model. I will be making a can of spray paint for demonstration, which may seem like a boring thing to make, but we will be taking advantage of texturing techniques, trail components, looping/trigger states, object types and LUA scripting to make it behave somewhat like it would in real life.

Starting our model I will be creating a primitive 64 point circle. A spray can is just a cylinder, so we can Extrude (E), Scale (S) and Grab (G) our loop as we work upwards shaping the geometry of the can. Starting with the primitive shape, then I apply bevels (CTRL+B > Scroll Wheel to increase steps) and adjust them whenever rounded corners are applicable.

Once our shape is blocked out, I will switch the Shading to Smooth and apply an Edge Split modifier to retain hard edges and flat surfaces where I need them. You can apply more manual control over Edge Split by selecting an Edge or loop and hitting CTRL+E > Mark Sharp. I’ve made another circle and extruded it to the rough shape of a nozzle.

This has Edge Split and Smooth Shading on too. I’ve also went ahead and created two cylinder to use as Boolean objects, outlined in blue. Booleans are used to cut shapes out of a mesh using an input mesh. In this case it is important to have our Boolean modifier before edge split, as otherwise we will have issues with the smooth shading. Shading and Edge Split has an effect on our input mesh as well, and in this case I want both rounded so I will apply it to the Boolean object.

After applying the Boolean modifier it can be important to clean up excess geometry. Good practice is to select the entire mesh (A) and click Remove Doubles under Tools > Remove.
This had a few floating vertices not forming proper loops, so I merged those with vertical edges in order to select edges around the top to give it a bevel. I’ve also given the circle in the nozzle some detail.

UV/Unwrapping

Before texturing our mesh, it needs to be UV mapped/unwrapped.

I will separate the center of the cylinder because we will be texturing it with a more matte graphic rather than the metal used for the rest of the can. You can of course not separate your mesh and sometimes it is just not an option to if you have shading/geometry that relies on being attached. This is entirely a preference thing because I work with a lot of submeshes for colliders and animation. The benefit of this is that it gives you more flexibility in controling your individual segments, i.e for animation purposes. The downside is that it can in some cases cause artifacting and other issues with submeshes (not always supported), but this can usually be worked around.

I’ve marked a seam (CTRL+E > Mark Seam) hit unwrap (U) and straightened and positioning it with the help of a plugin called UVGrids.

I’m doing more or less the same thing with the metal top and bottom, except since we wont be applying a graphic to this segment we don’t need to use a straight UV map and can just normally unwrap with a marked seam and angle based unwrapping.

The cap doesn’t need to be unwrapped because I’ll be using a flat colour for it.
We are now done with unwrapping and can export the can as FBX, making sure not to include our Boolean objects. Good practice is to name our segments so we can easily distinguish between them in Unity.

Textures

Now we will import our assets in to Unity and start working on our materials.
More often than not I tend to export directly in to Unity, as dragging objects in to the editor duplicates them in to your project. For meshes, Unity accepts both exported meshes and blend files, and you do not need to triangulate them on export as Unity handles triangulation for you.

For the top metal parts I’ve downloaded a basic seamless scratched metal texture and assigned it to a material in the albedo field. I set this material tiling X and Y to 4, metallic to 1 and smoothness to 0.75 to achieve the look I want, which is a basic metal with a little bit of detail, but not too much.
Since we are tiling this material four times, we can drop down the resolution of the texture down without a discernable loss in quality. Though I like the look of it being very low at 256, so I will keep it at this.

I’ve made a cover image for the can with some basic typography in Photoshop, essentially making it a parody of an existing brand. This applies uniformly across our can as we accounted for this in the unwrapping process.

The nozzle only needs a flat colour, so I will assign it a material with a light purple to match the design of the can.

Unity Components

Backtracking a little, we forgot to make a collider for our prop. Back in blender, I will create a 16 point circle and roughly trace the shape of the can. Colliders are used for as you might have guessed, collision detection, because it is far less resource intensive to use a lower poly mesh than a high poly model for physics calculation. The geometry limit for colliders in Unity is 255 triangles, but for more accurate collisions you can use multiple colliders on child objects.

For our purposes, a rough primitive shape outlining the can is more than enough.
Having imported it in to blender, we will select our parent object and add a Component in the Inspector window. We will navigate to Mesh Collider, and drop our collider mesh in to the Mesh field.

The idea here is for the can to play a rattling sound when shaken and emit a trail when moved around. For this we will need a Trail Renderer. For more control over trail components, we should add it to an empty game object which we can add by right clicking on our can in the Hierarchy tab and clicking Create Empty. This will create an empty Game Object as a child element. We want this positioned about where paint would exit the nozzle, so we will move it to it’s correct position. Using Orthographic view and the view snapping tool in the top right can help with this.

The trail needs a material that will accept our colour parameter, so we will create a new material for our trail and set it to the Particles > Additive shader and put it in to the Materials tab.
We can adjust the curve of our trail, which controls it’s scale over it’s lifetime. We want it to start slightly larger than the nozzle and fade out to a single point, so the pictured curve works for us. For the colour parameter I’ve set it up with a gradient between three colours. The Time parameters controls the duration of the lifetime of our trail. 4 works fine for our purposes, but we might come back and tweak it later.

I want the can to rattle when shaken. We could use the script that controls the sound played on different events, such as when dice are shaken, but this won’t work for our purposes. I will go through the inputs and explain it anyways.

The TTSAssetBundleSounds script controls sounds played on events, such as on picked up, collisions, when shaken and so on. Like the AssetBundleEffects script, expand the hook you want to use, as an example, if we were to import our can as dice and have our sound in the Shake input, it would play the sound every time we shake it. Set it’s size to the amount of sounds you want to add (usually 1) and drag in the sound in to the Element tab.

I’ve used Sony Vegas to crop out a single segment of a can rattle and encoded it as a 128kbps MP3. We don’t need to be stingy with quality here, you won’t notice a difference. For music or detailed sounds you may consider encoding in a higher bitrate however. I’ve added the sound clip in the “Sound” input of a trigger effect that I’m naming “shake_sound”.

We want the trail component on Looping Effect in order to only turn it on after the can has been shaken and have it running continuously. We can add Looping and Trigger effects by adding the TTSAssetBundleEffects script to our can.

  • Looping Effects
    This is a state that runs continuously, used for permanent states such as displaying different pages in a book or snapping to a specific rotation state.
  • Trigger Effects
    These run once and stop after a set duration. We can use this for i.e sound effects and animations that we only want to run once.

For this we need to create two looping states, one without the trail and another with the trail object on it. This script will take in to account all Game Objects added to it and ignore everything that is not added to the script, meaning all objects added will be toggled and everything else will be left as displayed by default.

For the looping effects size, we input 2. I’ve labelled each as On and Off. In the On effect, we want to assign the Game Objects parameter a value of 1 (one object > our trail) and drag the component with our trail to it. Now we’re done and we can assign our AssetBundle a name and build it.

LUA Scripting

Now that we have our asset built, let’s export it and try it out.

Our trail works on the “On” looping effects and the sound plays once each time we call the trigger effect. Though currently the behavior has to be enabled manually through the right click menu, so we will write a script to try and simulate how an actual can of spray paint would behave.

We can start by defining some basic variables in function onload(). Variables contain information that we can use and manipulate, which can be anything from strings, booleans or integers. We will create two variables, one called count which we will use for counting up when the can is shaken, and held which we will use to check wether a player is holding it.

function onload() count = 0 held = false end

Then we add two functions to check for wether our can is held or has been dropped. We do this so we can only look for shaking when the can is held by a player, and to reset our count variable when it’s been dropped.

function onObjectPickedUp(self) held = true end function onObjectDropped(self) held = false count = 0 end

Following our held and count variable toggles we will make an if statement to check for velocity on the X and Z axis. Our local variable “vel” checks for changes in velocity for self (the can, since the script is attached to the object). You can roughly translate this to if velocity on X or Y equals greater than 30 then increase count by 1, and play the shake sound trigger effect. Since this function is in update, it runs every frame and will count up very quickly as our object retains some velocity as it is moved and while it’s doing that, count is continuously adding 1.

function update() local vel = self.getVelocity() checkCount() if held == true then if vel[‘x’] >= 30 or vel[‘z’] >= 30 then count = count + 1 self.AssetBundle.playTriggerEffect(0) end end end

In our update function we are also continuously calling a function called checkCount().
Here we look for whether our count variable has exceeded 100. If count has exceeded 100, then call looping effect 1 which enables the trail. Otherwise, keep it at 0, our trail off state. This means that if the can is dropped and count is at 0, it will be off. Likewise if it has not been shaken enough (4-6 shakes approx) count will not have reached 100 and it will still be off.

function checkCount() if count >= 100 then self.AssetBundle.playLoopingEffect(1) elseif count <= 0 then self.AssetBundle.playLoopingEffect(0) end end

Animation

In this segment I will be making a different asset in order to demonstrate some basics about animation (because forward thinking failed me and I didn’t account for it with the previous example).
Animations can be used to have objects move without having to use convoluted scripting, most importantly to keep the moving parts self contained within the asset. We can also animate colliders to for example close off a door or a lid.

The example I will be making is a turn tracker button for a two player game. We have a wooden border and a metal button piece in the center that rotates between the “End Turn” and “Waiting” states. For animation involving rotation it is crucial to have a pivot point exactly from where you want to rotate it. In the pictured example we have a pivot point of the metal portion of our button perfectly centered. I’ve also shown an example of a pivot point of a lid, located at a bottom of the outer edge (aligned with the cylinders of the hinges). If the origins of your meshes don’t line up you can create an empty game object, position it as your pivot point and attach your meshes to it.

Select the parent object and go to the animation tab. There will be a button labelled “Create” that will add a new animation to our object. I’ll create an animation and call it “to_wait”, as this will rotate the button to the “waiting” position.

A timeline will pop up. If you’re familiar with keyframe animation, this should make sense to you.
In short the timeline controls parameters and will tween them between two points. The button is currently at -90deg rotation on X to display the “End Turn” graphic, so we want to rotate it 180deg over to 90deg to display the “Waiting” graphic. Dragging out the red line to frame 30, we can manually input 90 in to the X rotation field. This rotation will be tweened in between these two keyframes, so moving over to frame 15 in the timeline we can see that the rotation is set to 0 > the middle of the rotation from -90 to 90.

Right below the red dot record icon in the animation tab there is a drop down menu currently displaying the name of the current animation. We want to click here and select “Create New Clip” to add our second animation for the rotation back from waiting to end turn.

Making sure to have pressed the record button, we start by changing the rotaiton value to 90 in frame one, then scroll over to frame 30 and set it to -90. Now our button goes back from “waiting” to “end turn”. Our lightweight animation work is done and we can make sure the record icon is depressed so we don’t accidentally record more actions to our animation.

For our animations to work with trigger effects without an animator we need to make sure they are set to legacy mode. If your animations show “Wrap Mode” in the Inspector then you’re good to go. Otherwise, select your animations and right click on the Inspector window and set it to debug. Here you want to tick the “Legacy” checkbox. You can switch it back to normal mode when you’re done.

We need to add an Animation (not animator) component to our parent in order to actually contain the animation data on it. Since we’re using multiple animations, we set the size to 2 and drag our animations in to the element fields.

We also want to set our animations to run on trigger effects, so we add the TTSAssetBundleEffects script to it and create two trigger effects, calling one to_waiting and the other to_end.
Expanding the animation tabs, we need to drag our parent object in to the Animation Component fields (as this is the object the animation component is on) and type in the name of our animations in the animation fields.

LUA Scripting for Animations

Following our animation example, we now want our turn tracker buttons to function as we intended without manual input through assetbundle menus.

Define that “turn_tracker_1” and 2 are our objects as found through their GUID and make a variable that we will toggle between, turn = 0. Each object needs a LUA button so we can actually click it. We will put this at the center of our button and make it’s alpha value 0 in the color parameter because it doesn’t need to be visible. We also set one of the buttons to default to the “waiting” position on load.

function onload() turn = 0 turn_tracker_1 = getObjectFromGUID(“5933ca”) turn_tracker_2 = getObjectFromGUID(“a16e8a”) turn_tracker_2.AssetBundle.playTriggerEffect(0) turn_tracker_1.createButton({ click_function=”toggleTurn”, function_owner=self, position={0, 0, 0}, height=700, width=1100, color={1,1,1,0}, label=”” }) turn_tracker_2.createButton({ click_function=”toggleTurn”, function_owner=self, position={0, 0, 0}, height=700, width=1100, color={1,1,1,0}, label=”” }) end

In our toggleTurn function we will switch between setting our “turn” variable to 0 or 1 to keep track of our two states. You can also use more semantically understandable variables such as booleans (true/false) if you wanted to, but I will stick to 0 and 1.

When we call the toggleTurn function, if var “turn” equals to 0, set one button to trigger 0 (contrary to the right click menu these start at 0) and the other to 1, and also change the turn var to 1. Next time we run this function the turn var will be 1, so we will do the opposite by flipping the trigger effects and setting the turn variable back to 0.

function toggleTurn() if turn == 0 then turn_tracker_1.AssetBundle.playTriggerEffect(0) turn_tracker_2.AssetBundle.playTriggerEffect(1) turn = 1 elseif turn == 1 then turn = 0 turn_tracker_1.AssetBundle.playTriggerEffect(1) turn_tracker_2.AssetBundle.playTriggerEffect(0) end end

Conclusion

That concludes part 3 of the asset creation guide.

The assets created for these examples can be found here[link]
If you’ve spotted any mistakes or typos then don’t hesitate to point them out in the comment section.
Enjoy.

[ PATREON ][www.patreon.com]
Special thanks to my (current and past) Patreon supporters for supporting my projects:

  • Jeremy. S
  • Platypus
  • Corrodias
  • Ash Black
  • Connor

Official Discord: [link]

SteamSolo.com