Kgshop commited on
Commit
32e45e2
·
verified ·
1 Parent(s): 51977cd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +244 -347
app.py CHANGED
@@ -117,7 +117,7 @@ EDITOR_TEMPLATE = '''
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;
@@ -224,6 +224,10 @@ EDITOR_TEMPLATE = '''
224
  <label><input type="radio" name="texture-type" value="snow"> Снег</label>
225
  <label><input type="radio" name="texture-type" value="sand"> Песок</label>
226
  </div>
 
 
 
 
227
  </div>
228
  <div class="ui-group">
229
  <h3>Объекты</h3>
@@ -237,13 +241,13 @@ EDITOR_TEMPLATE = '''
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
 
@@ -276,16 +280,17 @@ EDITOR_TEMPLATE = '''
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
 
@@ -298,89 +303,56 @@ EDITOR_TEMPLATE = '''
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
  `
@@ -388,7 +360,6 @@ EDITOR_TEMPLATE = '''
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
 
@@ -405,9 +376,6 @@ EDITOR_TEMPLATE = '''
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;
@@ -425,14 +393,7 @@ EDITOR_TEMPLATE = '''
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;
@@ -466,28 +427,61 @@ EDITOR_TEMPLATE = '''
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;
@@ -497,134 +491,84 @@ EDITOR_TEMPLATE = '''
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', () => {
536
- document.getElementById('ui-panel').classList.toggle('open');
537
- });
538
- document.getElementById('clear-objects').addEventListener('click', () => {
539
- if (confirm('Вы уверены, что хотите удалить все объекты?')) {
540
- grassInstances.count = 0;
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) {
586
- scene.remove(terrainMesh);
587
- terrainMesh.geometry.dispose();
588
- }
589
  initFoliage();
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,
598
- terrainDimensions.segmentsX, terrainDimensions.segmentsY
599
- );
600
  geometry.rotateX(-Math.PI / 2);
601
-
602
- const colors = new Float32Array(geometry.attributes.position.count * 4);
603
- geometry.setAttribute('color', new THREE.BufferAttribute(colors, 4));
604
 
605
  if (terrainData) {
606
- const positions = geometry.attributes.position;
607
- for (let i = 0; i < positions.count; i++) {
608
- positions.setY(i, terrainData.heights[i]);
609
- }
610
- if (terrainData.colors) {
611
- geometry.attributes.color.array.set(terrainData.colors);
612
  }
 
 
 
613
  if (terrainData.foliage && terrainData.foliage.grass) {
614
- const matrices = terrainData.foliage.grass;
615
- grassInstances.count = matrices.length / 16;
616
  for (let i = 0; i < grassInstances.count; i++) {
617
- const matrix = new THREE.Matrix4();
618
- matrix.fromArray(matrices, i * 16);
619
- grassInstances.setMatrixAt(i, matrix);
620
  }
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;
@@ -638,60 +582,30 @@ EDITOR_TEMPLATE = '''
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;
647
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
648
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
649
-
650
  raycaster.setFromCamera(mouse, camera);
651
  if (!terrainMesh) return;
652
-
653
  const intersects = raycaster.intersectObject(terrainMesh);
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
 
672
- function updateBrushHelper() {
673
- const brushSize = parseFloat(document.getElementById('brush-size').value);
674
- brushHelper.scale.set(brushSize, 1, brushSize);
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;
@@ -704,248 +618,231 @@ EDITOR_TEMPLATE = '''
704
  }
705
  }
706
 
707
- function sculptTerrain(center, mode) {
708
  const positions = terrainMesh.geometry.attributes.position;
709
  const brushSize = parseFloat(document.getElementById('brush-size').value);
710
- const brushStrength = parseFloat(document.getElementById('brush-strength').value);
711
- const direction = (mode === 'raise') ? 1 : -1;
712
  const vertex = new THREE.Vector3();
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) {
730
- const positions = terrainMesh.geometry.attributes.position;
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) {
 
 
 
 
 
753
  const colors = terrainMesh.geometry.attributes.color;
 
754
  const positions = terrainMesh.geometry.attributes.position;
755
  const brushSize = parseFloat(document.getElementById('brush-size').value);
756
- const brushStrength = parseFloat(document.getElementById('brush-strength').value) * 0.1;
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
  }
791
  colors.needsUpdate = true;
 
792
  }
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
-
819
  if (placementIntersects.length > 0) {
820
  dummy.position.copy(placementIntersects[0].point);
821
  dummy.rotation.y = Math.random() * Math.PI * 2;
822
- dummy.scale.set(1, Math.random() * 0.5 + 0.75, 1);
823
  dummy.updateMatrix();
824
  grassInstances.setMatrixAt(grassInstances.count++, dummy.matrix);
825
  }
826
  }
827
- grassInstances.instanceMatrix.needsUpdate = true;
828
  }
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 {
855
- const response = await fetch('/api/project', {
856
- method: 'POST',
857
- headers: { 'Content-Type': 'application/json' },
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);
869
  }
870
- } catch (error) {
871
- alert(`Не удалось сохранить проект: ${error.message}`);
872
- } finally {
873
- showSpinner(false);
874
- }
875
  }
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;
889
  document.getElementById('terrain-width').value = result.width;
890
  document.getElementById('terrain-height').value = result.height;
891
- } catch (error) {
892
- alert(`Не удалось загрузить проект: ${error.message}`);
893
- } finally {
894
- showSpinner(false);
895
- }
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);
938
  }
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
 
 
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; margin-bottom: 5px;
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;
 
224
  <label><input type="radio" name="texture-type" value="snow"> Снег</label>
225
  <label><input type="radio" name="texture-type" value="sand"> Песок</label>
226
  </div>
227
+ <hr>
228
+ <input type="text" id="texture-name-input" placeholder="Имя текстуры (e.g. lava)">
229
+ <input type="text" id="texture-url-input" placeholder="URL текстуры (.jpg, .png)">
230
+ <button id="add-texture-btn">Добавить текстуру</button>
231
  </div>
232
  <div class="ui-group">
233
  <h3>Объекты</h3>
 
241
 
242
  <div id="blocker">
243
  <div id="instructions">
244
+ <p style="font-size:36px">Нажмите, чтобы играть</p>
245
  <p>
246
+ Движение: WASD<br/>
247
+ Прыжок: ПРОБЕЛ<br/>
248
+ Осмотр: МЫШЬ
249
  </p>
250
+ <p>Нажмите ESC, чтобы выйти</p>
251
  </div>
252
  </div>
253
 
 
280
  let isEditing = false;
281
  const noise2D = createNoise2D();
282
 
283
+ let grassInstances, grassMaterial;
284
  const MAX_GRASS_COUNT = 100000;
285
  const terrainDimensions = { width: 100, height: 100, segmentsX: 100, segmentsY: 100 };
286
 
287
  let isPlayMode = false;
288
  let player, playerVelocity = new THREE.Vector3(), playerOnGround = false;
289
  const playerHeight = 1.8;
290
+ const playerSpeed = 8.0;
291
  const playerJumpHeight = 8.0;
292
  const gravity = -30.0;
293
+ const maxSlope = Math.cos(THREE.MathUtils.degToRad(50));
294
  const keyStates = {};
295
  const clock = new THREE.Clock();
296
 
 
303
  return tex;
304
  };
305
 
306
+ let terrainTextures = {
307
+ grass: { tex: loadTexture('https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/textures/terrain/grasslight-big.jpg') },
308
+ rock: { tex: loadTexture('https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/textures/terrain/rock.jpg') },
309
+ dirt: { tex: loadTexture('https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/textures/terrain/sand-512.jpg') },
310
+ snow: { tex: loadTexture('https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/textures/snow.jpg') },
311
+ sand: { tex: loadTexture('https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/textures/terrain/sand-512.jpg') },
 
 
312
  };
