Clean start: remove large .spp files from history

This commit is contained in:
2026-02-21 16:06:11 +03:00
commit a600a7ad21
11 changed files with 2720 additions and 0 deletions

56
README.md Normal file
View File

@@ -0,0 +1,56 @@
# Room-3D
Интерактивный 3D-сайт для демонстрации контактов, презентации и резюме.
## О проекте
Это современный веб-сайт с 3D-интерактивной сценой интерьера, разработанный на базе **Three.js**. Проект служит персональной визитной карточкой, презентацией портфолио и онлайн-резюме.
## Возможности
- 🏠 Интерактивная 3D-комната с реалистичным освещением
- 🖱️ Вращение модели мышью и зум колёсиком
- 💡 Реалистичное освещение: Spot-светильники и LED-ленты
- 🌆 HDRI-окружение для фотореалистичности
- 📱 Адаптивный дизайн
## Технологии
- **Three.js** — 3D-графика
- **HTML5 / CSS3** — верстка и стили
- **JavaScript (ES6 modules)** — логика приложения
## Установка и запуск
Проект не требует сборки. Просто откройте `index.html` в браузере или используйте локальный сервер:
```bash
# Python 3
python -m http.server 8000
# Node.js (если установлен http-server)
npx http-server
```
Затем перейдите по адресу: `http://localhost:8000`
## Структура проекта
```
/
├── index.html # Главная страница
├── css/
│ └── style.css # Стили
├── js/
│ ├── main.js # Точка входа
│ ├── scene.js # Сцена и рендерер
│ ├── camera.js # Камера
│ ├── controls.js # Управление мышью
│ └── lights.js # Освещение
├── models/ # 3D-модели
└── textures/ # Текстуры и HDRI
```
## Лицензия
© Oleg Tolchin

35
css/style.css Normal file
View 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
View 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
View File

@@ -0,0 +1,12 @@
import * as THREE from 'three';
export const camera = new THREE.PerspectiveCamera(
27,
window.innerWidth / window.innerHeight,
0.1,
1000
);
// Камера смотрит в комнату через открытую стену
camera.position.set(0, 0, 0);
camera.lookAt(0, 0, 0);

50
js/controls.js vendored Normal file
View 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));
});

238
js/lights.js Normal file
View File

@@ -0,0 +1,238 @@
import * as THREE from 'three';
import { RectAreaLightUniformsLib } from 'three/examples/jsm/lights/RectAreaLightUniformsLib.js';
import { RectAreaLightHelper } from 'three/examples/jsm/helpers/RectAreaLightHelper.js';
RectAreaLightUniformsLib.init();
/**
* Базовый свет сцены (Ambient + Directional)
*/
export function setupLights(scene) {
const ambient = new THREE.AmbientLight(0xffffff, 0.8);
scene.add(ambient);
const key = new THREE.DirectionalLight(0xffffff, 0.2);
key.position.set(0, 0, 0);
scene.add(key);
const fill = new THREE.DirectionalLight(0xffffff, 0.2);
fill.position.set(0, 0, 0);
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, // intensity
80, // distance
Math.PI / 4, // angle
0.5, // penumbra
1.5 // decay (физически корректный)
);
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);
// === ВИЗУАЛЬНЫЙ СВЕТЯЩИЙСЯ КРУГ (LED-линза) ===
const bulb = new THREE.Mesh(
new THREE.CircleGeometry(0.01, 24),
new THREE.MeshStandardMaterial({
color: 0xffffff,
emissive: 0xffffff,
emissiveIntensity: 2.5,
roughness: 0.2,
metalness: 0.0,
side: THREE.DoubleSide
})
);
// CircleGeometry смотрит по +Z → поворачиваем вниз
bulb.rotation.x = -Math.PI;
bulb.position.set(0, 0, 0.001);
spotObject.add(bulb);
console.log(`Спот корректно создан: ${spotName}`);
});
}
/**
* Создает LED-ленту с рассеянным светом на объекте ceilinglight
* @param {THREE.Object3D} ceilinglight - объект для привязки LED-ленты
* @param {Object} options - настройки { position: [x,y,z], rotationDeg: [x,y,z], ... }
*/
function createLEDStrip(ceilinglight, options = {}) {
// Конвертация градусов в радианы
const toRad = (deg) => deg * Math.PI / 180;
// Размеры LED-ленты (подстрой под модель)
const width = options.width || 0.01;
const height = options.height || 1.0;
// RectAreaLight - рассеянный свет
const rectLight = new THREE.RectAreaLight(
options.color || 0xffffff,
options.intensity || 6,
width,
height
);
ceilinglight.add(rectLight);
// Позиция
if (options.position) {
rectLight.position.set(...options.position);
}
// Поворот (градусы)
if (options.rotationDeg) {
rectLight.rotation.set(...options.rotationDeg.map(toRad));
}
// Направление света (target - локальные координаты)
if (options.target) {
const targetObj = new THREE.Object3D();
targetObj.position.set(...options.target);
ceilinglight.add(targetObj);
rectLight.target = targetObj;
}
// Визуальная полоса LED
const ledMesh = new THREE.Mesh(
new THREE.PlaneGeometry(width, height),
new THREE.MeshStandardMaterial({
color: 0xffffff,
emissive: 0xffffff,
emissiveIntensity: 2,
side: THREE.DoubleSide
})
);
if (options.position) {
ledMesh.position.set(...options.position);
}
if (options.rotationDeg) {
ledMesh.rotation.set(...options.rotationDeg.map(toRad));
}
ceilinglight.add(ledMesh);
// Helper для визуализации RectAreaLight
const rectLightHelper = new RectAreaLightHelper(rectLight, 0xff0000);
if (options.position) {
rectLightHelper.position.set(...options.position);
}
if (options.rotationDeg) {
rectLightHelper.rotation.set(...options.rotationDeg.map(toRad));
}
ceilinglight.add(rectLightHelper);
// DirectionalLight для теней
const shadowLight = new THREE.DirectionalLight(0xffffff, 0.5);
shadowLight.castShadow = true;
shadowLight.shadow.mapSize.width = 1024;
shadowLight.shadow.mapSize.height = 1024;
shadowLight.shadow.camera.near = 0.1;
shadowLight.shadow.camera.far = 10;
shadowLight.shadow.camera.left = -5;
shadowLight.shadow.camera.right = 5;
shadowLight.shadow.camera.top = 5;
shadowLight.shadow.camera.bottom = -5;
shadowLight.shadow.bias = -0.001;
if (options.shadowPosition) {
shadowLight.position.set(...options.shadowPosition);
} else {
shadowLight.position.set(0, 0, 0);
}
shadowLight.target.position.set(0, 1, 0);
ceilinglight.add(shadowLight);
ceilinglight.add(shadowLight.target);
}
/**
* Создает LED-ленты на всех объектах ceilinglight_001 - ceilinglight_004
* @param {THREE.Object3D} room - объект комнаты, содержащий ceilinglight объекты
*/
export function setupLEDStrip(room) {
// Настройки для каждой ленты
const ledConfigs = {
ceilinglight_001: {
position: [0, 0, 0.01],
rotationDeg: [0, 0, 0],
target: [0, 1, 0],
shadowPosition: [0, 0, 0],
width: 0.01,
height: 1.75,
color: 0xffffff,
intensity: 50
},
ceilinglight_002: {
position: [0, 0, 0.01],
rotationDeg: [0, 0, 90],
target: [0, 1, 0],
shadowPosition: [0, 0, 0],
width: 0.01,
height: 3.6,
color: 0xffffff,
intensity: 50
},
ceilinglight_003: {
position: [0, 0, 0.01],
rotationDeg: [0, 0, 0],
target: [0, 1, 0],
shadowPosition: [0, 0, 0],
width: 0.01,
height: 1.75,
color: 0xffffff,
intensity: 50
},
ceilinglight_004: {
position: [0, 0, 0.01],
rotationDeg: [0, 0, 90],
target: [0, 1, 0],
shadowPosition: [0, 0, 0],
width: 0.01,
height: 3.6,
color: 0xffffff,
intensity: 50
}
};
let created = 0;
for (let i = 1; i <= 4; i++) {
const name = `ceilinglight_${String(i).padStart(3, '0')}`;
const ceilinglight = room.getObjectByName(name);
const options = ledConfigs[name] || {};
if (ceilinglight) {
createLEDStrip(ceilinglight, options);
const worldPos = new THREE.Vector3();
ceilinglight.getWorldPosition(worldPos);
console.log(`LED-лента создана: ${name} | позиция: (${worldPos.x.toFixed(2)}, ${worldPos.y.toFixed(2)}, ${worldPos.z.toFixed(2)})`);
created++;
} else {
console.warn(`Объект не найден: ${name}`);
}
}
console.log(`Создано LED-лент: ${created}`);
}

11
js/main.js Normal file
View 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();

74
js/scene.js Normal file
View File

@@ -0,0 +1,74 @@
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js';
import { setupLights, setupRoomLights, setupLEDStrip } 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);
// ===== HDRI =====
const rgbeLoader = new RGBELoader();
rgbeLoader.load(
'textures/hdri_1.exr',
(texture) => {
texture.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = texture;
scene.background = texture;
console.log('HDRI загружен: textures/hdri_1.exr');
},
undefined,
(error) => {
console.error('HDRI LOAD ERROR', error);
}
);
// ===== ROOM =====
export let room = null; // Экспортируем для вращения
const loader = new GLTFLoader();
loader.load(
'models/room.glb',
(gltf) => {
room = gltf.scene;
// Центрируем модель по Y (только вверх/вниз)
const box = new THREE.Box3().setFromObject(room);
const centerY = (box.min.y + box.max.y) / 2;
room.position.y = -centerY;
// Инициализируем свет комнаты
setupRoomLights(room);
// LED-ленты на ceilinglight_001 - ceilinglight_004
setupLEDStrip(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();
});

2215
models/room.glb.log Normal file

File diff suppressed because it is too large Load Diff

BIN
textures/hdri.hdr Normal file

Binary file not shown.

BIN
textures/hdri_1.exr Normal file

Binary file not shown.