Clearing or updating tileset cache without RecreateTileset()

Hi! I’m working on creating a realistic water wave effect for in my Cesium for Unity based application. I’m doing this by having a custom shader/material based of the CesiumDefaultTilesetShader, where the main addition is a Color mask layer that adds a wave time-offsetted normal map to parts of the tiles that represent water:

In the application the user is able to toggle this water effect on and off. When this effect gets enabled the bool property _enableWaveEffect is set to true in the material instance assign to the Cesium3DTileset Opaque Material property and all the existing submeshes are iterated through (as explained in this post: How to modify 3dtiles material parameters at runtime - #2 by Kevin_Ring):

    private void SetWaveEffectToMaterial(bool enable) {
        int value = enable ? 1 : 0;
        bingMapTileset.opaqueMaterial.SetInt("_enableWaveEffect", value);
        
        // Loop through instanced tiles and update them
        foreach (var mesh in bingMapTileset.GetComponentsInChildren<MeshRenderer>()) {
            mesh.material.SetInt("_enableWaveEffect", value);
        }
    }

This works mostly, but the problem I’ve observed is that some tiles can still appear on the map without its shader having been updated, and I suspect these are map tiles in the cache. This will then end up looking like this:

To avoid this I’ve tried solutions such as recreating the maptiles (RecreateTileset()) after updating the Opaque Material Property or simply setting the cache size to 0, but both of these solutions seems unideal. So my questions are as follows:

  1. Is this behavior indeed due to the maptiles in the cache?
  2. Are there any current way of making sure these are updated as well, or alternatively a way of clearing/resetting the cache without a call to recreateTileset?

Additional note/context:

There seems to be an issue where if you assign a normal map at compile time (i.e to the material or the shader itself through the editor) the normal map texture/asset is attempted to be deleted, which is forbidden by Unity, often resulting in 999+ exception log messages in Unity:

Destroying assets is not permitted to avoid data loss.
If you really want to remove an asset use DestroyImmediate (theObject, true);
UnityEngine.Object:Destroy (UnityEngine.Object)
Reinterop.ReinteropInitializer:UnityEngine_Object_CallDestroy_x7aQMuJcEpatC9689ghI4A (intptr) (at ./Library/PackageCache/com.cesium.unity@1.7.1/Runtime/generated/Reinterop/Reinterop.RoslynSourceGenerator/ReinteropInitializer.cs:40164)
CesiumForUnity.Cesium3DTileset:OnDisable () (at ./Library/PackageCache/com.cesium.unity@1.7.1/Runtime/generated/Reinterop/Reinterop.RoslynSourceGenerator/Cesium3DTileset-generated.cs:582)

or

Destroying assets is not permitted to avoid data loss.
If you really want to remove an asset use DestroyImmediate (theObject, true);
UnityEngine.Object:DestroyImmediate (UnityEngine.Object)
Reinterop.ReinteropInitializer:UnityEngine_Object_CallDestroyImmediate_x7aQMuJcEpatC9689ghI4A (intptr) (at ./Library/PackageCache/com.cesium.unity@1.7.1/Runtime/generated/Reinterop/Reinterop.RoslynSourceGenerator/ReinteropInitializer.cs:40180)
CesiumForUnity.Cesium3DTileset:OnDisable () (at ./Library/PackageCache/com.cesium.unity@1.7.1/Runtime/generated/Reinterop/Reinterop.RoslynSourceGenerator/Cesium3DTileset-generated.cs:582)

My workaround for this issue was to create a script that creates a runtime copy of the normal maps, assigns those instead and then check every frame whether those were removed from the source material and conditionally recopies and reassigns:

using UnityEngine;
using UnityEngine.UI;
using CesiumForUnity;

public class CesiumMaterialHandling : MonoBehaviour
{
    public Cesium3DTileset bingMapTileset;
    public Material customCesiumMaterial; // Contains extension shader functionaity such as water effect
    public bool waveEffectEnabled;
    public Texture2D waveNormalSourceX;
    public Texture2D waveNormalSourceY;
    private Texture2D waveNormalCopyX;
    private Texture2D waveNormalCopyY;

    void Awake() {
        bingMapTileset.opaqueMaterial = customCesiumMaterial;
        CopyWaveNormals();
        AssignWaveNormals();
        SetWaveEffectToMaterial(waveEffectEnabled);
    }

    void Update() {
        if (waveEffectEnabled && (waveNormalCopyX == null || waveNormalCopyY == null)) {
            CopyWaveNormals();
            AssignWaveNormals();
            SetWaveEffectToMaterial(waveEffectEnabled);
        }
    }

    private void CopyWaveNormals() {
        waveNormalCopyX = new Texture2D(waveNormalSourceX.width, waveNormalSourceX.height, waveNormalSourceX.format, true);
        waveNormalCopyY = new Texture2D(waveNormalSourceY.width, waveNormalSourceY.height, waveNormalSourceY.format, true);
        waveNormalCopyX.wrapMode = TextureWrapMode.Repeat; waveNormalCopyY.wrapMode = TextureWrapMode.Repeat;
        waveNormalCopyX.filterMode = FilterMode.Bilinear; waveNormalCopyY.filterMode = FilterMode.Bilinear;
        Graphics.CopyTexture(waveNormalSourceX, waveNormalCopyX);
        Graphics.CopyTexture(waveNormalSourceY, waveNormalCopyY);
    }

    private void AssignWaveNormals() {
        bingMapTileset.opaqueMaterial.SetTexture("_waveNormalOne", waveNormalCopyX);
        bingMapTileset.opaqueMaterial.SetTexture("_waveNormalTwo", waveNormalCopyY);
        
        // Loop through instanced tiles and update them
        foreach (var mesh in bingMapTileset.GetComponentsInChildren<MeshRenderer>()) {
            mesh.material.SetTexture("_waveNormalOne", waveNormalCopyX);
            mesh.material.SetTexture("_waveNormalTwo", waveNormalCopyY);
        }
    }

    private void SetWaveEffectToMaterial(bool enable) {
        int value = enable ? 1 : 0;
        bingMapTileset.opaqueMaterial.SetInt("_enableWaveEffect", value);
        
        // Loop through instanced tiles and update them
        foreach (var mesh in bingMapTileset.GetComponentsInChildren<MeshRenderer>()) {
            mesh.material.SetInt("_enableWaveEffect", value);
        }
    }

But with this script or without (i.e accepting the error messages from Unity) the issue described above occurs unfortunately.

Hi @Andreas4Pro,

For your original question, have you tried using the GetComponentsInChildren(bool includeInactive) overload? When true, the function will include inactive game objects in its search. This should account for tiles that have been hidden by the plugin.

For your follow-up question, this is unfortunately a known bug with the plugin, and we have an issue on Github to track it here. This happens because Cesium for Unity tries to free the texture memory that is instantiated with every material instance, and it (seemingly incorrectly) assumes that the textures are copies of the original instance. I’m glad you were able to develop a workaround, though!

Also, just wanted to say that your water shader looks great! :smiley:

Hi @janine,

First and foremost, thank you for your quick response and for all the great work you folks are doing (including the numerous helpful responses I’ve read on this forum in the past)!

Interesting. I hadn’t seen that issue thread before, but it makes sense that it’s been encountered by others. Like advised in the Github thread I also ended up removing the green1x1 texture to avoid that issue, but then reencountered it again when working on the water wave shader.

Thanks for the tip on the includeInactive overload! I hadn’t considered that cached tiles were already instantiated as GameObject with meshes, but of course that makes sense! I tested it out and it partially fixed the issue (i.e all tiles were correctly updated), however it did lead to a sort of flashing effect where the code from the script I posted above and the logic around cleaning up textures would rapidly overwrite/fight each other resulting in the water areas occasionally flashing between the a material variant with and without the normal texture.

However, after fiddling a while with that solution I got an idea that ended up working better for me, and that I think is a bit more elegant: Using the OnTileGameObjectCreated event to dynamically create the required texture copies for each tile instance. Here is the code I wrote for doing this:

using UnityEngine;
using UnityEngine.UI;
using CesiumForUnity;

public class CesiumMaterialHandling : MonoBehaviour {
    public Cesium3DTileset bingMapTileset;
    public Texture2D waveNormalSourceX;
    public Texture2D waveNormalSourceY;
    public bool waveEffectEnabled;

    void Awake() {
        bingMapTileset.OnTileGameObjectCreated += OnTileGameObjectCreated;
    }

    private void OnTileGameObjectCreated(GameObject tile) {
        var (waveNormalCopyX, waveNormalCopyY) = CopyWaveNormals();
        MeshRenderer mesh = tile.GetComponentInChildren<MeshRenderer>(true);
        mesh.material.SetTexture("_waveNormalOne", waveNormalCopyX);
        mesh.material.SetTexture("_waveNormalTwo", waveNormalCopyY);
        mesh.material.SetInt("_enableWaveEffect", waveEffectEnabled ? 1 : 0);
    }

    private (Texture2D, Texture2D) CopyWaveNormals() {
        // Create a per-tile copy of the X and Y normal textures
        Texture2D waveNormalCopyX = new Texture2D(waveNormalSourceX.width, waveNormalSourceX.height, waveNormalSourceX.format, false);
        Texture2D waveNormalCopyY = new Texture2D(waveNormalSourceY.width, waveNormalSourceY.height, waveNormalSourceY.format, false);
        Graphics.CopyTexture(waveNormalSourceX, waveNormalCopyX);
        Graphics.CopyTexture(waveNormalSourceY, waveNormalCopyY);
        return (waveNormalCopyX, waveNormalCopyY);
    }

    private void SetWaveEffectToMaterial(bool enable) {
        // Update material and loop through the existing tiles' mesh instances and update them
        bingMapTileset.opaqueMaterial.SetInt("_enableWaveEffect", enable ? 1 : 0);
        foreach (var mesh in bingMapTileset.GetComponentsInChildren<MeshRenderer>(true)) {
            mesh.material.SetInt("_enableWaveEffect", enable ? 1 : 0);
        }
    }

    public void OnToggleWaveEffect(Toggle toggle) { 
        waveEffectEnabled = toggle.isOn;
        SetWaveEffectToMaterial(toggle.isOn);
    }
    
    void OnDestroy() {
        bingMapTileset.OnTileGameObjectCreated -= OnTileGameObjectCreated;
    }
}

This solution has worked very well for me so far, and should in principle work for any issue encountered in the github issue thread. It’s a bit costly on the memory side, but measures such as e.g turning off mipmap generation on the normal textures seems to greatly offset this.

And thanks, happy to hear that! :blush: I look forward to work more with Cesium for Unity and expore the material side of things (e.g vertex displace all sea-land boarders, so please let me know if you have any insights on that!) :smile:

Hi @Andreas4Pro,

Thanks for sharing your solution! This is a helpful workaround for community members who are working with custom materials / :smile:

(e.g vertex displace all sea-land boarders, so please let me know if you have any insights on that!)

Hm, I don’t have many ideas at the top of my head at the moment. The displacement effect would require vertices to know whether or not they are along a border, which isn’t easy to derive at the vertex shader stage.

Maybe as tiles load, you can pre-process their textures, making a new “depth” texture that is 1-to-1 with the original one. Not sure what algorithm is best for detecting changes between water and land-colored pixels, but as a very simple start, you could assign lower height to blue pixels. Each pixel would store that “difference” value, and use it to displace vertices in the vertex shader. I haven’t tried this myself, though. Just throwing around ideas. :sweat_smile:

Hi @janine !

Yes exactly! It seems to be a bit more complicated to do this during the vertex stage, but I think I’ll attempt something akin to what you said and see how it goes. Thanks again for all the help! :smile:

能分享下你的CesiumDefaultTilesetShader水着色器吗?感谢

How do you handle the Color mask layer if a tile contains both water and non water parts, such as the coastline?