diff --git a/packages/frontend/assets/world/avatar/avatar.blend b/packages/frontend/assets/world/avatar/avatar.blend new file mode 100644 index 0000000000..9aecb44220 Binary files /dev/null and b/packages/frontend/assets/world/avatar/avatar.blend differ diff --git a/packages/frontend/assets/world/avatar/avatar.glb b/packages/frontend/assets/world/avatar/avatar.glb new file mode 100644 index 0000000000..d1bd44b03b Binary files /dev/null and b/packages/frontend/assets/world/avatar/avatar.glb differ diff --git a/packages/frontend/assets/world/lobby/default.blend b/packages/frontend/assets/world/lobby/default.blend new file mode 100644 index 0000000000..4ab9184ab5 Binary files /dev/null and b/packages/frontend/assets/world/lobby/default.blend differ diff --git a/packages/frontend/assets/world/lobby/default.glb b/packages/frontend/assets/world/lobby/default.glb new file mode 100644 index 0000000000..146c1da2ac Binary files /dev/null and b/packages/frontend/assets/world/lobby/default.glb differ diff --git a/packages/frontend/assets/world/lobby/dummy-ads/1.png b/packages/frontend/assets/world/lobby/dummy-ads/1.png new file mode 100644 index 0000000000..49e0552e25 Binary files /dev/null and b/packages/frontend/assets/world/lobby/dummy-ads/1.png differ diff --git a/packages/frontend/assets/world/lobby/dummy-ads/2.png b/packages/frontend/assets/world/lobby/dummy-ads/2.png new file mode 100644 index 0000000000..15d94cf5a7 Binary files /dev/null and b/packages/frontend/assets/world/lobby/dummy-ads/2.png differ diff --git a/packages/frontend/assets/world/lobby/dummy-ads/3.png b/packages/frontend/assets/world/lobby/dummy-ads/3.png new file mode 100644 index 0000000000..9e1213c5ed Binary files /dev/null and b/packages/frontend/assets/world/lobby/dummy-ads/3.png differ diff --git a/packages/frontend/assets/world/lobby/dummy-ads/4.png b/packages/frontend/assets/world/lobby/dummy-ads/4.png new file mode 100644 index 0000000000..10976aef38 Binary files /dev/null and b/packages/frontend/assets/world/lobby/dummy-ads/4.png differ diff --git a/packages/frontend/src/pages/world.vue b/packages/frontend/src/pages/world.vue index dbd40aff3c..35d092bbe8 100644 --- a/packages/frontend/src/pages/world.vue +++ b/packages/frontend/src/pages/world.vue @@ -23,60 +23,12 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -93,7 +45,7 @@ import * as os from '@/os.js'; import MkInput from '@/components/MkInput.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkRange from '@/components/MkRange.vue'; -import { RoomController } from '@/world/room/controller.js'; +import { WorldController } from '@/world/controller.js'; const canvas = useTemplateRef('canvas'); @@ -110,74 +62,7 @@ function resize() { const isZenMode = ref(false); -const data = localStorage.getItem('roomData') != null ? { ...JSON.parse(localStorage.getItem('roomData')!), ...{ - heya: { - type: 'simple', - options: { - dimension: [300, 300], - window: 'demado', - wallN: { - material: null, - color: [0.9, 0.9, 0.9], - }, - wallE: { - material: null, - color: [0.33, 0.34, 0.35], - }, - wallS: { - material: null, - color: [0.9, 0.9, 0.9], - }, - wallW: { - material: null, - color: [0.9, 0.9, 0.9], - }, - flooring: { - material: 'wood', - color: [0.9, 0.9, 0.9], - }, - ceiling: { - material: null, - color: [0.9, 0.9, 0.9], - }, - }, - }, -} } : { - heya: { - type: 'simple', - options: { - dimension: [300, 300], - window: 'demado', - wallN: { - material: null, - color: [0.9, 0.9, 0.9], - }, - wallE: { - material: null, - color: [0.9, 0.9, 0.9], - }, - wallS: { - material: null, - color: [0.9, 0.9, 0.9], - }, - wallW: { - material: null, - color: [0.9, 0.9, 0.9], - }, - flooring: { - material: 'wood', - color: [0.9, 0.9, 0.9], - }, - ceiling: { - material: null, - color: [0.9, 0.9, 0.9], - }, - }, - }, - installedObjects: [], -}; - -const controller = new RoomController(data); +const controller = new WorldController(); onMounted(async () => { controller.init(canvas.value!); @@ -185,19 +70,6 @@ onMounted(async () => { canvas.value!.focus(); window.addEventListener('resize', resize); - - //watch(controller.selected, (v) => { - // if (v == null) { - // interacions.value = []; - // } else { - // interacions.value = Object.entries(v.objectEntity.instance.interactions).map(([interactionId, interactionInfo]) => ({ - // id: interactionId, - // label: interactionInfo.label, - // isPrimary: v.objectEntity.instance.primaryInteraction === interactionId, - // fn: interactionInfo.fn, - // })); - // } - //}); }); onUnmounted(() => { @@ -206,132 +78,6 @@ onUnmounted(() => { window.removeEventListener('resize', resize); }); -function beginSelectedInstalledObjectGrabbing() { - controller.beginSelectedInstalledObjectGrabbing(); - canvas.value!.focus(); -} - -function endGrabbing() { - controller.endGrabbing(); - canvas.value!.focus(); -} - -function toggleLight() { - controller.toggleRoomLight(); - canvas.value!.focus(); -} - -function showSnappingMenu(ev: PointerEvent) { - os.popupMenu([{ - type: 'switch', - text: i18n.ts._room.snapToGrid, - ref: computed({ - get: () => controller.gridSnapping.value.enabled, - set: v => controller.setGridSnapping({ ...controller.gridSnapping.value, enabled: v }), - }), - }, { - type: 'radioOption', - text: '1cm', - active: computed(() => controller.gridSnapping.value.scale === 1), - action: () => controller.setGridSnapping({ ...controller.gridSnapping.value, scale: 1 }), - }, { - type: 'radioOption', - text: '2cm', - active: computed(() => controller.gridSnapping.value.scale === 2), - action: () => controller.setGridSnapping({ ...controller.gridSnapping.value, scale: 2 }), - }, { - type: 'radioOption', - text: '4cm', - active: computed(() => controller.gridSnapping.value.scale === 4), - action: () => controller.setGridSnapping({ ...controller.gridSnapping.value, scale: 4 }), - }, { - type: 'radioOption', - text: '8cm', - active: computed(() => controller.gridSnapping.value.scale === 8), - action: () => controller.setGridSnapping({ ...controller.gridSnapping.value, scale: 8 }), - }], ev.currentTarget ?? ev.target); -} - -function rotate() { - controller.changeGrabbingRotationY(Math.PI / 8); - canvas.value!.focus(); -} - -async function addObject(ev: PointerEvent) { - const { dispose } = await os.popupAsyncWithDialog(import('./room.add-object-dialog.vue').then(x => x.default), { - }, { - ok: async (res) => { - controller.addObject(res); - canvas.value!.focus(); - }, - closed: () => dispose(), - }); -} - -function removeSelectedObject() { - controller.removeSelectedObject(); - canvas.value!.focus(); -} - -function enterEditMode() { - controller.enterEditMode(); -} - -function exitEditMode() { - controller.exitEditMode(); -} - -function getHex(c: [number, number, number]) { - return `#${c.map(x => Math.round(x * 255).toString(16).padStart(2, '0')).join('')}`; -} - -function getRgb(hex: string | number): [number, number, number] | null { - if ( - typeof hex === 'number' || - typeof hex !== 'string' || - !/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(hex) - ) { - return null; - } - - const m = hex.slice(1).match(/[0-9a-fA-F]{2}/g); - if (m == null) return [0, 0, 0]; - return m.map(x => parseInt(x, 16) / 255) as [number, number, number]; -} - -function save() { - localStorage.setItem('roomData', JSON.stringify(controller.roomState.value)); -} - -function expor() { - const dataStr = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(controller.roomState.value)); - const dlAnchorElem = window.document.createElement('a'); - dlAnchorElem.setAttribute('href', dataStr); - dlAnchorElem.setAttribute('download', 'room.json'); - dlAnchorElem.click(); -} - -function impor() { - const inputElem = window.document.createElement('input'); - inputElem.setAttribute('type', 'file'); - inputElem.setAttribute('accept', 'application/json'); - inputElem.addEventListener('change', () => { - const file = inputElem.files?.[0]; - if (file == null) return; - const reader = new FileReader(); - reader.onload = () => { - try { - localStorage.setItem('roomData', reader.result as string); - window.location.reload(); - } catch (e) { - alert('Failed to load room data: ' + e); - } - }; - reader.readAsText(file); - }); - inputElem.click(); -} - definePage(() => ({ title: 'Room', icon: 'ti ti-door', @@ -367,24 +113,6 @@ definePage(() => ({ .controls { } -.overlayControls { - position: absolute; - bottom: 0; - left: 0; - z-index: 1; - width: 100%; -} - -.overlayObjectInfoPanel { - position: absolute; - top: 16px; - right: 16px; - z-index: 1; - padding: 16px; - box-sizing: border-box; - width: 300px; -} - .loading { position: absolute; top: 0; diff --git a/packages/frontend/src/world/controller.ts b/packages/frontend/src/world/controller.ts new file mode 100644 index 0000000000..c064c2416f --- /dev/null +++ b/packages/frontend/src/world/controller.ts @@ -0,0 +1,141 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { reactive, ref, shallowRef, triggerRef, watch } from 'vue'; +import * as BABYLON from '@babylonjs/core'; +import { WorldEngine } from './engine.js'; +import type { ShallowRef } from 'vue'; +import * as sound from '@/utility/sound.js'; + +// 抽象化レイヤー +export class WorldController { + private worker: Worker | null = null; + private engine: WorldEngine | null = null; + private canvas: HTMLCanvasElement | null = null; + public isReady = ref(false); + public isSitting = ref(false); + public initializeProgress = ref(0); + + constructor() { + } + + public async init(canvas: HTMLCanvasElement, workerMode = false) { + this.canvas = canvas; + this.canvas.width = canvas.clientWidth; + this.canvas.height = canvas.clientHeight; + + if (workerMode) { + //const offscreen = canvas.transferControlToOffscreen(); + //this.worker = new RoomWorker(); + //this.worker.postMessage({ type: 'init', canvas: offscreen }, [offscreen]); + //this.isReady.value = true; + } else { + const babylonEngine = new BABYLON.WebGPUEngine(canvas, { doNotHandleContextLost: true }); + babylonEngine.compatibilityMode = false; + await babylonEngine.initAsync(); + this.engine = new WorldEngine({ canvas, engine: babylonEngine }); + this.engine.on('loadingProgress', ({ progress }) => { + this.initializeProgress.value = progress; + }); + await this.engine.init(); + this.initializeProgress.value = 1; + this.isReady.value = true; + + this.engine.on('playSfxUrl', ({ url, options }) => { + sound.playUrl(url, options); + }); + } + + this.canvas.addEventListener('keydown', (ev) => { + if (this.worker != null) { + this.worker.postMessage({ type: 'dom:keydown', ev: { code: ev.code, shiftKey: ev.shiftKey } }); + } else if (this.engine != null) { + this.engine.domEvents.emit('keydown', { code: ev.code, shiftKey: ev.shiftKey }); + } + ev.preventDefault(); + ev.stopPropagation(); + return false; + }); + + this.canvas.addEventListener('keyup', (ev) => { + if (this.worker != null) { + this.worker.postMessage({ type: 'dom:keyup', ev: { code: ev.code, shiftKey: ev.shiftKey } }); + } else if (this.engine != null) { + this.engine.domEvents.emit('keyup', { code: ev.code, shiftKey: ev.shiftKey }); + } + ev.preventDefault(); + ev.stopPropagation(); + return false; + }); + + this.canvas.addEventListener('pointerdown', (ev) => { + // todo + }); + + this.canvas.addEventListener('wheel', (ev) => { + if (this.worker != null) { + this.worker.postMessage({ type: 'dom:wheel', ev: { deltaY: ev.deltaY } }); + } else if (this.engine != null) { + this.engine.domEvents.emit('wheel', { deltaY: ev.deltaY }); + } + ev.preventDefault(); + ev.stopPropagation(); + return false; + }); + + let isDragging = false; + + this.canvas.addEventListener('pointerdown', (ev) => { + this.canvas.setPointerCapture(ev.pointerId); + }); + + this.canvas.addEventListener('pointermove', (ev) => { + if (this.canvas.hasPointerCapture(ev.pointerId)) { + isDragging = true; + } + }); + + this.canvas.addEventListener('pointerup', (ev) => { + window.setTimeout(() => { + isDragging = false; + this.canvas.releasePointerCapture(ev.pointerId); + }, 0); + }); + + this.canvas.addEventListener('click', (ev) => { + if (isDragging) return; + if (this.worker != null) { + this.worker.postMessage({ type: 'dom:click', ev: { offsetX: ev.offsetX, offsetY: ev.offsetY } }); + } else if (this.engine != null) { + this.engine.domEvents.emit('click', { offsetX: ev.offsetX, offsetY: ev.offsetY }); + } + ev.preventDefault(); + ev.stopPropagation(); + return false; + }); + } + + public resize() { + if (this.canvas == null) return; + const width = this.canvas.clientWidth; + const height = this.canvas.clientHeight; + if (this.worker != null) { + this.worker.postMessage({ type: 'resize', width, height }); + } else if (this.engine != null) { + this.engine.resize(); + } + } + + public destroy() { + if (this.worker != null) { + this.worker.terminate(); + this.worker = null; + } + if (this.engine != null) { + this.engine.destroy(); + this.engine = null; + } + } +} diff --git a/packages/frontend/src/world/engine.ts b/packages/frontend/src/world/engine.ts index e98e2e56b7..2a0652b57b 100644 --- a/packages/frontend/src/world/engine.ts +++ b/packages/frontend/src/world/engine.ts @@ -3,21 +3,17 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -// TODO: 家具設置時のコリジョン判定(めりこんで設置されないようにする) -// TODO: 近くのオブジェクトの端にスナップオプション -// TODO: 近くのオブジェクトの原点に軸を揃えるオプション -// TODO: glbを事前に最適化(なるべくメッシュをマージするなど)するツールもしくはMisskeyビルド時処理。ついでにカタログ用スクショも自動生成したい - import * as BABYLON from '@babylonjs/core'; import { AxesViewer } from '@babylonjs/core/Debug/axesViewer'; import { registerBuiltInLoaders } from '@babylonjs/loaders/dynamic'; import { EventEmitter } from 'eventemitter3'; -import { HorizontalCameraKeyboardMoveInput, camelToKebab, cm } from './utility.js'; +import tinycolor from 'tinycolor2'; +import { HorizontalCameraKeyboardMoveInput, WORLD_SCALE, camelToKebab, cm, createPlaneUvMapper, normalizeUvToSquare } from './utility.js'; import { TIME_MAP } from './utility.js'; import { genId } from '@/utility/id.js'; import { deepClone } from '@/utility/clone.js'; -const SNAPSHOT_RENDERING = true; // 実験的 +const SNAPSHOT_RENDERING = false; // 実験的 const USE_GLOW = true; // ドローコールが増えて重い const IN_WEB_WORKER = typeof window === 'undefined'; @@ -41,9 +37,10 @@ export class WorldEngine extends EventEmitter { public intervalIds: number[] = []; public timeoutIds: number[] = []; private time: 0 | 1 | 2 = 0; // 0: 昼, 1: 夕, 2: 夜 - private envMapOutdoor: BABYLON.CubeTexture; + private envMap: BABYLON.CubeTexture; public lightContainer: BABYLON.ClusteredLightContainer; public sr: BABYLON.SnapshotRenderingHelper; + private gl: BABYLON.GlowLayer | null = null; public isSitting = false; private fps: number | null = null; @@ -68,9 +65,6 @@ export class WorldEngine extends EventEmitter { this.engine = options.engine; this.scene = new BABYLON.Scene(this.engine); - // なんかレンダリングがおかしくなるときがあるのでコメントアウト - // オブジェクトを選択し、後ろを向いて別のオブジェクトを選択した後、最初のオブジェクトに振り返ると消えているなど - //this.scene.performancePriority = BABYLON.ScenePerformancePriority.Intermediate; this.scene.autoClear = false; //this.scene.autoClearDepthAndStencil = false; this.scene.skipPointerMovePicking = true; @@ -78,35 +72,36 @@ export class WorldEngine extends EventEmitter { this.sr = new BABYLON.SnapshotRenderingHelper(this.scene); - const skybox = BABYLON.MeshBuilder.CreateBox('skybox', { size: cm(1000) }, this.scene); + const skybox = BABYLON.MeshBuilder.CreateBox('skybox', { size: cm(50000) }, this.scene); const skyboxMat = new BABYLON.StandardMaterial('skyboxMat', this.scene); skyboxMat.backFaceCulling = false; skyboxMat.disableLighting = true; skybox.material = skyboxMat; skybox.infiniteDistance = true; - this.time = TIME_MAP[new Date().getHours() as keyof typeof TIME_MAP]; + //this.time = TIME_MAP[new Date().getHours() as keyof typeof TIME_MAP]; + this.time = TIME_MAP[12 as keyof typeof TIME_MAP]; if (this.time === 0) { - skyboxMat.emissiveColor = new BABYLON.Color3(0.7, 0.9, 1.0); + skyboxMat.emissiveColor = new BABYLON.Color3(0.87, 0.89, 0.9); } else if (this.time === 1) { - skyboxMat.emissiveColor = new BABYLON.Color3(0.8, 0.5, 0.3); + skyboxMat.emissiveColor = new BABYLON.Color3(0.7, 0.68, 0.66); } else { - skyboxMat.emissiveColor = new BABYLON.Color3(0.05, 0.05, 0.2); + skyboxMat.emissiveColor = new BABYLON.Color3(0.48, 0.48, 0.5); } - this.scene.ambientColor = new BABYLON.Color3(1.0, 0.9, 0.8); + this.scene.ambientColor = new BABYLON.Color3(0.9, 0.9, 0.9); - this.envMapOutdoor = BABYLON.CubeTexture.CreateFromPrefilteredData(this.time === 2 ? '/client-assets/room/outdoor-night.env' : '/client-assets/room/outdoor-day.env', this.scene); - this.envMapOutdoor.level = this.time === 0 ? 0.5 : this.time === 1 ? 0.3 : 0.1; + this.envMap = BABYLON.CubeTexture.CreateFromPrefilteredData(this.time === 2 ? '/client-assets/room/outdoor-night.env' : '/client-assets/room/outdoor-day.env', this.scene); + this.envMap.level = 0.3; this.scene.collisionsEnabled = true; - this.camera = new BABYLON.UniversalCamera('camera', new BABYLON.Vector3(0, cm(130), cm(0)), this.scene); + this.camera = new BABYLON.UniversalCamera('camera', new BABYLON.Vector3(cm(0), cm(200), cm(3000)), this.scene); this.camera.inputs.removeByType('FreeCameraKeyboardMoveInput'); - this.camera.inputs.add(new HorizontalCameraKeyboardMoveInput(this.camera)); + this.camera.inputs.add(new HorizontalCameraKeyboardMoveInput(this.camera, 0.3)); this.camera.attachControl(this.canvas); this.camera.minZ = cm(1); - this.camera.maxZ = cm(2000); + this.camera.maxZ = cm(100000); this.camera.fov = 1; this.camera.ellipsoid = new BABYLON.Vector3(cm(15), cm(65), cm(15)); this.camera.checkCollisions = true; @@ -127,25 +122,26 @@ export class WorldEngine extends EventEmitter { sunLight.shadowMinZ = cm(1000); sunLight.shadowMaxZ = cm(2000); - this.shadowGeneratorForSunLight = new BABYLON.ShadowGenerator(2048, sunLight); + this.shadowGeneratorForSunLight = new BABYLON.ShadowGenerator(4096, sunLight); this.shadowGeneratorForSunLight.forceBackFacesOnly = true; this.shadowGeneratorForSunLight.bias = 0.0001; this.shadowGeneratorForSunLight.usePercentageCloserFiltering = true; this.shadowGeneratorForSunLight.usePoissonSampling = true; - if (!SNAPSHOT_RENDERING) this.shadowGeneratorForSunLight.getShadowMap().refreshRate = 60; // snapshot renderingではrefreshRateが設定されているとなぜかクラッシュする + //this.shadowGeneratorForSunLight.getShadowMap().refreshRate = 60; this.lightContainer = new BABYLON.ClusteredLightContainer('clustered', [], this.scene); if (USE_GLOW) { - const gl = new BABYLON.GlowLayer('glow', this.scene, { + this.gl = new BABYLON.GlowLayer('glow', this.scene, { //mainTextureFixedSize: 512, blurKernelSize: 64, }); - gl.intensity = 0.5; - this.scene.setRenderingAutoClearDepthStencil(gl.renderingGroupId, false); + this.gl.intensity = 0.5; + this.gl.addExcludedMesh(skybox); + this.scene.setRenderingAutoClearDepthStencil(this.gl.renderingGroupId, false); if (SNAPSHOT_RENDERING) { - this.sr.updateMeshesForEffectLayer(gl); + this.sr.updateMeshesForEffectLayer(this.gl); } } @@ -169,7 +165,7 @@ export class WorldEngine extends EventEmitter { } public async init() { - this.scene.blockMaterialDirtyMechanism = true; + await this.loadEnvModel(); if (SNAPSHOT_RENDERING) { this.sr.enableSnapshotRendering(); @@ -214,19 +210,138 @@ export class WorldEngine extends EventEmitter { } private async loadEnvModel() { - const envObj = await BABYLON.ImportMeshAsync('/client-assets/room/env.glb', this.scene); + const envObj = await BABYLON.ImportMeshAsync('/client-assets/world/lobby/default.glb', this.scene); envObj.meshes[0].scaling = envObj.meshes[0].scaling.scale(WORLD_SCALE); envObj.meshes[0].bakeCurrentTransformIntoVertices(); - envObj.meshes[0].position = new BABYLON.Vector3(0, cm(-900), 0); // 4階くらいの想定 - envObj.meshes[0].rotation = new BABYLON.Vector3(0, -Math.PI, 0); for (const mesh of envObj.meshes) { - mesh.isPickable = false; - mesh.checkCollisions = false; - - //if (mesh.name === '__root__') continue; - mesh.receiveShadows = false; - if (mesh.material) (mesh.material as BABYLON.PBRMaterial).reflectionTexture = this.envMapOutdoor; + if (mesh.name === '__root__') continue; + mesh.checkCollisions = true; + if (mesh.material) (mesh.material as BABYLON.PBRMaterial).reflectionTexture = this.envMap; } + + for (let i = 0; i < 16; i++) { + const sphereRoot = new BABYLON.TransformNode('', this.scene); + sphereRoot.position = new BABYLON.Vector3(cm(0), cm(500 + (100 * i)), cm(0)); + const rotation = Math.random() * Math.PI * 2; + const sphere = BABYLON.MeshBuilder.CreateSphere('', { diameter: cm(50 + (Math.random() * 250)) }, this.scene); + sphere.parent = sphereRoot; + sphere.position = new BABYLON.Vector3(cm(0), cm(0), cm(4000 + (Math.random() * 500))); + + const mat = new BABYLON.PBRMaterial('', this.scene); + const color = tinycolor({ h: Math.random() * 360, s: 1, l: 0.5 }).toRgb(); + mat.emissiveColor = new BABYLON.Color3(color.r / 255, color.g / 255, color.b / 255); + mat.disableLighting = true; + this.gl?.addExcludedMesh(sphere); + sphere.material = mat; + + const speed = 10000 + (Math.random() * 10000); + const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE); + anim.setKeys([ + { frame: 0, value: rotation }, + { frame: speed, value: Math.random() < 0.5 ? rotation + (Math.PI * 2) : rotation - (Math.PI * 2) }, + ]); + sphereRoot.animations = [anim]; + this.scene.beginAnimation(sphereRoot, 0, speed, true); + } + + //const sphere = BABYLON.MeshBuilder.CreateSphere('', { diameter: cm(10) }, this.scene); + + const adsCountCol = 4; + const adsCountRow = 2; + for (let j = 0; j < adsCountRow; j++) { + for (let i = 0; i < adsCountCol; i++) { + const adRoot = new BABYLON.TransformNode(`ad_${j}_${i}_root`, this.scene); + adRoot.position = new BABYLON.Vector3(cm(0), cm(500 + (1000 * j)), cm(0)); + const rotation = (i / adsCountCol) * Math.PI * 2; + const adMesh = BABYLON.MeshBuilder.CreatePlane(`ad_${j}_${i}`, { width: cm(1000), height: cm(700) }, this.scene); + adMesh.parent = adRoot; + adMesh.position = new BABYLON.Vector3(cm(0), cm(0), cm(7500)); + + const tex = new BABYLON.Texture(`/client-assets/world/lobby/dummy-ads/${1 + Math.floor(Math.random() * 4)}.png`, this.scene); + const adMat = new BABYLON.StandardMaterial(`ad_${j}_${i}_mat`, this.scene); + adMat.emissiveTexture = tex; + adMat.disableLighting = true; + adMesh.material = adMat; + + const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE); + anim.setKeys([ + { frame: 0, value: rotation }, + { frame: 15000, value: j % 2 === 0 ? rotation + (Math.PI * 2) : rotation - (Math.PI * 2) }, + ]); + adRoot.animations = [anim]; + this.scene.beginAnimation(adRoot, 0, 15000, true); + } + } + + const worldRingH = envObj.meshes.find(m => m.name.includes('__WORLD_RING_H__')); + const worldRingM = envObj.meshes.find(m => m.name.includes('__WORLD_RING_M__')); + + worldRingH.rotationQuaternion = null; + worldRingM.rotationQuaternion = null; + + const _1h = 1000 * 60 * 60; + const _12h = _1h * 12; + const _7days = _1h * 24 * 7; + const _30days = _1h * 24 * 30; + + setInterval(() => { + const time = Date.now(); + worldRingH.rotation.x = ((time % _30days) / _30days) * Math.PI * 2; + worldRingH.rotation.z = ((time % _12h) / _12h) * Math.PI * 2; + + worldRingM.rotation.x = ((time % _7days) / _7days) * Math.PI * 2; + worldRingM.rotation.z = ((time % _1h) / _1h) * Math.PI * 2; + }, 100); + + const screenMeshes = envObj.meshes.filter(m => m.name.includes('__SCREEN__')); + const screenMaterial = screenMeshes[0].material as BABYLON.PBRMaterial; + + setTimeout(() => { + const tex = new BABYLON.VideoTexture('', 'http://syu-win.local:3000/files/cbf69c5f-ea63-4b14-a4c7-9148557d87d4', this.scene, true, true); + tex.level = 0.5; + tex.video.loop = true; + tex.video.volume = 0.25; + //tex.video.muted = true; + + screenMaterial.emissiveTexture = tex; + screenMaterial.emissiveColor = new BABYLON.Color3(1, 1, 1); + + tex.onLoadObservable.addOnce(() => { + tex.video.play(); + for (const mesh of screenMeshes) { + normalizeUvToSquare(mesh); + const updateUv = createPlaneUvMapper(mesh); + if (tex == null) return; + const srcAspect = tex.getSize().width / tex.getSize().height; + const targetAspect = 16 / 9; + updateUv(srcAspect, targetAspect, 'cover'); + } + }); + }, 3000); + + const emitter = new BABYLON.TransformNode('emitter', this.scene); + emitter.position = new BABYLON.Vector3(0, cm(-1000), 0); + const ps = new BABYLON.ParticleSystem('', 128, this.scene); + ps.particleTexture = new BABYLON.Texture('/client-assets/room/objects/lava-lamp/bubble.png'); + ps.emitter = emitter; + ps.isLocal = true; + ps.minEmitBox = new BABYLON.Vector3(cm(-1000), 0, cm(-1000)); + ps.maxEmitBox = new BABYLON.Vector3(cm(1000), 0, cm(1000)); + ps.minEmitPower = 100; + ps.maxEmitPower = 500; + ps.minLifeTime = 30; + ps.maxLifeTime = 30; + ps.minSize = cm(30); + ps.maxSize = cm(300); + ps.direction1 = new BABYLON.Vector3(0, 1, 0); + ps.direction2 = new BABYLON.Vector3(0, 1, 0); + ps.emitRate = 1.5; + ps.blendMode = BABYLON.ParticleSystem.BLENDMODE_ADD; + ps.color1 = new BABYLON.Color4(1, 1, 1, 0.3); + ps.color2 = new BABYLON.Color4(1, 1, 1, 0.2); + ps.colorDead = new BABYLON.Color4(1, 1, 1, 0); + ps.preWarmCycles = Math.random() * 1000; + ps.start(); } public sitChair(objectId: string) { diff --git a/packages/frontend/src/world/utility.ts b/packages/frontend/src/world/utility.ts index 3807be351f..3c64efa337 100644 --- a/packages/frontend/src/world/utility.ts +++ b/packages/frontend/src/world/utility.ts @@ -50,12 +50,14 @@ export class HorizontalCameraKeyboardMoveInput extends BABYLON.BaseCameraPointer onCanvasBlurObserver = null; onKeyboardObserver = null; public canMove = true; + private moveSpeed = 0.1; - constructor(camera: BABYLON.UniversalCamera) { + constructor(camera: BABYLON.UniversalCamera, moveSpeed = 0.1) { super(); this.camera = camera; this.scene = this.camera.getScene(); this.engine = this.scene.getEngine(); + this.moveSpeed = moveSpeed; } attachControl() { @@ -129,9 +131,9 @@ export class HorizontalCameraKeyboardMoveInput extends BABYLON.BaseCameraPointer const dir = this.camera.getDirection(local.normalize()); dir.y = 0; dir.normalize(); - const rate = this.preShift ? 3 : 1; - const moveSpeed = 0.1 * this.scene.getAnimationRatio(); - const move = dir.scale(moveSpeed * rate); + const dashFactor = this.preShift ? 3 : 1; + const moveSpeed = this.moveSpeed * this.scene.getAnimationRatio(); + const move = dir.scale(moveSpeed * dashFactor); if (this.canMove) { this.camera.cameraDirection.addInPlace(move);