Goniometer.shader

// Copyright (C) 2026 kubectl
//
// This work is licensed under the Creative Commons
// Attribution-NonCommercial-ShareAlike 4.0 International License.
// To view a copy of this license, visit
// http://creativecommons.org
//
// Goniometer: https://en.wikipedia.org/wiki/Goniometer_%28audio%29
// Revision: 0.0.2
//
// Inspired by:
// - https://www.youtube.com/watch?v=9gjw0ewojgI : Aloboi - All 'Raw Tracks' Instrumentals
// - https://www.youtube.com/watch?v=6DifrkaALOg : BUS ERROR Collective - Primer
//
// Requirements:
// - AudioLink 3.1.2 : https://github.com/llealloo/audiolink
//
// Usage:
// - Create CRT & Material with this Shader
// - Set Dimension to `2D`
// - Set Color Format to `R16G16B16A16_SFLOAT`
// - Add Material to CRT (also in `Initlaization Mode`, set `Source` to `Material`)
// - Set `Update Mode` to `Realtime`
// - Set `Period` to 0
// - Enable `Double Buffered`
//
// Enjoy :]
//
// Changelog:
// R0.0.2:
// - fixed moving average for odd window sizes
// - removed dependecy on CRTStandart2D.cginc
// - allow glow to bleed past line segment sdf
// - added NaN & zero guard to `distance_to_line`
// - add loop unrolling annotation
// - recommend float16 texture
// - proper int range for _Sample & _SmoothWindow
// - added frametime dependant delay
// - organized header sections
Shader "kubectl/Goniometer"
{
Properties
{
[Header(Trail)]
// Can be set to zero on non-8bit CRTs
_DecayFloor ("Decay Floor", Range(0.0, 0.05)) = 0.04
_Decay ("Line Decay", Range(0.0, 1.0)) = 0.3
[Header(Line)]
_Color ("Line Color", Color) = (0, 1, 0, 1)
_DotSize ("Line Thickness", Range(0.001, 0.1)) = 0.008
_Exposure ("Exposure (Tone mapping)", Range(0.5, 10.0)) = 1.5
[Header(Glow)]
_GlowColor ("Glow Color", Color) = (1, 1, 1, 1)
_GlowSize ("Glow Size", Range(0.001, 1)) = 0.5
[Header(Geometry)]
_EllipseAspect ("Ellipse Aspect", Range(0.001, 1.0)) = 0.4
_Rotation ("Rotation (Degrees)", Range(-180, 180)) = 0
[Header(Signal)]
_Gain ("Manual Gain", Range(0.1, 20)) = 0.7
[IntRange] _Samples ("Sample Count", Range(64, 2048)) = 512
[Header(Smoothing)]
[KeywordEnum(None, IIR, Window)] _Smooth ("Smoothing Mode", Float) = 0
_SmoothAmount ("IIR Smooth Amount", Range(0.0, 0.95)) = 0.3
[IntRange] _SmoothWindow ("Box Window Size", Range(1, 9)) = 3
}
SubShader
{
Tags
{
"RenderType" = "Opaque"
"PreviewType" = "Skybox"
}
Blend One Zero
Lighting Off
ZTest Always
ZWrite Off
Pass
{
Name "AudioLink Goniometer"
CGPROGRAM
#pragma vertex CustomRenderTextureVertexShader
#pragma fragment frag
#pragma shader_feature_local _SMOOTH_NONE _SMOOTH_IIR _SMOOTH_WINDOW
#include "UnityCustomRenderTexture.cginc"
#include "Packages/com.llealloo.audiolink/Runtime/Shaders/AudioLink.cginc"
float4 _Color;
float4 _GlowColor;
float _Exposure;
float _Decay;
float _DotSize;
float _DecayFloor;
float _Rotation;
float _EllipseAspect;
float _GlowSize;
float _Gain;
int _Samples;
float _SmoothAmount;
int _SmoothWindow;
float2 rotate_2d(float2 p, float degrees)
{
float rad = radians(degrees);
float s, c;
sincos(rad, s, c);
return float2(p.x * c - p.y * s, p.x * s + p.y * c);
}
// https://iquilezles.org/articles/distfunctions2d
float distance_to_line( float2 p, float2 a, float2 b )
{
float2 pa = p-a, ba = b-a;
float h = clamp(dot(pa, ba) / max(dot(ba, ba), 1e-9), 0.0, 1.0);
return length( pa - ba*h );
}
// https://github.com/llealloo/audiolink/tree/master/Docs#alpass_waveform
float2 get_diff_audio(float sample_index)
{
sample_index = fmod(sample_index, AUDIOLINK_SAMPLEDATA24);
if (sample_index < 0) sample_index += AUDIOLINK_SAMPLEDATA24;
float4 waveform_sample = AudioLinkLerpMultiline(
ALPASS_WAVEFORM + float2( sample_index, 0 ) );
float left = (waveform_sample.r + waveform_sample.a) * _Gain;
float right = (waveform_sample.r - waveform_sample.a) * _Gain;
return -float2(left, right);
}
#ifdef _SMOOTH_WINDOW
// Moving Average: https://en.wikipedia.org/wiki/Moving_average
float2 get_diff_audio_smoothed_ma(float sample_index, int window_size)
{
float2 acc = 0;
int half_window = window_size / 2;
for (int w = -half_window; w <= half_window; w++)
{
acc += get_diff_audio(sample_index + w);
}
return acc / ( 2 * half_window + 1);
}
#endif
float4 frag(v2f_customrendertexture i) : SV_Target
{
float2 sample_uv = i.localTexcoord.xy;
float2 uv = sample_uv * 2.0 - 1.0;
// gived decay value is scaled to 60fps
float frame_scale = unity_DeltaTime.z * 60.0;
float decay = pow(_Decay, frame_scale);
float decay_floor = _DecayFloor * frame_scale;
// decay previous frame
float3 trail = tex2D(_SelfTexture2D, sample_uv).rgb * decay - decay_floor;
trail = max(trail, 0.0);
// how many samples to read from the audiolink texture per frame
// anyhting above 512 starts to get pretty expensive
int samples = max(_Samples, 2);
float3 line_color = 0;
// scaling the uv is cheaper than calculating an actual ellipse
float2 uv_scaled = uv;
uv_scaled.x /= _EllipseAspect;
float2 prev_pos = 0;
#ifdef _SMOOTH_IIR
// IIR filter: https://en.wikipedia.org/wiki/Infinite_impulse_response
float2 prevSmoothed = get_diff_audio(0);
#endif
// emperical value, may need tuning
float glow_size_scaled = _GlowSize / 500;
[loop]
for (int s = 0; s < samples; s++)
{
#if defined(_SMOOTH_WINDOW)
float2 pos = get_diff_audio_smoothed_ma((float)s, _SmoothWindow);
#elif defined(_SMOOTH_IIR)
// lerp between current and previous value
float2 pos = lerp(get_diff_audio((float)s), prevSmoothed, _SmoothAmount);
prevSmoothed = pos;
#else
float2 pos = get_diff_audio((float)s);
#endif
pos = rotate_2d(pos, _Rotation);
pos.x /= _EllipseAspect; // "unsquish" by squishing to the same space
if (s > 0)
{
float d = distance_to_line(uv_scaled, prev_pos, pos);
float line_mask = 1.0 - smoothstep(0.0, _DotSize, d);
// approximate phosphor glowing with gaussian curve
float glow = exp(-d * d / (glow_size_scaled * glow_size_scaled));
float3 segment_color = _Color * line_mask + _GlowColor * glow;
line_color += segment_color;
}
prev_pos = pos;
}
line_color = 1.0 - exp(-line_color * _Exposure);
// add new segments + existing tails
float3 color = trail + line_color;
return float4(color, 1);
}
ENDCG
}
}
}
添加评论
点赞收藏
点踩分享查看原文
评论
?
参与讨论