How to increase smoothness of spheres3d in rgl
Expanding on cuttlefish44's excellent answer, I found a parameterization that works better - i.e. it has no defect at the poles (the black artifact on the lightblue sphere in the image).
library(rgl)
sphere.f <- function(x0 = 0, y0 = 0, z0 = 0, r = 1, n = 101, ...){
f <- function(s, t) cbind(r * cos(s) * cos(t) + x0,
r * sin(s) * cos(t) + y0,
r * sin(t) + z0)
persp3d(f, slim = c(0, pi), tlim = c(0, 2*pi), n = n, add = T, ...)
}
sphere1.f <- function(x0 = 0, y0 = 0, z0 = 0, r = 1, n = 101, ...){
f <- function(s,t){
cbind( r * cos(t)*cos(s) + x0,
r * sin(s) + y0,
r * sin(t)*cos(s) + z0)
}
persp3d(f, slim = c(-pi/2,pi/2), tlim = c(0, 2*pi), n = n, add = T, ...)
}
sphere.f( -1.5,0, col = "lightblue")
sphere1.f( 1.5,0, col = "pink")
The image:
R+RGL: plot of spheres and segments, segments do not zoom properly
The previous answer https://stackoverflow.com/a/71379091/2554330 comes very close to solving the problem, but there are some minor issues:
Some of the links between the spheres are somewhat flat, because specifying the
e2
argument incylinder3d
means the rotationally symmetric cross section is not perpendicular to the cylinder. Leaving it out fixes this.You can see the facets on the cylinders (which are 6 sided by default). Since these are supposed to be interpreted as lines which resize with the scene, suppressing the lighting using the
lit = FALSE
material property makes them look more like fat lines.The
sphere1.f
function has a noticeable seam in it where the edges of the curved surface join, becausepersp3d
estimates the normals using interior points. Specifying the normals explicitly fixes this. They are specified with a function likef
, but giving unit normals to the surface, i.e.sphere1.f <- function(x0 = 0, y0 = 0, z0 = 0, r = 1, n = 101, ...){
f <- function(s,t){
cbind( r * cos(t)*cos(s) + x0,
r * sin(s) + y0,
r * sin(t)*cos(s) + z0)
}
g <- function(s,t){
cbind( cos(t)*cos(s),
sin(s),
sin(t)*cos(s))
}
persp3d(f, slim = c(-pi/2,pi/2), tlim = c(0, 2*pi), n = n, add = T, normal = g, ...)
}Each of the spheres drawn by
sphere1.f
has 101^2 vertices.rgl
can handle this, but it is fairly inefficient. Since they are all identical, thesprites3d
function can be used to replicate a single sphere at all the different locations. The appropriate code to do this would be## plot the spheres
agg %>%
rowwise() %>%
mutate(x = X1, y = X2, z = X3) %>%
sprites3d(shapes = sphere1.f(r = 0.5))
where a single sphere centred at (0, 0, 0) is redrawn at each of the computed locations. This looks the same as the original in R, but will make the output from rglwidget()
much smaller. (I notice there seems to be a bug in the lighting code, so the shading looks wrong in rglwidget()
with
the specified lights. Commenting out the lighting code fixes it, but that shouldn't be necessary.)
Dply and RGL: replace sapply
Here are couple of dplyr
/tidyverse
approaches :
Use pmap_dbl
:
library(dplyr)
library(purrr)
spheres %>%
mutate(spheres = pmap_dbl(., ~sphere1.f(..1, ..2, ..3)))
Or with rowwise
:
spheres %>%
rowwise() %>%
mutate(spheres = sphere1.f(x, y, z))
Draw only positive octant with rgl.sphere in R
You can use cliplanes3d()
to do that. You should also avoid using any of the rgl.*
functions; use the *3d
alternatives instead unless you really know what you're doing. It's almost never a good idea to mix the two types.
For example:
# Fake data
norm_vec <- function(x) sqrt(sum(x ^ 2))
data <- data.frame(T3 = runif(100), T6 = runif(100), P4 = runif(100))
norms <- apply(data, 1, norm_vec)
data <- data / norms
cluster <- sample(1:6, 100, replace = T)
#' Initialize a rgl device
#'
#' @param new.device a logical value. If TRUE, creates a new device
#' @param bg the background color of the device
#' @param width the width of the device
rgl_init <- function(new.device = FALSE, bg = "white", width = 640) {
if( new.device || rgl.cur() == 0 ) {
open3d(windowRect = 50 + c( 0, 0, width, width ) )
bg3d(color = bg )
}
clear3d(type = c("shapes", "bboxdeco"))
view3d(theta = 30, phi = 0, zoom = 0.90)
}
#' Get colors for the different levels of a factor variable
#'
#' @param groups a factor variable containing the groups of observations
#' @param colors a vector containing the names of the default colors to be used
get_colors <- function(groups, group.col = palette()){
groups <- as.factor(groups)
ngrps <- length(levels(groups))
if(ngrps > length(group.col))
group.col <- rep(group.col, ngrps)
color <- group.col[as.numeric(groups)]
names(color) <- as.vector(groups)
return(color)
}
# Setting colors according to the cluster column
my_cols <- get_colors(cluster, c("#56B4E9", "#009E73", "#F0E442", "#0072B2", "#D55E00", "#CC79A7"))
# Ploting sphere
rgl_init()
par3d(cex = 1.35)
plot3d(x = data[, "T3"], y = data[, "P4"], z = data[, "T6"],
type = "s", r = .04,
col = my_cols,
xlab = 'T3', ylab = 'P4', zlab = 'T6')
spheres3d(0, 0, 0, radius = 0.995, col = 'lightgray', alpha = 0.6, back = 'lines')
arc3d(c(1, 0, 0), c(0, 1, 0), c(0, 0, 0), radius = 1, lwd = 7.5, col = "black")
arc3d(c(1, 0, 0), c(0, 0, 1), c(0, 0, 0), radius = 1, lwd = 7.5, col = "black")
arc3d(c(0, 0, 1), c(0, 1, 0), c(0, 0, 0), radius = 1, lwd = 7.5, col = "black")
bbox3d(col = c("black", "black"),
xat = c(0, 0.5, 1), yat = c(0, 0.5, 1), zat = c(0, 0.5, 1),
polygon_offset = 1)
aspect3d(1, 1, 1)
clipplanes3d(c(1,0,0), c(0,1,0), c(0,0,1), d=0)
This produces
How to draw parametric 3d curve with smoothing in R?
You can use spline
to interpolate between your points and smooth the curve.
d <- read.delim(textConnection(
"t x y z
0.000 3.734 2.518 -0.134
0.507 2.604 9.059 0.919
0.861 1.532 11.584 -0.248
1.314 1.015 1.886 -0.325
1.684 2.815 4.596 3.275
1.938 1.359 8.015 2.873
2.391 1.359 8.015 2.873"
), sep=" ")
ts <- seq( from = min(d$t), max(d$t), length=100 )
d2 <- apply( d[,-1], 2, function(u) spline( d$t, u, xout = ts )$y )
library(scatterplot3d)
p <- scatterplot3d(d2, type="l", lwd=3)
p$points3d( d[,-1], type="h" )
As per @Spacedman's comment, you could also use rgl
:
this allows you to interactively rotate the scene.
library(rgl)
plot3d( d2, type="l", lwd=5, col="navy" )
points3d(d[,-1])
spheres3d(d[,-1], radius=.1, col="orange")
segments3d( matrix( t( cbind( d[,-1], d[,2:3], 0 ) ), nc=3, byrow=TRUE ) )
planes3d(0,0,1,0, col="yellow", alpha=.5) # Plane z=0
Related Topics
Connect Ggplot Boxplots Using Lines and Multiple Factor
Order X Axis Day Values in Ggplot2
How to Minimize Size of Object of Class "Lm" Without Compromising It Being Passed to Predict()
The Representation of an Empty Argument in a "Call"
In R, Check If String Appears in Row of Dataframe (In Any Column)
Draw Histograms Per Row Over Multiple Columns in R
R Predict Function Returning Too Many Values
Get Data Frame from Character Variable
Flattening a Delimited Composite Column
Importing Data into R (Rdata) from Github
Plot Line and Bar Graph (With Secondary Axis for Line Graph) Using Ggplot
Group Vector on Conditional Sum
Extract Data Between a Pattern from a Text File
Subset Dataframe Based on Posixct Date and Time Greater Than Datetime Using Dplyr
How to Calculate a Table of Pairwise Counts from Long-Form Data Frame
Warning "The Condition Has Length > 1 and Only the First Element Will Be Used"