diff --git a/src/ExplorerBehaviour.cs b/src/ExplorerBehaviour.cs index 2d94683..fe555de 100644 --- a/src/ExplorerBehaviour.cs +++ b/src/ExplorerBehaviour.cs @@ -8,6 +8,8 @@ using Il2CppInterop.Runtime.Injection; #endif #endif using UnityExplorer.UI.Panels; +using System; + namespace UnityExplorer { @@ -195,7 +197,7 @@ namespace UnityExplorer this.controlPoints = controlPoints; this.closedLoop = closedLoop; - GenerateSplinePoints(); + GenerateSplinePoints2(); } //Updates control points @@ -208,7 +210,7 @@ namespace UnityExplorer this.controlPoints = controlPoints; - GenerateSplinePoints(); + GenerateSplinePoints2(); } //Updates closed loop values @@ -216,7 +218,7 @@ namespace UnityExplorer { this.closedLoop = closedLoop; - GenerateSplinePoints(); + GenerateSplinePoints2(); } //Draws a line between every point and the next. @@ -495,6 +497,8 @@ namespace UnityExplorer if(ExplorerCore.CameraPathsManager != null && currentPoint < splinePoints.Length){ CatmullRomPoint point = splinePoints[currentPoint]; + //ExplorerCore.Log($"velocity: {Vector3.Distance(point.position,FreeCamPanel.ourCamera.transform.position)}/node"); + FreeCamPanel.ourCamera.transform.position = point.position; FreeCamPanel.ourCamera.transform.rotation = point.rotation; FreeCamPanel.ourCamera.fieldOfView = point.fov; @@ -523,5 +527,273 @@ namespace UnityExplorer if(currentPoint < ExplorerCore.CameraPathsManager.GetPoints().Length) playingPath = true; } + + public float[] GenerateSplinePointsByRes(int resolution = 3000) //resolution of each segment + { + List splineSegmentDistances = new List(); + + Vector3 p0, p1; //Start point, end point + Vector3 m0, m1; //Tangents + + // First for loop goes through each individual control point and connects it to the next, so 0-1, 1-2, 2-3 and so on + int closedAdjustment = closedLoop ? 0 : 1; + for (int currentPoint = 0; currentPoint < controlPoints.Length - closedAdjustment; currentPoint++) + { + bool closedLoopFinalPoint = (closedLoop && currentPoint == controlPoints.Length - 1); + + p0 = controlPoints[currentPoint].position; + + if(closedLoopFinalPoint) + { + p1 = controlPoints[0].position; + } + else + { + p1 = controlPoints[currentPoint + 1].position; + } + + // m0 + if (currentPoint == 0) // Tangent M[k] = (P[k+1] - P[k-1]) / 2 + { + if(closedLoop) + { + m0 = p1 - controlPoints[controlPoints.Length - 1].position; + } + else + { + m0 = p1 - p0; + } + } + else + { + m0 = p1 - controlPoints[currentPoint - 1].position; + } + + // m1 + if (closedLoop) + { + if (currentPoint == controlPoints.Length - 1) //Last point case + { + m1 = controlPoints[(currentPoint + 2) % controlPoints.Length].position - p0; + } + else if (currentPoint == 0) //First point case + { + m1 = controlPoints[currentPoint + 2].position - p0; + } + else + { + m1 = controlPoints[(currentPoint + 2) % controlPoints.Length].position - p0; + } + } + else + { + if (currentPoint < controlPoints.Length - 2) + { + m1 = controlPoints[(currentPoint + 2) % controlPoints.Length].position - p0; + } + else + { + m1 = p1 - p0; + } + } + + m0 *= 0.5f; //Doing this here instead of in every single above statement + m1 *= 0.5f; + + float pointStep = 1.0f / resolution; + + if ((currentPoint == controlPoints.Length - 2 && !closedLoop) || closedLoopFinalPoint) //Final point + { + pointStep = 1.0f / (resolution - 1); // last point of last segment should reach p1 + } + + Vector3 pos1; + Vector3 pos0 = new Vector3(0, 0, 0); //simply initialization so the compiler doesnt complain. + float segmentDistance = 0; + + // Creates [resolution] points between this control point and the next + for (int tesselatedPoint = 1; tesselatedPoint < resolution; tesselatedPoint++) + { + if(tesselatedPoint == 1) + pos0 = CalculatePosition(p0, p1, m0, m1, 0); + + float t = tesselatedPoint * pointStep; + pos1 = CalculatePosition(p0, p1, m0, m1, t); + segmentDistance += Vector3.Distance(pos0, pos1); + + pos0 = pos1; + } + + splineSegmentDistances.Add(segmentDistance); + } + + return splineSegmentDistances.ToArray(); + } + + private void GenerateSplinePoints2() + { + InitializeProperties(); + + Vector3 p0, p1, p2, p3; //Previous position, Start position, end position, Next position + Vector4 r0, r1, r2, r3; //Previous rotation, Start rotation, end rotation, Next rotation + float fov0, fov1; + + //Fix rotation + for(int i = 0; i < controlPoints.Length; i++){ + if(i>0 && Dot(controlPoints[i - 1].rotation, controlPoints[i].rotation) < 0){ + Quaternion q = controlPoints[i].rotation; + controlPoints[i].rotation = new Quaternion(- q.x, - q.y, - q.z, - q.w); + ExplorerCore.Log($"cambio orientacion n° {i}"); + } + } + + // First for loop goes through each individual control point and connects it to the next, so 0-1, 1-2, 2-3 and so on + int closedAdjustment = closedLoop ? 0 : 1; + int totalframes = 0;//count the total number of frames so we can save the points in the right splinePoints position + + for (int currentPoint = 0; currentPoint < controlPoints.Length - closedAdjustment; currentPoint++) + { + bool closedLoopFinalPoint = (closedLoop && currentPoint == controlPoints.Length - 1); + + //Had tu add "controlPoints.Length" because C# mod doesnt work well with negative values + int previousPoint = closedLoop ? (currentPoint + controlPoints.Length - 1)%(controlPoints.Length) : System.Math.Max(currentPoint - 1, 0); + int endSegmentPoint = closedLoop ? (currentPoint + 1)%(controlPoints.Length) : System.Math.Min(currentPoint + 1, controlPoints.Length - 1); + int nextPoint = closedLoop ? (currentPoint + 2)%(controlPoints.Length) : System.Math.Min(currentPoint + 2, controlPoints.Length - 1); + + p0 = controlPoints[previousPoint].position; + r0 = QuaternionToVector4(controlPoints[previousPoint].rotation); + + p1 = controlPoints[currentPoint].position; + r1 = QuaternionToVector4(controlPoints[currentPoint].rotation); + + p2 = controlPoints[endSegmentPoint].position; + r2 = QuaternionToVector4(controlPoints[endSegmentPoint].rotation); + + p3 = controlPoints[nextPoint].position; + r3 = QuaternionToVector4(controlPoints[nextPoint].rotation); + + //Check if we are using the shortest path on the rotation. If not, change r1 to represent that shortest path. + if (Vector4.Dot(r0, r1) < 0) + r1 = - r1; + + if (Vector4.Dot(r1, r2) < 0) + r2 = - r2; + + if (Vector4.Dot(r2, r3) < 0) + r3 = - r3; + + fov0 = controlPoints[currentPoint].fov; + fov1 = controlPoints[endSegmentPoint].fov; + + int frames = controlPoints[currentPoint].frames; //resolution and time the node takes to get to the other node + float pointStep = 1.0f / frames; + + if ((currentPoint == controlPoints.Length - 2 && !closedLoop) || closedLoopFinalPoint) //Final point + { + pointStep = 1.0f / (frames - 1); // last point of last segment should reach p1 + } + + // Creates [frames] points between this control point and the next + for (int tesselatedPoint = 0; tesselatedPoint < frames; tesselatedPoint++) + { + float t = tesselatedPoint * pointStep; + + CatmullRomPoint point = Evaluate2(p0, p1, p2, p3, r0, r1, r2, r3, fov0, fov1, t); + + splinePoints[totalframes + tesselatedPoint] = point; + } + totalframes+=frames; + } + } + + static Vector3 CatmullRomInterpolate(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) + { + float t2 = t * t; + float t3 = t2 * t; + + Vector3 v0 = (p2 - p0) * 0.5f; + Vector3 v1 = (p3 - p1) * 0.5f; + + float h1 = 2 * t3 - 3 * t2 + 1; + float h2 = -2 * t3 + 3 * t2; + float h3 = t3 - 2 * t2 + t; + float h4 = t3 - t2; + + Vector3 interpolatedPoint = h1 * p1 + h2 * p2 + h3 * v0 + h4 * v1; + return interpolatedPoint; + } + + static Vector4 CatmullRomInterpolate(Vector4 p0, Vector4 p1, Vector4 p2, Vector4 p3, float t) + { + float t2 = t * t; + float t3 = t2 * t; + + Vector4 v0 = (p2 - p0) * 0.5f; + Vector4 v1 = (p3 - p1) * 0.5f; + + float h1 = 2 * t3 - 3 * t2 + 1; + float h2 = -2 * t3 + 3 * t2; + float h3 = t3 - 2 * t2 + t; + float h4 = t3 - t2; + + Vector4 interpolatedPoint = h1 * p1 + h2 * p2 + h3 * v0 + h4 * v1; + return interpolatedPoint; + } + + //Implementation from: https://qroph.github.io/2018/07/30/smooth-paths-using-catmull-rom-splines.html + static Vector3 CatmullRom2(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t){ + float tension = 0; + float alpha = 0.5f; + + float t01 = (float) System.Math.Pow((double) Vector3.Distance(p0, p1), (double) alpha); + float t12 = (float) System.Math.Pow((double) Vector3.Distance(p1, p2), (double) alpha); + float t23 = (float) System.Math.Pow((double) Vector3.Distance(p2, p3), (double) alpha); + + Vector3 v1 = t01 != 0 ? (p1 - p0) / t01 : new Vector3(0, 0, 0); + Vector3 v2 = t23 != 0 ? (p3 - p2) / t23 : new Vector3(0, 0, 0); + + Vector3 m1 = (1.0f - tension) * (p2 - p1 + t12 * (v1 - (p2 -p0) / (t01 + t12))); + Vector3 m2 = (1.0f - tension) * (p2 - p1 + t12 * (v2 - (p3 -p1) / (t12 + t23))); + + Vector3 a = 2.0f * (p1 - p2) + m1 + m2; + Vector3 b = -3.0f * (p1 - p2) - m1 - m1 - m2; + Vector3 c = m1; + Vector3 d = p1; + + return a * t * t * t + b * t * t + c * t + d; + } + + static Vector4 CatmullRom2(Vector4 r0, Vector4 r1, Vector4 r2, Vector4 r3, float t){ + float tension = 0; + float alpha = 0.5f; + + float t01 = (float) System.Math.Pow((double) Vector4.Distance(r0, r1), (double) alpha); + float t12 = (float) System.Math.Pow((double) Vector4.Distance(r1, r2), (double) alpha); + float t23 = (float) System.Math.Pow((double) Vector4.Distance(r2, r3), (double) alpha); + + Vector4 v1 = t01 != 0 ? (r1 - r0) / t01 : new Vector4(0, 0, 0, 0); + Vector4 v2 = t23 != 0 ? (r3 - r2) / t23 : new Vector4(0, 0, 0, 0); + + Vector4 m1 = (1.0f - tension) * (r2 - r1 + t12 * (v1 - (r2 -r0) / (t01 + t12))); + Vector4 m2 = (1.0f - tension) * (r2 - r1 + t12 * (v2 - (r3 -r1) / (t12 + t23))); + + Vector4 a = 2.0f * (r1 - r2) + m1 + m2; + Vector4 b = -3.0f * (r1 - r2) - m1 - m1 - m2; + Vector4 c = m1; + Vector4 d = r1; + + return a * t * t * t + b * t * t + c * t + d; + //return Vector4.Lerp(r1, r2, t); + } + + static CatmullRomPoint Evaluate2(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, Vector4 r0, Vector4 r1, Vector4 r2, Vector4 r3, float fovStart, float fovEnd, float t) + { + Vector3 position = CatmullRom2(p0, p1, p2, p3, t); + Vector4 v4rot = CatmullRom2(r0, r1, r2, r3, t); + Quaternion rotation = new Quaternion(v4rot.x, v4rot.y, v4rot.z, v4rot.w); + float fov = Mathf.SmoothStep(fovStart, fovEnd, t); + + return new CatmullRomPoint(position, rotation, new Vector3(0,0,0), new Vector3(0,0,0), fov); + } } } diff --git a/src/UI/Panels/CamPaths.cs b/src/UI/Panels/CamPaths.cs index a7430ec..6592f8f 100644 --- a/src/UI/Panels/CamPaths.cs +++ b/src/UI/Panels/CamPaths.cs @@ -23,7 +23,7 @@ namespace UnityExplorer.UI.Panels public override string Name => "CamPaths"; public override UIManager.Panels PanelType => UIManager.Panels.CamPaths; - public override int MinWidth => 700; + public override int MinWidth => 1000; public override int MinHeight => 600; public override Vector2 DefaultAnchorMin => new(0.4f, 0.4f); public override Vector2 DefaultAnchorMax => new(0.6f, 0.6f); @@ -33,6 +33,9 @@ namespace UnityExplorer.UI.Panels List UINodes = new List(); bool closedLoop; + bool constantSpeed; + int pathTotalFrames = 2000; + // ~~~~~~~~ UI construction / callbacks ~~~~~~~~ protected override void ConstructPanelContent() @@ -62,11 +65,22 @@ namespace UnityExplorer.UI.Panels DeletePath.OnClick += () => {controlPoints.Clear(); UpdateListNodes();}; Toggle closedLoopToggle = new Toggle(); - GameObject toggleObj = UIFactory.CreateToggle(horiGroup, "Close path in a loop", out closedLoopToggle, out Text toggleText); - UIFactory.SetLayoutElement(toggleObj, minHeight: 25, flexibleWidth: 9999); + GameObject toggleClosedLoopObj = UIFactory.CreateToggle(horiGroup, "Close path in a loop", out closedLoopToggle, out Text toggleClosedLoopText); + UIFactory.SetLayoutElement(toggleClosedLoopObj, minHeight: 25, flexibleWidth: 9999); closedLoopToggle.onValueChanged.AddListener((isClosedLoop) => {closedLoop = isClosedLoop;}); closedLoopToggle.isOn = false; - toggleText.text = "Close path in a loop"; + toggleClosedLoopText.text = "Close path in a loop"; + + Toggle constantspeedToggle = new Toggle(); + GameObject toggleConstantSpeedObj = UIFactory.CreateToggle(horiGroup, "Constant speed", out constantspeedToggle, out Text toggleConstantSpeedText); + UIFactory.SetLayoutElement(toggleConstantSpeedObj, minHeight: 25, flexibleWidth: 9999); + constantspeedToggle.onValueChanged.AddListener((isConstantSpeed) => {constantSpeed = isConstantSpeed; UpdateListNodes();}); + constantspeedToggle.isOn = false; + toggleConstantSpeedText.text = "Constant speed"; + + InputFieldRef CompletePathFramesInput = null; + AddInputField("Frames", "Complete path duration (in frames):", "Default: 2000", out CompletePathFramesInput, TotalFrames_OnEndEdit, false); + CompletePathFramesInput.Text = pathTotalFrames.ToString(); } private void UpdateListNodes(){ @@ -108,9 +122,11 @@ namespace UnityExplorer.UI.Panels destroyButton.OnClick += () => {controlPoints.Remove(point); UpdateListNodes();}; //Frames - InputFieldRef framesInput = null; - AddInputField("Frames", "Length (in frames):", "Default: 500", out framesInput, (frames) => { FramesInput_OnEndEdit(point, index, frames); }); - framesInput.Text = point.frames.ToString(); + if (!constantSpeed){ + InputFieldRef framesInput = null; + AddInputField("Frames", "Length (in frames):", $"Default: {point.frames}", out framesInput, (frames) => { FramesInput_OnEndEdit(point, index, frames); }); + framesInput.Text = point.frames.ToString(); + } //ShowPos and rot? } @@ -131,11 +147,12 @@ namespace UnityExplorer.UI.Panels FreeCamPanel.ourCamera.fieldOfView = point.fov; } - GameObject AddInputField(string name, string labelText, string placeHolder, out InputFieldRef inputField, Action onInputEndEdit) + GameObject AddInputField(string name, string labelText, string placeHolder, out InputFieldRef inputField, Action onInputEndEdit, bool shouldDeleteOnUpdate = true) { GameObject row = UIFactory.CreateHorizontalGroup(ContentRoot, "Editor Field", false, false, true, true, 5, default, new Color(1, 1, 1, 0)); - UINodes.Add(row); //To delete it when we update the node list + if(shouldDeleteOnUpdate) + UINodes.Add(row); //To delete it when we update the node list Text posLabel = UIFactory.CreateLabel(row, $"{name}_Label", labelText); UIFactory.SetLayoutElement(posLabel.gameObject, minWidth: 100, minHeight: 25); @@ -155,8 +172,12 @@ namespace UnityExplorer.UI.Panels if(ExplorerCore.CameraPathsManager == null) ExplorerCore.CameraPathsManager = new CatmullRom(controlPoints.ToArray(), closedLoop); else{ - ExplorerCore.CameraPathsManager.Update(controlPoints.ToArray()); ExplorerCore.CameraPathsManager.Update(closedLoop); + ExplorerCore.CameraPathsManager.Update(controlPoints.ToArray()); + } + + if(constantSpeed){ + UpdateNodeFramesConstantspeed(); } ExplorerCore.CameraPathsManager.StartPath(); @@ -189,5 +210,42 @@ namespace UnityExplorer.UI.Panels UpdateListNodes(); } + + void UpdateNodeFramesConstantspeed(){ + float[] d = ExplorerCore.CameraPathsManager.GenerateSplinePointsByRes(3000); + + ExplorerCore.Log($"d.Length: {d.Length}"); + + float dTotal = d.Sum(); + + ExplorerCore.Log($"dTotal: {dTotal}"); + + int closedAdjustment = closedLoop ? 0 : 1; + for(int i = 0; i < controlPoints.Count - closedAdjustment; i++) { + CatmullRom.PathControlPoint point = controlPoints[i]; + point.frames = (int)(pathTotalFrames *(d[i] / dTotal)); + controlPoints[i] = point; + + ExplorerCore.Log($"d[{i}]: {d[i]}"); + ExplorerCore.Log(controlPoints[i].frames); + } + + ExplorerCore.CameraPathsManager.Update(controlPoints.ToArray()); + } + + + void TotalFrames_OnEndEdit(string input) + { + EventSystemHelper.SetSelectedGameObject(null); + + if (!ParseUtility.TryParse(input, out int parsed, out Exception parseEx)) + { + ExplorerCore.LogWarning($"Could not parse value: {parseEx.ReflectionExToString()}"); + //pathTotalFrames.Text = pathTotalFrames.ToString(); + return; + } + + pathTotalFrames = parsed; + } } }