WebGL のユーザインタラクション (更新)

Duncan Murdoch

5月 15, 2021

はじめに

このドキュメントでは、HTMLドキュメントに rgl のシーンを埋め込み、埋め込まれたJavascriptを使って HTML ドキュメント内の WebGL 表示を制御する方法を説明します。 rglについてのより一般的な情報は、rgl Overview を参照してください。

HTML ドキュメントは、knitrrmarkdown を使った R の Markdown ソースから生成されることを想定しています。 このフォーマットでは、テキストと Markdown マークアップと R コードのチャンクが混在しています。 他の方法についての議論は限られています。

ドキュメントの中に rgl のシーンを埋め込むには2つの方法があります。 推奨されるのは、rglwidgetを呼び出して、印刷することでドキュメントに埋め込むことができる「ウィジェット」を生成する方法です。

rgl バージョン0.102.0以前に使われていた古い方法(例えば、writeWebGL)はサポートされなくなりました。

私は3つ目の方法について実験を行いました。 これは、knitr で標準的な2Dグラフィックスをインクルードする方法に似たものを目指しています。 つまり、あなたが何かを描いたという事実を検知して、それを自動的にインクルードします。 現在のところ、この方法は推奨されていませんが、将来的には変更される可能性があります。

ブラウザのサポート

現在、ほとんどのブラウザがWebGLをサポートしていますが、ブラウザによってはデフォルトで無効になっているものもあります。 様々なブラウザのヘルプについては、https://get.webgl.org をご覧ください。

まずは、虹彩データの簡単なプロットから始めます。 コードチャンクを挿入し、オプションの引数 elementIdrglwidget 関数を呼び出します。 これにより、後の Javascript コードで画像を参照することができます。 また、プロットからオブジェクトIDを保存し、後で操作できるようにしています。

library(rgl)
plotids <- with(iris, plot3d(Sepal.Length, Sepal.Width, Petal.Length, 
                  type="s", col=as.numeric(Species)))
rglwidget(elementId = "plot3drgl")

次に、データの表示を切り替えるためのボタンを挿入します。

toggleWidget(sceneId = "plot3drgl", ids = plotids["data"], label = "Data")

sceneIdrglwidget() で使用した elementId と同じで、ids はトグルさせたいオブジェクトのオブジェクト ID、label はボタンに表示されるラベルです。 変数 plotids の中の名前を調べるには、names() または unclass() を適用します。:

names(plotids)
## [1] "data" "axes" "xlab" "ylab" "zlab"
unclass(plotids)
## data axes xlab ylab zlab 
##   13   14   15   16   17

magrittr やベースパイプの使用

rglwidget() 内の elementIdtoggleWidget() (または後述の playwidget()) 内の sceneId と一致させるのはエラーになりやすいです。 両方が一緒に表示されるような通常のケースでは、magrittr スタイルのパイプを非常に柔軟に使用することができます。 コントロールウィジェットの第一引数は、rglwidget() の結果(または他のコントロールウィジェット)を受け入れ、rglwidget()controllers 引数は、コントロールウィジェットを受け入れます。 R 4.1.0 では、新しいベースパイプ演算子 |> も同じように使用できるはずです。

例えば、以下のようになります。

rglwidget() %>%
toggleWidget(ids = plotids["data"], label = "Data")

R 4.1.0以上をお持ちの方は、こちらも同様です。:

rglwidget() |>
toggleWidget(ids = plotids["data"], label = "Data")

ボタンとシーンの順番を入れ替えることができます。magrittr のドット(またはbase pipesの=>構文)を使って、toggleWidgetcontrollers の引数で rglwidget に渡します。:

toggleWidget(NA, ids = plotids["data"], label = "Data") %>%
rglwidget(controllers = .) 

または、R 4.1.0 以降を使って

toggleWidget(NA, ids = plotids["data"], label = "Data") |> 
  w => rglwidget(controllers = w) 

コントロール

toggleWidget を使ってプロットの内容を変更する方法を見てきました。 もっと精巧な表示をすることもできます。 例えば、先ほどのプロットをやり直すことができますが、3つの種を別々の「球体」オブジェクトとして表示し、それらを切り替えるためのボタンを用意します。:

clear3d() # Remove the earlier display

setosa <- with(subset(iris, Species == "setosa"), 
     spheres3d(Sepal.Length, Sepal.Width, Petal.Length, 
                  col=as.numeric(Species),
                  radius = 0.211))
versicolor <- with(subset(iris, Species == "versicolor"), 
     spheres3d(Sepal.Length, Sepal.Width, Petal.Length, 
               col=as.numeric(Species),
               radius = 0.211))
virginica <- with(subset(iris, Species == "virginica"), 
     spheres3d(Sepal.Length, Sepal.Width, Petal.Length, 
               col=as.numeric(Species),
               radius = 0.211))
aspect3d(1,1,1)
axesid <- decorate3d()
rglwidget() %>%
toggleWidget(ids = setosa) %>%
toggleWidget(ids = versicolor) %>%
toggleWidget(ids = virginica) %>%
toggleWidget(ids = axesid) %>% 
asRow(last = 4)

label の引数を省略したので、ボタンには ids として渡された変数の名前がラベルとして表示されます。 asRow関数については between で説明します。

toggleWidget() は、playwidgetsubsetControl という2つの関数の便利なラッパーです。 playwidget() はWebページにボタンを追加し(スライダーの追加やアニメーションなども可能)、subsetControl()は表示するオブジェクトのサブセットを選択します。

subsetControl

より一般的な例では、スライダーを使って、虹彩ディスプレイのデータのいくつかのサブセットを選択することができます。 例えば

rglwidget() %>%
playwidget(start = 0, stop = 3, interval = 1,
       subsetControl(1, subsets = list(
                 Setosa = setosa,
                 Versicolor = versicolor,
                 Virginica = virginica,
                 All = c(setosa, versicolor, virginica)
                 )))

この他にもいくつかの「制御」機能があります。

par3dinterpControl

par3dinterpControl は、par3dinterp の結果を近似したものです。

例えば、次のコード(play3d の例に似ています)は、複雑な方法でシーンを回転させます。

M <- r3dDefaults$userMatrix
fn <- par3dinterp(time = (0:2)*0.75, userMatrix = list(M,
                                      rotate3d(M, pi/2, 1, 0, 0),
                                      rotate3d(M, pi/2, 0, 1, 0)) )
rglwidget() %>%
playwidget(par3dinterpControl(fn, 0, 3, steps=15),
       step = 0.01, loop = TRUE, rate = 0.5)

注意点としては,生成された Javascript のスライダーは300刻みで,動きが滑らかに見えるようになっています. しかし,300個の userMatrix の値を保存するには多くのスペースを必要とするため,Javascript のコードで補間を使用しています. しかし,Javascript のコードでは線形補間しかできず,par3dinterp で行うような複雑なスプラインベースの SO(3) 補間はできません. このため、par3dinterpControlから15ステップの出力を行い、線形補間の歪みが見えないようにする必要があります。

propertyControl

propertyControl は、シーンのプロパティの値を設定する、より一般的な関数です。 現在、ほとんどのプロパティに対応していますが、使用するには内部実装の知識が必要です。

clipplaneControl

clipplaneControl では、スライダを動かしてクリッピングプレーンの位置を制御することができます。

vertexControl (頂点コントロール)

propertyControl よりも汎用性が低いのが vertexControl です。 この関数は、シーン内の個々の頂点の属性を設定します。 例えば、setosa グループの最も近い点のx座標を設定したり、その色を黒から白に変更したりします。

setosavals <- subset(iris, Species == "setosa")
which <- which.min(setosavals$Sepal.Width)
init <- setosavals$Sepal.Length[which]
rglwidget() %>%
playwidget(
  vertexControl(values = matrix(c(init,   0,   0,   0, 
                                     8,   1,   1,   1), 
                                nrow = 2, byrow = TRUE),
                attributes = c("x", "red", "green", "blue"),
                vertices = which, objid = setosa),
    step = 0.01)

