The surface3d function in package:rgl looks like a good match. It would be very simple to create a wrapper that would take your function, create an x-y set of vectors with seq() and then pass those vectors to outer with your f2 as the FUN argument, and then call surface3d.
There is also a persp3d which the authors (Duncan Murdoch and perhaps others) say is "higher level" and it does appear to add axes by default which surface3d does not.
curve_3d <- function(f2, x_range=c(-1, 1), y_range=c(-1, 1), col=1:6 ){ 
       if (!require(rgl) ) {stop("load rgl")}
       xvec <- seq(x_range[1], x_range[2], len=15)
        yvec <- seq(y_range[1], y_range[2], len=15)
       fz <- outer(xvec, yvec, FUN=f2)
       open3d()
       persp3d( xvec, yvec, fz, col=col) }
curve_3d(f2)
snapshot3d("out3dplane.png")

Now that I think about it further, you could have done something similar with persp() or wireframe(). The "trick" is using outer(..., FUN=fun). And as I think about it even further ... the ability to use it with outer depends on it being composed of all vectorized operations. If they were not vectorized, we would need to rewrite with Vectorize or mapply.