diff --git a/packages/frontend/assets/room/objects/tabletop-picture-frame/tabletop-picture-frame.blend b/packages/frontend/assets/room/objects/tabletop-picture-frame/tabletop-picture-frame.blend new file mode 100644 index 0000000000..ce1d379dca Binary files /dev/null and b/packages/frontend/assets/room/objects/tabletop-picture-frame/tabletop-picture-frame.blend differ diff --git a/packages/frontend/assets/room/objects/tabletop-picture-frame/tabletop-picture-frame.glb b/packages/frontend/assets/room/objects/tabletop-picture-frame/tabletop-picture-frame.glb new file mode 100644 index 0000000000..0154a7ba06 Binary files /dev/null and b/packages/frontend/assets/room/objects/tabletop-picture-frame/tabletop-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 b3b342450e..258b2822be 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 { tabletopPictureFrame } from './objects/tabletopPictureFrame.js'; import { tapestry } from './objects/tapestry.js'; import { tv } from './objects/tv.js'; import { wallClock } from './objects/wallClock.js'; @@ -110,6 +111,7 @@ export const OBJECT_DEFS = [ steelRack, tabletopCalendar, tabletopDigitalClock, + tabletopPictureFrame, tapestry, tv, wallClock, diff --git a/packages/frontend/src/utility/room/objects/tabletopPictureFrame.ts b/packages/frontend/src/utility/room/objects/tabletopPictureFrame.ts new file mode 100644 index 0000000000..352564e14f --- /dev/null +++ b/packages/frontend/src/utility/room/objects/tabletopPictureFrame.ts @@ -0,0 +1,262 @@ +/* + * 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 } from '../utility.js'; + +// NOTE: シェイプキーのnormalのエクスポートは無効にしないとmatを大きくしたときに面のレンダリングがグリッチする + +export const tabletopPictureFrame = defineObject({ + id: 'tabletopPictureFrame', + name: 'Tabletop simple picture frame', + options: { + schema: { + frameColor: { + type: 'color', + label: 'Frame color', + }, + width: { + type: 'range', + label: 'Width', + min: 0, + max: 1, + step: 0.01, + }, + height: { + type: 'range', + label: 'Height', + min: 0, + max: 1, + step: 0.01, + }, + frameThickness: { + type: 'range', + label: 'Frame thickness', + min: 0, + max: 1, + step: 0.01, + }, + depth: { + type: 'range', + label: 'Depth', + min: 0, + max: 1, + step: 0.01, + }, + matHThickness: { + type: 'range', + label: 'Mat horizontal thickness', + min: 0, + max: 1, + step: 0.01, + }, + matVThickness: { + type: 'range', + label: 'Mat vertical thickness', + 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: { + frameColor: [0.71, 0.58, 0.39], + width: 0.07, + height: 0.07, + frameThickness: 0.1, + depth: 0, + matHThickness: 0, + matVThickness: 0, + customPicture: null, + fit: 'cover', + }, + }, + placement: 'top', + createInstance: ({ room, root, options, findMaterial, findMesh, meshUpdated }) => { + const frameMesh = findMesh('__X_FRAME__'); + frameMesh.rotationQuaternion = null; + const matMesh = findMesh('__X_MAT__'); + matMesh.rotationQuaternion = null; + const coverMesh = findMesh('__X_COVER__'); + coverMesh.rotationQuaternion = null; + const pictureMesh = findMesh('__X_PICTURE__'); + pictureMesh.rotationQuaternion = null; + + const pictureMaterial = 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 = options.width * (1 - options.matHThickness); + const targetHeight = options.height * (1 - options.matVThickness); + const targetAspect = targetWidth / targetHeight; + + updateUv(srcAspect, targetAspect, options.fit); + }; + + applyFit(); + + const applyFrameThickness = () => { + frameMesh.morphTargetManager!.getTargetByName('Thickness')!.influence = options.frameThickness; + coverMesh.morphTargetManager!.getTargetByName('FrameThickness')!.influence = options.frameThickness; + matMesh.morphTargetManager!.getTargetByName('FrameThickness')!.influence = options.frameThickness; + pictureMesh.morphTargetManager!.getTargetByName('FrameThickness')!.influence = options.frameThickness; + meshUpdated(); + }; + + applyFrameThickness(); + + const applyMatThickness = () => { + matMesh.morphTargetManager!.getTargetByName('MatH')!.influence = options.matHThickness * options.width; + matMesh.morphTargetManager!.getTargetByName('MatV')!.influence = options.matVThickness * options.height; + pictureMesh.morphTargetManager!.getTargetByName('Width')!.influence = options.width * (1 - options.matHThickness); + pictureMesh.morphTargetManager!.getTargetByName('Height')!.influence = options.height * (1 - options.matVThickness); + pictureMesh.morphTargetManager!.getTargetByName('MatH')!.influence = options.matHThickness * options.width; + pictureMesh.morphTargetManager!.getTargetByName('MatV')!.influence = options.matVThickness * options.height; + meshUpdated(); + + applyFit(); + }; + + applyMatThickness(); + + const applySize = () => { + frameMesh.morphTargetManager!.getTargetByName('Width')!.influence = options.width; + frameMesh.morphTargetManager!.getTargetByName('Height')!.influence = options.height; + matMesh.morphTargetManager!.getTargetByName('Width')!.influence = options.width; + matMesh.morphTargetManager!.getTargetByName('Height')!.influence = options.height; + coverMesh.morphTargetManager!.getTargetByName('Width')!.influence = options.width; + coverMesh.morphTargetManager!.getTargetByName('Height')!.influence = options.height; + meshUpdated(); + + applyMatThickness(); + }; + + applySize(); + + const applyDepth = () => { + frameMesh.morphTargetManager!.getTargetByName('Depth')!.influence = options.depth; + //coverMesh.morphTargetManager!.getTargetByName('Depth')!.influence = options.depth; + coverMesh.morphTargetManager!.getTargetByName('Depth')!.influence = 0; + meshUpdated(); + }; + + applyDepth(); + + 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(); + + const frameMaterial = findMaterial('__X_FRAME__'); + + const applyFrameColor = () => { + const [r, g, b] = options.frameColor; + frameMaterial.albedoColor = new BABYLON.Color3(r, g, b); + }; + + applyFrameColor(); + + return { + onInited: () => { + + }, + onOptionsUpdated: ([k, v]) => { + if (k === 'frameColor') { + applyFrameColor(); + } + if (k === 'width' || k === 'height') { + applySize(); + } + if (k === 'frameThickness') { + applyFrameThickness(); + } + if (k === 'depth') { + applyDepth(); + } + if (k === 'matHThickness' || k === 'matVThickness') { + applyMatThickness(); + } + if (k === 'customPicture') { + applyCustomPicture(); + } + if (k === 'fit') { + applyFit(); + } + }, + interactions: {}, + }; + }, +}); + +/* + +const applyDirection = () => { + if (options.direction === 'vertical') { + frameMesh.rotation.z = 0; + matMesh.rotation.z = 0; + coverMesh.rotation.z = 0; + pictureMesh.rotation.z = 0; + + uvs[6] = ax; + uvs[7] = ay; + uvs[2] = bx; + uvs[3] = by; + uvs[4] = cx; + uvs[5] = cy; + uvs[0] = dx; + uvs[1] = dy; + } else if (options.direction === 'horizontal') { + frameMesh.rotation.z = -Math.PI / 2; + matMesh.rotation.z = -Math.PI / 2; + coverMesh.rotation.z = -Math.PI / 2; + pictureMesh.rotation.z = -Math.PI / 2; + + uvs[6] = cy; + uvs[7] = cx; + uvs[2] = dy; + uvs[3] = dx; + uvs[4] = ay; + uvs[5] = ax; + uvs[0] = by; + uvs[1] = bx; + } + + pictureMesh.updateVerticesData(BABYLON.VertexBuffer.UVKind, uvs); +}; + +*/