This “magnificent 360-degree panorama” of the Milky Way was created by the European Southern Observatory. The source image is in equirectangular coordinates, and is reprojected here using the stereographic projection, simulating the view of the night sky from varying positions on Earth.
This interactive requires WebGL, but see an earlier, slower software implementation of raster reprojection for comparison.
xxxxxxxxxx
<meta charset="utf-8">
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
<canvas></canvas>
<script id="vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;
void main(void) {
gl_Position = vec4(a_position, 0.0, 1.0);
}
</script>
<script id="fragment-shader" type="x-shader/x-fragment">
precision mediump float;
uniform sampler2D u_image;
uniform vec2 u_translate;
uniform float u_scale;
uniform vec2 u_rotate;
const float c_pi = 3.14159265358979323846264;
const float c_halfPi = c_pi * 0.5;
const float c_twoPi = c_pi * 2.0;
float cosphi0 = cos(u_rotate.y);
float sinphi0 = sin(u_rotate.y);
void main(void) {
float x = (gl_FragCoord.x - u_translate.x) / u_scale;
float y = (u_translate.y - gl_FragCoord.y) / u_scale;
// inverse stereographic projection
float rho = sqrt(x * x + y * y);
float c = 2.0 * atan(rho);
float sinc = sin(c);
float cosc = cos(c);
float lambda = atan(x * sinc, rho * cosc);
float phi = asin(y * sinc / rho);
// inverse rotation
float cosphi = cos(phi);
float x1 = cos(lambda) * cosphi;
float y1 = sin(lambda) * cosphi;
float z1 = sin(phi);
lambda = atan(y1, x1 * cosphi0 + z1 * sinphi0) + u_rotate.x;
phi = asin(z1 * cosphi0 - x1 * sinphi0);
gl_FragColor = texture2D(u_image, vec2((lambda + c_pi) / c_twoPi, (phi + c_halfPi) / c_pi));
}
</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 = "milky-way.jpg";
image.onload = readySoon;
self.onresize = resize;
// Hack to ensure correct inference of window dimensions.
function readySoon() {
setTimeout(function() {
resize();
ready();
}, 10);
}
function resize() {
var width = Math.max(960, self.innerWidth),
height = Math.max(500, self.innerHeight);
canvas.setAttribute("width", width);
canvas.setAttribute("height", height);
context.uniform2f(translateUniform, width / 2, height / 2);
context.uniform1f(scaleUniform, 500);
context.viewport(0, 0, width, height);
}
function ready() {
// 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.generateMipmap(context.TEXTURE_2D);
// The current rotation and speed.
var rotate = [0, 0],
speed = [-.001, .0004];
redraw();
// Rotate and redraw!
function redraw() {
rotate[0] += speed[0], rotate[1] += speed[1];
context.uniform2fv(rotateUniform, rotate);
context.bindTexture(context.TEXTURE_2D, texture); // XXX Safari
context.drawArrays(context.TRIANGLE_FAN, 0, 4);
requestAnimationFrame(redraw);
}
}
// A polyfill for requestAnimationFrame.
if (!self.requestAnimationFrame) requestAnimationFrame =
self.webkitRequestAnimationFrame
|| self.mozRequestAnimationFrame
|| self.msRequestAnimationFrame
|| self.oRequestAnimationFrame
|| function(f) { setTimeout(f, 17); };
</script>