


使用AviUtl制作视频时,我们常常会用到丰富的“动画效果“,十分方便。而这些效果都是用Lua语言编写的。听到编程,很多人都会有些顾虑。
不过坦率地说,如果你学会了加减等运算和AviUtl的独特渲染方法后,那么编写脚本会比写暑假读后感更简单。哇哦。AviUtl里,能够运行Lua的是滤镜效果(模糊和RGB分离等) “脚本控制”(图1)或是“动画效果”和被称为“自定义物件”的这两种效果。从规模来说后者比较庞大,但简单操作一般用“脚本控制”。
今天的目标
hankei = obj.time * 120
if 300 < hankei then
hankei = 300
end
if 4 < obj.time then
plus_kakudo = (obj.time - 4) * 2
else
plus_kakudo = 0
end
for i = 1, 6 do
kakudo = i / 6 * 2 * math.pi + plus_kakudo
x = hankei * math.sin(kakudo)
y = hankei * math.cos(kakudo)
obj.draw(x, y)
end

(图1、脚本控制效果)
首先,在时间轴上添加图形(圆),并在物件上添加“脚本控制”效果。从起头的hankei开始,将所有无法理解的代码公式复制下来,粘贴在文本栏中,并开始播放,图形就会从中心向6个方向散开,短暂停顿后,开始顺时针旋转。如果是第一次使用脚本控制效果,那么不懂也是很正常的。
为了理解本次目标,我会提到三个东西:变量与数值;for语句;if语句。首先,从变量和数值讲起。刚上初中,数学就是必修科目之一了。我记得当时好像就有x,y这种东西,这就是变量!课上的提法也可以,不过在这里我要重新讲一遍。编程必须全部用半角字母和数字(大小写罗马字,0到9的数字和其他符号)来记编写。不仅不能用日文字符(例如汉字和平假名),全角字母数字字符也不能用,这点要注意。稍微提一下,变量常常被比喻为“箱子”,箱子有一个名字,叫做变量名。那么变量x就是一个叫做x的箱子(图二),变量hankei就是一个叫做hankei的箱子。箱子的名称可以自由取定。箱子如果是空的,那就没有任何意义,里面有东西它才能起到效果。

(图二、名为变量x的箱子)
大家一般在箱子里放些什么呢?重要的东西,想藏起来的东西或者平时不常用的东西等等等等,不过变量这个箱子内就得放置数值了。其实也可以放别的,但本次学习中请认为不能。往现实的箱子里放东西很简单,因为就是把东西放进去。而在编程中则称为“赋值”,表示这种动作的符号是半角的“=”。这是平时代数运算中表等价的符号,不过在这里的用法则不同。
5 + 4 = 8
和
x = 8
两种等号就不一样了。上面的5+4=8,等号表示左右两式相等。下面的等号是将8这个数值赋给变量x(图3)。数学的中8 = 5 + 4也是同样的意思,但是编程的中8 = x是没有意义的。变量赋值时,变量在左,数值在右。如果你把数值赋给变量,那么你就可以把这个变量当作数值。因此也可以将变量赋给其他变量(图4)。
x = 10
hensu = x + 5
就像这样,把变量x代入进变量hensu中。因为变量x的内容是10,所以变量hensu的内容是15。这里要注意的是,我们仅把变量x的值代入到了变量hensu中,因此变量x仍保持10不变。如果是现实中的箱子,那么此时的箱子x就会变成空的,而这是编程。
例1
obj.setfont("", 50)
nobita = 30
obj.load(nobita)
把上方代码粘贴到脚本控制上的话应该会出现30这个数值。第一行语句是AviUtl内置的功能函数,不知道是干嘛的,姑且先把字体大小想成是50。第2行终于来了,是变量。意思是把30这个数值赋给了变量nobita。而第三行是AviUtl中用于显示变量nobita所含数值的函数。总之,请记住在()中写入变量名就会得出其数值。改变赋给nobita的数值的话画面上的数值也会随之改变。在nobita = 30和obj.load(nobita)之间插入一行nobita = 5的话,画面上的数值就应该是5。是5而不是35啊,因为重新赋值,覆盖了初始化赋值。

(图3、往变量x中放入8的情况)

