Skip to content

isoteriksoftware/react-gallery-3d

Repository files navigation

Preview

Version Downloads

react-gallery-3d provides React components to create awesome 3D galleries. It supports rendering with solid colors, images, videos, etc., offering a wide range of possibilities for showcasing media in a 3D environment. Version 2 introduces exciting new features and optimizations to enhance your gallery-building experience.

New Features

  • Transparent Items: Gallery items can now be transparent, allowing you to create more complex and visually appealing galleries.
  • Object Items: You can now use custom three.js objects as gallery items, giving you more control over the appearance and behavior of your gallery.
  • New Item Material API: The item material API has been redesigned to make it easier to create and reuse custom materials for gallery items. Full control over the material properties is now possible.
  • Improved Performance: The library has been optimized to improve performance and reduce memory usage, making it faster and more efficient.

Demo

Check out the live demo (playground) to see react-gallery-3d in action and explore its capabilities.

Table of Contents

Installation

npm install react-gallery-3d three @react-three/fiber @react-three/drei

or

yarn add react-gallery-3d three @react-three/fiber @react-three/drei

Peer Dependencies

This library is designed to work alongside @react-three/drei, @react-three/fiber, and three.js. These are listed as peer dependencies, meaning that it expects these packages to be present in your project:

  • three.js: A JavaScript 3D library that creates and displays animated 3D computer graphics in a web browser.
  • @react-three/fiber: A React renderer for three.js that brings declarative, reactive, and component-based patterns to 3D rendering.
  • @react-three/drei: A useful collection of helpers and abstractions for react-three-fiber.

As peer dependencies, they are not automatically installed when you install this library. You need to manually install them in your project, if not already present. This approach helps to avoid version conflicts and reduce bundle size.

Basic Usage

import { Gallery, GalleryScene, ImageItem, SolidColorItem, VideoItem } from "react-gallery-3d";

function App() {
  return (
    <main
      style={{
        height: "100vh",
        width: "100vw",
      }}
    >
      <GalleryScene>
        <Gallery>
          <ImageItem src="https://picsum.photos/1280/720" />
          <SolidColorItem color="teal" />
          <VideoItem
            src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
            crossOrigin="anonymous"
          />
        </Gallery>
      </GalleryScene>
    </main>
  );
}

export default App;

This renders a gallery with three items: a solid color, an image, and a video. The gallery is rendered in a canvas element using react-three-fiber.

Basic Gallery

Note
You may see a different image when you run the code because the image is fetched from an external source (picsum). The image may change each time you reload the page.

Advanced Usage

You can create more complex galleries by customizing the gallery items and their materials. The following example demonstrates how to create a gallery with transparent items, custom materials, and models:

import {
  Gallery,
  GalleryScene,
  ImageItem,
  VideoItem,
  TransparentItem,
  ObjectItem,
} from "react-gallery-3d";
import { useState } from "react";
import { Mesh } from "three";
import { useGLTF } from "@react-three/drei";

function App() {
  const [box, setBox] = useState<Mesh | null>(null);
  const { scene: model } = useGLTF("/models/low_poly_character_swordsman.glb");
  const [itemHeight] = useState(60);

  return (
    <main
      style={{
        height: "100vh",
        width: "100vw",
      }}
    >
      <GalleryScene
        orbitControls={{
          autoRotate: true,
          rotateSpeed: -1,
          enableZoom: false,
        }}
        environment={{
          preset: "city",
        }}
        camera={{
          position: [0, 60, 150],
          fov: 45,
        }}
        fog={{
          color: "black",
          near: 10,
          far: 520,
        }}
        ground={{
          position: [0, -itemHeight / 2, 0],
          reflectorMaterial: {
            mirror: 0.95,
            resolution: 2048,
            roughness: 1,
            metalness: 0.7,
          },
        }}
      >
        <Gallery
          item={{
            width: 150,
            height: itemHeight,
            radialSegments: 50,
            innerRadiusPercent: 0.01,
          }}
        >
          <TransparentItem />
          <VideoItem src="/videos/vid6.mp4" />
          <ImageItem src="/images/img1.jpg" />
          <VideoItem src="/videos/vid4.mp4" />
          <ImageItem src="/images/img2.jpg" />
          <ObjectItem object={box} objectOffset={0} disableObjectRender>
            <mesh ref={(boxObj) => setBox(boxObj)}>
              <boxGeometry args={[10, 10, 10]} />
              <meshStandardMaterial color="white" />
            </mesh>
          </ObjectItem>
          <ObjectItem
            object={model}
            objectProps={{
              scale: [20, 20, 20],
            }}
            onObjectAlignmentChange={(object) => {
              object.position.y = -itemHeight / 2;
              object.rotateY(Math.PI);
            }}
          />
        </Gallery>
      </GalleryScene>
    </main>
  );
}

export default App;

Advanced Gallery

Note
The example uses a 3D model from the low-poly-character-swordsman collection on Sketchfab. The model, images, and videos are included in the public folder of the project.

Components

<GalleryScene/>

The GalleryScene is a wrapper around the Canvas component from @react-three/fiber. It sets up the 3D environment, camera, lighting, fog, and controls for galleries. It also provides a ground plane with a reflector material for realistic reflections.

You can customize the scene by passing props to the GalleryScene component. This component can render galleries and other 3D objects.

Props

type GallerySceneProps = Omit<CanvasProps, "children"> & {
  /**
   * The children to render.
   *
   * @default undefined
   */
  children?: ReactNode;

  /**
   * The background color of the scene.
   *
   * @default #000000
   */
  backgroundColor?: string;

  /**
   * The Fog properties
   *
   * @default {
   *   color: "#000000",
   *   near: 10,
   *   far: 400
   * }
   */
  fog?: FogProps;

  /**
   * The OrbitControls properties.
   *
   * @default {
   *   enableDamping: true,
   *   enableZoom: true,
   *   dampingFactor: 0.01,
   *   autoRotate: true,
   *   autoRotateSpeed: -1
   * }
   */
  orbitControls?: OrbitControlsProps;

  /**
   * Whether to disable the controls.
   *
   * @default false
   */
  disableControls?: boolean;

  /**
   * Whether to disable the fog.
   *
   * @default false
   */
  disableFog?: boolean;

  /**
   * Whether to disable the environment.
   *
   * @default false
   */
  disableEnvironment?: boolean;

  /**
   * The Environment properties.
   *
   * @default {
   *   preset: "sunset"
   * }
   */
  environment?: EnvironmentProps;

  /**
   * The Ground properties.
   *
   * @default {
   *   position: [0, -25, 0]
   * }
   */
  ground?: GroundProps;

  /**
   * Whether to disable the ground.
   *
   * @default false
   */
  disableGround?: boolean;
};

Example Usage

<GalleryScene
  backgroundColor="#000000"
  fog={{
    color: "#000000",
    near: 10,
    far: 400,
  }}
  orbitControls={{
    enableDamping: true,
    enableZoom: true,
    dampingFactor: 0.01,
    autoRotate: true,
    autoRotateSpeed: -1,
  }}
  disableControls={false}
  disableFog={false}
  disableEnvironment={false}
  environment={{
    preset: "sunset",
  }}
  ground={{
    position: [0, -25, 0],
  }}
  disableGround={false}
>
  <Gallery>
    {/* Gallery items */}
  </Gallery>

  <Gallery position-x={160}>
    {/* Gallery items */}
  </Gallery>

  {/* Other 3D objects */}
</GalleryScene>

<Gallery/>

The Gallery component is a container for gallery items. It arranges the items in a cylindrical layout around the center of the scene. It requires at least three items to render.

All children of the Gallery component must be GalleryItem or its subclasses (e.g., ImageItem, VideoItem, SolidColorItem, etc.). Unknown children will be ignored.

It accepts props to customize the appearance and layout of the gallery items.

Props

type GalleryProps = Omit<GroupProps, "children" | "ref"> & {
  /**
   * The children of the gallery.
   * These are the gallery items.
   */
  children: GalleryChildren;

  /**
   * The gallery item properties.
   */
  item?: {
    /**
     * The width of the gallery item.
     *
     * @default 120
     */
    width?: number;

    /**
     * The height of the gallery item.
     *
     * @default 50
     */
    height?: number;

    /**
     * The number of radial segments.
     *
     * @default 50
     */
    radialSegments?: number;

    /**
     * The number of height segments.
     *
     * @default 1
     */
    heightSegments?: number;

    /**
     * The percentage of the outer radius to use as the inner radius.
     *
     * @default 0.01
     */
    innerRadiusPercent?: number;
  };
}

Example Usage

<Gallery
  item={{
    width: 120,
    height: 50,
    radialSegments: 50,
    heightSegments: 1,
    innerRadiusPercent: 0.01,
  }}
>
  {/* Gallery items */}
</Gallery>

<GalleryItem/>

The GalleryItem component is the base component for gallery items. It provides a common interface for all gallery items. This can be used to create custom gallery items.

Any 3D object can be a child of the GalleryItem component. Children objects can be positioned and aligned correctly using ObjectItem or using the usePlacementOnGalleryItem hook for more control over the placement of the object.

The component has a material prop that accepts a material or an array of materials to apply to the gallery item. This allows you to customize the appearance of the gallery item.

Props

type GalleryItemProps = MeshProps & {
  /**
   * The material to apply to the gallery item.
   */
  material: Material | Material[];
};

Example Usage

import { Gallery, GalleryItem, GalleryScene } from "react-gallery-3d";
import { useMemo } from "react";
import { MeshStandardMaterial } from "three";

function App() {
  const greenMaterial = useMemo(
    () =>
      new MeshStandardMaterial({
        color: "green",
        emissive: "red",
        roughness: 0.5,
        metalness: 1,
      }),
    [],
  );

  const redMaterial = useMemo(
    () =>
      new MeshStandardMaterial({
        color: "red",
        emissive: "blue",
        roughness: 0.5,
        metalness: 1,
      }),
    [],
  );

  const blueMaterial = useMemo(
    () =>
      new MeshStandardMaterial({
        color: "blue",
        emissive: "green",
        roughness: 0.5,
        metalness: 1,
      }),
    [],
  );

  return (
    <main
      style={{
        height: "100vh",
        width: "100vw",
      }}
    >
      <GalleryScene
        ground={{
          reflectorMaterial: {
            metalness: 1,
            roughness: 1,
            mirror: 1,
            resolution: 2048,
          },
        }}
      >
        <Gallery>
          <GalleryItem material={greenMaterial} />
          <GalleryItem material={redMaterial} />
          <GalleryItem material={blueMaterial} />
        </Gallery>
      </GalleryScene>
    </main>
  );
}

export default App;

Example Output


<SolidColorItem/>

The SolidColorItem component renders a gallery item with a solid color material. It accepts a color prop to specify the color of the item.

This component is useful for creating simple gallery items with solid colors. It does not require any additional setup or configuration. If you need more control over the appearance of the gallery item, you can use the GalleryItem component with a custom material.

Props

type SolidColorItemProps = Omit<GalleryItemProps, "material"> & {
  /**
   * The color of the solid color item.
   */
  color: ColorRepresentation;
};

Example Usage

import { Gallery, GalleryScene, SolidColorItem } from "react-gallery-3d";

function App() {
  return (
    <main
      style={{
        height: "100vh",
        width: "100vw",
      }}
    >
      <GalleryScene>
        <Gallery>
          <SolidColorItem color="red" />
          <SolidColorItem color="green" />
          <SolidColorItem color="blue" />
          <SolidColorItem color="yellow" />
          <SolidColorItem color="purple" />
          <SolidColorItem color="orange" />
        </Gallery>
      </GalleryScene>
    </main>
  );
}

export default App;

Example Output


<ImageItem/>

The ImageItem component renders a gallery item with an image (texture) mapped to a material. This component is useful for creating gallery items with images.

It accepts a texture prop to provide a custom texture for the item. It also accepts a src prop to specify the URL of the image to use as the texture.

Using the texture prop is recommended for better performance and control over loading the texture using useTexture from @react-three/drei.

Props

type ImageItemProps = Omit<GalleryItemProps, "material"> & {
  /**
   * The image source.
   * If a texture is provided, this will be ignored.
   */
  src?: string;

  /**
   * The texture to use.
   * If provided, the src will be ignored.
   */
  texture?: Texture;

  /**
   * The material to use.
   * If not provided, a new MeshStandardMaterial will be created.
   */
  material?: MappableMaterial;
};

Example Usage

import { Gallery, GalleryScene, ImageItem } from "react-gallery-3d";
import { useTexture } from "@react-three/drei";

function MyGallery() {
  const textures = useTexture([
    "/images/img1.jpg",
    "/images/img2.jpg",
    "/images/img3.jpg",
    "/images/img4.jpg",
    "/images/img5.jpg",
    "/images/img6.jpg",
  ]);

  return (
    <Gallery>
      {textures.map((texture, index) => (
        <ImageItem key={index} texture={texture} />
      ))}
    </Gallery>
  );
}

function App() {
  return (
    <main
      style={{
        height: "100vh",
        width: "100vw",
      }}
    >
      <GalleryScene
        environment={{
          preset: "city",
        }}
      >
        <MyGallery />
      </GalleryScene>
    </main>
  );
}

export default App;

Example Output

Note

  • The images are included in the public folder of the project.
  • useTexture is used to load the images as textures for better performance.
  • useTexture must be called in a component that is a child of the Canvas component from @react-three/fiber.
  • The GalleryScene component is a wrapper around the Canvas component.

<VideoItem/>

The VideoItem component renders a gallery item with a video texture mapped to a material. This component is useful for creating gallery items with videos.

It accepts a src prop to specify the URL of the video to use as the texture. It also accepts props to control the video playback, such as autoplay, muted, and loop.

When autoplay is set to true, the video will be muted by default. This is a requirement for autoplaying videos in most browsers.

Props

type VideoItemProps = Omit<GalleryItemProps, "material"> & {
  /**
   * The video source.
   */
  src: string;

  /**
   * Whether to autoplay the video.
   *
   * If this is set to true, the video will be muted.
   *
   * @default true
   */
  autoplay?: boolean;

  /**
   * Whether to mute the video.
   *
   * @default true
   */
  muted?: boolean;

  /**
   * Whether to loop the video.
   *
   * @default true
   */
  loop?: boolean;

  /**
   * The cross-origin attribute for the video.
   */
  crossOrigin?: JSX.IntrinsicElements["video"]["crossOrigin"];

  /**
   * A callback that is called when the video is initialized.
   * This is useful for getting references to the video and texture.
   *
   * @param video the video element
   * @param texture the video texture
   */
  onInit?: (video: HTMLVideoElement, texture: VideoTexture) => void;

  /**
   * The material to use for the video.
   *
   * If not provided, a new MeshStandardMaterial will be created.
   */
  material?: MappableMaterial;
};

Example Usage

import { Gallery, GalleryScene, VideoItem } from "react-gallery-3d";

function App() {
  const videos = [
    "/videos/vid1.mp4",
    "/videos/vid2.mp4",
    "/videos/vid3.mp4",
    "/videos/vid4.mp4",
    "/videos/vid5.mp4",
    "/videos/vid6.mp4",
  ];

  return (
    <main
      style={{
        height: "100vh",
        width: "100vw",
      }}
    >
      <GalleryScene
        environment={{
          preset: "city",
        }}
        fog={{
          far: 500,
        }}
      >
        <Gallery>
          {videos.map((video) => (
            <VideoItem key={video} src={video} />
          ))}
        </Gallery>
      </GalleryScene>
    </main>
  );
}

export default App;

Example Output

Note
The videos are included in the public folder of the project.


<TransparentItem/>

The TransparentItem component renders a gallery item with a transparent material. This component is useful for creating gallery items with transparent background.

It is not very useful on its own, but can be combined with other components to create more complex galleries.

Props

type TransparentItemProps = Omit<GalleryItemProps, "material"> & {
  /**
   * The opacity of the item.
   *
   * @default 0
   */
  opacity?: number;
};

Example Usage

import {
  Gallery,
  GalleryScene,
  ImageItem,
  SolidColorItem,
  VideoItem,
  TransparentItem,
  usePlacementOnGalleryItem,
} from "react-gallery-3d";
import { useEffect, useRef, useState } from "react";
import { Mesh } from "three";

function GalleryItemBox() {
  const [size] = useState(10);
  const boxRef = useRef<Mesh>(null!);
  const { position, orientation } = usePlacementOnGalleryItem(size / 2);

  useEffect(() => {
    const box = boxRef.current;
    box.position.copy(position);
    box.lookAt(orientation);
  }, [orientation, position]);

  return (
    <mesh ref={boxRef}>
      <boxGeometry args={[size, size, size]} />
      <meshStandardMaterial color="white" />
    </mesh>
  );
}

function App() {
  return (
    <main
      style={{
        height: "100vh",
        width: "100vw",
      }}
    >
      <GalleryScene>
        <Gallery>
          <ImageItem src="https://picsum.photos/1280/720" />

          <SolidColorItem color="teal" />

          <TransparentItem />

          <VideoItem
            src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
            crossOrigin="anonymous"
          />

          <TransparentItem>
            <GalleryItemBox />
          </TransparentItem>
        </Gallery>
      </GalleryScene>
    </main>
  );
}

export default App;

Example Output

Note

  • The image is fetched from an external source (picsum). The image may change each time you reload the page.
  • The GalleryItemBox uses the usePlacementOnGalleryItem hook to position the box on the gallery item.

<ObjectItem/>

The ObjectItem component renders a gallery item with a custom three.js object. This component is useful for creating gallery items with custom 3D objects.

It accepts an object prop to specify the custom object to render. The object can be a reference to an object you render yourself or leave it to the component to render the object by setting the disableObjectRender prop to true.

When disableObjectRender is set to true, the component will not pass the objectProps prop to the object, but it will still automatically align the object on the gallery item surface using the usePlacementOnGalleryItem hook. This allows you to render the object yourself and have full control over its appearance and behavior.

This component makes it very easy render models on gallery items. It can also be used to automatically place an object on a gallery item using the usePlacementOnGalleryItem hook internally.

Props

type ObjectItemProps = TransparentItemProps & {
  /**
   * The object to render.
   * If null, nothing will be rendered.
   */
  object?: Object3D | null;

  /**
   * The object properties.
   */
  objectProps?: Object3DProps;

  /**
   * The object z-axis offset from the center of the item.
   */
  objectOffset?: number;

  /**
   * The object horizontal alignment offset.
   */
  objectAlignmentOffset?: number;

  /**
   * Whether to disable rendering the object.
   * This is useful when you want to render the object in a different way.
   *
   * When rendering is disabled, the objectProps provided will be ignored.
   * It becomes your responsibility to render and update the object directly.
   */
  disableObjectRender?: boolean;

  /**
   * A callback that is called when the object alignment changes.
   * This is useful when you want to update the object alignment after the object is aligned on the item.
   *
   * @param object The object that was aligned.
   */
  onObjectAlignmentChange?: (object: Object3D) => void;

  /**
   * A callback that is called before the object alignment is applied.
   * This is useful when you want to update the calculated alignment before it is applied.
   *
   * @param alignment The calculated object alignment.
   */
  onBeforeObjectAlignmentApplied?: (alignment: ObjectAlignment) => void;
};

Example Usage

import { Gallery, GalleryScene, ObjectItem, SolidColorItem } from "react-gallery-3d";
import { useEffect, useState } from "react";
import { useAnimations, useGLTF } from "@react-three/drei";
import { Mesh } from "three";

function BellyDancer() {
  const { scene: model, animations } = useGLTF("/models/belly-dancer.glb");
  const { ref, mixer } = useAnimations(animations);

  useEffect(() => {
    animations.forEach((clip) => {
      mixer.clipAction(clip).play();
    });
  }, [animations, mixer]);

  return (
    <ObjectItem
      object={model}
      disableObjectRender
      onObjectAlignmentChange={(object) => {
        object.position.y = -25;
      }}
    >
      <primitive ref={ref} object={model} scale={[0.5, 0.5, 0.5]} />
    </ObjectItem>
  );
}

function App() {
  const [box, setBox] = useState<Mesh | null>(null);
  const { scene: model } = useGLTF("/models/low_poly_character_swordsman.glb");

  return (
    <main
      style={{
        height: "100vh",
        width: "100vw",
      }}
    >
      <GalleryScene>
        <Gallery>
          <ObjectItem object={box} objectOffset={0} disableObjectRender>
            <mesh ref={(boxObj) => setBox(boxObj)}>
              <boxGeometry args={[10, 10, 10]} />
              <meshStandardMaterial color="white" />
            </mesh>
          </ObjectItem>

          <SolidColorItem color="blue" />

          <ObjectItem
            object={model}
            objectProps={{
              scale: [20, 20, 20],
            }}
            onObjectAlignmentChange={(object) => {
              object.position.y = -25;
              object.rotateY(Math.PI);
            }}
          />

          <SolidColorItem color="green" />

          <BellyDancer />
        </Gallery>
      </GalleryScene>
    </main>
  );
}

export default App;

Example Output

Note

  • The models are included in the public folder of the project.
  • The BellyDancer component uses the useAnimations and useGLTF hooks from @react-three/drei to load and animate the model.

Hooks

useGallery()

The useGallery hook provides access to the gallery context, allowing you to access the gallery and items properties.

This hook is useful when you need to access the gallery properties in a child component of the Gallery component. It returns a GalleryState object that contains the gallery item properties.

type GalleryState = {
  /**
   * The total number of items in the gallery.
   */
  itemCount: number;

  /**
   * The gallery item properties.
   */
  item: {
    /**
     * The width of the gallery item.
     */
    width: number;

    /**
     * The height of the gallery item.
     */
    height: number;

    /**
     * The number of radial segments.
     */
    radialSegments: number;

    /**
     * The number of height segments.
     */
    heightSegments: number;

    /**
     * The percentage of the outer radius to use as the inner radius.
     */
    innerRadiusPercent: number;

    /**
     * The angle of the section of the gallery item.
     * This is used to calculate the position of the gallery item.
     */
    sectionAngle: number;

    /**
     * The radius of the gallery item.
     */
    outerRadius: number;

    /**
     * The inner radius of the gallery item.
     */
    innerRadius: number;

    /**
     * The index of the gallery item.
     */
    itemIndex?: number;
  };
};

Example Usage

import {
  Gallery,
  GalleryScene,
  SolidColorItem,
  useGallery,
  usePlacementOnGalleryItem,
} from "react-gallery-3d";
import { Text } from "@react-three/drei";
import { useEffect, useRef } from "react";
import { MathUtils, Mesh } from "three";

function ItemLabel() {
  const {
    item: { itemIndex },
  } = useGallery();
  const textRef = useRef<Mesh | null>(null);
  const { position, orientation } = usePlacementOnGalleryItem(5);

  useEffect(() => {
    const text = textRef.current;
    if (!text) return;

    text.position.copy(position);
    text.lookAt(orientation);
    text.rotateY(MathUtils.degToRad(180));
  }, [orientation, position]);

  return (
    <Text ref={textRef} fontSize={6} color="white" textAlign="center">
      ITEM {(itemIndex || 0) + 1}
    </Text>
  );
}

function App() {
  return (
    <main
      style={{
        height: "100vh",
        width: "100vw",
      }}
    >
      <GalleryScene>
        <Gallery>
          <SolidColorItem color="red">
            <ItemLabel />
          </SolidColorItem>

          <SolidColorItem color="green">
            <ItemLabel />
          </SolidColorItem>

          <SolidColorItem color="blue">
            <ItemLabel />
          </SolidColorItem>

          <SolidColorItem color="yellow">
            <ItemLabel />
          </SolidColorItem>
        </Gallery>
      </GalleryScene>
    </main>
  );
}

export default App;

Example Result

Hints

  • The ItemLabel component uses the useGallery hook to access the gallery item properties.
  • The usePlacementOnGalleryItem hook is used to position the text on the gallery item surface.

usePlacementOnGalleryItem()

The usePlacementOnGalleryItem hook calculates the values required to place an object on a gallery item surface.

You can pass an objectOffset to specify the offset of the object from the center of the gallery item.

You can also pass itemAlignmentOffset to adjust the alignment of the object on the gallery item surface. This will be calculated automatically if you do not provide one.

It returns an ObjectAlignment object that contains the position and orientation of the object:

type ObjectAlignment = {
  /**
   * The calculated position for the object.
   */
  position: Vector3;

  /**
   * The calculated orientation for the object.
   */
  orientation: Vector3;
};

Hint
Check the example above for how to use the usePlacementOnGalleryItem hook.


useImageMaterial()

The useImageMaterial hook creates a material mapped to an image texture. It is useful for creating custom materials for gallery items with images.

An existing material can be wrapped using the wrappedMaterial prop. This allows you to create a custom material and use the image texture as the map property.

If no wrappedMaterial is provided, a new MeshStandardMaterial is created.

If a texture is provided, it is used instead of loading the source:

type UseImageMaterialOptions = {
  /**
   * The image source.
   * If a texture is provided, this will be ignored.
   */
  src?: string;

  /**
   * The texture to use.
   * If provided, the src will be ignored.
   */
  texture?: Texture;

  /**
   * The wrapped material.
   * If not provided, a new MeshStandardMaterial will be created.
   */
  wrappedMaterial?: MappableMaterial;
};

The hook returns a UseImageMaterialResult object that contains the material and texture:

type UseImageMaterialResult = {
  /**
   * The texture for the image.
   */
  texture: Texture;

  /**
   * The material for the image.
   */
  material: MappableMaterial;
};

Example Usage

import {
  Gallery,
  GalleryScene,
  GalleryItem,
  ImageItemProps,
  useImageMaterial,
} from "react-gallery-3d";
import { useMemo } from "react";
import { MeshPhysicalMaterial } from "three";

function GlassImageItem({ src, texture, children }: ImageItemProps) {
  const material = useMemo(() => {
    return new MeshPhysicalMaterial({
      toneMapped: true,
      polygonOffset: true,
      polygonOffsetFactor: 1,
      polygonOffsetUnits: 1,
      metalness: 0,
      roughness: 0,
      transmission: 0.2,
      clearcoat: 0.1,
      transparent: true,
      opacity: 0.7,
    });
  }, []);

  const { material: finalMaterial } = useImageMaterial({
    src,
    texture,
    wrappedMaterial: material,
  });

  return <GalleryItem material={finalMaterial}>{children}</GalleryItem>;
}

function App() {
  return (
    <main
      style={{
        height: "100vh",
        width: "100vw",
      }}
    >
      <GalleryScene
        ground={{
          reflectorMaterial: {
            metalness: 1,
            roughness: 0.9,
            mirror: 1,
            resolution: 2048,
          },
        }}
        environment={{
          preset: "city",
        }}
      >
        <Gallery>
          <GlassImageItem src="/images/img4.jpg" />
          <GlassImageItem src="/images/img5.jpg" />
          <GlassImageItem src="/images/img6.jpg" />
          <GlassImageItem src="/images/img1.jpg" />
          <GlassImageItem src="/images/img2.jpg" />
          <GlassImageItem src="/images/img3.jpg" />
        </Gallery>
      </GalleryScene>
    </main>
  );
}

export default App;

Example Output


useVideoMaterial()

The useVideoMaterial hook creates a material mapped to a video texture. It is useful for creating custom materials for gallery items with videos.

An existing material can be wrapped using the wrappedMaterial prop. This allows you to create a custom material and use the video texture as the map property.

If no wrappedMaterial is provided, a new MeshStandardMaterial is created:

