Processing math: 100%

Saturday, January 31, 2015

BSSRDF Importance Sampling 6 - params tweaking, test render and future wishlist

This post actually has nothing to do with importance sampling at all already :P To get some relatively more interesting render images, I tried to tweak the reflection albedo for some different colors, but how does that get mapped to corresponding σa and σs ?(what BSSRDF takes as constructor input)

In Jensen 01 there is a BRDF approximation of BSSRDF based on the assumption that the incident illumination is uniform, and they can integrate the dipole diffusion approximation to get a total diffuse reflectance Rd at a surface intersection point:
Rd=2π0rRd(r)dr=α2(1+e43A3(1α))e3(1α)

Jensen 02 further purposed that using this approximated Rd(the diffusion reflection color) and diffuse mean free path ld (the average distance light travels in medium before scattering, my understanding is how translucent the medium is) as input parameters to derived out σa and σs
1. first get reduced albedo α from above diffuse reflectance equation (the function is hard to invert but it's monotonic so Jensen suggests just using numerical approximation)
2. get effective transport coefficient: σtr1/ld
3. get reduced extinction coefficient: σt=σtr3(1α)
4. get reduced scatter coefficient: σs=ασt
5, get absorb coefficient: σa=σtσs
and we got the ingredient to feed in to BSSRDF. The code looks like this (the numerical approximation part I just modify it from pbrt, it actually looks kinda like some google style interview question :P)
void BSSRDF::convertFromDiffuse(const Color& Kd,
const Color& diffuseMeanFreePath, float A,
Color* absorb, Color* scatterPrime) {
float diffuse[3] = {Kd.r, Kd.g, Kd.b};
float sigmaTr[3] = {
1.0f / diffuseMeanFreePath.r,
1.0f / diffuseMeanFreePath.g,
1.0f / diffuseMeanFreePath.b};
float sigmaSPrime[3];
float sigmaA[3];
// RGB channel
for(int i = 0; i < 3; ++i) {
// use few binary search iterations to approximate alpha prime
// since there is no easy way to inverse diffuseReflectance method
// directly...
float alphaLow = 0.0f;
float alphaHigh = 1.0f;
float rdLow = diffuseReflectance(alphaLow, A);
float rdHigh = diffuseReflectance(alphaHigh, A);
for(int j = 0; j < 16; ++j) {
float alphaMid = 0.5f * (alphaLow + alphaHigh);
float rdMid = diffuseReflectance(alphaMid, A);
if(rdMid > diffuse[i]) {
alphaHigh = alphaMid;
rdHigh = rdMid;
} else {
alphaLow = alphaMid;
rdLow = rdMid;
}
}
float alphaPrime = 0.5f * (alphaLow + alphaHigh);
float sigmaTPrime = sigmaTr[i] /
sqrt(3.0f * (1.0f - alphaPrime));
sigmaSPrime[i] = alphaPrime * sigmaTPrime;
sigmaA[i] = sigmaTPrime - sigmaSPrime[i];
}
*scatterPrime = Color(sigmaSPrime[0], sigmaSPrime[1], sigmaSPrime[2]);
*absorb = Color(sigmaA[0], sigmaA[1], sigmaA[2]);
}
float BSSRDF::diffuseReflectance(float alphaPrime, float A) {
float sqrtTerm = sqrt(3.0f * (1.0f - alphaPrime));
return 0.5f * alphaPrime *
(1.0f + exp(-(4.0f / 3.0f) * A * sqrtTerm)) *
exp(-sqrtTerm);
}
with the above tweaking method in hand, we got some test rendering images, all use a same random Rd color (0.478431, 0.513725, 0.521569) from color picker and different mean free path parameters (1-8 from up to down, you can see it gets more and more translucent, while I feel the last few is kinda way too over the top already :P ):









and a diffuse Lambert render reference:

And it's postmortem time, what's not there and should get addressed later:
1. more dynamic splitting: at this moment each BSSRDF probe ray sample only pair with one light sample. For simple lighting setup maybe works fine, but it definitely going to be a pain when the lighting getting more complex. Should make it available to specify how many lighting sample can be pair with each probe ray sample. Also the single scattering evaluation share the same lighting sample at this moment, they can probably get split up too or let user have the option to skip single scattering(which is also quite expensive to evaluate but relatively subtle effect for BSSRDF material like skin)

2. look up surface mesh in scene hierarchy with material id: we are still shooting probe ray to the whole scene graph for intersection test, which is not necessarily and should only do the intersection test against the surface mesh with same material. If the probe ray only intersect with a limited portion of surface, it would definitely speed up the integration process.

3. indirect lighting. Yes I confess...we only calculate the direct lighting for each light sample we cast out for BSSRDF so all our GI information is only appeared on those specular reflection area. The naive implementation on this shouldn't be hard (just like regular path tracing shoot out bounce path for each probe intersection to gather bounce lighting information) but I'm kinda afraid that it's gonna be really expensive. Thinking about solution at this moment......

4. use different max probe radius Rmax for different spectrum channel. There is only one probe radius for all spectrum channel now, but the diffusion profile shows clearly that red channel last much longer than the other two, it doesn't make sense to use the same probe radius for all of them. Kulla 13 mentioned about they also random picking the probe radius with different diffusion profile and combine the result with MIS for their skin rendering, while I haven't moved forward to this part yet.

5. the σs and σa coefficient are measured in per mm unit, which I believe the scale of geometry will cause look difference at this moment, it would be good if we can come up some normalization mechanics for these coefficient tweaking so the artistic control can be more intuitive.

There should be more, and I'll append them when they pop up in my mind :)


Saturday, January 24, 2015

BSSRDF Importance Sampling 5 - Integrating BSSRDF and single scattering evaluation

The BSSRDF model Jensen 01 propose is "a sum of the diffusion approximation and the single scattering term"
S(xi,ωi,xo,ωo)=Sd(xi,ωi,xo,ωo)+S(1)(xi,ωi,xo,ωo)

The previous few posts all surrounds on the topic about: "how to get Sd(xi,ωi,xo,ωo) sample?", "How to get the sample distributed well for importance sampling?" Now we can put these parts all together :) Last look (for now) on the integration equation:

L(xo,ωo)=AΩLi(xi,ωi)Sd(xi,ωi,xo,ωo)cos(θ)dωdA

The basic flowchart of this part will be:
1. when intersect a BSSRDF material, start figure out the probe radius Rmax (this can be precomputed actually but considering σa and σtr may varying in texture space, I made the computation on the fly for now)
2. casting out probe rays for scene intersection test (we can actually just do the intersection test with this particular surface with BSSRDF material instead of intersection test the whole scene. This is a BIG TODO in my mind now for efficiency concern)
3. for each probe ray intersection sample point, calculate the irradiance (the current method is picking light based on power distribution, and only casting one lighting sample for each probe ray intersection, we can definitely do splitting here!(multiple lighting sample with one BSSRDF sample) another BIG TODO item)
4. evaluate Sd(xi,ωi,xo,ωo)=1πFr(η,ωi)Rd(xi,xo)Fr(η,ωo), the two Fresnel terms can be calculated with ray direction, intersection normal and the refraction index
5. add up the 3. 4. calculation with appropriate MIS weight we mentioned in the previous post, and divide it with corresponding pdf (pick the light, pick the geometry point on light surface, pick the axis, pick the disc sample, the ray/intersection normal dot product, all need to be taken into account)

and here's the naive implementation with the above flowchart:

Color Renderer::LbssrdfDiffusion(const ScenePtr& scene,
const Fragment& fragment, const BSSRDF* bssrdf, const Vector3& wo,
const Sample& sample,
const BSSRDFSampleIndex* bssrdfSampleIndex,
WorldDebugData* debugData) const {
const Vector3& pwo = fragment.getPosition();
float coso = absdot(wo, fragment.getNormal());
float eta = bssrdf->getEta();
float Ft = 1.0f - Material::fresnelDieletric(coso, 1.0f, eta);
float sigmaTr = bssrdf->getSigmaTr(fragment).luminance();
const vector<Light*>& lights = scene->getLights();
// figure out the sample probe radius, we ignore the integration
// of area with pdf too small compare to center, yes...this introduces
// bias...but can limit the probe ray as short as possible
// skipRatio = pdf(r) / pdf(0, 0) = exp(-sigmaTr * r^2)
float skipRatio = 0.01f;
float Rmax = sqrt(log(skipRatio) / -sigmaTr);
Color Lmultiscatter(0.0f);
for(uint32_t i = 0; i < bssrdfSampleIndex->samplesNum; ++i) {
const BSSRDFSample bssrdfSample(sample, *bssrdfSampleIndex, i);
// sample a probe ray with gaussian falloff pdf from intersection
Ray probeRay;
float discPdf;
BSSRDFSampleAxis axis = bssrdf->sampleProbeRay(fragment,
bssrdfSample, sigmaTr, Rmax, &probeRay, &discPdf);
Intersection probeIntersect;
float epsilon;
// TODO we can use a material indexed lookup table to query
// surface with this particular BSSRDF instead of doing a whole
// scene intersaction test
if(scene->intersect(probeRay, &epsilon, &probeIntersect)) {
if(probeIntersect.getMaterial()->getBSSRDF() == bssrdf) {
const Fragment& probeFragment = probeIntersect.fragment;
const Vector3& pProbe = probeFragment.getPosition();
Color Rd = bssrdf->Rd(probeFragment,
squaredLength(pProbe - pwo));
// calculate the irradiance on the sample point
float pickLightPdf;
int lightIndex = mPowerDistribution->sampleDiscrete(
bssrdfSample.uPickLight, &pickLightPdf);
const Light* light = lights[lightIndex];
Vector3 wi;
float lightPdf;
Ray shadowRay;
const Vector3& ni = probeFragment.getNormal();
Color L = light->sampleL(pProbe, epsilon, bssrdfSample.ls,
&wi, &lightPdf, &shadowRay);
if(L == Color::Black || lightPdf == 0.0f ||
scene->intersect(shadowRay)) {
continue;
}
float cosi = absdot(ni, wi);
Color irradiance = L * cosi / (lightPdf * pickLightPdf);
float Fti = 1.0f -
Material::fresnelDieletric(cosi, 1.0f, eta);
// evaluate the MIS weight
float pdf = discPdf * absdot(probeRay.d, ni);
float w = bssrdf->MISWeight(fragment, probeFragment, axis,
pdf, sigmaTr, Rmax);
Lmultiscatter +=
(w * INV_PI * Ft * Fti * Rd * irradiance) / pdf;
}
}
}
Lmultiscatter /= (float)bssrdfSampleIndex->samplesNum;
return Lmultiscatter;
}
Lots of stuff can be further optimized at this moment, but let's give it a quick taste on the new dishes :P, with just a little bit seasoning pepper: 25 samples

For comparison I put another simplest Lambert shade version. Same camera/lighting settings, same samples number:
we can see the dark edges(the head and tail part) in BRDF shading got soften a lot in BSSRDF since the lighting contribution from nearby area, also the bumpy body become not that hard lined in BSSRDF version.

Now moving to the single scattering part, as the term suggests, this part simulate the case that light specular refract into medium, encounter a scatter event to change its direction, refract out the medium and enter the camera. (or think in the other way: camera shoot eye rays with importance, refract into the medium, during the travel in the medium, it absorbs photon that emit from light and refract into the medium) The following images illustrate the single scattering idea:

'
Image taken from Hery 03, the whole radiance received by this eye ray would be the integration alone the refract ray pTo in the medium

Put in Fresnel, scatter event, phase function into accout, Jensen 01 derive the equation for each sample point alone the refracted eye ray:

L(1)(xo,ωo)=(σs(xo)Fp(ωi,ωo)/σtc)esiσt(xi)esoσt(xo)Li(xi,ωi)


another round of scary symbol sort:
F=Fr(xo,η)Fr(xi,η) : the Fresnel factor
σa: the absorb coefficient
σs: the scatter coefficient
p: Henry-Greenstein phase function, similar to BRDF for surface, this describe the scatter property for volume
ωi: photon Li refraction direction
ωo: eye ray refraction direction
G=∥nxiωo/nxiωi : geometry factor
σt=σa+σs : the extinction coefficient
σtc=σt(xo)+Gσt(xi) : combined extinction coefficient
si: the refracted photon traveling distance
so: the refracted eye ray traveling distance

For the refract photon distance si, it is actually pretty difficult to sample correctly since there is a specular refraction direction change from the inside medium sample to light sample point. Jensen proposed an approximation for the true refracted distance with Snell's law:
si=sinxiωi1(1η)2(1nxiωi2)

where si is the distance between sample point in medium to light sample point minus the part this ray is not in the medium.

