Skip to content

Commit

Permalink
Basic Materials (#812)
Browse files Browse the repository at this point in the history
* remove deprecated material functions

* wip

* It works now

* wip

* fix the dither shader compiler directives and some other stuff

* add some basic tests for material conversion

* license

* update material on needsUpdate, set basic materials on quality 0

* use merge instead of new array w/ spread

---------

Co-authored-by: Josh Field <10372036+HexaField@users.noreply.github.com>
  • Loading branch information
AidanCaruso and HexaField authored Dec 18, 2024
1 parent c43bfe4 commit d54761e
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 142 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
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/ir-engine/ir-engine/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 Infinite Reality Engine.
The Original Developer is the Initial Developer. The Initial Developer of the
Original Code is the Infinite Reality Engine team.
All portions of the code written by the Infinite Reality Engine team are Copyright © 2021-2023
Infinite Reality Engine. All Rights Reserved.
*/

import {
EntityUUID,
UUIDComponent,
UndefinedEntity,
createEngine,
createEntity,
destroyEngine,
getComponent,
setComponent
} from '@ir-engine/ecs'
import { NameComponent } from '@ir-engine/spatial/src/common/NameComponent'
import { MeshComponent } from '@ir-engine/spatial/src/renderer/components/MeshComponent'
import {
MaterialInstanceComponent,
MaterialStateComponent
} from '@ir-engine/spatial/src/renderer/materials/MaterialComponent'
import { mockSpatialEngine } from '@ir-engine/spatial/tests/util/mockSpatialEngine'
import { act, render } from '@testing-library/react'
import React from 'react'
import { Mesh, MeshLambertMaterial, MeshPhysicalMaterial } from 'three'
import { afterEach, assert, beforeEach, describe, it } from 'vitest'
import { convertMaterials } from './MaterialLibrarySystem'

describe('MaterialLibrarySystem', () => {
describe('convertMaterials', () => {
let instanceEntity = UndefinedEntity
let material = UndefinedEntity
const materialUuid = 'materialUuid' as EntityUUID
beforeEach(async () => {
createEngine()
mockSpatialEngine()
material = createEntity()
setComponent(material, UUIDComponent, materialUuid)
setComponent(material, NameComponent, 'Material')
setComponent(material, MaterialStateComponent, { material: new MeshPhysicalMaterial() })

instanceEntity = createEntity()
setComponent(instanceEntity, MaterialInstanceComponent, {
uuid: ['mockUuid1' as EntityUUID, materialUuid, 'mockUuid2' as EntityUUID]
})
setComponent(instanceEntity, MeshComponent, new Mesh())
const { rerender, unmount } = render(<></>)
await act(async () => rerender(<></>))
})

afterEach(() => {
return destroyEngine()
})

it('should convert a physical material to a basic material and update the instance', () => {
convertMaterials(material, true)
const basicUuid = ('basic-' + materialUuid) as EntityUUID
const basicMaterialEntity = UUIDComponent.getEntityByUUID(basicUuid)
assert(getComponent(basicMaterialEntity, UUIDComponent) === basicUuid)
const basicMaterialComponent = getComponent(basicMaterialEntity, MaterialStateComponent)
const basicMaterial = basicMaterialComponent.material as MeshLambertMaterial
const originalMaterial = getComponent(material, MaterialStateComponent).material as MeshPhysicalMaterial
assert(basicMaterial.reflectivity === originalMaterial.metalness)
assert(basicMaterial.envMap === originalMaterial.envMap)
assert(basicMaterial.uuid === 'basic-' + materialUuid)
assert(basicMaterial.alphaTest === originalMaterial.alphaTest)
assert(basicMaterial.side === originalMaterial.side)

assert(getComponent(instanceEntity, MaterialInstanceComponent).uuid[1] === basicUuid)
})

it('should switch the instance back to physical when disabling basic materials', async () => {
convertMaterials(material, true)

const basicUuid = ('basic-' + materialUuid) as EntityUUID
const basicMaterialEntity = UUIDComponent.getEntityByUUID(basicUuid)
assert(getComponent(basicMaterialEntity, UUIDComponent) === basicUuid)
const instanceComponent = getComponent(instanceEntity, MaterialInstanceComponent)
assert(instanceComponent.uuid[1] === basicUuid)
convertMaterials(basicMaterialEntity, false)
assert(instanceComponent.uuid[1] === materialUuid)
})
})
})
115 changes: 110 additions & 5 deletions packages/engine/src/scene/materials/systems/MaterialLibrarySystem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,38 @@ Infinite Reality Engine. All Rights Reserved.

import { useEffect } from 'react'

import { PresentationSystemGroup, UndefinedEntity } from '@ir-engine/ecs'
import {
createEntity,
Entity,
EntityUUID,
getComponent,
getMutableComponent,
PresentationSystemGroup,
QueryReactor,
removeEntity,
setComponent,
UndefinedEntity,
useComponent,
useEntityContext,
UUIDComponent
} from '@ir-engine/ecs'
import { defineSystem } from '@ir-engine/ecs/src/SystemFunctions'
import { NO_PROXY, useMutableState } from '@ir-engine/hyperflux'
import { NameComponent } from '@ir-engine/spatial/src/common/NameComponent'
import {
MaterialInstanceComponent,
MaterialPrototypeDefinition,
MaterialPrototypeDefinitions,
MaterialStateComponent
} from '@ir-engine/spatial/src/renderer/materials/MaterialComponent'
import {
createAndAssignMaterial,
createMaterialPrototype
createMaterialPrototype,
getMaterialIndices
} from '@ir-engine/spatial/src/renderer/materials/materialFunctions'
import { MeshBasicMaterial } from 'three'
import { RendererState } from '@ir-engine/spatial/src/renderer/RendererState'
import { isMobileXRHeadset } from '@ir-engine/spatial/src/xr/XRState'
import React from 'react'
import { MeshBasicMaterial, MeshLambertMaterial, MeshPhysicalMaterial } from 'three'

const reactor = () => {
useEffect(() => {
Expand All @@ -45,12 +65,97 @@ const reactor = () => {
)
const fallbackMaterial = new MeshBasicMaterial({ name: 'Fallback Material', color: 0xff69b4 })
fallbackMaterial.uuid = MaterialStateComponent.fallbackMaterial
createAndAssignMaterial(UndefinedEntity, fallbackMaterial)
const fallbackMaterialEntity = createEntity()
setComponent(fallbackMaterialEntity, MaterialStateComponent, {
material: fallbackMaterial,
instances: [UndefinedEntity]
})
setComponent(fallbackMaterialEntity, UUIDComponent, MaterialStateComponent.fallbackMaterial)
setComponent(fallbackMaterialEntity, NameComponent, 'Fallback Material')
}, [])

const rendererState = useMutableState(RendererState)
useEffect(() => {
if (rendererState.qualityLevel.value === 0) rendererState.forceBasicMaterials.set(true)
}, [rendererState.qualityLevel, rendererState.forceBasicMaterials])

return <QueryReactor Components={[MaterialStateComponent]} ChildEntityReactor={ChildMaterialReactor} />
}

const ChildMaterialReactor = () => {
const entity = useEntityContext()
const forceBasicMaterials = useMutableState(RendererState).forceBasicMaterials
const materialComponent = useComponent(entity, MaterialStateComponent)
useEffect(() => {
if (!materialComponent.material.value || !materialComponent.instances.length) return
convertMaterials(entity, forceBasicMaterials.value)
}, [
materialComponent.material,
materialComponent.material.needsUpdate,
materialComponent.instances,
forceBasicMaterials
])
return null
}

const ExpensiveMaterials = new Set(['MeshStandardMaterial', 'MeshPhysicalMaterial'])
/**@todo refactor this to use preprocessor directives instead of new cloned materials with different shaders */
export const convertMaterials = (material: Entity, forceBasicMaterials: boolean) => {
const materialComponent = getComponent(material, MaterialStateComponent)
const setMaterial = (uuid: EntityUUID, newUuid: EntityUUID) => {
for (const instance of materialComponent.instances) {
const indices = getMaterialIndices(instance, uuid)
for (const index of indices) {
const instanceComponent = getMutableComponent(instance, MaterialInstanceComponent)
const uuids = instanceComponent.uuid.get(NO_PROXY) as EntityUUID[]
uuids[index] = newUuid
instanceComponent.uuid.set(uuids)
}
}
}
const shouldMakeBasic =
(forceBasicMaterials || isMobileXRHeadset) && ExpensiveMaterials.has(materialComponent.material.type)

const uuid = getComponent(material, UUIDComponent)
const basicUuid = ('basic-' + uuid) as EntityUUID
const existingMaterialEntity = UUIDComponent.getEntityByUUID(basicUuid)
if (shouldMakeBasic) {
if (existingMaterialEntity) {
removeEntity(existingMaterialEntity)
return
}

const prevMaterial = materialComponent.material as MeshPhysicalMaterial
const onlyEmmisive = prevMaterial.emissiveMap && !prevMaterial.map
const newBasicMaterial = new MeshLambertMaterial().copy(prevMaterial)
newBasicMaterial.specularMap = prevMaterial.roughnessMap ?? prevMaterial.specularIntensityMap
if (onlyEmmisive) newBasicMaterial.emissiveMap = prevMaterial.emissiveMap
else newBasicMaterial.map = prevMaterial.map
newBasicMaterial.reflectivity = prevMaterial.metalness
newBasicMaterial.envMap = prevMaterial.envMap
newBasicMaterial.uuid = basicUuid
newBasicMaterial.alphaTest = prevMaterial.alphaTest
newBasicMaterial.side = prevMaterial.side
newBasicMaterial.plugins = undefined

const newMaterialEntity = createEntity()
setComponent(newMaterialEntity, MaterialStateComponent, {
material: newBasicMaterial,
instances: materialComponent.instances
})
setComponent(newMaterialEntity, UUIDComponent, basicUuid)
setComponent(newMaterialEntity, NameComponent, 'basic-' + getComponent(material, NameComponent))
setMaterial(uuid, basicUuid)
} else if (!forceBasicMaterials) {
const basicMaterialEntity = UUIDComponent.getEntityByUUID(uuid)
if (!basicMaterialEntity) return
const nonBasicUUID = uuid.slice(6) as EntityUUID
const materialEntity = UUIDComponent.getEntityByUUID(nonBasicUUID)
if (!materialEntity) return
setMaterial(uuid, nonBasicUUID)
}
}

export const MaterialLibrarySystem = defineSystem({
uuid: 'ee.engine.scene.MaterialLibrarySystem',
insert: { after: PresentationSystemGroup },
Expand Down
75 changes: 4 additions & 71 deletions packages/engine/src/scene/systems/SceneObjectSystem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,7 @@ Infinite Reality Engine. All Rights Reserved.
*/

import React, { useEffect } from 'react'
import {
Light,
Material,
Mesh,
MeshLambertMaterial,
MeshPhongMaterial,
MeshPhysicalMaterial,
MeshStandardMaterial,
Object3D,
SkinnedMesh,
Texture
} from 'three'
import { Light, Material, Mesh, Object3D, SkinnedMesh, Texture } from 'three'

import { useEntityContext, UUIDComponent } from '@ir-engine/ecs'
import {
Expand All @@ -47,30 +36,24 @@ import {
useOptionalComponent
} from '@ir-engine/ecs/src/ComponentFunctions'
import { ECSState } from '@ir-engine/ecs/src/ECSState'
import { Entity, EntityUUID } from '@ir-engine/ecs/src/Entity'
import { Entity } from '@ir-engine/ecs/src/Entity'
import { defineQuery, QueryReactor } from '@ir-engine/ecs/src/QueryFunctions'
import { defineSystem } from '@ir-engine/ecs/src/SystemFunctions'
import { AnimationSystemGroup } from '@ir-engine/ecs/src/SystemGroups'
import { getMutableState, getState, useHookstate, useImmediateEffect } from '@ir-engine/hyperflux'
import { getState, useHookstate, useImmediateEffect } from '@ir-engine/hyperflux'
import { CallbackComponent } from '@ir-engine/spatial/src/common/CallbackComponent'
import { ColliderComponent } from '@ir-engine/spatial/src/physics/components/ColliderComponent'
import { RigidBodyComponent } from '@ir-engine/spatial/src/physics/components/RigidBodyComponent'
import { ThreeToPhysics } from '@ir-engine/spatial/src/physics/types/PhysicsTypes'
import { GroupComponent, GroupQueryReactor } from '@ir-engine/spatial/src/renderer/components/GroupComponent'
import { MeshComponent } from '@ir-engine/spatial/src/renderer/components/MeshComponent'
import { VisibleComponent } from '@ir-engine/spatial/src/renderer/components/VisibleComponent'
import {
MaterialInstanceComponent,
MaterialStateComponent
} from '@ir-engine/spatial/src/renderer/materials/MaterialComponent'
import { createAndAssignMaterial } from '@ir-engine/spatial/src/renderer/materials/materialFunctions'
import { RendererState } from '@ir-engine/spatial/src/renderer/RendererState'
import { MaterialInstanceComponent } from '@ir-engine/spatial/src/renderer/materials/MaterialComponent'
import { ResourceManager } from '@ir-engine/spatial/src/resources/ResourceState'
import {
DistanceFromCameraComponent,
FrustumCullCameraComponent
} from '@ir-engine/spatial/src/transform/components/DistanceComponents'
import { isMobileXRHeadset } from '@ir-engine/spatial/src/xr/XRState'
import { GLTFComponent } from '../../gltf/GLTFComponent'
import { KHRUnlitExtensionComponent } from '../../gltf/MaterialDefinitionComponent'
import { UpdatableCallback, UpdatableComponent } from '../components/UpdatableComponent'
Expand Down Expand Up @@ -113,58 +96,12 @@ export const disposeObject3D = (obj: Object3D) => {
if (typeof light.dispose === 'function') light.dispose()
}

export const ExpensiveMaterials = new Set([MeshPhongMaterial, MeshStandardMaterial, MeshPhysicalMaterial])
/**@todo refactor this to use preprocessor directives instead of new cloned materials with different shaders */
export function setupObject(obj: Object3D, entity: Entity, forceBasicMaterials = false) {
const child = obj as any as Mesh<any, any>
if (child.material) {
const shouldMakeBasic =
(forceBasicMaterials || isMobileXRHeadset) && ExpensiveMaterials.has(child.material.constructor)
if (shouldMakeBasic) {
const basicUUID = `basic-${child.material.uuid}` as EntityUUID
const basicMaterialEntity = UUIDComponent.getEntityByUUID(basicUUID)
if (basicMaterialEntity) {
child.material = getComponent(basicMaterialEntity, MaterialStateComponent).material
return
}
const prevMaterial = child.material
const onlyEmmisive = prevMaterial.emissiveMap && !prevMaterial.map
const newBasicMaterial = new MeshLambertMaterial().copy(prevMaterial)
newBasicMaterial.specularMap = prevMaterial.roughnessMap ?? prevMaterial.specularIntensityMap
if (onlyEmmisive) newBasicMaterial.emissiveMap = prevMaterial.emissiveMap
else newBasicMaterial.map = prevMaterial.map
newBasicMaterial.reflectivity = prevMaterial.metalness
newBasicMaterial.envMap = prevMaterial.envMap
newBasicMaterial.uuid = basicUUID
newBasicMaterial.alphaTest = prevMaterial.alphaTest
newBasicMaterial.side = prevMaterial.side
newBasicMaterial.plugins = undefined

createAndAssignMaterial(entity, newBasicMaterial)
setComponent(entity, MaterialInstanceComponent, { uuid: [basicUUID] })
} else {
const UUID = child.material.uuid as EntityUUID
const basicMaterialEntity = UUIDComponent.getEntityByUUID(UUID)
if (!basicMaterialEntity) return

const nonBasicUUID = UUID.slice(6) as EntityUUID
const materialEntity = UUIDComponent.getEntityByUUID(nonBasicUUID)
if (!materialEntity) return

setComponent(entity, MaterialInstanceComponent, { uuid: [nonBasicUUID] })
}
}
}

const groupQuery = defineQuery([GroupComponent])
const updatableQuery = defineQuery([UpdatableComponent, CallbackComponent])

function SceneObjectReactor(props: { entity: Entity; obj: Object3D }) {
const { entity, obj } = props

const renderState = getMutableState(RendererState)
const forceBasicMaterials = useHookstate(renderState.forceBasicMaterials)

useImmediateEffect(() => {
setComponent(entity, DistanceFromCameraComponent)
}, [])
Expand All @@ -178,10 +115,6 @@ function SceneObjectReactor(props: { entity: Entity; obj: Object3D }) {
}
}, [])

useEffect(() => {
setupObject(obj, entity, forceBasicMaterials.value)
}, [forceBasicMaterials])

return null
}

Expand Down
2 changes: 2 additions & 0 deletions packages/spatial/src/renderer/materials/MaterialComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ const MaterialInstanceSubReactor = (props: { array: boolean; uuid: EntityUUID; e
} else {
meshComponent.material.set(material)
}

materialStateComponent.instances.merge([entity])
}, [materialStateComponent?.material, !!meshComponent])

return null
Expand Down
Loading

0 comments on commit d54761e

Please sign in to comment.