Implementing Realistic Wear Effects in Three.js
Based on your code, I can see you're trying to apply wear effects like scratches, rust, and dirt to your models. The issue with your current approach is that the black background in your textures is showing up in the overlay.
Recommended Approach
Instead of creating separate overlay meshes, I recommend using a more integrated approach with the following techniques:
1. Use Alpha Maps for Wear Effects
For wear effects, you should use textures with alpha channels that define where the effect should appear. This way, you can blend the wear effect only where needed.
2. Modify the Original Material
Rather than creating separate meshes, consider modifying the original material or using a custom shader material that combines the base appearance with the wear effects.
Implementation Example
Here's a revised version of your component:
import React, { useEffect, useRef, useState } from 'react';
import { useThree } from '@react-three/fiber';
import * as THREE from 'three';
const TextureWearManager = ({ modelRef, mode, healthPercentage = 100 }) => {
const originalMaterials = useRef(new Map());
const [isInitialized, setIsInitialized] = useState(false);
const { scene } = useThree();
const loadAndProcessTexture = (url) => {
return new Promise((resolve) => {
const loader = new THREE.TextureLoader();
loader.load(url, (texture) => {
texture.colorSpace = THREE.SRGBColorSpace; // Updated from encoding
texture.flipY = false;
texture.repeat.set(0.5, 0.5);
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
texture.needsUpdate = true;
resolve(texture);
});
});
};
const initializeWearSystems = async () => {
const [scratchesTexture, rustTexture, dirtTexture] = await Promise.all([
loadAndProcessTexture('/scratches.jpg'),
loadAndProcessTexture('/Rust.png'),
loadAndProcessTexture('/Dirt.jpg')
]);
// Calculate wear intensity based on health
const wearIntensity = Math.max(0, 1 - (healthPercentage / 100));
const scratchesIntensity = wearIntensity * 0.7;
const rustIntensity = wearIntensity * 0.5;
const dirtIntensity = wearIntensity * 0.8;
const processObject = (object) => {
if (object.isMesh && object.material) {
// Store original material for cleanup
originalMaterials.current.set(object.uuid, object.material);
// Clone the original material to avoid affecting other objects
let newMaterial;
if (object.material.map) {
// If the material already has a texture, we'll use MeshStandardMaterial
newMaterial = new THREE.MeshStandardMaterial().copy(object.material);
// Add roughness map for scratches
newMaterial.roughnessMap = scratchesTexture;
newMaterial.roughness = 0.7 + scratchesIntensity * 0.3;
// Add metalness variation for rust
newMaterial.metalnessMap = rustTexture;
newMaterial.metalness = Math.max(0.1, object.material.metalness - (rustIntensity * 0.5));
// Add dirt as an additional texture in a custom shader
// This would require a custom shader implementation
// For simplicity, we'll use the aoMap as a stand-in for dirt
newMaterial.aoMap = dirtTexture;
newMaterial.aoMapIntensity = dirtIntensity;
} else {
// For objects without textures, create a basic material with the wear effects
newMaterial = new THREE.MeshStandardMaterial({
color: object.material.color,
roughnessMap: scratchesTexture,
roughness: 0.7 + scratchesIntensity * 0.3,
metalnessMap: rustTexture,
metalness: Math.max(0.1, object.material.metalness - (rustIntensity * 0.5)),
aoMap: dirtTexture,
aoMapIntensity: dirtIntensity
});
}
// Ensure all maps affect the material
newMaterial.needsUpdate = true;
// Apply the new material
object.material = newMaterial;
// Make sure the geometry has UV2 coordinates for aoMap
if (!object.geometry.hasAttribute('uv2') && object.geometry.hasAttribute('uv')) {
object.geometry.setAttribute('uv2', object.geometry.getAttribute('uv'));
}
}
};
if (mode === 'crafting') {
scene.traverse(processObject);
} else if (modelRef.current) {
modelRef.current.traverse(processObject);
}
setIsInitialized(true);
};
const cleanupWearSystems = () => {
// Restore original materials
originalMaterials.current.forEach((material, uuid) => {
const object = findObjectByUUID(uuid);
if (object) {
object.material = material;
}
});
originalMaterials.current.clear();
};
// Helper function to find an object by UUID
const findObjectByUUID = (uuid) => {
let foundObject = null;
const traverse = (obj) => {
if (obj.uuid === uuid) {
foundObject = obj;
return;
}
if (obj.children) {
obj.children.forEach(traverse);
}
};
if (mode === 'crafting') {
scene.traverse(traverse);
} else if (modelRef.current) {
modelRef.current.traverse(traverse);
}
return foundObject;
};
useEffect(() => {
if (!isInitialized) {
initializeWearSystems();
}
return () => cleanupWearSystems();
}, [healthPercentage]); // Re-apply when health changes
return null;
};
export default TextureWearManager;
Key Improvements
-
Material Modification: Instead of creating overlay meshes, we modify the original material to incorporate wear effects.
-
Using Appropriate Maps:
- Scratches as a roughness map
- Rust affecting metalness
- Dirt as an ambient occlusion map
-
Health-Based Intensity: The wear effects intensity is calculated based on the health percentage.
-
Original Material Restoration: The component stores and restores original materials for proper cleanup.
Advanced Options
For even more realistic results, you could:
-
Use Custom Shaders: Implement a custom shader that blends multiple wear effects with precise control.
-
Create Texture Masks: Use grayscale textures as masks to control where each effect appears.
-
Consider Post-Processing: For some effects, post-processing with EffectComposer might give better results.
-
Use Normal Maps: Add normal maps for scratches to create actual surface detail.
This approach should give you more realistic wear effects that integrate well with your existing materials without the black background issues.