import React, { useCallback } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' import { DoubleSide, Mesh, MeshStandardMaterial } from 'three' import { API } from '@xrengine/client-core/src/API' import { FileBrowserService } from '@xrengine/client-core/src/common/services/FileBrowserService' import { ModelTransformParameters } from '@xrengine/engine/src/assets/classes/ModelTransform' import { Entity } from '@xrengine/engine/src/ecs/classes/Entity' import { ComponentType, getComponentState, hasComponent } from '@xrengine/engine/src/ecs/functions/ComponentFunctions' import { MaterialSource, SourceType } from '@xrengine/engine/src/renderer/materials/components/MaterialSource' import MeshBasicMaterial from '@xrengine/engine/src/renderer/materials/constants/material-prototypes/MeshBasicMaterial.mat' import bakeToVertices from '@xrengine/engine/src/renderer/materials/functions/bakeToVertices' import { materialsFromSource } from '@xrengine/engine/src/renderer/materials/functions/MaterialLibraryFunctions' import { ModelComponent } from '@xrengine/engine/src/scene/components/ModelComponent' import { useHookstate } from '@xrengine/hyperflux' import { State } from '@xrengine/hyperflux/functions/StateFunctions' import { ToggleButton } from '@mui/material' import exportGLTF from '../../functions/exportGLTF' import { accessSelectionState } from '../../services/SelectionServices' import BooleanInput from '../inputs/BooleanInput' import { Button } from '../inputs/Button' import InputGroup from '../inputs/InputGroup' import NumericInputGroup from '../inputs/NumericInputGroup' import ParameterInput from '../inputs/ParameterInput' import SelectInput from '../inputs/SelectInput' import StringInput from '../inputs/StringInput' import TexturePreviewInput from '../inputs/TexturePreviewInput' import CollapsibleBlock from '../layout/CollapsibleBlock' import LightmapBakerProperties from './LightmapBakerProperties' const TransformContainer = (styled as any).div` color: var(--textColor); text-align: -webkit-center; margin-top: 2em; margin-bottom: 4em; background-color: var(--background2); overflow: scroll; ` const ElementsContainer = (styled as any).div` margin: 16px; padding: 8px; color: var(--textColor); ` const FilterToggle = styled(ToggleButton)` color: var(--textColor); ` const OptimizeButton = styled(Button)` @keyframes glowing { 0% { background-color: #f00; box-shadow: 0 0 5px #f00; } 16% { background-color: #ff0; box-shadow: 0 0 20px #ff0; } 33% { background-color: #0f0; box-shadow: 0 0 5px #0f0; } 50% { background-color: #0ff; box-shadow: 0 0 20px #0ff; } 66% { background-color: #00f; box-shadow: 0 0 5px #00f; } 83% { background-color: #f0f; box-shadow: 0 0 20px #f0f; } 100% { background-color: #f00; box-shadow: 0 0 5px #f00; } } animation: glowing 5000ms infinite; &:hover { animation: glowing 250ms infinite; } ` export default function ModelTransformProperties({ modelState, onChangeModel }: { modelState: State> onChangeModel: any }) { const { t } = useTranslation() const selectionState = accessSelectionState() const transforming = useHookstate(false) const transformHistory = useHookstate([]) const transformParms = useHookstate({ modelFormat: 'glb', dedup: true, prune: true, dracoCompression: { enabled: true, options: { method: 'sequential', encodeSpeed: 0, decodeSpeed: 0, quantizePosition: 14, quantizeNormal: 8, quantizeColor: 8, quantizeTexcoord: 12, quantizeGeneric: 16, quantizationVolume: 'mesh' } }, gltfPack: { enabled: false, options: { meshopt: true, basisU: true, instancing: false, mergeNodes: true, mergeMaterials: true } }, meshQuantization: { enabled: true, options: { quantizePosition: 14, quantizeNormal: 8, quantizeTexcoord: 8, quantizeColor: 8, quantizeWeight: 8, quantizeGeneric: 8, normalizeWeights: false } }, textureFormat: 'ktx2', maxTextureSize: 1024 }) const vertexBakeOptions = useHookstate({ map: true, emissive: true, lightMap: true, matcapPath: '' }) const doVertexBake = useCallback( (modelState: State>) => async () => { const attribs = [ ...(vertexBakeOptions.map.value ? [{ field: 'map', attribName: 'uv' }] : []), ...(vertexBakeOptions.emissive.value ? [{ field: 'emissiveMap', attribName: 'uv' }] : []), ...(vertexBakeOptions.lightMap.value ? [{ field: 'lightMap', attribName: 'uv2' }] : []) ] as { field: keyof MeshStandardMaterial; attribName: string }[] const colors: (keyof MeshStandardMaterial)[] = ['color'] const src: MaterialSource = { type: SourceType.MODEL, path: modelState.src.value } await Promise.all( materialsFromSource(src)?.map((matComponent) => bakeToVertices( matComponent.material as MeshStandardMaterial, colors, attribs, modelState.scene.value, MeshBasicMaterial.prototypeId ) ) ?? [] ) /* if ([AssetClass.Image, AssetClass.Video].includes(AssetLoader.getAssetClass(vertexBakeOptions.matcapPath.value))) { batchSetMaterialProperty(src, 'matcap', await AssetLoader.loadAsync(vertexBakeOptions.matcapPath.value)) }*/ }, [vertexBakeOptions] ) const attribToDelete = useHookstate('uv uv2') const deleteAttribute = useCallback( (modelState: State>) => () => { const toDeletes = attribToDelete.value.split(/\s+/) modelState.scene.value?.traverse((mesh: Mesh) => { if (!mesh?.isMesh) return const geometry = mesh.geometry if (!geometry?.isBufferGeometry) return toDeletes.map((toDelete) => { if (geometry.hasAttribute(toDelete)) { geometry.deleteAttribute(toDelete) } }) }) }, [attribToDelete] ) const onChangeTransformParm = useCallback( (state: State, k: keyof typeof state.value) => { return (val) => { state[k].set(val) } }, [transformParms] ) const onTransformModel = useCallback( (modelState: State>) => async () => { transforming.set(true) const modelSrc = modelState.src.value const nuPath = await API.instance.client.service('model-transform').create({ path: modelSrc, transformParameters: transformParms.value }) transformHistory.set([modelSrc, ...transformHistory.value]) const [_, directoryToRefresh, fileName] = /.*\/(projects\/.*)\/([\w\d\s\-_\.]*)$/.exec(nuPath)! await FileBrowserService.fetchFiles(directoryToRefresh) onChangeModel(nuPath) transforming.set(false) }, [transformParms] ) const onUndoTransform = useCallback(async () => { const prev = transformHistory[0] onChangeModel(prev) transformHistory.set(transformHistory.value.slice(1)) }, [transforming]) const onBakeSelected = useCallback(async () => { const selectedModelEntities: Entity[] = selectionState.selectedEntities .get() .filter((entity) => typeof entity !== 'string' && hasComponent(entity, ModelComponent)) .map((entity: Entity) => entity) for (const entity of selectedModelEntities) { console.log('at entity ' + entity) const modelComponent = getComponentState(entity, ModelComponent) console.log('processing model from src ' + modelComponent.src.value) //bake lightmaps to vertices console.log('baking vertices...') await doVertexBake(modelComponent)() console.log('baked vertices') //delete uv and uv2 attributes console.log('deleting attributes...') await deleteAttribute(modelComponent)() console.log('deleted attributes') //set materials to be double-sided modelComponent.scene.value?.traverse((mesh: Mesh) => { if (!mesh?.isMesh) return const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material] materials.map((material) => (material.side = DoubleSide)) }) //save changes to model const bakedPath = modelComponent.src.value.replace(/\.glb$/, '-baked.glb') console.log('saving baked model to ' + bakedPath + '...') await exportGLTF(entity, bakedPath) console.log('saved baked model') //perform gltf transform console.log('transforming model at ' + bakedPath + '...') const transformedPath = await API.instance.client.service('model-transform').create({ path: bakedPath, transformParameters: transformParms.value }) console.log('transformed model into ' + transformedPath) onChangeModel(transformedPath) } }, [selectionState.selectedEntities]) return ( {!transforming.value && Optimize} {transforming.value &&

Transforming...

} {transformHistory.length > 0 && }
{ vertexBakeOptions.map.set(val) }} /> { vertexBakeOptions.lightMap.set(val) }} /> { vertexBakeOptions.emissive.set(val) }} /> { vertexBakeOptions.matcapPath.set(val) }} />
) }