Jensen also suggested that we importance sampling alone the eye ray with pdf(d)=σteσtd since the exponential attenuation alone the medium. This 1D sample was covered in previous post , so we are pretty much ready to go!

The flowchar for single scattering will be:
1. when intersect a BSSRDF material, change the ray direction with specular refraction
2. alone the refraction ray sample point with 1D exponential falloff
3. for each sample point from 2. , sample light source and casting shadow ray to see if there is other geometry blocking the light besides the current medium (need to be a little bit careful on this part when playing around with the shadow ray length)
4. approximate si with Snell's law, evaluate Fresnel term and phase function

put them into codes:

Color Renderer::LbssrdfSingle(const ScenePtr& scene,
const Fragment& fragment, const BSSRDF* bssrdf, const Vector3& wo,
const Sample& sample,
const BSSRDFSampleIndex* bssrdfSampleIndex,
WorldDebugData* debugData) const {
const Vector3& pwo = fragment.getPosition();
const Vector3& no = fragment.getNormal();
float coso = absdot(wo, fragment.getNormal());
float eta = bssrdf->getEta();
float Ft = 1.0f - Material::fresnelDieletric(coso, 1.0f, eta);
float sigmaTr = bssrdf->getSigmaTr(fragment).luminance();
Color scatter = bssrdf->getScatter(fragment);
Color sigmaT = bssrdf->getAttenuation(fragment);
Vector3 woRefract = Goblin::specularRefract(wo, no, 1.0f, eta);
const vector<Light*>& lights = scene->getLights();
Color Lsinglescatter(0.0f);
// single scattering part
for(uint32_t i = 0; i < bssrdfSampleIndex->samplesNum; ++i) {
const BSSRDFSample bssrdfSample(sample, *bssrdfSampleIndex, i);
// sample a distance with exponential falloff
float d = exponentialSample(bssrdfSample.uSingleScatter, sigmaTr);
Vector3 pSample = pwo + d * woRefract;
float samplePdf = exponentialPdf(d, sigmaTr);
// sample a light from pSample
float pickLightPdf;
int lightIndex = mPowerDistribution->sampleDiscrete(
bssrdfSample.uPickLight, &pickLightPdf);
const Light* light = lights[lightIndex];
Vector3 wi;
float epsilon, lightPdf;
Ray shadowRay;
Color L = light->sampleL(pSample, 1e-5f, bssrdfSample.ls,
&wi, &lightPdf, &shadowRay);
if(L == Color::Black || lightPdf == 0.0f) {
continue;
}
// preserve the maxt since the next intersection test will modify
// it to the closest intersection, but we still need this maxt to
// cast shadow ray from that intersection to light
float maxt = shadowRay.maxt;
Intersection wiIntersect;
// TODO we can use a material indexed lookup table to query
// surface with this particular BSSRDF instead of doing a whole
// scene intersaction test
if(scene->intersect(shadowRay, &epsilon, &wiIntersect)) {
// if not, the pSample is out of the BSSRDF geometry already
if(wiIntersect.getMaterial()->getBSSRDF() == bssrdf) {
// update shadow ray to start from pwi
const Fragment& fwi = wiIntersect.fragment;
const Vector3& pwi = fwi.getPosition();
const Vector3& ni = fwi.getNormal();
shadowRay.mint = shadowRay.maxt + epsilon;
shadowRay.maxt = maxt;
if(!scene->intersect(shadowRay)) {
float p = bssrdf->phase(wi, woRefract);
float cosi = absdot(ni, wi);
float Fti = 1.0f -
Material::fresnelDieletric(cosi, 1.0f, eta);
Color sigmaTi = bssrdf->getAttenuation(fwi);
float G = absdot(ni, woRefract) / cosi;
Color sigmaTC = sigmaT + G * sigmaTi;
float di = length(pwi - pSample);
float et = 1.0f / eta;
float diPrime = di * absdot(wi, ni) /
sqrt(1.0f - et * et * (1.0f - cosi * cosi));
Lsinglescatter += (Ft * Fti * p * scatter / sigmaTC) *
expColor(-diPrime * sigmaTi) *
expColor(-d * sigmaT) * L /
(lightPdf * pickLightPdf * samplePdf);
}
}
}
}
Lsinglescatter /= (float)bssrdfSampleIndex->samplesNum;
return Lsinglescatter;
}
and we got the single scattering part :) (Though we still face the similar further optimization in TODO list like splitting and direct surface intersection test)

One more! in Jensen 01's final demo image marble bust he added a Fresnel reflection pass. Though it's not necessary, I feel that shiny sparkling highlight is pretty charming. Plus, we already have that in our BRDF, so I just directly plug it into the new Subsurface material:
All the jigsaw pieces are put in place now. Finally! Put them altogether, we have this shiny marble cutie :)

So how's the multiple importance sampling benefit we have? That's actually the most exciting part! For comparison, I render another image with sample alone normal axis only without multiple importance sampling, same 25 samples.
And the side by side comparison (left side no MIS, right side with MIS):

In the highly curvy surface, The MIS version did a pretty good job removing those most noticeable variance noise, while in the relatively flat surface(the dragon right side body part), project all samples alone normal has a better result.  Kulla 13 did mentioned about this part that there will be performance lost in overall flat surface since we would have extra wasted sample projected alone dpdu/dpdv and intersect nothing. The algorithm generally remove the spiky high variance but introduce some relatively more uniform variance, which is easier to converge when the sample amount got cranked up.

Image taken from Kulla 13, explains the issue that MIS doesn't work particular well for overall flat surface

Alright! TODO item 5, 6 done! I probably will post up some more interesting rendering images later, but these few posts summarize what my research/interpretation about Kulla 13 and Jensen 01. Math is definitely not my strongest field so if there's anything wrong in the post. Please let me know. Thanks!



Friday, January 23, 2015

BSSRDF Importance Sampling 4 - multiple axis probe ray sampling

First of all, Big big thanks to Chris Kulla for borrowing the slide pictures and the explanation on the way he calculated the max probe radius in paper! This is by far the most fun part in this project spring.

Yet another look on BSSRDF integration equation......
L(xo,ωo)=AΩLi(xi,ωi)S(xi,ωi,xo,ωo)cos(θ)dωdA

Let the inside integration be F(xi)=ΩLi(xi,ωi)S(xi,ωi,xo,ωo)cos(θ)dω. This part should be pretty similar to BRDF calculation: we sample the light, calculate the irradiance, apply BSSRDF and cos(θ)...Then we integrate this F(xi) over the surface area. It should look like this: AF(xi)dA , and its Monte Carlo Estimator will look like this:
1NF(Xi)pdfA(Xi)

