Skip to content

3D Model System

hifihedgehog edited this page Mar 19, 2026 · 9 revisions

3D Model System

Renders interactive Xbox 360 and DualShock 4 controller models from Wavefront OBJ meshes using HelixToolkit.WPF. Adapted from Handheld Companion (CC BY-NC-SA 4.0).

Namespace: PadForge.Models3D (model classes), PadForge.Views (view)


Architecture Overview

ControllerModelBase (abstract)
    |
    +-- ControllerModelXbox360  (Xbox 360 meshes, colors, rotation points)
    +-- ControllerModelDS4      (DualShock 4 meshes, colors, rotation points)

ControllerModelView (UserControl)
    |
    +-- HelixViewport3D         (3D rendering viewport)
    +-- ModelVisual3D            (hosts the model3DGroup scene graph)
    +-- CompositionTarget.Rendering  (per-frame visual updates)

Model classes own geometry and materials. The view class owns the viewport, input handling, and animation. ControllerModelView.EnsureModel() instantiates the correct model class and assigns it to ModelVisual3D.Content.


ControllerModelBase

File: PadForge.App/Models3D/ControllerModelBase.cs

Abstract base class. Each subclass defines its own meshes, colors, and rotation points.

public abstract class ControllerModelBase : IDisposable

Data Dictionaries

Field Type Description
ButtonMap Dictionary<string, List<Model3DGroup>> PadSetting name to Model3DGroups for highlighting (supports multi-mesh buttons like button + overlay).
ClickMap Dictionary<Model3DGroup, string> Model3DGroup to PadSetting name for hit-test click-to-record. Reverse of ButtonMap.
DefaultMaterials Dictionary<Model3DGroup, Material> Original material per group. Restored after highlight/flash.
HighlightMaterials Dictionary<Model3DGroup, Material> Accent-colored material per group. Applied on press or flash.

Scene Graph

Field Type Description
model3DGroup Model3DGroup Root scene group containing all child meshes. Assigned to ModelVisual3D.Content.
ModelName string "XBOX360" or "DS4". Used for embedded resource path resolution.

Common Geometry Groups

Field Loaded From Description
MainBody MainBody.obj Main controller body mesh
LeftThumb LeftStickClick.obj Left stick click mesh
LeftThumbRing Joystick-Left-Ring.obj Left stick ring mesh (torus)
RightThumb RightStickClick.obj Right stick click mesh
RightThumbRing Joystick-Right-Ring.obj Right stick ring mesh (torus)
LeftShoulderTrigger Shoulder-Left-Trigger.obj Left shoulder trigger mesh
RightShoulderTrigger Shoulder-Right-Trigger.obj Right shoulder trigger mesh
LeftMotor MotorLeft.obj Left rumble motor mesh
RightMotor MotorRight.obj Right rumble motor mesh

Rotation Parameters

Field Type Description
JoystickRotationPointCenterLeftMillimeter Vector3D Left stick tilt pivot
JoystickRotationPointCenterRightMillimeter Vector3D Right stick tilt pivot
JoystickMaxAngleDeg float Max stick tilt angle (degrees)
ShoulderTriggerRotationPointCenterLeftMillimeter Vector3D Left trigger rotation pivot
ShoulderTriggerRotationPointCenterRightMillimeter Vector3D Right trigger rotation pivot
TriggerMaxAngleDeg float Max trigger depression angle (degrees)
UpwardVisibilityRotationAxisLeft/Right Vector3D Shoulder visibility correction axis
UpwardVisibilityRotationPointLeft/Right Vector3D Shoulder visibility correction origin

ButtonFileMap

Maps Handheld Companion .obj filenames (using ButtonFlags enum names) to PadSetting property names (used by the recording system).

protected static readonly Dictionary<string, string> ButtonFileMap = new()
{
    { "B1.obj", "ButtonA" },
    { "B2.obj", "ButtonB" },
    { "B3.obj", "ButtonX" },
    { "B4.obj", "ButtonY" },
    { "L1.obj", "LeftShoulder" },
    { "R1.obj", "RightShoulder" },
    { "Back.obj", "ButtonBack" },
    { "Start.obj", "ButtonStart" },
    { "Special.obj", "ButtonGuide" },
    { "DPadUp.obj", "DPadUp" },
    { "DPadDown.obj", "DPadDown" },
    { "DPadLeft.obj", "DPadLeft" },
    { "DPadRight.obj", "DPadRight" },
    { "LeftStickClick.obj", "LeftThumbButton" },
    { "RightStickClick.obj", "RightThumbButton" },
};

