Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to exclude tiles by implementing a C# class. #248

Merged
merged 19 commits into from
Mar 31, 2023
Merged

Conversation

kring
Copy link
Member

@kring kring commented Mar 16, 2023

Depends on CesiumGS/cesium-native#608

Added CesiumTileExcluder, which is a C# version of cesium-native's ITileExcluder.

The idea is that we can add a C# class like this to our project:

using CesiumForUnity;
using UnityEngine;

[RequireComponent(typeof(BoxCollider))]
public class CesiumBoxExcluder : CesiumTileExcluder
{
    private BoxCollider _boxCollider;
    private Bounds _bounds;

    public bool invert = false;

    protected override void OnEnable()
    {
        this._boxCollider = this.gameObject.GetComponent<BoxCollider>();
        this._bounds = new Bounds(this._boxCollider.center, this._boxCollider.size);

        base.OnEnable();
    }

    protected void Update()
    {
        this._bounds.center = this._boxCollider.center;
        this._bounds.size = this._boxCollider.size;
    }

    public bool CompletelyContains(Bounds bounds)
    {
        return Vector3.Min(this._bounds.max, bounds.max) == bounds.max &&
               Vector3.Max(this._bounds.min, bounds.min) == bounds.min;
    }

    public override bool ShouldExclude(Cesium3DTile tile)
    {
        if (!this.enabled)
        {
            return false;
        }

        if (this.invert)
        {
            return this.CompletelyContains(tile.bounds);
        }

        return !this._bounds.Intersects(tile.bounds);
    }
}

And then add an instance of this class as a component on the Cesium3DTileset or any of its parents, up to and including the CesiumGeoreference.

With that in place, cesium-native will call the ShouldExclude method for each tile that it is considering loading or rendering. This will happen a lot, so this method needs to be fast. Return true to skip loading and rendering that tile. Return false to load it and render it as normal.

In the implementation above, a user-defined BoxCollider is also attached to the same GameObject as the CesiumBoxExcluder. Any tiles that intersect this BoxCollider are loaded and rendered, others are not. So tiles that are entirely outside of the box are ignored. The given tile's bounds property is an axis-aligned bounding box in the same coordinate system as the CesiumBoxExcluder, so it's easy to do tests like this.

In this screenshot, the BoxCollider is the green box in the middle of Melbourne. All of the visible tiles are at least partially inside the box:

image

If we disable the CesiumBoxExcluder, all the tiles load as normal:

image

Also in this PR: Extended Reinterop to allow constructors of blittable value types to be called from C++. On the C++ side, these are exposed as Constrct methods rather than C++ constructors so that brace initialization can continue to be used to initialize fields without calling into C#.

@kring
Copy link
Member Author

kring commented Mar 16, 2023

The above avoids loading tiles that are entirely outside of the area of interest. But we may also want to clip the partially-overlapping tiles so they aren't drawn outside the box. This can be accomplished with a custom material.

First, make a copy of CesiumDefaultTilesetMaterial and CesiumDefaultTilesetShader found in the Cesium for Unity -> Runtime -> Resources folder. Copy them into your project rather than into the Cesium for Unity plugin. Rename them to CesiumClippingTilesetMaterial and CesiumClippingTilesetShader.

Click CesiumClippingTilesetMaterial and on the Inspector panel, change the Shader property to CesiumClippingTilesetShader (the shader you just copied). Enable the "Alpha Clipping" property.

Double-click CesiumClippingTilesetShader to open it in the shader graph editor. Near the top-right of the graph, add some three new nodes: Position, Custom Function, and Multiply.

Click the Custom Function node. Add a Vector 3 input named position. Add a Float output named alpha. Switch the Type to String, give it a Name of Clipper (or whatever you like), and enter the following code in the Body:

if (abs(position.x) > 500.0 || abs(position.y) > 500.0 || abs(position.z) > 500.0) alpha = 0.0; else alpha = 1.0;