We have the 2D sample technique and its corresponding pdfdisc in hand now, how we convert it into pdfA ? If the surface is a flat surface and we project the disc sample in its normal direction (towdown) , then this two pdf should be the same. If we encounter the curvy surface, then pdfdisc=pdfA  . Since A=Anxonxi , and pdfA:pdfA=1A:1A , we have pdfA=pdfAnxonxi∥=pdfdiscnxonxi . This can be interpreted in a more intuitive way: we sample by projecting down based on nxo,  and if nxi is almost perpendicular to  nxo, then chance it got sampled is almost 0 since nxonxi approaches 0 . The following image illustrates the idea:

image taken from Kulla 13, the right most red sample point will have a relative low area pdf since its surface normal is almost perpendicular to the disc sample center xo

uhoh......In this case, if the nearby surface of xo all have really different normal direction from nxo, even the top down projection disc sampling make flat surface sampling ideal, we would get bunch of sample with really small pdfA since lots of the sample may have nxonxi approaches 0, and this can be source of variance noise! It makes sense since highly curvy geometry info means high frequency data in local area, and it does need more sample to converge. Besides, one of dipole diffusion approximation's assumptions is that the surface should be semi-infinite flat.

BSSRDF Importance Sampling (I'll just call it Kulla 13 or BIS later) proposed a simple and elegant solution for the above issue: instead of only doing the disc sampling based on normal direction projection, they also do the sampling alone the orthogonal axis (the slides mentioned that the orthogonal axis can be arbitrary, and their experiment show shading tangents work pretty well). After taking samples from three different axis (normal, dpdu, dpdv), they further weight sum the samples with multiple importance sampling technique so that the sample taken from normal projection but falling on almost vertical neighbors (and also, the sample taken from tangent projection but falling on almost horizontal neighbors) never get high contribution, and the sample that is appropriate for corresponding case can get relatively higher weight. Below two screen shots taken from Kulla 13 illustrate the idea pretty clear:



For further optimization, the talk also suggests that by specifying a max probe radius Rmax, we can limit the sample probe ray length to just cover nearby region. so the intersection test can be terminated faster. My understanding about this part, is that we sacrifice part of the accuracy for efficiency: since we only casting sample within 0-Rmax range, there will be surface point with non zero irradiance got assigned pdf 0, and it means we introduce bias in this case. The bias probably won't matter that much as long as the pdf integration got ignored is relatively small. After all, the BSSRDF we have is already an approximation model derived from semi-infinite flat surface, while most of the geometry we tried to render don't satisfy this assumption. 

And now we try to get these probe rays, first thing to do is to figure out the max probe radius Rmax. What I did was using a skip threshold to discard the r distance that its pdf compare to pdf(center) is way to small, something like this: 
// skipRatio = pdf(r) / pdf(0, 0) = exp(-sigmaTr * r^2)
float skipRatio = 0.01f;
float Rmax = sqrt(log(skipRatio) / -sigmaTr);
view raw get_rmax.cpp hosted with ❤ by GitHub
There is a magic value Kulla 13 suggest in the paper 12.46v that I was really not exactly sure how they come up the value. After asking original author Chris about this, he said that the value was calculated by integrate the Gaussian profile from 0 to r to get a value really close to 1, something like 0.9999, and the r value will be the Rmax. I did a quick experiment with this: 2π0Rmax012πver2/2vdrdθ=Rmax01ver2/2vdr=1eR2max/2v=0.998 then we can get result: Rmax=12.429216v That's pretty close to the magic value Chris suggest :) The other fun fact is that the alternative method I used earlier actually come up the same math equation to solve at last. While my threshhold value is a more aggressive 0.01, the idea is pretty similar: discard the sample with too small contribution.

With Rmax in hand, now we gonna pick the axis to project sample disc. Kulla 13 suggested the ratio N:U:V = 2:1:1. I pretty much hardcode this dice throwing probability to the code. Few things to notice: 
1. besides the pdfdisc, we now need to take into account the probability each axis got pick up(since they are 2:1:1, pdfN=0.5, pdfU=0.25, pdfV=0.25 ). 
2. the probe ray length should be based on its distance to disc center, the longer the distance, the shorter the length.
3. dpdu and dpdv are not necessarily normalized values for their other purpose like anti-alias, we probably need to normalize it for initializing ray's direction (at least in my case)

The code snippet:
enum BSSRDFSampleAxis {
UAxis,
VAxis,
NAxis
};
BSSRDFSampleAxis BSSRDF::sampleProbeRay(const Fragment& fragment,
const BSSRDFSample& sample, float sigmaTr, float Rmax,
Ray* probeRay, float* pdf) const {
Matrix3 shadeToWorld = fragment.getWorldToShade().transpose();
const Vector3& pwo = fragment.getPosition();
// sample disk with gaussian falloff
Vector2 pSample = gaussianSample2D(
sample.uDisc[0], sample.uDisc[1], sigmaTr, Rmax);
float halfProbeLength = sqrt(Rmax * Rmax - pSample.squaredLength());
// figure out we should sample alone U or V or N
// The chance to pick up U, V, N is 1 : 1 : 2
BSSRDFSampleAxis axis;
if(sample.uPickAxis <= 0.5f) {
probeRay->o = pwo + shadeToWorld *
Vector3(pSample.x, pSample.y, -halfProbeLength);
probeRay->d = fragment.getNormal();
axis = NAxis;
*pdf = 0.5f;
} else if(sample.uPickAxis <= 0.75f) {
probeRay->o = pwo + shadeToWorld *
Vector3(-halfProbeLength, pSample.x, pSample.y);
probeRay->d = normalize(fragment.getDPDU());
axis = UAxis;
*pdf = 0.25f;
} else {
probeRay->o = pwo + shadeToWorld *
Vector3(pSample.y, -halfProbeLength, pSample.x);
probeRay->d = normalize(fragment.getDPDV());
axis = VAxis;
*pdf = 0.25f;
}
probeRay->mint = 0.0f;
probeRay->maxt = 2.0f * halfProbeLength;
*pdf *= gaussianSample2DPdf(pSample.x, pSample.y, sigmaTr, Rmax);
return axis;
}


And here's a screenshot of testing with debug line visualizing the probe ray on one particular pixel(the red dot), probe rays with normal axis direction are blue, and dpdu for red, dpdv for green, the yellow dots are the probe ray intersection that we gonna do the irradiance calculation. YUP! That pixel is quite expensive!!

 
OK with all those probe rays we had, how we assign the appropriate weight sum for them with multiple importance sampling? According to Veach 97, the multi-importance estimator looks like this:
F=ni=11ninij=1ωi(Xi,j)f(Xi,j)pdfi(Xi,j)

where n is the number of sampling techniques (in this case: n=3 we sampling in three ways, from normal, from dpdu and from dpdv), and ni is the number of sample for technique i. (in this case, n0=2c, n1=c, n2=c, c is a constant since we casting probe ray with ratio N:U:V=2:1:1) I used the power heuristic approach Veach propose:
ωi=(nipdfi(Xi,j))2/nk=1(nkpdfk(Xk,j))2

The constant c got divided out in above equation so it doesn't matter we assign ni with 2,1,1 or 4,2,2 or 2c, 1c, 1c. As for pdfk(Xk,j), it stands for the pdf evaluation on Xi,j with sampling technique k where k ranges from 1 to ni. (In English: "I got a sample point by picking N axis and Gaussian disc sampling around it, intersect the surface at point p, what's the pdfA of this sample if I pick U or V axis and Gaussian disc sample around that axis projecting down to point p?") Friendly reminder: this pdf include the pdf picking the axis, the pdf of disc sampling and projecting it down to surface. (I actually forget putting pdf picking the axis into account in the first place and trouble shoot it for a few moments > <)
Put this into code:
float BSSRDF::MISWeight(const Fragment& fo, const Fragment& fi,
BSSRDFSampleAxis mainAxis, float pdf, float sigmaTr,
float Rmax) const {
float weight = 0.0f;
// The U, V, N ratio is 1 : 1 : 2, and we do MIS with
// power heuristic, so 1: 1: 4
const Vector3& pwo = fo.getPosition();
const Vector3& pwi = fi.getPosition();
const Vector3& ni = fi.getNormal();
if(mainAxis == NAxis) {
const Vector3& u = normalize(fo.getDPDU());
const Vector3& v = normalize(fo.getDPDV());
float UPdf = 0.25f *
gaussianSample2DPdf(pwo, pwi, u, sigmaTr, Rmax) *
absdot(u, ni);
float VPdf = 0.25f *
gaussianSample2DPdf(pwo, pwi, v, sigmaTr, Rmax) *
absdot(v, ni);
float numerator = 4 * pdf * pdf;
weight = numerator / (numerator + UPdf* UPdf + VPdf * VPdf);
} else if(mainAxis == UAxis) {
const Vector3& n = fo.getNormal();
const Vector3& v = normalize(fo.getDPDV());
float NPdf = 0.5f *
gaussianSample2DPdf(pwo, pwi, n, sigmaTr, Rmax) *
absdot(n, ni);
float VPdf = 0.25f *
gaussianSample2DPdf(pwo, pwi, v, sigmaTr, Rmax) *
absdot(v, ni);
float numerator = pdf * pdf;
weight = numerator / (4 * NPdf * NPdf + numerator + VPdf * VPdf);
} else if(mainAxis == VAxis) {
const Vector3& n = fo.getNormal();
const Vector3& u = normalize(fo.getDPDU());
float NPdf = 0.5f *
gaussianSample2DPdf(pwo, pwi, n, sigmaTr, Rmax) *
absdot(n, ni);
float UPdf = 0.25f *
gaussianSample2DPdf(pwo, pwi, u, sigmaTr, Rmax) *
absdot(u, ni);
float numerator = pdf * pdf;
weight = numerator / (4 * NPdf * NPdf + UPdf * UPdf + numerator);
}
return weight;
}
This post summarizes the TODO item 4, we have all the fresh ingredients in hand now. The next post will be about putting them together to get the BSSRDF integration. 

BSSRDF Importance Sampling 3 - exponential falloff sampling on 1D and 2D space

Take one more look on our BSSRDF integration equation:
L(xo,ωo)=AΩLi(xi,ωi)S(xi,ωi,xo,ωo)cos(θ)dωdA


The solid angle part seems to be easier since we already got methods to sampling lights at specified intersection point for our previous work on BRDF integration. The area integral part is more interesting: how we integrate the BSSRDF surface. Sure the naive way will be uniform sample the BSSRDF surface area, but look at that dipole model we got, that radically symmetric fast falloff: how much variance we gonna suffer before converge the clean render if we do things by uniform sampling? (and how much ray casting we gonna do to figure out the projection point on surface?) There are definitely some better ways......we should definitely try importance sampling.

OK importance sampling, then what is the dominant factor in that Rd equation? In Jensen01, he mentioned "we use standard Monte Carlo techniques to randomly sample the surface with density (σtreσtrd) at some distance d from xo" which means he suggests the dominant factor to be σtreσtrd, though I feel σtreσtrd/d2 is the closest one. Jensen said the word, let's try it~ time to brush up the inverse method sampling


-------------------------------WARNING DEAD END ENTRY!-------------------------------
I actually fail to get this answer derived out and change the route at last, but still think I should write down what I thought at that time...if you know how to get this derivation out right, please definitely let me know! I'm really curious about it

pdf in 2D space: pdf(x,y)=ceσtrx2+y2 where c is the constant we try to figure out to normalize the pdf
transform it into polar coordinate, the Jacobian is:
Jr=(dxdrdxdθdydrdydθ)=r

so pdf(r,θ)=rpdf(x,y)=creσtrr
cdf(r,θ)=2π00pdf(r,θ)drdθ=2πc0reσtrrdr=1

now integration by part:

uvdr=uvuvdr
u=r and dvdr=eσtrr =>
v=eσtrr/σtr and dvdr=1
0reσtrrdr=reσtrrσtr|r=r=00eσtrrσtrdr=1σ2tr(σtr+1)eσtrr|r=r=0=1σ2tr=12cπ
c=σ2tr2π
pdf(r,θ)=σ2tr2πreσtrr

almost there, now integrate out the θ part to get the marginal pdf(r)=σ2trreσtrr
then cdf(r)=r0σ2trreσtrrdr=1(σtrr+1)eσtrr
and now we just need to get the inverse function of r......and then I stuck........I can't seem to find a easy way to inverse this function......

By doing some google work, it seems that I bump into something called Laplace Transform Inverse , and this stuff kinda out of my college freshman level calculus....Hery 03 seems use a precompute table to solve this sampling part, he also kindly offered the source code for precomuting process, but I'm not sure if I really get what that code does......maybe go learn some advanced calculus can shed some lights for me on this issue, until then, I need to find some workaround.....

-----------------------------------EXIT OF THE DEAD END-----------------------------------

How about Gaussian? Like I mentioned in previous post, we can approximate dipole model with weight sum of Gaussian filters, plus Gaussian's falloff speed is higher than the above exponential falloff, consider there's a (1+σtrd)/d3 factor the above pdf didn't take into account. Maybe Gaussian turned out to be a better fit?


Out of curiosity, I throw in the above 3 normalized equation into some online math drawing tool, the result shows up in above image: green for eσtrd (the Jensen suggestion), red for eσtrd2 (Gaussian, which is my guess), blue for (1+σtrd)eσtr/d3 (which is probably the closest factor to dipole model, but looks really hard to figure out the inverse function) , and ya, Gaussian actually fit closer to the dipole model in this case. Worst of all, it falls off with d increase so it's always better than uniform sample. importance sampling works as long as the pdf is closer to original function, but doesn't need to perfectly fit, right?

So we try to do things Gaussian way now, let's try the inverse method again:

pdf in 2D space: pdf(x,y)=ceσtr(x2+y2) where c is the constant we try to figure out to normalize the pdf
so pdf(r,θ)=rpdf(x,y)=creσtrr2
cdf(r,θ)=2π00pdf(r,θ)drdθ=2πc0reσtrr2dr=2πc(eσtrr22σtr|r=r=0)=cπσtr=1
c=σtrπ
pdf(r,θ)=σtrπreσtrr2

integrate out the theta factor:
pdr(r)=2σtrreσtrr2
cdf(r)=r0pdf(r)=eσtrr2|r0=1eσtrr2
inverse function:
ξ=1eσtrr2
r=ln(1ξ)σtr
and we can further simplified it to:
r=ln(ξ)σtr
since ξ is uniform distribution between 0 and 1
and θ=2πξ

whew~~that's much easier than the previous one....

The above case works when we assume the sample distribute in the whole 2D space( >, how if we want to sample on a disc with radius Rmax? You can actually just replace the integration range to Rmax to derive it out the result. I'll just list the result here directly since it's almost the exact same process:
r=ln(1ξ(1eσtrR2max))σtr
θ=2πξ
pdf(x,y)=(σtrπeσtrr(x2+y2))/(1eσtrRmax)
The reason I brought up this explicit radius version is that we gonna use it later and the original BSSRDF Importance Sampling paper use this as their sampling method. This post basically explain how that equation in paper come out. :)  (it may looks tiny different but if you replace the 12v in paper with σtr , they are exactly the same thing)

Last bonus one, sample in 1D with distribution proportional to eax , this one is used in BSSRDF single scattering and actually the easiest one to derive.

pdf(x)=ceax
cdf(x)=0ceaxdx=caeax|0=ca=1
c=a
pdf(x)=aeax
cdf(x)=x0aeaxdx=1eax
ξ=1eax
x=ln(1ξ)a
simplified to
x=lnξa

Boiling those stuff down to codes are just the following few lines:

/*
* let falloff be a
* since the pdf is proportional to exp(-a * (x * x + y * y))
* let pdf(x, y) = c * exp(-a * (x * x + y * y))
* convert to spherical coordinate:
* pdf(r, theta) = c * r * exp(-a * r * r)
* integrate c * r * exp(-a * r * r) over r (from 0 to inf) and theta = 1 ->
* c = a / pi -> pdf(r, theta) = a / pi * r * exp(-a * r * r) ->
* pdf(x, y) = a / pi * exp(-a * (x * x + y * y))
* marginal pdf(r) = integrage pdf((r, theta) over 0 to 2pi
* = 2 * a * r * exp(-a * r * r)
* conditional pdf(theta|r) = 1 / 2pi
* cdf(r) = integrate 2 * a * r * exp(-a * r * r) over 0 to r ->
* cdf(r) = 1 - exp(-a * r * r)
* cdf(theta) = integrate 1 / 2pi over 0 to theta = theta / 2pi
* inverse method:
* u1 = 1 - exp(-a * r * r) -> r = sqrt(ln(1 - u1) / -a) ->
* r = sqrt(ln(u1) / -a) since u1 is 0-1 uniform distribution
* u2 = theta / 2pi -> theta = 2pi * u2
* transform it back from spherical coordinate:
* x = r * cos(theta)
* y = r * sin(theta)
*/
Vector2 gaussianSample2D(float u1, float u2, float falloff) {
float r = sqrtf(log(u1) / -falloff);
float theta = TWO_PI * u2;
return Vector2(r * cos(theta), r * sin(theta));
}
/*
* similar to the above gaussianSample2D, but the integrate range of r
* from 0 to Rmax, which makes us able to sample a disc with radius Rmax in
* gaussian distribution
*/
Vector2 gaussianSample2D(float u1, float u2, float falloff, float Rmax) {
float r = sqrtf(log(1.0f - u1 * (1.0f - exp(-falloff * Rmax * Rmax))) /
-falloff);
float theta = TWO_PI * u2;
return Vector2(r * cos(theta), r * sin(theta));
}
/*
* integrate c * exp(-falloff * x) from 0 to inf = 1 ->
* c = falloff -> pdf(x) = falloff * exp(-falloff * x)
* cdf(x) = 1 - exp(-falloff * x) -> u = 1 - exp(-falloff * x) ->
* x = -ln(1 - u) / falloff -> simplified to x = -ln(u) / falloff
* since u are [0, 1) uniform distribution
*/
float exponentialSample(float u, float falloff) {
return -log(u) / falloff;
}
float gaussianSample2DPdf(const Vector3& pCenter,
const Vector3& pSample, const Vector3& N, float falloff) {
Vector3 d = pSample - pCenter;
Vector3 projected = d - N * dot(d, N);
return INV_PI * falloff * exp(-falloff * squaredLength(projected));
}
float gaussianSample2DPdf(const Vector3& pCenter,
const Vector3& pSample, const Vector3& N, float falloff, float Rmax) {
return gaussianSample2DPdf(pCenter, pSample, N, falloff) /
(1.0f - exp(-falloff * Rmax * Rmax));
}
float gaussianSample2DPdf(float x, float y, float falloff) {
return INV_PI * falloff * exp(-falloff * (x * x + y * y));
}
float gaussianSample2DPdf(float x, float y, float falloff,
float Rmax) {
return gaussianSample2DPdf(x, y, falloff) /
(1.0f - exp(-falloff * Rmax * Rmax));
}
// project pSample on the plane form by pCenter and N, then calculate
// the corresponding gaussian 2D pdf
float gaussianSample2DPdf(const Vector3& pCenter,
const Vector3& pSample, const Vector3& N, float falloff);
float gaussianSample2DPdf(const Vector3& pCenter,
const Vector3& pSample, const Vector3& N, float falloff, float Rmax);
float exponentialPdf(float x, float falloff) {
return falloff * exp(-falloff * x);
}
And here's some testing result with our freshly cooked sampling strategy, for comparison I put in another set of uniform disc sampling implemented based on Shirly 97

left up: 100 Gaussian sample with max radius 2 and falloff 2
right up: 1024 Gaussian sample with max radius 2 and falloff 2
left down: 100 uniform disc sample with radius 2
right down: 1024 uniform disc sample with radius 2

Now we have the TODO list item 3 in hand. We can start to look into the multiple importance sampling method BSSRDF Importance Sampling proposed, which in my opinion, is the most exciting part for this round.


Thursday, January 22, 2015

BSSRDF Importance Sampling 2 - Dipole Diffusion Approximation101

Super quick review on the dipole diffusion approximation model.
Compare to the classic BRDF integration:
L(xo,ωo)=Le(xo,ωo)+ΩLi(xi,ωi)fr(ωi,ωo)cos(θ)dω

This is BSSRDF integration:
L(xo,ωo)=AΩLi(xi,ωi)S(xi,ωi,xo,ωo)cos(θ)dωdA


In short words, to evaluate the radiance come out from surface point xo with direction ωo, we need to integrate over the whole surface area (the AdA part ), at each surface point xi, we integrate over the hemisphere(the Ωdω part) that for radiance L(xi,ωi), how much portion arrive at point xo with direction ωo(the S(xi,ωi,xo,ωo) part)

the BSSRDF S(xi,ωi,xo,ωo) is a 4 dimensional function, but most of the time it got simplified to radially symmetric and become a 1D distance funtion Rd(d) where d is distance between xo and xi.

Dipole Diffusion Approximation made assumptions that:
-light distribution  in highly scattering media tends to become isotropic. (My understanding for this part is that when light enter into medium with really high Albedo, after multiple scattering event happens, the light scatter distribution will become more and more uniform, and approach 1/4π at last)
-the surface is semi-infinite flat surface since for finite media there is no general way to solve diffusion equation in analytic way.

By putting two point light source above and under the medium(that's why it's called dipole method), we can get the analytic solution for fluence on the semi-infinite flat surface (OK...."we" not including "me" at this moment......I confess...... Donner 06 thesis Chapter 5 contains a detail derivation on this, while I just grab the result equation directly...)

Rd(r)=α4π[zr(σtrdr+1)eσtrdrdr3+zv(σtrdv+1)eσtrdvdv3]


After a topological sort on those scary symbols:
σa: absorb coefficient, describe how much radiance got absorbed in medium
σs: scatter coefficient, describe how much radiance got scattered in medium
g: anisotropy parameter (range from -1 to 1, -1 for fully backward scattering, 1 for fully forward scattering, 0 for isotropy)
σs=σs(1g): reduced scattering coefficient
σt=σs+σa: reduced extinction coefficient
α=σs/σt: the reduced albedo
σtr=3σaσt: the effective transport coefficient
Fr: the Fresnel formula for the reflection at a dielectric surface
Fdr=ΩFr(η,nω)(nω)dω: the average diffuse Fresnel reflectance
A=(1+Fdr)/(1Fdr): the coefficient takes into account the effect of internal diffuse reflection
D=13σt: the diffusion constant
zr=1/σt: the distance beneath surface where we put the positive dipole light
zv=zr+4AD: the distance above surface where we put the negative dipole light
dr=∥xxr: the distance from x to the positive dipole light
dv=∥xxv: the distance from x to the negative dipole light

Hopefully this dipole image illustrates the thing a little bit clearer...

At last, we put into account the Fresnel reflection at the boundary for both the incoming light and the outgoing light, we get:
Sd(xi,ωi,xo,ωo)=1πFr(η,ωi)Rd(xi,xo)Fr(η,ωo)

Image taken from GPU Gems3, showing the BSSRDF falloff pretty quick when the distance between xo and xi increases, we can also see that red channel tend to resist more before fully extinct, this sorta explains the idea human skin have that red texture underneath

For Fresnel diffuse reflection Fdr , it's an integration that can be approximated with the following code snippet. It was taken from Donner 06 too:

inline float BSSRDF::Fdr(float eta) {
// see Donner. C 2006 Chapter 5
// the internal Fresnel reflectivity
// approximated with a simple polynomial expansion
if(eta < 1.0f) {
return -0.4399f + 0.7099f / eta - 0.3319f / (eta * eta) +
0.0636f / (eta * eta * eta);
} else {
return -1.4399f / (eta * eta) + 0.7099f / eta + 0.6681f +
0.0636f * eta;
}
}
YUUUP.....we got the diffusion approximation part BSSRDF Sd(xi,ωi,xo,ωo) now, and it looks pretty mathmatically daunting (again, for me....). So, I encapsulate all these stuff into the BSSRDF black box, and hopefully I don't need to see them most of the time :P The constructor takes σa, σs, refraction index η, anisotropy parameter g , the rest of the stuff it can cook indoor. Something looks like this:

BSSRDF::BSSRDF(const ColorTexturePtr& absorb,
const ColorTexturePtr& scatterPrime, float eta, float g):
mAbsorb(absorb), mScatterPrime(scatterPrime),
mEta(eta), mG(g) {
float fdr = Fdr(eta);
mA = (1.0f + fdr) / (1.0f - fdr);
}
Color BSSRDF::Rd(const Fragment& fragment, float d2) const {
// see Donner. C 2006 Chapter 5 for the full derivation
// of the following disffusion dipole approximation equation
Color sigmaA = mAbsorb->lookup(fragment);
Color sigmaSPrime = mScatterPrime->lookup(fragment);
Color sigmaTPrime = sigmaA + sigmaSPrime;
Color sigmaTr = sqrtColor(3.0f * sigmaA * sigmaTPrime);
Color one(1.0f);
Color zr = one / sigmaTPrime;
// zv = zr + 4AD where D = 1/(3 * sigmaT') = zr / 3
Color zv = zr * (1.0f + 4.0f / 3.0f * mA);
Color dr = sqrtColor(zr * zr + Color(d2));
Color dv = sqrtColor(zv * zv + Color(d2));
Color alphaPrime = sigmaSPrime / sigmaTPrime;
Color sTrDr = sigmaTr * dr;
Color sTrDv = sigmaTr * dv;
Color rd = 0.25f * INV_PI * alphaPrime * (
(zr * (one + sTrDr) * expColor(-sTrDr) / (dr * dr * dr)) +
(zv * (one + sTrDv) * expColor(-sTrDv) / (dv * dv * dv)));
return clampColor(rd);
}
view raw BSSRDF_Rd.cpp hosted with ❤ by GitHub
OK! That pretty much sums up the TODO list item1, now moves on to item2, get a separate integration for BSSRDF. This is just open another cut in point for some future implementation, pretty straightforward for small side project scale code base:

Color Renderer::Lsubsurface(const ScenePtr& scene,
const Intersection& intersection, const Vector3& wo,
const Sample& sample,
const BSSRDFSampleIndex* bssrdfSampleIndex) const {
const MaterialPtr& material =
intersection.primitive->getMaterial();
const BSSRDF* bssrdf = material->getBSSRDF();
const vector<Light*>& lights = scene->getLights();
if(bssrdf == NULL || lights.size() == 0) {
return Color(0.0f);
}
const Fragment& fragment = intersection.fragment;
Color Lsinglescatter = LbssrdfSingle(scene, fragment, bssrdf,
wo, sample, bssrdfSampleIndex);
// multiple scattering part with diffusion approximation
Color Lmultiscatter = LbssrdfDiffusion(scene, fragment, bssrdf,
wo, sample, bssrdfSampleIndex);
return Lsinglescatter + Lmultiscatter;
}
view raw sss_entry.cpp hosted with ❤ by GitHub
Done!

One particular thought pop in my mind while reading Jensen01 was: "For the sake of god!!! To just get something radially symmetrically falloff you really need to be that hardcore? I mean....there should be something so much easier symmetrically falloff model you can use right? like Gaussian distribution maybe?" The fun fact is yes! There are already some research about using multiple weight sum of Gaussian filters to approximate BSSRDF and it can be really close to the dipole model. This GPU gems 3 article mentioned the idea and give you the weight sum parameters directly for skin BSSRDF. If you are interested in the full story under the hood, d'Eon11 "A Quantized-Diffusion Model for Rendering Translucent Materials" is a cool ride on it.

Image taken from GPU Gems3 , we can see the 4 Gaussian weight sum is really close to the Dipole model already


BSSRDF Importance Sampling 1 - Kickoff

Recently I start to put in subsurface scattering into my personal side project, and there are some quite fun thoughts and learning experience during the research/implementation time. I decide to write it down as memo and documentation. The following few posts are most likely surround on this topic :)

