MQL5-Handelswerkzeuge (Teil 24): Verbesserte Tiefenwahrnehmung mit 3D-Kurven, Pan-Modus und ViewCube-Navigation
Einführung
Sie verfügen zwar über eine dreidimensionale Visualisierung der Binomialverteilung mit Dreh- und Zoomfunktionen, jedoch ohne eine räumlich präzise Kurve, ohne die Möglichkeit, den Fokuspunkt der Szene zu verschieben, und ohne schnelle Möglichkeit, zur Standardausrichtung zurückzukehren. Das Untersuchen der Formen der PMF erfordert daher ständiges manuelles Drehen. Außerhalb des Zentrums liegende Bereiche lassen sich nur schwer erreichen, und die Rückkehr zur Standardansicht bedeutet, die Ansicht gewissermaßen blind manuell zurückzusetzen. Dieser Artikel richtet sich an Entwickler der Programmiersprache MetaQuotes Language 5 (MQL5) sowie an algorithmische Händler, die interaktive dreidimensionale Statistik-Tools um intuitive Navigationselemente erweitern möchten, um tiefgreifendere Wahrscheinlichkeitsanalysen durchzuführen.
In unserem vorherigen Artikel (Teil 23) haben wir Direct3D in den MQL5-Viewer der Binomialverteilung integriert und damit umschaltbare 2D-/3D-Modi sowie Kamerasteuerung für Drehung, Zoom und automatische Anpassung ermöglicht. In Teil 24 erweitern wir das Tool um eine segmentierte dreidimensionale Kurve zur besseren Tiefenwahrnehmung der PMF, integrieren einen Pan-Modus zum Verschieben des Kamerazielpunkts und implementieren einen interaktiven ViewCube mit Hover-Zonen und animierten Kameraübergängen für schnelle Orientierungswechsel. Wir werden die folgenden Themen behandeln:
- Die 3D-Kurve, den Pan-Modus und das ViewCube-Framework verstehen
- Implementierung in MQL5
- Backtests
- Schlussfolgerung
Am Ende verfügen Sie über ein erweitertes MQL5-Tool mit erweiterter 3D-Navigation für die Verteilungsanalyse, das Sie ganz nach Ihren Wünschen anpassen können – legen wir los!
Die 3D-Kurve, den Pan-Modus und das ViewCube-Framework verstehen
Die Darstellung der PMF als flache, projizierte Linie im dreidimensionalen Raum verliert die Tiefe, die die dreidimensionale Darstellung so wertvoll macht – eine segmentierte, röhrenförmige Kurve, die aus orientierten Box-Primitiven aufgebaut ist, die an aufeinanderfolgende Datenpunkte ausgerichtet sind, verleiht der Kurve eine greifbare räumliche Präsenz in der Szene, sodass Höhenunterschiede zwischen den Bins in der Perspektive natürlich zur Geltung kommen. Der Pan-Modus behebt eine andere Einschränkung: Durch Drehen und Zoomen bleibt die Szene an einem festen Zielpunkt verankert, sodass außermittige Verteilungen oder eine große Anzahl von Versuchen den relevanten Bereich an den Rand verschieben; durch die Verschiebung des Kamerazielpunkts mittels kamerarelativer Vektorarithmetik können wir die gesamte Szene erkunden, ohne den aktuellen Blickwinkel oder die aktuelle Zoomstufe zu verändern. Der ViewCube ist ein kompaktes Orientierungs-Widget, das die aktuelle Drehung der Kamera in Echtzeit widerspiegelt. Er ist in anklickbare Flächen, Kanten und Ecken unterteilt, die jeweils einem vordefinierten Winkelpaar zugeordnet sind und einen sanft interpolierten Übergang anstelle eines sofortigen Sprungs auslösen.
Nutzen Sie diese räumlich präzise dargestellte Kurve im Handel zusammen mit dem Histogramm, um zu beurteilen, ob die Spitze der PMF mit dem Bin der höchsten Frequenz übereinstimmt, was auf ein gut kalibriertes Modell hindeutet. Richten Sie den Fokus zunächst auf die Verteilungsschwänze mit geringer Wahrscheinlichkeit, um diese genauer zu untersuchen, bevor Sie Positionen in der Nähe der Extremwerte festlegen. Verwenden Sie die Draufsicht des ViewCubes, um die Barhöhen aller Bins gleichzeitig zu vergleichen, und die Vorderansicht, um die Werte der einzelnen Bars übersichtlich abzulesen.
Wir werden das Tool erweitern, indem wir boxbasierte Kurvensegmente erstellen, die so skaliert und gedreht werden, dass sie eine durchgehende Röhre bilden. Außerdem werden wir eine Pan-Logik unter Verwendung kamerarelativer Vektorberechnungen implementieren und den ViewCube mit projizierten Scheitelpunkten, Flächensortierung für die korrekte Zeichenreihenfolge sowie bilinearer Interpolation für unterteilte Hover-Zonen rendern. Außerdem werden wir zeitgesteuerte Animationen für Ansichtsübergänge, überblendete Pixel-Overlays für Symbole und den Würfel sowie die Ereignisbehandlung für Klicks und Hover-Ereignisse integrieren, um die Benutzeroberfläche vollständig responsiv zu gestalten. Werfen Sie unten einen Blick auf unsere Ziele.

Implementierung in MQL5
Erweiterung von Eingaben, Konstanten und Klassenelementen zur Unterstützung von Pan-Modus und ViewCube
Um den neuen Pan-Modus und das ViewCube-Widget zu unterstützen, erweitern wir zunächst die Eingaben um einen Schalter für den ViewCube-Hintergrund, definieren Konstanten, die die Größe von Symbolen und Würfeln steuern, führen neue geschützte Member-Variablen in der Visualizer-Klasse für Hover-Zustände, Kurvensegmente, das Verschieben und die Animationssteuerung ein, initialisieren diese alle im Konstruktor und passen den Destruktor an, um das neue Kurvensegment-Array zusammen mit den bestehenden Szenenobjekten sauber freizugeben.
//+------------------------------------------------------------------+ //| 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(); } }
Zunächst fügen wir eine neue Eingabegruppe === VIEW CUBE SETTINGS === hinzu, bei der showViewCubeBackground standardmäßig auf false gesetzt ist, sodass wir einen Hintergrund für die ViewCube-Überlagerung ein- und ausschalten können. Als Nächstes definieren wir Konstanten für das Pan-Icon und den ViewCube: PAN_ICON_SIZE und PAN_ICON_MARGIN für die Abmessungen der Pan-Schaltfläche, VCUBE_SIZE und VCUBE_MARGIN für die Platzierung des ViewCubes. Im geschützten Bereich der Klasse DistributionVisualizer definieren wir Mitglieder für erweiterte Interaktionen: m_isHoveringPanIcon und m_isHoveringViewCube zur Hover-Erkennung, das Array m_curveSegments und m_curveSegmentCount zur Verwaltung von 3D-Kurven, die Booleschen Werte m_panMode und m_isPanning zusammen mit m_panStartX und m_panStartY für den Pan-Status, m_viewTarget als DXVector3 für den Kamerafokus, m_targetAngleX und m_targetAngleY für Animationsziele, m_isAnimating mit m_animSteps, m_animStep, m_animStartAngleX und m_animStartAngleY für flüssige Übergänge, m_vcubeHoverZone (Zeichenkette) für erkannte Würfelbereiche sowie m_vcubeCenterX und m_vcubeCenterY für die Würfelpositionierung.
Im Konstruktor initialisieren wir folgende Variablen: Wir setzen m_isHoveringPanIcon und m_isHoveringViewCube auf false, m_curveSegmentCount auf 0, m_panMode und m_isPanning auf false, m_panStartX und m_panStartY auf 0, m_viewTarget auf den Ursprungsvektor, die Zielwinkel auf 0,0, m_isAnimating auf false, m_animSteps auf 20, m_animStep auf 0, Startwinkel auf 0,0, m_vcubeHoverZone auf eine leere Zeichenkette und die Würfelmittelpunkte auf 0. Wir passen den Destruktor so an, dass die Elemente des neuen Arrays m_curveSegments freigegeben werden, indem wir eine Schleife über m_curveSegmentCount durchlaufen und für jedes Element die Funktion Shutdown aufrufen – zusätzlich zu den bereits vorhandenen Bereinigungsmaßnahmen für Bars, Ebenen und Achsen. Nachdem dies erledigt ist, passen wir die Initialisierung des dreidimensionalen Kontexts wie folgt an, damit die neue Member-Variable verwendet wird. Zur besseren Übersichtlichkeit haben wir die konkrete Änderung hervorgehoben.
Aktualisierung der Initialisierung des dreidimensionalen Kontexts für dynamische Kamerazielpunkte
Die einzige Änderung besteht darin, den an ViewTargetSet übergebenen fest codierten Ursprungsvektor durch die Membervariable m_viewTarget zu ersetzen, sodass Pan-Operationen, die m_viewTarget aktualisieren, sofort berücksichtigt werden, wenn die Kamera neu positioniert wird.
//+------------------------------------------------------------------+ //| 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; }
Nachdem der Kontext aktualisiert wurde, erstellen wir nun die dreidimensionalen Kurvensegmente, um die flache, projizierte Spline-Kurve zu ersetzen.
Erstellen der dreidimensionalen Kurvensegmente
Anstatt die PMF als flache Linie in die Szene zu projizieren, erstellen wir eine Reihe ausgerichteter Box-Primitive, die eine durchgehende Röhre bilden, der an aufeinanderfolgende theoretische Datenpunkte ausgerichtet ist. Dadurch erhält die Kurve in der dreidimensionalen Szene räumliche Präsenz, sodass sich die Höhenunterschiede zwischen den Bins in der Perspektive natürlich darstellen.
//+------------------------------------------------------------------+ //| 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; }
Hier definieren wir die Funktion create3DCurveSegments, um eine Reihe von 3D-Box-Primitiven zu erzeugen, die eine röhrenförmige Darstellung der theoretischen Wahrscheinlichkeitsmassenfunktionskurve bilden. In diesem Zusammenhang bedeutet die rohrförmige Darstellung, die Kurve als eine Abfolge miteinander verbundener, boxbasierter Segmente darzustellen, die zusammen einem durchgehenden Rohr ähneln und der Kurve in der 3D-Szene Tiefe sowie eine fließende visuelle Kontinuität verleihen. Die rohrförmige Darstellung ist die beste und einfachste Methode, die uns für diese Präsentation eingefallen ist. Sie können jeden beliebigen Ansatz wählen. Zunächst ermitteln wir die Anzahl der theoretischen Punkte mit ArraySize in m_theoreticalXValues und geben eine Debug-Meldung aus. Wenn weniger als zwei Punkte vorhanden sind, wird false zurückgegeben, da keine Segmente erstellt werden können. Wir durchlaufen das Array m_curveSegments, um alle vorhandenen Segmente mit Shutdown zu schließen, setzen m_curveSegmentCount auf die Anzahl der Punkte minus eins und passen die Größe des Arrays mit der Funktion ArrayResize an.
Als Nächstes werden die RGB-Komponenten mittels Bitoperationen aus theoreticalCurveColor extrahiert. In einer Schleife, die für jedes Segment durchlaufen wird, rufen wir die Methode Create für die Box auf, wobei wir den DX-Dispatcher, die Eingabeszene und die Vektorabmessungen für eine zylinderartige Einheitsform übergeben; Fehler werden durch eine Fehlermeldung und die Rückgabe von false behandelt. Wir legen die Materialeigenschaften fest: diffuse Farbe mit DiffuseColorSet unter Verwendung von normiertem RGB, Spiegelung über SpecularColorSet für den Glanz, Stärke mit SpecularPowerSet auf 32,0f und Emission mit einer leichten Tönung für das Leuchten mithilfe von EmissionColorSet. Anschließend fügen wir jedes Segment mit ObjectAdd unter Übergabe eines Zeigers zur Zeichenfläche hinzu, geben eine Debug-Meldung aus, und true zurück, wenn der Vorgang erfolgreich war.
Bei diesem segmentierten Ansatz wird die Kurve durch zusammenhängende Röhren approximiert, wobei jedes Kästchen später so transformiert wird, dass es den Kurvenpunkten entspricht. Dies ermöglicht Rotation und Tiefenwahrnehmung ohne komplexe Geometrie, was entscheidend ist, um fließende Wahrscheinlichkeitsübergänge in drei Dimensionen zu visualisieren und gleichzeitig eine hohe Leistung zu gewährleisten. Nachdem die Segmente erstellt sind, müssen wir sie nun so positionieren und ausrichten, dass sie mit den tatsächlichen Kurvendaten übereinstimmen.
Aktualisierung der Transformationen von Kurvensegmenten, um den Datenpunkten zu folgen
Da die Segmentboxen als einfache Grundelemente erstellt wurden, berechnet diese Funktion für jede einzelne eine benutzerdefinierte Transformationsmatrix und wendet diese an: Dabei wird das Element auf die richtige Länge gestreckt, so gedreht, dass es mit der Richtung zwischen aufeinanderfolgenden Punkten der Wahrscheinlichkeitsmassenfunktion übereinstimmt, und an der richtigen Position in der dreidimensionalen Szene platziert.
//+------------------------------------------------------------------+ //| 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; }
Hier definieren wir die Funktion update3DCurveSegments, um jedes 3D-Box-Segment, das die Kurve der PMF darstellt, dynamisch zu positionieren und auszurichten, sodass es den Datenpunkten folgt und durch seine röhrenförmige Erscheinung eine verbesserte Tiefenwirkung erzeugt. Wir kehren vorzeitig zurück, wenn keine Daten geladen oder keine Segmente vorhanden sind, berechnen dann die X- und Y-Bereiche mit minimalen Sicherheitsabständen, legen eine Gesamtbreite fest, die dem Histogramm für die Ausrichtung entspricht, und leiten daraus den Versatz, den Barsabstand, die Breite, den Röhrenradius sowie eine Z-Position leicht vor den Bars für die Tiefenstaffelung ab. In einer Schleife über m_curveSegmentCount skalieren wir die Koordinaten x1, y1, x2 und y2 so, dass sie in den 3D-Raum bis zu einer Höhe von 15,0f passen, berechnen den Richtungsvektor aus den durch die Segmentlänge normierten Deltas (mit einem winzigen Mindestwert, um eine Division durch Null zu vermeiden) und geben optional beim ersten Aufruf mithilfe eines statischen Flags Debug-Informationen für das erste Segment aus.
Um jede Einheitsbox in ein orientiertes Rohr umzuwandeln, konstruieren wir eine DXMatrix-Transformation: Zeile 0 streckt die Box entlang der Richtung um die Länge length, Zeile 1 dreht sie senkrecht um den Radius thickness, Zeile 2 legt den Tiefenradius fest und Zeile 3 verschiebt sie an die Startposition bei zPos; anschließend wenden wir sie mit TransformMatrixSet an, um eine präzise Platzierung zu gewährleisten. Abschließend setzen wir das Debug-Flag nach dem ersten Durchlauf zurück und schließen damit die Aktualisierung ab, durch die flache Kurvendaten in eine drehbare dreidimensionale Struktur umgewandelt werden, wodurch die Wahrnehmbarkeit von Wahrscheinlichkeitsschwankungen über die gesamte Verteilung hinweg verbessert wird. Nachdem nun sowohl die Erstellungs- als auch die Aktualisierungsfunktion fertig sind, binden wir sie wie folgt in den bestehenden Szenenaufbau ein.
Einbindung der Erstellung und Aktualisierung in die Szenen-Pipeline
Drei gezielte Ergänzungen verbinden die neuen Kurvenfunktionen mit dem bestehenden Aufbau: Segmente werden bei der anfänglichen Objektkonstruktion erstellt, sofern bereits Daten geladen sind; sie werden beim Wechsel in den dreidimensionalen Modus neu erstellt, falls noch keine vorhanden sind, und bei jedem Neuladen der Verteilungsdaten zusammen mit den Histogramm-Bars aktualisiert.
//--- 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();
Nach dem Kompilieren werden die dreidimensionalen Kurvensegmente wie folgt neben den Histogramm-Bars angezeigt.

Nachdem die Kurve erstellt ist, fügen wir nun das Pan-Symbol-Overlay hinzu, mit dem der Benutzer den Pan-Modus direkt in der dreidimensionalen Ansicht ein- und ausschalten kann.
Das Symbol zum Umschalten des Pan-Modus zeichnen
Um dem Benutzer eine sichtbare Steuerung zur Aktivierung des Pan-Modus zu bieten, zeigen wir ein kreisförmiges Symbol an, das direkt unter der Modusumschalttaste positioniert ist. Das Symbol färbt sich grün, wenn der Pan-Modus aktiv ist, wird beim Darüberfahren mit der Maus dunkler und zeigt ein aus überblendeten Pixeln bestehendes Kreuz-mit-Pfeilen-Symbol an, um darauf hinzuweisen, dass es zum Verschieben durch Ziehen dient.
//+------------------------------------------------------------------+ //| 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 } }
Wir definieren die Funktion drawPanIconOverlay, um ein kreisförmiges Umschalt-Symbol für den Pan-Modus in der 3D-Ansicht darzustellen, das unterhalb der Kopfzeile positioniert wird, wobei wir Konstanten wie PAN_ICON_SIZE und PAN_ICON_MARGIN verwenden. Wir wählen die Hintergrundfarbe aus: grün, wenn m_panMode aktiv ist, eine abgedunkelte Version von themeColor beim Überfahren mit der Maus mithilfe von DarkenColor oder andernfalls eine aufgehellte Version über LightenColor, und wandeln sie anschließend mit teilweiser Deckkraft in ARGB um. Um die Schaltfläche zu erstellen, durchlaufen wir einen quadratischen Bereich in einer Schleife und mischen die Pixel innerhalb des Radius mit blendPixelSet für die Füllung sowie separat für den Rand, indem wir den äußeren Ring überprüfen und dabei das ARGB-Thema für den Umriss verwenden.
Für die Symbolgrafiken legen wir die Pfeillänge und die Größe der Pfeilspitzen fest, zeichnen mit blendPixelSet in weißem ARGB horizontale und vertikale Linien durch die Mitte und fügen an jedem Ende dreieckige Pfeilspitzen hinzu, indem wir versetzte Pixel überblenden, wodurch ein Kreuz mit Zeigern entsteht, das eine intuitive Pan-Anzeige bildet. Dieses Overlay bietet visuelles Feedback und Zugriff auf Umschaltfunktionen. Es wird nach dem Rendern der dreidimensionalen Szene gezeichnet, sodass es darüber liegt, ohne die DirectX-Ausgabe zu beeinträchtigen. Beim Aufruf der Funktion wird das Pan-Overlay wie folgt dargestellt.

Nachdem das Pan-Overlay eingeblendet ist, erstellen wir nun das ViewCube-Overlay, das die Kameraausrichtung widerspiegelt und anklickbare Navigationsbereiche bereitstellt.
Projektion und Darstellung der ViewCube-Überlagerung
Der ViewCube wird als zweidimensionale Überlagerung dargestellt, indem die acht Eckpunkte eines Einheitswürfels durch die aktuellen Kamerawinkel projiziert werden, wobei seine sechs Flächen für eine korrekte Okklusion von hinten nach vorne sortiert werden, wobei jede sichtbare Fläche in ein 3×3-Raster aus anklickbaren Teilbereichen mit bilinearer Interpolation unterteilt wird, jeder Teilbereich mit einer auf der Helligkeit basierenden Schattierung gefüllt wird und farbige Achsenlinien vom projizierten Ursprung als Orientierungshilfe gezeichnet werden.
//+------------------------------------------------------------------+ //| 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); } }
Zunächst definieren wir die Funktion vcubeProject, um einen 3D-Punkt (x, y, z) anhand der Drehwinkel angX und angY auf 2D-Bildschirmkoordinaten (sx, sy) zu projizieren und so eine isometrische Ansicht für den ViewCube zu simulieren. Wir berechnen Kosinus- und Sinuswerte mit MathCos und MathSin, drehen um die X-Achse, indem wir y und z in y2 und z2 umwandeln, anschließend um die Y-Achse, indem wir x mit z2 auf x2 anpassen, skalieren mit einem aus VCUBE_SIZE abgeleiteten Faktor und verschieben die Werte um m_vcubeCenterX und m_vcubeCenterY, um sie zu zentrieren, wodurch eine genaue 2D-Abbildung der Eckpunkte des Würfels ermöglicht wird.
Um den interaktiven ViewCube im 3D-Modus als Overlay darzustellen, implementieren wir die Funktion drawViewCubeOverlay und positionieren sie mithilfe von Konstanten wie VCUBE_SIZE und VCUBE_MARGIN unterhalb des Pan-Icons, und legen die Mittelpunkte m_vcubeCenterX und m_vcubeCenterY fest. Wenn showViewCubeBackground auf true gesetzt ist, mischen wir ein halbtransparentes Feld mit blendPixelSet für die Füllung und die Ränder unter Verwendung einer aufgehellten Version von themeColor. Wir synchronisieren die Winkel mit der aktuellen Kamera, definieren die Eckpunkte des Würfels als Array von Double-Werten und projizieren jeden einzelnen mit vcubeProject auf Ganzzahlpaare in sv. Wir legen Flächenindizes, Namen und Normalen fest, berechnen den Kamerarichtungsvektor aus den Winkeln, ermitteln die Tiefe jeder Fläche durch Mittelwertbildung der projizierten Mittelpunkte und das Skalarprodukt mit den Normalen und sortieren die faceOrder nach aufsteigender Tiefe, um die Darstellung von hinten nach vorne zu gewährleisten und so die Okklusion zu berücksichtigen. Bei den Farben weisen wir den Flächen unterschiedliche Farben zu und passen die Helligkeit anhand des absoluten Skalarprodukts für die Schattierung an.
Mit einem Verkleinerungsfaktor für Unterteilungen durchlaufen wir pro Fläche ein 3x3-Raster, berechnen die Koordinaten der Teilfläche unter Verwendung von bilinear, prüfen, ob der Mauszeiger über einer SubZone aus dem Array faceSubNames schwebt (ein großes statisches 6×3×3-Zeichenfolgenarray, das Zonen wie TopFrontLeft definiert), hellen die Fläche auf und erhöhen den Alpha-Wert, wenn der Mauszeiger darüber schwebt, setzen ARGB zusammen und füllen mit fillQuad. Wir fügen mithilfe von LineAA-Schleifen schwarze Ränder hinzu, zentrieren Beschriftungen auf helleren Flächen mit TextOut und einer kleinen, fetten Schriftart und projizieren und zeichnen dann farbige Achsenlinien vom Ursprung aus mit den Beschriftungen X, Y und Z zur Orientierung.
Wir definieren die bilineare Funktion zur Interpolation von 2D-Quad-Werten am Punkt (u, v) zwischen den Eckpunkten p00 und p11 unter Verwendung gewichteter Summen, was eine glatte Unterteilung im ViewCube ermöglicht und so eine präzise Hover-Erkennung ohne gezackte Kanten gewährleistet. Abschließend definieren wir die Funktion fillQuad, um beliebige Vierecke anhand von px- und py-Arrays zu rasterisieren und auszufüllen, wobei die Min-/Max-Y-Werte ermittelt werden, horizontale Linien abtasten, um über Kantenkreuzungspunkte die Min-/Max-X-Werte zu berechnen (wobei horizontale Kanten separat behandelt werden), und jedes Pixel in der Zeile mit blendPixelSet mischen, wodurch eine einheitliche Flächenwiedergabe in der Würfelüberlagerung ermöglicht wird. Wenn wir diese Funktion aufrufen, wird der ViewCube wie folgt dargestellt.

Nachdem der ViewCube nun korrekt dargestellt wird, implementieren wir nun die Interaktionslogik für die Zonenerkennung, die Klickverarbeitung und die Kameraanimation.
Erkennen von Hover-Zonen und Verarbeiten von Klicks auf den ViewCube
Bei der Zonenerkennung werden die Mittelpunkte aller Flächen, Kanten und Ecken projiziert und der projizierte Punkt ermittelt, der innerhalb eines zonenspezifischen Abstandsschwellenwerts am nächsten am Cursor liegt; das Ergebnis wird in m_vcubeHoverZone gespeichert. Ein Klick auf einen der erkannten Bereiche ordnet diesem Bereichsnamen ein Zielwinkelpaar zu und startet eine zeitgesteuerte Animation in diese Richtung.
//+------------------------------------------------------------------+ //| 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); }
Wir definieren die Funktion detectViewCubeZone, um die jeweilige Unterzone (Fläche, Kante oder Ecke) unter dem Mauszeiger in lokalen Koordinaten zu identifizieren und so eine interaktive Rückmeldung zum ViewCube zu ermöglichen. Wir synchronisieren die Winkel mit der aktuellen Kamera, berechnen den Kamerarichtungsvektor mithilfe von MathCos und MathSin, definieren Arrays für Flächennamen, Normalen und Mittelpunkte als feste Eigenschaften des Würfels und initialisieren bestDist mit einem hohen Wert, während wir m_vcubeHoverZone zurücksetzen. Beim Durchlaufen der Schleife über die Flächen lassen wir die nach hinten gerichteten Flächen anhand des Skalarprodukts mit den Normalen aus, projizieren die Mittelpunkte mit vcubeProject, berechnen den euklidischen Abstand zum Cursor und aktualisieren die Zone, wenn der Abstand weniger als 15 Einheiten beträgt.
Ebenso gilt für 12 Kanten mit vordefinierten Mittelpunkten und Bezeichnungen wie TopFront, die innerhalb von 10 Einheiten überprüft werden, sowie für 8 Ecken wie TopFrontLeft innerhalb von 8 Einheiten, wobei der kleinste Abstand priorisiert wird, um feinkörnige Hover-Bereiche für eine präzise Navigation genau zu erkennen. Diese Zonenerkennung ist für eine benutzerfreundliche Interaktion von entscheidender Bedeutung, da sie den Würfel anhand einfacher Abstandsschwellenwerte ohne Raycasting in anklickbare Bereiche unterteilt und so eine schnelle Identifizierung von Orientierungszielen unter Berücksichtigung der perspektivischen Projektion ermöglicht.
Um auf Klicks zu reagieren, implementieren wir die Funktion handleViewCubeClick, die vorzeitig zurückkehrt, wenn keine Zone vorhanden ist. Wir berechnen die isometrische Neigung vorab mit MathArctan auf 1/sqrt(2) für 35-Grad-Ansichten und einem Wert nahe dem Pol von PI/2 und ordnen dann m_vcubeHoverZone über eine umfangreiche bedingte Kette den Zielwinkeln tX und tY für Standardausrichtungen zu (z. B. Front bei (0,0), Top am Nahepol auf der X-Achse, Kanten bei PI/4-Varianten, Ecken bei isoTilt in Kombination mit Quadranten). Wir setzen m_targetAngleX und m_targetAngleY, erfassen die aktuellen Winkel zu Beginn der Animation, setzen den Schritt auf 0, wobei m_animSteps auf 20 gesetzt ist, aktivieren m_isAnimating, und starten einen 30-ms-Timer mit EventSetMillisecondTimer, um flüssige Übergänge zu gewährleisten. Dieser Klick-Handler ermöglicht ein intuitives Zurücksetzen der Ansicht durch Animationen zu vordefinierten Winkeln und verbessert so die Benutzerfreundlichkeit bei der 3D-Navigation, da keine abrupten Sprünge auftreten. Nachdem der Klick-Handler eingerichtet ist, fügen wir nun den Animations-Tick hinzu und passen den Maus-Ereignis-Handler an, damit die Interaktionen beim Verschieben und Anzeigen des Würfels korrekt weitergeleitet werden.
Animieren von Kameraübergängen und Weiterleiten von Mausereignissen für Verschieben und ViewCube
Der Animations-Tick verschiebt die Kamerawinkel in jedem Zeitintervall mithilfe einer Smooth-Step-Kurve, sodass die Übergänge auf natürliche Weise verlangsamt werden. Der Ereignis-Handler der Maus wird erweitert, um die Hover-Zustände des Pan-Icons und des ViewCubes zu erfassen, je nach m_panMode zwischen Rotation und Pan-Ziehen zu unterscheiden und Linksklick-Ereignisse an den ViewCube-Handler oder die Pan-Umschaltfunktion weiterzuleiten, bevor die bestehende Logik für Ziehen und Größenänderung zum Tragen kommt.
//+------------------------------------------------------------------+ //| 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(); } }
Um die Subzone, über der sich der Mauszeiger befindet, anhand lokaler Koordinaten auf dem ViewCube zu lokalisieren, definieren wir die Funktion detectViewCubeZone. Wir richten die Winkel auf die aktuelle Kamera aus, berechnen den Richtungsvektor mit MathCos und MathSin und definieren Arrays für Flächennamen, Normalen und Mittelpunkte als Würfelflächen. Ausgehend von der größten Entfernung und einer leeren m_vcubeHoverZone durchlaufen wir die Flächen in einer Schleife, überspringen die nach hinten gerichteten Flächen anhand des Skalarprodukts, projizieren die Mittelpunkte mit vcubeProject, berechnen den Abstand zum Cursor mit MathSqrt und aktualisieren den Wert, wenn die Entfernung weniger als 15 Einheiten beträgt. Wir wiederholen diesen Vorgang für 12 Kanten mit vordefinierten Mittelpunkten und Bezeichnungen wie TopFront, wobei wir einen Bereich von 10 Einheiten prüfen, sowie für 8 Ecken wie TopFrontLeft innerhalb von 8 Einheiten, wobei wir den nächstgelegenen Punkt auswählen, um eine detaillierte Interaktion zu ermöglichen.
In handleMouseEvent fügen wir frühere Zustände für Pan- und ViewCube-Hover-Ereignisse hinzu, einschließlich der Zone, aktualisieren m_isHoveringPanIcon über isMouseOverPanIcon und m_isHoveringViewCube über isMouseOverViewCube (Zurücksetzen, wenn nicht 3D), und beziehen diese in die needRedraw-Prüfungen ein. Bei 3D-Canvas-Hover ohne Header oder Würfel: Wenn m_panMode deaktiviert ist, die Rotationslogik beibehalten; wenn aktiviert, bei Betätigung m_isPanning auf true setzen und das Scrollen deaktivieren; beim Ziehen die Deltas berechnen, den Kameravektor durch Rotationen transformieren, die normierten Vektoren forward und right über DXVec3Cross und DXVec3Length ableiten, camUp analog berechnen, Bewegungen anhand des Pan-Faktors aus der Entfernung skalieren, mit DXVec3Add zu m_viewTarget addieren, Startpunkte aktualisieren und Neuzeichnung markieren; beim Loslassen Pan zurücksetzen und Scrollen aktivieren. Im Druckblock: Wenn der Mauszeiger im 3D-Modus über dem ViewCube mit Zone steht, handleViewCubeClick aufrufen, Neuzeichnung setzen, Status aktualisieren, zurückkehren; wenn der Mauszeiger im 3D-Modus über dem Pan-Icon steht, m_panMode umschalten, Neuzeichnung setzen, Status aktualisieren, zurückkehren.
Wir definieren die Funktion isMouseOverPanIcon, um zu erkennen, ob der Mauszeiger über dem Umschaltelement des Pan-Modus schwebt. Die Funktion gibt false zurück, wenn es sich nicht um 3D handelt; andernfalls berechnet sie die Begrenzungen des Symbols anhand der Position und der Konstanten und prüft, ob sich die Maus innerhalb dieser Begrenzungen befindet. Als Nächstes prüft isMouseOverViewCube, ob der 3D-Modus aktiv ist, oder gibt false zurück, berechnet die Fläche anhand der Position und der Ränder; befindet sich die Maus innerhalb dieses Bereichs, ruft die Funktion detectViewCubeZone mit lokalen Versätzen auf, gibt true zurück, wenn die Zone festgelegt ist, andernfalls setzt sie die Zone auf leer zurück und gibt false zurück. Um das Verschieben zu deaktivieren, definieren wir exitPanMode, das – sofern m_panMode aktiv ist – diesen zurücksetzt, mit renderVisualization neu rendert und das Diagramm neu zeichnet, sodass das Beenden über die Escape-Taste gemäß der Ereignisbehandlung möglich ist. Um die Animationen zu aktivieren, rufen wir die Funktion animations im Timer-Ereignis-Handler auf.
Anschluss des Timers, Deinitialisierung und Ereignisbehandler für Charts
Im letzten Schritt wird der Animations-Tick mit dem Timer-Ereignis-Handler verknüpft, die Timer-Bereinigung zur Deinitialisierung hinzugefügt und der Diagramm-Ereignis-Handler um eine Tastaturverzweigung erweitert, die bei Drücken der Escape-Taste exitPanMode aufruft.
//+------------------------------------------------------------------+ //| 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(); }
Hier definieren wir die Ereignisbehandlung durch OnTimer, um Animationen in regelmäßigen Abständen voranzutreiben. Wir prüfen, ob distributionVisualizer nicht NULL ist, und rufen dann tickAnimation dafür auf, um den Übergang des ViewCubes oder andere zeitgesteuerte Effekte zu aktualisieren. In OnDeinit fügen wir EventKillTimer hinzu, um den Timer anzuhalten und sicherzustellen, dass nach der Deinitialisierung keine Ereignisse mehr bestehen bleiben. Wir erweitern OnChartEvent um die Tastenbehandlung: Wenn id den Wert CHARTEVENT_KEYDOWN hat und lparam den Wert 27 (ESC-Taste) annimmt, wird die Funktion exitPanMode des Visualizers aufgerufen, um den Pan-Modus zu deaktivieren, falls dieser aktiv ist. Damit wird eine Tastenkombination zur Vereinfachung bereitgestellt. Damit ist die Implementierung abgeschlossen. Nun muss das System noch getestet werden; darauf wird im nächsten Abschnitt eingegangen.
Backtests
Wir haben die Tests durchgeführt, und unten sehen Sie die kompilierte Visualisierung in einer einzigen GIF-Animation.

