Note: You need the current master build of QGIS or wait for QGIS 3.10. I’m a cool kid.

Load a raster layer in QGIS.

Add a new Scratch Layer of Polygon type (in any CRS). Set its Symbology to Inverted Polygons. Use a Geometry Generator as symbol layer type. Set it to LineString/MultiLineString. Enter the expression below and adjust the layer ID (best enter the expression editor and find it in the “layers” tree). Then adjust the scaling factor at the very bottom.























-- UPPER CASE comments below are where you can change things with_variable( 'raster_layer', 'long_and_complicated_layer_id', -- RASTER LAYER to sample from -- this collects all the linestrings generated below into one multilinestring collect_geometries( -- a loop for each y value of the grid array_foreach( -- array_foreach loops over all elements of the series generated below -- which is a range of numbers from the bottom to the top of y values -- of the map canvas extent coordinates. -- the result will be an array of linestrings generate_series( y(@map_extent_center)-(@map_extent_height/2), -- bottom y y(@map_extent_center)+(@map_extent_height/2), -- top y @map_extent_height/50 -- stepsize -> HOW MANY LINES ), -- we want to enter another loop so we assign the name 'y' to -- the current element of the array_foreach loop with_variable( 'y', @element, -- now we are ready to generate the line for this y value make_line( -- another loop, this time for the x values. same logic as before -- the result will be an array of points array_foreach( generate_series( x(@map_extent_center)-(@map_extent_width/2), -- left x x(@map_extent_center)+(@map_extent_width/2), -- right x @map_extent_width/50 -- stepsize -> HOW MANY POINTS PER LINE ), -- and here we create each point of the line make_point( @element, -- the current value from the loop over the x value range @y -- the y value from the outer loop + -- will get an additional offset to generate the effect -- we look for values at _this point_ in the raster, and since -- the raster might not have any value here, we must use coalesce -- to use a replacement value in those cases coalesce( -- coalesce to catch raster null values raster_value( @raster_layer, 1, -- band 1, *snore* -- to look up the raster value we need to look in the right position -- so we make a sampling point in the same CRS as the raster layer transform( make_point(@element, @y), @map_crs, layer_property(@raster_layer,'crs') ) ), 0 -- coalesce 0 if raster_value gave null -- here is where we set the scaling factor for the raster -> y values -- if things are weird, set it to 0 and try small multiplications or divisions -- to see what happens. -- for metric systems you will want to multiply -- for geographic coordinates you will want to divide )*10 -- user-defined factor for VERTICAL EXAGGERATION ) ) ) ) ) ) ) -- wee

If you don’t have your raster data on a SSD this can be a bit slow.

Yes, this works if you change your CRS!