
In our last post, we added 2 elevation levels to our map. To enhance the elevation effect, we want to shade certain sides of our terrain to invoke the idea of lighting.
Cornering
Before we do that, let’s clean up our meta template to shave off the sharp corners and get us some finer control.
In the end I a added 4 new zones:
- east-corner
- west-corner
- east-shared-transition
- west-shared-transition
I recolored the tilemap so that the colors are further appart
Added the new colorings to our yaml
colors:
# base colors
# ../
mustard: &mustard "#888800"
olive: &olive "#666600"
khaki: &khaki "#444400"
ochre: &ochre "#222200"
lime: &lime "#008800"
forest: &forest "#006600"
emerald: &emerald "#004400"
pine: &pine "#002200"
navy: &navy "#000088"
ocean: &ocean "#000066"
midnight: &midnight "#000044"
sapphire: &sapphire "#000022"
violet: &violet "#440044"
imperial: &imperial "#880088"
zones:
floor: *olive
border: *ochre
shared-east-transition: *mustard
shared-west-transition: *khaki
west_corner: *lime
sunken_west_wall: *forest
shared_west_wall: *emerald
raised_west_wall: *pine
sunken_east_wall: *ocean
east_corner: *navy
shared_east_wall: *midnight
raised_east_wall: *sapphire
sunken_north_wall: *violet
raised_south_wall: *imperial
And we updated our make files to recolor the new zones accordingly
%-flat-template: $(template_file)
convert $(template_file) \
-fill "$(call conf,terrains.$*.border.color)" -opaque "$(call conf,zones.border)" \
-fill "$(call conf,terrains.$*.floor.color)" -opaque "$(call conf,zones.floor)" \
-fill "$(call conf,terrains.$*.transition.color)" -opaque "$(call conf,zones.sunken_west_wall)" \
-fill "$(call conf,terrains.$*.transition.color)" -opaque "$(call conf,zones.shared_west_wall)" \
-fill "$(call conf,terrains.$*.transition.color)" -opaque "$(call conf,zones.raised_west_wall)" \
-fill "$(call conf,terrains.$*.transition.color)" -opaque "$(call conf,zones.sunken_east_wall)" \
-fill "$(call conf,terrains.$*.transition.color)" -opaque "$(call conf,zones.shared_east_wall)" \
-fill "$(call conf,terrains.$*.transition.color)" -opaque "$(call conf,zones.raised_east_wall)" \
-fill "$(call conf,terrains.$*.transition.color)" -opaque "$(call conf,zones.sunken_north_wall)" \
-fill "$(call conf,terrains.$*.transition.color)" -opaque "$(call conf,zones.raised_south_wall)" \
-fill "$(call conf,terrains.$*.transition.color)" -opaque "$(call conf,zones.west_corner)" \
-fill "$(call conf,terrains.$*.transition.color)" -opaque "$(call conf,zones.east_corner)" \
-fill "$(call conf,terrains.$*.transition.color)" -opaque "$(call conf,zones.shared-east-transition)" \
-fill "$(call conf,terrains.$*.transition.color)" -opaque "$(call conf,zones.shared-west-transition)" \
$*-template.png
# For RAISED terrain we merge the sunken zones to the floor color
%-raised-template: $(template_file)
convert $(template_file) \
-fill "$(call conf,terrains.$*.border.color)" -opaque "$(call conf,zones.border)" \
-fill "$(call conf,terrains.$*.floor.color)" -opaque "$(call conf,zones.floor)" \
-fill "$(call conf,terrains.$*.floor.color)" -opaque "$(call conf,zones.sunken_west_wall)" \
-fill "$(call conf,terrains.$*.transition.color)" -opaque "$(call conf,zones.shared_west_wall)" \
-fill "$(call conf,terrains.$*.transition.color)" -opaque "$(call conf,zones.raised_west_wall)" \
-fill "$(call conf,terrains.$*.floor.color)" -opaque "$(call conf,zones.sunken_east_wall)" \
-fill "$(call conf,terrains.$*.transition.color)" -opaque "$(call conf,zones.shared_east_wall)" \
-fill "$(call conf,terrains.$*.transition.color)" -opaque "$(call conf,zones.raised_east_wall)" \
-fill "$(call conf,terrains.$*.transition.color)" -opaque "$(call conf,zones.sunken_north_wall)" \
-fill "$(call conf,terrains.$*.floor.color)" -opaque "$(call conf,zones.raised_south_wall)" \
-fill "$(call conf,terrains.$*.border.color)" -opaque "$(call conf,zones.west_corner)" \
-fill "$(call conf,terrains.$*.border.color)" -opaque "$(call conf,zones.east_corner)" \
-fill "$(call conf,terrains.$*.floor.color)" -opaque "$(call conf,zones.shared-east-transition)" \
-fill "$(call conf,terrains.$*.floor.color)" -opaque "$(call conf,zones.shared-west-transition)" \
$*-template.png
# For sunken terrain we merge the raised zones to the floor color
%-sunken-template: $(template_file)
convert $(template_file) \
-fill "$(call conf,terrains.$*.border.color)" -opaque "$(call conf,zones.border)" \
-fill "$(call conf,terrains.$*.floor.color)" -opaque "$(call conf,zones.floor)" \
-fill "$(call conf,terrains.$*.transition.color)" -opaque "$(call conf,zones.sunken_west_wall)" \
-fill "$(call conf,terrains.$*.transition.color)" -opaque "$(call conf,zones.shared_west_wall)" \
-fill "$(call conf,terrains.$*.floor.color)" -opaque "$(call conf,zones.raised_west_wall)" \
-fill "$(call conf,terrains.$*.transition.color)" -opaque "$(call conf,zones.sunken_east_wall)" \
-fill "$(call conf,terrains.$*.transition.color)" -opaque "$(call conf,zones.shared_east_wall)" \
-fill "$(call conf,terrains.$*.floor.color)" -opaque "$(call conf,zones.raised_east_wall)" \
-fill "$(call conf,terrains.$*.floor.color)" -opaque "$(call conf,zones.sunken_north_wall)" \
-fill "$(call conf,terrains.$*.transition.color)" -opaque "$(call conf,zones.raised_south_wall)" \
-fill "$(call conf,terrains.$*.floor.color)" -opaque "$(call conf,zones.west_corner)" \
-fill "$(call conf,terrains.$*.floor.color)" -opaque "$(call conf,zones.east_corner)" \
-fill "$(call conf,terrains.$*.floor.color)" -opaque "$(call conf,zones.shared-east-transition)" \
-fill "$(call conf,terrains.$*.floor.color)" -opaque "$(call conf,zones.shared-west-transition)" \
$*-template.png
These extra zones give us sharp coners for the elevations, bur soft rounded corners in the flat templates and they avoid annoying artefacts:

Shading
Now that we have a new and improved meta_template, we want to extract certain sections and darken them.
Let’s start with adding some information to our config.
# summer.yaml
# ...
shading:
west: 0.5
east: 0.0
south: 0.1
north: 0.1
This config shades the west walls, and a bit of the north and south walls giving what i hope to be a bit of a sunrise effect.
Lets start with extracting the south wall. We will call it the ‘raised’ shading section, because it will only apply to raised terrains
raised-shared-shade-section: $(template_file)
convert $(template_file) \
-alpha set +transparent "$(call conf,zones.raised_south_wall)" -fill black -colorize 100 \
-channel A -evaluate multiply "$(call conf,shading.south)" +channel \
raised-shared-shade-section.png
We will extract that section, color it black and set the opacity to our config value.
This gives us a nice overlay:

Let’s do the same for the north (sunken) section:
sunken-shared-shade-section: $(template_file)
convert $(template_file) \
-alpha set +transparent "$(call conf,zones.sunken_north_wall)" -fill black -colorize 100 \
-channel A -evaluate multiply "$(call conf,shading.north)" +channel \
sunken-shared-shade-section.png
Now we need to do the east and west sections. These are a bit tricky, because we will get different results depending on whether the terrain is sunken or raised. That’s why we will pass the terrain type as a wildcard to the target:
%-west-shade-section: $(template_file)
convert $(template_file) \
-alpha set \
-fill "$(call conf,zones.$*_west_wall)" \
-opaque "$(call conf,zones.shared_west_wall)" \
+transparent "$(call conf,zones.$*_west_wall)" \
-fill black -colorize 100 \
-channel A -evaluate multiply $(call conf,shading.west) +channel \
$*-west-shade-section.png
# and another one for the east side...
Here we have to extract two sections. So first we recolor the shared saction to the non-shared, then we can extract only that color code. So when we go:
make raised-west-shade-section
This nets us a neat map:

All we need to do now is combine 3 of the 4 together based on the terrain.
sunken-shadow-mask: raised-shared-shade-section sunken-shared-shade-section sunken-east-shade-section sunken-west-shade-section
convert sunken-shared-shade-section.png \
sunken-west-shade-section.png -compose over -composite \
sunken-east-shade-section.png -compose over -composite \
sunken-shadow-mask.png
# and another variant for the raised-shadow-mask with a different section
For flat terrains, instead of dealing with conditional, we will just create an empty transparent png that will be our “flat-shadow-mask”
flat-shadow-mask:
convert -size 512x384 xc:none flat-shadow-mask.png
Now we can make them conditional, based on the target terrain
%-shadow-mask: sunken-shadow-mask raised-shadow-mask
# a little guard to drop any invalid terrains
[ $(call conf,terrains.$*.type) != "null" ] || { exit 1; }; \
# rename the one we want
cp $$(echo $(call conf,terrains.$*.type)-shadow-mask.png) $*-shadow-mask.png
And we update our existing target, to take the shadow map as a dependency AND to apply it as a last layer on top of the other sections
%-tileset.png: %-border-cutout %-floor-cutout %-transition-cutout %-shadow-mask
convert $*-border-cutout.png \
$*-floor-cutout.png -compose over -composite\
$*-transition-cutout.png -compose over -composite \
$*-shadow-mask.png -compose multiply -composite \
$*-tileset.png
and now when we generate our tilesets again
$ make tilesets
We get nicely shaded walls.
Notice how the east and west walls are misaligned. The sunken sea is lit from the west, while the raised mountains are lit from the east. The is because the shade directions between the sunken and raised terrains need to be swapped.
Let’s determine the values dynamically based on terrain type. For this we will use the [$(if …) ](Conditional Functions (GNU make) )function and the $(filter …) function. Make a has a ton of useful functions , that I should be using more to reduce duplication
#Functions determine the shade value for the walls based on terrain type
west-shade-value= $(if $(filter sunken,$1),$(call conf,shading.west),$(call conf,shading.east))
east-shade-value= $(if $(filter sunken,$1),$(call conf,shading.east),$(call conf,shading.west))
the config declares, where the light is coming from, so if the light is coming from the east, then the sunken west walls need to be lit and the raised east walls need to be lit.
We can use the functions like this:
%-east-shade-section: $(template_file)
convert $(template_file) \
-alpha set \
-fill "$(call conf,zones.$*_east_wall)" \
-opaque "$(call conf,zones.shared_east_wall)" \
+transparent "$(call conf,zones.$*_east_wall)" \
-fill black -colorize 100 \
-channel A -evaluate multiply $(call east-shade-value,$*) +channel \
$*-east-shade-section.png
# Don't forget to update the corner shadow as well
%-corner-shade-section:
convert $(template_file) \
-alpha set \
+transparent "$(call conf,zones.$*_corner)" \
-fill black -colorize 100 \
-channel A -evaluate multiply $(call $*-shade-value,sunken) +channel \
$*-corner-shade-section.png
And if we run our tileset again:
Nice!
If you think this last part was confusing, you’re right! It’s because west/east in the config refers to the direction the light is coming from while west/east in the makefile refers to the position of the walls. I’m bad at languages and don’t have the brain power to fix it so adding a bunch of functions to duct-tape the issue should be enough. Just don’t look at it too much
Finally let’s add a global shading that we can customize for color and opacity. let’s add it to our config:
shading:
global:
color: *black # Anchors! Yay!
opacity: .2
west: .0
east: .8
south: 0.4
north: 0.4
Now make a single overlay images based on the parameters
global-shadow-mask:
convert -size 512x384 "xc: $(call conf,shading.color)" \
-alpha on \
-channel A -evaluate multiply $(call conf,shading.global) +channel \
global-shadow-mask.png
And add it to our terrain stackstack:
%-tileset.png: %-border-cutout %-floor-cutout %-transition-cutout %-shadow-mask global-shadow-mask
convert $*-border-cutout.png \
$*-floor-cutout.png -compose over -composite\
$*-transition-cutout.png -compose over -composite \
$*-shadow-mask.png -compose multiply -composite \
#Here it is
global-shadow-mask.png -compose multiply -composite \
$*-tileset.png
I kept the wall shadows black with opacity, but you can adjust it to use the global color instead if that looks better.
Conclusion
The combinatorics are getting a bit out of hand we are doing a ton of duplication even though most of the stuff is configured through yaml.
I’m starting to run into Make’s quirks where instructions are processed in different passes which often prohibits me from using certain features dynamically.
I have to think about which phase a certain condition is being executed. This is the reason I wanted to use something declarative and functional like make a.o.t. imperative build tools.
Next steps
I still need to scale the textures down to 32x32 for the target Tiled tileset.
I still need to generate the tileset dynamically
I might start exploring Make’s templating. But i think I will instead rewrite the file in Just . That seems like a more appropriate tool for the job.
I want to add some drop shadows using the new color keys
I still need to make sure dimensions are calculated dynamically by reading the inputs
The reason I wanted to use the bigger tile set template is to be able to add variation, but i am not looking forward to having to edit tile on the template individually if i want to create more or less pronounced zones for certain tilesets. I think ill have to generate the template itself from a reduced prime-template.

