Generatywne kwiaty Fibonacciego w Three.js

W tym tutorialu stworzymy proceduralny generator organicznych kwiatów w Three.js - inspirowany strukturami biologicznymi, spiralami Fibonacciego i nowoczesnym generative art.

Efekt końcowy pozwala:

  • generować płynne organiczne formy,
  • zmieniać liczbę płatków,
  • sterować deformacją,
  • używać gradientów kolorystycznych,
  • obracać i przybliżać model myszą,
  • korzystać z gotowych presetów.
Generatywne kwiaty Fibonacciego w Three.js

Najważniejsze jest jednak to, że po drodze dokładnie omówimy matematykę i sposób działania całego algorytmu.

Przygotowanie sceny Three.js

Zaczynamy od klasycznej konfiguracji Three.js.

Import bibliotek

Najpierw importujemy Three.js, OrbitControls oraz bibliotekę GUI.

import * as THREE from "three";

import { OrbitControls }
from "three/examples/jsm/controls/OrbitControls.js";

import GUI
from "https://cdn.jsdelivr.net/npm/[email protected]/+esm";

OrbitControls pozwoli obracać scenę myszą, a lil-gui stworzy panel parametrów.

Tworzenie sceny

let scene = new THREE.Scene();

scene.background = new THREE.Color(0xf6f6f6);

Scena jest kontenerem na wszystkie obiekty 3D.

Dodatkowo ustawiamy jasne tło, dzięki któremu kolory kwiatu będą dobrze widoczne.

Kamera

let camera = new THREE.PerspectiveCamera(
    45,
    innerWidth / innerHeight,
    0.1,
    100
);

camera.position.set(0, 1.5, 4);

Tutaj definiujemy kamerę perspektywiczną.

Parametry oznaczają:

  • 45 - kąt widzenia,
  • 0.1 - minimalna odległość renderowania,
  • 100 - maksymalna odległość renderowania.

Kamera zostaje odsunięta od środka sceny, aby cały kwiat był widoczny.

Renderer WebGL

let renderer = new THREE.WebGLRenderer({
    antialias: true
});

renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(devicePixelRatio);
document.body.appendChild(renderer.domElement);

Renderer odpowiada za rysowanie grafiki.

Opcja antialias wygładza krawędzie, co ma duże znaczenie przy organicznych kształtach.

Sterowanie myszą - OrbitControls

let controls = new OrbitControls(
    camera,
    renderer.domElement
);

controls.enableDamping = true;

Dzięki temu użytkownik może przybliżać, oddalać, obracać oraz przesuwać scenę. enableDamping dodaje płynności ruchu kamery.

Oświetlenie sceny

Dodajemy dwa źródła światła - ambient oraz kierunkowe.

Światło ambient

scene.add(
    new THREE.AmbientLight(
        0xffffff,
        0.7
    )
);

To miękkie światło rozjaśniające całą scenę.

Światło kierunkowe

let dir = new THREE.DirectionalLight(
    0xffffff,
    1.2
);

dir.position.set(2, 3, 3);
scene.add(dir);

To światło działa trochę jak słońce - ma konkretny kierunek padania.

Parametry generatora

Naturalne formy nigdy nie są idealne matematycznie. Dlatego dodajemy delikatne zakłócenia chociaż możemy to wyłączyć.

function noise(x, y){
    return (
        Math.sin(x * 2.3 + y * 3.1) * 0.5 +
        Math.sin(x * 5.7 - y * 1.9) * 0.25
    );
}

To bardzo prosty proceduralny noise oparty o funkcje sinusoidalne.

Generowanie geometrii

Najważniejsza część programu to funkcja createFlower(). To właśnie tutaj budujemy cały model.

function createFlower() {
    // tutaj generujemy geometrię
}

Tworzenie BufferGeometry

let geo = new THREE.BufferGeometry();

BufferGeometry to najwydajniejszy sposób tworzenia własnych modeli w Three.js.

Musimy ręcznie zdefiniować:

  • pozycje wierzchołków,
  • kolory,
  • normalne,
  • indeksy trójkątów.

Tablice geometrii

let vertices = [];
let normals = [];
let colors = [];
let indices = [];

Każda z tych tablic pełni inną funkcję.

Siatka punktów

Generator działa na siatce parametrów u i v.

for(let j = 0; j <= heightSegments; j++) {
    let v = j / heightSegments;
    for(let i = 0; i <= radialSegments; i++) {
        let u = i / radialSegments;
    }
}

Można to traktować jak współrzędne na powierzchni. u odpowiada za obrót wokół środka, a v za odległość od centrum.

Tworzenie fal płatków

Najważniejszy fragment matematyczny:

let petalWave = Math.pow(
    0.5 + 0.5 *
    Math.sin(angle * params.petals),
    params.softness
);

To właśnie tutaj rodzą się płatki.

Jak działa ten wzór?

Krok 1 - sinusoida

Math.sin(angle * params.petals)

Tworzy falę wokół okręgu.

Jeśli petals = 8, otrzymamy osiem szczytów.

Krok 2 - zmiana zakresu

Sinus zwraca wartości od -1 do 1. Dlatego przesuwamy zakres:

0.5 + 0.5 * Math.sin(...)

Teraz wartości mieszczą się w zakresie od 0 do 1.

Krok 3 - wygładzanie

Math.pow(..., params.softness)

To niezwykle ważny trik.

Potęgowanie powoduje:

  • miękkie przejścia,
  • bardziej organiczny profil,
  • naturalniejsze płatki.

Dodawanie deformacji

let flowNoise =
    noise(
        angle + v * params.flow,
        v * 2
    )
    * params.noiseAmp;

To tworzy lokalne zakłócenia powierzchni. Dzięki temu kwiat nie wygląda idealnie symetrycznie. Oczywiście można z tego zrezygnować i uzyskać idealną symetrię.

Obliczanie promienia

let r =
    v +
    petalWave *
    params.amplitude *
    v +
    flowNoise;

Promień każdego punktu zależy od:

  • odległości od środka,
  • wysokości płatka,
  • deformacji noise.

Skręcanie płatków

let bendFactor = Math.pow(
    v,
    params.bendPower
);

let bendAngle =
    angle +
    bendFactor *
    params.bend;

Im dalej od środka tym mocniejsze skręcenie.

Współrzędne 3D

let x =
    Math.cos(bendAngle) * r;
let z =
    Math.sin(bendAngle) * r;

To klasyczna konwersja: promień + kąt → współrzędne kartezjańskie.

Dodawanie wysokości

let y =
    Math.pow(v, 1.2) *
    0.85 *
    (1 + petalWave * 0.35);

To nadaje płatkom objętość.

Gradient kolorystyczny

let c = colA.clone().lerp(
    colB,
    v
);

Kolory są płynnie interpolowane od środka ku końcówkom płatków.

Tworzenie trójkątów

indices.push(a, b, d);
indices.push(b, c, d);

W ten sposób punkty zostają połączone w pełną siatkę geometryczną.

Materiał fizyczny

let material = new THREE.MeshPhysicalMaterial({
    vertexColors: true,
    roughness: 0.55,
    transmission: 0.25,
    thickness: 0.7,
    transparent: true,
    opacity: 0.96,
    side: THREE.DoubleSide
});

To właśnie materiał odpowiada za:

  • miękkie światło,
  • półprzezroczystość,
  • organiczny wygląd.

GUI - interaktywny edytor

gui.add(
    params,
    "petals",
    3,
    120,
    1
).onChange(createFlower);

Każdy slider zmienia parametr i automatycznie przebudowuje geometrię.

Animacja obrotu

if (params.rotate && flowerMesh) {
    flowerMesh.rotation.y += 0.002;
}

Subtelny ruch bardzo poprawia odbiór modelu.

Pętla renderująca

function animate() {
    requestAnimationFrame(animate);
    controls.update();
    renderer.render(scene, camera);
}

To serce każdej aplikacji realtime.

Najciekawsze w tym projekcie jest to, że organiczne struktury powstają z kilku prostych funkcji matematycznych.