重点サンプリングの収束の様子を確認する。その2。

簡単な多重重点サンプリングの検証の続き。

ma38su.hatenablog.com

早く綺麗な絵をレイトレで描こうとした時に、ハマって確認した内容を残しておく。 直接レイトレは扱わないが、暗いシーンに限られた光源が置かれているようなシーンにおいては、収束を早くするため、ライト方向の拡散や反射を重点サンプリングすることがある。

https://raytracing.github.io/books/RayTracingTheRestOfYourLife.html#samplinglightsdirectly

参考にGLSLに書き下ろしたもの。 https://www.shadertoy.com/view/WlycW3

この光源への重点サンプリング、 単独で積分して期待値を求めると違う値に収束するので、適切に混ぜないとちゃんと収束しない。

拡散(ランバート反射)とライトの重点サンプリングのを混合した多重重点サンプリング

f:id:ma38su:20210202095324p:plain

まず、ライト方向だけで重点サンプリングした場合は異なる値に収束することが確認できる。 それに対して、cosine分布との多重重点サンプリングした場合には、同じ値に収束する。

ただ、ライト方向を加えた多重重点サンプリングの場合は、cosine分布のみの重点サンプリングより収束が遅くなっていることがわかる。 これは期待する収束値に対してライト方向のサンプリング値が小さく乖離した値であるため、収束への貢献が小さいためであると考えられる。

通常はライト方向にサンプリングは、大きな値(明るい色)が取得できるため、収束が早まることが期待できる。

検証のためのコード

サンプリングの起点は、原点(0, 0, 0)。 ライトの重点サンプリングは、ライト上の点に向かう反射だけをサンプリングするもの。 ライトは、(1, 5, 1)を中心として、XZ平面に幅light_w=2、奥行きlight_d=2にあると仮定しており、 PDFは確率密度関数

import math
import random

import numpy as np
import matplotlib.pyplot as plt

def main():

    nv = np.array([0, 1, 0])

    # ベクトルを正規化する
    def normalize(v):
        l = np.linalg.norm(v)
        return v / l

    # 最小値と最大値を指定して乱数を生成する
    def random_double(min_v, max_v):
        return min_v + (max_v - min_v) * random.random()

    # 球内の点を一様分布で生成
    def random_in_sphere():
        while True:
            x = random_double(-1,1)
            y = random_double(-1,1)
            z = random_double(-1,1)
            dist_sq = x * x + y * y + z * z
            if dist_sq > 0 and dist_sq < 1:
                return np.array([x, y, z])

    # ランバート反射方向(cosine分布)を生成
    def random_lambertian():
        return normalize(nv + normalize(random_in_sphere()))

    # ランバート反射の確率密度関数
    def cosine_pdf(v):
        cosine = np.dot(nv, normalize(v))
        return 0 if cosine <= 0 else cosine / math.pi

    light_p = np.array([1, 5, 1])
    light_nv = np.array([0, -1, 0])
    light_w = 2
    light_d = 2
    
    # 正方形のライト内部の点を一様分布で生成
    def random_in_light():
        x = light_p[0] + random_double(-light_w / 2, light_w / 2)
        y = light_p[1]
        z = light_p[2] + random_double(-light_d / 2, light_d / 2)
        return normalize(np.array([x, y, z]))

    # ライト内部の点の確率密度関数
    def light_pdf(v):
        x, y, z = v
        t = light_p[1] / y
        if t <= 0:
            return 0
        cx = x * t
        cz = z * t
        if cx < light_p[0] - light_w / 2 or cx > light_p[0] + light_w / 2:
            return 0
        if cz < light_p[2] - light_d / 2 or cz > light_p[2] + light_d / 2:
            return 0
        dist_sq = t * t
        cosine = np.dot(v, light_nv)
        return dist_sq / (4 * abs(cosine))

    def sampling_cosine(n):
        s = 0.0
        nv = np.array([0, 1, 0])
        seq = []

        for i in range(1, n+1):
            v = random_lambertian()
            cosine = np.dot(v, nv)
            pdf_val = cosine_pdf(v)
            if pdf_val > 0:
                s += math.pow(cosine, 2) / pdf_val

            seq.append(s / i)

        return seq

    def sampling_light(n):
        s = 0.0
        seq = []

        for i in range(1, n+1):
            v = random_in_light()
            cosine = np.dot(nv, v)
            pdf_val = light_pdf(v)
            if pdf_val > 0:
                s += math.pow(cosine, 2) / pdf_val

            seq.append(s / i)

        return seq

    def sampling_mix_cosine_and_light(n):
        s = 0.0
        mix_rate = 0.5
        seq = []

        def random_mix_direction():
            if random.random() < mix_rate:
                return random_in_light()
            else:
                return random_lambertian()

        def mix_pdf(v):
            return (1 - mix_rate) * cosine_pdf(v) + mix_rate * light_pdf(v)

        for i in range(1, n+1):
            v = random_mix_direction()
            cosine = np.dot(nv, v)
            pdf_val = mix_pdf(v)
            if pdf_val > 0:
                s += math.pow(cosine, 2) / pdf_val
            seq.append(s / i)
        return seq

    # main body

    n = 10000
    sampling=5

    colors = ('#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf')
    col_idx = 0

    for i in range(sampling):
        if i == 0:
            label='cosine'
        else:
            label=None
        plt.scatter(range(1, n+1), sampling_cosine(n), s=2, c=colors[col_idx], marker='o', label=label)
    col_idx += 1

    for i in range(sampling):
        if i == 0:
            label='light'
        else:
            label=None
        plt.scatter(range(1, n+1), sampling_light(n), s=2, c=colors[col_idx], marker='o', label=label)
    col_idx += 1

    for i in range(sampling):
        if i == 0:
            label='mix'
        else:
            label=None
        plt.scatter(range(1, n+1), sampling_mix_cosine_and_light(n), s=2, c=colors[col_idx], marker='o', label=label)
    col_idx += 1

    plt.scatter(range(1, n+1), [math.pi*2/3]*n, c=colors[col_idx], s=1, marker='.', label='ans')

    plt.xscale('log')
    plt.grid(which='major',color='black',linestyle='-')
    plt.grid(which='minor',color='gray',linestyle='-')
    plt.legend()
    plt.savefig('chart.png')

main()

とはいえ、いろいろ前提の説明は足りないので詳しくは原著にあたってください。

https://raytracing.github.io/books/RayTracingTheRestOfYourLife.html#samplinglightsdirectly