Compare commits

...

3 Commits

Author SHA1 Message Date
622e0a5259 Fix VR pointer interaction with GUI by removing restrictive picking predicate
All checks were successful
Build / build (push) Successful in 1m34s
Resolved issue where VR laser pointers could not click mission brief buttons. Root cause was scene.pointerMovePredicate filtering out GUI meshes before pointer events could reach AdvancedDynamicTexture.

Changes:
- Commented out restrictive pointerMovePredicate that blocked GUI mesh picking
- Temporarily disabled renderingGroupId=3 on mission brief for VR compatibility
- Adjusted ship physics: reduced angular force multiplier (1.5→0.5) and increased damping (0.5→0.6)

Technical details:
- WebXRControllerPointerSelection uses scene.pointerMovePredicate during pickWithRay()
- If predicate returns false, pickInfo.hit=false and GUI events never fire
- AdvancedDynamicTexture requires pickInfo.pickedMesh === mesh to process events
- Removing predicate allows default behavior (all isPickable meshes are candidates)

TODO: Re-implement predicate using renderingGroupId === 3 check for production

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-24 17:53:31 -06:00
fa15fce4ef Fixed some physics problems. 2025-11-24 17:03:41 -06:00
e31e25f9e5 Fix physics issues: sleep behavior, center of mass, and input scaling
This commit resolves several physics-related issues that were causing
unexpected behavior in ship and asteroid movement:

**Physics Sleep System**
- Fixed abrupt stops by preventing Havok from putting bodies to sleep
- Added PhysicsActivationControl.ALWAYS_ACTIVE for ship and asteroids
- Made ship sleep behavior configurable via shipPhysics.alwaysActive
- Sleep was causing sudden velocity zeroing at low speeds

**Center of Mass Issues**
- Discovered mesh-based physics calculated offset CoM: (0, -0.38, 0.37)
- Override ship center of mass to (0, 0, 0) to prevent thrust torque
- Applying force at offset CoM was creating unwanted pitch rotation
- Added debug logging to track mass properties

**Input Deadzone Improvements**
- Implemented smooth deadzone scaling (0.1-0.15 range)
- Replaced hard threshold cliff with linear interpolation
- Prevents abrupt control cutoff during gentle inputs
- Added VR mode check to disable keyboard fallback in VR

**Configuration System**
- Added DEFAULT_SHIP_PHYSICS constant as single source of truth
- Added tunable parameters: linearDamping, angularDamping, alwaysActive
- Added fuel consumption rates: linearFuelConsumptionRate, angularFuelConsumptionRate
- Tuned for 1 minute linear thrust, 2 minutes angular thrust at 60Hz
- All physics parameters now persist to localStorage

**Other Fixes**
- Changed orbit center to STATIC motion type (was ANIMATED)
- Fixed linear force application point (removed offset)
- Added ship initial velocity support from level config
- Changed physics update from every 10 frames to every physics tick
- Increased linear input threshold from 0.1 to 0.15

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-24 14:03:32 -06:00
11 changed files with 172 additions and 49 deletions

9
.claude/mcp.json Normal file
View File

@ -0,0 +1,9 @@
{
"mcpServers": {
"babylon-mcp": {
"command": "npx",
"args": ["mcp-proxy", "http://localhost:4000/mcp"]
}
}
}

View File

@ -1,5 +1,5 @@
{
"version": "1.0",
"version": "1.1",
"difficulty": "recruit",
"timestamp": "2025-11-11T23:44:24.807Z",
"metadata": {
@ -11,8 +11,8 @@
"ship": {
"position": [
0,
1.5,
500
2,
0
],
"rotation": [
0,

View File

@ -1,3 +1,19 @@
/**
* Default ship physics configuration
*/
const DEFAULT_SHIP_PHYSICS = {
maxLinearVelocity: 200,
maxAngularVelocity: 1.4,
linearForceMultiplier: 500,
angularForceMultiplier: .5,
linearFuelConsumptionRate: 0.0002778, // 1 minute at full thrust (60 Hz)
angularFuelConsumptionRate: 0.0001389, // 2 minutes at full thrust (60 Hz)
linearDamping: 0.2,
angularDamping: 0.6, // Moderate damping for 2-3 second coast
alwaysActive: true, // Prevent physics sleep (false may cause abrupt stops at zero velocity)
reverseThrustFactor: 0.3 // Reverse thrust at 50% of forward thrust power
};
/**
* Global game configuration settings
* Singleton class for managing game-wide settings
@ -13,12 +29,7 @@ export class GameConfig {
public progressionEnabled: boolean = true; // Enable level progression system
// Ship physics tuning parameters
public shipPhysics = {
maxLinearVelocity: 200,
maxAngularVelocity: 1.4,
linearForceMultiplier: 800,
angularForceMultiplier: 15
};
public shipPhysics = { ...DEFAULT_SHIP_PHYSICS };
/**
* Private constructor for singleton pattern
@ -66,10 +77,16 @@ export class GameConfig {
// Load ship physics with fallback to defaults
if (config.shipPhysics) {
this.shipPhysics = {
maxLinearVelocity: config.shipPhysics.maxLinearVelocity ?? 200,
maxAngularVelocity: config.shipPhysics.maxAngularVelocity ?? 1.4,
linearForceMultiplier: config.shipPhysics.linearForceMultiplier ?? 800,
angularForceMultiplier: config.shipPhysics.angularForceMultiplier ?? 15
maxLinearVelocity: config.shipPhysics.maxLinearVelocity ?? DEFAULT_SHIP_PHYSICS.maxLinearVelocity,
maxAngularVelocity: config.shipPhysics.maxAngularVelocity ?? DEFAULT_SHIP_PHYSICS.maxAngularVelocity,
linearForceMultiplier: config.shipPhysics.linearForceMultiplier ?? DEFAULT_SHIP_PHYSICS.linearForceMultiplier,
angularForceMultiplier: config.shipPhysics.angularForceMultiplier ?? DEFAULT_SHIP_PHYSICS.angularForceMultiplier,
linearFuelConsumptionRate: config.shipPhysics.linearFuelConsumptionRate ?? DEFAULT_SHIP_PHYSICS.linearFuelConsumptionRate,
angularFuelConsumptionRate: config.shipPhysics.angularFuelConsumptionRate ?? DEFAULT_SHIP_PHYSICS.angularFuelConsumptionRate,
linearDamping: config.shipPhysics.linearDamping ?? DEFAULT_SHIP_PHYSICS.linearDamping,
angularDamping: config.shipPhysics.angularDamping ?? DEFAULT_SHIP_PHYSICS.angularDamping,
alwaysActive: config.shipPhysics.alwaysActive ?? DEFAULT_SHIP_PHYSICS.alwaysActive,
reverseThrustFactor: config.shipPhysics.reverseThrustFactor ?? DEFAULT_SHIP_PHYSICS.reverseThrustFactor,
};
}
} else {
@ -87,12 +104,7 @@ export class GameConfig {
this.physicsEnabled = true;
this.debug = false;
this.progressionEnabled = true;
this.shipPhysics = {
maxLinearVelocity: 200,
maxAngularVelocity: 1.4,
linearForceMultiplier: 800,
angularForceMultiplier: 15
};
this.shipPhysics = { ...DEFAULT_SHIP_PHYSICS };
this.save();
}
}

View File

@ -2,9 +2,11 @@ import {
AbstractMesh,
AudioEngineV2,
DistanceConstraint,
HavokPlugin,
InstancedMesh,
Mesh,
Observable,
PhysicsActivationControl,
PhysicsAggregate,
PhysicsBody,
PhysicsMotionType,
@ -45,8 +47,8 @@ export class RockFactory {
// Initialize explosion manager
const node = new TransformNode('orbitCenter', DefaultScene.MainScene);
node.position = Vector3.Zero();
this._orbitCenter = new PhysicsAggregate(node, PhysicsShapeType.SPHERE, {radius: .1, mass: 1000}, DefaultScene.MainScene );
this._orbitCenter.body.setMotionType(PhysicsMotionType.ANIMATED);
this._orbitCenter = new PhysicsAggregate(node, PhysicsShapeType.SPHERE, {radius: .1, mass: 0}, DefaultScene.MainScene );
this._orbitCenter.body.setMotionType(PhysicsMotionType.STATIC);
this._explosionManager = new ExplosionManager(DefaultScene.MainScene, {
duration: 2000,
explosionForce: 150.0,
@ -115,6 +117,12 @@ export class RockFactory {
body.setMotionType(PhysicsMotionType.DYNAMIC);
body.setCollisionCallbackEnabled(true);
// Prevent asteroids from sleeping to ensure consistent physics simulation
const physicsPlugin = DefaultScene.MainScene.getPhysicsEngine()?.getPhysicsPlugin() as HavokPlugin;
if (physicsPlugin) {
physicsPlugin.setActivationControl(body, PhysicsActivationControl.ALWAYS_ACTIVE);
}
debugLog(`[RockFactory] Setting velocities for ${rock.name}:`);
debugLog(`[RockFactory] Linear velocity input: ${linearVelocitry.toString()}`);
debugLog(`[RockFactory] Angular velocity input: ${angularVelocity.toString()}`);

View File

@ -61,6 +61,12 @@ export class Level1 implements Level {
const currPose = xr.baseExperience.camera.globalPosition.y;
xr.baseExperience.camera.position = new Vector3(0, 1.5, 0);
// Disable keyboard input in VR mode to prevent interference
if (this._ship.keyboardInput) {
this._ship.keyboardInput.setEnabled(false);
debugLog('[Level1] Keyboard input disabled for VR mode');
}
// Track WebXR session start
try {
const analytics = getAnalytics();
@ -290,6 +296,22 @@ export class Level1 implements Level {
await this._ship.initialize();
setLoadingMessage("Loading level from configuration...");
// Apply ship configuration from level config
const shipConfig = this._deserializer.getShipConfig();
this._ship.position = new Vector3(...shipConfig.position);
if (shipConfig.linearVelocity) {
this._ship.setLinearVelocity(new Vector3(...shipConfig.linearVelocity));
} else {
this._ship.setLinearVelocity(Vector3.Zero());
}
if (shipConfig.angularVelocity) {
this._ship.setAngularVelocity(new Vector3(...shipConfig.angularVelocity));
} else {
this._ship.setAngularVelocity(Vector3.Zero());
}
// Use deserializer to create all entities from config
const entities = await this._deserializer.deserialize(this._ship.scoreboard.onScoreObservable);

View File

@ -538,6 +538,13 @@ export class Main {
const inputManager = InputControlManager.getInstance();
inputManager.registerPointerFeature(pointerFeature);
debugLog("Pointer selection feature registered with InputControlManager");
// Configure scene-wide picking predicate to only allow UI meshes
/*DefaultScene.MainScene.pointerMovePredicate = (mesh) => {
// Only allow picking meshes with metadata.uiPickable = true
return mesh.metadata?.uiPickable === true;
};*/
debugLog("Scene picking predicate configured for VR UI only");
}
// Hide Discord widget when entering VR, show when exiting

View File

@ -119,10 +119,12 @@ export class InputControlManager {
}
// Enable pointer selection
console.log(`[InputControlManager] About to update pointer feature...`);
this.updatePointerFeature();
// Emit state change event
this.emitStateChange(requester);
console.log(`[InputControlManager] ===== Ship controls disabled =====`);
}
/**

View File

@ -2,8 +2,10 @@ import {
AbstractMesh,
Color3,
FreeCamera,
HavokPlugin,
Mesh,
Observable,
PhysicsActivationControl,
PhysicsAggregate,
PhysicsMotionType,
PhysicsShapeType,
@ -118,6 +120,18 @@ export class Ship {
});
}
public setLinearVelocity(velocity: Vector3): void {
if (this._ship?.physicsBody) {
this._ship.physicsBody.setLinearVelocity(velocity);
}
}
public setAngularVelocity(velocity: Vector3): void {
if (this._ship?.physicsBody) {
this._ship.physicsBody.setAngularVelocity(velocity);
}
}
public async initialize() {
this._scoreboard = new Scoreboard();
this._scoreboard.setShip(this); // Pass ship reference for velocity reading
@ -126,7 +140,7 @@ export class Ship {
const data = await loadAsset("ship.glb");
this._ship = data.container.transformNodes[0];
// this._ship.id = "Ship"; // Set ID so mission brief can find it
this._ship.position.y = 5;
// Position is now set from level config in Level1.initialize()
// Create physics if enabled
const config = GameConfig.getInstance();
@ -144,11 +158,37 @@ export class Ship {
);
agg.body.setMotionType(PhysicsMotionType.DYNAMIC);
agg.body.setLinearDamping(0.2);
agg.body.setAngularDamping(0.4);
agg.body.setLinearDamping(config.shipPhysics.linearDamping);
agg.body.setAngularDamping(config.shipPhysics.angularDamping);
agg.body.setAngularVelocity(new Vector3(0, 0, 0));
agg.body.setCollisionCallbackEnabled(true);
// Debug: Log center of mass before override
const massProps = agg.body.getMassProperties();
console.log(`[Ship] Original center of mass (local): ${massProps.centerOfMass.toString()}`);
console.log(`[Ship] Mass: ${massProps.mass}`);
console.log(`[Ship] Inertia: ${massProps.inertia.toString()}`);
// Override center of mass to origin to prevent thrust from causing torque
// (mesh-based physics was calculating offset center of mass from geometry)
agg.body.setMassProperties({
mass: 10,
centerOfMass: new Vector3(0, 0, 0),
inertia: massProps.inertia,
inertiaOrientation: massProps.inertiaOrientation
});
console.log(`[Ship] Center of mass overridden to: ${agg.body.getMassProperties().centerOfMass.toString()}`);
// Configure physics sleep behavior from config
// (disabling sleep prevents abrupt stops at zero linear velocity)
if (config.shipPhysics.alwaysActive) {
const physicsPlugin = DefaultScene.MainScene.getPhysicsEngine()?.getPhysicsPlugin() as HavokPlugin;
if (physicsPlugin) {
physicsPlugin.setActivationControl(agg.body, PhysicsActivationControl.ALWAYS_ACTIVE);
}
}
// Register collision handler for energy-based hull damage
const observable = agg.body.getCollisionObservable();
observable.add((collisionEvent) => {
@ -276,18 +316,14 @@ export class Ship {
this._physics.setGameStats(this._gameStats);
// Setup physics update loop (every 10 frames)
DefaultScene.MainScene.onAfterPhysicsObservable.add(() => {
this.updatePhysics();
})
DefaultScene.MainScene.onAfterRenderObservable.add(() => {
this._frameCount++;
if (this._frameCount >= 10) {
this._frameCount = 0;
this.updatePhysics();
}
// Update voice audio system (checks for completed sounds and plays next in queue)
if (this._voiceAudio) {
this._voiceAudio.update();
}
// Check game end conditions every frame (but only acts once)
this.checkGameEndConditions();
});
@ -473,6 +509,9 @@ export class Ship {
return;
}
// Check if we're in VR mode
const inVRMode = DefaultScene.XR?.baseExperience?.state === 2; // WebXRState.IN_XR = 2
// Combine input from keyboard and controller
const keyboardState = this._keyboardInput?.getInputState() || {
leftStick: Vector2.Zero(),
@ -483,16 +522,24 @@ export class Ship {
rightStick: Vector2.Zero(),
};
// Merge inputs (controller takes priority if active)
// Merge inputs with smooth deadzone scaling (controller takes priority if active, keyboard disabled in VR)
// Deadzone: 0.1-0.15 range with linear scaling (avoids abrupt cliff effect)
const leftMagnitude = controllerState.leftStick.length();
const rightMagnitude = controllerState.rightStick.length();
// Scale factor: 0% at 0.1, 100% at 0.15, linear interpolation between
const leftScale = Math.max(0, Math.min(1, (leftMagnitude - 0.1) / 0.05));
const rightScale = Math.max(0, Math.min(1, (rightMagnitude - 0.1) / 0.05));
const combinedInput = {
leftStick:
controllerState.leftStick.length() > 0.1
? controllerState.leftStick
: keyboardState.leftStick,
leftMagnitude > 0.1
? controllerState.leftStick.scale(leftScale)
: (inVRMode ? Vector2.Zero() : keyboardState.leftStick),
rightStick:
controllerState.rightStick.length() > 0.1
? controllerState.rightStick
: keyboardState.rightStick,
rightMagnitude > 0.1
? controllerState.rightStick.scale(rightScale)
: (inVRMode ? Vector2.Zero() : keyboardState.rightStick),
};
// Apply forces and get magnitudes for audio

View File

@ -63,7 +63,7 @@ export class ShipPhysics {
let angularMagnitude = 0;
// Apply linear force from left stick Y (forward/backward)
if (Math.abs(leftStick.y) > 0.1) {
if (Math.abs(leftStick.y) > 0.15) {
linearMagnitude = Math.abs(leftStick.y);
// Check if we have fuel before applying force
@ -71,24 +71,33 @@ export class ShipPhysics {
// Only apply force if we haven't reached max velocity
if (currentSpeed < this._config.maxLinearVelocity) {
// Get local direction (Z-axis for forward/backward thrust)
const localDirection = new Vector3(0, 0, -leftStick.y);
const thrustDirection = -leftStick.y; // negative = forward, positive = reverse
const localDirection = new Vector3(0, 0, thrustDirection);
// Transform to world space
const worldDirection = Vector3.TransformNormal(
localDirection,
transformNode.getWorldMatrix()
);
const force = worldDirection.scale(this._config.linearForceMultiplier);
// Calculate thrust point: center of mass + offset (0, 1, 0) in world space
const thrustPoint = Vector3.TransformCoordinates(
physicsBody.getMassProperties().centerOfMass.add(new Vector3(0, 1, 0)),
transformNode.getWorldMatrix()
// Apply reverse thrust factor: forward at full power, reverse at reduced power
const thrustMultiplier = thrustDirection < 0
? 1.0 // Forward thrust at full power
: this._config.reverseThrustFactor; // Reverse thrust scaled down
const force = worldDirection.scale(
this._config.linearForceMultiplier * thrustMultiplier
);
// Apply force at ship's world position (center of mass)
// Since we overrode center of mass to (0,0,0) in local space, the transform origin is the CoM
// Using getAbsolutePosition() instead of transforming CoM avoids gyroscopic coupling during rotation
const thrustPoint = transformNode.getAbsolutePosition();
physicsBody.applyForce(force, thrustPoint);
// Consume fuel: normalized magnitude (0-1) * 0.005 per frame
const fuelConsumption = linearMagnitude * 0.005;
// Consume fuel based on config rate (tuned for 1 minute at full thrust)
const fuelConsumption = linearMagnitude * this._config.linearFuelConsumptionRate;
this._shipStatus.consumeFuel(fuelConsumption);
// Track fuel consumed for statistics
@ -126,11 +135,15 @@ export class ShipPhysics {
transformNode.getWorldMatrix()
);
// Note: Havok only exposes angular impulse, not torque
// Babylon.js implements applyForce() as: impulse = force * timeStep
// We do the same for angular: scale torque by physics timestep (1/60)
// Since we call this every 10 frames, we accumulate 10 timesteps worth
physicsBody.applyAngularImpulse(worldTorque);
// Consume fuel: normalized magnitude (0-3 max) / 3 * 0.005 per frame
// Consume fuel based on config rate (tuned for 2 minutes at full thrust)
const normalizedAngularMagnitude = Math.min(angularMagnitude / 3.0, 1.0);
const fuelConsumption = normalizedAngularMagnitude * 0.005;
const fuelConsumption = normalizedAngularMagnitude * this._config.angularFuelConsumptionRate;
this._shipStatus.consumeFuel(fuelConsumption);
// Track fuel consumed for statistics

View File

@ -46,6 +46,8 @@ export class MissionBrief {
mesh.parent = ship;
mesh.position = new Vector3(0,1,2.8);
//mesh.renderingGroupId = 3; // Same as status screen for consistent rendering
mesh.metadata = { uiPickable: true }; // TAG: VR UI - allow pointer selection
console.log('[MissionBrief] Mesh parented to ship at position:', mesh.position);
console.log('[MissionBrief] Mesh absolute position:', mesh.getAbsolutePosition());
console.log('[MissionBrief] Mesh scaling:', mesh.scaling);

View File

@ -94,6 +94,7 @@ export class StatusScreen {
this._screenMesh.position = new Vector3(0, 0, 2); // 2 meters forward in local space
//this._screenMesh.rotation.y = Math.PI; // Face backward (toward user)
this._screenMesh.renderingGroupId = 3; // Always render on top
this._screenMesh.metadata = { uiPickable: true }; // TAG: VR UI - allow pointer selection
// Create material
const material = new StandardMaterial("statusScreenMaterial", this._scene);