Getting Started

Basic installation and setup workflows to get started with the composable player.

Requirements

The Composable SDK requires to be used within a React-based environment.

Installation

The player package can be installed using the following command:

npx @threekit/create-app@latest


This will provide the option to install the composable sdk either as a new project using react/next.js, or as part of an existing react/next.js project.

You will initially be prompted to provide the name of your project. If the project folder with that name already exists, the installation wizard will determine if it is already a react/next.js installation. If it is not already a react/next.js installation, then you will be prompted with the create-react-app or create-next-app installations.

You will also have the option to choose whether to install the additional composable configurator and rest-api sdk.

The installer will also add and install the following required dependencies:

  • @react-three/drei
  • @react-three/fiber
  • three

Import all composable player components, hooks, and types from "@threekit/react-three-fiber"

Basic Project Setup

🚧

The Composable Player is in a very early stage of development.

Improvements will follow, and some of the features and workflows listed will change in the future.

Embedding the player in a React app is pretty straight-forward. At the very minimum, all that is needed is the use of the and components.

In a Next.JS app, however, we need to turn off SSR completely, as the Three.JS libraries make use of the window object. This can be accomplished using the dynamic library.

The example setup below is for Next.JS 14 using TypeScript. We need to use a client component for embedding the player, which is then referenced using the dynamic library into the main page. This example sets up the player embed area to match the size and aspect ratio of the player preview in the ThreeKit platform, for easier comparison.

Example page.tsx

"use client";
import dynamic from "next/dynamic";
const Player = dynamic(() => import("../components/Player"), { ssr: false });

export default function Home() {
  return (
    <main className="flex min-h-screen flex-col items-center p-24">
      <div style={{ width: "673px", height: "550px", background: "#eee" }}>
        <Player />
      </div>
    </main>
  );
}

Example Player.tsx

"use client";
import { Viewer, UnifiedResolver } from "@threekit/react-three-fiber";

export default function Player() {
  const auth = {
    orgId: "YOUR_ORG_ID",
    host: "preview.threekit.com",
    publicToken: "YOUR_PUBLIC_TOKEN",
  };

  return (
    <div style={{ width: "100%", height: "100%" }}>
      <Viewer
        auth={auth}
        ui={true}
        resolver={UnifiedResolver({
          cacheScope: "v1",
          optimize: false,
        })}
      >
        <MyScene assetId={"b1571ea4-e7f3-400f-ba58-b875b382a4a5"} />
      </Viewer>
    </div>
  );
}

Requirements

The example above relies on a very particular setup on the ThreeKit platform. The Scene assetId in this example is designed to point to a Catalog Item which has a Scene asset referenced to it as its 3D Asset.

Stages are not currently supported by the composable player. However, in the case where you would like to reuse a scene with multiple different Items in a similar way to a Stage setup, this can be accomplished by using the <Scene> component in conjunction with an component or useAsset hook.. The component can point to the shareable Scene, while the component can point to the Catalog Item that has its 3D Model asset referenced.

The component would then need to be assigned to a node from the imported scene, or a fresh <group> node in React if a particular transform is needed.

It is certainly possible to build up the whole entire setup from scratch in React, relying only on Model Assets that are imported through the component. However, this will prove to be a lot more difficult, as cameras, lights, and scene setup would then have to be added and set up in the front-end code, instead of relying on artist-based setups on the ThreeKit platform.

Caching

The cacheScope option on the Viewer's resolver allows us to specify which cached version of the assets we need to load.

🚧

Refreshing the Cache

If changes are made to the assets in the Platform, then a new cacheScope string is required in order to invalidate the cache and generate a new cached version of the assets. For example, changing the string in the above example from "v1" to "v2".

Querying For Nodes

Wait For Assets To Load

The recommended way to check when the assets have been loaded is to build your own Asset and Scene components using the useAsset and useScene hooks. This way we can pass a ref prop to the <primitive> react-three-fiber component, and tie it to a state variable. Use the example Asset and MyScene component code provided.

  1. In the example below we create a state variable called scene, and we pass its set function as the ref to the <MyScene> component.
  2. The set function will update the state of the scene variable when the asset finished loading.

Example

Here is the modified version of the Player.tsx file from above.

"use client";
import { Viewer, UnifiedResolver } from "@threekit/react-three-fiber";
import { useEffect, useState } from "react";
import * as THREE from "three";
import MyScene from "./MyScene";

export default function Player() {
  const auth = {
    orgId: "YOUR_ORG_ID",
    host: "preview.threekit.com",
    publicToken: "YOUR_PUBLIC_TOKEN",
  };
  
  const [scene, setScene] = useState<THREE.Object3D | null>(null);

  useEffect(() => {
    if (scene) {
      console.log ("Scene is now loaded:", scene);
    }
  }, [scene]);

  return (
    <div style={{ width: "100%", height: "100%" }}>
      <Viewer
        auth={auth}
        ui={true}
        resolver={UnifiedResolver({
          cacheScope: "v1",
          optimize: false,
        })}
      >
        <MyScene assetId={"b1571ea4-e7f3-400f-ba58-b875b382a4a5"} ref={setScene} />
      </Viewer>
    </div>
  );
}

