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

2

6

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

Schilit answered 17/3, 2022 at 13:1 Comment(1)
I have answered a question like this before. You can "use it as a react component", pass in event handlers and interact with it via ref. Accessing it via a ref gives you methods such as positionAndNormalFromPoint This is probably the only way to interact with it in React since it is not a "react" component. does this help? codesandbox.io/s/lingering-tree-d41cr?file=/src/App.jsFinzer
F
11
  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

Finzer answered 17/3, 2022 at 14:59 Comment(5)
Thanks this is a really great example. As mentioned in the question I've gone down the ref route before. Was hoping there were other ways around it. So far it doesn't seem like it though. I will mark this as solved after I do a few tests.Schilit
Update: This is the direction I ended up going with. It is not ideal, as there is a great deal of tiptoeing around updates and <model-viewer> doesn't have everything I need exposed outside of the attribute API.Schilit
@Schilit What is your approach now? Did you figure out ho to integrate model-viewer with react better?Assessor
@AlexanderP unfortunately not really. This is currently doing everything I need, just a bit cumbersome to work with. Have you had better luck?Schilit
Nope, I suppose this currently is the best approachAssessor
E
-1

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.

Ess answered 11/3, 2023 at 3:58 Comment(1)
The react-model-viewer package is a completely different one from what the OP is trying to useShaefer

© 2022 - 2024 — McMap. All rights reserved.