|
| 1 | +<!DOCTYPE html> |
| 2 | +<html lang="en"> |
| 3 | +<head> |
| 4 | + <meta charset="utf-8"> |
| 5 | + <title>Raster projection with GPU.js</title> |
| 6 | + <script src="../../bin/gpu-browser.js"></script> |
| 7 | +</head> |
| 8 | +<body> |
| 9 | +<h1>Raster projection with GPU.js from <a href="https://observablehq.com/d/b70d084526a1a764">https://observablehq.com/d/b70d084526a1a764</a></h1> |
| 10 | +<div id="log-fps"></div> |
| 11 | +</body> |
| 12 | +<script> |
| 13 | + function frac(n) { |
| 14 | + return n - Math.floor(n); |
| 15 | + } |
| 16 | + function applyRotation(rotatex, rotatey, rotatez, lambda, phi, which) { |
| 17 | + const degrees = 57.29577951308232; |
| 18 | + |
| 19 | + lambda = lambda / degrees; |
| 20 | + phi = phi / degrees; |
| 21 | + |
| 22 | + var cosphi = Math.cos(phi), |
| 23 | + x = Math.cos(lambda) * cosphi, |
| 24 | + y = Math.sin(lambda) * cosphi, |
| 25 | + z = Math.sin(phi); |
| 26 | + |
| 27 | + // inverse rotation |
| 28 | + var deltaLambda = rotatex / degrees; // rotate[0] |
| 29 | + var deltaPhi = -rotatey / degrees; // rotate[1] |
| 30 | + var deltaGamma = -rotatez / degrees; // rotate[2] |
| 31 | + |
| 32 | + var cosDeltaPhi = Math.cos(deltaPhi), |
| 33 | + sinDeltaPhi = Math.sin(deltaPhi), |
| 34 | + cosDeltaGamma = Math.cos(deltaGamma), |
| 35 | + sinDeltaGamma = Math.sin(deltaGamma); |
| 36 | + |
| 37 | + var k = z * cosDeltaGamma - y * sinDeltaGamma; |
| 38 | + |
| 39 | + lambda = Math.atan2( |
| 40 | + y * cosDeltaGamma + z * sinDeltaGamma, |
| 41 | + x * cosDeltaPhi + k * sinDeltaPhi |
| 42 | + ) - deltaLambda; |
| 43 | + k = k * cosDeltaPhi - x * sinDeltaPhi; |
| 44 | + |
| 45 | + phi = Math.asin(k); |
| 46 | + |
| 47 | + lambda *= degrees; |
| 48 | + phi *= degrees; |
| 49 | + |
| 50 | + // return [lambda,phi]; // fails so we need to call this function twice |
| 51 | + // and ask first for lambda, then for phi |
| 52 | + if (which === 0) return lambda; |
| 53 | + else return phi; |
| 54 | + } |
| 55 | + // the kernel runs for each pixel, with: |
| 56 | + // - this.thread.x = horizontal position in pixels from the left edge |
| 57 | + // - this.thread.y = vertical position in pixels from the bottom edge (*opposite of canvas*) |
| 58 | + const kernel = function(pixels, rotate0, rotate1, rotate2, scale) { |
| 59 | + |
| 60 | + // azimuthal equal area |
| 61 | + function radius(rho) { |
| 62 | + return 2.0 * Math.asin(rho / 2.0); |
| 63 | + } |
| 64 | + // orthographic |
| 65 | + function __radius(rho) { |
| 66 | + return Math.asin(rho); |
| 67 | + } |
| 68 | + |
| 69 | + // equirectangular projection (reads the (lon,lat) color from the base image) |
| 70 | + function pixelx(lon, srcw) { |
| 71 | + lon = frac((lon + 180) / 360); |
| 72 | + return Math.floor(lon * srcw); |
| 73 | + } |
| 74 | + function pixely(lat, srch) { |
| 75 | + lat = frac((lat + 90) / 180); |
| 76 | + return Math.floor(lat * srch); |
| 77 | + } |
| 78 | + |
| 79 | + var x = (this.thread.x / this.constants.w - 1 / 2) / scale, |
| 80 | + y = ((this.thread.y - this.constants.h / 2) / this.constants.w) / scale; |
| 81 | + |
| 82 | + // inverse projection |
| 83 | + const rho = Math.sqrt(x * x + y * y) + 1e-12; |
| 84 | + |
| 85 | + const c = radius(rho), |
| 86 | + sinc = Math.sin(c), |
| 87 | + cosc = Math.cos(c); |
| 88 | + |
| 89 | + // x, y : pixel coordinates if rotation was null |
| 90 | + const lambda = Math.atan2(x * sinc, rho * cosc) * 57.29577951308232; |
| 91 | + const z = y * sinc / rho; |
| 92 | + if (Math.abs(z) < 1) { |
| 93 | + const phi = Math.asin(z) * 57.29577951308232; |
| 94 | + |
| 95 | + // apply rotation |
| 96 | + // [lambda, phi] = applyRotation(centerx, centery, centerz, lambda, phi); // TODO |
| 97 | + const lambdan = applyRotation(rotate0, rotate1, rotate2, lambda, phi, 0); |
| 98 | + const phin = applyRotation(rotate0, rotate1, rotate2, lambda, phi, 1); |
| 99 | + //var n = n0(lambda, phi, this.constants.srcw, this.constants.srch); |
| 100 | + //this.color(pixels[n]/256, pixels[n+1]/256,pixels[n+2]/256,1); |
| 101 | + const pixel = pixels[pixely(phin, this.constants.srch)][pixelx(lambdan, this.constants.srcw)]; |
| 102 | + this.color(pixel[0], pixel[1], pixel[2], 1); |
| 103 | + } |
| 104 | + }; |
| 105 | + const gpu = new GPU(); |
| 106 | + const logFps = document.querySelector('#log-fps'); |
| 107 | + const image = new Image(); |
| 108 | + image.src = './earth-map.jpg'; |
| 109 | + image.onload = () => { |
| 110 | + const w = 975; |
| 111 | + const h = 975; |
| 112 | + const render = gpu |
| 113 | + .createKernel(kernel, { functions: [applyRotation, frac] }) |
| 114 | + .setConstants({ w, h, srcw: image.width, srch: image.height }) |
| 115 | + .setOutput([w, h]) |
| 116 | + .setGraphical(true); |
| 117 | + const canvas = render.canvas; |
| 118 | + document.body.appendChild(canvas); |
| 119 | + var lastCalledTime; |
| 120 | + var fps; |
| 121 | + function callRender() { |
| 122 | + let r0 = (-Date.now() / 30) % 360, |
| 123 | + r1 = 35 * Math.sin((-Date.now() / 1030) % 360), |
| 124 | + r2 = 0, |
| 125 | + scale = 0.49; |
| 126 | + render(image, r0, r1, r2, scale); |
| 127 | + delta = (Date.now() - lastCalledTime)/1000; |
| 128 | + lastCalledTime = Date.now(); |
| 129 | + fps = 1/delta; |
| 130 | + logFps.innerHTML = fps.toFixed(0) + ' FPS'; |
| 131 | + window.requestAnimationFrame(() => { |
| 132 | + callRender(); |
| 133 | + }); |
| 134 | + } |
| 135 | + callRender(); |
| 136 | + }; |
| 137 | +</script> |
| 138 | +</html> |
0 commit comments