Material-based cel-shading with support for multiple light sources

So for a while now, I’ve been working on an anime style cel-shading. It’s not perfect but I thought I’d share what I have so far, so anyone who is looking for something similar could get, at the very least, a starting point for their shader. Also my brain is starting run out RAM and I’d appreciate if someone could help me improve it…

Firstly, here’s a video of what it looks like now: https://www.youtube.com/watch?v=Jt3UuPxsj4k

I used these two tutorials as my own starting point:

https://www.youtube.com/watch?v=xf21CBx8rYs


so this is how the main light source is handled in the material blueprint. (It’s basically the same as colorRamp from blender)
The amount of if-nodes translates to the amount shadow bands you’ll see, and with param values, their area can be customized per instance. There’s also support for both pure color and texture, depending on which one you want to use for the instance (I also set the texture for the opacity, so you can use textures with alpha channels.
The direction of the light is calculated from the dot product of the direction of the light source vector and normals of the mesh the material is on (using normal world space node, which you can also change to vertex world space)
The color of the light source is used to multiply the base colors of the material which, just like the direction of the light, comes through a material parameter collection.


For other light sources I just copy pasted to primary one.


And all of them are united through this lerp-spaghetti which uses the lightDistance parameter values as the lerping alpha. It feels unnecessarily complex, and I’m sure there is a cleaner way to do it, but I don’t know how. At the very least, it works… but if anyone reading this has a better suggestion for how that could be done, please share.

As for how the light sources and material parameter values are set, I made a lightBox blueprint which I attach to a specific light source (I originally thought about adding the light component inside the blueprint, but figured that, in case of spotlights, you wouldn’t want the effective area to be around the light source but where the light is aimed at).


It look like this (don’t mind my stitch work). On beginPlay it looks for any other lightboxes overlapping it and then checks if the player is overlapping it on load game (without that check the lightBox and ita effect would be ignored should the player spawn inside of a lightBox).
On playerOverlap it checks for available source slots (in my case I have 3: A, B and C, but you can add as many as you want, just more lerping on the material and a couple more parameter values on the collection) if all source slots are already taken it erases the checks and just overwrites the first slot (which is originally set for the default light source, such as the Sun). This in case are more light sources overlapping each other than the material can handle.
After that it sets its own values for parameters in the slot, direction and color at this point, and also informs all the overlapping lightboxes that that specific slot is now taken.
OnOverlapEnded it just does the same in reverse, and sets the default light source info on the parameters (default light source can be set to whatever, but generally you would set it to the directional light/Sun or whatever the main light source on the level is).


There is also the Tick which handles rotating pointlights towards player (because the direction of the light is taken from the direction of the pointlight actor. Unless of course anyone knows of a better way to do it, in which case, please share!).
It also takes care of calculating the distance of the player to the absolute light area (which by default is of size 0, equaling the center of the actor), and clamping that to fit the wanted strength of the lights effect. By default I’ve set the light strength to 1.3, because the center of the player actor will almost never at the exact center of the light source, thus meaning that the light strength will never reach 1 (full lerp value) if the maximum strength is set to 1 (so 1.2 - 1.3 tends to give the optimal results). Unless the absolute light area is bigger then 0, in which case, the maximum strength is reached inside the area.


And this how the lightbox is set in the world. The box size, control the main effect area, in which the light strength and effect will gradually increase as the player gets closer to the middle.
The absolute area ratio is the size of the inner sphere, where the strength of light is constant (it is set in construct by multiplying the value of boxSize with a value between 0.0 and 1.0.
Light strength sets the maximum wanted effect from the light (which directly translates to the lerping values in the material), and they go from 0.3 to 1.3 (in case I don’t use the absolute area).
Both light source and default light source are set from actors on the map, with light source being the one that effects the player, and default being the one it returns to once overlap ends.

Like I said, it is not perfect, for one there is quite an obvious snap coloring that happens once the player starts overlapping with the first lightbox, but trying to get rid of that just raises even worse problems. If anyone can find a fix please share.
Also really any ideas for improvement or features would be appreciated and I’m sure anyone else wanting to use this would most likely also benefit from them.
The one thing I’m currently struggling with is trying to get environmental shadows cast on the unlit material (atm, you can use a black light source with the lightbox to create a shadow effect, but it hardly works with dynamic shadows, such as those born from dynamic time of day cycling). If anyone has any ideas on how I could go about that, once again, please share.

And lastly, there probably a stuff that I forgot to mention, but you can always ask.
Even still, if anyone can get any use out of this, you’re welcome!

2 Likes