#include 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); }