313
+ let textureMapping = ['grass', 'rock', 'dirt', 'snow', 'sand'];
314
+
315
  const terrainMaterial = new THREE.ShaderMaterial({
316
  uniforms: {
317
+ textures: { value: Object.values(terrainTextures).map(t => t.tex) },
 
 
 
 
 
 
318
  lightDirection: { value: new THREE.Vector3(0.5, 0.5, 0.5).normalize() }
319
  },
320
  vertexShader: `
321
  varying vec2 vUv;
322
  varying vec3 vNormal;
 
323
  attribute vec4 color;
324
+ attribute vec4 color2;
325
  varying vec4 vColor;
326
+ varying vec4 vColor2;
 
 
 
327
  void main() {
328
  vUv = uv;
329
  vColor = color;
330
+ vColor2 = color2;
331
+ vNormal = normal;
332
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
 
 
 
333
  }
334
  `,
335
  fragmentShader: `
336
+ uniform sampler2D textures[10];
 
 
 
 
 
 
337
  uniform vec3 lightDirection;
 
338
  varying vec2 vUv;
 
339
  varying vec3 vNormal;
340
+ varying vec4 vColor;
341
+ varying vec4 vColor2;
 
 
342
  void main() {
343
  vec2 uv_scaled = vUv * 30.0;
344
+ float totalWeight = 1.0 - (vColor.r + vColor.g + vColor.b + vColor.a + vColor2.r + vColor2.g + vColor2.b + vColor2.a);
345
+ vec3 finalColor = texture2D(textures[0], uv_scaled).rgb * max(0.0, totalWeight);
346
+ finalColor += texture2D(textures[1], uv_scaled).rgb * vColor.r;
347
+ finalColor += texture2D(textures[2], uv_scaled).rgb * vColor.g;
348
+ finalColor += texture2D(textures[3], uv_scaled).rgb * vColor.b;
349
+ finalColor += texture2D(textures[4], uv_scaled).rgb * vColor.a;
350
+ finalColor += texture2D(textures[5], uv_scaled).rgb * vColor2.r;
351
+ finalColor += texture2D(textures[6], uv_scaled).rgb * vColor2.g;
352
+ finalColor += texture2D(textures[7], uv_scaled).rgb * vColor2.b;
353
+ finalColor += texture2D(textures[8], uv_scaled).rgb * vColor2.a;
354
 
355
+ float lighting = dot(vNormal, lightDirection) * 0.5 + 0.5;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
356
  gl_FragColor = vec4(finalColor * lighting, 1.0);
357
  }
358
  `
 
360
 
361
  function init() {
362
  scene = new THREE.Scene();
 
363
  camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000);
364
  camera.position.set(50, 50, 50);
365
 
 
376
  orbitControls.enableDamping = true;
377
  orbitControls.maxPolarAngle = Math.PI / 2.1;
378
 
 
 
 
379
  const dirLight = new THREE.DirectionalLight(0xffffff, 1.5);
380
  dirLight.position.set(100, 100, 50);
381
  dirLight.castShadow = true;
 
393
  sky.scale.setScalar(450000);
394
  scene.add(sky);
395
  sun = new THREE.Vector3();
396
+ const effectController = { turbidity: 10, rayleigh: 3, mieCoefficient: 0.005, mieDirectionalG: 0.7, elevation: 4, azimuth: 180 };
 
 
 
 
 
 
 
397
  const uniforms = sky.material.uniforms;
398
  uniforms[ 'turbidity' ].value = effectController.turbidity;
399
  uniforms[ 'rayleigh' ].value = effectController.rayleigh;
 
427
 
428
  function initPostprocessing() {
429
  composer = new EffectComposer(renderer);
430
+ composer.addPass(new RenderPass(scene, camera));
 
 
431
  const ssaoPass = new SSAOPass(scene, camera, window.innerWidth, window.innerHeight);
432
  ssaoPass.kernelRadius = 16;
 
 
433
  composer.addPass(ssaoPass);
434
+ composer.addPass(new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 0.5, 0.4, 0.85));
 
 
435
  }
436
 
437
  function initFoliage() {
438
  if (grassInstances) {
439
  scene.remove(grassInstances);
440
  grassInstances.geometry.dispose();
441
+ grassMaterial.dispose();
442
  }
443
+ const grassBlade = new THREE.PlaneGeometry(0.4, 1.5, 1, 4);
444
+ grassBlade.translate(0, 0.75, 0);
445
+ grassMaterial = new THREE.ShaderMaterial({
446
+ uniforms: {
447
+ time: { value: 0 },
448
+ map: { value: textureLoader.load('https://raw.githubusercontent.com/mrdoob/three.js/master/examples/textures/sprites/grass.png') },
449
+ lightDirection: terrainMaterial.uniforms.lightDirection
450
+ },
451
+ vertexShader: `
452
+ uniform float time;
453
+ varying vec2 vUv;
454
+ varying float vWave;
455
+ void main() {
456
+ vUv = uv;
457
+ vec4 modelPosition = modelMatrix * vec4(position, 1.0);
458
+ float waveFactor = position.y / 1.5;
459
+ float waveX = sin(modelPosition.x * 0.5 + time * 2.0) * 0.1 * waveFactor;
460
+ float waveZ = cos(modelPosition.z * 0.5 + time * 2.0) * 0.1 * waveFactor;
461
+ vWave = waveX + waveZ;
462
+ vec4 finalPosition = vec4(modelPosition.x + waveX, modelPosition.y, modelPosition.z + waveZ, 1.0);
463
+ gl_Position = projectionMatrix * viewMatrix * finalPosition;
464
+ }
465
+ `,
466
+ fragmentShader: `
467
+ uniform sampler2D map;
468
+ uniform vec3 lightDirection;
469
+ varying vec2 vUv;
470
+ varying float vWave;
471
+ void main() {
472
+ vec4 texColor = texture2D(map, vUv);
473
+ if (texColor.a < 0.5) discard;
474
+
475
+ vec3 light = normalize(lightDirection);
476
+ float dotNL = clamp(dot(vec3(0.0, 1.0, 0.0), light), 0.3, 1.0);
477
+
478
+ vec3 baseColor = texColor.rgb * (0.5 + vWave * 2.0);
479
+ gl_FragColor = vec4(baseColor * dotNL, texColor.a);
480
+ }
481
+ `,
482
+ side: THREE.DoubleSide,
483
+ transparent: true
484
+ });
485
  grassInstances = new THREE.InstancedMesh(grassBlade, grassMaterial, MAX_GRASS_COUNT);
486
  grassInstances.castShadow = true;
487
  grassInstances.count = 0;
 
491
  function initPlayer() {
492
  player = new THREE.Group();
493
  scene.add(player);
 
494
  pointerLockControls = new PointerLockControls(camera, document.body);
495
  const blocker = document.getElementById('blocker');
496
  const instructions = document.getElementById('instructions');
497
  instructions.addEventListener('click', () => pointerLockControls.lock());
498
  pointerLockControls.addEventListener('lock', () => { instructions.style.display = 'none'; blocker.style.display = 'none'; });
499
  pointerLockControls.addEventListener('unlock', () => { blocker.style.display = 'block'; instructions.style.display = ''; });
500
+ document.addEventListener('keydown', (event) => { keyStates[event.code] = true; if (isPlayMode && event.code === 'Escape') togglePlayMode(); });
 
501
  document.addEventListener('keyup', (event) => { keyStates[event.code] = false; });
 
 
 
 
 
502
  }
503
 
504
  function setupUIListeners() {
505
+ document.getElementById('create-terrain').addEventListener('click', () => createTerrain(parseInt(document.getElementById('terrain-width').value), parseInt(document.getElementById('terrain-height').value)));
 
 
 
 
506
  document.getElementById('save-project').addEventListener('click', saveProject);
507
  document.getElementById('load-project').addEventListener('click', loadProject);
508
+ document.getElementById('brush-size').addEventListener('input', (e) => { document.getElementById('brush-size-value').textContent = e.target.value; updateBrushHelper(); });
509
+ document.getElementById('brush-strength').addEventListener('input', (e) => { document.getElementById('brush-strength-value').textContent = e.target.value; });
510
+ document.getElementById('project-list').addEventListener('change', (e) => { document.getElementById('project-name').value = e.target.value; });
511
+ document.getElementById('burger-menu').addEventListener('click', () => { document.getElementById('ui-panel').classList.toggle('open'); });
512
+ document.getElementById('clear-objects').addEventListener('click', () => { if (confirm('Вы уверены?')) { grassInstances.count = 0; grassInstances.instanceMatrix.needsUpdate = true; }});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
513
  document.getElementById('play-mode-toggle').addEventListener('click', togglePlayMode);
514
+ document.getElementById('add-texture-btn').addEventListener('click', addCustomTexture);
515
  }
516
 
517
  function togglePlayMode() {
518
  isPlayMode = !isPlayMode;
519
  const toggleButton = document.getElementById('play-mode-toggle');
 
 
520
  if (isPlayMode) {
521
  toggleButton.textContent = "Редактор";
522
  toggleButton.classList.remove('play-button');
523
+ document.getElementById('ui-container').style.display = 'none';
524
  brushHelper.visible = false;
525
  orbitControls.enabled = false;
526
+ raycaster.set(new THREE.Vector3(0, 100, 0), new THREE.Vector3(0, -1, 0));
527
+ const intersects = raycaster.intersectObject(terrainMesh);
528
+ player.position.copy(intersects.length > 0 ? intersects[0].point : new THREE.Vector3(0, 0, 0));
 
 
 
 
 
529
  player.position.y += playerHeight;
530
  playerVelocity.set(0,0,0);
 
531
  pointerLockControls.lock();
 
532
  } else {
533
  toggleButton.textContent = "Играть";
534
  toggleButton.classList.add('play-button');
535
+ document.getElementById('ui-container').style.display = 'block';
536
  orbitControls.enabled = true;
537
  pointerLockControls.unlock();
538
  }
539
  }
540
 
541
+ function showSpinner(show) { document.getElementById('loading-spinner').style.display = show ? 'block' : 'none'; }
 
 
542
 
543
  function createTerrain(width, height, terrainData = null) {
544
+ if (terrainMesh) { scene.remove(terrainMesh); terrainMesh.geometry.dispose(); }
 
 
 
545
  initFoliage();
 
546
  terrainDimensions.width = width;
547
  terrainDimensions.height = height;
548
  terrainDimensions.segmentsX = Math.max(1, Math.round(width));
549
  terrainDimensions.segmentsY = Math.max(1, Math.round(height));
550
+ const geometry = new THREE.PlaneGeometry(width, height, terrainDimensions.segmentsX, terrainDimensions.segmentsY);
 
 
 
 
551
  geometry.rotateX(-Math.PI / 2);
552
+ geometry.setAttribute('color', new THREE.BufferAttribute(new Float32Array(geometry.attributes.position.count * 4), 4));
553
+ geometry.setAttribute('color2', new THREE.BufferAttribute(new Float32Array(geometry.attributes.position.count * 4), 4));
 
554
 
555
  if (terrainData) {
556
+ geometry.attributes.position.setY(0, 0);
557
+ for (let i = 0; i < terrainData.heights.length; i++) {
558
+ geometry.attributes.position.setY(i, terrainData.heights[i]);
 
 
 
559
  }
560
+ geometry.attributes.position.needsUpdate = true;
561
+ if(terrainData.colors) geometry.attributes.color.array.set(terrainData.colors);
562
+ if(terrainData.colors2) geometry.attributes.color2.array.set(terrainData.colors2);
563
  if (terrainData.foliage && terrainData.foliage.grass) {
564
+ grassInstances.count = terrainData.foliage.grass.length / 16;
 
565
  for (let i = 0; i < grassInstances.count; i++) {
566
+ grassInstances.setMatrixAt(i, new THREE.Matrix4().fromArray(terrainData.foliage.grass, i * 16));
 
 
567
  }
568
  grassInstances.instanceMatrix.needsUpdate = true;
569
  }
570
  }
 
 
571
  geometry.computeVertexNormals();
 
572
  terrainMesh = new THREE.Mesh(geometry, terrainMaterial);
573
  terrainMesh.castShadow = true;
574
  terrainMesh.receiveShadow = true;
 
582
  composer.setSize(window.innerWidth, window.innerHeight);
583
  }
584
 
585
+ function getBrushMode() { return document.querySelector('input[name="brush-mode"]:checked').value; }
 
 
586
 
587
  function onPointerMove(event) {
588
  if (isPlayMode) return;
589
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
590
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
 
591
  raycaster.setFromCamera(mouse, camera);
592
  if (!terrainMesh) return;
 
593
  const intersects = raycaster.intersectObject(terrainMesh);
 
594
  if (intersects.length > 0) {
595
+ brushHelper.position.copy(intersects[0].point).setY(0);
 
 
596
  brushHelper.visible = true;
597
  updateBrushHelper();
598
  orbitControls.enabled = !isEditing;
599
+ if (isEditing) applyBrush(intersects[0]);
 
 
 
600
  } else {
601
  brushHelper.visible = false;
602
  orbitControls.enabled = true;
603
  }
604
  }
605
 
606
+ function updateBrushHelper() { brushHelper.scale.set(parseFloat(document.getElementById('brush-size').value), 1, parseFloat(document.getElementById('brush-size').value)); }
607
+ function onPointerDown(event) { if (!isPlayMode && event.button === 0 && mouse.x > -0.9) { isEditing = true; orbitControls.enabled = false; }}
608
+ function onPointerUp(event) { if (!isPlayMode && event.button === 0) { isEditing = false; orbitControls.enabled = true; }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
609
 
610
  function applyBrush(intersection) {
611
  if (!terrainMesh) return;
 
618
  }
619
  }
620
 
621
+ function modifyTerrain(center, modificationFunc) {
622
  const positions = terrainMesh.geometry.attributes.position;
623
  const brushSize = parseFloat(document.getElementById('brush-size').value);
 
 
624
  const vertex = new THREE.Vector3();
 
625
  for (let i = 0; i < positions.count; i++) {
626
  vertex.fromBufferAttribute(positions, i);
627
  const distance = vertex.distanceTo(center);
628
  if (distance < brushSize) {
629
  const falloff = Math.pow(1 - (distance / brushSize), 2);
630
+ modificationFunc(i, falloff);
 
 
631
  }
632
  }
633
  positions.needsUpdate = true;
634
  terrainMesh.geometry.computeVertexNormals();
 
635
  }
636
+ function sculptTerrain(center, mode) {
637
+ const brushStrength = parseFloat(document.getElementById('brush-strength').value);
638
+ const direction = (mode === 'raise') ? 1 : -1;
639
+ modifyTerrain(center, (i, falloff) => {
640
+ const positions = terrainMesh.geometry.attributes.position;
641
+ positions.setY(i, positions.getY(i) + direction * falloff * brushStrength);
642
+ });
643
+ }
644
  function roughenTerrain(center) {
 
 
645
  const brushStrength = parseFloat(document.getElementById('brush-strength').value);
 
646
  const noiseFrequency = 1.5;
647
+ modifyTerrain(center, (i, falloff) => {
648
+ const positions = terrainMesh.geometry.attributes.position;
649
+ const vertex = new THREE.Vector3().fromBufferAttribute(positions, i);
650
+ let noiseVal = noise2D(vertex.x * noiseFrequency, vertex.z * noiseFrequency);
651
+ positions.setY(i, positions.getY(i) + noiseVal * falloff * brushStrength);
652
+ });
 
 
 
 
 
 
 
 
 
653
  }
 
654
  function paintTexture(center) {
655
+ const brushStrength = parseFloat(document.getElementById('brush-strength').value) * 0.1;
656
+ const textureName = document.querySelector('input[name="texture-type"]:checked').value;
657
+ const textureIndex = textureMapping.indexOf(textureName);
658
+ if (textureIndex === -1) return;
659
+
660
  const colors = terrainMesh.geometry.attributes.color;
661
+ const colors2 = terrainMesh.geometry.attributes.color2;
662
  const positions = terrainMesh.geometry.attributes.position;
663
  const brushSize = parseFloat(document.getElementById('brush-size').value);
 
 
 
 
 
 
 
 
 
664
  const vertex = new THREE.Vector3();
 
665
 
666
  for (let i = 0; i < positions.count; i++) {
667
  vertex.fromBufferAttribute(positions, i);
668
  const distance = vertex.distanceTo(center);
669
  if (distance < brushSize) {
670
+ const falloff = Math.pow(1 - (distance / brushSize), 2) * brushStrength;
671
+ let weights = [colors.getX(i), colors.getY(i), colors.getZ(i), colors.getW(i), colors2.getX(i), colors2.getY(i), colors2.getZ(i), colors2.getW(i)];
672
 
673
+ let otherWeightsSum = 0;
674
+ for(let j=1; j<textureMapping.length; ++j) {
675
+ if(j !== textureIndex) otherWeightsSum += weights[j-1];
 
 
 
 
676
  }
677
+ if(textureIndex > 0) {
678
+ weights[textureIndex-1] += falloff;
679
+ }
680
+ let totalSum = 0;
681
+ for(let j=1; j<textureMapping.length; ++j) totalSum += weights[j-1];
682
+ if (totalSum > 1.0) {
683
+ for(let j=1; j<textureMapping.length; ++j) weights[j-1] /= totalSum;
684
  }
685
+
686
+ colors.setXYZW(i, weights[0]||0, weights[1]||0, weights[2]||0, weights[3]||0);
687
+ colors2.setXYZW(i, weights[4]||0, weights[5]||0, weights[6]||0, weights[7]||0);
688
  }
689
  }
690
  colors.needsUpdate = true;
691
+ colors2.needsUpdate = true;
692
  }
693
 
694
  function placeObject(intersection) {
695
  const brushSize = parseFloat(document.getElementById('brush-size').value);
696
  const density = parseFloat(document.getElementById('brush-strength').value) * 2;
697
  const dummy = new THREE.Object3D();
 
698
  for (let i = 0; i < density; i++) {
699
  if (grassInstances.count >= MAX_GRASS_COUNT) break;
700
+ const randomPoint = new THREE.Vector2((Math.random() - 0.5) * brushSize * 2, (Math.random() - 0.5) * brushSize * 2);
701
+ raycaster.set( new THREE.Vector3(intersection.point.x + randomPoint.x, 100, intersection.point.z + randomPoint.y), new THREE.Vector3(0, -1, 0));
702
+ const placementIntersects = raycaster.intersectObject(terrainMesh);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
703
  if (placementIntersects.length > 0) {
704
  dummy.position.copy(placementIntersects[0].point);
705
  dummy.rotation.y = Math.random() * Math.PI * 2;
706
+ dummy.scale.setScalar(Math.random() * 0.5 + 0.75);
707
  dummy.updateMatrix();
708
  grassInstances.setMatrixAt(grassInstances.count++, dummy.matrix);
709
  }
710
  }
711
+ grassInstances.instanceMatrix.needsUpdate = true;
712
  }
713
 
714
  async function saveProject() {
715
  const projectName = document.getElementById('project-name').value.trim();
716
+ if (!projectName || !terrainMesh) { alert("Введите имя проекта и создайте ландшафт."); return; }
 
717
  showSpinner(true);
 
718
  const grassMatrices = [];
719
  for (let i = 0; i < grassInstances.count; i++) {
720
  const matrix = new THREE.Matrix4();
721
  grassInstances.getMatrixAt(i, matrix);
722
  grassMatrices.push(...matrix.elements);
723
  }
 
724
  const projectData = {
725
  name: projectName,
726
+ width: terrainDimensions.width, height: terrainDimensions.height,
 
727
  heights: Array.from(terrainMesh.geometry.attributes.position.array.filter((_, i) => i % 3 === 1)),
728
  colors: Array.from(terrainMesh.geometry.attributes.color.array),
729
+ colors2: Array.from(terrainMesh.geometry.attributes.color2.array),
730
+ foliage: { grass: grassMatrices },
731
+ customTextures: textureMapping.slice(5)
732
  };
 
733
  try {
734
+ const response = await fetch('/api/project', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(projectData) });
735
+ if (!response.ok) throw new Error((await response.json()).error || 'Ошибка сохранения.');
736
+ alert(`Проект '${projectName}' сохранен!`);
 
 
 
 
 
 
737
  const projectList = document.getElementById('project-list');
738
+ if (![...projectList.options].some(opt => opt.value === projectName)) {
739
  const newOption = document.createElement('option');
740
  newOption.value = newOption.textContent = projectName;
741
  projectList.appendChild(newOption);
742
  }
743
+ } catch (error) { alert(`Ошибка: ${error.message}`); } finally { showSpinner(false); }
 
 
 
 
744
  }
 
745
  async function loadProject() {
746
  const projectName = document.getElementById('project-list').value;
747
+ if (!projectName) { alert("Выберите проект."); return; }
748
  showSpinner(true);
 
749
  try {
750
  const response = await fetch(`/api/project/${projectName}`);
751
  const result = await response.json();
752
+ if (!response.ok) throw new Error(result.error || 'Ошибка загрузки.');
753
 
754
+ if (result.customTextures) {
755
+ for(const texName of result.customTextures) {
756
+ if (!textureMapping.includes(texName.name)) {
757
+ addCustomTexture(texName.name, texName.url, false);
758
+ }
759
+ }
760
+ }
761
  createTerrain(result.width, result.height, result);
762
  document.getElementById('project-name').value = projectName;
763
  document.getElementById('terrain-width').value = result.width;
764
  document.getElementById('terrain-height').value = result.height;
765
+ } catch (error) { alert(`Ошибка: ${error.message}`); } finally { showSpinner(false); }
 
 
 
 
766
  }
767
 
768
+ function addCustomTexture(nameOverride, urlOverride, fromUI = true) {
769
+ if (textureMapping.length >= 9) { alert("Достигнут лимит текстур."); return; }
770
+ const name = fromUI ? document.getElementById('texture-name-input').value.trim() : nameOverride;
771
+ const url = fromUI ? document.getElementById('texture-url-input').value.trim() : urlOverride;
772
+ if (!name || !url) { alert("Введите имя и URL."); return; }
773
+ if (textureMapping.includes(name)) { alert("Текстура с таким именем уже есть."); return; }
774
+
775
+ const newTexture = loadTexture(url);
776
+ textureMapping.push({name: name, url: url});
777
+ terrainMaterial.uniforms.textures.value.push(newTexture);
778
+
779
+ const container = document.getElementById('texture-selector');
780
+ const label = document.createElement('label');
781
+ const radio = document.createElement('input');
782
+ radio.type = 'radio';
783
+ radio.name = 'texture-type';
784
+ radio.value = name;
785
+ label.appendChild(radio);
786
+ label.appendChild(document.createTextNode(" " + name));
787
+ container.appendChild(label);
788
+ if(fromUI) {
789
+ document.getElementById('texture-name-input').value = '';
790
+ document.getElementById('texture-url-input').value = '';
791
+ }
792
+ }
793
  function updatePlayer(deltaTime) {
794
+ const speed = playerSpeed * deltaTime;
795
+ const moveVector = new THREE.Vector3();
796
+ if (keyStates['KeyW']) moveVector.z -= 1;
797
+ if (keyStates['KeyS']) moveVector.z += 1;
798
+ if (keyStates['KeyA']) moveVector.x -= 1;
799
+ if (keyStates['KeyD']) moveVector.x += 1;
800
+ moveVector.normalize().multiplyScalar(speed).applyQuaternion(camera.quaternion);
801
 
802
+ playerVelocity.y += gravity * deltaTime;
 
 
 
803
 
804
+ const potentialPosition = player.position.clone().add(moveVector);
805
+ const down = new THREE.Vector3(0,-1,0);
806
+
807
+ raycaster.set(potentialPosition.clone().setY(potentialPosition.y + 1), down);
808
+ let intersects = raycaster.intersectObject(terrainMesh);
809
 
810
+ if(intersects.length > 0) {
811
+ const groundNormal = intersects[0].face.normal;
812
+ if(groundNormal.y >= maxSlope) {
813
+ player.position.add(moveVector);
814
+ }
815
+ } else {
816
+ player.position.add(moveVector);
817
+ }
818
 
 
 
 
 
 
819
  player.position.y += playerVelocity.y * deltaTime;
820
 
821
+ raycaster.set(player.position, down);
822
+ intersects = raycaster.intersectObject(terrainMesh);
823
+ if (intersects.length > 0) {
824
+ const groundHeight = intersects[0].point.y;
825
+ if (player.position.y < groundHeight + playerHeight) {
826
+ player.position.y = groundHeight + playerHeight;
827
+ playerVelocity.y = 0;
828
+ playerOnGround = true;
829
+ } else {
830
+ playerOnGround = false;
 
 
 
 
 
831
  }
832
  }
833
+ if (playerOnGround && keyStates['Space']) playerVelocity.y = playerJumpHeight;
834
+
835
  camera.position.copy(player.position);
836
  }
837
 
838
  function animate() {
839
  requestAnimationFrame(animate);
840
  const deltaTime = clock.getDelta();
841
+ grassMaterial.uniforms.time.value += deltaTime;
842
+
843
+ if (isPlayMode && pointerLockControls.isLocked) updatePlayer(deltaTime);
844
+ else orbitControls.update();
845
 
 
 
 
 
 
846
  composer.render();
847
  }
848