예제 #1
0
    def get_diff_param(scene):
        # Create a differentiable hyperparameter
        diff_param = Float(0.0)
        ek.set_requires_gradient(diff_param)

        # Update vertices so that they depend on diff_param
        params = traverse(scene)
        t = Transform4f.translate(Vector3f(1.0) * diff_param)
        vertex_positions = params['light_shape.vertex_positions']
        vertex_positions_t = t.transform_point(vertex_positions)
        params['light_shape.vertex_positions'] = vertex_positions_t

        # Update the scene
        params.update()

        return diff_param
예제 #2
0
 def backward(ctx, grad_output):
     try:
         ek.set_gradient(ctx.output, ek.detach(Float(grad_output)))
         Float.backward()
         result = tuple(
             ek.gradient(i).torch() if i is not None else None
             for i in ctx.inputs)
         del ctx.output
         del ctx.inputs
         if ctx.malloc_trim:
             ek.cuda_malloc_trim()
         return result
     except Exception as e:
         print("render_torch(): critical exception during "
               "backward pass: %s" % str(e))
         raise e
예제 #3
0
def test06_discr_bruteforce(variant_packet_rgb):
    # Brute force validation of discrete distribution sampling
    from mitsuba.core import DiscreteDistribution, Float, PCG32, UInt64

    rng = PCG32(initseq=UInt64.arange(50))

    for size in range(2, 20):
        for i in range(2, 50):
            density = Float(rng.next_uint32_bounded(i)[0:size])
            if ek.hsum(density) == 0:
                continue
            ddistr = DiscreteDistribution(density)

            x = ek.linspace(Float, 0, 1, 20)
            y = ddistr.sample(x)
            z = ek.gather(ddistr.cdf(), y - 1, y > 0)
            x *= ddistr.sum()

            # Did we sample the right interval?
            assert ek.all((x > z) | (ek.eq(x, 0) & (x >= z)))
예제 #4
0
# This integrator simply computes the depth values per pixel
sensor = scene.sensors()[0]
film = sensor.film()
sampler = sensor.sampler()
film_size = film.crop_size()
spp = 32

# Enumerate discrete sample & pixel indices, and uniformly sample
# positions within each pixel.
pos = ek.arange(UInt64, ek.hprod(film_size) * spp)
if not sampler.ready():
    sampler.seed(pos)

