aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorRobert Adams2012-07-11 16:07:14 -0700
committerRobert Adams2012-07-11 16:12:38 -0700
commit743437262ed645204d9040e1705a41902860a648 (patch)
tree9e7f572b4460242963a4e313d82ba122ad1d1ae7
parentWhere possible, use the system Encoding.ASCII and Encoding.UTF8 rather than c... (diff)
downloadopensim-SC-743437262ed645204d9040e1705a41902860a648.zip
opensim-SC-743437262ed645204d9040e1705a41902860a648.tar.gz
opensim-SC-743437262ed645204d9040e1705a41902860a648.tar.bz2
opensim-SC-743437262ed645204d9040e1705a41902860a648.tar.xz
Many explanitory comments added to the link and delink code in
SOG and SOP. Should have no functionality changes.
-rw-r--r--OpenSim/Region/Framework/Scenes/SceneObjectGroup.cs88
-rw-r--r--OpenSim/Region/Framework/Scenes/SceneObjectPart.cs28
2 files changed, 93 insertions, 23 deletions
diff --git a/OpenSim/Region/Framework/Scenes/SceneObjectGroup.cs b/OpenSim/Region/Framework/Scenes/SceneObjectGroup.cs
index fc04761..52469a2 100644
--- a/OpenSim/Region/Framework/Scenes/SceneObjectGroup.cs
+++ b/OpenSim/Region/Framework/Scenes/SceneObjectGroup.cs
@@ -451,6 +451,8 @@ namespace OpenSim.Region.Framework.Scenes
451 } 451 }
452 } 452 }
453 453
454 // Restuff the new GroupPosition into each SOP of the linkset.
455 // This has the affect of resetting and tainting the physics actors.
454 SceneObjectPart[] parts = m_parts.GetArray(); 456 SceneObjectPart[] parts = m_parts.GetArray();
455 for (int i = 0; i < parts.Length; i++) 457 for (int i = 0; i < parts.Length; i++)
456 parts[i].GroupPosition = val; 458 parts[i].GroupPosition = val;
@@ -1133,6 +1135,9 @@ namespace OpenSim.Region.Framework.Scenes
1133 1135
1134 public void ResetChildPrimPhysicsPositions() 1136 public void ResetChildPrimPhysicsPositions()
1135 { 1137 {
1138 // Setting this SOG's absolute position also loops through and sets the positions
1139 // of the SOP's in this SOG's linkset. This has the side affect of making sure
1140 // the physics world matches the simulated world.
1136 AbsolutePosition = AbsolutePosition; // could someone in the know please explain how this works? 1141 AbsolutePosition = AbsolutePosition; // could someone in the know please explain how this works?
1137 1142
1138 // teravus: AbsolutePosition is NOT a normal property! 1143 // teravus: AbsolutePosition is NOT a normal property!
@@ -1987,6 +1992,8 @@ namespace OpenSim.Region.Framework.Scenes
1987 LinkToGroup(objectGroup, false); 1992 LinkToGroup(objectGroup, false);
1988 } 1993 }
1989 1994
1995 // Link an existing group to this group.
1996 // The group being linked need not be a linkset -- it can have just one prim.
1990 public void LinkToGroup(SceneObjectGroup objectGroup, bool insert) 1997 public void LinkToGroup(SceneObjectGroup objectGroup, bool insert)
1991 { 1998 {
1992// m_log.DebugFormat( 1999// m_log.DebugFormat(
@@ -1997,35 +2004,51 @@ namespace OpenSim.Region.Framework.Scenes
1997 if (objectGroup == this) 2004 if (objectGroup == this)
1998 return; 2005 return;
1999 2006
2007 // 'linkPart' == the root of the group being linked into this group
2000 SceneObjectPart linkPart = objectGroup.m_rootPart; 2008 SceneObjectPart linkPart = objectGroup.m_rootPart;
2001 2009
2002 // physics flags from group to be applied to linked parts 2010 // physics flags from group to be applied to linked parts
2003 bool grpusephys = UsesPhysics; 2011 bool grpusephys = UsesPhysics;
2004 bool grptemporary = IsTemporary; 2012 bool grptemporary = IsTemporary;
2005 2013
2014 // Remember where the group being linked thought it was
2006 Vector3 oldGroupPosition = linkPart.GroupPosition; 2015 Vector3 oldGroupPosition = linkPart.GroupPosition;
2007 Quaternion oldRootRotation = linkPart.RotationOffset; 2016 Quaternion oldRootRotation = linkPart.RotationOffset;
2008 2017
2018 // A linked SOP remembers its location and rotation relative to the root of a group.
2019 // Convert the root of the group being linked to be relative to the
2020 // root of the group being linked to.
2021 // Note: Some of the assignments have complex side effects.
2022
2023 // First move the new group's root SOP's position to be relative to ours
2024 // (radams1: Not sure if the multiple setting of OffsetPosition is required. If not,
2025 // this code can be reordered to have a more logical flow.)
2009 linkPart.OffsetPosition = linkPart.GroupPosition - AbsolutePosition; 2026 linkPart.OffsetPosition = linkPart.GroupPosition - AbsolutePosition;
2027 // Assign the new parent to the root of the old group
2010 linkPart.ParentID = m_rootPart.LocalId; 2028 linkPart.ParentID = m_rootPart.LocalId;
2029 // Now that it's a child, it's group position is our root position
2011 linkPart.GroupPosition = AbsolutePosition; 2030 linkPart.GroupPosition = AbsolutePosition;
2012 Vector3 axPos = linkPart.OffsetPosition;
2013 2031
2032 Vector3 axPos = linkPart.OffsetPosition;
2033 // Rotate the linking root SOP's position to be relative to the new root prim
2014 Quaternion parentRot = m_rootPart.RotationOffset; 2034 Quaternion parentRot = m_rootPart.RotationOffset;
2015 axPos *= Quaternion.Inverse(parentRot); 2035 axPos *= Quaternion.Inverse(parentRot);
2016
2017 linkPart.OffsetPosition = axPos; 2036 linkPart.OffsetPosition = axPos;
2037
2038 // Make the linking root SOP's rotation relative to the new root prim
2018 Quaternion oldRot = linkPart.RotationOffset; 2039 Quaternion oldRot = linkPart.RotationOffset;
2019 Quaternion newRot = Quaternion.Inverse(parentRot) * oldRot; 2040 Quaternion newRot = Quaternion.Inverse(parentRot) * oldRot;
2020 linkPart.RotationOffset = newRot; 2041 linkPart.RotationOffset = newRot;
2021 2042
2022 linkPart.ParentID = m_rootPart.LocalId; 2043 // If there is only one SOP in a SOG, the LinkNum is zero. I.e., not a linkset.
2023 2044 // Now that we know this SOG has at least two SOPs in it, the new root
2045 // SOP becomes the first in the linkset.
2024 if (m_rootPart.LinkNum == 0) 2046 if (m_rootPart.LinkNum == 0)
2025 m_rootPart.LinkNum = 1; 2047 m_rootPart.LinkNum = 1;
2026 2048
2027 lock (m_parts.SyncRoot) 2049 lock (m_parts.SyncRoot)
2028 { 2050 {
2051 // Calculate the new link number for the old root SOP
2029 int linkNum; 2052 int linkNum;
2030 if (insert) 2053 if (insert)
2031 { 2054 {
@@ -2041,6 +2064,7 @@ namespace OpenSim.Region.Framework.Scenes
2041 linkNum = PrimCount + 1; 2064 linkNum = PrimCount + 1;
2042 } 2065 }
2043 2066
2067 // Add the old root SOP as a part in our group's list
2044 m_parts.Add(linkPart.UUID, linkPart); 2068 m_parts.Add(linkPart.UUID, linkPart);
2045 2069
2046 linkPart.SetParent(this); 2070 linkPart.SetParent(this);
@@ -2048,6 +2072,8 @@ namespace OpenSim.Region.Framework.Scenes
2048 2072
2049 // let physics know preserve part volume dtc messy since UpdatePrimFlags doesn't look to parent changes for now 2073 // let physics know preserve part volume dtc messy since UpdatePrimFlags doesn't look to parent changes for now
2050 linkPart.UpdatePrimFlags(grpusephys, grptemporary, (IsPhantom || (linkPart.Flags & PrimFlags.Phantom) != 0), linkPart.VolumeDetectActive); 2074 linkPart.UpdatePrimFlags(grpusephys, grptemporary, (IsPhantom || (linkPart.Flags & PrimFlags.Phantom) != 0), linkPart.VolumeDetectActive);
2075
2076 // If the added SOP is physical, also tell the physics engine about the link relationship.
2051 if (linkPart.PhysActor != null && m_rootPart.PhysActor != null && m_rootPart.PhysActor.IsPhysical) 2077 if (linkPart.PhysActor != null && m_rootPart.PhysActor != null && m_rootPart.PhysActor.IsPhysical)
2052 { 2078 {
2053 linkPart.PhysActor.link(m_rootPart.PhysActor); 2079 linkPart.PhysActor.link(m_rootPart.PhysActor);
@@ -2056,20 +2082,26 @@ namespace OpenSim.Region.Framework.Scenes
2056 2082
2057 linkPart.LinkNum = linkNum++; 2083 linkPart.LinkNum = linkNum++;
2058 2084
2085 // Get a list of the SOP's in the old group in order of their linknum's.
2059 SceneObjectPart[] ogParts = objectGroup.Parts; 2086 SceneObjectPart[] ogParts = objectGroup.Parts;
2060 Array.Sort(ogParts, delegate(SceneObjectPart a, SceneObjectPart b) 2087 Array.Sort(ogParts, delegate(SceneObjectPart a, SceneObjectPart b)
2061 { 2088 {
2062 return a.LinkNum - b.LinkNum; 2089 return a.LinkNum - b.LinkNum;
2063 }); 2090 });
2064 2091
2092 // Add each of the SOP's from the old linkset to our linkset
2065 for (int i = 0; i < ogParts.Length; i++) 2093 for (int i = 0; i < ogParts.Length; i++)
2066 { 2094 {
2067 SceneObjectPart part = ogParts[i]; 2095 SceneObjectPart part = ogParts[i];
2068 if (part.UUID != objectGroup.m_rootPart.UUID) 2096 if (part.UUID != objectGroup.m_rootPart.UUID)
2069 { 2097 {
2070 LinkNonRootPart(part, oldGroupPosition, oldRootRotation, linkNum++); 2098 LinkNonRootPart(part, oldGroupPosition, oldRootRotation, linkNum++);
2071 // let physics know 2099
2100 // Update the physics flags for the newly added SOP
2101 // (Is this necessary? LinkNonRootPart() has already called UpdatePrimFlags but with different flags!??)
2072 part.UpdatePrimFlags(grpusephys, grptemporary, (IsPhantom || (part.Flags & PrimFlags.Phantom) != 0), part.VolumeDetectActive); 2102 part.UpdatePrimFlags(grpusephys, grptemporary, (IsPhantom || (part.Flags & PrimFlags.Phantom) != 0), part.VolumeDetectActive);
2103
2104 // If the added SOP is physical, also tell the physics engine about the link relationship.
2073 if (part.PhysActor != null && m_rootPart.PhysActor != null && m_rootPart.PhysActor.IsPhysical) 2105 if (part.PhysActor != null && m_rootPart.PhysActor != null && m_rootPart.PhysActor.IsPhysical)
2074 { 2106 {
2075 part.PhysActor.link(m_rootPart.PhysActor); 2107 part.PhysActor.link(m_rootPart.PhysActor);
@@ -2080,6 +2112,7 @@ namespace OpenSim.Region.Framework.Scenes
2080 } 2112 }
2081 } 2113 }
2082 2114
2115 // Now that we've aquired all of the old SOG's parts, remove the old SOG from the scene.
2083 m_scene.UnlinkSceneObject(objectGroup, true); 2116 m_scene.UnlinkSceneObject(objectGroup, true);
2084 objectGroup.IsDeleted = true; 2117 objectGroup.IsDeleted = true;
2085 2118
@@ -2152,7 +2185,7 @@ namespace OpenSim.Region.Framework.Scenes
2152 /// <remarks> 2185 /// <remarks>
2153 /// FIXME: This method should not be called directly since it bypasses update locking, allowing a potential race 2186 /// FIXME: This method should not be called directly since it bypasses update locking, allowing a potential race
2154 /// condition. But currently there is no 2187 /// condition. But currently there is no
2155 /// alternative method that does take a lonk to delink a single prim. 2188 /// alternative method that does take a lock to delink a single prim.
2156 /// </remarks> 2189 /// </remarks>
2157 /// <param name="partID"></param> 2190 /// <param name="partID"></param>
2158 /// <param name="sendEvents"></param> 2191 /// <param name="sendEvents"></param>
@@ -2165,6 +2198,7 @@ namespace OpenSim.Region.Framework.Scenes
2165 2198
2166 linkPart.ClearUndoState(); 2199 linkPart.ClearUndoState();
2167 2200
2201 Vector3 worldPos = linkPart.GetWorldPosition();
2168 Quaternion worldRot = linkPart.GetWorldRotation(); 2202 Quaternion worldRot = linkPart.GetWorldRotation();
2169 2203
2170 // Remove the part from this object 2204 // Remove the part from this object
@@ -2174,6 +2208,7 @@ namespace OpenSim.Region.Framework.Scenes
2174 2208
2175 SceneObjectPart[] parts = m_parts.GetArray(); 2209 SceneObjectPart[] parts = m_parts.GetArray();
2176 2210
2211 // Rejigger the linknum's of the remaining SOP's to fill any gap
2177 if (parts.Length == 1 && RootPart != null) 2212 if (parts.Length == 1 && RootPart != null)
2178 { 2213 {
2179 // Single prim left 2214 // Single prim left
@@ -2195,22 +2230,31 @@ namespace OpenSim.Region.Framework.Scenes
2195 2230
2196 PhysicsActor linkPartPa = linkPart.PhysActor; 2231 PhysicsActor linkPartPa = linkPart.PhysActor;
2197 2232
2233 // Remove the SOP from the physical scene.
2234 // If the new SOG is physical, it is re-created later.
2235 // (There is a problem here in that we have not yet told the physics
2236 // engine about the delink. Someday, linksets should be made first
2237 // class objects in the physics engine interface).
2198 if (linkPartPa != null) 2238 if (linkPartPa != null)
2199 m_scene.PhysicsScene.RemovePrim(linkPartPa); 2239 m_scene.PhysicsScene.RemovePrim(linkPartPa);
2200 2240
2201 // We need to reset the child part's position 2241 // We need to reset the child part's position
2202 // ready for life as a separate object after being a part of another object 2242 // ready for life as a separate object after being a part of another object
2203 Quaternion parentRot = m_rootPart.RotationOffset;
2204 2243
2244 /* This commented out code seems to recompute what GetWorldPosition already does.
2245 * Replace with a call to GetWorldPosition (before unlinking)
2246 Quaternion parentRot = m_rootPart.RotationOffset;
2205 Vector3 axPos = linkPart.OffsetPosition; 2247 Vector3 axPos = linkPart.OffsetPosition;
2206
2207 axPos *= parentRot; 2248 axPos *= parentRot;
2208 linkPart.OffsetPosition = new Vector3(axPos.X, axPos.Y, axPos.Z); 2249 linkPart.OffsetPosition = new Vector3(axPos.X, axPos.Y, axPos.Z);
2209 linkPart.GroupPosition = AbsolutePosition + linkPart.OffsetPosition; 2250 linkPart.GroupPosition = AbsolutePosition + linkPart.OffsetPosition;
2210 linkPart.OffsetPosition = new Vector3(0, 0, 0); 2251 linkPart.OffsetPosition = new Vector3(0, 0, 0);
2211 2252 */
2253 linkPart.GroupPosition = worldPos;
2254 linkPart.OffsetPosition = Vector3.Zero;
2212 linkPart.RotationOffset = worldRot; 2255 linkPart.RotationOffset = worldRot;
2213 2256
2257 // Create a new SOG to go around this unlinked and unattached SOP
2214 SceneObjectGroup objectGroup = new SceneObjectGroup(linkPart); 2258 SceneObjectGroup objectGroup = new SceneObjectGroup(linkPart);
2215 2259
2216 m_scene.AddNewSceneObject(objectGroup, true); 2260 m_scene.AddNewSceneObject(objectGroup, true);
@@ -2239,42 +2283,56 @@ namespace OpenSim.Region.Framework.Scenes
2239 m_isBackedUp = false; 2283 m_isBackedUp = false;
2240 } 2284 }
2241 2285
2286 // This links an SOP from a previous linkset into my linkset.
2287 // The trick is that the SOP's position and rotation are relative to the old root SOP's
2288 // so we are passed in the position and rotation of the old linkset so this can
2289 // unjigger this SOP's position and rotation from the previous linkset and
2290 // then make them relative to my linkset root.
2242 private void LinkNonRootPart(SceneObjectPart part, Vector3 oldGroupPosition, Quaternion oldGroupRotation, int linkNum) 2291 private void LinkNonRootPart(SceneObjectPart part, Vector3 oldGroupPosition, Quaternion oldGroupRotation, int linkNum)
2243 { 2292 {
2244 Quaternion parentRot = oldGroupRotation; 2293 Quaternion parentRot = oldGroupRotation;
2245 Quaternion oldRot = part.RotationOffset; 2294 Quaternion oldRot = part.RotationOffset;
2246 Quaternion worldRot = parentRot * oldRot;
2247
2248 parentRot = oldGroupRotation;
2249 2295
2296 // Move our position to not be relative to the old parent
2250 Vector3 axPos = part.OffsetPosition; 2297 Vector3 axPos = part.OffsetPosition;
2251
2252 axPos *= parentRot; 2298 axPos *= parentRot;
2253 part.OffsetPosition = axPos; 2299 part.OffsetPosition = axPos;
2254 part.GroupPosition = oldGroupPosition + part.OffsetPosition; 2300 part.GroupPosition = oldGroupPosition + part.OffsetPosition;
2255 part.OffsetPosition = Vector3.Zero; 2301 part.OffsetPosition = Vector3.Zero;
2302
2303 // Compution our rotation to be not relative to the old parent
2304 Quaternion worldRot = parentRot * oldRot;
2256 part.RotationOffset = worldRot; 2305 part.RotationOffset = worldRot;
2257 2306
2307 // Add this SOP to our linkset
2258 part.SetParent(this); 2308 part.SetParent(this);
2259 part.ParentID = m_rootPart.LocalId; 2309 part.ParentID = m_rootPart.LocalId;
2260
2261 m_parts.Add(part.UUID, part); 2310 m_parts.Add(part.UUID, part);
2262 2311
2263 part.LinkNum = linkNum; 2312 part.LinkNum = linkNum;
2264 2313
2314 // Compute the new position of this SOP relative to the group position
2265 part.OffsetPosition = part.GroupPosition - AbsolutePosition; 2315 part.OffsetPosition = part.GroupPosition - AbsolutePosition;
2266 2316
2267 Quaternion rootRotation = m_rootPart.RotationOffset; 2317 // (radams1 20120711: I don't know why part.OffsetPosition is set multiple times.
2318 // It would have the affect of setting the physics engine position multiple
2319 // times. In theory, that is not necessary but I don't have a good linkset
2320 // test to know that cleaning up this code wouldn't break things.)
2268 2321
2322 // Rotate the relative position by the rotation of the group
2323 Quaternion rootRotation = m_rootPart.RotationOffset;
2269 Vector3 pos = part.OffsetPosition; 2324 Vector3 pos = part.OffsetPosition;
2270 pos *= Quaternion.Inverse(rootRotation); 2325 pos *= Quaternion.Inverse(rootRotation);
2271 part.OffsetPosition = pos; 2326 part.OffsetPosition = pos;
2272 2327
2328 // Compute the SOP's rotation relative to the rotation of the group.
2273 parentRot = m_rootPart.RotationOffset; 2329 parentRot = m_rootPart.RotationOffset;
2274 oldRot = part.RotationOffset; 2330 oldRot = part.RotationOffset;
2275 Quaternion newRot = Quaternion.Inverse(parentRot) * oldRot; 2331 Quaternion newRot = Quaternion.Inverse(parentRot) * oldRot;
2276 part.RotationOffset = newRot; 2332 part.RotationOffset = newRot;
2277 2333
2334 // Since this SOP's state has changed, push those changes into the physics engine
2335 // and the simulator.
2278 part.UpdatePrimFlags(UsesPhysics, IsTemporary, IsPhantom, IsVolumeDetect); 2336 part.UpdatePrimFlags(UsesPhysics, IsTemporary, IsPhantom, IsVolumeDetect);
2279 } 2337 }
2280 2338
diff --git a/OpenSim/Region/Framework/Scenes/SceneObjectPart.cs b/OpenSim/Region/Framework/Scenes/SceneObjectPart.cs
index b3f11a7..4b2fede 100644
--- a/OpenSim/Region/Framework/Scenes/SceneObjectPart.cs
+++ b/OpenSim/Region/Framework/Scenes/SceneObjectPart.cs
@@ -693,9 +693,11 @@ namespace OpenSim.Region.Framework.Scenes
693 { 693 {
694 // If this is a linkset, we don't want the physics engine mucking up our group position here. 694 // If this is a linkset, we don't want the physics engine mucking up our group position here.
695 PhysicsActor actor = PhysActor; 695 PhysicsActor actor = PhysActor;
696 // If physical and the root prim of a linkset, the position of the group is what physics thinks.
696 if (actor != null && ParentID == 0) 697 if (actor != null && ParentID == 0)
697 m_groupPosition = actor.Position; 698 m_groupPosition = actor.Position;
698 699
700 // If I'm an attachment, my position is reported as the position of who I'm attached to
699 if (ParentGroup.IsAttachment) 701 if (ParentGroup.IsAttachment)
700 { 702 {
701 ScenePresence sp = ParentGroup.Scene.GetScenePresence(ParentGroup.AttachedAvatar); 703 ScenePresence sp = ParentGroup.Scene.GetScenePresence(ParentGroup.AttachedAvatar);
@@ -721,7 +723,7 @@ namespace OpenSim.Region.Framework.Scenes
721 } 723 }
722 else 724 else
723 { 725 {
724 // To move the child prim in respect to the group position and rotation we have to calculate 726 // The physics engine always sees all objects (root or linked) in world coordinates.
725 actor.Position = GetWorldPosition(); 727 actor.Position = GetWorldPosition();
726 actor.Orientation = GetWorldRotation(); 728 actor.Orientation = GetWorldRotation();
727 } 729 }
@@ -795,6 +797,8 @@ namespace OpenSim.Region.Framework.Scenes
795 { 797 {
796 // We don't want the physics engine mucking up the rotations in a linkset 798 // We don't want the physics engine mucking up the rotations in a linkset
797 PhysicsActor actor = PhysActor; 799 PhysicsActor actor = PhysActor;
800 // If this is a root of a linkset, the real rotation is what the physics engine thinks.
801 // If not a root prim, the offset rotation is computed by SOG and is relative to the root.
798 if (ParentID == 0 && (Shape.PCode != 9 || Shape.State == 0) && actor != null) 802 if (ParentID == 0 && (Shape.PCode != 9 || Shape.State == 0) && actor != null)
799 { 803 {
800 if (actor.Orientation.X != 0f || actor.Orientation.Y != 0f 804 if (actor.Orientation.X != 0f || actor.Orientation.Y != 0f
@@ -1980,14 +1984,20 @@ namespace OpenSim.Region.Framework.Scenes
1980 /// <returns>A Linked Child Prim objects position in world</returns> 1984 /// <returns>A Linked Child Prim objects position in world</returns>
1981 public Vector3 GetWorldPosition() 1985 public Vector3 GetWorldPosition()
1982 { 1986 {
1983 Quaternion parentRot = ParentGroup.RootPart.RotationOffset; 1987 Vector3 ret;
1984 Vector3 axPos = OffsetPosition; 1988 if (_parentID == 0)
1985 axPos *= parentRot; 1989 // if a root SOP, my position is what it is
1986 Vector3 translationOffsetPosition = axPos; 1990 ret = GroupPosition;
1987 if(_parentID == 0)
1988 return GroupPosition;
1989 else 1991 else
1990 return ParentGroup.AbsolutePosition + translationOffsetPosition; 1992 {
1993 // If a child SOP, my position is relative to the root SOP so take
1994 // my info and add the root's position and rotation to
1995 // get my world position.
1996 Quaternion parentRot = ParentGroup.RootPart.RotationOffset;
1997 Vector3 translationOffsetPosition = OffsetPosition * parentRot;
1998 ret = ParentGroup.AbsolutePosition + translationOffsetPosition;
1999 }
2000 return ret;
1991 } 2001 }
1992 2002
1993 /// <summary> 2003 /// <summary>
@@ -2004,6 +2014,8 @@ namespace OpenSim.Region.Framework.Scenes
2004 } 2014 }
2005 else 2015 else
2006 { 2016 {
2017 // A child SOP's rotation is relative to the root SOP's rotation.
2018 // Combine them to get my absolute rotation.
2007 Quaternion parentRot = ParentGroup.RootPart.RotationOffset; 2019 Quaternion parentRot = ParentGroup.RootPart.RotationOffset;
2008 Quaternion oldRot = RotationOffset; 2020 Quaternion oldRot = RotationOffset;
2009 newRot = parentRot * oldRot; 2021 newRot = parentRot * oldRot;