Extensible 3D (X3D)
Part 1: Architecture and base components

27 NURBS component

--- X3D separator bar ---

cube 27.1 Introduction

27.1.1 Name

The name of this component is "NURBS". This name shall be used when referring to this component in the COMPONENT statement (see 7.2.3.4 Component statement).

27.1.2 Overview

This subclause describes the Non-uniform Rational B-Spline (NURBS) component of this part of ISO/IEC 19775. Table 27.1 provides links to the major topics in this subclause.

Table 27.1 — Topics in this subclause

cube 27.2 Concepts

27.2.1 Overview of NURBS

Non-uniform Rational B-Splines provide a convenient and efficient manner to generate curved lines and surfaces which can be smooth at any viewing distance. Since these surfaces are generated parametrically, only a small amount of data need be provided for describing complex surfaces.

27.2.2 NURBS-related nodes

The characteristics of a NurbsSurface and a NurbsCurve are determined by a set of control points (control vertices, CV) similar to an ElevationGrid. These points are approximated to a certain degree that is defined in the weight value of every CV. The whole surface can be seen as the weighted average of all control points with the control points having only strong influence in their periphery. The range of the influence is determined by the knot vector.

There are many surface construction techniques including:

  1. special cases of NURBS surfaces such as sphere, cylinder or Bezier surfaces;
  2. Extrusion / swept surfaces, constructed given a spine and a cross section curve which both can be NURBS curves;
  3. surfaces of revolution, constructed given a circle/arc and a NURBS cross section curve;
  4. skinned surface constructed from a set of curves;
  5. Gordon surfaces interpolating two sets of curves;
  6. Coons patches, a bicubic blended surface constructed from 4 border curves;
  7. Surfaces interpolating a set of points.

For this standard, it is assumed that creation of such surfaces is only a construction step at authoring time and that the surface will be represented as a general NurbsSurface for X3D runtime delivery.

27.2.3 Tessellation strategies

Because low-level real-time rendering systems currently can handle only planar triangles, a NURBS surface needs to be broken down (i.e., tessellated) into a set of triangles approximating the true surface.

Tessellation can be done in different coordinate spaces:

  1. Tessellation in object space and the internal computation of the equivalent to an X3D IndexedFaceSet.
  2. Transforming the control vertices to screen space, and tessellation in screen space

There are different methods to determine tessellation points on the surface:

  1. fixed tessellation based on a absolute number of subdivisions;
  2. adaptive tessellation based on chord length;
  3. adaptive tessellation based on the angle between two triangles;
  4. view dependent tessellation, fine tessellation near silhouette edges.

This standard does not specify which method is used to tessellate the surface. However, the implementation shall render the NURBS such that the approximation produces a rendered image which in which the edges of the tessellation can not be perceived.

It should be noted that tessellation in screen space requires the ability to pass already transformed vertices for rendering. This requires the application to already light the vertices (see 17, Lighting component) and pass the resulting color and specular RGB values for each vertex of a triangle.

In order to avoid cracks at the junction of two surfaces, tessellation values of a whole set of surfaces can be specified in a NurbsGroup.

27.2.4 Automatic level of detail

By taking advantage of the totally flexible tessellation of NURBS models, new ways of  LOD can occur. Dependent on various parameters, the output of the tesselation process is adapted in every frame. This trade off between quality of the rendered model and the frame-rate can take into account:

  1. required target frame rate;
  2. available triangle budget per frame/object;
  3. number of triangles of the last frame/object;
  4. available CPU performance;
  5. distance of object to viewer;
  6. number of current visible NurbsSurfaces;
  7. area of control point grid projected to screen space / area of the bounding box of the control point grid projected to screen space.

It is not required that such automatic LOD be supported.

27.2.5 Trimmed NURBS

The trimming curve specifies a NURBS-curve that limits the NURBS surface in order to create NURBS surfaces that contain holes or have smooth boundaries. Trimming curves are curves in the parametric space of the surface. The following defines trimming:

A trimming region is defined by a set of closed trimming loops in the parameter space of a surface. When a loop is oriented counter-clockwise, the area within the loop is retained, and the part outside is discarded. When the loop is oriented clockwise, the area within the loop is discarded, and the rest is retained. Loops may be nested, but a nested loop must be oriented oppositely from the loop that contains it. The outermost loop must be oriented counter-clockwise.

