[AE表达式]使用createPath创建与控制曲线_Part.3

·这篇文章来介绍下用AE生成程序曲线,然后制作可以正确啮合的齿轮形状,没了解createPath这个函数的可以先看下前两篇文章
createPath基本用法:

createPath切线使用方法:

程序曲线就是利用曲线的坐标公式直接在AE中生成它的形状,比如AE自带的椭圆,矩形,多边形都是程序曲线,我们直接调节它的参数就能控制它的形状而不必自己调点

想要自定义程序曲线的话需要能代表曲线的公式,但是曲线公式分一般方程和参数方程两种,关于参数方程可以先百度一下看看定义

用最简单的圆来说,圆的普通方程是x²+y²=r²,我们是无法用这个方程来连续生成圆上每个点的,但圆还有其他的表示方式:

如果用圆的参数方程就可以用θ一个值来表示圆上θ处的点的坐标:

(这个动画是我用AE生成的所以Y轴坐标也是反的)
当然我们在不加入切线的情况下不会生成这么圆滑的圆的,因为曲线本身是无限个点的集合,但我们只能使用有限的点来表示曲线,当然这个问题用切线也很好解决,后面再说
现在我们来手动生成一个圆试试,先观察圆的参数方程,注意到我们需要的是r和θ两个参数,r代表圆的半径,θ是在它某角度上的点
创建空的形状层,添加一个path和一个strok,在path的表达式中先定义一个需要两个参数的函数
function circle(r, theta) {
var x = r * Math.cos(theta);
var y = r * Math.sin(theta);
return [x, y];
}
然后我们在这个形状层上建立一个滑块控制,重命名为r,把值改成200,建立一个变量拉皮筋连给r这个滑块,暂时先写成下面这样

然后我们通过这个circle函数生成四个点,分别是0°,90°,180°,270°四个角度的点,把这四个点装到一个数组里,然后用这四个数先来生成曲线看一下
var P1 = circle(r, 0);
var P2 = circle(r, 90);
var P3 = circle(r, 180);
var P4 = circle(r, 270);
var CVS = [P1, P2, P3, P4];
createPath(points=CVS, inTangents=[], outTangents=[], is_closed=true);

虽然生成了四个点,但是好像并不是我们需要的四个点,这是因为我们输入的是角度值,而cos和sin两个函数需要的是弧度值,我们可以用degreesToRadians()这个函数来把角度转化成弧度(同样弧度转角度就用radiansToDegrees())
var P1 = circle(r, degreesToRadians(0));
var P2 = circle(r, degreesToRadians(90));
var P3 = circle(r, degreesToRadians(180));
var P4 = circle(r, degreesToRadians(270));
将输入的数值用这个函数像上面这样转化一下,再看一下生成的曲线

这回生成了我们需要的对应四个角度的点,此时如果加入切线的话很容易就把它变成圆形了,不过这个不是重点,我们先试着增加点的数量来使曲线更接近圆形
如果我们想自己控制点的数量可以使用for循环把一定数量的点直接传递给数组中
先在图层上建立个滑块控制并命名为number of point, 用来控制点的数量,再建个滑块控制命名为step用来控制每两个点的参数相差多少

把这两个参数传入表达式,然后用for循环批量生成点,具体写法如下
function circle(r, theta) {
var x = r * Math.cos(theta);
var y = r * Math.sin(theta);
return [x, y];
}
var r = effect("r")("Slider");
var Numpt = effect("number of point")("Slider");
var step = effect("step")("Slider");
var CVS = new Array();
for(i = 0; i < Numpt; i++) {
var P = circle(r, degreesToRadians(i * step));
CVS.push(P);
}
createPath(points=CVS, inTangents=[], outTangents=[], is_closed=true);

现在我们可以自己控制每隔step个角度来生成numpt个点,但是这样的控制方式不如人意,如果我们要生成一个完整的圆,只有在step和numpt相乘为360的时候才可以,我希望这个圆一直是完整的,numpt还是点的数量,并且会自动调控step让所有点完成一个完整的圆,所以需要优化一下逻辑
我们不需要自己控制step,而是让step等于360除以点的数量,所以把表达式优化为
function circle(r, theta) {
var x = r * Math.cos(theta);
var y = r * Math.sin(theta);
return [x, y];
}
var r = effect("r")("Slider");
var Numpt = effect("number of point")("Slider");
var step = 2 * Math.PI / Numpt;
var CVS = new Array();
for(i = 0; i < Numpt; i++) {
var P = circle(r, i * step);
CVS.push(P);
}
createPath(points=CVS, inTangents=[], outTangents=[], is_closed=true);
这样就不需要自己控制step了,并且由于我们直接用弧度2π除以点数,得到的也是弧度值,所以也不需要转换角度的函数

目前来看貌似我们只是做了一个可控制边数的正多边形而已,AE本身就能调用多边形,但其实我们创建了一个可以自定义曲线的方式,现在要得到不同的程序曲线只要更改相应的曲线函数就可以了
比如这个外旋轮线

