diff --git a/api/material.js b/api/material.js index d375473b..92c6b452 100644 --- a/api/material.js +++ b/api/material.js @@ -1,7 +1,25 @@ let flock; +const materialCache = new Map(); +const materialCacheStats = { hits: 0, misses: 0 }; + +const buildMaterialCacheKey = ({ materialName, color, alpha, shaderType }) => { + const normalizedAlpha = alpha ?? 1; + const normalizeColour = (value) => + value == null ? "" : flock.getColorFromString(value); + + const normalizedColour = Array.isArray(color) + ? color.map(normalizeColour).join("|") + : normalizeColour(color); + + return [shaderType || "standard", materialName || "", normalizedColour, normalizedAlpha].join( + "::", + ); +}; + export function setFlockReference(ref) { flock = ref; + flock.materialCacheStats = materialCacheStats; } export const flockMaterial = { @@ -14,11 +32,14 @@ export const flockMaterial = { const bakedShapes = new Set(["Box", "Sphere", "Cylinder", "Capsule", "Plane"]); if (shapeType && bakedShapes.has(shapeType)) return; - const tex = - material.diffuseTexture || - material.albedoTexture || - material.baseTexture || - null; + const textureProp = material.diffuseTexture + ? "diffuseTexture" + : material.albedoTexture + ? "albedoTexture" + : material.baseTexture + ? "baseTexture" + : null; + let tex = textureProp ? material[textureProp] : null; if (!tex || typeof tex.uScale !== "number" || typeof tex.vScale !== "number") { return; @@ -49,8 +70,43 @@ export const flockMaterial = { const newUScale = worldWidth / tile; const newVScale = Math.max(worldHeight, worldDepth) / tile; + const needsTiling = + (Number.isFinite(newUScale) && newUScale > 0 && tex.uScale !== newUScale) || + (Number.isFinite(newVScale) && newVScale > 0 && tex.vScale !== newVScale); + + if (needsTiling && material.metadata?.sharedMaterial) { + const clonedMaterial = + typeof material.clone === "function" + ? material.clone( + `${material.name || "material"}_autoTile_${mesh?.id ?? mesh?.uniqueId ?? Date.now()}`, + ) + : null; + + if (clonedMaterial) { + clonedMaterial.metadata = { + ...(material.metadata || {}), + sharedMaterial: false, + internal: true, + }; + + const clonedTexture = tex?.clone?.() || null; + if (textureProp && clonedTexture) { + clonedMaterial[textureProp] = clonedTexture; + tex = clonedTexture; + } + + mesh.material = clonedMaterial; + material = clonedMaterial; + } else { + // Skip tiling when we cannot safely detach a shared material. + return material; + } + } + if (Number.isFinite(newUScale) && newUScale > 0) tex.uScale = newUScale; if (Number.isFinite(newVScale) && newVScale > 0) tex.vScale = newVScale; + + return material; }, adjustMaterialTilingForHierarchy(mesh, unitsPerTile) { if (!mesh) return; @@ -846,11 +902,42 @@ export const flockMaterial = { }, createMaterial({ color, materialName, alpha } = {}) { if (flock?.materialsDebug) console.log(`Create material: ${materialName}`); + const isTwoColor = Array.isArray(color) && color.length === 2; + const shaderType = isTwoColor + ? materialName === "none.png" + ? "gradient" + : "colorReplace" + : "standard"; + const resolvedAlpha = alpha ?? 1; + + const cacheKey = buildMaterialCacheKey({ + materialName, + color, + alpha: resolvedAlpha, + shaderType, + }); + + if (materialCache.has(cacheKey)) { + materialCacheStats.hits += 1; + const cachedMaterial = materialCache.get(cacheKey); + cachedMaterial.metadata = { + ...(cachedMaterial.metadata || {}), + sharedMaterial: true, + internal: true, + }; + if (flock?.materialsDebug) + console.log( + `Cache hit for material ${materialName}: ${materialCacheStats.hits} hits, ${materialCacheStats.misses} misses`, + ); + return cachedMaterial; + } + + materialCacheStats.misses += 1; let material; - const texturePath = flock.texturePath + materialName; + const texturePath = materialName ? flock.texturePath + materialName : null; // Handle two-color case - if (Array.isArray(color) && color.length === 2) { + if (isTwoColor) { // Use gradient for Flat material if (materialName === "none.png") { material = new flock.GradientMaterial(materialName, flock.scene); @@ -896,15 +983,26 @@ export const flockMaterial = { material.backFaceCulling = false; } - material.alpha = alpha; + material.alpha = resolvedAlpha; // Update alpha for shader materials - if (material.setFloat && alpha !== undefined) { - material.setFloat("alpha", alpha); + if (material.setFloat && resolvedAlpha !== undefined) { + material.setFloat("alpha", resolvedAlpha); } + material.metadata = { + ...(material.metadata || {}), + sharedMaterial: true, + internal: true, + cacheKey, + }; + + materialCache.set(cacheKey, material); + if (flock.materialsDebug) - console.log(`Created the material: ${material.name}`); + console.log( + `Created the material: ${material.name} (cache miss ${materialCacheStats.misses})`, + ); return material; }, createMultiColorGradientMaterial(name, colors) { @@ -1321,7 +1419,10 @@ export const flockMaterial = { const applyMaterialWithTilingIfAny = (m) => { mesh.material = m; - flock.adjustMaterialTilingToMesh(mesh, m); + const adjustedMaterial = flock.adjustMaterialTilingToMesh(mesh, m); + if (adjustedMaterial) { + mesh.material = adjustedMaterial; + } }; const clearVertexColors = () => {