1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-05-13 12:55:30 +02:00
This commit is contained in:
syuilo
2026-05-03 18:42:04 +09:00
parent fa0eac34c2
commit 203f29afb9
3 changed files with 353 additions and 304 deletions

View File

@@ -35,6 +35,7 @@ export class RoomController {
public isReady = ref(false);
public isSitting = ref(false);
public isEditMode = ref(false);
public isRoomLightOn = ref(true);
public grabbing = ref<{ forInstall: boolean } | null>(null);
public gridSnapping = ref({ enabled: true, scale: cm(4) });
public selected = ref<{
@@ -279,6 +280,7 @@ export class RoomController {
this.isReady.value = false;
this.isSitting.value = false;
this.isEditMode.value = false;
this.isRoomLightOn.value = true;
this.grabbing.value = null;
this.selected.value = null;
this.initializeProgress.value = 0;
@@ -372,7 +374,12 @@ export class RoomController {
}
public toggleRoomLight() {
this.call('toggleRoomLight');
if (this.isRoomLightOn.value) {
this.call('turnOffRoomLight');
} else {
this.call('turnOnRoomLight');
}
this.isRoomLightOn.value = !this.isRoomLightOn.value;
}
public resize() {

View File

@@ -124,8 +124,6 @@ export class RoomEngine extends EventEmitter {
private useGlow: boolean;
private engine: BABYLON.WebGPUEngine;
public scene: BABYLON.Scene;
private shadowGeneratorForRoomLight: BABYLON.ShadowGenerator | null = null;
private shadowGeneratorForSunLight: BABYLON.ShadowGenerator | null = null;
public camera: BABYLON.UniversalCamera;
public objectEntities: Map<string, {
rootMesh: BABYLON.Mesh;
@@ -187,7 +185,6 @@ export class RoomEngine extends EventEmitter {
}
private putParticleSystem: BABYLON.ParticleSystem;
private roomLight: BABYLON.SpotLight; // TODO: これの管理もenv側に持ってく
public lightContainer: BABYLON.ClusteredLightContainer;
private gridMaterial: GridMaterial | null = null;
private gridPlane: BABYLON.Mesh;
@@ -196,6 +193,7 @@ export class RoomEngine extends EventEmitter {
public sr: BABYLON.SnapshotRenderingHelper;
private gl: BABYLON.GlowLayer | null = null;
public timer: Timer = new Timer();
public graphicsQuality: number;
private _isEditMode = false;
get isEditMode() {
@@ -233,9 +231,9 @@ export class RoomEngine extends EventEmitter {
options: { ...getObjectDef(o.type).options.default, ...o.options },
})),
};
this.graphicsQuality = options.graphicsQuality;
this.fps = options.fps;
this.useGlow = options.graphicsQuality >= GRAPHICS_QUALITY_MEDIUM;
this.useGlow = this.graphicsQuality >= GRAPHICS_QUALITY_MEDIUM;
this.time = TIME_MAP[new Date().getHours() as keyof typeof TIME_MAP];
registerBuiltInLoaders();
@@ -279,51 +277,12 @@ export class RoomEngine extends EventEmitter {
//this.scene.activeCamera = this.camera;
this.roomLight = new BABYLON.SpotLight('roomLight', new BABYLON.Vector3(0, cm(249), 0), new BABYLON.Vector3(0, -1, 0), 16, 8, this.scene);
this.roomLight.diffuse = new BABYLON.Color3(...roomState.roomLightColor);
this.roomLight.shadowMinZ = cm(10);
this.roomLight.shadowMaxZ = cm(300);
this.roomLight.radius = cm(30);
if (options.graphicsQuality >= GRAPHICS_QUALITY_MEDIUM) {
this.shadowGeneratorForRoomLight = new BABYLON.ShadowGenerator(options.graphicsQuality <= GRAPHICS_QUALITY_MEDIUM ? 1024 : 2048, this.roomLight);
this.shadowGeneratorForRoomLight.forceBackFacesOnly = true;
this.shadowGeneratorForRoomLight.bias = 0.00001;
this.shadowGeneratorForRoomLight.normalBias = 0.005;
this.shadowGeneratorForRoomLight.usePercentageCloserFiltering = true;
this.shadowGeneratorForRoomLight.filteringQuality = BABYLON.ShadowGenerator.QUALITY_HIGH;
if (options.graphicsQuality <= GRAPHICS_QUALITY_MEDIUM) {
this.shadowGeneratorForRoomLight.getShadowMap().refreshRate = 60;
}
//this.shadowGeneratorForRoomLight.useContactHardeningShadow = true;
}
if (options.graphicsQuality >= GRAPHICS_QUALITY_MEDIUM) {
const sunLight = new BABYLON.DirectionalLight('sunLight', new BABYLON.Vector3(0.2, -1, -1), this.scene);
sunLight.position = new BABYLON.Vector3(cm(-20), cm(1000), cm(1000));
sunLight.diffuse = this.time === 0 ? new BABYLON.Color3(1.0, 0.9, 0.8) : this.time === 1 ? new BABYLON.Color3(1.0, 0.8, 0.6) : new BABYLON.Color3(0.6, 0.8, 1.0);
sunLight.intensity = this.time === 0 ? 3 : this.time === 1 ? 1 : 0.25;
sunLight.shadowMinZ = cm(1000);
sunLight.shadowMaxZ = cm(2000);
this.shadowGeneratorForSunLight = new BABYLON.ShadowGenerator(options.graphicsQuality <= GRAPHICS_QUALITY_MEDIUM ? 1024 : 2048, sunLight);
this.shadowGeneratorForSunLight.forceBackFacesOnly = true;
this.shadowGeneratorForSunLight.bias = 0.00001;
this.shadowGeneratorForSunLight.usePercentageCloserFiltering = true;
this.shadowGeneratorForSunLight.usePoissonSampling = true;
if (options.graphicsQuality <= GRAPHICS_QUALITY_MEDIUM) {
this.shadowGeneratorForSunLight.getShadowMap().refreshRate = 60;
}
}
this.lightContainer = new BABYLON.ClusteredLightContainer('clustered', [], this.scene);
this.lightContainer.maxRange = options.graphicsQuality >= GRAPHICS_QUALITY_HIGH ? cm(200) : options.graphicsQuality >= GRAPHICS_QUALITY_MEDIUM ? cm(90) : cm(30);
this.lightContainer.maxRange = this.graphicsQuality >= GRAPHICS_QUALITY_HIGH ? cm(200) : this.graphicsQuality >= GRAPHICS_QUALITY_MEDIUM ? cm(90) : cm(30);
this.lightContainer.verticalTiles = 32;
this.lightContainer.horizontalTiles = 32;
this.lightContainer.depthSlices = 32;
this.turnOnRoomLight(true);
if (this.useGlow) {
this.gl = new BABYLON.GlowLayer('glow', this.scene, {
//mainTextureFixedSize: 512,
@@ -363,13 +322,13 @@ export class RoomEngine extends EventEmitter {
this.gridPlane.isVisible = false;
this.gridPlane.setEnabled(false);
if (options.graphicsQuality >= GRAPHICS_QUALITY_MEDIUM) {
if (this.graphicsQuality >= GRAPHICS_QUALITY_MEDIUM) {
this.selectionOutlineLayer = new BABYLON.SelectionOutlineLayer('outliner', this.scene);
this.scene.setRenderingAutoClearDepthStencil(this.selectionOutlineLayer.renderingGroupId, false);
this.sr.updateMeshesForEffectLayer(this.selectionOutlineLayer);
}
if (options.graphicsQuality >= GRAPHICS_QUALITY_HIGH) {
if (this.graphicsQuality >= GRAPHICS_QUALITY_HIGH) {
const pipeline = new BABYLON.DefaultRenderingPipeline('default', true, this.scene);
if (options.antialias) {
pipeline.samples = 4;
@@ -574,238 +533,6 @@ export class RoomEngine extends EventEmitter {
this.startRenderLoop();
}
public cameraMove(vector: { x: number; y: number; }, dash: boolean) {
(this.camera.inputs.attached.manual as FreeCameraManualInput).setMoveVector(dash ? { x: vector.x * 3, y: vector.y * 3 } : vector);
}
public cameraRotate(vector: { x: number; y: number; }) {
(this.camera.inputs.attached.manual as FreeCameraManualInput).setRotationVector(vector);
}
public cameraJoystickMove(vector: { x: number; y: number; }) {
(this.camera.inputs.attached.manual as FreeCameraManualInput).setMoveVector(vector);
}
public selectObject(objectId: string | null) {
this.sr.disableSnapshotRendering(); // snapshot rendering中にbake/unbakeするとエラーになる。なおこのメソッドは参照カウント方式な点に留意
const currentSelected = this.selected;
if (currentSelected != null) {
this.selected = null;
this.clearHighlight();
currentSelected.objectEntity.model.bakeMesh();
}
if (objectId != null) {
const entity = this.objectEntities.get(objectId);
if (entity != null) {
entity.model.unbakeMesh();
//this.gizmoManager.positionGizmoEnabled = true;
//this.gizmoManager.gizmos.positionGizmo.updateGizmoRotationToMatchAttachedMesh = false;
//this.gizmoManager.attachToMesh(entity.rootMesh);
this.highlightMeshes(entity.rootMesh.getChildMeshes());
const state = this.roomState.installedObjects.find(o => o.id === objectId)!;
this.selected = {
objectId,
objectEntity: entity,
objectState: state,
objectDef: getObjectDef(state.type),
};
}
}
this.sr.enableSnapshotRendering(); // このメソッドは参照カウント方式な点に留意
}
private handleGrabbing() {
if (this.grabbingCtx == null) return;
const grabbing = this.grabbingCtx;
const placement = getObjectDef(grabbing.objectType).placement;
const dir = this.camera.getDirection(BABYLON.Axis.Z).scale(this.scene.useRightHandedSystem ? -1 : 1);
let newPos = this.camera.position.add(dir.scale(grabbing.distance)).add(grabbing.originalDiffOfPosition);
const newRotation = new BABYLON.Vector3(0, this.camera.rotation.y + grabbing.originalDiffOfRotation.y + grabbing.rotation, 0);
grabbing.ghost.position = newPos.clone();
grabbing.ghost.rotation = newRotation.clone();
let stickyOtherObject: string | null = null;
let sticky = false;
const isCollisionTarget = (m: BABYLON.AbstractMesh) => {
return m.metadata?.objectId !== grabbing.objectId &&
!m.metadata?.isGhost &&
!grabbing.descendantStickyObjectIds.includes(m.metadata?.objectId);
};
const pos = new BABYLON.Vector3(this.camera.position.x, this.camera.position.y, this.camera.position.z);
const _dir = newPos.subtract(pos).normalize();
for (let i = 0; i < 1000; i++) {
if (cm(i) > grabbing.distance) break;
const prevSticky = sticky;
// posを1cmずつnewPosの方向に動かす
pos.addInPlace(_dir.scale(cm(1)));
if (placement === 'side' || placement === 'wall') {
// 前方に向かってレイを飛ばす
const ray = new BABYLON.Ray(pos, dir, cm(1000));
const hit = placement === 'side' ? this.scene.pickWithRay(ray, (m) => isCollisionTarget(m) && (m.name.includes('__ROOM_WALL__') || m.name.includes('__SIDE__'))) : this.scene.pickWithRay(ray, (m) => isCollisionTarget(m) && (m.name.includes('__ROOM_WALL__')));
if (hit != null && hit.pickedPoint != null && hit.pickedMesh != null) {
sticky = true;
const pickedMeshNormal = hit.getNormal(true, true)!;
const targetRotationY = Math.atan2(pickedMeshNormal.x, pickedMeshNormal.z);
newRotation.y = targetRotationY;
newRotation.z = grabbing.originalDiffOfRotation.z + grabbing.rotation;
newPos = hit.pickedPoint;
stickyOtherObject = hit.pickedMesh.metadata?.objectId ?? null;
if (this.gridSnapping.enabled) {
newPos.y = Math.round(newPos.y / this.gridSnapping.scale) * this.gridSnapping.scale;
if (getYRotationDirection(targetRotationY) === '+x' || getYRotationDirection(targetRotationY) === '-x') {
newPos.z = Math.round(newPos.z / this.gridSnapping.scale) * this.gridSnapping.scale;
} else {
newPos.x = Math.round(newPos.x / this.gridSnapping.scale) * this.gridSnapping.scale;
}
this.gridPlane.rotationQuaternion = null;
this.gridPlane.rotation.x = Math.PI;
this.gridPlane.rotation.y = targetRotationY;
this.gridPlane.position = newPos.add(pickedMeshNormal.scale(cm(0.1)));
if (getYRotationDirection(targetRotationY) === '+x' || getYRotationDirection(targetRotationY) === '-x') {
this.gridPlane.position.y = 0;
this.gridPlane.position.z = 0;
} else {
this.gridPlane.position.y = 0;
this.gridPlane.position.x = 0;
}
this.gridPlane.isVisible = true;
}
}
} else if (placement === 'bottom' || placement === 'ceiling') {
// 上に向かってレイを飛ばす
const ray = new BABYLON.Ray(pos, new BABYLON.Vector3(0, 1, 0), cm(1000));
const hit = placement === 'bottom' ? this.scene.pickWithRay(ray, (m) => isCollisionTarget(m) && (m.name.includes('__ROOM_CEILING__') || m.name.includes('__ROOM_BOTTOM__') || m.name.includes('__BOTTOM__'))) : this.scene.pickWithRay(ray, (m) => isCollisionTarget(m) && (m.name.includes('__ROOM_CEILING__')));
if (hit != null && hit.pickedPoint != null && hit.pickedMesh != null) {
sticky = true;
newPos = hit.pickedPoint;
stickyOtherObject = hit.pickedMesh.metadata?.objectId ?? null;
if (this.gridSnapping.enabled) {
newPos.x = Math.round(newPos.x / this.gridSnapping.scale) * this.gridSnapping.scale;
newPos.z = Math.round(newPos.z / this.gridSnapping.scale) * this.gridSnapping.scale;
this.gridPlane.rotationQuaternion = null;
this.gridPlane.rotation.x = Math.PI * 1.5;
this.gridPlane.rotation.y = 0;
this.gridPlane.position = new BABYLON.Vector3(grabbing.mesh.position.x, grabbing.mesh.position.y - cm(0.1), grabbing.mesh.position.z);
this.gridPlane.position.x = 0;
this.gridPlane.position.z = 0;
this.gridPlane.isVisible = true;
}
}
} else { // top or floor
// 下に向かってレイを飛ばす
const ray = new BABYLON.Ray(pos, new BABYLON.Vector3(0, -1, 0), cm(1000));
const hit = placement === 'top' ? this.scene.pickWithRay(ray, (m) => isCollisionTarget(m) && (m.name.includes('__ROOM_FLOOR__') || m.name.includes('__ROOM_TOP__') || m.name.includes('__TOP__'))) : this.scene.pickWithRay(ray, (m) => isCollisionTarget(m) && (m.name.includes('__ROOM_FLOOR__')));
if (hit != null && hit.pickedPoint != null && hit.pickedMesh != null) {
sticky = true;
newPos = hit.pickedPoint;
stickyOtherObject = hit.pickedMesh.metadata?.objectId ?? null;
if (this.gridSnapping.enabled) {
newPos.x = Math.round(newPos.x / this.gridSnapping.scale) * this.gridSnapping.scale;
newPos.z = Math.round(newPos.z / this.gridSnapping.scale) * this.gridSnapping.scale;
this.gridPlane.rotationQuaternion = null;
this.gridPlane.rotation.x = Math.PI / 2;
this.gridPlane.rotation.y = 0;
this.gridPlane.position = new BABYLON.Vector3(grabbing.mesh.position.x, grabbing.mesh.position.y + cm(0.1), grabbing.mesh.position.z);
this.gridPlane.position.x = 0;
this.gridPlane.position.z = 0;
this.gridPlane.isVisible = true;
}
}
}
if (!sticky && prevSticky) {
sticky = true;
break;
}
}
if (sticky) {
if (this.gridSnapping.enabled) {
newRotation.y = Math.round(newRotation.y / (Math.PI / 8)) * (Math.PI / 8);
}
grabbing.mesh.position = newPos;
grabbing.mesh.rotation = newRotation;
}
if (!sticky) {
this.gridPlane.isVisible = false;
//for (const mesh of grabbing.ghost.getChildMeshes()) {
//if (mesh.material instanceof BABYLON.MultiMaterial) {
// for (const subMat of mesh.material.subMaterials) {
// if (subMat instanceof BABYLON.PBRMaterial) {
// subMat.emissiveColor = new BABYLON.Color3(1, 0, 0);
// }
// }
//} else {
// mesh.material.emissiveColor = new BABYLON.Color3(1, 0, 0);
//}
//}
}
//const pos = new BABYLON.Vector3(this.camera.position.x, this.camera.position.y, this.camera.position.z);
//const _dir = newPos.subtract(pos).normalize();
//for (let i = 0; i < grabbing.distance; i++) {
// // posを1cmずつnewPosの方向に動かす
// pos.addInPlace(_dir.scale(cm(1)));
// // 前方に向かってレイを飛ばして衝突チェック
// const ray = new BABYLON.Ray(this.camera.position, dir, i);
// const hit = this.scene.pickWithRay(ray, (m) => isCollisionTarget(m));
// if (hit != null && hit.pickedPoint != null && hit.pickedMesh != null) {
// //const isCollided = grabbing.mesh.intersectsMesh(hit.pickedMesh, false);
// //if (isCollided) {
// break;
// //}
// }
// grabbing.mesh.position = pos.clone();
//}
//const ray = new BABYLON.Ray(this.camera.position, this.camera.getDirection(BABYLON.Axis.Z), cm(1000));
//const hit = this.scene.pickWithRay(ray, (m) => m.name.includes('__COLLISION_WALL__'))!;
//if (hit.pickedMesh != null) {
// const grabbingBox = this.grabbing.mesh.getBoundingInfo().boundingBox;
// const grabDistanceVector = this.grabbing.mesh.position.subtract(this.camera.position);
// if (grabDistanceVector.length() > hit.distance) {
// this.grabbing.mesh.position = this.camera.position.add(dir.scale(hit.distance));
// this.grabbing.mesh.position.y = y;
// }
//}
//const displacementVector = new BABYLON.Vector3(
// this.grabbing.ghost.position.x - this.grabbing.mesh.position.x,
// 0,
// this.grabbing.ghost.position.z - this.grabbing.mesh.position.z,
//);
//this.grabbing.mesh.moveWithCollisions(displacementVector);
//this.grabbing.mesh.position.y = y;
//for (const soid of stickyObjectIds) {
// //const soMesh = this.objectMeshs.get(soid)!;
// //const offset = this.grabbing.mesh!.position.subtract(soMeshStartPosition);
// //soMesh.position = this.grabbing.mesh!.position.subtract(offset);
//}
grabbing.onMove?.({
position: newPos,
rotation: newRotation,
sticky: stickyOtherObject,
});
}
public async changeEnvType(type: RoomState['env']['type'], forInit = false) {
this.roomState.env.type = type;
@@ -829,9 +556,6 @@ export class RoomEngine extends EventEmitter {
m.isPickable = false;
m.checkCollisions = false;
m.receiveShadows = true;
this.shadowGeneratorForRoomLight?.addShadowCaster(m);
this.shadowGeneratorForSunLight?.addShadowCaster(m);
if (m.material != null) {
(m.material as BABYLON.PBRMaterial).useGLTFLightFalloff = true; // Clustered Lightingではphysical falloffを持つマテリアルはアーチファクトが発生する https://doc.babylonjs.com/features/featuresDeepDive/lights/clusteredLighting/#materials-with-a-physical-falloff-may-cause-artefacts
}
@@ -848,7 +572,7 @@ export class RoomEngine extends EventEmitter {
envManager = new MuseumEnvManager(onMeshUpdatedCallback);
}
await envManager.load(this.roomState.env.options, this.scene);
await envManager.load(this.roomState.env.options, this.scene, this);
envManager.setTime(this.time);
for (const mat of this.scene.materials) {
@@ -867,6 +591,7 @@ export class RoomEngine extends EventEmitter {
}
this.envManager = envManager;
this.turnOnRoomLight(true);
this.camera.maxZ = this.envManager.maxCameraZ;
@@ -1112,8 +837,7 @@ export class RoomEngine extends EventEmitter {
if (def.receiveShadows !== false) mesh.receiveShadows = true;
if (def.castShadows !== false) {
// TODO: メモリリークしそうだからいい感じにする
this.shadowGeneratorForRoomLight?.addShadowCaster(mesh);
this.shadowGeneratorForSunLight?.addShadowCaster(mesh);
this.envManager.addShadowCaster(mesh);
}
//if (mesh.material) (mesh.material as BABYLON.PBRMaterial).ambientColor = new BABYLON.Color3(0.2, 0.2, 0.2);
@@ -1206,6 +930,238 @@ export class RoomEngine extends EventEmitter {
return { root, objectInstance };
}
public cameraMove(vector: { x: number; y: number; }, dash: boolean) {
(this.camera.inputs.attached.manual as FreeCameraManualInput).setMoveVector(dash ? { x: vector.x * 3, y: vector.y * 3 } : vector);
}
public cameraRotate(vector: { x: number; y: number; }) {
(this.camera.inputs.attached.manual as FreeCameraManualInput).setRotationVector(vector);
}
public cameraJoystickMove(vector: { x: number; y: number; }) {
(this.camera.inputs.attached.manual as FreeCameraManualInput).setMoveVector(vector);
}
public selectObject(objectId: string | null) {
this.sr.disableSnapshotRendering(); // snapshot rendering中にbake/unbakeするとエラーになる。なおこのメソッドは参照カウント方式な点に留意
const currentSelected = this.selected;
if (currentSelected != null) {
this.selected = null;
this.clearHighlight();
currentSelected.objectEntity.model.bakeMesh();
}
if (objectId != null) {
const entity = this.objectEntities.get(objectId);
if (entity != null) {
entity.model.unbakeMesh();
//this.gizmoManager.positionGizmoEnabled = true;
//this.gizmoManager.gizmos.positionGizmo.updateGizmoRotationToMatchAttachedMesh = false;
//this.gizmoManager.attachToMesh(entity.rootMesh);
this.highlightMeshes(entity.rootMesh.getChildMeshes());
const state = this.roomState.installedObjects.find(o => o.id === objectId)!;
this.selected = {
objectId,
objectEntity: entity,
objectState: state,
objectDef: getObjectDef(state.type),
};
}
}
this.sr.enableSnapshotRendering(); // このメソッドは参照カウント方式な点に留意
}
private handleGrabbing() {
if (this.grabbingCtx == null) return;
const grabbing = this.grabbingCtx;
const placement = getObjectDef(grabbing.objectType).placement;
const dir = this.camera.getDirection(BABYLON.Axis.Z).scale(this.scene.useRightHandedSystem ? -1 : 1);
let newPos = this.camera.position.add(dir.scale(grabbing.distance)).add(grabbing.originalDiffOfPosition);
const newRotation = new BABYLON.Vector3(0, this.camera.rotation.y + grabbing.originalDiffOfRotation.y + grabbing.rotation, 0);
grabbing.ghost.position = newPos.clone();
grabbing.ghost.rotation = newRotation.clone();
let stickyOtherObject: string | null = null;
let sticky = false;
const isCollisionTarget = (m: BABYLON.AbstractMesh) => {
return m.metadata?.objectId !== grabbing.objectId &&
!m.metadata?.isGhost &&
!grabbing.descendantStickyObjectIds.includes(m.metadata?.objectId);
};
const pos = new BABYLON.Vector3(this.camera.position.x, this.camera.position.y, this.camera.position.z);
const _dir = newPos.subtract(pos).normalize();
for (let i = 0; i < 1000; i++) {
if (cm(i) > grabbing.distance) break;
const prevSticky = sticky;
// posを1cmずつnewPosの方向に動かす
pos.addInPlace(_dir.scale(cm(1)));
if (placement === 'side' || placement === 'wall') {
// 前方に向かってレイを飛ばす
const ray = new BABYLON.Ray(pos, dir, cm(1000));
const hit = placement === 'side' ? this.scene.pickWithRay(ray, (m) => isCollisionTarget(m) && (m.name.includes('__ROOM_WALL__') || m.name.includes('__SIDE__'))) : this.scene.pickWithRay(ray, (m) => isCollisionTarget(m) && (m.name.includes('__ROOM_WALL__')));
if (hit != null && hit.pickedPoint != null && hit.pickedMesh != null) {
sticky = true;
const pickedMeshNormal = hit.getNormal(true, true)!;
const targetRotationY = Math.atan2(pickedMeshNormal.x, pickedMeshNormal.z);
newRotation.y = targetRotationY;
newRotation.z = grabbing.originalDiffOfRotation.z + grabbing.rotation;
newPos = hit.pickedPoint;
stickyOtherObject = hit.pickedMesh.metadata?.objectId ?? null;
if (this.gridSnapping.enabled) {
newPos.y = Math.round(newPos.y / this.gridSnapping.scale) * this.gridSnapping.scale;
if (getYRotationDirection(targetRotationY) === '+x' || getYRotationDirection(targetRotationY) === '-x') {
newPos.z = Math.round(newPos.z / this.gridSnapping.scale) * this.gridSnapping.scale;
} else {
newPos.x = Math.round(newPos.x / this.gridSnapping.scale) * this.gridSnapping.scale;
}
this.gridPlane.rotationQuaternion = null;
this.gridPlane.rotation.x = Math.PI;
this.gridPlane.rotation.y = targetRotationY;
this.gridPlane.position = newPos.add(pickedMeshNormal.scale(cm(0.1)));
if (getYRotationDirection(targetRotationY) === '+x' || getYRotationDirection(targetRotationY) === '-x') {
this.gridPlane.position.y = 0;
this.gridPlane.position.z = 0;
} else {
this.gridPlane.position.y = 0;
this.gridPlane.position.x = 0;
}
this.gridPlane.isVisible = true;
}
}
} else if (placement === 'bottom' || placement === 'ceiling') {
// 上に向かってレイを飛ばす
const ray = new BABYLON.Ray(pos, new BABYLON.Vector3(0, 1, 0), cm(1000));
const hit = placement === 'bottom' ? this.scene.pickWithRay(ray, (m) => isCollisionTarget(m) && (m.name.includes('__ROOM_CEILING__') || m.name.includes('__ROOM_BOTTOM__') || m.name.includes('__BOTTOM__'))) : this.scene.pickWithRay(ray, (m) => isCollisionTarget(m) && (m.name.includes('__ROOM_CEILING__')));
if (hit != null && hit.pickedPoint != null && hit.pickedMesh != null) {
sticky = true;
newPos = hit.pickedPoint;
stickyOtherObject = hit.pickedMesh.metadata?.objectId ?? null;
if (this.gridSnapping.enabled) {
newPos.x = Math.round(newPos.x / this.gridSnapping.scale) * this.gridSnapping.scale;
newPos.z = Math.round(newPos.z / this.gridSnapping.scale) * this.gridSnapping.scale;
this.gridPlane.rotationQuaternion = null;
this.gridPlane.rotation.x = Math.PI * 1.5;
this.gridPlane.rotation.y = 0;
this.gridPlane.position = new BABYLON.Vector3(grabbing.mesh.position.x, grabbing.mesh.position.y - cm(0.1), grabbing.mesh.position.z);
this.gridPlane.position.x = 0;
this.gridPlane.position.z = 0;
this.gridPlane.isVisible = true;
}
}
} else { // top or floor
// 下に向かってレイを飛ばす
const ray = new BABYLON.Ray(pos, new BABYLON.Vector3(0, -1, 0), cm(1000));
const hit = placement === 'top' ? this.scene.pickWithRay(ray, (m) => isCollisionTarget(m) && (m.name.includes('__ROOM_FLOOR__') || m.name.includes('__ROOM_TOP__') || m.name.includes('__TOP__'))) : this.scene.pickWithRay(ray, (m) => isCollisionTarget(m) && (m.name.includes('__ROOM_FLOOR__')));
if (hit != null && hit.pickedPoint != null && hit.pickedMesh != null) {
sticky = true;
newPos = hit.pickedPoint;
stickyOtherObject = hit.pickedMesh.metadata?.objectId ?? null;
if (this.gridSnapping.enabled) {
newPos.x = Math.round(newPos.x / this.gridSnapping.scale) * this.gridSnapping.scale;
newPos.z = Math.round(newPos.z / this.gridSnapping.scale) * this.gridSnapping.scale;
this.gridPlane.rotationQuaternion = null;
this.gridPlane.rotation.x = Math.PI / 2;
this.gridPlane.rotation.y = 0;
this.gridPlane.position = new BABYLON.Vector3(grabbing.mesh.position.x, grabbing.mesh.position.y + cm(0.1), grabbing.mesh.position.z);
this.gridPlane.position.x = 0;
this.gridPlane.position.z = 0;
this.gridPlane.isVisible = true;
}
}
}
if (!sticky && prevSticky) {
sticky = true;
break;
}
}
if (sticky) {
if (this.gridSnapping.enabled) {
newRotation.y = Math.round(newRotation.y / (Math.PI / 8)) * (Math.PI / 8);
}
grabbing.mesh.position = newPos;
grabbing.mesh.rotation = newRotation;
}
if (!sticky) {
this.gridPlane.isVisible = false;
//for (const mesh of grabbing.ghost.getChildMeshes()) {
//if (mesh.material instanceof BABYLON.MultiMaterial) {
// for (const subMat of mesh.material.subMaterials) {
// if (subMat instanceof BABYLON.PBRMaterial) {
// subMat.emissiveColor = new BABYLON.Color3(1, 0, 0);
// }
// }
//} else {
// mesh.material.emissiveColor = new BABYLON.Color3(1, 0, 0);
//}
//}
}
//const pos = new BABYLON.Vector3(this.camera.position.x, this.camera.position.y, this.camera.position.z);
//const _dir = newPos.subtract(pos).normalize();
//for (let i = 0; i < grabbing.distance; i++) {
// // posを1cmずつnewPosの方向に動かす
// pos.addInPlace(_dir.scale(cm(1)));
// // 前方に向かってレイを飛ばして衝突チェック
// const ray = new BABYLON.Ray(this.camera.position, dir, i);
// const hit = this.scene.pickWithRay(ray, (m) => isCollisionTarget(m));
// if (hit != null && hit.pickedPoint != null && hit.pickedMesh != null) {
// //const isCollided = grabbing.mesh.intersectsMesh(hit.pickedMesh, false);
// //if (isCollided) {
// break;
// //}
// }
// grabbing.mesh.position = pos.clone();
//}
//const ray = new BABYLON.Ray(this.camera.position, this.camera.getDirection(BABYLON.Axis.Z), cm(1000));
//const hit = this.scene.pickWithRay(ray, (m) => m.name.includes('__COLLISION_WALL__'))!;
//if (hit.pickedMesh != null) {
// const grabbingBox = this.grabbing.mesh.getBoundingInfo().boundingBox;
// const grabDistanceVector = this.grabbing.mesh.position.subtract(this.camera.position);
// if (grabDistanceVector.length() > hit.distance) {
// this.grabbing.mesh.position = this.camera.position.add(dir.scale(hit.distance));
// this.grabbing.mesh.position.y = y;
// }
//}
//const displacementVector = new BABYLON.Vector3(
// this.grabbing.ghost.position.x - this.grabbing.mesh.position.x,
// 0,
// this.grabbing.ghost.position.z - this.grabbing.mesh.position.z,
//);
//this.grabbing.mesh.moveWithCollisions(displacementVector);
//this.grabbing.mesh.position.y = y;
//for (const soid of stickyObjectIds) {
// //const soMesh = this.objectMeshs.get(soid)!;
// //const offset = this.grabbing.mesh!.position.subtract(soMeshStartPosition);
// //soMesh.position = this.grabbing.mesh!.position.subtract(offset);
//}
grabbing.onMove?.({
position: newPos,
rotation: newRotation,
sticky: stickyOtherObject,
});
}
private highlightMeshes(meshes: BABYLON.AbstractMesh[]) {
if (this.selectionOutlineLayer == null) return;
@@ -1422,15 +1378,14 @@ export class RoomEngine extends EventEmitter {
}
public updateRoomLightColor(color: [number, number, number]) {
if (this.roomLight.diffuse.equalsFloats(...color)) return;
this.roomLight.diffuse = new BABYLON.Color3(...color);
this.envManager.updateRoomLightColor(new BABYLON.Color3(...color));
this.roomState.roomLightColor = color;
this.ev('changeRoomState', { roomState: this.roomState });
}
private turnOnRoomLight(forInit = false) {
public turnOnRoomLight(forInit = false) {
if (!forInit) this.sr.disableSnapshotRendering(); // このメソッドは参照カウント方式な点に留意
this.roomLight.intensity = 18 * WORLD_SCALE * WORLD_SCALE;
this.envManager.turnOnRoomLight();
if (this.envManager?.envMapIndoor != null) this.envManager.envMapIndoor.level = 0.6;
for (const m of this.scene.materials) {
if (m.metadata?.disableEnvMap) {
@@ -1446,9 +1401,9 @@ export class RoomEngine extends EventEmitter {
}
}
private turnOffRoomLight() {
public turnOffRoomLight() {
this.sr.disableSnapshotRendering(); // このメソッドは参照カウント方式な点に留意
this.roomLight.intensity = 0;
this.envManager.turnOffRoomLight();
if (this.envManager?.envMapIndoor != null) this.envManager.envMapIndoor.level = 0.025;
for (const m of this.scene.materials) {
if (m.metadata?.disableEnvMap) {
@@ -1462,14 +1417,6 @@ export class RoomEngine extends EventEmitter {
}, 10);
}
public toggleRoomLight() {
if (this.roomLight.intensity > 0) {
this.turnOffRoomLight();
} else {
this.turnOnRoomLight();
}
}
private createGhost(mesh: BABYLON.Mesh): BABYLON.Mesh {
// 対象のメッシュの子に、「子にlightを持つメッシュ」が含まれているとエンジンがクラッシュするので、とりあえず適当なメッシュを使う
/*

View File

@@ -6,7 +6,9 @@
import * as BABYLON from '@babylonjs/core';
import { cm, WORLD_SCALE } from '../utility.js';
import { findMaterial } from './utility.js';
import { findMaterial, SYSTEM_HEYA_MESH_NAMES } from './utility.js';
import { GRAPHICS_QUALITY_MEDIUM } from './engine.js';
import type { RoomEngine } from './engine.js';
//export interface EnvManager<T = any> {
// constructor(onMeshUpdatedCallback?: ((meshes: BABYLON.AbstractMesh[]) => void) | null): void;
@@ -19,15 +21,36 @@ export abstract class EnvManager<T = any> {
protected onMeshUpdatedCallback: ((meshes: BABYLON.AbstractMesh[]) => void) | null = null;
public abstract envMapIndoor: BABYLON.CubeTexture | null;
public abstract maxCameraZ: number;
protected shadowGenerators: BABYLON.ShadowGenerator[] = [];
constructor(onMeshUpdatedCallback?: ((meshes: BABYLON.AbstractMesh[]) => void) | null) {
this.onMeshUpdatedCallback = onMeshUpdatedCallback ?? null;
}
abstract load(options: T, scene: BABYLON.Scene): Promise<void>;
abstract load(options: T, scene: BABYLON.Scene, engine: RoomEngine): Promise<void>;
abstract applyOptions(options: T): void;
abstract setTime(time: number): void;
abstract dispose(): void;
abstract updateRoomLightColor(color: BABYLON.Color3): void;
abstract turnOnRoomLight(): void;
abstract turnOffRoomLight(): void;
public addShadowCaster(mesh: BABYLON.AbstractMesh) {
for (const shadowGen of this.shadowGenerators) {
shadowGen.addShadowCaster(mesh);
}
}
public removeShadowCaster(mesh: BABYLON.AbstractMesh) {
for (const shadowGen of this.shadowGenerators) {
shadowGen.removeShadowCaster(mesh);
}
}
public dispose() {
for (const shadowGen of this.shadowGenerators) {
shadowGen.dispose();
}
}
}
export type SimpleEnvOptions = {
@@ -74,6 +97,8 @@ export class SimpleEnvManager extends EnvManager<SimpleEnvOptions> {
private floorMaterial: BABYLON.PBRMaterial | null = null;
private skybox: BABYLON.Mesh | null = null;
private skyboxMat: BABYLON.StandardMaterial | null = null;
private roomLight: BABYLON.SpotLight | null = null;
private sunLight: BABYLON.DirectionalLight | null = null;
public envMapIndoor: BABYLON.CubeTexture | null = null;
public maxCameraZ = cm(1000);
@@ -81,7 +106,7 @@ export class SimpleEnvManager extends EnvManager<SimpleEnvOptions> {
super(onMeshUpdatedCallback);
}
public async load(options: SimpleEnvOptions, scene: BABYLON.Scene) {
public async load(options: SimpleEnvOptions, scene: BABYLON.Scene, engine: RoomEngine) {
this.skybox = BABYLON.MeshBuilder.CreateBox('skybox', { size: cm(1000) }, scene);
this.skyboxMat = new BABYLON.StandardMaterial('skyboxMat', scene);
this.skyboxMat.backFaceCulling = false;
@@ -89,6 +114,43 @@ export class SimpleEnvManager extends EnvManager<SimpleEnvOptions> {
this.skybox.material = this.skyboxMat;
this.skybox.infiniteDistance = true;
this.roomLight = new BABYLON.SpotLight('roomLight', new BABYLON.Vector3(0, cm(249), 0), new BABYLON.Vector3(0, -1, 0), 16, 8, scene);
this.roomLight.diffuse = new BABYLON.Color3(...engine.roomState.roomLightColor);
this.roomLight.shadowMinZ = cm(10);
this.roomLight.shadowMaxZ = cm(300);
this.roomLight.radius = cm(30);
if (engine.graphicsQuality >= GRAPHICS_QUALITY_MEDIUM) {
const shadowGeneratorForRoomLight = new BABYLON.ShadowGenerator(engine.graphicsQuality <= GRAPHICS_QUALITY_MEDIUM ? 1024 : 2048, this.roomLight);
shadowGeneratorForRoomLight.forceBackFacesOnly = true;
shadowGeneratorForRoomLight.bias = 0.00001;
shadowGeneratorForRoomLight.normalBias = 0.005;
shadowGeneratorForRoomLight.usePercentageCloserFiltering = true;
shadowGeneratorForRoomLight.filteringQuality = BABYLON.ShadowGenerator.QUALITY_HIGH;
if (engine.graphicsQuality <= GRAPHICS_QUALITY_MEDIUM) {
shadowGeneratorForRoomLight.getShadowMap().refreshRate = 60;
}
//this.shadowGeneratorForRoomLight.useContactHardeningShadow = true;
this.shadowGenerators.push(shadowGeneratorForRoomLight);
}
if (engine.graphicsQuality >= GRAPHICS_QUALITY_MEDIUM) {
this.sunLight = new BABYLON.DirectionalLight('sunLight', new BABYLON.Vector3(0.2, -1, -1), scene);
this.sunLight.position = new BABYLON.Vector3(cm(-20), cm(1000), cm(1000));
this.sunLight.shadowMinZ = cm(1000);
this.sunLight.shadowMaxZ = cm(2000);
const shadowGeneratorForSunLight = new BABYLON.ShadowGenerator(engine.graphicsQuality <= GRAPHICS_QUALITY_MEDIUM ? 1024 : 2048, this.sunLight);
shadowGeneratorForSunLight.forceBackFacesOnly = true;
shadowGeneratorForSunLight.bias = 0.00001;
shadowGeneratorForSunLight.usePercentageCloserFiltering = true;
shadowGeneratorForSunLight.usePoissonSampling = true;
if (engine.graphicsQuality <= GRAPHICS_QUALITY_MEDIUM) {
shadowGeneratorForSunLight.getShadowMap().refreshRate = 60;
}
this.shadowGenerators.push(shadowGeneratorForSunLight);
}
this.loaderResult = await BABYLON.ImportMeshAsync('/client-assets/room/envs/default/300.glb', scene);
this.envMapIndoor = BABYLON.CubeTexture.CreateFromPrefilteredData('/client-assets/room/indoor.env', scene);
@@ -182,6 +244,15 @@ export class SimpleEnvManager extends EnvManager<SimpleEnvOptions> {
const baseboardMaterial = findMaterial(this.meshes[0], '__BASEBOARD__');
//baseboardMaterial.metadata.disableEnvMap = true;
for (const mesh of this.meshes) {
if (SYSTEM_HEYA_MESH_NAMES.some(name => mesh.name.includes(name))) continue;
mesh.receiveShadows = true;
if (mesh.material !== this.floorMaterial) { // 床は他の何にも影を落とさないことが確定している
this.addShadowCaster(mesh);
}
}
await this.applyOptions(options);
}
@@ -193,6 +264,26 @@ export class SimpleEnvManager extends EnvManager<SimpleEnvOptions> {
} else {
this.skyboxMat.emissiveColor = new BABYLON.Color3(0.05, 0.05, 0.2);
}
if (this.sunLight != null) {
this.sunLight.diffuse = time === 0 ? new BABYLON.Color3(1.0, 0.9, 0.8) : time === 1 ? new BABYLON.Color3(1.0, 0.8, 0.6) : new BABYLON.Color3(0.6, 0.8, 1.0);
this.sunLight.intensity = time === 0 ? 3 : time === 1 ? 1 : 0.25;
}
}
public updateRoomLightColor(color: BABYLON.Color3): void {
if (this.roomLight == null) return;
this.roomLight.diffuse = color;
}
public turnOnRoomLight(): void {
if (this.roomLight == null) return;
this.roomLight.intensity = 18 * WORLD_SCALE * WORLD_SCALE;
}
public turnOffRoomLight(): void {
if (this.roomLight == null) return;
this.roomLight.intensity = 0;
}
public applyOptions(options: SimpleEnvOptions) {
@@ -362,6 +453,9 @@ export class SimpleEnvManager extends EnvManager<SimpleEnvOptions> {
this.skybox?.dispose();
this.skyboxMat?.dispose();
this.envMapIndoor?.dispose();
this.roomLight?.dispose();
this.sunLight?.dispose();
super.dispose();
}
}
@@ -431,5 +525,6 @@ export class MuseumEnvManager extends EnvManager<MuseumEnvOptions> {
m.dispose(false, true);
}
this.envMapIndoor?.dispose();
super.dispose();
}
}