import { ServerFolder } from '../../core/server-folder.js';
import { VRSPACEUI } from '../vrspace-ui.js';
import { TextArea } from '../widget/text-area.js';
import { SpeechInput } from '../../core/speech-input.js';
/**
Portal is an entrance to other worlds, disabled by default.
Once enabled, user enters by clicking on it, or by voice command.
Voice activation is by command + name, or by command + prefix, since names may hard to pronounce/recognize.
Portal prefix+name, subtitle and description are displayed above the portal.
Portal thumbnail is used as emissive texture on enabled portal, automatically set from relatedUrl of provided serverFolder.
Enabled portals play default sound.
General usage is simple: construct, optionally set parameters, call loadAt().
*/
export class Portal {
/** Create a portal
@param scene babylonjs scene
@param {ServerFolder} serverFolder containing world class and content
@param callback to execute when portal is activated (clicked, tapped)
@param shadowGenerator optionally, portal can cast shadows
*/
constructor(scene, serverFolder, callback, shadowGenerator) {
this.scene = scene;
/** @type {ServerFolder} */
this.serverFolder = serverFolder;
this.callback = callback;
this.command = "portal";
/** If set, portal name is prefixed by this prefix */
this.prefix = null;
this.prefixSeparator = ". ";
this.name = serverFolder.name;
this.description = null;
this.subTitle = null;
this.alwaysShowTitle = false;
this.imageUrl = null;
if (serverFolder.relatedUrl()) {
this.imageUrl = serverFolder.relatedUrl();
this.thumbnail = new BABYLON.Texture(this.imageUrl);
}
this.shadowGenerator = shadowGenerator;
this.isEnabled = false;
this.angle = 0;
// used in dispose:
this.controls = [];
this.textures = [];
this.materials = [];
this.soundUrl = VRSPACEUI.contentBase + "/babylon/portal/couchhero_portal-idle.mp3";
this.soundDistance = 5;
this.soundVolume = .5;
this.speechInput = new SpeechInput();
}
/** handy, returns base url and folder name */
worldUrl() {
return this.serverFolder.baseUrl + this.serverFolder.name;
}
/** dispose of everything */
dispose() {
this.playSound(false);
if (this.sound) {
this.sound.dispose();
}
this.group.dispose();
if (this.thumbnail) {
this.thumbnail.dispose();
}
this.material.dispose();
for (var i = 0;i < this.controls.length;i++) {
// CHECKME doesn's seem required
this.controls[i].dispose();
}
for (var i = 0;i < this.textures.length;i++) {
this.textures[i].dispose();
}
for (var i = 0;i < this.materials.length;i++) {
this.materials[i].dispose();
}
if (this.pointerTracker) {
this.scene.onPointerObservable.remove(this.pointerTracker);
delete this.pointerTracker;
}
this.speechInput.dispose();
}
/** Load and display portal at given coordinates. Copies existing portal mesh to new coordinates and angle.
@param x
@param y
@param z
@param angle
*/
async loadAt(x, y, z, angle) {
this.angle = angle;
this.group = new BABYLON.TransformNode('Portal:' + this.name);
this.group.position = new BABYLON.Vector3(x, y, z);
this.group.rotationQuaternion = BABYLON.Quaternion.RotationAxis(BABYLON.Axis.Y, angle);
this.group.Portal = this;
if (this.shadowGenerator) {
var clone = VRSPACEUI.portal.clone();
clone.parent = this.group;
var meshes = clone.getChildMeshes();
for (var i = 0;i < meshes.length;i++) {
this.shadowGenerator.getShadowMap().renderList.push(meshes[i]);
}
} else {
VRSPACEUI.copyMesh(VRSPACEUI.portal, this.group);
}
var plane = BABYLON.Mesh.CreatePlane("PortalEntrance:" + this.name, 1.60, this.scene);
plane.isNearPickable = VRSPACEUI.allowHands;
plane.parent = this.group;
plane.position = new BABYLON.Vector3(0, 1.32, 0);
this.pointerTracker = (e) => {
if (e.type == BABYLON.PointerEventTypes.POINTERDOWN) {
var p = e.pickInfo;
if (p.pickedMesh == plane) {
if (this.isEnabled) {
console.log("Entering " + this.name);
this.enter();
} else {
console.log("Not entering " + this.name + " - disabled");
}
}
}
};
this.scene.onPointerObservable.add(this.pointerTracker);
this.material = new BABYLON.StandardMaterial(this.name + "-noise", this.scene);
plane.material = this.material;
this.material.disableLighting = true;
this.material.backFaceCulling = false;
var noiseTexture = new BABYLON.NoiseProceduralTexture(this.name + "-perlin", 256, this.scene);
this.material.lightmapTexture = noiseTexture;
noiseTexture.octaves = 4;
noiseTexture.persistence = 1.2;
noiseTexture.animationSpeedFactor = 2;
plane.visibility = 0.85;
this.textures.push(noiseTexture);
this.showTitle();
this.attachSound();
this.attachSpeech();
return this;
}
attachSpeech() {
let command = this.command + " " + this.name;
if (this.prefix) {
command = this.command + " " + this.prefix;
}
//console.log("Portal "+this.name+" voice command " + command);
this.speechInput.addCommand(command,
() => {
if (this.isEnabled) this.enter();
}
);
this.speechInput.start();
}
attachSound() {
if (this.soundUrl) {
this.sound = new BABYLON.Sound(
"portalSound:" + this.name,
this.soundUrl,
this.scene, null, {
loop: true,
autoplay: false,
spatialSound: true,
streaming: false,
distanceModel: "linear",
maxDistance: this.soundDistance, // default 100, used only when linear
panningModel: "equalpower" // or "HRTF"
});
this.sound.attachToMesh(this.group);
this.sound.setVolume(this.soundVolume);
}
}
playSound(enable) {
if (this.sound) {
if (enable) {
/*
TODO audio V2
VRSPACEUI.audioEngine.audioContext?.resume();
VRSPACEUI.audioEngine.setGlobalVolume(1);
*/
this.sound.play();
// chrome hacks
BABYLON.Engine.audioEngine.audioContext?.resume();
BABYLON.Engine.audioEngine.setGlobalVolume(1);
} else if (this.sound) {
this.sound.stop();
}
}
}
showTitle() {
if (!this.title && (this.isEnabled || this.alwaysShowTitle)) {
if (this.prefix) {
this.title = new TextArea(this.scene, this.name, this.prefix+this.prefixSeparator+this.name);
} else {
this.title = new TextArea(this.scene, this.name, this.name);
}
this.title.autoScale = true;
this.title.addHandles = false;
this.title.addBackground = false;
this.title.textHorizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_CENTER;
this.title.textVerticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_TOP;
this.title.size = 0.1;
//this.title.group.parent = this.group;
this.title.position = new BABYLON.Vector3(0, 2.5, 0);
//this.title.isVisible = this.alwaysShowTitle; // TODO
this.title.text = "";
if (this.subTitle) {
this.title.text += this.subTitle + "\n";
}
if (this.description) {
this.title.text += this.description;
}
this.title.show();
this.title.group.parent = this.group;
}
}
setTitle(title) {
this.subTitle = title;
this.clearTitle();
this.showTitle();
}
getTitle() {
return this.subTitle;
}
clearTitle() {
if (this.title) {
this.title.dispose();
this.title == null;
}
}
/** Enables or disables the portal
@param enable
*/
enabled(enable) {
this.isEnabled = enable;
if (enable) {
this.material.emissiveTexture = this.thumbnail;
this.showTitle();
} else {
this.material.emissiveTexture = null;
this.clearTitle();
}
this.playSound(enable);
}
/** Executes callback on entry */
enter() {
if (this.callback) {
this.callback(this);
}
}
}