<br />
<b>Warning</b>:  Declaration of Jetpack_IXR_Client::query() should be compatible with IXR_Client::query(...$args) in <b>/home/clients/7267bc096562fcdb78c0ab60d3ac51fb/web/blog/wp-content/plugins/jetpack/class.jetpack-ixr-client.php</b> on line <b>91</b><br />
{"id":1208,"date":"2021-02-14T22:07:05","date_gmt":"2021-02-14T22:07:05","guid":{"rendered":"http:\/\/barradeau.com\/blog\/?p=1208"},"modified":"2021-02-15T22:31:48","modified_gmt":"2021-02-15T22:31:48","slug":"marseille","status":"publish","type":"post","link":"https:\/\/barradeau.com\/blog\/?p=1208","title":{"rendered":"Marseille"},"content":{"rendered":"<p><a href=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/brave_2W4vG1m2mf.jpg\" data-rel=\"lightbox-image-0\" data-rl_title=\"\" data-rl_caption=\"\" title=\"\"><img decoding=\"async\" loading=\"lazy\" class=\"aligncenter size-full wp-image-1264\" src=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/brave_2W4vG1m2mf.jpg\" alt=\"\" width=\"1589\" height=\"1057\" srcset=\"https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/brave_2W4vG1m2mf.jpg 1589w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/brave_2W4vG1m2mf-300x200.jpg 300w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/brave_2W4vG1m2mf-768x511.jpg 768w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/brave_2W4vG1m2mf-1024x681.jpg 1024w\" sizes=\"(max-width: 1589px) 100vw, 1589px\" \/><\/a><\/p>\n<p>last year I was contacted by\u00a0<a href=\"https:\/\/www.laphase5.com\/\">la phase 5<\/a> , they needed help with the WebGL part of their 2021 wish card.<br \/>\nthey&#8217;re based in Marseille, the second biggest french city, by the mediterranean sea. it&#8217;s quite a unique city, both classy and cheap, packed and desert,\u00a0La Phase 5&#8217;s plan\u00a0was to write a love letter to their city, it\u00a0is a &#8220;guided tour&#8221; of Marseille, in WebGL, with a curated list of handcrafted 360\u00b0 panoramas.<\/p>\n<h2>a &#8220;fun little project&#8221;<\/h2>\n<p>I like maps a lot, when they asked for a map I was all warm and fuzzy and given my <a href=\"http:\/\/www.barradeau.com\/projects\/cartography\/\">previous researches<\/a>, I was fairly confident that a 3D version could work.<\/p>\n<p>after some discussions, it appeared that we wouldn&#8217;t be able to model the city at the scale we needed it ; <a href=\"https:\/\/www.fonts.ninja\/\">Axel Corjon<\/a> the POWER Art Director I worked with, managed to create a model of Marseille in Cinema4D and ended up with roughly 4M triangles which is still a tad too much for real time rendering in a browser :)<br \/>\nwe could have modeled a &#8220;toy version&#8221; of Marseille but it went against the second part of the project that was about revealing 360\u00b0 panoramas of actual places in the city.<\/p>\n<p>that&#8217;s possibly when the <em>fun little project<\/em> turned into a <strong>sea dragon<\/strong> (the bad kind).<\/p>\n<p>&nbsp;<\/p>\n<h2>data acquisition and pre-process<\/h2>\n<p>the problem was that modeling the whole city in 3D meant using a tile map system which added <em>a touch<\/em> of preprocessing.<br \/>\nfirst we needed data, to do this, I:<\/p>\n<ul>\n<li>downloaded the &#8220;<a href=\"https:\/\/download.geofabrik.de\/europe\/france\/provence-alpes-cote-d-azur.html\">Provence Alpes-Cote-d&#8217;Azur<\/a>&#8221; PBF from\u00a0<a href=\"https:\/\/download.geofabrik.de\/\">geofabrik<\/a><\/li>\n<li>then used <a href=\"https:\/\/wiki.openstreetmap.org\/wiki\/Osmconvert\">Osmconvert<\/a> to crop a sub region (lat\/lng boundingbox) and save it as PBF<\/li>\n<li>then used <a href=\"https:\/\/wiki.openstreetmap.org\/wiki\/Osmosis\">Osmosis<\/a> to filter out amenities<\/li>\n<li>then used <a href=\"https:\/\/github.com\/simonpoole\/mapsplit\/releases\">mapsplit<\/a>\u00a0to split the cropped area into individual PBF tiles (slippy or XYZ tiles)<\/li>\n<li>then used<a href=\"https:\/\/github.com\/aspectumapp\/osm2geojson\">\u00a0osm2geojson<\/a> to convert the PBF tiles to individual GEOJSON to manipulate them easily with python<\/li>\n<li>then used\u00a0<a href=\"http:\/\/maperitive.net\/\">maperitive<\/a>\u00a0to check the data (not required but very handy!)<\/li>\n<li>then used custom python scripts to filter out amenities further and create custom tiles<\/li>\n<\/ul>\n<p>essentially:<\/p>\n<pre class=\"lang:sh decode:true\">\/\/crop a region and save as PBF\r\n.\\osmconvert.exe paca.pbf -b=5,44,6,45 --out-pbf -o=paca_crop.pbf\r\n\r\n\/\/split OSM to tiles\r\njava -Xmx6G -jar mapsplit-all-0.2.1.jar -tvm -c -i paca_crop.pbf -o tiles\/t_%x_%y_%z.pbf -z 16\r\n<\/pre>\n<p>where `-b` is the bounding box of the area to crop, additionally, I fetched the corresponding elevation tiles from the <a href=\"https:\/\/registry.opendata.aws\/terrain-tiles\/\">amazon LTS<\/a>, to create the terrain.<\/p>\n<p><strong>WHY?!<\/strong> you ask, let me illustrate.<br \/>\nbelow is a rendition of the <a href=\"https:\/\/tilezen.readthedocs.io\/en\/latest\/\">tilezen<\/a>\u00a0vector tileset at zoom level 15, buildings are rendered in red, those would be the shape we extrude to create the buildings<\/p>\n<p><a href=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/1KGzIGtyH1.png\" data-rel=\"lightbox-image-1\" data-rl_title=\"\" data-rl_caption=\"\" title=\"\"><img decoding=\"async\" loading=\"lazy\" class=\"aligncenter wp-image-1217 size-full\" src=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/1KGzIGtyH1.png\" alt=\"\" width=\"1279\" height=\"973\" srcset=\"https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/1KGzIGtyH1.png 1279w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/1KGzIGtyH1-300x228.png 300w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/1KGzIGtyH1-768x584.png 768w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/1KGzIGtyH1-1024x779.png 1024w\" sizes=\"(max-width: 1279px) 100vw, 1279px\" \/><\/a><\/p>\n<p>sparse, boring.<\/p>\n<p>now here&#8217;s roughly the same spot at zoom level 16:<a href=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_5z9kW5z1ll.png\" data-rel=\"lightbox-image-2\" data-rl_title=\"\" data-rl_caption=\"\" title=\"\"><img decoding=\"async\" loading=\"lazy\" class=\"aligncenter wp-image-1218 size-full\" src=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_5z9kW5z1ll.png\" alt=\"\" width=\"1261\" height=\"962\" srcset=\"https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_5z9kW5z1ll.png 1261w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_5z9kW5z1ll-300x229.png 300w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_5z9kW5z1ll-768x586.png 768w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_5z9kW5z1ll-1024x781.png 1024w\" sizes=\"(max-width: 1261px) 100vw, 1261px\" \/><\/a>now we&#8217;re talking :)<\/p>\n<p>for legibility &amp; performance reasons, we don&#8217;t need all the info at lower zoom levels. that&#8217;s why most tile providers don&#8217;t bother and simply discard smaller buildings at lower zoom levels based on their surface and &#8216;importance&#8217; (a tiny but famous landmark may still be visible at lower zoom levels).\u00a0so, to get the required precision upfront and have the ability to isolate amenities by type (buildings, roads,&#8230;), we needed to work with the &#8220;complete&#8221; dataset, the equivalent of the zoom level 16+.<\/p>\n<p>my first idea was to create a heightmap from the buildings geodata and displace a plane.\u00a0it would have allowed to easily merge the altitudes into the tiles (buildings would use the red channel, roads, the green and altitude would use the blue).<\/p>\n<div style=\"width: 700px;\" class=\"wp-video\"><!--[if lt IE 9]><script>document.createElement('video');<\/script><![endif]-->\n<video class=\"wp-video-shortcode\" id=\"video-1208-1\" width=\"700\" height=\"420\" preload=\"metadata\" controls=\"controls\"><source type=\"video\/mp4\" src=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/L3RHeYxkqe.mp4?_=1\" \/><a href=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/L3RHeYxkqe.mp4\">http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/L3RHeYxkqe.mp4<\/a><\/video><\/div>\n<p>images are also very easy to work with so the\u00a0<em>bulge<\/em>\u00a0effect was practically free.<br \/>\nit\u00a0<em>sort of<\/em>\u00a0worked, at least it was not uninteresting and even worked with very thin lines (the roads)<\/p>\n<p><a href=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_ldCfnjMdC0.png\" data-rel=\"lightbox-image-3\" data-rl_title=\"\" data-rl_caption=\"\" title=\"\"><img decoding=\"async\" loading=\"lazy\" class=\"aligncenter wp-image-1225 size-full\" src=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_ldCfnjMdC0.png\" alt=\"\" width=\"1679\" height=\"969\" srcset=\"https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_ldCfnjMdC0.png 1679w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_ldCfnjMdC0-300x173.png 300w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_ldCfnjMdC0-768x443.png 768w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_ldCfnjMdC0-1024x591.png 1024w\" sizes=\"(max-width: 1679px) 100vw, 1679px\" \/><\/a><\/p>\n<p>I liked the version above, it came from using binary ( black or white ) values to draw the buildings and add a small blur, very crisp, just not the intended aesthetics.<\/p>\n<p>below is a &#8220;merged&#8221; tileset test<\/p>\n<p><a href=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_ISpfDaelUB.png\" data-rel=\"lightbox-image-4\" data-rl_title=\"\" data-rl_caption=\"\" title=\"\"><img decoding=\"async\" loading=\"lazy\" class=\"aligncenter wp-image-1236 size-full\" src=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_ISpfDaelUB.png\" alt=\"\" width=\"1275\" height=\"968\" srcset=\"https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_ISpfDaelUB.png 1275w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_ISpfDaelUB-300x228.png 300w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_ISpfDaelUB-768x583.png 768w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_ISpfDaelUB-1024x777.png 1024w\" sizes=\"(max-width: 1275px) 100vw, 1275px\" \/><\/a><\/p>\n<p>the tile combines 3 layers: the buildings in the red channel, the roads in the green channel and the altitude in the blue channel, the altitude is subtle but shows on the horizon.<\/p>\n<p>raster tiles proved to be limited, especially as it sneakily re-introduced the precision issue ; as the geometric grid doesn&#8217;t correspond to the the pixels, it gives the impression that the buildings &#8220;slide&#8221; over the geometry instead of being solidly anchored in the geometry, not what we wanted.<\/p>\n<p>I tried a couple of things to create meshes, was interested in <a href=\"https:\/\/github.com\/heremaps\/tin-terrain\">TIN<\/a>\u00a0but it takes a Geotiff as an input and I didn&#8217;t manage to create it, try later.<\/p>\n<p>now the nice thing with having vector data is that we can apply various operations onto it. I pushed the pre-process a bit further and simplified, triangulated each building Polygon with <a href=\"https:\/\/opencv.org\/\">OPENCV<\/a>, then extruded and stored the tiles as binary objects.<br \/>\nand voil\u00e0! 3D tiles!<br \/>\n<img decoding=\"async\" loading=\"lazy\" class=\"aligncenter size-full wp-image-1228\" src=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_ExurDSiA41.png\" alt=\"\" width=\"1251\" height=\"953\" srcset=\"https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_ExurDSiA41.png 1251w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_ExurDSiA41-300x229.png 300w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_ExurDSiA41-768x585.png 768w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_ExurDSiA41-1024x780.png 1024w\" sizes=\"(max-width: 1251px) 100vw, 1251px\" \/>it&#8217;s not perfect and there are some holes in the caps but it&#8217;s good enough when seen from afar and it looks cool which is the only relevant metric.<\/p>\n<p>behold! the embodied nightmare of tile providers and GPU makers alike: a WebGL map of Marseille at zoom 19 ( ~3.5M triangles )<a href=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_njMg3rF4SG.png\" data-rel=\"lightbox-image-5\" data-rl_title=\"\" data-rl_caption=\"\" title=\"\"><img decoding=\"async\" loading=\"lazy\" class=\"aligncenter size-full wp-image-1215\" src=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_njMg3rF4SG.png\" alt=\"\" width=\"1920\" height=\"1080\" srcset=\"https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_njMg3rF4SG.png 1920w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_njMg3rF4SG-300x169.png 300w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_njMg3rF4SG-768x432.png 768w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_njMg3rF4SG-1024x576.png 1024w\" sizes=\"(max-width: 1920px) 100vw, 1920px\" \/><\/a><\/p>\n<p>so dense, so yummy.<\/p>\n<p><a href=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_5OaLJ55kEH.jpg\" data-rel=\"lightbox-image-6\" data-rl_title=\"\" data-rl_caption=\"\" title=\"\"><img decoding=\"async\" loading=\"lazy\" class=\"aligncenter size-full wp-image-1216\" src=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_5OaLJ55kEH.jpg\" alt=\"\" width=\"1920\" height=\"1080\" srcset=\"https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_5OaLJ55kEH.jpg 1920w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_5OaLJ55kEH-300x169.jpg 300w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_5OaLJ55kEH-768x432.jpg 768w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_5OaLJ55kEH-1024x576.jpg 1024w\" sizes=\"(max-width: 1920px) 100vw, 1920px\" \/><\/a><br \/>\nand the good part is that some buildings are well documented like this stadium<\/p>\n<p><img decoding=\"async\" loading=\"lazy\" class=\"aligncenter wp-image-1229 size-full\" src=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_8Jh5FPFIMx.png\" alt=\"\" width=\"1282\" height=\"969\" srcset=\"https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_8Jh5FPFIMx.png 1282w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_8Jh5FPFIMx-300x227.png 300w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_8Jh5FPFIMx-768x580.png 768w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_8Jh5FPFIMx-1024x774.png 1024w\" sizes=\"(max-width: 1282px) 100vw, 1282px\" \/><\/p>\n<p>which brings a great sense of detail and creates some <em>happy accidents<\/em> while navigating the city.<br \/>\nby the way, the OSM data contain a unique ID per feature so when making the tiles, I made sure to store them to prevent overlapping buildings as they may belong to various tiles. it&#8217;s even more important with roads that can cross many tiles and appear in each of the tiles. if it gives lighter tiles and prevents z-fighting, the tradeoff is that they&#8217;re not sorted : a tile may load with some &#8220;missing&#8221; buildings that appear later when the neighbouring tiles load.<\/p>\n<p>a further optimisation would consist in merging the tiles ; for now each tile contains roughly 2K vertices, I could have merged them 4 by 4 or up to a fixed size but this would have induced different loading strategies. I could also have simplified the paths more aggressively but as such, it&#8217;s not perfect but it works<\/p>\n<h2><\/h2>\n<p>&nbsp;<\/p>\n<h2>navigation &amp; tile management<\/h2>\n<p>a quick note about navigation: everything is located with a regular tile map engine, as if they were raster tiles.<br \/>\nrecomputing individual GPS coordinates of each building polygon would be prohibitively expensive for the CPU, so\u00a0the 3D tiles coordinates were computed in a 256\u00b2 space to match the size of a raster tile.\u00a0the engine then only places the 3D tiles according to the top left lat\/lng of the tile, at zoom level 16.\u00a0this still gives very high position values (in the tenth of millions units) so\u00a0everything is offset by &#8220;minus the center&#8221; of the map, this prevents very high positions values that cause the renderer to glitch because of the depth map precision (yes, even with a logarithmic depth buffer).<\/p>\n<p>there&#8217;s a second benefit in offseeting everything by minus the center of the map: the camera target is always at 0, 0, 0 !<\/p>\n<div style=\"width: 700px;\" class=\"wp-video\"><video class=\"wp-video-shortcode\" id=\"video-1208-2\" width=\"700\" height=\"300\" preload=\"metadata\" controls=\"controls\"><source type=\"video\/mp4\" src=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/xF8LrTCsAS.mp4?_=2\" \/><a href=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/xF8LrTCsAS.mp4\">http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/xF8LrTCsAS.mp4<\/a><\/video><\/div>\n<p>the map has a &#8220;display radius&#8221; that manages the tiles&#8217; visibility and there&#8217;s a vertex shader that handles the tiles&#8217; apparition, I used the rather <em>crypitc<\/em> yet so handy `onBeforeCompile` method of three.js&#8217; materials.<\/p>\n<pre class=\"lang:js decode:true \">\/\/material is a MeshStandardMaterial instance\r\nlet uniforms = { radius: { value: 1 } };\r\nmaterial.onBeforeCompile = (m) =&gt; {\r\n  m.uniforms = Object.assign(m.uniforms, uniforms);\r\n  let vs = m.vertexShader;\r\n  vs = vs.replace(\r\n    \"#include &lt;common&gt;\",\r\n    `#include &lt;common&gt;\r\n    uniform float radius;\r\n    float noise(vec2 p){\r\n      vec2 ip = floor(p);\r\n      vec2 u = fract(p);\r\n      u = u*u*(3.0-2.0*u);\r\n      float res = mix(\r\n        mix(rand(ip),rand(ip+vec2(1.0,0.0)),u.x),\r\n        mix(rand(ip+vec2(0.0,1.0)),rand(ip+vec2(1.0,1.0)),u.x),u.y);\r\n      return res*res;\r\n    }`\r\n  );\r\n  vs = vs.replace(\r\n    \"#include &lt;displacementmap_vertex&gt;\",\r\n    `#include &lt;displacementmap_vertex&gt;\r\n    float r = length( (modelMatrix * vec4( transformed, 1. ) ).xyz ) \/ radius;\r\n    r = 1. - smoothstep( .6, .75, r + .5 * ( noise(transformed.xz * 0.01 ) - noise(transformed.xz * 0.0001 ) ) );\r\n    transformed.y *= min( 1., r );`\r\n  );\r\n  m.vertexShader = vs;\r\n};<\/pre>\n<p>I control the height of the buildings by adding noise depending on the distance to the center of the world (which is &#8230; 0, 0, 0 ! thank you offset :))<\/p>\n<h2><\/h2>\n<p>&nbsp;<\/p>\n<h2>terrain and water<\/h2>\n<p>for the terrain, I thought I&#8217;d use a tileset, there&#8217;s a 1:1 match with the building tiles&#8217; locations, the data is readily available through the\u00a0<a href=\"https:\/\/registry.opendata.aws\/terrain-tiles\/\">amazon LTS<\/a> and it&#8217;s easy to compute.<\/p>\n<p><img decoding=\"async\" loading=\"lazy\" class=\"aligncenter wp-image-1240 size-full\" src=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_df19skkQdZ.png\" alt=\"\" width=\"1306\" height=\"969\" srcset=\"https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_df19skkQdZ.png 1306w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_df19skkQdZ-300x223.png 300w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_df19skkQdZ-768x570.png 768w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_df19skkQdZ-1024x760.png 1024w\" sizes=\"(max-width: 1306px) 100vw, 1306px\" \/><\/p>\n<p>well, think again tiger! couldn&#8217;t figure out exactly why but some seams appeared in 3D when they don&#8217;t show on a 2D map. <strong><a href=\"https:\/\/wwwtyro.net\/2019\/03\/21\/advanced-map-shading.html\">tyro<\/a> <\/strong>(&lt;check this out!) suggests to blend the seams by using the values of the neighbouring tiles. given the tile based nature of my map, it was too complex to set up (need to recompute up to 9 textures each time a tile was loaded).<\/p>\n<p>so instead I collected all the altitudes with this excruciatingly slow python method:<\/p>\n<pre class=\"lang:python decode:true\">import cv2\r\n# convert the elevetion to an altitude in meters\r\ndef decodeUint8Altitude(pixel):\r\n    b,g,r = pixel\r\n    return ( r * 256 + g + b \/ 256) - 32768\r\n\r\n#this parses each pixel of an image\r\ndef filter( img ):\r\n    height, width, color = img.shape\r\n    res = np.zeros( (img.shape[1], img.shape[0] ), dtype=np.float )\r\n    for y in range(height):\r\n        for x in range(width):\r\n            for c in range(color):\r\n                res[y, x] = max(0, decodeUint8Altitude(img[y, x]) )\r\n    return res\r\n#call \r\nres = filter( cv2.imread( \"tile.png\", cv2.IMREAD_UNCHANGED )\r\n#res now contains the altitudes as floats\r\n<\/pre>\n<p>I saved the altitude tiles as 8bit pngs with a 3 meters increment (not very precise but ok-ish) and computed a big png file of the altitudes, hoping to use it as a displacement map in blender.<\/p>\n<p>meanwhile I also triangulated and exported the land and water from my vector data, and merged them into OBJ files. in Blender, I cleaned them up manually, there are some insanely cool tools to decimate edges and create faces! ( edit mode &gt; edge mode : select &gt; select similar, mesh &gt; cleanup &gt; decimate geometry, you&#8217;re welcome :) )<\/p>\n<p><a href=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/blender_YlvTEASX7S.png\" data-rel=\"lightbox-image-7\" data-rl_title=\"\" data-rl_caption=\"\" title=\"\"><img decoding=\"async\" loading=\"lazy\" class=\"aligncenter size-full wp-image-1238\" src=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/blender_YlvTEASX7S.png\" alt=\"\" width=\"1920\" height=\"1017\" srcset=\"https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/blender_YlvTEASX7S.png 1920w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/blender_YlvTEASX7S-300x159.png 300w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/blender_YlvTEASX7S-768x407.png 768w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/blender_YlvTEASX7S-1024x542.png 1024w\" sizes=\"(max-width: 1920px) 100vw, 1920px\" \/><\/a><\/p>\n<p>it worked nicely for the water but unfortunately, the grid of the landmass mesh was too regular and not well defined enough so I gave up.<\/p>\n<p>first I though I&#8217;d use the altitudes texture as a displacement map in blender but instead, I used the\u00a0<a href=\"https:\/\/elevationapi.com\/\">https:\/\/elevationapi.com\/<\/a>\u00a0to produce a mesh, manually aligned it to the coastline and decimated it to reach a reasonable triangle count.\u00a0I used a smaller version of the altitudes&#8217; PNG to locate the surface and prevent the camera from going through the ground. as the ground mesh is much coarser than a tileset, I couldn&#8217;t get the roads to &#8220;stick&#8221; to the ground nicely so they float above the terrain, guess I&#8217;ll have to live with it.<\/p>\n<h2><\/h2>\n<h2>360\u00b0 panoramas &amp; titles<\/h2>\n<p>the map was fun already but there was a second important feature: 360\u00b0 panoramas.<\/p>\n<p>I happened to work on <a href=\"https:\/\/artsexperiments.withgoogle.com\/hopper\/?q=d0xQMVZydGYzVFFlTEZvbkVPM0hmQXw0LjU4MnwtNi42Nzd8MHwzNC40Mzd8ODIuNTE3fDEuMjV8MTYxNDcwOTk0NzczMg%3D%3D\">Hopper the Explorer<\/a>\u00a0and already knew a thing or two about photospheres, this time they needed a fresh take on panoramas. I suggested to use a <a href=\"https:\/\/github.com\/intel-isl\/MiDaS\">Machine Learning model<\/a> to infer depth and make the sphere appear using a depth map.<\/p>\n<p>the first transition is seen from outside the sphere to give an idea of how the geometry is distorted when using the depth map, the last bit of the video shows how it looks from inside, the intended effect,\u00a0the red ribbon is the text that was supposed to be partially occluded by the foreground.\u00a0this is the test I did to convince them it was a good idea:<\/p>\n<div style=\"width: 632px;\" class=\"wp-video\"><video class=\"wp-video-shortcode\" id=\"video-1208-3\" width=\"632\" height=\"358\" preload=\"metadata\" controls=\"controls\"><source type=\"video\/mp4\" src=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/ikA8OxngSD.mp4?_=3\" \/><a href=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/ikA8OxngSD.mp4\">http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/ikA8OxngSD.mp4<\/a><\/video><\/div>\n<p>and the least I can say is that they were <strong>not<\/strong> convinced ^^&#8217;<\/p>\n<p>so we dumped the depth maps and changed for a much more qualitative approach, closer to the art direction: Axel created 2D masks for the foreground to partially hide the text and did some transition mockups using an\u00a0<strong>optics compensation<\/strong> effect (aka a <strong>pincushion effect<\/strong>).<\/p>\n<p>the end result looks somewhat like this (better <a href=\"https:\/\/marseille.laphase5.com\/en\/\">check it live<\/a> though).<\/p>\n<div style=\"width: 700px;\" class=\"wp-video\"><video class=\"wp-video-shortcode\" id=\"video-1208-4\" width=\"700\" height=\"365\" preload=\"metadata\" controls=\"controls\"><source type=\"video\/mp4\" src=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/SmeaaugOHO.mp4?_=4\" \/><a href=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/SmeaaugOHO.mp4\">http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/SmeaaugOHO.mp4<\/a><\/video><\/div>\n<p>here&#8217;s the fragment shader used during the post processing, it takes 2 textures as input, tDiffuse the map, tSphere, the panorama, + a `mixer` value.<\/p>\n<pre class=\"lang:c# decode:true\">uniform float mixer;\r\nuniform sampler2D tDiffuse;\r\nuniform sampler2D tSphere;\r\nvarying vec2 vUv;\r\n\/\/idea\r\n\/\/https:\/\/www.decarpentier.nl\/lens-distortion\r\n\/\/distort method from \r\n\/\/ https:\/\/www.imaginationtech.com\/blog\/speeding-up-gpu-barrel-distortion-correction-in-mobile-vr\/\r\nvec2 distort(vec2 st, float alpha, float expo ){\r\n    vec2 p1 = vec2(2.0 * st - 1.0);\r\n    vec2 p2 = p1 \/ (1.0 - alpha * length( p1 ) * expo );\r\n    return (p2 + 1.0) * 0.5;\r\n}\r\nvoid main() {\r\n    float t = mixer;\r\n    float tr = smoothstep( 0.75,1., t );\r\n    float e = t * 4.;\r\n    \r\n    \/\/stretch out city scene\r\n    vec2 uv = distort( vUv, -10. * pow( .5 + t * .5, 32. ), e );\r\n    vec4 city = texture2D( tDiffuse, uv );\r\n    \r\n    \/\/pinch in sphere scene\r\n    uv = distort( vUv, -10. * ( 1. - tr ), e );\r\n    vec4 sphere = texture2D( tSphere, uv );\r\n    \/\/blend    \r\n    gl_FragColor = mix( city, sphere, tr );\r\n}<\/pre>\n<p>the titles needed to be animated letter by letter and localized, Axel pointed me to the JS library they use at <a href=\"https:\/\/www.fonts.ninja\/\">Fonts Ninja<\/a>\u00a0and I was impressed!<br \/>\n<a href=\"https:\/\/github.com\/opentypejs\/opentype.js\">opentype.js<\/a>\u00a0will give you access to the vector data of a font, with a proper kerning, it allowed to write arbitrary texts on the fly (I should probably start using SDF Fonts at some point).<\/p>\n<p>here&#8217;s a test of the split letters word<\/p>\n<p><img decoding=\"async\" loading=\"lazy\" class=\"aligncenter wp-image-1253 size-full\" src=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_ZiXuIUZTNS.png\" alt=\"\" width=\"1308\" height=\"432\" srcset=\"https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_ZiXuIUZTNS.png 1308w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_ZiXuIUZTNS-300x99.png 300w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_ZiXuIUZTNS-768x254.png 768w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/chrome_ZiXuIUZTNS-1024x338.png 1024w\" sizes=\"(max-width: 1308px) 100vw, 1308px\" \/><\/p>\n<p>and a quick animation test.<\/p>\n<div style=\"width: 692px;\" class=\"wp-video\"><video class=\"wp-video-shortcode\" id=\"video-1208-5\" width=\"692\" height=\"378\" preload=\"metadata\" controls=\"controls\"><source type=\"video\/mp4\" src=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/DH8KauTbFE.mp4?_=5\" \/><a href=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/DH8KauTbFE.mp4\">http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/DH8KauTbFE.mp4<\/a><\/video><\/div>\n<p>one of my regrets is not to have had more time to polish the text animation.<\/p>\n<p>&nbsp;<\/p>\n<p>&nbsp;<\/p>\n<h2>Post processing LUT<\/h2>\n<p>at some point Axel told me he used Photoshop LUTs (Look Up Table) to beautify the C4D renders and asked if we could have this on the website. little did I know how much of a pain it would be. first, a LUT is a tool to transform a pixel value, like a filter. to disambiguate, what most people call a LUT is often a palette or gradient mapping ; using a greyscale image input, we fetch the corresponding value in a palette (often a gradient) and replace the greyscale value with the color. here&#8217;s a three.js example:\u00a0<a href=\"https:\/\/threejs.org\/examples\/?q=lookuptable#webgl_geometry_colors_lookuptable\">https:\/\/threejs.org\/examples\/?q=lookuptable#webgl_geometry_colors_lookuptable<\/a>\u00a0so it&#8217;s usually limited to 256 discrete values and gives a very cartoon look to the final image ( it works very well for cell shading ).<\/p>\n<p>photoshop LUTs map each pixel of an image to a\u00a032768 (32^3) RGB values and give much more subtle results.<\/p>\n<p><strong>FUN FACT<\/strong> three.js does have a <a href=\"https:\/\/threejs.org\/examples\/?q=LUT#webgl_postprocessing_3dlut\">3D LUT post processing pass<\/a> since <a href=\"https:\/\/github.com\/mrdoob\/three.js\/pull\/20558\">r122<\/a>, I was simply using the r120 and by the time I realized it was in there, I had a working alternative. I&#8217;m leaving the code here for &#8220;learning purpose&#8221; or in case the 3D LUT uses 3D textures (I haven&#8217;t checked, I&#8217;m still too pissed!)<\/p>\n<p>to illustrate, the image below is the LUT I used, as rendered by photoshop.\u00a0on the left hand side, it&#8217;s a series of 64* ( 64 * 64 pixels ) quads that gradually brighten up. think of it as the slices of a box made of 63^3 cubes. on the right hand side, the LUT was applied to the same grid and &#8211; hopefully &#8211; you see that the colors behave differently. used on an image, this will produce the (in)famous Instagram filter look.<\/p>\n<p><a href=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/Photoshop_cQrIBX4nrv.png\" data-rel=\"lightbox-image-8\" data-rl_title=\"\" data-rl_caption=\"\" title=\"\"><img decoding=\"async\" loading=\"lazy\" class=\"aligncenter size-full wp-image-1239\" src=\"http:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/Photoshop_cQrIBX4nrv.png\" alt=\"\" width=\"1241\" height=\"467\" srcset=\"https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/Photoshop_cQrIBX4nrv.png 1241w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/Photoshop_cQrIBX4nrv-300x113.png 300w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/Photoshop_cQrIBX4nrv-768x289.png 768w, https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/Photoshop_cQrIBX4nrv-1024x385.png 1024w\" sizes=\"(max-width: 1241px) 100vw, 1241px\" \/><\/a><\/p>\n<p>in the app, we&#8217;ll use the right hand texture as our LUT.<\/p>\n<p>I asked some creative dev friends for help, they pointed to Matt <a href=\"https:\/\/github.com\/mattdesl\/glsl-lut\">DesLauriers implementation<\/a>\u00a0which helped a lot, I then used it as part of the post process pipeline. for the record here&#8217;s the code.<br \/>\nfirst a python script that generates the grid above:<\/p>\n<pre class=\"lang:python decode:true \">import cv2\r\nimport numpy as np\r\nsize = 64\r\nbuffer = np.zeros( (512, 512,3), dtype=np.uint8)\r\nfor b in range( 0, size ):\r\n    x = ( b % 8 ) * size\r\n    y = ( int( b \/ 8 ) ) * size\r\n    for r in range( 0, size ):\r\n        for g in range( 0, size ):\r\n            buffer[ y+g:y+g+1, x+r:x+r+1, 0 ] = int( b * 4 )\r\n            buffer[ y+g:y+g+1, x+r:x+r+1, 1 ] = int( g * 4 )\r\n            buffer[ y+g:y+g+1, x+r:x+r+1, 2 ] = int( r * 4 )\r\ncv2.imwrite( \"lut_src.png\", buffer )<\/pre>\n<p>this is not required by the app but it can help checking in photoshop and\/or debug in WebGL.<\/p>\n<p>the ShaderPass goes like:<\/p>\n<pre class=\"lang:js decode:true \">let LUTShader = {\r\n  uniforms: {\r\n    tDiffuse: { value: null },\r\n    tLut: { value: null },\r\n    opacity: { value: 1.0 },\r\n  },\r\n  vertexShader: `\r\n  varying vec2 vUv;\r\n  void main() {\r\n  \tvUv = uv;\r\n  \tgl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\r\n  }`,\r\n  fragmentShader: `\r\n  uniform float opacity;\r\n  uniform sampler2D tDiffuse;\r\n  uniform sampler2D tLut;\r\n  varying vec2 vUv;\r\n  vec4 lookup(in vec4 textureColor, in sampler2D lookupTable) {\r\n    \/\/square index by blue level (linear ; it gives the cell index in a 'flat' array)\r\n    float b = ( textureColor.b ) * 63.;\r\n    \/\/ current cell id : we remove the fractional part to \"snap\" to the previous integer (ie 12.3564546 &gt; 12)\r\n    float fb = floor( b ); \r\n    \/\/ current cell uv\r\n    \/\/ the x position of the cell in the grid is given by : mod( cellId, columns)\r\n    \/\/ the y position of the cell in the grid is given by : floor( cellId \/ columns)\r\n    vec2 uv0 = vec2( mod( fb, 8. ), floor( fb \/ 8. ) );\r\n    \/\/ and everything is downscaled by : 1 \/ columns ( 1. \/ .8 in this case as 64 px cell size = 512 px texture size \/ 8 )\r\n    float grid_cell = 1. \/ 8.;\r\n    \/\/this is the top left corner of the cell\r\n    uv0 *= grid_cell;\r\n    \/\/we need to \"pad\" the cell to make sure we hit the center of the cell's corner pixels\r\n    \/\/padding = half a pixel so: .5 \/ 512.\r\n    float padding = .5 \/ 512.;\r\n    vec2 cell_offset = vec2( padding );\r\n    \/\/the must be a little smaller too: the width = 2\r\n    vec2 cell_size = vec2( grid_cell - 2. * padding );\r\n    \/\/we add the offset to the uv and finally we lerp the uv between the top left and bottom right corners of the cell\r\n    \/\/we use the r an g value of the texel\r\n    uv0 += cell_offset + textureColor.rg * cell_size;\r\n    \/\/ next cell id and uv (same as above with the next index in the 'flat list' )\r\n    float cb = ceil( b );\/\/min( fb+1., 63. ); \r\n    vec2 uv1 = grid_cell * vec2( floor( mod( cb, 8. ) ), floor( cb \/ 8. ) ); \r\n    uv1 += cell_offset + textureColor.rg * cell_size;\r\n    \/\/flip Y\r\n    uv0.y = 1. - uv0.y;\r\n    uv1.y = 1. - uv1.y;\r\n    \/\/now we sample the 2 colors from the lookup table\r\n    vec4 c1 = texture2D(lookupTable, uv0);\r\n    vec4 c2 = texture2D(lookupTable, uv1);\r\n    \/\/and mix them using the fractional part of the ( blue value * cell count )\r\n    return mix(c1, c2, fract( b ) );\r\n  }\r\n  void main() {\r\n    vec4 texel = texture2D( tDiffuse, vUv );\r\n    gl_FragColor = lookup( texel, tLut );\r\n  }\r\n  `,\r\n};\r\nexport { LUTShader };<\/pre>\n<p>and finally, to use it with three.js effectComposer:<\/p>\n<pre class=\"lang:js decode:true \">renderer.autoClear = false;\r\nlet composer = new EffectComposer(renderer);\r\nconst renderPass = new RenderPass(scene, camera);\r\ncomposer.addPass(renderPass);\r\nlet LUTPass = new ShaderPass(LUTShader);\r\nlet loader = new TextureLoader();\r\nloader.load(\"\/textures\/lut.png\", (t) =&gt; {\r\n  LUTPass.uniforms.tLut.value = t;\r\n  t.magFilter = t.minFilter = LinearFilter; \/\/ important!\r\n});\r\ncomposer.addPass(LUTPass);<\/pre>\n<p>I wanted to add a bokeh filter too but it was deemed to CPU intensive and was removed (still available in the <a href=\"https:\/\/marseille.laphase5.com\/?debug=true\">debug panel<\/a>\u00a0under\/params\/postprocess) .<\/p>\n<h2><\/h2>\n<h2><\/h2>\n<h2>wrap up<\/h2>\n<p>what a &#8220;fun little project&#8221;!\u00a0I learnt a lot and it was so nice to work with very skilled people, <a href=\"https:\/\/twitter.com\/axelCorjon\">Axel<\/a> (AD, 2D\/3D motion &amp; visuals&#8230;) &amp; <a href=\"https:\/\/www.laphase5.com\/\">Emmanuel<\/a> (Back, Front, Photos&#8230;) are very talented and very demanding. I like that very much.<br \/>\nby the way the debug panel is still active, you can try :\u00a0https:\/\/marseille.laphase5.com\/?debug=true and play with some of the stuff, overshoot the LUT, add bokeh, change the colors&#8230;<\/p>\n<p>enjoy! :)<\/p>\n","protected":false},"excerpt":{"rendered":"<p>last year I was contacted by\u00a0la phase 5 , they needed help with the WebGL part of their 2021 wish card. they&#8217;re based in Marseille, the second biggest french city, by the mediterranean sea. it&#8217;s quite a unique city, both classy and cheap, packed and desert,\u00a0La Phase 5&#8217;s plan\u00a0was to write a love letter to &#8230; <span class=\"more\"><a class=\"more-link\" href=\"https:\/\/barradeau.com\/blog\/?p=1208\">[Read more&#8230;]<\/a><\/span><\/p>\n","protected":false},"author":1,"featured_media":1264,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"sharing_disabled":false,"spay_email":"","jetpack_publicize_message":""},"categories":[3],"tags":[8,10,9],"jetpack_featured_media_url":"https:\/\/barradeau.com\/blog\/wp-content\/uploads\/2021\/02\/brave_2W4vG1m2mf.jpg","jetpack_publicize_connections":[],"jetpack_shortlink":"https:\/\/wp.me\/p4oXhx-ju","_links":{"self":[{"href":"https:\/\/barradeau.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/1208"}],"collection":[{"href":"https:\/\/barradeau.com\/blog\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/barradeau.com\/blog\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/barradeau.com\/blog\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/barradeau.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=1208"}],"version-history":[{"count":37,"href":"https:\/\/barradeau.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/1208\/revisions"}],"predecessor-version":[{"id":1269,"href":"https:\/\/barradeau.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/1208\/revisions\/1269"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/barradeau.com\/blog\/index.php?rest_route=\/wp\/v2\/media\/1264"}],"wp:attachment":[{"href":"https:\/\/barradeau.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1208"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/barradeau.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1208"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/barradeau.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1208"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}