Skip to content

Instantly share code, notes, and snippets.

@HarryStevens HarryStevens/.block

Last active Feb 5, 2019
Embed
What would you like to do?
Raster Reprojection II
license: gpl-3.0

Using WebGL, you can reproject equirectangular raster tiles onto a Canvas with an orthographic projection. You can also combine the Canvas with SVG raster tiles.

WebGL is far more performant than HTML5 Canvas. For a comparison, see Raster Reprojection I.

The image is from Wikimedia Commons and described thus:

Heightmap of Earth's surface (including water and ice) in equirectangular projection, normalized as 8-bit grayscale, where lighter values indicate higher elevation. Sea level is shown as #0c0c0c.

Most of the code for this block comes from Mike Bostock, Jason Davies and Philippe Rivière.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
margin: 0;
}
canvas {
position: absolute;
z-index: -1;
}
.country, .boundary {
fill: none;
stroke: #fff;
}
.country {
stroke-opacity: .2;
stroke-dasharray: 5, 5;
}
.boundary {
stroke-opacity: .6;
}
</style>
</head>
<body>
<canvas></canvas>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/topojson@3.0.2/dist/topojson.min.js"></script>
<script id="vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;
varying vec2 pos;
void main(void) {
gl_Position = vec4(a_position, 0.0, 1.0);
pos = a_position;
}
</script>
<script id="fragment-shader" type="x-shader/x-fragment">
precision mediump float;
uniform sampler2D u_image;
uniform vec2 u_translate; /* width/2, height/2 */
uniform float u_scale; /* in pixels ! */
uniform vec3 u_rotate; /* rotation in degrees ! */
const float c_pi = 3.14159265358979323846264;
const float c_halfPi = c_pi * 0.5;
const float c_twoPi = c_pi * 2.0;
void applyRotation(in float rotatex, in float rotatey, in float rotatez,
inout float lambda, inout float phi) {
float x, y, rho, c, cosphi, z, deltaLambda, deltaPhi, deltaGamma, cosDeltaPhi,
sinDeltaPhi, cosDeltaGamma, sinDeltaGamma, k, circle, proj, a, b;
cosphi = cos(phi);
x = cos(lambda) * cosphi;
y = sin(lambda) * cosphi;
z = sin(phi);
// inverse rotation
deltaLambda = rotatex / 90.0 * c_halfPi; // rotate[0]
deltaPhi = -rotatey / 90.0 * c_halfPi; // rotate[1]
deltaGamma = -rotatez / 90.0 * c_halfPi; // rotate[2]
cosDeltaPhi = cos(deltaPhi);
sinDeltaPhi = sin(deltaPhi);
cosDeltaGamma = cos(deltaGamma);
sinDeltaGamma = sin(deltaGamma);
k = z * cosDeltaGamma - y * sinDeltaGamma;
lambda = atan(y * cosDeltaGamma + z * sinDeltaGamma,
x * cosDeltaPhi + k * sinDeltaPhi) -
deltaLambda;
k = k * cosDeltaPhi - x * sinDeltaPhi;
if (k > 0.99999)
k = 0.99999; // south pole (for some reason it goes > 1 near the pole??)
if (k < -0.99999)
k = -0.99999; // north pole
phi = asin(k);
}
void main(void) {
float x = (gl_FragCoord.x - u_translate.x) / u_scale;
float y = (u_translate.y - gl_FragCoord.y) / u_scale;
// Inverse orthographic projection
float rho = sqrt(x * x + y * y);
// Color the point (px, py) only if it exists in the texture
if (rho < 1.0) {
float c = asin(rho);
float sinc = sin(c);
float cosc = cos(c);
float lambda = atan(x * sinc, rho * cosc);
float phi = asin(y * sinc / rho);
// Apply the three-axis rotation
applyRotation(u_rotate.x, u_rotate.y, u_rotate.z, lambda, phi);
// pixels
float px = fract((lambda + c_pi) / c_twoPi);
float py = fract((phi + c_halfPi) / c_pi);
gl_FragColor = texture2D(u_image, vec2(px, py));
float intensity = 1.1; // boost the pixel by some factor
gl_FragColor[0] = intensity * gl_FragColor[0] * (1.3 - 0.3 * sqrt(gl_FragColor[0]));
gl_FragColor[1] = intensity * gl_FragColor[1];
gl_FragColor[2] = intensity * gl_FragColor[2];
}
}
</script>
<script>
// Select the canvas from the document.
var canvas = document.querySelector("canvas");
// Create the WebGL context, with fallback for experimental support.
var context = canvas.getContext("webgl")
|| canvas.getContext("experimental-webgl");
// Compile the vertex shader.
var vertexShader = context.createShader(context.VERTEX_SHADER);
context.shaderSource(vertexShader, document.querySelector("#vertex-shader").textContent);
context.compileShader(vertexShader);
if (!context.getShaderParameter(vertexShader, context.COMPILE_STATUS)) throw new Error(context.getShaderInfoLog(vertexShader));
// Compile the fragment shader.
var fragmentShader = context.createShader(context.FRAGMENT_SHADER);
context.shaderSource(fragmentShader, document.querySelector("#fragment-shader").textContent);
context.compileShader(fragmentShader);
if (!context.getShaderParameter(fragmentShader, context.COMPILE_STATUS)) throw new Error(context.getShaderInfoLog(fragmentShader));
// Link and use the program.
var program = context.createProgram();
context.attachShader(program, vertexShader);
context.attachShader(program, fragmentShader);
context.linkProgram(program);
if (!context.getProgramParameter(program, context.LINK_STATUS)) throw new Error(context.getProgramInfoLog(program));
context.useProgram(program);
// Define the positions (as vec2) of the square that covers the canvas.
var positionBuffer = context.createBuffer();
context.bindBuffer(context.ARRAY_BUFFER, positionBuffer);
context.bufferData(context.ARRAY_BUFFER, new Float32Array([
-1.0, -1.0,
+1.0, -1.0,
+1.0, +1.0,
-1.0, +1.0
]), context.STATIC_DRAW);
// Bind the position buffer to the position attribute.
var positionAttribute = context.getAttribLocation(program, "a_position");
context.enableVertexAttribArray(positionAttribute);
context.vertexAttribPointer(positionAttribute, 2, context.FLOAT, false, 0, 0);
// Extract the projection parameters.
var translateUniform = context.getUniformLocation(program, "u_translate"),
scaleUniform = context.getUniformLocation(program, "u_scale"),
rotateUniform = context.getUniformLocation(program, "u_rotate");
// Load the reference image.
var image = new Image;
image.src = "raster.jpg";
image.onload = readySoon;
// SVG map declarations
var svg = d3.select("body").append("svg");
var projection = d3.geoOrthographic();
var path = d3.geoPath(projection);
// Hack to ensure correct inference of window dimensions.
function readySoon() {
d3.json("world.json", (error, map) => {
if (error) throw error;
var feature = topojson.feature(map, map.objects.countries),
mesh = topojson.feature(map, map.objects.land);
setTimeout(() => {
resize(feature);
ready(feature, mesh);
}, 10);
self.onresize = () => resize(feature);
});
}
function resize(feature) {
var w = self.innerWidth, h = self.innerHeight;
var width = Math.min(w, h),
height = width;
// The canvas is absolutely positioned in order to overlay it with the SVG.
// To get it to center, do some math.
d3.select(canvas).style("left", ((w - width) / 2) + "px");
canvas.setAttribute("width", width);
canvas.setAttribute("height", height);
context.uniform2f(translateUniform, width / 2, height / 2);
context.uniform1f(scaleUniform, height / 2);
context.viewport(0, 0, width, height);
// Basic D3 + TopoJSON map
svg.attr("width", w).attr("height", h);
projection.fitSize([w, height], feature);
}
function ready(feature, mesh) {
// Create a texture and a mipmap for accurate minification.
var texture = context.createTexture();
context.bindTexture(context.TEXTURE_2D, texture);
context.texParameteri(context.TEXTURE_2D, context.TEXTURE_MAG_FILTER, context.LINEAR);
  context.texParameteri(context.TEXTURE_2D, context.TEXTURE_MIN_FILTER, context.LINEAR_MIPMAP_LINEAR);
context.texImage2D(context.TEXTURE_2D, 0, context.RGBA, context.RGBA, context.UNSIGNED_BYTE, image);
context.texParameteri(context.TEXTURE_2D, context.TEXTURE_WRAP_S, context.CLAMP_TO_EDGE);
context.texParameteri(context.TEXTURE_2D, context.TEXTURE_WRAP_T, context.CLAMP_TO_EDGE);
context.texParameteri(context.TEXTURE_2D, context.TEXTURE_MIN_FILTER, context.LINEAR); // or NEAREST
// The current rotation and speed.
var rotate =  [0, 0, 0],
speed = .5;
redraw();
// Rotate and redraw!
function redraw() {
rotate = rotate.map(d => d += speed);
context.uniform3fv(rotateUniform, rotate); // Three-axis rotation
context.bindTexture(context.TEXTURE_2D, texture); // XXX Safari
context.drawArrays(context.TRIANGLE_FAN, 0, 4);
requestAnimationFrame(redraw);
projection.rotate(rotate);
var countries = svg.selectAll(".country")
.data(feature.features);
countries.enter().append("path")
.attr("class", "country")
.merge(countries)
.attr("d", path);
var boundaries = svg.selectAll(".boundary")
.data([mesh]);
boundaries.enter().append("path")
.attr("class", "boundary")
.merge(boundaries)
.attr("d", path);
}
}
// A polyfill for requestAnimationFrame.
if (!self.requestAnimationFrame) requestAnimationFrame =
self.webkitRequestAnimationFrame
|| self.mozRequestAnimationFrame
|| self.msRequestAnimationFrame
|| self.oRequestAnimationFrame
|| function(f) { setTimeout(f, 17); };
</script>
</body>
</html>
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.