react-three-renderer icon indicating copy to clipboard operation
react-three-renderer copied to clipboard

externaly loaded model - how use a apply THREE.BufferGeometry from the loaded model

Open webmato opened this issue 8 years ago • 13 comments

I use externaly loaded models via the THREE.OBJLoader. From the loader I get the the object THREE.Group with several THREE.Mesh objects. Each THREE.Mesh object has a geometry:THREE.BufferGeometry property with this attributes

THREE.Mesh : {
   ...
   geometry:THREE.BufferGeometry : {
      attributes:Object : {
         normal:THREE.BufferAttribute : {
            array:Float32Array,
            ...
         },
         position:THREE.BufferAttribute : {
            array:Float32Array,
            ...
         },
         uv:THREE.BufferAttribute : {
            array:Float32Array,
            ...
         },
         ...
      },
      ...
   }, 
   ...
}

How to handle it in react-three-renderer? How can I make a mesh react element from this? I dont know how to use the THREE.BufferGeometry.

webmato avatar Apr 08 '16 13:04 webmato

I will write a guide for this, but basically you can create a <group ref='group'/> within a component, and in that component's componentDidMount callback, this.refs.group will refer to that group object. You can then say this.refs.group.add(theResultOfOBJLoader). And in the componentWillUnmount callback, you can do this.refs.group.remove(theResultOfOBJLoader).

toxicFork avatar Apr 10 '16 21:04 toxicFork

Going to leave this here because I had the same problem, and this issue helped me resolve it. Just using the method @toxicFork used above, here's the actual code. It's just a combination of THREE examples and this issue. Make sure you setup variables (PATH, OBJ_FILE, MTL_FILE) and have appropriate permissions to the files. onProgress and onError need to be defined as well.

This code loads the files into the THREE parsers, which you need to include because they don't come stock in THREE. You can find them here: OBJLoader.js and MTLLoader.js.

// MTL Loader
const mtlLoader = new THREE.MTLLoader();
mtlLoader.setBaseUrl(PATH);
mtlLoader.setPath(PATH); // One of these might not be needed
mtlLoader.crossOrigin = '*'; // Use as needed
mtlLoader.load(MTL_FILE, materials => {
    materials.preload();
    // OBJ Loader
    const objLoader = new THREE.OBJLoader();
    objLoader.setMaterials(materials);
    objLoader.setPath(PATH);
    objLoader.load(OBJ_FILE, object => {
        for(let child of object.children) {
            child.material.side = THREE.DoubleSide
        }
                // Simple example to load the file
        const YourComponent = (
            <Something
                loadedObject={object}
            />
        );
                // this comes from import {render} from 'react-dom'
        render(YourComponent, document.getElementById('your-container'));
    }, onProgress, onError);
});

And then in your React component, you just use the method above.

componentDidMount() {
    this.refs.group.add(this.props.loadedObject);
}

componentWillUnmount() {
    this.refs.group.remove(this.props.loadedObject);
}

The loaded data will go wherever you put this in your render() return:

<group ref='group' />

cwlsn avatar May 10 '16 14:05 cwlsn

I think this method works fine to load the object into an existing group. But the issue started to become more complex when I tried to do raycasting on a loaded OBJ for mouse event detection. So, following the DraggableCubes example and considering the above case you guys are discussing, you end up with the following structure:

  • group (with MouseEnter, MouseDown,...)
    • group (group from OBJ)
      • [array of meshes]

Following the Ray casting strategy, I was looking closely inside the dispatchEvent method of EventDispatcher, particularly this line and the object that is being passed, whenever an intersection happens is one of the meshes, not the top level group who holds the eventCallbacks inside the userData object.

Since the handlers are attached in the top container group (not attached to every single mesh of your loaded OBJ), this method will be dispatching an event over a object where the handlers are not attached.

I wanted to ask if there is a easy way to solve this. The ugliest solution that I can think of is to force to pass this: [mesh].parent.parent to the dispatch.

Here a reference to the mesh component example link

kuakman avatar Nov 09 '16 19:11 kuakman

@toxicFork did you ever write a guide for this? Perhaps in the future I can take the guide and create a component that loads files from a file name.

roshkins avatar May 31 '17 17:05 roshkins

This code loads the files into the THREE parsers, which you need to include because they don't come stock in THREE.

When I include these eg.

import * as THREE from 'three';
import * as objLoad from 'three/examples/js/loaders/OBJLoader';

const objectLoader = new objLoad.OBJLoader();

it doesn't work because THREE is undefined in OBJLoader.js

What is the correct way in React of including the parsers so they can be used?

Do I need to compile it in somehow?

jamesheazlewood avatar Jun 22 '17 10:06 jamesheazlewood

@jamesheazlewood Have you tried a similar method to the example here?

cwlsn avatar Jun 22 '17 15:06 cwlsn

@axiomaticdesign I had a look at that and I just wasn't sure which is best practice. Looks like that is the only way. Thanks for the fast reply.

jamesheazlewood avatar Jun 23 '17 02:06 jamesheazlewood

I wanted to ask if there is a easy way to solve this. The ugliest solution that I can think of is to force to pass this: [mesh].parent.parent to the dispatch.

@kuakman , Did you manage to crack this? Or come up with a more elegant solution? I too am having trouble handling selection states for these nested meshes.

kappa-gooner avatar Jul 06 '17 01:07 kappa-gooner

I took a different approach to this and arrived at a solution that extends the internal components to allow the OBJ's Object3D to be inserted without using <group> and a ref. The basic idea is to subclass Object3DDescriptor (the backing class for <object3D>) and add the object property, allowing the element's content to be swapped out:

import * as THREE from 'three';
import React3 from 'react-three-renderer';
import Object3DDescriptor from 'react-three-renderer/lib/descriptors/Object/Object3DDescriptor';
import propTypeInstanceOf from 'react-three-renderer/lib/utils/propTypeInstanceOf';

class Object3DCustom extends Object3DDescriptor {
  constructor(react3Instance) {
    super(react3Instance);

    this.hasProp('object', {
      type: propTypeInstanceOf(THREE.Object3D),
      update(threeObject, object) {
        // Completely copy `object` (the model) into the backing Object3D instance
        threeObject.copy(object);
      },
      default: new THREE.Object3D(),
    });
  }
}

export default Object3DCustom;

In an ideal world you'd be able to use this directly in your JSX, like <Object3DCustom object={object}>, but it's a R3R internal component so it needs to be registered first on the react3Renderer instance. In the component where you create that:

class Scene extends React.Component {
  componentDidMount() {
    // Load your model here ...
    objLoader.load(url, object => {
      this.setState({ object });
    });
  }
  
  componentWillReceiveProps(nextProps) {
    const renderer = this.refs.react3.react3Renderer;

    // Just once, monkey-patch internal component descriptors to include any custom ones
    if (!renderer.threeElementDescriptors.__customEnabled) {
      renderer.threeElementDescriptors.object3DCustom = new Object3DCustom(renderer);
      this.setState({ customObjectsEnabled: true });
      renderer.threeElementDescriptors.__customEnabled = true;
    }
  }

  render() {
    return (
      <React3
        ref="react3"
      >
        {this.state.customObjectsEnabled && this.state.object && <object3DCustom object={this.state.object} />}
      </React3>
    );
  }
}

The __customEnabled bit is a hack, and I hope to improve that over time. One possibility/improvement is that R3R could be instantiated ahead of time, before the React3 component mounts. Currently, the react3Renderer is a singleton, but it's created in the componentDidMount method. This could be cleaned up and done before the initial app render if React3 were extended to take one as an optional parameter.

Hope that's useful for someone.

bosgood avatar Jul 20 '17 20:07 bosgood

@bosgood I keep running into this error using your method:

Uncaught TypeError: Cannot read property 'on' of undefined
    at React3DInstance.objectMounted (React3Instance.js?02b4:881)
    at React3DInstance.objectMounted (React3Instance.js?02b4:905)
    at SceneDescriptor.setParent (THREEElementDescriptor.js?5394:308)
    at InternalComponent.createChild (InternalComponent.js?9ffb:795)
    at InternalComponent._mountChildAtIndex (ReactMultiChild.js?e1f8:424)
    at InternalComponent._updateChildren (InternalComponent.js?9ffb:754)
    at InternalComponent.updateChildren (ReactMultiChild.js?e1f8:295)
    at InternalComponent._updateChildrenObjects (InternalComponent.js?9ffb:522)
    at InternalComponent.updateComponent (InternalComponent.js?9ffb:507)
    at InternalComponent.receiveComponent (InternalComponent.js?9ffb:470)

What the objectMounted code does there is:

      if (process.env.NODE_ENV !== 'production' || process.env.ENABLE_REACT_ADDON_HOOKS === 'true') {
        object.userData.events.on('highlight', this._objectHighlighted);
      }

The problem is, unlike with other methods, object.userData.events is simply undefined when I use it like you suggested. I guess one must somehow put the object.userData through some mechanism that adds the events property..? Or was OBJLoader supposed to do that and something is fishy with my models?

It's too bad, as I basically like your approach.

Btw since it seems to work for you, here some ideas. Instead of using a string ref and componentWillReceiveProps, you should go for a callback function. Also, you don't need the __customEnabled hack - you are creating a new object3DCustom property anyways, and you can check for that:

export default class R3Patched extends Component {
    static propTypes = {
        children: PropTypes.node
    };

    state = {
        patched: false
    };

    render() {
        const { patched } = this.state;
        const { children, ...props } = this.props;
        return <React3 {...props} ref={this.patch} children={patched ? children : null} />;
    }

