Initial commit
This commit is contained in:
35
css/style.css
Normal file
35
css/style.css
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
|
background: #111;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#scene {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui {
|
||||||
|
position: absolute;
|
||||||
|
top: 40px;
|
||||||
|
left: 40px;
|
||||||
|
color: white;
|
||||||
|
z-index: 10;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui h1 {
|
||||||
|
font-size: 32px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui p {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
29
index.html
Normal file
29
index.html
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>3D Interior</title>
|
||||||
|
<link rel="stylesheet" href="css/style.css" />
|
||||||
|
|
||||||
|
<!-- IMPORT MAP (ОБЯЗАТЕЛЬНО) -->
|
||||||
|
<script type="importmap">
|
||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"three": "https://unpkg.com/three@0.158.0/build/three.module.js",
|
||||||
|
"three/examples/jsm/": "https://unpkg.com/three@0.158.0/examples/jsm/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<canvas id="scene"></canvas>
|
||||||
|
|
||||||
|
<div class="ui">
|
||||||
|
<h1>Современный интерьер</h1>
|
||||||
|
<p>Интерактивная 3D-сцена</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module" src="js/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
12
js/camera.js
Normal file
12
js/camera.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
|
||||||
|
export const camera = new THREE.PerspectiveCamera(
|
||||||
|
30,
|
||||||
|
window.innerWidth / window.innerHeight,
|
||||||
|
0.1,
|
||||||
|
1000
|
||||||
|
);
|
||||||
|
|
||||||
|
// Камера смотрит в комнату через открытую стену
|
||||||
|
camera.position.set(0, 0, 18);
|
||||||
|
camera.lookAt(0, 1, 0);
|
||||||
50
js/controls.js
vendored
Normal file
50
js/controls.js
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { camera } from './camera.js';
|
||||||
|
import { room } from './scene.js';
|
||||||
|
|
||||||
|
export const controls = {
|
||||||
|
isDragging: false,
|
||||||
|
previousMousePosition: { x: 0, y: 0 },
|
||||||
|
rotationSpeed: 0.01,
|
||||||
|
zoomSpeed: 0.1,
|
||||||
|
minZoom: 1,
|
||||||
|
maxZoom: 20,
|
||||||
|
|
||||||
|
// Обновление — пока не нужно, всё в событиях
|
||||||
|
update: function() {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Вращение модели при нажатой левой кнопке мыши ---
|
||||||
|
document.addEventListener('mousedown', (event) => {
|
||||||
|
if (event.button === 0) {
|
||||||
|
controls.isDragging = true;
|
||||||
|
controls.previousMousePosition = { x: event.clientX, y: event.clientY };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mouseup', () => {
|
||||||
|
controls.isDragging = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', (event) => {
|
||||||
|
if (controls.isDragging && room) {
|
||||||
|
const deltaX = event.clientX - controls.previousMousePosition.x;
|
||||||
|
const deltaY = event.clientY - controls.previousMousePosition.y;
|
||||||
|
|
||||||
|
room.rotation.y += deltaX * controls.rotationSpeed;
|
||||||
|
room.rotation.x += deltaY * controls.rotationSpeed;
|
||||||
|
|
||||||
|
// Ограничение наклона по X (чтобы не перевернуть комнату)
|
||||||
|
room.rotation.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, room.rotation.x));
|
||||||
|
}
|
||||||
|
|
||||||
|
controls.previousMousePosition = { x: event.clientX, y: event.clientY };
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Зум колёсиком мыши ---
|
||||||
|
document.addEventListener('wheel', (event) => {
|
||||||
|
const delta = event.deltaY * 0.01; // Чем меньше число, тем мягче зум
|
||||||
|
const newZ = camera.position.z + delta * controls.zoomSpeed * camera.position.z;
|
||||||
|
|
||||||
|
// Ограничение диапазона зума
|
||||||
|
camera.position.z = Math.max(controls.minZoom, Math.min(controls.maxZoom, newZ));
|
||||||
|
});
|
||||||
59
js/lights.js
Normal file
59
js/lights.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Базовый свет сцены (Ambient + Directional)
|
||||||
|
*/
|
||||||
|
export function setupLights(scene) {
|
||||||
|
const ambient = new THREE.AmbientLight(0xffffff, 1.3);
|
||||||
|
scene.add(ambient);
|
||||||
|
|
||||||
|
const key = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||||
|
key.position.set(5, 6, 4);
|
||||||
|
scene.add(key);
|
||||||
|
|
||||||
|
const fill = new THREE.DirectionalLight(0xffffff, 0.2);
|
||||||
|
fill.position.set(-5, 3, -4);
|
||||||
|
scene.add(fill);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Потолочные споты, привязанные к Spot_*
|
||||||
|
*/
|
||||||
|
export function setupRoomLights(room) {
|
||||||
|
const spotNames = [
|
||||||
|
'Spot_001','Spot_002','Spot_003','Spot_004',
|
||||||
|
'Spot_005','Spot_006','Spot_007','Spot_008'
|
||||||
|
];
|
||||||
|
|
||||||
|
spotNames.forEach((spotName) => {
|
||||||
|
const spotObject = room.getObjectByName(spotName);
|
||||||
|
if (!spotObject) {
|
||||||
|
console.warn(`Объект не найден: ${spotName}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === СПОТ-СВЕТ ===
|
||||||
|
const spotLight = new THREE.SpotLight(0xffffff, 5, 80, Math.PI/4, 0.5, 1);
|
||||||
|
spotObject.add(spotLight);
|
||||||
|
spotLight.position.set(0, 0, 0);
|
||||||
|
|
||||||
|
// === Target по -Z (вниз) ===
|
||||||
|
spotObject.add(spotLight.target);
|
||||||
|
spotLight.target.position.set(0, 0, -1);
|
||||||
|
spotLight.target.updateMatrixWorld(true);
|
||||||
|
|
||||||
|
// === ВИЗУАЛЬНАЯ ТОЧКА ИСТОЧНИКА (сфера) ===
|
||||||
|
const bulb = new THREE.Mesh(
|
||||||
|
new THREE.SphereGeometry(0.02, 12, 12),
|
||||||
|
new THREE.MeshStandardMaterial({
|
||||||
|
color: 0xffffff,
|
||||||
|
emissive: 0xffffff,
|
||||||
|
emissiveIntensity: 2
|
||||||
|
})
|
||||||
|
);
|
||||||
|
bulb.position.set(0, 0, 0);
|
||||||
|
spotObject.add(bulb);
|
||||||
|
|
||||||
|
console.log(`Спот корректно создан: ${spotName}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
11
js/main.js
Normal file
11
js/main.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { scene, renderer } from './scene.js';
|
||||||
|
import { camera } from './camera.js';
|
||||||
|
import { controls } from './controls.js';
|
||||||
|
|
||||||
|
function animate() {
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
controls.update();
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
}
|
||||||
|
|
||||||
|
animate();
|
||||||
25
js/materials.js
Normal file
25
js/materials.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
// materials.js
|
||||||
|
import * as THREE from 'three';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Применяет стандартный матовый материал ко всем мешам объекта
|
||||||
|
* @param {THREE.Object3D} object - объект сцены или room
|
||||||
|
* @param {Object} options - {color, roughness, metalness}
|
||||||
|
*/
|
||||||
|
export function applyMatteMaterial(object, options = {}) {
|
||||||
|
const color = options.color !== undefined ? options.color : 0xffffff;
|
||||||
|
const roughness = options.roughness !== undefined ? options.roughness : 1;
|
||||||
|
const metalness = options.metalness !== undefined ? options.metalness : 0;
|
||||||
|
|
||||||
|
object.traverse((child) => {
|
||||||
|
if (child.isMesh) {
|
||||||
|
child.material = new THREE.MeshStandardMaterial({
|
||||||
|
color,
|
||||||
|
roughness,
|
||||||
|
metalness
|
||||||
|
});
|
||||||
|
child.castShadow = true;
|
||||||
|
child.receiveShadow = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
57
js/scene.js
Normal file
57
js/scene.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
||||||
|
import { setupLights, setupRoomLights } from './lights.js';
|
||||||
|
import { camera } from './camera.js';
|
||||||
|
|
||||||
|
const canvas = document.getElementById('scene');
|
||||||
|
|
||||||
|
export const scene = new THREE.Scene();
|
||||||
|
scene.background = new THREE.Color(0x222222);
|
||||||
|
|
||||||
|
export const renderer = new THREE.WebGLRenderer({
|
||||||
|
canvas,
|
||||||
|
antialias: true
|
||||||
|
});
|
||||||
|
|
||||||
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
renderer.setPixelRatio(window.devicePixelRatio);
|
||||||
|
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||||
|
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||||
|
renderer.toneMappingExposure = 0.3;
|
||||||
|
|
||||||
|
// Свет
|
||||||
|
setupLights(scene);
|
||||||
|
|
||||||
|
// ===== ROOM =====
|
||||||
|
export let room = null; // Экспортируем для вращения
|
||||||
|
const loader = new GLTFLoader();
|
||||||
|
loader.load(
|
||||||
|
'models/room.glb',
|
||||||
|
(gltf) => {
|
||||||
|
room = gltf.scene;
|
||||||
|
|
||||||
|
// Центрируем модель
|
||||||
|
const box = new THREE.Box3().setFromObject(room);
|
||||||
|
const center = box.getCenter(new THREE.Vector3());
|
||||||
|
room.position.sub(center);
|
||||||
|
|
||||||
|
// Масштаб 1:1 так как модель уже в метрах
|
||||||
|
room.scale.set(1, 1, 1);
|
||||||
|
|
||||||
|
// Инициализируем свет комнаты
|
||||||
|
setupRoomLights(room);
|
||||||
|
|
||||||
|
scene.add(room);
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
(error) => {
|
||||||
|
console.error('GLB LOAD ERROR', error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Resize
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
camera.aspect = window.innerWidth / window.innerHeight;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
});
|
||||||
BIN
models/room.glb
Normal file
BIN
models/room.glb
Normal file
Binary file not shown.
BIN
textures/hdri.hdr
Normal file
BIN
textures/hdri.hdr
Normal file
Binary file not shown.
Reference in New Issue
Block a user