How to apply postprocessing effects (Contrast, Saturation, Exposure) only to a model, not the entire canvas?

Hi Team

I’m working with React Three Fiber and have added postprocessing effects like:

  • Contrast
  • Saturation (HueSaturation)
  • Directional Light
  • Exposure (via tone mapping or HDR)

Currently, I’m applying these effects using <EffectComposer> with components like <BrightnessContrast /> and <HueSaturation />.

The issue is: these effects apply to the entire canvas, affecting the background, other 3D objects, and UI overlays.
But I want these effects to apply only to my GLB model — not to the entire scene.

My goal:

Apply these visual effects only to the loaded model (from GLTF), without affecting the rest of the canvas or scene.

What I’ve tried:

  • Wrapping only the model with <EffectComposer> (doesn’t isolate it).
  • Changing lighting and tone mapping — still affects entire scene.
  • No clear way to apply postprocessing only to a portion of the scene.

What I’m looking for:

  • A clean solution or pattern to isolate postprocessing effects (contrast, saturation, etc.) to only a specific object or group — ideally the model I’m loading.
  • Bonus: Any example using render targets or offscreen buffers (useFBO) to isolate and compose only that part with effects.

Would appreciate guidance or best practices!

Thanks in advance

And effects to model material

I need the contrast and saturation to apply for model not for entire canvas

{glbUrl && (
										<ErrorBoundary onError={handleModelError}>
											<Model
												key={modelKey}
												url={glbUrl}
												position={position}
												rotation={rotation}
												opacity={opacity}
												selectedScreen={selectedScreen}
												setAvailableScreens={setAvailableScreens}
												customScreenImage={customScreenImage}
												setScrubTime={setScrubTime}
												screenRefs={screenRefs}
												scrubTime={scrubTime}
												contrast={contrast}
												saturation={saturation}
												directionalIntensity={directionalIntensity}
												enableHDR={enableHDR}
												hdrIntensity={hdrIntensity}
												metalness={metalness}
												vignetteOffset={vignetteOffset}
												vignetteDarkness={vignetteDarkness}
											/>
										</ErrorBoundary>
									)}




