Skip to content

Instantly share code, notes, and snippets.

@dmitry-osin
Last active January 21, 2026 16:13
Show Gist options
  • Select an option

  • Save dmitry-osin/e56bc041725d294521bdd1bf808d394e to your computer and use it in GitHub Desktop.

Select an option

Save dmitry-osin/e56bc041725d294521bdd1bf808d394e to your computer and use it in GitHub Desktop.
Подробный гайд по Three.js и React Three Fiber

Этап 1: Вход в профессию (Web & React Basics)

Цель: Перестать бояться кода и понять, как работает UI.

  1. HTML/CSS/Terminal: Верстка, Flexbox, npm/yarn, Vite.
  2. JavaScript Core: Переменные, циклы, функции, ES6+ (map, filter, destructuring, modules).
  3. React Fundamentals: JSX, Components, Props, Hooks (useState, useEffect, useRef). Без этого в R3F делать нечего.

Этап 2: 3D-Грамотность (General Three.js Concepts)

Цель: Научиться мыслить в 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).

Этап 3: Создание Миров (Interactive & Creative)

Цель: Делать красиво и интерактивно. 8. Интерактивность: Raycaster (клики, ховеры), события мыши. 9. Анимация и Стейт: Библиотеки анимации (GSAP или Framer Motion 3D). Использование Zustand для стейт-менеджмента (избегаем ре-рендеров React!). 10. Физика: Rapier (RigidBody, Collider, Gravity). 11. Пост-процессинг: Bloom, Noise, Glitch, Vignette (библиотека @react-three/postprocessing).

Этап 4: Deep Dive (Математика и Шейдеры) — Здесь начинается Senior

Цель: Создавать то, чего нет в стандартной библиотеке. 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. Партиклы (Частицы): Рендеринг миллионов точек с высокой производительностью.

Этап 5: Архитектура и Продакшен (Expert / Tech Lead)

Цель: Делать приложения, которые работают 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 на телефонах.

Этап 1: Вход в профессию (Web & React Basics)

Модуль 1. Настройка окружения и «Холст

Прежде чем писать код на JavaScript или React, нам нужно подготовить место, где будет жить наша 3D-сцена. В отличие от обычных сайтов, где контент идет сверху вниз, 3D-приложения обычно занимают весь экран (как игра).

В этом уроке мы:

  1. Развернем проект с помощью современного сборщика Vite.
  2. Настроим HTML (скелет).
  3. Настроим CSS (внешний вид), чтобы убрать белые рамки и растянуть приложение на весь экран.

1. Инструменты (Терминал и Node.js)

Чтобы запускать современные веб-проекты, нам нужна среда выполнения 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, значит сервер запущен. Откройте её в браузере.

2. HTML: Точка входа

В папке проекта найдите файл 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>

3. CSS: Подготовка холста

По умолчанию браузеры добавляют отступы (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;
}

Итог модуля

Что мы имеем на данный момент:

  1. Рабочий станок: У нас есть запущенный сервер (npm run dev), который мгновенно обновляет страницу, когда мы сохраняем код.
  2. Чистый лист: Наш index.html и index.css настроены так, чтобы приложение занимало 100% ширины и высоты окна браузера, без рамок и скроллбаров.

Это идеальная заготовка для полноэкранного 3D.

Тема: Модуль 2. JavaScript Core: Основы данных

В 3D-графике всё есть данные.

  • Позиция игрока — это Объект с координатами x, y, z.
  • Список врагов — это Массив.
  • Включен ли фонарик — это Булево значение (true/false).

В этом модуле мы разберем синтаксис языка JavaScript, который нужен для описания этих данных. Для вашего ученика это "строительные блоки".

1. Переменные: Коробки для данных

В старых учебниках можно встретить 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 нельзя положить другой объект, но можно менять содержимое этого объекта (об этом ниже).

2. Примитивные типы данных

JavaScript — язык с динамической типизацией. Нам не нужно писать int, float, string. Браузер сам понимает, что внутри.

  1. Number: В JS нет разделения на int и double. Всё есть число с плавающей точкой (IEEE 754).
    • 10 — число.
    • 3.14 — число.
  2. String: Текст. Можно писать в одинарных ' или двойных " кавычках.
  3. Boolean: Истина (true) или ложь (false).
const gravity = 9.8;     // Number
const levelName = "Mars"; // String
let isGameOver = false;   // Boolean

3. Объекты (Objects) — Главный инструмент

В 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;
Деструктуризация (Destructuring) — Магия синтаксиса

В 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

4. Массивы (Arrays)

Массив — это упорядоченный список. В отличие от 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

5. Функции (База)

Функция — это переиспользуемый блок кода. Она может принимать аргументы и возвращать результат.

// Объявление функции
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 не запрещает менять содержимое этой сумки.

Модуль 3. JavaScript: Продвинутый (ES6+)

В этом модуле мы разберем современный синтаксис (ES6+), на котором написан весь React и библиотека R3F. Если прошлый модуль был про «существительные» (данные), то этот — про «глаголы» (действия) и красивое оформление кода.

1. Стрелочные функции (Arrow Functions)

В современном 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!")} />

2. Метод массивов .map()

Это самый важный метод в 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()), на выходе — готовые детали (объекты сцены).

3. Spread-оператор (...)

Три точки ... — это оператор «распыления» (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         <-- Добавилось
}
*/

4. Модули (import / export)

Раньше весь 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-компонент).

5. Асинхронность (Async / Await)

JS однопоточный. Если вы начнете загружать тяжелую 3D-модель (100 Мб), и JS будет просто ждать, весь браузер зависнет. Чтобы этого не было, используются Промисы (Promises) — это как талончик в очереди. "Мы обещаем вернуть модель, когда она загрузится, а пока занимайся другими делами".

Современный способ работать с этим — ключевые слова async и await.

// Функция загрузки (симуляция)
const loadModel = async () => {
  console.log("Начинаю загрузку модели...");
  
  // await говорит: "Подожди здесь, пока fetch скачает файл, 
  // но не блокируй браузер (пусть анимации крутятся)".
  const response = await fetch('model.gltf'); 
  
  console.log("Модель загружена!");
  return response;
};

Итог модуля

Мы освоили инструменты профессионала:

  1. Стрелки => для краткости.
  2. .map() для превращения данных в элементы.
  3. ... (Spread) для копирования и объединения настроек.
  4. import/export для разделения кода на файлы.

Модуль 4. Философия React: Компоненты и Пропсы

Мы переходим к фундаменту React. React Three Fiber (R3F) — это не отдельная библиотека, это именно React, который просто рисует не <div> (блоки), а <mesh> (3D-объекты).

Поэтому, чтобы строить 3D-миры, ученик должен мыслить Компонентами.

1. JSX: HTML внутри JavaScript

Обычно новички пугаются, видя 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' }).

2. Компоненты (Components)

Компонент — это Функция, которая возвращает JSX. В 3D мы будем создавать компоненты: Spaceship, Planet, Tree. Это позволяет разбить огромный мир на маленькие, понятные куски.

Правила:

  1. Название функции всегда с Большой Буквы (MyComponent, а не myComponent).
  2. Функция должна вернуть один родительский элемент (нельзя вернуть два 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;

3. Пропсы (Props)

Пропсы (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;

Важно: Пропсы доступны только для чтения. Ребенок не может изменить свои пропсы (он не может сам себя переименовать). Изменить их может только Родитель.

4. Children (Вложенность)

Иногда нам нужно вложить один компонент внутрь другого, как матрешку. Для этого используется специальный пропс 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:

  1. Badge.jsx (Компонент-бейдж). Принимает пропсы: text (имя) и color (цвет).
  2. В 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>
  );
}

Итог модуля: Теперь ученик понимает, что интерфейс строится из кирпичиков.

  • Компонент = Кирпич.
  • Пропсы = Цвет и форма кирпича.

Модуль 5. Хуки (Hooks) — Магия React

Если Компоненты — это скелет и внешний вид, то Хуки — это мозг и нервная система. Без них наше приложение было бы просто статичной картинкой.

Хук (Hook) — это специальная функция, которая начинается с use (использовать). Она позволяет "подцепиться" к возможностям React.

Мы разберем «Святую Троицу» хуков, на которой держится 99% кода в React Three Fiber.

1. useState — Память компонента

Обычные переменные внутри функции "умирают", когда функция заканчивает работу. Чтобы компонент "запомнил", что мы нажали кнопку или изменили цвет, нужен 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>
  );
};

2. useEffect — Жизненный цикл

Компоненты рождаются (появляются на экране), живут (обновляются) и умирают (исчезают). useEffect позволяет выполнять код в эти моменты.

  • Аналогия для Java: Смесь @PostConstruct (инициализация) и @PreDestroy (очистка).

В 3D мы используем это, чтобы загрузить модель при старте или запустить звук.

Синтаксис:

useEffect(() => {
  // Код, который выполнится
}, [список_зависимостей]);

Массив зависимостей ([]) — самая важная часть:

  1. [] (пустой массив) — выполнить один раз при рождении (Mount).
  2. [count] — выполнять каждый раз, когда меняется переменная count.
  3. (нет массива) — выполнять при каждой перерисовке (Опасно! Может повесить браузер).

Пример:

import { useEffect } from 'react';

const Game = () => {
  useEffect(() => {
    console.log("Игра началась! (Компонент создан)");

    // Функция очистки (return function).
    // Сработает, когда компонент удалят со страницы.
    return () => {
      console.log("Игра окончена... (Компонент удален)");
    };
  }, []); // Пустой массив = только 1 раз при старте

  return <div>Game is running...</div>;
};

3. useRef — Прямой доступ (Критично для 3D!)

Это самый важный хук для производительности в 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.

ТЗ:

  1. При появлении на экране (useEffect) он запускает интервал (таймер), который тикает каждую секунду.
  2. В useState хранится количество секунд.
  3. Когда компонент удаляется, таймер должен остановиться (очистка в 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;

Итог Этапа 1

Мы закончили с основами Web и React. Ученик теперь знает:

  1. Как создать проект.
  2. Как писать на JS (Objects, Arrays, Arrow functions).
  3. Как создавать Компоненты.
  4. Как управлять ими через Хуки.

Этап 2: 3D-Грамотность (General Three.js Concepts)

Модуль 6. Анатомия 3D-сцены

Мы закончили с "плоским" миром веба и переходим в объем. Прежде чем написать <Canvas>, ученик должен визуализировать в голове, как устроен этот мир. Здесь мы почти не пишем код, мы строим ментальную модель.

1. Метафора "Съемочной площадки"

Three.js (и любой 3D-движок) работает как реальная киностудия. Чтобы получить картинку на экране, нам нужны три обязательных компонента. Если нет хотя бы одного — экран будет черным.

  1. Сцена (Scene): Это мир. Пустое пространство, куда мы ставим актеров (модели), декорации и свет. В React Three Fiber (R3F) это создается автоматически внутри компонента <Canvas>.
  2. Камера (Camera): Это глаз режиссера. Сцена может быть огромной, но мы видим только то, что попадает в объектив камеры.
  3. Рендерер (Renderer): Это художник, который берет данные Сцены и Камеры и рисует итоговую плоскую картинку (кадр) на экране монитора 60 раз в секунду. В R3F рендерер тоже спрятан внутри <Canvas>.

2. Система координат (X, Y, Z)

В школе на геометрии была ось 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), мы ничего не увидим (мы будем внутри объекта).

