<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="https://konrad.earth/feed_style.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <tabi:metadata xmlns:tabi="https://github.com/welpo/tabi">
        <tabi:base_url>https:&#x2F;&#x2F;konrad.earth</tabi:base_url>
        <tabi:separator>
            •
        </tabi:separator>
        <tabi:about_feeds>This is a web feed, also known as an Atom feed. Subscribe by copying the URL from the address bar into your newsreader. Visit About Feeds to learn more and get started. It&#x27;s free.</tabi:about_feeds>
        <tabi:visit_the_site>Visit website</tabi:visit_the_site>
        <tabi:recent_posts>Recent posts</tabi:recent_posts>
        <tabi:last_updated_on>Updated on $DATE</tabi:last_updated_on>
        <tabi:default_theme></tabi:default_theme>
        <tabi:post_listing_date>date</tabi:post_listing_date>
        <tabi:current_section>WebGL</tabi:current_section>
    </tabi:metadata><title>konrad.earth - WebGL</title>
        <subtitle>Konrad Heidler</subtitle>
    <link href="https://konrad.earth/tags/webgl/atom.xml" rel="self" type="application/atom+xml"/>
    <link href="https://konrad.earth/tags/webgl/" rel="alternate" type="text/html"/>
    <generator uri="https://www.getzola.org/">Zola</generator><updated>2025-05-26T00:00:00+00:00</updated><id>https://konrad.earth/tags/webgl/atom.xml</id><entry xml:lang="en">
        <title>Japan Shader</title>
        <published>2025-05-26T00:00:00+00:00</published>
        <updated>2025-05-26T00:00:00+00:00</updated>
        <author>
            <name>Konrad Heidler</name>
        </author>
        <link rel="alternate" href="https://konrad.earth/blog/japan-shader/" type="text/html"/>
        <id>https://konrad.earth/blog/japan-shader/</id>
        
            <content type="html">&lt;figure &gt;
    
    &lt;img src=photo.jpg &#x2F;&gt;
    &lt;figcaption&gt;
      
      &lt;p&gt;
         Mountains fading out toward the horizon. This is what we’re looking to re-create in a shader. Photo taken from Oyunohara View Point, Wakayama Prefecture 
        
        
        
      &lt;&#x2F;p&gt;
    &lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;The mountains in traditional japanese art prints, ridge behind ridge, each one paler than the last until they finally fade into the sky, always struck me as a stylistic choice.
A way of flattening distance onto the canvas.
Then I walked the Kumano Kodo, on the Kii Peninsula south of Osaka, and found out that these paintings are much closer to reality than I thought they were.
Near ridges dark green. The next one blue-grey. Four or five more behind that, each fainter, the last barely separable from the sky.
Dense folded ranges, a lot of humidity, and atmospheric scattering does the rest.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.openpv.de&#x2F;&quot;&gt;Having worked with elevation models and ray marching before&lt;&#x2F;a&gt;, I figured it would be an interesting challenge to try and recreate this prorammatically.
Not a static image, but an interactive applet where you can pick a point on the map and have the mountain ranges laid out before you, in the traditional Japanese art style.
So before we dive into how this is done, enjoy some dynamically rendered Japanese mountains, drawn live by your GPU:&lt;&#x2F;p&gt;
&lt;div class=&quot;applet&quot;&gt;
  &lt;iframe src=https:&amp;#x2F;&amp;#x2F;maps.heidler.info&amp;#x2F;japan-shader allow=&quot;fullscreen&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
  
    &lt;button
      type=&quot;button&quot;
      class=&quot;fullscreen&quot;
      aria-label=&quot;Open applet fullscreen&quot;
      title=&quot;Fullscreen&quot;
    &gt;
      &lt;svg viewBox=&quot;0 0 24 24&quot; aria-hidden=&quot;true&quot;&gt;
        &lt;path
          d=&quot;M5 9V5h4M15 5h4v4M19 15v4h-4M9 19H5v-4&quot;
          fill=&quot;none&quot;
          stroke=&quot;currentColor&quot;
          stroke-width=&quot;2&quot;
          stroke-linecap=&quot;round&quot;
        &#x2F;&gt;
      &lt;&#x2F;svg&gt;
    &lt;&#x2F;button&gt;
  
&lt;&#x2F;div&gt;
&lt;h1 id=&quot;the-shader&quot;&gt;The shader&lt;&#x2F;h1&gt;
&lt;p&gt;The core of this app is the WebGL shader.
For each pixel on the screen, it marches along the corresponding ray in 3d space and checks whether the ray intersects with the terrain at any point.
The ray marching loop is quite simple:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code data-lang=&quot;glsl&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;for&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;(;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; t &lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;&amp;lt;=&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-numeric&quot;&gt; 15.&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword&quot;&gt;e&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-numeric&quot;&gt;4&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; t &lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;*=&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-numeric&quot;&gt; 1.05&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;  vec3 check &lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;=&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; x0 &lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;+&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; t &lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;*&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; dx&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;  alpha &lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;*=&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-numeric&quot;&gt; 0.992&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-storage z-type&quot;&gt;  float&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; h &lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;=&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt; height&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;check&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;xy&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;  if&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;h &lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; check&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;z&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt; ||&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; h &lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-numeric&quot;&gt; 5000.&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;    break&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation&quot;&gt;  }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation&quot;&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;At each step it samples the digital elevation model (DEM) texture.
If the sampled terrain height is higher than the current ray height, the ray has hit something.
If it does not hit anything within about 150 km, we stop marching and treat this pixel as sky.&lt;&#x2F;p&gt;
&lt;p&gt;Now there is no actual lighting simulation or anything done in the shader.
Instead, the &lt;code&gt;alpha&lt;&#x2F;code&gt; variable starts at &lt;code&gt;1.0&lt;&#x2F;code&gt; and decays as the ray flies through space.
The later the ray hits the terrain, the less &lt;code&gt;alpha&lt;&#x2F;code&gt; is left.
For near terrain, we colour the pixel dark green, while farther terrain shifts toward blue, and long unobstructed rays fade toward a pale grey sky:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code data-lang=&quot;glsl&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;if&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;alpha &lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-numeric&quot;&gt; 0.5&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;  fragColor &lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;=&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt; mix&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;sky&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;,&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; blue&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;,&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; alpha &lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;*&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-numeric&quot;&gt; 2.0&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation&quot;&gt;}&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword&quot;&gt; else&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;  fragColor &lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;=&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt; mix&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt;blue&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;,&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; green&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;,&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; alpha &lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt;*&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-numeric&quot;&gt; 2.0&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt; -&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-numeric&quot;&gt; 1.0&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation&quot;&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That is why the output feels more like a painting than a shaded relief map.
The visible shapes are controlled by real topography, but the color ramp is chosen to emphasize distance, haze, and silhouettes.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;getting-elevation-data&quot;&gt;Getting Elevation Data&lt;&#x2F;h1&gt;
&lt;p&gt;Copernicus provides &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;dataspace.copernicus.eu&#x2F;explore-data&#x2F;data-collections&#x2F;copernicus-contributing-missions&#x2F;collections-description&#x2F;COP-DEM&quot;&gt;global elevation models&lt;&#x2F;a&gt; at multiple resolutions.
As the browser will have to load all of Japan into RAM, the processed DEM will have to be quite low-res, so I used the coarse 90m product as a good starting point.
After downloading 71 DEM tiles from the Copernicus S3 bucket, this data now needs to be reprojected from a lat&#x2F;lon grid (EPSG:4326) to something more suited for building something resembling 3d space.
Considering our rough radius requirement of 150km, we can get away with using Mercator (EPSG:3857) here, ignoring the fact that Earth is a sphere.
Also, using Mercator makes the calculations between points on the map and the pixel space of the DEM much easier.
The reprojection happens in 2 steps:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Building a VRT from the elevation tiles&lt;&#x2F;li&gt;
&lt;li&gt;Warping the VRT to Web Mercator and subsetting to the bounds of Japan&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;gdalbuildvrt&lt;&#x2F;span&gt;&lt;span class=&quot;z-string&quot;&gt; dem.vrt dem_tiles&#x2F;&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable&quot;&gt;*&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-entity z-name z-function&quot;&gt;gdal_translate&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-character z-escape&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-string&quot;&gt;  -projwin&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-numeric&quot;&gt; 14411296.0 5015741.0 15844896.0 3582141.0&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-character z-escape&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-string&quot;&gt;  -tr&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-numeric&quot;&gt; 700 700&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-character z-escape&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-string&quot;&gt;  -co COMPRESS=DEFLATE&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-character z-escape&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-string&quot;&gt;  dem.vrt dem.tif&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Bounds and resolution are chosen in such a way that the resulting DEM texture will be 2048×2048 pixels in size. Finally, PIL converts the TIF image into a 16-bit grayscale PNG, making it easy to serve and load into WebGL.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;import&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; rasterio&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword&quot;&gt; as&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; rio&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;from&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; PIL&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword&quot;&gt; import&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; Image&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword z-control z-flow z-python&quot;&gt;with&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; rio&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-python&quot;&gt;open&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;&amp;#39;dem.tif&amp;#39;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-control z-flow z-python&quot;&gt; as&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; raster&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-comment&quot;&gt;    # Scale by 10.0 to keep more vertical accuracy&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;    data&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt; =&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; raster&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-python&quot;&gt;read&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-numeric&quot;&gt;1&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt; *&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-numeric&quot;&gt; 10.0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;img&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator&quot;&gt; =&lt;&#x2F;span&gt;&lt;span class=&quot;z-source&quot;&gt; Image&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-python&quot;&gt;fromarray&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-arguments z-python&quot;&gt;data&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-python&quot;&gt;astype&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-arguments z-python&quot;&gt;np&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-arguments z-python&quot;&gt;uint16&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;))&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-source&quot;&gt;img&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-python&quot;&gt;save&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;&amp;#39;dem.png&amp;#39;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This is the practical trick that makes the whole applet lightweight. All the geospatial work happens offline. The web app only needs this one 2048 by 2048 elevation texture and a WebGL fragment shader.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;tradeoffs&quot;&gt;Tradeoffs&lt;&#x2F;h1&gt;
&lt;p&gt;There are plenty of inaccuracies. Web Mercator distorts scale, the elevation texture is coarse, the ray marching uses fixed exponential steps, the camera sits just above the DEM surface, and the atmosphere is just a hand-tuned color fade. There is no lighting model, no shadows, no clouds, and no curvature correction.&lt;&#x2F;p&gt;
&lt;p&gt;But for this purpose those are acceptable compromises. The goal is not to answer “what exactly would I see from this point?” The goal is to make a fast, explorable sketch of how Japan’s terrain stacks up visually: ridge after ridge, each one a little paler than the last.&lt;&#x2F;p&gt;
</content>
        </entry><entry xml:lang="en">
        <title>Metaballs</title>
        <published>2020-05-16T09:00:00+01:00</published>
        <updated>2020-05-16T09:00:00+01:00</updated>
        <author>
            <name>Konrad Heidler</name>
        </author>
        <link rel="alternate" href="https://konrad.earth/blog/metaballs/" type="text/html"/>
        <id>https://konrad.earth/blog/metaballs/</id>
        
            <content type="html">&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Metaballs&quot;&gt;Metaballs&lt;&#x2F;a&gt;
