0

I'm experiencing a runtime error when using the three-loader-3dtiles library.

"Uncaught TypeError: can't access property 'elements", m is undefined". The error occurs when I try to update the runtime using the

  useFrame(({ size, camera }, dt) => {
    if (runtime) runtime.update(dt, size.height, camera);
  });

But If I comment out above line, the error goes away, but the 3D tiles are not rendered.

here is my code loader-3dtiles-rf3.tsx

import { Loader3DTiles, LoaderProps, Runtime } from 'three-loader-3dtiles';
import { useLoader, useThree, useFrame } from '@react-three/fiber';
import { Loader, Vector2 } from 'three';


class Loader3DTilesBridge extends Loader {
  props: LoaderProps;


  load(url, onLoad, onProgress, onError) {
    const loadTileset = async () => {
      try {
        const result = await Loader3DTiles.load({
          url,
          ...this.props,
          onProgress,
        });
        onLoad(result);
        console.log('result', result);
      } catch (e) {
        console.log('Error loading 3d tiles!', e);
        onError(e);
      }
    };
    loadTileset();
  }
  setProps(props) {
    this.props = props;
  }
}


function Loader3DTilesR3FAsset(props) {
  const threeState = useThree();
  const loaderProps = {
    renderer: threeState.gl,
    viewport: getViewport(threeState.gl),
    options: {
      ...props,
    },
  };


  // TODO: Getting type error
  // @ts-ignore
  const { model, runtime } = useLoader(Loader3DTilesBridge, props.url, (loader: Loader3DTilesBridge) => {
    loader.setProps(loaderProps);
  });


  useFrame(({ size, camera }, dt) => {
    if (runtime) runtime.update(dt, size.height, camera);
  });


  return (
    <group {...props} dispose={runtime.dispose}>
      <primitive object={model} />
    </group>
  );
}
function getViewport(renderer) {
  const viewSize = renderer.getSize(new Vector2());
  return {
    width: viewSize.x,
    height: viewSize.y,
    devicePixelRatio: renderer.getPixelRatio(),
  };
}


export { Loader3DTilesR3FAsset };

and I'm rendering in canvas but rf3 this way

<Canvas shadows style={{ background: '#272730' }}>
        <PerspectiveCamera ref={camera}>
          <Suspense fallback={null}>
            <Loader3DTilesR3FAsset
              dracoDecoderPath={'https://unpkg.com/[email protected]/examples/jsm/libs/draco'}
              basisTranscoderPath={'https://unpkg.com/[email protected]/examples/jsm/libs/basis'}
              rotation={new THREE.Euler(-Math.PI / 2, 0, 0)}
              url="https://int.nyt.com/data/3dscenes/ONA360/TILESET/0731_FREEMAN_ALLEY_10M_A_36x8K__10K-PN_50P_DB/tileset_tileset.json"
              maximumScreenSpaceError={48}
            />
          </Suspense>
        </PerspectiveCamera>
        <OrbitControls camera={camera.current} />
     </Canvas>
