commit 4a4a7f021340e870cef6541fe5a13d58c2cd8b5f Author: Oleg Tolchin Date: Thu Jan 8 16:37:17 2026 +0300 Initial commit diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..074b36f --- /dev/null +++ b/css/style.css @@ -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; +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..4628687 --- /dev/null +++ b/index.html @@ -0,0 +1,29 @@ + + + + + 3D Interior + + + + + + + + + +
+

Современный интерьер

+

Интерактивная 3D-сцена

+
+ + + + diff --git a/js/camera.js b/js/camera.js new file mode 100644 index 0000000..6c2b624 --- /dev/null +++ b/js/camera.js @@ -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); diff --git a/js/controls.js b/js/controls.js new file mode 100644 index 0000000..02611d4 --- /dev/null +++ b/js/controls.js @@ -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)); +}); diff --git a/js/lights.js b/js/lights.js new file mode 100644 index 0000000..8d12758 --- /dev/null +++ b/js/lights.js @@ -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}`); + }); +} diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..52a2ca9 --- /dev/null +++ b/js/main.js @@ -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(); diff --git a/js/materials.js b/js/materials.js new file mode 100644 index 0000000..74dbeef --- /dev/null +++ b/js/materials.js @@ -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; + } + }); +} diff --git a/js/scene.js b/js/scene.js new file mode 100644 index 0000000..48677e6 --- /dev/null +++ b/js/scene.js @@ -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(); +}); diff --git a/models/room.glb b/models/room.glb new file mode 100644 index 0000000..40e713b Binary files /dev/null and b/models/room.glb differ diff --git a/textures/hdri.hdr b/textures/hdri.hdr new file mode 100644 index 0000000..4c85efc Binary files /dev/null and b/textures/hdri.hdr differ