ageControl

関連する関数として、ageControl がありますが、これは属性の仕様が大きく異なります。 この関数は、スライダがシーンの「年齢」をコントロールし、頂点の属性がその年齢に応じて変化する場合に使用します。

説明のために、曲線に沿って動く点を示します。 1つのリストに2つの ageControl コールを入れます。 最初のものは軌跡の色をコントロールし、2番目のものは点の位置をコントロールします。:

time <- 0:500
xyz <- cbind(cos(time/20), sin(time/10), time)
lineid <- plot3d(xyz, type="l", col = "black")["data"]
sphereid <- spheres3d(xyz[1, , drop=FALSE], radius = 8, col = "red")
rglwidget() %>%
playwidget(list(
  ageControl(births = time, ages = c(0, 0, 50),
        colors = c("gray", "red", "gray"), objids = lineid),
  ageControl(births = 0, ages = time,
        vertices = xyz, objids = sphereid)),
  start = 0, stop = max(time) + 20, rate = 50,
  components = c("Reverse", "Play", "Slower", "Faster",
                 "Reset", "Slider", "Label"),
  loop = TRUE)

rglMouse

rglMouse 関数は、このセクションの他の関数のような意味でのコントロールではありませんが、HTMLコントロールをディスプレイに追加して、ユーザーがマウスモードを選択できるようにするために使用します。

例えば、以下のディスプレイは、最初は特定のポイントを選択できるようになっていますが、マウスモードを変更することで、ユーザがディスプレイを回転させてシーンを別の角度から見ることができるようになります。

ids <- with(iris, plot3d(Sepal.Length, Sepal.Width, Petal.Length, 
                  type="s", col=as.numeric(Species)))
par3d(mouseMode = "selecting")
rglwidget(shared = rglShared(ids["data"])) %>% 
rglMouse()

ここで使われている rglShared() コールについては、below で説明しています。

ディスプレイのレイアウト

多くの rgl ディスプレイは、1つまたは複数の rgl シーンとコントロールという複数の要素を含みます。 内部的には rglmanipulateWidget パッケージの combineWidgets 関数を使用します。

rgl パッケージはディスプレイをアレンジするための3つの便利な関数を提供します。 最初の関数は magrittr パイプ、%>% です。 パイプを使ってディスプレイを1つのオブジェクトとして構築すると、パイプライン内のオブジェクトは1列に配置されます。

2つ目の便利な関数は、asRow です。 これは、オブジェクトのリストまたは combineWidgets オブジェクト(おそらくパイプの結果)を入力として受け取り、それらの(いくつかの)オブジェクトを横一列に並べ替えます。 toggleWidget example のように、last 引数を使用して、asRow のアクションを指定されたコンポーネントの数に制限することができます。 (last = 0 の場合は、すべてのオブジェクトがスタックされます。これは、いくつかのオブジェクトが rgl パッケージのものではないので、パイピングが機能しない場合に便利です)。

最後に、getWidgetIdを使うと、HTMLウィジェットからHTML要素のIDを抽出することができます。 これは、以下の crosstalk の例のように、すべてが同じパイプの要素ではないウィジェットを組み合わせるときに便利です。

これらの便利な関数では不十分な場合は、manipulateWidget::combineWidgets や、manipulateWidget の他の関数を呼ぶことで、より柔軟な表示のアレンジが可能になります。

crosstalk との統合

crosstalk パッケージを使うと、ウィジェット同士の通信が可能になります。 現在、観測データの選択とフィルタリングをサポートしています。

rgl はこれらのメッセージを送信、受信、表示することができます。 rgl のディスプレイは複数のサブシーンを持つことができ、それぞれが異なるデータセットを表示します。 シーン内の各オブジェクトは、潜在的に crosstalk 的な意味での共有データセットです。