Constructor Flow (Model Loading)

protected ControllerModelBase(string modelName)

Steps are order-dependent:

  1. Set ModelName — determines the resource path prefix ("XBOX360" or "DS4").
  2. Load common geometry via LoadModel(): MainBody, stick rings, motors, triggers.
  3. Register trigger ClickMap entries: LeftShoulderTrigger -> "LeftTrigger", RightShoulderTrigger -> "RightTrigger". Triggers use ClickMap (not ButtonMap) because they are continuous axes, not toggle buttons.
  4. Iterate ButtonFileMap: Calls TryLoadModel() per entry, then RegisterButton() to populate both ButtonMap and ClickMap. Special cases: LeftStickClick.obj and RightStickClick.obj also set LeftThumb/RightThumb references for tilt animation.
  5. Add all parts to model3DGroup.Children — assigned to ModelVisual3D.Content.
  6. Subclass constructor continues — loads extra meshes (face button overlays, symbol meshes), assigns colored materials, calls DrawAccentHighlights() last.

Note: Stick rings are NOT in ClickMap. The view handles ring clicks via IsStickRingHit() with quadrant-based axis detection, since ring clicks must determine axis direction from click position.

RegisterButton

protected void RegisterButton(string padSettingName, Model3DGroup group)

Adds group to ButtonMap[padSettingName] (creates list if needed) and sets ClickMap[group] = padSettingName. This bidirectional mapping enables highlighting (name -> groups) and click detection (group -> name).

DrawAccentHighlights

protected virtual void DrawAccentHighlights()

Creates accent-colored DiffuseMaterial for all children using the app's AccentButtonBackground resource (ModernWpfUI). Falls back to #2196F3 blue. Called at the end of each subclass constructor.

Embedded Resource Loading

protected Model3DGroup LoadModel(string filename)     // Throws FileNotFoundException
protected Model3DGroup TryLoadModel(string filename)  // Returns null on failure

Loads .obj meshes from embedded resources via HelixToolkit's ObjReader. Searches manifest resource names by suffix (.{ModelName}.{filename}) to handle MSBuild digit-prefix mangling.

MSBuild mangling: 3DModels becomes _3DModels in resource names because MSBuild prefixes digit-leading folder names. Suffix matching avoids hard-coding the prefix.