The idea is that if the world position of the currently-rendered pixel is outside our box, which is centered at the origin and is 1000 meters long in all directions, then we'll set the alpha value of this pixel to 0.0 so it won't be rendered. The code here can be adjusted for other box dimensions or other shapes entirely.

Click the "Position" node and change the Space property to Absolute World. Wire the Position output into the Custom Function's input.

Wire the output of the custom function to the "A" slot of the Multiply node. Connect the "A" channel of the Split node just below the Raster Overlays section of the shader to the "B" slot of the Multiply node. Connect the output of the Multiply node to the Alpha property of the Fragment on the middle-right side of the shader graph.

It should look something like this:

image

Save the material, and then select the Cesium3DTileset and change the "Opaque Material" property to the CesiumClippingTilesetMaterial you created. The tileset should now look like this:

image

The end result is that pixels outside the box aren't drawn, and tiles entirely outside the box aren't even loaded.

Copy link
Contributor

@j9liu j9liu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kring looks good for the most part! I left a handful of comments. I tested the example you provided and it works well, though I had two concerns:

  1. The example excluder gets less accurate when the tileset itself is transformed. For example, here the box collider is at (0, 0, 0), but the tileset has been shifted, and the excluded tiles are off:

image

If it's too complicated to make a fix, we can merge this preliminary support and open a follow-up PR, but just let me know.

  1. If I try to invert the ShouldExclude condition in the example, nothing shows up. That is, if the function is defined as so:
public override bool ShouldExclude(Cesium3DTile tile) 
{
     return this._box.bounds.Intersects(tile.bounds);
}

This doesn't make sense to me intuitively, because I'd expect to be cutting out a square in the tileset, instead of limiting the tileset to a square. But maybe I'm missing something?

CHANGES.md Outdated Show resolved Hide resolved
Runtime/CesiumTileExcluder.cs Outdated Show resolved Hide resolved
native~/Runtime/src/UnityLifetime.h Outdated Show resolved Hide resolved
Runtime/Cesium3DTile.cs Show resolved Hide resolved
Runtime/CesiumTileExcluder.cs Show resolved Hide resolved
@kring kring changed the base branch from main to no-priority-inversion March 23, 2023 06:21
@kring
Copy link
Member Author

kring commented Mar 23, 2023

The example excluder gets less accurate when the tileset itself is transformed. For example, here the box collider is at (0, 0, 0), but the tileset has been shifted, and the excluded tiles are off:

This turned out to be a problem with the example CesiumBoxExcluder class, rather than the implementation in Cesium for Unity itself. I didn't realize that BoxCollider.Bounds returns the world space bounds, not the local bounds. Because Cesium3DTile.bounds always provides the bounds in the excluder's local frame, the example code was comparing bounds in mismatched frames. I changed the example to construct a local-space bounds instead, and now it works well. See the updated example in the original post above. I tested this by translating/rotation/scaling both the CesiumGeoreference and the tileset (and even both at the same time), and the excluder now works correctly in all cases.

@kring
Copy link
Member Author

kring commented Mar 23, 2023

If I try to invert the ShouldExclude condition in the example, nothing shows up.

This surprised me at first, too, but it's actually correct. If you invert the condition, any tile that intersects the box at all will be excluded. The root tile intersects the box, so it's excluded. No further tiles are considered.

If you want to exclude tiles that are completely inside the box, that's written as return this._box.bounds.CompletelyContains(tile.bounds);. Except Unity doesn't provide a CompletelyContains or similar method. But we could write one as an extension method. I think it would be something like this:

    public static bool CompletelyContains(this Bounds a, Bounds b)
    {
        return Vector3.Min(a.max, b.max) == b.max && Vector3.Max(a.min, b.min) == b.min;
    }

(Edit: My original version wasn't quite right, but I've confirmed the edited version above works)

@kring
Copy link
Member Author

kring commented Mar 23, 2023

One thing that doesn't work quite right is when the CesiumTileExcluder is not on the same GameObject as the Cesium3DTileset, and there is a non-identity transformation between them. For example, if the excluder is on the CesiumGeoreference (a reasonable thing to do!) and the tileset has an offset relative to the georeference (also fairly common). I'm working on it.

@kring
Copy link
Member Author

kring commented Mar 23, 2023

This depends on CesiumGS/cesium-native#613 now.

@kring
Copy link
Member Author

kring commented Mar 23, 2023

I think the major functionality is all in place now. I still need to fill in the missing docs and address your other comments. Thanks for the thorough review @j9liu!

Base automatically changed from no-priority-inversion to main March 23, 2023 17:12
@kring
Copy link
Member Author

kring commented Mar 24, 2023

I think I've addressed everything now. Let me know what you think, @j9liu!

Copy link
Contributor

@j9liu j9liu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kring These changes look great! Just one more issue: if I remove my excluder component from a tileset (I was moving components between the tileset and georeference in edit mode), it will still try to access it, resulting in this error:

image

Also, if anyone's interested, I made the excluder script interactive and responsive to changes:

public class CesiumBoxExcluder: CesiumTileExcluder
{
    private BoxCollider _boxCollider;
    private Bounds _bounds;

    public bool invert = false;

    protected void OnEnable()
    {
        this._boxCollider = this.gameObject.GetComponent<BoxCollider>();
        this._bounds = new Bounds(this._boxCollider.center, this._boxCollider.size);
    }

    protected void Update()
    {
        this._bounds.center = this._boxCollider.center;
        this._bounds.size = this._boxCollider.size;
    }

    public bool CompletelyContains(Bounds bounds)
    {
        return Vector3.Min(this._bounds.max, bounds.max) == bounds.max && 
               Vector3.Max(this._bounds.min, bounds.min) == bounds.min;
    }

    public override bool ShouldExclude(Cesium3DTile tile)
    {
        if (!this.enabled)
        {
            return false;
        }

        if (this.invert)
        {
            return this.CompletelyContains(tile.bounds);
        }

        return !this._bounds.Intersects(tile.bounds);
    }
}

@kring
Copy link
Member Author

kring commented Mar 27, 2023

Thanks @j9liu, I fixed that error when removing the excluder.

One remaining problem is that the excluder does not automatically take effect when you add it; you need to refresh the tileset first. I thought about changing CesiumTileExcluder.OnEnable to automatically refresh all Cesium3DTilesets in children, but that will often be redundant. I think the tileset will need to keep track of which excluders it knows about, and then CesiumTileExcluder.OnEnable can check itself against that list and only refresh the tileset if its missing.

@kring
Copy link
Member Author

kring commented Mar 30, 2023

@j9liu this is ready. Adding an excluder immediately affects the tilesets now. It works more or less the same way as raster overlays.

I also updated the top post in this PR with the latest version of the example CesiumBoxExcluder.

@j9liu
Copy link
Contributor

j9liu commented Mar 30, 2023

Looks great @kring ! I'll merge after CI passes.

@j9liu j9liu merged commit d079b03 into main Mar 31, 2023
@j9liu j9liu deleted the tile-excluder branch March 31, 2023 00:53
@JackMoljerc
Copy link

Great Job!! I give the component on Cesium World Terrain‘s parent, it works, but I still meet some troubles.The BoxCollider gameobject will move with the DynamicCamera at running time. How can I do for this?Maybe I miss something.
image
image

@JackMoljerc
Copy link

The BoxCollider‘s world postion didn’t move but the clipping plane move with camera moving.

@kring
Copy link
Member Author

kring commented Apr 27, 2023

Please post to the community forum rather than commenting on closed PRs.

The problem is caused by origin shifting, which keeps the origin of the Unity coordinate system near the camera in order to avoid vertex precision artifacts. The easiest solution is to disable origin shifting by removing the component from the DynamicCamera. This will be fine if you stay within a relatively small area.

@j9liu
Copy link
Contributor

j9liu commented Apr 27, 2023

