Skip to content

Instantly share code, notes, and snippets.

@handcircus
Created May 11, 2018 11:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save handcircus/0b06a18189662afe34a7abf6d95c5ea3 to your computer and use it in GitHub Desktop.
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)
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