I am developing an 3D engine from scratch and I am trying to unproject a vector. I use my own Math library called ALMath.js. As far as I know, to convert 2d screen coordinates into 3d world coordinates I need multiply the vector formed by x and y coordinates in the canvas by the inverse of ViewProjection Matrix. This is the code for unproject:
unproject : function (vector){
var viewMatrix = camera.viewMatrix;
var projectionMatrix = camera.projectionMatrix;
var viewProjection = viewMatrix.multiply(projectionMatrix);
var inverseViewProjection = viewProjection.getInverse();
var x = ((vector.x -0) / (AL3D.width)) *2 -1;
var y = ((vector.y -0) / (AL3D.height)) * 2 -1;
var z = 2*vector.z-1;
var w = 1;
var vec4 = new ALMath.Vector4(x,y,z,w);
var transformedVector = inverseViewProjection.multiplyByVector4(vec4);
var wordCoords = new ALMath.Vector3(transformedVector.x/transformedVector.w,transformedVector.y/transformedVector.w,transformedVector.z/transformedVector.w);
return wordCoords;
}
The ALMath library works fine. I use it around all engine (compute the model-view-projection, create projection matrix, do the inverse ...) and works well. In fact I check the result of the operations using Octave (alternative to matlab) and the result is the same with ALMath.
The problem is that if I click in the upper left corner:
canvas.addEventListener('click', function(event) {
var rect = canvas.getBoundingClientRect();
var x = event.pageX - rect.left,
y = event.pageY - rect.top;
var vector = camera.unproject(new ALMath.Vector3(x,y,0));
});
with x = 0 and y = 2 I get a vector = (-0.12131, -0.25894, -0.79409) and I know that it is wrong because If I set the cube mesh in that position I see that this is not the upper left corner.
I have programmed a lookAt function in the camera class
lookAt : function (eye, target, up)
As a example I show the operations for x = 0 and y = 2 with octave.
Firs I set my camera as follow:
camera = new AL3D.PerspectiveCamera(40, window.innerWidth/window.innerHeight);
camera.lookAt(new ALMath.Vector3(), new ALMath.Vector3(0,-0.5,-2), new ALMath.Vector3(0,1,0));
And this is step by step calculations in octave that match with javascript code result
viewMatrix =
1.00000 0.00000 0.00000 0.00000
0.00000 0.97014 0.24254 0.00000
0.00000 -0.24254 0.97014 0.00000
0.00000 0.00000 0.00000 1.00000
projectionMatrix =
1.37374 0.00000 0.00000 0.00000
0.00000 2.82610 0.00000 0.00000
0.00000 0.00000 -1.00020 -0.20002
0.00000 0.00000 -1.00000 0.00000
octave:7> viewProjectionMatrix = viewMatrix * projectionMatrix
viewProjectionMatrix =
1.37374 0.00000 0.00000 0.00000
0.00000 2.74171 -0.24258 -0.04851
0.00000 -0.68543 -0.97034 -0.19405
0.00000 0.00000 -1.00000 0.00000
octave:8> inverseViewProjectionMatrix = inv(viewProjectionMatrix)
inverseViewProjectionMatrix =
0.72794 0.00000 0.00000 -0.00000
0.00000 0.34328 -0.08582 0.00000
0.00000 0.00000 0.00000 -1.00000
0.00000 -1.21256 -4.85023 5.00050
AL3D.width = 1366
AL3D.height = 664
x = -1
y = -0.9939759036144579
z = -1
w = 1
octave:9> vector = [ -1 -0.9939759036144579 -1 1]
vector =
-1.00000 -0.99398 -1.00000 1.00000
octave:10> transformedVector = vector * inverseViewProjectionMatrix
transformedVector =
-0.72794 -1.55377 -4.76492 6.00050
// Perspective division
octave:12> result = [ transformedVector(1)/transformedVector(4) transformedVector(2)/transformedVector(4) transformedVector(3)/transformedVector(4)]
result =
-0.12131 -0.25894 -0.79409
Maybe I am forgeting something, but I don't know. What is wrong in my logic. Thanks.
Edit: The problem seem to be the view matrix. This is the code for my view matrix:
lookAt : function(eye, target, up){
var eye = eye || new ALMath.Vector3();
var up = up || new ALMath.Vector3();
var target = target || new ALMath.Vector3();
var c = this.components;
var z = target.sub(eye);
z = z.normalize();
var x = z.cross(up);
x = x.normalize();
var y = x.cross(z);
y = y.normalize();
c[0] = x.x; c[1] = x.y; c[2] = x.z;
c[4] = y.x; c[5] = y.y; c[6] = y.z;
c[8] = -z.x; c[9] = -z.y; c[10] = -z.z;
c[12] = -x.dot(eye); c[13] = -y.dot(eye); c[14] = z.dot(eye);
return this;
},
and this is for projection:
perspectiveProjection : function ( fov, aspect, zNear, zFar ) {
var a = aspect;
var tan=Math.tan(ALMath.degToRad(0.5*fov)),
A=-(zFar+zNear)/(zFar-zNear),
B=(-2*zFar*zNear)/(zFar-zNear);
var c = this.components;
c[ 0 ] = 0.5/tan; c[ 4 ] = 0; c[ 8 ] = 0; c[ 12 ] = 0;
c[ 1 ] = 0; c[ 5 ] = (0.5*a/tan); c[ 9 ] = 0; c[ 13 ] = 0;
c[ 2 ] = 0; c[ 6 ] = 0; c[ 10 ] = A; c[ 14 ] = B;
c[ 3 ] = 0; c[ 7 ] = 0; c[ 11 ] =-1; c[ 15 ] = 0;
return this;
},
My projection matrix is fine. But my view matrix is not the same that the view matrix compute in the gman library: https://github.com/greggman/twgl.js/blob/master/src/m4.js in m4.js the matrix is computed
c[0] = x.x; c[1] = x.y; c[2] = x.z; c[3] = 0;
c[4] = y.x; c[5] = y.y; c[6] = y.z; c[7] = 0;
c[8] = -z.x; c[9] = -z.y; c[10] = -z.z; c[11] = 0;
c[12] = eye.x; c[13] = eye.y; c[14] = eye.z; c[15] = 1;
instead of
c[0] = x.x; c[1] = x.y; c[2] = x.z;
c[4] = y.x; c[5] = y.y; c[6] = y.z;
c[8] = -z.x; c[9] = -z.y; c[10] = -z.z;
c[12] = -x.dot(eye); c[13] = -y.dot(eye); c[14] = z.dot(eye);
Note that in my math library I computed it with dot between axis and eye as said here: Calculating a LookAt matrix
So,that thread is wrong? I should use eye directly instead dot product between axis and eye?
If I run the script posted by gman I get the following ouput:
frustum points
0 -0.414 -0.207 -0.500
1 0.414 -0.207 -0.500
2 -0.414 0.207 -0.500
3 0.414 0.207 -0.500
4 -82.843 -41.421 -100.000
5 82.843 -41.421 -100.000
6 -82.843 41.421 -100.000
7 82.843 41.421 -100.000
camerafrustum points
0 1.666 2.120 3.080
1 1.080 2.120 3.666
2 1.497 2.458 2.911
3 0.911 2.458 3.497
4 134.224 25.915 19.067
5 17.067 25.915 136.224
6 100.403 93.555 -14.754
7 -16.754 93.555 102.403
screen points (should match width, height)
0 148.858 -47.653 4.029
1 111.806 -38.903 3.734
2 147.454 -72.303 4.217
3 108.845 -59.000 3.876
4 951.911 101.710 9.651
5 61.823 20.354 3.229
6 -833.522 732.104 -10.661
7 25.094 -97.340 4.035
unprojected (should match cameraFrustum points)
0 1.666 2.120 3.080
1 1.080 2.120 3.666
2 1.497 2.458 2.911
3 0.911 2.458 3.497
4 134.224 25.915 19.067
5 17.067 25.915 136.226
6 100.404 93.556 -14.754
7 -16.754 93.557 102.405
Is the same that result posted by gman but it differs in the screen points (should match width, height) section.
If I run the gman script in my pc with this directive: <script src="https://twgljs.org/dist/2.x/twgl-full.min.js"></script> the output is the same that gman posted
screen points (should match width, height)
0 -0.000 -0.000 -1.000
1 300.000 -0.000 -1.000
2 -0.000 150.000 -1.000
3 300.000 150.000 -1.000
4 0.000 0.000 1.000
5 300.000 0.000 1.000
6 -0.000 150.000 1.000
7 300.000 150.000 1.000
But If I download https://twgljs.org/dist/2.x/twgl-full.min.js and store in the same directory that html file and use the directive <script src="twgl.js"></script> in the html file, the output is like my Math library, this is:
screen points (should match width, height)
0 148.858 -47.653 4.029
1 111.806 -38.903 3.734
2 147.454 -72.303 4.217
3 108.845 -59.000 3.876
4 951.911 101.710 9.651
5 61.823 20.354 3.229
6 -833.522 732.104 -10.661
7 25.094 -97.340 4.035
The script adapted to my library is the following:
function main()
{
var width = 300;
var height = 150;
var aspect = width / height
var fieldOfView = Math.PI * 0.25; // 45 degrees
var zNear = 0.5;
var zFar = 100;
var projection = new ALMath.Matrix4();
projection = projection.perspectiveProjection(45, aspect, zNear, zFar);
var eye = new ALMath.Vector3(1, 2, 3);
var target = new ALMath.Vector3(4, 5, 6);
var up = new ALMath.Vector3(0, 1, 0);
var camera = new ALMath.Matrix4();
camera = camera.lookAt(eye, target, up);
var view = camera.getInverse();
var viewProjection = view.multiply(projection);
var inverseViewProjection = viewProjection.getInverse();
function getFrustumPoints(fieldOfView, aspect, zNear, zFar) {
var f = 1 / Math.tan(fieldOfView / 2);
var nearY = zNear / f;
var nearX = nearY * aspect;
var farY = zFar / f;
var farX = farY * aspect;
return [
new ALMath.Vector3(-nearX, -nearY, -zNear),
new ALMath.Vector3( nearX, -nearY, -zNear),
new ALMath.Vector3(-nearX, nearY, -zNear),
new ALMath.Vector3( nearX, nearY, -zNear),
new ALMath.Vector3(-farX, -farY, -zFar),
new ALMath.Vector3( farX, -farY, -zFar),
new ALMath.Vector3(-farX, farY, -zFar),
new ALMath.Vector3( farX, farY, -zFar),
];
}
function projectScreenPoint(width, height, projection, point) {
var c = projection.transformPoint(point);
return new ALMath.Vector3((c.x * 0.5 + 0.5) * width,(c.y * 0.5 + 0.5) * height, c.z);
}
function unproject(width, height, inverseViewProjection, p) {
return inverseViewProjection.transformPoint(new ALMath.Vector3(p.x / width * 2 - 1, p.y / height * 2 - 1, p.z));
}
function showPoints(label, points) {
log(label);
points.forEach((p, ndx) => log(ndx, p.x.toFixed(3), p.y.toFixed(3), p.z.toFixed(3)));
}
var frustumPoints = getFrustumPoints(fieldOfView, aspect, zNear, zFar);
showPoints("frustum points", frustumPoints);
var cameraFrustumPoints = frustumPoints.map(
p => camera.transformPoint(p));
showPoints("camerafrustum points", cameraFrustumPoints);
var screenPoints = cameraFrustumPoints.map(
p => projectScreenPoint(width, height, viewProjection, p));
showPoints("screen points (should match width, height)", screenPoints);
var unprojectedPoints = screenPoints.map(
p => unproject(width, height, inverseViewProjection, p));
showPoints("unprojected (should match cameraFrustum points)",
unprojectedPoints);
function log(...args) {
var elem = document.createElement("pre");
elem.textContent = [...args].join(' ');
document.body.appendChild(elem);
}
}
So the questions now are:
I should use eye directly instead dot product between axis and eye?
Why the screen points differs, from my library and local stored gman's library and from the
If I were you I'd write some tests. You have a frustum and a camera. You should easily be able to compute the corners of your frustum. Then using those corners you should be able to project them to get screen coordinates. Then check if you unproject those screen coordinates that you get the frustum points back.
Since you didn't post your math library I'll use my own
var m4 = twgl.m4;
// Plug in your math lib here
var m = {
multiply: (a, b) => m4.multiply(a, b),
inverse: (a) => m4.inverse(a),
identity: () => m4.identity(),
lookAt: (eye, target, up) => m4.lookAt(eye, target, up),
perspective: (fov, aspect, zNear, zFar) => m4.perspective(fov, aspect, zNear, zFar),
transformPoint: (m, p) => m4.transformPoint(m, p),
};
var width = 300;
var height = 150;
var aspect = width / height
var fieldOfView = Math.PI * 0.25; // 45 degrees
var zNear = 0.5;
var zFar = 100;
var projection = m.perspective(fieldOfView, aspect, zNear, zFar);
var eye = [1, 2, 3];
var target = [4, 5, 6];
var up = [0, 1, 0];
var camera = m.lookAt(eye, target, up);
var view = m.inverse(camera);
var viewProjection = m.multiply(projection, view);
var inverseViewProjection = m.inverse(viewProjection);
function getFrustumPoints(fieldOfView, aspect, zNear, zFar) {
var f = 1 / Math.tan(fieldOfView / 2);
var nearY = zNear / f;
var nearX = nearY * aspect;
var farY = zFar / f;
var farX = farY * aspect;
return [
[-nearX, -nearY, -zNear],
[ nearX, -nearY, -zNear],
[-nearX, nearY, -zNear],
[ nearX, nearY, -zNear],
[-farX, -farY, -zFar],
[ farX, -farY, -zFar],
[-farX, farY, -zFar],
[ farX, farY, -zFar],
];
}
function projectScreenPoint(width, height, projection, point) {
var c = m.transformPoint(projection, point);
return [
(c[0] * 0.5 + 0.5) * width,
(c[1] * 0.5 + 0.5) * height,
c[2],
];
}
function unproject(width, height, inverseViewProjection, p) {
return m.transformPoint(
inverseViewProjection,
[
p[0] / width * 2 - 1,
p[1] / height * 2 - 1,
p[2],
]);
}
function showPoints(label, points) {
log(label);
points.forEach((p, ndx) => log(ndx, p[0].toFixed(3), p[1].toFixed(3), p[2].toFixed(3)));
}
var frustumPoints = getFrustumPoints(fieldOfView, aspect, zNear, zFar);
showPoints("frustum points", frustumPoints);
var cameraFrustumPoints = frustumPoints.map(
p => m.transformPoint(camera, p));
showPoints("camerafrustum points", cameraFrustumPoints);
var screenPoints = cameraFrustumPoints.map(
p => projectScreenPoint(width, height, viewProjection, p));
showPoints("screen points (should match width, height)", screenPoints);
var unprojectedPoints = screenPoints.map(
p => unproject(width, height, inverseViewProjection, p));
showPoints("unprojected (should match cameraFrustum points)",
unprojectedPoints);
function log(...args) {
var elem = document.createElement("pre");
elem.textContent = [...args].join(' ');
document.body.appendChild(elem);
}
pre { margin: 0 };
<script src="https://twgljs.org/dist/2.x/twgl-full.min.js"></script>
Note: m4.transformPoint does the divide by w for the result
Plug your math lib in above?
Here's an example of plugging in glMatrix
// Plug in your math lib here
var m = {
multiply: (a, b) => mat4.multiply(mat4.create(), a, b),
inverse: (a) => mat4.invert(mat4.create(), a),
identity: () => mat4.create(),
lookAt: (eye, target, up) => mat4.invert(
mat4.create(),
mat4.lookAt(mat4.create(), eye, target, up)),
perspective: (fov, aspect, zNear, zFar) => mat4.perspective(
mat4.create(), fov, aspect, zNear, zFar),
transformPoint: (m, p) => vec3.transformMat4(vec3.create(), p, m),
};
var width = 300;
var height = 150;
var aspect = width / height
var fieldOfView = Math.PI * 0.25; // 45 degrees
var zNear = 0.5;
var zFar = 100;
var projection = m.perspective(fieldOfView, aspect, zNear, zFar);
var eye = [1, 2, 3];
var target = [4, 5, 6];
var up = [0, 1, 0];
var camera = m.lookAt(eye, target, up);
var view = m.inverse(camera);
var viewProjection = m.multiply(projection, view);
var inverseViewProjection = m.inverse(viewProjection);
function getFrustumPoints(fieldOfView, aspect, zNear, zFar) {
var f = 1 / Math.tan(fieldOfView / 2);
var nearY = zNear / f;
var nearX = nearY * aspect;
var farY = zFar / f;
var farX = farY * aspect;
return [
[-nearX, -nearY, -zNear],
[ nearX, -nearY, -zNear],
[-nearX, nearY, -zNear],
[ nearX, nearY, -zNear],
[-farX, -farY, -zFar],
[ farX, -farY, -zFar],
[-farX, farY, -zFar],
[ farX, farY, -zFar],
];
}
function projectScreenPoint(width, height, projection, point) {
var c = m.transformPoint(projection, point);
return [
(c[0] * 0.5 + 0.5) * width,
(c[1] * 0.5 + 0.5) * height,
c[2],
];
}
function unproject(width, height, inverseViewProjection, p) {
return m.transformPoint(
inverseViewProjection,
[
p[0] / width * 2 - 1,
p[1] / height * 2 - 1,
p[2],
]);
}
function showPoints(label, points) {
log(label);
points.forEach((p, ndx) => log(ndx, p[0].toFixed(3), p[1].toFixed(3), p[2].toFixed(3)));
}
var frustumPoints = getFrustumPoints(fieldOfView, aspect, zNear, zFar);
showPoints("frustum points", frustumPoints);
var cameraFrustumPoints = frustumPoints.map(
p => m.transformPoint(camera, p));
showPoints("camerafrustum points", cameraFrustumPoints);
var screenPoints = cameraFrustumPoints.map(
p => projectScreenPoint(width, height, viewProjection, p));
showPoints("screen points (should match width, height)", screenPoints);
var unprojectedPoints = screenPoints.map(
p => unproject(width, height, inverseViewProjection, p));
showPoints("unprojected (should match cameraFrustum points)",
unprojectedPoints);
function log(...args) {
var elem = document.createElement("pre");
elem.textContent = [...args].join(' ');
document.body.appendChild(elem);
}
pre { margin: 0 };
<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.3.2/gl-matrix-min.js"></script>
As for your example you're passing on 0 for z which is somewhere in the middle of the frustum depth wise. Also I see code like this
transformedVector(1)/transformedVector(4)
I don't know your math library but mine would be zero based indices so
transformedVector(0)/transformedVector(3)
Here's the code you added to your example. It's working for me. I filled in the missing math functions.
const m4 = twgl.m4;
class Vector3 {
constructor(x, y, z) {
this.x = x || 0;
this.y = y || 0;
this.z = z || 0;
}
sub(v) {
return new Vector3(this.x - v.x, this.y - v.y, this.z - v.z);
}
cross(v) {
return new Vector3(
this.y * v.z - this.z * v.y,
this.z * v.x - this.x * v.z,
this.x * v.y - this.y * v.x);
}
dot(v) {
return (this.x * v.x) + (this.y * v.y) + (this.z * v.z);
}
normalize() {
var lenSq = this.x * this.x + this.y * this.y + this.z * this.z;
var len = Math.sqrt(lenSq);
if (len > 0.00001) {
return new Vector3(this.x / len, this.y / len, this.z / len);
} else {
return new Vector3();
}
}
}
class Vector4 {
constructor(x, y, z, w) {
this.x = x || 0;
this.y = y || 0;
this.z = z || 0;
this.w = w || 0;
}
}
class Matrix4 {
constructor(components) {
this.components = components || m4.identity();
}
multiply(m) {
return new Matrix4(m4.multiply(m.components, this.components));
}
getInverse() {
return new Matrix4(m4.inverse(this.components));
}
multiplyByVector4(v) {
const m = this.components;
const x = v.x * m[0 * 4 + 0] + v.y * m[1 * 4 + 0] + v.z * m[2 * 4 + 0] + v.w * m[3 * 4 + 0];
const y = v.x * m[0 * 4 + 1] + v.y * m[1 * 4 + 1] + v.z * m[2 * 4 + 1] + v.w * m[3 * 4 + 1];
const z = v.x * m[0 * 4 + 2] + v.y * m[1 * 4 + 2] + v.z * m[2 * 4 + 2] + v.w * m[3 * 4 + 2];
const w = v.x * m[0 * 4 + 3] + v.y * m[1 * 4 + 3] + v.z * m[2 * 4 + 3] + v.w * m[3 * 4 + 3];
return new Vector4(x, y, z, w);
}
transformPoint(v) {
const v4 = this.multiplyByVector4(new Vector4(v.x, v.y, v.z, 1));
return new Vector3(v4.x / v4.w, v4.y / v4.w, v4.z / v4.w);
}
lookAt(eye, target, up) {
var eye = eye || new ALMath.Vector3();
var up = up || new ALMath.Vector3();
var target = target || new ALMath.Vector3();
var c = this.components;
var z = target.sub(eye);
z = z.normalize();
var x = z.cross(up);
x = x.normalize();
var y = x.cross(z);
y = y.normalize();
c[0] = x.x; c[1] = x.y; c[2] = x.z;
c[4] = y.x; c[5] = y.y; c[6] = y.z;
c[8] = -z.x; c[9] = -z.y; c[10] = -z.z;
c[12] = -x.dot(eye); c[13] = -y.dot(eye); c[14] = z.dot(eye);
return this;
}
perspectiveProjection( fov, aspect, zNear, zFar ) {
var a = aspect;
var tan=Math.tan(ALMath.degToRad(0.5*fov)),
A=-(zFar+zNear)/(zFar-zNear),
B=(-2*zFar*zNear)/(zFar-zNear);
var c = this.components;
c[ 0 ] = 0.5/tan; c[ 4 ] = 0; c[ 8 ] = 0; c[ 12 ] = 0;
c[ 1 ] = 0; c[ 5 ] = (0.5*a/tan); c[ 9 ] = 0; c[ 13 ] = 0;
c[ 2 ] = 0; c[ 6 ] = 0; c[ 10 ] = A; c[ 14 ] = B;
c[ 3 ] = 0; c[ 7 ] = 0; c[ 11 ] =-1; c[ 15 ] = 0;
return this;
}
}
class PerspectiveCamera {
constructor(fieldOfViewDegrees, aspect, zNear, zFar) {
this.fieldOfViewDegrees = fieldOfViewDegrees || 45;
this.aspect = aspect || 1;
this.zNear = zNear || 0.5;
this.zFar = zFar || 100;
this.projectionMatrix = new Matrix4();
this.viewMatrix = new Matrix4();
this.updateProjection();
}
updateProjection() {
this.projectionMatrix.perspectiveProjection(
this.fieldOfViewDegrees, this.aspect, this.zNear, this.zFar);
}
lookAt(eye, target, up) {
//this.viewMatrix.lookAt(eye, target, up);
this.cameraMatrix = this.viewMatrix.getInverse();
}
transformPoint(v) {
// note this tranasforms by the camera matrix
// (which is the inverse view matrix)
// and not the perspective matrix
return this.cameraMatrix.transformPoint(v);
}
}
const ALMath = {
Vector3: Vector3,
Matrix4: Matrix4,
degToRad: d => d * Math.PI / 180,
};
const AL3D = {
width: 300,
height: 150,
PerspectiveCamera: PerspectiveCamera,
};
const camera = new AL3D.PerspectiveCamera(40, AL3D.width/AL3D.height);
camera.lookAt(
new ALMath.Vector3(),
new ALMath.Vector3(0,-0.5,-2),
new ALMath.Vector3(0,1,0));
function getFrustumPoints(fieldOfView, aspect, zNear, zFar) {
var f = 1 / Math.tan(ALMath.degToRad(fieldOfView) / 2);
var nearY = zNear / f;
var nearX = nearY * aspect;
var farY = zFar / f;
var farX = farY * aspect;
return [
new ALMath.Vector3(-nearX, -nearY, -zNear),
new ALMath.Vector3( nearX, -nearY, -zNear),
new ALMath.Vector3(-nearX, nearY, -zNear),
new ALMath.Vector3( nearX, nearY, -zNear),
new ALMath.Vector3(-farX, -farY, -zFar),
new ALMath.Vector3( farX, -farY, -zFar),
new ALMath.Vector3(-farX, farY, -zFar),
new ALMath.Vector3( farX, farY, -zFar),
];
}
const projectionMatrix = camera.projectionMatrix;
const viewMatrix = camera.viewMatrix;
const viewProjection = viewMatrix.multiply(projectionMatrix);
const inverseViewProjection = viewProjection.getInverse();
function projectScreenPoint(width, height, projection, point) {
var c = projectionMatrix.transformPoint(point);
return new ALMath.Vector3(
(c.x * 0.5 + 0.5) * width,
(c.y * 0.5 + 0.5) * height,
c.z);
}
function unproject(width, height, inverseViewProjection, p) {
return inverseViewProjection.transformPoint(new ALMath.Vector3(
p.x / width * 2 - 1,
p.y / height * 2 - 1,
p.z));
}
function showPoints(label, points) {
log(label);
points.forEach((p, ndx) => log(ndx, p.x.toFixed(3), p.y.toFixed(3), p.z.toFixed(3)));
}
var frustumPoints = getFrustumPoints(camera.fieldOfViewDegrees, camera.aspect, camera.zNear, camera.zFar);
showPoints("frustum points", frustumPoints);
var cameraFrustumPoints = frustumPoints.map(
p => camera.transformPoint(p));
showPoints("camerafrustum points", cameraFrustumPoints);
var screenPoints = cameraFrustumPoints.map(
p => projectScreenPoint(AL3D.width, AL3D.height, viewProjection, p));
showPoints("screen points (should match width, height)", screenPoints);
var unprojectedPoints = screenPoints.map(
p => unproject(AL3D.width, AL3D.height, inverseViewProjection, p));
showPoints("unprojected (should match cameraFrustum points)",
unprojectedPoints);
function log(...args) {
var elem = document.createElement("pre");
elem.textContent = [...args].join(' ');
document.body.appendChild(elem);
}
pre { margin: 0; }
<script src="https://twgljs.org/dist/2.x/twgl-full.min.js"></script>
Related
I am trying to create a portal rendering engine in WebGL 2.0 (so far I am trying to port this C++ project: https://github.com/HackerPoet/NonEuclidean), and am trying to implement oblique projection in order to align the clipping plane of the portal cameras to their respective portals; despite finding multiple implementations across various OpenGL platforms, however, I can't get it to work in my WebGL project. Here is the code related to it:
const Mat4 = (() =>
{
const { mat3, mat4, vec2, vec3, vec4 } = glMatrix;
return {
...mat4,
zAxis(out, mat)
{
return vec3.set(out, mat[2], mat[6], mat[10]);
},
mulVec4(out, mat, vec)
{
const [x, y, z, w] = vec;
return vec3.set(out,
mat[ 0] * x + mat[ 1] * y + mat[ 2] * z + mat[ 3] * w,
mat[ 4] * x + mat[ 5] * y + mat[ 6] * z + mat[ 7] * w,
mat[ 8] * x + mat[ 9] * y + mat[10] * z + mat[11] * w,
mat[12] * x + mat[13] * y + mat[14] * z + mat[15] * w);
},
mulPoint(out, mat, vec)
{
const [x, y, z] = vec;
const w = mat[12] * x + mat[13] * y + mat[14] * z + mat[15];
return vec3.set(out,
(mat[0] * x + mat[1] * y + mat[ 2] * z + mat[ 3]) / w,
(mat[4] * x + mat[5] * y + mat[ 6] * z + mat[ 7]) / w,
(mat[8] * x + mat[9] * y + mat[10] * z + mat[11]) / w);
},
};
})();
const Camera = () =>
{
const { vec3, vec4 } = glMatrix;
const projection = Mat4.create();
const view = Mat4.create();
return {
width: 0,
height: 0,
aspect: 1,
near: 0.1,
far: 100.0,
fov: Math.PI / 3,
pos: [0, 0, 0],
euler: [0, 0, 0],
get view()
{
return view;
},
set view(matrix)
{
return Mat4.copy(view, matrix);
},
get matrix()
{
return Mat4.multiply(Mat4.create(), projection, view)
},
get projection()
{
return projection;
},
set transform([x = 0, y = 0, z = 0, rx = 0, ry = 0])
{
const matrix = Mat4.fromXRotation(Mat4.create(), rx);
Mat4.multiply(matrix, matrix, Mat4.fromYRotation(Mat4.create(), ry));
Mat4.multiply(matrix, matrix, Mat4.fromTranslation(Mat4.create(), [-x, -y, -z]));
Mat4.copy(view, matrix);
return view;
},
inverse()
{
const inv = Mat4.create();
const a = projection[0];
const b = projection[5];
const c = projection[10];
const d = projection[11];
const e = projection[14];
inv[0] = 1 / a;
inv[5] = 1 / b;
inv[11] = 1 / e;
inv[14] = 1 / d;
inv[15] = -c / (d * e)
return inv;
},
clipOblique(pos, norm)
{
const cpos = Mat4.mulPoint(vec3.create(), view, pos);
const cnorm = vec3.normalize(vec3.create(), Mat4.mulPoint(vec3.create(), view, norm));
const point = Mat4.mulPoint(vec3.create(), Mat4.invert(Mat4.create(), view), [0, 0, 0]);
cpos[1] -= point[1];
const cplane = vec4.set(vec4.create(), cnorm[0], cnorm[1], cnorm[2], -vec3.dot(cpos, cnorm));
const q = Mat4.mulVec4(vec4.create(), Mat4.invert(Mat4.create(), projection), [
(cplane[0] > 0 ? 1 : -1),
(cplane[1] > 0 ? 1 : -1),
1,
1]);
const c = cplane.map(x => x * 2 / vec4.dot(cplane, q));
projection[ 2] = c[0] - projection[ 3];
projection[ 6] = c[1] - projection[ 7];
projection[10] = c[2] - projection[11];
projection[14] = c[3] - projection[15];
return this;
},
copy(that)
{
this.setup(that.width, that.height, that.near, that.far, that.fov, that.aspect);
Mat4.copy(view, that.view);
Mat4.copy(projection, that.projection);
return this;
},
setup(width = this.width, height = this.height, near = this.near, far = this.far, fov = this.fov, aspect = height / width)
{
this.width = width;
this.height = height;
this.near = near;
this.far = far;
this.fov = fov;
this.aspect = aspect;
const f = 1.0 / Math.tan(fov / 2);
const range = 1.0 / (near - far);
projection[0] = f * aspect
projection[1] = 0.0;
projection[2] = 0.0;
projection[3] = 0.0;
projection[4] = 0.0;
projection[5] = f;
projection[6] = 0.0;
projection[7] = 0.0;
projection[8] = 0.0;
projection[9] = 0.0;
projection[10] = (near + far) * range;
projection[11] = -1.0;
projection[12] = 0.0;
projection[13] = 0.0;
projection[14] = 2 * near * far * range;
projection[15] = 0.0;
return this;
}
}
};
const GameObject = (gl, mesh) =>
{
const { vec3 } = glMatrix;
return {
pos: [0, 0, 0],
euler: [0, 0, 0],
scale: [1, 1, 1],
mesh,
forward()
{
const matrix = Mat4.fromZRotation(Mat4.create(), this.euler[2]);
Mat4.multiply(matrix, matrix, Mat4.fromXRotation(Mat4.create(), this.euler[0]));
Mat4.multiply(matrix, matrix, Mat4.fromYRotation(Mat4.create(), this.euler[1]));
return vec3.negate(vec3.create(), Mat4.zAxis(vec3.create(), matrix));
},
localToWorld()
{
const matrix = Mat4.fromTranslation(Mat4.create(), this.pos);
Mat4.multiply(matrix, matrix, Mat4.fromYRotation(Mat4.create(), this.euler[1]));
Mat4.multiply(matrix, matrix, Mat4.fromXRotation(Mat4.create(), this.euler[0]));
Mat4.multiply(matrix, matrix, Mat4.fromZRotation(Mat4.create(), this.euler[2]));
Mat4.multiply(matrix, matrix, Mat4.fromScaling(Mat4.create(), this.scale));
return matrix;
},
worldToLocal()
{
const matrix = Mat4.fromScaling(Mat4.create(), this.scale);
Mat4.invert(matrix, matrix);
Mat4.multiply(matrix, matrix, Mat4.fromZRotation(Mat4.create(), this.euler[2]));
Mat4.multiply(matrix, matrix, Mat4.fromXRotation(Mat4.create(), this.euler[0]));
Mat4.multiply(matrix, matrix, Mat4.fromYRotation(Mat4.create(), this.euler[1]));
Mat4.multiply(matrix, matrix, Mat4.fromTranslation(Mat4.create(), vec3.negate(vec3.create(), this.pos)));
return matrix;
},
render(camera, fbo = null)
{
const mv = Mat4.transpose(Mat4.create(), this.worldToLocal());
const mvp = Mat4.multiply(Mat4.create(), camera.matrix, this.localToWorld());
this.mesh.render(mvp, mv);
return this;
},
};
};
const Portal = (() =>
{
const { vec3 } = glMatrix;
const Warp = (fromPortal = {}) =>
{
const delta = Mat4.identity(Mat4.create());
const deltaInv = Mat4.identity(Mat4.create());
return {
fromPortal,
toPortal: null,
delta, deltaInv,
};
};
return {
connectWarp(a, b)
{
a.toPortal = b.fromPortal;
b.toPortal = a.toPortal;
a.delta = Mat4.multiply(Mat4.create(), a.fromPortal.localToWorld(), b.fromPortal.worldToLocal());
b.delta = Mat4.multiply(Mat4.create(), b.fromPortal.localToWorld(), a.fromPortal.worldToLocal());
a.deltaInv = b.delta;
b.deltaInv = a.delta;
return this;
},
connect(a, b)
{
this.connectWarp(a.front, b.back);
this.connectWarp(b.front, a.back);
return this;
},
create: async (gl) =>
{
const mesh = await Mesh.load('double_quad.obj');
const shader = await Shader(gl, '.', 'portal-shader');
const { a_position } = shader.attribute;
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
const buffers = createBuffers(gl, new Array(1));
setupArrayBuffer(gl, buffers[0], new Float32Array(mesh.geometries[0].data.position), 3, a_position);
const portalCam = Camera();
return (self =>
{
self.front = Warp(self);
self.back = Warp(self);
return self;
})({
...GameObject(gl, null),
front: null,
back: null,
get cam()
{
return portalCam;
},
/*Override*/ render(camera, fbo, func)
{
console.assert(this.euler[0] === 0);
console.assert(this.euler[2] === 0);
const normal = this.forward();
const camPos = Mat4.getTranslation(vec3.create(), Mat4.invert(Mat4.create(), camera.view));
const isFront = vec3.dot(vec3.subtract(vec3.create(), camPos, this.pos), normal) > 0;
if (isFront)
{
vec3.negate(normal, normal);
}
const warp = isFront? this.front : this.back;
const mvp = Mat4.multiply(Mat4.create(), camera.matrix, this.localToWorld());
portalCam.copy(camera);
portalCam.clipOblique(vec3.add(vec3.create(), this.pos, normal.map(x => x * 0.1)), vec3.negate(vec3.create(), normal));
Mat4.multiply(portalCam.view, portalCam.view, warp.delta);
shader.use();
gl.bindVertexArray(vao);
const { u_mvp, u_texture } = shader.uniform;
gl.uniform1i(u_texture, 0);
gl.activeTexture(gl.TEXTURE0 + 0);
fbo.use();
gl.uniformMatrix4fv(u_mvp, false, mvp);
gl.drawArrays(gl.TRIANGLES, 0, mesh.geometries[0].data.position.length / 3);
return this;
},
});
},
};
})();
At first the portal seems normal, but when I try to go behind it, the objects are still rendered (if I position myself so that the portal's camera is behind them), but in an unusual way. I am not very good at matrix math or linear algebra, and would appreciate some help/insight; thank you.
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>
Say I have two vectors:
V1 = { x: 3.296372727813439, y: -14.497928014719344, z: 12.004105246875968 }
V2 = { x: 2.3652551657790695, y: -16.732085083053185, z: 8.945905454164146 }
How can I figure out what angle v1 needs to be rotated to look directly at v2?
Put into English: say I knew exactly where I was in space, and exactly where another person was somewhere else in space.... Mathematically, how could I figure out what angles to put my finger at to point at them?
Here's what my axis looks like
My current (incorrect) formula
v2.x -= v1.x; // move v2 (vector 2) relative to the new origin
v2.y -= v1.y;
v2.z -= v1.z;
v1.x = 0; // set v1 (vector 1) as the origin
v1.y = 0;
v1.z = 0;
var r = Math.sqrt(Math.pow(v2.x,2) + Math.pow(v2.y,2) + Math.pow(v2.z,2));
var θ = Math.acos((Math.pow(v2.x,2) + Math.pow(v2.z,2))/(r*Math.sqrt(Math.pow(v2.x,2) + Math.pow(v2.z,2))));
var ϕ = Math.acos(v2.x/Math.sqrt(Math.pow(v2.x,2) + Math.pow(v2.z,2)));
Then I rotate v1 with the theta and phi.
v1.rotation.y = θ;
v2.rotation.x = ϕ;
But this is giving me the wrong rotation.
θ = 0.6099683401012933
ϕ = 1.8663452274936656
But if I use THREE.js and use the lookAt function, it spits out these rotations instead, that work beautifully:
y/θ: -0.24106818240525682
x/ϕ: 2.5106584861123644
Thanks for all the help in advance! I cannot use THREE.js in my final result, I'm trying to go pure vanilla JS so I can port it to another language.
How about using matrix? I think v1 is your view point, and looking towards v2. matrix is a good way to show orientation. Euler Angle is another interpretation of orientation.
My idea is building a transformation matrix from object space to world space, what you want to do can be translated in three steps:
at the beginning, the camera is at the world space origin, camera rotation is (0,0,0) world space is same as object space. v1'(0,0,0).
we translate camera to v1(3.296372727813439,-14.497928014719344,12.004105246875968), object space has an offset with world space, but object space axises are parallel with world space axises, camera rotation is still (0,0,0).
we make camera look at v2, as you see, camera rotation would change.
If I can build a transformation matrix represent all action above, I can get the orientation.
First, calculating translation matrix: Because translation is an affine transformation, we need to use a 4x4 matrix to represent translation. we can easily get the matrix:
we use basis axis to get rotation matrix.
you may need to set the camera up vector. the default is (0,1,0). in object space, the basis z axis can be calculated by v1-v2.
z = (v1.x-v2.x,v1.y-v2.y,v1.z-v2.z).normalize()
basis x vector: we know basis vector is a vector perpendicular to z-up plane, we get the x vector by cross product up and z.
x = up.crossproduct(z)
basis y vector, y is prependicular to z-x plane.
y = z.product(x)
we can build the rotation matrix as a 3 x 3 matrix:
then, we finally get the transformation matrix:
we can use the matrix represent the camera orientation. if you need Euler Angle or Quaternion. there some ways convert between them. you can find in this book: 3D Math Primer for Graphics and Game Developmen
Three.js implements LookAt() function same as my way.
Here is three.js source code, and I add some comments:
function lookAt( eye, target, up ) //eye : your camera position; target : which point you want to look at; up : camera up vector
{
if ( x === undefined ) {
x = new Vector3();
y = new Vector3();
z = new Vector3();
}
var te = this.elements; //this.elements is a 4 x 4 matrix stored in a list.
z.subVectors( eye, target ).normalize(); // set z vector with the direction from your camera to target point.
if ( z.lengthSq() === 0 ) {
z.z = 1;
}
x.crossVectors( up, z ).normalize(); // set the x vector by cross product up and z vector, you know cross product would get a //vector which perpendicular with these two vectors.
if ( x.lengthSq() === 0 ) {
z.z += 0.0001; // if z is ZERO vector, then, make a little addition to z.z
x.crossVectors( up, z ).normalize();
}
y.crossVectors( z, x ); // set y by cross product z and x.
// using basic axises to set the matrix.
te[ 0 ] = x.x; te[ 4 ] = y.x; te[ 8 ] = z.x;
te[ 1 ] = x.y; te[ 5 ] = y.y; te[ 9 ] = z.y;
te[ 2 ] = x.z; te[ 6 ] = y.z; te[ 10 ] = z.z;
return this;
};
// now you get the transformation matrix, you can set the rotation or orientation with this matrix.
You can implement Matrix with list like three.js does.
I also have another idea--Spherical Polar Coordinates System. Spherical coordinates always be noted like (r,Θ,Φ), Θ is heading angle, and Φ is pitch angle. what you need to do is convert v1 and v2 Cartesian coordinates to spherical coordinates. because the second and third element in spherical is angle, we can calculate angular displacement between v1 and v2. then, make this displacement as an addition to you camera rotation.
Here is my code(suppose you camera is at the world origin(0,0,0)):
//convert v1 and v2 Cartesian coordinates to Spherical coordinates;
var radiusV1 = Math.sqrt( Math.pow(v1.x) + Math.pow(v1.y) + Math.pow(v1.z));
var headingV1 = Math.atan2(v1.x , v1.z);
var pitchV1 = Math.asin(-(v1.y) / radiusV1);
var radiusV2 = Math.sqrt( Math.pow(v2.x) + Math.pow(v2.y) + Math.pow(v2.z));
var headingV2 = Math.atan2(v2.x , v2.z);
var pitchV2 = Math.asin(-(v2.y) / radiusV2);
//calculate angular displacement.
var displacementHeading = headingV2 - headingV1;
var displacementPitch = pitchV2 - pitchV1;
//make this displacement as an addition to camera rotation.
camera.rotation.x += displacementPitch;
camera.rotation.y += displacementHeading;
By the way, 3D math is very helpful and worth to learn, all the formula or concept I reference can be found in the book.
Wish it can help you.
The correct equations are:
var dx = v2.x-v1.x; //-0.93
var dy = v2.y-v1.y; //-31.22
var dz = v2.z-v1.z;
var rxy = Math.sqrt( Math.pow(dx,2) + Math.pow(dy,2) );
var lambda = Math.atan(dy/dx);
var phi = Math.atan(dz/rxy)
The above formula for phi and lambda needs to be adjusted based on which quarter your vector lies in. I made it easy for you:
//if you do the calculations in degrees, you need to add 180 instead of PI
if (dx < 0) phi = phi + Math.PI;
if (dz < 0) lambda = -1 * lambda;
x/ϕ is rotation around the x Axis so its equal to the angle between y,z and for y/θ we have to find angle between x,z.
V1 = { x: 3.296372727813439, y: -14.497928014719344, z: 12.004105246875968 }
V2 = { x: 2.3652551657790695, y: -16.732085083053185, z: 8.945905454164146 }
var v={dx:V2.x-V1.x, dy:V2.y-V1.y, dz:V2.z-V1.z}
testVector(v);
function testVector(vec){
console.log();
var angles=calcAngles(vec);
console.log("phi:"+angles.phi+" theta:"+angles.theta);
}
function calcAngles(vec){
return {
theta:(Math.PI/2)+Math.atan2(vec.dz, vec.dx),
phi:(3*Math.PI/2)+Math.atan2(vec.dz, vec.dy)
};
}
I've extracted the relevant code from the latest version of THREE.js (r84).
I think this is the best way of getting the result you're after.
// Unless otherwise noted by comments, all functions originate from the latest version of THREE.js (r84)
// https://github.com/mrdoob/three.js/tree/master
// THREE.js is licensed under MIT (Copyright © 2010-2017 three.js authors)
//
// Some functions have been changed by K Scandrett to work within this setting,
// but not the calculations.
// Any mistakes are considered mine and not the authors of THREE.js.
// I provide no guarantees that I haven't created any bugs in reworking the original code
// so use at your own risk. Enjoy the pizza.
var v1 = {x: 3.296372727813439, y: -14.497928014719344, z: 12.004105246875968};
var v2 = {x: 2.3652551657790695, y: -16.732085083053185,z: 8.945905454164146};
var startVec = {x: v1.x, y: v1.y, z: v1.z, w: 0};
var endVec = {x: v2.x, y: v2.y, z: v2.z, w: 0};
var upVec = {x: 0, y: 1, z: 0}; // y up
var quat = lookAt(startVec, endVec, upVec);
var angles = eulerSetFromQuaternion(quat);
console.log(angles.x + " " + angles.y + " " + angles.z);
/* KS function */
function magnitude(v) {
return Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
}
/* KS function */
function normalize(v) {
var mag = magnitude(v);
return {
x: v.x / mag,
y: v.y / mag,
z: v.z / mag
};
}
function subVectors(a, b) {
return {
x: a.x - b.x,
y: a.y - b.y,
z: a.z - b.z
};
}
function crossVectors(a, b) {
var ax = a.x,
ay = a.y,
az = a.z;
var bx = b.x,
by = b.y,
bz = b.z;
return {
x: ay * bz - az * by,
y: az * bx - ax * bz,
z: ax * by - ay * bx
};
}
function lengthSq(v) {
return v.x * v.x + v.y * v.y + v.z * v.z;
}
function makeRotationFromQuaternion(q) {
var matrix = new Float32Array([
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
]);
var te = matrix;
var x = q.x,
y = q.y,
z = q.z,
w = q.w;
var x2 = x + x,
y2 = y + y,
z2 = z + z;
var xx = x * x2,
xy = x * y2,
xz = x * z2;
var yy = y * y2,
yz = y * z2,
zz = z * z2;
var wx = w * x2,
wy = w * y2,
wz = w * z2;
te[0] = 1 - (yy + zz);
te[4] = xy - wz;
te[8] = xz + wy;
te[1] = xy + wz;
te[5] = 1 - (xx + zz);
te[9] = yz - wx;
te[2] = xz - wy;
te[6] = yz + wx;
te[10] = 1 - (xx + yy);
// last column
te[3] = 0;
te[7] = 0;
te[11] = 0;
// bottom row
te[12] = 0;
te[13] = 0;
te[14] = 0;
te[15] = 1;
return te;
}
function RotationMatrix(m) {
// http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/index.htm
// assumes the upper 3x3 of m is a pure rotation matrix (i.e, unscaled)
var _w, _x, _y, _z;
var te = m,
m11 = te[0],
m12 = te[4],
m13 = te[8],
m21 = te[1],
m22 = te[5],
m23 = te[9],
m31 = te[2],
m32 = te[6],
m33 = te[10],
trace = m11 + m22 + m33,
s;
if (trace > 0) {
s = 0.5 / Math.sqrt(trace + 1.0);
_w = 0.25 / s;
_x = (m32 - m23) * s;
_y = (m13 - m31) * s;
_z = (m21 - m12) * s;
} else if (m11 > m22 && m11 > m33) {
s = 2.0 * Math.sqrt(1.0 + m11 - m22 - m33);
_w = (m32 - m23) / s;
_x = 0.25 * s;
_y = (m12 + m21) / s;
_z = (m13 + m31) / s;
} else if (m22 > m33) {
s = 2.0 * Math.sqrt(1.0 + m22 - m11 - m33);
_w = (m13 - m31) / s;
_x = (m12 + m21) / s;
_y = 0.25 * s;
_z = (m23 + m32) / s;
} else {
s = 2.0 * Math.sqrt(1.0 + m33 - m11 - m22);
_w = (m21 - m12) / s;
_x = (m13 + m31) / s;
_y = (m23 + m32) / s;
_z = 0.25 * s;
}
return {
w: _w,
x: _x,
y: _y,
z: _z
};
}
function eulerSetFromQuaternion(q, order, update) {
var matrix;
matrix = makeRotationFromQuaternion(q);
return eulerSetFromRotationMatrix(matrix, order);
}
function eulerSetFromRotationMatrix(m, order, update) {
var _x, _y, _z;
var clamp = function(value, min, max) {
return Math.max(min, Math.min(max, value));
};
// assumes the upper 3x3 of m is a pure rotation matrix (i.e, unscaled)
var te = m;
var m11 = te[0],
m12 = te[4],
m13 = te[8];
var m21 = te[1],
m22 = te[5],
m23 = te[9];
var m31 = te[2],
m32 = te[6],
m33 = te[10];
//order = order || this._order;
order = order || 'XYZ'; // KS added. Other code sets the rotation order default
if (order === 'XYZ') {
_y = Math.asin(clamp(m13, -1, 1));
if (Math.abs(m13) < 0.99999) {
_x = Math.atan2(-m23, m33);
_z = Math.atan2(-m12, m11);
} else {
_x = Math.atan2(m32, m22);
_z = 0;
}
} else if (order === 'YXZ') {
_x = Math.asin(-clamp(m23, -1, 1));
if (Math.abs(m23) < 0.99999) {
_y = Math.atan2(m13, m33);
_z = Math.atan2(m21, m22);
} else {
_y = Math.atan2(-m31, m11);
_z = 0;
}
} else if (order === 'ZXY') {
_x = Math.asin(clamp(m32, -1, 1));
if (Math.abs(m32) < 0.99999) {
_y = Math.atan2(-m31, m33);
_z = Math.atan2(-m12, m22);
} else {
_y = 0;
_z = Math.atan2(m21, m11);
}
} else if (order === 'ZYX') {
_y = Math.asin(-clamp(m31, -1, 1));
if (Math.abs(m31) < 0.99999) {
_x = Math.atan2(m32, m33);
_z = Math.atan2(m21, m11);
} else {
_x = 0;
_z = Math.atan2(-m12, m22);
}
} else if (order === 'YZX') {
_z = Math.asin(clamp(m21, -1, 1));
if (Math.abs(m21) < 0.99999) {
_x = Math.atan2(-m23, m22);
_y = Math.atan2(-m31, m11);
} else {
_x = 0;
_y = Math.atan2(m13, m33);
}
} else if (order === 'XZY') {
_z = Math.asin(-clamp(m12, -1, 1));
if (Math.abs(m12) < 0.99999) {
_x = Math.atan2(m32, m22);
_y = Math.atan2(m13, m11);
} else {
_x = Math.atan2(-m23, m33);
_y = 0;
}
} else {
console.warn('THREE.Euler: .setFromRotationMatrix() given unsupported order: ' + order);
}
//_order = order;
//if ( update !== false ) this.onChangeCallback();
return {
x: _x,
y: _y,
z: _z
};
}
function setFromQuaternion(q, order, update) {
var matrix = makeRotationFromQuaternion(q);
return setFromRotationMatrix(matrix, order, update);
}
function setFromRotationMatrix(m) {
// http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/index.htm
// assumes the upper 3x3 of m is a pure rotation matrix (i.e, unscaled)
var _w, _x, _y, _z;
var te = m,
m11 = te[0],
m12 = te[4],
m13 = te[8],
m21 = te[1],
m22 = te[5],
m23 = te[9],
m31 = te[2],
m32 = te[6],
m33 = te[10],
trace = m11 + m22 + m33,
s;
if (trace > 0) {
s = 0.5 / Math.sqrt(trace + 1.0);
_w = 0.25 / s;
_x = (m32 - m23) * s;
_y = (m13 - m31) * s;
_z = (m21 - m12) * s;
} else if (m11 > m22 && m11 > m33) {
s = 2.0 * Math.sqrt(1.0 + m11 - m22 - m33);
_w = (m32 - m23) / s;
_x = 0.25 * s;
_y = (m12 + m21) / s;
_z = (m13 + m31) / s;
} else if (m22 > m33) {
s = 2.0 * Math.sqrt(1.0 + m22 - m11 - m33);
_w = (m13 - m31) / s;
_x = (m12 + m21) / s;
_y = 0.25 * s;
_z = (m23 + m32) / s;
} else {
s = 2.0 * Math.sqrt(1.0 + m33 - m11 - m22);
_w = (m21 - m12) / s;
_x = (m13 + m31) / s;
_y = (m23 + m32) / s;
_z = 0.25 * s;
}
return {
w: _w,
x: _x,
y: _y,
z: _z
};
}
function lookAt(eye, target, up) {
// This routine does not support objects with rotated and/or translated parent(s)
var m1 = lookAt2(target, eye, up);
return setFromRotationMatrix(m1);
}
function lookAt2(eye, target, up) {
var elements = new Float32Array([
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
]);
var x = {
x: 0,
y: 0,
z: 0
};
var y = {
x: 0,
y: 0,
z: 0
};
var z = {
x: 0,
y: 0,
z: 0
};
var te = elements;
z = subVectors(eye, target);
z = normalize(z);
if (lengthSq(z) === 0) {
z.z = 1;
}
x = crossVectors(up, z);
x = normalize(x);
if (lengthSq(x) === 0) {
z.z += 0.0001;
x = crossVectors(up, z);
x = normalize(x);
}
y = crossVectors(z, x);
te[0] = x.x;
te[4] = y.x;
te[8] = z.x;
te[1] = x.y;
te[5] = y.y;
te[9] = z.y;
te[2] = x.z;
te[6] = y.z;
te[10] = z.z;
return te;
}
function lookatOld(vecstart, vecEnd, vecUp) {
var temp = new THREE.Matrix4();
temp.lookAt(vecEnd, vecstart, vecUp);
var m00 = temp.elements[0],
m10 = temp.elements[1],
m20 = temp.elements[2],
m01 = temp.elements[4],
m11 = temp.elements[5],
m21 = temp.elements[6],
m02 = temp.elements[8],
m12 = temp.elements[9],
m22 = temp.elements[10];
var t = m00 + m11 + m22,
s, x, y, z, w;
if (t > 0) {
s = Math.sqrt(t + 1) * 2;
w = 0.25 * s;
x = (m21 - m12) / s;
y = (m02 - m20) / s;
z = (m10 - m01) / s;
} else if ((m00 > m11) && (m00 > m22)) {
s = Math.sqrt(1.0 + m00 - m11 - m22) * 2;
x = s * 0.25;
y = (m10 + m01) / s;
z = (m02 + m20) / s;
w = (m21 - m12) / s;
} else if (m11 > m22) {
s = Math.sqrt(1.0 + m11 - m00 - m22) * 2;
y = s * 0.25;
x = (m10 + m01) / s;
z = (m21 + m12) / s;
w = (m02 - m20) / s;
} else {
s = Math.sqrt(1.0 + m22 - m00 - m11) * 2;
z = s * 0.25;
x = (m02 + m20) / s;
y = (m21 + m12) / s;
w = (m10 - m01) / s;
}
var rotation = new THREE.Quaternion(x, y, z, w);
rotation.normalize();
return rotation;
}
Here's the same code in Plunker: http://plnkr.co/edit/vgNko1fJu9eYYCnJbYVo?p=preview
The exact/literal answer for your question would be a bad/unethical answer. Don't try to use Euler angles. The Euler coordinate system is for coordination. It's not a good system to do orientation/rotation. While being easy to read for human, it is prone to Gimbal lock-ing that will produce incorrect result.
There are 2 common systems for orientation: Transform matrix and Quaternions. three.js lookAt() uses quaternions, Crag.Li's answer uses Transform Matrix.
I feel obliged to emphasize this because I once underestimated 3D transformation and tried to solve it "the simple way" too, wasting nearly a month doing fool's work. 3D transformation is hard. There's no quick, dirty way to it, you can only do it the proper way. Grab a book (3D math primer is a good one) and spend time to learn the math if you truly want to do it.