void compute_ibl_environment_sampling(
    SamplingContext&        sampling_context,
    const ShadingContext&   shading_context,
    const EnvironmentEDF&   environment_edf,
    const ShadingPoint&     shading_point,
    const Vector3d&         outgoing,
    const BSDF&             bsdf,
    const void*             bsdf_data,
    const int               env_sampling_modes,
    const size_t            bsdf_sample_count,
    const size_t            env_sample_count,
    Spectrum&               radiance)
{
    assert(is_normalized(outgoing));

    const Vector3d& geometric_normal = shading_point.get_geometric_normal();
    const Basis3d& shading_basis = shading_point.get_shading_basis();

    radiance.set(0.0f);

    // todo: if we had a way to know that a BSDF is purely specular, we could
    // immediately return black here since there will be no contribution from
    // such a BSDF.

    sampling_context.split_in_place(2, env_sample_count);

    for (size_t i = 0; i < env_sample_count; ++i)
    {
        // Generate a uniform sample in [0,1)^2.
        const Vector2d s = sampling_context.next_vector2<2>();

        // Sample the environment.
        InputEvaluator input_evaluator(shading_context.get_texture_cache());
        Vector3d incoming;
        Spectrum env_value;
        double env_prob;
        environment_edf.sample(
            input_evaluator,
            s,
            incoming,
            env_value,
            env_prob);

        // Cull samples behind the shading surface.
        assert(is_normalized(incoming));
        const double cos_in = dot(incoming, shading_basis.get_normal());
        if (cos_in < 0.0)
            continue;

        // Discard occluded samples.
        const double transmission =
            shading_context.get_tracer().trace(
                shading_point,
                incoming,
                ShadingRay::ShadowRay);
        if (transmission == 0.0)
            continue;

        // Evaluate the BSDF.
        Spectrum bsdf_value;
        const double bsdf_prob =
            bsdf.evaluate(
                bsdf_data,
                false,                          // not adjoint
                true,                           // multiply by |cos(incoming, normal)|
                geometric_normal,
                shading_basis,
                outgoing,
                incoming,
                env_sampling_modes,
                bsdf_value);
        if (bsdf_prob == 0.0)
            continue;

        // Compute MIS weight.
        const double mis_weight =
            mis_power2(
                env_sample_count * env_prob,
                bsdf_sample_count * bsdf_prob);

        // Add the contribution of this sample to the illumination.
        env_value *= static_cast<float>(transmission / env_prob * mis_weight);
        env_value *= bsdf_value;
        radiance += env_value;
    }

    if (env_sample_count > 1)
        radiance /= static_cast<float>(env_sample_count);
}
void compute_ibl_environment_sampling(
    SamplingContext&        sampling_context,
    const ShadingContext&   shading_context,
    const EnvironmentEDF&   environment_edf,
    const BSSRDF&           bssrdf,
    const void*             bssrdf_data,
    const ShadingPoint&     incoming_point,
    const ShadingPoint&     outgoing_point,
    const Dual3d&           outgoing,
    const size_t            bssrdf_sample_count,
    const size_t            env_sample_count,
    Spectrum&               radiance)
{
    assert(is_normalized(outgoing.get_value()));

    const Basis3d& shading_basis = incoming_point.get_shading_basis();

    radiance.set(0.0f);

    sampling_context.split_in_place(2, env_sample_count);

    for (size_t i = 0; i < env_sample_count; ++i)
    {
        // Generate a uniform sample in [0,1)^2.
        const Vector2d s = sampling_context.next_vector2<2>();

        // Sample the environment.
        InputEvaluator input_evaluator(shading_context.get_texture_cache());
        Vector3d incoming;
        Spectrum env_value;
        double env_prob;
        environment_edf.sample(
            shading_context,
            input_evaluator,
            s,
            incoming,
            env_value,
            env_prob);

        // Cull samples behind the shading surface.
        assert(is_normalized(incoming));
        const double cos_in = dot(incoming, shading_basis.get_normal());
        if (cos_in <= 0.0)
            continue;

        // Discard occluded samples.
        const double transmission =
            shading_context.get_tracer().trace(
                incoming_point,
                incoming,
                VisibilityFlags::ShadowRay);
        if (transmission == 0.0)
            continue;

        // Evaluate the BSSRDF.
        Spectrum bssrdf_value;
        bssrdf.evaluate(
            bssrdf_data,
            outgoing_point,
            outgoing.get_value(),
            incoming_point,
            incoming,
            bssrdf_value);

        // Compute MIS weight.
        const double bssrdf_prob = cos_in * RcpPi;
        const double mis_weight =
            mis_power2(
                env_sample_count * env_prob,
                bssrdf_sample_count * bssrdf_prob);

        // Add the contribution of this sample to the illumination.
        env_value *= static_cast<float>(transmission * cos_in / env_prob * mis_weight);
        env_value *= bssrdf_value;
        radiance += env_value;
    }

    if (env_sample_count > 1)
        radiance /= static_cast<float>(env_sample_count);
}