Search code examples
reactjsreact-three-fiberreact-three-dreizustandclone-element

React Three Fiber Outline only selected model from an objectArray of meshes


I'm working on a R3F project with quite a bunch of models that are accessed through an object array. I want to highlight the currently selected model through the postprocessing EffectComposer, but that seems not to work since I load my models through mapping the array I add them to. I can only outline all or none.

Can somebody help me selecting only one model out of my model imports?

Some info:

This is the flow of adding models:

  1. All the models have a JSX file generated through https://gltf.pmnd.rs/
  2. I add models through a button in the sidepanel, adding them to an activeModules objectArray inside my zustand appStore.
  3. I map out the array and display the models using cloneElement for all the module names that are inside the activeModules array.

Models can be selected through adding them, or later on by onContextMenu or onPointerOver and deselected when selecting another model. I haven't worked on a deselect function yet. I set them as SelectedModule in a global state in the zustand store through accessing a custom id I set on each added module.

Ideally I'd like to enable the like this= . For now it's done through enabled={highlighted}, which is a filter of the activeModules where I check if the name is the currently selectedModule.

For what it's worth, the activeModules array looks like this in console, with the ref being the main link to the jsx file of the model (example below): enter image description here

Warning :P -- sorry if the code looks messy to some of you, I'm quite new to React and R3F

This is where I add and switch my modules:

import React, { cloneElement, useRef } from "react";
import { EffectComposer, Selection, Select, Outline } from "@react-three/postprocessing";
import { useAppStore } from "@/stores/appStore";

import Module_sidebox_10x10 from "./SofaParts/Module_sidebox_10x10";
import Module_sidebox_10x12 from "./SofaParts/Module_sidebox_10x12";
import Module_double_16x10 from "./SofaParts/Module_double_16x10";
import Module_double_20x10 from "./SofaParts/Module_double_20x10";
import Module_double_24x10 from "./SofaParts/Module_double_24x10";
import Module_8x10 from "./SofaParts/Module_8x10";
import Module_10x10 from "./SofaParts/Module_10x10";
import Module_12x10 from "./SofaParts/Module_12x10";
import Module_10x12 from "./SofaParts/Module_10x12";
import Module_12x12 from "./SofaParts/Module_12x12";
import Armless_7x10 from "./SofaParts/Armless_7x10";
import Armless_10x10 from "./SofaParts/Armless_10x10";
import Armless_12x12 from "./SofaParts/Armless_12x12";
import Sidebox_10x2_5 from "./SofaParts/Sidebox_10x2_5";
import Sidebox_12x2_5 from "./SofaParts/Sidebox_12x2_5";
import Sides_10x1_25 from "./SofaParts/Sides_10x1_25";
import Sides_10x2_5 from "./SofaParts/Sides_10x2_5";
import Sides_12x2_5 from "./SofaParts/Sides_12x2_5";

const parts = {
  Module_sidebox_10x10: <Module_sidebox_10x10 />,
  Module_sidebox_10x12: <Module_sidebox_10x12 />,
  Double_16x10: <Module_double_16x10 />,
  Double_20x10: <Module_double_20x10 />,
  Double_24x10: <Module_double_24x10 />,
  Module_8x10: <Module_8x10 />,
  Module_10x10: <Module_10x10 />,
  Module_12x10: <Module_12x10 />,
  Module_10x12: <Module_10x12 />,
  Module_12x12: <Module_12x12 />,
  Armless_7x10: <Armless_7x10 />,
  Armless_10x10: <Armless_10x10 />,
  Armless_12x12: <Armless_12x12 />,
  Sidebox_10x2_5: <Sidebox_10x2_5 />,
  Sidebox_12x2_5: <Sidebox_12x2_5 />,
  Sides_10x1_25: <Sides_10x1_25 />,
  Sides_10x2_5: <Sides_10x2_5 />,
  Sides_12x2_5: <Sides_12x2_5 />,
};

const PartSwitch = () => {
  const modelMesh = useRef();
  const active = useAppStore((state) => state.activeModules);
  const update = useAppStore((state) => state.update);
  const selectedModule = useAppStore((state) => state.selectedModule);
  const openDetails = useAppStore((state) => state.openDetails);
  const highlighted = active.filter((m) => m.name === selectedModule?.name);

  return (
    <>
      {active.map((m, index) => {
        const module = { ...m, id: index };
        const detailhandler = () => {
          if (selectedModule !== null && selectedModule.name === module.name) {
            update({ selectedModule: module });
            if (selectedModule !== null) {
              update({ openDetails: !openDetails });
            }
          } else {
            return;
          }
        };
        const selecthandler = () => {
          update({ selectedModule: module });
        };
        const onPointerOver = (e) => {
          e.stopPropagation();
          selecthandler();
        };
        const onPointerDown = (e) => {
          // Only the mesh closest to the camera will be processed
          e.stopPropagation();
          // You may optionally capture the target
          e.target.setPointerCapture(e.pointerId);
        };
        const onPointerUp = (e) => {
          e.stopPropagation();
          // Optionally release capture
          e.target.releasePointerCapture(e.pointerId);
        };
        return (
          <Selection key={index}>
            <EffectComposer multisampling={8} autoClear={false}>
              <Outline blur visibleEdgeColor="white" edgeStrength={2} width={500} />
            </EffectComposer>
            <mesh
              ref={modelMesh}
              position={[0, 30, 0]}
              boundingBox
              castShadow
              receiveShadow
              onContextMenu={detailhandler}
              onPointerDown={onPointerDown}
              onPointerUp={onPointerUp}
              onPointerOver={onPointerOver}
            >
              <Select enabled={highlighted}>
                {cloneElement(parts[module.name], {
                  id: index,
                  p: module.position,
                  r: module.rotation,
                  pillows: module.pillows,
                })}
              </Select>
            </mesh>
          </Selection>
        );
      })}
    </>
  );
};

export default PartSwitch;

Don't know if it is useful for the question, but this is an example of one of the model files:


import React, { useRef } from "react";
import { useLoader } from "@react-three/fiber";
import { DRACOLoader, GLTFLoader } from "three-stdlib";
import { useAppStore } from "@/stores/appStore";
import useModelDrag from "@/hooks/useModelDrag";
import Snappers from "./Snappers";
import { useGLTF } from "@react-three/drei";

export default function Armless_7x10({ id, p }) {
  const module = useRef();
  const texture = useAppStore((state) => state.texture);
  const modelNormalMap = useAppStore((state) => state.modelNormalMap);

  const { nodes } = useLoader(GLTFLoader, "./models/gltf/module_armless_7.3x10.glb", (loader) => {
    const dracoLoader = new DRACOLoader();
    dracoLoader.setDecoderPath("/draco/");
    loader.setDRACOLoader(dracoLoader);
  });

  const [bind] = useModelDrag(id);

  return (
    <group {...bind()} ref={module} dispose={null}>
      <Snappers id={id} module={module} position={p} visible={false} />
      <group name="isle">
        <mesh
          name="module_armless_73x10--stands"
          geometry={nodes["module_armless_73x10--stands"].geometry}
          material={nodes["module_armless_73x10--stands"].material}
        />
        <mesh
          name="module_armless_73x10--cushion"
          geometry={nodes["module_armless_73x10--cushion"].geometry}
        >
          <meshStandardMaterial attach="material" map={texture} normalMap={modelNormalMap} />
        </mesh>
        <mesh
          name="module_armless_73x10--body"
          geometry={nodes["module_armless_73x10--body"].geometry}
        >
          <meshStandardMaterial attach="material" map={texture} normalMap={modelNormalMap} />
        </mesh>
      </group>
    </group>
  );
}

useGLTF.preload("./models/gltf/module_armless_7.3x10.glb");


Solution

  • To outline elements the following structure must be present:

    1. The <Selection> component wraps the <EffectComposer> and the elements to be outlined.
    2. The elements to be outlined must be wrapped by a <Select> component

    Check out this example linked in the react-three-fiber documentation.

    Right now you are returning a new <Selection> and a new <EffectComposer> for every element in your active array. Furthermore the <Select> component isn’t wrapping your model mesh. To resolve this you can move the <Selection> and the <EffectComposer> out of your map function and wrap your mesh with the <Select> component.

    The return of your PartSwitch component could look like this:

    <Selection>
        <EffectComposer ...>
            <Outline ... />
        </EffectComposer>
        {active.map…
            // rest of your map function
            return (
                <Select enabled={highlighted}>
                    // model mesh
                </Select>
            )
        }
    </Selection>
    

    Here is a fork of the example from the documentation with a map function. Edit Selective outlines (forked)

    Hope this works for you!