Skip to content

Commit

Permalink
Custom layer immediate and draped rendering on globe and terrain (#12182
Browse files Browse the repository at this point in the history
)

* WIP: Custom layer draping support

CustomStyleLayer API extended to support draping with new methods:

export type CustomLayerInterface = {
...
    // misusing MercatorCoordiate's xyz for tile id (x, y, z) where wrap is baked into x.
    renderToTile: ?(gl: WebGLRenderingContext, tileId: MercatorCoordinate) => void,

    // return true only for frame when content has changed - otherwise, all the terrain
    // render cache would be invalidated and redrawn causing huge drop in performance.
    shouldRerenderTiles: ?() => boolean,
}

Using WebGL Wind demo code, for simplicity of use, instead of adding submodule, copied dist code. Full code is available at https://github.com/mapbox/webgl-wind/tree/astojilj-draping-support

* dirty quick way to render a geometry with custom layer on globe

* expose necessary API, some clean up

* use sphere model

* WIP combining PRs and changing immediate more rendering demo

* satellite vis

* satellite vis complete

* clean up

* continued clean up

* add utility func to lnglat class and also render in mercator

* format

* rm threejs

* rm unused funcs

* minor

* render test for draping

* render test updated

* minor update to render test

* update debug page to use pure gl only

* revert accidentally changed files to main

* address review remarks, fix incorrect color, disable stencil

* disable stencil from gl-js side before invoking renderToTile

* Pass projection and resolve mercatorMatrix during transition

In globe projection, mercator matrix z scale was incorrect, disabling possibility for transition. Transform.mercatorMatrix changed in order to produce the expected mercator matrix, even in globe (zoom 5->6) transition, and then the same compensated in globe to mercator matrix.

updated satellites_custom_layer.js to show transition and satellites in mercator projection.

* Lint and Flow fixes

* break circular dependency, final minor fixes

Co-authored-by: Aleksandar Stojiljković <aleksandar.stojiljkovic@mapbox.com>
  • Loading branch information
akoylasar and astojilj authored Jan 24, 2023
1 parent 55eab7c commit 5aed4f6
Show file tree
Hide file tree
Showing 17 changed files with 10,559 additions and 24 deletions.
67 changes: 67 additions & 0 deletions debug/color-earth-custom-layer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
function getColor(tileId) {
const m = Math.pow(2, tileId.z);
const s = tileId.z + tileId.x * m + tileId.y * m;
const r = (Math.sin(s + 5) * 1924957) % 1;
const g = (Math.sin(s + 7) * 3874133) % 1;
const b = (Math.sin(s + 3) * 7662617) % 1;
return [r, g, b];
};

var coloredEarthLayer = {
id: 'coloredEarth',
type: 'custom',

onAdd: (map, gl) => {
const vertexSource = `
attribute vec2 a_pos;
void main() {
gl_Position = vec4(a_pos, 1.0, 1.0);
}`;

const fragmentSource = `
precision highp float;
uniform vec3 u_color;
void main() {
gl_FragColor = vec4(u_color, 0.5);
}`;

const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexSource);
gl.compileShader(vertexShader);
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentSource);
gl.compileShader(fragmentShader);

this.program = gl.createProgram();
gl.attachShader(this.program, vertexShader);
gl.attachShader(this.program, fragmentShader);
gl.linkProgram(this.program);

this.program.aPos = gl.getAttribLocation(this.program, "a_pos");
this.program.uColor = gl.getUniformLocation(this.program, "u_color");

const verts = new Float32Array([1, 1, 1, -1, -1, -1, -1, -1, -1, 1, 1, 1]);
this.vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, verts, gl.STATIC_DRAW);
},

shouldRerenderTiles: () => {
// return true only when frame content has changed otherwise, all the terrain
// render cache would be invalidated and redrawn causing huge drop in performance.
return true;
},

renderToTile: (gl, tileId) => {
gl.useProgram(this.program);
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
gl.enableVertexAttribArray(this.program.aPos);
gl.vertexAttribPointer(this.program.aPos, 2, gl.FLOAT, false, 0, 0);
const color = getColor(tileId);
gl.uniform3f(this.program.uColor, color[0], color[1], color[2]);
gl.drawArrays(gl.TRIANGLES, 0, 6);
},

render: (gl, matrix) => {
}
};
79 changes: 79 additions & 0 deletions debug/custom-layer-globe.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html>
<head>
<title>Mapbox GL JS custom layers on globe page</title>
<meta charset='utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel='stylesheet' href='../dist/mapbox-gl.css' />
<style>
body { margin: 0; padding: 0; }
html, body, #map { height: 100%; }
#checkboxes {
position: absolute;
top:0;
left:0;
padding:10px;
}
</style>
</head>

<body>
<div id='map'></div>
<div id='checkboxes'>
<label><input id='immediate-checkbox' type='checkbox' checked> add a custom layer with immediate rendering</label><br />
<label><input id='draped-checkbox' type='checkbox'> add a custom layer with draped rendering</label><br />
<label><input id='repaint-checkbox' type='checkbox'> repaint. FPS: </label><label id='fps'>0</label><br />
</div>

<script src="//unpkg.com/satellite.js/dist/satellite.min.js"></script>
<script src="satellites-custom-layer.js"></script>

<script src="color-earth-custom-layer.js"></script>

<script src='../dist/mapbox-gl-dev.js'></script>
<script src='../debug/access_token_generated.js'></script>

<script>
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/satellite-v9',
center: [0, 0],
zoom: 10,
antialias: true, // create the gl context with MSAA antialiasing, so custom layers are antialiased
hash:true,
projection: 'globe'
});

map.on('style.load', () => {
map.addLayer(satellitesLayer); // eslint-disable-line no-undef
map.addLayer(coloredEarthLayer); // eslint-disable-line no-undef

document.getElementById('immediate-checkbox').onclick();
document.getElementById('draped-checkbox').onclick();
});

document.getElementById('immediate-checkbox').onclick = function() {
map.setLayoutProperty('satellites', 'visibility', this.checked ? 'visible' : 'none');
};

document.getElementById('draped-checkbox').onclick = function() {
map.setLayoutProperty('coloredEarth', 'visibility', this.checked ? 'visible' : 'none');
};

document.getElementById('repaint-checkbox').onclick = function() {
map.repaint = !!this.checked;
if (this.checked) {
this['frameCounter'] = map.painter.frameCounter;
this['fpsTimer'] = window.setInterval(() => {
document.getElementById('fps').innerHTML = `${(map.painter.frameCounter - this.frameCounter) / 2}`;
this.frameCounter = map.painter.frameCounter;
}, 2000);
} else {
window.clearInterval(this.fpsTimer);
document.getElementById('fps').innerHTML = `0`;
}
};

</script>
</body>
</html>
169 changes: 169 additions & 0 deletions debug/satellites-custom-layer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
const EARTH_RADIUS_METERS = 6371008.8;
const EARTH_CIRCUMFERENCE_METERS = 2 * Math.PI * EARTH_RADIUS_METERS;
const GLOBE_CIRCUMFERENCE_ECEF = 8192;
const METERS_TO_ECEF = GLOBE_CIRCUMFERENCE_ECEF / EARTH_CIRCUMFERENCE_METERS;

const KM_TO_M = 1000;
const TIME_STEP = 3 * 1000;

const globeVertCode = `
attribute vec3 a_pos_ecef;
attribute vec3 a_pos_merc;
uniform mat4 u_projection;
uniform mat4 u_globeToMercMatrix;
uniform float u_globeToMercatorTransition;
void main() {
vec4 p = u_projection * u_globeToMercMatrix * vec4(a_pos_ecef, 1.);
p /= p.w;
if (u_globeToMercatorTransition > 0.) {
vec4 merc = u_projection * vec4(a_pos_merc, 1.);
merc /= merc.w;
p = mix(p, merc, u_globeToMercatorTransition);
}
gl_PointSize = 30.;
gl_Position = p;
}
`;

const mercVertCode = `
precision highp float;
attribute vec3 a_pos_merc;
uniform mat4 u_projection;
void main() {
gl_PointSize = 30.;
gl_Position = u_projection * vec4(a_pos_merc, 1.);
}
`;

const fragCode = `
precision highp float;
uniform vec4 u_color;
void main() {
gl_FragColor = vec4(1., 0., 0., 1.);
}
`;

let time = new Date();

function createShader(gl, src, type) {
var shader = gl.createShader(type);
gl.shaderSource(shader, src);
gl.compileShader(shader);
const message = gl.getShaderInfoLog(shader);
if (message.length > 0) {
console.error(message);
}
return shader;
};

function createProgram(gl, vert, frag) {
var vertShader = this.createShader(gl, vert, gl.VERTEX_SHADER);
var fragShader = this.createShader(gl, frag, gl.FRAGMENT_SHADER);

var program = gl.createProgram();
gl.attachShader(program, vertShader);
gl.attachShader(program, fragShader);
gl.linkProgram(program);
gl.validateProgram(program);

if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
const info = gl.getProgramInfoLog(program);
console.error(`Could not compile WebGL program. \n\n${info}`);
}

return program;
};

function updateVboAndActivateAttrib(gl, prog, vbo, data, attribName) {
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.DYNAMIC_DRAW);
const attribLoc = gl.getAttribLocation(prog, attribName);
gl.vertexAttribPointer(attribLoc, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(attribLoc);
}

const satellitesLayer = {
id: 'satellites',
type: 'custom',
onAdd (map, gl) {
this.map = map;

this.posEcef = [];
this.posMerc = [];

this.posEcefVbo = gl.createBuffer();
this.posMercVbo = gl.createBuffer();

this.globeProgram = createProgram(gl, globeVertCode, fragCode);
this.mercProgram = createProgram(gl, mercVertCode, fragCode);

fetch('space-track-leo.txt').then(r => r.text()).then(rawData => {
const tleData = rawData.replace(/\r/g, '')
.split(/\n(?=[^12])/)
.filter(d => d)
.map(tle => tle.split('\n'));
this.satData = tleData.map(([name, ...tle]) => ({
satrec: satellite.twoline2satrec(...tle),
name: name.trim().replace(/^0 /, '')
}))
// exclude those that can't be propagated
.filter(d => !!satellite.propagate(d.satrec, new Date()).position)
.slice(0, 10);

this.updateBuffers();
});
},

updateBuffers() {
time = new Date(+time + TIME_STEP);
const gmst = satellite.gstime(time);
this.posEcef = [];
this.posMerc = [];
for (let i = 0; i < this.satData.length; ++i) {
const satrec = this.satData[i].satrec;
const eci = satellite.propagate(satrec, time);
if (eci.position) {
const geodetic = satellite.eciToGeodetic(eci.position, gmst);

const lngLat = [satellite.degreesLong(geodetic.longitude), satellite.degreesLat(geodetic.latitude)];
const altitude = geodetic.height * KM_TO_M;

const merc = mapboxgl.MercatorCoordinate.fromLngLat(lngLat, altitude);
const ecef = mapboxgl.LngLat.convert(lngLat).toEcef(altitude);

this.posEcef.push(...ecef);
this.posMerc.push(...[merc.x, merc.y, merc.z]);
}
}
},

render (gl, projectionMatrix, projection, globeToMercMatrix, transition) {
if (this.satData) {
this.updateBuffers();

const primitiveCount = this.posEcef.length / 3;
gl.enable(gl.DEPTH_TEST);
if (projection && projection.name === 'globe') { // globe projection and globe to mercator transition
gl.useProgram(this.globeProgram);

updateVboAndActivateAttrib(gl, this.globeProgram, this.posEcefVbo, this.posEcef, "a_pos_ecef");
updateVboAndActivateAttrib(gl, this.globeProgram, this.posMercVbo, this.posMerc, "a_pos_merc");

gl.uniformMatrix4fv(gl.getUniformLocation(this.globeProgram, "u_projection"), false, projectionMatrix);
gl.uniformMatrix4fv(gl.getUniformLocation(this.globeProgram, "u_globeToMercMatrix"), false, globeToMercMatrix);
gl.uniform1f(gl.getUniformLocation(this.globeProgram, "u_globeToMercatorTransition"), transition);

gl.drawArrays(gl.POINTS, 0, primitiveCount);
} else { // mercator projection
gl.useProgram(this.mercProgram);
updateVboAndActivateAttrib(gl, this.mercProgram, this.posMercVbo, this.posMerc, "a_pos_merc");
gl.uniformMatrix4fv(gl.getUniformLocation(this.mercProgram, "u_projection"), false, projectionMatrix);
gl.drawArrays(gl.POINTS, 0, primitiveCount);
}
}
}
};
Loading

0 comments on commit 5aed4f6

Please sign in to comment.