Kgshop commited on
Commit
bcf83c2
·
verified ·
1 Parent(s): 83c1a59

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +120 -455
app.py CHANGED
@@ -125,11 +125,9 @@ EDITOR_TEMPLATE = '''
125
  button:hover { background: #0099ff; }
126
  button.play-button { background: #22aa22; }
127
  button.play-button:hover { background: #33cc33; }
128
- button.danger-button { background: #cc2222; }
129
- button.danger-button:hover { background: #ff3333; }
130
  .slider-container { margin-top: 10px; }
131
  input[type="range"] { width: 100%; }
132
- .radio-group label { display: inline-block; margin-right: 10px; cursor: pointer; font-size: 0.9em; }
133
  .radio-group input { margin-right: 5px; }
134
 
135
  #loading-spinner {
@@ -204,16 +202,10 @@ EDITOR_TEMPLATE = '''
204
  <div class="radio-group">
205
  <label><input type="radio" name="brush-mode" value="raise" checked> Поднять</label>
206
  <label><input type="radio" name="brush-mode" value="lower"> Опустить</label>
 
207
  <label><input type="radio" name="brush-mode" value="smooth"> Сгладить</label>
208
  <label><input type="radio" name="brush-mode" value="flatten"> Выровнять</label>
209
- <br>
210
- <label><input type="radio" name="brush-mode" value="roughen"> Шум</label>
211
- <label><input type="radio" name="brush-mode" value="erode"> Эрозия</label>
212
- <label><input type="radio" name="brush-mode" value="terrace"> Терраса</label>
213
- <label><input type="radio" name="brush-mode" value="pinch"> Щипок</label>
214
- <br>
215
  <label><input type="radio" name="brush-mode" value="paint"> Текстура</label>
216
- <label><input type="radio" name="brush-mode" value="foliage"> Растительность</label>
217
  <label><input type="radio" name="brush-mode" value="place"> Объект</label>
218
  </div>
219
  <div class="slider-container">
@@ -221,7 +213,7 @@ EDITOR_TEMPLATE = '''
221
  <input type="range" id="brush-size" min="1" max="50" value="10">
222
  </div>
223
  <div class="slider-container">
224
- <label for="brush-strength">Сила/Плотность: <span id="brush-strength-value">0.5</span></label>
225
  <input type="range" id="brush-strength" min="0.1" max="2" step="0.1" value="0.5">
226
  </div>
227
  </div>
@@ -234,14 +226,6 @@ EDITOR_TEMPLATE = '''
234
  <label><input type="radio" name="texture-type" value="snow"> Снег</label>
235
  <label><input type="radio" name="texture-type" value="sand"> Песок</label>
236
  </div>
237
- </div>
238
- <div class="ui-group">
239
- <h3>Авто-текстурирование</h3>
240
- <label for="snow-height">Высота снега: <span id="snow-height-value">30</span></label>
241
- <input type="range" id="snow-height" min="0" max="100" value="30">
242
- <label for="rock-slope">Угол скал: <span id="rock-slope-value">0.6</span></label>
243
- <input type="range" id="rock-slope" min="0.1" max="0.9" step="0.05" value="0.6">
244
- <button id="apply-auto-texture">Применить ко всему ландшафту</button>
245
  </div>
246
  <div class="ui-group" id="texture-management-group">
247
  <h3>Управление текстурами</h3>
@@ -253,7 +237,7 @@ EDITOR_TEMPLATE = '''
253
  <option value="snow">Снег</option>
254
  <option value="sand">Песок</option>
255
  </select>
256
- <label for="custom-texture-file">Выберите файл текстуры (Albedo):</label>
257
  <input type="file" id="custom-texture-file" accept="image/*">
258
  <button id="update-texture-btn">Обновить текстуру</button>
259
  <label for="texture-slot-name">Новое имя для слота:</label>
@@ -261,19 +245,11 @@ EDITOR_TEMPLATE = '''
261
  <button id="rename-texture-slot-btn">Переименовать</button>
262
  </div>
263
  <div class="ui-group">
264
- <h3>Размещение объектов</h3>
265
- <p style="font-size:0.8em; color: #aaa;">Режим "Объект" в кистях. Выберите модель и рисуйте ей на карте.</p>
266
- <label for="object-select">Модель для размещения:</label>
267
- <select id="object-select">
268
- <option value="tree">Дерево</option>
269
- <option value="rock">Камень</option>
270
- </select>
271
- <button id="clear-placed-objects" class="danger-button">Очистить размещенные объекты</button>
272
- </div>
273
- <div class="ui-group">
274
- <h3>Растительность</h3>
275
- <p style="font-size:0.8em; color: #aaa;">Режим "Растительность" в кистях.</p>
276
- <button id="clear-foliage" class="danger-button">Очистить растительность</button>
277
  </div>
278
  </div>
279
  </div>
@@ -311,8 +287,6 @@ EDITOR_TEMPLATE = '''
311
  import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
312
  import { SSAOPass } from 'three/addons/postprocessing/SSAOPass.js';
313
  import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
314
- import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
315
- import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
316
  import { createNoise2D } from 'simplex-noise';
317
 
318
  let scene, camera, renderer, orbitControls, pointerLockControls, composer, brushHelper, terrainMesh, sky, sun;
@@ -325,9 +299,6 @@ EDITOR_TEMPLATE = '''
325
  const MAX_GRASS_COUNT = 100000;
326
  const terrainDimensions = { width: 100, height: 100, segmentsX: 100, segmentsY: 100 };
327
 
328
- let placedObjectsGroup;
329
- let models = {};
330
-
331
  let isPlayMode = false;
332
  let player, playerVelocity = new THREE.Vector3(), playerOnGround = false;
333
  const playerHeight = 1.8;
@@ -341,231 +312,99 @@ EDITOR_TEMPLATE = '''
341
  const textureLoader = new THREE.TextureLoader();
342
  let customTextures = {};
343
  let flattenHeight = null;
344
-
345
- const TEXTURE_ASSET_BASE = 'https://raw.githubusercontent.com/dream-dev-ar/3D-Designer-Assets/main/textures/';
346
- const MODEL_ASSET_BASE = 'https://raw.githubusercontent.com/dream-dev-ar/3D-Designer-Assets/main/models/';
347
- const ENV_MAP_ASSET = 'https://raw.githubusercontent.com/dream-dev-ar/3D-Designer-Assets/main/env/kloofendal_48d_partly_cloudy_puresky_2k.hdr';
348
 
349
- const loadTexture = (path) => {
350
- const tex = textureLoader.load(TEXTURE_ASSET_BASE + path);
351
  tex.wrapS = THREE.RepeatWrapping;
352
  tex.wrapT = THREE.RepeatWrapping;
353
  tex.anisotropy = 16;
354
- tex.colorSpace = THREE.SRGBColorSpace;
355
  return tex;
356
  };
357
 
358
- const pbrTextures = {
359
- grass: {
360
- map: loadTexture('grass/color.jpg'),
361
- normalMap: loadTexture('grass/normal.jpg'),
362
- roughnessMap: loadTexture('grass/roughness.jpg'),
363
- aoMap: loadTexture('grass/ao.jpg'),
364
- },
365
- rock: {
366
- map: loadTexture('rock/color.jpg'),
367
- normalMap: loadTexture('rock/normal.jpg'),
368
- roughnessMap: loadTexture('rock/roughness.jpg'),
369
- aoMap: loadTexture('rock/ao.jpg'),
370
- },
371
- dirt: {
372
- map: loadTexture('dirt/color.jpg'),
373
- normalMap: loadTexture('dirt/normal.jpg'),
374
- roughnessMap: loadTexture('dirt/roughness.jpg'),
375
- aoMap: loadTexture('dirt/ao.jpg'),
376
- },
377
- snow: {
378
- map: loadTexture('snow/color.jpg'),
379
- normalMap: loadTexture('snow/normal.jpg'),
380
- roughnessMap: loadTexture('snow/roughness.jpg'),
381
- aoMap: loadTexture('snow/ao.jpg'),
382
- },
383
- sand: {
384
- map: loadTexture('sand/color.jpg'),
385
- normalMap: loadTexture('sand/normal.jpg'),
386
- roughnessMap: loadTexture('sand/roughness.jpg'),
387
- aoMap: loadTexture('sand/ao.jpg'),
388
- },
389
  };
390
 
391
  const terrainMaterial = new THREE.ShaderMaterial({
392
- lights: true,
393
  uniforms: {
394
- ...THREE.UniformsLib.lights,
395
- ...THREE.UniformsLib.fog,
396
-
397
- uvScale: { value: 30.0 },
398
-
399
- grass_map: { value: pbrTextures.grass.map },
400
- grass_normalMap: { value: pbrTextures.grass.normalMap },
401
- grass_roughnessMap: { value: pbrTextures.grass.roughnessMap },
402
- grass_aoMap: { value: pbrTextures.grass.aoMap },
403
-
404
- rock_map: { value: pbrTextures.rock.map },
405
- rock_normalMap: { value: pbrTextures.rock.normalMap },
406
- rock_roughnessMap: { value: pbrTextures.rock.roughnessMap },
407
- rock_aoMap: { value: pbrTextures.rock.aoMap },
408
-
409
- dirt_map: { value: pbrTextures.dirt.map },
410
- dirt_normalMap: { value: pbrTextures.dirt.normalMap },
411
- dirt_roughnessMap: { value: pbrTextures.dirt.roughnessMap },
412
- dirt_aoMap: { value: pbrTextures.dirt.aoMap },
413
-
414
- snow_map: { value: pbrTextures.snow.map },
415
- snow_normalMap: { value: pbrTextures.snow.normalMap },
416
- snow_roughnessMap: { value: pbrTextures.snow.roughnessMap },
417
- snow_aoMap: { value: pbrTextures.snow.aoMap },
418
-
419
- sand_map: { value: pbrTextures.sand.map },
420
- sand_normalMap: { value: pbrTextures.sand.normalMap },
421
- sand_roughnessMap: { value: pbrTextures.sand.roughnessMap },
422
- sand_aoMap: { value: pbrTextures.sand.aoMap },
423
  },
424
  vertexShader: `
425
- attribute vec4 color;
426
- attribute vec4 tangent;
427
-
428
  varying vec2 vUv;
429
  varying vec3 vNormal;
430
  varying vec3 vViewPosition;
 
431
  varying vec4 vColor;
432
- varying mat3 vTBN;
433
- varying vec3 vWorldPosition;
 
434
 
435
  void main() {
436
  vUv = uv;
437
  vColor = color;
438
-
439
  vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
440
  vViewPosition = -mvPosition.xyz;
441
- vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
442
  vNormal = normalize(normalMatrix * normal);
443
-
444
- vec3 t = normalize(normalMatrix * tangent.xyz);
445
- vec3 b = normalize(cross(vNormal, t) * tangent.w);
446
- vTBN = mat3(t, b, vNormal);
447
-
448
  gl_Position = projectionMatrix * mvPosition;
449
  }
450
  `,
451
  fragmentShader: `
452
- #include <common>
453
- #include <packing>
454
- #include <lights_pars_begin>
455
- #include <fog_pars_fragment>
456
-
457
- uniform float uvScale;
458
-
459
- uniform sampler2D grass_map;
460
- uniform sampler2D grass_normalMap;
461
- uniform sampler2D grass_roughnessMap;
462
- uniform sampler2D grass_aoMap;
463
-
464
- uniform sampler2D rock_map;
465
- uniform sampler2D rock_normalMap;
466
- uniform sampler2D rock_roughnessMap;
467
- uniform sampler2D rock_aoMap;
468
-
469
- uniform sampler2D dirt_map;
470
- uniform sampler2D dirt_normalMap;
471
- uniform sampler2D dirt_roughnessMap;
472
- uniform sampler2D dirt_aoMap;
473
-
474
- uniform sampler2D snow_map;
475
- uniform sampler2D snow_normalMap;
476
- uniform sampler2D snow_roughnessMap;
477
- uniform sampler2D snow_aoMap;
478
-
479
- uniform sampler2D sand_map;
480
- uniform sampler2D sand_normalMap;
481
- uniform sampler2D sand_roughnessMap;
482
- uniform sampler2D sand_aoMap;
483
 
484
  varying vec2 vUv;
 
485
  varying vec3 vNormal;
486
  varying vec3 vViewPosition;
487
- varying vec4 vColor;
488
- varying mat3 vTBN;
489
- varying vec3 vWorldPosition;
490
 
491
  void main() {
492
- vec2 scaledUv = vUv * uvScale;
493
-
494
- vec4 grassColor = texture2D(grass_map, scaledUv);
495
- vec4 rockColor = texture2D(rock_map, scaledUv);
496
- vec4 dirtColor = texture2D(dirt_map, scaledUv);
497
- vec4 snowColor = texture2D(snow_map, scaledUv);
498
- vec4 sandColor = texture2D(sand_map, scaledUv);
499
 
500
- float grassAmount = 1.0 - vColor.r - vColor.g - vColor.b - vColor.a;
501
-
502
- vec3 albedo = grassColor.rgb * grassAmount;
503
- albedo = mix(albedo, rockColor.rgb, vColor.r);
504
- albedo = mix(albedo, dirtColor.rgb, vColor.g);
505
- albedo = mix(albedo, snowColor.rgb, vColor.b);
506
- albedo = mix(albedo, sandColor.rgb, vColor.a);
 
 
 
 
507
 
508
- vec3 grassNormal = texture2D(grass_normalMap, scaledUv).xyz * 2.0 - 1.0;
509
- vec3 rockNormal = texture2D(rock_normalMap, scaledUv).xyz * 2.0 - 1.0;
510
- vec3 dirtNormal = texture2D(dirt_normalMap, scaledUv).xyz * 2.0 - 1.0;
511
- vec3 snowNormal = texture2D(snow_normalMap, scaledUv).xyz * 2.0 - 1.0;
512
- vec3 sandNormal = texture2D(sand_normalMap, scaledUv).xyz * 2.0 - 1.0;
513
-
514
- vec3 blendedNormal = grassNormal * grassAmount;
515
- blendedNormal = mix(blendedNormal, rockNormal, vColor.r);
516
- blendedNormal = mix(blendedNormal, dirtNormal, vColor.g);
517
- blendedNormal = mix(blendedNormal, snowNormal, vColor.b);
518
- blendedNormal = mix(blendedNormal, sandNormal, vColor.a);
519
-
520
- vec3 normal = normalize(vTBN * normalize(blendedNormal));
521
-
522
- float grassRoughness = texture2D(grass_roughnessMap, scaledUv).r;
523
- float rockRoughness = texture2D(rock_roughnessMap, scaledUv).r;
524
- float dirtRoughness = texture2D(dirt_roughnessMap, scaledUv).r;
525
- float snowRoughness = texture2D(snow_roughnessMap, scaledUv).r;
526
- float sandRoughness = texture2D(sand_roughnessMap, scaledUv).r;
527
-
528
- float roughness = grassRoughness * grassAmount;
529
- roughness = mix(roughness, rockRoughness, vColor.r);
530
- roughness = mix(roughness, dirtRoughness, vColor.g);
531
- roughness = mix(roughness, snowRoughness, vColor.b);
532
- roughness = mix(roughness, sandRoughness, vColor.a);
533
-
534
- float grassAo = texture2D(grass_aoMap, scaledUv).r;
535
- float rockAo = texture2D(rock_aoMap, scaledUv).r;
536
- float dirtAo = texture2D(dirt_aoMap, scaledUv).r;
537
- float snowAo = texture2D(snow_aoMap, scaledUv).r;
538
- float sandAo = texture2D(sand_aoMap, scaledUv).r;
539
-
540
- float ao = grassAo * grassAmount;
541
- ao = mix(ao, rockAo, vColor.r);
542
- ao = mix(ao, dirtAo, vColor.g);
543
- ao = mix(ao, snowAo, vColor.b);
544
- ao = mix(ao, sandAo, vColor.a);
545
-
546
- float metalness = 0.0;
547
-
548
- ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );
549
-
550
- vec3 viewDir = normalize(vViewPosition);
551
- float dotNV = saturate( dot( normal, viewDir ) );
552
- vec3 F0 = vec3(0.04);
553
- vec3 specular = vec3(0.0);
554
-
555
- #if NUM_DIR_LIGHTS > 0
556
- for( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) {
557
- vec3 lightDirection = directionalLights[i].direction;
558
- vec3 lightColor = directionalLights[i].color;
559
- reflectedLight.directDiffuse += BRDF_Lambert( lightColor ) * albedo;
560
- }
561
- #endif
562
 
563
- vec3 outgoingLight = reflectedLight.directDiffuse * ao + reflectedLight.indirectDiffuse * ao + reflectedLight.directSpecular + reflectedLight.indirectSpecular;
564
- gl_FragColor = vec4( outgoingLight, 1.0 );
 
 
 
565
 
566
- #include <tonemapping_fragment>
567
- #include <colorspace_fragment>
568
- #include <fog_fragment>
569
  }
570
  `
571
  });
@@ -584,32 +423,49 @@ EDITOR_TEMPLATE = '''
584
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
585
  renderer.outputColorSpace = THREE.SRGBColorSpace;
586
  document.body.appendChild(renderer.domElement);
587
-
588
- const rgbeLoader = new RGBELoader();
589
- rgbeLoader.load(ENV_MAP_ASSET, function (texture) {
590
- texture.mapping = THREE.EquirectangularReflectionMapping;
591
- scene.environment = texture;
592
- scene.background = texture;
593
- });
594
 
595
  orbitControls = new OrbitControls(camera, renderer.domElement);
596
  orbitControls.enableDamping = true;
597
  orbitControls.maxPolarAngle = Math.PI / 2.1;
598
 
599
- const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.1);
600
  scene.add(hemiLight);
601
 
602
- const dirLight = new THREE.DirectionalLight(0xffffff, 4.0);
603
  dirLight.position.set(100, 100, 50);
604
  dirLight.castShadow = true;
605
- dirLight.shadow.mapSize.width = 4096;
606
- dirLight.shadow.mapSize.height = 4096;
607
- dirLight.shadow.camera.top = 150;
608
- dirLight.shadow.camera.bottom = -150;
609
- dirLight.shadow.camera.left = -150;
610
- dirLight.shadow.camera.right = 150;
611
  dirLight.shadow.bias = -0.001;
612
  scene.add(dirLight);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
613
 
614
  const brushGeometry = new THREE.CylinderGeometry(1, 1, 100, 32, 1, true);
615
  const brushMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00, wireframe: true, transparent: true, opacity: 0.5 });
@@ -617,11 +473,7 @@ EDITOR_TEMPLATE = '''
617
  brushHelper.visible = false;
618
  scene.add(brushHelper);
619
 
620
- placedObjectsGroup = new THREE.Group();
621
- scene.add(placedObjectsGroup);
622
-
623
  initFoliage();
624
- initModels();
625
  initPostprocessing();
626
  initPlayer();
627
 
@@ -634,31 +486,6 @@ EDITOR_TEMPLATE = '''
634
  setupUIListeners();
635
  animate();
636
  }
637
-
638
- function initModels() {
639
- const gltfLoader = new GLTFLoader();
640
- gltfLoader.load(MODEL_ASSET_BASE + 'tree.glb', (gltf) => {
641
- const model = gltf.scene;
642
- model.traverse(node => {
643
- if (node.isMesh) {
644
- node.castShadow = true;
645
- node.receiveShadow = true;
646
- }
647
- });
648
- models['tree'] = model;
649
- });
650
- gltfLoader.load(MODEL_ASSET_BASE + 'rock.glb', (gltf) => {
651
- const model = gltf.scene;
652
- model.traverse(node => {
653
- if (node.isMesh) {
654
- node.castShadow = true;
655
- node.receiveShadow = true;
656
- }
657
- });
658
- models['rock'] = model;
659
- });
660
- }
661
-
662
 
663
  function initPostprocessing() {
664
  composer = new EffectComposer(renderer);
@@ -671,7 +498,7 @@ EDITOR_TEMPLATE = '''
671
  ssaoPass.maxDistance = 0.1;
672
  composer.addPass(ssaoPass);
673
 
674
- const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 0.4, 0.4, 0.85);
675
  composer.addPass(bloomPass);
676
  }
677
 
@@ -681,7 +508,7 @@ EDITOR_TEMPLATE = '''
681
  grassInstances.geometry.dispose();
682
  grassMaterial.dispose();
683
  }
684
- const grassTexture = textureLoader.load(TEXTURE_ASSET_BASE + 'foliage/grass.png');
685
  grassMaterial = new THREE.ShaderMaterial({
686
  uniforms: {
687
  time: { value: 0 },
@@ -766,32 +593,18 @@ EDITOR_TEMPLATE = '''
766
  document.getElementById('brush-strength').addEventListener('input', (e) => {
767
  document.getElementById('brush-strength-value').textContent = e.target.value;
768
  });
769
- document.getElementById('snow-height').addEventListener('input', e => {
770
- document.getElementById('snow-height-value').textContent = e.target.value;
771
- });
772
- document.getElementById('rock-slope').addEventListener('input', e => {
773
- document.getElementById('rock-slope-value').textContent = e.target.value;
774
- });
775
- document.getElementById('apply-auto-texture').addEventListener('click', applyAutoTexturing);
776
  document.getElementById('project-list').addEventListener('change', (e) => {
777
  document.getElementById('project-name').value = e.target.value;
778
  });
779
  document.getElementById('burger-menu').addEventListener('click', () => {
780
  document.getElementById('ui-panel').classList.toggle('open');
781
  });
782
- document.getElementById('clear-foliage').addEventListener('click', () => {
783
- if (confirm('Вы уверены, что хотите удалить всю растительность?')) {
784
  grassInstances.count = 0;
785
  grassInstances.instanceMatrix.needsUpdate = true;
786
  }
787
  });
788
- document.getElementById('clear-placed-objects').addEventListener('click', () => {
789
- if (confirm('Вы уверены, что хотите удалить все размещенные объекты?')) {
790
- while (placedObjectsGroup.children.length > 0) {
791
- placedObjectsGroup.remove(placedObjectsGroup.children[0]);
792
- }
793
- }
794
- });
795
  document.getElementById('play-mode-toggle').addEventListener('click', togglePlayMode);
796
  document.getElementById('update-texture-btn').addEventListener('click', updateCustomTexture);
797
  document.getElementById('rename-texture-slot-btn').addEventListener('click', renameTextureSlot);
@@ -808,7 +621,7 @@ EDITOR_TEMPLATE = '''
808
  const textureSlot = document.getElementById('texture-slot-select').value;
809
 
810
  if (fileInput.files.length === 0) {
811
- alert('Пожалуйста, выберите файл изображения. Обновятся все PBR карты, если найдутся файлы с именами color, normal, roughness, ao.');
812
  return;
813
  }
814
 
@@ -817,13 +630,11 @@ EDITOR_TEMPLATE = '''
817
 
818
  reader.onload = (event) => {
819
  const dataUrl = event.target.result;
820
- const newTexture = textureLoader.load(dataUrl);
821
- newTexture.wrapS = THREE.RepeatWrapping;
822
- newTexture.wrapT = THREE.RepeatWrapping;
823
- newTexture.anisotropy = 16;
824
- newTexture.colorSpace = THREE.SRGBColorSpace;
825
 
826
- const uniformName = textureSlot + '_map';
827
  if (terrainMaterial.uniforms[uniformName]) {
828
  if (terrainMaterial.uniforms[uniformName].value) {
829
  terrainMaterial.uniforms[uniformName].value.dispose();
@@ -875,6 +686,7 @@ EDITOR_TEMPLATE = '''
875
  });
876
  }
877
 
 
878
  function togglePlayMode() {
879
  isPlayMode = !isPlayMode;
880
  const toggleButton = document.getElementById('play-mode-toggle');
@@ -918,9 +730,6 @@ EDITOR_TEMPLATE = '''
918
  terrainMesh.geometry.dispose();
919
  }
920
  initFoliage();
921
- while (placedObjectsGroup.children.length > 0) {
922
- placedObjectsGroup.remove(placedObjectsGroup.children[0]);
923
- }
924
 
925
  terrainDimensions.width = width;
926
  terrainDimensions.height = height;
@@ -954,17 +763,19 @@ EDITOR_TEMPLATE = '''
954
  }
955
  grassInstances.instanceMatrix.needsUpdate = true;
956
  }
957
- if(terrainData.placedObjects) {
958
- terrainData.placedObjects.forEach(objData => {
959
- const model = models[objData.type];
960
- if (model) {
961
- const newObj = model.clone();
962
- newObj.position.copy(objData.position);
963
- newObj.quaternion.copy(objData.quaternion);
964
- newObj.scale.copy(objData.scale);
965
- placedObjectsGroup.add(newObj);
 
966
  }
967
  });
 
968
  }
969
  updateTextureUIAfterLoad(terrainData);
970
  }
