Unity3D, построить PNG из панели Unity.UI?

Подумайте о любом Unity.UI Canvas, который у вас может быть.

Представьте типичный Panel на этом холсте. Скажем, он содержит какие-то изображения, может быть текст и так далее.

Было бы очень удобно, если бы вы могли превратить эту панель (только панель) в скриншот: Texture2D или PNG.

Единственное, о чем я могу думать, это просто использовать ReadPixels и вычислить область рассматриваемого Panel на экране (и на самом деле это довольно сложно); и это работает только в том случае, если панель квадратная и не повернута под углом.

Вы думаете, что должен быть способ отрисовать одну панель или, по крайней мере, один холст целиком? Я ничего не могу найти.

введите описание изображения здесь

введите описание изображения здесь

В примере сделайте розовую панель изображением PNG. Ой.

(Очевидно, что если у кого-то есть решение, которое делает просто «весь холст», а не одну панель, конечно, даже это замечательно.)


person Fattie    schedule 11.04.2016    source источник
comment
Это на самом деле звучит сложно. Сделать скриншот — это одно, а сделать скриншот панели — совсем другое. Когда вы говорите о снимке экрана панели, вы имеете в виду пользовательский интерфейс, который является дочерним элементом панели?   -  person Programmer    schedule 11.04.2016
comment
Вы имеете в виду весь пользовательский интерфейс на панели?   -  person Programmer    schedule 11.04.2016
comment
Справа — панель, то есть CanvasRenderer, в просторечии известная как «панель», когда вы создаете ее с помощью меню или контекстного меню create -> UI .... изображение добавлено!   -  person Fattie    schedule 11.04.2016
comment
Ох, хорошо. Я посмотрю, смогу ли я справиться с этим.   -  person Programmer    schedule 11.04.2016
comment
Только что опубликовал свое решение с рабочим примером. Он выполняет свою работу. Я трачу время на устранение ошибок. Если вы обнаружите ошибку, вы можете исправить и обновить мой ответ. Теперь ваша задача - оптимизировать ....   -  person Programmer    schedule 12.04.2016
comment
@Programmer невероятно, сейчас проверяю!!!   -  person Fattie    schedule 13.04.2016


Ответы (3)


Код ниже может сфотографировать Canvas. Canvas должен быть прикреплен к объекту, который вы передаете в него. Единственная функция, которую нужно вызвать, это void takeScreenShot(Canvas canvasPanel, SCREENSHOT_TYPE screenShotType = SCREENSHOT_TYPE.IMAGE_AND_TEXT, bool createNewInstance = true)

Параметр SCREENSHOT_TYPE.IMAGE_AND_TEXT будет делать снимки изображений и текстов.

Параметр SCREENSHOT_TYPE.IMAGE_ONLY будет снимать только изображения. Каждый текст на экране будет исключен. Вы можете использовать это из соображений безопасности, чтобы удалить текст и просто показать только графику.

Параметр SCREENSHOT_TYPE.TEXT_ONLY будет фотографировать только тексты.

Как пользоваться. Создайте GameObject, прикрепите к нему скрипт CanvasScreenShot. Подпишитесь на CanvasScreenShot.OnPictureTaken(byte[] pngArray);, затем позвоните screenShot.takeScreenShot(canvasToSreenShot, SCREENSHOT_TYPE.IMAGE_AND_TEXT, false);

Полный код:

Ваш test.cs скрипт:

public class test : MonoBehaviour
{
    public Canvas canvasToSreenShot;

    // Use this for initialization
    void Start()
    {
        //Subscribe
        CanvasScreenShot.OnPictureTaken += receivePNGScreenShot;
        CanvasScreenShot screenShot = GameObject.Find("GameObject").GetComponent<CanvasScreenShot>();

        //take ScreenShot(Image and Text)
        //screenShot.takeScreenShot(canvasToSreenShot, SCREENSHOT_TYPE.IMAGE_AND_TEXT, false);
        //take ScreenShot(Image only)
        screenShot.takeScreenShot(canvasToSreenShot, SCREENSHOT_TYPE.IMAGE_ONLY, false);
        //take ScreenShot(Text only)
        // screenShot.takeScreenShot(canvasToSreenShot, SCREENSHOT_TYPE.TEXT_ONLY, false);

    }

