Kgshop commited on
Commit
51977cd
·
verified ·
1 Parent(s): 67b98fc

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +356 -363
app.py CHANGED
@@ -95,11 +95,11 @@ EDITOR_TEMPLATE = '''
95
  <head>
96
  <meta charset="UTF-8">
97
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
98
- <title>AAA Level Designer</title>
99
  <style>
100
  body { margin: 0; overflow: hidden; background-color: #000; color: white; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
101
  canvas { display: block; }
102
- #ui-container { position: absolute; top: 0; left: 0; height: 100%; z-index: 10; pointer-events: none; }
103
  #ui-panel {
104
  background: rgba(0, 0, 0, 0.75);
105
  padding: 15px;
@@ -116,27 +116,33 @@ EDITOR_TEMPLATE = '''
116
  .ui-group:last-child { border-bottom: none; }
117
  h3 { margin-top: 0; font-size: 1.1em; color: #00aaff; }
118
  label { display: block; margin-bottom: 5px; font-size: 0.9em; }
119
- input, select {
120
  width: 100%; padding: 8px; box-sizing: border-box; background: #222; border: 1px solid #555; color: white; border-radius: 4px;
121
  }
122
  button {
123
- width: 100%; padding: 10px; border: none; color: white; border-radius: 4px; cursor: pointer; font-weight: bold; margin-top: 10px;
124
  }
125
- .primary-button { background: #0077cc; }
126
- .primary-button:hover { background: #0099ff; }
127
- .secondary-button { background: #555; }
128
- .secondary-button:hover { background: #777; }
129
- .play-button { background: #28a745; }
130
- .play-button:hover { background: #218838; }
131
  .slider-container { margin-top: 10px; }
132
  input[type="range"] { width: 100%; }
133
  .radio-group label { display: inline-block; margin-right: 10px; cursor: pointer; }
134
  .radio-group input { margin-right: 5px; }
 
135
  #loading-spinner {
136
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
137
  border: 8px solid #f3f3f3; border-top: 8px solid #3498db; border-radius: 50%;
138
  width: 60px; height: 60px; animation: spin 1s linear infinite; display: none; z-index: 100;
139
  }
 
 
 
 
 
 
 
 
140
  #burger-menu {
141
  position: absolute; top: 15px; left: 15px; z-index: 20;
142
  display: none; width: 30px; height: 22px; cursor: pointer; pointer-events: auto;
@@ -150,19 +156,6 @@ EDITOR_TEMPLATE = '''
150
  #burger-menu span:nth-child(2) { top: 9px; }
151
  #burger-menu span:nth-child(3) { top: 18px; }
152
 
153
- #play-mode-ui {
154
- position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
155
- color: white; font-size: 2em; text-shadow: 2px 2px 4px #000;
156
- display: none; pointer-events: none;
157
- }
158
- #play-mode-ui span { font-size: 5em; }
159
-
160
- #play-instructions {
161
- position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);
162
- background: rgba(0,0,0,0.5); padding: 10px 20px; border-radius: 10px;
163
- display: none; pointer-events: none;
164
- }
165
-
166
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
167
 
168
  @media (max-width: 800px) {
@@ -174,32 +167,35 @@ EDITOR_TEMPLATE = '''
174
  </head>
175
  <body>
176
  <div id="ui-container">
177
- <div id="burger-menu"><span></span><span></span><span></span></div>
 
 
178
  <div id="ui-panel">
179
- <div class="ui-group">
180
- <h3>Игровой режим</h3>
181
- <button id="play-button" class="play-button">Запустить игру</button>
182
  </div>
183
  <div class="ui-group">
184
  <h3>Проекты</h3>
 
185
  <select id="project-list">
186
  <option value="">Выберите проект...</option>
187
  {% for project in projects %}
188
  <option value="{{ project }}">{{ project }}</option>
189
  {% endfor %}
190
  </select>
191
- <button id="load-project" class="primary-button">Загрузить</button>
192
  <label for="project-name">Имя проекта:</label>
193
  <input type="text" id="project-name" placeholder="new-level-01">
194
- <button id="save-project" class="primary-button">Сохранить</button>
195
  </div>
196
  <div class="ui-group">
197
  <h3>Ландшафт</h3>
198
- <label for="terrain-width">Ширина (X):</label>
199
- <input type="number" id="terrain-width" value="256">
200
- <label for="terrain-height">Длина (Z):</label>
201
- <input type="number" id="terrain-height" value="256">
202
- <button id="create-terrain" class="secondary-button">Создать новый ландшафт</button>
203
  </div>
204
  <div class="ui-group">
205
  <h3>Кисть</h3>
@@ -211,8 +207,8 @@ EDITOR_TEMPLATE = '''
211
  <label><input type="radio" name="brush-mode" value="place"> Объект</label>
212
  </div>
213
  <div class="slider-container">
214
- <label for="brush-size">Размер: <span id="brush-size-value">15</span></label>
215
- <input type="range" id="brush-size" min="1" max="50" value="15">
216
  </div>
217
  <div class="slider-container">
218
  <label for="brush-strength">Сила: <span id="brush-strength-value">0.5</span></label>
@@ -234,14 +230,24 @@ EDITOR_TEMPLATE = '''
234
  <div class="radio-group" id="object-selector">
235
  <label><input type="radio" name="object-type" value="grass" checked> Трава</label>
236
  </div>
237
- <button id="clear-objects" class="secondary-button">Очистить объекты</button>
238
  </div>
239
  </div>
240
  </div>
241
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  <div id="loading-spinner"></div>
243
- <div id="play-mode-ui"><span>+</span></div>
244
- <div id="play-instructions">W, A, S, D - Движение | SPACE - Прыжок | ESC - Выход</div>
245
 
246
  <script type="importmap">
247
  {
@@ -252,6 +258,7 @@ EDITOR_TEMPLATE = '''
252
  }
253
  }
254
  </script>
 
255
  <script type="module">
256
  import * as THREE from 'three';
257
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
@@ -261,173 +268,127 @@ EDITOR_TEMPLATE = '''
261
  import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
262
  import { SSAOPass } from 'three/addons/postprocessing/SSAOPass.js';
263
  import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
264
- import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
265
  import { createNoise2D } from 'simplex-noise';
266
 
267
- let scene, camera, renderer, editorControls, playControls, composer;
268
- let terrainMesh, brushHelper, player, sky, sun;
269
  let raycaster = new THREE.Raycaster();
270
  let mouse = new THREE.Vector2();
271
- let isInteracting = false, isPlayMode = false;
272
  const noise2D = createNoise2D();
273
- const clock = new THREE.Clock();
274
-
275
  let grassInstances;
276
  const MAX_GRASS_COUNT = 100000;
277
- const terrainDimensions = { width: 256, height: 256, segmentsX: 256, segmentsY: 256 };
278
-
279
- const playerVelocity = new THREE.Vector3();
280
- const playerDirection = new THREE.Vector3();
281
- const PLAYER_HEIGHT = 2.0;
282
- const GRAVITY = -20;
283
- const PLAYER_SPEED = 20;
284
- const JUMP_FORCE = 8;
285
- let onGround = false;
286
-
287
- const moveState = { forward: 0, right: 0 };
288
 
 
 
 
 
 
 
 
 
 
289
  const textureLoader = new THREE.TextureLoader();
290
- const textures = {};
291
- const pbrTextureUrls = {
292
- grass: { map: 'https://ambientcg.com/get/GenericGrass002_2K_Color.jpg', normal: 'https://ambientcg.com/get/GenericGrass002_2K_NormalGL.jpg', rough: 'https://ambientcg.com/get/GenericGrass002_2K_Roughness.jpg'},
293
- rock: { map: 'https://ambientcg.com/get/Rock030_2K_Color.jpg', normal: 'https://ambientcg.com/get/Rock030_2K_NormalGL.jpg', rough: 'https://ambientcg.com/get/Rock030_2K_Roughness.jpg'},
294
- dirt: { map: 'https://ambientcg.com/get/Ground037_2K_Color.jpg', normal: 'https://ambientcg.com/get/Ground037_2K_NormalGL.jpg', rough: 'https://ambientcg.com/get/Ground037_2K_Roughness.jpg'},
295
- snow: { map: 'https://ambientcg.com/get/Snow006_2K_Color.jpg', normal: 'https://ambientcg.com/get/Snow006_2K_NormalGL.jpg', rough: 'https://ambientcg.com/get/Snow006_2K_Roughness.jpg'},
296
- sand: { map: 'https://ambientcg.com/get/Sand004_2K_Color.jpg', normal: 'https://ambientcg.com/get/Sand004_2K_NormalGL.jpg', rough: 'https://ambientcg.com/get/Sand004_2K_Roughness.jpg'},
297
  };
298
-
299
- function loadTextures() {
300
- for (const key in pbrTextureUrls) {
301
- textures[key] = {
302
- map: textureLoader.load(pbrTextureUrls[key].map),
303
- normalMap: textureLoader.load(pbrTextureUrls[key].normal),
304
- roughnessMap: textureLoader.load(pbrTextureUrls[key].rough),
305
- };
306
- Object.values(textures[key]).forEach(tex => {
307
- tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
308
- tex.colorSpace = THREE.SRGBColorSpace;
309
- });
310
- }
311
- }
312
 
313
- const terrainMaterial = new THREE.MeshStandardMaterial({
314
- vertexColors: true,
315
- roughness: 0.8,
316
- metalness: 0.2,
317
- });
318
-
319
- terrainMaterial.onBeforeCompile = shader => {
320
- shader.uniforms.grassMap = { value: textures.grass.map };
321
- shader.uniforms.rockMap = { value: textures.rock.map };
322
- shader.uniforms.dirtMap = { value: textures.dirt.map };
323
- shader.uniforms.snowMap = { value: textures.snow.map };
324
- shader.uniforms.sandMap = { value: textures.sand.map };
325
-
326
- shader.uniforms.grassNormal = { value: textures.grass.normalMap };
327
- shader.uniforms.rockNormal = { value: textures.rock.normalMap };
328
- shader.uniforms.dirtNormal = { value: textures.dirt.normalMap };
329
- shader.uniforms.snowNormal = { value: textures.snow.normalMap };
330
- shader.uniforms.sandNormal = { value: textures.sand.normalMap };
331
-
332
- shader.uniforms.grassRough = { value: textures.grass.roughnessMap };
333
- shader.uniforms.rockRough = { value: textures.rock.roughnessMap };
334
- shader.uniforms.dirtRough = { value: textures.dirt.roughnessMap };
335
- shader.uniforms.snowRough = { value: textures.snow.roughnessMap };
336
- shader.uniforms.sandRough = { value: textures.sand.roughnessMap };
337
-
338
- shader.vertexShader = `
339
  varying vec2 vUv;
 
 
 
340
  varying vec4 vColor;
341
- ${shader.vertexShader}
342
- `.replace(
343
- `#include <uv_vertex>`,
344
- `#include <uv_vertex>
345
- vUv = uv;
346
- vColor = color;`
347
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
 
349
- shader.fragmentShader = `
350
  varying vec2 vUv;
351
  varying vec4 vColor;
 
 
 
 
352
 
353
- uniform sampler2D grassMap;
354
- uniform sampler2D rockMap;
355
- uniform sampler2D dirtMap;
356
- uniform sampler2D snowMap;
357
- uniform sampler2D sandMap;
358
-
359
- uniform sampler2D grassNormal;
360
- uniform sampler2D rockNormal;
361
- uniform sampler2D dirtNormal;
362
- uniform sampler2D snowNormal;
363
- uniform sampler2D sandNormal;
364
-
365
- uniform sampler2D grassRough;
366
- uniform sampler2D rockRough;
367
- uniform sampler2D dirtRough;
368
- uniform sampler2D snowRough;
369
- uniform sampler2D sandRough;
370
-
371
- vec4 blend(vec4 texture1, vec4 texture2, float alpha) {
372
- return mix(texture1, texture2, alpha);
 
 
 
 
 
373
  }
 
 
374
 
375
- ${shader.fragmentShader}
376
- `.replace(
377
- `#include <map_fragment>`,
378
- `
379
- vec2 uv_scaled = vUv * 50.0;
380
- vec4 grassTex = texture(grassMap, uv_scaled);
381
- vec4 rockTex = texture(rockMap, uv_scaled);
382
- vec4 dirtTex = texture(dirtMap, uv_scaled);
383
- vec4 snowTex = texture(snowMap, uv_scaled);
384
- vec4 sandTex = texture(sandMap, uv_scaled);
385
-
386
- vec4 diffuseColor = grassTex;
387
- diffuseColor = blend(diffuseColor, rockTex, vColor.r);
388
- diffuseColor = blend(diffuseColor, dirtTex, vColor.g);
389
- diffuseColor = blend(diffuseColor, snowTex, vColor.b);
390
- diffuseColor = blend(diffuseColor, sandTex, vColor.a);
391
- `
392
- ).replace(
393
- `#include <normal_fragment_maps>`,
394
- `
395
- vec3 grassN = texture(grassNormal, uv_scaled).xyz * 2.0 - 1.0;
396
- vec3 rockN = texture(rockNormal, uv_scaled).xyz * 2.0 - 1.0;
397
- vec3 dirtN = texture(dirtNormal, uv_scaled).xyz * 2.0 - 1.0;
398
- vec3 snowN = texture(snowNormal, uv_scaled).xyz * 2.0 - 1.0;
399
- vec3 sandN = texture(sandNormal, uv_scaled).xyz * 2.0 - 1.0;
400
-
401
- vec3 blendedNormal = grassN;
402
- blendedNormal = mix(blendedNormal, rockN, vColor.r);
403
- blendedNormal = mix(blendedNormal, dirtN, vColor.g);
404
- blendedNormal = mix(blendedNormal, snowN, vColor.b);
405
- blendedNormal = mix(blendedNormal, sandN, vColor.a);
406
-
407
- normal = normalize( vTBN * blendedNormal );
408
- `
409
- ).replace(
410
- `#include <roughnessmap_fragment>`,
411
- `
412
- float grassR = texture(grassRough, uv_scaled).r;
413
- float rockR = texture(rockRough, uv_scaled).r;
414
- float dirtR = texture(dirtRough, uv_scaled).r;
415
- float snowR = texture(snowRough, uv_scaled).r;
416
- float sandR = texture(sandRough, uv_scaled).r;
417
-
418
- float blendedRoughness = grassR;
419
- blendedRoughness = mix(blendedRoughness, rockR, vColor.r);
420
- blendedRoughness = mix(blendedRoughness, dirtR, vColor.g);
421
- blendedRoughness = mix(blendedRoughness, snowR, vColor.b);
422
- blendedRoughness = mix(blendedRoughness, sandR, vColor.a);
423
-
424
- roughnessFactor *= blendedRoughness;
425
- `
426
- );
427
- };
428
-
429
  function init() {
430
  scene = new THREE.Scene();
 
431
  camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000);
432
  camera.position.set(50, 50, 50);
433
 
@@ -440,75 +401,58 @@ EDITOR_TEMPLATE = '''
440
  renderer.outputColorSpace = THREE.SRGBColorSpace;
441
  document.body.appendChild(renderer.domElement);
442
 
443
- editorControls = new OrbitControls(camera, renderer.domElement);
444
- editorControls.enableDamping = true;
445
- editorControls.maxPolarAngle = Math.PI / 2.1;
446
 
447
- playControls = new PointerLockControls(camera, document.body);
448
- playControls.addEventListener('lock', () => {
449
- document.getElementById('play-mode-ui').style.display = 'none';
450
- document.getElementById('play-instructions').style.display = 'block';
451
- });
452
- playControls.addEventListener('unlock', () => {
453
- document.getElementById('play-mode-ui').style.display = 'block';
454
- document.getElementById('play-instructions').style.display = 'none';
455
- if(isPlayMode) togglePlayMode();
456
- });
457
-
458
  const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.5);
459
  scene.add(hemiLight);
460
 
461
- const dirLight = new THREE.DirectionalLight(0xffffff, 2.5);
462
  dirLight.position.set(100, 100, 50);
463
  dirLight.castShadow = true;
464
- dirLight.shadow.mapSize.width = 4096;
465
- dirLight.shadow.mapSize.height = 4096;
466
- dirLight.shadow.camera.top = 150;
467
- dirLight.shadow.camera.bottom = -150;
468
- dirLight.shadow.camera.left = -150;
469
- dirLight.shadow.camera.right = 150;
470
  dirLight.shadow.bias = -0.001;
471
  scene.add(dirLight);
 
472
 
473
  sky = new Sky();
474
- sky.scale.setScalar(1000);
475
  scene.add(sky);
476
-
477
  sun = new THREE.Vector3();
 
 
 
 
 
 
 
 
478
  const uniforms = sky.material.uniforms;
479
- uniforms['turbidity'].value = 10;
480
- uniforms['rayleigh'].value = 2;
481
- uniforms['mieCoefficient'].value = 0.005;
482
- uniforms['mieDirectionalG'].value = 0.8;
483
- const phi = THREE.MathUtils.degToRad(90 - 25);
484
- const theta = THREE.MathUtils.degToRad(180);
485
- sun.setFromSphericalCoords(1, phi, theta);
486
- uniforms['sunPosition'].value.copy(sun);
487
- dirLight.position.copy(sun).multiplyScalar(400);
488
-
489
- const brushGeometry = new THREE.CylinderGeometry(1, 1, 200, 32, 1, true);
490
  const brushMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00, wireframe: true, transparent: true, opacity: 0.5 });
491
  brushHelper = new THREE.Mesh(brushGeometry, brushMaterial);
492
- brushHelper.position.y = -100;
493
  brushHelper.visible = false;
494
  scene.add(brushHelper);
495
 
496
- const playerGeometry = new THREE.CapsuleGeometry(0.35, PLAYER_HEIGHT - 0.7, 32);
497
- const playerMaterial = new THREE.MeshStandardMaterial({ color: 0x00ff00, transparent: true, opacity: 0.5 });
498
- player = new THREE.Mesh(playerGeometry, playerMaterial);
499
- player.visible = false;
500
- scene.add(player);
501
-
502
  initFoliage();
503
-
504
- composer = new EffectComposer(renderer);
505
- composer.addPass(new RenderPass(scene, camera));
506
- const ssaoPass = new SSAOPass(scene, camera, window.innerWidth, window.innerHeight);
507
- ssaoPass.kernelRadius = 16;
508
- composer.addPass(ssaoPass);
509
- const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 0.5, 0.4, 0.85);
510
- composer.addPass(bloomPass);
511
- composer.addPass(new OutputPass());
512
 
513
  window.addEventListener('resize', onWindowResize);
514
  renderer.domElement.addEventListener('pointermove', onPointerMove);
@@ -516,41 +460,76 @@ EDITOR_TEMPLATE = '''
516
  renderer.domElement.addEventListener('pointerup', onPointerUp);
517
  renderer.domElement.addEventListener('pointerout', () => { brushHelper.visible = false; });
518
 
519
- document.addEventListener('keydown', onKeyDown);
520
- document.addEventListener('keyup', onKeyUp);
521
-
522
  setupUIListeners();
523
  animate();
524
  }
525
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
526
  function initFoliage() {
527
- if (grassInstances) scene.remove(grassInstances);
528
- const grassBlade = new THREE.PlaneGeometry(0.4, 2.0);
529
- grassBlade.translate(0, 1.0, 0);
530
- const grassMaterial = new THREE.MeshStandardMaterial({ color: 0x339933, side: THREE.DoubleSide, alphaTest: 0.5 });
 
 
 
 
531
  grassInstances = new THREE.InstancedMesh(grassBlade, grassMaterial, MAX_GRASS_COUNT);
532
- grassInstances.count = 0;
533
  grassInstances.castShadow = true;
 
534
  scene.add(grassInstances);
535
  }
536
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
537
  function setupUIListeners() {
538
  document.getElementById('create-terrain').addEventListener('click', () => {
539
  const width = parseInt(document.getElementById('terrain-width').value);
540
  const height = parseInt(document.getElementById('terrain-height').value);
541
  createTerrain(width, height);
542
  });
543
- document.getElementById('play-button').addEventListener('click', togglePlayMode);
544
  document.getElementById('save-project').addEventListener('click', saveProject);
545
  document.getElementById('load-project').addEventListener('click', loadProject);
546
- document.getElementById('brush-size').addEventListener('input', e => {
547
  document.getElementById('brush-size-value').textContent = e.target.value;
548
  updateBrushHelper();
549
  });
550
- document.getElementById('brush-strength').addEventListener('input', e => {
551
  document.getElementById('brush-strength-value').textContent = e.target.value;
552
  });
553
- document.getElementById('project-list').addEventListener('change', e => {
554
  document.getElementById('project-name').value = e.target.value;
555
  });
556
  document.getElementById('burger-menu').addEventListener('click', () => {
@@ -562,46 +541,45 @@ EDITOR_TEMPLATE = '''
562
  grassInstances.instanceMatrix.needsUpdate = true;
563
  }
564
  });
 
565
  }
566
 
567
  function togglePlayMode() {
568
  isPlayMode = !isPlayMode;
569
- const playButton = document.getElementById('play-button');
570
  const uiPanel = document.getElementById('ui-container');
571
-
572
  if (isPlayMode) {
573
- if(!terrainMesh) {
574
- alert('Создайте ландшафт перед входом в игровой режим');
575
- isPlayMode = false;
576
- return;
577
- }
578
- playButton.textContent = "Выйти из игры";
579
- playButton.classList.remove('play-button');
580
- editorControls.enabled = false;
581
- brushHelper.visible = false;
582
  uiPanel.style.display = 'none';
583
- playControls.lock();
 
584
 
585
- player.position.set(0, 50, 0);
586
- const startRay = new THREE.Raycaster(player.position, new THREE.Vector3(0, -1, 0));
587
- const intersect = startRay.intersectObject(terrainMesh);
588
- if (intersect.length > 0) {
589
- player.position.copy(intersect[0].point).y += PLAYER_HEIGHT / 2;
 
590
  }
 
591
  playerVelocity.set(0,0,0);
592
- player.visible = true;
 
593
 
594
  } else {
595
- playButton.textContent = "Запустить игру";
596
- playButton.classList.add('play-button');
597
- editorControls.enabled = true;
598
  uiPanel.style.display = 'block';
599
- playControls.unlock();
600
- player.visible = false;
601
  }
602
  }
603
-
604
- function showSpinner(show) { document.getElementById('loading-spinner').style.display = show ? 'block' : 'none'; }
 
 
605
 
606
  function createTerrain(width, height, terrainData = null) {
607
  if (terrainMesh) {
@@ -612,8 +590,8 @@ EDITOR_TEMPLATE = '''
612
 
613
  terrainDimensions.width = width;
614
  terrainDimensions.height = height;
615
- terrainDimensions.segmentsX = Math.min(256, Math.round(width));
616
- terrainDimensions.segmentsY = Math.min(256, Math.round(height));
617
 
618
  const geometry = new THREE.PlaneGeometry(
619
  terrainDimensions.width, terrainDimensions.height,
@@ -643,12 +621,14 @@ EDITOR_TEMPLATE = '''
643
  grassInstances.instanceMatrix.needsUpdate = true;
644
  }
645
  }
 
 
 
646
 
647
  terrainMesh = new THREE.Mesh(geometry, terrainMaterial);
 
648
  terrainMesh.receiveShadow = true;
649
  scene.add(terrainMesh);
650
-
651
- geometry.computeVertexNormals();
652
  }
653
 
654
  function onWindowResize() {
@@ -658,7 +638,9 @@ EDITOR_TEMPLATE = '''
658
  composer.setSize(window.innerWidth, window.innerHeight);
659
  }
660
 
661
- function getBrushMode() { return document.querySelector('input[name="brush-mode"]:checked').value; }
 
 
662
 
663
  function onPointerMove(event) {
664
  if (isPlayMode) return;
@@ -672,16 +654,18 @@ EDITOR_TEMPLATE = '''
672
 
673
  if (intersects.length > 0) {
674
  const intersectionPoint = intersects[0].point;
675
- brushHelper.position.x = intersectionPoint.x;
676
- brushHelper.position.z = intersectionPoint.z;
677
  brushHelper.visible = true;
678
  updateBrushHelper();
679
- editorControls.enabled = !isInteracting;
680
 
681
- if (isInteracting) applyBrush(intersects[0]);
 
 
682
  } else {
683
  brushHelper.visible = false;
684
- editorControls.enabled = true;
685
  }
686
  }
687
 
@@ -691,43 +675,24 @@ EDITOR_TEMPLATE = '''
691
  }
692
 
693
  function onPointerDown(event) {
694
- if (event.button === 0 && !isPlayMode && mouse.x > -0.9) {
 
695
  if (window.innerWidth < 800 && !document.getElementById('ui-panel').contains(event.target)) {
696
  document.getElementById('ui-panel').classList.remove('open');
697
  }
698
- isInteracting = true;
699
- editorControls.enabled = false;
700
  }
701
  }
702
 
703
  function onPointerUp(event) {
704
- if (event.button === 0 && !isPlayMode) {
705
- isInteracting = false;
706
- editorControls.enabled = true;
 
707
  }
708
  }
709
 
710
- function onKeyDown(event) {
711
- if (!isPlayMode) return;
712
- switch (event.code) {
713
- case 'KeyW': moveState.forward = 1; break;
714
- case 'KeyS': moveState.forward = -1; break;
715
- case 'KeyA': moveState.right = -1; break;
716
- case 'KeyD': moveState.right = 1; break;
717
- case 'Space': if(onGround) playerVelocity.y = JUMP_FORCE; break;
718
- }
719
- }
720
-
721
- function onKeyUp(event) {
722
- if (!isPlayMode) return;
723
- switch (event.code) {
724
- case 'KeyW': moveState.forward = 0; break;
725
- case 'KeyS': moveState.forward = 0; break;
726
- case 'KeyA': moveState.right = 0; break;
727
- case 'KeyD': moveState.right = 0; break;
728
- }
729
- }
730
-
731
  function applyBrush(intersection) {
732
  if (!terrainMesh) return;
733
  const brushMode = getBrushMode();
@@ -748,14 +713,17 @@ EDITOR_TEMPLATE = '''
748
 
749
  for (let i = 0; i < positions.count; i++) {
750
  vertex.fromBufferAttribute(positions, i);
751
- const distanceSq = vertex.distanceToSquared(center);
752
- if (distanceSq < brushSize * brushSize) {
753
- const falloff = 1 - Math.sqrt(distanceSq) / brushSize;
754
- positions.setY(i, positions.getY(i) + direction * falloff * brushStrength);
 
 
755
  }
756
  }
757
  positions.needsUpdate = true;
758
  terrainMesh.geometry.computeVertexNormals();
 
759
  }
760
 
761
  function roughenTerrain(center) {
@@ -763,18 +731,22 @@ EDITOR_TEMPLATE = '''
763
  const brushSize = parseFloat(document.getElementById('brush-size').value);
764
  const brushStrength = parseFloat(document.getElementById('brush-strength').value);
765
  const vertex = new THREE.Vector3();
 
766
 
767
  for (let i = 0; i < positions.count; i++) {
768
  vertex.fromBufferAttribute(positions, i);
769
- const distanceSq = vertex.distanceToSquared(center);
770
- if (distanceSq < brushSize * brushSize) {
771
- const falloff = 1 - Math.sqrt(distanceSq) / brushSize;
772
- const noiseVal = noise2D(vertex.x * 2, vertex.z * 2);
773
- positions.setY(i, positions.getY(i) + noiseVal * falloff * brushStrength);
 
 
774
  }
775
  }
776
  positions.needsUpdate = true;
777
  terrainMesh.geometry.computeVertexNormals();
 
778
  }
779
 
780
  function paintTexture(center) {
@@ -785,31 +757,34 @@ EDITOR_TEMPLATE = '''
785
  const textureType = document.querySelector('input[name="texture-type"]:checked').value;
786
 
787
  const targetColor = new THREE.Vector4(0,0,0,0);
788
- if(textureType === 'rock') targetColor.x = 1;
789
- else if (textureType === 'dirt') targetColor.y = 1;
790
- else if (textureType === 'snow') targetColor.z = 1;
791
- else if (textureType === 'sand') targetColor.w = 1;
792
 
793
  const vertex = new THREE.Vector3();
794
  const currentColor = new THREE.Vector4();
795
 
796
  for (let i = 0; i < positions.count; i++) {
797
  vertex.fromBufferAttribute(positions, i);
798
- const distanceSq = vertex.distanceToSquared(center);
799
- if (distanceSq < brushSize * brushSize) {
800
- const falloff = 1 - Math.sqrt(distanceSq) / brushSize;
801
  currentColor.fromBufferAttribute(colors, i);
802
 
803
  if (textureType === 'grass') {
804
- currentColor.lerp(new THREE.Vector4(0,0,0,0), falloff * brushStrength);
 
 
 
805
  } else {
806
- const otherLayersWeight = 1.0 - (currentColor.x + currentColor.y + currentColor.z + currentColor.w);
807
  currentColor.lerp(targetColor, falloff * brushStrength);
808
  }
809
 
810
  let sum = currentColor.x + currentColor.y + currentColor.z + currentColor.w;
811
- if (sum > 1.0) currentColor.divideScalar(sum);
812
-
 
813
  colors.setXYZW(i, currentColor.x, currentColor.y, currentColor.z, currentColor.w);
814
  }
815
  }
@@ -818,20 +793,26 @@ EDITOR_TEMPLATE = '''
818
 
819
  function placeObject(intersection) {
820
  const brushSize = parseFloat(document.getElementById('brush-size').value);
821
- const density = parseFloat(document.getElementById('brush-strength').value) * 5;
822
  const dummy = new THREE.Object3D();
823
 
824
  for (let i = 0; i < density; i++) {
825
  if (grassInstances.count >= MAX_GRASS_COUNT) break;
826
 
827
- const angle = Math.random() * Math.PI * 2;
828
- const radius = Math.random() * brushSize;
829
- const offset = new THREE.Vector3(Math.cos(angle) * radius, 0, Math.sin(angle) * radius);
 
830
 
831
- dummy.position.copy(intersection.point).add(offset);
 
 
 
 
832
 
833
  const placementRaycaster = new THREE.Raycaster(
834
- new THREE.Vector3(dummy.position.x, 100, dummy.position.z), new THREE.Vector3(0, -1, 0)
 
835
  );
836
  const placementIntersects = placementRaycaster.intersectObject(terrainMesh);
837
 
@@ -848,22 +829,26 @@ EDITOR_TEMPLATE = '''
848
 
849
  async function saveProject() {
850
  const projectName = document.getElementById('project-name').value.trim();
851
- if (!projectName || !terrainMesh) { alert("Введите имя проекта и создайте ландшафт."); return; }
 
852
  showSpinner(true);
853
 
854
  const grassMatrices = [];
855
- const matrix = new THREE.Matrix4();
856
  for (let i = 0; i < grassInstances.count; i++) {
 
857
  grassInstances.getMatrixAt(i, matrix);
858
  grassMatrices.push(...matrix.elements);
859
  }
860
 
861
  const projectData = {
862
  name: projectName,
863
- width: terrainDimensions.width, height: terrainDimensions.height,
 
864
  heights: Array.from(terrainMesh.geometry.attributes.position.array.filter((_, i) => i % 3 === 1)),
865
  colors: Array.from(terrainMesh.geometry.attributes.color.array),
866
- foliage: { grass: grassMatrices }
 
 
867
  };
868
 
869
  try {
@@ -873,11 +858,11 @@ EDITOR_TEMPLATE = '''
873
  body: JSON.stringify(projectData)
874
  });
875
  const result = await response.json();
876
- if (!response.ok) throw new Error(result.error);
877
  alert(`Проект '${projectName}' успешно сохранен!`);
878
 
879
  const projectList = document.getElementById('project-list');
880
- if (![...projectList.options].some(opt => opt.value === projectName)) {
881
  const newOption = document.createElement('option');
882
  newOption.value = newOption.textContent = projectName;
883
  projectList.appendChild(newOption);
@@ -891,13 +876,13 @@ EDITOR_TEMPLATE = '''
891
 
892
  async function loadProject() {
893
  const projectName = document.getElementById('project-list').value;
894
- if (!projectName) { alert("Выберите проект для загрузки."); return; }
895
  showSpinner(true);
896
 
897
  try {
898
  const response = await fetch(`/api/project/${projectName}`);
899
  const result = await response.json();
900
- if (!response.ok) throw new Error(result.error);
901
 
902
  createTerrain(result.width, result.height, result);
903
  document.getElementById('project-name').value = projectName;
@@ -911,32 +896,42 @@ EDITOR_TEMPLATE = '''
911
  }
912
 
913
  function updatePlayer(deltaTime) {
914
- const speed = (moveState.forward && moveState.right) ? PLAYER_SPEED * 0.707 : PLAYER_SPEED;
 
915
 
916
- playerVelocity.y += GRAVITY * deltaTime;
 
 
 
 
 
 
 
917
 
918
- playerDirection.set(moveState.right, 0, -moveState.forward);
919
- playerDirection.applyQuaternion(camera.quaternion).normalize();
920
 
921
- const move = playerDirection.multiplyScalar(speed * deltaTime);
922
- player.position.add(move);
923
 
 
 
924
  player.position.y += playerVelocity.y * deltaTime;
 
 
 
925
 
926
  raycaster.set(player.position, new THREE.Vector3(0, -1, 0));
927
- if(terrainMesh){
928
  const intersects = raycaster.intersectObject(terrainMesh);
929
  if (intersects.length > 0) {
930
- const distance = intersects[0].distance;
931
- if (distance < PLAYER_HEIGHT / 2) {
932
- player.position.y += (PLAYER_HEIGHT / 2 - distance);
933
  playerVelocity.y = 0;
934
- onGround = true;
935
  } else {
936
- onGround = false;
937
  }
938
- } else {
939
- onGround = false;
940
  }
941
  }
942
  camera.position.copy(player.position);
@@ -944,20 +939,18 @@ EDITOR_TEMPLATE = '''
944
 
945
  function animate() {
946
  requestAnimationFrame(animate);
947
- const deltaTime = Math.min(0.05, clock.getDelta());
948
 
949
- if (isPlayMode) {
950
  updatePlayer(deltaTime);
951
  } else {
952
- editorControls.update();
953
  }
954
-
955
  composer.render();
956
  }
957
 
958
- loadTextures();
959
  init();
960
- createTerrain(256, 256);
961
  </script>
962
  </body>
963
  </html>
 
95
  <head>
96
  <meta charset="UTF-8">
97
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
98
+ <title>3D Level Designer</title>
99
  <style>
100
  body { margin: 0; overflow: hidden; background-color: #000; color: white; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
101
  canvas { display: block; }
102
+ #ui-container { position: absolute; top: 0; left: 0; height: 100%; z-index: 10; pointer-events: none;}
103
  #ui-panel {
104
  background: rgba(0, 0, 0, 0.75);
105
  padding: 15px;
 
116
  .ui-group:last-child { border-bottom: none; }
117
  h3 { margin-top: 0; font-size: 1.1em; color: #00aaff; }
118
  label { display: block; margin-bottom: 5px; font-size: 0.9em; }
119
+ input[type="number"], input[type="text"], select {
120
  width: 100%; padding: 8px; box-sizing: border-box; background: #222; border: 1px solid #555; color: white; border-radius: 4px;
121
  }
122
  button {
123
+ width: 100%; padding: 10px; background: #0077cc; border: none; color: white; border-radius: 4px; cursor: pointer; font-weight: bold; margin-top: 10px;
124
  }
125
+ button:hover { background: #0099ff; }
126
+ button.play-button { background: #22aa22; }
127
+ button.play-button:hover { background: #33cc33; }
 
 
 
128
  .slider-container { margin-top: 10px; }
129
  input[type="range"] { width: 100%; }
130
  .radio-group label { display: inline-block; margin-right: 10px; cursor: pointer; }
131
  .radio-group input { margin-right: 5px; }
132
+
133
  #loading-spinner {
134
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
135
  border: 8px solid #f3f3f3; border-top: 8px solid #3498db; border-radius: 50%;
136
  width: 60px; height: 60px; animation: spin 1s linear infinite; display: none; z-index: 100;
137
  }
138
+ #blocker {
139
+ position: absolute; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); display: none;
140
+ }
141
+ #instructions {
142
+ width: 100%; height: 100%;
143
+ display: flex; flex-direction: column; justify-content: center; align-items: center;
144
+ text-align: center; font-size: 14px; cursor: pointer; color: white;
145
+ }
146
  #burger-menu {
147
  position: absolute; top: 15px; left: 15px; z-index: 20;
148
  display: none; width: 30px; height: 22px; cursor: pointer; pointer-events: auto;
 
156
  #burger-menu span:nth-child(2) { top: 9px; }
157
  #burger-menu span:nth-child(3) { top: 18px; }
158
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
160
 
161
  @media (max-width: 800px) {
 
167
  </head>
168
  <body>
169
  <div id="ui-container">
170
+ <div id="burger-menu">
171
+ <span></span><span></span><span></span>
172
+ </div>
173
  <div id="ui-panel">
174
+ <div class="ui-group">
175
+ <h3>Режим</h3>
176
+ <button id="play-mode-toggle" class="play-button">Играть</button>
177
  </div>
178
  <div class="ui-group">
179
  <h3>Проекты</h3>
180
+ <label for="project-list">Загрузить проект:</label>
181
  <select id="project-list">
182
  <option value="">Выберите проект...</option>
183
  {% for project in projects %}
184
  <option value="{{ project }}">{{ project }}</option>
185
  {% endfor %}
186
  </select>
187
+ <button id="load-project">Загрузить</button>
188
  <label for="project-name">Имя проекта:</label>
189
  <input type="text" id="project-name" placeholder="new-level-01">
190
+ <button id="save-project">Сохранить</button>
191
  </div>
192
  <div class="ui-group">
193
  <h3>Ландшафт</h3>
194
+ <label for="terrain-width">Ширина:</label>
195
+ <input type="number" id="terrain-width" value="100">
196
+ <label for="terrain-height">Длина:</label>
197
+ <input type="number" id="terrain-height" value="100">
198
+ <button id="create-terrain">Создать новый ландшафт</button>
199
  </div>
200
  <div class="ui-group">
201
  <h3>Кисть</h3>
 
207
  <label><input type="radio" name="brush-mode" value="place"> Объект</label>
208
  </div>
209
  <div class="slider-container">
210
+ <label for="brush-size">Размер: <span id="brush-size-value">10</span></label>
211
+ <input type="range" id="brush-size" min="1" max="50" value="10">
212
  </div>
213
  <div class="slider-container">
214
  <label for="brush-strength">Сила: <span id="brush-strength-value">0.5</span></label>
 
230
  <div class="radio-group" id="object-selector">
231
  <label><input type="radio" name="object-type" value="grass" checked> Трава</label>
232
  </div>
233
+ <button id="clear-objects">Очистить объекты</button>
234
  </div>
235
  </div>
236
  </div>
237
 
238
+ <div id="blocker">
239
+ <div id="instructions">
240
+ <p style="font-size:36px">Click to play</p>
241
+ <p>
242
+ Move: WASD<br/>
243
+ Jump: SPACE<br/>
244
+ Look: MOUSE
245
+ </p>
246
+ <p>Press ESC to exit</p>
247
+ </div>
248
+ </div>
249
+
250
  <div id="loading-spinner"></div>
 
 
251
 
252
  <script type="importmap">
253
  {
 
258
  }
259
  }
260
  </script>
261
+
262
  <script type="module">
263
  import * as THREE from 'three';
264
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
 
268
  import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
269
  import { SSAOPass } from 'three/addons/postprocessing/SSAOPass.js';
270
  import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
 
271
  import { createNoise2D } from 'simplex-noise';
272
 
273
+ let scene, camera, renderer, orbitControls, pointerLockControls, composer, brushHelper, terrainMesh, sky, sun;
 
274
  let raycaster = new THREE.Raycaster();
275
  let mouse = new THREE.Vector2();
276
+ let isEditing = false;
277
  const noise2D = createNoise2D();
278
+
 
279
  let grassInstances;
280
  const MAX_GRASS_COUNT = 100000;
281
+ const terrainDimensions = { width: 100, height: 100, segmentsX: 100, segmentsY: 100 };
 
 
 
 
 
 
 
 
 
 
282
 
283
+ let isPlayMode = false;
284
+ let player, playerVelocity = new THREE.Vector3(), playerOnGround = false;
285
+ const playerHeight = 1.8;
286
+ const playerSpeed = 20.0;
287
+ const playerJumpHeight = 8.0;
288
+ const gravity = -30.0;
289
+ const keyStates = {};
290
+ const clock = new THREE.Clock();
291
+
292
  const textureLoader = new THREE.TextureLoader();
293
+ const loadTexture = (url) => {
294
+ const tex = textureLoader.load(url);
295
+ tex.wrapS = THREE.RepeatWrapping;
296
+ tex.wrapT = THREE.RepeatWrapping;
297
+ tex.anisotropy = 16;
298
+ return tex;
 
299
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
 
301
+ const textures = {
302
+ grass: loadTexture('https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/textures/terrain/grasslight-big.jpg'),
303
+ rock: loadTexture('https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/textures/terrain/rock.jpg'),
304
+ dirt: loadTexture('https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/textures/terrain/sand-512.jpg'),
305
+ snow: loadTexture('https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/textures/snow.jpg'),
306
+ sand: loadTexture('https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/textures/terrain/sand-512.jpg'),
307
+ grassNormal: loadTexture('https://raw.githubusercontent.com/mrdoob/three.js/master/examples/textures/terrain/grasslight-big-nm.jpg'),
308
+ rockNormal: loadTexture('https://raw.githubusercontent.com/mrdoob/three.js/master/examples/textures/terrain/rock-nm.jpg')
309
+ };
310
+
311
+ const terrainMaterial = new THREE.ShaderMaterial({
312
+ uniforms: {
313
+ grassTexture: { value: textures.grass },
314
+ rockTexture: { value: textures.rock },
315
+ dirtTexture: { value: textures.dirt },
316
+ snowTexture: { value: textures.snow },
317
+ sandTexture: { value: textures.sand },
318
+ grassNormalMap: { value: textures.grassNormal },
319
+ rockNormalMap: { value: textures.rockNormal },
320
+ lightDirection: { value: new THREE.Vector3(0.5, 0.5, 0.5).normalize() }
321
+ },
322
+ vertexShader: `
 
 
 
 
323
  varying vec2 vUv;
324
+ varying vec3 vNormal;
325
+ varying vec3 vViewPosition;
326
+ attribute vec4 color;
327
  varying vec4 vColor;
328
+ attribute vec4 tangent;
329
+ varying vec3 vTangent;
330
+ varying vec3 vBitangent;
331
+
332
+ void main() {
333
+ vUv = uv;
334
+ vColor = color;
335
+ vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
336
+ vViewPosition = -mvPosition.xyz;
337
+ vNormal = normalize(normalMatrix * normal);
338
+ vTangent = normalize(normalMatrix * tangent.xyz);
339
+ vBitangent = normalize(cross(vNormal, vTangent) * tangent.w);
340
+ gl_Position = projectionMatrix * mvPosition;
341
+ }
342
+ `,
343
+ fragmentShader: `
344
+ uniform sampler2D grassTexture;
345
+ uniform sampler2D rockTexture;
346
+ uniform sampler2D dirtTexture;
347
+ uniform sampler2D snowTexture;
348
+ uniform sampler2D sandTexture;
349
+ uniform sampler2D grassNormalMap;
350
+ uniform sampler2D rockNormalMap;
351
+ uniform vec3 lightDirection;
352
 
 
353
  varying vec2 vUv;
354
  varying vec4 vColor;
355
+ varying vec3 vNormal;
356
+ varying vec3 vViewPosition;
357
+ varying vec3 vTangent;
358
+ varying vec3 vBitangent;
359
 
360
+ void main() {
361
+ vec2 uv_scaled = vUv * 30.0;
362
+
363
+ vec4 grass = texture2D(grassTexture, uv_scaled);
364
+ vec4 rock = texture2D(rockTexture, uv_scaled);
365
+ vec4 dirt = texture2D(dirtTexture, uv_scaled);
366
+ vec4 snow = texture2D(snowTexture, uv_scaled);
367
+ vec4 sand = texture2D(sandTexture, uv_scaled);
368
+
369
+ vec3 finalColor = grass.rgb;
370
+ finalColor = mix(finalColor, rock.rgb, vColor.r);
371
+ finalColor = mix(finalColor, dirt.rgb, vColor.g);
372
+ finalColor = mix(finalColor, snow.rgb, vColor.b);
373
+ finalColor = mix(finalColor, sand.rgb, vColor.a);
374
+
375
+ mat3 tbn = mat3(vTangent, vBitangent, vNormal);
376
+
377
+ vec3 grassNormal = texture2D(grassNormalMap, uv_scaled).xyz * 2.0 - 1.0;
378
+ vec3 rockNormal = texture2D(rockNormalMap, uv_scaled).xyz * 2.0 - 1.0;
379
+
380
+ vec3 blendedNormal = normalize(mix(grassNormal, rockNormal, vColor.r));
381
+ vec3 normal = normalize(tbn * blendedNormal);
382
+
383
+ float lighting = dot(normal, lightDirection) * 0.5 + 0.5;
384
+ gl_FragColor = vec4(finalColor * lighting, 1.0);
385
  }
386
+ `
387
+ });
388
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
  function init() {
390
  scene = new THREE.Scene();
391
+
392
  camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000);
393
  camera.position.set(50, 50, 50);
394
 
 
401
  renderer.outputColorSpace = THREE.SRGBColorSpace;
402
  document.body.appendChild(renderer.domElement);
403
 
404
+ orbitControls = new OrbitControls(camera, renderer.domElement);
405
+ orbitControls.enableDamping = true;
406
+ orbitControls.maxPolarAngle = Math.PI / 2.1;
407
 
 
 
 
 
 
 
 
 
 
 
 
408
  const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.5);
409
  scene.add(hemiLight);
410
 
411
+ const dirLight = new THREE.DirectionalLight(0xffffff, 1.5);
412
  dirLight.position.set(100, 100, 50);
413
  dirLight.castShadow = true;
414
+ dirLight.shadow.mapSize.width = 2048;
415
+ dirLight.shadow.mapSize.height = 2048;
416
+ dirLight.shadow.camera.top = 100;
417
+ dirLight.shadow.camera.bottom = -100;
418
+ dirLight.shadow.camera.left = -100;
419
+ dirLight.shadow.camera.right = 100;
420
  dirLight.shadow.bias = -0.001;
421
  scene.add(dirLight);
422
+ terrainMaterial.uniforms.lightDirection.value = dirLight.position.clone().normalize();
423
 
424
  sky = new Sky();
425
+ sky.scale.setScalar(450000);
426
  scene.add(sky);
 
427
  sun = new THREE.Vector3();
428
+ const effectController = {
429
+ turbidity: 10,
430
+ rayleigh: 3,
431
+ mieCoefficient: 0.005,
432
+ mieDirectionalG: 0.7,
433
+ elevation: 4,
434
+ azimuth: 180,
435
+ };
436
  const uniforms = sky.material.uniforms;
437
+ uniforms[ 'turbidity' ].value = effectController.turbidity;
438
+ uniforms[ 'rayleigh' ].value = effectController.rayleigh;
439
+ uniforms[ 'mieCoefficient' ].value = effectController.mieCoefficient;
440
+ uniforms[ 'mieDirectionalG' ].value = effectController.mieDirectionalG;
441
+ const phi = THREE.MathUtils.degToRad( 90 - effectController.elevation );
442
+ const theta = THREE.MathUtils.degToRad( effectController.azimuth );
443
+ sun.setFromSphericalCoords( 1, phi, theta );
444
+ uniforms[ 'sunPosition' ].value.copy( sun );
445
+ dirLight.position.copy(sun).multiplyScalar(100);
446
+
447
+ const brushGeometry = new THREE.CylinderGeometry(1, 1, 100, 32, 1, true);
448
  const brushMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00, wireframe: true, transparent: true, opacity: 0.5 });
449
  brushHelper = new THREE.Mesh(brushGeometry, brushMaterial);
 
450
  brushHelper.visible = false;
451
  scene.add(brushHelper);
452
 
 
 
 
 
 
 
453
  initFoliage();
454
+ initPostprocessing();
455
+ initPlayer();
 
 
 
 
 
 
 
456
 
457
  window.addEventListener('resize', onWindowResize);
458
  renderer.domElement.addEventListener('pointermove', onPointerMove);
 
460
  renderer.domElement.addEventListener('pointerup', onPointerUp);
461
  renderer.domElement.addEventListener('pointerout', () => { brushHelper.visible = false; });
462
 
 
 
 
463
  setupUIListeners();
464
  animate();
465
  }
466
 
467
+ function initPostprocessing() {
468
+ composer = new EffectComposer(renderer);
469
+ const renderPass = new RenderPass(scene, camera);
470
+ composer.addPass(renderPass);
471
+
472
+ const ssaoPass = new SSAOPass(scene, camera, window.innerWidth, window.innerHeight);
473
+ ssaoPass.kernelRadius = 16;
474
+ ssaoPass.minDistance = 0.005;
475
+ ssaoPass.maxDistance = 0.1;
476
+ composer.addPass(ssaoPass);
477
+
478
+ const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 0.5, 0.4, 0.85);
479
+ composer.addPass(bloomPass);
480
+ }
481
+
482
  function initFoliage() {
483
+ if (grassInstances) {
484
+ scene.remove(grassInstances);
485
+ grassInstances.geometry.dispose();
486
+ grassInstances.material.dispose();
487
+ }
488
+ const grassBlade = new THREE.PlaneGeometry(0.3, 1.8);
489
+ grassBlade.translate(0, 0.9, 0);
490
+ const grassMaterial = new THREE.MeshLambertMaterial({ color: 0x339933, side: THREE.DoubleSide, alphaTest: 0.5 });
491
  grassInstances = new THREE.InstancedMesh(grassBlade, grassMaterial, MAX_GRASS_COUNT);
 
492
  grassInstances.castShadow = true;
493
+ grassInstances.count = 0;
494
  scene.add(grassInstances);
495
  }
496
 
497
+ function initPlayer() {
498
+ player = new THREE.Group();
499
+ scene.add(player);
500
+
501
+ pointerLockControls = new PointerLockControls(camera, document.body);
502
+ const blocker = document.getElementById('blocker');
503
+ const instructions = document.getElementById('instructions');
504
+ instructions.addEventListener('click', () => pointerLockControls.lock());
505
+ pointerLockControls.addEventListener('lock', () => { instructions.style.display = 'none'; blocker.style.display = 'none'; });
506
+ pointerLockControls.addEventListener('unlock', () => { blocker.style.display = 'block'; instructions.style.display = ''; });
507
+
508
+ document.addEventListener('keydown', (event) => { keyStates[event.code] = true; });
509
+ document.addEventListener('keyup', (event) => { keyStates[event.code] = false; });
510
+ document.addEventListener('keydown', (event) => {
511
+ if (isPlayMode && event.code === 'Escape') {
512
+ togglePlayMode();
513
+ }
514
+ });
515
+ }
516
+
517
  function setupUIListeners() {
518
  document.getElementById('create-terrain').addEventListener('click', () => {
519
  const width = parseInt(document.getElementById('terrain-width').value);
520
  const height = parseInt(document.getElementById('terrain-height').value);
521
  createTerrain(width, height);
522
  });
 
523
  document.getElementById('save-project').addEventListener('click', saveProject);
524
  document.getElementById('load-project').addEventListener('click', loadProject);
525
+ document.getElementById('brush-size').addEventListener('input', (e) => {
526
  document.getElementById('brush-size-value').textContent = e.target.value;
527
  updateBrushHelper();
528
  });
529
+ document.getElementById('brush-strength').addEventListener('input', (e) => {
530
  document.getElementById('brush-strength-value').textContent = e.target.value;
531
  });
532
+ document.getElementById('project-list').addEventListener('change', (e) => {
533
  document.getElementById('project-name').value = e.target.value;
534
  });
535
  document.getElementById('burger-menu').addEventListener('click', () => {
 
541
  grassInstances.instanceMatrix.needsUpdate = true;
542
  }
543
  });
544
+ document.getElementById('play-mode-toggle').addEventListener('click', togglePlayMode);
545
  }
546
 
547
  function togglePlayMode() {
548
  isPlayMode = !isPlayMode;
549
+ const toggleButton = document.getElementById('play-mode-toggle');
550
  const uiPanel = document.getElementById('ui-container');
551
+
552
  if (isPlayMode) {
553
+ toggleButton.textContent = "Редактор";
554
+ toggleButton.classList.remove('play-button');
 
 
 
 
 
 
 
555
  uiPanel.style.display = 'none';
556
+ brushHelper.visible = false;
557
+ orbitControls.enabled = false;
558
 
559
+ const spawnRaycaster = new THREE.Raycaster(new THREE.Vector3(0, 100, 0), new THREE.Vector3(0, -1, 0));
560
+ const intersects = spawnRaycaster.intersectObject(terrainMesh);
561
+ if (intersects.length > 0) {
562
+ player.position.copy(intersects[0].point);
563
+ } else {
564
+ player.position.set(0, 0, 0);
565
  }
566
+ player.position.y += playerHeight;
567
  playerVelocity.set(0,0,0);
568
+
569
+ pointerLockControls.lock();
570
 
571
  } else {
572
+ toggleButton.textContent = "Играть";
573
+ toggleButton.classList.add('play-button');
 
574
  uiPanel.style.display = 'block';
575
+ orbitControls.enabled = true;
576
+ pointerLockControls.unlock();
577
  }
578
  }
579
+
580
+ function showSpinner(show) {
581
+ document.getElementById('loading-spinner').style.display = show ? 'block' : 'none';
582
+ }
583
 
584
  function createTerrain(width, height, terrainData = null) {
585
  if (terrainMesh) {
 
590
 
591
  terrainDimensions.width = width;
592
  terrainDimensions.height = height;
593
+ terrainDimensions.segmentsX = Math.max(1, Math.round(width));
594
+ terrainDimensions.segmentsY = Math.max(1, Math.round(height));
595
 
596
  const geometry = new THREE.PlaneGeometry(
597
  terrainDimensions.width, terrainDimensions.height,
 
621
  grassInstances.instanceMatrix.needsUpdate = true;
622
  }
623
  }
624
+
625
+ geometry.computeTangents();
626
+ geometry.computeVertexNormals();
627
 
628
  terrainMesh = new THREE.Mesh(geometry, terrainMaterial);
629
+ terrainMesh.castShadow = true;
630
  terrainMesh.receiveShadow = true;
631
  scene.add(terrainMesh);
 
 
632
  }
633
 
634
  function onWindowResize() {
 
638
  composer.setSize(window.innerWidth, window.innerHeight);
639
  }
640
 
641
+ function getBrushMode() {
642
+ return document.querySelector('input[name="brush-mode"]:checked').value;
643
+ }
644
 
645
  function onPointerMove(event) {
646
  if (isPlayMode) return;
 
654
 
655
  if (intersects.length > 0) {
656
  const intersectionPoint = intersects[0].point;
657
+ brushHelper.position.copy(intersectionPoint);
658
+ brushHelper.position.y = 0;
659
  brushHelper.visible = true;
660
  updateBrushHelper();
661
+ orbitControls.enabled = !isEditing;
662
 
663
+ if (isEditing) {
664
+ applyBrush(intersects[0]);
665
+ }
666
  } else {
667
  brushHelper.visible = false;
668
+ orbitControls.enabled = true;
669
  }
670
  }
671
 
 
675
  }
676
 
677
  function onPointerDown(event) {
678
+ if (isPlayMode) return;
679
+ if (event.button === 0 && mouse.x > -0.9) {
680
  if (window.innerWidth < 800 && !document.getElementById('ui-panel').contains(event.target)) {
681
  document.getElementById('ui-panel').classList.remove('open');
682
  }
683
+ isEditing = true;
684
+ orbitControls.enabled = false;
685
  }
686
  }
687
 
688
  function onPointerUp(event) {
689
+ if (isPlayMode) return;
690
+ if (event.button === 0) {
691
+ isEditing = false;
692
+ orbitControls.enabled = true;
693
  }
694
  }
695
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
696
  function applyBrush(intersection) {
697
  if (!terrainMesh) return;
698
  const brushMode = getBrushMode();
 
713
 
714
  for (let i = 0; i < positions.count; i++) {
715
  vertex.fromBufferAttribute(positions, i);
716
+ const distance = vertex.distanceTo(center);
717
+ if (distance < brushSize) {
718
+ const falloff = Math.pow(1 - (distance / brushSize), 2);
719
+ let currentY = positions.getY(i);
720
+ let newY = currentY + direction * falloff * brushStrength;
721
+ positions.setY(i, newY);
722
  }
723
  }
724
  positions.needsUpdate = true;
725
  terrainMesh.geometry.computeVertexNormals();
726
+ terrainMesh.geometry.computeTangents();
727
  }
728
 
729
  function roughenTerrain(center) {
 
731
  const brushSize = parseFloat(document.getElementById('brush-size').value);
732
  const brushStrength = parseFloat(document.getElementById('brush-strength').value);
733
  const vertex = new THREE.Vector3();
734
+ const noiseFrequency = 1.5;
735
 
736
  for (let i = 0; i < positions.count; i++) {
737
  vertex.fromBufferAttribute(positions, i);
738
+ const distance = vertex.distanceTo(center);
739
+ if (distance < brushSize) {
740
+ const falloff = Math.pow(1 - (distance / brushSize), 2);
741
+ let noiseVal = noise2D(vertex.x * noiseFrequency, vertex.z * noiseFrequency);
742
+ let currentY = positions.getY(i);
743
+ let newY = currentY + noiseVal * falloff * brushStrength;
744
+ positions.setY(i, newY);
745
  }
746
  }
747
  positions.needsUpdate = true;
748
  terrainMesh.geometry.computeVertexNormals();
749
+ terrainMesh.geometry.computeTangents();
750
  }
751
 
752
  function paintTexture(center) {
 
757
  const textureType = document.querySelector('input[name="texture-type"]:checked').value;
758
 
759
  const targetColor = new THREE.Vector4(0,0,0,0);
760
+ if(textureType === 'rock') targetColor.x = 1;
761
+ else if (textureType === 'dirt') targetColor.y = 1;
762
+ else if (textureType === 'snow') targetColor.z = 1;
763
+ else if (textureType === 'sand') targetColor.w = 1;
764
 
765
  const vertex = new THREE.Vector3();
766
  const currentColor = new THREE.Vector4();
767
 
768
  for (let i = 0; i < positions.count; i++) {
769
  vertex.fromBufferAttribute(positions, i);
770
+ const distance = vertex.distanceTo(center);
771
+ if (distance < brushSize) {
772
+ const falloff = Math.pow(1 - (distance / brushSize), 2);
773
  currentColor.fromBufferAttribute(colors, i);
774
 
775
  if (textureType === 'grass') {
776
+ currentColor.x = Math.max(0, currentColor.x - falloff * brushStrength);
777
+ currentColor.y = Math.max(0, currentColor.y - falloff * brushStrength);
778
+ currentColor.z = Math.max(0, currentColor.z - falloff * brushStrength);
779
+ currentColor.w = Math.max(0, currentColor.w - falloff * brushStrength);
780
  } else {
 
781
  currentColor.lerp(targetColor, falloff * brushStrength);
782
  }
783
 
784
  let sum = currentColor.x + currentColor.y + currentColor.z + currentColor.w;
785
+ if (sum > 1.0) {
786
+ currentColor.divideScalar(sum);
787
+ }
788
  colors.setXYZW(i, currentColor.x, currentColor.y, currentColor.z, currentColor.w);
789
  }
790
  }
 
793
 
794
  function placeObject(intersection) {
795
  const brushSize = parseFloat(document.getElementById('brush-size').value);
796
+ const density = parseFloat(document.getElementById('brush-strength').value) * 2;
797
  const dummy = new THREE.Object3D();
798
 
799
  for (let i = 0; i < density; i++) {
800
  if (grassInstances.count >= MAX_GRASS_COUNT) break;
801
 
802
+ const randomPoint = new THREE.Vector2(
803
+ (Math.random() - 0.5) * brushSize * 2,
804
+ (Math.random() - 0.5) * brushSize * 2
805
+ );
806
 
807
+ dummy.position.set(
808
+ intersection.point.x + randomPoint.x,
809
+ intersection.point.y,
810
+ intersection.point.z + randomPoint.y
811
+ );
812
 
813
  const placementRaycaster = new THREE.Raycaster(
814
+ new THREE.Vector3(dummy.position.x, 100, dummy.position.z),
815
+ new THREE.Vector3(0, -1, 0)
816
  );
817
  const placementIntersects = placementRaycaster.intersectObject(terrainMesh);
818
 
 
829
 
830
  async function saveProject() {
831
  const projectName = document.getElementById('project-name').value.trim();
832
+ if (!projectName) { alert("Пожалуйста, введите имя проекта."); return; }
833
+ if (!terrainMesh) { alert("Сначала создайте ландшафт для сохранения."); return; }
834
  showSpinner(true);
835
 
836
  const grassMatrices = [];
 
837
  for (let i = 0; i < grassInstances.count; i++) {
838
+ const matrix = new THREE.Matrix4();
839
  grassInstances.getMatrixAt(i, matrix);
840
  grassMatrices.push(...matrix.elements);
841
  }
842
 
843
  const projectData = {
844
  name: projectName,
845
+ width: terrainDimensions.width,
846
+ height: terrainDimensions.height,
847
  heights: Array.from(terrainMesh.geometry.attributes.position.array.filter((_, i) => i % 3 === 1)),
848
  colors: Array.from(terrainMesh.geometry.attributes.color.array),
849
+ foliage: {
850
+ grass: grassMatrices
851
+ }
852
  };
853
 
854
  try {
 
858
  body: JSON.stringify(projectData)
859
  });
860
  const result = await response.json();
861
+ if (!response.ok) throw new Error(result.error || 'Ошибка при сохранении проекта.');
862
  alert(`Проект '${projectName}' успешно сохранен!`);
863
 
864
  const projectList = document.getElementById('project-list');
865
+ if (!Array.from(projectList.options).some(opt => opt.value === projectName)) {
866
  const newOption = document.createElement('option');
867
  newOption.value = newOption.textContent = projectName;
868
  projectList.appendChild(newOption);
 
876
 
877
  async function loadProject() {
878
  const projectName = document.getElementById('project-list').value;
879
+ if (!projectName) { alert("Пожалуйста, выберите проект для загрузки."); return; }
880
  showSpinner(true);
881
 
882
  try {
883
  const response = await fetch(`/api/project/${projectName}`);
884
  const result = await response.json();
885
+ if (!response.ok) throw new Error(result.error || 'Ошибка при загрузке проекта.');
886
 
887
  createTerrain(result.width, result.height, result);
888
  document.getElementById('project-name').value = projectName;
 
896
  }
897
 
898
  function updatePlayer(deltaTime) {
899
+ let speed = playerSpeed;
900
+ let speedDelta = deltaTime * speed;
901
 
902
+ if (keyStates['KeyW']) playerVelocity.z -= speedDelta;
903
+ if (keyStates['KeyS']) playerVelocity.z += speedDelta;
904
+ if (keyStates['KeyA']) playerVelocity.x -= speedDelta;
905
+ if (keyStates['KeyD']) playerVelocity.x += speedDelta;
906
+
907
+ if (playerOnGround && keyStates['Space']) {
908
+ playerVelocity.y = playerJumpHeight;
909
+ }
910
 
911
+ playerVelocity.y += gravity * deltaTime;
 
912
 
913
+ const moveDirection = new THREE.Vector3(playerVelocity.x, 0, playerVelocity.z);
914
+ moveDirection.applyQuaternion(camera.quaternion);
915
 
916
+ player.position.x += moveDirection.x;
917
+ player.position.z += moveDirection.z;
918
  player.position.y += playerVelocity.y * deltaTime;
919
+
920
+ playerVelocity.x *= 0.9;
921
+ playerVelocity.z *= 0.9;
922
 
923
  raycaster.set(player.position, new THREE.Vector3(0, -1, 0));
924
+ if (terrainMesh) {
925
  const intersects = raycaster.intersectObject(terrainMesh);
926
  if (intersects.length > 0) {
927
+ const groundHeight = intersects[0].point.y;
928
+ if (player.position.y < groundHeight + playerHeight) {
929
+ player.position.y = groundHeight + playerHeight;
930
  playerVelocity.y = 0;
931
+ playerOnGround = true;
932
  } else {
933
+ playerOnGround = false;
934
  }
 
 
935
  }
936
  }
937
  camera.position.copy(player.position);
 
939
 
940
  function animate() {
941
  requestAnimationFrame(animate);
942
+ const deltaTime = clock.getDelta();
943
 
944
+ if (isPlayMode && pointerLockControls.isLocked) {
945
  updatePlayer(deltaTime);
946
  } else {
947
+ orbitControls.update();
948
  }
 
949
  composer.render();
950
  }
951
 
 
952
  init();
953
+ createTerrain(100, 100);
954
  </script>
955
  </body>
956
  </html>