fragment shader generated interactive grid - javascript

I am trying to implement my own WebGL based 3D graph plotting app.
As I might by dealing with a lot of data points, I hope I can make an interactive grid to help visualize number of different order of magnitude with scrolling support(for scaling).
Here is my fragment shader for my grid cube.
precision highp float;
varying vec3 pos;
uniform float u_size;
uniform float u_scale;
float my_fmod(float inp_val, float inp_m) {
float m = inp_m;
float val = inp_val + 201.0 * m;
return abs(val - float(int(val/m)) * m);
}
float gridline(float nPos0, float nPos1, float fac, float very_small_number) {
if ( my_fmod(nPos0, 1./fac) < very_small_number || my_fmod(nPos1, 1./fac) < very_small_number) return 1.0;
return 0.0;
}
void main() {
float very_small_number = 0.015 / u_scale;
float size = u_size;
vec3 nPos = pos / u_scale;
vec3 color = vec3(0.,0.,0.5);
float sqrt5 = 2.236068;
gl_FragColor = vec4(0.,0.,0.,1.);
float n = 5.;
float sLine = 1.;
float t1 = 1.;
float t2 = 1.;
if (pos.x == -size || pos.x == size) {
if (gridline(nPos.y, nPos.z, n, very_small_number) > 0.5) gl_FragColor.xyz += color * t1;
if (gridline(nPos.y*n, nPos.z*n, n, very_small_number) > 0.5) gl_FragColor.xyz += color * t2;
} else if (pos.y == -size || pos.y == size) {
if (gridline(nPos.x, nPos.z, n, very_small_number) > 0.5) gl_FragColor.xyz += color * t1;
if (gridline(nPos.x*n, nPos.z*n, n, very_small_number) > 0.5) gl_FragColor.xyz += color * t2;
} else if (pos.z == -size || pos.z == size) {
if (gridline(nPos.x, nPos.y, n, very_small_number) > 0.5) gl_FragColor.xyz += color * t1;
if (gridline(nPos.x*n, nPos.y*n, n, very_small_number) > 0.5) gl_FragColor.xyz += color * t2;
}
}
As you can see, I drew two sets of grid. They represents two different order of magnitudes. At this moment, it can scale perfectly but I want to add a feature which allows fading between different order of magnitudes and only shows grids that are in certain scale.
Here is the original look of the App,
It looks nice. And when I zoom in,
The grid looks larger. But I hope that my shader can draw new grids inside the small grids and not to draw large grids which is not visible any more.
When I zoom out,
There are too many grids shown and it affects the user's experience.
So, how can I achieve transitioning between different order of magnitudes and apply fading between the transitions? Sorry for my poor English. Any help is appreciated. Have a nice day.
edit 1:
Here is the full code for experimenting.
https://github.com/Jonathan-D-Ip/WebGLPlottingApp/blob/master/Display.html

If it was me I wouldn't be using a fragment shader for a grid I'd be using lines. I'd draw one grid at one size and if I was close to the transition and needed a new grid I'd draw another grid at another scale. Both grids would have an alpha setting so I could fade them out.
Here's an example, use the mousewheel or equivalent to zoom. Maybe you can adapt the ideas to your preferred solution. The important part is probably this part
const gridLevel = Math.log10(zoom * zoomAdjust);
const gridFract = euclideanModulo(gridLevel, 1);
const gridZoom = Math.pow(10, Math.floor(gridLevel));
const alpha1 = Math.max((1 - gridFract) * 1);
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)
drawGrid(viewProjection, gridZoom, [0, alpha1, 0, alpha1]);
const alpha2 = Math.max(gridFract * 10) - 1;
if (alpha2 > 0) {
drawGrid(viewProjection, gridZoom * 10, [0, alpha2, 0, alpha2],);
}
zoom goes from 0.0001 to 10000 and represents the distance from the target. The code uses Math.log10 to find out 10 to the what power is needed to get that zoom level. In other words if zoom is 100 then gridLevel = 2. If zoom is 1000 then gridLevel = 3. From that we can get the fractional amount between powers of 10 in gridFract which will always be in the range of 0 to 1 as we move between zoom levels.
gridZoom tells us what scale to draw one of our grids (we just remove the fractional part of gridLevel) and then raise 10 to that power. gridZoom * 10 is the next largest grid size.
alpha1 is the alpha for the grid. alpha2 is the alpha for the second grid.
const m4 = twgl.m4;
const gl = document.querySelector("canvas").getContext("webgl", {alpha: false});
const zoomElem = document.querySelector("#zoom");
const zoomAdjust = 1; // change to adjust when things start/end. Try 5 or .5 for example
let zoom = 1;
const gridVS = `
attribute vec4 position;
uniform mat4 matrix;
void main() {
gl_Position = matrix * position;
}
`;
const gridFS = `
precision mediump float;
uniform vec4 color;
void main() {
gl_FragColor = color;
}
`;
const gridProgramInfo = twgl.createProgramInfo(gl, [gridVS, gridFS]);
const gridPlaneLines = [];
const numLines = 100;
for (let i = 0; i <= 100; ++i) {
gridPlaneLines.push(0, i, 100, i);
gridPlaneLines.push(i, 0, i, 100);
}
const gridPlaneBufferInfo = twgl.createBufferInfoFromArrays(gl, {
position: { numComponents: 2, data: gridPlaneLines },
});
function drawGrid(viewProjection, scale, color) {
gl.useProgram(gridProgramInfo.program);
twgl.setBuffersAndAttributes(gl, gridProgramInfo, gridPlaneBufferInfo);
const scaling = [scale, scale, scale];
// draw Z plane
{
let matrix = m4.scale(viewProjection, scaling);
matrix = m4.rotateY(matrix, Math.PI);
twgl.setUniforms(gridProgramInfo, {
matrix,
color,
});
twgl.drawBufferInfo(gl, gridPlaneBufferInfo, gl.LINES);
}
// draw X plane
{
let matrix = m4.scale(viewProjection, scaling);
matrix = m4.rotateY(matrix, Math.PI * .5);
twgl.setUniforms(gridProgramInfo, {
matrix,
color,
});
twgl.drawBufferInfo(gl, gridPlaneBufferInfo, gl.LINES);
}
// draw Y plane
{
let matrix = m4.scale(viewProjection, scaling);
matrix = m4.rotateY(matrix, Math.PI);
matrix = m4.rotateX(matrix, Math.PI * .5);
twgl.setUniforms(gridProgramInfo, {
matrix,
color,
});
twgl.drawBufferInfo(gl, gridPlaneBufferInfo, gl.LINES);
}
}
function render() {
twgl.resizeCanvasToDisplaySize(gl.canvas, window.devicePixelRatio);
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
zoomElem.textContent = zoom.toFixed(5);
const fov = degToRad(60);
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const zNear = zoom / 100;
const zFar = zoom * 100;
const projection = m4.perspective(fov, aspect, zNear, zFar);
const eye = [zoom * -10, zoom * 5, zoom * -10];
const target = [0, 0, 0];
const up = [0, 1, 0];
const camera = m4.lookAt(eye, target, up);
const view = m4.inverse(camera);
const viewProjection = m4.multiply(projection, view);
const gridLevel = Math.log10(zoom * zoomAdjust);
const gridFract = euclideanModulo(gridLevel, 1);
const gridZoom = Math.pow(10, Math.floor(gridLevel));
const alpha1 = Math.max((1 - gridFract) * 1);
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)
drawGrid(viewProjection, gridZoom, [0, alpha1, 0, alpha1]);
const alpha2 = Math.max(gridFract * 10) - 1;
if (alpha2 > 0) {
drawGrid(viewProjection, gridZoom * 10, [0, alpha2, 0, alpha2],);
}
}
render();
function euclideanModulo(n, m) {
return ((n % m) + m) % m;
};
function degToRad(deg) {
return deg * Math.PI / 180;
}
window.addEventListener('wheel', (e) => {
e.preventDefault();
const amount = e.deltaY;
if (e.deltaY < 0) {
zoom *= 1 - clamp(e.deltaY / -500, 0, 1);
} else {
zoom *= 1 + clamp(e.deltaY / 500, 0, 1);
}
zoom = clamp(zoom, 0.0001, 10000);
render();
}, {passive: false});
window.addEventListener('resize', render);
function clamp(v, min, max) {
return Math.max(min, Math.min(max, v));
}
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
#ui { position: absolute; left: 1em; top: 1em; padding: 1em; color: white; }
<canvas></canvas>
<script src="https://twgljs.org/dist/4.x/twgl-full.js"></script>
<div id="ui">
<div>zoom:<span id="zoom"></span></div>
</div>

Related

WebGL fade-out does not work for white color

I am trying to adapt a webgl code that generates firework explositions, where projectiles fade out from canvas by means of painting over with 0.1 alpha (as I understand it).
However, this technique somehow doesn't work for the explosition of white color.
You can see that white projectiles leave traces and never fully disappear, while other colors work fine (or maybe they also don't work, but I don't see the traces on black background).
Can somebody help me understand what is happening here?
// from https://codepen.io/towc/pen/oBvYEL
var gl = c.getContext('webgl2', {preserveDrawingBuffer: true})
, w = c.width = window.innerWidth
, h = c.height = window.innerHeight
, webgl = {}
, opts = {
projectileAlpha: .8,
projectileLineWidth: 2,
fireworkAngleSpan: .5,
baseFireworkVel: 3,
gravity: .02,
xFriction: .995,
baseShardVel: 1,
addedShardVel: .2,
fireworks: 1, // 1 firework for each 10x10 pixels,
baseShardsParFirework: 10,
addedShardsParFirework: 10,
shardFireworkVelMultiplier: .3
}
// updated to use WebGL2 and GL ES 3.0
// pass full color instead of just hue
// remove moving projectile, keep only the explosion
webgl.vertexShaderSource = `#version 300 es
uniform int u_mode;
uniform vec2 u_res;
in vec4 a_data;
out vec4 v_color;
vec3 c2rgb(int color_int){
int r = color_int >> 16;
int g = color_int >> 8 & 0xff;
int b = color_int & 0xff;
return vec3(r/255, g/255, b/255);
}
void clear(){
gl_Position = vec4( a_data.xy, 0, 1 );
v_color = vec4( 0, 0, 0, a_data.w );
}
void draw(){
gl_Position = vec4( vec2( 1, -1 ) * ( ( a_data.xy / u_res ) * 2. - 1. ), 0, 1 );
v_color = vec4( 1, 1, 1, a_data.w );
}
void main(){
if( u_mode == 0 )
draw();
else
clear();
}
`;
webgl.fragmentShaderSource = `#version 300 es
precision mediump float;
in vec4 v_color;
out vec4 fragColor;
void main(){
fragColor = v_color;
}
`;
webgl.vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(webgl.vertexShader, webgl.vertexShaderSource);
gl.compileShader(webgl.vertexShader);
if (!gl.getShaderParameter(webgl.vertexShader, gl.COMPILE_STATUS))
console.log(gl.getShaderInfoLog(webgl.vertexShader));
webgl.fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(webgl.fragmentShader, webgl.fragmentShaderSource);
gl.compileShader(webgl.fragmentShader);
if (!gl.getShaderParameter(webgl.fragmentShader, gl.COMPILE_STATUS))
console.log(gl.getShaderInfoLog(webgl.fragmentShader));
webgl.shaderProgram = gl.createProgram();
gl.attachShader(webgl.shaderProgram, webgl.vertexShader);
gl.attachShader(webgl.shaderProgram, webgl.fragmentShader);
gl.linkProgram(webgl.shaderProgram);
gl.useProgram(webgl.shaderProgram);
webgl.dataAttribLoc = gl.getAttribLocation(webgl.shaderProgram, 'a_data');
webgl.dataBuffer = gl.createBuffer();
gl.enableVertexAttribArray(webgl.dataAttribLoc);
gl.bindBuffer(gl.ARRAY_BUFFER, webgl.dataBuffer);
gl.vertexAttribPointer(webgl.dataAttribLoc, 4, gl.FLOAT, false, 0, 0);
webgl.resUniformLoc = gl.getUniformLocation(webgl.shaderProgram, 'u_res');
webgl.modeUniformLoc = gl.getUniformLocation(webgl.shaderProgram, 'u_mode');
gl.viewport(0, 0, w, h);
gl.uniform2f(webgl.resUniformLoc, w, h);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.enable(gl.BLEND);
gl.lineWidth(opts.projectileLineWidth);
webgl.data = [];
webgl.clear = function () {
gl.uniform1i(webgl.modeUniformLoc, 1);
let a = .1;
webgl.data = [
-1, -1, 0, 0.1,
1, -1, 0, 0.1,
-1, 1, 0, 0.1,
-1, 1, 0, 0.1,
1, -1, 0, 0.1,
1, 1, 0, 0.1
];
webgl.draw(gl.TRIANGLES);
gl.uniform1i(webgl.modeUniformLoc, 0);
webgl.data.length = 0;
}
webgl.draw = function (glType) {
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(webgl.data), gl.STATIC_DRAW);
gl.drawArrays(glType, 0, webgl.data.length / 4);
}
let fireworks = []
, tick = 0
, sins = []
, coss = []
, maxShardsParFirework = opts.baseShardsParFirework + opts.addedShardsParFirework
, tau = 6.283185307179586476925286766559;
for (let i = 0; i < maxShardsParFirework; ++i) {
sins[i] = Math.sin(tau * i / maxShardsParFirework);
coss[i] = Math.cos(tau * i / maxShardsParFirework);
}
function Firework() {
this.reset();
this.shards = [];
for (let i = 0; i < maxShardsParFirework; ++i)
this.shards.push(new Shard(this));
}
Firework.prototype.reset = function () {
let angle = -Math.PI / 2 + (Math.random() - .5) * opts.fireworkAngleSpan
, vel = opts.baseFireworkVel * Math.random();
this.mode = 0;
this.vx = vel * Math.cos(angle);
this.vy = vel * Math.sin(angle);
this.x = Math.random() * w;
this.y = Math.random() * h;
this.hue = 0;
let ph = this.hue
, px = this.x
, py = this.y;
webgl.data.push(
px, py, ph, opts.projectileAlpha * .2,
this.x, this.y, this.hue, opts.projectileAlpha * .2);
}
Firework.prototype.step = function () {
if (this.mode === 0) {
this.mode = 1;
this.shardAmount = opts.baseShardsParFirework + opts.addedShardsParFirework * Math.random() | 0;
let baseAngle = Math.random() * tau
, x = Math.cos(baseAngle)
, y = Math.sin(baseAngle)
, sin = sins[this.shardAmount]
, cos = coss[this.shardAmount];
for (let i = 0; i < this.shardAmount; ++i) {
let vel = opts.baseShardVel + opts.addedShardVel * Math.random();
this.shards[i].reset(x * vel, y * vel)
let X = x;
x = x * cos - y * sin;
y = y * cos + X * sin;
}
}
if (this.mode === 1) {
this.ph = this.hue
this.hue = 0;
let allDead = true;
for (let i = 0; i < this.shardAmount; ++i) {
let shard = this.shards[i];
if (!shard.dead) {
shard.step();
allDead = false;
}
}
if (allDead)
this.reset();
}
}
function Shard(parent) {
this.parent = parent;
}
Shard.prototype.reset = function (vx, vy) {
this.x = this.parent.x;
this.y = this.parent.y;
this.vx = this.parent.vx * opts.shardFireworkVelMultiplier + vx;
this.vy = this.parent.vy * opts.shardFireworkVelMultiplier + vy;
this.starty = this.y;
this.dead = false;
this.tick = 1;
}
Shard.prototype.step = function () {
this.tick += .05;
let px = this.x
, py = this.y;
this.x += this.vx *= opts.xFriction;
this.y += this.vy += opts.gravity;
var proportion = 1 - (this.y - this.starty) / (h - this.starty);
webgl.data.push(
px, py, this.parent.ph, opts.projectileAlpha / this.tick,
this.x, this.y, this.parent.hue, opts.projectileAlpha / this.tick);
if (this.y > h)
this.dead = true;
}
function anim() {
window.requestAnimationFrame(anim);
webgl.clear();
++tick;
if (fireworks.length < opts.fireworks)
fireworks.push(new Firework);
fireworks.map(function (firework) {
firework.step();
});
webgl.draw(gl.LINES);
}
anim();
window.addEventListener('resize', function () {
w = c.width = window.innerWidth;
h = c.height = window.innerHeight;
gl.viewport(0, 0, w, h);
gl.uniform2f(webgl.resUniformLoc, w, h);
});
body {
overflow: hidden;
margin:0;
}
#c {
position: absolute;
top: 0;
left: 0;
background-color: #111;
}
<canvas id="c"> </canvas>

How is it possible to change the position of an object consisted of points floating around a center in Three Js?

So I have made this galaxy object consisted of points floating around its center
const parameters = {}
parameters.count = 4500
parameters.size = 0.005
parameters.radius = 0.7
parameters.branches = 4
parameters.spin = 1
parameters.randomness = 0.1
parameters.randomnessPower = 5
parameters.insideColor = '#d42b2b'
parameters.outsideColor = '#14be4c'
let galaxyGeometry = null
let galaxyMaterial = null
let galaxyPoints = null
const generateGalaxy = () =>
{
//DESTROY STUFF
if(galaxyPoints !== null)
{
galaxyGeometry.dispose()
galaxyMaterial.dispose()
scene.remove(galaxyPoints)
}
//GALAXY GEOM AND PARTICLES
galaxyGeometry = new THREE.BufferGeometry()
const positions = new Float32Array(parameters.count * 3)
const randomness = new Float32Array(parameters.count * 3)
const galaxyColors = new Float32Array(parameters.count * 3)
const galaxyScales = new Float32Array(parameters.count * 1)
const colorInside = new THREE.Color(parameters.insideColor)
const colorOutside = new THREE.Color(parameters.outsideColor)
for(let i = 0; i < parameters.count; i++)
{
const i3 = i * 3
//POSITION
const radius = Math.random() * parameters.radius
const branchAngle = (i % parameters.branches) / parameters.branches * Math.PI * 2
const randomX = Math.pow(Math.random(), parameters.randomnessPower) * (Math.random() < 0.5 ? 1: -1) * parameters.randomness * radius
const randomY = Math.pow(Math.random(), parameters.randomnessPower) * (Math.random() < 0.5 ? 1: -1) * parameters.randomness * radius
const randomZ = Math.pow(Math.random(), parameters.randomnessPower) * (Math.random() < 0.5 ? 1: -1) * parameters.randomness * radius
positions[i3 ] = Math.cos(branchAngle) * radius
positions[i3 + 1] = 0
positions[i3 + 2] = Math.sin(branchAngle) * radius
randomness[i3 ] = randomX
randomness[i3 + 1] = randomY
randomness[i3 + 2] = randomZ
//COLORS
// Color
const mixedColor = colorInside.clone()
mixedColor.lerp(colorOutside, radius / parameters.radius)
galaxyColors[i3 ] = mixedColor.r
galaxyColors[i3 + 1] = mixedColor.g
galaxyColors[i3 + 2] = mixedColor.b
// Scale
galaxyScales[i] = Math.random()
}
galaxyGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
galaxyGeometry.setAttribute('aRandomness', new THREE.BufferAttribute(randomness, 3))
galaxyGeometry.setAttribute('color', new THREE.BufferAttribute(galaxyColors, 3))
galaxyGeometry.setAttribute('aScale', new THREE.BufferAttribute(galaxyColors, 3))
//GALAXY PARTICLE MATERIALS
galaxyMaterial = new THREE.ShaderMaterial({
depthWrite: false,
blending: THREE.AdditiveBlending,
vertexColors: true,
uniforms:
{
vAlpha: { value: 1.0 },
uTime: { value: 0 },
uSize: { value: 40 * renderer.getPixelRatio() }
},
vertexShader: galaxyVertex,
fragmentShader: galaxyFragment,
})
if(galaxyMaterial.uniforms.vAlpha.value !== 1.0)
{
galaxyGeometry.dispose()
galaxyMaterial.dispose()
scene.remove(galaxyPoints)
}
//GALAXY MESH
galaxyPoints = new THREE.Points(galaxyGeometry, galaxyMaterial)
console.log(galaxyPoints.position)
scene.add(galaxyPoints)
}
along with its vertex
uniform float uTime;
uniform float uSize;
attribute vec3 aRandomness;
attribute float aScale;
varying vec3 vColor;
void main()
{
//POSITION
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
// Rotate
float angle = atan(modelPosition.x, modelPosition.z);
float distanceToCenter = length(modelPosition.xz);
float angleOffset = (1.0 / distanceToCenter) * uTime * 3.0;
angle += angleOffset;
modelPosition.x = cos(angle) * distanceToCenter;
modelPosition.y = sin(angle) * distanceToCenter;
// Randomness
modelPosition.xyz += aRandomness;
vec4 viewPosition = viewMatrix * modelPosition;
vec4 projectedPosition = projectionMatrix * viewPosition;
gl_Position = projectedPosition;
//SIZE
gl_PointSize = uSize * aScale;
gl_PointSize *= (1.0 / - viewPosition.z);
//COLOR
vColor = color;
}
and fragment shader
varying vec3 vColor;
uniform float vAlpha;
void main()
{
//LIGHT PATTERN
float strength = distance(gl_PointCoord, vec2(0.5));
strength = 1.0 - strength;
strength = pow(strength, 8.0);
//FINAL COLOR
vec3 color = mix(vec3(0.0), vColor, strength);
gl_FragColor = vec4(color, vAlpha);
}
. So my question here is this.How is it possible to move the object around the scene without changing its original look (galaxy/portal like). As I am new to Three js the only way I've known to move an object around is picking up the mesh and setting its position to whatever needed. The problem I'm having right now though is that the points are moving around a central point. When I try to move the mesh around the whole galaxy is breaking up. What is the correct approach here in order to move the whole galaxy thing, let's say by 2.0 in the x , y or z axis? I would really appreciate an explanation to why this is happening as well. Thank you in advance.

