1
0
mirror of https://github.com/Pomax/BezierInfo-2.git synced 2025-08-22 16:23:12 +02:00

experimental tangents and normals

This commit is contained in:
Pomax
2017-04-11 20:38:43 -07:00
parent 97958c17ee
commit 3343284d2d
40 changed files with 601 additions and 133 deletions

View File

@@ -0,0 +1,68 @@
# Getting 3D normals
Before we move on to the next section we need to spend a little bit of time on the difference between 2D and 3D, because while for many things this difference is irrelevant and the procedures are identical (for instance, getting the 3D tangent is just doing what we do for 2D, but for x, y, and z, instead of just for x and y), when it comes to normals things are a little more complex, and thus more work. Mind you, it's not "super hard", but there are more steps involved and we should have a look at those.
Getting normals in 3D is in principle the same as in 2D: we need to take the normalised tangent vector, and then rotate it by a quarter turn. However, this is where things get that little more complex: we can turn in quite a few directions, so we need to restrict the rotation to the plane that the tangent lies on. That might sound strange: tangents are themselves lines and lines simultaneously lie on an infinite number of planes, so what's up with that?
Well, we know more about the tangent: we also know its rate of change. Think of the Bezier curve as the path of a car. The curve itself tells us the "place in space" at any given time, and the first derivative at any point tells us the "speed of the car at that point". However, we know more: the second derivative tells us the "accelleration of the car at that point", and if we add the accelleration to the velocity, we know where the car will be "if the curve stopped changing": as long as the curve we're dealing with is not degenerate (that is to say: it isn't actually a pure 2D curve that we simply rotated in 3D) then at any point in time we know two vectors in the same plane, with a third vector in that same plane, and a fourth vector perpendicular that we don't know yet:
- **t**, the (normalized) vector for the direction of travel at some point B(t),
- **a**, the difference vector between "the tangent here" to what "the tangent at the next point" would be,
- **t'** = **t** + **a**, that (normalized) "tangent at the next point", superimposed on the current point,
- **r**, a (normalized) vector aligned with the axis over which we can rotate **t** to overlap **t'**, and
- **n**, the normal at B(t).
<Graphic title="Known and unknown vectors" setup={this.setupVectors} draw={this.drawVectors}/>
All these vectors have the same origin (except for **a** but we only use that to find **t'**): our on-curve point. And that means we can quite easily compute the axis over which we need to rotate any of these vectors to overlap another. Since we know **t** and **t'**, we can compute that axis with some linear algebra, and then we're almost done, because as in the 2D case getting the normal is a question of rotating the (normalized) tangent by a quarter turn over our axis of rotation.
First up: we need to actually *find* that axis of rotation. As it turns out, this is quite easy: we just compute the [cross product](https://en.wikipedia.org/wiki/Cross_product#Mnemonic) of our two known vectors, and that will give us **r**:
\[
r = t' \times t = \begin{bmatrix}
t'_y \cdot t_z - t'_z \cdot t_y\\
t'_z \cdot t_x - t'_x \cdot t_z\\
t'_x \cdot t_y - t'_y \cdot t_x
\end{bmatrix}
\]
(Note that the order of operations matters for cross products: we compute **t'**×**t**, because if we compute **t**×**t'** we'll be computing the samerotation axis but represented by a vector in the opposite direction, so our final normal will actually be rotated a quarter turn "the wrong way". While correcting that is super easy, literally just taking our final normal and multiplying by -1, why correct after the fact what we can get it right from the start?)
Now we have everything we need: in order to turn our normalised tangent vectors into normal vectors, all we have to do is rotate them about the axes we just found by a quarter turn. If we turn them one way, we get normals, if we turn them the other, we get backfacing normals.
[Rotating about an axis is perhaps laborious, but not difficult](https://en.wikipedia.org/wiki/Rotation_matrix#Rotation_matrix_from_axis_and_angle), and much like in the 2D case, quarter turns in 3D greatly simplify the maths. To rotate a point a quarter turn over our rotation axis **r**, the rotation matrix is:
\[
R = \begin{bmatrix}
r^2_x & r_x \cdot r_y - r_z & r_x \cdot r_z + r_y \\
r_x \cdot r_y + r_z & r^2_y & r_y \cdot r_z - r_x \\
r_x \cdot r_z - r_y & r_y \cdot r_z + r_x & r^2_z
\end{bmatrix}
\]
So that's still easy: just tell the computer to evaluate those nine values, and all that's left is a matrix multiplication to get our 3D normal:
\[
n = R \cdot t
\]
Which means computing:
\[
n =
\begin{bmatrix}
n_x \\
n_y \\
n_z
\end{bmatrix}
=
\begin{bmatrix}
t_x \cdot R_{1,1} + t_y \cdot R_{1,2} + t_z \cdot R_{1,3} \\
t_x \cdot R_{2,1} + t_y \cdot R_{2,2} + t_z \cdot R_{2,3} \\
t_x \cdot R_{3,1} + t_y \cdot R_{3,2} + t_z \cdot R_{3,3}
\end{bmatrix}
\]
And we have the normal vector(s) we need. Perfect! And if we need backfacing normals, we can either effect those "from the start" by evaluating the cross product as **t**×**t'** as mentioned above, or we can multiply the normal vector we get here by -1.
<Graphic title="3D curve normals" setup={this.setupNormals} draw={this.drawNormals}/>

View File

@@ -0,0 +1,173 @@
var vectorOffset;
var normalsOffset;
module.exports = {
setupVectors: function(api) {
var curve = api.getDefault3DCubic();
vectorOffset = {
x: 2 * api.getPanelWidth() / 5,
y: 4 * api.getPanelHeight() / 5
};
api.setCurve(curve);
api.setSize(1.25 * api.getPanelWidth(),api.getPanelHeight());
},
drawCube: function(api) {
var prj = p => api.project(p, vectorOffset);
var cube = [
{x:0, y:0, z:0},
{x:200,y:0, z:0},
{x:200,y:200,z:0},
{x:0, y:200,z:0},
{x:0, y:0, z:200},
{x:200,y:0, z:200},
{x:200,y:200,z:200},
{x:0, y:200,z:200}
].map(p => prj(p));
// "most of the cube"
api.setColor("grey");
api.drawLine(cube[1], cube[2]);
api.drawLine(cube[2], cube[3]);
api.drawLine(cube[1], cube[5]);
api.drawLine(cube[2], cube[6]);
api.drawLine(cube[3], cube[7]);
api.drawLine(cube[4], cube[5]);
api.drawLine(cube[5], cube[6]);
api.drawLine(cube[6], cube[7]);
api.drawLine(cube[7], cube[4]);
// x axis
api.setColor("blue");
api.drawLine(cube[0], cube[1]);
// y axis
api.setColor("red");
api.drawLine(cube[3], cube[0]);
// z axis
api.setColor("green");
api.drawLine(cube[0], cube[4]);
},
drawCurveProjection(api, curvepoints) {
var prj = p => api.project(p, vectorOffset),
curve2d = curvepoints.map(p => prj(p)),
points;
// projections
api.setColor("#E0E0E0");
api.drawCurve({ points: curvepoints.map(p => api.projectXY(p, vectorOffset)) });
api.drawCurve({ points: curvepoints.map(p => api.projectYZ(p, vectorOffset)) });
api.drawCurve({ points: curvepoints.map(p => api.projectXZ(p, vectorOffset)) });
// control lines
api.setColor("#333");
api.drawLine(curve2d[0], curve2d[1]);
api.drawCircle(curve2d[1], 3);
api.drawCircle(curve2d[2], 3);
api.drawLine(curve2d[2], curve2d[3]);
// main curve
api.setColor("black");
api.drawCircle(curve2d[0], 3);
api.drawCircle(curve2d[3], 3);
var curve = new api.Bezier(curve2d);
api.drawCurve({ points: curve2d });
},
drawVectors: function(api) {
api.reset();
var prj = p => api.project(p, vectorOffset);
var t = api.hover.x? api.hover.x / api.getPanelWidth() : 0.35;
this.drawCube(api);
var curvepoints = [
{x:120,y:0,z:0},
{x:120,y:220,z:0},
{x:30,y:0,z:30},
{x:0,y:0,z:200}
];
this.drawCurveProjection(api, curvepoints);
var curve = new api.Bezier(curvepoints);
var d1curve = new api.Bezier(curve.dpoints[0]);
var d2curve = new api.Bezier(curve.dpoints[1]);
// let's mark t
var mt = curve.get(t);
api.drawCircle(prj(mt), 3);
// and let's show the tangent at that point
var dt = d1curve.get(t);
var pt1 = { x: mt.x + dt.x, y: mt.y + dt.y, z: mt.z + dt.z };
// and then let's work in the change in tangent
var roc = d2curve.get(t);
var f = 10;
var d = Math.sqrt(roc.x*roc.x + roc.y*roc.y + roc.z*roc.z);
roc = { x: f * roc.x/d, y: f * roc.y/d, z: f * roc.z/d };
var pt2 = { x: mt.x + dt.x + roc.x, y: mt.y + dt.y + roc.y, z: mt.z + dt.z + roc.z };
api.drawLine(prj(mt), prj(pt1));
api.drawLine(prj(mt), prj(pt2));
// normalize t (=dt) and t' (=ddt) and compute the crossproduct:
roc = d2curve.get(t);
var ddt = { x: dt.x + roc.x, y: dt.y + roc.y, z: dt.z + roc.z };
d = Math.sqrt(dt.x*dt.x + dt.y*dt.y + dt.z*dt.z);
dt = { x: dt.x/d, y: dt.y/d, z: dt.z/d };
d = Math.sqrt(ddt.x*ddt.x + ddt.y*ddt.y + ddt.z*ddt.z);
ddt = { x: ddt.x/d, y: ddt.y/d, z: ddt.z/d };
var r = {
x: ddt.y * dt.z - ddt.z * dt.y,
y: ddt.z * dt.x - ddt.x * dt.z,
z: ddt.x * dt.y - ddt.y * dt.x
};
f = 20;
var mc = {
x: mt.x + f * r.x,
y: mt.y + f * r.y,
z: mt.z + f * r.z
};
// let's see the cross product
api.setColor("darkgreen");
api.drawLine(prj(mt), prj(mc));
// and finally, compute the normal
var R = [
r.x*r.x, r.x*r.y -r.z, r.x*r.z + r.y,
r.x*r.y + r.z, r.y*r.y, r.y*r.z - r.x,
r.x*r.z - r.y, r.y*r.z + r.x, r.z*r.z
];
var n = {
x: dt.x * R[0] + dt.y * R[1] + dt.z * R[2],
y: dt.x * R[3] + dt.y * R[4] + dt.z * R[5],
z: dt.x * R[6] + dt.y * R[6] + dt.z * R[7]
};
var mr = {
x: mt.x + f * n.x,
y: mt.y + f * n.y,
z: mt.z + f * n.z
};
api.setColor("red");
api.drawLine(prj(mt), prj(mr));
},
setupNormals: function(api) {
var curve = api.getDefault3DCubic();
normalsOffset = {
x: api.getPanelWidth() / 2,
y: api.getPanelHeight() / 2
};
api.setCurve(curve);
},
drawNormals: function(api, curve) {
api.reset();
}
};