Während der Tests stimmte die segmentierte dreidimensionale Kurve über unterschiedliche Versuchsanzahlen hinweg stets mit den Spitzenwerten der Histogramm-Bars überein; im Pan-Modus wurde der Kamerazielpunkt sanft verschoben, ohne die aktuelle Drehung oder den Zoomfaktor zu stören, und durch Klicks auf den ViewCube wurde die Kamera für alle getesteten Flächen-, Kanten- und Eckbereiche sauber in die korrekte Standardausrichtung animiert.
Schlussfolgerung
Zusammenfassend lässt sich sagen, dass wir den dreidimensionalen Viewer für die Binomialverteilung in MQL5 erweitert haben, indem wir eine segmentierte, röhrenförmige Kurve zur räumlich präzise Visualisierung der PMF hinzugefügt, einen Pan-Modus zur kamerarelativen Verschiebung des Kamerazielpunkts integriert und einen interaktiven ViewCube mit Hover-Zonen für Flächen, Kanten und Ecken implementiert haben, die die Kamera in Standardausrichtungen animieren. Die Umsetzung umfasste die Erstellung von Kurvensegmenten auf Basis von Box-Primitive und die Ausrichtung mittels Transformationsmatrizen, die bilineare Unterteilung zur Erkennung von Hover-Bereichen, zeitgesteuerte, sanfte Kameraübergänge sowie eine überarbeitete Ereignisbehandlung für Klicks, Panning und Tastaturkürzel. Nach der Lektüre dieses Artikels können Sie:
- Verwenden Sie die dreidimensionale Kurve, um visuell zu überprüfen, ob der Peak der Wahrscheinlichkeitsmassenfunktion mit dem Histogramm-Bin der höchsten Frequenz übereinstimmt, und so die Modellanpassung direkt aus der Darstellung heraus zu erkennen, ohne in den zweidimensionalen Modus wechseln zu müssen
- Aktivieren Sie den Pan-Modus, um den Fokus der Szene auf Bereiche mit geringer Wahrscheinlichkeit oder weit außerhalb der Mitte liegende Versuchsbereiche zu verlagern, ohne dabei Ihren aktuellen Drehwinkel oder Zoomfaktor zu verlieren.
- Klicken Sie auf eine beliebige Fläche, Kante oder Ecke des ViewCubes, um die Kamera in eine Standardausrichtung zu bewegen – dabei dient die Draufsicht zum gleichzeitigen Vergleich aller Barhöhen und die Vorderansicht zum übersichtlichen Ablesen einzelner Bins.
In den nächsten Teilen werden wir uns mit den statistischen 2D-Verteilungen befassen und weitere Verteilungen grafisch darstellen. Bleiben Sie dran!
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/21335
Warnung: Alle Rechte sind von MetaQuotes Ltd. vorbehalten. Kopieren oder Vervielfältigen untersagt.
Dieser Artikel wurde von einem Nutzer der Website verfasst und gibt dessen persönliche Meinung wieder. MetaQuotes Ltd übernimmt keine Verantwortung für die Richtigkeit der dargestellten Informationen oder für Folgen, die sich aus der Anwendung der beschriebenen Lösungen, Strategien oder Empfehlungen ergeben.
Larry Williams’ Marktgeheimnisse (Teil 15): Handel mit Hidden-Smash Day-Umkehrungen unter Berücksichtigung des Marktkontexts
PD-Arrays meistern: Optimierung des Handels mit Ungleichgewichten in PD-Arrays
Entwicklung eines dynamischen Multi-Pair-EA (Teil 7): Paarübergreifende Korrelationsabbildung für die Handelsfilterung in Echtzeit
Entwicklung eines Toolkits zur Price-Action-Analyse (Teil 63): Automatisierung der Erkennung von steigenden und fallenden Keilen in MQL5
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.
Ich kann die 3D-Kurven nicht sehen
Derzeit positionieren wir die 3D-Kurvensegmente erst einen Schritt nach dem Rendern der Szene, sodass sie im ersten statischen Bild noch nicht an der richtigen Stelle sind. Führen Sie einfach einmal eine Drehbewegung (Linksklick und Ziehen) oder eine Zoom-Geste (Scrollrad) über die Arbeitsfläche aus – dadurch wird ein erneutes Rendern erzwungen und die Kurve wird angezeigt.
Wenn du möchtest, dass die Kurve sofort gerendert wird, sobald du in die 3D-Ansicht wechselst, verschiebe einfach die Funktion zum Rendern der Kurve vor den Render-Aufruf. Siehe unten:
Vorher:
Nachher:
Danke, dass du das bemerkt hast. Ich hoffe, das hilft weiter. Danke.
Ich sehe keine 3D-Kurven
Derzeit werden die Segmente der 3D-Kurve einen Schritt später platziert, als die Szene gerendert wird, weshalb sie im ersten statischen Bild noch nicht zu sehen sind. Führen Sie einfach einmal eine Drehung (Ziehen mit der linken Maustaste) oder eine Skalierung (mit dem Scrollrad) über der Leinwand durch – dies löst ein erneutes Rendern aus, und die Kurve wird angezeigt.
Wenn Sie möchten, dass die Kurve beim Wechsel in den 3D-Modus sofort gerendert wird, verschieben Sie die Funktion zum Rendern der Kurve einfach vor den Renderaufruf. Siehe unten:
Vorher:
Nachher:
Vielen Dank für den Hinweis. Ich hoffe, das hilft weiter. Vielen Dank.
Genauer gesagt, haben Sie mich falsch verstanden.
Sie haben keine 3D-Kurven, sondern nur 2D-Kurven. Es ist wichtig, die korrekte Terminologie zu verwenden, insbesondere in Artikeln.
Hier ist ein Beispiel für eine 3D-Kurve. Wenn sich eine Kurve im dreidimensionalen Raum befindet, ergibt die Projektion einer solchen Kurve auf eine beliebige Ebene niemals eine gerade Linie, wie in Ihrem Fall.
Du hast mich nicht verstanden.
Oder besser gesagt: Du hast mich nicht richtig verstanden.
Du hast keine 3D-Kurven, sondern nur 2D-Kurven. Es ist wichtig, die korrekte Terminologie zu verwenden, insbesondere in Artikeln.
Hier ist ein Beispiel für eine 3D-Kurve. Befindet sich eine Kurve im dreidimensionalen Raum, ergibt die Projektion einer solchen Kurve auf eine beliebige Ebene niemals eine Gerade, wie in deinem Fall
Jetzt verstehe ich, was du meinst. Einverstanden; du hast Recht, was die Terminologie angeht. Streng genommen handelt es sich um eine ebene Kurve (festes Z; nur die Anzahl der Erfolge und die Wahrscheinlichkeit variieren), es ist also keine 3D-Kurve im eigentlichen Sinne, und ich verstehe deinen Einwand.
Die Absicht war, die PMF als reales Objekt in der 3D-Szene darzustellen; drehbar, mit Röhrengeometrie, neben den 3D-Histogrammbalken platziert, sodass sie beim Rendern mit diesen ausgerichtet bleibt und man beide gemeinsam im selben Raum verfolgen kann. „3D“ war also im Sinne einer Darstellung in der 3D-Ansicht gemeint und nicht als echte Raumkurve. Danke für den Hinweis.