Introduction

As a nerdy gamer who wasted 2000+ hours of life in competitive Team Fortress 2 gaming, I always loved the unique cartoon art style of the game. I looked at the shading paper from the game and it seems mixed with many shading techniques. I wonder if I can replicate the implementation of it.

Paper link: Illustrative Rendering in Team Fortress 2

The formula of TF2 shading

The shading in Team Fortress 2 (TF2) has a blend of cartoon and realistic styles. According to the conference paper of TF2 shading, a variety of view independent lighting and view dependent lighting are combined in the TF2 model shading. To be exact, the complete shader in Team Fortress 2 is merely the summation of the view independent lighting term and view dependent lighting term. The view-independent lighting term can be formulated as:

Kd[a(n)+jnum of lightsIdjw((α(nωj)+β)γ)]\begin{equation}K_d\left[a(\vec{n})+\sum^{\text{num of lights}}_{j} I_{d_j} w\left(\left(\alpha\left(\vec{n} \cdot \omega_j\right)+\beta\right)^\gamma\right)\right]\end{equation}

where Kd{K_d} is the diffuse component, a(n)a(\vec{n}) is the ambient light, n\vec{n} is the normalized surface normal at point p\vec{p}, ωj\omega_j is the normalized incoming light direction vector at point p\vec{p}, IdI_d is the light intensity at point p\vec{p} from light source. (α(nωj)+β)γ\left(\alpha\left(\vec{n} \cdot \omega_j\right)+\beta\right)^\gamma is essentially the Half Lambert term, but in Team Fortress 2 shading a different parameter setting is used such that α=0.5,β=0.5,γ=1\alpha = 0.5, \beta = 0.5, \gamma = 1. w()w() is an artistic warping function to tune the Half Lambert term.

On the other hand, the view-dependent light term can be formulated as:

jnum of lights[Idjksmax(fs(vrj)kspec ,frkr(vrj)krim )]+(nu)frkra(v)\begin{equation}\sum^{\text{num of lights}}_{j} \left[I_{d_j} k_s \max \left(f_s\left(\vec{v} \cdot \vec{r}_j\right)^{k_{\text {spec }}}, f_r k_r\left(\vec{v} \cdot \vec{r}_j\right)^{k_{\text {rim }}}\right)\right]+(\vec{n} \cdot \vec{u}) f_r k_r a(\vec{v})\end{equation}

Looks familar right? Thats the favourite equation of TF2 engineer:

where IdI_d is the light intensity at point p\vec{p} from light source. ksk_s is a specular mask painted into a texture channel. fsf_s is an artist-tuned Fresnel term for general specular highlights, frf_r is another Fresnel term used to mask rim highlights. v\vec{v} is the view direction. r\vec{r} is the reflected vector i.e. r=2(nωj)nωj\vec{r} = 2(\vec{n}\cdot \omega_j)\vec{n} - \omega_j. kspeck_\text{spec} is the specular exponent fetched from a texture map, krimk_\text{rim} is a constant exponent which controls the breadth of the rim highlights. (nu)(\vec{n}\cdot \vec{u}) is the rim effect result from the dot of the normal vector and up vector. a(v)a(\vec{v}) is the ambient light. From the phong terms, we can see the larger value of phong term will be selected, which means either the ‘spec’ phong term or ‘rim’ phong term will be selected.

Final result adding up two lights together.