Skip to content

Commit

Permalink
Merge pull request #507 from CesiumGS/height-query
Browse files Browse the repository at this point in the history
Height query API for Unity
  • Loading branch information
j9liu authored Sep 27, 2024
2 parents 966498e + e17c287 commit 917ba10
Show file tree
Hide file tree
Showing 16 changed files with 405 additions and 9 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ jobs:
name: Windows Package
path: d:\cesium\CesiumForUnityBuildProject\*.tgz
- name: Run Tests
env:
CESIUM_ION_TOKEN_FOR_TESTS: ${{ secrets.CESIUM_ION_TOKEN_FOR_TESTS }}
run: |
start -FilePath "C:\Program Files\Unity\Hub\Editor\2022.3.41f1\Editor\Unity.exe" -ArgumentList "-runTests -batchmode -projectPath d:\cesium\CesiumForUnityBuildProject -testResults d:\cesium\temp\TestResults.xml -testPlatform PlayMode -logFile d:\cesium\temp\test-log.txt" -Wait
cat d:\cesium\temp\test-log.txt
Expand Down Expand Up @@ -271,6 +273,8 @@ jobs:
name: macOS Package
path: ~/cesium/CesiumForUnityBuildProject/*.tgz
- name: Run Tests
env:
CESIUM_ION_TOKEN_FOR_TESTS: ${{ secrets.CESIUM_ION_TOKEN_FOR_TESTS }}
run: |
/Applications/Unity/Hub/Editor/2022.3.41f1/Unity.app/Contents/MacOS/Unity -runTests -batchmode -projectPath ~/cesium/CesiumForUnityBuildProject -testResults ~/cesium/CesiumForUnityBuildProject/TestResults.xml -testPlatform PlayMode -logFile ~/cesium/CesiumForUnityBuildProject/test-log.txt
cat ~/cesium/CesiumForUnityBuildProject/test-log.txt
Expand Down
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
##### Additions :tada:

- Added a new `CesiumCameraManager` component. It allows configuration of the cameras to use for Cesium3DTileset culling and level-of-detail.
- Added `SampleHeightMostDetailed` method to `Cesium3DTileset`. It asynchronously queries the height of a tileset at a list of positions.

##### Fixes :wrench:

Expand Down
14 changes: 13 additions & 1 deletion Reinterop~/CSharpObjectHandleUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,19 @@ public static void FreeHandle(IntPtr handle)
if (handle == IntPtr.Zero)
return;

GCHandle.FromIntPtr(handle).Free();
try
{
GCHandle.FromIntPtr(handle).Free();
}
catch (ArgumentException e)
{
// The "GCHandle value belongs to a different domain" exception tends
// to happen on AppDomain reload, which is common in Unity.
// Catch the exception to prevent it propagating through our native
// code and blowing things up.
// See: https://github.com/CesiumGS/cesium-unity/issues/18
System.Console.WriteLine(e.ToString());
}
}

public static object GetObjectFromHandle(IntPtr handle)
Expand Down
10 changes: 5 additions & 5 deletions Reinterop~/MethodsImplementedInCpp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ private static void GenerateMethod(CppGenerationContext context, TypeToGenerate

CSharpType csWrapperType = CSharpType.FromSymbol(context, item.Type);
CSharpType csReturnType = CSharpType.FromSymbol(context, method.ReturnType);
var csParameters = method.Parameters.Select(parameter => (Name: parameter.Name, CallName: parameter.Name, Type: CSharpType.FromSymbol(context, parameter.Type)));
var csParameters = method.Parameters.Select(parameter => (Name: parameter.Name, CallName: parameter.Name, Type: CSharpType.FromSymbol(context, parameter.Type), IsParams: parameter.IsParams));
var csParametersInterop = csParameters;
var implementationPointer = new CSharpType(context, InteropTypeKind.Primitive, csWrapperType.Namespaces, csWrapperType.Name + ".ImplementationHandle", csWrapperType.SpecialType, null);

Expand All @@ -354,13 +354,13 @@ private static void GenerateMethod(CppGenerationContext context, TypeToGenerate
{
csParametersInterop = new[]
{
(Name: "implementation", CallName: "_implementation", Type: implementationPointer)
(Name: "implementation", CallName: "_implementation", Type: implementationPointer, IsParams: false)
}.Concat(csParametersInterop);
}

csParametersInterop = new[]
{
(Name: "thiz", CallName: "this", Type: csWrapperType),
(Name: "thiz", CallName: "this", Type: csWrapperType, IsParams: false),
}.Concat(csParametersInterop);
}

Expand All @@ -370,7 +370,7 @@ private static void GenerateMethod(CppGenerationContext context, TypeToGenerate
{
csParametersInterop = csParametersInterop.Concat(new[]
{
(Name: "pReturnValue", CallName: "&returnValue", Type: csInteropReturnType.AsPointer())
(Name : "pReturnValue", CallName : "&returnValue", Type : csInteropReturnType.AsPointer(), IsParams : false)
});
csInteropReturnType = CSharpType.FromSymbol(context, returnType.Kind == InteropTypeKind.Nullable ? context.Compilation.GetSpecialType(SpecialType.System_Byte) : context.Compilation.GetSpecialType(SpecialType.System_Void));
}
Expand Down Expand Up @@ -429,7 +429,7 @@ private static void GenerateMethod(CppGenerationContext context, TypeToGenerate
result.CSharpPartialMethodDefinitions.Methods.Add(new(
methodDefinition:
$$"""
{{modifiers}} partial {{csReturnType.GetFullyQualifiedName()}} {{method.Name}}({{string.Join(", ", csParameters.Select(parameter => $"{parameter.Type.GetFullyQualifiedName()} {parameter.Name}"))}})
{{modifiers}} partial {{csReturnType.GetFullyQualifiedName()}} {{method.Name}}({{string.Join(", ", csParameters.Select(parameter => $"{(parameter.IsParams ? "params " : "")}{parameter.Type.GetFullyQualifiedName()} {parameter.Name}"))}})
{
unsafe
{
Expand Down
31 changes: 31 additions & 0 deletions Runtime/Cesium3DTileset.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
using Reinterop;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Mathematics;
using UnityEngine;

namespace CesiumForUnity
Expand Down Expand Up @@ -714,6 +718,30 @@ public bool createPhysicsMeshes
/// </summary>
public partial void FocusTileset();

/// <summary>
/// Initiates an asynchronous query for the height of this tileset at a list of
/// cartographic positions, where the longitude (X) and latitude (Y) are given in degrees.
/// The most detailed available tiles are used to determine each height.
/// </summary>
/// <remarks>
/// <para>
/// The height of the input positions is ignored, unless height sampling fails
/// at that location. The output height is expressed in meters above the ellipsoid
/// (usually WGS84), which should not be confused with a height above mean sea level.
/// </para>
/// <para>
/// Use <see cref="WaitForTask"/> inside a coroutine to wait for the asynchronous height
/// query to complete.
/// </para>
/// </remarks>
/// <param name="longitudeLatitudeHeightPositions">
/// The cartographic positions for which to sample heights. The X component is the
/// Longitude (degrees), the Y component is the Latitude (degrees), and the Z component
/// is the Height (meters).
/// </param>
/// <returns>An asynchronous task that will provide the requested heights when complete.</returns>
public partial Task<CesiumSampleHeightResult> SampleHeightMostDetailed(params double3[] longitudeLatitudeHeightPositions);

#endregion

#region Private Methods
Expand All @@ -731,6 +759,8 @@ public bool createPhysicsMeshes

#endregion

#region Backward Compatibility

void ISerializationCallbackReceiver.OnBeforeSerialize()
{
}
Expand All @@ -748,5 +778,6 @@ void ISerializationCallbackReceiver.OnAfterDeserialize()
#if UNITY_EDITOR
private bool _useDefaultServer = false;
#endif
#endregion
}
}
47 changes: 47 additions & 0 deletions Runtime/CesiumSampleHeightResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using Unity.Mathematics;

namespace CesiumForUnity
{
/// <summary>
/// The asynchronous result of a call to <see cref="Cesium3DTileset.SampleHeightMostDetailed"/>.
/// </summary>
public class CesiumSampleHeightResult
{
/// <summary>
/// The positions and their sampled heights. The X component is Longitude (degrees),
/// the Y component is Latitude (degrees), and the Z component is Height (meters)
/// above the ellipsoid (usually WGS84).
/// </summary>
/// <remarks>
/// <para>
/// For each resulting position, its longitude and latitude values will match
/// values from its input. Its height will either be the height sampled from
/// the tileset at that position, or the original input height if the sample
/// was unsuccessful. To determine which, look at the value of
/// <see cref="CesiumSampleHeightResult.sampleSuccess"/> at the same index.
/// </para>
/// <para>
/// The returned height is measured from the ellipsoid (usually WGS84) and
/// should not be confused with a height above Mean Sea Level.
/// </para>
/// </remarks>
public double3[] longitudeLatitudeHeightPositions { get; set; }

/// <summary>
/// Indicates whether the height for the position at the corresponding index was sampled
/// successfully.
/// </summary>
/// <remarks>
/// If true, then the corresponding position in
/// <see cref="CesiumSampleHeightResult.longitudeLatitudeHeightPositions"/> uses
/// the height sampled from the tileset. If false, the height could not be sampled for
/// the position, so its height is the same as the original input height.
/// </remarks>
public bool[] sampleSuccess { get; set; }

/// <summary>
/// Any warnings that occurred while sampling heights.
/// </summary>
public string[] warnings { get; set; }
}
}
11 changes: 11 additions & 0 deletions Runtime/CesiumSampleHeightResult.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions Runtime/ConfigureReinterop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,21 @@ Cesium3DTilesetLoadFailureDetails tilesetDetails
camera = manager.additionalCameras[i];
}

TaskCompletionSource<CesiumSampleHeightResult> promise = new TaskCompletionSource<CesiumSampleHeightResult>();
promise.SetException(new Exception("message"));
CesiumSampleHeightResult result = new CesiumSampleHeightResult();
result.longitudeLatitudeHeightPositions = null;
result.sampleSuccess = null;
result.warnings = null;
promise.SetResult(result);
Task<CesiumSampleHeightResult> task = promise.Task;

double3[] positions = null;
for (int i = 0; i < positions.Length; ++i)
{
positions[i] = positions[i];
}

#if UNITY_EDITOR
SceneView sv = SceneView.lastActiveSceneView;
sv.pivot = sv.pivot;
Expand Down
25 changes: 25 additions & 0 deletions Runtime/WaitForTask.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;
using UnityEngine;

namespace CesiumForUnity
{
/// <summary>
/// A YieldInstruction that can be yielded from a coroutine in order to wait
/// until a given task completes.
/// </summary>
public class WaitForTask : CustomYieldInstruction
{
private IAsyncResult _task;

/// <summary>
/// Initializes a new instance.
/// </summary>
/// <param name="task">The task to wait for.</param>
public WaitForTask(IAsyncResult task)
{
this._task = task;
}

public override bool keepWaiting => !this._task.IsCompleted;
}
}
11 changes: 11 additions & 0 deletions Runtime/WaitForTask.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

129 changes: 129 additions & 0 deletions Tests/TestCesium3DTileset.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
using CesiumForUnity;
using NUnit.Framework;
using System;
using System.Collections;
using System.Threading.Tasks;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.TestTools;

public class TestCesium3DTileset
{
[UnityTest]
public IEnumerator SampleHeightMostDetailedWorksWithAnEmptyArrayOfPositions()
{
GameObject go = new GameObject();
go.name = "Cesium World Terrain";
Cesium3DTileset tileset = go.AddComponent<Cesium3DTileset>();
tileset.ionAccessToken = Environment.GetEnvironmentVariable("CESIUM_ION_TOKEN_FOR_TESTS") ?? "";
tileset.ionAssetID = 1;

Task<CesiumSampleHeightResult> task = tileset.SampleHeightMostDetailed();

yield return new WaitForTask(task);

CesiumSampleHeightResult result = task.Result;
Assert.IsNotNull(result);
Assert.IsNotNull(result.longitudeLatitudeHeightPositions);
Assert.IsNotNull(result.sampleSuccess);
Assert.IsNotNull(result.warnings);
Assert.AreEqual(result.longitudeLatitudeHeightPositions.Length, 0);
Assert.AreEqual(result.sampleSuccess.Length, 0);
Assert.AreEqual(result.warnings.Length, 0);
}

[UnityTest]
public IEnumerator SampleHeightMostDetailedWorksWithASinglePosition()
{
GameObject go = new GameObject();
go.name = "Cesium World Terrain";
Cesium3DTileset tileset = go.AddComponent<Cesium3DTileset>();
tileset.ionAccessToken = Environment.GetEnvironmentVariable("CESIUM_ION_TOKEN_FOR_TESTS") ?? "";
tileset.ionAssetID = 1;

Task<CesiumSampleHeightResult> task = tileset.SampleHeightMostDetailed(new double3(-105.1, 40.1, 1.0));

yield return new WaitForTask(task);

CesiumSampleHeightResult result = task.Result;
Assert.IsNotNull(result);
Assert.IsNotNull(result.longitudeLatitudeHeightPositions);
Assert.IsNotNull(result.sampleSuccess);
Assert.IsNotNull(result.warnings);
Assert.AreEqual(result.longitudeLatitudeHeightPositions.Length, 1);
Assert.AreEqual(result.sampleSuccess.Length, 1);
Assert.AreEqual(result.warnings.Length, 0);

Assert.AreEqual(result.sampleSuccess[0], true);
Assert.AreEqual(result.longitudeLatitudeHeightPositions[0].x, -105.1, 1e-12);
Assert.AreEqual(result.longitudeLatitudeHeightPositions[0].y, 40.1, 1e-12);
// Returned height should be different from the original height (1.0) by at least one meter.
Assert.IsTrue(math.abs(result.longitudeLatitudeHeightPositions[0].z - 1.0) > 1.0);
}

[UnityTest]
public IEnumerator SampleHeightMostDetailedWorksWithMultiplePositions()
{
GameObject go = new GameObject();
go.name = "Cesium World Terrain";
Cesium3DTileset tileset = go.AddComponent<Cesium3DTileset>();
tileset.ionAccessToken = Environment.GetEnvironmentVariable("CESIUM_ION_TOKEN_FOR_TESTS") ?? "";
tileset.ionAssetID = 1;

Task<CesiumSampleHeightResult> task = tileset.SampleHeightMostDetailed(
new double3(-105.1, 40.1, 1.0),
new double3(105.1, -40.1, 1.0));

yield return new WaitForTask(task);

CesiumSampleHeightResult result = task.Result;
Assert.IsNotNull(result);
Assert.IsNotNull(result.longitudeLatitudeHeightPositions);
Assert.IsNotNull(result.sampleSuccess);
Assert.IsNotNull(result.warnings);
Assert.AreEqual(result.longitudeLatitudeHeightPositions.Length, 2);
Assert.AreEqual(result.sampleSuccess.Length, 2);
Assert.AreEqual(result.warnings.Length, 0);

Assert.AreEqual(result.sampleSuccess[0], true);
Assert.AreEqual(result.longitudeLatitudeHeightPositions[0].x, -105.1, 1e-12);
Assert.AreEqual(result.longitudeLatitudeHeightPositions[0].y, 40.1, 1e-12);
// Returned height should be different from the original height (1.0) by at least one meter.
Assert.IsTrue(math.abs(result.longitudeLatitudeHeightPositions[0].z - 1.0) > 1.0);

Assert.AreEqual(result.sampleSuccess[1], true);
Assert.AreEqual(result.longitudeLatitudeHeightPositions[1].x, 105.1, 1e-12);
Assert.AreEqual(result.longitudeLatitudeHeightPositions[1].y, -40.1, 1e-12);
// Returned height should be different from the original height (1.0) by at least one meter.
Assert.IsTrue(math.abs(result.longitudeLatitudeHeightPositions[1].z - 1.0) > 1.0);
}

[UnityTest]
public IEnumerator SampleHeightMostDetailedIndicatesNotSampledForPositionOutsideTileset()
{
GameObject go = new GameObject();
go.name = "Melbourne Photogrammetry";
Cesium3DTileset tileset = go.AddComponent<Cesium3DTileset>();
tileset.ionAccessToken = Environment.GetEnvironmentVariable("CESIUM_ION_TOKEN_FOR_TESTS") ?? "";
tileset.ionAssetID = 69380;

// Somewhere in Sydney, not Melbourne
Task<CesiumSampleHeightResult> task = tileset.SampleHeightMostDetailed(new double3(151.20972, -33.87100, 1.0));

yield return new WaitForTask(task);

CesiumSampleHeightResult result = task.Result;
Assert.IsNotNull(result);
Assert.IsNotNull(result.longitudeLatitudeHeightPositions);
Assert.IsNotNull(result.sampleSuccess);
Assert.IsNotNull(result.warnings);
Assert.AreEqual(result.longitudeLatitudeHeightPositions.Length, 1);
Assert.AreEqual(result.sampleSuccess.Length, 1);
Assert.AreEqual(result.warnings.Length, 0);

Assert.AreEqual(result.sampleSuccess[0], false);
Assert.AreEqual(result.longitudeLatitudeHeightPositions[0].x, 151.20972, 1e-12);
Assert.AreEqual(result.longitudeLatitudeHeightPositions[0].y, -33.87100, 1e-12);
Assert.AreEqual(result.longitudeLatitudeHeightPositions[0].z, 1.0, 1e-12);
}
}
Loading

0 comments on commit 917ba10

Please sign in to comment.