'Displaying an image to a camera

How would you approach displaying an image to a camera, with the option of editing it pixel by pixel at runtime?



Solution 1:[1]

Definitely possible. With a combination of SetPixels and setting your texture to Read Write Enabled, you can write to a texture and manipulate the data. I am not sure what you mean by

has it's view replaced with the image

Here is how I approached your problem

// color we are setting pixels to
[SerializeField] private Color clr = Color.white;

// our source UI image - it can be a raw image or sprite renderer, I just used UI image
[SerializeField] private Image img = null;

// the size of our 'brush'
[Range(1, 100)]
[SerializeField] private int BrushSize = 1;

// the texture we are going to manipulate
private Texture2D tex2D = null;

private void Awake()
{
    Sprite imgSprite = img.sprite;

    // create a new instance of our texture to not write to it directly and overwrite it
    tex2D = new Texture2D((int)imgSprite.rect.width, (int)imgSprite.rect.height);
    var pixels = imgSprite.texture.GetPixels((int)imgSprite.textureRect.x,
                                            (int)imgSprite.textureRect.y,
                                            (int)imgSprite.textureRect.width,
                                            (int)imgSprite.textureRect.height);
    tex2D.SetPixels(pixels);
    tex2D.Apply();

    // assign this new texture to our image by creating a new sprite
    img.sprite = Sprite.Create(tex2D, img.sprite.rect, img.sprite.pivot);
}

public void OnDrag(PointerEventData eventData)
{
    Vector2 localCursor;

    // convert the position click to a local position on our rect
    if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(img.rectTransform, eventData.position, eventData.pressEventCamera, out localCursor))
        return;

    // convert this position to pixel coordinates on our texture
    int px = Mathf.Clamp(0, (int)((localCursor.x - img.rectTransform.rect.x) * tex2D.width / img.rectTransform.rect.width), tex2D.width);
    int py = Mathf.Clamp(0, (int)((localCursor.y - img.rectTransform.rect.y) * tex2D.height / img.rectTransform.rect.height), tex2D.height);

    // debugging - you can remove this
    print(px + ", " + py);


    // if our brush size is greater than 1, then we need to grab neighbors
    if (BrushSize > 1)
    {
        // create an array for our colors
        Color[] colorArray = new Color[BrushSize * BrushSize];

        // fill this with our color
        for(int x = 0; x < BrushSize * BrushSize; ++x)
            colorArray[x] = clr;

        tex2D.SetPixels(px, py, BrushSize, BrushSize, colorArray);
    }
    else
    {
        // set our color at our position
        tex2D.SetPixel(px, py, clr);
    }

    // apply the changes
    tex2D.Apply();

    // set our sprite to the new texture data
    img.sprite = Sprite.Create(tex2D, img.sprite.rect, img.sprite.pivot);
}

Here is the snippet in action. I have not accounted for some error handling such as the brush stroke being outside the bounds of the texture when using SetPixels. The current brush is also bottom left-aligned, which can be changed to be centered. As you had not added any progress of your own, I will leave you with cleaning up the implementation and changing it as you need for your use case.

Example

And for clarity, here are the import settings on the texture.

Import setting

Edit: I answered another question related to drawing onto textures in real time. Here is an updated snippet with error handling, centered brush size, etc.

using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class TestScript : MonoBehaviour, IPointerClickHandler, IDragHandler
{
    // color we are setting pixels to
    [SerializeField] private Color clr = Color.white;

    // our source UI image - it can be a raw image or sprite renderer, I just used UI image
    [SerializeField] private Image img = null;

    [Range(1, 255)]
    [SerializeField] private int BrushSize = 1;

    // the texture we are going to manipulate
    private Texture2D tex2D = null;

    private void Awake()
    {
        Sprite imgSprite = img.sprite;

        // create a new instance of our texture to not write to it directly and overwrite it
        tex2D = new Texture2D((int)imgSprite.rect.width, (int)imgSprite.rect.height);
        var pixels = imgSprite.texture.GetPixels((int)imgSprite.textureRect.x,
                                                (int)imgSprite.textureRect.y,
                                                (int)imgSprite.textureRect.width,
                                                (int)imgSprite.textureRect.height);

        tex2D.SetPixels(pixels);
        tex2D.Apply();

        // assign this new texture to our image by creating a new sprite
        img.sprite = Sprite.Create(tex2D, img.sprite.rect, img.sprite.pivot);
    }

    public void OnPointerClick(PointerEventData eventData)
    {
        Draw(eventData);
    }

    public void OnDrag(PointerEventData eventData)
    {
        Draw(eventData);
    }

    private void Draw(in PointerEventData eventData)
    {
        Vector2 localCursor;

        // convert the position click to a local position on our rect
        if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(img.rectTransform, eventData.position, eventData.pressEventCamera, out localCursor))
            return;

        // convert this position to pixel coordinates on our texture
        int px = Mathf.Clamp(0, (int)((localCursor.x - img.rectTransform.rect.x) * tex2D.width / img.rectTransform.rect.width), tex2D.width);
        int py = Mathf.Clamp(0, (int)((localCursor.y - img.rectTransform.rect.y) * tex2D.height / img.rectTransform.rect.height), tex2D.height);

        // confirm we are in the bounds of our texture
        if (px >= tex2D.width || py >= tex2D.height)
            return;

        // debugging - you can remove this
        // print(px + ", " + py);

        // if our brush size is greater than 1, then we need to grab neighbors
        if (BrushSize > 1)
        {
            // bottom - left aligned, so find new bottom left coordinate then use that as our starting point
            px = Mathf.Clamp(px - (BrushSize / 2), 0, tex2D.width);
            py = Mathf.Clamp(py - (BrushSize / 2), 0, tex2D.height);

            // add 1 to our brush size so the pixels found are a neighbour search outward from our center point
            int maxWidth = Mathf.Clamp(BrushSize + 1, 0, tex2D.width - px);
            int maxHeight = Mathf.Clamp(BrushSize + 1, 0, tex2D.height - py);

            // cache our maximum dimension size
            int blockDimension = maxWidth * maxHeight;

            // create an array for our colors
            Color[] colorArray = new Color[blockDimension];

            // fill this with our color
            for (int x = 0; x < blockDimension; ++x)
                colorArray[x] = clr;

            // set our pixel colors
            tex2D.SetPixels(px, py, maxWidth, maxHeight, colorArray);
        }
        else
        {
            // set our color at our position - note this will almost never be seen as most textures are rather large, so a single pixel is not going to
            // appear most of the time
            tex2D.SetPixel(px, py, clr);
        }

        // apply the changes - this is what you were missing
        tex2D.Apply();

        // set our sprite to the new texture data
        img.sprite = Sprite.Create(tex2D, img.sprite.rect, img.sprite.pivot);
    }
}   

And a gif: Example

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1