Skip to content

Instantly share code, notes, and snippets.

@Fil
Last active Oct 5, 2019
Embed
What would you like to do?
Mars on a WebGL globe
license: mit

Preparatory work for Missions to Mars.

 

Problems solved:

-- invert geoOrthographic (instead of geoStereographic).

-- what to do for pixels outside the globe

-- white artifacts on the boundary of the jpg image (they were on the original image!)

-- upgrade to d3 v4.

-- made sure the image's center has lonlat = [0,0]

-- got rid of the pesky dot in the center on iPhone (reported by Harry Stevens)

Forked from @mbostock's Milky Way block. License GPL-3.

Image: USGS.

<!DOCTYPE html>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v5.min.js"></script>
<!--
*Note to self: do not edit this block on blockbuilder*
-->
<style>
body {
margin: 0;
overflow: hidden;
background: black;
font-family: "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif;
}
canvas {
cursor: move;
}
svg, canvas {
position:absolute;
top:0;
}
.missions path { fill: white }
.polygons path {
fill: rgba(0,0,0,0.001); /* to receive mouseover events */
stroke: rgba(200,255,220,0.1);
stroke-width: 1.5;
}
.links path {
fill: none;
stroke: rgba(180,90,90,0.9);
stroke-width: 3;
}
path.glow {
fill: none;
stroke: rgba(255,255,255,0.7);
stroke-width: 1;
}
</style>
<canvas></canvas>
<svg>
<defs>
<radialGradient id="grad1" cx="50%" cy="50%" r="50%" fx="50%" fy="50%">
<stop offset="0%" style="stop-color:black;stop-opacity:0" />
<stop offset="94%" style="stop-color:black;stop-opacity:0.01" />
<stop offset="97%" style="stop-color:black;stop-opacity:0.2" />
<stop offset="110%" style="stop-color:black;stop-opacity:0.6" />
</radialGradient>
</defs>
</svg>
<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; /* 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;
// Inclination of the equator on Mars = 25.19° (earth= 23.44)
const float declination = 25.19 / 90.0 * c_halfPi;
float phi0 = -u_rotate.y / 90.0 * c_halfPi;
float cosphi0 = cos(phi0);
float sinphi0 = sin(phi0);
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 if the point (px, py) does not exist in the texture
if (rho >= 1.0) {
gl_FragColor = texture2D(u_image, vec2(0.0, 0.0));
gl_FragColor[0] = 0.1*(rho-1.0+0.1);
gl_FragColor[1] = 0.06*(rho-1.0+0.1);
gl_FragColor[2] = 0.2*(rho-1.0+0.1);
}
else {
float cosc = cos(asin(rho));
float lambda = atan(x, cosc);
float phi = asin(y);
// inverse rotation
float cosphi = cos(phi);
float x0 = cos(lambda) * cosphi;
float y0 = sin(lambda) * cosphi;
float cosgamma = cos(u_rotate.z / 90.0 * c_halfPi);
float singamma = sin(u_rotate.z / 90.0 * c_halfPi);
float x1 = x0 * cosgamma - y0 * singamma;
float y1 = y0 * cosgamma + x0 * singamma;
float z1 = y;
lambda = atan(y1, x1 * cosphi0 + z1 * sinphi0) - u_rotate.x / 90.0 * c_halfPi;
phi = asin(z1 * cosphi0 - x1 * sinphi0);
// pixels
float px = (lambda + c_pi) / c_twoPi;
float py = (phi + c_halfPi) / c_pi;
gl_FragColor = texture2D(u_image, vec2(px, py));
// terminator ?? see https://github.com/joergdietrich/Leaflet.Terminator/blob/master/L.Terminator.js
// float sinh = sin(lambda)*sin(declination) + cos(lambda)*cos(declination)*cos(1.0);
// float intensity = (sinh > 0.0) ? 1.0 + 0.1*sinh : 0.2 + 0.8 * exp(6.0*sinh);
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");
var width = +canvas.getAttribute('width') || 600,
height = +canvas.getAttribute('height') || 400;
width = Math.max(width, self.innerWidth);
height = Math.max(height, self.innerHeight);
canvas.setAttribute('width', width);
canvas.setAttribute('height', height);
// 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.crossOrigin="anonymous";
image.src = "https://gist.githubusercontent.com/Fil/358e889380bfc9d8e4871cc9dc338cf9/raw/745cff357604fd7780b6efafec56dd7455a4a8d3/Mars_Viking_MDIM21_ClrMosaic_global_1024.jpg";
image.onload = readySoon;
var projection = d3.geoOrthographic()
.translate([width / 2, height / 2])
.scale(0.95 * height / 2);
var path = d3.geoPath()
.projection(projection);
var svg = d3.select('svg')
.attr('width', width)
.attr('height', height);
svg._defs = svg.append("defs");
svg._clip = svg._defs.append("path")
.datum({
type: "Sphere"
})
.attr("id", "sphere");
svg._defs.append("clipPath")
.attr("id", "clip")
.append("use")
.attr("xlink:href", "#sphere");
svg._earth = svg.append('g')
.attr("xlink:href", "#sphere")
.style('cursor', '-webkit-grab');
svg._earth.append('use')
.attr("xlink:href", "#sphere")
.attr('fill', 'rgba(0,0,0,0.001)'); /* to receive mouseover events */
svg._shade = svg.append("use")
.attr("class", "stroke")
.attr("xlink:href", "#sphere")
.attr("stroke", "black")
.attr("stroke-width", 3)
.attr("fill", "url(#grad1)")
.attr('pointer-events', 'none');
// Hack to ensure correct inference of window dimensions.
function readySoon() {
setTimeout(function () {
resize();
ready();
}, 10);
}
function resize() {
canvas.setAttribute("width", width);
canvas.setAttribute("height", height);
context.uniform2f(translateUniform, width / 2, height / 2);
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
var scale = scale0 = projection.scale(),
rotate = [0, 0, 0];
// Rotate and redraw!
function redraw() {
projection.scale(scale).rotate(rotate);
svg._clip.attr('d', path);
context.uniform1f(scaleUniform, scale);
context.uniform3fv(rotateUniform, rotate);
context.bindTexture(context.TEXTURE_2D, texture); // XXX Safari
context.drawArrays(context.TRIANGLE_FAN, 0, 4);
}
svg._earth
.on('mousemove', function () {
var p = d3.mouse(this),
c = projection.invert(p);
/* do something with coords c */
});
var lambda = d3.scaleLinear()
.domain([-width / 2, width / 2])
.range([-180, 180]);
var phi = d3.scaleLinear()
.domain([0, height])
.range([90, -90]);
var q, r, transform, d;
zoom = d3.zoom()
.scaleExtent([.8, 1.5])
.on("start", function () {
q = rotate, d = [0, 0, 0]; // accumulate change in d
r = d3.mouse(this);
svg._earth.style('cursor', '-webkit-grabbing');
})
.on("zoom.redraw", function () {
scale = scale0 * d3.event.transform.k;
var p = d3.mouse(this);
var dr = [lambda(p[0]) - lambda(r[0]), phi(p[1]) - phi(r[1])];
r = p;
// inverse dr[0] if the mouse is beyond one of the poles
var a = (phi(p[1]) - rotate[1]) * Math.PI / 180,
ca = Math.cos(a),
sa = Math.sin(a);
d = [d[0] + dr[0] * (ca < 0 ? -1 : 1),
d[1] + dr[1], d[2] + dr[0] * -sa];
rotate = [q[0] + d[0], q[1] + d[1], q[2] + 0 * d[2]];
redraw();
})
.on("end", function() {
svg._earth.style('cursor', '-webkit-grab');
});
d3.select("svg")
.call(zoom);
redraw();
var elapsed = null;
function animate(t) {
elapsed = t;
//d3.select("canvas").transition().call(zoom.transform, d3.zoomIdentity);
requestAnimationFrame(animate);
}
//animate();
}
// A polyfill for requestAnimationFrame.
if (!self.requestAnimationFrame) requestAnimationFrame =
self.webkitRequestAnimationFrame || self.mozRequestAnimationFrame || self.msRequestAnimationFrame || self.oRequestAnimationFrame || function (f) {
setTimeout(f, 17);
};
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment