English Русский Deutsch
preview
MQL5取引ツール(第24回):3Dカーブ、パンモード、ViewCubeナビゲーションによる奥行き感の向上

MQL5取引ツール(第24回):3Dカーブ、パンモード、ViewCubeナビゲーションによる奥行き感の向上

MetaTrader 5トレーディングシステム |
22 5
Allan Munene Mutiiria
Allan Munene Mutiiria

はじめに

3D二項分布ビューアに回転やズーム機能が備わっていても、奥行きを正確に表現するカーブ、シーンの注視点を移動する手段、そして視点を素早くリセットする機能がなければ、確率質量関数の形状を確認するには何度も手動で回転操作を繰り返す必要があります。また、中心から外れた領域は見づらく、標準ビューへ戻す際にも手探りでドラッグしなければなりません。本記事は、より高度な確率解析を実現するために、インタラクティブな3D統計ツールへ直感的なナビゲーション機能を追加したいMetaQuotes Language 5 (MQL5)開発者およびアルゴリズムトレーダーを対象としています。

前回の記事(第23回)では、MQL5の二項分布ビューアにDirectX3Dを統合し、2D表示と3D表示を切り替えられる機能に加え、回転・ズーム・自動フィットに対応したカメラ操作を実装しました。第24回となる今回は、さらにツールを強化し、確率質量関数の奥行き感を高めるセグメント化された3Dカーブ、注視点を移動できるパンモード、そしてアニメーション付きのカメラ遷移を備えたViewCubeを実装します。本記事では以下のトピックを扱います。

  1. 3Dカーブ、パンモード、ViewCubeフレームワークの仕組み
  2. MQL5での実装
  3. バックテスト
  4. 結論

記事を読み終える頃には、確率分布をより直感的に分析できる高度な3Dナビゲーション機能を備えたMQL5ツールが完成し、用途に応じて自由に拡張できるようになります。それでは始めましょう。


3Dカーブ、パンモード、ViewCubeフレームワークの仕組み

確率質量関数を3D空間内で単なる平面的な投影線として表現すると、3Dレンダリング本来の奥行き表現が失われてしまいます。そこで、隣接するデータ点を結ぶ方向に合わせてボックスプリミティブを配置したセグメント構造のチューブ状カーブを構築することで、カーブそのものに立体感を持たせます。これにより、各ビンの高さの違いを遠近法の中で自然に把握できるようになります。パンモードは別の課題を解決します。回転やズームだけではシーンは固定された注視点を中心に操作されるため、分布が中心から外れていたり試行回数が多かったりすると、注目したい領域が画面端へ寄ってしまいます。カメラ基準のベクトル演算によって注視点そのものを移動させることで、現在の視点角度やズーム倍率を維持したまま、シーン全体を自由に探索できます。ViewCubeは、現在のカメラ回転をリアルタイムに反映するコンパクトな方向表示ウィジェットです。キューブは面・辺・頂点ごとのクリック可能な領域に分割され、それぞれがあらかじめ定義された視点角度へ対応しています。クリックすると視点は瞬時に切り替わるのではなく、補間アニメーションによって滑らかに遷移するため、視覚的にも自然な操作感が得られます。

実際のマーケット分析では、奥行きを持つ3Dカーブとヒストグラムを組み合わせることで、確率質量関数のピークが最頻度ビンと一致しているかを視覚的に確認し、モデルが適切に較正されているかを判断できます。また、パン機能を利用して低確率領域(テール)を画面中央へ移動すれば、極端な結果に基づくポジションサイズの検討前に詳細な分析をおこなえます。さらに、ViewCubeの上面ビューでは全ビンの高さを一度に比較でき、正面ビューでは各ビンの値を読み取りやすくなります。

本記事では、ボックスベースのカーブセグメントを生成し、それらを適切なサイズ・回転で配置して連続したチューブ状カーブを構築します。また、カメラ基準のベクトル計算を用いたパン処理、投影頂点と面ソートによる正しい描画順序を備えたViewCubeのレンダリング、さらに細分化されたホバー領域を実現する双線形補間についても実装します。加えて、視点遷移のタイマー制御アニメーション、アイコンやViewCubeを描画するためのアルファブレンドオーバーレイ、クリック・ホバーイベント処理も統合し、ユーザーインターフェース全体をより応答性の高いものへ仕上げます。以下では、本記事で実現する機能をご覧ください。

3Dカーブ、パンモード、ViewCubeフレームワーク


MQL5での実装

パンモードおよびViewCube対応のための入力項目・定数・クラスメンバーの拡張

新たにパンモードとViewCubeウィジェットをサポートするため、まずViewCube背景の表示切り替え入力を追加します。続いて、アイコンやViewCubeのサイズを管理する定数を定義し、ビジュアライザクラスの保護メンバーへ、ホバー状態、カーブセグメント、パン操作、およびアニメーション管理に関する変数を追加します。さらに、コンストラクタですべての新規メンバーを適切に初期化し、デストラクタでは既存のシーンオブジェクトに加えて、新たに追加したカーブセグメント配列も確実に解放するよう更新します。

//+------------------------------------------------------------------+
//|    Canvas Graphing PART 3.2 - Statistical Distributions (3D).mq5 |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"
#property strict

input group "=== VIEW CUBE SETTINGS ==="
input bool             showViewCubeBackground = false;       // Show View Cube Background Panel

//--- Add new constants for pan icon and view cube dimensions

const int PAN_ICON_SIZE     = 24;   // Size of the pan mode toggle icon
const int PAN_ICON_MARGIN   = 6;    // Margin around the pan icon
const int VCUBE_SIZE        = 70;   // Pixel size of the view cube widget
const int VCUBE_MARGIN      = 6;    // Margin around the view cube widget

//--- Add new member variables in the "DistributionVisualizer" class for hovering states, curve segments, panning, animation, and view cube.

//+------------------------------------------------------------------+
//| Distribution visualization window class                          |
//+------------------------------------------------------------------+
class DistributionVisualizer
  {
protected:

   bool              m_isHoveringPanIcon;     // True when mouse is over the pan mode icon
   bool              m_isHoveringViewCube;    // True when mouse is over the view cube widget
   int               m_lastMouseX;            // Last recorded mouse X coordinate
   int               m_lastMouseY;            // Last recorded mouse Y coordinate
   int               m_previousMouseButtonState; // Mouse button state on previous event

   ViewModeType      m_currentViewMode;       // Active view mode (2D or 3D)
   bool              m_are3DObjectsCreated;   // True once 3D scene objects have been built

   CDXBox            m_histogramBars[];       // Array of 3D boxes representing histogram bars
   CDXBox            m_groundPlane;           // 3D box used as the ground reference plane
   CDXBox            m_axisX;                 // 3D box representing the X axis
   CDXBox            m_axisY;                 // 3D box representing the Y axis
   CDXBox            m_axisZ;                 // 3D box representing the Z axis
   CDXBox            m_curveSegments[];       // Array of 3D boxes representing PMF curve tube segments
   int               m_curveSegmentCount;     // Number of active curve segment boxes

   double            m_cameraDistance;        // Distance from camera to scene target
   double            m_cameraAngleX;          // Camera elevation angle (radians)
   double            m_cameraAngleY;          // Camera azimuth angle (radians)
   int               m_mouse3DStartX;         // Mouse X when 3D orbit drag began
   int               m_mouse3DStartY;         // Mouse Y when 3D orbit drag began
   bool              m_isRotating3D;          // True while user is orbiting the 3D scene
   bool              m_panMode;               // True when pan mode is active (vs orbit mode)
   bool              m_isPanning;             // True while a pan drag is in progress
   int               m_panStartX;             // Mouse X when pan drag began
   int               m_panStartY;             // Mouse Y when pan drag began
   DXVector3         m_viewTarget;            // World-space point the camera looks at

   double            m_sampleData[];                  // Raw binomial sample values
   double            m_histogramIntervals[];           // Histogram bin centre positions
   double            m_histogramFrequencies[];         // Scaled frequency per histogram bin
   double            m_theoreticalXValues[];           // X values for the theoretical PMF curve
   double            m_theoreticalYValues[];           // Y values for the theoretical PMF curve
   double            m_minDataValue;                   // Minimum value in data range
   double            m_maxDataValue;                   // Maximum value in data range
   double            m_maxFrequency;                   // Peak raw frequency across all bins
   double            m_maxTheoreticalValue;            // Peak theoretical PMF value
   bool              m_isDataLoaded;                   // True once distribution data is ready

   double            m_sampleMean;                     // Computed sample mean
   double            m_sampleStandardDeviation;        // Computed sample standard deviation
   double            m_sampleSkewness;                 // Computed sample skewness
   double            m_sampleKurtosis;                 // Computed sample excess kurtosis
   double            m_percentile25;                   // 25th percentile (Q1)
   double            m_percentile50;                   // 50th percentile (median)
   double            m_percentile75;                   // 75th percentile (Q3)
   double            m_confidenceInterval95Lower;      // Lower bound of 95% confidence interval
   double            m_confidenceInterval95Upper;      // Upper bound of 95% confidence interval
   double            m_confidenceInterval99Lower;      // Lower bound of 99% confidence interval
   double            m_confidenceInterval99Upper;      // Upper bound of 99% confidence interval

   double            m_targetAngleX;          // Target camera elevation angle for animation
   double            m_targetAngleY;          // Target camera azimuth angle for animation
   bool              m_isAnimating;           // True while a camera snap animation is running
   int               m_animSteps;             // Total number of animation interpolation steps
   int               m_animStep;              // Current animation step index
   double            m_animStartAngleX;       // Camera elevation angle at animation start
   double            m_animStartAngleY;       // Camera azimuth angle at animation start
   string            m_vcubeHoverZone;        // Name of the view cube zone under the cursor
   int               m_vcubeCenterX;          // Screen X of the view cube widget centre
   int               m_vcubeCenterY;          // Screen Y of the view cube widget centre

public:
   //+------------------------------------------------------------------+
   //| Initialise all member variables to safe defaults                 |
   //+------------------------------------------------------------------+
   DistributionVisualizer(void)
     {
      //--- Reset canvas hover flag
      m_isHoveringCanvas = false;
      //--- Reset header hover flag
      m_isHoveringHeader = false;
      //--- Reset resize zone hover flag
      m_isHoveringResizeZone = false;
      //--- Reset switch icon hover flag
      m_isHoveringSwitchIcon = false;
      //--- Reset pan icon hover flag
      m_isHoveringPanIcon = false;
      //--- Reset view cube hover flag
      m_isHoveringViewCube = false;
      //--- Reset last mouse X
      m_lastMouseX = 0;
      //--- Reset last mouse Y
      m_lastMouseY = 0;
      //--- Reset previous mouse button state
      m_previousMouseButtonState = 0;

      //--- Set view mode from input
      m_currentViewMode = viewMode;
      //--- Mark 3D objects as not yet created
      m_are3DObjectsCreated = false;

      //--- Set camera distance from input
      m_cameraDistance = initialCameraDistance;
      //--- Set camera elevation angle from input
      m_cameraAngleX = initialCameraAngleX;
      //--- Set camera azimuth angle from input
      m_cameraAngleY = initialCameraAngleY;
      //--- Reset 3D orbit drag origin X
      m_mouse3DStartX = -1;
      //--- Reset 3D orbit drag origin Y
      m_mouse3DStartY = -1;
      //--- Mark 3D orbit as inactive
      m_isRotating3D = false;
      //--- Start in orbit mode (not pan)
      m_panMode = false;
      //--- Mark pan drag as inactive
      m_isPanning = false;
      //--- Reset pan drag origin X
      m_panStartX = 0;
      //--- Reset pan drag origin Y
      m_panStartY = 0;
      //--- Initialise view target at scene origin
      m_viewTarget = DXVector3(0.0f, 0.0f, 0.0f);

      //--- Reset data range minimum
      m_minDataValue = 0.0;
      //--- Reset data range maximum
      m_maxDataValue = 0.0;
      //--- Reset peak histogram frequency
      m_maxFrequency = 0.0;
      //--- Reset peak theoretical PMF value
      m_maxTheoreticalValue = 0.0;
      //--- Mark data as not yet loaded
      m_isDataLoaded = false;

      //--- Reset sample mean
      m_sampleMean = 0.0;
      //--- Reset standard deviation
      m_sampleStandardDeviation = 0.0;
      //--- Reset skewness
      m_sampleSkewness = 0.0;
      //--- Reset kurtosis
      m_sampleKurtosis = 0.0;
      //--- Reset 25th percentile
      m_percentile25 = 0.0;
      //--- Reset median
      m_percentile50 = 0.0;
      //--- Reset 75th percentile
      m_percentile75 = 0.0;
      //--- Reset 95% CI lower bound
      m_confidenceInterval95Lower = 0.0;
      //--- Reset 95% CI upper bound
      m_confidenceInterval95Upper = 0.0;
      //--- Reset 99% CI lower bound
      m_confidenceInterval99Lower = 0.0;
      //--- Reset 99% CI upper bound
      m_confidenceInterval99Upper = 0.0;

      //--- Reset animation target elevation
      m_targetAngleX = 0.0;
      //--- Reset animation target azimuth
      m_targetAngleY = 0.0;
      //--- Mark animation as inactive
      m_isAnimating = false;
      //--- Set default animation step count
      m_animSteps = 20;
      //--- Reset current animation step
      m_animStep = 0;
      //--- Reset animation start elevation
      m_animStartAngleX = 0.0;
      //--- Reset animation start azimuth
      m_animStartAngleY = 0.0;
      //--- Clear view cube hover zone
      m_vcubeHoverZone = "";
      //--- Reset view cube centre X
      m_vcubeCenterX = 0;
      //--- Reset view cube centre Y
      m_vcubeCenterY = 0;
      //--- Reset curve segment count
      m_curveSegmentCount = 0;
     }

   //--- Update destructor to shutdown new curve segments array

   //+------------------------------------------------------------------+
   //| Release all 3D scene objects on destruction                      |
   //+------------------------------------------------------------------+
   ~DistributionVisualizer(void)
     {
      //--- Get total number of histogram bar boxes
      int count = ArraySize(m_histogramBars);
      //--- Release DirectX resources for every histogram bar
      for(int i = 0; i < count; i++)
         m_histogramBars[i].Shutdown();
      //--- Release DirectX resources for every curve tube segment
      for(int i = 0; i < m_curveSegmentCount; i++)
         m_curveSegments[i].Shutdown();
      //--- Release ground plane DirectX resources
      m_groundPlane.Shutdown();
      //--- Release X axis DirectX resources
      m_axisX.Shutdown();
      //--- Release Y axis DirectX resources
      m_axisY.Shutdown();
      //--- Release Z axis DirectX resources
      m_axisZ.Shutdown();
     }
   }

まず、新しいinputグループ 「=== VIEW CUBE SETTINGS ===」を追加し、既定値をfalseとするshowViewCubeBackgroundを導入します。これにより、ViewCubeオーバーレイの背景表示を切り替えられるようになります。続いて、パンアイコンおよびViewCube用の定数を定義します。具体的には、パン切り替えボタンのサイズと余白を指定するPAN_ICON_SIZEおよびPAN_ICON_MARGIN、ViewCubeのサイズと配置位置を指定するVCUBE_SIZEおよびVCUBE_MARGINを追加します。次に、DistributionVisualizerクラスのprotectedセクションを拡張し、インタラクション機能を強化するためのメンバー変数を追加します。ホバー状態を検出するm_isHoveringPanIconおよびm_isHoveringViewCube、3Dカーブを管理するm_curveSegments配列とm_curveSegmentCount、パン操作の状態を管理するm_panMode、m_isPanning、m_panStartX、m_panStartY、カメラの注視点を保持するDXVector3型のm_viewTarget、アニメーションの目標角度を保持するm_targetAngleXおよびm_targetAngleYを追加します。さらに、滑らかな視点遷移を実現するため、m_isAnimating、m_animSteps、m_animStep、m_animStartAngleX、m_animStartAngleYを追加します。また、ViewCube上で検出された領域を保持するm_vcubeHoverZoneと、ViewCubeの画面上の中心座標を保持するm_vcubeCenterXおよびm_vcubeCenterYも追加します。

コンストラクタでは、これらの新しいメンバーを初期化します。m_isHoveringPanIconとm_isHoveringViewCubeはfalse、m_curveSegmentCountは0、m_panModeとm_isPanningはfalse、m_panStartXとm_panStartYは0に設定します。また、m_viewTargetは原点ベクトル、目標角度は0.0、m_isAnimatingはfalse、m_animStepsは20、m_animStepは0、開始角度は0.0、m_vcubeHoverZoneは空文字列、ViewCubeの中心座標は0に初期化します。デストラクタも更新し、新たに追加したm_curveSegments配列を適切に解放します。具体的には、m_curveSegmentCountの数だけループ処理をおこない、各セグメントに対してShutdownを呼び出します。これにより、既存のバー、平面、座標軸のクリーンアップに加え、3Dカーブのリソースも確実に解放されます。これらの準備が整ったら、続いて3Dコンテキストの初期化処理を更新し、新しいメンバー変数を利用するよう変更します。変更箇所が分かりやすいよう、該当部分をハイライトしています。

動的な注視点に対応する3Dコンテキスト初期化の更新

ここでの変更は1か所のみです。ViewTargetSetに渡していた固定の原点ベクトルを、m_viewTargetメンバー変数へ置き換えます。これにより、パン操作によってm_viewTargetが更新されるたびに、その変更内容がカメラの再配置へ即座に反映されるようになります。

//+------------------------------------------------------------------+
//| Set up projection, lighting and initial camera for 3D rendering  |
//+------------------------------------------------------------------+
bool initialize3DContext()
  {
   //--- Set perspective projection matrix with 30-degree FOV
   m_mainCanvas.ProjectionMatrixSet((float)(DX_PI / 6.0),
                                    (float)m_currentWidth / (float)m_currentHeight,
                                    0.1f, 1000.0f);
   //--- Point the camera at the current view target
   m_mainCanvas.ViewTargetSet(m_viewTarget);
   //--- Define world up direction as positive Y
   m_mainCanvas.ViewUpDirectionSet(DXVector3(0.0f, 1.0f, 0.0f));
   //--- Set directional light colour to near-white
   m_mainCanvas.LightColorSet(DXColor(1.0f, 1.0f, 1.0f, 0.9f));
   //--- Set ambient light colour for soft fill
   m_mainCanvas.AmbientColorSet(DXColor(0.6f, 0.6f, 0.6f, 0.5f));

   //--- Auto-fit camera if enabled and data is already available
   if(autoFitCamera && m_isDataLoaded)
      autoFitCameraPosition();
   //--- Recompute and apply the camera transform
   updateCameraPosition();

   Print("SUCCESS: 3D context initialized");
   return true;
  }

コンテキストの更新が完了したら、次は平面的に投影されたスプラインを置き換えるため、3Dカーブセグメントを作成します。

3Dカーブセグメントの作成

確率質量関数をシーン上へ単なる平面の線として投影するのではなく、隣接する理論データ点に沿って配置・回転させた一連のボックスプリミティブを生成し、それらを連結して連続したチューブ状の3Dカーブを構築します。これにより、カーブ自体に立体的な奥行きが生まれ、各ビン間の高さの違いを遠近法に沿って自然に把握できるようになります。

//+------------------------------------------------------------------+
//| Allocate one DXBox tube segment per PMF curve interval           |
//+------------------------------------------------------------------+
bool create3DCurveSegments()
  {
   //--- Get the total number of PMF sample points
   int numPoints = ArraySize(m_theoreticalXValues);
   Print("DEBUG: create3DCurveSegments called, numPoints=", numPoints);
   //--- Need at least two points to form a segment
   if(numPoints < 2) return false;

   //--- Shutdown any previously created segments before rebuilding
   for(int i = 0; i < m_curveSegmentCount; i++)
      m_curveSegments[i].Shutdown();

   //--- One segment per adjacent pair of PMF points
   m_curveSegmentCount = numPoints - 1;
   ArrayResize(m_curveSegments, m_curveSegmentCount);

   //--- Decompose PMF curve colour into RGB byte components
   uchar cr = (uchar)((theoreticalCurveColor)       & 0xFF);
   uchar cg = (uchar)((theoreticalCurveColor >> 8)  & 0xFF);
   uchar cb = (uchar)((theoreticalCurveColor >> 16) & 0xFF);

   //--- Create and configure each tube segment box
   for(int i = 0; i < m_curveSegmentCount; i++)
     {
      //--- Create unit box; actual transform applied in update step
      if(!m_curveSegments[i].Create(m_mainCanvas.DXDispatcher(), m_mainCanvas.InputScene(),
                                    DXVector3(0.0f, -0.5f, -0.5f),
                                    DXVector3(1.0f,  0.5f,  0.5f)))
        {
         Print("ERROR: Failed to create curve segment ", i);
         return false;
        }
      //--- Set the segment diffuse colour from the curve colour input
      m_curveSegments[i].DiffuseColorSet(DXColor(cr / 255.0f, cg / 255.0f, cb / 255.0f, 1.0f));
      //--- Add a specular highlight to the tube surface
      m_curveSegments[i].SpecularColorSet(DXColor(0.3f, 0.3f, 0.3f, 0.5f));
      //--- Set specular shininess exponent
      m_curveSegments[i].SpecularPowerSet(32.0f);
      //--- Add a faint self-emission for visibility in shadowed areas
      m_curveSegments[i].EmissionColorSet(DXColor(cr / 255.0f * 0.25f,
                                                   cg / 255.0f * 0.25f,
                                                   cb / 255.0f * 0.25f, 1.0f));
      //--- Register the segment with the 3D scene
      m_mainCanvas.ObjectAdd(GetPointer(m_curveSegments[i]));
     }

   Print("DEBUG: Created ", m_curveSegmentCount, " curve segments successfully");
   return true;
  }

ここでは、理論上の確率質量関数カーブをチューブ状に表現する一連の3Dボックスプリミティブを生成するcreate3DCurveSegments関数を定義します。ここでいうチューブ状アプローチとは、カーブを連続するボックス形状のセグメントとして表現し、それらを接続することで一本の連続したチューブのように見せる手法です。これにより、3Dシーン内でカーブに奥行きと滑らかな連続性を与えることができます。本記事では、この表現方法が最もシンプルかつ実装しやすいと判断して採用していますが、必要に応じて別の実装方法を使用しても構いません。まず、m_theoreticalXValuesに対してArraySizeを呼び出し、理論データ点の数を取得するとともに、デバッグメッセージを出力します。データ点が2点未満であればセグメントを生成できないため、falseを返して処理を終了します。続いて、m_curveSegmentsに格納されている既存のセグメントをループ処理で順番にShutdownし、m_curveSegmentCountをデータ点数から1を引いた値に設定した後、ArrayResize関数を使用して配列サイズを変更します。

次に、theoreticalCurveColorからビット演算によってRGB成分を取得します。その後、各セグメントに対するループ内で、DXディスパッチャ、入力シーン、および単位サイズの円柱に近い形状を表すベクトル寸法を指定して、ボックスのCreateメソッドを呼び出します。作成に失敗した場合はエラーメッセージを出力し、falseを返します。生成したボックスにはマテリアルを設定します。DiffuseColorSetでは正規化したRGB値を使用して拡散色を設定し、SpecularColorSetでは鏡面反射色を指定して光沢を与えます。さらに、SpecularPowerSetを32.0fに設定してハイライトの鋭さを調整し、EmissionColorSetではわずかに色味を加えた発光色を設定して視認性を高めます。最後に、各セグメントへのポインタをObjectAddへ渡してキャンバスへ追加し、デバッグメッセージを出力して、すべて正常に完了したらtrueを返します。

このセグメント方式では、カーブを複数のチューブ状セグメントの集合として近似的に表現します。各ボックスは後の処理で実際のカーブ上のデータ点に合わせて変換されるため、複雑なジオメトリを生成することなく、回転や奥行き表現を実現できます。これにより、3D空間内における確率の滑らかな変化を効率良く可視化しながら、高い描画パフォーマンスを維持できます。セグメントの生成が完了したら、次は実際のカーブデータに合わせて各セグメントの位置と向きを更新します。

カーブデータに合わせたセグメント変換の更新

単位サイズのプリミティブとして生成した各セグメントに対し、この関数ではカスタム変換行列を計算して適用します。これにより、各セグメントは隣接する確率質量関数のデータ点間の距離に合わせて適切な長さへ拡大・縮小され、データ点間の方向に合わせて回転し、さらに3Dシーン内の正しい位置へ配置されます。

//+------------------------------------------------------------------+
//| Reposition and orient every PMF curve tube segment to match data |
//+------------------------------------------------------------------+
void update3DCurveSegments()
  {
   //--- Abort if data or segments are not ready
   if(!m_isDataLoaded || m_curveSegmentCount == 0) return;

   //--- Compute data ranges for world-space mapping
   double rangeX = m_maxDataValue - m_minDataValue;
   double rangeY = m_maxTheoreticalValue;
   if(rangeX == 0) rangeX = 1;
   if(rangeY == 0) rangeY = 1;

   //--- Match the total scene width used for the histogram bars
   float totalWidth  = 30.0f;
   float offsetX     = -totalWidth / 2.0f;
   float barSpacing  = totalWidth / (float)histogramCells;
   float bw          = barSpacing * 0.8f;
   //--- Tube radius controls the 3D thickness of each curve segment
   float tubeRadius  = 0.2f;
   //--- Place the curve in front of the histogram bars along Z
   float zPos        = bw / 2.0f + 0.5f;

   //--- Limit debug logging to the first call only
   static bool firstDebug = true;

   //--- Build and apply a custom transform matrix for each segment
   for(int i = 0; i < m_curveSegmentCount; i++)
     {
      //--- Map PMF X and Y values to 3D world space coordinates
      float x1 = offsetX + (float)((m_theoreticalXValues[i]     - m_minDataValue) / rangeX * totalWidth);
      float y1 = (float)(m_theoreticalYValues[i]     / rangeY * 15.0);
      float x2 = offsetX + (float)((m_theoreticalXValues[i + 1] - m_minDataValue) / rangeX * totalWidth);
      float y2 = (float)(m_theoreticalYValues[i + 1] / rangeY * 15.0);

      //--- Compute the segment direction vector components
      float dx = x2 - x1;
      float dy = y2 - y1;
      //--- Compute segment length for normalisation
      float segLen = (float)MathSqrt(dx * dx + dy * dy);
      if(segLen < 0.0001f) segLen = 0.0001f;
      //--- Normalise the direction vector
      float dirX = dx / segLen;
      float dirY = dy / segLen;

      //--- Log the first segment only for diagnostics
      if(firstDebug && i == 0)
         Print("DEBUG curve seg0: x1=", x1, " y1=", y1, " x2=", x2, " y2=", y2,
               " len=", segLen, " z=", zPos);

      //--- Build a custom transform that scales, rotates and translates the unit box
      //--- The transform rows encode: [right, up, depth, translation]
      DXMatrix transform;
      transform.m[0][0] = dirX * segLen;      transform.m[0][1] = dirY * segLen;      transform.m[0][2] = 0.0f;         transform.m[0][3] = 0.0f;
      transform.m[1][0] = -dirY * tubeRadius; transform.m[1][1] = dirX * tubeRadius;  transform.m[1][2] = 0.0f;         transform.m[1][3] = 0.0f;
      transform.m[2][0] = 0.0f;               transform.m[2][1] = 0.0f;               transform.m[2][2] = tubeRadius;   transform.m[2][3] = 0.0f;
      transform.m[3][0] = x1;                 transform.m[3][1] = y1;                 transform.m[3][2] = zPos;         transform.m[3][3] = 1.0f;

      //--- Apply the combined transform to this curve segment
      m_curveSegments[i].TransformMatrixSet(transform);
     }
   //--- Suppress first-call debug logging after the first pass
   firstDebug = false;
  }

ここでは、確率質量関数カーブを構成する各3Dボックスセグメントの位置と向きを動的に更新するupdate3DCurveSegments関数を定義します。この関数により、各セグメントがデータ点に沿って正しく配置され、チューブ状の3Dカーブとして描画されるため、分布の奥行きをより直感的に把握できるようになります。まず、データが読み込まれていない場合、またはセグメントが存在しない場合は処理を終了します。続いて、最小値による保護をおこないながらX軸とY軸の範囲を計算し、ヒストグラムと位置を揃えるために同じ全体幅を設定します。その後、オフセット、バー間隔、バー幅、チューブ半径を算出し、バーの前方に配置するz座標を設定することで、描画時の前後関係を適切に保ちます。次に、m_curveSegmentCountの数だけループ処理をおこない、x1、y1、x2、y2の座標を3D空間へスケーリングして、高さ15.0f以内に収まるよう変換します。その後、座標差分から方向ベクトルを計算し、セグメント長で正規化します。この際、ゼロ除算を防ぐためにセグメント長には極小値を下限として設定します。また、初回実行時のみ、staticフラグを利用して最初のセグメントのデバッグ情報を出力できるようにしています。

続いて、単位サイズのボックスをチューブ状セグメントへ変換するため、DXMatrixによる変換行列を構築します。1行目には方向ベクトルに沿った長さ方向のスケーリングを設定し、2行目には半径方向の厚みを表す回転成分を設定します。3行目では奥行き方向の半径を設定し、4行目ではzPosを含む開始位置への平行移動を設定します。完成した変換行列をTransformMatrixSetで適用することで、各セグメントを3D空間内の正しい位置と向きへ正確に配置できます。最後に、初回実行後はデバッグ用フラグをリセットします。これにより、平面的だったカーブデータは回転可能な立体構造へ変換され、分布全体における確率の変化を奥行きとともに視覚的に把握しやすくなります。カーブセグメントの生成と更新の両方が実装できたので、次はこれらを既存のシーン構築処理へ組み込みます。

カーブセグメントの生成と更新をシーンパイプラインへ組み込む

3か所の変更を加えることで、新しいカーブ機能を既存の描画フローへ統合します。まず、データがすでに読み込まれている場合は、初期オブジェクト生成時にカーブセグメントを作成します。次に、3Dモードへ切り替えた際、セグメントが存在しなければ新たに生成します。最後に、分布データが再読み込みされるたびに、ヒストグラムバーの更新とあわせてカーブセグメントも更新するようにします。

//--- Update "create3DObjects" to attempt creating curve segments if data is loaded

//--- Attempt to create PMF curve tube segments (non-fatal if it fails)
if(m_isDataLoaded && !create3DCurveSegments())
   Print("WARNING: Failed to create 3D curve segments");

//--- Update "setup3DMode" to create and update curve segments if needed

//--- Build curve segments if they were not created during init
if(m_isDataLoaded && m_curveSegmentCount == 0)
   create3DCurveSegments();

//--- Update "loadDistributionData" to update curve segments in 3D mode

if(m_curveSegmentCount == 0)
   create3DCurveSegments();
//--- Reposition every curve segment in 3D space
update3DCurveSegments();

コンパイルすると、3Dカーブセグメントがヒストグラムバーとともに次のように表示されます。

3Dカーブセグメント

3Dカーブの実装が完了したので、次はパンモードのオン・オフを3Dビュー上から直接切り替えられるパンアイコンオーバーレイを追加します。

パンモード切り替えアイコンの描画

ユーザーがパンモードを有効化できる分かりやすい操作部を提供するため、表示モード切り替えボタンのすぐ下に円形のアイコンを描画します。パンモードが有効なときはアイコンを緑色で塗りつぶし、マウスカーソルが重なると色を暗く変化させます。また、ドラッグによるパン操作を直感的に示すため、アルファブレンドされたピクセル描画を用いて、四方向の矢印付き十字アイコンを表示します。

//+------------------------------------------------------------------+
//| Draw the pan mode toggle button overlaid on the 3D canvas        |
//+------------------------------------------------------------------+
void drawPanIconOverlay()
  {
   //--- Pan icon is only drawn in 3D mode
   if(m_currentViewMode != VIEW_3D_MODE) return;

   //--- Compute icon position below the header on the right side
   int iconX = m_currentWidth  - PAN_ICON_SIZE - PAN_ICON_MARGIN;
   int iconY = HEADER_BAR_HEIGHT + PAN_ICON_MARGIN;
   int cx    = iconX + PAN_ICON_SIZE / 2;
   int cy    = iconY + PAN_ICON_SIZE / 2;
   int rad   = PAN_ICON_SIZE / 2;

   //--- Compute icon background colour based on interaction state
   color iconBgColor;
   if(m_panMode)
      iconBgColor = clrGreen;                         // Active pan mode: green
   else if(m_isHoveringPanIcon)
      iconBgColor = DarkenColor(themeColor, 0.1);     // Hover: slightly darker
   else
      iconBgColor = LightenColor(themeColor, 0.5);    // Default: lighter shade

   uint argbBg     = ColorToARGB(iconBgColor, 220);
   uint argbBorder = ColorToARGB(themeColor, 255);
   uint argbWhite  = ColorToARGB(clrWhite, 255);

   //--- Fill the circular icon background
   for(int py = cy - rad; py <= cy + rad; py++)
      for(int px = cx - rad; px <= cx + rad; px++)
        {
         int dx = px - cx;
         int dy = py - cy;
         if(dx * dx + dy * dy <= rad * rad)
            blendPixelSet(m_mainCanvas, px, py, argbBg);
        }

   //--- Draw the circular icon border ring
   for(int py = cy - rad; py <= cy + rad; py++)
      for(int px = cx - rad; px <= cx + rad; px++)
        {
         int dx = px - cx;
         int dy = py - cy;
         int distSq = dx * dx + dy * dy;
         if(distSq <= rad * rad && distSq >= (rad - 1) * (rad - 1))
            blendPixelSet(m_mainCanvas, px, py, argbBorder);
        }

   //--- Draw the four-arrow pan icon symbol in white
   int arrLen  = 4;
   int arrHead = 2;
   //--- Draw the horizontal crosshair bar
   for(int i = -arrLen; i <= arrLen; i++)
      blendPixelSet(m_mainCanvas, cx + i, cy, argbWhite);
   //--- Draw the vertical crosshair bar
   for(int i = -arrLen; i <= arrLen; i++)
      blendPixelSet(m_mainCanvas, cx, cy + i, argbWhite);
   //--- Draw the four arrowheads at each end of the crosshair
   for(int i = 0; i <= arrHead; i++)
     {
      blendPixelSet(m_mainCanvas, cx + arrLen - i, cy - i, argbWhite); // Right arrow top
      blendPixelSet(m_mainCanvas, cx + arrLen - i, cy + i, argbWhite); // Right arrow bottom
      blendPixelSet(m_mainCanvas, cx - arrLen + i, cy - i, argbWhite); // Left arrow top
      blendPixelSet(m_mainCanvas, cx - arrLen + i, cy + i, argbWhite); // Left arrow bottom
      blendPixelSet(m_mainCanvas, cx - i, cy - arrLen + i, argbWhite); // Up arrow left
      blendPixelSet(m_mainCanvas, cx + i, cy - arrLen + i, argbWhite); // Up arrow right
      blendPixelSet(m_mainCanvas, cx - i, cy + arrLen - i, argbWhite); // Down arrow left
      blendPixelSet(m_mainCanvas, cx + i, cy + arrLen - i, argbWhite); // Down arrow right
     }
  }

drawPanIconOverlay関数を定義し、3Dビュー内でパンモードを切り替える円形アイコンを描画します。このアイコンは、PAN_ICON_SIZEやPAN_ICON_MARGINなどの定数を使用し、ヘッダーの下側に配置されます。まず、背景色を選択します。m_panModeが有効な場合は緑色を使用し、ホバー状態の場合はDarkenColorを利用してthemeColorを暗くします。それ以外の場合はLightenColorによって明るくした色を使用します。その後、部分的な透明度を持つARGB形式へ変換します。ボタン本体を描画するため、正方形の描画領域内をループ処理し、円の半径内に含まれるピクセルに対してblendPixelSetを使用して塗りつぶします。また、境界線については外側のリング部分を判定し、themeColorのARGB値を使用して輪郭を描画します。

アイコン内部のグラフィックでは、矢印の長さと矢印先端のサイズを設定します。中央を通る水平方向および垂直方向のラインを、白色ARGB値とblendPixelSetを使用して描画します。さらに、両端に三角形状の矢印先端を追加し、オフセットしたピクセルをブレンドすることで、パン操作を直感的に示す十字型の矢印アイコンを形成します。 このオーバーレイは、3Dシーンの描画後にレンダリングされるため、DirectX出力へ干渉することなく常に最前面へ表示されます。関数を呼び出すと、パンオーバーレイは次のように描画されます。

パンオーバーレイ

パンオーバーレイの実装が完了したので、次はカメラの向きを反映し、クリック可能なナビゲーション領域を提供するViewCubeオーバーレイを構築します。

ViewCubeオーバーレイの投影と描画

ViewCubeは2Dオーバーレイとして描画します。まず、単位立方体の8つの頂点を現在のカメラ角度に基づいて投影します。次に、正しい遮蔽処理をおこなうため、6つの面を奥から手前の順番へソートします。表示されている各面は、クリック可能な3×3のサブゾーンへ分割します。各領域は双線形補間を使用して生成し、それぞれのサブ四角形へ明るさベースのシェーディングを適用します。さらに、方向確認用の基準として、投影された原点から色付きの軸ラインを描画します。

//+------------------------------------------------------------------+
//| Project a 3D view cube vertex into 2D screen coordinates         |
//+------------------------------------------------------------------+
void vcubeProject(double x, double y, double z, double angX, double angY, int &sx, int &sy)
  {
   //--- Pre-compute trigonometric values for both rotation angles
   double cosAX = MathCos(angX), sinAX = MathSin(angX);
   double cosAY = MathCos(angY), sinAY = MathSin(angY);
   //--- Rotate the point around the X axis (elevation)
   double y2 = y * cosAX - z * sinAX;
   double z2 = y * sinAX + z * cosAX;
   //--- Rotate the result around the Y axis (azimuth)
   double x2 = x * cosAY + z2 * sinAY;
   //--- Apply a fixed orthographic scale relative to the cube widget size
   double scale = VCUBE_SIZE * 0.30;
   //--- Map to screen pixels relative to the view cube centre
   sx = m_vcubeCenterX + (int)(x2 * scale);
   sy = m_vcubeCenterY - (int)(y2 * scale);
  }

//+------------------------------------------------------------------+
//| Draw and label the interactive 3D view cube overlay widget       |
//+------------------------------------------------------------------+
void drawViewCubeOverlay()
  {
   //--- View cube is only drawn in 3D mode
   if(m_currentViewMode != VIEW_3D_MODE) return;

   //--- Compute the view cube bounding area below the pan icon
   int areaX       = m_currentWidth  - VCUBE_SIZE - VCUBE_MARGIN;
   int areaY       = HEADER_BAR_HEIGHT + PAN_ICON_MARGIN + PAN_ICON_SIZE + VCUBE_MARGIN;
   m_vcubeCenterX  = areaX + VCUBE_SIZE / 2;
   m_vcubeCenterY  = areaY + VCUBE_SIZE / 2;

   //--- Optionally draw a semi-transparent background panel behind the cube
   if(showViewCubeBackground)
     {
      uint argbPanelBg     = ColorToARGB(LightenColor(themeColor, 0.92), 200);
      uint argbPanelBorder = ColorToARGB(themeColor, 200);
      //--- Fill the background panel
      for(int py = areaY; py < areaY + VCUBE_SIZE; py++)
         for(int px = areaX; px < areaX + VCUBE_SIZE; px++)
            blendPixelSet(m_mainCanvas, px, py, argbPanelBg);
      //--- Draw top and bottom borders
      for(int px = areaX; px < areaX + VCUBE_SIZE; px++)
        {
         blendPixelSet(m_mainCanvas, px, areaY, argbPanelBorder);
         blendPixelSet(m_mainCanvas, px, areaY + VCUBE_SIZE - 1, argbPanelBorder);
        }
      //--- Draw left and right borders
      for(int py = areaY; py < areaY + VCUBE_SIZE; py++)
        {
         blendPixelSet(m_mainCanvas, areaX, py, argbPanelBorder);
         blendPixelSet(m_mainCanvas, areaX + VCUBE_SIZE - 1, py, argbPanelBorder);
        }
     }

   //--- Use the current camera angles as the cube orientation
   double angX = m_cameraAngleX;
   double angY = m_cameraAngleY;

   //--- Define the 8 vertices of the unit cube in local space
   double verts[8][3];
   verts[0][0] = -1; verts[0][1] = -1; verts[0][2] = -1;
   verts[1][0] =  1; verts[1][1] = -1; verts[1][2] = -1;
   verts[2][0] =  1; verts[2][1] =  1; verts[2][2] = -1;
   verts[3][0] = -1; verts[3][1] =  1; verts[3][2] = -1;
   verts[4][0] = -1; verts[4][1] = -1; verts[4][2] =  1;
   verts[5][0] =  1; verts[5][1] = -1; verts[5][2] =  1;
   verts[6][0] =  1; verts[6][1] =  1; verts[6][2] =  1;
   verts[7][0] = -1; verts[7][1] =  1; verts[7][2] =  1;

   //--- Project all 8 vertices to screen space
   int sv[8][2];
   for(int i = 0; i < 8; i++)
      vcubeProject(verts[i][0], verts[i][1], verts[i][2], angX, angY, sv[i][0], sv[i][1]);

   //--- Define the 6 faces by their vertex indices (counter-clockwise)
   int faces[6][4];
   faces[0][0]=0; faces[0][1]=1; faces[0][2]=2; faces[0][3]=3; // Front
   faces[1][0]=5; faces[1][1]=4; faces[1][2]=7; faces[1][3]=6; // Back
   faces[2][0]=3; faces[2][1]=2; faces[2][2]=6; faces[2][3]=7; // Top
   faces[3][0]=0; faces[3][1]=1; faces[3][2]=5; faces[3][3]=4; // Bottom
   faces[4][0]=1; faces[4][1]=5; faces[4][2]=6; faces[4][3]=2; // Right
   faces[5][0]=4; faces[5][1]=0; faces[5][2]=3; faces[5][3]=7; // Left

   //--- Face label strings for text overlay
   string faceNames[6];
   faceNames[0] = "Front";
   faceNames[1] = "Back";
   faceNames[2] = "Top";
   faceNames[3] = "Bottom";
   faceNames[4] = "Right";
   faceNames[5] = "Left";

   //--- Face outward normals for back-face culling
   double faceNormals[6][3];
   faceNormals[0][0]= 0; faceNormals[0][1]= 0; faceNormals[0][2]=-1;
   faceNormals[1][0]= 0; faceNormals[1][1]= 0; faceNormals[1][2]= 1;
   faceNormals[2][0]= 0; faceNormals[2][1]= 1; faceNormals[2][2]= 0;
   faceNormals[3][0]= 0; faceNormals[3][1]=-1; faceNormals[3][2]= 0;
   faceNormals[4][0]= 1; faceNormals[4][1]= 0; faceNormals[4][2]= 0;
   faceNormals[5][0]=-1; faceNormals[5][1]= 0; faceNormals[5][2]= 0;

   //--- Camera direction vector for depth sorting and back-face culling
   double camDir[3];
   camDir[0] =  MathCos(angX) * MathSin(angY);
   camDir[1] = -MathSin(angX);
   camDir[2] = -MathCos(angX) * MathCos(angY);

   //--- Sort faces back-to-front using their projected depth (painter's algorithm)
   int    faceOrder[6];
   double faceDepth[6];
   for(int i = 0; i < 6; i++)
     {
      faceOrder[i] = i;
      double cx = 0, cy2 = 0, cz = 0;
      //--- Compute the face centre as the average of its four vertices
      for(int j = 0; j < 4; j++)
        {
         cx  += verts[faces[i][j]][0];
         cy2 += verts[faces[i][j]][1];
         cz  += verts[faces[i][j]][2];
        }
      cx /= 4.0; cy2 /= 4.0; cz /= 4.0;
      //--- Project the centre onto the camera direction for depth
      faceDepth[i] = cx * camDir[0] + cy2 * camDir[1] + cz * camDir[2];
     }
   //--- Bubble-sort the face order from farthest to nearest
   for(int i = 0; i < 5; i++)
      for(int j = i + 1; j < 6; j++)
         if(faceDepth[faceOrder[i]] > faceDepth[faceOrder[j]])
           {
            int tmp = faceOrder[i];
            faceOrder[i] = faceOrder[j];
            faceOrder[j] = tmp;
           }

   //--- Assign a distinct base colour to each face for identification
   color faceColors[6];
   faceColors[0] = clrCornflowerBlue; // Front
   faceColors[1] = clrSteelBlue;      // Back
   faceColors[2] = clrLimeGreen;      // Top
   faceColors[3] = clrDarkGreen;      // Bottom
   faceColors[4] = clrOrangeRed;      // Right
   faceColors[5] = clrDarkOrange;     // Left

   //--- Sub-face zone names for all 6 faces (3x3 grid per face)
   string faceSubNames[6][3][3];
   // Front face sub-zones
   faceSubNames[0][0][0] = "BottomFrontLeft";   faceSubNames[0][1][0] = "BottomFront";   faceSubNames[0][2][0] = "BottomFrontRight";
   faceSubNames[0][0][1] = "FrontLeft";          faceSubNames[0][1][1] = "Front";         faceSubNames[0][2][1] = "FrontRight";
   faceSubNames[0][0][2] = "TopFrontLeft";       faceSubNames[0][1][2] = "TopFront";      faceSubNames[0][2][2] = "TopFrontRight";
   // Back face sub-zones
   faceSubNames[1][0][0] = "BottomBackLeft";     faceSubNames[1][1][0] = "BottomBack";    faceSubNames[1][2][0] = "BottomBackRight";
   faceSubNames[1][0][1] = "BackLeft";           faceSubNames[1][1][1] = "Back";          faceSubNames[1][2][1] = "BackRight";
   faceSubNames[1][0][2] = "TopBackLeft";        faceSubNames[1][1][2] = "TopBack";       faceSubNames[1][2][2] = "TopBackRight";
   // Top face sub-zones
   faceSubNames[2][0][0] = "TopFrontLeft";       faceSubNames[2][1][0] = "TopFront";      faceSubNames[2][2][0] = "TopFrontRight";
   faceSubNames[2][0][1] = "TopLeft";            faceSubNames[2][1][1] = "Top";           faceSubNames[2][2][1] = "TopRight";
   faceSubNames[2][0][2] = "TopBackLeft";        faceSubNames[2][1][2] = "TopBack";       faceSubNames[2][2][2] = "TopBackRight";
   // Bottom face sub-zones
   faceSubNames[3][0][0] = "BottomFrontLeft";   faceSubNames[3][1][0] = "BottomFront";   faceSubNames[3][2][0] = "BottomFrontRight";
   faceSubNames[3][0][1] = "BottomLeft";         faceSubNames[3][1][1] = "Bottom";        faceSubNames[3][2][1] = "BottomRight";
   faceSubNames[3][0][2] = "BottomBackLeft";     faceSubNames[3][1][2] = "BottomBack";    faceSubNames[3][2][2] = "BottomBackRight";
   // Right face sub-zones
   faceSubNames[4][0][0] = "BottomFrontRight";  faceSubNames[4][1][0] = "BottomRight";   faceSubNames[4][2][0] = "BottomBackRight";
   faceSubNames[4][0][1] = "FrontRight";         faceSubNames[4][1][1] = "Right";         faceSubNames[4][2][1] = "BackRight";
   faceSubNames[4][0][2] = "TopFrontRight";      faceSubNames[4][1][2] = "TopRight";      faceSubNames[4][2][2] = "TopBackRight";
   // Left face sub-zones
   faceSubNames[5][0][0] = "BottomFrontLeft";   faceSubNames[5][1][0] = "BottomLeft";    faceSubNames[5][2][0] = "BottomBackLeft";
   faceSubNames[5][0][1] = "FrontLeft";          faceSubNames[5][1][1] = "Left";          faceSubNames[5][2][1] = "BackLeft";
   faceSubNames[5][0][2] = "TopFrontLeft";       faceSubNames[5][1][2] = "TopLeft";       faceSubNames[5][2][2] = "TopBackLeft";

   //--- Shrink factor creates a small visible gap between sub-face tiles
   double shrink = 0.03;

   //--- Render visible faces in back-to-front order
   for(int fi = 0; fi < 6; fi++)
     {
      int f = faceOrder[fi];
      //--- Compute the dot product to cull back-facing faces
      double dot = faceNormals[f][0] * camDir[0] +
                   faceNormals[f][1] * camDir[1] +
                   faceNormals[f][2] * camDir[2];
      if(dot >= 0) continue;

      //--- Compute Lambert diffuse brightness from the dot product
      double brightness = MathAbs(dot);
      brightness = 0.4 + 0.6 * brightness;

      //--- Apply brightness to the base face colour
      color fc = faceColors[f];
      uchar fr_base = (uchar)MathMin(255, (int)(((fc >> 16) & 0xFF) * brightness));
      uchar fg_base = (uchar)MathMin(255, (int)(((fc >> 8)  & 0xFF) * brightness));
      uchar fb_base = (uchar)MathMin(255, (int)(( fc        & 0xFF) * brightness));

      uchar alpha = 180;

      //--- Get the four projected screen vertices for this face
      int fx[4], fy[4];
      for(int j = 0; j < 4; j++)
        {
         fx[j] = sv[faces[f][j]][0];
         fy[j] = sv[faces[f][j]][1];
        }

      //--- Draw each 3x3 sub-face tile using bilinear interpolation
      for(int i = 0; i < 3; i++)
         for(int j = 0; j < 3; j++)
           {
            //--- Compute the UV bounds for this sub-tile with shrink margin
            double u1 = i / 3.0 + shrink;
            double u2 = (i + 1) / 3.0 - shrink;
            double v1 = j / 3.0 + shrink;
            double v2 = (j + 1) / 3.0 - shrink;
            if(u1 >= u2 || v1 >= v2) continue;

            //--- Compute the four screen-space corners of this sub-tile
            int sub_fx[4], sub_fy[4];
            sub_fx[0] = (int)bilinear(fx[0], fx[1], fx[3], fx[2], u1, v1);
            sub_fy[0] = (int)bilinear(fy[0], fy[1], fy[3], fy[2], u1, v1);
            sub_fx[1] = (int)bilinear(fx[0], fx[1], fx[3], fx[2], u2, v1);
            sub_fy[1] = (int)bilinear(fy[0], fy[1], fy[3], fy[2], u2, v1);
            sub_fx[2] = (int)bilinear(fx[0], fx[1], fx[3], fx[2], u2, v2);
            sub_fy[2] = (int)bilinear(fy[0], fy[1], fy[3], fy[2], u2, v2);
            sub_fx[3] = (int)bilinear(fx[0], fx[1], fx[3], fx[2], u1, v2);
            sub_fy[3] = (int)bilinear(fy[0], fy[1], fy[3], fy[2], u1, v2);

            //--- Look up the zone name for this sub-tile
            string subZone  = faceSubNames[f][i][j];
            //--- Check if this tile is the one currently hovered
            bool isHovered = (m_vcubeHoverZone == subZone);

            //--- Copy base colour channels for potential brightening
            uchar fr = fr_base;
            uchar fg = fg_base;
            uchar fb = fb_base;
            //--- Increase alpha and brighten colour when hovered
            uchar subAlpha = isHovered ? (uchar)240 : alpha;
            if(isHovered)
              {
               fr = (uchar)MathMin(255, fr + 40);
               fg = (uchar)MathMin(255, fg + 40);
               fb = (uchar)MathMin(255, fb + 40);
              }
            //--- Pack ARGB colour for this sub-tile
            uint argbFace = ((uint)subAlpha << 24) | ((uint)fr << 16) | ((uint)fg << 8) | (uint)fb;
            //--- Fill the sub-tile quad with the computed colour
            fillQuad(sub_fx, sub_fy, argbFace);
           }

      //--- Draw the four black outline edges of the face
      uint argbEdge = ColorToARGB(clrBlack, 200);
      for(int j = 0; j < 4; j++)
        {
         int next = (j + 1) % 4;
         m_mainCanvas.LineAA(fx[j], fy[j], fx[next], fy[next], argbEdge);
        }

      //--- Compute the face screen-space centre for text placement
      int fcx = (fx[0] + fx[1] + fx[2] + fx[3]) / 4;
      int fcy = (fy[0] + fy[1] + fy[2] + fy[3]) / 4;

      //--- Draw the face name label only for well-lit faces
      if(brightness > 0.5)
        {
         m_mainCanvas.FontSet("Arial Bold", 7);
         uint textCol = ColorToARGB(clrWhite, 220);
         m_mainCanvas.TextOut(fcx, fcy - 4, faceNames[f], textCol, TA_CENTER);
        }
     }

   //--- Draw the three coordinate axis indicators on the view cube
   uint argbAxis;
   int  ox, oy;
   vcubeProject(0, 0, 0, angX, angY, ox, oy);

   //--- Draw the X axis indicator and label
   int axEnd;
   argbAxis = ColorToARGB(clrRed, 220);
   vcubeProject(1.4, 0, 0, angX, angY, axEnd, oy);
   m_mainCanvas.LineAA(ox, oy, axEnd, oy, argbAxis);
   m_mainCanvas.FontSet("Arial Bold", 7);
   m_mainCanvas.TextOut(axEnd + 2, oy - 4, "X", ColorToARGB(clrRed, 255), TA_LEFT);

   //--- Draw the Y axis indicator and label
   int dummy;
   argbAxis = ColorToARGB(clrGreen, 220);
   vcubeProject(0, 1.4, 0, angX, angY, dummy, axEnd);
   m_mainCanvas.LineAA(ox, oy, dummy, axEnd, argbAxis);
   m_mainCanvas.TextOut(dummy + 2, axEnd - 4, "Y", ColorToARGB(clrGreen, 255), TA_LEFT);

   //--- Draw the Z axis indicator and label
   argbAxis = ColorToARGB(clrBlue, 220);
   vcubeProject(0, 0, 1.4, angX, angY, axEnd, dummy);
   m_mainCanvas.LineAA(ox, oy, axEnd, dummy, argbAxis);
   m_mainCanvas.TextOut(axEnd + 2, dummy - 4, "Z", ColorToARGB(clrBlue, 255), TA_LEFT);
  }

//+------------------------------------------------------------------+
//| Bilinearly interpolate between four corner values                |
//+------------------------------------------------------------------+
double bilinear(double p00, double p10, double p01, double p11, double u, double v)
  {
   //--- Compute the weighted blend of the four corner values
   return (1 - u) * (1 - v) * p00 + u * (1 - v) * p10 +
          (1 - u) *      v  * p01 + u *       v  * p11;
  }

//+------------------------------------------------------------------+
//| Scanline-fill a convex quadrilateral with the given ARGB colour  |
//+------------------------------------------------------------------+
void fillQuad(int &px[], int &py[], uint clr)
  {
   //--- Determine the vertical span of the quad
   int minY = py[0], maxY = py[0];
   for(int i = 1; i < 4; i++)
     {
      if(py[i] < minY) minY = py[i];
      if(py[i] > maxY) maxY = py[i];
     }

   //--- Scanline-fill the quad row by row
   for(int y = minY; y <= maxY; y++)
     {
      int minX = 99999, maxX = -99999;
      //--- Find the left and right intersection X for this scanline
      for(int i = 0; i < 4; i++)
        {
         int j  = (i + 1) % 4;
         int y1 = py[i], y2 = py[j];
         int x1 = px[i], x2 = px[j];
         //--- Process edges that cross this scanline
         if((y1 <= y && y2 >= y) || (y2 <= y && y1 >= y))
           {
            if(y1 == y2)
              {
               //--- Horizontal edge: include both endpoints
               if(x1 < minX) minX = x1;
               if(x2 < minX) minX = x2;
               if(x1 > maxX) maxX = x1;
               if(x2 > maxX) maxX = x2;
              }
            else
              {
               //--- Non-horizontal edge: interpolate the X intersection
               int ix = x1 + (y - y1) * (x2 - x1) / (y2 - y1);
               if(ix < minX) minX = ix;
               if(ix > maxX) maxX = ix;
              }
           }
        }
      //--- Paint every pixel on this scanline row
      for(int x = minX; x <= maxX; x++)
         blendPixelSet(m_mainCanvas, x, y, clr);
     }
  }

まず、vcubeProject関数を定義します。この関数は、3D座標点(x, y, z)を2D画面座標(sx, sy)へ投影し、angXおよびangYの回転角度に基づいてViewCube用のアイソメトリック表示をシミュレートします。MathCosMathSinを使用してコサイン値とサイン値を計算し、まずX軸回転を適用します。具体的には、yとzの値を変換してy2とz2を求め、その後Y軸回転をおこない、z2を利用してxを調整しx2を計算します。続いて、VCUBE_SIZEから求めた倍率でスケーリングをして、m_vcubeCenterXとm_vcubeCenterYを基準にオフセットを加えることで、ViewCubeの頂点を正確に2D画面上へ配置できるようにします。

3Dモード時にインタラクティブなViewCubeをオーバーレイ表示するため、drawViewCubeOverlay関数を実装します。この関数では、VCUBE_SIZEやVCUBE_MARGINなどの定数を使用してパンアイコンの下側へViewCubeを配置し、m_vcubeCenterXおよびm_vcubeCenterYに中心座標を設定します。showViewCubeBackgroundがtrueの場合は、blendPixelSetを使用して半透明の背景パネルを描画します。塗りつぶしと境界線には、明るく調整したthemeColorを使用します。次に、現在のカメラ角度をViewCubeに反映させます。立方体の8頂点をdouble型配列として定義し、それぞれの頂点をvcubeProjectで投影してsv配列へ整数座標として格納します。続いて、面のインデックス、面名、法線ベクトルを定義します。カメラ角度からカメラ方向ベクトルを計算し、各面について投影後の中心座標の平均値と法線ベクトルの内積を利用して奥行きを算出します。その後、面の描画順を決定するため、faceOrderを深度の昇順にソートします。これにより、奥側の面から手前の面へ描画され、正しい遮蔽表現が実現されます。面の色については、各面へ異なるclrs値を割り当てます。また、絶対値化した内積値を利用して明るさを調整し、面の向きに応じたシェーディング効果を追加します。

さらに、縮小係数を用いたサブディビジョン処理をおこないます。各面について3×3グリッドのループを実行し、bilinear関数を使用して各サブ四角形の座標を計算します。ホバー状態はfaceSubNames配列から取得したサブゾーン名によって判定します。この配列は6×3×3の静的文字列配列で、TopFrontLeftなどのゾーン名を定義しています。ホバー中の領域では、明るさとアルファ値を増加させ、ARGB値を構成した後、fillQuadを使用して面を塗りつぶします。その後、LineAAによるループ処理で黒色の境界線を追加し、明るい面にはTextOutを使用して中央へラベルを描画します。ラベルには小さな太字フォントを使用します。最後に、原点を投影し、そこから色付きの軸ラインを描画します。X、Y、Zのラベルを追加することで、ViewCubeの方向基準を明確にします。

ViewCubeの滑らかな分割表示と正確なホバー判定を実現するため、bilinear関数を作成します。この関数では、四隅の座標p00からp11を使用し、重み付き加算によって(u,v)位置における2D四角形上の補間値を計算します。これにより、ギザギザした境界を発生させることなく、ViewCube内の細かなサブゾーン分割を実現できます。最後に、任意の四角形をラスタライズして塗りつぶすfillQuad関数を定義します。この関数では、px配列とpy配列で指定された四角形について、まずY座標の最小値と最大値を取得します。その後、水平ラインごとにスキャンし、辺との交点からX座標の最小値と最大値を計算します。この処理では水平辺も個別に処理し、各行のピクセルに対してblendPixelSetでブレンド描画をおこないます。これにより、ViewCubeオーバーレイ上で面を完全に塗りつぶすことができます。 この関数を呼び出すと、ViewCubeは次のように描画されます。

ViewCube描画結果

ViewCubeが正常に描画できるようになったので、次はゾーン検出、クリック処理、およびカメラアニメーションを制御するインタラクションロジックを実装します。

ホバーゾーンの検出とViewCubeのクリック処理

ゾーン検出では、すべての面・辺・頂点の中心位置を投影し、カーソル位置から最も近い投影点を検索します。その際、各ゾーンごとに設定した距離閾値を使用して有効範囲を判定し、検出された結果をm_vcubeHoverZoneへ保存します。検出されたゾーンがクリックされた場合、そのゾーン名を対応する目標角度ペアへ変換し、その方向へ向かうタイマー制御アニメーションを開始します。

//+------------------------------------------------------------------+
//| Detect which face, edge or corner of the view cube is hovered    |
//+------------------------------------------------------------------+
void detectViewCubeZone(int localX, int localY)
  {
   //--- Use the current camera angles for the cube orientation
   double angX = m_cameraAngleX;
   double angY = m_cameraAngleY;

   //--- Compute the camera direction for back-face filtering
   double camDir[3];
   camDir[0] =  MathCos(angX) * MathSin(angY);
   camDir[1] = -MathSin(angX);
   camDir[2] = -MathCos(angX) * MathCos(angY);

   //--- Face name and normal lookup tables
   string faceNames[6];
   faceNames[0] = "Front"; faceNames[1] = "Back"; faceNames[2] = "Top";
   faceNames[3] = "Bottom"; faceNames[4] = "Right"; faceNames[5] = "Left";

   double faceNormals[6][3];
   faceNormals[0][0]= 0; faceNormals[0][1]= 0; faceNormals[0][2]=-1;
   faceNormals[1][0]= 0; faceNormals[1][1]= 0; faceNormals[1][2]= 1;
   faceNormals[2][0]= 0; faceNormals[2][1]= 1; faceNormals[2][2]= 0;
   faceNormals[3][0]= 0; faceNormals[3][1]=-1; faceNormals[3][2]= 0;
   faceNormals[4][0]= 1; faceNormals[4][1]= 0; faceNormals[4][2]= 0;
   faceNormals[5][0]=-1; faceNormals[5][1]= 0; faceNormals[5][2]= 0;

   //--- Face centre lookup table
   double faceCenters[6][3];
   faceCenters[0][0]= 0; faceCenters[0][1]= 0; faceCenters[0][2]=-1;
   faceCenters[1][0]= 0; faceCenters[1][1]= 0; faceCenters[1][2]= 1;
   faceCenters[2][0]= 0; faceCenters[2][1]= 1; faceCenters[2][2]= 0;
   faceCenters[3][0]= 0; faceCenters[3][1]=-1; faceCenters[3][2]= 0;
   faceCenters[4][0]= 1; faceCenters[4][1]= 0; faceCenters[4][2]= 0;
   faceCenters[5][0]=-1; faceCenters[5][1]= 0; faceCenters[5][2]= 0;

   //--- Initialise the best match distance and clear the zone
   double bestDist = 99999;
   m_vcubeHoverZone = "";

   //--- Check each visible face centre against the cursor position
   for(int i = 0; i < 6; i++)
     {
      //--- Skip back-facing faces (they cannot be clicked)
      double dot = faceNormals[i][0] * camDir[0] +
                   faceNormals[i][1] * camDir[1] +
                   faceNormals[i][2] * camDir[2];
      if(dot >= 0) continue;
      int sx, sy;
      vcubeProject(faceCenters[i][0], faceCenters[i][1], faceCenters[i][2], angX, angY, sx, sy);
      double dx = localX - sx;
      double dy = localY - sy;
      double d  = MathSqrt(dx * dx + dy * dy);
      //--- Update the closest zone within a 15-pixel radius
      if(d < 15 && d < bestDist)
        {
         bestDist = d;
         m_vcubeHoverZone = faceNames[i];
        }
     }

   //--- Edge midpoint lookup tables
   double edgeCenters[12][3];
   string edgeNames[12];
   edgeCenters[0][0]= 0;  edgeCenters[0][1]= 1;  edgeCenters[0][2]=-1;  edgeNames[0]  = "TopFront";
   edgeCenters[1][0]= 0;  edgeCenters[1][1]= 1;  edgeCenters[1][2]= 1;  edgeNames[1]  = "TopBack";
   edgeCenters[2][0]= 1;  edgeCenters[2][1]= 1;  edgeCenters[2][2]= 0;  edgeNames[2]  = "TopRight";
   edgeCenters[3][0]=-1;  edgeCenters[3][1]= 1;  edgeCenters[3][2]= 0;  edgeNames[3]  = "TopLeft";
   edgeCenters[4][0]= 0;  edgeCenters[4][1]=-1;  edgeCenters[4][2]=-1;  edgeNames[4]  = "BottomFront";
   edgeCenters[5][0]= 0;  edgeCenters[5][1]=-1;  edgeCenters[5][2]= 1;  edgeNames[5]  = "BottomBack";
   edgeCenters[6][0]= 1;  edgeCenters[6][1]=-1;  edgeCenters[6][2]= 0;  edgeNames[6]  = "BottomRight";
   edgeCenters[7][0]=-1;  edgeCenters[7][1]=-1;  edgeCenters[7][2]= 0;  edgeNames[7]  = "BottomLeft";
   edgeCenters[8][0]= 1;  edgeCenters[8][1]= 0;  edgeCenters[8][2]=-1;  edgeNames[8]  = "FrontRight";
   edgeCenters[9][0]=-1;  edgeCenters[9][1]= 0;  edgeCenters[9][2]=-1;  edgeNames[9]  = "FrontLeft";
   edgeCenters[10][0]= 1; edgeCenters[10][1]= 0; edgeCenters[10][2]= 1; edgeNames[10] = "BackRight";
   edgeCenters[11][0]=-1; edgeCenters[11][1]= 0; edgeCenters[11][2]= 1; edgeNames[11] = "BackLeft";

   //--- Check each edge midpoint against the cursor position (10-pixel radius)
   for(int i = 0; i < 12; i++)
     {
      int sx, sy;
      vcubeProject(edgeCenters[i][0], edgeCenters[i][1], edgeCenters[i][2], angX, angY, sx, sy);
      double dx = localX - sx;
      double dy = localY - sy;
      double d  = MathSqrt(dx * dx + dy * dy);
      if(d < 10 && d < bestDist)
        {
         bestDist = d;
         m_vcubeHoverZone = edgeNames[i];
        }
     }

   //--- Corner position and name lookup tables
   double cornerCenters[8][3];
   string cornerNames[8];
   cornerCenters[0][0]=-1; cornerCenters[0][1]= 1; cornerCenters[0][2]=-1; cornerNames[0]="TopFrontLeft";
   cornerCenters[1][0]= 1; cornerCenters[1][1]= 1; cornerCenters[1][2]=-1; cornerNames[1]="TopFrontRight";
   cornerCenters[2][0]= 1; cornerCenters[2][1]= 1; cornerCenters[2][2]= 1; cornerNames[2]="TopBackRight";
   cornerCenters[3][0]=-1; cornerCenters[3][1]= 1; cornerCenters[3][2]= 1; cornerNames[3]="TopBackLeft";
   cornerCenters[4][0]=-1; cornerCenters[4][1]=-1; cornerCenters[4][2]=-1; cornerNames[4]="BottomFrontLeft";
   cornerCenters[5][0]= 1; cornerCenters[5][1]=-1; cornerCenters[5][2]=-1; cornerNames[5]="BottomFrontRight";
   cornerCenters[6][0]= 1; cornerCenters[6][1]=-1; cornerCenters[6][2]= 1; cornerNames[6]="BottomBackRight";
   cornerCenters[7][0]=-1; cornerCenters[7][1]=-1; cornerCenters[7][2]= 1; cornerNames[7]="BottomBackLeft";

   //--- Check each corner against the cursor position (8-pixel radius)
   for(int i = 0; i < 8; i++)
     {
      int sx, sy;
      vcubeProject(cornerCenters[i][0], cornerCenters[i][1], cornerCenters[i][2], angX, angY, sx, sy);
      double dx = localX - sx;
      double dy = localY - sy;
      double d  = MathSqrt(dx * dx + dy * dy);
      if(d < 8 && d < bestDist)
        {
         bestDist = d;
         m_vcubeHoverZone = cornerNames[i];
        }
     }
  }

//+------------------------------------------------------------------+
//| Snap the camera to the orientation indicated by a cube zone click|
//+------------------------------------------------------------------+
void handleViewCubeClick(int mouseX, int mouseY)
  {
   //--- Abort if no valid zone is hovered
   if(m_vcubeHoverZone == "") return;

   //--- Diagonal tilt angle used for isometric corner/edge views
   double isoTilt = MathArctan(1.0 / MathSqrt(2.0));
   //--- Near-vertical angle used for top/bottom face views
   double nearPole = DX_PI / 2.0 - 0.0001;

   //--- Look up the target camera angles for the clicked zone
   double tX = 0, tY = 0;
   if     (m_vcubeHoverZone == "Front")              { tX = 0.0;        tY = 0.0; }
   else if(m_vcubeHoverZone == "Back")               { tX = 0.0;        tY = DX_PI; }
   else if(m_vcubeHoverZone == "Top")                { tX = nearPole;   tY = 0.0; }
   else if(m_vcubeHoverZone == "Bottom")             { tX = -nearPole;  tY = 0.0; }
   else if(m_vcubeHoverZone == "Right")              { tX = 0.0;        tY =  DX_PI / 2.0; }
   else if(m_vcubeHoverZone == "Left")               { tX = 0.0;        tY = -DX_PI / 2.0; }
   else if(m_vcubeHoverZone == "TopFront")           { tX = isoTilt;    tY = 0.0; }
   else if(m_vcubeHoverZone == "TopBack")            { tX = isoTilt;    tY = DX_PI; }
   else if(m_vcubeHoverZone == "TopRight")           { tX = isoTilt;    tY =  DX_PI / 2.0; }
   else if(m_vcubeHoverZone == "TopLeft")            { tX = isoTilt;    tY = -DX_PI / 2.0; }
   else if(m_vcubeHoverZone == "BottomFront")        { tX = -isoTilt;   tY = 0.0; }
   else if(m_vcubeHoverZone == "BottomBack")         { tX = -isoTilt;   tY = DX_PI; }
   else if(m_vcubeHoverZone == "BottomRight")        { tX = -isoTilt;   tY =  DX_PI / 2.0; }
   else if(m_vcubeHoverZone == "BottomLeft")         { tX = -isoTilt;   tY = -DX_PI / 2.0; }
   else if(m_vcubeHoverZone == "FrontRight")         { tX = 0.0;        tY =  DX_PI / 4.0; }
   else if(m_vcubeHoverZone == "FrontLeft")          { tX = 0.0;        tY = -DX_PI / 4.0; }
   else if(m_vcubeHoverZone == "BackRight")          { tX = 0.0;        tY =  DX_PI * 3.0 / 4.0; }
   else if(m_vcubeHoverZone == "BackLeft")           { tX = 0.0;        tY = -DX_PI * 3.0 / 4.0; }
   else if(m_vcubeHoverZone == "TopFrontRight")      { tX = isoTilt;    tY =  DX_PI / 4.0; }
   else if(m_vcubeHoverZone == "TopFrontLeft")       { tX = isoTilt;    tY = -DX_PI / 4.0; }
   else if(m_vcubeHoverZone == "TopBackRight")       { tX = isoTilt;    tY =  DX_PI * 3.0 / 4.0; }
   else if(m_vcubeHoverZone == "TopBackLeft")        { tX = isoTilt;    tY = -DX_PI * 3.0 / 4.0; }
   else if(m_vcubeHoverZone == "BottomFrontRight")   { tX = -isoTilt;   tY =  DX_PI / 4.0; }
   else if(m_vcubeHoverZone == "BottomFrontLeft")    { tX = -isoTilt;   tY = -DX_PI / 4.0; }
   else if(m_vcubeHoverZone == "BottomBackRight")    { tX = -isoTilt;   tY =  DX_PI * 3.0 / 4.0; }
   else if(m_vcubeHoverZone == "BottomBackLeft")     { tX = -isoTilt;   tY = -DX_PI * 3.0 / 4.0; }
   else return;

   //--- Store the target angles for the animation
   m_targetAngleX    = tX;
   m_targetAngleY    = tY;
   //--- Record the current angles as the animation start
   m_animStartAngleX = m_cameraAngleX;
   m_animStartAngleY = m_cameraAngleY;
   //--- Reset and start the animation
   m_animStep        = 0;
   m_animSteps       = 20;
   m_isAnimating     = true;
   //--- Start the millisecond timer to drive the animation
   EventSetMillisecondTimer(30);
  }

detectViewCubeZone関数を定義し、マウスカーソル下にあるViewCubeの特定のサブゾーン(面、辺、または頂点)をローカル座標で識別します。これにより、ViewCube上でのインタラクティブなフィードバックを実現します。まず、現在のカメラ角度をViewCubeに反映させ、MathCosMathSinを使用してカメラ方向ベクトルを計算します。次に、固定された立方体プロパティとして、面名、法線、中心座標の配列を定義します。そして、最短距離を保持するための初期値を大きく設定し、m_vcubeHoverZoneをリセットします。面のループ処理では、法線との内積を使用して背面を向いた面をスキップします。その後、vcubeProjectで中心座標を投影し、カーソルまでのユークリッド距離を計算します。距離が最小で15以内の場合、検出されたゾーンを更新します。

同様に、12個の辺についても、TopFrontなどの名前を持つ事前定義された中心座標を使用して10以内の距離で判定します。また、8個の頂点についてはTopFrontLeftなどのゾーンを8以内の距離で判定します。最小距離を優先することで、細かなホバー領域を正確に検出します。このゾーン検出は、ユーザー操作性において重要な役割を持ちます。レイキャストを使用せず、単純な距離しきい値によってキューブをクリック可能な領域へ分割することで、透視投影を考慮しながら素早く視点ターゲットを識別できます。

クリック操作に応答するため、handleViewCubeClick関数を実装します。ゾーンが存在しない場合は早期終了します。まず、MathArctanを使用して1/sqrt(2)からアイソメトリック傾斜角を事前計算します。これは35度ビューに対応します。また、極方向に近い値としてPI/2に近い角度も設定します。次に、m_vcubeHoverZoneを目標角度tXおよびtYへ変換します。標準的な視点を定義するため、大規模な条件分岐によって各ゾーンを対応付けます。例えば、Frontは(0,0)、TopはX軸方向を極付近の角度に設定し、辺はPI/4の組み合わせ、頂点は象限ごとのisoTiltを組み合わせた角度へマッピングします。その後、m_targetAngleXとm_targetAngleYへ目標角度を設定し、現在の角度をアニメーション開始値として保存します。m_animStepを0にリセットし、m_animStepsを20に設定した上でm_isAnimatingを有効化します。最後にEventSetMillisecondTimerを使用して30ms間隔のタイマーを開始し、滑らかな視点遷移を実行します。このクリック処理により、急激な切り替えではなく、あらかじめ定義された角度へアニメーションしながら移動するため、3Dナビゲーションにおける視点リセット操作が直感的になります。 クリック処理の実装が完了したので、次はアニメーション更新処理とマウスイベント処理を追加し、パン操作とViewCube操作を正しく振り分けます。

カメラ遷移のアニメーションとパン・ViewCubeマウスイベントの処理

アニメーション更新処理では、タイマー間隔ごとにカメラ角度を更新します。スムーズステップ曲線を使用することで、遷移開始時と終了時の速度を自然に減速させます。マウスイベントハンドラは拡張され、パンアイコンとViewCubeのホバー状態を追跡します。また、m_panModeに基づいて回転操作とパン操作のドラッグ処理を切り替え、左クリック入力をViewCube処理またはパン切り替え処理へ振り分けた後、既存のドラッグおよびリサイズ処理へ渡します。

//+------------------------------------------------------------------+
//| Advance the camera snap animation by one timer tick              |
//+------------------------------------------------------------------+
void tickAnimation()
  {
   //--- Abort if no animation is running
   if(!m_isAnimating) return;

   //--- Advance to the next animation step
   m_animStep++;
   //--- Compute normalised interpolation parameter [0, 1]
   double t = (double)m_animStep / (double)m_animSteps;
   //--- Apply smoothstep easing for a natural deceleration curve
   t = t * t * (3.0 - 2.0 * t);

   //--- Interpolate the elevation angle toward the target
   m_cameraAngleX = m_animStartAngleX + (m_targetAngleX - m_animStartAngleX) * t;
   //--- Interpolate the azimuth angle toward the target
   m_cameraAngleY = m_animStartAngleY + (m_targetAngleY - m_animStartAngleY) * t;

   //--- Snap to the exact target on the final step
   if(m_animStep >= m_animSteps)
     {
      m_cameraAngleX = m_targetAngleX;
      m_cameraAngleY = m_targetAngleY;
      m_isAnimating = false;
     }

   //--- Re-render and push the frame to the chart
   renderVisualization();
   ChartRedraw();
  }

//--- Update "handleMouseEvent" with new hover checks, panning logic, and view cube handling


bool previousPanHoverState    = m_isHoveringPanIcon;
bool previousVCubeHoverState  = m_isHoveringViewCube;
string previousVCubeZone      = m_vcubeHoverZone;

//--- Update all hover flags for the current cursor position
m_isHoveringCanvas = (mouseX >= m_currentPositionX &&
                      mouseX <= m_currentPositionX + m_currentWidth &&
                      mouseY >= m_currentPositionY &&
                      mouseY <= m_currentPositionY + m_currentHeight);
m_isHoveringHeader      = isMouseOverHeaderBar(mouseX, mouseY);
m_isHoveringSwitchIcon  = isMouseOverSwitchIcon(mouseX, mouseY);
m_isHoveringPanIcon     = isMouseOverPanIcon(mouseX, mouseY);
m_isHoveringResizeZone  = isMouseInResizeZone(mouseX, mouseY, m_hoverResizeMode);

//--- Update view cube hover only in 3D mode
if(m_currentViewMode == VIEW_3D_MODE)
   m_isHoveringViewCube = isMouseOverViewCube(mouseX, mouseY);
else
   m_isHoveringViewCube = false;