(图4、将x和5放入变量hensu)
下面讲讲for语句。在编程中,for语句指循环执行某段程序。比如,让机器人学会削苹果皮:把苹果放在案板上;然后拿起果刀;开始剥皮。如果只执行一次的话,用普通的表达式即可,但是若要重复100次,那人工命令100次是很麻烦的。如果你一次让机器人执行剥皮操作100次,那么总共只需4条语句,就能让机器人帮你剥掉100个苹果皮。这就是一个for循环结构。我觉得这种比喻相比于冗长的专业说明,更能让人理解实际的例子。
例2
obj.setfont("",50)
s = ""
for i = 1, 10 do
s = s .. i .. "\n"
end
obj.load(s)
例3
obj.setfont ("", 50)
shokichi = 2
saishuchi = 22
s= ""
for i = shokichi, saishuchi do
s = s .. i .. "\n"
end
obj.load(s)
咦,除了for以外还有很多不认识的东西啊。obj.setfont("",50)是用来更改已显示的字体的大小。下一行的s = ""是啥呢?虽然知道是给变量s赋值,但是右边却是对引号。这是一个将字符串而非数值赋给变量的符号。变量内不但可以装数值,也可以装字符,我们这次先不讲。总之,就是把文字赋给变量s。for语句终于来了啊。写法有规则
for 变量 = 初值、终值 do
执行体
end
要用do和end把循环体包住。为了执行for循环,需在结构起始处写上for。然后声明一个变量,表示将在其中重复输入\n。循环通常使用i和j,但只要是变量,基本都行。初始值和最终值请根据需求决定。如果for i = 1,10 do,意味着i从1开始,循环到10结束。第一次i是1,第二次是2,变量i的值逐次加1。直到i大于10,循环结束。因此,重复处理共进行了10次。循环体是s = s .. i .. "\n"这个谜一般的表达式。你可以看到该语句是在为变量s赋值,但在等号的右边有着像蚂蚁一样“..”;还有“\”符号,是不是看不太明白了?
先从变量s两边的谜团开始分析。之前我把变量比喻成箱子,但其实还有一些细节没有讲明。那就是赋值的处理顺序。赋值先看右边,计算变量的内容和数值等。因为计算的时候我们取出了箱子里的东西,所以可以认为变量s这个箱子是空的。下一步将计算后的得数放回变量s,那么在编程中,变量s原有的值便被替换了。然后变量s继续处理第二行的字符串。数值相加用“+”,字符串的加法则是用“..”两个英文句号。所以这个公式的意思就是把变量i的内容加到变量s中。还有末尾的“\n”是什么,简单来说就是换行符。用“\n”来表示换行。end结束for循环,最后调用obj.load函数将变量s的内容显示在画面上。

(图5、变量s(红)加上变量i(绿)后,将结果(黄)赋给变量s)
进一步讲讲for循环,循环范围只是固定的数字,完全没有实用性。因此可以把for语句的初始值或最终值设为变量,以增加自由度。例3中便添加了例2没有的变量shokichi和变量saishuchi。然后观察此脚本控制,实际上会发现新数列是shokichi的数值到saishuchi的数值。试试设置不同的初始值吧。0怎么样,-5又如何,25呢。这三个中应该只有25不会显示。说明for语句中声明的变量i,加1后大于最终值的话,循环就会结束。也就是说,如果初始值大于最终值,则循环终止且不执行任何操作。虽然也有初始值逐渐减小等于最终值的for语句,但不详细讲了。想深究的人可以自己去查。
讲完for语句就该讲if语句了,for语句是用来重复执行某段代码的,而if语句则是判断是否执行某段代码,就视频制作而言,可以按照“视频文件这样处理,图像文件那样处理”的情况来区分对待。先来看看范例吧。
例4
a = 1
if a == 1 then
obj.setfont("", 50)
obj.load(a)
end
复制上述代码到脚本控制,画面上应该会出现1。例子中if语句的含义是:变量a等于1时,画面显示变量a。如果变量a的数值改变,那么就会显示物件本身的内容。If语句的写法:
if 条件 then
表达式
end
和for语句类似,if语句是用then和end把想要执行的语句包在中间。for语句是do和end包裹,而if语句是then和end包裹,所以“条件”是执行表达式的条件。图6是常用条件表。根据条件表可知,例4的条件是在相等时生效的。变量中表示数学等价的符号是“==”。由于是等价条件(而非赋值),所以a ==1和1 == a是等效的。请自行运行例4来确认效果。

