- Этап 1: Вход в профессию (Web & React Basics)
- Этап 2: 3D-Грамотность (General Three.js Concepts)
- Этап 3: Создание Миров (Interactive & Creative)
- Этап 4: Deep Dive (Математика и Шейдеры)
- Этап 4: Deep Dive (Математика и Шейдеры)
- Этап 5: Архитектура и Продакшен (Expert / Tech Lead)
Цель: Перестать бояться кода и понять, как работает UI.
- HTML/CSS/Terminal: Верстка, Flexbox, npm/yarn, Vite.
- JavaScript Core: Переменные, циклы, функции, ES6+ (map, filter, destructuring, modules).
- React Fundamentals: JSX, Components, Props, Hooks (
useState,useEffect,useRef). Без этого в R3F делать нечего.
Цель: Научиться мыслить в 3D-пространстве. 4. Сцена и Объекты: Mesh, Geometry, Material. Граф сцены (Parent-Child). 5. Камеры и Свет: Perspective vs Orthographic. PBR-материалы (Physical based rendering). 6. R3F База: <Canvas>, декларативный подход, хук useFrame (game loop). 7. Экосистема Drei: OrbitControls, Environment, Loading Models (GLTF).
Цель: Делать красиво и интерактивно. 8. Интерактивность: Raycaster (клики, ховеры), события мыши. 9. Анимация и Стейт: Библиотеки анимации (GSAP или Framer Motion 3D). Использование Zustand для стейт-менеджмента (избегаем ре-рендеров React!). 10. Физика: Rapier (RigidBody, Collider, Gravity). 11. Пост-процессинг: Bloom, Noise, Glitch, Vignette (библиотека @react-three/postprocessing).
Цель: Создавать то, чего нет в стандартной библиотеке. 12. Математика 3D: * Векторы (сложение, нормализация, Dot/Cross products). * Матрицы (Model, View, Projection matrices). * Кватернионы (почему Euler rotation вызывает Gimbal Lock). 13. Введение в Шейдеры (GLSL): * Vertex Shader (изменяем форму) vs Fragment Shader (рисуем пиксели). * Attributes, Uniforms, Varyings. * ShaderMaterial в R3F. 14. Процедурная генерация: Шум Перлина, создание ландшафтов математикой. 15. Партиклы (Частицы): Рендеринг миллионов точек с высокой производительностью.
Цель: Делать приложения, которые работают 60 FPS на слабом ноутбуке. 16. Производительность (Performance): * InstancedMesh (1000 объектов за 1 вызов отрисовки). * Draco/Meshopt компрессия. * Управление ресурсами: dispose() текстур и геометрии (утечки памяти в WebGL фатальны). * Мониторинг r3f-perf. 17. Интеграция с Blender: * Texture Baking (запекание теней и света в текстуру для реализма без нагрузки на GPU). * Оптимизация топологии модели. 18. TypeScript в R3F: Типизация пропсов, рефов и событий. 19. WebXR (VR/AR): Адаптация сцены под шлемы и AR на телефонах.
Прежде чем писать код на JavaScript или React, нам нужно подготовить место, где будет жить наша 3D-сцена. В отличие от обычных сайтов, где контент идет сверху вниз, 3D-приложения обычно занимают весь экран (как игра).
В этом уроке мы:
- Развернем проект с помощью современного сборщика Vite.
- Настроим HTML (скелет).
- Настроим CSS (внешний вид), чтобы убрать белые рамки и растянуть приложение на весь экран.
Чтобы запускать современные веб-проекты, нам нужна среда выполнения Node.js (это как JRE для Java, только для JavaScript вне браузера).
- Node.js: Двигатель.
- npm (Node Package Manager): Магазин запчастей (библиотек). Аналог Maven/Gradle.
Практика (в терминале/консоли): Мы будем использовать Vite (читается «Вит»). Это современный инструмент, который создает пустой проект с React очень быстро.
Откройте терминал в папке, где будут лежать проекты, и введите:
# Создаем проект.
# npm create vite@latest <имя-папки> -- --template react
npm create vite@latest my-3d-world -- --template react
# Переходим в созданную папку
cd my-3d-world
# Устанавливаем зависимости (скачиваем библиотеки, указанные в package.json)
npm install
# Запускаем локальный сервер разработки
npm run dev
Если в терминале появилась ссылка http://localhost:5173, значит сервер запущен. Откройте её в браузере.
В папке проекта найдите файл index.html. Это единственный HTML-файл в нашем приложении (Single Page Application).
В React-приложениях HTML почти пустой. Нам важен только один элемент — контейнер, внутрь которого React будет «вставлять» всё наше приложение.
Файл: index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My First 3D World</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
По умолчанию браузеры добавляют отступы (margin) для страницы, и div имеет высоту 0, если в нем нет текста. Для 3D это катастрофа: мы хотим видеть сцену на весь экран, без белых полос по краям и полос прокрутки.
Найдите файл src/index.css (или создайте его и подключите). Удалите оттуда всё лишнее и напишите этот базовый код.
Файл: src/index.css
/* Звездочка (*) означает "все элементы".
box-sizing: border-box упрощает расчет размеров:
отступы (padding) и границы (border) не увеличивают ширину элемента.
*/
* {
box-sizing: border-box;
}
/* Настройки для тела страницы и корневого элемента */
html,
body,
#root {
width: 100%; /* Ширина на весь экран */
height: 100%; /* Высота на весь экран */
margin: 0; /* Убираем стандартные отступы браузера (те самые белые полосы) */
padding: 0; /* Убираем внутренние отступы */
overflow: hidden; /* ВАЖНО: Скрываем полосы прокрутки. В 3D скролл обычно не нужен. */
}
/* Дополнительно: зададим базовый цвет фона, чтобы глазам было приятно */
body {
background-color: #1a1a1a; /* Темно-серый цвет */
font-family: sans-serif; /* Шрифт без засечек */
}
/* Специальный хак для Canvas (холста Three.js).
Canvas - это строчный элемент (как буква).
Из-за этого под ним может появляться микро-отступ в 4 пикселя.
Превращаем его в блок, чтобы он вел себя предсказуемо.
*/
canvas {
display: block;
}
Что мы имеем на данный момент:
- Рабочий станок: У нас есть запущенный сервер (
npm run dev), который мгновенно обновляет страницу, когда мы сохраняем код. - Чистый лист: Наш
index.htmlиindex.cssнастроены так, чтобы приложение занимало 100% ширины и высоты окна браузера, без рамок и скроллбаров.
Это идеальная заготовка для полноэкранного 3D.
В 3D-графике всё есть данные.
- Позиция игрока — это Объект с координатами
x, y, z. - Список врагов — это Массив.
- Включен ли фонарик — это Булево значение (true/false).
В этом модуле мы разберем синтаксис языка JavaScript, который нужен для описания этих данных. Для вашего ученика это "строительные блоки".
В старых учебниках можно встретить var. Забудьте о нем. В современном JS (ES6+) мы используем только let и const.
const(Константа): Используем в 95% случаев. Это обещание не переприсваивать ссылку на переменную.let: Используем только тогда, когда точно знаем, что значение изменится (например, счетчик очков или текущее время).
// Файл: script.js (пример)
// --- CONST ---
// Мы создаем коробку "playerName" и кладем туда строку.
// Поменять содержимое целиком нельзя.
const playerName = "Neo";
// Ошибка! JavaScript остановит выполнение.
// playerName = "Trinity"; // Uncaught TypeError: Assignment to constant variable.
// --- LET ---
// Создаем коробку "score". Значение может меняться.
let score = 0;
// Это валидно
score = 10;
score = score + 5;
Важный нюанс для 3D: В
constнельзя положить другой объект, но можно менять содержимое этого объекта (об этом ниже).
JavaScript — язык с динамической типизацией. Нам не нужно писать int, float, string. Браузер сам понимает, что внутри.
- Number: В JS нет разделения на
intиdouble. Всё есть число с плавающей точкой (IEEE 754).10— число.3.14— число.
- String: Текст. Можно писать в одинарных
'или двойных"кавычках. - Boolean: Истина (
true) или ложь (false).
const gravity = 9.8; // Number
const levelName = "Mars"; // String
let isGameOver = false; // Boolean
В Java объект — это экземпляр класса. В JavaScript объект — это просто коллекция пар "ключ: значение". Это похоже на HashMap в Java или JSON.
В Three.js и React мы постоянно используем объекты для настроек (пропсов).
// Создаем объект "cubeConfig"
const cubeConfig = {
// Ключ: Значение
color: "red",
width: 1,
height: 2,
isVisible: true,
// Вложенный объект (позиция в 3D)
position: {
x: 0,
y: 10,
z: 0
}
};
// --- ЧТЕНИЕ ДАННЫХ ---
// Доступ через точку (.)
console.log(cubeConfig.color); // Выведет "red"
console.log(cubeConfig.position.y); // Выведет 10
// --- ИЗМЕНЕНИЕ ДАННЫХ ---
// Помните про const?
// Мы не можем сделать: cubeConfig = {} (заменить весь объект).
// Но мы МОЖЕМ менять его внутренности! Это называется мутация (mutation).
cubeConfig.color = "blue";
cubeConfig.position.x = 5;
В React это используется повсеместно. Это способ быстро "вытащить" значения из объекта в отдельные переменные.
const camera = { fov: 75, aspect: 1.5, near: 0.1 };
// СТАРЫЙ СПОСОБ (много кода):
// const fov = camera.fov;
// const aspect = camera.aspect;
// НОВЫЙ СПОСОБ (Деструктуризация):
// Мы говорим: "Найди в объекте camera свойства fov и aspect
// и создай одноименные переменные".
const { fov, aspect } = camera;
console.log(fov); // 75
Массив — это упорядоченный список. В отличие от Java, в один массив JS можно положить что угодно (числа, строки, объекты вперемешку), но хорошим тоном считается хранить однотипные данные.
В 3D массивы часто используются для координат [x, y, z] или цветов [r, g, b].
// Массив координат
const position = [10, 5, 0];
// Доступ по индексу (начинается с 0)
console.log(position[0]); // 10 (это X)
console.log(position[1]); // 5 (это Y)
// Узнать длину
console.log(position.length); // 3
// --- МАССИВ ОБЪЕКТОВ ---
// Представьте инвентарь в игре
const inventory = [
{ id: 1, name: "Sword", damage: 10 },
{ id: 2, name: "Shield", defense: 5 }
];
// Получить урон меча
console.log(inventory[0].damage); // 10
Функция — это переиспользуемый блок кода. Она может принимать аргументы и возвращать результат.
// Объявление функции
function calculateDistance(x1, x2) {
const result = x2 - x1;
return result; // Возврат значения
}
// Вызов функции
const distance = calculateDistance(0, 10);
console.log(distance); // 10
Попросите ученика создать файл practice.js и описать "сущность" игрока, используя пройденные темы.
Пример ожидаемого решения:
// 1. Константа для имени (оно не меняется)
const name = "Hero";
// 2. Let для здоровья (оно меняется)
let hp = 100;
// 3. Объект для позиции
const position = {
x: 0,
y: 0,
z: 0
};
// 4. Массив для оружия
const weapons = ["Sword", "Bow"];
// 5. Простое действие: игрок получил урон и сдвинулся
hp = hp - 10;
position.x = 5;
// Вывод в консоль (для проверки)
console.log("Player:", name, "HP:", hp, "Position:", position);
Итог модуля: Мы научились создавать "контейнеры" для данных. Мы поняли, что объект — это удобная сумка для свойств, а const не запрещает менять содержимое этой сумки.
В этом модуле мы разберем современный синтаксис (ES6+), на котором написан весь React и библиотека R3F. Если прошлый модуль был про «существительные» (данные), то этот — про «глаголы» (действия) и красивое оформление кода.
В современном JS слово function пишут редко. Вместо него используют «стрелку» =>. Это не просто сокращение, это стандарт для написания компонентов в React.
Синтаксис: () => { ... }
// --- СТАРЫЙ СТИЛЬ (Declaration) ---
function add(a, b) {
return a + b;
}
// --- НОВЫЙ СТИЛЬ (Arrow Function) ---
// Читается так: "Создай константу add, которая принимает (a, b) и возвращает..."
const add = (a, b) => {
return a + b;
};
// --- СУПЕР-КОРОТКИЙ СТИЛЬ ---
// Если действие в одну строчку, можно убрать фигурные скобки и слово return.
// JS сам поймет, что нужно вернуть результат вычисления.
const multiply = (a, b) => a * b;
console.log(multiply(2, 3)); // 6
Почему это важно для 3D: В React мы будем часто писать такие функции прямо внутри HTML-тегов для обработки событий. Пример (забегая вперед):
<mesh onClick={() => console.log("Clicked!")} />
Это самый важный метод в React. Забудьте про циклы for (let i = 0...) для отрисовки списков.
В 3D у нас часто есть массив данных (например, координаты 100 астероидов), и нам нужно превратить его в 100 3D-объектов на экране. Метод .map() берет массив, проходит по каждому элементу, как-то его преобразует и возвращает новый массив.
// У нас есть данные (просто числа)
const asteriodSizes = [10, 20, 5];
// Нам нужно превратить их в строки описания
// item — это текущий элемент, который мы обрабатываем прямо сейчас
const descriptions = asteriodSizes.map((item) => {
return `Asteroid size: ${item}`;
});
console.log(descriptions);
// Результат: ["Asteroid size: 10", "Asteroid size: 20", "Asteroid size: 5"]
Аналогия: Представьте конвейер на заводе. На входе — металлические болванки (данные), посередине робот (
.map()), на выходе — готовые детали (объекты сцены).
Три точки ... — это оператор «распыления» (spread). Он позволяет взять все свойства из одного объекта и скопировать их в другой. Это критически важно, когда мы хотим обновить настройки объекта, не переписывая их вручную.
const defaultCube = {
color: "red",
width: 1,
height: 1,
depth: 1
};
// Мы хотим создать "Игрока", который такой же, как куб,
// НО синего цвета.
const player = {
...defaultCube, // "Распакуй" сюда все свойства defaultCube (width, height...)
color: "blue", // Перезапиши color
hp: 100 // Добавь новое свойство
};
console.log(player);
/* Результат:
{
color: "blue", <-- Изменилось
width: 1, <-- Скопировалось
height: 1, <-- Скопировалось
depth: 1, <-- Скопировалось
hp: 100 <-- Добавилось
}
*/
Раньше весь JS-код писали в одном файле на 5000 строк. Сейчас мы разбиваем код на маленькие файлы. Один файл = Один Компонент (обычно).
Чтобы использовать код из FileA.js внутри FileB.js, мы должны его экспортировать и импортировать.
Файл: MathUtils.js (Библиотека полезностей)
// Экспортируем функцию, чтобы другие могли её брать
export const double = (n) => n * 2;
// Экспортируем константу
export const PI = 3.14159;
Файл: Game.js (Главный файл)
// Импортируем то, что нам нужно.
// Обратите внимание на фигурные скобки { } - это именованный импорт.
import { double, PI } from './MathUtils.js';
console.log(double(5)); // 10
Существует также export default (экспорт по умолчанию), когда из файла экспортируется только одна главная вещь (обычно React-компонент).
JS однопоточный. Если вы начнете загружать тяжелую 3D-модель (100 Мб), и JS будет просто ждать, весь браузер зависнет. Чтобы этого не было, используются Промисы (Promises) — это как талончик в очереди. "Мы обещаем вернуть модель, когда она загрузится, а пока занимайся другими делами".
Современный способ работать с этим — ключевые слова async и await.
// Функция загрузки (симуляция)
const loadModel = async () => {
console.log("Начинаю загрузку модели...");
// await говорит: "Подожди здесь, пока fetch скачает файл,
// но не блокируй браузер (пусть анимации крутятся)".
const response = await fetch('model.gltf');
console.log("Модель загружена!");
return response;
};
Мы освоили инструменты профессионала:
- Стрелки
=>для краткости. .map()для превращения данных в элементы....(Spread) для копирования и объединения настроек.import/exportдля разделения кода на файлы.
Мы переходим к фундаменту React. React Three Fiber (R3F) — это не отдельная библиотека, это именно React, который просто рисует не <div> (блоки), а <mesh> (3D-объекты).
Поэтому, чтобы строить 3D-миры, ученик должен мыслить Компонентами.
Обычно новички пугаются, видя HTML-теги внутри JS-файла. Это называется JSX (JavaScript XML).
- Суть: Это синтаксический сахар. Браузер его не понимает, но сборщик (Vite) превращает его в обычные JS-вызовы.
- Главное правило: Внутри JSX можно писать обычный JavaScript, если обернуть его в фигурные скобки
{ ... }.
Пример:
const name = "Neo";
const age = 30;
// Это JSX. Выглядит как HTML, но переменные вставляются через {}
const element = (
<div>
<h1>Hello, {name}!</h1>
<p>Age: {age + 5}</p> {/* Внутри скобок можно считать! Будет 35 */}
</div>
);
Для Java-разработчика: JSX — это не строки (как в JSP), это вызовы функций.
<div id="app">компилируется примерно вReact.createElement('div', { id: 'app' }).
Компонент — это Функция, которая возвращает JSX. В 3D мы будем создавать компоненты: Spaceship, Planet, Tree. Это позволяет разбить огромный мир на маленькие, понятные куски.
Правила:
- Название функции всегда с Большой Буквы (
MyComponent, а неmyComponent). - Функция должна вернуть один родительский элемент (нельзя вернуть два
divподряд без обертки).
Код компонента:
// Файл: Planet.jsx
// 1. Создаем функцию-компонент
const Planet = () => {
const planetName = "Mars";
// 2. Возвращаем разметку
return (
<div className="planet-card">
<h2>{planetName}</h2>
<p>Status: Unknown</p>
</div>
);
};
// 3. Экспортируем, чтобы использовать в других файлах
export default Planet;
Пропсы (Properties) — это данные, которые родитель передает ребенку. Это как аргументы функции. Если компонент — это чертеж детали, то пропсы — это её цвет и размер.
В 3D это критически важно: мы создаем один компонент Asteroid, а потом используем его 100 раз с разными пропсами (координаты x, y, z).
Родитель (App.jsx):
import Planet from './Planet';
const App = () => {
return (
<div>
{/* Мы используем компонент как тег и передаем ему данные */}
<Planet name="Mars" isHabitable={false} />
<Planet name="Earth" isHabitable={true} />
</div>
);
};
Ребенок (Planet.jsx):
// props — это объект, в который React упаковал всё, что передал родитель.
// props = { name: "Mars", isHabitable: false }
// Используем деструктуризацию (тема из Модуля 2), чтобы сразу достать переменные!
const Planet = ({ name, isHabitable }) => {
return (
<div style={{ border: "1px solid white", padding: "10px" }}>
<h2>Planet: {name}</h2>
{/* Тернарный оператор (короткий if/else) */}
{/* Если isHabitable true, пишем "Yes", иначе "No" */}
<p>Can we live here? {isHabitable ? "Yes" : "No"}</p>
</div>
);
};
export default Planet;
Важно: Пропсы доступны только для чтения. Ребенок не может изменить свои пропсы (он не может сам себя переименовать). Изменить их может только Родитель.
Иногда нам нужно вложить один компонент внутрь другого, как матрешку. Для этого используется специальный пропс children.
В R3F это используется постоянно:
<SolarSystem>
<Planet />
<Sun />
</SolarSystem>
Чтобы SolarSystem отрисовала то, что внутри неё, она должна использовать children.
const Card = ({ title, children }) => {
return (
<div className="card">
<h1>{title}</h1>
<div className="content">
{/* Сюда React вставит всё, что мы напишем между открывающим и закрывающим тегом <Card> */}
{children}
</div>
</div>
);
};
Пусть ученик создаст два файла в папке src:
Badge.jsx(Компонент-бейдж). Принимает пропсы:text(имя) иcolor(цвет).- В
App.jsxотрисует три таких бейджа с разными данными.
Пример ожидаемого решения:
// Badge.jsx
const Badge = ({ text, color }) => {
// style в React принимает объект JS, а не строку!
// Поэтому двойные скобки {{ ... }}: внешние для JS, внутренние для объекта.
return (
<div style={{ backgroundColor: color, padding: '10px', margin: '5px' }}>
User: {text}
</div>
);
};
export default Badge;
// App.jsx
import Badge from './Badge';
function App() {
return (
<div>
<Badge text="Admin" color="red" />
<Badge text="User" color="blue" />
<Badge text="Guest" color="grey" />
</div>
);
}
Итог модуля: Теперь ученик понимает, что интерфейс строится из кирпичиков.
- Компонент = Кирпич.
- Пропсы = Цвет и форма кирпича.
Если Компоненты — это скелет и внешний вид, то Хуки — это мозг и нервная система. Без них наше приложение было бы просто статичной картинкой.
Хук (Hook) — это специальная функция, которая начинается с use (использовать). Она позволяет "подцепиться" к возможностям React.
Мы разберем «Святую Троицу» хуков, на которой держится 99% кода в React Three Fiber.
Обычные переменные внутри функции "умирают", когда функция заканчивает работу. Чтобы компонент "запомнил", что мы нажали кнопку или изменили цвет, нужен useState.
- Аналогия для Java: Это поле класса (
private int count) + Сеттер (setCount), который автоматически перерисовывает компонент при изменении.
Синтаксис:
const [переменная, функцияИзменения] = useState(начальноеЗначение);
Пример:
import { useState } from 'react';
const Counter = () => {
// 1. Создаем состояние "count" со значением 0.
// setCount — это "рычаг", чтобы изменить count.
const [count, setCount] = useState(0);
const handleClick = () => {
// ВАЖНО: Мы не пишем count = count + 1. React этого не увидит.
// Мы обязаны использовать функцию-сеттер.
setCount(count + 1);
// Как только сработал setCount, React стирает старый HTML
// и рисует новый с обновленной цифрой.
};
return (
<div>
<p>Вы кликнули: {count} раз</p>
<button onClick={handleClick}>Нажми меня</button>
</div>
);
};
Компоненты рождаются (появляются на экране), живут (обновляются) и умирают (исчезают). useEffect позволяет выполнять код в эти моменты.
- Аналогия для Java: Смесь
@PostConstruct(инициализация) и@PreDestroy(очистка).
В 3D мы используем это, чтобы загрузить модель при старте или запустить звук.
Синтаксис:
useEffect(() => {
// Код, который выполнится
}, [список_зависимостей]);
Массив зависимостей ([]) — самая важная часть:
[](пустой массив) — выполнить один раз при рождении (Mount).[count]— выполнять каждый раз, когда меняется переменнаяcount.- (нет массива) — выполнять при каждой перерисовке (Опасно! Может повесить браузер).
Пример:
import { useEffect } from 'react';
const Game = () => {
useEffect(() => {
console.log("Игра началась! (Компонент создан)");
// Функция очистки (return function).
// Сработает, когда компонент удалят со страницы.
return () => {
console.log("Игра окончена... (Компонент удален)");
};
}, []); // Пустой массив = только 1 раз при старте
return <div>Game is running...</div>;
};
Это самый важный хук для производительности в Three.js.
Проблема: useState вызывает перерисовку (Re-render). Если мы хотим вращать куб 60 раз в секунду, и будем использовать useState, React попытается 60 раз в секунду пересчитать весь интерфейс. Это убьет процессор.
Решение: useRef создает хранилище, изменение которого НЕ вызывает перерисовку. Это позволяет нам менять свойства 3D-объектов напрямую, в обход React.
- Аналогия: Это как ссылка (Pointer) на реальный DOM-элемент или объект Three.js.
Пример (псевдо-3D логика):
import { useRef } from 'react';
const Box = () => {
// 1. Создаем реф. Изначально он пустой (null).
const myBoxRef = useRef(null);
const rotateBox = () => {
// 2. Обращаемся к .current.
// В React Three Fiber там будет лежать настоящий 3D-объект (Mesh).
if (myBoxRef.current) {
// Мы меняем свойство НАПРЯМУЮ. React даже не знает об этом.
// Интерфейс не перерисовывается, но куб на экране крутится!
myBoxRef.current.rotation += 0.1;
console.log("Повернули!");
}
};
return (
// 3. Привязываем реф к элементу через атрибут ref={}
<div ref={myBoxRef} onClick={rotateBox}>
Я коробка (представьте)
</div>
);
};
Это задание объединяет все три хука. Попросите ученика написать компонент Timer.
ТЗ:
- При появлении на экране (
useEffect) он запускает интервал (таймер), который тикает каждую секунду. - В
useStateхранится количество секунд. - Когда компонент удаляется, таймер должен остановиться (очистка в
useEffect), чтобы не было ошибок.
Пример ожидаемого решения:
import { useState, useEffect } from 'react';
const Timer = () => {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
// Запускаем интервал
const intervalId = setInterval(() => {
// Используем callback-версию сеттера, чтобы всегда иметь актуальное значение
setSeconds((prev) => prev + 1);
console.log("Тик...");
}, 1000);
// CLEANUP FUNCTION (Уборка)
// Очень важно! Если не очистить интервал, он продолжит тикать
// даже после удаления компонента и будет вызывать ошибки.
return () => {
clearInterval(intervalId);
console.log("Таймер остановлен");
};
}, []); // [] -> Запустить 1 раз при старте
return <h1>Прошло времени: {seconds} сек.</h1>;
};
export default Timer;
Мы закончили с основами Web и React. Ученик теперь знает:
- Как создать проект.
- Как писать на JS (Objects, Arrays, Arrow functions).
- Как создавать Компоненты.
- Как управлять ими через Хуки.
Мы закончили с "плоским" миром веба и переходим в объем. Прежде чем написать <Canvas>, ученик должен визуализировать в голове, как устроен этот мир. Здесь мы почти не пишем код, мы строим ментальную модель.
Three.js (и любой 3D-движок) работает как реальная киностудия. Чтобы получить картинку на экране, нам нужны три обязательных компонента. Если нет хотя бы одного — экран будет черным.
- Сцена (Scene): Это мир. Пустое пространство, куда мы ставим актеров (модели), декорации и свет. В React Three Fiber (R3F) это создается автоматически внутри компонента
<Canvas>. - Камера (Camera): Это глаз режиссера. Сцена может быть огромной, но мы видим только то, что попадает в объектив камеры.
- Рендерер (Renderer): Это художник, который берет данные Сцены и Камеры и рисует итоговую плоскую картинку (кадр) на экране монитора 60 раз в секунду. В R3F рендерер тоже спрятан внутри
<Canvas>.
В школе на геометрии была ось X и Y. В 3D добавляется глубина — ось Z. Three.js использует правостороннюю систему координат (Right-handed system).
- X (Красная): Вправо (+) / Влево (-).
- Y (Зеленая): Вверх (+) / Вниз (-).
- Z (Синяя): На нас (+) / От нас вглубь экрана (-).
Мнемоника для ученика: Пусть поднимет правую руку.
- Большой палец вправо — это X.
- Указательный вверх — это Y.
- Средний палец на себя — это Z.
По умолчанию камера смотрит в точку (0, 0, 0) с позиции z = 5 (чуть отодвинута назад). Если поставить объект в (0,0,0), а камеру тоже в (0,0,0), мы ничего не увидим (мы будем внутри объекта).
В 3D-графике видимый объект называется Mesh (Сетка). Mesh состоит из двух независимых частей. Это как тело и одежда.
- Geometry (Геометрия): Форма объекта. Это просто набор точек (вершин/vertices) в пространстве, соединенных линиями.
- Примеры:
BoxGeometry(куб),SphereGeometry(шар),PlaneGeometry(плоскость). - Важно: Геометрия невидима. Это просто математический каркас.
- Примеры:
- Material (Материал): Внешний вид. Как поверхность реагирует на свет. Цвета, блеск, текстуры.
- Примеры: "Красный пластик", "Полированный металл", "Светящаяся лампа".
Формула:
Если у объекта есть Геометрия, но нет Материала — он часто розовый (цвет ошибки) или невидимый. Если есть Материал, но нет Геометрии — рисовать нечего.
Это иерархическая структура (дерево), в которой живут объекты. В Java это можно сравнить с деревом DOM или файловой системой.
- Суть: Объекты могут быть вложены друг в друга (Parent -> Child).
- Правило наследования: Если мы двигаем, вращаем или увеличиваем Родителя, все его Дети повторяют это действие автоматически.
Пример из жизни: Представьте машину (Car). У неё есть 4 колеса (Wheels).
Car— родитель.Wheels— дети.
Если мы двигаем машину (Car.position.x += 10), нам не нужно двигать колеса вручную. Они поедут вместе с машиной, потому что они прикреплены к ней в графе сцены.
Но если мы покрутим колесо (Wheel.rotation.x += 1), машина останется на месте.
В коде (R3F) это выглядит как вложенность тегов:
<mesh position={[10, 0, 0]}> {/* РОДИТЕЛЬ (Машина) */}
<boxGeometry />
<meshStandardMaterial color="blue" />
{/* РЕБЕНОК (Колесо) */}
{/* Его позиция [2, 0, 0] относительна РОДИТЕЛЯ, а не центра мира! */}
<mesh position={[2, 0, 0]}>
<sphereGeometry />
</mesh>
</mesh>
Попросите ученика нарисовать дерево (Граф сцены) для Солнечной системы:
- Есть центр — Солнце.
- Вокруг Солнца вращается пустой контейнер (Группа) — Орбита Земли.
- Внутри Орбиты сидит Земля.
- Внутри Земли сидит Луна.
Вопрос на проверку: Если мы начнем вращать "Орбиту Земли" вокруг центра, что произойдет с Землей и Луной? (Ответ: Они начнут летать вокруг Солнца. Это самый простой способ сделать анимацию орбиты без сложной тригонометрии).
Ученик узнал словарь 3D-разработчика:
- XYZ (правосторонняя система).
- Mesh (Геометрия + Материал).
- Parent/Child (если папа идет в магазин, ребенок на руках идет с ним).
В прошлом уроке мы создали форму (Geometry). Но без материала и света эта форма невидима или выглядит как черное пятно.
В этом модуле мы разберем, как раскрашивать объекты. Это самая творческая часть 3D. Для новичка здесь важно усвоить главное правило: Выбор материала диктует требования к свету.
В Three.js десятки материалов, но в 90% случаев мы используем только два. Разница между ними колоссальная.
Это самый простой материал. Он не реагирует на свет.
- Если покрасить куб в красный цвет, он будет ярко-красным со всех сторон. Вы не увидите граней, углов или теней. Он будет выглядеть как плоский 2D-квадрат.
- Зачем нужен: Для UI, для объектов, которые должны "светиться" сами по себе (как интерфейс в шлеме Железного Человека), или для отладки, когда свет еще не настроен.
- Цена: Самый дешевый для процессора.
Это PBR-материал (Physically Based Rendering). Это индустриальный стандарт.
- Он реагирует на свет. Если на него не светить — он будет черным.
- Если посветить сбоку — появится блик и тень.
- У него есть два главных параметра "физики":
- Roughness (Шероховатость):
0.0— зеркало,1.0— мел или резина (матовый). - Metalness (Металличность):
0.0— пластик/дерево,1.0— металл.
- Roughness (Шероховатость):
Совет: Пусть ученик всегда начинает с
MeshStandardMaterial. Это сразу дает "объемную" картинку, как только мы добавим свет.
Без света MeshStandardMaterial невидим. В R3F свет — это просто компоненты, которые мы добавляем внутрь <Canvas>.
Это свет, который есть везде. Он не имеет источника и направления. Он просто равномерно подсвечивает все объекты, чтобы тени не были абсолютно черными.
- Аналогия: Пасмурный день, свет идет отовсюду. Теней нет.
Лучи идут параллельно. Положение источника не важно (он бесконечно далеко), важно только направление.
- Аналогия: Солнце.
Свет исходит из одной точки во все стороны и затухает с расстоянием.
- Аналогия: Лампочка, свеча, фаербол.
Цвет (color) — это скучно. Чтобы сделать кирпичную стену, мы не моделируем каждый кирпич. Мы берем картинку (JPG/PNG) кирпичей и "натягиваем" её на плоскость.
Это называется Texture Mapping. Чтобы картинка легла правильно, у каждой 3D-модели есть невидимая разметка — UV-координаты. Это инструкция: "Какой пиксель картинки соответствует какой точке на 3D-модели".
Теперь покажем, как эти теоретические концепции выглядят в React-компонентах. Обратите внимание, как мы вкладываем <material> внутрь <mesh>.
const Scene = () => {
return (
<>
{/* 1. Свет */}
{/* Базовая подсветка, интенсивность 0.5 (слабая) */}
<ambientLight intensity={0.5} />
{/* Солнце, светит сверху (y=10) и сбоку (x=10) */}
<directionalLight position={[10, 10, 5]} intensity={1} />
{/* 2. Объект 1: Плоский (Basic) */}
<mesh position={[-2, 0, 0]}>
<boxGeometry />
{/* Выглядит плоским, свет на него не влияет */}
<meshBasicMaterial color="orange" />
</mesh>
{/* 3. Объект 2: Объемный (Standard) */}
<mesh position={[2, 0, 0]}>
<sphereGeometry />
{/* Реагирует на свет! Блестящий пластик */}
<meshStandardMaterial color="hotpink" roughness={0.1} metalness={0.5} />
</mesh>
</>
);
};
Попросите ученика сделать "Мастерскую материалов":
- Создать сцену с 3 сферами в ряд.
- Добавить свет (
ambient+directional). - Настроить материалы сфер так, чтобы показать разные физические свойства:
- Сфера 1: Матовый шар для боулинга (черный,
roughness: 0,metalness: 0). - Сфера 2: Золотой шар (желтый,
roughness: 0.1,metalness: 1). - Сфера 3: Резиновый мяч (синий,
roughness: 1,metalness: 0).
- Сфера 1: Матовый шар для боулинга (черный,
Это научит его "чувствовать" параметры PBR.
Мы прошли теорию "3D-грамотности". Ученик понимает, что такое Сцена, Меши и Материалы. Теперь у нас есть база, чтобы написать первое настоящее приложение.
Мы прошли теорию, настроили окружение и выучили основы React. Настало время Большого Взрыва. В этом модуле мы создадим нашу первую вселенную.
Здесь мы соединим знания:
- React: Компоненты и пропсы.
- Three.js: Меши и материалы.
- R3F: "Клей", который соединяет их вместе.
В React Three Fiber (R3F) всё начинается с компонента <Canvas>. Это не просто HTML-тег. Это "портал".
- Снаружи Canvas: Обычный HTML/DOM (заголовки, кнопки, скролл).
- Внутри Canvas: Мир Three.js. Здесь не работают HTML-теги типа
<div>или<span>. Здесь живут только 3D-объекты.
Canvas автоматически делает за нас огромную "грязную" работу:
- Создает Сцену и Камеру.
- Запускает Рендерер (WebGL).
- Запускает цикл анимации (Game Loop) на 60 FPS.
- Следит за изменением размера окна.
import { Canvas } from '@react-three/fiber';
function App() {
return (
// Этот div растянут на весь экран (благодаря CSS из Модуля 1)
<div id="canvas-container">
<Canvas>
{/* ВНУТРИ - ТОЛЬКО 3D */}
<mesh>
<boxGeometry />
<meshStandardMaterial color="hotpink" />
</mesh>
</Canvas>
</div>
);
}
Для Java-разработчика это будет знакомо как разница между "ручным созданием бинов" и "Spring Context".
В "чистом" JS (Vanilla Three.js) мы пишем Императивно (Приказываем: создай, настрой, добавь):
// Vanilla JS (Старый способ)
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 'red' });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh); // Руками добавляем в сцену
В R3F мы пишем Декларативно (Описываем: я хочу, чтобы здесь был куб):
// R3F (React способ)
<mesh>
<boxGeometry args={[1, 1, 1]} />
<meshBasicMaterial color="red" />
</mesh>
Правило перевода:
- Классы Three.js превращаются в теги с маленькой буквы (
new THREE.Mesh()-><mesh />). - Аргументы конструктора передаются в массив
args(new BoxGeometry(1, 2, 3)-><boxGeometry args={[1, 2, 3]} />). - Свойства (
mesh.position.x = 5) становятся пропсами (<mesh position-x={5} />).
В 3D картинка должна двигаться. Статичная сцена скучна. React обновляет экран только когда меняется стейт (useState). Но для 3D это слишком медленно (нам нужно 60 кадров в секунду).
Для этого в R3F есть специальный хук: useFrame. Код внутри него выполняется каждый кадр (60 раз в секунду).
Важно: Чтобы использовать useFrame, компонент должен находиться ВНУТРИ <Canvas>. Мы не можем использовать его в главном App, только в дочерних компонентах.
Пример Вращающегося Куба (Полный код файла App.jsx):
import { useRef } from 'react';
import { Canvas, useFrame } from '@react-three/fiber';
// 1. Создаем отдельный компонент для Куба
// Мы вынесли его, чтобы использовать внутри хук useFrame
const RotatingCube = () => {
// Ссылка на реальный 3D-объект (Mesh).
// Изначально null, но React привяжет его к тегу <mesh> ниже.
const myMesh = useRef();
// Этот код запускается 60 раз в секунду!
// state - состояние сцены, delta - время между кадрами (чтобы анимация была плавной)
useFrame((state, delta) => {
// Проверка на всякий случай (Java-привычка: null check)
if (myMesh.current) {
// Прямое изменение свойств (Мутация).
// Мы НЕ используем useState, потому что это убило бы производительность.
myMesh.current.rotation.x += delta; // Крутим по X
myMesh.current.rotation.y += delta * 0.5; // И немного по Y
}
});
return (
<mesh ref={myMesh} position={[0, 0, 0]}>
{/* Куб размером 2x2x2 */}
<boxGeometry args={[2, 2, 2]} />
{/* Стандартный материал цвета лайма */}
<meshStandardMaterial color="lime" />
</mesh>
);
};
// 2. Главный компонент приложения
const App = () => {
return (
<Canvas>
{/* Свет нужен, чтобы meshStandardMaterial было видно */}
<ambientLight intensity={0.5} />
<directionalLight position={[10, 10, 5]} />
{/* Наш куб */}
<RotatingCube />
</Canvas>
);
};
export default App;
Пусть ученик модифицирует код выше, чтобы создать "Танец фигур":
- Создать два компонента:
CubeиSphere(используя<sphereGeometry />). - Разместить их в сцене слева и справа (
position={[-2, 0, 0]}иposition={[2, 0, 0]}). - Заставить их вращаться в разные стороны внутри
useFrame. - Бонус: Попробовать сделать так, чтобы сфера вращалась быстрее куба.
Чему он научится:
- Создавать и переиспользовать компоненты в 3D.
- Понимать координатную сетку (минус влево, плюс вправо).
- Управлять скоростью анимации через математику (
+= delta * speed).
Ученик написал свой первый 3D-код.
<Canvas>— это мир.useFrame— это время (пульс мира).useRef— это доступ к объектам для управления ими.
Мы научились создавать куб и вращать его. Но современный 3D-мир — это не просто геометрические примитивы. Нам нужны красивые модели машин или персонажей, реалистичный свет и возможность вращать камерой.
Писать всё это с нуля на чистом Three.js — это сотни строк математики. В мире React Three Fiber есть библиотека-спаситель: @react-three/drei.
Для Java-разработчика: Если R3F — это Spring Core (база), то Drei — это Spring Boot Starter + Lombok. Это набор готовых, преднастроенных компонентов, которые решают 90% рутинных задач одной строкой кода. Название "Drei" — это немецкая цифра 3 (игра слов с Three.js).
Чтобы магия заработала, нужно добавить библиотеку в проект:
npm install @react-three/drei
Сейчас наша камера "прибита гвоздями". Чтобы дать пользователю возможность вращать сцену мышкой, зумить колесиком и двигаться, нужен контроллер.
В Drei это делается одним тегом:
import { OrbitControls } from '@react-three/drei';
<Canvas>
<mesh>...</mesh>
{/* Всё! Камера теперь управляется мышкой. */}
{/* enableZoom={false} чтобы отключить зум, autoRotate чтобы вращалась сама */}
<OrbitControls />
</Canvas>
Настройка света вручную (ambientLight, pointLight) — это сложно. Легко получить плоскую или пересвеченную картинку. Профессионалы используют HDRI-карты. Это сферическая фотография реального мира (панорама 360°), которая "светит" на ваши объекты.
- Если на карте закат — объект будет оранжевым.
- Если лес — зеленым.
Drei делает это элементарно:
import { Environment } from '@react-three/drei';
<Canvas>
<mesh>
<sphereGeometry />
{/* Чтобы HDRI работала, материал должен быть "стандартным" (PBR) */}
{/* metalness={1} roughness={0} сделает идеальное зеркало, отражающее мир */}
<meshStandardMaterial metalness={1} roughness={0} />
</mesh>
{/* preset="sunset" | "city" | "park" | "studio" */}
{/* background={true} показывает саму картинку на фоне */}
<Environment preset="sunset" background />
</Canvas>
Мы не лепим персонажей кодом из кубиков. Мы делаем их в Blender и экспортируем. Стандарт индустрии — формат .gltf или .glb. Это "JPEG для 3D".
Чтобы загрузить модель, используется хук useGLTF.
Простой способ (Черный ящик):
import { useGLTF } from '@react-three/drei';
const Model = () => {
// Загружаем файл (может лежать в папке public или по ссылке)
const gltf = useGLTF('/car.glb');
// primitive — это специальный компонент R3F,
// который берет готовый объект Three.js и вставляет его в сцену.
return <primitive object={gltf.scene} />;
}
Простой способ плох тем, что мы не видим структуру модели. Мы не можем поменять цвет двери машины или скрыть колесо, потому что модель — это единый "черный ящик".
Инструмент gltfjsx превращает файл .glb в React-компонент. Он "взрывает" модель на части кода.
Как использовать (в терминале): Не нужно ничего устанавливать, запускаем через npx:
# Перейдите в папку, где лежит модель (например, public)
# npx gltfjsx <имя-файла>.glb
npx gltfjsx car.glb
Он создаст файл Car.jsx. Внутри вы увидите что-то такое:
// Автогенерированный код
export function Model(props) {
const { nodes, materials } = useGLTF('/car.glb')
return (
<group {...props} dispose={null}>
{/* Теперь мы видим каждую деталь! */}
<mesh geometry={nodes.Wheel.geometry} material={materials.Rubber} />
<mesh geometry={nodes.Body.geometry}>
{/* Мы можем переопределить материал прямо здесь! */}
<meshStandardMaterial color="red" />
</mesh>
</group>
)
}
Давайте создадим сцену с красивым шаром (имитация модели), управлением и светом.
Файл: App.jsx
import { Canvas } from '@react-three/fiber';
// Импортируем полезности из Drei
import { OrbitControls, Environment, ContactShadows } from '@react-three/drei';
function App() {
return (
<Canvas camera={{ position: [0, 0, 5], fov: 45 }}>
{/* 1. Управление камерой.
makeDefault отключает другие обработчики событий, если они есть.
*/}
<OrbitControls makeDefault />
{/* 2. Освещение (HDRI).
preset="city" дает нейтральный городской свет (бетон, небо).
background={false} - свет есть, но картинки на фоне нет (просто цвет).
*/}
<Environment preset="city" />
{/* 3. Наш объект (вместо модели).
Сделаем его зеркальным, чтобы видеть отражение города.
*/}
<mesh position={[0, 0, 0]}>
<sphereGeometry args={[1, 32, 32]} />
<meshStandardMaterial
color="white"
metalness={1} // 100% металл
roughness={0.1} // Почти зеркало
/>
</mesh>
{/* 4. Красивая тень под объектом.
ContactShadows — это фейковая тень, которая выглядит очень реалистично,
будто объект стоит на столе. Гораздо дешевле настоящих теней.
*/}
<ContactShadows
position={[0, -1, 0]} // Чуть ниже шара
opacity={0.5}
blur={2}
color="black"
/>
</Canvas>
);
}
export default App;
- Установите
drei:npm install @react-three/drei. - Запустите код из примера выше.
- Поиграйтесь с
presetв Environment: попробуйте"sunset","forest","night". Посмотрите, как меняется шар. - Челлендж: Найдите в интернете любую
.glbмодель (например, на Poly Pizza ), скачайте в папкуpublic, загрузите её через<primitive object={...} />вместо шара.
С библиотекой Drei мы перешли от "низкоуровневого кода" к "сборке конструктора".
OrbitControlsдал свободу.Environmentдал реализм.useGLTFпозволил загружать контент.
Мы создали красивый мир, но он ведет себя как музейный экспонат — «смотреть можно, трогать нельзя». В этом модуле мы оживим сцену, научив объекты реагировать на клики и наведение мыши.
Для Java-разработчика это будет приятно: система событий в R3F практически идентична стандартному React DOM (onClick, onMouseEnter), но под капотом происходит сложная математика.
В обычном вебе, когда вы кликаете на кнопку, браузер точно знает координаты прямоугольника кнопки на экране. В 3D всё сложнее. Экран плоский (2D), а мир объемный (3D).
Raycasting (Пускание лучей) Чтобы понять, попали ли вы мышкой по кубу, движок делает следующее:
- Из точки клика на экране (X, Y) выпускается невидимый лазерный луч (Ray) вглубь сцены.
- Луч летит сквозь пространство.
- Движок проверяет: пересек ли этот луч какой-нибудь объект (Mesh)?
- Если да — событие срабатывает на ближайшем к камере объекте.
В ванильном Three.js это 10-20 строк кода настройки Raycaster. В R3F это работает автоматически.
В R3F мы используем универсальные события Pointer (они работают и для мыши, и для тач-экранов телефонов). Мы вешаем их прямо на <mesh>.
onClick— клик (нажал и отпустил).onPointerOver— курсор "зашел" на объект (аналог hover).onPointerOut— курсор "ушел" с объекта.onPointerDown/onPointerUp— нажатие и отпускание.
Синтаксис:
<mesh onClick={(event) => console.log('Бум!')} />
Чтобы объект изменился при клике, нам нужно сохранить его состояние. Здесь в игру вступает хук useState, который мы изучили в Модуле 5.
Сценарий:
- Куб оранжевый.
- При наведении курсора (
hover) он становится розовым. - При клике (
click) он увеличивается в размерах.
Код компонента:
import { useState } from 'react';
import { useCursor } from '@react-three/drei'; // Полезный бонус
const InteractiveBox = (props) => {
// 1. Состояние "Наведен ли курсор?"
const [hovered, setHover] = useState(false);
// 2. Состояние "Активен ли объект?" (кликнули или нет)
const [active, setActive] = useState(false);
// Бонус: меняем курсор на "руку" (pointer) при наведении.
// Это правило хорошего тона в UX.
useCursor(hovered);
return (
<mesh
{...props} // Передаем позицию сверху
// --- СОБЫТИЯ ---
// Клик: переключаем active (true <-> false)
onClick={(event) => setActive(!active)}
// Наведение: включаем hover
onPointerOver={(event) => setHover(true)}
// Уход курсора: выключаем hover
onPointerOut={(event) => setHover(false)}
// --- РЕАКЦИЯ НА СОСТОЯНИЕ ---
// Если active=true, масштаб 1.5, иначе 1.
scale={active ? 1.5 : 1}
>
<boxGeometry args={[1, 1, 1]} />
{/* Цвет тоже зависит от стейта */}
<meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
</mesh>
);
};
Иногда нам нужно знать больше, чем просто факт клика. В функцию обработчика приходит объект event, который содержит массу полезной информации из 3D-мира.
onClick={(event) => {
// Точка в 3D пространстве, куда попал луч [x, y, z]
console.log(event.point);
// Расстояние от камеры до точки касания
console.log(event.distance);
// Объект, по которому кликнули
console.log(event.object);
// ВАЖНО: stopPropagation (Остановка всплытия)
// Если за кубом стоит другой куб, луч может пробить их обоих.
// stopPropagation говорит: "Я поймал клик, дальше луч не пускай".
event.stopPropagation();
}}
Создайте "Интерактивную галерею":
- Сделайте сцену с 3 разными объектами (Куб, Сфера, Тетраэдр —
<tetrahedronGeometry />). - Напишите компонент, который принимает цвет и позицию.
- Логика: При клике на объект он должен начать вращаться (используйте
useStateдля флагаisSpinningи проверяйте этот флаг внутриuseFrame).- Подсказка: Вам понадобится
useRefдля вращения иuseStateдля разрешения вращения. useFrame(() => { if (isSpinning) ref.current.rotation.y += 0.05 })
- Подсказка: Вам понадобится
Поздравляю, мы закончили основной блок творчества! Ученик теперь может:
- Создать сцену.
- Наполнить её объектами и моделями.
- Сделать её красивой (свет, тени).
- Сделать её живой (анимация и взаимодействие).
До сих пор наши объекты левитировали в пустоте. Чтобы сделать игру или реалистичную симуляцию, нам нужна гравитация, столкновения и отскоки.
Писать математику столкновений вручную — это ад (нужно рассчитывать векторы отражения, искать точки пересечения граней). Вместо этого мы используем Физический Движок. В R3F стандартом сейчас является библиотека @react-three/rapier.
Почему Rapier? Он написан на Rust и скомпилирован в WebAssembly. Это значит, что он работает очень быстро, не нагружая основной JS-поток. Для React есть отличная обертка.
npm install @react-three/rapier
Чтобы внедрить физику, нам нужно обернуть наши 3D-объекты в специальные контейнеры.
<Physics>: Это "Бог" физического мира. Он создает гравитацию и рассчитывает симуляцию. Оборачивает всю сцену.RigidBody(Твердое тело): Это компонент, который говорит движку: "Этот объект имеет массу и участвует в физике".- У
RigidBodyесть типы (как в Unity/Unreal): - Dynamic (По умолчанию): Имеет массу, падает, отскакивает. (Пример: мяч, персонаж).
- Fixed: Недвижимый объект. Бесконечная масса. (Пример: пол, стены).
- Kinematic: Движется программно (анимацией), снося всё на пути. (Пример: движущаяся платформа, лифт).
- У
- Colliders (Коллайдеры): Это форма для расчетов столкновений. Обычно Rapier сам угадывает форму по геометрии (
Cuboid,Ball), но иногда их нужно настраивать вручную.
Давайте уроним куб на пол.
Обратите внимание: мы не пишем никакой анимации. Мы просто говорим "Это тело динамическое", и движок сам тянет его вниз.
Файл: App.jsx
import { Canvas } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei';
// Импортируем физику
import { Physics, RigidBody } from '@react-three/rapier';
const App = () => {
return (
<Canvas camera={{ position: [0, 5, 10], fov: 50 }}>
<ambientLight intensity={0.5} />
<directionalLight position={[10, 10, 10]} />
<OrbitControls />
{/* debug={true} — ВАЖНЕЙШИЙ флаг для новичка.
Он рисует цветные линии (wireframe) вокруг физических тел.
Так мы видим, совпадают ли невидимые коллайдеры с видимыми мешами.
*/}
<Physics debug>
{/* --- ПАДАЮЩИЙ КУБ --- */}
{/* position ставим RigidBody, а не мешу!
Потому что физика управляет положением, а меш просто следует за ней.
restitution = упругость (0 - кирпич, 1 - попрыгунчик)
*/}
<RigidBody position={[0, 5, 0]} restitution={1.2}>
<mesh>
<boxGeometry />
<meshStandardMaterial color="hotpink" />
</mesh>
</RigidBody>
{/* --- ПОЛ --- */}
{/* type="fixed" — значит он не упадет в бездну, а останется на месте */}
<RigidBody type="fixed" position={[0, -1, 0]} rotation={[-Math.PI / 2, 0, 0]}>
{/* Огромная плоскость */}
<mesh>
<planeGeometry args={[20, 20]} />
<meshStandardMaterial color="gray" />
</mesh>
</RigidBody>
</Physics>
</Canvas>
);
};
export default App;
Физика становится интересной, когда мы начинаем вмешиваться. Чтобы толкнуть объект, мы не меняем его position (это телепортация, физика сломается). Мы применяем Силы (Forces) или Импульсы (Impulses).
- Impulse: Мгновенный удар (пинок мяча, выстрел).
- Force: Постоянное воздействие (ветер, двигатель ракеты).
Для этого нам снова нужен useRef, но теперь он ссылается на API физического тела.
import { useRef } from 'react';
import { RigidBody } from '@react-three/rapier';
const Kicker = () => {
const rigidBodyRef = useRef();
const jump = () => {
// Проверяем, загрузилось ли тело
if (rigidBodyRef.current) {
// applyImpulse({ x, y, z }, true - просыпаться ли телу)
// Толкаем вверх (y: 5) и немного вбок (x: 1)
// Аналог в Java: physicsBody.applyImpulse(new Vector3(1, 5, 0));
rigidBodyRef.current.applyImpulse({ x: 1, y: 5, z: 0 }, true);
// А еще можно крутануть (Torque)
rigidBodyRef.current.applyTorqueImpulse({ x: 0, y: 1, z: 0 }, true);
}
};
return (
<RigidBody
ref={rigidBodyRef}
position={[0, 2, 0]}
colliders="cuboid" // Явно указываем форму коллайдера (куб)
>
{/* Клик по мешу вызывает функцию прыжка */}
<mesh onClick={jump}>
<boxGeometry />
<meshStandardMaterial color="orange" />
</mesh>
</RigidBody>
);
};
По умолчанию RigidBody пытается угадать форму (Cuboid для куба, Ball для сферы). Но что если у нас модель "Бублик" (Torus) или сложный стул?
Есть три основных стратегии для свойства colliders:
colliders="hull"(Convex Hull): Оборачивает объект в "подарочную упаковку". Все дырки закрываются. Если бублик упадет, он будет вести себя как шайба (дырки нет). Быстро.colliders="trimesh": Точная копия сетки объекта. Дырка в бублике будет физической. Медленно и глючно для динамических объектов (может провалиться сквозь пол). Используйте только для статичных стен/ландшафта.- Составные примитивы: Лучше всего собрать сложный объект из простых невидимых кубов и шаров.
Проект "Дженга" (Башня):
- Создайте пол (
type="fixed"). - В цикле (используя
.mapиз JS) создайте башню из 10-15 кубиков, стоящих друг на друге. - Создайте отдельный "Шар-разрушитель".
- Добавьте обработчик клика на шар: при клике примените к нему сильный импульс в сторону башни.
- Результат: Шар врезается в башню, кубики реалистично разлетаются.
До этого момента мы двигали объекты так: x = x + 1. Это работает для движения по прямой. Но что, если нужно двигаться "вперед, куда смотрит персонаж" или "плавно повернуться к врагу"? Тут простая арифметика ломается. Добро пожаловать в Линейную Алгебру.
Для Senior Java Dev: В Three.js математика инкапсулирована в классы
Vector3,Matrix4,Quaternion. Это аналоги классов изjavax.vecmathилиlibgdx. Главное отличие от Java-бэкенда: здесь мы избегаем создания новых объектов (new Vector3()) в цикле рендера (useFrame), чтобы не триггерить Garbage Collector 60 раз в секунду.
Вектор — это просто стрелка в пространстве. В Three.js это класс THREE.Vector3(x, y, z).
У вектора есть два смысла (контекст определяет смысл):
- Точка (Position): "Где я нахожусь". Координаты в мире.
- Направление (Direction): "Куда я смотрю" или "С какой скоростью лечу".
- Сложение (
add):- Смысл: Движение.
Текущая Позиция + Вектор Скорости = Новая Позиция.
- Вычитание (
sub):- Смысл: Вектор от А к Б.
Цель - Я = Вектор направления к цели.
- Нормализация (
normalize):- Смысл: Нам часто важно только направление, а не длина. Нормализация делает длину стрелки равной 1, сохраняя направление.
- Если не нормализовать вектор скорости, то персонаж будет бежать быстрее по диагонали (классический баг новичка).
- Длина (
length/distanceTo):- Смысл: Дистанция. "Далеко ли до врага?"
Практика в R3F (Движение к цели):
useFrame((state, delta) => {
const player = playerRef.current;
const target = targetRef.current;
// 1. Создаем временный вектор (ВНИМАНИЕ: лучше вынести из useFrame в константу, чтобы не мусорить память)
const direction = new THREE.Vector3();
// 2. Получаем вектор "ОТ игрока К цели" (Target - Player)
direction.subVectors(target.position, player.position);
// 3. Если мы далеко (> 1 метра)
if (direction.length() > 1) {
// 4. Делаем длину вектора равной 1 (только направление)
direction.normalize();
// 5. Двигаем: Позиция += Направление * Скорость * ВремяКадра
direction.multiplyScalar(5 * delta); // Скорость 5 м/с
player.position.add(direction);
// 6. Поворачиваем лицо к цели (чит-код Three.js)
player.lookAt(target.position);
}
});
Это самая больная тема.
Это то, к чему мы привыкли: rotation.x, rotation.y, rotation.z.
- Плюсы: Понятно человеку ("Поверни на 90 градусов по Y").
- Минусы: Gimbal Lock (Шарнирный замок).
Проблема Gimbal Lock: Если повернуть объект на 90 градусов по оси X, то оси Y и Z "склеиваются". Вращение по Y начинает делать то же самое, что и вращение по Z. Вы теряете одну степень свободы. Самолет в симуляторе начинает вести себя неадекватно.
Это 4-мерные числа (x, y, z, w).
- Плюсы: Нет Gimbal Lock. Идеальная интерполяция (плавный поворот от угла А к углу Б по кратчайшему пути).
- Минусы: Человеку невозможно представить 4-мерное число. Мы никогда не задаем их вручную.
Как жить? В Three.js под капотом ВСЕГДА используются кватернионы. rotation.x — это просто удобная обертка.
- Для простых вещей (вращение монетки): Используйте
rotation(Euler). - Для сложных (камера, физика, полет самолета): Используйте методы, работающие с кватернионами (
lookAt,quaternion.slerp).
Пример плавного поворота (Slerp):
// Вместо резкого lookAt, делаем плавный поворот
// Slerp = Spherical Linear Interpolation
useFrame((state, delta) => {
const targetRot = new THREE.Quaternion();
targetRot.setFromEuler(new THREE.Euler(0, targetAngle, 0)); // Куда хотим
// Плавно крутим текущий кватернион к целевому на 10% каждый кадр
myMesh.current.quaternion.slerp(targetRot, 0.1);
});
Матрица 4x4 (Matrix4) — это контейнер, который хранит ВСЁ о трансформации объекта сразу: позицию, вращение и масштаб.
Local Space vs World Space:
- Local: "Моя рука находится в координатах (2, 0, 0) относительно моего тела".
- World: "Где моя рука находится относительно центра вселенной (0,0,0)?"
Чтобы узнать мировые координаты руки, движок умножает: Matrix_Hand_World = Matrix_Body_World * Matrix_Hand_Local.
Совет: Вам редко придется трогать матрицы напрямую. Но важно знать: если вы прикрепили ребенка к родителю, а потом пытаетесь прочитать
child.position, вы получите локальные координаты. Чтобы получить реальную позицию в мире, нужно использоватьchild.getWorldPosition(targetVector).
"Следящий Глаз" (LookAt + Vectors):
- Создайте "Глаз" (Сферу с "зрачком" — еще одной маленькой черной сферой, прилепленной сбоку, чтобы было видно вращение).
- Добавьте на сцену другой объект ("Муха"), который летает по кругу (используйте
Math.sinиMath.cosс таймеромstate.clock.elapsedTime). - Задача: В
useFrameзаставьте Глаз постоянно смотреть на Муху, используя метод.lookAt(). - Усложнение (Math Challenge): Не используйте
.lookAt(). Попробуйте рассчитать вектор направления вручную и повернуть глаз, используя кватернионы (опционально для Junior, обязательно для Middle).
Математика в Three.js — это не про вычисление интегралов. Это про Векторы.
- Хочешь узнать расстояние?
vec.distanceTo(). - Хочешь двигаться?
pos.add(velocity). - Хочешь смотреть?
obj.lookAt().
Это "Финал" для большинства разработчиков. Если вы умеете писать шейдеры — вы волшебник. Вы больше не ограничены стандартными материалами Three.js. Вы можете создать воду, голограмму, огненный шар или искажение пространства.
Что такое Шейдер? Это небольшая программа, которая выполняется не на процессоре (CPU), а на видеокарте (GPU).
- CPU (Java/JS): Умный, но медленный. Может делать сложные вычисления последовательно.
- GPU (GLSL): "Глупый", но их тысячи. Видеокарта имеет тысячи микро-ядер, которые могут обработать каждый пиксель экрана одновременно.
Язык шейдеров называется GLSL (OpenGL Shading Language).
Для Java-разработчика: Синтаксис GLSL на 99% похож на C или Java.
- Строгая типизация (
float,int,vec3).- Обязательная точка с запятой
;.- Функция
void main()как точка входа.- Важно: Нет автокастинга.
1 + 1.5вызовет ошибку компиляции. Нужно писать1.0 + 1.5.
Чтобы нарисовать объект, GPU выполняет два шага (два шейдера):
- Задача: Взять точки 3D-модели (вершины) и перевести их в 2D-координаты экрана.
- Метафора: Скульптор. Он может раздуть модель, сплющить её или заставить извиваться (как флаг на ветру), меняя координаты вершин
position. - Выход: Переменная
gl_Position(координата на экране).
- Задача: Покрасить пиксели между вершинами. Выполняется миллионы раз (для каждого пикселя монитора).
- Метафора: Художник. Он решает, какого цвета будет эта конкретная точка: красная, с текстурой или прозрачная.
- Выход: Переменная
gl_FragColor(итоговый цветrgba).
Как передать данные из JS в C-код шейдера? Есть три типа переменных:
- Uniforms (Униформы): Глобальные переменные. Одинаковы для всех вершин и пикселей.
- Пример:
uTime(время),uColor(цвет),uMouse(мышь). Мы шлем их из React.
- Пример:
- Attributes (Атрибуты): Персональные данные вершины.
- Пример:
position(где точка),uv(координата текстуры),normal(куда смотрит грань). Three.js дает их нам автоматически.
- Пример:
- Varyings (Вариинги): Почтальон. Передает данные из Vertex Shader -> в Fragment Shader.
- Вершинный шейдер считает данные, кладет в
varying, и Фрагментный их получает интерполированными.
- Вершинный шейдер считает данные, кладет в
Давайте напишем свой материал. Мы сделаем сферу, которая:
- Меняет форму (волны) — Vertex Shader.
- Меняет цвет в зависимости от высоты волны — Fragment Shader.
В R3F для этого есть удобный хелпер shaderMaterial из библиотеки Drei.
Файл: Blob.jsx
import { useRef } from 'react';
import { Canvas, useFrame, extend } from '@react-three/fiber';
import { shaderMaterial, OrbitControls } from '@react-three/drei';
import * as THREE from 'three';
// --- 1. ПИШЕМ ШЕЙДЕРЫ (GLSL) ---
// Вершинный шейдер: Управляет формой
const vertexShader = `
// Глобальная переменная времени (приходит из JS)
uniform float uTime;
// "Почтальон": передадим UV-координаты во фрагментный шейдер
varying vec2 vUv;
// Передадим "высоту" искажения, чтобы раскрасить верхушки волн
varying float vElevation;
void main() {
vUv = uv; // uv - встроенный атрибут Three.js
// Копируем позицию, чтобы не ломать оригинал
vec3 pos = position;
// --- МАТЕМАТИКА ВОЛН ---
// Формула: pos.y += sin(x * частота + время) * амплитуда
// Мы искажаем позицию по нормали (наружу от центра сферы), а не просто вверх
float elevation = sin(pos.x * 3.0 + uTime) * sin(pos.y * 2.0 + uTime) * 0.2;
// Сохраняем высоту для покраски
vElevation = elevation;
// Применяем искажение: сдвигаем вершину вдоль её нормали
pos += normal * elevation;
// --- МАГИЯ МАТРИЦ ---
// Превращаем 3D-мир в 2D-экран.
// Это стандартная строка, которая должна быть почти в каждом Vertex Shader.
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`;
// Фрагментный шейдер: Управляет цветом
const fragmentShader = `
uniform vec3 uColor; // Базовый цвет
varying float vElevation; // Получаем высоту волны из vertex shader
void main() {
// Делаем цвет ярче на верхушках волн
// mix(цвет1, цвет2, процент) - смешивание
// vElevation у нас от -0.2 до +0.2. Приведем к 0.0 - 1.0 примерно.
float mixStrength = vElevation * 5.0 + 0.5;
vec3 finalColor = mix(uColor, vec3(1.0, 1.0, 1.0), mixStrength);
// gl_FragColor - это RGBA (vec4). A = 1.0 (непрозрачный)
gl_FragColor = vec4(finalColor, 1.0);
}
`;
// --- 2. СОЗДАЕМ МАТЕРИАЛ В REACT ---
// shaderMaterial создает класс материала Three.js автоматически
const ColorShiftMaterial = shaderMaterial(
// Объект с начальными Uniforms
{ uTime: 0, uColor: new THREE.Color(0.2, 0.0, 0.5) }, // Фиолетовый старт
// Код шейдеров
vertexShader,
fragmentShader
);
// extend делает этот класс доступным в JSX как <colorShiftMaterial />
// (по аналогии с тем, как THREE.Mesh становится <mesh>)
extend({ ColorShiftMaterial });
// --- 3. КОМПОНЕНТ СЦЕНЫ ---
const Scene = () => {
const materialRef = useRef();
useFrame((state, delta) => {
// Обновляем время каждый кадр, чтобы волны двигались
if (materialRef.current) {
materialRef.current.uTime += delta;
}
});
return (
<>
<OrbitControls />
<ambientLight />
<mesh>
{/* Сфера с большим количеством полигонов (128x128),
чтобы волны были плавными, а не угловатыми */}
<sphereGeometry args={[1, 128, 128]} />
{/* Наш кастомный материал */}
<colorShiftMaterial
ref={materialRef}
uColor="hotpink" // React сам конвертирует строку в THREE.Color!
/>
</mesh>
</>
);
};
// Boilerplate запуска
export default function App() {
return (
<Canvas>
<Scene />
</Canvas>
);
}
projectionMatrix * modelViewMatrix * vec4(...)— Что это за заклинание?- Это умножение матриц из Модуля 12.
vec4(pos, 1.0)— Превращаем точкуx,y,zв 4D-вектор (нужно для умножения).modelViewMatrix— Переносит точку из локальных координат (относительно центра сферы) в координаты камеры (относительно глаз игрока).projectionMatrix— Добавляет перспективу (далекое становится маленьким) и сплющивает всё в 2D-экран.
varying:- Заметьте, мы вычислили
vElevationдля 3-х вершин треугольника. - А пикселей внутри треугольника — сотни.
- GPU сам плавно интерполирует (смешивает) значение
vElevationдля каждого пикселя. Если в одном углу 0, а в другом 1, посередине само появится 0.5.
- Заметьте, мы вычислили
"Cyberpunk Grid" (Фрагментный шейдер):
- Возьмите код выше, но уберите искажение вершин (верните обычный
gl_Position). - В
fragmentShaderиспользуйтеvUv(текстурные координаты, от 0 до 1). - Напишите формулу, которая рисует решетку.
- Подсказка:
step(0.9, sin(vUv.x * 20.0))даст полоски. Объедините полоски по X и Y.
- Подсказка:
- Сделайте так, чтобы решетка "текла" вверх (
vUv.y + uTime).
Шейдеры — это мощь.
- Vertex Shader = Форма.
- Fragment Shader = Цвет.
- Uniforms = Параметры из React.
Мы научились делать красивые объекты и писать шейдеры. Но иногда нам нужно добавить эффект, который затрагивает всю картинку сразу, а не отдельный объект. Например:
- Сделать яркие объекты светящимися (Bloom).
- Размыть задний план, как в фотоаппарате (Depth of Field).
- Добавить шум зернистой пленки или эффект глюка (Glitch).
Это называется Пост-процессинг. Метафора: Сначала мы рендерим 3D-сцену, получаем плоскую картинку (как фото), а потом накладываем на неё Instagram-фильтры.
В R3F для этого есть шикарная библиотека: @react-three/postprocessing.
npm install @react-three/postprocessing
Все эффекты должны жить внутри компонента <EffectComposer>. Он берет на себя сложную работу:
- Рендерит сцену не на экран, а во внутреннюю память (буфер/текстуру).
- Прогоняет эту текстуру через цепочку шейдеров-эффектов.
- Выводит результат на экран.
Базовый пример (Неоновое свечение):
import { Canvas } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei';
// Импортируем контейнер и сами эффекты
import { EffectComposer, Bloom, Vignette } from '@react-three/postprocessing';
const Scene = () => {
return (
<>
<ambientLight intensity={0.5} />
{/* СВЕТЯЩИЙСЯ ОБЪЕКТ
Чтобы Bloom сработал, цвет должен быть ЯРЧЕ единицы.
В RGB цвет [10, 2, 5] — это супер-яркий цвет, который выбивает монитор в белый,
но Bloom понимает, что это "свет".
toneMapped={false} — Важно! Отключаем тональную компрессию,
чтобы цвет не обрезался до [1, 1, 1] перед попаданием в Bloom.
*/}
<mesh position={[0, 0, 0]}>
<boxGeometry />
<meshStandardMaterial
color={[1.5, 10, 1.5]} // RGB: Супер-яркий зеленый
emissive={[0, 2, 0]} // Собственное свечение
emissiveIntensity={2}
toneMapped={false}
/>
</mesh>
{/* КОНВЕЙЕР ЭФФЕКТОВ */}
<EffectComposer disableNormalPass>
{/* Bloom (Свечение)
luminanceThreshold: всё, что ярче этого значения (1.0), начнет светиться.
intensity: сила ореола.
*/}
<Bloom luminanceThreshold={1} mipmapBlur intensity={1.5} />
{/* Vignette (Виньетка) - затемнение по краям
eskil: алгоритм затемнения (более мягкий)
*/}
<Vignette eskil={false} offset={0.1} darkness={1.1} />
</EffectComposer>
<OrbitControls />
</>
);
};
export default function App() {
return (
<Canvas>
<Scene />
</Canvas>
);
}
Вот "джентльменский набор", который делает картинку кинематографичной:
<Bloom>: Свечение. Делает магию, неон, лазеры.- Совет: Используйте свойство
mipmapBlur, это более производительный и красивый алгоритм размытия.
- Совет: Используйте свойство
<DepthOfField>(DOF): Глубина резкости.- Нужно указать
focusDistance(на каком расстоянии фокус) иfocalLength(длина объектива). - Внимание: Очень тяжелый эффект для GPU.
- Нужно указать
<Noise>: Шум (зерно).- Скрывает "лесенки" и делает картинку менее стерильной/компьютерной.
<Glitch>: Глюки.- Полезно для Cyberpunk стилистики или эффекта поломки интерфейса.
Пост-процессинг — это дорого. Каждый эффект — это Full Screen Pass. Это значит, что для каждого кадра шейдер должен пробежаться по каждому из 2 миллионов пикселей (на экране 1080p). Если у вас 5 эффектов, видеокарта делает 10 миллионов операций на пикселях за один кадр.
Правила оптимизации:
- Не жадничайте. Используйте только то, что действительно нужно.
disableNormalPass: В<EffectComposer>часто можно добавить этот проп, если вы не используете эффекты, зависящие от геометрии (например, SSAO). Это экономит один рендер сцены.- Разрешение: Можно рендерить эффекты в меньшем разрешении (половина экрана), но это продвинутая техника.
Проект "Сломанный Терминал":
- Создайте сцену с черным фоном (
<color attach="background" args={['black']} />). - Добавьте зеленый текст (можно использовать
<Text>из@react-three/drei), который пишет "SYSTEM FAILURE". - Настройте материал текста так, чтобы он был очень ярким (
color={[0, 5, 0]}). - Добавьте
<EffectComposer>:- Bloom: Чтобы текст светился как на старых ЭЛТ-мониторах.
- Scanline (из @react-three/postprocessing): Полоски сканирования (опционально, если найдете, или замените на Noise).
- Glitch: Добавьте эффект глюка, который срабатывает иногда.
Пост-процессинг превращает "3D-модельку" в "Кадр из фильма". Но помните: лучший пост-процессинг — тот, который незаметен. Он должен дополнять атмосферу, а не перекрывать её.
Мы подошли к теме, которая отличает демо-проект от продакшен-решения. Представьте: вы создали красивый лес. В нем 5000 деревьев. Вы запускаете проект... и видите 3 кадра в секунду (FPS). Процессор кипит, кулер воет.
Почему? Проблема не в количестве полигонов (GPU может нарисовать миллионы треугольников). Проблема в общении между CPU (JS) и GPU.
Для Java-разработчика: Это классическая проблема N+1 запросов к базе данных, только здесь вместо БД — видеокарта. Каждый
<mesh>вызывает команду отрисовки (Draw Call). 5000 мешей = 5000 обращений к драйверу видеокарты за один кадр (16 мс). Это "смерть через тысячу порезов".
Решение: Instancing (Инстансинг). Мы отправляем геометрию и материал один раз, а вместе с ними — массив координат для 5000 копий. GPU рисует их всех за 1 вызов (1 Draw Call).
Вместо <mesh> мы используем специальный объект <instancedMesh>.
Главная сложность: У обычного меша мы пишем position={[x,y,z]}. У инстансед-меша такой роскоши нет. Все позиции хранятся в одной гигантской матрице. Нам нужно напрямую работать с математикой матриц (на низком уровне), чтобы расставить объекты.
Для упрощения работы с матрицами мы используем "пустышку" — временный объект THREE.Object3D.
Код (Поле из 1000 кубов):
import { useRef, useLayoutEffect } from 'react';
import { Canvas } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei';
import * as THREE from 'three';
const COUNT = 1000;
const InstancedCubes = () => {
const meshRef = useRef();
// useLayoutEffect срабатывает синхронно ДО отрисовки кадра.
// Идеально для первоначальной расстановки.
useLayoutEffect(() => {
if (!meshRef.current) return;
// 1. Создаем "пустышку-помощника".
// Мы будем двигать этот невидимый объект, брать его матрицу
// и записывать в InstancedMesh.
const tempObject = new THREE.Object3D();
for (let i = 0; i < COUNT; i++) {
// Генерируем случайную позицию
const x = (Math.random() - 0.5) * 20; // от -10 до 10
const y = (Math.random() - 0.5) * 20;
const z = (Math.random() - 0.5) * 20;
// 2. Настраиваем помощника
tempObject.position.set(x, y, z);
tempObject.rotation.y = Math.random() * Math.PI;
tempObject.updateMatrix(); // Важно! Пересчитываем матрицу помощника
// 3. Копируем матрицу помощника в i-тую ячейку нашего InstancedMesh
// .setMatrixAt(index, matrix)
meshRef.current.setMatrixAt(i, tempObject.matrix);
}
// 4. ОЧЕНЬ ВАЖНО: Сообщаем Three.js, что матрицы обновились.
// Без этого флага ничего не отрисуется или отрисуется в (0,0,0).
meshRef.current.instanceMatrix.needsUpdate = true;
}, []);
return (
// args: [geometry, material, count] - но в R3F геометрию и материал передаем детьми
// args={[null, null, COUNT]} - говорим, что будет COUNT копий
<instancedMesh ref={meshRef} args={[null, null, COUNT]}>
<boxGeometry />
<meshStandardMaterial color="orange" />
</instancedMesh>
);
};
export default function App() {
return (
<Canvas camera={{ position: [0, 0, 15] }}>
<ambientLight />
<directionalLight position={[10, 10, 5]} />
<InstancedCubes />
<OrbitControls />
</Canvas>
);
}
В Java есть Garbage Collector (GC), который чистит память. В JS он тоже есть. НО: WebGL хранит текстуры и геометрию не в оперативной памяти (RAM), а в видеопамяти (VRAM). JS GC не имеет доступа к видеокарте.
Проблема: Если вы удалите компонент <mesh> из React-дерева, JS-объект удалится. Но текстура на 50 Мб останется висеть в видеопамяти. Если пользователь будет переходить между страницами, VRAM переполнится и вкладка упадет (Crash).
Решение в R3F: Хорошая новость: React Three Fiber автоматически вызывает метод .dispose() для всех геометрий и материалов, когда компонент удаляется из дерева. Вам нужно беспокоиться об этом только если вы:
- Создаете ресурсы вручную (
new THREE.TextureLoader().load(...)) вне хуков. - Храните глобальные ссылки на текстуры.
В Java вы используете JProfiler или VisualVM. В R3F есть r3f-perf. Это панель мониторинга, которая показывает правду.
Установка:
npm install r3f-perf
Использование:
import { Perf } from 'r3f-perf';
<Canvas>
<Perf position="top-left" />
{/* Ваша сцена */}
</Canvas>
На что смотреть:
- FPS: Должно быть 60.
- Draw Calls: Если их > 100-200, пора объединять меши или использовать Instancing.
- Textures: Сколько памяти занимают картинки.
- Geometries: Количество треугольников.
"Астероидный пояс" (Оптимизация):
- Скачайте любую простую модель камня (low-poly rock .glb).
- Загрузите её геометрию и материал с помощью
useGLTF.- Подсказка: Вам нужно вытащить
nodes.Rock.geometryиmaterials.RockMaterial.
- Подсказка: Вам нужно вытащить
- Используйте
<instancedMesh>, чтобы создать кольцо из 5000 астероидов.- Формула кольца:
angle = (i / count) * Math.PI * 2radius = 10 + Math.random() * 2x = Math.cos(angle) * radiusz = Math.sin(angle) * radius
- Формула кольца:
- Добавьте
<Perf />и сравните производительность:- Сначала сделайте 5000 обычных
<mesh>через.map(). Посмотрите на FPS. - Потом замените на
<instancedMesh>. Почувствуйте разницу.
- Сначала сделайте 5000 обычных
Теперь вы знаете разницу между "Кодом, который работает" и "Кодом, который летает".
- Draw Calls — враг.
- Instancing — друг.
- Perf — судья.
Включаем строгий режим?
Как Senior Java Developer, вы наверняка чувствуете дискомфорт от динамической типизации JavaScript. Когда вы пишете mesh.positon.x (с опечаткой), JS молчит, а браузер падает в рантайме. В Java IDE ударила бы вас по рукам еще на этапе компиляции.
TypeScript (TS) возвращает этот комфорт во фронтенд. В экосистеме React Three Fiber типизация работает великолепно. Она дает IntelliSense: вы ставите курсор в тег <mesh castShadow ...>, нажимаете Ctrl+Space, и видите все 500 доступных свойств с документацией.
Если вы создавали проект через Vite как react, его можно мигрировать, но проще создать новый как react-ts.
npm create vite@latest my-3d-app -- --template react-ts
npm install three @types/three @react-three/fiber @react-three/drei
Важно: Пакет
@types/threeкритически важен. Он содержит "контракты" (d.ts файлы) для всех классов Three.js.
Это самое частое место, где TS спасает жизнь (и создает проблемы новичкам). useRef по умолчанию возвращает undefined. Если вы попытаетесь обратиться к ref.current.rotation, TS скажет: "Объект может быть null".
В Java вы бы написали: private Mesh mesh;. В TS мы используем Дженерики:
import { useRef } from 'react';
import { Canvas, useFrame } from '@react-three/fiber';
import * as THREE from 'three'; // Импортируем классы Three.js
const Box = () => {
// 1. Явно указываем, ЧТО будет лежать в рефе.
// null! (non-null assertion) — хак для TS.
// Мы говорим: "Мамой клянусь, React положит сюда меш до того, как сработает useFrame".
// Иначе пришлось бы везде писать ref.current?.rotation
const meshRef = useRef<THREE.Mesh>(null!);
useFrame((state, delta) => {
// Теперь IDE знает, что это THREE.Mesh
// Она подскажет методы .rotateX(), .position, и проверит типы аргументов.
meshRef.current.rotation.x += delta;
});
return (
<mesh ref={meshRef}>
<boxGeometry />
<meshStandardMaterial color="orange" />
</mesh>
);
};
Как передать параметры в компонент? В Java это аргументы конструктора. В TS это интерфейс.
Best Practice: Если ваш компонент — это обертка над <mesh>, наследуйте стандартные пропсы меша! Тогда вы сможете передавать position, rotation, onClick без лишнего кода.
import { ThreeElements } from '@react-three/fiber';
// Мы говорим: "Мои пропсы — это всё, что умеет обычный <mesh>, ПЛЮС мой color"
interface MyBoxProps extends ThreeElements['mesh'] {
customColor: string;
speed?: number; // ? означает необязательное поле
}
const MyBox = ({ customColor, speed = 1, ...props }: MyBoxProps) => {
// ...props прокидывает position, rotation и всё остальное в <mesh>
return (
<mesh {...props}>
<boxGeometry />
<meshStandardMaterial color={customColor} />
</mesh>
);
};
// Использование:
// TS не даст забыть customColor.
// TS разрешит передать position (благодаря наследованию).
<MyBox customColor="red" position={[0, 1, 0]} />
Что за объект event приходит в onClick? Это не стандартный DOM Event, это специальный R3F Event.
import { ThreeEvent } from '@react-three/fiber';
const handleClick = (e: ThreeEvent<MouseEvent>) => {
// e.object — это Object3D (базовый класс)
// e.point — это Vector3
console.log(e.point.x);
e.stopPropagation();
};
<mesh onClick={handleClick} />
Помните инструмент gltfjsx из Модуля 9? Если запустить его с флагом --types, он сгенерирует .tsx файл с интерфейсом вашей модели!
npx gltfjsx model.glb --types
Результат (Model.tsx):
import * as THREE from 'three'
import { GLTF } from 'three-stdlib'
// Автоматически сгенерированный тип структуры GLTF
type GLTFResult = GLTF & {
nodes: {
RobotArm: THREE.Mesh; // Мы точно знаем, что это Меш!
RobotHead: THREE.Mesh;
}
materials: {
Metal: THREE.MeshStandardMaterial; // Мы точно знаем материал!
}
}
export function Model(props: JSX.IntrinsicElements['group']) {
const { nodes, materials } = useGLTF('/model.glb') as GLTFResult
// Здесь nodes.RobotArm.geometry будет работать с автокомплитом
return <mesh geometry={nodes.RobotArm.geometry} ... />
}
"Рефакторинг на TS":
- Создайте новый проект на Vite + React + TS.
- Перенесите туда код "Вращающегося куба" из Модуля 8.
- Задача: Сделайте так, чтобы IDE не ругалась, и работал автокомплит:
- Типизируйте
useRef<THREE.Mesh>. - Создайте интерфейс
CubePropsс полемcolorиsize. - В
useFrameиспользуйте типизированные методы Three.js (например,mesh.current.rotateOnAxis(axis, angle)), гдеaxisдолжен бытьTHREE.Vector3.
- Типизируйте
Что мы имеем в арсенале:
- React Three Fiber: Декларативное ядро.
- Drei: Готовые компоненты (Камеры, Свет, Модели).
- Rapier: Физика.
- Zustand (подразумевается): Управление состоянием.
- Shaders/Post-processing: Визуальные эффекты.
- TypeScript: Надежность кода.
Чтобы закрепить навыки, я предлагаю вам реализовать проект "Интерактивное Портфолио Разработчика".
ТЗ:
- Сцена: Комната программиста (стол, ноутбук, кружка кофе). Найти бесплатную модель
.glb. - Интерактивность:
- При наведении на ноутбук экран ноутбука светится (Bloom).
- При клике на ноутбук камера плавно наезжает на экран (анимация камеры).
- На экране ноутбука — HTML-интерфейс (используйте компонент
<Html>из Drei) с текстом "Senior Java Developer" и ссылками на GitHub.
- Физика: На столе стоят кубики с логотипами Java, Spring, React. Их можно раскидывать мышкой.
- Технологии: React, TS, R3F, Drei, Post-processing.