它的意思是一个小圆在大圆上滚动,圆内的某一点在小圆滚动过程中形成的路径曲线(实际上点不需要在小圆内部),那么根据它的定义,需要四个参数,R是大圆的半径,r是小圆的半径,d是形成路径的点距离小圆中心的距离,t就是我们需要生成曲线用的通用参数
(使用圆的时候使用theta作为参数比较便于理解,为了书写方便,后面定义函数参数的时候theta就直接写t了)
那么我们建立几个滑块控制分别对应R,r,d,再加上一个控制点数的滑块,然后把上面我们生成圆的表达式中的函数按照百科中的写的抄成表达式语言,记得for循环中调用的函数也要相应的改成我们改过的曲线函数,改完如下:
function curve(R, r, d, t) {
var x = (R + r) * Math.cos(t) - d * Math.cos((R + r) * t/ r);
var y = (R + r) * Math.sin(t) - d * Math.sin((R + r) * t / r);
return [x, y];
}
var Numpt = effect("number of point")("Slider");
var R = effect("R")(1);
var r = effect("r")(1);
var d = effect("d")(1);
var step = 2 * Math.PI / Numpt;
var CVS = new Array();
for (i = 0; i < Numpt; i++) {
var P = curve(R, r, d, i * step);
CVS.push(P);
}
createPath(points = CVS, inTangents = [], outTangents = [], is_closed = true);

根据不同的参数可以生成不同的样式,但是由于它的曲线特性,只有R是r的整数倍的时候才能完整的让t在0~π中完美衔接,当然也可以改范围让它多转几圈

比如当我设置为R=200,r=120,d=200时,t的范围要从0~6π才能让这个曲线顺利首尾相接
而且对于很多不闭合的曲线来说,随着t不断增大,曲线是无限延展的,所以我们继续改进一下表达式,让它可以延展下去
比如这个阿基米德螺旋线

首先按照现有的表达式直接替换函数(注意α,β是控制形状的参数,θ是theta,也就是t)

这是t在0~2π范围生成的曲线,而它还可以继续延展,并且由于是开放曲线,不需要把is_closed属性设置为true,首先我们先建立一个checkbox control(中文忘了,也在表达式控制那栏里),命名为Closed,把这个参数传入,输入给is_closed,这样我们可以直接使用这个按钮来控制曲线是开放还是闭合的

然后再建立一个滑块控制命名为Endt,用这个值来除以点数就可以自由控制t的结束值是多少了,我为了让这个值是100的时候t的值正好是2π,所以把他又做了个乘法映射,最终写法如下:
function curve(alpha, beta, t) {
var x = (alpha + beta * t) * Math.cos(t);
var y = (alpha + beta * t) * Math.sin(t);
return [x, y];
}
var Numpt = effect("number of point")("Slider");
var a = effect("alpha")(1);
var b = effect("beta")(1);
var step = effect("Endt")(1) / Numpt * 0.2 / Math.PI;
var CVS = new Array();
for (i = 0; i < Numpt; i++) {
var P = curve(a, b, i * step);
CVS.push(P);
}
var Closed = effect("Closed")(1);
createPath(points = CVS, inTangents = [], outTangents = [], is_closed = Closed);

这回我们可以自由设置曲线延展的范围了
设置好之后我们可以多试一些有趣的曲线试试看
抛物线:

摆线(内旋线和外旋线其实就是摆线的一种):

渐开线:

渐开线这个很重要,是齿轮上的主要参数线
心形曲线:

曲线是倒的,因为AE坐标是Y轴向下的,所以可以把return成[x, -y]来修正它(当然你也可以直接Y轴缩放图层)

关于心形曲线有很多不同的样式,可以百度搜一下,我是参考的这篇文章
https://blog.csdn.net/stereohomology/article/details/51581391

然后我在这个视频中看到这样一个很有意思的心形曲线[数学之美]看完你会爱上数学的~谨以此片致敬高质量科普工作者们(manim制作)

这个曲线会随着系数a的增加使函数图像越来越像心形,虽然它不是参数方程,但是我们可以令x=t来强行把它定义成参数方程
实际上它是一个设置了Y轴坐标映射的正弦函数,它的有效定义域是-2√2 ~ 2√2,我在这个函数最两端生成两个点,中间的点都落在正弦函数的的π/2的倍数上(也就是πax,但这只能对应到正弦部分的峰值,不能对应到整个函数的峰值),所以算起x的点分布还是有点绕,具体生成方法在下面
//Controllable parameters
var size = effect("size")(1); //Total size
var n = effect("a")(1); //coefficient a
//Main function of curve
function curve(s, a, t) {
var x = t;
var y = Math.pow(Math.pow(x, 2), (1 / 3)) + 0.9 * Math.sqrt(8 - Math.pow(x, 2)) * Math.sin(Math.PI * a * x);
return [x, -y] * s;
}
//Derive parameters
var step1 = 1 / (2 * n); //x step with equal spacing
var step2 = (2 * Math.SQRT2 - 2) % step1; //x step in both sides
var numptConsOneSide = n * 4 + Math.floor((2 * Math.SQRT2 - 2) / step1); // The count of points with equal spacing in on side
var numpt = numptConsOneSide * 2 + 3; //tatal count of points
var posiPT = new Array();
for (i = 1; i <= numptConsOneSide; i++) {
posiPT.push(i * step1);
}
var negaPT = posiPT.map(p => {
return -p;
}).reverse();
var pt = [-(2 * Math.SQRT2 - 0.00000001)].concat(negaPT, [0], posiPT, [(2 * Math.SQRT2 - 0.00000001)]);
var CVS = new Array();
for (i = 0; i < numpt; i++) {
var P = curve(size, n, pt[i]);
CVS.push(P);
}
var Closed = effect("Closed")(1);
createPath(points = CVS, inTangents = [], outTangents = [], is_closed = Closed);
仔细看的话就是随着a的增加,sin函数会让曲线的波长变短,使曲线的局部峰值数量变多

设定x取值的细节的话就是定义一个numptConsOneSide变量,让它算出在0~2√2范围内能容纳多少个sin函数能取到π/2整数倍的值,然后把这些值放入posiPT这个集合中,再把posiPT中每个值乘以-1,然后反转一下数组,装到negaPT,作为0~-2√2范围的点,然后把这两个数组和-2√2、0、2√2这三个值按照大小排列再连接起来,就是整个曲线上需要取到的x的值,只要把这些x传递给t求出y就可以了
你懵了吗?反正我是挺懵的

所以我做了个动画演示一下x的取值过程

需要注意的是由于表达式的精度问题,我们的值不能精确的取到-2√2和2√2这两个点,因为它在实际计算的时候用的是很接近这两个值的小数,所以我把他们减去0.00000001来修正这个计算机误差,中间有多少个0是无所谓的,只是为了让这个数稍微偏移一下

然后我们要面对一个问题,就是贝塞尔曲线的优势是可以用少量的点描述一个比较复杂的曲线,为此要在AE中为它设置手柄,之前我们完全是靠增加点的数量来使曲线尽量显示得平滑的,虽然我们可以在曲线上点击右键,选择rotoBezier,但是这种方式获得的平滑曲线在点数较少的情况下非常不精确,如果为了精确度再增加曲线点的话就没有意义了

所以我们要让程序自动计算函数的切线,而函数的导数就代表了函数曲线的切线,这个在百度上就能查到

最简单的圆的函数时x=sinθ, y=cosθ
根据导数公式,圆的导函数就是x=cosθ,y=-sinθ
那么我们定义一个圆的函数,再定义一个圆的导函数,把导函数的值加上圆的函数,得出的值和原函数得出的值作为数组传递给createPath的point属性,顺便把这条线的长度乘以一个系数,不然在AE中会因为太短看不见
function circle(r, theta){
x = Math.sin(theta);
y = Math.cos(theta);
return [x, y] * r;
}
function dCircle(theta) {
x = Math.cos(theta);
y = -Math.sin(theta);
return [x, y];
}
var r = effect("r")(1);
var sLength = effect("Length Multi")(1) * 0.1;
var theta = effect("theta")(1);
var pos = circle(r, degreesToRadians(theta));
var tan = pos + dCircle(degreesToRadians(theta)) * sLength;
createPath(points = [pos, tan], inTangents = [], outTangents = [], is_closed = 0);

通过导数,我们可以获得任意theta值上的圆的点的切线方向
同样,我们能得到已知公式的曲线上任意一点的切线方向,那么我们来试试正弦曲线,先按照之前的方法生成一个基于点数量的正弦函数曲线表达式
function toPara(r, t) {
var x = t;
var y = Math.sin(t);
return [x, -y] * r;
}
var ptArray = new Array();
var numpt = effect("Points Amount")(1);
var size = effect("Size")(1);
var closure = effect("Close")(1);
var sLength = effect("Length Multi")(1) * 0.1;
if (numpt > 0) {
for (i = 0; i < numpt; i++) {
step = 2 * Math.PI / (numpt);
ptArray.push(toPara(size, (step * i * sLength)));
}
} else {
ptArray.push([0, 0]);
}
createPath(points = ptArray, inTangents = [], outTangents = [], is_closed = closure);


当点数量不够多的时候,我们只能得到一段棱角分明的线,然后新定义一个函数,根据导数公式,导函数写法如下
function dPara(l, t) {
var dx = 1;
var dy = Math.cos(t);
return [dx, -dy] * l;
}
它的第一个参数l,也是为了加长切线长度设置的,我们可以新建一个滑块控制把参数传入l,这样可以手动控制切线大小,然后同样使用for循环把切线装入数组中传递给createPath的outTangents属性中,完整写法如下
function toPara(r, t) {
var x = t;
var y = Math.sin(t);
return [x, -y] * r;
}
function dPara(l, t) {
var dx = 1;
var dy = Math.cos(t);
return [dx, -dy] * l;
}
var ptArray = new Array();
var numpt = effect("Points Amount")(1);
var size = effect("Size")(1);
var closure = effect("Close")(1);
var sLength = effect("Length Multi")(1) * 0.1;
var tanLen = effect("Tangent Length")(1);
for (i = 0; i < numpt; i++) {
step = 2 * Math.PI / (numpt);
ptArray.push(toPara(size, (step * i * sLength)));
}
var outTans = new Array();
for (i = 0; i < numpt; i++) {
step = 2 * Math.PI / (numpt);
outTans.push(dPara(tanLen, (step * i * sLength)));
}
createPath(points = ptArray, inTangents = [], outTangents = outTans, is_closed = closure);

现在每个曲线点上有正确的切线方向了,但是只有出切线,入切线直接把出切线反向就可以了,所以定义一个入切线的变量,把出切线的数组全部映射成负的传进来
function flipTan(p) {
return -p;
}
var inTans = outTans.map(flipTan);
使用这个map()方法可以把一个数组的所有元素全部用括号中的函数计算一遍输出出来,使用方法就是定义一个函数,把这个函数名放到map()中作为属性,输出出来的就是经过函数计算过的数组了,它还有一种写法是这样的
var inTans = outTans.map(p => {
return -p;
});
关于这个p => {}是javascript的箭头函数,它可以在括号内部直接写函数而不用在外部定义,如果是一个比较简单的算法可以直接用下面这种写法
写好之后把入切线的数组给到createPath的inTangents属性,再来调节下切线的长度来看看

现在我们通过很少的点就生成了一条足够平滑的正弦函数(不要把切线点当成曲线点了哦,实际上我们的点只有12个)
要说明的是贝塞尔曲线无法完全精确的还原函数曲线的真实值,只能通过调节点之间的插值来拟合,比如圆形这种简单的图形用贝塞尔曲线就是无法完全拟合的,即使我们在软件中看到的圆已经非常近似圆,但是它并不是真正的圆,有非常小的误差值,当然这个误差值我们肉眼几乎不可见
所以在制作程序化曲线的时候我们也只能尽量拟合原曲线,在点数和精确度之间找到一个平衡来用最少的点描述尽量精确的线
由于正弦函数的特点,我们可以精确的把点放到它的峰值、峰谷、“山腰”上,也就是让sin的取值点总是π/2的倍数,具体写法如下
function sinCurve(s, a, t) {
var x = Math.PI * t;
var y = Math.sin(Math.PI * a * t);
return [x, y] * s;
}
function dSin(s, a, t) {
dy = Math.PI * a * Math.cos(Math.PI * a * t);
return [Math.PI, dy] * s;
}
var size = effect("size")(1);
var a = effect("a")(1);
var tanLen = effect("Tangents length")(1) / a;
var numpt = 4 * a + 1;
var CVS = new Array();
for (i = 0; i < numpt; i++) {
var t = -1 + i / (2 * a);
CVS.push(sinCurve(size, a, t));
}
var outTans = new Array();
for (i = 0; i < numpt; i++) {
var t = -1 + i / (2 * a);
outTans.push(dSin(tanLen, a, t));
}
var inTans = outTans.map(p => {
return -p;
});
createPath(points = CVS, inTangents = inTans, outTangents = outTans, is_closed = 0);

我们在sin中安排了一个系数a来控制曲线有多少个正弦循环,并且把切线的长度除以a,这样在a增加的时候切线长度会相应的减少来适配调节后的曲线,并且在a为0.5的倍数的时候,曲线的点会精确的落在正弦曲线在π/2的取值点上
然后我们可以来测试一下其他的曲线,比如上面那个阿基米德螺旋线,我们用了240个点才生成了近似平滑的形状

现在我们添加导数公式
function curve(alpha, beta, t) {
var x = (alpha + beta * t) * Math.cos(t);
var y = (alpha + beta * t) * Math.sin(t);
return [x, y];
}
function dCurve(alpha, beta, t) {
var dx = beta * Math.cos(t) - (alpha + beta * t) * Math.sin(t);
var dy = beta * Math.sin(t) + (alpha + beta * t) * Math.cos(t);
return [dx, dy];
}
var Numpt = effect("number of point")("Slider");
var a = effect("alpha")(1);
var b = effect("beta")(1);
var tanLen = 10 * effect("Tangents Length")(1) / b / Numpt;
var step = effect("Endt")(1) / Numpt * 0.2 / Math.PI;
var CVS = new Array();
for (i = 0; i < Numpt; i++) {
var P = curve(a, b, i * step);
CVS.push(P);
}
var outTans = new Array();
for (i = 0; i < Numpt; i++) {
var P = dCurve(a, b, i * step) * tanLen;
outTans.push(P);
}
var inTans = outTans.map(p => {
return -p;
});
var Closed = effect("Closed")(1);
createPath(points = CVS, inTangents = inTans, outTangents = outTans, is_closed = Closed);

这次同样的大小与螺旋圈数的情况下(实际上由于是开放曲线所以稍微少了一段,因为这个表达式本来是基于闭合曲线改的,首尾不相连的情况下会少一个点,但是这不重要),我们只用了16个点就生成了这个曲线
我把两条曲线同时显示出来,看一下拟合程度


我把不带切线的螺旋线隐藏起来再选中它的图层,这样在视图中只显示它的点
白线是用切线生成的螺旋线,放大之后是可以看到有轻微的偏差的,这个偏差大小可以通过控制点数和切线的大小来调节,使它尽量逼近原曲线
我就不在继续深入研究如何获得最大的拟合度了,一来是网上有很多计算获得最大拟合度的方法的文章,二来做个动画没必要搞得如此精确,三来我是真滴不会了

然后我们可以把那个心形的曲线计算一下切线,它的切线公式是这样的

导数这种东西当然是早就忘了,复合函数导数算法我还是现查的,不过现在有很多可以符号计算的东西帮助我们算导数,比如python有个sympy库就可以直接用,但要是不太复杂的话就手算一下也可以,要是实在不会算的可以去找算卦的师傅算

关于这个导数要注意的是有小数次幂的情况下它是不能算出来负数区间的导数的,但是从图像上很容易就看出来x^(2/3)这段曲线的负数部分就是正数部分的镜像,导数也是一样的镜像,但是由于它的导数是(2/3)*x^(-1/3),表达式不认这个函数有负区间

所以在写这个导数的时候要做一个判断,让x<0的时候x^(2/3)的导数为-(2/3)*(-x)^(-1/3),意思就是让它计算对应正区间的导数,然后把得到的数值乘以负数来得到正确的负区间的导数,另外再加一个当x=0时候的情况,单独设置这个点的导数为0
然后这个函数的导函数就写成了这个样子
function dCurve(s, a, t) {
var x = t;
if (x > 0.000001) {
var y = (2 / 3) * Math.pow(x, (-1 / 3)) + 0.9 * Math.PI * a * Math.sqrt(8 - x ** 2) * Math.cos(Math.PI * a * x) - 0.9 * x * Math.pow((8 - x ** 2), -0.5) * Math.sin(Math.PI * a * x);
return [1, -y] * s * Math.pow(length([x, y]), -1);
} else if (x < -0.000001) {
var y = -(2 / 3) * Math.pow(-x, (-1 / 3)) + 0.9 * Math.PI * a * Math.sqrt(8 - x ** 2) * Math.cos(Math.PI * a * x) - 0.9 * x * Math.pow((8 - x ** 2), -0.5) * Math.sin(Math.PI * a * x); //Horizontal mirroring the positive derivative curve
return [1, -y] * s * Math.pow(length([x, y]), -1);
} else {
return [0, 0]; //If x is 0
}
}
写好之后最终这个函数被改成这样了,其实很多是我加的注释

看起来好像很复杂,其实还好啦,为了防止浮点误差,我把判断x是否大于小于0的判断写成了与±0.000001进行比较

//Controllable parameters
var size = effect("size")(1); //Total size
var n = effect("a")(1); //coefficient a
var tanLeng = effect("Tangents length")(1) / n; //The Length of tangents decrease as a increases
//Main function of curve
function curve(s, a, t) {
var x = t;
var y = Math.pow(Math.pow(x, 2), (1 / 3)) + 0.9 * Math.sqrt(8 - Math.pow(x, 2)) * Math.sin(Math.PI * a * x);
return [x, -y] * s;
}
//Derivative of the curve, need to determin the interval if the x is a negative number or 0
function dCurve(s, a, t) {
var x = t;
if (x > 0.000001) {
var y = (2 / 3) * Math.pow(x, (-1 / 3)) + 0.9 * Math.PI * a * Math.sqrt(8 - x ** 2) * Math.cos(Math.PI * a * x) - 0.9 * x * Math.pow((8 - x ** 2), -0.5) * Math.sin(Math.PI * a * x);
return [1, -y] * s * Math.pow(length([x, y]), -1);
} else if (x < -0.000001) {
var y = -(2 / 3) * Math.pow(-x, (-1 / 3)) + 0.9 * Math.PI * a * Math.sqrt(8 - x ** 2) * Math.cos(Math.PI * a * x) - 0.9 * x * Math.pow((8 - x ** 2), -0.5) * Math.sin(Math.PI * a * x); //Horizontal mirroring the positive derivative curve
return [1, -y] * s * Math.pow(length([x, y]), -1);
} else {
return [0, 0]; //If x is 0
}
}
//Derive parameters
var step1 = 1 / (2 * n); //x step with equal spacing
var step2 = (2 * Math.SQRT2 - 2) % step1; //x step in both sides
var numptConsOneSide = n * 4 + Math.floor((2 * Math.SQRT2 - 2) / step1); // The count of points with equal spacing in on side
var numpt = numptConsOneSide * 2 + 3; //tatal count of points
var posiPT = new Array();
for (i = 1; i <= numptConsOneSide; i++) {
posiPT.push(i * step1);
}
var negaPT = posiPT.map(p => {
return -p;
}).reverse();
var pt = [-(2 * Math.SQRT2 - 0.00000001)].concat(negaPT, [0], posiPT, [(2 * Math.SQRT2 - 0.00000001)]);
var CVS = new Array();
for (i = 0; i < numpt; i++) {
var P = curve(size, n, pt[i]);
CVS.push(P);
}
var outTans = new Array();
for (i = 0; i < numpt; i++) {
var tan = dCurve(tanLeng, n, pt[i]) * linear(Math.pow(-1, i + 1), -1, 1, 4, 1);
outTans.push(tan);
}
var inTans = outTans.map(p => {
return -p;
});
var Closed = effect("Closed")(1);
createPath(points = CVS, inTangents = inTans, outTangents = outTans, is_closed = Closed);
但这个切线在某些地方算出来的导数数值过大,所以我把最终的切线又做了一个映射让太长的切线自动收成短的,虽然最终效果还是不太理想,但是勉强过得去吧
其他正常的参数方程我测试之后的效果都还可以


最后我们来解决齿轮的啮合问题
首先我们要知道,齿轮是一种工业零件,有很严格的参数要求,如果你想做一个正常的齿轮啮合动画,像这种图片里的简单齿轮是肯定不行的,它只不过是“看起来像齿轮”的东西

真正的齿轮为了保证传动稳定,传动效率高,有模数、压力角等一系列标准参数,只有满足参数的齿轮才能正确的啮合在一起,我找了一张网上的啮合原理动图

标准齿轮的啮合曲面是个渐开线,啮合过程中接触位置的运动方向和面法线方向要保持一个稳定的角度......
写到这里的时候我看了一下右下角已输入字数

我决定还是直接贴代码结束这篇又臭又长的文章吧

首先建立这几个控制选项

然后建立空path,在其中输入一下表达式
/*
Standard gear parameters:
z = 17~40, α = 20°, in some occasions, α=14.5°, 15°, 22.50° and 25°
*/
//Basic parameter m, z, alpha
var m = effect("Module m")(1); //Module
var z = Math.round(effect("Teeth z")(1)); //Number of teeth
var alpha = effect("Pressure Angle alpha")(1); //The pressure angle
//Derived parameter d, da, df, db
var d = m * z; //Pitch diameter
var da = m * (z + 2); //Tip circle diameter
var df = m * (z - 2.5); //Root circle diameter
var db = d * Math.cos(degreesToRadians(alpha)); //Base circle diameter
//Involute Formula
function Involute(r, theta) {
var x = r * Math.cos(theta) + (theta) * r * Math.sin(theta);
var y = r * Math.sin(theta) - (theta) * r * Math.cos(theta);
return [x, y];
}
//The derivative of involute
function dInvolute(r, theta) {
var dx = theta * Math.cos(theta);
var dy = theta * Math.sin(theta);
return [dx, dy];
}
//Calculate intersection of involute and circle
function calInter(j, r) {
var t = Math.sqrt(Math.pow((r / j), 2) - 1);
return t;
}
//Point on base circle
var pointBC = [db * 0.5, 0];
//Point on tip circle
var pointTC = Involute(db * 0.5, calInter(db * 0.5, da * 0.5));
//Point interpolation in the BC and TC
var pointHalfTC = Involute(db * 0.5, 0.7 * calInter(db * 0.5, da * 0.5));
//Point on pitch circle
var pointPC = Involute(db * 0.5, calInter(db * 0.5, d * 0.5));
//The function calculate rotated point
function rotateP(P, theta) {
var x = P[0];
var y = P[1];
var x0 = x * Math.cos(theta) - y * Math.sin(theta);
var y0 = x * Math.sin(theta) + y * Math.cos(theta);
return [x0, y0];
}
/*Rotate pointPC by π divide z*2,
and Get symmetry axis point*/
mirrorP = rotateP(pointPC, Math.PI / (z * 2));
//Symmetrical point of point A about a straight line passing through two points L1, L2
function symmetryP(P1, L1, L2) {
var x = P1[0];
var y = P1[1];
var A = L2[1] - L1[1];
var B = L1[0] - L2[0];
var C = -1 * A * L1[0] - B * L1[1];
var x0 = x - 2 * A * ((A * x + B * y + C) / (Math.pow(A, 2) + Math.pow(B, 2)));
var y0 = y - 2 * B * ((A * x + B * y + C) / (Math.pow(A, 2) + Math.pow(B, 2)));
return [x0, y0];
}
//Point on root circle, and round point
var pointRC = pointBC * df / db;
var Rround = clamp(effect("Roof Round")(1), 0, 80) * 0.01;
var roundAngle = (db - df) * Rround / df;
var pointRCV = pointRC * ((db - df) * Rround + df) / df;
var pointRCU = rotateP(pointRC, -roundAngle);
//Teeth point Array, mirror all points and copy around origin
var halfUnitPointsSet = [pointRCU, pointRCV, pointBC, pointHalfTC, pointTC];
var unitPointsSet = halfUnitPointsSet.concat(halfUnitPointsSet.map(p => {
return symmetryP(p, [0, 0], mirrorP);
}).reverse());
var totalPointsSet = new Array();
for (var i = 0; i < z; i++) {
totalPointsSet = totalPointsSet.concat(unitPointsSet.map(p => {
return rotateP(p, i * 2 * Math.PI / z);
}));
}
//Set tangents
function TanOfCP(cp) {
return normalize([cp[1], -cp[0]]);
}
var magicNumber = (4 / 3) * (Math.SQRT2 - 1);
var inTanRCV = [-(db - df) * Rround * 0.5 * magicNumber, 0];
var outTanRCV = [linear(Rround, 0, 1, (db - df) * 0.1, 0), 0];
var inTanRCU = TanOfCP(pointRCU) * length(outTanRCV);
var outTanRCU = -TanOfCP(pointRCU) * length(inTanRCV);
var inTanBC = [-outTanRCV[0], 0];
var outTanBC = [m * 0.2, 0];
var inTanHalfTC = -dInvolute(db * 0.5, 0.7 * calInter(db * 0.5, da * 0.5)) * m * 0.7;
var outTanHalfTC = -inTanHalfTC * 1.25;
var inTanTC = -dInvolute(da * 0.5, calInter(db * 0.5, da * 0.5)) * m * 0.5;
var outTanTC = -TanOfCP(pointTC) * m * 0.2;
//Tangents Array
var halfUnitIntangentsSet = [inTanRCU, inTanRCV, inTanBC, inTanHalfTC, inTanTC];
var halfUnitOuttangentsSet = [outTanRCU, outTanRCV, outTanBC, outTanHalfTC, outTanTC];
var unitInTangentsSet = halfUnitIntangentsSet.concat(halfUnitOuttangentsSet.map(p => {
return symmetryP(p, [0, 0], mirrorP);
}).reverse());
var unitOutTangentsSet = halfUnitOuttangentsSet.concat(halfUnitIntangentsSet.map(p => {
return symmetryP(p, [0, 0], mirrorP);
}).reverse());
var totalInTangentsSet = new Array();
for (var i = 0; i < z; i++) {
totalInTangentsSet = totalInTangentsSet.concat(unitInTangentsSet.map(p => {
return rotateP(p, i * 2 * Math.PI / z);
}));
}
var totalOutTangentsSet = new Array();
for (var i = 0; i < z; i++) {
totalOutTangentsSet = totalOutTangentsSet.concat(unitOutTangentsSet.map(p => {
return rotateP(p, i * 2 * Math.PI / z);
}));
}
//Create gear spline
createPath(points = totalPointsSet, inTangents = totalInTangentsSet, outTangents = totalOutTangentsSet, is_closed = true);

中间的小圆是后添加的,跟表达式无关
Module m是模数m,控制齿宽,直观上直接控制了齿轮的整体缩放,
Teeth z是齿数,
Pressure Angle就是压力角,直观来看的话是控制啮合曲面的倾斜度,
RoofRound是控制齿根的圆滑程度,影响不大,只是控制根部过渡线而已,不参与啮合
齿轮要啮合的条件是模数与压力角a要相等,百度百科中是这样写的

标准齿轮的压力角一般为20度,这种情况下齿数控制在17~40个是最好的,如果有特殊需求需要增加更多的齿数的话,需要适量得减小压力角

然后我们来生成两个不同齿数的齿轮,一个齿数为30,一个齿数为20,将他们两个的距离调成500(因为两个齿轮计算出来的分度圆直径分别是600、400,所以需要相距500),然后把齿数为20的齿轮稍微旋转一点角度让他们在静态上先啮合上

两个齿轮的传动比就是齿数之比,也就是齿数30的齿轮转20圈,齿数20的齿轮就转30圈,那么我们在齿数20的齿轮上引用齿数30的齿轮的旋转值乘以-3/2(因为旋转方向要相反),并且加上刚才我们旋转之后的那个偏移量
var rot = -thisComp.layer("Cogwheel").transform.rotation * 3 / 2;
rot + thisProperty.value
然后对另一个齿轮K旋转动画,或者直接输入time表达式,看一下它们是否完美的啮合在了一起

虽然我一直是用形状层在生成这些曲线,但是这些其实都只是路径而已,也就是说把他们放到mask里也同样适用,只不过mask根据其依附的图层类型不同可能中心点不一样,比如固态层中心点不是[0,0]
我们可以把这些效果粘贴到一个mask中作为E3D的自定义形状

要注意在E3D中给了齿轮倒角的话要用expand edges这个属性把倒角造成的边缘扩展再收回来,不然齿宽会加厚

由于AE中表达式的执行效率不是很高,这个齿轮其实生成出来之后就有点卡了,如果不需要对齿轮里的一些参数做动画,可以把这个齿轮变成非参数曲线,就好像自己画出来的一样,如果直接禁用表达式的话,它无法保留这个表达式的输出

那么可以用这个脚本来把表达式生成的属性坍缩到属性本身上,并且会自动禁用表达式,拷贝以下代码
var selComp = app.project.activeItem;
var selPro = selComp.selectedProperties;
function collapsePro(Pro) {
if(Pro instanceof Property) {
if (Pro.numKeys != 0 || Pro.expressionEnabled == 1) {
try {
var vl = Pro.value;
var nKeys = Pro.numKeys;
for (j = 0; j < nKeys; j++) {
Pro.removeKey(1);
} //Delete all keyframes
Pro.expressionEnabled = 0;
Pro.setValue(vl);
} catch(err) {
alert(Pro.name + " can not collapse");
}
} else {
;
}
} else {
;
}
}
app.beginUndoGroup("Collapse Properties");
for (i = 0; i<selPro.length; i++)
{
collapsePro(selPro[i]);
}
复制到一个文本文件,保存之后把文本文件的名字设置一下,并且后缀改成jsx,然后把这个文件放到AE的脚本目录中

重启AE,选中我们需要转化的带有表达式的属性

在文件-脚本中找到我们刚才设置的脚本,也可以直接点击run script file去文件夹中找到你要运行的脚本


OK,表达式禁用了,而我们的齿轮形状保留下来了,现在就不卡了
(如果你需要坍缩生成了动画的表达式,请使用这个功能

但是我们的旋转动画不是齿轮的path动画,path是静态的,所以不需要这样转化关键帧)
然后如何计算这个齿轮,中间过程确实比较麻烦,我做了好多辅助线来计算渐开线,仿佛回到了大学对着一张图一画一整天的时代

所以这篇文章里就不讲解具体怎么做了,反正也没人学,学了也没有用,真有用的话直接拿来用不就好了,再不行把C4D里的齿轮曲线导出一下也行
但如果真滴有人能看完这三篇文章的话就算什么都没学到起码也获得了一个生成工业齿轮的方法(强行解说存在价值)

当然为了防止有那么无聊的人真的想学,之后我就单开一个专栏贴解释一下计算过程吧

后记:自从写完上一篇贴之后楞是忙到一直没空写内容,但是只要一有点琐碎的时间就会找算法,找代码写法,哪怕是半夜等渲染空出来的一点点时间
其实我也怀疑这些东西到底有没有用,一辈子能不能有机会用上,但是看了毕导那个讲化学家有多无聊的视频之后我也就释然了,可能我无需思考这些问题,这是我想研究的内容,所以我就去研究了,是兴趣使然,而不是为了用在xxx上所以去找方法,大部分的实际工作都如拧螺丝一般无聊枯燥,我也完全可以做一个无情的套模板机器,但是当你的技能需要的知识水平远远超出工作的需求而你还能继续向着更深层次钻研的话,可能你就是真的喜欢那件事情
当我花了很多时间把之前的某个想法实现之后,也远比结束了一个无聊的项目要开心,当然能结掉一个项目也很高兴啦,但是这两种心情好比一个是获得了新礼物的喜悦,一个是终于放下了负担的喜悦
其实人们或多或少在人生的某个阶段相信过读书无用论,觉得学校这些知识在社会上毫无用处,这种想法随着阅历增加都会渐渐摒弃掉,曾经我也在怀疑高中学的东西除了为了高考拿个分数是否还有用,疑惑浪费四年在大学学知识但最终从事了一个不相干的工作,那大学四年的时光对我又有什么意义,但是随着我想要实现的效果难道越来越难,需要的知识越来越多的时候,曾经觉得完全用不到的知识竟然派上了用场,应了那句“书到用时方恨少”。
某种意义上,这就是一种废物利用吧,高数微积分对我来说本是废物,但现在我能利用上它们的时候,就不是废物了
可惜我自己还是个废物,啥时候我能被利用上啊


而且我发现没时间学习的时候反而注重抓住一些琐碎的时间来学习,好几天都有时间反而懒惰了
总之通过这段时间的学习突然意识到以前学过的东西真滴能用上,就很开心了,即使现在看起来不堪大用,未来也说不定会成为某个想法的实现基础
哦,以上这些不在正文,可以不用看
不知不觉这个文章快写了2w字了,怎么高中写作文就憋不出这么多屁来
