From d30268d4fe8325ea17a8a8206711bf42dbd79974 Mon Sep 17 00:00:00 2001 From: Appaji Date: Wed, 3 Jan 2024 05:21:13 +0530 Subject: [PATCH 1/2] Add scrubber to UVOL2 --- packages/client-core/i18n/en/editor.json | 1 + packages/common/src/utils/usePrevious.ts | 35 +++++++++++ .../properties/VolumetricNodeEditor.tsx | 22 ++++++- .../src/scene/components/UVOL1Component.ts | 2 + .../src/scene/components/UVOL2Component.ts | 63 ++++++++++++------- .../scene/components/VolumetricComponent.ts | 11 +++- 6 files changed, 108 insertions(+), 26 deletions(-) create mode 100644 packages/common/src/utils/usePrevious.ts diff --git a/packages/client-core/i18n/en/editor.json b/packages/client-core/i18n/en/editor.json index 195dceccc6..dd4bc3a4d9 100755 --- a/packages/client-core/i18n/en/editor.json +++ b/packages/client-core/i18n/en/editor.json @@ -235,6 +235,7 @@ "lbl-synchronize": "Synchronize", "info-synchronize": "If true, the media will be synchronized on all the connected clients.", "lbl-volume": "Volume", + "lbl-currentTime": "Current Time", "lbl-isMusic": "Is Music", "lbl-size": "Size", "seektime": "Start Time" diff --git a/packages/common/src/utils/usePrevious.ts b/packages/common/src/utils/usePrevious.ts new file mode 100644 index 0000000000..82fbc65e58 --- /dev/null +++ b/packages/common/src/utils/usePrevious.ts @@ -0,0 +1,35 @@ +/* +CPAL-1.0 License + +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + +The Original Code is Ethereal Engine. + +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. + +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ + +import { StateMethods } from '@hookstate/core' +import { useEffect, useRef } from 'react' + +export const usePrevious = (value: StateMethods) => { + const ref = useRef(null as T | null) + useEffect(() => { + ref.current = value.value + }, [value]) + return ref.current +} diff --git a/packages/editor/src/components/properties/VolumetricNodeEditor.tsx b/packages/editor/src/components/properties/VolumetricNodeEditor.tsx index fc71ede2a3..9ca347c160 100755 --- a/packages/editor/src/components/properties/VolumetricNodeEditor.tsx +++ b/packages/editor/src/components/properties/VolumetricNodeEditor.tsx @@ -27,11 +27,13 @@ import React from 'react' import { useTranslation } from 'react-i18next' import { VolumetricFileTypes } from '@etherealengine/engine/src/assets/constants/fileTypes' -import { useComponent } from '@etherealengine/engine/src/ecs/functions/ComponentFunctions' +import { hasComponent, useComponent } from '@etherealengine/engine/src/ecs/functions/ComponentFunctions' import { VolumetricComponent } from '@etherealengine/engine/src/scene/components/VolumetricComponent' import { PlayMode } from '@etherealengine/engine/src/scene/constants/PlayMode' +import { UVOL2Component } from '@etherealengine/engine/src/scene/components/UVOL2Component' import VideocamIcon from '@mui/icons-material/Videocam' +import { debounce } from 'lodash' import { ItemTypes } from '../../constants/AssetTypes' import ArrayInputGroup from '../inputs/ArrayInputGroup' import BooleanInput from '../inputs/BooleanInput' @@ -76,6 +78,12 @@ export const VolumetricNodeEditor: EditorComponentType = (props) => { volumetricComponent.paused.set(!volumetricComponent.paused.value) } + const onChange = (value: number) => { + volumetricComponent.startTime.set(value) + } + + const debouncedOnChange = debounce(onChange, 200) + return ( { acceptDropItems={ItemTypes.Volumetrics} /> + {hasComponent(props.entity, UVOL2Component) && ( + + + + )} + { removeObjectFromGroup(entity, mesh) @@ -253,6 +254,7 @@ function UVOL1Reactor() { mesh.material.needsUpdate = true oldMaterial.dispose() } + volumetric.currentTrackInfo.currentTime.set(frameToPlay / component.data.frameRate.value) if (meshBuffer.has(frameToPlay)) { // @ts-ignore: value cannot be anything else other than BufferGeometry diff --git a/packages/engine/src/scene/components/UVOL2Component.ts b/packages/engine/src/scene/components/UVOL2Component.ts index df65138ea0..98ef765359 100644 --- a/packages/engine/src/scene/components/UVOL2Component.ts +++ b/packages/engine/src/scene/components/UVOL2Component.ts @@ -23,6 +23,7 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ +import { usePrevious } from '@etherealengine/common/src/utils/usePrevious' import { getState } from '@etherealengine/hyperflux' import { useEffect, useMemo } from 'react' import { @@ -237,7 +238,6 @@ export const UVOL2Component = defineComponent({ } }, playbackStartTime: 0, - currentTime: 0, initialGeometryBuffersLoaded: false, initialTextureBuffersLoaded: false, firstGeometryFrameLoaded: false, @@ -373,6 +373,7 @@ function UVOL2Reactor() { let bufferThreshold = 6 // seconds. If buffer health is less than this, fetch new data const repeat = useMemo(() => new Vector2(1, 1), []) const offset = useMemo(() => new Vector2(0, 0), []) + const previousStartTime = usePrevious(volumetric.startTime) const material = useMemo(() => { const manifest = component.data.value @@ -525,7 +526,8 @@ transformed.z += mix(keyframeA.z, keyframeB.z, mixRatio); audio.playbackRate = sortedManifest.audio.playbackRate } - component.currentTime.set(volumetric.startTime.value) + volumetric.currentTrackInfo.currentTime.set(volumetric.startTime.value) + volumetric.currentTrackInfo.duration.set(sortedManifest.duration) const intervalId = setInterval(bufferLoop, 1000) bufferLoop() // calling now because setInterval will call after 1 second @@ -726,7 +728,8 @@ transformed.z += mix(keyframeA.z, keyframeB.z, mixRatio); const fetchGeometry = () => { const currentBufferLength = - component.geometryInfo.bufferHealth.value - (component.currentTime.value - volumetric.startTime.value) + component.geometryInfo.bufferHealth.value - + (volumetric.currentTrackInfo.currentTime.value - volumetric.startTime.value) if ( currentBufferLength >= Math.min(bufferThreshold, maxBufferHealth) || component.geometryInfo.pendingRequests.value > 0 @@ -772,7 +775,8 @@ transformed.z += mix(keyframeA.z, keyframeB.z, mixRatio); const textureTypeData = component.data.texture[textureType].value if (!textureTypeData) return const currentBufferLength = - component.textureInfo[textureType].bufferHealth.value - (component.currentTime.value - volumetric.startTime.value) + component.textureInfo[textureType].bufferHealth.value - + (volumetric.currentTrackInfo.currentTime.value - volumetric.startTime.value) if ( currentBufferLength >= Math.min(bufferThreshold, maxBufferHealth) || component.textureInfo[textureType].pendingRequests.value > 0 @@ -868,8 +872,8 @@ transformed.z += mix(keyframeA.z, keyframeB.z, mixRatio); if (!component.firstGeometryFrameLoaded.value || !component.firstTextureFrameLoaded.value) { return } - updateGeometry(component.currentTime.value) - updateAllTextures(component.currentTime.value) + updateGeometry(volumetric.currentTrackInfo.currentTime.value) + updateAllTextures(volumetric.currentTrackInfo.currentTime.value) if (volumetric.useLoadingEffect.value) { let headerTemplate: RegExp | undefined = /\/\/\sHEADER_REPLACE_START([\s\S]*?)\/\/\sHEADER_REPLACE_END/ @@ -921,17 +925,17 @@ transformed.z += mix(keyframeA.z, keyframeB.z, mixRatio); } return } - component.playbackStartTime.set(engineState.elapsedSeconds * 1000) - component.geometryInfo.bufferHealth.set( - component.geometryInfo.bufferHealth.value - (component.currentTime.value - volumetric.startTime.value) - ) - component.textureInfo.textureTypes.value.forEach((textureType) => { - const currentHealth = component.textureInfo[textureType].bufferHealth.value - component.textureInfo[textureType].bufferHealth.set( - currentHealth - (component.currentTime.value - volumetric.startTime.value) - ) - }) - volumetric.startTime.set(component.currentTime.value) + // component.playbackStartTime.set(engineState.elapsedSeconds * 1000) + // component.geometryInfo.bufferHealth.set( + // component.geometryInfo.bufferHealth.value - (volumetric.currentTrackInfo.currentTime.value - volumetric.startTime.value) + // ) + // component.textureInfo.textureTypes.value.forEach((textureType) => { + // const currentHealth = component.textureInfo[textureType].bufferHealth.value + // component.textureInfo[textureType].bufferHealth.set( + // currentHealth - (volumetric.currentTrackInfo.currentTime.value - volumetric.startTime.value) + // ) + // }) + volumetric.startTime.set(volumetric.currentTrackInfo.currentTime.value) if (mesh.material !== material) { mesh.material = material @@ -940,9 +944,22 @@ transformed.z += mix(keyframeA.z, keyframeB.z, mixRatio); if (component.hasAudio.value) { handleAutoplay(audioContext, audio, volumetric) } - component.canPlay.set(true) }, [volumetric.paused]) + useEffect(() => { + if (volumetric.paused.value) return + const previousValue = previousStartTime != null ? previousStartTime : 0 + component.playbackStartTime.set(engineState.elapsedSeconds * 1000) + component.geometryInfo.bufferHealth.set( + component.geometryInfo.bufferHealth.value - (volumetric.startTime.value - previousValue) + ) + component.textureInfo.textureTypes.value.forEach((textureType) => { + const currentHealth = component.textureInfo[textureType].bufferHealth.value + component.textureInfo[textureType].bufferHealth.set(currentHealth - (volumetric.startTime.value - previousValue)) + }) + component.canPlay.set(true) + }, [volumetric.startTime, volumetric.paused]) + const getFrame = (currentTime: number, frameRate: number, integer = true) => { const frame = currentTime * frameRate return integer ? Math.round(frame) : frame @@ -1253,11 +1270,11 @@ transformed.z += mix(keyframeA.z, keyframeB.z, mixRatio); _currentTime = volumetric.startTime.value + (engineState.elapsedSeconds * 1000 - component.playbackStartTime.value) / 1000 } - component.currentTime.set(_currentTime) + volumetric.currentTrackInfo.currentTime.set(_currentTime) - if (component.currentTime.value > component.data.value.duration || audio.ended) { + if (volumetric.currentTrackInfo.currentTime.value > component.data.value.duration || audio.ended) { if (component.data.deletePreviousBuffers.value === false && volumetric.playMode.value === PlayMode.loop) { - component.currentTime.set(0) + volumetric.currentTrackInfo.currentTime.set(0) component.playbackStartTime.set(engineState.elapsedSeconds * 1000) } else { volumetric.ended.set(true) @@ -1265,8 +1282,8 @@ transformed.z += mix(keyframeA.z, keyframeB.z, mixRatio); } } - updateGeometry(component.currentTime.value) - updateAllTextures(component.currentTime.value) + updateGeometry(volumetric.currentTrackInfo.currentTime.value) + updateAllTextures(volumetric.currentTrackInfo.currentTime.value) } useExecute(update, { diff --git a/packages/engine/src/scene/components/VolumetricComponent.ts b/packages/engine/src/scene/components/VolumetricComponent.ts index a0b47dc5e4..7132043d99 100755 --- a/packages/engine/src/scene/components/VolumetricComponent.ts +++ b/packages/engine/src/scene/components/VolumetricComponent.ts @@ -89,7 +89,6 @@ export const VolumetricComponent = defineComponent({ useLoadingEffect: true, autoplay: true, startTime: 0, - currentTime: 0, lastUpdatedTime: 0, paused: true, initialBuffersLoaded: false, @@ -97,7 +96,11 @@ export const VolumetricComponent = defineComponent({ ended: true, volume: 1, playMode: PlayMode.loop as PlayMode, - track: -1 + track: -1, + currentTrackInfo: { + currentTime: 0, + duration: 0 + } } }, @@ -219,6 +222,10 @@ export function VolumetricReactor() { volumetric.paused.set(true) volumetric.startTime.set(0) volumetric.lastUpdatedTime.set(0) + volumetric.currentTrackInfo.set({ + currentTime: 0, + duration: 0 + }) } resetTrack() From 970041c7001d18460aaf9063352e5281d0b9b5f5 Mon Sep 17 00:00:00 2001 From: Appaji Date: Wed, 3 Jan 2024 21:29:09 +0530 Subject: [PATCH 2/2] Adds read-only scrubber for UVOL1 & UVOL2 Also adds currentTrack's label to the node editor. Longest common prefix among the tracks is omitted for brevity. --- .../src/components/inputs/StringInput.tsx | 5 ++ .../properties/VolumetricNodeEditor.tsx | 40 ++++++++++++---- .../src/scene/components/UVOL2Component.ts | 46 ++++++++----------- 3 files changed, 56 insertions(+), 35 deletions(-) diff --git a/packages/editor/src/components/inputs/StringInput.tsx b/packages/editor/src/components/inputs/StringInput.tsx index 654528a460..cdd50dc839 100755 --- a/packages/editor/src/components/inputs/StringInput.tsx +++ b/packages/editor/src/components/inputs/StringInput.tsx @@ -41,6 +41,11 @@ interface StyledNumericInputProps { const StyledNumericInput = React.forwardRef( ({ className = '', onChange, ...rest }, ref) => { + if (!onChange) { + return ( + + ) + } return ( ) diff --git a/packages/editor/src/components/properties/VolumetricNodeEditor.tsx b/packages/editor/src/components/properties/VolumetricNodeEditor.tsx index 9ca347c160..d5b923aca0 100755 --- a/packages/editor/src/components/properties/VolumetricNodeEditor.tsx +++ b/packages/editor/src/components/properties/VolumetricNodeEditor.tsx @@ -23,7 +23,7 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import React from 'react' +import React, { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { VolumetricFileTypes } from '@etherealengine/engine/src/assets/constants/fileTypes' @@ -31,9 +31,9 @@ import { hasComponent, useComponent } from '@etherealengine/engine/src/ecs/funct import { VolumetricComponent } from '@etherealengine/engine/src/scene/components/VolumetricComponent' import { PlayMode } from '@etherealengine/engine/src/scene/constants/PlayMode' +import { UVOL1Component } from '@etherealengine/engine/src/scene/components/UVOL1Component' import { UVOL2Component } from '@etherealengine/engine/src/scene/components/UVOL2Component' import VideocamIcon from '@mui/icons-material/Videocam' -import { debounce } from 'lodash' import { ItemTypes } from '../../constants/AssetTypes' import ArrayInputGroup from '../inputs/ArrayInputGroup' import BooleanInput from '../inputs/BooleanInput' @@ -41,6 +41,7 @@ import { Button } from '../inputs/Button' import CompoundNumericInput from '../inputs/CompoundNumericInput' import InputGroup from '../inputs/InputGroup' import SelectInput from '../inputs/SelectInput' +import StringInput from '../inputs/StringInput' import NodeEditor from './NodeEditor' import { EditorComponentType, commitProperty, updateProperty } from './Util' @@ -71,6 +72,7 @@ const PlayModeOptions = [ */ export const VolumetricNodeEditor: EditorComponentType = (props) => { const { t } = useTranslation() + const [trackLabel, setTrackLabel] = React.useState('') const volumetricComponent = useComponent(props.entity, VolumetricComponent) @@ -78,11 +80,30 @@ export const VolumetricNodeEditor: EditorComponentType = (props) => { volumetricComponent.paused.set(!volumetricComponent.paused.value) } - const onChange = (value: number) => { - volumetricComponent.startTime.set(value) - } + useEffect(() => { + const tracks = volumetricComponent.paths.value + if (tracks.length === 0) { + return + } + if (tracks.length === 1) { + const segments = tracks[0].split('/') + setTrackLabel(segments[segments.length - 1]) + return + } + + let prefix = tracks[0] + + // Don't show the longest common prefix + for (let j = 1; j < tracks.length; j++) { + while (tracks[j].indexOf(prefix) !== 0) { + prefix = prefix.substring(0, prefix.length - 1) + } + } - const debouncedOnChange = debounce(onChange, 200) + const currentTrackPath = tracks[volumetricComponent.track.value] + + setTrackLabel(currentTrackPath.slice(prefix.length)) + }, [volumetricComponent.track, volumetricComponent.ended, volumetricComponent.paths]) return ( { acceptDropItems={ItemTypes.Volumetrics} /> - {hasComponent(props.entity, UVOL2Component) && ( + {(hasComponent(props.entity, UVOL2Component) || hasComponent(props.entity, UVOL1Component)) && ( )} @@ -156,6 +176,10 @@ export const VolumetricNodeEditor: EditorComponentType = (props) => { )} + + + + ) } diff --git a/packages/engine/src/scene/components/UVOL2Component.ts b/packages/engine/src/scene/components/UVOL2Component.ts index 246f222fef..cf4423a40a 100644 --- a/packages/engine/src/scene/components/UVOL2Component.ts +++ b/packages/engine/src/scene/components/UVOL2Component.ts @@ -926,16 +926,17 @@ transformed.z += mix(keyframeA.z, keyframeB.z, mixRatio); } return } - // component.playbackStartTime.set(engineState.elapsedSeconds * 1000) - // component.geometryInfo.bufferHealth.set( - // component.geometryInfo.bufferHealth.value - (volumetric.currentTrackInfo.currentTime.value - volumetric.startTime.value) - // ) - // component.textureInfo.textureTypes.value.forEach((textureType) => { - // const currentHealth = component.textureInfo[textureType].bufferHealth.value - // component.textureInfo[textureType].bufferHealth.set( - // currentHealth - (volumetric.currentTrackInfo.currentTime.value - volumetric.startTime.value) - // ) - // }) + component.playbackStartTime.set(engineState.elapsedSeconds * 1000) + component.geometryInfo.bufferHealth.set( + component.geometryInfo.bufferHealth.value - + (volumetric.currentTrackInfo.currentTime.value - volumetric.startTime.value) + ) + component.textureInfo.textureTypes.value.forEach((textureType) => { + const currentHealth = component.textureInfo[textureType].bufferHealth.value + component.textureInfo[textureType].bufferHealth.set( + currentHealth - (volumetric.currentTrackInfo.currentTime.value - volumetric.startTime.value) + ) + }) volumetric.startTime.set(volumetric.currentTrackInfo.currentTime.value) if (mesh.material !== material) { @@ -945,21 +946,8 @@ transformed.z += mix(keyframeA.z, keyframeB.z, mixRatio); if (component.hasAudio.value) { handleAutoplay(audioContext, audio, volumetric) } - }, [volumetric.paused]) - - useEffect(() => { - if (volumetric.paused.value) return - const previousValue = previousStartTime != null ? previousStartTime : 0 - component.playbackStartTime.set(engineState.elapsedSeconds * 1000) - component.geometryInfo.bufferHealth.set( - component.geometryInfo.bufferHealth.value - (volumetric.startTime.value - previousValue) - ) - component.textureInfo.textureTypes.value.forEach((textureType) => { - const currentHealth = component.textureInfo[textureType].bufferHealth.value - component.textureInfo[textureType].bufferHealth.set(currentHealth - (volumetric.startTime.value - previousValue)) - }) component.canPlay.set(true) - }, [volumetric.startTime, volumetric.paused]) + }, [volumetric.paused]) const getFrame = (currentTime: number, frameRate: number, integer = true) => { const frame = currentTime * frameRate @@ -1266,8 +1254,12 @@ transformed.z += mix(keyframeA.z, keyframeB.z, mixRatio); if (volumetric.autoPauseWhenBuffering.value) { const currentGeometryBufferHealth = - component.geometryInfo.bufferHealth.value - (component.currentTime.value - volumetric.startTime.value) - const currentMinBuffer = Math.min(minBufferToPlay, component.data.duration.value - component.currentTime.value) + component.geometryInfo.bufferHealth.value - + (volumetric.currentTrackInfo.currentTime.value - volumetric.startTime.value) + const currentMinBuffer = Math.min( + minBufferToPlay, + component.data.duration.value - volumetric.currentTrackInfo.currentTime.value + ) if (currentGeometryBufferHealth < currentMinBuffer) { return } @@ -1275,7 +1267,7 @@ transformed.z += mix(keyframeA.z, keyframeB.z, mixRatio); const textureType = component.textureInfo.textureTypes[i].value const currentTextureBufferHealth = component.textureInfo[textureType].bufferHealth.value - - (component.currentTime.value - volumetric.startTime.value) + (volumetric.currentTrackInfo.currentTime.value - volumetric.startTime.value) if (currentTextureBufferHealth < currentMinBuffer) { return }