Heavy Watal

rgl — 3Dグラフ描画

3D visualization device system (OpenGL).

APIもガチャガチャだしドキュメントも結構いい加減なので分かりにくい。 かといってJavaScript+WebGLベースの plotlythreejs などもPNG保存や陰影などまだそれなりに問題がある。 科学プロット用のOpenGLラッパーとしてはPythonのVisPy が将来有望だがまだ開発途上で仕様があまり固まってない。

関数は低水準のrgl.*()と高水準の*3d()に分かれていて、 両者を混ぜて使うのは避けたほうがいいらしい。

デバイスの起動と終了

rgl::open3d(..., params=get3dDefaults(), useNULL=rgl.useNULL())
明示的に新しいデバイスを開く。 何も無い状態でplot3d()などが呼ばれたら勝手に開かれる。 サイズ指定はwindowRect=c(0, 0, 600, 600)のような引数で。
useNULL=TRUEはWebGLを書き出すだけで描画デバイスが必要ないときに。 セッション全体に渡ってデバイス不要な(あるいはそもそも無い)場合は、 library(rgl)の前にoptions(rgl.useNULL=TRUE)するのがよい。
rgl.close()
デバイスを閉じる。close3d()はなぜか存在しない。

rgl::clear3d(type=c('shapes', 'bboxdeco', 'material'), defaults, subscene=0)

プロット

rgl::points3d()
散布図。
rgl::spheres3d()
球体。
rgl::lines3d()
折れ線
rgl::segments3d()
線分
rgl::triangles3d()
3点を結ぶ面
rgl::quads3d()
4点を結ぶ三角形2つ
rgl::surface3d(), rgl::terrain3d()
地形図のような局面
rgl::plot3d()
type=引数で上記のタイプを決定できる高次関数。 ホントはあんまり使いたくないけど、 xlim, ylim, zlim オプションを受け付ける関数がこれしかないようなので、 境界を指定しつつ球体を描きたい場合は spheres3d() ではなく plot3d(type='s') を使うしかないっぽい。

背景や軸などの調整

rgl::title3d(main, sub, xlab, ylab, zlab, line=NA, ...)
これを使うとmainとsubも視点によって動いてしまう。 bgplot3d({plot.new(); title('main')}) なら固定背景に書ける。

rgl::mtext3d(text, edge, line=0, at=NULL, pos=NA, ...)

rgl::bg3d()

rgl::light3d()

rgl::par3d()

rgl::material3d(...)
プロットに渡せるオプション(colorなど)はここで確認

rgl::axis3d(edge, at=NULL, labels=TRUE, tick=TRUe, line=0, pos=NULL, nticks=5, ...)
xyz+- の組み合わせで軸1本を指定して描く。 x は x-- と等価。
rgl::box3d(...)
12辺の箱を描く。
rgl::bbox3d(xat=NULL, yat, zat, xunit='pretty', yunit, zunit, expand=1.03, draw_front=FALSE)
手前の辺が自動で消えるような箱を描く。
rgl::axes3d(edges='bbox', labels=TRUE, tick=TRUE, nticks=5, box=FALSE, expand=1.03, ...)
上記の3つをまとめる関数。分かりにくいので使わないほうがいい。 edges='bbox' の場合 tick=FALSE は効かないので xlen=0, ylen=0, zlen=0 とする必要がある。
rgl::view3d(theta=0, phi=15, fov=60, zoom=1, scale=par3d("scale"), interactive=TRUE, userMatrix)
theta: 0のとき正面がxy平面。観察者が地球の公転と同じ方向に動くのが正。
phi [-90, 90]: 0のとき視点が水平面(xz平面)上。観察者が上に動くのが正。
fov [0, 179]: 0のとき無限遠から見たような平行投影。

複数の図をまとめる

# レイアウトを指定
mfrow3d(nr, nc, byrow=TRUE, parent=NA, sharedMouse=FALSE, ...)
layout3d(mat, widths, heights, parent=NA, sharedMouse=FALSE, ...)

# 次のsubsceneに移動
next3d(current=NA, clear=TRUE, reuse=TRUE)

これらはなぜかグローバルスコープでしか動作しない。 つまり、関数やループ内に入れるとサイズなどがうまく反映されない。

出力

ファイルに書き出す

rgl::scene3d()
rglネイティブな形での全構成要素リスト。
rgl::snapshot3d(filename, fmt='png', top=TRUE)
PNGのみ。 top=FALSEにしてはダメ。謎。
rbl.postscript(filename, fmt='eps', drawText=TRUE)
ps, eps, tex, pdf, svg をサポート。 透過や bgplot3d は反映されないらしいので注意。
rgl::writeWebGL(dir='webGL', filename, template, prefix, snapshot, commonParts, reuse, font, width, height)
ディレクトリ構造無しの単発HTMLでいい場合は writeWebGL('.', 'rgl.html') のように指定する。 ヘルプには snapshot がファイル名も受け取れると書いてあるが嘘で TRUE/FALSE のみ。 rglデバイスが不要なのでopen3d(useNULL=TRUE)としておくと余計なウィンドウを開かずに済む。

rgl::writeASY(), rgl::writeOBJ(), rgl::writePLY(), rgl::writeSTL().

rmarkdown/knitrでHTMLに埋め込む

ライブラリを読み込み、フックを設定しておく:

```{r library}
library(rgl)
rgl::setupKnitr()
```

rgl::setupKnitr()の中身は knitr::knit_hooks$set(webgl=rgl::hook_webgl) といくつかの初期化コード。 それらを実行しないと、同じコードでも時によってscriptが正しく埋め込まれず、 You must enable Javascript to view this page properly. という的外れなエラーが表示される。

プロットしたいchunkにwebgl=TRUEを指定 (PNG静止画にしたい場合はデバイス有りでrgl=TRUE):

```{r plot, webgl=TRUE}
rgl::open3d(useNULL=TRUE)
rgl::box3d()
rgl::title3d('main', 'sub', 'x', 'y', 'z')
```

ループで複数描画したいときはまずrglwidgetとしてlistに詰めていき、 最後にhtmltools::tagList()に詰め替える。 このwidget方式の場合はsetupKnitr()不要。

```{r widget}
library(rgl)
purrr::map(seq_len(3), ~{
    on.exit(rgl::rgl.close())
    rgl::open3d(useNULL=TRUE)
    rgl::box3d()
    rgl::rglwidget(width=200, height=200)
}) %>>% htmltools::tagList()
```

options(rgl.printRglwidget=TRUE) とするとrglwidget()を省略できるが、途中経過も逐一表示されてしまう。

アニメーション

rgl::spin3d(axis=c(0, 0, 1), rpm=5)

rgl::par3dinterp(times=NULL, userMatrix, scale, zoom, FOV, method, extrapolate)

rgl::play3d(f, duration=Inf, ...)

rgl::movie3d(f, duration, ..., fps=10, movie="movie", frames=movie, dir=tempdir(), ...)

## 角度をセット
rgl::view3d(-25, 15, 40)

## 最前面に持ってくる
rgl.bringtotop()

## アニメーション関数を作る
.anime = rgl::spin3d(axis=c(0, 1, 0), rpm=15)

## X11で再生
rgl::play3d(.anime)

## GIFアニメとして保存
rgl::movie3d(.anime, duration=4, fps=16, movie="basename", dir="~/tmp")