🎯

Кодингові завдання

Відточуйте свої навички WebGL і симуляцій на практичних завданнях. Кожне завдання містить стартову заготовку, поступові підказки та еталонний розв'язок, який можна розблокувати.

Ваш прогрес 0 / 6 виконано
2легкі завдання
2середні завдання
2складні завдання
~12 годзагальний час навчання

Усі завдання

Починаючи зі сфери, що зависла в повітрі, додайте силу гравітації, яка щокадру прискорює м'яч донизу. Коли він торкається підлоги, застосуйте відскок із поглинанням енергії, щоб м'яч зрештою зупинився. Опануйте інтегрування за Ейлером, накопичення швидкості та коефіцієнт відновлення.

  • М'яч падає з vy += g * dt, що застосовується щокадру
  • М'яч відскакує від площини підлоги за vy *= -restitution
  • М'яч сповільнюється та зупиняється (restitution < 1)
  • Керування: повзунок сили гравітації (0 → 30 м/с²)
// Three.js r160 вже завантажено
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, innerWidth/innerHeight, 0.1, 100);
camera.position.set(0, 2, 8);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);

// М'яч
const ball = new THREE.Mesh(
  new THREE.SphereGeometry(0.5, 32, 32),
  new THREE.MeshStandardMaterial({ color: 0x3b82f6 })
);
ball.position.y = 5;
scene.add(ball);

// Підлога
scene.add(new THREE.Mesh(
  new THREE.PlaneGeometry(20, 20),
  new THREE.MeshStandardMaterial({ color: 0x1e293b })
).rotateX(-Math.PI / 2));

scene.add(new THREE.AmbientLight(0xffffff, 0.5));
scene.add(Object.assign(new THREE.DirectionalLight(0xffffff, 1), { position: { set: (x,y,z) => { } } }));

// TODO: додайте фізику гравітації тут
let vy = 0;
const GRAVITY = 9.8;        // м/с²
const RESTITUTION = 0.7;    // пружність
const FLOOR_Y = 0.5;        // радіус м'яча

let last = performance.now();
function animate() {
  requestAnimationFrame(animate);
  const now = performance.now();
  const dt = Math.min((now - last) / 1000, 0.05);
  last = now;

  // ← Ваш код тут

  renderer.render(scene, camera);
}
animate();
Щокадру додавайте гравітацію до вертикальної швидкості: vy -= GRAVITY * dt. Потім рухайте м'яч: ball.position.y += vy * dt. Знак мінус змушує гравітацію тягнути донизу (від'ємний Y).
Після переміщення м'яча перевірте if (ball.position.y < FLOOR_Y). Якщо умова істинна, поверніть його назад: ball.position.y = FLOOR_Y, потім змініть напрямок і послабте швидкість: vy = Math.abs(vy) * RESTITUTION. Якщо vy < 0.1, прирівняйте її до 0, щоб м'яч остаточно зупинився.
Додайте поверх полотна <input type="range" min="0" max="30" value="9.8">. Зчитуйте його значення всередині циклу анімації як const g = parseFloat(slider.value). Не забудьте про <label> для доступності.
Відкрити у Пісочниці ↗
⚠️ Спершу спробуйте написати код самостійно — так ви навчитеся більшого.
vy -= GRAVITY * dt;
ball.position.y += vy * dt;

if (ball.position.y < FLOOR_Y) {
  ball.position.y = FLOOR_Y;
  vy = Math.abs(vy) * RESTITUTION;
  if (vy < 0.05) vy = 0;
}

Створіть плавну анімовану синусоїду за допомогою лінії THREE.BufferGeometry. Хвиля має рухатися в часі (зсув фази щокадру) та реагувати на повзунки амплітуди, частоти й швидкості. Це основа багатьох візуалізацій хвильової фізики.

  • Відрендерте плавну лінію за допомогою THREE.Line із BufferGeometry
  • Анімуйте хвилю: y = A * sin(f*x - speed*t)
  • Оновлюйте позиції вершин щокадру (повторно використовуйте буфер — без повторного виділення пам'яті)
  • Повзунки HUD: амплітуда (0.1 → 3), частота (0.5 → 10), швидкість (0 → 5)
const NUM_POINTS = 512;
const X_RANGE = 20; // хвиля простягається від -10 до +10

// Будуємо геометрію один раз
const positions = new Float32Array(NUM_POINTS * 3);
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const wave = new THREE.Line(
  geometry,
  new THREE.LineBasicMaterial({ color: 0x38bdf8, linewidth: 2 })
);
scene.add(wave);

let t = 0;
function updateWave(amplitude, frequency, speed) {
  // TODO: заповніть positions[] даними синусоїди
  // x змінюється від -X_RANGE/2 до +X_RANGE/2
  // y = amplitude * Math.sin(frequency * x - speed * t)
  // z = 0
  geometry.attributes.position.needsUpdate = true;
}
Пройдіть циклом i від 0 до NUM_POINTS. Обчисліть x = (i / (NUM_POINTS - 1)) * X_RANGE - X_RANGE / 2, щоб рівномірно розподілити від -10 до +10. Збережіть як positions[i*3] = x.
Збільшуйте лічильник часу щокадру: t += dt. Використовуйте його в синусі: y = A * Math.sin(freq * x - spd * t). Доданок - spd * t створює рух праворуч.
Збільште NUM_POINTS (спробуйте 512+). Також перевірте, що ви встановлюєте geometry.attributes.position.needsUpdate = true щокадру, інакше Three.js не перезавантажить буфер.
Відкрити у Пісочниці ↗
⚠️ Спершу спробуйте написати код самостійно.
function updateWave(amplitude, frequency, speed) {
  for (let i = 0; i < NUM_POINTS; i++) {
    const x = (i / (NUM_POINTS - 1)) * X_RANGE - X_RANGE / 2;
    const y = amplitude * Math.sin(frequency * x - speed * t);
    positions[i * 3]     = x;
    positions[i * 3 + 1] = y;
    positions[i * 3 + 2] = 0;
  }
  geometry.attributes.position.needsUpdate = true;
}

Реалізуйте алгоритм Boids Крейга Рейнольдса: 100 агентів, які збираються у зграю лише за трьома локальними правилами. Кожен boid дивиться на сусідів у межах радіуса сприйняття та керує собою, щоб розділятися, вирівнюватися та згуртовуватися. Зграйна поведінка виникає без жодної глобальної координації.

  • Розділення: відхиляйтеся від boids, що надто близько
  • Вирівнювання: керуйте до середнього курсу сусідніх boids
  • Згуртування: керуйте до середньої позиції сусідніх boids
  • Замкнені межі (boids телепортуються через краї)
  • Повзунки HUD для радіуса сприйняття та ваг 3 правил
Кожному boid потрібно: { pos: THREE.Vector3, vel: THREE.Vector3 }. Зберігайте плоский масив із N boids. Обмежуйте швидкість щокадру: if (vel.length() > MAX_SPEED) vel.setLength(MAX_SPEED).
Для кожного boid A пройдіть усіма boids B. Якщо A.pos.distanceTo(B.pos) < PERCEPTION (і B ≠ A), то B — сусід. Зберігайте позиції та швидкості сусідів у тимчасових масивах, а потім усереднюйте їх для згуртування та вирівнювання.
Для кожного близького сусіда B обчисліть вектор away = A.pos.clone().sub(B.pos). Масштабуйте його обернено до відстані: away.divideScalar(dist * dist). Просумуйте всі ці вектори, а потім додайте steerSeparation.multiplyScalar(wSeparation) до швидкості.
Перевірте, що ви обмежуєте швидкість після підсумовування всіх трьох правил. Також переконайтеся, що зона розділення менша за радіус сприйняття. Типове налаштування: сприйняття = 50, розділення = 25. Налаштуйте ваги: розділення > вирівнювання > згуртування зазвичай працює найкраще.
Відкрити у Пісочниці ↗ Переглянути повну симуляцію
⚠️ Спершу спробуйте написати код самостійно.
function applyBoidRules(boids, wSep, wAli, wCoh, perception, sepDist) {
  const steer = new THREE.Vector3();
  boids.forEach((b, i) => {
    const sep = new THREE.Vector3();
    const ali = new THREE.Vector3();
    const coh = new THREE.Vector3();
    let nAli = 0, nCoh = 0;
    boids.forEach((o, j) => {
      if (i === j) return;
      const d = b.pos.distanceTo(o.pos);
      if (d < sepDist) {
        sep.add(b.pos.clone().sub(o.pos).divideScalar(d * d));
      }
      if (d < perception) {
        ali.add(o.vel); nAli++;
        coh.add(o.pos); nCoh++;
      }
    });
    if (nAli) ali.divideScalar(nAli).sub(b.vel).multiplyScalar(wAli);
    if (nCoh) coh.divideScalar(nCoh).sub(b.pos).multiplyScalar(wCoh);
    sep.multiplyScalar(wSep);
    b.vel.add(sep).add(ali).add(coh);
    if (b.vel.length() > MAX_SPEED) b.vel.setLength(MAX_SPEED);
  });
}

Модель хвилі Герстнера зміщує вершини як вертикально, так і горизонтально, утворюючи характерні загострені гребені океанських хвиль. Реалізуйте її як вершинний шейдер GLSL за допомогою Three.js ShaderMaterial. Накладіть 4 хвильові поїзди з різними напрямками та частотами для реалістичної поверхні моря.

  • Вершинний шейдер обчислює зміщення Герстнера для кожної вершини сітки
  • Накладіть щонайменше 4 хвильові поїзди (змінюйте напрямок, частоту, амплітуду)
  • Фрагментний шейдер використовує нормалі для освітлення Фонга/Блінна з відбиттям неба
  • HUD: повзунок швидкості вітру, що масштабує амплітуди хвиль і крутість
Для хвилі з хвильовим числом k, напрямком D, амплітудою A, крутістю Q, швидкістю c:

x += Q * A * D.x * cos(k * dot(D, pos.xz) - c * t)
z += Q * A * D.y * cos(k * dot(D, pos.xz) - c * t)
y += A * sin(k * dot(D, pos.xz) - c * t)

Де Q — крутість (0 = синусоїдальні, 1 = гострі гребені).
Використовуйте ShaderMaterial.uniforms. Передайте масив vec4: { waveParams: { value: [ new THREE.Vector4(amp, freq, dir.x, dir.y), ... ] } }. У GLSL оголосіть uniform vec4 waveParams[4];. Також передавайте uniform float time;, що оновлюється щокадру.
Продиференціюйте зміщення Герстнера аналітично. Для кожної хвилі: N.x -= D.x * k * A * cos(phase), N.z -= D.y * k * A * cos(phase), N.y -= Q * k * A * sin(phase) (накопичуйте, потім почніть з N=(0,1,0) і змініть знак). Унормуйте результат.
Відкрити у Пісочниці ↗ Переглянути симуляцію Океану
⚠️ Спершу спробуйте написати код самостійно.
// Вершинний шейдер GLSL (спрощена одиночна хвиля)
vec3 gerstner(vec3 pos, vec2 D, float amp, float freq, float steep, float t) {
  float phase = freq * dot(D, pos.xz) - freq * 1.5 * t;
  return vec3(
    steep * amp * D.x * cos(phase),
    amp * sin(phase),
    steep * amp * D.y * cos(phase)
  );
}

// У main():
vec3 displaced = position;
for (int i = 0; i < 4; i++) {
  vec4 wp = waveParams[i]; // x=amp, y=freq, z=dirX, w=dirZ
  displaced += gerstner(position, vec2(wp.z, wp.w), wp.x, wp.y, 0.5, time);
}

Згладжена гідродинаміка частинок (SPH) розглядає рідину як хмару частинок, що діють одна на одну силами тиску та в'язкості. Реалізуйте формулювання SPH за Мюллером та ін. із 200 частинками у 2D, включно з оцінкою густини, силами тиску та в'язкості й граничними умовами на стінках.

  • Оцінка густини через ядро poly6: ρᵢ = Σ m · W_poly6(rᵢⱼ, h)
  • Сила тиску через ядро гострого градієнта (spiky)
  • Сила в'язкості через лапласіанове ядро
  • Прямокутна межа з демпфуванням (частинки відскакують від стінок)
  • Рендеринг інстансованими сферами, забарвленими за швидкістю
Три стандартні ядра з радіусом згладжування h:
Poly6 (густина): W = (315/(64π h⁹)) · (h²−r²)³ при r < h
Гострий градієнт (spiky) (тиск): ∇W = −(45/(π h⁶)) · (h−r)² · r̂
Лапласіан в'язкості: ∇²W = (45/(π h⁶)) · (h−r)
Використовуйте рівняння стану ідеального газу: P = GAS_CONST * (density - REST_DENSITY). REST_DENSITY ≈ 1000 кг/м³, GAS_CONST ≈ 2000. Сила тиску на частинку i з боку j: F = −m · (Pᵢ+Pⱼ)/(2·ρⱼ) · ∇W_spiky.
Наївний пошук сусідів O(N²) працює для 200 частинок при 60fps. Для більшої кількості частинок побудуйте просторову геш-сітку: поділіть простір на комірки розміром h. Кожна частинка перевіряє лише свої 9 сусідніх комірок замість усіх N частинок.
Зазвичай це означає, що ваш крок за часом надто великий. Обмежте dt до 0.005с. Також додайте обмеження максимального прискорення: якщо force.length() > MAX_FORCE, зменшіть її. Переконайтеся, що ви пропускаєте самодію (i !== j) у циклі сил.
Відкрити у Пісочниці ↗ Переглянути симуляцію Рідини
⚠️ Повна реалізація SPH — це 150+ рядків; підказки вище містять усі ключові рівняння. Вивчіть вихідний код симуляції Рідини для повного зразка.

Змоделюйте N гравітаційних тіл за допомогою інтегратора «жабка» / Velocity-Verlet, який зберігає енергію значно краще за інтегрування Ейлера. Додайте параметр пом'якшення, щоб запобігти сингулярностям при близькому зближенні. Візуалізуйте орбітальні сліди та вимірюйте загальний дрейф енергії в часі.

  • Обчисліть гравітаційне прискорення: a = G·m/|r|² (з пом'якшенням ε)
  • Інтегрування «жабка»: схема поштовх–дрейф–поштовх
  • Рендеринг слідів за допомогою кільцевих буферів THREE.Line
  • HUD: загальна кінетична + потенціальна енергія в реальному часі
  • Заготовки: Сонячна система, подвійна зоря, орбіта-вісімка
«Жабка» — симплектичний метод: він точно зберігає модифіковану енергію, тож орбіти не закручуються всередину чи назовні з часом. Форма «поштовх-дрейф-поштовх» така:

v_half = v + (a/2) * dt
pos += v_half * dt
a_new = computeAccel(pos)
v = v_half + (a_new/2) * dt
Додайте довжину пом'якшення ε² до знаменника: a = G * m * r / (|r|² + ε²)^(3/2). Типове пом'якшення — 0.1–1% від середньої відстані між частинками. Це запобігає нескінченним силам, коли два тіла дуже зближуються.
Заздалегідь виділіть Float32Array довжиною TRAIL_LENGTH * 3 для кожного тіла. Щокадру зсувайте буфер ліворуч на одну вершину (або використовуйте циклічний індекс). Запишіть поточну позицію за активним індексом, потім встановіть geometry.attributes.position.needsUpdate = true.
Відкрити у Пісочниці ↗ Переглянути симуляцію N тіл
⚠️ Повна система N тіл зі слідами — це 200+ рядків. Підказки містять усі ключові рівняння. Вивчіть вихідний код симуляції N тіл для повного зразка.