@JackMoljerc If you don't want to disable origin shifting, you can also convert your desired position into ECEF or longitude / latitude coordinates. Then, in the Update() function of your excluder, continually update the center of the collider with the new Unity world position, derived from the initial ECEF position.

@JackMoljerc
Copy link

Please post to the community forum rather than commenting on closed PRs.

The problem is caused by origin shifting, which keeps the origin of the Unity coordinate system near the camera in order to avoid vertex precision artifacts. The easiest solution is to disable origin shifting by removing the component from the DynamicCamera. This will be fine if you stay within a relatively small area.

Next time, I promise.

@JonathanWiii
Copy link

@kring @j9liu Thanks for the great work! It was really easy to follow the steps!
However I could not find an Alpha Clipping setting in my CesiumClippingTilesetMaterial.
Does this work for URP? Could you tell which version of unity you where using?

So I followed every step (without the alpha clipping) Saved my unity project - but now every time I try and open it, it crashes immediately. - Maybe because of the missing alpha clipping?

Thanks for your help, appreciated!

@kring
Copy link
Member Author

kring commented Jun 2, 2023

@JonathanWiii I wouldn't expect the lack of alpha clipping to cause a crash. Do you have a call stack or any other details about it?

I'm using 2021.3 (patch release varies, and shouldn't matter I hope). URP is the pipeline I was using. The alpha clipping setting exists on both the Shader and Material. On the shader, it's here:
image

And on the Material, it's here:
image

@jolly17ify
Copy link

How to pass dynamic values instead of 500 that you have used in the example?

@anishpatel44
Copy link

anishpatel44 commented Jul 26, 2023

@jolly17ify You could create a Vector3 Parameter in the shader and then set the shader values. Then set the param to exposed

image image

@shijasck11
Copy link

Is there any way to make multiple tiles to exculde on the same world?

@kring
Copy link
Member Author

kring commented Jul 26, 2023

Is there any way to make multiple tiles to exculde on the same world?

Sure. You can put whatever logic you like in your CesiumTileExcluder-derived class and in your custom material.

@jolly17ify
Copy link

When I am using the shader to clip the part of a tile that is not required and changing the Vector3 parameter in the shader, it doesn't clip automatically and I have to always re-update the material using following:
cesiumModel.opaqueMaterial = cesiumClipMaterial;
This works but since the calculation happens for clipping again, it gives the feeling that the model was hidden for a frame and re-loaded. Is there any way we can do it in a way that the part of model whose visibility is unaffected by the change in the Vector3 parameter remains as it is and only the part that is affected is shown/hidden.

@kring
Copy link
Member Author

kring commented Aug 14, 2023

Just a reminder everyone: please post questions to the community forum, not here!

Setting the tileset's opaqueMaterial property will cause a refresh of the tileset. To avoid that, instead get the MeshRenderer components from the child game objects of the tileset and modify the material in each of their sharedMaterial properties.

@peterclemenko
Copy link

peterclemenko commented Mar 13, 2024

image
I'm trying to make a project anywhere style map and this is what i'm getting with the provided code and tutorial, is there a lead on the problem?

Or better yet can a material and code snippet premade be merged into cesium that works

@j9liu
Copy link
Contributor

j9liu commented Mar 14, 2024

Hi @peterclemenko,

Please direct questions like these to our community forum! From there, both the Cesium team and other community members will be able to help.

You may have already posted there, but I want to remind everyone to move these questions and discussions to the appropriate place. We reserve Github for feature requests and confirmed bug reports. Thank you all! 😄

@06simtech
Copy link

Hi,
It seems the CesiumClippingTilesetMaterial does not work on vision pro. So, Is there a way I can get this to work? Can you help me?
Thanks

@kring
Copy link
Member Author

kring commented Nov 6, 2024

@06simtech perhaps you missed the message above?

Just a reminder everyone: please post questions to the community forum, not here!

To answer your question, Vision Pro is not a supported platform at all. But if you post to the community forum with a lot more details - starting with how you've managed to run the plugin on Vision Pro! - then maybe we'll have some ideas.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants