Skip to content
This repository has been archived by the owner on Aug 21, 2024. It is now read-only.

Instanced LOD System #7782

Merged
merged 39 commits into from
Apr 12, 2023
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
aaa141f
define LODComponent, LODSystem
dinomut1 Mar 21, 2023
33d1437
Merge branch 'instanced-lods' of https://github.com/EtherealEngine/et…
dinomut1 Mar 21, 2023
35ffdb8
add dynamic loading of LODs based on reference
dinomut1 Mar 23, 2023
75dbc1a
create lods for all model meshes in scene reactor
dinomut1 Mar 24, 2023
4f1bfb1
refactor model node editor to hyperflux
dinomut1 Mar 24, 2023
38a4d0f
add LODProperties component to Model Properties
dinomut1 Mar 24, 2023
71f8e2a
fix type error in bakeToVertices
dinomut1 Mar 24, 2023
42b7d66
fix lodComponent assignment
dinomut1 Mar 24, 2023
1875cd7
Merge branch 'dev' of https://github.com/EtherealEngine/etherealengin…
dinomut1 Mar 24, 2023
359ad64
integrate LODSystem and onBeforeCompile
dinomut1 Mar 25, 2023
3054e08
add lod name labelling
dinomut1 Mar 25, 2023
465f2e7
Merge branch 'dev' of https://github.com/EtherealEngine/etherealengin…
dinomut1 Mar 28, 2023
55c6ccf
WIP implementing scene segmentation for LODs
dinomut1 Mar 29, 2023
bd4439d
Merge branch 'dev' of https://github.com/EtherealEngine/etherealengin…
dinomut1 Mar 29, 2023
b2ce289
move lod serialization into editor package
dinomut1 Mar 29, 2023
51ff0c8
Merge branch 'dev' of https://github.com/EtherealEngine/etherealengin…
dinomut1 Mar 29, 2023
15ea99e
Merge branch 'dev' of https://github.com/EtherealEngine/etherealengin…
dinomut1 Mar 29, 2023
ea72ce6
add button to serialize individual lods
dinomut1 Mar 29, 2023
e8b1539
add resourceURI to GLTFExporter options
dinomut1 Mar 30, 2023
53a9188
Merge branch 'dev' of https://github.com/EtherealEngine/etherealengin…
dinomut1 Mar 30, 2023
2097a1a
use resourceURI option for LOD serialization
dinomut1 Mar 30, 2023
6b98b34
Merge branch 'dev' of https://github.com/EtherealEngine/etherealengin…
dinomut1 Mar 30, 2023
50badf7
Merge branch 'dev' of https://github.com/EtherealEngine/etherealengin…
dinomut1 Mar 31, 2023
f09a4a2
expose parameters for KTX2Worker
dinomut1 Mar 31, 2023
78142c7
fix lod culling onBeforeCompile plugin
dinomut1 Mar 31, 2023
79d4dd1
rebuild KTX2Encoder and KTX2Worker
dinomut1 Mar 31, 2023
f477cc9
*fix gltf export from LODComponent
dinomut1 Apr 4, 2023
5aa73fd
Merge branch 'dev' of https://github.com/EtherealEngine/etherealengin…
dinomut1 Apr 4, 2023
ce33de6
fix cicd errors
dinomut1 Apr 4, 2023
169b9b2
base mvp for working lods
dinomut1 Apr 4, 2023
143622a
fix serialization
dinomut1 Apr 6, 2023
45d7f0c
change LODProperties into prefab editor
dinomut1 Apr 6, 2023
5a1948b
fix lint errors
dinomut1 Apr 6, 2023
ba6255f
implement mvp for instanced LODs
dinomut1 Apr 7, 2023
7a151ce
Merge branch 'dev' of https://github.com/EtherealEngine/etherealengin…
dinomut1 Apr 8, 2023
9ee74aa
refactor lod functions and system
dinomut1 Apr 8, 2023
eb239fa
Merge branch 'dev' of https://github.com/EtherealEngine/etherealengin…
dinomut1 Apr 11, 2023
b6c8795
share images and buffers for lod serialization
dinomut1 Apr 12, 2023
0286026
remove unused line
dinomut1 Apr 12, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export const ResourceService = {
}
},
fetchAdminResources: async (skip = 0, search: string | null = null, sortField = 'key', orderBy = 'asc') => {
let sortData = {}
const sortData = {}
if (sortField.length > 0) {
sortData[sortField] = orderBy === 'desc' ? 0 : 1
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ const FileBrowserContentPanel: React.FC<FileBrowserContentPanelProps> = (props)
}

const onBackDirectory = () => {
const pattern = /([^\/]+)/g
const pattern = /([^/]+)/g
const result = selectedDirectory.value.match(pattern)
if (!result) return
let newPath = '/'
Expand Down Expand Up @@ -308,7 +308,7 @@ const FileBrowserContentPanel: React.FC<FileBrowserContentPanelProps> = (props)
await onRefreshDirectory()
}

let currentContent = null! as { item: FileDataType; isCopy: boolean }
const currentContent = null! as { item: FileDataType; isCopy: boolean }
const currentContentRef = useRef(currentContent)

const headGrid = {
Expand All @@ -319,7 +319,7 @@ const FileBrowserContentPanel: React.FC<FileBrowserContentPanelProps> = (props)
}

function handleClick(targetFolder: string) {
const pattern = /([^\/]+)/g
const pattern = /([^/]+)/g
const result = selectedDirectory.value.match(pattern)
if (!result) return
let newPath = '/'
Expand Down
2 changes: 1 addition & 1 deletion packages/editor/src/components/inputs/NumericInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export interface NumericInputProp {
precision?: number
mediumStep?: number
onChange?: (n: number) => void
onCommit?: Function
onCommit?: (n: number) => void
smallStep?: number
largeStep?: number
min?: number
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface NumericInputGroupProp {
max?: number
value: any
onChange: (value: any) => void
onCommit?: (value: any) => void
unit?: string
convertFrom?: any
convertTo?: any
Expand Down
8 changes: 5 additions & 3 deletions packages/editor/src/components/layout/PaginatedList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ export default function PaginatedList<T>({
<Grid item xs={1}>
<Button
onClick={() =>
currentPage.set(Math.min(Math.floor(list.length / countPerPage), Math.max(0, currentPage.value + 1)))
currentPage.set(
Math.min(Math.floor((list.length - 1) / countPerPage), Math.max(0, currentPage.value + 1))
)
}
style={{ width: 'auto' }}
>
Expand All @@ -78,8 +80,8 @@ export default function PaginatedList<T>({
</Grid>
</Grid>
</Well>
{(pageView.value[0] === pageView.value[1] ? list : list.slice(...pageView.value)).map((index) => {
return element(index)
{(pageView.value[0] === pageView.value[1] ? list : list.slice(...pageView.value)).map((index, _index) => {
return <div key={`${_index}`}>{element(index)}</div>
})}
</>
)
Expand Down
120 changes: 120 additions & 0 deletions packages/editor/src/components/properties/LODProperties.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'

import { Entity } from '@etherealengine/engine/src/ecs/classes/Entity'
import {
getComponent,
getMutableComponent,
useComponent
} from '@etherealengine/engine/src/ecs/functions/ComponentFunctions'
import { LODComponent, LODLevel } from '@etherealengine/engine/src/scene/components/LODComponent'
import { ModelComponent } from '@etherealengine/engine/src/scene/components/ModelComponent'
import { NameComponent } from '@etherealengine/engine/src/scene/components/NameComponent'
import { State } from '@etherealengine/hyperflux'

import { serializeLOD } from '../../functions/lodsFromModel'
import { Button } from '../inputs/Button'
import InputGroup, { InputGroupContainer } from '../inputs/InputGroup'
import ModelInput from '../inputs/ModelInput'
import NumericInput from '../inputs/NumericInput'
import NumericInputGroup from '../inputs/NumericInputGroup'
import SelectInput from '../inputs/SelectInput'
import PaginatedList from '../layout/PaginatedList'

export function LODProperties({ entity }: { entity: Entity }) {
const { t } = useTranslation()
const entities = LODComponent.lodsByEntity[entity].value

const model = getComponent(entity, ModelComponent)

const onChangeLevelProperty = useCallback(
(level: State<LODLevel>, property: keyof LODLevel) => {
return (value) => {
level[property].set(value)
}
},
[entity]
)

if (!entities) return <></>
return (
<div>
<h2 className="text-white text-2xl font-bold m-4">LODs</h2>
<PaginatedList
list={entities}
element={(entity: Entity) => {
const lodComponent = useComponent(entity, LODComponent)
const nameComponent = getComponent(entity, NameComponent)
return (
<div className="bg-gray-800 rounded-lg p-4 m-4">
<h2 className="text-white text-xl font-bold mb-4">{nameComponent}</h2>
<InputGroup name="lodHeuristic" label={t('editor:properties.lod.heuristic')}>
<SelectInput
value={lodComponent.lodHeuristic.value}
onChange={(val: typeof lodComponent.lodHeuristic.value) => lodComponent.lodHeuristic.set(val)}
options={[
{ value: 'DISTANCE', label: t('editor:properties.lod.heuristic.distance') },
{ value: 'SCENE_SCALE', label: t('editor:properties.lod.heuristic.sceneScale') },
{ value: 'MANUAL', label: t('editor:properties.lod.heuristic.manual') }
]}
/>
</InputGroup>
<Button
onClick={() =>
lodComponent.levels[lodComponent.levels.length].set({
distance: 0,
src: '',
loaded: false,
model: null
})
}
>
Add LOD
</Button>
<PaginatedList
options={{ countPerPage: 1 }}
list={lodComponent.levels}
element={(level: State<LODLevel>) => {
return (
<div className="bg-gray-900 m-2">
<div style={{ margin: '2em' }}>
<InputGroup name="distance" label={t('editor:properties.lod.distance')}>
<NumericInput
value={level.distance.value}
onChange={onChangeLevelProperty(level, 'distance')}
/>
</InputGroup>
<InputGroup name="src" label={t('editor:properties.lod.src')}>
<ModelInput value={level.src.value} onChange={onChangeLevelProperty(level, 'src')} />
</InputGroup>
</div>
<div className="flex justify-end">
<Button
onClick={() => {
const index = lodComponent.levels.indexOf(level)
lodComponent.levels.set(lodComponent.levels.value.filter((_, i) => i !== index))
}}
>
Remove
</Button>
</div>
<div className="flex justify-end">
<Button
onClick={() => {
serializeLOD(model.src, entity, level)
}}
>
Serialize
</Button>
</div>
</div>
)
}}
/>
</div>
)
}}
/>
</div>
)
}
109 changes: 47 additions & 62 deletions packages/editor/src/components/properties/ModelNodeEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,34 @@
import React, { useState } from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { Object3D } from 'three'

import { StaticResourceService } from '@etherealengine/client-core/src/media/services/StaticResourceService'
import {
AudioFileTypes,
VideoFileTypes,
VolumetricFileTypes
} from '@etherealengine/engine/src/assets/constants/fileTypes'
import { AnimationManager } from '@etherealengine/engine/src/avatar/AnimationManager'
import { LoopAnimationComponent } from '@etherealengine/engine/src/avatar/components/LoopAnimationComponent'
import { Engine } from '@etherealengine/engine/src/ecs/classes/Engine'
import { useEngineState } from '@etherealengine/engine/src/ecs/classes/EngineState'
import { SceneState } from '@etherealengine/engine/src/ecs/classes/Scene'
import {
addComponent,
getComponent,
getMutableComponent,
getOptionalComponent,
hasComponent,
removeComponent,
useComponent
} from '@etherealengine/engine/src/ecs/functions/ComponentFunctions'
import { traverseEntityNode } from '@etherealengine/engine/src/ecs/functions/EntityTree'
import { EquippableComponent } from '@etherealengine/engine/src/interaction/components/EquippableComponent'
import { ErrorComponent, getEntityErrors } from '@etherealengine/engine/src/scene/components/ErrorComponent'
import { ImageComponent } from '@etherealengine/engine/src/scene/components/ImageComponent'
import { MediaComponent } from '@etherealengine/engine/src/scene/components/MediaComponent'
import { getEntityErrors } from '@etherealengine/engine/src/scene/components/ErrorComponent'
import { LODComponent } from '@etherealengine/engine/src/scene/components/LODComponent'
import { ModelComponent } from '@etherealengine/engine/src/scene/components/ModelComponent'
import { NameComponent } from '@etherealengine/engine/src/scene/components/NameComponent'
import { UUIDComponent } from '@etherealengine/engine/src/scene/components/UUIDComponent'
import { addError, clearErrors } from '@etherealengine/engine/src/scene/functions/ErrorFunctions'
import { getState } from '@etherealengine/hyperflux'
import { useState } from '@etherealengine/hyperflux'

import ViewInArIcon from '@mui/icons-material/ViewInAr'

import exportGLTF from '../../functions/exportGLTF'
import { createLODsFromModel } from '../../functions/lodsFromModel'
import BooleanInput from '../inputs/BooleanInput'
import { PropertiesPanelButton } from '../inputs/Button'
import { Button, PropertiesPanelButton } from '../inputs/Button'
import InputGroup from '../inputs/InputGroup'
import ModelInput from '../inputs/ModelInput'
import SelectInput from '../inputs/SelectInput'
import Well from '../layout/Well'
import { LODProperties } from './LODProperties'
import ModelTransformProperties from './ModelTransformProperties'
import NodeEditor from './NodeEditor'
import ScreenshareTargetNodeEditor from './ScreenshareTargetNodeEditor'
Expand All @@ -55,68 +42,57 @@ import { EditorComponentType, updateProperty } from './Util'
*/
export const ModelNodeEditor: EditorComponentType = (props) => {
const { t } = useTranslation()
const [isEquippable, setEquippable] = useState(hasComponent(props.entity, EquippableComponent))
const isEquippable = useState(hasComponent(props.entity, EquippableComponent))

const entity = props.entity
const modelComponent = useComponent(entity, ModelComponent)
const [exporting, setExporting] = useState(false)
const [exportPath, setExportPath] = useState(modelComponent?.src.value)
const exporting = useState(false)
const exportPath = useState(modelComponent?.src.value)

if (!modelComponent) return <></>
const errors = getEntityErrors(props.entity, ModelComponent)
const obj3d = modelComponent.value.scene

const loopAnimationComponent = getOptionalComponent(entity, LoopAnimationComponent)

const textureOverrideEntities = [] as { label: string; value: string }[]
traverseEntityNode(getState(SceneState).sceneEntity, (node) => {
if (entity === entity) return

textureOverrideEntities.push({
label: getComponent(entity, NameComponent) ?? getComponent(entity, UUIDComponent),
value: getComponent(entity, UUIDComponent)
})
})

const onChangeEquippable = () => {
if (isEquippable) {
const onChangeEquippable = useCallback(() => {
if (isEquippable.value) {
removeComponent(props.entity, EquippableComponent)
setEquippable(false)
isEquippable.set(false)
} else {
addComponent(props.entity, EquippableComponent, true)
setEquippable(true)
isEquippable.set(true)
}
}

const animations = loopAnimationComponent?.hasAvatarAnimations
? AnimationManager.instance._animations
: obj3d?.animations ?? []

const animationOptions = [{ label: 'None', value: -1 }]
if (animations?.length) animations.forEach((clip, i) => animationOptions.push({ label: clip.name, value: i }))
}, [entity])

const animationOptions = useState(() => {
const obj3d = modelComponent.value.scene
const animations = loopAnimationComponent?.hasAvatarAnimations
? AnimationManager.instance._animations
: obj3d?.animations ?? []
return [{ label: 'None', value: -1 }, ...animations.map((clip, index) => ({ label: clip.name, value: index }))]
})

const onExportModel = async () => {
if (exporting) {
const onExportModel = useCallback(() => {
if (exporting.value) {
console.warn('already exporting')
return
}
setExporting(true)
await exportGLTF(entity, exportPath)
setExporting(false)
}
exporting.set(true)
exportGLTF(entity, exportPath.value).then(() => exporting.set(false))
}, [])

const updateResources = async (path: string) => {
let model
const updateResources = useCallback((path: string) => {
clearErrors(entity, ModelComponent)
try {
model = await StaticResourceService.uploadModel(path)
StaticResourceService.uploadModel(path).then((model) => {
updateProperty(ModelComponent, 'resource')(model)
})
} catch (err) {
console.log('Error getting path', path)
addError(entity, ModelComponent, 'INVALID_URL', path)
return {}
}
updateProperty(ModelComponent, 'resource')(model)
}
}, [])

return (
<NodeEditor
Expand Down Expand Up @@ -152,12 +128,12 @@ export const ModelNodeEditor: EditorComponentType = (props) => {
/>
</InputGroup>
<InputGroup name="Is Equippable" label={t('editor:properties.model.lbl-isEquippable')}>
<BooleanInput value={isEquippable} onChange={onChangeEquippable} />
<BooleanInput value={isEquippable.value} onChange={onChangeEquippable} />
</InputGroup>
<InputGroup name="Loop Animation" label={t('editor:properties.model.lbl-loopAnimation')}>
<SelectInput
key={props.entity}
options={animationOptions}
options={animationOptions.value}
value={loopAnimationComponent?.activeClipIndex}
onChange={updateProperty(LoopAnimationComponent, 'activeClipIndex')}
/>
Expand All @@ -170,14 +146,23 @@ export const ModelNodeEditor: EditorComponentType = (props) => {
</InputGroup>
<ScreenshareTargetNodeEditor entity={props.entity} multiEdit={props.multiEdit} />
<ShadowProperties entity={props.entity} />
<div className="bg-gradient-to-b from-blue-gray-400 to-cool-gray-800 rounded-lg shadow-lg">
<div className="px-4 py-2 border-b border-gray-300">
<h2 className="text-lg font-semibold text-gray-100">LODs</h2>
</div>
<div className="p-4">
<Button onClick={createLODsFromModel.bind({}, entity)}>{t('editor:properties.model.generate-lods')}</Button>
</div>
</div>
{LODComponent.lodsByEntity[props.entity].value && <LODProperties entity={entity} />}
<ModelTransformProperties modelState={modelComponent} onChangeModel={(val) => modelComponent.src.set(val)} />
{!exporting && modelComponent.src.value && (
{!exporting.value && modelComponent.src.value && (
<Well>
<ModelInput value={exportPath} onChange={setExportPath} />
<ModelInput value={exportPath.value} onChange={exportPath.set} />
<PropertiesPanelButton onClick={onExportModel}>Save Changes</PropertiesPanelButton>
</Well>
)}
{exporting && <p>Exporting...</p>}
{exporting.value && <p>Exporting...</p>}
</NodeEditor>
)
}
Expand Down
Loading