A trimming loop consists of a connected sequence of NURBS curves and piece-wise linear curves. The last point of every curve in the sequence must be the same as the first point of the next curve, and the last point of the last curve must be the same as the first point of the first curve. Self intersecting curves are not allowed.

27.2.6 Using NURBS for animation

NURBS can be simply animated through alteration of single control vertices. Thereby the NURBS surface will always keep its smoothness. By changing the order of the surface, the impact of the CV-animation on adjacent control points and so the range of the animation can be changed.

NURBS are also applicable for the animation of values using smooth curves expressed in NURBS format. An adaption of the  PositionInterpolator node type to a NURBS description leads to the NurbsPositionInterpolator.

Extending the concept NurbsCurve (one Parameter), NurbsSurface (two parameters) to the parametric dimension 3 results in a NurbsVolume. So given a (u,v,w) parameter as input a 3D (x,y,z) output can be computed. The CoordinateDeformer defines a volume and applies a space warp to this volume. Any given conventional X3D node can be deformed by this node.

cube 27.3 Abstract types

27.3.1 X3DParametricGeometryNode

X3DParametricGeometryNode : X3DGeometryNode {
}

This abstract node type is the base type for all geometry node types that are created parametrically.

cube 27.4 Node reference

27.4.1 Contour2D

Contour2D : X3DGroupingNode, X3DBoundedNode { 
  MFNode [in]     addChildren       [NurbsCurve|ContourPolyline2D]
  MFNode [in]     removeChildren    [NurbsCurve|ContourPolyline2D]
  MFNode [in,out] children       [] [NurbsCurve|ContourPolyline2D]
}

The Contour2D node groups a set of curve segments to a composite contour. The children shall form a closed loop with the first point of the first child repeated as the last point of the last child and the last point of a segment repeated as the first point of the consecutive one. The segments shall be defined either by NurbsCurve2D or ContourPolyline2D nodes and shall be enumerated in the child field in consecutive order according to the topology of the contour. 

Nested Contour2D nodes alternately specify invalid and valid regions depending on the clockwiseness of the contours. Additionally, each Contour2D may contain multiple, non-intersecting Contour2D nodes. This allows specification of multiple invalid or valid regions within a single outerlying valid or invalid region. See 27.2.5 Trimmed NURBS for a description of how clockwiseness is computed.

27.4.2 ContourPolyline2D

ContourPolyline2D : X3DParametricGeometryNode {
  MFVec2f [in,out] point [] (-∞,∞);
}

The ContourPolyline2D node defines a piecewise linear curve segment as a part of a trimming contour in the u-v domain of a surface.

point specifies the end points of the piecewise linear curve.

ContourPolyline2D are used as children to the Contour2D group.

27.4.3 CoordinateDeformer

CoordinateDeformer : X3DGroupingNode, X3DBoundedNode { 
  MFNode   [in]     addChildren             [X3DShapeNode]
  MFNode   [in]     removeChildren          [X3DShapeNode]
  MFNode   [in,out] children       []       [X3DShapeNode]
  MFVec3f  [in,out] controlPoint   [] 
  MFNode   [in,out] inputCoord     []       [Coordinate]
  MFNode   [in,out] inputTransform []       [Transform]
  MFNode   [in,out] outputCoord    []       [Coordinate]
  MFFloat  [in,out] weight         []       (-∞,∞)
  SFVec3f  []       bboxCenter     0 0 0    (-∞,∞)
  SFVec3f  []       bboxSize       -1 -1 -1 (0,∞) or -1 -1 -1
  SFInt32  []       uDimension     0        (0,∞)
  MFDouble []       uKnot          []       (-∞,∞)  
  SFInt32  []       uOrder         2        (2,∞)
  SFInt32  []       vDimension     0        (0,∞)
  MFDouble []       vKnot          []       (-∞,∞)
  SFInt32  []       vOrder         2        (2,∞)
  SFInt32  []       wDimension     0        (0,∞)
  MFFloat  []       wKnot          []       (-∞,∞)
  SFInt32  []       wOrder         2        (2,∞)
}

CoordinateDeformer allows a free form deformation on a set of MFVec3f Coordinate nodes by using a NURBS volume. Conceptually, a set of input Coordinate nodes is placed into a non-uniform grid volume. If the grid control points are animated or deformed the output Coordinate nodes are updated accordingly.

The input to the deformer is a list of Coordinate nodes, where each Coordinate node defines a 3D parameter (u,v,w) for evaluation. The corresponding Cartesian output value is computed from the NURBS control grid. The children node contains a scene graph that typically consists of IndexedFaceSet nodes referring to a deformed Coordinate node in outputCoords.

The uDimension, vDimension, wDimension, controlPoint, weight, uKnot, vKnot, wKnot, uOrder, vOrder, and wOrder fields define the NURBS in three dimensions. The definition is similar to the NurbsSurface node.

The inputCoord field, if specified, shall contain a set of Coordinate nodes.

The outputCoord field, shall contain a set of Coordinate nodes. The number of nodes shall be equal to the number of nodes in inputCoord. The nodes themselves should be distinct from nodes in inputCoords.

The inputTransform field, if specified, shall contain a set of Transform nodes, the number of nodes shall be equal to the number of nodes in inputCoord.

By animating the controlPoint field, IndexedFaceSet nodes using a Coordinate node from outputCoords are deformed over time. Similarly, the point field in an input Coordinate node can be animated. By changing a Transform node in the inputTransform parameter, geometry can be moved through the deformation space (space warp).

CoordinateDeformer is a group node and shall be part of the transform hierarchy if evaluation is required. Points in the Coordinate node contained in outputCoord are recomputed and updated whenever the points of the inputCoord Coordinate are changed, any exposedField of the CoordinateDeformer itself is changed, or if any of the supplied inputTransform nodes are changed. Implementations may defer or even skip evaluation until the CoordinateDeformer node is displayed; i.e., if the node is not currently part of the traversed scene graph, or the node is not being rendered because the bounding box of the node (or the bounding box computed from the controlPoint list) falls outside the view frustum.

In some respects, CoordinateDeformer is a special version of a CoordinateInterpolator. The CoordinateDeformer is a group node in order to make the animation locatable in the scene graph at a certain 3D position. Normally, VRML interpolators do not define a bounding box and so are not culled from the scene. This behaviour may be achieved explicitly by routing the output of a VisibilitySensor to the controlling TimeSensor node.

The following is an example of this node:

DEF FFD CoordinateDeformer {
  controlPoint [ ..... ]
  inputCoord  DEF inputCoord  
    Coordinate  { point [ ...] }
      outputCoord DEF outputCoord 
    Coordinate { point [] } 
      children Shape { 
        geometry IndexedFaceSet {
          coord USE outputCoord 
          coordIndex [ .... ]
       } 
   } 
 }
    ... # additional code to animate the FFDGrid goes here 

DEF Timer TimeSensor {}
DEF FFDGridInterpolator CoordinateInterpolator { ..... }
ROUTE FFDGridInterpolator.value_changed TO FFD.set_controlPoint
ROUTE Timer.fraction_changed O FFDGridInterpolator.set_fraction

If a given input coordinate value (optionally transformed  by the matrix of the corresponding Transform node) exceeds the parametric range of one of the knot vectors, the corresponding output coordinate value will be left unchanged. This is useful to deform only a subset of the coordinates or to animated different parts of the coordinate node by different CoordinateDeformerGroup nodes.

The following pseudo- code formulates the operation of this node:

function PerformDeformation() {
  for (n=0; n<inputCoords.length;i++) {
   Deform(inputCoord[i].point,MatrixOf(inputTransform[i]),outputCoord[i].point);
  }
}

function Deform(MFVec3f input, Matrix m, MFVec3f output)
  if (output.length < input.length) output.length = input.length;
  for (i=0; i<input.length;i++) {
    SFVec3f parameter = m * input[i];
    if (ParameterInRange(parameter)) {
      output[i] = NurbsEvaluate3(parameter);
    }
  } 
}
  Vec3f NurbsEvaluate3(SFVec3f parameter)
    -- evaluate standard NURBS formula

27.4.4 NurbsCurve

NurbsCurve : X3DParametricGeometryNode {
  MFVec3f  [in,out] controlPoint [] (-∞,∞)
  SFInt32  [in,out] tessellation 0  (-∞,∞)
  MFDouble [in,out] weight       [] (0,∞)
  MFDouble []       knot         [] (-∞,∞)
  SFInt32  []       order        3  [2,∞)
}

