Ecctrl API and Configuration Guide

June 15, 2026 ยท View on GitHub

This guide documents the public Ecctrl API, exported types, default values, and runtime data available through refs and stores.

The README is the high-level introduction. This file is the reference document for configuration and integration.

Import Paths

Ecctrl is split into subpath exports so you can import only the systems you use.

import { Ecctrl } from "ecctrl";
import { EcctrlVehicle, ShapeCastWheel, ThrustPropeller } from "ecctrl/vehicle";
import { Joystick, VirtualButton } from "ecctrl/input";
import { EcctrlAnimationStateController } from "ecctrl/animation";
import { useCustomGravity } from "ecctrl/gravity";
import { EcctrlCameraControls } from "ecctrl/camera";
import { TimeControl } from "ecctrl/time";
import { bakeCurveLUT, evaluateCurveLUT } from "ecctrl/curves";
import { CurveEditorPlugin } from "ecctrl/leva";

ecctrl/all re-exports every public subpath and is useful for examples or prototypes.

Package Exports

PathMain exports
ecctrlEcctrl, character types, animation exports, EcctrlUserDataType
ecctrl/vehicleEcctrlVehicle, ShapeCastWheel, ThrustPropeller, vehicle/wheel/propeller types
ecctrl/inputJoystick, VirtualButton, useJoystickStore, useButtonStore
ecctrl/animationEcctrlAnimationStateController, animation store, resolver, animation state types
ecctrl/gravityuseCustomGravity
ecctrl/cameraEcctrlCameraControls
ecctrl/timeTimeControl
ecctrl/curvesbakeCurveLUT, evaluateCurveLUT, curve types
ecctrl/levaCurveEditorPlugin, Leva curve editor types
ecctrl/utilsShared utility exports
ecctrl/allAll public exports

Common Notes

  • Ecctrl components are designed for @react-three/fiber and @react-three/rapier.
  • Ecctrl and EcctrlVehicle extend Rapier RigidBodyProps, so you can pass normal rigid body props such as position, rotation, canSleep, density, gravityScale, and userData.
  • Default controller values are tuned for the default Rapier density/mass scale, roughly density={1}. If you use much heavier bodies, such as the demo's density={200}, suspension, damping, engine power, braking torque, and thrust values need to be scaled up accordingly.
  • ShapeCastWheel and ThrustPropeller accept their underlying R3F group props through intersection types.
  • JoystickProps and VirtualButtonProps extend DOM attributes for typing compatibility, but the current components only consume the documented id, label, and style props.
  • Many handle values are live objects stored in refs. For example, currPos is a THREE.Vector3 object that updates over time. Read it directly during useFrame; clone it if you need a persistent snapshot.
  • readonly runtime data is intended for reading. Use setter functions such as setMovement, setTarget, and setGear for control.

Ecctrl

Import:

import { Ecctrl, type EcctrlHandle, type EcctrlProps } from "ecctrl";

Basic usage:

const ecctrl = useRef<EcctrlHandle>(null);

<Ecctrl ref={ecctrl}>
  <CharacterModel />
</Ecctrl>

EcctrlProps

EcctrlProps extends RigidBodyProps.

PropTypeDefaultDescription
childrenReactNodeundefinedCharacter model or child content inside the rigid body.
debugbooleanfalseShows debug indicators.
enablebooleantrueRuns the controller update loop when true. Set false to skip the whole Ecctrl algorithm.
capsuleHalfHeightnumber0.3Half height of the capsule collider.
capsuleRadiusnumber0.3Radius of the capsule collider.
lockForwardbooleanfalseLocks movement/turning toward the forward direction instead of input direction. Can also be changed with setLockForward.
useCustomForwardbooleanfalseUses the custom forward vector set by setForwardDir instead of camera forward.
useCharacterUpAxisbooleanfalseUses the character local up axis as the movement reference up axis.
enableCustomGravitybooleanfalseReads and applies gravity from useCustomGravity.
gravityDirLerpSpeednumber6Smooth speed for gravity direction/up-axis changes.
maxWalkVelnumber2Target walking speed.
maxRunVelnumber5Target running speed.
accDeltaTimenumber0.2Movement acceleration response factor.
decDeltaTimenumber0.2Deceleration/friction response factor used when damping relative planar velocity on ground.
rejectVelFactornumber1Rejects sideways velocity while moving.
moveImpulsePointOffsetnumber0.5Offset along character Y axis for the movement impulse point.
jumpVelnumber5Upward jump velocity.
jumpDurationnumber0.1Time window where a held jump input is considered active, in seconds.
slopeJumpFactornumber0Adds slope normal influence to the jump direction.
airDragFactornumber0.1Movement control strength while not grounded.
slideGripFactornumber0.5Grip multiplier while sliding on steep slopes.
fallingGravityScalenumber3Gravity scale used while falling.
fallingMaxVelnumber20Maximum falling speed before gravity scale is reduced to zero.
enableToggleRunbooleantruetrue makes run input toggle run state; false makes run input hold-to-run.
groundDetection"shapeCast" | "rayCast""shapeCast"Ground detection mode. ShapeCast is more robust; RayCast is cheaper.
slopeMaxAnglenumberMath.PI / 2.5Maximum walkable slope angle in radians.
floatHeightnumber0.2Target floating height above ground contact.
rayOriginOffestnumber-capsuleHalfHeightLocal Y offset for the ground cast origin. Name keeps current source spelling.
rayHitForgivenessnumber0.28Extra tolerance before losing ground contact.
rayLengthnumbercapsuleRadius + 1Ground cast length.
rayRadiusnumbercapsuleRadius / 2ShapeCast ball radius.
springKnumber80Floating spring stiffness.
dampingCnumber6Floating damping coefficient.
autoBalancebooleantrueApplies angular correction to keep the character upright.
autoBalanceSpringKnumber0.5Auto-balance spring stiffness.
autoBalanceDampingCnumber0.03Auto-balance damping.
autoBalanceSpringOnYnumber0.08Auto-balance local Y spring term.
autoBalanceDampingOnYnumber0.006Auto-balance local Y damping term.
followPlatformbooleantrueSubtracts moving/rotating platform velocity from relative movement.
massRatioFallOffCurveDataCurveData{ points: [{ x: 0, y: 0, r_out: 0 }, { x: 0.5, y: 0, r_in: 0, r_out: 0 }, { x: 1, y: 1, r_in: 0 }] }Curve used to reduce counter impulses based on contacted body mass ratio.
applyCounterMassbooleantrueApplies character support force to the standing body.
applyCounterJumpImpbooleantrueApplies downward impulse to the standing body when jumping.
counterJumpImpFactornumber1Multiplier for jump counter impulse.
applyCounterMoveImpbooleantrueApplies opposite movement impulse to the standing body.
counterMoveImpFactornumber1Multiplier for movement counter impulse.

Additional RigidBodyProps are forwarded to the internal RigidBody. Ecctrl also sets fallback values when not provided:

Rigid body propFallback
position[0, 1, 0]
friction-0.5

The default character spring, damping, jump, and balance values assume a normal default-density character. If you increase collider density heavily, retune springK, dampingC, and balance values with that mass scale in mind.

MovementInput

export type MovementInput = {
  forward?: boolean;
  backward?: boolean;
  leftward?: boolean;
  rightward?: boolean;
  joystick?: { x: number; y: number };
  run?: boolean;
  jump?: boolean;
};

setMovement is partial. Passing only one field updates that field and keeps the previous values for the others.

ecctrl.current?.setMovement({ forward: true });
ecctrl.current?.setMovement({ joystick: { x: 0.2, y: 0.8 } });
ecctrl.current?.setMovement({ jump: true });

EcctrlHandle

FieldTypeDescription
bodyRapierRigidBodyMutable Rapier body. You can use this for teleporting, impulses, sleeping, etc.
colliderRapierColliderCharacter capsule collider.
upAxisTHREE.Vector3Current character up axis.
gravityDirTHREE.Vector3Current gravity direction.
gravityMagnumberCurrent gravity magnitude.
currPosTHREE.Vector3Current world position.
currQuatTHREE.QuaternionCurrent world rotation.
currLinVelTHREE.Vector3Current linear velocity.
currAngVelTHREE.Vector3Current angular velocity.
inputReadonlyMovementInputCurrent input state.
inputDirTHREE.Vector3Normalized movement input direction in world space.
movingDirectionTHREE.Vector3Actual movement direction after slope handling.
relativeVelTHREE.Vector3Velocity relative to the contacted moving object.
relativeVelOnPlaneTHREE.Vector3Relative velocity projected onto the movement plane.
relativeVelOnUpTHREE.Vector3Relative velocity projected onto the up axis.
moveImpulseTHREE.Vector3Last movement impulse applied to the character.
floatingImpulseTHREE.Vector3Last floating/support impulse.
dragFrictionImpulseTHREE.Vector3Last drag/friction impulse.
bodyXAxisTHREE.Vector3Character local X axis in world space.
bodyYAxisTHREE.Vector3Character local Y axis in world space.
bodyZAxisTHREE.Vector3Character local Z axis in world space.
standColliderRapierRigidBody | nullRigid body currently below the character.
standPointTHREE.Vector3Current standing/support point.
standNormalTHREE.Vector3Current ground normal.
isOnGroundbooleanWhether the controller considers the character grounded.
isFallingbooleanWhether the character is currently falling.
isOnPlatformbooleanWhether the character is on a moving/rotating object.
slopeAnglenumberForward slope angle in radians.
actualSlopeAnglenumberActual contacted slope angle in radians.
standFrictionnumberFriction value read from the standing collider.
slideFrictionnumberEffective slide friction coefficient.
isMovingbooleanTrue when movement input direction has length.
moveSpeednumberRelative planar speed.
verticalSpeednumberRelative vertical speed along the reference up axis.
runActivebooleanCurrent run state after toggle/hold logic.
jumpActivebooleanCurrent jump-active state.
lockForwardbooleanCurrent lock-forward state.
turnOnYQuatTHREE.QuaternionPlatform angular follow rotation.

Methods:

MethodSignatureDescription
setMovement(state: MovementInput) => voidPartially updates character input.
setLockForward(lock: boolean) => voidUpdates lockForward at runtime.
setForwardDir(dir: THREE.Vector3) => voidSets custom forward direction when useCustomForward is true.

EcctrlUserDataType

Put this shape on the parent RigidBody userData to exclude objects from Ecctrl casts. The current cast filter reads collider.parent()?.userData, so putting it only on a collider will not affect Ecctrl filtering.

export interface EcctrlUserDataType {
  ecctrl?: {
    excludeRay?: boolean;
    excludeCharacterRay?: boolean;
    excludeVehicleRay?: boolean;
  };
}
FieldDescription
excludeRayExcludes from both character and vehicle Ecctrl ray/shape queries.
excludeCharacterRayExcludes from character ground detection only.
excludeVehicleRayExcludes from vehicle wheel ground detection only.

Example:

<RigidBody userData={{ ecctrl: { excludeCharacterRay: true } } satisfies EcctrlUserDataType} />

EcctrlVehicle

Import:

import {
  EcctrlVehicle,
  type EcctrlVehicleHandle,
  type EcctrlVehicleProps,
  type CarConfigType,
  type DroneConfigType,
  type VehicleInput,
} from "ecctrl/vehicle";

EcctrlVehicle is the parent controller for wheels and propellers. ShapeCastWheel and ThrustPropeller register with the nearest vehicle context at runtime.

Vehicle defaults also assume default-density bodies. Heavy example vehicles use much larger wheel spring/damping, engine horsepower, brake torque, and propeller thrust values.

EcctrlVehicleProps

EcctrlVehicleProps extends RigidBodyProps.

PropTypeDefaultDescription
childrenReactNodeundefinedCollider, visual model, wheels, propellers, or other children.
enablebooleantrueRuns the vehicle update loop when true. Set false to skip the whole vehicle algorithm.
carConfigPartial<CarConfigType>{}Car/wheel control config merged with defaults.
droneConfigPartial<DroneConfigType>{}Drone/propeller control config merged with defaults.
enableCustomGravitybooleanfalseUses useCustomGravity for this vehicle.
gravityDirLerpSpeednumber6Smooth speed for gravity direction/up-axis changes.

VehicleInput

export type VehicleInput = {
  forward?: boolean;
  backward?: boolean;
  steerLeft?: boolean;
  steerRight?: boolean;
  brake?: boolean;

  throttleUp?: boolean;
  throttleDown?: boolean;
  yawLeft?: boolean;
  yawRight?: boolean;
  pitchForward?: boolean;
  pitchBackward?: boolean;
  rollLeft?: boolean;
  rollRight?: boolean;

  joystickL?: { x: number; y: number };
  joystickR?: { x: number; y: number };
};

Car input:

FieldMeaning
forward / backwardDrive demand.
steerLeft / steerRightSteering demand.
brakeBrake demand.
joystickL.xSteering input. In car control it is subtracted from the digital steer value.

Drone input:

FieldMeaning
throttleUp / throttleDownVertical target input.
yawLeft / yawRightYaw input.
pitchForward / pitchBackwardForward/backward input.
rollLeft / rollRightLateral roll input.
joystickLVertical and yaw analog input.
joystickRPitch and roll analog input.

CarConfigType

Defaults:

FieldTypeDefaultDescription
controlMode"VELOCITY" | "POSITION""VELOCITY"Reserved car control mode. Current wheel control path uses velocity-style input.
engineHorsepowernumber6Engine horsepower used to compute max engine torque.
engineMaxRPMnumber6000Engine max RPM used for wheel max angular velocity and torque calculation.
gearRatiosnumber[][10]Gear ratios. One value behaves like a fixed-ratio motor setup.
finalDriveRationumber1Final drive multiplier. driveRatio = gearRatios[gearIndex] * finalDriveRatio.
transmissionMode"auto" | "manual""auto"Auto shifts between gears when gearRatios.length > 1; manual uses setGear.
shiftUpRPMnumber5200RPM threshold for auto upshift.
shiftDownRPMnumber2200RPM threshold for auto downshift.
shiftCooldownnumber0.35Seconds to wait after shifting before the next auto shift.
steerRatenumberMath.PI * 2Steering angle change speed.
maxSteerAnglenumberMath.PI / 6Max steering angle in radians.
reverseTorqueScalenumber1Torque scale when reversing.
reverseRPMScalenumber0.3Reverse wheel speed/RPM scale.
engineTorqueCurveDataCurveData{ points: [{ x: 0, y: 1, r_out: 0 }, { x: 1, y: 0, r_in: 0 }], samples: 50 }Engine torque curve sampled by wheel drive logic.
steerAngleCurveDataCurveData{ points: [{ x: 0, y: 1, r_out: 0 }, { x: 0.2, y: 1, r_in: 0, r_out: 0 }, { x: 1, y: 0.4, r_in: 0 }], samples: 50 }Speed-based steering angle curve.

Engine notes:

  • engineMaxTorque = engineHorsepower * 7022 / engineMaxRPM.
  • Drive torque is distributed to registered drive wheels by driveTorqueWeight.
  • engineRPM is estimated from weighted drive wheel RPM times absolute driveRatio.

DroneConfigType

Defaults:

FieldTypeDefaultDescription
controlMode"VELOCITY" | "POSITION""VELOCITY"Drone control mode.
maxYawRatenumber2Max yaw target rate.
maxHorizSpeednumber30Max horizontal speed target.
maxVertSpeednumber8Max vertical speed target.
maxTiltAnglenumberMath.PI / 4Max tilt angle used by controller.
airDragFactornumber0.2Air drag impulse factor.
TILT_Pnumber15Tilt proportional gain.
TILT_Dnumber3Tilt damping gain.
YAW_POS_Pnumber6Yaw position proportional gain in position mode.
YAW_VEL_Pnumber4Yaw velocity gain.
VERT_POS_Pnumber9Vertical position proportional gain.
VERT_POS_Dnumber7Vertical position damping gain.
HORIZ_POS_Pnumber5Horizontal position proportional gain.
HORIZ_POS_Dnumber5.5Horizontal position damping gain.
HORIZ_VEL_Pnumber1Horizontal velocity gain.
VERT_VEL_Pnumber2Vertical velocity gain.

EcctrlVehicleHandle

FieldTypeDescription
bodyRapierRigidBodyMutable Rapier body.
upAxisTHREE.Vector3Vehicle up axis.
gravityDirTHREE.Vector3Current gravity direction.
gravityMagnumberCurrent gravity magnitude.
currPosTHREE.Vector3Current vehicle position.
currQuatTHREE.QuaternionCurrent vehicle rotation.
currLinVelTHREE.Vector3Current linear velocity.
currAngVelTHREE.Vector3Current angular velocity.
bodyXAxisTHREE.Vector3Vehicle local X axis in world space.
bodyYAxisTHREE.Vector3Vehicle local Y axis in world space.
bodyZAxisTHREE.Vector3Vehicle local Z axis in world space.
targetPosTHREE.Vector3Target position for position-based drone control.
targetFwdTHREE.Vector3Target heading for position-based drone control.
inputReadonlyVehicleInputCurrent vehicle input state.
wheelsInfoWheelsInfoTypeReadonly map of registered wheel info refs.
propellersInfoPropellersInfoTypeReadonly map of registered propeller info refs.
gearIndexnumberCurrent gear index.
driveRationumberCurrent effective drive ratio.
engineRPMnumberCurrent estimated engine RPM.

Methods:

MethodSignatureDescription
setMovement(state: VehicleInput) => voidPartially updates vehicle input.
setTarget(pos?: THREE.Vector3, dir?: THREE.Vector3) => voidUpdates position-mode target position and/or heading.
setGear(gearIndex: number) => voidSets manual gear index. Index is clamped to the available gear range.

WheelsInfoType And PropellersInfoType

export type WheelsInfoType = ReadonlyMap<string, React.RefObject<Readonly<WheelInfoType>>>;
export type PropellersInfoType = ReadonlyMap<string, React.RefObject<Readonly<PropellerInfoType>>>;

Use these maps for effects, audio, UI, telemetry, and debugging. Do not add/remove entries manually; wheels and propellers register themselves.

ShapeCastWheel

Import:

import {
  ShapeCastWheel,
  type ShapeCastWheelProps,
  type WheelInfoType,
} from "ecctrl/vehicle";

ShapeCastWheel must be rendered inside EcctrlVehicle. The forwarded ref is a THREE.Group.

ShapeCastWheelProps

ShapeCastWheelProps extends R3F group props, so position, rotation, scale, and id can be passed normally.

Wheel suspension defaults target default-density vehicle bodies. For heavier vehicles, increase springK and dampingC; also retune tire grip, brake torque, and engine power at the vehicle level.

PropTypeDefaultDescription
childrenReactNodeundefinedVisual wheel model. Rendered only when showWheelModel is true.
debugbooleanfalseShows wheel debug indicators.
enablebooleantrueSkips wheel update when false.
namestring""Optional user label copied into WheelInfoType.
groundDetection"shapeCast" | "rayCast""shapeCast"Wheel ground detection mode.
rayShapeRnumber0.5Wheel cast radius.
rayShapeHnumber0.15Half height of the cylinder ShapeCast.
rayLengthnumber0.5Suspension/cast travel length.
springKnumber180Suspension spring stiffness.
dampingCnumber16Suspension damping coefficient.
driveInvertbooleanfalseInverts drive direction for this wheel.
driveWheelbooleanfalseMarks this wheel as a drive wheel.
driveTorqueWeightnumber1Weight used by EcctrlVehicle when distributing engine torque.
steerInvertbooleanfalseInverts steering direction for this wheel.
steerWheelbooleanfalseMarks this wheel as a steering wheel.
brakeWheelbooleanfalseMarks this wheel as a brake wheel.
maxBrakeTorquenumber40Max brake torque for this wheel.
rollingResistanceCoefnumber0.007Free-rolling resistance coefficient.
lowVelThresholdnumber0.4Low-speed threshold used by tire slip logic.
tireGripFactornumber1.5Overall tire grip multiplier.
lngFrictionEllipseScalenumber1Longitudinal friction ellipse scale.
latFrictionEllipseScalenumber1Lateral friction ellipse scale.
relaxLngRatenumber0.05Longitudinal tire relaxation rate.
relaxLatRatenumber0.1Lateral tire relaxation rate.
minLngRelaxCoeffnumber0.3Minimum longitudinal relaxation coefficient.
minLatRelaxCoeffnumber0.3Minimum lateral relaxation coefficient.
lngSlipRatioCurveDataCurveData{ points: [{ x: 0, y: 0, r_out: 1.45 }, { x: 0.25, y: 1, r_in: 0, r_out: 0 }, { x: 1, y: 0.7, r_in: 0 }] }Longitudinal slip-to-grip curve.
latSlipRatioCurveDataCurveData{ points: [{ x: 0, y: 0, r_out: 1.45 }, { x: 0.15, y: 1, r_in: 0, r_out: 0 }, { x: 1, y: 0.9, r_in: 0 }] }Lateral slip-to-grip curve.
followPlatformbooleantrueSubtracts contacted moving object velocity when computing wheel slip.
massRatioFallOffCurveDataCurveData{ points: [{ x: 0, y: 0.5, r_out: 0 }, { x: 0.5, y: 1, r_out: 0 }, { x: 1, y: 1, r_in: 0 }] }Counter impulse mass ratio curve.
applyCounterMassbooleantrueApplies wheel support force to the contacted dynamic body.
applyCounterFrictionbooleantrueApplies opposite tire friction to the contacted dynamic body.
showWheelModelbooleantrueRenders children as a visual wheel model.
wheelModelDensitynumber1.5Used to estimate wheel model mass/inertia.
wheelModelUpdatebooleantrueUpdates visual wheel suspension position and rotation.
wheelModelRadiusnumber0.5Visual wheel radius used for model offset.
wheelModelLerpPosRatenumber10Visual suspension position smoothing.
wheelModelReversRotationbooleanfalseReverses visual wheel spin direction. Name keeps current source spelling.
debuggerArrowScalenumber10Scale for debug arrows.

WheelInfoType

WheelInfoType is exposed through EcctrlVehicleHandle.wheelsInfo.

FieldTypeDescription
idstringStable wheel id. Uses props.id or generated UUID.
namestringWheel name prop.
enablebooleanCurrent enable prop.
debugbooleanCurrent debug prop.
rayShapeR, rayShapeH, rayLengthnumberWheel cast/suspension dimensions.
springK, dampingCnumberSuspension config.
steerInvert, steerWheelbooleanSteering config.
driveInvert, driveWheel, driveTorqueWeightboolean / numberDrive config.
brakeWheel, maxBrakeTorqueboolean / numberBrake config.
rollingResistanceCoefnumberRolling resistance config.
lowVelThreshold, tireGripFactornumberGrip config.
lngFrictionEllipseScale, latFrictionEllipseScalenumberFriction ellipse scaling.
relaxLngRate, relaxLatRatenumberTire relaxation rates.
minLngRelaxCoeff, minLatRelaxCoeffnumberMinimum tire relaxation coefficients.
followPlatformbooleanMoving platform follow config.
applyCounterMass, applyCounterFrictionbooleanCounter impulse config.
showWheelModel, wheelModelDensity, wheelModelUpdatevariousVisual model config.
wheelModelRadius, wheelModelLerpPosRate, wheelModelReversRotationvariousVisual model config.
debuggerArrowScalenumberDebug arrow scale.
rayPosTHREE.Vector3Current wheel cast origin.
rayDirTHREE.Vector3Current cast direction.
rayRotTHREE.QuaternionCurrent cast rotation.
rayUpDirTHREE.Vector3Current wheel up direction.
rayFwdDirTHREE.Vector3Current wheel forward direction.
rayLeftDirTHREE.Vector3Current wheel left direction.
floatImpTHREE.Vector3Current suspension impulse.
rayHitColliderShapeCastHit | RayColliderIntersection | nullCurrent cast hit.
rayHitBodyRapierRigidBody | nullContacted rigid body.
rayHitPosTHREE.Vector3Contact point used for tire friction.
rayHitNormalTHREE.Vector3Contact normal.
rayHitFricitonnumberContact friction value. Name keeps current source spelling.
rayOriginVelTHREE.Vector3Velocity at wheel origin.
rayHitPointVelTHREE.Vector3Velocity at wheel contact point.
isOnPlatformbooleanWhether the wheel is on a moving object.
lngSlipRationumberLongitudinal slip ratio.
latSlipRationumberLateral slip ratio.
slipStrengthnumberCombined slip strength. Useful for skid marks, tire smoke, and audio.
lngAxisTHREE.Vector3Longitudinal friction axis.
latAxisTHREE.Vector3Lateral friction axis.
lngFricImpTHREE.Vector3Longitudinal friction impulse applied to vehicle.
latFricImpTHREE.Vector3Lateral friction impulse applied to vehicle.
effInertianumberEffective wheel inertia.
supPosTHREE.Vector3Suspension support point used by vehicle impulse application.
steerAnglenumberCurrent steering angle.
driveTorquenumberCurrent drive torque.
brakeTorquenumberCurrent brake torque.
wheelLinVelnumberWheel linear speed derived from angular velocity and radius.
wheelAngVelnumberWheel angular velocity.
setDriveDemand(value: number) => voidInternal demand setter used by EcctrlVehicle.
setBrakeDemand(value: number) => voidInternal demand setter used by EcctrlVehicle.
setSteerDemand(value: number) => voidInternal demand setter used by EcctrlVehicle.
setDriveWheelConfig(value: DriveWheelConfigType) => voidInternal drive config setter used by EcctrlVehicle.
setSteerWheelConfig(value: SteerWheelConfigType) => voidInternal steer config setter used by EcctrlVehicle.

These internal setters are exposed because wheel info is read from live refs, but they are not recommended for user code. Prefer EcctrlVehicleHandle.setMovement, setGear, and vehicle config props.

DriveWheelConfigType

export type DriveWheelConfigType = {
  maxDriveTorque: number;
  maxWheelAngVel: number;
  engineTorqueCurve: CurveLUT;
  reverseTorqueScale: number;
  reverseRPMScale: number;
  driveRatio: number;
};

SteerWheelConfigType

export type SteerWheelConfigType = {
  steerAngleCurve: CurveLUT;
  steerRate: number;
  maxSteerAngle: number;
  maxWheelAngVel: number;
};

ThrustPropeller

Import:

import {
  ThrustPropeller,
  type ThrustPropellerProps,
  type PropellerInfoType,
} from "ecctrl/vehicle";

ThrustPropeller must be rendered inside EcctrlVehicle. The forwarded ref is a THREE.Group.

Propeller thrust defaults target light/default-density bodies. Heavy drones need proportionally larger maxThrust and may need retuned drone PD gains.

ThrustPropellerProps

ThrustPropellerProps extends R3F group props.

PropTypeDefaultDescription
childrenReactNodeundefinedVisual propeller model.
debugbooleantrueShows thrust/torque debug indicators.
enablebooleantrueSkips propeller update when false.
namestring""Optional user label copied into PropellerInfoType.
maxThrustnumber500Max thrust force potential.
torqueRationumber0.6Reaction torque strength relative to thrust.
invertThrustbooleanfalseInverts thrust direction.
invertTorquebooleanfalseInverts reaction torque direction.
showPropellerModelbooleantrueRenders children as propeller model.
propellerModelUpdatebooleantrueUpdates visual propeller spin.
propellerModelMaxSpinnumber50Max visual spin speed.
propellerModelLerpSpinRatenumber10Spin smoothing rate.
debuggerScalenumber1Size scale for static debug indicators.
debuggerArrowScalenumber35Size scale for thrust/torque arrows.

PropellerInfoType

PropellerInfoType is exposed through EcctrlVehicleHandle.propellersInfo.

FieldTypeDescription
idstringStable propeller id. Uses props.id or generated UUID.
namestringPropeller name prop.
enablebooleanCurrent enable prop.
debugbooleanCurrent debug prop.
maxThrustnumberMax thrust config.
torqueRationumberReaction torque config.
invertThrustbooleanThrust inversion config.
invertTorquebooleanTorque inversion config.
thrustPosTHREE.Vector3Local thrust position in vehicle space.
thrustDirTHREE.Vector3Local thrust direction in vehicle space.
thrustPotTHREE.Vector3Local max thrust potential.
torqueDirTHREE.Vector3Local reaction torque direction.
torquePotTHREE.Vector3Local total torque potential, including leverage and reaction torque.
worldThrustPosTHREE.Vector3World-space thrust position after mixer output.
worldThrustDirTHREE.Vector3World-space thrust direction after mixer output.
worldTorqueDirTHREE.Vector3World-space torque direction after mixer output.
thrustImpulseTHREE.Vector3Actual thrust impulse applied this frame.
torqueImpulseTHREE.Vector3Actual torque impulse applied this frame.
finalThrottlenumberMixer output throttle for this propeller.
throttlenumberCurrent local throttle value used by model/debug update.
setThrottle(value: number) => voidSets local throttle value, clamped to [0, 1]. Vehicle mixer also uses this.
lx, ly, lznumberLocal linear thrust potential components.
ax, ay, aznumberLocal angular torque potential components.

Custom Gravity

Import:

import { useCustomGravity, type CustomGravityState } from "ecctrl/gravity";

CustomGravityState

export interface CustomGravityState {
  gravityField: (pos: THREE.Vector3) => THREE.Vector3;
  setGravityField: (fn: (pos: THREE.Vector3) => THREE.Vector3) => void;
  applyGravityField: (body: RapierRigidBody, timeStep: number) => void;
}
FieldDescription
gravityFieldFunction that receives a world position and returns gravity vector at that position. Default returns (0, -9.81, 0).
setGravityFieldReplaces the gravity field function. For per-frame changes, keep the function stable and update refs read by the function.
applyGravityFieldApplies gravity impulse to a body using gravityField(body.translation()) * body.mass() * body.gravityScale() * timeStep. Sleeping bodies are skipped.

Example:

const setGravityField = useCustomGravity((state) => state.setGravityField);
const center = useRef(new THREE.Vector3(0, 20, 0));
const gravity = useMemo(() => new THREE.Vector3(), []);

useEffect(() => {
  setGravityField((pos) => {
    return gravity.subVectors(center.current, pos).normalize().multiplyScalar(9.81);
  });
}, [setGravityField, gravity]);

EcctrlCameraControls

Import:

import {
  EcctrlCameraControls,
  type EcctrlCameraControlsHandle,
} from "ecctrl/camera";

EcctrlCameraControls wraps Drei CameraControls and accepts CameraControlsProps.

Additional handle method:

MethodSignatureDescription
setUp(newUp: THREE.Vector3) => voidUpdates the camera control up axis for custom gravity scenes.

TimeControl

Import:

import { TimeControl, type TimeControlProps } from "ecctrl/time";

Use TimeControl with <Physics paused> when you want manual physics stepping.

const timeScale = useRef(1);

<Physics paused>
  <TimeControl timeScale={timeScale} maxDelta={1 / 30} />
</Physics>

TimeControlProps

type TimeControlValue = number | RefObject<number>;
PropTypeDefaultDescription
pausedbooleanfalseSkips manual stepping when true.
timeScalenumber | RefObject<number>1Multiplier applied to physics delta. Values <= 0 stop stepping.
maxDeltanumber | RefObject<number>1 / 30Clamps large frame deltas before stepping.

Input Components

Import:

import {
  Joystick,
  VirtualButton,
  useJoystickStore,
  useButtonStore,
} from "ecctrl/input";

Joystick

JoystickProps extends React.HTMLAttributes<HTMLDivElement>.

PropTypeDefaultDescription
idstring"default" in storeStore id for this joystick.
joystickMaxRadiusnumber50Max knob movement radius in pixels.
joystickWrapperStyleReact.CSSPropertiesfixed 200x200 circle areaOverrides wrapper style.
joystickBaseStyleReact.CSSProperties100x100 circular baseOverrides base style.
joystickKnobStyleReact.CSSProperties70x70 circular knobOverrides knob style.

Store:

export interface JoystickState {
  active: boolean;
  x: number;
  y: number;
}

export interface JoystickStoreState {
  joysticks: Record<string, JoystickState>;
  setJoystick: (x: number, y: number, id?: string) => void;
  resetJoystick: (id?: string) => void;
}

Joystick output is normalized to roughly [-1, 1].

VirtualButton

VirtualButtonProps extends React.HTMLAttributes<HTMLDivElement>.

PropTypeDefaultDescription
idstringrequiredButton id used in useButtonStore.
labelstringundefinedText displayed on the button cap.
buttonWrapperStyleReact.CSSPropertiesfixed 60x60 circular areaOverrides wrapper style.
buttonCapStyleReact.CSSProperties45x45 circular capOverrides cap style.

Store:

export interface ButtonStoreState {
  buttons: Record<string, boolean>;
  setButtonActive: (id: string, active: boolean) => void;
  resetAllButtons: () => void;
}

Animation State

Import:

import {
  EcctrlAnimationStateController,
  resolveEcctrlAnimationState,
  useEcctrlAnimationStore,
  type EcctrlAnimationState,
  type EcctrlAnimationStateContext,
  type EcctrlAnimationStateResolver,
} from "ecctrl/animation";

EcctrlAnimationState

export type EcctrlAnimationState =
  | "IDLE"
  | "WALK"
  | "RUN"
  | "JUMP_START"
  | "JUMP_IDLE"
  | "JUMP_FALL"
  | "JUMP_LAND";

EcctrlAnimationStateControllerProps

PropTypeDefaultDescription
ecctrlRefObject<EcctrlHandle | null>requiredCharacter controller ref to read.
enabledbooleantrueSkips animation state updates when false.
resolverEcctrlAnimationStateResolverresolveEcctrlAnimationStateCustom state resolver.
onChange(state, context) => voidundefinedCalled when resolved state changes.

EcctrlAnimationStateContext

FieldTypeDescription
handleEcctrlHandleCurrent character handle.
isOnGroundbooleanCurrent grounded state.
wasOnGroundbooleanPrevious grounded state.
isFallingbooleanCurrent falling state.
isMovingbooleanCurrent movement input state.
runActivebooleanCurrent run state.
jumpActivebooleanCurrent jump-active state.

Default resolver behavior:

ConditionState
jumpActive && wasOnGroundJUMP_START
isOnGround && !wasOnGroundJUMP_LAND
isOnGround && !isMovingIDLE
isOnGround && isMoving && runActiveRUN
isOnGround && isMoving && !runActiveWALK
!isOnGround && isFallingJUMP_FALL
!isOnGround && !isFallingJUMP_IDLE

useEcctrlAnimationStore

export interface EcctrlAnimationStoreState {
  animationState: EcctrlAnimationState;
  setAnimationState: (animationState: EcctrlAnimationState) => void;
}

setAnimationState ignores duplicate state values.

Curve LUTs

Import:

import {
  bakeCurveLUT,
  evaluateCurveLUT,
  type CurvePoint,
  type CurveData,
  type CurveLUT,
} from "ecctrl/curves";

Types

export type CurvePoint = {
  x: number;
  y: number;
  r_in?: number;
  r_out?: number;
  w_in?: number;
  w_out?: number;
};

export type CurveData = {
  points: CurvePoint[];
  samples?: number;
};

export type CurveLUT = {
  lut: Float32Array;
  xMin: number;
  xMax: number;
  samples: number;
};
FieldDescription
xInput coordinate. Points are sorted by x before baking.
yOutput coordinate.
r_inIncoming tangent angle in radians.
r_outOutgoing tangent angle in radians.
w_inIncoming tangent weight. 0 blends to linear segment, 1 uses user tangent.
w_outOutgoing tangent weight. 0 blends to linear segment, 1 uses user tangent.
samplesLUT sample count. Default is 50 when not specified by caller code.

Functions

FunctionSignatureDescription
bakeCurveLUT(points: CurvePoint[], samples = 50) => CurveLUTSorts points by x and bakes a weighted cubic Hermite curve into a Float32Array. Requires at least two points.
evaluateCurveLUT(x: number, curve: CurveLUT) => numberSamples the LUT with linear interpolation. Values outside [xMin, xMax] clamp to the end samples.

Leva CurveEditorPlugin

Import:

import {
  CurveEditorPlugin,
  type CurveInput,
  type CurveSettings,
} from "ecctrl/leva";

CurveEditorPlugin is optional and requires leva.

CurveInput

type RangeSetting = {
  value: number;
  min?: number;
  max?: number;
  step?: number;
  pad?: number;
};

type LevaNum = number | RangeSetting;

export type CurveInput = {
  points?: CurvePoint[] | {
    x: LevaNum;
    y: LevaNum;
    r_in?: LevaNum;
    r_out?: LevaNum;
    w_in?: LevaNum;
    w_out?: LevaNum;
  }[];
  samples?: LevaNum;
};

Defaults:

FieldDefault
points[{ x: 0, y: 0, r_out: 0 }, { x: 1, y: 1, r_in: 0 }]
samples50

Normalized editor ranges:

FieldMinMaxStep
samples25001
x010.01
y010.01
r_in / r_out-Math.PI / 2Math.PI / 20.01
w_in / w_out030.01

Endpoint behavior:

  • First point does not expose r_in or w_in.
  • Last point does not expose r_out or w_out.
  • Stale endpoint tangent values are removed during sanitization.

Runtime Integration Patterns

Reading Character State For Gameplay

useFrame(() => {
  const c = ecctrl.current;
  if (!c) return;

  if (c.isOnGround && c.moveSpeed > 0.1) {
    // Footstep, dust, UI, gameplay logic, etc.
  }
});

Reading Wheel State For Effects

useFrame(() => {
  const vehicleHandle = vehicle.current;
  if (!vehicleHandle) return;

  for (const wheel of vehicleHandle.wheelsInfo.values()) {
    const w = wheel.current;
    if (!w.rayHit) continue;

    if (w.slipStrength > 1) {
      // Skid marks, tire smoke, tire audio.
    }
  }
});

Reading Propeller Output For Effects

useFrame(() => {
  const vehicleHandle = vehicle.current;
  if (!vehicleHandle) return;

  for (const propeller of vehicleHandle.propellersInfo.values()) {
    const p = propeller.current;
    const throttle = p.finalThrottle;
    const thrust = p.thrustImpulse.length();
  }
});

Driving Input Without Drei

useFrame(() => {
  ecctrl.current?.setMovement({
    forward: input.forward,
    leftward: input.left,
    rightward: input.right,
    jump: input.jump,
  });

  vehicle.current?.setMovement({
    forward: input.drive,
    steerLeft: input.left,
    steerRight: input.right,
    brake: input.brake,
  });
});

Version Notes

This document targets ecctrl@2.0.0.