このブログポストはレイトレアドベントカレンダー2022の19日目の投稿です。


はじめに

テクスチャをGL_REPEATなどでタイリングすると、リピート感が出てしまい、とてもチープな見た目になってしまいます。

この問題を解決するために、入力画像と似たようなパターンを無限に作り出し、それをスムースに繋ぐという手法[1][2][3][4]があります。[1][2]の手法の核である「似たようなパターンを無限に作る」の部分は、手法の内容を知らないと魔法のように感じる部分ですが、根底にあるアイデアはとてもシンプルです。この投稿では、この核となるアイデアについて解説していきます。

それではまずは前準備からはじめてみましょう!

前準備

まずDr.Jitをインストールしします。 Dr.Jitとは、PythonコードをJITコンパイルしてSIMDやCudaで動くようにしたり、自動微分ができるライブラリです。微分可能レンダラーのMitsuba3のバックエンドとしても使われています。今回は自動微分機能は使わず、JITコンパイルをした上でCuda上で高速に動かすために使用します。Dr.Jitの詳細はgithubや、リファレンスを参照してください。このJupyterのNotebookはGoogle Colab上で作られているのですが、Colabの拡張機能としてターミナルのコマンドをNobtebook上で実行できるので、それを利用してpipを使ってDr.Jitをインストールします。

In [1]:
!pip install drjit --quiet
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.5/4.5 MB 24.8 MB/s eta 0:00:00

次にPOT(Python Optimal Transport)をインストールします。これは最適輸送(Optimal Transport, OT))を解くためのライブラリです。詳細は後述します。

In [2]:
!pip install POT --quiet
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 682.3/682.3 KB 9.4 MB/s eta 0:00:00

無事インストールできました。
次に必要なライブラリをimportしていきます。

Dr.JitとPOT以外にもnumpymatploblibなどもimportしていきます。

In [3]:
# 定番ライブラリ
import math 
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
from scipy import stats

# Dr.JIT
import drjit as dr
from drjit.cuda import Array2f, Array3f, Texture2f, Texture3f, Float, TensorXf
#from drjit.llvm import Array2f, Array3f, Texture2f, Texture3f, Float, TensorXf

# POT
import ot
import ot.plot

各種ライブラリをimportできました。

次に便利な機能を先に作っておきます。画像処理をShaderToyのようにフラグメントシェーダー的に書くための便利な関数も作ります。

In [4]:
# ---------------------------
# 便利な定数と関数の準備
# ---------------------------

# ドライブをマウントし、Notebookがあるフォルダまで移動する
import google.colab as colab
colab.drive.mount('/content/drive/')
%cd "/content/drive/MyDrive/colab/infinite patterns"

# テクスチャのサンプル
def sample(tex,uv):
    return Array3f(tex.eval_cubic(uv))
    #return Array3f(tex.eval(uv))
    
Texture2f.sample = sample
Texture3f.sample = sample

# テクスチャの値をRGBの配列にする
def texToRGB(tex: Texture2f):
    tv = tex.value().numpy()
    tv = tv.reshape(tex.shape[0]*tex.shape[1], 3)
    return tv

# ファイル名からテクスチャを生成
def loadTexture(filename : str) -> Texture2f:
    img = np.asarray(Image.open(filename))
    # 4チャンネル画像も3チャンネルに変換
    img = img[:,:,:3]
    img = img / 256.0
    tex = Texture2f(TensorXf(img))
    return tex

# DrJitにfmodがないので追加
def fmod(a,b):
    return a - dr.floor(a/b) * b
dr.fmod = fmod

# shadertoyのように画像処理を書けるようにする関数
def shadertoy(f, width):
    # UVの生成
    x = dr.linspace(Float, 0.0, 1.0, width)
    y = dr.linspace(Float, 0.0, 1.0, width)
    u, v = dr.meshgrid(x, y)
    uv = Array2f(u, v)
    # レンダリングする関数を呼ぶ
    img = f(uv)
    # テンソル型にして表示
    imgt = TensorXf(dr.ravel(img), shape=(width, width,3))
    dpi = 80
    height_ratio = 4
    figsize = width / float(dpi), (width*(1+height_ratio)/height_ratio) / float(dpi)
    fig, ax = plt.subplots(2, 1, figsize=figsize,tight_layout=True, gridspec_kw={'height_ratios': [height_ratio, 1]})
    ax[0].axis("off")
    ax[0].imshow(imgt)
    # ヒストグラムをカラーチャンネル毎に表示する
    ax[1].tick_params(left=False)
    ax[1].axes.yaxis.set_ticklabels([])
    ax[1].set_xlim([0.0,1.0]) 
    img_tmp = img.numpy()
    ax[1].hist(img_tmp[:,0].flatten(), bins=256, histtype='step', color='red')
    ax[1].hist(img_tmp[:,1].flatten(), bins=256, histtype='step', color='green')
    ax[1].hist(img_tmp[:,2].flatten(), bins=256, histtype='step', color='blue')
    plt.tight_layout()
    plt.show()
Mounted at /content/drive/
/content/drive/MyDrive/colab/infinite patterns

元画像の表示

これで下準備が整いました。
ちゃんと想定通り動くか試すために画像を単純に貼り付けてみます。

In [5]:
# イメージファイルからテクスチャをロードして、そのまま貼り付ける
# 画像はPolyで生成しました
# https://withpoly.com/texture/create?asset_id=Q9NrGKoKuj
tex_example = loadTexture(f'moss_128.png')
def simpleDisplay(uv : Array2f) -> Array3f:
    return tex_example.sample(uv)
shadertoy(simpleDisplay, 512)

ちゃんと想定通り動きました。
簡単にコードの説明をします。

  • shadertoy関数は、画像を生成し、それを表示します。第1引数に画像全体で実行したいフラグメントシェーダーのような関数、第2引数にその画像のサイズを指定します。今回は512x512の画像を最終的に出力することになります。
  • simpleDisplay関数は、UVを引数にとります。これはArray2f型で、UV座標が全ピクセル数分だけ入ってます。今回の場合は512x512=262144個のUVが格納されています。
  • texは今回貼り付けるテクスチャが入っており、sample関数でそのテクスチャの指定した座標のピクセル値をサンプルして返します。
  • 下に表示されているのはチャンネル毎のヒストグラムです。

変数uvや、関数tex.sampleの戻り値は全ピクセルの値が入った(Dr.Jitの)配列であることに注意してください。そのため、フラグメントシェーダーのようなピクセル単位の分岐は通常のif文ではできないことに注意してください。もう一つの例題として、試しにGL_REPEATのような絵を作ってみます。

In [6]:
# 単純にリピートして表示
def simpleRepeat(uv : Array2f) -> Array3f:
    uv = dr.fmod(uv*4.0, 1.0)
    return tex_example.sample(uv)
shadertoy(simpleRepeat, 512)