1
0
mirror of https://github.com/Pomax/BezierInfo-2.git synced 2025-08-10 18:54:23 +02:00

3d normals

This commit is contained in:
Pomax
2017-04-11 21:55:41 -07:00
parent 3343284d2d
commit 253eb14579
14 changed files with 338 additions and 260 deletions

View File

@@ -4,30 +4,38 @@ Before we move on to the next section we need to spend a little bit of time on t
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:
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: we also know the tangent at "some next moment in time", 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,
- **t'** = **t** + **a**, that (normalized) "tangent at the next 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}/>
The following graphic shows us some of those known and unknown vectors: when you mouse-over the graphic, you will see a point on the curve selected, showing **t** in green, **r** in blue, and **n** in red (note: all of them scaled to a uniform length). As you move across the graphic, you will see the point, and the corresponding vectors, move along the curve. Notice how the vectors rotate around the curve itself: by visualising them, we can see something that would otherwise be invisible to us: this curve twists!
<Graphic title="Some known and unknown vectors" setup={this.setup} 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}
r = \textit{normalize} \left ( t' \times t \right ) = \textit{normalize} \left ( \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} \right )
\]
(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?)
Note that the cross product does not yield a normalized vector, so we have to do this manually. We already saw how to do this in the above section, though:
\[
\textit{normalize}(v) = \frac{v}{\left \| v \right \|}
\]
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:
@@ -63,6 +71,8 @@ Which means computing:
\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.
And with that, we have the normal vector(s) we were looking for. 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}/>
So, let's look at the same graphic as above to see it all in action again, but this time with some projections turned on, so that you can see how different things are in 3D, compared to 2D: look at how the tangent and normal (and axis of rotation) change as you move along the curve in each projection: that doesn't look anything like what we'd see if we compute the normal purely in 2D!
<Graphic title="Appreciating 3D curve normals" setup={this.setup} draw={this.drawNormals}/>

View File

@@ -1,17 +1,15 @@
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());
},
var SHADOW_ALPHA = 0.2;
var SHOW_PROJECTIONS = true;
function normalize(v) {
var d = Math.sqrt(v.x*v.x + v.y*v.y + v.z*v.z);
return { x:v.x/d, y:v.y/d, z:v.z/d };
}
module.exports = {
drawCube: function(api) {
var prj = p => api.project(p, vectorOffset);
@@ -51,16 +49,18 @@ module.exports = {
api.drawLine(cube[0], cube[4]);
},
drawCurveProjection(api, curvepoints) {
drawCurve(api, curvepoints, project) {
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)) });
if (project) {
// projections
api.setColor(`rgba(0,0,0,${SHADOW_ALPHA})`);
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");
@@ -78,10 +78,71 @@ module.exports = {
api.drawCurve({ points: curve2d });
},
getVectors: function(d1curve, t) {
var dt, a, ddt, d, r, R, n;
// get the normalized tangent
dt = d1curve.get(t);
// and then let's work in the change in tangent
a = d1curve.derivative(t);
ddt = { x: dt.x + a.x, y: dt.y + a.y, z: dt.z + a.z };
// compute the crossproduct, and normalize it
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
};
d = Math.sqrt(r.x*r.x + r.y*r.y + r.z*r.z);
r = { x: r.x/d, y: r.y/d, z: r.z/d };
// compute the normal, which should not need renormalization
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
];
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[7] + dt.z * R[8]
};
return { dt, a, ddt, r, R, n };
},
drawVector: function(api, from, to, len, r,g,b, project) {
var prj = p => api.project(p, vectorOffset);
to = normalize(to);
to = {
x: from.x + len * to.x,
y: from.y + len * to.y,
z: from.z + len * to.z
};
api.setColor(`rgba(${r},${g},${b},1)`);
// draw the actual vector
api.drawLine(prj(from), prj(to));
if (project) {
// and the side projections.
api.setColor(`rgba(${r},${g},${b},${SHADOW_ALPHA})`);
api.drawLine(api.projectXY(from, vectorOffset), api.projectXY(to, vectorOffset));
api.drawLine(api.projectXZ(from, vectorOffset), api.projectXZ(to, vectorOffset));
api.drawLine(api.projectYZ(from, vectorOffset), api.projectYZ(to, vectorOffset));
}
},
setup: function(api) {
vectorOffset = {
x: 2 * api.getPanelWidth() / 5,
y: 4 * api.getPanelHeight() / 5
};
api.setSize(1.25 * api.getPanelWidth(),api.getPanelHeight());
},
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);
@@ -92,82 +153,56 @@ module.exports = {
{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]);
this.drawCurve(api, curvepoints);
// let's mark t
var curve = new api.Bezier(curvepoints);
var d1curve = new api.Bezier(curve.dpoints[0]);
var t = Math.max(api.hover.x? api.hover.x / api.getPanelWidth() : 0, 0);
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));
// draw the tangent, rotational axis, and normal
var vectors = this.getVectors(d1curve, t);
this.drawVector(api, mt, vectors.dt, 40, 0,200,0);
this.drawVector(api, mt, vectors.r, 40, 0,0,200);
this.drawVector(api, mt, vectors.n, 40, 200,0,0);
},
setupNormals: function(api) {
var curve = api.getDefault3DCubic();
normalsOffset = {
x: api.getPanelWidth() / 2,
y: api.getPanelHeight() / 2
x: 2 * api.getPanelWidth() / 5,
y: 4 * api.getPanelHeight() / 5
};
api.setCurve(curve);
api.setSize(1.25 * api.getPanelWidth(),api.getPanelHeight());
},
drawNormals: function(api, curve) {
drawNormals: function(api) {
api.reset();
var prj = p => api.project(p, vectorOffset);
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.drawCurve(api, curvepoints, SHOW_PROJECTIONS);
// let's mark t
var curve = new api.Bezier(curvepoints);
var d1curve = new api.Bezier(curve.dpoints[0]);
var t = Math.max(api.hover.x? api.hover.x / api.getPanelWidth() : 0, 0);
var mt = curve.get(t);
api.drawCircle(prj(mt), 3);
// draw the tangent, rotational axis, and normal
var vectors = this.getVectors(d1curve, t);
this.drawVector(api, mt, vectors.dt, 40, 0,200,0, SHOW_PROJECTIONS);
this.drawVector(api, mt, vectors.r, 40, 0,0,200, SHOW_PROJECTIONS);
this.drawVector(api, mt, vectors.n, 40, 200,0,0, SHOW_PROJECTIONS);
}
};