Skip to content

Instantly share code, notes, and snippets.

@Jerdak
Last active December 19, 2015 20:49
Show Gist options
  • Save Jerdak/6015609 to your computer and use it in GitHub Desktop.
Save Jerdak/6015609 to your computer and use it in GitHub Desktop.
Unity multiple echo shader. Uses floating point -> pixel packing to support transmitting data from Unity component to the shader through a texture rather than as properties.
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
[Serializable]
public class EchoSphere2 {
public enum ShaderPackingMode { Texture, Property };
public ShaderPackingMode CurrentPackingMode = ShaderPackingMode.Texture;
public Texture2D EchoTexture;
public Material EchoMaterial = null;
public Vector3 Position;
public int SphereIndex = 0;
// Echo sphere Properties
public float SphereMaxRadius = 10.0f; //Final size of the echo sphere.
private float sphereCurrentRadius = 0.0f; //Current size of the echo sphere
public float FadeDelay = 0.0f; //Time to delay before triggering fade.
public float FadeRate = 1.0f; //Speed of the fade away
public float echoSpeed = 1.0f; //Speed of the sphere growth.
public bool is_manual = false; //Is pulse manual. if true, pulse triggered by left-mouse click
private bool is_animated = false; //If true, pulse is currently running.
public float pulse_frequency = 5.0f;
private float deltaTime = 0.0f;
private float fade = 0.0f;
public EchoSphere2(){}
// Update is called once per frame
public void Update () {
if(EchoMaterial == null)return;
// If manual selection is disabled, automatically trigger a pulse at the given freq.
deltaTime += Time.deltaTime;
UpdateEcho();
if(CurrentPackingMode == ShaderPackingMode.Texture)UpdateTexture();
if(CurrentPackingMode == ShaderPackingMode.Property)UpdateProperties();
}
// Called to trigger an echo pulse
public void TriggerPulse(){
deltaTime = 0.0f;
sphereCurrentRadius = 0.0f;
fade = 0.0f;
is_animated = true;
}
// Called to halt an echo pulse.
void HaltPulse(){
Debug.Log("HaltPulse reached");
is_animated = false;
}
void ClearPulse(){
fade = 0.0f;
sphereCurrentRadius = 0.0f;
is_animated = false;
}
void UpdateProperties(){
if(!is_animated)return;
float maxRadius = SphereMaxRadius;
float maxFade = SphereMaxRadius / echoSpeed;
Debug.Log("Updating _Position"+SphereIndex.ToString());
EchoMaterial.SetVector("_Position"+SphereIndex.ToString(),Position);
EchoMaterial.SetFloat("_Radius"+SphereIndex.ToString(),sphereCurrentRadius);
EchoMaterial.SetFloat("_Fade"+SphereIndex.ToString(),fade);
EchoMaterial.SetFloat("_MaxRadius",maxRadius);
EchoMaterial.SetFloat("_MaxFade",maxFade);
}
void UpdateTexture(){
if(!is_animated)return;
float maxRadius = SphereMaxRadius;
float maxFade = SphereMaxRadius / echoSpeed;
EchoTexture.SetPixel(SphereIndex,0,FloatPacking.ToColor(Position.x));
EchoTexture.SetPixel(SphereIndex,1,FloatPacking.ToColor(Position.y));
EchoTexture.SetPixel(SphereIndex,2,FloatPacking.ToColor(Position.z));
EchoTexture.SetPixel(SphereIndex,3,FloatPacking.ToColor(sphereCurrentRadius));
EchoTexture.SetPixel(SphereIndex,4,FloatPacking.ToColor(fade));
EchoTexture.Apply();
EchoMaterial.SetFloat("_MaxRadius",maxRadius);
EchoMaterial.SetFloat("_MaxFade",maxFade);
}
// Called to update the echo front edge
void UpdateEcho(){
if(!is_animated)return;
if(sphereCurrentRadius >= SphereMaxRadius){
HaltPulse();
} else {
sphereCurrentRadius += Time.deltaTime * echoSpeed;
}
float radius = sphereCurrentRadius;
float maxRadius = SphereMaxRadius;
float maxFade = SphereMaxRadius / echoSpeed;
if(fade > maxFade){
return;
}
if(deltaTime > FadeDelay)
fade += Time.deltaTime * FadeRate;
}
}
public class EchoSpheres : MonoBehaviour {
public EchoSphere2.ShaderPackingMode CurrentPackingMode = EchoSphere2.ShaderPackingMode.Texture;
public Texture2D EchoTexture;
public Material EchoMaterial = null;
public int SphereCount = 1;
public int CurrentSphere = 0;
// Echo sphere Properties
public float SphereMaxRadius = 10.0f; //Final size of the echo sphere.
public float FadeDelay = 0.0f; //Time to delay before triggering fade.
public float FadeRate = 1.0f; //Speed of the fade away
public float echoSpeed = 1.0f; //Speed of the sphere growth.
private List<EchoSphere2> Spheres = new List<EchoSphere2>();
// Use this for initialization
void Start () {
CreateEchoTexture();
InitializeSpheres();
}
void InitializeSpheres(){
for(int i = 0; i < SphereCount; i++){
EchoSphere2 es = new EchoSphere2{
EchoMaterial = EchoMaterial,
EchoTexture = EchoTexture,
echoSpeed = echoSpeed,
SphereMaxRadius = SphereMaxRadius,
FadeDelay = FadeDelay,
FadeRate = FadeRate,
SphereIndex = i,
CurrentPackingMode = CurrentPackingMode
};
Spheres.Add(es);
}
}
/// <summary>
/// Create an echo texture used to hold multiple echo sources and fades.
/// </summary>
void CreateEchoTexture(){
EchoTexture = new Texture2D(128,128,TextureFormat.RGBA32,false);
EchoTexture.filterMode = FilterMode.Point;
EchoTexture.Apply();
EchoMaterial.SetTexture("_EchoTex",EchoTexture);
}
// Update is called once per frame
void Update () {
if(EchoMaterial == null)return;
foreach (EchoSphere2 es in Spheres){
es.Update();
}
UpdateRayCast();
}
// Called to manually place echo pulse
void UpdateRayCast() {
if (Input.GetButtonDown("Fire1")){
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray,out hit, 10000)) {
Debug.Log("Triggering pulse["+CurrentSphere.ToString()+"]");
Spheres[CurrentSphere].TriggerPulse();
Spheres[CurrentSphere].Position = hit.point;
CurrentSphere += 1;
if(CurrentSphere >= Spheres.Count)CurrentSphere = 0;
}
}
}
}
using UnityEngine;
using System;
using System.Collections;
/// <summary>
/// Float packing class handles
/// </summary>
/// <notes>
/// step,exp2,abs,floor,mod,log2 methods are included here to mimic shader
/// functionality. Some methods were copied from Nvidia's Cg reference
/// implementations, others are just wrappers around C# functions.
///
/// In practice I don't think it matters.
///
/// Code taken directly from: http://stackoverflow.com/questions/7059962/how-do-i-convert-a-vec4-rgba-value-to-a-float (hrehfeld's answer)
/// </notes>
///
public static class FloatPacking {
public static float step(float edge, float x) {
return (x < edge)?0.0f:1.0f;
}
public static float exp2(float x){
return (float)Math.Pow(2,x);
}
public static float abs(float x){
return (float)Math.Abs(x);
}
public static int floor(float x){
return (int)Math.Floor(x);
}
public static float mod(float x, float y){
return x - y * floor(x/y);
}
public static float log2(float x){
return (float)Math.Log(x,2);
}
//unpack a 32bit float from 4 8bit, [0;1] clamped floats
public static float FromFloat4( Vector4 _packed)
{
Vector4 rgba = 255.0f * _packed;
float sign = step(-128.0f, -rgba.y) * 2.0f - 1.0f;
float exponent = rgba.x - 127.0f;
if (abs(exponent + 127.0f) < 0.001f)
return 0.0f;
float mantissa = mod(rgba.y, 128.0f) * 65536.0f + rgba.z * 256.0f + rgba.w + (0x800000);
return sign * exp2(exponent-23.0f) * mantissa ;
}
//pack a 32bit float into 4 8bit, [0;1] clamped floats
public static Vector4 ToFloat4(float f)
{
float F = abs(f);
if(F == 0.0)
{
return new Vector4(0,0,0,0);
}
float Sign = step(0.0f, -f);
float Exponent = floor( log2(F));
float Mantissa = F/ exp2(Exponent);
//std::cout << " sign: " << Sign << ", exponent: " << Exponent << ", mantissa: " << Mantissa << std::endl;
//denormalized values if all exponent bits are zero
if(Mantissa < 1.0f)
Exponent -= 1;
Exponent += 127;
Vector4 rgba = new Vector4(0,0,0,0);
rgba.x = Exponent;
rgba.y = 128.0f * Sign + mod(floor(Mantissa * 128.0f),128.0f);
rgba.z = floor( mod(floor(Mantissa* exp2(23.0f - 8.0f)), exp2(8.0f)));
rgba.w = floor( exp2(23.0f)* mod(Mantissa, exp2(-15.0f)));
return (1 / 255.0f) * rgba;
}
public static Color ToColor(float f){
Vector4 tmp = ToFloat4(f);
return new Color(tmp.x,tmp.y,tmp.z,tmp.w);
}
}
Shader "Custom/Echo/MultipleUsingProperty" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_EchoTex ("Echo (RGBA)", 2D) = "white" {}
_MainColor ("Main Color",Color) = (1.0,1.0,1.0,1.0)
_DistanceFade("Distance Fade",float) = 1.0
_MaxRadius("MaxRadius",float) = 1.0
_MaxFade("MaxFade",float) = 1.0
_Position0("Position0",Vector) = (0.0,0.0,0.0)
_Position1("Position1",Vector) = (0.0,0.0,0.0)
_Position2("Position2",Vector) = (0.0,0.0,0.0)
_Radius0("Radius0",float) = 0.0
_Radius1("Radius1",float) = 0.0
_Radius2("Radius2",float) = 0.0
_Fade0("Fade0",float) = 0.0
_Fade1("Fade1",float) = 0.0
_Fade2("Fade2",float) = 0.0
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#pragma target 3.0
#pragma surface surf NoLighting
#include "UnityCG.cginc"
struct Input {
float2 uv_MainTex;
float3 worldPos;
};
sampler2D _MainTex;
sampler2D _EchoTex;
float4 _MainColor;
float _DistanceFade;
float _MaxRadius;
float _MaxFade;
float3 _Position0;
float3 _Position1;
float3 _Position2;
float _Radius0;
float _Radius1;
float _Radius2;
float _Fade0;
float _Fade1;
float _Fade2;
// Custom light model that ignores actual lighting.
half4 LightingNoLighting (SurfaceOutput s, half3 lightDir, half atten) {
half4 c;
c.rgb = s.Albedo;
c.a = s.Alpha;
return c;
}
float ApplyFade(Input IN,float3 position, float radius, float infade){
float size = 128.0; //hardcoded width/height of echo texture
float dist = distance(IN.worldPos, position); // Distance from current pixel (from its world coord) to center of echo sphere
if(radius >= 3*_MaxRadius || dist >= radius){
return 0.0;
} else {
// If _DistanceFade = true, fading is related to vertex distance from echo origin.
// If false, fading is even across entire echo.
float c1 = (_DistanceFade>=1.0)?dist/radius:1.0;
// Apply fading effect.
c1 *= (infade<=_MaxFade)?1.0-infade/_MaxFade:0.0; //adjust by fade distance.
// Ignore Fade values <= 0 (meaning no fade.)
c1 = (infade<=0)?1.0:c1;
return c1;
}
}
// Custom surfacer that mimics an echo effect
void surf (Input IN, inout SurfaceOutput o) {
float c1;
// manually add more echos here.
c1 += ApplyFade(IN,_Position0,_Radius0,_Fade0);
c1 += ApplyFade(IN,_Position1,_Radius1,_Fade1);
c1 += ApplyFade(IN,_Position2,_Radius2,_Fade2);
c1 /= 3.0;
float c2 = 1.0 - c1;
o.Albedo = _MainColor.rgb * c2 + tex2D (_MainTex, IN.uv_MainTex).rgb * c1 ;
}
ENDCG
}
FallBack "Diffuse"
}
Shader "Custom/Echo/MultipleUsingTexture" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_EchoTex ("Echo (RGBA)", 2D) = "white" {}
_MainColor ("Main Color",Color) = (1.0,1.0,1.0,1.0)
_DistanceFade("Distance Fade",float) = 1.0
_MaxRadius("MaxRadius",float) = 1.0
_MaxFade("MaxFade",float) = 1.0
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
// Target Shader Model 3.0 or you won't have enough arithmetic instructions for float unpack
#pragma target 3.0
#pragma surface surf NoLighting
#include "UnityCG.cginc"
struct Input {
float2 uv_MainTex;
float3 worldPos;
};
sampler2D _MainTex;
sampler2D _EchoTex;
float4 _MainColor;
float _DistanceFade;
float _MaxRadius;
float _MaxFade;
// Custom light model that ignores actual lighting.
half4 LightingNoLighting (SurfaceOutput s, half3 lightDir, half atten) {
half4 c;
c.rgb = s.Albedo;
c.a = s.Alpha;
return c;
}
//unpack float from pixel
float unpackFloat4( float4 _packed)
{
float4 rgba = 255.0 * _packed;
float sign = step(-128.0, -rgba[1]) * 2.0 - 1.0;
float exponent = rgba[0] - 127.0;
if (abs(exponent + 127.0) < 0.001){
return 0.0;
} else {
float mantissa = fmod(rgba[1], 128.0) * 65536.0 + rgba[2] * 256.0 + rgba[3] + (0x800000);
return sign * exp2(exponent-23.0) * mantissa ;
}
}
float ApplyFade(Input IN,float textureOffset){
float3 position;
float size = 128.0; //hard coded texture width/height
//NOTE: UV's are [0,1] so divide by the width(or height, they should be equal) of the image.
//In this example the texture is 128x128.
position[0] = unpackFloat4(tex2D(_EchoTex,float2(textureOffset/size,0.0)));
position[1] = unpackFloat4(tex2D(_EchoTex,float2(textureOffset/size,1.0/size)));
position[2] = unpackFloat4(tex2D(_EchoTex,float2(textureOffset/size,2.0/size)));
float radius = unpackFloat4(tex2D(_EchoTex,float2(textureOffset/size,3.0/size)));
float infade = unpackFloat4(tex2D(_EchoTex,float2(textureOffset/size,4.0/size)));
float dist = distance(IN.worldPos, position); // Distance from current pixel (from its world coord) to center of echo sphere
if(radius >= 3*_MaxRadius || dist >= radius){
return 0.0;
} else {
// If _DistanceFade = true, fading is related to vertex distance from echo origin.
// If false, fading is even across entire echo.
float c1 = (_DistanceFade>=1.0)?dist/radius:1.0;
// Apply fading effect.
c1 *= (infade<=_MaxFade)?1.0-infade/_MaxFade:0.0; //adjust by fade distance.
// Ignore Fade values <= 0 (meaning no fade.)
c1 = (infade<=0)?1.0:c1;
return c1;
}
}
// Custom surfacer that mimics an echo effect
void surf (Input IN, inout SurfaceOutput o) {
float c1;
//Unity was not pleased with my use of a for loop. Somewhere
//in the loop unroll Unity blew up and crashed. So echo count
//is hardcoded to be 3 here.
c1 += ApplyFade(IN,0.0);
c1 += ApplyFade(IN,1.0);
c1 += ApplyFade(IN,2.0);
c1 /= 3.0;
float c2 = 1.0 - c1;
o.Albedo = _MainColor.rgb * c2 + tex2D (_MainTex, IN.uv_MainTex).rgb * c1 ;
}
ENDCG
}
FallBack "Diffuse"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment