🎯

Wyzwania Programistyczne

Rozwijaj swoje umiejętności WebGL i symulacyjne poprzez praktyczne zadania. Każde wyzwanie zawiera szkielet startowy, stopniowe wskazówki i odblokowywalne rozwiązanie referencyjne.

Twój postęp 0 / 6 ukończonych
2Łatwe wyzwania
2Średnie wyzwania
2Trudne wyzwania
~12hŁączny czas nauki

Wszystkie Wyzwania

Zaczynając od kuli unoszącej się w powietrzu, dodaj siłę grawitacji, która przyspiesza piłkę w dół w każdej klatce. Gdy uderzy w podłogę, zastosuj odbicie pochłaniające energię, aby ostatecznie się zatrzymała. Poznaj całkowanie Eulera, akumulację prędkości i współczynnik restytucji.

  • Piłka spada z zastosowanym vy += g * dt w każdej klatce
  • Piłka odbija się od podłogi z vy *= -restitution
  • Piłka zwalnia i zatrzymuje się (restitution < 1)
  • Sterowanie: suwak siły grawitacji (0 → 30 m/s²)
// Three.js r160 jest już wczytane
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);

// Piłka
const ball = new THREE.Mesh(
  new THREE.SphereGeometry(0.5, 32, 32),
  new THREE.MeshStandardMaterial({ color: 0x3b82f6 })
);
ball.position.y = 5;
scene.add(ball);

// Podłoga
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: dodaj tutaj fizykę grawitacji
let vy = 0;
const GRAVITY = 9.8;        // m/s²
const RESTITUTION = 0.7;    // odbojność
const FLOOR_Y = 0.5;        // promień piłki

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

  // ← Twój kod tutaj

  renderer.render(scene, camera);
}
animate();
W każdej klatce dodaj grawitację do prędkości pionowej: vy -= GRAVITY * dt. Następnie przesuń piłkę: ball.position.y += vy * dt. Znak minus sprawia, że grawitacja ciągnie w dół (ujemne Y).
Po przesunięciu piłki sprawdź if (ball.position.y < FLOOR_Y). Jeśli prawda, przypnij ją z powrotem: ball.position.y = FLOOR_Y, następnie odwróć i osłab prędkość: vy = Math.abs(vy) * RESTITUTION. Jeśli vy < 0.1, ustaw ją na 0, aby piłka naprawdę się zatrzymała.
Dodaj nakładkę <input type="range" min="0" max="30" value="9.8"> na canvasie. Odczytaj jej wartość w pętli animacji jako const g = parseFloat(slider.value). Nie zapomnij o <label> dla dostępności.
Otwórz w Playground ↗
⚠️ Spróbuj najpierw sam napisać kod — więcej się nauczysz.
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;
}

Stwórz płynną animowaną falę sinusoidalną za pomocą linii THREE.BufferGeometry. Fala powinna „podróżować” w czasie (przesunięcie fazowe co klatkę) i reagować na suwaki amplitudy, częstotliwości i prędkości. To podstawa wielu wizualizacji fizyki fal.

  • Renderuj gładką linię za pomocą THREE.Line z BufferGeometry
  • Animuj falę: y = A * sin(f*x - speed*t)
  • Aktualizuj pozycje wierzchołków co klatkę (ponownie wykorzystuj bufor — bez ponownej alokacji)
  • Suwaki HUD: Amplituda (0.1 → 3), Częstotliwość (0.5 → 10), Prędkość (0 → 5)
const NUM_POINTS = 512;
const X_RANGE = 20; // fala rozciąga się od -10 do +10

// Zbuduj geometrię raz
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: wypełnij positions[] danymi fali sinusoidalnej
  // x biegnie od -X_RANGE/2 do +X_RANGE/2
  // y = amplitude * Math.sin(frequency * x - speed * t)
  // z = 0
  geometry.attributes.position.needsUpdate = true;
}
Zapętl i od 0 do NUM_POINTS. Oblicz x = (i / (NUM_POINTS - 1)) * X_RANGE - X_RANGE / 2 aby rozłożyć równomiernie od -10 do +10. Zapisz jako positions[i*3] = x.
Zwiększaj licznik czasu co klatkę: t += dt. Użyj go w sinusie: y = A * Math.sin(freq * x - spd * t). Wyraz - spd * t tworzy ruch w prawo.
Zwiększ NUM_POINTS (spróbuj 512+). Sprawdź też, czy ustawiasz geometry.attributes.position.needsUpdate = true co klatkę, w przeciwnym razie Three.js nie prześle ponownie bufora.
Otwórz w Playground ↗
⚠️ Spróbuj najpierw sam napisać kod.
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;
}

Zaimplementuj algorytm Boidów Craiga Reynoldsa: 100 agentów, które łączą się w stado dzięki tylko trzem lokalnym regułom. Każdy boid patrzy na sąsiadów w promieniu percepcji i steruje, aby się odseparować, wyrównać i skupić. Emergentne zachowanie stadne pojawia się bez globalnej koordynacji.

  • Separacja: steruj z dala od zbyt bliskich boidów
  • Wyrównanie: steruj w kierunku średniego kursu pobliskich boidów
  • Kohezja: steruj w kierunku średniej pozycji pobliskich boidów
  • Granice zawijające (boidy teleportują się przez krawędzie)
  • Suwaki HUD dla promienia percepcji i 3 wag reguł
Każdy boid potrzebuje: { pos: THREE.Vector3, vel: THREE.Vector3 }. Trzymaj płaską tablicę N boidów. Ogranicz prędkość co klatkę: if (vel.length() > MAX_SPEED) vel.setLength(MAX_SPEED).
Dla każdego boida A, zapętl wszystkie boidy B. Jeśli A.pos.distanceTo(B.pos) < PERCEPTION (i B ≠ A), B jest sąsiadem. Zapisz pozycje i prędkości sąsiadów w tymczasowych tablicach, następnie uśrednij je dla kohezji i wyrównania.
Dla każdego bliskiego sąsiada B, oblicz wektor away = A.pos.clone().sub(B.pos). Przeskaluj go odwrotnie proporcjonalnie do odległości: away.divideScalar(dist * dist). Zsumuj wszystkie te wektory, następnie dodaj steerSeparation.multiplyScalar(wSeparation) do prędkości.
Sprawdź, czy ograniczasz prędkość po zsumowaniu wszystkich trzech reguł. Upewnij się też, że strefa separacji jest mniejsza niż promień percepcji. Typowa konfiguracja: percepcja = 50, separacja = 25. Dostrój wagi: separacja > wyrównanie > kohezja zwykle działa najlepiej.
Otwórz w Playground ↗ Zobacz pełną symulację
⚠️ Spróbuj najpierw sam napisać kod.
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);
  });
}

Model fali Gerstnera przesuwa wierzchołki zarówno w pionie, jak i w poziomie, tworząc charakterystyczne szczytowe grzbiety fal oceanicznych. Zaimplementuj go jako vertex shader GLSL za pomocą Three.js ShaderMaterial. Nałóż na siebie 4 pociągi fal o różnych kierunkach i częstotliwościach dla realistycznej powierzchni morza.

  • Vertex shader oblicza przemieszczenie Gerstnera dla każdego wierzchołka siatki
  • Nałóż co najmniej 4 pociągi fal (różny kierunek, częstotliwość, amplituda)
  • Fragment shader wykorzystuje normalne do oświetlenia Phong/Blinn z odbiciem nieba
  • HUD: suwak siły wiatru skalujący amplitudy i stromość fal
Dla fali z liczbą falową k, kierunkiem D, amplitudą A, stromością Q, prędkością 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)

Gdzie Q to stromość (0=sinusoidalna, 1=ostre grzbiety).
Użyj ShaderMaterial.uniforms. Przekaż tablicę wektorów vec4: { waveParams: { value: [ new THREE.Vector4(amp, freq, dir.x, dir.y), ... ] } }. W GLSL zadeklaruj uniform vec4 waveParams[4];. Przekaż też uniform float time; aktualizowaną co klatkę.
Zróżniczkuj analitycznie przemieszczenie Gerstnera. Dla każdej fali: N.x -= D.x * k * A * cos(phase), N.z -= D.y * k * A * cos(phase), N.y -= Q * k * A * sin(phase) (akumulowane, zacznij od N=(0,1,0) i zanegowanego wyniku). Znormalizuj wynik.
Otwórz w Playground ↗ Zobacz symulację Ocean
⚠️ Spróbuj najpierw sam napisać kod.
// GLSL Vertex Shader (uproszczona pojedyncza fala)
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)
  );
}

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

Smoothed Particle Hydrodynamics (SPH) traktuje płyn jako chmurę cząstek, które wywierają na siebie ciśnienie i lepkość. Zaimplementuj formułę SPH Müllera i in. ze 200 cząstkami w 2D, w tym estymację gęstości, siły ciśnienia i lepkości oraz warunki brzegowe ścian.

  • Estymacja gęstości przez jądro poly6: ρᵢ = Σ m · W_poly6(rᵢⱼ, h)
  • Siła ciśnienia przez gradient jądra spiky
  • Siła lepkości przez laplasjan jądra
  • Ściany pudełka z tłumieniem (cząstki odbijają się od ścian)
  • Renderowanie przy użyciu instancjonowanych kul kolorowanych wg prędkości
Trzy standardowe jądra z promieniem wygładzania h:
Poly6 (gęstość): W = (315/(64π h⁹)) · (h²−r²)³ gdy r < h
Gradient spiky (ciśnienie): ∇W = −(45/(π h⁶)) · (h−r)² · r̂
Laplasjan lepkości: ∇²W = (45/(π h⁶)) · (h−r)
Użyj równania stanu gazu doskonałego: P = GAS_CONST * (density - REST_DENSITY). REST_DENSITY ≈ 1000 kg/m³, GAS_CONST ≈ 2000. Siła ciśnienia na cząstkę i od j: F = −m · (Pᵢ+Pⱼ)/(2·ρⱼ) · ∇W_spiky.
Naiwne wyszukiwanie sąsiadów O(N²) działa dla 200 cząstek przy 60fps. Dla większej liczby cząstek zbuduj siatkę haszującą przestrzennie: podziel przestrzeń na komórki o rozmiarze h. Każda cząstka sprawdza tylko 9 sąsiednich komórek zamiast wszystkich N cząstek.
Zwykle oznacza to, że twój krok czasowy jest zbyt duży. Ogranicz dt do 0.005s. Dodaj też maksymalne ograniczenie przyspieszenia: jeśli force.length() > MAX_FORCE, przeskaluj je w dół. Upewnij się, że pomijasz interakcję ze sobą (i !== j) w pętli sił.
Otwórz w Playground ↗ Zobacz symulację Fluid
⚠️ Pełna implementacja SPH to ponad 150 linii — powyższe wskazówki zawierają wszystkie kluczowe równania. Zbadaj źródło symulacji Fluid jako pełną referencję.

Symuluj N ciał grawitacyjnych za pomocą integratora Leapfrog / Velocity-Verlet, który zachowuje energię znacznie lepiej niż całkowanie Eulera. Dodaj parametr wygładzania, aby zapobiec osobliwościom przy bliskim zbliżeniu. Wizualizuj ślady orbitalne i mierz dryf całkowitej energii w czasie.

  • Oblicz przyspieszenie grawitacyjne: a = G·m/|r|² (z wygładzaniem ε)
  • Całkowanie Leapfrog: schemat kick–drift–kick
  • Renderowanie śladów za pomocą kołowych buforów THREE.Line
  • HUD: całkowita energia kinetyczna + potencjalna wyświetlana w czasie rzeczywistym
  • Ustawienia gotowe: układ słoneczny, gwiazda podwójna, orbita ósemkowa
Leapfrog jest symplektyczny — zachowuje zmodyfikowaną energię dokładnie, więc orbity nie spiralują się do wewnątrz ani na zewnątrz przez długi czas. Forma „kick-drift-kick” to:

v_half = v + (a/2) * dt
pos += v_half * dt
a_new = computeAccel(pos)
v = v_half + (a_new/2) * dt
Dodaj długość wygładzania ε² do mianownika: a = G * m * r / (|r|² + ε²)^(3/2). Typowe wygładzanie to 0,1–1% średniej odległości między cząstkami. Zapobiega to nieskończonym siłom, gdy dwa ciała zbliżą się bardzo blisko.
Wstępnie zaalokuj Float32Array o długości TRAIL_LENGTH * 3 dla każdego ciała. Co klatkę przesuwaj bufor w lewo o jeden wierzchołek (lub użyj indeksu kołowego). Zapisz bieżącą pozycję pod aktywnym indeksem, następnie ustaw geometry.attributes.position.needsUpdate = true.
Otwórz w Playground ↗ Zobacz symulację N-Body
⚠️ Pełny system N-ciał ze śladami to ponad 200 linii. Wskazówki zawierają wszystkie kluczowe równania. Zbadaj źródło symulacji N-Body jako pełną referencję.