1+ // Check the results of the replacement camera projection functions against Unity's own for correctness.
2+ //#define DEBUG_CAMERAPROJECTION
3+
14using HarmonyLib ;
25using KSPCommunityFixes . Library ;
36using System ;
710using UnityEngine ;
811using Vectrosity ;
912
13+ #if DEBUG_CAMERAPROJECTION
14+ using System . IO ;
15+ using System . Text ;
16+ #endif
17+
1018namespace KSPCommunityFixes . Performance
1119{
1220 public class OptimisedVectorLines : BasePatch
@@ -91,12 +99,12 @@ private static IEnumerable<CodeInstruction> ReplaceScreenToWorldPoint(IEnumerabl
9199 return ReplaceCall ( instructions , Camera_ScreenToWorldPoint , VectorLineOptimisation_ScreenToWorldPoint , count ) ;
92100 }
93101
94- private static IEnumerable < CodeInstruction > ReplaceScreenToViewportPoint ( IEnumerable < CodeInstruction > instructions , int count )
102+ private static IEnumerable < CodeInstruction > ReplaceViewportToWorldPoint ( IEnumerable < CodeInstruction > instructions , int count )
95103 {
96- MethodInfo Camera_ScreenToViewportPoint = AccessTools . Method ( typeof ( Camera ) , nameof ( Camera . ScreenToViewportPoint ) , new Type [ ] { typeof ( Vector3 ) } ) ;
97- MethodInfo VectorLineOptimisation_ScreenToViewportPoint = AccessTools . Method ( typeof ( VectorLineCameraProjection ) , nameof ( VectorLineCameraProjection . ScreenToViewportPoint ) ) ;
104+ MethodInfo Camera_ViewportToWorldPoint = AccessTools . Method ( typeof ( Camera ) , nameof ( Camera . ViewportToWorldPoint ) , new Type [ ] { typeof ( Vector3 ) } ) ;
105+ MethodInfo VectorLineOptimisation_ViewportToWorldPoint = AccessTools . Method ( typeof ( VectorLineCameraProjection ) , nameof ( VectorLineCameraProjection . ViewportToWorldPoint ) ) ;
98106
99- return ReplaceCall ( instructions , Camera_ScreenToViewportPoint , VectorLineOptimisation_ScreenToViewportPoint , count ) ;
107+ return ReplaceCall ( instructions , Camera_ViewportToWorldPoint , VectorLineOptimisation_ViewportToWorldPoint , count ) ;
100108 }
101109
102110 #endregion
@@ -111,7 +119,8 @@ public static class VectorLineCameraProjection
111119 public static long lastCachedFrame ;
112120
113121 private static TransformMatrix worldToClip ;
114- private static TransformMatrix clipToWorld ;
122+ private static TransformMatrix screenToWorld ; // Not a normal transformation matrix, do not use as such.
123+ private static Matrix4x4 projectionMatrix ;
115124
116125 // Storing viewport info instead of using Rect properties grants us a few extra frames.
117126 public struct ViewportInfo
@@ -142,20 +151,27 @@ private static void UpdateCache()
142151 Camera camera = VectorLine . cam3D ;
143152
144153 viewport = new ViewportInfo ( camera . pixelRect ) ;
145-
146- Matrix4x4 worldToClip = camera . projectionMatrix * camera . worldToCameraMatrix ;
147- VectorLineCameraProjection . worldToClip = new TransformMatrix ( ref worldToClip ) ;
148-
149- Matrix4x4 worldToCameraInv = camera . worldToCameraMatrix . inverse ;
150- Matrix4x4 projectionInv = camera . projectionMatrix . inverse ;
151- projectionInv . m02 += projectionInv . m03 ;
152- projectionInv . m12 += projectionInv . m13 ;
153- projectionInv . m22 += projectionInv . m23 ;
154-
155- Matrix4x4 clipToWorld = worldToCameraInv * projectionInv ;
156- VectorLineCameraProjection . clipToWorld = new TransformMatrix ( clipToWorld . m00 , clipToWorld . m01 , clipToWorld . m02 , camera . worldToCameraMatrix . inverse . m03 ,
157- clipToWorld . m10 , clipToWorld . m11 , clipToWorld . m12 , camera . worldToCameraMatrix . inverse . m13 ,
158- clipToWorld . m20 , clipToWorld . m21 , clipToWorld . m22 , camera . worldToCameraMatrix . inverse . m23 ) ;
154+ projectionMatrix = camera . projectionMatrix ;
155+
156+ // WorldToClip.
157+ // Normally this would be a 4x4 matrix, but we omit the third row.
158+ // This is because users of WorldToScreen/Viewport expect world distance from the camera plane
159+ // (which is w after projection) in the z component, not z in NDC.
160+ // Therefore we never need to calculate the z component, only x, y and w.
161+ // w is the right value because m32 in the projection matrix just copies z in view space into w.
162+
163+ Matrix4x4 worldToClip = projectionMatrix * camera . worldToCameraMatrix ;
164+ VectorLineCameraProjection . worldToClip = new TransformMatrix (
165+ worldToClip . m00 , worldToClip . m01 , worldToClip . m02 , worldToClip . m03 ,
166+ worldToClip . m10 , worldToClip . m11 , worldToClip . m12 , worldToClip . m13 ,
167+ worldToClip . m30 , worldToClip . m31 , worldToClip . m32 , worldToClip . m33 ) ;
168+
169+ // ScreenToWorld (NB: not ClipToWorld).
170+ // Credit to Charles Rogers for this incredible solution.
171+ // It takes screen space coordinates (x, y, w) and converts them directly to world space
172+ // without any intermediate steps. It's at least 6x faster than Unity's own ScreenToWorldPoint.
173+
174+ BuildScreenToWorldMatrix ( out screenToWorld , in worldToClip ) ;
159175 }
160176
161177 #region World to Clip
@@ -173,15 +189,23 @@ public static Vector3 WorldToScreenPoint(Camera camera, Vector3 worldPosition)
173189
174190 double x = worldPosition . x ;
175191 double y = worldPosition . y ;
176- double z = worldPosition . z ;
192+ double w = worldPosition . z ;
177193
178- worldToClip . MutateMultiplyPoint3x4 ( ref x , ref y , ref z ) ;
194+ // z becomes w after this projection with our xyw matrix. Clip space z is never calculated.
195+ worldToClip . MutateMultiplyPoint3x4 ( ref x , ref y , ref w ) ;
179196
180- double num = 0.5 / z ;
197+ // Perspective division and viewport conversion.
198+ double num = 0.5 / w ;
181199 x = ( 0.5 + num * x ) * viewport . width + viewport . x ;
182200 y = ( 0.5 + num * y ) * viewport . height + viewport . y ;
183201
184- return new Vector3 ( ( float ) x , ( float ) y , ( float ) z ) ;
202+ #if ! DEBUG_CAMERAPROJECTION
203+ return new Vector3 ( ( float ) x , ( float ) y , ( float ) w ) ;
204+ #else
205+ Vector3 result = new Vector3 ( ( float ) x , ( float ) y , ( float ) w ) ;
206+ CheckResult ( nameof ( WorldToScreenPoint ) , in worldPosition , camera . WorldToScreenPoint ( worldPosition ) , in result ) ;
207+ return result ;
208+ #endif
185209 }
186210
187211 public static Vector3 WorldToViewportPoint ( Camera camera , Vector3 worldPosition )
@@ -194,21 +218,49 @@ public static Vector3 WorldToViewportPoint(Camera camera, Vector3 worldPosition)
194218
195219 double x = worldPosition . x ;
196220 double y = worldPosition . y ;
197- double z = worldPosition . z ;
221+ double w = worldPosition . z ;
198222
199- worldToClip . MutateMultiplyPoint3x4 ( ref x , ref y , ref z ) ;
223+ // z becomes w after this projection with our xyw matrix. Clip space z is never calculated.
224+ worldToClip . MutateMultiplyPoint3x4 ( ref x , ref y , ref w ) ;
200225
201- double num = 0.5 / z ;
226+ // Perspective division and viewport conversion.
227+ double num = 0.5 / w ;
202228 x = 0.5 + num * x ;
203229 y = 0.5 + num * y ;
204230
205- return new Vector3 ( ( float ) x , ( float ) y , ( float ) z ) ;
231+ #if ! DEBUG_CAMERAPROJECTION
232+ return new Vector3 ( ( float ) x , ( float ) y , ( float ) w ) ;
233+ #else
234+ Vector3 result = new Vector3 ( ( float ) x , ( float ) y , ( float ) w ) ;
235+ CheckResult ( nameof ( WorldToViewportPoint ) , in worldPosition , camera . WorldToViewportPoint ( worldPosition ) , in result ) ;
236+ return result ;
237+ #endif
206238 }
207239
208240 #endregion
209241
210242 #region Clip to World
211243
244+ private static void BuildScreenToWorldMatrix ( out TransformMatrix matrix , in Matrix4x4 worldToClip )
245+ {
246+ double xScaled = viewport . x / viewport . halfWidth ;
247+ double yScaled = viewport . y / viewport . halfHeight ;
248+
249+ Matrix4x4 clipToWorld = worldToClip . inverse ;
250+ matrix . m00 = clipToWorld . m00 / viewport . halfWidth ;
251+ matrix . m01 = clipToWorld . m01 / viewport . halfHeight ;
252+ matrix . m02 = - clipToWorld . m00 * xScaled - clipToWorld . m01 * yScaled - clipToWorld . m02 * projectionMatrix . m22 + clipToWorld . m03 - clipToWorld . m00 - clipToWorld . m01 ;
253+ matrix . m03 = clipToWorld . m02 * projectionMatrix . m23 ;
254+ matrix . m10 = clipToWorld . m10 / viewport . halfWidth ;
255+ matrix . m11 = clipToWorld . m11 / viewport . halfHeight ;
256+ matrix . m12 = - clipToWorld . m10 * xScaled - clipToWorld . m11 * yScaled - clipToWorld . m12 * projectionMatrix . m22 + clipToWorld . m13 - clipToWorld . m10 - clipToWorld . m11 ;
257+ matrix . m13 = clipToWorld . m12 * projectionMatrix . m23 ;
258+ matrix . m20 = clipToWorld . m20 / viewport . halfWidth ;
259+ matrix . m21 = clipToWorld . m21 / viewport . halfHeight ;
260+ matrix . m22 = - clipToWorld . m20 * xScaled - clipToWorld . m21 * yScaled - clipToWorld . m22 * projectionMatrix . m22 + clipToWorld . m23 - clipToWorld . m20 - clipToWorld . m21 ;
261+ matrix . m23 = clipToWorld . m22 * projectionMatrix . m23 ;
262+ }
263+
212264 public static Vector3 ScreenToWorldPoint ( Camera camera , Vector3 screenPosition )
213265 {
214266 //if (!patchEnabled)
@@ -221,18 +273,24 @@ public static Vector3 ScreenToWorldPoint(Camera camera, Vector3 screenPosition)
221273 double y = screenPosition . y ;
222274 double z = screenPosition . z ;
223275
224- x = z * ( ( x - viewport . x ) / viewport . halfWidth - 1 ) ;
225- y = z * ( ( y - viewport . y ) / viewport . halfHeight - 1 ) ;
226-
227- clipToWorld . MutateMultiplyPoint3x4 ( ref x , ref y , ref z ) ;
228-
229- return new Vector3 ( ( float ) x , ( float ) y , ( float ) z ) ;
276+ // NB: not a normal matrix multiplication.
277+ double x1 = ( screenToWorld . m00 * x + screenToWorld . m01 * y + screenToWorld . m02 ) * z + screenToWorld . m03 ;
278+ double y1 = ( screenToWorld . m10 * x + screenToWorld . m11 * y + screenToWorld . m12 ) * z + screenToWorld . m13 ;
279+ double z1 = ( screenToWorld . m20 * x + screenToWorld . m21 * y + screenToWorld . m22 ) * z + screenToWorld . m23 ;
280+
281+ #if ! DEBUG_CAMERAPROJECTION
282+ return new Vector3 ( ( float ) x1 , ( float ) y1 , ( float ) z1 ) ;
283+ #else
284+ Vector3 result = new Vector3 ( ( float ) x1 , ( float ) y1 , ( float ) z1 ) ;
285+ CheckResult ( nameof ( ScreenToWorldPoint ) , in screenPosition , camera . ScreenToWorldPoint ( screenPosition ) , in result ) ;
286+ return result ;
287+ #endif
230288 }
231289
232- public static Vector3 ScreenToViewportPoint ( Camera camera , Vector3 position )
290+ public static Vector3 ViewportToWorldPoint ( Camera camera , Vector3 position )
233291 {
234292 //if (!patchEnabled)
235- // return camera.ScreenToViewportPoint (position);
293+ // return camera.ViewportToWorldPoint (position);
236294
237295 //if (lastCachedFrame != KSPCommunityFixes.frameCount)
238296 // UpdateCache();
@@ -242,5 +300,174 @@ public static Vector3 ScreenToViewportPoint(Camera camera, Vector3 position)
242300 }
243301
244302 #endregion
303+
304+ #if DEBUG_CAMERAPROJECTION
305+ #region Debugging
306+
307+ // Debugging settings. Edit these values in-game with UnityExplorerKSP.
308+ public static int maxUlps = 1000 ;
309+ public static int maxLogsPerFrame = 2 ; // Logging many hundreds or thousands of times per frame will freeze the game.
310+ public static LogLevel logLevel = LogLevel . All ;
311+
312+ public enum LogLevel
313+ {
314+ None ,
315+ Summary ,
316+ All
317+ }
318+
319+ private static long lastFrameDebug ;
320+ private static long logsThisFrame ;
321+ private static long totalDiscrepanciesThisFrame ;
322+ private static long totalCorrectThisFrame ;
323+ private static Dictionary < string , int > logCount = new Dictionary < string , int > ( ) ;
324+ private static StringBuilder sb = new StringBuilder ( ) ;
325+
326+ private static Dictionary < int , FrameDebugData > csv = new Dictionary < int , FrameDebugData > ( ) ;
327+ private static bool takingCSV ;
328+ private static int csvFrameCount ;
329+ private static int framesPerUlps ;
330+ private static int [ ] ulpLevels ;
331+
332+ private struct FrameDebugData
333+ {
334+ public int count ;
335+ public float averageCorrect ;
336+ public float averageIncorrect ;
337+ }
338+
339+ private static void CheckResult ( string functionName , in Vector3 input , Vector3 expected , in Vector3 result )
340+ {
341+ // Exit early if we are not logging.
342+ if ( logLevel == LogLevel . None && ! takingCSV )
343+ return ;
344+
345+ // Once per frame that one of the functions is used.
346+ if ( lastFrameDebug != KSPCommunityFixes . UpdateCount )
347+ {
348+ // Summary is logged regardless of whether there are any discrepancies.
349+ if ( logLevel == LogLevel . Summary )
350+ {
351+ sb . Clear ( ) ;
352+ sb . AppendLine ( $ "[KSPCF/OptimisedVectorLines]: Camera projection result mismatches detected:") ;
353+ foreach ( var kvp in logCount )
354+ sb . AppendLine ( $ "{ kvp . Key } : { kvp . Value } ") ;
355+
356+ sb . AppendLine ( $ "Total: { totalDiscrepanciesThisFrame } mismatches, { totalCorrectThisFrame } correct results this frame ({ totalCorrectThisFrame / ( float ) ( totalCorrectThisFrame + totalDiscrepanciesThisFrame ) * 100 : N0} %).") ;
357+
358+ Debug . Log ( sb . ToString ( ) ) ;
359+ logCount . Clear ( ) ;
360+ }
361+
362+ if ( takingCSV )
363+ {
364+ maxUlps = ulpLevels [ Mathf . FloorToInt ( csvFrameCount / ( float ) framesPerUlps ) ] ;
365+ csvFrameCount ++ ;
366+
367+ AddToCSV ( ) ;
368+
369+ if ( csvFrameCount >= framesPerUlps * ulpLevels . Length )
370+ FinishCSV ( ) ;
371+ }
372+
373+ lastFrameDebug = KSPCommunityFixes . UpdateCount ;
374+ logsThisFrame = 0 ;
375+ totalCorrectThisFrame = 0 ;
376+ totalDiscrepanciesThisFrame = 0 ;
377+ }
378+
379+ // Do the actual comparison.
380+ if ( Numerics . AlmostEqual ( result . x , expected . x , maxUlps )
381+ && Numerics . AlmostEqual ( result . y , expected . y , maxUlps )
382+ && Numerics . AlmostEqual ( result . z , expected . z , maxUlps ) )
383+ {
384+ totalCorrectThisFrame ++ ;
385+
386+ return ;
387+ }
388+
389+ totalDiscrepanciesThisFrame ++ ;
390+
391+ // Only log that there was a mismatch on this function if it's only a summary.
392+ if ( logLevel == LogLevel . Summary )
393+ {
394+ logCount [ functionName ] = logCount . GetValueOrDefault ( functionName ) + 1 ;
395+ return ;
396+ }
397+ else if ( logLevel == LogLevel . All )
398+ {
399+ // Log the details of the mismatch, only if the maxLogsPerFrame is not exceeded.
400+ if ( logsThisFrame >= maxLogsPerFrame )
401+ return ;
402+
403+ logsThisFrame ++ ;
404+ sb . Clear ( ) ;
405+ sb . AppendLine ( $ "[KSPCF/OptimisedVectorLines]: Camera projection result mismatch in { functionName } on frame { KSPCommunityFixes . UpdateCount } .") ;
406+ sb . AppendLine ( $ "Input: ({ input . x : G9} , { input . y : G9} , { input . z : G9} )") ;
407+ sb . AppendLine ( $ "Expected: ({ expected . x : G9} , { expected . y : G9} , { expected . z : G9} )") ;
408+ sb . AppendLine ( $ "Result: ({ result . x : G9} , { result . y : G9} , { result . z : G9} )") ;
409+ Debug . LogWarning ( sb . ToString ( ) ) ;
410+ }
411+ }
412+
413+ // Call from the UnityExplorer console to start taking a CSV.
414+ public static void StartCSV ( int framesPerUlps , params int [ ] ulpLevels )
415+ {
416+ if ( takingCSV )
417+ return ;
418+
419+ csv . Clear ( ) ;
420+ takingCSV = true ;
421+ csvFrameCount = 0 ;
422+
423+ VectorLineCameraProjection . framesPerUlps = framesPerUlps ;
424+ VectorLineCameraProjection . ulpLevels = ulpLevels ;
425+ }
426+
427+ private static void AddToCSV ( )
428+ {
429+ if ( csv . TryGetValue ( maxUlps , out FrameDebugData data ) )
430+ {
431+ data . count ++ ;
432+ data . averageCorrect += ( totalCorrectThisFrame - data . averageCorrect ) / data . count ;
433+ data . averageIncorrect += ( totalDiscrepanciesThisFrame - data . averageIncorrect ) / data . count ;
434+
435+ csv [ maxUlps ] = data ;
436+ }
437+ else
438+ {
439+ data = new FrameDebugData
440+ {
441+ count = 1 ,
442+ averageCorrect = totalCorrectThisFrame ,
443+ averageIncorrect = totalDiscrepanciesThisFrame
444+ } ;
445+ csv . Add ( maxUlps , data ) ;
446+ }
447+ }
448+
449+ private static void FinishCSV ( )
450+ {
451+ if ( ! takingCSV )
452+ return ;
453+
454+ takingCSV = false ;
455+
456+ string dateTime = DateTime . Now . ToString ( "yyyy-MM-dd_HH-mm-ss" ) ;
457+ string path = Path . Combine ( KSPCommunityFixes . ModPath , $ "KSPCF_CameraProjection_{ dateTime } .csv") ;
458+
459+ sb . Clear ( ) ;
460+ sb . AppendLine ( "maxUlps,averageCorrect,averageIncorrect" ) ;
461+ foreach ( var kvp in csv )
462+ sb . AppendLine ( $ "{ kvp . Key } ,{ kvp . Value . averageCorrect } ,{ kvp . Value . averageIncorrect } ") ;
463+
464+ File . WriteAllText ( path , sb . ToString ( ) ) ;
465+ Debug . Log ( $ "[KSPCF/OptimisedVectorLines]: CSV saved to { path } .") ;
466+ csv . Clear ( ) ;
467+ sb . Clear ( ) ;
468+ }
469+
470+ #endregion
471+ #endif
245472 }
246473}
0 commit comments