Here's the generated code, show it to your favorite AI and watch how it ACTUALLY understands what's going on ๐
// RC Airplane Demo for Figmin XR
// Config constants
var PLANE_MODEL_URL = "
sketchfab.com/3d-models/a2b7โฆ"; //brown biplane
var GRAVITY_SCALE = 0.037;
var CONTROL_SENSITIVITY = 0.2;
// Sim params (tuned for targetSize โ 0.163)
var MAX_AIRSPEED_FOR_TORQUE = 0.25;
var RHO = 1.2; // Air density kg/m3
var WING_AREA = 0.01; // m2
var TAIL_WING_AREA = 0.005; // mยฒ
var TAIL_LEN = 0.1; // meters from CG to tail
var TAIL_LIFT_COEF = 3;
var CD0 = 0.02; // Parasite drag coeff
var K = 0.05; // Induced drag factor
var CL_MAX = 7.5; // Max lift coeff
var DEBUG_SCALE = 0.1; // Scale for debug line lengths
var STALL_ANGLE_RAD = Math.PI / 18; // ~10 deg in radians for peak Cl
var TAKEOFF_SPEED = 0.15; // m/s threshold to apply lift
var planeHeight = 0.05;
var grounded = false;
// Globals
var plane = null;
var isInitialized = false;
// Hardcoded defaults
var yawSensitivity = 0.04;
var pitchSensitivity = 0.03;
var rollSensitivity = 0.1;
var maxThrust = 0.25;
var clMax = 1.5;
var wingArea = 0.05;
function init() {
plane = figmin.createObject(figmin.ObjectType.MODEL3D, {
url: PLANE_MODEL_URL,
name: "RCPlane",
position: { x: 0, y: 1, z: -2 },
targetSize: 0.163
});
plane.onStatusChanged(onPlaneStatusChanged);
figmin.setUpdateFunction(update);
}
function onPlaneStatusChanged(obj) {
if (obj.status.state !== figmin.ObjectState.READY || isInitialized) {
return;
}
isInitialized = true;
obj.collider.type = figmin.ColliderType.APPROXIMATE;
obj.addComponent(figmin.ComponentType.PHYSICS, {
mass: 0.05,
drag: 0.5,
angularDrag: 0.3,
gravityStrength: GRAVITY_SCALE,
collisionDetectionMode: figmin.physics.CollisionDetectionMode.CONTINUOUS_DYNAMIC
});
obj.addComponent(figmin.ComponentType.PHYSICS_MATERIAL, {
bounciness: 0.3,
staticFriction: 0,
dynamicFriction: 0
});
figmin.enableInputConnect("RC Controls", [
figmin.InputCode.SELECT_RIGHT,
figmin.InputCode.AXIS_Y_RIGHT,
figmin.InputCode.AXIS_X_RIGHT,
figmin.InputCode.AXIS_X_LEFT,
figmin.InputCode.THUMB_RIGHT
], onInputConnectionChanged, plane);
// plane height ready
planeHeight = obj.transform.size.y;
//play animation if any
obj.model3d.play();
figmin.connectToInputRequest();
}
function onInputConnectionChanged(connected, playerId) {
console.log("Inputs " (connected ? "connected" : "disconnected") " from player " playerId);
}
function update(dt) {
if (plane === null || plane.status.state !== figmin.ObjectState.READY || plane.physics === null) {
return;
}
// Inputs
var throttleIn = figmin.getInput(
figmin.InputCode.SELECT_RIGHT);
var pitchIn = -figmin.getInput(figmin.InputCode.AXIS_Y_RIGHT);
var rollIn = -figmin.getInput(figmin.InputCode.AXIS_X_RIGHT);
var yawIn = figmin.getInput(figmin.InputCode.AXIS_X_LEFT);
var resetDown = figmin.getInput(figmin.InputCode.THUMB_RIGHT);
// Velocity and airspeed
var velocity = plane.physics.velocity;
var airspeed2 = velocity.x * velocity.x velocity.y * velocity.y velocity.z * velocity.z;
var airspeed = Math.sqrt(airspeed2);
if (airspeed < 0.01 && throttleIn < 0.01) return; //Early out if not moving and don't intend to move
// Update display with airspeed and AoA
var forward = plane.transform.forward;
var velocityNorm = vectorNormalize(velocity);
var signedAoaRad = Math.atan2(vectorDot(velocityNorm, plane.transform.up), vectorDot(velocityNorm, forward));
var signedAoa = signedAoaRad * (180 / Math.PI);
// Reset rotation
if (resetDown) {
var currentRot = plane.transform.rotation;
plane.transform.rotation = { x: 0, y: currentRot.y, z: 0 };
}
// Lift coeff (signed)
var clRaw = -CL_MAX * signedAoaRad / (Math.PI / 18); // negative to fix direction
var cl = Math.max(-CL_MAX, Math.min(CL_MAX, clRaw));
// Lift force
var v = vectorNormalize(velocity);
var up = plane.transform.up;
var dot = vectorDot(up, v);
var proj = vectorScale(v, dot);
var liftDir = {
x: up.x - proj.x,
y: up.y - proj.y,
z: up.z - proj.z
};
liftDir = vectorNormalize(liftDir);
var liftMagnitude = 0.5 * RHO * airspeed2 * WING_AREA * cl;
var liftForceWorld = vectorScale(liftDir, liftMagnitude);
var rotation = plane.transform.rotationq;
var liftLocal = quaternionRotateVector(quaternionConjugate(rotation), liftForceWorld);
plane.physics.applyForce(liftLocal);
// Drag: Parasite induced, opposite velocity
var cd = CD0 K * cl * cl; // Uses signed cl^2 so positive
var dragMagnitude = 0.5 * RHO * airspeed2 * WING_AREA * cd;
var dragDir = vectorNormalize(velocity);
var dragForceWorld = vectorScale(dragDir, -dragMagnitude);
// Transform drag world to local
var dragLocal = quaternionRotateVector(quaternionConjugate(rotation), dragForceWorld);
plane.physics.applyForce(dragLocal);
// Thrust: Local Z
var thrustLocal = {
x: 0,
y: 0,
z: throttleIn * maxThrust / 10
};
plane.physics.applyForce(thrustLocal);
// Stabilizer Forces
// This section simulates aerodynamic effects from the vertical and horizontal stabilizers.
var velocityLocal = quaternionRotateVector(
quaternionConjugate(plane.transform.rotationq),
velocity
);
// Vertical stabilizer
var rudderDeflection = -yawIn * 0.1; // Simulate a small rudder deflection: This reduces the stabilizer's tendency to resist yaw at higher speeds,
var effectiveLateralVelocity = velocityLocal.x - velocityLocal.z * Math.tan(rudderDeflection);
var slipSign = Math.sign(effectiveLateralVelocity);
var tailForceY = 0.5 * RHO * effectiveLateralVelocity * effectiveLateralVelocity * TAIL_WING_AREA * TAIL_LIFT_COEF;
var tailTorqueY = slipSign * tailForceY * TAIL_LEN;
// Horizontal stabilizer
var effectiveVerticalVelocity = velocityLocal.y; // how fast the tail is being pushed up or down
var pitchSlipSign = Math.sign(effectiveVerticalVelocity);
var tailForceX = 0.5 * RHO * effectiveVerticalVelocity * effectiveVerticalVelocity * TAIL_WING_AREA * TAIL_LIFT_COEF;
var tailTorqueX = pitchSlipSign * tailForceX * TAIL_LEN;
// Apply combined torques
plane.physics.applyTorque({
x: -tailTorqueX, // tail up = nose down
y: tailTorqueY,
z: 0
});
// Controls: Local torques scaled by airspeed^2, capped, dt, sensitivities
var torqueScale = Math.min(airspeed2 / (MAX_AIRSPEED_FOR_TORQUE * MAX_AIRSPEED_FOR_TORQUE), 1);
plane.physics.applyTorque({
x: pitchIn * CONTROL_SENSITIVITY * dt * torqueScale * pitchSensitivity,
y: yawIn * CONTROL_SENSITIVITY * dt * torqueScale * yawSensitivity,
z: rollIn * CONTROL_SENSITIVITY * dt * torqueScale * rollSensitivity
});
// Debug lines: Lift (green), Drag (red) in world space
var position = plane.transform.position;
var liftEnd = vectorAdd(position, vectorScale(liftForceWorld, DEBUG_SCALE));
var dragEnd = vectorAdd(position, vectorScale(dragForceWorld, DEBUG_SCALE));
figmin.debugDraw([position, liftEnd], "green");
figmin.debugDraw([position, dragEnd], "red");
// Ground Effect: Takeoff Assist
//TODO: once we can calculate the effective gravity force calculate what the lift force should be istead of hardcoding
var TAKEOFF_LIFT_FORCE = 0.03; // base upward force magnitude at CG (tune; to reduce friction load)
// Takeoff ground check: Raycast down for surface
var down = vectorScale(plane.transform.up, -1);;
figmin.raycast({
origin: plane.transform.position,
direction: down,
layers: [figmin.Layers.SURFACE],
maxDistance: planeHeight
}, function(hit) {
if (hit) {
// Grounded if the raycast is smaller than half height
grounded = hit.distance <= planeHeight * 0.51;
// Calculate takeoff scale: grows quadratically with airspeed from zero, caps at 1
var takeoffScale = Math.min((airspeed * airspeed) / (TAKEOFF_SPEED * TAKEOFF_SPEED), 1);
// Modulate by proximity: full force at distance=0, linear fade to 0 at GROUND_THRESHOLD
var proximityFactor = 1 - (hit.distance / planeHeight);
// Apply small upward lift force at CG to reduce friction
var groundUp = hit.normal;
var takeoffLift = vectorScale(groundUp, TAKEOFF_LIFT_FORCE * takeoffScale * proximityFactor);
plane.physics.applyForce(takeoffLift);
// Debug ray (yellow)
var rayEnd = vectorAdd(plane.transform.position, vectorScale(down, hit.distance));
figmin.debugDraw([plane.transform.position, rayEnd], "yellow");
// Debug takeoffLift (magenta, exaggerated, upward/opposite raycast)
var liftDebugEnd = vectorAdd(plane.transform.position, vectorScale(takeoffLift, DEBUG_SCALE*100));
figmin.debugDraw([plane.transform.position, liftDebugEnd], "magenta");
} else {
grounded = false;
}
});
}
// Start: Wait for ready
document.addEventListener("FigminReady", init);
// Vector helpers
function vectorAdd(a, b) {
return { x: a.x b.x, y: a.y b.y, z: a.z b.z };
}
function vectorScale(v, s) {
return { x: v.x * s, y: v.y * s, z: v.z * s };
}
function vectorNormalize(v) {
var mag = Math.sqrt(v.x * v.x v.y * v.y v.z * v.z);
if (mag === 0) return { x: 0, y: 0, z: 0 };
return { x: v.x / mag, y: v.y / mag, z: v.z / mag };
}
function vectorDot(a, b) {
return a.x * b.x a.y * b.y a.z * b.z;
}
// Quaternion helpers for world-to-local (for drag)
function quaternionMultiply(q1, q2) {
return {
w: q1.w * q2.w - q1.x * q2.x - q1.y * q2.y - q1.z * q2.z,
x: q1.w * q2.x q1.x * q2.w q1.y * q2.z - q1.z * q2.y,
y: q1.w * q2.y - q1.x * q2.z q1.y * q2.w q1.z * q2.x,
z: q1.w * q2.z q1.x * q2.y - q1.y * q2.x q1.z * q2.w
};
}
function quaternionConjugate(q) {
return { w: q.w, x: -q.x, y: -q.y, z: -q.z };
}
function quaternionRotateVector(q, v) {
var qv = { w: 0, x: v.x, y: v.y, z: v.z };
var qConj = quaternionConjugate(q);
var q1 = quaternionMultiply(q, qv);
var q2 = quaternionMultiply(q1, qConj);
return { x: q2.x, y: q2.y, z: q2.z };
}