Skip to main content

Link for demo video: YOUTUBE


Link for demo project: MEGA

Github

Callisto BRDF — UE4.27 Vite Fork Port Attempt

Reference

Based on Jorge Jimenez et al., "The Character Rendering Art of The Callisto Protocol", SIGGRAPH 2023 — Advances in Real-Time Rendering in Games.

Work in Progress

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

FilePurpose
ShadingCommon.ushAdds SHADINGMODELID_CALLISTO = 12, bumps SHADINGMODELID_NUM to 13
Definitions.usfDefault-defines MATERIAL_SHADINGMODEL_CALLISTO 0
ShadingModels.ushCore BxDF functions + CallistoBxDF, SubsurfaceProfileBxDF replacement
ShadingModelsMaterial.ushGBuffer packing for Callisto
DeferredShadingCommon.ushRegisters Callisto in HasCustomGBufferData, custom Anisotropy decode
BasePassCommon.ushEnables custom data writes for Callisto
BasePassPixelShader.usfTranslucency volume lighting guard
ClusteredDeferredShadingPixelShader.usfClustered light loop registration
TiledDeferredLightShaders.usfTiled light loop registration
AnisotropyRendering.cppAnisotropy pass compatibility
PrimitiveSceneInfo.cppAnisotropy relevance flag
Material.cppMaterial property activation (Opacity, Anisotropy, CustomData, AO pins)
MaterialShared.cppPin name overrides, custom attribute GUID registration
MaterialShader.cppShader stats + GetShadingModelString
MaterialInterface.cppbUsesAnisotropy relevance
HLSLMaterialTranslator.cppEmits MATERIAL_SHADINGMODEL_CALLISTO define
EngineTypes.hAdds MSM_CallistoBRDF to EMaterialShadingModel enum
MaterialExpressionShadingModel.hExposes MSM_CallistoBRDF in shading model picker
MaterialExpressions.cppUMaterialExpressionCallistoAdvancedParams implementation
PixelInspectorResult.cpp/.hEditor 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 NameShader VariableRangeDefault
OpacityRetroreflectionGBuffer.CustomData.g0–2561.0
AnisotropyDiffuse FresnelGBuffer.Anisotropy0–2561.0
Custom Data 0Smooth TerminatorGBuffer.CustomData.b0–10.5
Custom Data 1Diffuse Fresnel FalloffGBuffer.CustomData.a0–10.75
Ambient OcclusionRetroreflection FalloffGBuffer.GBufferAO0–10.75
(via custom node)Specular Fresnel FalloffGBuffer.CustomData.r0–10.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);
Anisotropy Scaling

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:

ParameterPaper MeaningHardcoded ValueLocation
TerminatorTintTint of the smooth shadow transition(0.5, 0.5, 0.5)CallistoBxDF
RetroflectionFresnelTintColour tint for retroreflection(1, 1, 1)CallistoBxDF
DiffuseFresnelTintColour tint for diffuse fresnel(1, 1, 1)CallistoBxDF
RetroreflectionTangentFalloff (mr)Tangent falloff for retro0.75CallistoBxDF
DiffuseFresnelTangentFalloff (mf)Tangent falloff for diffuse fresnel0.75CallistoBxDF

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.5 if unconnected.

This custom output uses the GUID for the attribute, compiled into the CallistoAdvancedParams shader function.

Callisto BRDF — Mathematical Reference

Source

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:

SlideContent
67Design Principles
68BRDF Structure
69–71Diffuse Fresnel (visual comparisons)
76–78Retroreflection (visual comparisons)
85Fresnel & Retroreflection math
86–87Smooth Terminator (visual comparisons)
90Smooth Terminator math
92Specular Fresnel Falloff (visual comparisons)
95Specular Fresnel Falloff math
98Parameter Tiers (Base / Advanced / Full)
125Functional Approximation: Proxima BRDF
128Proxima BRDF math
129–130Callisto + Proxima results (visual comparisons)
131Callisto + 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 2014CallistoMeaning
v\mathbf{v}ωo\omega_oView / outgoing direction
l\mathbf{l}ωi\omega_iIncident light direction
n\mathbf{n}(implicit via cosθ\cos\theta)Surface normal
h\mathbf{h}θh\theta_hHalf vector / half-angle
fff(ωi,ωo)f(\omega_i, \omega_o)BRDF
fdf_dflambertf_\text{lambert} or fproximaf_\text{proxima}Diffuse component
frf_rfggxf_\text{ggx}Specular component
α\alphaα\alphaMaterial roughness (GGX alpha)
αlin\alpha_\text{lin}Perceptually linear roughness (not used)
\langle \cdot \ranglex=max(x,0)\overline{x} = \max(x, 0)Clamped dot product
\lvert \cdot \rvertAbsolute dot product (not used)
ρ\rhoρ\rhoDiffuse reflectance / albedo
χ+(a)\chi^+(a)Heaviside function (not used)
LLLiL_i, LoL_oLighting 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:

r(x)=2(1x)r(x) = 2\,(1 - x)

Remaps a [0,1][0,1] parameter into the [0,2][0,2] range used as a Schlick exponent multiplier. At the default value x=0.5x = 0.5, r=1.0r = 1.0 which recovers standard Schlick behaviour.

t(x)=min(max(x,0),  1)=saturate(x)t(x) = \min(\max(x, 0),\; 1) = \text{saturate}(x)

Clamp to unit range.

xˉ=max(x,  0)\bar{x} = \max(x,\; 0)

Clamped value. Equivalent to DICE's \langle \cdot \rangle 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

SymbolNameRangeDefaultTier
ρf\rho_fDiffuse Fresnel[0,256][0, 256]1Base
nfn_fDiffuse Fresnel Falloff[0,1][0, 1]0.75Base
mfm_fDiffuse Fresnel Tangent Falloff[0,1][0, 1]0.75Full
ρr\rho_rRetroreflection[0,256][0, 256]1Base
nrn_rRetroreflection Falloff[0,1][0, 1]0.75Base
mrm_rRetroreflection Tangent Falloff[0,1][0, 1]0.75Full
nsn_sSpecular Fresnel Falloff[0,1][0, 1]0.5Advanced
ooSmooth Terminator[1,1][-1, 1]0Base
ppSmooth Terminator Length[0,1][0, 1]0.5Base
Parameter Tiers

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:

Lo=(flambert(ωi,ωo)+fggx(ωi,ωo))  Li  cosθi  dωiL_o = \Big( f_\text{lambert}(\omega_i, \omega_o) + f_\text{ggx}(\omega_i, \omega_o) \Big)\; L_i\; \overline{\cos\theta_i}\; d\omega_i

where flambert(ωi,ωo)=ρ/πf_\text{lambert}(\omega_i, \omega_o) = \rho / \pi.

Callisto BRDF (Full Form)

Slide 85 — second equation

Lo=(c1(θd,θh)  flambert(ωi,ωo)+fggx(ωi,ωo))  c2(θi)  Li  cosθi  dωiL_o = \Big( c_1(\theta_d, \theta_h)\; f_\text{lambert}(\omega_i, \omega_o) + f_\text{ggx}(\omega_i, \omega_o) \Big)\; c_2(\theta_i)\; L_i\; \overline{\cos\theta_i}\; d\omega_i

Two modulation coefficients wrap the baseline model:

  • c1c_1 — Diffuse Fresnel & Retroreflection (modulates diffuse lobe)
  • c2c_2 — Smooth Terminator (modulates both diffuse and specular at the shadow edge)

c1c_1 — Diffuse Fresnel & Retroreflection

Slide 85 — middle and bottom equations;

c1(θd,θh)=l(1,  ρf,  αf)Fresnel    l(1,  ρr,  αr)Retroreflectionc_1(\theta_d, \theta_h) = \underbrace{l\big(1,\; \boldsymbol{\rho}_f,\; \alpha_f\big)}_{\text{Fresnel}} \;\cdot\; \underbrace{l\big(1,\; \boldsymbol{\rho}_r,\; \alpha_r\big)}_{\text{Retroreflection}}

where ll denotes lerp (linear interpolation) and the blend weights αf\alpha_f, αr\alpha_r are evaluated with the hh function:

h(θ,  n,  φ,  m)=(1cosθ)5n    cosφ  5mh(\theta,\; n,\; \varphi,\; m) = \big(1 - \overline{\cos\theta}\big)^{5n} \;\cdot\; \overline{\cos\varphi}^{\;5m}

The Fresnel and retroreflection terms use swapped angle pairs — this is the key insight:

αf=h ⁣(θd,  r(nf),  θh,  r(mf))\alpha_f = h\!\big(\theta_d,\; r(n_f),\; \theta_h,\; r(m_f)\big) αr=h ⁣(θh,  r(nr),  θd,  r(mr))\alpha_r = h\!\big(\theta_h,\; r(n_r),\; \theta_d,\; r(m_r)\big)

The Fresnel component peaks at grazing view angles (θd\theta_d large), while retroreflection peaks when light bounces back toward the viewer (θh\theta_h large, i.e. front-lit). The tangent falloff parameters (mfm_f, mrm_r) control how the effect attenuates toward the tangent plane via the cosφ\cos\varphi term.

Default Behaviour

When ρf=1\rho_f = 1 and ρr=1\rho_r = 1 (defaults), c1=1c_1 = 1 everywhere — the diffuse lobe is unmodified, matching Lambert.


c2c_2 — Smooth Terminator

*Slide 90 — math;

c2(θi)=l(1,  s(0,  αsp,  cosθi),  αs,  o)c_2(\theta_i) = l\big(1,\; s(0,\; \alpha_s \cdot p,\; \cos\theta_i),\; \alpha_s,\; o\big)

where s=smoothsteps = \text{smoothstep} and the mask αs\alpha_s prevents the terminator from affecting areas already dominated by Fresnel or retroreflection:

αs=(1(1θd)3)(1(1θh)3)\alpha_s = \big(1 - (1 - \theta_d)^3\big)\,\big(1 - (1 - \theta_h)^3\big)
ParameterPurpose
oo (Smooth Terminator)Intensity / sign of the terminator softening. Range [1,1][-1, 1], default 00 (disabled).
pp (Smooth Terminator Length)Width of the smoothstep transition zone. Range [0,1][0, 1], default 0.50.5.

Modified Specular Fresnel

*Slide 95 — math;

The GGX specular lobe uses a parameterised Schlick Fresnel:

fggx(ωi,ωo)=FGD4cosθicosθvf_\text{ggx}(\omega_i, \omega_o) = \frac{F \cdot G \cdot D}{4\,\cos\theta_i\,\cos\theta_v}

with the modified Fresnel term:

F=f0+t ⁣(2r(ns))  (1f0)  (1cosθ)5r(ns)F = f_0 + t\!\big(2 - r(n_s)\big)\;\big(1 - f_0\big)\;\big(1 - \cos\theta\big)^{5\,r(n_s)}

This provides two controls via nsn_s:

  • Exponent scaling5r(ns)5 \cdot r(n_s) compresses or expands the Fresnel peak. At ns=0.5n_s = 0.5: r(0.5)=1.0r(0.5) = 1.0, exponent =5= 5, recovering standard Schlick.
  • Amplitude clampingt(2r(ns))t(2 - r(n_s)) can fully suppress the Fresnel boost when ns0n_s \to 0.

Proxima Diffuse

*Slide 128 — math;

Proxima is a functional approximation to a GGX-based microfacet diffuse BRDF, replacing Lambert as the base diffuse model:

fproxima(ωi,ωo)=ρπ  (α(0.55+0.19cosθi1)(1cosθk1/2)+1)f_\text{proxima}(\omega_i, \omega_o) = \frac{\rho}{\pi}\;\Big(\alpha\,\big(-0.55 + 0.19\,\overline{\cos\theta_i}^{-1}\big)\,\big(1 - \overline{\cos\theta_k}^{1/2}\big) + 1\Big)

where:

cosθk=VL\cos\theta_k = -\mathbf{V} \cdot \mathbf{L}

and α\alpha is the GGX alpha (roughness²).

warning

The α\alpha in the Proxima formula refers specifically to GGX alpha (roughness2\text{roughness}^2), not the same α\alpha used for Fresnel/retroreflection blend weights earlier. The slides note this distinction explicitly.

Numerical Stability

The slides recommend pre-multiplying by cosθi\cos\theta_i to remove the division (cosθi1\overline{\cos\theta_i}^{-1}), 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:

Lo=(c1(θd,θh)  fproxima(ωi,ωo)+fggx(ωi,ωo))  c2(θi)  Li  cosθi  dωiL_o = \Big( c_1(\theta_d, \theta_h)\; f_\text{proxima}(\omega_i, \omega_o) + f_\text{ggx}(\omega_i, \omega_o) \Big)\; c_2(\theta_i)\; L_i\; \overline{\cos\theta_i}\; d\omega_i

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:

TierAudienceParameters
BaseNon-Specialised UsersDiffuse Fresnel, Retroreflection, Diffuse Fresnel Falloff, Retroreflection Falloff, Smooth Terminator
AdvancedTechnical / Material ArtistsDiffuse Fresnel Tint, Retroreflection Tint, Smooth Terminator Tint, Specular Fresnel Falloff, Dual Specular Roughness Scale, Dual Specular Opacity
FullMachine LearningDiffuse 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.

MathHLSL FunctionLocation
r(x)=2(1x)r(x) = 2(1-x)Callisto_R(x)ShadingModels.ush
t(x)=saturate(x)t(x) = \text{saturate}(x)Callisto_T(x)ShadingModels.ush
h(θ,n,φ,m)h(\theta, n, \varphi, m)Callisto_H(CosTheta, FalloffParam, CosPhi, TangentParam)ShadingModels.ush
c1(θd,θh)c_1(\theta_d, \theta_h)GetCallistoC1(...)ShadingModels.ush
c2(θi)c_2(\theta_i) (Smooth Terminator)GetCallistoTerminator(NoL, LoH, NoV, Length, Tint)ShadingModels.ush
Modified Schlick FFCallisto_F_Schlick(f0, VoH, n_s)ShadingModels.ush
fproximaf_\text{proxima}GetProximaDiffuse(DiffuseColor, Roughness, NoL, VoL)ShadingModels.ush
Full BxDF evaluationCallistoBxDF(...)ShadingModels.ush
Single-lobe specularCallistoSpecularGGX(...)ShadingModels.ush
Dual-lobe specularDualSpecularGGX_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 (c1c_1)

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 (c2c_2)

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);
}
Proxima discrepancy -

The shader code writes (Alpha * TermA * TermB) + NoL which differs slightly from the slide's formula α(0.55+0.19cosθi1)(1cosθk1/2)+1\alpha(-0.55 + 0.19\,\overline{\cos\theta_i}^{-1})(1 - \overline{\cos\theta_k}^{1/2}) + 1. The slide formula multiplies the full bracket by ρ/π\rho/\pi and adds 11 inside, while the shader factors NoL\text{NoL} out of the reciprocal term and absorbs it into the additive constant. The result is equivalent when pre-multiplied by cosθi\cos\theta_i (as recommended in the slides for numerical stability) but the code path avoids the division.