pos //= spp
scale = Vector2f(1.0 / film_size[0], 1.0 / film_size[1])
pos = Vector2f(Float(pos % int(film_size[0])), Float(pos // int(film_size[0])))

pos += sampler.next_2d()

# Sample rays starting from the camera sensor
rays, weights = sensor.sample_ray_differential(time=0,
                                               sample1=sampler.next_1d(),
                                               sample2=pos * scale,
                                               sample3=0)

# Intersect rays with the scene geometry
surface_interaction = scene.ray_intersect(rays)

# Given intersection, compute the final pixel values as the depth t
# of the sampled surface interaction
result = surface_interaction.t
예제 #5
0
def _render_helper(scene, spp=None, sensor_index=0):
    """
    Internally used function: render the specified Mitsuba scene and return a
    floating point array containing RGB values and AOVs, if applicable
    """
    from mitsuba.core import (Float, UInt32, UInt64, Vector2f,
                              is_monochromatic, is_rgb, is_polarized)
    from mitsuba.render import ImageBlock

    sensor = scene.sensors()[sensor_index]
    film = sensor.film()
    sampler = sensor.sampler()
    film_size = film.crop_size()
    if spp is None:
        spp = sampler.sample_count()

    total_sample_count = ek.hprod(film_size) * spp

    if sampler.wavefront_size() != total_sample_count:
        sampler.seed(ek.arange(UInt64, total_sample_count))

    pos = ek.arange(UInt32, total_sample_count)
    pos //= spp
    scale = Vector2f(1.0 / film_size[0], 1.0 / film_size[1])
    pos = Vector2f(Float(pos % int(film_size[0])),
                   Float(pos // int(film_size[0])))

    pos += sampler.next_2d()

    rays, weights = sensor.sample_ray_differential(
        time=0,
        sample1=sampler.next_1d(),
        sample2=pos * scale,
        sample3=0
    )

    spec, mask, aovs = scene.integrator().sample(scene, sampler, rays)
    spec *= weights
    del mask

    if is_polarized:
        from mitsuba.core import depolarize
        spec = depolarize(spec)

    if is_monochromatic:
        rgb = [spec[0]]
    elif is_rgb:
        rgb = spec
    else:
        from mitsuba.core import spectrum_to_xyz, xyz_to_srgb
        xyz = spectrum_to_xyz(spec, rays.wavelengths)
        rgb = xyz_to_srgb(xyz)
        del xyz

    aovs.insert(0, Float(1.0))
    for i in range(len(rgb)):
        aovs.insert(i + 1, rgb[i])
    del rgb, spec, weights, rays

    block = ImageBlock(
        size=film.crop_size(),
        channel_count=len(aovs),
        filter=film.reconstruction_filter(),
        warn_negative=False,
        warn_invalid=False,
        border=False
    )

    block.clear()
    block.put(pos, aovs)

    del pos
    del aovs

    data = block.data()

    ch = block.channel_count()
    i = UInt32.arange(ek.hprod(block.size()) * (ch - 1))

    weight_idx = i // (ch - 1) * ch
    values_idx = (i * ch) // (ch - 1) + 1

    weight = ek.gather(data, weight_idx)
    values = ek.gather(data, values_idx)

    return values / (weight + 1e-8)
def mis_weight(pdf_a, pdf_b):
    pdf_a *= pdf_a
    pdf_b *= pdf_b
    return ek.select(pdf_a > 0.0, pdf_a / (pdf_a + pdf_b), Float(0.0))
예제 #7
0
def postprocess_render(results,
                       weights,
                       blocks,
                       pos,
                       aovs=False,
                       invalid_sample=False):
    """postprocessing for sampling result"""

    result = results[0]
    valid_rays = results[1]

    result *= weights
    xyz = Color3f(srgb_to_xyz(result))
    aovs_result = [
        xyz[0], xyz[1], xyz[2],
        ek.select(valid_rays, Float(1.0), Float(0.0)), 1.0
    ]

    if not aovs and not invalid_sample:
        block = blocks
    else:
        block = blocks["result"]

    block.put(pos, aovs_result)

    if aovs:
        scatter = results[2]
        non_scatter = results[3]

        scatter *= weights
        non_scatter *= weights

        xyz_scatter = Color3f(srgb_to_xyz(scatter))
        xyz_nonscatter = Color3f(srgb_to_xyz(non_scatter))

        aovs_scatter = [
            xyz_scatter[0], xyz_scatter[1], xyz_scatter[2],
            ek.select(valid_rays, Float(1.0), Float(0.0)), 1.0
        ]
        aovs_nonscatter = [
            xyz_nonscatter[0], xyz_nonscatter[1], xyz_nonscatter[2],
            ek.select(valid_rays, Float(1.0), Float(0.0)), 1.0
        ]

        block_scatter = blocks["scatter"]
        block_nonscatter = blocks["non_scatter"]

        block_scatter.put(pos, aovs_scatter)
        block_nonscatter.put(pos, aovs_nonscatter)

    if invalid_sample:
        invalid = results[4]

        invalid *= weights

        xyz_invalid = Color3f(srgb_to_xyz(invalid))

        aovs_invalid = [
            xyz_invalid[0], xyz_invalid[1], xyz_invalid[2],
            ek.select(valid_rays, Float(1.0), Float(0.0)), 1.0
        ]

        block_invalid = blocks["invalid"]

        block_invalid.put(pos, aovs_invalid)
예제 #8
0
from mitsuba.core import Float, Thread
from mitsuba.core.xml import load_file
from mitsuba.python.util import traverse
from mitsuba.python.autodiff import render, write_bitmap, Adam
import time

# Load example scene
Thread.thread().file_resolver().append('bunny')
scene = load_file('bunny/bunny.xml')

# Find differentiable scene parameters
params = traverse(scene)

# Make a backup copy
param_res = params['my_envmap.resolution']
param_ref = Float(params['my_envmap.data'])

# Discard all parameters except for one we want to differentiate
params.keep(['my_envmap.data'])

# Render a reference image (no derivatives used yet)
image_ref = render(scene, spp=16)
crop_size = scene.sensors()[0].film().crop_size()
write_bitmap('out_ref.png', image_ref, crop_size)

# Change to a uniform white lighting environment
params['my_envmap.data'] = ek.full(Float, 1.0, len(param_ref))
params.update()

# Construct an Adam optimizer that will adjust the parameters 'params'
opt = Adam(params, lr=.02)
scene = load_file(xmlfilename)
width, height = scene.sensors()[0].film().crop_size()

# Load a reference image (no derivatives used yet)
bitmap_ref = Bitmap(os.path.join('outputs', render_config['ref'])).convert(
    Bitmap.PixelFormat.RGB, Struct.Type.Float32, srgb_gamma=False)
image_ref = np.array(bitmap_ref).flatten()

# Find differentiable scene parameters
params = traverse(scene)
print(params)

opt_param_name = 'textured_lightsource.emitter.radiance.data'
# Make a backup copy
param_res = params['textured_lightsource.emitter.radiance.resolution']
param_ref = Float(params[opt_param_name])

# Discord all parameters except for one we want to differentiate
params.keep([opt_param_name])

# texture_bitmap = Bitmap('img/checker.jpg').convert(Bitmap.PixelFormat.RGB, Struct.Type.Float32, srgb_gamma=False)
# texture_float = Float(texture_bitmap)
# params['textured_lightsource.emitter.radiance.data'] = texture_bitmap
params['textured_lightsource.emitter.radiance.data'] = ek.full(
    Float, 0.0, len(param_ref))

opt = Adam(params, lr=render_config['learning_rate'])

time_a = time.time()

outpath = os.path.join('outputs/invert_infloasion/', outimg_dir)
예제 #10
0
def render_sample(scene, sampler, rays, bdata, heightmap_pybind, bssrdf=None):
    """
    Sample RTE
    TODO: Support multi channel sampling

    Args:
        scene: Target scene object
        sampler: Sampler object for random number
        rays: Given rays for sampling
        bdata: BSSRDF Data object
        heightmap_pybind: Object for getting height map around incident position.
                          Refer src/librender/python/heightmap.cpp

    Returns:
        result: Sampling RTE result
        valid_rays: Mask data whether rays are valid or not
        scatter: Scatter components of Sampling RTE result
        non_scatter: Non scatter components of Sampling RTE result
        invalid_sample: Sampling RTE result with invalid sampled data by VAEBSSRDF
    """

    eta = Float(1.0)
    emission_weight = Float(1.0)
    throughput = Spectrum(1.0)
    result = Spectrum(0.0)
    scatter = Spectrum(0.0)
    non_scatter = Spectrum(0.0)
    invalid_sample = Spectrum(0.0)
    active = True
    is_bssrdf = False

    ##### First interaction #####
    si = scene.ray_intersect(rays, active)
    active = si.is_valid() & active
    valid_rays = si.is_valid()

    emitter = si.emitter(scene, active)

    depth = 0

    # Set channel
    # At and after evaluating BSSRDF, a ray consider only this one channel
    n_channels = 3
    channel = UInt32(
        ek.min(sampler.next_1d(active) * n_channels, n_channels - 1))

    d_out_local = Vector3f().zero()
    d_out_pdf = Float(0)

    sss = Mask(False)

    while (True):
        depth += 1
        if config.aovs and depth == 2:
            sss = is_bssrdf

        ##### Interaction with emitters #####
        emission_val = emission_weight * throughput * Emitter.eval_vec(
            emitter, si, active)

        result += ek.select(active, emission_val, Spectrum(0.0))
        invalid_sample += ek.select(active, emission_val, Spectrum(0.0))
        scatter += ek.select(active & sss, emission_val, Spectrum(0.0))
        non_scatter += ek.select(active & ~sss, emission_val, Spectrum(0.0))

        active = active & si.is_valid()

        # Process russian roulette
        if depth > config.rr_depth:
            q = ek.min(ek.hmax(throughput) * ek.sqr(eta), 0.95)
            active = active & (sampler.next_1d(active) < q)
            throughput *= ek.rcp(q)

        # Stop if the number of bouces exceeds the given limit bounce, or
        # all rays are invalid. latter check is done only when the limit
        # bounce is infinite
        if depth >= config.max_depth:
            break

        ##### Emitter sampling #####
        bsdf = si.bsdf(rays)
        ctx = BSDFContext()

        active_e = active & has_flag(BSDF.flags_vec(bsdf), BSDFFlags.Smooth)
        ds, emitter_val = scene.sample_emitter_direction(
            si, sampler.next_2d(active_e), True, active_e)
        active_e &= ek.neq(ds.pdf, 0.0)

        # Query the BSDF for that emitter-sampled direction
        wo = si.to_local(ds.d)
        bsdf_val = BSDF.eval_vec(bsdf, ctx, si, wo, active_e)
        # Determine density of sampling that same direction using BSDF sampling
        bsdf_pdf = BSDF.pdf_vec(bsdf, ctx, si, wo, active_e)

        mis = ek.select(ds.delta, Float(1), mis_weight(ds.pdf, bsdf_pdf))

        emission_val = mis * throughput * bsdf_val * emitter_val

        result += ek.select(active, emission_val, Spectrum(0.0))
        invalid_sample += ek.select(active, emission_val, Spectrum(0.0))
        scatter += ek.select(active & sss, emission_val, Spectrum(0.0))
        non_scatter += ek.select(active & ~sss, emission_val, Spectrum(0.0))

        ##### BSDF sampling #####
        bs, bsdf_val = BSDF.sample_vec(bsdf, ctx, si, sampler.next_1d(active),
                                       sampler.next_2d(active), active)

        ##### BSSRDF replacing #####
        if (config.enable_bssrdf):
            # Replace bsdf samples by ones of BSSRDF
            bs.wo = ek.select(is_bssrdf, d_out_local, bs.wo)
            bs.pdf = ek.select(is_bssrdf, d_out_pdf, bs.pdf)
            bs.sampled_component = ek.select(is_bssrdf, UInt32(1),
                                             bs.sampled_component)
            bs.sampled_type = ek.select(is_bssrdf,
                                        UInt32(+BSDFFlags.DeltaTransmission),
                                        bs.sampled_type)
        ############################

        throughput *= ek.select(is_bssrdf, Float(1.0), bsdf_val)
        active &= ek.any(ek.neq(throughput, 0))

        eta *= bs.eta

        # Intersect the BSDF ray against the scene geometry
        rays = RayDifferential3f(si.spawn_ray(si.to_world(bs.wo)))
        si_bsdf = scene.ray_intersect(rays, active)

        ##### Checking BSSRDF #####
        if (config.enable_bssrdf):
            # Whether the BSDF is BSS   RDF or not?
            is_bssrdf = (active
                         & has_flag(BSDF.flags_vec(bsdf), BSDFFlags.BSSRDF)
                         & (Frame3f.cos_theta(bs.wo) < Float(0.0))
                         & (Frame3f.cos_theta(si.wi) > Float(0.0)))

            # Decide whether we should use 0-scattering or multiple scattering
            is_zero_scatter = utils_render.check_zero_scatter(
                sampler, si_bsdf, bs, channel, is_bssrdf)
            is_bssrdf = is_bssrdf & ~is_zero_scatter

            throughput *= ek.select(is_bssrdf, ek.sqr(bs.eta), Float(1.0))
        ###########################

        ###### Process for BSSRDF ######
        if (config.enable_bssrdf and not ek.none(is_bssrdf)):
            # Get projected samples from BSSRDF
            projected_si, project_suc, abs_prob = bssrdf.sample_bssrdf(
                scene, bsdf, bs, si, bdata, heightmap_pybind, channel,
                is_bssrdf)

            if config.visualize_invalid_sample and (depth <= 1):
                active = active & (~is_bssrdf | project_suc)
                invalid_sample += ek.select((is_bssrdf & (~project_suc)),
                                            Spectrum([100, 0, 0]),
                                            Spectrum(0.0))

            # Sample outgoing direction from projected position
            d_out_local, d_out_pdf = utils_render.resample_wo(
                sampler, is_bssrdf)
            # Apply absorption probability
            throughput *= ek.select(is_bssrdf,
                                    Spectrum(1) - abs_prob, Spectrum(1))
            # Replace interactions by sampled ones from BSSRDF
            si_bsdf = SurfaceInteraction3f().masked_si(si_bsdf, projected_si,
                                                       is_bssrdf)
        ################################

        # Determine probability of having sampled that same
        # direction using emitter sampling
        emitter = si_bsdf.emitter(scene, active)
        ds = DirectionSample3f(si_bsdf, si)
        ds.object = emitter

        delta = has_flag(bs.sampled_type, BSDFFlags.Delta)
        emitter_pdf = ek.select(delta, Float(0.0),
                                scene.pdf_emitter_direction(si, ds))
        emission_weight = mis_weight(bs.pdf, emitter_pdf)

        si = si_bsdf

    return result, valid_rays, scatter, non_scatter, invalid_sample
예제 #11
0
    def tabulate_histogram(self):
        """
        Invoke the provided sampling strategy many times and generate a
        histogram in the parameter domain. If ``sample_func`` returns a tuple
        ``(positions, weights)`` instead of just positions, the samples are
        considered to be weighted.
        """

        # Generate a table of uniform variates
        from mitsuba.core import Float, Vector2f, Vector2u, Float32, \
            UInt64, PCG32

        rng = PCG32(initseq=ek.arange(UInt64, self.sample_count))

        samples_in = getattr(mitsuba.core, 'Vector%if' % self.sample_dim)()
        for i in range(self.sample_dim):
            samples_in[i] = rng.next_float32() if Float is Float32 \
                else rng.next_float64()

        self.pdf_start = time.time()

        # Invoke sampling strategy
        samples_out = self.sample_func(samples_in)

        if type(samples_out) is tuple:
            weights_out = samples_out[1]
            samples_out = samples_out[0]
        else:
            weights_out = Float(1.0)

        # Map samples into the parameter domain
        xy = self.domain.map_backward(samples_out)

        # Sanity check
        eps = self.bounds.extents() * 1e-4
        in_domain = ek.all((xy >= self.bounds.min - eps)
                           & (xy <= self.bounds.max + eps))
        if not ek.all(in_domain):
            self._log('Encountered samples outside of the specified '
                      'domain: %s' % str(ek.compress(xy, ~in_domain)))
            self.fail = True

        # Normalize position values
        xy = (xy - self.bounds.min) / self.bounds.extents()
        xy = Vector2u(
            ek.clamp(xy * Vector2f(self.res), 0, Vector2f(self.res - 1)))

        # Compute a histogram of the positions in the parameter domain
        self.histogram = ek.zero(Float, ek.hprod(self.res))

        ek.scatter_add(target=self.histogram,
                       index=xy.x + xy.y * self.res.x,
                       source=weights_out)

        self.pdf_end = time.time()

        histogram_min = ek.hmin(self.histogram)
        if not histogram_min >= 0:
            self._log('Encountered a cell with negative sample '
                      'weights: %f' % histogram_min)
            self.fail = True

        self.histogram_sum = ek.hsum(self.histogram) / self.sample_count
        if self.histogram_sum > 1.1:
            self._log('Sample weights add up to a value greater '
                      'than 1.0: %f' % self.histogram_sum)
            self.fail = True
예제 #12
0
			print("[WARNING] NaNs in the vertex displacements. ({iteration:d})".format(iteration=i))
			exit(-1)

	vertex_pos_optim = ravel(params[vertex_pos_key])
	vertex_np = np.array(vertex_pos_optim)
	ind = np.linspace(0, vertex_np.shape[0]-1, num=vertex_np.shape[0])
	ind = np.reshape(ind,(-1,1))
	vertex_np = np.concatenate((vertex_np, ind), axis = 1)
	vertex_np = vertex_np[vertex_np[:,1].argsort()]
	vertex_np = vertex_np[vertex_np[:,0].argsort(kind='mergesort')]
	Z = np.reshape(vertex_np[:,2], (256, 256)).T
	Z_smooth = gaussian_filter(Z, sigma = smooth_sigma)
	vertex_np[:,2] =  np.reshape(Z_smooth, (-1,))
	vertex_np = vertex_np[vertex_np[:,3].argsort()]
	vertex_np = vertex_np[:, 0:3]
	params_opt['displacements'] = Float(vertex_np[:,2])

# Plot the loss curve
fig, ax = plt.subplots()
ax.plot(loss_list, '-b', label = 'Squared error between image_render and image_ref')
ax.plot(diff_render_init, '-r', label = 'Squared error between image_render and image_init')
leg = ax.legend()
plt.title('loss')
plt.xlabel('iteration')
plt.savefig(output_path + 'loss.png')

# Plot the difference between the vertex positions
fig, ax = plt.subplots()
ax.plot(diff_vertex_ref, '-b', label = 'Squared error between vertex_render and vertex_ref')
ax.plot(diff_vertex_init, '-r', label = 'Squared error between vertex_render and vertex_init')
leg = ax.legend()
예제 #13
0
    def sample(self, scene, sampler, ray, medium=None, active=True):
        result = Vector3f(0.0)
        si = scene.ray_intersect(ray, active)
        active = si.is_valid() & active

        # emitter = si.emitter(scene)
        # result = ek.select(active, Emitter.eval_vec(emitter, si, active), Vector3f(0.0))

        # z_axis = np.array([0, 0, 1])

        # vertex = si.p.numpy()
        # v_count = vertex.shape[0]
        # vertex = np.expand_dims(vertex, axis=1)
        # vertex = np.repeat(vertex, self.light.vertex_count(), axis=1)

        # light_vertices = self.light.vertex_positions_buffer().numpy().reshape(self.light.vertex_count(), 3)
        # light_vertices = np.expand_dims(light_vertices, axis=0)
        # light_vertices = np.repeat(light_vertices, v_count, axis=0)

        # sph_polygons = light_vertices - vertex
        # sph_polygons = sph_polygons / np.linalg.norm(sph_polygons, axis=2, keepdims=True)

        # z_axis = np.repeat( np.expand_dims(z_axis, axis=0), v_count, axis=0 )
        # result_np = np.zeros(v_count, dtype=np.double)

        # for idx in range( self.light.vertex_count() ):
        #     idx1 = (idx+1) % self.light.vertex_count()
        #     idx2 = (idx) % self.light.vertex_count()

        #     dp = np.sum( sph_polygons[:, idx1, :] * sph_polygons[:, idx2, :], axis=1 )
        #     acos = np.arccos(dp)

        #     cp = np.cross( sph_polygons[:, idx1, :], sph_polygons[:, idx2, :] )
        #     cp = cp / np.linalg.norm(cp, axis=1, keepdims=True)

        #     dp = np.sum( cp * z_axis, axis=1 )

        #     result_np += acos * dp

        # result_np *= 0.5 * 1.0/math.pi
        # result_np = np.repeat( result_np.reshape((v_count, 1)), 3, axis=1 )

        # fin = self.light_radiance * Vector3f(result_np)
        # fin[fin < 0] = 0
        # result += ek.select(active, fin, Vector3f(0.0))

        ctx = BSDFContext()
        bsdf = si.bsdf(ray)

        # bs, bsdf_val = BSDF.sample_vec(bsdf, ctx, si, sampler.next_1d(active), sampler.next_2d(active), active)
        # pdf = bs.pdf
        # wo = si.to_world(bs.wo)

        ds, emitter_val = scene.sample_emitter_direction(
            si, sampler.next_2d(active), True, active)
        pdf = ds.pdf
        active_e = active & ek.neq(ds.pdf, 0.0)
        wo = ds.d
        bsdf_val = BSDF.eval_vec(bsdf, ctx, si, wo, active_e)
        emitter_val[emitter_val > 1.0] = 1.0
        bsdf_val *= emitter_val

        # _, wi_theta, wi_phi = sph_convert(si.to_world(si.wi))
        _, wo_theta, wo_phi = sph_convert(wo)

        y_0_0_ = y_0_0(bsdf_val, wo_theta, wo_phi) / ek.select(
            pdf > 0, pdf, Float(0.01))

        y_1_n1_ = y_1_n1(bsdf_val, wo_theta, wo_phi) / ek.select(
            pdf > 0, pdf, Float(0.01))
        y_1_0_ = y_1_0(bsdf_val, wo_theta, wo_phi) / ek.select(
            pdf > 0, pdf, Float(0.01))
        y_1_p1_ = y_1_p1(bsdf_val, wo_theta, wo_phi) / ek.select(
            pdf > 0, pdf, Float(0.01))

        y_2_n2_ = y_2_n2(bsdf_val, wo_theta, wo_phi) / ek.select(
            pdf > 0, pdf, Float(0.01))
        y_2_n1_ = y_2_n1(bsdf_val, wo_theta, wo_phi) / ek.select(
            pdf > 0, pdf, Float(0.01))
        y_2_0_ = y_2_0(bsdf_val, wo_theta, wo_phi) / ek.select(
            pdf > 0, pdf, Float(0.01))
        y_2_p1_ = y_2_p1(bsdf_val, wo_theta, wo_phi) / ek.select(
            pdf > 0, pdf, Float(0.01))
        y_2_p2_ = y_2_p2(bsdf_val, wo_theta, wo_phi) / ek.select(
            pdf > 0, pdf, Float(0.01))

        return result, si.is_valid(), [ Float(y_0_0_[0]), Float(y_0_0_[1]), Float(y_0_0_[2]),\
                                        Float(y_1_n1_[0]), Float(y_1_n1_[1]), Float(y_1_n1_[2]),\
                                        Float(y_1_0_[0]), Float(y_1_0_[1]), Float(y_1_0_[2]),\
                                        Float(y_1_p1_[0]), Float(y_1_p1_[1]), Float(y_1_p1_[2]),\
                                        Float(y_2_n2_[0]), Float(y_2_n2_[1]), Float(y_2_n2_[2]),\
                                        Float(y_2_n1_[0]), Float(y_2_n1_[1]), Float(y_2_n1_[2]),\
                                        Float(y_2_0_[0]), Float(y_2_0_[1]), Float(y_2_0_[2]),\
                                        Float(y_2_p1_[0]), Float(y_2_p1_[1]), Float(y_2_p1_[2]),\
                                        Float(y_2_p2_[0]), Float(y_2_p2_[1]), Float(y_2_p2_[2]) ]