//--- Determine if any hover state change requires a redraw
bool needRedraw = (previousHoverState       != m_isHoveringCanvas      ||
                   previousHeaderHoverState != m_isHoveringHeader      ||
                   previousResizeHoverState != m_isHoveringResizeZone  ||
                   previousSwitchHoverState != m_isHoveringSwitchIcon  ||
                   previousPanHoverState    != m_isHoveringPanIcon     ||
                   previousVCubeHoverState  != m_isHoveringViewCube    ||
                   previousVCubeZone        != m_vcubeHoverZone);

//--- Handle 3D orbit and pan drags when over the canvas body (not header or cube)
if(m_currentViewMode == VIEW_3D_MODE && m_isHoveringCanvas &&
   !m_isHoveringHeader && !m_isHoveringViewCube)
  {
   //--- Orbit mode: rotate the camera around the target
   if(!m_panMode)
     {
      //--- Begin orbit on fresh left-button press
      if(mouseState == 1 && m_previousMouseButtonState == 0)
        {
         m_isRotating3D  = true;
         m_mouse3DStartX = mouseX;
         m_mouse3DStartY = mouseY;
         ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
        }
      //--- Continue orbit while button is held
      else if(mouseState == 1 && m_previousMouseButtonState == 1 && m_isRotating3D)
        {
         //--- Update azimuth proportional to horizontal mouse delta
         m_cameraAngleY += (mouseX - m_mouse3DStartX) / 300.0;
         //--- Update elevation proportional to vertical mouse delta
         m_cameraAngleX += (mouseY - m_mouse3DStartY) / 300.0;
         //--- Clamp elevation to avoid gimbal lock at poles
         if(m_cameraAngleX < -DX_PI * 0.499) m_cameraAngleX = -DX_PI * 0.499;
         if(m_cameraAngleX >  DX_PI * 0.499) m_cameraAngleX =  DX_PI * 0.499;
         //--- Update anchor for next delta computation
         m_mouse3DStartX = mouseX;
         m_mouse3DStartY = mouseY;
         //--- Cancel any running snap animation
         m_isAnimating = false;
         needRedraw = true;
        }
      //--- End orbit on button release
      else if(mouseState == 0 && m_previousMouseButtonState == 1)
        {
         m_isRotating3D = false;
         ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
        }
     }
   else
     {
      //--- Pan mode: translate the camera target in the view plane
      if(mouseState == 1 && m_previousMouseButtonState == 0)
        {
         //--- Begin pan drag
         m_isPanning = true;
         m_panStartX = mouseX;
         m_panStartY = mouseY;
         ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
        }
      else if(mouseState == 1 && m_previousMouseButtonState == 1 && m_isPanning)
        {
         //--- Compute mouse delta since last event
         int deltaX = mouseX - m_panStartX;
         int deltaY = mouseY - m_panStartY;

         //--- Reconstruct the camera position vector in world space
         DXVector4 camera(0.0f, 0.0f, (float)-m_cameraDistance, 1.0f);
         DXMatrix rotX;
         DXMatrixRotationX(rotX, (float)m_cameraAngleX);
         DXVec4Transform(camera, camera, rotX);
         DXMatrix rotY;
         DXMatrixRotationY(rotY, (float)m_cameraAngleY);
         DXVec4Transform(camera, camera, rotY);
         DXVector3 cameraPos(camera.x, camera.y, camera.z);

         //--- Compute and normalise the forward direction toward the target
         DXVector3 forward;
         DXVec3Subtract(forward, m_viewTarget, cameraPos);
         float len = DXVec3Length(forward);
         if(len > 0.0f) DXVec3Scale(forward, forward, 1.0f / len);

         //--- Compute the camera right vector via cross product
         DXVector3 worldUp(0.0f, 1.0f, 0.0f);
         DXVector3 right;
         DXVec3Cross(right, worldUp, forward);
         len = DXVec3Length(right);
         if(len > 0.0f) DXVec3Scale(right, right, 1.0f / len);

         //--- Compute the camera up vector orthogonal to forward and right
         DXVector3 camUp;
         DXVec3Cross(camUp, forward, right);
         len = DXVec3Length(camUp);
         if(len > 0.0f) DXVec3Scale(camUp, camUp, 1.0f / len);

         //--- Scale the pan movement proportional to camera distance
         float panFactor = (float)m_cameraDistance / 500.0f;
         DXVector3 moveRight;
         DXVec3Scale(moveRight, right,  (float)(-deltaX) * panFactor);
         DXVector3 moveUp;
         DXVec3Scale(moveUp,   camUp,   (float)( deltaY) * panFactor);

         //--- Translate the view target by the computed pan offset
         DXVector3 temp;
         DXVec3Add(temp,        m_viewTarget, moveRight);
         DXVec3Add(m_viewTarget, temp,        moveUp);

         //--- Update pan anchor for next delta computation
         m_panStartX = mouseX;
         m_panStartY = mouseY;
         needRedraw = true;
        }
      //--- End pan drag on button release
      else if(mouseState == 0 && m_previousMouseButtonState == 1)
        {
         m_isPanning = false;
         ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
        }
     }
  }

//--- In the mouse down block:

//--- Handle view cube face/edge/corner click in 3D mode
if(m_isHoveringViewCube && m_currentViewMode == VIEW_3D_MODE && m_vcubeHoverZone != "")
  {
   handleViewCubeClick(mouseX, mouseY);
   needRedraw = true;
   m_previousMouseButtonState = mouseState;
   return;
  }
//--- Toggle pan mode when the pan icon is clicked
else if(m_isHoveringPanIcon && m_currentViewMode == VIEW_3D_MODE)
  {
   m_panMode = !m_panMode;
   needRedraw = true;
   m_previousMouseButtonState = mouseState;
   return;
  }

//+------------------------------------------------------------------+
//| Return true when the cursor is over the pan mode toggle icon     |
//+------------------------------------------------------------------+
bool isMouseOverPanIcon(int mouseX, int mouseY)
  {
   //--- Pan icon is only visible in 3D mode
   if(m_currentViewMode != VIEW_3D_MODE) return false;
   //--- Align pan icon with the right edge below the header
   int iconX = m_currentPositionX + m_currentWidth - PAN_ICON_SIZE - PAN_ICON_MARGIN;
   int iconY = m_currentPositionY + HEADER_BAR_HEIGHT + PAN_ICON_MARGIN;
   //--- Return true if the cursor falls within the icon bounding box
   return (mouseX >= iconX && mouseX <= iconX + PAN_ICON_SIZE &&
           mouseY >= iconY && mouseY <= iconY + PAN_ICON_SIZE);
  }

//+------------------------------------------------------------------+
//| Return true when the cursor is over the view cube widget         |
//+------------------------------------------------------------------+
bool isMouseOverViewCube(int mouseX, int mouseY)
  {
   //--- View cube is only visible in 3D mode
   if(m_currentViewMode != VIEW_3D_MODE) return false;
   //--- Compute the view cube bounding area below the pan icon
   int cubeAreaX = m_currentPositionX + m_currentWidth - VCUBE_SIZE - VCUBE_MARGIN;
   int cubeAreaY = m_currentPositionY + HEADER_BAR_HEIGHT + PAN_ICON_MARGIN + PAN_ICON_SIZE + VCUBE_MARGIN;
   int cubeAreaW = VCUBE_SIZE;
   int cubeAreaH = VCUBE_SIZE;

   //--- Check if the cursor is within the bounding box
   if(mouseX >= cubeAreaX && mouseX <= cubeAreaX + cubeAreaW &&
      mouseY >= cubeAreaY && mouseY <= cubeAreaY + cubeAreaH)
     {
      //--- Detect the specific cube face, edge or corner under the cursor
      detectViewCubeZone(mouseX - m_currentPositionX, mouseY - m_currentPositionY);
      return (m_vcubeHoverZone != "");
     }
   //--- Cursor is outside the view cube; clear hover zone
   m_vcubeHoverZone = "";
   return false;
  }

//+------------------------------------------------------------------+
//| Deactivate pan mode and trigger a redraw                        |
//+------------------------------------------------------------------+
void exitPanMode()
  {
   //--- Only act if pan mode is currently active
   if(m_panMode)
     {
      m_panMode = false;
      renderVisualization();
      ChartRedraw();
     }
  }

ViewCube上のホバー中のサブゾーンをローカル座標で特定するため、detectViewCubeZone関数を定義します。現在のカメラに合わせて角度を同期し、MathCosとMathSinを使用して方向ベクトルを計算します。次に、キューブの面要素として面名、法線、中心座標の配列を定義します。最初に最大距離値と空のm_vcubeHoverZoneを設定します。面のループでは、法線との内積によって背面を向いた面をスキップし、vcubeProjectを使用して中心座標を投影します。その後、MathSqrtでカーソルまでの距離を計算し、15以内で最も近い場合にゾーンを更新します。同様の処理を12個の辺にも適用します。TopFrontなどの名前を持つ事前定義された中心座標を使用し、10以内の距離で判定します。また、8個の頂点についてはTopFrontLeftなどを8以内の距離で判定し、最も近いものを選択することで、細かなインタラクションを可能にします。

handleMouseEventでは、パンおよびViewCubeのホバー状態について以前の状態を保持する処理を追加します。ゾーン情報を含め、isMouseOverPanIconによってm_isHoveringPanIconを更新し、isMouseOverViewCubeによってm_isHoveringViewCubeを更新します(3Dモードでない場合はリセットします)。また、これらの状態をneedRedraw判定に含めます。ヘッダーやViewCube上ではない3Dキャンバスのホバー処理では、m_panModeが無効の場合は既存の回転処理を維持します。有効の場合、マウス押下時にm_isPanningをtrueへ設定し、開始座標を保存してスクロールを無効化します。ドラッグ中は移動量を計算し、カメラベクトルを回転変換します。その後、正規化したforwardベクトルを求め、DXVec3CrossとDXVec3Lengthを使用してrightベクトルを計算します。camUpについても同様に取得します。カメラ距離から求めたパン係数を使用して移動量をスケーリングし、DXVec3Addによってm_viewTargetへ加算します。開始座標を更新し、再描画フラグを設定します。マウスリリース時にはm_isPanningをリセットし、スクロールを再度有効化します。マウス押下処理では、3Dモード中でViewCube上にあり、ゾーンが検出されている場合、handleViewCubeClickを呼び出します。再描画フラグを設定し、状態を更新して処理を終了します。また、3Dモード中にパンアイコン上でクリックされた場合は、m_panModeを切り替えます。再描画フラグを設定し、状態を更新して処理を終了します。

パン切り替えアイコン上のホバーを検出するため、isMouseOverPanIcon関数を作成します。この関数では、3Dモードでない場合はfalseを返します。それ以外では、位置情報と定数からアイコン領域を計算し、マウスが範囲内にあるかを判定します。次に、isMouseOverViewCube関数を作成します。この関数では、3Dモードでない場合はfalseを返します。ViewCubeの位置とマージンから領域を計算し、マウスが範囲内にある場合はローカルオフセットを使用してdetectViewCubeZoneを呼び出します。ゾーンが設定されていればtrueを返し、そうでなければゾーンを空文字列へリセットしてfalseを返します。パンモードを無効化するため、exitPanMode関数を定義します。m_panModeが有効な場合、この関数ではモードをリセットし、renderVisualizationで再描画して、チャートを更新します。これにより、イベント処理でEscapeキーによるパンモード終了を実現できます。アニメーションを有効化するため、次はタイマーイベントハンドラ内でアニメーション更新関数を呼び出します。

タイマー、初期化解除、チャートイベントハンドラへの組み込み

最後の手順では、アニメーション更新処理をタイマーイベントハンドラへ接続し、終了処理にタイマーのクリーンアップを追加します。また、チャートイベントハンドラを拡張し、Escapeキーが押された場合にexitPanModeを呼び出すキーボード処理分岐を追加します。

//+------------------------------------------------------------------+
//| Advance the camera snap animation on each timer tick             |
//+------------------------------------------------------------------+
void OnTimer()
  {
   //--- Delegate to the visualizer's per-tick animation updater
   if(distributionVisualizer != NULL)
      distributionVisualizer.tickAnimation();
  }

//--- Add "EventKillTimer()" in "OnDeinit"

//+------------------------------------------------------------------+
//| Release all resources when the EA is removed                     |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   //--- Stop the animation timer
   EventKillTimer();
   //--- Destroy the visualizer object if it still exists
   if(distributionVisualizer != NULL)
     {
      delete distributionVisualizer;
      distributionVisualizer = NULL;
     }
   //--- Redraw the chart to remove the canvas bitmap object
   ChartRedraw();
   Print("Distribution window deinitialized");
  }

//--- Update "OnChartEvent" to handle ESC key for exiting pan mode
//--- We added this for enhanced simplicity


//+------------------------------------------------------------------+
//| Route chart mouse, wheel and key events to the visualizer        |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   //--- Abort if the visualizer has not been initialised
   if(distributionVisualizer == NULL) return;

   //--- Handle mouse move and button events
   if(id == CHARTEVENT_MOUSE_MOVE)
     {
      int mouseX     = (int)lparam;       // Horizontal cursor position in pixels
      int mouseY     = (int)dparam;       // Vertical cursor position in pixels
      int mouseState = (int)sparam;       // Bitmask of pressed mouse buttons

      distributionVisualizer.handleMouseEvent(mouseX, mouseY, mouseState);
     }

   //--- Handle mouse wheel scroll events
   if(id == CHARTEVENT_MOUSE_WHEEL)
     {
      //--- Unpack cursor X from the low 16 bits of lparam
      int mouseX = (int)(short) lparam;
      //--- Unpack cursor Y from the high 16 bits of lparam
      int mouseY = (int)(short)(lparam >> 16);
      distributionVisualizer.handleMouseWheel(mouseX, mouseY, dparam);
     }

   //--- Handle Escape key press to exit pan mode
   if(id == CHARTEVENT_KEYDOWN && lparam == 27)
      distributionVisualizer.exitPanMode();
  }

