---
title: 3d Model Viewer
description: An interactive 3D model viewer component for a variety of file formats including GLTF, GLB, FBX, and OBJ. It features dynamic lighting, environmental presets, and full user control over rotation and zoom. It's a versatile component for displaying 3D assets in web applications.
---

<ComponentPreview name="3d-model-viewer-demo" />

## Installation

<Tabs defaultValue="cli">

<TabsList>
  <TabsTrigger value="cli">CLI</TabsTrigger>
  <TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>
<TabsContent value="cli">

```bash
npx lightswind@latest add 3d-model-viewer
```

</TabsContent>

<TabsContent value="manual">

<Steps>

<Step>Copy and paste the following code into your project.</Step>

```tsx
import { FC, Suspense, useRef, useLayoutEffect, useEffect } from "react";
import { Canvas, useFrame, useLoader } from "@react-three/fiber";
import { OrbitControls, useGLTF, useFBX, Environment, ContactShadows, Center } from "@react-three/drei";
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader.js";
import * as THREE from "three";

// ---
// Types and Constants
// ---

export interface ViewerProps {
  url: string;
  width?: number | string;
  height?: number | string;
  defaultZoom?: number;
  minZoomDistance?: number;
  maxZoomDistance?: number;
  enableManualRotation?: boolean;
  enableManualZoom?: boolean;
  ambientIntensity?: number;
  keyLightIntensity?: number;
  fillLightIntensity?: number;
  rimLightIntensity?: number;
  environmentPreset?:
    | "city"
    | "sunset"
    | "night"
    | "dawn"
    | "studio"
    | "apartment"
    | "forest"
    | "park"
    | "none";
  autoRotate?: boolean;
  autoRotateSpeed?: number;
  onModelLoaded?: () => void;
}

const isTouch =
  typeof window !== "undefined" &&
  ("ontouchstart" in window || navigator.maxTouchPoints > 0);

const deg2rad = (d: number) => (d * Math.PI) / 180;

// ---
// Reusable Components
// ---

const Loader: FC = () => {
  return null;
};

// Component for handling GLTF/GLB models
const GltfContent: FC<{ url: string; onLoaded: () => void }> = ({
  url,
  onLoaded,
}) => {
  const { scene } = useGLTF(url);
  useLayoutEffect(() => {
    if (scene) {
      scene.traverse((o) => {
        if ((o as THREE.Mesh).isMesh) {
          o.castShadow = true;
          o.receiveShadow = true;
        }
      });
      onLoaded();
    }
  }, [scene, onLoaded]);
  return <primitive object={scene} />;
};

// Component for handling FBX models
const FbxContent: FC<{ url: string; onLoaded: () => void }> = ({
  url,
  onLoaded,
}) => {
  const fbx = useFBX(url);
  useLayoutEffect(() => {
    if (fbx) {
      fbx.traverse((o) => {
        if ((o as THREE.Mesh).isMesh) {
          o.castShadow = true;
          o.receiveShadow = true;
        }
      });
      onLoaded();
    }
  }, [fbx, onLoaded]);
  return <primitive object={fbx} />;
};

// Component for handling OBJ models
const ObjContent: FC<{ url: string; onLoaded: () => void }> = ({
  url,
  onLoaded,
}) => {
  const obj = useLoader(OBJLoader as unknown as any, url);
  useLayoutEffect(() => {
    if (obj) {
      obj.traverse((o) => {
        if ((o as THREE.Mesh).isMesh) {
          o.castShadow = true;
          o.receiveShadow = true;
        }
      });
      onLoaded();
    }
  }, [obj, onLoaded]);
  return <primitive object={obj} />;
};

const SceneContent: FC<{
  url: string;
  autoRotate?: boolean;
  autoRotateSpeed?: number;
  onLoaded?: () => void;
}> = ({ url, autoRotate, autoRotateSpeed, onLoaded }) => {
  const modelRef = useRef<THREE.Group>(null!);
  const ext = url.split(".").pop()?.toLowerCase();

  useFrame((state, delta) => {
    if (autoRotate && modelRef.current) {
      modelRef.current.rotation.y += (autoRotateSpeed || 1) * delta;
    }
  });

  const onLoadedHandler = () => {
    onLoaded?.();
  };

  const ModelComponent = () => {
    switch (ext) {
      case "glb":
      case "gltf":
        return <GltfContent url={url} onLoaded={onLoadedHandler} />;
      case "fbx":
        return <FbxContent url={url} onLoaded={onLoadedHandler} />;
      case "obj":
        return <ObjContent url={url} onLoaded={onLoadedHandler} />;
      default:
        return null;
    }
  };

  return (
    <Center>
      <group ref={modelRef}>
        <ModelComponent />
      </group>
    </Center>
  );
};

// ---
// Main Viewer Component
// ---

const ModelViewer: FC<ViewerProps> = ({
  url,
  width = "100%",
  height = "100%",
  defaultZoom = 2,
  minZoomDistance = 0.5,
  maxZoomDistance = 10,
  enableManualRotation = true,
  enableManualZoom = true,
  ambientIntensity = 0.3,
  keyLightIntensity = 1,
  fillLightIntensity = 0.5,
  rimLightIntensity = 0.8,
  environmentPreset = "forest",
  autoRotate = false,
  autoRotateSpeed = 0.35,
  onModelLoaded,
}) => {
  useEffect(() => void useGLTF.preload(url), [url]);

  return (
    <div style={{ width, height }} className="relative">
      <Canvas
        shadows
        camera={{
          fov: 50,
          position: [0, 0, defaultZoom],
          near: 0.01,
          far: 100,
        }}
        gl={{
          toneMapping: THREE.ACESFilmicToneMapping,
          outputColorSpace: THREE.SRGBColorSpace,
        }}
      >
        <Suspense fallback={<Loader />}>
          <SceneContent
            url={url}
            autoRotate={autoRotate}
            autoRotateSpeed={deg2rad(autoRotateSpeed)}
            onLoaded={onModelLoaded}
          />
        </Suspense>

        {environmentPreset !== "none" && (
          <Environment preset={environmentPreset as any} />
        )}

        <ambientLight intensity={ambientIntensity} />
        <directionalLight
          position={[5, 5, 5]}
          intensity={keyLightIntensity}
          castShadow
        />
        <directionalLight
          position={[-5, 2, 5]}
          intensity={fillLightIntensity}
        />
        <directionalLight position={[0, 4, -5]} intensity={rimLightIntensity} />

        <ContactShadows
          position={[0, -0.5, 0]}
          opacity={0.35}
          scale={10}
          blur={2}
        />

        <OrbitControls
          makeDefault
          enablePan={false}
          enableDamping={false}
          enableRotate={enableManualRotation}
          enableZoom={enableManualZoom}
          minDistance={minZoomDistance}
          maxDistance={maxZoomDistance}
          autoRotate={isTouch ? false : autoRotate}
          autoRotateSpeed={autoRotateSpeed}
        />
      </Canvas>
    </div>
  );
};

export default ModelViewer;

```

</Steps>

</TabsContent>

</Tabs>

## Usage

```tsx
 import ModelViewer from '@/components/lightswind/ModelViewer';
```

```tsx
import ModelViewer from '@/components/lightswind/ModelViewer';

<ModelViewer
  url="https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/main/2.0/FlightHelmet/glTF/FlightHelmet.gltf"
  autoRotate={true}
  autoRotateSpeed={1.0}
  environmentPreset="studio"
/>
```
