diff --git a/packages/frontend/assets/room/objects/poster/poster.blend b/packages/frontend/assets/room/objects/poster/poster.blend new file mode 100644 index 0000000000..608d038353 Binary files /dev/null and b/packages/frontend/assets/room/objects/poster/poster.blend differ diff --git a/packages/frontend/assets/room/objects/poster/poster.glb b/packages/frontend/assets/room/objects/poster/poster.glb new file mode 100644 index 0000000000..9fe6b411cf Binary files /dev/null and b/packages/frontend/assets/room/objects/poster/poster.glb differ diff --git a/packages/frontend/src/utility/room/engine.ts b/packages/frontend/src/utility/room/engine.ts index 7d8355d265..f2cab8730d 100644 --- a/packages/frontend/src/utility/room/engine.ts +++ b/packages/frontend/src/utility/room/engine.ts @@ -134,6 +134,7 @@ type ObjectDef = { loaderResult: BABYLON.ISceneLoaderAsyncResult; meshUpdated: () => void; findMesh: (keyword: string) => BABYLON.Mesh; + findMeshes: (keyword: string) => BABYLON.Mesh[]; findMaterial: (keyword: string) => BABYLON.PBRMaterial; }) => RoomObjectInstance>; }; @@ -911,6 +912,10 @@ export class RoomEngine { } return mesh as BABYLON.Mesh; }, + findMeshes: (keyword) => { + const meshes = root.getChildMeshes().filter(m => m.name.includes(keyword)); + return meshes as BABYLON.Mesh[]; + }, findMaterial: (keyword) => { for (const m of root.getChildMeshes()) { if (m.material == null) continue; diff --git a/packages/frontend/src/utility/room/object-defs.ts b/packages/frontend/src/utility/room/object-defs.ts index 4e80ec1244..5e62675e47 100644 --- a/packages/frontend/src/utility/room/object-defs.ts +++ b/packages/frontend/src/utility/room/object-defs.ts @@ -43,6 +43,7 @@ import { petBottle } from './objects/petBottle.js'; import { pictureFrame } from './objects/pictureFrame.js'; import { plant } from './objects/plant.js'; import { plant2 } from './objects/plant2.js'; +import { poster } from './objects/poster.js'; import { powerStrip } from './objects/powerStrip.js'; import { rolledUpPoster } from './objects/rolledUpPoster.js'; import { roundRug } from './objects/roundRug.js'; @@ -98,6 +99,7 @@ export const OBJECT_DEFS = [ pictureFrame, plant, plant2, + poster, powerStrip, rolledUpPoster, roundRug, diff --git a/packages/frontend/src/utility/room/objects/pictureFrame.ts b/packages/frontend/src/utility/room/objects/pictureFrame.ts index 960dff148f..4208c9e348 100644 --- a/packages/frontend/src/utility/room/objects/pictureFrame.ts +++ b/packages/frontend/src/utility/room/objects/pictureFrame.ts @@ -8,7 +8,7 @@ import { defineObject } from '../engine.js'; export const pictureFrame = defineObject({ id: 'pictureFrame', - name: 'Rectangular picture frame', + name: 'Simple picture frame', options: { schema: { frameColor: { @@ -103,11 +103,10 @@ export const pictureFrame = defineObject({ const srcWidth = tex.getSize().width; const srcHeight = tex.getSize().height; + const srcAspect = srcWidth / srcHeight; const targetWidth = options.width * (1 - (options.matHThickness * MAT_THICKNESS_FACTOR)); const targetHeight = options.height * (1 - (options.matVThickness * MAT_THICKNESS_FACTOR)); - const targetAspect = targetWidth / targetHeight; - const srcAspect = srcWidth / srcHeight; let newAx = ax; let newAy = ay; diff --git a/packages/frontend/src/utility/room/objects/poster.ts b/packages/frontend/src/utility/room/objects/poster.ts new file mode 100644 index 0000000000..bdd761d2c6 --- /dev/null +++ b/packages/frontend/src/utility/room/objects/poster.ts @@ -0,0 +1,189 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as BABYLON from '@babylonjs/core'; +import { defineObject } from '../engine.js'; + +export const poster = defineObject({ + id: 'poster', + name: 'Poster', + options: { + schema: { + width: { + type: 'range', + label: 'Width', + min: 0, + max: 1, + step: 0.01, + }, + height: { + type: 'range', + label: 'Height', + min: 0, + max: 1, + step: 0.01, + }, + customPicture: { + type: 'image', + label: 'Custom picture', + }, + fit: { + type: 'enum', + label: 'Custom picture fit', + enum: ['cover', 'contain', 'stretch'], + }, + }, + default: { + width: 0.15, + height: 0.15, + customPicture: null, + fit: 'cover', + }, + }, + placement: 'side', + createInstance: ({ room, root, options, findMaterial, findMesh, findMeshes, meshUpdated }) => { + const pictureMesh = findMesh('__X_PICTURE__'); + pictureMesh.rotationQuaternion = null; + pictureMesh.markVerticesDataAsUpdatable(BABYLON.VertexBuffer.UVKind, true); + + const pictureMaterial = findMaterial('__X_PICTURE__'); + + const pinMeshes = findMeshes('__X_PIN__'); + + const uvs = pictureMesh.getVerticesData(BABYLON.VertexBuffer.UVKind); + const ax = uvs[6]; + const ay = uvs[7]; + const bx = uvs[2]; + const by = uvs[3]; + const cx = uvs[4]; + const cy = uvs[5]; + const dx = uvs[0]; + const dy = uvs[1]; + + const applyFit = () => { + const tex = pictureMaterial.albedoTexture; + if (tex == null) return; + + const srcWidth = tex.getSize().width; + const srcHeight = tex.getSize().height; + const srcAspect = srcWidth / srcHeight; + const targetWidth = options.width; + const targetHeight = options.height; + const targetAspect = targetWidth / targetHeight; + + let newAx = ax; + let newAy = ay; + let newBx = bx; + let newBy = by; + let newCx = cx; + let newCy = cy; + let newDx = dx; + let newDy = dy; + + if (options.fit === 'cover') { + if (targetAspect > srcAspect) { + const fitHeight = targetWidth / srcAspect; + const crop = (fitHeight - targetHeight) / fitHeight / 2; + newAy = ay + crop * (by - ay); + newBy = by - crop * (by - ay); + newCy = cy + crop * (dy - cy); + newDy = dy - crop * (dy - cy); + } else { + const fitWidth = targetHeight * srcAspect; + const crop = (fitWidth - targetWidth) / fitWidth / 2; + newAx = ax + crop * (bx - ax); + newBx = bx - crop * (bx - ax); + newCx = cx + crop * (dx - cx); + newDx = dx - crop * (dx - cx); + } + } else if (options.fit === 'contain') { + if (targetAspect > srcAspect) { + const fitWidth = targetHeight * srcAspect; + const crop = (fitWidth - targetWidth) / fitWidth / 2; + newAx = ax + crop * (bx - ax); + newBx = bx - crop * (bx - ax); + newCx = cx + crop * (dx - cx); + newDx = dx - crop * (dx - cx); + } else { + const fitHeight = targetWidth / srcAspect; + const crop = (fitHeight - targetHeight) / fitHeight / 2; + newAy = ay + crop * (by - ay); + newBy = by - crop * (by - ay); + newCy = cy + crop * (dy - cy); + newDy = dy - crop * (dy - cy); + } + } else if (options.fit === 'stretch') { + // do nothing + } + + uvs[6] = newAx; + uvs[7] = newAy; + uvs[2] = newBx; + uvs[3] = newBy; + uvs[4] = newCx; + uvs[5] = newCy; + uvs[0] = newDx; + uvs[1] = newDy; + + pictureMesh.updateVerticesData(BABYLON.VertexBuffer.UVKind, uvs); + }; + + applyFit(); + + const applySize = () => { + pictureMesh.morphTargetManager!.getTargetByName('Width')!.influence = options.width; + pictureMesh.morphTargetManager!.getTargetByName('Height')!.influence = options.height; + for (const pinMesh of pinMeshes) { + pinMesh.morphTargetManager!.getTargetByName('Width')!.influence = options.width; + pinMesh.morphTargetManager!.getTargetByName('Height')!.influence = options.height; + } + meshUpdated(); + + applyFit(); + }; + + applySize(); + + const applyCustomPicture = () => { + if (options.customPicture != null) { + const tex = new BABYLON.Texture(options.customPicture, room.scene, false, false); + tex.wrapU = BABYLON.Texture.MIRROR_ADDRESSMODE; + tex.wrapV = BABYLON.Texture.MIRROR_ADDRESSMODE; + + pictureMaterial.albedoColor = new BABYLON.Color3(1, 1, 1); + pictureMaterial.albedoTexture = tex; + + applyFit(); + + tex.onLoadObservable.addOnce(() => { + applyFit(); + }); + } else { + pictureMaterial.albedoColor = new BABYLON.Color3(0.5, 0.5, 0.5); + pictureMaterial.albedoTexture = null; + } + }; + + applyCustomPicture(); + + return { + onInited: () => { + + }, + onOptionsUpdated: ([k, v]) => { + if (k === 'width' || k === 'height') { + applySize(); + } + if (k === 'customPicture') { + applyCustomPicture(); + } + if (k === 'fit') { + applyFit(); + } + }, + interactions: {}, + }; + }, +});