diff --git a/api/material.js b/api/material.js index cd4595268..aaa1d4a29 100644 --- a/api/material.js +++ b/api/material.js @@ -900,8 +900,8 @@ export const flockMaterial = { // Normalize single-element array to plain value if (Array.isArray(color) && color.length === 1) color = color[0]; - // Handle two-color case - if (Array.isArray(color) && color.length === 2) { + // Handle two-color case (extra colors beyond 2 are ignored) + if (Array.isArray(color) && color.length >= 2) { // Use gradient for Flat material if (materialName === "none.png") { material = new flock.GradientMaterial(materialName, flock.scene); @@ -989,8 +989,9 @@ export const flockMaterial = { }, ); - // Convert colors to Color3 array + // Convert colors to Color3 array (max 16, matching shader array size) const color3Array = colors + .slice(0, 16) .map((c) => { const hex = flock.getColorFromString(c); const color3 = flock.BABYLON.Color3.FromHexString(hex); @@ -1003,7 +1004,7 @@ export const flockMaterial = { console.log("Color array:", color3Array); } - shaderMaterial.setInt("colorCount", colors.length); + shaderMaterial.setInt("colorCount", Math.min(colors.length, 16)); shaderMaterial.setArray3("colors", color3Array); shaderMaterial.setFloat("alpha", 1.0); shaderMaterial.setVector2("minMax", new flock.BABYLON.Vector2(-1, 1)); // Will be updated when applied to mesh @@ -1088,6 +1089,8 @@ export const flockMaterial = { uniform sampler2D textureSampler; uniform vec3 lightColor; // Replaces white uniform vec3 greyTintColor; // Tints greys in proportion + uniform vec3 darkColor; // Replaces black (when colorCount >= 3) + uniform int colorCount; uniform float alpha; uniform float uScale; // Horizontal tiling uniform float vScale; // Vertical tiling @@ -1110,6 +1113,9 @@ export const flockMaterial = { if (brightness > 0.95 && colorDiff < 0.05) { // Replace near-white finalColor = lightColor; + } else if (colorCount >= 3 && brightness < 0.05 && colorDiff < 0.05) { + // Replace near-black (third color) + finalColor = darkColor; } else if (colorDiff < 0.05) { // Tint greys finalColor = brightness * greyTintColor; @@ -1137,6 +1143,8 @@ export const flockMaterial = { "textureSampler", "lightColor", "greyTintColor", + "darkColor", + "colorCount", "alpha", "uScale", "vScale", @@ -1185,6 +1193,19 @@ export const flockMaterial = { ), ); + const colorDark = colors.length >= 3 + ? flock.hexToRgb(flock.getColorFromString(colors[2])) + : { r: 0, g: 0, b: 0 }; + shaderMaterial.setVector3( + "darkColor", + new flock.BABYLON.Vector3( + colorDark.r / 255.0, + colorDark.g / 255.0, + colorDark.b / 255.0, + ), + ); + shaderMaterial.setInt("colorCount", colors.length); + shaderMaterial.setFloat("alpha", 1.0); return shaderMaterial; @@ -1335,7 +1356,7 @@ export const flockMaterial = { if (cacheKey && flock.materialCache[cacheKey]) { delete flock.materialCache[cacheKey]; } - oldMat.dispose(true, true); + oldMat.dispose(false, true); } } }, @@ -1442,8 +1463,12 @@ export const flockMaterial = { const isMaterialDescriptor = (v) => typeof v === "object" && v !== null && !Array.isArray(v); - const getRawColor = (v) => - isMaterialDescriptor(v) ? v.color || v.baseColor : v; + const getRawColor = (v) => { + const raw = isMaterialDescriptor(v) ? v.color || v.baseColor : v; + // A single-element colour list produces ["#rrggbb"]; normalise to a string + // so that downstream callers like getColorFromString receive a plain string. + return Array.isArray(raw) && raw.length === 1 ? raw[0] : raw; + }; const getTexName = (v) => isMaterialDescriptor(v) diff --git a/api/scene.js b/api/scene.js index 45b20c2d1..f5097120c 100644 --- a/api/scene.js +++ b/api/scene.js @@ -188,10 +188,6 @@ export const flockScene = { } dt.update(false); - const tex = new flock.BABYLON.Texture(null, flock.scene); - tex._texture = dt.getInternalTexture(); - tex.wrapU = flock.BABYLON.Texture.CLAMP_ADDRESSMODE; - tex.wrapV = flock.BABYLON.Texture.CLAMP_ADDRESSMODE; return dt; }, createMap(image, material) { @@ -202,6 +198,7 @@ export const flockScene = { const applyMaterialToGround = (mesh, mat) => { if (Array.isArray(mat) && mat.length === 1) mat = mat[0]; if (Array.isArray(mat) && mat.length >= 2) { + const oldMat = mesh.material; const standardMat = new flock.BABYLON.StandardMaterial( "mapGradientMat", flock.scene, @@ -216,9 +213,40 @@ export const flockScene = { flock.BABYLON.Texture.CLAMP_ADDRESSMODE; standardMat.diffuseTexture.wrapV = flock.BABYLON.Texture.CLAMP_ADDRESSMODE; - flock.setMaterialWithCleanup(mesh, standardMat); + mesh.material = standardMat; + if (oldMat && oldMat.name === "mapGradientMat") { + oldMat.dispose(false, true); + } } else { - flock.setMaterialWithCleanup(mesh, material); + // Update an existing GradientMaterial in-place to avoid shader recompilation. + const colors = + mat && typeof mat === "object" && Array.isArray(mat.color) + ? mat.color + : null; + if ( + colors?.length >= 2 && + (mat.materialName === "none.png" || !mat.materialName) && + mesh.material instanceof flock.GradientMaterial + ) { + const existingMat = mesh.material; + existingMat.bottomColor = flock.BABYLON.Color3.FromHexString( + flock.getColorFromString(colors[0]), + ); + existingMat.topColor = flock.BABYLON.Color3.FromHexString( + flock.getColorFromString(colors[1]), + ); + existingMat.alpha = parseFloat(mat.alpha ?? 1); + const oldKey = existingMat.metadata?.cacheKey; + if (oldKey) delete flock.materialCache[oldKey]; + const alphaKey = parseFloat(mat.alpha ?? 1).toFixed(2); + const newKey = + `mat_${colors.join("-")}_${alphaKey}_${mat.materialName ?? "none.png"}_noglow`.toLowerCase(); + existingMat.name = newKey; + if (existingMat.metadata) existingMat.metadata.cacheKey = newKey; + flock.materialCache[newKey] = existingMat; + } else { + flock.setMaterialWithCleanup(mesh, material); + } } }; diff --git a/ui/blockmesh.js b/ui/blockmesh.js index 34174272d..d60d5d0be 100644 --- a/ui/blockmesh.js +++ b/ui/blockmesh.js @@ -777,25 +777,39 @@ function updateMapFromBlock(mesh, block, changeEvent) { if (!materialBlock) return; - const { textureSet, alpha } = extractMaterialInfo(materialBlock); - let read = readColourFromInputOrShadow(materialBlock, "BASE_COLOR"); - - if (read.value == null && !block.__mapRetry) { - block.__mapRetry = true; - requestAnimationFrame(() => { - block.__mapRetry = false; - updateMapFromBlock(mesh, block, changeEvent); - }); + // A raw colour/list block may be connected directly to MATERIAL (not via a + // material block), so dispatch on block type and pass the raw value straight + // to createMap to match the generated-JS code path. + const isMaterialBlock = materialBlock.type === "material"; + let read, mapArg; + if (isMaterialBlock) { + const { textureSet, alpha } = extractMaterialInfo(materialBlock); + read = readColourFromInputOrShadow(materialBlock, "BASE_COLOR"); + const materialName = + !textureSet || textureSet === "NONE" ? "none.png" : textureSet; + mapArg = { color: read.value, materialName, alpha }; + } else { + read = readColourValue(materialBlock); + mapArg = read.value; + } + + const colorIsEmpty = + read.value == null || + (Array.isArray(read.value) && read.value.length === 0); + if (colorIsEmpty) { + // Retry once — mutator operations briefly leave the colour list empty. + // If still empty after the retry, bail silently to avoid an infinite loop. + const wasRetrying = block.__mapRetry; + block.__mapRetry = false; + if (!wasRetrying) { + block.__mapRetry = true; + requestAnimationFrame(() => updateMapFromBlock(mesh, block, changeEvent)); + } return; } + block.__mapRetry = false; - const materialOptions = { - color: read.value, - materialName: textureSet, - alpha, - }; - - flock.createMap(mapName, materialOptions); + flock.createMap(mapName, mapArg); } function resolveColorAndMaterialForBlock(block) {