BorisKourt
BorisKourt

Reputation: 307

Integrating Google's <model-viewer> with React/Reagent

Google's <model-viewer> provides all the key features I need without having write a custom solution via something like react-three-fiber or directly in three.js.

I am struggling with how to properly integrate it into a Reagent (and React for that matter) structure.

In order to make it easy to use with vanilla JS, is built as a web component and is largely controlled via the attributes on its html element. Normally that wouldn't be much of a problem, but with the overhead of 3D and loading large models re-rendering this is expensive and in many cases functionality-breaking.

I've tried to naively use it inside a component and trying to eliminate the possibility of re-rendering. Using a ref to mutate it directly.

I have also tried setting it up as a manually created html element from the Reagent/React controlled application and reference it in various events via its dom element.

Both of these options introduced a lot of hacks and were not ideal in a single page application.

I am wondering if anyone has any tips on how to best wrap this in a React/Reagent shell, while still having access to the core element to use their underlying JS api.

Answers don't have to be in ClojureScript.


Here is the example of its usage from their page:

<model-viewer 
  alt="Neil Armstrong's Spacesuit from the Smithsonian Digitization Programs Office and National Air and Space Museum" 
  src="shared-assets/models/NeilArmstrong.glb" 
  ar ar-modes="webxr scene-viewer quick-look" environment-image="shared-assets/environments/moon_1k.hdr" 
  poster="shared-assets/models/NeilArmstrong.webp" 
  seamless-poster 
  shadow-intensity="1" 
  camera-controls>
</model-viewer>

Thanks for any tips.


Additional discussion on Clojurians Slack (link requires access to Slack)

Additional discussion on Clojureverse

Upvotes: 6

Views: 9186

Answers (2)

Admani
Admani

Reputation: 7

To use the <model-viewer> element in React, you can use the react-model-viewer library, which provides a React component wrapper around the <model-viewer> element.

First, you need to install react-model-viewer by running the following command in your project directory

npm install react-model-viewer

Then, in your React component file, you can import the ModelViewer component from react-model-viewer:

import React from 'react';
import { ModelViewer } from 'react-model-viewer';

const MyComponent = () => {
  return (
    <ModelViewer
      alt="Neil Armstrong's Spacesuit from the Smithsonian Digitization Programs Office and National Air and Space Museum"
      src="shared-assets/models/NeilArmstrong.glb"
      environmentImage="shared-assets/environments/moon_1k.hdr"
      poster="shared-assets/models/NeilArmstrong.webp"
      shadowIntensity={1}
      cameraControls
      touchAction="pan-y"
    />
  );
}

export default MyComponent;

Note that the ModelViewer component props are written in camelCase instead of kebab-case, so environment-image becomes environmentImage in the example you provided.

Upvotes: -1

GBourke
GBourke

Reputation: 1943

  1. Include it in your index.html as a module type script.
<!DOCTYPE html>
<html lang="en">
  <head>
....
    <script
      type="module"
      src="https://unpkg.com/@google/model-viewer/dist/model-viewer.min.js"
    ></script>
...

  </head>

  ...
</html>
  1. use it as an element in your react component
    import "./styles.css";
    import React, { useState } from "react";
    
    export default function App() {
      const modelRef = React.useRef();
    
      return (
        <model-viewer
          // className="model-viewer"
          src="./M08.glb"
          alt="A rock"
          exposure="0.008"
          camera-controls
          ar
          ar-modes="webxr"
          ref={(ref) => {
            modelRef.current = ref;
          }}
        >
        </model-viewer>
      );
    }
  1. interact with it

here is an interaction, where an onclick event translates screen coordinates to model coordinates using a function available from the model. It stores these annotations and renders them as children of the model. This is available in the documentation

import "./styles.css";
import React, { useState } from "react";

export default function App() {
  const modelRef = React.useRef();
  const [annots, setAnnots] = useState([]);

  const handleClick = (event) => {
    const { clientX, clientY } = event;

    if (modelRef.current) {
      let hit = modelRef.current.positionAndNormalFromPoint(clientX, clientY);
      if (hit) {
        setAnnots((annots) => {
          return [...annots, hit];
        });
      }
    }
  };

  const getDataPosition = (annot) => {
    return `${annot.position.x} ${annot.position.y} ${annot.position.z}`;
  };

  const getDataNormal = (annot) => {
    return `${annot.normal.x} ${annot.normal.y} ${annot.normal.z}`;
  };

  return (
    <model-viewer
      // className="model-viewer"
      src="./M08.glb"
      alt="A rock"
      exposure="0.008"
      camera-controls
      ar
      ar-modes="webxr"
      onClick={handleClick}
      ref={(ref) => {
        modelRef.current = ref;
      }}
    >
      {annots.map((annot, idx) => (
        <button
          key={`hotspot-${idx}`}
          className="view-button"
          slot={`hotspot-${idx}`}
          data-position={getDataPosition(annot)}
          data-normal={getDataNormal(annot)}
        ></button>
      ))}
    </model-viewer>
  );
}

Code Sandbox: https://codesandbox.io/s/lingering-tree-d41cr?file=/src/App.js:0-1287

Upvotes: 11

Related Questions