So I am not sure if my question is more of a programming question, more of a pure math question, or equally both.
Short version: I have a rotating sphere in webgl, but it isn't rotating the way I want it to. I think what I want is to rotate the sphere about an axis, and then rotate that axis about a second axis.
So I have downloaded and tweaked some javascript webgl code that renders an icosahedron and can animate its rotation along x, y, or z axes, either individually or simultaneously in a composite rotation group. I have some sense of how it is working.
I've also read (such as here) that any rotation group of Euler angles actually just yields some new, net, single axis of rotation. Which means I can't get the animation I want by just using the rotations of those three axes.
So I think what I need is a new kind of rotation, that rotates the axis of rotation itself. But I am at a loss as for how to apply that to the matrix transformation.
- Would that be implemented as a separate transformation?
- Would it be included into the geometry of the y axis rotation?
- Would it be easier to implement by rotating the camera around the x axis?
- Should I cross-post this to math stack exchange for help with the pure math?
I would appreciate any help, whether high level conceptual suggestions or detailed code suggestions.
Below is the working snippet I'm currently using. It is rotating about the y axis, and I'd like that axis of rotation to rotate around the x axis. But that's not the same as just doing a rotation group around the x and y axis simultaneously - that just yields a single, composite rotation axis of the line y = x.
var App = (function () {
function App(canvas) {
this._canvas = canvas;
this._ctx = canvas.getContext('webgl');
this._ctx.viewport(0, 0, canvas.width, canvas.height);
this._canvas.setAttribute('width', this._canvas.clientWidth.toString());
this._canvas.setAttribute('height', this._canvas.clientHeight.toString());
this._config =
{
DrawMode: this._ctx.TRIANGLES,
Quality: 3,
ZoomLevel: -4,
Rotation: {
X: 0.0000,
Y: 0.0100,
Z: 0.0000
}
};
}
App.prototype._setData = function () {
var ctx = this._ctx;
var icosahedron = new Icosahedron3D(this._config.Quality);
this._vertices = icosahedron.Points.reduce(function (a, b, i) { return i === 1 ? [a.x, a.y, a.z, b.x, b.y, b.z] : a.concat([b.x, b.y, b.z]); });
this._indices = icosahedron.TriangleIndices;
this._colors = this._generateColors(this._vertices);
var vertex_buffer = ctx.createBuffer();
ctx.bindBuffer(ctx.ARRAY_BUFFER, vertex_buffer);
ctx.bufferData(ctx.ARRAY_BUFFER, new Float32Array(this._vertices), ctx.STATIC_DRAW);
var color_buffer = ctx.createBuffer();
ctx.bindBuffer(ctx.ARRAY_BUFFER, color_buffer);
ctx.bufferData(ctx.ARRAY_BUFFER, new Float32Array(this._colors), ctx.STATIC_DRAW);
var index_buffer = ctx.createBuffer();
ctx.bindBuffer(ctx.ELEMENT_ARRAY_BUFFER, index_buffer);
ctx.bufferData(ctx.ELEMENT_ARRAY_BUFFER, new Uint16Array(this._indices), ctx.STATIC_DRAW);
return {
vertex: vertex_buffer,
color: color_buffer,
index: index_buffer
};
};
App.prototype._generateColors = function (vertices) {
var colors = [];
for (var i = 0; i < vertices.length; i += 3) {
var cvalue = 0;
var testvalue = 0;
if (vertices[i] >= 0) testvalue++;
if (vertices[i+1] >= 0) testvalue++;
if (vertices[i+2] >= 0) testvalue++;
else testvalue = 0;
if (testvalue > 0) cvalue = 1;
colors.push(cvalue);
colors.push(cvalue);
colors.push(cvalue);
}
return colors;
}
App.prototype._animate = function (proj_matrix, view_matrix, mov_matrix) {
var _this = this;
var ctx = this._ctx;
var rotThetas = this._config.Rotation;
var time_old = 0;
var zoomLevel_old = 0;
var execAnimation = function (time) {
var dt = time - time_old;
time_old = time;
for (var axis in rotThetas) {
var theta = rotThetas[axis];
if (theta > 0.0 || theta < 0.0) {
Matrix[("Rotate" + axis)](mov_matrix, dt * theta);
}
}
if (Math.abs(_this._config.ZoomLevel - zoomLevel_old) >= 0.01) {
view_matrix[14] = view_matrix[14] + (zoomLevel_old * -1) + _this._config.ZoomLevel;
zoomLevel_old = _this._config.ZoomLevel;
}
ctx.enable(ctx.DEPTH_TEST);
ctx.depthFunc(ctx.LEQUAL);
ctx.clearDepth(1.0);
ctx.viewport(0.0, 0.0, _this._canvas.width, _this._canvas.height);
ctx.clear(ctx.COLOR_BUFFER_BIT | ctx.DEPTH_BUFFER_BIT);
ctx.uniformMatrix4fv(_this._shader.Pmatrix, false, proj_matrix);
ctx.uniformMatrix4fv(_this._shader.Vmatrix, false, view_matrix);
ctx.uniformMatrix4fv(_this._shader.Mmatrix, false, mov_matrix);
ctx.drawElements(_this._config.DrawMode, _this._indices.length, ctx.UNSIGNED_SHORT, 0);
window.requestAnimationFrame(execAnimation);
};
execAnimation(0);
};
App.prototype.Draw = function () {
var buffers = this._setData();
this._shader = App.UseQuarternionShaderProgram(this._ctx, buffers.vertex, buffers.color);
var proj_matrix = new Float32Array(Matrix.GetProjection(40, this._canvas.width / this._canvas.height, 1, 100));
var view_matrix = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
var mov_matrix = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
this._animate(proj_matrix, view_matrix, mov_matrix);
};
App.UseQuarternionVertShader = function (context) {
var vertCode = "\n\t\t\tattribute vec3 position;\n\t\t\tattribute highp vec3 aVertexNormal;\n\t\t\t\n\t\t\tuniform mat4 Pmatrix;\n\t\t\tuniform mat4 Vmatrix;\n\t\t\tuniform mat4 Mmatrix;\n\n\t\t\tattribute vec4 color;\n\t\t\tvarying lowp vec4 vColor;\n\n\t\t\tvarying vec3 vLightWeighting;\n\t\t\t\n\t\t\tuniform vec3 uAmbientColor;\n\t\t\tuniform vec3 uPointLightingLocation;\n\t\t\tuniform vec3 uPointLightingColor;\n\n\t\t\tvoid main(void) {\n\t\t\t\tvec4 mvPosition = Mmatrix * vec4(position, 1.);\n\t\t\t\tgl_Position = Pmatrix*Vmatrix*mvPosition;\n\t\t\t\tgl_PointSize = 4.0;\n\t\t\t\tvColor = color;\n\n\t\t\t\tvec3 lightDirection = normalize(uPointLightingLocation - mvPosition.xyz);\n\t\t\t\tvec3 transformedNormal = vec3(Vmatrix) * aVertexNormal;\n\t\t\t\tfloat directionalLightWeighting = max(dot(transformedNormal, lightDirection), 0.0);\n\t\t\t\tvLightWeighting = uAmbientColor + uPointLightingColor * directionalLightWeighting;\n\t\t\t}";
var vertShader = context.createShader(context.VERTEX_SHADER);
context.shaderSource(vertShader, vertCode);
context.compileShader(vertShader);
return vertShader;
};
App.UseVariableFragShader = function (context) {
var fragCode = "\n\t\t\tprecision mediump float;\n\t\t\tvarying lowp vec4 vColor;\n\t\t\tvarying vec3 vLightWeighting;\n\t\t\tvoid main(void) {\n\t\t\t\tgl_FragColor = vec4(vColor.rgb, 1.);\n\t\t\t}";
var fragShader = context.createShader(context.FRAGMENT_SHADER);
context.shaderSource(fragShader, fragCode);
context.compileShader(fragShader);
return fragShader;
};
App.UseQuarternionShaderProgram = function (ctx, vertex_buffer, color_buffer) {
var vertShader = App.UseQuarternionVertShader(ctx);
var fragShader = App.UseVariableFragShader(ctx);
var shaderProgram = ctx.createProgram();
ctx.attachShader(shaderProgram, vertShader);
ctx.attachShader(shaderProgram, fragShader);
ctx.linkProgram(shaderProgram);
var Pmatrix = ctx.getUniformLocation(shaderProgram, "Pmatrix");
var Vmatrix = ctx.getUniformLocation(shaderProgram, "Vmatrix");
var Mmatrix = ctx.getUniformLocation(shaderProgram, "Mmatrix");
ctx.bindBuffer(ctx.ARRAY_BUFFER, vertex_buffer);
var position = ctx.getAttribLocation(shaderProgram, "position");
ctx.vertexAttribPointer(position, 3, ctx.FLOAT, false, 0, 0);
ctx.enableVertexAttribArray(position);
ctx.bindBuffer(ctx.ARRAY_BUFFER, color_buffer);
var color = ctx.getAttribLocation(shaderProgram, "color");
ctx.vertexAttribPointer(color, 3, ctx.FLOAT, false, 0, 0);
ctx.enableVertexAttribArray(color);
ctx.useProgram(shaderProgram);
var ambientColor = ctx.getUniformLocation(shaderProgram, "uAmbientColor");
var pointLightingLocation = ctx.getUniformLocation(shaderProgram, "uPointLightingLocation");
var pointLightingColor = ctx.getUniformLocation(shaderProgram, "uPointLightingColor");
ctx.uniform3f(ambientColor, 0.2, 0.2, 0.2);
ctx.uniform3f(pointLightingLocation, 0.0, 0.0, -20.0);
ctx.uniform3f(pointLightingColor, 0.8, 0.8, 0.8);
return {
Pmatrix: Pmatrix,
Vmatrix: Vmatrix,
Mmatrix: Mmatrix,
ShaderProgram: shaderProgram
};
};
return App;
})();
var Matrix = (function () {
function Matrix() {
}
Matrix.GetProjection = function (angle, a, zMin, zMax) {
var ang = Math.tan((angle * .5) * Math.PI / 180);
return [
0.5 / ang, 0, 0, 0,
0, 0.5 * a / ang, 0, 0,
0, 0, -(zMax + zMin) / (zMax - zMin), -1,
0, 0, (-2 * zMax * zMin) / (zMax - zMin), 0
];
};
Matrix.RotateX = function (m, angle) {
var c = Math.cos(angle);
var s = Math.sin(angle);
var mv1 = m[1], mv5 = m[5], mv9 = m[9];
m[1] = m[1] * c - m[2] * s;
m[5] = m[5] * c - m[6] * s;
m[9] = m[9] * c - m[10] * s;
m[2] = m[2] * c + mv1 * s;
m[6] = m[6] * c + mv5 * s;
m[10] = m[10] * c + mv9 * s;
};
Matrix.RotateY = function (m, angle) {
var c = Math.cos(angle);
var s = Math.sin(angle);
var mv0 = m[0], mv4 = m[4], mv8 = m[8];
m[0] = c * m[0] + s * m[2];
m[4] = c * m[4] + s * m[6];
m[8] = c * m[8] + s * m[10];
m[2] = c * m[2] - s * mv0;
m[6] = c * m[6] - s * mv4;
m[10] = c * m[10] - s * mv8;
};
Matrix.RotateZ = function (m, angle) {
var c = Math.cos(angle);
var s = Math.sin(angle);
var mv0 = m[0], mv4 = m[4], mv8 = m[8];
m[0] = c * m[0] - s * m[1];
m[4] = c * m[4] - s * m[5];
m[8] = c * m[8] - s * m[9];
m[1] = c * m[1] + s * mv0;
m[5] = c * m[5] + s * mv4;
m[9] = c * m[9] + s * mv8;
};
Matrix.Translate = function (a, b, c) {
var d = b[0], e = b[1], s = b[2];
if (!c || a == c) {
a[12] = a[0] * d + a[4] * e + a[8] * s + a[12];
a[13] = a[1] * d + a[5] * e + a[9] * s + a[13];
a[14] = a[2] * d + a[6] * e + a[10] * s + a[14];
a[15] = a[3] * d + a[7] * e + a[11] * s + a[15];
return a;
}
var g = a[0], f = a[1], h = a[2], i = a[3], j = a[4], k = a[5], l = a[6], o = a[7], m = a[8], n = a[9], p = a[10], r = a[11];
c[0] = g;
c[1] = f;
c[2] = h;
c[3] = i;
c[4] = j;
c[5] = k;
c[6] = l;
c[7] = o;
c[8] = m;
c[9] = n;
c[10] = p;
c[11] = r;
c[12] = g * d + j * e + m * s + a[12];
c[13] = f * d + k * e + n * s + a[13];
c[14] = h * d + l * e + p * s + a[14];
c[15] = i * d + o * e + r * s + a[15];
return c;
};
;
return Matrix;
})();
var Icosahedron3D = (function () {
function Icosahedron3D(quality) {
this._quality = quality;
this._calculateGeometry();
}
Icosahedron3D.prototype._calculateGeometry = function () {
this.Points = [];
this.TriangleIndices = [];
this._middlePointIndexCache = {};
this._index = 0;
var t = (1.0 + Math.sqrt(5.0)) / 2.0;
this._addVertex(-1, t, 0);
this._addVertex(1, t, 0);
this._addVertex(-1, -t, 0);
this._addVertex(1, -t, 0);
this._addVertex(0, -1, t);
this._addVertex(0, 1, t);
this._addVertex(0, -1, -t);
this._addVertex(0, 1, -t);
this._addVertex(t, 0, -1);
this._addVertex(t, 0, 1);
this._addVertex(-t, 0, -1);
this._addVertex(-t, 0, 1);
this._addFace(0, 11, 5);
this._addFace(0, 5, 1);
this._addFace(0, 1, 7);
this._addFace(0, 7, 10);
this._addFace(0, 10, 11);
this._addFace(1, 5, 9);
this._addFace(5, 11, 4);
this._addFace(11, 10, 2);
this._addFace(10, 7, 6);
this._addFace(7, 1, 8);
this._addFace(3, 9, 4);
this._addFace(3, 4, 2);
this._addFace(3, 2, 6);
this._addFace(3, 6, 8);
this._addFace(3, 8, 9);
this._addFace(4, 9, 5);
this._addFace(2, 4, 11);
this._addFace(6, 2, 10);
this._addFace(8, 6, 7);
this._addFace(9, 8, 1);
this._refineVertices();
};
Icosahedron3D.prototype._addVertex = function (x, y, z) {
var length = Math.sqrt(x * x + y * y + z * z);
this.Points.push({
x: x / length,
y: y / length,
z: z / length
});
return this._index++;
};
Icosahedron3D.prototype._addFace = function (x, y, z) {
this.TriangleIndices.push(x);
this.TriangleIndices.push(y);
this.TriangleIndices.push(z);
};
Icosahedron3D.prototype._refineVertices = function () {
for (var i = 0; i < this._quality; i++) {
var faceCount = this.TriangleIndices.length;
for (var face = 0; face < faceCount; face += 3) {
var x1 = this.TriangleIndices[face];
var y1 = this.TriangleIndices[face + 1];
var z1 = this.TriangleIndices[face + 2];
var x2 = this._getMiddlePoint(x1, y1);
var y2 = this._getMiddlePoint(y1, z1);
var z2 = this._getMiddlePoint(z1, x1);
this._addFace(x1, x2, z2);
this._addFace(y1, y2, x2);
this._addFace(z1, z2, y2);
this._addFace(x2, y2, z2);
}
}
};
Icosahedron3D.prototype._getMiddlePoint = function (p1, p2) {
var firstIsSmaller = p1 < p2;
var smallerIndex = firstIsSmaller ? p1 : p2;
var greaterIndex = firstIsSmaller ? p2 : p1;
var key = (smallerIndex << 32) + greaterIndex;
var p = this._middlePointIndexCache[key];
if (p !== undefined)
p;
var point1 = this.Points[p1];
var point2 = this.Points[p2];
var middle = {
x: (point1.x + point2.x) / 2.0,
y: (point1.y + point2.y) / 2.0,
z: (point1.z + point2.z) / 2.0,
};
var i = this._addVertex(middle.x, middle.y, middle.z);
this._middlePointIndexCache[key] = i;
return i;
};
return Icosahedron3D;
})();
(function () {
var app = new App(document.getElementById('canvas'));
app.Draw();
})();
<body style="background-color: rgb(55,55,55);">
<canvas id="canvas" style="position: absolute;top:0;left:0;width:100%;height:100%;" />
</body>