This document describes how to embed rgl scenes in HTML documents and use embedded Javascript to control a WebGL display in an HTML document. For more general information, see rgl Overview.
We assume that the HTML document is produced from R markdown source using knitr or rmarkdown. This format mixes text with Markdown markup with chunks of R code.
There are two ways to embed an rgl scene in the document. The older one is to use the chunk option webgl = TRUE. With that option, whatever rgl scene is active at the end of the chunk will be embedded. See the setupKnitr help page.
The second way is to use a call to rglwidget. Each call to this function will insert a scene into the document. Do not set webgl = TRUE.
The second method is easier for me to maintain, so it is likely to receive more support in the future, but for now both methods are supported, and there are examples of both in this document.
I am currently conducting experiments on a third method. This is intended to be similar to the way standard 2D graphics are included by knitr, i.e. it will detect the fact that you’ve drawn something, and just include it automatically.
Most browsers now support WebGL, but it may be disabled by default. See http://get.webgl.org for help on a number of different browsers.
If you are using the internal browser in RStudio, support varies by version. I believe it is enabled by default in Windows versions, but until recently was not enabled in Mac OSX versions. You can run this command in Terminal:
defaults write org.rstudio.RStudio WebKitWebGLEnabled -bool YES
to enable it. I do not have much experience with RStudio in Linux, but it does seem that WebGL is enabled there.
We start with two simple examples. The next section gives reference information.
Consider the simple plot of the iris data. We insert a code chunk and call the rglwidget function with optional argument elementId. This allows later Javascript code to refer to the image.
library(rgl)
with(iris, plot3d(Sepal.Length, Sepal.Width, Petal.Length,
type="s", col=as.numeric(Species)))
subid <- currentSubscene3d()
rglwidget(elementId="plot3drgl")<button type="button" onclick="rotate(10)">Forward</button>
<button type="button" onclick="rotate(-10)">Backward</button>
which produces these buttons:
We stored the subscene number that is currently active in subid in the code chunk above, and use it as in the script below. `r subid`knitr substitutes the value 1 when it processes the document.
The rotate() function uses the Javascript function document.getElementById to retrieve the <div> component of the web page containing the scene. It will have a component named rglinstance which contains information about the scene that we can modify:
<script type="text/javascript">
var rotate = function(angle) {
var rgl = document.getElementById("plot3drgl").rglinstance;
rgl.getObj(`r subid`).par3d.userMatrix.rotate(angle, 0,1,0);
rgl.drawScene();
};
</script>
If we had used webGL=TRUE in the chunk header, the knitr WebGL support would create a global object with a name of the form <chunkname>rgl. For example, if the code chunk was named plot3d, the object would be called plot3drgl, and this code would work:
<script type="text/javascript">
var rotate = function(angle) {
plot3drgl.getObj(`r subid`).par3d.userMatrix.rotate(angle, 0,1,0);
plot3drgl.drawScene();
};
</script>
We can also change the contents of the plot using toggleButton. For example, we can redo the previous plot, but with the three species as separate “spheres” objects and buttons to toggle them:
sphereid <- with(subset(iris, Species == "setosa"),
spheres3d(Sepal.Length, Sepal.Width, Petal.Length,
col=as.numeric(Species),
radius = 0.211))
with(subset(iris, Species == "versicolor"),
spheres3d(Sepal.Length, Sepal.Width, Petal.Length,
col=as.numeric(Species),
radius = 0.211))
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()
subid <- currentSubscene3d()You must enable Javascript to view this page properly.
toggleButton(sphereid, label = "setosa", prefix = "toggle", subscene = subid)toggleButton(sphereid+1, label = "versicolor", prefix = "toggle", subscene = subid)toggleButton(sphereid+2, label = "virginica", prefix = "toggle", subscene = subid)Note that we need to use results="asis" for the button code.
Normally we would also use echo=FALSE, though I didn’t do so above; then the buttons will end up side-by-side. We also add another button to toggle the axes:
An alternate control to achieve the same thing is subsetSlider. Here we also illustrate the elementId2Prefix bridge to allow an rglwidget to be controlled by the old-style slider.
rglwidget(elementId = "slider")elementId2Prefix("slider")subsetSlider(subsets = list(setosa = sphereid,
versicolor = sphereid + 1,
virginica = sphereid + 2,
all = sphereid + 0:2),
prefixes = "slider", subscenes = subid,
init = 3)There are several other functions to generate the Javascript code for controls. par3dinterpSetter generates a function that approximates the result of par3dinterp. propertySetter is a more general function to set the value of properties of the scene. Both generate Javascript functions, but not the controls to use them; for that, use propertySlider or your own custom code.
play3d example) rotates the scene in a complex way.
You must enable Javascript to view this page properly.
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) ) )
propertySlider(setter = par3dinterpSetter(fn, 0, 1.5, steps=15,
prefix = "userMatrix",
subscene = subid),
step = 0.01)Some things to note: The generated Javascript slider has 150 increments, so that motion appears smooth. However, storing 150 userMatrix values would take up a lot of space, so we use interpolation in the Javascript code. However, the Javascript code can only do linear interpolation, not the more complex spline-based SO(3) interpolation done by par3dinterp. Because of this, we need to output 15 steps from par3dinterpSetter so that the distortions of linear interpolation are not visible.
Another function that auto-generates Javascript code is clipplaneSlider. This function allows the user to control the location of a clipping plane by moving a slider. Both it and par3dinterpSetter are implemented using the more general propertySlider, which allows control of multiple objects in multiple scenes, but which does require knowledge of the internal representation of the scene in its Javascript implementation.
Less general than propertySetter is vertexSetter. This function sets attributes of individual vertices in a scene. For example, to set the x-coordinate of the closest point in the setosa group, and modify its colour from black to white,