most games that have graphics options like the sumi-e/pencil character costumes in sf4 or the b&w filter from the resident evil games are either model/texture swaps or simple filters applied post processing, the actual shader encompasses everything you see (and dont) and is a large collection of values, a lot of coding, and quite a bit of math on the back end
void GenerateFGLookupTexture(Graphics::Texture* texture, RenderingDevice& device)
{
Graphics::TextureDesc textureDesc;
device.GetDesc(texture, textureDesc);
assert(textureDesc.format == Graphics::Format::G16R16);
assert(textureDesc.levels == 1);
const float deltaNdotV = 1.f / static_cast<float>(textureDesc.width);
const float deltaRoughness = 1.f / static_cast<float>(textureDesc.height);
// Lock texture
const Graphics::MappedData mappedData = device.Map(texture, 0, Graphics::MapType::Write, 0);
uint8* const RESTRICT dataPtr = reinterpret_cast<uint8*>(mappedData.data);
const uint numSamples = 512;
float roughness = 0.f;
for (uint v = 0; v < textureDesc.height; v++)
{
float NdotV = 0.f;
uint32* dst = reinterpret_cast<uint32*>(dataPtr + v * mappedData.rowPitch);
for (uint u = 0; u < textureDesc.width; u++, ++dst)
{
float a = 0;
float b = 0;
CalculateFGCoeff(NdotV, roughness, numSamples, a, b);
assert(a <= 1.f);
assert(b <= 1.f);
*dst = (PACKINTOSHORT_0TO1(b) << 16) | PACKINTOSHORT_0TO1(a);
NdotV += deltaNdotV;
}
roughness += deltaRoughness;
}
device.Unmap(texture, 0);
}
void PlaneHammersley(float& x, float& y, int k, int n)
{
float u = 0;
float p = 0.5f;
// FIXME Optimize by removing conditional
for (int kk = k; kk; p *= 0.5f, kk /= 2)
{
if (kk & 1)
{
u += p;
}
}
x = u;
y = (k + 0.5f) / n;
}
vec3 ImportanceSampleGGX(float x, float y, float a4)
{
const float PI = 3.1415926535897932384626433832795028841971693993751f;
// Convert uniform random variables x, y to a sample direction
const float phi = 2 * PI * x;
const float cosTheta = std::sqrt( (1 - y) / ( 1 + (a4 - 1) * y) );
const float sinTheta = std::sqrt(1 - cosTheta * cosTheta);
// Convert direction to cartesian coordinates
const vec3 H(sinTheta * std::cos(phi), sinTheta * std::sin(phi), cosTheta);
//D = a2 / (PI * std::pow(cosTheta * (a2 - 1) + 1, 2));
return H;
}
// Reference: GPU Gems 3 - GPU Based Importance Sampling
//s2012_pbs_physics_math_slides.pdf
vec3 ImportanceSampleBlinn(float x, float y, float specularPower)
{
const float PI = 3.1415926535897932384626433832795028841971693993751f;
// Convert uniform random variables x, y to a sample direction
const float phi = 2 * PI * x;
const float cosTheta = std::pow(y, 1.f / (specularPower + 1));
const float sinTheta = std::sqrt(1 - cosTheta * cosTheta);
// Convert direction to cartesian coordinates
const vec3 H(sinTheta * std::cos(phi), sinTheta * std::sin(phi), cosTheta);
//D = (specularPower + 2) / (2 * PI) * std::pow(cosTheta, specularPower);
return H;
}
float G_Schlick(float k, float NdotV, float NdotL)
{
return (NdotV * NdotL) / ( (NdotV * (1 - k) + k) * (NdotL * (1 - k) + k) );
}
void CalculateFGCoeff(float NoV, float roughness, uint numSamples, float& a, float& b)
{
// Work in a coordinate system where normal = vec3(0,0,1)
#define GGX 0
#define BLINN 1
#define G_SCHLICK 0
// Build view vector
const vec3 V(std::sqrt(1.0f - NoV * NoV), 0, NoV);
#if BLINN
const float blinnSpecularPower = std::pow(2.f, 13.f * roughness);
#elif GGX
const float GXX_a4 = std::pow(roughness, 4);
#endif
#if G_SCHLICK
const float G_k = std::pow(roughness + 1, 2.f) / 8.f;
#endif
a = 0;
b = 0;
for (uint i = 0; i < numSamples; ++i)
{
float x, y;
PlaneHammersley(x, y, i, numSamples);
// Microfacet specular model:
// f = D*G*F / (4*NoL*NoV)
// V = G / (NoL*NoV)
// Importance-based sampling:
// f / pdf
// Calculate random half vector based on roughness
#if BLINN
const vec3 H = ImportanceSampleBlinn(x, y, blinnSpecularPower);
// D and pdfH cancel each other so just set to 1
const float D = 1;
const float pdfH = D;
#elif GXX
const vec3 H = ImportanceSampleGGX(x, y, GXX_a4);
// D and pdfH cancel each other so just set to 1
const float D = 1;
const float pdfH = D;
#endif
// Calculate light direction
const vec3 L = 2 * dot( V, H ) * H - V;
const float NoL = saturate( L.z );
const float VoH = saturate( dot( V, H ) );
const float NoH = saturate( H.z );
const float LoH = saturate( dot( L, H ) ); // = VoH
// Convert pdf(H) to pdf(L)
// Reference: Torrance and Sparrow
//
graphics.stanford.edu/~boulos/papers/brdftog.pdf const float pdfL = pdfH / (4 * LoH);
if (NoL > 0)
{
#if G_SCHLICK
// FIXME NoV cancel out
const float G = G_Schlick(G_k, NoV, NoL);
const float V = G / (NoL * NoV);
#elif
const float V = 1;
#endif
const float G_Vis = D * V / 4 / pdfL;
const float Fc = std::pow(1 - VoH, 5.f);
// FIXME : NoL ? Part of the lighting eq but gives dark reflections at grazing angles. Need a better BRDF probably
a += (1 - Fc) * G_Vis * NoL;
b += Fc * G_Vis * NoL;
}
}
a /= numSamples;
b /= numSamples;
}
}</uint32*></uint8*></float></float>
it would be cool if as a bonus they figured out how to texture miriam so you get the shader 1 clothlike look in shader 3 as a model swap option or bonus costume, but actually changing shaders ingame would necessitate nearly the whole game being made twice to accomodate for it