Erasing a part of an image using SetPixel without gaps
Asked Answered
J

2

0

I am creating a project in which a part of an image can be erased with a mouse click/drag. I have already made the mouse click. However, when the mouse is dragged quickly, there are gaps between the erased part like this.

123425-unity.png

Is there a solution for this? Below is my code which was taken from here]:

Painting sprite in unity - Stack Overflow

void Update () {
	hit = Physics2D.Raycast (Camera.main.ScreenToWorldPoint (Input.mousePosition), Vector2.zero);
	if (Input.GetMouseButton (0) && hit) {
		UpdateTexture ();
	}
}
  
public void UpdateTexture() {
	spriteRend = gameObject.GetComponent<SpriteRenderer> ();
	tex = CopyTexture (spriteRend.sprite.texture);
	string tempName = spriteRend.sprite.name;
	spriteRend.sprite = Sprite.Create (tex, spriteRend.sprite.rect, new Vector2 (0.5f, 0.5f));
	spriteRend.sprite.name = tempName;
}
  
public Texture2D CopyTexture(Texture2D copiedTexture) {
	float dX, dY;
	Texture2D newTex = new Texture2D (copiedTexture.width, copiedTexture.height);
	newTex.filterMode = FilterMode.Bilinear;
	newTex.wrapMode = TextureWrapMode.Clamp;
  
	int mX = (int)((hit.point.x - hit.collider.bounds.min.x) * (copiedTexture.width / hit.collider.bounds.size.x));
	int mY = (int)((hit.point.y - hit.collider.bounds.min.y) * (copiedTexture.height / hit.collider.bounds.size.y));
  
	for (int x = 0; x < newTex.width; x++) {
		for (int y = 0; y < newTex.height; y++) {
			dX = x - mX;
			dY = y - mY;
  
			if (dX * dX + dY * dY <= erSize * erSize) {
				newTex.SetPixel (x, y, zeroAlpha);
			} else {
				newTex.SetPixel (x, y, copiedTexture.GetPixel (x, y));
			}
		}
	}
	newTex.Apply ();
	return newTex;
}
Jugal answered 5/4, 2024 at 12:44 Comment(0)
D
0

Don’t do a check against a point but against a line segment. The only difference is that before you do the distance check you have to project the coordinate of the current pixel you’re testing onto the line segment. Of course this projected point need to be clamped between the start and end points of the line. Once you have that point you can simply subtract the projected point from x and y instead of just the new point. This will naturally draw a “line” with a width of twice your radius. It’s basically the shadow of a capsule.

So all you need is to save the last position while dragging so you have that line segment you need. Note that currently you iterate over all pixels in the image. This could be optimised by just iterating over the bounding rectangle of your line segment. So just figure out the min and max values of your start and end coordinates and add / subract the radius and finally clamp the result to the image resolution. Finally you shouldn’t use SetPixel in a loop. It’s much more efficient to use GetPixels once in Start, modify the array and use SetPixels after your processing.

Also you shouldn’t create a new texture each time. Unused Textures need to be destroyed or you will run out of memory. You may also want to create a single texture which you are going to modify in start and reuse that texture…

EDIT

I just created a script based on your approach but heavily modified it with the changes i’ve mentioned.

using UnityEngine;

public class DrawLines : MonoBehaviour
{
    private Texture2D m_Texture;
    private Color[] m_Colors;
    RaycastHit2D hit;
    SpriteRenderer spriteRend;
    Color zeroAlpha = Color.clear;
    public int erSize;
    public Vector2Int lastPos;
    public bool Drawing = false;
    void Start ()
    {
        spriteRend = gameObject.GetComponent<SpriteRenderer>();
        var tex = spriteRend.sprite.texture;
        m_Texture = new Texture2D(tex.width, tex.height, TextureFormat.ARGB32, false);
        m_Texture.filterMode = FilterMode.Bilinear;
        m_Texture.wrapMode = TextureWrapMode.Clamp;
        m_Colors = tex.GetPixels();
        m_Texture.SetPixels(m_Colors);
        m_Texture.Apply();
        spriteRend.sprite = Sprite.Create(m_Texture, spriteRend.sprite.rect, new Vector2(0.5f, 0.5f));
	}

    void Update()
    {
        if (Input.GetMouseButton(0))
        {
            hit = Physics2D.Raycast(Camera.main.ScreenToWorldPoint(Input.mousePosition), Vector2.zero);
            if (hit.collider != null)
            {
                UpdateTexture();
                Drawing = true;
            }
        }
        else
            Drawing = false;
    }

    public void UpdateTexture()
    {
        int w = m_Texture.width;
        int h = m_Texture.height;
        var mousePos = hit.point - (Vector2)hit.collider.bounds.min;
        mousePos.x *= w / hit.collider.bounds.size.x;
        mousePos.y *= h / hit.collider.bounds.size.y;
        Vector2Int p = new Vector2Int((int)mousePos.x, (int)mousePos.y);
        Vector2Int start = new Vector2Int();
        Vector2Int end = new Vector2Int();
        if(!Drawing)
            lastPos = p;
        start.x = Mathf.Clamp(Mathf.Min(p.x, lastPos.x) - erSize, 0, w);
        start.y = Mathf.Clamp(Mathf.Min(p.y, lastPos.y) - erSize, 0, h);
        end.x = Mathf.Clamp(Mathf.Max(p.x, lastPos.x) + erSize, 0, w);
        end.y = Mathf.Clamp(Mathf.Max(p.y, lastPos.y) + erSize, 0, h);
        Vector2 dir = p - lastPos;
        for (int x = start.x; x < end.x; x++)
        {
            for (int y = start.y; y < end.y; y++)
            {
                Vector2 pixel = new Vector2(x, y);
                Vector2 linePos = p;
                if (Drawing)
                {
                    float d = Vector2.Dot(pixel - lastPos, dir) / dir.sqrMagnitude;
                    d = Mathf.Clamp01(d);
                    linePos = Vector2.Lerp(lastPos, p, d);
                }
                if ((pixel - linePos).sqrMagnitude <= erSize * erSize)
                {
                    m_Colors[x + y * w] = zeroAlpha;
                }
            }
        }
        lastPos = p;
        m_Texture.SetPixels(m_Colors);
        m_Texture.Apply();
        spriteRend.sprite = Sprite.Create(m_Texture, spriteRend.sprite.rect, new Vector2(0.5f, 0.5f));
    }
}

Here’s an example of the script in action. The first radius is 10, the second is 40

EOXH7yr4
original gif link

Note the image used here is “1024x240” and works smoothly. Though if you have a larger image you may run into performance issues as updating such large images is always a problem as it has to be uploaded to the GPU each time. You might get better performance when creating an actual mask mesh and only apply the drawn lines every now and then. Though for relatively small images the current approach seems to work fine.

Deaton answered 29/1, 2024 at 8:59 Comment(5)

Thank you so much for your answer. I never thought you would modify the script. I had a hard time understanding your explanation because I am new in Unity and it is advanced for me. But based on your script I kinda understand it now. It worked perfectly. Again, thank you so much.

Jugal

Amazing answer, but could you please explain for real newbie: 1) Is this script should be attached to Image that have Sprite Renderer? 2) What type of collider i have to use? I create sprite renderer object, assign to sprite my texture, in texture setting made it readable and type of Sprite then add your script and looks likes it needs collider 2d? Because "Drawing" bool variable never true without it. After adding Box 2d collider i can only cut center part, but can't drag or something like you show. Could you help please?

Heathenish

@Deaton thank you so much. It is working perfectly. I have one request. Would you please tell us here — how to calculate the pixel amount which have been erased (how much portion is erased). For my game I want to show LEVEL COMPLETE after fully erased the sprite. Sorry for my bad English. Btw I am very new in unity. Thanks.

Tatman

So cmd works Now you need to set the rpc just as it is done in cmd How differently have you done for the playre? Maybe some info to [read][1] [1]: https://mirror-networking.gitbook.io/docs/guides/communications/remote-actions#bypassing-authority

Pilaf

your link script was dead, can you give other link ?

Mariandi
M
0

@Deaton

brother your code is very clean and good in working thanks for that but, this work fine in a single sprite when I try this with sprite sheet it work something wrong could you please help.

,your code is very clean and good in working thanks for that but, this work fine in a single sprite when I try this with sprite sheet it work something wrong could you please help

Millrun answered 6/6, 2023 at 4:38 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.