type UseVideoMaterialOptions = {
  /**
   * The video source.
   */
  src: string;

  /**
   * The wrapped material.
   *
   * If not provided, a new MeshStandardMaterial will be created.
   */
  wrappedMaterial?: MappableMaterial;

  /**
   * Whether to autoplay the video.
   *
   * If this is set to true, the video will be muted.
   *
   * @default true
   */
  autoplay?: boolean;

  /**
   * Whether to mute the video.
   *
   * @default true
   */
  muted?: boolean;

  /**
   * Whether to loop the video.
   *
   * @default true
   */
  loop?: boolean;

  /**
   * The cross-origin attribute for the video.
   */
  crossOrigin?: JSX.IntrinsicElements["video"]["crossOrigin"];
};

The hook returns a UseVideoMaterialResult object that contains the material, video texture, and video element:

type UseVideoMaterialResult = {
  /**
   * The video element.
   */
  video: HTMLVideoElement;

  /**
   * The video texture.
   */
  texture: VideoTexture;

  /**
   * The material.
   */
  material: MappableMaterial;
};

Example Usage

import {
  Gallery,
  GalleryScene,
  GalleryItem,
  VideoItemProps,
  useVideoMaterial,
} from "react-gallery-3d";
import { useMemo } from "react";
import { MeshPhysicalMaterial } from "three";

function ShinyVideoItem({ children, ...rest }: VideoItemProps) {
  const material = useMemo(() => {
    return new MeshPhysicalMaterial({
      toneMapped: true,
      polygonOffset: true,
      polygonOffsetFactor: 1,
      polygonOffsetUnits: 1,
      reflectivity: 1,
      metalness: 1,
      roughness: 0.2,
      clearcoat: 1,
      clearcoatRoughness: 0.1,
      emissive: "rgba(67,9,9,0.1)",
    });
  }, []);

  const { material: finalMaterial } = useVideoMaterial({
    ...rest,
    wrappedMaterial: material,
  });

  return <GalleryItem material={finalMaterial}>{children}</GalleryItem>;
}

function App() {
  return (
    <main
      style={{
        height: "100vh",
        width: "100vw",
      }}
    >
      <GalleryScene
        ground={{
          reflectorMaterial: {
            metalness: 1,
            roughness: 0.9,
            mirror: 1,
            resolution: 2048,
          },
        }}
        environment={{
          preset: "sunset",
        }}
      >
        <Gallery>
          <ShinyVideoItem src="/videos/vid4.mp4" />
          <ShinyVideoItem src="/videos/vid5.mp4" />
          <ShinyVideoItem src="/videos/vid6.mp4" />
          <ShinyVideoItem src="/videos/vid1.mp4" />
          <ShinyVideoItem src="/videos/vid2.mp4" />
          <ShinyVideoItem src="/videos/vid3.mp4" />
        </Gallery>
      </GalleryScene>
    </main>
  );
}

export default App;

Example Output


Breaking Changes

Version 2.x.x introduces breaking changes to the API.

The biggest change is on the material generation API. The library now gives you more control over how and when materials are generated and reused across gallery items.

Please review the following changes carefully to update your code accordingly:

  • The GalleryItem component now requires a material prop to specify the material for the gallery item. Previously, the material was created internally using a generator function itemMaterial prop. This prop is no longer supported.
  • GalleryItemMaterial and its subclasses are no longer available. You should use the GalleryItem component with a custom material instead.
  • The Gallery component no longer accepts props for managing a Ground component. The Ground component is now managed internally by the GalleryScene component. All the props for the Ground are now available in the GalleryScene component.

Migration Guide

This guide will help you migrate from version 1.x.x to version 2.x.x of the library.

GalleryItem Material API

In version 1.x.x, the GalleryItem component generated the material internally using the itemMaterial prop. This was a convenient way to create materials for gallery items without having to manage them manually.

In version 2.x.x, the GalleryItem component requires a material prop to specify the material for the gallery item. This gives you more control over how and when materials are created and reused across gallery items.

Before

class ShinyRedMaterial implements GalleryItemMaterial {
  public generate() {
    return new MeshPhysicalMaterial({
      color: 'red',
      reflectivity: 1,
      metalness: 1,
      roughness: 0.2,
      clearcoat: 1,
      clearcoatRoughness: 0.1,
      polygonOffsetFactor: 1,
      polygonOffsetUnits: 1,
    });
  }
}

function App() {
  const shinyRedMaterialGenerator = useMemo(() => new ShinyRedMaterial(), []);
  const [shinyRedMaterial, setShinyRedMaterial] = useState<MeshPhysicalMaterial>();

  return (
    <main
      style={{
        height: "100vh",
        width: "100vw",
      }}
    >
      <GalleryScene>
        <Gallery>
          <GalleryItem
            itemMaterial={shinyRedMaterialGenerator}
            onInit={({ material }) => setShinyRedMaterial(material as MeshPhysicalMaterial)}
          />

          {/* Other items... */}
        </Gallery>
      </GalleryScene>
    </main>
  );
}

