Creating Custom Effects

Learn how to create custom effects for your glTF models

While effectable-gltf comes with two built-in effects, you can create your own custom effects by following the same pattern. This guide will show you how to create custom effects using the same hooks and patterns as the built-in effects.

Effect Structure

Custom effects in effectable-gltf are React components that use the useEffectData hook to access the model's data. Here's the basic structure:

import { type FC } from "react";
import { useEffectData } from "@vantezzen/effectable-gltf";

interface MyCustomEffectProps {
  // Define your effect's props here
  someProp?: string;
}

export const MyCustomEffect: FC<MyCustomEffectProps> = ({ someProp }) => {
  const { root, meshes } = useEffectData();

  // Your effect implementation here

  return null;
};

Proper Cleanup and Unmounting

When creating effects, it's crucial to properly clean up resources when the effect is unmounted. This prevents memory leaks and ensures your effect doesn't leave any lingering changes to the model. Here are the key points to consider:

1. Using useEffect for Cleanup

Always use the cleanup function returned by useEffect to handle unmounting:

useEffect(() => {
  // Setup code here

  return () => {
    // Cleanup code here
  };
}, [dependencies]);

2. What to Clean Up

Common resources that need cleanup include:

  • Materials: Dispose of any custom materials you created
  • Geometries: Dispose of any custom geometries
  • Textures: Dispose of any loaded textures
  • Event Listeners: Remove any event listeners
  • Animations: Cancel any ongoing animations

3. Example of Proper Cleanup

Here's an example of an effect that properly cleans up after itself:

import { type FC, useEffect, useRef } from "react";
import { useEffectData } from "effectable-gltf";
import { MeshStandardMaterial, TextureLoader } from "three";

export const TextureEffect: FC<{ textureUrl: string }> = ({ textureUrl }) => {
  const { meshes } = useEffectData();
  const materialsRef = useRef<MeshStandardMaterial[]>([]);
  const originalMaterialsRef = useRef<THREE.Material[]>([]);
  const textureRef = useRef<THREE.Texture | null>(null);

  useEffect(() => {
    // Store original materials
    originalMaterialsRef.current = meshes.map((mesh) => mesh.material);

    // Load and apply texture
    const loader = new TextureLoader();
    textureRef.current = loader.load(textureUrl);

    // Create and apply new materials
    materialsRef.current = meshes.map((mesh) => {
      const material = new MeshStandardMaterial();
      material.map = textureRef.current;
      mesh.material = material;
      return material;
    });

    // Cleanup function
    return () => {
      // Restore original materials
      meshes.forEach((mesh, i) => {
        mesh.material = originalMaterialsRef.current[i];
      });

      // Dispose of created materials
      materialsRef.current.forEach((material) => {
        material.dispose();
      });
      materialsRef.current = [];

      // Dispose of texture
      if (textureRef.current) {
        textureRef.current.dispose();
        textureRef.current = null;
      }
    };
  }, [meshes, textureUrl]);

  return null;
};

4. React Component Effects

If your effect uses React components (like from @react-three/postprocessing), let React handle the cleanup:

import { type FC } from "react";
import { EffectComposer, Outline } from "@react-three/postprocessing";
import { useEffectData } from "effectable-gltf";

export const OutlineEffect: FC<{ color: string }> = ({ color }) => {
  const { meshes } = useEffectData();

  if (!meshes.length) return null;

  return (
    <EffectComposer autoClear={false}>
      <Outline selection={meshes} visibleEdgeColor={color} />
    </EffectComposer>
  );
};

5. Common Pitfalls to Avoid

  • Forgetting to Clean Up: Always include a cleanup function in your useEffect
  • Incomplete Cleanup: Make sure to clean up all resources you create
  • Missing Dependencies: Include all dependencies in the useEffect dependency array
  • State Persistence: Don't leave any state changes that should be temporary
  • Memory Leaks: Clear all refs and cancel all animations

Example: Creating a Simple Color Effect

Let's create a simple effect that changes the color of all meshes in the model. This example follows the same pattern as the OverlayEffect:

import { type FC, useEffect, useRef } from "react";
import { type ColorRepresentation, Mesh, MeshStandardMaterial } from "three";
import { useEffectData } from "@vantezzen/effectable-gltf";

interface ColorEffectProps {
  color: ColorRepresentation;
}

export const ColorEffect: FC<ColorEffectProps> = ({ color }) => {
  const { meshes } = useEffectData();
  const clonesRef = useRef<Mesh[]>([]);

  // Create clones of the meshes with the new color
  useEffect(() => {
    const clones: Mesh[] = [];

    meshes.forEach((mesh) => {
      const clone = mesh.clone() as Mesh;
      const mat = (clone.material as MeshStandardMaterial).clone();

      mat.color.set(color);
      clone.material = mat;
      clone.raycast = () => null; // Optional: prevent raycasting on the clone

      (mesh.parent ?? mesh).add(clone);
      clones.push(clone);
    });

    clonesRef.current = clones;

    // Cleanup function
    return () => {
      clones.forEach((c) => {
        c.parent?.remove(c);
        (c.material as MeshStandardMaterial).dispose();
      });
      clonesRef.current = [];
    };
  }, [meshes]);

  // Update color when it changes
  useEffect(() => {
    clonesRef.current.forEach((c) => {
      const mat = c.material as MeshStandardMaterial;
      mat.color.set(color);
      mat.needsUpdate = true;
    });
  }, [color]);

  return null;
};

Example: Creating a Post-Processing Effect

You can also create effects that use Three.js post-processing, similar to the OutlineEffect:

import { type FC } from "react";
import { type ColorRepresentation } from "three";
import { EffectComposer, BloomEffect } from "@react-three/postprocessing";
import { useEffectData } from "@vantezzen/effectable-gltf";

interface BloomEffectProps {
  intensity?: number;
  luminanceThreshold?: number;
}

export const CustomBloomEffect: FC<BloomEffectProps> = ({
  intensity = 1,
  luminanceThreshold = 0.9,
}) => {
  const { meshes } = useEffectData();
  if (!meshes.length) return null;

  return (
    <EffectComposer autoClear={false} multisampling={8}>
      <BloomEffect
        selection={meshes}
        intensity={intensity}
        luminanceThreshold={luminanceThreshold}
      />
    </EffectComposer>
  );
};

Using Your Custom Effect

Once you've created your custom effect, you can use it just like the built-in effects:

import { EffectableGltf } from "@vantezzen/effectable-gltf";
import { ColorEffect } from "./ColorEffect";

function MyScene() {
  return (
    <EffectableGltf src="/path/to/model.gltf">
      <ColorEffect color="blue" />
    </EffectableGltf>
  );
}

Best Practices

  1. Cleanup: Always clean up your effects when they unmount

    • Remove any added meshes
    • Dispose of any created materials
    • Remove any event listeners
  2. Performance: Consider performance implications

    • Use useRef to store references to created objects
    • Avoid unnecessary re-renders
    • Clean up resources properly
  3. TypeScript: Use proper types

    • Define prop interfaces
    • Use proper Three.js types
    • Handle null cases
  4. Error Handling: Handle edge cases

    • Check if meshes exist
    • Handle material type mismatches
    • Provide fallbacks when needed

Next Steps