Why subsurface scattering? Simply feel it looks really cool :P the warm and rich color tone rendered from light bouncing beneath the surface is really different from the regular BRDF material. Theoretically, all material in world is actually BSSRDF instead of BRDF, but BRDF do a good job on approximating those surface light didn't penetrate "deep enough". It would be cool, if I can start to render out some non metal/plastic material, something organic! that's pretty much the original motivation.

After some homework research (mainly the legendary Jensen 01, 02 and Donner's 06 thesis), question pop up: which way should I use? Both Pixar and DreamWorks seems using Jensen 02's point cloud approach from interview and paper release (and my personal working experience= =||), but thinking about managing point cloud and a pre-pass before rendering kinda give me a uncomfortable feeling. Part of my daily job is troubleshooting point cloud artifact and managing its gigantic space usage, and thinking about dealing with it at home after work just feel... not really good....Phar 00's Monte Carlo approach sounds like the most "righteous" method, but it also sounds brutally hardcore :| Jensen 01's Monte Carlo disc sampling sounds like the most straightforward method compare to the previous two...

How about Arnold? The most bad ass MCRT player in the industry at this moment, which method are they using? I googled "Arnold subsurface scattering", then this super awesome slide immediately pop into my eyes!@@ After reading the slides, I felt pretty excited: Cool! They are also using the Jensen 01 approach. Furthermore, the multiple importance sampling method they propose looks brilliant and doesn't seem to be really hard to implement. Alright! I am gonna do thing this way!

nothing relate to SSS in this video actually, but Arnold is kicking some serious ass with EPIC meal time and the awesome sandwich!


Now it's TODO list:

1. I need to have a BSSRDF class pack in relative texture info(absorb, scatter coefficient, refraction index and phase coefficient for single scattering), it should also encapsulate the BSSRDF dipole approximation evaluation.

2. I need one separate BSSRDF integration since the current BRDF integration is apparently doesn't fit with BSSRDF (For direct lighting, BSSRDF needs four dimensional integration on surface area and half sphere solid angle for irradiance, while BRDF only evaluate the later part since all things happen in one intersection point)

3. I need to do importance sampling in 2D with exponential falloff, which is what Jensen01 mentioned and sounds right (it does look like the dominant factor in that dipole equation) and 1D exponential falloff (for single scattering part, this should be pretty straightforward)

4. Figure out the Rmax probe radius that Solid Angle mentioned in above SIGGRAPH13 talk, randomly pick up the probe axis with N:U:V = 2:1:1 ratio they suggested.

5. Implement the multiple scattering part with dipole diffusion approximation (ingredients are listed in the first 4 steps), then combine the sample with multiple importance sampling

6. Implement single scattering part based on Jensen01, Hery03 offered some pseudo code for this, should be straightforward.

And here's the paper references:

BSSRDF Importance Sampling : really brilliant MIS approach for variance noise on complex geometry

A Practical Model For Subsurface Light Transport : the basic implementation I did is a step by step go through on this paper

Implement a Skin BSSRDF (or several) : very throughout tutorial with pseudo code, clarify lots of my unclear thoughts on Jensen01

Towards Realistic Image Synthesis of Scattering Materials : this one got a full derivation of dipole diffusion approximation, and ya....it has some really intense math...

A Rapid Hierarchical Rendering For Translucent Material : the classic point cloud approach that Pixar and DreamWorks use (I believe)

Physically Based Rendering : the ultimate offline rendering strategy guide, though the method in the codebase is based on Jensen 02 approach

Monte Carlo Evaluation on Non-Linear Scattering Equations for Subsurface Reflection : just a really brief and quick pass, but this should be the most physically accurate method (and the slowest...)