297 lines
9.9 KiB
GLSL
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);
|
|
}
|