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
-
Cleanup: Always clean up your effects when they unmount
- Remove any added meshes
- Dispose of any created materials
- Remove any event listeners
-
Performance: Consider performance implications
- Use
useRef
to store references to created objects - Avoid unnecessary re-renders
- Clean up resources properly
- Use
-
TypeScript: Use proper types
- Define prop interfaces
- Use proper Three.js types
- Handle null cases
-
Error Handling: Handle edge cases
- Check if meshes exist
- Handle material type mismatches
- Provide fallbacks when needed
Next Steps
- Check out the API Reference for more details about the available hooks and types
- Look at the source code of OverlayEffect and OutlineEffect for more examples