Wednesday, September 11, 2019

unity - Reshapable polygonsprite



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?



Answer




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 _triangles = new List();
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();
var filter = GetComponent();

// 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 ring = new List(_cachedPoints);
List indices = new List(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 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;
}
}

No comments:

Post a Comment

Simple past, Present perfect Past perfect

Can you tell me which form of the following sentences is the correct one please? Imagine two friends discussing the gym... I was in a good s...