export declare const vertexShaderSource = "void main() {\n vUv = uv;\n vPosition = position;\n\n // SCROLLING LOGIC\n // Separate multipliers for wave, color, and flow offsets\n float waveOffset = -u_y_offset * u_y_offset_wave_multiplier;\n float colorOffset = -u_y_offset * u_y_offset_color_multiplier;\n float flowOffset = -u_y_offset * u_y_offset_flow_multiplier;\n\n // 1. DISPLACEMENT (WAVES)\n // We add waveOffset to Y to scroll the wave pattern\n v_displacement_amount = cnoise( vec3(\n u_wave_frequency_x * position.x + u_time,\n u_wave_frequency_y * (position.y + waveOffset) + u_time,\n u_time\n ));\n\n // 1b. SECONDARY WAVES\n // A second noise layer sampled on a rotated domain and moving at its own\n // rate. Crossing the base layer at an angle is what turns the regular swell\n // into interference, so the ridges stop repeating along one direction.\n if (NEAT_SECONDARY_WAVE_ENABLED > 0.5) {\n float t2 = u_time * u_wave2_speed;\n float ca = cos(u_wave2_angle);\n float sa = sin(u_wave2_angle);\n float px = position.x;\n float py = position.y + waveOffset;\n vec2 rp = vec2(ca * px - sa * py, sa * px + ca * py);\n\n float secondary = cnoise( vec3(\n u_wave2_frequency_x * rp.x + t2,\n u_wave2_frequency_y * rp.y - t2,\n t2 * 0.6 + 41.7\n ));\n\n // Normalised blend rather than a plain sum: the displacement drives the\n // highlight/shadow terms downstream, which expect roughly the same range\n // whatever the mix.\n v_displacement_amount = (v_displacement_amount + secondary * u_wave2_amplitude)\n / (1.0 + u_wave2_amplitude);\n }\n\n // 2. FLOW FIELD\n // Apply flow offset to scroll the flow field mask\n vec2 baseUv = vUv;\n baseUv.y += flowOffset / u_plane_height; // Scale to match wave speed\n vec2 flowUv = baseUv;\n\n if (NEAT_FLOW_ENABLED > 0.5) {\n if (u_flow_ease > 0.0 || u_flow_distortion_a > 0.0) {\n vec2 ppp = -1.0 + 2.0 * baseUv;\n ppp += 0.1 * cos((1.5 * u_flow_scale) * ppp.yx + 1.1 * u_time + vec2(0.1, 1.1));\n ppp += 0.1 * cos((2.3 * u_flow_scale) * ppp.yx + 1.3 * u_time + vec2(3.2, 3.4));\n ppp += 0.1 * cos((2.2 * u_flow_scale) * ppp.yx + 1.7 * u_time + vec2(1.8, 5.2));\n ppp += u_flow_distortion_a * cos((u_flow_distortion_b * u_flow_scale) * ppp.yx + 1.4 * u_time + vec2(6.3, 3.9));\n\n float r = length(ppp);\n flowUv = mix(baseUv, vec2(baseUv.x * (1.0 - u_flow_ease) + r * u_flow_ease, baseUv.y), u_flow_ease);\n }\n }\n\n // Pass the standard flow UV to fragment shader (for texture)\n vFlowUv = flowUv;\n\n // 3. COLOR MIXING\n // We take the computed flow UVs and apply the color offset\n // Scale by plane height to match wave offset speed (world space vs UV space)\n vec3 color = u_colors[0].color;\n\n vec3 distortedPos = position;\n if (NEAT_FLAT_SHADING < 0.5) {\n if (NEAT_FLOW_ENABLED > 0.5) {\n if (u_flow_ease > 0.0 || u_flow_distortion_a > 0.0) {\n vec3 ppp = position / 25.0;\n ppp.xyz += 0.1 * cos((1.5 * u_flow_scale) * ppp.yxz + 1.1 * u_time + vec3(0.1, 1.1, 2.1));\n ppp.xyz += 0.1 * cos((2.3 * u_flow_scale) * ppp.zxy + 1.3 * u_time + vec3(3.2, 3.4, 1.2));\n ppp.xyz += 0.1 * cos((2.2 * u_flow_scale) * ppp.yxz + 1.7 * u_time + vec3(1.8, 5.2, 3.1));\n ppp.xyz += u_flow_distortion_a * cos((u_flow_distortion_b * u_flow_scale) * ppp.zxy + 1.4 * u_time + vec3(6.3, 3.9, 4.5));\n\n float r = length(ppp);\n distortedPos = mix(position, vec3(\n position.x * (1.0 - u_flow_ease) + r * u_flow_ease * 25.0,\n position.y,\n position.z * (1.0 - u_flow_ease) + r * u_flow_ease * 25.0\n ), u_flow_ease);\n }\n }\n }\n\n vec3 noise_cord;\n if (NEAT_FLAT_SHADING < 0.5) {\n noise_cord = vec3(distortedPos.x / 50.0, (distortedPos.y + colorOffset) / 50.0, distortedPos.z / 50.0);\n } else {\n vec2 adjustedUv = flowUv;\n adjustedUv.y += colorOffset / u_plane_height;\n noise_cord = vec3(adjustedUv, 0.0);\n }\n\n const float minNoise = .0;\n const float maxNoise = .9;\n\n // Where the colour seams are, for the prism fringe.\n // x \u2014 how mid-transition the most-transitioning colour is, 1 on a seam and\n // 0 deep inside a colour. A max of smooth terms, so it stays continuous\n // even where which colour is winning changes; picking one transition per\n // vertex instead makes the varying jump between triangles and the fringe\n // comes out as a staircase.\n // y \u2014 total mix progress, which climbs by ~1 across each seam and so gives\n // the hue a ramp to run along.\n vec2 edge = vec2(0.0);\n\n // The whole mix below is dead weight when a procedural texture is supplying the\n // colour \u2014 the fragment shader reads v_color only on the non-texture path \u2014 and\n // that is up to five simplex-noise evaluations per vertex thrown away. Both flags\n // are compile-time constants, so this folds away entirely rather than branching.\n // The prism fringe reads the same field, so it has to keep the loop alive.\n if (NEAT_PROC_TEXTURE_ENABLED < 0.5 || NEAT_PRISM_EDGE_ENABLED > 0.5) {\n for (int i = 1; i < 6; i++) {\n if (u_colors[i].is_active > 0.5) {\n float noiseFlow = (1. + float(i)) / 30.;\n float noiseSpeed = (1. + float(i)) * 0.11;\n float noiseSeed = 13. + float(i) * 7.;\n\n float noise_z = u_time * noiseSpeed;\n if (NEAT_FLAT_SHADING < 0.5) {\n noise_z = noise_cord.z * u_color_pressure.x * u_color_pressure.x + u_time * noiseSpeed;\n }\n\n float noise = snoise(\n vec3(\n noise_cord.x * u_color_pressure.x * u_color_pressure.x + u_time * noiseFlow * 2.,\n noise_cord.y * u_color_pressure.y * u_color_pressure.y,\n noise_z\n ) + noiseSeed\n ) - (.1 * float(i)) + (.5 * u_color_blending);\n\n // Influence moves the threshold this colour has to clear, so it wins\n // more or less ground against the ones under it. Scaling mixAmount\n // instead would just make it translucent over the same territory,\n // which is opacity, not influence. The span is wide enough that 0\n // pushes the whole field below the floor (the colour disappears) and\n // 2 pushes most of it above the ceiling, with 1 shifting by nothing\n // so existing configs are untouched.\n noise += (u_colors[i].influence - 1.0) * 0.6;\n\n noise = clamp(noise, minNoise, maxNoise + float(i) * 0.02);\n float mixAmount = smoothstep(0.0, u_color_blending, noise);\n // The bias alone leaves a faint trace at influence 0, because the\n // noise still pokes above the floor at its strongest points. Fade it\n // out over the bottom of the range so 0 means gone; above 0.08 this\n // is 1 and the useful range is untouched.\n mixAmount *= smoothstep(0.0, 0.08, u_colors[i].influence);\n color = mix(color, u_colors[i].color, mixAmount);\n\n if (NEAT_PRISM_EDGE_ENABLED > 0.5) {\n // Seams found so far sit *under* this colour, so fade them by\n // how much of them it covers before folding in its own. Without\n // this a buried transition still lights up, and fringes appear\n // stranded in the middle of a solid area with no seam in sight.\n edge.x *= (1.0 - mixAmount);\n edge.x = max(edge.x, 4.0 * mixAmount * (1.0 - mixAmount));\n edge.y += mixAmount;\n }\n }\n }\n }\n\n v_color = color;\n v_edge = edge;\n\n // 4. FRESNEL (rim glow)\n // (Calculated in fragment shader using displacement slope approximation)\n\n // 5. VERTEX POSITION\n vec3 newPosition = position + normal * v_displacement_amount * u_wave_amplitude;\n vec4 mvPosition = modelViewMatrix * vec4(newPosition, 1.0);\n vNormal = normalize((modelViewMatrix * vec4(normal, 0.0)).xyz);\n gl_Position = projectionMatrix * mvPosition;\n v_new_position = gl_Position;\n}\n"; export declare const fragmentShaderSource = "float random(vec2 p) {\n return fract(sin(dot(p, vec2(12.9898,78.233))) * 43758.5453);\n}\n\nfloat fbm(vec3 x) {\n float value = 0.0;\n float amplitude = 0.5;\n float frequency = 1.0;\n for (int i = 0; i < 2; i++) {\n value += amplitude * snoise(x * frequency);\n frequency *= 2.0;\n amplitude *= 0.5;\n }\n return value;\n}\n\n// Grain-only simplex noise.\n//\n// Identical to snoise() above except for the hash: this uses the canonical\n// polynomial permute the algorithm was published with, where snoise() uses a\n// sin()-based one. Two octaves of grain therefore cost 24 sin() calls per pixel,\n// which measures as the most expensive single thing in this shader.\n//\n// Swapping the hash changes which pattern comes out, not what kind of pattern it is\n// \u2014 same lattice, same gradients, same frequency response \u2014 so grain keeps its\n// character at every grain scale and drifts over time exactly as before. Lattice\n// value/gradient noise is cheaper still but does not hold that property: its variance\n// falls off differently with scale, so it thins out at mid scales no single gain can\n// correct. Grain is random by nature, so a different draw of the same distribution is\n// not something anyone can pick out.\nvec4 permuteFast(vec4 x) {\n return mod(((x * 34.0) + 1.0) * x, 289.0);\n}\n\nfloat snoiseFast(vec3 v) {\n const vec2 C = vec2(1.0/6.0, 1.0/3.0) ;\n const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);\n\n vec3 i = floor(v + dot(v, C.yyy) );\n vec3 x0 = v - i + dot(i, C.xxx) ;\n\n vec3 g = step(x0.yzx, x0.xyz);\n vec3 l = 1.0 - g;\n vec3 i1 = min( g.xyz, l.zxy );\n vec3 i2 = max( g.xyz, l.zxy );\n\n vec3 x1 = x0 - i1 + C.xxx;\n vec3 x2 = x0 - i2 + C.yyy;\n vec3 x3 = x0 - D.yyy;\n\n i = mod(i, 289.0);\n vec4 p = permuteFast( permuteFast( permuteFast(\n i.z + vec4(0.0, i1.z, i2.z, 1.0 ))\n + i.y + vec4(0.0, i1.y, i2.y, 1.0 ))\n + i.x + vec4(0.0, i1.x, i2.x, 1.0 ));\n\n float n_ = 0.142857142857;\n vec3 ns = n_ * D.wyz - D.xzx;\n\n vec4 j = p - 49.0 * floor(p * ns.z * ns.z);\n\n vec4 x_ = floor(j * ns.z);\n vec4 y_ = floor(j - 7.0 * x_ );\n\n vec4 x = x_ *ns.x + ns.yyyy;\n vec4 y = y_ *ns.x + ns.yyyy;\n vec4 h = 1.0 - abs(x) - abs(y);\n\n vec4 b0 = vec4( x.xy, y.xy );\n vec4 b1 = vec4( x.zw, y.zw );\n\n vec4 s0 = floor(b0)*2.0 + 1.0;\n vec4 s1 = floor(b1)*2.0 + 1.0;\n vec4 sh = -step(h, vec4(0.0));\n\n vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ;\n vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ;\n\n vec3 p0 = vec3(a0.xy,h.x);\n vec3 p1 = vec3(a0.zw,h.y);\n vec3 p2 = vec3(a1.xy,h.z);\n vec3 p3 = vec3(a1.zw,h.w);\n\n vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));\n p0 *= norm.x;\n p1 *= norm.y;\n p2 *= norm.z;\n p3 *= norm.w;\n\n vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);\n m = m * m;\n return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),\n dot(p2,x2), dot(p3,x3) ) );\n}\n\n// Same two octaves and amplitudes as fbm(). Grain only \u2014 domain warping keeps the\n// original, where the noise is a large visible structure rather than a fine overlay.\nfloat grainFbm(vec3 x) {\n return 0.5 * snoiseFast(x) + 0.25 * snoiseFast(x * 2.0);\n}\n\n// Branchless HSL to RGB for iridescence\nvec3 hsl2rgb(float h, float s, float l) {\n vec3 rgb = clamp(abs(mod(h * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);\n return l + s * (rgb - 0.5) * (1.0 - abs(2.0 * l - 1.0));\n}\n\n// Thin-film interference ramp.\n//\n// A film reflects each wavelength by how its own period fits the extra distance\n// through the film, so the channels oscillate at *different rates* set by their\n// wavelengths \u2014 they do not sit at fixed offsets around a colour wheel. That is\n// the whole difference between a soap film and a rainbow: the Newton series runs\n// white \u2192 straw \u2192 magenta \u2192 blue \u2192 green and washes out as the film thickens,\n// where a hue sweep would just cycle evenly forever. The constants are the red\n// wavelength over each channel's, near enough for something decorative.\n// Rescaled to peak at 1. Raw interference is dark over much of the series, and\n// the fringe is screened on, which ignores dark \u2014 so untouched it simply vanishes\n// at half the thicknesses. Scaling keeps the ratios between channels, so the hue\n// order and the wash-out towards white both survive; only the overall level moves,\n// and that is what the intensity control is for.\nvec3 thinFilm(float t) {\n const vec3 inverseWavelength = vec3(1.0, 1.18, 1.42);\n vec3 f = 0.5 + 0.5 * cos(6.283185 * inverseWavelength * t);\n return f / max(max(f.r, max(f.g, f.b)), 0.0001);\n}\n\nvoid main() {\n vec2 finalUv = vFlowUv;\n \n vec3 baseColor;\n float texAlpha = 1.0;\n\n if (NEAT_PROC_TEXTURE_ENABLED > 0.5) {\n if (NEAT_FLAT_SHADING < 0.5) {\n float parallaxFactor = 0.25;\n float scrollOffset = (u_y_offset * u_y_offset_color_multiplier) * parallaxFactor;\n vec3 scrolledPos = vPosition;\n scrolledPos.y -= scrollOffset;\n \n vec3 p = (scrolledPos * 1.5) / 50.0;\n vec2 uvX = p.yz + vec2(0.5);\n vec2 uvY = p.zx + vec2(0.5);\n vec2 uvZ = p.xy + vec2(0.5);\n \n vec4 colX = texture2D(u_procedural_texture, uvX);\n vec4 colY = texture2D(u_procedural_texture, uvY);\n vec4 colZ = texture2D(u_procedural_texture, uvZ);\n \n vec3 n = normalize(vNormal);\n vec3 blendWeights = abs(n);\n blendWeights = blendWeights / (blendWeights.x + blendWeights.y + blendWeights.z + 0.0001);\n \n vec4 texSample = colX * blendWeights.x + colY * blendWeights.y + colZ * blendWeights.z;\n baseColor = texSample.rgb;\n if (u_transparent_texture_void > 0.5) {\n texAlpha = texSample.a;\n }\n } else {\n vec2 ppp = -1.0 + 2.0 * finalUv;\n ppp += 0.1 * cos((1.5 * u_flow_scale) * ppp.yx + 1.1 * u_time + vec2(0.1, 1.1));\n ppp += 0.1 * cos((2.3 * u_flow_scale) * ppp.yx + 1.3 * u_time + vec2(3.2, 3.4));\n ppp += 0.1 * cos((2.2 * u_flow_scale) * ppp.yx + 1.7 * u_time + vec2(1.8, 5.2));\n ppp += u_flow_distortion_a * cos((u_flow_distortion_b * u_flow_scale) * ppp.yx + 1.4 * u_time + vec2(6.3, 3.9));\n float r = length(ppp);\n \n float vx = (finalUv.x * u_texture_ease) + (r * (1.0 - u_texture_ease));\n float vy = (finalUv.y * u_texture_ease) + (0.0 * (1.0 - u_texture_ease));\n vec2 texUv = vec2(vx, vy);\n\n float parallaxFactor = 0.25;\n texUv.y -= (u_y_offset * u_y_offset_color_multiplier / u_plane_height) * parallaxFactor;\n texUv *= 1.5;\n\n vec4 texSample = texture2D(u_procedural_texture, texUv);\n baseColor = texSample.rgb;\n if (u_transparent_texture_void > 0.5) {\n texAlpha = texSample.a;\n }\n }\n } else {\n baseColor = v_color;\n }\n\n vec3 color = baseColor;\n\n // === DOMAIN WARPING (simplified: 3 fbm calls instead of 5) ===\n if (NEAT_DOMAIN_WARP_ENABLED > 0.5) {\n vec3 p;\n if (NEAT_FLAT_SHADING < 0.5) {\n p = vec3((vPosition / 50.0 + vec3(0.5)) * u_domain_warp_scale);\n p.z += u_time * 0.15;\n } else {\n p = vec3(finalUv * u_domain_warp_scale, u_time * 0.15);\n }\n vec2 q = vec2(fbm(p), fbm(p + vec3(5.2, 1.3, 0.0)));\n float f = fbm(p + vec3(4.0 * q, 0.0));\n vec3 warpColor = color * (1.0 + f * 0.8 * u_domain_warp_intensity);\n float pattern = clamp(f * f * f + 0.6 * f * f + 0.5 * f, 0.0, 1.0);\n color = mix(color, warpColor * (0.6 + pattern * 0.8), u_domain_warp_intensity * 0.7);\n }\n\n // Post-processing\n // Compute dynamic pixel-perfect normal using smooth normal\n vec3 normal = normalize(vNormal);\n vec3 viewDir = vec3(0.0, 0.0, 1.0);\n float ndotv = dot(normal, viewDir);\n \n // Cull back-faces for closed 3D shapes (Sphere=1, Torus=2, Cylinder=3)\n if (u_shape_type > 0.5 && u_shape_type < 3.5) {\n if (ndotv < 0.0) {\n discard;\n }\n } else {\n // Double-sided shapes (Plane, Ribbon): flip normal if back-facing\n if (ndotv < 0.0) {\n normal = -normal;\n ndotv = -ndotv;\n }\n }\n vec3 lightDir = normalize(vec3(1.0, 1.0, 1.0));\n float diffuse = max(dot(normal, lightDir), 0.0);\n vec3 halfDir = normalize(lightDir + viewDir);\n float specular = pow(max(dot(normal, halfDir), 0.0), 32.0);\n\n // Blend smooth 3D shading with smooth height-based wave shading\n if (NEAT_FLAT_SHADING > 0.5) {\n // Flat / height-based wave shading (plane style)\n color += v_displacement_amount * u_highlights;\n float heightShadow = 1.0 - v_displacement_amount;\n color -= heightShadow * heightShadow * u_shadows;\n } else {\n // 3D shading\n color += specular * u_highlights;\n color += v_displacement_amount * u_highlights * 0.5;\n float heightShadow = 1.0 - v_displacement_amount;\n color -= heightShadow * heightShadow * u_shadows * 0.5;\n color -= (1.0 - diffuse) * u_shadows * 0.5;\n }\n color = saturation(color, 1.0 + u_saturation);\n color = color * u_brightness;\n\n // === IRIDESCENCE ===\n if (NEAT_IRIDESCENCE_ENABLED > 0.5) {\n float hue = fract(v_displacement_amount * 0.5 + 0.5 + u_time * u_iridescence_speed * 0.05);\n vec3 iriColor = hsl2rgb(hue, 0.8, 0.6);\n color = mix(color, iriColor, u_iridescence_intensity * abs(v_displacement_amount) * 0.6);\n }\n\n // === PRISM EDGES (thin-film fringe along colour seams) ===\n // Oil-slick behaviour: the rainbow lives on the boundary between two colours,\n // not on the surface, and it runs through the spectrum as you cross it. Both\n // halves come straight off the colour-mix field the vertex shader already\n // built, so no screen-space derivatives are needed.\n if (NEAT_PRISM_EDGE_ENABLED > 0.5) {\n // pow() is undefined for a zero base with a non-positive exponent, and\n // both are reachable from config \u2014 off-seam the base is exactly 0.\n float band = pow(clamp(v_edge.x, 0.0, 1.0), max(u_prism_edge_thinness, 0.001));\n\n // Thickness has to vary *along* the seam, not just across it. A tight band\n // samples one slice of the series, so on its own it paints the whole rim a\n // single colour \u2014 where a real slick shifts hue as you follow the edge.\n // Riding the wave height is what a film on a rippling surface actually\n // does, and it means the wave layers show through the fringe even when the\n // lighting is flat enough that their shading contributes nothing.\n float thickness = v_edge.y * u_prism_edge_spread\n + v_displacement_amount * u_prism_edge_ripple\n + u_time * u_prism_edge_speed * 0.05;\n vec3 fringe = thinFilm(thickness);\n\n // Tint at constant luminance rather than screening the fringe on. Screening\n // only lightens, so over a pale surface \u2014 the usual case here \u2014 every channel\n // runs towards white and the hue washes out to a grey halo. Rescaling the\n // fringe to the surface's own brightness instead keeps a bright mass bright\n // and a dark one dark while the film supplies the hue, which is what reads as\n // petrol on water rather than a glow behind it.\n const vec3 luma = vec3(0.2126, 0.7152, 0.0722);\n vec3 tinted = fringe * (dot(color, luma) / max(dot(fringe, luma), 0.001));\n\n // Intensity is a blend amount here; past 1 mix() extrapolates out of gamut.\n color = mix(color, min(tinted, vec3(1.0)), band * clamp(u_prism_edge_intensity, 0.0, 1.0));\n }\n\n // === FRESNEL (Rim glow) ===\n if (NEAT_FRESNEL_ENABLED > 0.5) {\n float slope = 1.0 - abs(v_displacement_amount);\n float fresnel = pow(max(slope, 0.0), u_fresnel_power);\n color += u_fresnel_color * fresnel * u_fresnel_intensity;\n }\n\n // === VIGNETTE ===\n if (NEAT_VIGNETTE_ENABLED > 0.5 && u_vignette_intensity > 0.0) {\n vec2 vigUv = vUv;\n if (NEAT_FLAT_SHADING < 0.5) {\n vigUv = (v_new_position.xy / v_new_position.w) * 0.5 + vec2(0.5);\n }\n float dist = length(vigUv - vec2(0.5));\n float vig = smoothstep(u_vignette_radius, u_vignette_radius * 0.3, dist);\n color *= mix(1.0, vig, u_vignette_intensity);\n }\n\n // === FAKE BLOOM ===\n if (NEAT_BLOOM_ENABLED > 0.5 && u_bloom_intensity > 0.0) {\n float luma = dot(color, vec3(0.2126, 0.7152, 0.0722));\n float bloomMask = smoothstep(u_bloom_threshold, 1.0, luma);\n color += color * bloomMask * u_bloom_intensity;\n }\n\n // === CHROMATIC ABERRATION ===\n if (NEAT_CHROMATIC_ENABLED > 0.5 && u_chromatic_aberration > 0.0) {\n float caAmount = u_chromatic_aberration * 0.008;\n vec2 caUv = vUv;\n if (NEAT_FLAT_SHADING < 0.5) {\n caUv = (v_new_position.xy / v_new_position.w) * 0.5 + vec2(0.5);\n }\n float dist = length(caUv - vec2(0.5));\n float rShift = v_displacement_amount + caAmount * dist;\n float bShift = v_displacement_amount - caAmount * dist;\n color.r *= 1.0 + rShift * caAmount * 10.0;\n color.b *= 1.0 - bShift * caAmount * 10.0;\n }\n\n // Grain (use cheap hash noise instead of expensive fbm when static)\n float grain = 0.0;\n if (NEAT_GRAIN_ENABLED > 0.5 && u_grain_intensity > 0.0) {\n vec2 noiseCoords = gl_FragCoord.xy / u_grain_scale;\n if (u_grain_speed != 0.0 || NEAT_FLAT_SHADING > 0.5) {\n grain = grainFbm(vec3(noiseCoords, u_time * u_grain_speed));\n } else {\n // Static grain: use cheap hash instead of fbm\n grain = random(noiseCoords) - 0.5;\n }\n\n grain = grain * 0.5 + 0.5;\n grain -= 0.5;\n grain = (grain > u_grain_sparsity) ? grain : 0.0;\n grain *= u_grain_intensity;\n }\n\n color += vec3(grain);\n\n float edgeAlpha = 1.0;\n \n // Silhouette falloff for 3D shapes (skip when flat shading or fade is zero)\n if (u_silhouette_fade > 0.0 && NEAT_FLAT_SHADING < 0.5) {\n edgeAlpha = smoothstep(0.0, u_silhouette_fade, ndotv);\n }\n \n // UV boundary falloff for open shapes\n if (u_shape_type == 3.0) { // Cylinder: fade top/bottom ends\n float vFade = smoothstep(0.0, u_cylinder_fade, vUv.y) * smoothstep(1.0, 1.0 - u_cylinder_fade, vUv.y);\n edgeAlpha *= vFade;\n } else if (u_shape_type == 4.0) { // Ribbon: fade all 4 borders\n float uFade = smoothstep(0.0, u_ribbon_fade, vUv.x) * smoothstep(1.0, 1.0 - u_ribbon_fade, vUv.x);\n float vFade = smoothstep(0.0, u_ribbon_fade, vUv.y) * smoothstep(1.0, 1.0 - u_ribbon_fade, vUv.y);\n edgeAlpha *= uFade * vFade;\n }\n\n edgeAlpha *= texAlpha;\n gl_FragColor = vec4(color, edgeAlpha);\n}\n"; export declare function buildVertUniforms(): string; export declare function buildFragUniforms(): string; export declare function buildNoise(): string; export declare function buildColorFunctions(): string;