4
  • Any chance for Sandbox? Commented Jul 12 at 9:46
  • A minimal reproducible example would be appreciated (I think that's what the previous comment was asking for too). Commented Jul 12 at 11:31
  • You have a line marked with // @ts-ignore. What was the error that you're suppressing? Commented Jul 12 at 11:31
  • @ŁukaszDanielMastalerz @MattKantor here is a sandbox codesandbox.io/p/sandbox/kx86gz , caution it has an infinite loop , you can comment useFrame to disable it Commented Jul 12 at 16:09

1 Answer 1

0

Every time React re-renders Loader3DTilesR3FAsset, you create a completely new loaderProps object. useLoader and reloads the entire set of tiles, then React updates the state, and re-renders, and re-loads, hence you get an infinite loop. Try using useMemo which sets the object only when one of the dependencies actually changes. Otherwise, React reuses the same reference.

Also, use makeDefault in your camera so that you don't have to manually pass a reference to <OrbitControls>, Drei automatically selects the default camera.

https://github.com/pmndrs/react-three-fiber/discussions/1064#discussioncomment-433672

import React, { Suspense, useMemo } from "react";
import { Canvas, useLoader, useThree, useFrame } from "@react-three/fiber";
import { PerspectiveCamera, OrbitControls } from "@react-three/drei";
import { Loader as ThreeLoader, Vector2, WebGLRenderer } from "three";
import * as THREE from "three";
import { Loader3DTiles, LoaderProps, Runtime } from "three-loader-3dtiles";

class Loader3DTilesBridge extends ThreeLoader {
  private props!: Omit<LoaderProps, "url">;

  load(
    url: string,
    onLoad: (data: { model: THREE.Object3D; runtime: Runtime }) => void,
    onProgress?: (event: ProgressEvent) => void,
    onError?: (event: ErrorEvent) => void
  ) {
    (async () => {
      try {
        const { model, runtime } = await Loader3DTiles.load({
          url,
          ...this.props,
          onProgress,
        });
        onLoad({ model, runtime });
      } catch (e) {
        console.error("Error loading 3D Tiles:", e);
        onError?.(e as any);
      }
    })();
  }

  setProps(props: Omit<LoaderProps, "url">) {
    this.props = props;
  }
}

function getViewport(renderer: WebGLRenderer) {
  const size = renderer.getSize(new Vector2());
  return {
    width: size.x,
    height: size.y,
    devicePixelRatio: renderer.getPixelRatio(),
  };
}

type TilesAssetProps = {
  url: string;
  dracoDecoderPath: string;
  basisTranscoderPath: string;
  maximumScreenSpaceError?: number;
  rotation?: THREE.Euler;
  position?: [number, number, number];
  scale?: number;
};

function Loader3DTilesR3FAsset(props: TilesAssetProps) {
  const { gl } = useThree();

  const loaderProps = useMemo<Omit<LoaderProps, "url">>(
    () => ({
      renderer: gl,
      viewport: getViewport(gl),
      resetTransform: true, //look at https://github.com/nytimes/three-loader-3dtiles/pull/173
      options: {
        dracoDecoderPath: props.dracoDecoderPath,
        basisTranscoderPath: props.basisTranscoderPath,
        maximumScreenSpaceError: props.maximumScreenSpaceError,
      },
    }),
    [
      gl,
      props.dracoDecoderPath,
      props.basisTranscoderPath,
      props.maximumScreenSpaceError,
    ]
  );

  const { model, runtime } = useLoader(
    Loader3DTilesBridge,
    props.url,
    (loader: Loader3DTilesBridge) => loader.setProps(loaderProps)
  );

  useFrame((state, dt) => {
    runtime.update(dt, state.camera);
  });

  return (
    <group
      rotation={props.rotation}
      position={props.position}
      scale={props.scale}
      dispose={runtime.dispose}
    >
      <primitive object={model} />
    </group>
  );
}

const URL =
  "https://int.nyt.com/data/3dscenes/ONA360/TILESET/0731_FREEMAN_ALLEY_10M_A_36x8K__10K-PN_50P_DB/tileset_tileset.json";
const DRACO = "https://unpkg.com/[email protected]/examples/jsm/libs";
const ROTATION = new THREE.Euler(-Math.PI / 2, 0, 0);

const Scene: React.FC = () => {
  return (
    <Canvas shadows style={{ background: "#272730" }}>
      <PerspectiveCamera makeDefault />
      <Suspense fallback={null}>
        <Loader3DTilesR3FAsset
          url={URL}
          dracoDecoderPath={`${DRACO}/draco`}
          basisTranscoderPath={`${DRACO}/basis`}
          maximumScreenSpaceError={48}
          rotation={ROTATION}
        />
      </Suspense>
      <OrbitControls />
    </Canvas>
  );
};

export default Scene;
Sign up to request clarification or add additional context in comments.

1 Comment

That's thoughtful, in this case useMemo doesn't affect cause there is no outer state that causes a re-render. beside this runtime.update(dt, state.camera); here you removed state.size.height as a middle parameter, may I ask why?, with only two parameters ther is no console error, but nothing renders. And with height parameters, there is an infinite loop

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.