Isosurface Object

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

- P0: the minimum max_gradient in the estimation process,
- P1: an over-estimating factor. This means that the max_gradient is multiplied by the P1 parameter.
- P2: an attenuation parameter (1 or less)

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:

- P0 : found max_gradient * min_factor
  'min_factor' being a float between 0 and 1 to reduce the `max_gradient` to a 'minimum max_gradient'. The ideal value for P0 would be the average of the found max_gradients, but we do not have access to that information.
  A good starting point is 0.6 for the min_factor
- P1 : sqrt(found max_gradient/(found max_gradient * min_factor))
  'min_factor' being the same as used in P0 this will give an over-estimation factor of more than 1, based on your minimum max_gradient and the found max_gradient.
- P2 : 1 or less
  0.7 is a good starting point.

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:

- F( ).`x` | F( ).`u` | F( ).`red`
  - to get the red value of the color vector
- F( ).`y` | F( ).`v` | F( ).`green`
  - to get the green value of the color vector
- F( ).`z` | F( ).`blue`
  - to get the blue value of the color vector
- F( ).`filter` | F( ).`t`
  - to get the filter value of the color vector
- F( ).`transmit`
  - to get the transmit value of the color vector
- F( ).`gray`
  - to get the gray value of the color vector
  - gray value = Red*29.7% + Green*58.9% + Blue*11.4%
- F( ).`hf`
  - to get the height_field value of the color vector
  - hf value = (Red + Green/255)*0.996093
  - the .hf operator is experimental and will generate a warning.

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.