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

add save path and drag material on object #9517

Merged
merged 10 commits into from
Jan 10, 2024
4 changes: 3 additions & 1 deletion packages/common/src/utils/CommonKnownContentTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ 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,
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,
Expand All @@ -29,6 +29,7 @@ Ethereal Engine. All Rights Reserved.
* @type {Object}
*/
export const CommonKnownContentTypes = {
material: 'model/material',
xre: 'prefab/xre',
gltf: 'model/gltf',
glb: 'model/gltf-binary',
Expand All @@ -52,6 +53,7 @@ export const CommonKnownContentTypes = {

export const MimeTypeToExtension = {
'prefab/xre': 'xre',
'model/material': 'material',
'model/gltf': 'gltf',
'model/gltf-binary': 'glb',
'model/vrm': 'vrm',
Expand Down
4 changes: 2 additions & 2 deletions packages/common/src/utils/guessContentType.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ import { CommonKnownContentTypes } from './CommonKnownContentTypes'
export function guessContentType(url: string): string {
const contentPath = new URL(url).pathname
//check for xre gltf extension
if (/\.xre\.gltf$/.test(contentPath)) {
return CommonKnownContentTypes.xre
if (/\.material\.gltf$/.test(contentPath)) {
return CommonKnownContentTypes.material
}
const extension = contentPath.split('.').pop()!
return CommonKnownContentTypes[extension]
Expand Down
50 changes: 43 additions & 7 deletions packages/common/src/utils/miscUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,17 +84,21 @@ export function pathJoin(...parts: string[]): string {
}
// If it's the last part, we only want to remove leading slashes
else if (index === parts.length - 1) {
while (part.startsWith(separator)) {
part = part.substring(1)
if (part) {
while (part.startsWith(separator)) {
part = part.substring(1)
}
}
}
// For all other parts, remove leading and trailing slashes
else {
while (part.startsWith(separator)) {
part = part.substring(1)
}
while (part.endsWith(separator)) {
part = part.substring(0, part.length - 1)
if (part) {
while (part.startsWith(separator)) {
part = part.substring(1)
}
while (part.endsWith(separator)) {
part = part.substring(0, part.length - 1)
}
}
}

Expand All @@ -106,3 +110,35 @@ export function pathJoin(...parts: string[]): string {
export function baseName(path: string): string {
return path.split(/[\\/]/).pop()!
}

export function relativePathTo(src: string, dst: string): string {
const normalizePath = (path: string) => path.split('/').filter(Boolean)

const srcSegments = normalizePath(src)
const dstSegments = normalizePath(dst)
let commonIndex = 0

// Find common path segments
while (
srcSegments[commonIndex] === dstSegments[commonIndex] &&
commonIndex < Math.min(srcSegments.length, dstSegments.length)
) {
commonIndex++
}

// Calculate the number of '../' needed
let relativePathArray: string[] = []
for (let i = commonIndex; i < srcSegments.length; i++) {
relativePathArray.push('..')
}

// Append the destination path
relativePathArray = relativePathArray.concat(dstSegments.slice(commonIndex))

// Handle the special case where src and dst are the same directory
if (relativePathArray.length === 0) {
return '.'
}

return relativePathArray.join('/')
}
1 change: 1 addition & 0 deletions packages/common/tests/utils/guessContentType.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { guessContentType } from '../../src/utils/guessContentType'

describe('guessContentType', () => {
it('guessContentType', () => {
assert(guessContentType('https://mydomain.com/myfile.mat'), 'model/material')
assert(guessContentType('https://mydomain.com/myfile.gltf'), 'model/gltf')
assert(guessContentType('https://mydomain.com/myfile.glb'), 'model/gltf-binary')
assert(guessContentType('https://mydomain.com/myfile.png'), 'image/png')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import { areEqual, FixedSizeList } from 'react-window'
import { MeshBasicMaterial } from 'three'

import exportMaterialsGLTF from '@etherealengine/engine/src/assets/functions/exportMaterialsGLTF'
import { EngineState } from '@etherealengine/engine/src/ecs/classes/EngineState'
import { SourceType } from '@etherealengine/engine/src/renderer/materials/components/MaterialSource'
import { LibraryEntryType } from '@etherealengine/engine/src/renderer/materials/constants/LibraryEntry'
import {
Expand All @@ -42,10 +41,13 @@ import { getMutableState, getState, NO_PROXY, useHookstate, useState } from '@et

import { Stack } from '@mui/material'

import { pathJoin } from '@etherealengine/common/src/utils/miscUtils'
import { uploadProjectFiles } from '../../functions/assetFunctions'
import { EditorState } from '../../services/EditorServices'
import styles from '../hierarchy/styles.module.scss'
import { Button } from '../inputs/Button'
import InputGroup from '../inputs/InputGroup'
import StringInput from '../inputs/StringInput'
import MaterialLibraryEntry, { MaterialLibraryEntryType } from './MaterialLibraryEntry'
import { MaterialSelectionState } from './MaterialLibraryState'

Expand All @@ -55,7 +57,7 @@ export default function MaterialLibraryPanel() {
const materialLibrary = useHookstate(getMutableState(MaterialLibraryState))
const MemoMatLibEntry = memo(MaterialLibraryEntry, areEqual)
const nodeChanges = useState(0)
const publicPath = getState(EngineState).publicPath
const srcPath = useState('/mat/material-test')

const createSrcs = useCallback(() => Object.values(materialLibrary.sources.get(NO_PROXY)), [materialLibrary.sources])
const srcs = useState(createSrcs())
Expand Down Expand Up @@ -161,15 +163,21 @@ export default function MaterialLibraryPanel() {
>
New
</Button>
<InputGroup name="File Path" label="File Path">
<StringInput value={srcPath.value} onChange={(e) => srcPath.set(e.target.value)} />
</InputGroup>
<Button
onClick={async () => {
const projectName = editorState.projectName.value!
const materials = selectedMaterial.value ? [materialFromId(selectedMaterial.value)] : []
const libraryName = 'material-test.gltf'
const path = `${publicPath}/projects/${projectName}/assets/${libraryName}`
let libraryName = srcPath.value
if (!libraryName.endsWith('.material.gltf')) {
libraryName += '.material.gltf'
}
const relativePath = pathJoin('assets', libraryName)
const gltf = (await exportMaterialsGLTF(materials, {
binary: false,
path
relativePath
})!) as /*ArrayBuffer*/ { [key: string]: any }

const blob = [JSON.stringify(gltf)]
Expand Down
54 changes: 49 additions & 5 deletions packages/editor/src/functions/addMediaNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ import { VideoComponent } from '@etherealengine/engine/src/scene/components/Vide
import { VolumetricComponent } from '@etherealengine/engine/src/scene/components/VolumetricComponent'

import { ComponentJsonType } from '@etherealengine/common/src/schema.type.module'
import { AssetLoaderState } from '@etherealengine/engine/src/assets/state/AssetLoaderState'
import { CameraComponent } from '@etherealengine/engine/src/camera/components/CameraComponent'
import { Engine } from '@etherealengine/engine/src/ecs/classes/Engine'
import { defineQuery, getComponent } from '@etherealengine/engine/src/ecs/functions/ComponentFunctions'
import { GroupComponent } from '@etherealengine/engine/src/scene/components/GroupComponent'
import { ObjectLayerComponents } from '@etherealengine/engine/src/scene/components/ObjectLayerComponent'
import { ObjectLayers } from '@etherealengine/engine/src/scene/constants/ObjectLayers'
import iterateObject3D from '@etherealengine/engine/src/scene/util/iterateObject3D'
import { getState } from '@etherealengine/hyperflux'
import { Material, Mesh, Raycaster, Vector2 } from 'three'
import { EditorControlFunctions } from './EditorControlFunctions'

/**
Expand All @@ -42,6 +52,7 @@ import { EditorControlFunctions } from './EditorControlFunctions'
* @param before Newly created node will be set before this node in parent's children array
* @returns Newly created media node
*/

export async function addMediaNode(
url: string,
parent?: Entity,
Expand All @@ -52,11 +63,44 @@ export async function addMediaNode(
const { hostname } = new URL(url)

if (contentType.startsWith('model/')) {
EditorControlFunctions.createObjectFromSceneElement(
[{ name: ModelComponent.jsonID, props: { src: url } }, ...extraComponentJson],
parent!,
before
)
if (contentType.startsWith('model/material')) {
// find current intersected object
const objectLayerQuery = defineQuery([ObjectLayerComponents[ObjectLayers.Scene]])
const sceneObjects = objectLayerQuery().flatMap((entity) => getComponent(entity, GroupComponent))
//const sceneObjects = Array.from(Engine.instance.objectLayerList[ObjectLayers.Scene] || [])
let mouse = new Vector2()
const camera = getComponent(Engine.instance.cameraEntity, CameraComponent)
const pointerScreenRaycaster = new Raycaster()

const mouseEvent = event as MouseEvent // Type assertion
mouse.x = (mouseEvent.clientX / window.innerWidth) * 2 - 1
mouse.y = -(mouseEvent.clientY / window.innerHeight) * 2 + 1
pointerScreenRaycaster.setFromCamera(mouse, camera) // Assuming 'camera' is your Three.js camera

pointerScreenRaycaster.setFromCamera(mouse, camera) // Assuming 'camera' is your Three.js camera

const intersect = pointerScreenRaycaster.intersectObjects(sceneObjects, true)
//change states
const intersected = pointerScreenRaycaster.intersectObjects(sceneObjects)[0]
const gltfLoader = getState(AssetLoaderState).gltfLoader
gltfLoader.load(url, (gltf) => {
const material = iterateObject3D(
gltf.scene,
(mesh: Mesh) => mesh.material as Material,
(mesh: Mesh) => mesh?.isMesh
)[0]
iterateObject3D(intersected.object, (mesh: Mesh) => {
if (!mesh?.isMesh) return
mesh.material = material
})
})
} else {
EditorControlFunctions.createObjectFromSceneElement(
[{ name: ModelComponent.jsonID, props: { src: url } }, ...extraComponentJson],
parent!,
before
)
}
} else if (contentType.startsWith('video/') || hostname.includes('twitch.tv') || hostname.includes('youtube.com')) {
EditorControlFunctions.createObjectFromSceneElement(
[
Expand Down
10 changes: 10 additions & 0 deletions packages/engine/src/assets/classes/AssetLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ const getAssetType = (assetFileName: string): AssetType => {
return AssetType.MKV
case 'm3u8':
return AssetType.M3U8
case 'material':
return AssetType.MAT
default:
return null!
}
Expand All @@ -238,6 +240,10 @@ const getAssetType = (assetFileName: string): AssetType => {
const getAssetClass = (assetFileName: string): AssetClass => {
assetFileName = assetFileName.toLowerCase()
if (/\.(gltf|glb|vrm|fbx|obj|usdz)$/.test(assetFileName)) {
if (/\.(material.gltf)$/.test(assetFileName)) {
console.log('Material asset')
return AssetClass.Material
}
return AssetClass.Model
} else if (/\.(png|jpg|jpeg|tga|ktx2|dds)$/.test(assetFileName)) {
return AssetClass.Image
Expand Down Expand Up @@ -345,6 +351,10 @@ const assetLoadCallback =
registerMaterials(asset.scene, SourceType.MODEL, url)
}
}
if (assetClass === AssetClass.Material) {
const material = asset as Material
material.userData.type = assetType
}
if ([AssetClass.Image, AssetClass.Video].includes(assetClass)) {
const texture = asset as Texture
texture.wrapS = RepeatWrapping
Expand Down
1 change: 1 addition & 0 deletions packages/engine/src/assets/enum/AssetClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Ethereal Engine. All Rights Reserved.

/** List of Asset Classes. */
export enum AssetClass {
Material = 'Material',
Asset = 'Asset',
Model = 'Model',
Image = 'Image',
Expand Down
3 changes: 2 additions & 1 deletion packages/engine/src/assets/enum/AssetType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,6 @@ export enum AssetType {
DDS = 'dds',
KTX2 = 'ktx2',
USDZ = 'usdz',
M3U8 = 'm3u8'
M3U8 = 'm3u8',
MAT = 'material'
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export interface GLTFExporterOptions {
*/
includeCustomExtensions?: boolean;

path?: string;
relativePath?: string;

resourceURI?: string;

Expand Down
5 changes: 3 additions & 2 deletions packages/engine/src/assets/exporters/gltf/GLTFExporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -1392,7 +1392,7 @@ export class GLTFWriter {
sampler: this.processSampler( map )
}

if ( mimeType === 'image/ktx2' || map.isCompressedTexture ) {
if ( this.options.embedImages && ( mimeType === 'image/ktx2' || map.isCompressedTexture ) ) {
//textureDef.source = this.processImage( map.mipmaps[0], map.format, map.flipY, mimeType )
} else {
textureDef.source = this.processImage( map.image, map.format, map.flipY, mimeType )
Expand Down Expand Up @@ -1717,7 +1717,8 @@ export class GLTFWriter {

if ( originalNormal !== undefined ) geometry.setAttribute( 'normal', originalNormal );
// Skip if no exportable attributes found
if ( Object.keys( attributes ).length === 0 ) return null;
//if ( Object.keys( attributes ).length === 0 ) return null;


// Morph targets
if ( mesh.morphTargetInfluences !== undefined && mesh.morphTargetInfluences.length > 0 ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,16 @@ export default class BasisuExporterExtension extends ExporterExtension implement
sampler: number

writeTexture(_texture: CompressedTexture, textureDef) {
//only operate on compressed textures
if (!_texture?.isCompressedTexture) return
const writer = this.writer
//if we're not embedding images and this image already has a src, just use that
if (!writer.options.embedImages && _texture.userData.src) {
textureDef.extensions[this.name] = { source: textureDef.source }
writer.extensionsUsed[this.name] = true
delete textureDef.source
return
}
_texture.colorSpace = NoColorSpace
writer.pending.push(
new Promise((resolve) => {
Expand Down
Loading
Loading