Skip to content

Commit afcc469

Browse files
HalbannCharleRoger
andauthored
Fix projection mistake in Optimised Vector Lines (#315)
Fix for issue #306 * Optimised Vector Lines cleanup * Fix Optimised Vector Lines projection mistake * Improved vector line projection explanation. * Fix OptimisedVectorLines.ScreenToWorldPoint * Charlie's ScreenToWorldPoint Optimisation * Added camera projection debugging --------- Co-authored-by: Charles Rogers <[email protected]>
1 parent d1fe6b1 commit afcc469

File tree

1 file changed

+262
-35
lines changed

1 file changed

+262
-35
lines changed

KSPCommunityFixes/Performance/OptimisedVectorLines.cs

Lines changed: 262 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// Check the results of the replacement camera projection functions against Unity's own for correctness.
2+
//#define DEBUG_CAMERAPROJECTION
3+
14
using HarmonyLib;
25
using KSPCommunityFixes.Library;
36
using System;
@@ -7,6 +10,11 @@
710
using UnityEngine;
811
using Vectrosity;
912

13+
#if DEBUG_CAMERAPROJECTION
14+
using System.IO;
15+
using System.Text;
16+
#endif
17+
1018
namespace 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

Comments
 (0)