Clean start: remove large .spp files from history
This commit is contained in:
56
README.md
Normal file
56
README.md
Normal 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
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(
|
||||
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
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));
|
||||
});
|
||||
238
js/lights.js
Normal file
238
js/lights.js
Normal 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
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();
|
||||
74
js/scene.js
Normal file
74
js/scene.js
Normal 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
2215
models/room.glb.log
Normal file
File diff suppressed because it is too large
Load Diff
BIN
textures/hdri.hdr
Normal file
BIN
textures/hdri.hdr
Normal file
Binary file not shown.
BIN
textures/hdri_1.exr
Normal file
BIN
textures/hdri_1.exr
Normal file
Binary file not shown.
Reference in New Issue
Block a user