4
\$\begingroup\$

I have an sprite I want to use as a texture. I want to create an area, defined by a polygon collider, where the sprite is drawn.

For example:

  • Having the following sprite

    Sprite

  • And the following polygon collider 2d:

    polygon

  • I want to have the resulting sprite/mesh:

This way I would have an image that covers exactly the polygon collider area.

Having an sprite and modifying it's vertexes doesn't seem to do the trick, as they are only editable while loading the sprite into unity.

Is there a way to make it editable in the editor, so while I change my collider, the image is updated to fit inside of it?

\$\endgroup\$
4
  • 1
    \$\begingroup\$ I remember hearing about a future feature called smart sprites that did this, did they get implemented by now? \$\endgroup\$
    – Bálint
    Commented Jul 28, 2017 at 17:07
  • 1
    \$\begingroup\$ Have you checked out the OverrideGeometry method of the Sprite class, or the mesh creation features of the Mesh class? \$\endgroup\$
    – DMGregory
    Commented Jul 28, 2017 at 17:10
  • \$\begingroup\$ @DMGregory Yeah, I tried using the OverrideGeometry, but I got an error. It seems it can only be used during the loading of the sprite into the assets, so you can make your own geometry for the asset sprite. This would mean all sprites I instantiate would have the same shape. Since I want to be able to have different instances with different shapes, this approach doesn't really fit my needs. \$\endgroup\$
    – Leo
    Commented Jul 28, 2017 at 17:21
  • \$\begingroup\$ @Bálint It seems it was not implemented yet. It really seems exactly to be what I was looking for (as long as polygon collider matches the sprite form). \$\endgroup\$
    – Leo
    Commented Jul 28, 2017 at 17:29

2 Answers 2

3
\$\begingroup\$

As you've pointed out, Sprite.OverrideGeometry is a bit too picky with what it lets you do to make this simple.

But, we can get a lot of the same outcomes by using a MeshRenderer with a custom-shaped Mesh. The downside is losing the easy layer sorting we get with Sprites, but it's possible to imitate that by other means if you need granular sorting control.

Animation showing the process of editing a shape's collider and rendered mesh in sync

In the example above I've got a GameObject with an (initially empty) Mesh Filter, a MeshRenderer with a basic Unlit Texture material assigned, and the script below. You can see it handles interactively reshaping the rendered polygon to match the collider and tiling the texture across the result. It does not correctly handle self-intersecting polygons in its current incarnation.

[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(PolygonCollider2D))]
public class 2DPolyColliderToMesh : MonoBehaviour {

    PolygonCollider2D _collider;
    Vector2[] _cachedPoints;
    List<int> _triangles = new List<int>();
    Mesh _myMesh;

    // In-editor, poll for collider updates so we can react 
    // to shape changes with realtime interactivity.
#if UNITY_EDITOR
    void OnDrawGizmosSelected()
    {
        if (_collider == null)
            Initialize();
        else {
            var colliderPoints = _collider.GetPath(0);
            if(colliderPoints.Length == _cachedPoints.Length) {
                bool mismatch = false;
                for(int i = 0; i < colliderPoints.Length; i++) {
                    if (colliderPoints[i] != _cachedPoints[i]) {
                        mismatch = true;
                        break;
                    }
                }
                if (mismatch == false)
                    return;
            }

            Reshape();
        }
    }
#endif

    // Wire up references and set initial shape.
    void Initialize()
    {
        _collider = GetComponent<PolygonCollider2D>();
        var filter = GetComponent<MeshFilter>();

        // This creates a unique mesh per instance. If you re-use shapes
        // frequently, then you may want to look into sharing them in a pool.
        _myMesh = new Mesh();
        _myMesh.MarkDynamic();

        Reshape();

        filter.sharedMesh = _myMesh;
    }        

    // Call this if you edit the collider at runtime 
    // and need the visual to update.
    public void Reshape() {
        // For simplicity, we'll only handle colliders made of a single path.
        // This method can be extended to handle multi-part colliders and
        // colliders with holes, but triangulating these gets more complex.
        _cachedPoints = _collider.GetPath(0);

        // Triangulate the loop of points around the collider's perimeter.
        LoopToTriangles();

        // Populate our mesh with the resulting geometry.
        Vector3[] vertices = new Vector3[_cachedPoints.Length];
        for (int i = 0; i < vertices.Length; i++)
            vertices[i] = _cachedPoints[i];

        // We want to make sure we never assign fewer verts than we're indexing.
        if(vertices.Length <= _myMesh.vertexCount) {
            _myMesh.triangles = _triangles.ToArray();
            _myMesh.vertices = vertices;
            _myMesh.uv = _cachedPoints;
        } else {
            _myMesh.vertices = vertices;
            _myMesh.uv = _cachedPoints;
            _myMesh.triangles = _triangles.ToArray();
        }        
    }

    void LoopToTriangles() {
        // This uses a naive O(n^3) ear clipping approach for simplicity.
        // Higher-performance triangulation methods exist if you need to
        // do this at runtime or with high-vertex-count polygons, or
        // polygons with holes & self-intersections.
        _triangles.Clear();

        // Mode switch for clockwise/counterclockwise paths.
        int winding = ComputeWinding(_cachedPoints);

        List<Vector2> ring = new List<Vector2>(_cachedPoints);
        List<int> indices = new List<int>(ring.Count);
        for (int i = 0; i < ring.Count; i++)
            indices.Add(i);

        while(indices.Count > 3) {
            int tip;
            for (tip = 0; tip < indices.Count; tip++)
                if (IsEar(ring, tip, winding))
                    break;

            int count = indices.Count;
            int cw = (tip + count + winding) % count;
            int ccw = (tip + count - winding) % count;
            _triangles.Add(indices[cw]);
            _triangles.Add(indices[ccw]);
            _triangles.Add(indices[tip]);            
            ring.RemoveAt(tip);
            indices.RemoveAt(tip);
        }

        if (winding < 0) {
            _triangles.Add(indices[2]);
            _triangles.Add(indices[1]);
            _triangles.Add(indices[0]);
        } else _triangles.AddRange(indices);
    }

    // Returns -1 for counter-clockwise, +1 for clockwise.
    int ComputeWinding(Vector2[] ring)
    {
        float windingSum = 0;
        Vector2 previous = ring[ring.Length - 1];        
        for (int i = 0; i < ring.Length; i++)
        {
            Vector2 next = ring[i];
            windingSum += (next.x - previous.x) * (next.y + previous.y);
            previous = next;
        }

        return windingSum > 0f ? 1 : -1;
    }

    // Checks if a given point forms an "ear" of the polygon.
    // (A convex protrusion with no other vertices inside it)
    bool IsEar(List<Vector2> ring, int tip, int winding) {
        int count = ring.Count;
        int cw = (tip + count + winding) % count;
        int ccw = (tip + count - winding) % count;
        Vector2 a = ring[cw];
        Vector2 b = ring[tip];
        Vector2 c = ring[ccw];

        Vector2 ab = b - a;
        Vector2 bc = c - b;
        Vector2 ca = a - c;

        // Early-out for concave vertices.
        if (DotPerp(ab, bc) < 0f)
            return false;

        float abThresh = DotPerp(ab, a);
        float bcThresh = DotPerp(bc, b);
        float caThresh = DotPerp(ca, c);

        for (int i = (ccw + 1) % count; i != cw; i = (i + 1) % count) {
            Vector2 test = ring[i];
            if (   DotPerp(ab, test) > abThresh 
                && DotPerp(bc, test) > bcThresh 
                && DotPerp(ca, test) > caThresh )
                return false;
        }

        return true;
    }

    // Dot product of the perpendicular of vector a against vector b.
    float DotPerp(Vector2 a, Vector2 b) {
        return a.x * b.y - a.y * b.x;
    }
}
\$\endgroup\$
16
  • \$\begingroup\$ From your gif, it seems exactly what I need! I tried to implement it in a project, but I'm getting an error. I have been debuging, and it seems like sometimes no ear is found, and an index out of bound is selected. Any idea what could be causing this? \$\endgroup\$
    – Leo
    Commented Jul 29, 2017 at 11:36
  • 1
    \$\begingroup\$ My triangulation is pretty slapdash, just enough to show the concept, so I haven't stress tested it. How is your collider shaped? (Images help) \$\endgroup\$
    – DMGregory
    Commented Jul 29, 2017 at 11:38
  • \$\begingroup\$ Its just apolygon with 5 vertices. I tried rearanging them in any way, but it keeps failing, no matter how I set them up. I'll review the function to check for ears. I'll let you know if I find something. \$\endgroup\$
    – Leo
    Commented Jul 29, 2017 at 11:43
  • 1
    \$\begingroup\$ Hmmm... I wonder if Unity doesn't always wind the points of colliders in counter-clockwise order? All my tests happened to be CCW, but that might have been coincidence rather than a hard rule Unity uses. \$\endgroup\$
    – DMGregory
    Commented Jul 29, 2017 at 11:45
  • 1
    \$\begingroup\$ That's a whole new question, so the best thing to do is to create a new question post. Be sure to define specifically what layering scenario you need. "Correctly" replicating all of Unity's sprite sorting features on meshes would be very hard. But getting a specific handful of content to layer in a particular way is often much easier. \$\endgroup\$
    – DMGregory
    Commented Jul 29, 2017 at 14:36
-1
\$\begingroup\$

You could make a nested for-loop to draw the sprite the right number of times in each direction. For each sprite, draw a point, if the point is within the bounds of the collider.

In other words, you need to define a boolean function that will return true if the point is within the collider and false otherwise. There are many ways to this, but by testing if the point is in a polygon (or in any one of a set of polygons) should do the trick.

\$\endgroup\$
6
  • 2
    \$\begingroup\$ Point-in-collider functions already exist in Unity, but plotting a polygon shape pixel by pixel based on these hit tests is not an efficient solution to this problem. Note that in OP's example, the target shape includes sharp pointed features much smaller than the size of the source sprite. \$\endgroup\$
    – DMGregory
    Commented Jul 28, 2017 at 18:25
  • \$\begingroup\$ I do not use Unity, but couldn't you render that to a texture and then render the texture, so you don't have to run the algorithm each frame? \$\endgroup\$
    – clabe45
    Commented Jul 28, 2017 at 18:29
  • 1
    \$\begingroup\$ Then that limits you to drawing it at the cached scale & rotation if you don't want to incur another expensive re-draw to the texture, or cope with sampling artifacts. Drawing a tiled texture over an arbitrary mesh is something GPUs do fluently in parallel, so why would we try to re-implement this functionality ourselves on the CPU? \$\endgroup\$
    – DMGregory
    Commented Jul 28, 2017 at 18:35
  • \$\begingroup\$ So tell me, how would the GPU do this task? \$\endgroup\$
    – clabe45
    Commented Jul 28, 2017 at 18:53
  • 1
    \$\begingroup\$ A GPU would take a mesh consisting of a collection of vertices with positions & texture coordinates and indices ordering them into triangles, project each triangle onto the screen, rasterize the visible portion into fragments, and sample the corresponding texels from the texture for each interpolated fragment. See my answer for a method to construct such an input mesh. The engine can handle everything downstream from there using its built-in features and default shaders. \$\endgroup\$
    – DMGregory
    Commented Jul 29, 2017 at 2:57

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .