Update app.py
Browse files
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;
|
| 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"
|
| 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">Выберите файл
|
| 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
|
| 265 |
-
<
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 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 = (
|
| 350 |
-
const tex = textureLoader.load(
|
| 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
|
| 359 |
-
grass:
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 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 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 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 |
-
|
| 433 |
-
varying vec3
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
uniform
|
| 458 |
-
|
| 459 |
-
uniform
|
| 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
|
| 488 |
-
varying
|
| 489 |
-
varying vec3 vWorldPosition;
|
| 490 |
|
| 491 |
void main() {
|
| 492 |
-
vec2
|
| 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 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 507 |
|
| 508 |
-
|
| 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
|
| 564 |
-
|
|
|
|
|
|
|
|
|
|
| 565 |
|
| 566 |
-
|
| 567 |
-
|
| 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.
|
| 600 |
scene.add(hemiLight);
|
| 601 |
|
| 602 |
-
const dirLight = new THREE.DirectionalLight(0xffffff,
|
| 603 |
dirLight.position.set(100, 100, 50);
|
| 604 |
dirLight.castShadow = true;
|
| 605 |
-
dirLight.shadow.mapSize.width =
|
| 606 |
-
dirLight.shadow.mapSize.height =
|
| 607 |
-
dirLight.shadow.camera.top =
|
| 608 |
-
dirLight.shadow.camera.bottom = -
|
| 609 |
-
dirLight.shadow.camera.left = -
|
| 610 |
-
dirLight.shadow.camera.right =
|
| 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.
|
| 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(
|
| 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-
|
| 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('Пожалуйста, выберите файл изображения.
|
| 812 |
return;
|
| 813 |
}
|
| 814 |
|
|
@@ -817,13 +630,11 @@ EDITOR_TEMPLATE = '''
|
|
| 817 |
|
| 818 |
reader.onload = (event) => {
|
| 819 |
const dataUrl = event.target.result;
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
newTexture
|
| 823 |
-
|
| 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 |
-
|
| 958 |
-
terrainData.
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
|
| 962 |
-
|
| 963 |
-
|
| 964 |
-
|
| 965 |
-
|
|
|
|
| 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
|
| 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 |
-
|
| 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();
|