Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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