Created
May 11, 2018 11:21
-
-
Save handcircus/0b06a18189662afe34a7abf6d95c5ea3 to your computer and use it in GitHub Desktop.
Create a decal mesh, based upon projection onto another mesh. Optimised for mobile (so works in forward, rejects invalid triangles to prevent overdraw)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections; | |
using System.Collections.Generic; | |
using UnityEngine; | |
// Builds a mesh that represents a projected sprite onto another mesh | |
// Clips unused polys to prevent overdraw | |
// To use, just create a child game object on the mesh you want to project onto, and assign a sprite | |
// Currently doesnt support sprite sheets with multiple sprites | |
public class DecalMeshProjection : MonoBehaviour { | |
[SerializeField] | |
private Shader m_DecalShader; | |
[SerializeField] | |
private Sprite m_ProjectionSprite; | |
[SerializeField] | |
private float m_OffsetDistance=0.01f; // Distance to offset the vertices from sources mesh (based on local normal) | |
[SerializeField] | |
private float m_DecalWidth=0.2f; | |
[SerializeField] | |
private float m_DecalHeight=0.2f; | |
[SerializeField] | |
private bool m_UpdatePerFrame=false; | |
private MeshFilter m_MeshFilter; | |
private MeshRenderer m_MeshRenderer; | |
private Mesh m_Mesh; // Generated mesh | |
private Transform m_SourceTransform; // Referenced transform of parent | |
private Mesh m_SourceMesh; // Referenced mesh of parent | |
private Material m_ProjectionMaterial; // Material to project | |
private Rect m_DecalRect; // Dynamic rect to create | |
private void Start () { | |
CreateMesh(); | |
} | |
private void Update () { | |
if (m_UpdatePerFrame) UpdateMesh(); | |
} | |
private void CreateMesh() { | |
Debug.Assert(transform.parent!=null,"No parent node - requires a parent to project onto!"); | |
m_SourceTransform=transform.parent; | |
var srcMeshFilter=m_SourceTransform.GetComponent<MeshFilter>(); | |
Debug.Assert(srcMeshFilter!=null,"No mesh filter on parent node!"); | |
m_SourceMesh=srcMeshFilter.mesh; | |
m_Mesh=new Mesh(); | |
m_Mesh.MarkDynamic(); | |
m_MeshFilter=gameObject.AddComponent<MeshFilter>(); | |
m_MeshFilter.sharedMesh=m_Mesh; | |
m_MeshRenderer=gameObject.AddComponent<MeshRenderer>(); | |
m_ProjectionMaterial=new Material(m_DecalShader); | |
m_ProjectionMaterial.SetTexture("_MainTex",m_ProjectionSprite.texture); | |
m_MeshRenderer.sharedMaterial=m_ProjectionMaterial; | |
UpdateMesh(); | |
} | |
private void UpdateMesh() { | |
if (m_Mesh==null) return; | |
m_DecalRect=new Rect(new Vector2(-m_DecalWidth*0.5f,-m_DecalHeight*0.5f),new Vector2(m_DecalWidth,m_DecalHeight)); | |
var vertices=new List<Vector3>(); | |
var normals=new List<Vector3>(); | |
var triangles=new List<int>(); | |
var uvs=new List<Vector2>(); | |
var meshVertCount=0; | |
var srcVertices=m_SourceMesh.vertices; | |
var srcTris=m_SourceMesh.triangles; | |
var srcNormals=m_SourceMesh.normals; | |
Plane triPlane=new Plane();// Use to test face normal | |
for (int triIndex=0;triIndex<srcTris.Length;triIndex+=3) { | |
bool triWithinScope=false; // Check to see if any verts are within the decal rect (just need one) | |
var transformedVerts=new Vector3[3]; | |
var transformedNormals=new Vector3[3]; | |
for (int triVert=0;triVert<3;triVert++) { | |
var srcVertIndex=srcTris[triIndex+triVert]; | |
//Debug.Log("Accessing vertex "+srcVertIndex); | |
var vertex=srcVertices[srcVertIndex]; | |
var normal=srcNormals[srcVertIndex]; | |
var worldPos=m_SourceTransform.TransformPoint(vertex); // Get position in world space | |
var worldNormal=m_SourceTransform.TransformDirection(normal); // Get normal in world space | |
transformedVerts[triVert]=transform.InverseTransformPoint(worldPos); // Transform position to local space | |
transformedNormals[triVert]=transform.InverseTransformDirection(worldNormal); // Transform normal to local space | |
if (m_DecalRect.Contains(transformedVerts[triVert])) triWithinScope=true; // Test point within decal rect. If so pass automically | |
} | |
// Check that the triangle and rect intersect (ie that any part is visible within the rect) | |
if (!triWithinScope) triWithinScope=RectIntersectsTriangle(m_DecalRect,transformedVerts[0],transformedVerts[1],transformedVerts[2]); | |
// Calculate tri face direction | |
triPlane.Set3Points(transformedVerts[0],transformedVerts[1],transformedVerts[2]); | |
bool facingProjection=triPlane.normal.z<0; // Only project onto facing | |
if (triWithinScope && facingProjection) { | |
// Add triangles | |
triangles.AddRange(new List<int>{meshVertCount,meshVertCount+1,meshVertCount+2}); | |
meshVertCount+=3; | |
for (int triVert=0;triVert<3;triVert++) { | |
var localPos=transformedVerts[triVert]; | |
var localNormal=transformedNormals[triVert]; | |
vertices.Add(localPos+localNormal*m_OffsetDistance); | |
normals.Add(localNormal); | |
// Generate UVs based upon local pos. Offset by 0.5 in either direction to center | |
// TODO - Support pivot for sprite and sprite sheets (m_ProjectionSprite.bounds.center) | |
var uv=new Vector2(localPos.x/m_DecalRect.width+0.5f,localPos.y/m_DecalRect.height+0.5f); | |
uvs.Add(uv); | |
} | |
} | |
} | |
// Assign to mesh | |
m_Mesh.Clear(); // Need to clear to avoid errors | |
m_Mesh.SetVertices(vertices); | |
m_Mesh.SetNormals(normals); | |
m_Mesh.SetUVs(0,uvs); | |
m_Mesh.SetTriangles(triangles,0); | |
m_Mesh.RecalculateBounds(); | |
m_MeshFilter.sharedMesh=m_Mesh; | |
} | |
public static bool RectIntersectsTriangle(Rect rect, Vector2 p0, Vector2 p1, Vector2 p2) { | |
// This could probably be sped up | |
// Are any tri points in rect? | |
if (rect.Contains(p0) || rect.Contains(p1) || rect.Contains(p2)) return true; | |
// Are any of these quad points in the triangle? | |
if (PointInTriangle(new Vector2(rect.xMin,rect.yMax),p0,p1,p2)) return true; | |
if (PointInTriangle(new Vector2(rect.xMax,rect.yMax),p0,p1,p2)) return true; | |
if (PointInTriangle(new Vector2(rect.xMin,rect.yMin),p0,p1,p2)) return true; | |
if (PointInTriangle(new Vector2(rect.xMax,rect.yMin),p0,p1,p2)) return true; | |
// Quad and triangle COULD overlap but not contain each others points, check line intersections | |
if (LineRectIntersection(p0,p1,rect)) return true; | |
if (LineRectIntersection(p1,p2,rect)) return true; | |
if (LineRectIntersection(p2,p0,rect)) return true; | |
return false; | |
} | |
// From https://stackoverflow.com/questions/2049582/how-to-determine-if-a-point-is-in-a-2d-triangle | |
public static bool PointInTriangle(Vector2 p, Vector2 p0, Vector2 p1, Vector2 p2) { | |
var s = p0.y * p2.x - p0.x * p2.y + (p2.y - p0.y) * p.x + (p0.x - p2.x) * p.y; | |
var t = p0.x * p1.y - p0.y * p1.x + (p0.y - p1.y) * p.x + (p1.x - p0.x) * p.y; | |
if ((s < 0) != (t < 0)) return false; | |
var A = -p1.y * p2.x + p0.y * (p2.x - p1.x) + p0.x * (p1.y - p2.y) + p1.x * p2.y; | |
if (A < 0.0) { | |
s = -s; | |
t = -t; | |
A = -A; | |
} | |
return s > 0 && t > 0 && (s + t) <= A; | |
} | |
private static bool LineRectIntersection(Vector2 lineStartPoint, Vector2 lineEndPoint, Rect rectangle) { | |
Vector2 minXLinePoint = lineStartPoint.x <= lineEndPoint.x ? lineStartPoint : lineEndPoint; | |
Vector2 maxXLinePoint = lineStartPoint.x <= lineEndPoint.x ? lineEndPoint : lineStartPoint; | |
Vector2 minYLinePoint = lineStartPoint.y <= lineEndPoint.y ? lineStartPoint : lineEndPoint; | |
Vector2 maxYLinePoint = lineStartPoint.y <= lineEndPoint.y ? lineEndPoint : lineStartPoint; | |
double rectMaxX = rectangle.xMax; | |
double rectMinX = rectangle.xMin; | |
double rectMaxY = rectangle.yMax; | |
double rectMinY = rectangle.yMin; | |
if (minXLinePoint.x <= rectMaxX && rectMaxX <= maxXLinePoint.x) { | |
double m = (maxXLinePoint.y - minXLinePoint.y) / (maxXLinePoint.x - minXLinePoint.x); | |
double intersectionY = ((rectMaxX - minXLinePoint.x) * m) + minXLinePoint.y; | |
if (minYLinePoint.y <= intersectionY && intersectionY <= maxYLinePoint.y && rectMinY <= intersectionY && intersectionY <= rectMaxY) | |
return true; | |
} | |
if (minXLinePoint.x <= rectMinX && rectMinX <= maxXLinePoint.x) { | |
double m = (maxXLinePoint.y - minXLinePoint.y) / (maxXLinePoint.x - minXLinePoint.x); | |
double intersectionY = ((rectMinX - minXLinePoint.x) * m) + minXLinePoint.y; | |
if (minYLinePoint.y <= intersectionY && intersectionY <= maxYLinePoint.y && rectMinY <= intersectionY && intersectionY <= rectMaxY) | |
return true; | |
} | |
if (minYLinePoint.y <= rectMaxY && rectMaxY <= maxYLinePoint.y) { | |
double rm = (maxYLinePoint.x - minYLinePoint.x) / (maxYLinePoint.y - minYLinePoint.y); | |
double intersectionX = ((rectMaxY - minYLinePoint.y) * rm) + minYLinePoint.x; | |
if (minXLinePoint.x <= intersectionX && intersectionX <= maxXLinePoint.x && rectMinX <= intersectionX && intersectionX <= rectMaxX) | |
return true; | |
} | |
if (minYLinePoint.y <= rectMinY && rectMinY <= maxYLinePoint.y) { | |
double rm = (maxYLinePoint.x - minYLinePoint.x) / (maxYLinePoint.y - minYLinePoint.y); | |
double intersectionX = ((rectMinY - minYLinePoint.y) * rm) + minYLinePoint.x; | |
if (minXLinePoint.x <= intersectionX && intersectionX <= maxXLinePoint.x && rectMinX <= intersectionX && intersectionX <= rectMaxX) | |
return true; | |
} | |
return false; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment