Renderer3d の使用

Curl® 言語は、Renderer3d の使用に 2 つのアプローチを提供します。この章で前述した例では、render-primitive マクロおよび Primitive クラスを含む、下位レベルのより強力なアプローチを使用しています
もう 1 つのアプローチでは、たとえば Renderer3d.render-lineRenderer3d.render-ellipse など、名前がすべて 'render-' で始まる Renderer3d クラス メソッドのセットを使用します。これらのヘルパー メソッドは、render-primitive の機能のサブセットを提供します。

Primitiverender-primitive を使用したレンダリング

render-primitive マクロは、Primitive オブジェクトのレンダリングに強力かつ下位レベルのインターフェイスを提供します。これは、指定した型の Primitive オブジェクトを暗黙的に作成します。render-primitive を参照してください。
Primitive クラスは、Primitive の頂点を指定できるアクション メソッド、およびそれらの頂点のプロパティを指定できるプロパティ メソッドを提供します。Primitive の型によって、render-primitive が提供された頂点をどのように解釈して Primitive オブジェクトを作成するかが決まります。Primitive を参照してください。各頂点について、複数のプロパティ メソッドを呼び出すことができます。各頂点のプロパティ呼び出しは、その頂点を指定するアクション呼び出しより前に行なわれます。
プロパティ メソッドは、以下のカテゴリに分けられます。
アクション メソッドは、'vertex' で始まります。アクション メソッドは、それに先立つプロパティ メソッド呼び出しに関連付けられる頂点を指定します。プロパティ メソッドおよびアクション メソッドの全リストについては、『API リファレンス マニュアル』の Primitive の項目を参照してください。
次の例は、Renderer3d、および render-primitivetexture-coord アクション メソッドと組み合わせて使用して、テクスチャ マッピングした三角形をレンダリングします。

例: テクスチャ座標を指定した render-primitive の使用
{import * from CURL.GRAPHICS.RENDERER3D}
{define-proc {primitive-proc
                 rg:Renderer3dGraphic,
                 ren:Renderer3d,
                 area:#RectangleSet}:void
    let d:Drawable={non-null rg.drawable}
    let fp:FillPattern =
        {FillPattern.from-url {url "curl://install/docs/default/images/adria.jpg"}}
    set ren.texture = {fp.to-Texture}
    {render-primitive
        p:Primitive,
        type=Primitive.triangles
        on ren do
    {p.texture-coord2 0.0, 0.0}
    {p.vertex2 0in, 1in}
    {p.texture-coord2 1.0, 0.0}
    {p.vertex2 1in, 0in}
    {p.texture-coord2 1.0, 1.0}
    {p.vertex2 1in, 2in}

    {p.texture-coord2 0.0, 0.0}
    {p.vertex2 1in, 0in}
    {p.texture-coord2 1.0, 0.0}
    {p.vertex2 1in, 2in}
    {p.texture-coord2 1.0, 1.0}
    {p.vertex2 2in, 1in}
   }
}
||create a Renderer3dGraphic,
||passing that procedure as repaint-handler
{Renderer3dGraphic repaint-handler=primitive-proc}
       

Renderer3d.render-* メソッドによるレンダリング

Renderer3d.render-* メソッドは、Renderer3d を使用したより高位レベルの手段を提供します。以下のリストにあるメソッドは、文字および文字列のレンダリング、およびピクスマップまたは描画対象の別の描画対象へのレンダリングなど、render-primitive ではそれほど簡単に利用できない機能を提供します。
次のリストにあるメソッドは、render-primitive を通して利用できる総合的な機能のサブセットを提供します。render-primitive ブロックの代わりにこれらを使用できます。.
次の例では前の例で行ったレンダリングと同じ方法で実行しますが、ここでは Renderer3d.render-vertices を使用しています。

例: render-vertices メソッドの使用
{import * from CURL.GRAPHICS.RENDERER3D}
||define a simple repaint handler procedure
{define-proc {primitive-proc
                 rg:Renderer3dGraphic,
                 ren:Renderer3d,
                 area:#RectangleSet}:void
    let d:Drawable={non-null rg.drawable}
    let vertices:{FastArray-of FloatDistance3d} =
        {new {FastArray-of FloatDistance3d},
            {FloatDistance3d 0f(in), 1f(in), 0f(in)},
            {FloatDistance3d 1f(in), 0f(in), 0f(in)},
            {FloatDistance3d 1f(in), 2f(in), 0f(in)},

            {FloatDistance3d 1f(in), 0f(in), 0f(in)},
            {FloatDistance3d 1f(in), 2f(in), 0f(in)},
            {FloatDistance3d 2f(in), 1f(in), 0f(in)}
        }
    let colors:{FastArray-of Pixel} =
        {new {FastArray-of Pixel},
            {Pixel.create 1, 0, 0, alpha = 0},
            {Pixel.create 1, 0, 0, alpha = 0},
            {Pixel.create 1, 0, 0, alpha = 0},
            {Pixel.create 0, 1, 0, alpha = 0},
            {Pixel.create 0, 1, 0, alpha = 0},
            {Pixel.create 0, 1, 0, alpha = 0}
        }

    {ren.render-vertices
        Primitive.triangles,
        vertices,
        color-array = colors
    }
}
||create a Renderer3dGraphic,
||passing that procedure as repaint-handler
{Renderer3dGraphic repaint-handler=primitive-proc}
       

陰面消去

3 次元空間でのオブジェクトのレンダリングには、その空間でのオブジェクトの位置、見る人のオブジェクトに対する位置、そして見る人が他のものの下になっているオブジェクトを見ることができるかどうかが関係します。
オブジェクトの配置には、オブジェクトの前面と背面の決定を伴います。頂点を指定する順序、つまり頂点のワインディング順は、右手の法則に従って、頂点によって定義される面の前面と背面の方向を決定します。右手の親指が前面を指しているとします。その他の指を曲げるときに指を動かす方向に頂点を指定します。自分がオブジェクトの前面に向いている場合、これは反時計回りになります。
Renderer3d は、cull-face-enabled? プロパティおよび cull-face プロパティを使用して、レンダリングする面を決定します。
以下に、背面ポリゴンのカリングの例を示します。赤い三角形の頂点は、画面に向かっているユーザーの視点から見て時計回りで指定されているので、その三角形は、背面に向いていると見なされます。cull-face-enabled?true に設定すると、Renderer3d は背面ポリゴンをレンダリングしません。
cull-face を既定値の Cull.back から Cull.true に変更した場合、Renderer3d は、代わりに緑の前面の三角形をカリングします。

例: cull-face-enabled? の使用
{import * from CURL.GRAPHICS.RENDERER3D}
{define-proc {primitive-proc
                 rg:Renderer3dGraphic,
                 ren:Renderer3d,
                 area:#RectangleSet}:void
    let d:Drawable={non-null rg.drawable}
    set ren.cull-face-enabled? = true
    ||Uncomment the following line to cull the front facing
    ||(green) face instead of the back facing (red) face
    ||set ren.cull-face = Cull.front
    {render-primitive
        p:Primitive,
        type=Primitive.triangles
        on ren do
        {p.color3 1.0, 0, 0} || red
        {p.vertex2 0in, 1in}
        {p.vertex2 1in, 0in}
        {p.vertex2 1in, 2in}

        {p.color3 0, 1.0, 0} || green
        {p.vertex2 1in, 0in}
        {p.vertex2 1in, 2in}
        {p.vertex2 2in, 1in}
    }
}
{Renderer3dGraphic repaint-handler=primitive-proc}
この例は、cull-face-enabled? のアクションを示していますが、これは現実的ではありません。3D レンダリングでは一般に、表面のカリングは、複雑な 3D オブジェクトで、背面に向きオブジェクトの前面を向いた部分によって覆い隠される面のレンダリングを避けるために使用されます。閉じた 3D 形状の場合、表面のカリングは、形状の現れ方に影響を与えませんが、特定のフレームにレンダリングされるポリゴンの数を減らすのでパフォーマンスが向上します。次のセクションでは、この問題とその他の 3D レンダリングの問題をさらに掘り下げます。

投影マトリックスとモデルビュー マトリックス

前のセクションの例は、Renderer3d を使用しているにもかかわらず、実際は 2 次元でレンダリングされます。例では、 (x,y) の 2 次元空間における点を指定できる Primitive.vertex2 アクション メソッドを使用しています。z 座標は 0 に設定されています。3 次元でレンダリングを行うには、z 座標を指定する Primitive.vertex3 などのアクション メソッドを使用する必要があります。
3D レンダリングを実行するには、透視投影と正投影のどちらを使用するか、またレンダリングするオブジェクトを見る仮想カメラの向きを合わせる方法を考える必要があります。これらの問題は、投影マトリックスおよびモデルビュー マトリックスによって扱われます。

投影マトリックス

投影マトリックスには、Renderer3d.projection-matrix からアクセスします。投影マトリックスの型は Matrix3dProjectionStack です。これは、レンダリングされるオブジェクトが可視である 3D ボリュームの境界を指定します。Matrix3dProjectionStack.ortho を呼び出すとき、レンダリング ボリュームは平行六面体で、その軸はカメラと位置合わせされています。同じサイズのオブジェクトは、ビューアから離れているものもビューアに近いものと同じサイズでレンダリングされます。
Matrix3dProjectionStack.frustum を呼び出すと、レンダリング ボリュームが錐台、つまり、角錐になります。同じサイズのオブジェクトは、ビューアの近くにあるものは、ビューアから離れているものよりも大きく表示されます。モデルビュー マトリックスは、投影される前に頂点を変換します。
次の例は、投影マトリックスの設定、および正投影と透視投影の違いを示しています。また、この例では Renderer3d.cleardepth 引数と、深度テストを示しています。これで赤い三角形が青い三角形の後ろに配置されます。z 座標の値を変更して、赤い三角形を青い三角形の前面に配置してみましょう。例では、モデルビュー マトリックスも使用しています。これについては、「モデルビュー マトリックス」で詳しく説明します。

例: 正投影と透視投影
{import * from CURL.GRAPHICS.RENDERER3D}

{let (r:Renderer3d, d:Drawable) =
    {Renderer3d.create-offscreen 10cm, 10cm, resolution=100dpi}}

{define-proc {draw3D what-projection:int=0}:void
    {r.clear color={Palette.get-black}, depth=1}
    {if what-projection == 0 then
        {r.projection-matrix.ortho
            -5cm,  5cm,  || X extents (left/right)
            5cm, -5cm,   || Y extents (bottom/top)
            -50cm,  50cm || Z extents (near/far)
        }
     else
        {r.projection-matrix.frustum
            -1.5cm,  1.5cm,  || X extents (left/right)
            1.5cm, -1.5cm,   || Y extents (bottom/top)
            2cm,  25cm || Z extents (near/far)
        }
    }
    {r.modelview-matrix.load-identity}
    {r.modelview-matrix.translate -5cm, -5cm, 0cm}
    {r.modelview-matrix.scale 1, 1, -1}
    set r.depth-test-enabled? = true
    set r.depth-function = Compare.less
    {render-primitive p:Primitive, type="triangles" on r do
        || render the first triangle (blue)
        {p.color3 0.0, 0.0, 1.0}
        {p.vertex3  0cm,  0cm, 5cm}
        {p.vertex3  0cm, 10cm, 5cm}
        {p.vertex3 10cm, 10cm, 5cm}

        || render the second triangle (red)
        {p.color3 1.0, 0.0, 0.0}
        {p.vertex3 10cm,  0cm, 15cm}
        {p.vertex3  0cm,  0cm, 15cm}
        {p.vertex3  0cm, 10cm, 15cm}
    }
}

{draw3D what-projection = 0}
{let ortho-backgrnd:Pixmap = {d.to-Pixmap}}
{draw3D what-projection = 1}
{let pers-backgrnd:Pixmap = {d.to-Pixmap}}

{spaced-hbox
    {VBox "Orthographic:",
        {value
            {Frame
                width=2in, height=2in,
                background=ortho-backgrnd
            }
        }
    },
    {VBox "Perspective (frustum):",
        {value
            {Frame
                width=2in, height=2in,
                background=pers-backgrnd
            }
        }
    }
}
{d.destroy}
       

モデルビュー マトリックス

モデルビュー マトリックスには、Renderer3d.modelview-matrix からアクセスします。モデルビュー マトリックスの型は Matrix3dStack です。これは、レンダリングされるモデルとそれを見る仮想カメラとの関係を指定します。仮想カメラは、まず原点 (0m, 0m, 0m) に配置され、負の z 軸に沿って構えられ、位置を合わせます。こうすることで、結果のイメージのでは、正の y 軸と一致します。モデルビュー変換は、モデルの変換またはカメラでの逆変換のどちらかと見なすことができます。たとえば、カメラを (0m, 0m, 5m) に配置したい場合、次のように呼び出します。
{renderer.modelview-matrix.translate 0m, 0m, -5m}
Matrix3dStack の平行移動、回転、スケールのメソッドの個々の呼び出しは、相対する効果を持っています。つまり、モデル (ワールド) を z 軸を中心に 10 度回転し、その後再び同じ呼び出しを同じ値で行うと、モデルは、最終的に z 軸を中心に 20 度回転します。これが予想した効果でない場合は、Matrix3dStack-of.load-identity メソッドを使用してマトリックスをリセットできます。
すべての頂点は、両方のマトリックスによって変換されます。テキスト、描画対象、ピクスマップのレンダリングは、特別なケースです。テキストを変換するには、Renderer3d.text-matrix および Renderer3d.text-projection-matrix を使用します。modelview-matrix は、render-pixmap でレンダリングされたピクスマップに適用されますが、回転は、方向ではなく位置に作用します。詳細については、Renderer3d.render-pixmap を参照してください。変換マトリックスは、Renderer3d.render-drawable には適用されません。これは単純にソース描画対象から出力先にコピーします。
Renderer3d では、投影またはモデルビュー マトリックスが指定されない場合は既定値が使用されます。
既定の投影マトリックスは、正投影です。Renderer3d は、Matrix3dProjectionStack.ortho メソッドを以下の引数で呼び出して、その原点が Drawable の左上隅と確実に一致するようにします。
{renderer.projection-matrix.ortho
    0in, width, || x extents, left and right
    height, 0in,|| y extents, bottom and top
    -1in,       || the near plane
    1in         || the far plane.
    || near and far are relative
    || to the eye position: 0, 0, 0
}
これは、(0in, 0in)Drawable の左上隅に関連付けられ、drawable.width, drawable.height が右下隅に関連付けられていることを意味します。つまり、Renderer3d 座標での 1 インチは、Drawable での 1 インチに対応します。
既定のモデルビュー マトリックスは、単位行列です。
モデルビュー マトリックスを使用して三角形を y 軸を中心に回転する例を次に示します。この例にはほかに注意すべき点がいくつかあります。

例: モデルビュー マトリックスを使用した三角形の回転
{import * from CURL.GRAPHICS.RENDERER3D}

{define-class public ExampleObject {inherits BaseFrame}
  field rotation-angle:Angle = 0deg
  field rg:Renderer3dGraphic
  field public _timer:Timer

  {constructor public {default ...}
    set self.rg =
        {Renderer3dGraphic
            repaint-handler=
            {proc {rg:Renderer3dGraphic,
                   ren:Renderer3d,
                   area:#RectangleSet
                  }:void
                {self.redraw-proc ren}
            }, ...}
    {self.add-internal self.rg}
    {construct-super ...}
    set self._timer =
        {self.rg.animate
            frequency=30fps,
            repeat = 0,
            {on TimerEvent do
                {self.rg.update-drawable}
            }
        }
  }
  {method {draw-a-triangle ren:Renderer3d}:void
    {render-primitive
        p:Primitive,
        type=Primitive.triangles
        on ren do
        {p.color3 1, 0, 0}
        {p.vertex3 -1in, 1in, 0in}

        {p.color3 0, 1, 0}
        {p.vertex3 0in, -1in, 0in}

        {p.color3 0, 0, 1}
        {p.vertex3 1in, 1in, 0in}
    }
  }
  {method {redraw-proc ren:Renderer3d}:void
    {ren.clear}
    {ren.projection-matrix.ortho
        -2in, 2in, || left, right
        -2in,2in,  || bottom, top
        0in, 3in   || "near" and "far" are distances from the eye
    }
    {ren.modelview-matrix.load-identity}
    || Next line moves the model away from
    || the eye position (which is 0, 0, 0)
    {ren.modelview-matrix.translate 0in, 0in, -2in}

    {ren.modelview-matrix.rotate {Direction3d 0, 1, 0}, self.rotation-angle}
    ||    {ren.modelview-matrix.rotate {Direction3d 1, 0, 0}, self.rotation-angle}
    ||    {ren.modelview-matrix.rotate {Direction3d 0, 0, 1}, self.rotation-angle}

    {self.draw-a-triangle ren}
    || Increment or decrement the angle by 1 deg so that the object spins
    {if self.rotation-angle >= 0deg then
        {inc self.rotation-angle, 1deg}
     else
        {dec self.rotation-angle, 1deg}
    }
  }
}
{value
    let example-obj:ExampleObject = {ExampleObject width = 2in, height = 2in}
    example-obj
    let switch:CommandButton =
        {CommandButton
            label="Start",
            width=0.75in,
            {on Action do
                {if example-obj._timer.repeat == 0 then
                    set example-obj._timer.repeat = -1
                    set switch.label = "Stop"
                 else
                    set example-obj._timer.repeat = 0
                    set switch.label = "Start"
                }
            }
        }
    {VBox example-obj,
        switch
    }
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             

モデルビュー マトリックス スタックのプッシュとポップ

モデルビュー マトリックス スタックは、任意のスタックと同じように、push および pop メソッドを使って操作できます。これは、階層モデルでは便利です。
変換は、常にスタックの一番上のマトリックスを修正します。
モデルビュー マトリックス スタックでの pushpop の呼び出しの典型的な例は、以下のように進行します。
  1. 単位行列をロードします。
  2. それに対して変換を実行して、ワールドをカメラの外に配置します。
  3. それに対して push を呼び出し、現在のモデルビュー マトリックスのクローンを作成して、それをスタックにプッシュします。
  4. 変換を実行して、モデルの一部を描画したい位置を取得します(これは、スタックの一番上のクローン化されたマトリックスを修正します)。
  5. Renderer3d のプロパティを設定します。
  6. Primitive のプロパティを設定します。
  7. モデルの一部を描画します。
  8. モデルビュー マトリックス スタックで pop を呼び出し、修正されていたマトリックスを廃棄し、手順 3 より前の状態に戻します。
  9. 手順 3 ~ 8 を繰り返して、モデルの他の部分を描画します。
照明のセクションでは、モデルビュー マトリックスも使用して、モデルのさまざまな部分をレンダリングする例もあります。

Renderer3d の例

次の例は、Renderer3d を使用して、正六面体を構成する 6 つの四角形をレンダリングし、その正六面体を y 軸で回転します。この例では、クラス SpinCube を定義します。これは、BaseFrame サブクラスであり、フィールドおよびグラフィカルな子として Renderer3dGraphic を持っています。
この例の全体の構造は、「モデルビュー マトリックスを使用した三角形の回転」と同様です。この例を検討するときに確認すべきことを次に示します。

例: スピンする正六面体のレンダリング
{import * from CURL.GRAPHICS.RENDERER3D}

|| Define a class SpinCube that "has a" Renderer3dGraphic
{define-class public SpinCube {inherits BaseFrame}
  field left-top-back-vertex:Distance3d = {Distance3d -1in,  1in, -1in}
  field right-top-back-vertex:Distance3d = {Distance3d 1in,  1in, -1in}
  field right-top-front-vertex:Distance3d = {Distance3d 1in,  1in,  1in}
  field left-top-front-vertex:Distance3d = {Distance3d -1in,  1in,  1in}
  field left-bottom-back-vertex:Distance3d = {Distance3d -1in, -1in, -1in}
  field right-bottom-back-vertex:Distance3d = {Distance3d 1in, -1in, -1in}
  field right-bottom-front-vertex:Distance3d = {Distance3d 1in, -1in,  1in}
  field left-bottom-front-vertex:Distance3d = {Distance3d -1in, -1in,  1in}

  field cube-angle:Angle = 0deg
  field _alpha:Fraction = 1.0
  field _face:Texture

  field rg:Renderer3dGraphic
  field public _timer:Timer

  {constructor public {default face:FillPattern={url "../../default/images/generic.gif"}, ...}
    set self._face = {face.to-Texture}
    set self.rg =
        {Renderer3dGraphic
            repaint-handler=
            {proc {rg:Renderer3dGraphic,
                   ren:Renderer3d,
                   area:#RectangleSet
                  }:void
                {self.redraw-proc ren}
            }, ...}
    {self.add-internal self.rg}
    {construct-super ...}
    set self._timer =
        {self.rg.animate
            frequency=30fps,
            repeat = 0,
            {on TimerEvent do
                {self.rg.update-drawable}
            }
        }
  }
  {method {draw-quad
              renderer:Renderer3d,
              vertex-0:Distance3d,
              vertex-1:Distance3d,
              vertex-2:Distance3d,
              vertex-3:Distance3d,
              color-0:Fraction3d,
              alpha:double
          }:void

    || Clamp the alpha channel to zero (fully transparent) if the
    || given parameter is less than zero.
    {if alpha < 0.0 then set alpha = 0.0}
    || Add 4 vertices
    {render-primitive
        p:Primitive,
        type=Primitive.quads
        on renderer do

        {p.color4 color-0.x, color-0.y, color-0.z, alpha}
        {p.texture-coord2 0, 1}
        {p.vertex3v vertex-3}

        {p.texture-coord2 1, 1}
        {p.vertex3v vertex-2}

        {p.texture-coord2 1, 0}
        {p.vertex3v vertex-1}

        {p.texture-coord2 0, 0}
        {p.vertex3v vertex-0}
    }
  }
  {method {redraw-proc renderer:Renderer3d}:void
    || Clear window graphic with background color
    {renderer.clear color={Color.from-rgb 0.8, 0.8, 0.8}}
    || uncomment the next line to see 'inside' the cube
    ||    set renderer.cull-face = Cull.front
    set renderer.cull-face-enabled? = true

    || The following calls enable transparency (alpha blending)
    set renderer.blend-enabled? = true
    set renderer.blend-src-function = Blend.src-alpha
    set renderer.blend-dst-function = Blend.one-minus-src-alpha
    || Render in perspective
    {renderer.projection-matrix.frustum
        -0.5in, 0.5in, || left, right
        -0.5in, 0.5in, || bottom, top
        || "near" and "far" are distances from the eye position (for frustum)
        1in, 10in
    }
    {renderer.modelview-matrix.load-identity}

    || This moves the model away from the eye position (which is 0, 0, 0)
    {renderer.modelview-matrix.translate 0in, 0in, -5in}

    || This rotates the whole cube along the y-axis by cube-angle
    {renderer.modelview-matrix.rotate {Direction3d 0, 1, 0}, self.cube-angle}

    || This rotates the whole cube along the z-axis by 30deg
    {renderer.modelview-matrix.rotate {Direction3d 0, 0, 1}, -45deg}

    || Sets the texture to use.
    set renderer.texture = self._face

    || Draw the front face of the cube, blended with a white background.
    {self.draw-quad
        renderer,
        self.left-bottom-front-vertex,
        self.left-top-front-vertex,
        self.right-top-front-vertex,
        self.right-bottom-front-vertex,
        {Fraction3d 1, 1, 1},
        self._alpha
    }
    || Draw the back face of the cube, blended with a yellow background.
    {self.draw-quad
        renderer,
        self.left-top-back-vertex,
        self.left-bottom-back-vertex,
        self.right-bottom-back-vertex,
        self.right-top-back-vertex,
        {Fraction3d 1, 1, 0},
        self._alpha
    }
    || Draw the bottom face of the cube, blended with a red background.
    {self.draw-quad
        renderer,
        self.left-bottom-back-vertex,
        self.left-bottom-front-vertex,
        self.right-bottom-front-vertex,
        self.right-bottom-back-vertex,
        {Fraction3d 1, 0, 0},
        self._alpha
    }
    || Draw the top face of the cube, blended with a green background.
    {self.draw-quad
        renderer,
        self.left-top-front-vertex,
        self.left-top-back-vertex,
        self.right-top-back-vertex,
        self.right-top-front-vertex,
        {Fraction3d 0, 1, 0},
        self._alpha
    }
    || Draw the right face of the cube, blended with a blue background.
    {self.draw-quad
        renderer,
        self.right-bottom-back-vertex,
        self.right-bottom-front-vertex,
        self.right-top-front-vertex,
        self.right-top-back-vertex,
        {Fraction3d 0, 0, 1},
        self._alpha
    }
    || Draw the left face of the cube, blended with a purple background.
    {self.draw-quad
        renderer,
        self.left-bottom-front-vertex,
        self.left-bottom-back-vertex,
        self.left-top-back-vertex,
        self.left-top-front-vertex,
        {Fraction3d 1, 0, 1},
        self._alpha
    }
    || Increment the angle by 1 deg so that the cube spins
    {if self.cube-angle >= 0deg then
        {inc self.cube-angle, 1deg}
     else
        {dec self.cube-angle, 1deg}
    }
  }
}
{value
    let my-cube:SpinCube = {SpinCube}
    let switch:CommandButton =
        {CommandButton
            label="Spin",
            width=0.75in,
            {on Action do
                {if my-cube._timer.repeat == 0 then
                    set my-cube._timer.repeat = -1
                    set switch.label = "Stop"
                 else
                    set my-cube._timer.repeat = 0
                    set switch.label = "Spin"
                }
            }
        }
    {spaced-vbox halign="center", my-cube,
        {HBox {Fill}, switch, {Fill}}
    }
}