    public void OnEnable()
    {
        //Un-Subscribe
        CanvasScreenShot.OnPictureTaken -= receivePNGScreenShot;
    }

    void receivePNGScreenShot(byte[] pngArray)
    {
        Debug.Log("Picture taken");

        //Do Something With the Image (Save)
        string path = Application.persistentDataPath + "/CanvasScreenShot.png";
        System.IO.File.WriteAllBytes(path, pngArray);
        Debug.Log(path);
    }

}

Скрипт CanvasScreenShot.cs:

public class CanvasScreenShot : MonoBehaviour
{
    /*
 CanvasScreenShot by programmer.
 http://stackoverflow.com/questions/36555521/unity3d-build-png-from-panel-of-a-unity-ui#36555521
 http://stackoverflow.com/users/3785314/programmer
 */

    //Events
    public delegate void takePictureHandler(byte[] pngArray);
    public static event takePictureHandler OnPictureTaken;

    private GameObject duplicatedTargetUI;
    private Image[] allImages;
    private Text[] allTexts;

    //Store all other canvas that will be disabled and re-anabled after screenShot
    private Canvas[] allOtherCanvas;

    //takes Screenshot
    public void takeScreenShot(Canvas canvasPanel, SCREENSHOT_TYPE screenShotType = SCREENSHOT_TYPE.IMAGE_AND_TEXT, bool createNewInstance = true)
    {
        StartCoroutine(_takeScreenShot(canvasPanel, screenShotType, createNewInstance));
    }

    private IEnumerator _takeScreenShot(Canvas canvasPanel, SCREENSHOT_TYPE screenShotType = SCREENSHOT_TYPE.IMAGE_AND_TEXT, bool createNewInstance = true)
    {
        //Get Visible Canvas In the Scene
        allOtherCanvas = getAllCanvasInScene(false);

        //Hide all the other Visible Canvas except the one that is passed in as parameter(Canvas we want to take Picture of)
        showCanvasExcept(allOtherCanvas, canvasPanel, false);
        //Reset the position so that both UI will be in the-same place if we make the duplicate a child
        resetPosAndRot(gameObject);

        //Check if we should operate on the original image or make a duplicate of it
        if (createNewInstance)
        {
            //Duplicate the Canvas we want to take Picture of
            duplicatedTargetUI = duplicateUI(canvasPanel.gameObject, "ScreenShotUI");
            //Make this game object the parent of the Canvas
            duplicatedTargetUI.transform.SetParent(gameObject.transform);

            //Hide the orginal Canvas we want to take Picture of
            showCanvas(canvasPanel, false);
        }
        else
        {
            //No duplicate. Use original GameObject
            //Make this game object the parent of the Canvas
            canvasPanel.transform.SetParent(gameObject.transform);
        }

        RenderMode defaultRenderMode;

        //Change the duplicated Canvas to RenderMode to overlay
        Canvas duplicatedCanvas = null;
        if (createNewInstance)
        {
            duplicatedCanvas = duplicatedTargetUI.GetComponent<Canvas>();
            defaultRenderMode = duplicatedCanvas.renderMode;
            duplicatedCanvas.renderMode = RenderMode.ScreenSpaceOverlay;
        }
        else
        {
            defaultRenderMode = canvasPanel.renderMode;
            canvasPanel.renderMode = RenderMode.ScreenSpaceOverlay;
        }


        if (screenShotType == SCREENSHOT_TYPE.IMAGE_AND_TEXT)
        {
            //No Action Needed
        }
        else if (screenShotType == SCREENSHOT_TYPE.IMAGE_ONLY)
        {
            if (createNewInstance)
            {
                //Get all images on the duplicated visible Canvas
                allTexts = getAllTextsFromCanvas(duplicatedTargetUI, false);
                //Hide those images
                showTexts(allTexts, false);
            }
            else
            {
                //Get all images on the duplicated visible Canvas
                allTexts = getAllTextsFromCanvas(canvasPanel.gameObject, false);
                //Hide those images
                showTexts(allTexts, false);
            }
        }
        else if (screenShotType == SCREENSHOT_TYPE.TEXT_ONLY)
        {
            if (createNewInstance)
            {
                //Get all images on the duplicated visible Canvas
                allImages = getAllImagesFromCanvas(duplicatedTargetUI, false);
                //Hide those images
                showImages(allImages, false);
            }
            else
            {
                //Get all images on the duplicated visible Canvas
                allImages = getAllImagesFromCanvas(canvasPanel.gameObject, false);
                //Hide those images
                showImages(allImages, false);
            }
        }

        //////////////////////////////////////Finally Take ScreenShot///////////////////////////////
        yield return new WaitForEndOfFrame();
        Texture2D screenImage = new Texture2D(Screen.width, Screen.height);
        //Get Image from screen
        screenImage.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0);
        screenImage.Apply();

