brianzinn / react-babylonjs

React for Babylon 3D engine
https://brianzinn.github.io/react-babylonjs/
809 stars 102 forks source link

Reusing Model with same GLB file #305

Closed bdlb77 closed 6 months ago

bdlb77 commented 6 months ago

I'm attempting to create a Model with a clickable Mesh, and create multiple instances of the model (Thinking like Prefab in Unity and reusing prefab in the scene)

Currently, I have 2 separate .glb files and attempting to create 3 objects on scene. 2 from item1.glb and fromitem2.glb`..

what renders is 1 from each file, with seemingly the first object from item1.glb overriding the 2nd object

What would be the best way to set up how to use the Model and create "instance" objects based on this model?

Goal: I have reusable assets that I want to use and create a plethora of instances of those assets in scene

Here's a sample of what I'm trying to do:


const ClickableMesh: React.FC = ({ mesh, onClick }) => {
    const ref = useRef(mesh);

    useEffect(() => {
        ref.current = mesh;
    }, [mesh])

         useClick(() => {
        if (ref.current) {
            onClick(ref.current)
        }
    }, ref)

    return null;
}

const Item = (item) => {
    [loadedMeshes, setMeshes] = useState([]);

    const handleClick = (mesh) => console.log(`Mesh Name: ${mesh.name}`);

    const itemRef = useRef(null);

    const onModelLoaded = (model) => {
        setLoadedMeshes([...model.meshes] as Mesh)
    }

    return (
        <>
        <Suspense fallback={<box />}>
            <Model
                ref={itemRef}
                rootUrl={ASSET_FOLDER}
                sceneFileName={"item.glb"}
                name={item.name}
                position={item.coords}
                scaling={new Vector3(1, 1, 1)}
                onModelLoaded={onModelLoaded}

            />
        </Suspense>
        {loadedMeshes.map((mesh, idx) => <ClickableMesh key={idx} mesh={mesh} onClick={handleClick}/>)}
        </>
    )

}
brianzinn commented 6 months ago

Did you want to clone or instance your meshes? Also, you could cache-bust, but it’s harder on bandwidth.

bdlb77 commented 6 months ago

Probably Clone?.. What I want to do is use item.glb as a prefab-like entity and create item1 item2 item3 all with distinct positions, etc.

brianzinn commented 6 months ago

Will something like this meet your needs? https://github.com/brianzinn/react-babylonjs/issues/210

Since .glb is a single file then you should also be able to just preload/download and use data:;base64,{base64} as the source directly. Specify the file extension as .glb to have the correct loader: https://doc.babylonjs.com/features/featuresDeepDive/importers/loadingFileTypes#direct-loading-base64-encoded-models

There is a mechanism proposal upstream - that would simplify this as you could preload and apply: https://github.com/BabylonJS/Babylon.js/issues/14567

If you look at the model itself there's not a lot going on. You could probably write your own cache to manage those: https://github.com/brianzinn/react-babylonjs/blob/master/packages/react-babylonjs/src/customComponents/Model.tsx#L56C1-L56C1

Lastly, I don't think there is a way to load a model and not show it, so you may went to hide it and use as a source for cloning. You can disable/hide the model in the onModelLoaded callback, if you continue using "Model". You could tap into the underlying cache on your own as well.

bdlb77 commented 6 months ago

Thank you for the suggestions and guidance! I'll take a look at these resources

bdlb77 commented 6 months ago

super helpful! thank you @brianzinn

here's what the final component looks like with using useHover and useClick:

const Item: React.FC<Props> = ({itemName, position}) => {

    const scene = useScene();
    const material = new StandardMaterial("blueMat", scene!);
    material.diffuseColor = new Color3(0, 0, 1);

    const hoveredMat = new StandardMaterial("hoveredMat", scene!);
    hoveredMat.diffuseColor = new Color3(1, 0, 1);

    const itemRef = useRef<Mesh | null>(null);
    const [hovered, setHovered] = useState<boolean>(false);
    const [currentMat, setCurrentMat] = useState<Material | null>(material);

    const meshAssetResult = useAssetManager([{
        taskType: TaskType.Mesh,
        name: taskName,
        rootUrl: url,
        sceneFilename: itemName

    }]);

    const taskData = meshAssetResult.taskNameMap[taskName] as MeshAssetTask;
    const [_, itemMesh] = taskData.loadedMeshes as Mesh[];

    itemMesh.position = position;
    itemMesh.material = currentMat;

    useEffect(() => {
        itemRef.current = itemMesh;
        return () => {
            itemRef.current = null;
        };

    }, [itemRef]);

    useEffect(() => {
        setCurrentMat(hovered ? hoveredMat : material);
    }, [hovered]);

    useHover(
        () => setHovered(true),
        () => setHovered(false),
        itemRef
    )

    useClick(() => console.log("click"), itemRef);

    return (
        <>
            {hovered && (
                <adtFullscreenUi name="ui">
                    <rectangle
                    name="rect"
                    height="200px"
                    width="100px"
                    background="black">
                        <textBlock
                            text={`Hovering On ${itemName}`}
                            fontFamily={"FontAwesome"}
                            fontStyle={"bold"}
                            fontSize={20}
                            color="white"
                            textWrapping={TextWrapping.WordWrap}

                        />
                    </rectangle>
                </adtFullscreenUi >
            )}
        </>
    ) 
}
bdlb77 commented 6 months ago

so this allows me to take dataset of item names and positions.. map over them and render muleiple <Item />s with intractability.

I think I had to just wrap my head around understanding that the loadedMeshes brought in by the assetManager create their own mesh..

WIth performance, though, I imagine I'll have to use one of your recommended approaches with doing a "baseItem" component, and then cloning or something in a ChildItem that can be used based off of the initital asset loading BaseItem . I'll cross that bridge when I get there though :D