Looking for ideas

I am creating obstacles based on a grid as you can see on the image. The obstacle is draggable to change its height. There are 2 types of obstacles one is relative where each cube is raised the same amount. And non relative, where each cube is raised a different amount to form a flat top. My implementation is to to keep an array of cells with position and dimensions. Based on this array I create using InstancedMesh all the cubes and use transformation to place them on the right place and scale them to needed height. The problem is that for dragging and large obstacles with 2000+ cells im hitting performance problems. I just cannot think of a way to raise the obstacle how it needs to be raised without iterating through all of the cells in the array and do the calculations.
If anyone has done something similar, any info will be appreciated.

1 Like

Maybe BVH: three.js examples

1 Like

I think you have too many draw calls, you probably need to draw the group as one.

op said he’s instancing them already.

sounds like just the javascript side update traversal of OPs datastructure to recompute the transforms, that is chugging them.

1 Like

OP post some code… otherwise it will be harder to help you/diagnose.

What does your loop to recompute the boxes and set their .instanceMatrix’s look like?

1 Like

2000 instances as boxes sounds rapid

1 Like

Tried it with InstancedBufferGeometry.
Picture:


Demo: https://codepen.io/prisoner849/full/QwLyzbd

4 Likes

Beautifully elegant :ok_hand:

2 Likes

Wow this really looks amazing. I am having trouble understanding the shader code, as I have no experience in shaders, so Ill have to start learning, but do you think your implementation would work well with dragging?

I will show my component here. I think the main problem is that when I start dragging, the changeObstacleHeight function is running on every drag function call, and this drag function call happens 20-100 times per sec. Also I have to sync it with a mobx store.

import { Instances, Instance } from '@react-three/drei';
import { Map3DObstacleProps } from './map-3d-obstacle.types';
import { useMemo, useRef } from 'react';
import { BoxGeometry, DoubleSide, EdgesGeometry, Group, Mesh } from 'three';
import {
  darkenColor,
  useObstacleBoundingBox,
  useObstacleDragAttributes
} from './map-3d-obstacle.utils';
import { Cube, ObstacleType } from 'models';
import { toCalendarColor, useSyncedState } from 'utilities';
import {
  FilterFunctionType,
  markHiddenCubesForNonRelativeObstacles,
  updateCubeHeight,
  updateCubePosition
} from 'stores/state-ui';
import { Map3DObstacleHighlight } from 'components/atoms';
import { MeshPhongMaterialProps } from '@react-three/fiber';

const Map3DObstacleComponent = (props: Map3DObstacleProps): JSX.Element => {
  // Commented props might be used later, if not they will be removed
  const {
    type,
    color: entityColor,
    // description,
    // name,
    // radius,
    selected,
    highlighted,
    grayedOut,
    cubes,
    isBeingEdited,
    // elevationArray,
    // changeObstacleHeight,
    enableCameraControls,
    disableCameraControls,
    onClick,
    onPointerOver,
    onPointerOut,
    setCubes,
    setRelativeHeight,
    setNonRelativeHeight,
    obstacleHeight,
    shrinkOverlappingObstacles
  } = props;

  const groupRef = useRef<Group>(null);
  const boundingBoxRef = useRef<Mesh>(null);
  const [localCubes, setLocalCubes] = useSyncedState<Cube[]>(cubes);

  const changeObstacleHeight = (deltaHeight: number): void => {
    let targetTopHeight: number;
    let cubes: Cube[];

    switch (type) {
      case ObstacleType.EXTEND:
      case ObstacleType.OVERRIDE: {
        const lowestTop = Math.min(
          ...localCubes.map((cube) => cube.position[2] + cube.dimensions[2] / 2)
        );
        targetTopHeight = lowestTop + deltaHeight;
        cubes = localCubes.map((cube) => {
          const currentTop = cube.position[2] + cube.dimensions[2] / 2;
          const requiredIncrease = targetTopHeight - currentTop;
          return updateCubeHeight(cube, requiredIncrease);
        });

        if (type === ObstacleType.EXTEND) {
          cubes = markHiddenCubesForNonRelativeObstacles(
            cubes,
            FilterFunctionType.EXTEND
          );
        }
        break;
      }
      case ObstacleType.SHRINK:
        cubes = localCubes.map((cube) => {
          return updateCubePosition(cube, deltaHeight);
        });
        break;
      case ObstacleType.RELATIVE:
      default:
        // obstacleHeight += deltaHeight;
        cubes = localCubes.map((cube) => updateCubeHeight(cube, deltaHeight));
        break;
    }

    setLocalCubes(cubes);
  };

  const onDragEnd = () => {
    switch (type) {
      case ObstacleType.RELATIVE:
        //Remove 1 to account for default cube starting height
        //which can be found in create-obstacle-store
        setRelativeHeight(localCubes[0].dimensions[2] - 1);
        break;
      case ObstacleType.EXTEND:
      case ObstacleType.SHRINK:
      case ObstacleType.OVERRIDE:
        const maxTop = Math.max(
          ...localCubes.map((cube) => cube.position[2] + cube.dimensions[2] / 2)
        );
        setNonRelativeHeight(maxTop);

        if (type === ObstacleType.SHRINK) {
          shrinkOverlappingObstacles();
        }
        break;
    }
    setCubes(localCubes);
  };

  useObstacleBoundingBox(localCubes, obstacleHeight, groupRef, boundingBoxRef);

  const dragAttributes = useObstacleDragAttributes(
    boundingBoxRef,
    changeObstacleHeight,
    onDragEnd,
    disableCameraControls,
    enableCameraControls
  );

  const attributesToSpread = useMemo(() => {
    return isBeingEdited && type !== ObstacleType.RESET ? dragAttributes : [];
  }, [isBeingEdited, dragAttributes, type]);

  const edgesGeometry = useMemo(() => {
    const blockBox = new BoxGeometry(1, 1, 1);
    return new EdgesGeometry(blockBox);
  }, []);
  const color = useMemo((): string => {
    return toCalendarColor(entityColor);
  }, [entityColor]);

  const showHighlight = useMemo<boolean>(
    () => highlighted || selected,
    [highlighted, selected]
  );

  const materialProps = useMemo<MeshPhongMaterialProps>(() => {
    const baseColor = grayedOut ? 'gray' : color;
    return {
      color: baseColor,
      transparent: true,
      opacity: showHighlight ? 0.9 : 0.8,
      emissive: showHighlight ? baseColor : '#000000',
      emissiveIntensity: 1,
      side: DoubleSide
    };
  }, [grayedOut, color, showHighlight]);

  return (
    <group ref={groupRef}>
      <Instances limit={localCubes.length}>
        <boxGeometry args={[1, 1, 1]} />
        <meshPhongMaterial {...materialProps} colorWrite={false} />
        {localCubes.map((cube, index) => (
          <Instance
            key={index}
            scale={cube.hidden ? [0.00001, 0.00001, 0.00001] : cube.dimensions}
            position={cube.hidden ? [0, 0, 0] : cube.position}
            color={cube.dark && !grayedOut ? darkenColor(color) : undefined}
          />
        ))}
      </Instances>
      <Instances limit={localCubes.length}>
        <boxGeometry args={[1, 1, 1]} />
        <meshPhongMaterial {...materialProps} colorWrite={true} />
        {localCubes.map((cube, index) => (
          <Instance
            key={index}
            scale={cube.hidden ? [0.00001, 0.00001, 0.00001] : cube.dimensions}
            position={cube.hidden ? [0, 0, 0] : cube.position}
            color={cube.dark && !grayedOut ? darkenColor(color) : undefined}
          >
            <lineSegments geometry={edgesGeometry}>
              <lineBasicMaterial color='black' />
            </lineSegments>
          </Instance>
        ))}
      </Instances>

      <mesh
        {...attributesToSpread}
        ref={boundingBoxRef}
        onPointerOver={onPointerOver}
        onPointerOut={onPointerOut}
        onClick={onClick}
        visible={false}
      >
        <boxGeometry args={[1, 1, 1]} />
        <meshBasicMaterial />
      </mesh>

      {showHighlight && <Map3DObstacleHighlight cubes={localCubes} />}
    </group>
  );
};

const Map3DObstacle = Map3DObstacleComponent;

export { Map3DObstacle };

I noticed that in your implementation, the obstacle is part of the grid.(correct me if im wrong). I need the obstacle to be a separate entity from the underlying grid, as it will have to support hover,click and drag