import * as THREE from 'three';
import { SubmarineType } from '../model/SubmarineType';
import { ResourceNames } from '../model/Utils/Resources/ResourcesNames';
import { Events } from '../controller/Utils/Events';
import SubmarineDebug from './SubmarineDebug';
import * as SeaFloor from './environment/scene/SeaFloor.js';
import { enableSubmarineSound } from './environment/scripts/Debug.js';
const ROLLOFF_FACTOR_WATER = 0.23; // For underwater sound
const ROLLOFF_FACTOR_AIR = 1; // For above water sound
const WATER_LEVEL = 0; // Y position of the water surface
import { setCollistionDetection } from './environment/scripts/Debug.js';
/**
* Class representing the view of a submarine in the scene.
* Manages the loading, initialization, and updating of submarine models.
*
* @class
*/
class SubmarineView {
/**
* Constructor for the SubmarineView class.
* Initializes submarines scene models.
* Sets up the initial submarine and debug tools.
*
* @param {Simulator} simulator - The simulator instance.
*/
constructor(simulator) {
this.simulator = simulator;
this.resources = this.simulator.resources;
this.scene = this.simulator.scene;
this.submarines = this.simulator.submarines;
this.items = {};
this.submarineData = this.simulator.submarines.getCurrentSubmarine();
this.initSubmarines();
const submarineType = this.submarineData.getType();
this.submarineScene = this.items[submarineType];
SeaFloor.addSubmarineDebugMesh(this.submarineScene)
this.scene.add(this.submarineScene);
new SubmarineDebug(this.simulator);
this.setupEngineSound();
this.simulator.submarines.on(Events.SwitchSubmarine, () => {
this.setSubmarineDataAndScene();
});
this.submarineData.on(Events.SubmarineUpdate, () => {
this.setFanSound(this.submarineData.getState().getCurrentRotorRPS());
this.setFanRotation(THREE.MathUtils.degToRad(this.submarineData.getState().getCurrentRotorRPS() * ((Math.PI * 2))));
this.setSternRotation(THREE.MathUtils.degToRad(this.submarineData.getState().getSternAngle()));
this.setRudderRotation(THREE.MathUtils.degToRad(this.submarineData.getState().rudderAngle));
this.setFiarwaterRotation(THREE.MathUtils.degToRad(this.submarineData.getState().getFairwaterAngle()));
this.updateSubmarine();
this.simulator.camera.updateTarget(this.submarineScene.position);
});
}
setupEngineSound() {
// Step 1: Create an Audio Listener and add it to the camera
const listener = new THREE.AudioListener();
this.simulator.camera.instance.add(listener); // Assuming `camera.instance` is your camera object
// Step 2: Create a PositionalAudio object and attach it to the submarine
const engineSound = new THREE.PositionalAudio(listener);
const buffer = this.resources.getResource(ResourceNames.EngineSound);
engineSound.setBuffer(buffer);
engineSound.setLoop(true); // Make sure the sound loops
engineSound.setRefDistance(20); // Set reference distance for volume attenuation
engineSound.setMaxDistance(100); // Max distance where sound is audible
engineSound.setDistanceModel('exponential');
const enginePosition = new THREE.Vector3(0, 0, -14); // Adjust based on your model
engineSound.position.set(enginePosition.x, enginePosition.y, enginePosition.z);
// engineSound.setPlaybackRate(1.0); // Normal speed
this.engineSound = engineSound;
this.submarineScene.add(this.engineSound);
function showAudioPermissionDialog() {
const dialog = document.getElementById('audio-permission-dialog');
dialog.style.display = 'flex';
// Wait for user to click the allow button
document.getElementById('allow-audio-button').addEventListener('click', () => {
resumeAudioContext();
dialog.style.display = 'none';
});
}
let world = this.simulator.world;
// Function to resume the AudioContext after user gesture
function resumeAudioContext() {
if (listener.context.state === 'suspended') {
listener.context.resume();
world.resumeAudioContext();
}
}
showAudioPermissionDialog();
}
// Fade-Out Effect
fadeOutAudio(audio, duration) {
const fadeTime = duration || 2000; // Default fade-out time in milliseconds
const interval = 50; // Interval in milliseconds
const fadeStep = interval / fadeTime;
let volume = audio.getVolume();
const fadeOutInterval = setInterval(() => {
volume -= fadeStep;
if (volume <= 0) {
volume = 0;
clearInterval(fadeOutInterval);
audio.pause(); // Ensure audio is paused when volume is zero
}
audio.setVolume(volume);
}, interval);
}
// Fade-In Effect
fadeInAudio(audio, duration) {
const fadeTime = duration || 2000; // Default fade-in time in milliseconds
const interval = 50; // Interval in milliseconds
const fadeStep = interval / fadeTime;
let volume = 0;
audio.setVolume(volume);
audio.play(); // Ensure audio is playing
const fadeInInterval = setInterval(() => {
volume += fadeStep;
if (volume >= 1) {
volume = 1;
clearInterval(fadeInInterval);
}
audio.setVolume(volume);
}, interval);
}
setFanSound(rps) {
// Define the base playback rate
const baseRate = 1.0; // Normal speed
// Define the maximum playback rate
const maxRate = 2.0; // Max speed, can be adjusted
// Calculate the playback rate based on RPS
const playbackRate = baseRate + (rps / 10) * (maxRate - baseRate);
// Apply the playback rate to the sound
this.engineSound.setPlaybackRate(playbackRate);
if (rps === 0) {
// Fade out the engine sound
this.fadeOutAudio(this.engineSound, 2000); // Fade out over 2 seconds
} else {
if ((!this.engineSound.isPlaying) && enableSubmarineSound) {
// Fade in the engine sound
this.fadeInAudio(this.engineSound, 1000); // Fade in over 2 seconds
}
}
}
update() {
if (this.simulator.camera.instance.position.y < WATER_LEVEL) {
// Camera is underwater
if (this.engineSound.getRolloffFactor() !== ROLLOFF_FACTOR_WATER) {
this.engineSound.setRolloffFactor(ROLLOFF_FACTOR_WATER);
}
} else {
// Camera is above water
if (this.engineSound.getRolloffFactor() !== ROLLOFF_FACTOR_AIR) {
this.engineSound.setRolloffFactor(ROLLOFF_FACTOR_AIR);
}
}
}
/**
* Sets the current submarine data and updates the scene with the corresponding submarine model.
*/
setSubmarineDataAndScene() {
this.submarineData = this.simulator.submarines.getCurrentSubmarine();
this.scene.remove(this.submarineScene);
const submarineType = this.submarineData.getType();
this.submarineScene = this.items[submarineType];
this.scene.add(this.submarineScene);
}
setFanRotation(angle) {
this.items[SubmarineType.Typhoon].children[0].children[3].rotation.y -= angle;
this.items[SubmarineType.Typhoon].children[0].children[4].rotation.y += angle;
}
setRudderRotation(angle) {
this.items[SubmarineType.Typhoon].children[0].children[5].rotation.z = - angle;
}
setFiarwaterRotation(angle) {
this.items[SubmarineType.Typhoon].children[0].children[1].rotation.x = angle;
}
setSternRotation(angle) {
this.items[SubmarineType.Typhoon].children[0].children[2].rotation.x = - angle;
}
/**
* Initializes the submarine meshes for different submarine types and stores them in the items map.
*/
initSubmarines() {
const ohioSubmarine = this.initOhioSubmarineMesh();
this.items[SubmarineType.Ohio] = ohioSubmarine;
const typhoonSubmarine = this.initTyhpponSubmarineMesh();
this.items[SubmarineType.Typhoon] = typhoonSubmarine;
}
/**
* Initializes the Ohio submarine mesh by loading it from resources, applying necessary transformations,
* and baking its rotation into the geometry.
*
* @returns {THREE.Scene} - The transformed Ohio submarine scene.
*/
initOhioSubmarineMesh() {
const ohioSubmarineScene = this.getSceneFromResource(ResourceNames.OhioSubmarineModel, SubmarineType.Ohio);
ohioSubmarineScene.rotation.y = Math.PI;
this.bakeRotationIntoGeometry(ohioSubmarineScene);
ohioSubmarineScene.rotation.y = 0;
return ohioSubmarineScene;
}
/**
* Initializes the Typhoon submarine mesh by loading it from resources.
*
* @returns {THREE.Scene} - The Typhoon submarine scene.
*/
initTyhpponSubmarineMesh() {
const typhoonSubmarineScene = this.getSceneFromResource(ResourceNames.TyphoonSubmarineModel, SubmarineType.Typhoon);
return typhoonSubmarineScene;
}
/**
* Retrieves the submarine scene from the given resource and scales it according to the desired length.
*
* @param {ResourceNames} resourceName - The name of the resource to load.
* @param {SubmarineType} submarineType - The type of the submarine.
* @returns {THREE.Scene} - The scaled submarine scene.
*/
getSceneFromResource(resourceName, submarineType) {
const submarineGTFL = this.resources.getResource(resourceName);
const submarineScene = submarineGTFL.scene;
const boundingBox = new THREE.Box3().setFromObject(submarineScene);
const initialLength = boundingBox.max.z - boundingBox.min.z;
const desiredLength = this.submarines.getSubmarine(submarineType).getConstants().getLength();
const scaleFactor = desiredLength / initialLength;
submarineScene.scale.set(scaleFactor, scaleFactor, scaleFactor);
return submarineScene;
}
/**
* Updates the submarine's position and orientation in the scene based on its current state.
*/
updateSubmarine() {
this.updateSubmarineWithCollisionDetection();
// SeaFloor.submarineBoundingBoxScene.position=this.submarineData.getState().getCurrentPosition()
// this.submarineScene.position.copy(this.submarineData.getState().getCurrentPosition());
this.submarineScene.quaternion.copy(this.submarineData.getState().getCurrentOrientation());
}
updateSubmarineWithCollisionDetection() {
// Assuming submarineView.submarineData.getState().getCurrentPosition() returns the target position
const submarineScene = this.submarineScene;
const submarineData = this.submarineData;
// Get the submarine's target position
const targetPosition = submarineData.getState().getCurrentPosition();
// Create a bounding box for the submarine at its current position
let submarineBox = new THREE.Box3().setFromObject(submarineScene);
// Create a temporary bounding box for the submarine at the target position
let tempBox = submarineBox.clone();
tempBox.translate(targetPosition.clone().sub(submarineScene.position));
// Check for potential collision using bounding boxes
const potentialBox = SeaFloor.boundingBoxes.find(box => box.intersectsBox(tempBox));
if (potentialBox) {
setCollistionDetection(true);
// Correct the submarine's position to prevent it from intersecting the sea floor
// Calculate the minimum distance to move the submarine out of the sea floor
let correctionX = 0, correctionY = 0, correctionZ = 0;
// Get the difference between tempBox and potentialBox
const overlap = tempBox.intersect(potentialBox);
// Correct Y (vertical) position
if (overlap.max.y > potentialBox.max.y) {
correctionY = potentialBox.max.y - tempBox.min.y;
} else if (overlap.min.y < potentialBox.min.y) {
correctionY = potentialBox.min.y - tempBox.max.y;
}
// Correct X position
if (overlap.max.x > potentialBox.max.x) {
correctionX = potentialBox.max.x - tempBox.min.x;
} else if (overlap.min.x < potentialBox.min.x) {
correctionX = potentialBox.min.x - tempBox.max.x;
}
// Correct Z position
if (overlap.max.z > potentialBox.max.z) {
correctionZ = potentialBox.max.z - tempBox.min.z;
} else if (overlap.min.z < potentialBox.min.z) {
correctionZ = potentialBox.min.z - tempBox.max.z;
}
// Apply corrections
submarineScene.position.x += correctionX;
submarineScene.position.y += correctionY;
submarineScene.position.z += correctionZ;
// Ensure the submarine's updated position is reflected in its state
submarineData.getState().setCurrentPosition(submarineScene.position);
SeaFloor.submarineBoundingBoxScene.position.set(submarineScene.position.x, submarineScene.position.y, submarineScene.position.z)
} else {
setCollistionDetection(false);
// No collision detected; update normally
submarineScene.position.copy(targetPosition);
// Update the submarine's position in its state
submarineData.getState().setCurrentPosition(submarineScene.position);
SeaFloor.submarineBoundingBoxScene.position.set(submarineScene.position.x, submarineScene.position.y, submarineScene.position.z)
}
}
/**
* Function to bake rotation into geometry.
* Ensures the object's world matrix is up-to-date and applies the world matrix to the geometry.
*
* @param {THREE.Object3D} object - The object whose rotation to bake into its geometry.
*/
bakeRotationIntoGeometry(object) {
object.updateMatrixWorld(true); // Ensure the world matrix is up-to-date
object.traverse((child) => {
if (child instanceof THREE.Mesh) {
const geometry = child.geometry;
geometry.applyMatrix4(object.matrixWorld); // Apply world matrix to geometry
geometry.computeVertexNormals(); // Recompute vertex normals
child.position.set(0, 0, 0); // Reset position
child.rotation.set(0, 0, 0); // Reset rotation
child.scale.set(1, 1, 1); // Reset scale
child.updateMatrixWorld(true); // Update matrix
}
});
}
}
export default SubmarineView;