(图6、if语句可用条件表)
如何把所有的条件都设定在if语句中呢,让我们来看看通过将示例条件部分中的“==”替换为列表中的其他条件并更改赋给变量a的值,会发生什么?然而条件并不总是只有一个。我们先来看看事例吧。
例5
zukei = 0
if zukei == 0 then
obj.load("figure", "四边形")
elseif zukei < 0 then
obj.load("figure", "六边形")
elseif zukei <= 9 then
obj.load("figure", "星形")
else
obj.load("figure", "五边形")
end
总觉得有点像编程了。将上述表达式复制到脚本控制中。首先应该会显示正方形,如果变量zukei的值发生改变,相应的图形也会随之改变。看过图6所示的条件应该就懂了,不过,这里面还混有一个新面孔。那就是elseif和else。例5中,
zukei = 0
if zukei == 0 then
obj.load("figure", "四边形")
end
这么写可能能看懂。但是,这里的if语句中只有在zukei为0的时候才符合条件。那如果我们希望让zukei在负值时显示六边形,该怎么办?
zukei = 0
if zukei == 0 then
obj.load("figure", "四边形")
end
if zukei < 0 then
obj.load("figure", "六边形")
end
乍一看好像是正确的,然而这样的话就出现了两个if结构。为了简洁,我们可以用elseif将其合为一体。
zukei = 0
if zukei == 0 then
obj.load("figure", "四边形")
elseif zukei < 0 then
obj.load("figure", "六边形")
end
用elseif代替if,然后按顺序写上条件then。每个if结构都要有一个结束,所以在最下面加上end。其他的条件都包裹在内就行了。
zukei = 0
if zukei == 0 then
obj.load("figure", "四边形")
elseif zukei < 0 then
obj.load("figure", "六边形")
elseif zukei <= 10 then
obj.load("figure", "星形")
end
这么写就可以附加更多条件。但是里面却没有zukei大于等于10的条件。坦率地讲,我觉得有elseif 10<=zukei then就够了,再加别的条件不是很麻烦吗?虽然最后的条件包括大于等于10的情况(显示物件本身的内容),但是我们还可以这样写“目前所有条件以外的范围”:如果要创建“目前所有条件以外的范围”的条件,请使用else(例五代码的末尾)。通常判断语句要用then收尾,不过else不用。虽然有点麻烦,但是记住吧。这样一来,对应全实数域的if结构就完成了。
其实if语句还有很多功能,但再写下去,文章就太长了,我打算放到下次讲。我们最后要做的就是尝试改变目标。在此之前,我们将本次未讲解的obj.time赋给到变量hankei,obj.time是表示物件当前时刻的变量,根据时间的不同,会有0或1的变化。math.pi是数学里的圆周率π,数值上是3.1415…一直往后循环。另外math.sin, math.cos是三角函数,就是高中数学里cos(2π)=1之类的。()里需要写弧度制角度。obj.draw是在画面上渲染图象的函数。将x, y坐标填入()中,图像将会被渲染在坐标位置。现在我们就来切身理解一下这些函数。(详见AviUtl中文版内的lua函数)
hankei = obj.time * 120
if 300 < hankei then
hankei = 300
end
if 4 < obj.time then
plus_kakudo = (obj.time - 4) * 2
else
plus_kakudo = 0
end
for i = 1, 6 do
kakudo = i / 6 * 2 * math.pi + plus_kakudo
x = hankei * math.sin(kakudo)
y = hankei * math.cos(kakudo)
obj.draw(x, y)
end
和刚开始看的时候相比,如果你能理解的部分变多了,就说明本次讲座有意义。把物件的当前时间乘以120后的数值赋给hankei。变量hankei是渲染物件的半径,随时间变大。但如果不加限制,半径就会无限增长。所以我使用了if语句来限制这一问题,如果hankei的数值大于300(阈值)的话,就把hankei强制定为300。另外一条if语句也很重要,它将时间大于4秒的情况与其他时间范围条件划分开来(负责调控旋转,4秒后开始旋转)。然后是for循环。初始值是1,最终值是6,共循环6次。画面上也出现了六个圆。赋给变量kakudo的值是每次循环的角度(2*math.pi为360°,而后等分),但是我还不需要知道for语句的内容。这部分都是数学(请自行百度“如何计算圆上点的坐标”)。值得注意的是,如果for语句的do和end间,可以放置多个表达式。
谢谢你陪我到最后。这次的讲习会到此为止。以下内容面向高手,但我会在里面介绍很多有关脚本控制的知识。如果你对自己的编写技术有信心的话,到时候回过头来看,会有创造性的发现。
WARNING:以下内容较难理解
现在开始讲解视频(sm27247848)中使用的脚本控制。若无特殊说明,就把脚本控制加在图形上,将代码复制进去。
开始(0秒~)
骰子共由5个脚本控制构成。其中三个是用来制作骰子的标志的,一个是定义的旋转移动的函数,还有一个是来实际渲染的。拆分的理由是脚本控制有1024个字符的限制。
脚本讲座社Logo的黑框
骰子单面大小是500×500。因此,首先在虚拟缓存中设定单面的大小。接下来要制作黑框,用obj.load来条用黑色的四边形。第二个参数用来调用图形的形状,第三个参数用来指定图形的颜色。黑色在RGB模式中的值是0,AviUtl中常用obj.load调用图形(四边形),创造新形状。再用obj.drawpoly来勾画想做的形状。obj.effect是调用效果的函数,本次调用尺寸调整(合成大小适配)效果。因为缩放率效果不能再虚拟缓存中使用。然后把调整了尺寸的四边形用obj.draw渲染到指定坐标位置。最后将虚拟缓存中创建的黑框图象复制到“cache:d1”。那就是obj.copybuffer("cache:d1","tmp")。第一个参数用来指定目标地址,第二个参数用来指定复制源。“ tmp”是将虚拟缓存转为复制源的命令。

obj.setoption("drawtarget","tempbuffer",500,500)
obj.load("figure","四边形",0)
obj.drawpoly(-170,-250,0,-160,-250,0,-160,250,0,-170,250,0)
obj.drawpoly(160,-250,0,170,-250,0,170,250,0,160,250,0)
obj.drawpoly(-220,-250,0,-215,-250,0,-215,250,0,-220,250,0)
obj.drawpoly(215,-250,0,220,-250,0,220,250,0,215,250,0)
obj.drawpoly(-170,-180,0,170,-180,0,170,-170,0,-170,-170,0)
obj.drawpoly(-170,170,0,170,170,0,170,180,0,-170,180,0)
obj.effect("尺寸调整","X",20,"Y",50)
obj.draw(-185)
obj.draw(-185,-100)
obj.draw(-185,-200)
obj.draw(-185,100)
obj.draw(-185,200)
obj.draw(185)
obj.draw(185,-100)
obj.draw(185,-200)
obj.draw(185,100)
obj.draw(185,200)
obj.copybuffer("cache:d1","tmp")
obj.alpha=0
脚本讲座社的中心logo
同样,准备一个单面大小的虚拟缓存。用obj.load调用四边形做背景。将其渲染在虚拟缓存中。这样虚拟缓存就有背景了。然后创建闭合的虚线圆环。虚线为大小是278,线宽为5。线段的数量为36,因此每条线段间隔角为10°。也就是说,一条线段可以在10度范围内自由变动。因此用obj.effect调用斜向裁剪,将两端削去3度。图象就变为了线宽为5,半径278的圆,是像瓦片一样被削掉的圆。共需36条,只要每10度渲染一次,就会变出logo的虚线环。由于只是将其旋转,因此裁剪后中心保持不变。接着将3个环渲染在指定的位置。接下来轮到文本了,如果照原样画的话,文本会变得异常的小。那是因为字体大小被设定在了1左右。我们要用obj.setfont来更改字体的大小。obj.setfont可以设定字体类型、字体大小、字体的装饰类型、、字体颜色和阴影颜色。第一个参数用来指定字体,若留空(“”)则为默认。文本之后的是obj.copybuffer。调用由黑框脚本控制创建的图像。和之前一样,第一个参数用来指定目标地址,第二个参数用来指定复制源。在“obj.copybuffer”(“obj”,“cache:d1”)中,我们要把当前的图像要复制到“obj”地址,复制源是黑框,所以第二个参数设为“cache:d1”。最后用obj.load调用出黑色四边形。以此做阴影。用obj.effect做遮罩。以虚拟缓存的左上方为中心,并从此处添加遮罩。如果将“type”设为1,遮罩类型就是圆,如果将“遮罩反转”设为1,那么就会在中心按遮罩类型挖掉一块。设定模糊值,调节右下角“变黑”的尺寸。这样脚本讲座社的标志就完工了,将obj.copybuffer创建的虚拟缓存复制到“ cache:d2”保存。

obj.setoption("dst","tmp",500,500)
obj.load("figure","四边形",0xfafafa,500)
obj.draw()
obj.load("figure","圆",0xe2e2e2,278,5)
obj.effect("斜向裁剪","角度",87)
obj.effect("斜向裁剪","角度",-87)
for i=0,35 do
obj.draw(0,0,0,1,1,0,0,i*10)
end
obj.load("figure","圆",0x80,208)
obj.draw()
obj.load("figure","圆",0x80,62)
obj.draw(104,-104)
obj.load("figure","圆",0xe2e2e2,62)
obj.draw(45,-45)
obj.setfont("",90,4,0xe2e2e2,0x80)
obj.load("Aviutl")
obj.draw(-30)
obj.load("L")
obj.draw(5,-60)
obj.load("a")
obj.draw(5,60)
obj.copybuffer("obj","cache:d1")
obj.draw()
obj.load("figure","四边形",0,500)
obj.effect("遮罩","遮罩反转",1,"type",1,"模糊",200,
"大小",1100,"X",-250,"Y",-250)
obj.draw()
obj.copybuffer("cache:d2","tmp")
obj.alpha=0
骰子
制作立体骰子的方法有好几种,其中一种是制作像展开图一样的东西。创建一个2000×1000的虚拟缓存,和2-6的点子。整体用虚拟缓存大小的白色四边形覆盖,作为背景。点子都是大小为50的黑色圆圈。最后用obj.copybuffer将缓存复制到“cache:d3”处。这次做了个长2000,宽1000的展开图,其实1500×1000就行了,我反思。

obj.setoption("drawtarget","tempbuffer",2000,1000)
obj.load("figure","四边形",0xffffff)
obj.effect("尺寸调整","X",2000,"Y",1000)
obj.draw()
obj.load("figure","圆",0,50)
obj.draw(-750,-250)
obj.draw(-890,-390)
obj.draw(-610,-390)
obj.draw(-890,-110)
obj.draw(-610,-110)
obj.draw(-390,-390)
obj.draw(-110,-110)
obj.draw(250,-250)
obj.draw(110,-390)
obj.draw(390,-110)
obj.draw(610,-390)
obj.draw(890,-390)
obj.draw(890,-110)
obj.draw(610,-110)
obj.draw(-890,110)
obj.draw(-890,250)
obj.draw(-890,390)
obj.draw(-610,110)
obj.draw(-610,250)
obj.draw(-610,390)
obj.copybuffer("cache:d3","tmp")
obj.alpha=0
立方体旋转移动(较难)
创建rot函数。有关参数的说明:L是骰子的边长,本次设定为500。t是表示旋转的进度值。2.5表示两圈半。ft是旋转圈数,与t的整数部分相同。startx和starty则是骰子的初始坐标。初始坐标指的是转轴的初始位置。正立方体旋转的时候,分为四个部分。一个是当前转轴位置(x1,y1),一个是水平轴位置(x2,y2),一个是对角轴位置(x3,y3),一个是竖直轴位置(x4,y4)(其实就是行进方向的四条边,滚动过程中交替成为转轴)。它们都绕转轴旋转。由于本次是在平面上旋转,所以计算很简单。首先确定转轴的位置(其实就是贴合平面的边),只需使其旋转角为90°即可。X轴方向使用math.cos, y方向使用math.sin,求得各自的投影。转轴位置(x1,y1)的求法很简单。x1是边长乘以旋转圈数,再加上转轴的初始x坐标。y1保持其初始y坐标,p是骰子当前状态下的非整数旋转圈数,也即t的小数部分。其范围是0到1,便于实时计算。p*math.pi*0.5的意思就是不满一圈时的旋转角(0到90°间)。(x2,y2)与转轴位于同一水平面。因此,除了旋转角部分以外,不需要在三角函数中加偏离角。(x3,y3)相对转轴处于对角,本身自带45度偏离 (math.pi*0.25),所以需要加上这部分。(x4,y4)在转轴正上方,自带90度偏离(math.pi*0.5)。最后将所有值指定为返回值后,自制函数就完成了。(单向滚动,所以为二维坐标)
function rot(L,t,ft,startx,starty)
x1=L*ft+startx
y1=starty
p=t-ft
x2=x1-L*math.cos(p*math.pi*0.5)
y2=y1-L*math.sin(p*math.pi*0.5)
LL=L*2^0.5
x3=x1-LL*math.cos(p*math.pi*0.5+*math.pi*0.25)
y3=y1-LL*math.sin(p*math.pi*0.5+*math.pi*0.25)
x4=x1-L*math.cos(p*math.pi*0.5+*math.pi*0.5)
y4=y1-L*math.sin(p*math.pi*0.5+*math.pi*0.5)
return x1,y1,x2,y2,x3,y3,x4,y4
end
obj.alpha=0
旋转主体
这一部分,我们终于可以渲染出和视频里一样的运动物件了。L是边长,V是一条边完成旋转所需的时间。因为一条边完成旋转所需0.25秒,所以骰子转完一周需要1秒。t是obj.time/V。实际时间除以某项行为所花费的单位时间,就是实际时间内该行为进行的次数。刚才讲了用math.floor对t取整,现在用if语句将四轴坐标分为4个情况。通过倒换返回值(见rot函数)和各个变量(下方if结构部分),使正方形能够顺利的旋转起来。不同情况分配好后,剩下的就是渲染了。先用obj.copybuffer首先调出logo,然后调出骰子展开图。由于logo是渲染在整体上的,所以只需要12个参数就行,但点子就得指定特定的位置了。这里注意一下,就完成了。

