@@ -448,15 +448,38 @@ struct Refraction {
448448 float d;
449449};
450450
451- void refractionSolidSphere(float etaIR, float etaRI, float thickness,
452- const vec3 n, vec3 r, out Refraction ray) {
453- r = refract (r, n, etaIR);
454- float NoR = dot (n, r);
451+ /* *
452+ * Internal helper for solid sphere refraction.
453+ * Optimized to take pre-calculated geometric constants to support efficient spectral dispersion.
454+ */
455+ void refractedRaySolidSphere(const vec3 r_in, float NoR_in, float sin2Theta_in,
456+ float etaIR, float etaRI, float thickness, const vec3 n, out Refraction ray) {
457+
458+ // Snell's Law for the first interface (entry into medium)
459+ // We use the pre-calculated sin^2(theta) to solve for the internal ray direction
460+ float k = 1.0 - etaIR * etaIR * sin2Theta_in;
461+ vec3 rr = etaIR * r_in - (etaIR * NoR_in + sqrt (max (k, 0.0 ))) * n;
462+
463+ float NoR = dot (n, rr);
455464 float d = thickness * - NoR;
456- ray.position = vec3 (shading_position + r * d);
465+
457466 ray.d = d;
458- vec3 n1 = normalize (NoR * r - n * 0.5 );
459- ray.direction = refract (r, n1, etaRI);
467+ ray.position = shading_position + rr * d;
468+
469+ // Second interface exit (Sphere fudge)
470+ // Simulates the curvature of a sphere without full ray-intersection math
471+ vec3 n1 = normalize (NoR * rr - n * 0.5 );
472+ ray.direction = refract (rr, n1, etaRI);
473+ }
474+
475+ /* *
476+ * Standard Solid Sphere Refraction (N=1)
477+ */
478+ void refractionSolidSphere(float etaIR, float etaRI, float thickness,
479+ const vec3 n, vec3 r, out Refraction ray) {
480+ float NoR_in = dot (n, r);
481+ float sin2Theta_in = 1.0 - NoR_in * NoR_in;
482+ refractedRaySolidSphere(r, NoR_in, sin2Theta_in, etaIR, etaRI, thickness, n, ray);
460483}
461484
462485void refractionSolidBox(float etaIR, float thickness,
@@ -526,18 +549,127 @@ vec3 evaluateRefraction(const PixelParams pixel, const vec3 n0, const float lod,
526549 return t;
527550}
528551
552+ #if defined(MATERIAL_HAS_DISPERSION) && (REFRACTION_TYPE == REFRACTION_TYPE_SOLID)
553+
554+ /* *
555+ * HIGH-FIDELITY SPECTRAL DISPERSION (N=4)
556+ * ---------------------------------------
557+ * This algorithm approximates the spectral integral of refracted light by sampling
558+ * four optimized wavelengths and collapsing the color-matching and color-space
559+ * transforms into four pre-computed 3x3 matrices (K0-K3).
560+ * See tools/specgen.
561+ */
562+
563+ vec3 calculateDispersion(const PixelParams pixel, const vec3 n0, const float lod) {
564+ const mat3 K0 = mat3 (
565+ 0.00581637 , 0.02312851 , 0.01689631 ,
566+ - 0.11782236 , 0.11316202 , 0.11098148 ,
567+ - 0.45422013 , 0.04493517 , 0.98249798
568+ ); // 486.1nm
569+
570+ const mat3 K1 = mat3 (
571+ 0.14291703 , 0.10429778 , - 0.01556522 ,
572+ - 0.27560148 , 0.57678541 , - 0.06412244 ,
573+ 0.06839811 , 0.02732891 , 0.01602064
574+ ); // 546.1nm
575+
576+ const mat3 K2 = mat3 (
577+ 0.70106120 , - 0.09440402 , - 0.00241699 ,
578+ 0.29545674 , 0.29931852 , - 0.04351961 ,
579+ 0.31884400 , - 0.05627069 , 0.00083808
580+ ); // 589.3nm
581+
582+ const mat3 K3 = mat3 (
583+ 0.15020522 , - 0.03302213 , 0.00108589 ,
584+ 0.09796715 , 0.01073410 , - 0.00333946 ,
585+ 0.06697807 , - 0.01599341 , 0.00064333
586+ ); // 656.3nm
587+
588+ const float offsets[4 ] = float [](0.70795215 , 0.24790980 , 0.00000000 , - 0.29204785 );
589+
590+ float n_base = pixel.etaRI;
591+ float P = pixel.dispersion / 20.0 ;
592+ float dispFactor = P * (n_base - 1.0 );
593+
594+ // JITTER / DITHERING
595+ // Calculate per-pixel jitter to fill the gaps between the 4 samples
596+ // 0.35 is roughly the distance between the Gaussian sample points.
597+ float jitter = (interleavedGradientNoise(gl_FragCoord .xy + vec2 (frameUniforms.temporalNoise)) - 0.5 )
598+ * 0.35 * dispFactor;
599+
600+ // Gaussian mapping for t in [0, 1] relative to the 420-680nm range
601+ float nd0 = n_base + dispFactor * offsets[0 ] + jitter;
602+ float nd1 = n_base + dispFactor * offsets[1 ] + jitter;
603+ float nd2 = n_base + dispFactor * offsets[2 ] + jitter;
604+ float nd3 = n_base + dispFactor * offsets[3 ] + jitter;
605+
606+ // Compute 4 Ray Directions
607+ // We unroll the refraction model logic to compute all 4 rays at once
608+ Refraction r0, r1, r2, r3;
609+
610+ vec3 r_in = - shading_view;
611+ float NoR_in = dot (n0, r_in);
612+ float sin2Theta_in = 1.0 - NoR_in * NoR_in;
613+ refractedRaySolidSphere(r_in, NoR_in, sin2Theta_in, 1.0 / nd0, nd0, pixel.thickness, n0, r0);
614+ refractedRaySolidSphere(r_in, NoR_in, sin2Theta_in, 1.0 / nd1, nd1, pixel.thickness, n0, r1);
615+ refractedRaySolidSphere(r_in, NoR_in, sin2Theta_in, 1.0 / nd2, nd2, pixel.thickness, n0, r2);
616+ refractedRaySolidSphere(r_in, NoR_in, sin2Theta_in, 1.0 / nd3, nd3, pixel.thickness, n0, r3);
617+
618+ // Batch Texture Fetches
619+ // Issuing all textureLod calls back-to-back is the key to performance here.
620+ vec3 s0, s1, s2, s3;
621+ #if REFRACTION_MODE == REFRACTION_MODE_CUBEMAP
622+ s0 = prefilteredRadiance(r0.direction, lod);
623+ s1 = prefilteredRadiance(r1.direction, lod);
624+ s2 = prefilteredRadiance(r2.direction, lod);
625+ s3 = prefilteredRadiance(r3.direction, lod);
626+ float ibl = frameUniforms.iblLuminance;
627+ s0 *= ibl;
628+ s1 *= ibl;
629+ s2 *= ibl;
630+ s3 *= ibl;
631+ #else
632+ // SSR Path: Calculate 4 UVs, then 4 Samples
633+ highp mat4 clipFromWorld = getClipFromWorldMatrix();
634+ vec4 p0 = mulMat4x4Float3(clipFromWorld, r0.position);
635+ vec4 p1 = mulMat4x4Float3(clipFromWorld, r1.position);
636+ vec4 p2 = mulMat4x4Float3(clipFromWorld, r2.position);
637+ vec4 p3 = mulMat4x4Float3(clipFromWorld, r3.position);
638+
639+ vec2 uv0 = uvToRenderTargetUV(p0.xy * (0.5 / p0.w) + 0.5 );
640+ vec2 uv1 = uvToRenderTargetUV(p1.xy * (0.5 / p1.w) + 0.5 );
641+ vec2 uv2 = uvToRenderTargetUV(p2.xy * (0.5 / p2.w) + 0.5 );
642+ vec2 uv3 = uvToRenderTargetUV(p3.xy * (0.5 / p3.w) + 0.5 );
643+
644+ s0 = textureLod(sampler0_ssr, vec3 (uv0, 0.0 ), lod).rgb;
645+ s1 = textureLod(sampler0_ssr, vec3 (uv1, 0.0 ), lod).rgb;
646+ s2 = textureLod(sampler0_ssr, vec3 (uv2, 0.0 ), lod).rgb;
647+ s3 = textureLod(sampler0_ssr, vec3 (uv3, 0.0 ), lod).rgb;
648+ #endif
649+
650+ // Apply Absorption (Optimized: Use r1 as the representative path length)
651+ #if defined(MATERIAL_HAS_ABSORPTION)
652+ vec3 T = saturate(exp (- pixel.absorption * r1.d));
653+ s0 *= T;
654+ s1 *= T;
655+ s2 *= T;
656+ s3 *= T;
657+ #endif
658+
659+ // 6. Spectral Integration
660+ return max (K0 * s0 + K1 * s1 + K2 * s2 + K3 * s3, 0.0 );
661+ }
662+
663+ #endif
664+
529665vec3 evaluateRefraction(const PixelParams pixel, const vec3 n0, vec3 E) {
530666 vec3 Ft;
531667
532668 // Note: We use the average IOR for the roughness lod calculation.
533669
534670 // Roughness remapping so that an IOR of 1.0 means no microfacet refraction and an IOR
535671 // of 1.5 has full microfacet refraction
536- #if defined(MATERIAL_HAS_DISPERSION) && (REFRACTION_TYPE == REFRACTION_TYPE_SOLID)
537- float perceptualRoughness = mix (pixel.perceptualRoughnessUnclamped, 0.0 , saturate(pixel.etaIR[1 ] * 3.0 - 2.0 ));
538- #else
539672 float perceptualRoughness = mix (pixel.perceptualRoughnessUnclamped, 0.0 , saturate(pixel.etaIR * 3.0 - 2.0 ));
540- #endif
541673
542674#if REFRACTION_MODE == REFRACTION_MODE_CUBEMAP
543675 float lod = perceptualRoughnessToLod(perceptualRoughness);
@@ -548,10 +680,7 @@ vec3 evaluateRefraction(const PixelParams pixel, const vec3 n0, vec3 E) {
548680#endif
549681
550682#if defined(MATERIAL_HAS_DISPERSION) && (REFRACTION_TYPE == REFRACTION_TYPE_SOLID)
551- for (int i = 0 ; i < 3 ; i++ ) {
552- vec3 t = evaluateRefraction(pixel, n0, lod, pixel.etaIR[i], pixel.etaRI[i]);
553- Ft[i] = t[i];
554- }
683+ Ft = calculateDispersion(pixel, n0, lod);
555684#else
556685 Ft = evaluateRefraction(pixel, n0, lod, pixel.etaIR, pixel.etaRI);
557686#endif
0 commit comments