// Apply contrast and saturation to all materials in the scene
useEffect(() => {
	if (ref.current) {
		console.log(
			`🎨 Applying contrast ${contrast}, saturation ${saturation}, vignetteOffset ${vignetteOffset}, and vignetteDarkness ${vignetteDarkness} to all materials`,
		);

		try {
			ref.current.traverse((child) => {
				if (child && child.isMesh && child.material) {
					// Handle single material or material array
					const materials = Array.isArray(child.material)
						? child.material
						: [child.material];

					materials.forEach((material) => {
						if (material && material.isMaterial) {
							// If the material doesn't have our custom uniforms yet, set them up
							if (!material.userData.hasContrastSaturation) {
								// Create uniforms for contrast, saturation, vignetteOffset, and vignetteDarkness
								material.userData.contrastUniform = { value: contrast * 2.0 };
								material.userData.saturationUniform = {
									value: saturation * 2.0,
								};
								material.userData.vignetteOffsetUniform = {
									value: vignetteOffset,
								};
								material.userData.vignetteDarknessUniform = {
									value: vignetteDarkness,
								};

								// Store original onBeforeCompile if it exists
								const originalOnBeforeCompile = material.onBeforeCompile;

								// Add our custom shader modification
								material.onBeforeCompile = (shader) => {
									// Call original onBeforeCompile if it exists
									if (originalOnBeforeCompile) {
										originalOnBeforeCompile(shader);
									}

									// Add our uniforms to the shader
									shader.uniforms.contrastValue =
										material.userData.contrastUniform;
									shader.uniforms.saturationValue =
										material.userData.saturationUniform;
									shader.uniforms.vignetteOffset =
										material.userData.vignetteOffsetUniform;
									shader.uniforms.vignetteDarkness =
										material.userData.vignetteDarknessUniform;

									// Add shader code for contrast, saturation, and vignette
									shader.fragmentShader = shader.fragmentShader.replace(
										"#include <output_fragment>",
										`
                #include <output_fragment>

                // BEGIN_CONTRAST_SATURATION
                uniform float contrastValue;
                uniform float saturationValue;
                uniform float vignetteOffset;
                uniform float vignetteDarkness;

                // Apply contrast
                vec3 midpoint = vec3(0.5);
                gl_FragColor.rgb = (gl_FragColor.rgb - midpoint) * contrastValue + midpoint;
                gl_FragColor.rgb = clamp(gl_FragColor.rgb, 0.0, 1.0);

                // Apply saturation
                vec3 gray = vec3(dot(gl_FragColor.rgb, vec3(0.299, 0.587, 0.114)));
                gl_FragColor.rgb = mix(gray, gl_FragColor.rgb, saturationValue);
                gl_FragColor.rgb = clamp(gl_FragColor.rgb, 0.0, 1.0);

                // Apply vignette effect
                vec2 center = vec2(0.5, 0.5);
                vec2 position = gl_FragCoord.xy / vec2(1920.0, 1080.0); // Approximate screen size
                float dist = distance(position, center);
                float vignette = smoothstep(vignetteOffset, 0.7, dist);
                gl_FragColor.rgb = mix(gl_FragColor.rgb, gl_FragColor.rgb * (1.0 - vignetteDarkness), vignette);
                // END_CONTRAST_SATURATION
                `,
									);
								};

								// Mark this material as having our contrast/saturation modifications
								material.userData.hasContrastSaturation = true;
								material.needsUpdate = true;
							} else {
								// Just update the uniform values
								if (material.userData.contrastUniform) {
									material.userData.contrastUniform.value = contrast * 2.0;
								}
								if (material.userData.saturationUniform) {
									material.userData.saturationUniform.value =
										saturation * 2.0;
								}
								if (material.userData.vignetteOffsetUniform) {
									material.userData.vignetteOffsetUniform.value =
										vignetteOffset;
								}
								if (material.userData.vignetteDarknessUniform) {
									material.userData.vignetteDarknessUniform.value =
										vignetteDarkness;
								}
							}
						}
					});
				}
			});

			console.log(
				"✅ Applied contrast, saturation, and vignette to all materials",
			);
		} catch (error) {
			console.error("Error applying effects:", error);
		}
	}
}, [contrast, saturation, vignetteOffset, vignetteDarkness]);

// Update all material uniforms when contrast, saturation, or vignette changes
useEffect(() => {
	console.log(
		`🔄 Updating all material uniforms - contrast: ${contrast}, saturation: ${saturation}, vignette: ${vignetteOffset}/${vignetteDarkness}`,
	);

	// Helper to update uniforms on a material
	const updateMaterialUniforms = (material) => {
		if (material && material.userData) {
			// Update contrast
			if (material.userData.contrastUniform) {
				material.userData.contrastUniform.value = contrast * 2.0;
			}

			// Update saturation
			if (material.userData.saturationUniform) {
				material.userData.saturationUniform.value = saturation * 2.0;
			}

			// Update vignette
			if (material.userData.vignetteOffsetUniform) {
				material.userData.vignetteOffsetUniform.value = vignetteOffset;
			}

			if (material.userData.vignetteDarknessUniform) {
				material.userData.vignetteDarknessUniform.value = vignetteDarkness;
			}

			// Force material update
			material.needsUpdate = true;
		}
	};

	// Update all materials in the scene
	if (ref.current) {
		ref.current.traverse((child) => {
			if (child && child.isMesh && child.material) {
				// Handle single material or material array
				const materials = Array.isArray(child.material)
					? child.material
					: [child.material];
				materials.forEach(updateMaterialUniforms);
			}
		});
	}

	// Update screen materials
	if (screenRefs.current) {
		Object.values(screenRefs.current).forEach((mesh) => {
			if (mesh && mesh.material) {
				updateMaterialUniforms(mesh.material);
			}
		});
	}
}, [contrast, saturation, vignetteOffset, vignetteDarkness]);