I'm trying to make a dynamic light beam effect in 2D using Unity 6.1 (version 6000.1.13f1).
I want to use a mesh that I've created, that I already have got to emulate light beams, as a mask for a slightly transparent dark square sprite. I'm trying to use Stencil Buffer to:
1 - test if the mesh would be drawn in that pixel
2 - if so, don't draw any pixels from a specific layer (the darkness layer)
Is this possible, and if so, how could I go about doing it? I've looked at the shader lab stencil buffer docs, but they seem to assume a much higher level of understanding of shaders than I currently have.
#Create sprite mask from 3d/2d mesh using Stencil Buffer (2D)
1 messages · Page 1 of 1 (latest)
An update: I currently have it so that the mesh can mask other meshes, and 2d sprites can mask both meshes and sprites, but the mesh is unable to mask a sprite for some reason. Here is my current shader code:
Shader "Unlit/Mask"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry-1"}
LOD 100
Pass
{
Stencil {
Ref 1
Comp Always
Pass replace
}
ColorMask 0
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
If anyone knows why this is happening and how I can fix it, I would greatly appreciate it!
Do you have a demonstration of what it's meant to look like?
Sprite renderer's mask feature is the same as stencils
Custom shaders might not be required
Thanks for the response, but I ended up solving the issue! I do think the custom shader was required for this case since I wasn't using a 2d sprite to mask, but a 3d mesh (that happened to be a plane), and if there's a way to do that without the shader, I couldn't find it.
For anyone looking for the solution in future:
This was my mask shader:
Shader "Unlit/Mask"
{
Properties
{
[PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
_Color ("Tint", Color) = (1,1,1,1)
[MaterialToggle] PixelSnap ("Pixel snap", Float) = 0
[HideInInspector] _RendererColor ("RendererColor", Color) = (1,1,1,1)
[HideInInspector] _Flip ("Flip", Vector) = (1,1,1,1)
[PerRendererData] _AlphaTex ("External Alpha", 2D) = "white" {}
[PerRendererData] _EnableExternalAlpha ("Enable External Alpha", Float) = 0
}
SubShader
{
Tags { "Queue"="Geometry-100"
"IgnoreProjector"="True"
"RenderType"="Opaque"
"PreviewType"="Plane"
"CanUseSpriteAtlas"="True"}
LOD 100
Pass
{
Cull Off
Lighting Off
ZWrite Off
Blend One OneMinusSrcAlpha
Stencil {
Ref 1
Comp Always
Pass replace
}
ColorMask 0
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
if (col.a < 0.1) clip (-1);
col.rgb *= col.a;
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
Importantly, the z value of the object being masked did matter, I think the sprite had to be in front of the mask for some reason? Anyway, this shader goes on the mask, and the below shader goes on the object to be hidden:
Shader "Unlit/Darkness"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color ("Tint", Color) = (1,1,1,1)
}
SubShader
{
Tags { "Queue"="Transparent" "RenderType"="Tranparent" "IgnoreProjector"="True"}
LOD 200
Blend SrcAlpha OneMinusSrcAlpha
Pass
{
Stencil{
Ref 1
Comp NotEqual
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float4 color : COLOR; //this is from the spriterenderer, it turns out
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
fixed4 color : COLOR;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.color = v.color;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 c = tex2D(_MainTex, i.uv);
c.rgba = i.color;
return c;
}
ENDCG
}
}
}
This one also takes the rgba values from the masked object's SpriteRenderer.
This is also the first time I've worked with shaders, so I'm sure there are some unnecessary leftovers from the many sources I went through, but this one does work for me.
Also the reason I was using a mesh in this case was because I was making a dynamic flashlight so the mesh was modified every update to match the line of sight of the player.
Create sprite mask from 3d/2d mesh using Stencil Buffer (2D)
In the end this is what it looks like, I'll also provide the mesh script if anyone wants it (again probably not perfect)
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using static UnityEditor.Searcher.SearcherWindow.Alignment;
public class LightBeam : MonoBehaviour
{
public List<Vector3> newVertices;
public List<int> newTriangles;
public List<Vector2> uvs;
Mesh mesh;
Bounds bounds;
public LayerMask wallLayer;
public GameObject obj;
public GameObject obj1;
public GameObject left;
public GameObject right;
public GameObject lightHolder;
public float angle;
public float arcAngle;
public int nTriangles;
void Start()
{
//Instantiate lists:
//1st - vertices and uv: n = 1 + 1 for each triangle
newVertices.Add(new Vector3(0, 0, 0));
for (var i = 0; i < nTriangles + 1; i++)
{
newVertices.Add(new Vector3(0, 0, 0));
}
for (int i = 0; i < newVertices.Count(); i++)
{
uvs.Add(new Vector2(newVertices[i].x / bounds.size.x, newVertices[i].y / bounds.size.y));
}
//2nd - triangles:
for (var j = 1; j < nTriangles + 1; j++)
{
newTriangles.Add(0);
newTriangles.Add(j +1);
newTriangles.Add(j);
}
//Move left and right marker to the right angle
left.transform.position = new Vector2(Mathf.Cos((angle + arcAngle) * Mathf.Deg2Rad), Mathf.Sin((angle + arcAngle) * Mathf.Deg2Rad));
right.transform.position = new Vector2(Mathf.Cos((angle - arcAngle) * Mathf.Deg2Rad), Mathf.Sin((angle - arcAngle) * Mathf.Deg2Rad));
// Extract a new Mesh instance and automatically assign it to the MeshFilter component.
mesh = GetComponent<MeshFilter>().mesh;
// Clear the Mesh to reset its data before assigning new values.
mesh.Clear();
// Update the Mesh data.
mesh.vertices = newVertices.ToArray();
mesh.uv = uvs.ToArray();
mesh.triangles = newTriangles.ToArray();
// After updating the Mesh data, recalculate the normals.
// If the Mesh uses shaders with normal maps, also call RecalculateTangents.
mesh.RecalculateNormals();
}
private void Update()
{
//newVertices[0] = obj.transform.position;
obj.transform.rotation = Quaternion.identity;
angle = lightHolder.transform.rotation.eulerAngles.z;
float angleHolder = 0;
//Find the right vertices and assign them in newVertices.
for (var i = 1; i < newVertices.Count(); i++)
{
newVertices[i] = FindVertex(new Vector2(Mathf.Cos((angle - arcAngle + angleHolder) * Mathf.Deg2Rad), Mathf.Sin((angle - arcAngle + angleHolder) * Mathf.Deg2Rad)), obj.transform.position);
angleHolder += 2 * arcAngle / nTriangles;
}
// Clear the Mesh to reset its data before assigning new values.
mesh.Clear();
// Update the Mesh data.
mesh.vertices = newVertices.ToArray();
mesh.uv = uvs.ToArray();
mesh.triangles = newTriangles.ToArray();
// After updating the Mesh data, recalculate the normals.
// If the Mesh uses shaders with normal maps, also call RecalculateTangents.
mesh.RecalculateNormals();
}
//Find a vertex by raycast
Vector3 FindVertex(Vector2 direction, Vector2 startPos)
{
RaycastHit2D hit = Physics2D.Raycast(startPos, direction, Mathf.Infinity, wallLayer);
//Instantiate(obj1, hit.point, Quaternion.identity);
return new Vector3(hit.point.x, hit.point.y, 0) - obj.transform.position;
}
}
This is the script for the mesh
Sprite renderers are 3D meshes too, and inversely mesh renderers can use sprite shaders
Alternatively when using a sprite renderer you could utilize Sprite.OverrideGeometry to use your procedural geometry instead of sprite geometry
If you want to use a mesh renderer with sprite shaders, you'd be setting sorting order and mask interaction manually
The first alternative seems convenient for this type of use case
The second maybe, but I'm not sure how those two would be set
Sorting order I believe is a system to calculate render queue amongst sprite renderers that specify sorting order/layer
Mask interaction maybe sets stencil ID via a material property block but just guessing
Sorting Group component can set the sorting order for any renderer with a transparent material, which is what I've gotten by with when using meshes between sprites
And if you want actionable opinions or ideas in time, provide details and examples in your question first