Traversing the Scene Graph

Once we have detected that the Scene has finished loading, we can proceed to traverse the Scene Graph and look for nodes by name using the traverse method, which is available on the THREE.Object3D element loaded by the Scene component.

We could insert the following example in the useEffect hook listed above:

useEffect(() => {
  if (scene) {
    console.log ("Scene is now loaded:", scene);
    scene.traverse((node) => {
      if (node.name === "My_Node_Name") {
        console.log("Found the node:", node);                
      }
    });
  }
}, [scene]);

🚧

Warning!

Care must be taken to avoid performing the traverse repeatedly. Try to perform it only once per asset load.

Camera Setup

Importing a Scene using the component will automatically load the default camera from that scene along with any other camera nodes.

Camera Controls

Simply loading a scene will not automatically make the scene interactive for the user. In order to allow the user to interact with the viewer and look around, we need to rely primarily on two components:

  • <OrbitControls> from the @react-three/drei library. Read the official docs from THREE.OrbitControls on usage. This component will immitate the Orbit control mode, where the camera rotates around the target.
  • <TurntableControls> from the @threekit/react-three-fiber library. This option immitates the Node Turntable control mode from the ThreeKit Platform UI, whereby the target node is rotated around its Y axis, while the camera is rotated around the target around the view X axis.

The component needs to be added as a child of the component. The order of where this component is located is not important, as it takes effect at the overall Viewer level.

Example

<Viewer
  auth={auth}
  ui={true}
  resolver={UnifiedResolver({
    cacheScope: "v1",
    optimize: false,
  })}
>
  <OrbitControls />
  <MyScene assetId={"b1571ea4-e7f3-400f-ba58-b875b382a4a5"} />
</Viewer>

Camera Target

Using the <OrbitControls> by itself will prove insufficient, as by default it will always focus on the world center at (0,0,0). We need to set its focus target using the target prop. However, we need a target location. The recommended workflow would be to have the artists create a Null node inside the Scene at the correct location, and name it something like CamTarget.

On the front-end we could then traverse the scene graph and look for this particular node, as shown in the Traversing the Scene Graph section above.

🚧

Warning!

If the camera in the ThreeKit scene is not facing the target node directly, then the <OrbitControls> will show a shift from original orientation to the new orientation to face the target. This is due to differences in how the Three.JS <OrbitControls> operates compared to the in-platform camera functionality.

To compensate for this, the artist could set up the camera in the platform by parenting it to another null, which is in turn parented to the CameraTarget. All camera rotations should be performed on the null, rather than the camera. The camera's Z position can be used for positioning the camera closer or farther from the target.

This ensures the camera will match between the Classic Player and the Composable Player.

Here is an example of how we could modify the Player.tsx file to perform this task, and assign the node's position as the target of the <OrbitControls> after we have waited for the scene to load:

"use client";
import { Viewer, UnifiedResolver } from "@threekit/react-three-fiber";
import { OrbitControls } from "@react-three/drei";
import { useEffect, useState } from "react";
import * as THREE from "three";
import MyScene from "./MyScene";

export default function Player() {
  const auth = {
    orgId: "YOUR_ORG_ID",
    host: "preview.threekit.com",
    publicToken: "YOUR_PUBLIC_TOKEN",
  };
  
  const [scene, setScene] = useState<THREE.Object3D | null>(null);
  const [camTarget, setCamTarget] = useState<THREE.Object3D | null>(null);

  useEffect(() => {
    if (scene) {
      scene.traverse((node) => {
        if (node.name === "CamTarget") {
          console.log("Found the target node:", node);
          setCamTarget(node);
        }
      });
    }
  }, [scene]);

  return (
    <div style={{ width: "100%", height: "100%" }}>
      <Viewer
        auth={auth}
        ui={true}
        resolver={UnifiedResolver({
          cacheScope: "v1",
          optimize: false,
        })}
      >
        {scene ? (
          camTarget ? (
            <OrbitControls target={camTarget.position} />
          ) : (
            <OrbitControls />
          )
        ) : null}
        <MyScene assetId={"b1571ea4-e7f3-400f-ba58-b875b382a4a5"} ref={setScene} />
      </Viewer>
    </div>
  );
}

Switching Cameras

Switching between cameras is possible through the configuration attributes (such as a CameraAngle attribute), but that will end up serving a new gltf file for the whole asset, from the new camera angle.

This will prove inefficient in most cases, so it is advisable to extract the cameras from the gltf file (or create new ones in React) and switch the player camera between them using the provided component.

The cameras can be extracted from the assets using the traverse method shown above. Then, these camera objects can be passed to the camera prop of the <MyScene> component. It would be best to bundle all node searches under a single traverse.

Avoid performing additional traverses unnecessarily.

