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

Material Library #6261

Merged
merged 11 commits into from
May 31, 2022
9 changes: 9 additions & 0 deletions packages/client-core/i18n/en/editor.json
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@
"lbl-modelurl": "Model Url",
"lbl-envmapUrl": "Environment Map Url",
"lbl-textureOverride": "Override Texture",
"lbl-materialOverride": "Material Overrides",
"error-url": "Error Loading From URL",
"lbl-loopAnimation": "Loop Animation",
"lbl-isAvatar": "Is Avatar",
Expand All @@ -249,6 +250,14 @@
"lbl-interactionText": "Interaction Text",
"lbl-interactionType": "Interaction Type"
},
"materialAssignment": {
"lbl-materialID": "Material",
"error-materialID": "Error finding Material",
"placeholder-materialID": "Select Material",
"lbl-patternTarget": "Pattern Match",
"lbl-pattern": "Pattern",
"lbl-isRegex": "Regex"
},
"interaction": {
"type": "Interaction Type",
"text": "Interaction Text",
Expand Down
18 changes: 18 additions & 0 deletions packages/editor/src/components/inputs/InputGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,24 @@ export const InputGroupVerticalContainer = (styled as any).div`
}
`

export const InputGroupVerticalContainerWide = (styled as any).div`

${(props) =>
props.disabled &&
`
pointer-events: none;
opacity: 0.3;
`}

& > label {
display: block;
width: 100%;
color: var(--textColor);
padding-bottom: 2px;
padding-top: 4px;
}
`

export const InputGroupVerticalContent = (styled as any).div`
display: flex;
flex-direction: column;
Expand Down
180 changes: 180 additions & 0 deletions packages/editor/src/components/inputs/MaterialAssignment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'

import { getComponent, removeComponent } from '@xrengine/engine/src/ecs/functions/ComponentFunctions'
import { MaterialLibrary } from '@xrengine/engine/src/renderer/materials/MaterialLibrary'
import { PatternTarget } from '@xrengine/engine/src/renderer/materials/MaterialParms'
import {
MaterialOverrideComponent,
MaterialOverrideComponentType
} from '@xrengine/engine/src/scene/components/MaterialOverrideComponent'
import { refreshMaterials } from '@xrengine/engine/src/scene/functions/loaders/MaterialOverrideFunctions'

import { Button } from './Button'
import InputGroup, { InputGroupContent, InputGroupVerticalContainerWide, InputGroupVerticalContent } from './InputGroup'
import SelectInput from './SelectInput'
import StringInput, { ControlledStringInput } from './StringInput'

const GroupContainer = (styled as any).label`
background-color: $transparent;
color: #9FA4B5;
white-space: pre-wrap;
padding: 0 8px 8px;
`

const ArrayInputGroupContent = (styled as any)(InputGroupContent)`
margin: 4px 0px;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-wrap: wrap;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
}`
/*
& > label {
max-width: 33.33333% !important;
}
& > input {
max-width: 66.66666% !important;
}
& > div {
max-width: 66.66666% !important;
}
`*/

