Spaces:
Running
Running
Update index.html
Browse files- index.html +285 -301
index.html
CHANGED
|
@@ -1,355 +1,339 @@
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
-
<html>
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
-
|
| 6 |
-
<
|
| 7 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
<style>
|
| 9 |
-
/* Basic CSS Reset & Canvas Styling */
|
| 10 |
html, body {
|
|
|
|
|
|
|
|
|
|
| 11 |
margin: 0;
|
| 12 |
padding: 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
width: 100%;
|
| 14 |
height: 100%;
|
| 15 |
-
|
| 16 |
-
|
| 17 |
}
|
| 18 |
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
width: 100%;
|
| 21 |
height: 100%;
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
}
|
| 25 |
|
| 26 |
-
|
| 27 |
-
#
|
| 28 |
position: absolute;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
top: 50%;
|
| 30 |
left: 50%;
|
| 31 |
transform: translate(-50%, -50%);
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
z-index: 10;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
}
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
</style>
|
| 43 |
-
<!-- Babylon.js Core Library -->
|
| 44 |
-
<script src="https://cdn.babylonjs.com/babylon.js"></script>
|
| 45 |
-
<!-- Babylon.js Loaders (optional, if you were loading external models) -->
|
| 46 |
-
<!-- <script src="https://cdn.babylonjs.com/loaders/babylonjs.loaders.min.js"></script> -->
|
| 47 |
-
<!-- Babylon.js GUI Library (for virtual joysticks and UI elements) -->
|
| 48 |
-
<script src="https://cdn.babylonjs.com/gui/babylon.gui.min.js"></script>
|
| 49 |
</head>
|
| 50 |
<body>
|
| 51 |
-
|
|
|
|
| 52 |
<canvas id="renderCanvas"></canvas>
|
| 53 |
|
| 54 |
-
<!--
|
| 55 |
-
<div id="
|
| 56 |
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
const loadingIndicator = document.getElementById('loadingIndicator');
|
| 62 |
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
const engine = new BABYLON.Engine(canvas, true, { preserveDrawingBuffer: true, stencil: true, disableWebGL2Support: false });
|
| 66 |
-
engine.loadingUIText = "Initializing Scene..."; // Text shown during engine init
|
| 67 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
const createScene = () => {
|
| 69 |
const scene = new BABYLON.Scene(engine);
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
//
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
camera.
|
| 83 |
-
camera.
|
| 84 |
-
|
| 85 |
-
//
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
//
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
//
|
| 101 |
-
|
| 102 |
-
const
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
// --- Shadows ---
|
| 112 |
-
// Shadow generator needs a light source
|
| 113 |
-
const shadowGenerator = new BABYLON.ShadowGenerator(1024, dirLight); // 1024 is shadow map size
|
| 114 |
-
shadowGenerator.useExponentialShadowMap = true;
|
| 115 |
-
shadowGenerator.useBlurExponentialShadowMap = true; // Smoother shadows
|
| 116 |
-
shadowGenerator.blurKernel = 32;
|
| 117 |
-
shadowGenerator.darkness = 0.5;
|
| 118 |
-
|
| 119 |
-
// --- Environment ---
|
| 120 |
-
// Ground
|
| 121 |
-
const ground = BABYLON.MeshBuilder.CreateGround("ground", { width: 100, height: 100 }, scene);
|
| 122 |
-
ground.checkCollisions = true; // Ground should impede camera movement
|
| 123 |
-
const groundMat = new BABYLON.StandardMaterial("groundMat", scene);
|
| 124 |
-
// Simple procedural texture for the ground
|
| 125 |
-
const groundTexture = new BABYLON.Texture("https://www.babylonjs-playground.com/textures/grass.jpg", scene); // Example texture
|
| 126 |
-
groundTexture.uScale = 10;
|
| 127 |
-
groundTexture.vScale = 10;
|
| 128 |
-
groundMat.diffuseTexture = groundTexture;
|
| 129 |
-
groundMat.specularColor = new BABYLON.Color3(0.1, 0.1, 0.1); // Less shiny
|
| 130 |
-
ground.material = groundMat;
|
| 131 |
-
ground.receiveShadows = true; // Ground should receive shadows
|
| 132 |
-
|
| 133 |
-
// Physics for the ground (static object)
|
| 134 |
-
ground.physicsImpostor = new BABYLON.PhysicsImpostor(ground, BABYLON.PhysicsImpostor.BoxImpostor, { mass: 0, restitution: 0.9 }, scene);
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
// Skybox (creates a sense of space)
|
| 138 |
-
const skybox = BABYLON.MeshBuilder.CreateBox("skyBox", { size: 500.0 }, scene);
|
| 139 |
const skyboxMaterial = new BABYLON.StandardMaterial("skyBoxMat", scene);
|
| 140 |
-
skyboxMaterial.backFaceCulling = false; //
|
| 141 |
-
//
|
| 142 |
-
|
| 143 |
-
skyboxMaterial.reflectionTexture =
|
| 144 |
-
//
|
| 145 |
-
// skyboxMaterial.
|
| 146 |
-
//
|
| 147 |
-
|
| 148 |
-
// "data:image/png;base64,...", // pz
|
| 149 |
-
// "data:image/png;base64,...", // nx
|
| 150 |
-
// "data:image/png;base64,...", // ny
|
| 151 |
-
// "data:image/png;base64,...", // nz
|
| 152 |
-
// ], scene);
|
| 153 |
-
|
| 154 |
-
skyboxMaterial.reflectionTexture.coordinatesMode = BABYLON.Texture.SKYBOX_MODE;
|
| 155 |
-
skyboxMaterial.diffuseColor = new BABYLON.Color3(0, 0, 0); // Don't add color
|
| 156 |
-
skyboxMaterial.specularColor = new BABYLON.Color3(0, 0, 0); // Don't make it shiny
|
| 157 |
skybox.material = skyboxMaterial;
|
| 158 |
-
skybox.infiniteDistance = true; //
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
// ---
|
| 162 |
-
//
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
box.material =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
shadowGenerator.addShadowCaster(box);
|
| 183 |
-
box.physicsImpostor = new BABYLON.PhysicsImpostor(box, BABYLON.PhysicsImpostor.BoxImpostor, { mass: 1, restitution: 0.1 }, scene);
|
| 184 |
-
|
| 185 |
-
// Small Green Cylinder
|
| 186 |
-
const cylinder = BABYLON.MeshBuilder.CreateCylinder("cylinder", { height: 3, diameter: 1.5, tessellation: 24 }, scene);
|
| 187 |
-
cylinder.position = new BABYLON.Vector3(0, 1.5, 15);
|
| 188 |
-
cylinder.checkCollisions = true;
|
| 189 |
-
const cylMat = new BABYLON.StandardMaterial("cylMat", scene);
|
| 190 |
-
cylMat.diffuseColor = new BABYLON.Color3(0.2, 0.8, 0.3);
|
| 191 |
-
cylinder.material = cylMat;
|
| 192 |
shadowGenerator.addShadowCaster(cylinder);
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
// Add a few smaller dynamic boxes
|
| 196 |
-
for (let i = 0; i < 5; i++) {
|
| 197 |
-
const smallBox = BABYLON.MeshBuilder.CreateBox(`smallBox${i}`, { size: 1 }, scene);
|
| 198 |
-
smallBox.position = new BABYLON.Vector3(Math.random() * 10 - 5, 5 + i * 1.5, Math.random() * 10 - 5);
|
| 199 |
-
smallBox.checkCollisions = true;
|
| 200 |
-
const smallBoxMat = new BABYLON.StandardMaterial(`smallBoxMat${i}`, scene);
|
| 201 |
-
smallBoxMat.diffuseColor = BABYLON.Color3.Random(); // Random color
|
| 202 |
-
smallBox.material = smallBoxMat;
|
| 203 |
-
shadowGenerator.addShadowCaster(smallBox);
|
| 204 |
-
smallBox.physicsImpostor = new BABYLON.PhysicsImpostor(smallBox, BABYLON.PhysicsImpostor.BoxImpostor, { mass: 0.2, restitution: 0.4 }, scene);
|
| 205 |
-
}
|
| 206 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
const advancedTexture = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI("UI");
|
| 218 |
-
|
| 219 |
-
// --- Left Joystick (Movement) ---
|
| 220 |
-
leftJoystick = new BABYLON.GUI.VirtualJoystick({
|
| 221 |
-
limitToContainer: true, // Keep nub inside the container
|
| 222 |
-
alwaysVisible: true, // Show even when not touched
|
| 223 |
-
});
|
| 224 |
-
leftJoystick.setJoystickColor("rgba(180, 180, 180, 0.6)"); // Semi-transparent gray
|
| 225 |
-
leftJoystick.container.width = "150px";
|
| 226 |
-
leftJoystick.container.height = "150px";
|
| 227 |
-
leftJoystick.container.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
|
| 228 |
-
leftJoystick.container.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_BOTTOM;
|
| 229 |
-
leftJoystick.container.left = "50px"; // Offset from edge
|
| 230 |
-
leftJoystick.container.top = "-30px"; // Offset from bottom
|
| 231 |
-
advancedTexture.addControl(leftJoystick.container); // Add joystick container to UI
|
| 232 |
-
|
| 233 |
-
// --- Right Joystick (Look/Rotation - Optional, sometimes just touch-drag on screen is preferred) ---
|
| 234 |
-
// Let's implement touch-drag look instead of a right joystick for simplicity
|
| 235 |
-
let isPointerDown = false;
|
| 236 |
-
let pointerStartX = 0;
|
| 237 |
-
let pointerStartY = 0;
|
| 238 |
-
const lookSensitivity = 0.005; // Adjust sensitivity for touch look
|
| 239 |
-
|
| 240 |
-
scene.onPointerObservable.add((pointerInfo) => {
|
| 241 |
-
// Check if the pointer event is on the canvas and not over a GUI element (like the joystick)
|
| 242 |
-
const target = pointerInfo.pickInfo?.pickedMesh || pointerInfo.event.target;
|
| 243 |
-
const isOverGUI = pointerInfo.event.target && pointerInfo.event.target.closest('.babylonGUI'); // Rough check
|
| 244 |
-
|
| 245 |
-
if (target === canvas && !isOverGUI && !leftJoystick.isPointerDown(pointerInfo.event.pointerId) ) {
|
| 246 |
-
switch (pointerInfo.type) {
|
| 247 |
-
case BABYLON.PointerEventTypes.POINTERDOWN:
|
| 248 |
-
isPointerDown = true;
|
| 249 |
-
pointerStartX = pointerInfo.event.clientX;
|
| 250 |
-
pointerStartY = pointerInfo.event.clientY;
|
| 251 |
-
// Prevent default actions if needed
|
| 252 |
-
// pointerInfo.event.preventDefault();
|
| 253 |
-
break;
|
| 254 |
-
case BABYLON.PointerEventTypes.POINTERUP:
|
| 255 |
-
isPointerDown = false;
|
| 256 |
-
break;
|
| 257 |
-
case BABYLON.PointerEventTypes.POINTERMOVE:
|
| 258 |
-
if (isPointerDown) {
|
| 259 |
-
const deltaX = pointerInfo.event.clientX - pointerStartX;
|
| 260 |
-
const deltaY = pointerInfo.event.clientY - pointerStartY;
|
| 261 |
-
|
| 262 |
-
// Update camera rotation based on drag
|
| 263 |
-
// Invert Y-axis movement if desired (common for touch look)
|
| 264 |
-
camera.cameraRotation.y += deltaX * lookSensitivity;
|
| 265 |
-
camera.cameraRotation.x += deltaY * lookSensitivity * -1; // Adjust multiplier/sign as needed
|
| 266 |
-
|
| 267 |
-
// Clamp vertical rotation to prevent flipping upside down
|
| 268 |
-
camera.cameraRotation.x = Math.max(-Math.PI / 2.1, Math.min(Math.PI / 2.1, camera.cameraRotation.x));
|
| 269 |
-
|
| 270 |
-
// Update start position for next move calculation
|
| 271 |
-
pointerStartX = pointerInfo.event.clientX;
|
| 272 |
-
pointerStartY = pointerInfo.event.clientY;
|
| 273 |
-
}
|
| 274 |
-
break;
|
| 275 |
-
}
|
| 276 |
-
} else {
|
| 277 |
-
// Reset if pointer goes up outside canvas or over GUI
|
| 278 |
-
if (pointerInfo.type === BABYLON.PointerEventTypes.POINTERUP) {
|
| 279 |
-
isPointerDown = false;
|
| 280 |
-
}
|
| 281 |
-
}
|
| 282 |
-
});
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
// --- Link Left Joystick to Camera Movement ---
|
| 286 |
-
scene.onBeforeRenderObservable.add(() => {
|
| 287 |
-
if (leftJoystick && leftJoystick.pressed) {
|
| 288 |
-
// Get joystick delta vector
|
| 289 |
-
const joyX = leftJoystick.deltaPosition.x;
|
| 290 |
-
const joyY = leftJoystick.deltaPosition.y; // Y is forward/backward
|
| 291 |
-
|
| 292 |
-
// Calculate movement direction based on camera's orientation
|
| 293 |
-
const forward = camera.getDirection(BABYLON.Axis.Z);
|
| 294 |
-
const right = camera.getDirection(BABYLON.Axis.X);
|
| 295 |
-
|
| 296 |
-
// Note: Joystick Y is typically inverted (up is positive, forward is negative Z)
|
| 297 |
-
let moveDirection = forward.scale(joyY * -1).add(right.scale(joyX));
|
| 298 |
-
|
| 299 |
-
// Normalize and scale by camera speed and joystick magnitude
|
| 300 |
-
if (moveDirection.length() > 0.01) { // Avoid tiny movements
|
| 301 |
-
moveDirection.normalize();
|
| 302 |
-
// Apply movement - camera.cameraDirection is the vector to add to position
|
| 303 |
-
camera.cameraDirection.addInPlace(moveDirection.scale(camera.speed));
|
| 304 |
-
}
|
| 305 |
-
}
|
| 306 |
-
});
|
| 307 |
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
|
| 322 |
|
| 323 |
-
// ---
|
| 324 |
-
|
|
|
|
| 325 |
|
|
|
|
| 326 |
return scene;
|
| 327 |
-
}; //
|
| 328 |
|
| 329 |
-
// --- Create and Render ---
|
| 330 |
-
try {
|
| 331 |
-
const scene = createScene();
|
| 332 |
|
| 333 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
engine.runRenderLoop(() => {
|
| 335 |
-
|
| 336 |
-
scene.render();
|
| 337 |
-
}
|
| 338 |
});
|
| 339 |
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
loadingIndicator.style.color = "red";
|
| 349 |
-
}
|
| 350 |
-
}
|
| 351 |
|
| 352 |
-
}); // End of DOMContentLoaded listener
|
| 353 |
</script>
|
|
|
|
| 354 |
</body>
|
| 355 |
</html>
|
|
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
+
<html lang="ru">
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
| 6 |
+
<title>Прото-Метавселенная Демо (Babylon.js)</title>
|
| 7 |
+
<meta name="description" content="Демонстрация простого 3D-мира на Babylon.js в одном HTML файле.">
|
| 8 |
+
|
| 9 |
+
<!-- Подключение Babylon.js и его зависимостей из CDN -->
|
| 10 |
+
<script src="https://cdn.babylonjs.com/babylon.js"></script>
|
| 11 |
+
<!-- <script src="https://cdn.babylonjs.com/loaders/babylonjs.loaders.min.js"></script> -->
|
| 12 |
+
<!-- <script src="https://cdn.babylonjs.com/materialsLibrary/babylonjs.materials.min.js"></script> -->
|
| 13 |
+
<!-- Раскомментируйте loaders/materials, если будете загружать модели или использовать спец. материалы -->
|
| 14 |
+
|
| 15 |
<style>
|
|
|
|
| 16 |
html, body {
|
| 17 |
+
overflow: hidden;
|
| 18 |
+
width: 100%;
|
| 19 |
+
height: 100%;
|
| 20 |
margin: 0;
|
| 21 |
padding: 0;
|
| 22 |
+
font-family: Arial, sans-serif;
|
| 23 |
+
background-color: #000; /* Фон на случай долгой загрузки */
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
#renderCanvas {
|
| 27 |
width: 100%;
|
| 28 |
height: 100%;
|
| 29 |
+
touch-action: none; /* Важно для управления камерой на мобильных */
|
| 30 |
+
outline: none; /* Убирает рамку фокуса */
|
| 31 |
}
|
| 32 |
|
| 33 |
+
/* Стили для простого индикатора загрузки */
|
| 34 |
+
#loadingScreen {
|
| 35 |
+
position: absolute;
|
| 36 |
+
top: 0;
|
| 37 |
+
left: 0;
|
| 38 |
width: 100%;
|
| 39 |
height: 100%;
|
| 40 |
+
background-color: #222;
|
| 41 |
+
color: white;
|
| 42 |
+
display: flex;
|
| 43 |
+
justify-content: center;
|
| 44 |
+
align-items: center;
|
| 45 |
+
font-size: 2em;
|
| 46 |
+
z-index: 100;
|
| 47 |
+
transition: opacity 0.5s ease-out;
|
| 48 |
}
|
| 49 |
|
| 50 |
+
/* Стили для простого виртуального джойстика (пример) */
|
| 51 |
+
#joystickArea {
|
| 52 |
position: absolute;
|
| 53 |
+
bottom: 20px;
|
| 54 |
+
left: 20px;
|
| 55 |
+
width: 150px;
|
| 56 |
+
height: 150px;
|
| 57 |
+
background: rgba(255, 255, 255, 0.2);
|
| 58 |
+
border-radius: 50%;
|
| 59 |
+
display: none; /* Скрыт по умолчанию, можно показать для мобильных */
|
| 60 |
+
z-index: 10;
|
| 61 |
+
border: 2px solid rgba(255, 255, 255, 0.4);
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
#joystickThumb {
|
| 65 |
+
position: absolute;
|
| 66 |
+
width: 60px;
|
| 67 |
+
height: 60px;
|
| 68 |
+
background: rgba(255, 255, 255, 0.5);
|
| 69 |
+
border-radius: 50%;
|
| 70 |
top: 50%;
|
| 71 |
left: 50%;
|
| 72 |
transform: translate(-50%, -50%);
|
| 73 |
+
cursor: grab;
|
| 74 |
+
}
|
| 75 |
+
/* Стиль для кнопки действия (пример) */
|
| 76 |
+
#actionButton {
|
| 77 |
+
position: absolute;
|
| 78 |
+
bottom: 40px;
|
| 79 |
+
right: 40px;
|
| 80 |
+
width: 80px;
|
| 81 |
+
height: 80px;
|
| 82 |
+
background: rgba(0, 150, 255, 0.6);
|
| 83 |
+
border-radius: 50%;
|
| 84 |
+
display: none; /* Скрыт по умолчанию */
|
| 85 |
z-index: 10;
|
| 86 |
+
border: 2px solid rgba(255, 255, 255, 0.4);
|
| 87 |
+
color: white;
|
| 88 |
+
font-size: 1.5em;
|
| 89 |
+
text-align: center;
|
| 90 |
+
line-height: 80px; /* Центрирование текста по вертикали */
|
| 91 |
+
cursor: pointer;
|
| 92 |
+
user-select: none; /* Запретить выделение текста */
|
| 93 |
}
|
| 94 |
|
| 95 |
+
/* Медиа-запрос для отображения мобильных контролов */
|
| 96 |
+
@media (max-width: 768px) { /* или другое условие для тач-устройств */
|
| 97 |
+
#joystickArea {
|
| 98 |
+
/* display: block; */ /* Пока джойстик не реализован */
|
| 99 |
+
}
|
| 100 |
+
#actionButton {
|
| 101 |
+
/* display: block; */ /* Пока кнопка не реализована */
|
| 102 |
+
}
|
| 103 |
+
}
|
| 104 |
|
| 105 |
</style>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
</head>
|
| 107 |
<body>
|
| 108 |
+
|
| 109 |
+
<!-- Элемент Canvas для рендеринга 3D -->
|
| 110 |
<canvas id="renderCanvas"></canvas>
|
| 111 |
|
| 112 |
+
<!-- Экран загрузки -->
|
| 113 |
+
<div id="loadingScreen">Загрузка мира...</div>
|
| 114 |
|
| 115 |
+
<!-- Область для виртуального джойстика (упрощенно) -->
|
| 116 |
+
<div id="joystickArea">
|
| 117 |
+
<div id="joystickThumb"></div>
|
| 118 |
+
</div>
|
|
|
|
| 119 |
|
| 120 |
+
<!-- Кнопка действия (упрощенно) -->
|
| 121 |
+
<div id="actionButton">A</div>
|
|
|
|
|
|
|
| 122 |
|
| 123 |
+
|
| 124 |
+
<script>
|
| 125 |
+
// Получаем элементы DOM
|
| 126 |
+
const canvas = document.getElementById('renderCanvas');
|
| 127 |
+
const loadingScreen = document.getElementById('loadingScreen');
|
| 128 |
+
const joystickArea = document.getElementById('joystickArea');
|
| 129 |
+
const actionButton = document.getElementById('actionButton');
|
| 130 |
+
|
| 131 |
+
// Проверка поддержки WebGL
|
| 132 |
+
if (!BABYLON.Engine.isSupported()) {
|
| 133 |
+
window.alert('Ваш браузер не поддерживает WebGL, необходимый для Babylon.js!');
|
| 134 |
+
} else {
|
| 135 |
+
// Создание движка Babylon.js
|
| 136 |
+
const engine = new BABYLON.Engine(canvas, true, {
|
| 137 |
+
preserveDrawingBuffer: true, // Для скриншотов, если нужно
|
| 138 |
+
stencil: true,
|
| 139 |
+
antialias: true // Включаем сглаживание
|
| 140 |
+
}, true);
|
| 141 |
+
|
| 142 |
+
// Показываем экран загрузки Babylon по умолчанию
|
| 143 |
+
engine.displayLoadingUI();
|
| 144 |
+
|
| 145 |
+
// --- Создание сцены ---
|
| 146 |
const createScene = () => {
|
| 147 |
const scene = new BABYLON.Scene(engine);
|
| 148 |
+
scene.clearColor = new BABYLON.Color3(0.1, 0.1, 0.2); // Темно-синий фон
|
| 149 |
+
|
| 150 |
+
// --- Камера ---
|
| 151 |
+
// ArcRotateCamera - вращается вокруг цели, хорошо для мобильных и ПК
|
| 152 |
+
const camera = new BABYLON.ArcRotateCamera("camera",
|
| 153 |
+
-Math.PI / 2.5, // Начальный угол (alpha)
|
| 154 |
+
Math.PI / 3, // Начальный угол (beta)
|
| 155 |
+
20, // Начальный радиус (удаление)
|
| 156 |
+
new BABYLON.Vector3(0, 1, 0), // Точка, вокруг которой вращаемся
|
| 157 |
+
scene);
|
| 158 |
+
|
| 159 |
+
camera.attachControl(canvas, true); // Привязываем управление к canvas (мышь/тач)
|
| 160 |
+
camera.lowerRadiusLimit = 5; // Минимальное приближение
|
| 161 |
+
camera.upperRadiusLimit = 50; // Максимальное удаление
|
| 162 |
+
camera.wheelPrecision = 50; // Чувствительность колеса мыши/pinch-зума
|
| 163 |
+
// camera.useAutoRotationBehavior = true; // Автоматическое медленное вращение (опционально)
|
| 164 |
+
|
| 165 |
+
// --- Освещение ---
|
| 166 |
+
// Основной свет (имитация солнца)
|
| 167 |
+
const light = new BABYLON.DirectionalLight("dirLight", new BABYLON.Vector3(-0.5, -1, 0.3), scene);
|
| 168 |
+
light.position = new BABYLON.Vector3(20, 40, 20);
|
| 169 |
+
light.intensity = 1.0;
|
| 170 |
+
|
| 171 |
+
// Рассеянный свет (подсветка теней)
|
| 172 |
+
const hemiLight = new BABYLON.HemisphericLight("hemiLight", new BABYLON.Vector3(0, 1, 0), scene);
|
| 173 |
+
hemiLight.intensity = 0.4;
|
| 174 |
+
hemiLight.diffuse = new BABYLON.Color3(0.8, 0.85, 1); // Светлый холодный оттенок
|
| 175 |
+
hemiLight.groundColor = new BABYLON.Color3(0.3, 0.2, 0.1); // Теплый оттенок снизу
|
| 176 |
+
|
| 177 |
+
// --- Окружение ---
|
| 178 |
+
// Земля (плоскость)
|
| 179 |
+
const ground = BABYLON.MeshBuilder.CreateGround("ground", {width: 50, height: 50}, scene);
|
| 180 |
+
const groundMaterial = new BABYLON.PBRMetallicRoughnessMaterial("groundMat", scene);
|
| 181 |
+
groundMaterial.baseColor = new BABYLON.Color3(0.3, 0.5, 0.3); // Зеленоватый цвет
|
| 182 |
+
groundMaterial.metallic = 0.1; // Почти не металл
|
| 183 |
+
groundMaterial.roughness = 0.8; // Довольно шероховатая
|
| 184 |
+
ground.material = groundMaterial;
|
| 185 |
+
ground.receiveShadows = true; // Земля принимает тени
|
| 186 |
+
|
| 187 |
+
// Скайбокс (небо)
|
| 188 |
+
const skybox = BABYLON.MeshBuilder.CreateBox("skyBox", {size: 1000.0}, scene);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
const skyboxMaterial = new BABYLON.StandardMaterial("skyBoxMat", scene);
|
| 190 |
+
skyboxMaterial.backFaceCulling = false; // Видим внутренние стороны куба
|
| 191 |
+
// Простой процедурный скайбокс, чтобы не зависеть от внешних текстур
|
| 192 |
+
skyboxMaterial.reflectionTexture = new BABYLON.HDRCubeTexture("./textures/environment.env", scene, 512, false, true, false, true); // Замените на реальный путь к .env/.hdr если есть
|
| 193 |
+
skyboxMaterial.reflectionTexture.coordinatesMode = BABYLON.Texture.SKYBOX_MODE; // Режим скайбокса
|
| 194 |
+
// Или можно сделать просто цвет:
|
| 195 |
+
// skyboxMaterial.diffuseColor = new BABYLON.Color3(0.5, 0.8, 1.0); // Голубой
|
| 196 |
+
// skyboxMaterial.specularColor = new BABYLON.Color3(0, 0, 0); // Без бликов
|
| 197 |
+
skyboxMaterial.disableLighting = true; // Не зависит от освещения сцены
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
skybox.material = skyboxMaterial;
|
| 199 |
+
skybox.infiniteDistance = true; // Скайбокс всегда далеко
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
// --- Объекты в мире ---
|
| 203 |
+
// Сфера
|
| 204 |
+
const sphere = BABYLON.MeshBuilder.CreateSphere("sphere", {diameter: 2, segments: 32}, scene);
|
| 205 |
+
sphere.position.y = 1.5;
|
| 206 |
+
sphere.position.x = -5;
|
| 207 |
+
const sphereMaterial = new BABYLON.PBRMetallicRoughnessMaterial("sphereMat", scene);
|
| 208 |
+
sphereMaterial.baseColor = new BABYLON.Color3(0.8, 0.2, 0.2); // Красный
|
| 209 |
+
sphereMaterial.metallic = 0.8;
|
| 210 |
+
sphereMaterial.roughness = 0.2;
|
| 211 |
+
sphere.material = sphereMaterial;
|
| 212 |
+
|
| 213 |
+
// Куб
|
| 214 |
+
const box = BABYLON.MeshBuilder.CreateBox("box", {size: 2}, scene);
|
| 215 |
+
box.position.y = 1;
|
| 216 |
+
box.position.x = 0;
|
| 217 |
+
box.rotation.y = Math.PI / 5; // Немного повернем
|
| 218 |
+
const boxMaterial = new BABYLON.PBRMetallicRoughnessMaterial("boxMat", scene);
|
| 219 |
+
boxMaterial.baseColor = new BABYLON.Color3(0.2, 0.3, 0.8); // Синий
|
| 220 |
+
boxMaterial.metallic = 0.2;
|
| 221 |
+
boxMaterial.roughness = 0.6;
|
| 222 |
+
box.material = boxMaterial;
|
| 223 |
+
|
| 224 |
+
// Цилиндр
|
| 225 |
+
const cylinder = BABYLON.MeshBuilder.CreateCylinder("cylinder", {height: 3, diameter: 1.5}, scene);
|
| 226 |
+
cylinder.position.y = 1.5;
|
| 227 |
+
cylinder.position.x = 5;
|
| 228 |
+
const cylinderMaterial = new BABYLON.PBRMetallicRoughnessMaterial("cylMat", scene);
|
| 229 |
+
cylinderMaterial.baseColor = new BABYLON.Color3(0.9, 0.9, 0.9); // Белый
|
| 230 |
+
cylinderMaterial.metallic = 1.0; // Полностью металлический
|
| 231 |
+
cylinderMaterial.roughness = 0.1; // Очень гладкий
|
| 232 |
+
cylinder.material = cylinderMaterial;
|
| 233 |
+
|
| 234 |
+
// --- Тени ---
|
| 235 |
+
const shadowGenerator = new BABYLON.ShadowGenerator(1024, light); // 1024 - размер карты теней
|
| 236 |
+
shadowGenerator.useExponentialShadowMap = true; // Мягкие тени
|
| 237 |
+
// Добавляем объекты, отбрасывающие тень
|
| 238 |
+
shadowGenerator.addShadowCaster(sphere);
|
| 239 |
shadowGenerator.addShadowCaster(box);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
shadowGenerator.addShadowCaster(cylinder);
|
| 241 |
+
// Тень от скайбокса не нужна
|
| 242 |
+
// ground.receiveShadows уже установлено
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
|
| 244 |
+
// --- Простое взаимодействие (клик/тап по объекту) ---
|
| 245 |
+
scene.onPointerDown = (evt, pickResult) => {
|
| 246 |
+
// Проверяем, кликнули ли мы по какому-то мешу
|
| 247 |
+
if (pickResult.hit && pickResult.pickedMesh && pickResult.pickedMesh !== ground && pickResult.pickedMesh !== skybox) {
|
| 248 |
+
const pickedMesh = pickResult.pickedMesh;
|
| 249 |
+
console.log("Кликнули на:", pickedMesh.name);
|
| 250 |
|
| 251 |
+
// Простая анимация: пульсация размера
|
| 252 |
+
const animationScale = new BABYLON.Animation(
|
| 253 |
+
"scaleAnim", // Имя
|
| 254 |
+
"scaling", // Анимируемое свойство
|
| 255 |
+
30, // FPS
|
| 256 |
+
BABYLON.Animation.ANIMATIONTYPE_VECTOR3, // Тип данных
|
| 257 |
+
BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT // Режим проигрывания
|
| 258 |
+
);
|
| 259 |
|
| 260 |
+
const keys = [];
|
| 261 |
+
const originalScale = pickedMesh.scaling.clone();
|
| 262 |
+
const largeScale = originalScale.multiply(new BABYLON.Vector3(1.3, 1.3, 1.3));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
|
| 264 |
+
keys.push({ frame: 0, value: originalScale });
|
| 265 |
+
keys.push({ frame: 5, value: largeScale }); // Увеличиваем за 5 кадров
|
| 266 |
+
keys.push({ frame: 15, value: originalScale }); // Возвращаем за 10 кадров
|
| 267 |
+
|
| 268 |
+
animationScale.setKeys(keys);
|
| 269 |
+
|
| 270 |
+
// Добавляем easing для плавности
|
| 271 |
+
const easingFunction = new BABYLON.SineEase();
|
| 272 |
+
easingFunction.setEasingMode(BABYLON.EasingFunction.EASINGMODE_EASEINOUT);
|
| 273 |
+
animationScale.setEasingFunction(easingFunction);
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
pickedMesh.animations = []; // Очищаем предыдущие анимации (если есть)
|
| 277 |
+
pickedMesh.animations.push(animationScale);
|
| 278 |
+
|
| 279 |
+
scene.beginAnimation(pickedMesh, 0, 15, false, 1.0); // Запускаем анимацию
|
| 280 |
+
}
|
| 281 |
+
};
|
| 282 |
|
| 283 |
|
| 284 |
+
// --- Оптимизации (можно добавить больше) ---
|
| 285 |
+
scene.freezeMaterials(); // Оптимизация материалов после их настройки
|
| 286 |
+
scene.blockMaterialDirtyMechanism = true; // Блокировка проверки "грязных" материалов
|
| 287 |
|
| 288 |
+
// Возвращаем созданную сцену
|
| 289 |
return scene;
|
| 290 |
+
}; // --- Конец createScene ---
|
| 291 |
|
|
|
|
|
|
|
|
|
|
| 292 |
|
| 293 |
+
// Создаем сцену
|
| 294 |
+
const scene = createScene();
|
| 295 |
+
|
| 296 |
+
// Прячем стандартный экран загрузки Babylon и наш кастомный
|
| 297 |
+
scene.executeWhenReady(() => {
|
| 298 |
+
engine.hideLoadingUI();
|
| 299 |
+
if (loadingScreen) {
|
| 300 |
+
loadingScreen.style.opacity = '0';
|
| 301 |
+
// Можно удалить элемент после анимации исчезновения
|
| 302 |
+
setTimeout(() => {
|
| 303 |
+
loadingScreen.style.display = 'none';
|
| 304 |
+
}, 500); // Должно совпадать с transition в CSS
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
// Определяем, мобильное ли устройство (очень упрощенно!)
|
| 308 |
+
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
| 309 |
+
if (isMobile) {
|
| 310 |
+
// Показываем мобильные контролы, если нужно
|
| 311 |
+
// joystickArea.style.display = 'block';
|
| 312 |
+
// actionButton.style.display = 'block';
|
| 313 |
+
console.log("Обнаружено мобильное устройство (упрощенная проверка).");
|
| 314 |
+
// Здесь можно добавить логику для виртуального джойстика и кнопки,
|
| 315 |
+
// которая будет управлять камерой или персонажем (сейчас не реализовано).
|
| 316 |
+
// Babylon.js имеет VirtualJoysticksCamera, но она сложнее в настройке.
|
| 317 |
+
// ArcRotateCamera уже хорошо работает с тачем (перетаскивание - вращение, щипок - зум).
|
| 318 |
+
} else {
|
| 319 |
+
console.log("Обнаружено десктопное устройство.");
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
// Главный цикл рендеринга
|
| 323 |
engine.runRenderLoop(() => {
|
| 324 |
+
scene.render();
|
|
|
|
|
|
|
| 325 |
});
|
| 326 |
|
| 327 |
+
});
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
// Обработка изменения размера окна/экрана
|
| 331 |
+
window.addEventListener('resize', () => {
|
| 332 |
+
engine.resize();
|
| 333 |
+
});
|
| 334 |
+
} // Конец проверки WebGL
|
|
|
|
|
|
|
|
|
|
| 335 |
|
|
|
|
| 336 |
</script>
|
| 337 |
+
|
| 338 |
</body>
|
| 339 |
</html>
|