/* ** Command & Conquer Renegade(tm) ** Copyright 2025 Electronic Arts Inc. ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . */ /*********************************************************************************************** *** C O N F I D E N T I A L --- W E S T W O O D S T U D I O S *** *********************************************************************************************** * * * Project Name : LevelEdit * * * * $Archive:: /Commando/Code/wwmath/vehiclecurve.cpp $* * * * Author:: Patrick Smith * * * * $Modtime:: 6/12/01 10:02a $* * * * $Revision:: 8 $* * * *---------------------------------------------------------------------------------------------* * Functions: * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ #include "vehiclecurve.h" #include "vector3.h" #include "matrix3d.h" #include "persistfactory.h" #include "wwmathids.h" #include "wwmemlog.h" ////////////////////////////////////////////////////////////////////// // Save-Load stuff ////////////////////////////////////////////////////////////////////// SimplePersistFactoryClass _VehicleCurveFactory; //////////////////////////////////////////////////////////////// // Save/Load constants //////////////////////////////////////////////////////////////// enum { CHUNKID_PARENT = 0x11071217, CHUNKID_ARC_INFO, CHUNKID_VARIABLES }; enum { VARID_IS_DIRTY = 1, VARID_RADIUS, }; ////////////////////////////////////////////////////////////////////// // Local prototypes ////////////////////////////////////////////////////////////////////// bool Find_Tangent (const Vector3 ¢er, float radius, const Vector3 &point, bool clockwise, float *result); float Get_Angle_Delta (float angle1, float angle2, bool clockwise); void Find_Turn_Arc (const Matrix3D &transform, float radius, const Vector3 &prev_pt, const Vector3 &curr_pt, const Vector3 &next_pt, Vector3 *arc_center, bool *is_right_turn); void Find_Tangents (float radius, const Vector3 &prev_pt, const Vector3 &curr_pt, const Vector3 &next_pt, const Vector3 &arc_center, bool is_right_turn, float *point_angle, float *angle_in_delta, float *angle_out_delta); ////////////////////////////////////////////////////////////////////// // // Find_Tangent // ////////////////////////////////////////////////////////////////////// bool Find_Tangent ( const Vector3 & center, float radius, const Vector3 & point, bool clockwise, float * result ) { bool retval = false; // // Calculate the distance from the point to the center of the circle // float delta_x = point.X - center.X; float delta_y = point.Y - center.Y; float dist = ::sqrt (delta_x * delta_x + delta_y * delta_y); if (dist >= radius) { // // Determine the offset angle (from the line between the point and center) // where the 2 tangent points lie. // float angle_offset = WWMath::Acos (radius / dist); float base_angle = WWMath::Atan2 (delta_x, -delta_y); base_angle = WWMath::Wrap (base_angle, 0, DEG_TO_RADF (360)); // // Determine which tangent angle we would come across first, depending // on our orientation // float angle = 0; if (clockwise) { angle = base_angle - angle_offset; } else { angle = base_angle + angle_offset; } angle = WWMath::Wrap (angle, 0, DEG_TO_RADF (360)); (*result) = angle; retval = true; } return retval; } ////////////////////////////////////////////////////////////////////// // // Get_Angle_Delta // // Angle deltas need to be wrapped around 360 degrees differently // depending on the orientation (clockwise/counterclockwise). This // function takes orientation into consideration when determining // the delta. // ////////////////////////////////////////////////////////////////////// float Get_Angle_Delta ( float angle1, float angle2, bool clockwise ) { float result = angle1 - angle2; if (clockwise) { if (angle1 < angle2) { result = angle1 - (angle2 - DEG_TO_RADF (360)); } } else { if (angle1 > angle2) { result = (angle1 - DEG_TO_RADF (360)) - angle2; } } return result; } ////////////////////////////////////////////////////////////////////// // // Find_Turn_Arc // ////////////////////////////////////////////////////////////////////// void Find_Turn_Arc ( const Matrix3D & transform, float radius, const Vector3 & prev_pt, const Vector3 & curr_pt, const Vector3 & next_pt, Vector3 * arc_center, bool * is_right_turn ) { // // The center of the turn arc can lie anywhere on the circle centered // at the current point and 'radius' meters in radius. // // We will assume the optimal center of the turn arc will lie at // the point halfway between the angles formed by the (prev-curr) and // (next-curr) vectors. // float angle1 = ::WWMath::Atan2 ((prev_pt.Y - curr_pt.Y), prev_pt.X - curr_pt.X); angle1 = WWMath::Wrap (angle1, 0, DEG_TO_RADF (360)); float angle2 = ::WWMath::Atan2 ((next_pt.Y - curr_pt.Y), next_pt.X - curr_pt.X); angle2 = WWMath::Wrap (angle2, 0, DEG_TO_RADF (360)); float avg_angle = (angle1 + angle2) * 0.5F; // // Find the shortest delta between the two angles (either clockwise or // counterclockwise). // float delta1 = WWMath::Fabs (::Get_Angle_Delta (angle1, angle2, true)); float delta2 = WWMath::Fabs (::Get_Angle_Delta (angle1, angle2, false)); if (delta1 < delta2) { avg_angle = angle1 - (delta1 * 0.5F); } else { avg_angle = angle1 + (delta2 * 0.5F); } // // Find the point on the circle at this angle // arc_center->X = curr_pt.X + (radius * ::WWMath::Cos (avg_angle)); arc_center->Y = curr_pt.Y + (radius * ::WWMath::Sin (avg_angle)); arc_center->Z = curr_pt.Z; // // Will we be making a right turn or a left turn? // Vector3 rel_center; Matrix3D::Inverse_Transform_Vector (transform, *arc_center, &rel_center); (*is_right_turn) = (rel_center.Y > 0); return ; } ////////////////////////////////////////////////////////////////////// // // Find_Tangents // ////////////////////////////////////////////////////////////////////// void Find_Tangents ( float radius, const Vector3 & prev_pt, const Vector3 & curr_pt, const Vector3 & next_pt, const Vector3 & arc_center, bool is_right_turn, float * point_angle, float * angle_in_delta, float * angle_out_delta ) { // // Find the 'in' and 'out' tangent angles // float angle_in = 0; float angle_out = 0; bool valid_in = ::Find_Tangent (arc_center, radius, prev_pt, is_right_turn, &angle_in); bool valid_out = ::Find_Tangent (arc_center, radius, next_pt, !is_right_turn, &angle_out); // // Find the angle where the current position lies on the turn arc // (*point_angle) = ::WWMath::Atan2 (curr_pt.X - arc_center.X, -(curr_pt.Y - arc_center.Y)); (*point_angle) = WWMath::Wrap ((*point_angle), 0, DEG_TO_RADF (360)); // // If the tangent-in is valid, find its delta from the 'point angle. // if (valid_in) { (*angle_in_delta) = ::Get_Angle_Delta (angle_in, (*point_angle), is_right_turn); } else { (*angle_in_delta) = 0; } // // If the tangent-out is valid, find its delta from the 'point angle. // if (valid_out) { (*angle_out_delta) = ::Get_Angle_Delta (angle_out, (*point_angle), !is_right_turn); } else { (*angle_out_delta) = 0; } return ; } ////////////////////////////////////////////////////////////////////// // // Update_Arc_List // ////////////////////////////////////////////////////////////////////// void VehicleCurveClass::Update_Arc_List (void) { WWMEMLOG(MEM_PATHFIND); m_ArcList.Delete_All (); // // Bail out if there is nothing to do // int count = Key_Count (); if (count == 0) { return ; } // // Add a record for the starting point of the arc... // ArcInfoStruct arc_start; arc_start.point_in = Keys[0].Point; arc_start.point_out = Keys[0].Point; arc_start.center = Keys[0].Point; arc_start.point_angle = 0; arc_start.radius = 0; arc_start.angle_in_delta = 0; arc_start.angle_out_delta = 0; m_ArcList.Add (arc_start); // // Loop over each 'interior' point and generate arc information // for each. // for (int index = 1; index < count - 1; index ++) { // // Get information about the previous, next, and current points. // Vector3 prev_pt; Vector3 next_pt; Vector3 curr_pt; float time = 0; Get_Key (index-1, &prev_pt, &time); Get_Key (index, &curr_pt, &time); Get_Key (index+1, &next_pt, &time); // // Determine the last known point on the path // Vector3 last_path_pt = m_ArcList[index-1].point_out; // // Create a transformation matrix to simulate the vehicle's position and // orientation at the last point... // Vector3 x_vector (curr_pt - last_path_pt); Vector3 z_vector (0, 0, 1); x_vector.Normalize (); Vector3 y_vector = Vector3::Cross_Product (x_vector, z_vector); Matrix3D tm (x_vector, y_vector, z_vector, last_path_pt); // // Find where the turn arc should be centered and whether we should // make a right-turn or a left turn... // bool is_right_turn = false; Vector3 arc_center (0, 0, 0); ::Find_Turn_Arc ( tm, m_Radius, last_path_pt, curr_pt, next_pt, &arc_center, &is_right_turn); // // Determine where the vehicle should enter and exit the turn // float angle_in_delta = 0; float angle_out_delta = 0; float point_angle = 0; ::Find_Tangents ( m_Radius, last_path_pt, curr_pt, next_pt, arc_center, is_right_turn, &point_angle, &angle_in_delta, &angle_out_delta); // // Determine at what points these angles intersect the arc // Vector3 point_in (0, 0, 0); point_in.X = arc_center.X + (m_Radius * ::WWMath::Sin (point_angle + angle_in_delta)); point_in.Y = arc_center.Y + (m_Radius * -::WWMath::Cos (point_angle + angle_in_delta)); Vector3 point_out (0, 0, 0); point_out.X = arc_center.X + (m_Radius * ::WWMath::Sin (point_angle + angle_out_delta)); point_out.Y = arc_center.Y + (m_Radius * -::WWMath::Cos (point_angle + angle_out_delta)); // // Sanity check to ensure the vehicle doesn't try to go the long way around the // turn arc... // if ( angle_in_delta > DEG_TO_RADF (200) || angle_out_delta > DEG_TO_RADF (200) || angle_in_delta < -DEG_TO_RADF (200) || angle_out_delta < -DEG_TO_RADF (200) ) { // // Record information about this arc // ArcInfoStruct arc_info; arc_info.center = curr_pt; arc_info.point_angle = 0; arc_info.point_in = curr_pt; arc_info.point_out = curr_pt; arc_info.radius = 0; arc_info.angle_in_delta = 0; arc_info.angle_out_delta = 0; m_ArcList.Add (arc_info); } else { // // Record information about this arc // ArcInfoStruct arc_info; arc_info.center = arc_center; arc_info.point_angle = point_angle; arc_info.point_in = point_in; arc_info.point_out = point_out; arc_info.radius = m_Radius; arc_info.angle_in_delta = angle_in_delta; arc_info.angle_out_delta = angle_out_delta; m_ArcList.Add (arc_info); } } // // Add a record for the starting point of the arc... // if (count > 1) { ArcInfoStruct arc_end; arc_end.point_in = Keys[count-1].Point; arc_end.point_out = Keys[count-1].Point; arc_end.center = Keys[count-1].Point; arc_end.point_angle = 0; arc_end.radius = 0; arc_end.angle_in_delta = 0; arc_end.angle_out_delta = 0; m_ArcList.Add (arc_end); } m_IsDirty = false; return ; } ////////////////////////////////////////////////////////////////////// // // Evaluate // ////////////////////////////////////////////////////////////////////// void VehicleCurveClass::Evaluate (float time, Vector3 *set_val) { int count = Keys.Count (); m_Sharpness = 0; if (time < Keys[0].Time) { *set_val = Keys[0].Point; m_LastTime = Keys[0].Time; return; } if (time >= Keys[count - 1].Time) { *set_val = Keys[count - 1].Point; m_LastTime = Keys[count - 1].Time; return; } // // Update the arc information if any of the keys have changed... // if (m_IsDirty) { Update_Arc_List (); } // // Determine which segment we are on // int index0 = 0; int index1 = 0; float seg_time = 0; Find_Interval (time, &index0, &index1, &seg_time); ArcInfoStruct &arc_info0 = m_ArcList[index0]; ArcInfoStruct &arc_info1 = m_ArcList[index1]; // // Determine the lengths of each segment of this curve. // The segments are: // - Exit curve from prev point // - Straight line from exit of last curve to enter of this curve // - Enter curve for the current point // float arc_length0 = arc_info0.radius * WWMath::Fabs (arc_info0.angle_out_delta); float arc_length1 = arc_info1.radius * WWMath::Fabs (arc_info1.angle_in_delta); float other_length = ((arc_info1.point_in - arc_info0.point_out).Length ()) / 2; float total_length = arc_length0 + arc_length1 + other_length; // // Determine at what times we should switch between parts of the segment // float time1 = arc_length0 / total_length; float time2 = (arc_length0 + other_length) / total_length; // // Determine which part of the segment we are on // if (seg_time < time1) { // // We are on the initial curve of the segment, so calculate where // on the curve we are... // //float percent = seg_time / time1; //float angle = arc_info0.point_angle + (arc_info0.angle_out_delta) * percent; float angle = arc_info0.point_angle + arc_info0.angle_out_delta; set_val->X = arc_info0.center.X + (arc_info0.radius * ::WWMath::Sin (angle)); set_val->Y = arc_info0.center.Y + (arc_info0.radius * -::WWMath::Cos (angle)); m_Sharpness = WWMath::Clamp (WWMath::Fabs (arc_info0.angle_out_delta) / DEG_TO_RADF (15), 0, 1.0F); m_SharpnessPos.X = set_val->X; m_SharpnessPos.Y = set_val->Y; m_SharpnessPos.Z = Keys[index0].Point.Z + (Keys[index1].Point.Z - Keys[index0].Point.Z) * seg_time; m_LastTime = Keys[index0].Time + (Keys[index1].Time - Keys[index0].Time) * time1; } else if (seg_time < time2) { // // We are on the line between the two curves, so calculate where on // the line we are // float percent = (seg_time - time1) / (time2 - time1); if (percent == 0) { set_val->X = arc_info0.point_out.X; set_val->Y = arc_info0.point_out.Y; } else { set_val->X = arc_info1.point_in.X; set_val->Y = arc_info1.point_in.Y; } //set_val->X = arc_info0.point_out.X + (arc_info1.point_in.X - arc_info0.point_out.X) * percent; //set_val->Y = arc_info0.point_out.Y + (arc_info1.point_in.Y - arc_info0.point_out.Y) * percent; m_Sharpness = WWMath::Clamp (WWMath::Fabs (arc_info1.angle_out_delta) / DEG_TO_RADF (15), 0, 1.0F); m_SharpnessPos = arc_info1.point_in; m_LastTime = Keys[index0].Time + (Keys[index1].Time - Keys[index0].Time) * time2; } else { // // We are on the ending curve of the segment, so calculate where // on the curve we are... // /*float percent = 1.0F - ((seg_time - time2) / (1.0F - time2)); float angle = arc_info1.point_angle + (arc_info1.angle_in_delta * percent); set_val->X = arc_info1.center.X + (arc_info1.radius * ::WWMath::Sin (angle)); set_val->Y = arc_info1.center.Y + (arc_info1.radius * -::WWMath::Cos (angle)); */ float angle = arc_info1.point_angle + (arc_info1.angle_out_delta); set_val->X = arc_info1.center.X + (arc_info1.radius * ::WWMath::Sin (angle)); set_val->Y = arc_info1.center.Y + (arc_info1.radius * -::WWMath::Cos (angle)); m_Sharpness = WWMath::Clamp (WWMath::Fabs (arc_info1.angle_out_delta) / DEG_TO_RADF (15), 0, 1.0F); m_SharpnessPos.X = set_val->X; m_SharpnessPos.Y = set_val->Y; m_SharpnessPos.Z = Keys[index0].Point.Z + (Keys[index1].Point.Z - Keys[index0].Point.Z) * seg_time; m_LastTime = Keys[index1].Time; } // // Our Z value is just a linear interpolation // set_val->Z = Keys[index0].Point.Z + (Keys[index1].Point.Z - Keys[index0].Point.Z) * seg_time; return ; } const PersistFactoryClass & VehicleCurveClass::Get_Factory(void) const { return _VehicleCurveFactory; } //////////////////////////////////////////////////////////////////////////////////////////// // // Save // //////////////////////////////////////////////////////////////////////////////////////////// bool VehicleCurveClass::Save (ChunkSaveClass &csave) { csave.Begin_Chunk (CHUNKID_PARENT); Curve3DClass::Save (csave); csave.End_Chunk (); csave.Begin_Chunk (CHUNKID_VARIABLES); // // Save each variable to its own microchunk // WRITE_MICRO_CHUNK (csave, VARID_IS_DIRTY, m_IsDirty); WRITE_MICRO_CHUNK (csave, VARID_RADIUS, m_Radius); csave.End_Chunk (); // // Save each arc info struct to its own chunk // for (int index = 0; index < m_ArcList.Count (); index ++) { ArcInfoStruct &arc_info = m_ArcList[index]; csave.Begin_Chunk (CHUNKID_ARC_INFO); csave.Write (&arc_info, sizeof (arc_info)); csave.End_Chunk (); } return true; } //////////////////////////////////////////////////////////////////////////////////////////// // // Load // //////////////////////////////////////////////////////////////////////////////////////////// bool VehicleCurveClass::Load (ChunkLoadClass &cload) { while (cload.Open_Chunk ()) { switch (cload.Cur_Chunk_ID ()) { case CHUNKID_PARENT: Curve3DClass::Load (cload); break; case CHUNKID_ARC_INFO: { ArcInfoStruct arc_info; cload.Read (&arc_info, sizeof (arc_info)); m_ArcList.Add (arc_info); } break; case CHUNKID_VARIABLES: Load_Variables (cload); break; } cload.Close_Chunk (); } return true; } /////////////////////////////////////////////////////////////////////// // // Load_Variables // /////////////////////////////////////////////////////////////////////// void VehicleCurveClass::Load_Variables (ChunkLoadClass &cload) { // // Loop through all the microchunks that define the variables // while (cload.Open_Micro_Chunk ()) { switch (cload.Cur_Micro_Chunk_ID ()) { READ_MICRO_CHUNK (cload, VARID_IS_DIRTY, m_IsDirty); READ_MICRO_CHUNK (cload, VARID_RADIUS, m_Radius); } cload.Close_Micro_Chunk (); } return ; }