• Unity
  • Use Premultiplied Alpha when texture packing normal maps?

My project uses Unity's 2D lights system with the new Spine shaders (namely the URP/2D/Spine/Sprite shader). When I pack my regular spine images, I use Premultiplied Alpha. When packing my normal-map images into their own atlas, I also was using Premultiplied Alpha - I'm wondering if this is correct to do or not?

Related Discussions
...

You have to be very carefol when packing normal maps in any texture packer. You have to be sure to prevent image rotation while packing, otherwise your normal orientation will be off by 90 degrees in these parts.

Regarding your question above:
Premultiply alpha is only used on the color output channel, for use with additive blend mode.
Premultiply alpha makes no real sense with normalmaps, as it effectively bends the normal to face in negative X and Y direction. When fetching normal info, there is no such thing as transparency. So in short, you should disable it when packing your normalmaps (unless you want some strange lighting effect in semi-transparent regions).

Harald wrote

You have to be very carefol when packing normal maps in any texture packer. You have to be sure to prevent image rotation while packing, otherwise your normal orientation will be off by 90 degrees in these parts.

Regarding your question above:
Premultiply alpha is only used on the color output channel, for use with additive blend mode.
Premultiply alpha makes no real sense with normalmaps, as it effectively bends the normal to face in negative X and Y direction. When fetching normal info, there is no such thing as transparency. So in short, you should disable it when packing your normalmaps (unless you want some strange lighting effect in semi-transparent regions).

Thanks Harald!

Yeah, that makes total sense - and I did indeed have a bit of weird lighting at the edges, which is now fixed 8)

Also, thanks for the other tips - I've had to disable both rotation and alias'ing (since we have some images that are slight color alterations, but their normal maps end up exactly the same). Its tricky/aggravating when you're packing hundreds of images across multiple folders, and your source images atlas doesn't end up looking like your normalmap atlas :bigeye: I still plan on doing a Start-To-Finish guide on Spine + Unity's lights, including making and packing the normalmaps. Hopefully will have some time next month 🙂

Glad it helped! Sorry to hear that the whole normalmap packing procedure is somewhat tedious. And thanks in advance for sharing any best-practice info!

7 months later

Hey Jamez, did you ever find the time to make a guide on this topic? I'm currently running in to the same questions it seems you figured out months ago. Haha

SquaLLio wrote

Hey Jamez, did you ever find the time to make a guide on this topic? I'm currently running in to the same questions it seems you figured out months ago. Haha

Hey Squal - I'm really busy with my project for the foreseeable future so I don't have time to make a full tutorial, but I'd be glad to help with anything in particular, or if you just want a broad overview of what to do.

Where are you at with it right now? Have you made your normal maps? Packed everything? Do you just need help with the Unity implementation or with the whole process?

Jamez0r wrote
SquaLLio wrote

Hey Jamez, did you ever find the time to make a guide on this topic? I'm currently running in to the same questions it seems you figured out months ago. Haha

Hey Squal - I'm really busy with my project for the foreseeable future so I don't have time to make a full tutorial, but I'd be glad to help with anything in particular, or if you just want a broad overview of what to do.

Where are you at with it right now? Have you made your normal maps? Packed everything? Do you just need help with the Unity implementation or with the whole process?

I'm going to start the deep dive tonight, and just kinda wanted a head start. I have sprite illuminator to make the normal maps, I'm just not sure where to start with packing them and implementing in Unity. I'm using the PMA workflow, so I'm assuming I can make a shader graph that has a SampleTexture2D for the normal maps. I think I'm just a little iffy on all the packing. I only have maybe 10 or so different Spines, so ideally I would like to minimize the number of atlases. Also, not sure if I should be using TexturePacker, Spine's Texture Packer, or Unity's atlas.

Honestly, any tiny bit of information you can provide would be beyond helpful, a broad overview would be golden. I can figure out the details.

You're awesome man. Thank you for the reply.

Here is a brief overview. I would start with a single Spine animation with its own atlas, get everything working for that, and then if you want to combine multiple Spine animations into a single atlas you can do that afterward. Note that this is mostly off of the top of my head and may not be absolutely 100% complete. If you get stuck let me know.

1) Create your character in Spine as usual, test them out Unity and make sure everything works. Set the material to use the correct "lit" shader (if you're using URP with 2D Renderer, you can use the "Universal Render Pipeline/2D/Spine/Sprite" shader).

2) Make a standalone scene and add a Point Light 2D, and enable "Use Normal Map" on it. You can set the Distance to a low number (like 0.5 or 1) to make the effect of the normal maps really stand out for testing purposes. But since you haven't set up your normal map yet, right now it should just light up the Spine animation as though it was flat.

