mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-06-25 17:10:43 +00:00
wip
This commit is contained in:
parent
9a91170839
commit
5998b01ffc
15 changed files with 461 additions and 38 deletions
|
|
@ -5,6 +5,8 @@
|
|||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { cm, WORLD_SCALE } from 'misskey-world/src/utility.js';
|
||||
import { AccessoryContainer } from './avatars/AccessoryContainer.js';
|
||||
import { getAccessoryDef } from './avatars/accessory-defs.js';
|
||||
import type { WorldAvatar } from 'misskey-world/src/types.js';
|
||||
|
||||
export type PlayerProfile = {
|
||||
|
|
@ -51,6 +53,7 @@ export class PlayerContainer {
|
|||
private scene: BABYLON.Scene;
|
||||
public registerMeshes: (meshes: BABYLON.Mesh[]) => void = () => {};
|
||||
private animationObserver: BABYLON.Observer<BABYLON.Scene> | null = null;
|
||||
private accessoryContainers: AccessoryContainer[] = [];
|
||||
|
||||
constructor(params: { id: string; profile: PlayerProfile; state: PlayerState | null; sr: BABYLON.SnapshotRenderingHelper; scene: BABYLON.Scene; }) {
|
||||
this.id = params.id;
|
||||
|
|
@ -86,7 +89,7 @@ export class PlayerContainer {
|
|||
|
||||
modelRootMesh.dispose();
|
||||
|
||||
const avatarTex = new BABYLON.Texture(this.profile.avatarUrl, this.scene, false, false);
|
||||
//const avatarTex = new BABYLON.Texture(this.profile.avatarUrl, this.scene, false, false);
|
||||
|
||||
let eyesTex: BABYLON.Texture | null = null;
|
||||
if (this.profile.worldAvatar.eyes.type in DEFAULT_FACE_PARTS_EYES) {
|
||||
|
|
@ -107,17 +110,17 @@ export class PlayerContainer {
|
|||
}
|
||||
|
||||
for (const mesh of this.modelRoot.getChildMeshes()) {
|
||||
if (mesh.name.includes('__AVATAR__')) {
|
||||
const mat = new BABYLON.PBRMaterial('', this.scene);
|
||||
mat.albedoColor = new BABYLON.Color3(0.5, 0.5, 0.5);
|
||||
mat.albedoTexture = avatarTex;
|
||||
mat.emissiveColor = new BABYLON.Color3(0.5, 0.5, 0.5);
|
||||
mat.emissiveTexture = avatarTex;
|
||||
mat.roughness = 0;
|
||||
mat.metallic = 0;
|
||||
mat.backFaceCulling = false;
|
||||
mesh.material = mat;
|
||||
}
|
||||
//if (mesh.name.includes('__AVATAR__')) {
|
||||
// const mat = new BABYLON.PBRMaterial('', this.scene);
|
||||
// mat.albedoColor = new BABYLON.Color3(0.5, 0.5, 0.5);
|
||||
// mat.albedoTexture = avatarTex;
|
||||
// mat.emissiveColor = new BABYLON.Color3(0.5, 0.5, 0.5);
|
||||
// mat.emissiveTexture = avatarTex;
|
||||
// mat.roughness = 0;
|
||||
// mat.metallic = 0;
|
||||
// mat.backFaceCulling = false;
|
||||
// mesh.material = mat;
|
||||
//}
|
||||
if (mesh.name.includes('__BODY__')) {
|
||||
mesh.material.albedoColor = new BABYLON.Color3(this.profile.worldAvatar.body.color[0], this.profile.worldAvatar.body.color[1], this.profile.worldAvatar.body.color[2]);
|
||||
}
|
||||
|
|
@ -145,6 +148,21 @@ export class PlayerContainer {
|
|||
|
||||
this.registerMeshes(this.modelRoot.getChildMeshes());
|
||||
|
||||
// debug
|
||||
this.profile.worldAvatar.accessories = [{
|
||||
id: 'a',
|
||||
type: 'mug',
|
||||
options: {},
|
||||
}];
|
||||
|
||||
this.accessoryContainers = await Promise.all(this.profile.worldAvatar.accessories.map(ac => this.loadAccessory({
|
||||
type: ac.type,
|
||||
id: ac.id,
|
||||
position: new BABYLON.Vector3(0, cm(20), 0),
|
||||
rotation: new BABYLON.Vector3(0, 0, 0),
|
||||
options: ac.options,
|
||||
})));
|
||||
|
||||
const anim = new BABYLON.Animation('', 'position.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
|
||||
anim.setKeys([
|
||||
{ frame: 0, value: cm(0) },
|
||||
|
|
@ -160,6 +178,36 @@ export class PlayerContainer {
|
|||
this.scene.beginAnimation(this.modelRootContainerForAnim, 0, 120, true);
|
||||
}
|
||||
|
||||
private async loadAccessory(args: {
|
||||
type: string;
|
||||
id: string;
|
||||
position: BABYLON.Vector3;
|
||||
rotation: BABYLON.Vector3;
|
||||
options: Record<string, unknown>;
|
||||
}) {
|
||||
const def = getAccessoryDef(args.type);
|
||||
|
||||
const container = new AccessoryContainer({
|
||||
id: args.id,
|
||||
type: args.type,
|
||||
position: args.position.clone(),
|
||||
rotation: args.rotation.clone(),
|
||||
options: args.options,
|
||||
sr: this.sr,
|
||||
getIsSrReady: () => true,
|
||||
lightContainer: this.lightContainer,
|
||||
graphicsQuality: this.graphicsQuality,
|
||||
scene: this.scene,
|
||||
});
|
||||
container.registerMeshes = (meshes) => {
|
||||
this.registerMeshes(meshes);
|
||||
};
|
||||
|
||||
await container.load();
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
public applyState(state: PlayerState, forInit = false) {
|
||||
this.root.position.set(...state.position);
|
||||
if (this.modelRoot) this.modelRoot.rotation.set(...state.rotation);
|
||||
|
|
@ -173,6 +221,10 @@ export class PlayerContainer {
|
|||
if (this.animationObserver != null) {
|
||||
this.scene.onAfterAnimationsObservable.remove(this.animationObserver);
|
||||
}
|
||||
for (const ac of this.accessoryContainers) {
|
||||
ac.destroy();
|
||||
}
|
||||
this.accessoryContainers = [];
|
||||
this.root.dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { camelToKebab, WORLD_SCALE } from 'misskey-world/src/utility.js';
|
||||
import { ModelExplorer, scaleMorph, Timer } from '../utility.js';
|
||||
import { getAccessoryDef } from './accessory-defs.js';
|
||||
import type { AvatarAccessoryInstance } from './accessory.js';
|
||||
|
||||
export class AccessoryContainer {
|
||||
public id: string;
|
||||
public type: string;
|
||||
private options: Record<string, unknown>;
|
||||
public root: BABYLON.TransformNode;
|
||||
private subRoot: BABYLON.TransformNode | null = null;
|
||||
public instance: AvatarAccessoryInstance | null = null;
|
||||
public model: ModelExplorer | null = null;
|
||||
private scene: BABYLON.Scene;
|
||||
public registerMeshes: (meshes: BABYLON.Mesh[]) => void = () => {};
|
||||
private sr: BABYLON.SnapshotRenderingHelper;
|
||||
private getIsSrReady: () => boolean;
|
||||
private lightContainer: BABYLON.ClusteredLightContainer;
|
||||
private graphicsQuality: number;
|
||||
private timer: Timer = new Timer();
|
||||
|
||||
constructor(args: {
|
||||
id: string;
|
||||
type: string;
|
||||
options: Record<string, unknown>;
|
||||
position: BABYLON.Vector3;
|
||||
rotation: BABYLON.Vector3;
|
||||
sr: BABYLON.SnapshotRenderingHelper;
|
||||
getIsSrReady: () => boolean;
|
||||
lightContainer: BABYLON.ClusteredLightContainer;
|
||||
scene: BABYLON.Scene;
|
||||
graphicsQuality: number;
|
||||
}) {
|
||||
this.id = args.id;
|
||||
this.type = args.type;
|
||||
this.options = args.options;
|
||||
this.sr = args.sr;
|
||||
this.getIsSrReady = args.getIsSrReady;
|
||||
this.lightContainer = args.lightContainer;
|
||||
this.scene = args.scene;
|
||||
this.graphicsQuality = args.graphicsQuality;
|
||||
this.root = new BABYLON.TransformNode(`accessory_${args.id}_${args.type}`, this.scene);
|
||||
this.root.position = args.position;
|
||||
this.root.rotation = args.rotation;
|
||||
}
|
||||
|
||||
public async load() {
|
||||
const def = getAccessoryDef(this.type);
|
||||
|
||||
const filePath = def.path != null ? `/client-assets/world/avatar-accessories/${def.path(this.options)}.glb` : `/client-assets/world/avatar-accessories/${camelToKebab(this.type)}/${camelToKebab(this.type)}.glb`;
|
||||
const loaderResult = await BABYLON.LoadAssetContainerAsync(filePath, this.scene);
|
||||
|
||||
// babylonによって自動で追加される右手系変換用ノード
|
||||
const subRootMesh = loaderResult.meshes[0] as BABYLON.Mesh;
|
||||
|
||||
// meshじゃなくtransform nodeにしてパフォーマンス向上
|
||||
this.subRoot = new BABYLON.TransformNode('__root__', this.scene);
|
||||
this.subRoot.parent = this.root;
|
||||
this.subRoot.scaling.x = -1;
|
||||
this.subRoot.scaling = this.subRoot.scaling.scale(WORLD_SCALE);// cmをmに
|
||||
|
||||
for (const m of subRootMesh.getChildren()) {
|
||||
if (m.parent === subRootMesh) {
|
||||
m.parent = this.subRoot;
|
||||
}
|
||||
}
|
||||
|
||||
subRootMesh.dispose();
|
||||
|
||||
this.registerMeshes(this.subRoot.getChildMeshes());
|
||||
|
||||
this.model = new ModelExplorer(this.subRoot);
|
||||
|
||||
this.instance = await def.createInstance({
|
||||
scene: this.scene,
|
||||
sr: {
|
||||
updateMesh: (mesh) => {
|
||||
if (!this.getIsSrReady()) return;
|
||||
this.sr.updateMesh(mesh);
|
||||
},
|
||||
reset: () => {
|
||||
if (!this.getIsSrReady()) return;
|
||||
this.sr.disableSnapshotRendering();
|
||||
this.sr.enableSnapshotRendering();
|
||||
},
|
||||
fixParticleSystem: (ps) => this.sr.fixParticleSystem(ps),
|
||||
},
|
||||
lc: this.lightContainer,
|
||||
root: this.root,
|
||||
options: this.options,
|
||||
model: this.model!,
|
||||
timer: this.timer,
|
||||
graphicsQuality: this.graphicsQuality,
|
||||
reloadModel: () => {
|
||||
this.reload();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async reload() {
|
||||
this.timer.dispose();
|
||||
this.instance?.dispose?.();
|
||||
this.instance = null;
|
||||
this.model = null;
|
||||
this.subRoot?.dispose();
|
||||
this.root.removeChild(this.subRoot);
|
||||
this.scene.removeTransformNode(this.subRoot);
|
||||
|
||||
this.timer = new Timer();
|
||||
|
||||
await this.load();
|
||||
|
||||
this.sr.disableSnapshotRendering();
|
||||
this.sr.enableSnapshotRendering();
|
||||
}
|
||||
|
||||
public optionsUpdated(options: Record<string, unknown>, key: string, value: any) {
|
||||
if (this.instance == null) return;
|
||||
this.options[key] = options[key]; // 参照を切れさせないようにプロパティ個別にmutate
|
||||
this.sr.disableSnapshotRendering();
|
||||
this.instance.onOptionsUpdated?.([key, this.options[key]]);
|
||||
this.sr.enableSnapshotRendering();
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.sr.disableSnapshotRendering();
|
||||
this.timer.dispose();
|
||||
this.instance?.dispose?.();
|
||||
this.subRoot.dispose();
|
||||
this.root.dispose();
|
||||
this.scene.removeTransformNode(this.root);
|
||||
this.sr.enableSnapshotRendering();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { cm } from 'misskey-world/src/utility.js';
|
||||
import { mug_schema } from 'misskey-world/src/room/objects/mug.schema.js';
|
||||
import { defineAccessory } from '../accessory.js';
|
||||
import { yuge } from '../utility.js';
|
||||
|
||||
export const mug = defineAccessory(mug_schema, {
|
||||
createInstance: ({ scene, root, sr }) => {
|
||||
const yugeDispose = yuge(scene, root, new BABYLON.Vector3(0, cm(5), 0), sr);
|
||||
|
||||
return {
|
||||
dispose: () => {
|
||||
yugeDispose?.();
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { mug } from './accessories/mug.js';
|
||||
import type { AvatarAccessoryDef } from './accessory.js';
|
||||
|
||||
export const AVATAR_ACCESSORY_DEFS = [
|
||||
mug,
|
||||
] as AvatarAccessoryDef[];
|
||||
|
||||
export function getAccessoryDef(type: string): AvatarAccessoryDef {
|
||||
const def = AVATAR_ACCESSORY_DEFS.find(x => x.id === type) as AvatarAccessoryDef | undefined;
|
||||
if (def == null) {
|
||||
throw new Error(`Unrecognized accessory type: ${type}`);
|
||||
}
|
||||
return def;
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { findMaterial, ModelExplorer, type Timer } from '../utility.js';
|
||||
import type { GetAvatarAccessoryOptionsSchemaValues, AccessorySchemaDef, AvatarAccessoryOptionsSchema } from 'misskey-world/src/avatars/accessory.js';
|
||||
|
||||
export type AvatarAccessoryInstance<Options = any> = {
|
||||
onOptionsUpdated?: <K extends keyof Options, V extends Options[K]>(kv: [K, V]) => void;
|
||||
dispose: () => void;
|
||||
};
|
||||
|
||||
export type SnapshotRenderingHelperWrapper = {
|
||||
updateMesh: (meshes: BABYLON.Mesh[]) => void;
|
||||
reset: () => void;
|
||||
fixParticleSystem: (ps: BABYLON.ParticleSystem) => void;
|
||||
};
|
||||
|
||||
export type AvatarAccessoryDef<Schema extends AccessorySchemaDef = AccessorySchemaDef> = Schema & {
|
||||
path?: (options: string extends keyof Schema['options']['schema'] ? Record<string, unknown> : Readonly<GetAvatarAccessoryOptionsSchemaValues<Schema['options']['schema']>>) => string;
|
||||
createInstance: (args: {
|
||||
scene: BABYLON.Scene;
|
||||
// TODO: snapshot renderingの関心を隠蔽した方が綺麗かもしれない
|
||||
// 例えばmaterialUpdatedというメソッドを用意して内部的にresetを呼ぶなど
|
||||
sr: SnapshotRenderingHelperWrapper;
|
||||
lc: BABYLON.ClusteredLightContainer | null;
|
||||
root: BABYLON.TransformNode;
|
||||
options: string extends keyof Schema['options']['schema'] ? Record<string, unknown> : Readonly<GetAvatarAccessoryOptionsSchemaValues<Schema['options']['schema']>>;
|
||||
model: ModelExplorer;
|
||||
timer: Timer;
|
||||
graphicsQuality: number;
|
||||
reloadModel: () => void;
|
||||
}) => AvatarAccessoryInstance<string extends keyof Schema['options']['schema'] ? Record<string, unknown> : GetAvatarAccessoryOptionsSchemaValues<Schema['options']['schema']>> | Promise<RoomAccessoryInstance<Schema['options']['schema'] extends undefined ? Record<string, unknown> : GetConvertedOptionsSchemaValues<Schema['options']['schema']>>>; // TODO: createInstanceをasyncにするのではなく、別にreadyみたいなものを返させる
|
||||
};
|
||||
|
||||
export function defineAccessorySchema<const OpSc extends AvatarAccessoryOptionsSchema>(def: AccessorySchemaDef<OpSc>): AccessorySchemaDef<OpSc> {
|
||||
return def;
|
||||
}
|
||||
|
||||
export function defineAccessory<const Schema extends AccessorySchemaDef<any>>(schema: Schema, def: Pick<AccessoryDef<Schema>, 'path' | 'createInstance'>): AccessoryDef<Schema> {
|
||||
return { ...schema, ...def };
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { cm } from 'misskey-world/src/utility.js';
|
||||
import { applyMorphTargetsToMesh, getPlaneUvIndexes, GRAPHICS_QUALITY, Timer } from '../utility.js';
|
||||
import { applyMorphTargetsToMesh, findMaterial, getPlaneUvIndexes, GRAPHICS_QUALITY, Timer } from '../utility.js';
|
||||
|
||||
export function getLightRangeFactorByGraphicsQuality(quality: number) {
|
||||
if (quality >= GRAPHICS_QUALITY.HIGH) {
|
||||
|
|
@ -171,31 +171,6 @@ export function initTv(scene: BABYLON.Scene, screenMesh: BABYLON.Mesh, timer: Ti
|
|||
};
|
||||
}
|
||||
|
||||
export function findMaterial(rootMesh: BABYLON.AbstractMesh | BABYLON.TransformNode, keyword: string, allowMultiMaterial = false): BABYLON.PBRMaterial {
|
||||
for (const m of rootMesh.getChildMeshes()) {
|
||||
if (m.material == null) continue;
|
||||
if (m.material instanceof BABYLON.MultiMaterial) {
|
||||
if (allowMultiMaterial && m.material.name.includes(keyword)) {
|
||||
return m.material as BABYLON.MultiMaterial;
|
||||
}
|
||||
|
||||
if ((m.material as BABYLON.MultiMaterial).subMaterials == null) continue;
|
||||
|
||||
for (const sm of (m.material as BABYLON.MultiMaterial).subMaterials) {
|
||||
if (sm == null) continue;
|
||||
if (sm.name.includes(keyword)) {
|
||||
return sm as BABYLON.PBRMaterial;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (m.material.name.includes(keyword)) {
|
||||
return m.material as BABYLON.PBRMaterial;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(`Material with keyword "${keyword}" not found`);
|
||||
}
|
||||
|
||||
export class ModelManager {
|
||||
public root: BABYLON.TransformNode;
|
||||
public bakedCallback: ((meshes: (BABYLON.Mesh | BABYLON.AbstractMesh)[]) => void) | null = null;
|
||||
|
|
|
|||
|
|
@ -711,3 +711,61 @@ export class ArcRotateCameraManualInput implements BABYLON.ICameraInput<BABYLON.
|
|||
checkInputs() {
|
||||
}
|
||||
}
|
||||
|
||||
export function findMaterial(rootMesh: BABYLON.AbstractMesh | BABYLON.TransformNode, keyword: string, allowMultiMaterial = false): BABYLON.PBRMaterial {
|
||||
for (const m of rootMesh.getChildMeshes()) {
|
||||
if (m.material == null) continue;
|
||||
if (m.material instanceof BABYLON.MultiMaterial) {
|
||||
if (allowMultiMaterial && m.material.name.includes(keyword)) {
|
||||
return m.material as BABYLON.MultiMaterial;
|
||||
}
|
||||
|
||||
if ((m.material as BABYLON.MultiMaterial).subMaterials == null) continue;
|
||||
|
||||
for (const sm of (m.material as BABYLON.MultiMaterial).subMaterials) {
|
||||
if (sm == null) continue;
|
||||
if (sm.name.includes(keyword)) {
|
||||
return sm as BABYLON.PBRMaterial;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (m.material.name.includes(keyword)) {
|
||||
return m.material as BABYLON.PBRMaterial;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(`Material with keyword "${keyword}" not found`);
|
||||
}
|
||||
|
||||
export class ModelExplorer {
|
||||
public root: BABYLON.TransformNode;
|
||||
|
||||
constructor(root: BABYLON.TransformNode) {
|
||||
this.root = root;
|
||||
}
|
||||
|
||||
public findMesh(keyword: string) {
|
||||
const mesh = this.root.getChildMeshes().find(m => m.name.includes(keyword));
|
||||
if (mesh == null) {
|
||||
throw new Error(`Mesh with keyword "${keyword}" not found for object ${this.root.metadata?.objectType}`);
|
||||
}
|
||||
return mesh as BABYLON.Mesh;
|
||||
}
|
||||
|
||||
public findMeshes(keyword: string) {
|
||||
const meshes = this.root.getChildMeshes().filter(m => m.name.includes(keyword));
|
||||
return meshes as BABYLON.Mesh[];
|
||||
}
|
||||
|
||||
public findMaterial(keyword: string) {
|
||||
return findMaterial(this.root, keyword);
|
||||
}
|
||||
|
||||
public findTransformNode(keyword: string) {
|
||||
const node = this.root.getChildTransformNodes().find(n => n.name.includes(keyword));
|
||||
if (node == null) {
|
||||
throw new Error(`TransformNode with keyword "${keyword}" not found for object ${this.root.metadata?.objectType}`);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
BIN
packages/frontend/assets/world/avatar-accessories/mug/mug.blend
Normal file
BIN
packages/frontend/assets/world/avatar-accessories/mug/mug.blend
Normal file
Binary file not shown.
BIN
packages/frontend/assets/world/avatar-accessories/mug/mug.glb
Normal file
BIN
packages/frontend/assets/world/avatar-accessories/mug/mug.glb
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
13
packages/misskey-world/src/avatars/accessories/mug.schema.ts
Normal file
13
packages/misskey-world/src/avatars/accessories/mug.schema.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineAccessorySchema } from '../accessory.js';
|
||||
export const mug_schema = defineAccessorySchema({
|
||||
id: 'mug',
|
||||
options: {
|
||||
schema: {},
|
||||
default: {},
|
||||
},
|
||||
});
|
||||
19
packages/misskey-world/src/avatars/accessory-schema-defs.ts
Normal file
19
packages/misskey-world/src/avatars/accessory-schema-defs.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { mug_schema } from './accessories/mug.schema.js';
|
||||
import type { AccessorySchemaDef } from './accessory.js';
|
||||
|
||||
export const OBJECT_SCHEMA_DEFS = {
|
||||
mug: mug_schema,
|
||||
} as Record<string, AccessorySchemaDef<any>>;
|
||||
|
||||
export function getAccessorySchemaDef(type: string): AccessorySchemaDef {
|
||||
const def = OBJECT_SCHEMA_DEFS[type as keyof typeof OBJECT_SCHEMA_DEFS];
|
||||
if (def == null) {
|
||||
throw new Error(`Unrecognized accessory type: ${type}`);
|
||||
}
|
||||
return def;
|
||||
}
|
||||
77
packages/misskey-world/src/avatars/accessory.ts
Normal file
77
packages/misskey-world/src/avatars/accessory.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export type AvatarAccessoryInstance<Options = any> = {
|
||||
onOptionsUpdated?: <K extends keyof Options, V extends Options[K]>(kv: [K, V]) => void;
|
||||
dispose: () => void;
|
||||
};
|
||||
|
||||
export type NumberOptionSchema = {
|
||||
type: 'number';
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
};
|
||||
|
||||
export type BooleanOptionSchema = {
|
||||
type: 'boolean';
|
||||
};
|
||||
|
||||
export type StringOptionSchema = {
|
||||
type: 'string';
|
||||
};
|
||||
|
||||
export type ColorOptionSchema = {
|
||||
type: 'color';
|
||||
};
|
||||
|
||||
export type MaterialOptionSchema = {
|
||||
type: 'material';
|
||||
};
|
||||
|
||||
export type LightOptionSchema = {
|
||||
type: 'light';
|
||||
};
|
||||
|
||||
export type EnumOptionSchema = {
|
||||
type: 'enum';
|
||||
enum: {
|
||||
value: string | number;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type RangeOptionSchema = {
|
||||
type: 'range';
|
||||
min: number;
|
||||
max: number;
|
||||
step?: number;
|
||||
};
|
||||
|
||||
export type AvatarAccessoryOptionsSchema = Record<string, NumberOptionSchema | BooleanOptionSchema | StringOptionSchema | ColorOptionSchema | MaterialOptionSchema | LightOptionSchema | EnumOptionSchema | RangeOptionSchema | ImageOptionSchema | SeedOptionSchema>;
|
||||
|
||||
export type GetAvatarAccessoryOptionsSchemaValues<T extends AvatarAccessoryOptionsSchema> = {
|
||||
[K in keyof T]:
|
||||
T[K] extends NumberOptionSchema ? number :
|
||||
T[K] extends BooleanOptionSchema ? boolean :
|
||||
T[K] extends StringOptionSchema ? string :
|
||||
T[K] extends ColorOptionSchema ? [number, number, number] :
|
||||
T[K] extends MaterialOptionSchema ? { color: [number, number, number]; metallic: number; roughness: number; } :
|
||||
T[K] extends LightOptionSchema ? { color: [number, number, number]; brightness: number; } :
|
||||
T[K] extends EnumOptionSchema ? T[K]['enum'][number]['value'] :
|
||||
T[K] extends RangeOptionSchema ? number :
|
||||
never;
|
||||
};
|
||||
|
||||
export type AccessorySchemaDef<OpSc extends AvatarAccessoryOptionsSchema = AvatarAccessoryOptionsSchema> = {
|
||||
id: string;
|
||||
options: {
|
||||
schema: string extends keyof OpSc ? AvatarAccessoryOptionsSchema : OpSc;
|
||||
default: string extends keyof OpSc ? Record<string, unknown> : GetAvatarAccessoryOptionsSchemaValues<OpSc>; // 関数にした方が使用側でdeepCloneの必要がなくて綺麗かもしれない
|
||||
};
|
||||
};
|
||||
|
||||
export function defineAccessorySchema<const OpSc extends AvatarAccessoryOptionsSchema>(def: AccessorySchemaDef<OpSc>): AccessorySchemaDef<OpSc> {
|
||||
return def;
|
||||
}
|
||||
|
|
@ -19,6 +19,10 @@ export type WorldAvatar = {
|
|||
color: [number, number, number];
|
||||
};
|
||||
accessories: {
|
||||
id: string;
|
||||
type: string;
|
||||
options: Record<string, unknown>;
|
||||
position?: [number, number, number];
|
||||
rotation?: [number, number, number];
|
||||
}[];
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue