didvan-app/lib/shaders/ai_orb_3d.frag

297 lines
9.9 KiB
GLSL

#include <flutter/runtime_effect.glsl>
uniform vec2 uResolution;
uniform float uTime;
uniform float uActivity;
uniform vec3 uAccent;
uniform float uTransient;
uniform float uTexture;
uniform float uMotionPhase;
out vec4 fragColor;
const float PI = 3.14159265359;
float saturate(float value) {
return clamp(value, 0.0, 1.0);
}
float hash21(vec2 point) {
point = fract(point * vec2(123.34, 456.21));
point += dot(point, point + 45.32);
return fract(point.x * point.y);
}
float softNoise(vec2 point) {
vec2 cell = floor(point);
vec2 local = fract(point);
local = local * local * (3.0 - 2.0 * local);
return mix(
mix(hash21(cell), hash21(cell + vec2(1.0, 0.0)), local.x),
mix(hash21(cell + vec2(0.0, 1.0)), hash21(cell + 1.0), local.x),
local.y
);
}
float softBox(vec2 point, vec2 halfSize, float feather) {
vec2 distance = abs(point) - halfSize;
return 1.0 - smoothstep(
0.0,
feather,
max(distance.x, distance.y)
);
}
float directionalLobe(vec2 radial, vec2 direction, float sharpness) {
return pow(saturate(dot(radial, normalize(direction))), sharpness);
}
vec3 studioEnvironment(vec3 direction) {
float skyMix = smoothstep(-0.55, 0.72, direction.y);
vec3 floorColor = vec3(0.012, 0.016, 0.028);
vec3 skyColor = vec3(0.36, 0.46, 0.68);
vec3 environmentColor = mix(floorColor, skyColor, skyMix);
float horizonStrip = pow(
saturate(1.0 - abs(direction.y + 0.09) * 4.8),
10.0
);
environmentColor += vec3(0.68, 0.78, 1.0) * horizonStrip * 0.42;
float keyPanel = pow(
saturate(dot(direction, normalize(vec3(-0.69, -0.28, 0.67)))),
24.0
);
environmentColor += vec3(0.93, 0.96, 1.0) * keyPanel * 0.72;
float rimPanel = pow(
saturate(dot(direction, normalize(vec3(0.76, 0.04, 0.65)))),
34.0
);
environmentColor += vec3(0.34, 0.52, 1.0) * rimPanel * 0.48;
float warmPanel = pow(
saturate(dot(direction, normalize(vec3(0.42, 0.58, 0.69)))),
42.0
);
environmentColor += vec3(0.76, 0.49, 0.31) * warmPanel * 0.16;
return environmentColor;
}
void main() {
vec2 fragCoord = FlutterFragCoord().xy;
vec2 centered = fragCoord - uResolution * 0.5;
vec2 uv = centered / min(uResolution.x, uResolution.y);
float activity = saturate(uActivity);
float transientEnergy = saturate(uTransient);
float textureEnergy = saturate(uTexture);
float timePhase = uTime * PI * 2.0;
float voiceEnergy = smoothstep(0.045, 0.88, activity);
float radius = 0.326 + voiceEnergy * 0.0015;
float distanceToCenter = length(uv);
float safeDistance = max(distanceToCenter, 0.0001);
vec2 radial = uv / safeDistance;
float distanceFromSurface = distanceToCenter - radius;
// Reflections around the silhouette are directional rather than a uniform
// neon ring. They read as large studio lights caught by a glass surface.
float perimeterBand = exp(-abs(distanceFromSurface) * 72.0);
float upperLeftArc = directionalLobe(
radial,
vec2(-0.72, -0.69),
8.0
);
float rightArc = directionalLobe(radial, vec2(0.96, -0.12), 13.0);
float lowerArc = directionalLobe(radial, vec2(0.28, 0.96), 18.0);
float arcBreath = 0.92 + 0.08 * sin(
uMotionPhase * 0.72 + textureEnergy * 1.8
);
vec3 perimeterReflection =
vec3(0.88, 0.93, 1.0) * upperLeftArc * 0.38 +
mix(uAccent, vec3(0.48, 0.68, 1.0), 0.48) * rightArc * 0.3 +
vec3(0.95, 0.58, 0.38) * lowerArc * 0.075;
perimeterReflection *= perimeterBand * arcBreath;
float halo = exp(-max(distanceFromSurface, 0.0) * 15.0);
halo *= 1.0 - smoothstep(radius, radius * 2.42, distanceToCenter);
vec3 haloColor = uAccent * halo * (
0.025 + voiceEnergy * 0.055 + transientEnergy * 0.025
);
// A soft contact shadow and a dim, compressed floor reflection ground the
// sphere in space. Voice changes the reflection, not its physical position.
vec2 shadowUv = vec2(
uv.x / (radius * 1.08),
(uv.y - radius * 1.12) / (radius * 0.18)
);
float contactShadow = exp(
-(shadowUv.x * shadowUv.x * 2.3 + shadowUv.y * shadowUv.y * 1.55)
);
float reflectionBreakup = 0.72 + 0.28 * sin(
shadowUv.x * 7.0 + uMotionPhase * 0.36
);
float floorReflection = contactShadow * reflectionBreakup *
(0.12 + voiceEnergy * 0.1);
vec3 outsideColor =
haloColor +
perimeterReflection +
uAccent * floorReflection * 0.16 +
vec3(0.018, 0.022, 0.036) * contactShadow * 0.62;
float outsideAlpha = saturate(
halo * 0.17 +
perimeterBand * (upperLeftArc * 0.42 + rightArc * 0.34) +
contactShadow * 0.34
);
if (distanceToCenter > radius) {
fragColor = vec4(outsideColor, outsideAlpha);
return;
}
float z = sqrt(max(radius * radius - dot(uv, uv), 0.0));
vec3 geometricNormal = normalize(vec3(uv / radius, z / radius));
// Microscopic normal variation gives the material thickness without
// visibly deforming the sphere. Speech texture subtly increases it.
vec2 noiseCoordinate =
geometricNormal.xy * 5.1 +
vec2(timePhase * 0.025, -timePhase * 0.018);
float noiseA = softNoise(noiseCoordinate);
float noiseB = softNoise(noiseCoordinate * 1.73 + vec2(2.7, -1.9));
vec2 microNormal = vec2(noiseA - 0.5, noiseB - 0.5) *
(0.0035 + textureEnergy * 0.009);
vec3 normal = normalize(vec3(
geometricNormal.xy + microNormal,
geometricNormal.z
));
vec3 viewDirection = vec3(0.0, 0.0, 1.0);
vec3 keyDirection = normalize(vec3(
-0.48 + sin(uMotionPhase * 0.31) * voiceEnergy * 0.012,
-0.63 + cos(uMotionPhase * 0.27) * voiceEnergy * 0.009,
0.88
));
vec3 fillDirection = normalize(vec3(0.76, 0.16, 0.54));
float diffuse = max(dot(normal, keyDirection), 0.0);
float fill = max(dot(normal, fillDirection), 0.0);
float ndv = saturate(dot(normal, viewDirection));
float fresnel = 0.035 + 0.965 * pow(1.0 - ndv, 5.0);
float thickness = (2.0 * z) / radius;
vec3 reflectedDirection = normalize(reflect(-viewDirection, normal));
vec3 reflection = studioEnvironment(reflectedDirection);
// Three close indices of refraction create restrained chromatic dispersion
// at the edge, a key cue for real optical glass.
vec3 refractedRed = studioEnvironment(normalize(
refract(-viewDirection, normal, 0.695)
));
vec3 refractedGreen = studioEnvironment(normalize(
refract(-viewDirection, normal, 0.715)
));
vec3 refractedBlue = studioEnvironment(normalize(
refract(-viewDirection, normal, 0.738)
));
vec3 refractedEnvironment = vec3(
refractedRed.r,
refractedGreen.g,
refractedBlue.b
);
vec3 absorption = vec3(0.19, 0.13, 0.075);
vec3 transmission = exp(-absorption * thickness);
vec3 deepTint = mix(vec3(0.008, 0.012, 0.024), uAccent * 0.12, 0.38);
vec3 color = deepTint * (0.2 + diffuse * 0.16 + fill * 0.04);
color += refractedEnvironment * transmission * (0.15 + ndv * 0.08);
color += reflection * (0.22 + fresnel * 1.04);
color += uAccent * fresnel * (0.035 + voiceEnergy * 0.065);
// Coherent rectangular reflections sell the object as a glossy 3D sphere.
float surfaceVisibility = smoothstep(0.12, 0.7, geometricNormal.z);
float keySoftbox = softBox(
normal.xy - vec2(-0.34, -0.3),
vec2(0.055, 0.235),
0.055
) * surfaceVisibility;
float topSoftbox = softBox(
normal.xy - vec2(0.05, -0.49),
vec2(0.27, 0.035),
0.06
) * surfaceVisibility;
float sideSoftbox = softBox(
normal.xy - vec2(0.56, 0.03),
vec2(0.026, 0.19),
0.048
) * surfaceVisibility;
color += vec3(0.91, 0.95, 1.0) * keySoftbox * 0.19;
color += vec3(0.72, 0.82, 1.0) * topSoftbox * 0.08;
color += mix(uAccent, vec3(0.5, 0.7, 1.0), 0.52) *
sideSoftbox * 0.13;
vec3 halfVector = normalize(keyDirection + viewDirection);
float sharpSpecular = pow(max(dot(normal, halfVector), 0.0), 180.0);
color += vec3(1.0, 0.98, 0.95) * sharpSpecular * 1.55;
vec3 broadHalf = normalize(vec3(-0.58, -0.4, 0.72) + viewDirection);
float broadSpecular = pow(max(dot(normal, broadHalf), 0.0), 25.0);
color += vec3(0.72, 0.82, 1.0) * broadSpecular * 0.1;
// Keep the optical center dark and clean. Speech travels through the glass
// as thin refracted sheets instead of an artificial glowing blob.
vec2 normalizedUv = uv / radius;
float internalDepth =
1.0 - smoothstep(0.12, 0.98, length(normalizedUv));
float cleanCenter = pow(internalDepth, 1.65);
color *= 1.0 - cleanCenter * 0.115;
float voicePulse = 0.82 + 0.18 * sin(
uMotionPhase * 1.23 + textureEnergy * 1.6
);
float primarySheetY =
normalizedUv.y +
normalizedUv.x * 0.11 +
0.13 -
sin(uMotionPhase * 0.54) * voiceEnergy * 0.055;
float primarySheet = exp(-primarySheetY * primarySheetY * 82.0) *
exp(-dot(normalizedUv, normalizedUv) * 1.15);
float secondarySheetY =
normalizedUv.y -
normalizedUv.x * 0.07 -
0.42 +
cos(uMotionPhase * 0.39) * voiceEnergy * 0.035;
float secondarySheet = exp(
-secondarySheetY * secondarySheetY * 115.0
) * exp(-dot(normalizedUv, normalizedUv) * 1.5);
color += mix(uAccent, vec3(0.66, 0.8, 1.0), 0.38) *
primarySheet * voiceEnergy * voicePulse * 0.052;
color += vec3(0.68, 0.8, 1.0) * secondarySheet *
voiceEnergy * 0.026;
float onsetFlash = pow(1.0 - ndv, 2.2) *
upperLeftArc * transientEnergy;
color += vec3(0.86, 0.92, 1.0) * onsetFlash * 0.18;
float causticPhase =
normalizedUv.x * 14.0 +
normalizedUv.y * 9.0 -
uMotionPhase * (0.52 + textureEnergy * 0.38) +
noiseA * 2.4;
float caustic = pow(saturate(0.5 + 0.5 * sin(causticPhase)), 9.0);
float causticBand = smoothstep(0.24, 0.52, length(normalizedUv)) *
(1.0 - smoothstep(0.78, 0.97, length(normalizedUv)));
caustic *= causticBand * voiceEnergy * (0.25 + textureEnergy * 0.75);
color += mix(uAccent, vec3(0.78, 0.88, 1.0), 0.58) *
caustic * 0.045;
// Bring the outside light sources continuously across the glass boundary.
color += perimeterReflection * (0.56 + fresnel * 0.64);
color *= mix(0.88, 1.025, geometricNormal.z);
float edgeAlpha = 1.0 - smoothstep(
radius * 0.982,
radius,
distanceToCenter
);
fragColor = vec4(color, edgeAlpha);
}