Last active
March 3, 2022 07:38
-
-
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
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; | |
[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); | |
} | |
} |
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
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 | |
} | |
} | |
} |
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; | |
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