Spaces:
Running
Running
Update index.html
Browse files- index.html +312 -344
index.html
CHANGED
@@ -3,415 +3,383 @@
|
|
3 |
<head>
|
4 |
<meta charset="UTF-8">
|
5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
6 |
-
<title>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7 |
<style>
|
8 |
-
/* Базовые стили */
|
9 |
html, body {
|
10 |
overflow: hidden;
|
11 |
width: 100%;
|
12 |
height: 100%;
|
13 |
margin: 0;
|
14 |
padding: 0;
|
15 |
-
font-family: sans-serif;
|
16 |
-
color:
|
17 |
-
background-color: #
|
18 |
}
|
19 |
|
20 |
#renderCanvas {
|
21 |
width: 100%;
|
22 |
height: 100%;
|
23 |
-
touch-action: none; /*
|
24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
}
|
26 |
|
27 |
-
|
28 |
-
|
|
|
|
|
|
|
|
|
|
|
29 |
position: absolute;
|
30 |
-
top:
|
31 |
-
left:
|
32 |
background-color: rgba(0, 0, 0, 0.5);
|
33 |
padding: 10px;
|
34 |
border-radius: 5px;
|
35 |
-
|
36 |
-
max-width: calc(100% - 40px); /* Чтобы не вылезал на мобилках */
|
37 |
}
|
38 |
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
top: 50%;
|
43 |
-
left: 50%;
|
44 |
-
width: 10px;
|
45 |
-
height: 10px;
|
46 |
-
border: 1px solid white;
|
47 |
-
transform: translate(-50%, -50%);
|
48 |
-
pointer-events: none; /* Чтобы не мешал кликам */
|
49 |
-
mix-blend-mode: difference; /* Чтобы был виден на любом фоне */
|
50 |
}
|
51 |
|
52 |
-
|
53 |
-
|
|
|
|
|
|
|
|
|
|
|
54 |
position: absolute;
|
55 |
bottom: 20px;
|
56 |
-
|
57 |
-
display: none; /* По умолчанию
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
|
|
|
|
68 |
border-radius: 50%;
|
69 |
-
|
70 |
-
|
71 |
display: flex;
|
72 |
justify-content: center;
|
73 |
align-items: center;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
74 |
cursor: pointer;
|
75 |
-
|
|
|
|
|
|
|
|
|
76 |
}
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
/* Медиа-запрос для показа мобильных кнопок на тач-устройствах */
|
82 |
-
@media (hover: none) and (pointer: coarse) {
|
83 |
-
#mobileControls {
|
84 |
-
display: flex;
|
85 |
-
}
|
86 |
}
|
87 |
|
88 |
</style>
|
89 |
-
<!-- Подключаем Babylon.js с CDN -->
|
90 |
-
<script src="https://cdn.babylonjs.com/babylon.js"></script>
|
91 |
-
<!-- (Опционально) PEP для поддержки Pointer Events в старых браузерах -->
|
92 |
-
<script src="https://code.jquery.com/pep/0.4.3/pep.js"></script>
|
93 |
</head>
|
94 |
<body>
|
95 |
-
|
96 |
-
<
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
<
|
105 |
-
<span id="fpsCounter">FPS: </span><br>
|
106 |
-
<span id="blockInfo">Наведите курсор на блок</span>
|
107 |
</div>
|
108 |
-
<div id="crosshair">+</div>
|
109 |
|
110 |
-
|
111 |
-
<div
|
112 |
-
<div id="
|
113 |
-
|
|
|
|
|
|
|
|
|
114 |
</div>
|
115 |
|
116 |
<script>
|
117 |
const canvas = document.getElementById('renderCanvas');
|
118 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
119 |
|
120 |
-
// ---
|
121 |
-
const
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
|
128 |
-
let
|
129 |
-
let blocks = {}; // Объект для хранения координат существующих блоков { "x_y_z": mesh }
|
130 |
|
131 |
// --- Создание сцены ---
|
132 |
-
const createScene = function () {
|
133 |
-
|
134 |
-
scene.clearColor = new BABYLON.
|
135 |
-
scene.collisionsEnabled = true; // Включаем проверку столкновений на уровне сцены
|
136 |
-
scene.gravity = new BABYLON.Vector3(0, GRAVITY / 60, 0); // Применяем гравитацию (делим на ~FPS)
|
137 |
|
138 |
// --- Камера ---
|
139 |
-
//
|
140 |
-
const camera = new BABYLON.
|
141 |
-
camera.setTarget(BABYLON.Vector3
|
142 |
camera.attachControl(canvas, true);
|
143 |
|
144 |
-
//
|
145 |
-
camera.speed =
|
146 |
-
camera.
|
147 |
-
camera.
|
148 |
-
camera.
|
149 |
-
|
150 |
-
|
151 |
-
camera.keysRight = [68]; // D
|
152 |
-
camera.keysUpward = [32]; // Space
|
153 |
-
camera.keysDownward = [16]; // Shift
|
154 |
-
|
155 |
-
// Включаем физику для камеры (чтобы она падала и сталкивалась)
|
156 |
-
camera.checkCollisions = true;
|
157 |
camera.applyGravity = true;
|
158 |
-
//
|
159 |
-
|
160 |
-
|
|
|
161 |
|
162 |
// --- Освещение ---
|
163 |
-
|
|
|
164 |
light.intensity = 0.8;
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
document.addEventListener('pointerlockchange', lockChangeAlert, false);
|
251 |
-
document.addEventListener('mozpointerlockchange', lockChangeAlert, false);
|
252 |
-
document.addEventListener('webkitpointerlockchange', lockChangeAlert, false);
|
253 |
-
|
254 |
-
|
255 |
-
// Обработка кликов/касаний для ломания/установки
|
256 |
-
scene.onPointerDown = function (evt, pickResult) {
|
257 |
-
// evt.button === 0: Левая кнопка мыши / Основное касание
|
258 |
-
// evt.button === 1: Средняя кнопка мыши
|
259 |
-
// evt.button === 2: Правая кнопка мыши
|
260 |
-
|
261 |
-
const isBreakAction = (evt.button === 0);
|
262 |
-
const isPlaceAction = (evt.button === 2);
|
263 |
-
|
264 |
-
handleInteraction(isBreakAction, isPlaceAction, pickResult);
|
265 |
-
};
|
266 |
-
|
267 |
-
// Обработка нажатий мобильных кнопок
|
268 |
-
const placeButton = document.getElementById('placeButton');
|
269 |
-
const breakButton = document.getElementById('breakButton');
|
270 |
-
|
271 |
-
breakButton.addEventListener('pointerdown', (e) => {
|
272 |
-
e.preventDefault(); // Предотвращаем стандартное поведение (например, двойной тап зум)
|
273 |
-
const pickResult = scene.pick(scene.pointerX, scene.pointerY, (mesh) => mesh.isPickable);
|
274 |
-
handleInteraction(true, false, pickResult); // Имитируем "ломание"
|
275 |
-
});
|
276 |
-
placeButton.addEventListener('pointerdown', (e) => {
|
277 |
-
e.preventDefault();
|
278 |
-
const pickResult = scene.pick(scene.pointerX, scene.pointerY, (mesh) => mesh.isPickable);
|
279 |
-
handleInteraction(false, true, pickResult); // Имитируем "установку"
|
280 |
-
});
|
281 |
-
|
282 |
-
|
283 |
-
function handleInteraction(isBreak, isPlace, pickResult) {
|
284 |
-
if (pickResult.hit && pickResult.pickedMesh && blocks[pickResult.pickedMesh.name.replace('box_','')]) {
|
285 |
-
const pickedMesh = pickResult.pickedMesh;
|
286 |
-
const blockId = pickedMesh.name.replace('box_','');
|
287 |
-
|
288 |
-
// Ломание блока
|
289 |
-
if (isBreak) {
|
290 |
-
console.log("Breaking block:", blockId);
|
291 |
-
delete blocks[blockId]; // Удаляем из учета
|
292 |
-
pickedMesh.dispose(); // Удаляем со сцены
|
293 |
}
|
294 |
-
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
}
|
324 |
-
}
|
325 |
-
}
|
326 |
-
}
|
327 |
-
|
328 |
-
|
329 |
-
// --- Обновление информации на экране ---
|
330 |
-
const fpsCounter = document.getElementById('fpsCounter');
|
331 |
-
const blockInfo = document.getElementById('blockInfo');
|
332 |
-
scene.onBeforeRenderObservable.add(() => {
|
333 |
-
fpsCounter.textContent = `FPS: ${engine.getFps().toFixed()}`;
|
334 |
-
|
335 |
-
// Показываем информацию о блоке под курсором
|
336 |
-
const pickResult = scene.pick(canvas.width / 2, canvas.height / 2, (mesh) => mesh.isPickable);
|
337 |
-
if (pickResult.hit && pickResult.pickedMesh && pickResult.pickedMesh.metadata) {
|
338 |
-
const meta = pickResult.pickedMesh.metadata;
|
339 |
-
blockInfo.textContent = `Блок: ${meta.type} (${meta.x}, ${meta.y}, ${meta.z})`;
|
340 |
-
} else {
|
341 |
-
blockInfo.textContent = 'Наведите курсор на блок';
|
342 |
-
}
|
343 |
-
});
|
344 |
-
|
345 |
-
// --- Простое тач управление камерой (вращение) ---
|
346 |
-
let lastTouchX = null;
|
347 |
-
let lastTouchY = null;
|
348 |
-
let isTouching = false;
|
349 |
-
|
350 |
-
canvas.addEventListener('pointerdown', (evt) => {
|
351 |
-
if(evt.pointerType === 'touch') {
|
352 |
-
isTouching = true;
|
353 |
-
lastTouchX = evt.clientX;
|
354 |
-
lastTouchY = evt.clientY;
|
355 |
-
// Важно: Отключаем стандартное управление мышью на время тача, чтобы избежать конфликтов
|
356 |
-
// camera.detachControl(); // Это может быть слишком резко, нужно более тонкое управление
|
357 |
-
}
|
358 |
-
});
|
359 |
-
|
360 |
-
canvas.addEventListener('pointermove', (evt) => {
|
361 |
-
if(isTouching && evt.pointerType === 'touch') {
|
362 |
-
const deltaX = evt.clientX - lastTouchX;
|
363 |
-
const deltaY = evt.clientY - lastTouchY;
|
364 |
-
|
365 |
-
// Вращаем камеру вручную
|
366 |
-
// Коэффициенты подбираются экспериментально
|
367 |
-
camera.cameraRotation.y += deltaX * TOUCH_SENSITIVITY;
|
368 |
-
camera.cameraRotation.x += deltaY * TOUCH_SENSITIVITY;
|
369 |
-
|
370 |
-
// Ограничиваем вертикальное вращение (чтобы не смотреть вверх ногами)
|
371 |
-
camera.cameraRotation.x = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, camera.cameraRotation.x));
|
372 |
-
|
373 |
-
|
374 |
-
lastTouchX = evt.clientX;
|
375 |
-
lastTouchY = evt.clientY;
|
376 |
-
}
|
377 |
-
});
|
378 |
-
|
379 |
-
const touchEndHandler = (evt) => {
|
380 |
-
if(evt.pointerType === 'touch') {
|
381 |
-
isTouching = false;
|
382 |
-
lastTouchX = null;
|
383 |
-
lastTouchY = null;
|
384 |
-
// Возвращаем стандартное управление, если оно было отключено
|
385 |
-
// if(!camera._attachedCanvas) {
|
386 |
-
// camera.attachControl(canvas, true);
|
387 |
-
// }
|
388 |
-
}
|
389 |
-
}
|
390 |
-
canvas.addEventListener('pointerup', touchEndHandler);
|
391 |
-
canvas.addEventListener('pointerout', touchEndHandler); // Также сбрасываем при выходе за пределы канваса
|
392 |
-
canvas.addEventListener('pointercancel', touchEndHandler); // И при отмене касания
|
393 |
|
394 |
return scene;
|
395 |
};
|
396 |
|
397 |
-
// --- Вспомогательная функция для определения тач-устройства ---
|
398 |
-
function isTouchDevice() {
|
399 |
-
return (('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0));
|
400 |
-
}
|
401 |
-
|
402 |
// --- Запуск ---
|
403 |
-
|
404 |
-
|
405 |
-
|
406 |
-
|
407 |
-
|
408 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
409 |
});
|
410 |
|
|
|
|
|
411 |
window.addEventListener('resize', function () {
|
412 |
engine.resize();
|
413 |
});
|
414 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
415 |
</script>
|
|
|
416 |
</body>
|
417 |
</html>
|
|
|
3 |
<head>
|
4 |
<meta charset="UTF-8">
|
5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
6 |
+
<title>Metaverse Prototype - Babylon.js Demo</title>
|
7 |
+
<meta name="description" content="Прототип метавселенной, созданный с помощью Babylon.js. Демонстрация 3D-мира для презентации.">
|
8 |
+
|
9 |
+
<!-- Подключение Babylon.js -->
|
10 |
+
<script src="https://cdn.babylonjs.com/babylon.js"></script>
|
11 |
+
<!-- Опционально: загрузчики для моделей (например, glTF) -->
|
12 |
+
<script src="https://cdn.babylonjs.com/loaders/babylonjs.loaders.min.js"></script>
|
13 |
+
<!-- Опционально: библиотека для GUI Babylon.js (здесь используется HTML/CSS) -->
|
14 |
+
<!-- <script src="https://cdn.babylonjs.com/gui/babylon.gui.min.js"></script> -->
|
15 |
+
<!-- Опционально: Post-processing эффекты -->
|
16 |
+
<script src="https://cdn.babylonjs.com/postProcessesLibrary/babylon.postProcessLibrary.min.js"></script>
|
17 |
+
|
18 |
+
|
19 |
<style>
|
|
|
20 |
html, body {
|
21 |
overflow: hidden;
|
22 |
width: 100%;
|
23 |
height: 100%;
|
24 |
margin: 0;
|
25 |
padding: 0;
|
26 |
+
font-family: Arial, sans-serif;
|
27 |
+
color: #fff;
|
28 |
+
background-color: #000; /* Фон на время загрузки */
|
29 |
}
|
30 |
|
31 |
#renderCanvas {
|
32 |
width: 100%;
|
33 |
height: 100%;
|
34 |
+
touch-action: none; /* Отключает стандартные действия браузера при касании (важно для управления) */
|
35 |
+
}
|
36 |
+
|
37 |
+
/* --- Экран загрузки --- */
|
38 |
+
#loadingScreen {
|
39 |
+
position: absolute;
|
40 |
+
top: 0;
|
41 |
+
left: 0;
|
42 |
+
width: 100%;
|
43 |
+
height: 100%;
|
44 |
+
background-color: #1a1a1a;
|
45 |
+
display: flex;
|
46 |
+
justify-content: center;
|
47 |
+
align-items: center;
|
48 |
+
font-size: 2em;
|
49 |
+
color: #ccc;
|
50 |
+
z-index: 100;
|
51 |
+
transition: opacity 0.5s ease-out;
|
52 |
}
|
53 |
|
54 |
+
#loadingScreen.hidden {
|
55 |
+
opacity: 0;
|
56 |
+
pointer-events: none;
|
57 |
+
}
|
58 |
+
|
59 |
+
/* --- UI Оверлей (Пример) --- */
|
60 |
+
#ui-overlay {
|
61 |
position: absolute;
|
62 |
+
top: 15px;
|
63 |
+
left: 15px;
|
64 |
background-color: rgba(0, 0, 0, 0.5);
|
65 |
padding: 10px;
|
66 |
border-radius: 5px;
|
67 |
+
z-index: 10;
|
|
|
68 |
}
|
69 |
|
70 |
+
#ui-overlay h1 {
|
71 |
+
margin: 0 0 5px 0;
|
72 |
+
font-size: 1.2em;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
73 |
}
|
74 |
|
75 |
+
#ui-overlay p {
|
76 |
+
margin: 0;
|
77 |
+
font-size: 0.9em;
|
78 |
+
}
|
79 |
+
|
80 |
+
/* --- Мобильные элементы управления (визуальные) --- */
|
81 |
+
.mobile-controls {
|
82 |
position: absolute;
|
83 |
bottom: 20px;
|
84 |
+
width: 100%;
|
85 |
+
display: none; /* По умолчанию скрыты */
|
86 |
+
justify-content: space-between;
|
87 |
+
align-items: flex-end;
|
88 |
+
padding: 0 20px;
|
89 |
+
box-sizing: border-box;
|
90 |
+
z-index: 10;
|
91 |
+
pointer-events: none; /* Сами контейнеры не должны перехватывать события */
|
92 |
+
}
|
93 |
+
|
94 |
+
.joystick-area {
|
95 |
+
width: 120px;
|
96 |
+
height: 120px;
|
97 |
+
background-color: rgba(80, 80, 80, 0.4);
|
98 |
border-radius: 50%;
|
99 |
+
position: relative;
|
100 |
+
pointer-events: auto; /* Область джойстика должна быть интерактивна */
|
101 |
display: flex;
|
102 |
justify-content: center;
|
103 |
align-items: center;
|
104 |
+
}
|
105 |
+
|
106 |
+
.joystick-thumb { /* Просто визуальный индикатор центра */
|
107 |
+
width: 50px;
|
108 |
+
height: 50px;
|
109 |
+
background-color: rgba(200, 200, 200, 0.6);
|
110 |
+
border-radius: 50%;
|
111 |
+
}
|
112 |
+
|
113 |
+
.action-button {
|
114 |
+
width: 80px;
|
115 |
+
height: 80px;
|
116 |
+
background-color: rgba(0, 150, 255, 0.6);
|
117 |
+
border: 2px solid rgba(255, 255, 255, 0.7);
|
118 |
+
border-radius: 50%;
|
119 |
+
display: flex;
|
120 |
+
justify-content: center;
|
121 |
+
align-items: center;
|
122 |
+
font-size: 1.1em;
|
123 |
+
font-weight: bold;
|
124 |
+
color: #fff;
|
125 |
cursor: pointer;
|
126 |
+
pointer-events: auto; /* Кнопка должна быть интерактивна */
|
127 |
+
user-select: none; /* Предотвратить выделение текста на кнопке */
|
128 |
+
-webkit-user-select: none;
|
129 |
+
-moz-user-select: none;
|
130 |
+
-ms-user-select: none;
|
131 |
}
|
132 |
+
|
133 |
+
/* Показываем мобильные контролы только на тач-устройствах (простая проверка) */
|
134 |
+
body.is-mobile .mobile-controls {
|
135 |
+
display: flex;
|
|
|
|
|
|
|
|
|
|
|
136 |
}
|
137 |
|
138 |
</style>
|
|
|
|
|
|
|
|
|
139 |
</head>
|
140 |
<body>
|
141 |
+
|
142 |
+
<canvas id="renderCanvas"></canvas>
|
143 |
+
|
144 |
+
<div id="loadingScreen">
|
145 |
+
<div>Загрузка метавселенной...</div>
|
146 |
+
</div>
|
147 |
+
|
148 |
+
<div id="ui-overlay">
|
149 |
+
<h1>Metaverse Demo</h1>
|
150 |
+
<p>Используйте WASD/Стрелки + Мышь или Тач</p>
|
|
|
|
|
151 |
</div>
|
|
|
152 |
|
153 |
+
<!-- Визуальные элементы для мобильного управления -->
|
154 |
+
<div class="mobile-controls">
|
155 |
+
<div id="joystickArea" class="joystick-area">
|
156 |
+
<div class="joystick-thumb"></div>
|
157 |
+
</div>
|
158 |
+
<div id="actionButton" class="action-button">
|
159 |
+
Действие
|
160 |
+
</div>
|
161 |
</div>
|
162 |
|
163 |
<script>
|
164 |
const canvas = document.getElementById('renderCanvas');
|
165 |
+
const loadingScreen = document.getElementById('loadingScreen');
|
166 |
+
const mobileControls = document.querySelector('.mobile-controls');
|
167 |
+
const actionButton = document.getElementById('actionButton');
|
168 |
+
|
169 |
+
// Простая проверка на тач-устройство
|
170 |
+
const isMobile = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
|
171 |
+
if (isMobile) {
|
172 |
+
document.body.classList.add('is-mobile');
|
173 |
+
}
|
174 |
|
175 |
+
// --- Настройка Babylon.js ---
|
176 |
+
const engine = new BABYLON.Engine(canvas, true, {
|
177 |
+
preserveDrawingBuffer: true,
|
178 |
+
stencil: true,
|
179 |
+
antialias: true // Включаем сглаживание
|
180 |
+
});
|
181 |
+
engine.displayLoadingUI = function() { /* Отключаем стандартный загрузчик Babylon */ };
|
182 |
|
183 |
+
let scene;
|
|
|
184 |
|
185 |
// --- Создание сцены ---
|
186 |
+
const createScene = async function () {
|
187 |
+
scene = new BABYLON.Scene(engine);
|
188 |
+
scene.clearColor = new BABYLON.Color4(0.1, 0.1, 0.2, 1.0); // Цвет фона, если скайбокс не загрузится
|
|
|
|
|
189 |
|
190 |
// --- Камера ---
|
191 |
+
// UniversalCamera подходит для всех типов ввода (клавиатура, мышь, тач, геймпад)
|
192 |
+
const camera = new BABYLON.UniversalCamera("UniversalCamera", new BABYLON.Vector3(0, 1.8, -10), scene);
|
193 |
+
camera.setTarget(new BABYLON.Vector3(0, 1, 0));
|
194 |
camera.attachControl(canvas, true);
|
195 |
|
196 |
+
// Параметры движения камеры
|
197 |
+
camera.speed = 0.2; // Скорость движения
|
198 |
+
camera.angularSensibility = 4000; // Чувствительность мыши/тачпада
|
199 |
+
camera.touchAngularSensibility = 4000; // Чувствительность тач-поворота
|
200 |
+
camera.touchMoveSensibility = 250.0; // Чувствительность тач-перемещения (для джойстика лучше другая логика)
|
201 |
+
|
202 |
+
// Включаем гравитацию и коллизии для камеры
|
|
|
|
|
|
|
|
|
|
|
|
|
203 |
camera.applyGravity = true;
|
204 |
+
camera.ellipsoid = new BABYLON.Vector3(0.5, 0.9, 0.5); // Размеры "капсулы" игрока для коллизий
|
205 |
+
scene.gravity = new BABYLON.Vector3(0, -0.981, 0); // Стандартная гравитация
|
206 |
+
scene.collisionsEnabled = true; // Включаем систему коллизий в сцене
|
207 |
+
camera.checkCollisions = true; // Камера будет проверять коллизии
|
208 |
|
209 |
// --- Освещение ---
|
210 |
+
// HemisphericLight - простое общее освещение (сверху/снизу)
|
211 |
+
const light = new BABYLON.HemisphericLight("hemiLight", new BABYLON.Vector3(0, 1, 0), scene);
|
212 |
light.intensity = 0.8;
|
213 |
+
light.diffuse = new BABYLON.Color3(1, 1, 1); // Белый свет сверху
|
214 |
+
light.groundColor = new BABYLON.Color3(0.4, 0.4, 0.4); // Серый свет снизу
|
215 |
+
|
216 |
+
// DirectionalLight - имитация солнца (дает тени)
|
217 |
+
const dirLight = new BABYLON.DirectionalLight("dirLight", new BABYLON.Vector3(-0.5, -1, -0.5), scene);
|
218 |
+
dirLight.intensity = 0.6;
|
219 |
+
dirLight.position = new BABYLON.Vector3(20, 40, 20);
|
220 |
+
|
221 |
+
// --- Тени (могут быть ресурсоемкими, особенно на мобильных) ---
|
222 |
+
const shadowGenerator = new BABYLON.ShadowGenerator(1024, dirLight); // Размер карты теней
|
223 |
+
shadowGenerator.useBlurExponentialShadowMap = true; // Более мягкие тени
|
224 |
+
shadowGenerator.blurKernel = 32;
|
225 |
+
shadowGenerator.setDarkness(0.5); // Насколько темные тени
|
226 |
+
|
227 |
+
// --- Окружение: Скайбокс (Небо) ---
|
228 |
+
const skybox = BABYLON.MeshBuilder.CreateBox("skyBox", { size: 1000.0 }, scene);
|
229 |
+
const skyboxMaterial = new BABYLON.StandardMaterial("skyBoxMat", scene);
|
230 |
+
skyboxMaterial.backFaceCulling = false; // Видеть текстуру изнутри куба
|
231 |
+
// Используем процедурную текстуру неба для простоты (не нужны внешние файлы)
|
232 |
+
// Либо можно использовать CubeTexture с внешними изображениями:
|
233 |
+
// skyboxMaterial.reflectionTexture = new BABYLON.CubeTexture("URL_TO_TEXTURES/skybox", scene);
|
234 |
+
// skyboxMaterial.reflectionTexture.coordinatesMode = BABYLON.Texture.SKYBOX_MODE;
|
235 |
+
const skyMaterial = new BABYLON.SkyMaterial("skyMaterial", scene);
|
236 |
+
skyMaterial.backFaceCulling = false;
|
237 |
+
skyMaterial.turbidity = 10; // облачность
|
238 |
+
skyMaterial.luminance = 1; // яркость
|
239 |
+
skyMaterial.useSunPosition = true; // Использовать позицию "солнца"
|
240 |
+
skyMaterial.sunPosition = dirLight.position; // Установить позицию солнца как у направленного света
|
241 |
+
skybox.material = skyMaterial;
|
242 |
+
|
243 |
+
|
244 |
+
// --- Окружение: Земля ---
|
245 |
+
const ground = BABYLON.MeshBuilder.CreateGround("ground", { width: 100, height: 100, subdivisions: 2 }, scene);
|
246 |
+
const groundMaterial = new BABYLON.StandardMaterial("groundMat", scene);
|
247 |
+
// Простая текстура (можно заменить на URL к изображению)
|
248 |
+
// groundMaterial.diffuseTexture = new BABYLON.Texture("URL_TO_GROUND_TEXTURE.jpg", scene);
|
249 |
+
// groundMaterial.diffuseTexture.uScale = 10; // Повторение текстуры
|
250 |
+
// groundMaterial.diffuseTexture.vScale = 10;
|
251 |
+
groundMaterial.diffuseColor = new BABYLON.Color3(0.5, 0.7, 0.3); // Зеленоватый цвет
|
252 |
+
groundMaterial.specularColor = new BABYLON.Color3(0.1, 0.1, 0.1); // Небольшой блик
|
253 |
+
ground.material = groundMaterial;
|
254 |
+
ground.checkCollisions = true; // Земля участвует в коллизиях
|
255 |
+
ground.receiveShadows = true; // Земля может принимать тени
|
256 |
+
|
257 |
+
// --- Добавим несколько объектов в мир ---
|
258 |
+
|
259 |
+
// Сфера
|
260 |
+
const sphere = BABYLON.MeshBuilder.CreateSphere("sphere", { diameter: 2 }, scene);
|
261 |
+
sphere.position = new BABYLON.Vector3(5, 1, 5);
|
262 |
+
const sphereMaterial = new BABYLON.StandardMaterial("sphereMat", scene);
|
263 |
+
sphereMaterial.diffuseColor = new BABYLON.Color3(1, 0.5, 0); // Оранжевый
|
264 |
+
sphere.material = sphereMaterial;
|
265 |
+
sphere.checkCollisions = true;
|
266 |
+
shadowGenerator.addShadowCaster(sphere); // Сфера отбрасывает тень
|
267 |
+
|
268 |
+
// Куб
|
269 |
+
const box = BABYLON.MeshBuilder.CreateBox("box", { size: 2 }, scene);
|
270 |
+
box.position = new BABYLON.Vector3(-5, 1, -3);
|
271 |
+
const boxMaterial = new BABYLON.StandardMaterial("boxMat", scene);
|
272 |
+
boxMaterial.diffuseColor = new BABYLON.Color3(0.2, 0.5, 1); // Синий
|
273 |
+
box.material = boxMaterial;
|
274 |
+
box.checkCollisions = true;
|
275 |
+
shadowGenerator.addShadowCaster(box);
|
276 |
+
|
277 |
+
// "Здание" (просто вытянутый куб)
|
278 |
+
const building = BABYLON.MeshBuilder.CreateBox("building", { width: 5, height: 10, depth: 5 }, scene);
|
279 |
+
building.position = new BABYLON.Vector3(10, 5, -10);
|
280 |
+
const buildingMaterial = new BABYLON.StandardMaterial("buildingMat", scene);
|
281 |
+
buildingMaterial.diffuseColor = new BABYLON.Color3(0.8, 0.8, 0.8); // Светло-серый
|
282 |
+
building.material = buildingMaterial;
|
283 |
+
building.checkCollisions = true;
|
284 |
+
shadowGenerator.addShadowCaster(building);
|
285 |
+
|
286 |
+
|
287 |
+
// --- Интерактивность (Пример: клик по сфере) ---
|
288 |
+
sphere.actionManager = new BABYLON.ActionManager(scene);
|
289 |
+
sphere.actionManager.registerAction(
|
290 |
+
new BABYLON.ExecuteCodeAction(
|
291 |
+
BABYLON.ActionManager.OnPickTrigger, // Действие при клике/тапе
|
292 |
+
function (evt) {
|
293 |
+
console.log("Клик по сфере!");
|
294 |
+
// Пример действия: изменить цвет
|
295 |
+
sphereMaterial.diffuseColor = sphereMaterial.diffuseColor.equals(BABYLON.Color3.Red())
|
296 |
+
? new BABYLON.Color3(1, 0.5, 0) // Вернуть оранжевый
|
297 |
+
: BABYLON.Color3.Red(); // Сделать красным
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
298 |
}
|
299 |
+
)
|
300 |
+
);
|
301 |
+
|
302 |
+
// --- Пост-обработка (для "флагманской" графики) ---
|
303 |
+
// DefaultRenderingPipeline применяет несколько эффектов сразу
|
304 |
+
const pipeline = new BABYLON.DefaultRenderingPipeline(
|
305 |
+
"defaultPipeline", // Имя
|
306 |
+
true, // Использовать HDR текстуры (лучше качество)
|
307 |
+
scene, // Сцена
|
308 |
+
[camera] // Камеры, к которым применяется
|
309 |
+
);
|
310 |
+
|
311 |
+
// Настройки эффектов (можно настроить или отключить)
|
312 |
+
pipeline.samples = 4; // Сглаживание MSAA (если поддерживается)
|
313 |
+
|
314 |
+
pipeline.bloomEnabled = true; // Эффект свечения (Bloom)
|
315 |
+
pipeline.bloomThreshold = 0.7;
|
316 |
+
pipeline.bloomWeight = 0.5;
|
317 |
+
pipeline.bloomKernel = 64;
|
318 |
+
pipeline.bloomScale = 0.5;
|
319 |
+
|
320 |
+
pipeline.fxaaEnabled = true; // Сглаживание FXAA (быстрее MSAA)
|
321 |
+
|
322 |
+
// pipeline.imageProcessingEnabled = true; // Общая обработка изображения
|
323 |
+
// if (pipeline.imageProcessingEnabled) {
|
324 |
+
// pipeline.imageProcessing.contrast = 1.2;
|
325 |
+
// pipeline.imageProcessing.exposure = 1.0;
|
326 |
+
// // Можно добавить виньетку, зернистость и т.д.
|
327 |
+
// }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
328 |
|
329 |
return scene;
|
330 |
};
|
331 |
|
|
|
|
|
|
|
|
|
|
|
332 |
// --- Запуск ---
|
333 |
+
createScene().then((createdScene) => {
|
334 |
+
scene = createdScene;
|
335 |
+
// Убираем экран загрузки после инициализации сцены
|
336 |
+
engine.hideLoadingUI();
|
337 |
+
loadingScreen.classList.add('hidden');
|
338 |
+
|
339 |
+
// Основной цикл рендеринга
|
340 |
+
engine.runRenderLoop(function () {
|
341 |
+
if (scene) {
|
342 |
+
scene.render();
|
343 |
+
}
|
344 |
+
});
|
345 |
+
}).catch(error => {
|
346 |
+
console.error("Ошибка создания сцены:", error);
|
347 |
+
loadingScreen.innerHTML = "<div>Ошибка загрузки :(</div>";
|
348 |
});
|
349 |
|
350 |
+
|
351 |
+
// --- Обработка изменения размера окна ---
|
352 |
window.addEventListener('resize', function () {
|
353 |
engine.resize();
|
354 |
});
|
355 |
|
356 |
+
// --- Обработка мобильной кнопки "Действие" (пример) ---
|
357 |
+
if (isMobile && actionButton) {
|
358 |
+
actionButton.addEventListener('pointerdown', function(event) {
|
359 |
+
event.preventDefault(); // Предотвратить стандартное поведение (например, прокрутку)
|
360 |
+
console.log("Нажата кнопка Действие!");
|
361 |
+
// Здесь можно добавить логику действия:
|
362 |
+
// Например, эмулировать взаимодействие с объектом перед камерой
|
363 |
+
// или выполнить другое игровое действие.
|
364 |
+
|
365 |
+
// Визуальный фидбек нажатия
|
366 |
+
actionButton.style.backgroundColor = 'rgba(0, 100, 200, 0.8)';
|
367 |
+
});
|
368 |
+
actionButton.addEventListener('pointerup', function(event) {
|
369 |
+
actionButton.style.backgroundColor = 'rgba(0, 150, 255, 0.6)';
|
370 |
+
});
|
371 |
+
actionButton.addEventListener('pointerleave', function(event) { // Если палец ушел с кнопки
|
372 |
+
actionButton.style.backgroundColor = 'rgba(0, 150, 255, 0.6)';
|
373 |
+
});
|
374 |
+
}
|
375 |
+
|
376 |
+
// --- Дополнительно: Логика для виртуального джойстика (Упрощенная) ---
|
377 |
+
// UniversalCamera уже обрабатывает перетаскивание по экрану для движения/поворота.
|
378 |
+
// Этот код просто показывает визуальные элементы.
|
379 |
+
// Для полноценного джойстика потребовалась бы сложная логика отслеживания
|
380 |
+
// пальца в пределах joystickArea и преобразования смещения в векторы движения камеры.
|
381 |
+
|
382 |
</script>
|
383 |
+
|
384 |
</body>
|
385 |
</html>
|