コード例 #1
0
double Path::G(int i, int j, ManifoldPerturbation* offsetMutator) const
{
	// No edges => error.

	if (i >= j)
	{
		SLog(EError, "Path::G() called with i >= j");
		return 1.0;
	}

	// One edge => traditional G( x(i) <-> x(j) )

	if (j == i + 1)
	{
		double cosI = absDot(edge(i)->d, vertex(i)->getShadingNormal());
		double cosJ = absDot(edge(i)->d, vertex(j)->getShadingNormal());
		double len = edge(i)->length;
		return cosI * cosJ / (len * len);
	}

	// Two or more edges => generalized G

	double res = offsetMutator->getSpecularManifold()->G(*this, i, j);

	// Check errors.

	if (res <= 0.0 || !std::isfinite(res)){
		SLog(EWarn, "Invalid Path::G(%d,%d)=%g (k=%d)", i, j, res, length());
	}
	return res;
}
コード例 #2
0
ファイル: edge.cpp プロジェクト: akaterin/ray-tracer
Spectrum PathEdge::evalCached(const PathVertex *pred, const PathVertex *succ,
		unsigned int what) const {
	/* Extract the requested information based on what is currently cached in the
	   vertex. The actual computation that has to happen here is pretty awful, but
	   it works. It might be worth to change the caching scheme to make this function
	   simpler in a future revision */
	Spectrum result(1.0f);

	if (length == 0) {
		if (what & EValueImp)
			result *= pred->weight[EImportance] * pred->pdf[EImportance];
		if (what & EValueRad)
			result *= succ->weight[ERadiance] * succ->pdf[ERadiance];
	} else {
		if (what & EValueImp) {
			Float tmp = pred->pdf[EImportance];
			if (pred->isConnectable()) {
				tmp *= length * length;
				if (succ->isOnSurface())
					tmp /= dot(succ->getGeometricNormal(), d);
				if (pred->isOnSurface() && !(what & ECosineImp))
					tmp /= dot(pred->getShadingNormal(), d);
			}
			result *= pred->weight[EImportance] * std::abs(tmp);
		} else if ((what & ECosineImp) && pred->isOnSurface() && pred->isConnectable()) {
			result *= absDot(pred->getShadingNormal(), d);
		}

		if (what & EValueRad) {
			Float tmp = succ->pdf[ERadiance];
			if (succ->isConnectable()) {
				tmp *= length * length;
				if (pred->isOnSurface())
					tmp /= dot(pred->getGeometricNormal(), d);
				if (succ->isOnSurface() && !(what & ECosineRad))
					tmp /= dot(succ->getShadingNormal(), d);
			}
			result *= succ->weight[ERadiance] * std::abs(tmp);
		} else if ((what & ECosineRad) && succ->isOnSurface() && succ->isConnectable()) {
			result *= absDot(succ->getShadingNormal(), d);
		}

		if (what & EInverseSquareFalloff)
			result /= length * length;

		if (what & ETransmittance)
			result *= weight[EImportance] * pdf[EImportance];
	}

	return result;
}
コード例 #3
0
	Float pdf(const BSDFSamplingRecord &bRec, EMeasure measure) const {
		if (measure != ESolidAngle ||
			Frame::cosTheta(bRec.wi) < 0 ||
			Frame::cosTheta(bRec.wo) < 0 ||
			((bRec.component != -1 && bRec.component != 0) ||
			!(bRec.typeMask & EGlossyReflection)))
			return 0.0f;

		/* Calculate the reflection half-vector */
		Vector H = normalize(bRec.wo+bRec.wi);

		/* Construct the microfacet distribution matching the
		   roughness values at the current surface position. */
		MicrofacetDistribution distr(
			m_type,
			m_alphaU->eval(bRec.its).average(),
			m_alphaV->eval(bRec.its).average(),
			m_sampleVisible
		);

		if (m_sampleVisible)
			return distr.eval(H) * distr.smithG1(bRec.wi, H)
				/ (4.0f * Frame::cosTheta(bRec.wi));
		else
			return distr.pdf(bRec.wi, H) / (4 * absDot(bRec.wo, H));
	}
コード例 #4
0
ファイル: roughcoating.cpp プロジェクト: blckshrk/IFT6042
	Spectrum eval(const BSDFSamplingRecord &bRec, EMeasure measure) const {
		bool hasNested = (bRec.typeMask & m_nested->getType() & BSDF::EAll)
			&& (bRec.component == -1 || bRec.component < (int) m_components.size()-1);
		bool hasSpecular = (bRec.typeMask & EGlossyReflection)
			&& (bRec.component == -1 || bRec.component == (int) m_components.size()-1)
			&& measure == ESolidAngle;

		/* Evaluate the roughness texture */
		Float alpha = m_alpha->eval(bRec.its).average();
		Float alphaT = m_distribution.transformRoughness(alpha);

		Spectrum result(0.0f);
		if (hasSpecular && Frame::cosTheta(bRec.wo) * Frame::cosTheta(bRec.wi) > 0) {
			/* Calculate the reflection half-vector */
			const Vector H = normalize(bRec.wo+bRec.wi)
				* signum(Frame::cosTheta(bRec.wo));

			/* Evaluate the microsurface normal distribution */
			const Float D = m_distribution.eval(H, alphaT);

			/* Fresnel term */
			const Float F = fresnelDielectricExt(absDot(bRec.wi, H), m_eta);

			/* Smith's shadow-masking function */
			const Float G = m_distribution.G(bRec.wi, bRec.wo, H, alphaT);

			/* Calculate the specular reflection component */
			Float value = F * D * G /
				(4.0f * std::abs(Frame::cosTheta(bRec.wi)));

			result += m_specularReflectance->eval(bRec.its) * value;
		}

		if (hasNested) {
			BSDFSamplingRecord bRecInt(bRec);
			bRecInt.wi = refractTo(EInterior, bRec.wi);
			bRecInt.wo = refractTo(EInterior, bRec.wo);

			Spectrum nestedResult = m_nested->eval(bRecInt, measure) *
				m_roughTransmittance->eval(std::abs(Frame::cosTheta(bRec.wi)), alpha) *
				m_roughTransmittance->eval(std::abs(Frame::cosTheta(bRec.wo)), alpha);

			Spectrum sigmaA = m_sigmaA->eval(bRec.its) * m_thickness;
			if (!sigmaA.isZero())
				nestedResult *= (-sigmaA *
					(1/std::abs(Frame::cosTheta(bRecInt.wi)) +
					 1/std::abs(Frame::cosTheta(bRecInt.wo)))).exp();

			if (measure == ESolidAngle) {
				/* Solid angle compression & irradiance conversion factors */
				nestedResult *= m_invEta * m_invEta *
					  Frame::cosTheta(bRec.wi) * Frame::cosTheta(bRec.wo)
				   / (Frame::cosTheta(bRecInt.wi) * Frame::cosTheta(bRecInt.wo));
			}

			result += nestedResult;
		}

		return result;
	}
コード例 #5
0
ファイル: roughcoating.cpp プロジェクト: blckshrk/IFT6042
	Float pdf(const BSDFSamplingRecord &bRec, EMeasure measure) const {
		bool hasNested = (bRec.typeMask & m_nested->getType() & BSDF::EAll)
			&& (bRec.component == -1 || bRec.component < (int) m_components.size()-1);
		bool hasSpecular = (bRec.typeMask & EGlossyReflection)
			&& (bRec.component == -1 || bRec.component == (int) m_components.size()-1)
			&& measure == ESolidAngle;

		/* Calculate the reflection half-vector */
		const Vector H = normalize(bRec.wo+bRec.wi)
				* signum(Frame::cosTheta(bRec.wo));

		/* Evaluate the roughness texture */
		Float alpha = m_alpha->eval(bRec.its).average();
		Float alphaT = m_distribution.transformRoughness(alpha);

		Float probNested, probSpecular;
		if (hasSpecular && hasNested) {
			/* Find the probability of sampling the specular component */
			probSpecular = 1-m_roughTransmittance->eval(
				std::abs(Frame::cosTheta(bRec.wi)), alpha);

			/* Reallocate samples */
			probSpecular = (probSpecular*m_specularSamplingWeight) /
				(probSpecular*m_specularSamplingWeight +
				(1-probSpecular) * (1-m_specularSamplingWeight));

			probNested = 1 - probSpecular;
		} else {
			probNested = probSpecular = 1.0f;
		}

		Float result = 0.0f;
		if (hasSpecular && Frame::cosTheta(bRec.wo) * Frame::cosTheta(bRec.wi) > 0) {
			/* Jacobian of the half-direction mapping */
			const Float dwh_dwo = 1.0f / (4.0f * absDot(bRec.wo, H));

			/* Evaluate the microsurface normal distribution */
			const Float prob = m_distribution.pdf(H, alphaT);

			result = prob * dwh_dwo * probSpecular;
		}

		if (hasNested) {
			BSDFSamplingRecord bRecInt(bRec);
			bRecInt.wi = refractTo(EInterior, bRec.wi);
			bRecInt.wo = refractTo(EInterior, bRec.wo);

			Float prob = m_nested->pdf(bRecInt, measure);

			if (measure == ESolidAngle) {
				prob *= m_invEta * m_invEta * Frame::cosTheta(bRec.wo)
			          / Frame::cosTheta(bRecInt.wo);
			}

			result += prob * probNested;
		}

		return result;
	}
コード例 #6
0
Float Shape::pdfDirect(const DirectSamplingRecord &dRec) const {
	Float pdfPos = pdfPosition(dRec);

	if (dRec.measure == ESolidAngle)
		return pdfPos * (dRec.dist * dRec.dist) / absDot(dRec.d, dRec.n);
	else if (dRec.measure == EArea)
		return pdfPos;
	else
		return 0.0f;
}
コード例 #7
0
ファイル: merl_sgd.cpp プロジェクト: davidlee80/dj_brdf
	Float pdf(const BSDFSamplingRecord &bRec, EMeasure measure) const {
		if (measure != ESolidAngle ||
			Frame::cosTheta(bRec.wi) <= 0 ||
			Frame::cosTheta(bRec.wo) <= 0 ||
			((bRec.component != -1 && bRec.component != 0) ||
			!(bRec.typeMask & EGlossyReflection)))
			return 0.0f;

		/* Calculate the reflection half-vector */
		Vector H = normalize(bRec.wo+bRec.wi);

		return PDF(H) / (4 * absDot(bRec.wo, H));
	}
コード例 #8
0
void Shape::sampleDirect(DirectSamplingRecord &dRec,
			const Point2 &sample) const {
	/* Piggyback on sampleArea() */
	samplePosition(dRec, sample);

	dRec.d = dRec.p - dRec.ref;

	Float distSquared = dRec.d.lengthSquared();
	dRec.dist = std::sqrt(distSquared);
	dRec.d /= dRec.dist;
	Float dp = absDot(dRec.d, dRec.n);
	dRec.pdf *= dp != 0 ? (distSquared / dp) : 0.0f;
	dRec.measure = ESolidAngle;
}
コード例 #9
0
ファイル: constant.cpp プロジェクト: blckshrk/IFT6042
	Float pdfDirect(const DirectSamplingRecord &dRec) const {
		Float pdfSA;

		if (!dRec.refN.isZero())
			pdfSA = INV_PI * std::max((Float) 0.0f, dot(dRec.d, dRec.refN));
		else
			pdfSA = Warp::squareToUniformSpherePdf();

		if (dRec.measure == ESolidAngle)
			return pdfSA;
		else if (dRec.measure == EArea)
			return pdfSA * absDot(dRec.d, dRec.n)
				/ (dRec.dist * dRec.dist);
		else
			return 0.0f;
	}
コード例 #10
0
	Spectrum Lo(const Scene *scene, Sampler *sampler,
			const Intersection &its, const Vector &d, int depth) const {
		if (!m_ready || m_ssFactor.isZero())
			return Spectrum(0.0f);
		IsotropicDipoleQuery query(m_zr, m_zv, m_sigmaTr, m_Fdt, its.p);
	
		const Normal &n = its.shFrame.n;
		m_octree->execute(query);

		if (m_eta == 1.0f) {
			return query.getResult() * m_ssFactor * INV_PI;
		} else {
			Float Ft = 1.0f - fresnel(absDot(n, d));
			return query.getResult() * m_ssFactor * INV_PI * (Ft / m_Fdr);
		}
	}
コード例 #11
0
 void PathTracingRenderer::Job::kernel(uint32_t threadID) {
     ArenaAllocator &mem = mems[threadID];
     IndependentLightPathSampler &pathSampler = *pathSamplers[threadID];
     for (int ly = 0; ly < numPixelY; ++ly) {
         for (int lx = 0; lx < numPixelX; ++lx) {
             float time = pathSampler.getTimeSample(timeStart, timeEnd);
             PixelPosition p = pathSampler.getPixelPositionSample(basePixelX + lx, basePixelY + ly);
             
             float selectWLPDF;
             WavelengthSamples wls = WavelengthSamples::createWithEqualOffsets(pathSampler.getWavelengthSample(), pathSampler.getWLSelectionSample(), &selectWLPDF);
             
             LensPosQuery lensQuery(time, wls);
             LensPosQueryResult lensResult;
             SampledSpectrum We0 = camera->sample(lensQuery, pathSampler.getLensPosSample(), &lensResult);
             
             IDFSample WeSample(p.x / imageWidth, p.y / imageHeight);
             IDFQueryResult WeResult;
             IDF* idf = camera->createIDF(lensResult.surfPt, wls, mem);
             SampledSpectrum We1 = idf->sample(WeSample, &WeResult);
             
             Ray ray(lensResult.surfPt.p, lensResult.surfPt.shadingFrame.fromLocal(WeResult.dirLocal), time);
             SampledSpectrum C = contribution(*scene, wls, ray, pathSampler, mem);
             SLRAssert(C.hasNaN() == false && C.hasInf() == false && C.hasMinus() == false,
                       "Unexpected value detected: %s\n"
                       "pix: (%f, %f)", C.toString().c_str(), px, py);
             
             SampledSpectrum weight = (We0 * We1) * (absDot(ray.dir, lensResult.surfPt.gNormal) / (lensResult.areaPDF * WeResult.dirPDF * selectWLPDF));
             SLRAssert(weight.hasNaN() == false && weight.hasInf() == false && weight.hasMinus() == false,
                       "Unexpected value detected: %s\n"
                       "pix: (%f, %f)", weight.toString().c_str(), px, py);
             sensor->add(p.x, p.y, wls, weight * C);
             
             mem.reset();
         }
     }
 }
コード例 #12
0
Float Path::getGeometryTerm(int i) const{
	Float cosI = absDot(edge(i)->d, vertex(i)->getShadingNormal());
	float cosJ = absDot(edge(i)->d, vertex(i + 1)->getShadingNormal());
	Float len = edge(i)->length;
	return cosI * cosJ / (len * len);
}
コード例 #13
0
    SampledSpectrum PathTracingRenderer::Job::contribution(const Scene &scene, const WavelengthSamples &initWLs, const Ray &initRay, IndependentLightPathSampler &pathSampler, ArenaAllocator &mem) const {
        WavelengthSamples wls = initWLs;
        Ray ray = initRay;
        SurfacePoint surfPt;
        SampledSpectrum alpha = SampledSpectrum::One;
        float initY = alpha.importance(wls.selectedLambda);
        SampledSpectrumSum sp(SampledSpectrum::Zero);
        uint32_t pathLength = 0;
        
        Intersection isect;
        if (!scene.intersect(ray, &isect))
            return SampledSpectrum::Zero;
        isect.getSurfacePoint(&surfPt);
        
        Vector3D dirOut_sn = surfPt.shadingFrame.toLocal(-ray.dir);
        if (surfPt.isEmitting()) {
            EDF* edf = surfPt.createEDF(wls, mem);
            SampledSpectrum Le = surfPt.emittance(wls) * edf->evaluate(EDFQuery(), dirOut_sn);
            sp += alpha * Le;
        }
        if (surfPt.atInfinity)
            return sp;

        while (true) {
            ++pathLength;
            if (pathLength >= 100)
                break;
            Normal3D gNorm_sn = surfPt.shadingFrame.toLocal(surfPt.gNormal);
            BSDF* bsdf = surfPt.createBSDF(wls, mem);
            BSDFQuery fsQuery(dirOut_sn, gNorm_sn, wls.selectedLambda);
            
            // Next Event Estimation (explicit light sampling)
            if (bsdf->hasNonDelta()) {
                float lightProb;
                Light light;
                scene.selectLight(pathSampler.getLightSelectionSample(), &light, &lightProb);
                SLRAssert(!std::isnan(lightProb) && !std::isinf(lightProb), "lightProb: unexpected value detected: %f", lightProb);
                
                LightPosQuery lpQuery(ray.time, wls);
                LightPosQueryResult lpResult;
                SampledSpectrum M = light.sample(lpQuery, pathSampler.getLightPosSample(), &lpResult);
                SLRAssert(!std::isnan(lpResult.areaPDF)/* && !std::isinf(xpResult.areaPDF)*/, "areaPDF: unexpected value detected: %f", lpResult.areaPDF);
                
                if (scene.testVisibility(surfPt, lpResult.surfPt, ray.time)) {
                    float dist2;
                    Vector3D shadowDir = lpResult.surfPt.getDirectionFrom(surfPt.p, &dist2);
                    Vector3D shadowDir_l = lpResult.surfPt.shadingFrame.toLocal(-shadowDir);
                    Vector3D shadowDir_sn = surfPt.shadingFrame.toLocal(shadowDir);
                    
                    EDF* edf = lpResult.surfPt.createEDF(wls, mem);
                    SampledSpectrum Le = M * edf->evaluate(EDFQuery(), shadowDir_l);
                    float lightPDF = lightProb * lpResult.areaPDF;
                    SLRAssert(!Le.hasNaN() && !Le.hasInf(), "Le: unexpected value detected: %s", Le.toString().c_str());
                    
                    SampledSpectrum fs = bsdf->evaluate(fsQuery, shadowDir_sn);
                    float cosLight = absDot(-shadowDir, lpResult.surfPt.gNormal);
                    float bsdfPDF = bsdf->evaluatePDF(fsQuery, shadowDir_sn) * cosLight / dist2;
                    
                    float MISWeight = 1.0f;
                    if (!lpResult.posType.isDelta() && !std::isinf(lpResult.areaPDF))
                        MISWeight = (lightPDF * lightPDF) / (lightPDF * lightPDF + bsdfPDF * bsdfPDF);
                    SLRAssert(MISWeight <= 1.0f, "Invalid MIS weight: %g", MISWeight);
                    
                    float G = absDot(shadowDir_sn, gNorm_sn) * cosLight / dist2;
                    sp += alpha * Le * fs * (G * MISWeight / lightPDF);
                    SLRAssert(!std::isnan(G) && !std::isinf(G), "G: unexpected value detected: %f", G);
                }
            }
            
            // get a next direction by sampling BSDF.
            BSDFQueryResult fsResult;
            SampledSpectrum fs = bsdf->sample(fsQuery, pathSampler.getBSDFSample(), &fsResult);
            if (fs == SampledSpectrum::Zero || fsResult.dirPDF == 0.0f)
                break;
            if (fsResult.dirType.isDispersive()) {
                fsResult.dirPDF /= WavelengthSamples::NumComponents;
                wls.flags |= WavelengthSamples::LambdaIsSelected;
            }
            alpha *= fs * absDot(fsResult.dir_sn, gNorm_sn) / fsResult.dirPDF;
            SLRAssert(!alpha.hasInf() && !alpha.hasNaN(),
                      "alpha: %s\nlength: %u, cos: %g, dirPDF: %g",
                      alpha.toString().c_str(), pathLength, absDot(fsResult.dir_sn, gNorm_sn), fsResult.dirPDF);
            
            Vector3D dirIn = surfPt.shadingFrame.fromLocal(fsResult.dir_sn);
            ray = Ray(surfPt.p, dirIn, ray.time, Ray::Epsilon);
            
            // find a next intersection point.
            isect = Intersection();
            if (!scene.intersect(ray, &isect))
                break;
            isect.getSurfacePoint(&surfPt);
            
            dirOut_sn = surfPt.shadingFrame.toLocal(-ray.dir);
            
            // implicit light sampling
            if (surfPt.isEmitting()) {
                float bsdfPDF = fsResult.dirPDF;
                
                EDF* edf = surfPt.createEDF(wls, mem);
                SampledSpectrum Le = surfPt.emittance(wls) * edf->evaluate(EDFQuery(), dirOut_sn);
                float lightProb = scene.evaluateProb(Light(isect.obj));
                float dist2 = surfPt.getSquaredDistance(ray.org);
                float lightPDF = lightProb * surfPt.evaluateAreaPDF() * dist2 / absDot(ray.dir, surfPt.gNormal);
                SLRAssert(!Le.hasNaN() && !Le.hasInf(), "Le: unexpected value detected: %s", Le.toString().c_str());
                SLRAssert(!std::isnan(lightPDF)/* && !std::isinf(lightPDF)*/, "lightPDF: unexpected value detected: %f", lightPDF);
                
                float MISWeight = 1.0f;
                if (!fsResult.dirType.isDelta())
                    MISWeight = (bsdfPDF * bsdfPDF) / (lightPDF * lightPDF + bsdfPDF * bsdfPDF);
                SLRAssert(MISWeight <= 1.0f, "Invalid MIS weight: %g", MISWeight);
                
                sp += alpha * Le * MISWeight;
            }
            if (surfPt.atInfinity)
                break;
            
            // Russian roulette
            float continueProb = std::min(alpha.importance(wls.selectedLambda) / initY, 1.0f);
            if (pathSampler.getPathTerminationSample() < continueProb)
                alpha /= continueProb;
            else
                break;
        }
        
        return sp;
    }
コード例 #14
0
ファイル: path.cpp プロジェクト: JonCG90/GraphicsEngine
Spectrum Path::computeLi( const vec2f &pFilm, const SceneData &i_scene, const Camera &i_camera, const Sampler &i_sampler, int i_depth )
{
    Camera::CameraSample cameraSample;
    cameraSample.pixelSample = pFilm + i_sampler.sample2D();
    cameraSample.lensSample = i_sampler.sample2D();
    
    Ray ray;
    float weight = i_camera.generateRay( cameraSample, &ray );
    
    // Radiance
    Spectrum L = Spectrum( 0.0f );
    
    // Importance
    Spectrum beta = Spectrum( 1.0 );
    
    for ( int bounces = 0;; bounces++ ) {
        
        RayIntersection intersection;
        bool foundIntersection = i_scene.intersect( ray, intersection );
        
        float maxDepth = 5;
        if ( !foundIntersection || bounces >= maxDepth ) {
            break;
        }
        
        Spectrum Le = intersection.Le();
        if ( Le.isBlack() )
        {
            // Direct illumination sampling

            Spectrum Ld = beta * uniformSampleOneLight( intersection, i_sampler, i_scene );
            L += Ld;
            
            // BSDF Sampling
            const shading::BSDF &bsdf = intersection.getBSDF();
            const vec3f &surfacePosition = intersection.getSurfacePoint().pos;
            
            vec3f woLocal = -intersection.worldToSurface( -intersection.dir );
            vec3f wiLocal;
            float pdf = 1.0;
            
            const vec2f &bsdfSample = i_sampler.sample2D();
            Spectrum f = bsdf.sampleF( woLocal, &wiLocal, bsdfSample, &pdf );
            
            // No point of continuing if no color
            if ( f.isBlack() || pdf == 0.0 ) {
                break;
            }
            
            // Transform to world space
            vec3f wiWorld = intersection.surfaceToWorld( wiLocal );
            
            // Generate new ray
            ray = Ray( surfacePosition, wiWorld );
            
            beta *= ( f * absDot( wiWorld, intersection.getSurfacePoint().normal ) ) / pdf;
            
            // Russian Roulette
            if ( beta.y() < 1.0f && bounces > 3 ) {
                
                float q = std::max( .05f, 1 - beta.maxComponentValue() );
                
                if ( i_sampler.sample1D() > q ) {
                    beta /= ( 1.0 - q );
                }
                else {
                    break;
                }
            }
        }
        else {
            L += beta * intersection.Le() / 50.0;
            break;
        }
    }
    
    return weight * L;
}
コード例 #15
0
ファイル: path.cpp プロジェクト: JonCG90/GraphicsEngine
Spectrum Path::estimateDirectLighting( const RayIntersection i_intersection, RenderablePtr i_light, const Sampler &i_sampler, const SceneData &i_scene )
{
    // Direct contribution
    Spectrum Ld = Spectrum::black();
    
    float lightPdf = 0.0f;
    float surfacePdf = 0.0f;
    
    vec3f wiWorld;
    
    const vec3f &woWorld = -i_intersection.dir;
    const vec3f &woLocal = i_intersection.worldToSurface( woWorld );
    const vec3f &normal = i_intersection.getSurfacePoint().normal;
    const shading::BSDF &bsdf = i_intersection.getBSDF();
    
    /* ==================== */
    /*     Sample Light     */
    /* ==================== */
    {
        VisibilityTester visibilityTester;
        const vec3f &uLight = i_sampler.sample3D();
        
        Spectrum Li = i_light->sampleIncidenceRadiance( i_intersection, uLight, &wiWorld, &lightPdf, &visibilityTester );
        
        // Convert to local (surface) coords
        const vec3f &wiLocal = i_intersection.worldToSurface( wiWorld );
        
        // Sample light
        if ( !Li.isBlack() && lightPdf > 0.0f ) {
            
            const Spectrum &f = bsdf.computeF( woLocal, wiLocal ) * absDot( wiWorld, normal );
            surfacePdf = bsdf.pdf( woLocal, wiLocal );
            
            if ( !f.isBlack() ) {
                
                // Ray is in shadow
                if ( visibilityTester.isOccluded( i_scene ) ) {
                    Li = Spectrum::black();
                }
                
                if ( !Li.isBlack() ) {
                    
                    // Weight with MIS
                    float misWeight = powerHeuristic( lightPdf, surfacePdf );
                    Ld += ( Li * f * misWeight ) / lightPdf;
                }
            }
        }
    }
    
    /* ==================== */
    /*     Sample BSDF      */
    /* ==================== */
    {
        const vec2f &uSurface = i_sampler.sample2D();
        vec3f wiLocal;
        
        const Spectrum &f = bsdf.sampleF( woLocal, &wiLocal, uSurface, &surfacePdf ) * absDot( wiWorld, normal );
        
        if ( !f.isBlack() && surfacePdf > 0.0f ) {
            
            // TODO computer light PDF
            lightPdf = 0.0;
            
            // No contribution, return
            if ( lightPdf == 0.0 ) {
                return Ld;
            }

            // Convert to local (surface) coords
            vec3f wiWorld = i_intersection.surfaceToWorld( wiLocal );
            
            const Ray bsdfRay = Ray( i_intersection.getSurfacePoint().pos, wiWorld );
            RayIntersection lightIntersection;
            
            bool foundIntersection = i_scene.intersect( bsdfRay, lightIntersection );
            Spectrum Li = Spectrum::black();
            
            if ( foundIntersection ) {
                
                if ( lightIntersection.m_shapeId == i_light->getIdentifier() ) {
                    Li = i_light->computeRadiance( lightIntersection.getSurfacePoint(), wiWorld );
                }
            }
            
            if ( !Li.isBlack() ) {
                
                // Weight with MIS
                float misWeight = powerHeuristic( lightPdf, surfacePdf );
                Ld += ( Li * f * misWeight ) / surfacePdf;
            }
        }
    }
    
    return Ld;
}