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 | |
| } | |
| } | |
| } |
评论
?
参与讨论