Code:
--[[
Advanced animated mechanics for the 8860 set, extensively using the aniTools module.
Copyright Roland Melkert 2017
Free for non-commercial use.
Version 2017-12-29
--]]
--==Options=============================================================================================================
--[[ local doLenStats=false --prints info about minimal needed animation length and estimates POV-Ray rendering time.
local frtA=35 --normal radiosity
local frtB=3.5*60 --HQ radiosity
local doLeftChairTiltDemo=true --left or right chair tilt demo
]]
--==Includes============================================================================================================
genTools=require('genTools')
aniTools=require('aniTools')
--==Sub actors==========================================================================================================
function initEngine(actor)
--Engine table
local result={}
--Animation elements
result.mainAxleAngle=actor:addAniElm(aniTools.aniElm(0))
result.pistonRodPosOri={}
result.pistonTopPosOri={}
local m=ldc.matrix() --can use a single one as aniElm clones it.
for i=1,4 do
result.pistonRodPosOri[i]=actor:addAniElm(aniTools.aniElm(m))
result.pistonTopPosOri[i]=actor:addAniElm(aniTools.aniElm(m))
end
result.coverVis=actor:addAniElm(aniTools.aniElm(true))
result.coverPos=actor:addAniElm(aniTools.aniElm(0))
--Joints
local sf=ldc.subfile()
local ZZ=ldc.vector(0, 0, -1) --negative to force positive angles for forward motion.
local mainAxleRef=sf:getRef('axe_motor.ldr')
actor:addJoint(aniTools.angleJoint(mainAxleRef, result.mainAxleAngle, ZZ))
result.pistonRodJoint={}
result.pistonTopJoint={}
local c=mainAxleRef:getPos()
for i=1,4 do
--Use relative posOri joints for both pistons and tops.
-- This simplifies the below maths as we can assume everything is relative to points along the main axle.
local ref=sf:getRef('piston.ldr', i)
c:setZ(ref:getPos():getZ())
result.pistonRodJoint[i]=actor:addJoint(aniTools.posOriJoint(ref, result.pistonRodPosOri[i], c, false, ldc.matrix()))
result.pistonTopJoint[i]=actor:addJoint(aniTools.posOriJoint(sf:getRef('2851.dat', i), result.pistonTopPosOri[i], c, false, ldc.matrix()))
end
--All axles are depended on de main engine axle, and thus normally would/could be handled in the engine apply function below.
-- But the gearbox is depended on the resulting drive axle angle, so in order to animate that non static (gear shifts) this angle must be known/available during sequence processing.
-- For this to be possible actions must be used, so lets define a custom one doing all axles in one go.
result.depsAction=function(self)
result=aniTools.depAction(0)
result.engine=self
result.calc=function(self, time)
end
return result
end
result.apply=function(self)
--The pistons are always depended on a 'normal' animation element, so we can handle their final position in this apply funciton outside normal sequence processing.
-- This allows for a slightly more efficient way of dealing with all of them in one go and removes the need to worry about it during sequencing.
--We need to calculate their orientation changes resulting from the cylinder constrains.
local mainAxleOri=ldc.matrix()
mainAxleOri:setRotate(self.mainAxleAngle.value, 0, 0, -1)
for i=1,4 do
--All related joints maintain a center along the main axle.
-- So we need the relative position of the piston (in rest) to that.
local rpNeg=genTools.IF(i>2, 1, -1) --The rods are connect left and right in pairs.
local bpNeg=genTools.IF((i%2)==0, 1, -1) --The heads go from left to right, odd being at the positive side of X
--The rods are connected to the offcenter axle's
local p1=ldc.vector(rpNeg*16, 0, 0)
--Rotate it to its current position.
p1:transform(mainAxleOri)
--The head is normally located 60 units further away.
local blockPos=ldc.vector(bpNeg*(60+16), 0, 0)
--Now we get the pistons 'Y' direction in local space directly from its original reference.
local yDir=self.pistonRodJoint[i].orgAbsPosOri:getOriYRow()
--How far is the piston position from the line the piston moves on.
-- getDistance also returns the (pos/neg) distance from the line's position to p1 when based upon the normalized line vector.
local dist, mul=p1:getDistance(blockPos, yDir)
--Use this to calculate the (shortest) position on the movement line in relation to the piston position.
local pShort=blockPos+yDir*mul
--These two points together with the unknown top position form a triangle.
-- We use this to calculate the distance of the 'used' part of the movement line in relation to the nearest point on it.
local s=math.acos(dist/60)
s=math.sin(s)*60
--And then use that distance to move the 'near' point along the line.
-- which is the position of the top.
local p2=pShort-yDir*s
--Now we need to orientate the piston so the joint can place it correctly.
-- Local Y goes from piston to top, note neg due to LDraw.
local pistonY=p1-p2
-- Local Z never changes as the axle is parallel to it, so get it from the original reference.
local pistonZ=self.pistonRodJoint[i].orgAbsPosOri:getOriZRow()
-- And having two, we can get the third as the crossproduct because everything needs to be perpendicular.
local pistonX=pistonY:getCross(pistonZ)
--Use this and the calculated position to construct a matrix.
local posOri=ldc.matrix()
posOri:setOriRows(pistonX, pistonY, pistonZ) --Do note we can't do it in the matrix constructor function as that expects 'columns', you could invert it later though.
posOri:setPos(p1)
--Apply to the aniElm
self.pistonRodJoint[i].elm.value:set(posOri)
--The top piston only needs its position changed, but as we are using relative joints we also need to apply the original top orientation.
posOri:setOri(self.pistonTopJoint[i].orgAbsPosOri)
posOri:setPos(p2)
--Apply to the aniElm
self.pistonTopJoint[i].elm.value:set(posOri)
end
end
return result
end
--==Main actor==========================================================================================================
function carActor()
--Start with a basic actor table.
local result=aniTools.actor()
--Split mechanics / control elements into logical sub tables.
result.engine=initEngine(result) -- This handles the engine axles and pistons.
--Override the apply function so we can do all the auto dependencies.
-- This could also be done using custom dependency actions.
-- But there is no real advantage to that method in a large animation like this one.
--Buffer the default apply function as we are overriding it to do some auto dependencies.
-- You could skip this if you don't use joints in an animation, but we do.
result.orgApply=result.apply
result.apply=function(self)
self.engine:apply()
-- The default function processes all joints so it must be called in our version too.
self:orgApply()
end
return result
end
function camActor(story)
--Initial/rest state 3rd person camera.
local cam=ldc.camera(3, ldc.vector(46.4297, -27.8264, -53.1873), 2074.9246, 69.8029, 35.1886, 0)
--Register it as an output camera, the returned cam object is a DIFFERENT one as it needs to be a 'linked' one.
cam=story:addCamera(cam)
--Create an actor for this output camera.
result=story:addActor(aniTools.cameraActor(cam))
--Easy to use spin action generator.
result.spin=function(self, seq, ofs, len)
seq:addAction(aniTools.diffAction(self.yaw, ofs, len, 360))
return len
end
return result
end
--==Events==============================================================================================================
function onStart()
movie=aniTools.story()
local car=movie:addActor(carActor())
print (car)
-- local cam=camActor(movie)
--Add the main sequence to the story
local mainSeq=movie:addSequence(aniTools.sequence());
-- Main dependency actions.
mainSeq:addAction(car.engine:depsAction())
--Engine speed
local driveSeq=aniTools.sequence()
driveSeq:addAction(aniTools.accelAction(car.engine.mainAxleAngle, 0, 2, 0, 150))
driveSeq:addAction(aniTools.speedAction(car.engine.mainAxleAngle, 2, nil, 150))
driveSeq:addAction(aniTools.accelAction(car.engine.mainAxleAngle, 0, 2, 150, 0))
driveSeq.finLen=function(self, len) self.actions[2].len=len-4 self.actions[3].ofs=len-2 end --The length of motion is set at the end of storyboarding.
--Put together the story.
local ofs=0
local len=0
local driveOfs=ofs
mainSeq:addRef(driveSeq, ofs, nil)
--Done setting up the story
movie:prep() --If not here it will be automatically done in the first apply call, which might cause a 'too slow playback' error in complicated animations.
if doLenStats then
print('Length needed: ', ofs, ', est render time: ', genTools.round(ofs*25*frtA/60/60, 2), ' hours, HQ: ', genTools.round(ofs*25*frtB/60/60, 2), ' hours.')
end
end
function onFrame()
movie:apply(ldc.animation():getFrameTime())
end
--==Register============================================================================================================
function register()
local ani=ldc.animation('Demo')
ani:setLength(10)
ani:setEvent('start', 'onStart')
ani:setEvent('frame', 'onFrame')
end
register()