Solar Panels sun tracking

Code side

Class hierarchy

Module_SolarPanel → Module_Deployable → PartBehaviourModule

The tracking logic lives almost entirely in Module_Deployable, with Module_SolarPanel overriding two hooks (CalculateTrackingLOS and PostCalculateTracking) to add occlusion testing and power computation.

Per-frame tick

OnModuleFixedUpdate (Module_Deployable.cs) calls UpdateDeployment, which dispatches to CalculateTracking only when the panel is in Extended state.


CalculateTracking:

  1. Find the tracking target. GetTrackingBodyTransformFrame resolves a trackingTransform. For trackingMode = "Sun" it calls Game.UniverseModel.GetStar(transform.Position) to pick the nearest star dynamically.

  2. Compute tracking direction. Converts the star's position into the part's local body frame, then transforms it into world space and into the pivot's local space (relativeDirection).

  3. LOS check. Calls CalculateTrackingLOS (overridden in Module_SolarPanel).

  4. Rotate the pivot (if LOS is clear and isTracking is on). For rotationMode = YAW:

    • Computes a yaw delta: Atan2(relativeDirection.x, z) * 57.3 + TrackingAlignmentOffset (90° for the accordion).

    • Applies a Quaternion.Lerp toward the target using trackingSpeed * UniverseTimeDelta, storing the result in dataDeployable.currentRotation.

  5. Calls PostCalculateTracking on Module_SolarPanel to update exposure and power output.

The actual Transform.localRotation assignment happens in OnLateUpdate, which runs after all physics to avoid conflicts.

LOS / occlusion

Module_SolarPanel.CalculateTrackingLOS:

Checks in order:

  1. SimBlockingBody — set by orbital simulation when the part is in a body's shadow.

  2. Incidence dot — Dot(panelIncidenceDirection, sunDir). If the dot is ≤ 0, the sun is behind the panel and the ray isn't even cast.

  3. Physics.Raycast from secondaryTransform.position + sunDir * RaycastOffset toward the sun:

    • Hit a PQSRenderer → blocked by terrain.

    • Hit a PartBehavior that is not this.part → blocked by another vessel part (self-hits filtered out as geometry artifacts, see the issue #684 comment).

Power output

Module_SolarPanel.PostCalculateTracking:

exposure = Dot(PanelIncidenceDirection, sunDir) // 0..1 flowRate = Rate * exposure * SimStarEnergyScale * EfficiencyMultiplier

SimStarEnergyScale is injected by the sim layer to account for distance from the star.

Retraction

On retract, HasTrackerReturnedToOrigin lerps the pivot back to originalRotation (captured at spawn) in sync with the retraction animation, so the panels don't snap.

Prefab side

Example with solararray_2v_accordion.

From the part JSON:

Field

Value

Meaning

pivotName

"bone_rotator"

The named transform in the mesh that physically rotates to face the sun

secondaryTransform

"raycastTransform"

The named transform used as the LOS ray origin

trackingMode

"Sun"

Tracks the nearest star (resolved dynamically)

rotationMode

"YAW"

Single-axis rotation only

TrackingAlignmentOffset

90.0

90° correction because bone_rotator's local forward doesn't coincide with the panel surface normal

trackingSpeed

0.25

Slew rate (fraction of remaining angle per UniverseTimeDelta tick)

UseRaycastForTrackingDot

true

Incidence dot computed from raycastTransform's orientation, not bone_rotator directly

PanelIncidenceDirection

(1, 0, 0)

Local +X is the panel's active face axis

RaycastOffset

0.25

25 cm offset to prevent the ray from hitting the panel's own collider

LimitRotation

false

Full 360° rotation allowed

isTracking

true

Enables the tracking behavior at all

The mesh exposes two named transforms that must exist in the model hierarchy: bone_rotator (rotated each frame by the tracking math) and raycastTransform (a fixed reference point used for the sun-facing dot product and LOS ray origin).