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 :
what arrays we want to send : data bindings (abstraction made with THREE.js)
how to make geometry from those : vertexShader
and how to put them on screen : fragmentShader
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 :
attribute : input data
varying : vshader to fshader. Right now the convention is that we prefix variable names with a v [*]
uniform : constant shared by everyone (you may be interested by this one later on)
[*] 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 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);