Sync texture over network using netcode

Hi all, I am a newer in netcode and currently struggling in sending/receiving Texture(1024*1024) over network for few days.
I need to update the texture from local client to server when they first connected and share the new texture to each connected clients, I’ve tried by Rpc and custom messaging(seems this can send bigger package), but still get the same error even setting Max Payload Size to 500000:
OverflowException: Writing past the end of the buffer

public void SendTexture()
{
   
    Texture clientTex = gameObject.GetComponentInChildren<Renderer>().material.mainTexture;
    byte[] clientTex2Bytes = ConvertTexture2DToBytes(matchShapesTest.TextureToTexture2D(clientTex));
    Debug.Log($"client {NetworkManager.LocalClientId} spawn with tex2bytes length {clientTex2Bytes.Length}");
    PrintByteArray(clientTex2Bytes);
    var msgManager = NetworkManager.Singleton.CustomMessagingManager;
    var writer = new FastBufferWriter(FastBufferWriter.GetWriteSize(clientTex2Bytes), Allocator.Temp);

    using (writer)
    {
        writer.WriteValueSafe(clientTex2Bytes);
        msgManager.SendNamedMessage("UpateTexture", NetworkManager.ServerClientId, writer, NetworkDelivery.Reliable);
    }
}

public void UpdateTexture(ulong senderId, FastBufferReader payload)
{
    payload.ReadValueSafe(out byte[] bytes);
    Debug.Log($"receive {bytes.Length} from {senderId}");
    PrintByteArray(bytes);
    Texture2D tex2d = ConvertBytesToTexture2D(bytes);
}

One more question, the network object texture(default) that default using is showing different size on server and local client, why?
(server)
9509620--1340050--upload_2023-12-4_14-52-53.png
(local client)
9509620--1340053--upload_2023-12-4_14-53-39.png
Is there any way that I can reduce the texture byte array or I just having wrong settings?
Can anyone help?

Oh, the error end by changing NetworkDelivery.Reliable to NetworkDelivery.ReliableFragmentedSequenced, but I still cannot see received output on server side.

public override void OnNetworkSpawn()
{
    base.OnNetworkSpawn();
    NetworkManager.Singleton.CustomMessagingManager.RegisterNamedMessageHandler("UpdateTexture", UpdateTexture);
}

For one, it‘s best to send the original image. It‘llbe a png or jpg and thus much smaller than the texture bytes. Next, the texture bytes require a specific texture format but a client‘s hardware may not support that specific format. Again sending the original image‘s bytes is preferred as it provides you with all the options of loading the image and using it for various purposes.

To send large chunks you could manually split the data into xx kb chunks with staggered ROC messages. Or use a file hosting service and only share the download url. A lot easier!

If all builds of the game are aware of the texture, you don’t need to send it. Just send an ID, and use it to retrieve the texture from the local files.

1 Like

What RikuTheFuffs-U points out, unless you are capturing a picture via webcam or the like it would be much easier to create an ID for the texture. You can actually look at how we generate the NetworkObjectIdHash value to get an idea of how you could go about doing this.

Hi all, thanks for answering my question.
Sorry for not explained my case clearly. Actually I am working on sync texture which is captured via device camera, every client should have their unique texture when they start connection.
Currently, server can get the new texture when client connected, but I am struggling on how to synchronize to other existing and late join clients.
Client update their own texture to server when they connected, server share new received texture to other clients (by custom messaging). Then I need a list to store and sync texture to each client (seems NetworkList is the only way to go).
I am working on NetworkList with struct which contains clientId and texture byte array, but stuck on error below:
Error CS8377 The type ‘DisplayPattern.ClientPatternData’ must be a non-nullable value type, along with all fields at any level of nesting, in order to use it as parameter ‘T’ in the generic type or method ‘NetworkList’

public NetworkList<ClientPatternData> connectClients = new NetworkList<ClientPatternData>();

public struct ClientPatternData
{
    public ulong clientId;
    public byte[] textureBytes;
    public bool isUpdated;
   
    public ClientPatternData(ulong id, byte[] bytes, bool isupdated)
    {
        clientId = id;
        textureBytes = bytes;
        isUpdated = isupdated;
    }

    public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
    {
        serializer.SerializeValue(ref clientId);
        serializer.SerializeValue(ref textureBytes);
        serializer.SerializeValue(ref isUpdated);
    }
}

Actually I am not sure if this is a good way to go, please give me some advice…qq

Okay, I can now have the new texture on both sides.
I have a local dictionary connectedClients to record income textures and another dictionary syncedClients to record texture update status. serverRpc call by client to change texture on gameobject when client received new updated texture from server, but still cannot sync texture on clients.

private Dictionary<ulong, byte[]> connectedClients = new Dictionary<ulong, byte[]>();
private Dictionary<ulong, bool> syncedClients = new Dictionary<ulong, bool>();

[ServerRpc(RequireOwnership = false)]
public void SetTextureServerRpc(ServerRpcParams serverRpcParams = default)
{
    ulong senderId = serverRpcParams.Receive.SenderClientId;
    Debug.Log("ServerRpc called by " + senderId);
    if (NetworkManager.ConnectedClients.ContainsKey(senderId))
    {
        //sync texture for sender client
        foreach(var syncClient in syncedClients)
        {
            //if not synced on client yet
            if(syncClient.Value == false && NetworkManager.Singleton.ConnectedClients.TryGetValue(syncClient.Key, out NetworkClient clientObject))
            {
                NetworkObject obj = clientObject.PlayerObject;
                if (connectedClients.TryGetValue(syncClient.Key, out byte[] receivedBytes))
                {
                    //clientTexture.Value = receivedBytes;
                    Debug.Log($"sync {syncClient.Key} owned object on {senderId}");
                    Texture2D tex2d = ConvertBytesToTexture2D(receivedBytes);
                    obj.GetComponentInChildren<Renderer>().material.mainTexture = tex2d;
                    syncedClients[syncClient.Key] = true;
                }
                else
                {
                    Debug.LogError($"{syncClient.Key} texture not found");
                }
            }
        }
    }
  
}

Actually I don’t know how NetworkVariable work on my case…

Custom messages seems to be the solution

Here is an example that I have made, It could be enough to get you started.

It does need the StandaloneFileBrowser.

But you can find the file in some other way.

using SFB; //Standard File browser.
using System.Collections; // Required for using IEnumerator
using Unity.Collections; // Required for FastBufferWriter and FastBufferReader
using Unity.Netcode; // Required for networking functionalities
using UnityEngine; // Required for Unity engine functionalities
using UnityEngine.Networking; // Required for UnityWebRequest
using UnityEngine.UI; // Required for Image component

public class LoadTexture : NetworkBehaviour // Inherits from NetworkBehaviour for networking functionalities
{
    private byte[] data; // Byte array to hold the texture data
    private int dataSize
    {
        get
        {
            return data.Length;
        }
    }

    void Start()
    {
        NetworkManager.Singleton.CustomMessagingManager.OnUnnamedMessage += OnUnnamedMessage; // Register OnUnnamedMessage callback
    }

    void Update()
    {
        if (!IsOwner) // Check if the current instance is the owner
        {
            return; // Exit if not the owner
        }
        if (Input.GetKeyDown(KeyCode.T)) // Check if the 'T' key is pressed
        {
            StartCoroutine(GetText()); // Start the coroutine to get the texture
        }
        if (Input.GetKeyDown(KeyCode.G)) // Check if the 'G' key is pressed
        {
            //Send message to everyone who is not this client.
            //BEWARE: non server clients cannot send to other non Server clients.
            foreach (var id in NetworkManager.ConnectedClientsIds)
            {
                if (NetworkManager.LocalClientId == id)
                {
                    continue;
                }
                SendData(id, data);
            }

        }
    }

    IEnumerator GetText()
    {

        var paths = StandaloneFileBrowser.OpenFilePanel("Open File", "", "", false);
        if (paths.Length == 0)
        {
            yield break;
        }
        // Request texture from the file path
        using (UnityWebRequest uwr = UnityWebRequestTexture.GetTexture(paths[0]))
        {
            yield return uwr.SendWebRequest(); // Wait for the request to complete

            if (uwr.result != UnityWebRequest.Result.Success) // Check if the request failed
            {
                Debug.Log(uwr.error); // Log the error;
            }
            else
            {
                var texture = DownloadHandlerTexture.GetContent(uwr); // Get the texture from the response
                SetTextureOnGameObject(texture);

                // Convert texture to byte array
                data = texture.EncodeToPNG(); // Encode the texture to PNG format
            }
        }
    }

    private void SetTextureOnGameObject(Texture2D texture)
    {
        Renderer renderer = GetComponent<Renderer>(); // Get the Renderer component
        if (renderer != null)
        {
            renderer.material.mainTexture = texture; // Set the texture to the material
        }
        else
        {
            Image image = GetComponent<Image>(); // Get the Image component
            if (image != null)
            {
                // Create a new sprite from the texture
                Sprite sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f));
                image.sprite = sprite; // Set the sprite to the Image component
            }
            else
            {
                Debug.LogError("No Renderer or Image component found on the GameObject."); // Log error if no Renderer or Image found
            }
        }
    }

    private void OnUnnamedMessage(ulong clientId, FastBufferReader reader)
    {
        int dataLength = reader.Length; // Get the length of the received data
        Debug.Log("OnUnnamedMessage reader length: " + dataLength); // Log the length of the received data

        data = new byte[dataLength]; // Initialize the data array with the received data length
        reader.ReadBytesSafe(ref data, dataLength); // Read the bytes from the reader into the data array

        // Recreate the texture from the byte array
        Texture2D texture = new Texture2D(2, 2); // Create a new texture with default size
        texture.LoadImage(data); // Load the texture from the byte array

        SetTextureOnGameObject(texture);
    }

    private void SendData(ulong receivingClientId, byte[] data)
    {
        var writer = new FastBufferWriter(dataSize, Allocator.Temp); // Create a new FastBufferWriter with the data size
        var customMessagingManager = NetworkManager.Singleton.CustomMessagingManager; // Get the CustomMessagingManager

        using (writer)
        {
            writer.WriteBytesSafe(data); // Write the data bytes to the writer
                                         // Send the data to the connected client with ReliableFragmentedSequenced delivery
            customMessagingManager.SendUnnamedMessage(receivingClientId, writer, NetworkDelivery.ReliableFragmentedSequenced);
        }
    }
}