
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 POVRay 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.
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...] }
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 POVRay samples for the surface of the function. This container can either be a
sphere or a box, both of which use the standard POVRay 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 POVRay 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
POVRay 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 POVRay 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
POVRay 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 POVRay 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 overestimation 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:
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.#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 POVRay 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
POVRay keeps looking until all surfaces are found.
With a max_trace
it only checks until that number is reached.
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
"UserDefined Functions") which are legal
in POVRay, 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, POVRay has a large amount of predefined 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:
x
 F( ).u
 F( ).red
y
 F( ).v
 F( ).green
z
 F( ).blue
filter
 F( ).t
transmit
gray
hf
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(xOffset,y,z,0.7*Scale) } function { (1Thr) #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
Transforming an isosurface object is done like transforming any POVRay 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: x2=0
Now that both equations equal 0, we can replace parameter x with x2
So our Sphere(x2, 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 ysize 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)
scale x : replace "x
" with "x/scale
" (idem other parameters)
scale x*2 gives P(x/2,y,z)
scale x infinitely : replace "x
" with "0
" (idem other parameters)
scale y infinitely gives P(x,0,z)
translate x : replace "x
" with "x  translation
" (idem other parameters)
translate z*3 gives P(x,y,z3)
shear in XYplane : replace "x
" with "x + y*tan(radians(Angle))
" (idem other parameters)
shear 45 degrees left gives P(x+y*tan(radians(45)), y, z)
Note: these rotation substitutions work like normal POVrotations: 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)
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)
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)
"
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 CSGlike 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:
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))}
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))}
A way to do this are: using "max(A, (B2*Threshold))
"
function{max( Function_A(x,y,z), (Function_B(x,y,z) 2*Threshold))} threshold ThresholdIf you are using the default threshold (=0) you don't need to subtract the 2*Threshold, since 2*0=0
Two possible ways to do this are:
(A*B) Blob_threshold
"(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)) }
use "A +(Blob_threshold ^(B + Strength)^(C + Strength))
"
function{Function_A + pow(Blob_threshold,(Function_B + Strength))}
use "A + B" or "A  B
"
This produces a kind of blend of the two functions.
function { Function_A + Function_B }
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:
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. 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.
accuracy
can be set lower than the default with no
noticeable differences on the isosurface.
contained_by
'object' fits as tightly as possible. An
oversized container can skyrocket the render time. max_gradient
needed to get a proper
surface will also increase rapidly (almost proportional to the oversize!).
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.
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. POVRay 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
, POVRay 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.
