I've given almost all the images in this tutorial a visible indication of the object that the isosurface is contained_by. This is particularly useful in those images where the isosurface touches the container, so you can clearly see which features are caused by intersections with the container and which are a natural feature of the isosurface.
Hint I find it useful to add such visualisations of the container when developing surfaces. It helps avoid confusion that may arise if the isosurface accidentally touches the container. It also helps me spot situations in which the container is excessively large, wasting lots of rendering time. I comment out the sphere {0,R pigment {rgbt <1,0,0,0.9>}} once I'm happy with the behaviour of the isosurface.
The source files for the examples on each page are available as ZIP files.
The tutorial is split into the following sections:
In order to get a feel for what isosurfaces are, it might be useful to think about something that you might be more familiar with, the isobars that are drawn on weather maps.
Weather stations gather information about the air pressure at different points on the ground, and then the meteorologists draw lines through the points where the numbers are the same.
As well as isobars, which connect points of equal pressure, there are
isotherms for temperature, isoclines for inclination, isogonics for
magnetic declination etc. They are all curves that join points where
some numeric value is the same.
The first image shows a ground level with numerical values
at various points.
In second image there are lines joining points where the numerical values are 20, 30, 40, 50 and 60.
In the third image, we remove the limitation that the numerical values
are only on the ground. Suppose there are numerical values (not shown)
at every point in space. We can now join points that have the same numerical value
with 3d surfaces. In this case the surface shown connects all the points
where the numerical value is 30.
Rather than using external numbers that represent something real, like air pressure, the numerical values that are used to generate POVRay isosurfaces come from mathematical functions. A function is simply a formula that generates a numerical value for points in 3d space. For example if we take function { x*x + y*y + z*z - 4 }, then POVRay can calculate the value at a point like <1,2,0> by plugging those x,y,z values into the formula 1*1 + 2*2 + 0*0 - 4 giving a value of 1.
By carefully choosing the mathematical formula, we can make the associated isosurface
take up a vast range of shapes that would be difficult to achieve otherwise.
Up |
Next: Simple surfaces
| Alphabetical Index
The syntax used for the isosurfaces used on this page is like
isosurface { function { x*2 + y*2 + z*2 - R*R } accuracy 0.001 max_gradient 4 contained_by{sphere{0,1.2}} pigment {rgb .9} finish {phong 0.5 phong_size 10} } sphere {0,1.2 pigment {rgbt <1,0,0,0.9>}}
Maths: Take a look at what happens on the x, y, and z planes.
On the x plane, x=0, the equation reduces to y*y + z*z - R*R = 0
the two dimensional equation of a circle radius R. Similarly the intersection with the y and z planes are
also circles. This probably doesn't come as much of a surprise, but if you
get a feel for how the 2D equations come together to make the 3D equation
you can sometimes get a feel for how to build other 3D surfaces.
Maths: The cross section of this object on any x plane is clearly always the circle
y*y + z*z - R*R = 0.
Maths: Well, functions don't come much simpler than that. The isosurface exists wherever y takes the value zero.
This is the y plane. The x plane and z plane are simply function {x} and function {z}.
The plane through the point <0,1,0> is given mathematically by y = 1
so the function (with threshold zero) is therefore function { y - 1 }.
Maths: What I've done here is to take the double-plane function { y*y - 1} and intersect it with similar x and z double planes. max() performs intersections of isosurfaces.
function { y*y - 1} is mathematically y^2 -1 = 0 which
has the two planar solutions y = +1 and y = -1, so the function describes the two planes.
Maths: Observe that the intersection with the y plane is a circle x^2 + z^2 - 1 = 0
and the intersections with the x and z planes are parabolas y = x^2 + 1.
Maths: Observe that the intersection with the y plane is a hyperbola x^2 - z^2 = 0
and the intersections with the x and z planes are parabolas y = -x^2.
Here's a very simple example where nasty holes appear in the surface:-
isosurface { function { x*x + y*y + z*z - 1 } accuracy 0.0001 contained_by{sphere{0,1.2}} pigment {rgb .9} finish {phong 0.5 phong_size 10} }
That's just the Sphere from the Simple Surfaces page, but with the max_gradient 2.4 missing.
What happens is that the function evaluator guesses where each ray hits the surface and then improves the guess by walking up and down the ray. If max_gradient is set higher then the evaluator will take a larger number of small steps. If max_gradient is set too low, then the evaluator will take fewer large steps and may step right past the intersection without noticing.
If you're mathematically inclined, then it is possible to calculate the actual maximum gradient. In the case of this sphere, we happen to know that the function is symmetrical so we can go looking for the maximum gradient in any direction. Let's choose to look along the x axis. Along the x axis the value of y and z are always zero, so the function simplifies to x*x -1. If we differentiate that we obtain the gradient as 2*x. Within contained_by{sphere{0,1.2}}, 2*x ranges from -2.4 to +2.4, so we should add max_gradient 2.4.
This mathematical method is rather tedious, and we'll soon be meeting isosurface functions that we don't know how to differentiate, so we need to find another way to find the max_gradient. We could just guess, and keep trying higher values until the nasty holes disappear, or we could use the evaluate keyword.
In practice, I usually guess something like max_gradient 2 and render a small image and look at the Messages pane. There will be a warning in the Messages if POVRay considers the max_gradient to be incorrect, e.g.
Warning: The maximum gradient found was 4.734, but max_gradient of the isosurface was set to 7.000. Adjust max_gradient to get a faster rendering of the isosurface.
Some functions contain singularities: points where the gradient becomes infinite.
The message will say that the maximum gradient found was some large number like 518238.857.
In most such cases you can get away with using a considerably lower value if you are prepared
to accept that the regions extremely close to the singularity may not render correctly.
Download a zip file containing the POV source files
for all the images that appear on this page.
Differences© Mike Williams 2002,2003,2004 |
Up | Previous: POV-Ray Syntax Subtleties | Next: Variable Substitution | Alphabetical Index |
Up | Previous: POV-Ray Syntax Subtleties | Next: Variable Substitution | Alphabetical Index |
#declare S = function {x*x + y*y + z*z - 1} isosurface { function { S(x,y,z) } accuracy 0.001 contained_by{sphere{0,R}} pigment {rgb .9} }In this case, we use the same actual parameters as the formal parameters, and the result is a sphere, exactly as if we had said
isosurface { function {x*x + y*y + z*z - 1} ... }
isosurface { function {(x/2)*(x/2) + y*y + z*z - 1} ... }We could have written that in the first place, or we could have applied a conventional scale <2,1,1> transformation to the whole thing. Note: Applying scale would also scale the contained_by surface.
Note that to scale the x dimension by a factor of 2, we substituted x/2.
This is a common feature of substitutions - things change in an inverse
way to the change made to the variable. If the variable is halved then the scale doubles.
function { S(x, 0, z) }
Substituting 0 for y causes the
sphere to degenerate into a cylinder.
You might consider this to be equivalent of an infinite scaling in the y direction.
Substituting y-0.5 for y causes the
surface to be translated +0.5 in the y direction. We could have
achieved this effect by translate <0, 0.5, 0>.
We first create a shear transformation, then declare a transformation function using its inverse.
#include "transforms.inc" #declare TR=Shear_Trans(<-0.5,0.5,0.5>,<1.1,0,-0.3>,<-0.3,0.5,0>) #declare TRFI = function {transform {TR inverse}}Now we can apply that transformation function to the unit vectors which gives us the values we need for the substitution. However, we can't use vectors from inside an isosurface function, so we have to extract the x, y and z values of each of those vectors outside the isosurface.
#declare A=TRFI(1,0,0); #declare B=TRFI(0,1,0); #declare C=TRFI(0,0,1); #declare Ax=A.x;#declare Bx=B.x;#declare Cx=C.x; #declare Ay=A.y;#declare By=B.y;#declare Cy=C.y; #declare Az=A.z;#declare Bz=B.z;#declare Cz=C.z;We can now use these scalar values in the variable substitution.
function { F(Ax*x+Bx*y+Cx*z, Ay*x+By*y+Cy*z, Az*x+Bz*y+Cz*z) }The image on the left shows two boxes. One of them is sheared by variable substitution and the other is sheared with the equivalent conventional shear transform.
The same method can be used for combinations of shear, rotation and scale operations.
It doesn't seem to work for transformations that involve translation, but we already know how to do that.
#declare P = function {x*x + y + z*z - 1} isosurface { function { P(y,-x,z) } ...Substituting y for x and substituting -x for y causes this paraboloid to be flipped round to face in the -x direction instead of the +y direction.
#declare P = function {x*x + y*y + z*z - 1} isosurface { function { P(x,y*(1.05-y/5),z) } ...A non-linear stretch has turned this sphere into something like a hen's egg. The sphere is stretched more as y becomes larger, and compressed more as y becomes more negative.
In this case the function F describes the sin wave "y = sin(x) + 4", the "+4" is there just to make the value of y always positive.
#declare F = function {y-sin(x)-4} isosurface { function { F(x, sqrt(y*y+z*z), z) } ...
A particular instance of a surface of revolution is the torus. The parameters r1 and r2 are the major and minor radii.
The original function {x*x + y*y - r2*r2} creates a 2d circle of radius r2, and the substitution shifts that circle a distance r1 along the x axis, then creates a surface of revolution from it.
#declare F = function {x*x + y*y - r2*r2} isosurface { function { F(sqrt(x*x+z*z)-r1, y, z) } ...
The cylindical polar coordinates are made available through the f_th() and f_r() functions, so you don't have to work them out for yourself.
#declare F=function { f_helix1 (x, z, y, Strands, Turns, R1, R2, 0.6, 2, 0) } isosurface { function{F(f_r(x,y,z)-R3, y, f_th(x,y,z))} ...
The spherical polar coordinates are made available through the f_th(), f_r() and f_ph() functions, so you don't have to work them out for yourself.
The image on the left is created from a f_mesh1() function which has been converted to polar coordinates. The threads that previously lay in the x and z directions now lie in the longitude and latitude directions of the sphere. What was previously the height in the y direction is now altitude in the radial direction, with the "-1" lifting the whole thing radially away from the origin.
#declare F=function {f_mesh1 (x, y, z, 0.15, 0.15, 1, 0.02, 1) - 0.03} isosurface { function { F(f_ph(x,y,z), f_r(x,y,z)-1, f_th(x,y,z)) } ...
Using mod(x,2) will cause the shape to be repeated in the x direction every 2 units. If your original isosurface created a shape centred at the origin, then there's a problem with the way that it chooses which bits to repeat. The left half of the object gets repeated in one direction and the right half of the object gets repeated in the other direction. To fix this, we'd like to substitute mod(x,2)+1 where x is negative and mod(x,2)-1 where x is positive. To fix both directions at once, for a symmetrical object, we can use abs(x) like this mod(abs(x),2)-1.
To change the length of the repeat unit we can do this mod(abs(x),Step)-Step/2.
We can repeat things in more than one direction, making a sheet or filling a volume with repeated units by performing similar substitutions in the x, y and z directions.
This trick can be extremely useful in situations where awkward CSG operations cause POV to apply very inefficient bounding. If we had created thousands of separate badly bounded objects, then POV would have to perform thousands of intersection tests on each ray. If we use this trick to use a single isosurface to generate thousands of objects, then POV only has to perform a single intersection test on each ray. That intersection test may well be somewhat slower than that for a non repeating surface, but it's likely to be much faster than performing a thousand such tests.
You need to be particularly careful with your max_gradient setting for this trick.
Slight changes to the step size sometimes require large changes in max_gradient.
#declare F = function {f_torus(y,x,z,0.8,0.19)} #declare Step = 0.75; isosurface { function { F ( mod(abs(x),Step)-Step/2, y, z) }
#declare F = function {y +f_noise3d(x*2, 0, z*2) } isosurface { function { abs(F(x,y,z))-0.1 } ...
#declare S = function {x*x + y*y +z*z - 1} isosurface { function { min(S(x+0.5,y,z), S(x-0.5,y,z)) } max_gradient 2 contained_by{sphere{0,R}} pigment {rgb .9} }min() can be used to produce the union of two or more functions. In this case the union of two spheres, one translated -0.5 in the x direction and one translated +0.5 in the x direction.
#declare S = function {x*x + y*y +z*z - 1} isosurface { function { max(S(x+0.5,y,z),S(x-0.5,y,z)) } max_gradient 5 contained_by{sphere{0,R}} pigment {rgb .9} }max() can be used to produce the intersection of two or more functions. In this case the same two spheres as above.
#include "functions.inc" #declare S = function {x*x + y*y + z*z - 1} isosurface { function { S(x,y,z) + f_noise3d(x*10, y*10, z*10)*0.3 } max_gradient 7 contained_by{sphere{0,R}} pigment {rgb .9} }
Adding two functions together produces a sort of blend between the two functions. A particular use of such a blend is to add one or more noise or noise-like functions to a surface.
In this case I've added a bit of f_noise3d to a sphere. The result of the addition is a surface with a generally spherical shape but with a noisy surface. I've used variable substitution to control the frequency of the noise.
Adding noise creates a surface that is slightly smaller than the original sphere -
the noise pokes inwards from the surface. Subtracting noise creates a surface that
is slightly larger than the original sphere - the noise stands slightly proud
of the surface.
#include "functions.inc" #declare S = function {f_sphere(x,y,z,0.5)} #declare T = function {f_torus(x,y,z,1,0.2)} isosurface { function { S(x-0.7,y,z) * T(x,y,z) - 0.05} max_gradient 2 accuracy 0.001 contained_by{sphere{0,R}} pigment {rgb .9} }
Multiplying surfaces then adding a small constant produces an effect
that's similar to using blobs. However, this technique can be used
to blob together any kind of isosurface, not just spheres and cylinders.
Download a zip file containing the POV source files
for all the images that appear on this page.
It's possible to use the components of these colours as an isosurface function.
If we choose the "red" component, then the surface is considered to exist
wherever the red component of the pigment is equal to the isosurface threshold.
#declare F=function{pigment{ crackle turbulence 0.1 color_map { [0 rgb 1] [1 rgb 0] } scale 0.5 } } isosurface { function { F(x,y,z).red - 0.5 } max_gradient 5.5 contained_by{box{-1,1}} pigment {rgb .9} }
On its own a pigment function doesn't usually look like much of anything.
#declare F=function{pigment{ crackle turbulence 0.1 color_map { [0 rgb 1] [1 rgb 0] } scale 0.5 } } isosurface { function { x*x + y*y +z*z -1 + F(x,y,z).grey*0.3 } max_gradient 3.5 contained_by{sphere{0,R}} pigment {rgb .9} }But when we add or subtract a pigment function to a shape like a sphere it's possible to produce interesting effects. This is the same pigment function as above, but this time 0.3 of it is added to a sphere instead of just the pigment being allowed to fill the contained_by object.
#declare F=function{pigment{ mandel 50 color_map { [0 rgb 0] [1 rgb 1] } scale 8 translate <-3,0,0> } } isosurface { function { y - F(x,z,y).grey*0.3 } contained_by{sphere{0,R}} open pigment {rgb .9} }Pigment isosurfaces can be useful for landscapes. This canyon is made from a Mandelbrot pigment added to the y plane.
Because the Mandelbrot pigment faces the z direction, I flipped the axes by using F(x,z,y) instead of F(x,y,z).
The Mandelbrot pigment has, by default, a rather strange colour map
which isn't suitable for our purposes, so color_map { [0 rgb 0] [1 rgb 1] }
is used to disable the default colour map.
These pigment isosurfaces can be used to give something like "greebles"
and might make a nice surface for a spaceship hull.
This version uses the crackle pigment with metric 0 and solid modifiers.
#declare P=pigment{ crackle metric 0 solid color_map { [0 rgb 1] [1 rgb 0.5] } scale 0.1 } #declare F = function{pigment {P}} function { x*x + y*y +z*z -1 +F(x,y,z).red*0.2 }
#declare P=pigment{ cells color_map { [0 rgb 1] [1 rgb 0.5] } scale 0.1 } #declare F = function{pigment {P}} function { x*x + y*y +z*z -1 +F(x,y,z).red*0.1 }This version produces a similar effect using the cells pattern, and runs very much faster.
You don't always have to stick with simple colour maps like colour_map {[0 rgb 0] [1 rgb 1]}.
These two surfaces were made with the leopard pigment by changing the colour map.
For the first one, the insertion of [0.3 rgb 0] holds the pigment value at zero for a while, creating a flat zone from which the bumps rise sharply.
I've used P(x,0,z) instead of P(x,y,z)
to prevent changes in the function values
above the plane which would otherwise cause blobs of surface to become detached.
#declare P=function{pigment{leopard colour_map{[0.0 rgb 0.0] [0.3 rgb 0.0] [1.0 rgb 1.0]} scale 0.1 }} function { y - P(x,0,z).red*0.4}In the second one, I've changed the zero entry of the colour map to push up what would normally be the lowest part of the surface.
#declare P=function{pigment{leopard colour_map{[0.0 rgb 0.3] [0.1 rgb 0.0] [1.0 rgb 1.0]} scale 0.15 rotate y*45 }} function { y - P(x,0,z).red*0.4}
It turns out that we are allowed to create patterns from functions and use
them for pigments or normals. In this way it's possible to create a pigment
that obeys any mathematical equation we like.
sphere {0,1 pigment { function {sin(x*10) + 10*y} color_map { [0.0 rgb <1,0,0>] [0.5 rgb <1,1,0>] [1.0 rgb <1,0,0>] } } }
sphere {0,1 pigment { function {sin(x*30) + sin(y*30)} color_map { [0.0 rgb <1,0,0>] [0.2 rgb <0,0,1>] [0.5 rgb <1,1,0>] [0.7 rgb <0,1,0>] [1.0 rgb <1,0,0>] } } }
box {-1,1 pigment { function {x*x + y*y +z*z} color_map { [0.0 rgb <1,0,0>] [0.5 rgb <1,1,0>] [1.0 rgb <1,0,0>] } } }
box {-1,1 pigment { function {(x*x + z*z)*3} color_map { [0.0 rgb <1,0,0>] [0.5 rgb <1,1,0>] [1.0 rgb <1,0,0>] } } }
sphere {0,1 pigment { function { sin(x*20)/sin(y*20)*0.7} color_map { [0.0 rgb <1,0,0>] [0.5 rgb <1,1,0>] [1.0 rgb <1,0,0>] } } }
sphere {0,1 pigment { function { atan2(z,x)*2 } color_map { [0.0 rgb <1,0,0>] [0.5 rgb <1,1,0>] [1.0 rgb <1,0,0>] } } }It's even possible to start out with pigment patterns, convert them into functions, perform mathematical operations on those functions that are not possible with pigments, then convert them back into pigments again.
#declare F1 = function{pigment{onion scale 0.5}} #declare F2 = function{pigment{leopard scale 0.1}} box {-1,1 pigment { function {F1(x,y,z).grey + F2(x,y,z).grey*0.5} color_map { [0.0 rgb <1,0,0>] [0.5 rgb <1,1,0>] [1.0 rgb <1,0,0>] } } }In this case, I've made functions F1 and F2 from the onion and leopard pigment patterns and simply added them together. If you use certain built in functions to generate pigments you may find some areas (like alternate corners of the following cube) where there are patches of plain colour. This isn't a natural feature of the function, but an artefact of the library code.
These functions set a bound on the maximum numerical value that can be returned. This value is usually 10.0 but a few surfaces allow the value to be passed as a parameter. If there's a point where the function calculates a value greater than 10.0, the library returns the value 10.0, which for the purposes of calculating a pigment is equivalent to 0.0
I've made this more apparent in this image by mapping values that are very close to 0.0 to white.
Where the function values vary the white stripe is so thin that it gets anti-aliassed into invisibility,
but in areas where the function would exceed 10.0 there is plain whiteness. There are 10 yellow bands visible
in this image, corresponding to regions where the Steiner's Roman function takes the values 0.5 to 9.5.
#include "functions.inc" box {-0.5, 0.5 pigment { function { f_steiners_roman(x,y,z,90) } color_map { [0.0 rgb <1,1,1>] [0.001 rgb <1,0,0>] [0.5 rgb <1,1,0>] [0.999 rgb <1,0,0>] [1.0 rgb <1,1,1>] } } }
Sometimes it is difficult, or even impossible, to specify a single equation in x, y and z that specifies the surface but a set of parametric equations might be available.
In the 2d case, we know that x*x + y*y - r*r = 0 is the equation of a circle, but we can also describe it by the two parametric equations x = cos(theta), y = sin(theta). Each value of theta gives a value for x and y, i.e. a 2d point. As theta varies from 0 to 2pi the point goes round the unit circle.
In the 3d case we need three parametric equations, one each for x, y and z; and we need two parameters which we will call u and v. If we consider the equations
x = sin(u) y = cos(u) z = vwe can see that each pair of values for u and v gives a single xyz point in 3d space. As u varies from 0 to 2pi, the point goes round a circle. As v varies from -2 to 2 the point moves parallel to the z axis. It turns out that these are the parametric equations for a cylinder.
The three parametric functions are listed; then the u,v bounds; Then the contained_by object.
The u,v bounds used here specify that {u,v} varies from {0,-2} to {2*pi, 2} i.e. u varies from 0 to 2*pi and v varies from -2 to 2.
The "contained_by" object can be a sphere or a box.
The "isosurface", "evaluate", "max_trace", "threshold", "open" and "closed" keywords are not used.
parametric { function {sin(u)} function {cos(u)} function {v} <0,-2>,<2*pi,2> contained_by{box{<-2,-2,-2>,<2,2,2>}} pigment {rgb 0.9} finish {phong 0.5 phong_size 10} }
parametric { function {u*v*sin(15*v)} function {v} function {u*v*cos(15*v)} <0,-1>,<1,1> max_gradient 4 contained_by{box{<-R,-R,-R>,<R,R,R>}}
The "precompute" keyword can speed up the rendering by telling the renderer to store some calculations in an array, thus trading memory and parsing time against rendering time. I find that "precompute 18, x,y,z" tends to give a reasonable speed improvement on my machine. Higher values cause the parse time to become rather long. In this case, there's no speed gain from precomputing y, but there's not much of a penalty either.
#declare F1 = function {u*v*sin(15*v)} #declare F2 = function {u*v*cos(15*v)} parametric { function {F1(u,v,0)} function {v} function {F2(u,v,0)} <0,-1>,<1,1> contained_by{box{<-R,-R,-R>,<R,R,R>}} precompute 18, x,y,z accuracy 0.003 pigment {rgb 0.9} finish {phong 0.5 phong_size 10} }
#declare Fx = function {u*v*sin(15*v)} #declare Fy = function {v} #declare Fz = function {u*v*cos(15*v)} #declare Fp = function {pigment {granite scale 0.1}} parametric { function {Fx(u,v,0)} function {Fy(u,v,0) + Fp(u,v,0).grey*0.2} function {Fz(u,v,0)} <0,-1>,<1,1> contained_by{box{<-R,-R,-R>,<R,R,R>}} precompute 18, x,y,z pigment {rgb 0.9} finish {phong 0.5 phong_size 10} no_shadow }
#declare A=1; #declare B=1; #declare C=1; #declare Fx = function {pow(A*cos(u)*cos(v),3)} #declare Fy = function {pow(B*sin(u)*cos(v),3)} #declare Fz = function {pow(C*sin(v),3)} parametric { function {Fx(u,v,0)} function {Fy(u,v,0)} function {Fz(u,v,0)} <0,0>,<2*pi,2*pi> contained_by{box{<-R,-R,-R>,<R,R,R>}} precompute 18, x,y,z
#declare A=0.5; #declare B=1.5; #declare C=1.0; #declare Fx = function {A*cos(u)} #declare Fy = function {B*cos(v)+A*sin(u)} #declare Fz = function {C*sin(v)} parametric { function {Fx(u,v,0)} function {Fy(u,v,0)} function {Fz(u,v,0)} <0,0>,<2*pi,2*pi> contained_by{box{<-R,-R,-R>,<R,R,R>}} precompute 18, x,y,z
#declare A=1; #declare B=0.2; #declare Fx = function {A*cos(u)*sin(v)} #declare Fy = function {A*sin(u)*sin(v)} #declare Fz = function {A*(cos(v)+ln(tan(v/2))) + B*u} parametric { function {Fx(u,v,0)} function {Fy(u,v,0)} function {Fz(u,v,0)} <2*pi,0>,<4*pi,2.1*pi> contained_by{box{<-R,-R,-R>,<R,R,R>}} precompute 18, x,y,z
The range of "u" values controls how far round the circle we go (0 to 2*pi is a complete circle) and the range of "v" values controls the width of the strip.
#declare Fx = function {cos(u)+v*cos(u/2)*cos(u)} #declare Fy = function {sin(u)+v*cos(u/2)*sin(u)} #declare Fz = function {v*sin(u/2)} #declare U1 = 0; #declare U2 = 2*pi; #declare V1 = -0.3; #declare V2 = 0.3; parametric { function {Fx(u,v,0)} function {Fy(u,v,0)} function {Fz(u,v,0)} <U1,V1>,<U2,V2> contained_by{box{<-R,-R,-R>,<R,R,R>}}
#declare Fx = function {u*v*v + 3*pow(v,4)} #declare Fy = function {-2*u*v - 4*pow(v,3)} #declare Fz = function {u} #declare U1 = -2; #declare U2 = 2; #declare V1 = -0.8; #declare V2 = 0.8; parametric { function {Fx(u,v,0)} function {Fy(u,v,0)} function {Fz(u,v,0)} <U1,V1>,<U2,V2> contained_by{box{<-R,-R,-R>,<R,R,R>}}Several of these surfaces have been shamelessly nicked from http://www.uib.no/People/nfytn/mathgal.htm. You might like to take a look at the Pov 3.1 code on that site that was used to generate some of the images there. Now that we have parametric isosurfaces, we can specify a surface in a few lines that used to take yards of mathematics to specify with smooth triangles.
If you use combinations of cos and sin functions you can ensure that your surface always remains within a certain distance of the origin, or that it only goes off to infinity in one dimension. For this surface that I just invented Fx, Fy and Fz are all trig functions that are never outside the range -1 to +1, so the complete surface must lie inside the unit cube.
#declare Fx = function {sin(u)*sin(v)} #declare Fy = function {cos(u)*sin(v)} #declare Fz = function {cos(u)*cos(v)} #declare U1 = -1*pi; #declare U2 = 1*pi; #declare V1 = -1*pi; #declare V2 = 1*pi; parametric { function {Fx(u,v,0)} function {Fy(u,v,0)} function {Fz(u,v,0)} <U1,V1>,<U2,V2> contained_by{box{<-R,-R,-R>,<R,R,R>}}
#declare Fx = function {sin(u)} #declare Fy = function {cos(u+v)} #declare Fz = function {v} #declare U1 = -pi; #declare U2 = pi; #declare V1 = -1.4; #declare V2 = 1.4; parametric { function {Fx(u,v,0)} function {Fy(u,v,0)} function {Fz(u,v,0)} <U1,V1>,<U2,V2> contained_by{box{<-R,-R,-R>,<R,R,R>}}
#declare Fx = function {sin(u)} #declare Fz = function {cos(u+v)} #declare Fy = function {-abs(v)/2} #declare U1 = -pi; #declare U2 = pi; #declare V1 = -40; #declare V2 = 40; parametric { function {Fx(u,v,0)} function {Fy(u,v,0)} function {Fz(u,v,0)} <U1,V1>,<U2,V2> contained_by{box{<-R,-R,-R>,<R,R,R>}}
x = sin(u)*sin(v) y = cos(u)*sin(v) z = cos(v)To these equations I've added "+cos(20*v)*0.05" to the x and y equations. The "20*v" controls the frequency of the ripples, and the "*0.05" factor controls their amplitude.
Numerous different ripple effects can be achieved by using "sin" instead of "cos", "20*u" instead of "20*v", and applying perturbations to different combinations of Fx, Fy Fz.
#declare Fx = function {sin(u)*sin(v) +cos(20*v)*0.05} #declare Fy = function {cos(u)*sin(v) +cos(20*u)*0.05} #declare Fz = function {cos(v)} #declare U1 = -1*pi; #declare U2 = 1*pi; #declare V1 = -1*pi; #declare V2 = 1*pi; parametric { function {Fx(u,v,0)} function {Fy(u,v,0)} function {Fz(u,v,0)} <U1,V1>,<U2,V2> contained_by{box{<-R,-R,-R>,<R,R,R>}}
The first term of Fx and Fy makes a circle of radius r2. The second term offsets that circle by a distance r1 in a direction that spirals round as u varies by 1/Turns. The third term creates the fluting effect.
#declare Turns=3; #declare r1 = 0.3; #declare r2 = 1.0; #declare Flute = 0.04; #declare Freq = 24; #declare Fx = function {r2*cos(v) + r1*sin(u*Turns) + Flute*sin(Freq*v)} #declare Fy = function {u} #declare Fz = function {r2*sin(v) + r1*cos(u*Turns) + Flute*sin(Freq*v)} #declare U1 = -1*pi; #declare U2 = 1*pi; #declare V1 = -1*pi; #declare V2 = 1*pi; parametric { function {Fx(u,v,0)} function {Fy(u,v,0)} function {Fz(u,v,0)} <U1,V1>,<U2,V2> contained_by{box{<-R,-R,-R>,<R,R,R>}}
Param.inc and associated files can be found on Ingo's webpage. There are several advantages to this technique:
The top image was created as a POV 3.5 parametric surface.
On my machine it took over 12 minutes to render.
The second image was created with the "param.inc" file as a mesh2 object. On my machine it took 5 seconds to render. I can't tell the difference between the resulting images.
I've added a grid to the third image to show the edges of the mesh.
The syntax of the POV 3.5 parametric isosurface is:
#declare Fx = function {A*cos(u)} #declare Fy = function {B*cos(v)+A*sin(u)} #declare Fz = function {C*sin(v)} parametric { function {Fx(u,v,0)} function {Fy(u,v,0)} function {Fz(u,v,0)} <0,0>,<2*pi,2*pi> contained_by{box{<-R,-R,-R>,<R,R,R>}} precompute 18, x,y,z}The syntax for the mesh2 object is:
#declare Fx = function(u,v) {A*cos(u)} #declare Fy = function(u,v) {B*cos(v)+A*sin(u)} #declare Fz = function(u,v) {C*sin(v)} #include "param.inc" object { Parametric(Fx,Fy,Fz,<0,0>,<2*pi,2*pi>,30,30,"") }When used this way, param.inc requires
For details of the options, read the comments inside Ingo's param.inc file.
Here's an example of one way of using uv mapping on a mesh2 created with param.inc.
The first surface is a simple torus
#declare Fx = function(u,v){cos(u)*(R1 + R2*cos(v))} #declare Fy = function(u,v){sin(u)*(R1 + R2*cos(v))} #declare Fz = function(u,v){R2*sin(v)}I've used a layered texture. The bottom layer is green and white stripes in the u direction, and the top layer is red and white stripes in the v direction. These are actually the same u and v that occur in the functions that specify the surface.
In this case, u and v are both in the range 0 to 2*pi, but I want an integral number of stripes (otherwise I get one stripe with a different width) so I've multiplied the texture parameters by 7/pi to get 14 stripes instead of 2*pi stripes.
texture { pigment { uv_mapping function{u*7/pi} colour_map {[0.5 rgb y][0.5 rgb 1]} } } texture { pigment { uv_mapping function{v*7/pi} colour_map {[0.4 rgb x][0.4 rgbt 1]} } finish { phong 0.5 phong_size 10 } }In the second example, the textures are still in the u and v directions. It's the actual surface that's twisted.
#declare Fx = function(u,v){cos(u-v)*(R1 + R2*cos(v+u))} #declare Fy = function(u,v){sin(u-v)*(R1 + R2*cos(v+u))} #declare Fz = function(u,v){R2*sin(v+u)}Without the uv mapped textures, these two surfaces look identical.
Continuous symmetrical pigment
Continuous asymmetric pigment
If the object you are attempting to uvmap has a place where the inside meets the outside,
like in a Moebius strip, then you may see a discontinuity in the mapping.
To fix this, you could use a texture that is symmetrical, like this:
pigment { uv_mapping function{v*3/0.6} colour_map {[0.2 rgb x][0.2 rgbt 1] [0.8 rgbt 1][0.8 rgb x]} }Or, if you really want an asymmetric texture you could paint the interior_texture with a copy of the same texture that is inverted. The easiest way to do this is to apply scale -1 to the interior_texture. In some cases it may be necessary to use scale <-1,1,1> or scale <1,-1,1>
Kevin's isosurface.inc can be found on Kevin's webpage.
The mesh data can be written to a file, so it need only be parsed once making it faster to re-render the scene or to have more than one copy of the surface in a scene.
In the vast majority of cases, the total time required to parse and render the
approximation is greater than the time taken to render a real isosurface,
but there could well be situations where using the approximation could be faster,
for example if the same isosurface were to appear in each frame of an animation
it would only need to be parsed once and then the data could be read from the file
for all the subsequent frames. This might be useful for Mechsim animations -
the real isosurface would be used for the simulation calculations, but the
approximation could be used for the actual rendering.
The top image was created as a POV 3.5 isosurface.
The second image was created as an approximation.
The third image shows how the approximation is constructed from a mesh of triangles.
The last image shows the same approximation but with a mesh of flat triangles instead of smooth_triangles
The syntax of the POV 3.5 isosurface is:
#declare f = function {f_torus(x,y,z,1,0.4)} #declare isoMin = <-R,-R,-R> #declare isoMax = <R,R,R> isosurface { function { f(x,y,z)} max_gradient 1.1 contained_by{box {isoMin,isoMax}} open }The syntax for the approximation is:
#declare f = function {f_torus(x,y,z,1,0.4)} #declare isoMin = <-R,-R,-R> #declare isoMax = <R,R,R> #declare isoSmooth = yes; #declare isoSegs = <15, 15, 15> #declare isoFileOption = 1; #declare isoFile = "KL01.iso"; #declare isoName = "Surface"; #include "isosurface.inc" object { Surface }The possible parameters for the macro are:-
The result is returned as a mesh which is #declared with the name that you specified in isoName, except when isoFileOption is set to 2, in which case the mesh is actioned immediately rather than being #declared.
The syntax when using isoFileOption = 2 is like this:
#declare f = function {f_torus(x,y,z,1,0.4)} #declare isoMin = <-R,-R,-R> #declare isoMax = <R,R,R> #declare isoSmooth = yes; #declare isoSegs = <15, 15, 15> #declare isoFileOption = 2; object { #include "isosurface.inc" pigment {rgb 1} }There is now also a modified version of this macro by Jaap Frank, which runs nearly twice as fast.
In the lastest version of the macro there's an additional Depth parameter. The macro performs surface subdivision on cells which contain part of the surface, and skips cells which don't.
For example, rendering this example file without subdivision, like this
#declare isoSegs = <32, 32, 32> #declare Depth = 0;causes the macro to examine all 32768 cells (32*32*32), but only 2664 of those cells contain parts of the surface.
Rendering the example with one level of subdivision, like this
#declare isoSegs = <16, 16, 16> #declare Depth = 1;causes the macro to examine 4096 cells at the initial depth, and find that only 720 of them contain parts of the surface. It then subdivides each of these 720 cells into 8 subcells and examines them and finds that 2664 of the subcells contain parts of the surface. It renders the same 2664 surface fragments as before, but it only needed to examine 9856 cells in order to find them (4096 + 720*8) instead of 32768.
Don't forget to change your isoSegs to smaller values when using subdivision, and be aware that the default Depth value is 2.
All the other parameters are the same as the Kevin Loney version.
Download a zip file containing the POV source files
associated with the images on this page.
#declare Gravity_Well = function(x,y,z,Strength) { x*x -Strength/y*y +z*z }declares a function that takes an extra "Strength" parameter. We don't tell the function what the value of "Strength" is when we declare it, but only when we reference it
isosurface { function { Gravity_Well(x, y, z, 0.002) }
#declare S = function { spline { linear_spline -1.0, < 0.5, 0, 0>, -0.5, < 0, 0, 0>, 0.01,< 0.2, 0, 0>, 0.5, <-0.2, 0, 0>, 1, <-0.2, 0, 0> } } isosurface { function { y - S(x).x }
In this case I'm using the first column of the spline only.
As x goes from -1 to +1 S(x) goes from <0.5,0,0> to <-0.2,0,0> along the spline.
For an isosurface, we don't want a vector function, so we choose the S(x).x component
of the function.
We can see that the isosurface follows the x co-ordinate of the spline, which is shown in red in the attached image.
Warning: There are known bugs in spline functions in POV 3.5b1. For example, if we replace the 0.01 point in the above example by
0.0, < 0.2, 0, 0>,the spline behaves in an unexpected manner.
#declare S = function { spline { natural_spline -1, < 0.5, 0, 0.0>, -0.5, < 0.2, 0, 0.4>, 0.01, < 0.2, 0, 0.2>, 0.5, < 0.4, 0, 0.4>, 1, < 0.0, 0,-0.6> } } isosurface { function { y - S(x).x - S(z).z }But we're not restricted to using just one dimension of the spline, or to using linear splines.
Here's a natural spline function. We can see that the surface follows both the x and z co-ordinates of the spline, which are shown in red and green respectively in the attached image.
In this way, it's possible to generate a 3d isosurface sheet from two 2d splines.
The new POV 3.5 "cubic_spline" is not very suitable for isosurface work because
it can be undefined at the endpoints, which causes strange effects in the surface.
Another thing that we can do with spline functions is to sweep along a 3d spline,
in a similar manner to a sphere_sweep.
#declare S = function { spline { natural_spline -1, < 0, 0.5, 0.0>, -0.5, < 0, 0.2, 0.4>, 0.01, < 0, 0.2, 0.2>, 0.5, < 0, 0.4, 0.4>, 1, < 0, 0.0,-0.6> } } isosurface { function { pow(y - S(x).y),2) + pow(z - S(x).z,2) - 0.05 }What you get isn't exactly the same as a sphere_sweep, it's more like a "hoop sweep" with the hoop always oriented in the same direction rather than turning as the spline turns.
It's also possible to write it in a way that more clearly separates the "hoop" function from the spline function, making it easier to replace the circular hoop with some other shape.
#declare Hoop = function(x,y,z,r){y*y + z*z - r*r} isosurface { function {Hoop(x, y-S(x).y, z-S(x).z, 0.223)}In the upper image, I've made the isosurface slightly transparent and indicated the path of the spline itself in yellow.
In the lower image I've added a ripple to the isosurface, so now you can
drape intestines along a 3d spline path.
parametric { function {S(u).x + sin(v)} function {S(u).y + cos(v)} function {S(u).z} <0,-pi><17,pi> contained_by{box {min_extent(The_Path)-<1,1,0.1> max_extent(The_Path)+<1,1,0.1>} }But that previous technique doesn't work with splines that have loops in them. The problem is that we were using the spline control points as our x co-ordinate and expressing y and z in terms of that x. The sequence of control points can't loop back on itself.
The way to follow 3d splines that contain loops is to use all three of the spline dimensions to specify the path of the spline, and express x, y, and z in terms of a fourth variable, call it "u", which follows the control points.
So what we need is a parametric isosurface.
In this case "u" follows the spline control points, and x, y, z are expressed in terms of u like S(u).x, S(u).y, S(u).z. On its own that would give an infinitely thin string that follows the spline path. We can fatten it out by adding some terms in a second variable "v" which describe how far the surface is from the path.
Once again I have made the isosurface transparent and drawn the actual spline path inside it in yellow.
For this image, I didn't know how large to make the contained_by box, so I've
calculated it during the parsing of the scene by taking the min_extent() and
max_extent() vectors of the yellow spline path and added something to allow for
the thickness of the wrapping.
You may have noticed that in the previous examples, the swept shape always faces
in the same direction. In the attached zip file I've included the SweepSpline macro that
sweeps a shape in such a way that it turns to always be perpendicular to the
spline that it is being swept along.
It's not actually an isosurface at all, but a mesh. I just stuck it here since it's
doing a similar job to some of the isosurface examples on this page.
Download a zip file containing the POV source files
for all the images that appear on this page.
#declare Fx = function(x,y) {S(u).x + sin(v)} #declare Fy = function(x,y) {S(u).y + cos(v)} #declare Fz = function(x,y) {S(u).z} #include "param.inc" object {Parametric(Fx,Fy,Fz, <0,-pi>,<17,pi>,100,20,"") pigment {rgbt <1,1,1,0.4>} finish {phong 0.5 phong_size 10} no_shadow }The spline technique shown on the previous page doesn't work with splines that have loops in them. The problem is that we were using the spline control points as our x co-ordinate and expressing y and z in terms of that x. The sequence of control points can't loop back on itself.
The way to follow 3d splines that contain loops is to use all three of the spline dimensions to specify the path of the spline, and express x, y, and z in terms of a fourth variable, call it "u", which follows the control points.
So what we need is a parametric isosurface.
In this case "u" follows the spline control points, and x, y, z are expressed in terms of u like S(u).x, S(u).y, S(u).z. On its own that would give an infinitely thin string that follows the spline path. We can fatten it out by adding some terms in a second variable "v" which describe how far the surface is from the path.
Once again I have made the isosurface transparent and drawn the actual spline path
inside it in yellow.
#declare Fx = function(x,y) {u} #declare Fy = function(x,y) {S(u).y + S2(v).y} #declare Fz = function(x,y) {S(u).z + S2(v).z}
In this case, the prism is a five pointed star with cubic interpolation, given
by the spline function S2(). The curved path along which the prism is bent
is given by the spline function S().
#declare Fx = function(x,y) {(S(u).x * sin(v)/2)} #declare Fy = function(x,y) {u} #declare Fz = function(x,y) {(S(u).x * cos(v)/2)}
I could have created this goblet as a lathe object, as a sor object or I could have used a non-parametric isosurface. However, I've used a parametric isosurface so that I can perform the alterations shown in the later examples.
The spline function S() is a one-dimensional spline which gives the profile of the goblet.
#declare Fx = function(x,y) {u} #declare Fy = function(x,y) {S(u).y * S2(v).y} #declare Fz = function(x,y) {S(u).y * S2(v).z}
The profile of the goblet is given by the one-dimensional spline function S(),
and the star-shaped cross section is given by the two-dimensional spline function S2().
#declare Fx = function(x,y) {(S(u).x * sin(v)/2) + S2(u).x} #declare Fy = function(x,y) {u} #declare Fz = function(x,y) {(S(u).x * cos(v)/2) + S2(u).z}
The profile of the goblet is given by the one-dimensional spline function S(),
and it is then bent along the two-dimensional spline S2().
#declare Fx = function(x,y) {(S(u).x * sin(v)/2)} #declare Fy = function(x,y) {u} #declare Fz = function(x,y) {(S(u).z * cos(v)/2)}
The x profile is given by the x component of the two-dimensional spline function S(),
and the z profile is given by the z component of the same spline function.
This is achieved by mutiplying one x spline function by (v<=pi) and the other by (v>pi). The comparison operators return 0 for false and 1 for true, so the sum switches from being one spline to the other when v becomes pi.
Similarly the z spline functions are multiplied by (cos(v)<=0) and (cos(v)>0), which switch
when v=pi/2 and switch back when v=3*pi/2.
#declare Fx = function(x,y) {sin(v)/2 * (S(u).x*-(v<=pi) + S2(u).x*-(v>pi)) } #declare Fy = function(x,y) {u} #declare Fz = function(x,y) {cos(v)/2 * (S(u).z*(cos(v)<=0) + S2(u).z*(cos(v)>0))}I've drawn in the four splines on one copy of the surface.
function {y-f_wood(x*2,0,z*2)*0.2 }
#declare P = function { pigment { wood colour_map {[0 rgb 0][1 rgb 1]}} } isosurface { function {y-P(x*2,0,z*2).grey*0.2}This is how we would generate the same surface from a pigment. The result is the same, but the processing is slightly less efficient. The difference in speed is only about 5% and there's no difference in the memory usage, so there's no real need to use pattern functions if you're happy with pigment functions.
#declare P = function { pattern{wood turbulence 0.02} } isosurface { function {y-P(x*2,0,z*2)*0.2 }Some pattern functions are built into "functions.inc", as above, but we could just as well define our own pattern functions directly. This allows us to add some pattern modifiers such as turbulence and warps.
f_agate
f_boxed
f_bozo
f_bumps
f_crackle
f_cylindrical
f_dents
f_granite
f_leopard
f_marble
f_ripples
f_spherical
f_spiral1
f_spotted
f_waves
f_wrinkles
function {y-f_sine_wave(x,0.1,3)}
function {y-f_scallop_wave(x,0.1,3)}
[If I'd have been creating these functions I would have defined them to be called like "my_sine_wave(x,y,z,amplitude,frequency)" even if the y and z values are not used, for consistency with all the other functions]
The "value" parameter is expected to be something that increases linearly across the space that you're working in. For example "x" or "z" or "3*x + y". The "amplitude" and "frequency" parameters are expected to be constants.
[You don't have to use the expected types of parameters, you could use variables for "amplitude" and "frequency" and a constant for "value" but the result probably won't be a sine_wave or scallop_wave any longer. You can use unexpected things for the parameters of any of the built in functions, e.g. function { f_sphere(x, y, z, abs(sin(x-y)))} misuses the f_sphere function by specifying a radius that isn't constant, but the result isn't much like a sphere].
The top image shows a black hole warp of a wood pattern.
#declare F=function{pattern{ wood scale 0.15 warp {black_hole 0,1 strength 2} } }The second image shows a repeat warp of a ripple pattern.
#declare F=function{pattern{ ripples scale 0.2 rotate y*30 warp{repeat x*0.4 flip x} } }The third image shows a cylindrical warp of a leopard pattern.
#declare F=function{pattern{ leopard warp{cylindrical orientation y dist_exp 1.5 } scale 0.1 } }
The upper image on the left was created with an isosurface
#declare P1=function{pattern{leopard turbulence 0.3 scale 0.05}} #declare P2=function{pattern{crackle turbulence 0.3 scale 0.7}} #declare P=function{P1(x,0,z)*0.3 +P2(x,0,z)} isosurface { function { y - P(x,0,1-z)} contained_by{box{<0,0,0>,<1,1,1>}} translate <-0.5,0,-0.5>The lower image was created with a height field using the same pattern.
height_field{function 300,300 {P(x,0,y)} translate <-0.5,0,-0.5>The odd looking P(x,0,1-z) and P(x,0,y) adjust for the different ways that isosurface and height field data is oriented, so that the same parts of the function are used in the same places. This allows you to develop your scene using the fast height field code and then produce a final render using the isosurface code which may be 20 times slower.
The significant differences are
#declare F=function{ pattern{ density_file df3 "spiral.df3" interpolate 1 } } isosurface {function { 0.05 - F(x,y,z) }Similarly, any function can be used as a media density pattern.
media{ scattering {1, 0.7} density { function { (sin(x*10)*0.2+y*2) - f_noise3d(x*20,y*20,z*20) } } }
function{y-f_noise_generator (x*3,0,z*3,1)*0.2}
function{y-f_noise_generator (x*3,0,z*3,2)*0.4}
function{y-f_noise_generator (x*3,0,z*3,3)*0.4}
function{y-f_snoise3d(x*3,0,z*3)*0.2}
The old noise algorithm looked OK when used for generating bozo pigments, and bump and dent normals, but when used in an isosurface it becomes clear that there are ugly plateau regions. This is now known as noise type 1. The plateau artefacts are generated when the noise function evaluates to a value outside the region 0 to 1, and the result gets clipped to keep it within that region.
Noise type 2 is the same algorithm as type 1, except that the values are scaled down so that they don't need to be clipped.
Noise type 3 is an entirely new perlin noise algorithm.
Type 3 is default, and it's probably a good idea to stick with it unless you have an image created with a previous version where you want to reproduce the behaviour of the old noise. In which case use type 1 if you want to reproduce the plateau artefacts or type 2 if you just need the bumps in the same places and don't want the plateaux.
Noise type 1 produces a stronger effect than the other two types (because it is effectively scaled over a range greater than [0,1]) so I've scaled it down in the top image on the left.
The type of noise can either be chosen by using
#include "functions.inc" isosurface { function { f_sphere(x,y,z,1) } accuracy 0.001 contained_by{sphere{0,1.2}} pigment {rgb .9} }Which gives exactly the same results as you would get by using a conventional sphere {0, 1} object or using the mathematical isosurface function { x*x + y*y + z*z - 1 } .
The ,1 is a parameter that gets applied to the built in function.
In the case of a sphere, the parameter sets the radius.
To perform the intersection of two built in isosurfaces, we
take the max() of the two function, like this
function {max(f_torus(x,y,z,1, 0.3), f_sphere(x,y+0.5,z,1))}Notice that it also possible to perform variable substitution at the same time. In this case the sphere has been shifted down by using "y+0.5" instead of "y".
The functions f_r(), f_th() and f_ph() are equivalent to the standard 3d polar co-ordinates Radius, Theta and Phi.
This surface is a sphere to which has been added a height that's proportional to sin(theta), but which also decreases as you get near the poles (otherwise the ridges look out of proportion when the come close together).
#declare Theta = function{f_th(x,y,z)} #declare Sphere = function{f_sphere(x,y,z,1)} isosurface { function { Sphere(x,y,z) + sin(Theta(x,y,z)*20)*0.05*(1-y*y) }
isosurface { function { - f_glob(x,y,z,0.1) } max_gradient 2 contained_by{sphere{0,1.2}} pigment {rgb .9} finish {phong 0.5 phong_size 10} }One part of this surface would actually go off to infinity if it were not restricted by the contained_by shape. It's possible to select just the teardrop part by choosing the contained_by shape appropriately.
The first three functions are more useful when used in combination
with other functions, or for expressing a surface in terms of 3d
polar co-ordinates, but these images show them working alone.
In the helixes and spiral functions, the 6th parameter is the cross section type.
The following values are possible:-
0: square
1: circle
2: diamond
3: concave diamond
Fractional values produce results that are intermediate between these shapes, i.e.
0.0 to 1.0: rounded squares
1.0 to 2.0: rounded diamonds
2.0 to 3.0: partially concave diamonds
function { f_r(x,y,z) - 0.7}When used alone, the f_r() function gives a surface that consists of all the points that are a specific distance from the origin, i.e. a sphere. If you use a threshold of zero (the default) this gives a sphere of size zero, which is invisible.
In this image I've subtracted 0.7 from the function, which is identical
to setting the threshold to 0.7. (My mathematical background causes me
to prefer to think of the surface as "R - 0.7 = 0" rather than "R = 0.7".
function { f_th(x,y,z) }When used alone, the f_th() function gives a surface which consists of all points that have a longitude of zero or 180 degrees. I.e. a plane through the origin.
function { f_ph(x,y,z) } threshold 1When used alone, the f_ph() function gives a surface that consists of all points that are at a particular latitude, i.e. a cone. If you use a threshold of zero (the default) this gives a cone of width zero, which is invisible.
For this image, I've set the threshold to 1. The cone consists of all points
that have Phi equal to 1 radian.
function { f_sphere(x,y,z,0.9) }The f_sphere() function creates a sphere.
There is one parameter:
function { f_helix1 (x,y,z,2,5,0.1,0.3,1,2,45) }At last, an interesting shape. f_helix1() is intended for use with helixes where the major radius is greater than the minor radius. There are seven parameters:-
function { f_helix2 (x,y,z,0, 8, 0.3, 0.1, 1, 1, 0) }f_helix2() is intended for use with helixes where the minor radius is greater than the major radius. I.e. for situations like twisty table legs.
function { f_spiral (x,y,z,0.3,0.1,1,-0,-0,2) }The parameters of the f_spiral() function are:-
function { f_mesh1 (x,y,z,1,0.2,1,0.1,2)} threshold 0.08 }f_mesh1() gives a set of threads that weave up and down through each other in a rectangular pattern.
function { f_rounded_box (x,y,z,0.1,0.7,0.2,0.7) }The f_rounded_box() takes 4 parameters:
function { f_torus (x,y,z,0.8,0.1) }The f_torus() function takes 2 parameters:
function { - f_superellipsoid (x,y,z,0.5,0.5) }The f_superellipsoid() isosurface function creates a surface that's the same as the conventional superellipsoid object, and the two parameters have the same effects.
It happens that the algorithm used for this function is inside out, so if you render
function {f_superellipsoid(x,y,z,0.5,0.5) } all you see is the outside of
your contained_by surface, and you may not be aware that there's a superellipsoidal
hole buried inside it.
To turn it right side out, a minus sign must be applied. If using a non-zero threshold, it needs to be negated also.
Download a zip file containing the POV source files
for all the images that appear on this page.
What you're actually seeing is the "contained_by" surface.
isosurface { function {f_bicorn(x,y,z,1,1)} max_gradient 2 contained_by{sphere {0,R}} pigment {rgb .9} finish {phong 0.5 phong_size 10} }
We can see that the surface that we wanted is buried inside, and it's hollow.
intersection { plane {-z, 0 } isosurface { function { f_bicorn(x,y,z,1,1) } max_gradient 20 contained_by{sphere {0,R}} max_trace 3 } pigment {rgb .9} finish {phong 0.5 phong_size 10} }
We can invert the isosurface by making the function negative. (Note that we can't use the "inverse" command because that inverts the contained_by surface as well and the result looks the same apart from the "Camera is inside a non-hollow object" warning.) If we were using a threshold value we would have to make that negative also.
isosurface { function { 0 - f_bicorn(x,y,z,1,1) } max_gradient 5 contained_by{sphere {0,R}} pigment {rgb .9} finish {phong 0.5 phong_size 10} }
When you specify "open", the contained_by surface becomes invisible.
isosurface { function { f_bicorn(x,y,z,1,1) } max_gradient 2 contained_by{sphere {0,R}} open pigment {rgb .9} finish {phong 0.5 phong_size 10} }
These functions are now included in the program code, so they can be used without using external library calls. There are some special parameters with complicated effects that are found in several of these functions. Rather than describe them each time they occur, I'll describe them here, and refer to them later as "Field Strength" and "Field Limit" etc.
Field Strength The numerical value at a point in space generated by the function is multiplied by the Field Strength. The set of points where the function evaluates to zero are unaffected by any positive value of this parameter, so if you're just using the function on its own with threshold = 0, the generated surface is still the same.
In some cases, the field strength has a considerable effect on the speed and accuracy of rendering the surface. In general, increasing the field strength speeds up the rendering, but if you set the value too high the surface starts to break up and may disappear completely.
The surface generated with threshold = 0.1 and field strength 1 is the same as the surface generated with threshold = 0.2 and field strength 2. Doubling the field strength has doubled the numeric value at a point in space, but doubling the threshold tells the program to look for points where the numeric value is double.
Setting the field strength to a negative value produces the inverse of the surface, like setting sign -1.
Field Limit The numerical value at a point in space generated by the function is limited to plus or minus the field limit. E.g. if you set the field limit to 2, then the value returned for a point that is a large distance from the surface will be +2 or -2.
This won't make any difference to the generated surface if you're using threshold that's within the field limit (and will kill the surface completely if the threshold is greater than the field limit). However, it may make a huge difference to the rendering times.
If you use the function to generate a pigment, then all points that are a long way from the surface will have the same colour, the colour that corresponds to the numerical value of the field limit.
SOR Switch If greater than zero, the curve is swept out as a surface of revolution. If the value is zero or negative, the curve is extruded linearly in the Z direction.
SOR Offset If the SOR switch is on, then the curve is shifted this distance in the X direction before being swept out.
SOR Angle If the SOR switch is on, then the curve is rotated
this number of degrees about the Z axis before being swept out.
An algebraic cylinder is what you get if you take any 2d curve and plot it in 3d.
The 2d curve is simply extruded along the third axis, in this case the z axis.
In the article that started all this, Tore Nordstrand just happened to mention four particular 2d curves that could be used in this way, and these have become the Algbr_Cyl functions that are implemented.
function {f_algbr_cyl1(x,y,z,1,1,0,0,0)}
function { f_algbr_cyl2(x,y,z,1,1,0,0,0)}
function { f_algbr_cyl3(x,y,z,1,1,0,0,0)}
function { 0 - f_algbr_cyl4(x,y,z,1,1,0,0,0)}
I've sliced these three images in half so that you can see the 2d figure-of-eight curve that generates them more easily.
function { f_algbr_cyl1(x,y,z,1,1,1,0,0)}
function { f_algbr_cyl1(x,y,z,1,1,1,0.4,0)}
function { f_algbr_cyl1(x,y,z,1,1,1,0.6,90)}
The parameters are:
function { - f_bicorn(x,y,z,1,1)}
function { - f_bifolia(x,y,z,1,3)}
For this surface, it helps if the field strength is set extremely low, otherwise the surface has a tendency to break up or disappear entirely. This has the side effect of making the rendering times extremely long - this image took 16 times longer to render than any other surface on this page.
The parameters are:
function { - f_boy_surface(x,y,z,0.0000001,1)}
function { - f_ovals_of_cassini (x, y, z, 1, 0.5, 0.24, 5)}
The parameters are:
function { f_cubic_saddle(x,y,z,0.1)}
The parameters are:
function { - f_cushion(x,y,z,1)}
The parameters are:
function { -f_devils_curve(x,y,z,1)}
The parameters are:
function { f_devils_curve_2d (x, y, z, 1, 1.5, 1.52, 0, 0, 0) }
A Dupin Cyclid can be considered to be the envelope of all the possible spheres that just kiss three other spheres. It can also be considered to be the envelope of all spheres on a conic section that just touch a given sphere. And it can also be considered to be the "inversion" of a torus.
function { f_dupin_cyclid (x, y, z, 0.0001, 4.9, 5, 2, 0, 3)}
The parameters are:
function { f_dupin_cyclid (x, y, z, 0.000001, 3, 5, 3, 0, 9)}
function { - f_dupin_cyclid (x,y,z,0.0000001, 6, 0.5, 3, 0, 12)}Download a zip file containing the POV source files for all the images that appear on this page.
The parameters are:
function { - f_folium_surface(x,y,z,0.01,3,5)}
The parameters are:
function { f_folium_surface_2d(x,y,z,0.01,1,1,1,0,0)}
The parameters are:
function { - f_torus_gumdrop(x,y,z,0.01)}
The parameters are:
function { -f_hunt_surface(x,y,z,0.1)}
The parameters are:
function { - f_hyperbolic_torus (x, y, z, 1, 0.6, 0.4)}
The parameters are:
function { f_kampyle_of_eudoxus(x,y,z,1,0,1)}
The parameters are:
function { - f_kampyle_of_eudoxus_2d (x, y, z, 1, 0, 1, 1, 0, 90)}
The parameters are:
function { f_klein_bottle(x,y,z,-1)}
The parameters are:
function { -f_kummer_surface_v1(x,y,z,0.01)}
The parameters are:
function {f_kummer_surface_v2 (x, y, z, 0.001, -2, -0.94, 0.4)}
The parameters are:
function { f_lemniscate_of_gerono(x,y,z,1)}
I've cut the surface in half so that you can see the figure-of-eight curve that sweeps round the Y axis to generate this surface of revolution.
The parameters are:
function { f_lemniscate_of_gerono_2d (x,y,z,-0.1,1,1,1,2,-45)}
#declare F = function {y - x*x} isosurface { function { F(sqrt(x*x + z*z), y, z)}
The parameters are:
function { - f_paraboloid(x,y,z,1)}
The parameters are:
function { - f_parabolic_torus(x,y,z, 0.1, 0.6, 0.5)}
The parameters are:
function { f_piriform(x, y, z, 1)}
This might be a useful shape for making hot air balloons - reduce the fatness parameter to make a weather balloon.
The parameters are:
function { f_piriform_2d (x, y, z, -1, 1, -1, 1, 1, 0, -90)}
The parameters are:
function { -f_quartic_paraboloid(x,y,z,1)}
The parameters are:
function { f_quartic_saddle(x,y,z,1)}
The parameters are:
function { - f_quartic_cylinder(x,y,z,1,0.6,0.3)}
It is a model of the projective plane.
The parameters are:
function { f_steiners_roman(x,y,z,-1)+0}
The parameters are:
When the Size parameter is 3 times the Sharpness, the surface is given the special name "trisectrix of Maclaurin".
function { f_strophoid(x,y,z,1,1.5,1,1.2)}
The parameters are:
function { f_strophoid_2d(x,y,z,-1,1.5,1,1.2,1,1,180)}
The parameters are:
function { f_glob(x,y,z,-1)}
The parameters are:
function { f_pillow(x,y,z,1)}
The parameters are:
function { - f_crossed_trough(x,y,z,1)+0}
The parameters are:
function { - f_witch_of_agnesi(x,y,z,1,0.02)}
The parameters are:
function { -f_witch_of_agnesi_2d (x,y,z,1, 0.2, 0.04, 1, 0, 0)}
The parameters are:
function { f_mitre(x,y,z,-1)}
This time I've cut it in half so that you can see the way that the rear face puckers forward to meet the dimple from the front face.
The parameters are:
function { f_odd(x,y,z,-1)}
The parameters are:
function { f_heart(z,x,y,-1)}
The parameters are:
function { f_nodal_cubic(x,y,z,-0.1)}
The parameters are:
function { f_umbrella(x,y,z,1)}
The parameters are:
function { f_enneper(x,y,z,-0.1)}Download a zip file containing the POV source files for all the images that appear on this page.
The parameters are:
function {f_ellipsoid(x,y,z,1,3,1)} threshold 1
For some unknown reason, this function only seems to work with negative threshold settings. (It took me lots of attempts with blank images before I thought of trying that.)
The parameters are:
function {f_blob(x,y,z,1,1,0.7,1,1)} threshold -0.01
The parameters are:
function {f_flange_cover(x,y,z,0.01,35,1.5,1)}
The parameters are:
function {f_blob2(x,y,z,1,3,2,1)}
The parameters are:
function {f_cross_ellipsoids(x,y,z,0.1,8,4,1)}
The parameters are:
f_isect_ellipsoids(x,y,z,3,1,4,1)
The parameters are:
function { - f_spikes(x,y,z,0.04,8,1,1,1)} threshold -1
The parameters are:
function {f_poly4(x,y,z,0,1,-1,0,0)}
The parameters are:
f_spikes_2d(x,y,z,0.4,15,15,2.5)
The parameters are:
f_quantum(x,y,z,0)
I'm not completely sure about how the parameters work for this surface.
The parameters are something like:
function {f_helical_torus (x,y,z, 6, 12, 2, 0.1, .5, 1, 0.1, 1, 1.0, 0)} function {f_helical_torus (x,y,z,2, 5, 1, 0.1, 1, 0.5, 1, 6, 3, 0)}A more controlable helical torus can be created by starting with an ordinary helix and then transforming the coordinate system from cartesian to cylindrical polar coordinates.
I've included two of them in the image, and chosen the viewing angle so that you can't see that they don't quite fit together properly in the middle.
Actually, they fit reasonably well if you set the threshold to about 0.05.
The parameters are:
function {f_comma(x,y,z,1)
The parameters are:
function {f_polytubes(x,y,z,4,0,-1,0,1,0)}
The parameters are:
function { f_hex_x(x,y,z,0)}
The parameters are:
function { - f_hex_x(x,y,z,0)}
The three images show
The parameters are:
function { f_ridge(x,y,z,1,3,1,0.2,0,0)}
The three images show
The parameters are:
function { - f_ridged_mf(x,y,z,2,3,1,0.1,1,2)}
The three images show
The parameters are:
#declare F = function {f_hetero_mf(x,y,z,2,3,1,0.1,1,0)}
This is the built-in function f_helix1, but instead of using a constant for the major radius (like 0.4) I've used the expression 0.4*(1-y). This causes the major radius to vary from 0 to 0.8 as y goes from 1 to -1.
I've deliberately chosen the expression 0.4*(1-y) so that y doesn't
become negative anywhere in the evaluated region.
function { f_helix1(x,y,z,1,10*(2+y),0.07*(1-y),0.4*(1-y),1,0,0)}
In this scene the period, minor radius and major radius all vary as y changes.
function { f_torus(x,y,z,0.5,0.1*(0.6+x)) }
Changing the minor radius of a torus as a function of x can give an effect similar to
the Dupin cyclid
function { f_torus(x,y,z,1*(y+0.4),0.1 )}
Changing the major radius of a torus as a function of y can give an effect
similar to an elliptical torus.
Download a zip file containing the POV source files
for all the images that appear on this page.
You can use individual fixed elements of an array, like A[3] but you can't use the function variables to index the array like A[floor(x)].
If you've got a reasonably small number of elements in your array, you can use nested select() operations to pick out the individual elements of the array. In this example the array has eight elements.
#declare Index=function(i){ select(i-4, select(i-2, select(i-1,A[0],A[1]), select(i-3,A[2],A[3]) ), select(i-6, select(i-5,A[4],A[5]), select(i-7,A[6],A[7]) ) ) } isosurface { function {F_cylinder(x,y,z,Index(x))}This Index() function behaves rather like an array lookup, but it's going to be rather cumbersome to use this technique with large arrays. You're not restricted to using float arrays. If you wish, you can create arrays of functions, like this:
#declare A[0] = function{f_sphere(x,y,z,1.0)}
And reference them like
You can call a macro from inside an isosurface function, but it gets evaluated once, at parse time.
However, it is possible to use macros when you approximate a parametric surface, because Ingo's "param.inc" includes the ability to work with macros instead of functions. You can't use Ingo's Parametric macro, because it only works with functions, you have to call the underlying Paramcalc macro directly, which is a bit trickier.
When using Paramcalc you have to name your macros (or functions) __Fx, __Fy, __Fz, rather than passing them as parameters to the macro. The remaining parameters of Paramcalc are the same as those for Parametric.
The code inside your macros has access to the #local variables within Ingo's include file. This means that you have to be careful to avoid variable name conflicts if you want the macro to work with variables that are declared in your own code. E.g. if you #declare variables in your own code called 'A', 'B', 'C', 'D', 'P', 'U' or 'V' then you can't use them within the passed macros because Paramcalc has #local variables with the same names.
You can't use variables u, v, x, y or z inside your macros, even as their formal parameters, because POV interprets them as shorthand for the unit vectors when parsing macros.
You can use arrays and vectors inside your macros.
Your macros can call functions and other macros.
#macro __Fx(U,V) My_Function(U) * cos(My_B*U) + My_Array[U] #end #include "param.inc" object{ Paramcalc(<0,0>,<2*pi,2*pi>,50,40,"")
All Steiner surfaces contain singularities of some form or other.
This is Steiner's Roman Surface. It has three double lines, six pinch points, and a triple point.
function {x*x*y*y + x*x*z*z + y*y*z*z - x*y*z}
function {x*x*y*y - x*x*z*z + y*y*z*z - x*y*z}
function {4*x*x*(x*x + y*y + z*z + z) + y*y*(y*y + z*z - 1) }
function {y*y - 2*x*y*y -x*z*z +x*x*y*y +x*x*z*z -z*z*z*z}
For some reason, this surface didn't look anything remotely like what it was supposed to when I used function x*x*(z-1)*(z-1) +y*y*(y*y+z*z-1) so I've actually used a parametric isosurface to generate it.
#declare Fx = function {2*u*cos(v)*sqrt(1-u*u)} #declare Fy = function {2*u*sin(v)*sqrt(1-u*u)} #declare Fz = function {1-2*u*u*cos(v)*cos(v)} parametric { function {Fx(u,v,0)} function {Fy(u,v,0)} function {Fz(u,v,0)} <0,0>,<1,2*pi> contained_by{box{<-R,-R,-R>,<R,R,R>}} precompute 18, x,y,z }
For some reason, this surface didn't look anything remotely like what it was supposed to when I used a conventional isosurface so I've actually used a parametric isosurface to generate it.
#declare Fx = function {1 -u*u +u*u*sin(v)*sin(v)} #declare Fy = function {u*u*sin(v)*sin(v) +2*u*u*sin(v)*cos(v)} #declare Fz = function {sqrt((1-u*u)/2)*u*(sin(v)+cos(v))} parametric { function {Fx(u,v,0)} function {Fy(u,v,0)} function {Fz(u,v,0)} <0,0>,<1,2*pi> contained_by{box{V1,V2}} max_gradient 10 accuracy 0.00001 precompute 20, x,y,zWarning: This surface takes a ridiculously long time to render.
function {x*y*y -y*z -x*z*z }
function {x*x -y*y*z}
function {x*x - x*x*z -y*y*z }
function {y*y*y +x*y*z -z*z}
#declare Flare=function(u){u/(2*pi) + pow(u/(2*pi),20)*2} #declare Fx = function(u,v) {(2 + u/(2*pi)*cos(v))*sin(u) - 0.1*u} #declare Fy = function(u,v) {(2 + Flare(u)*cos(v))*cos(u) + 0.5*u} #declare Fz = function(u,v) {Flare(u)*sin(v)}Once again I started with a parametric torus. In this case I happened to start with
#declare Fx = function(u,v){(2 + cos(v))*sin(u)} #declare Fy = function(u,v){(2 + cos(v))*cos(u)} #declare Fz = function(u,v){sin(v)}I initially added u/(2*pi) factors to all three components, but then replaced these by Flare(u) in Fy and Fz. The Flare(u) function is designed so that it looks very similar to u/(2*pi) when u lies between 0 and about 1.8*pi, but increases rapidly between u=1.8*pi and u=2*pi. Thus causing the final widening at the top of the instrument in the Z and Y directions.
The final - 0.1*u and + 0.5*u factors fine tune the position
of the top of the instrument.
I've cheated slightly by adding a flattened sphere to the centre. That sphere is not part of the actual parametric surface.
The functions are:
#declare R = function(u,v){ cos(v)*cos(v) * max(abs(sin(4*u)), 0.9-0.2*abs(cos(8*u))) } #declare Fx= function(u,v){R(u,v)*cos(u)*cos(v)} #declare Fy= function(u,v){R(u,v)*sin(u)*cos(v)} #declare Fz= function(u,v){R(u,v)*sin(v)*0.5} sphere {-0.2*z,0.2 scale <1,1,0.5>}This shape renders rather slowly as a real parametric isosurface, so I suggest using Ingo's param.inc macro to approximate it.
I started from the parametric functions for a sphere:
#declare Fx= function(u,v){R*cos(u)*cos(v)} #declare Fy= function(u,v){R*sin(u)*cos(v)} #declare Fz= function(u,v){R*sin(v)}Then replaced the constant R with the function R(u,v).
There are three main parts to the function R(u,v). This bit
#declare R = function(u,v){cos(v)*cos(v)}controls the 2d shape we see if we take a vertical cross section. It's a sort of figure-8. By using this figure-8 to modify the radius of the sphere, we get something like a torus but with the central hole just filled in.
The abs(sin(4*u)) varies the radius as we go round the longitude of the sphere. So
#declare R = function(u,v){cos(v)*cos(v) * abs(sin(4*u))}gives the shape shown in the second image on the left, and
#declare R = function(u,v){cos(v)*cos(v) * 0.9-0.2*abs(cos(8*u))}gives the shape shown in the third image.
The max() operation takes the union of those two shapes.
#declare R=function(u,v) {4*cos(v)*pow(sin(abs(u)),abs(u)) } #declare Fx = function(u,v){R(u,v)*cos(u)*cos(v)*1.6} #declare Fy = function(u,v){R(u,v)*sin(u)*cos(v)} #declare Fz = function(u,v){2*sin(v)}The functions are based on the parametric form of the sphere, but with the radius varying under the control of the R() function rather then being constant.
The *1.6 in Fx and the *2 in Fz are
simply there to perform scaling. I could just as easily left them out
of the functions and performed scale <1.6, 1, 2>
on the completed surface.
These shapes are created by performing variable transformations on the standard f_torus() and f_sphere() functions. The y variable is replaced by y-pow(abs(x),0.8)*0.5 which causes the shape to be bent upwards.The z variable is replaced by z*2 in the f_sphere() to perform simple scaling.
function { f_torus(y-pow(abs(x),0.8)*0.5,z,x,0.8,0.1) } function { f_sphere(y-pow(abs(x),0.8)*0.5,z*2,x,0.6) }
#declare Fx = function(u,v){cos(u)*(R1 + R2*cos(v)) + pow((v/pi),100)} #declare Fy = function(u,v){sin(u)*(R1 + R2*cos(v)) + 0.25*cos(5*u)} #declare Fz = function(u,v){-2.3*ln(1 - v*0.3157) + 6*sin(v) +2*cos(v)}The stalk of the apple really is part of the surface.
The shape is based on a fat torus, but with the tube part of the torus made slightly elliptical and tilted by means of the +2*cos(v) at the end of the Fz function.
The ln(1 - v*0.3157) bit in Fz becomes significant when v approaches pi (0.3157 is just a little lower than 1/pi). It creates the stalk.
The + pow((v/pi),100) bit in Fx adds a curve to the stalk. The effect only happens when v is very close to pi, so when using Ingo's macro to approximate the parametric it is necessary to use a large number of segments in the v direction.
The +0.25*cos(5*u) bit in Fy adds a gentle ripple to the shape.
The code can be used to generate any NACA 4-digit aerofoil by changing the settings of "M", "P" and "T".
A description of the system can be found at
www.aerospaceweb.org/question/airfoils/q0100.shtml.
Download a zip file containing the POV source files
for all the images that appear on this page. The zip file contains
versions of each shape using real parametric isosurfaces and using
the param.inc approximation macro.
#declare W = function(u){u/(2*pi)} #declare Fx = function(u,v){W(u)*cos(N*u)*(1+cos(v))} #declare Fy = function(u,v){W(u)*sin(N*u)*(1+cos(v))} #declare Fz = function(u,v){W(u)*sin(v) + H*pow(W(u),2)}Where
u/(2*pi) simply gives a value that goes linearly from 0 to 1 as u goes from 0 to 2*pi. It turns up several times in the functions, so I've made it into a separate sub-function W(u)
I started with the parametric functions for a torus:
#declare Fx = function(u,v){cos(u)*(1+cos(v))} #declare Fy = function(u,v){sin(u)*(1+cos(v))} #declare Fz = function(u,v){sin(v)}The first parts of Fx, Fy and Fz are multiplied by R(u) so that the radius of the tube increases linearly as u increases.
The N*u factors that occur in Fx and Fy cause the tube to go round the origin N times.
Then I added +H*pow(W(u),2) to Fz so that the turns
of the spiral are offset in the z direction by a distance that varies from 0 to H.
I found that I needed to
square the u/(2*pi) term, otherwise the offset was too
rapid at the start.
Download a zip file containing the POV source files
for all the images that appear on this page. The zip file contains
versions of each shape using real parametric isosurfaces and using
the param.inc approximation macro.
#declare Thing = difference {box {-1,1} sphere {0,1.2} } #declare P=function{pattern{object {Thing}}} #include "functions.inc" isosurface{ function{P(x,y,z)-0.999} max_gradient 100 contained_by {box{-1.1,1.1}} openIt might seem like there would be all sorts of interesting effects that could be achieved by using the "object" pattern.
function {pattern {object {My_Object}}}
is a function that returns the value 1 for all points that are inside My_Object
and the value 0 for all points that are outside My_Object.
So we might think that we could make an isosurface from that function
and then modify it in interesting ways, for example by adding a noise function.
Unfortunately, any path through the isosurface encounters a point where the value jumps instantly from 0 to 1, so the actual max_gradient of every point on the surface is infinite. Instead of getting a few nasty little holes in our nice isosurface, we get a few tiny patches of surface and the rest is hole, unless very extreme values of max_gradient are used (and then it takes ages).
Also, the idea of adding noise to the surface doesn't work quite like what you'd intuitively expect.
Adding a noise function to the object pattern function gives one region of space where
the function evaluates to noise+1.0 and in the rest of space it evaluates to noise+0.0.
One thing we could do is to apply a turbulence warp to the object pattern, but
that's not so easy to control and still suffers from infinite max_gradient.
If you think about max_gradient for a while you might get the idea that
something with a low max_gradient will always render faster than something
with a higher max_gradient. If this were the case, then we could speed things
up by artificially reducing the gradient of the function.
For example, if we consider
isosurface { function { 0.3 - F(x,y,z) } max_gradient 10we might notice that we can obtain exactly the same surface by using
isosurface { function { (0.3 - F(x,y,z))*0.1 } max_gradient 1or even
isosurface { function { (0.3 - F(x,y,z))*0.01 } max_gradient 0.1These do in fact produce the same image when rendered. However, although the max_gradient is very different, the rendering times are exactly the same. You can't use vectors in an isosurface function.
You can't even extract float values from a vector by using things like V.red
You can use individual fixed elements of an array, like A[3] but you can't use the function variables to index the array like A[floor(x)].
You can call a macro from inside an isosurface function, but it gets evaluated once, at parse time.
Don't get confused by the fact that you can use "x", "y" and "z" in your macro. "x", "y" and "z" have two different meanings in POV-Ray, and inside a macro they are always interpreted as having the other meaning: shortcuts for the unit vectors.
You can, however, use macros with Ingo's "param.inc" file, like this
Download a zip file containing the POV source files
for all the images that appear on this page. (POV-ray 3.5 format only)
Alphabetical Index© Mike Williams 2003, 2004 |
Back to the Isosurface Index |
|
Back to the Isosurface Index |