@@ -1057,11 +868,7 @@ EDITOR_TEMPLATE = '''
1057
  case 'roughen': roughenTerrain(intersection.point); break;
1058
  case 'smooth': smoothTerrain(intersection.point); break;
1059
  case 'flatten': flattenTerrain(intersection.point); break;
1060
- case 'erode': erodeTerrain(intersection.point); break;
1061
- case 'terrace': terraceTerrain(intersection.point); break;
1062
- case 'pinch': pinchTerrain(intersection.point); break;
1063
  case 'paint': paintTexture(intersection.point); break;
1064
- case 'foliage': placeFoliage(intersection); break;
1065
  case 'place': placeObject(intersection); break;
1066
  }
1067
  }
@@ -1166,91 +973,6 @@ EDITOR_TEMPLATE = '''
1166
  terrainMesh.geometry.computeVertexNormals();
1167
  terrainMesh.geometry.computeTangents();
1168
  }
1169
-
1170
- function erodeTerrain(center) {
1171
- const positions = terrainMesh.geometry.attributes.position;
1172
- const brushSize = parseFloat(document.getElementById('brush-size').value);
1173
- const brushStrength = parseFloat(document.getElementById('brush-strength').value) * 0.05;
1174
- const vertex = new THREE.Vector3();
1175
-
1176
- for (let i = 0; i < positions.count; i++) {
1177
- vertex.fromBufferAttribute(positions, i);
1178
- const distance = vertex.distanceTo(center);
1179
- if (distance < brushSize) {
1180
- const falloff = Math.pow(1 - (distance / brushSize), 2);
1181
- const currentY = positions.getY(i);
1182
-
1183
- let neighborHeightSum = 0;
1184
- let neighborCount = 0;
1185
-
1186
- if (i > terrainDimensions.segmentsX + 1) {
1187
- neighborHeightSum += positions.getY(i - terrainDimensions.segmentsX - 1); neighborCount++;
1188
- }
1189
- if (i < positions.count - terrainDimensions.segmentsX - 1) {
1190
- neighborHeightSum += positions.getY(i + terrainDimensions.segmentsX + 1); neighborCount++;
1191
- }
1192
- if (i % (terrainDimensions.segmentsX + 1) !== 0) {
1193
- neighborHeightSum += positions.getY(i - 1); neighborCount++;
1194
- }
1195
- if (i % (terrainDimensions.segmentsX + 1) !== terrainDimensions.segmentsX) {
1196
- neighborHeightSum += positions.getY(i + 1); neighborCount++;
1197
- }
1198
-
1199
- if(neighborCount > 0) {
1200
- const avgNeighborHeight = neighborHeightSum / neighborCount;
1201
- if(currentY > avgNeighborHeight) {
1202
- positions.setY(i, THREE.MathUtils.lerp(currentY, avgNeighborHeight, falloff * brushStrength));
1203
- }
1204
- }
1205
- }
1206
- }
1207
- positions.needsUpdate = true;
1208
- terrainMesh.geometry.computeVertexNormals();
1209
- terrainMesh.geometry.computeTangents();
1210
- }
1211
-
1212
- function terraceTerrain(center) {
1213
- const positions = terrainMesh.geometry.attributes.position;
1214
- const brushSize = parseFloat(document.getElementById('brush-size').value);
1215
- const brushStrength = parseFloat(document.getElementById('brush-strength').value);
1216
- const vertex = new THREE.Vector3();
1217
-
1218
- for (let i = 0; i < positions.count; i++) {
1219
- vertex.fromBufferAttribute(positions, i);
1220
- const distance = vertex.distanceTo(center);
1221
- if (distance < brushSize) {
1222
- const falloff = Math.pow(1 - (distance / brushSize), 2);
1223
- const currentY = positions.getY(i);
1224
- const steppedY = Math.round(currentY / brushStrength) * brushStrength;
1225
- const newY = THREE.MathUtils.lerp(currentY, steppedY, falloff * 0.5);
1226
- positions.setY(i, newY);
1227
- }
1228
- }
1229
- positions.needsUpdate = true;
1230
- terrainMesh.geometry.computeVertexNormals();
1231
- terrainMesh.geometry.computeTangents();
1232
- }
1233
-
1234
- function pinchTerrain(center) {
1235
- const positions = terrainMesh.geometry.attributes.position;
1236
- const brushSize = parseFloat(document.getElementById('brush-size').value);
1237
- const brushStrength = parseFloat(document.getElementById('brush-strength').value);
1238
- const vertex = new THREE.Vector3();
1239
-
1240
- for (let i = 0; i < positions.count; i++) {
1241
- vertex.fromBufferAttribute(positions, i);
1242
- const distance = vertex.distanceTo(center);
1243
- if (distance < brushSize) {
1244
- const falloff = Math.pow(1 - (distance / brushSize), 4);
1245
- let currentY = positions.getY(i);
1246
- let newY = currentY + falloff * brushStrength * 2;
1247
- positions.setY(i, newY);
1248
- }
1249
- }
1250
- positions.needsUpdate = true;
1251
- terrainMesh.geometry.computeVertexNormals();
1252
- terrainMesh.geometry.computeTangents();
1253
- }
1254
 
1255
  function paintTexture(center) {
1256
  const colors = terrainMesh.geometry.attributes.color;
@@ -1294,7 +1016,7 @@ EDITOR_TEMPLATE = '''
1294
  colors.needsUpdate = true;
1295
  }
1296
 
1297
- function placeFoliage(intersection) {
1298
  const brushSize = parseFloat(document.getElementById('brush-size').value);
1299
  const density = parseFloat(document.getElementById('brush-strength').value) * 2;
1300
  const dummy = new THREE.Object3D();
@@ -1330,54 +1052,6 @@ EDITOR_TEMPLATE = '''
1330
  grassInstances.instanceMatrix.needsUpdate = true;
1331
  }
1332
 
1333
- function placeObject(intersection) {
1334
- const density = parseFloat(document.getElementById('brush-strength').value) * 0.2;
1335
- if(Math.random() > density) return;
1336
-
1337
- const objectType = document.getElementById('object-select').value;
1338
- const model = models[objectType];
1339
- if (!model) return;
1340
-
1341
- const newObject = model.clone();
1342
- newObject.position.copy(intersection.point);
1343
- newObject.rotation.y = Math.random() * Math.PI * 2;
1344
- const scale = Math.random() * 0.5 + 0.75;
1345
- newObject.scale.set(scale, scale, scale);
1346
-
1347
- placedObjectsGroup.add(newObject);
1348
- }
1349
-
1350
- function applyAutoTexturing() {
1351
- if (!terrainMesh) return;
1352
- showSpinner(true);
1353
-
1354
- setTimeout(() => {
1355
- const snowHeight = parseFloat(document.getElementById('snow-height').value);
1356
- const rockSlopeThreshold = 1.0 - parseFloat(document.getElementById('rock-slope').value);
1357
-
1358
- const positions = terrainMesh.geometry.attributes.position;
1359
- const normals = terrainMesh.geometry.attributes.normal;
1360
- const colors = terrainMesh.geometry.attributes.color;
1361
-
1362
- for (let i = 0; i < positions.count; i++) {
1363
- const yPos = positions.getY(i);
1364
- const yNorm = normals.getY(i);
1365
-
1366
- let rock = 0, dirt = 0, snow = 0, sand = 0;
1367
-
1368
- if (yPos > snowHeight) {
1369
- snow = 1.0;
1370
- } else if (yNorm < rockSlopeThreshold) {
1371
- rock = 1.0;
1372
- }
1373
- colors.setXYZW(i, rock, dirt, snow, sand);
1374
- }
1375
- colors.needsUpdate = true;
1376
- showSpinner(false);
1377
- alert('Авто-текстурирование применено.');
1378
- }, 50);
1379
- }
1380
-
1381
  async function saveProject() {
1382
  const projectName = document.getElementById('project-name').value.trim();
1383
  if (!projectName) { alert("Пожалуйста, введите имя проекта."); return; }
@@ -1399,16 +1073,6 @@ EDITOR_TEMPLATE = '''
1399
  textureNames[slotValue] = name;
1400
  });
1401
 
1402
- const serializedObjects = placedObjectsGroup.children.map(obj => {
1403
- const modelName = Object.keys(models).find(key => obj.uuid.startsWith(models[key].uuid));
1404
- return {
1405
- type: modelName || 'rock',
1406
- position: obj.position,
1407
- quaternion: obj.quaternion,
1408
- scale: obj.scale
1409
- }
1410
- });
1411
-
1412
  const projectData = {
1413
  name: projectName,
1414
  width: terrainDimensions.width,
@@ -1418,7 +1082,7 @@ EDITOR_TEMPLATE = '''
1418
  foliage: {
1419
  grass: grassMatrices
1420
  },
1421
- placedObjects: serializedObjects,
1422
  textureNames: textureNames
1423
  };
1424
 
@@ -1513,6 +1177,7 @@ EDITOR_TEMPLATE = '''
1513
  camera.position.copy(player.position);
1514
  }
1515
 
 
1516
  function animate() {
1517
  requestAnimationFrame(animate);
1518
  const deltaTime = clock.getDelta();
 
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 {
 
202
  <div class="radio-group">
203
  <label><input type="radio" name="brush-mode" value="raise" checked> Поднять</label>
204
  <label><input type="radio" name="brush-mode" value="lower"> Опустить</label>
205
+ <label><input type="radio" name="brush-mode" value="roughen"> Шум</label>
206
  <label><input type="radio" name="brush-mode" value="smooth"> Сгладить</label>
207
  <label><input type="radio" name="brush-mode" value="flatten"> Выровнять</label>
 
 
 
 
 
 
208
  <label><input type="radio" name="brush-mode" value="paint"> Текстура</label>
 
209
  <label><input type="radio" name="brush-mode" value="place"> Объект</label>
210
  </div>
211
  <div class="slider-container">
 
213
  <input type="range" id="brush-size" min="1" max="50" value="10">
214
  </div>
215
  <div class="slider-container">
216
+ <label for="brush-strength">Сила: <span id="brush-strength-value">0.5</span></label>
217
  <input type="range" id="brush-strength" min="0.1" max="2" step="0.1" value="0.5">
218
  </div>
219
  </div>
 
226
  <label><input type="radio" name="texture-type" value="snow"> Снег</label>
227
  <label><input type="radio" name="texture-type" value="sand"> Песок</label>
228
  </div>
 
 
 
 
 
 
 
 
229
  </div>
230
  <div class="ui-group" id="texture-management-group">
231
  <h3>Управление текстурами</h3>
 
237
  <option value="snow">Снег</option>
238
  <option value="sand">Песок</option>
239
  </select>
240
+ <label for="custom-texture-file">Выберите файл текстуры:</label>
241
  <input type="file" id="custom-texture-file" accept="image/*">
242
  <button id="update-texture-btn">Обновить текстуру</button>
243
  <label for="texture-slot-name">Новое имя для слота:</label>
 
245
  <button id="rename-texture-slot-btn">Переименовать</button>
246
  </div>
247
  <div class="ui-group">
248
+ <h3>Объекты</h3>
249
+ <div class="radio-group" id="object-selector">
250
+ <label><input type="radio" name="object-type" value="grass" checked> Трава</label>
251
+ </div>
252
+ <button id="clear-objects">Очистить объекты</button>
 
 
 
 
 
 
 
 
253
  </div>
254
  </div>
255
  </div>
 
287
  import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
288
  import { SSAOPass } from 'three/addons/postprocessing/SSAOPass.js';
289
  import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
 
 
290
  import { createNoise2D } from 'simplex-noise';
291
 
292
  let scene, camera, renderer, orbitControls, pointerLockControls, composer, brushHelper, terrainMesh, sky, sun;
 
299
  const MAX_GRASS_COUNT = 100000;
300
  const terrainDimensions = { width: 100, height: 100, segmentsX: 100, segmentsY: 100 };
301
 
 
 
 
302
  let isPlayMode = false;
303
  let player, playerVelocity = new THREE.Vector3(), playerOnGround = false;
304
  const playerHeight = 1.8;
 
312
  const textureLoader = new THREE.TextureLoader();
313
  let customTextures = {};
314
  let flattenHeight = null;
 
 
 
 
315
 
316
+ const loadTexture = (url) => {
317
+ const tex = textureLoader.load(url);
318
  tex.wrapS = THREE.RepeatWrapping;
319
  tex.wrapT = THREE.RepeatWrapping;
320
  tex.anisotropy = 16;
 
321
  return tex;
322
  };
323
 
324
+ const textures = {
325
+ grass: loadTexture('https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/textures/terrain/grasslight-big.jpg'),
326
+ rock: loadTexture('https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/textures/terrain/rock.jpg'),
327
+ dirt: loadTexture('https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/textures/terrain/sand-512.jpg'),
328
+ snow: loadTexture('https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/textures/snow.jpg'),
329
+ sand: loadTexture('https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/textures/terrain/sand-512.jpg'),
330
+ grassNormal: loadTexture('https://raw.githubusercontent.com/mrdoob/three.js/master/examples/textures/terrain/grasslight-big-nm.jpg'),
331
+ rockNormal: loadTexture('https://raw.githubusercontent.com/mrdoob/three.js/master/examples/textures/terrain/rock-nm.jpg')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
  };
333
 
334
  const terrainMaterial = new THREE.ShaderMaterial({
 
335
  uniforms: {
336
+ grassTexture: { value: textures.grass },
337
+ rockTexture: { value: textures.rock },
338
+ dirtTexture: { value: textures.dirt },
339
+ snowTexture: { value: textures.snow },
340
+ sandTexture: { value: textures.sand },
341
+ grassNormalMap: { value: textures.grassNormal },
342
+ rockNormalMap: { value: textures.rockNormal },
343
+ lightDirection: { value: new THREE.Vector3(0.5, 0.5, 0.5).normalize() }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
  },
345
  vertexShader: `
 
 
 
346
  varying vec2 vUv;
347
  varying vec3 vNormal;
348
  varying vec3 vViewPosition;
349
+ attribute vec4 color;
350
  varying vec4 vColor;
351
+ attribute vec4 tangent;
352
+ varying vec3 vTangent;
353
+ varying vec3 vBitangent;
354
 
355
  void main() {
356
  vUv = uv;
357
  vColor = color;
 
358
  vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
359
  vViewPosition = -mvPosition.xyz;
 
360
  vNormal = normalize(normalMatrix * normal);
361
+ vTangent = normalize(normalMatrix * tangent.xyz);
362
+ vBitangent = normalize(cross(vNormal, vTangent) * tangent.w);
 
 
 
363
  gl_Position = projectionMatrix * mvPosition;
364
  }
365
  `,
366
  fragmentShader: `
367
+ uniform sampler2D grassTexture;
368
+ uniform sampler2D rockTexture;
369
+ uniform sampler2D dirtTexture;
370
+ uniform sampler2D snowTexture;
371
+ uniform sampler2D sandTexture;
372
+ uniform sampler2D grassNormalMap;
373
+ uniform sampler2D rockNormalMap;
374
+ uniform vec3 lightDirection;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
 
376
  varying vec2 vUv;
377
+ varying vec4 vColor;
378
  varying vec3 vNormal;
379
  varying vec3 vViewPosition;
380
+ varying vec3 vTangent;
381
+ varying vec3 vBitangent;
 
382
 
383
  void main() {
384
+ vec2 uv_scaled = vUv * 30.0;
 
 
 
 
 
 
385
 
386
+ vec4 grass = texture2D(grassTexture, uv_scaled);
387
+ vec4 rock = texture2D(rockTexture, uv_scaled);
388
+ vec4 dirt = texture2D(dirtTexture, uv_scaled);
389
+ vec4 snow = texture2D(snowTexture, uv_scaled);
390
+ vec4 sand = texture2D(sandTexture, uv_scaled);
391
+
392
+ vec3 finalColor = grass.rgb;
393
+ finalColor = mix(finalColor, rock.rgb, vColor.r);
394
+ finalColor = mix(finalColor, dirt.rgb, vColor.g);
395
+ finalColor = mix(finalColor, snow.rgb, vColor.b);
396
+ finalColor = mix(finalColor, sand.rgb, vColor.a);
397
 
398
+ mat3 tbn = mat3(vTangent, vBitangent, vNormal);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
 
400
+ vec3 grassNormal = texture2D(grassNormalMap, uv_scaled).xyz * 2.0 - 1.0;
401
+ vec3 rockNormal = texture2D(rockNormalMap, uv_scaled).xyz * 2.0 - 1.0;
402
+
403
+ vec3 blendedNormal = normalize(mix(grassNormal, rockNormal, vColor.r));
404
+ vec3 normal = normalize(tbn * blendedNormal);
405
 
406
+ float lighting = dot(normal, lightDirection) * 0.5 + 0.5;
407
+ gl_FragColor = vec4(finalColor * lighting, 1.0);
 
408
  }
409
  `
410
  });
 
423
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
424
  renderer.outputColorSpace = THREE.SRGBColorSpace;
425
  document.body.appendChild(renderer.domElement);
 
 
 
 
 
 
 
426
 
427
  orbitControls = new OrbitControls(camera, renderer.domElement);
428
  orbitControls.enableDamping = true;
429
  orbitControls.maxPolarAngle = Math.PI / 2.1;
430
 
431
+ const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.5);
432
  scene.add(hemiLight);
433
 
434
+ const dirLight = new THREE.DirectionalLight(0xffffff, 1.5);
435
  dirLight.position.set(100, 100, 50);
436
  dirLight.castShadow = true;
437
+ dirLight.shadow.mapSize.width = 2048;
438
+ dirLight.shadow.mapSize.height = 2048;
439
+ dirLight.shadow.camera.top = 100;
440
+ dirLight.shadow.camera.bottom = -100;
441
+ dirLight.shadow.camera.left = -100;
442
+ dirLight.shadow.camera.right = 100;
443
  dirLight.shadow.bias = -0.001;
444
  scene.add(dirLight);
445
+ terrainMaterial.uniforms.lightDirection.value = dirLight.position.clone().normalize();
446
+
447
+ sky = new Sky();
448
+ sky.scale.setScalar(450000);
449
+ scene.add(sky);
450
+ sun = new THREE.Vector3();
451
+ const effectController = {
452
+ turbidity: 10,
453
+ rayleigh: 3,
454
+ mieCoefficient: 0.005,
455
+ mieDirectionalG: 0.7,
456
+ elevation: 4,
457
+ azimuth: 180,
458
+ };
459
+ const uniforms = sky.material.uniforms;
460
+ uniforms[ 'turbidity' ].value = effectController.turbidity;
461
+ uniforms[ 'rayleigh' ].value = effectController.rayleigh;
462
+ uniforms[ 'mieCoefficient' ].value = effectController.mieCoefficient;
463
+ uniforms[ 'mieDirectionalG' ].value = effectController.mieDirectionalG;
464
+ const phi = THREE.MathUtils.degToRad( 90 - effectController.elevation );
465
+ const theta = THREE.MathUtils.degToRad( effectController.azimuth );
466
+ sun.setFromSphericalCoords( 1, phi, theta );
467
+ uniforms[ 'sunPosition' ].value.copy( sun );
468
+ dirLight.position.copy(sun).multiplyScalar(100);
469
 
470
  const brushGeometry = new THREE.CylinderGeometry(1, 1, 100, 32, 1, true);
471
  const brushMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00, wireframe: true, transparent: true, opacity: 0.5 });
 
473
  brushHelper.visible = false;
474
  scene.add(brushHelper);
475
 
 
 
 
476
  initFoliage();
 
477
  initPostprocessing();
478
  initPlayer();
479
 
 
486
  setupUIListeners();
487
  animate();
488
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
489
 
490
  function initPostprocessing() {
491
  composer = new EffectComposer(renderer);
 
498
  ssaoPass.maxDistance = 0.1;
499
  composer.addPass(ssaoPass);
500
 
501
+ const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 0.5, 0.4, 0.85);
502
  composer.addPass(bloomPass);
503
  }
504
 
 
508
  grassInstances.geometry.dispose();
509
  grassMaterial.dispose();
510
  }
511
+ const grassTexture = textureLoader.load('https://threejs.org/examples/textures/sprites/grass.png');
512
  grassMaterial = new THREE.ShaderMaterial({
513
  uniforms: {
514
  time: { value: 0 },
 
593
  document.getElementById('brush-strength').addEventListener('input', (e) => {
594
  document.getElementById('brush-strength-value').textContent = e.target.value;
595
  });
 
 
 
 
 
 
 
596
  document.getElementById('project-list').addEventListener('change', (e) => {
597
  document.getElementById('project-name').value = e.target.value;
598
  });
599
  document.getElementById('burger-menu').addEventListener('click', () => {
600
  document.getElementById('ui-panel').classList.toggle('open');
601
  });
602
+ document.getElementById('clear-objects').addEventListener('click', () => {
603
+ if (confirm('Вы уверены, что хотите удалить все объекты?')) {
604
  grassInstances.count = 0;
605
  grassInstances.instanceMatrix.needsUpdate = true;
606
  }
607
  });
 
 
 
 
 
 
 
608
  document.getElementById('play-mode-toggle').addEventListener('click', togglePlayMode);
609
  document.getElementById('update-texture-btn').addEventListener('click', updateCustomTexture);
610
  document.getElementById('rename-texture-slot-btn').addEventListener('click', renameTextureSlot);
 
621
  const textureSlot = document.getElementById('texture-slot-select').value;
622
 
623
  if (fileInput.files.length === 0) {
624
+ alert('Пожалуйста, выберите файл изображения.');
625
  return;
626
  }
627
 
 
630
 
631
  reader.onload = (event) => {
632
  const dataUrl = event.target.result;
633
+ customTextures[textureSlot] = dataUrl;
634
+
635
+ const newTexture = loadTexture(dataUrl);
636
+ const uniformName = textureSlot + 'Texture';
 
637
 
 
638
  if (terrainMaterial.uniforms[uniformName]) {
639
  if (terrainMaterial.uniforms[uniformName].value) {
640
  terrainMaterial.uniforms[uniformName].value.dispose();
 
686
  });
687
  }
688
 
689
+
690
  function togglePlayMode() {
691
  isPlayMode = !isPlayMode;
692
  const toggleButton = document.getElementById('play-mode-toggle');
 
730
  terrainMesh.geometry.dispose();
731
  }
732
  initFoliage();
 
 
 
733
 
734
  terrainDimensions.width = width;
735
  terrainDimensions.height = height;
 
763
  }
764
  grassInstances.instanceMatrix.needsUpdate = true;
765
  }
766
+ if (terrainData.customTextures) {
767
+ customTextures = terrainData.customTextures;
768
+ Object.entries(customTextures).forEach(([slot, dataUrl]) => {
769
+ const newTexture = loadTexture(dataUrl);
770
+ const uniformName = slot + 'Texture';
771
+ if (terrainMaterial.uniforms[uniformName]) {
772
+ if (terrainMaterial.uniforms[uniformName].value) {
773
+ terrainMaterial.uniforms[uniformName].value.dispose();
774
+ }
775
+ terrainMaterial.uniforms[uniformName].value = newTexture;
776
  }
777
  });
778
+ terrainMaterial.needsUpdate = true;
779
  }
780
  updateTextureUIAfterLoad(terrainData);
781
  }
 
868
  case 'roughen': roughenTerrain(intersection.point); break;
869
  case 'smooth': smoothTerrain(intersection.point); break;
870
  case 'flatten': flattenTerrain(intersection.point); break;
 
 
 
871
  case 'paint': paintTexture(intersection.point); break;
 
872
  case 'place': placeObject(intersection); break;
873
  }
874
  }
 
973
  terrainMesh.geometry.computeVertexNormals();
974
  terrainMesh.geometry.computeTangents();
975
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
976
 
977
  function paintTexture(center) {
978
  const colors = terrainMesh.geometry.attributes.color;
 
1016
  colors.needsUpdate = true;
1017
  }
1018
 
1019
+ function placeObject(intersection) {
1020
  const brushSize = parseFloat(document.getElementById('brush-size').value);
1021
  const density = parseFloat(document.getElementById('brush-strength').value) * 2;
1022
  const dummy = new THREE.Object3D();
 
1052
  grassInstances.instanceMatrix.needsUpdate = true;
1053
  }
1054
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1055
  async function saveProject() {
1056
  const projectName = document.getElementById('project-name').value.trim();
1057
  if (!projectName) { alert("Пожалуйста, введите имя проекта."); return; }
 
1073
  textureNames[slotValue] = name;
1074
  });
1075
 
 
 
 
 
 
 
 
 
 
 
1076
  const projectData = {
1077
  name: projectName,
1078
  width: terrainDimensions.width,
 
1082
  foliage: {
1083
  grass: grassMatrices
1084
  },
1085
+ customTextures: customTextures,
1086
  textureNames: textureNames
1087
  };
1088
 
 
1177
  camera.position.copy(player.position);
1178
  }
1179
 
1180
+
1181
  function animate() {
1182
  requestAnimationFrame(animate);
1183
  const deltaTime = clock.getDelta();