<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="../assets/xml/rss.xsl" media="all"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>.:: Marcos Dione/StyXman's glob ::. (Posts about hillshading)</title><link>https://www.grulic.org.ar/~mdione/glob/</link><description></description><atom:link href="https://www.grulic.org.ar/~mdione/glob/categories/hillshading.xml" rel="self" type="application/rss+xml"></atom:link><language>en</language><copyright>Contents © 2025 &lt;a href="mailto:mdione@grulic.org.ar"&gt;Marcos Dione&lt;/a&gt; </copyright><lastBuildDate>Sat, 15 Nov 2025 20:52:05 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>Automating blender based hillshading with Python</title><link>https://www.grulic.org.ar/~mdione/glob/posts/automating-blender-based-hillshading-with-python/</link><dc:creator>Marcos Dione</dc:creator><description>&lt;p&gt;Remember my &lt;a href="http://www.grulic.org.ar/~mdione/glob/posts/blender-hillshading-and-mapnik/"&gt;Blend based hillshading&lt;/a&gt;? I
promised to try to automate it, right? Well, it seems I have the interest and stamina now, so that's what I'm doing.
But boys and girls and anything in between and beyond, the stamina is waning and the culprit is Blender's internals
being exposed into a non-Pythonic API&lt;sup id="fnref:3"&gt;&lt;a class="footnote-ref" href="https://www.grulic.org.ar/~mdione/glob/posts/automating-blender-based-hillshading-with-python/#fn:3"&gt;3&lt;/a&gt;&lt;/sup&gt;. I swear if I worked in anything remotely close to this, I would be writing a
wrapper for all this. But in the meantime, it's all a discovery path to something that does not resemble a
hack. Just read some of
&lt;a href="https://docs.blender.org/api/current/info_quickstart.html#data-creation-removal"&gt;Blender's Python Quickstart&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;When you are familiar with other Python APIs you may be surprised that new data-blocks in the bpy API cannot be
created by calling the class:&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nx"&gt;bpy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Mesh&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;Traceback&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;most&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;recent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="nx"&gt;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;blender_console&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nx"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;bpy_struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__new__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;single&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;argument&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;This is an intentional part of the API design. The Blender Python API can’t create Blender data that exists outside
the main Blender database (accessed through bpy.data), because this data is managed by Blender (save, load, undo,
append, etc).&lt;/p&gt;
&lt;p&gt;Data is added and removed via methods on the collections in bpy.data, e.g:&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;mesh = bpy.data.meshes.new(name="MyMesh")
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That is, instead of making the constructor call this internal API, they make it fail miserably and force you to use the
internal API! Today I was mentioning that Asterisk's programming language was definitely designed by a
Telecommunications Engineer, so I guess this one was designed by a 3D artist? But I digress...&lt;/p&gt;
&lt;p&gt;One of the first thing about Blender's internals is that one way to work is based on Contexts. This makes sense when
developing plugins, where you mostly need to apply things to the
selected object, but for someone really building everything from scratch like I need to, it feels weird.&lt;/p&gt;
&lt;p&gt;One of the advantages is that you can open a Python console and let Blender show you the calls it makes for every step
you make on the UI, but it's so context based that the results is useless as a script. Or for instance, linking the
output of a thing into he the input of another is registered as a drag-and-drop call that includes the distance the
mouse moved during the drag, so it's relative of the output dot where you started and what it links to also depends on
the physical and not logical position of the things you're linking,&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;bpy.ops.node.link(detach=False, drag_start=(583.898, 257.74))
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It takes quite a lot of digging around in a not very friendly REPL&lt;sup id="fnref:1"&gt;&lt;a class="footnote-ref" href="https://www.grulic.org.ar/~mdione/glob/posts/automating-blender-based-hillshading-with-python/#fn:1"&gt;1&lt;/a&gt;&lt;/sup&gt; with limited scrollback and
not much documentation to find more reproducible, less context dependent alternatives. This is what's eating up my
stamina, it's not so fun anymore. Paraphrasing someone on Mastodon: What use is a nice piece of Open Software if it's
documentation is not enough to be useful&lt;sup id="fnref:2"&gt;&lt;a class="footnote-ref" href="https://www.grulic.org.ar/~mdione/glob/posts/automating-blender-based-hillshading-with-python/#fn:2"&gt;2&lt;/a&gt;&lt;/sup&gt;?&lt;/p&gt;
&lt;p&gt;Another very important thing is that all objects have two views: one that has generic properties like position and
rotation, which can be reacheched by &lt;code&gt;bpy.data.objects&lt;/code&gt;; and one that has specific properties like a light's power or a
camera's lens angle, which can be reached by f.i. &lt;code&gt;bpy.data.cameras&lt;/code&gt;. This was utterly confusing, specially since
all &lt;code&gt;bpy.data&lt;/code&gt;'s documentation is &lt;strong&gt;4 lines long&lt;/strong&gt;. Later I found out you can get specific data from the generic one in
the &lt;code&gt;.data&lt;/code&gt; attribute, so the take out is: &lt;strong&gt;always get your objects from &lt;code&gt;bpy.data.objects&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Once we get over that issue, things are quite straightforward, but not necessarily easy. The script as it is can already
be used with &lt;code&gt;blender --background --python &amp;lt;script_file&amp;gt;&lt;/code&gt;, but have in account that when you do that, you start with
the default generic 3D setup, with a light, a camera and a cube. You have to delete the cube, but you can get a
reference to the other two to reuse them.&lt;/p&gt;
&lt;p&gt;Then comes the administrative stuff around just rendering the scene. To industrialize it and be able to quickly test
stuff, you can try to get command line options. You can use Python's &lt;code&gt;argparser&lt;/code&gt; module for this, but have in account
that those &lt;code&gt;--background --python blender.py&lt;/code&gt; options are going to be passed to the script, so you either ignore unknown
options or you declare those too:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;mdione&lt;/span&gt;&lt;span class="nv"&gt;@ioniq&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="o"&gt;~/&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;projects&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;elevation&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;blender&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;background&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;blender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;
&lt;span class="n"&gt;Blender&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.6.2&lt;/span&gt;
&lt;span class="k"&gt;Read&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;prefs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;"/home/mdione/.config/blender/3.6/config/userpref.blend"&lt;/span&gt;
&lt;span class="k"&gt;usage&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;blender&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;-h&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;--render-samples RENDER_SAMPLES&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;--render-scale RENDER_SCALE&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;--height-scale HEIGHT_SCALE&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FILE&lt;/span&gt;
&lt;span class="nl"&gt;blender&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;unrecognized&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;--background --python&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Also, those options are going to be passed to Blender! So at the end of your run, Blender is going to complain that it
doesn't understand your options:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;unknown&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;argument&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;loading&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;samples&lt;/span&gt;
&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Cannot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/home/mdione/src/projects/elevation/--render-samples"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;No&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;such&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;
&lt;span class="n"&gt;Blender&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quit&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The other step you should do is to copy the Geo part of GeoTIFF to the output file. I used &lt;code&gt;rasterio&lt;/code&gt;, mostly because at
first I tried &lt;code&gt;gdal&lt;/code&gt; (I was already using &lt;code&gt;gdal_edit.py&lt;/code&gt; to do this in my previous manual procedure), but it's API was
quite confusing and &lt;code&gt;rasterio&lt;/code&gt;'s is more plain. But, &lt;code&gt;rasterio&lt;/code&gt; can't actually open a file just to write the metadata
like &lt;code&gt;gdal&lt;/code&gt; does, so I had to open the output file, read all data, open it again for writing (this truncates the file)
and write metadata and data.&lt;/p&gt;
&lt;p&gt;Now, some caveats. First, as I advanced in my last post, the method as it is right now has issues at the seams. Blender
can't read GDAL VRT files, so either I build 9 planes instead of 1 (&lt;strong&gt;all&lt;/strong&gt; the neighbors are needed to properly
calculate the shadows because Blender is also taking in account light reflected back from other features, meaning
mountains) or for each 1x1 tile I generate another with some buffer. I will try the first one and see if it fixes this
issue without much runtime impact.&lt;/p&gt;
&lt;p&gt;Second, the script is not 100% parametrized. Sun size and power are fixed based on my tests. Maybe in the future. Third,
I will try to add a scattering sky, so we get a bluish tint to the shadows, and set the Sun's color to something
yellowish. These should probably be options too.&lt;/p&gt;
&lt;p&gt;Fourth, and probably most important. I discovered that this hillshading method is really sensible to missing or bad data,
because they look like dark, deep holes. This is probably a deal breaker for many, so you either fix your data, or you
search for better data, or you live with it. I'm not sure what I'm going to do.&lt;/p&gt;
&lt;p&gt;So, what did I do with this? Well, first, find good parameters, one for render samples and another for height scale.
Render time grows mostly linearly with render samples, so I just searched for the one before detail stopped appearing;
the value I found was 120 samples.
When we left off I was using 10 instead of 5 for height scale, but it looks too exaggerated on hills (but it looks
AWESOME in mountains like the Mount Blanc/Monte Bianco! See below), so I tried to pinpoint a good balance. For me it's 8,
maybe 7.&lt;/p&gt;
&lt;p&gt;Why get these values right? Because like I mentioned before, a single 1x1°, 3601x5137px tile takes some 40m in my laptop
at 100 samples, so the more tuned the better. One nice way to quickly test is to lower the samples or use the
&lt;code&gt;--render-scale&lt;/code&gt; option of the script to reduce the size of the output. Note that because you reduce both dimensions at
the same time, the final render (and the time that takes) is actually the square of this factor: 50% is actually 25%
(because 0.50 * 0.50 = 0.25).&lt;/p&gt;
&lt;p&gt;So, without further addo, here's my script. If you find it useful but want more power, open issues or PRs, everything
is welcome.&lt;/p&gt;
&lt;p&gt;https://github.com/StyXman/blender_hilllshading &lt;sup id="fnref:5"&gt;&lt;a class="footnote-ref" href="https://www.grulic.org.ar/~mdione/glob/posts/automating-blender-based-hillshading-with-python/#fn:5"&gt;5&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Try to use the &lt;code&gt;main&lt;/code&gt; branch; &lt;code&gt;develop&lt;/code&gt; is considered unstable and can be broken.&lt;/p&gt;
&lt;p&gt;A couple of images of the shadows applied to my style as teaser, both using only 20 samples and x10 height scale:&lt;/p&gt;
&lt;p&gt;Dhaulagiri:&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://www.grulic.org.ar/~mdione/glob/images/Dhaulagiri.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Mont Blanc/Monte Bianco:&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://www.grulic.org.ar/~mdione/glob/images/Mont_Blanc-Monte_Bianco.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Man, I love the fact that the tail of the Giacchiaio del Miage is in shadows, but the rest is not; or how
Monte Bianco/Mont Blanc's shadow reaches across the valley to the base of la Tête d'Arp. But also notice the bad data
close to la Mer de Glace.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;Ok, TBH here, I'm very much used to &lt;code&gt;ipython&lt;/code&gt;'s console, it's really closer to the plain &lt;code&gt;python&lt;/code&gt; one. No tab
completion, so lots of calls to &lt;code&gt;dir()&lt;/code&gt; and a few &lt;code&gt;help()&lt;/code&gt;s. &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/automating-blender-based-hillshading-with-python/#fnref:1" title="Jump back to footnote 1 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;I couldn't find it again. Mastodon posts are not searchable by default, which I understand is good for privacy, but
on the other hand the current clients don't store anything locally, so you can't even search what you already saw.
I have several semi-ranting posts about this and I would show them to you, but they got lost on Mastodon. See what I
mean? &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/automating-blender-based-hillshading-with-python/#fnref:2" title="Jump back to footnote 2 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;So you have an idea, this took me a whole week of free time to finish, including but not in the text, my old nemesis,
terracing effect. This thing is &lt;strong&gt;brittle&lt;/strong&gt;. &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/automating-blender-based-hillshading-with-python/#fnref:3" title="Jump back to footnote 3 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;Yeah, maybe the API is mostly designed for this. &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/automating-blender-based-hillshading-with-python/#fnref:4" title="Jump back to footnote 4 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:5"&gt;
&lt;p&gt;My site generator keeps breaking. This is the second time I have to publicly admit this. Maybe next weekend I'll
gather steam and replace it with &lt;code&gt;nikola&lt;/code&gt;. &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/automating-blender-based-hillshading-with-python/#fnref:5" title="Jump back to footnote 5 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description><category>blender</category><category>dem</category><category>elevation</category><category>gdal</category><category>gis</category><category>hillshading</category><category>openstreetmap</category><category>python</category><category>rasterio</category><guid>https://www.grulic.org.ar/~mdione/glob/posts/automating-blender-based-hillshading-with-python/</guid><pubDate>Sun, 05 Nov 2023 16:19:45 GMT</pubDate></item><item><title>Blender hillshading and mapnik</title><link>https://www.grulic.org.ar/~mdione/glob/posts/blender-hillshading-and-mapnik/</link><dc:creator>Marcos Dione</dc:creator><description>&lt;p&gt;How did it start? Well, it actually started a long time ago, maybe a year ago, most probably more. I got myself
tumbling down the beautiful rabbit hole of Blender based mapping. The idea is very simple: if you have a DEM,
you can build a 3D representation of the terrain and use a renderer to build your map. To me the most striking
thing about those maps were not their 3D-ness (which
&lt;a href="https://stamen.com/shadows-on-maps-are-getting-a-lot-more-exciting-and-heres-why/"&gt;to some it's starting to be tiresome&lt;/a&gt;
, and I agree), but &lt;em&gt;the shadows&lt;/em&gt;. I've been
&lt;a href="https://www.grulic.org.ar/~mdione/glob/posts/trying-to-calculate-proper-shading/"&gt;pursuing the best shadow&lt;/a&gt; for a
while, and this seemed like the perfect fit.&lt;/p&gt;
&lt;p&gt;So, like I said, one year ago or so I took
&lt;a href="https://somethingaboutmaps.wordpress.com/2017/11/16/creating-shaded-relief-in-blender/"&gt;"the" Blender relief tutorial&lt;/a&gt;
and run with it. I got to the point where I could reproduce it with a 1x1, 3600x3600px DEM from mapzen, but when I tried
to automate it, I found out that Blender has a python console where it prints out the commands that are
equivalent to the actions you make in the UI, but the resulting script was too horrible to my eyes and run out
of breath (another of those cases of the perfect being the enemy of the good).&lt;/p&gt;
&lt;p&gt;Then a few days ago I read that first link and got some steam build up. In fact, it was two passages in it that
lit up the fire:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Most of these use a tool called Blender, an extremely powerful open-source tool for all kinds of 3D modeling and
rendering. A few cartographers use other tools, such as Aerialod, or R‘s Rayshader plugin.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;R! I can easily automate this!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If we stick to a very zoomed-out map, or if we have a really huge computer running Blender, we could try to do
a hillshade for the entire world, and then slice that up for our map tiles. But there’s no way we could do this
at a high-enough resolution so you could zoom all the way in, as in the sample tiles above.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Challenge accepted! (Spoiler: I'm not there yet).&lt;/p&gt;
&lt;p&gt;I tried Rayshader. I wanna be honest: it's easy, quick, but I didn't like the results. It seemed like no matter
how high you put the sun, it always drew very long shadows. So despite its pragmaticism, I left it on a side.&lt;/p&gt;
&lt;p&gt;So I picked up what I did in the past and tried to apply it to a map. I re-rendered everything and applied it to
my style. The outcome was encouraging:&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://www.grulic.org.ar/~mdione/glob/images/MontBlanc-blender1-mapnik.jpg"&gt;&lt;/p&gt;
&lt;p&gt;To start with, I probably did that render with not enough render passes, so the output looks grainy. Second, the
material color is too bright, so the height tints are washed out. Still, we can see the big shadow cast over the
valley some 3200m below the Mont Blanc/Monte Bianco.&lt;/p&gt;
&lt;p&gt;This proved to be a good place to test the method, because of the great difference between the valley and the
peak casting the shadow over it, and that lead me to think: are there more extreme places in the world? An easy
bet is yes, and the place to look for them was the Himalayas. The Aconcagua could have been a good contender, but
the valley at its SE is some 4550m already. Surprisingly, the way I searched for a good place was to use existing
maps with the usual hill shade technique, looking for big dark spots, specially those wide in the NW-SE direction.
I first found my spot in the Tukuche Peak, that looms some 4350m above the Gandaki River, and then the nearby
Dhaulagiri, that's even some 1250m higher, clocking at 8167m. Here's how they look (not the big structure in the
upper left, but the bigger grey [8000m+] blob to the East of it; the river snakes in the right all the way
down):&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://www.grulic.org.ar/~mdione/glob/images/Dhaulagiri-blender2-mapnik.jpg"&gt;&lt;/p&gt;
&lt;p&gt;I had to add 3 more color bands to my style and reshuffle them because I never rendered any 8k'er before, so the colors
were haphazardly concocted for the rendering and are not definitive. At least it lets you behold the grandiosity
of that rock jutting through thousands and thousands of meters with very steep sides.&lt;/p&gt;
&lt;p&gt;Time to get real. I usually render regions were I'll be going, and next time it's the Upper Segre Valley, so I
rendered N42E001-2 in one go. That's 7201x4884px after reprojecting to WebMercator (and compensating as
described in the second link!), so some 35Mpx. Blender took some 44m+ on a 4.5yo medium-high spec'ed laptop
at 200 render samples, which means that I can continue to render small regions this way, but that for the moment
I won't be applying this technique to the whole Europe.&lt;/p&gt;
&lt;p&gt;Up to here I was just applying the same style in QGIS, which has been an indispensable tool to develop this
style. But trying these TIFFs in mapnik for the first time needed an extra step. Well, two, in fact. Blender
does not save the TIFFs georeferenced, so you have to copy the data from the original DEM. For that, use
&lt;code&gt;gdal_edit.py -a_srs ... -a_ullr ...&lt;/code&gt; with the right ESPG and the data from the output of &lt;code&gt;gdalinfo&lt;/code&gt;. Next, for
some reson, it always use 16bits integers, even when explicitly saying to use 8. This little snippet takes care
of that:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;imageio&lt;/span&gt;

&lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;imageio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;imread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pirinoak-blender-10x.tif'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;
&lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'uint8'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;imageio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;imwrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pirinoak-blender-10x-8bits.tif'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Thank $DEITY (and developers!) for good libraries.&lt;/p&gt;
&lt;p&gt;The first thing I noticed was that we have been lied by maps (again!) for a long time. Most hill shading algos
use a 45° high sun (the direction does not matter much). But if you think about it, how many mountains have sides
45°+ steep? According to a (real, not like me!) cartographer friend, for continental Argentina it's less than 1%
at 30arcsecs of resolution
(note that SRTM is 1arcsec). Still, some shadows are there, and they help us (and we get used to that) to
recognize slope direction. And now we're asking a raytracing program to calculate &lt;em&gt;real&lt;/em&gt; shadows? The result I
initially got was underwhelming, &lt;em&gt;even when I was already asking Blender to exaggerate height by 5x!&lt;/em&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://www.grulic.org.ar/~mdione/glob/images/Pirinoak-blender2-mapnik.jpg"&gt;&lt;/p&gt;
&lt;p&gt;So, I bit the bullet and went all in with 10x:&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://www.grulic.org.ar/~mdione/glob/images/Pirinoak-blender3-mapnik.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Much better, but not definitive. I still have to render Dhaulagiri again, and at least some region I already
know well by having being there &lt;em&gt;a lot&lt;/em&gt;. Here's how that region looks in my style:&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://www.grulic.org.ar/~mdione/glob/images/Pirinoak-mapnik.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Now some notes about "the" Blender relief tutorial. I followed it to the letter, but with my experience I had to
make some changes. One you already know, using a displacement scale of 10x instead of 0.3. I have no exact idea
why his initial rendering were so spiky, but I suspect that the DEM grid unit was not meters.&lt;/p&gt;
&lt;p&gt;Second, since that first Mount Blanc/Monte Bianco render, we know the color is too bright. I lowered it to 0.6
(and later I found that that's what he actually suggests at the end of the plane section) and then compared the
grey in a plain (&lt;code&gt;#b5b5b5&lt;/code&gt;) to what GDAL outputs and compensated using a simple coefficient. The final value is
0.402.&lt;/p&gt;
&lt;p&gt;Third, I was having issues rendering: I was getting a lot of terracing. After
&lt;a href="https://blender.chat/channel/support/thread/mbFzP43mtZzNAzMCy"&gt;a long chat with Viktor_smg from blender.chat/support&lt;/a&gt;
they figured out that the sRGB color space in the Image Texture is broken and that I should use XYZ instead. This
meant installing Blender by hand instead of relying on the one in Debian Unstable because it's too old and does
not have it.
&lt;a href="https://blender.chat/channel/support/thread/FSeYL7Jh8mnBCdiHF"&gt;They also gave me pointers about how to automate it&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Last, &lt;a href="https://wwwtyro.net/2019/03/21/advanced-map-shading.html"&gt;you can't apply this technique DEM by DEM&lt;/a&gt;
because you want the shadows from the neighbouring tiles to spill over the current one. That link shows how to
render the tile and its 8 neighbouring ones, but I think that you can optimize it in two ways: First, since
shadows come from the NW, just add the tiles that lie in that general direction. Second, no shadow would cast
over 10s of kilometers. You could even get away with just adding a smaller band around the tile.&lt;/p&gt;
&lt;p&gt;That's it for now. The next step is to automate this an publish that. $DEITY knows when that will happen.&lt;/p&gt;</description><category>blender</category><category>dem</category><category>elevation</category><category>gdal</category><category>gis</category><category>hillshading</category><category>imageio</category><category>openstreetmap</category><category>python</category><guid>https://www.grulic.org.ar/~mdione/glob/posts/blender-hillshading-and-mapnik/</guid><pubDate>Sat, 22 Oct 2022 23:55:01 GMT</pubDate></item></channel></rss>