L=500
V=0.25
startx=-300
starty=L*0.5
t=obj.time/V
ft=math.floor(t)
if ft%4==0 then
x1,y1,x2,y2,x3,y3,x4,y4=rot(L,t,ft,startx,starty)
elseif ft%4==1 then
x4,y4,x1,y1,x2,y2,x3,y3=rot(L,t,ft,startx,starty)
elseif ft%4==2 then
x3,y3,x4,y4,x1,y1,x2,y2=rot(L,t,ft,startx,starty)
else
x2,y2,x3,y3,x4,y4,x1,y1=rot(L,t,ft,startx,starty)
end
obj.copybuffer("obj","cache:d2")
obj.drawpoly(-L*0.5,y4,-x4,L*0.5,y4,-x4,L*0.5,y1,-x1,-L*0.5,y1,-x1)
obj.copybuffer("obj","cache:d3")
obj.drawpoly(-L*0.5,y2,-x2,L*0.5,y2,-x2,L*0.5,y3,-x3,-L*0.5,y3,-x3,0,500,500,500,500,
1000,0,1000)
obj.drawpoly(-L*0.5,y4,-x4,L*0.5,y4,-x4,L*0.5,y3,-x3,-L*0.5,y3,-x3,0,0,500,0,500,500,0,
500)
obj.drawpoly(L*0.5,y1,-x1,-L*0.5,y1,-x1,-L*0.5,y2,-x2,L*0.5,y2,-x2,500,0,1000,0,1000,
500,500,500)
obj.drawpoly(-L*0.5,y2,-x2,-L*0.5,y3,-x3,-L*0.5,y4,-x4,-L*0.5,y1,-x1,1000,0,1500,0,1500,
500,1000,500)
obj.drawpoly(L*0.5,y1,-x1,L*0.5,y2,-x2,L*0.5,y3,-x3,L*0.5,y4,-x4,1500,0,2000,0,2000,
500,1500,500)
转场(6秒~)
像被虫蛀一样的黑色方形窟窿单个尺寸为100×100,画面尺寸是1280×720。因为没法整分,所以x方向重复数为13,y方向为8。i是全体黑块的定位序号(比如y=0,x=1,2,3,..那就是第一行的方块了)。r是决定方块旋转方向的变量,当r为0时,会进一步转换为-1以使方块逆时针旋转。t是进度比例,范围在0到1之间。首先用随机数来随机化图形的旋转起始时间。再用当前时间减去这个值,然后除以完成旋转所需的时间。这样过了0.6秒后,t就变为1了。去除0以下,1以上的部分后,就可以按照从上到下,从左往右的顺序依此进行渲染了。Z轴方向旋转的t*t只是用来加速旋转的。

for y=0,7 do
for x=0,12 do
i=x+13*y
r=obj.rand(0,1,i,0)
obj.load("figure","四边形",0)
if r==0 then r=-1 end
t=(obj.time-obj.rand(0,1000,i,0)*0.001)/0.6
if t<0 then t=0 elseif 1<t then t=1 end
obj.draw(x*100-600,y*100-350,0,1-t,1,0,0,t*t*(360+obj.rand(-10,10,i,3))*r)
end
end
背景(6秒~)
字符面板还有空余,因此我们用obj.screen_h和obj.screen_w赋值变量来对应任意尺寸的画面。分割高度hb是画面高度的九分之一。随机变量n用来指定横向分割的数量(2条或3条)。x1是各四边形的左边坐标,y2是它们的下边坐标。y1,y2需在for循环分割前进行声明,它们在横向分割时不会发生变化。首先用obj.load来调用四边形。颜色是较为柔和的蓝色。再通过obj.effect的色调修正来改变色调,使四边形颜色发生变化。变化范围在0到300之间。对比度也会发生变化。上述过程是制作随机颜色的方法之一。下面,我们同样用随机数来决定横向分割的宽度。范围从150到画面横向尺寸,150是随便取的。 x2就是x1加上L(右边坐标)。如果横向分割中的最后一个四边形触及右端,则会显示黑色背景。所以在循环变量j等于n时,强制将最后一个四边形的右边坐标赋值为sw*0.5。然后用obj.drawpoly按顺时针指定位置渲染就行了。最后,将x2的值传递给x1,因为当前分割的右边坐标是下一条分割的左边坐标。