are a fun way of creating blobby looking shapes.
The idea stems from a method for rendering molecules,
where each atom contributes to the overall electron density&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-1-1&quot;&gt;&lt;a href=&quot;#fn-1&quot;&gt;1&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;In the easy case of hydrogen, this contribution is a radially symmetric shape centered around the
atom’s center \(c\):&lt;&#x2F;p&gt;
&lt;p&gt;$$
f(v; c) = a \exp(- \|v - c\|_2^2)
$$&lt;&#x2F;p&gt;
&lt;p&gt;To render a collection of such atoms, one then takes the sum over these contributions,
and renders a level-set:&lt;&#x2F;p&gt;
&lt;p&gt;$$
\left\{\,v \,\middle|\, \sum_{i=1}^n a_i\exp(- \|v - c_i\|_2^2) = L\,\right\}
$$&lt;&#x2F;p&gt;
&lt;p&gt;Here’s a visualization in two dimensions with moving centers:&lt;&#x2F;p&gt;
&lt;div class=&quot;applet&quot;&gt;
  &lt;iframe src=metaballs.html allow=&quot;fullscreen&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
  
    &lt;button
      type=&quot;button&quot;
      class=&quot;fullscreen&quot;
      aria-label=&quot;Open applet fullscreen&quot;
      title=&quot;Fullscreen&quot;
    &gt;
      &lt;svg viewBox=&quot;0 0 24 24&quot; aria-hidden=&quot;true&quot;&gt;
        &lt;path
          d=&quot;M5 9V5h4M15 5h4v4M19 15v4h-4M9 19H5v-4&quot;
          fill=&quot;none&quot;
          stroke=&quot;currentColor&quot;
          stroke-width=&quot;2&quot;
          stroke-linecap=&quot;round&quot;
        &#x2F;&gt;
      &lt;&#x2F;svg&gt;
    &lt;&#x2F;button&gt;
  
