Files
aibis-dream/Assets/Render/OceanAsciiDoor.shader

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"
}