Swagrim
Swagrim

Reputation: 422

Unity's SetPixel method does not color out the given pixels

I am currently developing a pixel art program in Unity. Obviously, it has a pencil tool with a script on it that I have made.

Unfortunately, the SetPixel method does not color the pixels. I don't know if it is the method itself that it's not working or something else.

This is the code I am using:

[SerializeField] private Sprite textureRendererSprite;
private Texture2D texture;
private MouseCoordinates mouseCoordinates;

void Start()
{
    mouseCoordinates = GetComponent<MouseCoordinates>();
    texture = textureRendererSprite.texture;
}

void Update()
{
    if (Input.GetMouseButtonDown(0))
    {
        texture.SetPixel(int.Parse(mouseCoordinates.posInt.x.ToString()), int.Parse(mouseCoordinates.posInt.y.ToString()), Color.black);
        Debug.Log(int.Parse(mouseCoordinates.posInt.x.ToString()));
        Debug.Log(int.Parse(mouseCoordinates.posInt.y.ToString()));
    }
}

Also, this is my MouseCoordinates script:

[SerializeField] private Canvas parentCanvas = null;
[SerializeField] private RectTransform rect = null;
[SerializeField] private Text text;
public Vector2 posInt;


[SerializeField] private Camera UICamera = null;

void Start()
{
    if (rect == null)
        rect = GetComponent<RectTransform>();

    if (parentCanvas == null)
        parentCanvas = GetComponentInParent<Canvas>();

    if (UICamera == null && parentCanvas.renderMode == RenderMode.WorldSpace)
        UICamera = parentCanvas.worldCamera;
}

public void OnPointerClick(PointerEventData eventData)
{
    RectTransformUtility.ScreenPointToLocalPointInRectangle(rect, eventData.position, UICamera, out Vector2 localPos);

    localPos.x += rect.rect.width / 2f;
    localPos.y += rect.rect.height / 2f;

    posInt.x = ((int)localPos.x);
    posInt.y = ((int)localPos.y);

    text.text = (posInt.x + ", " + posInt.y).ToString();
}

Upvotes: 0

Views: 2868

Answers (1)

TEEBQNE
TEEBQNE

Reputation: 6266

I was a little bored, so here is a fully working pixel draw I just whipped up. The one part you were missing with your implementation is Texture2D.Apply, which based on the Texture2D.SetPixels doc page,

This function takes a color array and changes the pixel colors of the whole mip level of the texture. Call Apply to actually upload the changed pixels to the graphics card.

Now to your actual implementation. You do not need a majority of the data you are caching, as a PointerEventData already has most of it. The only component you will need is the Image component that you want to change.

OnPointerClick is fine, but that only registers clicks, not dragging. If you want to make a pixel art tool, most art is done by dragging a cursor or stylus, so you will want to use an OnDragHandler instead or, along with your click.

One other note, you are not adding any brush size. More of a QoL update to your snippet, but with the addition of a brush size there are other complications that arise. SetPixel is bottom left aligned and must be contained within the bounds of the texture. You can correct this by offsetting the center point of your click by half a brush size, then clamping the width and height of your box.

Here is the current snippet:

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);
    }
}

Example

Here is a gif of the snippet in action. Quite fun to play around with. And remember, whatever texture you use for this must have the setting Read and Write enabled on the import settings. Without this setting, the data is not mutable and you can not access the texture data at runtime.

Edit: Skimmed your question a bit too quickly. Realizing you are using a 2D sprite and not a UI Image or RawImage. You can still draw to a Sprite, but as it is not a UI object, it does not have a RectTransform. However, in your second snippet you reference a RectTransform. Can you explain your setup a bit more? The answer I provided should be enough to point you in the right direction either way.

Upvotes: 3

Related Questions