Powrót do strony głównej

Renderowanie 3D-mieszkań w przeglądarce na Three.js

Artykuł opisuje implementację przeglądarkowego renderowania 3D-mieszkań z proceduralnymi meblami na Next.js i Three.js. Omówiono architekturę scen, oświetlenie, multiplayer na Socket.io, optymalizacje i gamifikację. Nadaje się dla middle/senior deweloperów.

3D-mieszkania w przeglądarce: Three.js + proceduralne meble
Advertisement 728x90

Proceduralne renderowanie 3D mieszkań w przeglądarce: Next.js, Three.js i WebSocket multiplayer

W projekcie Przykolnia zaimplementowano renderowanie pikselowych mieszkań 3D dla gier multiplayer bez zewnętrznych modeli. Wykorzystano Next.js 16 z React Three Fiber, proceduralną generację mebli z prymitywów, tekstury Canvas dla treści oraz Socket.io do synchronizacji pozycji. Sceny z 300–800 obiektami działają płynnie na nowoczesnych urządzeniach.

Architektura scen i szablony mieszkań

Mieszkania budowane są ze stref (pomieszczeń) ze wspólnymi ścianami i drzwiami. Szablony definiowane są danymi bez sztywnego kodu:

type RoomDef = {
  id: string;
  name: string;
  x: number; z: number;
  width: number; depth: number;
  isSecret?: boolean;
};

type WallDef = {
  x1: number; z1: number;
  x2: number; z2: number;
  hasDoor?: boolean;
  doorOffset?: number;
};

const LOFT: ApartmentTemplate = {
  id: "loft",
  rooms: [
    { id: "lounge", name: "Lounge", x: 0, z: 0, width: 12, depth: 8 },
    { id: "gaming", name: "Pokój gier", x: 12, z: 0, width: 10, depth: 8 },
    { id: "bar", name: "Bar", x: 0, z: 8, width: 10, depth: 6 },
    { id: "studio", name: "Studio", x: 10, z: 8, width: 12, depth: 6 },
  ],
  walls: [
    { x1: 0, z1: 0, x2: 22, z2: 0 },
    { x1: 22, z1: 0, x2: 22, z2: 14 },
    { x1: 12, z1: 0, x2: 12, z2: 8, hasDoor: true },
  ],
};

Szablony mieszczą się od studia (12x10) do penthouse'u (24x22). Renderowanie jest deklaratywne: dane przekształcane są w obiekty Three.js.

Google AdInline article slot

Oświetlenie: warstwy i presety nastroju

Scena oświetlana jest trzema warstwami:

  • Ambient — podstawowe, zależne od pory dnia (rano: #ffd4a0, intensywność 1.1; noc: #1a2244, 0.4).
  • Directional — główne z cieniami + wypełniające.
  • Point lights — po jednym na pomieszczenie na wysokości 2.3 jednostek.

Dodano 8 presetów nastroju. Party: sufit #ff2288 (1.8), ambient #110022 (0.2), akcent #00ffff (3.0). Pełna transformacja bez zmiany geometrii.

Proceduralne meble z prymitywów

Meble składane są z boxGeometry i cylinderGeometry. Zalety: brak ładowania, natychmiastowa generacja, prostota opisów AI.

Google AdInline article slot

Przykład automatu do gier:

function ArcadeModel({ color, accent }: { color: string; accent: string }) {
  return (
    <group>
      <Block args={[0.7, 1.6, 0.7]} position={[0, 0.8, 0]} color={color} />
      <Block args={[0.5, 0.35, 0.05]} position={[0, 1.2, 0.33]}
             color="#0a0a2a" emissive={accent} emissiveIntensity={0.3} />
      <Block args={[0.5, 0.05, 0.25]} position={[0, 0.85, 0.25]}
             color="#1a1a2e" rotation={[-0.3, 0, 0]} />
      <mesh position={[-0.1, 0.92, 0.25]}>
        <cylinderGeometry args={[0.02, 0.02, 0.12]} />
        <meshStandardMaterial color="#ff2e63" />
      </mesh>
      {[0.05, 0.15].map((x) => (
        <mesh key={x} position={[x, 0.9, 0.2]}>
          <cylinderGeometry args={[0.025, 0.025, 0.02]} />
          <meshStandardMaterial color={accent} emissive={accent}
                                emissiveIntensity={0.5} />
        </mesh>
      ))}
    </group>
  );
}

Scena: 300–800 obiektów. AI generuje JSON z prymitywami dla wizytówek marki.

Treść na ścianach przez CanvasTexture

Plakaty renderowane dynamicznie:

Google AdInline article slot
  • Canvas 256x170.
  • Tło #1a1a2e, ramka kolorem autora, zawijanie tekstu.
  • THREE.CanvasTexture z NearestFilter.
  • meshStandardMaterial z emissiveIntensity: 0.05.
const canvas = document.createElement("canvas");
canvas.width = 256;
canvas.height = 170;
const ctx = canvas.getContext("2d")!;
ctx.fillStyle = "#1a1a2e";
ctx.strokeStyle = authorColor;
// zawijanie tekstu, rysowanie

const texture = new THREE.CanvasTexture(canvas);
texture.minFilter = THREE.NearestFilter;

3 sloty na ścianę (0.25, 0.5, 0.75), wysokość 1.3 jednostki. Obsługa obrazów, wideo, audio.

Pikselowe awatary i pupile

Awatary — tekstura Canvas na billboardzie. Warstwy: ciało (5 odcieni), oczy, spodnie (6), ubranie (10), włosy (12), akcesoria (10). Skala 16x24 → 128x192, NearestFilter.

Pupile interpolowane z lerp 0.05 (blisko) / 0.15 (daleko).

Multiplayer z interpolacją

Socket.io broadcast'uje pozycje (~20 FPS):

useFrame(() => {
  const now = Date.now();
  if (now - lastRef.current.t < 50) return;
  const dx = Math.abs(pos.x - lastRef.current.x);
  const dz = Math.abs(pos.z - lastRef.current.z);
  if (dx < 0.05 && dz < 0.05) return;

  socketRef.current?.volatile.emit("position", { x: pos.x, z: pos.z });
});

Interpolacja: position.x += (target.x - position.x) * 0.15 (15% na klatkę).

volatile.emit odrzuca przestarzałe pakiety.

Pogoda i efekty

Systemy cząsteczek na BufferGeometry:

  • Deszcz: 600 cząstek, lineSegments.
  • Śnieg: 400, points ze znoszeniem.
  • Gwiazdy: 200, migotanie po Y.
useFrame(() => {
  for (let i = 0; i < count; i++) {
    positions[i * 3 + 1] -= velocities[i];
    positions[i * 3] += Math.sin(Date.now() * 0.0008 + i * 0.7) * 0.004;
    if (positions[i * 3 + 1] < -0.5) {
      positions[i * 3 + 1] = 7 + Math.random() * 2;
    }
  }
  posAttr.needsUpdate = true;
});

Drzwi i animacje

Otwarcie o 81° (π*0.45) przez dot product normalnej ściany:

const wallNormal = { x: -(z2 - z1), z: x2 - x1 };
const toPlayer = { x: playerX - doorX, z: playerZ - doorZ };
const dot = wallNormal.x * toPlayer.x + wallNormal.z * toPlayer.z;
const direction = dot > 0 ? 1 : -1;
currentAngle += (targetAngle * direction - currentAngle) * 0.1;

Optymalizacje wydajności

| Rozwiązanie | Efekt |

|---------|--------|

| dynamic(() => import("./Apartment3D"), { ssr: false }) | Three.js leniwie, ~150 KB gzip |

| antialias: false | -15-20% fillrate |

| dpr: Math.min(devicePixelRatio, 2) | Oszczędność 2-4x na HiDPI |

| NearestFilter | Bez filtrowania trójliniowego |

| castShadow tylko meble | Cienie selektywnie |

| receiveShadow tylko podłoga | Jeden receiver |

Bundle 3D: ~600 KB raw, tree-shaken Three.js.

Grywalizacja i plany

  • Ekonomia: monety za posty, limit 100/dzień.
  • Poziomy: 10, do 3500 XP.
  • Rosnące budynki: 1-6 pięter według treści.
  • Eventy: 8/rok, x3 XP.
  • Tajne pokoje: 10 po kodach.

Dalej: AI-meble, trofea, instancing/LOD.

Co jest ważne

  • Proceduralne meble z 4-15 prymitywów eliminują ładowanie glTF.
  • Socket.io z volatile.emit i 15% lerp zapewnia płynny multiplayer.
  • CanvasTexture + NearestFilter dla pikselowego stylu treści i awatarów.
  • Optymalizacje redukują bundle do 150 KB gzip przy 300-800 obiektach.
  • Deklaratywne szablony pozwalają na 5 wariantów mieszkań bez kodu.

— Editorial Team

Advertisement 728x90

Czytaj dalej