&lt;&#x2F;div&gt;
&lt;section class=&quot;footnotes&quot;&gt;
&lt;ol class=&quot;footnotes-list&quot;&gt;
&lt;li id=&quot;fn-1&quot;&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;dl.acm.org&#x2F;doi&#x2F;10.1145&#x2F;357306.357310&quot;&gt;Blinn, J. (1982). A Generalization of Algebraic Surface Drawing. ACM Trans. Graph., 1, 235-256.&lt;&#x2F;a&gt; &lt;a href=&quot;#fr-1-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;section&gt;
</content>
        <summary type="html">Moving blobs</summary>
        </entry><entry xml:lang="en">
        <title>Soap Bubbles</title>
        <published>2019-06-06T09:00:00+01:00</published>
        <updated>2019-06-06T09:00:00+01:00</updated>
        <author>
            <name>Konrad Heidler</name>
        </author>
        <link rel="alternate" href="https://konrad.earth/blog/soap-bubbles/" type="text/html"/>
        <id>https://konrad.earth/blog/soap-bubbles/</id>
        
            <content type="html">&lt;p&gt;Have you ever wondered about the rainbows on CDs, gasoline puddles or soap bubbles?
All of these have the same cause: The &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Thin-film_interference&quot;&gt;interference of light on thin surfaces&lt;&#x2F;a&gt;.
Today, we’ll try to render something that looks like a soap bubble.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;some-physics&quot;&gt;Some Physics&lt;&#x2F;h2&gt;
&lt;p&gt;When a ray of light hits the surface of the soap bubble,
it is either instantly reflected, or it enters the soap film and is refracted.
When it is refracted, it can then be reflected off the other end of the soap,
and then leave it again at a slightly different spot.
There are countless other possiblities for the ray to bounce around,
but these are the two that we will focus on here:&lt;&#x2F;p&gt;
&lt;img class=&quot;img-light&quot; src=&quot;paths.svg&quot; loading=&quot;lazy&quot;&gt;
&lt;img class=&quot;img-dark&quot; src=&quot;paths-dark.svg&quot; loading=&quot;lazy&quot;&gt;
&lt;p&gt;Because the width of the soap layer is comparable to the wavelength of visible light,
the resulting color of the light rays will change considerably depending on the
incidence angle.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;finished-visualization&quot;&gt;Finished visualization&lt;&#x2F;h2&gt;
&lt;p&gt;Putting it all together,
we get this nice little interactive visualization.&lt;&#x2F;p&gt;
&lt;div class=&quot;applet&quot;&gt;
  &lt;iframe src=bubbles.html allow=&quot;fullscreen&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
  
    &lt;button
      type=&quot;button&quot;
      class=&quot;fullscreen&quot;
      aria-label=&quot;Open applet fullscreen&quot;
      title=&quot;Fullscreen&quot;
    &gt;
      &lt;svg viewBox=&quot;0 0 24 24&quot; aria-hidden=&quot;true&quot;&gt;
        &lt;path
          d=&quot;M5 9V5h4M15 5h4v4M19 15v4h-4M9 19H5v-4&quot;
          fill=&quot;none&quot;
          stroke=&quot;currentColor&quot;
          stroke-width=&quot;2&quot;
          stroke-linecap=&quot;round&quot;
        &#x2F;&gt;
      &lt;&#x2F;svg&gt;
    &lt;&#x2F;button&gt;
  
&lt;&#x2F;div&gt;
&lt;!-- If you&#x27;re interested in the source code, --&gt;
&lt;!-- # you can find it &lt;a download=&quot;bubbles.html&quot; href=&quot;bubbles.html&quot;&gt;here&lt;&#x2F;a&gt;. --&gt;
</content>
        </entry><entry xml:lang="en">
        <title>Particle Life</title>
        <published>2019-01-31T09:00:00+01:00</published>
        <updated>2019-01-31T09:00:00+01:00</updated>
        <author>
            <name>Konrad Heidler</name>
        </author>
        <link rel="alternate" href="https://konrad.earth/blog/particlelife/" type="text/html"/>
        <id>https://konrad.earth/blog/particlelife/</id>
        
            <content type="html">&lt;p&gt;Particle Simulations are great.
Life Simulations are fun.
Meet Particle Life – the intersection of these two worlds.&lt;&#x2F;p&gt;
&lt;p&gt;Inspired by Biologist &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;evolution.berkeley.edu&#x2F;evolibrary&#x2F;article&#x2F;history_24&quot;&gt;Lynn Margulis’&lt;&#x2F;a&gt;
theory of endosymbiosis, Jeffrey Ventrella invented his &lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;www.ventrella.com&#x2F;Clusters&#x2F;&quot;&gt;Clusters&lt;&#x2F;a&gt;.
The main idea is simulating microorganisms that have pretty basic interaction patterns.
Some species are drawn towards certain others, while some will be repelled by others.
Implementing a particle simulation with these rules leads to very interesting patterns.&lt;&#x2F;p&gt;
&lt;p&gt;Compared to physics-based particle simulations, this approach explicitly violates the
&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Momentum#Conservation&quot;&gt;Conservation of Momentum&lt;&#x2F;a&gt;.
This allows for self-propelling structures, clusters that look like biological cells, and much more.&lt;&#x2F;p&gt;
&lt;p&gt;Porting this simulation to WebGL code was pretty straight-forward using
&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;cindyjs.org&quot;&gt;CindyJS&lt;&#x2F;a&gt;.
Play around with the simulation here:&lt;&#x2F;p&gt;
&lt;div class=&quot;applet&quot;&gt;
  &lt;iframe src=particlelife.html allow=&quot;fullscreen&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
  
    &lt;button
      type=&quot;button&quot;
      class=&quot;fullscreen&quot;
      aria-label=&quot;Open applet fullscreen&quot;
      title=&quot;Fullscreen&quot;
    &gt;
      &lt;svg viewBox=&quot;0 0 24 24&quot; aria-hidden=&quot;true&quot;&gt;
        &lt;path
          d=&quot;M5 9V5h4M15 5h4v4M19 15v4h-4M9 19H5v-4&quot;
          fill=&quot;none&quot;
          stroke=&quot;currentColor&quot;
          stroke-width=&quot;2&quot;
          stroke-linecap=&quot;round&quot;
        &#x2F;&gt;
      &lt;&#x2F;svg&gt;
    &lt;&#x2F;button&gt;
  
&lt;&#x2F;div&gt;
</content>
        </entry>
</feed>
