/* ** a_movingcamera.cpp ** Cameras that move and related neat stuff ** **--------------------------------------------------------------------------- ** Copyright 1998-2016 Randy Heit ** All rights reserved. ** ** Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions ** are met: ** ** 1. Redistributions of source code must retain the above copyright ** notice, self list of conditions and the following disclaimer. ** 2. Redistributions in binary form must reproduce the above copyright ** notice, self list of conditions and the following disclaimer in the ** documentation and/or other materials provided with the distribution. ** 3. The name of the author may not be used to endorse or promote products ** derived from self software without specific prior written permission. ** ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR ** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES ** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. ** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, ** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT ** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF ** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **--------------------------------------------------------------------------- ** */ /* == InterpolationPoint: node along a camera's path == == args[0] = pitch == args[1] = time (in octics) to get here from previous node == args[2] = time (in octics) to stay here before moving to next node == args[3] = low byte of next node's tid == args[4] = high byte of next node's tid */ class InterpolationPoint : Actor { InterpolationPoint Next; bool bVisited; default { +NOBLOCKMAP +NOGRAVITY +DONTSPLASH RenderStyle "None"; } override void BeginPlay () { Super.BeginPlay (); Next = null; } override void Tick () {} // Nodes do no thinking void FormChain () { let me = self; while (me != null) { if (me.bVisited) return; me.bVisited = true; let iterator = ActorIterator.Create(me.args[3] + 256 * me.args[4], "InterpolationPoint"); me.Next = InterpolationPoint(iterator.Next ()); if (me.Next == me) // Don't link to self me.Next = InterpolationPoint(iterator.Next ()); int pt = (me.args[0] << 24) >> 24; // this is for truncating the value to a byte, presumably because some old WAD needs it... me.Pitch = clamp(pt, -89, 89); if (me.Next == null && (me.args[3] | me.args[4])) { A_Log("Can't find target for camera node " .. me.tid); } me = me.Next; } } // Return the node (if any) where a path loops, relative to self one. InterpolationPoint ScanForLoop () { InterpolationPoint node = self; while (node.Next && node.Next != self && node.special1 == 0) { node.special1 = 1; node = node.Next; } return node.Next == self ? node : null; } } /* == InterpolationSpecial: Holds a special to execute when a == PathFollower reaches an InterpolationPoint of the same TID. */ class InterpolationSpecial : Actor { default { +NOBLOCKMAP +NOSECTOR +NOGRAVITY +DONTSPLASH } override void Tick () {} // Does absolutely nothing itself } /* == PathFollower: something that follows a camera path == Base class for some moving cameras == == args[0] = low byte of first node in path's tid == args[1] = high byte of first node's tid == args[2] = bit 0 = follow a linear path (rather than curved) == bit 1 = adjust angle == bit 2 = adjust pitch == bit 3 = aim in direction of motion == == Also uses: == target = first node in path == lastenemy = node prior to first node (if looped) */ class PathFollower : Actor { default { +NOBLOCKMAP +NOSECTOR +NOGRAVITY +DONTSPLASH } bool bActive, bJustStepped; InterpolationPoint PrevNode, CurrNode; double Time; // Runs from 0.0 to 1.0 between CurrNode and CurrNode.Next int HoldTime; // Interpolate between p2 and p3 along a Catmull-Rom spline // http://research.microsoft.com/~hollasch/cgindex/curves/catmull-rom.html double Splerp (double p1, double p2, double p3, double p4) { double t = Time; double res = 2*p2; res += (p3 - p1) * Time; t *= Time; res += (2*p1 - 5*p2 + 4*p3 - p4) * t; t *= Time; res += (3*p2 - 3*p3 + p4 - p1) * t; return 0.5 * res; } // Linearly interpolate between p1 and p2 double Lerp (double p1, double p2) { return p1 + Time * (p2 - p1); } override void BeginPlay () { Super.BeginPlay (); PrevNode = CurrNode = null; bActive = false; } override void PostBeginPlay () { // Find first node of path let iterator = ActorIterator.Create(args[0] + 256 * args[1], "InterpolationPoint"); let node = InterpolationPoint(iterator.Next ()); InterpolationPoint prevnode; target = node; if (node == null) { A_Log ("PathFollower " .. tid .. ": Can't find interpolation pt " .. args[0] + 256 * args[1] .. "%d\n"); return; } // Verify the path has enough nodes node.FormChain (); if (args[2] & 1) { // linear path; need 2 nodes if (node.Next == null) { A_Log ("PathFollower " .. tid .. ": Path needs at least 2 nodes\n"); return; } lastenemy = null; } else { // spline path; need 4 nodes if (node.Next == null || node.Next.Next == null || node.Next.Next.Next == null) { A_Log ("PathFollower " .. tid .. ": Path needs at least 4 nodes\n"); return; } // If the first node is in a loop, we can start there. // Otherwise, we need to start at the second node in the path. prevnode = node.ScanForLoop (); if (prevnode == null || prevnode.Next != node) { lastenemy = target; target = node.Next; } else { lastenemy = prevnode; } } } override void Deactivate (Actor activator) { bActive = false; } override void Activate (Actor activator) { if (!bActive) { CurrNode = InterpolationPoint(target); PrevNode = InterpolationPoint(lastenemy); if (CurrNode != null) { NewNode (); SetOrigin (CurrNode.Pos, false); Time = 0.; HoldTime = 0; bJustStepped = true; bActive = true; } } } override void Tick () { if (!bActive) return; if (bJustStepped) { bJustStepped = false; if (CurrNode.args[2]) { HoldTime = Level.time + CurrNode.args[2] * TICRATE / 8; SetXYZ(CurrNode.Pos); } } if (HoldTime > Level.time) return; // Splines must have a previous node. if (PrevNode == null && !(args[2] & 1)) { bActive = false; return; } // All paths must have a current node. if (CurrNode.Next == null) { bActive = false; return; } if (Interpolate ()) { Time += (8. / (max(1, CurrNode.args[1]) * TICRATE)); if (Time > 1.) { Time -= 1.; bJustStepped = true; PrevNode = CurrNode; CurrNode = CurrNode.Next; if (CurrNode != null) NewNode (); if (CurrNode == null || CurrNode.Next == null) Deactivate (self); if ((args[2] & 1) == 0 && CurrNode.Next.Next == null) Deactivate (self); } } } void NewNode () { let iterator = ActorIterator.Create(CurrNode.tid, "InterpolationSpecial"); InterpolationSpecial spec; while ( (spec = InterpolationSpecial(iterator.Next ())) ) { Level.ExecuteSpecial(spec.special, null, null, false, spec.args[0], spec.args[1], spec.args[2], spec.args[3], spec.args[4]); } } virtual bool Interpolate () { Vector3 dpos = (0, 0, 0); LinkContext ctx; if ((args[2] & 8) && Time > 0) { dpos = Pos; } if (CurrNode.Next==null) return false; UnlinkFromWorld (ctx); Vector3 newpos; if (args[2] & 1) { // linear newpos.X = Lerp(CurrNode.pos.X, CurrNode.Next.pos.X); newpos.Y = Lerp(CurrNode.pos.Y, CurrNode.Next.pos.Y); newpos.Z = Lerp(CurrNode.pos.Z, CurrNode.Next.pos.Z); } else { // spline if (CurrNode.Next.Next==null) return false; newpos.X = Splerp(PrevNode.pos.X, CurrNode.pos.X, CurrNode.Next.pos.X, CurrNode.Next.Next.pos.X); newpos.Y = Splerp(PrevNode.pos.Y, CurrNode.pos.Y, CurrNode.Next.pos.Y, CurrNode.Next.Next.pos.Y); newpos.Z = Splerp(PrevNode.pos.Z, CurrNode.pos.Z, CurrNode.Next.pos.Z, CurrNode.Next.Next.pos.Z); } SetXYZ(newpos); LinkToWorld (ctx); if (args[2] & 6) { if (args[2] & 8) { if (args[2] & 1) { // linear dpos.X = CurrNode.Next.pos.X - CurrNode.pos.X; dpos.Y = CurrNode.Next.pos.Y - CurrNode.pos.Y; dpos.Z = CurrNode.Next.pos.Z - CurrNode.pos.Z; } else if (Time > 0) { // spline dpos = newpos - dpos; } else { int realarg = args[2]; args[2] &= ~(2|4|8); Time += 0.1; dpos = newpos; Interpolate (); Time -= 0.1; args[2] = realarg; dpos = newpos - dpos; newpos -= dpos; SetXYZ(newpos); } if (args[2] & 2) { // adjust yaw Angle = VectorAngle(dpos.X, dpos.Y); } if (args[2] & 4) { // adjust pitch; double dist = dpos.XY.Length(); Pitch = dist != 0 ? VectorAngle(dist, -dpos.Z) : 0.; } } else { if (args[2] & 2) { // interpolate angle double angle1 = Normalize180(CurrNode.angle); double angle2 = angle1 + deltaangle(angle1, CurrNode.Next.angle); angle = Lerp(angle1, angle2); } if (args[2] & 1) { // linear if (args[2] & 4) { // interpolate pitch Pitch = Lerp(CurrNode.Pitch, CurrNode.Next.Pitch); } } else { // spline if (args[2] & 4) { // interpolate pitch Pitch = Splerp(PrevNode.Pitch, CurrNode.Pitch, CurrNode.Next.Pitch, CurrNode.Next.Next.Pitch); } } } } return true; } } /* == ActorMover: Moves any actor along a camera path == == Same as PathFollower, except == args[2], bit 7: make nonsolid == args[3] = tid of thing to move == == also uses: == tracer = thing to move */ class ActorMover : PathFollower { override void BeginPlay() { ChangeStatNum(STAT_ACTORMOVER); } override void PostBeginPlay () { Super.PostBeginPlay (); let iterator = ActorIterator.Create(args[3]); tracer = iterator.Next (); if (tracer == null) { A_Log("ActorMover " .. tid .. ": Can't find target " .. args[3] .. "\n"); } else { special1 = tracer.bNoGravity + (tracer.bNoBlockmap<<1) + (tracer.bSolid<<2) + (tracer.bInvulnerable<<4) + (tracer.bDormant<<8); } } override bool Interpolate () { if (tracer == null) return true; if (Super.Interpolate ()) { double savedz = tracer.pos.Z; tracer.SetZ(pos.Z); if (!tracer.TryMove (Pos.XY, true)) { tracer.SetZ(savedz); return false; } if (args[2] & 2) tracer.angle = angle; if (args[2] & 4) tracer.Pitch = Pitch; return true; } return false; } override void Activate (Actor activator) { if (tracer == null || bActive) return; Super.Activate (activator); let tracer = self.tracer; special1 = tracer.bNoGravity + (tracer.bNoBlockmap<<1) + (tracer.bSolid<<2) + (tracer.bInvulnerable<<4) + (tracer.bDormant<<8); bNoGravity = true; if (args[2] & 128) { LinkContext ctx; tracer.UnlinkFromWorld (ctx); bNoBlockmap = true; bSolid = false; tracer.LinkToWorld (ctx); } if (tracer.bIsMonster) { bInvulnerable = true; bDormant = true; } // Don't let the renderer interpolate between the actor's // old position and its new position. Interpolate (); tracer.ClearInterpolation(); } override void Deactivate (Actor activator) { if (bActive) { Super.Deactivate (activator); let tracer = self.tracer; if (tracer != null) { LinkContext ctx; tracer.UnlinkFromWorld (ctx); tracer.bNoGravity = (special1 & 1); tracer.bNoBlockmap = !!(special1 & 2); tracer.bSolid = !!(special1 & 4); tracer.bInvulnerable = !!(special1 & 8); tracer.bDormant = !!(special1 & 16); tracer.LinkToWorld (ctx); } } } } /* == MovingCamera: Moves any actor along a camera path == == Same as PathFollower, except == args[3] = tid of thing to look at (0 if none) == == Also uses: == tracer = thing to look at */ class MovingCamera : PathFollower { Actor activator; default { CameraHeight 0; } override void PostBeginPlay () { Super.PostBeginPlay (); Activator = null; if (args[3] != 0) { let iterator = ActorIterator.Create(args[3]); tracer = iterator.Next (); if (tracer == null) { A_Log("MovingCamera " .. tid .. ": Can't find thing " .. args[3] .. "\n"); } } } override bool Interpolate () { if (tracer == null) return Super.Interpolate (); if (Super.Interpolate ()) { angle = AngleTo(tracer, true); if (args[2] & 4) { // Also aim camera's pitch; Vector3 diff = Pos - tracer.Pos - (0, 0, tracer.Height / 2); double dist = diff.XY.Length(); Pitch = dist != 0 ? VectorAngle(dist, diff.Z) : 0.; } return true; } return false; } }