Back to Home

Rendering 3D apartments in the browser on Three.js

The article describes the implementation of browser rendering of 3D apartments with procedural furniture on Next.js and Three.js. It covers scene architecture, lighting, multiplayer on Socket.io, optimizations, and gamification. Suitable for middle/senior developers.

3D apartments in the browser: Three.js + procedural furniture
Advertisement 728x90

Procedural 3D Apartment Rendering in the Browser: Next.js, Three.js, and WebSocket Multiplayer

In the Prikolnya project, pixelated 3D apartments are rendered for multiplayer arcades without external models. It uses Next.js 16 with React Three Fiber, procedural furniture generation from primitives, Canvas textures for content, and Socket.io for position synchronization. Scenes with 300–800 objects run smoothly on modern devices.

Scene Architecture and Apartment Templates

Apartments are built from zones (rooms) with shared walls and doors. Templates are defined by data without hardcoded logic:

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: "Gaming Room", 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 },
  ],
};

Templates range from a studio (12x10) to a penthouse (24x22). Rendering is declarative: data is transformed into Three.js objects.

Google AdInline article slot

Lighting: Layers and Mood Presets

The scene is lit with three layers:

  • Ambient — base layer, depends on time of day (morning: #ffd4a0, intensity 1.1; night: #1a2244, 0.4).
  • Directional — main light with shadows + fill light.
  • Point lights — one per room at a height of 2.3 units.

Eight mood presets are added. Party: ceiling #ff2288 (1.8), ambient #110022 (0.2), accent #00ffff (3.0). Complete transformation without changing geometry.

Procedural Furniture from Primitives

Furniture is assembled from boxGeometry and cylinderGeometry. Advantages: no downloads, instant generation, simplicity for AI descriptions.

Google AdInline article slot

Example arcade machine:

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

Scene: 300–800 objects. AI generates JSON with primitives for brand showcases.

Wall Content via CanvasTexture

Posters are rendered dynamically:

Google AdInline article slot
  • Canvas 256x170.
  • Background #1a1a2e, frame in author's color, word-wrapped text.
  • THREE.CanvasTexture with NearestFilter.
  • meshStandardMaterial with 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;
// word wrap, drawing

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

3 slots per wall (0.25, 0.5, 0.75), height 1.3 units. Supports images, video, audio.

Pixel Avatars and Pets

Avatars are Canvas textures on a billboard. Layers: body (5 tones), eyes, pants (6), clothing (10), hair (12), accessories (10). Scale 16x24 → 128x192, NearestFilter.

Pets are interpolated with lerp 0.05 (close) / 0.15 (far).

Multiplayer with Interpolation

Socket.io broadcasts positions (~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 });
});

Interpolation: position.x += (target.x - position.x) * 0.15 (15% per frame).

volatile.emit drops outdated packets.

Weather and Effects

Particle systems on BufferGeometry:

  • Rain: 600 particles, lineSegments.
  • Snow: 400 particles, points with drift.
  • Stars: 200 particles, flickering on 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;
});

Doors and Animations

Opening to 81° (π*0.45) based on dot product of wall normal:

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;

Performance Optimizations

| Solution | Effect |

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

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

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

| dpr: Math.min(devicePixelRatio, 2) | Saves 2-4x on HiDPI |

| NearestFilter | No trilinear filtering |

| castShadow only on furniture | Shadows selectively |

| receiveShadow only on floor | One receiver |

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

Gamification and Plans

  • Economy: coins for posts, limit 100/day.
  • Levels: 10, up to 3500 XP.
  • Growing buildings: 1-6 floors based on content.
  • Events: 8/year, x3 XP.
  • Secret rooms: 10 via codes.

Next: AI furniture, trophies, instancing/LOD.

Key Takeaways

  • Procedural furniture from 4-15 primitives eliminates glTF downloads.
  • Socket.io with volatile.emit and 15% lerp ensures smooth multiplayer.
  • CanvasTexture + NearestFilter for pixel-style content and avatars.
  • Optimizations reduce bundle to 150 KB gzip with 300-800 objects.
  • Declarative templates allow 5 apartment variants without code.

— Editorial Team

Advertisement 728x90

Read Next