How to create nodes with a border with the webgl renderer in sigmajs

I have a rather large graph so it is necessary to use webgl instead of canvas. I tried to change the webgl node renderer by trying to trick it to draw two circles with the outer one being a little bit bigger, thus creating an border. Unfortunately this didn't work. In the data array the extra code is completely ignored. If someone has an idea it would be appreciated! Below is the code that renders the nodes for the webgl renderer.
sigma.webgl.nodes.def = {
POINTS: 3,
ATTRIBUTES: 5,
addNode: function(node, data, i, prefix, settings) {
var color = sigma.utils.floatColor(
node.color || settings('defaultNodeColor')
);
data[i++] = node[prefix + 'x'];
data[i++] = node[prefix + 'y'];
data[i++] = node[prefix + 'size'];
data[i++] = 7864320;
data[i++] = 0;
data[i++] = node[prefix + 'x'];
data[i++] = node[prefix + 'y'];
data[i++] = node[prefix + 'size'];
data[i++] = 7864320;
data[i++] = 2 * Math.PI / 3;
data[i++] = node[prefix + 'x'];
data[i++] = node[prefix + 'y'];
data[i++] = node[prefix + 'size'];
data[i++] = 7864320;
data[i++] = 4 * Math.PI / 3;
/* This below was my idea to create another node which is slightly bigger
and white. The parameters for that are not the issue. The issue is that the
log seems to skip this after 12 indexes of the array data for every node. I
wasn't able to find how they define this. */
data[i++] = node[prefix + 'x'];
data[i++] = node[prefix + 'y'];
data[i++] = node[prefix + 'size'];
data[i++] = color;
data[i++] = 0;
data[i++] = node[prefix + 'x'];
data[i++] = node[prefix + 'y'];
data[i++] = node[prefix + 'size'];
data[i++] = color;
data[i++] = 2 * Math.PI / 3;
data[i++] = node[prefix + 'x'];
data[i++] = node[prefix + 'y'];
data[i++] = node[prefix + 'size'];
data[i++] = color;
data[i++] = 4 * Math.PI / 3;
*/
//The log is in the picture below
console.log(data);
},
render: function(gl, program, data, params) {
var buffer;
// Define attributes:
// I guess they define the location and the attributes here.
var positionLocation =
gl.getAttribLocation(program, 'a_position'),
sizeLocation =
gl.getAttribLocation(program, 'a_size'),
colorLocation =
gl.getAttribLocation(program, 'a_color'),
angleLocation =
gl.getAttribLocation(program, 'a_angle'),
resolutionLocation =
gl.getUniformLocation(program, 'u_resolution'),
matrixLocation =
gl.getUniformLocation(program, 'u_matrix'),
ratioLocation =
gl.getUniformLocation(program, 'u_ratio'),
scaleLocation =
gl.getUniformLocation(program, 'u_scale');
buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW);
// I don't know what happens here
gl.enableVertexAttribArray(positionLocation);
gl.enableVertexAttribArray(sizeLocation);
gl.enableVertexAttribArray(colorLocation);
gl.enableVertexAttribArray(angleLocation);
gl.vertexAttribPointer(
positionLocation,
2,
gl.FLOAT,
false,
this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
0
);
gl.vertexAttribPointer(
sizeLocation,
1,
gl.FLOAT,
false,
this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
8
);
gl.vertexAttribPointer(
colorLocation,
1,
gl.FLOAT,
false,
this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
12
);
gl.vertexAttribPointer(
angleLocation,
1,
gl.FLOAT,
false,
this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
16
);
gl.drawArrays(
gl.TRIANGLES,
params.start || 0,
params.count || (data.length / this.ATTRIBUTES)
);
},
From Without Border
To with Border(I did this with the canvas renderer, where it was really easy)
This is the log. You can see that only the first 3 blocks are looped(only the ones with the color value 7864320
If any of you know another method to achieve the border I would love to know.
A simple way to plot circles with WebGL is to use gl.POINTS instead of gl.TRIANGLES. With this trick, one vertex is used for one circle, whatever big the radius is. Moreover, you can have a border of the size you want.
In the vertex shader, you can use gl_PointSize to set the diameter (in pixels) of the circle to draw for your vertex.
attribute vec2 attCoords;
attribute float attRadius;
attribute float attBorder;
attribute vec3 attColor;
varying float varR1;
varying float varR2;
varying float varR3;
varying float varR4;
varying vec4 varColor;
const float fading = 0.5;
void main() {
float r4 = 1.0;
float r3 = 1.0 - fading / attRadius;
float r2 = 1.0 - attBorder / attRadius;
float r1 = r2 - fading / attRadius;
varR4 = r4 * r4 * 0.25;
varR3 = r3 * r3 * 0.25;
varR2 = r2 * r2 * 0.25;
varR1 = r1 * r1 * 0.25;
varColor = vec4( attColor.rgb, 1.0 );
gl_PointSize = 2.0 * attRadius;
gl_Position = vec4( attCoords.xy, 0, 1 );
}
In the fragment shader, you can know which of the POINT pixel you are processing. You get the coords of this pixel in gl_PointCoord. (0,0) is th top-left pixel and (1,1) is the bottom-right pixel.
Moreover, you can use the keyword discard which is equivalent to return but telling WebGL tht the current fragment must not be drawn.
precision mediump float;
varying float varR1;
varying float varR2;
varying float varR3;
varying float varR4;
varying vec4 varColor;
const vec4 WHITE = vec4(1, 1, 1, 1);
const vec4 TRANSPARENT = vec4(1, 1, 1, 0);
void main() {
float x = gl_PointCoord.x - 0.5;
float y = gl_PointCoord.y - 0.5;
float radius = x * x + y * y;
if( radius > 1.0 ) discard;
if( radius < varR1 )
gl_FragColor = varColor;
else if( radius < varR2 )
gl_FragColor = mix(varColor, WHITE, (radius - varR1) / (varR2 - varR1));
else if( radius < varR3 )
gl_FragColor = WHITE;
else
gl_FragColor = mix(WHITE, TRANSPARENT, (radius - varR3) / (varR4 - varR3));
}
Basically, if the pixel is at more than attRadius from the center, you discard the pixel. If it is inside attRadius - attBorder you use the color. And in between, you use white.
Finally, we added a subtlety consisting in bluring the limits between color and white, and white and transparent. This gives us anti-aliasing by adding a little bluriness.
Here is a full working example: https://jsfiddle.net/7rh2eog1/2/

Moving entire shape to point on circle webgl

I have the following code trying to draw a wreath by putting stars on points on a circle. I am able to draw one star, but when I try to draw a wreath it only draws one branch around the circle, or right now on one point on the circle. I know there is a problem with how I am nesting the modelViewMatrices I can't think of the proper way to go about doing the transformation. I need to draw the star and then translate the entire star.
function DrawWreath()
{
var radius = 0.5;
for (var i = 0; i < 1; i++) {
var theta = i * 30;
var x = radius * Math.cos(theta);
var y = radius * Math.sin(theta);
var t = translate(x, y, 0);
if (modelViewMatrix) {
modelViewMatrix = mult(modelViewMatrix, t) ;
} else {
modelViewMatrix = t;
}
modelViewStack.push(modelViewMatrix);
DrawOneStar();
modelViewMatrix = modelViewStack.pop();
}
}
function DrawOneStar()
{
// draw the full star
for (var i=1; i <= 5; i++) {
r = rotate(72*i, 0, 0, 1);
if (modelViewMatrix) {
modelViewMatrix = mult(r, modelViewMatrix) ;
} else {
modelViewMatrix = r;
}
modelViewMatrix = r;
DrawOneBranch();
}
}
function DrawOneBranch()
{
var s;
// one branch
s = scale4(1/16, 1/16, 1);
modelViewStack.push(modelViewMatrix);
modelViewMatrix = mult(modelViewMatrix, s);
gl.uniformMatrix4fv(modelViewMatrixLoc, false, flatten(modelViewMatrix));
gl.drawArrays( gl.LINE_LOOP, 0, vertices.length);
/*
modelViewMatrix = modelViewStack.pop();
//s = scale4(1/8, -1/8, 1);
modelViewMatrix = mult(modelViewMatrix, s);
gl.uniformMatrix4fv(modelViewMatrixLoc, false, flatten(modelViewMatrix));
gl.drawArrays( gl.LINE_STRIP, 0, vertices.length);
*/
}
Lots of issues with the code
The code in DrawOneStar is rotating on the left
mult(r, modelViewMatrix) // ???
Seems like you want this
mult(modelViewMatrix, r)
just like you did with translate and scale
The code in DrawOneStar is not saving the matrix
that means you either want to fix the code so it saves
the matrix or, you want to rotate a fixed amount.
As the code is now it's rotating 72, then rotating 72 + 144, then rotating
72 + 144 + 216 because each time it's rotating the matrix it previously
rotated
The code in DrawOneBranch is not popping the matrix
That line is commented out
theta is using degrees
Most math libraries use radians so this code is probably not doing
what you expect
var theta = i * 30;
var x = radius * Math.cos(theta);
var y = radius * Math.sin(theta);
Math.sin and Math.cos require radians not degrees.
The outer loop is only doing one iteration
for (var i = 0; i < 1; i++) { // ???
Other suggestions
use a better math library. Whatever math library requires calling a flatten function to prepare the matrices to be usable by WebGL will be slower than one that doesn't. Also a library that takes radians for rotation and field of view means it will match the other built in math functions like Math.cos etc...
Put a matrix in modelViewMatrix to start. Then you can remove all the checks for if there is a matrix or not
When looping and computing a value consider using normalized numbers (numbers that go from 0 to 1) then computing other values based on that.
For example the code has theta = i * 30 in the outer loop and in the next loop there's rotate(i * 72, ...) but if you change the number of iterations then you also have to change those numbers to match.
Instead first compute a value that goes from to 0 to 1 based on the loop. Example
const numStars = 10;
for (let i = 0; i < numStars; ++i) {
const l = i / numStars; // goes from 0 to 1
Then use that value to compute the angle;
const theta = l * 360; // or l * Math.PI * 2 for radians
Similarly
const numRotations = 5;
for (let i = 0; i < numRotations; ++i) {
const l = i / numRotations; // goes from 0 to 1
rotate(i * 360, ....
That way you can change numStars and numRotations easily and not
have to change any other code
function DrawWreath()
{
var radius = 0.5;
for (var i = 0; i < 10; i++) {
var theta = i / 10 * Math.PI * 2;
var x = radius * Math.cos(theta);
var y = radius * Math.sin(theta);
var t = translate(x, y, 0);
modelViewStack.push(modelViewMatrix);
modelViewMatrix = mult(modelViewMatrix, t) ;
DrawOneStar();
modelViewMatrix = modelViewStack.pop();
}
}
function DrawOneStar()
{
// draw the full star
for (var i=1; i <= 5; i++) {
var r = rotate(72, 0, 0, 1);
modelViewMatrix = mult(modelViewMatrix, r) ;
DrawOneBranch();
}
}
function DrawOneBranch()
{
var s;
// one branch
s = scale4(1/16, 1/16, 1);
modelViewStack.push(modelViewMatrix);
modelViewMatrix = mult(modelViewMatrix, s);
gl.uniformMatrix4fv(modelViewMatrixLoc, false, flatten(modelViewMatrix));
gl.drawArrays( gl.LINE_LOOP, 0, vertices.length);
modelViewMatrix = modelViewStack.pop();
/*
//s = scale4(1/8, -1/8, 1);
modelViewMatrix = mult(modelViewMatrix, s);
gl.uniformMatrix4fv(modelViewMatrixLoc, false, flatten(modelViewMatrix));
gl.drawArrays( gl.LINE_STRIP, 0, vertices.length);
*/
}
function flatten(m) {
return m;
}
function translate(x, y, z) {
return m4.translation([x, y, z]);
}
function scale4(x, y, z) {
return m4.scaling([x, y, z]);
}
function rotate(a, x, y, z) {
return m4.axisRotation([x, y, z], a * Math.PI / 180);
}
function mult(a, b) {
return m4.multiply(a, b);
}
const m4 = twgl.m4;
const gl = document.querySelector("canvas").getContext("webgl");
const modelViewStack = [];
let modelViewMatrix = m4.identity();
const vs = `
attribute vec4 position;
uniform mat4 u_projectionMatrix;
uniform mat4 u_modelViewMatrix;
void main() {
gl_Position = u_projectionMatrix * u_modelViewMatrix * position;
}
`;
const fs = `
void main() { gl_FragColor = vec4(1,0,0,1); }
`;
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
position: {
numComponents: 2,
data: [
0, 1,
-.33, 0,
.33, 0,
],
},
});
twgl.resizeCanvasToDisplaySize(gl.canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.useProgram(programInfo.program);
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const scale = 1;
twgl.setUniforms(programInfo, {
u_projectionMatrix: m4.ortho(
-aspect / scale, aspect / scale, -1 / scale, 1 / scale, -1, 1),
u_modelViewMatrix: m4.identity(),
});
const vertices = { length: 3, };
const modelViewMatrixLoc = gl.getUniformLocation(programInfo.program, "u_modelViewMatrix");
DrawWreath();
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<script src="https://twgljs.org/dist/3.x/twgl-full.min.js"></script>
<canvas></canvas>
One more thing, rather than manually computing a position on a cirlce you could use the matrix function for that as well, rotate, then translate
function DrawWreath()
{
const radius = 0.5;
const numStars = 20;
for (let i = 0; i < numStars; ++i) {
const l = i / numStars;
const theta = l * Math.PI * 2;
const r = rotateInRadians(theta, 0, 0, 1);
const t = translate(radius, 0, 0);
modelViewStack.push(modelViewMatrix);
modelViewMatrix = mult(modelViewMatrix, r);
modelViewMatrix = mult(modelViewMatrix, t);
DrawOneStar();
modelViewMatrix = modelViewStack.pop();
}
}
function DrawOneStar()
{
// draw the full star
const numParts = 6;
for (let i = 0; i < numParts; ++i) {
const l = i / numParts;
const r = rotateInRadians(l * Math.PI * 2, 0, 0, 1);
modelViewStack.push(modelViewMatrix);
modelViewMatrix = mult(modelViewMatrix, r) ;
DrawOneBranch();
modelViewMatrix = modelViewStack.pop();
}
}
function DrawOneBranch()
{
var s;
// one branch
s = scale4(1/16, 1/16, 1);
modelViewStack.push(modelViewMatrix);
modelViewMatrix = mult(modelViewMatrix, s);
gl.uniformMatrix4fv(modelViewMatrixLoc, false, flatten(modelViewMatrix));
gl.drawArrays( gl.LINE_LOOP, 0, vertices.length);
modelViewMatrix = modelViewStack.pop();
}
function flatten(m) {
return m;
}
function translate(x, y, z) {
return m4.translation([x, y, z]);
}
function scale4(x, y, z) {
return m4.scaling([x, y, z]);
}
function rotate(a, x, y, z) {
return m4.axisRotation([x, y, z], a * Math.PI / 180);
}
function rotateInRadians(a, x, y, z) {
return m4.axisRotation([x, y, z], a);
}
function mult(a, b) {
return m4.multiply(a, b);
}
const m4 = twgl.m4;
const gl = document.querySelector("canvas").getContext("webgl");
const modelViewStack = [];
let modelViewMatrix = m4.identity();
const vs = `
attribute vec4 position;
uniform mat4 u_projectionMatrix;
uniform mat4 u_modelViewMatrix;
void main() {
gl_Position = u_projectionMatrix * u_modelViewMatrix * position;
}
`;
const fs = `
void main() { gl_FragColor = vec4(1,0,0,1); }
`;
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
position: {
numComponents: 2,
data: [
0, 1,
-.33, 0,
.33, 0,
],
},
});
twgl.resizeCanvasToDisplaySize(gl.canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.useProgram(programInfo.program);
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const scale = 1;
twgl.setUniforms(programInfo, {
u_projectionMatrix: m4.ortho(
-aspect / scale, aspect / scale, -1 / scale, 1 / scale, -1, 1),
u_modelViewMatrix: m4.identity(),
});
const vertices = { length: 3, };
const modelViewMatrixLoc = gl.getUniformLocation(programInfo.program, "u_modelViewMatrix");
DrawWreath();
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<script src="https://twgljs.org/dist/3.x/twgl-full.min.js"></script>
<canvas></canvas>
You might find these articles useful

Canvas Rotating Star Field

I'm taking the following approach to animate a star field across the screen, but I'm stuck for the next part.
JS
var c = document.getElementById('stars'),
ctx = c.getContext("2d"),
t = 0; // time
c.width = 300;
c.height = 300;
var w = c.width,
h = c.height,
z = c.height,
v = Math.PI; // angle of vision
(function animate() {
Math.seedrandom('bg');
ctx.globalAlpha = 1;
for (var i = 0; i <= 100; i++) {
var x = Math.floor(Math.random() * w), // pos x
y = Math.floor(Math.random() * h), // pos y
r = Math.random()*2 + 1, // radius
a = Math.random()*0.5 + 0.5, // alpha
// linear
d = (r*a), // depth
p = t*d; // pixels per t
x = x - p; // movement
x = x - w * Math.floor(x / w); // go around when x < 0
(function draw(x,y) {
var gradient = ctx.createRadialGradient(x, y, 0, x + r, y + r, r * 2);
gradient.addColorStop(0, 'rgba(255, 255, 255, ' + a + ')');
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
ctx.beginPath();
ctx.arc(x, y, r, 0, 2*Math.PI);
ctx.fillStyle = gradient;
ctx.fill();
return draw;
})(x, y);
}
ctx.restore();
t += 1;
requestAnimationFrame(function() {
ctx.clearRect(0, 0, c.width, c.height);
animate();
});
})();
HTML
<canvas id="stars"></canvas>
CSS
canvas {
background: black;
}
JSFiddle
What it does right now is animate each star with a delta X that considers the opacity and size of the star, so the smallest ones appear to move slower.
Use p = t; to have all the stars moving at the same speed.
QUESTION
I'm looking for a clearly defined model where the velocities give the illusion of the stars rotating around the expectator, defined in terms of the center of the rotation cX, cY, and the angle of vision v which is what fraction of 2π can be seen (if the center of the circle is not the center of the screen, the radius should be at least the largest portion). I'm struggling to find a way that applies this cosine to the speed of star movements, even for a centered circle with a rotation of π.
These diagrams might further explain what I'm after:
Centered circle:
Non-centered:
Different angle of vision:
I'm really lost as to how to move forwards. I already stretched myself a bit to get here. Can you please help me with some first steps?
Thanks
UPDATE
I have made some progress with this code:
// linear
d = (r*a)*z, // depth
v = (2*Math.PI)/w,
p = Math.floor( d * Math.cos( t * v ) ); // pixels per t
x = x + p; // movement
x = x - w * Math.floor(x / w); // go around when x < 0
JSFiddle
Where p is the x coordinate of a particle in uniform circular motion and v is the angular velocity, but this generates a pendulum effect. I am not sure how to change these equations to create the illusion that the observer is turning instead.
UPDATE 2:
Almost there. One user at the ##Math freenode channel was kind enough to suggest the following calculation:
// linear
d = (r*a), // depth
p = t*d; // pixels per t
x = x - p; // movement
x = x - w * Math.floor(x / w); // go around when x < 0
x = (x / w) - 0.5;
y = (y / h) - 0.5;
y /= Math.cos(x);
x = (x + 0.5) * w;
y = (y + 0.5) * h;
JSFiddle
This achieves the effect visually, but does not follow a clearly defined model in terms of the variables (it just "hacks" the effect) so I cannot see a straightforward way to do different implementations (change the center, angle of vision). The real model might be very similar to this one.
UPDATE 3
Following from Iftah's response, I was able to use Sylvester to apply a rotation matrix to the stars, which need to be saved in an array first. Also each star's z coordinate is now determined and the radius r and opacity a are derived from it instead. The code is substantially different and lenghthier so I am not posting it, but it might be a step in the right direction. I cannot get this to rotate continuously yet. Using matrix operations on each frame seems costly in terms of performance.
JSFiddle
Here's some pseudocode that does what you're talking about.
Make a bunch of stars not too far but not too close (via rejection sampling)
Set up a projection matrix (defines the camera frustum)
Each frame
Compute our camera rotation angle
Make a "view" matrix (repositions the stars to be relative to our view)
Compose the view and projection matrix into the view-projection matrix
For each star
Apply the view-projection matrix to give screen star coordinates
If the star is behind the camera skip it
Do some math to give the star a nice seeming 'size'
Scale the star coordinate to the canvas
Draw the star with its canvas coordinate and size
I've made an implementation of the above. It uses the gl-matrix Javascript library to handle some of the matrix math. It's good stuff. (Fiddle for this is here, or see below.)
var c = document.getElementById('c');
var n = c.getContext('2d');
// View matrix, defines where you're looking
var viewMtx = mat4.create();
// Projection matrix, defines how the view maps onto the screen
var projMtx = mat4.create();
// Adapted from http://stackoverflow.com/questions/18404890/how-to-build-perspective-projection-matrix-no-api
function ComputeProjMtx(field_of_view, aspect_ratio, near_dist, far_dist, left_handed) {
// We'll assume input parameters are sane.
field_of_view = field_of_view * Math.PI / 180.0; // Convert degrees to radians
var frustum_depth = far_dist - near_dist;
var one_over_depth = 1 / frustum_depth;
var e11 = 1.0 / Math.tan(0.5 * field_of_view);
var e00 = (left_handed ? 1 : -1) * e11 / aspect_ratio;
var e22 = far_dist * one_over_depth;
var e32 = (-far_dist * near_dist) * one_over_depth;
return [
e00, 0, 0, 0,
0, e11, 0, 0,
0, 0, e22, e32,
0, 0, 1, 0
];
}
// Make a view matrix with a simple rotation about the Y axis (up-down axis)
function ComputeViewMtx(angle) {
angle = angle * Math.PI / 180.0; // Convert degrees to radians
return [
Math.cos(angle), 0, Math.sin(angle), 0,
0, 1, 0, 0,
-Math.sin(angle), 0, Math.cos(angle), 0,
0, 0, 0, 1
];
}
projMtx = ComputeProjMtx(70, c.width / c.height, 1, 200, true);
var angle = 0;
var viewProjMtx = mat4.create();
var minDist = 100;
var maxDist = 1000;
function Star() {
var d = 0;
do {
// Create random points in a cube.. but not too close.
this.x = Math.random() * maxDist - (maxDist / 2);
this.y = Math.random() * maxDist - (maxDist / 2);
this.z = Math.random() * maxDist - (maxDist / 2);
var d = this.x * this.x +
this.y * this.y +
this.z * this.z;
} while (
d > maxDist * maxDist / 4 || d < minDist * minDist
);
this.dist = Math.sqrt(d);
}
Star.prototype.AsVector = function() {
return [this.x, this.y, this.z, 1];
}
var stars = [];
for (var i = 0; i < 5000; i++) stars.push(new Star());
var lastLoop = Date.now();
function loop() {
var now = Date.now();
var dt = (now - lastLoop) / 1000.0;
lastLoop = now;
angle += 30.0 * dt;
viewMtx = ComputeViewMtx(angle);
//console.log('---');
//console.log(projMtx);
//console.log(viewMtx);
mat4.multiply(viewProjMtx, projMtx, viewMtx);
//console.log(viewProjMtx);
n.beginPath();
n.rect(0, 0, c.width, c.height);
n.closePath();
n.fillStyle = '#000';
n.fill();
n.fillStyle = '#fff';
var v = vec4.create();
for (var i = 0; i < stars.length; i++) {
var star = stars[i];
vec4.transformMat4(v, star.AsVector(), viewProjMtx);
v[0] /= v[3];
v[1] /= v[3];
v[2] /= v[3];
//v[3] /= v[3];
if (v[3] < 0) continue;
var x = (v[0] * 0.5 + 0.5) * c.width;
var y = (v[1] * 0.5 + 0.5) * c.height;
// Compute a visual size...
// This assumes all stars are the same size.
// It also doesn't scale with canvas size well -- we'd have to take more into account.
var s = 300 / star.dist;
n.beginPath();
n.arc(x, y, s, 0, Math.PI * 2);
//n.rect(x, y, s, s);
n.closePath();
n.fill();
}
window.requestAnimationFrame(loop);
}
loop();
<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.3.1/gl-matrix-min.js"></script>
<canvas id="c" width="500" height="500"></canvas>
Some links:
More on projection matrices
gl-matrix
Using view/projection matrices
Update
Here's another version that has keyboard controls. Kinda fun. You can see the difference between rotating and parallax from strafing. Works best full page. (Fiddle for this is here or see below.)
var c = document.getElementById('c');
var n = c.getContext('2d');
// View matrix, defines where you're looking
var viewMtx = mat4.create();
// Projection matrix, defines how the view maps onto the screen
var projMtx = mat4.create();
// Adapted from http://stackoverflow.com/questions/18404890/how-to-build-perspective-projection-matrix-no-api
function ComputeProjMtx(field_of_view, aspect_ratio, near_dist, far_dist, left_handed) {
// We'll assume input parameters are sane.
field_of_view = field_of_view * Math.PI / 180.0; // Convert degrees to radians
var frustum_depth = far_dist - near_dist;
var one_over_depth = 1 / frustum_depth;
var e11 = 1.0 / Math.tan(0.5 * field_of_view);
var e00 = (left_handed ? 1 : -1) * e11 / aspect_ratio;
var e22 = far_dist * one_over_depth;
var e32 = (-far_dist * near_dist) * one_over_depth;
return [
e00, 0, 0, 0,
0, e11, 0, 0,
0, 0, e22, e32,
0, 0, 1, 0
];
}
// Make a view matrix with a simple rotation about the Y axis (up-down axis)
function ComputeViewMtx(angle) {
angle = angle * Math.PI / 180.0; // Convert degrees to radians
return [
Math.cos(angle), 0, Math.sin(angle), 0,
0, 1, 0, 0,
-Math.sin(angle), 0, Math.cos(angle), 0,
0, 0, -250, 1
];
}
projMtx = ComputeProjMtx(70, c.width / c.height, 1, 200, true);
var angle = 0;
var viewProjMtx = mat4.create();
var minDist = 100;
var maxDist = 1000;
function Star() {
var d = 0;
do {
// Create random points in a cube.. but not too close.
this.x = Math.random() * maxDist - (maxDist / 2);
this.y = Math.random() * maxDist - (maxDist / 2);
this.z = Math.random() * maxDist - (maxDist / 2);
var d = this.x * this.x +
this.y * this.y +
this.z * this.z;
} while (
d > maxDist * maxDist / 4 || d < minDist * minDist
);
this.dist = 100;
}
Star.prototype.AsVector = function() {
return [this.x, this.y, this.z, 1];
}
var stars = [];
for (var i = 0; i < 5000; i++) stars.push(new Star());
var lastLoop = Date.now();
var dir = {
up: 0,
down: 1,
left: 2,
right: 3
};
var dirStates = [false, false, false, false];
var shiftKey = false;
var moveSpeed = 100.0;
var turnSpeed = 1.0;
function loop() {
var now = Date.now();
var dt = (now - lastLoop) / 1000.0;
lastLoop = now;
angle += 30.0 * dt;
//viewMtx = ComputeViewMtx(angle);
var tf = mat4.create();
if (dirStates[dir.up]) mat4.translate(tf, tf, [0, 0, moveSpeed * dt]);
if (dirStates[dir.down]) mat4.translate(tf, tf, [0, 0, -moveSpeed * dt]);
if (dirStates[dir.left])
if (shiftKey) mat4.rotate(tf, tf, -turnSpeed * dt, [0, 1, 0]);
else mat4.translate(tf, tf, [moveSpeed * dt, 0, 0]);
if (dirStates[dir.right])
if (shiftKey) mat4.rotate(tf, tf, turnSpeed * dt, [0, 1, 0]);
else mat4.translate(tf, tf, [-moveSpeed * dt, 0, 0]);
mat4.multiply(viewMtx, tf, viewMtx);
//console.log('---');
//console.log(projMtx);
//console.log(viewMtx);
mat4.multiply(viewProjMtx, projMtx, viewMtx);
//console.log(viewProjMtx);
n.beginPath();
n.rect(0, 0, c.width, c.height);
n.closePath();
n.fillStyle = '#000';
n.fill();
n.fillStyle = '#fff';
var v = vec4.create();
for (var i = 0; i < stars.length; i++) {
var star = stars[i];
vec4.transformMat4(v, star.AsVector(), viewProjMtx);
if (v[3] < 0) continue;
var d = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
v[0] /= v[3];
v[1] /= v[3];
v[2] /= v[3];
//v[3] /= v[3];
var x = (v[0] * 0.5 + 0.5) * c.width;
var y = (v[1] * 0.5 + 0.5) * c.height;
// Compute a visual size...
// This assumes all stars are the same size.
// It also doesn't scale with canvas size well -- we'd have to take more into account.
var s = 300 / d;
n.beginPath();
n.arc(x, y, s, 0, Math.PI * 2);
//n.rect(x, y, s, s);
n.closePath();
n.fill();
}
window.requestAnimationFrame(loop);
}
loop();
function keyToDir(evt) {
var d = -1;
if (evt.keyCode === 38) d = dir.up
else if (evt.keyCode === 37) d = dir.left;
else if (evt.keyCode === 39) d = dir.right;
else if (evt.keyCode === 40) d = dir.down;
return d;
}
window.onkeydown = function(evt) {
var d = keyToDir(evt);
if (d >= 0) dirStates[d] = true;
if (evt.keyCode === 16) shiftKey = true;
}
window.onkeyup = function(evt) {
var d = keyToDir(evt);
if (d >= 0) dirStates[d] = false;
if (evt.keyCode === 16) shiftKey = false;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.3.1/gl-matrix-min.js"></script>
<div>Click in this pane. Use up/down/left/right, hold shift + left/right to rotate.</div>
<canvas id="c" width="500" height="500"></canvas>
Update 2
Alain Jacomet Forte asked:
What is your recommended method of creating general purpose 3d and if you would recommend working at the matrices level or not, specifically perhaps to this particular scenario.
Regarding matrices: If you're writing an engine from scratch on any platform, then you're unavoidably going to end up working with matrices since they help generalize the basic 3D mathematics. Even if you use OpenGL/WebGL or Direct3D you're still going to end up making a view and projection matrix and additional matrices for more sophisticated purposes. (Handling normal maps, aligning world objects, skinning, etc...)
Regarding a method of creating general purpose 3d... Don't. It will run slow, and it won't be performant without a lot of work. Rely on a hardware-accelerated library to do the heavy lifting. Creating limited 3D engines for specific projects is fun and instructive (e.g. I want a cool animation on my webpage), but when it comes to putting the pixels on the screen for anything serious, you want hardware to handle that as much as you can for performance purposes.
Sadly, the web has no great standard for that yet, but it is coming in WebGL -- learn WebGL, use WebGL. It runs great and works well when it's supported. (You can, however, get away with an awful lot just using CSS 3D transforms and Javascript.)
If you're doing desktop programming, I highly recommend OpenGL via SDL (I'm not sold on SFML yet) -- it's cross-platform and well supported.
If you're programming mobile phones, OpenGL ES is pretty much your only choice (other than a dog-slow software renderer).
If you want to get stuff done rather than writing your own engine from scratch, the defacto for the web is Three.js (which I find effective but mediocre). If you want a full game engine, there's some free options these days, the main commercial ones being Unity and Unreal. Irrlicht has been around a long time -- never had a chance to use it, though, but I hear it's good.
But if you want to make all the 3D stuff from scratch... I always found how the software renderer in Quake was made a pretty good case study. Some of that can be found here.
You are resetting the stars 2d position each frame, then moving the stars (depending on how much time and speed of each star) - this is a bad way to achieve your goal. As you discovered, it gets very complex when you try to extend this solution to more scenarios.
A better way would be to set the stars 3d location only once (at initialization) then move a "camera" each frame (depending on time). When you want to render the 2d image you then calculate the stars location on screen. The location on screen depends on the stars 3d location and the current camera location.
This will allow you to move the camera (in any direction), rotate the camera (to any angle) and render the correct stars position AND keep your sanity.

Categories

Resources