1
\$\begingroup\$

I have a 3D car which follows a predefined 3D Bezier path. I want the car's front wheels' rotation to match the car's changing direction.

I had the idea to match the wheel's orientation to the derivative of the path's direction (3D vector), aka the 2nd degree derivative of the Bezier path.

For some reason, this barely works. At some point it seems to work fine, while at others the wheel spins like hell. I noted that the 2nd degree derivative changes even when the Bezier path is a straight line: AFAIK in this case it should be 0.

So, my 1st question is if my idea to match the wheel's rotation to the 2nd degree is the way to go. If yes, my 2nd question is what on earth is going wrong?

Here is my Bezier 3D curve code:

package fanlib.math {

import flash.geom.Vector3D;

public class BezierCubic3D
{
    public const anchor1:Vector3D = new Vector3D();
    public const anchor2:Vector3D = new Vector3D();
    public const control1:Vector3D = new Vector3D();
    public const control2:Vector3D = new Vector3D();
    /**
     * Gets values from both 'getPointAt' and 'getDirectionAt'
     */
    public const result:Vector3D = new Vector3D();
    private const previous:Vector3D = new Vector3D(); // temporary (optimization)

    // normalization aka arc-parameterization
    public var arcLengths:Vector.<Number> = new Vector.<Number>;
    public var steps:Number = 100;

    private var _length:Number;

    public function BezierCubic3D()
    {
    }

    /**
     * To get a point between anchor1 and anchor2, pass value [0...1]
     * @param t
     */
    public function getPointAt(t:Number):Vector3D {
        const t2:Number = t*t;
        const t3:Number = t*t2;
        const threeT:Number = 3*t;
        const threeT2:Number = 3*t2;
        result.x = getPointAxisAt(anchor1.x, anchor2.x, control1.x, control2.x, t3, threeT, threeT2);
        result.y = getPointAxisAt(anchor1.y, anchor2.y, control1.y, control2.y, t3, threeT, threeT2);
        result.z = getPointAxisAt(anchor1.z, anchor2.z, control1.z, control2.z, t3, threeT, threeT2);
        return result;
    }
    public function getPointAxisAt(a1:Number,a2:Number,c1:Number,c2:Number, t3:Number, threeT:Number, threeT2:Number):Number {
        return  t3      * (a2+3*(c1-c2)-a1) +
                threeT2 * (a1-2*c1+c2) +
                threeT  * (c1-a1) +
                a1;
    }

    /**
     * @param t
     * @return Un-normalized Vector3D! 
     */
    public function getDirectionAt(t:Number):Vector3D {
        const threeT2:Number = 3 * t * t;
        const sixT:Number = 6 * t;
        result.x = getDirAxisAt(anchor1.x, anchor2.x, control1.x, control2.x, threeT2, sixT);
        result.y = getDirAxisAt(anchor1.y, anchor2.y, control1.y, control2.y, threeT2, sixT);
        result.z = getDirAxisAt(anchor1.z, anchor2.z, control1.z, control2.z, threeT2, sixT);
        return result;
    }
    public function getDirAxisAt(a1:Number,a2:Number,c1:Number,c2:Number, threeT2:Number, sixT:Number):Number {
        return  threeT2 * (a2+3*(c1-c2)-a1) +
                sixT    * (a1-2*c1+c2) +
                3       * (c1-a1);
    }

    public function getDirectionDerivativeAt(t:Number):Vector3D {
        const sixT:Number = 6 * t;
        result.x = getDirDerAxisAt(anchor1.x, anchor2.x, control1.x, control2.x, sixT);
        result.y = getDirDerAxisAt(anchor1.y, anchor2.y, control1.y, control2.y, sixT);
        result.z = getDirDerAxisAt(anchor1.z, anchor2.z, control1.z, control2.z, sixT);
        return result;
    }
    public function getDirDerAxisAt(a1:Number,a2:Number,c1:Number,c2:Number, sixT:Number):Number {
        return  sixT    * (a2+3*(c1-c2)-a1) +
                6       * (a1-2*c1+c2);
    }

    /**
     * Call this after any change to defining points and before accessing normalized points of curve.
     */
    public function recalc():void {
        arcLengths.length = steps + 1;
        arcLengths[0] = 0;
        const step:Number = 1 / steps;

        previous.copyFrom(getPointAt(0));
        _length = 0;
        for (var i:int = 1; i <= steps; ++i) {
            _length += Vector3D.distance(getPointAt(i * step), previous);
            arcLengths[i] = _length;
            previous.copyFrom(result);
        }
    }

    /**
     * 'recalc' must have already been called if any changes were made to any of the defining points 
     * @param u
     * @return u normalized/converted to t
     */
    public function normalizeT(u:Number):Number {
        var targetLength:Number = u * arcLengths[steps];
        var low:int = 0,
            high:int = steps,
            index:int; // TODO : have a look-up table of starting low/high indices for each step!
        while (low < high) {
            index = low + ((high - low) >>> 1);
            if (arcLengths[index] < targetLength) {
                low = index + 1;
            } else {
                high = index;
            }
        }
        if (this.arcLengths[index] > targetLength) {
            --index;
        }
        var lengthBefore:Number = arcLengths[index];
        if (lengthBefore === targetLength) {
            return index / steps;
        } else {
            return (index + (targetLength - lengthBefore) / (arcLengths[index + 1] - lengthBefore)) / steps;
        }
    }

    public function getNormalizedPointAt(u:Number):Vector3D {
        return getPointAt(normalizeT(u));
    }

    /**
     * "Normalized" goes for t, not the return Vector3D!!! 
     * @param u
     * @return Un-normalized Vector3D!
     */
    public function getNormalizedDirectionAt(u:Number):Vector3D {
        return getDirectionAt(normalizeT(u));
    }

    public function getNormalizedDirectionDerivativeAt(u:Number):Vector3D {
        return getDirectionDerivativeAt(normalizeT(u));
    }

    public function get length():Number
    {
        return _length;
    }

}
}