Here is an example of the Player.tsx file, with a dropdown added to allow switching between the cameras found in the scene:

"use client";
import { Viewer, UnifiedResolver } from "@threekit/react-three-fiber";
import { OrbitControls } from "@react-three/drei";
import { ChangeEvent, useEffect, useState } from "react";
import * as THREE from "three";
import MyScene from "./MyScene";
import React from "react";

let cameras: THREE.PerspectiveCamera[] = [];

export default function Player() {
  const auth = {
    orgId: "YOUR_ORG_ID",
    host: "preview.threekit.com",
    publicToken: "YOUR_PUBLIC_TOKEN",
  };
  
  const [scene, setScene] = useState<THREE.Object3D | null>(null);
  const [camTarget, setCamTarget] = useState<THREE.Object3D | null>(null);
  const [camera, setCamera] = useState<THREE.PerspectiveCamera | null>(null);

  useEffect(() => {
    if (scene) {
      scene.traverse((node) => {
        if (node.name === "CamTarget") {
          console.log("Found the target node:", node);
          setCamTarget(node);
        }
        if (node instanceof THREE.PerspectiveCamera) {
          if (cameras.includes(node)) return;
          console.log("found camera:", node);
          cameras.push(node);
        }
      });
    }
  }, [scene]);

  function handleCameraChange(e: ChangeEvent<HTMLSelectElement>) {
    const cam = cameras.find((cam) => cam.name === e.target.value);
    if (!cam) return;
    setCamera(cam);
  }
  
  return (
    <>
      <div style={{ width: "100%", height: "100%" }}>
        <Viewer
          auth={auth}
          ui={true}
          resolver={UnifiedResolver({
            cacheScope: "v1",
            optimize: false,
          })}
        >
          {scene ? (
            camTarget ? (
              <OrbitControls target={camTarget.position} />
            ) : (
              <OrbitControls />
            )
          ) : null}
          <MyScene
            assetId={"b1571ea4-e7f3-400f-ba58-b875b382a4a5"}
            camera={camera}
            ref={setScene}
          />
        </Viewer>
      </div>
      <div className="pt-3">
        <label htmlFor="options">Choose a camera:</label>
        <select id="options" onChange={(e) => handleCameraChange(e)}>
          {cameras.map((cam) => (
            <React.Fragment key={cam.name}>
              <option value={cam.name}>{cam.name}</option>
            </React.Fragment>
          ))}
        </select>
      </div>
    </>
  );
}

Passing Configuration

The configuration prop on the <Scene> and <Asset> components, as well as on the useAsset hook will help determine which gltf file is going to be loaded.

The recommended workflow would be to use the Composable Configurator's features in conjunction with the configuration prop on these asset components.

For example:

  1. Declaring a const to load the configuration: const product = useConfigurator().
  2. Assign product.configuration to the configuration prop on the <Scene> or <Asset> components.
  3. Setting the configuration using product.setConfiguration(), based on choices made in the UI.
  4. Using the product.setConfiguration() method will update the internal state of product.configuration, which will in turn trigger a reload of the components with a new gltf model request.

PostProcessing

Adding post processing to the scene can be a common step with a lot of implementations. By default, the Viewer component will have Linear tonemapping added with default antialiasing.

In order to add effects like SAO, Bloom, Tonemapping, ColorCorrections, and different Aliasing modes we would need to insert a component as a child of the Viewer.

📘

Tip:

Please note that adding this post processing effect will override the background of the Viewer's parent <div> element. The <StandardPostProcessing> component has its own background prop for controlling the background color and transparency of the Viewer.

Here is an example of how we can add postProcessing to our example from above, to match more closely to the visuals seen in the Platform. This example sets the transparency of the Viewer to full transparency using the alpha: 0 option.

<Viewer
  auth={auth}
  ui={true}
  resolver={UnifiedResolver({
    cacheScope: "v1",
    optimize: false,
  })}
>
  {scene ? (
    camTarget ? (
      <OrbitControls target={camTarget.position} />
    ) : (
      <OrbitControls />
    )
  ) : null}
  <MyScene
    assetId={"b1571ea4-e7f3-400f-ba58-b875b382a4a5"}
    camera={camera}
    ref={setScene}
  />
  <StandardPostProcessing
    sao={{ intensity: 0 }}
    bloom={{ intensity: 0 }}
    toneMapping={{ mode: ToneMappingMode.Uncharted }}
    background={{ alpha: 0 }}
    antiAliasing={{ mode: AntiAliasingMode.MSAA, msaaSamples: 4 }}
  />
</Viewer>

🚧

Warning!

The PostEffects used by the composable player are slightly different from the ones used by the Classic Player. Thus, you will likely notice small differences in visuals between the player view in the Platform vs what the composable player produces.

Player Snapshots

In order take snapshots of the player with a given configuration, please follow the instructions on the Snapshots page.

Conclusion

It will prove very helpful to learn more about react-three-fiber and Three.JS, as this will open up many more possibilities in terms of enhancing the player experience.