Link for demo video: YOUTUBE
Link for demo project: MEGA
Callisto BRDF — UE4.27 Vite Fork Port Attempt
Based on Jorge Jimenez et al., "The Character Rendering Art of The Callisto Protocol", SIGGRAPH 2023 — Advances in Real-Time Rendering in Games.
This is a v1.1 partial port. Several features are incomplete, hardcoded, or diverge from the reference paper.
What This Port Does
This patch adds a new MSM_CallistoBRDF shading model to UE4.27. It implements a subset of the Callisto BRDF — the custom shading model developed by Striking Distance Studios for The Callisto Protocol — as a first-class deferred shading model alongside UE4's existing models (DefaultLit, SubsurfaceProfile, etc.). The Callisto BRDF augments the baseline Lambert + GGX model with:
- Diffuse Fresnel — Artistic control over the grazing-angle brightening/darkening of the diffuse lobe.
- Retroreflection — Control over how much light bounces back toward the viewer when a surface is front-lit.
- Smooth Terminator — Softens the harsh light/shadow boundary on curved surfaces.
- Specular Fresnel Falloff — Parameterises the Schlick exponent to compress or expand the specular Fresnel peak.
- Proxima Diffuse — A functional approximation to GGX-based microfacet diffuse (replaces Lambert).
Additionally, a modified SubsurfaceProfileBxDF is included that applies Callisto diffuse terms to skin rendering. You can delete it and uncomment the old Shading model as it was used for comparison purposes.
Files Changed
| File | Purpose |
|---|---|
| ShadingCommon.ush | Adds SHADINGMODELID_CALLISTO = 12, bumps SHADINGMODELID_NUM to 13 |
| Definitions.usf | Default-defines MATERIAL_SHADINGMODEL_CALLISTO 0 |
| ShadingModels.ush | Core BxDF functions + CallistoBxDF, SubsurfaceProfileBxDF replacement |
| ShadingModelsMaterial.ush | GBuffer packing for Callisto |
| DeferredShadingCommon.ush | Registers Callisto in HasCustomGBufferData, custom Anisotropy decode |
| BasePassCommon.ush | Enables custom data writes for Callisto |
| BasePassPixelShader.usf | Translucency volume lighting guard |
| ClusteredDeferredShadingPixelShader.usf | Clustered light loop registration |
| TiledDeferredLightShaders.usf | Tiled light loop registration |
| AnisotropyRendering.cpp | Anisotropy pass compatibility |
| PrimitiveSceneInfo.cpp | Anisotropy relevance flag |
| Material.cpp | Material property activation (Opacity, Anisotropy, CustomData, AO pins) |
| MaterialShared.cpp | Pin name overrides, custom attribute GUID registration |
| MaterialShader.cpp | Shader stats + GetShadingModelString |
| MaterialInterface.cpp | bUsesAnisotropy relevance |
| HLSLMaterialTranslator.cpp | Emits MATERIAL_SHADINGMODEL_CALLISTO define |
| EngineTypes.h | Adds MSM_CallistoBRDF to EMaterialShadingModel enum |
| MaterialExpressionShadingModel.h | Exposes MSM_CallistoBRDF in shading model picker |
| MaterialExpressions.cpp | UMaterialExpressionCallistoAdvancedParams implementation |
| PixelInspectorResult.cpp/.h | Editor Pixel Inspector support |
GBuffer Layout & Material Inputs
Material Pin Mapping
When MSM_CallistoBRDF is selected, the standard UE4 material pins are repurposed. These renames are registered in MaterialShared.cpp.
| UE4 Pin (Default Name) | Callisto Repurposed Name | Shader Variable | Range | Default |
|---|---|---|---|---|
| Opacity | Retroreflection | GBuffer.CustomData.g | 0–256 | 1.0 |
| Anisotropy | Diffuse Fresnel | GBuffer.Anisotropy | 0–256 | 1.0 |
| Custom Data 0 | Smooth Terminator | GBuffer.CustomData.b | 0–1 | 0.5 |
| Custom Data 1 | Diffuse Fresnel Falloff | GBuffer.CustomData.a | 0–1 | 0.75 |
| Ambient Occlusion | Retroreflection Falloff | GBuffer.GBufferAO | 0–1 | 0.75 |
| (via custom node) | Specular Fresnel Falloff | GBuffer.CustomData.r | 0–1 | 0.5 |
The Specular Fresnel Falloff is the only parameter that requires a custom material expression node (CallistoAdvancedParams) because UE4's built-in pin budget for a shading model is exhausted by the other five parameters.
GBuffer Packing (ShadingModelsMaterial.ush)
#if MATERIAL_SHADINGMODEL_CALLISTO
else if (ShadingModel == SHADINGMODELID_CALLISTO)
{
GBuffer.CustomData.r = CallistoAdvancedParams0(MaterialParameters); // SpecularFresnelFalloff
GBuffer.CustomData.g = Opacity; // Retroreflection
GBuffer.CustomData.b = GetMaterialCustomData0(MaterialParameters); // TerminatorLength
GBuffer.CustomData.a = GetMaterialCustomData1(MaterialParameters); // DiffuseFresnelFalloff
}
#endif
GBuffer Unpacking (CallistoBxDF)
float TerminatorLength = saturate(GBuffer.CustomData.b);
float DiffuseFresnel = saturate(GBuffer.Anisotropy) * 256.0f;
float DiffuseFresnelFalloff = saturate(GBuffer.CustomData.a);
float Retroreflection = GBuffer.CustomData.g;
float RetroReflectionFalloff= GBuffer.GBufferAO;
float SpecularFresnelFalloff= saturate(GBuffer.CustomData.r);
DiffuseFresnel is multiplied by 256 on unpack because according to the paper its in range of 0-256.Anisotropy is stored in the GBuffer normalised to [0,1], however, the GBuffer stores Anisotropy through the anisotropy pass pipeline — which normally remaps to [−1,1]. A special-case decode is added in DeferredShadingCommon.ush to skip the 2x - 1 remap for SHADINGMODELID_CALLISTO, keeping it in raw [0,1]. However I am pretty sure its messed up.
Hardcoded Advanced Parameters
Several parameters documented in the paper are hardcoded in the shader and not exposed to the material editor:
| Parameter | Paper Meaning | Hardcoded Value | Location |
|---|---|---|---|
TerminatorTint | Tint of the smooth shadow transition | (0.5, 0.5, 0.5) | CallistoBxDF |
RetroflectionFresnelTint | Colour tint for retroreflection | (1, 1, 1) | CallistoBxDF |
DiffuseFresnelTint | Colour tint for diffuse fresnel | (1, 1, 1) | CallistoBxDF |
RetroreflectionTangentFalloff (mr) | Tangent falloff for retro | 0.75 | CallistoBxDF |
DiffuseFresnelTangentFalloff (mf) | Tangent falloff for diffuse fresnel | 0.75 | CallistoBxDF |
These are all exposed as per-material parameters in the original Callisto implementation. Exposing them would require either more custom output nodes or packing them into unused GBuffer channels. The logic behind these being hardcoded, is low overall impact.
CallistoAdvancedParams Custom Output Node
A new UMaterialExpressionCallistoAdvancedParams expression is added. It exposes a single input pin:
- SpecularFresnelFalloff — Float, defaults to
0.5if unconnected.
This custom output uses the GUID for the attribute, compiled into the CallistoAdvancedParams shader function.
Callisto BRDF — Mathematical Reference
Jorge Jimenez, "The Character Rendering Art of The Callisto Protocol", SIGGRAPH 2023 — Advances in Real-Time Rendering in Games.
Developed with Jose Naranjo and Miguel Rodriguez. Proxima BRDF additionally credits Jon Diego and Jay Ryness.
Key Slides — Key math slides referenced throughout this page:
| Slide | Content |
|---|---|
| 67 | Design Principles |
| 68 | BRDF Structure |
| 69–71 | Diffuse Fresnel (visual comparisons) |
| 76–78 | Retroreflection (visual comparisons) |
| 85 | Fresnel & Retroreflection math |
| 86–87 | Smooth Terminator (visual comparisons) |
| 90 | Smooth Terminator math |
| 92 | Specular Fresnel Falloff (visual comparisons) |
| 95 | Specular Fresnel Falloff math |
| 98 | Parameter Tiers (Base / Advanced / Full) |
| 125 | Functional Approximation: Proxima BRDF |
| 128 | Proxima BRDF math |
| 129–130 | Callisto + Proxima results (visual comparisons) |
| 131 | Callisto + Proxima combined math |
Mathematical Notation
Design principles: Slide 67 — "using simple arithmetic", "avoiding magic numbers", "orthogonal parameters"
The Callisto BRDF uses a notation style that differs from the DICE/Frostbite 2014 PBR convention (Lagarde & de Rousiers). The table below tries to map between the two so we can follow and port Callisto's math without friction.
Core Symbols
| DICE 2014 | Callisto | Meaning |
|---|---|---|
| View / outgoing direction | ||
| Incident light direction | ||
| (implicit via ) | Surface normal | |
| Half vector / half-angle | ||
| BRDF | ||
| or | Diffuse component | |
| Specular component | ||
| Material roughness (GGX alpha) | ||
| — | Perceptually linear roughness (not used) | |
| Clamped dot product | ||
| — | Absolute dot product (not used) | |
| Diffuse reflectance / albedo | ||
| — | Heaviside function (not used) | |
| , | Lighting function (Callisto distinguishes incident / outgoing) |
Callisto Helper Functions
Slide 85 — bottom-left block
The BRDF is built from three small helper functions that appear throughout:
Remaps a parameter into the range used as a Schlick exponent multiplier. At the default value , which recovers standard Schlick behaviour.
Clamp to unit range.
Clamped value. Equivalent to DICE's bracket notation but applied to scalars rather than dot products.
Callisto-Specific Parameters
Slide 85 — right-side parameter table; Slide 90 — terminator params; Slide 95 — specular param
| Symbol | Name | Range | Default | Tier |
|---|---|---|---|---|
| Diffuse Fresnel | 1 | Base | ||
| Diffuse Fresnel Falloff | 0.75 | Base | ||
| Diffuse Fresnel Tangent Falloff | 0.75 | Full | ||
| Retroreflection | 1 | Base | ||
| Retroreflection Falloff | 0.75 | Base | ||
| Retroreflection Tangent Falloff | 0.75 | Full | ||
| Specular Fresnel Falloff | 0.5 | Advanced | ||
| Smooth Terminator | 0 | Base | ||
| Smooth Terminator Length | 0.5 | Base |
The original Callisto implementation in exposes parameters in three tiers — Base (5 params for non-specialised users), Advanced (adds tints + specular controls), and Full (all parameters, including tangent falloffs, intended for ML fitting). See Parameters for the full tier breakdown.
BRDF Formulation
Baseline: Lambert + GGX
Slide 68 — BRDF structure diagram; Slide 85 — top equation
The industry-standard diffuse + specular decomposition:
where .
Callisto BRDF (Full Form)
Slide 85 — second equation
Two modulation coefficients wrap the baseline model:
- — Diffuse Fresnel & Retroreflection (modulates diffuse lobe)
- — Smooth Terminator (modulates both diffuse and specular at the shadow edge)
— Diffuse Fresnel & Retroreflection
Slide 85 — middle and bottom equations;
where denotes lerp (linear interpolation) and the blend weights , are evaluated with the function:
The Fresnel and retroreflection terms use swapped angle pairs — this is the key insight:
The Fresnel component peaks at grazing view angles ( large), while retroreflection peaks when light bounces back toward the viewer ( large, i.e. front-lit). The tangent falloff parameters (, ) control how the effect attenuates toward the tangent plane via the term.
When and (defaults), everywhere — the diffuse lobe is unmodified, matching Lambert.
— Smooth Terminator
*Slide 90 — math;
where and the mask prevents the terminator from affecting areas already dominated by Fresnel or retroreflection:
| Parameter | Purpose |
|---|---|
| (Smooth Terminator) | Intensity / sign of the terminator softening. Range , default (disabled). |
| (Smooth Terminator Length) | Width of the smoothstep transition zone. Range , default . |
Modified Specular Fresnel
*Slide 95 — math;
The GGX specular lobe uses a parameterised Schlick Fresnel:
with the modified Fresnel term:
This provides two controls via :
- Exponent scaling — compresses or expands the Fresnel peak. At : , exponent , recovering standard Schlick.
- Amplitude clamping — can fully suppress the Fresnel boost when .
Proxima Diffuse
*Slide 128 — math;
Proxima is a functional approximation to a GGX-based microfacet diffuse BRDF, replacing Lambert as the base diffuse model:
where:
and is the GGX alpha (roughness²).
The in the Proxima formula refers specifically to GGX alpha (), not the same used for Fresnel/retroreflection blend weights earlier. The slides note this distinction explicitly.
The slides recommend pre-multiplying by to remove the division (), improving numerical stability at grazing angles.
Final Combined Form: Callisto + Proxima
*Slide 131 — math;
The shipped form replaces Lambert with Proxima inside the Callisto wrapper:
This is an approximation — the mathematically correct approach would be to supersample the full Callisto BRDF with Proxima as the microfacet diffuse model, but the slides show the visual difference is negligible.
Callisto BRDF Parameters
Slide 98
The original implementation in UE5 organises parameters into three tiers:
| Tier | Audience | Parameters |
|---|---|---|
| Base | Non-Specialised Users | Diffuse Fresnel, Retroreflection, Diffuse Fresnel Falloff, Retroreflection Falloff, Smooth Terminator |
| Advanced | Technical / Material Artists | Diffuse Fresnel Tint, Retroreflection Tint, Smooth Terminator Tint, Specular Fresnel Falloff, Dual Specular Roughness Scale, Dual Specular Opacity |
| Full | Machine Learning | Diffuse Fresnel Tangent Falloff, Retroreflection Tangent Falloff, Smooth Terminator Length |
Math → Shader Mapping
This table maps each mathematical expression to its HLSL implementation in the UE4.27 port.
| Math | HLSL Function | Location |
|---|---|---|
Callisto_R(x) | ShadingModels.ush | |
Callisto_T(x) | ShadingModels.ush | |
Callisto_H(CosTheta, FalloffParam, CosPhi, TangentParam) | ShadingModels.ush | |
GetCallistoC1(...) | ShadingModels.ush | |
| (Smooth Terminator) | GetCallistoTerminator(NoL, LoH, NoV, Length, Tint) | ShadingModels.ush |
| Modified Schlick | Callisto_F_Schlick(f0, VoH, n_s) | ShadingModels.ush |
GetProximaDiffuse(DiffuseColor, Roughness, NoL, VoL) | ShadingModels.ush | |
| Full BxDF evaluation | CallistoBxDF(...) | ShadingModels.ush |
| Single-lobe specular | CallistoSpecularGGX(...) | ShadingModels.ush |
| Dual-lobe specular | DualSpecularGGX_Callisto(...) | ShadingModels.ush |
Shader Code: Helper Functions
float Callisto_R(float x)
{
return 2.0f * (1.0f - saturate(x));
}
float Callisto_T(float x)
{
return saturate(x);
}
float Callisto_H(float CosTheta, float FalloffParam, float CosPhi, float TangentParam)
{
float n = Callisto_R(FalloffParam);
float m = Callisto_R(TangentParam);
float ExpN = 5.0f * n;
float ExpM = 5.0f * m;
float Term1 = pow(1.0f - saturate(CosTheta), ExpN);
float Term2 = pow(saturate(CosPhi), ExpM);
return Term1 * Term2;
}
Shader Code: Diffuse Fresnel & Retroreflection ()
float3 GetCallistoC1(
float NoL, float NoV,
float PF_Intensity, float3 PF_Tint, float NF_Falloff, float MF_TangentFalloff,
float PR_Intensity, float3 PR_Tint, float NR_Falloff, float MR_TangentFalloff)
{
float AlphaF = Callisto_H(NoL, NF_Falloff, NoV, MF_TangentFalloff);
float3 TargetF = PF_Intensity * PF_Tint;
float3 ComponentF = lerp(float3(1,1,1), TargetF, AlphaF);
float AlphaR = Callisto_H(NoV, NR_Falloff, NoL, MR_TangentFalloff);
float3 TargetR = PR_Intensity * PR_Tint;
float3 ComponentR = lerp(float3(1,1,1), TargetR, AlphaR);
return ComponentF * ComponentR;
}
Shader Code: Smooth Terminator ()
float3 GetCallistoTerminator(float NoL, float LoH, float NoV, float Length, float3 Tint)
{
float MaskD = 1.0 - Pow3(1.0 - saturate(LoH));
float MaskH = 1.0 - Pow3(1.0 - saturate(NoV));
float AlphaS = MaskD * MaskH;
float SmoothEdge = max(AlphaS * Length, 0.001f);
float S = smoothstep(0.0f, SmoothEdge, NoL);
return lerp(float3(1,1,1), float3(S, S, S), AlphaS * Tint);
}
Shader Code: Modified Specular Fresnel
float3 Callisto_F_Schlick(float3 f0, float VoH, float n_s)
{
float r = Callisto_R(n_s);
float exponent = 5.0f * r;
float t_term = Callisto_T(2.0f - r);
float base = max(1.0f - saturate(VoH), 0.0001f);
float3 F = f0 + t_term * (1.0f - f0) * pow(base, exponent);
return F;
}
Shader Code: Proxima Diffuse
float3 GetProximaDiffuse(float3 DiffuseColor, float Roughness, float NoL, float VoL)
{
float Alpha = Roughness * Roughness;
float CosThetaK = -VoL;
float TermA = -0.55f * NoL + 0.19f;
float TermB = 1.0f - sqrt(saturate(CosThetaK));
float Bracket = (Alpha * TermA * TermB) + NoL;
return (DiffuseColor / PI) * max(0.0f, Bracket);
}
The shader code writes (Alpha * TermA * TermB) + NoL which differs slightly from the slide's formula . The slide formula multiplies the full bracket by and adds inside, while the shader factors out of the reciprocal term and absorbs it into the additive constant. The result is equivalent when pre-multiplied by (as recommended in the slides for numerical stability) but the code path avoids the division.