And here is the code that applies the 2nd degree derivative orientation to the car's wheels:

            const dirDer:Vector3D = bezier.getDirectionDerivativeAt(time);
            dirDer.negate(); // negate vector's values; for some reason, this gives better results
            for each (wheel in dirWheels) {
                wheel.setRotation(0,0,0); // must nullify before below line
                const localDirDer:Vector3D = wheel.globalToLocalVector(dirDer); // convert dirDer vector to wheel's local axis; wheel translation does NOT affect conversion
                wheel.setOrientation(localDirDer); // orients the object in a specific direction; Up-vector's default value = (0,1,0) 
            }

I even tried (to no avail):

            for each (wheel in dirWheels) {
                const localDirDer:Vector3D = wheel.parent.globalToLocalVector(dirDer); // convert dirDer vector to wheel's local axis; wheel translation does NOT affect conversion
                wheel.setOrientation(localDirDer); // orients the object in a specific direction; Up-vector's default value = (0,1,0) 
            }

One clear example that something is wrong: even when the car is on a straight line, the wheel originally is non-rotated (as it should), but after the car passes the center point of the line, the wheel rotates 180 degrees! 1st OK 2nd wrong

\$\endgroup\$
1
\$\begingroup\$

I haven't digested all of your code. Consider the effect of using the center of the rear axle as the vehicle's origin and have that point ride the actual Bezier. As such, the car's forward direction is coincident with the tangent. Both of these relate to the path of a real car. The front wheels just need to always point directly toward their next-frame-location (t+0.0001).

If the Bezier is too "tight", the back wheels "break lose" from the pavement and the same effect occurs at the center of the front axle, instead of the back.

car bezier

Edit: Added control points/lines in red, as well as, first- and second-order derivatives for t=0.5. The car/wheel locations for t=0.5 are in blue.

When interpolating the car's location between magenta and cyan, also lerp the wheel rotation between magenta and cyan. The positive difference in 't' between magenta and cyan is arbitrary, since it will be updated every frame.

Edit2: I use an AutoCAD addon called AutoTurn to design driveways that can accommodate fire-truck and/or semi-truck turn radii; it makes similar paths to the one shown below, in yellow. It is an extremely specific path based on the vehicle's lock-angle, wheel-base, etc., etc. It is not even remotely parametric.

enter image description here

Interpolating using the direction between (normalizedT) and (normalizedT + 0.0001) should look great.

\$\endgroup\$
  • \$\begingroup\$ Yes, that's what I thought. I thought of using 2nd degree derivative for more accurate results (instead of approximating with next-frame-location (t+0.0001)). Maybe I have a bug there with my 2nd degree der function, but I can't see it. Or maybe I need to combine 1st and 2nd degree derivatives..? Dunno \$\endgroup\$ – Bill Kotsias Apr 24 '15 at 6:13
  • \$\begingroup\$ @BillKotsias, aren't you interpolating movement along the Bezier anyway, and by deltaTime (probably ~0.01666)? \$\endgroup\$ – Jon Apr 24 '15 at 6:42
  • \$\begingroup\$ Interpolating, no. I get the Bezier's exact position over time with getPointAt and assign that directly to the car's position. I don't use deltaTime but exact time values. \$\endgroup\$ – Bill Kotsias Apr 24 '15 at 6:47
  • \$\begingroup\$ @BillKotsias, do you suspect that the error is somewhere within the basis and/or derivative calculation? \$\endgroup\$ – Jon Apr 24 '15 at 7:15
  • \$\begingroup\$ After thinking about it, no, there can't be an error in the derivative calculation. I think my concept is wrong: the 2nd degree der shows how the direction changes, NOT where it goes. I think I should add the 2nd degree to the 1st degree to get the tires' direction. In the end, if I can't find out how to fix it, I'll just do it exactly as you describe! :-) \$\endgroup\$ – Bill Kotsias Apr 24 '15 at 8:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.