383 lines
16 KiB
GLSL
383 lines
16 KiB
GLSL
Shader "Custom/OceanAsciiDoor"
|
|
{
|
|
Properties
|
|
{
|
|
[PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
|
|
_Color ("Tint", Color) = (1,1,1,1)
|
|
|
|
[Header(Grid)]
|
|
_GridCols ("Grid Columns", Range(20, 200)) = 80
|
|
_GridRows ("Grid Rows", Range(15, 150)) = 55
|
|
|
|
[Header(Perspective)]
|
|
_HorizonRow ("Horizon Offset (Rows)", Range(0, 12)) = 0
|
|
_PerspectiveStrength ("Perspective Strength", Range(0, 1)) = 0.75
|
|
|
|
[Header(Wave)]
|
|
_WaveSpeed ("Wave Travel (Rows/Second)", Range(0, 8.0)) = 2.6
|
|
_WaveCount ("Wave Count", Range(2, 10)) = 5
|
|
_ShoreRows ("Shore Rows", Range(1, 8)) = 3
|
|
_WaveBodyWidth ("Wave Body Width", Range(0.25, 0.95)) = 0.68
|
|
_CrestScale ("Crest Scale", Range(0.5, 2.0)) = 1.0
|
|
_GlyphSpeed ("Glyph Flicker Speed", Range(0, 8.0)) = 3.2
|
|
|
|
[HideInInspector] _WaveStartTime ("Wave Start Time", Float) = 0
|
|
[HideInInspector] _WavePhase ("Wave Start Phase", Float) = 0
|
|
|
|
[Header(Intensity)]
|
|
_CrestBright ("Crest Brightness", Range(0.3, 1)) = 0.85
|
|
_BodyBright ("Body Brightness", Range(0.1, 1)) = 0.65
|
|
|
|
[Header(Background)]
|
|
_BgColor ("Background Color", Color) = (0,0,0,1)
|
|
}
|
|
SubShader
|
|
{
|
|
Tags
|
|
{
|
|
"Queue"="Transparent"
|
|
"IgnoreProjector"="True"
|
|
"RenderType"="Transparent"
|
|
"PreviewType"="Plane"
|
|
"CanUseSpriteAtlas"="True"
|
|
}
|
|
Cull Off
|
|
Lighting Off
|
|
ZWrite Off
|
|
Blend SrcAlpha OneMinusSrcAlpha
|
|
|
|
Pass
|
|
{
|
|
CGPROGRAM
|
|
#pragma vertex vert
|
|
#pragma fragment frag
|
|
#pragma target 3.0
|
|
|
|
#include "UnityCG.cginc"
|
|
|
|
struct appdata
|
|
{
|
|
float4 vertex : POSITION;
|
|
float4 color : COLOR;
|
|
float2 uv : TEXCOORD0;
|
|
};
|
|
|
|
struct v2f
|
|
{
|
|
float4 vertex : SV_POSITION;
|
|
fixed4 color : COLOR;
|
|
float2 uv : TEXCOORD0;
|
|
};
|
|
|
|
sampler2D _MainTex;
|
|
float4 _MainTex_ST;
|
|
fixed4 _Color;
|
|
fixed4 _BgColor;
|
|
float _GridCols;
|
|
float _GridRows;
|
|
float _HorizonRow;
|
|
float _PerspectiveStrength;
|
|
float _WaveSpeed;
|
|
float _WaveCount;
|
|
float _ShoreRows;
|
|
float _WaveBodyWidth;
|
|
float _CrestScale;
|
|
float _GlyphSpeed;
|
|
float _WaveStartTime;
|
|
float _WavePhase;
|
|
float _CrestBright;
|
|
float _BodyBright;
|
|
|
|
// ─── Bitmap font: 5 wide x 7 tall ───────────────────────────
|
|
// charId mapping (matches HTML CREST_CHARS order):
|
|
// 0 = ~ (tilde)
|
|
// 1 = ≈ (approx equal)
|
|
// 2 = ∿ (sine wave)
|
|
// 3 = 0 (zero digit)
|
|
// 4 = 1 (one digit)
|
|
//
|
|
// HTML: CREST_CHARS = ['~','≈','~','≈','∿','~','~','≈']
|
|
// indices: 0 1 0 1 2 0 0 1
|
|
// So crestCharMap[8] = {0,1,0,1,2,0,0,1}
|
|
|
|
float glyphPixel(int charId, int px, int py)
|
|
{
|
|
// ~ (tilde) — classic wave shape
|
|
if (charId == 0)
|
|
{
|
|
if (py == 2) return (px == 0 || px == 1) ? 1.0 : 0.0;
|
|
if (py == 3) return (px == 0 || px == 2 || px == 4) ? 1.0 : 0.0;
|
|
if (py == 4) return (px == 3 || px == 4) ? 1.0 : 0.0;
|
|
return 0.0;
|
|
}
|
|
// ≈ (approx equal — stacked double tilde)
|
|
if (charId == 1)
|
|
{
|
|
if (py == 1) return (px == 0 || px == 1) ? 1.0 : 0.0;
|
|
if (py == 2) return (px == 0 || px == 2 || px == 4) ? 1.0 : 0.0;
|
|
if (py == 3) return (px == 3 || px == 4) ? 1.0 : 0.0;
|
|
if (py == 4) return (px == 0 || px == 1) ? 1.0 : 0.0;
|
|
if (py == 5) return (px == 0 || px == 2 || px == 4) ? 1.0 : 0.0;
|
|
if (py == 6) return (px == 3 || px == 4) ? 1.0 : 0.0;
|
|
return 0.0;
|
|
}
|
|
// ∿ (sine wave — S-curve)
|
|
if (charId == 2)
|
|
{
|
|
if (py == 0) return (px == 1 || px == 2) ? 1.0 : 0.0;
|
|
if (py == 1) return (px == 0 || px == 3) ? 1.0 : 0.0;
|
|
if (py == 2) return (px == 0) ? 1.0 : 0.0;
|
|
if (py == 3) return (px == 0 || px == 4) ? 1.0 : 0.0;
|
|
if (py == 4) return (px == 4) ? 1.0 : 0.0;
|
|
if (py == 5) return (px == 1 || px == 4) ? 1.0 : 0.0;
|
|
if (py == 6) return (px == 2 || px == 3) ? 1.0 : 0.0;
|
|
return 0.0;
|
|
}
|
|
// 0 (zero digit)
|
|
if (charId == 3)
|
|
{
|
|
if (py == 0) return (px >= 1 && px <= 3) ? 1.0 : 0.0;
|
|
if (py == 1) return (px == 0 || px == 4) ? 1.0 : 0.0;
|
|
if (py == 2) return (px == 0 || px == 3 || px == 4) ? 1.0 : 0.0;
|
|
if (py == 3) return (px == 0 || px == 2 || px == 4) ? 1.0 : 0.0;
|
|
if (py == 4) return (px == 0 || px == 1 || px == 4) ? 1.0 : 0.0;
|
|
if (py == 5) return (px == 0 || px == 4) ? 1.0 : 0.0;
|
|
if (py == 6) return (px >= 1 && px <= 3) ? 1.0 : 0.0;
|
|
return 0.0;
|
|
}
|
|
// 1 (one digit)
|
|
if (charId == 4)
|
|
{
|
|
if (py == 0) return (px == 2) ? 1.0 : 0.0;
|
|
if (py == 1) return (px == 1 || px == 2) ? 1.0 : 0.0;
|
|
if (py >= 2 && py <= 5) return (px == 2) ? 1.0 : 0.0;
|
|
if (py == 6) return (px >= 1 && px <= 3) ? 1.0 : 0.0;
|
|
return 0.0;
|
|
}
|
|
return 0.0;
|
|
}
|
|
|
|
// HTML: CREST_CHARS = ['~','≈','~','≈','∿','~','~','≈']
|
|
// Map index 0..7 to charId
|
|
int crestCharId(int idx)
|
|
{
|
|
// 0:~ 1:≈ 2:~ 3:≈ 4:∿ 5:~ 6:~ 7:≈
|
|
if (idx == 1 || idx == 3 || idx == 7) return 1; // ≈
|
|
if (idx == 4) return 2; // ∿
|
|
return 0; // ~
|
|
}
|
|
|
|
// ─── Wave math (matches HTML prototype) ──────────────────────
|
|
float bodyNoise(float c, float r, float t)
|
|
{
|
|
return (
|
|
sin(c * 1.3 + r * 0.7 + t * 0.83) * 0.35 +
|
|
sin(c * 0.37 + r * 1.17 + t * 0.51) * 0.30 +
|
|
sin(c * 2.1 + r * 0.31 + t * 1.07) * 0.20 +
|
|
sin(c * 0.71 + r * 2.03 + t * 0.37) * 0.15
|
|
) * 0.5 + 0.5;
|
|
}
|
|
|
|
float hash11(float p)
|
|
{
|
|
p = frac(p * 0.1031);
|
|
p *= p + 33.33;
|
|
p *= p + p;
|
|
return frac(p);
|
|
}
|
|
|
|
v2f vert(appdata v)
|
|
{
|
|
v2f o;
|
|
o.vertex = UnityObjectToClipPos(v.vertex);
|
|
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
|
|
o.color = v.color * _Color;
|
|
return o;
|
|
}
|
|
|
|
fixed4 frag(v2f i) : SV_Target
|
|
{
|
|
float2 uv = i.uv;
|
|
|
|
// Project the flat character grid onto a low, receding sea plane.
|
|
// Rows and columns become denser near the horizon while the foreground
|
|
// stays broad, matching the perspective of the glass-sea animation.
|
|
float horizonUv = saturate(_HorizonRow / max(1.0, _GridRows));
|
|
float rawDepth = 1.0 - uv.y;
|
|
float screenDepth = saturate(
|
|
(rawDepth - horizonUv) / max(0.001, 1.0 - horizonUv));
|
|
float perspective = saturate(_PerspectiveStrength);
|
|
float depthExponent = lerp(1.0, 0.42, perspective);
|
|
float projectedDepth = pow(screenDepth, depthExponent);
|
|
|
|
float farColumnDensity = lerp(1.0, 2.35, perspective);
|
|
float columnDensity = lerp(
|
|
farColumnDensity,
|
|
1.0,
|
|
smoothstep(0.0, 1.0, screenDepth));
|
|
|
|
float r = projectedDepth * _GridRows;
|
|
float c = (uv.x - 0.5) * _GridCols * columnDensity + _GridCols * 0.5;
|
|
|
|
float shoreY = _GridRows - _ShoreRows;
|
|
float sp = shoreY / max(1.0, _WaveCount);
|
|
|
|
float cellFracX = frac(c);
|
|
float cellFracY = frac(r);
|
|
int glyphX = (int)(cellFracX * 5.0);
|
|
int glyphY = (int)(cellFracY * 7.0);
|
|
glyphX = clamp(glyphX, 0, 4);
|
|
glyphY = clamp(glyphY, 0, 6);
|
|
|
|
float cellC = floor(c);
|
|
float cellR = floor(r);
|
|
|
|
// CodeSea owns a local clock. DreamDoorSystem resets it only when
|
|
// entering CodeSea, so the reveal is deterministic while reframing
|
|
// from fullscreen back into the door remains continuous.
|
|
float localTime = max(0.0, _Time.y - _WaveStartTime);
|
|
float waveTravel = _WavePhase + localTime * _WaveSpeed;
|
|
float glyphTime = localTime * _GlyphSpeed;
|
|
|
|
// ── Wave layer computation ───────────────────────────────
|
|
float bestDepth = 999.0;
|
|
float bestWaveY = -999.0;
|
|
|
|
for (float wi = 0; wi < _WaveCount + 2; wi++)
|
|
{
|
|
float wSeed = hash11(wi * 7.31);
|
|
|
|
float baseY = -sp + wi * sp;
|
|
// Every crest advances from the horizon toward the viewer with
|
|
// only a small depth variation. Keeping a shared travel phase
|
|
// makes the bands read as one ocean instead of unrelated noise.
|
|
float depthSpeed = lerp(0.92, 1.12, saturate(baseY / max(1.0, shoreY)));
|
|
baseY += waveTravel * depthSpeed;
|
|
baseY = baseY - floor(baseY / (_GridRows + sp)) * (_GridRows + sp) - sp;
|
|
|
|
float p1 = hash11(wi * 3.17) * 6.2832;
|
|
float p2 = hash11(wi * 5.23) * 6.2832;
|
|
float p3 = hash11(wi * 9.41) * 6.2832;
|
|
p1 += localTime * 0.045;
|
|
p2 -= localTime * 0.070;
|
|
p3 += localTime * 0.100;
|
|
|
|
// Long, broad crests echo the gold montage's composition.
|
|
// Higher-frequency detail is deliberately restrained so the
|
|
// result remains a readable wave rather than code rain.
|
|
float a1 = (1.15 + hash11(wi * 2.71) * 0.65) * _CrestScale;
|
|
float a2 = (0.32 + hash11(wi * 4.13) * 0.20) * _CrestScale;
|
|
float a3 = (0.10 + hash11(wi * 6.67) * 0.08) * _CrestScale;
|
|
|
|
float f1 = 0.026 + hash11(wi * 1.23) * 0.014;
|
|
float f2 = 0.072 + hash11(wi * 8.37) * 0.030;
|
|
float f3 = 0.140 + hash11(wi * 11.03) * 0.040;
|
|
|
|
float crest = baseY
|
|
+ sin(cellC * f1 + p1) * a1
|
|
+ sin(cellC * f2 + p2) * a2
|
|
+ sin(cellC * f3 + p3) * a3;
|
|
|
|
float d = cellR - crest;
|
|
if (d >= -0.5 && d < bestDepth)
|
|
{
|
|
bestDepth = d;
|
|
bestWaveY = crest;
|
|
}
|
|
}
|
|
|
|
// ── Determine brightness and character ───────────────────
|
|
float bright = 0.0;
|
|
int charId = 0;
|
|
|
|
if (bestWaveY > -900.0 && cellR < shoreY)
|
|
{
|
|
float depthVal = bestDepth;
|
|
float bodyDepth = max(1.5, sp * _WaveBodyWidth);
|
|
float depthRatio = saturate(depthVal / bodyDepth);
|
|
float proximity = saturate(bestWaveY / max(0.001, shoreY));
|
|
|
|
if (depthVal < 1.35)
|
|
{
|
|
// ── CREST ────────────────────────────────────
|
|
bright = (0.55 + proximity * 0.45) * _CrestBright;
|
|
|
|
float rawIdx = cellC * 0.35 + glyphTime * 1.4;
|
|
int idx = ((int)floor(rawIdx)) % 8;
|
|
if (idx < 0) idx += 8;
|
|
charId = crestCharId(idx);
|
|
}
|
|
else if (depthVal < bodyDepth)
|
|
{
|
|
// ── WAVE BODY ────────────────────────────────
|
|
bright = pow(1.0 - depthRatio, 1.1) * (0.24 + proximity * 0.66) * _BodyBright;
|
|
|
|
if (bright >= 0.03)
|
|
{
|
|
float n = bodyNoise(cellC, cellR, glyphTime);
|
|
float threshold = lerp(0.12, 0.42, pow(depthRatio, 1.3));
|
|
if (n < threshold)
|
|
{
|
|
bright = 0.0;
|
|
}
|
|
else
|
|
{
|
|
charId = sin(cellC * 0.97 + cellR * 1.53 + glyphTime * 1.9) > 0 ? 4 : 3;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bright = 0.0;
|
|
}
|
|
}
|
|
}
|
|
else if (cellR >= shoreY)
|
|
{
|
|
// ── Shore foam ───────────────────────────────────
|
|
float dInShore = (cellR - shoreY) / max(1.0, _ShoreRows);
|
|
float n = bodyNoise(cellC, cellR, glyphTime * 1.15);
|
|
bright = (1.0 - dInShore) * (0.3 + n * 0.6) * _BodyBright;
|
|
|
|
if (bright >= 0.06)
|
|
{
|
|
if (cellR < shoreY + 1.0)
|
|
{
|
|
float rawCi = cellC * 0.4 + glyphTime * 1.6;
|
|
int ci = ((int)floor(rawCi)) % 8;
|
|
if (ci < 0) ci += 8;
|
|
charId = crestCharId(ci);
|
|
}
|
|
else
|
|
{
|
|
// HTML: n > 0.5 ? '1' : '0'
|
|
charId = n > 0.5 ? 4 : 3;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bright = 0.0;
|
|
}
|
|
}
|
|
|
|
// ── Render glyph on black background ─────────────────────
|
|
bright = saturate(bright);
|
|
|
|
float pixel = 0.0;
|
|
if (bright > 0.01)
|
|
pixel = glyphPixel(charId, glyphX, glyphY);
|
|
|
|
float charBright = pixel * bright;
|
|
float3 col = float3(charBright, charBright, charBright) * i.color.rgb;
|
|
|
|
// Black opaque background — character brightness on top
|
|
float3 finalCol = lerp(_BgColor.rgb, col, charBright);
|
|
float finalAlpha = _BgColor.a * i.color.a;
|
|
|
|
return fixed4(finalCol, finalAlpha);
|
|
}
|
|
ENDCG
|
|
}
|
|
}
|
|
Fallback "Sprites/Default"
|
|
}
|