Related
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>
So the simplest solution seems to be the notion of moving the view instead of the object.
To do this, I keep the existing rotation axis information which is applied to the mov matrix unchanged, but added a new triple to provide rotation axis information to be applied to the view matrix:
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,
MovRotation: {
X: 0.00000,
Y: 0.01000,
Z: 0.00000
},
ViewRotation: {
X: 0.00100,
Y: 0.00000,
Z: 0.00000
}
};
}
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 movThetas = this._config.MovRotation;
var viewThetas = this._config.ViewRotation
var time_old = 0;
var zoomLevel_old = 0;
var execAnimation = function (time) {
var dt = time - time_old;
time_old = time;
for (var m_axis in movThetas) {
var theta = movThetas[m_axis];
if (theta > 0.0 || theta < 0.0) {
Matrix[("Rotate" + m_axis)](mov_matrix, dt * theta);
}
}
for (var v_axis in viewThetas) {
var theta = viewThetas[v_axis];
if (theta > 0.0 || theta < 0.0) {
Matrix[("Rotate" + v_axis)](view_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>
I know that in theory you have to first find the coordinates on the height map like (x = width HM / width Terrain * x Terrain) and y coordinate (y = height HM / height Terrain * y Terrain) and after getting the location on the height map, we get the actual height by min_height + (colorVal / (max_color - min_color) * *max_height - min_height) thus returning a Z value for a particular segment.
But how can i actually import the height map and take its parameters? I'm writing in javascript with no additional libraries (three,babylon).
edit
Currently i'm hardcoding the z values based on x and y ranges:
Plane.prototype.modifyGeometry=function(x,y){
if((x>=0&&x<100)&&(y>=0&&y<100)){
return 25;
}
else if((x>=100&&x<150)&&(y>=100&&y<150)){
return 20;
}
else if((x>=150&&x<200)&&(y>=150&&y<200)){
return 15;
}
else if((x>=200&&x<250)&&(y>=200&&y<250)){
return 10;
}
else if((x>=250&&x<300)&&(y>=250&&y<300)){
return 5;
}
else{
return 0;
}
** edit **
i can get a flat grid (or with randomly generated heights), but as soon as i add the image data, i get a blank screen(no errors though). Here is the code (i changed it up a bit):
var gl;
var canvas;
var img = new Image();
// img.onload = run;
img.crossOrigin = 'anonymous';
img.src = 'https://threejsfundamentals.org/threejs/resources/images/heightmap-96x64.png';
var gridWidth;
var gridDepth;
var gridPoints = [];
var gridIndices = [];
var rowOff = 0;
var rowStride = gridWidth + 1;
var numVertices = (gridWidth * 2 * (gridDepth + 1)) + (gridDepth * 2 * (gridWidth + 1));
//creating plane
function generateHeightPoints() {
var ctx = document.createElement("canvas").getContext("2d"); //using 2d canvas to read image
ctx.canvas.width = img.width;
ctx.canvas.height = img.height;
ctx.drawImage(img, 0, 0);
var imgData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
gridWidth = imgData.width - 1;
gridDepth = imgData.height - 1;
for (var z = 0; z <= gridDepth; ++z) {
for (var x = 0; x <= gridWidth; ++x) {
var offset = (z * imgData.width + x) * 4;
var height = imgData.data[offset] * 10 / 255;
gridPoints.push(x, height, z);
}
}
}
function generateIndices() {
for (var z = 0; z<=gridDepth; ++z) {
rowOff = z*rowStride;
for(var x = 0; x<gridWidth; ++x) {
gridIndices.push(rowOff+x,rowOff+x+1);
}
}
for (var x = 0; x<=gridWidth; ++x) {
for(var z = 0; z<gridDepth; ++z) {
rowOff = z * rowStride;
gridIndices.push(rowOff+x,rowOff+x+rowStride);
}
}
}
//init
//program init
window.onload = function init()
{
canvas = document.getElementById( "gl-canvas" );
gl = WebGLUtils.setupWebGL( canvas );
if ( !gl ) { alert( "WebGL isn't available" ); }
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
var program = initShaders( gl, "vertex-shader", "fragment-shader" );
gl.useProgram( program );
generateHeightPoints();
generateIndices();
var positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(gridPoints),
gl.STATIC_DRAW);
var indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(gridIndices),
gl.STATIC_DRAW);
var vPosition = gl.getAttribLocation( program, "vPosition" );
gl.vertexAttribPointer( vPosition, 3, gl.FLOAT, false, 0, 0 );
gl.enableVertexAttribArray( vPosition );
var matrixLoc = gl.getUniformLocation(program, 'matrix');
var m4 = twgl.m4;
var projection = m4.perspective(60 * Math.PI / 180, gl.canvas.clientWidth /
gl.canvas.clientHeight, 0.1, 100);
var cameraPosition = [-18, 15, -10];
var target = [gridWidth / 2, -10, gridDepth / 2];
var up = [0, 1, 0];
var camera = m4.lookAt(cameraPosition, target, up);
var view = m4.inverse(camera);
var mat = m4.multiply(projection, view);
gl.uniformMatrix4fv(matrixLoc, false, mat);
render();
}
function render() {
gl.drawElements(gl.LINES, numVertices, gl.UNSIGNED_SHORT, 0);
gl.drawElements(gl.LINES,numVertices,gl.UNSIGNED_SHORT,0);
requestAnimFrame(render);
}
You basically just make a grid of points and change the Z values.
First a flat grid
const gl = document.querySelector('canvas').getContext('webgl');
const vs = `
attribute vec4 position;
uniform mat4 matrix;
void main() {
gl_Position = matrix * position;
}
`;
const fs = `
precision highp float;
void main() {
gl_FragColor = vec4(0, 1, 0, 1);
}
`;
const program = twgl.createProgram(gl, [vs, fs]);
const positionLoc = gl.getAttribLocation(program, 'position');
const matrixLoc = gl.getUniformLocation(program, 'matrix');
const gridWidth = 40;
const gridDepth = 40;
const gridPoints = [];
for (let z = 0; z <= gridDepth; ++z) {
for (let x = 0; x <= gridWidth; ++x) {
gridPoints.push(x, 0, z);
}
}
const gridIndices = [];
const rowStride = gridWidth + 1;
// x lines
for (let z = 0; z <= gridDepth; ++z) {
const rowOff = z * rowStride;
for (let x = 0; x < gridWidth; ++x) {
gridIndices.push(rowOff + x, rowOff + x + 1);
}
}
// z lines
for (let x = 0; x <= gridWidth; ++x) {
for (let z = 0; z < gridDepth; ++z) {
const rowOff = z * rowStride;
gridIndices.push(rowOff + x, rowOff + x + rowStride);
}
}
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(gridPoints), gl.STATIC_DRAW);
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(gridIndices), gl.STATIC_DRAW);
twgl.resizeCanvasToDisplaySize(gl.canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
const m4 = twgl.m4;
const projection = m4.perspective(
60 * Math.PI / 180, // field of view
gl.canvas.clientWidth / gl.canvas.clientHeight, // aspect
0.1, // near
100, // far
);
const cameraPosition = [-gridWidth / 8, 10, -gridDepth / 8];
const target = [gridWidth / 2, -10, gridDepth / 2];
const up = [0, 1, 0];
const camera = m4.lookAt(cameraPosition, target, up);
const view = m4.inverse(camera);
const mat = m4.multiply(projection, view);
gl.enableVertexAttribArray(positionLoc);
gl.vertexAttribPointer(
positionLoc, // location
3, // size
gl.FLOAT, // type
false, // normalize
0, // stride
0, // offset
);
gl.useProgram(program);
gl.uniformMatrix4fv(matrixLoc, false, mat);
const numVertices = (gridWidth * 2 * (gridDepth + 1)) +
(gridDepth * 2 * (gridWidth + 1));
gl.drawElements(
gl.LINES, // primitive type
numVertices, //
gl.UNSIGNED_SHORT, // type of indices
0, // offset
);
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<canvas></canvas>
Grid with height map.
Here's a gray scale image we can use as a height map
Read it by loading a img, drawing to a 2D canvas, calling getImageData. Then just read the red values for height.
const img = new Image();
img.onload = run;
img.crossOrigin = 'anonymous';
img.src = 'https://threejsfundamentals.org/threejs/resources/images/heightmap-96x64.png';
function run() {
const gl = document.querySelector('canvas').getContext('webgl');
const vs = `
attribute vec4 position;
uniform mat4 matrix;
void main() {
gl_Position = matrix * position;
}
`;
const fs = `
precision highp float;
void main() {
gl_FragColor = vec4(0, 1, 0, 1);
}
`;
const program = twgl.createProgram(gl, [vs, fs]);
const positionLoc = gl.getAttribLocation(program, 'position');
const matrixLoc = gl.getUniformLocation(program, 'matrix');
// use a canvas 2D to read the image
const ctx = document.createElement('canvas').getContext('2d');
ctx.canvas.width = img.width;
ctx.canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const imgData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
const gridWidth = imgData.width - 1;
const gridDepth = imgData.height - 1;
const gridPoints = [];
for (let z = 0; z <= gridDepth; ++z) {
for (let x = 0; x <= gridWidth; ++x) {
const offset = (z * imgData.width + x) * 4;
// height 0 to 10
const height = imgData.data[offset] * 10 / 255;
gridPoints.push(x, height, z);
}
}
const gridIndices = [];
const rowStride = gridWidth + 1;
// x lines
for (let z = 0; z <= gridDepth; ++z) {
const rowOff = z * rowStride;
for (let x = 0; x < gridWidth; ++x) {
gridIndices.push(rowOff + x, rowOff + x + 1);
}
}
// z lines
for (let x = 0; x <= gridWidth; ++x) {
for (let z = 0; z < gridDepth; ++z) {
const rowOff = z * rowStride;
gridIndices.push(rowOff + x, rowOff + x + rowStride);
}
}
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(gridPoints), gl.STATIC_DRAW);
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(gridIndices), gl.STATIC_DRAW);
twgl.resizeCanvasToDisplaySize(gl.canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
const m4 = twgl.m4;
const projection = m4.perspective(
60 * Math.PI / 180, // field of view
gl.canvas.clientWidth / gl.canvas.clientHeight, // aspect
0.1, // near
100, // far
);
const cameraPosition = [-10, 10, -10];
const target = [gridWidth / 2, -10, gridDepth / 2];
const up = [0, 1, 0];
const camera = m4.lookAt(cameraPosition, target, up);
const view = m4.inverse(camera);
const mat = m4.multiply(projection, view);
gl.enableVertexAttribArray(positionLoc);
gl.vertexAttribPointer(
positionLoc, // location
3, // size
gl.FLOAT, // type
false, // normalize
0, // stride
0, // offset
);
gl.useProgram(program);
gl.uniformMatrix4fv(matrixLoc, false, mat);
const numVertices = (gridWidth * 2 * (gridDepth + 1)) +
(gridDepth * 2 * (gridWidth + 1));
gl.drawElements(
gl.LINES, // primitive type
numVertices, //
gl.UNSIGNED_SHORT, // type of indices
0, // offset
);
}
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<canvas></canvas>
Then instead of making a grid of lines make a grid of triangles. There's lots of ways to do that. You could put 2 triangle per grid square. This code put's 4. You also need to generate normals. I copied the code to generate normals from this article which is fairly generic normal generating code. Being a grid you could make a grid specific normal generator which would be faster since being a grid you know which vertices are shared.
This code is also using twgl because WebGL is too verbose but it should be clear how to do it in plain WebGL from reading the names of the twgl functions.
'use strict';
/* global twgl, m4, requestAnimationFrame, document */
const img = new Image();
img.onload = run;
img.crossOrigin = 'anonymous';
img.src = 'https://threejsfundamentals.org/threejs/resources/images/heightmap-96x64.png';
function run() {
// use a canvas 2D to read the image
const ctx = document.createElement('canvas').getContext('2d');
ctx.canvas.width = img.width;
ctx.canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const imgData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
function getHeight(offset) {
const v = imgData.data[offset * 4]; // x4 because RGBA
return v * 10 / 255; // 0 to 10
}
// first generate a grid of triangles, at least 2 or 4 for each cell
//
// 2 4
// +----+ +----+
// | /| |\ /|
// | / | | \/ |
// | / | | /\ |
// |/ | |/ \|
// +----+ +----+
//
const positions = [];
const texcoords = [];
const indices = [];
const cellsAcross = imgData.width - 1;
const cellsDeep = imgData.height - 1;
for (let z = 0; z < cellsDeep; ++z) {
for (let x = 0; x < cellsAcross; ++x) {
const base0 = z * imgData.width + x;
const base1 = base0 + imgData.width;
const h00 = getHeight(base0); const h01 = getHeight(base0 + 1);
const h10 = getHeight(base1);
const h11 = getHeight(base1 + 1);
const hm = (h00 + h01 + h10 + h11) / 4;
const x0 = x;
const x1 = x + 1;
const z0 = z;
const z1 = z + 1;
const ndx = positions.length / 3;
positions.push(
x0, h00, z0,
x1, h01, z0,
x0, h10, z1,
x1, h11, z1,
(x0 + x1) / 2, hm, (z0 + z1) / 2,
);
const u0 = x / cellsAcross;
const v0 = z / cellsDeep;
const u1 = (x + 1) / cellsAcross;
const v1 = (z + 1) / cellsDeep;
texcoords.push(
u0, v0,
u1, v0,
u0, v1,
u1, v1,
(u0 + u1) / 2, (v0 + v1) / 2,
);
//
// 0----1
// |\ /|
// | \/4|
// | /\ |
// |/ \|
// 2----3
indices.push(
ndx, ndx + 4, ndx + 1,
ndx, ndx + 2, ndx + 4,
ndx + 2, ndx + 3, ndx + 4,
ndx + 1, ndx + 4, ndx + 3,
);
}
}
const maxAngle = 2 * Math.PI / 180; // make them facetted
const arrays = generateNormals({
position: positions,
texcoord: texcoords,
indices,
}, maxAngle);
const gl = document.querySelector('canvas').getContext('webgl');
const vs = `
attribute vec4 position;
attribute vec3 normal;
attribute vec2 texcoord;
uniform mat4 projection;
uniform mat4 modelView;
varying vec3 v_normal;
varying vec2 v_texcoord;
void main() {
gl_Position = projection * modelView * position;
v_normal = mat3(modelView) * normal;
v_texcoord = texcoord;
}
`;
const fs = `
precision highp float;
varying vec3 v_normal;
varying vec2 v_texcoord;
varying float v_modelId;
void main() {
vec3 lightDirection = normalize(vec3(1, 2, -3)); // arbitrary light direction
float l = dot(lightDirection, normalize(v_normal)) * .5 + .5;
gl_FragColor = vec4(vec3(0,1,0) * l, 1);
}
`;
// compile shader, link, look up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
// make some vertex data
// calls gl.createBuffer, gl.bindBuffer, gl.bufferData for each array
const bufferInfo = twgl.createBufferInfoFromArrays(gl, arrays);
function render(time) {
time *= 0.001; // seconds
twgl.resizeCanvasToDisplaySize(gl.canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.CULL_FACE);
const fov = Math.PI * 0.25;
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const near = 0.1;
const far = 100;
const projection = m4.perspective(fov, aspect, near, far);
const eye = [0, 10, 25];
const target = [0, 0, 0];
const up = [0, 1, 0];
const camera = m4.lookAt(eye, target, up);
const view = m4.inverse(camera);
let modelView = m4.yRotate(view, time);
modelView = m4.translate(modelView, imgData.width / -2, 0, imgData.height / -2)
gl.useProgram(programInfo.program);
// calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
// calls gl.activeTexture, gl.bindTexture, gl.uniformXXX
twgl.setUniforms(programInfo, {
projection,
modelView,
});
// calls gl.drawArrays or gl.drawElements
twgl.drawBufferInfo(gl, bufferInfo);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
function generateNormals(arrays, maxAngle) {
const positions = arrays.position;
const texcoords = arrays.texcoord;
// first compute the normal of each face
let getNextIndex = makeIndiceIterator(arrays);
const numFaceVerts = getNextIndex.numElements;
const numVerts = arrays.position.length;
const numFaces = numFaceVerts / 3;
const faceNormals = [];
// Compute the normal for every face.
// While doing that, create a new vertex for every face vertex
for (let i = 0; i < numFaces; ++i) {
const n1 = getNextIndex() * 3;
const n2 = getNextIndex() * 3;
const n3 = getNextIndex() * 3;
const v1 = positions.slice(n1, n1 + 3);
const v2 = positions.slice(n2, n2 + 3);
const v3 = positions.slice(n3, n3 + 3);
faceNormals.push(m4.normalize(m4.cross(m4.subtractVectors(v1, v2), m4.subtractVectors(v3, v2))));
}
let tempVerts = {};
let tempVertNdx = 0;
// this assumes vertex positions are an exact match
function getVertIndex(x, y, z) {
const vertId = x + "," + y + "," + z;
const ndx = tempVerts[vertId];
if (ndx !== undefined) {
return ndx;
}
const newNdx = tempVertNdx++;
tempVerts[vertId] = newNdx;
return newNdx;
}
// We need to figure out the shared vertices.
// It's not as simple as looking at the faces (triangles)
// because for example if we have a standard cylinder
//
//
// 3-4
// / \
// 2 5 Looking down a cylinder starting at S
// | | and going around to E, E and S are not
// 1 6 the same vertex in the data we have
// \ / as they don't share UV coords.
// S/E
//
// the vertices at the start and end do not share vertices
// since they have different UVs but if you don't consider
// them to share vertices they will get the wrong normals
const vertIndices = [];
for (let i = 0; i < numVerts; ++i) {
const offset = i * 3;
const vert = positions.slice(offset, offset + 3);
vertIndices.push(getVertIndex(vert));
}
// go through every vertex and record which faces it's on
const vertFaces = [];
getNextIndex.reset();
for (let i = 0; i < numFaces; ++i) {
for (let j = 0; j < 3; ++j) {
const ndx = getNextIndex();
const sharedNdx = vertIndices[ndx];
let faces = vertFaces[sharedNdx];
if (!faces) {
faces = [];
vertFaces[sharedNdx] = faces;
}
faces.push(i);
}
}
// now go through every face and compute the normals for each
// vertex of the face. Only include faces that aren't more than
// maxAngle different. Add the result to arrays of newPositions,
// newTexcoords and newNormals, discarding any vertices that
// are the same.
tempVerts = {};
tempVertNdx = 0;
const newPositions = [];
const newTexcoords = [];
const newNormals = [];
function getNewVertIndex(x, y, z, nx, ny, nz, u, v) {
const vertId =
x + "," + y + "," + z + "," +
nx + "," + ny + "," + nz + "," +
u + "," + v;
const ndx = tempVerts[vertId];
if (ndx !== undefined) {
return ndx;
}
const newNdx = tempVertNdx++;
tempVerts[vertId] = newNdx;
newPositions.push(x, y, z);
newNormals.push(nx, ny, nz);
newTexcoords.push(u, v);
return newNdx;
}
const newVertIndices = [];
getNextIndex.reset();
const maxAngleCos = Math.cos(maxAngle);
// for each face
for (let i = 0; i < numFaces; ++i) {
// get the normal for this face
const thisFaceNormal = faceNormals[i];
// for each vertex on the face
for (let j = 0; j < 3; ++j) {
const ndx = getNextIndex();
const sharedNdx = vertIndices[ndx];
const faces = vertFaces[sharedNdx];
const norm = [0, 0, 0];
faces.forEach(faceNdx => {
// is this face facing the same way
const otherFaceNormal = faceNormals[faceNdx];
const dot = m4.dot(thisFaceNormal, otherFaceNormal);
if (dot > maxAngleCos) {
m4.addVectors(norm, otherFaceNormal, norm);
}
});
m4.normalize(norm, norm);
const poffset = ndx * 3;
const toffset = ndx * 2;
newVertIndices.push(getNewVertIndex(
positions[poffset + 0], positions[poffset + 1], positions[poffset + 2],
norm[0], norm[1], norm[2],
texcoords[toffset + 0], texcoords[toffset + 1]));
}
}
return {
position: newPositions,
texcoord: newTexcoords,
normal: newNormals,
indices: newVertIndices,
};
}
function makeIndexedIndicesFn(arrays) {
const indices = arrays.indices;
let ndx = 0;
const fn = function() {
return indices[ndx++];
};
fn.reset = function() {
ndx = 0;
};
fn.numElements = indices.length;
return fn;
}
function makeUnindexedIndicesFn(arrays) {
let ndx = 0;
const fn = function() {
return ndx++;
};
fn.reset = function() {
ndx = 0;
}
fn.numElements = arrays.positions.length / 3;
return fn;
}
function makeIndiceIterator(arrays) {
return arrays.indices
? makeIndexedIndicesFn(arrays)
: makeUnindexedIndicesFn(arrays);
}
}
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<script src="https://webglfundamentals.org/webgl/resources/3d-math.js"></script>
<canvas></canvas>
I would like to have more than one Three.js transition in one page but it only displays the second instance. I know it is possible because I have read some other questions on the subject but I'm very new to Three.js and I still don't understand how I would do it in this specific instance. Thanks in advance!
Codepen here:
https://codepen.io/mastroneel/pen/pQqWKd
window.onload = init;
console.ward = function() {}; // what warnings?
function init() {
var root = new THREERoot({
createCameraControls: !true,
antialias: (window.devicePixelRatio === 1),
fov: 80
});
root.renderer.setClearColor(0x000000, 0);
root.renderer.setPixelRatio(window.devicePixelRatio || 1);
root.camera.position.set(0, 0, 60);
var width = 100;
var height = 100;
var slide = new Slide(width, height, 'out');
var l1 = new THREE.ImageLoader();
l1.setCrossOrigin('Anonymous');
slide.setImage(l1.load('https://image.ibb.co/f6mVsA/helmet.png'));
root.scene.add(slide);
var slide2 = new Slide(width, height, 'in');
var l2 = new THREE.ImageLoader();
l2.setCrossOrigin('Anonymous');
slide2.setImage(l2.load('https://image.ibb.co/mb1KkV/player.png'));
root.scene.add(slide2);
var tl = new TimelineMax({repeat:-1, repeatDelay:1.0, yoyo: true});
tl.add(slide.transition(), 0);
tl.add(slide2.transition(), 0);
createTweenScrubber(tl);
window.addEventListener('keyup', function(e) {
if (e.keyCode === 80) {
tl.paused(!tl.paused());
}
});
}
////////////////////
// CLASSES
////////////////////
function Slide(width, height, animationPhase) {
var plane = new THREE.PlaneGeometry(width, height, width * 2, height * 2);
THREE.BAS.Utils.separateFaces(plane);
var geometry = new SlideGeometry(plane);
geometry.bufferUVs();
var aAnimation = geometry.createAttribute('aAnimation', 2);
var aStartPosition = geometry.createAttribute('aStartPosition', 3);
var aControl0 = geometry.createAttribute('aControl0', 3);
var aControl1 = geometry.createAttribute('aControl1', 3);
var aEndPosition = geometry.createAttribute('aEndPosition', 3);
var i, i2, i3, i4, v;
var minDuration = 0.8;
var maxDuration = 1.2;
var maxDelayX = 0.9;
var maxDelayY = 0.125;
var stretch = 0.11;
this.totalDuration = maxDuration + maxDelayX + maxDelayY + stretch;
var startPosition = new THREE.Vector3();
var control0 = new THREE.Vector3();
var control1 = new THREE.Vector3();
var endPosition = new THREE.Vector3();
var tempPoint = new THREE.Vector3();
function getControlPoint0(centroid) {
var signY = Math.sign(centroid.y);
tempPoint.x = THREE.Math.randFloat(0.1, 0.3) * 50;
tempPoint.y = signY * THREE.Math.randFloat(0.1, 0.3) * 70;
tempPoint.z = THREE.Math.randFloatSpread(20);
return tempPoint;
}
function getControlPoint1(centroid) {
var signY = Math.sign(centroid.y);
tempPoint.x = THREE.Math.randFloat(0.3, 0.6) * 50;
tempPoint.y = -signY * THREE.Math.randFloat(0.3, 0.6) * 70;
tempPoint.z = THREE.Math.randFloatSpread(20);
return tempPoint;
}
for (i = 0, i2 = 0, i3 = 0, i4 = 0; i < geometry.faceCount; i++, i2 += 6, i3 += 9, i4 += 12) {
var face = plane.faces[i];
var centroid = THREE.BAS.Utils.computeCentroid(plane, face);
// animation
var duration = THREE.Math.randFloat(minDuration, maxDuration);
var delayX = THREE.Math.mapLinear(centroid.x, -width * 0.5, width * 0.5, 0.0, maxDelayX);
var delayY;
if (animationPhase === 'in') {
delayY = THREE.Math.mapLinear(Math.abs(centroid.y), 0, height * 0.5, 0.0, maxDelayY)
}
else {
delayY = THREE.Math.mapLinear(Math.abs(centroid.y), 0, height * 0.5, maxDelayY, 0.0)
}
for (v = 0; v < 6; v += 2) {
aAnimation.array[i2 + v] = delayX + delayY + (Math.random() * stretch * duration);
aAnimation.array[i2 + v + 1] = duration;
}
// positions
endPosition.copy(centroid);
startPosition.copy(centroid);
if (animationPhase === 'in') {
control0.copy(centroid).sub(getControlPoint0(centroid));
control1.copy(centroid).sub(getControlPoint1(centroid));
}
else { // out
control0.copy(centroid).add(getControlPoint0(centroid));
control1.copy(centroid).add(getControlPoint1(centroid));
}
for (v = 0; v < 9; v += 3) {
aStartPosition.array[i3 + v] = startPosition.x;
aStartPosition.array[i3 + v + 1] = startPosition.y;
aStartPosition.array[i3 + v + 2] = startPosition.z;
aControl0.array[i3 + v] = control0.x;
aControl0.array[i3 + v + 1] = control0.y;
aControl0.array[i3 + v + 2] = control0.z;
aControl1.array[i3 + v] = control1.x;
aControl1.array[i3 + v + 1] = control1.y;
aControl1.array[i3 + v + 2] = control1.z;
aEndPosition.array[i3 + v] = endPosition.x;
aEndPosition.array[i3 + v + 1] = endPosition.y;
aEndPosition.array[i3 + v + 2] = endPosition.z;
}
}
var material = new THREE.BAS.BasicAnimationMaterial(
{
shading: THREE.FlatShading,
side: THREE.DoubleSide,
uniforms: {
uTime: {type: 'f', value: 0}
},
shaderFunctions: [
THREE.BAS.ShaderChunk['cubic_bezier'],
//THREE.BAS.ShaderChunk[(animationPhase === 'in' ? 'ease_out_cubic' : 'ease_in_cubic')],
THREE.BAS.ShaderChunk['ease_in_out_cubic'],
THREE.BAS.ShaderChunk['quaternion_rotation']
],
shaderParameters: [
'uniform float uTime;',
'attribute vec2 aAnimation;',
'attribute vec3 aStartPosition;',
'attribute vec3 aControl0;',
'attribute vec3 aControl1;',
'attribute vec3 aEndPosition;',
],
shaderVertexInit: [
'float tDelay = aAnimation.x;',
'float tDuration = aAnimation.y;',
'float tTime = clamp(uTime - tDelay, 0.0, tDuration);',
'float tProgress = ease(tTime, 0.0, 1.0, tDuration);'
//'float tProgress = tTime / tDuration;'
],
shaderTransformPosition: [
(animationPhase === 'in' ? 'transformed *= tProgress;' : 'transformed *= 1.0 - tProgress;'),
'transformed += cubicBezier(aStartPosition, aControl0, aControl1, aEndPosition, tProgress);'
]
},
{
map: new THREE.Texture(),
}
);
THREE.Mesh.call(this, geometry, material);
this.frustumCulled = false;
}
Slide.prototype = Object.create(THREE.Mesh.prototype);
Slide.prototype.constructor = Slide;
Object.defineProperty(Slide.prototype, 'time', {
get: function () {
return this.material.uniforms['uTime'].value;
},
set: function (v) {
this.material.uniforms['uTime'].value = v;
}
});
Slide.prototype.setImage = function(image) {
this.material.uniforms.map.value.image = image;
this.material.uniforms.map.value.needsUpdate = true;
};
Slide.prototype.transition = function() {
return TweenMax.fromTo(this, 3.0, {time:0.0}, {time:this.totalDuration, ease:Power0.easeInOut});
};
function SlideGeometry(model) {
THREE.BAS.ModelBufferGeometry.call(this, model);
}
SlideGeometry.prototype = Object.create(THREE.BAS.ModelBufferGeometry.prototype);
SlideGeometry.prototype.constructor = SlideGeometry;
SlideGeometry.prototype.bufferPositions = function () {
var positionBuffer = this.createAttribute('position', 3).array;
for (var i = 0; i < this.faceCount; i++) {
var face = this.modelGeometry.faces[i];
var centroid = THREE.BAS.Utils.computeCentroid(this.modelGeometry, face);
var a = this.modelGeometry.vertices[face.a];
var b = this.modelGeometry.vertices[face.b];
var c = this.modelGeometry.vertices[face.c];
positionBuffer[face.a * 3] = a.x - centroid.x;
positionBuffer[face.a * 3 + 1] = a.y - centroid.y;
positionBuffer[face.a * 3 + 2] = a.z - centroid.z;
positionBuffer[face.b * 3] = b.x - centroid.x;
positionBuffer[face.b * 3 + 1] = b.y - centroid.y;
positionBuffer[face.b * 3 + 2] = b.z - centroid.z;
positionBuffer[face.c * 3] = c.x - centroid.x;
positionBuffer[face.c * 3 + 1] = c.y - centroid.y;
positionBuffer[face.c * 3 + 2] = c.z - centroid.z;
}
};
function THREERoot(params) {
params = utils.extend({
fov: 60,
zNear: 10,
zFar: 100000,
createCameraControls: true
}, params);
this.renderer = new THREE.WebGLRenderer({
antialias: params.antialias,
alpha: true
});
this.renderer.setPixelRatio(Math.min(2, window.devicePixelRatio || 1));
document.getElementById('three-container1').appendChild(this.renderer.domElement);
this.camera = new THREE.PerspectiveCamera(
params.fov,
window.innerWidth / window.innerHeight,
params.zNear,
params.zfar
);
this.scene = new THREE.Scene();
if (params.createCameraControls) {
this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
}
this.resize = this.resize.bind(this);
this.tick = this.tick.bind(this);
this.resize();
this.tick();
window.addEventListener('resize', this.resize, false);
}
THREERoot.prototype = {
tick: function () {
this.update();
this.render();
requestAnimationFrame(this.tick);
},
update: function () {
this.controls && this.controls.update();
},
render: function () {
this.renderer.render(this.scene, this.camera);
},
resize: function () {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
}
};
////////////////////
// UTILS
////////////////////
var utils = {
extend: function (dst, src) {
for (var key in src) {
dst[key] = src[key];
}
return dst;
},
randSign: function () {
return Math.random() > 0.5 ? 1 : -1;
},
ease: function (ease, t, b, c, d) {
return b + ease.getRatio(t / d) * c;
},
fibSpherePoint: (function () {
var vec = {x: 0, y: 0, z: 0};
var G = Math.PI * (3 - Math.sqrt(5));
return function (i, n, radius) {
var step = 2.0 / n;
var r, phi;
vec.y = i * step - 1 + (step * 0.5);
r = Math.sqrt(1 - vec.y * vec.y);
phi = i * G;
vec.x = Math.cos(phi) * r;
vec.z = Math.sin(phi) * r;
radius = radius || 1;
vec.x *= radius;
vec.y *= radius;
vec.z *= radius;
return vec;
}
})(),
spherePoint: (function () {
return function (u, v) {
u === undefined && (u = Math.random());
v === undefined && (v = Math.random());
var theta = 2 * Math.PI * u;
var phi = Math.acos(2 * v - 1);
var vec = {};
vec.x = (Math.sin(phi) * Math.cos(theta));
vec.y = (Math.sin(phi) * Math.sin(theta));
vec.z = (Math.cos(phi));
return vec;
}
})()
};
function createTweenScrubber(tween, seekSpeed) {
seekSpeed = seekSpeed || 0.001;
function stop() {
TweenMax.to(tween, 1, {timeScale:0});
}
function resume() {
TweenMax.to(tween, 1, {timeScale:1});
}
function seek(dx) {
var progress = tween.progress();
var p = THREE.Math.clamp((progress + (dx * seekSpeed)), 0, 1);
tween.progress(p);
}
var _cx = 0;
// desktop
var mouseDown = false;
document.body.style.cursor = 'pointer';
window.addEventListener('mousedown', function(e) {
mouseDown = true;
document.body.style.cursor = 'ew-resize';
_cx = e.clientX;
stop();
});
window.addEventListener('mouseup', function(e) {
mouseDown = false;
document.body.style.cursor = 'pointer';
resume();
});
window.addEventListener('mousemove', function(e) {
if (mouseDown === true) {
var cx = e.clientX;
var dx = cx - _cx;
_cx = cx;
seek(dx);
}
});
// mobile
window.addEventListener('touchstart', function(e) {
_cx = e.touches[0].clientX;
stop();
e.preventDefault();
});
window.addEventListener('touchend', function(e) {
resume();
e.preventDefault();
});
window.addEventListener('touchmove', function(e) {
var cx = e.touches[0].clientX;
var dx = cx - _cx;
_cx = cx;
seek(dx);
e.preventDefault();
});
}
window.onload = init;
console.ward = function() {}; // what warnings?
function init() {
var root = new THREERoot({
createCameraControls: !true,
antialias: (window.devicePixelRatio === 1),
fov: 80
});
root.renderer.setClearColor(0x000000, 0);
root.renderer.setPixelRatio(window.devicePixelRatio || 1);
root.camera.position.set(0, 0, 60);
var width = 100;
var height = 100;
var slide = new Slide(width, height, 'out');
var l1 = new THREE.ImageLoader();
l1.setCrossOrigin('Anonymous');
slide.setImage(l1.load('https://image.ibb.co/f6mVsA/helmet.png'));
root.scene.add(slide);
var slide2 = new Slide(width, height, 'in');
var l2 = new THREE.ImageLoader();
l2.setCrossOrigin('Anonymous');
slide2.setImage(l2.load('https://image.ibb.co/mb1KkV/player.png'));
root.scene.add(slide2);
var tl = new TimelineMax({repeat:-1, repeatDelay:1.0, yoyo: true});
tl.add(slide.transition(), 0);
tl.add(slide2.transition(), 0);
createTweenScrubber(tl);
window.addEventListener('keyup', function(e) {
if (e.keyCode === 80) {
tl.paused(!tl.paused());
}
});
}
////////////////////
// CLASSES
////////////////////
function Slide(width, height, animationPhase) {
var plane = new THREE.PlaneGeometry(width, height, width * 2, height * 2);
THREE.BAS.Utils.separateFaces(plane);
var geometry = new SlideGeometry(plane);
geometry.bufferUVs();
var aAnimation = geometry.createAttribute('aAnimation', 2);
var aStartPosition = geometry.createAttribute('aStartPosition', 3);
var aControl0 = geometry.createAttribute('aControl0', 3);
var aControl1 = geometry.createAttribute('aControl1', 3);
var aEndPosition = geometry.createAttribute('aEndPosition', 3);
var i, i2, i3, i4, v;
var minDuration = 0.8;
var maxDuration = 1.2;
var maxDelayX = 0.9;
var maxDelayY = 0.125;
var stretch = 0.11;
this.totalDuration = maxDuration + maxDelayX + maxDelayY + stretch;
var startPosition = new THREE.Vector3();
var control0 = new THREE.Vector3();
var control1 = new THREE.Vector3();
var endPosition = new THREE.Vector3();
var tempPoint = new THREE.Vector3();
function getControlPoint0(centroid) {
var signY = Math.sign(centroid.y);
tempPoint.x = THREE.Math.randFloat(0.1, 0.3) * 50;
tempPoint.y = signY * THREE.Math.randFloat(0.1, 0.3) * 70;
tempPoint.z = THREE.Math.randFloatSpread(20);
return tempPoint;
}
function getControlPoint1(centroid) {
var signY = Math.sign(centroid.y);
tempPoint.x = THREE.Math.randFloat(0.3, 0.6) * 50;
tempPoint.y = -signY * THREE.Math.randFloat(0.3, 0.6) * 70;
tempPoint.z = THREE.Math.randFloatSpread(20);
return tempPoint;
}
for (i = 0, i2 = 0, i3 = 0, i4 = 0; i < geometry.faceCount; i++, i2 += 6, i3 += 9, i4 += 12) {
var face = plane.faces[i];
var centroid = THREE.BAS.Utils.computeCentroid(plane, face);
// animation
var duration = THREE.Math.randFloat(minDuration, maxDuration);
var delayX = THREE.Math.mapLinear(centroid.x, -width * 0.5, width * 0.5, 0.0, maxDelayX);
var delayY;
if (animationPhase === 'in') {
delayY = THREE.Math.mapLinear(Math.abs(centroid.y), 0, height * 0.5, 0.0, maxDelayY)
}
else {
delayY = THREE.Math.mapLinear(Math.abs(centroid.y), 0, height * 0.5, maxDelayY, 0.0)
}
for (v = 0; v < 6; v += 2) {
aAnimation.array[i2 + v] = delayX + delayY + (Math.random() * stretch * duration);
aAnimation.array[i2 + v + 1] = duration;
}
// positions
endPosition.copy(centroid);
startPosition.copy(centroid);
if (animationPhase === 'in') {
control0.copy(centroid).sub(getControlPoint0(centroid));
control1.copy(centroid).sub(getControlPoint1(centroid));
}
else { // out
control0.copy(centroid).add(getControlPoint0(centroid));
control1.copy(centroid).add(getControlPoint1(centroid));
}
for (v = 0; v < 9; v += 3) {
aStartPosition.array[i3 + v] = startPosition.x;
aStartPosition.array[i3 + v + 1] = startPosition.y;
aStartPosition.array[i3 + v + 2] = startPosition.z;
aControl0.array[i3 + v] = control0.x;
aControl0.array[i3 + v + 1] = control0.y;
aControl0.array[i3 + v + 2] = control0.z;
aControl1.array[i3 + v] = control1.x;
aControl1.array[i3 + v + 1] = control1.y;
aControl1.array[i3 + v + 2] = control1.z;
aEndPosition.array[i3 + v] = endPosition.x;
aEndPosition.array[i3 + v + 1] = endPosition.y;
aEndPosition.array[i3 + v + 2] = endPosition.z;
}
}
var material = new THREE.BAS.BasicAnimationMaterial(
{
shading: THREE.FlatShading,
side: THREE.DoubleSide,
uniforms: {
uTime: {type: 'f', value: 0}
},
shaderFunctions: [
THREE.BAS.ShaderChunk['cubic_bezier'],
//THREE.BAS.ShaderChunk[(animationPhase === 'in' ? 'ease_out_cubic' : 'ease_in_cubic')],
THREE.BAS.ShaderChunk['ease_in_out_cubic'],
THREE.BAS.ShaderChunk['quaternion_rotation']
],
shaderParameters: [
'uniform float uTime;',
'attribute vec2 aAnimation;',
'attribute vec3 aStartPosition;',
'attribute vec3 aControl0;',
'attribute vec3 aControl1;',
'attribute vec3 aEndPosition;',
],
shaderVertexInit: [
'float tDelay = aAnimation.x;',
'float tDuration = aAnimation.y;',
'float tTime = clamp(uTime - tDelay, 0.0, tDuration);',
'float tProgress = ease(tTime, 0.0, 1.0, tDuration);'
//'float tProgress = tTime / tDuration;'
],
shaderTransformPosition: [
(animationPhase === 'in' ? 'transformed *= tProgress;' : 'transformed *= 1.0 - tProgress;'),
'transformed += cubicBezier(aStartPosition, aControl0, aControl1, aEndPosition, tProgress);'
]
},
{
map: new THREE.Texture(),
}
);
THREE.Mesh.call(this, geometry, material);
this.frustumCulled = false;
}
Slide.prototype = Object.create(THREE.Mesh.prototype);
Slide.prototype.constructor = Slide;
Object.defineProperty(Slide.prototype, 'time', {
get: function () {
return this.material.uniforms['uTime'].value;
},
set: function (v) {
this.material.uniforms['uTime'].value = v;
}
});
Slide.prototype.setImage = function(image) {
this.material.uniforms.map.value.image = image;
this.material.uniforms.map.value.needsUpdate = true;
};
Slide.prototype.transition = function() {
return TweenMax.fromTo(this, 3.0, {time:0.0}, {time:this.totalDuration, ease:Power0.easeInOut});
};
function SlideGeometry(model) {
THREE.BAS.ModelBufferGeometry.call(this, model);
}
SlideGeometry.prototype = Object.create(THREE.BAS.ModelBufferGeometry.prototype);
SlideGeometry.prototype.constructor = SlideGeometry;
SlideGeometry.prototype.bufferPositions = function () {
var positionBuffer = this.createAttribute('position', 3).array;
for (var i = 0; i < this.faceCount; i++) {
var face = this.modelGeometry.faces[i];
var centroid = THREE.BAS.Utils.computeCentroid(this.modelGeometry, face);
var a = this.modelGeometry.vertices[face.a];
var b = this.modelGeometry.vertices[face.b];
var c = this.modelGeometry.vertices[face.c];
positionBuffer[face.a * 3] = a.x - centroid.x;
positionBuffer[face.a * 3 + 1] = a.y - centroid.y;
positionBuffer[face.a * 3 + 2] = a.z - centroid.z;
positionBuffer[face.b * 3] = b.x - centroid.x;
positionBuffer[face.b * 3 + 1] = b.y - centroid.y;
positionBuffer[face.b * 3 + 2] = b.z - centroid.z;
positionBuffer[face.c * 3] = c.x - centroid.x;
positionBuffer[face.c * 3 + 1] = c.y - centroid.y;
positionBuffer[face.c * 3 + 2] = c.z - centroid.z;
}
};
function THREERoot(params) {
params = utils.extend({
fov: 60,
zNear: 10,
zFar: 100000,
createCameraControls: true
}, params);
this.renderer = new THREE.WebGLRenderer({
antialias: params.antialias,
alpha: true
});
this.renderer.setPixelRatio(Math.min(2, window.devicePixelRatio || 1));
document.getElementById('three-container2').appendChild(this.renderer.domElement);
this.camera = new THREE.PerspectiveCamera(
params.fov,
window.innerWidth / window.innerHeight,
params.zNear,
params.zfar
);
this.scene = new THREE.Scene();
if (params.createCameraControls) {
this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
}
this.resize = this.resize.bind(this);
this.tick = this.tick.bind(this);
this.resize();
this.tick();
window.addEventListener('resize', this.resize, false);
}
THREERoot.prototype = {
tick: function () {
this.update();
this.render();
requestAnimationFrame(this.tick);
},
update: function () {
this.controls && this.controls.update();
},
render: function () {
this.renderer.render(this.scene, this.camera);
},
resize: function () {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
}
};
////////////////////
// UTILS
////////////////////
var utils = {
extend: function (dst, src) {
for (var key in src) {
dst[key] = src[key];
}
return dst;
},
randSign: function () {
return Math.random() > 0.5 ? 1 : -1;
},
ease: function (ease, t, b, c, d) {
return b + ease.getRatio(t / d) * c;
},
fibSpherePoint: (function () {
var vec = {x: 0, y: 0, z: 0};
var G = Math.PI * (3 - Math.sqrt(5));
return function (i, n, radius) {
var step = 2.0 / n;
var r, phi;
vec.y = i * step - 1 + (step * 0.5);
r = Math.sqrt(1 - vec.y * vec.y);
phi = i * G;
vec.x = Math.cos(phi) * r;
vec.z = Math.sin(phi) * r;
radius = radius || 1;
vec.x *= radius;
vec.y *= radius;
vec.z *= radius;
return vec;
}
})(),
spherePoint: (function () {
return function (u, v) {
u === undefined && (u = Math.random());
v === undefined && (v = Math.random());
var theta = 2 * Math.PI * u;
var phi = Math.acos(2 * v - 1);
var vec = {};
vec.x = (Math.sin(phi) * Math.cos(theta));
vec.y = (Math.sin(phi) * Math.sin(theta));
vec.z = (Math.cos(phi));
return vec;
}
})()
};
function createTweenScrubber(tween, seekSpeed) {
seekSpeed = seekSpeed || 0.001;
function stop() {
TweenMax.to(tween, 1, {timeScale:0});
}
function resume() {
TweenMax.to(tween, 1, {timeScale:1});
}
function seek(dx) {
var progress = tween.progress();
var p = THREE.Math.clamp((progress + (dx * seekSpeed)), 0, 1);
tween.progress(p);
}
var _cx = 0;
// desktop
var mouseDown = false;
document.body.style.cursor = 'pointer';
window.addEventListener('mousedown', function(e) {
mouseDown = true;
document.body.style.cursor = 'ew-resize';
_cx = e.clientX;
stop();
});
window.addEventListener('mouseup', function(e) {
mouseDown = false;
document.body.style.cursor = 'pointer';
resume();
});
window.addEventListener('mousemove', function(e) {
if (mouseDown === true) {
var cx = e.clientX;
var dx = cx - _cx;
_cx = cx;
seek(dx);
}
});
// mobile
window.addEventListener('touchstart', function(e) {
_cx = e.touches[0].clientX;
stop();
e.preventDefault();
});
window.addEventListener('touchend', function(e) {
resume();
e.preventDefault();
});
window.addEventListener('touchmove', function(e) {
var cx = e.touches[0].clientX;
var dx = cx - _cx;
_cx = cx;
seek(dx);
e.preventDefault();
});
}
body {
margin: 0;
background: #fff;
}
canvas {
background: #fff;
}
<div id="three-container1"></div>
<div id="three-container2"></div>
I have been trying to learn filters in javascript, i have been following
https://www.html5rocks.com/en/tutorials/canvas/imagefilters/
this tutorial.
I came across some of code i don't get, can some body help me understanding these codes.
Filters.convolute = function(pixels, weights, opaque) {
var side = Math.round(Math.sqrt(weights.length));
var halfSide = Math.floor(side/2);
var src = pixels.data;
var sw = pixels.width;
var sh = pixels.height;
// pad output by the convolution matrix
var w = sw;
var h = sh;
var output = Filters.createImageData(w, h);
var dst = output.data;
// go through the destination image pixels
var alphaFac = opaque ? 1 : 0;
for (var y=0; y<h; y++) {
for (var x=0; x<w; x++) {
var sy = y;
var sx = x;
var dstOff = (y*w+x)*4;
// calculate the weighed sum of the source image pixels that
// fall under the convolution matrix
var r=0, g=0, b=0, a=0;
for (var cy=0; cy<side; cy++) {
for (var cx=0; cx<side; cx++) {
var scy = sy + cy - halfSide;
var scx = sx + cx - halfSide;
if (scy >= 0 && scy < sh && scx >= 0 && scx < sw) {
var srcOff = (scy*sw+scx)*4;
var wt = weights[cy*side+cx];
r += src[srcOff] * wt;
g += src[srcOff+1] * wt;
b += src[srcOff+2] * wt;
a += src[srcOff+3] * wt;
}
}
}
dst[dstOff] = r;
dst[dstOff+1] = g;
dst[dstOff+2] = b;
dst[dstOff+3] = a + alphaFac*(255-a);
}
}
return output;
};
what is side and halfSide and why 4 for nested loop is used for. i am stuck here like many days.
I do, like you the same thing, I am trying to implement convolution filters using Javascript - TypeScript.
The reason why is 4 is because we have r, g, b, a
where r = red
where g = green
where b = blue
where a = alpha
this image data is inside an array of type Uint8ClampedArray
you get this information with this way:
const width = canvas.width;
const height = canvas.height;
const imageData = ctx.getImageData(0, 0, width, height);
and then to get the real image data:
const pixels = imageData.data;
The pixel data is a type of Uint8ClampedArray and can be represented like this:
[r, g, b, a, r, g, b, a, r, g, b, a ]
and every 4 elements in the array you get the pixel index and every 1,5 times you get the kernel Center but this depends on the kernel size 3x3 or 9x9
const image = imageData.data
The only code is working for me is this.
init() {
const img = new Image();
const img2 = new Image();
img.src = '../../../assets/graffiti.jpg';
img2.src = '../../../assets/graffiti.jpg';
const canvas: HTMLCanvasElement = this.canvas1.nativeElement;
const canvas2: HTMLCanvasElement = this.canvas2.nativeElement;
const ctx: CanvasRenderingContext2D = canvas.getContext('2d');
const ctx2: CanvasRenderingContext2D = canvas2.getContext('2d');
this.onImgLoad(img, ctx, canvas.width, canvas.height);
this.input(img2, ctx2, canvas2.width, canvas2.height);
}
input(img, ctx: CanvasRenderingContext2D, width, height) {
img.onload = () => {
ctx.drawImage(img, 0, 0);
};
}
onImgLoad(img, ctx: CanvasRenderingContext2D, width, height) {
img.onload = () => {
ctx.drawImage(img, 0, 0);
const kernelArr = new Kernel([
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
]);
const kernel = [
0, 1, 0,
0, 1, 0,
0, 1, 0
];
console.log(kernel);
const newImg = new Filter2D(ctx, width, height);
// const imgData = newImg.inverse(width, height); // applys inverse filter
const imgData = newImg.applyKernel(kernel);
ctx.putImageData(imgData, 0, 0);
};
}
class Filter2D {
width: number;
height: number;
ctx: CanvasRenderingContext2D;
imgData: ImageData;
constructor(ctx: CanvasRenderingContext2D, width: number, height: number) {
this.width = width;
this.height = height;
this.ctx = ctx;
this.imgData = ctx.getImageData(0, 0, width, height);
console.log(this.imgData);
}
grey(width: number, height: number): ImageData {
return this.imgData;
}
inverse(width: number, height: number): ImageData {
console.log('Width: ', width);
console.log('Height: ', height);
const pixels = this.imgData.data;
for (let i = 0; i < pixels.length; i += 4) {
pixels[i] = 255 - pixels[i]; // red
pixels[i + 1] = 255 - pixels[i + 1]; // green
pixels[i + 2] = 255 - pixels[i + 2]; // blue
}
return this.imgData;
}
applyKernel(kernel: any[]): ImageData {
const k1: number[] = [
1, 0, -1,
2, 0, -2,
1, 0, -1
];
const k2: number[] = [
-1, -1, -1,
-1, 8, -1,
-1, -1, -1
];
kernel = k2;
const dim = Math.sqrt(kernel.length);
const pad = Math.floor(dim / 2);
const pixels: Uint8ClampedArray = this.imgData.data;
const width: number = this.imgData.width;
const height: number = this.imgData.height;
console.log(this.imgData);
console.log('applyKernelMethod start');
console.log('Width: ', width);
console.log('Height: ', height);
console.log('kernel: ', kernel);
console.log('dim: ', dim); // 3
console.log('pad: ', pad); // 1
console.log('dim % 2: ', dim % 2); // 1
console.log('pixels: ', pixels);
if (dim % 2 !== 1) {
console.log('Invalid kernel dimension');
}
let pix, i, r, g, b;
const w = width;
const h = height;
const cw = w + pad * 2; // add padding
const ch = h + pad * 2;
for (let row = 0; row < height; row++) {
for (let col = 0; col < width; col++) {
r = 0;
g = 0;
b = 0;
for (let kx = -pad; kx <= pad; kx++) {
for (let ky = -pad; ky <= pad; ky++) {
i = (ky + pad) * dim + (kx + pad); // kernel index
pix = 4 * ((row + ky) * cw + (col + kx)); // image index
r += pixels[pix++] * kernel[i];
g += pixels[pix++] * kernel[i];
b += pixels[pix ] * kernel[i];
}
}
pix = 4 * ((row - pad) * w + (col - pad)); // destination index
pixels[pix++] = r;
pixels[pix++] = g;
pixels[pix++] = b;
pixels[pix ] = 255; // we want opaque image
}
}
console.log(pixels);
return this.imgData;
}
}
I wanna create my own filter for HTML Canvas. Code Below. I get error:
Failed to execute 'putImageData' on 'CanvasRenderingContext2D': parameter 1 is not of type 'ImageData'
But if I change return outputData; at end of filter(input, values){} with
for (let i = 0; i < outputData.length; i++){
inputData[i] = outputData[i];
}
Everything works fine, but I wanna, that filter(input, values){} returns value. How I can do that?
//////////////////////////////
// Distortion Filter //
// Originally by JoelBesada //
//////////////////////////////
class FilterDistortion {
constructor() {
this.name = 'Distortion';
this.defaultValues = {
size: 4,
density: 0.5,
mix: 0.5,
};
this.valueRanges = {
size: { min: 1, max: 10 },
density: { min: 0.0, max: 1.0 },
mix: { min: 0.0, max: 1.0 },
};
}
filter(input, values = this.defaultValues) {
const { width, height } = input;
const inputData = input.data;
const outputData = inputData.slice();
let size = (values.size === undefined)
? this.defaultValues.size
: parseInt(values.size, 10);
if (size < 1) size = 1;
const density = (values.density === undefined)
? this.defaultValues.density
: Number(values.density);
const mix = (values.mix === undefined)
? this.defaultValues.mix
: Number(values.mix);
const radius = size + 1;
const numShapes = parseInt(((((2 * density) / 30) * width) * height) / 2, 10);
for (let i = 0; i < numShapes; i++) {
const sx = (Math.random() * (2 ** 32) & 0x7fffffff) % width;
const sy = (Math.random() * (2 ** 32) & 0x7fffffff) % height;
const rgb2 = [
inputData[(((sy * width) + sx) * 4) + 0],
inputData[(((sy * width) + sx) * 4) + 1],
inputData[(((sy * width) + sx) * 4) + 2],
inputData[(((sy * width) + sx) * 4) + 3],
];
for (let x = sx - radius; x < sx + radius + 1; x++) {
for (let y = sy - radius; y < sy + radius + 1; y++) {
if (x >= 0 && x < width && y >= 0 && y < height) {
const rgb1 = [
outputData[(((y * width) + x) * 4) + 0],
outputData[(((y * width) + x) * 4) + 1],
outputData[(((y * width) + x) * 4) + 2],
outputData[(((y * width) + x) * 4) + 3],
];
const mixedRGB = this.mixColors(mix, rgb1, rgb2);
for (let k = 0; k < 3; k++) {
outputData[(((y * width) + x) * 4) + k] = mixedRGB[k];
}
}
}
}
}
return outputData;
}
linearInterpolate(t, a, b) {
return a + (t * (b - a));
}
mixColors(t, rgb1, rgb2) {
const r = this.linearInterpolate(t, rgb1[0], rgb2[0]);
const g = this.linearInterpolate(t, rgb1[1], rgb2[1]);
const b = this.linearInterpolate(t, rgb1[2], rgb2[2]);
const a = this.linearInterpolate(t, rgb1[3], rgb2[3]);
return [r, g, b, a];
}
}
/////////////////
// Driver Code //
/////////////////
var img = new Image(); img.onload = draw; img.src = "//i.imgur.com/Kzz84cr.png";
function draw() {
c.width = this.width; c.height = this.height;
var ctx = c.getContext("2d");
// main loop
ctx.drawImage(this, 0, 0); // draw video frame
var data = ctx.getImageData(0, 0, c.width, c.height);
ctx.putImageData(new FilterDistortion().filter(data), 0, 0);
ctx.globalCompositeOperation = "source-over"; // "reset"
// rinse, repeat
}
<canvas id=c></canvas>
The error message says it all, you need to pass an ImageData as the first param of putImageData.
Here you are passing an UInt8ClampedArray.
So you would have to either create a new ImageData from this TypedArray (when the ImageData constructor is available), or to create an empty ImageData from your canvas context and then set all its .data's values to the new values.
//////////////////////////////
// Distortion Filter //
// Originally by JoelBesada //
//////////////////////////////
class FilterDistortion {
constructor() {
this.name = 'Distortion';
this.defaultValues = {
size: 4,
density: 0.5,
mix: 0.5,
};
this.valueRanges = {
size: { min: 1, max: 10 },
density: { min: 0.0, max: 1.0 },
mix: { min: 0.0, max: 1.0 },
};
}
filter(input, values = this.defaultValues) {
const { width, height } = input;
const inputData = input.data;
const outputData = inputData.slice();
let size = (values.size === undefined)
? this.defaultValues.size
: parseInt(values.size, 10);
if (size < 1) size = 1;
const density = (values.density === undefined)
? this.defaultValues.density
: Number(values.density);
const mix = (values.mix === undefined)
? this.defaultValues.mix
: Number(values.mix);
const radius = size + 1;
const numShapes = parseInt(((((2 * density) / 30) * width) * height) / 2, 10);
for (let i = 0; i < numShapes; i++) {
const sx = (Math.random() * (2 ** 32) & 0x7fffffff) % width;
const sy = (Math.random() * (2 ** 32) & 0x7fffffff) % height;
const rgb2 = [
inputData[(((sy * width) + sx) * 4) + 0],
inputData[(((sy * width) + sx) * 4) + 1],
inputData[(((sy * width) + sx) * 4) + 2],
inputData[(((sy * width) + sx) * 4) + 3],
];
for (let x = sx - radius; x < sx + radius + 1; x++) {
for (let y = sy - radius; y < sy + radius + 1; y++) {
if (x >= 0 && x < width && y >= 0 && y < height) {
const rgb1 = [
outputData[(((y * width) + x) * 4) + 0],
outputData[(((y * width) + x) * 4) + 1],
outputData[(((y * width) + x) * 4) + 2],
outputData[(((y * width) + x) * 4) + 3],
];
const mixedRGB = this.mixColors(mix, rgb1, rgb2);
for (let k = 0; k < 3; k++) {
outputData[(((y * width) + x) * 4) + k] = mixedRGB[k];
}
}
}
}
}
return outputData;
}
linearInterpolate(t, a, b) {
return a + (t * (b - a));
}
mixColors(t, rgb1, rgb2) {
const r = this.linearInterpolate(t, rgb1[0], rgb2[0]);
const g = this.linearInterpolate(t, rgb1[1], rgb2[1]);
const b = this.linearInterpolate(t, rgb1[2], rgb2[2]);
const a = this.linearInterpolate(t, rgb1[3], rgb2[3]);
return [r, g, b, a];
}
}
/////////////////
// Driver Code //
/////////////////
var img = new Image(); img.crossOrigin=1; img.onload = draw; img.src = "//i.imgur.com/Kzz84cr.png";
function draw() {
c.width = this.width; c.height = this.height;
var ctx = c.getContext("2d");
// main loop
ctx.drawImage(this, 0, 0); // draw video frame
var data = ctx.getImageData(0, 0, c.width, c.height);
var distortedData = new FilterDistortion().filter(data);
var distortedImg;
try{
distortedImg = new window.ImageData(distortedData, c.width, c.height);
}
catch(e) {
distortedImg = ctx.createImageData(c.width, c.height);
distortedImg.data.set(distortedData);
}
ctx.putImageData(distortedImg, 0, 0);
ctx.globalCompositeOperation = "source-over"; // "reset"
// rinse, repeat
}
<canvas id=c></canvas>