Skip to content

Instantly share code, notes, and snippets.

@astrellon
Last active February 21, 2022 21:44
Show Gist options
  • Save astrellon/d782870d6e15bcff0a9ea77e9b53ae4a to your computer and use it in GitHub Desktop.
Save astrellon/d782870d6e15bcff0a9ea77e9b53ae4a to your computer and use it in GitHub Desktop.
A shader that contains multiple effects depending on pixel location. Designed for low poly with flat coloured faces.
// The MIT License (MIT)
// Copyright © 2022 Alan Lawrey
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Shader "Custom/TailspinBaseShader"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
[PerRendererData] _Glossiness ("Smoothness", Range(0,1)) = 0.5
[PerRendererData] _MetallicScale ("Metallic Scale", Float) = 1
[PerRendererData] _EmissionScale("Emission Scale", Float) = 0.1
[PerRendererData] _MapScale("Map Scale", Vector) = (5,5,5,1)
[PerRendererData] _Persistance("Persistance", Float) = 1
[PerRendererData] _Roughness("Roughness", Float) = 1
[PerRendererData] _SmoothStep("Smooth Step", Float) = 0.5
[PerRendererData] _SmoothStepRange("Smooth Step Range", Float) = 0.1
[PerRendererData] _TextureMoveVector("Texture Move Vector", Vector) = (1,1,1,1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
// Physically based Standard lighting model, and enable shadows on all light types
#pragma surface surf Standard vertex:vert nodirlightmap nodynlightmap
// Use shader model 3.0 target, to get nicer looking lighting
#pragma target 3.0
// #pragma debug
#define OCTAVES 3
#define APPROX(x, y) abs(x - y) < 0.001
#define SMOOTH 0
#define SMOOTH_METALLIC 1
#define SMOOTH_EMISSIVE 2
#define SMOOTH_METALLIC_EMISSIVE 3
#define FLAT 0
#define SCRATCHES 1
#define SIMPLEX 2
#define SIMPLEX_ANIMATED 3
#define VORONOI 4
#define VORONOI_ANIMATED 5
#define VORONOI_SQUARE 6
#define VORONOI_SQUARE_ANIMATED 7
#define HEXAGONS 8
sampler2D _MainTex;
struct Input
{
half3 localCoord : POSITION;
half3 localNormal : NORMAL;
half3 localTangent : TANGENT;
half2 uv_MainTex : TEXCOORD0;
};
struct GridPosition
{
half fields;
half textureType;
};
half _Glossiness;
half _Persistance;
half _Roughness;
fixed4 _Color;
half3 _MapScale;
half _MetallicScale;
half _EmissionScale;
half4 _TextureMoveVector;
half _SmoothStep;
half _SmoothStepRange;
// Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
// See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
// #pragma instancing_options assumeuniformscaling
UNITY_INSTANCING_BUFFER_START(Props)
// put more per-instance properties here
UNITY_INSTANCING_BUFFER_END(Props)
// Utilities
GridPosition getColourGridLocation(float2 uvInput)
{
half2 grid = frac(uvInput * 8);
half fields = floor(frac(grid.y * 2) * 4);
half type = floor(grid.x * 8) + (floor(grid.y * 2) * 8);
GridPosition result;
result.fields = fields;
result.textureType = type;
return result;
}
// SimplexNoise taken from
// https://github.com/atyuwen/bitangent_noise/blob/main/Develop/SimplexNoise.hlsl
uint pcg3d16(uint3 p)
{
uint3 v = p * 1664525u + 1013904223u;
v.x += v.y*v.z; v.y += v.z*v.x; v.z += v.x*v.y;
v.x += v.y*v.z;
return v.x;
}
// Get random gradient from hash value.
half3 gradient3d(uint hash)
{
half3 g = half3(hash.xxx & uint3(0x80000, 0x40000, 0x20000));
return g * half3(1.0 / 0x40000, 1.0 / 0x20000, 1.0 / 0x10000) - 1.0;
}
// 3D Simplex Noise. Approximately 71 instruction slots used.
// Assume p is in the range [-32768, 32767].
float SimplexNoise3D(float3 p)
{
const float2 C = float2(1.0 / 6.0, 1.0 / 3.0);
const float4 D = float4(0.0, 0.5, 1.0, 2.0);
// First corner
half3 i = floor(p + dot(p, C.yyy));
half3 x0 = p - i + dot(i, C.xxx);
// Other corners
half3 g = step(x0.yzx, x0.xyz);
half3 l = 1.0 - g;
half3 i1 = min(g.xyz, l.zxy);
half3 i2 = max(g.xyz, l.zxy);
// x0 = x0 - 0.0 + 0.0 * C.xxx;
// x1 = x0 - i1 + 1.0 * C.xxx;
// x2 = x0 - i2 + 2.0 * C.xxx;
// x3 = x0 - 1.0 + 3.0 * C.xxx;
half3 x1 = x0 - i1 + C.xxx;
half3 x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y
half3 x3 = x0 - D.yyy; // -1.0+3.0*C.x = -0.5 = -D.y
i = i + 32768.5;
uint hash0 = pcg3d16((uint3)i);
uint hash1 = pcg3d16((uint3)(i + i1));
uint hash2 = pcg3d16((uint3)(i + i2));
uint hash3 = pcg3d16((uint3)(i + 1 ));
half3 p0 = gradient3d(hash0);
half3 p1 = gradient3d(hash1);
half3 p2 = gradient3d(hash2);
half3 p3 = gradient3d(hash3);
// Mix final noise value.
half4 m = saturate(0.5 - half4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)));
half4 mt = m * m;
half4 m4 = mt * mt;
return 62.6 * dot(m4, half4(dot(x0, p0), dot(x1, p1), dot(x2, p2), dot(x3, p3)));
}
float sampleLayeredNoise(float3 value)
{
float noise = 0;
float frequency = 1;
float factor = 1;
[unroll]
for(int i=0; i<OCTAVES; i++){
noise = noise + SimplexNoise3D(value * frequency + i * 0.72354) * factor;
factor *= _Persistance;
frequency *= _Roughness;
}
return noise;
}
// Modified version from this shader
// https://www.shadertoy.com/view/XlXBWj
half3 Hash3( half3 p ) { return frac(sin(half3( dot(p,half3(127.1,311.7,786.6)), dot(p,half3(269.5,183.3,455.8)), dot(p,half3(419.2,371.9,948.6))))*43758.5453); }
half Voronoi(half3 p)
{
half3 n = floor(p);
half3 f = frac(p);
half shortestDistance = 1.0;
for (int x = -1; x < 1; x++) {
for (int y = -1; y < 1; y++) {
for (int z = -1; z < 1; z++) {
half3 o = half3(x,y,z);
half3 r = (o - f) + 1.0 + sin(Hash3(n + o)*50.0)*0.2;
half d = dot(r,r);
if (d < shortestDistance) {
shortestDistance = d;
}
}
}
}
return shortestDistance;
}
half VoronoiSquare(half3 p)
{
half3 n = floor(p);
half3 f = frac(p);
half shortestDistance = 1.0;
for (int x = -1; x < 1; x++) {
for (int y = -1; y < 1; y++) {
for (int z = -1; z < 1; z++) {
half3 o = half3(x,y,z);
half3 r = (o - f) + 1.0 + sin(Hash3(n + o)*50.0)*0.2;
half d = max(abs(r.x), max(abs(r.y), abs(r.z))) * 0.8;
if (d < shortestDistance) {
shortestDistance = d;
}
}
}
}
return shortestDistance;
}
half FractalVoronoi(half3 p)
{
half n = 0.0;
half f = 0.5, a = 0.5;
half2x2 m = half2x2(0.8, 0.6, -0.6, 0.8);
for (int i = 0; i < OCTAVES; i++) {
n += Voronoi(p * f) * a;
f *= _Roughness;
a *= _Persistance;
p.xy = mul(m, p.xy);
}
return n * 1.5;
}
half FractalVoronoiSquare(half3 p)
{
half n = 0.0;
half f = 0.5, a = 0.5;
half2x2 m = half2x2(0.8, 0.6, -0.6, 0.8);
for (int i = 0; i < 3; i++) {
n += VoronoiSquare(p * f) * a;
f *= _Roughness;
a *= _Persistance;
p.xy = mul(m, p.xy);
}
return n * 1.2;
}
// https://www.shadertoy.com/view/Md2GDz
float hex(half2 p) {
p.x *= 0.57735*2.0;
p.y += fmod(floor(p.x), 2.0)*0.5;
p = abs((fmod(p, 1.0) - 0.5));
return abs(max(p.x*1.5 + p.y, p.y*2.0) - 1.0);
}
void vert(inout appdata_tan v, out Input data)
{
UNITY_INITIALIZE_OUTPUT(Input, data);
data.localCoord = v.vertex.xyz;
data.localNormal = v.normal;
data.localTangent = v.tangent;
}
void surf (Input IN, inout SurfaceOutputStandard o)
{
// Albedo comes from a texture tinted by color
half2 uv = IN.uv_MainTex;
fixed4 c = tex2D (_MainTex, uv) * _Color;
o.Albedo = c.rgb;
o.Alpha = c.a;
GridPosition gridPosition = getColourGridLocation(uv);
half4 textureMoveVector = UNITY_ACCESS_INSTANCED_PROP(Props, _TextureMoveVector);
half3 textureOffset = textureMoveVector.xyz;
if (textureMoveVector.w > 0)
{
textureOffset *= (IN.localTangent + IN.localNormal);
}
half textureValue = 1;
half smoothStep = UNITY_ACCESS_INSTANCED_PROP(Props, _SmoothStep);
if (APPROX(gridPosition.textureType, FLAT))
{
gridPosition.fields -= 1;
}
if (APPROX(gridPosition.textureType, SCRATCHES))
{
textureValue = sampleLayeredNoise(IN.localCoord * _MapScale * 10);
smoothStep *= 2.5;
}
if (APPROX(gridPosition.textureType, SIMPLEX))
{
textureValue = sampleLayeredNoise(IN.localCoord * _MapScale);
}
else if (APPROX(gridPosition.textureType, SIMPLEX_ANIMATED))
{
textureValue = sampleLayeredNoise(IN.localCoord * _MapScale + textureOffset);
}
else if (APPROX(gridPosition.textureType, VORONOI))
{
textureValue = 1 - FractalVoronoi(IN.localCoord * _MapScale);
}
else if (APPROX(gridPosition.textureType, VORONOI_ANIMATED))
{
textureValue = 1 - FractalVoronoi(IN.localCoord * _MapScale + textureOffset);
}
else if (APPROX(gridPosition.textureType, VORONOI_SQUARE))
{
textureValue = 1 - FractalVoronoiSquare(IN.localCoord * _MapScale);
}
else if (APPROX(gridPosition.textureType, VORONOI_SQUARE_ANIMATED))
{
textureValue = 1 - FractalVoronoiSquare(IN.localCoord * _MapScale + textureOffset);
}
else if (APPROX(gridPosition.textureType, HEXAGONS))
{
half xyDot = abs(dot(IN.localNormal, half3(0, 0, 1)));
half xzDot = abs(dot(IN.localNormal, half3(0, 1, 0)));
half yzDot = abs(dot(IN.localNormal, half3(1, 0, 0)));
half3 inputPos = IN.localCoord * _MapScale;
half2 pos = inputPos.yz;
if (xyDot > xzDot && xyDot > yzDot)
{
pos = inputPos.xy;
}
else if (xzDot > xyDot && xzDot > yzDot)
{
pos = inputPos.xz;
}
textureValue = hex(abs(pos));
}
half smoothStepRange = UNITY_ACCESS_INSTANCED_PROP(Props, _SmoothStepRange);
half smoothTexture = smoothstep(smoothStep - smoothStepRange, smoothStep + smoothStepRange, textureValue);
if (APPROX(gridPosition.fields, SMOOTH))
{
o.Smoothness = _Glossiness * smoothTexture;
}
if (APPROX(gridPosition.fields, SMOOTH_METALLIC))
{
o.Smoothness = _Glossiness * (1 - smoothTexture);
o.Metallic = _MetallicScale * smoothTexture;
}
if (APPROX(gridPosition.fields, SMOOTH_EMISSIVE))
{
o.Smoothness = _Glossiness * smoothTexture;
o.Emission = c.rgb * UNITY_ACCESS_INSTANCED_PROP(Props, _EmissionScale) * smoothTexture;
}
if (APPROX(gridPosition.fields, SMOOTH_METALLIC_EMISSIVE))
{
o.Metallic = _MetallicScale;
o.Smoothness = _Glossiness;
o.Emission = c.rgb * UNITY_ACCESS_INSTANCED_PROP(Props, _EmissionScale) * smoothTexture;
}
}
ENDCG
}
FallBack "Diffuse"
}
// The MIT License (MIT)
// Copyright © 2022 Alan Lawrey
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Tailspin
{
[ExecuteInEditMode]
public class TailspinMaterial : MonoBehaviour
{
[System.Flags]
private enum DirtyFlag
{
None = 0x0000,
Smoothness = 0x0001,
MetallicScale = 0x0002,
EmissionScale = 0x0004,
Persistance = 0x0008,
Roughness = 0x10,
SmoothStep = 0x20,
SmoothStepRange = 0x40,
MapScale = 0x80,
TextureMove = 0x100,
All = 0x7FFFFFFF
}
private const string SmoothnessProp = "_Glossiness";
private const string MetallicScaleProp = "_MetallicScale";
private const string EmissionScaleProp = "_EmissionScale";
private const string TextureMoveProp = "_TextureMoveVector";
private const string PersistanceProp = "_Persistance";
private const string RoughnessProp = "_Roughness";
private const string SmoothStepProp = "_SmoothStep";
private const string SmoothStepRangeProp = "_SmoothStepRange";
private const string MapScaleProp = "_MapScale";
public MeshRenderer Renderer;
public Vector4 TextureMove = new Vector4(1, 1, 1, 0);
public float TextureMoveSpeed = 0.0f;
public float Smoothness = 0.25f;
public float MetallicScale = 0.25f;
public float EmissionScale;
public float Persistance = 1.0f;
public float Roughness = 3.0f;
public float SmoothStep = 0.5f;
public float SmoothStepRange = 0.05f;
public Vector3 MapScale = new Vector3(4, 4, 4);
private DirtyFlag dirty = DirtyFlag.All;
private Vector4 currentTextureOffset = Vector3.zero;
private MaterialPropertyBlock _propertyBlock;
private MaterialPropertyBlock propertyBlock
{
get
{
if (this._propertyBlock == null)
{
this._propertyBlock = new MaterialPropertyBlock();
if (this.Renderer == null)
{
this.Renderer = this.GetComponent<MeshRenderer>();
}
this.Renderer.GetPropertyBlock(this.propertyBlock);
}
return this._propertyBlock;
}
}
// Start is called before the first frame update
void Awake()
{
if (this.Renderer == null)
{
this.Renderer = this.GetComponent<MeshRenderer>();
}
}
// Update is called once per frame
void Update()
{
if (this.TextureMove != Vector4.zero && !Mathf.Approximately(this.TextureMoveSpeed, 0.0f))
{
this.currentTextureOffset = (Vector3)this.currentTextureOffset + (Vector3)this.TextureMove * this.TextureMoveSpeed * Time.smoothDeltaTime;
this.currentTextureOffset.w = this.TextureMove.w;
this.dirty |= DirtyFlag.TextureMove;
this.propertyBlock.SetVector(TextureMoveProp, this.currentTextureOffset);
}
if (this.dirty != DirtyFlag.None)
{
this.SetFloat(DirtyFlag.Smoothness, SmoothnessProp, this.Smoothness);
this.SetFloat(DirtyFlag.MetallicScale, MetallicScaleProp, this.MetallicScale);
this.SetFloat(DirtyFlag.EmissionScale, EmissionScaleProp, this.EmissionScale);
this.SetFloat(DirtyFlag.Persistance, PersistanceProp, this.Persistance);
this.SetFloat(DirtyFlag.Roughness, RoughnessProp, this.Roughness);
this.SetFloat(DirtyFlag.SmoothStep, SmoothStepProp, this.SmoothStep);
this.SetFloat(DirtyFlag.SmoothStepRange, SmoothStepRangeProp, this.SmoothStepRange);
this.SetVector(DirtyFlag.MapScale, MapScaleProp, this.MapScale);
this.Renderer.SetPropertyBlock(this.propertyBlock);
this.dirty = DirtyFlag.None;
}
}
void OnValidate()
{
this.dirty = DirtyFlag.All;
}
private void SetFloat(DirtyFlag checkFlag, string prop, float value)
{
if (this.dirty.HasFlag(checkFlag))
{
this.propertyBlock.SetFloat(prop, value);
}
}
private void SetVector(DirtyFlag checkFlag, string prop, Vector3 value)
{
if (this.dirty.HasFlag(checkFlag))
{
this.propertyBlock.SetVector(prop, value);
}
}
public void SetSmoothness(float value)
{
if (this.Smoothness != value)
{
this.Smoothness = value;
this.dirty |= DirtyFlag.Smoothness;
}
}
public void SetMetallicScale(float value)
{
if (this.MetallicScale != value)
{
this.MetallicScale = value;
this.dirty |= DirtyFlag.MetallicScale;
}
}
public void SetEmissionScale(float value)
{
if (this.EmissionScale != value)
{
this.EmissionScale = value;
this.dirty |= DirtyFlag.EmissionScale;
}
}
public void SetTextureMoveSpeed(float value)
{
this.TextureMoveSpeed = value;
}
public void SetMapScale(float value)
{
this.MapScale = new Vector3(value, value, value);
this.dirty |= DirtyFlag.MapScale;
}
public void SetPersistance(float value)
{
if (this.Persistance != value)
{
this.Persistance = value;
this.dirty |= DirtyFlag.Persistance;
}
}
public void SetRoughness(float value)
{
if (this.Roughness != value)
{
this.Roughness = value;
this.dirty |= DirtyFlag.Roughness;
}
}
public void SetSmoothStep(float value)
{
if (this.SmoothStep != value)
{
this.SmoothStep = value;
this.dirty |= DirtyFlag.SmoothStep;
}
}
public void SetSmoothStepRange(float value)
{
if (this.SmoothStepRange != value)
{
this.SmoothStepRange = value;
this.dirty |= DirtyFlag.SmoothStepRange;
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment