1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-05-13 14:05:35 +02:00
This commit is contained in:
syuilo
2026-03-26 21:09:24 +09:00
parent 42c7a483a4
commit 5886260e0b
2 changed files with 232 additions and 15 deletions

View File

@@ -156,7 +156,7 @@ type ObjectDef<OpSc extends OptionsSchema = OptionsSchema> = {
mergeMeshes?: string[] | null; // multi-materialなメッシュは複数のメッシュに分割されるが、それだと不便な場合にメッシュをマージするための指定
isChair?: boolean;
createInstance: (args: {
room: RoomEngine;
room?: RoomEngine | null;
scene: BABYLON.Scene;
root: BABYLON.Mesh;
options: Readonly<GetOptionsSchemaValues<OpSc>>;
@@ -845,7 +845,6 @@ export class RoomEngine {
position: BABYLON.Vector3;
rotation: BABYLON.Vector3;
options: any;
isMainLight?: boolean;
}) {
const def = getObjectDef(args.type);
@@ -915,8 +914,8 @@ export class RoomEngine {
mesh.receiveShadows = false;
mesh.isVisible = false;
} else {
if (!args.isMainLight && def.receiveShadows !== false) mesh.receiveShadows = true;
if (!args.isMainLight && def.castShadows !== false) {
if (def.receiveShadows !== false) mesh.receiveShadows = true;
if (def.castShadows !== false) {
this.shadowGenerator1.addShadowCaster(mesh);
this.shadowGenerator2.addShadowCaster(mesh);
}
@@ -934,8 +933,6 @@ export class RoomEngine {
meshUpdated(loaderResult.meshes);
this.objectMeshs.set(args.id, root);
const objectInstance = def.createInstance({
room: this,
scene: this.scene,
@@ -943,7 +940,7 @@ export class RoomEngine {
options: args.options,
loaderResult: loaderResult,
meshUpdated: () => {
meshUpdated(this.objectMeshs.get(args.id)!.getChildMeshes() as BABYLON.Mesh[]);
meshUpdated(root.getChildMeshes());
},
findMesh: (keyword) => {
const mesh = root.getChildMeshes().find(m => m.name.includes(keyword));
@@ -968,15 +965,10 @@ export class RoomEngine {
},
});
objectInstance.onInited?.();
this.objectInstances.set(args.id, objectInstance);
if (objectInstance.onInited != null) {
objectInstance.onInited();
}
if (args.isMainLight) {
this.roomLight.position = root.position.add(new BABYLON.Vector3(0, -1/*cm*/, 0));
}
this.objectMeshs.set(args.id, root);
return { root, objectInstance };
}
@@ -1344,3 +1336,226 @@ export class RoomEngine {
this.engine.dispose();
}
}
export class RoomObjectPreviewEngine {
private canvas: HTMLCanvasElement;
private engine: BABYLON.Engine;
public scene: BABYLON.Scene;
private shadowGenerator1: BABYLON.ShadowGenerator;
private camera: BABYLON.ArcRotateCamera;
private objectMeshs: Map<string, BABYLON.Mesh> = new Map();
public objectInstance: RoomObjectInstance<any> | null = null;
private envMapIndoor: BABYLON.CubeTexture;
private roomLight: BABYLON.SpotLight;
private zGridPreviewPlane: BABYLON.Mesh;
constructor(roomState: RoomState, options: {
canvas: HTMLCanvasElement;
}) {
this.canvas = options.canvas;
registerBuiltInLoaders();
this.engine = new BABYLON.Engine(options.canvas, false, { alpha: false, antialias: false });
this.scene = new BABYLON.Scene(this.engine);
this.scene.ambientColor = new BABYLON.Color3(1.0, 0.9, 0.8);
this.envMapIndoor = BABYLON.CubeTexture.CreateFromPrefilteredData('/client-assets/room/indoor.env', this.scene);
this.envMapIndoor.boundingBoxSize = new BABYLON.Vector3(500/*cm*/, 500/*cm*/, 500/*cm*/);
this.camera = new BABYLON.ArcRotateCamera('camera', -Math.PI / 2, Math.PI / 2.5, 300/*cm*/, new BABYLON.Vector3(0, 90/*cm*/, 0), this.scene);
this.camera.attachControl(this.canvas);
this.camera.minZ = 1/*cm*/;
this.camera.maxZ = 100000/*cm*/;
this.camera.fov = 0.5;
this.camera.lowerBetaLimit = 0;
this.camera.upperBetaLimit = (Math.PI / 2) + 0.1;
this.camera.lowerRadiusLimit = 50/*cm*/;
this.camera.upperRadiusLimit = 1000/*cm*/;
this.scene.activeCamera = this.camera;
const ambientLight = new BABYLON.HemisphericLight('ambientLight', new BABYLON.Vector3(0, 1, -0.5), this.scene);
ambientLight.diffuse = new BABYLON.Color3(1.0, 1.0, 1.0);
ambientLight.intensity = 0.5;
//ambientLight.intensity = 0;
this.roomLight = new BABYLON.SpotLight('roomLight', new BABYLON.Vector3(0, 249/*cm*/, 0), new BABYLON.Vector3(0, -1, 0), 16, 8, this.scene);
this.roomLight.diffuse = new BABYLON.Color3(1.0, 0.9, 0.8);
this.roomLight.shadowMinZ = 10/*cm*/;
this.roomLight.shadowMaxZ = 300/*cm*/;
this.shadowGenerator1 = new BABYLON.ShadowGenerator(4096, this.roomLight);
this.shadowGenerator1.forceBackFacesOnly = true;
this.shadowGenerator1.bias = 0.0001;
this.shadowGenerator1.usePercentageCloserFiltering = true;
this.shadowGenerator1.filteringQuality = BABYLON.ShadowGenerator.QUALITY_HIGH;
//this.shadowGenerator1.useContactHardeningShadow = true;
const gridMaterial = new GridMaterial('grid', this.scene);
gridMaterial.lineColor = new BABYLON.Color3(0.5, 0.5, 0.5);
gridMaterial.mainColor = new BABYLON.Color3(0, 0, 0);
gridMaterial.minorUnitVisibility = 1;
gridMaterial.opacity = 0.5;
gridMaterial.gridRatio = 10/*cm*/;
this.zGridPreviewPlane = BABYLON.MeshBuilder.CreatePlane('zGridPreviewPlane', { width: 1000/*cm*/, height: 1000/*cm*/ }, this.scene);
this.zGridPreviewPlane.material = gridMaterial;
this.zGridPreviewPlane.isPickable = false;
this.zGridPreviewPlane.isVisible = false;
if (_DEV_) {
const axes = new AxesViewer(this.scene, 30);
axes.xAxis.position = new BABYLON.Vector3(0, 30, 0);
axes.yAxis.position = new BABYLON.Vector3(0, 30, 0);
axes.zAxis.position = new BABYLON.Vector3(0, 30, 0);
}
}
public async init() {
this.engine.runRenderLoop(() => {
this.scene.render();
});
}
// TODO: RoomEngineのものとほぼ同じだからいい感じに共通化
private async loadObject(args: {
type: string;
id: string;
position: BABYLON.Vector3;
rotation: BABYLON.Vector3;
options: any;
isMainLight?: boolean;
}) {
const def = getObjectDef(args.type);
// ex) hangingTShirt -> hanging-t-shirt
const camelToKebab = (s: string) => {
return s
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
.toLowerCase();
};
const root = new BABYLON.Mesh(`object_${args.id}_${args.type}`, this.scene);
const loaderResult = await BABYLON.ImportMeshAsync(`/client-assets/room/objects/${camelToKebab(args.type)}/${camelToKebab(args.type)}.glb`, this.scene);
// babylonによって自動で追加される右手系変換用ード
const subRoot = loaderResult.meshes[0];
subRoot.scaling = subRoot.scaling.scale(WORLD_SCALE);// cmをmに
if (def.mergeMeshes != null) {
for (const groupingMeshKeyword of def.mergeMeshes) {
const meshes = loaderResult.meshes.filter(m => m.name.includes(groupingMeshKeyword));
const merged = BABYLON.Mesh.MergeMeshes(meshes as BABYLON.Mesh[], true, true, undefined, false, true);
merged.name = `${groupingMeshKeyword}.grouped`;
merged.setParent(subRoot);
loaderResult.meshes.push(merged);
}
}
let hasCollisionMesh = false;
for (const mesh of loaderResult.meshes) {
if (mesh.name.includes('__COLLISION__')) {
hasCollisionMesh = true;
break;
}
}
const metadata = {
isObject: true,
objectId: args.id,
objectType: args.type,
isCollision: !hasCollisionMesh,
};
root.addChild(subRoot);
root.position = args.position.clone();
root.rotation = args.rotation.clone();
root.metadata = metadata;
const meshUpdated = (meshes: BABYLON.Mesh[]) => {
for (const m of meshes) {
const mesh = m;
// シェイプキー(morph)を考慮してbounding boxを更新するために必要
mesh.refreshBoundingInfo({ applyMorph: true });
mesh.metadata = metadata;
mesh.checkCollisions = !hasCollisionMesh;
if (mesh.name.includes('__COLLISION__')) {
mesh.receiveShadows = false;
mesh.isVisible = false;
mesh.metadata.isCollision = true;
mesh.checkCollisions = true;
} else if (mesh.name.includes('__TOP__') || mesh.name.includes('__SIDE__')) {
mesh.receiveShadows = false;
mesh.isVisible = false;
} else {
if (def.receiveShadows !== false) mesh.receiveShadows = true;
if (def.castShadows !== false) {
this.shadowGenerator1.addShadowCaster(mesh);
this.shadowGenerator2.addShadowCaster(mesh);
}
if (mesh.material) {
(mesh.material as BABYLON.PBRMaterial).reflectionTexture = this.envMapIndoor;
}
}
}
};
meshUpdated(loaderResult.meshes);
const objectInstance = def.createInstance({
room: null,
scene: this.scene,
root,
options: args.options,
loaderResult: loaderResult,
meshUpdated: () => {
meshUpdated(root.getChildMeshes());
},
findMesh: (keyword) => {
const mesh = root.getChildMeshes().find(m => m.name.includes(keyword));
if (mesh == null) {
throw new Error(`Mesh with keyword "${keyword}" not found for object ${args.type} (${args.id})`);
}
return mesh as BABYLON.Mesh;
},
findMeshes: (keyword) => {
const meshes = root.getChildMeshes().filter(m => m.name.includes(keyword));
return meshes as BABYLON.Mesh[];
},
findMaterial: (keyword) => {
return findMaterial(root, keyword);
},
findTransformNode: (keyword) => {
const node = root.getChildTransformNodes().find(n => n.name.includes(keyword));
if (node == null) {
throw new Error(`TransformNode with keyword "${keyword}" not found for object ${args.type} (${args.id})`);
}
return node;
},
});
objectInstance.onInited?.();
this.objectInstance = objectInstance;
}
public updateObjectOption(key: string, value: any) {
this.objectInstance?.onOptionsUpdated?.([key, value]);
}
public resize() {
this.engine.resize();
}
public destroy() {
this.engine.dispose();
}
}

View File

@@ -14,6 +14,8 @@ export const ceilingFanLight = defineObject({
default: {},
},
placement: 'ceiling',
receiveShadows: false,
castShadows: false,
createInstance: ({ scene, root }) => {
return {
onInited: () => {