Skip to content

Commit ad2c291

Browse files
authored
dispersion in XYZ space (#9572)
- spectral reconstructions with 4 samples - unroll the whole dispersion computation to improve performance (batch texture fetches and reuse common values) - tool to generate the matrices for spectral integration
1 parent 52ffd80 commit ad2c291

File tree

7 files changed

+828
-26
lines changed

7 files changed

+828
-26
lines changed

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -976,6 +976,7 @@ if (IS_HOST_PLATFORM)
976976
add_subdirectory(${TOOLS}/roughness-prefilter)
977977
add_subdirectory(${TOOLS}/specular-color)
978978
add_subdirectory(${TOOLS}/uberz)
979+
add_subdirectory(${TOOLS}/specgen)
979980
endif()
980981

981982
# Generate exported executables for cross-compiled builds (Android, WebGL, and iOS)

shaders/src/surface_light_indirect.fs

Lines changed: 144 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -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

462485
void 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+
529665
vec3 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

shaders/src/surface_lighting.fs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,10 @@ struct PixelParams {
6262
#endif
6363

6464
#if defined(MATERIAL_HAS_REFRACTION)
65-
#if defined(MATERIAL_HAS_DISPERSION) && (REFRACTION_TYPE == REFRACTION_TYPE_SOLID)
66-
vec3 etaRI;
67-
vec3 etaIR;
68-
#else
6965
float etaRI;
7066
float etaIR;
67+
#if defined(MATERIAL_HAS_DISPERSION) && (REFRACTION_TYPE == REFRACTION_TYPE_SOLID)
68+
float dispersion;
7169
#endif
7270
float transmission;
7371
float uThickness;

shaders/src/surface_shading_lit.fs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -110,15 +110,10 @@ void getCommonPixelParams(const MaterialInputs material, inout PixelParams pixel
110110
float materialIor = max(1.0, material.ior);
111111
#endif
112112

113-
#if defined(MATERIAL_HAS_DISPERSION) && (REFRACTION_TYPE == REFRACTION_TYPE_SOLID)
114-
float halfSpread = (materialIor - 1.0) * 0.025 * material.dispersion;
115-
vec3 iors = vec3(materialIor - halfSpread, materialIor, materialIor + halfSpread);
116-
117-
pixel.etaIR = vec3(airIor) / iors; // air -> material
118-
pixel.etaRI = iors / vec3(airIor); // material -> air
119-
#else
120113
pixel.etaIR = airIor / materialIor; // air -> material
121114
pixel.etaRI = materialIor / airIor; // material -> air
115+
#if defined(MATERIAL_HAS_DISPERSION) && (REFRACTION_TYPE == REFRACTION_TYPE_SOLID)
116+
pixel.dispersion = material.dispersion;
122117
#endif
123118
#if defined(MATERIAL_HAS_TRANSMISSION)
124119
pixel.transmission = saturate(material.transmission);

tools/specgen/CMakeLists.txt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
cmake_minimum_required(VERSION 3.19)
2+
project(specgen)
3+
4+
set(TARGET specgen)
5+
6+
# ==================================================================================================
7+
# Source files
8+
# ==================================================================================================
9+
set(SRCS specgen.cpp)
10+
11+
# ==================================================================================================
12+
# Target definitions
13+
# ==================================================================================================
14+
add_executable(${TARGET} ${SRCS})
15+
target_link_libraries(${TARGET} PRIVATE getopt math utils)
16+
set_target_properties(${TARGET} PROPERTIES FOLDER Tools)
17+
18+
# =================================================================================================
19+
# Licenses
20+
# ==================================================================================================
21+
set(MODULE_LICENSES getopt libpng tinyexr libz stb)
22+
set(GENERATION_ROOT ${CMAKE_CURRENT_BINARY_DIR}/generated)
23+
list_licenses(${GENERATION_ROOT}/licenses/licenses.inc ${MODULE_LICENSES})
24+
target_include_directories(${TARGET} PRIVATE ${GENERATION_ROOT})
25+
26+
# ==================================================================================================
27+
# Installation
28+
# ==================================================================================================
29+
install(TARGETS ${TARGET} RUNTIME DESTINATION bin)

0 commit comments

Comments
 (0)