Skip to content

Instantly share code, notes, and snippets.

@FleshMobProductions
Last active March 3, 2022 07:38
Show Gist options
  • Save FleshMobProductions/38518779cd35f613cb9a456fe5e82f87 to your computer and use it in GitHub Desktop.
Save FleshMobProductions/38518779cd35f613cb9a456fe5e82f87 to your computer and use it in GitHub Desktop.
2D Edgewave shader - Shader for the Unity Builtin Render Pipeline to cause surface waves on the edges for the local x and y positions of a mesh
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(MeshRenderer))]
[RequireComponent(typeof(MeshFilter))]
public class CirceMeshBuilder : MonoBehaviour
{
[SerializeField] private Material materialTemplate;
[SerializeField] private bool createMaterialCopy;
[Range(0.01f, 10f)]
[SerializeField] private float circleRadius = 1f;
[Range(10,3600)]
[SerializeField] private int edgeSegments = 360;
public Material MaterialInstance { get; private set; }
private MeshRenderer meshRenderer;
private MeshFilter meshFilter;
private void Awake()
{
MaterialInstance = createMaterialCopy ? new Material(materialTemplate) : materialTemplate;
meshRenderer = GetComponent<MeshRenderer>();
meshRenderer.material = MaterialInstance;
meshFilter = GetComponent<MeshFilter>();
}
private void Start()
{
Mesh circleMesh = CreateCircleMesh();
meshFilter.mesh = circleMesh;
}
private Mesh CreateCircleMesh()
{
Mesh mesh = new Mesh();
// We start with a vertex in the center at 0,0,0. Faces for the vertices
// need to be created clockwise relative to the view direction. Every face will connect to the
// center vertex. The triangle indices have to be populated this way
Vector3[] vertices = new Vector3[1 + edgeSegments];
Vector3[] normals = new Vector3[vertices.Length];
Vector2[] uvs = new Vector2[vertices.Length];
int[] triangles = new int[edgeSegments * 3];
vertices[0] = Vector3.zero;
uvs[0] = new Vector2(0.5f, 0.5f);
float radiansPerRotation = Mathf.Deg2Rad * 360f;
for (int i = 0; i < edgeSegments; i++)
{
int vertId = i + 1;
float radians = (i / (float)edgeSegments) * radiansPerRotation;
Vector3 angleVector = AngleInRadiansToVector(radians);
vertices[vertId] = AngleInRadiansToVector(radians) * circleRadius;
triangles[i * 3] = 0;
triangles[i * 3 + 1] = vertId;
// If vertId + 1 exceeds the available vertex array indices, we have to connect to the first edge vert again
// which has the index 1 (because 0 is already occupied with the 0 position)
triangles[i * 3 + 2] = vertId + 1 < vertices.Length ? vertId + 1 : 1;
// Get uvs in the 0-1 range with 0.5 being the center of the circle
uvs[vertId] = new Vector2(angleVector.x * 0.5f + 0.5f, angleVector.y * 0.5f + 0.5f);
}
Vector3 normalDirection = new Vector3(0, 0, -1);
for (int i = 0; i < normals.Length; i++)
{
normals[i] = normalDirection;
}
mesh.vertices = vertices;
mesh.normals = normals;
mesh.triangles = triangles;
mesh.uv = uvs;
mesh.RecalculateBounds();
return mesh;
}
private Vector3 AngleInRadiansToVector(float radians)
{
return new Vector3(Mathf.Cos(radians), Mathf.Sin(radians), 0f);
}
// Not necessary, but just to be safe about avoiding ghost instances
private void OnDestroy()
{
if (MaterialInstance != null && MaterialInstance != materialTemplate)
Destroy(MaterialInstance);
}
}
Shader "Unlit/EdgeWave"
{
Properties
{
_Color("Color", Color) = (1,1,1,1)
_MainTex ("Texture", 2D) = "white" {}
[Toggle(_DISABLE_INTERPOLATION)]
_DisableWaveEdgeInterpolation("Disable Wave Edge Interpolation", Float) = 0.0
[Toggle]
_UseVertDistAsWaveUnit("Use vertex center distance as Wave Unit?", Float) = 0.0
// https://gist.github.com/smkplus/2a5899bf415e2b6bf6a59726bb1ae2ec
// Each option will set _WAVECOMBINATIONSTRATEGY_ADD, _WAVECOMBINATIONSTRATEGY_MIN, _WAVECOMBINATIONSTRATEGY_MAX shader keywords.
[KeywordEnum(Add, Min, Max)] _WaveCombinationStrategy("Wave Combination Strategy", Float) = 0
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fog
#include "UnityCG.cginc"
#pragma multi_compile_local __ _DISABLE_INTERPOLATION
#pragma multi_compile_local _WAVECOMBINATIONSTRATEGY_ADD _WAVECOMBINATIONSTRATEGY_MIN _WAVECOMBINATIONSTRATEGY_MAX
#define MAX_LOOP_COUNT 10
#define DEG2RAD(degrees) (degrees * 0.01745329252)
#define RAD2DEG(radians) (radians * 57.29577951308232)
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;
float4 _Color;
// It is not necessary to declare the underlying toggle vars as shader variables
// if they are not used by the shader
float _UseVertDistAsWaveUnit;
int _WaveCount;
float4 _WaveAttributesSet1[MAX_LOOP_COUNT];
float4 _WaveAttributesSet2[MAX_LOOP_COUNT];
inline float2 degreesToVector(float degrees)
{
float radians = DEG2RAD(degrees);
return float2(cos(radians), sin(radians));
}
// TODO: one issue remains, the output seems to never be negative right now, so the edge of the circle
// only grows bigger but won't grow smaller
inline float3 waveAdjustVert(float3 localPosition)
{
float time = _Time.y;
float dist = length(localPosition);
float3 positionNormalized = dist >= 0.0001 ? localPosition / dist : float3(0,0,0);
float waveDeltaSum = 0;
int loopIterations = min(_WaveCount, MAX_LOOP_COUNT);
for (int i = 0; i < loopIterations; i++)
{
float4 waveData = _WaveAttributesSet1[i];
float startAngle = waveData.x;
float frequency = waveData.y;
float magnitudeMul = waveData.z;
float angularSpeed = waveData.w;
float edgeTravelAngleSpeed = _WaveAttributesSet2[i].x;
float directionAlignmentMin = _WaveAttributesSet2[i].y;
float radiusOffset = _WaveAttributesSet2[i].z;
float edgeAngle = startAngle + time * edgeTravelAngleSpeed;
float2 angleVec = degreesToVector(edgeAngle);
float directionalAlignment = dot(positionNormalized, angleVec);
// Get a value between 0 and 1
#if defined(_DISABLE_INTERPOLATION)
// Shift directionalAlignment so that directionAlignmentMin is the 0 value, then get the sign which gives
// either 1 or -1 and map it to the 0-1 range
float intensity = sign(directionalAlignment - directionAlignmentMin) * 0.5 + 0.5;
#else
// A bad inverse lerp from directionAlignmentMin to 1, where directionalAlignment is the inverse lerp weight
float intensity = saturate((directionalAlignment - directionAlignmentMin) / (1.0 - directionAlignmentMin));
#endif
// Take the angle of the current vertex position into account
float waveAngleRad = startAngle + atan2(positionNormalized.y, positionNormalized.x) * frequency;
float waveDelta = intensity * (sin(waveAngleRad + DEG2RAD(time * angularSpeed)) * magnitudeMul + radiusOffset);
#if defined(_WAVECOMBINATIONSTRATEGY_MAX)
waveDeltaSum = max(waveDeltaSum, waveDelta);
#elif defined(_WAVECOMBINATIONSTRATEGY_MIN)
waveDeltaSum = min(waveDeltaSum, waveDelta);
#else
// TODO: This calculation works for 3d meshes, but makes the circle mesh generated by CircleMeshBuilder.cs disappear. Why?
waveDeltaSum += waveDelta;
#endif
}
float waveUnit = lerp(1.0, dist, _UseVertDistAsWaveUnit);
// Add the waveDelta along the vertex direction
return positionNormalized * (dist + waveUnit * waveDeltaSum);
}
v2f vert (appdata v)
{
v2f o;
// Calculation needs be happen before the vertex is turned from local into clip space
v.vertex.xyz = waveAdjustVert(v.vertex.xyz);
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
{
fixed4 col = tex2D(_MainTex, i.uv) * _Color;
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EdgeWaveShaderAttributes : MonoBehaviour
{
[System.Serializable]
public struct WaveData
{
public float startAngle;
public float frequency;
[Tooltip("Multiplier for the wave unit to determine the offset of the vertex. " +
"The wave unit is either 1 in object space or the processed vertex length of the mesh")]
public float magnitudeMul;
public float angularSpeed;
public float edgeTravelAngleSpeed;
// TODO: could later be improved to be an angle range instead
// Don't allow 1 directly to avoid division by zero in the shader code
[Tooltip("Is only active when the dot product between wave angle and the vertex angle is bigger than this")]
[Range(-1.001f, 0.999f)]
public float directionAlignmentMin;
public float radiusOffset;
public Vector4 DataSet1AsVector4()
{
return new Vector4(startAngle, frequency, magnitudeMul, angularSpeed);
}
public Vector4 DataSet2AsVector4()
{
return new Vector4(edgeTravelAngleSpeed, directionAlignmentMin, radiusOffset, 0f);
}
}
private const int MAX_LOOP_COUNT = 10;
private static readonly int propertyIdWaveCount = Shader.PropertyToID("_WaveCount");
private static readonly int propertyIdWaveAttributesSet1 = Shader.PropertyToID("_WaveAttributesSet1");
private static readonly int propertyIdWaveAttributesSet2 = Shader.PropertyToID("_WaveAttributesSet2");
[SerializeField] private Renderer targetRenderer;
[SerializeField] private bool useSharedMaterial;
[SerializeField] private WaveData[] waveAttributes;
private Material targetMaterial;
private Vector4[] waveDataVectors = new Vector4[MAX_LOOP_COUNT];
void Start()
{
UnityEngine.Assertions.Assert.IsNotNull(targetRenderer);
if (targetRenderer)
{
targetMaterial = useSharedMaterial ? targetRenderer.sharedMaterial : targetRenderer.material;
SetMaterialProperties();
}
}
[ContextMenu("Update Material Properties")]
private void SetMaterialProperties()
{
if (!targetMaterial) return;
targetMaterial.SetInt(propertyIdWaveCount, waveAttributes.Length);
for (int i = 0; i < waveAttributes.Length; i++)
{
waveDataVectors[i] = waveAttributes[i].DataSet1AsVector4();
}
targetMaterial.SetVectorArray(propertyIdWaveAttributesSet1, waveDataVectors);
for (int i = 0; i < waveAttributes.Length; i++)
{
waveDataVectors[i] = waveAttributes[i].DataSet2AsVector4();
}
targetMaterial.SetVectorArray(propertyIdWaveAttributesSet2, waveDataVectors);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment