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
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
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
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.
- In the example below we create a state variable called
scene
, and we pass its set function as theref
to the<MyScene>
component. - 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:
- Declaring a const to load the configuration:
const product = useConfigurator()
. - Assign
product.configuration
to theconfiguration
prop on the<Scene>
or<Asset>
components. - Setting the configuration using
product.setConfiguration()
, based on choices made in the UI. - Using the
product.setConfiguration()
method will update the internal state ofproduct.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 ownbackground
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.