6.5.4  Isosurface Object

An isosurface is a very versatile object. Mathematically its surface is defined by a function. If there is a way to describe a surface with a function, it can also be rendered as an isosurface object.
This object also allows for real deformations and surface displacements.

All points which are tested against a defined function and equal a required threshold value, belong to the object's surface. It is obvious that POV-Ray couldn't test all points in infinite space, since it would take forever. To speed things up, points are sampled within a defined area and within a specified accuracy range. Then the surface is created by interpolation between the matching points. This means that an isosurface is an approximation (accuracy depending on the settings) of the exact location of the function's surface. But for the vast majority of scenes this is more than accurate enough.

6.5.4.1  Isosurface Syntax

The syntax for the isosurface is:

isosurface {
  function { FUNCTION_ITEMS }
  [contained_by { SPHERE | BOX }]
  [threshold FLOAT_VALUE]
  [accuracy FLOAT_VALUE]
  [max_gradient FLOAT_VALUE]
  [evaluate P0, P1, P2]
  [open]
  [max_trace INTEGER] | [all_intersections]
  [OBJECT_MODIFIERS...]
}

Isosurface default values:

contained_by : box{-1,1}
threshold    : 0.0
accuracy     : 0.001
max_gradient : 1.1

function { ... } This must be specified and be the first item of the isosurface statement. Here you place all the mathematical functions that will describe the surface.

contained_by { ... } The contained_by 'object' limits the area where POV-Ray samples for the surface of the function. This container can either be a sphere or a box, both of which use the standard POV-Ray syntax. If not specified a box {<-1,-1,-1>, <1,1,1>} will be used as default.

contained_by { sphere { CENTER, RADIUS } }
contained_by { box { CORNER1, CORNER2 } }

threshold This specifies how much strength, or substance to give the isosurface. The surface appears where the function value equals the threshold value. The default threshold is 0.

function = threshold

accuracy The isosurface finding method is a recursive subdivision method. This subdivision goes on until the length of the interval where POV-Ray finds a surface point is less than the specified accuracy. The default value is 0.001.
Smaller values produces more accurate surfaces, but it takes longer to render.

max_gradient POV-Ray can find the first intersecting point between a ray and the isosurface of any continuous function if the maximum gradient of the function is known. Therefore you can specify a max_gradient for the function. The default value is 1.1. When the max_gradient used to find the intersecting point is too high, the render slows down considerably. When it is too low, artefacts or holes may appear on the isosurface. When it is way too low, the surface doesn't show at all. While rendering the isosurface POV-Ray records the found gradient values and prints a warning if these values are higher or much lower than the specified max_gradient:

Warning: The maximum gradient found was 5.257, but max_gradient of
the isosurface was set to 5.000. The isosurface may contain holes!
Adjust max_gradient to get a proper rendering of the isosurface.
Warning: The maximum gradient found was 5.257, but max_gradient of
the isosurface was set to 7.000. Adjust max_gradient to
get a faster rendering of the isosurface.
For best performance you should specify a value close to the real maximum gradient.

evaluate POV-Ray can also dynamically adapt the used max_gradient. To activate this technique you have to specify the evaluate keyword followed by three parameters:

In this case POV-Ray starts with the max_gradient value P0 and dynamically changes it during the render using P1 and P2. In the evaluation process, the P1 and P2 parameters are used in quadratic functions. This means that over-estimation increases more rapidly with higher values and attenuation more rapidly with lower values. Also with dynamic max_gradient, there can be artefacts and holes.

If you are unsure what values to use, start a render without evaluate to get a value for max_gradient. Now you can use it with evaluate like this:

When there are artifacts / holes in the isosurface, increase the min_factor and / or P2 a bit. Example: when the first run gives a found max_gradient of 356, start with
  #declare Min_factor= 0.6;
  isosurface {
     ...
     evaluate 356*Min_factor,  sqrt(356/(356*Min_factor)),  0.7
     //evaluate 213.6, 1.29, 0.7
     ...
   }
This method is only an approximation of what happens internally, but it gives faster rendering speeds with the majority of isosurfaces.

open When the isosurface isn't fully contained within the contained_by object, there will be a cross section. Where this happens, you will see the surface of the container. With the open keyword, these cross section surfaces are removed. The inside of the isosurface becomes visible.

Note: that open slows down the render speed. Also, it is not recommended to use it with CSG operations.

max_trace Isosurfaces can be used in CSG shapes since they are solid finite objects - if not finite by themselves, they are through the cross section with the container.
By default POV-Ray searches only for the first surface which the ray intersects. But when using an isosurface in CSG operations, the other surfaces must also be found. Therefore, the keyword max_trace must be added to the isosurface statement. It must be followed by an integer value. To check for all surfaces, use the keyword all_intersections instead.
With all_intersections POV-Ray keeps looking until all surfaces are found. With a max_trace it only checks until that number is reached.

6.5.4.2  Functions in Isosurface

The functions used to define the isosurface are written in the function{...} block.

Allowed are:

User defined functions (like equations). All float expressions and operators (see section "User-Defined Functions") which are legal in POV-Ray, can be used.
With the equation of a sphere "x^2+y^2+z^2 = Threshold" we get:

isosurface {
function {pow(x,2) + pow(y,2) + pow(z,2)}
  threshold Threshold
  ...
}

Functions can be declared first (see section "Declaring Functions") and then used in the isosurface.

#declare Sphere = function {pow(x,2) + pow(y,2) + pow(z,2)}
isosurface {
  function { Sphere(x,y,z) }
  threshold Threshold
  ...
}

By default a function takes three parameters (x,y,z) and you do not have to explicitly specify the parameter names when declaring it.
When using the identifier, the parameters must be specified.
On the other hand, if you need more or less than three parameters when declaring a function, you also have to explicitly specify the parameter names.

#declare Sphere = function(x,y,z,Radius) {
    pow(x,2) + pow(y,2) + pow(z,2) - pow(Radius,2) 
}
isosurface {
  function { Sphere(x,y,z,1) }
  ...
}

To make it easier for you, POV-Ray has a large amount of pre-defined functions. These are mainly algebraic surfaces but there is also a mesh function and noise3d function. See section "Internal Functions" for a complete list and some explanation on the parameters to use. These internal functions can be included through the functions.inc include file.
For the internal paraboloid shape, use:

#include "functions.inc"
isosurface {
  function  { f_paraboloid(x,y,z, -1) }
  ...
}

Since pigments can be declared as functions, they can also be used in isosurfaces. They must be declared first. When using the identifier, you have to specify which component of the color vector should be used. To do this, the dot notation is used: Function(x,y,z).red

#declare FBozo = function { 
    pigment { bozo color_map { [0 rgb 0] [1 rgb 1] }}
}
isosurface {
  function  { FBozo(x,y,z).gray }
  ...
}

A color vector has five components. Supported dot types to access these components are:

Conditional directives are allowed

#declare Rough = yes;
#include "functions.inc"
isosurface {
  function { y #if(Rough=1)-f_noise3d(x/0.5,y/0.3,z/0.4)*0.8 #end }
  ...
}

Loops can also be used in functions:

#include "functions.inc"
#declare Thr = 1/1000;
#declare Ang = radians(45);
#declare Offset = 1.5;
#declare Scale = 1.2;
#declare TrSph = function { f_sphere(x-Offset,y,z,0.7*Scale) }

function {
  (1-Thr)
  #declare A = 0;
  #while (A<8)
  -pow(Thr, TrSph(x*cos(A*Ang) + y*sin(A*Ang),
                  y*cos(A*Ang) -x*sin(A*Ang), z) )
    #declare A=A+1;
  #end
}

Of course functions can be combined and parameters can be substituted. Learn more about it in the next sections

6.5.4.3  Transformations on Functions

Transforming an isosurface object is done like transforming any POV-Ray object. Simply use the object modifiers (scale, translate, rotate, ...).

However, when you want to transform functions within the contained_by object, you have to substitute parameters in the functions.

The results seem inverted to what you would normally expect. Here is an explanation:
Take a Sphere(x,y,z). We know it sits at the origin because x=0. When we want it at x=2 (translating 2 units to the right) we need to write the second equation in the same form: x-2=0
Now that both equations equal 0, we can replace parameter x with x-2
So our Sphere(x-2, y,z) moves two units to the right.

Let's scale our Sphere 0.5 in the y direction. Default size is y=1 (one unit). We want y=0.5.
To get this equation in the same form as the first one, we have to multiply both sides by two. y*2 = 0.5*2, which gives y*2=1
Now we can replace the y parameter in our sphere: Sphere(x, y*2, z). This squishes the y-size of the sphere by half.
Well, this is the general idea of substitutions.

Here's an overview of some useful substitutions:
Using a declared object P(x,y,z)

6.5.4.3.1  Functions, Scale

scale x : replace "x" with "x/scale" (idem other parameters)

scale x*2   gives    P(x/2,y,z)
6.5.4.3.2  Scale Infinitely

scale x infinitely : replace "x" with "0" (idem other parameters)

scale y infinitely   gives    P(x,0,z)
6.5.4.3.3  Functions, Translate

translate x : replace "x" with "x - translation" (idem other parameters)

translate z*3   gives    P(x,y,z-3)
6.5.4.3.4  Functions, Shear

shear in XY-plane : replace "x" with "x + y*tan(radians(Angle))" (idem other parameters)

shear 45 degrees left   gives    P(x+y*tan(radians(45)), y, z)
6.5.4.3.5  Functions, Rotate

Note: these rotation substitutions work like normal POV-rotations: they already compensate for the inverse working

rotate around X
: replace "y" with "z*sin(radians(Angle)) + y*cos(radians(Angle))"
: replace "z" with "z*cos(radians(Angle)) - y*sin(radians(Angle))"

rotate around Y
: replace "x" with "x*cos(radians(Angle)) - z*sin(radians(Angle))"
: replace "z" with "x*sin(radians(Angle)) + z*cos(radians(Angle))"

rotate around Z
: replace "x" with "x*cos(radians(Angle)) + y*sin(radians(Angle))"
: replace "y" with "-x*sin(radians(Angle)) + y*cos(radians(Angle)) "

  rotate z*75   gives:
  P(x*cos(radians(75)) + y*sin(radians(75)),
    -x*sin(radians(75)) + y*cos(radians(75)), z)
 
6.5.4.3.6  Functions, Flip

flip X - Y : replace "x" with "y" and replace "y" with "-x"

flip Y - Z : replace "y" with "z" and replace "z" with "-y"

flip X - Z : replace "x" with "-z" and replace "z" with "x"

flip x and y   gives    P(y, -x, z)
6.5.4.3.7  Functions, Twist

twist N turns/unit around X
: replace "y" with "z*sin(x*2*pi*N) + y*cos(x*2*pi*N)"
: replace "z" with "z*cos(x*2*pi*N) - y*sin(x*2*pi*N)"

6.5.4.4  Combining Functions

CSG operations can be performed on isosurface objects since they are solid finite objects - if not finite by themselves, they are through the cross section with the container. This is done in the usual way.

However, when CSG-like operations on functions within the contained_by object are needed, functions have to be combined with the appropriate operators to do so.

Here's an overview of some useful combinations of functions:

6.5.4.4.1  Functions, Merge

A merge can be obtained with "min(A, B, ...)"

function{min(Function_A(x,y,z),Function_B(x,y,z))}
function{min(Function_A(x,y,z),Function_B(x,y,z),Function_C(x,y,z))}

6.5.4.4.2  Functions, Intersection

A way to do this : using "max(A,B,...)"

function{max(Function_A(x,y,z),Function_B(x,y,z))}
function{max(Function_A(x,y,z),Function_B(x,y,z),Function_C(x,y,z))}

6.5.4.4.3  Functions, Difference

A way to do this are: using "max(A, -(B-2*Threshold))"

function{max( Function_A(x,y,z), -(Function_B(x,y,z) -2*Threshold))}
threshold Threshold
If you are using the default threshold (=0) you don't need to subtract the 2*Threshold, since 2*0=0

6.5.4.4.4  Functions, Blob

Two possible ways to do this are:

  1.   using "(A*B)- Blob_threshold"
  2.   using "(1+Blob_threshold) -Blob_threshold^A -Blob_threshold^B"
function{ (Function_A(x,y,z) * Function_B(x,y,z)) -Blob_threshold)}
function{
  (1+Blob_threshold)
  -pow(Blob_threshold, Function_A(x,y,z))
  -pow(Blob_threshold, Function_B(x,y,z))
}

6.5.4.4.5  Functions, Negative Blob

use "A +(Blob_threshold ^(B + Strength)^(C + Strength))"

function{Function_A + pow(Blob_threshold,(Function_B + Strength))}

6.5.4.4.6  Functions, Blend

use "A + B" or "A - B"
This produces a kind of blend of the two functions.

  function { Function_A +  Function_B }

6.5.4.5  Improving Isosurface Speed

Rendering speed of isosurfaces can vary considerably depending on the used settings. Usually they render quite fast when the settings are optimized. Here are some rules to keep in mind when designing isosurfaces:

6.5.4.5.1  Using Accuracy
Setting accuracy 0.1 (default value is 0.001) is a good value to start with. You will not see a difference on isosurfaces with a gradually changing surface but it will render more than two times faster.
A higher accuracy can be needed when the surface has sudden changes or a higher frequency of changes on parts that face the camera (and when the accuracy of these details matter). You may possibly need a higher accuracy when doing a trace on the isosurface or when the surface should match the function very closely.
But usually the accuracy can be set lower than the default with no noticeable differences on the isosurface.

6.5.4.5.2  Container
Make sure your contained_by 'object' fits as tightly as possible. An oversized container can sky-rocket the render time.
When the container has a lot of empty space around the actual isosurface, POV-Ray has to do a lot of superfluous sampling: especially with complex functions this can become very time consuming. On top of this, the max_gradient needed to get a proper surface will also increase rapidly (almost proportional to the oversize!).
You could use a transparent copy of the container (using exactly the same transformations) to check how it fits. Getting the min_extent and max_extent of the isosurface isn't useful because it only gives the extent of the container and not of the actual isosurface.

6.5.4.5.3  Maximum Gradient

It is important to specify a correct max_gradient value. When it is set too high, it slows down rendering. On the other hand, when set too low, the surface may not render properly, showing artefacts or holes. So, find the real maximum gradient and use it. POV-Ray will warn you when a bad max_gradient is used. It is usually safe to use the measured value printed in the warning as new max_gradient value but it can happen that this value is insufficient.

When you use evaluate, POV-Ray uses a dynamic calculation method for the max_gradient value. This can help to achieve a faster rendering than with a correct max_gradient in some situations. Optimizing the evaluate parameters always means balancing between artefacts and slow calculation. It requires a lot of patience and some experience to find the best values.