import { ServerFolder } from '../../core/server-folder.js';
import { VRSPACEUI } from '../vrspace-ui.js';
import { TextArea } from '../widget/text-area.js';
/**
Portal is an entrance to other worlds, disabled by default.
*/
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.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;
}
/** 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;
}
}
/** 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 = new 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();
return this;
}
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)) {
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);
}
}
}