Variable Substitution

© Mike Williams 2001,2002,2003,2004

Up | Previous: Differences between MegaPOV and POV 3.5 | Next: Combining Functions | Alphabetical Index

#declaring functions

It's possible to #declare a function for later use in an isosurface, but the x, y and z values used in the declaration are formal parameters and it's possible to substitute different actual parameters when the function is invoked.

#declare  S = function {x*x + y*y + z*z - 1}
isosurface {
  function { S(x,y,z) }
        accuracy 0.001
        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}


But we don't have to use the same parameters when we invoke the function. We could use function { S(x/2,y,z) } instead of function { S(x,y,z) }. This has the effect of performing a substitution of x/2 in the equation wherever the variable x occurred, giving us a surface equivalent to

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.


function { S(x/2, y-0.5, z) }

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>.


It's a bit more dificult to produce a particular shear transformation, because the values that we need to use in the variable substitution need to be the inverse of those normally used to generate the shear. We could calculate the inverse of the transformation matrix by hand, but it is possible to get POV-Ray to do it for us.

We first create a shear transformation, then declare a transformation function using its inverse.

#include ""
#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.

Non-linear scale

#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.

Surface of revolution

This is a particularly interesting variable substitution. It generates a surface of revolution from the positive part of an equation of two variables.

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) }
You can create a surface of revolution from the equation of any 2d curve in the same way.

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) }
By transforming from cartesian to cylindrical polar coordinates, it's possible to bend any isosurface into a circle around the origin.

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))}
Similarly, it's possible to wrap a sheetlike isosurface around a sphere by transforming from cartesian to spherical polar coordinates.

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)) }
It's possible to use a single isosurface to produce multiple copies of a shape by using the mod operator in a substitution.

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) }


If we have a function F(x,y,z) we can turn it into two parallel surfaces by using abs(F(x,y,z))-C where C is some small value. The original function should be one that works with zero threshold. The two resulting surfaces are what you would get by rendering the original function with threshold +C and -C, but combined into a single image. The space between the two surfaces becomes the "inside" of the object. In this way we can construct things like glasses and cups that have walls of non-zero thickness.

#declare  F = function {y
         +f_noise3d(x*2, 0, z*2)

isosurface {
  function { abs(F(x,y,z))-0.1 }

Download a zip file containing the POV source files for all the images that appear on this page.

Up | Previous: Differences between MegaPOV and POV 3.5 | Next: Combining Functions | Alphabetical Index