sh=obj.screen_h
sw=obj.screen_w
hb=sh/9
for i=0,8 do
n=obj.rand(2,3,i*9,4)
x1=-sw*0.5
y1=i*hb-sh*0.5
y2=y1+hb
for j=1,n do
obj.load("figure","四边形",0x88b0f0)
obj.effect("色调修正","色相",obj.rand(0,300,j,i),"对比度",170)
L=obj.rand(150,sw,i*11+n*13,j*7+2)
x2=x1+L
if j==n then
x2=sw*0.5
end
obj.drawpoly(
x1,y1,0,
x2,y1,0,
x2,y2,0,
x1,y2,0
)
x1=x2
end
end
卷页(9秒~)
在讲基础的编程前,先把数学方面的问题想明白。首先想象一下卷轴展开是怎么样的一种情况。如果直接从卷曲状态变成平整状态来考虑,头就大了,所以我们可以反着想。例如把厚度均匀的面胚子卷起来(这里的例子是披萨饼胚,不是我们中国人家中日常的擀面),卷曲起始处的曲率半径(对于曲线,它等于最接近该点处曲线的圆弧的半径。详见百度百科,这里简化为曲线方便理解。)等于擀面杖的半径。那么从这里开始绕擀面杖行进一圈会如何呢。行进一圈后被卷起来的地方的曲率半径就等于擀面杖的半径加上单层面胚厚度。第二圈就是擀面杖半径+两倍面胚厚度。我们可以归纳得出,n值(行进圈数)不大时,卷曲部分的半径等于卷曲起始处半径(擀面杖半径)+n×面胚厚度。
搞清楚卷曲部分的半径后,我们来想想擀面杖转动一圈能行进的距离。嗯,得出2πR这个结论过于草率了。因为半径在不断增大,所以我们不能这么简单的算,应该考虑用积分。假设某处面胚厚度为a,擀面杖半径为r,角度最小变化量为dθ,那么它的行进距离就等于(n*a+r)*dθ(弧长公式)。n为行进圈数,但如果要用擀面杖转动角度来表示,那么行进距离就变成了(a *θ/2π+r)*dθ(因为n=θ/2π),对其积分后得到行进距离a*θ^2/4π+r*θ(基础)。综上我们就得到了卷曲部分的曲率半径和卷曲后行进的距离,接下来就可以编写了。脚本部分会在本次课程中作为动画效果发布,详细文字说明日后公布。

正视图

侧视图
文字(11秒~)
这个效果中文本会跟随着黑点轨迹逐渐出现。当obj.time为0时,列表Q初始化。文本变化后,轨迹也会随之变化。但其缺点是不能绑定多个字符。首先按obj.index=0或oy≠obj.oy对不同情况进行区分。然后在该条件下定义start_x,start_y和oy三个变量。start_x是单个字符的左边缘坐标,start_y是单个文本中心点的高度(就是中心点相对于画面底边的坐标)。oy是文本的高度。文本中单一独立字符的行为十分复杂,请注意:如果执行obj.ox-obj.cx,那么这就是那个独立字符的x坐标;如果执行obj.oy-obj.cy,那么这就是那个独立字符的y坐标。因此我们可以通过比较前后obj.oy的值来大致判断各字符是否处于同一行。若不满足判断条件,则将end_x,end_y赋值给start_x和start_y。end_x和end_y是指前一位字符的右边缘坐标和中心高度。k是字符相继出现的间隔时间。用obj.time减去obj.index×间隔时间,就能得出对应索引(字符)出现的基准时间。除以k后,将其转化为范围为0到1的进度比例。t小于0时,文本不显示;大于1时,不发生变化;若介于0到1之间,则对其进行特殊处理——用obj.effect在文本上添加斜向裁剪效果。斜向裁剪的中心X会从文本的左边缘向右边缘推移。然后将文本的高度暂时复制到变量oh上,开始渲染。接着用obj.load调出黑圆。文本高度信息会在此步骤中丢失,因此我之前先备份了一份到变量中。圆的渲染坐标取决于t的值。因为它是跟随着文本从左到右移动,所以start和end值与t成一次函数关系。另外只有y添加了正弦函数math.sin。到渲染部分,最先声明的Q在这里就派上用场了,由于Q是列表,所以需要在顶头加上#来表示列表的元素数。根据Q中保存的信息开始渲染黑圆。渲染完成后,将x,y数组添加至Q中。但是如果持续添加的话,轨迹就会残留下来;如果组数大于5,就会自动删除首位位置信息 (通过table.remove函数实现,第一个变量是要进行清理的目标列表,第二个变量是要删除的元素序号)。table.remove的好处在于:被清除的位置会被下一个元素补齐,也就是第二个元素会偏移至首位,随后添加的x,y数组顺位成为第五号元素。

if obj.time==0 then
Q={}
end
if obj.index==0 or oy~= obj.oy then
start_x=obj.ox-obj.cx-obj.w*0.5
start_y=obj.oy-obj.cy
oy=obj.oy
if obj.time==0 then
else
start_x=end_x
start_y=end_y
end
end_x=obj.ox-obj.cx+obj.w*0.5
end_y=obj.oy-obj.cy
k=0.15
t=(obj.time-obj.index*k)/k
if t<=0 then
obj.alpha=0
elseif t<1 then
obj.effect("斜向裁剪","角度",-90,"中心X",obj.w*(t*2-1)*0.5)
oh=obj.h*0.5
obj.draw()
obj.load("figure","圆",0,20)
pi=math.pi*2*t
x=start_x+t*(end_x-start_x)
y=start_y+t*(end_y-start_y)+oh*math.sin(pi)
obj.draw(x,y)
for i=1,#Q do
obj.draw(Q[i][1],Q[i][2],0,i/#Q)
end
Q[#Q+1]={x,y}
if #Q==6 then
table.remove(Q,1)
end
end
沙漏(11秒~)
由于1024个字符的上限,这部分的变量和函数简化为单个字符。沙漏动画分为四个模式:从物件开始旋转到0.5秒旋转半圈(模式1);从0.5秒到1秒间沙子开始掉落(模式2);从1秒到最后0.5秒前沙粒慢慢流逝(模式3);从最后0.5秒到完全流完(模式4)。T是物件的整体时间,t是当前时间。在此定义一个函数“d”(内容为obj.drawpoly),由于只渲染平面,所以z轴的值为0。同上,定义函数“I”(内容为obj.load),开始制作沙漏形状。然后定义一个沙漏侧面坐标的函数。将侧面的x坐标传递给y坐标,然后再用函数y将y坐标作为返回值传递。有点混乱...每次翻转,沙粒数必须保持相同。换言之,上下两个梯形必须全等。首先将上一个梯形的上底设为100。X是一个未知常数的立方根,它是翻转后下底的x坐标。本来想用公式求解,但限于脚本规模,我就提前算出来了。将当前时间翻倍,令模式1的p从0变成1(进入模式1),让下层沙堆的沙子开始向上移动。模式2包含沙粒掉落和掉落渲染两部分。过程中p会赋值两次。第一次从0.5秒开始,到最后0.5秒前结束,p由0变成1,调控顶部沙堆的掉落;第二次从0.5秒开始,到1秒结束,p由0变成1,调控漏斗颈沙粒的通过。模式3也要赋值两次p。第一次和模式2的第一次相同,表示上层沙粒的减少比例。第二次表示下层沙堆的增加比例。掉落的沙粒束其实就是一根未加修饰的白棒。模式4也要赋值两次p。第一个p表示0.5秒内沙子掉落的比例。第二个和模式3中的第二次相同。这样沙漏动画就做好了。最后用obj.copybuffer将虚拟缓存中的图像覆盖到物件的上。然后返回模式1,用obj.rz转180度,让沙漏旋转。

T=obj.totaltime
t=obj.time
function d(x,y,X,Y,v,u,V,U)
obj.drawpoly(x,y,0,X,Y,0,v,u,0,V,U,0)
end
l=obj.load
obj.setoption("dst","tmp",200,200)
l("figure","四边形",255)
d(-5,-5,5,-5,5,5,-5,5)
d(-5,5,5,5,70,100,-70,100)
d(-70,-100,70,-100,5,-5,-5,-5)
function y(x)
return (19*x-30)/13
end
l("figure","四边形")
X=2425^0.5
if t<0.5 then
p=t*2
v=-50+45*p
u=y(50)*(1-p)+5*p
V=-70+(-X+70)*p
U=100+(y(X)-100)*p
d(v,u,-v,u,-V,U,V,U)
elseif t<1 then
p=(t-0.5)/(T-1)
v=-X*(1-p)-5*p
u=-y(X)*(1-p)-5*p
V=-5;U=-5
d(v,u,-v,u,-V,U,V,U)
p=(t-0.5)*2
u=-5+105*p
d(-5,-5,5,-5,5,u,-5,u)
elseif t<T-0.5 then
p=(t-0.5)/(T-1)
v=-X*(1-p)-5*p
u=-y(X)*(1-p)-5*p
d(v,u,-v,u,5,-5,-5,-5)
d(-5,-5,5,-5,5,100,-5,100)
p=(t-1)/(T-1)
v=-70+20*p
u=100+(y(50)-100)*p
d(v,u,-v,u,70,100,-70,100)
else
p=(t-T+0.5)*2
u=-5+105*p
d(-5,u,5,u,5,100,-5,100)
p=(t-1)/(T-1)
v=-70+20*p
u=100+(y(50)-100)*p
d(v,u,-v,u,70,100,-70,100)
end
obj.copybuffer("obj","tmp")
if t<0.5 then
obj.rz=t*360
end