        //Convert to png
        byte[] pngBytes = screenImage.EncodeToPNG();

        /*FOR TESTING/DEBUGGING PURPOSES ONLY. COMMENT THIS
        string path = Application.persistentDataPath + "/CanvasScreenShot.png";
        System.IO.File.WriteAllBytes(path, pngBytes);
        Debug.Log(path);*/

        //Notify functions that are subscribed to this event that picture is taken then pass in image bytes as png
        if (OnPictureTaken != null)
        {
            OnPictureTaken(pngBytes);
        }


        ///////////////////////////////////RE-ENABLE OBJECTS

        //Change the duplicated Canvas RenderMode back to default Value
        if (createNewInstance)
        {
            duplicatedCanvas.renderMode = defaultRenderMode;
        }
        else
        {
            canvasPanel.renderMode = defaultRenderMode;
        }
        //Un-Hide all the other Visible Canvas except the one that is passed in as parameter(Canvas we want to take Picture of)
        showCanvas(allOtherCanvas, true);
        if (screenShotType == SCREENSHOT_TYPE.IMAGE_AND_TEXT)
        {
            //No Action Needed
        }
        else if (screenShotType == SCREENSHOT_TYPE.IMAGE_ONLY)
        {
            //Un-Hide those images
            showTexts(allTexts, true);
        }
        else if (screenShotType == SCREENSHOT_TYPE.TEXT_ONLY)
        {
            //Un-Hide those images
            showImages(allImages, true);
        }

        //Un-hide the orginal Canvas we want to take Picture of
        showCanvas(canvasPanel, true);

        if (createNewInstance)
        {
            //Destroy the duplicated GameObject
            Destroy(duplicatedTargetUI, 1f);
        }
        else
        {
            //Remove the Canvas as parent 
            canvasPanel.transform.SetParent(null);
        }
    }

    private GameObject duplicateUI(GameObject parentUICanvasOrPanel, string newOBjectName)
    {
        GameObject tempObj = Instantiate(parentUICanvasOrPanel);
        tempObj.name = newOBjectName;
        return tempObj;
    }


    private Image[] getAllImagesFromCanvas(GameObject canvasParentGameObject, bool findDisabledCanvas = false)
    {
        Image[] tempImg = canvasParentGameObject.GetComponentsInChildren<Image>(findDisabledCanvas);
        if (findDisabledCanvas)
        {
            return tempImg;
        }
        else
        {
            System.Collections.Generic.List<Image> canvasList = new System.Collections.Generic.List<Image>();
            for (int i = 0; i < tempImg.Length; i++)
            {
                if (tempImg[i].enabled)
                {
                    canvasList.Add(tempImg[i]);
                }
            }
            return canvasList.ToArray();
        }
    }

    private Text[] getAllTextsFromCanvas(GameObject canvasParentGameObject, bool findDisabledCanvas = false)
    {
        Text[] tempImg = canvasParentGameObject.GetComponentsInChildren<Text>(findDisabledCanvas);
        if (findDisabledCanvas)
        {
            return tempImg;
        }
        else
        {
            System.Collections.Generic.List<Text> canvasList = new System.Collections.Generic.List<Text>();
            for (int i = 0; i < tempImg.Length; i++)
            {
                if (tempImg[i].enabled)
                {
                    canvasList.Add(tempImg[i]);
                }
            }
            return canvasList.ToArray();
        }
    }

    private Canvas[] getAllCanvasFromCanvas(Canvas canvasParentGameObject, bool findDisabledCanvas = false)
    {
        Canvas[] tempImg = canvasParentGameObject.GetComponentsInChildren<Canvas>(findDisabledCanvas);
        if (findDisabledCanvas)
        {
            return tempImg;
        }
        else
        {
            System.Collections.Generic.List<Canvas> canvasList = new System.Collections.Generic.List<Canvas>();
            for (int i = 0; i < tempImg.Length; i++)
            {
                if (tempImg[i].enabled)
                {
                    canvasList.Add(tempImg[i]);
                }
            }
            return canvasList.ToArray();
        }
    }

    //Find Canvas.
    private Canvas[] getAllCanvasInScene(bool findDisabledCanvas = false)
    {
        Canvas[] tempCanvas = GameObject.FindObjectsOfType<Canvas>();
        if (findDisabledCanvas)
        {
            return tempCanvas;
        }
        else
        {
            System.Collections.Generic.List<Canvas> canvasList = new System.Collections.Generic.List<Canvas>();
            for (int i = 0; i < tempCanvas.Length; i++)
            {
                if (tempCanvas[i].enabled)
                {
                    canvasList.Add(tempCanvas[i]);
                }
            }
            return canvasList.ToArray();
        }
    }

    //Disable/Enable Images
    private void showImages(Image[] imagesToDisable, bool enableImage = true)
    {
        for (int i = 0; i < imagesToDisable.Length; i++)
        {
            imagesToDisable[i].enabled = enableImage;
        }
    }

    //Disable/Enable Texts
    private void showTexts(Text[] imagesToDisable, bool enableTexts = true)
    {
        for (int i = 0; i < imagesToDisable.Length; i++)
        {
            imagesToDisable[i].enabled = enableTexts;
        }
    }


    //Disable/Enable Canvas
    private void showCanvas(Canvas[] canvasToDisable, bool enableCanvas = true)
    {
        for (int i = 0; i < canvasToDisable.Length; i++)
        {
            canvasToDisable[i].enabled = enableCanvas;
        }
    }


    //Disable/Enable one canvas
    private void showCanvas(Canvas canvasToDisable, bool enableCanvas = true)
    {
        canvasToDisable.enabled = enableCanvas;
    }

    //Disable/Enable Canvas Except
    private void showCanvasExcept(Canvas[] canvasToDisable, Canvas ignoreCanvas, bool enableCanvas = true)
    {
        for (int i = 0; i < canvasToDisable.Length; i++)
        {
            if (!(canvasToDisable[i] == ignoreCanvas))
            {
                canvasToDisable[i].enabled = enableCanvas;
            }
        }
    }

    //Disable/Enable Canvas Except
    private void showCanvasExcept(Canvas[] canvasToDisable, Canvas[] ignoreCanvas, bool enableCanvas = true)
    {
        for (int i = 0; i < canvasToDisable.Length; i++)
        {
            for (int j = 0; j < ignoreCanvas.Length; j++)
            {
                if (!(canvasToDisable[i] == ignoreCanvas[j]))
                {
                    canvasToDisable[i].enabled = enableCanvas;
                }
            }
        }
    }

    //Reset Position
    private void resetPosAndRot(GameObject posToReset)
    {
        posToReset.transform.position = Vector3.zero;
        posToReset.transform.rotation = Quaternion.Euler(Vector3.zero);
    }

}

public enum SCREENSHOT_TYPE
{
    IMAGE_AND_TEXT, IMAGE_ONLY, TEXT_ONLY
}
person Programmer    schedule 12.04.2016
comment
Когда вы закончите тестирование, вы сообщите мне, если возникнут какие-либо проблемы. - person Programmer; 13.04.2016
comment
это, кажется, отлично работает. Я не думаю, что что-то еще можно просить! отличный материал! - person Fattie; 13.04.2016
comment
Рад, что смог помочь. Говоря о вашем комментарии о возвращении старого кода, его больше нет. Я выбрал его и удалил все из них и начал заново с приведенным выше кодом, после того как я подумал, что производительность является требованием. Я не думаю, что когда-нибудь смогу вернуть его, потому что я закрыл визуальную студию и снова открыл Это. Отмена пропала..... - person Programmer; 13.04.2016
comment
Я знаю, что это действительно старо, но вот - поскольку вы передаете весь холст сценарию, как он сохраняет только определенную часть холста в изображение? Кроме того, я попробовал этот скрипт, и на сохраненном снимке экрана также есть спрайт, который является частью сцены, но не находится на холсте. Я использую единство 5.3.2. Любая помощь действительно приветствуется, так как я долго ломал голову над этим. - person TheDevilsFire; 23.04.2016
comment
@TheDevilsFire Это не ошибка. Когда я писал это, я имел в виду только исключение другого холста пользовательского интерфейса. Я не делал этого, чтобы исключить спрайты или 3D-объекты/Mesh Renderer, но вы можете реализовать это самостоятельно. Все, что делает этот код, это скрытие других объектов, их фотографирование, отображение их, а затем возврат сделанного изображения. Вы можете расширить его, включив в него спрайт и 3D-объекты/Mesh Renderer. - person Programmer; 23.04.2016
comment
@Programmer Хорошо, сделаю это, но другая проблема заключается в том, как мне указать только часть холста, который нужно сохранить, а не сохранять весь холст? - person TheDevilsFire; 23.04.2016
comment
@TheDevilsFire Это было сделано для Canvas. Таким образом, холст в середине или углу экрана может быть сделан без другого пользовательского интерфейса на другом холсте. Чтобы сделать то, что вы просите, вам придется переписать весь скрипт. Вы можете это сделать, если понимаете, как это работает. Он скрывает другой пользовательский интерфейс с другого холста, делает снимок, показывает их и возвращает сделанный снимок. Вы можете легко реализовать это для своих нужд. Есть много важных функций, которые можно повторно использовать в моем коде. - person Programmer; 23.04.2016

Это всего лишь быстрый удар, так как я не могу найти никакого официального простого способа сделать это.

Я понятия не имею, насколько интенсивной будет производительность, так как я не пробовал.

Добавьте камеру, направленную на панель пользовательского интерфейса, чтобы получить прямой снимок. Рендерим камеру в текстуру, только эта панель рендерится на камеру, уничтожаем камеру.

Затем закодируйте текстуру в png.

  1. Это может быть ужасно дорого.
  2. Он также будет отображать остальную часть экрана, если вы не подберете камеру к панели, и даже в этом случае, если панель не является квадратной или прямоугольной, вы получите часть цвета игры/скайбокса/фона в кадре.
  3. Это могло просто не сработать
person Allen    schedule 11.04.2016

Я не изучал это глубоко, но я предполагаю, что невозможно отобразить панель без рендеринга ее родительского холста. Я также предполагаю, что вы не хотите делать это каждый кадр, а только в определенных случаях.

В этом контексте, вот что я бы попробовал:

  • Создайте второй холст с режимом RenderMode «Экранное пространство — камера». Это позволяет указать камеру, используемую для рендеринга этого холста.
  • Имейте специальную камеру для рендеринга вашего второго холста.
  • Дайте камере скрипт, который обрабатывает OnPreCull и OnPostRender.
  • OnPreCull прикрепите целевую панель к вторичному холсту. OnPostRender, прикрепите его туда, где он был раньше (мне так... очень жаль)
  • Пусть вторичная камера визуализирует на RenderTexture
  • ReadPixels, Apply, EncodeToPNG, вуаля

Конечно, есть много неуказанных деталей, таких как размеры, положение и тому подобное. Но наличие выделенных Canvas и Camera должно позволить все это понять и правильно настроить, не путаясь.

person Thomas Hilbert    schedule 12.04.2016
comment
но только в особых случаях., прямо конечно - только разово. Я изучу ваш метод! - person Fattie; 12.04.2016
comment
О, может случиться так, что OnPreRender уже слишком поздно для фактического включения Panel в рендеринг. Я изменю свой ответ, чтобы предложить OnPreCull. - person Thomas Hilbert; 12.04.2016
comment
Я думаю, что ключевым моментом может быть то, что, по сути, вам нужна еще одна камера с дубликатом рассматриваемой панели (что, в конце концов, не так уж сложно организовать). возможно, это решение KISS. - person Fattie; 12.04.2016
comment
@JoeBlow, я работал над этим вчера, это была первая идея, которая пришла мне в голову. Мой был немного другим. Я попытался продублировать игровой объект, создать новую камеру, затем переместить пользовательский интерфейс и камеру в другое место и сделать снимок. Это не сработало, потому что пользовательский интерфейс двигался, но камера отказывалась двигаться вместе с пользовательским интерфейсом. Я пытался использовать слой и маску отсечения, но столкнулся со слишком многими проблемами, и вам нужно создать новый слой в редакторе, чтобы он заработал. Вы не можете создать слой из кода... Я хочу что-то простое. Наконец я заставил его работать. Я улучшаю его перед публикацией. - person Programmer; 12.04.2016
comment
Хотя код длиннее, чем исходный код stackoverflow. - person Programmer; 12.04.2016
comment
это блестящая идея использовать Слои Я просто не подумал об этом. Я имею в виду, что это нормально, если вам нужно иметь слой в проекте для этой цели, это здорово. Отличное мышление!!! @Программист - person Fattie; 12.04.2016
comment
На самом деле вам вообще не нужно создавать слои, слой — это не что иное, как значение поля слоя GameObject в диапазоне от 0 до 31. Просто установите для него любое свободное значение. Затем просто установите для LayerMask камеры значение 1 ‹‹ (слой), и все будет хорошо. - person Thomas Hilbert; 12.04.2016
comment
Лол, я помню, что тег - это то, что вы не можете создать кодом, а не слоем. Виноват. Есть проблема с Unity UI. Материал слоя не работает с ним, если вы не измените режим рендеринга холста. Таким образом, это означает, что вам нужно перебрать все включенные холсты в сцене и изменить каждый режим рендеринга, а затем поместить основную камеру в свойство Render Camera каждого холста в другом, чтобы отбраковка работала. Пока это не будет исправлено для пользовательского интерфейса, я не думаю, что это будет быстро. - person Programmer; 12.04.2016
comment
примечание.... здесь НЕ требуется скорость. это очень похоже на то, что конечный пользователь может собрать небольшое изображение (возможно, с текстом и т. д.), а затем щелкнуть «Создать PNG»! (представьте, что вы отправляете это себе по электронной почте). так что все, что у вас есть, ребята, здорово! - person Fattie; 12.04.2016
comment
@JoeBlow этого не знал. Скорость мысли является требованием, и мне пришлось удалить свой первый код, который работал, но медленно. - person Programmer; 13.04.2016
comment
хм, может быть, тебе стоит вернуть и это! :) - person Fattie; 13.04.2016