3) Create a folder called "normalmap" next to your source images folder for your Spine project. Open Sprite Iluminator, drag all of your source images in, and make your normal maps. Export into the "normalmap" folder, with Transparent Background.

4) From now on, you'll want to export your Spine projects "data" separately from its "atlas" stuff. So when you do Spine Menu -> Export... you'll want to uncheck the "Texter Atlas: Pack" button. So when you run the Export this way, you'll only get the ".json" or ".skel.bytes" files.

5) Now you'll want to pack your two atlases (the 'regular source images', which I'll call the Albedo atlas for now, and then the normalmap atlas). You can start by doing Spine Menu -> Texture Packer, and selecting the images folder for the Albedo images, configuring the settings, and Packing. And then the same for the Normalmap images. So you will end up with a .png + .atlas for the Albedo, and a .png + .atlas for the NormalMap.

The goal here is to get the normal map .png to look EXACTLY the same as the Albedo .png. A single pixel difference between the albedo images and the normalmap images will make it so the images do not match up.

For the Albedo packer settings I did this:
-Disable Strip Whitespace, Rotation, and Alias
-Alpha Threshold = 0
-Premultiply Alpha

For the NormalMap packer settings I did this:
-Same settings as Albedo packer, but without Premultiply Alpha checked.

If everything is set up properly, you should be able to open the exported .pngs of the Albedo and the NormalMap side by side, and see that they match up perfectly.

6) At this point its probably best to delete your entire Spine animation setup and files from step #1, that way everything is done from scratch and there is no confusion as to what file is referencing what image.

7) Now drag in those .png + .atlas files into Unity (I like to keep them in separate folders), and it should create the Material for them. We only need to use the Material from the Albedo version - in fact, the only file we really need from the NormalMap is the .png, so I suppose you don't have to drag in the .atlas for the NormalMap at all.

8 ) Select the NormalMap .png, and change its Texture Type to Normal Map. Select the Material that was created for the Albedo, and set the Shader to the correct shader. Now drag the NormalMap .png into the Normal map slot.

9) So now you have your Material prepped, but no Spine animation files yet. Drag your .skel.bytes into Unity -> it may pop up a warning saying "atlas not found", if so, point it at your Albedo atlas.

10) Now you should be able to create a Skeleton Animation by dragging the SkeletonData file into the Scene, and it should be using your Albedo atlas, and the Albedo atlas' Material. On the SkeletonAnimation component, you will need to expand the Advanced section, and check the two boxes "Add Normals" and "Solve Tangents".

So if you have the 2D Point Light in the scene, with Use Normal Map selected, it should be lighting it up correctly.

Once you have that process working, you can then look into how to Texture Pack the source images of multiple projects into a single Albedo atlas (and doing the same with the normal map images) if you want multiple Spine animations to share a single atlases (the pair of Albedo + NormalMap atlases). I use one script to export my Data, and another to pack/export my Albedo + Normalmap atlases.

If you have any specific questions with the texture packing I can help, but unfortunately don't have time right now to go through it all. There's a documentation page for exporting using command line, so check that out. Here is a copy/paste of my two .bat folders that I use for a character. This character has a frontside skeleton and a backside skeleton in the same Spine Project, and has a normal map. I'm not sure if uploading these scripts will be helpful or just more confusing haha, but it might help in some way.

The DATA EXPORT .bat file:

REM ***Set the path below to the folder that contains Spine.exe***
SET spineExecutablePath="C:\Program Files (x86)\Spine\Spine"

REM ***Set the path of the Character's Spine Project***
SET characterSpineProject="E:\GardenfiendGamesDropbox\Dropbox\Perennial Order\Animation\Spine Projects\Sandir\Knight Sandir Felix_1.11.spine"

REM ***Set the path of the Character's Local Output Folder***
SET characterOutputFolder="E:\GardenfiendGamesDropbox\Dropbox\Perennial Order\Animation\Spine Projects\Sandir\export"

REM ***Set the path of the Export Settings***
SET characterExportSettings="E:\GardenfiendGamesDropbox\Dropbox\Perennial Order\Animation\Export Packing Settings\BINARY_ONLY_USE_SCRIPT_OUTPUT_DESTINATION.export.json"



%spineExecutablePath% 

---

input %characterSpineProject% 

---

output %characterOutputFolder% 

---

export %characterExportSettings%



REM ***Copy this section for each Skeleton.**
REM ***Enter name of Skeleton and its Unity Folder Path***

SET skeletonName="sandir_new_front"
SET skeletonUnityFolder="C:\Users\HYDRALISK\Unity Projects\Perennial Order\Assets\Graphics\Spine\Characters\Sandir\Frontside"
SET skeletonNameFullPath=%characterOutputFolder:"=%\%skeletonName:"=%
COPY "%skeletonNameFullPath%.skel.bytes" %skeletonUnityFolder%

SET skeletonName="sandir_new_back"
SET skeletonUnityFolder="C:\Users\HYDRALISK\Unity Projects\Perennial Order\Assets\Graphics\Spine\Characters\Sandir\Backside"
SET skeletonNameFullPath=%characterOutputFolder:"=%\%skeletonName:"=%
COPY "%skeletonNameFullPath%.skel.bytes" %skeletonUnityFolder%

PAUSE


REM NOTES
REM When using a variable, encase in % (ex: %variableName%)
REM Remove quotes from strings when using variable by adding :"= (ex: %variableName:"=%)

The ATLAS EXPORT .bat file:

REM ***Set the path below to the folder that contains Spine.exe***
SET spineExecutablePath="C:\Program Files (x86)\Spine\Spine"


REM ***Texture Atlas Packing**
SET atlasName="sandir_new_front"
SET pathToImages="E:\GardenfiendGamesDropbox\Dropbox\Perennial Order\Animation\Spine Projects\Sandir\images\front"
SET pathToOutput="E:\GardenfiendGamesDropbox\Dropbox\Perennial Order\Animation\Spine Projects\Sandir\export"
SET pathToPack="E:\GardenfiendGamesDropbox\Dropbox\Perennial Order\Animation\Export Packing Settings\UNITY_CHARACTER_PACKER.pack.json"
SET atlasUnityFolder="C:\Users\HYDRALISK\Unity Projects\Perennial Order\Assets\Graphics\Spine\Characters\Sandir\Frontside"
SET atlasLocalFullPath=%pathToOutput:"=%\%atlasName:"=%
%spineExecutablePath% 

---

input %pathToImages% 

---

output %pathToOutput% 

---

name %atlasName% 

---

pack %pathToPack% 
COPY "%atlasLocalFullPath%.atlas.txt" %atlasUnityFolder%
COPY "%atlasLocalFullPath%.png" %atlasUnityFolder%

SET atlasName="sandir_new_back"
SET pathToImages="E:\GardenfiendGamesDropbox\Dropbox\Perennial Order\Animation\Spine Projects\Sandir\images\back"
SET pathToOutput="E:\GardenfiendGamesDropbox\Dropbox\Perennial Order\Animation\Spine Projects\Sandir\export"
SET pathToPack="E:\GardenfiendGamesDropbox\Dropbox\Perennial Order\Animation\Export Packing Settings\UNITY_CHARACTER_PACKER.pack.json"
SET atlasUnityFolder="C:\Users\HYDRALISK\Unity Projects\Perennial Order\Assets\Graphics\Spine\Characters\Sandir\Backside"
SET atlasLocalFullPath=%pathToOutput:"=%\%atlasName:"=%
%spineExecutablePath% 

---

input %pathToImages% 

---

output %pathToOutput% 

---

name %atlasName% 

---

pack %pathToPack% 
COPY "%atlasLocalFullPath%.atlas.txt" %atlasUnityFolder%
COPY "%atlasLocalFullPath%.png" %atlasUnityFolder%

SET atlasName="sandir_new_front_normalmap"
SET pathToImages="E:\GardenfiendGamesDropbox\Dropbox\Perennial Order\Animation\Spine Projects\Sandir\normalmap\front"
SET pathToOutput="E:\GardenfiendGamesDropbox\Dropbox\Perennial Order\Animation\Spine Projects\Sandir\export_normalmap"
SET pathToPack="E:\GardenfiendGamesDropbox\Dropbox\Perennial Order\Animation\Export Packing Settings\UNITY_CHARACTER_NORMALMAP.pack.json"
SET atlasUnityFolder="C:\Users\HYDRALISK\Unity Projects\Perennial Order\Assets\Graphics\Spine\Characters\Sandir\Frontside"
SET atlasLocalFullPath=%pathToOutput:"=%\%atlasName:"=%
%spineExecutablePath% 

---

input %pathToImages% 

---

output %pathToOutput% 

---

name %atlasName% 

---

pack %pathToPack% 
COPY "%atlasLocalFullPath%.atlas.txt" %atlasUnityFolder%
COPY "%atlasLocalFullPath%.png" %atlasUnityFolder%

SET atlasName="sandir_new_back_normalmap"
SET pathToImages="E:\GardenfiendGamesDropbox\Dropbox\Perennial Order\Animation\Spine Projects\Sandir\normalmap\back"
SET pathToOutput="E:\GardenfiendGamesDropbox\Dropbox\Perennial Order\Animation\Spine Projects\Sandir\export_normalmap"
SET pathToPack="E:\GardenfiendGamesDropbox\Dropbox\Perennial Order\Animation\Export Packing Settings\UNITY_CHARACTER_NORMALMAP.pack.json"
SET atlasUnityFolder="C:\Users\HYDRALISK\Unity Projects\Perennial Order\Assets\Graphics\Spine\Characters\Sandir\Backside"
SET atlasLocalFullPath=%pathToOutput:"=%\%atlasName:"=%
%spineExecutablePath% 

---

input %pathToImages% 

---

output %pathToOutput% 

---

name %atlasName% 

---

pack %pathToPack% 
COPY "%atlasLocalFullPath%.atlas.txt" %atlasUnityFolder%
COPY "%atlasLocalFullPath%.png" %atlasUnityFolder%


PAUSE


REM NOTES
REM When using a variable, encase in % (ex: %variableName%)
REM Remove quotes from strings when using variable by adding :"= (ex: %variableName:"=%)

Let me know if you get stuck anywhere. Good luck!


Ah, forgot to mention that there aren't any ShaderGraph shaders for Spine. I believe the Spine lit shaders use some custom functions for determining the direction of the Normals, so you can't just use Unity's default "sprite lit" functions if you are wanting to use a Normal Map.

I've set up a dissolve shader with ShaderGraph that works with Spine Animations and is "lit" (using Unity's default lighting function), but I can't use a normal-map with it.

I'm planning on looking into how to combine a custom ShaderGraph shader with the Spine "lit" shader, but probably won't be doing that for another month or so. I believe it was suggested on the forums here that you can use ShaderGraph to create the code for the shader, and then modify the code to add in the correct spine lighting.

Wow, thank you for such a detailed run down, because of this, I have an idea how I'm going to accomplish what I need. I didn't expect you to spend that much time on it, and I feel bad. For your time and efforts, I am truly grateful.

I think I've managed to create a Shader Graph that does a decent job of accounting for normals, and with some dissolve functionality. Here is an example...

https://www.youtube.com/watch?v=rvfA7l8VgrA&feature=youtu.be&ab_channel=2BeeGames

I'm not sure if it's accurate or not, and there's a few things that feel off. It's not perfect, but it's a start. It appears to look the same, as far as normal map goes, as the Spine URP Sprite shader you suggested to use. It also seems to play well with animation.
One thing I don't like is that without a dim global light, it's too dark, regardless of distance on the point light. (feel like it's something stupid I'm missing)
The distance you're seeing in the video is 0, anything above around 0.5 doesn't even show on the fish.
I don't know enough to know whether it's expected behavior or not. The 2d light system, especially normal mapped sprites, are completely new to me.

Here is a screenshot of the shader graph used here. I'm willing to bet you have the knowledge to improve upon it for your own project. The normal map implementation is so simple, it makes me feel like I'm missing something on why this wouldn't work?

Edit 1: On further testing, you're right! This shader doesn't work if you use Spine's Initial Flip X, or Initial Flip Y. It doesn't change the normal direction to account for it, or it seems that way to a novice. It's the opposite of the expected result.
Transform rotation and scale -1 works fine. Unfortunately, that's not going to work considering how much I have to use Skeleton.ScaleY in the project. It's back to the drawing board I suppose.

Edit 2: Figured out that if you check "Add Normals" and "Solve Tangents" in the SkeletonAnimation component, it will properly calculate the normals on the Sprite Lit Master node from 2d Shader Graph.
Here is an example of the dissolve shader, with an exaggerated normal map to demonstrate easier. Also, using Skeleton.ScaleY and ScaleX...

https://www.youtube.com/watch?v=oxYn3nyFg3Y&feature=youtu.be&ab_channel=2BeeGames

SquaLLio wrote

Wow, thank you for such a detailed run down, because of this, I have an idea how I'm going to accomplish what I need. I didn't expect you to spend that much time on it, and I feel bad. For your time and efforts, I am truly grateful.

I think I've managed to create a Shader Graph that does a decent job of accounting for normals, and with some dissolve functionality. Here is an example...

https://www.youtube.com/watch?v=rvfA7l8VgrA&feature=youtu.be&ab_channel=2BeeGames

I'm not sure if it's accurate or not, and there's a few things that feel off. It's not perfect, but it's a start. It appears to look the same, as far as normal map goes, as the Spine URP Sprite shader you suggested to use. It also seems to play well with animation.
One thing I don't like is that without a dim global light, it's too dark, regardless of distance on the point light. (feel like it's something stupid I'm missing)
The distance you're seeing in the video is 0, anything above around 0.5 doesn't even show on the fish.
I don't know enough to know whether it's expected behavior or not. The 2d light system, especially normal mapped sprites, are completely new to me.

Here is a screenshot of the shader graph used here. I'm willing to bet you have the knowledge to improve upon it for your own project. The normal map implementation is so simple, it makes me feel like I'm missing something on why this wouldn't work?

Edit 1: On further testing, you're right! This shader doesn't work if you use Spine's Initial Flip X, or Initial Flip Y. It doesn't change the normal direction to account for it, or it seems that way to a novice. It's the opposite of the expected result.
Transform rotation and scale -1 works fine. Unfortunately, that's not going to work considering how much I have to use Skeleton.ScaleY in the project. It's back to the drawing board I suppose.

Edit 2: Figured out that if you check "Add Normals" and "Solve Tangents" in the SkeletonAnimation component, it will properly calculate the normals on the Sprite Lit Master node from 2d Shader Graph.
Here is an example of the dissolve shader, with an exaggerated normal map to demonstrate easier. Also, using Skeleton.ScaleY and ScaleX...

https://www.youtube.com/watch?v=oxYn3nyFg3Y&feature=youtu.be&ab_channel=2BeeGames

:detective: :detective:

Could you check if rotating bones (rather than flipping/scale) is also accounted for with the direction of the Normals?

From the documentation:

Add Normals. When enabled, the mesh generator adds normals to the output mesh. Enable if your shader requires vertex normals. For better performance and reduced memory usage, you can instead use a shader such as the Spine/Skeleton Lit shader that assumes the desired normal. Note that the Spine/Sprite shaders can be configured to assume a Fixed Normal as well.
Solve Tangents. Some lit shaders require vertex tangents, usually for applying normal maps. When enabled, tangents are calculated every frame and added to the output mesh.

The "Add Normals" says to enable if your shader requires vertex normals. Is that correct to use for our case? AKA would that be used for vertex-lit rather than pixel-lit? I'm not sure. Maybe Harald can chime in when he gets back.

I did a bit more testing and I think I'm getting the expected result. Here's another video example of some obvious bone rotations. Also, I increased the volume opacity, slightly, on the point light to better illustrate exactly where the light is hitting.

https://www.youtube.com/watch?v=npsGlw8g9YA&feature=youtu.be&ab_channel=2BeeGames

I, too, would like to hear Herald's input on this. After I read the documentation I didn't expect the result I'm getting, but I decided to try it anyway... ya know... for science.

Edit: It even plays nice with vertex displacement shader...

https://www.youtube.com/watch?v=iNeVbPGqLoM&feature=youtu.be&ab_channel=2BeeGames

Harri's currently on vacation and will get back to you on this next week. I'm afraid I don't have enough insight into the normal mapping code to give any advice.

7 days later

Wow, thanks Jamez0r for sharing your workflow! ("Here is a brief overview." is a kind of an understatement 😉 )!

I, too, would like to hear Herald's input on this. After I read the documentation I didn't expect the result I'm getting, but I decided to try it anyway... ya know... for science.

It looks correct to my eyes. The most important part is that the normals and tangents are generated by the SkeletonAnimation component. If the tangents or normals were off, it would react very strangely to rotations of object or bone, so they seem to arrive correctly.

As you said "I didn't expect the result I'm getting": Is there anything you would like to behave differently, or were you just not expecting it to work right away? 🙂

Harald wrote

Wow, thanks Jamez0r for sharing your workflow! ("Here is a brief overview." is a kind of an understatement 😉 )!

I, too, would like to hear Herald's input on this. After I read the documentation I didn't expect the result I'm getting, but I decided to try it anyway... ya know... for science.

It looks correct to my eyes. The most important part is that the normals and tangents are generated by the SkeletonAnimation component. If the tangents or normals were off, it would react very strangely to rotations of object or bone, so they seem to arrive correctly.

As you said "I didn't expect the result I'm getting": Is there anything you would like to behave differently, or were you just not expecting it to work right away? 🙂

I can see how that would be ambiguous. Haha
And yes, I was surprised to see it working perfectly with such minimal effort.

Great to hear, thanks for the clarification! 🙂