Now

function App() {
  const shinyRedMaterial = useMemo(
    () =>
      new MeshPhysicalMaterial({
        color: "red",
        reflectivity: 1,
        metalness: 1,
        roughness: 0.2,
        clearcoat: 1,
        clearcoatRoughness: 0.1,
        polygonOffsetFactor: 1,
        polygonOffsetUnits: 1,
      }),
    [],
  );

  return (
    <main
      style={{
        height: "100vh",
        width: "100vw",
      }}
    >
      <GalleryScene>
        <Gallery>
          <GalleryItem material={shinyRedMaterial} />

          {/* Other items... */}
        </Gallery>
      </GalleryScene>
    </main>
  );
}

GalleryItemMaterial Implementations

In version 1.x.x, the library provided GalleryItemMaterial and its implementations (ImageItemMaterial, SolidColorItemMaterial, and VideoItemMaterial) to create materials for gallery items.

These implementations are no longer available in version 2.x.x. You should use the GalleryItem component with a custom material or use new hooks like useImageMaterial() and useVideoMaterial() to create materials for gallery items.

Before

class GlassyImageMaterial extends ImageItemMaterial {
  constructor() {
    super("/images/img1.jpg");
  }

  public generate() {
    this.initTexture();

    return new MeshPhysicalMaterial({
      toneMapped: false,
      map: this.texture,
      polygonOffset: true,
      polygonOffsetFactor: 1,
      polygonOffsetUnits: 1,
      metalness: 0,
      roughness: 0,
      transmission: 0.2,
      clearcoat: 0.3,
    });
  }
}

function App() {
  const glassyImageMaterialGenerator = useMemo(
    () => new GlassyImageMaterial(),
    [],
  );
  const [glassyImageMaterial, setGlassyImageMaterial] =
    useState<MeshPhysicalMaterial>();

  return (
    <main
      style={{
        height: "100vh",
        width: "100vw",
      }}
    >
      <GalleryScene>
        <Gallery>
          <GalleryItem
            itemMaterial={glassyImageMaterialGenerator}
            onInit={({ material }) =>
              setGlassyImageMaterial(material as MeshPhysicalMaterial)
            }
          />

          {/* Other items... */}
        </Gallery>
      </GalleryScene>
    </main>
  );
}

Now

function GlassyImage() {
  const material = useMemo(() => {
    return new MeshPhysicalMaterial({
      toneMapped: false,
      polygonOffset: true,
      polygonOffsetFactor: 1,
      polygonOffsetUnits: 1,
      metalness: 0,
      roughness: 0,
      transmission: 0.2,
      clearcoat: 0.3,
    });
  }, []);

  const { material: finalMaterial } = useImageMaterial({
    src: "/images/img1.jpg",
    wrappedMaterial: material,
  });

  return <GalleryItem material={finalMaterial} />;
}

function App() {
  return (
    <main
      style={{
        height: "100vh",
        width: "100vw",
      }}
    >
      <GalleryScene>
        <Gallery>
          <GlassyImage />

          {/* Other items... */}
        </Gallery>
      </GalleryScene>
    </main>
  );
}

Hint
Import useImageMaterial from react-gallery-3d.


Gallery Ground API

In version 1.x.x, the Gallery component accepted props for managing a Ground component. This allowed you to customize the ground material and properties for the gallery.

In version 2.x.x, the Gallery component no longer accepts props for managing a Ground component. The Ground component is now managed internally by the GalleryScene component. All the props for the Ground are now available in the GalleryScene component.

This makes it easier to use one Ground component for the entire gallery scene.

Before

function App() {
  return (
    <main
      style={{
        height: "100vh",
        width: "100vw",
      }}
    >
      <GalleryScene>
        <Gallery
          ground={{
            reflectorMaterial: {
              metalness: 1,
              roughness: 0.9,
              mirror: 1,
              resolution: 2048,
            },
          }}
        >
          {/* Gallery items... */}
        </Gallery>
      </GalleryScene>
    </main>
  );
}

Now

function App() {
  return (
    <main
      style={{
        height: "100vh",
        width: "100vw",
      }}
    >
      <GalleryScene
        ground={{
          reflectorMaterial: {
            metalness: 1,
            roughness: 0.9,
            mirror: 1,
            resolution: 2048,
          },
        }}
      >
        <Gallery>
          {/* Gallery items... */}
        </Gallery>

        <Gallery position-x={160}>
          {/* Gallery items... */}
        </Gallery>
      </GalleryScene>
    </main>
  );
}

Hint
You can now render multiple Gallery components in the GalleryScene component, and they will share the same ground.

Contributing

Contributions are welcome! Please read our Code of Conduct and Contributing