From c3e407f4e01f66e1b70ac59a4681cc6ba53e88bd Mon Sep 17 00:00:00 2001 From: Giga Date: Wed, 25 Oct 2023 00:18:46 +1300 Subject: [PATCH] Rename NametagEntity to LabelEntity for clarity --- src/layouts/MainLayout.vue | 12 +- .../avatar/controller/inputController.ts | 4 +- .../components/components/ModelComponent.ts | 30 +- .../{NametagEntity.ts => LabelEntity.ts} | 964 +++++++++--------- src/modules/entity/entities/index.ts | 2 +- src/modules/scene/vscene.ts | 82 +- src/stores/user-store.ts | 2 +- 7 files changed, 548 insertions(+), 548 deletions(-) rename src/modules/entity/entities/{NametagEntity.ts => LabelEntity.ts} (80%) diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue index 87f578c9..fe4d7f4d 100644 --- a/src/layouts/MainLayout.vue +++ b/src/layouts/MainLayout.vue @@ -514,14 +514,14 @@ export default defineComponent({ }, { icon: "badge", - label: "Nametags", + label: "Labels", action: () => { - userStore.avatar.showNametags = !userStore.avatar.showNametags; - Log.info(Log.types.OTHER, "Toggle Avatar Nametags"); + userStore.avatar.showLabels = !userStore.avatar.showLabels; + Log.info(Log.types.OTHER, "Toggle Labels"); }, isCategory: false, separator: true, - caption: "nametag_setting" + caption: "boolean" }, { icon: "lightbulb", @@ -722,8 +722,8 @@ export default defineComponent({ }, formatMenuItemCaption(caption: string) { switch (caption) { - case "nametag_setting": - return this.userStore.avatar.showNametags ? "On" : "Off"; + case "boolean": + return this.userStore.avatar.showLabels ? "On" : "Off"; default: return caption; } diff --git a/src/modules/avatar/controller/inputController.ts b/src/modules/avatar/controller/inputController.ts index 3688e8b0..e4d3243b 100644 --- a/src/modules/avatar/controller/inputController.ts +++ b/src/modules/avatar/controller/inputController.ts @@ -904,8 +904,8 @@ export class InputController extends ScriptComponent { // Set the visibility of the avatar. this._gameObject.isVisible = visible; - // Set the visibility of the avatar's nametag. - const meshes = this._gameObject.getChildMeshes(true, (mesh) => mesh.name === "Nametag"); + // Set the visibility of the avatar's label. + const meshes = this._gameObject.getChildMeshes(true, (mesh) => mesh.name === "Label"); if (meshes.length > 0) { meshes[0].isVisible = visible; } diff --git a/src/modules/entity/components/components/ModelComponent.ts b/src/modules/entity/components/components/ModelComponent.ts index ac05ee3b..78cdf7ab 100644 --- a/src/modules/entity/components/components/ModelComponent.ts +++ b/src/modules/entity/components/components/ModelComponent.ts @@ -18,7 +18,7 @@ import { Node, } from "@babylonjs/core"; import { IModelEntity } from "../../EntityInterfaces"; -import { NametagEntity } from "@Modules/entity/entities"; +import { LabelEntity } from "@Modules/entity/entities"; import { updateContentLoadingProgress } from "@Modules/scene/LoadingScreen"; import { applicationStore } from "@Stores/index"; import Log from "@Modules/debugging/log"; @@ -74,10 +74,10 @@ export class ModelComponent extends MeshComponent { this.mesh = meshes[0]; this.renderGroupId = DEFAULT_MESH_RENDER_GROUP_ID; - // Add a nametag to any of the model's children if they match any of the InteractiveModelTypes. - const defaultNametagHeight = 0.6; - const nametagOffset = 0.25; - const nametagPopDistance = + // Add a label to any of the model's children if they match any of the InteractiveModelTypes. + const defaultLabelHeight = 0.6; + const labelOffset = 0.25; + const labelPopDistance = applicationStore.interactions.interactionDistance; const childNodes = this.mesh.getChildren( (node) => "getBoundingInfo" in node, @@ -96,13 +96,13 @@ export class ModelComponent extends MeshComponent { const boundingInfo = childNode.getBoundingInfo(); const height = boundingInfo.maximum.y - boundingInfo.minimum.y; - NametagEntity.create( + LabelEntity.create( childNode, - height + nametagOffset, + height + labelOffset, genericModelType.name, true, undefined, - nametagPopDistance, + labelPopDistance, () => !applicationStore.interactions.isInteracting ); }); @@ -113,29 +113,29 @@ export class ModelComponent extends MeshComponent { if (!genericModelType) { return; } - NametagEntity.create( + LabelEntity.create( childNode, - defaultNametagHeight, + defaultLabelHeight, genericModelType.name, true, undefined, - nametagPopDistance, + labelPopDistance, () => !applicationStore.interactions.isInteracting ); }); - // Add a nametag to the model itself if it matches any of the InteractiveModelTypes. + // Add a label to the model itself if it matches any of the InteractiveModelTypes. const genericModelType = InteractiveModelTypes.find((type) => type.condition.test(this.mesh?.name ?? "") ); if (genericModelType) { - NametagEntity.create( + LabelEntity.create( this.mesh, - defaultNametagHeight, + defaultLabelHeight, genericModelType.name, true, undefined, - nametagPopDistance, + labelPopDistance, () => !applicationStore.interactions.isInteracting ); } diff --git a/src/modules/entity/entities/NametagEntity.ts b/src/modules/entity/entities/LabelEntity.ts similarity index 80% rename from src/modules/entity/entities/NametagEntity.ts rename to src/modules/entity/entities/LabelEntity.ts index 35619686..3cddc3df 100644 --- a/src/modules/entity/entities/NametagEntity.ts +++ b/src/modules/entity/entities/LabelEntity.ts @@ -1,482 +1,482 @@ -// -// NametagEntity.ts -// -// Created by Giga on 16 Feb 2023. -// Copyright 2023 Vircadia contributors. -// Copyright 2023 DigiSomni LLC. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -/* eslint-disable @typescript-eslint/no-magic-numbers */ - -import { - type AbstractMesh, - Color3, - DynamicTexture, - Matrix, - Mesh, - MeshBuilder, - Scene, - StandardMaterial, - TransformNode, - Vector3, -} from "@babylonjs/core"; -import { DEFAULT_MESH_RENDER_GROUP_ID } from "@Modules/object"; -import { Renderer } from "@Modules/scene"; -import { userStore } from "@Stores/index"; -import { Hysteresis } from "@Modules/utility/hysteresis"; - -/** - * Contains all of the memoized nametag meshes within the scene. - */ -let nametagMemoNode: TransformNode | undefined = undefined; - -const meshMemo = new Map(); -const foregroundTextureMemo = new Map(); -const backgroundTextureMemo = new Map(); -const foregroundMaterialMemo = new Map(); -const backgroundMaterialMemo = new Map(); - -/** - * Create a sector mesh. - * @param name The name of the mesh. - * @param vector1 The vector at the leading edge of the sector. - * @param vector2 The vector at the trailing edge of the sector. - * @param radius The radius of the sector. - * @param scene The hosting scene. - * @returns The new sector mesh. - */ -function createSector( - name: string, - vector1: Vector3, - vector2: Vector3, - radius = 1, - scene?: Scene -): Mesh { - // Get the angle between the two vectors. - const sectorAngle = Math.acos( - Vector3.Dot(vector1, vector2) / (vector1.length() * vector2.length()) - ); - const minNumberOfSegments = 5; - const diameter = radius * 2; - const origin = Vector3.Zero(); - const firstPoint = Vector3.Normalize(vector1).scale(radius); - const lastPoint = Vector3.Normalize(vector2).scale(radius); - - // Divide the sector angle into a number of segments angles. - const segments = Math.max( - Math.floor(diameter * sectorAngle), - minNumberOfSegments - ); - const segmentAngle = sectorAngle / segments; - - // Create points to connect each segment. - const points = new Array(); - for (let i = 0; i < segments; i++) { - const matrix = Matrix.RotationAxis( - Vector3.Cross(vector1, vector2), - segmentAngle * i - ); - const rotated = Vector3.TransformCoordinates(firstPoint, matrix); - points.push(rotated.add(origin)); - } - points.push(lastPoint.add(origin)); - - // Connect each segment point back to the origin. - const originPoints = new Array(); - points.forEach(() => { - originPoints.push(origin); - }); - - // Create a ribbon mesh from the points. - const sector = MeshBuilder.CreateRibbon( - name, - { - pathArray: [points, originPoints], - offset: 0, - sideOrientation: Mesh.DOUBLESIDE - }, - scene - ); - - return sector; -} - -export class NametagEntity { - private static _textFont = { - name: "monospace", - size: 70, - characterWidth: 38.5, - characterRatio: 1.43, - contentRatio: 0.1, - }; - - private static _iconFont = { - name: "Material Icons", - size: 100, - characterWidth: 100, - characterRatio: 1, - contentRatio: 0.16, - }; - - /** - * Create a new nametag entity and attach it to an object. - * @param object The mesh/transform node to attach the nametag to. - * @param height The height of the object (the nametag will be positioned above this point). - * @param name The name to be displayed on the nametag. - * @param icon Display the name as an icon instead of text. - * @param color The color of the nametag's background. - * @param popDistance The distance from the player's avatar at which the nametag will stop being visible. - * @param popOverride A function overriding the visibility of the nametag. - * This function receives the distance from the player's avatar to the nametag, - * and should return a boolean indicating whether the nametag should be visible (`true`) or not (`false`). - * @returns A reference to the new nametag mesh. - */ - public static create( - object: Mesh | AbstractMesh | TransformNode, - height: number | (() => number), - name: string, - icon = false, - color?: Color3, - popDistance = 20, - popOverride?: (distance: number) => boolean - ): Mesh | undefined { - const scene = object.getScene(); - const font = icon ? this._iconFont : this._textFont; - const tagTextureWidth = icon - ? font.characterWidth * 1.2 - : (name.length + 1) * font.characterWidth; - const tagTextureHeight = font.size * font.characterRatio; - const tagWidth = - (font.contentRatio * tagTextureWidth) / tagTextureHeight; - const tagHeight = font.contentRatio; - const tagCornerRadius = tagHeight / 6; - const nametagArrowSize = 0.02; - const tagBackgroundColor = color ?? new Color3(0.07, 0.07, 0.07); - const tagBackgroundColorString = tagBackgroundColor.toHexString(); - const memoName = `${name}${icon ? "-i" : "" - }-${tagBackgroundColorString}`; - - // Attempt to reuse a memoized mesh, if one exists. - let mesh = meshMemo - .get(memoName) - ?.clone("Nametag", object, false, false); - - // If a matching mesh doesn't already exist, create a new one. - if (!mesh) { - // Textures. - let foregroundTexture = foregroundTextureMemo.get(memoName); - // If a matching texture doesn't already exist, create a new one. - if (!foregroundTexture) { - // Create the texture. - const newForegroundTexture = new DynamicTexture( - `NametagTexture-${memoName}`, - { width: tagTextureWidth, height: tagTextureHeight }, - scene - ); - // Center the name on the tag. - const textPosition = icon - ? tagTextureWidth / 2 - font.characterWidth / 2 - : tagTextureWidth / 2 - - (name.length / 2) * font.characterWidth; - newForegroundTexture.drawText( - name, - textPosition, - font.size, - `${font.size}px ${font.name}`, - "white", - tagBackgroundColorString, - true, - true - ); - newForegroundTexture.getAlphaFromRGB = true; - // Memoize the texture. - foregroundTextureMemo.set(memoName, newForegroundTexture); - foregroundTexture = newForegroundTexture; - } - - let backgroundTexture = backgroundTextureMemo.get( - tagBackgroundColorString - ); - // If a matching texture doesn't already exist, create a new one. - if (!backgroundTexture) { - // Create the texture. - const newBackgroundTexture = new DynamicTexture( - `NametagBackgroundTexture-${tagBackgroundColorString}`, - { width: tagTextureWidth, height: tagTextureHeight }, - scene - ); - newBackgroundTexture.drawText( - "", - 0, - 0, - `${font.size}px ${font.name}`, - "white", - tagBackgroundColorString, - true, - true - ); - newBackgroundTexture.getAlphaFromRGB = true; - // Memoize the texture. - backgroundTextureMemo.set( - tagBackgroundColorString, - newBackgroundTexture - ); - backgroundTexture = newBackgroundTexture; - } - - // Materials. - let foregroundMaterial = foregroundMaterialMemo.get(memoName); - // If a matching material doesn't already exist, create a new one. - if (!foregroundMaterial) { - // Create the material. - const newForegroundMaterial = new StandardMaterial( - `NametagMaterial-${memoName}`, - scene - ); - newForegroundMaterial.diffuseTexture = foregroundTexture; - newForegroundMaterial.specularTexture = foregroundTexture; - newForegroundMaterial.emissiveTexture = foregroundTexture; - newForegroundMaterial.disableLighting = true; - // Memoize the material. - foregroundMaterialMemo.set(memoName, newForegroundMaterial); - foregroundMaterial = newForegroundMaterial; - } - - let backgroundMaterial = backgroundMaterialMemo.get( - tagBackgroundColorString - ); - // If a matching material doesn't already exist, create a new one. - if (!backgroundMaterial) { - // Create the material. - const newBackgroundMaterial = new StandardMaterial( - `NametagBackgroundMaterial-${tagBackgroundColorString}`, - scene - ); - newBackgroundMaterial.diffuseTexture = backgroundTexture; - newBackgroundMaterial.specularTexture = backgroundTexture; - newBackgroundMaterial.emissiveTexture = backgroundTexture; - newBackgroundMaterial.disableLighting = true; - // Memoize the material. - backgroundMaterialMemo.set( - tagBackgroundColorString, - newBackgroundMaterial - ); - backgroundMaterial = newBackgroundMaterial; - } - - // Meshes. - const plane = MeshBuilder.CreatePlane( - "Nametag", - { - width: tagWidth, - height: tagHeight, - sideOrientation: Mesh.DOUBLESIDE, - updatable: true, - }, - scene - ); - plane.material = foregroundMaterial; - - // Rounded corners. - const corners = new Array(); - const cornerPositions = [ - new Vector3(-tagWidth / 2, tagHeight / 2 - tagCornerRadius, 0), - new Vector3(tagWidth / 2, tagHeight / 2 - tagCornerRadius, 0), - new Vector3(tagWidth / 2, -tagHeight / 2 + tagCornerRadius, 0), - new Vector3(-tagWidth / 2, -tagHeight / 2 + tagCornerRadius, 0), - ]; - const sector = createSector( - "NametagCorner", - Vector3.Up(), - Vector3.Left(), - tagCornerRadius, - scene - ); - corners.push(sector); - corners.push(sector.clone("NametagCorner")); - corners.push(sector.clone("NametagCorner")); - corners.push(sector.clone("NametagCorner")); - let index = 0; - for (const cornerMesh of corners) { - cornerMesh.material = backgroundMaterial; - cornerMesh.position = cornerPositions[index]; - cornerMesh.rotate(new Vector3(0, 0, 1), -index * (Math.PI / 2)); - index += 1; - } - - // Left and right edges. - const edges = new Array(); - const edgeOptions = { - width: tagCornerRadius, - height: tagHeight - tagCornerRadius * 2, - sideOrientation: Mesh.DOUBLESIDE, - updatable: true, - }; - const edgePositions = [ - new Vector3(-tagWidth / 2 - tagCornerRadius / 2, 0, 0), - new Vector3(tagWidth / 2 + tagCornerRadius / 2, 0, 0), - ]; - edges.push( - MeshBuilder.CreatePlane("NametagLeftEdge", edgeOptions, scene) - ); - edges.push( - MeshBuilder.CreatePlane("NametagRightEdge", edgeOptions, scene) - ); - index = 0; - for (const edgeMesh of edges) { - edgeMesh.material = backgroundMaterial; - edgeMesh.position = edgePositions[index]; - index += 1; - } - - // Arrow mesh. - const arrow = MeshBuilder.CreateDisc( - "NametagArrow", - { - radius: nametagArrowSize, - tessellation: 3, - sideOrientation: Mesh.DOUBLESIDE, - updatable: true, - }, - scene - ); - arrow.material = backgroundMaterial; - arrow.position = new Vector3( - 0, - -(tagHeight / 2 + nametagArrowSize / 4), - 0 - ); - arrow.rotation.z = -Math.PI / 2; - arrow.scaling.x = 0.5; - - // Merge the nametag meshes. - const mergedMesh = Mesh.MergeMeshes( - [plane, ...corners, ...edges, arrow], - true, - true, - undefined, - false, - true - ); - - if (!mergedMesh) { - return undefined; - } - - // Disable the merged-mesh so it isn't rendered. - mergedMesh.setEnabled(false); - // Parent it to the memo node. - if (!nametagMemoNode) { - nametagMemoNode = new TransformNode("NametagDuplicates", scene); - } - mergedMesh.parent = nametagMemoNode; - - // Memoize the mesh. - meshMemo.set(memoName, mergedMesh); - mesh = mergedMesh.clone("Nametag", object, false, false); - } - - // Position the nametag above the center of the object. - const positionOffset = new Vector3(0, 0.15, 0); - let h = 0; - let heightHysteresis: Nullable = null; - if (typeof height === "number") { - h = height + positionOffset.y; - } else { - h = height() + positionOffset.y; - heightHysteresis = new Hysteresis( - () => height() + positionOffset.y, - 100, - positionOffset.y - ); - } - mesh.position = new Vector3(positionOffset.x, h, positionOffset.z); - - const scaleAdjustmentFactorX = - object.scaling.x > 0 ? 1 / object.scaling.x : 1; - const scaleAdjustmentFactorY = - object.scaling.y > 0 ? 1 / object.scaling.y : 1; - const scaleAdjustmentFactorZ = - object.scaling.z > 0 ? 1 / object.scaling.z : 1; - mesh.scaling.x = scaleAdjustmentFactorX; - mesh.scaling.y = scaleAdjustmentFactorY; - mesh.scaling.z = scaleAdjustmentFactorZ; - - mesh.scaling.x *= -1; - mesh.billboardMode = Mesh.BILLBOARDMODE_Y; - mesh.parent = object; - mesh.isPickable = false; - mesh.renderingGroupId = DEFAULT_MESH_RENDER_GROUP_ID; - mesh.setEnabled(true); - - // Hide the nametag if it is too far from the avatar, - // or if `showNametags` has been turned off in the Store. - scene.registerBeforeRender(() => { - if (!mesh) { - return; - } - // Update the nametag's position. - if (heightHysteresis) { - mesh.position.y = heightHysteresis.get(); - } - // Update the nametag's opacity. - const avatar = Renderer.getScene()?.getMyAvatar(); - if (avatar) { - const avatarPosition = avatar.getAbsolutePosition().clone(); - const nametagPosition = mesh.getAbsolutePosition(); - const distance = avatarPosition - .subtract(nametagPosition) - .length(); - // Clamp the opacity between 0 and 0.94. - // Max opacity of 0.94 reduces the chance that the nametag will be affected by bloom. - const opacity = Math.min( - Math.max(popDistance + 1 - distance, 0), - 0.94 - ); - mesh.visibility = - opacity * - Number( - userStore.avatar.showNametags && - (popOverride?.(distance) ?? true) - ); - mesh.isVisible = true; - } - }); - - return mesh; - } - - /** - * Remove a nametag entity from an object. - * @param nametagMesh The nametag mesh to remove. - */ - public static remove( - nametagMesh: Mesh | AbstractMesh | TransformNode | undefined | null - ): void { - if (!nametagMesh || !(/^Nametag/iu).test(nametagMesh.name)) { - return; - } - nametagMesh.dispose(false, true); - } - - /** - * Remove all nametag entities from an object. - * @param object The object to remove all nametags from. - */ - public static removeAll( - object: Mesh | AbstractMesh | TransformNode | undefined | null - ): void { - if (!object) { - return; - } - const nametagMeshes = object.getChildMeshes(false, (node) => - (/^Nametag/iu).test(node.name) - ); - nametagMeshes.forEach((nametagMesh) => - nametagMesh.dispose(false, true) - ); - } -} +// +// LabelEntity.ts +// +// Created by Giga on 16 Feb 2023. +// Copyright 2023 Vircadia contributors. +// Copyright 2023 DigiSomni LLC. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* eslint-disable @typescript-eslint/no-magic-numbers */ + +import { + type AbstractMesh, + Color3, + DynamicTexture, + Matrix, + Mesh, + MeshBuilder, + Scene, + StandardMaterial, + TransformNode, + Vector3, +} from "@babylonjs/core"; +import { DEFAULT_MESH_RENDER_GROUP_ID } from "@Modules/object"; +import { Renderer } from "@Modules/scene"; +import { userStore } from "@Stores/index"; +import { Hysteresis } from "@Modules/utility/hysteresis"; + +/** + * Contains all of the memoized label meshes within the scene. + */ +let labelMemoNode: TransformNode | undefined = undefined; + +const meshMemo = new Map(); +const foregroundTextureMemo = new Map(); +const backgroundTextureMemo = new Map(); +const foregroundMaterialMemo = new Map(); +const backgroundMaterialMemo = new Map(); + +/** + * Create a sector mesh. + * @param name The name of the mesh. + * @param vector1 The vector at the leading edge of the sector. + * @param vector2 The vector at the trailing edge of the sector. + * @param radius The radius of the sector. + * @param scene The hosting scene. + * @returns The new sector mesh. + */ +function createSector( + name: string, + vector1: Vector3, + vector2: Vector3, + radius = 1, + scene?: Scene +): Mesh { + // Get the angle between the two vectors. + const sectorAngle = Math.acos( + Vector3.Dot(vector1, vector2) / (vector1.length() * vector2.length()) + ); + const minNumberOfSegments = 5; + const diameter = radius * 2; + const origin = Vector3.Zero(); + const firstPoint = Vector3.Normalize(vector1).scale(radius); + const lastPoint = Vector3.Normalize(vector2).scale(radius); + + // Divide the sector angle into a number of segments angles. + const segments = Math.max( + Math.floor(diameter * sectorAngle), + minNumberOfSegments + ); + const segmentAngle = sectorAngle / segments; + + // Create points to connect each segment. + const points = new Array(); + for (let i = 0; i < segments; i++) { + const matrix = Matrix.RotationAxis( + Vector3.Cross(vector1, vector2), + segmentAngle * i + ); + const rotated = Vector3.TransformCoordinates(firstPoint, matrix); + points.push(rotated.add(origin)); + } + points.push(lastPoint.add(origin)); + + // Connect each segment point back to the origin. + const originPoints = new Array(); + points.forEach(() => { + originPoints.push(origin); + }); + + // Create a ribbon mesh from the points. + const sector = MeshBuilder.CreateRibbon( + name, + { + pathArray: [points, originPoints], + offset: 0, + sideOrientation: Mesh.DOUBLESIDE + }, + scene + ); + + return sector; +} + +export class LabelEntity { + private static _textFont = { + name: "monospace", + size: 70, + characterWidth: 38.5, + characterRatio: 1.43, + contentRatio: 0.1, + }; + + private static _iconFont = { + name: "Material Icons", + size: 100, + characterWidth: 100, + characterRatio: 1, + contentRatio: 0.16, + }; + + /** + * Create a new label entity and attach it to an object. + * @param object The mesh/transform node to attach the label to. + * @param height The height of the object (the label will be positioned above this point). + * @param name The name to be displayed on the label. + * @param icon Display the name as an icon instead of text. + * @param color The color of the label's background. + * @param popDistance The distance from the player's avatar at which the label will stop being visible. + * @param popOverride A function overriding the visibility of the label. + * This function receives the distance from the player's avatar to the label, + * and should return a boolean indicating whether the label should be visible (`true`) or not (`false`). + * @returns A reference to the new label mesh. + */ + public static create( + object: Mesh | AbstractMesh | TransformNode, + height: number | (() => number), + name: string, + icon = false, + color?: Color3, + popDistance = 20, + popOverride?: (distance: number) => boolean + ): Mesh | undefined { + const scene = object.getScene(); + const font = icon ? this._iconFont : this._textFont; + const tagTextureWidth = icon + ? font.characterWidth * 1.2 + : (name.length + 1) * font.characterWidth; + const tagTextureHeight = font.size * font.characterRatio; + const tagWidth = + (font.contentRatio * tagTextureWidth) / tagTextureHeight; + const tagHeight = font.contentRatio; + const tagCornerRadius = tagHeight / 6; + const tagArrowSize = 0.02; + const tagBackgroundColor = color ?? new Color3(0.07, 0.07, 0.07); + const tagBackgroundColorString = tagBackgroundColor.toHexString(); + const memoName = `${name}${icon ? "-i" : "" + }-${tagBackgroundColorString}`; + + // Attempt to reuse a memoized mesh, if one exists. + let mesh = meshMemo + .get(memoName) + ?.clone("Label", object, false, false); + + // If a matching mesh doesn't already exist, create a new one. + if (!mesh) { + // Textures. + let foregroundTexture = foregroundTextureMemo.get(memoName); + // If a matching texture doesn't already exist, create a new one. + if (!foregroundTexture) { + // Create the texture. + const newForegroundTexture = new DynamicTexture( + `LabelTexture-${memoName}`, + { width: tagTextureWidth, height: tagTextureHeight }, + scene + ); + // Center the name on the tag. + const textPosition = icon + ? tagTextureWidth / 2 - font.characterWidth / 2 + : tagTextureWidth / 2 - + (name.length / 2) * font.characterWidth; + newForegroundTexture.drawText( + name, + textPosition, + font.size, + `${font.size}px ${font.name}`, + "white", + tagBackgroundColorString, + true, + true + ); + newForegroundTexture.getAlphaFromRGB = true; + // Memoize the texture. + foregroundTextureMemo.set(memoName, newForegroundTexture); + foregroundTexture = newForegroundTexture; + } + + let backgroundTexture = backgroundTextureMemo.get( + tagBackgroundColorString + ); + // If a matching texture doesn't already exist, create a new one. + if (!backgroundTexture) { + // Create the texture. + const newBackgroundTexture = new DynamicTexture( + `LabelBackgroundTexture-${tagBackgroundColorString}`, + { width: tagTextureWidth, height: tagTextureHeight }, + scene + ); + newBackgroundTexture.drawText( + "", + 0, + 0, + `${font.size}px ${font.name}`, + "white", + tagBackgroundColorString, + true, + true + ); + newBackgroundTexture.getAlphaFromRGB = true; + // Memoize the texture. + backgroundTextureMemo.set( + tagBackgroundColorString, + newBackgroundTexture + ); + backgroundTexture = newBackgroundTexture; + } + + // Materials. + let foregroundMaterial = foregroundMaterialMemo.get(memoName); + // If a matching material doesn't already exist, create a new one. + if (!foregroundMaterial) { + // Create the material. + const newForegroundMaterial = new StandardMaterial( + `LabelMaterial-${memoName}`, + scene + ); + newForegroundMaterial.diffuseTexture = foregroundTexture; + newForegroundMaterial.specularTexture = foregroundTexture; + newForegroundMaterial.emissiveTexture = foregroundTexture; + newForegroundMaterial.disableLighting = true; + // Memoize the material. + foregroundMaterialMemo.set(memoName, newForegroundMaterial); + foregroundMaterial = newForegroundMaterial; + } + + let backgroundMaterial = backgroundMaterialMemo.get( + tagBackgroundColorString + ); + // If a matching material doesn't already exist, create a new one. + if (!backgroundMaterial) { + // Create the material. + const newBackgroundMaterial = new StandardMaterial( + `LabelBackgroundMaterial-${tagBackgroundColorString}`, + scene + ); + newBackgroundMaterial.diffuseTexture = backgroundTexture; + newBackgroundMaterial.specularTexture = backgroundTexture; + newBackgroundMaterial.emissiveTexture = backgroundTexture; + newBackgroundMaterial.disableLighting = true; + // Memoize the material. + backgroundMaterialMemo.set( + tagBackgroundColorString, + newBackgroundMaterial + ); + backgroundMaterial = newBackgroundMaterial; + } + + // Meshes. + const plane = MeshBuilder.CreatePlane( + "Label", + { + width: tagWidth, + height: tagHeight, + sideOrientation: Mesh.DOUBLESIDE, + updatable: true, + }, + scene + ); + plane.material = foregroundMaterial; + + // Rounded corners. + const corners = new Array(); + const cornerPositions = [ + new Vector3(-tagWidth / 2, tagHeight / 2 - tagCornerRadius, 0), + new Vector3(tagWidth / 2, tagHeight / 2 - tagCornerRadius, 0), + new Vector3(tagWidth / 2, -tagHeight / 2 + tagCornerRadius, 0), + new Vector3(-tagWidth / 2, -tagHeight / 2 + tagCornerRadius, 0), + ]; + const sector = createSector( + "LabelCorner", + Vector3.Up(), + Vector3.Left(), + tagCornerRadius, + scene + ); + corners.push(sector); + corners.push(sector.clone("LabelCorner")); + corners.push(sector.clone("LabelCorner")); + corners.push(sector.clone("LabelCorner")); + let index = 0; + for (const cornerMesh of corners) { + cornerMesh.material = backgroundMaterial; + cornerMesh.position = cornerPositions[index]; + cornerMesh.rotate(new Vector3(0, 0, 1), -index * (Math.PI / 2)); + index += 1; + } + + // Left and right edges. + const edges = new Array(); + const edgeOptions = { + width: tagCornerRadius, + height: tagHeight - tagCornerRadius * 2, + sideOrientation: Mesh.DOUBLESIDE, + updatable: true, + }; + const edgePositions = [ + new Vector3(-tagWidth / 2 - tagCornerRadius / 2, 0, 0), + new Vector3(tagWidth / 2 + tagCornerRadius / 2, 0, 0), + ]; + edges.push( + MeshBuilder.CreatePlane("LabelLeftEdge", edgeOptions, scene) + ); + edges.push( + MeshBuilder.CreatePlane("LabelRightEdge", edgeOptions, scene) + ); + index = 0; + for (const edgeMesh of edges) { + edgeMesh.material = backgroundMaterial; + edgeMesh.position = edgePositions[index]; + index += 1; + } + + // Arrow mesh. + const arrow = MeshBuilder.CreateDisc( + "LabelArrow", + { + radius: tagArrowSize, + tessellation: 3, + sideOrientation: Mesh.DOUBLESIDE, + updatable: true, + }, + scene + ); + arrow.material = backgroundMaterial; + arrow.position = new Vector3( + 0, + -(tagHeight / 2 + tagArrowSize / 4), + 0 + ); + arrow.rotation.z = -Math.PI / 2; + arrow.scaling.x = 0.5; + + // Merge the label meshes. + const mergedMesh = Mesh.MergeMeshes( + [plane, ...corners, ...edges, arrow], + true, + true, + undefined, + false, + true + ); + + if (!mergedMesh) { + return undefined; + } + + // Disable the merged-mesh so it isn't rendered. + mergedMesh.setEnabled(false); + // Parent it to the memo node. + if (!labelMemoNode) { + labelMemoNode = new TransformNode("LabelDuplicates", scene); + } + mergedMesh.parent = labelMemoNode; + + // Memoize the mesh. + meshMemo.set(memoName, mergedMesh); + mesh = mergedMesh.clone("Label", object, false, false); + } + + // Position the label above the center of the object. + const positionOffset = new Vector3(0, 0.15, 0); + let h = 0; + let heightHysteresis: Nullable = null; + if (typeof height === "number") { + h = height + positionOffset.y; + } else { + h = height() + positionOffset.y; + heightHysteresis = new Hysteresis( + () => height() + positionOffset.y, + 100, + positionOffset.y + ); + } + mesh.position = new Vector3(positionOffset.x, h, positionOffset.z); + + const scaleAdjustmentFactorX = + object.scaling.x > 0 ? 1 / object.scaling.x : 1; + const scaleAdjustmentFactorY = + object.scaling.y > 0 ? 1 / object.scaling.y : 1; + const scaleAdjustmentFactorZ = + object.scaling.z > 0 ? 1 / object.scaling.z : 1; + mesh.scaling.x = scaleAdjustmentFactorX; + mesh.scaling.y = scaleAdjustmentFactorY; + mesh.scaling.z = scaleAdjustmentFactorZ; + + mesh.scaling.x *= -1; + mesh.billboardMode = Mesh.BILLBOARDMODE_Y; + mesh.parent = object; + mesh.isPickable = false; + mesh.renderingGroupId = DEFAULT_MESH_RENDER_GROUP_ID; + mesh.setEnabled(true); + + // Hide the label if it is too far from the avatar, + // or if `showLabels` has been turned off in the Store. + scene.registerBeforeRender(() => { + if (!mesh) { + return; + } + // Update the label's position. + if (heightHysteresis) { + mesh.position.y = heightHysteresis.get(); + } + // Update the label's opacity. + const avatar = Renderer.getScene()?.getMyAvatar(); + if (avatar) { + const avatarPosition = avatar.getAbsolutePosition().clone(); + const labelPosition = mesh.getAbsolutePosition(); + const distance = avatarPosition + .subtract(labelPosition) + .length(); + // Clamp the opacity between 0 and 0.94. + // Max opacity of 0.94 reduces the chance that the label will be affected by bloom. + const opacity = Math.min( + Math.max(popDistance + 1 - distance, 0), + 0.94 + ); + mesh.visibility = + opacity * + Number( + userStore.avatar.showLabels && + (popOverride?.(distance) ?? true) + ); + mesh.isVisible = true; + } + }); + + return mesh; + } + + /** + * Remove a label entity from an object. + * @param labelMesh The label mesh to remove. + */ + public static remove( + labelMesh: Mesh | AbstractMesh | TransformNode | undefined | null + ): void { + if (!labelMesh || !(/^Label/iu).test(labelMesh.name)) { + return; + } + labelMesh.dispose(false, true); + } + + /** + * Remove all label entities from an object. + * @param object The object to remove all labels from. + */ + public static removeAll( + object: Mesh | AbstractMesh | TransformNode | undefined | null + ): void { + if (!object) { + return; + } + const labelMeshes = object.getChildMeshes(false, (node) => + (/^Label/iu).test(node.name) + ); + labelMeshes.forEach((labelMesh) => + labelMesh.dispose(false, true) + ); + } +} diff --git a/src/modules/entity/entities/index.ts b/src/modules/entity/entities/index.ts index e0f9e462..bc6a1ab5 100644 --- a/src/modules/entity/entities/index.ts +++ b/src/modules/entity/entities/index.ts @@ -17,4 +17,4 @@ export { ZoneEntity } from "./ZoneEntity"; export { ImageEntity } from "./ImageEntity"; export { MaterialEntity } from "./materialEntity"; export { WebEntity } from "./WebEntity"; -export { NametagEntity } from "./NametagEntity"; +export { LabelEntity } from "./LabelEntity"; diff --git a/src/modules/scene/vscene.ts b/src/modules/scene/vscene.ts index 7f60f503..d61a1d47 100644 --- a/src/modules/scene/vscene.ts +++ b/src/modules/scene/vscene.ts @@ -56,7 +56,7 @@ import { EntityBuilder, EntityEvent, } from "@Modules/entity"; -import { NametagEntity } from "@Modules/entity/entities"; +import { LabelEntity } from "@Modules/entity/entities"; import { DomainManager } from "@Modules/domain"; import { Location } from "@Modules/domain/location"; import { DataMapper } from "@Modules/domain/dataMapper"; @@ -454,7 +454,7 @@ export class VScene { avatarController.avatarRoot = myAvatarController.skeletonRootPosition; avatarController.camera = this._camera as ArcRotateCamera; - const nametagHeightGetter = () => + const labelHeightGetter = () => myAvatarController.skeletonRootPosition.y + avatarHeight / 2; this._myAvatar.addComponent(avatarController); this._myAvatar.addComponent(myAvatarController); @@ -471,50 +471,50 @@ export class VScene { this._myAvatar ); - // Add a nametag to the avatar. - let nametagColor = userStore.account.isAdmin + // Add a label to the avatar. + let labelColor = userStore.account.isAdmin ? Color3.FromHexString(applicationStore.theme.colors.primary) : undefined; - NametagEntity.create( + LabelEntity.create( this._myAvatar, - nametagHeightGetter, + labelHeightGetter, userStore.avatar.displayName, false, - nametagColor + labelColor ); - // Update the nametag color when the player's admin state is changed in the Store. + // Update the label color when the player's admin state is changed in the Store. watch( () => userStore.account.isAdmin, (value: boolean) => { - nametagColor = value + labelColor = value ? Color3.FromHexString( applicationStore.theme.colors.primary ) : undefined; - NametagEntity.removeAll(this._myAvatar); + LabelEntity.removeAll(this._myAvatar); if (this._myAvatar) { - NametagEntity.create( + LabelEntity.create( this._myAvatar, - nametagHeightGetter, + labelHeightGetter, userStore.avatar.displayName, false, - nametagColor + labelColor ); } } ); - // Update the nametag when the displayName is changed in the Store. + // Update the label when the displayName is changed in the Store. watch( () => userStore.avatar.displayName, (value: string) => { - NametagEntity.removeAll(this._myAvatar); + LabelEntity.removeAll(this._myAvatar); if (this._myAvatar) { - NametagEntity.create( + LabelEntity.create( this._myAvatar, - nametagHeightGetter, + labelHeightGetter, value, false, - nametagColor + labelColor ); } } @@ -581,59 +581,59 @@ export class VScene { (bone) => bone.name === "Hips" ); const hipPosition = hipBone?.position; - const nametagHeight = () => + const labelHeight = () => (hipPosition?.y ?? avatarHeight / 2) + avatarHeight / 2; this._avatarList.set(stringId, avatar); - // Add a nametag to the avatar. - let nametagColor = applicationStore.avatars.avatarsInfo.get(id) + // Add a label to the avatar. + let labelColor = applicationStore.avatars.avatarsInfo.get(id) ?.isAdmin ? Color3.FromHexString(applicationStore.theme.colors.primary) : undefined; - NametagEntity.create( + LabelEntity.create( avatar, - nametagHeight, + labelHeight, domain.displayName, false, - nametagColor + labelColor ); - // Update the nametag color when the player's admin state is changed. + // Update the label color when the player's admin state is changed. watch( () => Boolean( applicationStore.avatars.avatarsInfo.get(id)?.isAdmin ), (value: boolean) => { - nametagColor = value + labelColor = value ? Color3.FromHexString( applicationStore.theme.colors.primary ) : undefined; - const nametagAvatar = this._avatarList.get(stringId); - NametagEntity.removeAll(nametagAvatar); - if (nametagAvatar) { - NametagEntity.create( - nametagAvatar, - nametagHeight, + const labelAvatar = this._avatarList.get(stringId); + LabelEntity.removeAll(labelAvatar); + if (labelAvatar) { + LabelEntity.create( + labelAvatar, + labelHeight, domain.displayName, false, - nametagColor + labelColor ); } } ); - // Update the nametag when the displayName is changed. + // Update the label when the displayName is changed. domain.displayNameChanged.connect(() => { - const nametagAvatar = this._avatarList.get(stringId); - NametagEntity.removeAll(nametagAvatar); - if (nametagAvatar) { - NametagEntity.create( - nametagAvatar, - nametagHeight, + const labelAvatar = this._avatarList.get(stringId); + LabelEntity.removeAll(labelAvatar); + if (labelAvatar) { + LabelEntity.create( + labelAvatar, + labelHeight, domain.displayName, false, - nametagColor + labelColor ); } }); diff --git a/src/stores/user-store.ts b/src/stores/user-store.ts index fb3918ca..dadf1f4a 100644 --- a/src/stores/user-store.ts +++ b/src/stores/user-store.ts @@ -81,7 +81,7 @@ export const useUserStore = defineStore("user", { "userAvatarSettings", { displayName: "anonymous", - showNametags: true, + showLabels: true, position: Vec3.ZERO, location: "0,0,0", models: defaultAvatars(),