diff --git a/packages/frontend/assets/room/objects/tapestry/tapestry.blend b/packages/frontend/assets/room/objects/tapestry/tapestry.blend new file mode 100644 index 0000000000..07e65dfda1 Binary files /dev/null and b/packages/frontend/assets/room/objects/tapestry/tapestry.blend differ diff --git a/packages/frontend/assets/room/objects/tapestry/tapestry.glb b/packages/frontend/assets/room/objects/tapestry/tapestry.glb new file mode 100644 index 0000000000..5746b60053 Binary files /dev/null and b/packages/frontend/assets/room/objects/tapestry/tapestry.glb differ diff --git a/packages/frontend/src/utility/room/object-defs.ts b/packages/frontend/src/utility/room/object-defs.ts index 5e62675e47..b3b342450e 100644 --- a/packages/frontend/src/utility/room/object-defs.ts +++ b/packages/frontend/src/utility/room/object-defs.ts @@ -53,6 +53,7 @@ import { speaker } from './objects/speaker.js'; import { steelRack } from './objects/steelRack.js'; import { tabletopCalendar } from './objects/tabletopCalendar.js'; import { tabletopDigitalClock } from './objects/tabletopDigitalClock.js'; +import { tapestry } from './objects/tapestry.js'; import { tv } from './objects/tv.js'; import { wallClock } from './objects/wallClock.js'; import { wallShelf } from './objects/wallShelf.js'; @@ -109,6 +110,7 @@ export const OBJECT_DEFS = [ steelRack, tabletopCalendar, tabletopDigitalClock, + tapestry, tv, wallClock, wallShelf, diff --git a/packages/frontend/src/utility/room/objects/tapestry.ts b/packages/frontend/src/utility/room/objects/tapestry.ts new file mode 100644 index 0000000000..ef67109bab --- /dev/null +++ b/packages/frontend/src/utility/room/objects/tapestry.ts @@ -0,0 +1,195 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as BABYLON from '@babylonjs/core'; +import { defineObject } from '../engine.js'; +import { getPlaneUvIndexes } from '../utility.js'; + +export const tapestry = defineObject({ + id: 'tapestry', + name: 'Tapestry', + 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 uvs = pictureMesh.getVerticesData(BABYLON.VertexBuffer.UVKind)!; + const uvIndexes = getPlaneUvIndexes(pictureMesh); + + const ax = uvs[uvIndexes[0]]; + const ay = uvs[uvIndexes[0] + 1]; + const bx = uvs[uvIndexes[1]]; + const by = uvs[uvIndexes[1] + 1]; + const cx = uvs[uvIndexes[2]]; + const cy = uvs[uvIndexes[2] + 1]; + const dx = uvs[uvIndexes[3]]; + const dy = uvs[uvIndexes[3] + 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') { + const ratio = targetAspect / srcAspect; + + let uRange: number; + let vRange: number; + + if (ratio < 1) { + uRange = ratio; // < 1 + vRange = 1; + } else { + uRange = 1; + vRange = 1 / ratio; // < 1 + } + + const uMin = (1 - uRange) / 2; + const uMax = uMin + uRange; + const vMin = (1 - vRange) / 2; + const vMax = vMin + vRange; + + newAx = uMin; + newBx = uMax; + newCx = uMin; + newDx = uMax; + + newAy = 1 - vMax; + newBy = 1 - vMax; + newCy = 1 - vMin; + newDy = 1 - vMin; + } else if (options.fit === 'contain') { + const ratio = targetAspect / srcAspect; + + let uRange: number; + let vRange: number; + + if (ratio > 1) { + uRange = ratio; // > 1 + vRange = 1; + } else { + uRange = 1; + vRange = 1 / ratio; // > 1 + } + + const uMin = (1 - uRange) / 2; + const uMax = uMin + uRange; + const vMin = (1 - vRange) / 2; + const vMax = vMin + vRange; + + newAx = uMin; + newBx = uMax; + newCx = uMin; + newDx = uMax; + + newAy = 1 - vMax; + newBy = 1 - vMax; + newCy = 1 - vMin; + newDy = 1 - vMin; + } + + uvs[uvIndexes[0]] = newAx; + uvs[uvIndexes[0] + 1] = newAy; + uvs[uvIndexes[1]] = newBx; + uvs[uvIndexes[1] + 1] = newBy; + uvs[uvIndexes[2]] = newCx; + uvs[uvIndexes[2] + 1] = newCy; + uvs[uvIndexes[3]] = newDx; + uvs[uvIndexes[3] + 1] = newDy; + + pictureMesh.updateVerticesData(BABYLON.VertexBuffer.UVKind, uvs); + }; + + applyFit(); + + 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 === 'customPicture') { + applyCustomPicture(); + } + if (k === 'fit') { + applyFit(); + } + }, + interactions: {}, + }; + }, +});