    patch = (react3Ref) => {
        if (react3Ref) {
            const renderer = react3Ref.react3Renderer;
            if (!renderer.threeElementDescriptors.object3DCustom) {
                renderer.threeElementDescriptors.object3DCustom = new Object3DCustom(renderer);
            }
            this.setState({ patched: true });
        }
    }
}

I don't think my error is related, I tried it your way first.

Using the <group ref={}/> suggested by @toxicFork in https://github.com/toxicFork/react-three-renderer/issues/57#issuecomment-208078848 works for me. It also seems to be not necessary to add a group - using <scene ref={}>...</scene> and adding to the scene seems to do the trick too. However, I'm not happy with the manual handling, maybe I will think of something..

loopmode avatar Sep 30 '17 13:09 loopmode

@bosgood did you ever make any improvements on this?

dav92lee avatar Oct 03 '17 15:10 dav92lee

@dav92lee I'm not actually using this library anymore because I've stopped working on the project that required it, so I haven't, sorry!

@loopmode Thanks for the different approach. I'm not 100% sure why I didn't get that same error. userData.events appears to be created in THREEElementDescriptor.js#L215, so you might make sure that applyInitialProps is getting called properly.

FWIW I was targeting react-three-renderer v3.0.1 when I originally wrote that workaround, so things may have changed a bit.

bosgood avatar Oct 03 '17 16:10 bosgood

Still work in progress, but I ended up with this approach, successfully:

import React, { PureComponent } from 'react';
import { PropTypes, THREE, autobind } from '@/libs';
export default class Model extends PureComponent {
    static propTypes = {
        children: PropTypes.node,
        model: PropTypes.object,
        material: PropTypes.instanceOf(THREE.Material),
        rotation: PropTypes.instanceOf(THREE.Euler),
        position: PropTypes.instanceOf(THREE.Vector3),
        scale: PropTypes.oneOfType([PropTypes.instanceOf(THREE.Vector3), PropTypes.number])
    };
    static defaultProps = {
        rotation: new THREE.Euler(0, 0, 0),
        position: new THREE.Vector3(0, 0, 0),
        scale: new THREE.Vector3(1, 1, 1)
    };
    constructor(props, context) {
        super(props, context);
        if (props.model) {
            this.model(props);
        }
    }
    shouldComponentUpdate() {
        return false;
    }
    componentWillReceiveProps(nextProps) {
        if (nextProps.model !== this.props.model) {
            if (this.props.model) {
                this._groupRef.remove(this.props.model);
            }
            if (nextProps.model) {
                this.model(nextProps);
                if (this._groupRef) {
                    this._groupRef.add(nextProps.model);
                }
            }
        } else {
            if (nextProps.rotation !== this.props.rotation) {
                this.rotation(nextProps.rotation);
            }
            if (nextProps.position !== this.props.position) {
                this.position(nextProps.position);
            }
        }
    }
    render() {
        return <group ref={this.handleRef} children={this.props.children} />;
    }
    @autobind
    handleRef(group) {
        const { model } = this.props;
        if (group && model) {
            group.add(model);
        }
        this._groupRef = group;
    }
    @autobind
    model({ model, material, position, rotation, scale }) {
        if (model) {
            model.traverse(child => {
                if (material && child instanceof THREE.Mesh) {
                    child.material = material;
                }

                if (child instanceof THREE.Mesh) {
                    const geo = new THREE.EdgesGeometry(child.geometry);
                    const mat = new THREE.LineBasicMaterial({ color: 0xffffff, linewidth: 1 });
                    const wireframe = new THREE.LineSegments(geo, mat);
                    child.add(wireframe);
                }
            });
            if (position !== undefined) this.position(position, model);
            if (rotation !== undefined) this.rotation(rotation, model);
            if (scale !== undefined) this.scale(scale, model);
            return model;
        }
    }
    @autobind
    rotation(rotation, model = this.props.model) {
        model.quaternion.setFromEuler(rotation);
    }
    @autobind
    position(position, model = this.props.model) {
        model.position.copy(position);
    }
    @autobind
    scale(scale, model = this.props.model) {
        if (typeof scale === 'number') {
            model.scale.x = model.scale.y = model.scale.z = scale;
        } else if (model.scale) {
            model.scale.copy(scale);
        } else {
            model.scale = scale;
        }
    }
}

I use it like this:

// using electron, so OBJLoader with require + parse instead of load, but should be the same thing
const model = this.objLoader.parse(require('@/renderer/models/gears/gear1.obj')), 

...
 <Model model={model} {...otherProps} />

Works fine so far. Feels weird to replicate stuff that's probably already in r3r tho (rotation, position etc manually handled). However, it's been my first couple days and I don'tknow r3r well yet, hope I can get rid of that.

Also I will try the events thing again as suggested by @bosgood

loopmode avatar Oct 03 '17 18:10 loopmode