// A simplified version of TwoPointObjectTransformation. The following properties are true: // 1. The object-local-space direction between the left and right hands remains constant. // 2. The object-local-space position of LerpUnclamped(left, right, constraintPositionT) remains // constant. // 3. obj1 has the same scale as obj0. // 4. (Corollary of 1-3) The object-local-space positions of left and right remain constant, if // the distance between them does not change. public static TrTransform TwoPointObjectTransformationNoScale( TrTransform gripL0, TrTransform gripR0, TrTransform gripL1, TrTransform gripR1, TrTransform obj0, float constraintPositionT) { // Vectors from left-hand to right-hand Vector3 vLR0 = (gripR0.translation - gripL0.translation); Vector3 vLR1 = (gripR1.translation - gripL1.translation); Vector3 pivot0; TrTransform xfDelta; { pivot0 = Vector3.LerpUnclamped(gripL0.translation, gripR0.translation, constraintPositionT); var pivot1 = Vector3.LerpUnclamped(gripL1.translation, gripR1.translation, constraintPositionT); xfDelta.translation = pivot1 - pivot0; xfDelta.translation = Vector3.LerpUnclamped( gripL1.translation - gripL0.translation, gripR1.translation - gripR0.translation, constraintPositionT); // TODO: check edge cases: // - |vLR0| or |vLR1| == 0 (ie, from and/or to are undefined) // - vLR1 == vLR0 * -1 (ie, infinite number of axes of rotation) xfDelta.rotation = Quaternion.FromToRotation(vLR0, vLR1); xfDelta.scale = 1; } Quaternion deltaL = ConstrainRotationDelta(gripL0.rotation, gripL1.rotation, vLR0); Quaternion deltaR = ConstrainRotationDelta(gripR0.rotation, gripR1.rotation, vLR0); xfDelta = TrTransform.R(Quaternion.Slerp(deltaL, deltaR, 0.5f)) * xfDelta; // Set pivot point xfDelta = xfDelta.TransformBy(TrTransform.T(pivot0)); return(xfDelta * obj0); }
public void CreatePreviewModel() { // Is there a model, is it valid, and does it need a new preview object? if (m_Model == null || !m_Model.m_Valid || m_Model == m_ModelPreviewModel) { return; } // We know the model has changed, so destroy the preview. if (m_ModelPreview != null) { Destroy(m_ModelPreview.gameObject); } // Remember the model for which this preview was created. m_ModelPreviewModel = m_Model; // Build the actual preview. m_ModelPreview = Instantiate(m_Model.m_ModelParent); HierarchyUtils.RecursivelySetLayer(m_ModelPreview, LayerMask.NameToLayer("Panels")); m_ModelPreview.gameObject.SetActive(true); m_ModelPreview.parent = m_PreviewParent; float maxSide = Mathf.Max(m_Model.m_MeshBounds.size.x, Mathf.Max(m_Model.m_MeshBounds.size.y, m_Model.m_MeshBounds.size.z)); TrTransform xf = TrTransform.S(1 / maxSide) * TrTransform.T(-m_Model.m_MeshBounds.center); Coords.AsLocal[m_ModelPreview] = xf; HierarchyUtils.RecursivelyDisableShadows(m_ModelPreview); }
private static void SanityCheckVersusReplacementBrush(Stroke oldStroke) { BrushDescriptor desc = BrushCatalog.m_Instance.GetBrush(oldStroke.m_BrushGuid); BrushDescriptor replacementDesc = desc.m_Supersedes; if (replacementDesc == null) { return; } // Make a copy, since Begin/EndLineFromMemory mutate little bits of MemoryBrushStroke Stroke newStroke = new Stroke { m_BrushGuid = replacementDesc.m_Guid, m_IntendedCanvas = oldStroke.Canvas, m_ControlPoints = oldStroke.m_ControlPoints, m_BrushScale = oldStroke.m_BrushScale, m_BrushSize = oldStroke.m_BrushSize, m_Color = oldStroke.m_Color, m_Seed = oldStroke.m_Seed }; Array.Copy(oldStroke.m_ControlPointsToDrop, newStroke.m_ControlPointsToDrop, oldStroke.m_ControlPointsToDrop.Length); newStroke.Recreate(TrTransform.T(new Vector3(0.5f, 0, 0))); }
public void ExtendPath(Vector3 pos, CameraPathTool.ExtendPathType extendType) { Debug.Assert(extendType != CameraPathTool.ExtendPathType.None); int index = (extendType == CameraPathTool.ExtendPathType.ExtendAtHead) ? 0 : Path.NumPositionKnots; // If we're extending the path into a loop, ignore the passed position. if (extendType == CameraPathTool.ExtendPathType.Loop) { pos = Path.PositionKnots[0].transform.position; } SketchMemoryScript.m_Instance.PerformAndRecordCommand(new CreatePathKnotCommand( this, CameraPathKnot.Type.Position, new PathT(index), TrTransform.T(pos))); }
public void TestTwoPointObjectTransformationNoScale( float l0x, float l0y, float l0z, float r0x, float r0y, float r0z, float l1x, float l1y, float l1z, float r1x, float r1y, float r1z, float constraintPositionT, float tx, float ty, float tz, float angle, float ax, float ay, float az) { var obj1 = MathUtils.TwoPointObjectTransformationNoScale( TrTransform.T(new Vector3(l0x, l0y, l0z)), TrTransform.T(new Vector3(r0x, r0y, r0z)), TrTransform.T(new Vector3(l1x, l1y, l1z)), TrTransform.T(new Vector3(r1x, r1y, r1z)), TrTransform.identity, constraintPositionT); AssertAlmostEqual(obj1.translation, new Vector3(tx, ty, tz)); AssertAlmostEqual(obj1.rotation, angle, new Vector3(ax, ay, az)); AssertAlmostEqual(obj1.scale, 1); }
protected override TrTransform CalculateChildXf(List <PbKnot> parentKnots) { PbKnot lastKnot = parentKnots[parentKnots.Count - 1]; float distanceMeters = lastKnot.m_distance * App.UNITS_TO_METERS; float t = O.CyclesPerMeter * distanceMeters + (float)m_strand / O.NumStrands; // Our periodic function makes the plait look pretty square; maybe add // some rotation to break things up a bit? float rotations = (O.CyclesPerMeter * distanceMeters) * O.RotationsPerCycle; float amplitude = lastKnot.m_pressuredSize / 2; // /2 because size is diameter, not radius. TrTransform action = TrTransform.R(rotations * 360, Vector3.forward) * TrTransform.T(SomePeriodicFunction(t) * amplitude); TrTransform actionInCanvasSpace = action.TransformBy(lastKnot.GetFrame(AttachFrame.LineTangent)); return(actionInCanvasSpace * lastKnot.m_pointer); }
/// Given the position of a main pointer, find a corresponding symmetry position. /// Results are undefined unless you pass MainPointer or one of its /// dedicated symmetry pointers. public TrTransform GetSymmetryTransformFor(PointerScript pointer, TrTransform xfMain) { int child = pointer.ChildIndex; // "active pointers" is the number of pointers the symmetry widget is using, // including the main pointer. if (child == 0 || child >= m_NumActivePointers) { return(xfMain); } // This needs to be kept in sync with UpdateSymmetryPointerTransforms switch (m_CurrentSymmetryMode) { case SymmetryMode.SinglePlane: { return(m_SymmetryWidgetScript.ReflectionPlane.ReflectPoseKeepHandedness(xfMain)); } case SymmetryMode.FourAroundY: { // aboutY is an operator that rotates worldspace objects N degrees around the widget's Y TrTransform aboutY; { var xfWidget = TrTransform.FromTransform(m_SymmetryWidget); float angle = (360f * child) / m_NumActivePointers; aboutY = TrTransform.TR(Vector3.zero, Quaternion.AngleAxis(angle, Vector3.up)); // convert from widget-local coords to world coords aboutY = aboutY.TransformBy(xfWidget); } return(aboutY * xfMain); } case SymmetryMode.DebugMultiple: { var xfLift = TrTransform.T(m_SymmetryDebugMultipleOffset * child); return(xfLift * xfMain); } default: return(xfMain); } }
public void TestTwoPointTransformationAxisResize_Simple() { // TODO: test the bounds clamping too // Just a simple sanity check var xfL0 = TrTransform.T(new Vector3(3, 4, 1)); var xfR0 = TrTransform.T(new Vector3(4, 4, 1)); var xfL1 = TrTransform.T(new Vector3(3, 2, 2)); var xfR1 = TrTransform.T(new Vector3(3, 4, 2)); var axis = new Vector3(1, 0, 0); var size = 8; var obj0 = TrTransform.T(new Vector3(2, 3, 0)); float deltaScale; var obj1 = MathUtils.TwoPointObjectTransformationAxisResize( axis, size, xfL0, xfR0, xfL1, xfR1, obj0, out deltaScale); AssertAlmostEqual(deltaScale, 9 / 8f); AssertAlmostEqual(obj1.rotation, 90, new Vector3(0, 0, 1)); AssertAlmostEqual(obj1.translation, new Vector3(4, 1.5f, 1)); }
protected override void MaybeCreateChildrenImpl() { // Only create children once if (m_children.Count > 1) { return; } if (m_recursionLevel == 0) { float radiusMeters = m_caneGrossRadiusPct * BaseSize_LS * App.UNITS_TO_METERS; Vector3 offsetT = m_caneGrossRadiusPct * Vector3.right; for (int i = 0; i < m_numGrossParents; ++i) { Quaternion rotation = Quaternion.AngleAxis(i * 360.0f / m_numGrossParents, Vector3.forward); TrTransform offset = TrTransform.T(rotation * offsetT); float degreesPerMeter = m_grossRotationsPerRadian / radiusMeters * 360; InitializeAndAddChild( new PbChildWithOffset(-1, AttachFrame.LineTangent, offset, degreesPerMeter), m_Desc, // Recurse with same brush Color.white); } } else { float radiusMeters = m_caneFineRadiusPct * BaseSize_LS * App.UNITS_TO_METERS; Vector3 offsetT = m_caneFineRadiusPct * Vector3.right; for (int i = 0; i < kCaneColors.Length; ++i) { Quaternion rotation = Quaternion.AngleAxis(i * 360.0f / kCaneColors.Length, Vector3.forward); TrTransform offset = TrTransform.T(rotation * offsetT); float degreesPerMeter = m_fineRotationsPerRadian / radiusMeters * 360; InitializeAndAddChild( new PbChildWithOffset(-1, AttachFrame.LineTangent, offset, degreesPerMeter), m_caneBrushes[i % m_caneBrushes.Length], kCaneColors[i], m_strandSizePercent); } } }
public void TestTwoPointNonUniformScale( float l0x, float l0y, float l0z, float r0x, float r0y, float r0z, float l1x, float l1y, float l1z, float r1x, float r1y, float r1z, float deltaScaleMin, float deltaScaleMax, float desiredDeltaScale, float tx, float ty, float tz, float angle, float ax, float ay, float az) { float deltaScale; var obj1 = MathUtils.TwoPointObjectTransformationNonUniformScale( new Vector3(1, 0, 0), // axis TrTransform.T(new Vector3(l0x, l0y, l0z)), TrTransform.T(new Vector3(r0x, r0y, r0z)), TrTransform.T(new Vector3(l1x, l1y, l1z)), TrTransform.T(new Vector3(r1x, r1y, r1z)), TrTransform.identity, out deltaScale, deltaScaleMin: deltaScaleMin, deltaScaleMax: deltaScaleMax); AssertAlmostEqual(deltaScale, desiredDeltaScale); AssertAlmostEqual(obj1.translation, new Vector3(tx, ty, tz)); AssertAlmostEqual(obj1.rotation, angle, new Vector3(ax, ay, az)); }
// Constructs an updated transform obj1 such that the object-space // positions of the left and right grip are invariant. More generally, // all points on the line L->R are invariant. Precisely: // // - inv(obj0) * L0.pos == inv(obj1) * L1.pos // - inv(obj0) * R0.pos == inv(obj1) * L1.pos // // If abs(R0-L0) != abs(R1-L1), then a scaling is necessary. // This method chooses to apply a uniform scale. // // If scale bounds kick in, only a single point on the L->R line can // be made invariant; see bUseLeftAsPivot for how this is chosen. // // Given 2 position deltas, there are 6 DOFs available: // = 3DOF (movement of average position) // + 2DOF (change of direction of vector between L and R) // + 1DOF (scaling, change of length of vector between L and R) // // One more DOF is needed to fully specify the rotation. This method // chooses to get it from L and R's rotations about the L-R vector. // // Pass: // gripL0, L1, R0, R1 - // The left and right grip points, in their old (0) and new (1) positions. // The scale portion of the TrTransform is ignored. // obj0 - // Transform of the object being manipulated. // deltaScale{Min,Max} - // Constrains the range of result.scale. // Negative means do not constrain that endpoint. // rotationAxisConstraint - // Constrains the value of result.rotation. If passed, the delta rotation // from obj0 -> result will only be about this axis. // bUseLeftAsPivot - // Controls the invariant point if scale constraints are applied. // If false, uses the midpoint between L and R. // If true, uses the point L. // // Returns: // New position, rotation, and scale static public TrTransform TwoPointObjectTransformation( TrTransform gripL0, TrTransform gripR0, // prev TrTransform gripL1, TrTransform gripR1, // next TrTransform obj0, float deltaScaleMin = -1.0f, float deltaScaleMax = -1.0f, Vector3 rotationAxisConstraint = default(Vector3), bool bUseLeftAsPivot = false) { // Vectors from left-hand to right-hand Vector3 vLR0 = (gripR0.translation - gripL0.translation); Vector3 vLR1 = (gripR1.translation - gripL1.translation); // World-space position whose object-space position is used to constrain obj1 // inv(obj0) * vInvariant0 = inv(obj1) * vInvariant1 Vector3 vInvariant0, vInvariant1; { // Use left grip or average of grips as pivot point. Maybe switch the // bool to be a parametric t instead, so if caller wants to use right // grip as pivot they don't need to swap arguments? float t = bUseLeftAsPivot ? 0f : 0.5f; vInvariant0 = Vector3.Lerp(gripL0.translation, gripR0.translation, t); vInvariant1 = Vector3.Lerp(gripL1.translation, gripR1.translation, t); } // Strategy: // 1. Move invariant point to the correct spot, with a translation. // 2. Rotate about that point. // 3. Uniform scale about that point. // Items 2 and 3 can happen in the same TrTransform, since rotation // and uniform scale commute as long as they use the same pivot. TrTransform xfDelta1 = TrTransform.T(vInvariant1 - vInvariant0); TrTransform xfDelta23; { // calculate worldspace scale; will adjust center-of-scale later float dist0 = vLR0.magnitude; float dist1 = vLR1.magnitude; float deltaScale = (dist0 == 0) ? 1 : dist1 / dist0; // Clamp scale if requested. if (deltaScaleMin >= 0) { deltaScale = Mathf.Max(deltaScale, deltaScaleMin); } if (deltaScaleMax >= 0) { deltaScale = Mathf.Min(deltaScale, deltaScaleMax); } // This gets the left-right axis pointing in the correct direction Quaternion qSwing0To1 = Quaternion.FromToRotation(vLR0, vLR1); // This applies some twist about that left-right axis. The choice of constraint axis // (vLR0 vs vLR1) depends on whether qTwist is right- or left-multiplied vs qReach. Quaternion qTwistAbout0 = Quaternion.Slerp( ConstrainRotationDelta(gripL0.rotation, gripL1.rotation, vLR0), ConstrainRotationDelta(gripR0.rotation, gripR1.rotation, vLR0), 0.5f); Quaternion qDelta = qSwing0To1 * qTwistAbout0; // Constrain the rotation if requested. if (rotationAxisConstraint != default(Vector3)) { qDelta = ConstrainRotationDelta(Quaternion.identity, qDelta, rotationAxisConstraint); } xfDelta23 = TrTransform .TRS(Vector3.zero, qDelta, deltaScale) .TransformBy(TrTransform.T(vInvariant1)); } return(xfDelta23 * xfDelta1 * obj0); }
protected override void MaybeCreateChildrenImpl() { // TODO: when too many children, starve an old one in order to create another. int kBranchCount = 12; int kFrondCount = 16; if (m_recursionLevel == 0) { // Trunk if (m_children.Count == 0) { InitializeAndAddChild(new PbChildIdentityXf(), m_trunkBrush, m_trunkColor); } if (DistanceSinceLastKnotBasedChild() > m_branchFrequency && m_children.Count < kBranchCount + 1) { int salt = m_children.Count; // Children 1 - 13 are the branches; early ones grow faster. float growthPercent = (kBranchCount + 1 - (float)m_children.Count) / kBranchCount; TrTransform offset = // Branches don't extend as quickly as the trunk TrTransform.S(m_branchScale * growthPercent) * // Randomly place around the tree TrTransform.R(m_rng.InRange(salt, 0f, 360f), Vector3.forward) * // Angle the branches backwards (away from the stroke tip) TrTransform.R(120, Vector3.right); InitializeAndAddChild( new PbChildWithOffset(m_knots.Count - 1, AttachFrame.LineTangent, offset, 0), m_Desc, // Recurse with same brush Color.white, m_branchRelativeSize); } } else if (m_recursionLevel == 1) { // Branch if (m_children.Count == 0) { InitializeAndAddChild(new PbChildIdentityXf(), m_branchBrush, m_branchColor); } // TODO: would like this frequency to be higher for tinier branches if (DistanceSinceLastKnotBasedChild() > m_frondFrequency && m_children.Count < kFrondCount + 1) { float growthPercent = 1; // (kFrondCount + 1 - (float)m_children.Count) / kFrondCount; for (int deg = -30; deg <= 30; deg += 60) { TrTransform offset = // Fronds don't grow as quickly as the branch TrTransform.S(m_frondScale * growthPercent) * TrTransform.R(deg, Vector3.up); InitializeAndAddChild( new PbChildWithOffset(m_knots.Count - 1, AttachFrame.LineTangent, offset, 0), m_frondBrush, m_frondColor, m_frondRelativeSize); TrTransform decoOffset = TrTransform.T(m_decoOffset); InitializeAndAddChild( new PbChildWithOffset(-1, AttachFrame.LineTangent, decoOffset, m_decoTwist), m_decoBrush, Color.white, m_frondRelativeSize); } } } }
public void TestTwoPointNonUniform_Random() { // Test doesn't try very hard to avoid unstable regions int ALLOWED_FAILURES = 2; int TRIES = 5000; int seed = (int)((EditorApplication.timeSinceStartup % 1) * 100000); Random.InitState(seed); int failures = 0; for (int i = 0; i < TRIES; ++i) { try { var xfL0 = TrTransform.T(10 * Random.onUnitSphere); var xfR0 = TrTransform.T(xfL0.translation + 30 * Random.onUnitSphere); var xfL1 = TrTransform.T(xfL0.translation + 5 * Random.onUnitSphere); var xfR1 = TrTransform.T(xfR0.translation + 5 * Random.onUnitSphere); Vector3 axis = new Vector3(1, 0, 0); var obj0 = TrTransform.identity; float deltaScale; var obj1 = MathUtils.TwoPointObjectTransformationNonUniformScale( axis, xfL0, xfR0, xfL1, xfR1, obj0, out deltaScale); Assert.GreaterOrEqual(deltaScale, 0); // This unit test is too vulnerable to instability; detect unstable // regions in lieu of writing a better test if (deltaScale < .01f || deltaScale > 100f) { continue; } // Invariant: local-space grip positions are the same, before and after. // However, the invariant is broken when there is no solution (ie, when // deltaScale = 0) if (deltaScale > 1e-4f) { // inverse of identity is identity Matrix4x4 mInvObj0 = Matrix4x4.identity; Matrix4x4 mInvObj1 = Matrix4x4_InvTRS( obj1.translation, obj1.rotation, new Vector3(deltaScale, 1, 1)); // Accuracy is quite poor :-/ float ABSEPS = 2e-3f; float RELEPS = 5e-3f; try { var vL0 = mInvObj0.MultiplyPoint(xfL0.translation); var vL1 = mInvObj1.MultiplyPoint(xfL1.translation); CheckAlmostEqual(vL1, vL0, ABSEPS, RELEPS, "left"); var vR0 = mInvObj0.MultiplyPoint(xfR0.translation); var vR1 = mInvObj1.MultiplyPoint(xfR1.translation); CheckAlmostEqual(vR1, vR0, ABSEPS, RELEPS, "right"); } catch (NotAlmostEqual e) { Assert.Fail(e.Message); } } } catch (System.Exception) { if (++failures > ALLOWED_FAILURES) { Debug.LogFormat("Failed on seed {0} iteration {1}", seed, i); throw; } } } }
public void SetFixedPosition(Vector3 vPos_SS) { m_IsFixedPosition = true; m_Transform_SS = TrTransform.T(vPos_SS); }
/// Returns true on success /// Also updates m_TeleportForceBoundsPosition, m_LastParabolaVelocity, /// m_HideValidTeleportParabola, m_TeleportPlaceIcon, m_TeleportBoundsDesired /// Calls SetTeleportParabola() /// Calls UpdateIconScale() bool UpdateTool_PlaceParabola() { // Given pointing Y and our scalar, determine where our parabola intersects with y == 0. Vector3 vel = m_LastParabolaRay.direction * m_TeleportParabolaSpeed; float yPos = m_LastParabolaRay.origin.y; float fRadicand = vel.y * vel.y - 2.0f * m_TeleportParabolaGravity * yPos; if (fRadicand < 0f) { return(false); } Vector3 vFeet = ViewpointScript.Head.position; vFeet.y = 0.0f; Vector3 vNewFeet = m_LastParabolaRay.origin; vNewFeet.y = m_TeleportBounds.position.y; m_LastParabolaTime = (vel.y + Mathf.Sqrt(fRadicand)) / -m_TeleportParabolaGravity; m_TeleportTargetVector = new Vector3(vel.x * m_LastParabolaTime, 0, vel.z * m_LastParabolaTime); vNewFeet += m_TeleportTargetVector; // Ensure vNewFeet remains valid { // We don't have any functions for validating foot poses. In fact, the // room doesn't "move" since it's the root of our hierarchy. So: // 1. Turn foot move into room move TrTransform xfRoomMove = TrTransform.T(vNewFeet - vFeet); // 2. Turn room move into new scene pose and validate // Note: assumes old room transform is identity (which it is, because // the room is the root of our transform hierarchy). TrTransform newScene = xfRoomMove.inverse * App.Scene.Pose; newScene = SketchControlsScript.MakeValidSceneMove(App.Scene.Pose, newScene, BoundsRadius); // 3. Reverse of #2 xfRoomMove = App.Scene.Pose * newScene.inverse; // 4. Reverse of #1 vNewFeet = vFeet + xfRoomMove.translation; } // Dampen motion of vNewFeet // Invariant: new room center == (vNewFeet - vFeet) if (!m_TeleportForceBoundsPosition) { vNewFeet = Vector3.Lerp( m_TeleportBoundsDesired.transform.position + vFeet, vNewFeet, m_TeleportParabolaDampen); } else { m_TeleportForceBoundsPosition = false; } // Curve parabola to hit final position. m_LastParabolaVelocity = (vNewFeet - m_LastParabolaRay.origin) / m_LastParabolaTime; m_LastParabolaVelocity.y = ( (vNewFeet.y - m_LastParabolaRay.origin.y) - (m_TeleportParabolaGravity * m_LastParabolaTime * m_LastParabolaTime * 0.5f)) / m_LastParabolaTime; Vector3 vVelNoY = vel; vVelNoY.y = 0.0f; Vector3 vLastVelNoY = m_LastParabolaVelocity; vLastVelNoY.y = 0.0f; bool bReasonableAngle = Vector3.Angle(vVelNoY, vLastVelNoY) < 60.0f; m_HideValidTeleportParabola = !bReasonableAngle || (m_LastParabolaVelocity.normalized.y > m_TeleportParabolaMaxY); SetTeleportParabola(); // Place icon at the user's head position inside the new bounds, facing the user. m_TeleportPlaceIcon.position = vNewFeet; Vector3 vGazeDirNoY = ViewpointScript.Head.forward; vGazeDirNoY.y = 0.0f; m_TeleportPlaceIcon.forward = vGazeDirNoY.normalized; // Shrink the icon if it's too close to us. UpdateIconScale(); // Finally, set the desired bounds. m_TeleportBoundsDesired.transform.position = vNewFeet - vFeet; return(true); }
protected override IEnumerable <ControlPoint> DoGetPoints(TrTransform finalTransform) { double t0 = m_initialTime; double t1 = App.Instance.CurrentSketchTime; Vector3 center = m_initialTransform.translation; Vector3 nRadius = finalTransform.translation - center; float fRadius = nRadius.magnitude; nRadius /= fRadius; // Degenerate circle -- turn it into a line if (fRadius < 1e-5f) { yield return(new ControlPoint { m_Pos = m_initialTransform.translation, m_Orient = m_initialTransform.rotation, m_Pressure = 1f, m_TimestampMs = (uint)(t0 * kSecondsToMs) }); yield return(new ControlPoint { m_Pos = finalTransform.translation, m_Orient = finalTransform.rotation, m_Pressure = 1f, m_TimestampMs = (uint)(t1 * kSecondsToMs) }); yield break; } // Tangent must be perpendicular to nRadius var thisState = new ComputeTangentState { nRadius = nRadius, rotation = finalTransform.rotation, preferred = m_vPreferredTangent }; Vector3 nTangent = ComputeTangent(thisState); m_vPreferredTangent = nTangent; #if DEBUG_TANGENT if (m_oldState != null) { Vector3 nOldTangent = ComputeTangent(m_oldState.Value); if (fRadius > .5f && Vector3.Dot(nOldTangent, nTangent) < .966f) { int nn = 20; for (int i = 0; i < nn; ++i) { Vector3 v0 = ComputeTangent(thisState); Vector3 v1 = ComputeTangent(m_oldState.Value); } } } m_oldState = thisState; #endif // Axis is perpendicular to tangent and radius // TODO: experiment with removing this restriction? Vector3 nAxis = Vector3.Cross(nTangent, nRadius).normalized; TrTransform xf0 = finalTransform; // TODO: adjust control point density int n = 30; // number of points; must be >= 2 for (int i = 0; i <= n; ++i) { float t = (float)i / n; Quaternion rot = Quaternion.AngleAxis(360 * t, nAxis); TrTransform delta = TrTransform.R(rot).TransformBy(TrTransform.T(center)); TrTransform xf = delta * xf0; yield return(new ControlPoint { m_Pos = xf.translation, m_Orient = xf.rotation, m_Pressure = 1f, m_TimestampMs = (uint)(Mathf.Lerp((float)t0, (float)t1, t) * 1000) }); } #if DEBUG_TANGENT var start = finalTransform.translation; foreach (var val in DrawLine(start, nTangent, 5)) { yield return(val); } foreach (var val in DrawLine(start, nAxis, 7)) { yield return(val); } #endif }