このブログポストはレイトレアドベントカレンダー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をインストールします。
!pip install drjit --quiet
次にPOT(Python Optimal Transport)をインストールします。これは最適輸送(Optimal Transport, OT))を解くためのライブラリです。詳細は後述します。
!pip install POT --quiet
無事インストールできました。
次に必要なライブラリをimportしていきます。
Dr.JitとPOT以外にもnumpyやmatploblibなどもimportしていきます。
# 定番ライブラリ
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のようにフラグメントシェーダー的に書くための便利な関数も作ります。
# ---------------------------
# 便利な定数と関数の準備
# ---------------------------
# ドライブをマウントし、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()
これで下準備が整いました。
ちゃんと想定通り動くか試すために画像を単純に貼り付けてみます。
# イメージファイルからテクスチャをロードして、そのまま貼り付ける
# 画像は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)
ちゃんと想定通り動きました。
簡単にコードの説明をします。
変数uvや、関数tex.sampleの戻り値は全ピクセルの値が入った(Dr.Jitの)配列であることに注意してください。そのため、フラグメントシェーダーのようなピクセル単位の分岐は通常のif文ではできないことに注意してください。もう一つの例題として、試しにGL_REPEATのような絵を作ってみます。
# 単純にリピートして表示
def simpleRepeat(uv : Array2f) -> Array3f:
uv = dr.fmod(uv*4.0, 1.0)
return tex_example.sample(uv)
shadertoy(simpleRepeat, 512)