diff --git a/packages/frontend/assets/room/objects/wall-glass-picture-frame/wall-glass-picture-frame.blend b/packages/frontend/assets/room/objects/wall-glass-picture-frame/wall-glass-picture-frame.blend new file mode 100644 index 0000000000..8510020100 Binary files /dev/null and b/packages/frontend/assets/room/objects/wall-glass-picture-frame/wall-glass-picture-frame.blend differ diff --git a/packages/frontend/assets/room/objects/wall-glass-picture-frame/wall-glass-picture-frame.glb b/packages/frontend/assets/room/objects/wall-glass-picture-frame/wall-glass-picture-frame.glb new file mode 100644 index 0000000000..2f23126be4 Binary files /dev/null and b/packages/frontend/assets/room/objects/wall-glass-picture-frame/wall-glass-picture-frame.glb differ diff --git a/packages/frontend/src/utility/room/object-defs.ts b/packages/frontend/src/utility/room/object-defs.ts index ed6c4d8f36..bb16bfeb75 100644 --- a/packages/frontend/src/utility/room/object-defs.ts +++ b/packages/frontend/src/utility/room/object-defs.ts @@ -81,6 +81,7 @@ import { twistedCubeObjet } from './objects/twistedCubeObjet.js'; import { usedTissue } from './objects/usedTissue.js'; import { wallCanvas } from './objects/wallCanvas.js'; import { wallClock } from './objects/wallClock.js'; +import { wallGlassPictureFrame } from './objects/wallGlassPictureFrame.js'; import { wallMirror } from './objects/wallMirror.js'; import { wallShelf } from './objects/wallShelf.js'; import { woodRingFloorLamp } from './objects/woodRingFloorLamp.js'; @@ -166,6 +167,7 @@ export const OBJECT_DEFS = [ usedTissue, wallCanvas, wallClock, + wallGlassPictureFrame, wallMirror, wallShelf, woodRingFloorLamp, diff --git a/packages/frontend/src/utility/room/objects/wallGlassPictureFrame.ts b/packages/frontend/src/utility/room/objects/wallGlassPictureFrame.ts new file mode 100644 index 0000000000..88aac4548c --- /dev/null +++ b/packages/frontend/src/utility/room/objects/wallGlassPictureFrame.ts @@ -0,0 +1,137 @@ +/* + * 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 { createPlaneUvMapper, getPlaneUvIndexes } from '../utility.js'; + +const remap = (value: number, fromMin: number, fromMax: number, toMin: number, toMax: number) => { + return toMin + ((value - fromMin) / (fromMax - fromMin)) * (toMax - toMin); +}; + +export const wallGlassPictureFrame = defineObject({ + id: 'wallGlassPictureFrame', + name: 'wallGlassPictureFrame', + 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.1, + height: 0.1, + customPicture: null, + fit: 'cover', + }, + }, + placement: 'wall', + createInstance: async ({ scene, options, model }) => { + const pictureMesh = model.findMesh('__X_PICTURE__'); + const frameMesh = model.findMesh('__X_FRAME__'); + const pinMeshes = model.findMeshes('__X_PIN__'); + const pictureMaterial = model.findMaterial('__X_PICTURE__'); + + const updateUv = createPlaneUvMapper(pictureMesh); + + 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 = remap(options.width, 0, 1, 2, 172); // 最小値(値を0にした場合)でのサイズは2cmで、最大値(値を1にした場合)でのサイズは172cmなので。比率の計算だから単位はなんでもいいけど、とにかく0が0にならない点を考慮させる必要がある + const targetHeight = remap(options.height, 0, 1, 2, 172); // 最小値(値を0にした場合)でのサイズは2cmで、最大値(値を1にした場合)でのサイズは172cmなので。比率の計算だから単位はなんでもいいけど、とにかく0が0にならない点を考慮させる必要がある + const targetAspect = targetWidth / targetHeight; + + updateUv(srcAspect, targetAspect, options.fit); + + model.updated(); + }; + + applyFit(); + + const applySize = () => { + pictureMesh.morphTargetManager!.getTargetByName('Width')!.influence = options.width; + pictureMesh.morphTargetManager!.getTargetByName('Height')!.influence = options.height; + frameMesh.morphTargetManager!.getTargetByName('Width')!.influence = options.width; + frameMesh.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; + } + model.updated(); + + applyFit(); + }; + + applySize(); + + const applyCustomPicture = () => new Promise((resolve) => { + if (options.customPicture != null) { + const tex = new BABYLON.Texture(options.customPicture, scene, false, false); + tex.wrapU = BABYLON.Texture.MIRROR_ADDRESSMODE; + tex.wrapV = BABYLON.Texture.MIRROR_ADDRESSMODE; + + pictureMaterial.unfreeze(); + pictureMaterial.albedoColor = new BABYLON.Color3(1, 1, 1); + pictureMaterial.albedoTexture = tex; + + tex.onLoadObservable.addOnce(() => { + applyFit(); + resolve(); + }); + } else { + pictureMaterial.albedoColor = new BABYLON.Color3(0.5, 0.5, 0.5); + pictureMaterial.albedoTexture = null; + resolve(); + } + }); + + await applyCustomPicture(); + + return { + onInited: () => { + + }, + onOptionsUpdated: ([k, v]) => { + switch (k) { + case 'width': + case 'height': + applySize(); + break; + case 'customPicture': + applyCustomPicture(); + break; + case 'fit': + applyFit(); + break; + } + }, + interactions: {}, + }; + }, +});