このリンクは rglShared 関数に依存します。 rglShared(id) (id は現在のシーンにあるオブジェクトの rgl の id 値です) を呼び出すと、rgl オブジェクトの頂点の座標を含む共有データオブジェクトが作成されます。 このオブジェクトは rglwidgetshared 引数に渡されます。 また、共有データを受け付ける他のウィジェットにも渡すことができ、2つのディスプレイをリンクさせることができます。

共有データオブジェクトが他の方法で作成された場合は、以下の例のように、その keygroup プロパティをコピーすることで、特定の rgl id 値にリンクすることができます。

library(crosstalk)
sd <- SharedData$new(mtcars)
ids <- plot3d(sd$origData(), col = mtcars$cyl, type = "s")
# Copy the key and group from existing shared data
rglsd <- rglShared(ids["data"], key = sd$key(), group = sd$groupName())
rglwidget(shared = rglsd) %>%
asRow("Mouse mode: ", rglMouse(getWidgetId(.)), 
      "Subset: ", filter_checkbox("cylinderselector", 
                        "Cylinders", sd, ~ cyl, inline = TRUE),
      last = 4, colsize = c(1,2,1,2), height = 60)

rgl シーン内の複数のオブジェクトを共有データとみなす必要がある場合は、複数の rglShared() コールの結果をリストで渡すことができます。 キーの値は、データセット間で共有されていると仮定されます。 これが望ましくない場合は、プレフィックスを使用するなどして、オブジェクト間で異なるようにしてください。

同じ rgl ID を複数の rglShared() オブジェクトで使用した場合、すべてのオブジェクトからのメッセージに応答します。 これは、1つのメッセージが前のメッセージをキャンセルしてしまうため、望ましくない動作につながる可能性があります。

低レベルコントロール

このドキュメントの最初のプロットを繰り返します。:

plotids <- with(iris, plot3d(Sepal.Length, Sepal.Width, Petal.Length, 
                  type="s", col=as.numeric(Species)))
subid <- currentSubscene3d()
rglwidget(elementId="plot3drgl2")

ウェブページ上のボタンを使って、プロットの回転など、表示の変更を行いたい場合があります。 まず、ボタンを追加します。 “onclick”イベントには次のような関数を設定します。:

<button type="button" onclick="rotate(10)">Forward</button>
<button type="button" onclick="rotate(-10)">Backward</button>

このボタンを生成します。:

上のコードチャンクで subid に現在アクティブなサブシーンの番号を格納し、下のスクリプトで `r subid` として使用しています。 knitr はドキュメントを処理する際にその値を代入します。

rotate() 関数は、Javascript の関数 document.getElementById を使って、シーンを含むWebページの <div> コンポーネントを取得します。 その中には rglinstance という名前のコンポーネントが含まれており、修正可能なシーンの情報が含まれています。:

<script type="text/javascript">
var rotate = function(angle) {
  var rgl = document.getElementById("plot3drgl2").rglinstance;
  rgl.getObj(`r subid`).par3d.userMatrix.rotate(angle, 0,1,0);
  rgl.drawScene();
};
</script>

もし、chunk のヘッダに webGL=TRUE を使っていたら、knitr の WebGL サポートは、<chunkname>rgl という形式の名前を持つグローバルオブジェクトを作成します。 例えば、コードチャンクの名前が plot3d2 であれば、オブジェクトの名前は plot3d2rgl となり、このコードは動作します。:

<script type="text/javascript">
var rotate = function(angle) {
  plot3d2rgl.getObj(`r subid`).par3d.userMatrix.rotate(angle, 0,1,0);
  plot3d2rgl.drawScene();
};
</script>

Index

このドキュメントでは以下の機能について説明しています。:

ageControl   getWidgetId   propertyControl   subsetControl  
asRow   par3dinterpControl   rglMouse   toggleWidget  
clipplaneControl   playwidget   rglShared   vertexControl