Trajectory2 1.1 - by Bob Le Gob ******************************* Update from v1.0 1) Optimization of the v1.0 fix: ******************************** Version 1.1 no more computes the optimal angles at each rendering but computes them only once at the starting of the plugin. Let's have a look at the original newOb(): ... let prepanch ObAnchor o -> [_ l] in ... ( ObCbAnim o mkfun2 mkfun3 @slidepos ta [nbframes 0 0 l (!strcmpi PosAng "All")]; It now looks like: ... let prepanch ObAnchor o -> [_ l] in let BLG_PrepareOptimization l -> BLG_l in ... ( ObCbAnim o mkfun2 mkfun3 @slidepos ta [nbframes 0 0 BLG_l (!strcmpi PosAng "All")]; BLG_PrepareOptimization() and BLG_PrepareOptimization2(): After recovering the list of positions, we modify it so that we may add to the general description of a Scol position the computed optimized orientation of the next position in the list. It more or less uses the same method that I implemented in v1.0, taking into account the current and next position in the list, then calling the OptimizeDestinationsAngle() and OptSingleAng() functions. The main change is that the optimized values are stored in the list of positions. This of course impacted the slidepos2() function as the structure of the list was changed: - For the interpolated part: ... let endlist l i -> [p [q _]] in let p -> [_ [xp yp zp] [ap bp cp] [aq2 bq2 cq2]] in let q -> [_ [xq yq zq] _ _] in ( M3setObjVec session h [(xp*kp+xq*kq)/lastframe (yp*kp+yq*kq)/lastframe (zp*kp+zq*kq)/lastframe]; if flagPosAng then M3setObjAng session h [(ap*kp+aq2*kq)/lastframe (bp*kp+bq2*kq)/lastframe (cp*kp+cq2*kq)/lastframe] else nil; ) - For the non interpolated part: ... let nth_list l i -> [_ vec ang _] in ( M3setObjVec session h vec; if flagPosAng then M3setObjAng session h ang else nil; );; 2) Handling the end of a trajectory: ************************************ Original code in slidepos(): set currentframe = currentframe + deltaframe; ... if currentframe <= lastframe then (set a.TAtick = currenttick; mutate z<-[_ currentframe accumulator _ _]) else ( if a.TAlooponce then (set a.TAplaystop = 0; set a.TAtick = nil) else (set a.TAplaystop = 1; set a.TAtick = nil); mutate z<-[_ 0 0 _ _] ); apply_on_list x @slidepos2 [z a.TAinterpoflag prevframe] What are the problems with this code ? As long as we do not reach the lastframe, it's ok. But if you reach it, then you've at least three possible problems: - First of all, the basic comparison (currentframe <= lastframe) is wrong. If you defined a 300 frames long trajectory, it will run from 0 to 299. Frame 300 should never be reached. - This first problem explains the second one, a commonly seen bug. At the end of the trajectory, the object is teleported at the center of the scene. As we use an undefined frame value, we'll point to a non existing position in our positions list, retrieving probably nil values when trying to move our object, and thus moving it at the center of the scene. - Furthermore, when you reach the lastframe after a looponce, the currentframe of the trajectory is not updated. It means that if you later try to run again the same trajectory, it will still be at the end of the previous trajectory and will not start again (it will in fact try again to finish the previous trajectory, starting from its last stored frame). And there's a fourth problem, which certainly has only a very few chances to occur in this plugin (but this basic algorythm is used in other plugins - like the PlayAnim - where the problem is more serious), except if you define wrong frame events in the editor: - The above code will allow you to reach values beyond the last defined frame. When checking for possible frame events between the previous and current frames, you can thus check events that shouldn't be triggered. For example, if you wrongly defined our frame event #300 in the above example, even with the proper comparison stopping at 299, you could still trigger this event. New code: if ((currentframe + deltaframe) < (lastframe - 1)) then ( ... mutate z<-[_ (currentframe + deltaframe) accumulator _ _]; set a.TAtick = currenttick; apply_on_list x @slidepos2 [z a.TAinterpoflag prevframe]; 0; ) else ( ... mutate z<-[_ (lastframe - 1) _ _ _]; apply_on_list x @slidepos2 [z a.TAinterpoflag prevframe]; set a.TAtick = nil; if a.TAlooponce then set a.TAplaystop = 0 else set a.TAplaystop = 1; mutate z<-[_ 0 0 _ _]; 0; ); What are we doing here ? - As long as we do not reach the real last frame, we update our trajectory and move our object accordingly. - If we reach the last frame, or move past it, we force the current position to the last frame and move our object accordingly. Then we initialize again the trajectory at frame 0 (without moving the object). And it will start again if we are looping, or will be ready to start again if we are not looping and waiting for some other proper event. 3) Handling of events: ********************** Checking events in original version: fun testFrameEvent(fel,prevframe, curframe, fpos)= if fel == nil then nil else let fel -> [keyframe nfel] in if ((prevframe < keyframe) && (keyframe <= curframe)) || ((prevframe < keyframe) && (curframe < prevframe))then fpos else testFrameEvent nfel prevframe curframe fpos+1;; This function is called in slidepos(): ... set currentframe = currentframe + deltaframe; let (testFrameEvent a.TAfel prevframe currentframe 0)-> fpos in if fpos == nil then nil else _DMSevent this (strcatn (ObName o)::".Frame#"::(itoa (nth_list a.TAfel fpos))::nil) nil nil; ... Basically, what's the idea ? Each time we perform some interpolated jump, we check if a frame event should have occured between the previous position and the current one. If we find one, we return its position in the events' list and trigger the according event. Let's check the condition in testFrameEvent(): We have two possibilities to validate a 'keyframe' value for an event: ((prevframe < keyframe) && (keyframe <= curframe)) or ((prevframe < keyframe) && (curframe < prevframe)) - The first one simply says that keyframe has to be between prevframe (not included) and curframe. Which should be the right condition for most cases. - The second one says that keyframe should be greater than prevframe and that prevframe should be greater than curframe too. This condition was probably foreseen for the case of a loop with the current frame being reseted to 0 for example. What can we see here ? - The second condition can never be true as 'prevframe equals old curframe' and 'curframe equals old curframe plus deltaframe' when we call the test for events. - A frame event can never be triggered for frame#0, because 'prevframe < keyframe' can never be true in such a case. - The function can only trigger one frame event per jump. - As said before, it can trigger out of range frame events. (cf the 299/300 example in previous chapter). New code: fun CheckFrameEvents(l, fr1, fr2, o) = if (l == nil) then 0 else let hd l -> fre in if (fre > fr2) then 0 else if ((fre >= fr1) && (fre < fr2)) then ( _DMSevent this (strcatn (ObName o)::".Frame#"::(itoa fre)::nil) nil nil; CheckFrameEvents tl l fr1 fr2 o; ) else CheckFrameEvents tl l fr1 fr2 o;; And in slidepos(): ... if ((currentframe + deltaframe) < (lastframe - 1)) then ( CheckFrameEvents a.TAfel currentframe (currentframe + deltaframe) o; ... ) else ( CheckFrameEvents a.TAfel currentframe lastframe o; ... CheckFrameEvents() replaces the original testFrameEvent() and uses the same parameters types. What are the differences ? - What the function returns is of no importance. It ends when we've checked all the events list, or as soon as the keyframe (fre) found is greater than our current frame (fr2). For this last point, you may check the FrameEventList() function which has been modified to ensure that the list is properly sorted. - Frame events are triggered within the function and multiple events can be triggered if needed. - A frame event can now be triggered for frame#0 because of the new condition used: ((fre >= fr1) && (fre < fr2)) - with fr1 being the previous frame. slidepos(): - Out of range events can no more be triggered but the real last frame can still trigger its own frame event. This is done in two separate checks depending on the new current value that should be reached.