string suffix = $".{ModelName}.{filename}";
foreach (var name in assembly.GetManifestResourceNames())
    if (name.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
        // Found it

Dispose Pattern

public void Dispose()
protected virtual void Dispose(bool disposing)
~ControllerModelBase()

Clears all dictionaries and model3DGroup.Children. Standard dispose pattern with finalizer. Called by EnsureModel() when switching model types.


ControllerModelXbox360

File: PadForge.App/Models3D/ControllerModelXbox360.cs

public class ControllerModelXbox360 : ControllerModelBase

Calls base("XBOX360").

Xbox 360-Specific Mesh Groups

Field Loaded From Description
MainBodyCharger MainBody-Charger.obj Battery pack/charger compartment
SpecialRing SpecialRing.obj Guide button ring
SpecialLED SpecialLED.obj Guide button LED indicator
LeftShoulderBottom LeftShoulderBottom.obj Left bumper bottom piece
RightShoulderBottom RightShoulderBottom.obj Right bumper bottom piece
B1Button B1Button.obj A button colored overlay
B2Button B2Button.obj B button colored overlay
B3Button B3Button.obj X button colored overlay
B4Button B4Button.obj Y button colored overlay

Color Palette

Name Hex Usage
ColorPlasticBlack #707477 Default for most parts
ColorPlasticWhite #D4D4D4 Main body, motors, shoulder bottoms
ColorPlasticSilver #CEDAE1 Guide button
ColorPlasticGreen #7cb63b A button
ColorPlasticRed #ff5f4b B button
ColorPlasticBlue #6ac4f6 X button
ColorPlasticYellow #faa51f Y button

Face button overlays (B1ButtonB4Button) use transparent variants (Alpha = 150) so the base button color shows through.

Rotation Points

Parameter Value
JoystickRotationPointCenterLeftMillimeter (-42.231, -6.10, 21.436)
JoystickRotationPointCenterRightMillimeter (21.013, -6.1, -3.559)
JoystickMaxAngleDeg 19.0
ShoulderTriggerRotationPointCenterLeftMillimeter (-44.668, 3.087, 39.705)
ShoulderTriggerRotationPointCenterRightMillimeter (44.668, 3.087, 39.705)
TriggerMaxAngleDeg 16.0

Material Assignment Order

  1. Face button overlays (B1ButtonB4Button) get transparent color materials and register into ButtonMap alongside base meshes for joint highlighting.
  2. SpecialLED gets green transparent material.
  3. Base face buttons (B1.objB4.obj) get opaque color materials.
  4. Guide button gets silver material.
  5. White parts: MainBody, LeftMotor, RightMotor, LeftShoulderBottom, RightShoulderBottom.
  6. Remaining parts default to black.
  7. DrawAccentHighlights() called last.

ControllerModelDS4

File: PadForge.App/Models3D/ControllerModelDS4.cs

public class ControllerModelDS4 : ControllerModelBase

Calls base("DS4").

DS4-Specific Mesh Groups

Field Loaded From Description
LeftShoulderMiddle Shoulder-Left-Middle.obj Left shoulder middle piece
RightShoulderMiddle Shoulder-Right-Middle.obj Right shoulder middle piece
Screen Screen.obj Touchpad/screen area
MainBodyBack MainBodyBack.obj Back panel
AuxPort Aux-Port.obj Auxiliary port
Triangle Triangle.obj Decorative triangle element
DPadDownArrow DPadDownArrow.obj D-pad down arrow indicator
DPadUpArrow DPadUpArrow.obj D-pad up arrow indicator
DPadLeftArrow DPadLeftArrow.obj D-pad left arrow indicator
DPadRightArrow DPadRightArrow.obj D-pad right arrow indicator

PlayStation Color Palette

Name Hex Usage
ColorPlasticBlack #38383A Default body color
ColorPlasticWhite #E0E0E0 Main body, motors, triangle
MaterialPlasticTriangle #66a0a4 Triangle face button symbol
MaterialPlasticCross #96b2d9 Cross (X) face button symbol
MaterialPlasticCircle #d66673 Circle face button symbol
MaterialPlasticSquare #d7bee5 Square face button symbol

Face Button Symbols

DS4 loads separate symbol meshes (B1-Symbol.obj through B4-Symbol.obj) via TryLoadModel(). Each symbol shares its ButtonMap entry with the base button mesh for joint highlighting. Symbol meshes get PlayStation-specific colors; base meshes default to black.

Rotation Points

Parameter Value
JoystickRotationPointCenterLeftMillimeter (-25.5, -5.086, -21.582)
JoystickRotationPointCenterRightMillimeter (25.5, -5.086, -21.582)
JoystickMaxAngleDeg 19.0
ShoulderTriggerRotationPointCenterLeftMillimeter (-38.061, 3.09, 26.842)
ShoulderTriggerRotationPointCenterRightMillimeter (38.061, 3.09, 26.842)
TriggerMaxAngleDeg 16.0

OBJ Mesh Files

Directory Structure

PadForge.App/3DModels/
  XBOX360/         (31 meshes)
    MainBody.obj                             (body)
    MainBody-Charger.obj                     (battery pack)
    Joystick-Left-Ring.obj                   (left stick torus ring)
    Joystick-Right-Ring.obj                  (right stick torus ring)
    MotorLeft.obj, MotorRight.obj            (rumble motors)
    Shoulder-Left-Trigger.obj                (left trigger)
    Shoulder-Right-Trigger.obj               (right trigger)
    SpecialRing.obj, SpecialLED.obj          (guide button ring + LED)
    LeftShoulderBottom.obj                   (left bumper bottom)
    RightShoulderBottom.obj                  (right bumper bottom)
    B1.obj, B2.obj, B3.obj, B4.obj          (base face buttons: A, B, X, Y)
    B1Button.obj, B2Button.obj,             (colored face button overlays)
      B3Button.obj, B4Button.obj
    L1.obj, R1.obj                           (shoulder bumpers)
    Back.obj, Start.obj, Special.obj         (center buttons)
    DPadUp.obj, DPadDown.obj,               (D-pad directions)
      DPadLeft.obj, DPadRight.obj
    LeftStickClick.obj, RightStickClick.obj  (stick click caps)
  DS4/             (36 meshes)
    MainBody.obj                             (body)
    MainBodyBack.obj                         (back panel)
    Joystick-Left-Ring.obj                   (left stick torus ring)
    Joystick-Right-Ring.obj                  (right stick torus ring)
    MotorLeft.obj, MotorRight.obj            (rumble motors)
    Shoulder-Left-Trigger.obj                (left trigger L2)
    Shoulder-Right-Trigger.obj               (right trigger R2)
    Shoulder-Left-Middle.obj                 (left shoulder middle)
    Shoulder-Right-Middle.obj                (right shoulder middle)
    Screen.obj                               (touchpad area)
    Aux-Port.obj                             (auxiliary port)
    Triangle.obj                             (decorative triangle)
    DPadDownArrow.obj, DPadUpArrow.obj,     (D-pad arrow indicators)
      DPadLeftArrow.obj, DPadRightArrow.obj
    B1.obj, B2.obj, B3.obj, B4.obj          (base face buttons: Cross, Circle, Square, Triangle)
    B1-Symbol.obj, B2-Symbol.obj,           (PlayStation symbol overlays)
      B3-Symbol.obj, B4-Symbol.obj
    L1.obj, R1.obj                           (shoulder bumpers)
    Back.obj, Start.obj, Special.obj         (Share, Options, PS buttons)
    DPadUp.obj, DPadDown.obj,               (D-pad directions)
      DPadLeft.obj, DPadRight.obj
    LeftStickClick.obj, RightStickClick.obj  (stick click caps)

All OBJ files are embedded as EmbeddedResource in the project file:

<EmbeddedResource Include="3DModels\**\*.obj" />

ControllerModelView

File: PadForge.App/Views/ControllerModelView.xaml, ControllerModelView.xaml.cs

WPF UserControl hosting a HelixViewport3D for 3D controller visualization (~1438 lines code-behind).

XAML Structure

<helix:HelixViewport3D
    IsRotationEnabled="False" IsPanEnabled="False"
    IsMoveEnabled="False" IsZoomEnabled="False"
    ShowViewCube="False" Background="Transparent"
    IsManipulationEnabled="False">
    <helix:SunLight />
    <helix:DirectionalHeadLight Brightness="0.35" />
    <ModelVisual3D x:Name="ModelVisual3D" />
    <PerspectiveCamera FieldOfView="50"
        LookDirection="0,0.793,-0.609"
        Position="0,-159,122" UpDirection="0,0,1" />
</helix:HelixViewport3D>

All built-in HelixToolkit camera controls are disabled. Rotation, zoom, and pan are handled by custom event handlers to avoid conflicts with PadForge's click-to-map and touch gesture handling.

Events

public event EventHandler<string> ControllerElementRecordRequested;

Fired when the user clicks a mappable 3D element. The string argument is the PadSetting target name (e.g., "ButtonA", "LeftThumbAxisXNeg").

Private State

Field Type Description
_vm PadViewModel Bound ViewModel
_currentModel ControllerModelBase Active 3D model
_dirty bool Render-frame update flag
_triggerAngleLeft/Right float Current trigger angles (change detection)
_flashTimer DispatcherTimer Map All flash timer (400 ms)
_flashTarget string PadSetting name being flashed
_flashOn bool Flash toggle state
_arrowVisual ModelVisual3D Directional arrow for axis recording
_quadrantRingVisual ModelVisual3D Stick ring quadrant highlight
_quadrantRingMaterial DiffuseMaterial Quadrant ring material (alpha toggled for flash)
_hoverGroup Model3DGroup Hovered button/trigger group
_hoverStickRing Model3DGroup Hovered stick ring
_hoverQuadrant string Hover quadrant axis string
_hoverQuadrantVisual ModelVisual3D Quadrant wedge overlay for hover
_isLeftDragging bool Left-drag active (rotation)
_leftDragStart Point Left-button down position (drag threshold)
_isRightDragging bool Right-drag active (panning)
_rightDragLast Point Last mouse position during drag
_modelYaw double Yaw rotation (degrees, Z axis)
_modelPitch double Pitch rotation (degrees, X axis, clamped −60–60)
_touchDragId int? First touch ID (rotation)
_touchSecondId int? Second touch ID (pinch-to-zoom)
_touchSecondLast Point Last second-touch position
_pinchStartDist double Inter-finger distance at pinch start
_pinchMidpoint Point Two-finger midpoint for panning
_modelRotation Transform3DGroup Persistent rotation on ModelVisual3D
_yawRotation AxisAngleRotation3D Yaw: axis (0,0,1)
_pitchRotation AxisAngleRotation3D Pitch: axis (1,0,0)

ViewModel Binding

public void Bind(PadViewModel vm)
public void Unbind()

Bind subscribes to PropertyChanged, hooks CompositionTarget.Rendering, and calls EnsureModel(). OutputType changes trigger EnsureModel() to switch models. CurrentRecordingTarget changes trigger flash animation and arrow overlays. All other changes set _dirty.

Model Lifecycle

private void EnsureModel()

Selects model from OutputType and VJoyConfig.Preset:

  • DualShock4 or VJoy with Preset == DualShock4 -> "DS4"
  • All others -> "XBOX360"

Returns immediately if the current model matches. Otherwise disposes the old model, creates the new one, and assigns to ModelVisual3D.Content.

Render-Frame Update Pipeline

CompositionTarget.Rendering handler (~60 fps), gated by _dirty flag:

OnRendering()
    |
    +-> _dirty check (skip if clean)
    |
    +-> HighlightButtons()       -- swap materials for 15 buttons
    +-> UpdateJoystick() x2      -- tilt left/right stick meshes
    +-> UpdateTrigger() x2       -- rotate left/right trigger meshes

HighlightButtons()

Iterates the 15-element ButtonProperties array, reads each PadViewModel bool via GetButtonState(), and swaps between DefaultMaterials and HighlightMaterials:

private static readonly string[] ButtonProperties =
{
    "ButtonA", "ButtonB", "ButtonX", "ButtonY",
    "LeftShoulder", "RightShoulder",
    "ButtonBack", "ButtonStart", "ButtonGuide",
    "DPadUp", "DPadDown", "DPadLeft", "DPadRight",
    "LeftThumbButton", "RightThumbButton"
};

For each button, iterates all Model3DGroup entries in ButtonMap (multi-mesh support). Only modifies GeometryModel3D children with DiffuseMaterial.

UpdateJoystick()

private void UpdateJoystick(
    short rawX, short rawY,
    Model3DGroup thumbRing, Model3D thumb,
    Vector3D rotationPoint, float maxAngleDeg)
  1. Normalizes raw values (short.MaxValue) to −1–1 range.
  2. Gradient highlight: Blends default/highlight materials by deflection magnitude via GradientHighlight().
  3. Rotation: AxisAngleRotation3D for X (around Z) and Y (around X), centered at rotationPoint. Both ring and thumb meshes share the same Transform3DGroup.

UpdateTrigger()

private void UpdateTrigger(
    double triggerNorm,
    Model3DGroup triggerModel,
    Vector3D rotationPoint,
    float maxAngleDeg,
    ref float prevAngle)
  1. Gradient color: Blends default/highlight materials by trigger value (0–1).
  2. Rotation: AxisAngleRotation3D around X axis at rotationPoint. Max angle: -maxAngleDeg * value.
  3. Change detection: Skips update if angle delta < 0.01 degrees.

GradientHighlight()

private static DiffuseMaterial GradientHighlight(Material default, Material highlight, float factor)

ARGB linear interpolation between default and highlight colors. Creates a new DiffuseMaterial per call (no caching; only called when values change).

Model Rotation and Panning

Turntable rotation (left-drag) and camera panning (right-drag) via Preview (tunneling) events, which fire before HelixToolkit's built-in handlers and mark e.Handled = true to prevent double-processing.

Event Action
PreviewMouseLeftButtonDown Record start position, capture mouse for rotation
PreviewMouseLeftButtonUp Drag < 5 px -> hit-test for click-to-record; otherwise end drag
PreviewMouseRightButtonDown Capture mouse, store start position for panning
PreviewMouseRightButtonUp Release capture
PreviewMouseMove Left-drag: rotate; right-drag: pan; no button: hover highlight
PreviewMouseWheel Zoom camera along look direction
PreviewTouchDown First finger: rotation. Second finger: pinch-to-zoom + pan.
PreviewTouchMove One finger: rotation. Two fingers: pinch-to-zoom + midpoint pan.
PreviewTouchUp Release touch; demote second finger to first if needed
PreviewStylusSystemGesture Block WPF press-and-hold / flick gestures
ManipulationStarting Cancel WPF manipulation HelixToolkit may re-enable

Rotation is applied to ModelVisual3D.Transform (not the camera), keeping lighting screen-relative:

  • Yaw: axis (0,0,1), angle = _modelYaw
  • Pitch: axis (1,0,0), angle = _modelPitch (clamped −60–+60 degrees)
  • Sensitivity: 0.5 degrees per pixel. "Reset View" sets both to 0.

Touch details:

  • Single finger: Rotation (same as left-drag), captured via _touchDragId.
  • Second finger: Pinch-to-zoom (inter-finger distance vs _pinchStartDist, camera moves along look direction) + pan (midpoint tracking, camera moves perpendicular).
  • Stylus suppression: WPF press-and-hold (synthesized right-click) and flick gestures blocked via PreviewStylusSystemGesture.

Click-to-Record Hit Testing

private void Viewport_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
  1. Viewport3DHelper.FindHits() at click position.
  2. For each hit GeometryModel3D:
    • Stick ring: IsStickRingHit() checks LeftThumbRing/RightThumbRing, delegates to DetermineAxisFromQuadrant().
    • ClickMap: Walks entries to find the containing Model3DGroup.
  3. Fires ControllerElementRecordRequested with the PadSetting target name.

Quadrant Detection (Stick Rings)

private bool IsStickRingHit(GeometryModel3D hitGeo, Point3D hitPos, out string axis)

Checks if hit geometry belongs to a stick ring, then calls:

private static string DetermineAxisFromQuadrant(
    Point3D hitPos, Vector3D center, string xAxis, string yAxis)

Uses hit position relative to joystick rotation center:

  • Dominant X (|deltaX| > |deltaZ|): Returns xAxis or xAxis + "Neg" by deltaX sign.
  • Dominant Z: Returns yAxis or yAxis + "Neg" by deltaZ sign.
  • Y-axis inversion: Model Z-up = stick up. deltaZ >= 0 maps to yAxis + "Neg" because Step 3's NegateAxis inverts Y output, so stick-up in-game maps to the positive direction.

Hover Highlighting

Viewport_MouseMove hit-tests at the cursor on every move:

  • Buttons/triggers: ApplyHoverHighlight() sets highlight material. RestoreHoverGroup() restores default (skipped during flash animation).
  • Stick rings: ShowHoverQuadrant() creates a semi-transparent wedge overlay from the ring's mesh triangles, clipped to the target quadrant.
  • ClearHover() removes all hover state and resets the cursor.
  • Viewport_MouseLeave also clears hover (and releases dangling right-drag).

Flash Animation (Map All)

private void UpdateFlashTarget(string target)

Starts when CurrentRecordingTarget changes. A DispatcherTimer at 400 ms toggles highlight/default materials:

  • Buttons/triggers: Swaps materials via ResolveFlashGroups().
  • Stick axes: ShowQuadrantRingOverlay() for the target quadrant + ShowArrowForTarget() for direction. FlashQuadrantRing() toggles overlay alpha between 200 and 0.
  • Stops when CurrentRecordingTarget becomes null.

Arrow Overlay

private void ShowArrowForTarget(string target)
private void RemoveArrow()

Creates a 3D arrow (ModelVisual3D) via CreateFlatArrow():

  • Flat box (shaft) + triangular prism (head).
  • Positioned at stick center, offset forward (Y = center.Y − 25) for visibility.
  • Direction from target: LeftThumbAxisX = right, LeftThumbAxisXNeg = left, etc.
  • Uses the app's accent color.

Quadrant Ring Overlay

private void ShowQuadrantRingOverlay(string target)
private MeshGeometry3D BuildClippedQuadrantMesh(
    Model3DGroup ring, Vector3D center, bool isX, bool isNeg)

Builds a highlight overlay from the ring's mesh triangles:

  1. Two half-planes at +/−45 degrees isolate one quadrant.
  2. Sutherland-Hodgman clipping: Clips each triangle against both half-planes via ClipPolygonByHalfPlane().
  3. Torus-outward offset: OffsetTorusOutward() pushes vertices 0.8 mm outward along the tube's radial direction to prevent z-fighting (computes nearest point on torus center circle, offsets along tube normal).
  4. Triangulates clipped polygons as fans.

Material System

All models use WPF DiffuseMaterial with SolidColorBrush. Three categories:

Category Source Storage Usage
Default Static Color fields per subclass (e.g., ColorPlasticWhite). Xbox 360 face overlays use Alpha = 150. DefaultMaterials[group] Restored after highlight/flash
Highlight DrawAccentHighlights() reads AccentButtonBackground (ModernWpfUI), falls back to #2196F3. HighlightMaterials[group] Applied on press or flash
Gradient GradientHighlight() ARGB-interpolates default/highlight per call. None (created per call) Sticks and triggers (proportional)

Gradient materials are not cached — the dirty flag limits updates to one per render frame, WPF3D materials are lightweight, and gradients only update when values change.


Coordinate System and Transformations

Model Space

Standard WPF3D right-handed coordinates:

Axis Direction
X Left (negative) / Right (positive)
Y Forward-backward (camera view axis)
Z Up (positive) / Down (negative)

Camera Setup

PerspectiveCamera: Position (0, -159, 122) (behind and above), LookDirection (0, 0.793, -0.609) (forward + slightly down), Z-up, 50-degree FOV.

Model Rotation Transform

Applied to ModelVisual3D.Transform (not the camera) so lighting stays screen-relative.

Transform Axis Source Clamp
Yaw Z (0,0,1) Left-drag horizontal None
Pitch X (1,0,0) Left-drag vertical −60–+60 degrees

Joystick Tilt Transform

Transform3DGroup on both ring and thumb meshes:

  1. X tilt: Around Z axis, proportional to stick X, centered at JoystickRotationPointCenter{Left/Right}Millimeter.
  2. Y tilt: Around X axis, proportional to stick Y, same center.

Both capped at JoystickMaxAngleDeg (19 degrees for Xbox 360 and DS4).

Trigger Rotation Transform

Rotates around X axis at ShoulderTriggerRotationPointCenter{Left/Right}Millimeter. Angle: -TriggerMaxAngleDeg * value (negative = downward). Skips update if angle delta < 0.01 degrees.


Lighting

Two light sources in XAML:

  • SunLight: HelixToolkit built-in (ambient + directional, world-space).
  • DirectionalHeadLight: Camera-relative at brightness 0.35, prevents dark spots during rotation.

Performance Considerations

Technique Detail
Dirty flag batching 15 buttons + 4 axes + 2 triggers coalesced into one render-frame update.
Trigger change detection Skips rotation if angle delta < 0.01 degrees.
No gradient caching New DiffuseMaterial per call; acceptable since dirty flag limits frequency and WPF3D materials are lightweight.
One-time mesh loading OBJ meshes loaded in constructor; EnsureModel() only recreates on Xbox 360/DS4 switch.
Preview events Tunneling events prevent double-processing by HelixToolkit and PadForge.

See Also

  • 2D Overlay SystemControllerModel2DView (PNG overlay alternative to 3D), ControllerSchematicView, KBMPreviewView, MidiPreviewView
  • ViewModelsPadViewModel properties bound by ControllerModelView
  • XAML ViewsPadPage hosts and switches between 3D, 2D, schematic, KBM, and MIDI views
  • Virtual Controllers — Output type determines which preview view is active
  • Engine LibraryGamepad struct providing button/axis state for 3D animation
  • Build and Publish — 3D OBJ meshes (3DModels/) included as EmbeddedResource items

Clone this wiki locally