ここでは、アニメーションを定期的に進行させるOnTimerイベントハンドラを定義します。distributionVisualizerがNULLでないことを確認した後、tickAnimationを呼び出して、ViewCubeの遷移やその他のタイマー制御エフェクトを更新します。OnDeinitでは、EventKillTimerを追加してタイマーを停止します。これにより、初期化解除後に不要なイベントが残らないようにします。OnChartEventはキーボード処理を追加するよう更新します。idがCHARTEVENT_KEYDOWNで、lparamが27(ESCキー)の場合、ビジュアライザ上でexitPanModeを呼び出します。これにより、パンモードが有効な場合は無効化され、キーボードショートカットによる便利な操作が可能になります。これで実装は完了です。残る作業はシステムのテストであり、次のセクションで確認します。


バックテスト

テストを実施しました。以下はビジュアルを単一のGraphics Interchange Format (GIF)画像としてまとめたものです。

バックテストGIF

テスト中、セグメント化された3Dカーブは、異なる試行回数においてもヒストグラムバーのピークと一貫して一致しました。パンモードでは、現在の回転角度やズームレベルを変更することなく、注視点を滑らかに移動できました。また、ViewCubeのクリック操作では、テストしたすべての面・辺・頂点ゾーンにおいて、カメラが正しい標準方向へスムーズにアニメーション遷移しました。


結論

MQL5の3D二項分布ビューアを拡張し、確率質量関数を奥行き付きで正確に可視化するためのセグメント化されたチューブ状カーブ、カメラ基準で注視点を移動するパンモード、そして面・辺・頂点のホバーゾーンを備えたインタラクティブなViewCubeを実装しました。ViewCubeは標準方向へのカメラアニメーション遷移にも対応しています。実装では、ボックスベースのカーブセグメント構築と変換行列による方向付け、ホバーゾーン検出のための双線形分割、タイマー制御によるスムーズステップ型カメラ遷移、さらにクリック・パン操作・キーボードショートカットに対応したイベント処理の更新をおこないました。この記事を読み終えた今、以下のことができるでしょう。

  • 3Dカーブを使用して、確率質量関数のピークが最頻度のヒストグラムビンと一致しているかを視覚的に確認する。2Dモードへ切り替えることなく、シーン上で直接モデルの適合性を判断する。
  • パンモードを有効化することで、現在の回転角度やズームレベルを維持したまま、低確率領域のテール部分や広く偏在した試行回数レンジへシーンの焦点を移動する。
  • ViewCube上の任意の面・辺・頂点をクリックすることで、カメラを標準方向へアニメーション移動する。上面ビューではすべてのバー高さを比較でき、正面ビューでは個々のビン値を明確に読み取る。

次回以降では、2D統計分布へ進み、さらに多くの分布をプロットしていきます。どうぞお楽しみに。

MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/21335

最後のコメント | ディスカッションに移動 (5)
BeeXXI Corporation
Nikolai Semko | 15 6月 2026 において 11:50
3D曲線が表示されない
3次元表示で2D曲線が表示されている
Allan Munene Mutiiria
Allan Munene Mutiiria | 15 6月 2026 において 13:41
Nikolai Semko #:
3D曲線が表示されません
3Dビューで2D曲線が表示されています
ご意見をいただきありがとうございます。

現在、3Dカーブセグメントの位置決めはシーンのレンダリング完了後、1ステップ遅れて行われるため、最初の静止フレームではまだ配置されていません。キャンバス上で1回、オービット(左クリック&ドラッグ)またはズーム(スクロールホイール)を行ってください。これにより再度レンダリングが実行され、カーブが表示されます。

3D表示に切り替えた瞬間にカーブをレンダリングさせたい場合は、カーブのレンダリング関数をレンダリング呼び出しの前に移動させてください。以下を参照してください:

変更前:

   //+------------------------------------------------------------------+
   //| 3Dシーンを描画し、その上に2D UI要素を重ねる            |
   //+------------------------------------------------------------------+
   void render3DVisualization()
     {
      //--- データが読み込まれていない場合は処理を中止する
      if(!m_isDataLoaded) return;

      //--- このフレームのビュー変換とライト変換を再計算する
      updateCameraPosition();
      //--- 現在のデータに合わせて、すべての3Dヒストグラムのバーの位置を調整する
      update3DHistogramBars();

      //--- クリアパスには設定された背景色を使用する
      color bgColor     = backgroundTopColor;
      uint  bgColorArgb = ColorToARGB(bgColor, 255);
      //--- カラーバッファと深度バッファをクリアし、3Dシーンを描画する
      m_mainCanvas.Render(DX_CLEAR_COLOR | DX_CLEAR_DEPTH, bgColorArgb);

      //--- 2Dの境界枠を3Dレンダリングに重ね合わせる
      if(showBorderFrame)
         draw3DBorder();
      //--- レンダリングされた3D画像の上にヘッダーバーを重ねる
      drawHeaderBarOn3D();
      //--- 有効になっている場合は、統計パネルと凡例をオーバーレイ表示する
      if(showStatistics)
        {
         drawStatisticsPanelOn3D();
         drawLegendOn3D();
        }
      //--- このフレームのすべてのPMF曲線チューブセグメントの位置を再調整する
      update3DCurveSegments(); // THIS IS AFTER THE SCENE RENDERS, SO THE 3D SEGMENTS DELAY RENDERING UNTIL INTERACTION. SEE THE CANVAS RENDER ABOVE IN YELLOW
      //--- パンモードの切り替えアイコンを描画する
      drawPanIconOverlay();
      //--- インタラクティブなビューキューブウィジェットを描画する
      drawViewCubeOverlay();
      //--- マウスカーソルを乗せたときに、サイズ変更グリップのインジケーターを表示する
      if(m_isHoveringResizeZone && enableResizing)
         drawResizeIndicatorOn3D();
      //--- ピクセルバッファをチャートオブジェクトに書き込み
      m_mainCanvas.Update();
     }

変更後:

   //+------------------------------------------------------------------+
   //| 3Dシーンを描画し、その上に2D UI要素を重ねる            |
   //+------------------------------------------------------------------+
   void render3DVisualization()
     {
      //--- データが読み込まれていない場合は処理を中止する
      if(!m_isDataLoaded) return;

      //--- このフレームのビュー変換とライト変換を再計算する
      updateCameraPosition();
      //--- 現在のデータに合わせて、すべての3Dヒストグラムのバーの位置を調整する
      update3DHistogramBars();
      //--- レンダリングの前に、すべてのPMF曲線チューブセグメントの位置を再調整する
      update3DCurveSegments(); // JUST MOVE THE FUNCTION BEFORE THE 3D RENDER FOR INSTANT RENDERING, TO BE COUPLED WITH THE 3D BARS

      //--- クリアパスには設定された背景色を使用する
      color bgColor     = backgroundTopColor;
      uint  bgColorArgb = ColorToARGB(bgColor, 255);
      //--- カラーバッファと深度バッファをクリアし、3Dシーンを描画する
      m_mainCanvas.Render(DX_CLEAR_COLOR | DX_CLEAR_DEPTH, bgColorArgb);

      //--- 2Dの境界枠を3Dレンダリングに重ね合わせる
      if(showBorderFrame)
         draw3DBorder();
      //--- レンダリングされた3D画像の上にヘッダーバーを重ねる
      drawHeaderBarOn3D();
      //--- 有効になっている場合は、統計パネルと凡例をオーバーレイ表示する
      if(showStatistics)
        {
         drawStatisticsPanelOn3D();
         drawLegendOn3D();
        }
      //--- パンモードの切り替えアイコンを描画する
      drawPanIconOverlay();
      //--- インタラクティブなビューキューブウィジェットを描画する
      drawViewCubeOverlay();
      //--- マウスカーソルを乗せたときに、サイズ変更グリップのインジケーターを表示する
      if(m_isHoveringResizeZone && enableResizing)
         drawResizeIndicatorOn3D();
      //--- ピクセルバッファをチャートオブジェクトに書き込み
      m_mainCanvas.Update();
     }

ご指摘ありがとうございます。お役に立てれば幸いです。ありがとうございます。

Dmitriy Skub
Dmitriy Skub | 15 6月 2026 において 14:20
Nikolai Semko #:
3D曲線が表示されない
3次元表示で2D曲線が表示されている
同感です。
BeeXXI Corporation
Nikolai Semko | 15 6月 2026 において 14:52
Allan Munene Mutiiria #:
ご感想をいただき、ありがとうございます。

現在、3D曲線のセグメントはシーンのレンダリングが行われる1ステップ後に配置されるため、最初の静止画にはまだ表示されていません。 キャンバス上で一度、回転(左クリックでドラッグ)またはズーム(スクロールホイール)を行ってください。これにより再レンダリングが実行され、曲線が表示されます。

3D表示に切り替えた瞬間に曲線が即座にレンダリングされるようにしたい場合は、曲線のレンダリング処理を、シーン全体のレンダリング呼び出しの前に移動させてください。以下を参照してください:

変更前:

変更後:

ご指摘ありがとうございます。お役に立てれば幸いです。ありがとうございます。

私の言っていることが伝わっていませんでした。
正確には、正しく理解されていませんでした。
3D曲線はなく、2D曲線のみです。 特に記事では、正しい用語を使うことが重要です。

こちらが3D曲線の例です。3次元空間にある曲線の場合、その曲線を任意の平面に投影しても、あなたのケースのように直線になることは決してありません。

Allan Munene Mutiiria
Allan Munene Mutiiria | 15 6月 2026 において 15:17
Nikolai Semko #:
私の話を聞いていなかったんですね。
というか、正しく聞いていなかったということですね。
3Dの曲線など存在せず、あるのは2Dの曲線だけです。特に記事では、正しい用語を使うことが重要です。

こちらが3D曲線の例です。曲線が3次元空間にある場合、その曲線を任意の平面に投影しても、あなたのケースのように直線になることは決してありません。

なるほど、おっしゃる意味がわかりました。ごもっともです。用語に関してはご指摘の通りです。厳密に言えば、これは平面曲線(Z軸は固定で、成功回数と確率のみが変動する)ですので、本来の意味での3D曲線ではありません。その点は承知しました。

私の意図は、PMFを3Dシーン内の実際のオブジェクトとしてレンダリングすることでした。回転可能で、チューブ状の形状を持ち、3Dヒストグラムの棒グラフの横に配置し、レンダリング時にそれらと位置が揃ったままになり、同じ空間内で両方を一緒に追跡できるようにするというものです。 つまり、「3D」とは、真の空間曲線という意味ではなく、3Dビュー内でレンダリングされるという意味で用いました。ご指摘ありがとうございます。

コード、涙、そしてAlgo Forge コード、涙、そしてAlgo Forge
プログラムコードや記事の添付ファイルを、より快適に公開・管理するための新しい方法として、MQL5 Algo Forgeへの移行について紹介します。従来のZIPアーカイブ形式でソースコードを添付する方法の代わりにリポジトリを使用することで、プロジェクトを常に最新の状態に保ち、素早く編集し、読者とよりプロフェッショナルな形で関わることができるようになります。MetaEditorインターフェースを通じて、開発したものをクラウド環境へ素早く移行するための推奨方法についても説明します。
MQL5とデータ処理パッケージの統合(第8回):グラフニューラルネットワークによる流動性ゾーンの検出 MQL5とデータ処理パッケージの統合(第8回):グラフニューラルネットワークによる流動性ゾーンの検出
市場構造をMQL5上でグラフとして表現し、スイングハイとスイングローを特徴量を持つノードとして扱い、それらをエッジで接続する方法を紹介します。また、潜在的な流動性ゾーンをスコアリングするためのGNN(Graph Neural Network:グラフニューラルネットワーク)の学習、モデルのONNX形式へのエクスポート、さらにエキスパートアドバイザー(EA)内でリアルタイム推論を実行する方法についても解説します。読者は、データパイプラインの構築、モデルの統合、チャート上への流動性ゾーンの可視化、そしてそのシグナルをルールベースの売買執行へ活用する方法を学びます。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
プライスアクション分析ツールキットの開発(第64回):手動で描画したトレンドラインと自動監視の同期 プライスアクション分析ツールキットの開発(第64回):手動で描画したトレンドラインと自動監視の同期
手動で描画したトレンドラインを監視するには、常にチャートを確認し続ける必要があり、その結果、重要な価格反応を見逃す可能性があります。本記事では、MQL5 において手動で描画したトレンドラインと自動監視ロジックを同期させるトレンドライン監視エキスパートアドバイザーを開発し、価格が監視対象ラインに接近、タッチ、またはブレイクした際にアラートを生成します。