diff --git a/packages/frontend/assets/room/objects/wall-canvas/wall-canvas.blend b/packages/frontend/assets/room/objects/wall-canvas/wall-canvas.blend new file mode 100644 index 0000000000..92327ff5f3 Binary files /dev/null and b/packages/frontend/assets/room/objects/wall-canvas/wall-canvas.blend differ diff --git a/packages/frontend/assets/room/objects/wall-canvas/wall-canvas.glb b/packages/frontend/assets/room/objects/wall-canvas/wall-canvas.glb new file mode 100644 index 0000000000..2d5a77c8dd Binary files /dev/null and b/packages/frontend/assets/room/objects/wall-canvas/wall-canvas.glb differ diff --git a/packages/frontend/src/utility/room/object-defs.ts b/packages/frontend/src/utility/room/object-defs.ts index ade15b2564..ff0a9488f6 100644 --- a/packages/frontend/src/utility/room/object-defs.ts +++ b/packages/frontend/src/utility/room/object-defs.ts @@ -69,6 +69,7 @@ import { tabletopPictureFrame } from './objects/tabletopPictureFrame.js'; import { tapestry } from './objects/tapestry.js'; import { tetrapod } from './objects/tetrapod.js'; import { tv } from './objects/tv.js'; +import { wallCanvas } from './objects/wallCanvas.js'; import { wallClock } from './objects/wallClock.js'; import { wallShelf } from './objects/wallShelf.js'; import { woodRingFloorLamp } from './objects/woodRingFloorLamp.js'; @@ -140,6 +141,7 @@ export const OBJECT_DEFS = [ tapestry, tetrapod, tv, + wallCanvas, wallClock, wallShelf, woodRingFloorLamp, diff --git a/packages/frontend/src/utility/room/objects/wallCanvas.ts b/packages/frontend/src/utility/room/objects/wallCanvas.ts new file mode 100644 index 0000000000..6c1e9dc61d --- /dev/null +++ b/packages/frontend/src/utility/room/objects/wallCanvas.ts @@ -0,0 +1,125 @@ +/* + * 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'; + +export const wallCanvas = defineObject({ + id: 'wallCanvas', + name: 'wallCanvas', + 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: async ({ scene, options, model }) => { + const canvasMesh = model.findMesh('__X_CANVAS__'); + canvasMesh.rotationQuaternion = null; + + const canvasMaterial = model.findMaterial('__X_CANVAS__'); + + const updateUv = createPlaneUvMapper(canvasMesh); + + const applyFit = () => { + const tex = canvasMaterial.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; + + updateUv(srcAspect, targetAspect, options.fit); + + model.updated(); + }; + + applyFit(); + + const applySize = () => { + canvasMesh.morphTargetManager!.getTargetByName('Width')!.influence = options.width; + canvasMesh.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; + + canvasMaterial.unfreeze(); + canvasMaterial.albedoColor = new BABYLON.Color3(1, 1, 1); + canvasMaterial.albedoTexture = tex; + + tex.onLoadObservable.addOnce(() => { + applyFit(); + resolve(); + }); + } else { + canvasMaterial.albedoColor = new BABYLON.Color3(0.5, 0.5, 0.5); + canvasMaterial.albedoTexture = null; + resolve(); + } + }); + + await applyCustomPicture(); + + return { + onInited: () => { + + }, + onOptionsUpdated: ([k, v]) => { + switch (k) { + case 'width': + case 'height': + applySize(); + break; + case 'customPicture': + case 'fit': + applyCustomPicture(); + break; + } + }, + interactions: {}, + }; + }, +});