The NurbsCurve node is a geometry node defining a parametric curve.

Order defines the order of curve. From a mathematical point of view, the curve is defined by a polynomial of the degree order-1. The value of Order shall be greater than or equal to 2. An implementation may limit Order to a certain number. The most common orders are 3 (quadratic polynomial) and 4 (cubic polynomial), which are sufficient to achieve the desired curvature in most cases. The number of control points shall be at least equal to the order of the curve. The order defines the number of adjacent control points that influence a given control point.

The control points define a piecewise linear curve, where the points do not have a uniform spacing. Depending on the weight value and the order, this piecewise linear curve is approximated by the resulting parametric curve. The number of control points shall be equal to or greater than the order. The control points are all defined as 3D vertices in the x, y, z domain. A closed B-Spline curve can be specified by repeating the limiting control points and by specifying a periodic knot vector.

A weight value that shall be greater than zero is assigned to each controlPoint. The ordering of the values is equivalent to the ordering of the control point values. If the weight of a control point increased above 1, the point is more closely approximated by the curve. However the curve is not changed if all weights are multiplied by a common factor. The number of values shall be identical to the number of control points. If the length of the weight vector is 0, the default weight 1.0 is assumed for each control point.

As a result of the lack of a 4D Coordinate field type in VRML, the control points and the corresponding weight values are held in separate fields. This separation also allows independent animation of the controlPoint fields using a CoordinateInterpolator node.

knots defines the knot vector. The number of knots shall be equal to the number of control points plus the order of the curve. The order shall be non-decreasing. By setting successive knot values equal, the degree of continuity is decreased, which implies that the curve has corners. In general, the curve is of continuity Ck-1-m at a knot point, where k is the order and m is the number of consecutive knots being equal. If k is the order of the curve, k consecutive knots at the end or the beginning of the vector cause the curve to interpolate the last or the first control point respectively. Within the knot vector there may not be more than k-1 consecutive knots of equal value. If the length of a knot vector is 0, a default uniform knot vector is computed.

The tessellation field gives a hint to the curve tessellator by setting an absolute number of subdivision steps. These values shall be greater than or equal to the Order field. A value of 0 indicates that the browser choose a suitable tessellation. Interpretation of values below 0 is implementation dependent.