export default function MaterialAssignment({ entity, node, modelComponent, values, onChange }) {
let [count, setCount] = useState(values.length)

let [materialIDs, setMaterialIDs] = useState<any[]>(
Object.keys(MaterialLibrary).map((k) => {
return { label: k, value: k }
})
)
const { t } = useTranslation()

function onChangeSize(text, values, onChange) {
const count = parseInt(text)
let preCount = 0
if (!values) values = []
else preCount = values.length
if (count == undefined || preCount == count) return
if (preCount > count) values.splice(count)
else
for (let i = 0; i < count - preCount; i++) {
values.push({
entity: -1,
targetEntity: node.entity,
materialID: '',
patternTarget: PatternTarget.OBJ3D,
pattern: ''
})
}
preCount = count
onChange(values)
}

function onRemoveEntry(idx, values: any[], setCount, onChange) {
return () => {
const [removing] = values.splice(idx, 1) as MaterialOverrideComponentType[]
removeComponent(removing.entity, MaterialOverrideComponent)
setCount(values.length)
onChange(values)
}
}

function onAddEntry(values: any[], setCount, onChange) {
return () => {
setCount(values.length + 1)
onChangeSize(`${values.length + 1}`, values, onChange)
}
}

function onChangeAssignment(assignment, index, values, onChange) {
values[index] = assignment
onChange(values)
}

function onRefresh(onChange) {
return async () => {
await refreshMaterials(node.entity)
onChange(modelComponent.materialOverrides)
}
}

function MaterialAssignmentEntry(index) {
const assignment = modelComponent.materialOverrides[index]
function setAssignmentProperty(prop) {
return (value) => {
assignment[prop] = value
onChangeAssignment(assignment, index, values, onChange)
}
}

return (
<div>
<span>
<InputGroup name="Material ID" label={t('editor:properties.materialAssignment.lbl-materialID')}>
<SelectInput
key={`${entity}-${index}-materialID`}
error={t('editor:properties.materialAssignment.error-materialID')}
placeholder={t('editor:properties.materialAssignment.placeholder-materialID')}
value={assignment.materialID}
onChange={setAssignmentProperty('materialID')}
options={materialIDs}
/>
</InputGroup>
<InputGroup name="Pattern Target" label={t('editor:properties.materialAssignment.lbl-patternTarget')}>
<SelectInput
key={`${entity}-${index}-patternTarget`}
value={assignment.patternTarget}
onChange={setAssignmentProperty('patternTarget')}
options={[
{ label: 'Object3D Name', value: PatternTarget.OBJ3D },
{ label: 'Mesh Name', value: PatternTarget.MESH },
{ label: 'Material Name', value: PatternTarget.MATERIAL }
]}
/>
</InputGroup>
<InputGroup name="Pattern" label={t('editor:properties.materialAssignment.lbl-pattern')}>
<StringInput value={assignment.pattern} onChange={setAssignmentProperty('pattern')} />
</InputGroup>
</span>
<span>
<Button onClick={onRemoveEntry(index, values, setCount, onChange)}>Delete</Button>
</span>
</div>
)
}

return (
<GroupContainer>
<InputGroupVerticalContainerWide>
<InputGroupVerticalContent>
<Button onClick={onRefresh(onChange)}>
<p>Refresh</p>
</Button>
<ArrayInputGroupContent>
<label> Count: </label>
<ControlledStringInput value={count} onChange={(text) => onChangeSize(text, values, onChange)} />
<Button onClick={onAddEntry(values, setCount, onChange)}>+</Button>
</ArrayInputGroupContent>
{values &&
values.map((value, idx) => {
return (
<ArrayInputGroupContent key={`${entity}-${idx}`} style={{ margin: '4px 2px' }}>
<label>{idx + 1}: </label>
{MaterialAssignmentEntry(idx)}
</ArrayInputGroupContent>
)
})}
</InputGroupVerticalContent>
</InputGroupVerticalContainerWide>
</GroupContainer>
)
}
10 changes: 10 additions & 0 deletions packages/editor/src/components/properties/ModelNodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import BooleanInput from '../inputs/BooleanInput'
import { PropertiesPanelButton } from '../inputs/Button'
import InputGroup from '../inputs/InputGroup'
import InteractableGroup from '../inputs/InteractableGroup'
import MaterialAssignment from '../inputs/MaterialAssignment'
import ModelInput from '../inputs/ModelInput'
import SelectInput from '../inputs/SelectInput'
import EnvMapEditor from './EnvMapEditor'
Expand Down Expand Up @@ -129,6 +130,15 @@ export const ModelNodeEditor: EditorComponentType = (props) => {
onChange={updateProperty(ModelComponent, 'textureOverride')}
/>
</InputGroup>
<InputGroup name="Material Override" label={t('editor:properties.model.lbl-materialOverride')}>
<MaterialAssignment
entity={entity}
node={props.node}
modelComponent={modelComponent}
values={modelComponent.materialOverrides}
onChange={updateProperty(ModelComponent, 'materialOverrides')}
/>
</InputGroup>
<InputGroup name="MatrixAutoUpdate" label={t('editor:properties.model.lbl-matrixAutoUpdate')}>
<BooleanInput
value={modelComponent.matrixAutoUpdate}
Expand Down
4 changes: 4 additions & 0 deletions packages/engine/src/initializeEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ export const initializeCoreSystems = async () => {
{
type: SystemUpdateType.UPDATE,
systemModulePromise: import('./xrui/systems/XRUISystem')
},
{
type: SystemUpdateType.FIXED_LATE,
systemModulePromise: import('./scene/systems/MaterialOverrideSystem')
}
)
}
Expand Down
23 changes: 23 additions & 0 deletions packages/engine/src/renderer/materials/MaterialLibrary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import AliasVortex from './constants/AliasVortex.mat'
import Caustics from './constants/Caustics.mat'
import Circuits from './constants/Circuits.mat'
import Cubes from './constants/Cubes.mat'
import Fireball from './constants/Fireball.mat'
import Galaxy from './constants/Galaxy.mat'
import Generators from './constants/Generators.mat'
import Noise_1 from './constants/Noise_1.mat'
import ProteanClouds from './constants/ProteanClouds.mat'
import VoronoiClouds from './constants/VoronoiClouds.mat'

export const MaterialLibrary = {
AliasVortex: AliasVortex,
Caustics: Caustics,
Circuits: Circuits,
Cubes: Cubes,
Fireball: Fireball,
Galaxy: Galaxy,
Generators: Generators,
Noise_1: Noise_1,
ProteanClouds: ProteanClouds,
VoronoiClouds: VoronoiClouds
}
83 changes: 83 additions & 0 deletions packages/engine/src/renderer/materials/MaterialParms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Material, Mesh, Object3D } from 'three'

import { Entity } from '@xrengine/engine/src/ecs/classes/Entity'
import { addComponent, getComponent, hasComponent } from '@xrengine/engine/src/ecs/functions/ComponentFunctions'
import UpdateableObject3D from '@xrengine/engine/src/scene/classes/UpdateableObject3D'
import { Object3DComponent } from '@xrengine/engine/src/scene/components/Object3DComponent'
import { UpdatableComponent } from '@xrengine/engine/src/scene/components/UpdatableComponent'

import { Engine } from '../../ecs/classes/Engine'
import { MaterialOverrideComponentType } from '../../scene/components/MaterialOverrideComponent'
import { MatRend } from '../../scene/systems/MaterialOverrideSystem'
import { MaterialLibrary } from './MaterialLibrary'

export type MaterialParms = {
material: Material
update: (delta: number) => void
}

export enum PatternTarget {
OBJ3D,
MESH,
MATERIAL
}

function checkMatch(toCheck: string, assignment: MaterialOverrideComponentType): boolean {
switch (typeof assignment.pattern) {
case 'string':
return toCheck.includes(assignment.pattern as string)
case 'object':
return (assignment.pattern as RegExp).test(toCheck)
default:
return false
}
}

export async function assignMaterial(override: MaterialOverrideComponentType): Promise<[MatRend[], MaterialParms]> {
const result: MatRend[] = []
//first retrieve material to build assignment
const matParm: MaterialParms = await MaterialLibrary[override.materialID]()
const target = getComponent(override.targetEntity, Object3DComponent)?.value
if (!target) {
console.error('Failed material override for override', override, ': target Object3D does not exist')
}
const root = getComponent(override.targetEntity, Object3DComponent).value as UpdateableObject3D
root.traverse((obj3d) => {
let isMatch = false
switch (override.patternTarget) {
case PatternTarget.OBJ3D:
isMatch = checkMatch(obj3d.name, override)
break
case PatternTarget.MESH:
if ((obj3d as Mesh)?.isMesh) {
isMatch = checkMatch(obj3d.name, override)
}
break
case PatternTarget.MATERIAL:
let mesh = obj3d as Mesh
if (mesh?.isMesh && mesh.material) {
let mats = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
isMatch = mats.find((mat) => checkMatch(mat.name, override)) !== undefined
}
break
}
if (isMatch) {
let mesh: Mesh | undefined
switch (override.patternTarget) {
case PatternTarget.OBJ3D:
mesh = obj3d.children?.find((child: Mesh) => child.isMesh) as Mesh | undefined
break
case PatternTarget.MESH:
mesh = obj3d as Mesh
break
case PatternTarget.MATERIAL:
mesh = obj3d as Mesh
break
}
if (!mesh) return
result.push({ mesh: mesh, material: mesh.material })
mesh.material = matParm.material
}
})
return [result, matParm]
}
Loading