diff --git a/packages/frontend/assets/room/object-thumbs/tabletop-lcd-buttons-controller.png b/packages/frontend/assets/room/object-thumbs/tabletop-lcd-buttons-controller.png new file mode 100644 index 0000000000..dca55c710d Binary files /dev/null and b/packages/frontend/assets/room/object-thumbs/tabletop-lcd-buttons-controller.png differ diff --git a/packages/frontend/assets/room/objects/tabletop-lcd-buttons-controller/tabletop-lcd-buttons-controller.blend b/packages/frontend/assets/room/objects/tabletop-lcd-buttons-controller/tabletop-lcd-buttons-controller.blend new file mode 100644 index 0000000000..f66cc9fb5f Binary files /dev/null and b/packages/frontend/assets/room/objects/tabletop-lcd-buttons-controller/tabletop-lcd-buttons-controller.blend differ diff --git a/packages/frontend/assets/room/objects/tabletop-lcd-buttons-controller/tabletop-lcd-buttons-controller.glb b/packages/frontend/assets/room/objects/tabletop-lcd-buttons-controller/tabletop-lcd-buttons-controller.glb new file mode 100644 index 0000000000..5a30008f39 Binary files /dev/null and b/packages/frontend/assets/room/objects/tabletop-lcd-buttons-controller/tabletop-lcd-buttons-controller.glb differ diff --git a/packages/frontend/src/world/room/object-defs.ts b/packages/frontend/src/world/room/object-defs.ts index 44ee1dbed1..1a722c6a78 100644 --- a/packages/frontend/src/world/room/object-defs.ts +++ b/packages/frontend/src/world/room/object-defs.ts @@ -81,6 +81,7 @@ import { tabletopDigitalClock } from './objects/tabletopDigitalClock.js'; import { tabletopFlag } from './objects/tabletopFlag.js'; import { tabletopGlassPictureFrame } from './objects/tabletopGlassPictureFrame.js'; import { tabletopIronFrameStand } from './objects/tabletopIronFrameStand.js'; +import { tabletopLcdButtonsController } from './objects/tabletopLcdButtonsController.js'; import { tabletopPictureFrame } from './objects/tabletopPictureFrame.js'; import { tapestry } from './objects/tapestry.js'; import { tetrapod } from './objects/tetrapod.js'; @@ -176,6 +177,7 @@ export const OBJECT_DEFS = [ tabletopGlassPictureFrame, tabletopIronFrameStand, tabletopPictureFrame, + tabletopLcdButtonsController, tapestry, tetrapod, tv, diff --git a/packages/frontend/src/world/room/objects/tabletopLcdButtonsController.ts b/packages/frontend/src/world/room/objects/tabletopLcdButtonsController.ts new file mode 100644 index 0000000000..7767d6d3ac --- /dev/null +++ b/packages/frontend/src/world/room/objects/tabletopLcdButtonsController.ts @@ -0,0 +1,120 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as BABYLON from '@babylonjs/core'; +import { defineObject } from '../object.js'; +import { createPlaneUvMapper, normalizeUvToSquare } from '../../utility.js'; + +export const tabletopLcdButtonsController = defineObject({ + id: 'tabletopLcdButtonsController', + name: 'tabletopLcdButtonsController', + options: { + schema: { + bodyColor: { + type: 'color', + label: 'Body color', + }, + screenBrightness: { + type: 'range', + label: 'Screen brightness', + 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: { + bodyColor: [0.05, 0.05, 0.05], + screenBrightness: 0.5, + customPicture: null, + fit: 'cover', + }, + }, + placement: 'top', + hasCollisions: false, + hasTexture: true, + createInstance: async ({ model, options, scene }) => { + const bodyMaterial = model.findMaterial('__X_BODY__'); + const screenMesh = model.findMesh('__X_SCREEN__'); + const screenMaterial = model.findMaterial('__X_SCREEN__'); + const defaultScreenTexture = screenMaterial.emissiveTexture; + + normalizeUvToSquare(screenMesh); + const updateUv = createPlaneUvMapper(screenMesh); + + const applyBodyColor = () => { + const [r, g, b] = options.bodyColor; + bodyMaterial.albedoColor = new BABYLON.Color3(r, g, b); + }; + + applyBodyColor(); + + const applyFit = () => { + const tex = screenMaterial.emissiveTexture; + if (tex == null) return; + + const srcAspect = tex.getSize().width / tex.getSize().height; + const targetAspect = 9.5 / 5.55; + + updateUv(srcAspect, targetAspect, options.fit); + + model.updated(); + }; + + applyFit(); + + const applyCustomPicture = () => new Promise((resolve) => { + if (options.customPicture != null && options.customPicture !== '') { + const tex = new BABYLON.Texture(options.customPicture, scene, false, false); + tex.wrapU = BABYLON.Texture.MIRROR_ADDRESSMODE; + tex.wrapV = BABYLON.Texture.MIRROR_ADDRESSMODE; + tex.level = 0.5; + + screenMaterial.unfreeze(); + screenMaterial.emissiveTexture = tex; + screenMaterial.emissiveTexture.level = 2; + + tex.onLoadObservable.addOnce(() => { + applyFit(); + resolve(); + }); + } else { + screenMaterial.emissiveTexture = defaultScreenTexture; + screenMaterial.emissiveTexture.level = 2; + applyFit(); + resolve(); + } + }); + + await applyCustomPicture(); + + const applyScreenBrightness = () => { + const b = options.screenBrightness; + screenMaterial.emissiveColor = new BABYLON.Color3(b, b, b); + }; + + applyScreenBrightness(); + + return { + onOptionsUpdated: ([k, v]) => { + switch (k) { + case 'bodyColor': applyBodyColor(); break; + case 'screenBrightness': applyScreenBrightness(); break; + case 'customPicture': applyCustomPicture(); break; + case 'fit': applyFit(); break; + } + }, + interactions: {}, + }; + }, +});