For an implementation subdividing the surface into an equal number of subdivision steps, tessellation values are interpreted in the following way:

  1. if a tessellation value is greater than 0, the number of tessellation points is
    tessellation+1;

  2. if a tessellation value is smaller than 0, the number of tessellation points is
    (-tessellation × (number of control points)+1);

  3. if a tessellation value is 0, the number of tessellation points is
    (2 × (number of control points)+1.

For implementations doing tessellations based on chord length, tessellation values  less than zero are interpreted as the maximum chord length deviation in pixels. Implementations doing fully automatic tessellation may ignore the tessellation hint parameters.

27.4.5 NurbsCurve2D

NurbsCurve2D : X3DParametricGeometryNode {
  MFVec2f  [in,out] controlPoint [] (-∞,∞)
  SFInt32  [in,out] tessellation 0  (-∞,∞)
  MFDouble [in,out] weight       [] (0,∞)
  MFDouble []       knot         [] (-∞,∞)
  SFInt32  []       order        3  [2,∞)
}

The NurbsCurve2D node defines a trimming segment that is part of a trimming contour in the u-v domain of the surface. If the NurbsCurve2D forms a closed contour, it may be used as a Contour2D node.

In other respects, the NurbsCurve2D has the same functionality as defined for the NurbsCurve node.

27.4.6 NurbsGroup

NurbsGroup : X3DGroupingNode, X3DBoundedNode {
  MFNode  [in]     addChildren                [NurbsSurface]
  MFNode  [in]     removeChildren             [NurbsSurface]
  MFNode  [in,out] children          []       [NurbsSurface]
  SFFloat [in,out] tessellationScale 1.0      (0,∞)
  SFVec3f []       bboxCenter        0 0 0    (-∞,∞)
  SFVec3f []       bboxSize          -1 -1 -1 (0,∞)|[-1 -1 -1]
}

The NurbsGroup node groups a set of NurbsSurface nodes to a common group. This provides a hint to the browser to treat the set of NurbsSurface as a unit during tessellation to enforce tessellation continuity along borders. The tessellationScale parameter is scaling the tessellation values in lower level NurbsSurface nodes. If a set of NurbsSurfaces use a matching set of controlPoints along the borders, this results in a common tessellation stepping. Otherwise, the NurbsGroup behaves as a normal Group node.

27.4.7 NurbsPositionInterpolator

NurbsPositionInterpolator : X3DInterpolatorNode { 
  SFFloat  [in]     set_fraction     (-∞,∞)
  SFInt32  [in,out] dimension        0  (-∞,∞) 
  SFBool   [in,out] fractionAbsolute TRUE 
  MFVec3f  [in,out] keyValue         [] [0,1]
  MFDouble [in,out] keyWeight        [] (-∞,∞)
  MFDouble [in,out] knot             [] (-∞,∞)  
  SFInt32  [in,out] order            3  (2,∞)
  SFVec3f  [out]    value_changed
}

NurbsPositionInterpolator describes a 3D NURBS Curve using dimension, keyValue, keyWeight, knot, and order as described for the NurbsCurve node.

The fields set_fraction and value_changed have the same meaning as in the PositionInterpolator.

Sending a set_fraction input computes a 3D position on the curve, which is sent by value_changed. The set_fraction value is used as the input value for the tessellation function. Thereby, the knot corresponds to the key field of a conventional interpolator node; i.e., if the set_fraction value is within [0,1] and the knot vector within [0,2], only half of the curve is computed.

27.4.8 NurbsSurface

NurbsSurface : X3DParametricGeometryNode { 
  MFVec3f  [in,out] controlPoint  []   (-∞,∞)
  SFNode   [in,out] texCoord      []   [X3DTextureCoordinateNode]
  SFInt32  [in,out] uTessellation 0    (-∞,∞)
  SFInt32  [in,out] vTessellation 0    (-∞,∞)
  MFDouble [in,out] weight        []   (0,∞)
  SFBool   []       ccw           TRUE
  SFBool   []       solid         TRUE
  SFInt32  []       uDimension    0    [0,∞)
  MFDouble []       uKnot         []   (-∞,∞)
  SFInt32  []       uOrder        3    [2,∞)
  SFInt32  []       vDimension    0    [0,∞)
  MFDouble []       vKnot         []   (-∞,∞)
  SFInt32  []       vOrder        3    [2,∞)
}

uDimension and vDimension define the number of control points in the u and v dimensions.

uOrder and vOrder define the order of the surface. From a mathematical point of view, the surface is defined by polynomials of the degree order-1. The order of the curves uOrder and vOrder must be greater or equal to 2. An implementation may limit uOrder and vOrder to a certain number. The most common orders are 3 (quadratic polynomial) and 4 (cubic polynomial), which are sufficient to achieve the desired curvature in most cases. The number of control points must be at least equal to the order of the curve. The order defines the number of adjacent control points that influence a given control point.

controlPoint defines a set of control points of dimension uDimension × vDimension. This set of points defines a mesh  where the points do not have a uniform spacing. Depending on the weight-values and the order, this hull is approximated by the resulting surface. uDimension points define a polyline in u-direction followed by further u-polylines with the v-parameter in ascending order. The number of control points shall be equal or greater than the order. A closed B-Spline surface can be specified by repeating the limiting control points.

The control  vertex corresponding to the control point P[i, j] on the control grid is :

    P[i,j].x = controlPoints[i + ( j × uDimension)].x
    P[i,j].y = controlPoints[i + ( j × uDimension)].y
    P[i,j].z = controlPoints[i + ( j × uDimension)].z
    P[i,j].w = weight[ i + (j × uDimension)]

    where 0 ≤ i < uDimension and 
          0 ≤ j < vDimension.

A weight value that shall be greater than zero is assigned to each controlPoint. The ordering of the values is equivalent to the ordering of the control point values. If the weight of a control point increased above 1 the point is closer approximated by the surface. The number of values shall be identical to the number of control points. If the length of the weight vector is 0, the default weight 1.0 is assumed for each control point.

The control points and the corresponding weight values are held in separate fields. This separation allows independent animation of the controlPoint fields using a CoordinateInterpolator node.

uKnots and vKnots define the knot vector. The number of knots shall be equal to the number of control points plus the order of the curve. The order shall be non-decreasing. By setting successive knot values equal, the degree of continuity is decreased resulting in creases in the surface. If k is the order of the curve, k consecutive knots at the end or the beginning of the vector lets the curve converge to the last or the first control point respectively. Within the knot vector there may be not more than k-1 consecutive knots of equal value. If the length of a knot vector  is zero,  a default uniform knot vector is computed.

uTessellation and vTessellation provide hints to the surface tessellator, u/v Tessellation ≥ u/v. Order sets an absolute number of subdivision step, 0 allows the browser to choose a suitable tessellation. Interpretation of values below 0 are implementation dependent.

For an implementation subdividing the surface in a equal number of subdivision steps, tessellation values could be interpreted in the following way:

For implementations doing tessellations based on chord length, tessellation values less than zero are interpreted as the maximum chord length deviation in pixels. Implementations doing fully automatic tessellation may ignore the tessellation hint parameters.

texCoord provides additional information on how to generate texture coordinates. By default, texture coordinates in the unit square are generated automatically from the parametric subdivision. A NurbsSurfaceTextureCoordinate node or simply a TextureCoordinate node can then be used to compute a texture coordinate given a u/v parameter of the NurbsSurface. NurbsSurfaceTextureCoordinate would also allow for non-animated surfaces to specify a chord-length based texture coordinate parametrization.

ccw and solid are defined like in other X3D geometry nodes. solid TRUE enables two-sided lighting, the surface is visible from both sides, and normals are flipped toward the viewer, prior to shading.

27.4.9 NurbsTextureSurface

NurbsTextureSurface : X3DTextureCoordinateNode { 
  MFVec2f  [in,out] controlPoint [] (-∞,∞)
  MFFloat  [in,out] weight       [] (0,∞)
  SFInt32  []       uDimension   0  [0,∞)
  MFDouble []       uKnot        [] (-∞,∞)
  SFInt32  []       uOrder       3  [2,∞)
  SFInt32  []       vDimension   0  [0,∞)
  MFDouble []       vKnot        [] (-∞,∞)
  SFInt32  []       vOrder       3  [2,∞)
}

The NurbsTextureSurface node is a NURBS surface existing in the parametric domain of its surface host specifying the mapping of the texture onto the surface.

The parameters are as specified for the NurbsSurface node with the exception that the control points are specified in (u, v) coordinates.

The tessellation process generates 2D texture coordinates. If the NurbsTextureSurface is undefined, texture coordinates are computed by the client on the basis of parametric step size. Conventional vertex parameters do not apply on NURBS because triangles are only available after polygonalization, but the conventional texture transform may be used.

NurbsTextureSurface nodes are accessed through the texCoord field of the NurbsSurface node. A NurbsTextureSurface node separately encountered is ignored.

27.4.10 TrimmedSurface

TrimmedSurface : X3DParametricGeometryNode { 
  MFNode [in]     addTrimmingContour         [Contour2D]
  MFNode [in]     removeTrimmingContour      [Contour2D]
  SFNode [in,out] surface               NULL [NurbsSurface]
  MFNode [in,out] trimmingContour       []   [Contour2D]
}

The TrimmedSurface node defines a NURBS surface that is trimmed by a set of trimming loops. The outermost trimming loop shall be defined in a counterclockwise direction.

The surface field shall contain a NurbsSurface node that is to be trimmed.

The trimmingContour field, if specified, shall contain a set of Contour2D nodes. Trimming loops shall be processed as described for the Contour2D node.

cube 27.5 Support levels

The Non-uniform rational B-spline (NURBS) component provides 2 levels of support as specified in Table 27.2.

Table 27.2 — NURBS component support levels

Level Prerequisites Nodes/Features Support
1 Core 1
Grouping 1
Geometric property 1
Interpolator 1
Texturing 1
X3DParametricGeometryNode (abstract) n/a
CoordinateDeformer All fields
NurbsCurve All fields
NurbsGroup All fields
NurbsPositionInterpolator All fields
NurbsSurface All fields
NurbsTextureSurface All fields
2 Core 1
Grouping 1
Geometric property 1
Interpolator 1
Texturing 1
All Level 1 NURBS nodes As supported in Level 1
Contour2D All fields
NurbsCurve2D All fields
ContourPolyline2D All fields
TrimmedSurface All fields
--- X3D separator bar ---