From 2a38a13bfc5a82b6a616b20683388c7338177a10 Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Wed, 24 Jan 2024 19:07:41 +0100 Subject: [PATCH 1/2] Volume controls in the video menu --- src/bit-components.js | 2 + src/bit-systems/video-menu-system.ts | 34 +++++++++++++- src/prefabs/video-menu.tsx | 70 +++++++++++++++++++++++++++- src/utils/jsx-entity.ts | 2 + 4 files changed, 104 insertions(+), 4 deletions(-) diff --git a/src/bit-components.js b/src/bit-components.js index c71bfb4627..07ab035b78 100644 --- a/src/bit-components.js +++ b/src/bit-components.js @@ -281,6 +281,8 @@ export const VideoMenu = defineComponent({ playIndicatorRef: Types.eid, pauseIndicatorRef: Types.eid, snapRef: Types.eid, + volUpRef: Types.eid, + volDownRef: Types.eid, clearTargetTimer: Types.f64 }); export const AudioEmitter = defineComponent({ diff --git a/src/bit-systems/video-menu-system.ts b/src/bit-systems/video-menu-system.ts index 2224de0822..f50798639a 100644 --- a/src/bit-systems/video-menu-system.ts +++ b/src/bit-systems/video-menu-system.ts @@ -1,9 +1,10 @@ import { addComponent, defineQuery, entityExists, hasComponent, removeComponent } from "bitecs"; -import { Object3D, Plane, Ray, Vector3 } from "three"; +import { MathUtils, Object3D, Plane, Ray, Vector3 } from "three"; import { clamp, mapLinear } from "three/src/math/MathUtils"; import { Text as TroikaText } from "troika-three-text"; import { HubsWorld } from "../app"; import { + AudioEmitter, CursorRaycastable, Deleting, EntityStateDirty, @@ -27,9 +28,11 @@ import { paths } from "../systems/userinput/paths"; import { isFacingCamera } from "../utils/three-utils"; import { Emitter2Audio } from "./audio-emitter-system"; import { EntityID } from "../utils/networking-types"; -import { findAncestorWithComponent, hasAnyComponent } from "../utils/bit-utils"; +import { findAncestorWithComponent, findChildWithComponent, hasAnyComponent } from "../utils/bit-utils"; import { ObjectMenuTransformFlags } from "../inflators/object-menu-transform"; import { MediaType } from "../utils/media-utils"; +import { VolumeControlsMaterial } from "../prefabs/video-menu"; +import { updateAudioSettings } from "../update-audio-settings"; const videoMenuQuery = defineQuery([VideoMenu]); const hoveredQuery = defineQuery([HoveredRemoteRight]); @@ -116,6 +119,12 @@ function flushToObject3Ds(world: HubsWorld, menu: EntityID, frozen: boolean) { ObjectMenuTransform.flags[menu] |= ObjectMenuTransformFlags.Enabled; const snapButton = world.eid2obj.get(VideoMenu.snapRef[menu])!; snapButton.visible = MediaInfo.mediaType[target] === MediaType.VIDEO; + + const audioEid = findChildWithComponent(world, AudioEmitter, target)!; + if (audioEid) { + const audio = APP.audios.get(audioEid)!; + VolumeControlsMaterial.uniforms.volume.value = audio.gain.gain.value; + } } else { obj.removeFromParent(); setCursorRaycastable(world, menu, false); @@ -127,6 +136,17 @@ function clicked(world: HubsWorld, eid: EntityID) { return hasComponent(world, Interacted, eid); } +const MAX_GAIN_MULTIPLIER = 2; +function changeVolumeBy(audioEid: EntityID, volume: number) { + let gainMultiplier = APP.gainMultipliers.get(audioEid) || 1; + gainMultiplier = MathUtils.clamp(gainMultiplier + volume, 0, MAX_GAIN_MULTIPLIER); + APP.gainMultipliers.set(audioEid, gainMultiplier); + const audio = APP.audios.get(audioEid); + if (audio) { + updateAudioSettings(audioEid, audio); + } +} + function handleClicks(world: HubsWorld, menu: EntityID) { const videoEid = VideoMenu.videoRef[menu]; const video = MediaVideoData.get(videoEid)!; @@ -150,6 +170,16 @@ function handleClicks(world: HubsWorld, menu: EntityID) { } else if (clicked(world, VideoMenu.snapRef[menu])) { const video = VideoMenu.videoRef[menu]; addComponent(world, MediaSnapped, video); + } else if (clicked(world, VideoMenu.volUpRef[menu])) { + const audioEid = findChildWithComponent(world, AudioEmitter, VideoMenu.videoRef[menu])!; + if (audioEid) { + changeVolumeBy(audioEid, 0.2); + } + } else if (clicked(world, VideoMenu.volDownRef[menu])) { + const audioEid = findChildWithComponent(world, AudioEmitter, VideoMenu.videoRef[menu])!; + if (audioEid) { + changeVolumeBy(audioEid, -0.2); + } } } diff --git a/src/prefabs/video-menu.tsx b/src/prefabs/video-menu.tsx index 8cecb41e0d..08f38bb639 100644 --- a/src/prefabs/video-menu.tsx +++ b/src/prefabs/video-menu.tsx @@ -1,5 +1,5 @@ /** @jsx createElementEntity */ -import { BoxBufferGeometry, Mesh, MeshBasicMaterial, PlaneBufferGeometry } from "three"; +import { BoxBufferGeometry, FrontSide, Mesh, MeshBasicMaterial, PlaneBufferGeometry, ShaderChunk } from "three"; import { Label } from "../prefabs/camera-tool"; import { ArrayVec3, Attrs, createElementEntity, createRef } from "../utils/jsx-entity"; import playImageUrl from "../assets/images/sprites/notice/play.png"; @@ -16,10 +16,36 @@ export async function loadVideoMenuButtonIcons() { ]); } +export const VolumeControlsMaterial = new THREE.ShaderMaterial({ + uniforms: { + volume: { value: 0.0 } + }, + vertexShader: ` + varying vec2 vUv; + void main() + { + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.); + vUv = uv; + } + `, + fragmentShader: ` + uniform float time; + uniform float volume; + varying vec2 vUv; + + void main() { + float step = step(volume, vUv.x); + gl_FragColor = mix(vec4(vec3(0.8), 1.0), vec4(vec3(0.2), 1.0), step); + } + ` +}); +VolumeControlsMaterial.side = FrontSide; + const uiZ = 0.001; const BUTTON_HEIGHT = 0.2; const BIG_BUTTON_SCALE: ArrayVec3 = [0.8, 0.8, 0.8]; const BUTTON_SCALE: ArrayVec3 = [0.6, 0.6, 0.6]; +const SMALL_BUTTON_SCALE: ArrayVec3 = [0.4, 0.4, 0.4]; const BUTTON_WIDTH = 0.2; function Slider({ trackRef, headRef, ...props }: any) { @@ -45,6 +71,33 @@ function Slider({ trackRef, headRef, ...props }: any) { ); } +interface VolumeButtonProps extends Attrs { + text: string; +} + +function VolumeButton(props: VolumeButtonProps) { + return ( + + ); +} + +function VolumeControls({ volUpRef, volDownRef, ...props }: any) { + return ( + + + + + + ); +} + interface VideoButtonProps extends Attrs { buttonIcon: string; } @@ -87,13 +140,25 @@ export function VideoMenuPrefab() { const playIndicatorRef = createRef(); const pauseIndicatorRef = createRef(); const snapRef = createRef(); + const volUpRef = createRef(); + const volDownRef = createRef(); const halfHeight = 9 / 16 / 2; return ( ); } diff --git a/src/utils/jsx-entity.ts b/src/utils/jsx-entity.ts index 3803b90f9a..15e9e9c9f8 100644 --- a/src/utils/jsx-entity.ts +++ b/src/utils/jsx-entity.ts @@ -290,6 +290,8 @@ export interface JSXComponentData extends ComponentData { playIndicatorRef: Ref; pauseIndicatorRef: Ref; snapRef: Ref; + volUpRef: Ref; + volDownRef: Ref; }; videoMenuItem?: true; cursorRaycastable?: true; From 889701f6922b3bd1b916ff96443d4fdf249671b3 Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Wed, 24 Jan 2024 19:16:27 +0100 Subject: [PATCH 2/2] Check if current gain is undefined --- src/bit-systems/video-menu-system.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/bit-systems/video-menu-system.ts b/src/bit-systems/video-menu-system.ts index f50798639a..9939d73989 100644 --- a/src/bit-systems/video-menu-system.ts +++ b/src/bit-systems/video-menu-system.ts @@ -138,7 +138,10 @@ function clicked(world: HubsWorld, eid: EntityID) { const MAX_GAIN_MULTIPLIER = 2; function changeVolumeBy(audioEid: EntityID, volume: number) { - let gainMultiplier = APP.gainMultipliers.get(audioEid) || 1; + let gainMultiplier = 1.0; + if (APP.gainMultipliers.has(audioEid)) { + gainMultiplier = APP.gainMultipliers.get(audioEid)!; + } gainMultiplier = MathUtils.clamp(gainMultiplier + volume, 0, MAX_GAIN_MULTIPLIER); APP.gainMultipliers.set(audioEid, gainMultiplier); const audio = APP.audios.get(audioEid);