CesiumGS / cesium-unity

Bringing the 3D geospatial ecosystem to Unity
https://cesium.com/platform/cesium-for-unity/
Apache License 2.0
358 stars 83 forks source link

Globe Anchor Doesn't Support Prefab Overrides #335

Closed Waffle1434 closed 1 year ago

Waffle1434 commented 1 year ago

If the CesiumGlobeAnchor component is on a GameObject in a prefab, the prefab's world coordinates will always be used regardless of coordinate overrides for prefab instances.

For example, I have a prefab of a helicopter with a world anchor: image It has a default coordinate of 33.4588218, -111.72937073 because that's where I originally made it a prefab.

If I place multiple instances of the prefab, all instances will revert back to this coordinate either when "Play" mode enters, or the scene is reloaded: image

Two helicopters now overlapping at the same coordinate after play/reload: image

Notice how Scale has a blue highlight showing its an override, while the globe anchor has nothing marked as changed: image

I fixed the issue for now by not including the anchor in the prefab, and I manually add the anchor to each instance.

kring commented 1 year ago

This is a tricky one.

If you put the anchored object inside the CesiumGeoreference, and then modify its Transform, the CesiumGlobeAnchor position will update accordingly. And should remember its position from then on.

But if you don't explicitly set the position (after it's nested inside the CesiumGeoreference!), then the CesiumGlobeAnchor thinks that it has an authoritative ECEF position, and it overwrites the transform with one computed from that ECEF position.

Basically the Transform position needs to be set last, and all would be well. But when it's set too early (before the component is enabled, or before it's nested in the CesiumGeoreference), then the position inherited from the prefab is deemed accurate. When you call Instantiate and pass a position, that ends up setting the position too early.

I'm not quite sure what we can do about this. Perhaps if Unity sends some message to the component when it is created from a prefab? But I don't see one. The only other thing I can think of is to serialize the "last known" position, so we can detect a change in it even if a new position is already set by the time OnEnable is called. That will make the object a bit bigger, of course, but I'll have to think through whether there are any more serious consequences of this.

But as a workaround, can you try simply setting the position again after the Instantiate call returns?

Waffle1434 commented 1 year ago

In this case I'm not calling GameObject.Instantiate. I have a scene with multiple prefab instances hand-placed as children of the Georeference at edit-time, and I save the scene. They have only existed as children of the Georeference. image

It is as if the ECEF positions aren't being saved in the scene file for prefab instances, so when de-serialized the component has no idea it shouldn't use the prefab's default.

If the prefab doesn't contain an anchor, and I add it manually in the scene (my workaround), the _localToGlobeFixedMatrix is serialized inside the .unity file and it loads correctly:

PrefabInstance:
  m_ObjectHideFlags: 0
  serializedVersion: 2
  m_Modification:
    m_TransformParent: {fileID: 666272557}
    m_Modifications:
    - target: {fileID: 319014584688397647, guid: 724ba02cd68e7ca478e49f54efb04ea1,
        type: 3}
      propertyPath: m_Name
      value: AH-64D Cesium (1)
      objectReference: {fileID: 0}
...
MonoBehaviour:
...
  _adjustOrientationForGlobeWhenMoving: 1
  _detectTransformChanges: 1
  _localToGlobeFixedMatrix:
    c0:
      x: 0.6676539270647021
      y: -1.2620902257691409
      z: -1.4004906604188934
      w: 0
    c1:
      x: -0.6175723941958005
      y: -1.5501564798246226
      z: 1.102551235083885
      w: 0
    c2:
      x: 1.7812494046963758
      y: -0.0643908541531326
      z: 0.9072000718682209
      w: 0
    c3:
      x: -1972096.133820307
      y: -4948730.1963643925
      z: 3496569.9432099764
      w: 1
  _localToGlobeFixedMatrixIsValid: 1

If I try moving an instance around with the anchor inside the prefab, its never written to the scene. m_Modifications should have an entry for _localToGlobeFixedMatrix but it doesn't. Unity is storing modifications/overrides for the Transform's XYZ position though (of course, ignored by Cesium by design). When the scene is played or reloaded, it will use the prefab's default coordinates because no modification in the scene file says otherwise:

PrefabInstance:
  m_ObjectHideFlags: 0
  serializedVersion: 2
  m_Modification:
    m_TransformParent: {fileID: 666272557}
    m_Modifications:
    - target: {fileID: 319014584688397647, guid: 724ba02cd68e7ca478e49f54efb04ea1,
        type: 3}
      propertyPath: m_Name
      value: AH-64D Cesium
      objectReference: {fileID: 0}
...
      propertyPath: m_LocalPosition.x
      value: -348.56
      objectReference: {fileID: 0}
    - target: {fileID: 1091716913026111477, guid: 724ba02cd68e7ca478e49f54efb04ea1,
        type: 3}
      propertyPath: m_LocalPosition.y
      value: 20.17
      objectReference: {fileID: 0}
    - target: {fileID: 1091716913026111477, guid: 724ba02cd68e7ca478e49f54efb04ea1,
        type: 3}
      propertyPath: m_LocalPosition.z
      value: -128.88
      objectReference: {fileID: 0}
...
  m_SourcePrefab: {fileID: 100100000, guid: 724ba02cd68e7ca478e49f54efb04ea1, type: 3}
--- !u!4 &1261796114 stripped
Transform:
  m_CorrespondingSourceObject: {fileID: 1091716913026111477, guid: 724ba02cd68e7ca478e49f54efb04ea1,
    type: 3}
  m_PrefabInstance: {fileID: 1261796113}
  m_PrefabAsset: {fileID: 0}

From Cesium's perspective, in scene edit-time I'm moving the helicopter around the scene and its recalculating the ECEF. However, its not saving it anywhere for some reason when its a prefab instance. I don't think there's anything you can do in regards to being notified, it needs to be fixed at serialization-time.

I've never worked with serializing partial class's or internal [SerializeField]'s, maybe this is a bug with that, no idea.

kring commented 1 year ago

Thanks for the details! I think the problem is that modifying the Transform causes a coroutine to automatically update the ECEF coordinates, but Unity doesn't know about those changes so it doesn't save them as modifications of the prefab. We may be able to fix this by adding a call to https://docs.unity3d.com/ScriptReference/PrefabUtility.RecordPrefabInstancePropertyModifications.html to the coroutine.

kring commented 1 year ago

That seems to work. I opened a PR for it: #338