3. Анатомия Объекта: Mesh (Меш)

В 3D-графике видимый объект называется Mesh (Сетка). Mesh состоит из двух независимых частей. Это как тело и одежда.

  1. Geometry (Геометрия): Форма объекта. Это просто набор точек (вершин/vertices) в пространстве, соединенных линиями.
    • Примеры: BoxGeometry (куб), SphereGeometry (шар), PlaneGeometry (плоскость).
    • Важно: Геометрия невидима. Это просто математический каркас.
  2. Material (Материал): Внешний вид. Как поверхность реагирует на свет. Цвета, блеск, текстуры.
    • Примеры: "Красный пластик", "Полированный металл", "Светящаяся лампа".

Формула:

$$ Mesh=Geometry+Material $$

Если у объекта есть Геометрия, но нет Материала — он часто розовый (цвет ошибки) или невидимый. Если есть Материал, но нет Геометрии — рисовать нечего.

4. Граф сцены (Scene Graph)

Это иерархическая структура (дерево), в которой живут объекты. В 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>

Практическое задание для модуля (Мысленное или на бумаге)

Попросите ученика нарисовать дерево (Граф сцены) для Солнечной системы:

  1. Есть центр — Солнце.
  2. Вокруг Солнца вращается пустой контейнер (Группа) — Орбита Земли.
  3. Внутри Орбиты сидит Земля.
  4. Внутри Земли сидит Луна.

Вопрос на проверку: Если мы начнем вращать "Орбиту Земли" вокруг центра, что произойдет с Землей и Луной? (Ответ: Они начнут летать вокруг Солнца. Это самый простой способ сделать анимацию орбиты без сложной тригонометрии).

Итог модуля

Ученик узнал словарь 3D-разработчика:

  1. XYZ (правосторонняя система).
  2. Mesh (Геометрия + Материал).
  3. Parent/Child (если папа идет в магазин, ребенок на руках идет с ним).

Модуль 7. Свет и Материалы (Визуал)

В прошлом уроке мы создали форму (Geometry). Но без материала и света эта форма невидима или выглядит как черное пятно.

В этом модуле мы разберем, как раскрашивать объекты. Это самая творческая часть 3D. Для новичка здесь важно усвоить главное правило: Выбор материала диктует требования к свету.

1. Материалы: Из чего сделан объект?

В Three.js десятки материалов, но в 90% случаев мы используем только два. Разница между ними колоссальная.

А. MeshBasicMaterial (Базовый / "Картон")

Это самый простой материал. Он не реагирует на свет.

  • Если покрасить куб в красный цвет, он будет ярко-красным со всех сторон. Вы не увидите граней, углов или теней. Он будет выглядеть как плоский 2D-квадрат.
  • Зачем нужен: Для UI, для объектов, которые должны "светиться" сами по себе (как интерфейс в шлеме Железного Человека), или для отладки, когда свет еще не настроен.
  • Цена: Самый дешевый для процессора.
Б. MeshStandardMaterial (Стандартный / "Реалистичный")

Это PBR-материал (Physically Based Rendering). Это индустриальный стандарт.

  • Он реагирует на свет. Если на него не светить — он будет черным.
  • Если посветить сбоку — появится блик и тень.
  • У него есть два главных параметра "физики":
    1. Roughness (Шероховатость): 0.0 — зеркало, 1.0 — мел или резина (матовый).
    2. Metalness (Металличность): 0.0 — пластик/дерево, 1.0 — металл.

Совет: Пусть ученик всегда начинает с MeshStandardMaterial. Это сразу дает "объемную" картинку, как только мы добавим свет.

2. Источники Света: Солнце, Лампа, Фон

Без света MeshStandardMaterial невидим. В R3F свет — это просто компоненты, которые мы добавляем внутрь <Canvas>.

А. ambientLight (Окружающий свет)

Это свет, который есть везде. Он не имеет источника и направления. Он просто равномерно подсвечивает все объекты, чтобы тени не были абсолютно черными.

  • Аналогия: Пасмурный день, свет идет отовсюду. Теней нет.
Б. directionalLight (Направленный свет)

Лучи идут параллельно. Положение источника не важно (он бесконечно далеко), важно только направление.

  • Аналогия: Солнце.
В. pointLight (Точечный свет)

Свет исходит из одной точки во все стороны и затухает с расстоянием.

  • Аналогия: Лампочка, свеча, фаербол.

3. Текстуры: Оберточная бумага

Цвет (color) — это скучно. Чтобы сделать кирпичную стену, мы не моделируем каждый кирпич. Мы берем картинку (JPG/PNG) кирпичей и "натягиваем" её на плоскость.

Это называется Texture Mapping. Чтобы картинка легла правильно, у каждой 3D-модели есть невидимая разметка — UV-координаты. Это инструкция: "Какой пиксель картинки соответствует какой точке на 3D-модели".

4. Собираем всё в коде (R3F)

Теперь покажем, как эти теоретические концепции выглядят в 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>
    </>
  );
};

Практическое задание для модуля

Попросите ученика сделать "Мастерскую материалов":

  1. Создать сцену с 3 сферами в ряд.
  2. Добавить свет (ambient + directional).
  3. Настроить материалы сфер так, чтобы показать разные физические свойства:
    • Сфера 1: Матовый шар для боулинга (черный, roughness: 0, metalness: 0).
    • Сфера 2: Золотой шар (желтый, roughness: 0.1, metalness: 1).
    • Сфера 3: Резиновый мяч (синий, roughness: 1, metalness: 0).

Это научит его "чувствовать" параметры PBR.

Итог Этапа 2

Мы прошли теорию "3D-грамотности". Ученик понимает, что такое Сцена, Меши и Материалы. Теперь у нас есть база, чтобы написать первое настоящее приложение.

Этап 3: Создание Миров (Interactive & Creative)

Модуль 8. Первые шаги в R3F (Код)

Мы прошли теорию, настроили окружение и выучили основы React. Настало время Большого Взрыва. В этом модуле мы создадим нашу первую вселенную.

Здесь мы соединим знания:

  1. React: Компоненты и пропсы.
  2. Three.js: Меши и материалы.
  3. R3F: "Клей", который соединяет их вместе.

1. <Canvas> — Портал в 3D

В React Three Fiber (R3F) всё начинается с компонента <Canvas>. Это не просто HTML-тег. Это "портал".

  • Снаружи Canvas: Обычный HTML/DOM (заголовки, кнопки, скролл).
  • Внутри Canvas: Мир Three.js. Здесь не работают HTML-теги типа <div> или <span>. Здесь живут только 3D-объекты.

Canvas автоматически делает за нас огромную "грязную" работу:

  1. Создает Сцену и Камеру.
  2. Запускает Рендерер (WebGL).
  3. Запускает цикл анимации (Game Loop) на 60 FPS.
  4. Следит за изменением размера окна.
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>
  );
}

2. Декларативность: Магия превращения

Для 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} />).

3. Анимация: Хук useFrame

В 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;

Практическое задание для модуля

Пусть ученик модифицирует код выше, чтобы создать "Танец фигур":

  1. Создать два компонента: Cube и Sphere (используя <sphereGeometry />).
  2. Разместить их в сцене слева и справа (position={[-2, 0, 0]} и position={[2, 0, 0]}).
  3. Заставить их вращаться в разные стороны внутри useFrame.
  4. Бонус: Попробовать сделать так, чтобы сфера вращалась быстрее куба.

Чему он научится:

  • Создавать и переиспользовать компоненты в 3D.
  • Понимать координатную сетку (минус влево, плюс вправо).
  • Управлять скоростью анимации через математику (+= delta * speed).

Итог модуля

Ученик написал свой первый 3D-код.

  1. <Canvas> — это мир.
  2. useFrame — это время (пульс мира).
  3. useRef — это доступ к объектам для управления ими.

Модуль 9. Экосистема Drei (Швейцарский нож)

Мы научились создавать куб и вращать его. Но современный 3D-мир — это не просто геометрические примитивы. Нам нужны красивые модели машин или персонажей, реалистичный свет и возможность вращать камерой.

Писать всё это с нуля на чистом Three.js — это сотни строк математики. В мире React Three Fiber есть библиотека-спаситель: @react-three/drei.

Для Java-разработчика: Если R3F — это Spring Core (база), то Drei — это Spring Boot Starter + Lombok. Это набор готовых, преднастроенных компонентов, которые решают 90% рутинных задач одной строкой кода. Название "Drei" — это немецкая цифра 3 (игра слов с Three.js).

0. Установка

Чтобы магия заработала, нужно добавить библиотеку в проект:

npm install @react-three/drei

1. OrbitControls (Управление камерой)

Сейчас наша камера "прибита гвоздями". Чтобы дать пользователю возможность вращать сцену мышкой, зумить колесиком и двигаться, нужен контроллер.

В Drei это делается одним тегом:

import { OrbitControls } from '@react-three/drei';

<Canvas>
  <mesh>...</mesh>
  {/* Всё! Камера теперь управляется мышкой. */}
  {/* enableZoom={false} чтобы отключить зум, autoRotate чтобы вращалась сама */}
  <OrbitControls />
</Canvas>

2. Environment (Окружение и Свет)

Настройка света вручную (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>

3. Загрузка Моделей (GLTF)

Мы не лепим персонажей кодом из кубиков. Мы делаем их в 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

Простой способ плох тем, что мы не видим структуру модели. Мы не можем поменять цвет двери машины или скрыть колесо, потому что модель — это единый "черный ящик".

Инструмент 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;

Практическое задание для модуля

  1. Установите drei: npm install @react-three/drei.
  2. Запустите код из примера выше.
  3. Поиграйтесь с preset в Environment: попробуйте "sunset", "forest", "night". Посмотрите, как меняется шар.
  4. Челлендж: Найдите в интернете любую .glb модель (например, на Poly Pizza ), скачайте в папку public, загрузите её через <primitive object={...} /> вместо шара.

Итог модуля

С библиотекой Drei мы перешли от "низкоуровневого кода" к "сборке конструктора".

  • OrbitControls дал свободу.
  • Environment дал реализм.
  • useGLTF позволил загружать контент.

Модуль 10. Интерактивность

Мы создали красивый мир, но он ведет себя как музейный экспонат — «смотреть можно, трогать нельзя». В этом модуле мы оживим сцену, научив объекты реагировать на клики и наведение мыши.

Для Java-разработчика это будет приятно: система событий в R3F практически идентична стандартному React DOM (onClick, onMouseEnter), но под капотом происходит сложная математика.

1. Проблема: Как кликнуть в пустоту?

В обычном вебе, когда вы кликаете на кнопку, браузер точно знает координаты прямоугольника кнопки на экране. В 3D всё сложнее. Экран плоский (2D), а мир объемный (3D).

Raycasting (Пускание лучей) Чтобы понять, попали ли вы мышкой по кубу, движок делает следующее:

  1. Из точки клика на экране (X, Y) выпускается невидимый лазерный луч (Ray) вглубь сцены.
  2. Луч летит сквозь пространство.
  3. Движок проверяет: пересек ли этот луч какой-нибудь объект (Mesh)?
  4. Если да — событие срабатывает на ближайшем к камере объекте.

В ванильном Three.js это 10-20 строк кода настройки Raycaster. В R3F это работает автоматически.

2. События указателя (Pointer Events)

В R3F мы используем универсальные события Pointer (они работают и для мыши, и для тач-экранов телефонов). Мы вешаем их прямо на <mesh>.

  • onClick — клик (нажал и отпустил).
  • onPointerOver — курсор "зашел" на объект (аналог hover).
  • onPointerOut — курсор "ушел" с объекта.
  • onPointerDown / onPointerUp — нажатие и отпускание.

Синтаксис:

<mesh onClick={(event) => console.log('Бум!')} />

3. Связываем События и Стейт (State)

Чтобы объект изменился при клике, нам нужно сохранить его состояние. Здесь в игру вступает хук useState, который мы изучили в Модуле 5.

Сценарий:

  1. Куб оранжевый.
  2. При наведении курсора (hover) он становится розовым.
  3. При клике (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>
  );
};

4. Объект события (The Event Object)

Иногда нам нужно знать больше, чем просто факт клика. В функцию обработчика приходит объект event, который содержит массу полезной информации из 3D-мира.

onClick={(event) => {
  // Точка в 3D пространстве, куда попал луч [x, y, z]
  console.log(event.point); 
  
  // Расстояние от камеры до точки касания
  console.log(event.distance); 
  
  // Объект, по которому кликнули
  console.log(event.object);

  // ВАЖНО: stopPropagation (Остановка всплытия)
  // Если за кубом стоит другой куб, луч может пробить их обоих.
  // stopPropagation говорит: "Я поймал клик, дальше луч не пускай".
  event.stopPropagation();
}}

Практическое задание для модуля

Создайте "Интерактивную галерею":

  1. Сделайте сцену с 3 разными объектами (Куб, Сфера, Тетраэдр — <tetrahedronGeometry />).
  2. Напишите компонент, который принимает цвет и позицию.
  3. Логика: При клике на объект он должен начать вращаться (используйте useState для флага isSpinning и проверяйте этот флаг внутри useFrame).
    • Подсказка: Вам понадобится useRef для вращения и useState для разрешения вращения.
    • useFrame(() => { if (isSpinning) ref.current.rotation.y += 0.05 })

Итог Этапа 3

Поздравляю, мы закончили основной блок творчества! Ученик теперь может:

  1. Создать сцену.
  2. Наполнить её объектами и моделями.
  3. Сделать её красивой (свет, тени).
  4. Сделать её живой (анимация и взаимодействие).

Этап 4: Deep Dive (Математика и Шейдеры)

Модуль 11. Физика (Rapier)

До сих пор наши объекты левитировали в пустоте. Чтобы сделать игру или реалистичную симуляцию, нам нужна гравитация, столкновения и отскоки.

Писать математику столкновений вручную — это ад (нужно рассчитывать векторы отражения, искать точки пересечения граней). Вместо этого мы используем Физический Движок. В R3F стандартом сейчас является библиотека @react-three/rapier.

Почему Rapier? Он написан на Rust и скомпилирован в WebAssembly. Это значит, что он работает очень быстро, не нагружая основной JS-поток. Для React есть отличная обертка.

0. Установка

npm install @react-three/rapier

1. Основные концепции физики

Чтобы внедрить физику, нам нужно обернуть наши 3D-объекты в специальные контейнеры.

  1. <Physics>: Это "Бог" физического мира. Он создает гравитацию и рассчитывает симуляцию. Оборачивает всю сцену.
  2. RigidBody (Твердое тело): Это компонент, который говорит движку: "Этот объект имеет массу и участвует в физике".
    • У RigidBody есть типы (как в Unity/Unreal):
    • Dynamic (По умолчанию): Имеет массу, падает, отскакивает. (Пример: мяч, персонаж).
    • Fixed: Недвижимый объект. Бесконечная масса. (Пример: пол, стены).
    • Kinematic: Движется программно (анимацией), снося всё на пути. (Пример: движущаяся платформа, лифт).
  3. Colliders (Коллайдеры): Это форма для расчетов столкновений. Обычно Rapier сам угадывает форму по геометрии (Cuboid, Ball), но иногда их нужно настраивать вручную.

2. Простая сцена с гравитацией

Давайте уроним куб на пол.

Обратите внимание: мы не пишем никакой анимации. Мы просто говорим "Это тело динамическое", и движок сам тянет его вниз.

Файл: 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;

3. Взаимодействие (Импульсы)

Физика становится интересной, когда мы начинаем вмешиваться. Чтобы толкнуть объект, мы не меняем его 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>
  );
};

4. Авто-генерация Коллайдеров (Trimesh vs Convex Hull)

По умолчанию RigidBody пытается угадать форму (Cuboid для куба, Ball для сферы). Но что если у нас модель "Бублик" (Torus) или сложный стул?

Есть три основных стратегии для свойства colliders:

  1. colliders="hull" (Convex Hull): Оборачивает объект в "подарочную упаковку". Все дырки закрываются. Если бублик упадет, он будет вести себя как шайба (дырки нет). Быстро.
  2. colliders="trimesh": Точная копия сетки объекта. Дырка в бублике будет физической. Медленно и глючно для динамических объектов (может провалиться сквозь пол). Используйте только для статичных стен/ландшафта.
  3. Составные примитивы: Лучше всего собрать сложный объект из простых невидимых кубов и шаров.

Практическое задание для модуля

Проект "Дженга" (Башня):

  1. Создайте пол (type="fixed").
  2. В цикле (используя .map из JS) создайте башню из 10-15 кубиков, стоящих друг на друге.
  3. Создайте отдельный "Шар-разрушитель".
  4. Добавьте обработчик клика на шар: при клике примените к нему сильный импульс в сторону башни.
  5. Результат: Шар врезается в башню, кубики реалистично разлетаются.

Итог Этапа 3

Этап 4: Deep Dive (Математика и Шейдеры)

Модуль 12. Математика 3D (Векторы и Матрицы)

До этого момента мы двигали объекты так: x = x + 1. Это работает для движения по прямой. Но что, если нужно двигаться "вперед, куда смотрит персонаж" или "плавно повернуться к врагу"? Тут простая арифметика ломается. Добро пожаловать в Линейную Алгебру.

Для Senior Java Dev: В Three.js математика инкапсулирована в классы Vector3, Matrix4, Quaternion. Это аналоги классов из javax.vecmath или libgdx. Главное отличие от Java-бэкенда: здесь мы избегаем создания новых объектов (new Vector3()) в цикле рендера (useFrame), чтобы не триггерить Garbage Collector 60 раз в секунду.

1. Вектор (Vector3) — Атом 3D

Вектор — это просто стрелка в пространстве. В Three.js это класс THREE.Vector3(x, y, z).

У вектора есть два смысла (контекст определяет смысл):

  1. Точка (Position): "Где я нахожусь". Координаты в мире.
  2. Направление (Direction): "Куда я смотрю" или "С какой скоростью лечу".
Важнейшие операции:
  1. Сложение (add):
    • Смысл: Движение.
    • Текущая Позиция + Вектор Скорости = Новая Позиция.
  2. Вычитание (sub):
    • Смысл: Вектор от А к Б.
    • Цель - Я = Вектор направления к цели.
  3. Нормализация (normalize):
    • Смысл: Нам часто важно только направление, а не длина. Нормализация делает длину стрелки равной 1, сохраняя направление.
    • Если не нормализовать вектор скорости, то персонаж будет бежать быстрее по диагонали (классический баг новичка).
  4. Длина (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);
  }
});

2. Вращение: Эйлер vs Кватернионы

Это самая больная тема.

Углы Эйлера (Euler Angles)

Это то, к чему мы привыкли: rotation.x, rotation.y, rotation.z.

  • Плюсы: Понятно человеку ("Поверни на 90 градусов по Y").
  • Минусы: Gimbal Lock (Шарнирный замок).

Проблема Gimbal Lock: Если повернуть объект на 90 градусов по оси X, то оси Y и Z "склеиваются". Вращение по Y начинает делать то же самое, что и вращение по Z. Вы теряете одну степень свободы. Самолет в симуляторе начинает вести себя неадекватно.

Кватернионы (Quaternions)

Это 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);
});

3. Матрицы (Matrices): "Черный ящик"

Матрица 4x4 (Matrix4) — это контейнер, который хранит ВСЁ о трансформации объекта сразу: позицию, вращение и масштаб.

$$ \left​rrr0​rrr0​rrr0​xyz1​\right​ $$

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):

  1. Создайте "Глаз" (Сферу с "зрачком" — еще одной маленькой черной сферой, прилепленной сбоку, чтобы было видно вращение).
  2. Добавьте на сцену другой объект ("Муха"), который летает по кругу (используйте Math.sin и Math.cos с таймером state.clock.elapsedTime).
  3. Задача: В useFrame заставьте Глаз постоянно смотреть на Муху, используя метод .lookAt().
  4. Усложнение (Math Challenge): Не используйте .lookAt(). Попробуйте рассчитать вектор направления вручную и повернуть глаз, используя кватернионы (опционально для Junior, обязательно для Middle).

Итог модуля

Математика в Three.js — это не про вычисление интегралов. Это про Векторы.

  • Хочешь узнать расстояние? vec.distanceTo().
  • Хочешь двигаться? pos.add(velocity).
  • Хочешь смотреть? obj.lookAt().

Модуль 13. Введение в Шейдеры (GLSL)

Это "Финал" для большинства разработчиков. Если вы умеете писать шейдеры — вы волшебник. Вы больше не ограничены стандартными материалами 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.

1. Конвейер: Vertex и Fragment

Чтобы нарисовать объект, GPU выполняет два шага (два шейдера):

А. Вершинный шейдер (Vertex Shader)
  • Задача: Взять точки 3D-модели (вершины) и перевести их в 2D-координаты экрана.
  • Метафора: Скульптор. Он может раздуть модель, сплющить её или заставить извиваться (как флаг на ветру), меняя координаты вершин position.
  • Выход: Переменная gl_Position (координата на экране).
Б. Фрагментный шейдер (Fragment Shader)
  • Задача: Покрасить пиксели между вершинами. Выполняется миллионы раз (для каждого пикселя монитора).
  • Метафора: Художник. Он решает, какого цвета будет эта конкретная точка: красная, с текстурой или прозрачная.
  • Выход: Переменная gl_FragColor (итоговый цвет rgba).

2. Передача данных (Data Flow)

Как передать данные из JS в C-код шейдера? Есть три типа переменных:

  1. Uniforms (Униформы): Глобальные переменные. Одинаковы для всех вершин и пикселей.
    • Пример: uTime (время), uColor (цвет), uMouse (мышь). Мы шлем их из React.
  2. Attributes (Атрибуты): Персональные данные вершины.
    • Пример: position (где точка), uv (координата текстуры), normal (куда смотрит грань). Three.js дает их нам автоматически.
  3. Varyings (Вариинги): Почтальон. Передает данные из Vertex Shader -> в Fragment Shader.
    • Вершинный шейдер считает данные, кладет в varying, и Фрагментный их получает интерполированными.

3. Практика: "Дышащая" капля

Давайте напишем свой материал. Мы сделаем сферу, которая:

  1. Меняет форму (волны) — Vertex Shader.
  2. Меняет цвет в зависимости от высоты волны — 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>
  );
}

Разбор полетов (Q&A)

  1. projectionMatrix * modelViewMatrix * vec4(...) — Что это за заклинание?
    • Это умножение матриц из Модуля 12.
    • vec4(pos, 1.0) — Превращаем точку x,y,z в 4D-вектор (нужно для умножения).
    • modelViewMatrix — Переносит точку из локальных координат (относительно центра сферы) в координаты камеры (относительно глаз игрока).
    • projectionMatrix — Добавляет перспективу (далекое становится маленьким) и сплющивает всё в 2D-экран.
  2. varying:
    • Заметьте, мы вычислили vElevation для 3-х вершин треугольника.
    • А пикселей внутри треугольника — сотни.
    • GPU сам плавно интерполирует (смешивает) значение vElevation для каждого пикселя. Если в одном углу 0, а в другом 1, посередине само появится 0.5.

Практическое задание для модуля

"Cyberpunk Grid" (Фрагментный шейдер):

  1. Возьмите код выше, но уберите искажение вершин (верните обычный gl_Position).
  2. В fragmentShader используйте vUv (текстурные координаты, от 0 до 1).
  3. Напишите формулу, которая рисует решетку.
    • Подсказка: step(0.9, sin(vUv.x * 20.0)) даст полоски. Объедините полоски по X и Y.
  4. Сделайте так, чтобы решетка "текла" вверх (vUv.y + uTime).

Итог модуля

Шейдеры — это мощь.

  • Vertex Shader = Форма.
  • Fragment Shader = Цвет.
  • Uniforms = Параметры из React.

Модуль 14. Пост-процессинг (Post-processing)

Мы научились делать красивые объекты и писать шейдеры. Но иногда нам нужно добавить эффект, который затрагивает всю картинку сразу, а не отдельный объект. Например:

  • Сделать яркие объекты светящимися (Bloom).
  • Размыть задний план, как в фотоаппарате (Depth of Field).
  • Добавить шум зернистой пленки или эффект глюка (Glitch).

Это называется Пост-процессинг. Метафора: Сначала мы рендерим 3D-сцену, получаем плоскую картинку (как фото), а потом накладываем на неё Instagram-фильтры.

В R3F для этого есть шикарная библиотека: @react-three/postprocessing.

0. Установка

npm install @react-three/postprocessing

1. EffectComposer — Конвейер эффектов

Все эффекты должны жить внутри компонента <EffectComposer>. Он берет на себя сложную работу:

  1. Рендерит сцену не на экран, а во внутреннюю память (буфер/текстуру).
  2. Прогоняет эту текстуру через цепочку шейдеров-эффектов.
  3. Выводит результат на экран.

Базовый пример (Неоновое свечение):

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>
  );
}

2. Популярные эффекты

Вот "джентльменский набор", который делает картинку кинематографичной:

  1. <Bloom>: Свечение. Делает магию, неон, лазеры.
    • Совет: Используйте свойство mipmapBlur, это более производительный и красивый алгоритм размытия.
  2. <DepthOfField> (DOF): Глубина резкости.
    • Нужно указать focusDistance (на каком расстоянии фокус) и focalLength (длина объектива).
    • Внимание: Очень тяжелый эффект для GPU.
  3. <Noise>: Шум (зерно).
    • Скрывает "лесенки" и делает картинку менее стерильной/компьютерной.
  4. <Glitch>: Глюки.
    • Полезно для Cyberpunk стилистики или эффекта поломки интерфейса.

3. Производительность (Performance Warning)

Пост-процессинг — это дорого. Каждый эффект — это Full Screen Pass. Это значит, что для каждого кадра шейдер должен пробежаться по каждому из 2 миллионов пикселей (на экране 1080p). Если у вас 5 эффектов, видеокарта делает 10 миллионов операций на пикселях за один кадр.

Правила оптимизации:

  1. Не жадничайте. Используйте только то, что действительно нужно.
  2. disableNormalPass: В <EffectComposer> часто можно добавить этот проп, если вы не используете эффекты, зависящие от геометрии (например, SSAO). Это экономит один рендер сцены.
  3. Разрешение: Можно рендерить эффекты в меньшем разрешении (половина экрана), но это продвинутая техника.

Практическое задание для модуля

Проект "Сломанный Терминал":

  1. Создайте сцену с черным фоном (<color attach="background" args={['black']} />).
  2. Добавьте зеленый текст (можно использовать <Text> из @react-three/drei), который пишет "SYSTEM FAILURE".
  3. Настройте материал текста так, чтобы он был очень ярким (color={[0, 5, 0]}).
  4. Добавьте <EffectComposer>:
    • Bloom: Чтобы текст светился как на старых ЭЛТ-мониторах.
    • Scanline (из @react-three/postprocessing): Полоски сканирования (опционально, если найдете, или замените на Noise).
    • Glitch: Добавьте эффект глюка, который срабатывает иногда.

Итог модуля

Пост-процессинг превращает "3D-модельку" в "Кадр из фильма". Но помните: лучший пост-процессинг — тот, который незаметен. Он должен дополнять атмосферу, а не перекрывать её.

Этап 5: Архитектура и Продакшен (Expert / Tech Lead)

Модуль 15. Производительность (Instancing & Optimization)

Мы подошли к теме, которая отличает демо-проект от продакшен-решения. Представьте: вы создали красивый лес. В нем 5000 деревьев. Вы запускаете проект... и видите 3 кадра в секунду (FPS). Процессор кипит, кулер воет.

Почему? Проблема не в количестве полигонов (GPU может нарисовать миллионы треугольников). Проблема в общении между CPU (JS) и GPU.

Для Java-разработчика: Это классическая проблема N+1 запросов к базе данных, только здесь вместо БД — видеокарта. Каждый <mesh> вызывает команду отрисовки (Draw Call). 5000 мешей = 5000 обращений к драйверу видеокарты за один кадр (16 мс). Это "смерть через тысячу порезов".

Решение: Instancing (Инстансинг). Мы отправляем геометрию и материал один раз, а вместе с ними — массив координат для 5000 копий. GPU рисует их всех за 1 вызов (1 Draw Call).

1. InstancedMesh: Один Меш, Тысяча Копий

Вместо <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>
  );
}

2. Управление ресурсами (dispose)

В Java есть Garbage Collector (GC), который чистит память. В JS он тоже есть. НО: WebGL хранит текстуры и геометрию не в оперативной памяти (RAM), а в видеопамяти (VRAM). JS GC не имеет доступа к видеокарте.

Проблема: Если вы удалите компонент <mesh> из React-дерева, JS-объект удалится. Но текстура на 50 Мб останется висеть в видеопамяти. Если пользователь будет переходить между страницами, VRAM переполнится и вкладка упадет (Crash).

Решение в R3F: Хорошая новость: React Three Fiber автоматически вызывает метод .dispose() для всех геометрий и материалов, когда компонент удаляется из дерева. Вам нужно беспокоиться об этом только если вы:

  1. Создаете ресурсы вручную (new THREE.TextureLoader().load(...)) вне хуков.
  2. Храните глобальные ссылки на текстуры.

3. Профилирование (r3f-perf)

В Java вы используете JProfiler или VisualVM. В R3F есть r3f-perf. Это панель мониторинга, которая показывает правду.

Установка:

npm install r3f-perf

Использование:

import { Perf } from 'r3f-perf';

<Canvas>
  <Perf position="top-left" />
  {/* Ваша сцена */}
</Canvas>

На что смотреть:

  1. FPS: Должно быть 60.
  2. Draw Calls: Если их > 100-200, пора объединять меши или использовать Instancing.
  3. Textures: Сколько памяти занимают картинки.
  4. Geometries: Количество треугольников.

Практическое задание для модуля

"Астероидный пояс" (Оптимизация):

  1. Скачайте любую простую модель камня (low-poly rock .glb).
  2. Загрузите её геометрию и материал с помощью useGLTF.
    • Подсказка: Вам нужно вытащить nodes.Rock.geometry и materials.RockMaterial.
  3. Используйте <instancedMesh>, чтобы создать кольцо из 5000 астероидов.
    • Формула кольца: angle = (i / count) * Math.PI * 2 radius = 10 + Math.random() * 2 x = Math.cos(angle) * radius z = Math.sin(angle) * radius
  4. Добавьте <Perf /> и сравните производительность:
    • Сначала сделайте 5000 обычных <mesh> через .map(). Посмотрите на FPS.
    • Потом замените на <instancedMesh>. Почувствуйте разницу.

Итог модуля

Теперь вы знаете разницу между "Кодом, который работает" и "Кодом, который летает".

  • Draw Calls — враг.
  • Instancing — друг.
  • Perf — судья.

Модуль 16. TypeScript в R3F

Включаем строгий режим?

Как Senior Java Developer, вы наверняка чувствуете дискомфорт от динамической типизации JavaScript. Когда вы пишете mesh.positon.x (с опечаткой), JS молчит, а браузер падает в рантайме. В Java IDE ударила бы вас по рукам еще на этапе компиляции.

TypeScript (TS) возвращает этот комфорт во фронтенд. В экосистеме React Three Fiber типизация работает великолепно. Она дает IntelliSense: вы ставите курсор в тег <mesh castShadow ...>, нажимаете Ctrl+Space, и видите все 500 доступных свойств с документацией.

1. Настройка

Если вы создавали проект через 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.

2. useRef и Дженерики

Это самое частое место, где 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>
  );
};

3. Типизация Пропсов (Interfaces)

Как передать параметры в компонент? В 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]} />

4. Типизация Событий

Что за объект 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} />

5. GLTFJSX и Типы

Помните инструмент 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":

  1. Создайте новый проект на Vite + React + TS.
  2. Перенесите туда код "Вращающегося куба" из Модуля 8.
  3. Задача: Сделайте так, чтобы IDE не ругалась, и работал автокомплит:
    • Типизируйте useRef<THREE.Mesh>.
    • Создайте интерфейс CubeProps с полем color и size.
    • В useFrame используйте типизированные методы Three.js (например, mesh.current.rotateOnAxis(axis, angle)), где axis должен быть THREE.Vector3.

Итог курса

Что мы имеем в арсенале:

  1. React Three Fiber: Декларативное ядро.
  2. Drei: Готовые компоненты (Камеры, Свет, Модели).
  3. Rapier: Физика.
  4. Zustand (подразумевается): Управление состоянием.
  5. Shaders/Post-processing: Визуальные эффекты.
  6. TypeScript: Надежность кода.

Финальный проект (Дипломная работа)

Чтобы закрепить навыки, я предлагаю вам реализовать проект "Интерактивное Портфолио Разработчика".

ТЗ:

  1. Сцена: Комната программиста (стол, ноутбук, кружка кофе). Найти бесплатную модель .glb.
  2. Интерактивность:
    • При наведении на ноутбук экран ноутбука светится (Bloom).
    • При клике на ноутбук камера плавно наезжает на экран (анимация камеры).
    • На экране ноутбука — HTML-интерфейс (используйте компонент <Html> из Drei) с текстом "Senior Java Developer" и ссылками на GitHub.
  3. Физика: На столе стоят кубики с логотипами Java, Spring, React. Их можно раскидывать мышкой.
  4. Технологии: React, TS, R3F, Drei, Post-processing.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment