STELLAR1997

A lightweight visualization and data analysis tool. It's made to :

Here is the very first interactive animation made with stellar

Here is an animated plot that helps percieve motion

Here is the same thing on an other dataset

Here is some tests

about GLSL

We run on WebGL2. It's is a pipeline that eats arrays of numbers, and outputs pixels on a texture. It's generally super fast compared to plots on CPU, however, depending on the hardware, we can't guarantee precision. If you are worried about precision, avoid cheap/gaming cards.

So we are able to decide :

The last two are the interesting onces for you usage. Here is the minimum you should know to make custom dynamic plots.

THREE.js provides useful builtin variables/functions that are additionnal to the original webgl builtin variables/functions.
Those include camera projection matrices, small matrix inversions, trigonometric functions etc...
Also GLSL syntax is similar to C and super friendly with linear algebra's operations

The most basic vertex shader looks like this

void main() { gl_Position = projectionMatrix*modelViewMatrix*vec4(position,1.); // this is how we output geometry gl_PointSize = 3.; // if plotting points. // NB : plotting mode is set before in the pipeline, we can't control that here }

The most basic fragment shader looks like this

void main() { gl_FragColor = vec4(1.,1.,1.,1.); // thie is how we output colors (in RGBA) } The vertex shader receives the data. If we want to send that thing to the fragment shader, we need to create some sort of bond with the keyword varying // vertex shader // varying vec4 vColor; // ← prepare to send void main() { gl_Position = projectionMatrix*modelViewMatrix*vec4(position,1.); vColor = vec4(length(position),0.,0.,1.); // don't forget to set it ! } // fragment shader // varying vec4 vColor; // ← prepare to receive void main() { gl_FragColor = vColor; }

Note that any varying variable will be interpolated if the fragment shader is rendering a polygon/line.

Note that there are 3 keywords :

[*] this can be improved so that we can directly use variable names as is... but I would need to write a GLSL parser to implement such a thing. maybe later

I think this is enough to make nice things with stellar

POINTS

for quick/massive use cases : the shaderprogram associated is super lightweight

position,color,size are builtin variables for the default shaderprogram.
If they are absent from data, then memory is not allocated for those, which means more memory for position !
same goes with dimension : lower vec for 2d plots : more points can be stored in memory [wait this doesn't work now]

let data = {}; data.position = { vec:3, data:[x1,y1,z1,x2,y2,z2, ... ,xn,yn,zn]}; data.color = { vec:3, data:[r1,g1,b1,r2,g2,b2, ... ,rn,gn,bn]}; data.size = { vec:1, data:[s1, s2, ... ,sn ]}; let pts = points(data); output(pts);

Example

let range = i => [...Array(i).keys()]; let data = {}; data.position = {vec:3, data: range(3*100000).map(x => 2*(Math.random()-.5)) } data.color = {vec:4, data: range(4*100000).map(x => Math.random()) } data.size = {vec:1, data: range(1*100000).map(x => Math.random()) } let p = StellarPlot.pointsNew(data); output(p);

POINTS with computation on GPU

for animation/interraction, we execute code on the GPU with a custom shaderprogram

note that position is mandatory [I'd like to get rid of this limitation]

data.position = { vec:3, data:[x1,y1,z1,x2,y2,z2, ... ,xn,yn,zn]}; data.velocity = { vec:3, data:[ẋ1,ẏ1,ż1,ẋ2,ẏ2,ż2, ... ,ẋn,ẏn,żn]}; let vshader = ` varying vec4 vColor; void main() { // let's calculate a dynamic position ! this one is time dependant. vec3 positionRender = position + cos(time) * velocity; gl_Position = projectionMatrix*modelViewMatrix*vec4(positionRender,1.); // let's setup the color for fshader with a nice rainbow from |velocity| vColor = vec4( hsv2rgb(length(velocity),1.,1.) , 1.); } ` let fshader = ` varying vec4 vColor; void main() { gl_FragColor = vColor; } ` let pts = points(data,vshader,fshader); output(pts);

Example

input sliders + propper video player integration // forceAnimation(true); // draw all the time, don't try to optimize let videoPlayerTime = {value:0}; let videoPlayerRate = 0.; let videoPlayer_play = () => {videoPlayerRate=1} let videoPlayer_play_faster = () => {videoPlayerRate+=3} let videoPlayer_pause = () => {videoPlayerRate=0} let videoPlayer_rewind = () => {videoPlayerRate=-1} let videoPlayer_rewind_faster = () => {videoPlayerRate-=3} let videoPlayer_stop = () => {videoPlayerRate=0;videoPlayerTime.value=0} document.getElementById("example-stop").onclick = videoPlayer_stop; document.getElementById("example-rewind-faster").onclick = videoPlayer_rewind_faster; document.getElementById("example-rewind").onclick = videoPlayer_rewind; document.getElementById("example-pause").onclick = videoPlayer_pause; document.getElementById("example-play").onclick = videoPlayer_play; document.getElementById("example-play-faster").onclick = videoPlayer_play_faster; let videoPlayerGlobalTime = new Date(); let videoPlayerUpdate = () => { let now = new Date(); let dt = (now - videoPlayerGlobalTime)/1000; videoPlayerGlobalTime = now; if(keyboardState['k']=='k') { videoPlayer_pause(); } else if(keyboardState['l']=='l') { if(keyboardState['Shift']=='Shift') { videoPlayer_play_faster(); } else { videoPlayer_play(); } } else if(keyboardState['j']=='j') { if(keyboardState['Shift']=='Shift') { videoPlayer_rewind_faster(); } else { videoPlayer_rewind(); } } else if(keyboardState['h']=='h') { if(keyboardState['Shift']=='Shift') { videoPlayerRate=0;videoPlayerTime.value=60 } else { videoPlayer_stop(); } } videoPlayerTime.value += dt*videoPlayerRate; if(videoPlayerTime.value<0) { videoPlayerTime.value=0; videoPlayerRate=0; } if(videoPlayerRate!=0) { renderplease(); } window.requestAnimationFrame(videoPlayerUpdate) } videoPlayerUpdate(); let range = i => [...Array(i).keys()]; let data = {}; data.position = {vec:3, data: range(3*50000).map(x => 2*(Math.random()-.5)) } data.bobby = {vec:3, data: range(3*50000).map(x => Math.random()) } data.size = {vec:1, data: range(1*50000).map(x => Math.random()) } data.timep = {univec:1, data: videoPlayerTime } let vshader = ` uniform vec2 slider_s_log; varying vec4 vColor; varying vec4 vVertexId; void main() { vVertexId = id2rgba(gl_VertexID); // enable drilldown vColor = vec4(bobby/5. + hsv2rgb(vec3(cos(4.*pi*length(position)+2.*timep),1.,1.)),1.); vec3 newpos = position + 0.1*vec3(cos(4.*pi*length(position)+2.*timep),cos(8.*pi*(position.x*position.z) + 3.*timep),cos(2.*pi*position.y + 4.*timep)); vec4 mvPosition = modelViewMatrix * vec4( newpos, 1.0 ); gl_Position = projectionMatrix * mvPosition; float s = max(1. , size ); gl_PointSize = s * slider_s_log.y * max(1.,(8.*cos(1.*pi*(position.z*position.z-position.y*position.y+position.x)+1.*timep))/2.); }`; let p = StellarPlot.pointsNew(data,vshader); output(p);

SURFACE

Here instead of drawing from geometry, we go the other way around : from a surface coord, we trace back to the data we want to show.

This allows the cost of a rendering to be maximum at the resolution of the plot, which could be interesting for crazy big amount of data to be displayed.

! useful trick ! : to make a "reverse" function, the technique is to write it in the "normal" way, and then inverse all functions.

let data = {}; data.velocity = { tex:3, data:[x1,y1,z1,x2,y2,z2, ... ,xn,yn,zn]}; data.magnitude = { tex:1, data:[x1, x2, ... ,xn]}; let fshader = ` `; let surf = surface(data,fshader,[2*π,π]); output(surf);

Example

let videoPlayerTime = {value:0}; let videoPlayerRate = 0.; let videoPlayer_play = () => {videoPlayerRate=1} let videoPlayer_play_faster = () => {videoPlayerRate+=3} let videoPlayer_pause = () => {videoPlayerRate=0} let videoPlayer_rewind = () => {videoPlayerRate=-1} let videoPlayer_rewind_faster = () => {videoPlayerRate-=3} let videoPlayer_stop = () => {videoPlayerRate=0;videoPlayerTime.value=0} document.getElementById("example2-stop").onclick = videoPlayer_stop; document.getElementById("example2-rewind-faster").onclick = videoPlayer_rewind_faster; document.getElementById("example2-rewind").onclick = videoPlayer_rewind; document.getElementById("example2-pause").onclick = videoPlayer_pause; document.getElementById("example2-play").onclick = videoPlayer_play; document.getElementById("example2-play-faster").onclick = videoPlayer_play_faster; let videoPlayerGlobalTime = new Date(); let videoPlayerUpdate = () => { let now = new Date(); let dt = (now - videoPlayerGlobalTime)/1000; videoPlayerGlobalTime = now; if(keyboardState['k']=='k') { videoPlayer_pause(); } else if(keyboardState['l']=='l') { if(keyboardState['Shift']=='Shift') { videoPlayer_play_faster(); } else { videoPlayer_play(); } } else if(keyboardState['j']=='j') { if(keyboardState['Shift']=='Shift') { videoPlayer_rewind_faster(); } else { videoPlayer_rewind(); } } else if(keyboardState['h']=='h') { if(keyboardState['Shift']=='Shift') { videoPlayerRate=0;videoPlayerTime.value=60 } else { videoPlayer_stop(); } } videoPlayerTime.value += dt*videoPlayerRate; if(videoPlayerTime.value<0) { videoPlayerTime.value=0; videoPlayerRate=0; } if(videoPlayerRate!=0) { renderplease(); } window.requestAnimationFrame(videoPlayerUpdate) } videoPlayerUpdate(); let range = i => [...Array(i).keys()]; let data = {}; data.values = {tex:1, data: range(1*4000000).map(x => (x + 2048*Math.random())%4096) } data.timep = {univec:1, data: videoPlayerTime } let fshader = ` uniform mat4 slider_a_rot; uniform vec2 slider_c_log; uniform vec2 slider_c; varying vec3 vPosition; float dummyData(sampler2D megaTexture, vec2 thetaphi) { int depth = 10; int npix = ang2pix(1 << depth, thetaphi.yx ); return texture2D( megaTexture, vec2( float( (npix>>2) % (valuesResX)) / float(valuesResX), float( (npix>>2) / (valuesResX)) / float(valuesResY) ))[npix & 3]; } void main() { vec3 pos = vPosition; // spheric = spheric_rotation( pos.xy, inverse( slider_a_rot )); // if(length(vec2(pos.x,pos.y*2.)) > pi) { discard; } vec2 hammer = spheric_rotation( hammer2spheric(pos.xy), inverse( slider_a_rot )); // if(length(vec2(pos.x,pos.y)) > pi/2.) { discard; } vec2 xy = pos.xy/pi*2.; vec2 spheric3d = vec2(acos(xy.y/cos(asin(xy.x))),asin(xy.x)); spheric3d = spheric_rotation( spheric3d, inverse( slider_a_rot )); vec2 controller = vec2(cos(timep),cos(1.2*timep)) + slider_c; vec2 spheric = controller*hammer + (1.-controller)*spheric3d; float value = dummyData(values, spheric); vec4 valueColor = vec4(hsv2rgb(vec3(value/4096.,1.,1.)),1.); vec4 gridColor = vec4(gridBackgroundSpheric(spheric))*0.5; gl_FragColor = valueColor + gridColor; // gl_FragColor = vec4(hsv2rgb(vec3(cos(length(pos)+time),1.,1.)),1.); }`; let p = StellarPlot.surface(data,fshader,[2*π,π]); output(p);

CGPU library

CGPU is a set of useful functions for the gaia mission with both GPU and CPU implementation

so for all your shadersprograms, those functions will be available

please note that spherical coord are using gaia's standard : ie $θ∈[-\frac{π}{2},\frac{π}{2}], φ∈[p,2π]$

Issue

please note that depending on the GPU you are using, you may encounter numerical approximations going wrong !

Issue

only works in chrome (firefox and safari not supported yet)

TODO healpix

DONE healpix

TODO camera

TODO